feat(spec)!: retire CubeJoin.sql and CubeJoin.relationship — the ON clause is derived - #18938
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also 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 5b5ed09724b9d14a3083e0831e76e77f8329ce71 && git checkout 5b5ed09724b9d14a3083e0831e76e77f8329ce71
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b14610255101483c94dccf282bb3e68859641411 7caf92189a2780b22e3aab9bb6154f8604aa1974 && git checkout -B drift-repro b14610255101483c94dccf282bb3e68859641411 && git merge --no-ff 7caf92189a2780b22e3aab9bb6154f8604aa1974
node scripts/docs-audit/affected-docs.mjs --json b14610255101483c94dccf282bb3e68859641411
|
…ON clause is derived
`CubeJoin.sql` was REQUIRED and described itself as the `ON` clause, and nothing
ever read it. Both analytics strategies synthesise the join — `NativeSQLStrategy`
emits `LEFT JOIN <name> <alias> ON "<parent>"."<segment>" = "<alias>"."id"` from the
dotted member path alone, and `ObjectQLStrategy` resolves the join through
`cube.joins?.[alias]?.name` and lowers it to a relationship traversal with no `ON`
clause at all. So an authored join condition was not ignored, it was REPLACED under
a 200. `relationship` is the same shape one key over: it carried a
`.default('many_to_one')` and nothing dispatched on the cardinality.
`CubeJoinSchema` is a `strictObject`, so the route is strict deletion plus a
`guidance` prescription rather than a `retiredKey()` tombstone — the same route
`MetricSchema.filters` took in this file. The refusal names the key and states that
the `ON` clause is derived from the declared relationship between the two cubes'
objects. The `on` alias, which pointed at `sql`, becomes a `guidance` entry of its
own so an author is never sent to a key the shape cannot accept.
ADR-0087 registration: the two exact keys in `RETIRED_KEYS_BY_MAJOR[18]` plus the D3
semantic entry `cube-join-sql-and-relationship-retired`. Not a D2 conversion — there
is no consumer source to rewrite, and an author who wrote a non-FK condition wanted a
join the runtime does not perform, which is a judgement rather than a strip.
ADR-0049 enforce-or-remove; maintainer ruling 2026-09-18 (director batch #154
item 4, letter 2).
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
b43d445 to
6ac13a9
Compare
|
⭐ Adopted verbatim by the
Contract reviewServed-tier: 181/181 ① Derived judgmentsRead on the PR head
② Semver level
③ Boundary flags
Zeros I report carry these controls: consumer reads 0 vs 8 Implemented-by: VERDICT: FAIL Generated by Claude Code |
Provenance —— 契约复核已归档,两个载体同笔清除派发席(
⛔ 这不是放行FAIL 的阻塞项是 ③-1。本席不自行按复核开的处方施工 —— 那条处方与维护者裁决里的一句话直接相抵(裁决写「zero producers, so no conversion is owed」,复核量出的是「在持久化产物上欠一条 D2」)。⇒ 按复核自己的话「it must not ship silently either way」,本席把这个岔口交回维护者,已在卡 #18612 上另贴并挂 ⛔ 本 PR 仍是 draft, Generated by Claude Code |
wip — regeneration and verification follow. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…arker Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
|
⭐ 本席( 档位证据取自复核子代理自身 transcript:468 行中 喂给复核席的只有:卡 #18612 的正文、两条在先裁决(董事席补充裁决
⭐ 记录判 FAIL,其 ③-1 是可执行的处方,本席已据此安排第三轮;⛔ 本席不改写、不润色、不挑拣。 Commands run (head worktree Contract reviewServed-tier: ① Derived judgmentsFresh review of the head
② Semver level
③ Boundary flags
Zeros I report carry these controls and radii: consumer reads 0 vs 7+1 Implemented-by: VERDICT: FAIL Generated by Claude Code |
|
偏差一:上一条采纳的是 interim hand-back —— ⭐ 这是本席的错复核子代理第一次交回时,宿主的通知里写明它仍有后台工作在跑、结果可能是中途的。本席读到了那句话,仍然采纳了。随后它交回了最终记录。 两版的差别,本席逐段比对过:
⇒ ⭐ 结论没有因此改变,但「已测」与「未测」的分界改变了 —— 而那正是这类记录最该被准确引用的部分。 偏差二:平台把记录里的尖括号片段改写了 —— ⛔ 那不是本席改的⏱️ 同一时刻实测:上一条存回来的正文与本席送出的不逐字节相同,首个差异在偏移 4457 —— 本记录里同类片段共 15 处、9 种( 档位证据(与上一条相同的一次测量)复核子代理自身 transcript:
Commands run (head worktree Contract reviewServed-tier: ① Derived judgmentsFresh review of the head
② Semver level
③ Boundary flags
Zeros I report carry these controls and radii: consumer reads 0 vs 7+1 Implemented-by: VERDICT: FAIL Generated by Claude Code |
…bejoin-retire-sql-relationship
|
补记:同一次复核共交回 三 版记录。判决与阻塞项三版完全相同;差别只在「已测 / 未测」的分界。 ⏱️ 2026-09-18T14:41Z(本行时刻由本次动作读钟而得)。 三版的关系,本席机械比对过三版的 ③-1(唯一阻塞项)一字未变;②、①-1..①-8、①-10 亦同。变的只有 ①-9 与开头的命令清单:复核席排在共享验证锁后面的那批本地运行,逐次跑完了。 v2 → v3 的实质增量,逐字:
⇒ ⭐ 结论不因此改变,但「已测」与「未测」的分界改变了 —— 而这正是这类记录最该被准确引用的部分,所以本席把它记下来,⛔ 而不是让最新一版悄悄取代前一版。 本席据此停在哪里,以及为什么⛔ 本席不再贴第四份全文。采纳的记录以
|
`check:api-surface-declarations` landed on main after this branch's merge base, so the branch never carried the shards this retirement reshapes. The narrowing removes `sql` and `relationship` from `CubeJoin`, and type inlining propagates that into five declarations: ObjectStackDefinitionSchema, ObjectStackSchema, CubeJoinSchema, CubeSchema, EnvironmentArtifactSchema. Pure deletion — 30 lines, 0 added. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
轮次 3 交回 —— ③-1 的阻塞项,本席实测已解除本席 2026-09-18T15:02Z 复核。下面每一条都取自 GitHub API,⛔ 不取 dev 报告的叙述。 契约复核在 head 现读:
⇒ 纯删除,一行未加。停止线没有触发;⛔ 没有任何一行被手写进生成物。这正是复核预判的形状 —— 五个 reshape 的声明各自失去 CI 现读(⏱️ 2026-09-18T14:59Z 取):33 个 check run,0 个失败。 ③-2 本席已执行PR 正文已重写(就在本条之前)。旧正文描述的是一个已不存在的 head —— 它还在讲「 ③-3(非阻塞)本席的处置:不改复核指出 changeset 的 ADR-0087 标记只点了 D3 的 id,而 D2 的
|
入队 —— 并说清「复核判 FAIL,本席为什么仍然落它」本席 2026-09-18T15:25Z 入队。决定性读数:timeline 的 入队前的读数,全部第一手:
为什么那个 FAIL 不拦这一次落地契约复核的判词是 FAIL,而它的 ③-1 自己划定了这个 FAIL 的范围,逐字:
⇒ 它是落地阻塞,⛔ 不是契约缺陷。①(十条推断)与 ②(semver 档位)那两节,复核判的全是「correct」。 而同一份记录预先裁过那个修复 commit,逐字:
⇒ 本席没有为这个新 head 重开一次契约复核。理由不是省事:
③-2 / ③-3 的落点
轮次 3 的 dev 报告 —— 本席读到的、比本席自己量得更多的两件报告在卡 #18612 的评论
|
…bjectstack-ai#18968) Part of objectstack-ai#18915 Clause-②: no Executes maintainer decision batch objectstack-ai#156 item 2 — ruling F on objectstack-ai#18715. No gate, no ratchet, no CI wiring is added: this is the user-facing half of PR objectstack-ai#18751's census, corrected. ## Act 1 — the split, re-derived PR objectstack-ai#18751's instrument re-run on a fresh `origin/main` (`node scripts/measure-markdown-ts-blocks.mjs --json`, workspace built first, all three controls behaving: GREEN clean, FIRING reports TS2341, UNPUBLISHED_SUBPATH reports a counted TS2307 on each of its three specifiers). The card's numbers reproduce exactly: ``` handwritten stratum blocks 282 files 54 raw 195 tolerant 76 well-formed-and-wrong 58 changelog stratum blocks 720 (out of scope) ``` Split by publication — a block is *published* when its document is inside its package's `files[]`: | reading | total | published | internal | |:---|---:|---:|---:| | tolerant failures | 76 | **54** | 22 | | syntactically valid and wrong | 58 | **44** | 14 | The split is unambiguous in this repo: every non-private package's `files[]` is `["dist","README.md","CHANGELOG.md"]` (only `@objectstack/spec` lists more), so **the published package-root Markdown is exactly `README.md`**, and every other package-root document — `ADVANCED_FEATURES.md`, `PHASE2_IMPLEMENTATION.md`, `V3_MIGRATION_GUIDE.md`, `ARCHITECTURE.md`, `DEVELOPMENT_PLAN.md`, `PLUGIN_STANDARDS.md`, `REST_API_PLUGIN.md`, `ZOD_SCHEMA_AUDIT_REPORT.md`, `STACKBLITZ.md`, `CLIENT_SPEC_COMPLIANCE.md`, `ROADMAP.md`, plus the private `packages/qa/*` READMEs — is internal. Confirmed against the packer rather than asserted from `package.json`, with a control from the same population that must read the other way: ``` npm pack --dry-run --json, packages/core README.md in tarball: True # positive control ADVANCED_FEATURES.md in tarball: False # same population, must be absent PHASE2_IMPLEMENTATION.md in tarball: False ``` No count in the split is zero, so no zero needed pairing. **`packages/spec/liveness/README.md`** (PR objectstack-ai#18938's surface) measures as **published** — `liveness` is a `files[]` entry, and `npm pack` puts `liveness/README.md` and `liveness/state-counts.md` in the tarball. It is nonetheless **not in this card's population**: the census population is Markdown at a *package root* (a directory carrying a `package.json`), `packages/spec/liveness` is not one, and the file therefore contributes none of the 76/58. No overlap with objectstack-ai#18938, and nothing of theirs is touched here. ## Act 2 — the published corrections **43 of the 44** published syntactically-valid-and-wrong blocks now compile; 20 READMEs changed. Re-measured on the same instrument: | reading | before | after | |:---|---:|---:| | published, syntactically valid and wrong | 44 | **1** | | published, tolerant failures | 54 | 11 | | internal, syntactically valid and wrong | 14 | 14 (untouched) | | internal, tolerant failures | 22 | 22 (untouched) | The 11 remaining published tolerant failures are **10 syntax-only blocks** — bare type-signature fragments in `service-automation`, `trigger-record-change`, `trigger-schedule` and `service-job`, the `needs-explicit-partial-tag` class the instrument's own forward convention describes — plus the one block below. Syntax-only blocks are outside act 2's mandate, which is the syntactically-valid-and-wrong set. What was wrong, by class: - **Legacy option vocabulary.** `@objectstack/client-react`'s hooks take `fields` / `orderBy` / `limit` / `where`; the README still wrote `select` / `sort` / `top` / `filters`, and read `PaginatedResult.value` where the member is `records`. Also `timeout` to `timeoutMs` (`service-job`), `attempts` to `maxAttempts` (`service-queue`), `filter` to `where` (`IDataEngine.find`). - **An async API used as a chainable one.** `ObjectKernel.use()` is async and resolves to the kernel, so `kernel.use(a).use(b)` does not type-check at all; and `ObjectKernelConfig` has no `plugins` member. - **Interfaces implemented but never imported.** Four plugin examples wrote `implements Plugin` with no import — which silently bound to the DOM's `Plugin` — and three omitted the required `init`. `PluginContext.getService` is declared with a type parameter that has no default, so every example that read a service back left it `unknown`. - **Removed or never-existing API, rewritten rather than left as a fossil.** `@objectstack/driver-memory`'s default export is a legacy `onEnable` object that `kernel.use()` refuses on both the type and the boot path — the quick start now registers through `DriverPlugin`, and the "Key Exports" row that called it a drop-in plugin is corrected with it. Its persistence adapters take an options bag and hang under `persistence.adapter`. `defineStack` has no `driver` key. `@objectstack/rest`'s `RestServer` takes the host `IHttpServer` as its first argument and `registerRoutes()` takes none; `RouteManager` is constructed on a server. `ObjectSchema.parse()` returns the value — the `{ success, data }` envelope belongs to `safeParse`. `useMutation` has no `onMutate` and no mutation context, so the "Optimistic Updates" example was rebuilt on the options it does have. - **Untyped parameters under `--strict`** in React and handler examples, annotated. Two of the 44 (`packages/cli`, `packages/mcp`) were **measurement artefacts worth stating plainly**: `objects: Object.values(objects)` over an elided `./src/objects` barrel. The forgiven TS2307 leaves the namespace `any`, and `Object.values` then infers its type parameter from the union-shaped contextual type, producing a mismatch a reader's own resolvable barrel would not produce. Both now name the objects they import, which is typed and clearer either way. Beyond the counted blocks, the same defect class was corrected in three further `client-react` blocks (Master-Detail, Search with Debounce, and the Type Safety comment) that the census does not flag only because they import nothing and so type-check as `any`. Leaving `data.value` and `select:` standing one section below a corrected copy of themselves was not defensible; this is called out because it is work outside the measured set. ## The one block deliberately left, and why `packages/plugins/knowledge-ragflow/README.md` writes `source.options.datasetId`. That is what the shipped adapter reads (`extractRagflowOptions` casts the source to a shape carrying an optional `options` record, and its error text names `source.options.datasetId`), and it is **not** what `KnowledgeSourceSchema` declares — the declared key is `adapterConfig`, and the schema is a plain `z.object`, so a parse would strip `options` outright. Correcting the document to `adapterConfig` would make it compile and stop working. Correcting the adapter is a runtime change, out of this card's scope, and picks a winner between two live spellings. Contract-first says the defect is upstream, so the block is left as it stands and the conflict is reported for the maintainer instead of being papered over in a docs PR. That is why this PR says `Part of objectstack-ai#18915` and not `Fixes`. ## Changeset — measured for this diff, not inherited The house `skip-changeset` argument for docs cards is "no package's `files[]` reaches `content/docs/**`". **It inverts here.** `README.md` is listed in `files[]` for every one of the 20 packages touched, so the bytes this PR changes are inside the published tarball — measured above with `npm pack --dry-run` and a same-population control that reads the other way. AGENTS.md: `skip-changeset` "is for a diff that publishes nothing from any released package". This diff publishes changed bytes from twenty released packages, and those bytes are what an upgrading agent reads. So this PR carries a **`patch`** changeset naming all twenty, and ⛔ no `skip-changeset` label. ## Scope - Touched: `packages/*/README.md` only, plus the changeset. ⛔ No internal document, ⛔ no `CHANGELOG.md`, ⛔ no `content/docs/**`, ⛔ no runtime code, ⛔ no gate or CI wiring. - The changeset file is the one path outside the claim's declared file surface (`packages/**/README.md`); it is the companion artefact the measurement above obliges, and it is named here rather than slipped in. ## Acceptance notes - `packages/client/README.md` documents `data.find()`'s legacy vocabulary (`select` / `filters` / `sort` / `top`). Unlike the `client-react` case this **compiles** — `QueryOptions` still accepts it — so it is out of this card's set, but `find` itself carries `@deprecated` and `data.query()` is the canonical call. Noted, not filed. - The `check:undeclared-dep-imports` family is not affected: no `package.json` moved. ## Verification Tree: `a8b75f978` (`origin/main` merged in, workspace rebuilt, `pnpm install --frozen-lockfile` after the lockfile moved). Every number below is from that tree. **The census, final run.** `node scripts/measure-markdown-ts-blocks.mjs --json`, exit 0, all three controls behaving (`green=clean firing=fires unpublished-subpath=fires`): ``` handwritten blocks 284 files 54 raw 172 tolerant 33 well-formed-and-wrong 15 published tolerant 11 well-formed-and-wrong 1 internal tolerant 22 well-formed-and-wrong 14 ``` 284 rather than 282 because the `observability` wiring block, which redeclared `metrics` four times in one fence, is now three fences — one per deployment, which is how a reader picks between them. **Gate families**, derived in-worktree from this tree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no stale-tree warning after the merge): **63 derived, 63 run, all exit 0**, reconciled back through `--ran` with each command's exit code captured before any pipe — "63 derived familt(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)". `check:pm-dispatch-gates` is not among the derived families for this change set. That reconciliation answers one link only; it is not a complete account of CI. **Tests.** The diff changes no TypeScript, so no package's `tsc` program or vitest source set moves. Two suites do read a README this PR edits, found by grepping every test file in `packages/` for `README.md` (19 hits, triaged by the path each one reads), and both were run: ``` pnpm --filter @objectstack/client exec vitest run --maxWorkers=2 src/readme-package-install-example.test.ts Test Files 1 passed (1) · Tests 6 passed (6) · CLIENT_README_EXIT=0 pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/package-api.test.ts src/kernel/plugin-structure.test.ts Test Files 2 passed (2) · Tests 81 passed (81) · SPEC_TARGETED_EXIT=0 ``` The first one parses `packages/client/README.md` with the TypeScript parser and validates the manifest it finds against the install contract — it is the pin that a README edit in that package could break. **Lint.** Zero files in this diff are in eslint's population, measured from eslint's own config rather than assumed, with a control from the same tree that must read the other way: ``` ESLint#calculateConfigForFile packages/types/README.md rules: 0 ignored: true .changeset/18915-published-readme-examples-compile.md rules: 0 ignored: true packages/types/src/index.ts rules: 6 ignored: false # control ``` `eslint.config.mjs` scopes every block to `{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, so no configuration in this diff can move an untouched file's verdict either. **Control bytes.** `grep -naP` for the C0 range over every changed path: no hits; a fixture carrying one byte in that range hits, so the scan is live. `pnpm check:nul-bytes` is among the 63 green gates. Authored by Claude Code, session `session_017ef78bLdybu3AffehKkhfk`. --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18612
Clause-②: yes (narrowing)
Retires
sqlandrelationshipfromCubeJoin. A cube join declares WHICH object itreaches; the ON clause is derived from the declared relationship between the two cubes'
objects and is never authored. Per maintainer ruling
5725370783(director batch #154 item 4,letter 2), ADR-0049 enforce-or-remove. The other remedy — executing the author's SQL — was
declined by that ruling and is ⛔ not reopened here.
What this head carries — round 3 closed the one gap
The gap the earlier body described (
check:adr-0087-registrationRED on purpose, and a fence onpackages/spec/src/migrations/registry.ts) is gone. The maintainer answered that fork withA — lift the fence, and the registration is now in-diff:
cube-join-sql-and-relationship-retiredpackages/spec/src/migrations/entries/semantic/18.*cube-join-sql-and-relationship-removedpackages/spec/src/conversions/registry.ts, chained intostep18.conversionIdsRETIRED_KEYS_BY_MAJOR[18]gainsdata/CubeJoin:sqlanddata/CubeJoin:relationship<!-- adr-0087: registered cube-join-sql-and-relationship-retired -->check:adr-0087-registrationandcheck:migration-registryare both exit 0 on this head.Round 3 added three things beyond the registration:
packages/metadata/src/plugin.tsrun_convertArtifactForwardbefore the strict parse, so acube persisted with the old
{ name, relationship, sql }shape is converted rather thanrefused. Pinned by
analytics.test.ts— "a persisted cube heals at the door" — with its ownlit control and the per-cube notice paths.
name's describe now states the convention it always had: the join KEY is the foreign-keyfield on the cube's own object, and the emission is
LEFT JOIN <name> <key> ON <base>.<key> = <key>.id.showcase_project→project, matchingtask.object.ts'sField.masterDetail('showcase_project');gap-fill.test.tspins every join key against thebase object's real field map rather than against a literal.
The same measurement also chose the retirement ROUTE
The ruling says 「
retiredKey()tombstones per the standing shape」.CubeJoinSchemais astrictObject, and for a strict shape AGENTS.md's standing shape is strict deletion plus aguidanceprescription, not aretiredKey()tombstone — the routeMetricSchema.filterstook one shape over in this same file (
packages/spec/src/migrations/entries/retired-keys/18.data__Metric__filters.tsstates it in as many words). Measured both ways on this tree:
retiredKey()tombstones:check:authorable-surfaceexit 1 — "2 key(s) were tombstonedwith no registered retirement", naming
data/CubeJoin:relationshipanddata/CubeJoin:sqland demanding those exact lines in
RETIRED_KEYS_BY_MAJOR(the fenced file). Probe reverted;tree hash restored byte-identical to HEAD.
check:authorable-surfaceexit 0, adjudicating the two baseline deletionsunder the authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 proof 4 it prints itself —
"2 baseline deletion(s) since 84ba4a8 carry their own proof: data/CubeJoin:relationship —
def reachable from the metadata-type roots; writing 'relationship' on it is REFUSED as an
unrecognized key", and the same for
sql.review at head
e177aa2686; this seat adopted that record at 2026-09-18T14:39Z(comment
5731599385). ⛔ They are not this seat's own runs.Either route needs the registration; it is now in-diff, in the table above. The route choice is
independent of that registration, and is called out here so an at-tier reviewer can overrule it cheaply.
Acceptance legs, both readings
LIT — an authored ON clause must be refused, in words a JSON author reads
CubeJoinSchema.safeParse({ name: 'other', sql: 'a.id = b.a_id' }){"name":"other","relationship":"many_to_one","sql":"a.id = b.a_id"}unrecognized_keys, message: "…was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an effect… Delete the key. A cube join has no authorable ON clause: it is DERIVED from the declared relationship between the two cubes' objects, as a foreign-key equality."DARK — a join that declares only its object must still parse
CubeJoinSchema.safeParse({ name: 'other' })sqlwas required (invalid_typeat pathsql){"name":"other"}Alias leg —
{ on: 'x' }, read once before and once afterUnrecognized key(s) on this cube join: `on`. Did you mean `on` → `sql`?Unrecognized key(s) on this cube join: `on`.followed by the derivation prescription, and no rename suggestionaliases: { on: 'sql' }is deleted rather than left pointing at a retired key: an alias naming akey the shape cannot accept answers the author with a second rejection — the
triggerPhrasefailure
packages/spec/src/shared/strict-object.tsrecords.onnow carries its ownguidanceentry, and both directions are pinned.
The census the ruling took, re-taken — and one correction
The ruling recorded 「authored cube
joinsin hotcrm, objectstack examples and cloud — 0 files」.Re-measured first-hand on this tree, objectstack is not 0:
examples/app-showcase/src/data/analytics/showcase.cube.tsauthors both keys, includingsql: '${showcase_delivery}.project = ${showcase_project}.id'— a live instance of the defect,an ON clause the runtime was silently replacing. Fixed here.
packages/services/service-analytics's own test fixtures, foundby
tscafter the keys leftz.input, not by grep. Two of them authoredrelationship: 'belongsTo'— a value the enum never declared, which is its own evidence thatnothing validated or read the key. All fixed here.
This does not move the ruling: those are in-repo producers, fixed in this same diff, and they
are what the retirement checklist calls for. It does mean 「zero producers ⇒ no conversion is
owed」 rests on the external census only, and that half was not re-measurable from here
(hotcrm and cloud are other repositories).
Consumer census, with a lit control, on this tree:
sqlanywhere in source: 0relationshipanywhere in source: 0 (native-sql-strategy.tswas checkedby name: it does not read either)
name: 8 acrossnative-sql-strategy.ts,objectql-strategy.tsandanalytics-service.tsWhat else moved, and why
packages/services/service-analytics/src/dataset-compiler.tsconstructed both keys perjoin (a constant
'many_to_one'and a synthesised ON string). The literal now carriesnamealone;
parentAlias, which existed only to build that string, is gone. No read site changes —analytics-service.ts:1178still readsnameonly, exactly as the ruling said.packages/spec/liveness/analytics_cube.json),which is the strict-deletion route's disposition and the opposite of the tombstone route's.
analytics_cubedrops 12deadto 10;state-counts.mdregenerated, README notes cellrewritten to describe the set it now has.
content/docs/references/data/analytics.mdxis regenerated, not hand-edited. TheCubeJointable is now one row and its description states the derivation — which is the docs half the
ruling asked for.
packages/spec/src/data/analytics-strictness-batchd.test.tskeeps its batch-D pin that anundeclared join key is refused by name; the fixture drops the two now-retired spellings so
the pin isolates what it always pinned. Three new pins beside it cover
sql,relationshipand
on.Verification
Two readings, kept apart on purpose — one is the reviewer's, one is this seat's.
① At-tier contract review, taken at head
e177aa2686, adopted by this seat at2026-09-18T14:39Z (comment
5731599385), run in its own detached worktree (freshpnpm install --frozen-lockfile, heavy steps underscripts/pm/os-verify-lock.sh, exit codescaptured before any pipe). All exit 0: spec
build·check:generated("All 15 generatedartifacts are up to date") ·
check:authorable-surface·check:liveness·check:migration-registry("225 semantic, 195 retired-key, 181 retired-def") ·check-adr-0087-registrationand--self-test·check-changeset-no-major·check:spec-docblock-symbol-anchors("3130 anchors across 1462 spec sources resolve") · eslintover the 11 changed source/test files ·
@objectstack/spec test488 files / 14190 tests ·@objectstack/service-analytics test112 files / 2403 tests · showcasegap-fill.test.ts13 tests · typecheck for spec, service-analytics and the showcase ·
check:exported-any,check:yaml-examples,check:dual-source-exports,check:entry-nameability,check:browser-reachable-entries,check:skill-examples,check:i18n,check:i18n-coverage,check:i18n-walk-parity.That review — same adoption, ⏱️ 2026-09-18T14:39Z — returned FAIL on one mechanical
blocker and nothing else, not a judgment defect.
The REQUIRED context
TypeScript Type Checkwas red ate177aa2686becausecheck:api-surface-declarationslanded on main atd8b12fca97, after this branch'smerge-base, so the branch carried neither the gate nor
packages/spec/api-surface-declarations/.② This seat's own reading of the fix, taken from the GitHub API at head
7caf92189a(⏱️ 2026-09-18T14:59Z 取): commit
59bd587aeamergesorigin/main, and7caf92189aregenerates the shards. The API reports that commit as
{"total":30,"additions":0,"deletions":30}over exactly three files —
api-surface-declarations/data.txt−12,root.txt−12,system.txt−6. A pure deletion: ⛔ not one line was added, so nothing was hand-written intoa generated artefact. That is byte-for-byte the shape the review predicted (each reshaped
declaration loses
sql: z.ZodString;and therelationshipenum block, propagated by typeinlining).
CI at this head, ⏱️ 2026-09-18T14:59Z 取: 0 failing check runs out of 33.
Build Core,Dogfood Regression Gate,Temporal Conformance (live PG + MySQL)andGoverned Surface Queue Guardare success;Lint & Repo Gatesis in progress;TypeScript Type CheckandTest Corehave not reported yet. ⛔ Not-yet-reported is notpassing, and this PR is not landed on that basis.
⛔ Not a complete account of what CI runs here: the 50 artifact-roster families, the 11 declared
wide-population families, the 6 path-scheduled CI jobs and the always-runs tail each sit outside
any derived total above. Not measured anywhere: repo-wide
pnpm test/pnpm typecheck,check:dual-build-cjs-loads, and the external hotcrm / cloud census (other repositories).priority:p1)reverts #18971 and deletes all 17 declaration shards. Whichever of the two lands second must
merge the other first; if #19024 goes in ahead of this PR, the regeneration commit above becomes
moot and its three files disappear with the rest of the snapshot. ⛔ That is a mechanical merge,
not a defect in either diff.
Acceptance notes
Noted, not filed — observations, no card:
packages/spec/liveness/analytics_cube.jsonstill recordspublicas an access-control flagthat gates nothing and
refreshKey.every/refreshKey.sqlas a caching block with noscheduler. Both are already recorded there with their measurements; ADR-0049 wants a decision
on each, and neither is this card. Successor: whoever picks up the
analytics_cubeledger'sremaining
deadrows.AnalyticsQueryRequestSchemareachesCubeJoinSchemaonly throughCubeSchema, so no RESTrequest surface changes. Successor: none.
Generated by Claude Code