Skip to content

fix(service-analytics)!: a configured cube or registered dataset over an object the API does not serve is refused at registration, and GET /analytics/meta no longer lists it (#22663) - #22675

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22663-analytics-unexposed-cube-registration
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22663-analytics-unexposed-cube-registration

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22663
Clause-②: yes (narrowing)

A configured cube, or a dataset registered through registerDataset, whose base object or declared join is an object the spec's apiExposureDenialReason denies for the aggregate operation is now refused when it is registered. GET /analytics/meta no longer lists it. This is the card's preferred shape (a). It reuses the decision PR #22645 put at the query door (api-exposure-door.ts), for the same operation, over the same object set. There is no second rule, no new gate, and no lint rule.

Dispatched by the domain:services seat 1 (session session_013j5gkUCpqQiti4GgPqqmnt, seat post #6021) under claim 6096404246. File surface held: packages/services/service-analytics/src/** and one .changeset/22663-*.md. Nothing in packages/spec, packages/lint or content/docs/releases/ is touched.

Step 1: census (before any edit, on origin/main ee3ae0360d)

Producer Count Over a denied object?
Authored cubes (analyticsCubes / defineCube), examples and platform 1: showcase_delivery (showcase_task, joined to showcase_project) No. Neither object declares an enable block.
Datasets (defineDataset): examples/** 6, platform-objects 5 11 No. The five sys_* bases grant list or are not declared in this repo (sys_package_installation is a cloud object). None of the eleven enters the cube registry: stack datasets are compiled per request by queryDataset (request scope), never by registerDataset.
registerDataset callers outside tests 0 n/a
Writers of service.cubeRegistry / inferFromObject outside tests 0 n/a
analytics:ready listeners that register cubes 0 n/a
Dogfood suites (packages/qa/dogfood) with a cube or dataset over an apiEnabled: false / no-list object 0 n/a
Test fixtures 2: hidden_cube and nolist_cube in api-exposure-door.test.ts (PR #22645) Yes. Re-triaged below.

There is no real producer that shape (a) breaks, so shape (b) (keep the registration and hide the cube from getMeta) was not taken. Deployed and cloud-held definitions were NOT MEASURED.

Registration entry points

The before column was read on ee3ae0360d.

Door Reached by Bad definition, before After
AnalyticsServiceConfig.cubes the plugin's cubes, which os serve fills from analyticsCubes (CAPABILITY_PROVIDERS.analytics), inside the plugin's init() accept (no check at all) warn and skip: one warn per cube names the cube, the object and the declaration, and every other cube still registers
AnalyticsServiceConfig.datasets constructor → registerDataset accept; a dataset that cannot compile was warned and skipped (the #5115 channel) warn and skip, through that same existing channel
registerDataset() an embedder accept; a dataset that cannot compile threw refuse: throws a located error and registers nothing
service.cubeRegistry.register() / registerAll() / inferFromObject() the public cubeRegistry field (no in-repo caller) accept refuse: the registry's own admission throws, and nothing is stored
queryDataset() / ad-hoc query() / generateSql() requests never write the shared registry (#20356, #20381) unchanged

Refusal channel, measured and reused. The constructor already had a per-definition boot channel: a pre-registered dataset that cannot compile is warned and skipped, so the boot continues and the other datasets register (dataset-cross-datasource-registration.test.ts, #5115). Config cubes now take that channel. registerDataset already threw for a definition it could not compile, and it now throws for this one too. The thrown error carries the query door's envelope for the same declaration:

  • code: OBJECT_API_DISABLED, status: 404 for enable.apiEnabled: false;
  • code: OBJECT_API_METHOD_NOT_ALLOWED, status: 405 for an enable.apiMethods whitelist that does not grant the operation, plus allowed.

Both carry object. The message names the definition, the object, the declaration and the denial reason. Both codes are already ledgered under @objectstack/service-analytics (check:error-code-provenance OK).

Level. The boot-time line is warn, as for its dataset sibling. Under AGENTS.md's degradation rule this is a functional degradation: the refused cube is visibly absent from getMeta, and nothing claimed as persisted is lost.

Timing finding

The service registers configured cubes when the plugin's init() constructs it, so an object is judged only if its declaration exists at that moment. I measured this with a throwaway probe: a LiteKernel with the real ObjectQLPlugin, three apiEnabled: false objects registered through the manifest service, and an observer plugin inited just before analytics. The probe is not committed.

Object registered Declared when analytics inits? getMeta before this change getMeta after
by a plugin whose init() precedes analytics yes listed refused
by a plugin whose init() follows analytics no listed listed (window)
in a plugin's start() no listed listed (window)

After boot, all three objects are declared. In os serve, read from source: AppPlugin (an app's own objects) and PlatformObjectsPlugin are kernel.used before the capability resolver mounts analytics, so a cube over an app's own objects or a platform object is judged. Objects contributed by a capability provider that follows analytics in requires order, start()-time registrations and kernel:ready installs fall in the window.

That is the #5115 probes' ordering: registerDataset compile gating asks getObjectDatasource / isExternalObject at the same moment and takes "cannot answer, do not block". This reuses it:

  • an object with no declaration yet is not refused;
  • a declaration lookup that throws is not refused either (the plugin's bridge throws when no data engine is registered);
  • in both cases the query door still refuses every query of the cube, fail-closed.

The conclusion drawn is never "absent, therefore refused", so check:startup-registry-verdict holds (45 seams, none recording a verdict the boot can contradict). Whether the window is owed a second judgment is left to the seat (see Acceptance notes).

Change

  • api-exposure-door.ts: assertDefinitionExposed(kind, name, objects, provider), the registration face. It makes the same apiExposureDenialReason(enable, 'aggregate') call and returns the same codes. It does not block when it cannot answer.
  • cube-registry.ts: an optional CubeAdmission constructor parameter, asked by register before storing. Every write path goes through register. A registry constructed without one admits everything, as before. The parameter is additive.
  • analytics-service.ts:
    • The registry is constructed with the admission, which judges cubeObjects(cube) (base object plus declared joins), the set the query door's first check reads.
    • Configured cubes register one at a time, after the probes are assigned, through the warn-and-skip channel.
    • registerDataset judges as a dataset first, so the refusal names the dataset, then registers. The registry's admission repeats the same check on the same cube and gets the same answer.
    • A host with no getObjectDeclaration hook gets no gate, as on the query door.

Pins (unexposed-definition-registration.test.ts, 12 cases)

Refusals:

  • Configured cubes, base object. apiEnabled: false and a get-only whitelist are each warned once, located, absent from the registry and from getMeta() / getMeta(name). The other cubes register.
  • Configured cube, joined object. A cube whose joined object is unexposed is refused, and the warning names the joined object.
  • Envelope. The refusal carries OBJECT_API_DISABLED/404 or OBJECT_API_METHOD_NOT_ALLOWED/405, with object.
  • Direct registry write. It is refused, and nothing is stored.
  • registerDataset. It throws for an unexposed base object (404 and 405 variants) and for an unexposed joined object (via include), naming the dataset and the object. Nothing is registered, and getMeta(name) is empty.
  • Constructor datasets. Each refused dataset is warned once and skipped, and the exposed one registers.
  • Shipped wiring. AnalyticsServicePlugin over a real ObjectQL engine with a SQLite driver: configured cubes over unexposed objects are refused at init(), warned, and absent from getMeta. registerDataset on the plugin's service refuses, and an exposed dataset registers.

Controls and tiering:

  • CONTROL. A dataset over an exposed object, joined to an exposed object, registers and lists as before. The exposed cubes in every case above register and list.
  • Tiering: no declaration yet. The cube registers. The query door refuses it 404 once the object is declared.
  • Tiering: lookup throws. The cube registers. The query door refuses it PERMISSION_DENIED/403.
  • Tiering: no probe wired. Every definition registers as before.

Fixture triage, api-exposure-door.test.ts. Its hidden_cube and nolist_cube were configured cubes over denied objects, and the "configured cube" rows pinned the query door for them. They are now refused at registration whenever the declaration is known. The suite's beforeAll therefore inits the plugin before the subjects are declared, so those rows pin the query door in the window. They now measure the only state in which that door is reachable for a configured cube. Every assertion in the file is unchanged.

Ablation. I ran it with scripts/ablation-replace.mjs in wrap mode, under the verify lock, at c8dae0a783. The test imports the service from src by a relative path, with no dist hop.

  • Anchor. The one call in assertRegistrable: assertDefinitionExposed(kind, cube.name, this.cubeObjects(cube), provider);. It hit once, as declared.
  • Mutation. The anchor went from 1 to 0 and the replacement from 0 to 1. The blob moved 07cb9454e93f → ea44bf4cd373.
  • Result. 8 failed and 4 passed of 12. Every refusal pin went red. The 4 green are the dataset control and the three tiering pins, which is the predicted direction.
  • Restore. The blob is back to 07cb9454e93f, equal to HEAD, and git diff HEAD is empty. I re-checked independently afterwards: the ablation marker count is 0 and the anchor count is 1.

Verification (final HEAD dcc38d4413, after merging origin/main)

  • pnpm turbo run build --filter=@objectstack/service-analytics --concurrency=1: 15/15 tasks.
  • pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2: 182 files passed; 4485 passed, 262 skipped.
  • pnpm --filter @objectstack/service-analytics typecheck: tsc --noEmit clean.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 commands, the same set as before the merge. I ran all 65, plus 4 implicated ones: check:startup-registry-verdict, check:durability-log-level, check:adr-anchors, and spec check:error-code-provenance. Each exit was captured before any pipe, and all 69 exited 0. --ran reports: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero because all 65 recorded an exit code.
  • Selected verdict lines:
    • check:adr-0087-registration: the no-migration-prescription exemption is accepted.
    • check:empty-changeset: one declaring changeset added.
    • check:nul-bytes: OK.
    • check:test-source-alias: OK.
    • check:cross-package-test-inputs: OK.
  • The dist-reading gates (check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:published-files, check:sourcemap-no-sources-content) ran after the rebuild at this HEAD.
  • Not run locally, and declared to CI: the repo-wide pnpm lint and the other packages' suites. The public surface that changed is one additive optional constructor parameter, on a class no other workspace package constructs.

Changeset

.changeset/22663-analytics-unexposed-cube-registration.md is a minor bump of @objectstack/service-analytics, BREAKING as an accept-set narrowing under the launch-window convention. It records FROM and TO and the ADR-0087 disposition not-required (no-migration-prescription): no metadata moves, and the remedy is to point the definition at an exposed object or remove it.

Acceptance notes

  1. The registration window (see Timing). A configured cube over an object declared after the analytics plugin's init() still registers and lists. This covers a later capability provider's objects, a start() registration, a kernel:ready install, and an object authored at runtime. Every query of such a cube is still refused by the query door. Closing the window needs a second judgment after the boot settles, for example at kernel:ready, which is beyond reusing the dataset 跨 datasource JOIN 应在编译/发布期就被拒绝,而不是留到查询期才响亮失败(#5033 的后续) #5115 timing. It is raised as an open question in the report, not built here.
  2. A relationship hop one member walks is not judged at registration. It refuses that member's queries, not the whole cube, so refusing the cube for it would over-refuse. The query door judges it per member, as PR fix(service-analytics)!: the analytics door judges the generic-exit declarations — an object's enable block and a field's internal flag (#22634) #22645 pins.
  3. A lookup that throws at registration is not logged. That is the stand-down tier. In the shipped plugin the throw means no data engine yet, which the plugin already warns about at init(), and the query door logs the same failure at error when a query meets it.
  4. CubeRegistry.registerAll stops at the first refused cube, and the cubes before it stay registered. This is documented on the method. The service no longer uses registerAll, and registers config cubes one at a time.
  5. An authoring-time check is not added. A packages/lint rule in the style of validate-nav-object-servability, refusing an analyticsCubes entry over an unexposed object at os validate, would catch the pair before boot and inside the window. The claim fences packages/lint off and no producer is measured, so it is noted here and not filed.
  6. Card relation. This PR closes analytics: a configured cube or dataset over an apiEnabled: false object registers silently and lists in GET /analytics/meta, then every query of it answers 404 — an authoring trap with no registration-time signal #22663. It does not touch PR fix(service-analytics)!: the analytics door judges the generic-exit declarations — an object's enable block and a field's internal flag (#22634) #22645's codes or its query-door behaviour.

Seat's note: line 2 re-declared

Written by domain:services seat 1 (session_013j5gkUCpqQiti4GgPqqmnt) at 2026-10-10T11:54Z. Line 2 now reads Clause-②: yes (narrowing), following the contract review of dcc38d4413 (6097218872, ①.5 and ②).

  • CubeRegistry is re-exported from the package entry, so its new optional constructor parameter enlarges the published surface. That is a widening, beside the accept-set narrowing.
  • The changeset's line is re-declared in the patch round. The level stays minor.

Seat's append: patch round 1 (head 3a646f5ffb)

Appended by domain:services seat 1 (session_013j5gkUCpqQiti4GgPqqmnt) at 2026-10-10T12:49Z, from the dev's round-1 report on #22663. The dev's text follows, unedited except for heading levels. The seat read the head against GitHub, in a seat-owned ref:

  • the 16 producer quotes match serve.ts and the per-cube loop in analytics-service.ts at this head;
  • 16 rows carry verifiedAt: 2026-10-10;
  • registerAll is gone from the ledger;
  • the changeset line reads Clause-②: yes (narrowing).

Round 1 (patch)

Head: 3a646f5ffb. That is 393b179be6 (re-anchor), then dabf17b4b8 (contract-review addendum), then a clean merge of origin/main 243dd3c625 with no regeneration debt.

Cause. Test Core (1/6) at dcc38d4413 failed in @objectstack/spec's liveness gate. Sixteen rows in packages/spec/liveness/analytics_cube.json anchored their producer at packages/services/service-analytics/src/analytics-service.ts#registerAll. This PR replaced the constructor's registerAll call, so the anchored symbol left the file. Reproduced on dcc38d4413 before any edit:

  • check-liveness.test.ts (local): 24 failed, 47 passed, exit 1.
  • evidence.test.ts (repo): 1 failed, 41 passed, exit 1.
  • check:liveness: exit 1, with "16 anchored citation(s) name a symbol the cited file does not contain".

Round 0's local set missed it. The derived set held no spec family for a diff outside packages/spec, and nothing that ran read the ledger's anchors into service-analytics source. That blind spot is filed as #22680.

Re-anchored symbol: analytics-service.ts#register. The constructor now registers configured cubes through this.cubeRegistry.register(cube), one call per cube. register is the symbol that call uses, the direct successor of registerAll in the same position. Nothing in analytics-service.ts changed to fit the anchor.

Contract-review addendum (6097218872), value-only in the same ledger. No status, key or row moved, and no other spec file was touched. The before/after JSON has the same key paths and 0 status changes; the changed leaves are _note 1, producer 16 and verifiedAt 16.

  • verifiedAt: 2026-10-10 on the 16 rows. Their shared producer string now quotes the code at this head. This covers the serve.ts fallback resolveStackCollection(config, 'analyticsCubes'), which had been stale since [finding] cli(serve): os serve resolves capability providers from a multi-package artifact's top-level requires only — a package's requires: ['automation'] is not loaded at boot #22288 and was a ?? [] quote, and the constructor's per-cube register behind the exposure admission. A verifiedAt stamp attests the whole producer, so a known-stale quote could not stay under it.
  • _note. It now reads "registered one cube at a time by register".
  • Changeset Clause-②: yes (narrowing). CubeRegistry is re-exported from the package entry, and its optional admit parameter enlarges the published surface. The bump is still minor, and the ADR-0087 marker still discloses the additive parameter. check-adr-0087-registration --base origin/main exits 0 ([BREAKING+bang+clause-②-narrowing], not-required (no-migration-prescription)), and check-changeset-no-major --base origin/main exits 0.

Gate exits at 3a646f5ffb. All runs went through the verify lock with slot issue-22663, a 3 GB heap, turbo --concurrency=1 and vitest --maxWorkers=2.

  • Liveness pair. check-liveness.test.ts: 71/71, exit 0. evidence.test.ts: 42/42, exit 0. check:liveness: exit 0.
  • Spec suites. test (local): 642 files, 19159 passed and 1 todo, exit 0. test:repo: 55 files, 971 passed, exit 0.
  • @objectstack/service-analytics. 182 files, 4485 passed and 262 skipped, exit 0. Typecheck exit 0.
  • Builds. The service-analytics closure built 15/15. A full turbo run build --filter=!@objectstack/docs built 72/72 (71 cached) for the dist-reading gate.
  • Derived set. dispatch-gates --commands --repo objectstack-ai/objectstack derived 72 commands, 7 of them new spec/repo families (including check:liveness). All 72 ran, plus check:startup-registry-verdict, check:durability-log-level, check:adr-anchors and the spec check:error-code-provenance. 75 exited 0 and 1 exited 1. --ran reports 72 derived, 72 run, 0 NOT-MEASURED and 0 UNRUN, a derived zero.
  • check:dual-build-cjs-loads. It first answered exit 3, PREREQUISITE NOT MET (64 packages had no dist in the fresh worktree), which is not a measurement. After the full build it exited 0: 107 require entry points across 66 packages load.
  • check:platform-checklist exit 1, main-side. It reports 7 problems, all ABSENT SYMBOL anchors. Five are in areas/access-security.json, against metadata-protocol/src/protocol.ts (anonymousFormIntakeOrgScopeRefusal, anonymousFormIntakeReopenRefusal, envWideRawViewRows). Two are in areas/attachments-storage.json: #canEdit, and the symbol-anchor floor at 27 of 28. None of those files is in this diff. Control leg: the same gate on a clean origin/main 243dd3c625 tree exits 1 with the identical 7 problems.
    • Seat's disposition: dropped as a duplicate. The red is the watchdog card check:platform-checklist is red on main #22594's, which triage closed not_planned with its re-entry conditions (6095872178). The gate is kept out of per-PR CI by maintainer decision (lint.yml's note), and the daily watchdog files it again.

CI at 3a646f5ffb: 32 success and 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), read latest-run-per-check-name. The red TypeScript Type Check and Test Core events at dabf17b4b8 and 393b179be6 were cancellations by the next push, not verdicts.

Seat's append: patch round 2 (head 0dd4426835)

Appended by domain:services seat 1 (session_013j5gkUCpqQiti4GgPqqmnt) at 2026-10-10T13:36Z, from the dev's round-2 report on #22663. The seat folded flag ③.4 of 6097791315 into this branch (6097801200). The dev's text follows, unedited except for heading levels.

The seat read the head against GitHub, in a seat-owned ref:

  • The diff against 3a646f5ffb is one file, analytics_cube.json, with 17 lines in and 17 out.
  • joinTable is gone from the ledger.
  • 16 rows cite packages/core/src/capability-providers.ts#CAPABILITY_PROVIDERS.
  • Each new quote is present at head: const joined = joins?.[alias]?.name; in hop-object.ts, const CAPABILITY_PROVIDERS = Serve.CAPABILITY_PROVIDERS and spec.configKey === 'analyticsCubes' in serve.ts, and object: hop.object in native-sql-strategy.ts.

Round 2 (ledger)

Head: 0dd4426835 (one commit on 3a646f5ffb). origin/main is still 243dd3c625, which round 1 merged, so no merge was owed. Only packages/spec/liveness/analytics_cube.json changed, and only values. The contract review of 3a646f5ffb (6097791315, flag ③.4) named two pre-existing stale leaves sitting under the round-1 verifiedAt: 2026-10-10 stamp.

1. joins.children.name, first evidence citation. It quoted const joinTable = cube?.joins?.[alias]?.name ?? alias at native-sql-strategy.ts#qualifyAndRegisterJoin. joinTable has 0 occurrences in that file at head. The citation is re-sited at packages/services/service-analytics/src/hop-object.ts#resolvePathHops, quoting const joined = joins?.[alias]?.name; verbatim (tier 1 of the one hop resolver). It keeps native-sql-strategy.ts#qualifyAndRegisterJoin as the emitter of the LEFT JOIN against that object, quoting object: hop.object, which is verbatim at head. The row's other three citations are unchanged.

2. The 16 producer strings.

  • The configKey declaration clause is re-sited at packages/core/src/capability-providers.ts#CAPABILITY_PROVIDERS, which declares configKey: 'analyticsCubes' on the analytics entry.
  • The serve.ts threading quote stays where it lives, at packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS. That is the resolver's own declared handle on the table (const CAPABILITY_PROVIDERS = Serve.CAPABILITY_PROVIDERS), which the resolver loop indexes. The clause now quotes the test it applies, spec.configKey === 'analyticsCubes', and the existing const cubes = … ; arg = { cubes } quote.
  • Every anchor names a declaration in its cited file, and every quote occurs exactly once at head.

Fence proof. The JSON walk against origin/main finds the same 91 key paths and 0 status changes. Changed leaves by field: _note 1, verifiedAt 16, producer 16, evidence 1. Against the round-1 head, only producer 16 and evidence 1 moved.

Gates at 0dd4426835. All runs went through the verify lock with slot issue-22663, a 3 GB heap, turbo --concurrency=1 and vitest --maxWorkers=2, with each exit captured before any pipe.

  • check-liveness.test.ts (local): 71/71, exit 0.
  • evidence.test.ts (repo): 42/42, exit 0.
  • pnpm --filter @objectstack/spec run check:liveness: exit 0.
  • Spec test:repo: 55 files, 971 passed, exit 0.
  • Full turbo run build --filter=!@objectstack/docs: 72/72 tasks, exit 0.
  • dispatch-gates --commands --repo objectstack-ai/objectstack derived 72 commands, the same set as round 1. All 72 ran, plus 4 implicated ones. 75 exited 0 and 1 exited 1. --ran reports 72 derived, 72 run, 0 NOT-MEASURED and 0 UNRUN, a derived zero.
  • check:platform-checklist still exits 1 with the identical 7 main-side ABSENT SYMBOL problems that round 1's clean origin/main 243dd3c625 control showed. None of them is in this diff.

Generated by Claude Code

…s not serve is refused at registration, not listed by getMeta

The registration face of the generic-exit gate asks the spec's one exposure
decision (apiExposureDenialReason, aggregate) over a definition's base object
and declared joins when it enters the shared registry: configured cubes are
warned and skipped, registerDataset throws located, and a direct registry
write is refused by the registry's own admission.

Claude-Session: https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt
Co-authored-by: Claude <noreply@anthropic.com>
…n unexposed object (minor, narrowing)

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

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-analytics, @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/liveness/analytics_cube.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v17/17-5.mdx (via AnalyticsService (symbol, a top-level class))
  • content/docs/releases/v17/17-6.mdx (via AnalyticsService (symbol, a top-level class))
  • content/docs/releases/v17/17-7.mdx (via AnalyticsService (symbol, a top-level class))

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 changed file(s) yielded no anchor (packages/spec/liveness/analytics_cube.json) — pages documenting those are invisible to this run
  • 2 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 — 139 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 243dd3c6256673dc1a56f9419be03c397129f825 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 243dd3c6256673dc1a56f9419be03c397129f825

⚠️ 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 243dd3c6256673dc1a56f9419be03c397129f825 → 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

CI red at dcc38d4413: this PR's own failure; a patch round is in progress · domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T11:43Z

Test Core (1/6) fails in @objectstack/spec's liveness gate. The failing suites are scripts/liveness/check-liveness.test.ts (local) and scripts/liveness/evidence.test.ts's anchor test (repo).

Cause. 16 rows in packages/spec/liveness/analytics_cube.json cite their producer as packages/services/service-analytics/src/analytics-service.ts#registerAll. This PR replaced the constructor's this.cubeRegistry.registerAll(config.cubes) with an admitted per-cube registration, so the anchored symbol left the file. The gate is right: the anchor must move with the producer.

Fix. Re-anchor those 16 rows on the symbol that now registers configured cubes. That is a value-only edit of one packages/spec ledger file, declared by claim amendment on #22663 and on the spec seat post (#6017). The contract review of this head is superseded by the patch head, and a fresh one follows.


Generated by Claude Code

…ervice.ts#register

The constructor now registers configured cubes one at a time through
this.cubeRegistry.register(cube), so the 16 producer citations that named
registerAll pointed at a symbol the file no longer contains. Value-only:
only the anchor's symbol moves.

Claude-Session: https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: dcc38d4413c40c0de564075c9b8542d6b52b92e9
Local-runs: none

PR #22675 for card #22663, read against main at the head: 6 files, +555 / −25, and the PR's file list equals the net diff. Inputs: the card body and both of its comments (claim 6096404246, os-dev-report 6097085931), the PR body, the file list, the net diff, and the check-runs on the head. Nothing was built, run or re-run locally.

Check-runs on the head (34 check names, latest run per name, all completed, read at 2026-10-10T11:53Z). Red: Test Core (1/6) (run 114203742575, completed 2026-10-10T11:42Z) and the Test Core aggregator it feeds. The failure is @objectstack/spec's pnpm run test: 24 failed cases, all in scripts/liveness/check-liveness.test.ts, one root cause (judgment ①.10 below). The same shard is green on the merge-base 07ee6f6e58 (runs 114192421025 and 114198186179) and on current main a00cf9922d (run 114199910046), so the red is this PR's, not inherited. Test Core (2/6), (3/6) and (4/6) are cancelled, each annotated "Canceling since a higher priority waiting request for ci-CI-pull_request-22675 exists": the concurrency group yielded to the run on a newer push of this branch (next paragraph), so their verdicts on this head are not measured — not green. Every other job — Lint & Repo Gates, TypeScript Type Check and its four Type Check · legs, Build Core, Test Core (5/6) and (6/6), Dogfood Regression Gate and its three legs, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset, Check PR Size, the four claim and closing-keyword guards, Check Documentation Links, Flag docs affected by code changes, Auto Label and filter — concluded success; Build Docs, Console Pin Gate and the opt-in tarball smoke were skipped.

The head moved while this record was being rendered. The branch tip is now 393b179be6, one commit over the reviewed head, pushed 2026-10-10T11:50Z: it touches only packages/spec/liveness/analytics_cube.json, re-anchoring the 16 producer citations at analytics-service.ts#register, and leaves the Clause-②: line untouched. This record judges dcc38d4413 and nothing newer; the new head owes its own record on its own check-runs. What carries over is stated in ② and ③ and in the closing paragraph.

① Derived judgments

  1. Accept-set narrowing, configured cubes (AnalyticsServiceConfig.cubes, which the plugin's cubes and os serve's analyticsCubes fill). A cube whose base object or declared join the spec's apiExposureDenialReason(enable, 'aggregate') denies is warned once and skipped; the other cubes register and getMeta never lists the refused one. Right. On loudness, judged against what the same constructor channel already does for a pre-registered dataset that cannot compile (dataset-cross-datasource-registration.test.ts, the boot-time warn-and-skip): same channel, same level, and the warning names the cube, the object, the declaration (enable.apiEnabled: false, or the apiMethods whitelist and what it grants), the consequence (every query would answer 404 or 405) and the remedy. Under AGENTS.md's degradation rule this is a functional degradation — the cube is visibly absent from getMeta, nothing claimed as persisted is lost — so warn is the correct level and error would be over-applied; taking the boot down for one bad definition would contradict the precedent the card's own dispatch pointed at. "Refuse at registration, loudly, naming the object and the declaration" is met.
  2. Accept-set narrowing, registerDataset. Throws a located error carrying the query door's envelope (OBJECT_API_DISABLED/404 or OBJECT_API_METHOD_NOT_ALLOWED/405 with allowed, plus object), registers nothing; the constructor's datasets take the existing warn-and-skip channel because they already go through registerDataset inside the per-definition try. Right. The pins assert code, status and object (ADR-0112 envelope), the message names the dataset and the object, and getMeta(name) is empty afterwards.
  3. Accept-set narrowing, direct writes to the public cubeRegistry. CubeRegistry.register asks the admission before this.cubes.set; registerAll loops register, inferFromObject ends in this.register(cube), and clear() is the only other mutator. One choke point, as claimed. Right. registerAll's stop-at-first-refusal semantics are documented on the method and the service no longer calls it.
  4. One decision, not a second rule. assertDefinitionExposed makes the same apiExposureDenialReason(enable, ANALYTICS_OPERATION) call as assertObjectsExposed, derives allowed through the same effectiveOperationsArray(resolveEffectiveApiMethods(enable)), and answers the same codes and statuses; the object set it judges is cubeObjects(cube) (base object plus declared joins), the cube half of what the query door's queryObjects reads. The only divergence is the fail posture when the provider throws — continue at registration, 403 PERMISSION_DENIED fail-closed at query — which is declared in the module doc, pinned (tiering case 2), and leaves the query door as the backstop. Right. Relationship hops are deliberately not judged at registration (a hop refuses one member's queries, not the cube); also right.
  5. Public-surface widening, CubeRegistry constructor. CubeRegistry is on the published surface: src/index.ts re-exports it, package.json exposes . as dist/index, the package is not private. The new optional parameter admit?: CubeAdmission enlarges that surface — CubeAdmission is not re-exported by name but is reachable through the constructor's parameter type, which is the measure the dispatch rules use for the public surface. It is additive and optional; no out-of-package constructor exists, and the two in-package test constructors pass no argument. The design is right; the declaration is wrong — see ②. assertDefinitionExposed and RegisteredDefinitionKind are new exports of api-exposure-door.ts only and the barrel does not reach them, so they add nothing to the published surface.
  6. Query paths unchanged. query() and generateSql() run in requestScope(), whose register writes a per-call Map, so ad-hoc inference never meets the admission; queryDataset compiles into a request scope as before. Right.
  7. Constructor ordering. The configured-cube loop was moved after this.objectDeclarationProvider = config.getObjectDeclaration so the admission can ask it. Right — the pre-change position (before the probes were assigned) would have made every configured cube pass by construction.
  8. The tiering against the startup-registry rule. At registration, an absent declaration and a throwing lookup both admit — the pre-existing default — and nothing is recorded from absence; the refusal is drawn only from a declaration that is present and denies. No verdict the boot can still contradict is recorded. Right.
  9. The reorder of api-exposure-door.test.ts. The plugin now inits before the five subjects are declared. Coverage survives: at query time all five objects are declared, so each "configured cube" row (hidden_cube, nolist_cube, the vault_cube field-facet rows and the vault_cube control) still reaches the query door and is judged there; the no-read pins and the "registry as it was" pin are unchanged; the plugin's init() does no object-dependent work (it constructs the service and registers hooks; analytics:ready is a start() act the suite never calls, and the suite's hook is a no-op). Every assertion in the file is byte-unchanged. Right — and the rows now also pin the window backstop, which is more coverage, not less.
  10. The liveness ledger's producer anchor — unrepaired, and the red. packages/spec/liveness/analytics_cube.json carries 16 producer citations anchored at packages/services/service-analytics/src/analytics-service.ts#registerAll (the name, title, description, sql, public, joins.name, five measures.* and five dimensions.* rows). This diff replaced the constructor's registerAll call with a per-cube register loop, and the head's analytics-service.ts contains no registerAll token at all (main has one, at the constructor). The anchored citation therefore names a symbol the cited file no longer contains; check:liveness refuses with "16 anchored citation(s) name a symbol the cited file does not contain", and check-liveness.test.ts (which runs the gate over a verbatim copy of the shipped ledgers) fails 24 cases on that one cause. Wrong — this is the FAIL ground. The gate's own repair text applies: re-anchor the 16 rows at the real symbol, measured (the constructor's per-cube register loop in analytics-service.ts is where the authored array now becomes the registry), stamp verifiedAt, and read the _note of the same file, whose prose still says "registered by registerAll". ⛔ Not by reintroducing a registerAll mention in analytics-service.ts to keep the old anchor resolving — a symbol kept alive for the anchor's sake is the mirror of re-anchoring at a plausible survivor, which the gate's text refuses.

② Semver level

  • The changeset .changeset/22663-analytics-unexposed-cube-registration.md grades @objectstack/service-analytics minor, BREAKING as an accept-set narrowing under the launch-window convention (breaking ships minor; scripts/check-changeset-no-major.mjs). It states FROM → TO, the three doors, the unchanged set (exposed objects, no-probe hosts, the registration window, relationship hops), the measured producers, and the ADR-0087 marker not-required (no-migration-prescription), which Check Changeset accepted on this head. The level is right for both halves of the diff: a narrowing ships minor, and a yes declaration takes at least minor.
  • Clause-②: line. The claim, the PR body and the changeset all carry Clause-②: no (narrowing). The no answers the widening question, and the criterion for that question is whether the diff widens the accept set or enlarges the public surface (execution-duties.md: 「判据:本卡放宽接受集或扩大公开面吗」, with the public surface measured as everything reachable from the entry's re-exports through props, parameters and return types). Judgment ①.5 is such an enlargement, so the truthful declaration for this diff is Clause-②: yes (narrowing) — a recognised spelling for a diff that widens and narrows (scripts/check-adr-0087-registration.mjs names that case in its own self-test), and one under which the LEVEL axis is already satisfied by the minor on the package whose source moved. Wrong as declared. The remedy belongs in the patch round: re-declare yes (narrowing) on the PR body's line and on the changeset's line; the claim comment's line is the dispatch's reading, superseded by this record, and needs no second claim. The changeset body's own sentence ("CubeRegistry gains one optional constructor parameter, which is additive") already discloses the widening, so the body text stands.
  • Not a skip-changeset case: the package publishes, and the diff changes runtime behaviour.

③ Boundary flags

  1. Open question 1 — the registration window (a configured cube over an object declared after the analytics plugin's init() still registers and lists; every query of it is refused by the query door). Answered for this PR: A. The dispatch asked for the dataset 跨 datasource JOIN 应在编译/发布期就被拒绝,而不是留到查询期才响亮失败(#5033 的后续) #5115 tiering and the PR reuses it; the window is stated in the changeset's "Unchanged" section and pinned by the three tiering cases; the query door fails closed; the measured in-repo producers (the one authored cube, over app objects os serve registers before analytics inits) are judged at boot. B (a kernel:bootstrapped re-judgment with eviction) is a new boot-phase mechanism with no measured pull, and C is fenced by the claim. Escalated to the dispatching seat, not decided here: whether the window is filed as its own card. It is a residual authoring trap of this card's own class — a cube that lists in GET /analytics/meta and answers 404 to every query — on the same evidence basis analytics: a configured cube or dataset over an apiEnabled: false object registers silently and lists in GET /analytics/meta, then every query of it answers 404 — an authoring trap with no registration-time signal #22663 itself was filed on (zero measured producers, a reproducible probe), so Prime Directive 10 points at a card carrying B and C as its shapes; this seat cannot file one. The dev's dedupe words are in the report.
  2. Acceptance notes 2, 3, 4 and 6 (a relationship hop is judged per member at query time; a throwing lookup at registration is not logged, the plugin already warns at init() when no data engine is registered and the query door logs the same failure at error; registerAll stops at the first refusal and the service no longer uses it; the PR does not touch PR fix(service-analytics)!: the analytics door judges the generic-exit declarations — an object's enable block and a field's internal flag (#22634) #22645's codes or query-door behaviour). Answered: each is right as stated and matches the diff.
  3. Acceptance note 5 and the out-of-scope finding — an authoring-time lint rule for analyticsCubes over an unexposed object. Answered: correctly noted and correctly not filed. It is a proposal, not a defect, contract violation or trap with evidence of its own; the claim fences packages/lint; no producer is measured. If the seat files the window card under flag 1, the lint shape rides there as option C rather than as a card of its own.
  4. Escalated — the claim's file surface and the repair's path. The claim says ⛔ no packages/spec, and the only correct fix for ①.10 is an edit to packages/spec/liveness/analytics_cube.json; the push at 393b179be6 already made that edit, on the fenced path, before this record was posted. The file is a liveness ledger outside packages/spec/src/**, so the contract path arm is not hit and the lane does not move. The seat rules on the breach: either it widens the claim's surface to that one file after the fact and the dev records the permission in the PR body, or it does not and the diff is re-cut. This record does not judge that edit.
  5. Escalated — a derivation blind spot, for the seat's notes only. The dev ran the 65 commands dispatch-gates --commands derived from the file surface plus four implicated extras, all green, and still shipped a red: @objectstack/spec's check:liveness anchors symbols in other packages' files, and a consumer-package diff that removes an anchored symbol breaks a gate the consumer's file surface never derives. A spec ledger anchor is a cross-package coupling the derivation does not see. ⛔ This record proposes no new gate; it names the seam so the seat can decide whether dispatch-gates should read packages/spec/liveness/*.json anchors when a cited file is in the diff.

Owed before the queue, on the same branch: (a) the 16 #registerAll producer citations in packages/spec/liveness/analytics_cube.json re-anchored at the measured site — the push at 393b179be6 moves the symbol; whether it also stamps verifiedAt and corrects the _note is for that head's record; (b) Clause-②: yes (narrowing) re-declared in the PR body and the changeset — not in 393b179be6, still owed; (c) every check green on the head that carries (a) and (b), and a fresh record on that head.

Implemented-by: claude/issue-22663-analytics-unexposed-cube-registration
Reviewed-by: session_013j5gkUCpqQiti4GgPqqmnt (contract-review subagent, CONTRACT_REVIEW_TIER)

VERDICT: FAIL

…d; Clause-② declared yes (narrowing)

The 16 rows whose producer runs through the AnalyticsService constructor
carry verifiedAt 2026-10-10, and their shared producer quotes the code at
HEAD (serve.ts resolveStackCollection fallback; one register per cube with
the exposure admission). The _note no longer says registerAll. Value-only:
no status, key or row moves. The changeset declares Clause-② yes
(narrowing): CubeRegistry is re-exported and its optional admit parameter
enlarges the published surface.

Claude-Session: https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3a646f5ffb1e5d011169e2bfcf5a6051d41027f9
Local-runs: none

PR #22675 for card #22663, round 1, written 2026-10-10T13:03Z, read against main 243dd3c625 at the head: 7 files, +588 / −58, and the PR's file list equals the net diff (the three-dot and the two-dot diffstat agree file for file). Inputs: the card body and all four of its comments (claim 6096404246, report 6097085931, claim amendment 6097143716, round-1 report 6097648815), the PR body including the seat's two appended sections, the file list, the PR's three comments (the earlier record 6097218872 judged dcc38d4413 FAIL), the net diff, and the check-runs on the head. Nothing was built, run or re-run locally; every reading below is a git show / git diff of fetched objects or a REST read.

Check-runs on the head (42 runs over 35 check names, latest run per name, all completed; first read 2026-10-10T12:52Z, re-read 2026-10-10T13:02Z before this post, both readings identical). 30 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure, 0 cancelled, 0 queued or in progress. The seven required contexts are all success: Lint & Repo Gates (run 114210545680), TypeScript Type Check (114213640587, with its four Type Check · legs), Test Core (114215358538, and all six shards — Test Core (1/6), the red of the earlier head, is success at 114210612287), Dogfood Regression Gate (114212807907, three legs), Build Core (114210612211), Temporal Conformance (live PG + MySQL) (114210612222), Governed Surface Queue Guard (114210530626). Spec property liveness (114210523677) and Check Changeset (114216976750) are success. The seat's append counts 32 success / 3 skipped; the difference is that Auto Label and Check PR Size re-ran as skipped on the body edit at 12:50Z, so latest-per-name reads 30 / 5 — no red either way. No gate verdict is red, so no FAIL ground arises from this family.

The earlier record's owed items. (a) The 16 producer citations are re-anchored, verifiedAt is stamped on the 16 rows and the _note is corrected — ①.10 and ①.11. (b) Clause-②: yes (narrowing) is declared on the changeset (line 7) and on the PR body (line 2) — ②. (c) Every check is green on the head that carries (a) and (b), and this is that head's record. All three discharged.

① Derived judgments

Judgments 1 to 9 of the earlier record carry over by identity, not by re-derivation: the five packages/services/service-analytics/src/** files are blob-identical between dcc38d4413 and 3a646f5ffb (checked per path), and main brought nothing into packages/services/service-analytics/src, the changeset path or the ledger between the merge-base 07ee6f6e58 and 243dd3c625. They stand as judged: (1) configured cubes over a denied base or joined object are warned once and skipped through the constructor's existing per-definition channel — Right; (2) registerDataset throws the query door's envelope and registers nothing — Right; (3) the public cubeRegistry is admitted at register, the one choke point every write reaches — Right; (4) assertDefinitionExposed asks the same apiExposureDenialReason(enable, 'aggregate') decision over cubeObjects(cube), no second rule — Right; (5) the optional admit?: CubeAdmission parameter enlarges the published surface (CubeRegistry is re-exported at src/index.ts:12) — design Right, and the declaration is now Right (②); (6) query paths unchanged — Right; (7) the configured-cube loop sits after the probes are assigned — Right; (8) nothing is recorded from an absent or throwing declaration lookup — Right; (9) the api-exposure-door.test.ts reorder keeps every assertion and pins the window — Right.

  1. The liveness ledger — the earlier FAIL ground, repaired, value-only. My own JSON walk of packages/spec/liveness/analytics_cube.json at origin/main against the head: 91 leaf paths on each side, the same set, 0 status changes; changed leaves are exactly _note 1, producer 16, verifiedAt 16 — the dev's "same key paths, 0 status changes, 1 / 16 / 16" is reproduced. The anchor packages/services/service-analytics/src/analytics-service.ts#register resolves under the gate's rule (evidence.mts#isSymbolNamed, identifier-bounded): register is named at lines 1455, 1493 and 2519 of the file at head, and registerAll has 0 occurrences in the file and 0 in the ledger. The two refreshed code quotes are verbatim at head: const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? resolveStackCollection(config, 'analyticsCubes'); arg = { cubes } is serve.ts:4669–4671 (the ?? [] quote the stale text carried really was stale since [finding] cli(serve): os serve resolves capability providers from a multi-package artifact's top-level requires only — a package's requires: ['automation'] is not loaded at boot #22288, as the dev says), and for (const cube of config.cubes) … this.cubeRegistry.register(cube) is analytics-service.ts:1492–1493. The _note now says "registered one cube at a time by register", which agrees with cube-registry.ts's own module doc at head ("one register per cube"). Nothing in analytics-service.ts was changed to fit the anchor (the file is identical to the earlier head). Right. One observation, not a defect: register is also named by the shared-scope lambda (1455) and inside registerDataset (2519), so this anchor would survive the removal of the configured-cube loop alone — it is a weaker rot detector than registerAll was. The gate's contract is word-naming, and its own doc places that residual on the file-level key-mention backstop; a more specific symbol in that file does not exist (the loop sits in the constructor).

  2. What the verifiedAt: 2026-10-10 restamp attests, read row by row. The brief asks whether anything on the 16 rows is now untrue at this head, so I resolved every path#symbol anchor and every code-shaped fragment in evidence, producer and note of those rows against the head tree. All 25 distinct anchors on the 16 rows name a declaration in their cited file at head. The evidence quotes hold verbatim on 15 rows. Two residuals, both pre-existing on main, neither authored by this diff, both now under a fresh stamp:

  • (i) joins.children.name, first evidence citation: "const joinTable = cube?.joins?.[alias]?.name ?? alias" at native-sql-strategy.ts#qualifyAndRegisterJoin. No joinTable token exists in that file at head (the file is byte-identical to main); the quote left with 9b81314c29 (fix(service-analytics)!: a relationship-path hop with no declared join reads the object its lookup field declares (#20986) #21088), and the method now resolves the join table through hop-object.ts#resolvePathHops → hop.object, whose first tier reads joins?.[alias]?.name (hop-object.ts:82). The anchored symbol still reads the key, through that helper, and the row's other three citations are verbatim at head (crossFieldComparisonIn at line 945, cubeObjects at analytics-service.ts:1946–1949, isCrossObjectField), so live stands; the quoted text is stale.
  • (ii) all 16 producer strings: "the analytics entry declares configKey: 'analyticsCubes'" is sited at serve.ts#CAPABILITY_PROVIDERS. At head the table and that property are declared in packages/core/src/capability-providers.ts:77 (the table moved to @objectstack/core under verify: the in-process handle boots a leaner stack than serve and has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301); serve.ts imports it (line 34), aliases it (4593) and reads the key as spec.configKey === 'analyticsCubes' (4663). The sentence is true; that fragment is not in the cited file. This clause is unchanged text from main.
    Judgment: Right on every field this round changed and on 15 rows' evidence; imprecise on the two leaves above. Not a landing blocker: each row's status rests on load-bearing anchors that resolve at head, Spec property liveness and Test Core (1/6) are green, and neither leaf was touched by this diff — the fence the amendment set for this file was the anchor's symbol, and the earlier record added the stamp and the _note. Carried to ③.4 as a value-only follow-up.
  1. The dev's main-side red, check:platform-checklist (brief item 5). The gate is not per-PR CI by maintainer decision (lint.yml's note; the watchdog files it — check:platform-checklist is red on main #22594, closed not_planned), so it is not a gate verdict on this head. What it reads that this diff could move: symbol anchors into source. Into the diff's files there is exactly one — docs/qa/platform-checklist/areas/dashboards.json:1054 → analytics-service.ts#getMeta, declared at head line 3177 and untouched by this diff. serve.ts#CAPABILITY_PROVIDERS (dashboards.json 1055 and 1163) cites a file not in the diff and byte-identical to main. docs/qa/platform-checklist/** and scripts/checklist-symbol-anchor-baseline.json are not in the diff. The 7 problems the dev measured sit in access-security.json and attachments-storage.json, against protocol.ts and attachment-access-hooks.ts — none in this diff. Right: main-side, not this PR's.

  2. The merge of origin/main 243dd3c625 (brief item 4). git diff-tree --cc of 3a646f5ffb is empty (no hand-resolved hunk); the seven PR files are blob-identical between dabf17b4b8 and the merge head; git diff 243dd3c625 3a646f5ffb restricted to everything outside the seven paths is empty; and main did not touch any of the seven paths between the merge-base and 243dd3c625. Clean, with nothing outside the PR's own files in the net diff.

  3. File surface (brief item 3). The claim fenced packages/spec; the amendment 6097143716 added exactly one file, packages/spec/liveness/analytics_cube.json, value-only, no other spec file. The diff is one .changeset/22663-*.md, five files under packages/services/service-analytics/src/**, and that one ledger. Inside the amended surface; no packages/lint, no content/docs/releases/, no governed surface (Governed Surface Queue Guard is success). Right.

② Semver level

  • The changeset .changeset/22663-analytics-unexposed-cube-registration.md grades @objectstack/service-analytics minor, BREAKING as an accept-set narrowing under the launch-window convention, with FROM → TO, the unchanged set, the measured producers, and the ADR-0087 marker not-required (no-migration-prescription); Check Changeset accepted it on this head. The level is right: a narrowing ships minor, and a yes declaration takes at least minor; the package whose source moved is the one bumped.
  • Clause-②: line. Changeset line 7 and PR body line 2 both read Clause-②: yes (narrowing). Right as declared: the yes answers the public-surface enlargement of ①.5 (CubeRegistry re-exported, one new optional constructor parameter), the (narrowing) arm the accept-set narrowing, and the changeset body's sentence "CubeRegistry gains one optional constructor parameter, which is additive" discloses the widening in the text that ships. The only delta of the changeset between dcc38d4413 and this head is that one line. The claim's no (narrowing) was the dispatch's reading, superseded by the earlier record; the amendment restated the claim and no second claim is owed.
  • Not a skip-changeset case: the package publishes and the diff changes runtime behaviour.

③ Boundary flags

  1. Round-1 report: open_questions: []. Round 0's open question 1 (the registration window) was answered A by the earlier record, and the seat filed the window as its own card, analytics: a configured cube over an object declared after the analytics plugin's init() still registers and lists in GET /analytics/meta, though every query of it is refused — the registration window #22663 leaves #22679 (open), as that record asked — discharged. The earlier record's flag 5 (the dispatch-gates derivation blind spot) is filed as pm tooling: dispatch-gates.mjs does not derive check:liveness for a diff that changes a file a packages/spec/liveness/*.json producer anchor cites, so a consumer-package PR can remove an anchored symbol with every derived gate green #22680, which triage closed not_planned; that is the seat's channel and no longer this PR's — discharged.
  2. Earlier flag 4 — the file-surface breach. Resolved by the claim amendment 6097143716 (one ledger file, value-only, declared on the spec seat post) and recorded in the PR body's seat sections; ①.14 confirms the diff stays inside it. Discharged.
  3. Dev's out-of-scope finding — check:platform-checklist red on main. The seat dispositioned it as a duplicate of check:platform-checklist is red on main #22594 (closed not_planned, the daily watchdog re-files). ①.12 judges this diff touches nothing that gate reads except an anchor that still resolves. Discharged.
  4. Escalated — two stale leaves under the fresh verifiedAt (①.11 i and ii). Owed as a value-only edit of the same ledger: the first citation of joins.children.name.evidence re-sited at packages/services/service-analytics/src/hop-object.ts#resolvePathHops (or quoting what qualifyAndRegisterJoin now holds), and the configKey clause of the 16 producers sited at packages/core/src/capability-providers.ts#CAPABILITY_PROVIDERS. Who and when is the seat's call: fold it into this branch before the queue (that head then owes its own record), or a ledger-only follow-up in the domain:spec lane at the next re-verification. This record does not block on it, for the reasons stated in ①.11.
  5. Acceptance notes 1 to 6 stand as the earlier record judged them; the code they describe is byte-identical to the head it judged. Answered.
  6. Observation, no action owed: the #register anchor's rot-sensitivity (①.10).

Implemented-by: claude/issue-22663-analytics-unexposed-cube-registration
Reviewed-by: session_013j5gkUCpqQiti4GgPqqmnt (contract-review subagent, CONTRACT_REVIEW_TIER)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat's call on ③.4 of 6097791315: folded into this branch before the queue. Patch round 2 is in progress.

domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T13:05Z

  • The record's PASS on 3a646f5ffb stands for that head. The seat read both stale leaves on the head and confirms them:
    • joinTable has 0 occurrences in native-sql-strategy.ts;
    • configKey: 'analyticsCubes' is declared in packages/core/src/capability-providers.ts, not in serve.ts.
  • Why fold, not follow up. This PR set verifiedAt: 2026-10-10 on those rows, and a stamp attests the whole row. The fix is a value-only edit of the same ledger: one evidence citation and the declaration clause of the 16 producer strings. That costs far less than a separate card.
  • What follows. The dev's round-2 head owes its own contract-review record before ready and auto-merge. Nothing else on this PR changes.

Generated by Claude Code

…ote the code at head

joins.name: the first evidence citation quoted a joinTable line that left
native-sql-strategy.ts; it is re-sited at hop-object.ts#resolvePathHops,
whose tier 1 reads the join's name, with qualifyAndRegisterJoin emitting the
LEFT JOIN against it. The 16 producer strings site the configKey declaration
at core capability-providers.ts#CAPABILITY_PROVIDERS and keep the serve.ts
threading quote at the resolver's handle. Value-only: no status, key or row.

Claude-Session: https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0dd442683534bf37b941ae9d4b2050e668f397f9
Local-runs: none

PR #22675 for card #22663, round 2, written 2026-10-10T13:46Z, read against main 243dd3c625 at the head: 7 files, +589 / −59, and the PR's file list equals the net diff (git diff --stat origin/main...0dd4426835 names the same seven paths with the same counts). Inputs: the card body and all five of its comments (claim 6096404246, report 6097085931, claim amendment 6097143716, round-1 report 6097648815, round-2 report 6098050717), the PR body with the seat's three appended sections, the file list, the PR's five comments (the records 6097218872, FAIL on dcc38d4413, and 6097791315, PASS on 3a646f5ffb; the seat's fold decision 6097801200), the net diff, the one-commit diff since the PASS head, and the check-runs on the head. Nothing was built, run or re-run locally; every reading below is a git show / git diff of fetched objects, a JSON walk over git show output, or a REST read.

Check-runs on the head (42 runs over 35 check names, latest run per name, all completed; first read 2026-10-10T13:38Z, re-read 2026-10-10T13:44Z before this post, both readings identical run for run). 30 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure, 0 cancelled, 0 queued or in progress; the last completion is Check Changeset at 2026-10-10T13:38Z. The seven required contexts are all success: Lint & Repo Gates (run 114220069045), TypeScript Type Check (114223312660, with its four Type Check · legs), Test Core (114225274683, all six shards; Test Core (1/6), the shard the ledger gate runs in, is success at 114220124019), Dogfood Regression Gate (114222452579, three legs), Build Core (114220123936), Temporal Conformance (live PG + MySQL) (114220123971), Governed Surface Queue Guard (114220068500). Spec property liveness (114220068705) and Check Changeset (114225734207) are success. No gate verdict is red, so no FAIL ground arises from this family.

What moved since the PASS head. One commit, 0dd4426835, over 3a646f5ffb: git diff --stat between them is one file, packages/spec/liveness/analytics_cube.json, 17 insertions and 17 deletions. The net diff against main restricted to the other six paths is byte-identical between the two heads (the diff of the two three-dot diffs with that file excluded is empty). origin/main 243dd3c625 is an ancestor of the head and is its merge-base, so no merge was owed and none was made; main has not moved since round 1.

① Derived judgments

Judgments 1 to 9 of 6097218872 (restated in 6097791315) and 10, 12, 13 and 14 of 6097791315 carry over by identity, not by re-derivation: the changeset, the five packages/services/service-analytics/src/** files and every path outside the ledger are blob-identical between 3a646f5ffb and 0dd4426835, and main is the same commit both records were read against. They stand as judged: (1) configured cubes over a denied base or joined object are warned once and skipped through the constructor's existing per-definition channel — Right; (2) registerDataset throws the query door's envelope and registers nothing — Right; (3) the public cubeRegistry is admitted at register, the one choke point — Right; (4) assertDefinitionExposed asks the same apiExposureDenialReason(enable, 'aggregate') decision over cubeObjects(cube), no second rule — Right; (5) the optional admit?: CubeAdmission parameter enlarges the published surface, declared yes (narrowing) — Right; (6) query paths unchanged — Right; (7) the configured-cube loop sits after the probes are assigned — Right; (8) nothing is recorded from an absent or throwing declaration lookup — Right; (9) the api-exposure-door.test.ts reorder keeps every assertion and pins the window — Right; (10) the #register re-anchor, the verifiedAt stamp and the _note — Right; (12) the check:platform-checklist red is main-side — Right, and this round's one changed file is a ledger no checklist anchor points into; (13) the round-1 merge of main was clean — Right; (14) the diff sits inside the amended file surface — Right, re-confirmed in 20 below.

  1. The ledger delta, value-only, measured. A JSON walk of analytics_cube.json at origin/main, at 3a646f5ffb and at the head: 91 leaf paths on every side, the same set; 0 status changes. Changed leaves against main: producer 16, verifiedAt 16, evidence 1, _note 1. Changed leaves against the PASS head: producer 16 and evidence 1 — exactly the 17 lines of the commit, and exactly the two leaves flag ③.4 named. The 16 rows carrying verifiedAt: 2026-10-10 are name, title, description, sql, public, joins.name, the five measures.* and the five dimensions.* rows; their 16 producer strings are one string. The dev's fence numbers (91 / 0 / 1-16-16-1 against main, 16-1 against the PASS head) are reproduced. Right.

  2. Flag ③.4 (i) — joins.children.name, first evidence citation — discharged. The stale quote is gone: joinTable has 0 occurrences in native-sql-strategy.ts at head and 0 in the ledger. The new citation holds where it is sited: const joined = joins?.[alias]?.name; occurs once in hop-object.ts, at line 82, inside export function resolvePathHops( (declared at line 70), and it is the first tier tried — the via = 'join' arm precedes the referenceOf arm — so "tier 1 of the one hop resolver" is what the code says. The retained anchor native-sql-strategy.ts#qualifyAndRegisterJoin (declared at line 1516) holds its new quote: object: hop.object occurs once in the file, at line 1571, inside the joins.set call that qualifyAndRegisterJoin makes for each hop resolvePathHops returns, whose sql is the LEFT JOIN and whose object is the join's resolved target — the LEFT JOIN is emitted against the object the join's name named. The row's other three citations are byte-identical to the PASS head and still resolve: crossFieldComparisonIn (declared at 918) reads cube.joins?.[alias]?.name ?? alias at 945; analytics-service.ts#cubeObjects and objectql-strategy.ts#isCrossObjectField are named in their files. Right.

  3. Flag ③.4 (ii) — the configKey clause of the 16 producer strings — discharged. configKey: 'analyticsCubes' occurs once in packages/core/src/capability-providers.ts, at line 77, inside the analytics entry of export const CAPABILITY_PROVIDERS (declared at line 64): the clause is now sited where the declaration is. The serve.ts clause keeps packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS and now quotes what that file holds: const CAPABILITY_PROVIDERS = Serve.CAPABILITY_PROVIDERS occurs once (line 4593, the resolver's local handle, indexed as CAPABILITY_PROVIDERS[cap] at 4613), spec.configKey === 'analyticsCubes' occurs once (line 4663), arg = { cubes } occurs once (line 4671), and const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? resolveStackCollection(config, 'analyticsCubes'); is lines 4669 to 4670, the ledger joining the two source lines with one space — the one departure from byte-verbatim, a line wrap, read the same way by the PASS record. The symbol is also declared as the class's static readonly CAPABILITY_PROVIDERS at line 1863, so the anchor names a declaration in the cited file under the gate's rule either way. The dev's choice of the local const over the enclosing run() method is Right: the gate's unit is a symbol named in the file, the const is the handle the quoted loop indexes, and a method name spanning thousands of lines would anchor nothing the quote is about. The analytics-service.ts#register clause is unchanged from the PASS head and still true: for (const cube of config.cubes) occurs once, at line 1491, and this.cubeRegistry.register(cube) at line 1493 is its body (the other occurrence, line 1455, is the request-scope lambda). The superseded clause (the analytics entry declaring configKey at serve.ts#CAPABILITY_PROVIDERS) has 0 occurrences in the ledger. Right.

  4. Every anchor on the 16 rows resolves under the gate's rule. Applying evidence.mts#isSymbolNamed (identifier-bounded) to every path#symbol in the evidence, producer and note leaves of the 16 stamped rows against the head tree: 27 distinct anchors — the PASS record's 25 plus the two this round added, capability-providers.ts#CAPABILITY_PROVIDERS and hop-object.ts#resolvePathHops — and 0 unresolved, every cited file present at head. Across the whole ledger: 29 distinct anchors, 0 unresolved. The gate's own answer agrees: Spec property liveness and Test Core (1/6) are success on this head. Right.

  5. Brief item 3 — is any other leaf on the 16 rows still untrue at this head? The PASS record's ①.11 resolved every anchor and every code-shaped fragment on those rows and found exactly the two residuals now fixed. Every leaf of those rows other than the 17 this commit changed — the evidence of the other 15 rows, all 16 note leaves, every status and verifiedAt — is byte-identical to the PASS head, and so is the file-level _note ("registered one cube at a time by register"), so that reading carries over by identity; the anchors were re-resolved mechanically here (18) and the quotes in the two changed leaves were re-read against the code (16, 17). Nothing on the 16 rows is found untrue at this head, and verifiedAt: 2026-10-10 is the date of this re-verification too. Right.

  6. Fence and surface. The amendment 6097143716 admitted one file, packages/spec/liveness/analytics_cube.json, value-only, no status, key or row added or removed, no other spec file. This round: the same 91 key paths, 0 status changes, 17 string leaves changed, nothing else in the commit. The PR's file list is one .changeset/22663-*.md, five files under packages/services/service-analytics/src/** and that ledger; no packages/lint, no content/docs/releases/, no governed surface (Governed Surface Queue Guard is success); 648 changed lines; the head repository is the base repository. Right.

② Semver level

  • The changeset .changeset/22663-analytics-unexposed-cube-registration.md is blob-identical to the PASS head: @objectstack/service-analytics minor, BREAKING as an accept-set narrowing under the launch-window convention, with FROM → TO, the unchanged set, the measured producers and the ADR-0087 marker not-required (no-migration-prescription); Check Changeset accepted it on this head (114225734207). The level is right: a narrowing ships minor, a yes declaration takes at least minor, and the package whose source moved is the one bumped. This round publishes nothing new: a packages/spec/liveness/*.json ledger is a gate input, not an export or an authorable key, so no @objectstack/spec bump is owed for it.
  • Clause-②: line. Changeset line 7 and PR body line 2 both read Clause-②: yes (narrowing), unchanged since the PASS head and still the truthful declaration: yes for the public-surface enlargement of ①.5, (narrowing) for the accept-set narrowing. Right as declared.
  • Not a skip-changeset case: the package publishes and the diff changes runtime behaviour.

③ Boundary flags

  1. Flag ③.4 of 6097791315 — the two stale leaves under the fresh verifiedAt. The seat ruled fold-not-follow-up in 6097801200; the fold is 0dd4426835; ①.16 and ①.17 verify both leaves at head. Discharged.
  2. Round-2 report: open_questions: [], out_of_scope_findings: []. Nothing new is raised. The report's claims that were checkable from the fetched objects — one file, 17 in and 17 out, 91 key paths, 0 status changes, 16 + 1 changed leaves, every quote present at head, origin/main unmoved — are each reproduced above. Answered.
  3. Earlier flags, carried. ③.1 of the PASS record (the registration window as its own card, analytics: a configured cube over an object declared after the analytics plugin's init() still registers and lists in GET /analytics/meta, though every query of it is refused — the registration window #22663 leaves #22679, open; the derivation blind spot, pm tooling: dispatch-gates.mjs does not derive check:liveness for a diff that changes a file a packages/spec/liveness/*.json producer anchor cites, so a consumer-package PR can remove an anchored symbol with every derived gate green #22680, closed by triage), ③.2 (the file-surface amendment) and ③.3 (check:platform-checklist red on main, a duplicate of check:platform-checklist is red on main #22594) were discharged there and nothing in this round reopens them: the one changed file is a liveness ledger that no checklist anchor points into, and the dev's round-2 reading of that gate is line-for-line the round-1 control reading on clean main. Acceptance notes 1 to 6 stand as judged; the code they describe is byte-identical to both earlier heads. Discharged.
  4. Observation, no action owed: anchor rot-sensitivity, as the PASS record noted for analytics-service.ts#register (named 16 times in the file). The same holds for serve.ts#CAPABILITY_PROVIDERS (named 14 times): that anchor would survive the removal of the analyticsCubes branch alone. The gate's contract is word-naming, its own doc places that residual on the file-level key-mention backstop, and the quoted fragments (spec.configKey === 'analyticsCubes', the const cubes line) are what a reader checks next; no more specific declared symbol exists in either file for the site cited. Nothing to escalate.
  5. Nothing is owed before the queue on this head. The PR is a draft; readying and arming are the owning seat's acts under its records.

Implemented-by: claude/issue-22663-analytics-unexposed-cube-registration
Reviewed-by: session_013j5gkUCpqQiti4GgPqqmnt (contract-review subagent, CONTRACT_REVIEW_TIER)

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 13:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 13:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 7399080 Oct 10, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22663-analytics-unexposed-cube-registration branch October 10, 2026 14:17
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

2 participants