Files
msd-core/docs/PARTITION-RULES.md
Tom Boucher a0f8f956c4 enhance(#4139): Phase 3 — partition rules + the five checks (#4497)
* enhance(#4139): Phase 3 — partition rules + the five checks

ADR-4139 Decision 5, epic #4139 Phase 3. Issue #4403's own "Proposed behavior"
section lists four checks; the ADR's Decision 5 and its own phase table ("partition
rules + the five checks") list five — the same four plus "boundary moves are
declared, ongoing". Same issue-vs-ADR drift Phase 2 hit on the detail.md vs
detail/*.md layout: the ADR is the locked, reviewed document, so it wins. This PR
implements all five.

docs/PARTITION-RULES.md (new) is the partition-rules document: the partition rule
itself, the protected-content list and <!-- gsd:protected --> sentinel syntax
(relocated unchanged from gsd-core/references/compact-content-protected-content.md,
now deleted — it was never referenced by any runtime workflow Read, only by the
predecessor test as documentation, so nothing at runtime regresses, and removing it
from gsd-core/references/ also drops it from all 19 installed-project shipped-content
trees for a file nothing ever read), and the five checks explained for a human
reader. Referenced from a new CONTRIBUTING.md subsection under "Editing shipped
content".

tests/helpers/compact-content-split.cjs (new) is the shared mechanics: split
discovery (any gsd-core/workflows/<name>/detail/*.md paired with <name>.md — no
registry file, a pair is registered by existing on disk), line normalization
(carries forward Phase 2's bare-label-line isTrivial fix and the canonical
gsd_run-launcher-preamble exclusion), sentinel extraction, and a
Boundary-Move-Declared commit-trailer reader that is a direct structural port of
tests/helpers/emitted-runtime.cjs's Emitted-Drift-Ack-Hash/-Growth trailer reader
(ADR-3942) — same merge-base range, same fail-closed throw on an uncomputable range,
same dedupe/conflict rules.

tests/compact-content-partition-guard.test.cjs (new) is the actual guard, superseding
tests/plan-phase-compact-split.test.cjs (deleted — its per-pair checks are now the
general guard's job for plan-phase specifically). Checks 2 (disjointness) and 3
(registration + size cap) run unconditionally against every registered split. Checks
1 (completeness, fires once per split on the PR that introduces a new detail/ path),
4 (protected content — no trailer can ever excuse this one, unlike check 5) and 5
(boundary moves declared) are PR-diff-scoped against the resolved base ref and skip
cleanly when there's nothing to compare (a fresh clone, no PR in flight) — a
deliberate asymmetry from check 5's trailer reader, which must throw rather than
silently pass when ITS range is uncomputable, since that function is answering "did
this PR declare its moves" rather than "is there even a diff to look at". Each of the
five checks carries a RED (deliberately broken fixture) / GREEN (fixed) test pair,
built against synthetic temp files or real throwaway git repos, per this repo's rule
that a guard nobody has seen go red is not yet a guard. Building the real fixtures
caught and fixed one real bug before it shipped: check 4's line-presence test was
using the trivial-line-filtered normalizer, so a byte-identical spine falsely
reported its own protected code-fence line as "deleted" — fixed with a
non-filtering membership check.

Extending docs/INVENTORY.md's "Workflow Sub-Files" table for `detail` surfaced a
pre-existing, unrelated gap in the SAME area: gsd-core/workflows/<name>/templates/*.md
is a fourth workflow sub-file kind that already existed on disk and was already known
to lint-response-language-coverage.cjs's FRAGMENT_DIRS, but was invisible to
gen-inventory-manifest.cjs and undocumented in that table. Fixed alongside it, same
pattern, same PR, rather than deferred.

Also, mechanically required by the new fourth sub-file kind:
- scripts/lint-response-language-coverage.cjs: `detail` added to FRAGMENT_DIRS
  alongside modes/steps/templates — a detail/<part>.md inherits its parent's
  response_language coverage through the same per-file proof, not a parallel one.
- tests/workflow-size-budget.test.cjs: explicit regression test locking that
  detail/ files are governed solely by the hard, non-waivable NEW_FILE_CAP
  (tests/helpers/emitted-diff.cjs) and never by the XL/LARGE/DEFAULT spine tiers —
  true by construction (measureWorkflows/listWorkflowStems don't recurse), made
  explicit per the issue's own Done-when item rather than left true-by-omission.
- scripts/gen-inventory-manifest.cjs: `workflow_detail` and `workflow_templates`
  NESTED_FAMILIES entries; docs/INVENTORY-MANIFEST.json regenerated
  (plan-phase/detail/elaboration.md, discuss-phase/templates/*.md now tracked);
  docs/INVENTORY.md's table updated to four kinds.

Verified: `npm run lint:ci` clean with the eslint cache cleared.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4403): review findings + a real gsd-test failure in the new guard

Two orthogonal review passes (Standards + Spec, isolated sub-agents) plus a
separate security review ran against the prior commit. Fixed everything each
surfaced:

- Security (Low, path-traversal existence oracle): checkRegistration's
  dangling-reference check extracted detail-path-shaped substrings from spine
  PROSE via a regex that permits `.`/`/` freely, then joined them onto repoRoot
  and probed fs.existsSync with no containment check — a spine file containing
  `../../../etc/detail/passwd.md`-shaped text could make the guard test file
  existence outside the repo. Added a path.relative-based containment check
  before the fs.existsSync call; anything that resolves outside repoRoot is now
  reported as a dangling reference directly, never probed on disk.
- Standards (Boundary Coverage): the size-cap fixtures covered NEW_FILE_CAP and
  NEW_FILE_CAP-1 but not NEW_FILE_CAP+1 — added the third boundary-point case
  CLAUDE.md's TEST RULES require (limit-1/limit/limit+1).
- Standards (Property-Based Testing): extractProtectedBlocks (a sentinel
  parser) and the new parseBoundaryMoveTrailerValues (a declare/dedupe/conflict
  parser, bijective-shaped) had no fast-check property test. Added three: a
  render/parse bijectivity property for the trailer parser (mirroring the exact
  ADR-3942 sibling test's alphabet/idiom), a dedupe-is-idempotent property for
  the same parser, and a well-formed-sentinel-round-trips property for
  extractProtectedBlocks.

Then dispatched gsd-test on the resulting commit. It found a real bug the
reviews couldn't have caught (none of them can run inside gsd-test's sandbox):
checks 4/5's real-repo assertion failed against plan-phase's own split,
reporting DISK_PLANS/#3218-comment lines as "undeclared boundary moves" —
content Phase 2 (#4402) legitimately moved into detail/elaboration.md months
before this PR's Boundary-Move-Declared mechanism existed to require a
trailer for it. Root cause: `resolveBase()`'s own doc comment already documents
that no `origin/*` remote-tracking ref exists inside the gsd-test sandbox
container, and its fallback candidate (a bare `next` branch) can resolve to a
point in history that predates an already-merged, already-reviewed split —
making that split look "newly introduced" from the sandbox's vantage point.
Check 1 (completeness) already scopes itself correctly to only genuinely-new
detail paths (git diff status 'A'); checks 4 and 5 did not share that scoping,
so a stale base made them re-litigate a settled split retroactively. Fixed by
having checks 4/5 skip any split name check 1 already counted as newly-split —
their own premise ("did an EXISTING split shed/undeclare something") does not
apply to a split that is, from the resolved base's vantage point, brand new;
that is check 1's domain alone. Verified locally (25/25 tests pass via a
direct `node -e` require, since `node --test` is blocked in this repo) and via
re-reasoning through the exact real-repo scenario the gsd-test failure showed.

Also regenerated all 19 tests/fixtures/install-tree/*.json goldens — the
prior commit's deletion of gsd-core/references/compact-content-protected-content.md
was never reflected there, which is what golden-install-tree.test.cjs's other
19 failures in the same gsd-test run were.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4403): backfill changeset pr number to 4497

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4403): isolate codex-config.test.cjs into its own chunk, root-causing the Windows CI failure

PR #4497's "full test (windows-latest, 24, shard 2/3)" job failed: run-tests
killed chunk 3/8 at the 600s per-chunk backstop, with codex-config.test.cjs
(weight 17.87, by far the chunk's dominant cost) packed alongside 39 other
files. Traced, not assumed:

- scripts/run-tests.cjs's own timeout-headroom comment for the OUTER
  per-shard timeout documents that "adding one test file reshuffled 115 of
  268 unit files between shards" — shard/chunk composition is architecturally
  known to be unstable to single-file additions, which is exactly what this
  PR's own new tests/compact-content-partition-guard.test.cjs is.
- A second comment, dated 2026-09-06 (one day before this PR, PR #4428's own
  CI), already documents the SAME chunk hitting the SAME 600s backstop with
  the SAME file (codex-config.test.cjs, "a genuinely MEASURED weight of
  17.87 — not a stale-table miss") dominating it — the fix then was cutting
  the Windows per-chunk budget from 60 to 40. That cut clearly was not
  enough: two documented incidents in two days, at two different budget
  settings, both centered on one file that alone consumes ~45% of even the
  reduced Windows budget.
- tests/test-timings.json's own header confirms its source data
  (test-events-linux-node22/24.jsonl) is Linux-only, and run-tests.cjs's own
  chunk-timeout diagnostic already prints "real Windows cost runs ~2.2x the
  recorded figure" — the packer's weight-balancing is working off data that
  is both stale (table last regenerated 2026-08-07) and known to
  underestimate the platform where the failure occurs.

Given codex-config.test.cjs is disproportionately heavy AND every companion
sharing its chunk is decided by a packing algorithm already documented as
reshuffling unpredictably on any new file, tuning the shared budget a third
time only moves the marginal line to wherever the next new file happens to
land — it does not remove the gamble. Isolating codex-config.test.cjs into
its own dedicated single-file chunk, unconditionally and on every platform,
removes it at the source: the file never enters the pool packChunks balances,
so no other file's packing changes, and no future single-file addition
(mine or anyone else's) can silently reintroduce this exact failure by
landing in its chunk.

Extracted as a small pure function, partitionIsolatedFiles (mirroring this
file's existing pattern of pulling packing/analysis logic out of main() for
in-process unit coverage — see computeSweepProtectSet, analyzeChunkEvents),
with 6 new tests in tests/run-tests-harness.test.cjs covering basename
matching across path separators, near-miss non-matches, the empty-list case,
and the isolated-set contents.

Root cause is now closed rather than papered over with a retry: this failure
is a property of one specific heavy file's chunk placement, not something
that recurs randomly. If codex-config.test.cjs itself is ever genuinely sped
up, this isolation can be revisited — this is a packing-side mitigation for
a known file's cost, not a claim the cost is irreducible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:44:53 -04:00

107 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Partition rules for compact-content splits
(ADR-4139 Decision 5, epic [#4139](https://github.com/open-gsd/gsd-core/issues/4139),
Phase 3 [#4403](https://github.com/open-gsd/gsd-core/issues/4403).) These are the rules a
`workflow.compact_content` split — a workflow file broken into a spine plus one or more
`detail/*.md` parts — must obey, and the CI guard (`tests/compact-content-partition-guard.test.cjs`)
that enforces them. See `docs/adr/4139-compact-content-seam.md` for the full design rationale;
this document is the operational reference for anyone performing a split.
## The rule
**A split moves text. It does not restate it.** The spine and its detail parts are pieces of
one document, not two. There is exactly one copy of each sentence, so there is no stale twin
that can exist — which is what replaces the drift-parity check an earlier design of this
feature would have needed forever.
Rewriting for terseness is permitted **within** one half and is never a way to move a
sentence into both. Where a split cannot be made by moving text alone without breaking the
spine's ability to run the workflow correctly on its own, the correct answer is a different
split point, not a duplicated paragraph.
## The protected-content list
Content in this list may never leave a workflow spine during a split — it stays directly in
the eagerly-loaded spine file, never moved to a `detail/*.md` part, regardless of how much it
would shrink the spine:
1. **Negative instructions and guardrails** — any "do not X" / "never X" instruction that
changes what the orchestrator must refuse to do (e.g. "Never call `ScheduleWakeup`... to
literalize this wait").
2. **Output-format contracts** — any block defining the literal shape of output another
system consumes: a prompt template handed to a subagent, a JSON/XML schema, a
`<quality_gate>` or `<success_criteria>` checklist.
3. **Few-shot examples the workflow's own steps depend on** — a worked example whose absence
would leave a later instruction ambiguous (e.g. a `<verify>`/`<fails_when>` XML pair a
planner prompt's own rule depends on).
4. **Security and prompt-injection language** — any text establishing a security boundary or
defending against injected instructions.
5. **Machine-parsed structural headings** — a heading or marker another tool locates by exact
text (a `## PLANNING COMPLETE`-style return marker, a `<!-- gsd:section -->` directive, a
`<process>`/`</process>` boundary).
### Marking
A sentinel comment declares protection at authoring time. The guard checks for the
sentinel-wrapped content's continued presence in the spine, never for category membership —
a guard cannot judge prose category on its own, so protection is declared, not inferred:
```markdown
<!-- gsd:protected -->
… one protected block …
<!-- gsd:protected:start -->
… a protected region spanning several blocks …
<!-- gsd:protected:end -->
```
The categories above are authoring guidance for *where* to place a sentinel when splitting a
file — they are never what the automated guard evaluates; only the sentinel-wrapped content's
continued presence in the spine is.
## The five checks
The guard discovers registered splits by scanning `gsd-core/workflows/**` for any
`<name>/detail/*.md` path and pairing it with `gsd-core/workflows/<name>.md`. There is no
separate registry to maintain — a pair is registered by existing on disk.
1. **Completeness — once, at split time.** Fires only on the PR that introduces a new
`<name>/detail/*.md` path (i.e. the PR performing the split). The union of the new spine
and its new detail parts, whitespace-normalized, must contain every non-trivial line the
old spine carried at the merge-base. This is what makes a split reviewable; it never fires
again for that pair afterward.
2. **Disjointness — ongoing.** No non-trivial line may appear in both a spine and any of its
detail parts, checked on every PR against every registered pair regardless of what the PR
touched. This is the invariant that keeps duplication from creeping back in.
3. **Registration — ongoing.** A `<name>/detail/*.md` with no `<name>.md` spine, or a spine
whose prose names a detail path that does not exist on disk, fails and names the orphan.
4. **Protected content — ongoing, and cannot be excused.** For every registered spine a PR's
diff touches, every line that sat inside a `<!-- gsd:protected -->` sentinel at the
merge-base must still be physically present in the spine. Deleting it or moving it into a
detail part both fail — naming the sentinel's first line and, for a move, the destination
path. **No `Boundary-Move-Declared` trailer excuses this one**: protected content is
categorically barred from leaving the spine, not merely required to be declared when it
does.
5. **Boundary moves are declared — ongoing.** For ordinary (non-protected) content: if a
non-trivial line is removed from a registered spine and the identical line appears newly
added in that spine's own detail parts in the same diff, one of the PR's own commits
(`merge-base..HEAD`) must carry:
```
Boundary-Move-Declared: gsd-core/workflows/<name>.md — <why this moved>
```
A missing trailer fails and names the spine and the moved line. This mirrors
[ADR-3942](adr/3942-emitted-drift-ack-commit-trailer.md)'s `Emitted-Drift-Ack-Hash`/`-Growth`
trailers exactly — same `git log $(git merge-base <base> HEAD)..HEAD` range, same
fail-closed behavior when that range is uncomputable (a shallow clone throws, it never
silently reports "no violation"), same de-duplication of identical trailers across a
rebase, same hard error when two commits declare the same spine with different reasons.
See `CONTRIBUTING.md`'s "Editing shipped content" section for the trailer mechanism's
general shape.
Checks 1 and 4–5 read `merge-base..HEAD`, never `base..HEAD` (two-dot) — the same correction
ADR-3942 made for its own trailer range, for the same reason: a two-dot range would let the
set of commits being checked and the set of files being diffed disagree about what "this PR"
means.