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 的评论
|
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