Skip to content

feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit - #25181

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-7afd2e
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-7afd2e

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25092

Clause-②: no (narrowing)
Metadata-protocol: narrowed

The rule

The parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit: a required master_detail declared readonly: true or system: true.

  • Governing text: ADR-0055 §1 (a detail's access derives from exactly one required master) and §3; ADR-0087 (the kit a spec narrowing carries); ruling 6328204856, route A, under the standing line 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」 Ships in 18.x with no retirement stage. ⛔ Disclosure ruling 5323310017 (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.
  • Prescription: remove readonly: true / system: true from 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 own sharingModel.
  • BREAKING, FROM → TO, and the one-line fix are in .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/main e3aa111ad. 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 required master_detail, carrying readonly: true or system: true, refused at fields.

That predicate is read by ObjectSchema's refinement and by the stack's objectExtensions merge check, so one edit reaches:

door reached through measured
ObjectSchema.parse / safeParse the refinement spec test
ObjectSchema.create() the builder ends in the same parse spec test (ZodError at fields)
defineStack / os validate ObjectStackDefinitionSchema.objects is z.array(ObjectSchema) scratch project, CLI from source: os validate --json exit 1, code: STACK_SCHEMA_INVALID, issue at objects.1.fields (both flags); control exit 0, valid: true
os build the same stack parse os build --json exit 1, success: false, code: STACK_SCHEMA_INVALID; control exit 0, success: true
metadata save door saveMetaItem parses with METADATA_TYPE_SCHEMAS.object = ObjectSchema metadata-protocol test: 422 INVALID_METADATA, one issue at fields, nothing stored (both flags); control saves
objectExtensions merge stack.zod.ts re-judges the merged field set with the same predicate no new code
os migrate meta --stored conversions/refused-shapes.ts lists what the predicate refuses spec test: one TODO under cbp-master-relation-tiers-retired, the row not rewritten

The other candidates, not taken:

  • Wiring relationship/master-detail-required into AUTHORING_RULES: it would reach os validate and os build, but not ObjectSchema.create(), defineStack used 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 every master_detail field.
  • The builder (forceCbpMasterDetailRequired): it runs only in create(). The parse it ends in already refuses the shape, so the builder needs no copy. Its docblock now says so.
  • isControlledByParentMasterField is unchanged on purpose. The flags do not change which field the runtime resolves as the master, so a flagged required master_detail still 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/** or packages/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 that os validate, os build and 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-required master_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 declaring sharingModel: 'controlled_by_parent' (git grep, tests, **/test/** and docs excluded): examples/app-crm crm_opportunity_line_item, examples/app-showcase showcase_expense_line and showcase_invoice_line, and the four golden objects in packages/cli/src/lint/corpus.ts. None of their master_detail fields declares readonly or system. YAML/JSON metadata: none declare the model. Zero in-repo producers. HotCRM: NOT MEASURED, because the repository is not reachable from this container (gh api answers 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 and ObjectSchema agree on six new shapes (readonly, system, both, beside a required lookup, explicit false flags accepted, a flagged master beside a second required one still several-required-masters); isControlledByParentMasterField still counts a flagged master; the refusal names the object, the field and every flag; ObjectSchema.create() refuses with one issue at fields; 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 under cbp-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 answers 422 INVALID_METADATA (code + status), one issue at fields, stores nothing; control saves. Its fake engine is pinned (scripts/engine-double-contract.pinned.json, --write).

Verification (final head 85505b5f9, after merging origin/main c24203f9d)

  • Producer search before any edit, on e3aa111ad: see above.
  • pnpm --filter @objectstack/spec test (on e9259eedc): 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 was data-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.
  • After the merge, on 85505b5f9: spec check:generated all 14 artifacts up to date (after a spec build); spec src/migrations 5 files / 229 tests; the touched files 290 (spec, three files) + 24 (lint) + 3 (metadata-protocol) tests passed. plugin-security's two controlled_by_parent files that register these shapes raw against the runtime (they do not parse): 57/57 passed.
  • Ablation, red: scripts/ablation-replace.mjs replaced if (flags.length === 0) return undefined; in controlled-by-parent-master.ts with an unconditional return undefined; (anchor 1 to 0, blob 98db836bcf78 to b0b25dae1aea). The two spec files went 10 failed / 69 passed. Restored: blob equals HEAD 98db836bcf78, git diff HEAD empty. These tests import the module from source, so no dist rebuild was involved.
  • Doors measured through the CLI from source (scratch copy of examples/app-todo plus one object literal, deleted afterwards): see the table above.
  • node scripts/pm/dispatch-gates.mjs --commands on 85505b5f9: 102 families derived, 102 run, all exit 0; --ran reconciliation: 102 accounted, 0 NOT-MEASURED (a derived zero). check:changeset-gate-self-tests first exited 1 on a commit-signing server 503 inside its temp-repo fixture (infrastructure, not this diff) and exited 0 on re-run.
  • Dogfood battery on 85505b5f9 (pnpm --filter @objectstack/dogfood test under the verify lock, VERDICT command-exit 0): 287 files passed + 3 skipped, 2395 tests passed + 18 skipped, no failures.
  • Size: 13 files, +377 / −39 against the merge base.

Acceptance notes


Generated by Claude Code

…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
…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
… say the parse now refuses the flagged master

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

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/lint/src/data-model-rules.ts, packages/spec/src/data/object.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json b6e870707b95d7b8ee9cedd68ad358b51de0da91.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/lint/src/data-model-rules.ts, packages/spec/src/data/object.zod.ts) — pages documenting those are invisible to this run
  • 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 — 144 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 b6e870707b95d7b8ee9cedd68ad358b51de0da91 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9257c896a011041963a0e28e15125ed061f77193 — the merge of head e71d385be010b036a4315bf7d8625d9ac8d2a274 into base b6e870707b95d7b8ee9cedd68ad358b51de0da91, 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 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

⚠️ 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 b6e870707b95d7b8ee9cedd68ad358b51de0da91 → 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: 85505b5f9d76783d5d079467a3a44adecbab770f
Local-runs: none
Scope: packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/data/controlled-by-parent-master.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/migrations/entries/semantic/18.cbp-master-detail-required-lint-error.ts, packages/spec/src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts + .changeset/25092-cbp-master-writable.md, packages/spec/src/data/controlled-by-parent-master.test.ts, packages/spec/src/conversions/refused-shapes.test.ts, packages/lint/src/data-model-rules.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/data-model-rules.master-detail-required.test.ts, packages/metadata-protocol/src/protocol.cbp-master-not-writable-refusal.test.ts, scripts/engine-double-contract.pinned.json (the rest of the diff: on the narrowing face no path is a remainder); cited and read at the head: packages/spec/src/stack.zod.ts (the objectExtensions merge check), packages/spec/src/data/field.zod.ts (the readonly and system declarations), packages/spec/src/kernel/metadata-type-schemas.ts (the save door's object schema), packages/plugins/plugin-security/src/security-plugin.ts (the runtime's master-resolution predicate, read only to check the no-runtime-change claim), docs/adr/0055-master-detail-controlled-by-parent.md, docs/adr/0087-metadata-protocol-upgrade-contract.md (the disposition addendum), scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs (the level axis), packages/spec/CHANGELOG.md (disposition precedents).

Face: reviewFace reads the schema face off the file list (paths under packages/spec/src); the PR body and changeset also declare Clause-②: no (narrowing), the narrowing face. Both map to this tier. Inputs were the card body and every comment on #25092, PR #25181's body, file list and net diff against main at the head, the head's check-runs, and the files above as committed; nothing was built, run or re-run. The disclosure ruling on #9387 governs this record: it states the declaration rule, its governing text and its prescription at the level the PR body does, and nothing further. The reviewing session id is the session that dispatched the dev and this reviewer under an adversarial brief; the reader weighs the record with that known.

① Derived judgments

The rule, as the PR states it. A controlled_by_parent object whose one required master_detail carries readonly: true or system: true is refused at parse, at fields, naming the object, the field and each flag. Governing text: ADR-0055 §1 and §3, ADR-0087, ruling 6328204856 route A. Prescription: drop the flag and declare the master reference as an ordinary required field; rows that must outlive their parent belong to an object with its own sharingModel.

Each accept-set and public-surface change the diff implies, judged:

  1. One predicate, every parse door narrowed by one shape — right. controlledByParentMasterRefusal (controlled-by-parent-master.ts) gains the branch for exactly one counting master whose definition carries either flag === true. Read in the cited code, that predicate is the only judge at: ObjectSchema's refinement refuseControlledByParentWithoutOneMaster (object.zod.ts), which covers .parse, .safeParse and create() (the builder ends in ObjectSchemaBase.parse); ObjectStackDefinitionSchema.objects (so defineStack, os validate, os build); the metadata save door (METADATA_TYPE_SCHEMAS.object is ObjectSchema); and the objectExtensions merge check in stack.zod.ts, which calls the same predicate on the base fields and on the merged map with no code change. No second copy exists: the lint rule is not wired into AUTHORING_RULES and the builder's forceCbpMasterDetailRequired reads neither flag (its docblock now says so). Ruling item 2's rejections of B, C and D are honoured: nothing under packages/objectql or packages/plugins moves.

  2. The flags are read === true only — right. FieldSchema declares readonly as z.boolean().default(false) and system as optional, so on the parsed map and on a stored body the same verdict is reached; an explicit readonly: false / system: false is accepted, and the tests pin it.

  3. isControlledByParentMasterField unchanged — right. The runtime's master resolver counts master_detail + required and nothing else (security-plugin.ts, the resolver's own predicate since finding(security): controlled_by_parent derives its master through a REQUIRED LOOKUP (runtime and lint, third tier) where ADR-0055 §1 requires exactly one required master_detail and fails closed — a widening no record states #23100), so a flagged required master is still the resolved master, and a flagged one beside a second required one is still two masters; the parse and the runtime keep counting one set. The plugin's other read of the two flags serves a different question (whether record validation covers an omitted reference) and is not the resolver, so the PR's "no runtime change" claim holds as read.

  4. Public surface of @objectstack/spec/data — right, and declared. ControlledByParentMasterShape gains the member master-not-writable; the predicate returns a refusal at path fields for a shape it previously accepted. No new export, no new authorable key or value, and the api-surface artifact records a type's existence only, so no generated artifact moves. The one in-tree exhaustive consumer of the shape (refused-shapes.ts) gains its case in the same diff.

  5. Stored-row listing — right. refused-shapes.ts lists a stored object in the shape once under cbp-master-relation-tiers-retired, rewrites nothing, and its fate text for this shape no longer claims a runtime denial (the module's "one judge, two readers" invariant holds: listed exactly when the parse refuses). The non-null assertion on masterDetails.find(...) is safe: the same fields map produced a refusal with exactly one counting master.

  6. ADR-0087 kit — the step-18 D3 entry is amended rather than paralleled; the lint-promotion entry is corrected where this change made it false. Right as a method: packages/spec/CHANGELOG.md carries the same form repeatedly (a pre-existing step-18 entry amended in the diff, disposed already-registered), and the listing already reports this rule under that id. ⛔ One sentence of the amended entry is now false, see FAIL item 1 under ③.

  7. Lint package — right. relationship/master-detail-required is unchanged in reach (the os lint sweep and the rubric); its docblock and os explain text now say which shapes the parse refuses and which are the rule's alone; the test file flips the two builder cases to pin the builder's refusal and the rule's report on the literal.

  8. Tests — right for the claims they pin. Spec: six new shapes agree between the export and ObjectSchema, the refusal names object, field and every flag, create() refuses with one issue at fields, the unflagged control builds. Conversions: one TODO, not rewritten, same parse key, no denial claim. Metadata-protocol: the save door answers 422 INVALID_METADATA at fields and stores nothing, control saves; its engine double is pinned in scripts/engine-double-contract.pinned.json.

  9. Producer search (ruling item 3) — in-repo half confirmed, HotCRM half not. Re-read at the head: every non-test controlled_by_parent declarant (examples/app-crm, examples/app-showcase, the CLI lint corpus) declares its master without either flag; no YAML/JSON metadata declares the model. HotCRM is not readable from this session either (the repository is not enabled; a read answers 403), so that half of the ruling's first act stands unmeasured by both the dev and this review — FAIL item 2 under ③.

  10. Governed surfaces — none. No path under docs/adr, .claude, skills, AGENTS.md, CLAUDE.md or .github/workflows; the Governed Surface Queue Guard check-run is success.

Check-runs on the head (their conclusions are the gate verdicts). success: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · source gates, filter. in_progress at this read (reported as pending, not waited for): Build Core, Lint · CI, release & engine gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Test Core (1/6 to 6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace. skipped (opt-in or main-only): 18.x upgrade harness, Build Docs, Console Pin Gate, Dogfood Regression Gate shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. Three required contexts (Build Core, Test Core, Type Check · workspace, which holds the spec generated-artifact gates) are among the pending ones.

② Semver level

  • Changeset .changeset/25092-cbp-master-writable.md: @objectstack/spec: minor, @objectstack/lint: patch. Right. The kind is narrowed (a declaration the parse accepted is refused), which takes at least minor on a package whose published source the diff moves; spec carries it. The lint move is explanation and docblock text under its src/** and the level axis asks for at least one package at minor, so patch on lint stands. packages/metadata-protocol moves a test file only and scripts/ publishes nothing: no entry owed. Check Changeset is success.
  • Clause-②: no (narrowing) on the PR body and in the changeset: true. No published accept set widens and no new export, key or accepted value appears; the one additive type member (item ① 4) rides on an existing public discriminant and is declared in the body. Metadata-protocol: narrowed: true. The **BREAKING** banner, the FROM → TO block and the one-line fix are in the changeset body, as a breaking changeset must carry.
  • ADR-0087 disposition marker: not-required (already-registered cbp-master-relation-tiers-retired). Admitted and honest in the form the repository already uses: the id pre-dates the merge base, the entry is amended in this diff to carry the new shape, and the stored-row listing reports under it. The marker's reason reads "already carries this narrowing; its text is updated in this change" — accurate at the head, and the precedent wording ("registered before this change, amended in this diff") is the clearer spelling if the changeset is touched for FAIL item 1.
  • Ships in 18.x with no retirement stage, under the maintainer's standing line the ruling quotes.

③ Boundary flags

  • Dev open question — register under the existing parse entry (A) or a new step-18 entry (B)? Answered: A. It is the same parse rule, the same predicate and the id the listing already reports under; packages/spec/CHANGELOG.md records the same choice for other amended step-18 entries. B would give one rule two ledger ids, and the listing can name only one.
  • Dev flag: HotCRM not measured. Escalated, and a condition of PASS: FAIL item 2.
  • Dev flag: the plugin-security test file calls the two flagged fixtures "authorable" (stale word, outside the claim's surface, carrier none). Accepted as noted: the suite registers raw against an unchanged runtime and measures what it says.
  • Dev flag: a flag on a second, non-required master_detail beside the master stays the lint rule's report alone. Accepted: ADR-0055 §1 decides on the one master; the entry and the explanation say so.
  • Dev flag: os lint double-reports the flagged shape beside the parse's refusal. Accepted as the existing state for a missing required too; noted in the acceptance notes.
  • Disclosure. The PR body and changeset state the declaration rule, its governing text, its prescription and the authoring doors the parse reaches, and name no request, write door or served value beyond what the card and ruling already state. Within the ruling; nothing to redact.

FAIL items (what a PASS record on the next head needs):

  1. The amended step-18 entry 18.cbp-master-relation-tiers-retired.ts now contradicts the diff's own fate text for the new shape. Its acceptanceCriteria says a stored row "in any other retired shape still registers … and is denied until it is corrected and saved", then adds "a master marked readonly or system" to the list of such rows — while refused-shapes.ts and object.zod.ts in the same diff say that shape is NOT denied at runtime (the master still resolves; what it loses is the save door). The entry is the upgrade guide's and the package-shipped guide's source text, so this publishes a denial the runtime does not perform (Prime Directive 10, ADR-0049). Fix: scope the "is denied" clause to the shapes that lose their master and state the flagged shape's fate as the listing does (registers under its diagnostic; the save door refuses it until corrected). Same file, one sentence; re-read the replacement sentence "the runtime derives no master from them" for the same ambiguity while there.
  2. Ruling 6328204856 item 3 names HotCRM in the producer search, and it is unmeasured. Not a defect of the diff — the repository is unreachable from the dev's container and from this session — but the ruling's first act is incomplete. Before the next record: a seat with HotCRM read access posts on controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092 the census (the tree sha and the count of controlled_by_parent declarants whose master carries either flag; a producer found is reported as a fork, not pushed over), or the maintainer waives that half. The in-repo half is confirmed here.

Implemented-by: claude/project-thread-7afd2e
Reviewed-by: session_01B2EeooonSXrANpkpgUSfFZ

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e71d385be010b036a4315bf7d8625d9ac8d2a274
Local-runs: none
Scope: packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/data/controlled-by-parent-master.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/migrations/entries/semantic/18.cbp-master-detail-required-lint-error.ts, packages/spec/src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts + .changeset/25092-cbp-master-writable.md, packages/spec/src/data/controlled-by-parent-master.test.ts, packages/spec/src/conversions/refused-shapes.test.ts, packages/lint/src/data-model-rules.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/data-model-rules.master-detail-required.test.ts, packages/metadata-protocol/src/protocol.cbp-master-not-writable-refusal.test.ts, scripts/engine-double-contract.pinned.json (the rest of the diff: on the narrowing face no path is a remainder); cited and read at the head: packages/spec/src/stack.zod.ts (the objectExtensions merge check and the stack's object array), packages/spec/src/data/field.zod.ts (the readonly and system declarations, the builder's readonly injection), packages/spec/src/kernel/metadata-type-schemas.ts and packages/metadata-protocol/src/protocol.ts (the save door's per-type parse), packages/plugins/plugin-security/src/security-plugin.ts (the resolver's predicate, read only to check the no-runtime-change claim), packages/spec/api-surface/data.json and packages/spec/export-origins/data.json (what the artifacts record), docs/adr/0055-master-detail-controlled-by-parent.md, docs/adr/0087-metadata-protocol-upgrade-contract.md (the disposition addendum), scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, packages/spec/CHANGELOG.md (disposition precedents), examples/app-crm/src/objects/opportunity-line-item.object.ts, examples/app-showcase/src/data/objects/expense-report.object.ts, examples/app-showcase/src/data/objects/invoice.object.ts, packages/cli/src/lint/corpus.ts (the in-repo producer census).

Face and inputs. reviewFace reads the schema face off the file list (the five hits under packages/spec/src); the PR body and the changeset also declare Clause-②: no (narrowing), the narrowing face; both map to this tier. Inputs: card #25092's body and all nine comments (the ruling 6328204856, the FAIL record 6334988367 on 85505b5f9 and the HotCRM census 6335022110 included), PR #25181's body, file list and net diff against main at the head, the head's check-runs (read at 2026-10-22T07:30Z), and the files above as committed at the head. Nothing was built, run or re-run. The head is one commit past the FAIL head and touches two files, the amended step-18 entry and the changeset's disposition marker; the whole PR is judged here, not the delta. The disclosure ruling on #9387 governs this record: it states the declaration rule, its governing text and its prescription at the level the PR body does, and nothing further. The reviewing session id is the session that dispatched the dev, this reviewer and the census under an adversarial brief; the reader weighs the record with that known.

① Derived judgments

The rule, as the PR states it. A controlled_by_parent object whose one required master_detail carries readonly: true or system: true is refused at parse, at fields, naming the object, the field and each flag. Governing text: ADR-0055 §1 ("must declare exactly one required master_detail field … Validation error otherwise (fail closed)") and §3; ADR-0087; ruling 6328204856 route A under the maintainer's standing line (ships in 18.x, no retirement stage). Prescription: remove the flag and declare the master reference as an ordinary required field; rows that must outlive their parent belong to an object with its own sharingModel (private, public_read or public_read_write, the enum's other three members).

Each accept-set and public-surface change the diff implies, judged at the head:

  1. One predicate, every parse door narrowed by one shape — right. controlledByParentMasterRefusal (controlled-by-parent-master.ts) gains the branch for exactly one counting master whose definition carries either flag === true, and returns master-not-writable at fields. Read in the cited code, that predicate is the only judge at: ObjectSchemaBase's own .superRefine (refuseControlledByParentWithoutOneMaster, object.zod.ts), so ObjectSchema.parse, .safeParse and create() (which forces required and ends in ObjectSchemaBase.parse) all carry it; ObjectStackDefinitionSchema.objects, an array of ObjectSchema (defineStack, os validate, os build); the save door (getMetadataTypeSchema('object') is ObjectSchema in kernel/metadata-type-schemas.ts, and protocol.ts reads that registry for its per-type parse); and the objectExtensions merge check in stack.zod.ts, which re-judges the merged field map with the same predicate and blames the extension that wrote the master key, with no code change. No second copy: the lint rule stays out of AUTHORING_RULES, and forceCbpMasterDetailRequired reads neither flag (its docblock now says so). Ruling item 2's rejections of B, C and D hold: nothing under packages/objectql or packages/plugins moves (13 files, none there).

  2. The flags are read === true only — right. FieldSchema.readonly is z.boolean().default(false) and system is optional, so the parsed map and a stored body reach the same verdict; an explicit false is accepted, and the tests pin it. The builder's unconditional readonly: true injection is on the autonumber builder alone, so no builder output walks into this refusal.

  3. isControlledByParentMasterField unchanged — right, and the no-runtime-change claim holds as read. The runtime resolver's own predicate (security-plugin.ts) is master_detail + required + a reference and reads neither flag, so the parse and the runtime still count one set: a flagged required master beside a second required one is two masters at both. Nothing in that file moves.

  4. Public surface of @objectstack/spec/data — right, and declared. ControlledByParentMasterShape gains the member master-not-writable; the predicate returns a refusal for a shape it previously accepted. No new export, key or accepted value. The api-surface and export-origins artifacts record the type's existence only, so no generated artifact moves. The one in-tree exhaustive consumer of the union (refused-shapes.ts) gains its case in the same diff; no other in-tree consumer switches on the union.

  5. Stored-row listing — right. refused-shapes.ts lists a stored object in the shape once under cbp-master-relation-tiers-retired, rewrites nothing, and its left-as-stored sentence for this shape names the save door's refusal and no runtime denial, while the four other shapes keep theirs. The non-null assertion on masterDetails.find(...) is safe: the fate reads the same fields map the refusal was judged on, where exactly one field counts. The listing and the parse refuse one set; the test pins the same key.

  6. ADR-0087 kit — right, and FAIL item 1 of 6334988367 is closed at the head. 18.cbp-master-relation-tiers-retired is amended rather than paralleled. Read at the head blob: its replacement scopes "the runtime derives no master from them: every read … denied" to the shapes that lose their master and then states the flagged master's fate separately (refused at parse by the same doors, naming the field and the flag; the runtime still resolves that field as the master; the object is not denied; drop the flag); its acceptanceCriteria scopes "is denied until it is corrected and saved" to the four named shapes and states the flagged row's fate as the listing does (registers under its diagnostic, the runtime still resolves its master, the save door refuses it until corrected); the TODO shape list names the new shape. The entry and refused-shapes.ts now say one thing. 18.cbp-master-detail-required-lint-error is corrected where this change made it false: its WHICH DOOR sentence names what the parse refuses on the one master and what stays the lint rule's alone, and its reason gains the parenthetical that ObjectSchema.create no longer lets the flagged shapes through.

  7. Lint package — right. relationship/master-detail-required is unchanged in reach; its docblock and os explain text say which shapes the parse refuses and which are the rule's alone; the two builder cases now pin the builder's refusal and the rule's report on the literal.

  8. Tests — right for the claims they pin. Spec: six new shapes agree between the export and ObjectSchema (each flag, both together, beside a required lookup, explicit false accepted, a flagged master beside a second required one still several-required-masters); the refusal names object, field and every flag; create() throws one issue at fields; the unflagged control builds. Conversions: one TODO under the entry's id, not rewritten, same parse key, no denial claim, including the Studio shape plus the flag after D2. Metadata-protocol: 422 INVALID_METADATA at fields, nothing stored, control saves; the engine double is pinned.

  9. Producer search (ruling item 3) — measured in full. In-repo, re-read at the head: every non-test controlled_by_parent declarant (examples/app-crm crm_opportunity_line_item, examples/app-showcase's expense and invoice lines, the four CLI lint-corpus objects) declares its one master required: true with neither flag; no YAML/JSON metadata declares the model (the access-matrix files are permission records, not objects). HotCRM: the census 6335022110 on controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092 names the tree (c8a7a7047f…), eight declarants, none flagged — zero producers, no fork. FAIL item 2 of 6334988367 is closed.

  10. Governed surfaces — none. No path under docs/adr, .claude, skills, AGENTS.md, CLAUDE.md or .github/workflows; Governed Surface Queue Guard is success.

Check-runs on the head (their conclusions are the gate verdicts). success: Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, filter. in_progress at the read (reported as pending, not waited for): Test Core (1/6 to 6/6) and Type Check · workspace (which holds the spec generated-artifact gates). skipped (opt-in or main-only): 18.x upgrade harness, Build Docs, Console Pin Gate, Dogfood Regression Gate shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. None is failure. This record vouches for the contract, not for the pending contexts: the landing waits for every check to be green, as the tier's rule says.

② Semver level

  • Changeset .changeset/25092-cbp-master-writable.md: @objectstack/spec: minor, @objectstack/lint: patch. Right. The kind is narrowed (a declaration the parse accepted is refused), which takes at least minor on one package whose published source it moves; spec carries it. The lint move is docblock and os explain text with no change in the rule's reach, so patch stands. packages/metadata-protocol moves a test file only and scripts/ publishes nothing: no entry owed. Check Changeset is success.
  • Clause-②: no (narrowing), on the PR body and in the changeset: true. No published accept set widens and no new export, key or accepted value appears; the one additive union member rides on an existing public discriminant and is declared in the body and the changeset. Metadata-protocol: narrowed: true (a requirement tightened). The BREAKING banner, the FROM → TO block and the one-line fix are in the changeset body; shipped as minor under the 18.x convention with no retirement stage, as the ruling's standing line binds.
  • ADR-0087 disposition marker: not-required (already-registered cbp-master-relation-tiers-retired), with the precedent wording "registered before this change, amended in this diff". Admitted and honest. The id pre-dates the merge base (the entry is modified, not added), the entry is amended in this diff to carry the narrowing in its replacement, reason and acceptance criteria, and the stored-row listing reports under it; the gate verifies resolve and pre-date, and the coverage half is verified here by reading. packages/spec/CHANGELOG.md records the same form for other amended step-18 entries.

③ Boundary flags

  • Dev open question — register under the existing parse entry (A) or a new step-18 entry (B)? Answered: A. Same parse rule, same predicate, and the id the listing already reports under; B would give one rule two ledger ids while the listing can name one. Precedent: the amended step-18 entries in packages/spec/CHANGELOG.md.
  • Dev flag: HotCRM not measured. Closed by the census 6335022110 (① 9). The PR body's "HotCRM: NOT MEASURED" sentence is superseded by that card comment; the card, not the body, carries the measurement.
  • Dev flag: the plugin-security test file calls the two flagged fixtures "authorable". Accepted as noted: the suite registers raw against an unchanged runtime and measures what it says; a stale word, carrier none, outside the claim's surface.
  • Dev flag: a flag on a second, non-required master_detail beside the master stays the lint rule's report alone. Accepted: ADR-0055 §1 decides on the one master, and the predicate judges only the counting master; the entry and the explanation say so.
  • Dev flag: os lint double-reports the flagged shape beside the parse's refusal. Accepted as the existing state for a missing required too.
  • Reviewer notes, not conditions. The PR body's verification section still names 85505b5f9 as the final head and the size as +377/−39; the head is one prose-only commit later (+386/−42 by the file list) and its gates are re-measured above. In 18.cbp-master-detail-required-lint-error's reason, the historical sentence "two of the three shapes still pass the builder" keeps the present tense ahead of the parenthetical that corrects it; the paragraph reads right as a whole, and a tense fix is owed the next time that file is touched, not now.
  • Reviewer note, abstract under the disclosure ruling. This PR's rule is exactly as wide as the ruling's named declaration and as its enforcement (the entry names the two flags). Whether any other field-level declaration can leave the one master in the same state is neither this PR's claim nor the ruling's; it is the filing seat's to measure privately, off GitHub, and is not a condition of this record.
  • Disclosure. The PR body, the changeset and the diff's prose state the declaration rule, its governing text, its prescription and the authoring doors that now refuse; they name no write door, request, served value or runtime mechanism beyond what the card and its comments already state. Within the ruling; nothing to redact.

Implemented-by: claude/project-thread-7afd2e
Reviewed-by: session_01B2EeooonSXrANpkpgUSfFZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 (#18549) · os-bill · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-22T07:52Z · head e71d385be010b036a4315bf7d8625d9ac8d2a274

⛔ Disclosure (#9387, 5323310017) holds: this record states the declaration rule and its prescription at the level the PR body does, and nothing further.

  1. Shape: draft, base main, one closing keyword (Fixes #25092, line 4); no other card number sits beside a closing verb. check-governed-merges.mjs --pr 25181: not governed, 428 changed lines ≤ 3000, so ordinary queue landing. mergeable_state: clean.
  2. Premise measured: ruling 6328204856 item 3 in full. In-repo: zero producers (the dev's search, re-confirmed by the review). HotCRM: zero producers among 8 controlled_by_parent declarants at c8a7a7047 (census 6335022110). This supersedes the PR body's "HotCRM: NOT MEASURED" line. Route A as ruled: one predicate in packages/spec, reached by every parse door, with no runtime change and no second copy (B, C and D not taken).
  3. Gates (check-runs on this head): 35 success, 7 skipped, 0 failed or pending. The skipped seven are the opt-in or main-only jobs. The TypeScript Type Check red on the previous head 85505b5f9 was its workspace lane cancelled by the push of this head, not a failure. Local: 102 of 102 dispatch-gates families run on 85505b5f9 (--ran exit 0) and dogfood 2395 passed / 18 skipped on that head. The prose-only commit to this head re-ran check-adr-0087-registration, check:spec-changes, check:upgrade-guide, check:generated and the touched suites, all exit 0.
  4. Contract review: PASS at CONTRACT_REVIEW_TIER on this head, record 6335293694, after the FAIL record 6334988367 on 85505b5f9. Both FAIL items are closed: the step-18 entry's fate text, and the HotCRM census.
  5. Accepted deviations: the branch is claude/project-thread-7afd2e, as claim 6332990224 names it. The PR body opens with the dispatch's attribution line.
  6. Prose faces (the seat's own read): the changeset (BREAKING banner, FROM → TO, the one-line fix, both declaration lines, one ADR-0087 marker in the precedent wording); the two step-18 entries; the os explain paragraph and the lint docblock; the refused-shapes.ts TODO text. Each matches the code at this head and stays within the ruling.
  7. Out of scope: the stale "authorable" word in a plugin-security test, noted in the PR (carrier none). Any follow-up measurement under the disclosure ruling is held privately by the seat, off GitHub.
  8. Landing: pr_ready + automerge_enable through the relay now. The seat follows it to MERGED.

Generated by Claude Code

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 protocol:data size/m tests tooling

Projects

None yet

2 participants