Files
msd-core/docs/reference/workflow-fragments.md
Tom Boucher 640eaee16e 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>
2026-08-01 12:12:20 -04:00

8.8 KiB

Workflow fragments (reference)

Diátaxis quadrant: Reference. This is the canonical specification of the in-file <!-- gsd:section --> marker grammar used to fragmentize GSD workflow markdown for per-runtime emission. For the surrounding seam (why it exists and how it composes with the shared budget composer), see Architecture: Workflow Fragmentization and Emission and ADR-1671 (open questions 1 and 2).

Workflow authors can mark one or more sections of a gsd-core/workflows/*.md file so that bin/install.js's emission path can compose them per runtime. Today this is an authoring model with no run-time effect yet — see Not acted on yet below.

Marker syntax

An open marker is a line whose only content (after trimming leading/trailing whitespace) is:

<!-- gsd:section id="<id>" when="<when>" -->

A close marker is a line whose only content is:

<!-- /gsd:section -->
  • Attribute order is free and inner spacing around = and between attributes is flexible.
  • id must match /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ and must be unique within one file.
  • when must be exactly one entry of the frozen vocabulary below — no operators, no negation, no nesting.
  • Both id and when are required on every open marker; a marker missing either attribute fails closed (see Fails closed).

Text between an open marker and its matching close marker is that section's body, byte-for-byte (including its own line terminators). Text outside any marker pair becomes an implicit "gap" fragment — the file's ordinary, unmarked content — so a workflow with no markers at all parses to exactly one gap fragment and composes back byte-identical to its source.

The frozen when= vocabulary

when= takes exactly one of:

Value Meaning
always Section is always applicable.
flag:--wave Applicable when the workflow runs with --wave.
state:gap-closure-phase Applicable when the phase number is a gap-closure phase (has a decimal, e.g. 4.1).
state:has-prior-phases Applicable when prior phases (and their VERIFICATION.md files) exist.

This list is closed by design (Greenspun's Tenth Rule): left open-ended, when= would acquire boolean operators, negation, precedence, and runtime/capability predicates one edit at a time, becoming an ad-hoc, informally-specified applicability language. Widening the vocabulary is a coordinated ADR amendment to ADR-1671, never an organic edit to the parser.

Fails closed

An authoring mistake throws at parse time, naming the source file and 1-based line number, rather than being silently dropped or swallowed to end-of-file:

  • Missing id= or when= attribute (MISSING_ID, MISSING_WHEN).
  • when= value not in the frozen vocabulary above, including any boolean operator or negation form (UNKNOWN_WHEN).
  • id= value that does not match the id grammar (MALFORMED_ID).
  • Malformed attribute syntax on an open marker — the attribute text is not a run of well-formed key="value" tokens (e.g. an unterminated quote or a duplicate attribute key) (MALFORMED_ATTRIBUTES).
  • An unrecognized attribute on an open marker (UNRECOGNIZED_ATTRIBUTE).
  • A close marker carrying attributes (CLOSE_WITH_ATTRIBUTES).
  • An unmatched close marker, i.e. close with no open (UNMATCHED_CLOSE).
  • A nested marker, i.e. open marker while already inside an open section (NESTED_SECTION).
  • A duplicate id= within one file (DUPLICATE_ID).
  • An open marker with no matching close before end of file (UNCLOSED_SECTION).

An unrecognized when= is treated as an authoring instruction that must never be silently ignored, not as a value to fail open on — this is deliberately asymmetric with the marker formatting tolerance above (free attribute order, flexible spacing), which is liberal by design.

Markers are stripped at emit

Composition runs parseWorkflowSections → map sections to fragments → the shared context-composer.cjs budget seam (every fragment uses the verbatim strategy, so nothing is trimmed) → re-join fragment bodies in document order. The marker lines themselves are never part of any fragment body, so the composed output — and therefore every installed runtime artifact — contains no gsd:section markers at all. An unmarked file composes to itself exactly; a marked file composes to itself minus the marker line bytes.

Composition runs before the per-runtime converters (the .claude/ → .windsurf/-style path and reference rewrites), so a marker's id/when attribute text is never exposed to a rewrite regex.

Fenced and commented lookalikes are literal

A <!-- gsd:section ... -->-shaped line inside a fenced code block (three or more backticks or tildes, CommonMark-style) is not a marker — it is literal fence content, because workflows document their own marker syntax in fenced examples (as in this page and in the workflow files themselves). The same applies to a gsd:section mention inside an unrelated HTML comment, or in prose/backtick text that never opens a real one-line comment. Fence and comment detection run as a single interleaved left-to-right scan, mirroring the discipline used by the CONTEXT.md predicate parser (src/context-predicates.cts): while a fence is open, only a matching closer can end it; while a comment is open, only --> can end it; an unclosed fence running to end of file is not an error — everything after it is simply literal.

The pre-existing <!-- gsd:loop-host ... --> marker family (consumed by scripts/gen-loop-host-contract.cjs) is a different, already-established marker and is never treated as a gsd:section marker.

Not acted on yet

when= is parsed and validated today, but applicability selection — actually choosing which sections apply to a given invocation — is not implemented in this phase. Every fragment composes into the output regardless of its when= value; only the marker lines are stripped. Run-time selection is planned for a later phase of ADR-1671's epic.

Piloted on one workflow so far

Only gsd-core/workflows/execute-phase.md carries markers today. The marker grammar and composer seam are general-purpose across any workflow file, but rollout to other LARGE/XL workflows is intentionally sequenced as later work, not part of this phase.

The pilot marks three <step> blocks: partial-wave (flag:--wave), gap-closure-artifacts (state:gap-closure-phase), and regression-gate (state:has-prior-phases).

The pilot was retargeted from plan-phase.md mid-phase. Issue #2930's own motivating mutually-exclusive branches (--prd, --ingest, --mvp, --reviews) all live in plan-phase.md, not execute-phase.md. But plan-phase.md sits only 36 B under an independent, pre-existing size gate (tests/phase6-capstone-conformance.test.cjs's PRE_PHASE6, an ADR-857 Phase-6 completion property) and cannot absorb any marker overhead at all — so it could not be fragmentized under this phase's grammar regardless of branch shape. This is direct evidence for the epic's premise that fragmentization pays off, and it also means Phase 4 (moving size caps from source bytes to emitted bytes) may need to land before plan-phase.md itself can be fragmentized. Separately, and independent of the size-gate finding, --mvp would remain unmarkable by this grammar even if the size gate allowed it: its content in plan-phase.md is INTERLEAVED with other flags rather than living in its own contiguous section (MVP_MODE resolution shares a single bash block with --tdd, --no-tracer, and --no-reversibility-gates handling at plan-phase.md:125-158, and elsewhere it is inline ${MVP_MODE === 'true' ? ... } template interpolation embedded inside the planner prompt at plan-phase.md:794-803) — the marker grammar is closed, non-nesting, and whole-line (see Marker syntax above), with no way to wrap part of a line or split a shared conditional block without either corrupting the conditional or bundling unrelated flags into one section. See ADR-1671 open question 1's resolution for the full record, and Phase 6 (LARGE/XL rollout) for how both limits get addressed.

  • ADR-1671 — the platform decision record, including open questions 1 (fragment unit) and 2 (build-time vs. run-time emission), both resolved by this phase.
  • Architecture: Workflow Fragmentization and Emission.
  • src/workflow-fragments.cts — the compiled parser/composer source.
  • src/context-composer.cts — the shared budget-composition seam consumed by composeWorkflow.