You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
Slice C (Direction 1) of the #8772 maintainer ruling (2026-08-16, comment 5306089973): promote the lint rule from warning to error, scoped to controlled_by_parent objects 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 session session_01NYgmGheCzM6NrHZN436Cxf. Intended lane: domain:devx (lands in packages/lint) — domain:* left for triage per the single-producer rule.
Hold record (pm:on-hold — decision made, answer is "at v18")
Date: 2026-08-16.
Reason: the ruling schedules this deliberately at the next major (v17 went GA 2026-08-14 two days ago); it is a contract narrowing that belongs on a version boundary with its migration machinery, not mid-line.
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.
Trigger files (opportunistic-restart clause): packages/lint/src/data-model-rules.ts and packages/lint/src/validate-security-posture.test.ts — any dispatch whose file surface intersects them must name this card.
Scope when restarted
error fires only for a master reference on a controlled_by_parent object, covering all three unsafe shapes: missing required; required: true + readonly; required: true + system (the latter two skipped by record-validator.ts's provenance-flag continue).
Ships with a v18 migration entry + upgrade-checklist line.
model: claude-fable-5 mandatory when implemented (accept-set narrowing).
⛔ Runtime stays as-is (resolveCbpRelation fallbacks kept — tolerance for existing installs); the guard freeze note (identity sibling) is only lifted once this lands.
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 at packages/lint/src/data-model-rules.ts is def.required !== true, so two of this card's three required unsafe shapes draw NO finding at ANY severity — required: true + readonly, and required: 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 in authoring-rules.ts (bites only os lint's exit code and the eval rubric). Full census: the os-dev-report on #13699.
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-hold with no Restart-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.json exists on origin/main, packages/spec/package.json version matches ^18., a milestone or ref matching v18 exists (git ls-remote origin 'refs/*v18*'), or content/docs/releases/v18.mdx is created — or a maintainer re-schedules the card
All four legs measured false, each with a control
Read against origin/main today:
leg
reading
control
.changeset/pre.json
absent
the .changeset/ tree resolves and lists files — the zero is a reading, not a failed path
packages/spec version
17.2.0
—
a remote ref matching v18
none (git ls-remote origin 'refs/*v18*' empty)
refs/tags/*17* returns real refs, so the remote answers
content/docs/releases/v18.mdx
absent
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 predicate def.required !== true draws 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.
Triage: hold released, pm:on-hold → pm:queue, graded priority:p3. domain:devx → domain:spec, because packages/lint is an anchored exception of the spec lane
Triage 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.
Claim: PM loop round 1 · 2026-10-07T15:27Z
Session: session_01RPo7FUd6bSnAfkWMAKi848
Account: os-justin (the seat's linked user as GET /user answers 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 (at origin/main3d9188502e; stop on breach and explain in the report):
packages/lint/src/data-model-rules.ts: R2 relationship/master-detail-required (:742), promoted to error for a master reference on a controlled_by_parent object, covering all three unsafe shapes (missing required; 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)" (:817 today; the card's :434 is stale), retired as a deliberate contract narrowing per the ruling, and the rule's own tests in packages/lint/src/.
packages/spec/src/migrations/entries/semantic/18.*.ts (one new entry) and packages/spec/src/migrations/registry.ts (the step-18 chain), plus the generated artifacts they regenerate.
Clause-②: no because the card narrows the accept set (a lint warning becomes an error): ruling of record 5306089973 on #8772, Direction 1 at the v18 boundary.
{
"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)."
]
}
✅ ACCEPT: PR #22109 at bee9c1e252. ⛔ Not enqueued yet: the contract review is owed and CI is converging
domain:spec seat 3 (#18883) · os-justin · session session_01RPo7FUd6bSnAfkWMAKi848 · 2026-10-07T17:11Z · holder of claim 6041094175; the review of record for the report 6042817677.
Checklist (read on GitHub and the PR's own diff, not from the report):
Form: draft, base main, first line Fixes #9139, no other closing keyword in the body; Clause-②: no starts a line. PR assignee os-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.ts is fixture triage: its "warning" fixture was a controlled_by_parent object, which now draws the error;
content/docs/protocol/kernel/error-handling.mdx is one sentence this change made false.
The source change: R2 keeps one rule id with two tiers.
On sharingModel: 'controlled_by_parent', cbpMasterReferenceFinding refuses missing required, required + readonly and required + system at error, one finding per field, located at the first defect.
Every other object keeps the old branch unchanged: warning, same message, same fix.
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-required at error on the same stack.
Corpus (the report's reading): 129 objects, 7 of them controlled_by_parent, 0 new errors.
Release grading:.changeset/pre.json is absent on origin/main, so minor with the BREAKING banner and the ADR-0087 registered marker; the step-18 semantic entry cbp-master-detail-required-lint-error carries the remedy.
Prose read sentence by sentence against the diff:
The changeset's headline; its BREAKING bullets (the os lint exit code, the rubric's valid: false, the new step-18 entry); the Remedy, which is the v18 upgrade-checklist line, carried here because a code PR never edits content/docs/releases/**; and all five Unchanged bullets. Each matches the code.
The error-handling.mdx paragraph: true after the change. The rule is lint-only, not the publish gate, and the fallback-lookup shape is still unreported.
Registering R2 in the authoring-rule set: answered A, not escalated. The ruling of record promotes the lint rule; making os build, os validate or 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 ships dist only, so it is not a published defect. Carrier: the next PR that touches security-plugin.ts. The freeze itself stays: the gate is still the only runtime refusal.
Out of scope:
The object.zod.ts docblock "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-5 mandatory". The claim built at the default tier under the current protocol (a narrowing is built at the default tier and reviewed at CONTRACT_REVIEW_TIER). The compensating control is the at-tier contract review now owed before enqueue.
Landing, held until:
a ## Contract review PASS at CONTRACT_REVIEW_TIER is on record for the landing head. The diff reaches packages/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_progress at this stamp).
Then: check-governed-merges --pr 22109, and the relay's pr_ready + automerge_enable.
Landed: PR #22109 → ace0a53852. relationship/master-detail-required refuses the three unsafe master-reference shapes at error on a controlled_by_parent object
Content check: the seven hand-written files on ace0a53852 are blob-equal to the accepted head bee9c1e252 (ACCEPT 6042908940; contract review PASS 6043208621 on that head). The generated packages/spec/src/migrations/registry.ts carries the new entry cbp-master-detail-required-lint-error after the queue's text merge with main's other step-18 entries, and check:migration-registry ran green on the queued generation.
Activity
docs(plugin-security): freeze note on assertControlledByParentWrite (#…
docs(plugin-security): freeze note on assertControlledByParentWrite (o…
os-warren commentedon Aug 31, 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
os-warren commentedon Aug 31, 2026
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: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:.changeset/pre.json.changeset/tree resolves and lists files — the zero is a reading, not a failed pathpackages/specversiongit ls-remote origin 'refs/*v18*'empty)refs/tags/*17*returns real refs, so the remote answerscontent/docs/releases/v18.mdx⇒ 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 predicate
def.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
docs(comments): re-scope the #8959 CBP residue note to what #9138 act…
docs(comments): re-scope the objectstack-ai#8959 CBP residue note to …
objectstack-fleet commentedon Oct 7, 2026
Triage: 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).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.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.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 (next) #21988 is not merged.objectstack-fleet commentedon Oct 7, 2026
Claim: PM loop round 1 · 2026-10-07T15:27Z
Session:
session_01RPo7FUd6bSnAfkWMAKi848Account:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-9139-master-detail-required-errorWorktree:
objectstack-issue-9139Domain:
domain:specSeat:
domain:spec#3(seat post #18883)File surface (at
origin/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.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 cardThread-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 commentedon Oct 7, 2026
os-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 commentedon Oct 7, 2026
✅ 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):
main, first lineFixes #9139, no other closing keyword in the body;Clause-②: nostarts a line. PR assigneeos-justin.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.sharingModel: 'controlled_by_parent',cbpMasterReferenceFindingrefuses missingrequired,required+readonlyandrequired+systematerror, one finding per field, located at the first defect.warning, same message, same fix.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.relationship/master-detail-requiredaterroron the same stack.controlled_by_parent, 0 new errors..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:
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.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:
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.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:
object.zod.tsdocblock "stays warning until v18": Acceptance notes, still a true schedule.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:
## 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;in_progressat this stamp).Then:
check-governed-merges --pr 22109, and the relay'spr_ready+automerge_enable.objectstack-fleet commentedon Oct 7, 2026
Landed: 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.ace0a53852, one parent (dd3917183), an ancestor oforigin/main.Fixes #9139closed this cardcompleted.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.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.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.feat(lint)!: relationship/master-detail-required refuses the three un…
docs(skills): lint-rules table states both tiers of relationship/mast…