Repository navigation
[finding] relationship/master-detail-required is a lint WARNING while object.zod.ts's own docblock argues the unguarded shape "arms the worst measured failure shape" — severity judgment for the contract owner #13699
Description
Activity
Claim: PM loop round 1 (2026-08-31 shift) — census + proposal dispatch, not a severity-change dispatch
Session:session_01PBjwYLS6BciTQW3c9xQiD2
Branch:claude/issue-13699-master-detail-required-census(reserved; a measurement run may leave it unpushed — the deliverable is the census + staged proposal on this card)
Worktree:objectstack-issue-13699
Domain:domain:spec
File surface: READ-ONLY measurement (the lint rule atpackages/lint/src/data-model-rules.ts:592,object.zod.ts's docblock, and every in-tree authored corpus). ⛔ Zero severity edits: raising warning→error changes which authored corpora go red — gate-strength is the contract owner's call per this card's own framing, and gate-strength moves are the manual floor; the census output stages the decision (stop on breach; explain in the report)
Container & model: S–M measurement with judgment,mode:subagent,model: opus.
Clause-②: no for THIS dispatch (no behaviour change; the future severity decision is the clause-adjacent event)
Serial constraints cleared: read-only run ⇒ no file collision; in-flight set (#13669 review-chain sync = field.zod/census page; #13704 = view.zod; #13695 = search-fields) all disjoint from the read surface anyway.
Generated by Claude Code
Dev-seat claim (census run picked up from the PM claim above).
Session:
session_01PBjwYLS6BciTQW3c9xQiD2(os-dev subagent seat)
Branch:claude/issue-13699-master-detail-required-census(pushed empty as the write-route probe; no commits expected)
Worktree:objectstack-issue-13699offorigin/main@476ec0a1Scope: READ-ONLY census + staged proposal. Zero edits to
packages/lint/src/data-model-rules.ts, topackages/spec/src/data/object.zod.ts, or to any severity. No PR.Channel note: this seat's REST channel is gated (
403 GitHub access is not enabled for this sessionon a repo-scoped probe), so card/comment reads went through the public-repo embedded-JSON payload channel and writes go through MCP.
Generated by Claude Code
os-dev-report
Census + proposal. Read-only run: zero edits, no PR, no severity touched (
git status --porcelainempty,git diff HEADempty at the end of the run). Measured onorigin/main@476ec0a1, worktreeobjectstack-issue-13699, branchclaude/issue-13699-master-detail-required-census(pushed empty as the write-route probe; no commits).Headline:
premise_still_valid: false. The tension this card names does not exist in the form stated, and the judgment it asks for was already taken deliberately — maintainer ruling on #8772 (2026-08-16, comment 5306089973), carved as #9139, which is OPEN today with labelsdomain:devx/pm:on-hold/target:v18. The census below is still the number that prices it, and it prices it at zero.
Q1 — the tension, verified
The rule (line re-derived)
packages/lint/src/data-model-rules.ts, lines 591-601. The card cites:592; that is the R2 comment line. Theseverityliteral is on line 595.591: if (type === 'master_detail') { 592: // R2 — master-detail children should require their parent. 593: if (def.required !== true) { 594: issues.push({ 595: severity: 'warning', 596: rule: 'relationship/master-detail-required', 597: message: `master_detail "${obj.name}.${fieldName}" -> ${parent} should be required (a detail record cannot exist without its master)`, 598: path: `${fieldPath}.required`, 599: fix: 'required: true', 600: }); 601: }
The docblock (verbatim,
packages/spec/src/data/object.zod.ts)The danger argument, lines 2558-2568 — note that its first clause scopes it:
* Why: a controlled_by_parent detail's access is *derived* from its master
* through that reference (the sharingModel docblock above already states
* "exactly one required master_detail field"). A non-required master
* reference arms the worst measured failure shape: an insert may omit the
* master FK, the row lands with a null FK, the derived read filter
* masterFK IN (accessible master ids) can never match null — the row is
* invisible to everyone — and every later by-id write answers
* 422 MISSING_REQUIRED_FIELD. Today only the security gate
* (assertControlledByParentWrite) closes that shape, and #8772 measured that
* the declaration and the enforcement disagree. This makes the unsafe shape
* impossible to NEWLY declare:And lines 2576-2581, which pre-answer the severity question:
* Lives at create() — the authoring surface (ADR-0077) — beside
* {@link assertSystemDataIsWritable}, and deliberately NOT in raw
* .parse()/.safeParse(): metadata already at rest must keep loading.
* Runtime tolerance is the other half of the #8772 ruling — the security
* gate's fallbacks stay, and the lint rule stays warning until v18 — so
* publish-time refuses new declarations while runtime tolerates old ones.Measured behaviour (scratch probe: real
ObjectSchema+ the shippedlintDataModel)# object safeParseparsed requiredObjectSchema.create()lint A no sharingModel,master_detail, norequiredOK false(key materialized)OK, required: false1 x warning + 2 suggestions B no sharingModel, explicitrequired: falseOK falseOK, required: false1 x warning + 2 suggestions C public_read_write, explicitrequired: falseOK falseOK, required: false1 x warning + 2 suggestions D controlled_by_parent, norequiredOK falseOK, forced to true1 x warning E controlled_by_parent, explicitrequired: falseOK falseTHROWS (the "contradiction with no honest reading" error) 1 x warning F no sharingModel,required: trueOK trueOK 0 Field.masterDetail('probe_master', { label: 'Master' })returns{"type":"master_detail","reference":"probe_master","label":"Master"}— therequiredkey is absent, so every helper call that does not pass it lands on the schema defaultfalse.Verdict on Q1. The card's measurement is exactly right: outside
controlled_by_parent,master_detaildefaultsrequired: false, an explicitfalseparses clean, and the only objection is thewarning. The card's reading of the docblock is what does not hold, on two counts:- The danger argument is already scoped, in its own text, to
controlled_by_parent. The failure it describes is the derived read filtermasterFK IN (accessible master ids)never matching null. Outsidecontrolled_by_parentthere is no derived read filter, so the docblock is not arguing about the shape the card measured. It is not over-arguing about a generalmaster_detail; it is arguing precisely about the case where the spec already HARD-enforces. - The docblock already states the severity is deliberate and time-boxed — "the lint rule stays
warninguntil v18". So the card's disjunction ("either the warning severity is the deliberate judgment ... or the severity under-enforces") is missing the reading that is actually on record: the severity is deliberate AND scheduled to change, scoped, at a named boundary.
That schedule is not folklore. It is carded:
- spec builder: force
required: trueon amaster_detailreference undercontrolled_by_parent(ruled Direction 2 of #8772) #9138 (Direction 2, builder force) — CLOSED, landed; row D above is the proof it is live onmain. - lint: promote
relationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 (Direction 1) — OPEN, titled "lint: promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary". Labelsdomain:devx,pm:on-hold,target:v18. Its hold record names the restart condition ("the v18 major window opens") and an opportunistic-restart clause listingpackages/lint/src/data-model-rules.tsas a trigger file: "any dispatch whose file surface intersects them must name this card."
Also carried in the semantic migration entry
packages/spec/src/migrations/entries/semantic/18.cbp-master-detail-required-forced.ts:14-15: "the lint rulerelationship/master-detail-requiredstayswarninguntil its own v18 promotion (#8772 Direction 1)".
Q2 — blast census: who goes red under
error?Method
The finding SET is severity-invariant: the rule already emits exactly one finding per object that would fail; raising the severity changes the label and
os lint's exit code, not the population. So the census runs the shipped rule body (lintDataModel, imported from source) over each corpus and countsrelationship/master-detail-required— no mutation of the rule file, which is why this run leaves zero diff.Two censuses were run, because there are two candidate escalations:
- GLOBAL — the rule as written today, promoted for every
master_detail. - lint: promote
relationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139-SCOPED — the ruled shape: error only on acontrolled_by_parentobject's master reference, covering all three unsafe shapes (missingrequired;required: true+readonly;required: true+system).
Controls. Every corpus run appends a positive control (an object the rule must flag) and negative controls (the clean shapes) to that corpus's own array, so a probe that cannot see the corpus cannot see its control either. The scoped census carries one positive control per unsafe shape. Every control landed as expected in every corpus; no corpus reports a zero that was not proven reachable. Two corpora initially reported a zero from a wrong extraction path (
DEFAULT_METADATA_EVAL_CORPUSyielded 0 objects;platform-objectsfailed to load) — those were fixed and re-run, and the probe now throws rather than returning an empty array, so that failure mode cannot read as clean again.Results
corpus objects linted CBP objects GLOBAL red #9139-SCOPED red controls examples/app-showcase22 2 ( showcase_expense_line,showcase_invoice_line)1 0 pos reached, neg clean examples/app-showcase/.../external2 0 0 0 pos reached, neg clean examples/app-crm6 1 ( crm_opportunity_line_item)0 0 pos reached, neg clean examples/app-todo1 0 0 0 pos reached, neg clean packages/cliDEFAULT_METADATA_EVAL_CORPUS11 4 0 0 pos reached, neg clean packages/platform-objects53 0 0 0 pos reached, neg clean TOTAL 95 7 1 0 all 3 unsafe-shape controls reached in every corpus The single red, quoted
objects[9].fields.f_master_detail.required master_detail "showcase_field_zoo.f_master_detail" -> showcase_project should be required (a detail record cannot exist without its master)examples/app-showcase/src/data/objects/field-zoo.object.ts:107f_master_detail: Field.masterDetail('showcase_project', { label: 'Master-Detail -> Project' }),
No deliberate exemption is recorded for it — it is the builder's omitted-
requireddefault, sitting in a field-type gallery. The migration is one line and needs no seed change: bothshowcase_field_zooseed rows already setf_master_detail(examples/app-showcase/src/data/seed/index.ts:338='Website Relaunch',:351='Data Platform'), sorequired: trueis already satisfied by the data.Corpora deliberately EXCLUDED from the red count, and why
content/docsMDX snippets (4 declarations withoutrequired: true) — not linted by this rule.collectAndLintDocshandles packagesrc/docs/*.mdonly, and the docs gate that does readcontent/docsispackages/lint/scripts/check-doc-security-posture.mjs, which importsvalidateSecurityPosture, notlintDataModel. These are prose-accuracy items, not corpus breakage.skills/objectstack-data(5) — published skill prose, same reason. One row IS severity-bearing and would need updating on any escalation:skills/objectstack-data/SKILL.md:1139states the rule's severity aswarningin the lint-rule table.- Tests and fixtures (66 declarations without
required: trueacross 45 files) — test INPUTS, many deliberately unclean, none of them run throughos lint. Not migration cost. packages/spec,packages/lintsource — builder definitions, message strings and script self-test fixtures, not authored corpora.
A whole-tree static scan found 160
master_detaildeclarations, of which 87 are notrequired: true; the table above is where those 87 live. The scan is the coverage net for corpora the dynamic probe cannot load; the 95-object dynamic run is the authoritative number for corpora that actually lint.What
erroractually buys today — measured, and smaller than it looks- Nothing in CI runs
lintDataModelfor a verdict. The rule is NOT registered inpackages/lint/src/authoring-rules.ts, soos validateandos buildnever report it (the three rules from this file that ARE registered —lintUnscopedDeclaredIndexes,lintUniqueDeclarations,lintLegacyOrganizationComposites— say so explicitly in theirscopeReason). It reaches an author only throughos lint's best-practice sweep. - The one CI job that shells out to
os lintispnpm check:i18n-coverage, and it deliberately tolerates a non-zero exit (scripts/check-i18n-coverage.mjs:541-550: "os lintexits non-zero whenever the config has errors of any kind; the JSON payload is still what we want"). So escalation does not turn any CI job red on its own. - Where it does bite:
os lintexits 1 when errors exist (packages/cli/src/commands/lint.ts:635), and the metadata-generation rubric weights severity —SCORE_WEIGHTS.error = 8vswarning = 3, andvalidrequires zero errors (packages/cli/src/lint/score.ts:25-26, 97). That is the AI-authoring lever. - Bound on the exit-code claim: for
examples/app-showcase, the data-model family contributes 0 errors / 1 warning / 13 suggestions today, so a GLOBAL escalation would move that family from 0 to 1 error. Whether that flips the wholeos lintcommand's exit code depends on the other familieslintConfigfolds in, which this run did not measure (that needs the built CLI; NOT MEASURED, not zero).
Pin and prose surface an escalation would have to move
packages/cli/test/data-model-rules.test.ts:34—expect(req?.severity).toBe('warning'). The one severity pin.packages/lint/src/validate-security-posture.test.ts:434— "stays silent on step 2: ANY master_detail (not marked required)". lint: promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 already names this pin and instructs retiring it as a deliberate contract narrowing, and records the correction that it lives inpackages/lint/, notplugin-security.- Prose:
skills/objectstack-data/SKILL.md:1139;packages/spec/src/data/object.zod.ts:2580; the migration entry'sreplacementtext; and the security-plugin note (see follow-up F1 below).
Q3 — the docblock's claim, tested
The "worst measured failure shape" is NOT reproducible today. Two independent guards close it, and I measured the first one directly rather than trusting the prose.
Guard 1 — authoring (measured).
ObjectSchema.create()undercontrolled_by_parent:create() CBP + omitted required: ACCEPTED — required=true (forced) create() CBP + explicit false: REFUSED — "...declares `required: false` on a `master_detail` reference under `sharingModel: 'controlled_by_parent'` — a contradiction with no honest reading..." create() CBP + required:true + readonly: ACCEPTED create() CBP + required:true + system: ACCEPTEDThat is #9138 live on
main. The failure shape cannot be newly declared through the authoring surface.Guard 2 — runtime.
SecurityPlugin.assertControlledByParentWritestill refuses the insert for exactly the shapes validation does not cover:packages/plugins/plugin-security/src/security-plugin.ts:6402-6403—if (operation === 'insert' && rel.omissionRefusedByValidation) return;thenthrow new MasterReferenceMissingError(...).omissionRefusedByValidationistrueonly formaster_detail+required: trueand notreadonly/system(:386-403), so for a non-required master reference the gate does NOT stand down. This is the #9137 freeze note's "SOLE ENFORCEMENT POINT", still in force.Residual exposure, measured. Row E of the Q1 table: raw
safeParseacceptscontrolled_by_parent+required: false. That is intentional and stated in the docblock ("metadata already at rest must keep loading"), so a stack published by parsing raw metadata rather than throughObjectSchema.create()can still carry the shape — but Guard 2 still stands between it and a minted null-FK row.A finding the severity question does not reach. Two of the three shapes in the freeze note —
required: true+readonly, andrequired: true+system— draw norelationship/master-detail-requiredfinding at any severity, because the predicate isdef.required !== trueandrequiredIStruefor both. Raising this rule's severity, on its own, closes one of the three shapes and leaves two open. #9139's scope section already anticipates this and requires the promotion to cover all three; a severity flip that only edits line 595 would silently deliver a third of the coverage while reading as done.Consequence for the recommendation. A danger argument that cannot be reproduced is normally a reason to soften a proposal. Here it is not, because the argument is not stale — it is discharged: the docblock describes the shape the builder-force exists to prevent, and it says so in its own next sentence ("This makes the unsafe shape impossible to NEWLY declare"). The docblock is not over-arguing; it is stating a hazard and, immediately, the thing that closes it.
Q4 — adjacent discipline on this board
The tree already carries an explicit, written criterion for exactly this question, and it is not "how much corpus would break".
packages/lint/src/validate-security-posture.ts:26-33states it:"Per ADR-0049 discipline these are NOT advisory security: every
errorrule mirrors a runtime enforcement point (D1 fail-closed OWD default, D4 zod enum + fail-closed evaluator, D5/D9 anchor binding gate, D3 rename wave) — the lint moves the failure from runtime-deny to author-time fix-it. The non-errorrules are the ones with NO hard runtime refusal behind them: master-detail-ungranted mirrors a runtime gate (the ADR-0055 object-level CRUD check) but flags a likely misconfiguration whose per-permission-set nuance it cannot fully adjudicate; book-audience-unknown-set and private-no-readscope flag intent mismatches, not guaranteed denials."Applied here, that criterion produces the ruled answer by itself: under
controlled_by_parenta hard runtime refusal does stand behind the rule (MasterReferenceMissingErrorfrom the CBP write gate, plus the builder's own refusal), soerroris warranted there; outsidecontrolled_by_parentnothing at runtime refuses a non-requiredmaster_detail, so it is a "likely-wrong choice" andwarningis the correct tier — which is also the severity charter indata-model-rules.ts:9-13("structural problems areerror, likely-wrong choices arewarning... None of them block on a judgement call"). The same file supplies the direct staging precedent:unique/unscoped-declared-indexis ADR-0120 D5a, andauthoring-rules.ts:1174-1177records "17.x warns, protocol 18 rejects the spelling (#5082)" — warn in the current major, reject at the major boundary. That is structurally identical to #9139.The declared-equals-enforced pressure is real and points the same way, not further: ADR-0049 is the enforce-or-remove gate for spec properties that "imply an access-control boundary but enforce nothing"; Prime Directive #10's corollary is "never advertise or demo a capability the runtime doesn't actually deliver (declared is not enforced)"; Prime Directive #12 says fix it at the producer and "reject it at authoring/publish (validation / lint) so the error surfaces loudly". And ADR-0055's own line 34 is the sharpest statement of the contract: an object with
sharingModel: 'controlled_by_parent'"must declare exactly one requiredmaster_detailfield ... Validation error otherwise (fail closed — an unsatisfiable 'controlled by parent' must not silently fall open)." Every one of those is CBP-scoped or property-scoped. None of them argues for a global escalation, and the corpus-breakage cost never entered the reasoning on any of these precedents — it is the runtime-refusal test that decides tier.
Proposal — INPUT FOR THE MAINTAINER, not a ruling
Recommendation: option (c), and specifically "do nothing new" — the staged,
controlled_by_parent-scoped escalation already ruled and already carded as #9139, kept ontarget:v18. Close #13699 as a re-discovery of #9139 rather than opening a parallel decision.Neither (a) nor (b) survives measurement:
- (a) raise to
errorglobally contradicts the standing maintainer ruling (which scoped it tocontrolled_by_parent) and fails the tree's own severity criterion outside that scope. It is cheap — the census is 1 red, one line, no seed change — but cheapness is not the test the board uses, and a globalerrorwould put a hard label on a shape with no runtime refusal behind it. If it were adopted anyway it would need a fresh ruling, not this card. - (b) keep
warningand fix the docblock is not available: the docblock does not over-argue. It scopes its own danger claim tocontrolled_by_parentin the same sentence, and it already declares the warning severity deliberate and time-boxed. There is no prose to restore here. (There IS a genuinely stale note one package over — see F1 — but it is a different file and a different card.)
The four axes
- 业务实测 (real business need). Measured, not asserted: 7
controlled_by_parentobjects exist in the whole in-tree authored corpus and all 7 are already clean. The unsafe shape is not something authors in this tree are producing; the one miss anywhere is a demo field in a type gallery, outsidecontrolled_by_parent, where the docblock's hazard does not apply. So there is no live breakage pulling the escalation forward, and no corpus pain pushing it back — the axis says neither urgency nor obstacle, which is exactly the profile of a change that belongs on its scheduled boundary rather than being re-litigated now. - 长远 (long-term soundness). The tier criterion is written down and it is "does a hard runtime refusal stand behind this rule". Answering that per-scope —
errorundercontrolled_by_parent,warningelsewhere — is the sustainable shape; a global flip would be the workaround that buys a green feeling by mislabelling a judgement call as a structural defect, and Prime Directive [WIP] Fix error in step four of the action run #5 rules that out. The ADR-0120 D5a precedent (warn in 17.x, reject at protocol 18) shows this project already knows how to land a narrowing on a version boundary with migration machinery attached, which is what lint: promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 specifies. - 防 AI 错 (make AI-authored metadata hard to get wrong). This is the axis with the strongest pull toward escalation, and it is worth stating honestly rather than folding into the recommendation:
Field.masterDetail()omitsrequired(measured), so the default path an AI generator takes produces the warned shape, and the eval rubric currently prices that at 3 rather than 8. But the shape that actually hurts — thecontrolled_by_parentone — is already structurally impossible to author throughObjectSchema.create()(Q3, measured): omitted is forced totrue, explicitfalseis refused loudly. Declared already equals enforced at the authoring surface for the dangerous case. What lint: promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 adds is the third guard and, importantly, coverage of the two shapes the builder still accepts (required: true+readonly/+ system) that no severity change to line 595 can reach. That is a reason to keep lint: promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 scoped as written, not a reason to pull it forward. - 不扩散 (startup scope discipline). [finding] relationship/master-detail-required is a lint WARNING while object.zod.ts's own docblock argues the unguarded shape "arms the worst measured failure shape" — severity judgment for the contract owner #13699 asks for a decision that has an owner, a ruling, a card, a hold record, a named restart condition and a trigger-file clause. Re-opening it now spends maintainer attention on a settled question and risks producing a second, conflicting record of the same decision. The disciplined move is to fold this card into lint: promote
relationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 and spend nothing further.
Where the axes conflict, stated plainly
The 防-AI axis argues for enforcement sooner; 长远 and 不扩散 argue for the scheduled boundary. The census is what resolves it in this instance rather than a judgement call: with 0 scoped reds and 1 global red, escalating early buys about one line of corpus cleanup and no measured behaviour change in CI, while costing a re-ruling. If the maintainer nonetheless wants the AI-authoring lever pulled before v18, the cheapest honest version is not a severity flip — it is
Field.masterDetail()emittingrequired: trueby default (mirroring whatcreate()already does undercontrolled_by_parent), which closes the generator's default path without relabelling anything. That is offered as an option, not a recommendation; it is a spec-builder change with its own blast radius and would need its own measurement.
Follow-up candidates handed to the PM (no cards filed by this run, per dispatch)
- F1 — stale enforcement note,
packages/plugins/plugin-security/src/security-plugin.ts:6351-6367. It states "the ramp it ordered has two code legs and neither has landed yet: Direction 2 ... (spec builder: forcerequired: trueon amaster_detailreference undercontrolled_by_parent(ruled Direction 2 of #8772) #9138) is dispatchable now but not yet merged". Measured false: spec builder: forcerequired: trueon amaster_detailreference undercontrolled_by_parent(ruled Direction 2 of #8772) #9138 is CLOSED andcreate()forcesrequired: true(Q3 above). The note's own last line invites the correction ("Re-check this paragraph before trusting it; once both have landed it is the one that goes stale next"). It needs a correction, not a deletion — the residue it describes is still reachable through the raw.parse()path (Q1 row E, measured), so only the "neither has landed" clause and the "spec builder: forcerequired: trueon amaster_detailreference undercontrolled_by_parent(ruled Direction 2 of #8772) #9138 not yet merged" clause are wrong. Prose-truth class. - F2 — the single global red,
examples/app-showcase/src/data/objects/field-zoo.object.ts:107.f_master_detailis missingrequired: true; the seed already satisfies it, so the fix is one line with no data change. Worth doing on its own merits (it is the onlymaster_detailin any authored corpus that is not required) and it would make the tree's global census zero ahead of whatever v18 decides. Deliberately NOT done in this run: read-only dispatch, and it is a corpus edit, not a census. - F3 — coverage gap worth recording against lint: promote
relationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 when it restarts.required: true+readonlyandrequired: true+systemdraw no finding from this rule at any severity. lint: promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139's scope section already covers this; flagging it so an implementer does not deliver the one-line severity flip and read it as complete.
Deviations and channel notes
- REST channel gated for this seat. A repo-scoped probe returned
403 GitHub access is not enabled for this session. Per the degrade ladder, card and comment reads used the public-repo embedded-JSON payload channel (zero quota); writes used MCP.#9139/#9138status was read via the payload channel, not via search. - Narrowing declared: no repo-wide gate sweep was run, because this run produced zero diff — there is nothing for a gate to read.
git status --porcelainandgit diff HEADare both empty at the end of the run. Builds performed (both through the shared verify lock):pnpm --filter @objectstack/spec build(VERDICT command-exit 0, held 151s) andpnpm --filter @objectstack/metadata-core build(VERDICT command-exit 0, held 8s), needed only so the probes could load real schemas and real packaged metadata. - NOT MEASURED, stated rather than implied: the full
os lintexit code per config (needs the built CLI; only thelintDataModelfamily was measured), and any corpus outside the six loaded above.
{ "issue": 13699, "status": "done", "branch": "claude/issue-13699-master-detail-required-census", "pr": null, "premise_still_valid": false, "summary": "Census + proposal, read-only, zero diff. The card's measurement holds (outside controlled_by_parent a master_detail defaults required:false, explicit false parses clean, only the warning objects) but its premise does not: the object.zod.ts docblock scopes its danger argument to controlled_by_parent in its own text AND already states the severity is deliberate and time-boxed ('the lint rule stays warning until v18'). The judgment was taken by the maintainer on 2026-08-16 (#8772 ruling) and is carded as #9139 — OPEN, pm:on-hold, target:v18, scoped to controlled_by_parent — with a trigger-file clause naming data-model-rules.ts. Sibling #9138 (builder force) is CLOSED and measured live. Census: 95 objects across 6 loadable authored corpora, 7 controlled_by_parent objects, all controls reached; the RULED (CBP-scoped) escalation would turn 0 red, a GLOBAL escalation exactly 1 red (showcase_field_zoo.f_master_detail, one line, seed already satisfies it). Recommendation: keep #9139 as ruled and close #13699 as a re-discovery.", "tests": "No code changed, so no gate family applies; git status --porcelain and git diff HEAD both empty at end of run (read-only proven by state, not by exit code). Builds via scripts/pm/os-verify-lock.sh: 'pnpm --filter @objectstack/spec build' -> VERDICT command-exit 0, held 151s, waited 111s; 'pnpm --filter @objectstack/metadata-core build' -> VERDICT command-exit 0, held 8s, waited 285s. Census probes ran the SHIPPED lintDataModel imported from source with the severity untouched, since the finding set is severity-invariant; each corpus carried its own positive control (an object the rule must flag; for the scoped census one control per unsafe shape) and negative controls (clean shapes) appended to that corpus's own array — all positive controls REACHED, all negative controls clean, in every corpus. Two corpora first reported a zero from a wrong extraction path (DEFAULT_METADATA_EVAL_CORPUS yielded 0 objects; platform-objects failed to load); both were fixed, re-run, and the probe now throws on an empty extraction so that mode cannot read as clean. Q1/Q3 probe used the real built ObjectSchema: safeParse/create/lint measured across six object shapes. NOT MEASURED and reported as such: full `os lint` exit code per config (needs the built CLI).", "mcp_calls": "3 — issue_read (card), add_issue_comment (claim), add_issue_comment (this report); all other GitHub reads went through the zero-quota public payload channel", "open_questions": [ { "question": "#13699 asks for a severity judgment that was already ruled (2026-08-16, #8772) and carded as #9139 (OPEN, pm:on-hold, target:v18, scoped to controlled_by_parent). Does the maintainer want that ruling revisited now, or is #13699 a duplicate to fold into #9139?", "options": [ "A — fold: close #13699 as a re-discovery of #9139, cross-reference it, change nothing else. Census cost of the ruled escalation: 0 reds.", "B — raise to error GLOBALLY now: 1 red in the whole tree (showcase_field_zoo.f_master_detail, one line, seed already satisfies it), 1 severity pin, 1 published-skill table row. Requires a NEW ruling: it contradicts the CBP scoping of the standing one and fails the tree's own severity criterion (no hard runtime refusal outside controlled_by_parent).", "C — keep warning and rewrite the docblock: NOT AVAILABLE as measured. The docblock scopes its danger claim to controlled_by_parent in its own sentence and already declares the warning deliberate and time-boxed; there is no over-argument to correct.", "D — pull the AI-authoring lever early WITHOUT a severity change: make Field.masterDetail() emit required:true by default, mirroring what ObjectSchema.create() already does under controlled_by_parent. Closes the generator's default path; own blast radius, own measurement, separate card." ], "recommendation": "A, because every axis lands there once measured. 业务实测: all 7 controlled_by_parent objects in the tree are already clean, so nothing is breaking and nothing is blocking. 长远: the written tier criterion (validate-security-posture.ts:26-33, 'every error rule mirrors a runtime enforcement point') independently reproduces the ruled scope, and ADR-0120 D5a ('17.x warns, protocol 18 rejects') is the direct staging precedent. 防 AI 错: the dangerous shape is ALREADY structurally unauthorable via ObjectSchema.create() (measured), and a line-595 severity flip would reach only one of the three unsafe shapes #9139 requires — so escalating early would under-deliver while reading as done. 不扩散: the decision has an owner, a ruling, a hold record and a named restart condition; re-opening it spends maintainer attention on a settled question and risks a second conflicting record. If the maintainer does want movement before v18, D is the cheaper honest lever than B." } ], "out_of_scope_findings": [ "NOT FILED (read-only dispatch; handed to PM) — F1: packages/plugins/plugin-security/src/security-plugin.ts:6351-6367 states 'neither has landed yet' and '#9138 ... not yet merged'; measured false (#9138 CLOSED, create() forces required:true). Needs correction not deletion — the residue it describes is still reachable via raw .parse() (measured). Prose-truth class.", "NOT FILED (read-only dispatch; handed to PM) — F2: examples/app-showcase/src/data/objects/field-zoo.object.ts:107 f_master_detail lacks required:true; the only master_detail in any authored corpus that is not required. One line, seed already satisfies it.", "NOT FILED (recorded against #9139) — F3: `required: true` + `readonly` and `required: true` + `system` draw no finding from this rule at ANY severity (predicate is def.required !== true). #9139's scope already requires covering all three shapes; flagged so an implementer does not ship the one-line flip as complete." ] }
Generated by Claude Code
- The danger argument is already scoped, in its own text, to
PM verdict: ACCEPT (census run, zero diff — the intended outcome) + disposition: close as a RE-DISCOVERY of #9139; the judgment this card asks for already exists. (
session_01PBjwYLS6BciTQW3c9xQiD2)The census (os-dev-report above, read-only, tree proven untouched) falsified the card's premise on every limb:
- The severity judgment was already taken by the maintainer (2026-08-16, the A
controlled_by_parentobject may declare its master reference withoutrequired, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 ruling) and is carded as lint: promoterelationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139 — OPEN,pm:on-hold,target:v18, scoped tocontrolled_by_parent, with a hold record whose trigger-file clause names the very lint rule this card points at. - The docblock does not over-argue: it scopes its danger claim to CBP in its own sentence (the failure it names — a derived read filter never matching null — does not exist outside CBP) and already declares the warning deliberate and time-boxed (「stays warning until v18」,
object.zod.ts:2580). - The census prices both worlds: the RULED (CBP-scoped) escalation turns 0 corpora red (all 7 CBP objects in-tree already clean); a GLOBAL escalation turns exactly 1 red (one line in the showcase field-zoo). The rule is not registered in
authoring-rules.ts, so no CI job's colour moves either way. - Per the same-family meta-rule, a sibling card re-asking a ruled question folds into the standing ruling rather than re-entering the inbox. Closing duplicate of lint: promote
relationship/master-detail-requiredfrom warning to error, scoped tocontrolled_by_parent— ruled for the v18 boundary (Direction 1 of #8772) #9139; reopen is free if the maintainer wants the v18 timing revisited (the census's option D — makeField.masterDetail()emitrequired: trueby default, no severity change — is recorded there as the cheaper early lever if movement before v18 is ever wanted).
Handed findings routed: F3 (two of #9139's three unsafe shapes draw no finding at ANY severity — the one-line severity flip must not be read as completing #9139) recorded on #9139; F1 (stale "neither has landed yet" paragraph in
security-plugin.ts— measured false, #9138 is live) and F2 (the one un-requiredmaster_detailin any authored corpus, showcasefield-zoo.object.ts:107) filed as findings for triage.
Generated by Claude Code
- The severity judgment was already taken by the maintainer (2026-08-16, the A
Routed by the skills lane seat from the published-skills sweep (flight ②, PRs #13687/#13698 evidence; measured while correcting the data skill's "Always required" claim about
master_detail).Measured: outside
sharingModel: 'controlled_by_parent'(where the spec HARD-enforcesrequired: trueand refuses an explicitfalsewith 「a contradiction with no honest reading」), amaster_detailreference defaults torequired: falseand an explicitfalseparses clean — the only objection isrelationship/master-detail-requiredatseverity: 'warning'(packages/lint/src/data-model-rules.ts:592). Meanwhile the schema's own documentation argues the unguarded shape is dangerous. Tension, not defect: either the warning severity is the deliberate judgment (existing corpora would break on error) and the docblock over-argues, or the severity under-enforces the contract's own stated position.This is a contract-owner call, not a docs fix — the published skill now states the measured truth ("Forced only under
controlled_by_parent; else lint-warned", PR #13687). Filed so the judgment is taken deliberately rather than inherited. Evidence: the four-row parse table in PR #13687's body (§5).Generated by Claude Code