* 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>
176 lines
8.8 KiB
Markdown
176 lines
8.8 KiB
Markdown
# 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](../ARCHITECTURE.md#workflow-fragmentization-and-emission-srcworkflow-fragmentscts-adr-1671)
|
|
> and [ADR-1671](../adr/1671-dynamic-context-management-platform.md) (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](#not-acted-on-yet) below.
|
|
|
|
## Marker syntax
|
|
|
|
An open marker is a line whose only content (after trimming leading/trailing
|
|
whitespace) is:
|
|
|
|
```html
|
|
<!-- gsd:section id="<id>" when="<when>" -->
|
|
```
|
|
|
|
A close marker is a line whose only content is:
|
|
|
|
```html
|
|
<!-- /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](#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](#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](../adr/1671-dynamic-context-management-platform.md) open
|
|
question 1's resolution for the full record, and Phase 6 (LARGE/XL rollout)
|
|
for how both limits get addressed.
|
|
|
|
## Related
|
|
|
|
- [ADR-1671](../adr/1671-dynamic-context-management-platform.md) — 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](../ARCHITECTURE.md#workflow-fragmentization-and-emission-srcworkflow-fragmentscts-adr-1671).
|
|
- `src/workflow-fragments.cts` — the compiled parser/composer source.
|
|
- `src/context-composer.cts` — the shared budget-composition seam consumed by
|
|
`composeWorkflow`.
|