Skip to content

fix(service-analytics): a row-scope refusal carries a declared envelope, so queryDataset stops classifying refusals by their wording - #17336

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-17130-declared-envelope-on-analytics-refusals-r2
Sep 10, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-17130-declared-envelope-on-analytics-refusals-r2

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #17335

Clause-②: no — no error-code-ledger row is added. The existing READ_SCOPE_COMPILE_FAILED, already owned by @objectstack/service-analytics (packages/spec/src/api/error-code-ledger.zod.ts:783) and registered for exactly this condition, is reused. No packages/spec/** edit, no new exported symbol on any barrel (isMissingSourceError is exported from its module for the guard to ask, and stays absent from index.ts).

⚠️ This pull request is a re-creation, and its branch is a re-push. The original (#17316, branch claude/issue-17130-declared-envelope-on-analytics-refusals) and its card (#17130) were removed when the os-trump account was suspended. That PR is unreachable (404) yet still holds its branch, so GitHub refuses a second PR on it — hence the -r2 name. The card has been re-filed as #17335.

The commits are byte-identical: this branch points at d1cd397d4810f50e519fe351d616ad4be42a9aa8, the original head, pushed unchanged from the local object store. ⛔ No rebase, no amend, no squash.

The original body and the os-dev-report were lost with the account; what follows is reconstructed from the delivering seat's report. Every number below was measured at d1cd397d4 before the suspension and has NOT been re-run against today's main — CI on this PR is the current reading.

The defect

queryDataset decides between re-raising and degrading to an empty chart. A bare Error carries neither status nor code, so it misses hasDeclaredErrorEnvelope and falls through to isMissingSourceError, which judges by wording — and three of its six substrings (not registered, unknown object, is not a registered object) are exactly the phrasings a registry or security refusal reaches for.

PR #17125's fail-closed row-scope refusal propagates today only because its wording happens to match none of them. ⛔ A coincidence, not a construction: the next reword turns a fail-closed gate into a 200 with no rows.

Two legs, and the cheaper one is the one that keeps working

1 — the guard (landed first, deliberately). refusal-wording-collision.test.ts derives its population from source: every throw in every non-test .ts under the package's src/, read through the TypeScript AST, with wording resolved transitively through in-package message helpers, and the verdict asked of the real isMissingSourceError. It asserts no bare refusal this package raises can match it.

⇒ 22 files, 87 throws, 71 carrying wording, 0 collisions.

⭐ This is the half that protects the next reword rather than this one message. ⛔ Its population is derived, never hand-listed — a hand-listed corpus rots the same way the thing it guards did.

2 — the fix. readScopeUnresolvedError (src/read-scope-refusal.ts) stamps READ_SCOPE_COMPILE_FAILED / 500 on both row-scope resolution refusals — plugin.ts's security bridge and AnalyticsService.resolveReadScopes — so queryDataset re-throws them at hasDeclaredErrorEnvelope and never reaches the heuristic.

isMissingSourceError's subtraction list is untouched; PR #17125's refusal is not reworded; every message is byte-unchanged; no refusal is made likelier to degrade.

Why an existing code rather than a new one

The ledger was enumerated with two independent legs that reconcile: a real JS parse of the ERROR_CODE_LEDGER object literal (lines 190..1266) and a line-anchored regex over the same byte range — both answering owners=29 rows=324 uniqueCodes=282, identical sets. READ_SCOPE_COMPILE_FAILED is registered for "RLS read-scope lowering failed fail-closed — a SERVER fault (500), never the caller's", which is this condition. ⇒ no ledger row, Clause-② stays no, and the PR is landable without a CONTRACT_REVIEW_TIER verdict.

⚠️ One thing a reviewer should weigh: that ledger comment says "lowering", and after this change a second constructor stamps the same code from the resolution stage. Resolution and lowering are two stages of one pipeline with one outcome, and two spellings for one condition class is what ADR-0112 exists to remove — but the comment is now narrower than the code's coverage. Registering a separate READ_SCOPE_UNRESOLVED would be purely additive over what lands here (swap the constant, keep the constructor, keep every message), so choosing reuse now forecloses nothing.

PERMISSION_DENIED / 403 was rejected on a measured ground, not taste: at 4xx the boundary echoes the message, so "read-scope resolution failed for X" would tell a tenant that this deployment's security service is broken. The declared 500 withholds it — the disclosure posture #5367's maintainer ruling settled for this family.

Evidence (measured at d1cd397d4, pre-suspension)

Own package — 104 files / 2243 tests green; typecheck clean, and proven to cover the new files rather than exclude them (tsc --noEmit --listFiles places both new test files in the program).

Acceptance 1 (positive) — three colliding wordings (not registered, unknown object, is not a registered object), raised through the package's own constructor, all propagate out of the real queryDataset. ⛔ Never a 200 with an empty chart.

Acceptance 2 (negative control) — no such table, Postgres's real relation "x" does not exist and MySQL's doesn't exist still degrade to {rows:[],fields:[],totals:[]} with the "backing object … is unavailable" warn; an absent security service still runs unscoped; the pre-existing dataset-degradation-envelope.test.ts (11 cases) green unchanged.

Acceptance 3 (guard controls) — an in-suite synthetic control (one bare colliding throw + one enveloped one; exactly the bare one reported) and a live control on the real tree: BEFORE=0 / AFTER=1 marker counts proving the mutation hit disk, plugin.ts:1142 named in the failure, Tests 1 failed | 3 passed. Restored and proved by state (blob == HEAD blob, empty git diff HEAD, empty git status --porcelain), then Tests 4 passed.

Acceptance 4 (ablation of the fix) — prediction written first; mutate+measure in ONE shell under trap … EXIT INT TERM; on-disk proof BEFORE_CODE=1/BEFORE_STATUS=1 → AFTER_CODE=0/AFTER_STATUS=0. Result Tests 6 failed | 13 passed — exactly the predicted 6, in two distinct shapes: three rows fail on code/status undefined, and three fail with "a fail-closed refusal was served as an empty chart" — i.e. queryDataset resolved, the defect reproduced. ⭐ Predicted green and green: all four wording-guard cases and all three #5033 negative controls — stripping the envelope is invisible to a wording check, which is why the envelope needed pins of its own.

Acceptance 5 (cross-package sweep) — population derived two ways and reconciled: leg A = every workspace manifest depending on @objectstack/service-analytics (6 packages); leg B = every file outside the package naming it (81 files / 18 packages), narrowed to real imports (5 packages, all inside leg A). Positive control: leg A independently contains @objectstack/runtime (owner of analytics-query-read-scope-withhold.test.ts, which pins the wire answer for this very family). Results: service-analytics 104/2243 · rest 187/3118 · runtime 251/3531 · client 41/492 · verify 14/103 · cli 190/2644 · dogfood 135 files + 1064 tests.

Repo-wide lint, not narrowed — full eslint . --no-inline-config: 6477 files, 0 errors, 0 warnings, exit 0.

Gates — 61 families derived, 61 run. --ran printed 0 NOT-MEASURED; that zero is the runner's claim and is annotated by hand instead: check:dual-build-cjs-loads and check:type-check-debt each exited 3 (PREREQUISITE NOT MET) on their first run and were re-run to exit 0 once their prerequisites existed — ⛔ neither counted as a pass on the strength of the first run. Four artifact-roster families whose roster sits under a directory this diff touches were run explicitly (silence is evidence in neither direction), plus check:error-code-provenance because this diff stamps an error code: all PASS.

Known, recorded, not fixed here

missingSourceRelation has a second arm a bare colliding refusal can land in: "… unknown object in the resolved scope" makes the extractor read the relation name as in, so queryDataset re-reports the refusal as a cross-datasource topology error — loud, but describing a JOIN that does not exist. Both arms are wrong for a security refusal; this card is about the silent one, and the envelope closes both arms for the refusals this PR touches. ⛔ Not filed separately because it is the class this card fixes.


Generated by Claude Code

… source

`queryDataset` classifies a BARE error by its words, and three of the six
limbs (`not registered`, `unknown object`, `is not a registered object`) are
the phrasings a registry or security refusal reaches for. The guard walks
every `throw` in this package's non-test sources with the TypeScript AST,
resolves each one's wording through in-package message helpers, and asserts
none of the un-enveloped ones can be read as a driver reporting an absent
table. Population and verdict are both derived: the sites from the AST, the
verdict from `isMissingSourceError` itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…sed refusals

`queryDataset` re-throws any error whose producer declared `code` + `status`
and classifies everything else by wording — six substrings, three of which
(`not registered`, `unknown object`, `is not a registered object`) are what a
registry or security refusal reaches for. A hit is not a wrong status code, it
is `{rows: [], fields: [], totals: []}`: a fail-closed gate served as a
confident empty chart.

The row-scope RESOLUTION stage refused bare on both of its sites, and
propagated only because its text happened to miss all six. Both now carry
`READ_SCOPE_COMPILE_FAILED` / 500 — the code the sibling LOWERING stage has
owned since #5367, so no ledger row is added — through one constructor. Every
message is byte-unchanged: the fix is the declaration, not a luckier string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 7 documentable anchor(s).

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

  • content/docs/plugins/packages.mdx (via AnalyticsServicePlugin (symbol, a top-level class))
What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 9 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 cca1dc0bfb1e380442670843606d6a0c7762f6ba → packageMentionDocs.

Which tree this was computed on

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

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

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

@os-zhuang
os-zhuang marked this pull request as ready for review September 10, 2026 10:36
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 54b3d1d Sep 10, 2026
68 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17130-declared-envelope-on-analytics-refusals-r2 branch September 10, 2026 11:02
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…mit; merged-write rule; account-suspension recovery readings (objectstack-ai#18242)

Fixes objectstack-ai#17374

Second half of the shared-identity rate-limit card. The first half (PR
objectstack-ai#17860, landed as `ed8dea17b`) placed expectations 1 and 4 — the
identity-bound rate-limit rows in `references/rest-channel.md` and the
destroyed-evidence NOT MEASURED line in `.claude/agents/os-dev.md`;
expectation 5 is met by the half-state patrol's H40 row. This PR lands
what was deferred behind same-file serial: expectation 2 (merged
writes), expectation 3 (the account-suspension recovery steps as
readings), the reconciliation of the two quota rows that still
prescribed the same-identity channel switch, and the reviewing seat's
NOT MEASURED line. With this the card's five expectations are all
placed. F5 (GitHub App / machine users) is the maintainer's
infrastructure decision and not this card's; the disciplines here hold
independently of it.

Clause-②: no — no published `skills/**` file moves; no operator or
contract semantics.
skip-changeset — nothing published moves (`.claude/**` and
`scripts/pm/**` sit outside every package's `files[]`).

## What changed — 4 files, +60 / −7, head `694b1d2` on base `b3b43b6`

### `references/platform-readings.md` — 454 → 459 (+5 under the standing
one-file exception)

Two quota-block rows re-conditioned in place (line-neutral; they pay
nothing and buy nothing):

- 「限流、403、传输失败都要试过另一侧才说得出我没手段。」 → 「403 与传输失败要试过另一侧才说得出我没手段;限流先比身份,同 ID
的他侧不是手段。」 (114 B)
- 「⇒ MCP 限流先探 REST 再定退避,⛔ 不据一侧限流把整个平台的写都停掉。」 → 「⇒ MCP 限流先 `GET /user` 比
ID:同 ID 的 REST 满额不是退路,写排队到重置;异 ID 才是。」 (109 B)

Until this PR both rows told a seat to do the exact same-identity
channel switch that `rest-channel.md`'s identity-bound rows (landed by
the first half) forbid. The 403 and transport halves stay as they were:
those are per-side readings, and the row above them (「任一侧的拒绝只是那一侧的读数」)
is untouched. Reads are untouched too — the row below (「MCP
的读限流与写限流彼此独立」) still governs them, and the identity test is itself a
read on the other side. 「写排队到重置」 points at the standing
queue-into-the-patrol-word row (:159) rather than restating it.

Five new rows after the retry-alignment row (the end of the
quota-exhaustion prescriptions), one measured event per row, each under
120 B:

1. 「停用报文 account was suspended 遍及 /rate_limit 与 git,不给理由;非会话门 403、非限流。」
(108 B) — the recognition reading: the third 403 shape beside the
session gate and the rate limit, which is what tells a seat the four
steps below now apply.
2. 「账号停用销毁其名下 PR、卡与评论;分支与 commit 属仓库照留远端 ⇒ 代码从未真丢。」 (112 B) — F3 step 1.
3. 「被销毁的 PR 仍占分支名:API 答 404,同名开新 PR 仍被拒 ⇒ 同批 commit 推新分支名再开。」 (114 B) —
F3 step 2.
4. 「本地对象库是最后备份:复核时 fetch 过的每条分支,其 head 在停用后仍在本地可推。」 (110 B) — F3 step 3.
5. 「重建 PR 正文自报四件:head 逐字节同、无 rebase/amend/squash、数字出自旧基底、CI 为准。」 (113 B)
— F3 step 4.

Dedup table for the exception (候选/落地/已有/拒收): candidates 5 / landed 5 /
already present 0 / refused 0. The family grep `suspended`, `停用`, `销毁`,
`幽灵`, `重推`, `重建`, `对象库`, `分支名` on `origin/main` at `b3b43b6` hits only
the Routine-rebuild rows (:445–:446, :453), the issue-transfer rebuild
row (:144) and the landing criterion (:366); none carries any of the
five, and the later suspension-row family the dispatch word anticipated
does not exist on `origin/main`.

Ratchet: ceiling 454 → 459 in `scripts/pm/check-skill-line-ratchet.mjs`,
with a FIFTEENTH `ruledRaises` record citing the standing exception
verbatim — 「唯一例外:`platform-readings.md` 增量抬上限到落地行数,免决策卡,记 `ruledRaises`
引常设裁决。条件:席位验收评论逐条核实、去重计数(候选/落地/已有/拒收)、一事一行、不计重排」 — and the increment
accounted for line by line beside the ceiling. The cross-file-move
arithmetic re-derives unchanged (`+11 against a net source decrease of
20`).

### `SKILL.md` 〈平台读数纪律〉 — 812 → 812 (equal-line, paid by density)

Added as the section's last line, beside the pre-dispatch `rate_limit`
read:

- 「写少而大:同卡同轮结论合成一条评论,⛔ 不放慢单笔、不攒着一次性发;写量按身份计。」 (117 B)

Paid by folding the two maintainer-abort lines into one:
「维护者中止只在有显式信号时成立:原话,或宿主回报 stopped by the user。」 + 「⛔
不据推断立一道没有重启条件的门;判据是信号不是症状。」 → 「维护者中止只认原话或宿主回报 stopped by the user,⛔
不据推断立无重启条件的门。」 (108 B). Every operative clause survives (explicit signal
only; no inference-built gate without a restart condition); 「判据是信号不是症状」
is the first clause said twice. `references/core-rules.md` :46 already
digests this pair in one line, so the mirror is unchanged (151 / 151).

A2 measured on `b3b43b6`: `grep -c '合并\|少而大\|节流' SKILL.md` counts 0
lines in the write-merging sense — every 合并 hit is merge-queue /
merged-PR vocabulary — against control `限流` 3 lines. The quota rows in
`platform-readings.md` (:103–:105: the per-minute secondary limit, the
~1 s spacing between mutations, the same-second burst that hits the
minute wall) answer the BURST half of the card's shape and not the MERGE
half: nothing on the seat surface said fewer-and-larger, and nothing
said not-slower. The dev side already carries a merged budget
(`os-dev.md` 写预算四笔); the seat side now carries this line.

### `references/review-checklist.md` 〈测试与门禁证据〉 — 77 → 77 (equal-line,
paid by density)

- 「复核项证据已销毁(评论、卡或 PR 答 404)⇒ 记 NOT MEASURED 并写因,⛔ 不记通过或无旗。」 (113 B) —
directly under the INCONCLUSIVE row, so the reviewer's verdict
vocabulary now has all three: INCONCLUSIVE (the positive control
failed), NOT MEASURED (the evidence is destroyed), 不入账 (a dead-tree
reading).

A5 measured: `grep -c 'NOT MEASURED\|销毁\|404'` on `review-checklist.md`
and on `contract-review.md` at `b3b43b6` = 0 and 0 (control
`INCONCLUSIVE` = 2 in review-checklist), so the reviewer had no line to
read; the first half's line lives in the dev's file. Paid by folding the
dead-code deletion pair (:75–:76) into 「以死代码或不可达为由的删除,PM 先在
`origin/main` 用带引号精确名核引用面,再 ACCEPT。」 (113 B): the rule survives whole
and the deleted half (「这是断言不是 diff 里的事实,而这一查只花十秒」) is rationale.

Face note: the claim comment's file surface names
`platform-readings.md`, `SKILL.md` and `core-rules.md`; the dispatch
word's A5 asks for exactly this one reviewer line in
`review-checklist.md` or `contract-review.md`, inside the ratchets, and
this is the one deviation from the claim's list. All 11 open PRs' file
lists were read at 2026-09-15T02:46Z, immediately before this PR was
opened; none touches any file in this diff.

## Acceptance against the card

1. **Positive** (client A refused, client B has quota): the quota
section itself now answers 停 for the same user ID — 「限流先比身份,同 ID
的他侧不是手段」 and 「同 ID 的 REST 满额不是退路,写排队到重置」 — and 「异 ID 才是」 only when `GET
/user` answers a different id. Measured on this container: `GET /user`
on the REST credential answers `os-zhuang` id 277994282, the same login
as the card's assignee, so PM and dev are one identity in this seat —
exactly the case the rule is for.
2. **Negative control** (normal-quota writes unaffected): every changed
rule fires only on a refusal, a suspension or a destroyed record; the
new SKILL.md line says 「⛔ 不放慢单笔、不攒着一次性发」 in so many words, so it can
neither slow a write nor push a seat toward the burst shape. No
transport is banned, no 「下次注意」 is written, no green-widening exemption
is introduced — the card's three 「不要走的路」 hold.
3. **Rehearsed recovery**: on record rather than staged. The incident's
own recovery is the rehearsal — PRs objectstack-ai#17332, objectstack-ai#17334, objectstack-ai#17336 and objectstack-ai#17339
were rebuilt on new branch names from the same commits with
byte-identical heads, each self-reporting the four items row 5 now
spells. A staged suspension is not something this PR can or should
perform; the five rows are the steps that recovery took, written so the
next seat does not rediscover them.
4. **Ablation** (grep line counts, `origin/main` at `b3b43b6` versus
head `694b1d2`, with a lit control in the same file):
- `platform-readings.md`, pattern 停用 / 销毁 / 新分支名 / 对象库: 0 → 4 (row 5's
term 自报 is excluded because :220 already carries it in another sense;
`自报四件` alone reads 0 → 1); control `限流` 10 → 11.
- `platform-readings.md`, pattern 比 ID / 比身份: 0 → 2; the deleted
instruction 先探 REST / 整个平台的写: 1 → 0.
   - `SKILL.md`, pattern 少而大 / 攒着 / 按身份计: 0 → 1; control `限流` 3 → 3.
- `review-checklist.md`, pattern NOT MEASURED / 销毁: 0 → 1; control
`INCONCLUSIVE` 2 → 2.
Remove the new text and the positive case is again answered only by
`rest-channel.md`, with the quota section arguing the other way.

## Mechanism assumptions from the dispatch word, measured

- A1 — confirmed: :123 and :126 on `b3b43b6` prescribed the switch; both
re-conditioned in place as above.
- A2 — measured as above; a rule line was owed, and it lands in
〈平台读数纪律〉.
- A3 — the family grep found no suspension rows on `origin/main`; all
four F3 steps plus the recognition reading were absent and land.
- A4 — recorded under acceptance 3.
- A5 — measured as above; the reviewer line lands in
`review-checklist.md`.

## Gates — head `694b1d2`

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths; change set taken from git off
merge base `b3b43b6ea`, 4 committed paths) derived 40 commands. 39 of
the 40 ran in the foreground with the exit captured by redirect before
any pipe, and every one exits 0; the 40th is the tool's own self-test,
run detached (below). `--ran` on the exit-carrying record reconciles 40
derived / 40 run / 0 unrun.

- `check:pm-skill-ratchet` (self-test + gate): `cross-file move into
platform-readings.md: +11 (314→459, less 134 lines of ordinary ruled
raise) against a net source decrease of 20` · `declared cross-file
moves: 1, total ceilings down 9 lines`.
- `check:pm-dispatch-gates` (the tool's own self-test, 1723 cases):
detached per its header's foreground-cap warning, waited on in the
foreground with `tail --pid`; verdict line `✓ dispatch-gates self-test:
1723 cases pass.`
- The three lint-package families ran after `pnpm --filter
"@objectstack/lint..." build` under `os-verify-lock.sh` (VERDICT
command-exit 0, held 220 s, waited 0 s).
- `check:pm-skill-id-lint`, `check:skill-frame-sync`, `check:nul-bytes`,
`check:ratchet-remedy-authority`, `check:pm-governed-prose`,
`check:required-contexts`: exit 0.
- Three roster families the derivation marked as sitting under
`scripts/` beside this diff were run in addition:
`check-published-list-mirrors`, `check:pm-label-desc-cap`,
`check-skills-token-ratchet` — all three exit 0.
- Not measured locally, CI's: the 3 workflow-value families, the Test
Core job, the 11 wide-population families, the 14 changeset-pending
families (skip-changeset), the remaining artifact rosters; repo-wide
`pnpm lint` not run.
- Control-byte scan on the four files: 0 hits. No changed line over 120
B in the three markdown files. No card number in any added operative
line.

## Acceptance notes

- noted, not filed (承接者: the skills seat, holder of the same-batch
tension ruling): `platform-readings.md` :97 「⛔ 不据限流报文里的 user ID 推池子跨席共用」
and :143 「报文里的 user ID 只是报文」 stay as written — the ratchet's own record
says both hold pending a discriminating read, and the first half's `GET
/user` comparison is that read (the id in the refusal text names the
identity and says nothing about cross-seat pools), so :143's 「只是报文」
could become 「只标身份」 at equal bytes in a later density pass. Not edited
here because the ruling says neither is.
- noted, not filed (承接者: none): `core-rules.md` 〈平台读数纪律〉 carries no
digest of the new write-merging line; the mirror's own header says it
adds no rules, and the claim conditions a mirror edit on a core clause
moving, which none did.

## 维护者速读(草稿)

**改了什么**:三份 PM 席位的规则/事实文件加一份门禁台账。① 事实表 `platform-readings.md`
配额段:把两条还在教席位「限流了就换另一条通道继续写」的行改成「先比身份,同一身份就停写排队」,并新增五行记下账号被停用时的识别信号与恢复步骤(分支和提交不丢、被销毁的
PR 仍占着分支名要换名重推、本地对象库是备份、重建的 PR 要自报)。② 席位规则 `SKILL.md`:加一行「写少而大」——
同一张卡同一轮的结论合成一条评论,既不放慢单笔也不攒着突发,写入量按身份合计;用合并两行既有规则付账。③ 复核清单:复核项的证据被销毁时记
NOT MEASURED,不记通过;同样以合并两行付账。④ 棘轮台账:`platform-readings.md` 上限 454 →
459,按常设例外记录。

**为什么改**:这是 2026-09-10 整个 fleet
被停用的事故复盘卡的后半。前半已把「限流绑定身份」写进通道表;但事实表里还留着相反的指引,席位照读就会重演事故里的那个动作。恢复流程当时是现场摸索出来的,没写下来下次还要摸一遍。写入合并的口径此前完全空白。

**风险与代价(含回滚)**:纯文本规则与事实,不碰任何发布包,不改 CI 行为。代价是 `platform-readings.md` 多 5
行(每行 ≤120 字节,一事一行),其余三份文件行数不变。回滚 = revert 本 PR 的一个
commit,无迁移。误读风险:「写排队到重置」不是「所有写变慢」,新行已明写不放慢单笔。

**席位意见**:

**你要做的**:审批本 PR(受管面,需要你的 approve
后由席位入队);若认为「写量按身份计」或恢复五行的措辞有误,直接改文字或留言,席位按你的话修。

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…commits that decided them (objectstack-ai#20729)

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

## What changed

This is the eighth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-analytics/src/**` and nothing else. By the
seat's census at the claim (`5899485578`), it is the largest package in
the lane that no in-flight work holds. 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 the method of stages 1 to 7 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`). That is **76 sites on 76 lines
in 22 files, covering 14 numbers**:

- 42 census sites (every census site this package has);
- 34 sites in test comments, which the census defers.

The raw scan found no dead site the gate's grammar cannot see (see
Acceptance notes), so there is no third class this time.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **13 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` for
all 14 finds none, and a grep of the rest of `docs/` finds none either),
so every anchor is a commit, per ruling C's order. No number was
dropped.

Only comments changed. Every touched source file keeps its line count
(78 lines out, 78 in, over 22 files), so no line citation into these
files moves. 2 of those 78 lines hold no dead citation: they are reflow
lines, listed under Wordings below. 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: `objectstack-ai#10861` (5 lines), `objectstack-ai#12776` (3),
`objectstack-ai#10413` (2), `objectstack-ai#16750` (2), and `objectstack-ai#10759`, `objectstack-ai#11152`, `objectstack-ai#5716` and the
decision-batch ordinal `objectstack-ai#59` once each. Each tracker number among them
resolves. 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 is the
citation on an added line: the two `PR #N` spellings in scope became
their pull request's squash commit, and `objectstack-ai#16750` stays only as the
convenience link beside `ed7243d52`, on the line it already stood on.

Eight dead sites are left on purpose, all of them test strings (see the
list below).

One more file: a `patch` changeset for `@objectstack/service-analytics`,
because the rewritten docblocks and inline comments ship (see Changeset
below).

The `AnalyticsResultWithDrill` type and its four sidecar members are not
touched: its docblocks carry no dead number (`objectstack-ai#20644`, `objectstack-ai#3214` and
`objectstack-ai#1752` all resolve).

## Census: `service-analytics`, 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/services/service-analytics/`. 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` |
service-analytics sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `cbaf04c1f`, run 2026-09-29T21:41:53Z to 21:45:05Z |
enumerated, 186 pages, frontier objectstack-ai#20721 (newest objectstack-ai#20721 before and after),
18,548 numbers | 1,161 | **42** | 42 | 10 | 10 |
| after | head `967d73531`, run 21:55:23Z to 21:58:36Z | enumerated, 186
pages, frontier objectstack-ai#20723 (newest objectstack-ai#20723 before and after), 18,550 numbers
| 1,119 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim and A1 (42
sites): the two comments PR objectstack-ai#20712 rewrote in `analytics-service.ts` did
not move it. The whole-repo drop is 42, exactly this diff's census
sites. The `resolves` tally is 32,991 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did
not move either. The after run was taken on `967d73531`; the head
`82d2b40b2` adds only the changeset. No run was truncated or discarded:
both enumerations read 186 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 test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `service-analytics/src` (162 files). It
takes its verdicts from the before census's own board reading rather
than from a second enumeration: a number is dead when that census
reported it `allocated-but-absent`, and alive when that census judged it
on this board anywhere (its `--list` extraction) and did not report it.
The 21 numbers the census never saw, because they stand only in test
files or strings here, were read one by one on the issues endpoint: 17
answer 200, and `objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#16918` and `objectstack-ai#17125` answer 404.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `cbaf04c1f` | 3,514 | **84** | 42 | 34 | 0 | 8 |
| after, `967d73531` | 3,438 | **8** | 0 | 0 | 0 | 8 |

Its src-comment column equals the census's 42, which is the control on
the second instrument. The 3,410 live citations and the 20 cross-repo
citations are the same in both readings, and the drop of 76 citations is
exactly the rewritten sites. A third, raw reading (every `#` followed by
2 to 6 digits, whatever surrounds it) finds 3,598 occurrences and 84
dead before, 3,522 and 8 after; its residue equals the gate's residue
site for site, and it sees no dead site beyond the gate.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for each pair).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#11461` | 20/2 | 19/1 | `399ecad58`: a cross-object leaf in one
measure's own `filter` (the third producer, lowered onto
`aggregations[].filter`) is refused on both ObjectQL doors with
`INVALID_FIELD` / 400 naming the measure, folded into the one member
view, with insertion order keeping every earlier refusal's message. The
last line of its message names `objectstack-ai#11461` as the card it settles. New to
the sweep |
| `objectstack-ai#17130` | 17/5 | 13/4 | `54b3d1d4a` (PR objectstack-ai#17336): the row-scope
resolution refusals carry `READ_SCOPE_COMPILE_FAILED` / 500 through one
constructor, so `queryDataset`'s catch re-throws them instead of reading
their words, every message byte-unchanged; plus the source-derived
wording-collision guard. Named in its diff only (18 added lines carry
the tag). New to the sweep |
| `objectstack-ai#17124` | 12/8 | 10/2 | `86c505286` (PR objectstack-ai#17593):
`explicitDateRangeWindow` is the one reading of `dateRange`'s array arm
on all four faces, and an array that is not two string bounds is refused
with `ANALYTICS_DATE_RANGE_UNRECOGNIZED` / 400. Named in its diff only
(its changeset file is `17124-daterange-array-arm-arity.md`). New to the
sweep |
| `objectstack-ai#12209` | 10/5 | 10/0 | `017130a09` (PR objectstack-ai#12318): a custom-SQL measure
is refused on the ObjectQL aggregate path with `INVALID_FIELD` / 400,
keyed on the `EXPRESSION_METRIC_TYPES` partition shared with
`NativeSQLStrategy`. Its message records the two failure modes the lines
describe (`driver-sql` blaming a `function` key, the in-memory evaluator
answering `null` per bucket). Named in its diff only. New to the sweep |
| `objectstack-ai#16778` | 5/1 | 4/1 | `357f4992b`: the compile-leg refusal of an
aggregate a datetime measure's field type cannot carry, scoped to
temporal source fields. The squash commit of the pull request that was
`objectstack-ai#16778`; its subject carries the number. New to the sweep |
| `objectstack-ai#12940` | 4/2 | 4/0 | `aa16721b6` (PR objectstack-ai#13361): this package's
consumer-local `executeAggregate` config mirrors (the plugin options and
`AnalyticsServiceConfig`) narrow `aggregations[].method` to
`AggregationFunction`, after `objectstack-ai#12776` narrowed the contract. Named in
its diff only. New to the sweep |
| `objectstack-ai#17015` | 4/2 | 4/0 | `0da638cd9`: the closed `dateRange` preset
vocabulary is lowered once and the rest refused, the `[range, range]`
fallback is removed from the faces it reached, and the shared
conformance kit holds them. The squash commit of the pull request that
was `objectstack-ai#17015`. New to the sweep |
| `objectstack-ai#16860` | 3/1 | 3/0 | `041d9fdc6`: the object-level read grant is
asked at the analytics door, and its bridge to the `security` service
resolves an explicit three-way (absent admits; throwing or method-less
denies at `error`, finding F3 in its message). The squash commit of the
pull request that was `objectstack-ai#16860`. New to the sweep |
| `objectstack-ai#12248` | 2/1 | 2/0 | `8425c17cc`: the five ruled engine members,
`getDriverForObject?` and `resolveEffectiveDatasource` among them,
adopted onto `IDataEngine`, and `getObject` typed. Its subject names it.
Stage 5's and the spec stage's anchor |
| `objectstack-ai#16685` | 2/2 | 2/0 | `ed7243d52` (PR objectstack-ai#16750): `boolean` / `toggle`
accepted for `sum` / `avg` / `min` / `max` in the aggregate × field-type
table, holding maintainer ruling `objectstack-ai#11152`. Its subject names it. The
spec stage's anchor |
| `objectstack-ai#17125` | 2/2 | 2/0 | `5d12b16e7`: the row-scope bridge tells an
absent security service from a broken one, so a broken one refuses the
query. The squash commit of the pull request that was `objectstack-ai#17125` (404 on
the pulls endpoint too). New to the sweep |
| `objectstack-ai#16918` | 1/1 | 1/0 | `5d12b16e7`: the same commit. Its changeset's
headline names `objectstack-ai#16918` as the card it answers, and its diff writes the
line (`admission-bridge-resolution.test.ts:120`) |
| `objectstack-ai#6123` | 1/1 | 1/0 | `59d1933f9`: `err.code` lands at `error.code`,
not `error.details.code`; the commit that wrote this very line. The
`runtime` stage's anchor |
| `objectstack-ai#13279` | 1/1 | 1/0 | `6a180e42d`: permission-store read failures
fail loud, and the same commit renames
`metadata/src/utils/schema-sync-errors.ts` to
`packages/types/src/driver-error-classification.ts`, the move the line
describes. The anchor of stages 2, 5 and 6, and of the `types`, `rest`
and `runtime` stages |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 13), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13;
control leg: stage 1's landing `422db788a` exit 0; the history is
complete, `--is-shallow-repository` false, 15,135 commits). Each of the
14 numbers answers 404 on the issues endpoint, read one by one;
`objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#17015` and `objectstack-ai#17125` answer 404 on the pulls
endpoint too.

## Wordings to check

- **Bracket tags.** `[#N]` became `[commit SHA]`, as in stage 7;
`[objectstack-ai#10861 / objectstack-ai#11461]` and `[objectstack-ai#10861, objectstack-ai#11461]` keep the live `objectstack-ai#10861` beside
the new sha.
- **The boolean rows, `measure-result-type.ts:115-116` and
`aggregate-datetime-measure-refusal.test.ts:65-66`.** 「objectstack-ai#16685 ruled A,
landed as objectstack-ai#16750」 and 「objectstack-ai#16685 was ruled A and objectstack-ai#16750 added」 became
「commit ed7243d (objectstack-ai#16750) added those rows」 and 「commit ed7243d
(objectstack-ai#16750) added」. 「ruled A」 named an option on the dead card;
`ed7243d52`'s message records the decision itself. Line 116 of the first
file and line 66 of the second are the 2 reflow lines: each keeps the
`objectstack-ai#16750` it already carried.
- **PR numbers, `read-scope-resolution-envelope.test.ts:25` and
`refusal-wording-collision.test.ts:21`.** 「PR objectstack-ai#17125's refusal」 became
「Commit 5d12b16's refusal」, the pull request's squash commit.
- **`read-scope-refusal.ts:29`.** 「objectstack-ai#17130 exists to remove it」 became
「commit 54b3d1d was made to remove it」, the form stage 6 used.
- **`refusal-wording-collision.test.ts:49`.** 「the exact move objectstack-ai#17130
forbids」 became 「the exact move commit 54b3d1d ruled out」; its message
says the fix is the declaration, not a luckier string.
- **`read-scope-resolution-envelope.test.ts:161`.** The verb after the
number moved from present to past tense with the sha.
- **`measure-expression-both-strategies.test.ts:45` and `:166`.**
「deleting the objectstack-ai#12209 arm in」 became 「deleting the arm commit 017130a
added in」, and 「every objectstack-ai#12209 refusal」 became 「every custom-SQL refusal
(commit 017130a)」.
- **`dataset-executor.ts:609`.** 「objectstack-ai#17015's kit」 became 「commit
0da638c's kit」, the conformance kit that commit built.
- **`plugin.ts:116`.** 「and in objectstack-ai#12209:」 became 「and in commit
017130a:」, whose message records the two ways the engine failed.
- **`analytics-service.ts:238`.** 「objectstack-ai#13279 moved it there」 became 「commit
6a180e4 moved it there」; that commit's diff is the rename.

## The 8 sites left

- **Test strings, 8 sites**, left as stages 1 to 7 left theirs, all
`describe` / `it` titles:
  - `crossobject-conjunct-refusal.test.ts:589` (`objectstack-ai#11461`);
  - `aggregate-nontemporal-measure-refusal.test.ts:243` (`objectstack-ai#16778`);
  - `date-range-array-arm-arity.test.ts:213` and `:294` (`objectstack-ai#17124`);
- `read-scope-resolution-envelope.test.ts:155`, `:199` and `:226`, and
`refusal-wording-collision.test.ts:336` (`objectstack-ai#17130`).
- There is no operator string, generated file or quoted ruling carrying
a dead number in this package. It has no generated file at all.

## Mechanical guard: no code token moves

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

- Real run: 26,705 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `plugin.ts` (「refusal buys is in」 to 「refusal earns
is in」): 0 files changed, as expected (exit 0).
- Positive control, a code token added in `plugin.ts` (`field: a.field,`
given `as string`): DIFFER (exit 1).
- Positive control, one digit changed inside a kept test title
(`date-range-array-arm-arity.test.ts:213`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
landed (anchor 1 to 0, blob changed). Each restore was proven
byte-identical to the HEAD blob (`ad3dc9fff4d3`, `a606ffbb6ead`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-analytics`
(`.changeset/20596-service-analytics-provenance-anchors.md`) is
included. Its body is stage 7's, word for word, with the package name
changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build (a cache miss for this package, so
`dist` is this head's source), the rewritten comments reach `dist`:
`399ecad58` 6 times in each of `dist/index.js`, `index.cjs`,
`index.d.ts` and `index.d.cts`; `86c505286` twice in each JS file and
once in each declaration file; `54b3d1d4a` once in all four; `aa16721b6`
once in each JS file and twice in each declaration file; `017130a09`
once in each JS file. Positive controls: the unchanged line 「none of the
coverage: a compiled measure's own」, in the same docblock as the shipped
rewrite at `objectql-strategy.ts:744`, is found once in each of the four
files, and the unchanged line 「back into line. Widening it here again
would not be a local matter」 beside the shipped rewrite at
`analytics-service.ts:559` once in each declaration file. A
never-written negative phrase appears nowhere in `dist`. None of the 14
dead numbers is left anywhere in `dist`.

## Gates (head `82d2b40b2`)

- **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 11 citations across 10 files; 10 resolve and
1 resolves as a pull request (`objectstack-ai#16750`, the convenience link that
already stood on its line).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `82d2b40b2` derived 62 commands:
all 56 derived at dispatch, plus `check:engine-double-contract`,
`check:objectql-double-limit`, `check:query-options-erasure`,
`check:type-check-coverage`, `check:type-check-debt` and
`check:where-matcher`. Each ran with its exit code captured before any
pipe, and all 62 exit 0. `--ran`, fed each command with its exit code,
reports 62 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A
full `turbo run build` of `./packages/*` and `./packages/*/*` ran first
under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an
unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/service-analytics test`: 137 files pass
and 3,216 tests pass. That is every test file in the package, the 12
touched ones included.
- `pnpm --filter @objectstack/service-analytics typecheck` exits 0 (`tsc
--noEmit` on `tsconfig.json`). `--listFiles`: the program holds all 162
files under `src/`, the 137 test files and all 22 touched files
included.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 22 touched `.ts` files gives 22 files, 0 errors and 0
warnings. All 22 are in eslint's own population (`isPathIgnored` is
false for each; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 23 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word
「option」. In this package:
- `#N-word`: 8 lines by a plain grep, and 7 once a hyphen before the `#`
is excluded too, which is the claim's 7. The eighth is
「pre-objectstack-ai#10413-phase-2」 (`execution-context-bridge.test.ts:223`). The
numbers, `objectstack-ai#10413`, `objectstack-ai#5298`, `objectstack-ai#13570` and `objectstack-ai#13640`, all resolve.
- `#A/#B`: 29 lines, the claim's 29, over 28 distinct numbers. All
resolve; `objectstack-ai#2149`, which the census never judged, was read on its own.
  - `option #N`: none.
So nothing here needed a rewrite beyond the gate, and the raw scan
agrees.
- **「This card」 phrases are left.** 113 lines in 39 files of this
package speak of 「this card」, 「that card」 or 「the card」. They carry no
number, neither instrument sees them, and most sit in blocks whose
numbers still resolve. Stage 7 rewrote two such lines as lost referents;
here none is changed, because the phrase runs through the whole package
and rewriting a subset would be arbitrary.
- **Prose that names `queryDataset`'s catch, not changed.** Nine comment
lines say `queryDataset`'s catch. Since `10c36cc43` that catch sits in
the private `answerDataset`, whose docblock calls it the body of
`queryDataset`, so the lines still hold at the level of the public
method. This is not a dead citation, so it is outside this stage.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#11461` →
`399ecad58`; `objectstack-ai#17130` → `54b3d1d4a`; `objectstack-ai#17124` → `86c505286`; `objectstack-ai#12209` →
`017130a09`; `objectstack-ai#16778` → `357f4992b`; `objectstack-ai#12940` → `aa16721b6`; `objectstack-ai#17015` →
`0da638cd9`; `objectstack-ai#16860` → `041d9fdc6`; `objectstack-ai#17125` and `objectstack-ai#16918` →
`5d12b16e7`.
- **Base.** The branch is on `main` at `cbaf04c1f`. `main` has since
moved four commits (`3711e0b76`, `61455de27`, `6afccda5a`, `671d4c164`).
They touch `packages/spec`, `packages/metadata/package.json`,
`pnpm-lock.yaml`, docs and changesets, and no file under
`service-analytics` or in this diff, so no merge was taken; the merge
queue rebuilds on the merged generation. One of them, `671d4c164`,
declares the four drill-through sidecars on `AnalyticsResult` in the
spec. This diff leaves the local `AnalyticsResultWithDrill` untouched,
as the claim requires.

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

---------

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/l tests tooling

Projects

None yet

3 participants