Skip to content

docs(spec): re-anchor the dead tracker citations in kernel/ and contracts/ to the commits and ADRs that decided them (stage 1) - #20326

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20234-dead-citations-kernel-contracts
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20234-dead-citations-kernel-contracts

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20234
Clause-②: no

What changed

This is stage 1 of the staged sweep, covering packages/spec/src/kernel/** and packages/spec/src/contracts/** and nothing else. Later stages cover the other areas, so this PR carries Part of.

Every comment or docblock site in these two areas that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on #19123): 160 sites on 151 lines in 45 files, covering 47 numbers (152 lines rewritten). Each rewritten line now cites the object this repository controls that decided the matter:

  • an ADR or ruling record where one exists;
  • otherwise the commit in origin/main history that decided it.

Each line also says in its own words what that object decided. Where nothing answers, the sentence keeps its reason in words and the number is gone: that happened for four numbers.

Only comments changed. Every file keeps its line count (152 lines out, 152 in), so no line citation into these files moves. No code token moves (see the guard below). String literals carrying a dead number are tokens: 30 such sites are left as they were and listed below.

No citation number is added. Every tracker number on an added line was already on the line it replaces. That includes the two PR numbers now standing beside their shas as convenience links: PR #6900 beside b5404f496, and PR #7211 beside 1507ba356.

Census: this stage's two areas, before and after

Instrument. This is PR #20226's instrument: REST GET /repos/objectstack-ai/objectstack/issues/N without following redirects, over every distinct in-repo number cited in the two areas. The population is:

  • bare #N, objectstack#N, framework#N, and the pre-#N / post-#N spellings, with N of 100 or more;
  • excluding the ordinal heads the citation gate declares (Prime Directive, PD, decision batch) and summon.

Controls. The lit controls were #16862, #16847 and #17698. The dead controls were #16714, #16715 and #16697. They were probed at the start, after every 100 numbers and at the end: 6 checkpoints per run. They read 18 of 18 lit (200) and 18 of 18 dead (404) in both runs.

reading tree numbers probed 200 404 301 or other dead sites kernel contracts lines files dead numbers
before base 6a6a17b62, probed 2026-09-27T20:14Z to 20:16Z 445 398 47 0 190 106 84 180 45 47
after head (probe at eda5c6b27, 2026-09-27T21:24Z to 21:26Z; source identical at the final head) 415 398 17 0 30 18 12 29 14 17

Before, by class.

  • 73 non-test docblock sites and 26 non-test line comments.
  • 22 test docblock sites and 39 test line comments.
  • 28 test string sites (describe and it titles, one assertion message).
  • 2 non-test strings.

After. Only the 30 string sites remain. There are 0 comment sites. The head probe found no number newly dead since the base probe.

PR #20226's area table read kernel 105 and contracts 81 at an earlier base. This census reads 106 and 84 because it also counts the pre-#N / post-#N spelling: 3 dead sites.

Per-number table

The site counts give comments rewritten and strings left. The kind column says what each line now cites:

  • commit: the commit whose diff made the decision the line describes (read in each diff, not only in the subject);
  • ADR: the recorded decision;
  • dropped: the reason is kept in words and the number removed.
number sites / files rewritten / left (string) anchor kind
#5970 2/1 2/0 97e7e3caa: ActionSchema.visible gains the boolean arm commit
#6083 1/1 1/0 ADR-0122 phase 2, 53068c130: find and findOne pinned to the parsed state commit (ADR named)
#6206 12/5 12/0 d7e0b4212: maintainer ruling 2026-08-07, enforcement takes the full envelope with no per-site subset. One site cites 8e13ca876, the route half that restored the five dropped fields. Two name the ruling in words where the same block already cites the sha commit
#6216 2/2 2/0 f586f1a89: one ExecutionContext assembler, closed field-set pin commit
#6300 2/1 2/0 74155c735: find and findOne accept the author state commit
#6361 1/1 1/0 90bbf2510: notification-list cursor retired on both halves commit
#6362 3/2 3/0 b5404f496 (PR #6900 beside it): connector keeps the ADR-0010 envelope commit
#6363 3/1 3/0 17d095413: unreadCount counts the whole inbox, not the window commit
#6483 6/1 6/0 ADR-0005 whitelist, executed by ee58392e1: nine unratified allowOrgOverride: true rolled back commit (ADR named)
#6511 (a PR) 5/1 5/0 d7e0b4212, its squash commit commit
#6523 7/3 6/1 aa4b90d9a: sharing and approval enforcement take the full ExecutionContext commit
#6640 2/2 2/0 2ab1257c9: preserveAudit is UPDATE-only, with a loud INSERT warn commit
#6723 (a PR) 1/1 1/0 8ad609c69, its squash commit: getObject's declared answer commit
#6725 6/2 4/2 1507ba356 (PR #7211 beside it): facade object writes reach the map its reads use commit
#6745 2/1 2/0 7a5ef0008: the getObject-equals-get conformance pin commit
#8715 3/3 3/0 2c86fe3ea: the retirement pin form over export-origins/ commit
#8794, #8836 1/1 each 1/0 each 1850ebbb0: measured the filter-reuse invariant and pinned it commit
#10194 7/2 6/1 2306a765c: theme and analytics_cube bound at the /meta door commit
#10238 1/1 1/0 none: whether cube authoring is live end to end is still its own measurement (559041d39 leaves it open) dropped
#10338 2/1 1/1 d2619fd0c: ApiEndpoint.target optional, the publish gate holds the requirement commit
#10485 5/2 5/0 35ad101bc: themes carrier and ThemeSchema retired commit
#10627 3/2 3/0 be21955ba, whose message records that controlled census commit
#10724 16/5 15/1 be21955ba: nine dead contributes members tombstoned commit
#10726 6/3 4/2 bc56e1881: contributes.routes retired, ruled Option B commit
#10812 2/2 2/0 none: the cloud leg's clean close is kept as its date (2026-08-24, which be21955ba records) dropped
#11071 1/1 1/0 50fb191dc: os generate file names derived from the registry, with the parity pin commit
#11330 2/1 1/1 a9ee98992: trust-tier text states publish-gate-only enforcement commit
#11331 2/1 2/0 none: an open "tracked on" pointer, and the enforce leg is unbuilt. The lines now say so dropped
#11332 9/2 8/1 dce5cd4f0: three dead manifest containers retired commit
#11333 1/1 1/0 aaacf1d5c: the commit that corrected the permissions half commit
#11350 1/1 1/0 ece4dad31, which records the 2026-08-23 entry-nameability ruling commit
#11504 1/1 1/0 f90e82024: FLOW_INPUT_SCHEMA_INVALID registered commit
#11741 5/2 4/1 b706af987: SendEmailInput.organizationId commit
#11846 11/3 7/4 0c2334f6c: preview mode retired commit
#12010 5/2 3/2 none: that ConnectionEngineLike inventory is described in words dropped
#12165 1/1 1/0 b307bfd2a: the glob-discovery disposition recorded beside filePatterns commit
#12248 19/4 15/4 8425c17cc: the five ruled engine members adopted, getObject typed commit
#13135 9/7 8/1 9e0ba21a1: paper customization protocol retired. ADR-0126 section 6 wall 4 stays cited where the line had it commit (ADR named)
#13608 2/1 2/0 fc9ba76a5: eligibility held at redemption commit
#14143 2/2 2/0 f19475c0a: the handler-face ctx.recordLoadDenied signal commit
#14192 4/1 0/4 none rewritten: test titles only (strings)
#14722 2/1 2/0 23c72be3c: pinned the measurement that refuted that card's premise commit
#16559 2/1 1/1 c7aca0dce: ResumeFailureReport declared once commit
#16786 3/2 2/1 6059b29c0: updateById declares its answer commit
#17147 5/2 3/2 aaacf1d5c: the granted permission set is registered and refuses nothing commit
#18335 1/1 1/0 ADR-0090 D10's 2026-09-16 note: an API key is a credential ADR

Every cited sha resolves to exactly one commit, and every one is an ancestor of origin/main (38 shas, each merge-base --is-ancestor exit 0).

The 30 string sites left as tokens

  • Test titles and one assertion message (28 sites).
    • contracts: data-engine.test.ts (4), objectql-engine.test.ts (2), email-service.test.ts, resume-failure-report.pin.test.ts, scoped-context.test.ts and sharing-service.test.ts (1 each).
    • kernel: manifest-unknown-keys.test.ts (4), manifest.test.ts (4), preview-mode-retirement.test.ts (4, one of them an assertion message), plugin-runtime-tier-truthful-text.test.ts (3), and 1 each in metadata-customization-retirement.test.ts, metadata-type-api-registration.test.ts and metadata-type-schemas.test.ts.
  • Non-test strings (2 sites). Two why: strings on rows of the exported METADATA_ROUNDTRIP_CASES table in contracts/metadata-service-roundtrip-conformance.ts (lines 194 and 202). They ship in the package as data. No runtime path prints them to an author: both test drivers title each case by its id. So they are not author-shown text in the form D sense, and they are not rewritten here.

Mechanical guard: no code token moves

The check is a comments-stripped token comparison, base 6a6a17b62 against the head. It uses the TypeScript parser's leaf tokens, so template literals are scanned in context, and it excludes JSDoc nodes. It ran over all 45 touched .ts files.

  • Real run: 60,827 base tokens, 0 files with a token change (exit 0).
  • Comment-insertion control: 0 changes, as expected (exit 0).
  • Positive control (a declaration inserted): 1 file flagged (exit 1).
  • Positive control (one digit changed inside a test-title string): 1 file flagged (exit 1).

Changeset

This change ships bytes, so a patch changeset for @objectstack/spec is included; it says only that provenance comments were re-anchored.

Measured on the built package: rewritten docblocks reach dist/**/*.d.ts. For example, 8425c17cc, 17d095413, aa4b90d9a and fc9ba76a5 each appear in 1 declaration file, and the positive control, a pre-existing notification-service docblock sentence, appears in dist/contracts/index.d.ts. Rewritten comments also reach the bundled .js: be21955ba appears in 20 files. The src/**/*.zod.ts sources ship verbatim through files[].

Gates (head ebbea0b6e)

  • Citation judging pass, run as CI runs it: pnpm check:issue-citations && node scripts/check-issue-citations.mjs exits 0. It judged 23 citations: 21 resolve and 2 resolve as pull requests (the two convenience links).
  • Doc authoring: pnpm check:doc-authoring exits 0.
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 86 families, and all 86 exit 0. --ran reports 86 run, 0 NOT MEASURED, 0 unrun, and exits 0. Five of them (check:doc-formula-expressions, check:dual-build-cjs-loads, check:i18n, check:lean-entry-closure, check:type-check-debt) first exited 3, PREREQUISITE NOT MET. They exited 0 after a full turbo run build of ./packages/* (71 tasks, exit 0, under the shared verify lock).
  • Build, tests, typecheck:
    • pnpm --filter @objectstack/spec build exits 0.
    • vitest run src/kernel src/contracts in packages/spec: 99 files and 1,608 tests pass. That covers all 24 touched test files and every test here that reads contract source text.
    • pnpm --filter @objectstack/spec typecheck exits 0, including check:test-typecheck.

Acceptance notes


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 22 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/spec/src/contracts/approval-service.ts, packages/spec/src/contracts/metadata-service-roundtrip-conformance.ts, packages/spec/src/contracts/scoped-context.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json de091b50e67aec12764eccc18a87b1b4259573e3.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/spec/src/contracts/approval-service.ts, packages/spec/src/contracts/metadata-service-roundtrip-conformance.ts, packages/spec/src/contracts/scoped-context.ts, …) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 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 de091b50e67aec12764eccc18a87b1b4259573e3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e265b0d4f3cf3b7cf89bc06f12cdb97329e765a3 — the merge of head ebbea0b6e9a22b35a49b7fee578bae2d4b6d9986 into base de091b50e67aec12764eccc18a87b1b4259573e3, 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 e265b0d4f3cf3b7cf89bc06f12cdb97329e765a3 && git checkout e265b0d4f3cf3b7cf89bc06f12cdb97329e765a3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin de091b50e67aec12764eccc18a87b1b4259573e3 ebbea0b6e9a22b35a49b7fee578bae2d4b6d9986 && git checkout -B drift-repro de091b50e67aec12764eccc18a87b1b4259573e3 && git merge --no-ff ebbea0b6e9a22b35a49b7fee578bae2d4b6d9986

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

⚠️ 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 de091b50e67aec12764eccc18a87b1b4259573e3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ebbea0b6e9a22b35a49b7fee578bae2d4b6d9986

Read: card #20234 (body, 5856637615, 5858331362, 5859418643, 5860236501), ruling 5749154545 on #19123, AGENTS.md lines 11–18 and 1071–1080, precedent 66e266c, PR #20226 (body, instrument), PR #20326 (object, body, 46-file list, 6 commits, full diff against merge base 6a6a17b, 35 check-runs on the head), the 38 cited commits (subject, message, diff where the line claims more than the subject), ADR-0090 D10, the gate headers of check-issue-citations / check-changeset-no-major / check-empty-changeset and pr-automation.yml's WHICH LEVEL. Ran: sha ancestry and disambiguation, a TypeScript-parser leaf-token comparison over 45 files with three controls, a REST census of 445 numbers at base and head with lit and dead controls at six checkpoints, the citation judging pass in a detached worktree at the head under os-verify-lock, merge-tree against origin/main 10ea9eb, and the CI poll to convergence. NOT MEASURED: the derived gate families other than the citation pass (not re-run by rule), the dist build (ships-bytes taken from files[] and the precedent, not rebuilt), and the objectstack-ai/framework board.

① Derived judgments

(a) Deciding commits — PASS. 38 distinct 9-hex shas appear on added lines; every one resolves to exactly one commit (rev-parse --disambiguate count 1) and every one is an ancestor of origin/main (merge-base --is-ancestor exit 0, 38/38). Subjects read for all 38. 22 of the 38 name their card in the squash subject or body; the 16 whose message names only the PR were judged on their diff or body, and each is the commit that made the change the line now describes: 0c2334f retires 'preview' and PreviewModeConfig; 9e0ba21 deletes metadata-customization.zod.ts and the overlay members; dce5cd4 retires capabilities/configuration/extensions; bc56e18 retires contributes.routes under Option B (in its subject); f90e820 registers FLOW_INPUT_SCHEMA_INVALID; fc9ba76 holds eligibility at redemption; f19475c adds ctx.recordLoadDenied (action-execution.ts, body-runner, quickjs-runner); 7a5ef00 adds packages/objectql/src/metadata-service-getobject-equivalence.test.ts; 2c86fe3 adds api-key-retirement.test.ts importing holdersOf from the export-origins testkit (the pin form the three "Form follows" lines cite); 23c72be's own diff adds the lines "#14722 proposed reshaping the union above … Measured on origin/main 3386493: it does not" (so it is the commit that refuted and pinned); a9ee989 adds plugin-runtime-tier-truthful-text.test.ts and the manifest.runtime text; b307bfd adds the 24-line glob-discovery disposition; 50fb191 adds generate-file-name-registry-parity.test.ts (and its body says Fixes #11071); 6059b29 says Fixes #16786; b706af9 says Fixes #11741 (Decision 2 of #11303); c7aca0d's body names #16559. Claims beyond the subject, checked in the diff or message: 97e7e3c's test diff says "#5970 gave visible a boolean arm" (public-auth-features.ts:310, :328 true); f586f1a adds ENTRY_EXECUTION_CONTEXT_FIELDS and a describe('#6216 — the field set is CLOSED') (execution-context.test.ts:62, execution-context.zod.ts:175 true); 53068c1 flips find/findOne to EngineQueryOptionsParsed (data-engine.ts:251 true); 8e13ca8's message names exactly the five dropped dimensions accessible_org_ids, org_user_ids, systemPermissions, posture, tabPermissions (share-link-service.test.ts:133 "restored by commit 8e13ca8" true); d7e0b42's message says "Implements the maintainer ruling on #6206 (option A, 2026-08-07). Contract half" and it landed before 8e13ca8 (share-link-service.ts:136, sharing-service.test.ts:522 true); 1850ebb's message records the #8794 survey and "adds four pin tests naming the invariant verbatim: no filter object that can be vouched 'author' may outlive the request that vouched it" (security-service.ts:273 true); b5404f4's message says "webhook was measured in the same pass" (metadata-type-schemas.test.ts:264 true); ece4dad's message says "Invariant recorded (maintainer ruling 2026-08-23)" (kernel/index.ts:53 true); aaacf1d's message says #11333's pin "did its job. It is discharged" (plugin-runtime-tier-truthful-text.test.ts:113 true); 2ab1257's subject is "preserveAudit is UPDATE-only — narrow the contract" (data-engine.ts:167 "narrowed it so" true); be21955's message records "cloud census leg discharged clean 2026-08-24. #10627 measured exactly ONE non-test read" (manifest.test.ts:385, :439, manifest.zod.ts:586 true); ee58392's message says 「依据 2026-08-08 维护者三段式裁决(issue #6483)执行」 and cites the ADR-0005 security row. ADR-0090 D10 carries the 2026-09-16 note ruling an API key its owner's credential (batch #139 item 1, 「同意」), so execution-context.zod.ts:233 takes the ruling-record rung correctly. Mismatches found: none. Two wording compressions, both true of the commit and non-blocking, are listed under ③.

(b) The four dropped numbers — PASS. All four answer 404 (probed here). No ADR, scripts/adr-anchors or ruling-record file names any of them at origin/main. #10238: the only in-repo commit naming it is 559041d, whose body says 「#10238 is not prejudged. Whether cube authoring is live end to end remains its own measurement」, so nothing decided it; metadata-type-schemas.ts:278 keeps "whether analytics_cube authoring is LIVE end-to-end is a separate measurement", true. #10812: no commit names it; be21955 records the cloud leg closing clean on 2026-08-24, and both sites (manifest.test.ts:439, manifest.zod.ts:586) keep that date with be21955 cited in the same block, true. #11331: the two commits naming it (b60f48b, f89812e) each kept it as a pointer to unbuilt work and decided nothing about it; manifest.zod.ts:193 and :933 now say "The enforce leg is unbuilt", which those two commits' messages state, with ADR-0025 §3.5 steps 4–7 already cited on :931, true. #12010: 77b91bd's first-commit line carries "(#12010)" but that commit derives ConnectionEngineLike from the contract after 8425c17, and neither it nor 52954c0 records the sweep inventory that data-engine.test.ts:504, data-engine.ts:402 and :479 describe; so no deciding commit exists for what those lines say, and "the third type that ruling's sweep inventoried" / "the member that sweep's ConnectionEngineLike inventory left 'not verified'" stays true.

(c) No code token moved — PASS. Own instrument (tokcmp.mjs, TypeScript 5.9.3 parser, leaf nodes, JSDoc nodes excluded, comments never nodes) over the 45 touched .ts files, base 6a6a17b against head: 60,827 base tokens, 0 files with a token change, exit 0. Comment-insertion control: 0 files, exit 0. Positive control, declaration inserted into data-engine.ts: 1 file DIFFER at token 469, exit 1. Positive control, one digit changed in a data-engine.test.ts describe title: 1 file DIFFER at token 2994, exit 1. The 30 string sites are byte-identical at their lines, 30/30 (and, being tokens, are covered by the 0-change result).

(d) The census — PASS. Own extraction with the gate's grammar (bare, objectstack#, objectstack-ai/objectstack#, framework#, pre-#, post-#; N ≥ 100; ordinal heads excluded), sites classified by the TypeScript scanner as comment or string. Base: 1,712 sites, 445 distinct numbers (16 pre-, 2 post-). Head: 1,552 sites, 415 distinct. REST issues/N without redirects over all 445, 22:16:27Z–22:19:05Z: 398 × 200, 47 × 404, 0 × 301 or other; lit controls #16862 #16847 #17698 = 200 at 6/6 checkpoints, dead controls #16714 #16715 #16697 = 404 at 6/6. Dead sites at base: 190 (kernel 106, contracts 84) on 180 lines in 45 files, 47 numbers; by class 99 non-test comment, 61 test comment, 28 test string, 2 non-test string; 3 of them in the pre-/post- spelling. Dead sites at head: 30 (kernel 18, contracts 12) on 29 lines in 14 files, 17 numbers, all string; 0 comment or docblock sites. No number cited at head is absent from base, so no new dead number. The 47 dead numbers are exactly the dev's per-number table.

(e) Form C compliance — PASS. No tracker number stands as provenance on any added line; every number on an added line already stood on the removed line of the same file (added-minus-removed per file: empty). PR #6900 and #7211 appear only beside b5404f4 and 1507ba3. Judging pass, run as CI runs it, in a detached worktree at the head under os-verify-lock with GITHUB_TOKEN set: pnpm check:issue-citations && node scripts/check-issue-citations.mjs exit 0; self-test 73 cases in 7 batteries; live run judged 23 citations across 21 files, 21 resolve, 2 resolve-as-pull-request.

② Semver level

patch is right and Clause-②: no is right. The rule (pr-automation.yml WHICH LEVEL, maintainer 2026-09-04 on #15294; AGENTS.md step 3): an additive widening of a published surface takes at least minor; a change that moves no public surface stays patch; route 1 requires a changeset for anything that publishes. This PR publishes bytes: packages/spec/package.json files[] carries dist and src/**/*.zod.ts, so 12 touched .zod.ts sources ship verbatim and the docblocks reach dist; the token guard shows no export, key, value or type moved, so nothing widens. check-changeset-no-major's header: no major declared, and its LEVEL axis stands down on a no declaration (not-declared, exit 0). check-empty-changeset's header: the frontmatter names a package ('@objectstack/spec': patch), the file is A not M/D, no foreign changeset is touched. @objectstack/spec is in the fixed group. Precedent 66e266c took patch for the same act. Families not run, by rule.

③ Boundary flags

Blocking: none
Non-blocking: (1) CITATION_RE blind spot, verified against the gate's exported regex: pre-#12248 extracts with qualifier pre- and post-#6640 with post-, so the classifier reads them as another repository and never judges them; 18 such sites at base in these two areas (16 pre-, 2 post-), 3 dead, all 3 rewritten here, 15 remain at head, none dead. A gate-grammar gap outside this PR, carrier-less; a dead citation in that spelling would pass the diff-scoped verdict. (2) metadata-service-roundtrip-conformance.ts:194 and :202: two why strings on exported METADATA_ROUNDTRIP_CASES rows carry #6725; neither driver (packages/spec/src/contracts/…test.ts, packages/objectql/src/…test.ts) reads .why (0 reads each), so they are shipped data, not author-shown text; left as tokens by design, for a later stage. (3) framework#N: the five numbers (3265, 3308, 3366, 3786, 3828) answer 200 here; the objectstack-ai/framework board is NOT MEASURED by this review either. (4) Wording, true of the commit and not blocking: metadata-plugin.zod.ts:829 says "the 2026-08-08 maintainer ruling on ADR-0005, landed in commit ee58392" where the ruling was on the card applying ADR-0005's whitelist; security-service.ts:273 anchors both #8794 and #8836 to 1850ebb, whose message names only the #8794 survey, #8836 being unrecoverable. (5) Merge risk: git merge-tree --write-tree origin/main refs/pr-review/20326 at origin/main 10ea9eb writes tree 8cca13218, exit 0, no conflict; the branch is 11 commits behind (5 when the dev reported).

CI at this head: 35 check-runs, 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke opt-in), 0 failure, converged 22:22:12Z; the seven required contexts (TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard) all success. PR is a draft; mergeable: true, mergeable_state: blocked (draft). Closing keywords: Part of #20234, no closing keyword.

Implemented-by: claude/issue-20234-dead-citations-kernel-contracts
Reviewed-by: session_01CiCTczDo7tGhafXjf61dUJ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 22:27
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 21ab410 Sep 27, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20234-dead-citations-kernel-contracts branch September 27, 2026 22:49
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… and pruned to everyone else (objectstack-ai#20337)

Fixes objectstack-ai#20290
Clause-②: no

This executes triage's grade on the card (comment 5859504238). Triage
decided the carrier is the server, under maintainer ruling 5856774816 on
objectstack-ai#20156 (letter B). Triage's words, verbatim:

> **Carrier, decided here: the server.** Ruling B's own words decide it:
「Read-to-display is pruned per user; read-to-edit is whole for the
editor」, and 「whoever can save it must see it whole, or a save drops
entries silently」. A draft is a stored version, not a rendered one. So
the plain read's `?state=draft` branch takes the same author exemption
PR objectstack-ai#20284 gave the stored-version doors: whole for whoever may save the
app, pruned for everyone else.

## What changed

**The transport** (`packages/rest/src/rest-server.ts`)

- The plain read `GET /meta/:type/:name` now chooses its gate policy by
what it serves. Its `?state=draft` branch serves the pending draft row,
which is a stored version, so it runs
`RestServer.STORED_VERSION_DOOR_POLICY` (`{ arms: 'per-caller', app:
'author-exempt' }`). Every other read on that route keeps `{ arms:
'all', app: 'gate' }`, including the `?preview=draft` render.
- There is no second predicate and no route test in the gate.
`metaItemReadGate` already attaches `MetaReadGateCaller.mayWriteItem`
whenever the policy is `author-exempt`, from
`RestServer.metaSaveVerdict`: the admission of `PUT /meta/:type/:name`,
spelled once by PR objectstack-ai#20284. The draft read inherits that.
- **For an app:**
  - a caller the app's save door admits reads the stored draft whole;
- every other caller who may open the app reads it pruned per caller,
exactly as before;
- an app the plain read refuses whole (an app-level
`requiredPermissions` the caller lacks, or an unpublished app to a
non-builder) is still refused, to an author too.
- **Per-deployment gates:** the ADR-0057 D10 `requiresService` arms
(app, nav entry, dashboard widget) and the nav servability gate no
longer run on the draft read, for any caller. See Acceptance notes item
4 for the measurement and the reasons.
- Unchanged: `NO_DRAFT` (404) when nothing is pending, the docs audience
on `doc` and `book`, and the object mask.

**The shared gate** (`packages/rest/src/meta-item-read-gate.ts`):
docblock only. Three sentences listed the doors each policy member
serves and named "the plain read" as a rendered door without an
exception. They now name the draft branch. No code changed.

**Tests**

- The census `meta-alternate-door-read-gates.test.ts` gains the draft
read as a door, `?state=draft`, of kind `stored`. Its cells sit beside
`/layers`, `?layers=true` and `/diff` for every subject and caller (36
new cells).
- `AUTHOR_EXEMPTION` names four doors now, with `draftCarrier:
'5859504238'`.
- The scope pins hold the exemption to exactly the author's partial
`crm` cells: 4 changed cells and 8 whole refusals kept.
- Each authenticated draft cell asserts that the protocol was asked for
`state: 'draft'`.
- New: `meta-draft-read-author-exemption.test.ts` runs the real stack:
better-sqlite3 `:memory:`, the real `sys_metadata*` objects, a real
`ObjectStackProtocolImplementation` and the real routes. The only stubs
are the auth boundary and the `tenancy` service probe.
- An author holding `manage_metadata` but not `finance.access` reads the
draft whole: the withheld entries, a draft-only entry and one whose
service is off.
  - A member reads it pruned per caller (the control).
- **A draft save by that author keeps `nav_finance_ledger`.** The test
runs the editors' round trip: `/layers` effective, merged with the
stripped draft, saved back through `PUT ?mode=draft`. It then reads the
persisted `sys_metadata` draft row.
- It widens nothing: the author's draft answer equals what `/diff`
already serves them for the same history version.
  - The plain read and `?preview=draft` still prune for the author.
  - A whole refusal (`payroll`) stays `403` on the draft read.

**Docs and release notes**

- `content/docs/ui/apps.mdx` names `?state=draft` among the doors that
answer by who is asking, and states that those doors apply no
per-deployment gate. Its gate-table row now says "the rendered `/meta`
body".
- The two pending release notes are corrected in place (see the section
below). This PR's own note is
`.changeset/20290-draft-read-author-exemption.md`, `@objectstack/rest`
`patch`.

## Verification

**Tests**, at head `bd1361ee6`. `git diff bd1361e 8055937 --
packages/` is empty: the only change since is the one `apps.mdx` table
row.

- Census plus the new real-stack file: 303/303.
- `@objectstack/rest`, `vitest run --project local`: 203 files, 3704
passed, 1 skipped, 0 failed.
- `test:repo`: 8/8.
- `@objectstack/rest` `typecheck` (`tsc --noEmit` plus the test layer
under `tsconfig.test.json`): exit 0. `tsc -p tsconfig.test.json
--listFiles` includes both test files (204 test files in the program).
- `@objectstack/runtime` is not touched and no rest export changed, so
no runtime test is owed. The dispatcher does not serve `?state=draft`;
see Acceptance notes item 5.

**Ablations**, committed fix first, at head `bd1361ee6`.

- Each mutation ran through `scripts/ablation-replace.mjs` in wrap mode,
inside a script with its own EXIT, INT and TERM trap that restores
`packages/rest/src/rest-server.ts` from `HEAD` by absolute path.
- Each restore was proven twice: the blob equals `HEAD` (`d5ccd775bb3b`)
and `git diff HEAD` is empty.
- The subject is imported by relative path (`./rest-server.js`), so the
mutations act on source and no `dist` is involved.
- Each prediction was written down before its run.

| mutation | landed (anchor, blob) | predicted | result |
|:--|:--|:--|:--|
| A: the exemption off on the draft read (`{ arms: 'per-caller', app:
'gate' }`) | 1 → 0, `d5ccd775bb3b` → `4b8f055b9a0e` | 4 red | **4 red**:
the census `?state=draft app/crm × author` cell; real stack: author
reads whole, the draft-save round trip, "widens nothing" |
| B: the exemption for everyone (`mayWriteItem: true`) | 1 → 0,
`d5ccd775bb3b` → `f45c0e2167e9` | 7 red | **7 red**: `app/crm ×
non-reader` on all four stored doors (`?state=draft` included), the
fault-path `/layers` edge, the org-presentation edge, the real-stack
member control |
| C: per-deployment arms back on the draft read (`{ arms: 'all', app:
'author-exempt' }`) | 1 → 0, `d5ccd775bb3b` → `ad2e5f485f20` | 4 red |
**4 red**: `?state=draft dashboard/ops` × reader, non-reader and author;
the real-stack member control (`nav_org_directory`) |

**Gates**, at head `8055937f5`.

- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 91 families from merge base `10ea9eb2e`. Every
command ran with its exit code captured before any pipe.
- `--ran` reconciled the run: 91 derived, 91 run, 0 NOT-MEASURED, 0
UNRUN.
- 90 exit 0. `node scripts/check-empty-changeset.mjs --base origin/main`
exits 1 **by design** (next section).
- `check:skill-examples`, `check:type-check-debt` and
`check:dual-build-cjs-loads` first refused with exit 3, because the
merge moved `packages/spec/src` and the client SDK was unbuilt. All
three exit 0 once their named prerequisites were built:
  - `check:skill-examples`: 259 examples;
- `check:type-check-debt`: 4 entries re-measured, none above its record;
  - `check:dual-build-cjs-loads`: 104 entry points across 66 packages.
- Added by this lane:
- `pnpm lint` (`eslint . --no-inline-config`, the whole repo): exit 0.
It is a full run, not a narrowed one.
  - `pnpm --filter @objectstack/spec run check:liveness`: exit 0.
- The board check `node scripts/check-issue-citations.mjs`, after
merging `origin/main` at `10ea9eb2e`: exit 0 at the merge head, with 6
citations judged and all resolving. At the final head, `--base
10ea9eb`: exit 0, 5 of 5 resolve. `origin/main` then moved to
`a78f731ad`, and `--base origin/main` against that unmerged tip exits 2.
It counts 99 citations that objectstack-ai#20326 removed on `main` as if this change
had added them. They sit in 21 files, none of them in this diff: a
moving-ref reading, not a finding.

## A pending release note is corrected in place, so `Check Changeset`
stays red

`check-empty-changeset` names both notes: "present on the merge base and
CHANGED by this PR". This is the **DELIBERATE CORRECTION** class. Its
remedy text reads 「do NOT restore it -- say so on the PR and get it
confirmed」 and 「this gate stays red either way」. Both notes are pending,
not yet consumed by a Version Packages PR. Each said the plain read
prunes for every caller, authors included, which this PR makes false for
`?state=draft`:

- `.changeset/20156-alternate-door-read-gates.md`, in the `/layers` /
`?layers=true` / `/diff` bullet.
- Before: "The plain read and `/published` prune for every caller,
authors included."
- After: "The plain read and `/published` prune for every caller,
authors included, except the plain read's `?state=draft`: it serves the
pending draft, a stored version, and answers as these three doors do."
- `.changeset/20156-app-author-exemption.md`, in the "Unchanged" bullet.
- Before: "Unchanged: the plain read and `/published` still prune for
every caller, authors included."
- After: "Unchanged: the plain read (its `?preview=draft` included) and
`/published` still prune for every caller, authors included. The plain
read's `?state=draft` is the exception: it serves the pending draft, a
stored version, and answers as these three doors do."

This PR's own note is `.changeset/20290-draft-read-author-exemption.md`.
`skip-changeset` is not applied, because this PR publishes.

## Acceptance notes

All readings below were taken on the real stack described above. The
pre-fix readings are a one-shot probe at `de091b50e`, since removed. Its
caller "author" holds `manage_metadata` only; "member" holds nothing;
"finance author" holds `manage_metadata` and `finance.access`. `tenancy`
is off. The app is published with `nav_leads`, `nav_finance_ledger`
(`finance.access`) and `nav_org_directory` (`requiresService:
'tenancy'`). The finance author saved a draft that adds
`nav_finance_forecast` (`finance.access`).

**Item 1: the site, and the red re-measured through REST.**
- The site is the uncached arm of `GET /meta/:type/:name`: `stateParam`
sets `state: 'draft'` on `getMetaItem`, then the gate call ran `{ arms:
'all', app: 'gate' }`.
- Before the fix, the author's `?state=draft` answered 200 with the
draft's label and navigation `[nav_leads]`. `/layers` answered
`[nav_leads, nav_finance_ledger, nav_org_directory]` on all three
layers.

**Item 2: what the draft read is.**
- At `.objectui-sha` `f8a9d0fb` (a read-only clone),
`MetadataClient.getDraft` sends `GET /meta/:type/:name?state=draft`,
adding `&package=` when scoped, and maps 404 to `null`.
- Both editors use it for their baseline, never `?preview=draft`:
- `StudioDesignSurface.tsx`: about :1759-:1767 for the app (`{ ...eff,
...appDraftBody }`), and about :1885-:1893 for a nav leaf, whose type
can be `dashboard`, `page`, `object`, `report` or `action`;
- `ResourceEditPage.tsx`: about :1015-:1051 on load, :1509-:1518 after a
draft save and :1686-:1699 after a publish.
- With no draft pending, `?state=draft` answers `404` `{ error: "No
pending draft exists for app/NAME.", code: "NO_DRAFT" }` (measured). It
does not fall back to the active version: the protocol stops before the
registry for a draft read.

**Item 3: the premise, which holds on the stop condition as written.**
- `/layers` does **not** serve the draft. `getMetaItemLayered` looks its
overlay up with `state: 'active'` only, and the draft-only
`nav_finance_forecast` was absent from every layer for every caller.
- `/diff` **does**:
- a draft save goes through `SysMetadataRepository.put` with `state:
'draft'`, which appends a full-body `sys_metadata_history` row
(measured: version 1 is the active app, version 2 the draft with 4
entries);
- before this PR, `/diff?from=0&to=2` served the author `[nav_leads,
nav_finance_ledger, nav_org_directory, nav_finance_forecast]`, the
co-author's draft whole, under ruling B's exemption on `/diff`.
- So the exemption on the draft read discloses to an author nothing a
ruled stored-version door does not already serve them whole. The test
"it widens nothing" pins that permanently.
- For a non-author the permission axis is unchanged: `nav_finance_*` is
withheld before and after. Their only change is item 4's, from
`[nav_leads]` to `[nav_leads, nav_org_directory]`, which equals what
`/layers` and `/diff` already served the member before this PR.

**Item 4: the arms.**
- Before the fix, the draft read dropped `nav_org_directory` for
**every** caller, the finance author who holds every entry's permission
included (`[nav_leads, nav_finance_ledger, nav_finance_forecast]`). That
is the same data-loss class: a save of the merged baseline deletes the
entry.
- The census shows the dashboard twin: `dashboard/ops` has its widget
`w_org_kpi` bound to `tenancy`. The design surface loads dashboards
through the same door.
- Hence `STORED_VERSION_DOOR_POLICY`. The per-deployment arms need not
stay for non-authors:
- they withhold nothing from the caller (the `MetaReadGatePolicy`
docblock);
- the draft read is not a render door, since `?preview=draft` is, and it
keeps `arms: 'all'`;
- non-authors already read the per-caller-only answer on `/layers` and
`/diff`.
- Ablation C pins the choice.

**Item 5: the dispatcher.** `packages/runtime/src/domains/meta.ts`'s
item read passes `packageId`, `organizationId` and `previewDrafts` to
`getMetaItem`, and never reads `state`. So on a host that serves only
the dispatcher, `?state=draft` answers the active item under the
rendered policy: never the draft, and never `NO_DRAFT`. It does not
follow this rule because it does not serve this door. Left alone, as
ordered, and reported for objectstack-ai#20320's family. This is a source reading at
`bd1361ee6`, not measured through `dispatch()`.

**Item 6:** see the section above. `content/docs/ui/apps.mdx` is
corrected the same way.

**Triage note 3: another read-to-display baseline saved back in
objectui**, for the objectui seat. This is a source reading at
`f8a9d0fb`, not measured at runtime.
- `useMetadata().apps` is the list read, pruned per caller.
- Two console paths publish an app built from that list, without `mode:
'draft'`:
- `app-shell/src/hooks/useNavigationSync.ts` `saveApp`, via
`NavigationSyncEffect` when a page or dashboard is created or deleted:
`client.meta.saveItem('app', appName, { ...app, navigation: updated })`;
- `apps/console/src/pages/system/AppManagementPage.tsx`
`handleToggleActive` and `handleSetDefault`: `meta.saveItem('app',
app.name, { ...app, ... })`.
- A saving author who is withheld an entry, or an entry whose service is
off here, would publish the app without it.
- A server change here cannot reach these paths: the list read is
read-to-display by ruling B, so the client must read what it saves from
a stored-version door.

**Deviation from the claim's file surface.** The claim admits
`meta-item-read-gate.ts` "only if the policy type needs a member for the
draft read". This PR changes no member there. It edits three docblock
sentences that this change made false, and no code. The change is
declared here for confirmation.

**Observed, not in scope.** The draft reads carry no builder gate: a
member who may open an app reads its pending draft (pruned per caller)
through `?state=draft` and `?preview=draft`. ADR-0037's risk table plans
「confirm/add a builder/admin role gate on the dispatcher reads」. This PR
does not change who may read a draft: the non-author answer is unchanged
on the permission axis. It is reported to the seat.

## Not addressed here

- objectstack-ai#20139 remains open: the bare-number query reads in `rest-server.ts`.
- objectstack-ai#20320 remains open: the dispatcher's divergences. Item 5 above is a
candidate row for it.
- The objectui paths above remain for the objectui seat.

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

---------

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

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants