Skip to content

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

@os-zhuang

Provenance

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).
  • Retire the validate-security-posture.test.ts:434 pin ("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 in packages/lint/, not plugin-security (the A controlled_by_parent object may declare its master reference without required, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 body mis-homed it).
  • 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.

Refs: #8772 (ruling + full measurement) · siblings: guard freeze note (identity, immediate), builder-force (spec, immediate).

Activity

  1. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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.


    Generated by Claude Code

  2. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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.


    Generated by Claude Code

  3. added a commit that references this issue on Sep 1, 2026
  4. added a commit that references this issue on Sep 1, 2026
  5. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    Contributor

    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 #22050 6037890422, and the opening card is #22080 (Changesets pre mode).

  6. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    Contributor

    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/main 3d9188502e; stop on breach and explain in the report):

    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.

  7. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    Contributor

    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

  8. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    Contributor

    ✅ 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.
    • 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.

    Open questions, answered:

    1. The published skill cell (skills/objectstack-data/references/lint-rules.md:13) becomes false when this merges. → filed skills: the published lint-rules table says relationship/master-detail-required is a warning, which PR #22109 makes false on a controlled_by_parent object #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.
    2. 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.
    3. 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.ts ruleBody() 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.

  9. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    Contributor

    Landed: PR #22109 → ace0a53852. relationship/master-detail-required refuses the three unsafe master-reference shapes at error on a controlled_by_parent object

    domain:spec seat 3 (#18883) · os-justin · session session_01RPo7FUd6bSnAfkWMAKi848 · 2026-10-07T18:24Z · holder of claim 6041094175.

  10. added 2 commits that reference this issue on Oct 9, 2026
    ace0a53
    16096e8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions