Repository navigation
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
objectstack-fleet[bot] merged 7 commits into
Conversation
…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>
…alytics-unexposed-cube-registration
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 3 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 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 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
|
|
CI red at
Cause. 16 rows in Fix. Re-anchor those 16 rows on the symbol that now registers configured cubes. That is a value-only edit of one 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>
Contract reviewServed-tier: PR #22675 for card #22663, read against Check-runs on the head (34 check names, latest run per name, all completed, read at 2026-10-10T11:53Z). Red: The head moved while this record was being rendered. The branch tip is now ① Derived judgments
② Semver level
③ Boundary flags
Owed before the queue, on the same branch: (a) the 16 Implemented-by: 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>
…alytics-unexposed-cube-registration
Contract reviewServed-tier: PR #22675 for card #22663, round 1, written 2026-10-10T13:03Z, read against Check-runs on the head (42 runs over 35 check names, latest run per name, all The earlier record's owed items. (a) The 16 ① Derived judgmentsJudgments 1 to 9 of the earlier record carry over by identity, not by re-derivation: the five
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
Seat's call on ③.4 of
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>
Contract reviewServed-tier: PR #22675 for card #22663, round 2, written 2026-10-10T13:46Z, read against Check-runs on the head (42 runs over 35 check names, latest run per name, all What moved since the PASS head. One commit, ① Derived judgmentsJudgments 1 to 9 of
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
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'sapiExposureDenialReasondenies for theaggregateoperation is now refused when it is registered.GET /analytics/metano 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:servicesseat 1 (sessionsession_013j5gkUCpqQiti4GgPqqmnt, seat post #6021) under claim6096404246. File surface held:packages/services/service-analytics/src/**and one.changeset/22663-*.md. Nothing inpackages/spec,packages/lintorcontent/docs/releases/is touched.Step 1: census (before any edit, on
origin/mainee3ae0360d)analyticsCubes/defineCube), examples and platformshowcase_delivery(showcase_task, joined toshowcase_project)enableblock.defineDataset):examples/**6,platform-objects5sys_*bases grantlistor are not declared in this repo (sys_package_installationis a cloud object). None of the eleven enters the cube registry: stack datasets are compiled per request byqueryDataset(request scope), never byregisterDataset.registerDatasetcallers outside testsservice.cubeRegistry/inferFromObjectoutside testsanalytics:readylisteners that register cubespackages/qa/dogfood) with a cube or dataset over anapiEnabled: false/ no-listobjecthidden_cubeandnolist_cubeinapi-exposure-door.test.ts(PR #22645)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.AnalyticsServiceConfig.cubescubes, whichos servefills fromanalyticsCubes(CAPABILITY_PROVIDERS.analytics), inside the plugin'sinit()warnper cube names the cube, the object and the declaration, and every other cube still registersAnalyticsServiceConfig.datasetsregisterDatasetregisterDataset()service.cubeRegistry.register()/registerAll()/inferFromObject()cubeRegistryfield (no in-repo caller)queryDataset()/ ad-hocquery()/generateSql()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.registerDatasetalready 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: 404forenable.apiEnabled: false;code: OBJECT_API_METHOD_NOT_ALLOWED,status: 405for anenable.apiMethodswhitelist that does not grant the operation, plusallowed.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-provenanceOK).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 fromgetMeta, 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: aLiteKernelwith the realObjectQLPlugin, threeapiEnabled: falseobjects registered through themanifestservice, and an observer plugin inited just before analytics. The probe is not committed.getMetabefore this changegetMetaafterinit()precedes analyticsinit()follows analyticsstart()After boot, all three objects are declared. In
os serve, read from source:AppPlugin(an app's own objects) andPlatformObjectsPluginarekernel.used before the capability resolver mountsanalytics, so a cube over an app's own objects or a platform object is judged. Objects contributed by a capability provider that followsanalyticsinrequiresorder,start()-time registrations andkernel:readyinstalls fall in the window.That is the #5115 probes' ordering:
registerDatasetcompile gating asksgetObjectDatasource/isExternalObjectat the same moment and takes "cannot answer, do not block". This reuses it:The conclusion drawn is never "absent, therefore refused", so
check:startup-registry-verdictholds (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 sameapiExposureDenialReason(enable, 'aggregate')call and returns the same codes. It does not block when it cannot answer.cube-registry.ts: an optionalCubeAdmissionconstructor parameter, asked byregisterbefore storing. Every write path goes throughregister. A registry constructed without one admits everything, as before. The parameter is additive.analytics-service.ts:cubeObjects(cube)(base object plus declared joins), the set the query door's first check reads.registerDatasetjudges 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.getObjectDeclarationhook gets no gate, as on the query door.Pins (
unexposed-definition-registration.test.ts, 12 cases)Refusals:
apiEnabled: falseand aget-only whitelist are each warned once, located, absent from the registry and fromgetMeta()/getMeta(name). The other cubes register.OBJECT_API_DISABLED/404 orOBJECT_API_METHOD_NOT_ALLOWED/405, withobject.registerDataset. It throws for an unexposed base object (404 and 405 variants) and for an unexposed joined object (viainclude), naming the dataset and the object. Nothing is registered, andgetMeta(name)is empty.datasets. Each refused dataset is warned once and skipped, and the exposed one registers.AnalyticsServicePluginover a realObjectQLengine with a SQLite driver: configured cubes over unexposed objects are refused atinit(), warned, and absent fromgetMeta.registerDataseton the plugin's service refuses, and an exposed dataset registers.Controls and tiering:
PERMISSION_DENIED/403.Fixture triage,
api-exposure-door.test.ts. Itshidden_cubeandnolist_cubewere 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'sbeforeAlltherefore 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.mjsin wrap mode, under the verify lock, atc8dae0a783. The test imports the service fromsrcby a relative path, with nodisthop.assertRegistrable:assertDefinitionExposed(kind, cube.name, this.cubeObjects(cube), provider);. It hit once, as declared.07cb9454e93f→ea44bf4cd373.07cb9454e93f, equal to HEAD, andgit diff HEADis empty. I re-checked independently afterwards: the ablation marker count is 0 and the anchor count is 1.Verification (final HEAD
dcc38d4413, after mergingorigin/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 --noEmitclean.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 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 speccheck:error-code-provenance. Each exit was captured before any pipe, and all 69 exited 0.--ranreports: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero because all 65 recorded an exit code.check:adr-0087-registration: theno-migration-prescriptionexemption is accepted.check:empty-changeset: one declaring changeset added.check:nul-bytes: OK.check:test-source-alias: OK.check:cross-package-test-inputs: OK.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.pnpm lintand 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.mdis aminorbump of@objectstack/service-analytics, BREAKING as an accept-set narrowing under the launch-window convention. It records FROM and TO and the ADR-0087 dispositionnot-required (no-migration-prescription): no metadata moves, and the remedy is to point the definition at an exposed object or remove it.Acceptance notes
init()still registers and lists. This covers a later capability provider's objects, astart()registration, akernel:readyinstall, 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 atkernel: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.init(), and the query door logs the same failure aterrorwhen a query meets it.CubeRegistry.registerAllstops at the first refused cube, and the cubes before it stay registered. This is documented on the method. The service no longer usesregisterAll, and registers config cubes one at a time.packages/lintrule in the style ofvalidate-nav-object-servability, refusing ananalyticsCubesentry over an unexposed object atos validate, would catch the pair before boot and inside the window. The claim fencespackages/lintoff and no producer is measured, so it is noted here and not filed.apiEnabled: falseobject registers silently and lists inGET /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:servicesseat 1 (session_013j5gkUCpqQiti4GgPqqmnt) at 2026-10-10T11:54Z. Line 2 now readsClause-②: yes (narrowing), following the contract review ofdcc38d4413(6097218872, ①.5 and ②).CubeRegistryis 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.minor.Seat's append: patch round 1 (head
3a646f5ffb)Appended by
domain:servicesseat 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:producerquotes matchserve.tsand the per-cube loop inanalytics-service.tsat this head;verifiedAt: 2026-10-10;registerAllis gone from the ledger;Clause-②: yes (narrowing).Round 1 (patch)
Head:
3a646f5ffb. That is393b179be6(re-anchor), thendabf17b4b8(contract-review addendum), then a clean merge oforigin/main243dd3c625with no regeneration debt.Cause.
Test Core (1/6)atdcc38d4413failed in@objectstack/spec's liveness gate. Sixteen rows inpackages/spec/liveness/analytics_cube.jsonanchored their producer atpackages/services/service-analytics/src/analytics-service.ts#registerAll. This PR replaced the constructor'sregisterAllcall, so the anchored symbol left the file. Reproduced ondcc38d4413before 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 intoservice-analyticssource. That blind spot is filed as #22680.Re-anchored symbol:
analytics-service.ts#register. The constructor now registers configured cubes throughthis.cubeRegistry.register(cube), one call per cube.registeris the symbol that call uses, the direct successor ofregisterAllin the same position. Nothing inanalytics-service.tschanged 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_note1,producer16 andverifiedAt16.verifiedAt: 2026-10-10on the 16 rows. Their shared producer string now quotes the code at this head. This covers theserve.tsfallbackresolveStackCollection(config, 'analyticsCubes'), which had been stale since [finding] cli(serve):os serveresolves capability providers from a multi-package artifact's top-levelrequiresonly — a package'srequires: ['automation']is not loaded at boot #22288 and was a?? []quote, and the constructor's per-cuberegisterbehind the exposure admission. AverifiedAtstamp attests the whole producer, so a known-stale quote could not stay under it._note. It now reads "registered one cube at a time byregister".Clause-②: yes (narrowing).CubeRegistryis re-exported from the package entry, and its optionaladmitparameter enlarges the published surface. The bump is stillminor, and the ADR-0087 marker still discloses the additive parameter.check-adr-0087-registration --base origin/mainexits 0 ([BREAKING+bang+clause-②-narrowing],not-required (no-migration-prescription)), andcheck-changeset-no-major --base origin/mainexits 0.Gate exits at
3a646f5ffb. All runs went through the verify lock with slotissue-22663, a 3 GB heap, turbo--concurrency=1and vitest--maxWorkers=2.check-liveness.test.ts: 71/71, exit 0.evidence.test.ts: 42/42, exit 0.check:liveness: exit 0.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.turbo run build --filter=!@objectstack/docsbuilt 72/72 (71 cached) for the dist-reading gate.dispatch-gates --commands --repo objectstack-ai/objectstackderived 72 commands, 7 of them new spec/repo families (includingcheck:liveness). All 72 ran, pluscheck:startup-registry-verdict,check:durability-log-level,check:adr-anchorsand the speccheck:error-code-provenance. 75 exited 0 and 1 exited 1.--ranreports 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 nodistin 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-checklistexit 1, main-side. It reports 7 problems, all ABSENT SYMBOL anchors. Five are inareas/access-security.json, againstmetadata-protocol/src/protocol.ts(anonymousFormIntakeOrgScopeRefusal,anonymousFormIntakeReopenRefusal,envWideRawViewRows). Two are inareas/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 cleanorigin/main243dd3c625tree exits 1 with the identical 7 problems.not_plannedwith 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 redTypeScript Type CheckandTest Coreevents atdabf17b4b8and393b179be6were cancellations by the next push, not verdicts.Seat's append: patch round 2 (head
0dd4426835)Appended by
domain:servicesseat 1 (session_013j5gkUCpqQiti4GgPqqmnt) at 2026-10-10T13:36Z, from the dev's round-2 report on #22663. The seat folded flag ③.4 of6097791315into 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:
3a646f5ffbis one file,analytics_cube.json, with 17 lines in and 17 out.joinTableis gone from the ledger.packages/core/src/capability-providers.ts#CAPABILITY_PROVIDERS.const joined = joins?.[alias]?.name;inhop-object.ts,const CAPABILITY_PROVIDERS = Serve.CAPABILITY_PROVIDERSandspec.configKey === 'analyticsCubes'inserve.ts, andobject: hop.objectinnative-sql-strategy.ts.Round 2 (ledger)
Head:
0dd4426835(one commit on3a646f5ffb).origin/mainis still243dd3c625, which round 1 merged, so no merge was owed. Onlypackages/spec/liveness/analytics_cube.jsonchanged, and only values. The contract review of3a646f5ffb(6097791315, flag ③.4) named two pre-existing stale leaves sitting under the round-1verifiedAt: 2026-10-10stamp.1.
joins.children.name, firstevidencecitation. It quotedconst joinTable = cube?.joins?.[alias]?.name ?? aliasatnative-sql-strategy.ts#qualifyAndRegisterJoin.joinTablehas 0 occurrences in that file at head. The citation is re-sited atpackages/services/service-analytics/src/hop-object.ts#resolvePathHops, quotingconst joined = joins?.[alias]?.name;verbatim (tier 1 of the one hop resolver). It keepsnative-sql-strategy.ts#qualifyAndRegisterJoinas the emitter of the LEFT JOIN against that object, quotingobject: hop.object, which is verbatim at head. The row's other three citations are unchanged.2. The 16
producerstrings.configKeydeclaration clause is re-sited atpackages/core/src/capability-providers.ts#CAPABILITY_PROVIDERS, which declaresconfigKey: 'analyticsCubes'on theanalyticsentry.serve.tsthreading quote stays where it lives, atpackages/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 existingconst cubes = … ; arg = { cubes }quote.Fence proof. The JSON walk against
origin/mainfinds the same 91 key paths and 0 status changes. Changed leaves by field:_note1,verifiedAt16,producer16,evidence1. Against the round-1 head, onlyproducer16 andevidence1 moved.Gates at
0dd4426835. All runs went through the verify lock with slotissue-22663, a 3 GB heap, turbo--concurrency=1and 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.test:repo: 55 files, 971 passed, exit 0.turbo run build --filter=!@objectstack/docs: 72/72 tasks, exit 0.dispatch-gates --commands --repo objectstack-ai/objectstackderived 72 commands, the same set as round 1. All 72 ran, plus 4 implicated ones. 75 exited 0 and 1 exited 1.--ranreports 72 derived, 72 run, 0 NOT-MEASURED and 0 UNRUN, a derived zero.check:platform-checkliststill exits 1 with the identical 7 main-side ABSENT SYMBOL problems that round 1's cleanorigin/main243dd3c625control showed. None of them is in this diff.Generated by Claude Code