* 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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user