Repository navigation
lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139
Description
Activity
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 17, 2026 Evidence note for the eventual v18 implementer (domain:spec seat,
session_01PBjwYLS6BciTQW3c9xQiD2, from the #13699 census — that card is closed as a re-discovery of this one): the lint predicate atpackages/lint/src/data-model-rules.tsisdef.required !== true, so two of this card's three required unsafe shapes draw NO finding at ANY severity —required: true+readonly, andrequired: true+system(measured, not read). A one-line severity flip on the existing rule therefore under-delivers this card's own scope while reading as done; the implementation must extend the predicate to all three shapes. Also measured for pricing: the CBP-scoped escalation this card rules turns 0 in-tree corpora red today, and the rule is not registered inauthoring-rules.ts(bites onlyos lint's exit code and the eval rubric). Full census: the os-dev-report on #13699.
Generated by Claude Code
Hold-condition completion (triage seat, H9 patrol of 2026-08-31). ⛔ No state change, no re-grade, no new decision — this transcribes an exit condition this card already states into the machine form the unlock scan can read.
The half-state patrol flags this card under H9:
pm:on-holdwith noRestart-when:line in either channel. That reading is correct on the literal, and it is only a form problem — the body already carries, verbatim:Named restart condition: the v18 major window opens (first v18 RC branch cut / the protocol-18 migration window becomes the active target). Whichever seat runs the v18 release board re-queues this card at that moment.
The unlock scan greps the literal key, so a correctly-named condition under a different heading is invisible to it. Adding the line in the house form established on #8345 (triage seat, 2026-08-19, on-hold weak-hit audit), unchanged:
Restart-when: the v18 cycle opens — first true of:
.changeset/pre.jsonexists on origin/main,packages/spec/package.jsonversion matches^18., a milestone or ref matching v18 exists (git ls-remote origin 'refs/*v18*'), orcontent/docs/releases/v18.mdxis created — or a maintainer re-schedules the cardAll four legs measured false, each with a control
Read against
origin/maintoday:leg reading control .changeset/pre.jsonabsent the .changeset/tree resolves and lists files — the zero is a reading, not a failed pathpackages/specversion17.2.0 — a remote ref matching v18 none ( git ls-remote origin 'refs/*v18*'empty)refs/tags/*17*returns real refs, so the remote answerscontent/docs/releases/v18.mdxabsent the directory lists v9 … v17 ⇒ v18 has not opened; this hold is sound and stands. The existing Trigger files list (
packages/lint/src/data-model-rules.ts,packages/lint/src/validate-security-posture.test.ts) is unchanged and keeps its opportunistic-restart role — the two mechanisms answer different questions, which is why #8345 kept both: a trigger-file list over-fires on incidental churn in those files and never fires on "v18 opened".Freshness checked before writing: the most recent comment (2026-08-31T08:12:19Z, the #13699 census evidence note) confirms the card is still parked for v18 and changes nothing about the hold.
⚠️ That note also matters to whoever implements this — it measures that the existing predicatedef.required !== truedraws no finding at any severity for two of this card's three unsafe shapes, so a one-line severity flip would read as done while under-delivering the card's own scope.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026 objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsTriage: hold released,
pm:on-hold→pm:queue, gradedpriority:p3.domain:devx→domain:spec, becausepackages/lintis an anchored exception of the spec laneTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T13:23Z. ⛔ Not a claim, ⛔ not a dispatch.Released on the maintainer's order in the triage seat's chat: 「你应该先解锁 v18 所有的卡片」, then 「同意」 to the plan. #15193 (the v18 gate) closed on that word (
6037915987). The ruling record is #220506037890422, and the opening card is #22080 (Changesets pre mode).- Restart condition: "the v18 cycle opens … or a maintainer re-schedules the card". The maintainer has done so.
- Scope stands as ruled: Direction 1 of A
controlled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772. Promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent. - Read first: the evidence note on this thread from [finding] relationship/master-detail-required is a lint WARNING while object.zod.ts's own docblock argues the unguarded shape "arms the worst measured failure shape" — severity judgment for the contract owner #13699's census (2026-08-31, on this thread). The lint predicate is
def.required !== true, so two of the card's three unsafe shapes draw nothing today. The promotion covers all three, or the PR says which it leaves and why. - Why p3: an authoring gate tightens. No runtime defect waits on it.
- The release state: a breaking change that lands before release(v18): enter Changesets pre mode on main (
changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080 is gradedminor, with its BREAKING banner and ADR-0087 disposition. After release(v18): enter Changesets pre mode on main (changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080 it is gradedmajor. ⛔ chore: version packages #21988 is not merged.
objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsClaim: PM loop round 1 · 2026-10-07T15:27Z
Session:session_01RPo7FUd6bSnAfkWMAKi848
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-9139-master-detail-required-error
Worktree:objectstack-issue-9139
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main3d9188502e; stop on breach and explain in the report):packages/lint/src/data-model-rules.ts: R2relationship/master-detail-required(:742), promoted toerrorfor a master reference on acontrolled_by_parentobject, covering all three unsafe shapes (missingrequired;required: true+readonly;required: true+system). Other objects keep today's verdict.packages/lint/src/validate-security-posture.test.ts: the pin "stays silent on step 2: ANY master_detail (not marked required)" (:817today; the card's:434is stale), retired as a deliberate contract narrowing per the ruling, and the rule's own tests inpackages/lint/src/.packages/spec/src/migrations/entries/semantic/18.*.ts(one new entry) andpackages/spec/src/migrations/registry.ts(the step-18 chain), plus the generated artifacts they regenerate..changeset/9139-*.md.- ⛔ Not
content/docs/releases/**(no code PR edits release notes); ⛔ not the runtime (resolveCbpRelationand the guard stay as they are).
Container & model:M,mode:subagent,model: opus(--tier: no path-derived mandate; a contract-face narrowing, built at the default tier; the contract review is owed before enqueue from an isolated subagent atCONTRACT_REVIEW_TIER)
Clause-②: no
Responsibility:n/a — not a defect card
Thread-read: 6038892492
Serial constraints cleared:packages/spec/src/migrations/registry.tsis in PR feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) #22094 (Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458, seat 2) and PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 (service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898, seat 1's residual claim); both append to the step-18 chain. Ordinary concurrency, not a single-claim path: this card takes the next free order, and whichever lands later mergesmain. No open PR touchesdata-model-rules.tsorvalidate-security-posture.test.ts(13 open PRs' file lists read at this stamp).
Clause-②: nobecause the card narrows the accept set (a lint warning becomes an error): ruling of record5306089973on #8772, Direction 1 at the v18 boundary.objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsos-dev-report
{ "issue": 9139, "status": "done", "branch": "claude/issue-9139-master-detail-required-error", "pr": "https://github.com/objectstack-ai/objectstack/pull/22109", "session": "session_01RPo7FUd6bSnAfkWMAKi848 — the PM's id (mode:subagent); identity is the branch named in Claim 6041094175, verified as the newest Claim on #9139", "premise_still_valid": true, "summary": "R2 relationship/master-detail-required in packages/lint/src/data-model-rules.ts now has two tiers under one id. On a sharingModel: 'controlled_by_parent' object, every master_detail field is refused at error in all three unsafe shapes: required absent or false; required:true + readonly:true; required:true + system:true. One finding per field, located at the first defect; the fix names every edit. On every other object the verdict is byte-identical to before: a warning on a missing required, silence on the flagged shapes. The step-2 pin in validate-security-posture.test.ts is REVERSED, not removed. It still asserts the CBP no-relation rule is silent, because resolveCbpRelation keeps resolving a non-required master_detail at runtime, and it now also asserts the same stack draws R2 at error. Also in the diff: the step-18 semantic entry cbp-master-detail-required-lint-error and the regenerated registry; a minor changeset with the BREAKING banner, Clause-②: no (narrowing) and ADR-0087 'registered'; and one sentence in content/docs/protocol/kernel/error-handling.mdx that this change made false. Runtime untouched; R2 not registered into AUTHORING_RULES. Hypotheses: H1, H2, H3, H4 and H5 hold. H6 is falsified as worded: there is no order to take, because the registry is sorted by entry id, one file per entry. H5 census at b04a5295f + change: 129 objects / 7 controlled_by_parent / 0 new errors / 0 warnings, positive controls reached in every corpus.", "tests": "All at final head bee9c1e25 unless noted. Exit codes were captured before any pipe; lock runs are read from their VERDICT line. (1) Derived gates: dispatch-gates --commands gave 113 commands; 112 exit 0 and 1 exit 3. The exit 3 is check-plugin-teardown-shape --self-test, PREREQUISITE NOT MET because its positive control commit 621a4876 is outside this shallow clone. NOT MEASURED, not a finding. --ran reconciliation: '113 derived famil(ies) accounted for — 112 run, 1 NOT-MEASURED, 0 UNRUN'. Quoted lines: check-adr-0087-registration '[BREAKING+bang+clause-②-narrowing] registered cbp-master-detail-required-lint-error (new here)'; check:migration-registry 'registry.ts is current (382 semantic…)'; check-issue-citations 'every citation this change adds resolves' (the first pass was red on a #8772 citation, allocated-but-absent, and was fixed by citing the card); check:nul-bytes OK; check:doc-authoring clean. (2) pnpm --filter @objectstack/lint test: 122 files / 5665 passed. pnpm --filter @objectstack/lint typecheck: green, 'check:test-typecheck: OK'. Both VERDICT command-exit 0. (3) spec: vitest src/migrations + scripts/build-migration-registry-entry.test.ts + scripts/step18-rationale-merge.test.ts passed 5 files / 191 tests. check:generated 'All 15 generated artifacts are up to date', run on top of 3f6d9ca97; spec is unchanged since. (4) cli, a consumer of R2 findings: vitest --project unit passed 259 files / 3795 tests; cli typecheck green (VERDICT command-exit 0). The integration tier is declared to CI. (5) ESLint narrowing, proven: --format json over the 6 touched .ts files counted 6 files / 0 errors / 0 warnings. --print-config resolves all 6 in-config; the .md/.mdx files are 'File ignored because no matching configuration was supplied'. parserOptions is {ecmaVersion, sourceType} with no project, so no type-aware linting can move an untouched file's verdict. (6) Reverse verification from the committed fix, with data-model-rules.ts restored to b04a5295f: 10 failed / 142 passed. The 10 were the 4 CBP shapes, several-defects, every-field, the array form, 2 ObjectSchema.create refusals and the reversed step-2 pin. Controls and non-CBP cases stayed green. Direction: turn red, as predicted. Restore was via git checkout HEAD --, proven by the blob hash equal to HEAD's and an empty git diff HEAD, under a trap with absolute paths. No dist ablation was needed: the lint tests import ./data-model-rules.js from src. (7) Probes: before/after shape table (before: CBP +readonly / +system gave none; after: error at .readonly / .system). score fixture probe: the old CBP 'warning' fixture now gives errors:1 valid:false, which is why the fixture moved to private. PR CI at report time: 32 check runs, 6 success, 2 skipped, 24 in_progress, 0 failed — in_progress, not awaited.", "mcp_calls": "0", "api_writes": "3 — all through the scripts/pm fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]). (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft) and opened #22109. dispatch.mjs exited 6 UNCONFIRMED because no run appeared within 90s, with Actions queued. It was NOT re-sent: a foreground wait read the run reach in_progress and the PR appear, and the stored body was byte-identical to the one sent (12933 bytes). (2) label-write --issue 22109 --assign os-justin, which became POST /issues/22109/assignees; relay run 37656267121 ended success, and the read-back matched. (3) this os-dev-report, which becomes POST /issues/9139/comments. Plus git push of the branch, which is not REST. No label added: the dispatch named none, and skip-changeset does not apply because a changeset is present.", "open_questions": [ { "question": "skills/objectstack-data/references/lint-rules.md:13 (a published skill) lists relationship/master-detail-required as 'warning' and describes it as 'a master_detail that isn't required'. Since this change that is true only off controlled_by_parent: under controlled_by_parent it is error and also covers required+readonly / required+system. Rule 3 says text this change makes false must be fixed. But skills/** is a Tier H governed surface outside the claim's file surface, and touching it would make PR #22109 Tier H. Left untouched; the conflict is named here.", "options": [ "A — the seat carves a skills-only PR (Tier H, maintainer approval): change the cell to 'warning (error on a controlled_by_parent object, where it also refuses required+readonly / required+system)'. Zero net lines, and the skill line ratchet is not moved.", "B — ride it on #22109: the whole PR becomes Tier H and waits for the maintainer's approval.", "C — leave it until the v18 release-notes sweep." ], "recommendation": "A, because it keeps #22109 on its ungoverned landing path while closing the published inaccuracy in the same release window. The edit is one table cell, so no line budget is spent." }, { "question": "H3: R2 still reaches only os lint (exit 1) and the generation rubric (valid:false). It is not in AUTHORING_RULES, so os build, os validate and the metadata save door neither refuse nor report the CBP shapes. Was that the ruling's intent, or should a CBP-scoped R2 become a gating authoring rule?", "options": [ "A — keep it lint-only, as delivered. This matches the ruling text (a promotion of the existing lint rule) and DIRECT_CALL_RATCHET's 'making [build] reject is a product decision'.", "B — register a CBP-only slice of R2 in AUTHORING_RULES at gating tier, on all three commands and the runtime gate's object writes. Then a save or publish of a stored object carrying the shape is refused: a further narrowing with its own census of stored rows, and its own ruling." ], "recommendation": "A for this card. B widens where the narrowing bites, from authoring hygiene to publish and save refusals over metadata at rest, which the ruling's 'runtime tolerates old ones' half argues against. It needs its own maintainer ruling if wanted." }, { "question": "packages/plugins/plugin-security/src/security-plugin.ts, in the paragraph above the FREEZE NOTE, says 'Direction 1 … has NOT landed … nothing warns on the way past' and that it 'goes stale when #9139 lands'. Once #22109 merges it is false. The #9139 body also says 'the guard freeze note … is only lifted once this lands'. This dispatch fences the runtime package, so neither was touched.", "options": [ "A — a comment-only follow-up in plugin-security (identity lane) that records Direction 1 as landed at lint tier (os lint only, not the publish gate), and keeps the freeze: the gate stays the sole runtime/publish enforcement for stored and raw-parsed metadata.", "B — the same follow-up plus a decision on lifting the freeze note, which is the maintainer's or the identity seat's call." ], "recommendation": "A, because the guard is still the only publish/runtime refusal for these shapes (see question 2), so the freeze's premise survives this landing. Lifting it would be a separate decision." } ], "out_of_scope_findings": [ "carrier: domain:spec seat (Tier H skills PR) · noted, not filed — skills/objectstack-data/references/lint-rules.md:13 severity cell now stale for controlled_by_parent (open question 1)", "carrier: identity lane / #9137 owner · noted, not filed — security-plugin.ts paragraph above the freeze note says Direction 1 'has NOT landed'; false once #22109 merges (open question 3)", "carrier: 承接者:无 · noted, not filed — object.zod.ts forceCbpMasterDetailRequired docblock and the sibling entry cbp-master-detail-required-forced say the lint 'stays warning until v18'. Still accurate as a schedule, so neither was edited; whoever next touches them may point at cbp-master-detail-required-lint-error.", "carrier: 承接者:无 · noted, not filed — authoring-rule-wiring.test.ts ruleBody() reads an exported rule's body as the text up to the next export. An error-emitting helper placed after an advisory rule is misattributed (measured once here: a false red, fixed by moving the helper up). The converse blind spot is inference only: an advisory rule that reaches error through a helper defined elsewhere would pass. No instance found." ], "gates": "113 derived at bee9c1e25: 112 exit 0, 1 exit 3 (check-plugin-teardown-shape --self-test, shallow clone, NOT MEASURED); --ran: 0 UNRUN. Package runs: lint test 5665/5665 + typecheck exit 0; spec migrations 191/191; cli unit 3795/3795 + typecheck exit 0; eslint 6 files 0/0.", "files_changed": [ ".changeset/9139-cbp-master-detail-required-error.md (+33)", "content/docs/protocol/kernel/error-handling.mdx (+6/-5)", "packages/cli/test/score.test.ts (+8/-2)", "packages/lint/src/data-model-rules.master-detail-required.test.ts (+160, new)", "packages/lint/src/data-model-rules.ts (+114/-2)", "packages/lint/src/validate-security-posture.test.ts (+19/-4)", "packages/spec/src/migrations/entries/semantic/18.cbp-master-detail-required-lint-error.ts (+49, new)", "packages/spec/src/migrations/registry.ts (+45, generated)" ], "line_counts": "8 files, +434/-13 vs merge-base bafb58bb0 (under the 5000 human-merge threshold); no governed path touched.", "deviations": [ "File surface: beyond the claim's list, the diff edits packages/cli/test/score.test.ts and content/docs/protocol/kernel/error-handling.mdx. score.test.ts is fixture triage: its 'warning' fixture was a CBP object and now measures the error weight. The .mdx sentence was made false by this change. Both are named in the PR body.", "Clause-②: the PR body line is verbatim from the claim ('Clause-②: no'); the changeset carries 'Clause-②: no (narrowing)', the arm that marks it breaking for the ADR-0087 and level gates.", "Release grading: .changeset/pre.json was absent on origin/main at b04a5295f (15:37Z), bafb58bb0 and aa71c4d9d (16:59Z), so minor + BREAKING + ADR-0087 'registered'.", "Model: the card body says 'model: claude-fable-5 mandatory'. The PM claim 6041094175 sets 'model: opus (no path-derived mandate)'. This run followed the claim; the conflict is named, not resolved here.", "Merged origin/main once (bafb58bb0, a cli-only commit, clean) before the final gate union; main has since moved to aa71c4d9d and was not re-merged.", "Relay pr_create returned exit 6 UNCONFIRMED (Actions queued). It was not re-sent: a foreground wait confirmed the PR, and the body read back byte-identical.", "Cleanup: worktree node_modules removed and worktree removed after this report was posted (see final message)." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorMore actions✅ ACCEPT: PR #22109 at
bee9c1e252. ⛔ Not enqueued yet: the contract review is owed and CI is convergingdomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-07T17:11Z · holder of claim6041094175; the review of record for the report6042817677.Checklist (read on GitHub and the PR's own diff, not from the report):
- Form: draft, base
main, first lineFixes #9139, no other closing keyword in the body;Clause-②: nostarts a line. PR assigneeos-justin. - Scope: 8 files, +434 / −13, no governed path. Six are in the claim's surface. Two more are named in the PR body and accepted:
packages/cli/test/score.test.tsis fixture triage: its "warning" fixture was acontrolled_by_parentobject, which now draws the error;content/docs/protocol/kernel/error-handling.mdxis one sentence this change made false.
- The source change: R2 keeps one rule id with two tiers.
- On
sharingModel: 'controlled_by_parent',cbpMasterReferenceFindingrefuses missingrequired,required+readonlyandrequired+systematerror, one finding per field, located at the first defect. - Every other object keeps the old branch unchanged:
warning, same message, same fix. - The rule is not registered in
AUTHORING_RULES, and the runtime is untouched, as the ruling of record (Acontrolled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #87725306089973) and the card's "Runtime stays as-is" require.
- On
- The pin is reversed, not removed: "stays silent on step 2" still asserts the CBP no-relation rule's silence (the resolver still resolves step 2), and now also asserts
relationship/master-detail-requiredaterroron the same stack. - Corpus (the report's reading): 129 objects, 7 of them
controlled_by_parent, 0 new errors. - Release grading:
.changeset/pre.jsonis absent onorigin/main, sominorwith the BREAKING banner and the ADR-0087registeredmarker; the step-18 semantic entrycbp-master-detail-required-lint-errorcarries the remedy.
Prose read sentence by sentence against the diff:
- The changeset's headline; its BREAKING bullets (the
os lintexit code, the rubric'svalid: false, the new step-18 entry); the Remedy, which is the v18 upgrade-checklist line, carried here because a code PR never editscontent/docs/releases/**; and all five Unchanged bullets. Each matches the code. - The
error-handling.mdxparagraph: true after the change. The rule is lint-only, not the publish gate, and the fallback-lookupshape is still unreported.
Open questions, answered:
- The published skill cell (
skills/objectstack-data/references/lint-rules.md:13) becomes false when this merges. → filed skills: the published lint-rules table saysrelationship/master-detail-requiredis awarning, which PR #22109 makes false on acontrolled_by_parentobject #22111 (derived sub-issue,pm:blocked,Blocked-by: #9139). It is Tier H, so it does not ride this PR and lands as its own PR after this one. - Registering R2 in the authoring-rule set: answered A, not escalated. The ruling of record promotes the lint rule; making
os build,os validateor the save door refuse would be a further narrowing with its own census and its own ruling, and nobody has asked for one. plugin-security's comment above the freeze note ("Direction 1 … has NOT landed") goes stale on merge. → Acceptance notes: the package shipsdistonly, so it is not a published defect. Carrier: the next PR that touchessecurity-plugin.ts. The freeze itself stays: the gate is still the only runtime refusal.
Out of scope:
- The
object.zod.tsdocblock "stays warning until v18": Acceptance notes, still a true schedule. - The
authoring-rule-wiring.test.tsruleBody()misattribution: Acceptance notes, inference only, no instance found.
Tier note: the card body asks for "
model: claude-fable-5mandatory". The claim built at the default tier under the current protocol (a narrowing is built at the default tier and reviewed atCONTRACT_REVIEW_TIER). The compensating control is the at-tier contract review now owed before enqueue.Landing, held until:
- a
## Contract reviewPASS atCONTRACT_REVIEW_TIERis on record for the landing head. The diff reachespackages/spec/src/**(the migration entry), so the review is owed; it is dispatched to an isolated at-tier reviewer; - every check is green on that head (20 of 32 were
in_progressat this stamp).
Then:
check-governed-merges --pr 22109, and the relay'spr_ready+automerge_enable.- Form: draft, base
objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsLanded: PR #22109 →
ace0a53852.relationship/master-detail-requiredrefuses the three unsafe master-reference shapes aterroron acontrolled_by_parentobjectdomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-07T18:24Z · holder of claim6041094175.- Landed: PR feat(lint)!: relationship/master-detail-required refuses the three unsafe master-reference shapes at error on a controlled_by_parent object #22109 merged through the merge queue at 2026-10-07T18:24Z as
ace0a53852, one parent (dd3917183), an ancestor oforigin/main.Fixes #9139closed this cardcompleted. - Content check: the seven hand-written files on
ace0a53852are blob-equal to the accepted headbee9c1e252(ACCEPT6042908940; contract review PASS6043208621on that head). The generatedpackages/spec/src/migrations/registry.tscarries the new entrycbp-master-detail-required-lint-errorafter the queue's text merge withmain's other step-18 entries, andcheck:migration-registryran green on the queued generation. - What now holds:
os lintand the generation rubric refuse, aterror, amaster_detailon acontrolled_by_parentobject that lacksrequired: trueor carriesreadonly: true/system: truewith it; every other object keeps the oldwarning. Runtime,os build,os validateand the save door are unchanged, as ruled (Acontrolled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #87725306089973, Direction 1). It ships asfeat(lint)!:minorwith its BREAKING banner, and the v18 upgrade-checklist line lives in the changeset's Remedy. - Follow-up released: skills: the published lint-rules table says
relationship/master-detail-requiredis awarning, which PR #22109 makes false on acontrolled_by_parentobject #22111 (the published skill cell this change made false) is unblocked by this closure; this seat takes it next. pm:dispatchedremoved from this closed card in the same act.
- Landed: PR feat(lint)!: relationship/master-detail-required refuses the three unsafe master-reference shapes at error on a controlled_by_parent object #22109 merged through the merge queue at 2026-10-07T18:24Z as
- added 2 commits that reference this issue
on Oct 9, 2026
Provenance
Slice C (Direction 1) of the #8772 maintainer ruling (2026-08-16, comment 5306089973): promote the lint rule from
warningtoerror, scoped tocontrolled_by_parentobjects only, landing at the v18 major boundary — not before. Carved into its own card on the maintainer's 2026-08-16 instruction (PM chat, verbatim: 「接受你的建议,开新卡,现有的可以关闭?」), recorded by PM sessionsession_01NYgmGheCzM6NrHZN436Cxf. Intended lane:domain:devx(lands inpackages/lint) —domain:*left for triage per the single-producer rule.Hold record (
pm:on-hold— decision made, answer is "at v18")packages/lint/src/data-model-rules.tsandpackages/lint/src/validate-security-posture.test.ts— any dispatch whose file surface intersects them must name this card.Scope when restarted
errorfires only for a master reference on acontrolled_by_parentobject, covering all three unsafe shapes: missingrequired;required: true+readonly;required: true+system(the latter two skipped byrecord-validator.ts's provenance-flagcontinue).validate-security-posture.test.ts:434pin ("stays silent on step 2: ANY master_detail (not marked required)") as a deliberate contract narrowing — never a drive-by test edit. Correction on record: that pin lives inpackages/lint/, notplugin-security(the Acontrolled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 body mis-homed it).model: claude-fable-5mandatory when implemented (accept-set narrowing).resolveCbpRelationfallbacks kept — tolerance for existing installs); the guard freeze note (identity sibling) is only lifted once this lands.Refs: #8772 (ruling + full measurement) · siblings: guard freeze note (identity, immediate), builder-force (spec, immediate).