fix(service-analytics): an inferred cube lives only in its request — no request writes the shared cube registry (#20381) - #20433
Conversation
…no request writes the shared cube registry The ad-hoc `query` and `sql` doors no longer publish the cube `ensureCube` infers for an admitted request. It stays in the call's request scope and is dropped with it, like a measure appended to a configured cube, so the shared registry (and `getMeta`) holds configuration only: manifest cubes and `registerDataset` datasets. The next request of the same name infers again through the same gates. `publishInferredCube` had no caller left and is removed; `ensureCube` returns nothing again. The `CubeRegistry` class docblock now lists the two configuration sources and states that no request writes it. Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…pin that no request writes meta The tests that read an ad-hoc request's inferred cube back through `getMeta` or the shared registry now read it where it lives: the cube the request's strategies are handed (a declining probe strategy records `ctx.getCube`). The assertions whose subject was the retired registration now pin its absence. The route pin's two CONTROL legs compare member B's cube list for exact equality, and B's `meta` as raw bytes: an admitted ad-hoc query over a walled object leaves it byte-identical, and a second same-name request is re-inferred and answers the same. The new changeset records the retirement; the items 1-2 changeset no longer says an admitted request's inferred cube is still listed. Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…ol; index access for the es2021 lib Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
…tire-inferred-cube-source
…s the retired registration `cube-public-visibility.test.ts` asserted that an inferred cube is registered with `public: true` and listed by `getMeta`. Under the retirement it is answered on every request and never registered or listed; the case now pins that. `inferCubeFromQuery` keeps its `public: true` literal (moot, and the pending #20282 release note says it is written) with a comment that no longer claims the cube is registered. Claude-Session: https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ca154672820a81fc65253966943dbfc86aa978bb && git checkout ca154672820a81fc65253966943dbfc86aa978bb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e01d3473053d2d43f5fca0db98b0409744695a5a 8214a5b641abf70b0c875c846c583dd49ffc9d74 && git checkout -B drift-repro e01d3473053d2d43f5fca0db98b0409744695a5a && git merge --no-ff 8214a5b641abf70b0c875c846c583dd49ffc9d74
node scripts/docs-audit/affected-docs.mjs --json e01d3473053d2d43f5fca0db98b0409744695a5a
|
Contract reviewServed-tier: Inputs: card #20381 (body and all 11 comments, ruling ① Derived judgmentsEach accept-set and public-surface change the diff implies, named and judged:
② Semver level
③ Boundary flagsDev deviations (report
Out-of-scope carriers (report Security-family lane: the defect and the fix are described here at the code-path level only; the pins carry the observations. Check-runs on the head (REST, latest run per name, all 35 settled at 11:54Z): 31 Implemented-by: VERDICT: PASS Rendered by an isolated contract-review subagent and adopted by the Generated by Claude Code |
Landing with one red by design:
|
Fixes #20381
Clause-②: no
Item 3 of #20381, under director ruling
5866558247(letter A, maintainer 「同意」): registry source 3 is retired. Items 1–2 landed in PR #20407 (50e273fd), so this round completes the card.What changes
queryandsqldoors (POST /api/v1/analytics/query,POST /api/v1/analytics/sql) no longer publish the cubeensureCubeinfers for an ADMITTED request. That cube stays in the call's request scope, the one PR fix(service-analytics): the ad-hoc query and sql doors run in a request scope, publishing an inferred cube only after admission #20407 gave these doors, and it is dropped with the call, like a measure appended to a configured cube. The next request for the same name infers the cube again, through the same existence and source-field gates, and gets the same answer.CubeRegistry, and thereforegetMeta()andGET /api/v1/analytics/meta, is now written by configuration only: manifest cubes (AnalyticsServiceConfig.cubes) andregisterDatasetdatasets./analytics/metalists the authored vocabulary, whatever traffic the server has seen since boot.publishInferredCubehad no caller left and is removed, andensureCubereturnsvoidagain. TheCubeRegistryclass docblock now lists the two configuration sources and states that no request writes the registry. TheCubeScope,queryIn,requestScope,ensureCubeand analytics 自动推断路径:measures上的关系穿越点号 member 仍被剥成基表列 ——owner.region_count_distinct静默聚合基表region(#5739 裁决未覆盖的第四个铸造点) #5918 comments inanalytics-service.tsno longer describe the publication.packages/specchange, no visibility marker and no caller-awaregetMeta; options B, C and D are not taken.Landing point, as dispatched:
packages/services/service-analytics/src/analytics-service.ts(the producer of the write) andcube-registry.ts(docblock only).Tests: re-observed, not deleted
On the fix commit, 36 cases in six service-analytics test files went red; each of those files read an inferred cube back through
getMetaor the shared registry. PR #20348, which landed while this round ran, added a seventh such case. Each case is re-observed through a window that still exists after A: the cube the request's own strategies are handed. A probe strategy placed ahead of the built-in ones recordsctx.getCube(query.cube)and always declines, so the chain runs as it would without the probe. Where an assertion's subject was the retired registration itself, the assertion now pins its absence.infer-cube-where-spelling-parity.test.tsgetMeta('deal')infer-cube-relation-traversal.test.tsrun()members viagetMetadotted-measure-refusal.test.tsrun()measures viagetMeta; block 2 case 1 asserted that the first query warmed the registryanalytics-service.test.ts'auto-infer'cubeRegistry.has('case')is truecaseand backed bycase;has('case')is falsecube-inference-gate.test.tsKPI casecubeRegistry.get('crm_account')is truthyadhoc-query-request-scope.test.ts(PR #20407)cube-public-visibility.test.ts(PR #20348)public: trueand listedThe route pin is
packages/qa/dogfood/test/analytics-adhoc-query-isolation.dogfood.test.ts:bootStackwith two sign-ups plus the administrator, on sqlite-wasm and memory, through both doors.arrayContainingto exact equality on member B's cube list. B'smetais also kept as the raw response bytes.metais byte-identical and B's query of the configured cube is unchanged. B's own query of that object is still refused with the ADR-0112 envelope.metalists the name.Evidence (head
8214a5b6unless stated)pnpm --filter @objectstack/service-analytics typecheckis clean.vitest run: 132 files, 3093 passed.tsc --listFilesincludes every changed test file.pnpm --filter @objectstack/dogfood typecheckis clean, and--listFilesincludes the route pin. The six analytics dogfood files: 54 passed, on adistrebuilt after mergingmain.callCtx, as in50e273fd. It was applied withscripts/ablation-replace.mjs(anchor 2 → 0) and predicted before running.service-analytics, withablation-dist-preflightfinding the marker in 2 dist files: 8 red / 16 green, both CONTROL legs on all four boots. For example:expected [ 'open_summary', …(3) ] to deeply equal [ 'open_summary', …(2) ], with+ "admission_walled".HEAD,git diff HEADis empty, rebuilt, and--absentpreflight is green with a clean tree.fccfc3e5the same ablation gave 9/117 and 8/16.fccfc3e5, proves the probe window can fail. It makes an arraywhereseed no dimension, the pre-observation:inferCube仍把数组where当「不是筛选」跳过 —— #5334 之后这个!Array.isArray守卫已经过时 #5353 shape. Across parity and traversal: 16 red / 24 green. In parity, the 11 conjunction table cases, ALONGSIDE and the two dotted array-versus-object cases went red; both$orcases stayed green, as the file predicts. In traversal, the two array-spelling mint cases went red. The restore was proven the same way.dispatch-gates --commandsderives 66 commands over the 12 changed paths.--ranreconciles 66 derived, 66 run, 0 NOT MEASURED and 0 UNRUN. 65 exit 0;check:empty-changesetexits 1 by design (see Changesets).--no-inline-config --format jsonover the 10 changed.tsfiles reports 10 files, 0 errors and 0 warnings. The population is those 10 files, none ignored. Invariance:eslint.config.mjsnever enables type-aware linting, so an untouched file's verdict cannot move. The fullpnpm lintis left to CI.Changesets
.changeset/20381-retire-inferred-cube-source.md,@objectstack/service-analyticspatch,Clause-②: no..changeset/20381-adhoc-cube-request-scope.mdwas added by PR fix(service-analytics): the ad-hoc query and sql doors run in a request scope, publishing an inferred cube only after admission #20407 and is not yet released. It said that an admitted request's inferred cube "still registers the cube it inferred, as before" and that it "is still listed". This PR makes both sentences false, so they are removed and replaced by a pointer to the new entry.check:empty-changesetrefuses any PR that modifies a changeset it did not add. For this DELIBERATE CORRECTION class it stays red by design (ruling D on finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR'sminorchangeset and every gate stayed green #17712), and it needs a person's confirmation here. Restoring the file from50e273fdwould clear the gate, but the release would then ship both statements in one CHANGELOG.Overlap with PR #20348
PR #20348 landed first (
f2c7eef5), andmainis merged here (dfd185d7) with no textual conflict.public: trueon the inferred cube is moot under ruling A, because no visibility verdict ever reads that cube. The literal is kept: the pending note.changeset/20282-analytics-cube-public-enforced.mdsays the inferred cube "now writestrue", and dropping the key would falsify a second foreign changeset. Only its comment, which said the cube is registered, is corrected.generateSqlgate still asksthis.sharedScoperather than the call's scope. The answer is identical, because a fresh request scope with no dataset reads through to the shared registry, so this is noted, not changed.Acceptance notes
ensureCube'swarn("No cube registered …; auto-inferred a minimal cube …") used to fire once per name per process, because the second request found the published cube. It now fires on every such request; scalar metrics stay atdebug. This was not measured against real dashboard traffic, and it is noted, not changed: the ruling adds no state.content/docs/api/data-api.mdx, in itsGET /analytics/metasection, says that a cube a query references "is lazily auto-inferred from that query's shape". It does not claim the cube gets listed, but it could now say that it does not. That file is outside this card's file surface.metafor inferred cubes once this lands. They stay as notes for the ADR-0106 D5 audit.cubeRegistry.get(...)is undefined after a rejected query (the three source-field gate files,cube-inference-gate,dotted-measure-refusal) now hold by construction for every request, not only for refused ones. They are left as they are.Generated by Claude Code