* test(#3585): repo-wide guard for unguarded .planning/ git add Replaces the two-file #1783 scan, which required .planning/ on the git add line and so was structurally blind to fast.md's `git add -A` and to new-milestone.md (never scanned). Extracts the shell tokenizer, comment-position rule and gsd-scan-ignore marker from the #2269 guard into tests/helpers/shipped-command-scan.cjs so both guards consume one implementation. Commit-specific logic stays in commit-files-pathspec.test.cjs; every pre-existing test there passes unedited. Fails RED on five sites: fast.md:58, new-milestone.md:262, spec-phase.md:480, eval-review.md:148, ai-integration-phase.md:263. The last three carry a markdown prose conditional outside the bash block it claims to guard. * fix(#3585): close raw-git bypasses of the commit_docs gate Five shipped workflow steps staged .planning/ with raw git. Two had no check at all; three had a markdown prose conditional sitting outside the bash block it claimed to guard, so the block ran unconditionally. spec-phase, eval-review and ai-integration-phase now route through the gsd_run query commit seam, which performs the commit_docs and gitignore checks internally and returns a skipped envelope -- this deletes the raw git pair rather than wrapping it. new-milestone stages directories for a later commit and cannot use the seam, so it takes the executable guard form, fail-open on a tooling error. fast writes no planning artifacts and has no gsd_run in scope at that point, so it excludes .planning via pathspec instead of reading config. Guard now reports 0 offenders. * test(#3585): pin skipped_gitignored to production behavior COMMIT_REASON was a test-local frozen enum joined to production only by a hand-maintained keep-in-sync comment -- the Generative Fix Divergence class, whose required remedy is a parity assertion. B1-B3 already pinned SKIPPED_COMMIT_DOCS_FALSE. SKIPPED_GITIGNORED was pinned by nothing: production could rename it and every test still passed. G1-G3 drive the gitignore auto-detect path and assert the canonical reason. The fixture must OMIT .planning/config.json entirely -- with config.json present the loader resolves commit_docs to false first and cmdCommit returns skipped_commit_docs_false, never reaching its own isGitIgnored branch. * docs(#3585): document the planning commit gate and its guard CONTEXT.md had zero commit_docs entries. Adds a Planning Commit Gate glossary entry covering the resolution chain, the typed skip envelope, the measured ordering of the two reason codes, and why the gate is enforceable only as a text guard. CONTRIBUTING.md gains the contributor rule for the new guard, with the prose-is-not-a-guard example that caused three of the five defects. * fix(#3585): address review findings in the planning-add guard Spec review (blocker): fast.md excluded .planning unconditionally, changing behavior for commit_docs=true users and violating epic AC4. Now gated -- the launcher preamble was MOVED from log_to_state into the commit block rather than copied, so gsd_run is in scope for +4 lines instead of +4KB, and the else branch is byte-identical to the previous git add -A. Security review (major): git -C <dir> add was a false negative because the flag-skip loop never modelled flags that consume a separate value. Fixed for -C/-c/--git-dir/--work-tree/--namespace. The fail-closed rule now also covers $(...) substitution args and --pathspec-from-file, which were opaque in the same way $VAR is. git commit -a/-am is now classified as reaching, since it stages every tracked modification. Self-review: isSkippable treated any NAME= token as a skippable prefix, so V=$(git add -A) escaped -- the exact divergence the shared-helper extraction existed to prevent. Adopted the sibling predicate verbatim. eval, xargs, one-line function bodies and line-continuation remain blind and are now enumerated as declared limits in the guard docblock and CONTRIBUTING. The ifDepth clamp is defensive only: a 200k-case differential fuzz found no reproducing input, so its test is labeled a pin, not a failing-first test. * test(#3585): acknowledge emitted growth in three workflow files emitted-attribution has two arms: hash attribution AND per-file growth. The growth arm needs an acknowledgment even when every moved byte is attributable to the diff, which is why the first remote run went red on it. fast.md +417: the launcher preamble moved into the commit block so gsd_run is in scope for the commit_docs guard, plus the guard itself. new-milestone.md +281: the executable guard plus one line recording that the unstaged archive move is deliberate. spec-phase.md +21: reworded prose describing the skipped envelope. eval-review.md and ai-integration-phase.md shrank; no entry needed. * test(#3585): drop duplicate spec-phase ack, shrink its prose instead The base already acknowledges spec-phase.md (from #2733), and two ack sources may never name the same path. But a base-side ack is SPENT -- it cannot clear new growth -- so the two gates were in direct conflict: attribution wanted an ack, the ack lint forbade one. Resolved by removing the growth rather than the conflict. spec-phase.md's +21 was purely a prose reword; rewritten shorter, the file now shrinks 36 bytes against base and needs no acknowledgment at all. fast.md and new-milestone.md have no base ack and keep theirs. * chore(#3585): backfill changeset pr number to 3590 --------- Co-authored-by: sim <sim@local>
344 lines
18 KiB
JavaScript
344 lines
18 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Extracted from tests/commit-files-pathspec.test.cjs (#2269) so the #3585
|
||
* commit_docs guard consumes ONE shell tokenizer rather than hand-rolling a
|
||
* second. Only the primitives BOTH guards need live here — the shell
|
||
* tokenizer, the bare-command-name extraction, the -c payload search, and
|
||
* the `gsd-scan-ignore:` declaration machinery.
|
||
*
|
||
* Commit-specific logic stays in the test file and is deliberately NOT
|
||
* moved here: `isCommitInvocation`, `hasScopedFiles`, `SYNOPSIS_TOKEN_RE`,
|
||
* `METAVAR_REDIR_RE`, `segmentInvocations`, `codeSpans`, and
|
||
* `invocationCandidates` all encode assumptions specific to the commit
|
||
* scan and do not generalize to other guards.
|
||
*/
|
||
|
||
// 443 live substitution sites are `VAR=$(gsd_run …)`. That is the majority
|
||
// and it is the wrong generalisation — the remaining six are neither, and
|
||
// they are live today:
|
||
// for REVIEW_FLAG in $(gsd_run review-lane flags) (×3)
|
||
// ${PLAN_PRE_HOOKS_JSON:-$(gsd_run loop render-hooks …)} (×3)
|
||
// A parameter-expansion default glues just as hard as an assignment, and so
|
||
// does ordinary text (`pre$(…)`) and an indexed assignment (`A[0]=$(…)`).
|
||
// Enumerating prefixes means a new one every time the shell is used
|
||
// idiomatically, and each miss is silent. So the rule is positional instead:
|
||
// strip everything up to and including the LAST substitution opener in the
|
||
// token. Whatever preceded it was, by construction, not the command.
|
||
//
|
||
// ARITHMETIC IS EXCLUDED BY THE SAME MECHANISM, not by a special case.
|
||
// `$((x))` leaves a `(` in front of the name after the strip, and a leading
|
||
// `(` is not part of any binary name — so it is refused. That mirrors the
|
||
// shell, which also requires `$( (` with a space before it will read a nested
|
||
// subshell rather than an arithmetic expansion.
|
||
//
|
||
// The ARRAY LITERAL stays invisible for free: `arr=(a b c)` contains no `$(`
|
||
// at all, so neither alternative fires and the token is left whole. It is
|
||
// pinned as a zero-candidate case regardless, because that is a property of
|
||
// this regex rather than of the grammar and the next edit could lose it.
|
||
// THE STRIP IS REFUSED ON A TOKEN THAT CARRIED A QUOTE, because quoting is
|
||
// exactly what decides whether an opener is syntax or data. `printf %s
|
||
// '$(gsd_run' query commit fixup` runs no gsd command — the opener is
|
||
// single-quoted literal text — but quote removal leaves the token `$(gsd_run`
|
||
// and a strip would then read it as a binary, fabricating an invocation out
|
||
// of a string. The tokenizer already knows; it just was not saying.
|
||
//
|
||
// RESIDUAL, named rather than implied: this closes the strip's exposure, not
|
||
// the tokenizer's quote-blindness in general. `printf %s 'gsd_run' query
|
||
// commit fixup` still reads as an invocation, because no markup is stripped
|
||
// there — the token's value simply IS the binary name. That predates this
|
||
// rule and is unchanged by it; closing it needs quote provenance carried
|
||
// through the command-shape test, not just through the strip. The direction
|
||
// is a visible false positive with a declared remedy, not a silent miss.
|
||
const bareCommandName = (t) => {
|
||
const mask = t.qmask || '0'.repeat(t.value.length);
|
||
// The LAST opener whose own two characters were unquoted. Per-character,
|
||
// not per-token: a quote elsewhere in the word protects only itself, and
|
||
// `echo "pre"$(gsd_run …)` really does run.
|
||
for (let i = t.value.length - 2; i >= 0; i -= 1) {
|
||
if (t.value[i] === '$' && t.value[i + 1] === '(' && mask[i] === '0' && mask[i + 1] === '0') {
|
||
return t.value.slice(i + 2);
|
||
}
|
||
}
|
||
let j = 0;
|
||
while (j < t.value.length && t.value[j] === '(' && mask[j] === '0') j += 1;
|
||
return t.value.slice(j);
|
||
};
|
||
|
||
// Shell-like word splitting. Honours BOTH quote characters (the previous
|
||
// parity walk counted only `"`, so a single-quoted message was transparent
|
||
// to it in both directions), backslash escapes, and unquoted control
|
||
// operators — which become their own tokens, so an operator glued to its
|
||
// neighbour (`"a"&&gsd_run`) still separates. Quoted operators and quoted
|
||
// whitespace stay literal, which is what keeps a command substitution inside
|
||
// a message ("$(gsd-tools query phase-list)") from reading as a second
|
||
// invocation, with no separator lookahead needed.
|
||
//
|
||
// Beyond the operators, the shell consumes THREE more things before argv
|
||
// exists, and each one previously leaked into the token list as a value —
|
||
// so `--files >/dev/null 2>&1 || true` (a live tail shape in
|
||
// gsd-core/references/execute-phase-requirement-revert.md) scored as scoped
|
||
// while the runtime saw files=[] and took the blanket-.planning/ default:
|
||
// - a single `&` is a control operator like `;` (`&&` is checked first);
|
||
// - an unquoted `#` at the START of a word begins a comment and ends the
|
||
// command (mid-word `#` stays literal, as in the shell);
|
||
// - redirections (`> x`, `>> x`, `2>&1`, `< x`, with or without a space
|
||
// before the target) are consumed as `redir: true` tokens, along with a
|
||
// glued all-digit IO number, and consumers exclude them from argv.
|
||
// Variable tokens ($VAR / ${VAR}) stay ordinary values DELIBERATELY: whether
|
||
// one expands to something is unknowable statically, and the three original
|
||
// #2269 fix sites all pass `--files "${PHASE_DIR}/…"` — flagging `$`-tokens
|
||
// would false-flag every one of them.
|
||
//
|
||
// Never throws, and an unterminated quote simply runs to end of input: these
|
||
// inputs are substrings sliced out of prose, so a torn quote is expected
|
||
// rather than exceptional. Tokens carry offsets so callers can slice the
|
||
// ORIGINAL text and report a real excerpt instead of a re-joined echo.
|
||
//
|
||
// Operators are marked STRUCTURALLY (`op: true`), never re-identified by
|
||
// comparing a token's text against the operator set. That distinction is the
|
||
// whole point of tokenizing: `commit '|' --files a.md` produces a token
|
||
// whose VALUE is `|` and which is ordinary message text, and a consumer that
|
||
// matched on value would split there and score a scoped invocation as
|
||
// unscoped — the identify-structure-by-text mistake this rewrite exists to
|
||
// remove, reintroduced one layer up. (Caught by the delimiter-generating
|
||
// property below, on its first run.)
|
||
const tokenize = (str) => {
|
||
const tokens = [];
|
||
let cur = '';
|
||
let started = false;
|
||
let start = 0;
|
||
let quote = null;
|
||
// A PER-CHARACTER quote mask, parallel to `cur`: '1' where the character was
|
||
// quoted or backslash-escaped (i.e. protected, therefore data), '0' where it
|
||
// was bare syntax. A per-TOKEN boolean is the obvious shape and it is wrong
|
||
// in the unaffordable direction — `echo "pre"$(gsd_run …)` executes, and a
|
||
// token-wide flag suppresses the strip on it, turning a false positive into
|
||
// a silent false negative.
|
||
let curMask = '';
|
||
const add = (text, protectedChar) => {
|
||
cur += text;
|
||
curMask += (protectedChar ? '1' : '0').repeat(text.length);
|
||
};
|
||
const begin = (i) => {
|
||
if (!started) { started = true; start = i; }
|
||
};
|
||
const flush = (end) => {
|
||
if (started) tokens.push({ value: cur, start, end, qmask: curMask });
|
||
cur = '';
|
||
curMask = '';
|
||
started = false;
|
||
};
|
||
for (let i = 0; i < str.length; i += 1) {
|
||
const ch = str[i];
|
||
if (quote) {
|
||
if (ch === '\\' && quote === '"' && i + 1 < str.length) { add(str[i + 1], true); i += 1; continue; }
|
||
if (ch === quote) { quote = null; continue; }
|
||
add(ch, true);
|
||
continue;
|
||
}
|
||
if (ch === '\\' && i + 1 < str.length) { begin(i); add(str[i + 1], true); i += 1; continue; }
|
||
if (ch === '"' || ch === "'") { begin(i); quote = ch; continue; }
|
||
if (/\s/.test(ch)) { flush(i); continue; }
|
||
// An unquoted # at the start of a word ends the command line — the rest
|
||
// is comment and never reaches argv. Mid-word (`PR#42`) it is literal.
|
||
if (ch === '#' && !started) { flush(i); break; }
|
||
// Redirections: the operator and its target are consumed by the shell,
|
||
// not passed as arguments. A glued all-digit word is an IO number
|
||
// (`2>&1`) and belongs to the redirection, not to argv.
|
||
if (ch === '>' || ch === '<') {
|
||
if (started && /^[0-9]+$/.test(cur)) { cur = ''; curMask = ''; started = false; } else { flush(i); }
|
||
let j = i + 1;
|
||
if (str[j] === ch) j += 1; // >> / <<
|
||
if (str[j] === '&') j += 1; // >& (2>&1)
|
||
while (j < str.length && /[ \t]/.test(str[j])) j += 1; // gap before target
|
||
while (j < str.length && !/[\s;|&<>]/.test(str[j])) j += 1; // the target word
|
||
tokens.push({ value: str.slice(i, j), start: i, end: j, redir: true });
|
||
i = j - 1;
|
||
continue;
|
||
}
|
||
const pair = str.slice(i, i + 2);
|
||
if (pair === '&&' || pair === '||') { flush(i); tokens.push({ value: pair, start: i, end: i + 2, op: true }); i += 1; continue; }
|
||
if (ch === ';' || ch === '|' || ch === '&') { flush(i); tokens.push({ value: ch, start: i, end: i + 1, op: true }); continue; }
|
||
begin(i);
|
||
add(ch, false);
|
||
}
|
||
flush(str.length);
|
||
return tokens;
|
||
};
|
||
|
||
// The comment portion of a line — the same rule tokenize() applies, so the
|
||
// two cannot disagree about where the command ends: an unquoted `#` at word
|
||
// start begins a comment, and a `#` inside quotes or mid-word is literal
|
||
// (ship.md commits with `PR #${PR_NUMBER}` in the message).
|
||
// It tracks WORD START the way tokenize does, not "preceded by whitespace".
|
||
// Those differ, and the difference is exploitable: in `commit docs:\ # x` the
|
||
// backslash escapes the space, so the shell keeps `docs: #` as ONE word and
|
||
// `#` is literal — while a preceded-by-whitespace test reads it as a comment.
|
||
// The scan would then exempt a line the runtime still executes unscoped.
|
||
const commentPortion = (line) => {
|
||
let quote = null;
|
||
let started = false;
|
||
for (let i = 0; i < line.length; i += 1) {
|
||
const ch = line[i];
|
||
if (quote) {
|
||
if (ch === '\\' && quote === '"' && i + 1 < line.length) { i += 1; continue; }
|
||
if (ch === quote) quote = null;
|
||
continue;
|
||
}
|
||
if (ch === '\\' && i + 1 < line.length) { started = true; i += 1; continue; }
|
||
if (ch === '"' || ch === "'") { started = true; quote = ch; continue; }
|
||
if (/\s/.test(ch)) { started = false; continue; }
|
||
if (ch === '#' && !started) return line.slice(i);
|
||
if (ch === '>' || ch === '<' || ch === ';' || ch === '|' || ch === '&') { started = false; continue; }
|
||
started = true;
|
||
}
|
||
return '';
|
||
};
|
||
|
||
// The explicit opt-out for content that shows the defect ON PURPOSE. A
|
||
// wrong-example is otherwise indistinguishable from a regression — which is
|
||
// exactly why no property of the surrounding markup can stand in for the
|
||
// author's intent — so the author declares it, with a reason.
|
||
//
|
||
// IT IS ONLY A MARKER IN COMMENT POSITION. Matching the token anywhere on
|
||
// the line let a commit MESSAGE carry it — `commit "docs: explain
|
||
// gsd-scan-ignore: semantics"` — and silently exempt a real offender. A
|
||
// false-negative escape hatch is the one thing this file must not ship, and
|
||
// an exemption keyed to attacker-controlled-looking text is precisely that.
|
||
// AND IT MUST CARRY A TRACKING ISSUE. An exemption whose reason is free text
|
||
// has no expiry and no ledger — the `permanent-allow-test-rule` shape
|
||
// RULESET.TESTS.delete-bad-tests names. The repo already answered this for
|
||
// its sibling convention under ADR-456 (see #2269), enforced by
|
||
// scripts/lint-allow-test-rule-refs. That lint walks tests/ only, so it
|
||
// cannot see a marker living in a .md file; rather than teach a second token
|
||
// to a script whose whole contract is the ESLint comment form, this scan
|
||
// enforces the same rule over its own roots — it already runs in CI on every
|
||
// shard.
|
||
//
|
||
// THE PREDICATE IS THAT LINT'S, MIRRORED RATHER THAN RESTATED. Its
|
||
// ISSUE_REF_RE is `/#\d+|https?:\/\//` — `http://` counts, and `#\d+` has no
|
||
// trailing boundary. A hand-written near-copy drifts from it in exactly those
|
||
// details, and a contributor who satisfies one marker and not the other has
|
||
// been given two conventions wearing one name. (Both details were found by
|
||
// review against prose here that claimed HTTPS-only. The regex was right and
|
||
// the sentence describing it was not, which is its own lesson.)
|
||
const ISSUE_REF_RE = /#\d+|https?:\/\//;
|
||
//
|
||
// The reason is EXTRACTED, not pattern-matched in one shot, so that a marker
|
||
// with no reason at all is still recognized as an ATTEMPT. A rejected
|
||
// declaration must not fail as a MYSTERY: left un-exempt and nothing more,
|
||
// its author is told their commit is unscoped — a true statement about a line
|
||
// they had already explained, which is the mangle-until-it-shuts-up loop the
|
||
// marker exists to prevent. `# gsd-scan-ignore:` with an empty reason is the
|
||
// likeliest way to get this wrong and so is the case that most needs the
|
||
// specific diagnosis; a `\S`-requiring pattern silently drops it.
|
||
const SCAN_IGNORE_REASON_RE = /gsd-scan-ignore:(.*)$/;
|
||
const declarationReason = (portion) => {
|
||
const m = SCAN_IGNORE_REASON_RE.exec(portion);
|
||
return m === null ? null : m[1];
|
||
};
|
||
// Two independent conditions, and the second is the structural one: the
|
||
// marker must be in comment position AND must not survive tokenization as an
|
||
// ARGUMENT. tokenize() drops everything from a real comment onward, so a
|
||
// genuine declaration leaves no token carrying the token; anything that does
|
||
// reached argv, which means the runtime would have executed it. That makes
|
||
// "authored argument text can declare the line exempt" impossible by
|
||
// construction rather than by getting commentPortion's edges exactly right —
|
||
// and commentPortion has edges (redirection targets, escaped separators)
|
||
// where mirroring the tokenizer perfectly is fiddly and a miss is silent.
|
||
// The marker's reason, but only when the marker is in COMMENT position and
|
||
// survives neither as an argv token. Returns null when there is no attempt.
|
||
const declaredReasonFor = (line) => {
|
||
if (tokenize(line).some((t) => /gsd-scan-ignore:/.test(t.value))) return null;
|
||
return declarationReason(commentPortion(line));
|
||
};
|
||
const isDeclared = (line) => {
|
||
const reason = declaredReasonFor(line);
|
||
return reason !== null && ISSUE_REF_RE.test(reason);
|
||
};
|
||
// An ATTEMPTED declaration that carries no tracking reference — including one
|
||
// with no reason at all. Reported on its own terms rather than silently
|
||
// failing to exempt: see the extraction note above.
|
||
const isUntrackedDeclaration = (line) => {
|
||
const reason = declaredReasonFor(line);
|
||
return reason !== null && !ISSUE_REF_RE.test(reason);
|
||
};
|
||
|
||
// A shell invoked with -c runs its next argument AS A COMMAND, so the
|
||
// invocation lives inside a quoted token and no amount of markup-stripping
|
||
// reaches it: `bash -c "gsd_run query commit fixup"` scored zero candidates
|
||
// while being perfectly executable. The recursion is keyed on the INVOKER,
|
||
// not on "a quoted token that parses as an invocation" — that wider rule
|
||
// would flag a commit MESSAGE quoting a command, a false positive against
|
||
// ordinary documentation. Here the outer command is bash, so a gsd commit
|
||
// message can never reach it.
|
||
// The invoker set, widened past the four spellings the first cut guessed at.
|
||
// `ash`, `csh`, `tcsh`, `fish` and `yash` all take -c and all run what
|
||
// follows; leaving them out is a silent false negative in a function whose
|
||
// entire job is reaching a command the tokenizer cannot see.
|
||
const SHELL_INVOKER_RE = /^(?:.*\/)?(?:ba|da|k|z|a|c|tc|fi|ya)?sh$/;
|
||
const shellDashCPayloads = (str) => {
|
||
const payloads = [];
|
||
let group = [];
|
||
const drain = () => {
|
||
if (!group.length) { return; }
|
||
// THE INVOKER MUST BE THE COMMAND, not merely present in the segment.
|
||
// Searching anywhere made `echo bash -c "gsd_run query commit fixup"` a
|
||
// candidate — `echo` prints the string, it does not run it — which is a
|
||
// false positive against prose that merely quotes a command line. Only an
|
||
// env assignment, a shell keyword, or a list/prompt marker may precede the
|
||
// command name. Those are not commands; `echo` is, which is exactly the
|
||
// line the search-anywhere version got wrong.
|
||
// Shell keywords, prompt/list markers, and the command MODIFIERS that
|
||
// pass straight through to the command after them. The modifiers are the
|
||
// mirror of the invoker-set gap: omitting them is a false negative in the
|
||
// same direction, since `time bash -c "…"` really does run the payload.
|
||
//
|
||
// RESIDUAL, named rather than half-solved: a modifier that takes its OWN
|
||
// flags (`sudo -u alice bash -c …`) still stops the search, because
|
||
// skipping arbitrary flag/value pairs here would mean modelling each
|
||
// modifier's option grammar. No live instance exists in the six roots;
|
||
// the failure is a missed candidate, and the declaration marker is not
|
||
// involved either way.
|
||
const NON_COMMAND_PREFIX = new Set([
|
||
'then', 'else', 'do', '$', '-', '*', '&&', '||',
|
||
'time', 'exec', 'nohup', 'env', 'command',
|
||
]);
|
||
// AN ASSIGNMENT IS A SKIPPABLE PREFIX ONLY WHEN IT IS NOT A SUBSTITUTION.
|
||
// `FOO=1 bash -c "…"` prefixes the command; `V=$(bash -c "…")` IS the
|
||
// command, and skipping it walks the search past the invoker onto `-c`,
|
||
// which is not one — so the payload is never reached. Found by sweeping
|
||
// the substitution-glues-to-the-binary class across every command-name
|
||
// test rather than only the one the finding named.
|
||
const skippable = (t) => t.redir
|
||
|| (/^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?=/.test(t.value) && !t.value.includes('$('))
|
||
|| NON_COMMAND_PREFIX.has(t.value);
|
||
const gi = group.findIndex((t) => !skippable(t));
|
||
if (gi !== -1 && SHELL_INVOKER_RE.test(bareCommandName(group[gi]))) {
|
||
const ci = group.findIndex((t, k) => k > gi && !t.redir && t.value === '-c');
|
||
const payload = ci !== -1 ? group.slice(ci + 1).find((t) => !t.redir) : undefined;
|
||
if (payload) payloads.push(payload.value);
|
||
}
|
||
group = [];
|
||
};
|
||
for (const t of tokenize(str)) {
|
||
if (t.op) { drain(); } else { group.push(t); }
|
||
}
|
||
drain();
|
||
return payloads;
|
||
};
|
||
|
||
module.exports = {
|
||
bareCommandName,
|
||
tokenize,
|
||
shellDashCPayloads,
|
||
SHELL_INVOKER_RE,
|
||
commentPortion,
|
||
ISSUE_REF_RE,
|
||
declarationReason,
|
||
declaredReasonFor,
|
||
isDeclared,
|
||
isUntrackedDeclaration,
|
||
};
|