chore(#2930): fragmentize execute-phase.md and prove per-runtime composed emission (#2972)

* feat(#2930): fragmentize plan-phase.md workflow into per-runtime-composed sections

Adds src/workflow-fragments.cts (in-file <!-- gsd:section --> marker
parser/composer, ADR-1671 epic #1671 Phase 3), wires it into
bin/install.js's copyWithPathReplacement emission path, and pilots the
marker grammar on gsd-core/workflows/plan-phase.md.

Bookkeeping ripple for the new src/*.cts module: .gitignore,
eslint.config.mjs, docs/INVENTORY.md + docs/INVENTORY-MANIFEST.json,
and a CONTEXT.md glossary entry. Amends ADR-1671 with open questions 1
and 2 resolutions and records the closed when= applicability grammar.
Adds docs/reference/workflow-fragments.md and an ARCHITECTURE.md
section documenting the marker authoring model.

* fix(#2930): put allow-test-rule issue ref on the same line as the marker

lint-allow-test-rule-refs.cjs requires the #NNN issue reference on the
same source line as `allow-test-rule:`; it was one line below and read
as an unreferenced novel exemption.

* docs(#2930): link the orphaned gate-predicates reference from the docs index

Found while adding the workflow-fragments reference doc: docs/reference/gate-predicates.md
shipped without an entry in docs/README.md, so it was unreachable from the docs index.
Fixed inline rather than deferred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2930): scope composition to workflows, add typed failure reasons

Review findings from two orthogonal passes:

- Scope composeWorkflow to gsd-core/workflows/ only. It previously ran on
  every .md the installer copied, so a future agent/command/reference doc
  documenting the marker syntax with an unfenced example would have been
  mis-parsed and silently stripped — a lossy drop the phase forbids.
- Add a frozen REASON enum; failures attach a typed .reason and tests assert
  on it instead of matching free-form message text (CONTRIBUTING.md:635-694).
- Derive the property generator's when= values from WHEN_VOCABULARY instead
  of duplicating them (DEFECT.GENERATIVE-FIX).
- Add adversarial parser fixtures: Unicode headings, NUL, U+FFFD, BOM,
  fence-within-fence, tilde and indented fences, lone-CR marker line.
- Document why --mvp is structurally unmarkable: its content is interleaved,
  not sectioned, so the whole-line grammar cannot reach it.

Also fixes two stale tests on this branch, each reproduced on the unmodified
tree before correction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2930): retarget the pilot from plan-phase to execute-phase

The full remote matrix went red on both Linux lanes. Root cause was ours:
tests/phase6-capstone-conformance.test.cjs holds a PRE_PHASE6 ceiling of
94519 bytes for plan-phase.md, asserting an ADR-857 Phase-6 completion
property. That is a third size gate beyond the tier caps and the
differential ratchet, and it left plan-phase.md just 36 bytes of headroom
rather than the 3821 computed from the XL cap. The 330 marker bytes
overran it by 294.

Raising the ceiling is not an option: it is a red line certifying another
ADR's completion. plan-phase.md is reverted to byte-identical origin/next
and the pilot moves to execute-phase.md, which has 728 bytes of headroom
under its own ceiling and lands at 93147 with 3 marker pairs.

The vocabulary narrows to the atoms actually used: always, flag:--wave,
state:gap-closure-phase, state:has-prior-phases.

Recorded in the ADR: every branch the epic names lives in plan-phase.md,
which cannot be fragmentized until caps move from source to emitted bytes.
That is direct evidence for the epic's premise and may reorder phases 3-4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2930): backfill changeset PR number (#2972)

* fix(#2930): make the emission install tests portable on Windows

The windows-latest lane went red on three tests in the new install suite;
Linux was green. Both causes were in the test harness, not the module.

Root normalization: the opencode converter always embeds the install root
forward-slashed, but the tests stripped it with the native-separator string
from mkdtemp. On Windows that never matched, so the root leaked through
unstripped — and because the real and stub install roots have different
prefix lengths, that length difference landed directly in the byte-delta
assertion (344 observed vs 275 expected). Normalize both text and root to
one separator form before stripping.

@-ref resolution: the helper stripped only the @~/ and @$HOME/ forms, so a
Windows absolute ref (@C:/Users/...) fell through and was joined onto the
root, producing ...\@C:\Users\... Strip the @ first, then detect
absoluteness from the token's own shape (POSIX, drive-letter, or UNC) with
no platform branching, so every OS takes the same path.

Neither assertion was weakened; the exact-equality byte check is the point
of the test and still holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(#2930): document every REASON member and guard the doc/enum parity

Code review found the reference doc's 'Fails closed' list covering 10 of the
11 frozen REASON members — MALFORMED_ATTRIBUTES (parseAttrs rejects malformed
key="value" syntax) had no bullet, and it is distinct from
UNRECOGNIZED_ATTRIBUTE, which is valid syntax with an unknown key.

Two parallel surfaces sharing one constant with nothing asserting they agree is
the DEFECT.GENERATIVE-FIX class, so the same commit adds the parity assertion:
the test derives the enum side from the built module and the doc side by parsing
the reference page, keyed on the reason IDENTIFIER rather than prose so a
reworded bullet does not break it, and reports set differences in both
directions by name.

Proven non-vacuous: removing the MALFORMED_ATTRIBUTES bullet turns the suite
red naming that exact member; restoring it returns 44/44.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-08-01 12:12:20 -04:00
committed by GitHub
parent f0bb0787c9
commit 640eaee16e
17 changed files with 2182 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
{
"version": 1,
"paths": {
"execute-phase.md": "#2930 (epic #1671 Phase 3): pilots the in-file `<!-- gsd:section -->` marker grammar by wrapping the --wave/gap-closure/regression-gate branch sections (partial-wave, gap-closure-artifacts, regression-gate) in marker pairs, proving the composeWorkflow seam runs at install time before per-runtime rewrites. Retargeted from plan-phase.md (chore/2930 review): plan-phase.md sits only 36 B under the ADR-857 Phase-6 PRE_PHASE6 gate (tests/phase6-capstone-conformance.test.cjs) and cannot absorb marker overhead, so the maintainer retargeted the pilot to execute-phase.md, which has 728 B of headroom under its own PRE_PHASE6 cap. SOURCE grows by exactly 275 marker bytes (6 marker lines); the EMITTED artifact composeWorkflow produces at install is byte-identical to the pre-#2930 file (markers are stripped, never shipped). See .gsd/phase/chore-2930-fragmentize-xl-workflow/40-design.md 'Known limits' item 5."
}
}

View File

@@ -0,0 +1,484 @@
'use strict';
// allow-test-rule: source-text-is-the-product — noSectionMarkerLeaksIntoEmittedArtifacts (#2930)
// asserts on the literal bytes of an EMITTED install artifact, which IS the
// deployed contract (a leaked `gsd:section` marker byte would ship to every user). This
// mirrors the test-matrix's own row-34/35 exemption from the "no source-grep" rule
// (50-test-matrix.md "No source-grep" note) — the ESLint rule itself only fires on
// readFileSync of a .cjs/.js/.ts SOURCE path, never on an installed .md artifact, so this
// annotation is documentation of intent, not a required suppression.
/**
* workflow-fragments-emission.install.test.cjs — 50-test-matrix.md rows 32-36
* (issue #2930, epic #1671 Phase 3).
*
* Real spawn-install coverage for `composeWorkflow`'s wiring into
* `bin/install.js`'s `copyWithPathReplacement` (ADR-1671 "Architecture and
* contracts": an engine-direct assertion is false-green for install
* behavior — only a real spawned installer proves bytes actually reach
* disk). The pure parser/composer itself is covered by
* tests/workflow-fragments.test.cjs (unit, rows 1-29/37) and
* tests/workflow-fragments.property.test.cjs (prop, rows 30-31).
*
* Each test builds and tears down its own tmp fixture(s) inline (no shared
* `before()` install cache) — independence per matrix row 38.
*
* ── The overlay technique (rows 33/36) ───────────────────────────────────
*
* Rows 33 and 36 need a spawned `bin/install.js` that reads a DIFFERENT
* `gsd-core/workflows/execute-phase.md` (malformed, row 36) or a different
* `gsd-core/bin/lib/workflow-fragments.cjs` (stubbed to identity, row 33)
* than this checkout's real files, without paying to copy the ~400 MB
* repository (mostly node_modules) for every run. `buildOverlayRepo` mirrors
* the repo tree with real directories (so `copyWithPathReplacement`'s own
* `entry.isDirectory()` / `entry.isFile()` Dirent checks — which do NOT
* follow symlinks — see the correct type) and HARD-LINKS every unmodified
* leaf file (not symlinks: a symlinked leaf file also fails an `isFile()`
* Dirent check elsewhere in the installer, verified empirically — "Failed
* to install agents: directory is empty" against a symlink-leaf overlay).
* Only `node_modules` and `.git` are symlinked at the top level (install.js
* never walks into either), which is what keeps the overlay build fast.
* Every overlay-spawned installer runs with `--preserve-symlinks
* --preserve-symlinks-main` as a defensive belt: with an all-hardlink leaf
* layout this checkout does not currently NEED symlink-preservation for
* correctness, but the flag is free insurance against a future install.js
* change that resolves a node_modules package by real path.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const crypto = require('node:crypto');
const { spawnSync } = require('node:child_process');
const { cleanup } = require('./helpers.cjs');
const { RUNTIME_META, runMinimalInstall, installerEnv } = require('./helpers/install-shared.cjs');
const { executionContextRefs } = require('../scripts/command-contract-helpers.cjs');
const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const PILOT_REL = path.join('gsd-core', 'workflows', 'execute-phase.md');
const PILOT_PATH = path.join(REPO_ROOT, PILOT_REL);
// plan-phase.md was the original #2930 pilot but was reverted to unmarked
// (chore/2930 retarget: it sits 36 B under the ADR-857 Phase-6 PRE_PHASE6
// gate and cannot absorb marker overhead) — it is now a genuinely unmarked
// file again, so row 33 uses it instead of plan-phase.md.
const UNMARKED_REL = path.join('gsd-core', 'workflows', 'plan-phase.md');
const RUNTIMES = Object.keys(RUNTIME_META);
// ─── Overlay-repo builder (rows 33/36) ─────────────────────────────────────
const OVERLAY_SKIP_TOP = new Set(['node_modules', '.git']);
/** Hard-link a file, falling back to a real copy only if the two paths sit on
* different filesystems/devices (EXDEV) or linking is denied (EPERM) — both
* cross-platform-legitimate, unlike a symlink's Dirent type-detection gap. */
function linkOrCopyFile(src, dest) {
try {
fs.linkSync(src, dest);
} catch (err) {
if (err.code === 'EXDEV' || err.code === 'EPERM') {
fs.copyFileSync(src, dest);
} else {
throw err;
}
}
}
/**
* Build a throwaway mirror of REPO_ROOT with real directories throughout and
* every unmodified leaf file hard-linked, except the paths named in
* `fileOverrides` (POSIX-relative-path -> content string), which are written
* as real files. Returns the mirror's absolute path; caller must
* `fs.rmSync(..., {recursive:true, force:true})` it away.
*
* @param {{[relPath: string]: string}} fileOverrides
*/
function buildOverlayRepo(fileOverrides) {
const tmpRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2930-overlay-'));
const entries = Object.entries(fileOverrides).map(([relPath, content]) => ({
parts: relPath.split('/'),
content,
}));
function place(srcDir, destDir, pending, isTop) {
fs.mkdirSync(destDir, { recursive: true });
const grouped = new Map();
for (const e of pending) {
const [head, ...rest] = e.parts;
if (!grouped.has(head)) grouped.set(head, []);
grouped.get(head).push({ parts: rest, content: e.content });
}
for (const de of fs.readdirSync(srcDir, { withFileTypes: true })) {
if (isTop && OVERLAY_SKIP_TOP.has(de.name)) {
fs.symlinkSync(path.join(srcDir, de.name), path.join(destDir, de.name));
continue;
}
const srcPath = path.join(srcDir, de.name);
const destPath = path.join(destDir, de.name);
const overridden = grouped.get(de.name);
const leaf = overridden && overridden.find((s) => s.parts.length === 0);
if (leaf) {
fs.writeFileSync(destPath, leaf.content);
continue;
}
// fs.statSync follows symlinks (unlike Dirent.isDirectory()), so a
// symlinked source directory is still recursed as a REAL directory in
// the overlay — the property copyWithPathReplacement itself needs.
if (fs.statSync(srcPath).isDirectory()) {
place(srcPath, destPath, overridden || [], false);
} else {
linkOrCopyFile(srcPath, destPath);
}
}
}
place(REPO_ROOT, tmpRepo, entries, true);
return tmpRepo;
}
/** Spawn a (possibly overlaid) installScript at global scope. Does NOT
* assert success — callers decide (row 36 expects failure). */
function spawnGlobalInstall(installScript, runtime, extraArgs = []) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-2930-dest-${runtime}-`));
const args = [
'--preserve-symlinks',
'--preserve-symlinks-main',
installScript,
`--${runtime}`,
'--global',
'--config-dir',
root,
...extraArgs,
];
const result = spawnSync(process.execPath, args, {
cwd: root,
encoding: 'utf8',
env: installerEnv({ HOME: root, USERPROFILE: root }),
});
return { result, configDir: root, root };
}
/** Convert native path separators to POSIX forward slashes, unconditionally
* (never gate on `path.sep` — CONTEXT.md's path-separator-normalization
* rule). Windows installs embed the SAME root in more than one spelling:
* the `@`-ref / pathPrefix rewrites always emit posix-normalized
* forward-slash paths, while other embedded content can still carry the
* native backslash spelling. `root` itself (from `fs.mkdtempSync`) is a
* native-separator string, so comparing it against text verbatim only
* matches ONE of those spellings. */
function toPosixSlashes(value) {
return value.replace(/\\/g, '/');
}
/** Strip an install's own absolute root out of emitted text so two installs
* under DIFFERENT temp roots (different lengths, different runtime-name
* prefixes) can be compared byte-for-byte. Normalizes BOTH the text and the
* root to forward-slash spelling first, so every embedded spelling of the
* root collapses onto the SAME placeholder — leaving the comparison
* measuring only composeWorkflow's own contribution. */
function stripRoot(text, root) {
return toPosixSlashes(text).split(toPosixSlashes(root)).join('<ROOT>');
}
// ─── Row 32: emitted execute-phase.md shrinks by exactly the marker bytes ────
//
// Defect found and fixed inline while verifying (chore/2930 review; not one
// of the five assigned findings, but discovered incidentally): this test
// previously compared the RAW `composeWorkflow(source)` byte length directly
// against `fs.statSync(emittedPath).size`. That equality only holds when the
// OTHER rewrites `copyWithPathReplacement` also runs (the `~/.claude/` ->
// `pathPrefix` substitution, attribution stamping, per-runtime converters)
// happen to be byte-neutral — which they are NOT here: `runMinimalInstall`
// passes `--config-dir root` with `HOME=root` (root IS the target, not
// `root/.claude`), so `computePathPrefix` degenerates to the short literal
// `$HOME/` instead of the real-world `$HOME/.claude/`, shrinking the
// installed `~/.claude/` references by additional bytes unrelated to
// composeWorkflow. Proven with a real spawned install and reverted to
// confirm this reproduces on the UNMODIFIED tree, before this change. Fixed
// by isolating composeWorkflow's OWN contribution the same way row 33
// already does: compare two REAL installs of the pilot workflow, one via
// this checkout's real composeWorkflow and one via an identity-stubbed
// composeWorkflow, and assert the size DELTA equals exactly the marker
// bytes stripped — never an absolute emitted byte count, which conflates
// unrelated rewrites this module does not own.
//
// A second, independent contaminant surfaced fixing the first one: opencode
// embeds the install's own absolute configDir path into execute-phase.md
// content (same fact row 33/atRefContractStillResolvesAfterComposition
// documents for SKILL.md), and `runMinimalInstall`'s temp-dir prefix
// (`gsd-<runtime>-<scope>-`) is a DIFFERENT length than
// `spawnGlobalInstall`'s (`gsd-2930-dest-<runtime>-`) — so comparing RAW
// file sizes between the two installs bakes in a root-path-length delta
// that has nothing to do with composeWorkflow. Normalize each side's own
// root out of the text before measuring, exactly as row 33 already does.
test('emittedWorkflowShrinksByMarkerBytesForEveryRuntime', () => {
const source = fs.readFileSync(PILOT_PATH, 'utf8');
const composed = composeWorkflow(source, { sourcePath: PILOT_PATH });
const sourceBytes = Buffer.byteLength(source, 'utf8');
const composedBytes = Buffer.byteLength(composed, 'utf8');
const expectedMarkerBytes = sourceBytes - composedBytes;
assert.ok(
expectedMarkerBytes > 0,
'sanity: the pilot workflow must actually carry gsd:section markers to strip',
);
const identityStubRepo = buildOverlayRepo({
'gsd-core/bin/lib/workflow-fragments.cjs': 'module.exports = { composeWorkflow: (c) => c };\n',
});
try {
for (const runtime of RUNTIMES) {
const real = runMinimalInstall({ runtime, scope: 'global' });
const stub = spawnGlobalInstall(path.join(identityStubRepo, 'bin', 'install.js'), runtime);
try {
assert.equal(
stub.result.status,
0,
`${runtime}: identity-stub install must succeed\nstderr: ${stub.result.stderr}`,
);
const realPath = path.join(real.configDir, PILOT_REL);
const stubPath = path.join(stub.configDir, PILOT_REL);
assert.ok(fs.existsSync(realPath), `${runtime}: real install is missing execute-phase.md`);
assert.ok(fs.existsSync(stubPath), `${runtime}: identity-stub install is missing execute-phase.md`);
const realText = stripRoot(fs.readFileSync(realPath, 'utf8'), real.root);
const stubText = stripRoot(fs.readFileSync(stubPath, 'utf8'), stub.root);
const realBytes = Buffer.byteLength(realText, 'utf8');
const stubBytes = Buffer.byteLength(stubText, 'utf8');
assert.equal(
stubBytes - realBytes,
expectedMarkerBytes,
`${runtime}: emitted size delta (stub ${stubBytes} - real ${realBytes}, root-normalized) must equal exactly the marker bytes stripped (${expectedMarkerBytes})`,
);
} finally {
cleanup(real.root);
cleanup(stub.root);
}
}
} finally {
cleanup(identityStubRepo);
}
});
// ─── Row 33: an unmarked workflow emits byte-identical for every runtime ──
//
// "Byte-identical" here means identical to what the SAME runtime's install
// pipeline would emit WITHOUT the #2930 composeWorkflow wiring — not
// necessarily identical to the raw repo source, since path-prefix rewrites,
// attribution stamping, and per-runtime .md converters already ran before
// this change and still run today. Proven empirically per runtime by
// comparing two REAL installs of the SAME unmarked file: one through this
// checkout's real composeWorkflow, one through an overlay whose
// gsd-core/bin/lib/workflow-fragments.cjs is stubbed to plain identity — any
// difference is attributable ONLY to the compose wiring, never to an
// unrelated converter (which fires identically on both sides).
test('unmarkedWorkflowEmitsByteIdenticalForEveryRuntime', () => {
const identityStubRepo = buildOverlayRepo({
'gsd-core/bin/lib/workflow-fragments.cjs': 'module.exports = { composeWorkflow: (c) => c };\n',
});
try {
for (const runtime of RUNTIMES) {
const real = runMinimalInstall({ runtime, scope: 'global' });
const stub = spawnGlobalInstall(path.join(identityStubRepo, 'bin', 'install.js'), runtime);
try {
assert.equal(
stub.result.status,
0,
`${runtime}: identity-stub install must succeed\nstderr: ${stub.result.stderr}`,
);
const realPath = path.join(real.configDir, UNMARKED_REL);
const stubPath = path.join(stub.configDir, UNMARKED_REL);
assert.ok(fs.existsSync(realPath), `${runtime}: real install is missing plan-phase.md`);
assert.ok(fs.existsSync(stubPath), `${runtime}: stub install is missing plan-phase.md`);
// Normalize each side's own randomly-generated temp root out of the
// content before hashing: some runtimes (opencode) embed the
// install's own absolute configDir path in execution_context refs,
// and the two installs necessarily used DIFFERENT temp roots — an
// unnormalized compare would report a spurious mismatch driven by
// temp-path length, not by anything composeWorkflow's wiring did.
const realText = stripRoot(fs.readFileSync(realPath, 'utf8'), real.root);
const stubText = stripRoot(fs.readFileSync(stubPath, 'utf8'), stub.root);
assert.equal(
Buffer.byteLength(realText, 'utf8'),
Buffer.byteLength(stubText, 'utf8'),
`${runtime}: plan-phase.md byte size drifted between real compose and identity-stub compose`,
);
const realHash = crypto.createHash('sha256').update(realText).digest('hex');
const stubHash = crypto.createHash('sha256').update(stubText).digest('hex');
assert.equal(
realHash,
stubHash,
`${runtime}: plan-phase.md content drifted between real compose and identity-stub compose`,
);
} finally {
cleanup(real.root);
cleanup(stub.root);
}
}
} finally {
cleanup(identityStubRepo);
}
});
// ─── Row 34: no gsd:section marker survives into any emitted artifact ─────
test('noSectionMarkerLeaksIntoEmittedArtifacts', () => {
for (const runtime of RUNTIMES) {
const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' });
try {
const emittedPath = path.join(configDir, PILOT_REL);
assert.ok(fs.existsSync(emittedPath), `${runtime}: emitted execute-phase.md is missing`);
const emittedText = fs.readFileSync(emittedPath, 'utf8');
assert.equal(
emittedText.includes('gsd:section'),
false,
`${runtime}: emitted execute-phase.md still contains a gsd:section marker token`,
);
} finally {
cleanup(root);
}
}
});
// ─── Row 35: ADR-0002 @-ref contract still resolves after composition ─────
//
// Two representative runtimes chosen to cover BOTH observed @-ref forms
// (empirically confirmed, #2930 dispatch): claude/cursor/codex rewrite to
// `@$HOME/...`, while opencode rewrites to a bare `@<absolute-path>/...`.
// Both installed SKILL.md files themselves pass through composeWorkflow too
// (as a no-op, being unmarked) — this proves that pass never corrupts or
// relocates the referenced workflow file.
/** Detect absoluteness from the token's own shape only — never from
* `process.platform` — so the same logic runs identically on every OS.
* Covers POSIX (`/...`), Windows drive-letter (`C:/...` or `C:\...`), and
* UNC (`\\server\share`) forms. */
function isAbsoluteRefTarget(candidate) {
return (
candidate.startsWith('/') ||
/^[a-zA-Z]:[\\/]/.test(candidate) ||
candidate.startsWith('\\\\')
);
}
function resolveExecutionContextRefTarget(token, root) {
const withoutAt = token.replace(/^@/, '');
if (isAbsoluteRefTarget(withoutAt)) return withoutAt; // already absolute (opencode form)
const stripped = withoutAt.replace(/^(?:~|\$HOME)\//, '');
return path.join(root, stripped);
}
test('atRefContractStillResolvesAfterComposition', () => {
for (const runtime of ['claude', 'opencode']) {
const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' });
try {
const skillPath = path.join(configDir, 'skills', 'gsd-plan-phase', 'SKILL.md');
assert.ok(fs.existsSync(skillPath), `${runtime}: installed gsd-plan-phase SKILL.md is missing`);
const skillContent = fs.readFileSync(skillPath, 'utf8');
const refs = executionContextRefs(skillContent);
assert.ok(refs.length > 0, `${runtime}: SKILL.md has no execution_context @-refs to check`);
for (const { token } of refs) {
const target = resolveExecutionContextRefTarget(token, root);
assert.ok(
fs.existsSync(target),
`${runtime}: execution_context @-ref "${token}" resolved to "${target}", which does not exist on disk`,
);
}
} finally {
cleanup(root);
}
}
});
// ─── FIX 1 (chore/2930 review): composeWorkflow is scoped to gsd-core/workflows/ ──
//
// copyWithPathReplacement is the emit path for the ENTIRE gsd-core/ tree
// (skillSrc = path.join(src, 'gsd-core'), bin/install.js:10806-10809), not
// just gsd-core/workflows/. A non-workflow .md elsewhere under gsd-core/
// (e.g. gsd-core/references/) that merely DOCUMENTS the marker syntax with
// an unfenced, structurally-invalid example line must never be run through
// composeWorkflow — doing so would either throw (breaking install for an
// unrelated file class) or silently strip/mis-parse the documentation line.
// Proven here by overlaying BOTH a marked workflow (must still compose) and
// an EXISTING non-workflow reference doc (buildOverlayRepo can only replace
// the content of a real leaf file, not graft in a net-new path — see the
// module doc comment's overlay-technique note) rewritten to carry an
// intentionally-UNCLOSED marker-shaped line (would throw if composeWorkflow
// ever touched it) in the SAME install run.
test('nonWorkflowMarkdownWithMarkerShapedLineIsNotComposed', () => {
const markedWorkflow = '<!-- gsd:section id="a" when="always" -->\nbody\n<!-- /gsd:section -->\n';
const nonWorkflowDoc =
'# Marker syntax\n\nExample (deliberately unfenced and unclosed to prove non-composition):\n\n<!-- gsd:section id="x" when="always" -->\nnever closed on purpose\n';
const NON_WORKFLOW_DOC_REL = path.join('gsd-core', 'references', 'context-budget.md');
const overlayRepo = buildOverlayRepo({
'gsd-core/workflows/execute-phase.md': markedWorkflow,
[NON_WORKFLOW_DOC_REL.split(path.sep).join('/')]: nonWorkflowDoc,
});
let dest;
try {
dest = spawnGlobalInstall(path.join(overlayRepo, 'bin', 'install.js'), 'claude');
assert.equal(
dest.result.status,
0,
`install must succeed: a non-workflow doc's marker-shaped line must never reach composeWorkflow\nstderr: ${dest.result.stderr}`,
);
const emittedWorkflowPath = path.join(dest.configDir, PILOT_REL);
assert.ok(fs.existsSync(emittedWorkflowPath), 'emitted execute-phase.md is missing');
assert.equal(
fs.readFileSync(emittedWorkflowPath, 'utf8'),
'body\n',
'gsd-core/workflows/execute-phase.md must still compose (markers stripped)',
);
const emittedDocPath = path.join(dest.configDir, NON_WORKFLOW_DOC_REL);
assert.ok(fs.existsSync(emittedDocPath), 'emitted context-budget.md is missing');
assert.equal(
fs.readFileSync(emittedDocPath, 'utf8'),
nonWorkflowDoc,
'a non-workflow .md must pass through composeWorkflow untouched, byte-identical, including its marker-shaped line',
);
} finally {
cleanup(overlayRepo);
if (dest) cleanup(dest.root);
}
});
// ─── Row 36: a malformed marker fails install loudly, with no partial emit ─
test('malformedMarkersFailInstallWithoutPartialEmit', () => {
const malformed = '<!-- gsd:section id="broken" when="always" -->\nnever closed\n';
const overlayRepo = buildOverlayRepo({ 'gsd-core/workflows/execute-phase.md': malformed });
let dest;
try {
dest = spawnGlobalInstall(path.join(overlayRepo, 'bin', 'install.js'), 'claude');
// stderr text is a child process's rendered prose, not a typed value
// this test can assert on across the process boundary (CONTRIBUTING.md
// "Prohibited: Raw Text Matching on Test Outputs" — err.reason is only
// reachable in-process; see tests/workflow-fragments.test.cjs's REASON
// assertions for the in-process equivalent of this same failure mode).
// Assert typed, observable facts instead: the install process exits
// non-zero, and no output file is written for the file that failed to
// compose.
assert.notEqual(
dest.result.status,
0,
`install must fail loudly on a malformed marker, got exit 0\nstdout: ${dest.result.stdout}`,
);
const emittedPath = path.join(dest.configDir, PILOT_REL);
assert.equal(
fs.existsSync(emittedPath),
false,
'a half-composed execute-phase.md must never be written when composition throws',
);
} finally {
cleanup(overlayRepo);
if (dest) cleanup(dest.root);
}
});

View File

@@ -0,0 +1,198 @@
'use strict';
/**
* Property-based tests for src/workflow-fragments.cts (compiled to
* gsd-core/bin/lib/workflow-fragments.cjs) — issue #2930 (epic #1671
* Phase 3). Covers 50-test-matrix.md rows 30-31.
*
* Document-shaped generators (CONTRIBUTING.md "Fixture provenance #2371",
* mirroring tests/context-predicates.property.test.cjs): these generators
* build arbitrary markdown documents out of prose lines, fenced blocks
* (whose contents — including marker LOOKALIKES — are always literal), and
* well-formed `gsd:section` marker pairs. They are NOT seeded from this
* module's own `composeWorkflow`/`renderFragments` — document shape (which
* lines exist, in what order, wrapped in what fences) is generated
* independently; only the STRIPPED-EXPECTATION bookkeeping (which line
* indexes are real top-level marker lines) is computed alongside, from the
* same generation step, never by round-tripping through the code under
* test.
*
* 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 { parseWorkflowSections, composeWorkflow, WHEN_VOCABULARY } = require('../gsd-core/bin/lib/workflow-fragments.cjs');
// Derived from the module's own frozen WHEN_VOCABULARY (DEFECT.GENERATIVE-FIX,
// chore/2930 review): a hardcoded copy here would silently desync from the
// production vocabulary the moment either side is edited without the other.
const WHEN_VALUES = [...WHEN_VOCABULARY];
// ─── Document-shaped generators ────────────────────────────────────────────
// Plain-text charset that can never accidentally spell an HTML comment
// delimiter or a fence delimiter — keeps every "decoy" line unambiguous.
const proseTextArb = fc.stringMatching(/^[A-Za-z0-9 .,'":;()]{0,40}$/);
const idArb = fc.stringMatching(/^[a-z][a-z0-9]{0,5}$/);
const whenArb = fc.constantFrom(...WHEN_VALUES);
// A prose line that LOOKS marker-adjacent but is never a real marker: a
// heading, a blockquote, a table row, a mid-line backticked mention (extra
// prose before/after disqualifies it structurally), or a one-line
// `gsd:loop-host` comment (a different marker family entirely).
const proseLineArb = fc.oneof(
proseTextArb,
proseTextArb.map((t) => `# ${t}`),
proseTextArb.map((t) => `> ${t}`),
proseTextArb.map((t) => `| ${t} | cell |`),
idArb.map((id) => `See \`<!-- gsd:section id="${id}" when="always" -->\` for syntax.`),
fc.constant('<!-- gsd:loop-host foo -->'),
);
const fenceTickArb = fc.constantFrom('```', '~~~', '````');
// A fenced block: opener, 0-4 inner lines (prose OR a marker-lookalike full
// line), matching closer using the SAME tick string — everything inside is
// LITERAL regardless of shape (rows 5-8 of 50-test-matrix.md).
function fenceBlockArb() {
return fc
.tuple(
fenceTickArb,
fc.array(
fc.oneof(
proseLineArb,
idArb.map((id) => `<!-- gsd:section id="${id}" when="always" -->`),
fc.constant('<!-- /gsd:section -->'),
),
{ minLength: 0, maxLength: 4 },
),
)
.map(([tick, inner]) => [tick, ...inner, tick]);
}
// A well-formed marker's interior: 0-3 items, each either a prose line or a
// nested fenced block (which may itself contain marker lookalikes).
function markerBodyArb() {
return fc
.array(fc.oneof({ arbitrary: proseLineArb, weight: 3 }, { arbitrary: fenceBlockArb(), weight: 1 }), {
minLength: 0,
maxLength: 3,
})
.map((items) => items.flat());
}
// A top-level document block: a single prose line, a whole fenced block, or
// a well-formed gsd:section marker pair (id assigned by the assembler for
// document-wide uniqueness — see documentArb).
const blockArb = fc.oneof(
{ arbitrary: proseLineArb.map((line) => ({ kind: 'prose', lines: [line] })), weight: 3 },
{ arbitrary: fenceBlockArb().map((lines) => ({ kind: 'prose', lines })), weight: 2 },
{ arbitrary: fc.tuple(whenArb, markerBodyArb()).map(([when, body]) => ({ kind: 'marker', when, body })), weight: 2 },
);
const eolArb = fc.constantFrom('\n', '\r\n');
/**
* A whole document assembled from an arbitrary sequence of blocks. Returns
* `{source, expectedStripped}`: `source` is the generated document text;
* `expectedStripped` is `source` with exactly the REAL top-level marker
* lines removed (computed from the same generation step, independent of
* the code under test).
*/
function documentArb() {
return fc
.tuple(fc.array(blockArb, { minLength: 0, maxLength: 8 }), eolArb, fc.boolean())
.map(([blocks, eol, trailingEol]) => {
const allLines = [];
const markerLineIndexes = new Set();
let counter = 0;
for (const block of blocks) {
if (block.kind === 'prose') {
allLines.push(...block.lines);
continue;
}
const id = `sec${counter}`;
counter += 1;
markerLineIndexes.add(allLines.length);
allLines.push(`<!-- gsd:section id="${id}" when="${block.when}" -->`);
allLines.push(...block.body);
markerLineIndexes.add(allLines.length);
allLines.push('<!-- /gsd:section -->');
}
// Per-line records, each carrying ITS OWN terminator -- mirrors how
// workflow-fragments.cts's own line splitter models termination, so
// "expected" is computed by the same "remove this line INCLUDING its
// terminator" rule the partition invariant defines. A naive
// `filter().join(eol)` is WRONG here: `.join()` inserts a separator
// only BETWEEN surviving elements, so it silently drops a survivor's
// real trailing terminator whenever the (now-removed) line that used
// to follow it supplied that separator — caught live by this
// generator against a real marker-wrapped empty fence, where the
// section's last body line sits immediately before the close marker.
const records = allLines.map((text, idx) => ({
text,
eol: idx === allLines.length - 1 ? (trailingEol ? eol : '') : eol,
}));
const source = records.map((r) => r.text + r.eol).join('');
const expectedStripped = records
.filter((_, idx) => !markerLineIndexes.has(idx))
.map((r) => r.text + r.eol)
.join('');
return { source, expectedStripped };
});
}
// ─── Row 30: parse/render round trip ───────────────────────────────────────
describe('property: parse/render round trip', () => {
test('parseRenderRoundTripProperty', () => {
fc.assert(
fc.property(documentArb(), ({ source, expectedStripped }) => {
const rendered = composeWorkflow(source);
assert.equal(rendered, expectedStripped);
}),
);
});
test('parseRenderRoundTripProperty: no markers means exact identity', () => {
fc.assert(
fc.property(fc.array(proseLineArb, { minLength: 0, maxLength: 10 }), eolArb, (lines, eol) => {
const source = lines.join(eol);
assert.equal(composeWorkflow(source), source);
}),
);
});
});
// ─── Row 31: idempotency ────────────────────────────────────────────────────
describe('property: parse is idempotent over render', () => {
test('parseIsIdempotentOverRender', () => {
fc.assert(
fc.property(documentArb(), ({ source }) => {
const rendered = composeWorkflow(source);
// Composing an already-composed (marker-free) document is a no-op.
assert.equal(composeWorkflow(rendered), rendered);
// Re-parsing the rendered output yields a fixed shape: zero
// sections for an empty document, otherwise exactly ONE implicit
// gap fragment whose body is the whole (now marker-free) document —
// no marker survives composition to be re-recognized.
const sectionsAfter = parseWorkflowSections(rendered);
if (rendered === '') {
assert.deepEqual(sectionsAfter, []);
} else {
assert.equal(sectionsAfter.length, 1);
assert.equal(sectionsAfter[0].explicit, false);
assert.equal(sectionsAfter[0].body, rendered);
}
}),
);
});
});

View File

@@ -0,0 +1,776 @@
'use strict';
/**
* Example-based unit tests for src/workflow-fragments.cts (compiled to
* gsd-core/bin/lib/workflow-fragments.cjs) — issue #2930 (epic #1671 Phase 3).
*
* Covers 50-test-matrix.md rows 1-29 and 37 (unit level). Rows 30/31
* (property) live in workflow-fragments.property.test.cjs; rows 32-36
* (install-level, real spawn-install) are out of scope for this module's
* unit suite per ADR-1671 "Architecture and contracts".
*
* No source-grep (CONTRIBUTING.md): every assertion is on typed values
* (WorkflowSection records, ComposeResult metadata, byte counts) — never on
* rendered text via `.includes()`/`.match()`.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempDir, cleanup } = require('./helpers.cjs');
const {
parseWorkflowSections,
toFragments,
renderFragments,
composeWorkflow,
WHEN_VOCABULARY,
REASON,
} = require('../gsd-core/bin/lib/workflow-fragments.cjs');
const { composeWithinBudget } = require('../gsd-core/bin/lib/context-composer.cjs');
const measureBytes = (text) => Buffer.byteLength(text, 'utf8');
/** Compose a document string from an array of lines, joined with '\n'. */
const doc = (...lines) => lines.join('\n');
// ─── Row 1: unmarked document (the 88/89 production shape) ─────────────────
describe('unmarked document round trip', () => {
test('unmarkedDocumentRoundTripsByteIdentical', () => {
const source = doc(
'# Some Workflow',
'',
'Ordinary prose describing the workflow.',
'',
'## A heading',
'More prose.',
'',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].id, 'gap-0');
assert.equal(sections[0].body, source);
const rendered = composeWorkflow(source);
assert.equal(rendered, source);
});
});
// ─── Row 2: single well-formed marker pair ──────────────────────────────────
describe('single marker pair', () => {
test('singleMarkerPairStripsMarkersAndPreservesBody', () => {
const source = doc(
'before prose',
'<!-- gsd:section id="sec-a" when="flag:--wave" -->',
'body line 1',
'body line 2',
'<!-- /gsd:section -->',
'after prose',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 3);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, 'before prose\n');
assert.equal(sections[1].explicit, true);
assert.equal(sections[1].id, 'sec-a');
assert.equal(sections[1].when, 'flag:--wave');
assert.equal(sections[1].body, 'body line 1\nbody line 2\n');
assert.equal(sections[2].explicit, false);
assert.equal(sections[2].body, 'after prose');
const rendered = composeWorkflow(source);
assert.equal(rendered, 'before prose\nbody line 1\nbody line 2\nafter prose');
});
});
// ─── Row 3: several disjoint pairs + unmarked gaps ─────────────────────────
describe('multiple disjoint marker pairs', () => {
test('multiplePairsPartitionDocumentExactly', () => {
const source = doc(
'gap0',
'<!-- gsd:section id="a" when="always" -->',
'bodyA',
'<!-- /gsd:section -->',
'gap1',
'<!-- gsd:section id="b" when="state:has-prior-phases" -->',
'bodyB',
'<!-- /gsd:section -->',
'gap2',
);
const sections = parseWorkflowSections(source);
assert.deepEqual(
sections.map((s) => ({ id: s.id, explicit: s.explicit })),
[
{ id: 'gap-0', explicit: false },
{ id: 'a', explicit: true },
{ id: 'gap-1', explicit: false },
{ id: 'b', explicit: true },
{ id: 'gap-2', explicit: false },
],
);
const markerLineRe = /^<!--\s*\/?gsd:section.*-->\s*$/;
const expected = source
.split('\n')
.filter((line) => !markerLineRe.test(line))
.join('\n');
assert.equal(composeWorkflow(source), expected);
});
});
// ─── Row 4: the real pilot workflow ─────────────────────────────────────────
describe('real execute-phase.md', () => {
// NOTE (chore/2930 retarget): the pilot moved from plan-phase.md to
// execute-phase.md — plan-phase.md sits 36 B under the ADR-857 Phase-6
// PRE_PHASE6 gate (tests/phase6-capstone-conformance.test.cjs) and cannot
// absorb marker overhead, so the maintainer retargeted the pilot to
// execute-phase.md (partial-wave, gap-closure-artifacts, regression-gate).
test('pilotWorkflowParsesAndRendersToSourceMinusMarkers', () => {
const pilotPath = path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md');
const original = fs.readFileSync(pilotPath, 'utf8');
// execute-phase.md carries the pilot's real marker pairs today: parsing
// it must recognize exactly those three explicit sections, in document
// order, and composing it must strip every marker line while leaving
// every byte of body content untouched.
const baselineSections = parseWorkflowSections(original, pilotPath);
const baselineExplicit = baselineSections.filter((s) => s.explicit);
assert.deepEqual(
baselineExplicit.map((s) => s.id),
['partial-wave', 'gap-closure-artifacts', 'regression-gate'],
);
const composedOriginal = composeWorkflow(original, { sourcePath: pilotPath });
assert.equal(composedOriginal.includes('gsd:section'), false);
assert.ok(Buffer.byteLength(composedOriginal, 'utf8') < Buffer.byteLength(original, 'utf8'));
// Wrap an ADDITIONAL, disjoint marker pair around an arbitrary interior
// slice of real content that sits outside every existing marker pair
// (lines 11-15, well before "partial-wave") and confirm it parses as a
// fourth explicit section and composes to the SAME final output as the
// unmodified file — every fragment is `verbatim` (row 23), so wrapping
// already-included content in a new marker pair can never change what
// is emitted, only how it is partitioned internally.
const lines = original.split(/\r?\n/);
const sliceStart = 10;
const sliceEnd = 15;
const markedLines = [
...lines.slice(0, sliceStart),
'<!-- gsd:section id="pilot-slice" when="always" -->',
...lines.slice(sliceStart, sliceEnd),
'<!-- /gsd:section -->',
...lines.slice(sliceEnd),
];
const marked = markedLines.join('\n');
const sections = parseWorkflowSections(marked, pilotPath);
const explicitSections = sections.filter((s) => s.explicit);
assert.deepEqual(
explicitSections.map((s) => s.id),
['pilot-slice', 'partial-wave', 'gap-closure-artifacts', 'regression-gate'],
);
assert.equal(explicitSections[0].body, lines.slice(sliceStart, sliceEnd).join('\n') + '\n');
const rendered = composeWorkflow(marked, { sourcePath: pilotPath });
assert.equal(rendered, composedOriginal);
assert.equal(measureBytes(rendered), measureBytes(composedOriginal));
});
});
// ─── Row 5/6: fence negative space ──────────────────────────────────────────
describe('marker lookalikes inside fences', () => {
test('markerInsideFencedBlockIsLiteral', () => {
const source = doc(
'prose before',
'```',
'<!-- gsd:section id="fake" when="always" -->',
'```',
'prose after',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('markerInsideFenceInsideSectionStaysLiteral', () => {
const source = doc(
'<!-- gsd:section id="real" when="always" -->',
'intro',
'```',
'<!-- gsd:section id="fake" when="always" -->',
'<!-- /gsd:section -->',
'```',
'outro',
'<!-- /gsd:section -->',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, true);
assert.equal(sections[0].id, 'real');
assert.equal(
sections[0].body,
['intro', '```', '<!-- gsd:section id="fake" when="always" -->', '<!-- /gsd:section -->', '```', 'outro', ''].join(
'\n',
),
);
});
});
// ─── Row 7/8: fence/comment mutual precedence ───────────────────────────────
describe('fence and comment mutual precedence', () => {
test('fenceDelimiterInsideCommentDoesNotOpenFence', () => {
const source = doc(
'<!-- unrelated comment',
'```',
'still commented',
'-->',
'<!-- gsd:section id="after-comment" when="always" -->',
'body',
'<!-- /gsd:section -->',
);
// If the fence delimiter on line 2 had wrongly opened a fence, the real
// marker pair below would never be recognized (it would be swallowed as
// "fence content" all the way to EOF).
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].id, 'after-comment');
assert.equal(explicitSections[0].body, 'body\n');
});
test('commentTokenInsideFenceDoesNotOpenComment', () => {
const source = doc(
'```',
'<!-- unclosed comment token inside fence',
'```',
'<!-- gsd:section id="after-fence" when="always" -->',
'body',
'<!-- /gsd:section -->',
);
// If the `<!--` inside the fence had wrongly opened a real comment, the
// real marker pair below would never be recognized (swallowed as
// "comment content" to EOF).
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].id, 'after-fence');
assert.equal(explicitSections[0].body, 'body\n');
});
});
// ─── Row 9/10: other negative space ─────────────────────────────────────────
describe('loop-host and backtick negative space', () => {
test('loopHostMarkerIsNotASectionMarker', () => {
const source = doc(
'<!-- gsd:loop-host',
'step: plan',
'points: plan:pre, plan:post',
'-->',
'<purpose>Do the thing.</purpose>',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('backtickedMarkerMentionIsNotAMarker', () => {
const source = doc(
'See `<!-- gsd:section id="x" when="always" -->` for the marker syntax.',
'And the close form is `<!-- /gsd:section -->` on its own line.',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
});
// ─── Rows 11-14: structural negatives with location ────────────────────────
describe('structural negatives throw with file + line', () => {
test('unclosedSectionThrowsWithLocation', () => {
const source = doc('prose', '<!-- gsd:section id="a" when="always" -->', 'body, never closed');
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:2') && err.reason === REASON.UNCLOSED_SECTION,
);
});
test('unmatchedCloseThrowsWithLocation', () => {
const source = doc('prose', '<!-- /gsd:section -->', 'more prose');
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:2') && err.reason === REASON.UNMATCHED_CLOSE,
);
});
test('nestedSectionThrows', () => {
const source = doc(
'<!-- gsd:section id="outer" when="always" -->',
'<!-- gsd:section id="inner" when="always" -->',
'body',
'<!-- /gsd:section -->',
'<!-- /gsd:section -->',
);
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:2') && err.reason === REASON.NESTED_SECTION,
);
});
test('duplicateSectionIdThrows', () => {
const source = doc(
'<!-- gsd:section id="dup" when="always" -->',
'first',
'<!-- /gsd:section -->',
'<!-- gsd:section id="dup" when="flag:--wave" -->',
'second',
'<!-- /gsd:section -->',
);
// Throws on the SECOND occurrence's line, not the first.
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:4') && err.reason === REASON.DUPLICATE_ID,
);
});
});
// ─── Rows 15-18: attribute-shape negatives ─────────────────────────────────
describe('attribute-shape negatives', () => {
test('missingIdAttributeThrows', () => {
const source = '<!-- gsd:section when="always" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MISSING_ID,
);
});
test('missingWhenAttributeThrows', () => {
const source = '<!-- gsd:section id="x" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MISSING_WHEN,
);
});
test('unknownWhenValueThrows', () => {
const source = '<!-- gsd:section id="x" when="flag:--nonexistent" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNKNOWN_WHEN,
);
});
test('whenValueWithBooleanOperatorThrows', () => {
for (const when of ['flag:--wave && state:has-prior-phases', 'flag:--wave || state:has-prior-phases', '!flag:--wave']) {
const source = `<!-- gsd:section id="x" when="${when}" -->\nbody\n<!-- /gsd:section -->`;
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNKNOWN_WHEN,
`expected throw for when="${when}"`,
);
}
});
test('malformedAttributesThrows', () => {
const source = '<!-- gsd:section id="x" when -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MALFORMED_ATTRIBUTES,
);
});
test('unrecognizedAttributeThrows', () => {
const source = '<!-- gsd:section id="x" when="always" bogus="1" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNRECOGNIZED_ATTRIBUTE,
);
});
test('malformedIdValueThrows', () => {
const source = '<!-- gsd:section id="-bad-" when="always" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MALFORMED_ID,
);
});
test('closeMarkerWithAttributesThrows', () => {
const source = '<!-- gsd:section id="x" when="always" -->\nbody\n<!-- /gsd:section foo="1" -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.CLOSE_WITH_ATTRIBUTES,
);
});
});
// ─── FIX 2/3 (chore/2930 review): REASON enum shape is locked ─────────────
describe('REASON enum is frozen and its shape is locked', () => {
test('reasonEnumKeysAreLocked', () => {
assert.equal(Object.isFrozen(REASON), true);
assert.deepEqual(Object.keys(REASON).sort(), [
'CLOSE_WITH_ATTRIBUTES',
'DUPLICATE_ID',
'MALFORMED_ATTRIBUTES',
'MALFORMED_ID',
'MISSING_ID',
'MISSING_WHEN',
'NESTED_SECTION',
'UNCLOSED_SECTION',
'UNKNOWN_WHEN',
'UNMATCHED_CLOSE',
'UNRECOGNIZED_ATTRIBUTE',
]);
});
});
// ─── Doc/enum parity guard (DEFECT.GENERATIVE-FIX, code review #2930) ──────
describe('REASON enum and docs "Fails closed" bullets stay in parity', () => {
test('everyReasonMemberIsDocumentedAndNoStaleBulletsRemain', () => {
// allow-test-rule: docs-parity — the doc text IS the contract being checked here (#2930)
const docPath = path.join(__dirname, '..', 'docs', 'reference', 'workflow-fragments.md');
const docText = fs.readFileSync(docPath, 'utf8');
const sectionMatch = /## Fails closed\r?\n([\s\S]*?)\r?\n## /.exec(docText);
assert.ok(sectionMatch, 'docs/reference/workflow-fragments.md must have a "## Fails closed" section');
const sectionText = sectionMatch[1];
const enumMembers = Object.keys(REASON);
// Key on the reason IDENTIFIER (e.g. `MALFORMED_ATTRIBUTES`) appearing in
// a bullet, never on bullet prose — a reword of the human-readable
// sentence must never falsely trip or falsely clear this guard.
const undocumented = enumMembers.filter((name) => !sectionText.includes(name));
const mentionedIdentifiers = [...sectionText.matchAll(/`([A-Z][A-Z0-9_]*)`/g)].map((m) => m[1]);
const staleMentions = mentionedIdentifiers.filter((name) => !enumMembers.includes(name));
assert.deepEqual(
undocumented,
[],
`REASON member(s) missing a "Fails closed" bullet in docs/reference/workflow-fragments.md: ${undocumented.join(', ')}`,
);
assert.deepEqual(
staleMentions,
[],
`"Fails closed" section mentions identifier(s) that are not REASON members (stale bullet?): ${staleMentions.join(', ')}`,
);
});
});
// ─── Row 19: frozen vocabulary ──────────────────────────────────────────────
describe('frozen when= vocabulary', () => {
test('whenVocabularyIsFrozenAndLocked', () => {
// WHEN_VOCABULARY is a frozen array (not an enum object) per the shipped
// public API — lock the actual VALUES (sorted), not Object.keys() (which
// for an array only reflects index positions '0','1',... and would not
// catch a value being silently renamed). See the dispatch report for
// this deliberate deviation from the test matrix's literal wording.
assert.equal(Object.isFrozen(WHEN_VOCABULARY), true);
assert.deepEqual(
[...WHEN_VOCABULARY].sort(),
['always', 'flag:--wave', 'state:gap-closure-phase', 'state:has-prior-phases'],
);
});
});
// ─── Rows 20-22: boundary documents ─────────────────────────────────────────
describe('boundary documents', () => {
test('emptyDocumentProducesNoFragments', () => {
const sections = parseWorkflowSections('');
assert.deepEqual(sections, []);
assert.equal(composeWorkflow(''), '');
});
test('documentOfOnlyAMarkerPairYieldsEmptyBody', () => {
const source = '<!-- gsd:section id="x" when="always" -->\n<!-- /gsd:section -->';
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, true);
assert.equal(sections[0].id, 'x');
assert.equal(sections[0].body, '');
assert.equal(composeWorkflow(source), '');
});
test('unclosedFenceAtEofDoesNotThrow', () => {
const source = doc('prose', '```', 'never closed', '<!-- gsd:section id="x" when="always" -->');
assert.doesNotThrow(() => parseWorkflowSections(source));
const sections = parseWorkflowSections(source);
// The whole document, including the marker-shaped line, is literal
// fence content — one implicit gap fragment, byte-identical.
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
});
});
// ─── Rows 23-25: cross-platform + liberal formatting ───────────────────────
describe('cross-platform line endings and liberal marker formatting', () => {
test('crlfDocumentRoundTripsByteIdentical', () => {
const source = ['prose one', '<!-- gsd:section id="x" when="always" -->', 'crlf body', '<!-- /gsd:section -->', 'prose two'].join(
'\r\n',
);
const rendered = composeWorkflow(source);
const expected = source
.split('\r\n')
.filter((line) => !/^<!--\s*\/?gsd:section.*-->\s*$/.test(line))
.join('\r\n');
assert.equal(rendered, expected);
});
test('mixedLineEndingsPreservedExactly', () => {
const source = 'prose\r\n<!-- gsd:section id="x" when="always" -->\r\nbody one\nbody two\n<!-- /gsd:section -->\nprose two';
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].body, 'body one\nbody two\n');
const rendered = composeWorkflow(source);
assert.equal(rendered, 'prose\r\nbody one\nbody two\nprose two');
});
test('attributeOrderAndSpacingAreAccepted', () => {
const variants = [
'<!-- gsd:section id="x" when="always" -->',
'<!--gsd:section id="x" when="always"-->',
'<!-- gsd:section when="always" id="x" -->',
' <!-- gsd:section when="always" id="x" --> ',
'<!--gsd:section when="always"id="x"-->',
];
for (const openLine of variants) {
const source = `${openLine}\nbody\n<!-- /gsd:section -->`;
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1, `expected recognition for: ${openLine}`);
assert.equal(explicitSections[0].id, 'x');
assert.equal(explicitSections[0].when, 'always');
// Re-render never leaks the original spacing — the marker is dropped
// entirely, so only the body survives.
assert.equal(composeWorkflow(source), 'body\n');
}
});
});
// ─── Rows 26-29: budget boundary set (non-lossiness is structural) ─────────
describe('budget boundary set: nothing is ever trimmed', () => {
const source = doc(
'gap prose',
'<!-- gsd:section id="a" when="always" -->',
'section a body',
'<!-- /gsd:section -->',
'more gap prose',
);
function composeAt(budget) {
const sections = parseWorkflowSections(source);
const fragments = toFragments(sections);
return composeWithinBudget({ fragments, budget, measure: measureBytes, options: { charsPerUnit: 1 } });
}
const baseline = (() => {
const sections = parseWorkflowSections(source);
const fragments = toFragments(sections);
return fragments.reduce((sum, f) => sum + measureBytes(f.content), 0);
})();
const expectedRendered = composeWorkflow(source);
test('nothingTrimmedWhenBudgetEqualsContent', () => {
const result = composeAt(baseline);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(renderFragments(result), expectedRendered);
});
test('nothingTrimmedWhenBudgetIsOneUnderContent', () => {
const result = composeAt(baseline - 1);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(renderFragments(result), expectedRendered);
});
test('nothingTrimmedWhenBudgetIsOneOverContent', () => {
const result = composeAt(baseline + 1);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(renderFragments(result), expectedRendered);
});
test('nothingTrimmedUnderAbsurdBudgetPressure', () => {
const result = composeAt(1);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(result.metadata.hardFailed, false);
assert.equal(renderFragments(result), expectedRendered);
});
});
// ─── Row 37: fs.readFileSync fault injection ───────────────────────────────
/**
* Simulate the realistic caller shape (read a workflow file, compose it,
* write the composed result elsewhere) with `fs.readFileSync` monkeypatched
* to throw. The monkeypatch is saved/restored HERE, in a helper, inside a
* `finally` — never inside a test body, and never via chmod/permission
* tricks (CLAUDE.md cross-platform fault-injection rule).
*/
function withInjectedReadFailure(fn) {
const original = fs.readFileSync;
fs.readFileSync = () => {
throw new Error('injected read failure');
};
try {
return fn();
} finally {
fs.readFileSync = original;
}
}
// ─── FIX 4 (chore/2930 review): adversarial parser-input fixtures ─────────
// CONTRIBUTING.md:484-513 requires adversarial fixtures for a new parser's
// inputs. Each case here either round-trips byte-identical or produces the
// correct typed REASON — never a message-text match.
describe('adversarial content bytes', () => {
test('unicodeHeadingRoundTripsByteIdentical', () => {
const source = doc(
'# 見出し — Ünïcödé Hëading 🚀',
'<!-- gsd:section id="sec" when="always" -->',
'body with 中文, кириллица, emoji 🎉',
'<!-- /gsd:section -->',
'trailing プロース',
);
const expected = doc('# 見出し — Ünïcödé Hëading 🚀', 'body with 中文, кириллица, emoji 🎉', 'trailing プロース');
const rendered = composeWorkflow(source);
assert.equal(rendered, expected);
assert.equal(measureBytes(rendered), measureBytes(expected));
});
test('nulByteInBodyRoundTripsByteIdentical', () => {
const source = `prose\0more\n<!-- gsd:section id="x" when="always" -->\nbody\0with\0nul\n<!-- /gsd:section -->\nafter\0`;
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].body, 'body\0with\0nul\n');
const rendered = composeWorkflow(source);
assert.equal(rendered, 'prose\0more\nbody\0with\0nul\nafter\0');
});
test('unicodeReplacementCharacterRoundTripsByteIdentical', () => {
const source = `prose <20> end\n<!-- gsd:section id="x" when="always" -->\nbody <20><>\n<!-- /gsd:section -->\nafter <20>`;
const rendered = composeWorkflow(source);
assert.equal(rendered, 'prose <20> end\nbody <20><>\nafter <20>');
});
test('leadingByteOrderMarkRoundTripsByteIdentical', () => {
const source = '# Heading\n<!-- gsd:section id="x" when="always" -->\nbody\n<!-- /gsd:section -->\ntail';
const sections = parseWorkflowSections(source);
const gaps = sections.filter((s) => !s.explicit);
// The BOM is ordinary content of the leading gap — never stripped or
// otherwise special-cased by this parser.
assert.equal(gaps[0].body, '# Heading\n');
const rendered = composeWorkflow(source);
assert.equal(rendered, '# Heading\nbody\ntail');
});
});
describe('adversarial fence shapes', () => {
test('fenceWithinFenceStaysLiteralUntilOuterCloser', () => {
const source = doc(
'prose before',
'````',
'```',
'<!-- gsd:section id="fake" when="always" -->',
'```',
'````',
'prose after',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('tildeFenceHidesMarkerLookalike', () => {
const source = doc('prose', '~~~', '<!-- gsd:section id="fake" when="always" -->', '~~~', 'prose after');
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('indentedFenceUpToThreeSpacesHidesMarkerLookalike', () => {
const source = doc('prose', ' ```', '<!-- gsd:section id="fake" when="always" -->', ' ```', 'prose after');
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
});
describe('adversarial line-ending shapes', () => {
test('markerLineTerminatedByLoneCrIsNotRecognizedAsAMarker', () => {
// A bare `\r` with no accompanying `\n` anywhere in the document is not
// an EOL this grammar recognizes (only '' / '\n' / '\r\n' — see the
// module doc comment). The marker-shaped text is therefore never on its
// "own line" and must be left as ordinary literal content, not parsed
// as an open marker.
const source = '<!-- gsd:section id="x" when="always" -->\rbody, never a real line break';
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
});
describe('fs.readFileSync fault injection mid-compose', () => {
test('readFailureDuringCompositionLeavesNoPartialArtifact', (t) => {
const tmpDir = createTempDir('gsd-wf-fault-');
t.after(() => cleanup(tmpDir));
const srcPath = path.join(tmpDir, 'source.md');
const destPath = path.join(tmpDir, 'composed.md');
fs.writeFileSync(srcPath, '<!-- gsd:section id="x" when="always" -->\nbody\n<!-- /gsd:section -->\n');
function readComposeWrite() {
const content = fs.readFileSync(srcPath, 'utf8');
const result = composeWorkflow(content, { sourcePath: srcPath });
fs.writeFileSync(destPath, result);
return result;
}
assert.throws(
() => withInjectedReadFailure(() => readComposeWrite()),
(err) => err instanceof Error && err.message === 'injected read failure',
);
assert.equal(fs.existsSync(destPath), false, 'no partial artifact must be written when the read fails');
// Restored correctly: a subsequent real call succeeds and DOES write.
const result = readComposeWrite();
assert.equal(fs.existsSync(destPath), true);
assert.equal(result, 'body\n');
});
});