fix(lint): relationship/delete-behavior suggestion drops set_null on master_detail - #13668
Conversation
…master_detail The `relationship/delete-behavior` suggestion in `lintDataModel` told an author an undeclared `master_detail.deleteBehavior` could be `cascade`, `restrict`, or `set_null`. Since #9689 (PR #11406, maintainer ruling 2026-08-19), an authored `deleteBehavior: 'set_null'` on a `master_detail` field is a named parse-time rejection, so following the suggestion literally walked an author into a publish-time error. The message now enumerates only the two values FieldSchema actually accepts on a master_detail (cascade/restrict — matching object.form.ts's master_detail deleteBehavior options), and keeps the outcome-naming courtesy of the parse-time rejection message: it still names set_null to say plainly it is not honored on this type, and points to `lookup` for the case where children must survive the parent. The `fix` payload was already correct and is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 5 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 4947eb12026542fd849b442d930ec949ec979696 && git checkout 4947eb12026542fd849b442d930ec949ec979696
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 967402a12d6c7f43361df68357e29bba93465e47 387df3e81891bdf28f85b9480d64694cbf741b26 && git checkout -B drift-repro 967402a12d6c7f43361df68357e29bba93465e47 && git merge --no-ff 387df3e81891bdf28f85b9480d64694cbf741b26
node scripts/docs-audit/affected-docs.mjs --json 967402a12d6c7f43361df68357e29bba93465e47 |
⏸ Evicted in the same batch as #13659 — deliberately holding the re-queue for a few minutes, and saying why
What happenedQueue build 33363096644 went red and evicted both PRs in its batch — this one (enqueued 06:15:02Z) and #13659 (06:09:43Z). Failure:
|
| #13659 alone | reading | this PR's next step |
|---|---|---|
| passes | the failure is not #13659's, and the batch is the remaining variable | re-queue this one alone; a red then points at this PR and is mine to root-cause |
| fails again, same signature | reproducible and independent of this PR entirely | re-queue this one normally; the defect is packages/cli's, per #13683 |
⛔ Not a flake dismissal, ⛔ not an empty commit, ⛔ no test skipped or quarantined, ⛔ no re-run spent yet on this PR — its single allowance is intact.
The PR itself is unchanged and green: 33 checks, 0 failed, mergeable_state: clean, ACCEPTed on #11668. Nothing here is a defect in the change.
Re-queueing as soon as #13659 resolves.
Generated by Claude Code
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33364345029 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
✅ The sequenced experiment resolved — and this PR's own red is a validity guard, not a behaviour change. Re-queueing once.
1. The experiment: #13659 alone PASSED ⇒ the
|
| run | branch | outcome |
|---|---|---|
| 33363096644 | …/pr-13659-713f83f5… |
❌ run-dev-unbuilt-workspace |
| 33364345029 | …/pr-13668-713f83f5… |
❌ a different test entirely |
| — | …/pr-13659-94decf8a… |
✅ success |
⇒ The "grouped-queue neighbour" hypothesis I raised on #13683 is weakened, not confirmed: the two PRs' failures came from separate builds and were different tests. #13683 updated accordingly.
2. This PR's own red: Temporal Conformance (live PG + MySQL)
AssertionError: this run took over a second, so second-precision stamps could have differed too:
expected 1006 to be less than 1000
⭐ That assertion is not about the product. It is the test's own validity precondition. Read at packages/drivers/driver-sql/src/sql-driver-11224-update-stamp-precision.test.ts:384-391:
// Monotone regardless (the invariant), and — the point — the run spans
// less than the full second a truncated stamp would need to distinguish
// any two of these at all.
const span = stamps[stamps.length - 1] - stamps[0];
expect(span, 'this run took over a second, so …').toBeLessThan(1_000);The test needs the whole run to fit inside one second, so that a truncated second-precision stamp could not have distinguished the updates — which is what makes the following distinctness assertion mean anything. At span = 1006 the test detected that its own premise failed by 6 ms. Monotonicity — the actual invariant, asserted on the line above — held.
⇒ The experiment was invalidated; the product was not falsified.
AssertionError: …)才指向真实的行为改变」). That heuristic is sound in general, and it is what I used to refuse a flake reading on #13659. Here it misfires, because the assertion is a timing measurement. The discriminator is not AssertionError vs timeout — it is what the assertion measures.
3. Why it is not this PR's
This PR's diff: packages/lint/src/data-model-rules.ts (one message string), packages/cli/test/data-model-rules.test.ts (two assertions), one changeset. The failing test drives live PostgreSQL and MySQL through packages/drivers/driver-sql. No mechanism connects them.
⭐ And it is known ground: #11572 (closed, domain:engine) is this exact guard — "a probabilistic guard that reds with CORRECT code on a fast runner". Today's is its mirror image (too slow, not too fast), which that card did not cover. Filed as a fresh finding rather than reopening another lane's closed card.
4. Re-queueing — the one allowance, now spent
ROUNDS = 6, MIN_GAP_MS = 5 ⇒ ~25 ms of deliberate spacing plus 6 UPDATE round-trips against two live servers, inside a hard 1000 ms wall. Ordinarily ample; under runner contention, 1006 ms happens. This is the case the rules reserve a re-run for.
⛔ Not skipping, disabling or quarantining the test. ⛔ No empty commit. ⛔ Not touching driver-sql from this PR — that would widen a lint-message change into another lane's package. If it reds again with this signature, I stop re-queueing and it belongs to the new finding, not to this PR.
The PR itself remains green and unchanged: 33 checks, 0 failed, ACCEPTed on #11668.
Generated by Claude Code
Fixes #11668
What
lintDataModel'srelationship/delete-behaviorsuggestion (packages/lint/src/data-model-rules.ts) told an author an undeclaredmaster_detail.deleteBehaviorcould becascade,restrict, orset_null. Since #9689 (PR #11406, maintainer ruling 2026-08-19), an authoreddeleteBehavior: 'set_null'on amaster_detailfield is a named parse-time rejection (packages/spec/src/data/field.zod.ts) — a detail row cannot outlive its master, so the engine resolves every value exceptrestricttocascadeon this type. Following this suggestion's own enumeration literally walked an author into that rejection at publish time.Before:
After:
The corrected enumeration (
cascade/restrict) is not my own choice — it is read off two existing authorities that already agree:field.zod.ts'smaster_detail+set_nullcheck, whose rejection message explicitly walks the author to'restrict'or'cascade'(or omit the key).object.form.ts'smaster_detaildeleteBehaviorselect options, which already list exactly['cascade', 'restrict'](split from thelookupbranch's options for the same FieldSchema acceptsdeleteBehavior: 'set_null'on amaster_detail, and the engine silently resolves it tocascade#9689 reason).The message keeps the issue's suggested "outcome-naming courtesy": it still mentions
set_null, but only to say it is not honored on this type and to point atlookupas the alternative — it no longer offers it as a declarable choice.⛔ Not touched, per the card's Zone 1: the rule's logic (still consumes RAW authored objects —
def.deleteBehavior === undefinedstill means "not authored"; #9689's idempotent-materialization change never reaches this rule), and thefixpayload (deleteBehavior: 'cascade'), which was already correct.Tests
packages/cli/test/data-model-rules.test.ts— extended the existing'suggests an explicit deleteBehavior on master_detail'test with two assertions: the message no longer contains the stalecascade/restrict/set_nullenumeration, and does contain the corrected(cascade/restrictopening. No existing test pinned the old message text (only severity), so nothing else needed updating.@objectstack/lintsuite and the full@objectstack/clitypecheck/build closure, plus the dispatch-derived gate family for this diff — see the report comment on lintrelationship/delete-behaviorsuggestion still namesset_nullas declarable on a master_detail — a spelling #9689 makes a parse-time rejection #11668 for the complete list and a repo-head387df3e81reference.Generated by Claude Code
Generated by Claude Code