Skip to content

fix(spec): composeStacks carries each bound standalone action once in the composed object - #15022

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-14847-compose-stacks-action-echo
Sep 3, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-14847-compose-stacks-action-echo

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #14847

Summary

defineStack ends with mergeActionsIntoObjects: every standalone action carrying objectName is copied into that object's actions on the way out, and the standalone stays in stack.actions. composeStacks concatenates its inputs' actions and ended with the same merge — so each bound action was appended to its object a SECOND time, beside the copy the input's own build had put there.

mergeActionsIntoObjects is now idempotent over its own output: a bound action the object already carries by identity is not appended again. Identity against the standalone list, deliberately not equality — the only way an entry of stack.actions is the very same object as an entry of object.actions is that a previous merge put it there. Nothing else in the merge moves: order, the order sort, the untouched-object fast path.

Measured on main @ 6392b9c2 (BASE), defineStack outputs as inputs — before / after

case before after
two stacks, default a_item.actions = [dup_x/BOUND, dup_x/BOUND] [dup_x/BOUND]
… manifest: 'preserve' same doubling on objects; packages[].manifest.objects carried ONE each (the two halves disagreed) [dup_x/BOUND], both halves agree
three stacks each embedding embN and binding bN to one object, override / merge shared.actions = [emb3/EMB, b3/BOUND, b1/BOUND, b2/BOUND, b3/BOUND] [emb3/EMB, b3/BOUND, b1/BOUND, b2/BOUND] — the surviving stack's declared actions plus each concatenated standalone once
defineStack(composeStacks([a, b])) REFUSED — 'a_item:dup_x' is declared 3 times REFUSED — is declared twice, the identical line defineStack(a) gets for a lone built input
defineStack(a) (a lone built input) REFUSED — declared twice (the landed #14686 pin at stack-duplicate-action-key.test.ts:236) unchanged
hand-written twin, strict REFUSED by #14686's rule ('t_item:tw' is declared twice) unchanged
a marker key on a standalone action, strict REFUSED — actions.0: Unrecognized key(s) on this action: \__echo`` unchanged (why identity, not marking)
strict: false, ONE action object in both positions carried twice carried once — one declaration, in the mode that opts out of #14686's walk by choice
examples/app-multi-package composed object (the object compile.ts:318 parses), JSON sha256 b1225296475ff3c7… (7351 bytes, both objects carry 0 actions) byte-identical

The fork, decided by measurement (dispatch ruling)

Both directions can satisfy (a) the pins and (b) the twin refusal — both would key on identity, since a marker does not survive the strict parse. They differ on the published shape and on what they touch:

  • Idempotent merge (taken). Output = the previous output minus the duplicate entries; nothing reordered; collectComposedActionKeyCollisions and its message untouched (it runs before the merge). Smallest shape change.
  • Compose the authored shape first. Stripping echoes before mergeObjects changes the input of feat(spec): composeStacks refuses two stacks whose actions resolve to one scope-qualified runtime key #14854's walk and therefore its message — the landed pin 'com.example.b' (stack #1) at stack.actions[0] + objects['shared'].actions[0] names the echo site — and stripping after the walk but before the final merge is the same identity skip with a reorder of the surviving stack's own bound action ([emb3, b1, b2, b3] instead of [emb3, b3, b1, b2]). Larger shape change for the same result.

Criterion (c), round-trip through defineStack, holds in the only form the landed rulings allow. defineStack refuses ANY built stack that binds an action — pinned at stack-duplicate-action-key.test.ts:236 ("a built stack fed back in … is refused — author the source shape, not the artifact") — so defineStack(composeStacks([a, b])) cannot parse when an input binds an action under either direction without weakening that rule, which is ⛔. What this change restores is that composition adds no refusal of its own: with no bound action the composed output parses cleanly (as before), and with bound actions it is refused with the same declared twice line a single built input gets, no longer 3 times. Both are pinned. The card's and the triage's "not round-trippable through the door that built its inputs" is therefore a property of every built artifact, not of composition — noted for the contract review, not acted on here.

Rulings honoured

Changes

  • packages/spec/src/stack.zod.ts — mergeActionsIntoObjects filters the bound list by identity against the object's current actions before appending; TSDoc states the idempotence; composeStacks gains a step-7 comment at its final merge. 31 insertions, 4 deletions.
  • packages/spec/src/compose-stacks-action-echo.test.ts — NEW, 12 pins: once under default / preserve / three stacks override / merge; add-on binding to another stack's object; order across the once-merged set; twin refusal; marker refusal; strict: false shared reference; idempotent re-merge by reference; round-trip with and without bound actions.
  • packages/spec/src/compose-stacks-action-key-collision.test.ts — only the app-multi-package mirror pin that named composeStacks re-merges bound standalone actions that defineStack already copied into their objects — every bound action appears twice in the composed object's actions #14847 as "the measured shape, not the contract" is tightened to toEqual; no refusal message moves.
  • .changeset/compose-stacks-action-echo.md — @objectstack/spec patch.

Verification (all on f1ed87e8, the head; through scripts/pm/os-verify-lock.sh, shared-box seconds)

  • pnpm --filter @objectstack/spec build — check-dts-emitted: 34/34, .build-input-hash 6192f26e…; dist carries base.includes(action) (1) and the old append form (0).
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 over the 13 compose-stacks* / stack* / assembled-package-body suites — Test Files 13 passed (13), Tests 339 passed (339).
  • pnpm --filter @objectstack/spec typecheck — green, incl. check:test-typecheck: OK — 54 file(s) / 261 error(s) … held; tsc -p tsconfig.test.json --listFiles names the new test file (1 hit), so the test layer verdict covers it.
  • pnpm --filter @objectstack/spec check:generated — ✓ All 15 generated artifacts are up to date (check:api-surface, check:docs, check:authorable-surface, check:liveness … all ✓).
  • Gate family derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the actual diff (58 commands): 52 green at exit 0 — incl. check:nul-bytes, check:adr-0087-registration, check:empty-changeset, check:changeset-no-major, check:closing-keyword-parity, check:cross-package-test-inputs, check:test-source-alias, check:engine-double-contract, check:type-check-coverage, and every spec check:* in the list. The script flagged its tree as 4 commits behind origin/main with two gate scripts changed upstream (check-cross-package-test-inputs, engine-double-contract.pinned.json); CI runs the current ones. NOT MEASURED, each by its own verdict text (exit 3 / prerequisite, not a finding): check-dev-prereqs and check:dual-build-cjs-loads and check:type-check-debt (need the whole workspace built — CI owns), check-test-completeness (needs a turbo test log — its usage text says record NOT MEASURED locally), scripts/pm/check-half-states.mjs (live-repo PM sweeper; timed out at 180s with and without --use-env-proxy). check:doc-formula-expressions was re-run after building @objectstack/formula — see the report comment for its line.

Reverse verification (fix committed first)

Mutation: the identity filter removed (const fresh = actionsByObject.get(obj.name) ?? []; — the pre-fix append), proven on disk by anchor counts (injected=1 removed=0) and a differing blob hash (790cfe5c… vs HEAD 8c27145e…). Prediction recorded in the script header before the run: 8 red / 4 green controls in the new suite, 1 red in the collision suite (the tightened mirror pin), 0 red in the #14686 suite. Observed: compose-stacks-action-echo.test.ts (12 tests | 8 failed), compose-stacks-action-key-collision.test.ts (23 tests | 1 failed), stack-duplicate-action-key.test.ts all green — Tests 9 failed | 42 passed (51). No dist leg: these suites import ./stack.zod (relative source), not the package exports, so no rebuild was needed and dist/ stayed on the fix throughout (marker re-checked after restore). Restore by git checkout HEAD -- packages/spec/src/stack.zod.ts under an EXIT/INT/TERM trap with absolute paths, proven by git hash-object equal to the HEAD blob and git diff HEAD --stat EMPTY.

Contract review notes

  • Shape change: a composed object's actions is the previous output minus the duplicate entries. Accept set unchanged in both directions.
  • One measured consequence outside composition: under strict: false, one action object placed in both positions is now carried once (before: twice). Pinned and documented; flagged here for the reviewer.
  • H3 as dispatched ("defineStack(composeStacks([a, b])) parses when no hand-written twin exists") is falsified by the landed feat(spec): defineStack refuses two actions that resolve to one scope-qualified runtime key #14686 pin whenever an input binds an action; the pins assert the achievable form described above.

🤖 Generated with Claude Code

https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i

Generated by Claude Code


Generated by Claude Code

… the composed object

`mergeActionsIntoObjects` is idempotent over its own output: a bound action the
object already carries by identity is not appended again, so the second merge
`composeStacks` runs over inputs built by `defineStack` no longer doubles every
bound action in the composed object. Identity against the standalone list, not
equality — a hand-written twin is two objects after the strict parse and stays
refused by the same-key rule, which runs before the merge and is untouched.

Card: issue 14847 (spec lane). Pins in compose-stacks-action-echo.test.ts; the
app-multi-package mirror pin in the collision test tightened to the contract.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/examples.mdx (via composeStacks (symbol, a top-level function))
  • content/docs/getting-started/glossary.mdx (via composeStacks (symbol, a top-level function))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via composeStacks (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fddfc8db062d61ca68ba482531f5368326109554 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 41a7a209583ca5e4bac713bdb085e54f647ed503 — the merge of head f1ed87e8c84e5fd90836cbf904a44af06684ebf8 into base fddfc8db062d61ca68ba482531f5368326109554, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 41a7a209583ca5e4bac713bdb085e54f647ed503 && git checkout 41a7a209583ca5e4bac713bdb085e54f647ed503
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fddfc8db062d61ca68ba482531f5368326109554 f1ed87e8c84e5fd90836cbf904a44af06684ebf8 && git checkout -B drift-repro fddfc8db062d61ca68ba482531f5368326109554 && git merge --no-ff f1ed87e8c84e5fd90836cbf904a44af06684ebf8

node scripts/docs-audit/affected-docs.mjs --json fddfc8db062d61ca68ba482531f5368326109554

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fddfc8db062d61ca68ba482531f5368326109554 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

Landing provenance (PM seat domain:spec, session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T18:10Z) — flipping to ready and enabling auto-merge (squash) under the 2026-08-31 clear-is-land ruling.

  • Contract review: in-seat at CONTRACT_REVIEW_TIER, PASS · ACCEPT on the card — composeStacks re-merges bound standalone actions that defineStack already copied into their objects — every bound action appears twice in the composed object's actions #14847 comment 5529809100; needs:contract-review cleared on both carriers with compared readbacks in that stroke.
  • CI on head f1ed87e8: 50 check runs, 36 success + 14 skipped, 0 red, 0 in progress (18:09Z) — including Lint & Repo Gates and TypeScript Type Check at success.
  • Governed-surface test on the changed paths: NOT governed — queue landing applies.
  • mergeable_state: clean (18:10Z). Body first line Fixes #14847; no other closing keyword in the body (the other card numbers sit next to no verb).
  • Next reading: the added_to_merge_queue timeline event; on MERGED the card closes via Fixes and pm:dispatched is stripped in the same action, then origin/main is probed for the identity-skip in mergeActionsIntoObjects.

Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 3, 2026 18:11
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 773a999 Sep 3, 2026
52 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14847-compose-stacks-action-echo branch September 3, 2026 18:58
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…data/analytics.zod.ts to the commits that decided them (stage 7) (objectstack-ai#20616)

Part of objectstack-ai#20234
Clause-②: no

Stage 7 of the staged sweep: `packages/spec/src/stack.zod.ts` and
`packages/spec/src/data/analytics.zod.ts`, both freed by landings (PR
objectstack-ai#20579 and PR objectstack-ai#20458). Its claim is `5885635758`. Every comment or
docblock line in those two files that cited a tracker number answering
404 now cites the commit on `main` that decided its rule, in ruling
C+D's form C, and says in its own words what was decided. Comments only:
12 lines out, 12 in, across 2 files. No code token, string literal or
`describe()` text moves. No dead site stays: none of the 12 is read by
literal.

The census is the gate's own `node scripts/check-issue-citations.mjs
--census --json`, filtered to the two paths. Before: base `0f6dcac5e9`,
board enumerated (185 pages, frontier objectstack-ai#20611). After: head `cc0580d404`,
board enumerated (185 pages, frontier objectstack-ai#20615).

## Measurement

| file (under `packages/spec/src/`) | dead before | after | numbers,
then anchor |
|---|---:|---:|---|
| `stack.zod.ts` | 9 | 0 | objectstack-ai#10485 ×2 (`:415`, `:1023`) to `35ad101bc`;
objectstack-ai#6238 (`:633`) to `c8d6f6e08`; objectstack-ai#14192 (`:1233`) to `4d0d9445a`; objectstack-ai#14686
×2 (`:3037`, `:3194`) to `279431e7a`; objectstack-ai#14662 ×3 (`:4510`, `:5070`,
`:5293`) to `35dffeace` |
| `data/analytics.zod.ts` | 3 | 0 | objectstack-ai#10194 ×3 (`:404`, `:407`, `:485`)
to `2306a765c` |
| **2 files** | **12** | **0** | 6 numbers removed, 6 distinct shas |

Per-file counts at base equal the claim's (9 and 3, from stage 6's
census). A second instrument agrees site for site: every `#N` in the two
files, classified by the TypeScript parser, and each of the 84 distinct
numbers of 100 or more probed by REST `issues/N` without following
redirects (the other 3 are the ordinals `Prime Directive objectstack-ai#12`, `batch
objectstack-ai#23`, `batch objectstack-ai#57`).
- Base: 244 sites, all in comments (0 strings, 0 code). 78 numbers
answer 200 and 6 answer 404: the same 6 numbers and the same 12 sites as
the gate.
- Its string-class positive control found 11 string sites in
`kernel/manifest-unknown-keys.test.ts` and
`packages/cli/src/utils/lower-callables.test.ts`.
- Head: 232 sites, 78 numbers, all 78 answer 200 (the same 78), none
answers 404.
- Lit controls objectstack-ai#16862, objectstack-ai#16847 and objectstack-ai#17698 answered 200 at every
checkpoint (3 at base, 3 at head); dead controls objectstack-ai#16714, objectstack-ai#16715 and
objectstack-ai#16697 answered 404 at every checkpoint.

## Why each anchor decides its line

Each sha resolves uniquely, is an ancestor of `origin/main` (and of the
base), and has one parent. No file under `docs/adr/**`,
`docs/NORTH-STAR.md` or `scripts/adr-anchors/` names any of the six
numbers or records these rules, so each takes the commit rung, as stages
1–6 did.

- **objectstack-ai#10485 to `35ad101bc`** (`:415`, `:1023`): retires the `themes`
carrier key and `ThemeSchema` under ADR-0049. Its message records the
ruling, "Ruled B (退役授权面, 2026-08-21)", and its own `stack.zod.ts` diff
wrote both lines. `:415` keeps ADR-0049 and the ruling in its words; the
D3 entry `stack-themes-carrier-retired` it names on `:423` is unchanged.
This is the anchor stages 1, 5 and 6 used for the same retirement.
- **objectstack-ai#6238 to `c8d6f6e08`** (`:633`): widens the array member of
`functions` so its `handler` also takes the lowered string ref, which is
the fix for `objectstack build` refusing its own array output. Its
message names objectstack-ai#6238, and its own diff wrote the line. objectstack-ai#4343 and objectstack-ai#4976 on
the same line stay (both 200).
- **objectstack-ai#14192 to `4d0d9445a`** (`:1233`): turns `ManifestSchema` and its
nested blocks into `strictObject` and flips the assembled-body strip pin
to a refusal pin; each of its sub-commits names objectstack-ai#14192. The line itself
was written later by `c78c9180de`, whose own message says "objectstack-ai#14192 closed
ManifestSchema with strictObject", so the commit that closed it is the
anchor.
- **objectstack-ai#14686 to `279431e7a`** (`:3037`, `:3194`): "defineStack refuses two
actions that resolve to one scope-qualified runtime key". Its subject
names objectstack-ai#14686, and its diff adds `collectDuplicateActionKeyErrors` and
the changeset for that refusal. Both lines were written later by
`773a99960a` (PR objectstack-ai#15022), whose message describes the same "same-key
rule, which runs before the merge".
- **objectstack-ai#14662 to `35dffeace`** (`:4510`, `:5070`, `:5293`): "composeStacks
refuses two stacks whose actions resolve to one scope-qualified runtime
key". It checks the composed set with the rule `defineStack` applies
within one stack, with no `actionConflict` option (maintainer ruling
2026-09-03). Its message does not name objectstack-ai#14662; its own `stack.zod.ts`
diff wrote all three `(objectstack-ai#14662)` lines.
- **objectstack-ai#10194 to `2306a765c`** (`analytics.zod.ts:404`, `:407`, `:485`):
binds `analytics_cube` (and `theme`) in `UNREGISTERED_KIND_SCHEMAS`, so
`PUT /meta/analytics_cube/:name` parses through `CubeSchema`, and gives
`CubeSchema` the `...MetadataProtectionFields` spread. Its message names
objectstack-ai#10194, and its own diff wrote all three lines. The `[objectstack-ai#10194]` markers
become `[commit 2306a76]`, the spelling stages 1 and 5 already use in
`kernel/metadata-type-schemas.ts`.

## Mechanical proof

- **Token guard** (my `tokcmp.mjs`: TypeScript 6.0.3 leaf tokens, JSDoc
kinds excluded, controls mutate the head text in memory only). Base
`0f6dcac5e9` against the head, 2 files, 17,249 base tokens:
  - Real run: 0 files with a token change (exit 0).
  - Comment-insertion control (`data/analytics.zod.ts`): 0 (exit 0).
- Code-insertion positive control (`stack.zod.ts`, a declaration
appended): DIFFER at token 15388 (exit 1).
- String positive control (the first `StringLiteral` the parser locates
in each file): DIFFER at token 5 (exit 1), once per file.
- `describe()` positive control (the first `.describe()` string argument
the parser locates: `stack.zod.ts:133`, `analytics.zod.ts:244`): DIFFER
at tokens 507 and 442 (exit 1).
- **Line balance**: `stack.zod.ts` +9/−9, `data/analytics.zod.ts` +3/−3;
line counts equal at base and head (5344 and 853).
- **Tracker numbers**: added-not-removed is empty in both files, and no
`PR #N` is on an added line. Net-removed: 12 sites, 6 numbers. The only
numbers on added lines are objectstack-ai#4343 and objectstack-ai#4976, which stay on `:633`.
- **Shas**: 6 distinct on added lines, 0 on removed lines.
  - `rev-parse --disambiguate` answers 1 object for each.
- `merge-base --is-ancestor` exits 0 for each, against `origin/main`
`7510663c87` and against the base; each is single-parent; the repository
is not shallow.
- **Literal readers**: all 26 string, template and regex literals in the
repository that carry one of the six numbers (42 code files) were
matched against the two files' base text: 0 occur there. Each removed
line was also cut into 4-word windows (96) and searched across the tree:
the 9 hits inside string literals are other files' own test titles
sharing a phrase ("the ADR-0010 protection envelope", "an assembled body
is"), and none reads either file. The source-text readers of the two
files read code, not these comments:
`compose-stacks-refusal-envelopes.test.ts` counts `throw new Error(`,
and `check-stack-collection-maps.mjs` and
`check-skill-top-level-keys.mjs` read the declared collections and keys.

## Tests and gates (at head `cc0580d404`)

- `pnpm exec turbo run build --concurrency=2 --filter=./packages/*
--filter=./packages/*/*` under `os-verify-lock`: Tasks 71 successful, 71
total, VERDICT command-exit 0.
- `pnpm --filter @objectstack/spec check:generated` under the lock: all
15 generated artifacts up to date, `check:docs` over
`content/docs/references/**` included; VERDICT command-exit 0. No
reference page projects any of the 12 lines, so none is regenerated.
- `vitest run --maxWorkers=2` under the lock over the two files' own
suites (`src/stack*`, `src/compose-stacks*`, `src/define-stack*`,
`src/assembled-package-body`, `src/data/analytics*`, `src/data/cube*`):
Test Files 35 passed (35), Tests 976 passed (976).
- The 37 spec suites that read source text across `src/`, or carry one
of these numbers, under the lock: Test Files 37 passed (37), Tests 759
passed (759).
-
`scripts/{category-title,dist-freshness,dist-freshness-adoption,file-description,strictness-ledger,strictness-ledger-doc,root-index,skill-map-guards,export-origins,split-entries,root-entry-type-nameability.pin}`,
`scripts/liveness/{evidence,tombstoned-row-status}`;
- `src/type-alias-convention.pin`, `src/eager-entry-import`,
`src/api/{api-entry-graph.pin,auth,export-job-family-retirement}`,
`src/ai/tool-confirmation-prescription-tense.pin`,
`src/data/{currency-mode-family-closure.pin,external-lookup-retirement}`,
`src/identity/position-delegatable-enforcer.pin`,
`src/integration/{connector-connection-timeout-retirement,connector-resilience-keys-retirement}`,
`src/security/rls-tags-retirement`,
`src/shared/{alias-integrity,retired-key-migrate-sentence}`,
`src/system/{compliance-families-retirement,constants/platform-object-names,email-template-floor-locale-parity.pin,message-queue-retirement}`,
`src/ui/{action-requires-confirmation-docblock.pin,i18n,interaction-config-retirement,strictness-batch14}`,
`src/kernel/{manifest-unknown-keys,metadata-type-schemas}`.
- Left to CI:
`scripts/{build-schemas-check-mode,def-key-collisions,openapi-self-consistency}`
(each rebuilds artifacts in a temp tree) and
`scripts/{check-generated-ledger,check-generated-fix-rebuild.pin}` (read
the ledger and `dist`). None reads comment text.
- `pnpm --filter @objectstack/spec typecheck` under the lock: exit 0;
`check:test-typecheck` OK (53 files / 251 errors / 138 pinned signatures
held).
- Lint, a proven narrowing: `eslint --no-inline-config --format json`
over the 2 files gives 2 files, 0 errors, 0 warnings.
  - `isPathIgnored` is false for both, read through eslint's API.
- `eslint.config.mjs:327-328` says type-aware linting is never enabled,
so a comment edit cannot move an untouched file's verdict.
  - The repo-wide `pnpm lint` is CI's.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`: 79 families derived and run, every one exit 0. `--ran`
reads "79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN". Among them:
- `pnpm check:issue-citations` (self-test, 114 cases in 8 batteries) and
the live diff-scoped `node scripts/check-issue-citations.mjs`: it judged
the 2 citations on added lines, objectstack-ai#4343 and objectstack-ai#4976, and both are live
issues.
- `pnpm check:doc-authoring`: 16,804 customer-facing strings across
1,179 spec sources clean; the sibling baseline holds.
- `pnpm check:stack-collection-maps`: 8 enumerations reconciled against
31 declared collections.
- Changeset: `patch` for `@objectstack/spec`. Both files are
`src/**/*.zod.ts`, which `files[]` ships verbatim, and the rewritten
docblocks reach `dist`: "posture: commit 4d0d944 closed" and "[commit
2306a76] This docblock used to say" are each in 2 `.d.ts`, their old
spellings in 0. Positive control: the unchanged neighbouring sentence
"BY INHERITANCE — an undeclared key on one is REFUSED" is in the same 2
`.d.ts`.
- Merge probe: a no-driver `merge-tree` of the head onto `origin/main`
`7510663c87`, from a bare shared clone, exits 0. The 3 commits `main`
gained since the base touch neither file nor the citation or derivation
scripts, and a re-derivation prints the same 79 commands. No merge was
made.
- No ablation or reverse verification: the change is comment-only, so
there is no behaviour to invert.

## Hypotheses (measured first)

1. **Holds.** 12 dead sites at the tip, 9 in `stack.zod.ts` and 3 in
`data/analytics.zod.ts`, equal per file to stage 6's census.
2. **Holds.** Read at 2026-09-29T07:36Z and again at 08:16Z, after the
last push and before this PR was opened: all open PRs' full file lists
(9 PRs, 166 files at the second read) and the newest `Claim:` on all 11
`pm:dispatched` cards. None names either file, except this card's own
claim.
3. **Holds, with nothing to keep.** All 12 sites are comments. No test
string, exported string or `describe()` text carries one, and no test or
script reads any of them by literal.
4. **Holds.** No generated reference page projects these lines;
`check:docs` is green with no regeneration.

## Deviations

- None to the file surface: the 12 claimed lines and one changeset, no
generated page needed.
- Commit trailers follow AGENTS.md's model-free pair (`Claude-Session`
plus `Co-authored-by: Claude`); the pre-push trailer check passed on
every push.

## Acceptance notes

**What stays for later stages.** The gate's census at this PR's head
(base `0f6dcac5e9` plus this PR) reads **248** dead sites (29 numbers)
in `packages/spec/src`. The only `packages/spec/src` change `main` has
made since the base (objectstack-ai#20610's migrations entry and registry) adds four
live numbers and removes none, so 248 also stands at the tip
`7510663c87` plus this PR:
- `migrations/` **233**: objectstack-ai#20233 edits the same entry files (PR objectstack-ai#20607
holds `migrations/registry.ts`).
- `conversions/registry.ts` **12**: PRs objectstack-ai#20570 and objectstack-ai#20587 hold it.
- `integration/connector.zod.ts` **1**: PR objectstack-ai#20587 (objectstack-ai#20287).
- `data/api-derivation.ts:163` (objectstack-ai#6259) and
`identity/identity.zod.ts:230` (objectstack-ai#8715), **1** each: kept because tests
read them by literal, so removing them is form D.

**Outside the gate's census: test files.** The gate defers `*.test.ts`.
The same six dead numbers still stand at 15 comment sites and 10
test-title strings in `packages/spec/src` test files:
- `data/analytics-strictness-batchd.test.ts:96` (comment, objectstack-ai#10194) and
its title `:93`. This file is in the `analytics*` set stages 3 and 4
excluded while PR objectstack-ai#20458 held it;
`analytics-date-range-two-bound-window.test.ts` and
`cube-member-inner-name-retirement.test.ts` were in that set too and are
not re-measured here.
- The package root: `compose-stacks-action-echo.test.ts:20`, `:34`,
`:200` (objectstack-ai#14686) and titles `:176`, `:224`;
`compose-stacks-action-key-collision.test.ts:3` (objectstack-ai#14662);
`stack-top-level-strict.test.ts:103` (objectstack-ai#10485) and title `:128`;
`type-alias-convention.pin.test.ts:257`, `:1572`, `:1937` (objectstack-ai#10485).
- `shared/`: `metadata-collection.test.ts:250`,
`metadata-url-spelling.test.ts:51`, `:72`, `:168` (objectstack-ai#10485), `:257`
(objectstack-ai#10194), title `:254`. `automation/sync-retirement.test.ts:207`
(objectstack-ai#10485).
- `kernel/`: `manifest-unknown-keys.test.ts`, four titles (objectstack-ai#14192);
`metadata-type-schemas.test.ts:422`, a title (objectstack-ai#10194).
- Stage 6 took the package root, `shared/` and `automation/` through the
gate's census, which never lists a test file, so test-file comment lines
there may carry other dead numbers as well. That wider population is not
measured here.

**Outside `packages/spec/src`.** The same six numbers stand at 44 more
sites
(`packages/{metadata-protocol,objectql,rest,runtime,cli,core,metadata,qa}`,
`examples/`, `scripts/`, `packages/spec/scripts/`), and at 19 sites in
`migrations/` (the objectstack-ai#20233 area).

**Rung.** The objectstack-ai#10485 retirement also has the ADR-0087 D3 entry
`stack-themes-carrier-retired`, which `:423` already names. This PR
takes the commit rung, as stages 1–6 did.

**Wording, each true of its commit.** `:3037` and `:3194` now read
"commit 279431e's same-key refusal": the refusal that commit added, in
lines `773a99960a` wrote. `:1233` reads "commit 4d0d944 closed
`ManifestSchema`", in a line `c78c9180de` wrote.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants