* feat(#3626): make CONTEXT.md seam claims checkable via SEAM.*.enforced-by gate Adds SEAM.<id>.owns / SEAM.<id>.enforced-by=lint-rule:<name>|test:<path> predicates to CONTEXT.md, generalizing the existing WORKTREE.SEAM.* shape, plus scripts/lint-seam-enforcement.cjs (wired into lint:ci) which fails when a declared single-owner seam names no existing, registered enforcement mechanism. Backs all six current module-level single-seam/ single-canonical-owner claims found in CONTEXT.md, including the Shell Command Projection Module's Windows-binary-resolution claim via #3619's local/no-private-binary-resolution rule. Scope is resolves-only per maintainer decision: the gate proves an enforcement pointer exists and is registered, not that its surface covers every file the seam claims. See docs/adr/3626-context-md-seam-claim-gate.md. Closes #3626 * fix(#3626): back the Package Identity Module's seam claim too Isolated adversarial review caught a miss in the "no grandfather list" sweep: the Package Identity Module also declares itself "Single seam owning GSD's published-package coordinates" and already names its real enforcement (scripts/lint-package-identity-drift.cjs). Backs it with SEAM.package-identity.owns/enforced-by=test:tests/package-identity.test.cjs, bringing the total to 7 backed seams. Also makes explicit, in the design doc and ADR, that function-level "single owner" sentences inside already-covered modules (STATE.md Document Module, etc.) are deliberately out of scope — a seam claim is about a module's boundary, not every function inside it. * docs(#3626): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
95
docs/adr/3626-context-md-seam-claim-gate.md
Normal file
95
docs/adr/3626-context-md-seam-claim-gate.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# ADR-3626: CONTEXT.md seam claims carry a checkable enforcement pointer
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-08-27
|
||||
- **Issue:** [#3626](https://github.com/open-gsd/gsd-core/issues/3626)
|
||||
- **Amends:** none. Applies ADR-1703's "seam it" strategy (Decision 2) by adding the verification
|
||||
step that strategy lacked.
|
||||
|
||||
**Decision summary.** `CONTEXT.md` gains a new machine-readable predicate pair,
|
||||
`SEAM.<id>.owns=<capability>` / `SEAM.<id>.enforced-by=lint-rule:<name>|test:<path>`, generalizing
|
||||
the existing `WORKTREE.SEAM.*` vocabulary rather than introducing a parallel one. A new
|
||||
`scripts/lint-seam-enforcement.cjs`, wired into `lint:ci`, fails when an `owns` claim has no
|
||||
matching `enforced-by` pointer, or when that pointer names a lint rule that is not registered in
|
||||
`eslint.config.mjs` (or whose source file is missing), or a test file that does not exist on disk.
|
||||
|
||||
The gate is deliberately **resolves-only**: it proves the named enforcement mechanism *exists and
|
||||
is wired up*, not that its surface actually *covers* every file the seam claims to own. That
|
||||
broader "coverage" verification was the issue's own explicitly-flagged larger, harder design
|
||||
(ESLint glob matching for a rule; no static notion of "coverage" at all for a test-file anchor) and
|
||||
was decided against by the maintainer in chat before implementation (2026-08-27), per the "cheap
|
||||
and honest" framing in the issue's own scope caveat.
|
||||
|
||||
## Context
|
||||
|
||||
`CONTEXT.md` declares several single-owner/single-seam claims in prose — "the single canonical
|
||||
owner of X", "the single seam for Y". ADR-1703 established that a portability class gets either a
|
||||
lint rule (self-verifying) or a centralized seam (an assertion, with no verification step). Epic
|
||||
#3411 found the Shell Command Projection Module's Windows-binary-resolution seam claim was false
|
||||
for years: four divergent implementations existed, and a fix to one never reached the others,
|
||||
because nothing checked that the claimed seam was actually the only place that logic lived.
|
||||
|
||||
The gap: strategy 2 ("seam it") has no equivalent of strategy 1's self-verification. A seam
|
||||
declaration can decay silently as the next author needs something the seam doesn't offer and
|
||||
writes around it.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Vocabulary**: reuse and generalize the `<NAME>.SEAM.*` predicate-fact shape already shipped
|
||||
for the Worktree Safety Policy Module (`WORKTREE.SEAM.current`, `.files`, `.interface`,
|
||||
`.caller-rule`, `.test-anchor-w017`, ...) rather than invent a second one. The new top-level
|
||||
keys are `SEAM.<id>.owns` and `SEAM.<id>.enforced-by`, coexisting alongside any existing
|
||||
`<NAME>.SEAM.*` descriptive facts for the same module (see `WORKTREE.SEAM.*` +
|
||||
`SEAM.worktree-safety-policy.*` in `CONTEXT.md` for the pattern).
|
||||
2. **Enforcement pointer schemes**: exactly two, `lint-rule:<name>` (must resolve to a key in
|
||||
`eslint.config.mjs`'s `localPlugin.rules` map AND a corresponding `eslint-rules/<name>.cjs`
|
||||
file) and `test:<path>` (must exist relative to the repo root). A third scheme is a lint
|
||||
failure ("unrecognized enforcement-pointer scheme"), not a silent pass.
|
||||
3. **Scope**: resolves-only. The gate does not compute whether a rule's ESLint `files` glob or a
|
||||
test's exercised code paths actually reach every file the seam claims. This is a conscious,
|
||||
disclosed limitation — see Consequences.
|
||||
4. **No grandfather list** (ADR-1703 Decision 2, applied here): every current *module-level*
|
||||
single-owner/single-seam claim in `CONTEXT.md` was enumerated and either backed with a real
|
||||
`SEAM.*.owns`/`enforced-by` pair, or its prose would be corrected to stop claiming exclusive
|
||||
ownership. Seven claims were found (Shell Command Projection Module's Windows-binary-resolution
|
||||
axis, Verification Module's `isPhaseComplete`, Phase Locator Module's
|
||||
`listMilestonePhaseDirs`, Git Query Module, the capability-activation precedence engine, the
|
||||
Worktree Safety Policy Module, and the Package Identity Module — the last caught by an isolated
|
||||
adversarial review pass, not the first-pass sweep); all seven already had an existing, on-disk
|
||||
lint rule or test file that plausibly anchors the claim once actually pointed to — none
|
||||
required a prose downgrade. **Scope boundary**: several other "single owner" sentences exist at
|
||||
*function* granularity inside already-covered, multi-function module entries (e.g. individual
|
||||
STATE.md Document Module functions); these are deliberately out of scope — a seam claim is
|
||||
about a module's boundary, matching the granularity `WORKTREE.SEAM.*` already set as precedent,
|
||||
not every function-level ownership sentence. See
|
||||
`.gsd/phase/feat-3626-context-seam-claim-gate/40-design.md` for the full seam-by-seam
|
||||
disposition table, the scope-boundary rationale, and verification evidence.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:** an unbacked seam claim is now a `lint:ci` failure, not a silent, decaying assertion.
|
||||
The fixture in `tests/lint-seam-enforcement.test.cjs` (row 4, "owns with no matching enforced-by")
|
||||
proves the gate can actually fail, not just pass vacuously. The mechanism generalizes cleanly —
|
||||
adding a seventh seam claim later is one predicate pair, not a new gate.
|
||||
|
||||
**Cost / risk — the disclosed limitation.** A claim can be "backed" by a real rule or test that is
|
||||
narrow relative to what the prose claims to own. `SEAM.<id>.enforced-by=test:<path>` accepts any
|
||||
existing file at that path; the gate does not parse the test to confirm it actually references the
|
||||
claimed symbol (each of the six seams landed with this PR was spot-checked by hand via Memtrace's
|
||||
`find_code`, not by the gate itself). A future author could, in principle, satisfy the gate with a
|
||||
test file that exists but tests something unrelated. This is the accepted trade of the
|
||||
resolves-only decision: cheap and honest about what it checks, not a claim of exhaustive coverage
|
||||
verification.
|
||||
|
||||
**Revisit-if**: if a claim backed only by an existing-but-irrelevant test/rule pointer is found in
|
||||
practice (i.e., the resolves-only gap is exploited, deliberately or by drift), re-open the coverage
|
||||
-verification design the issue flagged and this ADR declined to build.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Coverage verification** (does the rule's ESLint `files` glob, or the test's exercised paths,
|
||||
actually include every file the seam declares) — rejected as the issue's own "much larger
|
||||
design," requiring a per-enforcement-type coverage computation with no honest static notion of
|
||||
"coverage" for a test-file anchor. See design doc's Rejected section.
|
||||
- **A new parallel predicate vocabulary** instead of generalizing `WORKTREE.SEAM.*` — rejected per
|
||||
explicit maintainer direction (issue comment, 2026-08-18) to generalize the shipped precedent.
|
||||
@@ -258,6 +258,7 @@ These govern the system as it stands. Cite these.
|
||||
| [ADR-3473](3473-enforcement-by-construction.md) | Enforcement by Construction — One Owner per Invariant | Accepted | — |
|
||||
| [ADR-3574](3574-install-materialization-primitives.md) | Install materialization shares primitives, not one writer | Accepted | — |
|
||||
| [ADR-3625](3625-vetted-spawn-library-evaluation.md) | The platform seam keeps its own Windows binary resolution rather than adopting a spawn library | Accepted | — |
|
||||
| [ADR-3626](3626-context-md-seam-claim-gate.md) | CONTEXT.md seam claims carry a checkable enforcement pointer | 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
|
||||
|
||||
Reference in New Issue
Block a user