fix(#4130): --context flag for check decision-coverage-plan + parseDecisions quadratic-backtracking hardening (#4374)
* test(#4130): failing-first regressions for --context flag + parseDecisions hardening Block A (flag): check decision-coverage-plan --context <path> must route identically to the positional form; flag wins over positional context; valueless --context falls through to the #2770 fail-closed caller error; verify keeps its positional surface (flag is plan-only). RED on base: the flag token lands in the args[2] phase slot (false uncovered) or the args[3] context slot (silent CONTEXT.md-missing skip). Block B (hardening): regex-lattice asserts pin the atomic-ID wrapper (?=(X))\1 and the em-dash first-separator narrowing [^*—–]*[—–] plus the no-adjacent-overlap property; a differential property compares the module against a frozen copy of the pre-hardening grammars (reference validated against the base build: 60k generated lines, 0 mismatches); 40k cliff shapes assert correct outcomes with no wall-time asserts (repo rule). A12: partitionPredicateArgs keeps one parser behind parsePredicateFlags. * fix(#4130): --context flag for check decision-coverage-plan + quadratic-backtracking hardening in parseDecisions (A) check decision-coverage-plan --context <path> — sibling convention (check predicate, #2008): --flag value pairs parsed by the new shared partitionPredicateArgs (parsePredicateFlags reimplemented as its flags half — one parser, cannot diverge), the flag winning over a same-purpose positional, positionals kept (no sibling deprecates them; the plan-phase workflow caller passes positionals), valueless --context falls through to the #2770 fail-closed caller error. Repair of the routing accident where --context landed in the args[2] phase slot (false uncovered) or the literal token in the args[3] context slot (silent green skip). (B) parseDecisions regex seam hardened, byte-identical on all legal inputs: the three bullet grammars consume the ID atomically via the (?=(X))\1 lookahead emulation (kills the tail/[^:*]* O(n^2) re-split, ~1.1s @ 40k), and the em-dash first separator narrows [^*]*[—–] to [^*—–]*[—–] (kills the dash-position O(n^2) retry, ~1.7s @ 40k). Group indices unchanged (handlers untouched). Pinned by regex-lattice tests, a differential fast-check property vs the frozen pre-hardening grammars, and 40k cliff/legal-shape outcome tests (no wall-time asserts per repo rule — no deterministic engine step counter exists in Node). * docs+test(#4130): document --context invocation; harden lattice test tooling - docs/CONFIGURATION.md Decision Coverage Gates: new 'Invoking the plan gate directly' block documenting both the positional and --context forms, flag precedence, and the valueless-flag fail-closed semantics (same place the gate's behavior is documented; sibling check predicate documents its flags the same way). - Two changeset fragments per the maintainer brief (Added: flag; Fixed: hardening), PR numbers to be backfilled. - tests/decisions.test.cjs review fixes: readRegExpTemplate template escaping (bare ')' SyntaxError), range-aware lattice checker with backreference skip and template unescape, honest A1 contract, lint escape warning. * fix(#4130): valueless --context fails closed per #2770; A8 isolates flag-vs-positional context Suite-caught fixes from the first verify run: - cmdDecisionCoveragePlan now refuses a flag-shaped token as the positional context path: a bare valueless --context stays a positional (sibling parser semantics, unchanged) but reading it as a PATH would turn a caller mistake into a silent 'CONTEXT.md missing' green skip — exactly what #2770's fail-closed law forbids. Now falls through to the missing-context-argument error, as documented. - A8 test compares decoy-positional+flag against flag-with-phase (phase held constant) so the row isolates WHICH context was read; the old form compared against a no-phase invocation that could never match. * chore(#4130): backfill PR number in changeset fragments (PR #4374) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -289,9 +289,34 @@ function loadDecisionExtraction(contextPath: string): { trackable: Decision[]; o
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `check decision-coverage-plan` — blocking plan-phase decision-coverage gate
|
||||
* (#2492, #1365 fail-loud, #2770 empty-arg fail-closed).
|
||||
*
|
||||
* Invocation (the context path may be supplied EITHER way; #4130 follow-up):
|
||||
* gsd_run check decision-coverage-plan <phase-dir> <context-path> (positional, the workflow caller's form)
|
||||
* gsd_run check decision-coverage-plan --context <path> [<phase-dir>]
|
||||
*
|
||||
* `--context <path>` follows the sibling flag convention (`check predicate`,
|
||||
* #2008): `--flag value` pairs parsed by the shared partitionPredicateArgs
|
||||
* pass, the flag WINNING over a same-purpose positional when both appear,
|
||||
* and a valueless `--context` counting as no context at all (it falls
|
||||
* through to the #2770 caller-error branch, not to the "CONTEXT.md missing"
|
||||
* green skip). The positional form keeps working unchanged — no sibling
|
||||
* check verb deprecates positionals and the plan-phase workflow passes them.
|
||||
*/
|
||||
function cmdDecisionCoveragePlan(projectDir: string, args: string[], raw: boolean): void {
|
||||
const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
|
||||
const contextArg = args[3];
|
||||
// args[0]='check', args[1]=subcommand — partition the REST so flag tokens
|
||||
// and their values never land in a positional slot.
|
||||
const { flags, positionals } = partitionPredicateArgs(args.slice(2));
|
||||
const phaseDir = positionals[0] ? resolvePath(positionals[0], projectDir) : '';
|
||||
// A VALUELESS `--context` stays a bare token in the positionals (sibling
|
||||
// parser semantics); it must not then be read as the context PATH — a
|
||||
// `--`-prefixed "path" is a caller mistake, and #2770's law says a missing
|
||||
// context argument fails CLOSED, never a silent "CONTEXT.md missing" green
|
||||
// skip. So only a non-flag positional may serve as the context.
|
||||
const positionalContext = positionals[1] && !positionals[1].startsWith('--') ? positionals[1] : '';
|
||||
const contextArg = flags['context'] ?? positionalContext ?? '';
|
||||
const contextPath = contextArg ? resolvePath(contextArg, projectDir) : '';
|
||||
|
||||
if (!gateEnabled(projectDir)) {
|
||||
@@ -1268,21 +1293,41 @@ function buildPredicateDeps() {
|
||||
};
|
||||
}
|
||||
|
||||
/** Parse `--flag value` pairs from an args array into a map (last write wins). */
|
||||
function parsePredicateFlags(args: string[]): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
/**
|
||||
* Split an args array into `--flag value` pairs and the leftover positional
|
||||
* tokens, in ONE pass, with the semantics `check predicate` established
|
||||
* (#2008): a `--flag` followed by a non-`--` token consumes it as the value
|
||||
* (last write wins); a `--flag` with no value stays a bare token and moves to
|
||||
* the positionals; everything else is positional. `parsePredicateFlags` is
|
||||
* the flags half of this same pass — there is exactly one parser, so the
|
||||
* flag-taking check verbs cannot drift apart (#4130 follow-up: `check
|
||||
* decision-coverage-plan --context <path>` shares it).
|
||||
*/
|
||||
function partitionPredicateArgs(args: string[]): { flags: Record<string, string>; positionals: string[] } {
|
||||
const flags: Record<string, string> = {};
|
||||
const positionals: string[] = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (typeof a !== 'string') continue;
|
||||
if (!a.startsWith('--')) continue;
|
||||
if (!a.startsWith('--')) {
|
||||
positionals.push(a);
|
||||
continue;
|
||||
}
|
||||
const key = a.slice(2);
|
||||
const next = args[i + 1];
|
||||
if (key.length > 0 && typeof next === 'string' && !next.startsWith('--')) {
|
||||
out[key] = next;
|
||||
flags[key] = next;
|
||||
i++;
|
||||
} else {
|
||||
positionals.push(a);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
return { flags, positionals };
|
||||
}
|
||||
|
||||
/** Parse `--flag value` pairs from an args array into a map (last write wins). */
|
||||
function parsePredicateFlags(args: string[]): Record<string, string> {
|
||||
return partitionPredicateArgs(args).flags;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1821,7 +1866,9 @@ function routeCheckCommand({ args, cwd, raw }: RouteCheckCommandOptions): void {
|
||||
// this for any gate whose `check` carries a `predicate` (instead of a `query`),
|
||||
// passing the predicate object as --predicate '<json>'. NOTE: unlike the
|
||||
// `check.query` subcommands above (which take positional phase args), this
|
||||
// subcommand parses --flag value pairs.
|
||||
// subcommand is flag-driven. `decision-coverage-plan` above now ALSO accepts
|
||||
// `--context <path>` (its positionals still work) — both share
|
||||
// partitionPredicateArgs, the one flag parser.
|
||||
cmdCheckPredicate(cwd, args, raw);
|
||||
return;
|
||||
}
|
||||
@@ -1850,6 +1897,7 @@ export = {
|
||||
cmdCheckPredicate,
|
||||
buildPredicateDeps,
|
||||
parsePredicateFlags,
|
||||
partitionPredicateArgs,
|
||||
// Fail-closed phase-scope reader for the api-coverage gate — exported for
|
||||
// in-process failure-injection tests (#2365 review).
|
||||
readPhaseScope,
|
||||
|
||||
Reference in New Issue
Block a user