feat(analytics): enforce analytics_cube.public and default it to visible - #20348
objectstack-fleet[bot] wants to merge 8 commits into
Conversation
…eta and every query door Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
CubeSchema.public defaults to true (it was false while nothing read it). service-analytics reads it: getMeta omits a cube declared public: false, and query() / generateSql() refuse it with CUBE_NOT_FOUND / 404 before any strategy runs. The three internal mints (inferCubeFromQuery, compileDataset, CubeRegistry.inferFromObject) write the visible default, so the ad-hoc KPI path and the dataset door keep answering. Test fixtures that restated the old default are re-spelled public: true; the showcase cube, which is the app's /analytics/* demonstration, drops its public: false. The liveness row for public flips to live. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…s projections data/Cube:public false -> true is declared in DEFAULT_CHANGES_BY_MAJOR (the authorable-defaults ratchet refuses an undeclared default move); the reference page and the liveness state counts are regenerated by their generators. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…alytics-cube-public-enforced
…clared dimension Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 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 c1557e6a68f03a049e235c0e9860669b73885c93 && git checkout c1557e6a68f03a049e235c0e9860669b73885c93
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d498113b505f7b29b5e2c554f3152145edeb3461 f84cd184dff0644cc2aca0e631a7309370b85db1 && git checkout -B drift-repro d498113b505f7b29b5e2c554f3152145edeb3461 && git merge --no-ff f84cd184dff0644cc2aca0e631a7309370b85db1
node scripts/docs-audit/affected-docs.mjs --json d498113b505f7b29b5e2c554f3152145edeb3461
|
…ced cube visibility getMeta no longer returns every registry cube: it omits one declaring public: false, and query()/generateSql() refuse it with 404 CUBE_NOT_FOUND. The showcase cube no longer declares public: false. Text only; the item's clauses and verdict are unchanged. Revision 2 with its history entry. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…alytics-cube-public-enforced
The os-regen driver kept the branch's side of state-counts.md in the merge of origin/main; os-regen-merge step 2 took main's side and this commit regenerates it with gen:liveness-counts over the merged ledger (analytics_cube live 18 / dead 9; total live 937, dead 165, 1117 rows). Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
Part of #20282
Clause-②: yes
Rework round 1
The order of record is seat comment
5861384124on #20282; claim5859909653is unchanged. The open question was ruled A in-seat: no ADR-0087 semantic entry, andClause-②: yesandminorstay as shipped. There is one new commit on top,2cfa134c34, with no rebase and no force-push.docs/qa/platform-checklist/areas/dashboards.json, itemdashboards.cube-query. It moves from revision 1 to 2 and gains a history entry. Three pieces of text were rewritten to what is true after this PR:getMetalists a cube only when itspublicis notfalse. Apublic: falsecube is omitted and refused byquery()/generateSql()with 404CUBE_NOT_FOUND.showcase_deliverydeclares nopublic, so it is visible by default.public:false.getMetasource line no longer says it "returns all registry cubes".showcase_deliverystays visible with the same four measures and four dimensions.pnpm check:platform-checklistis OK (266 items; symbol anchors 624/642, 18 on the named residual).@objectstack/verifybootStack(the real Hono app, in process), with theanalytics-admission-fixturestack on sqlite-wasm.CubeSchemaand passed asAnalyticsServicePlugin({ cubes }):open_summary(visible) andhidden_summary(public: false), both overadmission_open.GET /api/v1/analytics/metaand gets 200 listingopen_summary"Open Summary (authored)" with measureopen_summary.authored_total. B'sPOST /api/v1/analytics/queryfor that measure answers 200,authored_total: 2.POST /api/v1/analytics/dataset/querywith an inline dataset namedopen_summaryoveradmission_walled, an object A has no grant on. The answer is 403PERMISSION_DENIED. B's meta then answers 200 listingopen_summarytitled "inline by A" with onlyopen_summary.hijack_cnt. B's query ofauthored_totalanswers 400INVALID_FIELD("…which object 'admission_walled' does not have. Valid measures: hijack_cnt").queryDatasetregisters the compiled cube before its admission gate runs.admission_openanswers 200 and makes the same replacement. B'sopen_summary.hijack_cntanswers 200, and the count is B's own RLS scope.hidden_summaryanswers 404CUBE_NOT_FOUND(this PR's gate). A's inline dataset namedhidden_summarythen answers 200. B's meta now listshidden_summary, with A's definition. The authoredsecret_totalis gone (400INVALID_FIELD); the hidden definition was replaced, never disclosed.2cfa134c34.dispatch-gates --commandsderived the same 113-command set, and all of it was re-run on this head.--ran: 112 run with exit 0, 1 NOT MEASURED, 0 unrun.check:skill-examplesandcheck:type-check-debtfirst exited 3 and passed once their build prerequisites were built. The type-check-debt re-measure is OK: 4 ledger entries, 53 raw errors, none above its record.check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (34 workspace packages have nodist/here; a whole-tree build for CI).Stage 1 of the
analytics-cube-semanticsfamily:analytics_cube.publicand its default. Triage verdict ENFORCE (5859510828, execution note 1), claim5859909653. #20282 remains open for the later stages (format,granularities,refreshKey, descriptions, and the objectui picker sub-issue). This PR does not touch any of them.What changes
CubeSchema.publicdefaults totrue(it wasfalse) and gains a.describe().falsehides the cube from the analytics API. It is visibility, not row security. The default change is declared inDEFAULT_CHANGES_BY_MAJOR(packages/spec/scripts/lib/default-changes.ts), which the authorable-defaults ratchet requires.packages/services/service-analytics/src/cube-visibility.ts.isCubePublicis the one reader: a cube is exposed unless it declarespublic: false. The registry holds the input shape, so an omitted key reads as the schema default.cubeNotPublicErroris the refusal.AnalyticsService#getMetaomits a hidden cube.getMeta(name)for a hidden cube answers[], the same answer as a name no cube has.AnalyticsService#assertCubePublicruns first inquery()and ingenerateSql(). It runs before token resolution, cube inference, admission and every strategy, so the refusal leaves the registry untouched. The refusal is404 CUBE_NOT_FOUND, with a message that says how to expose the cube. It is never an empty result.inferCubeFromQuery,compileDatasetandCubeRegistry.inferFromObjecteach wrote a literalpublic: false, the old default. They now writetrue. Without that, the ad-hoc KPI path and the dataset door would refuse the cubes they mint themselves (see the internal-caller census below).examples/app-showcase/src/data/analytics/showcase.cube.tsdrops itspublic: false(evidence below).publicrow inpackages/spec/liveness/analytics_cube.jsonflipsdeadtolive, citingcube-visibility.ts#isCubePublic,analytics-service.ts#getMetaandanalytics-service.ts#assertCubePublic, with the CLI threading asproducer. The README cell andstate-counts.md(regenerated) now readanalytics_cubelive 18 / dead 9.authorable-defaults/data.json(by the build) andcontent/docs/references/data/analytics.mdx(gen:docs).state-counts.mdcomes fromgen:liveness-counts. No file underskills/**or any other governed surface changed, so this PR is not Tier H.Present state, measured before the change (base
4e0f72e8d2)packages/spec/src/data/analytics.zod.ts,public: z.boolean().default(false)under an/** Access Control */comment, with no describe.git grepforcube.publicor.publicfound no reader inpackages/services/service-analytics/srcorpackages/drivers.GET /api/v1/analytics/metagoes togetMeta.POST /api/v1/analytics/querygoes toquery().POST /api/v1/analytics/sqlgoes togenerateSql().domains/analytics.ts.POST /api/v1/analytics/dataset/querygoes toqueryDataset, which usesDatasetExecutorand thenquery().ctx.getCube(query.cube), the root cube. Joins reach objects, not cubes, so no second cube is resolved per query.public,examples/app-showcase/src/data/analytics/showcase.cube.ts:98withpublic: false. No platform object or qa fixture authors a cubepublicvalue. Test fixtures restatedpublic: falsein 45 files: 44 inservice-analytics, pluspackages/runtime/src/cross-field-refusal-operand-withhold.test.ts.99f0e9d296, test only) gaveTests 10 failed | 3 passed (13):expected [ 'hidden_cube', 'visible_cube', …(2) ] to not include 'hidden_cube'(getMeta lists the hidden cube).query():promise resolved "{ rows: [ {} ], fields: [ { …(2) } ] }" instead of rejecting.generateSql():promise resolved "{ …(2) }" instead of rejecting.The showcase decision, with evidence
The cube is meant to be seen and queried, so this PR drops the
falserather than keeping the cube hidden:examples/app-showcase/src/coverage.tsmarksanalyticsCubesdemonstrated, "Served by the foundational analytics capability (/api/v1/analytics/*)"./api/v1/analytics/*)".docs/qa/platform-checklist/areas/dashboards.jsonrunsGET /api/v1/analytics/meta?cube=showcase_deliveryandPOST /api/v1/analytics/query { cube: 'showcase_delivery', … }.A hidden showcase cube would demonstrate a 404. It is pinned in
examples/app-showcase/test/gap-fill.test.ts(DeliveryCube.public === true).Internal callers of the same door
Only
AnalyticsServicePluginregisters theanalyticsservice in this repo. In-repo consumers are the runtime REST domain, the REST dataset door andservice-analyticsitself.packages/runtime/src/domains/mcp.tsandaction-execution.tscall a differentgetMeta(the metadata protocol's).Two internal paths reach
query()with a cube they minted:queryDatasetusesDatasetExecutor, thenquery(), on the dataset's compiled cube.Both producers wrote the old default as a literal. Neither literal meant "hidden": if it had, enforcement would refuse the producer's own query. So they now write
true. That keeps their behavior (visible and queryable, as before) and does not widen the door. No internal caller needs to reach a non-public cube, so there is noneeds_decision.Pins
packages/services/service-analytics/src/__tests__/cube-public-visibility.test.ts, 13 cases:getMetaomits a hidden cube;getMeta(hidden)answers[].query()andgenerateSql()refuse with{ code: 'CUBE_NOT_FOUND', status: 404, cube }, and no strategy ran.CubeSchema-parsed cube are all answered.queryDatasetanswers.packages/spec/src/data/analytics.test.tspins the default (true) and an explicitfalsethat parses and is kept.Ablation
The reader was mutated at HEAD
33348d6ec3so that it stops filtering.node scripts/ablation-replace.mjs --file packages/services/service-analytics/src/cube-visibility.ts --anchor 'return cube.public !== false;' --replacement 'return true;'(WRAP mode, with the lock-held test run as its child).44d9fcf712d5toe144bb908748.srcpaths (../analytics-service.js,../cube-visibility.js), so no packageexportsand nodist/sit on the subject's resolution path.src/__tests__/cube-public-visibility.test.tsgaveTests 6 failed | 7 passed (13). The six reds are exactly the twogetMetapins and the four door-refusal pins. The controls and the internal-producer cases stay green. The direction was red, as expected.44d9fcf712d5, equal to the HEAD blob, andgit diff HEADis empty. Two earlier attempts timed out in the verify-lock queue (exit 99) and never ran the test; each of their restores was proven the same way.33348d6ec3.Tests 93 passed (93)across the pin file,analytics-service.test.ts,query-dataset.test.tsandcube-inference-gate.test.ts.Changeset
.changeset/20282-analytics-cube-public-enforced.mdbumps@objectstack/specand@objectstack/service-analyticsatminor. The reasons:Clause-②: yestakes at leastminor.fixedgroup.The changeset is not declared breaking and carries no ADR-0087 marker. It also records the upgrade notes: omitted key (no change), explicit
false(now hidden),os compileartifacts that carry a materializedfalse(recompile), and the platform-minted cubes.Local verification
Every reading below was taken at HEAD
33348d6ec3, a clean tree that includes a merge oforigin/mainateea8787aa7, unless a line says otherwise.@objectstack/service-analytics, fullvitest run, atc9382ff256.Test Files 1 failed | 129 passed (130),Tests 1 failed | 3053 passed (3054).dimensions. It is fixed in33348d6ec3, which changes only that test file.33348d6ec3the pin run is93 passed (93).@objectstack/service-analyticstypecheck(tsc --noEmit, which reaches the tests): exit 0 atc9382ff256.@objectstack/spec, targeted.src/data/analytics.test.ts,analytics-strictness-batchd.test.ts,src/api/analytics.test.ts,src/contracts/analytics-service.test.tsandsrc/kernel/metadata-type-schemas.test.tsgaveTests 227 passed (227).data/Cube:public: false → true.check:generatedreports all 15 artifacts up to date.check:livenessis green:analytics_cube 27 classified (live 18, dead 9).packages/runtime/src/cross-field-refusal-operand-withhold.test.ts, against a rebuiltservice-analyticsdist:Tests 11 passed (11).99f0e9d296.Tests 10 failed | 3 passed (13)(the readings are quoted above).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 113 commands. All were run and recorded, then reconciled with--ran: 111 exited 0, 2 are NOT MEASURED, 0 are unrun.check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET, 64 workspace packages have nodist/in this worktree, so this is a whole-tree build for CI.check:type-check-debt, reason: PREREQUISITE NOT MET,@objectstack/driver-tursohas no built types.check:skill-examplesfirst exited 3, because the client packages were not built. It was re-run after building them and exited 0.examples/app-showcase/test/gap-fill.test.ts, reason: the showcase's dependency closure is not built here (Failed to resolve entry for package "@objectstack/connector-mcp", so the run never reached a test). The pinned fact itself was measured lock-free: evaluatingsrc/data/analytics/showcase.cube.tswithtsxprintsDeliveryCube.public = true. The file runs in CI.@objectstack/spec's fullpnpm testandtypecheck./analytics/*routes call.Acceptance notes
Observations, not filed:
MemoryAnalyticsServicedoes not readpublic.@objectstack/driver-memory's standaloneMemoryAnalyticsService#getMetaand#queryignore the key.AnalyticsServicePluginregisters theanalyticsservice.AnalyticsServiceit is reached only through the gatedquery()andgenerateSql(), andAnalyticsService#getMetanever consults it.public,refreshKey,format,granularitiesand descriptions take effect (8 keys) #20282 descriptions stage, whose reader list already names bothgetMetaimplementations.docs/qa/platform-checklist/areas/dashboards.json: fixed in rework round 1 (item 1 above).public. It was measured at the public door in rework round 1 (item 2 above). Not fixed here; the seat decides whether to file it.Generated by Claude Code