chore(#2931): cap emitted per-runtime bytes and single-source windsurf (#2984)

* fix(#2931): preserve protected regions and cap emitted per-runtime bytes

Route every runtime brand swap through applyClaudeCodeBrandSwap so
"Claude Code" survives verbatim inside <runtime_compatibility> regions
(#2284b). The fix existed only in bin/install.js's local copies; the
src/*.cts exports still used a naive replace, so binding install.js to
the single source -- as this phase does for the Windsurf family --
would have silently regressed those runtimes. A table-driven parity
guard now covers all nine brand-swapping converters.

De-duplicate the Windsurf converter family: delete the six local copies
in bin/install.js and bind the four exported ones by reference, guarded
by reference-identity assertions (the ADR-1508/#1675 pattern). The two
unexported helpers and an unused tool table go with them.

Replace the Windsurf 12,000-byte throw with description truncation,
matching the bound its sibling skill converter already applied. The
throw could only fire on an ~11.7 KB frontmatter description: the
largest emitted workflow is 311 bytes. Truncation makes the cap
unreachable by construction and leaves 12,000 in exactly one place,
eliminating the dual-surface duplication rather than testing for it.

Add the emitted-byte cap gate: buildEmittedSizes captures LF- and
<HOME>-normalized bytes from the walk buildParityManifest already
performs, and evaluateEmittedCaps asserts them against a per-runtime
cap table with dead-rule detection. buildParityManifest's return shape
is deliberately unchanged -- diffEmitted compares its values with
===, so making them objects would report all 8,529 emitted paths as
moved. A regression test pins the values as strings.

Add a deterministic trim-safety gate over composeWithinBudget's
omitted/shrunk/floored/isolatePrefix metadata, with an anti-vacuity
rule, replacing the model-graded eval gate the issue described.

* docs(#2931): correct ADR-1671 windsurf premise and trim-safety contract

* fix(#2931): bound the windsurf command name and single-source the brand swap

Review findings from the orthogonal passes, all fixed inline.

The claim that removing the 12,000-byte throw left total emission
"bounded by construction" was false. The #1615 regex constrains the
character class but not the length, and commandName is interpolated
three times into the emitted workflow: a 20,000-character name emitted
60,162 bytes silently. Add WINDSURF_COMMAND_NAME_MAX=128 as a separate,
clearly-labelled size control that THROWS -- commandName is the @-ref
path target, so truncating it would point the workflow at a file that
does not exist (DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED). The
#1615 security regex is untouched and still runs first. 128 is generous:
the longest shipped name is gsd-plan-review-convergence at 27.

Harmonize convertClaudeCommandToWindsurfSkill onto the code-point-safe
truncation helper. It still used a UTF-16 slice(0,177) -- the exact
surrogate-splitting bug the helper was written to avoid, in the very
sibling the helper's comment cites as its model. Bounds are unchanged,
so output is byte-identical for every shipped command (descriptions max
out at 99 chars).

Export applyClaudeCodeBrandSwap and bind it in bin/install.js, deleting
the local copy. Adding it to the .cts left two unlinked implementations
of identical logic -- the drift class this change exists to remove.
Verified byte-identical across eight fixtures and five sequential calls
before merging, and guarded by a reference-identity assertion.

Convert three try/finally test bodies to t.after (CONTRIBUTING.md:344),
add fast-check property coverage for the trim-safety contract, and use
fc.pre instead of a bare return in a property callback.

* test(#2931): fix three test-authoring bugs the remote matrix caught

The remote runner returned 8 unique failures on 6f15cdeb8. All three
causes were in the test files, not the modules under test -- local
harnesses exercise the modules directly, so nothing executed the test
bodies until the matrix did.

`{ __proto__: [...] }` in an object literal sets the prototype instead
of an own key, so the JSON round-trip erased it and the cap table never
saw a reserved runtime key. The production rejection was already
correct; the test could not reach it. Use a computed key.

Two cap fixtures tripped orthogonal error paths rather than the paths
they name: one declared windsurf in the cap table but omitted it from
sizes (UNKNOWN_RUNTIME), the other left the sole windsurf pattern
matching nothing (a genuine dead rule). Both now include a compliant
artifact so the intended branch is what is asserted. The dead-rule and
unknown-runtime contracts are deliberate and unchanged.

`const { root } = makeSyntheticConfig({ ... `${root}` })` referenced
`root` from inside its own initializer -- a temporal dead zone error.
makeSyntheticConfig now optionally takes a (root) => files factory.

Also raise the npm pack --dry-run bound 60s -> 120s in the shipped-
scripts packaging test. That failure is NOT from this branch: the file
is byte-identical to next, a fresh tsc measures 1.98s there vs 2.14s
here, and the run recorded 60,637ms against a 60,000ms bound -- a
timeout under 28,948-test parallel contention, not a slowdown. Fixed
rather than deferred because a bound that tight is fragile regardless
of which branch trips it.

* chore(#2931): backfill changeset pr number to 2984

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-01 16:00:14 -04:00
committed by GitHub
parent 4df6d884b3
commit 628648d63a
15 changed files with 2284 additions and 221 deletions

View File

@@ -68,6 +68,34 @@ Pure Agent Skills (A alone) and pure MCP (D alone) were rejected as the foundati
**Amended by #2929 (Phase 2).** This ADR originally specified the contract as "priority + binary-search cutoff to a per-runtime budget". Implementing Phase 2 established that a cutoff alone **cannot express the function this platform generalizes**: `prompt-budget.applyBudget` is not a cutoff but a fixed five-step ladder in which each section carries its own shrink strategy, and only three of its eight sections are ever droppable — `PROJECT.md` is head-shrunk to N lines and plans are proportionally tail-truncated with a per-plan floor, while instructions and roadmap are never trimmed at all. A cutoff composer sorts by priority and discards the tail; it has no way to say "shrink this one", "truncate that one but never below its floor", or "these three are the only droppables, in this order". Building to the literal wording and routing `prompt-budget` through it would have silently changed review-prompt output. Shrink strategies are therefore the core abstraction, and **binary-search cutoff becomes one strategy among them** — the right one for per-runtime emission in Phases 3-4, not for this ladder. Ordering is declaration order rather than a numeric priority field. This is an elaboration of the decision's intent, not a reversal of it.
- **Applicability grammar (added by #2930, Phase 3).** The fragment unit's `when=` attribute is deliberately a CLOSED grammar: exactly one atom from a frozen vocabulary — `always`, `flag:--wave`, `state:gap-closure-phase`, `state:has-prior-phases` — with no boolean operators, negation, or nesting, and an unknown `when=` value throws rather than being ignored. This is a Greenspun's-Tenth-Rule guard: left open-ended, `when=` acquires `&&`/`!`/precedence/runtime-capability predicates and becomes an ad-hoc, informally-specified predicate language grown one condition at a time. Widening the vocabulary requires a coordinated ADR amendment, not an organic edit. `when=` is parsed and validated in Phase 3 but not yet acted on; applicability selection is Phase 5.
- **Budget unit:** bytes for emission caps (matches `lfByteCount`, deterministic, offline-safe); a token estimate for run-time selection.
**Corrected by #2931 (Phase 4) — the Windsurf cap was never load-bearing.** The Context
section above states that the one true emission-time cap, Windsurf's 12,000-byte limit,
"is a hard `throw` with no graceful fallback", and Phase 4 inherited that as "Windsurf
installs that currently hard-fail will succeed". Measured on `next` at `640eaee16`, that
is false. `convertClaudeCommandToWindsurfWorkflow` emits a **stub** — a title, a
one-line description, and an `@`-reference to the real command body — not an inlined
workflow. Across all 71 `commands/gsd/*.md` the largest emission is **304 bytes against
the 12,000-byte cap: 11,696 bytes of headroom, zero commands over.** Reaching the throw
requires a single frontmatter `description` field of ~11.7 KB.
`capabilities/windsurf/capability.json` confirms this is the only `commands` converter
for Windsurf (`destSubpath: workflows`).
This was wrong at authoring rather than expired: `fc2a7c055` (2026-06-23) introduced
**both** the stub and the throw in a single commit, one day before this ADR was written
(2026-06-24). The throw has never guarded a full body.
Two consequences. First, epic user story 1 — "a solo developer on a capped runtime can
install and run GSD without hitting size limits" — was **already satisfied** before this
epic began, because Windsurf already uses the stub + `@-ref` progressive-disclosure model
this ADR's Decision item 3 describes. Second, the real gap is narrower and was previously
unstated: **nothing anywhere measures an emitted artifact against its host's declared
limit.** Phase 4 closes that, and does not "fix Windsurf". The throw is removed in favor
of truncating the description — the same bound its sibling
`convertClaudeCommandToWindsurfSkill` already applied — which makes the cap unreachable
by construction and leaves the 12,000 constant in exactly one place: the guard table.
That eliminates the `DEFECT.GENERATIVE-FIX` dual-surface duplication this ADR flags,
rather than adding a parity test for it.
- **Determinism + drift-guard:** every generated artifact follows the universal `--check`/`--write` idiom and is committed; any constant shared between two surfaces gets a `DEFECT.GENERATIVE-FIX` parity assertion. Caps are asserted on **emitted per-runtime bytes** via real spawn-install tests (engine-direct tests are false-green for install behavior).
- **Boundary coverage:** the composer's budget logic is tested at `cap-1 / cap / cap+1` per `RULESET.TESTS.boundary-coverage`.
@@ -80,6 +108,19 @@ Sequenced to de-risk — prove the pattern on the smallest surface first, scale
3. **Lift `prompt-budget.cts`** out of the review silo into a shared `context-composer` seam with fast-check property tests + boundary coverage.
4. **Pilot fragmentization on one XL workflow** (`plan-phase.md` or `execute-phase.md`): split into priority-tagged sections + applicability; composer emits per-runtime; prove byte-identical-or-smaller output and green `gsd-test` docker.
5. **Move caps from source to emitted output**; turn the Windsurf `throw` into graceful auto-trim; auto-regenerate size baselines on intentional edits.
**Superseded in part by ADR-2719 (#2724), which landed after this ADR.** There are no
size baselines left to auto-regenerate: `tests/workflow-size-baseline.json`,
`tests/agent-size-baseline.json`, `scripts/update-size-baseline.cjs` and
`npm run size:baseline` were all deleted, and the differential attribution check is now
the sole gate (`RULESET.EMITTED_ATTRIBUTION`). ADR-2719 also already moved *hash*
propagation to emitted per-runtime artifacts across 19 manifests. What it did **not**
move is the size ratchet, which still keys on source dirs (`currentSizes` reads
`gsd-core/workflows/*.md` and `agents/*.md` by bare filename). Phase 4 therefore adds an
absolute per-runtime **cap** over emitted bytes — reusing ADR-2719's existing
spawn-install walk — and deliberately leaves the growth ratchet source-keyed: re-keying
it onto the 8,529 emitted paths would turn one acknowledgment per edited file into
roughly nineteen, which is how a gate becomes something contributors route around.
6. **Wire the init bundle (C)** to emit a per-invocation sections manifest; workflows consume it.
7. **Roll out across LARGE/XL tiers**; update INVENTORY families + parity tests.
8. **(Deferred)** MCP served catalog (ADR-857 §7 / #956).
@@ -96,6 +137,23 @@ Sequenced to de-risk — prove the pattern on the smallest surface first, scale
**Negative / risks**
- Trimming a load-bearing fragment is a correctness hazard (history: paraphrased `META.RULE` → agent violations). Mitigate with `flexReserve` floors, a Promptfoo-style eval gate, and boundary tests.
**Amended by #2931 (Phase 4) — the eval gate is deterministic, not model-graded.** A
*blocking* CI gate driven by exogenously-graded LLM judgment, as Phase 4 originally
worded it, contradicts two recorded decisions: `PROBE.ci.surface` — "the contract
(parse/validate, projection round-trip, fail-closed guards), **NEVER the LLM judgment**"
(ADR-550 D5) — and `PROHIB.judgment-tier` — "never-silent / never-hard-halt soft gate"
(ADR-550 D4). `PROHIB.recall` further records that there is no compiled prohibition-probe
recall engine to source an assertion set from; the `PROHIB.*`/`PROBE.*` classes describe
the *architecture* of that subsystem, not a corpus of prohibitions about workflow content.
The gate therefore asserts the **contract**, which is both blocking and deterministic:
`composeWithinBudget` already returns `omitted`, `shrunk`, `floored` and `isolatePrefix`,
so the gate proves no fragment declared load-bearing was omitted or shrunk, that a
floored fragment is a success rather than a finding, and that the `isolate` prefix
survives byte-identical. It carries an explicit anti-vacuity rule — an empty
load-bearing set fails, because a gate asserting over nothing proves nothing. No model
participates. This satisfies the mitigation this section asks for while honoring D4/D5.
- Per-runtime emission multiplies artifacts across the 15 × N matrix (inventory/parity surface).
- Build-order fragility (must run after `build:lib`).
- Dual-surface drift if any future MCP channel is added — requires parity assertions.