* 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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"count": 263,
|
||||
"count": 277,
|
||||
"classes": {
|
||||
"ARCH": 1,
|
||||
"CI": 2,
|
||||
@@ -18,6 +18,7 @@
|
||||
"PROHIB": 10,
|
||||
"RELEASE-NOTES": 31,
|
||||
"RULESET": 60,
|
||||
"SEAM": 14,
|
||||
"SESSION": 9,
|
||||
"WAVE": 5,
|
||||
"WORKSTREAM": 5,
|
||||
@@ -1184,6 +1185,76 @@
|
||||
"klass": "RULESET",
|
||||
"value": "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 an `Emitted-Drift-Ack-Growth:` commit trailer on the PR's own commits (ADR-3942, superseding ADR-2719 §3's fragment model — key is the bare filename, reason follows ` — `)) + 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"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.capability-activation-precedence-owner.enforced-by",
|
||||
"klass": "SEAM",
|
||||
"value": "test:tests/capability-precedence-parity.test.cjs"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.capability-activation-precedence-owner.owns",
|
||||
"klass": "SEAM",
|
||||
"value": "the config-key four-level precedence walk (loadConfig result → workstream config.json → root config.json → registry.configSchema default → absent), owned solely by src/capability-activation.cts"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.git-query-readonly-seam.enforced-by",
|
||||
"klass": "SEAM",
|
||||
"value": "test:tests/git-base-branch.test.cjs"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.git-query-readonly-seam.owns",
|
||||
"klass": "SEAM",
|
||||
"value": "bounded, never-throw git repository introspection — base-branch detection, worktree-info detection, phase change-set detection"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.package-identity.enforced-by",
|
||||
"klass": "SEAM",
|
||||
"value": "test:tests/package-identity.test.cjs"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.package-identity.owns",
|
||||
"klass": "SEAM",
|
||||
"value": "GSD's published-package coordinates (packageName, binName, repoSlug, changelogRawUrl, manualInstallCommand) — single seam so a repoint/rename is a one-line change"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.phase-locator-milestone-enum.enforced-by",
|
||||
"klass": "SEAM",
|
||||
"value": "test:tests/phase-locator.test.cjs"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.phase-locator-milestone-enum.owns",
|
||||
"klass": "SEAM",
|
||||
"value": "listMilestonePhaseDirs(phasesDir, opts) is the single canonical owner of milestone-scoped phase-directory enumeration (ADR-3180 Decision 1, Phase 3, #3185)"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.shellcmdproj-win-binary-resolution.enforced-by",
|
||||
"klass": "SEAM",
|
||||
"value": "lint-rule:no-private-binary-resolution"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.shellcmdproj-win-binary-resolution.owns",
|
||||
"klass": "SEAM",
|
||||
"value": "Windows binary resolution (resolveExecutableBinary, projectSpawnInvocation) — which file a declared command name actually names, and cmd.exe mediation"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.verification-isphasecomplete.enforced-by",
|
||||
"klass": "SEAM",
|
||||
"value": "test:tests/verification-status.test.cjs"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.verification-isphasecomplete.owns",
|
||||
"klass": "SEAM",
|
||||
"value": "isPhaseComplete(phaseDir, deps?) is the single canonical owner of \"is phase P complete?\" (ADR-3180 §7.4, issue #3186)"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.worktree-safety-policy.enforced-by",
|
||||
"klass": "SEAM",
|
||||
"value": "test:tests/worktree-safety.test.cjs"
|
||||
},
|
||||
{
|
||||
"id": "SEAM.worktree-safety-policy.owns",
|
||||
"klass": "SEAM",
|
||||
"value": "Worktree Safety Policy Module — resolveWorktreeContext, parseWorktreePorcelain, planWorktreePrune, executeWorktreePrunePlan, W017 classification (see WORKTREE.SEAM.* above for full interface/invariant detail)"
|
||||
},
|
||||
{
|
||||
"id": "SESSION.2026-05-05",
|
||||
"klass": "SESSION",
|
||||
|
||||
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