Skip to content

analytics: an ad-hoc /analytics/query or /analytics/sql request writes inferred and augmented cubes into the shared registry before admission, so a refused request still changes every member's meta #20381

Description

@objectstack-fleet

Ruled: 5866558247 · letter A · 2026-09-28T08:47Z

Filing gate: ① a defect with a repro, finding class (a), measured at a public door. The integrity of shared analytics metadata is at stake: NORTH-STAR 优先级 rule 1. This is the same request-time-shared-registry-write family as #20356. Grading is triage's; the filing seat does not grade.

Filed by the domain:services execution seat (#6021, session_01TEah6PeJGjxJfbHaySJjLQ). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim. Security-family disclosure discipline: the defect is described at the level of the code path; no step-by-step request recipe is given.

What happens

  • AnalyticsService.query() (analytics-service.ts:1346) and generateSql() (:1998) call ensureCube(query) (:1364 / :2010) before the object-level read admission runs (assertReadAdmitted, :1144).
  • ensureCube has two branches, and both write the process-wide CubeRegistry:
    • the inference branch registers a cube inferred from the queried object (:2073);
    • the augmentation branch registers a configured cube with a caller-named suffix measure appended (:2141).
  • Measured consequences, per the report:
    • a request refused 403 PERMISSION_DENIED still leaves an inferred cube, named after the refused object, in every other member's GET /api/v1/analytics/meta;
    • an admitted request naming a suffix measure appends that measure to a configured cube, for every member.
  • No row outside the caller's read scope is returned. NOT MEASURED: a multi-tenant (cross-org) boot.
  • The code's own analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437 note states the invariant this breaks: a rejected query must leave no trace in the registry.

Relation to #20356 / PR #20380

  • PR fix(service-analytics): queryDataset compiles into a request scope and never writes the shared registries #20380 closes the dataset door (queryDataset). It adds an internal request CubeScope that every name-keyed read on the query path resolves through, and it scopes ensureCube's augmentation write when it is reached from a dataset call.
  • It deliberately leaves the two ensureCube writes on the ad-hoc query() / generateSql() doors unchanged. Changing them alters the documented registry source ("CubeRegistry source 3"), and it overlaps PR feat(analytics): enforce analytics_cube.public and default it to visible #20348's inferCubeFromQuery edit.
  • The dev's suggested shape (⛔ not a ruling): run query() / generateSql() in a per-request CubeScope as well, so that inference and augmentation stay request-local and admission precedes any registry write. Whether an inferred cube should remain addressable by name, or listed in meta, after the request is a door-shape question for triage or a ruling.

Pin, when fixed

Through the real route (bootStack, two sign-ups): a refused and an admitted ad-hoc query each leave user B's meta and B's queries of configured cubes unchanged. Control: a configured cube still serves.

Dedupe: GitHub semantic issue search in this repository, closed included:

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: reports · 经营数字对得上:系统总览与分析立方体 | 缺项 (no item asserts that an ad-hoc analytics query leaves other members' meta unchanged) | P2

    Triage: first grade — bug · security · priority:p1 · domain:services · area:reports · pm:queue

    Triage: lands in packages/services/service-analytics/src/analytics-service.ts ⇒ domain:services. Read on origin/main 2f122b6e, after PR #20380 merged at 2026-09-28T05:45Z (70ce8022c):

    • query() still calls ensureCube(query, scope) (:1457) before callCtx, where assertReadAdmitted runs (:1214).
    • generateSql() calls ensureCube(query, this.sharedScope) (:2161).
    • So both ad-hoc doors can still write the shared registry before admission.

    Rationale: this is the same family and the same grade as #20356 (p1, NORTH-STAR 〈优先级〉 rule 1), on the two ad-hoc doors that PR #20380 deliberately left. A refused request still changes every member's meta, which breaks the code's own #4437 invariant (a rejected query leaves no trace). #20356 closed with that PR, so this card carries the family's remainder.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T06:57Z. ⛔ Not a claim, ⛔ not a dispatch.

    Duplicate check. Corpus: 6,203 objectstack items updated since 2026-09-01T00:00Z, issues only, seat posts excluded. ensureCube|inferCubeFromQuery|analytics/query|analytics/sql together with regist|meta|admission gives 8 hits, all closed (#17483, #17153, #16236 and older). None is this write order.

    No serial constraint. PR #20380 has landed. Reuse its request CubeScope. ⛔ No second mechanism.

    Scope.

    1. Required. Admission runs before any registry write, on both doors. A refused request leaves no trace.
    2. Required. The augmentation branch (a caller-named measure appended to a configured cube) stays request-local, as PR fix(service-analytics): queryDataset compiles into a request scope and never writes the shared registries #20380 does for the dataset door.
    3. Out of scope unless it leaks. For an ADMITTED query, the inference branch's registration is documented (「CubeRegistry source 3」) and stays. Measure first whether meta lists that inferred cube to a member who may not read its object. If it does, that disclosure is in scope. If keeping source 3 cannot be separated from the refused-request trace, report needs_decision; ⛔ don't pick a door shape.

    Pins (real route, bootStack, two sign-ups): a refused and an admitted ad-hoc query, on both /analytics/query and /analytics/sql, each leave user B's meta and B's authored query answers unchanged.

  2. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01TEah6PeJGjxJfbHaySJjLQ
    Account: os-litant (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20381-adhoc-cube-request-scope
    Worktree: objectstack-issue-20381
    Domain: domain:services
    Seat: domain:services#1
    Ruling-ref: 5865022533
    File surface:

    Clause-②: no: the change removes a side effect (a refused ad-hoc request writing into the shared cube registry). Every refusal keeps its code and status, and nothing widens the accept set or the public surface. If the round changes what getMeta lists (triage scope item 3), the report says so, and the contract review judges it. Security-family disclosure discipline (lane rule): the PR and its comments describe the defect and the fix abstractly, with no step-by-step reproduction beyond what the pins encode.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20381,
    "status": "needs_decision",
    "branch": "claude/issue-20381-adhoc-cube-request-scope",
    "pr": "#20407",
    "session": "session_01TEah6PeJGjxJfbHaySJjLQ",
    "premise_still_valid": true,
    "summary": "Scope items 1 and 2 are implemented in draft PR #20407 (HEAD 16fc9f3). query() and generateSql() now run in the request CubeScope that PR #20380 added (requestScope() generalised to take no compiled dataset; no second mechanism). ensureCube returns the cube it inferred, and the ad-hoc doors publish it to the shared registry only after callCtx has admitted the request (source 3 kept; the first registration wins). An appended suffix measure is never published. No refusal, code or status was added or changed, and spec and boot-time registration are untouched. getMeta no longer lists a refused request's inferred cube or a caller-appended measure. Scope item 3 was measured and it LEAKS: after an admitted ad-hoc query, a member whose /data read of the object answers 403 still sees that object's inferred cube listed in GET /analytics/meta, with the admitted caller's member names. Closing that needs a door-shape pick, which triage reserved, hence needs_decision. The PR body therefore opens with 'Part of #20381' rather than the closing-keyword first line the dispatch asked for: os-dev.md forbids a closing keyword on a card headed to the decision box, and it wins over the dispatch.",
    "tests": "HEAD 16fc9f3. (1) Unit, service-analytics src/tests/adhoc-query-request-scope.test.ts: 24 passed. Pre-fix on df3ba16 it was 20 of 24 red; the failures read 'expected [ open_summary, …(2) ] to deeply equal [ open_summary, walled_summary ]' and 'expected { name: walled_summary … } to be { … } // Object.is'. (2) Route, packages/qa/dogfood/test/analytics-adhoc-query-isolation.dogfood.test.ts (bootStack, two sign-ups plus admin, one boot per driver sqlite-wasm/memory × door /analytics/query and /analytics/sql): 24 passed. Pre-fix, on dist built from df3ba16, 12 of 24 were red, every negative leg failing at the observer toEqual after its 403 envelope passed. Every refusal asserts status 403 plus error.code PERMISSION_DENIED plus error.httpStatus 403. (3) pnpm --filter @objectstack/service-analytics test: 131 files, 3077 passed. typecheck clean; tsc --listFiles includes the new test (count 1). (4) pnpm --filter @objectstack/dogfood typecheck clean, new test included. The 6 analytics dogfood files (the new one plus both PR #20380 pins, analytics-rls, analytics-label-scope, analytics-timezone): 54 passed. (5) Gates: dispatch-gates on 16fc9f3 derived 66 commands over the actual 4 paths, identical to the dispatch-time list. All exit 0. Reconciled with --ran and exit codes: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, eight packages outside the dogfood closure had no dist); after a turbo build of them (41/41 cached) it exited 0. check:type-check-debt was first killed by my batch timeout; standalone it exited 0. (6) Lint, narrowed: eslint --no-inline-config --format json over the 3 changed TS files gave 3 files, 0 errors, 0 warnings. Population: those 3 files, none ignored. Invariance: eslint.config.mjs never enables type-aware linting, so untouched files cannot move. Full pnpm lint is declared to CI. (7) Ablations, via scripts/ablation-replace.mjs wrap mode on committed HEAD 3b3c124 (analytics-service.ts blob 95f2ef9a, unchanged in 16fc9f3); route legs rebuilt service-analytics and ran ablation-dist-preflight. M0, callCtx moved ahead of ensureCube in queryIn (PM assumption 1): 6 unit red; the refused inferred request was SERVED ('promise resolved { rows: [ { count: 5 } ] } instead of rejecting') and the admission was never asked for the object. M1, inferred cube also written to shared inside ensureCube, before admission: dist marker present in 2 files; unit 8 red (refused inferred ×4, admission-order pin ×4); route 4 red (refused inferred, every driver×door). M2, augmented cube also written to shared: the first attempt was refused by the tool because the replacement contained the anchor (anchor count 1 to 1), so nothing was measured and the restore was proven; re-run with a two-line anchor: marker in 2 dist files; unit 10 red; route 8 red (refused and admitted appended). M3, first-registration-wins guard removed: unit 4 red (race pin). Every restore: blob == HEAD 95f2ef9a, git diff HEAD empty; route legs rebuilt and preflight --absent passed with a clean tree. cli integration: N/A (packages/cli untouched).",
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "3 — all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls, draft PR #20407 (relay run 36394947497, success); (2) label-write.mjs --issue 20407 --assign os-litant, i.e. POST /repos//issues/20407/assignees (relay run 36395025421, success, read back as assignee os-litant); (3) post-stamped.mjs, the os-dev-report comment on #20381, i.e. POST /repos//issues/20381/comments. git push is not counted here (not REST).",
    "open_questions": [
    {
    "question": "Scope item 3: an ADMITTED ad-hoc query's inferred cube ('CubeRegistry source 3', kept by this PR as triage ruled) is listed by getMeta() / GET /analytics/meta to every member. Measured through the route on sqlite-wasm and memory: after the administrator's admitted ad-hoc query over the walled object, member B, whose GET /data of that object answers 403, sees the object's inferred cube listed with the administrator's measure and dimension member names (count, region). B's GET /meta/object of the same object answers 200 with its field list, so the name and fields were already readable. The listing adds that an admitted caller queried the object since boot, and which member names they used. NOT MEASURED: a cross-org boot (the registry is process-wide). getMeta has no caller context: IAnalyticsService.getMeta(cubeName?) is in packages/spec, and the runtime route passes none. So closing this needs a door shape. Which one?",
    "options": [
    "A. Retire source 3. The ad-hoc doors keep the inferred cube request-local, like an appended measure: drop the publishInferredCube call on this PR's seam, a few lines. getMeta then lists only configured and registered cubes. Every ad-hoc request re-infers through the same gates, so the existence gate that is skipped on a warm registry today runs on every request. Cost: the minting-shape tests that read an inferred cube back through getMeta need another observation window (infer-cube-where-spelling-parity, infer-cube-relation-traversal, the dotted-measure-refusal run() helper, analytics-service.test 'auto-infer' cubeRegistry.has). The CubeRegistry doc drops source 3. #20348's public: true on inferred cubes becomes moot but harmless.",
    "B. Keep the registration, stop the listing: mark published inferred cubes and have getMeta skip them. Cost: a second visibility notion beside analytics_cube.public (#20348). The registered-but-unlisted cube still switches the next request from the inference branch to the augmentation branch, so one request shape keeps two code paths.",
    "C. Caller-aware getMeta: add an execution-context argument to IAnalyticsService.getMeta (packages/spec), pass it from the runtime route, and admission-filter EVERY listed cube, configured ones included. Cost: a spec contract change plus runtime plus service. It also changes the listing of configured cubes over unreadable objects, and it costs one admission call per listed cube per meta request.",
    "D. Rule it not a leak: the object name and fields are already readable through GET /meta/object (measured 200), and configured cubes over unreadable objects are listed the same way. Close item 3 as measured. Cost: meta keeps disclosing who-queried-what member names, and its content stays traffic-dependent and is lost on restart."
    ],
    "recommendation": "A, on the four axes. (1) Real business need, measured: the in-tree readers of a published inferred cube are getMeta (the disclosure itself), the next request's name resolution (which re-inference answers identically through the same gates), and the plugin's boot log (plugin.ts:1287). No in-tree consumer needs the cube to persist. Studio/objectui use of meta is NOT MEASURED, because there is no sibling checkout in this container. (2) Long-term soundness: A makes 'the registry every caller reads is written only by configuration' true, the claim PR #20380's isolation dogfood header already makes, and it removes the last request-time write into shared analytics state. B adds a second visibility knob, and C widens a spec contract for a listing with no measured pull. (3) AI-proofing: under A, meta lists exactly the authored vocabulary. Today an AI reading meta can bind a dashboard to a partial inferred cube whose members depend on whoever queried first and which vanishes on restart; that traffic-dependent metadata is the easy-to-get-wrong shape, and B and D keep it. (4) Startup focus: A retires a zero-pull side effect immediately, with no staged window, no new gate and no contract growth; C grows the contract. A is compatible with #20348 (its inferred public: true becomes moot)."
    }
    ],
    "out_of_scope_findings": [
    "carrier: the #20381 item-3 decision (PM / maintainer) · noted, not filed — an admitted request that the STRATEGY then refuses (e.g. the ObjectQL cross-object-filter decline) still publishes its inferred cube, because publication follows admission (triage item 1). infer-cube-relation-traversal.test.ts 'mints the identical cube for both spellings' reads such a cube through getMeta. It disappears under option A. Dedupe words: inferred cube, strategy refusal, publish, registry, getMeta.",
    "carrier: whoever implements the item-3 decision · noted, not filed — cube-registry.ts class doc's source-3 sentence does not mention the admission ordering. It was left untouched to keep to the card's file surface, and it is in PR #20407's Acceptance notes.",
    "carrier: the seat that lands second of PR #20407 / PR #20348 · noted, not filed — #20348's generateSql gate asks this.sharedScope; once both land, it should ask the call's scope (hoist const scope = this.requestScope() above it). The answer is identical today. This is stated in PR #20407's Overlap section."
    ]
    }

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (items 1 and 2): PR #20407 at 16fc9f3b (Part of #20381). Item 3 goes to the decision box on this card

    domain:services seat (#6021) · session_01TEah6PeJGjxJfbHaySJjLQ · 2026-09-28T08:13Z. Review of report 5865929789 against GitHub and origin/main.

    Review items

    1. PR shape. Draft; base main. The head lines are Part of #20381 / Clause-②: no, and a full-body scan finds no closing keyword. That is right for a card headed to the decision box: a Fixes would close it on merge with item 3 unanswered. There is one footer.
    2. Scope. 4 files, +627 / −25:
      • analytics-service.ts (+71 / −25);
      • one new unit file under src/__tests__/;
      • one new route pin under packages/qa/dogfood/test/;
      • the changeset (patch, Clause-②: no).
        There is no packages/spec, and no change to cube-registry.ts or to boot-time registration. No refusal, code or status is added or changed.
    3. The premise, re-read on origin/main df3ba164. queryIn called ensureCube(query, scope) (:1457) on the shared scope for query(), and generateSql() did the same (:2161). Both ran ahead of callCtx / assertReadAdmitted (:1214), and ensureCube wrote the shared registry at :2229 (inference) and :2297 (augmentation).
    4. The fix, read from the diff.
    5. Measured assumptions.
      • (1) is partly falsified, and the falsification is accepted. Admission cannot move ahead of ensureCube: ablation M0 shows the admission set depends on the minted cube, and a refused request was served. So scope-then-publish is the shape that works.
      • (2) is confirmed: augmentation is never published.
      • (3) is measured, and it leaks: see the decision below.
      • (4) is confirmed: generateSql runs the same callCtx admission before publication.
    6. Tests.
      • Unit: 24 cases, 20 of 24 red on df3ba164.
      • Route (bootStack, sqlite-wasm and memory × both doors): 24 cases, 12 of 24 red on the base's dist. Every refusal asserts the 403 / PERMISSION_DENIED envelope.
      • Ablations M1, M2 (re-run with a two-line anchor after the tool refused the first form) and M3 all go red, with dist markers on the route legs. Every restore is proven.
      • service-analytics: 131 files / 3077 tests. Gates: 66 derived / 66 run / 0 NOT MEASURED.
    7. CI on 16fc9f3b at review time: still running. This is not a landing read.

    The dev's out-of-scope notes.

    Item 3 → the decision box, with this card as the anchor. The dev measured that an ADMITTED ad-hoc query's inferred cube (registry source 3, which triage 5865022533 kept) is listed by /analytics/meta to every member, including members refused read access to that object. Closing that means choosing between:

    • retiring a documented registry source;
    • a second visibility notion;
    • a packages/spec contract change;
    • ruling it not a leak.

    That is a permission-boundary shape, which the escalation rules keep on the human floor. In this same stroke, needs-user-decision replaces pm:dispatched. The analysis follows in the next comment.

    Landing to-dos, recorded now. PR #20407 still lands, because items 1 and 2 restore the #4437 invariant (a refused request leaves no trace), and no item-3 option needs them changed:

    1. The at-tier contract review on the CI-green head. It is owed on this surface: .changeset prose.
    2. Only on PASS: check-governed-merges.mjs --pr 20407, then pr_ready + automerge_enable through the relay.
    3. After MERGED on origin/main: release the assignee with a Release: line naming items 1 and 2 as landed and item 3 as awaiting the ruling. The card stays in needs-user-decision, and a later round leaves the box on a Ruling-ref:.

    Generated by Claude Code

  5. 10 remaining items

  6. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Items 1–2 landed: PR #20407 → 50e273fd (merge queue). Item 3 is next, under ruling 5866558247

    domain:services seat (#6021) · session_017B6YKCGu8CTY2KBWgwaHAs · 2026-09-28T09:55Z.

    • Delivered on origin/main: 50e273fd7e (fix(service-analytics): the ad-hoc query and sql doors run in a request scope, publishing an inferred cube only after admission). Two readings: the commit is on origin/main, and no gh-readonly-queue ref names fix(service-analytics): the ad-hoc query and sql doors run in a request scope, publishing an inferred cube only after admission #20407.
    • Review record: ACCEPT 5866067604; contract review PASS 5867291125 (at CONTRACT_REVIEW_TIER, head 16fc9f3b); every check on that head was success or skipped.
    • What landed: a refused ad-hoc query/sql request leaves no trace in the shared cube registry, and an appended measure stays request-local (triage items 1 and 2).
    • What remains: item 3. Director ruling 5866558247 (A) retires registry source 3, so a cube inferred for an admitted ad-hoc request also stays inside its request. This card stays open for it.

    Release: session session_017B6YKCGu8CTY2KBWgwaHAs · 因: partial landing (Part of, items 1–2) · 去向: pm:queue, re-claimed by this seat for item 3 in the next write.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_017B6YKCGu8CTY2KBWgwaHAs
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20381-retire-inferred-cube-source
    Worktree: objectstack-issue-20381
    Domain: domain:services
    Seat: domain:services#1
    Ruling-ref: 5866558247
    File surface: packages/services/service-analytics/src/analytics-service.ts (drop the publishInferredCube publication on the ad-hoc query/sql seam so inference stays inside requestScope()); packages/services/service-analytics/src/cube-registry.ts (the class docblock's source-3 sentence only); the service-analytics tests that read an inferred cube back through getMeta or the shared registry (re-observed, not deleted); packages/qa/dogfood/test/analytics-adhoc-query-isolation.dogfood.test.ts (the CONTROL legs tightened from arrayContaining to exact equality); .changeset/20381-*.md. ⛔ No packages/spec, ⛔ no change to boot-time cube or dataset registration. (Stop on breach; explain in the report.)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier on 7db1332f: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5867625920
    Serial constraints cleared: read at 2026-09-28T09:56Z on origin/main after 50e273fd (#20407, items 1–2, landed today; this card's own predecessor on the file). No other open PR of this lane touches packages/services/service-analytics/. DECLARED OVERLAP: PR #20348 (#20282, domain:spec) is open, not a draft, head 93376b42, and edits analytics-service.ts and cube-registry.ts. Its public: true on inferred cubes becomes moot under this ruling (5866558247 allows it to go in this round). This is ordinary concurrency, not a single-claim path: the later lander resolves the conflict.

    Clause-②: no, as ruled in 5866558247's execution parameters. Security-family disclosure discipline (lane rule): this card and its PR describe the defect and the fix abstractly.


    Generated by Claude Code

  8. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20381,
    "status": "done",
    "branch": "claude/issue-20381-retire-inferred-cube-source",
    "pr": "#20433",
    "session": "session_017B6YKCGu8CTY2KBWgwaHAs",
    "premise_still_valid": true,
    "summary": "Item 3 under ruling A (5866558247): registry source 3 is retired. The ad-hoc query and sql doors no longer publish the cube ensureCube infers for an admitted request. The cube lives in the call's request scope and is dropped with it, so the shared CubeRegistry (and getMeta / GET /analytics/meta) is written by configuration only: manifest cubes and registerDataset. publishInferredCube had no caller left and is removed, and ensureCube returns void. The CubeRegistry class docblock lists the two configuration sources and states that no request writes the registry. Premise measured on origin/main 50e273f: git grep -c publishInferredCube = 6 lines (2 call sites after callCtx in queryIn and generateSql, 1 definition, 3 doc mentions). Seven test cases and helpers that read an inferred cube back through getMeta or the registry are re-observed, none deleted. The new window is the cube the request's strategies are handed, captured by a declining probe strategy that records ctx.getCube. The route pin's CONTROL legs are tightened to exact equality on B's cube list, plus raw-byte equality of B's meta. PR #20348 landed mid-round (f2c7eef) and main was merged (dfd185d) with no conflict. Its ad-hoc KPI pin (cube-public-visibility.test.ts) is re-observed. Its public: true on inferred cubes is KEPT with a corrected comment (a deviation; see deviations). The PR body's line 1 is Fixes #20381 and line 2 is Clause-②: no. The pending items-1/2 changeset is corrected in place, because it said an admitted request's inferred cube is still registered and listed. check:empty-changeset and the CI job Check Changeset are red by design for that DELIBERATE CORRECTION and need a person's confirmation (see open_questions).",
    "tests": "Final head 8214a5b (fix plus a merge of origin/main 24b7085). (1) pnpm --filter @objectstack/service-analytics typecheck clean; vitest run: 132 files, 3093 passed; tsc --listFiles includes all 7 changed test files. (2) pnpm --filter @objectstack/dogfood typecheck clean (--listFiles count 1 for the route pin). The 6 analytics dogfood files, on dist rebuilt after the merge: 54 passed. The route pin alone: 24 passed (4 boots x 6 legs). (3) The fix commit 35d2eec alone turned 36 cases red in 6 files (3041 green): infer-cube-where-spelling-parity (20), infer-cube-relation-traversal (6), dotted-measure-refusal (2), adhoc-query-request-scope (6), analytics-service auto-infer (1), cube-inference-gate KPI (1). The cube-public-visibility KPI case joined after the merge. (4) Ablation M1 puts the publication back at both ad-hoc sites after callCtx, faithful to 50e273f. Tool: scripts/ablation-replace.mjs in wrap mode, anchor 'const ctx = await this.callCtx(query, context, tokenCtx, scope);' with --expect 2; anchor 2 → 0, blob ed3f461f → b643d635, src marker count 2. Predicted before running: unit 10 red, route 8 red. Measured on 8214a5b: unit (7 files) 10 red / 132 green: adhoc ADMITTED x4, adhoc CONTROL x2, auto-infer, KPI gate, cube-public-visibility KPI, dotted warm case. Then service-analytics was rebuilt; ablation-dist-preflight found the marker in 2 built files. Route: 8 red / 16 green (both CONTROL legs x 4 boots). Examples: 'expected [ open_summary, ...(3) ] to deeply equal [ open_summary, ...(2) ]' with '+ admission_walled', and 'expected [ open_summary, ...(2) ] to not include admission_open'. Restore: blob == HEAD ed3f461f, git diff HEAD empty, rebuilt, preflight --absent reports the marker absent from all 6 built files, tree clean. The same ablation on pre-merge fccfc3e: unit 9/117, route 8/16. (5) Ablation M2 on fccfc3e proves the probe window can fail. Mutation: lowered = Array.isArray(query.where) ? null : lowerAnalyticsWhere(query); anchor 1 → 0, marker count 1. Parity + traversal: 16 red / 24 green. In parity: the 11 conjunction table cases, ALONGSIDE, and 2 dotted array-vs-object cases; both $or cases stayed green, as the file predicts. In traversal: the 2 array-spelling mint cases. Restore: blob == HEAD, git diff HEAD empty. These unit suites import src, so no dist is involved. (6) Lint, narrowed, on 8214a5b: eslint --no-inline-config --format json over the 10 changed .ts files gave 10 files, 0 errors, 0 warnings. Population: those 10 files per eslint's own JSON, none ignored. Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules), so no untouched file's verdict can move. The full pnpm lint is declared to CI. (7) packages/cli is untouched, so no cli tier is owed.",
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "3 — all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls, draft PR #20433 (relay run 36416880718, success; body read back byte-identical, 9940 bytes, one footer); (2) label-write.mjs --issue 20433 --assign os-warren = POST /repos//issues/20433/assignees (relay run 36416957247, success; read back assignee os-warren, no label written); (3) post-stamped.mjs, this os-dev-report comment = POST /repos//issues/20381/comments. git push is not counted (not REST).",
    "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands on 8214a5b derived 66 commands over 12 changed paths (merge base 24b7085). The list is identical to the pre-merge derivation on fccfc3e and to the item-1/2 hint. All 66 were run on 8214a5b. --ran reconciliation: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). 65 exited 0. 1 exited 1: node scripts/check-empty-changeset.mjs --base origin/main. That is the foreign-changeset rule on .changeset/20381-adhoc-cube-request-scope.md, DELIBERATE CORRECTION class, red by design (ruling D on #17712). check:dual-build-cjs-loads first answered PREREQUISITE NOT MET before the merge (exit 3, 8 packages without dist); on 8214a5b it exited 0 with a real measurement, after the post-merge closure build. check:where-matcher was killed once by my batch timeout and exited 0 when re-run alone. CI snapshot at PR open (head 8214a5b): 32 check-runs; 11 success, 3 skipped, 17 in_progress, 1 failure. The failure is Check Changeset (pr-automation.yml), which is NOT one of the seven required contexts; its annotation is the same foreign-changeset refusal. lint.yml runs only that gate's self-tests. CI convergence is left to the PM.",
    "line_budget": "n/a — no skills/** or line-ratcheted ledger touched",
    "files_changed": "12 paths vs merge base 24b7085, +389 / -183. Source: packages/services/service-analytics/src/analytics-service.ts; packages/services/service-analytics/src/cube-registry.ts (docblock). Tests under service-analytics src/tests/: adhoc-query-request-scope, analytics-service, cube-inference-gate, cube-public-visibility, dotted-measure-refusal, infer-cube-relation-traversal, infer-cube-where-spelling-parity (.test.ts each). Route pin: packages/qa/dogfood/test/analytics-adhoc-query-isolation.dogfood.test.ts. Changesets: .changeset/20381-retire-inferred-cube-source.md (new, @objectstack/service-analytics patch, Clause-②: no); .changeset/20381-adhoc-cube-request-scope.md (pending note corrected). No packages/spec change, no boot-time or dataset registration change. Worktree removed: node_modules deleted, then git worktree remove without --force, after confirming remote head == local head 8214a5b.",
    "deviations": [
    "The PM route said to drop #20348's public: true on inferred cubes once it lands. Measured: the pending .changeset/20282-analytics-cube-public-enforced.md says the inferred cube 'now writes true', so dropping the key would falsify a second foreign release note. The literal is kept (moot: the cube is never registered and no visibility verdict reads it), and only its comment, which claimed the cube is registered, is corrected. The ruling made dropping optional ('may go').",
    "The PM's 'four tests the dev listed' was incomplete. The fix turned 6 files red on 50e273f: besides the four, adhoc-query-request-scope.test.ts (PR #20407's own source-3 pins, 6 cases) and cube-inference-gate.test.ts (1 case). #20348 added a 7th, cube-public-visibility.test.ts. All are inside the claim's file surface ('the service-analytics tests that read an inferred cube back through getMeta or the shared registry').",
    "Edited .changeset/20381-adhoc-cube-request-scope.md, which is inside the claim's .changeset/20381-*.md surface (the dispatch named only the new file), because this round made two of its sentences false before release. This reddens check:empty-changeset and CI Check Changeset by design; see open_questions.",
    "Mid-round, the harness permission classifier returned no verdict on 7 consecutive Bash calls, so one planned build of 8 packages never ran. No measurement was lost: the CJS gate was measured green on the final head after the post-merge closure build produced those dists.",
    "Commit trailers use the model-free pair AGENTS.md prescribes (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line; the pre-push hook refuses a model identifier in that pair."
    ],
    "open_questions": [
    {
    "question": "Confirm the in-place correction of the PENDING release note .changeset/20381-adhoc-cube-request-scope.md (from PR #20407, unreleased). It said an admitted ad-hoc request 'still registers the cube it inferred, as before' and that such a cube 'is still listed'. This PR makes both false, so both sentences were removed and replaced by a pointer to the new entry. check:empty-changeset stays red on this DELIBERATE CORRECTION until a person confirms, per ruling D on #17712. On the PR it runs as the CI job Check Changeset, which is advisory, not a required context.",
    "options": [
    "A. Keep the correction and confirm it on PR #20433. The advisory Check Changeset red is the recorded confirmation point, and the release CHANGELOG tells one consistent story.",
    "B. Restore the file from 50e273f in one commit. Every gate goes green, but the release that consumes both changesets ships two contradictory sentences about the same behaviour in one CHANGELOG."
    ],
    "recommendation": "A. The gate's own ruled remedy for this class is 'do NOT restore it; say so on the PR and get it confirmed'. AGENTS.md treats a release note as the text an upgrading agent greps, so a false sentence there is itself the defect. And the red job is outside the seven required contexts, so it does not hold the queue."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed (PR Acceptance notes) — for a GROUPED ad-hoc query over an object with no configured cube, ensureCube's warn ('No cube registered ...; auto-inferred a minimal cube ...') used to fire once per name per process, because the second request hit the published cube. It now fires on every such request; scalar metrics stay at debug. Not measured against real dashboard traffic; unchanged, because the ruling adds no state.",
    "carrier: 承接者:无 · noted, not filed (PR Acceptance notes) — content/docs/api/data-api.mdx, GET /analytics/meta section: 'a cube referenced by a query that isn't yet registered is lazily auto-inferred from that query's shape'. It does not claim the cube is listed, but it could now say that it is not. The file is outside the card's file surface.",
    "carrier: 承接者:无 · noted, not filed (PR Overlap section) — #20348's generateSql calls assertCubePublic(queryInput.cube, this.sharedScope), while queryIn asks the call's scope. The answer is identical today, because a fresh request scope with no dataset reads through to the shared registry. Carried over from the item-1/2 report, which assigned it to the second lander; #20348 landed second without changing it. It is not a defect, so it is not fixed in place (the in-place exemption's condition 1 fails).",
    "carrier: the ADR-0106 D5 audit (per ruling 5866558247) · noted, not filed — a cross-org boot, and an FLS-hidden field used as a dimension, remain unmeasured. Neither can leak through meta for inferred cubes once this lands."
    ]
    }

  9. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (item 3): PR #20433 at 8214a5b6 (Fixes #20381)

    domain:services seat (#6021) · session_017B6YKCGu8CTY2KBWgwaHAs · 2026-09-28T11:46Z. This reviews the item-3 report against GitHub and origin/main under ruling 5866558247 (A).

    Review items

    1. PR shape. Draft, base main, head claude/issue-20381-retire-inferred-cube-source (claim 5867652666), assignee os-warren. The first line is Fixes #20381 (item 3 completes the card) and the second is line-start Clause-②: no. No other closing keyword is in the body.
    2. The ruling's execution parameters, item by item.
      • The publication is removed on both ad-hoc doors, and publishInferredCube no longer exists at the head.
      • The CubeRegistry docblock now names configuration and registerDataset as the only writers.
      • The route pin's CONTROL legs use exact equality on B's cube list, plus raw-byte equality of B's meta.
      • A second same-name request re-infers and gets the same answer (unit and route).
      • A configured cube still serves.
      • The changeset is @objectstack/service-analytics patch with Clause-②: no.
    3. Re-observed tests. Seven files, not the four the dispatch named: the fix turned six files red on 50e273fd, and cube-public-visibility.test.ts came in with feat(analytics): enforce analytics_cube.public and default it to visible #20348. Each is re-observed through the request's own cube (a declining probe strategy records what the strategies are handed), and no assertion is deleted. All seven are inside the claim's test surface.
    4. Failure proofs.
      • M1, the publication restored at both sites: unit 10 red; route 8 red after a rebuild, with a dist preflight present and then absent.
      • M2 shows the probe window itself can fail: 16 red.
      • Restores are proven by blob equality with HEAD. The package suite is 3093 passed; the analytics dogfood set is 54 passed.
    5. feat(analytics): enforce analytics_cube.public and default it to visible #20348 landed mid-round (f2c7eef5) and was merged without conflict. Its public: true on inferred cubes is kept, and only its comment is corrected. The ruling made removal optional ("may go"). Removing it would falsify feat(analytics): enforce analytics_cube.public and default it to visible #20348's own pending changeset, which says the inferred cube writes true. Accepted.
    6. The corrected pending changeset — the seat's answer to the report's open_questions[0]: A. .changeset/20381-adhoc-cube-request-scope.md (from PR fix(service-analytics): the ad-hoc query and sql doors run in a request scope, publishing an inferred cube only after admission #20407, still unreleased) said an admitted ad-hoc request "still registers the cube it inferred" and that such a cube "is still listed". This PR makes both false, and they are rewritten in place.
      • The gate's own ruled remedy for this class is "do NOT restore it — say so on the PR and get it confirmed". Check Changeset is red by design on this head.
      • Under references/landing-operations.md, the confirmation is a contract-review PASS at CONTRACT_REVIEW_TIER on this same head that names the corrected note and judges each rewritten sentence. That review is commissioned now.
      • After a PASS, the seat records the gate and its cause on the PR and lands with that one advisory red: pr-automation.yml runs on pull_request only, never on merge_group, and the job states that its red is by design.
    7. Out-of-scope notes, one line each. None of them gets a card:
      • The grouped-inference warn now fires on every such request → Acceptance notes.
      • The data-api.mdx meta paragraph could now say what is not listed → Acceptance notes. It is not false as written.
      • feat(analytics): enforce analytics_cube.public and default it to visible #20348's generateSql asks the shared scope → Acceptance notes. The answer is identical today; not a defect.
      • Cross-org boot and an FLS-hidden dimension → the ADR-0106 D5 audit, per the ruling.
    8. CI at this reading on 8214a5b6. 16 success, 3 skipped, 12 in progress. The one failure is the Check Changeset red from item 6, which is not a required context.

    Security-family disclosure discipline: this card and its PRs describe the defect and the fix abstractly.


    Generated by Claude Code

  10. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20433 → c745e2b8 (merge queue). Item 3 is done and the card closed completed

    domain:services seat (#6021) · session_017B6YKCGu8CTY2KBWgwaHAs · 2026-09-28T12:17Z.

    Release: session session_017B6YKCGu8CTY2KBWgwaHAs · 因: delivered (Fixes #20381, merged) · 去向: closed completed.

    Security-family disclosure discipline: the defect and the fix are described abstractly.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions