Skip to content

feat(lint)!: relationship/master-detail-required refuses the three unsafe master-reference shapes at error on a controlled_by_parent object - #22109

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-9139-master-detail-required-error
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-9139-master-detail-required-error

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #9139

Clause-②: no

What this does

relationship/master-detail-required (R2 in packages/lint/src/data-model-rules.ts) now has two tiers under one rule id, as the maintainer ruling of 2026-08-16 (Direction 1, scheduled for the v18 boundary) orders.

  • On an object with sharingModel: 'controlled_by_parent', every master_detail field is refused at error in each of the three unsafe shapes: required absent (or false); required: true + readonly: true; required: true + system: true. One finding per field, located at the first defect (.required, .readonly or .system); the fix names every edit the field needs.
  • On every other object the verdict is unchanged: a warning for a master_detail without required: true, with the same message and fix, and silence on the two flagged shapes.

Before this change the predicate was required !== true at warning on every object. Two of the three shapes drew no finding at any severity, as the census note on #9139 measured. A one-line severity flip would have closed one shape and left two open, so the predicate covers all three. Each shape is pinned on its own.

Runtime is untouched, as the card's scope requires: resolveCbpRelation's fallbacks and the security gate stay as they are.

The mechanism hypotheses, measured

# hypothesis reading
H1 R2 at data-model-rules.ts:742, warning, required !== true, every object Holds at b04a5295f. Before-probe: CBP and non-CBP alike gave warning for absent / false, and nothing for +readonly / +system.
H2 the :817 pin filters to SECURITY_CBP_NO_RELATION, so promoting R2 leaves it green untouched Holds. The pin asserts only that the CBP no-relation rule (a mirror of resolveCbpRelation) is silent. That stays true, because the runtime still resolves step 2. The pin is reversed, not removed: see the pin section below.
H3 R2 bites os lint and the eval rubric only Holds. lintDataModel is called from lintConfig (os lint, exit 1 on any error) and from scoreMetadata (the generation rubric: valid requires zero errors). It is not an AUTHORING_RULES entry: authoring-rule-wiring.test.ts pins lintDataModel in DIRECT_CALL_RATCHET as lint-only. So os build, os validate and the metadata save door do not run it. check:i18n-coverage spawns os lint but tolerates a non-zero exit. Registration was not widened; see the open question in the report.
H4 #9138 (builder force) landed Holds. forceCbpMasterDetailRequired (object.zod.ts) forces an omitted required to true under CBP and refuses an explicit false. It skips the array field form and never inspects readonly / system. So through ObjectSchema.create only shapes 2 and 3 reach the lint, and both pass the builder untouched. Shape 1 reaches the lint from anything not built through create: a plain object literal, the array form, or raw parse of stored metadata. Pinned with real ObjectSchema.create objects.
H5 0 in-tree corpora turn red Holds. Census below.
H6 take the next free order in the step-18 chain Falsified as stated. There is no order to take: entries are one file each under entries/semantic/, and gen:migration-registry sorts the registry by id. The new entry is cbp-master-detail-required-lint-error. Whichever of this PR, #22094 and #21974 lands later merges main and re-runs the generator. Per the entries README's measured table, a driver-less merge conflicts only when two new ids are adjacent in sort order. Whether that holds against those two PRs' ids is NOT MEASURED.

The step-2 pin: reversed, not removed

validate-security-posture.test.ts "stays silent on step 2: ANY master_detail (not marked required)" pinned the step-2 shape as supported. The ruling retires that reading as a deliberate contract narrowing. Under H2 the pin's assertion is about the runtime mirror, and that half stays true, so the pin now asserts both halves on one stack:

  1. the CBP no-relation rule stays silent: resolveCbpRelation still resolves a non-required master_detail, and metadata at rest keeps loading;
  2. the same declaration draws relationship/master-detail-required at error from lintDataModel.

The pin's title and comment state the narrowing. The pin is kept rather than deleted, so a later change that re-tolerates the shape at authoring time, or drops the runtime tolerance, turns it red.

Census (H5), at b04a5295f plus this change

Every *.object.ts outside tests and fixtures (111 files, 0 import failures), plus the CLI's golden eval corpus and the multi-package example's two sub-stacks. Each corpus was linted with its own controls appended to its own array: one positive control per unsafe shape and a clean negative control.

corpus objects controlled_by_parent R2 error R2 warning controls
examples/app-crm 6 1 0 0 3 reached, neg clean
examples/app-showcase 22 2 0 0 3 reached, neg clean
examples/app-showcase (external sub-stack) 2 0 0 0 3 reached, neg clean
examples/app-todo 1 0 0 0 3 reached, neg clean
examples/app-multi-package (core, orders) 2 0 0 0 3 reached, neg clean
packages/cli DEFAULT_METADATA_EVAL_CORPUS 11 4 0 0 3 reached, neg clean
packages/platform-objects 46 0 0 0 3 reached, neg clean
packages/plugins, services, metadata-core, qa, create-objectstack 39 0 0 0 3 reached, neg clean
total 129 7 0 0

No stored in-tree metadata turns red.

v18 upgrade-checklist line (for the v18 release notes)

On every object with sharingModel: 'controlled_by_parent', give each master_detail reference required: true and remove any readonly: true or system: true from it. os lint now refuses the missing-required, required + readonly and required + system shapes there at error (relationship/master-detail-required). An object authored through ObjectSchema.create already gets required: true when the key is omitted, so the edit there is dropping the flag.

The changeset's Remedy section carries this line verbatim. The step-18 semantic entry cbp-master-detail-required-lint-error carries the same prescription for os migrate meta. This PR does not touch content/docs/releases/**.

Release grading

.changeset/pre.json is absent on origin/main: read at b04a5295f (2026-10-07T15:37Z) and again at bafb58bb0 before this push. So the changeset grades @objectstack/lint and @objectstack/spec at minor, with the BREAKING banner, Clause-②: no (narrowing) in its body, and the ADR-0087 disposition registered cbp-master-detail-required-lint-error.

Files

  • packages/lint/src/data-model-rules.ts: R2's CBP tier (cbpMasterReferenceFinding), placed above the first exported rule. authoring-rule-wiring.test.ts reads a rule body as the source text up to the next export, so an error-emitting helper placed below lintLegacyOrganizationComposites was read as that advisory rule emitting error. That was measured red once, then fixed by moving the helper.
  • packages/lint/src/data-model-rules.master-detail-required.test.ts (new): 19 cases. Each unsafe shape at error with its path; two clean controls; one finding for several defects; every master_detail field; the array field form; a lookup ignored; non-CBP unchanged across 4 sharing models plus the two flagged shapes; and real ObjectSchema.create objects.
  • packages/lint/src/validate-security-posture.test.ts: the step-2 pin, reversed as described above.
  • packages/spec/src/migrations/entries/semantic/18.cbp-master-detail-required-lint-error.ts and the regenerated registry.ts. The entry carries no tracker id and no call spellings. spec-changes.json and the upgrade guide do not project step 18 yet, and check:generated reports all 15 artifacts current.
  • packages/cli/test/score.test.ts: fixture triage. The "warning" fixture of "suggestions cost less than warnings cost less than errors" was a CBP object, so it now measured the error weight. Probe: counts.errors 1, valid: false, with R2 at error. Its assertions still passed, but for a different reason. The fixture moves to sharingModel: 'private' and two assertions pin it to the warning tier.
  • content/docs/protocol/kernel/error-handling.mdx: this change made one sentence false ("does not report the readonly, system … shapes at all"). It now names os lint's error tier and its reach.
  • .changeset/9139-cbp-master-detail-required-error.md.

Verification (final head bee9c1e25)

All readings below were taken at bee9c1e25, after the last commit. Each exit code was captured before any pipe.

  • Derived gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gave 113 commands. All 113 ran at bee9c1e25: 112 exit 0, 1 exit 3. The exit 3 is node scripts/check-plugin-teardown-shape.mjs --self-test refusing its own prerequisite, because its positive control is pinned to commit 621a4876 outside this shallow clone. That is NOT MEASURED, not a finding; CI runs it on a full checkout. Reconciled with --ran: "113 derived famil(ies) accounted for — 112 run, 1 NOT-MEASURED, 0 UNRUN". Gate lines quoted from the run:
    • check-adr-0087-registration: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing] registered cbp-master-detail-required-lint-error (new here: …)"
    • check:migration-registry: "registry.ts is current (382 semantic, 247 retired-key, 218 retired-def)"
    • check-issue-citations: "every citation this change adds resolves"
    • check:nul-bytes: "OK … no raw ASCII control bytes"
    • check:doc-authoring: "17816 customer-facing string(s) … clean"
  • @objectstack/lint: pnpm --filter @objectstack/lint test passed 122 files / 5665 tests. pnpm --filter @objectstack/lint typecheck was green; "check:test-typecheck: OK".
  • @objectstack/spec: vitest run src/migrations scripts/build-migration-registry-entry.test.ts scripts/step18-rationale-merge.test.ts passed 5 files / 191 tests. pnpm --filter @objectstack/spec check:generated was "All 15 generated artifacts are up to date". That run was on top of 3f6d9ca97 (the registry regeneration); packages/spec has not changed since, and its per-artifact gates are in the 113 above.
  • @objectstack/cli (consumer that reads R2 findings): vitest run --project unit passed 259 files / 3795 tests. pnpm --filter @objectstack/cli typecheck was green. The integration tier is declared to CI; migrate-meta-engine-guidance.test.ts, which holds every semantic entry's printed prose, lives there.
  • ESLint, a proven narrowing. eslint --no-inline-config --format json over the 6 touched .ts files counted 6 files, 0 errors and 0 warnings. --print-config resolves all 6 inside the config. The .md / .mdx files answer "File ignored because no matching configuration was supplied". The config enables no type-aware linting (parserOptions is { ecmaVersion, sourceType }, with no project), so this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is CI's run.

Reverse verification. With data-model-rules.ts reverted to b04a5295f and the tests run from the committed state, 10 failed / 142 passed. The failures: all 4 CBP shape cases, several-defects, every-field, array form, the 2 ObjectSchema.create refusals, and the reversed step-2 pin. The controls and every non-CBP case stayed green, as expected. The file was restored with git checkout HEAD -- …, and the restore was proven by blob hash equal to HEAD's and an empty git diff HEAD.

Acceptance notes (not changed here)

  • skills/objectstack-data/references/lint-rules.md:13 lists this rule's severity as warning. That is now true only off controlled_by_parent. skills/** is a Tier H governed surface outside this claim's file surface, so it is left for the seat (see the report's open question).
  • packages/plugins/plugin-security/src/security-plugin.ts, the paragraph above the freeze note: "Direction 1 … has NOT landed … nothing warns on the way past". It says itself that it "goes stale when 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 lands". The dispatch fences the runtime package, so it is left for the seat.
  • The object.zod.ts docblock of forceCbpMasterDetailRequired, and the sibling entry cbp-master-detail-required-forced, say the lint "stays warning until v18". That is still accurate as a schedule, so neither was edited.
  • The census's earlier global red (showcase_field_zoo.f_master_detail) was already fixed on main (required: true). The global warning count is now 0.

Generated by Claude Code

claude added 8 commits October 7, 2026 15:39
…safe master-reference shapes at error on a controlled_by_parent object

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
…p-2 pin; add the v18 migration entry

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
…required-lint-error

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
…int's error tier and its reach

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
…ng test slices rule bodies by export)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
…ginal thread no longer resolves)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 5 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/data-modeling/objects.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/getting-started/common-patterns.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/permissions/authorization.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/permissions/index.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/permissions/permissions-matrix.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/permissions/rls.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/permissions/sharing-rules.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/protocol/kernel/error-handling.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/protocol/objectql/security.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/releases/v15.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/releases/v17/17-1.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/releases/v17/17-2.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/releases/v17/17-3.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))
  • content/docs/releases/v17/index.mdx (via controlled_by_parent (literal, a string literal in a comment on a changed line; a string literal in lintDataModel))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aa71c4d9d146861fc09d63363e4c8fb8286719fb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 34256403b5ff388bdf117502267d320ea30c5aa6 — the merge of head bee9c1e2525ec655c1ee87c51f91081d2cf08c06 into base aa71c4d9d146861fc09d63363e4c8fb8286719fb, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 34256403b5ff388bdf117502267d320ea30c5aa6 && git checkout 34256403b5ff388bdf117502267d320ea30c5aa6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa71c4d9d146861fc09d63363e4c8fb8286719fb bee9c1e2525ec655c1ee87c51f91081d2cf08c06 && git checkout -B drift-repro aa71c4d9d146861fc09d63363e4c8fb8286719fb && git merge --no-ff bee9c1e2525ec655c1ee87c51f91081d2cf08c06

node scripts/docs-audit/affected-docs.mjs --json aa71c4d9d146861fc09d63363e4c8fb8286719fb

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs aa71c4d9d146861fc09d63363e4c8fb8286719fb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bee9c1e2525ec655c1ee87c51f91081d2cf08c06
Local-runs: none

Isolated at-tier review of PR #22109 (card #9139) against origin/main at aa71c4d9d (merge-base bafb58bb0). Inputs: the card body and its six comments, the #8772 thread read as the card's rulings (the issue itself answers 404 on REST; its comments still list; the ruling of record is 5306089973, superseding 5299877121), the PR body, file list and net diff, and the head's check-runs. The seat's ACCEPT 6042908940 was treated as a claim to test. Nothing was built, run or re-run; every reading below is off git objects and the REST API.

Check-runs on this head, read 2026-10-07T17:23:57Z: 38 runs — 29 success, 5 skipped, 4 in_progress (Test Core 1/6, 2/6, 5/6, 6/6), 0 failed. Green at that reading: Lint & Repo Gates (carries check:migration-registry, ESLint, check:adr-anchors), Type Check · source gates (carries the spec generated-artifact family: check:generated --reconcile-only, check:spec-changes, check:upgrade-guide, check:authorable-surface, check:docs), Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Build Core, Dogfood Regression Gate 1/3-3/3, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset (both runs), Spec property liveness, and the card/branch/claim guards. An earlier reading ten minutes before had 13 in_progress, 0 failed. in_progress is recorded as such, not as a pass: this verdict is on the contract, and landing still waits for every check on this head to complete green.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. os lint and the generation rubric now refuse, at error, inputs they accepted before — only on a sharingModel: 'controlled_by_parent' object (packages/lint/src/data-model-rules.ts: lintDataModel → cbpMasterReferenceFinding). Newly refused: every master_detail field of such an object that names a reference target and is in one of three shapes — required absent or false (before: warning, so os lint exited 0 unless --strict, and the rubric stayed valid: true with a warning penalty); required: true + readonly: true (before: no finding at any severity); required: true + system: true (before: no finding). Reach verified from the callers, not the report: packages/cli/src/commands/lint.ts:603 pushes lintDataModel's issues and reads the exit code off errors.length (:1049-1052); packages/cli/src/lint/score.ts:123,155 runs lintConfig and sets valid only on zero errors. One finding per field at the first defect (.required, else .readonly, else .system), fix naming every edit — matches the 19 new cases, and fieldEntries reads the object-map and the array field form alike. Right, and exactly the ruling of record's scope (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 5306089973, item 2: all three shapes, controlled_by_parent only).
  2. Nothing else moves for a CBP object. required: true with neither flag is silent (control pinned); explicit readonly: false / system: false is silent (pinned); a master_detail naming no reference still takes R1 relationship/missing-reference and continues before R2, unchanged; a lookup is not R2's subject (pinned). Right.
  3. Every non-controlled_by_parent object is byte-for-byte unchanged. The old branch survives as else if (def.required !== true) emitting severity: 'warning', the same message, path: fieldPath + '.required', fix: 'required: true'; the only textual change is the rule literal becoming the module const MASTER_DETAIL_REQUIRED, whose value is the same string. R3 and R5 untouched. The scope predicate obj.sharingModel === 'controlled_by_parent' is the accessor validate-security-posture.ts itself uses (:219), and no retired OWD alias maps onto controlled_by_parent (OWD_ALIAS_FIX names read, read_write, full, public, each failing closed elsewhere), so lint and runtime agree on which objects are in scope. Pinned across private, public_read, public_read_write and an unset model, and for both flagged shapes under private. The cli's existing R2 tests (packages/cli/test/data-model-rules.test.ts) use no CBP fixture, so their warning assertions still describe the code. Right.
  4. No publish-gate or runtime surface moves. R2 is not in AUTHORING_RULES (no master-detail-required in authoring-rules.ts), and DIRECT_CALL_RATCHET in authoring-rule-wiring.test.ts:112 pins lintDataModel lint-only, so os build, os validate and the metadata save door are unchanged. plugin-security is not in the diff, so resolveCbpRelation's three fallbacks and assertControlledByParentWrite stay as the card's "runtime stays as-is" requires. No export is added or removed in packages/lint (cbpMasterReferenceFinding and MASTER_DETAIL_REQUIRED are module-private) or in packages/spec (an entry file is generator input, not an entry point; Type Check · source gates is green). Right, and the changeset's "Unchanged" bullets say the same.
  5. The reversed step-2 pin pins BOTH facts. validate-security-posture.test.ts:827-836: cbpOnly(stack) → [] still pins the runtime fact — SECURITY_CBP_NO_RELATION is a point-for-point mirror of resolveCbpRelation, and the runtime still resolves a non-required master_detail at step 2, which the ruling keeps; and lintDataModel(stack.objects) → exactly one R2 error at objects[1].fields.order.required pins the new authoring fact on the SAME declaration. Deleting the first assertion would have un-pinned a runtime behaviour the ruling preserves and let the mirror drift from the resolver unseen; reversing records the narrowing in the pin itself, and the title and comment state it. Right — this is "retired as a deliberate contract narrowing, not a drive-by test edit", and the dev's reverse verification (restoring the rule reddens this pin) is consistent with the assertion as written.
  6. The step-18 semantic entry says what the code does. packages/spec/src/migrations/entries/semantic/18.cbp-master-detail-required-lint-error.ts: surface = the three keys on a CBP master reference as judged by os lint; replacement = required: true with neither flag, unchanged elsewhere; reason names the validator skip and the gate as sole refusal; acceptanceCriteria states the door (os lint and the rubric only — not os build, os validate or the save door; stored metadata not rewritten; the gate unchanged) and the census. Each sentence checked against items 1-4 and against forceCbpMasterDetailRequired (object.zod.ts:2868: returns early on required === true, never reads readonly or system, skips the array form) — all true. No tracker number in any printed field (the migrate-meta-engine-guidance pin derives its set from the registry, so the new entry is held on arrival), no call spelling (the entries README rule), no conversionIds (no D2 conversion exists). Right.
  7. The generated registry.ts rows are the regeneration. At the head, step 18's semantic rows (305) equal the entries/semantic/18.*.ts files as a set AND in sorted-by-id order; the new row's text equals the entry file's text indentation-normalised; it sits between cbp-master-detail-required-forced and cel-predicate-list-comparand-refused. check:migration-registry rides Lint & Repo Gates, success at the reading above. spec-changes.json and the upgrade guide project no step-18 entry on main either (feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 landed without touching them), so "nothing to regenerate there yet" is true, and Type Check · source gates (check:spec-changes, check:upgrade-guide) agrees. Right.
  8. Census (129 objects, 7 CBP, 0 new errors) spot-checked statically at the head: the 7 CBP objects are crm_opportunity_line_item, showcase_expense_line, showcase_invoice_line and the eval corpus's invoice_line, task, post_comment, expense_line; every master reference declares required: true and none carries readonly or system; packages/platform-objects declares no CBP object; the access-matrix.json hits are matrices, not lint input. Consistent with the maintainer's own survey on 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. Right.
  9. packages/cli/test/score.test.ts: the "warning" fixture WAS a CBP object and would now have measured the error weight while still passing; moved to private and pinned to errors: 0, warnings above 0. Necessary and correct. Right.
  10. content/docs/protocol/kernel/error-handling.mdx is a prose face (the dispatching seat's remit; read here, not ruled on): within the section's controlled_by_parent premise the rewritten paragraph is true after the change — os lint at error, not the publish gate, fallback-lookup still unreported. No other content/docs page names the rule.

② Semver level

  • The changeset matches what the diff publishes. .changeset/9139-cbp-master-detail-required-error.md: @objectstack/lint: minor, @objectstack/spec: minor, summary feat(lint)!:, a **BREAKING** banner, Clause-②: no (narrowing), and the ADR-0087 marker comment naming registered cbp-master-detail-required-lint-error — an id that exists in the registry at this head. Those are exactly the two packages whose published behaviour moves (lintDataModel's output; the exported MIGRATIONS_BY_MAJOR[18]); packages/cli changes a test only, and content/docs is not a package. The Remedy paragraph is the v18 upgrade-checklist line verbatim (the ruling's "migration entry + upgrade-checklist line"), carried in the one place a code PR may carry it. Right.
  • Grade. .changeset/pre.json is absent on origin/main at aa71c4d9d (git ls-tree origin/main .changeset/ lists 41 blobs, none pre.json; packages/spec is 17.7.0), so a BREAKING change ships minor with the banner and the disposition — major is what check-changeset-no-major refuses outside pre-mode, and the same form just landed on main (.changeset/21898-…, PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974). minor + BREAKING is right; major would be wrong today.
  • Clause-② line. The value no is truthful on both carriers: the card relaxes no accept set and grows no public surface (the judging question in execution-duties.md). The direction is a narrowing, and this record rules it so (instrument-discipline.md: the arm's direction is the at-tier review's call). The arm is declared where the breaking gate reads it — check-adr-0087-registration reads the changeset's line through the shared readClause2Line and classifies the changeset breaking (clause-②-narrowing, beside BREAKING and bang), which is the line the dev quoted from the run. The PR body carries the bare no copied verbatim from claim 6041094175 (the claim template offers yes | no); check-changeset-no-major reads that body line, a bare no stands its level axis down, and minor passes either way — Check Changeset is success on this head. Consistent, and not a false declaration. Flag (non-blocking; a body edit, no head change): the body line and the changeset line should be the same string — add (narrowing) to the body line, as PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 did, so the two carriers read identically.

③ Boundary flags

Dev report 6042817677 — open_questions, deviations, out_of_scope_findings — each answered or escalated:

  1. skills/objectstack-data/references/lint-rules.md:13 says warning, false on merge for CBP objects. Answered: not ridden here (Tier H; one hit would govern the whole PR); carved as 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 (verified: open, pm:blocked, domain:spec, "Serial: after PR feat(lint)!: relationship/master-detail-required refuses the three unsafe master-reference shapes at error on a controlled_by_parent object #22109 merges"). Escalated to the seat: 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's body carries no literal Blocked-by: line — only the label and the "Serial:" sentence — and the unlock scan greps the literal key (the H9 note on this card), so add Blocked-by: #9139 to 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 or the scan never returns it.
  2. Register a CBP-only R2 slice in AUTHORING_RULES? Answered A — lint-only, as delivered. The ruling of record promotes the lint rule and keeps the runtime tolerant; DIRECT_CALL_RATCHET already records that making os build reject is a product decision. Option B is a further narrowing over metadata at rest with its own census and needs its own ruling — not this card's.
  3. security-plugin.ts:8851-8853 says Direction 1 "has NOT landed … pm:on-hold", false on merge. Answered: comment-only text in a package the dispatch fenced and that ships dist; recorded in the PR's acceptance notes; carrier = the next PR touching security-plugin.ts (identity lane). The Freeze note on assertControlledByParentWrite — the guard is the sole enforcement for three authorable master-reference shapes until the #8772 ramp completes #9137 FREEZE NOTE itself stays correct: the gate remains the only publish/runtime refusal for these shapes (①.4), so its premise survives this landing. Not a defect; no issue owed.
  4. File surface beyond the claim (packages/cli/test/score.test.ts, content/docs/protocol/kernel/error-handling.mdx): both named in the PR body with the reason; the first a necessary fixture triage (①.9), the second a sentence this change made false. Accepted.
  5. Model tier. The card body asks for the at-tier model on implementation; claim 6041094175 built at the default tier. Answered: the mandate's substance — the accept-set narrowing judged at CONTRACT_REVIEW_TIER — is this record, rendered at that tier in isolation from the building session; the diff is judged on its content above and no finding depends on who built it. The deviation is on the record (report 6042817677, ACCEPT 6042908940) for the audit.
  6. main not re-merged after bafb58bb0 (now aa71c4d9d). The two commits main brought touch one PR file, registry.ts (feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 adds flow-builtin-node-config-values-refused). In the union's sorted order 86 entries separate the two new ids, so the queue's driver-less text merge takes both sides and IS the regeneration byte for byte (the entries README's measured table); GitHub reports mergeable: true; check:migration-registry re-runs on the rebuilt generation. feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) #22094's entry id is not measured here: an adjacent pair conflicts in registry.ts and ejects loudly, never lands stale. No action owed before enqueue.
  7. object.zod.ts docblock and the cbp-master-detail-required-forced entry say the lint "stays warning until v18" — true as a schedule, and true as history once this lands. Acceptance notes; no edit owed.
  8. ruleBody() misattribution in authoring-rule-wiring.test.ts: the helper sits above the first export function and the file comment says why, so the measured false red cannot recur from this diff; the converse blind spot is inference with no instance. Acceptance notes.
  9. Relay pr_create exit 6 UNCONFIRMED, not re-sent, body read back byte-identical; worktree cleanup — process notes consistent with the PR as read.

Implemented-by: claude/issue-9139-master-detail-required-error
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants