diff --git a/.changeset/curious-deer-cheer.md b/.changeset/curious-deer-cheer.md index bc5c20d82..334d3191a 100644 --- a/.changeset/curious-deer-cheer.md +++ b/.changeset/curious-deer-cheer.md @@ -2,4 +2,4 @@ type: Added pr: 2730 --- -**`npm run regen:derived` regenerates every derived artifact in one command, and a `gsd-regen` merge driver ends hand-resolving the generated parity manifests** — the golden install-parity fixtures and the workflow/agent size baselines are pure functions of the source tree, so neither side of a merge conflict on them is ever correct. Run `npm run setup:merge-driver` once per clone and conflicts on those files resolve to your branch's copy with a one-line notice; `npm run regen:derived` then recomputes them, replacing seven separate invocations with one dependency-ordered command over twelve generators. (#2721) +**`npm run regen:derived` regenerates every derived artifact in one command** — replacing several separate invocations (`build`, `gen:registry`, `gen-adr-index`, `gen-capability-matrix`, `gen-inventory-manifest`, `sync-manifest-versions`, `gen:install-tree`) with one dependency-ordered command. (#2721) diff --git a/.changeset/sturdy-seals-fly.md b/.changeset/sturdy-seals-fly.md new file mode 100644 index 000000000..ecc070e9c --- /dev/null +++ b/.changeset/sturdy-seals-fly.md @@ -0,0 +1,5 @@ +--- +type: Removed +pr: 2767 +--- +**`npm run gen:golden`, `UPDATE_GOLDEN`, `npm run size:baseline`, and `npm run setup:merge-driver` are removed** — the committed golden-install-parity fixtures and the two per-file size baselines they regenerated are deleted. The differential attribution check (`tests/emitted-attribution.test.cjs`) is now the sole gate for both emitted-content propagation and workflow/agent size growth; editing shipped content requires zero manual fixture regeneration. `npm run regen:derived` and `npm run gen:install-tree` are unaffected. (#2724) diff --git a/.gitattributes b/.gitattributes index 55e515071..a2d53626f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,25 +11,15 @@ *.ttf binary *.pdf binary -# --- Emitted Artifact Provenance (#2721, ADR-2719 Phase 1) ------------------- +# The `merge=gsd-regen` driver + linguist-generated block that used to live here +# (#2721, ADR-2719 Phase 1) covered tests/fixtures/golden-install-parity/*.json, +# tests/workflow-size-baseline.json, and tests/agent-size-baseline.json — all +# three deleted by #2724 (ADR-2719 Phase 4), which retired the bridge driver too. +# The differential attribution check (tests/emitted-attribution.test.cjs) is the +# sole gate now; it has no committed artifact to conflict on, so there is nothing +# left for a merge driver to resolve. # -# These artifacts are pure functions of the source tree. Git can offer ours or -# theirs; both are wrong, because the only correct value is recomputed from the -# merged tree — so `merge=gsd-regen` keeps your branch's copy and tells you to -# run `npm run regen:derived`, instead of asking you to hand-merge 7,500 lines -# of hex. `linguist-generated` stops GitHub rendering them expanded in diffs. -# -# BRIDGE — DELETE THIS BLOCK IN #2724, which removes these files entirely. -# Register the driver in your clone with: npm run setup:merge-driver -# -# Declared by exact path, never by a `tests/*-size-baseline.json` glob: a glob -# would silently capture any future baseline that had not been reasoned about. -tests/fixtures/golden-install-parity/*.json merge=gsd-regen linguist-generated=true -tests/workflow-size-baseline.json merge=gsd-regen linguist-generated=true -tests/agent-size-baseline.json merge=gsd-regen linguist-generated=true - -# tests/fixtures/install-tree/*.json is deliberately ABSENT from the block above. -# ADR-2719 §7 keeps it committed and normally-merged: it conflicts on 0 of 7, its -# diffs are readable, and it preserves "the installer stopped shipping X" as a -# hard absolute failure with no attribution reasoning involved. Capturing it here -# would silently convert that absolute into an auto-resolve. +# tests/fixtures/install-tree/*.json was, and remains, deliberately ABSENT from +# that block: ADR-2719 §7 keeps it committed and normally-merged (0 of 7 +# conflicts, readable diffs, preserves "the installer stopped shipping X" as a +# hard absolute failure with no attribution reasoning involved). diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4837f56f2..a04aa2e00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -186,6 +186,24 @@ jobs: - name: Dependency integrity gate run: node scripts/check-npm-integrity.cjs + # #2724 (ADR-2719 §5): the differential attribution check's baseline is cached, + # not committed. This restores what the push-to-next job published, keyed on + # the PR's base sha — the "next sha the PR was merged with" the ADR requires. + # A miss is NOT a failure here: resolveBaseline() degrades to an in-job build. + - name: Restore emitted-baseline cache + if: github.event_name == 'pull_request' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .gsd-cache/emitted-baseline.json + key: emitted-baseline-${{ github.event.pull_request.base.sha }} + + - name: Export GSD_EMITTED_BASELINE when the cache restored + if: github.event_name == 'pull_request' + # No `shell:` override — H1 shell policy requires each job's native shell + # (pwsh on Windows, zsh on macOS test-full, bash on Linux). A bare `node` + # invocation has no shell-specific syntax, so it is valid under all three. + run: node scripts/ci-export-emitted-baseline-env.cjs + - name: Prepare scoped test list if: matrix.scope != 'full' env: @@ -411,6 +429,23 @@ jobs: - name: Dependency integrity gate run: node scripts/check-npm-integrity.cjs + # #2724 (ADR-2719 §5): restore the differential attribution check's cached + # baseline, keyed on the PR's base sha. A miss degrades to an in-job build + # rather than failing (resolveBaseline()'s documented precedence). + - name: Restore emitted-baseline cache + if: github.event_name == 'pull_request' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .gsd-cache/emitted-baseline.json + key: emitted-baseline-${{ github.event.pull_request.base.sha }} + + - name: Export GSD_EMITTED_BASELINE when the cache restored + if: github.event_name == 'pull_request' + # No `shell:` override — H1 shell policy requires each job's native shell + # (pwsh on Windows, zsh on macOS test-full, bash on Linux). A bare `node` + # invocation has no shell-specific syntax, so it is valid under all three. + run: node scripts/ci-export-emitted-baseline-env.cjs + # The heavy unit suite is split across the 3 shards — each runs a # deterministic cost-balanced third of the sorted unit-file list (#2472). The # union of shards 1/3 + 2/3 + 3/3 is the full unit suite, so coverage is @@ -496,3 +531,39 @@ jobs: fi echo "Required test gate passed." + + # #2724 (ADR-2719 §5): publishes the differential attribution check's baseline + # artifact after `next` advances, keyed on the merge sha. PR lanes restore it + # (see the `test` and `test-full` jobs' "Restore emitted-baseline cache" steps), + # keyed on `pull_request.base.sha` — "the next sha the PR was merged with", the + # ADR's own phrasing. Not required by `required-tests`: a miss here degrades PR + # lanes to an in-job build (resolveBaseline()'s documented precedence) rather + # than failing them, so this job's own failure must not block `next`. + publish-emitted-baseline: + name: Publish emitted-baseline artifact + needs: [changes, test] + if: github.event_name == 'push' && github.ref == 'refs/heads/next' && needs.test.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: true + token: ${{ github.token }} + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Build emitted-baseline artifact + run: node scripts/gen-emitted-baseline.cjs --out .gsd-cache/emitted-baseline.json + + - name: Publish baseline cache (keyed on this sha) + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .gsd-cache/emitted-baseline.json + key: emitted-baseline-${{ github.sha }} diff --git a/CONTEXT.md b/CONTEXT.md index 0c13f7d0b..2b8ed3280 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -410,7 +410,7 @@ The producer half of the async external-job contract (#1164, part of #1105). Def The enforced cross-phase defect register operationalizing GSD's no-defer discipline as a tracked artifact (#1950). Markdown file at `.planning/WINDOWS.md` (project-level, cross-phase) with YAML frontmatter carrying scalar counts (`schema_version`, `open_count`, `waived_count`, `fixed_count`, `total_count`, `last_updated`) for the FAST path the gate reads via jq without parsing JSON, plus a JSON code block as the AUTHORITATIVE entries source; the two cross-check and fail closed on drift. Each entry: `{ id, kind, phase, file, line, description, status, reason, recorded_at, resolved_at }`; kinds are closed (`stub | todo | fixme | skipped-test | lint-warning | unmet-truth | unrun-verify | deviation`); statuses are closed (`open | waived | fixed`). The `broken-windows` Capability (`capabilities/broken-windows/capability.json`) registers one `ship:pre` gate with predicate `artifact-frontmatter-equals WINDOWS.md open_count == 0`; federated config key `workflow.windows_enforce` (default `false` — opt-in enforcement, tracking-only by default so a project can adopt the ledger before turning the gate on). Population is best-effort and never blocks execution: `agents/gsd-executor.md` appends stubs/skipped-tests/unrun-verifies via `gsd_run windows append` after writing SUMMARY.md. Source of truth: `src/broken-windows.cts` → `gsd-core/bin/lib/broken-windows.cjs` (pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed` + I/O `cmdWindowsStatus`/`Append`/`Waive`/`MarkFixed`); CLI surface `gsd-tools windows status|append|waive|fixed`. Ship gate enforcement is a `capId == "broken-windows"` branch in `gsd-core/workflows/ship.md` preflight (sibling to the `security` branch); it reads `gsd_run windows status --raw` and fails closed on a non-zero/non-numeric `open_count` (an unparseable ledger is itself a broken window). `/gsd:progress` surfaces the open+waived count. The ledger is optional and backward-compatible: a project with no `.planning/WINDOWS.md` reports `open_count: 0` and ships cleanly, and with `workflow.windows_enforce=false` (the default) ship never blocks on it. Frozen `REASON` enum: `WINDOWS_LEDGER_MISSING | WINDOWS_LEDGER_MALFORMED | WINDOWS_ID_NOT_FOUND | WINDOWS_ALREADY_RESOLVED | WINDOWS_WAIVE_REASON_EMPTY | WINDOWS_INVALID_KIND | WINDOWS_INVALID_FILE | WINDOWS_INVALID_ID | WINDOWS_APPEND_MISSING_FIELD | WINDOWS_USAGE | WINDOWS_OK` — surfaced through `--json-errors` for typed test assertions. Test seam: `tests/broken-windows.test.cjs`. Origin: *The Pragmatic Programmer* Topic 3 (Hunt & Thomas — software transplant of Wilson & Kelling's broken-windows metaphor) plus Cunningham's debt metaphor (decay accrues interest ⇒ accounting, not just habit). ### Emitted Artifact Provenance -Cross-seam principle (ADR-2719, epic #2719): a committed artifact that is a pure function of the source tree is not reviewable state — it is derived state wearing a review costume, and it must be *attributable* rather than *pinned*. Concept, not a Module: it ships nothing, so it takes no `Module` suffix (follows the `### Resolution Provenance` precedent). Scope is the emitted-artifact family named by `RULESET.EMITTED_ATTRIBUTION`. The principle: every emitted path whose hash moved between `next` HEAD and PR HEAD must be attributable — through a declarative provenance table — to a path the pull request actually changed; unattributable deltas are a hard failure that *names them* rather than an anomaly a reviewer must notice inside 7,500 lines of hex. Totality is enforced, so an emitted path matching no rule fails loudly instead of passing through unattributed. The escape hatch is a committed acknowledgment (`tests/emitted-drift-ack.json`), deliberately not a flag or env var — the file appears in the changed-files list ONLY when something rippled unexpectedly, so touching it IS the alarm, whereas today 100% of emitted-byte changes touch fixtures and touching them signals nothing. The same differential machine carries the size ratchet: growth is reported with exact byte deltas and needs the same acknowledgment, so anti-creep survives without pinning a number. Distinguish from the absolute check that remains: `tests/fixtures/install-tree/*.json` stays committed and normally-merged (ADR-2719 §7) because "the installer stopped shipping X" must fail with no attribution reasoning involved. Delivery is phased — #2721 naming + interim merge relief, #2722 provenance table + totality guard, #2723 differential check dual-run beside `golden-install-parity.test.cjs`, #2724 cutover. The table LANDED in #2722 as `tests/helpers/emitted-provenance.cjs` (19 rules, guarded by `tests/emitted-provenance.test.cjs`); it maps emitted path → repo source and is TOTAL over EVERY emitted path in all 19 manifests — exactly one rule per path, with zero-match, two-match, AND dead-rule (a rule matching nothing) all hard failures, so table rot is loud in both directions. Deliberately NO path/family counts are recorded here: those move with every shipped-content edit, and a hand-maintained number in glossary canon is the exact silent-drift failure this whole seam exists to end. The guard recomputes them from the fixtures on every run — read them from a failure message, never from prose. What IS stable is the rule count, which changes only when a new emitted family or host appears. Note the surface is materially wider than #2722 estimated from `claude.json` alone (its "13 families / 15-20 rules" was a single-runtime sample; the 19-manifest surface spans runtime-specific roots — `.agents/`, `.kimi/hooks/`, `command/`, `agents/subagents/`, `.clinerules/`, `plugins/`, `extensions/`, `.gsd/`, the hermes `skills/gsd/` category and the #69 nested `skills//skills//` layout). Two design invariants carry forward to #2723: emitted SHAPES are hard-coded (deriving them from the installer would make the guard tautological — it would follow any installer change silently), while source PATHS may read a first-party descriptor where that descriptor is the sole declaration (`hostBehaviors.nativePlugin.source`); and attribution is keyed on `(rel, runtime)`, never `rel` alone, because one emitted path has different sources per host (`plugins/gsd-core.js` ← `.opencode/` vs `.kilo/`). Emitted skills attribute to `commands/gsd/*.md`, NEVER the repo `skills/` dir — that dir is itself generated from `commands/gsd` by `scripts/gen-plugin-skills.cjs`, so attributing to it is false attribution that still passes totality. Totality does NOT catch a rule pointing at the WRONG source (the recorded residual); the guard against that is the companion assertion that every attributed source EXISTS in the repo, which caught three real cases while the table was built (Copilot's `.agent.md` rename, Kimi's code-literal `agents/gsd.{yaml,md}` root agent, and Copilot's `hooks/gsd-session.json`). A `sources` entry ending in `/` is a PREFIX, not a file — and prefix matching is SEGMENT-AWARE, so a source of `agents/` must not attribute `agentsfoo/x.md`. The differential check LANDED in #2723 as `tests/helpers/emitted-diff.cjs` (the conservation law, a PURE function — no fs/git/installer/clock) + `tests/helpers/emitted-baseline.cjs` (baseline resolution), guarded by `tests/emitted-attribution.test.cjs`, running DUAL beside `golden-install-parity.test.cjs` with both green and fixtures untouched. Purity is deliberate and load-bearing: the naive one-big-integration-test shape would need ~38 installer spawns per assertion, so the four failing-first criteria would not in practice get written — which is exactly how a phase ships promised-but-not-built. Buckets are CONSERVED: every moved emitted path lands in exactly one of `attributed | unattributable | acked` (property-tested), and a path the provenance table cannot resolve surfaces as an ERROR rather than a silent skip. Four asymmetries worth knowing: an ADDED emitted key is a ripple too (not just modified ones); `synthesized` paths are exempt but `code-derived` ones are NOT (that is why Phase 2 refused to mark them exempt — exempt means permanently blind); SHRINKAGE needs no ack while growth does (gating shrinkage would punish what the ratchet wants); and a STALE ack is a hard failure, because an ack outliving its ripple pre-clears the next one on that path. `tests/emitted-drift-ack.json` (absent = no acks; its PRESENCE is the alarm) requires a non-empty `reason` per path — "name them and say why" is the contract, and a document that parses but is not an object is rejected rather than read as "no acks", which would silently disarm the gate. Baseline is CACHED not committed, keyed on the `next` sha; a stale key is REFUSED, never used — absence fails loudly and gets fixed, whereas staleness produces a confident wrong answer. An explicitly-pointed-at (`GSD_EMITTED_BASELINE`) stale baseline is a hard stop, while a stale CACHE falls through to the in-job build. No baseline-unavailable path may `return` (in `node:test` that is a PASS, not a skip — ADR-2719 §6). Supersedes ADR-2264 §2–§4 and its Amendment; ADR-2264 Phase 1 (`buildParityManifest` and the exclusion constants in `tests/helpers/install-shared.cjs`) is retained and depended upon. +Cross-seam principle (ADR-2719, epic #2719): a committed artifact that is a pure function of the source tree is not reviewable state — it is derived state wearing a review costume, and it must be *attributable* rather than *pinned*. Concept, not a Module: it ships nothing, so it takes no `Module` suffix (follows the `### Resolution Provenance` precedent). Scope is the emitted-artifact family named by `RULESET.EMITTED_ATTRIBUTION`. The principle: every emitted path whose hash moved between `next` HEAD and PR HEAD must be attributable — through a declarative provenance table — to a path the pull request actually changed; unattributable deltas are a hard failure that *names them* rather than an anomaly a reviewer must notice inside 7,500 lines of hex. Totality is enforced, so an emitted path matching no rule fails loudly instead of passing through unattributed. The escape hatch is a committed acknowledgment (`tests/emitted-drift-ack.json`), deliberately not a flag or env var — the file appears in the changed-files list ONLY when something rippled unexpectedly, so touching it IS the alarm, whereas today 100% of emitted-byte changes touch fixtures and touching them signals nothing. The same differential machine carries the size ratchet: growth is reported with exact byte deltas and needs the same acknowledgment, so anti-creep survives without pinning a number. Distinguish from the absolute check that remains: `tests/fixtures/install-tree/*.json` stays committed and normally-merged (ADR-2719 §7) because "the installer stopped shipping X" must fail with no attribution reasoning involved. Delivery was phased — #2721 naming + interim merge relief, #2722 provenance table + totality guard, #2723 differential check dual-run beside `golden-install-parity.test.cjs`, #2724 cutover (COMPLETE: the dual-run window observed agreement on real PRs after fixing #2750/#2760, and the golden fixtures/test/generator/merge-driver bridge are now deleted; the differential is the sole gate). The table LANDED in #2722 as `tests/helpers/emitted-provenance.cjs` (19 rules, guarded by `tests/emitted-provenance.test.cjs`); it maps emitted path → repo source and is TOTAL over EVERY emitted path in all 19 manifests — exactly one rule per path, with zero-match, two-match, AND dead-rule (a rule matching nothing) all hard failures, so table rot is loud in both directions. Deliberately NO path/family counts are recorded here: those move with every shipped-content edit, and a hand-maintained number in glossary canon is the exact silent-drift failure this whole seam exists to end. The guard recomputes them from the fixtures on every run — read them from a failure message, never from prose. What IS stable is the rule count, which changes only when a new emitted family or host appears. Note the surface is materially wider than #2722 estimated from `claude.json` alone (its "13 families / 15-20 rules" was a single-runtime sample; the 19-manifest surface spans runtime-specific roots — `.agents/`, `.kimi/hooks/`, `command/`, `agents/subagents/`, `.clinerules/`, `plugins/`, `extensions/`, `.gsd/`, the hermes `skills/gsd/` category and the #69 nested `skills//skills//` layout). Two design invariants carry forward to #2723: emitted SHAPES are hard-coded (deriving them from the installer would make the guard tautological — it would follow any installer change silently), while source PATHS may read a first-party descriptor where that descriptor is the sole declaration (`hostBehaviors.nativePlugin.source`); and attribution is keyed on `(rel, runtime)`, never `rel` alone, because one emitted path has different sources per host (`plugins/gsd-core.js` ← `.opencode/` vs `.kilo/`). Emitted skills attribute to `commands/gsd/*.md`, NEVER the repo `skills/` dir — that dir is itself generated from `commands/gsd` by `scripts/gen-plugin-skills.cjs`, so attributing to it is false attribution that still passes totality. Totality does NOT catch a rule pointing at the WRONG source (the recorded residual); the guard against that is the companion assertion that every attributed source EXISTS in the repo, which caught three real cases while the table was built (Copilot's `.agent.md` rename, Kimi's code-literal `agents/gsd.{yaml,md}` root agent, and Copilot's `hooks/gsd-session.json`). A `sources` entry ending in `/` is a PREFIX, not a file — and prefix matching is SEGMENT-AWARE, so a source of `agents/` must not attribute `agentsfoo/x.md`. The differential check LANDED in #2723 as `tests/helpers/emitted-diff.cjs` (the conservation law, a PURE function — no fs/git/installer/clock) + `tests/helpers/emitted-baseline.cjs` (baseline resolution), guarded by `tests/emitted-attribution.test.cjs`. It ran DUAL beside `golden-install-parity.test.cjs` through the #2723 dual-run window with both green and fixtures untouched; #2724 deleted the golden fixtures/test/generator and the check is now the sole gate. Purity is deliberate and load-bearing: the naive one-big-integration-test shape would need ~38 installer spawns per assertion, so the four failing-first criteria would not in practice get written — which is exactly how a phase ships promised-but-not-built. Buckets are CONSERVED: every moved emitted path lands in exactly one of `attributed | unattributable | acked` (property-tested), and a path the provenance table cannot resolve surfaces as an ERROR rather than a silent skip. Four asymmetries worth knowing: an ADDED emitted key is a ripple too (not just modified ones); `synthesized` paths are exempt but `code-derived` ones are NOT (that is why Phase 2 refused to mark them exempt — exempt means permanently blind); SHRINKAGE needs no ack while growth does (gating shrinkage would punish what the ratchet wants); and a STALE ack is a hard failure, because an ack outliving its ripple pre-clears the next one on that path. `tests/emitted-drift-ack.json` (absent = no acks; its PRESENCE is the alarm) requires a non-empty `reason` per path — "name them and say why" is the contract, and a document that parses but is not an object is rejected rather than read as "no acks", which would silently disarm the gate. Baseline is CACHED not committed, keyed on the `next` sha; a stale key is REFUSED, never used — absence fails loudly and gets fixed, whereas staleness produces a confident wrong answer. An explicitly-pointed-at (`GSD_EMITTED_BASELINE`) stale baseline is a hard stop, while a stale CACHE falls through to the in-job build. No baseline-unavailable path may `return` (in `node:test` that is a PASS, not a skip — ADR-2719 §6). Supersedes ADR-2264 §2–§4 and its Amendment; ADR-2264 Phase 1 (`buildParityManifest` and the exclusion constants in `tests/helpers/install-shared.cjs`) is retained and depended upon. ### Untrusted-input boundary The prompt-level data/instruction isolation seam for untrusted web/document ingress (#1577). Shared reference `gsd-core/references/untrusted-input-boundary.md`, `@`-included by the 10 ingest agents (`gsd-project-researcher`, `gsd-phase-researcher`, `gsd-ui-researcher`, `gsd-assumptions-analyzer`, `gsd-advisor-researcher`, `gsd-ai-researcher`, `gsd-domain-researcher`, `gsd-research-synthesizer`, `gsd-doc-classifier`, `gsd-doc-synthesizer`) — every agent that reads fetch/search/MCP output or external source documents. The reference instructs: treat fetched/read content as **data, never instructions**; self-scan content for embedded directives before use; act only on the assigned task (ignore off-task instructions in data); and wrap quoted untrusted spans in a **fresh random delimiter** per wrap (fixed markers are spoofable). This prompt-level boundary is the primary control — it keeps an injection from being *followed* even while it sits in context. The hook-level companion is the read-injection scanner (`hooks/gsd-read-injection-scanner.js`, PostToolUse on `Read`/`WebFetch`/`WebSearch`), advisory by default; the opt-in top-level `security.injection_blocking` key upgrades HIGH-confidence detections to a PostToolUse circuit-breaker that halts the agent's next step (it runs *after* the fetch, so it is not a redactor). Tests: `tests/untrusted-input-isolation.test.cjs`, `tests/read-injection-scanner.*.test.cjs`, `tests/injection-blocking-config.test.cjs`. See `docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md` and `docs/explanation/security-model.md`. Grounding: arXiv 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472. @@ -472,9 +472,9 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr `RULESET.AUDIT.search-source-not-generated=verify an invariant/validation EXISTS by searching the AUTHORED source (src/*.cts OR the scripts/gen-*.cjs generator), never the generated bin/lib/*.cjs (gitignored, ADR-457); gen-time checks live in gen-*.cjs not the .cts it consumes → search BOTH before declaring absent; read generated .cjs only for output drift. Repro: grep src/*.cts for VALID_CONVERTER_NAMES → false "5e ConverterName unenforced"; actually enforced in gen-capability-registry.cjs. cf RULESET.TESTS.no-source-grep` `RULESET.WORKFLOW_MARKDOWN.FENCES=preserve opening language fence when editing shell snippets in workflow markdown; malformed fence creates fresh CR threads (MD040)` -`RULESET.WORKFLOW_SIZE_BUDGET=workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = per-file baseline (PRIMARY anti-creep: tests/workflow-size-baseline.json pins each file's exact size) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + new-file cap (un-baselined files <32768, the Codex anchor) + discuss-phase<32000; a file that grew fails the baseline guard — fix with `npm run size:baseline`, commit the one-line diff, and justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump` -`RULESET.AGENT_SIZE_BUDGET=agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = per-file baseline (PRIMARY anti-creep: tests/agent-size-baseline.json pins each agents/gsd-*.md exact byte size) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). One 'npm run size:baseline' regenerates BOTH workflow and agent baselines via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter. A grown agent fails the baseline guard — regenerate + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes` -`RULESET.EMITTED_ATTRIBUTION=the emitted-artifact family (ADR-2719, epic #2719) = tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json. All three are PURE FUNCTIONS of the source tree, so their correct merge is ALWAYS "recompute", which git's ours/theirs interface cannot express — 140 of 143 conflicted-file instances across the open PR queue are these files, and 3 of 7 conflicting PRs have ZERO overlapping keys (pure line-adjacency false conflicts in a sorted map). Amplification: gsd-core/workflows|references|templates|contexts is copied to every host, so ONE source edit rewrites the same hash in all 19 manifests (19× the change). REGENERATE with `npm run regen:derived` (single command replacing seven invocations, dependency-ordered, gen:golden LAST because it hashes installed output); never hand-merge. It covers TWELVE generators, not the eleven #2721 enumerates: `sync-manifest-versions` is folded in because `lint:generated-sync` checks it too, and a command that claims to regenerate everything must not leave that gate red. Phase 1 (#2721) relief = `merge=gsd-regen` in .gitattributes + scripts/git-merge-regen-driver.cjs, registered per-clone via `npm run setup:merge-driver`; the driver accepts OURS and does NOT regenerate, because at merge-driver time neither the working tree nor the index reflects the merge (proven) — regenerating there would emit a plausible-but-wrong manifest. It removes the labour, NOT the GitHub CONFLICTING label (drivers live in .git/config; forks lack it, github.com never runs them). BRIDGE ONLY: #2724 deletes the artifacts and retires the driver. tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves "the installer stopped shipping X" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`` +`RULESET.WORKFLOW_SIZE_BUDGET=workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4: tests/emitted-attribution.test.cjs's real-tree test reports growth in any gsd-core/workflows/*.md with its exact byte delta vs `next`, no committed snapshot, requires a tests/emitted-drift-ack.json entry) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + discuss-phase<32000; a file that grew fails the differential guard — add an ack entry naming the file and reason, justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump. The prior per-file baseline (tests/workflow-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724. Its new-file cap (ADR-1610 Decision point 3, un-baselined files <=32768, the Codex anchor) is REVIVED inside the differential's size ratchet itself (`NEW_FILE_CAP` in tests/helpers/emitted-diff.cjs) rather than lost: "not yet baselined" is exactly "present in sizeCurrent, absent from sizeBaseline", a signal the ratchet already computes for its own reasons. NOT ack-able — same as the tier hard caps, the fix is extraction. Narrower than the original: this check cannot see XL/LARGE tiering (tests/workflow-size-budget.test.cjs's classification, invisible to the pure differential module), so a legitimately large NEW file must extract rather than tier in, one release earlier than an existing file would need to — a disclosed, deliberate simplification` +`RULESET.AGENT_SIZE_BUDGET=agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = differential attribution size ratchet (PRIMARY anti-creep since #2724/ADR-2719 §4, same mechanism and same tests/emitted-drift-ack.json as WORKFLOW_SIZE_BUDGET, scoped to agents/gsd-*.md) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). Sizes are measured via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter (tests/helpers/emitted-runtime.cjs's currentSizes() and the guard's own tier-cap checks both import it). A grown agent fails the differential guard — ack + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes. The prior per-file baseline (tests/agent-size-baseline.json, `npm run size:baseline`) is REMOVED by #2724` +`RULESET.EMITTED_ATTRIBUTION=the emitted-artifact family (ADR-2719, epic #2719) — POST-CUTOVER (#2724, Phase 4). Historically tests/fixtures/golden-install-parity/*.json (19 path→hash manifests) + tests/workflow-size-baseline.json + tests/agent-size-baseline.json were all committed, PURE FUNCTIONS of the source tree whose correct merge was ALWAYS "recompute" — 140 of 143 conflicted-file instances across the open PR queue were these files. #2724 DELETES all three, the golden test (tests/golden-install-parity.test.cjs), the generator (scripts/gen-golden-install-parity-zcode.cjs), `npm run gen:golden`, `UPDATE_GOLDEN`, the merge-driver bridge (scripts/git-merge-regen-driver.cjs, `npm run setup:merge-driver`, the .gitattributes merge=gsd-regen block), and scripts/update-size-baseline.cjs (`npm run size:baseline`). The differential attribution check (tests/emitted-attribution.test.cjs + tests/emitted-provenance.test.cjs) is now the SOLE gate for emitted-artifact propagation AND size growth — no committed artifact, nothing to hand-merge, nothing to regenerate. `npm run regen:derived` still exists for what remains committed and derived: build, registry, ADR index, capability matrix, inventory manifest, manifest versions, and `tests/fixtures/install-tree/*.json` (now `npm run gen:install-tree`, folded into `regen:derived`). tests/fixtures/install-tree/*.json is DELIBERATELY EXCLUDED from the cutover (ADR-2719 §7): it conflicts on 0 of 7, its diffs are readable, and it preserves "the installer stopped shipping X" as a hard absolute failure — capturing it would convert that absolute into an attribution-free auto-resolve. The baseline the differential compares against is now published by `scripts/gen-emitted-baseline.cjs` on every push to `next` (cached, keyed on sha) and restored in PR lanes via `GSD_EMITTED_BASELINE`/`resolveBaseline()` (tests/helpers/emitted-baseline.cjs); a cache miss falls back to an in-job build via a throwaway `git worktree` (tests/helpers/emitted-runtime.cjs's `buildBaselineAtRef`). cf `RULESET.WORKFLOW_SIZE_BUDGET`, `RULESET.AGENT_SIZE_BUDGET`; see `### Emitted Artifact Provenance`` `RULESET.WORKFLOW_FILE_NAMES=workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name` `RULESET.WORKFLOW_EXECUTION_CONTEXT=@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/docs-update.test.cjs (folds former \`bug-3135-capture-backlog-workflow\`, consolidation epic #1969); INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; "Invoked by" attribution must move when a flag absorbs a micro-skill` `RULESET.WORKFLOW_EXECUTE_END_TO_END=standard for single-workflow commands is "Execute end-to-end." (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses "execute the X workflow end-to-end." in routing bullets — convention verified live across ~20 commands/gsd/*.md files; no ADR currently documents this specific phrasing rule (ADR-0002 covers the adjacent but distinct command-contract/@-ref-resolution seam, not this convention)` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15e2fa71d..d1e9cbb62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -763,27 +763,33 @@ npm run check:alias-drift This verifies generated alias artifacts are in sync with manifest source-of-truth. -### Generated artifacts that conflict on every merge +### Editing shipped content (gsd-core/workflows, references, templates, contexts, agents/, commands/gsd/) -If your PR conflicts on `tests/fixtures/golden-install-parity/*.json`, -`tests/workflow-size-baseline.json`, or `tests/agent-size-baseline.json`, **do not -hand-resolve them.** They are pure functions of the source tree, so neither "ours" -nor "theirs" is correct — the only correct value is recomputed. +Editing the content of a copied shipped file — a `gsd-core/workflows/*.md`, an agent, a +command definition — requires **zero manual fixture regeneration**. There is no +committed path→hash manifest or per-file size baseline to update by hand; the +differential attribution check (`tests/emitted-attribution.test.cjs`, ADR-2719) computes +what your PR changed against `next` and requires every emitted-artifact hash that moved +to be attributable to your diff. If it is not, the check fails and names the paths. + +Legitimate cases where emitted bytes move for a reason your diff cannot show directly — +a converter change, for example — go through `tests/emitted-drift-ack.json` (name the +path, say why); see `CONTEXT.md`'s `### Emitted Artifact Provenance` entry for the full +model. Growth in a `gsd-core/workflows/*.md` or `agents/gsd-*.md` file is reported with +its exact byte delta and needs the same acknowledgment; the outer tier hard caps in +`tests/workflow-size-budget.test.cjs` / `tests/agent-size-budget.test.cjs` are unaffected +and still apply. + +`npm run regen:derived` still exists for the artifacts that ARE committed and derived — +`sync-manifest-versions`, the ADR index, the capability matrix, the inventory manifest, +the registry, and `tests/fixtures/install-tree/*.json` (`npm run gen:install-tree`, the +one fixture family ADR-2719 §7 keeps committed, because it conflicts on 0 of 7 and its +diffs are readable). Run it after a change to any of those, before committing: ```bash -npm run setup:merge-driver # once per clone: conflicts resolve to your copy -npm run regen:derived # then recompute, before committing +npm run regen:derived ``` -`regen:derived` replaces the seven separate invocations this used to take (`npm run -build` plus six more), and runs them in dependency order — `gen:golden` last, because it -hashes installed output. It covers twelve generators: the eleven that produce committed -artifacts, plus `sync-manifest-versions`, which `npm run lint:generated-sync` also checks — -without it, the command that claims to regenerate everything could still leave that gate red. -Full guide, including what the driver deliberately does not do: -[docs/TESTING-SUITES.md](docs/TESTING-SUITES.md) → "the baselines or golden fixtures -conflict on merge". - Optional local pre-commit hook entry (Git-native): ```bash @@ -909,16 +915,16 @@ gsd-core/ the canonical example (the discuss-phase/modes split, #717). New modes for discuss-phase land in workflows/discuss-phase/modes/.md. - Per-file sizes are pinned by a committed baseline - (tests/workflow-size-baseline.json) plus loose tier - hard caps, both in tests/workflow-size-budget.test.cjs. - If you legitimately grow or shrink a workflow file, - run `npm run size:baseline` to update the snapshot and - justify any growth in your PR (or extract content - lazily). The same guard covers agent files - (agents/gsd-*.md). Full how-to + reference in - docs/TESTING-SUITES.md (Workflow & agent size - budget); see issue #1074. + Per-file growth is caught by the differential + attribution check (tests/emitted-attribution.test.cjs, + ADR-2719) — it reports the exact byte delta and + requires an entry in tests/emitted-drift-ack.json, + no committed snapshot to regenerate. Loose tier + hard caps remain in tests/workflow-size-budget.test.cjs. + The same applies to agent files (agents/gsd-*.md, + tests/agent-size-budget.test.cjs). Full how-to + + reference in docs/TESTING-SUITES.md (Workflow & + agent size budget); see issue #1074. references/ — Reference documentation (.md) templates/ — File templates agents/ — Agent definitions (.md) — CANONICAL SOURCE diff --git a/docs/TESTING-SUITES.md b/docs/TESTING-SUITES.md index 831e4fd2a..1eb6babf6 100644 --- a/docs/TESTING-SUITES.md +++ b/docs/TESTING-SUITES.md @@ -70,107 +70,69 @@ both ship in the installed runtime and are loaded into context — workflows on every command, agents on every subagent dispatch — so their byte size is a real cost. Two sibling guards (`tests/workflow-size-budget.test.cjs` and `tests/agent-size-budget.test.cjs`) keep that cost from creeping up invisibly, -sharing one byte-counter (`measureMdFiles`) and one `npm run size:baseline` -command that regenerates **both** snapshots. Each is an **anti-creep ratchet**, -sibling to the regression-name ratchet above — three layers (workflows), ordered -from day-to-day to last-resort: +sharing one byte-counter (`measureMdFiles`). Growth is caught by two independent +layers: | Layer | What it does | Where | |---|---|---| -| **Per-file baseline** (primary) | Pins every workflow's *exact* current size in a committed snapshot. Any growth, shrink, add, or removal fails until the snapshot is regenerated — so sub-ceiling creep is caught by name and delta, not just at the tier's single largest file. | `tests/workflow-size-baseline.json` | -| **Loose tier hard caps** (backstop) | Absolute outer red lines per tier — `XL ≤ 98304`, `LARGE ≤ 61440`, `DEFAULT ≤ 40960` bytes. Unlike the old tighten-only ceiling, a cap is **never raised** when a file approaches it: crossing it means *extract*, not bump. | `XL/LARGE/DEFAULT_CAP` | -| **New-file cap** | A workflow not yet in the baseline must stay under `32768` bytes (the Codex `project_doc_max_bytes` anchor) unless explicitly tiered into `XL_WORKFLOWS`/`LARGE_WORKFLOWS` in the same PR. Keeps net-new orchestrators from being born oversized. | `NEW_FILE_CAP` | +| **Differential attribution size ratchet** (primary, #2724 / ADR-2719 §4) | The same computed-attribution check that replaced the golden-install-parity fixtures also reports growth in any `gsd-core/workflows/*.md` or `agents/gsd-*.md` file, with the exact byte delta, comparing PR HEAD against `next`. Unacknowledged growth is a hard failure; shrinkage needs no acknowledgment. No committed snapshot — nothing to regenerate by hand. | `tests/emitted-attribution.test.cjs` (real-tree test) via `tests/helpers/emitted-diff.cjs` | +| **Loose tier hard caps** (backstop) | Absolute outer red lines per tier — workflows: `XL ≤ 98304`, `LARGE ≤ 61440`, `DEFAULT ≤ 40960` bytes; agents: `XL ≤ 57344`, `LARGE ≤ 49152`, `DEFAULT ≤ 24576` bytes. A cap is **never raised** when a file approaches it: crossing it means *extract*, not bump. Independent of the ratchet above — unaffected by #2724. | `XL/LARGE/DEFAULT_CAP` in each guard file | `discuss-phase.md` additionally has a thin-dispatcher target of `< 32000` bytes -(the discuss-phase progressive-disclosure split, #717). - -**Agents** (`tests/agent-size-budget.test.cjs`) use the same per-agent baseline -(`tests/agent-size-baseline.json`) + loose tier hard caps — `XL ≤ 57344` / -`LARGE ≤ 49152` / `DEFAULT ≤ 24576` bytes. There is no new-agent cap: a net-new -agent is DEFAULT-tier and already bounded by the DEFAULT cap. (This is distinct -from the separate 45 KB-*char* extraction-evidence threshold on `gsd-planner` -enforced by `tests/planner-decomposition.test.cjs` — that one proves mode -sections were extracted; this one bounds total agent bytes.) +(the discuss-phase progressive-disclosure split, #717). A net-new agent is +DEFAULT-tier and already bounded by the DEFAULT cap — no separate new-agent cap +is needed. (This tier-cap machinery is distinct from the separate 45 KB-*char* +extraction-evidence threshold on `gsd-planner` enforced by +`tests/planner-decomposition.test.cjs` — that one proves mode sections were +extracted; this one bounds total agent bytes.) ### How-to: a workflow or agent grew and CI is red -The baseline guard reports the file and the byte delta (the same flow for both -the workflow and agent guards). To resolve: +The differential attribution check reports the file and the byte delta. To resolve: -1. **Regenerate the snapshot** and inspect the one-line diff: - ```bash - npm run size:baseline - git diff tests/workflow-size-baseline.json - ``` -2. **Justify the growth in your PR** (a sentence in the description is enough) — - the committed baseline diff is the review record that the larger size was a - deliberate, seen decision, not silent drift. -3. **Or shrink it instead of baselining.** Prefer extraction when the growth is - incidental: for a workflow, move per-mode bodies to `workflows//modes/`, - templates to `workflows//templates/`, and shared prose to - `gsd-core/references/`; for an agent, lift shared boilerplate into - `gsd-core/references/` and `@`-reference it — then load it **LAZILY**. Do *not* convert them to eager `@-required_reading` - includes: that shrinks the file's bytes without shrinking loaded context, so - it games the guard while making the real cost worse. See - `workflows/discuss-phase/` for the progressive-disclosure pattern. +1. **Justify the growth in your PR** (a sentence in the description is enough) — + the acknowledgment entry (below) is the review record that the larger size + was a deliberate, seen decision, not silent drift. +2. **Add an acknowledgment entry** in `tests/emitted-drift-ack.json` naming the + file and the reason, per `CONTEXT.md`'s `### Emitted Artifact Provenance` + entry. This is deliberately a committed file, not a flag — the entry appears + in your PR diff, so touching it *is* the visible signal. +3. **Or shrink it instead of acknowledging.** Prefer extraction when the growth + is incidental: for a workflow, move per-mode bodies to + `workflows//modes/`, templates to `workflows//templates/`, and + shared prose to `gsd-core/references/`; for an agent, lift shared boilerplate + into `gsd-core/references/` and `@`-reference it — then load it **LAZILY**. Do + *not* convert them to eager `@-required_reading` includes: that shrinks the + file's bytes without shrinking loaded context, so it games the guard while + making the real cost worse. See `workflows/discuss-phase/` for the + progressive-disclosure pattern. -If a hard cap (not the baseline) is what failed, regeneration will **not** help — -that is the signal to extract, per step 3. - -### How-to: the baselines or golden fixtures conflict on merge - -`tests/workflow-size-baseline.json`, `tests/agent-size-baseline.json` and -`tests/fixtures/golden-install-parity/*.json` are **Emitted Artifact Provenance** -files (`CONTEXT.md` → `RULESET.EMITTED_ATTRIBUTION`): pure functions of the source -tree. Their correct merge is always *recompute*, which git's ours/theirs interface -cannot express — so a conflict here is never something to hand-resolve. - -Register the merge driver once per clone: - -```bash -npm run setup:merge-driver -``` - -Afterwards a conflicting merge, rebase or cherry-pick keeps your branch's copy and -prints a one-line notice. Recompute the artifacts before committing: - -```bash -npm run regen:derived -``` - -That one command runs every generator in dependency order (`gen:golden` last, -because it hashes installed output). On an unmodified tree it produces no diff. - -Two things it deliberately does **not** do: - -- **It does not clear GitHub's `CONFLICTING` label.** Merge drivers live in - `.git/config`, so forks do not have one and github.com's own merge never runs a - custom driver. The driver removes the labour, not the label. -- **It does not regenerate during the merge.** At the moment git invokes a merge - driver, neither the working tree nor the index reflects the merge yet — so - regenerating there would compute the artifact from the *pre-merge* tree and write - a confidently wrong answer. Running `regen:derived` afterwards is what makes it - correct. - -This driver is a bridge introduced by [#2721](https://github.com/open-gsd/gsd-core/issues/2721) -and retired by [#2724](https://github.com/open-gsd/gsd-core/issues/2724), which -replaces these committed artifacts with a computed attribution check (ADR-2719). -`tests/fixtures/install-tree/*.json` is deliberately excluded and keeps normal merge -semantics — its diffs are readable and it must stay an absolute "the installer -stopped shipping X" failure. +If a hard cap (not the ratchet) is what failed, an acknowledgment will **not** +help — that is the signal to extract, per step 3. ### Reference | Artifact | Role | |---|---| -| `scripts/workflow-size.cjs` | Single source of truth — LF-normalized byte counter (`lfByteCount`) + generic `measureMdFiles(dir, predicate)` (backs both workflows and agents) + workflow enumeration (`listWorkflowStems`, `measureWorkflows`). Imported by **both** the guards and the generator so they can never measure differently. | -| `scripts/update-size-baseline.cjs` (`npm run size:baseline`) | Regenerates **both** `tests/workflow-size-baseline.json` and `tests/agent-size-baseline.json` — sorted keys, trailing newline, idempotent. | -| `npm run regen:derived` | Runs every generator in dependency order (build → registry → ADR index → capability matrix → inventory manifest → manifest versions → size baselines → golden fixtures). Use it instead of remembering which generator owns which artifact. | -| `scripts/git-merge-regen-driver.cjs` (`npm run setup:merge-driver`) | Registers the `gsd-regen` merge driver in this clone. Keeps your branch's copy of a conflicting generated artifact and points you at `regen:derived`. Bridge for #2721; retired by #2724. | -| `tests/workflow-size-baseline.json` | The committed per-workflow snapshot (one entry per workflow). | -| `tests/agent-size-baseline.json` | The committed per-agent snapshot (one entry per `gsd-*` agent). | -| `tests/workflow-size-budget.test.cjs` | The three workflow guards above, plus the `discuss-phase` progressive-disclosure checks. | -| `tests/agent-size-budget.test.cjs` | The per-agent baseline + tier hard-cap guards (the agent analog). | +| `scripts/workflow-size.cjs` | Single source of truth — LF-normalized byte counter (`lfByteCount`) + generic `measureMdFiles(dir, predicate)` (backs both workflows and agents) + workflow enumeration (`listWorkflowStems`, `measureWorkflows`). Imported by both guards and by `tests/helpers/emitted-runtime.cjs`'s `currentSizes()` so they can never measure differently. | +| `tests/emitted-attribution.test.cjs` + `tests/helpers/emitted-diff.cjs` | The differential attribution check and its size ratchet (ADR-2719). The sole mechanism for both emitted-content propagation AND per-file size growth as of #2724. | +| `tests/emitted-drift-ack.json` | Committed acknowledgment file for unattributable emitted-content ripples and for size growth. Absent = no acks; its presence is the alarm. | +| `npm run regen:derived` | Runs every remaining generator in dependency order (build → registry → ADR index → capability matrix → inventory manifest → manifest versions → `tests/fixtures/install-tree/*.json`). | +| `tests/workflow-size-budget.test.cjs` | The workflow tier hard-cap guards, plus the `discuss-phase` progressive-disclosure checks. | +| `tests/agent-size-budget.test.cjs` | The agent tier hard-cap guards (the agent analog). | + +`tests/workflow-size-baseline.json`, `tests/agent-size-baseline.json`, +`tests/fixtures/golden-install-parity/*.json`, `scripts/update-size-baseline.cjs` +(`npm run size:baseline`), and `scripts/git-merge-regen-driver.cjs` +(`npm run setup:merge-driver`) are all removed by +[#2724](https://github.com/open-gsd/gsd-core/issues/2724): they were pure +functions of the source tree, conflicted on every merge that touched them, and +their functions are now served by the differential attribution check above. +`tests/fixtures/install-tree/*.json` is the one artifact family that stays +committed and normally-merged (ADR-2719 §7) — it conflicts on 0 of 7, its diffs +are readable, and it preserves "the installer stopped shipping X" as a hard +absolute failure with no attribution reasoning involved. Regenerate it with +`npm run gen:install-tree` (folded into `npm run regen:derived`). ## Running suites locally diff --git a/docs/adr/2719-emitted-artifact-attribution.md b/docs/adr/2719-emitted-artifact-attribution.md index 04906bace..c2222588c 100644 --- a/docs/adr/2719-emitted-artifact-attribution.md +++ b/docs/adr/2719-emitted-artifact-attribution.md @@ -1,6 +1,6 @@ # ADR-2719: Emitted-artifact attribution — replace the committed parity fixtures with a computed conservation law -- **Status:** Proposed +- **Status:** Accepted - **Date:** 2026-07-27 - **Issue:** [#2719](https://github.com/open-gsd/gsd-core/issues/2719) (epic); Phase 0 tracked by [#2720](https://github.com/open-gsd/gsd-core/issues/2720) - **Supersedes:** [ADR-2264](2264-golden-parity-redesign.md) (Redesign golden-install-parity) — replaces its Decision §2–§4 and its 2026-07-14 Amendment. ADR-2264 **Phase 1 is retained and depended upon**: the single-source `buildParityManifest` and the four exclusion constants in `tests/helpers/install-shared.cjs` are the foundation this design builds on, not something being reverted. diff --git a/docs/adr/README.md b/docs/adr/README.md index ef0ccb715..6481e8823 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -116,7 +116,7 @@ This replaces a hand-maintained table that had drifted to **40 of 65 ADRs** — -### Active decisions (51) +### Active decisions (52) These govern the system as it stands. Cite these. @@ -172,9 +172,10 @@ These govern the system as it stands. Cite these. | [ADR-2207](2207-status-field-lifecycle-ownership.md) | STATE.md `Status` lifecycle — phase-completion writes an intermediate state; milestone-close owns termination | Accepted | — | | [ADR-2346](2346-command-dispatch-completion.md) | Command Dispatch Completion | Accepted | — | | [ADR-2629](2629-phase-effort-estimation-calibration.md) | Phase effort is estimated against a calibrated smart-zone budget, not a static heuristic | Accepted | — | +| [ADR-2719](2719-emitted-artifact-attribution.md) | Emitted-artifact attribution — replace the committed parity fixtures with a computed conservation law | Accepted | — | | [ADR-3660](3660-runtime-artifact-layout-module.md) | Runtime Artifact Layout Module owns per-runtime artifact placement | Accepted | [ADR-1239](1239-gsd-embeddable-orchestration-engine.md) | -### Proposed (9) +### Proposed (8) Decided in principle, not yet ratified. Do not cite as settled architecture. @@ -188,7 +189,6 @@ Decided in principle, not yet ratified. Do not cite as settled architecture. | [ADR-1213](1213-capability-state-writer.md) | Capability write side — the Capability State Writer | Proposed | — | | [ADR-1606](1606-prohibition-enforcement-verify-seam.md) | prohibition-enforcement verify-time seam | Proposed | — | | [ADR-1671](1671-dynamic-context-management-platform.md) | Dynamic context management platform | Proposed | — | -| [ADR-2719](2719-emitted-artifact-attribution.md) | Emitted-artifact attribution — replace the committed parity fixtures with a computed conservation law | Proposed | — | ### Superseded, Retired, and Legacy (8) diff --git a/docs/how-to/add-or-update-a-host-integration.md b/docs/how-to/add-or-update-a-host-integration.md index dd51e4617..eddc1823a 100644 --- a/docs/how-to/add-or-update-a-host-integration.md +++ b/docs/how-to/add-or-update-a-host-integration.md @@ -139,10 +139,12 @@ yields the same truth value**, so behavior is unchanged and only the brittle cou delegates to the *same* engine functions, so the output is byte-identical — that is the point: the host is now driven **through** the interface, not around it. -5. **Prove parity, both scopes.** `tests/golden-install-parity.test.cjs` captures a byte-stable manifest - of every emitted file. Assert the host's install is unchanged for **global and local** scopes - (regenerate a baseline from `origin/next` first, then confirm the migrated tree matches it). Exclude - only genuinely volatile / platform-varying files (`settings.json`, `settings.local.json`, `.gsd-source`). +5. **Prove parity, both scopes.** The differential attribution check + (`tests/emitted-attribution.test.cjs`, ADR-2719) compares the emitted manifest built + from your branch against `next`'s recorded state and requires every moved hash to be + attributable to a path your PR changed — no fixture to regenerate by hand. Confirm the + host's install is unchanged for **global and local** scopes. Exclude only genuinely + volatile / platform-varying files (`settings.json`, `settings.local.json`, `.gsd-source`). 6. **Guard against regression.** Add a `*-imperative-reference.test.cjs` asserting the adapter classifies the host correctly, negotiation fails closed on a corrupted descriptor, and — with a source-grep behind diff --git a/docs/reference/host-integration-capability-matrix.md b/docs/reference/host-integration-capability-matrix.md index 50c7c8688..c6737c2fa 100644 --- a/docs/reference/host-integration-capability-matrix.md +++ b/docs/reference/host-integration-capability-matrix.md @@ -106,7 +106,7 @@ Sources consulted: | dispatch.backgroundDispatch | true | https://github.com/openai/codex/blob/main/codex-rs/core/templates/collab/experimental_prompt.md | "Sub-agents have access to the same set of tools as you do so you must tell them if they are allowed to spawn sub-agents themselves or not." The config (codex-rs/config/src/config_toml.rs) exposes an | | dispatch.isolation | orchestrator-worktree | https://learn.chatgpt.com/docs/environments/git-worktrees ; https://github.com/openai/codex/blob/main/codex-rs/utils/cli/src/shared_options.rs | "Worktrees are available only in Codex in the ChatGPT desktop app." (no native CLI worktree) + `codex exec --cd ` sets an explicit working root, so GSD creates+manages the worktree and points the executor at it (#2584) | -**GSD integration status — Phase D dogfood complete (#2088, ADR-1239).** Codex installs through the `declarative` embedding adapter (`createDeclarativeAdapter` → `installRuntimeArtifacts`); the hardcoded `runtime === 'codex'`/`isCodex` projection is folded into descriptor-driven `runtime.hostBehaviors`, and install/uninstall output is byte-parity-gated (`tests/fixtures/golden-install-parity/codex.json`). Three capability upgrades land, each with a test driving the user-reachable surface: +**GSD integration status — Phase D dogfood complete (#2088, ADR-1239).** Codex installs through the `declarative` embedding adapter (`createDeclarativeAdapter` → `installRuntimeArtifacts`); the hardcoded `runtime === 'codex'`/`isCodex` projection is folded into descriptor-driven `runtime.hostBehaviors`, and install/uninstall output is byte-parity-gated at the time (`tests/fixtures/golden-install-parity/codex.json`; superseded by the differential attribution check, #2724). Three capability upgrades land, each with a test driving the user-reachable surface: - **Skill root** — skills install to the canonical `$HOME/.agents/skills` (Codex core-skills `loader.rs` user-scope root), not the deprecated `$CODEX_HOME/skills` fallback. Declared via the skills-kind `home: ".agents"` override; pre-move installs are migrated (stale `~/.codex/skills/gsd-*` cleaned on both install and uninstall). - **Hook events** — GSD registers all documented `hooks.json` lifecycle events beyond `SessionStart`: `SubagentStart`, `Stop`, `PostToolUse` (#772), plus the six added in #2088 — `PreToolUse`, `PermissionRequest`, `PreCompact`, `PostCompact`, `SubagentStop`, `UserPromptSubmit` — all routed through `gsd-context-monitor.js`. (The descriptor `extendedHookEvents` field reflects the schema-valid cross-runtime subset `SubagentStop`/`Stop`/`PreCompact`; Codex's full event set is codex-hooks-json-native, registered directly in `hooks.json`.) @@ -196,7 +196,7 @@ Sources consulted: - https://cursor.com/docs/enterprise/llm-safety-and-controls - /websites/cursor (Context7) -**GSD integration status — Phase D dogfood complete (#2089, ADR-1239).** Cursor installs through the `imperative` embedding adapter (`createImperativeAdapter` → `installRuntimeArtifacts`); the hardcoded `runtime === 'cursor'` / `isCursor` projection is folded into descriptor-driven `runtime.hostBehaviors`, and install/uninstall output is byte-parity-gated (`tests/fixtures/golden-install-parity/cursor.json`). Two capability upgrades land, each with a test driving the user-reachable surface: +**GSD integration status — Phase D dogfood complete (#2089, ADR-1239).** Cursor installs through the `imperative` embedding adapter (`createImperativeAdapter` → `installRuntimeArtifacts`); the hardcoded `runtime === 'cursor'` / `isCursor` projection is folded into descriptor-driven `runtime.hostBehaviors`, and install/uninstall output is byte-parity-gated at the time (`tests/fixtures/golden-install-parity/cursor.json`; superseded by the differential attribution check, #2724). Two capability upgrades land, each with a test driving the user-reachable surface: - **Expanded hook-bus coverage** — GSD registers all 6 managed lifecycle events in `hooks.json` beyond the original `sessionStart`/`postToolUse`: `preToolUse`, `stop`, `subagentStart`, `subagentStop` (AC4a, cite https://cursor.com/docs/hooks). The hook-bus binding is descriptor-driven via `src/host-integration-adapters/imperative-hook-bus.cts` (reads `hostBehaviors.managedHookEvents`), not a hardcoded event pair. - **Named/background nested subagent dispatch** — `dispatch.background`/`backgroundDispatch`/`nested` are all `true` with `maxDepth: 2`; `shouldFlattenDispatch(cursor)` returns `false` so GSD's wave-based execution drives Cursor's native background + depth-2 nested subagent dispatch instead of flattening to inline sequential calls (AC4b, cite https://cursor.com/docs/subagents + https://cursor.com/docs/sdk/typescript). @@ -233,7 +233,7 @@ Sources consulted: - https://github.com/cline/cline/blob/main/sdk/packages/llms/README.md - /cline/cline (Context7) -**GSD integration status — Phase D dogfood complete (#2090, ADR-1239).** Cline installs through the `imperative` embedding adapter (`createImperativeAdapter` → `installRuntimeArtifacts`); the hardcoded `runtime === 'cline'` / `isCline` projection is folded into descriptor-driven `runtime.hostBehaviors`, and install/uninstall output is byte-parity-gated (`tests/fixtures/golden-install-parity/cline.json`). Two capability upgrades land, each with a test driving the user-reachable surface: +**GSD integration status — Phase D dogfood complete (#2090, ADR-1239).** Cline installs through the `imperative` embedding adapter (`createImperativeAdapter` → `installRuntimeArtifacts`); the hardcoded `runtime === 'cline'` / `isCline` projection is folded into descriptor-driven `runtime.hostBehaviors`, and install/uninstall output is byte-parity-gated at the time (`tests/fixtures/golden-install-parity/cline.json`; superseded by the differential attribution check, #2724). Two capability upgrades land, each with a test driving the user-reachable surface: - **`AgentPlugin.hooks.beforeTool` planning guard** — the `.clinerules/hooks/PreToolUse` file-convention hook (#787) is re-implemented as a real Cline SDK `AgentPlugin` registered through the negotiated `hookBus: host` interface point. Guard semantics are preserved exactly (fail-open, cancels write-class calls targeting `.planning/`); the SDK maps the file hook's `{cancel, errorMessage}` to `{skip, reason}`. The binding lives in `src/host-integration-adapters/cline-sdk-binding.cts` (cite https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx). - **`createAgentModel` per-subagent model overrides** — `DefaultGateway.createAgentModel({providerId, modelId})` is wired so GSD's `model_overrides` / `model_profile_overrides` resolution (already used for OpenCode/Codex passive hosts) applies to cline subagents (`modelMode: active`), instead of leaving model selection untouched (cite https://github.com/cline/cline/blob/main/docs/sdk/reference/gateway.mdx). diff --git a/docs/reference/host-integration-interface.md b/docs/reference/host-integration-interface.md index db2d8d024..11a6ba376 100644 --- a/docs/reference/host-integration-interface.md +++ b/docs/reference/host-integration-interface.md @@ -59,8 +59,8 @@ constructed fail-closed (they throw if a required host primitive is absent): The declarative + imperative adapters delegate install/uninstall in-process to the **same** `installRuntimeArtifacts` engine function `bin/install.js` uses, so -adapter output is byte-identical to a first-party install (gated by -`tests/golden-install-parity.test.cjs`). +adapter output is byte-identical to a first-party install (gated by the +differential attribution check, `tests/emitted-attribution.test.cjs`, ADR-2719). ## Profiles diff --git a/package.json b/package.json index d06c586f0..d61356413 100644 --- a/package.json +++ b/package.json @@ -92,9 +92,8 @@ "gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write", "gen:capability-registry": "node scripts/gen-capability-registry.cjs --write", "gen:registry": "node scripts/gen-registry.cjs --write", - "gen:golden": "node scripts/gen-golden-install-parity-zcode.cjs && node scripts/gen-install-tree-fixtures.cjs", - "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/sync-manifest-versions.cjs && npm run size:baseline && npm run gen:golden", - "setup:merge-driver": "node scripts/git-merge-regen-driver.cjs --install", + "gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs", + "regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree", "validate:registry": "node scripts/validate-registry.cjs", "prepack": "npm run build:lib", "prepare": "npm run build:lib", @@ -117,7 +116,6 @@ "lint:docs": "node scripts/lint-docs-required.cjs", "lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs", "ci:test-scope": "node scripts/ci-test-scope.cjs", - "size:baseline": "node scripts/update-size-baseline.cjs", "changeset": "node scripts/changeset/new.cjs", "changelog:render": "node scripts/changeset/cli.cjs render", "test": "node scripts/run-tests.cjs", diff --git a/scripts/ci-export-emitted-baseline-env.cjs b/scripts/ci-export-emitted-baseline-env.cjs new file mode 100644 index 000000000..b6e95b0f4 --- /dev/null +++ b/scripts/ci-export-emitted-baseline-env.cjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +'use strict'; + +/** + * ci-export-emitted-baseline-env.cjs — exports GSD_EMITTED_BASELINE for the + * differential attribution check when its baseline cache was restored (#2724, + * ADR-2719 §5). + * + * A plain `node