Skip to content

feat(spec,security): a controlled_by_parent object may declare an optional master - #30940

Queued
objectstack-fleet[bot] wants to merge 9 commits into
mainfrom
claude/project-thread-prdvgm
Queued

objectstack-fleet[bot] wants to merge 9 commits into
mainfrom
claude/project-thread-prdvgm

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#1 · os-elon-musk · session_016WV4cxZsyeZedvM4Ea8Ahs

Fixes #30905

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — #30907 (card 3/3 of the ruling: the docs and skills that teach controlled_by_parent still say the master is always required); this diff teaches the optional master in ADR-0055 §1 and §2 as amended, in os explain relationship/master-detail-required and os explain security-controlled-by-parent-no-relation, and in the semantic migration entry cbp-optional-master-admitted

Claim: 6685806956 (the seat's latest full re-post of 6682716818: this branch, the amended file surface, and five ui/ test files as a surface conditional on ruling A; supersedes 6684936456, 6683296158 and 6682753060) · Ruling: A, record 6687830530 · Branch: claude/project-thread-prdvgm

维护者速读

  • 改了什么:controlled_by_parent 的子对象现在可以把它唯一的 master_detail 字段写成 required: false(可选主表)。挂了主记录的行照旧由主记录决定谁能读;主表引用为空的行,只有它的属主(owner_id)和对该子对象有「查看全部」的人能读。另含 lint 规则、explain 叙述、ADR-0055 §1/§2 修订、升级清单的相应调整,以及 ui 里 5 个测试按裁决 A 改的对照断言(只改测试,Studio 源码不动)。
  • 为什么改:「销售看自己客户下的联系人;联系人可以不挂客户」以前没有声明式写法,只能复制一个属主字段再用钩子同步。这是您对 controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905 的裁决 A(三张卡的第 1 张)。
  • 风险与回滚:读侧只在唯一的读收口 computeControlledByParentFilter 里多了一个 $or 分支,仍在租户墙之下;必填主表的过滤条件逐字节不变。写侧不变:主表引用为空的写入仍然 422(第 2 张卡 controlled_by_parent admits an optional master (2/3): the write gate judges null-master rows (owner or modify-all), clearing a master needs edit on it, and the verify proof #30906 才改写闸)。回滚即撤 PR,没有数据迁移。
  • 先看:packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts 的新分支(约 30 行),再看新测试 controlled-by-parent-optional-master.test.ts 的读侧几条规则。
  • 两分钟验证:pnpm --filter @objectstack/plugin-security exec vitest run src/controlled-by-parent-optional-master.test.ts,预期 24/24 通过;把 owner 分支删掉再跑会红 6 条(见下文消融)。
  • 需要你做的:A/B/C 已裁 A(记录 6687830530),本 PR 按 A 实现,ui 的 5 个测试已随之改好。剩下的合同复审和 ADR 的 Tier H 批准由席位安排;Studio 不显示可选主表这个缺口,席位另开 ui 卡。

What changes

Spec (@objectstack/spec, minor). A controlled_by_parent object whose only master_detail is declared required: false, and which declares no required one, now has an optional master.

  • controlledByParentMasterOf(fields) is new in packages/spec/src/data/controlled-by-parent-master.ts, exported from @objectstack/spec/data: it answers the master field and whether it is optional (one required master_detail ⇒ that, not optional; more than one required ⇒ none; otherwise exactly one master_detail declared required: false ⇒ that, optional). controlledByParentMasterRefusal (the parse's one-master rule) judges through it. isControlledByParentMasterField still means the REQUIRED master.
  • ObjectSchema.create() (forceCbpMasterDetailRequired in object.zod.ts): an explicit required: false on the object's ONLY master_detail is kept as written; beside another master_detail it is still refused, with a message saying why. The force half (omitted required ⇒ true) is unchanged, and so is the D2 conversion cbp-master-detail-required-defaulted (its test file only gains rows that pin it).
  • Every other refusal stands: two masters, a required lookup only, a readonly / system master (required or optional).
  • conversions/refused-shapes.ts: the master-not-required arm no longer lists an explicit required: false sole master. A D3 semantic entry, migrations/entries/semantic/18.cbp-optional-master-admitted.ts (registered in registry.ts.template, order 119), records the widening.
  • readable-ids-conformance.ts gains a case for the leaf shape this produces: a leaf whose scope ORs a nested leaf, OR-ed with an empty-reference arm.
  • api-surface/data.json and export-origins/data.json regenerated (controlledByParentMasterOf).

Read (@objectstack/plugin-security, minor). computeControlledByParentFilter (security-plugin-cbp-read-filter.ts) is still the one read chokepoint. For a required master it returns the leaf it always did. For an optional master it returns

{ $or: [ { [fk]: { $in: ReadableIds.of(master) } }, nullMasterArm ] }

where nullMasterArm is { [fk]: null } for a holder of View All on the detail (permissionEvaluator.hasSuperuserReadBypass(object, permissionSets)), else { $and: [ { [fk]: null }, { owner_id: caller } ] } when the detail carries owner_id, else no arm (master arm only). The $or is this layer's answer, AND-ed by every caller with the tenant wall and the detail's own layers as before.

  • The relation resolver (security-plugin-cbp-relation.ts) picks the optional master through controlledByParentMasterOf when there is no required candidate, and CbpRelation carries optional and ownerColumn (security-plugin.ts). reportCbpMasterUnresolved's advice names both spellings.
  • The five call sites pass the permission sets they already resolved: the authz middleware for the caller and, on a delegated request, for the delegator; getReadFilter; and the delegated read filter for the delegate and the delegator. So View All is read for the principal each answer is for.
  • explain-engine.ts: security/explain on a record read names the null-master arm (which arm returned the row; for a withheld row with an empty reference, who reads it instead, and that a queue row is View All's alone).

Lint (@objectstack/lint, minor). relationship/master-detail-required passes the optional master, still reports readonly / system on it, and reports an omitted required on the only master_detail at error naming both spellings. validate-security-posture.ts resolves the optional master as the parse does, so security-controlled-by-parent-no-relation no longer calls a valid optional master "no relation". rule-explanations.ts teaches it.

ObjectQL (@objectstack/objectql, minor). engine-readable-ids-permission-formula.ts resolves the master through controlledByParentMasterOf, so the boot refusal of a master the engine cannot compose a read over judges an optional master's detail too.

Record (Tier H). docs/adr/0055-master-detail-controlled-by-parent.md: status line, inline notes at §1 and §2, and a new section "Amendment (2026-11-06, #30905): an optional master, and the null-master row its owner reads".

Ledger. scripts/engine-double-contract.pinned.json records the new suite's one pinned findOne double (check-engine-double-contract --write, no seam lost).

Why

Card #30905, maintainer ruling A: "a rep sees the contacts of the accounts they own; a contact need not have an account" had no declarative form. controlled_by_parent refused a master_detail that is not required at the builder and at the parse, a lookup derives no access, and the only route was a copied owner field kept in step by hooks. Premise re-read on main: both refusals are at the lines the card cites, object.zod.ts 4406-4417 (the builder's explicit-false throw) and 2743-2752 (the parse refinement), no drift. One gap the card did not name is the open question below.

Tests

New suite packages/plugins/plugin-security/src/controlled-by-parent-optional-master.test.ts (24 tests, both read faces: the data middleware and getReadFilter):

  • the injected leaf shape (master arm unchanged, OR-ed with fk IS NULL AND owner_id = caller), and a required master's leaf unchanged (no $or);
  • the rep reads the rows of masters they read and the null-master rows they own, nothing else;
  • a row that NAMES a master is never read through the owner arm (the rep owns a contact under an account they cannot read);
  • a caller who reads another master does not see the null-master rows they do not own;
  • a null-master row with no owner (an ADR-0142 queue row) is not read through the owner arm;
  • View All on the detail reads every null-master row, the queue row included, and does NOT open the master arm;
  • an object with no owner_id column: a null-master row is View All's alone;
  • chain: the children of an intermediate row with no master are read by that row's owner, exactly those;
  • the answer is the disjoint union of the two arms;
  • the write gate is unchanged: an empty master reference on insert is still 422 MISSING_REQUIRED_FIELD; an insert naming a readable master reaches the executor;
  • the resolver resolves the optional master (no "resolves no master relation" report), and the parse accepts the detail whole;
  • explain: a returned null-master row names the empty reference and the owner arm; a withheld one names who reads it, and a queue row is View All's alone; a row that names its master, and every row of a required master, keep the master-scope narration.

Updated pins: spec (controlled-by-parent-master.test.ts, object.test.ts, cbp-master-detail-required-defaulted.test.ts, refused-shapes.test.ts, stack-cbp-extension-master.test.ts (an extension making the one master optional passes like its declared control; beside a second master it is refused), and shared/retired-key-removal-version.pin.test.ts (the refusal the parse still answers on the defaulted conversion's surface is now two master_detail fields with no required)), lint (data-model-rules.master-detail-required.test.ts, validate-security-posture.test.ts, validate-security-posture-extension-merge.test.ts), controlled-by-parent-sharing.test.ts (the retired-tier table's omitted row now parses, pinned as the parse/builder divergence below), packages/metadata-protocol/src/protocol.stored-migration.test.ts (the stored required: false row is counted canonical, no longer listed), and five ui/packages/app-shell tests that used the spec as an oracle (below, under Verification). The readable-ids conformance suites passed on the memory, sql, sqlite-wasm, mongodb and turso drivers; the new case was confirmed by name on memory, sql and turso (local and remote).

Ablation (node scripts/ablation-replace.mjs, each restore verified blob equal to HEAD, 2ba0466291de for the read filter):

  • (a) the null-master arm dropped (nullMasterArm replaced with null): 6 red — the injected-shape pin, "the rep reads ... the null-master rows they own" on both faces, the chain case on both faces, and the disjoint-union case.
  • (b) the owner arm unconfined ({ owner_id: caller } without the fk IS NULL conjunct): 8 red — the six above plus "a row that NAMES a master is never read through the owner arm" on both faces.

Acceptance notes

  • A parse cannot tell an omitted required from required: false (the parsed field map defaults it to false). So under option A a raw parse, and defineStack over a plain object literal (it returns the parsed stack), admits an omitted required on the only master_detail as the optional master. The builder still forces it to true, the D2 conversion still converts a stored row, and os lint reports it at error. The runtime registry reads required === false, so an object that reaches it unparsed with required omitted still resolves no master and every read is denied. Pinned in controlled-by-parent-master.test.ts, object.test.ts, validate-security-posture.test.ts and controlled-by-parent-sharing.test.ts.
  • An extension that redeclares the master required: false makes it the optional master (validate-security-posture-extension-merge.test.ts).
  • A cycle or depth refusal of the chain still denies the whole object, null-master rows included.
  • The file surface grew beyond the first claim by the files the seat accepted in 6683296158: validate-security-posture.ts, rule-explanations.ts, engine-readable-ids-permission-formula.ts, a docblock in engine.ts, and the plumbing in security-plugin.ts, security-plugin-authz-middleware.ts, security-plugin-read-filter.ts. Declared to the seat after that: packages/metadata-protocol/src/protocol.stored-migration.test.ts (a test pin this change moves) and one docblock in packages/objectql/src/engine-top-level.ts (UnservableControlledByParentMaster.field, which the change made false). Generated or ledger files: api-surface/data.json, export-origins/data.json, scripts/engine-double-contract.pinned.json. On ruling A, the conditional surface of claim 6685806956: five ui/packages/app-shell test files, test-only.
  • forceCbpMasterDetailRequired is on the claim's ⛔ list for its force half; this diff changes only its explicit-false throw (the master-not-required refusal at ObjectSchema.create() the file surface names), narrowed to the object's only master_detail as the seat agreed. The force half and the D2 conversion source are unchanged.
  • Out of scope and untouched: the write gate (controlled_by_parent admits an optional master (2/3): the write gate judges null-master rows (owner or modify-all), clearing a master needs edit on it, and the verify proof #30906), the docs and skills (controlled_by_parent admits an optional master (3/3): docs and the authoring skills teach it instead of a copied owner field #30907), ui/** beyond those five test files (Studio source included), .claude/**.
  • Studio still writes required: true on a cbp master and its OWD overview names a master only for required: true (owd-sharing.ts deriveMasterObject), so it shows none for an optional master. The seat cards that display gap separately; this PR does not change it.

Ruling (maintainer, decision card)

FieldSchema.required is z.boolean().default(false) and the parse's cbp refinement reads the PARSED field map, so the parse cannot tell an explicit required: false from an omitted one. The card's "accept explicit false at the parse" and "omitted still means required" could not both hold at the doors that parse without the builder force or the D2 conversion (defineStack over a plain object literal, os validate / os build on such a config, the metadata save door). Three options went to the maintainer; A was ruled (record 6687830530):

  • A (implemented): the parse accepts the one master_detail that is not required: true, so at those raw doors an omitted required is an optional master, not a refusal. Omitted stays required where the omission is visible: ObjectSchema.create() (force unchanged), stored rows (D2 unchanged), Studio (writes required: true). os lint reports the omission at error. The runtime reads required === false, so an object that reaches it unparsed with no required key still resolves no master (fail closed). Consequence: a defineStack plain literal that omits required now passes and runs as an optional master.
  • B (not chosen): refuse the omission at every parse door through a new raw-input mechanism on ObjectSchema.
  • C (not chosen): a separate explicit opt-in key instead of required: false.

Verification

Head 1899fc5e98: the branch's four earlier commits (on main 98f640d5), a plain merge of main 1b3e22f0 (fd6d188b, one file in common, packages/spec/src/data/object.zod.ts, merged cleanly), the ui test commit 469499edc7, the contract-review fix commit fa5c3fee34 (the ADR-0055 invariant in five scripts/adr-anchors files moved to §1 as amended; ADR-0055's amendment cites ruling record 6687830530 instead of an open question), a plain merge of main 000a4c0d (c6541998, no file in common), and 1899fc5e98, test-only: module-top import '@objectstack/lint' in protocol.stored-migration.test.ts, and import '@objectstack/lint' + import '@objectstack/formula' in ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx, for the test-source alias gate's source-reach half (the sources keep their dynamic imports). The tables below were measured on 2374556f91 / 835c94d1 after a whole-tree turbo run build --filter=!@objectstack/docs (118/118 tasks, lock VERDICT command-exit 0); the delta since is listed after them.

Each run under the verify lock, VERDICT command-exit 0 for the lock, exit code per command recorded:

Package Command Result
spec, lint, objectql, plugin-security pnpm --filter @objectstack/NAME typecheck (tsc plus check:test-typecheck) exit 0 each
@objectstack/spec test (local project) 817 files passed, 1 skipped; 25856 tests passed, 1 skipped, 1 todo (the skip is scripts/root-entry-type-nameability.pin.test.ts's dist-freshness gate: spec test files changed after the build)
@objectstack/lint test 196 / 196 files; 8288 / 8288 tests
@objectstack/plugin-security test 247 / 247 files; 5710 passed, 45 skipped
@objectstack/objectql test (local project) 558 / 558 files; 11160 / 11160 tests
@objectstack/metadata-protocol test 320 files passed, 4 skipped; 22487 passed, 26 skipped
@objectstack/verify test 48 / 48 files; 408 / 408 tests
@objectstack/plugin-sharing test 67 / 67 files; 1436 / 1436 tests
@objectstack/service-analytics test 211 / 211 files; 5015 passed, 300 skipped
cli, plugin-audit, service-storage, rest the 13 test files there that name controlled_by_parent 13 / 13 files; 347 / 347 tests

The spec, lint, plugin-security and objectql full runs were taken on 835c94d1 (the source as here; the commit after it changes three test files and one docblock); the spec suite was then re-run whole on 2374556f91 (above), as were the rest.

Test UI under ruling A. On 2374556f91, Test UI was red in shards 1, 2, 7 and 16: six cases in five ui/packages/app-shell test files that used the spec's ObjectSchema as an oracle for "a controlled_by_parent object whose only master_detail omits required is refused". Under A the parse admits it as the optional master. Re-pointed test-only in 469499edc7 (Studio source unchanged):

  • four control legs now assert that shape parses with required: false before Studio writes required: true, and with required: true after: metadata-admin/previews/object-fields-io.test.ts, metadata-admin/inspectors/ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx, studio-design/ObjectSettingsPanel.cbpRequiredMaster-12326.test.tsx, and the save case of studio-design/PackageOwdOverviewPanel.cbpRequiredMaster-12326.test.tsx;
  • two display-agreement rows ("one master without required", in studio-design/owd-sharing.test.ts and in the shapes table of PackageOwdOverviewPanel.cbpRequiredMaster-12326.test.tsx) now record that the parse admits the shape while Studio names no master, as the runtime does for an unparsed object.

Run from ui/ on this head: pnpm exec vitest run on the five files, 5 / 5 files, 77 / 77 tests; tsc -p tsconfig.test.json --noEmit in ui/packages/app-shell, lock VERDICT command-exit 0, 0 errors. The whole Test UI matrix is read from this PR's check runs.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived the families; each ran sequentially with its exit code recorded before any pipe. Four needed a second run: check:dual-build-cjs-loads and check:i18n exit 3 (no dist) before the whole-tree build and exit 0 after; check-plugin-teardown-shape --self-test exit 3 (shallow clone, pinned fixture commit unreadable) and exit 0 after fetching that commit; check:engine-double-contract exit 1 (the new suite's double not in the ledger) and exit 0 after --write. pnpm lint: exit 0.
  • dispatch-gates --ran on 2374556f91: "108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED".
  • Delta on 469499edc7: the derivation grew to 112 families (check-sdui-manifest and its self-test, pnpm --dir ui exec vitest run scripts/, check:stack-collection-maps), all exit 0; every @objectstack/spec check:* family, check:doc-formula-expressions, check:engine-double-contract, check:nul-bytes and check:test-source-alias were re-run after the merge. Five spec checks exit 3 and check:generated exit 1 against the pre-merge dist; after turbo run build --filter=@objectstack/spec all six exit 0 and no artifact changed. pnpm lint exit 0. dispatch-gates --ran: "112 derived famil(ies) accounted for — 112 run, 0 NOT-MEASURED".
  • Delta on fa5c3fee34 (six files: five anchors, the ADR): dispatch-gates --commands on those paths derives 29 families, all exit 0 (check:adr-anchors, check:adr-links and its self-test, check:adr-index, check:adr-symbol-anchors among them); --ran: "29 derived famil(ies) accounted for — 29 run, 0 NOT-MEASURED". pnpm lint exit 0.
  • Delta on 1899fc5e98 (after the 000a4c0d merge, a spec rebuild and pnpm install for main's playwright-core): pnpm check:test-source-alias exit 0 (it was exit 1 on the two files before); protocol.stored-migration.test.ts 47 / 47; from ui/, ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx 5 / 5. dispatch-gates --commands on the two files derives 54 families, all exit 0 (check:type-check-debt exit 3 first, @objectstack/scenarios unbuilt, exit 0 after building it); --ran: "54 derived famil(ies) accounted for — 54 run, 0 NOT-MEASURED". pnpm lint exit 0.
  • After the merge, targeted re-runs: spec (object.test.ts, controlled-by-parent-master.test.ts, conversions/, stack-cbp-extension-master.test.ts, the retired-key pin, field.test.ts) 27 / 27 files, 1415 / 1415 tests; plugin-security optional-master and sharing suites 2 / 2 files, 63 / 63 tests.
  • Every family CI runs on this PR is read from the PR's check runs, not re-run here.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (controlledByParentMasterOf is a pure reading of a field map, beside the existing refusal.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is the maintainer's Chinese summary the fleet asks for.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…its null-master rows read by owner

ADR-0055 §1 and §2 as amended by maintainer ruling A: the only
master_detail of a controlled_by_parent object may be declared
required: false. A row that names its master is read through it as
before; a row whose master reference is empty is read by its owner and
by View All on the detail, through a second arm OR-ed with the master
arm inside computeControlledByParentFilter. Writes are unchanged.

Work in progress: tests for lint and plugin-security, the ADR and the
changesets follow.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
…0055 §1 and §2

One case per rule of the null-master arm on both read faces (the data
middleware and getReadFilter), the unchanged write gate, the explain
narration, the lint rules on the optional master, and a readable-ids
conformance case for a leaf whose scope ORs a nested leaf. ADR-0055 §1
and §2 amended; changeset; api-surface and export-origins regenerated.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
The new controlled-by-parent optional-master test file pins one findOne
double; check-engine-double-contract --write records it (no seam lost).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
…ter moves

Under ADR-0055 §1 as amended, an object's only master_detail declared
required: false is an optional master. Three pins outside the earlier
targeted runs still read it as refused:

- stack-cbp-extension-master: an extension making the one master optional
  now passes like its declared control; beside a second master it is
  refused (the twin).
- retired-key-removal-version pin: the parse reads an omitted required on
  the only master_detail as false, so the refusal it still answers on the
  defaulted conversion's surface is two master_detail fields with none.
- metadata-protocol stored migration: the required:false row is counted
  canonical, no longer listed under cbp-master-relation-tiers-retired.

And one docblock the change made false (UnservableControlledByParentMaster.field).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/lint, @objectstack/objectql, @objectstack/plugin-security, @objectstack/spec, touching 33 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/migrations/registry.ts.template), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 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 8ae0ab9b0d77d56473e89dae9fe437778172dda8.

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

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/migrations/registry.ts.template) — pages documenting those are invisible to this run
  • 4 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 80 pages), ObjectSchema (symbol, 45 pages), master_detail (literal, 40 pages), owner_id (literal, 38 pages)
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 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; 104 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 — 174 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 8ae0ab9b0d77d56473e89dae9fe437778172dda8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8d13c8f5326e373bb1769db64485f79e55da19f2 — the merge of head 1899fc5e98d39bbf682d4c71d9dfa33b5a0dc9fc into base 8ae0ab9b0d77d56473e89dae9fe437778172dda8, 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 8d13c8f5326e373bb1769db64485f79e55da19f2 && git checkout 8d13c8f5326e373bb1769db64485f79e55da19f2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8ae0ab9b0d77d56473e89dae9fe437778172dda8 1899fc5e98d39bbf682d4c71d9dfa33b5a0dc9fc && git checkout -B drift-repro 8ae0ab9b0d77d56473e89dae9fe437778172dda8 && git merge --no-ff 1899fc5e98d39bbf682d4c71d9dfa33b5a0dc9fc

node scripts/docs-audit/affected-docs.mjs --json 8ae0ab9b0d77d56473e89dae9fe437778172dda8

⚠️ 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 8ae0ab9b0d77d56473e89dae9fe437778172dda8 → 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

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract-review-hold: session_016WV4cytLmJxfAmgPVjUbbW · head 2374556 · the spec hunk's reading of an omitted required (A / B / C) is on the maintainer's decision card, and B or C would rewrite that hunk · until the maintainer rules, then this lane raises the CONTRACT_REVIEW_TIER review on the head that carries the ruling

Also owed before landing, unchanged: domain:spec consent for the spec hunk (all spec seats vacant, so the maintainer) and Tier H approval for the ADR-0055 amendment. The expected Test UI red named in the PR body is the conditional ui/ surface of claim 6684936456, edited only on a ruling of A.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Seat note · the expected Test UI red, measured in full · 2026-11-06T13:21Z

Head-sha: 2374556

  • Test UI on this head: all 16 shards ran; 4 shards red (1, 2, 7, 16), 6 red cases in 5 files under ui/packages/app-shell/src/views/. Every other check that has concluded is green. Nothing has been pushed since.
  • What they are: each one uses the spec's ObjectSchema as an oracle for "a lone master_detail with required omitted is refused". Four are control legs; two are display-agreement rows (Studio names a master exactly when the parse accepts). What Studio writes still parses under every option.
  • The claim's conditional ui/ surface is amended (claim 6685806956) to those 5 test files, test-only, and only if the maintainer rules A. Studio source stays untouched. owd-sharing.ts deriveMasterObject and the force in PackageOwdOverviewPanel.tsx are left out: under A the OWD panel would show "no master" for a shape the parse accepts, and that display gap becomes its own ui card.
  • Hold unchanged: the contract-review hold on this PR stands until the ruling. Under B or C these tests stay as they are.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract-review-hold lifted · 2026-11-06T14:58Z · the maintainer ruled A (ruling record 6687830530 on #30905). Hold 6685293293 no longer stands. The dev now pushes the test-only ui/ edits of claim 6685806956, and this lane runs the CONTRACT_REVIEW_TIER review on that head. Still owed: domain:spec consent for the spec hunk and Tier H approval for the ADR-0055 amendment.

Brings the branch up to main 1b3e22f. One file in common,
packages/spec/src/data/object.zod.ts (main's renamedFrom refinement);
merged cleanly.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
Under ADR-0055 §1 as amended (ruling A), the spec reads an object's only
master_detail that is not required as an optional master: a cbp object
whose lone master_detail omits `required` now parses, as required: false.
Five app-shell tests used that shape as a refused control.

- Four control legs (object-fields-io, ObjectFieldInspector,
  ObjectSettingsPanel, PackageOwdOverviewPanel save): the shape parses
  with required: false before Studio writes required: true, and with
  required: true after.
- Two display-agreement rows (owd-sharing deriveMasterObject,
  PackageOwdOverviewPanel shapes): the omitted row records that the parse
  admits it while the reader names no master, as the runtime does for an
  unparsed object.

Test-only; Studio source unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 469499edc7c7e84a9bc8de5b96b0f47f0ec85c9e
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/data/readable-ids-conformance.ts, packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts, packages/spec/src/migrations/registry.ts.template, docs/adr/0055-master-detail-controlled-by-parent.md + packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts, packages/plugins/plugin-security/src/security-plugin-cbp-relation.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts, packages/plugins/plugin-security/src/security-plugin-read-filter.ts, packages/plugins/plugin-security/src/explain-engine.ts, packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts (read, unchanged), packages/plugins/plugin-security/src/permission-evaluator.ts (read, unchanged), packages/plugins/plugin-security/src/controlled-by-parent-optional-master.test.ts, packages/lint/src/data-model-rules.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/validate-security-posture.ts, packages/objectql/src/engine-readable-ids-permission-formula.ts, packages/objectql/src/engine-top-level.ts, packages/objectql/src/engine.ts, packages/spec/src/conversions/entries/18/cbp-master-detail-required-defaulted.ts (read, unchanged), scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-read-filter.ts.json, scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-relation.ts.json, scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin.ts.json, scripts/adr-anchors/packages__spec__src__data__object.zod.ts.json (read, unchanged), .changeset/30905-cbp-optional-master.md, the five ui/packages/app-shell/src/views/** test files of commit 469499e

Reviewed against: card #30905 (body), claim 6685806956, dev report 6685276956, ruling record 6687830530 (option A: at the raw parse doors a cbp master with required omitted reads as optional; builder, stored rows and Studio still default to required; os lint errors on the omission), spec consent 6688103341, the PR body, ADR-0055 on main and on the head. Diff read whole against the merge base 1b3e22f0 (40 files, +1650 / -231). Head confirmed on the PR at the time of writing.

① Derived judgments

Must-fix

  1. Four ADR anchors still state the pre-amendment §1 rule as a "Do NOT" instruction. scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-read-filter.ts.json, …security-plugin-cbp-relation.ts.json, …security-plugin.ts.json and scripts/adr-anchors/packages__spec__src__data__object.zod.ts.json each carry the ADR-0055 invariant "derives a controlled_by_parent object's master from exactly one master_detail field declared required: true … and from nothing else: zero such fields … resolve null" and, for object.zod.ts, "no such field (a non-required master_detail, …) at sharingModel … do NOT accept a second tier here (any master_detail, …)". On this head the code those anchors bind resolves the only master_detail declared required: false as the optional master (security-plugin-cbp-relation.ts:169-181, object.zod.ts:4109 via controlled-by-parent-master.ts:168-179) and the ADR is amended to say so. check-adr-anchors.mjs is a presence check by design (its header: "the failure text carries the INVARIANT"), so CI stays green while the invariant it hands the next author now contradicts the amended ADR and the code — the exact hazard the ADR's own 2026-09-07 amendment names ("an ADR recording a narrower enforcement than the runtime performs errs in the unsafe direction for reviewers"). Fix: in each of the four files, amend the ADR-0055 sentence to §1 as amended (one master_detail declared required: true, or, with none, the object's only master_detail declared required: false — an optional master whose null-master rows are read by the row's owner and by View All on the detail through the one chokepoint; an omitted required on an unparsed object still resolves none), keeping the "do NOT restore the retired tiers (any master_detail, a required lookup, first-by-field-order)" instruction, which still holds. Not a governed path; no Tier H needed for it.

  2. The ADR amendment records a ruled question as open. docs/adr/0055-master-detail-controlled-by-parent.md:211: "Whether the parse should tell the two spellings apart is the maintainer's open question on controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905 (letters A, B and C); this amendment records option A, the one the code implements, and decides nothing further." The maintainer ruled A on that question at 14:57Z on 2026-11-06 (record 6687830530 on controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905), before this head was pushed. The ADR is the Tier H record the maintainer approves by hand; it must say the question was ruled, and cite the record. Fix: replace the sentence with one that names ruling record 6687830530 (option A) as the decision this bullet implements — the rest of the bullet is accurate as written.

Should-fix (for the seat; none blocks on its own)

  1. packages/spec/src/conversions/entries/18/cbp-master-detail-required-defaulted.ts:56-67 (unchanged, on the claim's ⛔ list) still says the builder refuses an explicit required: false and that a live author "still meets the parse's refusal"; both are false for the sole master under ruling A. The dev reported it (out-of-scope finding c). One sentence in this PR with the seat's consent, or in controlled_by_parent admits an optional master (3/3): docs and the authoring skills teach it instead of a copied owner field #30907.
  2. packages/plugins/plugin-security/src/explain-engine.ts:2042-2068: on a withheld null-master row the narration says "it has no owner (a queue row), so View All alone reads it" whenever record.owner_id == null. On an object with no owner_id column that is every row, and such a row is not a queue row. Narration only; the verdict is the read door's. Could read "it carries no owner" when CbpRelation.ownerColumn is false.
  3. Studio cannot author an optional master and the OWD overview shows none for one (owd-sharing.ts deriveMasterObject reads required === true); os db orphans skips optional-master details (orphan-detail-sweep.ts). Both already in the seat's findings list on record 6687830530; recorded here so the carrier is not lost.

Confirmed (file:line on the head)

  • The one read chokepoint. security-plugin-cbp-read-filter.ts:109-184 is the only place that builds the cbp read answer; every door composes it and none builds an arm of its own: the authz middleware for the caller and, on a delegated read, the delegator (security-plugin-authz-middleware.ts:2361, :2380, each pushed into the same extra that carries the Layer-0 tenant wall, :2347-2349, and AND-ed); getReadFilter (security-plugin-read-filter.ts:159, AND-ed with computeRlsFilter, which carries the wall, and the sharing layer, :164) — the door analytics, the engine's ReadableIds composer (attachments, comments, activity, approvals, and every chain hop) and search/export reach; the delegated read filter for the delegate and the delegator (:234, :236). The three hosts in security-plugin.ts:2218, :2898, :2955 forward the new argument; the private method :4878-4900 adds permissionEvaluator to the host. git grep on the head finds no other caller.
  • The arms. :159: a required master returns masterArm, the same object literal as before ({ [fk]: { $in: ReadableIds.of(master) } }) — no $or; pinned by "a REQUIRED master answers the leaf it always did" (controlled-by-parent-optional-master.test.ts:987-997). :178-184: for an optional master the null arm is { fk: null } for a holder of View All on the detail, else { $and: [{ fk: null }, { owner_id: caller }] } when the registered schema carries owner_id (CbpRelation.ownerColumn, security-plugin-cbp-relation.ts:181, :226), else no arm. A row with a set fk is matched by the master arm alone ($in never matches null); a null fk never by the master arm; a null fk with a null owner matches nobody but View All; an object without owner_id grants nothing on the null arm. hasSuperuserReadBypass is viewAllRecords/modifyAllRecords on the object (permission-evaluator.ts:475-481, bit view); its isPrivate option is documented as never changing that vote (:452), so omitting it here is not a divergence from security-plugin-rls-filter.ts:183/:374.
  • Fail-closed order kept. The deny sentinel for an unresolved relation (:130) and the $in: [] cycle/depth answers (:139-156) return before the optional branch, so a refused chain denies null-master rows too, as the ADR says.
  • Verbs. CBP_LEAF_READ_VERBS = find, findOne, count, aggregate (:83): count and aggregate carry the $or; a write verb's predicate is still answered null.
  • Engine composition of the new shape. The engine composes ReadableIds leaves inside $or arms (engine-readable-ids-composition.test.ts:302-308, engine-readable-ids-nested-composition.test.ts:388-393), and a chain's intermediate level with an optional master answers its getReadFilter as the $or, so the next level's leaf scope carries it (readable-ids-conformance.ts:273-300). The new conformance case's expected rows check by hand against the fixture (:99-126): inner parents {p2 (owner u2), p3 (owner u1, id p3)} → children c2, c3; the empty-reference arm admits c5 (record_id: null, label: 'keep'); c1, c4, c6, c7 excluded. The analytics raw-SQL path still refuses an uncomposed leaf (read-scope-sql.ts:8-12, :1059, the finder walks $or arrays) and the engine-bound door admits it (service-analytics/src/plugin.ts:1088-1091) — unchanged posture.
  • Relation resolver. security-plugin-cbp-relation.ts:164-172: a required candidate still wins; with none, controlledByParentMasterOf over the registry field map picks the only master_detail declared required: false, the reference read with the same tolerant spelling; optional: candidates.length === 0 (:225); an omitted required on an unparsed object is undefined !== false, so it resolves none and every read is denied (the ADR's fail-closed boundary). reportCbpMasterUnresolved names both spellings for a single non-required master (:254-267).
  • Writes unchanged. No write-gate or validation file is in the diff. security-plugin-cbp-write-gate.ts:312-313 (an update body that empties the reference) and :476-477 (an insert with no master, a by-id write on a null-fk row) still throw MasterReferenceMissingError (errors.ts:289-292: MISSING_REQUIRED_FIELD, 422); for an optional master omissionRefusedByValidation is false (security-plugin-cbp-relation.ts:215-216), so the gate, not record validation, answers — same code either way. Pinned at controlled-by-parent-optional-master.test.ts:1054-1071.
  • Spec. controlled-by-parent-master.ts:168-179: one required → that; more than one required → none; else exactly one master_detail with required === false → optional; required omitted, non-boolean, two non-required, or a required lookup only → none. controlledByParentMasterRefusal:206-290 judges through it: the master-not-writable arm covers the optional master (:211-230); the several-masters / master-not-required / required-lookup-only / no-master shapes stand with messages naming both spellings. object.zod.ts:4109 runs the refinement on the parse. forceCbpMasterDetailRequired:4514-4547: required: true kept; required: false kept only on the object's only master_detail (:4530), refused beside another with a message naming the other (:4531-4543); omitted → forced true (:4544-4545) — the force half is unchanged, as is cbp-master-detail-required-defaulted.ts. refused-shapes.ts:148-161 retires the explicit-false arm (what is left is a non-boolean required, which the field parse refuses on its own; an omitted one is converted by the chain first), and the master-not-writable fate names the optional master (:172-177). D3 entry 18.cbp-optional-master-admitted.ts registered at registry.ts.template:1321-1332 (order 119, unique); the sibling entries it names (cbp-master-relation-tiers-retired, cbp-master-detail-required-forced) exist. api-surface/data.json and export-origins/data.json carry the two new exports. The recorded divergence (raw required === false vs the parse's defaulted false) is pinned in controlled-by-parent-master.test.ts:1940-1954, object.test.ts:2373-2394, validate-security-posture.test.ts:484-502, controlled-by-parent-sharing.test.ts:1165-1192.
  • Lint. data-model-rules.ts:407-435: relationship/master-detail-required passes the only master_detail declared required: false, reports readonly/system on it, reports an omitted or non-boolean required on the sole one at error naming both spellings, and judges false beside another as before; masterDetailCount is read from the object's own field list (:1052). validate-security-posture.ts:413-427: cbpMasterCandidates returns the required masters, else the spec resolver's optional one, so security-controlled-by-parent-no-relation is silent on a valid optional master and still fires on two non-required masters and on an omitted required (the parse/lint divergence pinned at validate-security-posture.test.ts:484-502). rule-explanations.ts:716-735 (security-controlled-by-parent-no-relation) and :3302-3340 (relationship/master-detail-required, the optional-master paragraph at :3318) teach it. No runtime string in the diff carries a tracker number.
  • objectql boot. engine-readable-ids-permission-formula.ts:218-226: findUnservableControlledByParentMasters resolves the master through controlledByParentMasterOf, so an optional-master detail is judged for an uncomposable master too; docblocks at engine-top-level.ts:4931-4935 and engine.ts:1650-1663 say so.
  • ADR-0055. Additions only (the ADR hunk has no removed line): a status-line entry (:8), inline notes under §1 (:40) and §2 (:52), and the amendment section (:200-233). The amendment's statements match the code read above: the arm shapes, View All confined to the null arm, the queue row and the no-owner_id object as View All's alone, the chain rule, the fail-closed boundaries, §3 unchanged. One sentence is stale (must-fix 2).
  • The ui test-only commit 469499e. Five files, all *.test.ts(x), +87/-29, no Studio source. The four control legs now assert that the omitted shape parses with required: false before Studio writes required: true, and with required: true after (object-fields-io.test.ts:412-420, ObjectFieldInspector…:111-123, ObjectSettingsPanel…:59-63, :77-79, PackageOwdOverviewPanel…:105-109, :125-127); the two display rows (owd-sharing.test.ts:66-71, :92-95; PackageOwdOverviewPanel…:67-72, :85-88) record that the parse admits the omitted shape while the reader names no master — ruling A, encoded correctly; the "two required masters" rows still expect a refusal.
  • The merge fd6d188. git merge-tree --write-tree of its two parents reproduces its tree (d4b233c0…) exactly: the automatic merge, no hand edits; one file in common (object.zod.ts, main's renamedFrom refinement), and the diff against the merge base carries only this PR's hunks.
  • Tests and ablations, by reading. The new suite (controlled-by-parent-optional-master.test.ts, 24 cases) runs the real plugin and sharing service over an in-memory double on both read faces. Dropping the null arm reds the injected-shape pin (:971-985), "the rep reads … the null-master rows they own" on both faces, the chain case on both faces and the disjoint union — six, as reported. Unconfining the owner arm (dropping the fk IS NULL conjunct) makes ct_eu_mine (owned by the rep, under an unreadable master) and nt_eu_mine visible, so it reds those six plus "a row that NAMES a master is never read through the owner arm" on both faces — eight, as reported. Pins on a required master's leaf, View All's confinement, the queue row, the no-owner_id object, the write gate, the resolver and the explain narration are each present.
  • Check-runs on the head, latest per name, at the time of writing. Green: Lint & Repo Gates, Governed Surface Queue Guard, Lint · ESLint, Lint · CI/release & engine gates, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, the three scoped lint lanes, Type Check · source gates, Type Check · debt ledger, Check Changeset, Dogfood Regression Gate, Spec property liveness, PR-body guards, Plan Test Core shards, Build UI Deps, Check Documentation Links, Flag docs, filter. Pending: Build Core, Test Core 1-4 and 6-8 (5/8 green), Test UI 1-16, Type Check · workspace, Type Check · consumer gates, Type Check UI, Lint UI, Build & E2E UI, Live E2E UI. Red: none.

② Semver level

  • @objectstack/spec minor — right: two new exports (controlledByParentMasterOf, ControlledByParentMaster), a shape the parse and the builder now accept, a D3 entry; nothing removed or narrowed; api-surface and export-origins regenerated.
  • @objectstack/plugin-security minor — right: a behavioural widening on one object shape; computeControlledByParentFilter's new required argument is on a private method, and security-plugin-cbp-read-filter.ts is not re-exported from the package index.
  • @objectstack/lint minor — right: one spelling passes, messages reworded, severities unchanged.
  • @objectstack/objectql minor — acceptable: the boot refusal now also judges an optional-master detail whose master the engine cannot compose. The only shape it newly refuses is one the parse refused before and the runtime served as "every read denied"; refusing it at boot is the ADR-0139 D5 posture, not a narrowing of a delivered capability. Recorded here so the reader sees it was judged.
  • Metadata-protocol: added — right (a new accepted authorable shape, no key added).
  • Clause-②: yes (widening) — right. Looked for a narrowing and found none: before this head the sole-master required: false shape was refused at ObjectSchema.create() and at the parse, listed [metadata_spec_invalid] at rest with every read denied, and no reader could read a null-master row (the write gate refused one, and a raw-inserted one matched no $in); the required master's leaf is byte-identical; every lint error that fired still fires at error. @objectstack/metadata-protocol and ui/ change only tests, so no changeset is owed for them.

③ Boundary flags

④ Design judgment

  • Schema face against the ruling and its first consumer. The change puts the optional master into one spec resolver (controlledByParentMasterOf) and has the parse, the builder, the runtime relation resolver, the lint's posture rule and the engine's boot refusal all read it, so "which field is the master" is spelled once. isControlledByParentMasterField keeps meaning the required master, which is what keeps two required masters a refusal whatever else the object declares. That holds against ruling A and against its first consumer (resolveCbpRelation). The deliberate divergence — the raw predicate reads required === false, the parsed map defaults an omission to false — is the consequence the maintainer chose in option A, is fail-closed where it matters (an unparsed registry object with no required resolves nothing), is reported by os lint at error, and is pinned in four suites. Sound.
  • Read design. One $or inside the cbp layer, AND-ed under the tenant wall and beside the detail's own RLS and sharing layers by every caller as before, is the smallest shape that gives the ruling its rows; the arms are disjoint by construction ($in versus IS NULL), View All is confined to the null arm, and the chain composes through the existing engine leaf without a new mechanism. The driver-side shape is covered by a new conformance case on the shared table every driver suite runs. Sound.
  • ADR against its consumer. The §1/§2 notes and the amendment describe what the code does, in the additions-only convention the record already uses; one sentence lags the ruling it should cite (must-fix 2). The anchors that point code at this ADR were not moved with it (must-fix 1): on a security read path, a "Do NOT accept" instruction that now contradicts the accepted ADR is the reviewer-facing hazard this ADR's own history warns about, so it is held to the same bar as the record.
  • Upgrade record. Retiring the explicit-false arm of refused-shapes.ts and recording the widening as a D3 entry beside the step-18 retirement is the right pair: nothing at rest is rewritten, a stored row in the shape stops being listed and denied, and the omitted spelling keeps its conversion.

Implemented-by: claude/project-thread-prdvgm
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: FAIL

…ling

The ADR-0055 invariant in five anchors under scripts/adr-anchors still
stated the pre-amendment §1 rule as a Do-NOT instruction. Each now states
§1 as amended: the required master, or the only master_detail declared
required: false as an optional master, read through the null-master arm
by the row's owner and by View All on the detail. The instruction not to
restore the retired tiers stays.

ADR-0055's amendment named the parse question as open. It now cites the
maintainer's ruling record 6687830530 (option A) as the decision the
bullet implements.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review · delta

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fa5c3fee345f6855870baebc472280021967d646
Local-runs: none
Scope: docs/adr/0055-master-detail-controlled-by-parent.md, scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-read-filter.ts.json, scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-relation.ts.json, scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-write-gate.ts.json, scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin.ts.json, scripts/adr-anchors/packages__spec__src__data__object.zod.ts.json + (read, unchanged on this head) packages/plugins/plugin-security/src/security-plugin-cbp-relation.ts, packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts, packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts, packages/plugins/plugin-security/src/errors.ts, packages/spec/src/data/controlled-by-parent-master.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/data/field.zod.ts, scripts/check-adr-anchors.mjs

Delta over record 6688757325 (head 469499ed…). That record was FAIL with two must-fixes, both on the ADR record surface; the code was judged sound and is not re-reviewed here. One commit, fa5c3fee, parent 469499ed (nothing else rode along). git diff --stat between the two heads is exactly six files, one line each (+6 / −6): the four anchors named in must-fix 1, the ADR line named in must-fix 2, and the write-gate anchor, which carried the same stale sentence. Reviewed against: record 6688757325, ruling record 6687830530 on #30905 (the only ruling record on that card; the §1 widening itself is the card body's ruling 「A」), ADR-0055 on the head, and the code on the head. Head confirmed on the PR (draft, same-repo branch claude/project-thread-prdvgm) at the time of writing.

① Derived judgments

Must-fix — none. Both must-fixes of 6688757325 are closed.

  1. Closed: the anchors. All five anchors now open with "ADR-0055 §1 (as amended 2026-11-06, controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905, ruling record 6687830530)". The four plugin-security anchors carry one identical ADR-0055 paragraph (sha1 c6bbfcd1…); the object.zod.ts anchor carries a parse-side version. Each statement checked against the head:

    • "exactly one master_detail declared required: true … or, with none, the object's ONLY master_detail, declared required: false … picked by the spec's own controlledByParentMasterOf" — security-plugin-cbp-relation.ts:164-174 (candidates = required; judged = controlledByParentMasterOf(...) only when none; found only if judged.optional and the entry names a reference); controlled-by-parent-master.ts:168-179 (one required → it; more than one → none; else exactly one master_detail and required === false → optional).
    • "CbpRelation.optional" — security-plugin-cbp-relation.ts:225 (optional: candidates.length === 0).
    • "a required master's read leaf is byte-identical to before" — security-plugin-cbp-read-filter.ts:156-159 (masterArm returned alone when !rel.optional).
    • "read through the null-master arm … by the row's owner (owner_id, only where the detail carries that column) and by View All on the detail" — :177-184 (hasSuperuserReadBypass → {fk: null}; else rel.ownerColumn → {$and: [{fk: null}, {owner_id: caller}]}; else no arm); ownerColumn: hasOwnerColumn at security-plugin-cbp-relation.ts:226.
    • "the write gate still refuses a write that leaves the reference empty" — security-plugin-cbp-write-gate.ts:312-313, :476-477, :695 all throw MasterReferenceMissingError; for an optional master omissionRefusedByValidation is false (security-plugin-cbp-relation.ts:215-216), so the gate answers.
    • "Anything else resolves null … reads answer RLS_DENY_FILTER, writes throw MasterDetailRelationMissingError, 422 INVALID_METADATA" — security-plugin-cbp-read-filter.ts:130; security-plugin-cbp-write-gate.ts:277; errors.ts:162-165. Shapes listed (no master_detail, two or more required, two or more with none required, one whose required is not written) each resolve undefined in controlledByParentMasterOf — the last because it tests required === false, never absence, as the anchor says.
    • "Do NOT restore the retired fallback tiers (any master_detail, then a required lookup) or a first-by-field-order pick among two … cbp-master-relation-tiers-retired" — kept verbatim in the four plugin-security anchors; restated in the object.zod.ts anchor as "Do NOT accept a retired tier here (a master_detail that is not the one master by the rule above, a required lookup, a first-by-field-order pick among two)". The reference-spelling tolerance sentence is kept.
    • New instruction "Do NOT widen the null-master arm past rows whose master reference is empty, or let View All on the detail open the master arm" — matches the code at :177-184 and the ADR's "The rules the read keeps".
    • object.zod.ts anchor's refusal list: master-not-writable (readonly/system, required or optional) and several-required-masters at fields, every other shape at sharingModel — controlled-by-parent-master.ts:211-230, :232-245, :272-290. "The parse judges required after its default, so an omitted required on the only master_detail parses as the optional master" — field.zod.ts:1323 (required: z.boolean().default(false)), refinement at object.zod.ts:4109. "the builder still forces the omission to true" — forceCbpMasterDetailRequired, object.zod.ts:4544-4545; "beside another master_detail it still refuses it" is the builder's :4530-4543. "os lint reports it at error" — confirmed in 6688757325 (data-model-rules.ts:407-435), unchanged on this head.
    • Nothing in any anchor contradicts the code or the ADR on the head.
    • The non-ADR-0055 paragraphs of security-plugin.ts.json (ADR-0106 D7, ADR-0123 D2, ADR-0138 D1) and of object.zod.ts.json (ADR-0120 D1, ADR-0144 D2/D5) are byte-identical across the two heads; the file and adrs keys of all five anchors are unchanged; all five parse as JSON.
    • The write-gate anchor. Including it is right. Its ADR-0055 sentence was verbatim the same stale one as the relation anchor's, and the write gate reads resolveCbpRelation (:271) and rel.omissionRefusedByValidation (:312, :476), whose meaning changed with the optional master. The new text says of the gate exactly what the code does (it refuses an empty reference). Nothing in it is false. check-adr-anchors.mjs is a presence check (the code file still cites ADR-0055, 14 times), so this edit cannot red it.
  2. Closed: the ADR line. docs/adr/0055-master-detail-controlled-by-parent.md:211 now reads: "Whether the parse should tell the two spellings apart was ruled by the maintainer on controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905 (record 6687830530, 2026-11-06): option A, the parse does not tell them apart, and this bullet implements that ruling; option B (refusing the omitted spelling at every door through a new raw-input check) and option C (a separate explicit key) were not chosen." Checked against the record: it sits on controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905, 2026-11-06T14:57Z; A = "at the raw parse doors … a cbp master with required omitted is read as an optional master" ("does not tell them apart" is a faithful paraphrase, and the bullet's preceding sentences spell the behaviour out); B and C are quoted faithfully. The change is confined to that one bullet (the ADR hunk is one line removed, one added); the rest of the bullet and of the amendment are as the full record confirmed. The ADR remains accurate overall.

Should-fix (for the seat; none blocks)

  1. Attribution nit in the anchors' opening parenthetical. "as amended 2026-11-06, controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905, ruling record 6687830530" reads as if 6687830530 ruled the §1 widening. That record ruled the omitted-required parse question; the §1 widening itself is the card body's ruling 「A」 (no record comment id exists for it, and 6687830530 is the only ruling record on controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905). Every fact the anchors state under that heading is true, and the ADR they point at carries both rulings, so this is wording only. If touched: "controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905 (card ruling 「A」; parse ruling record 6687830530)".
  2. Should-fixes 3-5 of 6688757325 (the D2 conversion docblock, the explain narration on an object with no owner_id, the Studio / os db orphans gaps) are untouched by this commit and still stand for the seat's list.

Confirmed

  • fa5c3fee parent is 469499ed; git diff --stat is the six files above, +6 / −6.
  • The ADR header's 2026-11-06 entry (:8), the §1 and §2 inline notes (:40, :52) and the amendment section (:200-233) are unchanged from the head the full record read.
  • Check-runs on fa5c3fee at 15:57Z (pushed 15:54Z), latest per name, 42 runs: 12 success, 8 skipped (opt-in and schedule-gated lanes), 22 in progress, 0 red. Green: 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, PR-body guards, Plan Test Core shards, Spec property liveness, filter. Pending: Build Core, Build UI Deps, Lint · CI/release & engine gates, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, the three scoped lint lanes, Live E2E UI, Test Core 1-8, Type Check · consumer gates / debt ledger / source gates / workspace. The anchor checker's lane (Lint · repo & tooling gates / docs & code guards) is among the pending ones; by reading, it cannot red on this diff. Test UI and Lint UI runs had not been created yet at the time of writing.

② Semver level

Unchanged from 6688757325. This commit touches no package source: docs/** and scripts/adr-anchors/** only. No changeset change is owed. @objectstack/spec, @objectstack/plugin-security, @objectstack/lint, @objectstack/objectql stay minor; Metadata-protocol: added; Clause-②: yes (widening).

③ Boundary flags

④ Design judgment

  • Anchors against the ADR and their consumer. The five anchors now hand the next author §1 as amended, in one shared paragraph on the plugin-security side and a parse-flavoured one on the spec side, each naming the resolver the two sides share (controlledByParentMasterOf) so the reviewer-facing hazard the full record named is closed in the same direction as the code. Keeping the retired-tiers instruction and adding the two null-master-arm confinements is the right pair: the old "Do NOT" still binds, and the new widening is fenced where it could next be widened wrongly. Sound.
  • ADR against the ruling. One sentence changed, and it now says what happened: the question was ruled, which record ruled it, what A means for the parse, and what B and C were. Additions-only convention preserved. Sound.

Implemented-by: claude/project-thread-prdvgm
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: PASS

@objectstack-fleet

objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

维护者速读

  • 改了什么:controlled_by_parent 对象可以声明一个可选主字段(唯一一个声明 required: false 的 master_detail)。主字段为空的明细行:有 owner_id 时归行主人可读,另外明细对象上的 View All 可读;写入仍拒绝空引用。ADR-0055 §1 同步修订,五个 ADR 锚点改为修订后的说法。
  • 为什么改:这是你 2026-11-06T14:57:00Z 在 controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905 上裁决 A 的落地(记录 6687830530):原始解析时省略 required 视为可选主字段,builder、已存行和 Studio 仍默认必填,os lint 对省略报错。
  • 风险与回滚:读路径只在一个收口(computeControlledByParentFilter)加了「主字段为空」分支,必填主字段的过滤器逐字节不变并有测试钉住;写路径不变。改动 45 个文件 +1655 / -236;回滚即撤 PR。
  • 先看:docs/adr/0055-master-detail-controlled-by-parent.md 的 2026-11-06 修订段,和 packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts 里新加的空主字段分支。
  • 两分钟验证:Files 标签里 ADR 只增不改(第 211 行改为引用裁决记录);controlled-by-parent-optional-master.test.ts 里三种读者(行主人、View All、其他人)各有一条用例。
  • 需要你做的:docs/adr/** 属 Tier H,请 os-zhuang 或 hotlong 任一位 APPROVE(或维护者亲手合并)。契约复核已过:全量记录 6688757325 的两条必改已修,delta PASS 记录 6689219963 与 6690252532(后者对应现 head:合 main + 两个测试文件补顶层 import),Head-sha 1899fc5e98d39bbf682d4c71d9dfa33b5a0dc9fc。批准后由席位开 ready 并挂自动合并。
  • 席位意见:同意落地。代码全量复核即无必改,两条必改都在 ADR 记录面且已修;spec 变更你已同意(记录 6688103341)。

席位核对 · ACCEPT · 2026-11-06T16:00Z · #30905 (cbp optional master, card 1/3, Tier H)

Head-sha: 1899fc5

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 6, 2026
Brings the branch up to main 000a4c0, so the branch runs the test-source
alias gate CI runs. No file in common with this branch; merged cleanly.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
… top

The test-source alias gate's source-reach half judges the test files a
change edits. Two of this branch's reach a function-body import() in the
source under test with nothing loading it at module scope:

- protocol.stored-migration.test.ts reaches import('@objectstack/lint')
  in metadata-diagnostics.ts;
- ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx reaches
  import('@objectstack/lint') (objectPickerClosure.ts, celAuthoring.ts)
  and import('@objectstack/formula') (celAuthoring.ts).

Each now imports those specifiers at module top, so the first load is
paid at collection. The source keeps its dynamic imports.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxZsyeZedvM4Ea8Ahs
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review · delta

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1899fc5e98d39bbf682d4c71d9dfa33b5a0dc9fc
Local-runs: none
Scope: packages/metadata-protocol/src/protocol.stored-migration.test.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx + (read, for the gate and the loads) scripts/check-test-source-alias.mjs (header "The source reach", reachEdgesOf, sourceReachFindings), packages/metadata-protocol/package.json, ui/packages/app-shell/package.json, packages/lint/package.json, packages/lint/src/index.ts, packages/formula/package.json, packages/formula/src/index.ts, packages/metadata-protocol/src/metadata-diagnostics.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/objectPickerClosure.ts, ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectPicker.closure-28240.test.tsx, ui/packages/app-shell/src/preview/tests/DraftChangesPanel.securityLink.test.tsx, ui/packages/app-shell/src/preview/tests/DraftChangesPanel.securityProblems.test.tsx

Delta over record 6689219963 (head fa5c3fee…), which was PASS with no must-fix. Two commits since, both by the seat, nothing else rode along: c6541998 (merge of origin/main 000a4c0d into the branch, parents fa5c3fee + 000a4c0d) and 1899fc5e (test-only, parent c6541998). The ADR/anchor surface and the code the two prior records reviewed are not re-reviewed here; this delta checks that they are unchanged and that the two new commits are what they claim. Head confirmed on the PR (draft, same-repo branch claude/project-thread-prdvgm, auto-merge not armed, 45 files) at the time of writing.

① Derived judgments

Must-fix — none.

  1. The merge is a plain merge. git merge-tree --write-tree fa5c3fee 000a4c0d yields tree e5c16ea6…; c6541998^{tree} is e5c16ea6…. Tree-equal: no hand edit rode in on the merge commit. 000a4c0d is on origin/main. Main's 137 files between the merge-base (1b3e22f0) and 000a4c0d share no path with the PR's 45 files (intersection empty). The PR's diff against main, taken before the merge (1b3e22f0..fa5c3fee) and after (000a4c0d..1899fc5e), differs only by the two import hunks of 1899fc5e (the index lines aside). So main's merge brought no change to any reviewed file, and the reviewed behaviour is byte-for-byte what 6688757325 and 6689219963 read. Overlap list: none.

  2. 1899fc5e is exactly the two test files. --stat: 2 files, +11 / −0. protocol.stored-migration.test.ts gains a four-line comment and import '@objectstack/lint'; directly after the vitest import at module top; ObjectFieldInspector.cbpRequiredMaster-12326.test.tsx gains a four-line comment and import '@objectstack/lint'; / import '@objectstack/formula'; after the ../i18n import at module top. No source file, no config, no snapshot. Both files are already in the PR's 45 (added by it), so they are in the gate's "changed test files" scope.

  3. The imports clear main's source-reach gate, by reading. The gate (scripts/check-test-source-alias.mjs, now on this head via the merge, ci(test-source-alias): follow a changed test into the source it loads, and red a cold literal import() paid there #30969) walks a changed test's collection-time reach into same-package files and reports a function-body import('<measured spec>') it finds there unless "some file in the same reach loads that specifier at module scope" or the test mocks it; the remedy it prints is "a module-top import '<spec>' in the test file". In sourceReachFindings, warm is seeded from the test file's own moduleScope (root.warm) and a finding is dropped when warm.has(spec). A module-top bare import '@objectstack/lint' is in that set, so the finding is suppressed. The old head's lint lane log (run 127993049558) shows exactly the findings these close: protocol.stored-migration.test.ts → protocol.ts → metadata-diagnostics.ts:494 (import('@objectstack/lint')), and the inspector test → ObjectFieldInspector.tsx → ObjectPicker.tsx → objectPickerClosure.ts:57 / celAuthoring.ts:281,388 (lint) and → CelPredicateField.tsx → celAuthoring.ts:252 (formula). Same specifiers as the source sites, so the source's dynamic imports resolve from the module cache, as the comments say and as the three cited precedents (ObjectPicker.closure-28240, DraftChangesPanel.securityLink, DraftChangesPanel.securityProblems) do with the same one-line form.

    • Dependencies declared: ui/packages/app-shell/package.json dependencies carries @objectstack/formula: workspace:^ and @objectstack/lint: workspace:^; packages/metadata-protocol/package.json dependencies carries @objectstack/lint: workspace:*. No new dependency is introduced; the tests import what the source already imports.
    • No import-time side effect: both packages declare "sideEffects": false; packages/lint/src/index.ts (1227 lines) and packages/formula/src/index.ts (203 lines) contain only export … from / type re-exports, no top-level statement. The sources already load the same modules during the tests (lazily), so the assertions see the same module instances; only the moment of the first transform moves from inside a case to collection. Nothing the two tests assert changes.
  4. Reviewed surface unchanged. git diff fa5c3fee 1899fc5e --stat is 139 files; 137 are main's own (the merge) and the remaining two are the test files above. Of the PR's 45 files, the ADR (docs/adr/0055-…), the five scripts/adr-anchors/** entries, the spec and plugin-security sources are identical between fa5c3fee and 1899fc5e (the PR-vs-main diff comparison in item 1 covers every one of them).

Should-fix — none new. Should-fix 3 of 6689219963 (attribution wording in the anchors' parenthetical) and should-fixes 3-5 of 6688757325 are untouched and still stand for the seat's list.

Confirmed

  • Check-runs on 1899fc5e at 16:45Z (pushed ~16:43Z), latest per name, 42 runs, no reruns: 11 success, 8 skipped (opt-in and schedule-gated lanes), 23 in progress, 0 red. Green: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, PR-body guards, Plan Test Core shards, Spec property liveness, filter. In progress: Build Core, Build UI Deps, Live E2E UI, all eight Lint lanes (including Lint · CI, release & engine gates — in_progress, the lane that carries check-test-source-alias; it was red on fa5c3fee with exactly the three findings of item 3), Test Core 1-8, Type Check × 4. Test UI / Lint UI runs not yet created at the time of writing.
  • The Test Core (5/8) failure on fa5c3fee (run 127994034795, the @objectstack/scenarios built-consumer timeout): the PR touches zero scenarios paths; main's 6910797f (build(scenarios): a minimal unpublished build, and a narrow ./score entry that reads no objectql or plugin-sharing #30938, "build(scenarios): a minimal unpublished build…") rewrote packages/qa/scenarios/test/build/built-consumer.test.ts and its build config and rode in on the merge. If the timeout recurs on this head it is main's, not this PR's; if it goes away, the merge is the likely reason.

② Semver level

Unchanged from 6689219963. 1899fc5e touches test files only; the merge brings main's own changesets. No changeset change is owed. @objectstack/spec, @objectstack/plugin-security, @objectstack/lint, @objectstack/objectql stay minor; Metadata-protocol: added; Clause-②: yes (widening).

③ Boundary flags

Implemented-by: claude/project-thread-prdvgm
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: PASS

@os-zhuang
os-zhuang marked this pull request as ready for review November 6, 2026 17:02
@os-zhuang
os-zhuang enabled auto-merge November 6, 2026 17:02
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 6, 2026
Any commits made after this event will not be merged.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42259593632 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/admin-platform-admin-standing.dogfood.test.ts > #9482: what an ObjectStack platform admin gets from every /admin/ route > no /admin/ route refuses the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: the stack serves /admin/ route(s) this file neither admits, refuses-by-design, nor classifies as answering for a non-authorization reason. A new admin route 
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 19 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

This branch has not been deployed

No deployments
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 pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue protocol:data size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only

2 participants