Skip to content

fix(service-analytics): every dataset answer names its base object - #20712

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20644-dataset-answer-object
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20644-dataset-answer-object

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20644

Clause-②: no

This unblocks the consumer card objectstack-ai/objectui#11095. Its DatasetWidget keys record-change refresh on the answer's object, and it can stop waiting once this change is published.

What changed

Premises measured (dispatch A1 to A5)

  • A1, the exits. At defc7f7b there are three: preview :1815, degraded :1922 and main :2069, with no fourth among the method's return statements. object was set only at :1942 (equality drill) and :2022 (date-range drill). So before this change a grouped preview answer lacked object too.

  • A2, the member. The package had exactly two writes of AnalyticsResultWithDrill.object, and the type is module-private. Tests read result.object, and the new pin file reads answer.object against plain AnalyticsResult with no cast. tsc --noEmit is green, and --listFiles holds all 135 src/__tests__ files.

  • A3, the cube side. query() runs queryIn, then the strategy, then applySqlEchoPolicy, and nothing on that path stamps object. The pin: a cube answer over a registered dataset's cube carries none, even after a dataset answer on the same service.

  • A4, other producers. git grep -n queryDataset -- packages finds one implementation, AnalyticsService.queryDataset, and no other producer.

    • MemoryAnalyticsService (driver-memory) implements IAnalyticsService without the optional queryDataset.
    • @objectstack/client's analytics.queryDataset relays the route body, and @objectstack/rest relays the service answer.
    • metadata-protocol's build probes consume the answer.
  • A5, the wire. The route is packages/rest/src/rest-server.ts:11093-11099, and it ends res.json(result). No existing REST-level test covers a dimension-less answer: the route tests that drive the real service all select a dimension. I measured once through the real route handler over the real service (not committed):

    • dimension-less answer: keys fields, object, rows, with object = crm_account;
    • grouped answer: keys dimensionFields, drillRawRows, fields, object, rows;
    • zero-row, dimension-less answer: keys fields, object, rows.

    The committed pins are service-level.

Pins: red first, then the fix

New file src/__tests__/dataset-answer-object.test.ts holds 11 tests:

  • Main exit, on both NativeSQLStrategy and ObjectQLStrategy:
    • dimensions: [] answers object;
    • a zero-row answer answers object, grouped or not;
    • a grouped answer is unchanged: object sits beside dimensionFields and drillRawRows.
  • Draft-preview exit: a dimension-less preview and a grouped preview both answer object.
  • Degraded exit: the answer equals { rows: [], fields: [], totals: [], object: 'opportunity' }, and the unavailable-object warn fired.
  • Negative, on both strategies: a cube query answer carries no object.

Fixture triage: five existing files pinned the answer shape the contract now forbids, and they now expect the base object.

  • The degraded answer without object: three shared EMPTY constants and three inline literals, 11 assertion sites.
  • object absent when no dimension is drillable: one site, in query-dataset.test.ts.

Readings:

  • Red at 4e08d0730 (pins only): 23 failed and 68 passed over the six touched test files.
  • Green at f3fb94dfe: 91 passed.

Ablations

The fix was committed first. Each leg ran through scripts/ablation-replace.mjs in WRAP mode, inside an outer trap that restores on EXIT, INT and TERM. The subject is imported relatively (../analytics-service.js), so it resolves to src/ and no dist leg applies.

  • Leg A: delete the stamp. return { ...answer, object: dataset.object }; becomes return { ...answer }; (anchor 1 to 0, blob f5a69cba to c3d60928).
    • Result: 30 failed and 61 passed.
    • Red: every dataset-side pin (the main exit on both strategies, including grouped-unchanged; both previews; the degraded exit), every triaged fixture, and five pre-existing grouped-drill object assertions in query-dataset.test.ts.
    • Green: the two cube negatives.
    • Restore: blob equals HEAD (f5a69cba) and git diff HEAD is empty.
  • Leg B: stamp a registered dataset's object on the shared queryIn seam. Anchor 1 to 0, blob f5a69cba to 59ed0642.
    • Result: 2 failed and 89 passed. The two failures are exactly the two cube negatives, where 'object' in cube read true.
    • Restore proven the same way.

Verification, at f3fb94dfe

  • Package tests. pnpm --filter @objectstack/service-analytics test: 137 files and 3216 tests passed.
  • Typecheck. pnpm --filter @objectstack/service-analytics run typecheck: exit 0.
  • Consumers of the wire shape.
    • @objectstack/rest: 17 files and 308 tests, every test file that names the dataset route. service-analytics resolved through a dist rebuilt from this head.
    • @objectstack/client: 5 files and 251 tests.
  • Gates.
    • dispatch-gates --commands derived 62 commands, identical to the dispatch-time list. All 62 ran with exit 0, and --ran reads "62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN".
    • The four roster families outside that list also ran green: check-changeset-fixed, check:authz-resolver, check:error-code-casing and check:filter-alias-parity.
    • check:dual-build-cjs-loads first answered exit 3 (PREREQUISITE NOT MET: 44 unbuilt packages). After a full build it was green: 104 require entry points across 66 packages load.
  • Lint, a proven narrowing. The repo-wide pnpm lint is CI's to run.
    • Population: eslint's own config puts the 7 changed .ts files in scope, and isPathIgnored is true for the changeset.
    • Count: eslint --no-inline-config --format json on those files reads 7 files, 0 errors, 0 warnings.
    • Invariance: the config enables no type-aware linting (eslint.config.mjs:327-328, and calculateConfigForFile agrees for all 7). It reads only two baseline JSONs, which this diff does not touch, so no untouched file's verdict can move.

Acceptance notes


Generated by Claude Code

… object (red)

Pins, committed ahead of the fix and red against it:

- the main exit, on NativeSQLStrategy and ObjectQLStrategy: a
  `dimensions: []` answer and a zero-row answer (grouped or not) carry
  `object`; a grouped answer keeps `object` beside its drill-through keys;
- the draft-preview exit: a dimension-less and a grouped preview answer
  carry `object`;
- the degraded "backing object unavailable" exit answers no rows and
  still `object`;
- the negative: a cube `query` answer over a registered dataset's cube
  carries no `object`, even after a dataset answer on the same service.

Five existing files pinned the answer shape the contract now forbids:
the degraded answer without `object` (three shared EMPTY constants and
three inline literals, eleven assertion sites) and `object` absent when
no dimension is drillable (one site). They now expect the base object.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
`queryDataset` set `object` only inside the two drill-through blocks, which
need a drillable dimension and at least one row. A dimension-less answer, a
zero-row answer, the draft-preview answer and the degraded "backing object
unavailable" answer went without it, although `AnalyticsResult.object`
declares it on every dataset answer.

`queryDataset` now wraps its former body (`answerDataset`) and sets
`object: dataset.object` once, on the path all three exits leave through,
as a copy rather than a write onto the answer. The two drill-block writes
and the local `AnalyticsResultWithDrill.object` member are retired; the
four drill sidecars are unchanged. `query()` does not pass through the
wrapper, so a cube answer carries no `object`.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/releases/v16.mdx (via queryDataset (symbol, a method of class AnalyticsService))
  • content/docs/releases/v17/17-5.mdx (via AnalyticsService (symbol, a top-level class))
  • content/docs/releases/v9.mdx (via queryDataset (symbol, a method of class AnalyticsService))

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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 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 57574637129bebb4a6868c465be7e1b80eed1954 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 57574637129bebb4a6868c465be7e1b80eed1954

⚠️ 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 57574637129bebb4a6868c465be7e1b80eed1954 → 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: f3fb94dfe145aa6501ad8b62062b2e5e43fb06d3
Local-runs: none

Inputs read: card #20644 (body and all six comments: the block/unblock transitions, the claim 5897184311, the dev report 5898112319, the ACCEPT 5898215206); card #20647 and PR #20687 (the contract as landed, 35587f76ce); PR #20712's body, file list (8 files, +235/−16) and its net diff against main (merge-base defc7f7b); the check-runs on the head. Read-only throughout: the branch was fetched into the shared checkout's object store and read with git show / git diff; nothing was built, run or re-run.

① Derived judgments

  • Accept sets: none moved — right. The diff touches no Zod schema, no validator, no query-param allowlist, no route and no conversion entry. Files: analytics-service.ts, one new pin file, five existing test files, one changeset.
  • Public TypeScript surface of @objectstack/service-analytics: unchanged — right. queryDataset keeps its signature and its declared return type AnalyticsResult; answerDataset is private; AnalyticsResultWithDrill is a module-private type (no export), so dropping its object member removes nothing a consumer can import. Its four drill sidecars stay on it, untouched (spec(contracts): the dataset answer's drill sidecars (dimensionFields, drillRawRows, drillRawTotals, drillRanges) are emitted by service-analytics and read by objectui, but AnalyticsResult declares none of them #20700's).
  • Emitted shape of a dataset answer: object on every queryDataset answer — right, and already declared. The contract this producer must honour is AnalyticsResult.object and AnalyticsResultResponseSchema.data.object as PR feat(spec): AnalyticsResult declares object, the dataset answer's base object #20687 landed them: every dataset answer must carry it, whatever dimensions are selected and whether or not rows came back; absent on a cube query answer. Read at the head, answerDataset has exactly three method-level returns (the draft-preview return, the degraded { rows: [], fields: [], totals: [] } return, and the main return result); the only other returns in its range sit inside map callbacks of the drill blocks. The public queryDataset wraps it and returns { ...answer, object: dataset.object } once, so all three exits carry it and none can drift; the two former drill-block writes are deleted, so the stamp is the single source. DatasetSchema.object is a required string, so the value is always present.
  • Cube answers: unchanged — right. query() never enters the wrapper. The negative is pinned on both strategies over a registered dataset's own compiled cube, after a dataset answer on the same service (the hardest shape for a leak through the shared queryIn path), and the dev's ablation leg B showed that pin can fail.
  • The card's three pins, each present in dataset-answer-object.test.ts: dimensions: [] answers object (both strategies); a zero-row answer answers object (grouped and KPI, both strategies); a grouped answer is unchanged (toMatchObject with object beside dimensionFields and drillRawRows). Beyond the card: the preview exit (dimension-less and grouped), the degraded exit (toEqual, with the unavailable-object warn asserted so it is provably that exit), and the cube negative.
  • Fixture triage — right, not a weakening. Five existing files pinned the degraded envelope as { rows, fields, totals }; each now carries the base object (opportunity / sys_audit_log). query-dataset.test.ts:286 flips object from toBeUndefined() to toBe('opportunity') on the no-drillable-dimension case while still asserting all three drill sidecars absent — exactly the contract's split between the answer's subject and drill metadata.
  • Other producers: none — the dev's A4 holds on the head tree. git grep queryDataset over packages/ finds one implementation, AnalyticsService.queryDataset; @objectstack/client relays the route body, @objectstack/rest ends res.json(result) at rest-server.ts:11093, metadata-protocol's build probes consume through a shape-tolerant reader, and MemoryAnalyticsService does not implement the optional method. So POST /api/v1/analytics/dataset/query now carries object on dimension-less, zero-row, degraded and preview answers, with no other change to the wire.
  • Docs and generated artifacts: nothing owed — right. No packages/spec path is touched, so no regeneration is due; the docs-drift bot lists only release-owned pages, which are read-only by rule.
  • One gap, not a defect: no committed REST-level pin covers a dimension-less answer; the dev's wire reading (A5) was a one-shot, uncommitted measurement. The card's pins are service-level and all committed, and the REST relay is a pass-through. Noted, not required.

② Semver level

  • Changeset .changeset/20644-dataset-answer-object.md: @objectstack/service-analytics at patch — right. A released package's producer now honours a member the contract already declares; the package's exported API is unchanged and no accept set moves. That is the bug-fix-in-a-released-package case, and skip-changeset would be wrong because the diff publishes.
  • The declaration line — right. Both the PR body and the changeset carry the standalone line Clause-②: no, with no arm. The widening was declared and reviewed on the spec half (PR feat(spec): AnalyticsResult declares object, the dataset answer's base object #20687, Clause-②: yes (widening), @objectstack/spec minor, at-tier PASS 5896169822); this PR moves neither an accept set nor a public surface, so no is the truthful answer and patch satisfies it. No (narrowing) arm is owed: nothing a consumer could write or import is removed — the retired AnalyticsResultWithDrill.object was never exported.
  • ADR-0087: no marker owed — right. Not a breaking changeset; the Check Changeset run on the head is success, and the changeset body still states the before (drill-only object) and after (object on every dataset answer) for the upgrading reader.

③ Boundary flags

Dev report 5898112319 declares open_questions: []. Its five deviations and two out-of-scope findings, each answered:

Check-runs on f3fb94dfe145aa6501ad8b62062b2e5e43fb06d3 (latest run per check name, read at 2026-09-29T20:38Z): 34 runs — 31 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in): path and opt-in skips), none in progress, none failed. All seven required contexts are success: Lint & Repo Gates (the last to conclude, at 2026-09-29T20:35Z), TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard. Check Changeset, Check PR Size (251 changed lines, well under the human-merge threshold) and the card-claim / single-writer guards are success too. The PR is still a draft; readying and arming are the owning seat's acts, on its own re-read of the head.

Implemented-by: claude/issue-20644-dataset-answer-object
Reviewed-by: session_01XY5uCwTjZj7884yYtyur4H

VERDICT: PASS

Rendered 2026-09-29T20:39Z.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 20:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 10c36cc Sep 29, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20644-dataset-answer-object branch September 29, 2026 21:04
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/m tests tooling

Projects

None yet

2 participants