Skip to content

feat(spec): AnalyticsResult declares the four drill-through sidecars - #20720

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-20700-drill-sidecars-declared
Sep 29, 2026
Merged

os-justin merged 2 commits into
mainfrom
claude/issue-20700-drill-sidecars-declared

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #20700

AnalyticsResult and AnalyticsResultResponseSchema.data declare the four drill-through sidecars service-analytics emits on a dataset answer: dimensionFields, drillRawRows, drillRawTotals and drillRanges. Each is optional, with the shape the service emits today. The service's local augmentation stays until its own follow-up.

Clause-②: yes (widening)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1

AnalyticsResult and AnalyticsResultResponseSchema.data declare
dimensionFields, drillRawRows, drillRawTotals and drillRanges, each
optional, with the shapes and conditions service-analytics emits them
under today. Declaration only; the service keeps its local augmentation
until the follow-up retires it.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…ss count

gen:docs renders the four new AnalyticsResultResponse.data rows;
gen:strictness-ledger counts the one new object site (the drillRanges
entry shape), 431 to 432.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
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/spec, touching 6 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via AnalyticsResult (symbol, a top-level interface))
  • content/docs/api/data-api.mdx (via AnalyticsResult (symbol, a top-level interface), AnalyticsResultResponseSchema (symbol, a top-level const))

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

  • content/docs/releases/v16.mdx (via drillRanges (symbol, a field of interface AnalyticsResult), drillRawTotals (symbol, a field of interface AnalyticsResult))
  • content/docs/releases/v17/17-3.mdx (via AnalyticsResult (symbol, a top-level interface))
  • content/docs/releases/v9.mdx (via AnalyticsResult (symbol, a top-level interface))

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

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

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

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 679f95ec5c7c4b0797d10afb60efa8220d92a98a

⚠️ 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 679f95ec5c7c4b0797d10afb60efa8220d92a98a → 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: 852b4cf75c39f33d981e37d1b5ead8ffc2e568f1
Local-runs: none

Inputs, and nothing else: card #20700 body and its four comments (triage 5897666120, claim 5897867458, services finding 5898228574, os-dev-report 5899114237); PR #20720 body, its 7-file list, and the net diff of the two commits 45121017a6 and 852b4cf75c against merge-base 5757463712; the check-runs on the head. Tested against: the producer packages/services/service-analytics/src/analytics-service.ts at the head (unchanged by this PR, so equal to the merge-base) and at origin/main cbaf04c1fd, where #20712 (10c36cc43f) has since moved it; the precedent 35587f76ce (#20687); the objectui pin dd3f7e1be3 from .objectui-sha.

① Derived judgments

Public surface (all additive; judged right). AnalyticsResult (@objectstack/spec/contracts) gains four optional members; AnalyticsResultResponseSchema.data (@objectstack/spec/api) gains the same four optional keys; the generated reference content/docs/references/api/analytics.mdx gains four rows that are the zod describe strings verbatim; the generated strictness ledger api/ count moves 431 to 432 for the one new z.object (the drillRanges entry). No export is added, no member removed, renamed or retyped; the service file is untouched. authorable-surface, api-surface, export-origins and declaration-map have nothing to move.

Type parity with the producer (judged right). The local augmentation AnalyticsResultWithDrill (analytics-service.ts:100-133 at the head; the same four members on origin/main, where #20712 removed only object) declares: dimensionFields a string-keyed map of strings; drillRawRows an array of string-keyed maps of unknown; drillRawTotals an array of arrays of such maps; drillRanges an array of string-keyed maps of { field, gte, lt } strings. The contract declares exactly those four types, and the schema mirrors them as record(string, string), array(record(string, unknown)), array(array(record(string, unknown))) and array(record(string, object{ field, gte, lt })). The compile-time pin AnalyticsResultMatchesContract (api/analytics.test.ts:48) holds the two sides equal; the head's type-check runs are green.

Accept-set of AnalyticsResultResponseSchema (judged right). data is a plain z.object, which strips undeclared keys. After this diff a conforming answer keeps all four keys (widening on the conforming side, pinned by the preservation test); a value under one of the four names that is not the declared shape is now refused at its own path where it used to be stripped silently (a narrowing only on the malformed side, pinned by the four it.each rows). That is the reading the precedent established for object, and the only producer in the tree emits exactly the declared shapes. Contract side: optional members added to an interface widen every reader and constrain only a wrongly-typed writer; the service's own typecheck and the workspace typecheck are green.

Every author-shown or AI-facing sentence, tested against the tree. The zod describe strings and the mdx rows are one text; the contract doc comments are the fuller one.

  • Header block: "Each is set only on a drillable answer, by the conditions stated per member; none is set on a query (cube) answer, on an answer with no rows, or on a draft-preview answer computed over pending seed rows (previewDrafts)." True. The four are assigned only inside queryDataset (:1943, :1946, :1958, :2007) and query never reaches that body. Both blocks gate on result.rows.length; the degraded "backing object unavailable" exit returns { rows: [], fields: [], totals: [] } before them. The preview branch (options.previewDrafts and a resolver and a non-null seed) returns previewResult at :1819, before both blocks; on origin/main that body became answerDataset, the early return is unchanged, and queryDataset adds only object on top, so the sentence is true at the head and on main at landing. When previewDrafts is set but no pending seed exists the call falls through to the live path and can carry the sidecars; the sentence says "computed over pending seed rows", which is exactly that boundary.
  • dimensionFields: "dimension NAME to that dataset dimension's own field (a base-object field, or a relationship.field path, as the dataset declares it)". True. The emission is [d.name, d.field] verbatim off dataset.dimensions (selectedDimensions :2078 maps selection.dimensions onto the dataset's own entries and drops unknown names); DatasetDimensionSchema.field is "Base field, or relationship[.relationship].field path" (ui/dataset.zod.ts); the service test pins { region: 'account.region' } (query-dataset.test.ts:95). A compression, not an error: a multi-hop path (ADR-0071) is emitted verbatim too, and the text spells the single-hop form while saying "path". "Set only on a queryDataset answer that has rows and whose selection groups by at least one dimension with a field that is not type: 'date'": true and, given rows, also sufficient; drillDims is selectedDims.filter(d.field and d.type !== 'date'), the dataset dimension type vocabulary is string | number | date | boolean | lookup and optional, so an untyped dimension counts as non-date exactly as written. "A date dimension is never listed here; its drill scope is drillRanges": true. The api text says "groups by" without "whose selection"; same meaning, since the only dimensions in play are the selection's.
  • drillRawRows: aligned to rows by index, each dimensionFields name to row[d.name], captured before the label pass (:2025 onward, run only when a label resolver is wired): true. "Set exactly when dimensionFields is": same if block, true. "(a select option's value, a lookup's record id)": illustrative and consistent with what the label pass rewrites.
  • drillRawTotals: [i] to totals[i], [i][j] to totals[i].rows[j], each map restricted to the dimensionFields dimensions present in total.dimensions, so the grand total [] yields {} per row: true. "Set only when dimensionFields is and the answer carries at least one totals grouping": if (result.totals?.length) nested in the drillDims block, true.
  • drillRanges: bounds are YYYY-MM-DD except for a datetime source field, whose bounds are ISO-8601 instants at the reference timezone's midnight: true; bound(ymd, instant) goes through zonedDateStartToUtcMs(ymd, rangeTz) only when sourceFieldMeta(...).type === 'datetime', and date or (under UTC) unknown stays calendar; query-dataset.test.ts:342-366 pins 2026-06-01T04:00:00.000Z under America/New_York. Reference timezone "(the selection's timezone, else the request's, else UTC)": selection.timezone ?? context?.timezone ?? 'UTC'; DatasetSelectionSchema.timezone exists and ExecutionContext.timezone is "resolved once per request from the localization settings", so "the request's" names it fairly. "A row whose bucket value is null gets no entry for that dimension": true; bucketKeyToCalendarRange answers null for a null or empty key. Under-stated rather than false: an unparseable or out-of-range key also yields no entry (datetime.ts:358). "Set only on a queryDataset answer that has rows and whose selection groups by at least one type: 'date' dimension with a field and a granularity (the selection's, else the dimension's dateGranularity)": true; resolveDimensionGranularity reads the timeDimensions entry, else selection.dateGranularity, else the dataset default, and "the selection's" folds the two selection-side sources. "Left out when its source field is neither date nor datetime (or its type is unknown) and the reference timezone is not UTC": the four-way branch at :1994-1999, true. "Independent of dimensionFields; a date-only grouping carries drillRanges and none of the equality sidecars": separate block, separate gate, true. The api text omits the omission rule and the timezone chain; its own comment points at the contract "where each one's conditions are stated in full", and "set only" states a necessary condition, so it claims nothing false.
  • (ADR-0021 D2): D2 is "report becomes a pure pivot presentation over a dataset", and its ReportSchema carries drilldown ("click a cell to underlying records, free, dataset-backed"). The ADR names none of the four keys; the citation locates the feature, not the shape, and is the one the producer and objectui's data-objectstack already carry. Sourced by inheritance; not over-broad.
  • POST /analytics/dataset/query in the api text: the route exists (client/src/index.ts:2302 and the core utilities name it); the precedent's object describe uses the same phrase.
  • Changeset: "@objectstack/service-analytics already sets them on a drillable queryDataset answer": true. "A parse with AnalyticsResultResponseSchema keeps them where it used to strip them": true, z.object strips by default and the preservation test cannot pass at base. "Nothing is removed or renamed, and no existing member changes meaning": the diff is additive only, true.
  • PR body: "Each is optional, with the shape the service emits today": true. "The service's local augmentation stays until its own follow-up": true at the head and on origin/main, where fix(service-analytics): every dataset answer names its base object #20712 removed only object from it.

At which moment each is true. Every sentence above is true at the head and on origin/main cbaf04c1fd when this PR lands; none waits on another PR. The regenerated values hold too: main's ledger still reads api/ 431 and neither spec file, the mdx nor either test moved since the merge-base (only five unrelated changesets were added). One forward coupling, not a defect: the preview-exclusion sentence and the two omission rules restate producer behaviour, so a later service change to preview drilling or to the unknown-type-under-non-UTC rule must edit the contract comment in the same landing.

Readers at the objectui pin dd3f7e1be3. Eleven non-test files name at least one key (ten source files plus one demo); drillRawTotals has zero readers, which data-objectstack's CHANGELOG (about line 712) states is deliberate. data-objectstack reads the keys through untyped casts and types a drillRanges entry as its own { field, gte, lt } strings, so the declared shapes are what the readers expect. The diff removes nothing, so the pinned sibling cannot be broken by it.

② Semver level

.changeset/20700-analytics-result-drill-sidecars.md declares '@objectstack/spec': minor, with the standalone line Clause-②: yes (widening); the PR body carries the same line. The diff publishes four optional members on a contract interface and four optional keys on a response schema, removes and renames nothing: a widening. The rule (AGENTS.md, working-rule 3) makes yes take at least minor, and (widening) is the right arm. Same level and arm as the precedent 35587f76ce (#20687). @objectstack/spec is the only package that publishes; service-analytics is untouched, so no second changeset is owed. check:adr-0087-registration owes no marker for a non-breaking changeset. The Check Changeset run is success. Clause-②: yes (widening) is correct.

③ Boundary flags

From the os-dev-report 5899114237 (open_questions: [], one out_of_scope_findings entry, and the deviations its summary reports) and the services finding 5898228574:

  1. Deviation, one path beyond the claim's surface: docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md (generated; api/ 431 to 432). Answered: the file's own header forbids hand-patching; the plus-one follows mechanically from the one new z.object; check:generated (inside Lint & Repo Gates, success) would have failed without it; main still reads 431 so nothing conflicts. Accepted as regenerate-only; the claim's "no service-analytics change" line is intact.
  2. Two stale doc comments on the local AnalyticsResultWithDrill (drillRanges bounds and omission rule; dimensionFields values), the declaration following the emission rather than the comment. Verified: the code, query-dataset.test.ts:95 and :342-366 agree with the declaration. Answered: right call; the comments die with the augmentation in the service follow-up, which is not this PR's file.
  3. Out-of-scope carrier: the service follow-up retiring AnalyticsResultWithDrill (triage's "Afterwards"; the seat files it when this card lands). Since the claim, fix(service-analytics): every dataset answer names its base object #20712 removed object from that augmentation, so the follow-up retires the remaining four members. Escalated to the seat as triage ordered; noted, not filed, no write made here.
  4. drillRawTotals has no present objectui reader. Verified. The direction says declare all four as the service emits them; the wire and client.analytics.queryDataset carry it, and objectui defers it to a totals-row drill. Answered: per direction; no action.
  5. Services finding 5898228574: a previewDrafts seed-row answer carries none of the four, and whether such a preview should drill at all is open. The contract states the current fact and says so in one sentence; whether preview should drill is a service-behaviour question outside this card's file surface. Escalated: it stays open with the service follow-up (or a card of its own), and if the behaviour changes the header sentence must move with it in the same landing.
  6. NOT MEASURED locally by the dev (check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt; client, rest and driver-memory typecheck). Answered by the head's check-runs: Lint & Repo Gates, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates and TypeScript Type Check are all success.
  7. The report's session field is the parent seat's harness-stamped id, as the dev says. Implemented-by below is the branch, the identity of a mode:subagent dev.

Check-runs on the head, read last. 46 raw runs, 35 families after dedupe by name keeping the newest started_at: 31 success, 4 skipped (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in), each conditional or opt-in), 0 failure, 0 in progress. Nothing is still running.

Implemented-by: claude/issue-20700-drill-sidecars-declared
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-29T21:47Z · rendered by the seat's at-tier review subagent on this head. The seat read its served tier family from the subagent transcript before posting.


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 29, 2026 21:49
@os-justin
os-justin enabled auto-merge September 29, 2026 21:49
@os-justin
os-justin added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 671d4c1 Sep 29, 2026
51 checks passed
@os-justin
os-justin deleted the claude/issue-20700-drill-sidecars-declared branch September 29, 2026 22:15
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