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:
Tom Boucher
2026-09-06 02:55:17 -04:00
committed by GitHub
parent 6adf3098ac
commit 7bb366e836
7 changed files with 708 additions and 12 deletions

View File

@@ -104,3 +104,52 @@ describe('parsePredicateFlags', () => {
assert.deepEqual(parsePredicateFlags([]), {});
});
});
// ─── #4130 follow-up: partitionPredicateArgs (flags + positionals, one parser) ─
/**
* `partitionPredicateArgs` is the single pass behind `parsePredicateFlags`:
* it returns BOTH the --flag value map AND the non-consumed positional tokens
* under the exact same skip/consume/last-wins semantics. `check
* decision-coverage-plan --context <path>` uses it so the flag and the
* positional surface share one parser with `check predicate` — the two
* parsers cannot diverge because there is only one.
*/
describe('partitionPredicateArgs (#4130 follow-up)', () => {
const { partitionPredicateArgs } = require('../gsd-core/bin/lib/check-command-router.cjs');
test('splits --flag value pairs from positionals', () => {
const { flags, positionals } = partitionPredicateArgs(
['check', 'decision-coverage-plan', '--context', '/tmp/CONTEXT.md', 'phases/01-init'],
);
assert.deepEqual(flags, { context: '/tmp/CONTEXT.md' });
assert.deepEqual(positionals, ['check', 'decision-coverage-plan', 'phases/01-init']);
});
test('parsePredicateFlags is exactly the flags half (one source of truth)', () => {
const vectors = [
['check', 'predicate', '--predicate', '{"kind":"x"}', '--phase-number', '03', '--raw'],
['--phase-number', '01', '--phase-number', '02'],
['--predicate', '--phase-number'],
[],
['--context'],
['a', '--context', 'b', '--context', 'c', 'd'],
];
for (const v of vectors) {
assert.deepEqual(partitionPredicateArgs(v).flags, parsePredicateFlags(v),
`flags half must equal parsePredicateFlags for ${JSON.stringify(v)}`);
}
});
test('value that starts with -- is not consumed: both stay flags, neither becomes positional', () => {
const { flags, positionals } = partitionPredicateArgs(['--context', '--other']);
assert.deepEqual(flags, {});
assert.deepEqual(positionals, ['--context', '--other']);
});
test('last write wins; flag values never leak into positionals', () => {
const { flags, positionals } = partitionPredicateArgs(['p1', '--context', 'a', 'p2', '--context', 'b', 'p3']);
assert.equal(flags.context, 'b');
assert.deepEqual(positionals, ['p1', 'p2', 'p3']);
});
});