Repository navigation
feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit - #25181
Conversation
…efused at parse when marked readonly or system Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
… detail whose one master is not writable Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
…he rule at the level of the contract Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
…e's refusal and the rule's report on the literal Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
… say the parse now refuses the flagged master Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
📓 Docs Drift CheckThis PR changes 2 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9257c896a011041963a0e28e15125ed061f77193 && git checkout 9257c896a011041963a0e28e15125ed061f77193
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b6e870707b95d7b8ee9cedd68ad358b51de0da91 e71d385be010b036a4315bf7d8625d9ac8d2a274 && git checkout -B drift-repro b6e870707b95d7b8ee9cedd68ad358b51de0da91 && git merge --no-ff e71d385be010b036a4315bf7d8625d9ac8d2a274
node scripts/docs-audit/affected-docs.mjs --json b6e870707b95d7b8ee9cedd68ad358b51de0da91
|
Contract reviewServed-tier: Face: ① Derived judgmentsThe rule, as the PR states it. A Each accept-set and public-surface change the diff implies, judged:
Check-runs on the head (their conclusions are the gate verdicts). ② Semver level
③ Boundary flags
FAIL items (what a PASS record on the next head needs):
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…t lose their master, and states the flagged master's fate as the listing does Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ
Contract reviewServed-tier: Face and inputs. ① Derived judgmentsThe rule, as the PR states it. A Each accept-set and public-surface change the diff implies, judged at the head:
Check-runs on the head (their conclusions are the gate verdicts). ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · ⛔ Disclosure (#9387,
Generated by Claude Code |
Requested by Bill · project thread
Fixes #25092
Clause-②: no (narrowing)
Metadata-protocol: narrowed
The rule
The parse refuses a
controlled_by_parentdetail whose one required master reference ADR-0055 §1 does not admit: a requiredmaster_detaildeclaredreadonly: trueorsystem: true.6328204856, route A, under the standing line 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」 Ships in 18.x with no retirement stage. ⛔ Disclosure ruling5323310017(skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387) applies: this PR states the declaration rule and its prescription only.readonly: true/system: truefrom the master reference and declare it as an ordinary required field. If the rows must outlive their parent, the object is not a detail: give it its ownsharingModel..changeset/25092-cbp-master-writable.md. ADR-0087 disposition:not-required (already-registered cbp-master-relation-tiers-retired).Landing point, and why this one
Measured on
origin/maine3aa111ad. The rule lives in the one predicate module every parse door already reads,packages/spec/src/data/controlled-by-parent-master.ts(controlledByParentMasterRefusal). It gains one shape,master-not-writable: exactly one requiredmaster_detail, carryingreadonly: trueorsystem: true, refused atfields.That predicate is read by
ObjectSchema's refinement and by the stack's objectExtensions merge check, so one edit reaches:ObjectSchema.parse/safeParseObjectSchema.create()ZodErroratfields)defineStack/os validateObjectStackDefinitionSchema.objectsisz.array(ObjectSchema)os validate --jsonexit 1,code: STACK_SCHEMA_INVALID, issue atobjects.1.fields(both flags); control exit 0,valid: trueos buildos build --jsonexit 1,success: false,code: STACK_SCHEMA_INVALID; control exit 0,success: truesaveMetaItemparses withMETADATA_TYPE_SCHEMAS.object=ObjectSchema422 INVALID_METADATA, one issue atfields, nothing stored (both flags); control savesstack.zod.tsre-judges the merged field set with the same predicateos migrate meta --storedconversions/refused-shapes.tslists what the predicate refusescbp-master-relation-tiers-retired, the row not rewrittenThe other candidates, not taken:
relationship/master-detail-requiredintoAUTHORING_RULES: it would reachos validateandos build, but notObjectSchema.create(),defineStackused directly, or the save door's parse, and it would be a second copy of a rule the parse now owns (ruling item 2, C and D). The lint rule stays as it is:os lint's advisory sweep over everymaster_detailfield.forceCbpMasterDetailRequired): it runs only increate(). The parse it ends in already refuses the shape, so the builder needs no copy. Its docblock now says so.isControlledByParentMasterFieldis unchanged on purpose. The flags do not change which field the runtime resolves as the master, so a flagged requiredmaster_detailstill counts as one. Excluding it there would let an object with a flagged master beside a second required one parse as having one master, while the runtime still sees two.No engine or runtime change (ruling item 2, B not taken). Nothing in
packages/objectql/**orpackages/plugins/**is edited.ADR-0087 kit
18.cbp-master-relation-tiers-retired(the parse's one-master rule, and the id the stored-row listing already reports under) is updated rather than paralleled: its replacement, reason and acceptance criteria now name the writable master. No D2 conversion: dropping the flag changes who may write the reference, so it is the owner's call.18.cbp-master-detail-required-lint-error(the maintainer's 2026-08-16 "Direction 1" lint promotion, which that entry records as already carried out) is not what this change carries out, so it is not where the narrowing is registered. It is corrected where it had become false: its "WHICH DOOR" sentence said thatos validate,os buildand the save door accept the flagged shapes, and its reason said they still pass the builder. They now refuse them on the one master, and the entry says the lint rule alone reports a shape on a second, non-requiredmaster_detail.refused-shapes.ts: the new shape's TODO names the field and the owner's choice, and does not claim that the runtime denies the object, because for this shape it does not.Producer search (ruling item 3, before any edit)
At
e3aa111ad: every non-test file in this repo declaringsharingModel: 'controlled_by_parent'(git grep, tests,**/test/**and docs excluded):examples/app-crmcrm_opportunity_line_item,examples/app-showcaseshowcase_expense_lineandshowcase_invoice_line, and the four golden objects inpackages/cli/src/lint/corpus.ts. None of theirmaster_detailfields declaresreadonlyorsystem. YAML/JSON metadata: none declare the model. Zero in-repo producers. HotCRM: NOT MEASURED, because the repository is not reachable from this container (gh apianswers 403, "GitHub access to this repository is not enabled for this session"). Deployed tenants cannot be read from the repo (the card already records this).Tests
packages/spec/src/data/controlled-by-parent-master.test.ts: the export andObjectSchemaagree on six new shapes (readonly,system, both, beside a required lookup, explicitfalseflags accepted, a flagged master beside a second required one stillseveral-required-masters);isControlledByParentMasterFieldstill counts a flagged master; the refusal names the object, the field and every flag;ObjectSchema.create()refuses with one issue atfields; the unflagged builder call builds.packages/spec/src/conversions/refused-shapes.test.ts: a stored row with the shape (including the Studio shape plus the flag, which D2 makes required first) is listed once undercbp-master-relation-tiers-retired, is not rewritten, still fails the parse at the same key, and its TODO does not claim a runtime denial.packages/metadata-protocol/src/protocol.cbp-master-not-writable-refusal.test.ts: the save door answers422 INVALID_METADATA(code+status), one issue atfields, stores nothing; control saves. Its fake engine is pinned (scripts/engine-double-contract.pinned.json,--write).Verification (final head
85505b5f9, after mergingorigin/mainc24203f9d)e3aa111ad: see above.pnpm --filter @objectstack/spec test(one9259eedc): 706 files, 21476 passed, 1 todo.spec typecheck,lint typecheck,metadata-protocol typecheck: exit 0.pnpm --filter @objectstack/metadata-protocol test: 253 files passed + 3 skipped, 20840 tests passed + 19 skipped.pnpm --filter @objectstack/lint test: 152 of 153 files passed; the one red file wasdata-model-rules.master-detail-required.test.ts, whose two builder cases pinned "a readonly / system master passes the builder". Those cases now pin the opposite (the builder refuses, the rule still reports the literal); the file is 24/24 on the final head.85505b5f9: speccheck:generatedall 14 artifacts up to date (after a spec build); specsrc/migrations5 files / 229 tests; the touched files 290 (spec, three files) + 24 (lint) + 3 (metadata-protocol) tests passed.plugin-security's twocontrolled_by_parentfiles that register these shapes raw against the runtime (they do not parse): 57/57 passed.scripts/ablation-replace.mjsreplacedif (flags.length === 0) return undefined;incontrolled-by-parent-master.tswith an unconditionalreturn undefined;(anchor 1 to 0, blob98db836bcf78tob0b25dae1aea). The two spec files went 10 failed / 69 passed. Restored: blob equals HEAD98db836bcf78,git diff HEADempty. These tests import the module from source, so no dist rebuild was involved.examples/app-todoplus one object literal, deleted afterwards): see the table above.node scripts/pm/dispatch-gates.mjs --commandson85505b5f9: 102 families derived, 102 run, all exit 0;--ranreconciliation: 102 accounted, 0 NOT-MEASURED (a derived zero).check:changeset-gate-self-testsfirst exited 1 on a commit-signing server 503 inside its temp-repo fixture (infrastructure, not this diff) and exited 0 on re-run.85505b5f9(pnpm --filter @objectstack/dogfood testunder the verify lock, VERDICT command-exit 0): 287 files passed + 3 skipped, 2395 tests passed + 18 skipped, no failures.Acceptance notes
os lintstill reportsrelationship/master-detail-requiredaterroron the same object, beside the parse's refusal. That double report is the existing state for a missingrequiredon the one master as well; the lint rule's docblock and itsos explaintext now say which shapes the parse refuses and which are the rule's alone.relationship/master-detail-requiredexplanation said that a raw.parse()never refuses a missingrequired. That was stale since the tiers-retired rule; the sentence is replaced.packages/plugins/plugin-security/src/controlled-by-parent-sharing.test.tsdescribes itsreadonly/systemmaster fixtures as "AUTHORABLE". They are no longer authorable through any parsing door; the fixtures register the shapes raw against the runtime, which this change leaves as it was, so the suite still measures what it says it measures and stays green. The word is stale; the file is outside this claim's surface. Carrier: none.CONTRACT_REVIEW_TIERreview of record on the narrowing face is owed before the queue (the claim says so).Generated by Claude Code