Files
msd-core/tests/section-manifest.property.test.cjs
Tom Boucher ad3b9ec486 chore(#1671): fragmentize plan-phase.md and repair flag forwarding to the init bundle — Phase 6.2 (#3019)
* chore(#2993): fragmentize plan-phase.md onto the fragment model

Epic #1671 Phase 6.2. plan-phase.md is the largest workflow in the repo and
carried zero markers; it was deferred out of the Phase 3 pilot for two
reasons, both now dead. The 36-byte PRE_PHASE6 headroom was never the
blocker it looked like — fragmentizing is net-negative on host source, so
the trim is what creates the room. The --mvp interleaving was resolved by
measurement in #2992 and no sub-line mechanism is built.

- widen WHEN_VOCABULARY 14 -> 19 via a second coordinated ADR-1671
  amendment: flag:--ingest, flag:--prd, flag:--research-phase,
  flag:--reviews, state:chunked-mode
- state:chunked-mode is `--chunked` OR config workflow.plan_chunked, and
  that disjunction is resolved in the FACT, never in the grammar, so a
  compound condition never becomes an operator
- parse the new flags on the plan-phase route; extract six gated bodies to
  gsd-core/workflows/plan-phase/steps/ behind manifest-gated stubs
- prd-express-path.md was already extracted but read unconditionally; its
  wrapper is now gated, so the existing extraction finally pays off

plan-phase.md 94,483 -> 87,575 bytes (cap 94,519): headroom goes from 36
bytes to 6,944.

Also closes a surfaced docs gap: five real plan-phase flags (--chunked,
--skip-ui, --bounce, --skip-bounce, --granularity) were documented in
neither the argument-hint nor help. Making --chunked load-bearing without
fixing its siblings would leave the defect class half-open.

Refs #2993

* fix(#2993): forward flags to the init bundle so section gating actually fires

Blocker found by the correctness review, confirmed directly, and missed by
both the isolated reviewer and every test in this branch.

Neither workflow forwarded its flags to the init CLI:

  plan-phase.md:71    INIT=$(gsd_run query init.plan-phase "$PHASE" $GRAN_PARAM)
  execute-phase.md:84 INIT=$(gsd_run query init.execute-phase "${PHASE_ARG}")

So every flag: atom was permanently false in production and its section
permanently excluded. For plan-phase that made the PRD express path
UNREACHABLE — a regression, since it was an unconditional read before.
For execute-phase this is PRE-EXISTING: #2932 shipped `flag:--wave` gating
that has never once been true, so `--wave` silently dropped its own
wave-filtering guidance. Fixed here under the no-defer rule.

Why every test missed it: they drive the init CLI directly with flags,
which works. Production goes through the workflow's bash line, which did
not pass them — the exact "assert against the shape production uses" trap
this branch's own test matrix warns about.

- parse and forward --prd/--ingest/--research-phase/--reviews/--chunked
  (plan-phase) and --wave (execute-phase), using the anchored regex idiom
  the neighbouring GRAN_PARAM line already uses
- add a regression guard DERIVED FROM THE MANIFEST: for every flag:--X
  section, the owning workflow's init line must forward --X. It fails
  against the pre-fix files and covers any future atom, rather than
  spot-checking today's six.

Verified through the workflow shape, not the CLI shape: `3 --prd spec.md`
now yields ["prd-express-gate"] (was []), `2 --wave 2` yields
["partial-wave"] (was []).

Refs #2993

* test(#2993): acknowledge the execute-phase ripple and regenerate install-tree fixtures

Remote matrix was red with 46 unique failures, identical on both lanes.
Both causes are mechanical consequences of changing shipped workflow
content, and neither is visible to any local gate.

- emitted-attribution: execute-phase.md grew 163 bytes from the WAVE_PARAM
  forwarding fix and was unacknowledged, while the ack fragment named
  plan-phase.md, which SHRANK and therefore needed no ack at all — a stale
  entry is itself a failure. The reason now names the real ripple.
  The entry had to merge into the existing 2930 fragment: the ack linter
  does unconditional cross-fragment duplicate-key detection with no
  spent/live exception, so a second fragment declaring execute-phase.md
  collides even when the first is already merged and inert. Resolved per
  the linter's own guidance and that file's precedent of appending
  successive ripple reasons to one entry.
- golden-install-tree: tests/fixtures/install-tree/*.json are committed and
  deliberately excluded from the ADR-2719 attribution cutover, so they must
  be regenerated when shipped tree content changes. Regenerated after
  build:lib per the ordering landmine. 19 runtimes each gained exactly the
  six new plan-phase step files; zero paths removed, which is the absolute
  failure shape those fixtures exist to catch.

Refs #2993

* fix(#2993): restore the launcher preamble in an extracted step and follow moved content in its drift guards

Second red run: 26 unique failures, identical on both lanes, in two classes.

RUNTIME BUG (runtime-launcher-parity, 7 failures) — chunked-planning-mode.md
calls gsd_run but carried no canonical launcher preamble, which is what
DEFINES gsd_run(). On any non-Claude runtime that step would fail outright.
The preamble is now copied verbatim from the canonical source of truth,
gsd-core/workflows/_runtime-launcher.snippet.sh, and the fence dedented to
column 0 to match the prd-express-path.md sibling (a list-continuation
indent breaks the byte-equal preamble match). prd-express-path.md already
had a correct one. This is the same defect #2932 hit when it extracted
steps; the parity test caught a real bug, not a stale assertion.

DRIFT GUARDS (plan-phase-drift-guard, issue-2762-plan-reviews-chunked,
skill-frontmatter-contract) — these assert plan-phase.md contains content
this branch moved into step files. Retargeted at where the content now
lives, with the asserted property unchanged; the ALL-RUNTIMES label COUNT
test now reads host + every step file so the count is preserved across the
split rather than reduced. Each retargeted guard was verified to still fail
when its step file is stripped, so none was weakened into vacuity.

No emitted-drift ack was needed: currentSizes() enumerates
gsd-core/workflows/*.md non-recursively, so files under
plan-phase/steps/ are never in the size ratchet's scope.

Refs #2993

* chore(#2993): backfill changeset pr number to 3019

---------

Co-authored-by: sim <sim@local>
2026-08-03 09:38:58 -04:00

239 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Property-based tests for src/section-manifest.cts (compiled to
* gsd-core/bin/lib/section-manifest.cjs) — issue #2932 (epic #1671 Phase 5).
* Covers 50-test-matrix.md rows 25-28.
*
* Document-shaped generators (CONTRIBUTING.md "Fixture provenance #2371",
* mirroring tests/workflow-fragments.property.test.cjs): section lists are
* generated as arbitrary document-order id/when sequences — the SHAPE a
* real `parseWorkflowSections` output would have — never by round-tripping
* through `selectSections`/`WHEN_PREDICATES` itself. `when` values are drawn
* from the module's own frozen `WHEN_VOCABULARY` re-export (imported from
* `workflow-fragments.cjs`, the true source of truth) rather than a
* hardcoded local copy, so the generator can never silently desync from
* production (DEFECT.GENERATIVE-FIX).
*
* Deterministic per CONTRIBUTING.md: seed and numRuns are pinned by
* tests/helpers/fast-check-setup.cjs (seed 42, numRuns 200).
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fc = require('./helpers/fast-check-setup.cjs');
const { selectSections, REASON } = require('../gsd-core/bin/lib/section-manifest.cjs');
const { WHEN_VOCABULARY } = require('../gsd-core/bin/lib/workflow-fragments.cjs');
const WHEN_VALUES = [...WHEN_VOCABULARY];
// Object.prototype-shaped `when` values — added during review — prototype-
// chain fail-open found by isolated adversarial pass. A bracket lookup on a
// plain frozen object resolves these as inherited members instead of failing
// closed; the totality property below must cover them too.
const HOSTILE_WHEN_VALUES = [
'constructor',
'toString',
'valueOf',
'hasOwnProperty',
'__proto__',
'prototype',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
];
// ─── Document-shaped generators ────────────────────────────────────────────
const whenArb = fc.constantFrom(...WHEN_VALUES);
// Ids are made unique WITHIN one generated document by suffixing the array
// index at assembly time (below) rather than relying on the raw string
// generator for uniqueness — this keeps the shape arbitrary while still
// letting assertions key on "this exact id".
const idBaseArb = fc.stringMatching(/^[a-z][a-z0-9-]{0,8}$/);
/** A document-order list of `{id, when}` sections, id uniqueness enforced by index-suffixing. */
const sectionsArb = fc
.array(fc.tuple(idBaseArb, whenArb), { minLength: 0, maxLength: 15 })
.map((pairs) => pairs.map(([base, when], idx) => ({ id: `${base}-${idx}`, when })));
const phaseNumberArb = fc.oneof(
fc.constant(null),
fc.constant(''),
fc.stringMatching(/^[0-9]{1,2}$/),
fc.stringMatching(/^[0-9]{1,2}\.[0-9]{1,2}$/),
);
// Flag atoms drawn from the module's own frozen WHEN_VOCABULARY re-export
// (DEFECT.GENERATIVE-FIX — never a hardcoded local copy of flag names).
const FLAG_TOKENS = WHEN_VOCABULARY.filter((w) => w.startsWith('flag:--')).map((w) => w.slice('flag:'.length));
const flagsArb = fc.subarray(FLAG_TOKENS).map((tokens) => new Set(tokens));
const factsArb = fc.record({
flags: flagsArb,
phaseNumber: phaseNumberArb,
hasPriorPhases: fc.boolean(),
// #2992 review finding: the three newer state:* booleans (needsCodebaseMap,
// phaseMvpMode, worktreesEnabled) must also participate in the partition
// invariant, not just the original three W/D/P facts.
needsCodebaseMap: fc.boolean(),
phaseMvpMode: fc.boolean(),
worktreesEnabled: fc.boolean(),
// #2993 (epic #1671 Phase 6.2): chunkedMode is a shape addition to
// InvocationFacts — must also participate in the partition/totality
// properties below, not just the six pre-existing fact keys.
chunkedMode: fc.boolean(),
});
// ─── Row 25: exact partition ────────────────────────────────────────────────
describe('property: selection is always an exact partition', () => {
test('selectionIsAlwaysAnExactPartitionOfInputSections', () => {
fc.assert(
fc.property(sectionsArb, factsArb, (sections, facts) => {
const { included, excluded } = selectSections(sections, facts);
const allIds = sections.map((s) => s.id);
// Union recovers every id exactly once, intersection is empty.
const unionSorted = [...included, ...excluded].sort();
assert.deepEqual(unionSorted, [...allIds].sort());
const intersection = included.filter((id) => excluded.includes(id));
assert.deepEqual(intersection, []);
}),
);
});
});
// ─── Row 26: `always` sections included under every fact combination ──────
describe('property: always sections are included under every fact combination', () => {
test('alwaysSectionsAreIncludedUnderEveryFactCombination', () => {
fc.assert(
fc.property(sectionsArb, factsArb, (sections, facts) => {
const { included } = selectSections(sections, facts);
const alwaysIds = sections.filter((s) => s.when === 'always').map((s) => s.id);
for (const id of alwaysIds) {
assert.equal(included.includes(id), true, `expected always-section "${id}" to be included`);
}
}),
);
});
});
// ─── Row 27: totality — never throws for vocab-valid when × arbitrary facts ─
describe('property: never throws for vocabulary-valid when and arbitrary facts', () => {
test('neverThrowsForVocabularyValidWhenAndArbitraryFacts', () => {
fc.assert(
fc.property(sectionsArb, factsArb, (sections, facts) => {
assert.doesNotThrow(() => selectSections(sections, facts));
}),
);
});
test('neverThrowsWhenFactsAreMissingKeysEntirely', () => {
// Totality also over PARTIAL facts objects (row 19's property-level
// twin): dropping zero or more of the seven fact keys must never throw.
const factKeys = ['flags', 'phaseNumber', 'hasPriorPhases', 'needsCodebaseMap', 'phaseMvpMode', 'worktreesEnabled', 'chunkedMode'];
fc.assert(
fc.property(sectionsArb, factsArb, fc.subarray(factKeys), (sections, facts, keysToKeep) => {
const partialFacts = {};
for (const key of keysToKeep) partialFacts[key] = facts[key];
assert.doesNotThrow(() => selectSections(sections, partialFacts));
}),
);
});
});
// ─── Added during review: totality fails closed for prototype-shaped keys ──
// A hostile, Object.prototype-shaped `when` value injected anywhere in an
// otherwise vocab-valid document must always throw REASON.UNKNOWN_WHEN and
// must never appear in `included` — the prototype-chain fail-open the
// isolated adversarial pass found (constructor/toString/etc. resolving as
// truthy inherited members, or `__proto__` throwing an untyped error).
describe('property: prototype-shaped when values always fail closed', () => {
const hostileWhenArb = fc.constantFrom(...HOSTILE_WHEN_VALUES);
const hostileIdArb = fc.stringMatching(/^[a-z][a-z0-9-]{0,8}$/);
test('injectingAHostileWhenAnywhereAlwaysThrowsUnknownWhen', () => {
fc.assert(
fc.property(
sectionsArb,
hostileIdArb,
hostileWhenArb,
fc.nat(),
(sections, hostileIdBase, hostileWhen, rawIndex) => {
const hostileSection = { id: `hostile-${hostileIdBase}`, when: hostileWhen };
const insertAt = sections.length === 0 ? 0 : rawIndex % (sections.length + 1);
const withHostile = [...sections.slice(0, insertAt), hostileSection, ...sections.slice(insertAt)];
let caught;
try {
selectSections(withHostile, {});
} catch (err) {
caught = err;
}
assert.ok(caught instanceof TypeError, 'expected selectSections to throw for a hostile when value');
assert.equal(caught.reason, REASON.UNKNOWN_WHEN);
},
),
);
});
});
// ─── B18: flag monotonicity — more flags never yields fewer included ───────
describe('property: adding more flags never yields fewer included sections (#2992 row B18)', () => {
test('flagSupersetNeverShrinksIncluded', () => {
fc.assert(
fc.property(sectionsArb, flagsArb, fc.subarray(FLAG_TOKENS), factsArb, (sections, baseFlags, extraTokens, restFacts) => {
const supersetFlags = new Set([...baseFlags, ...extraTokens]);
const baseResult = selectSections(sections, { ...restFacts, flags: baseFlags });
const supersetResult = selectSections(sections, { ...restFacts, flags: supersetFlags });
for (const id of baseResult.included) {
assert.ok(
supersetResult.included.includes(id),
`id "${id}" included under a flags subset must remain included under a flags superset`,
);
}
}),
);
});
});
// ─── Row 28: order preservation ─────────────────────────────────────────────
describe('property: included ids preserve document order', () => {
test('includedIdsPreserveDocumentOrder', () => {
fc.assert(
fc.property(sectionsArb, factsArb, (sections, facts) => {
const { included, excluded } = selectSections(sections, facts);
const allIds = sections.map((s) => s.id);
// A subsequence check: the positions of `included` ids within
// `allIds`, taken in the order they appear in `included`, must be
// strictly increasing (never reordered relative to the input).
let cursor = -1;
for (const id of included) {
const pos = allIds.indexOf(id, cursor + 1);
assert.ok(pos > cursor, `id "${id}" out of document order in included[]`);
cursor = pos;
}
// Same subsequence guarantee for excluded[].
cursor = -1;
for (const id of excluded) {
const pos = allIds.indexOf(id, cursor + 1);
assert.ok(pos > cursor, `id "${id}" out of document order in excluded[]`);
cursor = pos;
}
}),
);
});
});