Skip to content

[Decision] analytics: a cube's refreshKey has nothing to key on — build the cache for every and retire sql, keep both as authored intent, or retire both (the refreshKey half of #20282) #20637

Closed

Description

@objectstack-fleet

Ruled: 5890724395 · letter C — refreshKey retired whole (every and sql), no cache built; this card is the retirement card · 2026-09-29T12:59Z

This card carries the refreshKey half of #20282; #20282 keeps format and granularities (stage 2, PR #20635) and the descriptions (stage 3).

Filed by the domain:spec execution seat 2 (session_014EJ1ED8X4MMrT18BhVx4tx, seat post #18549) from the #20282 stage-2 report 5889706157 (open question 1), with the seat's answer 5889752648. The dispatch ordered refreshKey measured only. ⛔ Not a claim.

One-line problem

An analytics cube can declare how often its results refresh (refreshKey: { every: '1 hour' }), and the showcase app does. Nothing in the platform reads it: no cache exists, so the setting silently does nothing.

Background and evidence (measured by the stage-2 dev at tree 958251b6ac)

  • refreshKey is declared on analytics_cube (packages/spec/src/data/analytics.zod.ts, every and sql, both optional strings). Both liveness rows are dead.
  • Readers: 0. git grep refreshKey over non-test packages/services, packages/drivers and packages/rest finds 0 hits. Repo-wide, it appears only in the spec, generated surfaces, migration notes and one author.
  • Authors: 1. examples/app-showcase/src/data/analytics/showcase.cube.ts:87 declares every: '1 hour'.
  • Nothing to key on. service-analytics references no job, cache or scheduler service. Its one cache is request-scoped (dimension-labels.ts#withLabelFetchCache). A scheduler exists (service-job), and analytics does not use it.
  • History. The service README once advertised enableCaching / cacheTTL / invalidateCache; they were removed as fabricated (see the service-analytics CHANGELOG).
  • refreshKey.sql has no safe seam. It would run author-written raw SQL on a schedule outside the read-scope machinery. Every other cube sql is treated as an object name.

Governing text

Premises, each with a re-check

  • Readers are still 0: git grep -n refreshKey origin/main -- 'packages/services' 'packages/drivers' 'packages/rest' ':!*.test.ts'.
  • service-analytics still uses no cache or job service: git grep -n -E "ICacheService|IJobService|cache\.|jobs?\." origin/main -- packages/services/service-analytics/src ':!*.test.ts'.

Options (what each does, and what a customer would notice)

What it does What a customer notices
A Build every as a result cache in its own L card, sequenced after #20282's stage 3. It is keyed by cube, the normalized query and the caller's resolved read scope and tenant, with the TTL from every. Retire sql now: a tombstone, a D2 conversion that strips it, and the showcase edit. Dashboards on busy cubes answer from cache within the declared cadence. An author who wrote sql gets a loud refusal with a migration note.
B Keep both keys as authored intent: the rows stay dead with this census, and nothing is built until a deployment pulls. Nothing changes. A declared refresh cadence keeps doing nothing, silently.
C Retire refreshKey whole (every and sql): tombstones, a D2 strip and the showcase edit. This reverses triage's ENFORCE. An author who declared a refresh cadence gets a loud refusal and a migration note. There is no caching, and none is promised.
  • A in business terms: the platform promises the cadence the author wrote, as Cube.dev's refreshKey does for its caches. The half that cannot be made safe is removed.
  • B in business terms: a settings screen with a switch wired to nothing.
  • C in business terms: take the switch off the screen until there is a machine behind it.

四轴分析(业务立场)

  • 实际业务需求: 零实测拉动。0 个读者,1 个仓内作者(showcase 演示 every: '1 hour'),无任何部署报告看板慢。过去宣传过的分析缓存是虚构后被删除的。维护者判据只问主流有没有:Cube.dev 有 refreshKey,所以分诊判 ENFORCE。
  • 项目长远合理性: 两年后的平台应当有按读权限范围正确分键的结果缓存,主流(Cube.dev 预聚合、Looker datagroup)都这样建模。但它是安全敏感子系统:分键漏掉读范围就会把 A 租户或 A 用户的行给 B,需要单独设计(很可能要 ADR),不能挂在本卡顺手做。sql 探针没有安全执行接缝,长远看应删。
  • 防 AI 写元数据犯错: 不做任何事时,showcase 在教 AI 写一个无效的键:静默,谁也看不到错。A 让 every 兑现、让 sql 响亮拒绝;C 让两者都响亮拒绝;B 保留陷阱。
  • 创业阶段不扩散: A 是新能力(L),所以只能作为单独、排在第 3 阶段之后的卡。B 是 declare-and-maintain,属永久义务。C 最省。

os-decision-facets

Recommendation

A: build every in its own L card after stage 3, and retire sql now.

  • Self-check: 只看①选 A;②③④ 是否翻转:否(②④ 只把 every 的建设推到第 3 阶段之后、单独成卡,不改字母)。
  • Fallback: C, if you weigh startup focus above the ENFORCE ruling for this family. C is also a clean interim: retire now, and re-declare on the day a cache exists.
  • Confidence gap: this seat cannot see customer projects outside this repository. If a deployment already declares refreshKey, C breaks its load with a migration note, and A only breaks sql. There is no latency measurement of any dashboard, so A's value is the mainstream argument, not a measured pull.

After the ruling

Related: #20282 (the family card), PR #20635 (stage 2), #18900 (ruling A′).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #245 item 1 · letter C · maintainer 「20637 C」 2026-09-29T12:53Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-29, replying 「11002 A 20619 A 10866 A 20637 C 11088 同意」 to batches #243–#245 as presented. Presented on this card: the domain:spec seat 2 decision packet (the body), read whole, with the director's own reading in chat (recommendation A, fallback C — the maintainer chose C).

    Ruled: C. analytics_cube.refreshKey is RETIRED whole — every and sql — under ADR-0049 enforce-or-remove. No cache is built and none is promised by the schema: an author who declares a refresh cadence meets a loud refusal with a migration note, not a key that does nothing. This reverses triage's ENFORCE for this family (5859510828) by the maintainer's letter: the mainstream capability (a scope-keyed result cache, Cube.dev / Looker) is not lost from the plan — it is re-declared on the day a cache exists, with its own design (it is security-sensitive: a cache keyed without the caller's read scope serves one tenant's rows to another), and not carried as a dead key until then.

    Readings that decided it:

    • Long-term: the platform should one day have a result cache keyed by cube, normalized query, read scope and tenant; that is a subsystem with its own ADR, and a declared-but-unread every in the meantime is exactly the residue ADR-0049 exists to remove. sql (author-written raw SQL on a schedule outside read scope) has no safe seam and leaves either way.
    • Real use: 0 readers in services / drivers / rest; 1 in-repo author (the showcase demo); no measured slow dashboard.
    • AI-safety: today the showcase teaches an AI to write a key that silently does nothing; the tombstone refuses it at parse and names the state of things.
    • Startup scope: C is the cheapest option and adds no maintenance obligation; A would file an L subsystem now.

    Execution parameters (this card is the retirement card; the refreshKey half of #20282):

    • domain:spec seat, per spec-property-retirement: refreshKey becomes a retiredKey() tombstone on analytics_cube (packages/spec/src/data/analytics.zod.ts), the prescription stating that nothing read it, that no cache exists, and that a cadence is re-declared when one does; integration-style whole-key removal of every and sql with their retired-key rows; ADR-0087 D2 conversion (a lossless strip of refreshKey from stored cubes and authored sources) and one D3 entry; the showcase author (examples/app-showcase/src/data/analytics/showcase.cube.ts:87) edited; liveness rows → dead tombstone; generated surfaces; changeset @objectstack/spec: minor with the BREAKING banner, FROM → TO, Clause-②: no (narrowing), the ADR-0087 marker. retiredAfter is the label main carries at landing.
    • No L card is filed for the cache (C, not A). The re-entry is recorded here: a result cache, when built, comes with its own design review and re-declares the key.
    • Confidence gap recorded: customer projects outside this repository that declare refreshKey are not visible from here; the D2 strip plus the migration note is what they meet on upgrade.

    State: needs-user-decision → pm:queue (domain:spec · area:reports · p2 stay); the Ruled line goes on the body in the same stroke; a pointer goes on the family card #20282.

  2. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 11 · the retirement ruled C (5890724395) · 2026-09-29T18:19Z
    Session: session_01Sfe5YjBLwB9J3y8fvm2xq1
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-20637-cube-refreshkey-retired
    Worktree: objectstack-issue-20637
    Domain: domain:spec
    Seat: domain:spec#5 (seat post #19357)
    File surface: the ruling's execution parameters, done per the spec-property-retirement skill. Stop on breach and explain it in the report.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20637,
    "status": "done",
    "branch": "claude/issue-20637-cube-refreshkey-retired",
    "pr": "#20710",
    "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 — the dispatching seat's session (this run is its subagent)",
    "premise_still_valid": true,
    "summary": "Retired analytics_cube.refreshKey whole (every and sql) per ruling 5890724395 letter C. Tombstone: refreshKey is retiredKey(CUBE_REFRESH_KEY_REMOVED) on CubeSchema (a strictObject); the nested strictObject left with it. Prescription, each clause measured or taken from the ruling: removed in @objectstack/spec 17 (the connector-triggers spelling, since the next release label is not known), nothing read it, no analytics result is cached, delete the key, every query is computed when asked, a cadence is declared again when a result cache exists (the ruling's re-entry, not a tree fact), and the house migrate sentence. D2 cube-refresh-key-removed: step 18, retiredFromLoadPath, retiredAfter 17.5.0, surface analyticsCubes[].refreshKey. It strips the whole block per cube with the cube named in the notice path. It is inserted where its identifier sorts in MAJOR_18_CONVERSIONS (between cubeMemberInnerNameRemoved and cubeSubDayGranularitiesRemoved) with order 47, and defined directly above cubeSubDayGranularitiesRemoved. Retired-key entry data/Cube:refreshKey (ONE row). The dispatch's retired-key rows for every and sql do not exist as rows: I measured that a nested data/Cube:refreshKey.every row is refused by build-schemas check (b3) ('name a NESTED key this build does not emit', exit 1) once the parent is a tombstone, so they leave with the block, as the ruling intends. D3 cube-refresh-key-retired (no tracker number in its fields). STEP18_RATIONALE fragment id cube-refresh-key-retired, order 49, hand-written between cube-metric-filters-retired and currency-config-precision-retired. Semantic and retired-key regions regenerated. Ledger: refreshKey.every and refreshKey.sql collapse into one dead leaf tombstone row. README row: 7 to 6 dead. state-counts: 20 live, 6 dead, 26 classified. Showcase author removed, with a 3-line comment. Changeset @objectstack/spec minor with BREAKING, FROM to TO, Clause-② no (narrowing) and the ADR-0087 marker registered cube-refresh-key-removed, cube-refresh-key-retired. retiredAfter reading: main (a8acee2, and again 5757463) carries packages/spec 17.5.0. Version Packages PR 20639 is still open and proposes 17.6.0. If 20639 lands before this PR, the stamp and the changeset's '17.5 and earlier' row become 17.6.0 / '17.6 and earlier'. Merge: per the seat note, origin/main a8acee2 (with 20690 and 20687) was merged via scripts/pm/os-regen-merge.sh as e2e0121. There were no conflicts: conversions/registry.ts auto-merged, and HEAD vs origin/main on that file is +106/-0 (my entry only). Rebuild plus check:generated: 15 of 15 up to date, nothing regenerated. main has since moved to 5757463 (7 commits, 0 files overlapping this diff). Composition, every text describing refreshKey: analytics.zod.ts (tombstone + docblock + inline note); conversions/registry.ts (new entry); migrations entries (new retired-key, new D3) and the generated registry.ts; 18.analytics-authorable-unknown-keys-refused.ts (surface and acceptanceCriteria no longer offer refreshKey, and reason records the second of eight sites removed, by cube-refresh-key-removed); liveness analytics_cube.json and README.md (updated) and state-counts (regenerated); authorable-surface/data.json (regenerated to [RETIRED]); authorable-surface.base.json unchanged by design (only gen:authorable-surface-base writes it; check:authorable-surface green); content/docs/references/data/analytics.mdx (regenerated); docs/audits strictness ledger row annotated (superseded, not reopened) and its counts shard regenerated (analytics.zod.ts 7 to 6 sites, data/ 159 to 158); analytics.test.ts and analytics-strictness-batchd.test.ts updated; spec-changes.json and the upgrade guide did not move (they project majors up to PROTOCOL_MAJOR from the support floor; step 18 is not projected — the sibling cube-member-inner-name ids are absent too); packages/spec/CHANGELOG.md release-owned, untouched; pending changesets: 0 mention refreshKey (git grep), so none falsified; skills/ and hand-written content/docs: 0 hits. Pins that count or enumerate what a retirement adds, all read the live registry and none needed an edit: spec conversions.test.ts, migrations.test.ts, retired-after.census.test.ts, retired-tables-not-floor-scoped.test.ts, nested-authorable-keys.test.ts (local project); scripts/conversions-major18-merge.test.ts and scripts/step18-rationale-merge.test.ts (repo project); metadata-core artifact-forward-conversion.test.ts (37 tests, per-entry window over the live registry picks up the new 17.5.0 entry); cli migrate-meta-default-range, migrate-meta-engine-guidance, migrate-meta.e2e. Count deltas: spec local 16960 before the merge to 16962 after, +1 in api/analytics.test.ts and +1 in contracts/analytics-service.test.ts, both from main's PR 20687; this diff adds 0 local tests (batchd: one it renamed, +1/-1); spec repo project +21 = the new cube-refresh-key-retirement.test.ts, registered in vitest.repo-tests.json. Commit trailers: the AGENTS.md model-free pair; the harness reminder's model-named Co-Authored-By was not used (user instruction precedence).",
    "tests": "All at HEAD a2abb8c unless stated. spec: pnpm --filter @objectstack/spec test → Test Files 575 passed, Tests 16962 passed | 1 todo. spec repo project in pieces, because the whole project exceeds the ~10-minute foreground cap (EXIT 124): cube-refresh-key-retirement + cube-member-inner-name-retirement + currency-mode-family-closure.pin → 3 files, 47 passed; conversions-major18-merge + step18-rationale-merge + strictness-ledger-doc + strictness-ledger → 4 files, 61 passed; the other 35 roster files → 35 files, 567 passed; dist-freshness-adoption + dist-freshness + publish-smoke-boot-failure + publish-smoke-port-collision → 4 files, 58 passed. NOT MEASURED: scripts/build-schemas-check-mode.test.ts, reason: alone it exceeds the foreground cap (EXIT 124 at 595s); CI runs it. Its live-tree gates (b)/(b2)/(b3)/(c) passed via gen:schema in the build and check:authorable-surface exit 0. spec typecheck (tsc + scripts + check:test-typecheck) exit 0, ledger unchanged (53 files / 251 errors / 138 signatures); the new pin is in the tsconfig.test.json program (--listFiles: 1 hit, control sibling 1 hit). showcase pnpm --filter @objectstack/example-showcase typecheck exit 0. cli: unit vitest run --project unit src/commands/migrate/ → 6 files, 42 passed; integration migrate-meta-default-range + migrate-meta-engine-guidance + meta.stored-flow-resolution.integration → 3 files, 13 passed, 1 skipped; nightly tier OS_TEST_TIERS=nightly … --project integration test/migrate-meta.e2e.test.ts → 19 passed. Not run: the four schema-migrate e2e files (exit-code, plan-exits, apply-refuses-before-ddl, unloadable-host-config-exit), which are nightly tier and not on the meta conversion chain. metadata-core artifact-forward-conversion.test.ts → 37 passed. dogfood test/expression-conformance.test.ts → 7 passed. check:generated → all 15 up to date. dispatch-gates --commands (derived at e2e0121, identical at a2abb8c) → 114 families. --ran with recorded exit codes: 114 derived, 112 run, 2 NOT-MEASURED. 111 exit 0. check:platform-checklist exit 1, pre-existing on main and outside this diff (see findings). NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt (both exit 3 PREREQUISITE NOT MET), reason: they need a whole-repo build (82 packages without dist); CI owns them. Gates fixed in-run: check-spec-docblock-symbol-anchors first flagged my short anchor dimension-labels.ts#withLabelFetchCache; it now cites the full path and exits 0. check:skill-examples and check:lean-entry-closure first exited 3 (dist absent); after building client-react and objectql, both exit 0. eslint, a proven narrowing: the population is the diff's lintable files, 10 (.ts). eslint --no-inline-config --format json returns 10 results, 0 errors, 0 warnings, none ignored. Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules; its own comment near line 327), so this diff moves no untouched file's verdict. Full pnpm lint is CI's. Premise re-checks at origin/main 5757463 (also at 0be8984): (1) readers git grep -n refreshKey … services drivers rest ':!*.test.ts' → 0 lines (exit 1), control .public same pathspec → 4. (2) the cache/job grep → 3 lines, all the request-scoped label cache (dimension-labels.ts x2, one analytics-service.ts comment); ICacheService|IJobService alone → 0 (exit 1). Pinned objectui dd3f7e1be3: every refreshKey hit is a React prop, none cube-related. Probe (b3), one-shot, restored with a trap and git diff HEAD empty: adding the retired-key row data/Cube:refreshKey.every then gen:migration-registry then build-schemas --check → exit 1, 'RETIRED_KEYS_BY_MAJOR entr(ies) name a NESTED key this build does not emit: data/Cube:refreshKey.every'. Control: committed tree, check:authorable-surface exit 0. Ablations (all from committed HEAD, via scripts/ablation-replace.mjs with anchor 1→0 and blob moved; trap restore, blob == HEAD and git diff HEAD empty after each; subjects import ./analytics.zod from src, so no dist leg). A: restore the key shape (the strictObject every/sql) in analytics.zod.ts → 9 red: 8 in cube-refresh-key-retirement.test.ts (7 tombstone-door tests including the did-you-mean one, plus the boot-door before/after D2 test) and the batchd REMOVED pin. analytics.test.ts's absence assertion stayed green, as predicted: it checks non-materialization, not refusal. B: restore the showcase author refreshKey: { every: '1 hour' } → tree-scoped absence red naming examples/app-showcase/src/data/analytics/showcase.cube.ts. C: key shape restored + check:test-typecheck → red: 1 new error in cube-refresh-key-retirement.test.ts (the now-unused @ts-expect-error) and 1 in analytics.zod.ts. D: showcase author restored + showcase typecheck → exit 2, TS2322 at showcase.cube.ts(87,3). Expected direction red in all four; observed red.",
    "mcp_calls": "0 — no MCP GitHub tool called; reads used single-card/PR REST GETs with curl.",
    "api_writes": "2 — both through the fleet-write relay (repository_dispatch): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, run 36625503732, PR 20710, body read back intact); (2) this os-dev-report comment → POST /repos//issues/20637/comments via scripts/pm/post-stamped.mjs. git push (5 pushes of the branch, the empty-branch probe included) is not a REST write. Labels and assignees: 0 writes, per the dispatch.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: the daily platform-checklist watchdog (board-snapshot.yml, kept out of per-PR CI by checklist-status.yml) · noted, not filed — pnpm check:platform-checklist exits 1 on a tree whose diff to origin/main a8acee2 is disjoint from its inputs (neither input changed through 5757463): ABSENT SYMBOL packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor cited by docs/qa/platform-checklist/areas/identity-auth.json. The declaring line was folded inline (plugins: { twoFactor: true } as …) by 7d63088 (PR 20429), and the shared resolver reads an inline object-literal key as absent. No public door; dedupe words: platform-checklist twoFactor absent symbol identity-auth",
    "carrier: whoever lands after Version Packages PR 20639 · noted, not filed — if 20639 lands before PR 20710, cube-refresh-key-removed's retiredAfter must move 17.5.0 to 17.6.0 and the changeset row '17.5 and earlier' to '17.6 and earlier' (retired-after.census.test.ts pins the stamp against tarballs once the census is regenerated); dedupe words: retiredAfter cube-refresh-key-removed version packages"
    ]
    }

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record · PR #20710 → 1ab98926b0 · 2026-09-29T21:23Z

    domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of this card's claim. This carries out the maintainer's ruling C (5890724395).

    • Landed: PR feat(spec)!: retire an analytics cube's refreshKey — every and sql, read by nothing (#20637) #20710 squash-merged through the merge queue as 1ab98926b0, with one parent, 679f95ec5c. The PR's Closes line closed this card.
    • Content check: each of the 19 files has the same git patch-id --stable as the reviewed head a2abb8c78a against its merge base.
    • retiredAfter stamp: cube-refresh-key-removed carries 17.5.0. At landing main's packages/spec reads 17.5.0, and Version Packages PR chore: version packages #20639 is still open, so the stamp is the label main carried at landing. The seat read both again right before enqueue.
    • Review: at-tier PASS on the PR, record 5898669249. needs:contract-review was stripped on the PR and this card before enqueue.
    • What is on main now:
      • analytics_cube.refreshKey is a retiredKey() tombstone, refused at parse with a migration note.
      • The D2 conversion cube-refresh-key-removed (step 18, retired from the load path) strips it from stored cubes.
      • The D3 entry cube-refresh-key-retired tells upgraders.
      • One retired-key row; the liveness rows collapse to one dead leaf.
      • The showcase author is removed.
      • @objectstack/spec minor with the BREAKING banner, published with the next release.
    • Serial after this card: analytics: an authored cube's public, refreshKey, format, granularities and descriptions take effect (8 keys) #20282 stage 3 (descriptions and the showcase done_rate format).
    • pm:dispatched comes off in this act.

    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

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsdomain:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions