Skip to content

docs(spec): re-anchor the dead tracker citations in stack.zod.ts and data/analytics.zod.ts to the commits that decided them (stage 7) - #20616

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20234-dead-citations-stack-analytics
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20234-dead-citations-stack-analytics

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #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 #20579 and PR #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 #20611). After: head cc0580d404, board enumerated (185 pages, frontier #20615).

Measurement

file (under packages/spec/src/) dead before after numbers, then anchor
stack.zod.ts 9 0 #10485 ×2 (:415, :1023) to 35ad101bc; #6238 (:633) to c8d6f6e08; #14192 (:1233) to 4d0d9445a; #14686 ×2 (:3037, :3194) to 279431e7a; #14662 ×3 (:4510, :5070, :5293) to 35dffeace
data/analytics.zod.ts 3 0 #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 #12, batch #23, batch #57).

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.

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 script 的 config 契约要接入 #4277 的执行期 parse,先得有判别式(actionType)形态 #4343 and functions: { fn: { handler, effect: 'writes' } } cannot survive objectstack build — lowering emits a shape FlowFunctionEntrySchema rejects #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:
  • 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 (#20610's migrations entry and registry) adds four live numbers and removes none, so 248 also stands at the tip 7510663c87 plus this PR:

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:

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 #20233 area).

Rung. The #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

…data/analytics.zod.ts to the commits that decided them

Twelve comment and docblock sites cited six tracker numbers that answer
404. Each now names the commit on main that decided the rule its line
states: 35ad101 (themes carrier retired), c8d6f6e (functions array
accepts the lowered handler), 4d0d944 (ManifestSchema closed),
279431e (defineStack same-key action refusal), 35dffea (composeStacks
cross-stack action key refusal) and 2306a76 (analytics_cube bound at the
/meta write door). Comment lines only, one for one.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
…tics.zod.ts provenance re-anchoring

Both files ship verbatim through the package's files[] (src/**/*.zod.ts),
so the rewritten comments publish.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:data tooling labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 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 COMPOSE_KEY_DISPOSITIONS (symbol, a top-level const object), composeStacks (symbol, a top-level function))
  • content/docs/getting-started/glossary.mdx (via composeStacks (symbol, a top-level function))

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

  • content/docs/releases/v17/17-3.mdx (via composeStacks (symbol, a top-level function))
  • content/docs/releases/v17/17-4.mdx (via COMPOSE_KEY_DISPOSITIONS (symbol, a top-level const object), composeStacks (symbol, a top-level function))
  • content/docs/releases/v17/index.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 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 — 137 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 7510663c878f13467198525570b87df5235578a5 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7510663c878f13467198525570b87df5235578a5

⚠️ 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 7510663c878f13467198525570b87df5235578a5 → 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: cc0580d404917d546c2a10ae745f95b77f3239be
Local-runs: none

Read (this act, 2026-09-29T08:39Z): card #20234 (body; every comment: triage 5856637615 and the form C it carries from 5749154545 on #19123; the seat-2 pointer 5858331362; the hand-over 5882628946; stages 1 to 6's claims, reports, ACCEPTs and landings, in particular stage 6's record 5885205776 on PR #20606 and its landing 5885602413; the stage-7 claim 5885635758 and the stage-7 report 5886485992), PR #20616 (body, its one comment 5886446833, the 3-file list, the two commits and the net diff against the merge base 0f6dcac5e9, read from the local ref at the head above, plus the base and head blobs of both files), the six anchor commits and the two later line-writing commits c78c9180de and 773a99960a (message, stat, and their own diffs of the files they now anchor), docs/adr/**, docs/NORTH-STAR.md and scripts/adr-anchors/ for the six numbers and the six rules, the source-text readers of the two files, packages/spec/package.json files[], scripts/check-issue-citations.mjs's deferred surfaces, content/docs/references/** at the head for the six numbers, and the whole origin/main tree for any projection of the twelve lines. Nothing was built, run or re-run: the diff and the anchors were read with git, the blobs compared with a comment-stripping residue scan in scratch (a read of the blobs, not a gate), the numbers probed with REST GET against this repository (a GitHub read). The check-runs were read for judgment once, at 2026-09-29T08:32Z.

① Derived judgments

(a) Scope and file surface: right. 3 files: the claim's two, packages/spec/src/stack.zod.ts (+9/-9) and packages/spec/src/data/analytics.zod.ts (+3/-3), and the new .changeset/spec-stack-analytics-provenance-anchors.md (+14). No generated page, and none is owed: no phrase of the twelve lines occurs anywhere else in the origin/main tree (git grep over the tree for a distinctive phrase of each line), content/docs/references/** at the head carries none of the six numbers, and check:docs sits inside Type Check · source gates, success at the read, which is the generator's own verdict that the committed pages equal its output at this head. No governed surface is touched. Head repository is the base repository; first line Part of #20234, no closing keyword; Clause-②: no on the body's second line; draft; assignee os-tesla; no auto-merge armed; no reviews; two commits, both carrying Claude-Session plus Co-authored-by: Claude and no model identifier. Merge base 0f6dcac5e9; origin/main at this act is 889139ceb5, five commits past it (eb4b17c346, 422db788a2, 7510663c87, aa23e2c8fd, 889139ceb5), and none of the five touches either file, the citation gate or its projection helper, so the net diff against current main is the diff judged here. GitHub reads mergeable: true, mergeable_state: blocked (draft). 38 changed lines.

(b) Comment-only, no code token, string, test title, exported string or describe() text moves: right. git diff over the two files: 12 lines removed, 12 added, every one of the 24 beginning with // or * after its indentation. Enclosure and residue, read on the base and head blobs with a string-, template- and regex-aware comment stripper: every one of the 12 changed lines lies inside a comment in both blobs, and the non-comment residue of each file is byte-identical base to head (91,482 residue bytes for stack.zod.ts, 17,250 for analytics.zod.ts), with both line counts equal (5,344 and 853). Controls, mutated in memory only: a code insertion and a string-literal edit into stack.zod.ts each move the residue; a comment-word edit does not. The residue holds every string literal, so no .describe() argument, exported note string or message text moved; no test file is in the diff, so no it or describe title is touched. The dev's TypeScript-token comparison is not re-run and not needed for that verdict.

(c) Numbers: right. Net-removed is exactly 6 numbers on 12 sites: #10485 x2 (:415, :1023), #6238 (:633), #14192 (:1233), #14686 x2 (:3037, :3194), #14662 x3 (:4510, :5070, :5293), #10194 x3 (analytics.zod.ts:404, :407, :485): 9 and 3 per file, equal to the claim's counts and to stage 6's landing. Added-minus-removed is empty in both files; no PR #N stands on an added line; the only numbers on added lines are #4343 and #4976, kept on :633. REST issues/N without redirects, read in this act: all six removed numbers answer 404 (and pulls/14686 answers 404 too, so #14686 was never a surviving pull request either); #4343 and #4976 answer 200 (closed issues); lit control #16862 answers 200 and dead control #16714 answers 404. At the head neither file carries any of the six numbers (git grep at the head). The dev's census over 84 numbers is otherwise not re-run.

(d) Anchor truth: right, 6 of 6. Six distinct 9-hex shas stand on added lines and none on removed lines. git rev-parse --disambiguate answers exactly one object for each; git merge-base --is-ancestor exits 0 for all six against origin/main; every one is a single-parent commit. Rung: no file under docs/adr/**, docs/NORTH-STAR.md or scripts/adr-anchors/ names any of the six numbers, and a keyword read for the six rules (the themes carrier and ThemeSchema, the lowered handler on the functions array member, ManifestSchema under strictObject, collectDuplicateActionKeyErrors and the scope-qualified key rule, analytics_cube in UNREGISTERED_KIND_SCHEMAS) finds only ADR-0021's description of CubeSchema as a semantic layer and ADR-0026's note that ManifestSchema refuses the ui-plugin kind, neither of which is the rule cited. So the commit rung is the right rung for all six, as in stages 1 to 6. Each anchor was read against the sentence it now supports:

(e) Form C over the whole diff: right. Each rewrite leads with commit 9-hex (or [commit 9-hex] / (commit 9-hex) where a marker stood) and says in its own words what was decided; no tracker number or PR #N is added; nothing author-shown is touched, so form D does not arise. Read end to end for sense, all twelve read as one sentence about the act their anchor performed. No site is kept: none of the twelve is read by literal, judged in (f).

(f) Readers: right. The source-text readers of the two files read code, not these comments: compose-stacks-refusal-envelopes.test.ts:279 reads stack.zod.ts to count throw new Error( lines; scripts/check-stack-collection-maps.mjs and scripts/check-skill-top-level-keys.mjs read it to parse the declared collections and keys of STACK_DEFINITION_COLLECTIONS_SHAPE. The 14 files that hold one of the six numbers inside a string literal (kernel/manifest-unknown-keys.test.ts, packages/cli/src/utils/lower-callables.test.ts, the two failure-text lines of check-stack-collection-maps.mjs and the rest) carry them in their own titles or messages, and none contains a phrase of the twelve lines. The two scripts run in Lint & Repo Gates, success at the read.

② Semver level

patch for @objectstack/spec is right and Clause-②: no is right. The package ships bytes from this diff: files[] at origin/main carries dist and src/**/*.zod.ts, so both touched sources ship verbatim with their rewritten comments, and the docblocks reach dist. (b) shows the non-comment residue identical base to head, so no export, key, value or type moves and nothing widens or narrows: WHICH LEVEL keeps a change that moves no public surface at patch, the level stages 1 to 6 took for the same act (21ab410417, 5cf58eb164, 03b19d9cfd, 6154165484, 2123fcca3b, 0f6dcac5e9). skip-changeset would be wrong, since the diff publishes from a released package. The changeset names one package, describes only the comment re-anchoring, carries no tracker number and no model identifier, and Check Changeset is success at the read.

③ Boundary flags

Blocking: none.

Dev deviations, each answered:

  1. None to the file surface, no generated page needed: right, judged in (a).
  2. Two anchors decide lines a later commit wrote (:1233 by c78c9180de, :3037/:3194 by 773a99960a): right, judged in (d); the cited object under form C is what decided, and each sentence is true of its anchor.
  3. A first changeset-measurement grep with its options misplaced, then re-run: a dev-side sequence, not a property of the diff; ② rests on files[] and the residue, not on that grep.
  4. No merge of origin/main: confirmed; the five commits main has gained touch neither file nor the citation gate.
  5. Commit trailers, the model-free pair: confirmed on both commits; the PR body ends with the session-URL footer.
  6. The empty probe push and two commits, no force-push: the PR shows exactly two commits; the rest is not observable from here and not this diff's.
  7. Worktree cleanup: not observable, not this diff's.
    Open questions: the report lists none.

Out-of-scope findings, each judged:

Non-blocking observations: (1) packages/spec/liveness/analytics_cube.json and packages/spec/liveness/README.md each carry #10194, the liveness-ledger class the seat-2 pointer 5858331362 left "for this card's claimant to say"; no stage's claim has said, and this stage's claim rightly did not touch them. For the seat, not this diff. (2) The docs-drift comment 5886446833 lists examples.mdx and glossary.mdx via composeStacks and COMPOSE_KEY_DISPOSITIONS, a symbol-anchored advisory on a comment-only diff; Flag docs affected by code changes is success. Its note that the CI checkout "carried uncommitted changes" is the runner's own state, not this diff's. (3) scripts/check-stack-collection-maps.mjs:554 and :598 print #10485 in the script's own failure text, outside this family (scripts/**).

Escalated: none.

CI at this head, the judging read at 2026-09-29T08:32Z: 33 check-runs, 24 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure, 6 not concluded. In progress: Test Core (1/6), (3/6), (4/6), (5/6), (6/6) and Type Check · workspace; the Test Core and TypeScript Type Check aggregates had not yet been created. So two of the seven required contexts are NOT presumed green here and must be read concluded before the PR is armed: Test Core (the 976 tests of the two files' own suites and the 37 source-text-reading suites the dev ran locally are CI's to confirm; Test Core (2/6) is success) and TypeScript Type Check (Type Check · source gates, · consumer gates and · debt ledger are success; · workspace is open). Success at the read, the other five: Governed Surface Queue Guard, Lint & Repo Gates (the diff-scoped citation verdict node scripts/check-issue-citations.mjs, check:doc-authoring, pnpm lint, check:stack-collection-maps, check:skill-top-level-keys and the repo check:* steps), Build Core, Dogfood Regression Gate (all three shards and the aggregate), Temporal Conformance (live PG + MySQL); and Type Check · source gates (the spec artifact gates: check:docs, check:authorable-surface, check:generated --reconcile-only, check:api-surface and siblings), Check Changeset, Spec property liveness, Dogfood Verify CLI, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the three claim and closing guards, Auto Label and filter. No red run exists to attribute. Of the 79 families the dev derived, the head's runs answer the citation, doc-authoring, lint, repo-check, changeset, generated-artifact, api-surface, liveness, build, dogfood, temporal and governed-surface families now; the test and workspace-typecheck families are the runs still open above. PR is a draft; mergeable: true, mergeable_state: blocked (draft); assignee os-tesla; no auto-merge armed.

Implemented-by: claude/issue-20234-dead-citations-stack-analytics
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 08:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 682873f Sep 29, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20234-dead-citations-stack-analytics branch September 29, 2026 09:12
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…mits that decided them (objectstack-ai#20626)

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

## What changed

This is the second stage of the `domain:services` lane of the
dead-citation sweep. It covers `packages/plugins/plugin-sharing/src/**`
and nothing else. By census, it is the largest package in the lane that
no open PR or in-flight claim holds (the claim, `5886159115`, gives the
order). Later stages cover the other packages, so this PR says `Part of`
and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by stage 1's method (PR objectstack-ai#20609, landed as
`422db788a`). That is **87 sites on 86 lines in 23 files, covering 13
numbers**: the 60 census sites outside the generated headers, and 27
sites in test comments, which the census defers. Each rewritten line now
cites the commit in `origin/main` history that decided what the line
describes, and it says in its own words what that commit decided.

No ADR or ruling-record file records the decision behind any of the 13
numbers (ADR-0131 names objectstack-ai#14484 only as evidence, not as the record of
its ruling), so every anchor is a commit: **13 distinct shas**. No
number was dropped.

Only comments changed. Every touched source file keeps its line count
(87 lines out, 87 in, over 23 files), so no line citation into these
files moves. One of those 87 lines held no dead citation:
`backfill-sys-record-share-organizations.ts:14`, where 「the cliff the
card names」 lost its referent once line 10 named a commit instead of a
card. It now reads 「the cliff that commit pins」, and `3f64fe6c6`'s
backfill test is the one titled 「the cliff」. No code token moves (see
the guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces. Over the whole diff, added minus
removed is 0 or negative for every number, and no number is new to the
diff. No PR number stands on an added line. The one PR spelling in scope
(`PR objectstack-ai#5973`, dead) became its squash commit.

Nineteen dead sites are left on purpose: 1 string literal, 14 test
titles, 1 verbatim ruling quotation and 3 generated file headers (see
the list below).

One more file: a `patch` changeset for `@objectstack/plugin-sharing`,
because the rewritten docblocks ship (see Changeset below).

## Census: `plugin-sharing`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/plugins/plugin-sharing/`. Each run counts as a reading only
because its board frontier equals the newest issue number, read by a
separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
plugin-sharing sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `422db788a`, run 2026-09-29T08:04:49Z to 08:08:19Z |
enumerated, 185 pages, frontier objectstack-ai#20614 (newest objectstack-ai#20614), 18,441 numbers |
2,300 | **63** | 62 | 14 | 13 |
| after | head `a6d231713`, run 08:27:44Z to 08:31:07Z | enumerated, 185
pages, frontier objectstack-ai#20616 (newest objectstack-ai#20616), 18,443 numbers | 2,240 | **3** |
3 | 3 | 1 |

The before count matches the 63 that census `5884031174` read at
`f11b5f20`. The whole-repo drop is 60, exactly this diff's census sites.
The `resolves` tally is 32,803 in both runs, and
`resolves-as-pull-request` (1,891) and `cross-repo-unjudged` (983) did
not move either. The 3 left are the generated headers below. No run was
truncated or discarded: all three enumerations in this stage (two census
runs and the supplementary board below) read 185 pages at the newest
frontier.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes both. So a second
reading runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) and `classifyCitation` over every `.ts` file
under `plugin-sharing/src` (74 files). It uses one board, enumerated by
the gate's own `enumerateBoard` at 08:12:18Z (185 pages, frontier
objectstack-ai#20614, equal to the newest).

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `422db788a` | 1,187 | **106** | 63 | 28 | 1 | 14 |
| after, `a6d231713` | 1,100 | **19** | 3 | 1 | 1 | 14 |

Its src-comment column equals the census's 63, which is the control on
the second instrument. The 989 resolving, 87 pull-request and 5
cross-repo citations are the same in both readings.

## Per-number table

Sites and files count all dead sites in scope at the base (comments and
strings, tests included). `rewritten / left` counts the sites rewritten
and the sites left. Each anchor was read in its message and diff, not
only its subject. It is the commit that decided what the line describes:
its own message or diff names the number it replaces, or, for a
squash-merged PR, it is the merge of that PR.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#5973` | 4/2 | 3/1 | `abeb3751f`:
`HierarchyScopeContext.organizationId` is the tenancy authority, and it
is required. `objectstack-ai#5973` was the PR itself; this is its squash commit |
| `objectstack-ai#6206` | 12/7 | 11/1 | `8e13ca876`: the share-link routes hand
enforcement the whole authz envelope, per maintainer ruling A of
2026-08-07. It is the plugin-sharing half; the spec stages anchor the
contract half at `d7e0b4212`. Three sites name the ruling in words, 「the
full-envelope ruling」, beside `aa4b90d9a`, which applied it |
| `objectstack-ai#6523` | 3/3 | 3/0 | `aa4b90d9a`: 36 contract signatures converge on
the full `ExecutionContext`. The same anchor the spec stages gave this
number |
| `objectstack-ai#8710` | 10/3 | 9/1 | `04d03c3a0`: a deactivated `sys_position`
confers no sharing-rule shares. Its message quotes the 2026-08-15
ruling: access-conferring paths filter, addressing paths do not |
| `objectstack-ai#8792` | 2/1 | 2/0 | `83c661d97`: the bulk-write merge's missing
provenance mark is recorded as ruled (2026-08-15), not oversight |
| `objectstack-ai#8836` | 1/1 | 1/0 | `1850ebbb0`: it pins 「no filter object that can
be vouched 'author' may outlive the request that vouched it」, the
invariant the line names. The same anchor the spec stages gave this
number |
| `objectstack-ai#11671` | 5/5 | 2/3 | `09b4f4e4e`: `os i18n extract --source-hashes`
writes the provenance companion (maintainer ruling objectstack-ai#12069 Option A,
which stays cited). The same anchor stage 1 gave it |
| `objectstack-ai#11674` | 4/2 | 4/0 | `1cba33f16`: the seed loader warns at load time
when a required column is deferred, and the ordering constraint is
written at the four pointer-pair sites, these two among them |
| `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog
gains `record_write_denied` ahead of its emitters. The same anchor the
spec stages gave this number |
| `objectstack-ai#13279` | 3/2 | 3/0 | `6a180e42d`: a permission-store read that
throws raises `AuthzStoreUnavailableError` (503), and each transport
re-raises it rather than laundering it into a 401 |
| `objectstack-ai#13398` | 1/1 | 1/0 | `953a81f4a`: the class ruling on published
logger sinks, applied at this site. `error` is reachable only because
the sink already declares it; growing `error?` onto a published sink is
forbidden. No record of the ruling exists in the repo, and this commit,
which wrote this heading, is its earliest application in history |
| `objectstack-ai#13608` | 23/3 | 21/2 | `fc9ba76a5`: `publicSharing.eligibility` is
held at redemption, not only at mint. The same anchor the spec stages
gave this number |
| `objectstack-ai#14484` | 36/8 | 25/11 | `3f64fe6c6`: `organization_id` is stamped on
every `sys_record_share` write, the stranded rows are backfilled, and
the object is admitted to the tenancy ledger (the 2026-09-02 ruling,
decision batch objectstack-ai#11 item 3) |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each), and every one is an ancestor of the
base (`merge-base --is-ancestor`, exit 0 for all 13). The history is
complete (`--is-shallow-repository` false, 15,073 commits). A
line-origin pickaxe (`git log -S` on each dead line's exact text) found
each line entering either in its anchor commit or in a later commit that
cites that commit's decision. For example, `65759baca` is the consumer
half that cites `aa5994e17`'s key, and `b70a55d62` cites `3f64fe6c6`'s
ledger admission.

Wordings to check, each true of its commit:
- `backfill-sys-record-share-organizations.ts:5`: 「rows that
`SharingService.grant`, before commit 3f64fe6, stranded」. `3f64fe6c6`
is the writer fix, and this module is its backfill.
- `backfill-sys-record-share-organizations.ts:36` and `:124`: 「the
2026-09-02 ruling commit 3f64fe6 applies (decision batch objectstack-ai#11 item 3,
…)」. The verbatim maintainer quotation on line 37 is untouched.
- `share-link-service.ts:841`: 「(published-sink level ruling, commit
953a81f)」. The heading's body already states the ruling (option C
allowed, option B forbidden).
- `exec-context-annotation.pin.ts:7-8`, `sharing-rule-service.ts:12-13`
and `sharing-service.ts:20`: 「since commit aa4b90d (the full-envelope
ruling: no per-site subset contracts)」. `aa4b90d9a`'s message: 「Apply
the … ruling default (converge on the full envelope, keep no per-site
subset contracts)」.
- `share-link-routes.ts:81`: 「[commit 8e13ca8, full-envelope ruling]」,
so that 「the whole point of the ruling」 five lines down still has a
referent.

## The 19 sites left

- **Non-test string (1 site).** `sharing-service.ts:1674` sits inside
the operator-facing `warn` text for a hierarchy scope that was not
widened (「… resolveOwnerIds, objectstack-ai#5973); …」). It is a runtime string, so it
is form D, not form C, and the shrink-only `doc-authoring-prose-id`
baseline already holds it (`sharing-service.ts` → `objectstack-ai#5973: 1`). Left and
listed, as stage 1 left its refusal strings.
- **Test titles (14 sites).** `describe` titles in
`backfill-sys-record-share-organizations.test.ts:185`, `:274`, `:324`,
`:367`, `record-share-organization-stamp.test.ts:194`, `:239`, `:278`,
`:315`, `:353`, `:436`, `sharing-service.test.ts:1798` (the `objectstack-ai#14484`
titles), `share-link-eligibility.test.ts:607` (`objectstack-ai#13608`),
`share-link-enforcement-context.test.ts:226` (`objectstack-ai#6206`) and
`sharing-rule.test.ts:1898` (`objectstack-ai#8710`). Tokens, left as they were.
- **A verbatim ruling quotation (1 site).**
`share-link-service.test.ts:478` is point 2 of the maintainer's
2026-09-01 ruling, quoted verbatim and untranslated. It carries 「沿
objectstack-ai#13608 先例」. AGENTS.md keeps a quoted Chinese ruling in its original
words, and rewriting the quote would rewrite the ruling. Left.
- **Generated headers (3 sites).** Line 8 of the `es-ES`, `ja-JP` and
`zh-CN` `.source-hashes.generated.ts` files carries 「(objectstack-ai#11671, maintainer
ruling objectstack-ai#12069 Option A, extending objectstack-ai#8765 Option B)」. `os i18n extract`
writes that line from `packages/cli/src/utils/i18n-extract.ts`, so the
fix belongs at the producer, the carrier stage 1 named. The hand-written
`translations/index.ts:26` is rewritten here, with the same wording
stage 1 used.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes excluded, base `422db788a` against head. Template
literals are therefore read in context. It ran over all 23 touched `.ts`
files.

- Real run: 96,665 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control in `share-link-service.ts`: 0 files changed,
as expected (exit 0). The first attempt was a no-op: its replacement
still contained the anchor, so `scripts/ablation-replace.mjs` refused it
before the guard ran. It was redone with an anchor the replacement does
not contain.
- Positive control, a code token changed in `share-link-service.ts`
(`Boolean(eligibility),` to `Boolean(eligibility) && true,`): DIFFER
(exit 1).
- Positive control, one digit changed inside the kept
`sharing-service.ts:1674` warn string: DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`. Each restore
was proven byte-identical to the HEAD blob (`ba7fba2e8199`,
`2833b9a1616d`), with `git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-sharing` is included. It says only that the
provenance comments were re-anchored.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, the rewritten comments reach both
halves of `dist`: `3f64fe6c6` appears 6 times in `dist/index.d.ts` and 8
in `dist/index.js`, `fc9ba76a5` 3 and 3, `04d03c3a0` 2 and 2,
`8e13ca876` twice in `index.d.ts`, and `1cba33f16` 4 times in
`index.js`. esbuild keeps only some comments, so the positive controls
are unchanged lines beside rewritten ones that shipped.
`share-link-service.ts:891` is found once in each half, and
`sharing-service.ts:1269` once in `index.js`. A never-written negative
phrase appears nowhere. The only dead number left in `dist` is the kept
`objectstack-ai#5973` warn string.

## Gates (head `a6d231713`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0:
the diff-scoped run judged 9 citations across 11 files, and all 9
resolve.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0, with the
sibling-package prose ids at their baseline and no growth.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `a6d231713` (re-derived after a
fresh `git fetch` at 09:58Z: the same 65, and none of the 8 new `main`
commits touch anything it derives from) derived 65 commands. They
include all 50 derived at dispatch, plus 15 more. All 65 exit 0. `--ran`
reports 65 run, 0 NOT MEASURED, 0 unrun, and exits 0.
- Three gates first exited 3 (PREREQUISITE NOT MET) because the
workspace was only partly built: `check:dual-build-cjs-loads`,
`check:i18n` and `check:type-check-debt`. A full `turbo run build` of
`./packages/*` and `./packages/*/*` then ran under the shared verify
lock (71 tasks, exit 0), and all three exited 0 on their rerun.
`check:dts-closure`, `check:sourcemap-no-sources-content` and
`check:lean-entry-closure` were rerun too, over 71, 68 and 15 built
packages, and exited 0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-sharing test`: 37 files and 913
tests pass. That is every test file in the package, the 12 touched ones
included.
- `pnpm --filter @objectstack/plugin-sharing typecheck` exits 0. Its
main `tsc` program reads the 37 non-test files, and its
`check:test-typecheck` program (`tsconfig.test.json`) reads all 74 files
under `src/`, the 37 test files included (`--listFiles`).
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 23 touched `.ts` files gives 23 files, 0 errors and 0
warnings. All 23 are in eslint's own population (`isPathIgnored` is
false for each). `eslint.config.mjs` never enables type-aware linting
(no `parserOptions.project`, as its own line 328 states), so a comment
edit here cannot move the verdict on any untouched file. The repo-wide
`pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 24 changed files for control bytes finds none.

## Acceptance notes

- **The census instrument did not truncate in this stage.** Three
enumerations read 185 pages each at the newest frontier. The truncation
stage 1 saw (1 run in 5) is carried on objectstack-ai#20556, and this stage changes no
instrument.
- **What stays for later stages.**
- The 3 generated `objectstack-ai#11671` headers, whose producer is
`packages/cli/src/utils/i18n-extract.ts`.
- The `objectstack-ai#5973` warn string (form D, held by the `doc-authoring-prose-id`
baseline), the 14 test titles and the verbatim ruling quotation.
- **Anchors the next stages can reuse.** The same numbers stand
elsewhere in `packages/**/src`: `objectstack-ai#6206` at 53 sites and `objectstack-ai#11674` at 46
(the ordering-constraint note has two sibling copies outside this
package, in `sys-approval-request.object.ts` and
`sys-audit-log.object.ts`). `8e13ca876` / `d7e0b4212` / `aa4b90d9a` and
`1cba33f16` are the anchors used here.
- **Base.** The branch is 8 commits behind `origin/main` (`1322cc72c`,
read at 09:58Z). None of them touches `plugin-sharing` or this
changeset, and none re-anchors any of these 13 numbers, so there was no
merge.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…anularities take effect on the query doors (objectstack-ai#20282, stage 2) (objectstack-ai#20635)

Part of objectstack-ai#20282
Clause-②: yes (narrowing)

Stage 2 of objectstack-ai#20282, under claim `5886559774`
(`session_014EJ1ED8X4MMrT18BhVx4tx`, `domain:spec` seat 2). An AUTHORED
analytics cube's `measures.format` and `dimensions.granularities` now
reach the readers a compiled dataset already reaches. `refreshKey` is
measured only, and nothing is built for it. The card stays open for
stage 3 (descriptions) and for the `refreshKey` decision.

## What changes

One Cube shape has three producers: authored cubes
(`AnalyticsServiceConfig.cubes`, which the CLI threads from
`analyticsCubes`), compiled datasets, and ad-hoc inference. Until now,
both keys were read only on the compiled-dataset path.

- **`measures.format`**
- `analytics-service.ts#withDeclaredMeasureFormats` runs in `queryIn`,
beside the SQL-echo gate.
- Every measure column a query names now carries its cube measure's
declared `format` as `fields[].format`. This holds for every strategy
(NativeSQL, ObjectQL, the delegated fallback) and for both member
spellings.
- A measure that declares no format gets no key. A value already on the
column is never replaced.
- On the dataset door, the value read is the compiler's copy of the
dataset measure's `format`, the same value `enrichResultColumns` writes
anyway.
  - `GET /analytics/meta` is unchanged. See the premise checks below.
- **`dimensions.granularities`**
- `analytics-service.ts#withDeclaredGranularityDefaults` runs on
`query()` and on the `generateSql()` dry run, before the source-field
gates and strategy selection.
- It reads `dataset-executor.ts#declaredDefaultGranularity`, a new
function extracted from `granularityOf`, which now calls it too. Both
producers are therefore read by one rule.
  - The rule, exactly the compiled-dataset path's:
- a single-entry list is the default bucket for a time dimension the
query groups by without stating a granularity;
    - a stated granularity always wins;
    - a granularity outside the list is not refused;
    - a list of two or more states no default;
- a `timeDimensions` entry that carries only a `dateRange`, for a
dimension the query does not group by, stays a filter.
- **Spec** (`packages/spec/src/data/analytics.zod.ts`, after PR objectstack-ai#20616
merged; `origin/main` merged first through
`scripts/pm/os-regen-merge.sh` as `d963f30e33`)
- `MetricSchema.format` and `DimensionSchema.granularities` gain
describes that state the enforcement.
- The metric's example values move from the names "currency" and
"percent" to numeral patterns, the vocabulary the `fields[].format` slot
documents.
- `content/docs/references/data/analytics.mdx` is regenerated with
`gen:docs`.
- **Ledger**
- Both rows in `packages/spec/liveness/analytics_cube.json` go `dead` →
`live`. Each cites its readers as `file#symbol` and the CLI threading
producer.
- The file note's two sentences that named both keys as dead are
rewritten.
- `state-counts/analytics_cube.md` is regenerated with
`gen:liveness-counts`: live/dead goes from 18/9 to 20/7.
- The `analytics_cube` Notes cell in `liveness/README.md`, which listed
both keys among the dead, is rewritten.
- **Changeset**: `@objectstack/spec` minor and
`@objectstack/service-analytics` minor, with a `**BREAKING**` sentence.

## Clause-② (measured arm): yes (narrowing)

- **Widening:** two authored keys take effect, and `fields[].format` is
populated for authored cubes. The contract already declares that member.
- **Narrowing, measured:**
- Method: a throwaway service-seam probe, run once on head and once with
the fill ablated through `scripts/ablation-replace.mjs` (round 1 at
`958251b6ac`; round 3 at `e1383be04d`, blob `9d77adcd798f` →
`94b1bc63165a`, restored to HEAD). The probe is not committed.
- Cube: an authored cube whose `placed_at` declares `granularities:
['month']`, whose `shipped_at` declares two intervals, and (round 3)
which declares `joins: { account: { name: 'crm_account' } }`.

| request (authored cube; `placed_at` declares `['month']`, `shipped_at`
two intervals; `joins: { account }` where named) | fill ablated (= base
behaviour) | head |
|:--|:--|:--|
| custom-SQL measure grouped by `placed_at` | 200 (raw SQL) | **400
`INVALID_FIELD`**, custom-SQL measure (`resolveMeasureAggregation`) |
| cross-object measure (`sum` of `account.balance`) grouped by
`placed_at` | 200 | **400 `INVALID_FIELD`**, cross-object measure
(`planCrossObject`) |
| `count` by `placed_at` with a `where` field over `account` | 200 |
**400 `INVALID_FIELD`**, cross-object filter, `param: where` |
| `count` grouped by a one-interval time dimension over `account`
(`account.created_at`) | 200 | **400 `INVALID_FIELD`**, cannot bucket a
cross-object time dimension |
| `count` by `placed_at` beside a multi-hop dimension
(`account.owner.region`) | 200 | **400 `INVALID_FIELD`**, single-hop
only |
| `avg` by `placed_at` beside a cross-object dimension
(`account.industry`) | 200 | **400 `INVALID_FIELD`**, non-recombinable
measure |
| `count_distinct` by `placed_at` beside a cross-object dimension | 200
| **400 `INVALID_FIELD`**, non-recombinable measure |
| host whose `queryCapabilities` offers raw SQL only (a hand override;
`AnalyticsServicePlugin` wires both): plain `count` grouped by
`placed_at` | 200 | **"No strategy can handle query"** (every newly
bucketed query) |
| control: `count` by `placed_at` beside a cross-object dimension
(recombinable) | 200 | 200 |
| control: `avg` by `placed_at`, no cross-object member | 200 | 200 |
| control: custom-SQL measure grouped by `shipped_at` (two intervals) |
200 | 200 |

- Every 400 is byte-identical (code, status, message, member, param,
cube) to what the same request with `granularity: 'month'` stated by
hand already got: the fill hands the engine path the very query a
hand-stated granularity does. The class is the engine aggregate path's
whole refusal set as the new bucketing reaches it, read from
`ObjectQLStrategy.planCrossObject` and `resolveMeasureAggregation`: a
custom-SQL measure; and, on a cube whose members resolve through
`joins`, a measure or `where` field over a joined object, a
`timeDimensions` entry over a joined object, a multi-hop dimension, and
an `avg` / `count_distinct` measure beside a dimension over a joined
object. `planCrossObject`'s two dataset-definition arms read only
compiled-dataset scope, so they cannot fire on an authored cube. Two
pins: `DECLARED NARROWING` (custom-SQL) and `DECLARED NARROWING, joined
cube` (cross-object measure).
- The changeset carries the `**BREAKING**` sentence (the class and its
remedy) and the disposition `registered
analytics-cube-single-granularity-default-enforced`, a new ADR-0087 D3
semantic entry (round 2, seat note `5889752648` Q4 = B).
- `check-adr-0087-registration`: `✓ 1 declared-breaking changeset(s),
each carrying an ADR-0087 disposition`.
- Round 2 registered it:
`18.analytics-cube-single-granularity-default-enforced.ts` tells authors
that a one-interval list is now a default bucket, including one the
protocol-18 conversion `cube-sub-day-granularities-removed` minted, and
(round 3) which queries grouped by it the engine path refuses: the whole
class above, plus every newly bucketed query on a raw-SQL-only host.
`registry.ts` is regenerated; `spec-changes.json` and the upgrade guide
were regenerated with no change, because neither projects major-18
entries yet.

## Premise checks, against `origin/main` `7510663c87`

- **`format` on `CubeMeta`: not done, because the premise does not
hold.**
- The dataset path surfaces `format` only through `fields[]`. A compiled
dataset's `getMeta` projection is `{ name, type, title }` too.
- `AnalyticsMetadataResponseSchema` records the narrowing for this
(`objectstack-ai#6442`).
- `content/docs/api/data-api.mdx` already sends clients to `fields[]`
for `format`.
- The spec contract files (`contracts/analytics-service.ts`,
`api/analytics.zod.ts`) are outside this claim's surface.
- **`granularities` refusal: none invented.**
- The dataset path never compares a requested granularity against the
list, so there is no refusal to mirror.
- What a multi-entry list should mean ("Supported Granularities") is an
open fork in the report.
- **`refreshKey` census** (tree `958251b6ac`; `packages/services`,
`packages/drivers`, `packages/rest`, non-test):
  - `refreshKey`: 0 hits. The repo-wide control finds 9 files.
- Pre-aggregation, materialized-view and rollup terms: 7 hits, all
unrelated (automation subflow rollups, and a driver-sql built-in column
flag).
- `ICacheService` consumers: plugin-auth rate-limit and secondary
storage, runtime inbound rate limit, dispatcher counter store, sms. None
is in analytics.
- service-analytics reads no job, cache or scheduler service. Its one
cache is the request-scoped label map in
`dimension-labels.ts#withLabelFetchCache` (the lit control, 1 hit).
- A scheduler exists (`service-job`'s `IJobService`: cron, interval and
db adapters), and nothing in analytics uses it.
- Nothing exists that could key on `refreshKey`, so building a cache is
a separate card.

## Verification

Final head `162e6c0f01` (round 3; merge base `f4ce10c89d`), unless a
line says otherwise. Builds and test runs went through
`os-verify-lock.sh`. The `check:*` gates and eslint ran outside it, as
the lock's scope prescribes. So did `gen:docs`, after two lock
acquisitions for `check:generated --fix` timed out in the queue (exit
99).

- **Round 3 (`162e6c0f01`):** service-analytics 136/3185 (includes the
new joined-cube pin), typecheck clean; runtime REST pin 1/3; spec
`src/migrations` 3/161; `migrate-meta-engine-guidance.test.ts` 3/3; spec
`--project repo` 43/761. Lit/dark: with the fill ablated, the pin file
goes 7 red (the 6 prior bucketing and narrowing cases plus the joined
pin), and the probe's E1 to E7 and R1 each answer 200; on head each is
refused. `dispatch-gates --ran`: 115 derived / 115 run / 0 NOT MEASURED,
114 exit 0, 1 exit 1 (`check:platform-checklist`, not a PR gate).
Regenerated artefacts vs `origin/main`: `registry.ts` +62/-0 (the entry
block only), `spec-changes.json` and the upgrade guide identical.
Driver-free merge-tree probe against `2473e26875`: clean, and the
migration and liveness checks are green on the merge tree.
- **Round 2 (`257ab1bc92`):** `migrate-meta-engine-guidance.test.ts`
3/3; spec `--project repo` 43/761; spec `src/migrations` 3/161;
`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`,
`check-adr-0087-registration` green; `dispatch-gates --ran` 115 derived
/ 115 run / 0 NOT MEASURED, 114 exit 0, 1 exit 1
(`check:platform-checklist`, inputs equal to merge base `3f45b6cc13`).
The lines below are round 1's, at `d665865d5b`.
- **Builds (①):**
  - `pnpm --filter '@objectstack/service-analytics...' build`: exit 0.
  - `turbo run build --filter='@objectstack/runtime^...'`: 29/29.
- `pnpm --filter @objectstack/spec build`, after the describe edit: exit
0.
- **Tests (②):**

| suite | files | tests | result |
|:--|--:|--:|:--|
| service-analytics, whole package | 135 | 3178 | pass |
| new service-door file | — | 13 | included above |
| spec `--project local` | 575 | 16917 (+1 todo) | pass |
| spec `--project repo` | 42 | 745 | pass |
| runtime: the new REST pin plus the 2 sibling harness files that
consume service-analytics | 3 | 24 | pass |
| rest: the 7 files that import service-analytics | 7 | 86 | pass (at
`958251b6ac`; service-analytics src is unchanged since) |

- Typecheck is clean for spec, service-analytics and runtime. Runtime's
includes `check:test-typecheck`, and the new file adds no debt.
- `tsc --listFiles` puts the new service test in service-analytics'
program.
- Two consumers were not run, and both are unaffected by construction
because their cubes declare no `format` and no time dimension:
`packages/client` `analytics-automation-json-erasure.test.ts`, and the
dogfood analytics files, which declare neither key.
- **Ablations (predicted before each run; every leg restored to the HEAD
blob with `git diff HEAD` empty):**

| mutation | suite | predicted | observed | at |
|:--|:--|:--|:--|:--|
| format early-return (`9d77adcd798f` → `0d48cfde9469`) | service door |
4 red | 4 red, 8 green | `9bf3b3b0b4` |
| format early-return | REST, through `dist/` | 1 red | 1 red, 2 green |
`9bf3b3b0b4` |
| granularity early-return (`9d77adcd798f` → `94b1bc63165a`) | service
door | 6 red | 6 red, 7 green | `958251b6ac` |
| granularity early-return | REST, through `dist/` | 2 red | 2 red, 1
green | `9bf3b3b0b4` |

- Both REST legs were rebuilt, then checked with
`ablation-dist-preflight` (marker present in 2 built files). Each
restore leg was rebuilt again and checked `--absent`, with the tree
clean.
- **Gates:**
- `dispatch-gates --commands`: 111 derived. `--ran` with exit codes: 111
run, 0 NOT MEASURED. 109 exit 0.
- Two exit 1, both pre-existing. Their inputs are byte-identical to the
merge base `1322cc72c`:
- `check:platform-checklist`: `areas/identity-auth.json` cites
`auth-plugin.ts#twoFactor`, which is absent;
- `check:docs-transcript-drift`: 4 CLI transcripts print "author-time
rules (47)", and the count derives to 46.
- `check:liveness`: `analytics_cube 27 classified (live 20, dead 7)`,
with the state-counts current.
  - `check:generated`: all 15 artifacts up to date.
- **Lint, narrowed:**
- `eslint --no-inline-config --format json` on the 4 changed `.ts`
files: 4 files, 0 errors, 0 warnings.
- The other 4 changed paths (`.md` / `.json`) are reported by eslint
itself as "File ignored because no matching configuration".
- Invariance: `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`), so this diff cannot move a verdict on an
untouched file.

## Acceptance notes

- **File-surface amendments to claim `5886559774`,** each forced by the
claim's own items:
- the `analytics_cube` Notes cell in `packages/spec/liveness/README.md`.
It is the prose half of the state table whose shard the claim names, and
it named both keys as dead.
-
`packages/runtime/src/analytics-authored-cube-format-granularity.test.ts`,
the REST pin the claim asks for "where the analytics harness reaches".
It is a new file, and the runtime harness drives the real dispatcher
route.
- the generated `content/docs/references/data/analytics.mdx`, which the
describes regenerate.
- the D3 entry, the regenerated `migrations/registry.ts` and the
changeset marker, per seat note `5889752648`.
- `packages/services/service-analytics/src/preview-evaluator.ts` still
open-codes the single-entry rule (`dim.granularities?.length === 1`) on
the draft-preview path. That makes it a third spelling beside
`declaredDefaultGranularity`. It is outside this surface; `carrier:`
承接者:无.
- `examples/app-showcase/src/data/analytics/showcase.cube.ts` authors
`done_rate: { format: 'percent' }`. That named style now reaches
`fields[].format` verbatim, and a numeral-pattern renderer does not read
it as a percentage. The spec describe now teaches the pattern
vocabulary. The example's value is outside this surface and is reported
to the seat.
- Carried from stage 1 and unchanged here: the open-core `os serve`
artifact-fallback boot threads no `analyticsCubes`.

---
_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

documentation Improvements or additions to documentation protocol:data size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants