Skip to content

feat(spec)!: a controlled_by_parent object derives its master from exactly one required master_detail; the runtime's fallback tiers retire (#23100) - #23179

Merged
objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-23100-cbp-one-master
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-23100-cbp-one-master

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23100
Clause-②: no (narrowing)

What this does

Ruling 6134079470 (batch #331 item 4, letter B) enforces ADR-0055 §1: a controlled_by_parent object derives its master from exactly one master_detail field declared required: true. Before this PR the parse, the linter and the runtime gave three different answers. They now apply that one rule, and each seam keeps its own read of it.

seam before after
parse — ObjectSchema (packages/spec/src/data/object.zod.ts) accepted a controlled_by_parent object with no master, with a non-required master_detail, with a required lookup, or with two masters refuseControlledByParentWithoutOneMaster in ObjectSchemaBase's superRefine refuses all four shapes. It names the object and ADR-0055 §1. Zero masters are reported at sharingModel, and the message names each field that relied on a retired tier. Two or more are reported at fields. It applies to raw .parse()/.safeParse(), create(), defineStack, os validate and the save door.
lint — packages/lint/src/validate-security-posture.ts CBP_TIERS, a three-tier table "mirroring the runtime's winner, exactly" One predicate, cbpMasterCandidates. The no-relation rule fires on zero candidates. The ambiguity rule fires on two or more, and its verdict is now "no master is derived": it no longer names a winner by field order. The os explain entries are rewritten to match.
runtime — resolveCbpRelation (packages/plugins/plugin-security/src/security-plugin.ts) pick(required master_detail) ?? pick(any master_detail) ?? pick(required lookup), first by field order Exactly one required master_detail that has a reference resolves. Anything else resolves null, which is fail-closed: reads get RLS_DENY_FILTER, writes get 422 INVALID_METADATA. A new reportCbpMasterUnresolved warns once per object per cache generation and names the shape the object relied on. The reference-spelling tolerance in the same reader is a separate, deliberate decision and is unchanged.

ADR-0087 disposition: one lossless shape converted, the rest refused loudly

  • Disposition. It is registered cbp-master-detail-required-defaulted, cbp-master-relation-tiers-retired: a new step-18 D2 conversion and a new step-18 D3 semantic entry. The D2 covers exactly one shape. A controlled_by_parent object whose one master_detail omits required gets required: true, the force ObjectSchema.create() already applies. objectui's Studio writes that shape: its newField emits { type, label }, its Required toggle writes required: v || undefined, and its Settings tab offers controlled_by_parent. The conversion is retired from the authoring load path, so a live author is still refused at parse.
    • Not converted: a required lookup. Turning it into a master_detail changes what deleting the master does to every row, and leaving controlled_by_parent is the author's call, per the ruling's fork clause.
    • Not converted: an explicit required: false (the builder refuses it too) and two masters (choosing one is the pick §1 refuses).
  • Stored rows. applyConversionsToStoredItem replays cbp-master-detail-required-defaulted. A stored row whose one master_detail omits required is therefore served with required: true and resolves the same master it did before. A stored row in any other retired shape fails ObjectSchema and is still registered under its [metadata_spec_invalid] diagnostic, which carries the parse's message; the runtime then denies the object and reports it once. No tier is left silently live anywhere.
  • Sibling entries. The two step-18 entries on the same surface (cbp-master-detail-required-forced, cbp-master-detail-required-lint-error) are amended in place. Their "raw parse stays tolerant" and "the runtime keeps resolving the master" sentences would otherwise contradict this step inside the same unreleased major.

HotCRM: measured, not edited

These are read-only readings of the clone at 0f84c9c. No hotcrm card is filed here, per the ruling.

object relation today refused by this rule
crm_campaign_member crm_campaign: Field.lookup(..., { required: true }), no master_detail yes (tier 3)
crm_event_attendee crm_event: Field.lookup(..., { required: true }), no master_detail yes (tier 3)
crm_article_feedback crm_knowledge_article: Field.lookup(..., { required: true, deleteBehavior: 'cascade' }), no master_detail yes (tier 3)
crm_contract, crm_quote, crm_quote_line_item, crm_opportunity_line_item, crm_contact one Field.masterDetail(..., { required: true }) each no

The changeset's FROM → TO names the three objects. For crm_campaign_member and crm_event_attendee it prescribes deleteBehavior: 'restrict'. Both objects document today's behaviour on purpose: a campaign or event that still has members or attendees cannot be deleted. A plain master_detail would cascade instead. crm_article_feedback already cascades.

Fixture triage (the three dispositions)

fixture disposition
spec object.test.ts: "forces EVERY master_detail reference" replaced. It pinned that nothing enforced the count, and now pins the two-master refusal.
spec object.test.ts: "raw .parse() stays TOLERANT" replaced. It is now the refusal, with the stored body still unrewritten.
lint posture tests: step-2 and step-3 silent controls, tier-2/3 ties, "names the OTHER field as the winner", the #22212 lookup-child warning replaced. Each is reversed into the retired-tier refusal.
lint validate-expressions.test.ts: master-detail count fixture re-spelled. It used controlled_by_parent incidentally and now uses private.
verify parent-write-reach.test.ts: every controlled_by_parent fixture's master_detail declaration added (required: true). These fixtures were never §1-legal: through defineStack they ran on tier 2. pwr_two_masters and pwr_no_master are now excluded by the parse's own refusal.
plugin-security controlled-by-parent-sharing.test.ts: the tier-2 row of the 422 MISSING_REQUIRED_FIELD it.each, and the required-lookup case replaced. Both move into a new retired-tier block (below).
objectql engine-unreadable-master-detail-header.test.ts: uh_cbp_line's master_detail declaration added (required: true, on that object only). objectql registers it raw, so the suite stayed green either way, but the shape is no longer §1-legal.

In-flight coupling

Pins (one refusal and one accepted control per seam)

  • Parse (packages/spec/src/data/object.test.ts): the accepted control covers raw parse, builder and lookups beside the master. The refusals cover the required lookup (parse and builder), the non-required master_detail (omitted and false), no relation, two masters, and other sharing models left alone. defineStack is covered both ways.
  • Lint (packages/lint/src/validate-security-posture.test.ts): each rule's control and its refusals. A new it.each holds the lint's verdict equal to the parse's on five shapes. The accepted shape is asserted to parse whole, so the refusals come from this rule rather than from a refinement that never ran.
  • Runtime (packages/plugins/plugin-security/src/controlled-by-parent-sharing.test.ts, block "the retired master tiers derive NO master"): the accepted control reads rows, reaches the executor and reports nothing. Each retired shape (non-required master_detail, required lookup, two masters) is checked for all of the following:
    • reads denied on both the middleware face and the analytics face;
    • the by-id update and the insert answer 422 INVALID_METADATA;
    • the report fires exactly once;
    • the parse refuses the same schema at the same path.

Verification

Readings are at three trees. ba6686ce6 is the final head: main at 02bec6ca8 merged in. 7ed362fd2 is the same diff before that merge; the only difference is 7 main commits that touch none of this PR's files. b17a2e120 is the first merge of main, before one verify-test commit. Every exit code was captured before any pipe.

Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at ba6686ce6 derives 127 commands, and all 127 were run there.

  • 126 exited 0.
  • 1 exited 2: node scripts/check-engine-split-ratio.mjs --days 90 refused because this clone is shallow (its oldest commit, 2026-09-12, is inside the 90-day window). NOT MEASURED (shallow clone). CI runs it on a full checkout.
  • --ran reconcile line: "✓ dispatch-gates --ran: 127 derived famil(ies) accounted for — 127 run, 0 NOT-MEASURED".

Verdict lines quoted from that run:

  • check-adr-0087-registration: "1 declared-breaking changeset(s) … [BREAKING+bang+clause-②-narrowing] registered cbp-master-relation-tiers-retired (new here: cbp-master-relation-tiers-retired)"
  • check:api-surface: "@objectstack/spec public API surface + factory signatures unchanged ✓"
  • check:liveness: "packages/spec/liveness/state-counts/ is current"
  • check:type-check-debt: "1 ledger entr(ies) re-measured … none above its recorded number"
  • check:error-status-conformance: "every derivable runtime status is documented, and every documented status is reachable"
  • check:nul-bytes: "no raw ASCII control bytes"
  • check:doc-authoring: "no growth"
  • check:adr-anchors: OK
  • check:changeset-no-major and check:empty-changeset: green

Suites.

package tree files / tests
@objectstack/spec b17a2e120 660 / 20027 pass (1 todo)
@objectstack/spec targeted (object.test.ts, migrations, error-catalog docs, step-18 merge) ba6686ce6 7 / 468
@objectstack/lint b17a2e120 138 / 6654
@objectstack/lint targeted (posture, expressions, R2, runtime surface) ba6686ce6 4 / 619
@objectstack/plugin-security b17a2e120 192 / 3984 (45 skipped)
@objectstack/plugin-security CBP files ba6686ce6 9 / 221
@objectstack/verify 7ed362fd2 36 / 289
@objectstack/verify parent-write-reach.test.ts ba6686ce6 1 / 10
@objectstack/objectql b17a2e120 409 / 7998
@objectstack/platform-objects b17a2e120 70 / 1096
@objectstack/example-crm b17a2e120 6 / 46
@objectstack/example-showcase b17a2e120 33 / 409

typecheck exited 0 for spec, lint and plugin-security at b17a2e120, and for verify at ba6686ce6. Each printed its check:test-typecheck: OK line. Every suite ran on a rebuilt dependency closure (turbo run build --filter=…^...).

The sweep direction is downstream: the consumers of @objectstack/spec that hold controlled_by_parent fixtures or seeded objects. turbo ls --affected was not used to narrow anything here.

Census. At b17a2e120, all 115 authored *.object.ts files outside tests and fixtures were imported through the built spec, so ObjectSchema.create() ran the new rule at module load. There were 0 import failures. Of 120 objects, 3 are controlled_by_parent: crm_opportunity_line_item, showcase_expense_line and showcase_invoice_line. Each has one required master_detail and parses. The CLI eval corpus adds 4 more controlled_by_parent objects, and the rule refuses none of them. A tier-3 control object was refused, which proves the census can see a refusal.

Ablation (reverse verification). The fix was committed first. Each mutation went through scripts/ablation-replace.mjs in WRAP mode, which checks the anchor hit and the blob change on disk and restores with git checkout HEAD. Both subjects import from src/ (relative imports), so no dist/ leg applies.

  • Parse. Deleting the refuseControlledByParentWithoutOneMaster(...) call (anchor 1 → 0, call sites on disk 0) failed 6 / 205 in object.test.ts: every refusal pin. The accepted control and the "other sharing models" pin stayed green, as predicted.
  • Runtime. Putting the three-tier ladder back into resolveCbpRelation (marker on disk 1, anchor 0) failed 3 / 36 in controlled-by-parent-sharing.test.ts: the three retired-tier cases. The control and the no-master-detail 422 stayed green, as predicted.
  • Restores. Both were proven: the blob equals HEAD (1f74c94d8fd6, ffac0c727d4f) and git diff HEAD is empty.

ESLint (a declared narrowing). eslint --no-inline-config --format json over the 15 touched .ts files reported 15 files, 0 errors and 0 warnings.

  • --print-config resolves each of the 15 inside the config.
  • The config enables no type-aware linting (parserOptions is { ecmaVersion, sourceType } with no project), so this diff cannot move a verdict on an untouched file.
  • The repo-wide pnpm lint is CI's run.

Acceptance notes (not changed here)

  • packages/verify/src/parent-write-reach.ts masterLinkOf. Its zero-master reason ends "a master the runtime would derive through a required lookup is the runtime's own precedence". No input the parse accepts reaches that branch now, and the runtime no longer has that precedence. Its two-master branch also counts every master_detail, so one required plus one non-required is still excluded, although the parse and the runtime both derive the required one. Report-line text only. Carrier: none.
  • The same family's notDerivable reason. "the master … cannot be synthesized" is unreachable for a §1-legal detail: its required master reference blocks the detail first. The pinned case now names the detail's own block, which is the same master one hop earlier. Carrier: none.
  • Stale freeze note in security-plugin.ts. The note in assertControlledByParentWrite still says "Direction 1 (lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139) has NOT landed". That has been stale since ace0a5385, before this PR. Only its first-shape bullet is marked superseded here.
  • required reading. The runtime now reads required as === true (it was truthy), matching the lint. A raw-registered object with a non-boolean required on its master now fails closed.
  • Object extensions are not judged by this rule. An ObjectExtensionSchema that adds a second required master_detail to a controlled_by_parent base object is not judged at parse. The runtime fails such a merged object closed and reports it. Not measured beyond reading the code.
  • For whoever files the HotCRM card. crm_campaign_member and crm_event_attendee document refusal on delete as the intended behaviour, so deleteBehavior: 'restrict' on the new master_detail keeps it. The ruling's hotcrm text reads "delete semantics become cascade"; that holds only without restrict.

Patch round 1 (review record 6139386784)

  • content/docs/permissions/authorization.mdx's security-linter bullet now states the one-master rule, both rule ids included.
  • The step-18 rationale fragment cbp-master-relation-tiers-retired is added.
  • The D2 conversion cbp-master-detail-required-defaulted is added and pinned (packages/spec/src/conversions/cbp-master-detail-required-defaulted.test.ts).
  • The objectql fixture is re-judged.
  • Verified at 6582c1f0d: pnpm --filter @objectstack/spec test:repo 'Test Files 57 passed (57)', spec test 661 passed + 1 skipped, lint test 137, plugin-security CBP files 9, objectql file 1, check:generated up to date, gates 127 derived / 124 run / 3 NOT-MEASURED (prerequisite), 0 UNRUN.

Generated by Claude Code

claude added 11 commits October 13, 2026 01:44
…equired master_detail

Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H
Co-authored-by: Claude <noreply@anthropic.com>
…, lint and runtime

Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H
Co-authored-by: Claude <noreply@anthropic.com>
…ts master is required

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

github-actions Bot commented Oct 13, 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 27 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/objectql/src/master-detail.ts, 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.

21 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 d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4.

⛔ 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/objectql/src/master-detail.ts, packages/spec/src/migrations/registry.ts.template) — pages documenting those are invisible to this run
  • 3 anchor(s) matched too much of the corpus to be a work list: ObjectSchema (symbol, 41 pages), master_detail (literal, 33 pages), sharingModel (literal, 43 pages)
  • 9 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 — 145 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 d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4 → 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: ba6686ce6344a4e4d50b841dcb7c7550a94ed98a
Local-runs: none

Inputs read: card #23100 (body, triage 6133304643, ruling 6134079470, claim 6136101631, dev report 6139196718), PR #23179 (body, 20-file list, net diff against main at the head), the head's check-runs, origin/main at e540e7ffb for context, and the read-only HotCRM clone at 0f84c9c for the three named producers. Nothing was built, run or re-run.

Gate verdicts on this head, read at 2026-10-13T06:05Z: 39 check-runs — 28 success, 5 skipped (the full-battery jobs that do not run on a PR head: Console Pin Gate, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI, the dogfood shard matrix, Packed-tarball smoke (opt-in)), 0 failed, and 6 still in_progress at this reading, reported as such and not assumed: Test Core shards 1, 2, 3, 4 and 6, and Type Check · workspace. Of the required contexts, Lint & Repo Gates, Build Core and Governed Surface Queue Guard conclude success; Test Core (shard 5 success, five shards pending) and the workspace type check have not concluded. The verdict below is the contract verdict on the diff; the queue still waits for those six to conclude success, and a red among them re-opens this record.

① Derived judgments

Ruling B (6134079470): a controlled_by_parent object derives its master from exactly one required master_detail; the parse refuses the object with none or with two, naming the object and the rule; lint's CBP_TIERS collapses to one tier; the runtime's second tier (a non-required master_detail) and third tier (a required lookup) retire under ADR-0049 with an ADR-0087 disposition and a BREAKING changeset naming HotCRM's three objects. Every accept-set and public-surface change the diff implies, judged against that:

  1. Parse refusal — RIGHT. refuseControlledByParentWithoutOneMaster sits in ObjectSchemaBase's superRefine beside the existing checks, so it reaches raw .parse()/.safeParse(), ObjectSchema.create(), defineStack, os validate and the save door, which is what the ruling's "the parse (ObjectSchema) refuses" means. Zero required masters are reported at sharingModel naming each field that relied on a retired tier; two or more at fields; both messages name the object and ADR-0055 §1. It judges the PARSED field map, where required defaults to false, so a plain-literal master_detail with required omitted is refused while create()'s existing spec builder: force required: true on a master_detail reference under controlled_by_parent (ruled Direction 2 of #8772) #9138 force still passes it — the builder's convenience is not the rule, and the ruling said the parse. Other sharing models are left alone (pinned). fields is a record on ObjectSchema, so the array-form early return in the helper is unreachable there and harmless. The ADR-0055 anchor added for object.zod.ts states this invariant. Right.
  2. Lint's new verdicts — RIGHT. CBP_TIERS is gone; cbpMasterCandidates is the one predicate (master_detail, required === true, with a reference). security-controlled-by-parent-no-relation now fires on the non-required master_detail and the required lookup; security-controlled-by-parent-ambiguous-relation fires on two or more required master_detail fields with the verdict "no master is derived" and names no winner, which is the runtime's new answer, so the module header's "one exception" paragraph is correctly retired. required is read as === true (was truthy), matching the runtime's new read. security-master-detail-ungranted stops treating a lookup-bound controlled_by_parent object as a child: consistent, since the runtime derives nothing from it any more and the no-relation error speaks. The new it.each holds the lint equal to the parse on the five ruled shapes and asserts the accepted shape parses whole. Right.
  3. Runtime resolver — RIGHT, with one choice beyond the ruling's wording, judged right. resolveCbpRelation resolves exactly one required master_detail naming a reference and otherwise null, which both legs already treat as fail-closed (reads RLS_DENY_FILTER, writes MasterDetailRelationMissingError, 422 INVALID_METADATA). The ruling named tiers two and three for the runtime; the diff also refuses TWO required master_detail fields instead of picking the first by field order. That follows ADR-0055 §1's "exactly one … fail closed" and the ruling's own sentence "derives its master from exactly one required master_detail", it removes the [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747 order-dependence the lint already called an error, and it is what makes the three seams agree; recording a first-by-order pick would have kept the inference ruling B refused in option A. Right. The reference-spelling tolerance (reference_to/referenceTo, a separate ruling) is untouched: right. reportCbpMasterUnresolved warns once per object per cache generation (its only call site is the cbpRelCache miss body), is gated on masterGovernsRowWrites(schema) so a non-controlled_by_parent caller never reports, and names the retired shape the object relied on: right, a denial that is an empty result must not be silent. Reading required as === true means a raw-registered object with a non-boolean truthy required on its master now fails closed; the spec's type is boolean and the lint reads the same, so accepted.
  4. Error-message text — RIGHT. MasterDetailRelationMissingError now says "not exactly one required master_detail field" with the remedy "Declare one master_detail field with required: true", and the explain-engine condition string follows. The old sentence ("no master_detail relation") was false for the tier-two and two-master cases this PR routes to the same 422. No other consumer of the old text exists on main outside the three changed files. Same code, same status, no new ERROR_CODE_LEDGER row, so the claim's Clause-②: no stands. Right.
  5. ADR-0087 disposition — RIGHT, one residual named. registered cbp-master-relation-tiers-retired: a new step-18 D3 semantic entry (surface, replacement, reason, acceptanceCriteria, filename derived from the id, registry row generated, no index to edit) and no D2 conversion. For the required-lookup tier that is correct: a lookup turned master_detail changes what deleting the master does to every row, and whether the object should instead leave controlled_by_parent is the author's decision, so no lossless mapping exists and ADR-0087 D2 excludes it. For the non-required-master_detail tier the dev's "neither shape maps losslessly" is weaker: an OMITTED required on the one master_detail of a controlled_by_parent object maps to required: true exactly as the builder's own spec builder: force required: true on a master_detail reference under controlled_by_parent (ruled Direction 2 of #8772) #9138 force already maps it, and ADR-0087 D2 would ordinarily ship that as a stored-row conversion so a row at rest keeps serving. The chosen refusal is the conservative direction and is within the ruling (which orders the raw-parse refusal and names no conversion), the row still registers under [metadata_spec_invalid] and is reported once rather than dropped (ADR-0087's stored-metadata addendum, [P2] The spec contract stops at authored source — stored metadata is rehydrated unparsed, unconverted, and ungated #3903, "reads diagnose, never drop", is honoured), and major 18 is unreleased. Accepted; the residual is in ③ with a carrier. Stored tier-two and tier-three rows: diagnosed at registration, denied by the runtime, prescribed by the entry. Right.
  6. The two amended sibling step-18 entries — RIGHT. cbp-master-detail-required-forced and cbp-master-detail-required-lint-error are prescriptions os migrate meta prints to a consumer, not decision records; their "raw parse stays tolerant" and "the runtime keeps resolving the master for metadata at rest" sentences became false the moment this PR lands, and a step that contradicts itself is a worse record than one amended in place. Ids and surfaces unchanged, the amendments point at the new entry by id. Right.
  7. ADR-0055 itself — untouched, RIGHT. The PR implements §1 as written; no amendment is owed, no docs/adr/** path is in the diff (not Tier H), and both load-bearing spots gained an ADR-0055 anchor (check:adr-anchors green).
  8. Docs — two pages right, one page missed. error-catalog.mdx and error-handling.mdx drop the non-required master_detail and required lookup rows from the 422 MISSING_REQUIRED_FIELD matrix and say why: right. MISSED: content/docs/permissions/authorization.mdx (the security publish linter bullet) still describes the no-relation rule's trigger as "no required master_detail, no master_detail at all, and no required lookup", which is the three-tier emptiness; after this PR a required lookup alone IS reported, so the sentence now understates the rule in exactly the direction the ruling closed. One sentence, not in the diff. Carrier in ③.
  9. Fixture triage — right, one fixture missed. Every controlled_by_parent fixture the dev's table names is re-judged correctly; the in-tree authored objects (crm_opportunity_line_item, showcase_expense_line, showcase_invoice_line) and the dogfood fixtures declare required: true and stay legal; the plugin-security suites that register schemas raw all declare required: true on their master. Missed: packages/objectql/src/engine-unreadable-master-detail-header.test.ts builds uh_cbp_line as controlled_by_parent over invoiceField, a master_detail with no required. objectql boots no security plugin and its registry's spec check is diagnostic-only, so the suite stays green, but the fixture is no longer a §1-legal shape. One-line respell; carrier in ③.
  10. Public surface. No export, signature, JSON-schema key or error code is added or removed (check:api-surface unchanged, check:generated green). The one new runtime behaviour an operator sees is the warn line; the one new author-facing text is the parse issue. Nothing is advertised that the runtime does not deliver.

② Semver level

  • Changeset .changeset/23100-cbp-one-required-master.md: @objectstack/spec, @objectstack/lint, @objectstack/plugin-security at minor, title with the breaking bang, body headed BREAKING, exactly one ADR-0087 marker (registered cbp-master-relation-tiers-retired), and the Clause-②: no (narrowing) line. That is the launch-window convention (a breaking change ships as minor while the fixed group is in lockstep; check:changeset-no-major and check-adr-0087-registration green) and it matches what the diff publishes: a narrowed ObjectSchema accept-set, new lint error verdicts, a narrowed runtime resolver. Matches.
  • Clause-②: no (narrowing) is right: no new key on any published payload; a narrowing is BREAKING, and the changeset says so. The PR body carries the same line.
  • FROM → TO names HotCRM's three objects, and the FROM column is verified on the clone at 0f84c9c: crm_campaign_member.crm_campaign and crm_event_attendee.crm_event are Field.lookup(…, { required: true }) with no deleteBehavior (so the engine's required-lookup escalation refuses the parent delete today, which both objects' own comments say is intended); crm_article_feedback.crm_knowledge_article is Field.lookup(…, { required: true, deleteBehavior: 'cascade' }). The other five controlled_by_parent objects each declare one required Field.masterDetail. The TO prescribes deleteBehavior: 'restrict' for the first two to keep that behaviour; that diverges from the ruling's hotcrm sentence "delete semantics become cascade", and the divergence is routed to the hotcrm card's filer (③). The migration is one line and shippable in CHANGELOG. Right.
  • No changeset owed for @objectstack/verify (test file only) or @objectstack/objectql (header comment only); Check Changeset / check:empty-changeset green.

③ Boundary flags

Every dev deviation and every out-of-scope finding in 6139196718, answered or escalated. open_questions was empty.

Deviations:

  1. File-surface breaches in plugin-security outside resolveCbpRelation (reportCbpMasterUnresolved, the errors.ts message, the explain-engine condition, the computeControlledByParentFilter docblock, one SUPERSEDED bullet in the write-gate freeze note) — answered, accepted: each is text or a reporter the rule change made false or necessary; none is in the regions PR fix(plugin-security): a controlled_by_parent middle master is judged at the chain hop by its own direct-write composition (#23093) #23114 or feat(plugin-security)!: no seeder or materializer writes a sys_permission_set row; the boot seeds no row for a declared set (#15204 stage 6b-2) #22900 edited, and both landed and are merged into this head.
  2. Further breaches (rule-explanations.ts, the objectql master-detail.ts header, two docs pages, the two sibling step-18 entries, two ADR-0055 anchors, the verify test) — answered, accepted for the same reason; the docs sweep is short by one page (① 8) and the fixture sweep by one file (① 9).
  3. The runtime refuses two required master_detail fields instead of picking the first — answered, accepted as ① 3: ADR-0055 §1 and the ruling's "exactly one" cover it; it is a narrowing the lint already graded an error, not a widening.
  4. PR opened after the readings were in and 5. lock-queue time — process notes, no contract effect.

Out-of-scope findings:

  • verify masterLinkOf zero-master tail and the two-master branch counting non-required masters; notDerivable unreachable for a §1-legal detail — answered, accepted as acceptance notes: report-line text in a branch no parse-legal object reaches; not a defect, a contract violation or an authoring trap, so a note is the right carrier.
  • Stale freeze note in assertControlledByParentWrite — accepted: pre-existing, only its first-shape bullet is touched here.
  • HotCRM restrict vs the ruling's "cascade" — confirmed on the clone and escalated with the carrier the dev named: whoever files the ruling's hotcrm card (the hotcrm seat at hotcrm#1353, or triage) carries the choice; the fork clause keeps letter B either way.
  • ObjectExtensionSchema is not judged by the one-master rule — escalated: an extension that adds a second required master_detail to a controlled_by_parent base is refused at no parse and goes dark at runtime (fail-closed, reported once). The ruling named ObjectSchema, so this PR is not short; but "code read only, not measured" on a shape an author can write is a metadata-authoring trap candidate, which Prime Directive chore: version packages #10 files rather than notes. Carrier: the dispatching seat files it as a finding (measure whether the merged object is re-parsed with a diagnostic and whether the warn fires; decide whether the extension seam owes the same refinement).

Items this review adds (none blocks the verdict; each has a carrier):

  • content/docs/permissions/authorization.mdx — the three-tier sentence in the security-linter bullet (① 8). Carrier: the dev, as a docs-only follow-up on this card, or on this PR if it re-spins for any other reason.
  • packages/objectql/src/engine-unreadable-master-detail-header.test.ts — invoiceField gains required: true so uh_cbp_line is a §1-legal fixture (① 9). Same carrier.
  • STEP18_RATIONALE fragment — the entries README says a step-18 retirement adds its fragment to registry.ts.template; none was added. 116 fragments stand against 357 step-18 entries and the two sibling cbp entries carry none either, and no gate pins parity, so this is a convention gap shared with its siblings, not a contract breach. Same carrier.
  • D2 for the omitted-required half (① 5) — a stored controlled_by_parent row whose one master_detail omits required goes dark on the 17 → 18 upgrade with a diagnostic and one warn where a lossless conversion (the builder's own force) would keep it serving. Carrier: the owning seat decides before 18 ships whether to add that one conversion or record in the entry why not; the ruling itself does not require it.

Implemented-by: claude/issue-23100-cbp-one-master
Reviewed-by: session_01BRQ9Mp1yH7R1az6MaUM18H

VERDICT: PASS

…--from marker the house-sentence pin reserves

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6582c1f0d8e30afbbb1386d731d524966075a46e
Local-runs: none

Review after patch round 1. Inputs read: card #23100 (body; triage 6133304643, ruling 6134079470, claim 6136101631, dev reports 6139196718 and 6141838120, ACCEPT 6139237069), PR #23179 (body, 25-file list, both comments including the round-0 record 6139386784, and the net diff against main at this head's merge base 20949e7a7), the head's check-runs, origin/main at 150da61aa for context, objectui origin/main at cea429b98 and the pinned 1fb1ced36 for the Studio writer, and the read-only HotCRM clone at 0f84c9c. Nothing was built, run or re-run.

Gate verdicts on this head, read at 2026-10-13T08:56Z: 48 check-runs — 41 success, 7 skipped, 0 failed, 0 in progress. The skipped seven are the full-battery jobs a PR head does not run (Console Pin Gate, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI, the dogfood shard matrix, Packed-tarball smoke (opt-in)) plus Check PR Size and Auto Label on a second workflow run whose trigger does not fire them. All five required contexts conclude success: Lint & Repo Gates, TypeScript Type Check, Test Core (and each of its six shards), Build Core, Governed Surface Queue Guard. Check Changeset concluded success on both of its runs; the second (started 08:49Z) was still in_progress at this review's first reading and had concluded by the time this record was written.

Round scope, measured. The PR-side edits since ba6686ce6 are exactly the eight files the dev lists. Three more files differ between the two heads (packages/lint/src/rule-explanations.ts, packages/lint/src/validate-expressions.test.ts, packages/plugins/plugin-security/src/security-plugin.ts), but each one's PR-side net diff — against 02bec6ca8 for the old head and 20949e7a7 for this one — is byte-identical: main moved underneath them in merge e09bd7673, nothing on the PR side did. Round 0's judgments (record 6139386784 ① 1–10) were re-read on this head and stand; this record repeats them only where round 1 touched them.

① Derived judgments

The earlier record's four carried items, each answered.

  1. content/docs/permissions/authorization.mdx — DONE, right. The security-linter bullet now reads "without exactly one required master_detail to derive access from (ADR-0055 §1: security-controlled-by-parent-no-relation when it declares none — a master_detail without required: true and a required lookup derive nothing — and security-controlled-by-parent-ambiguous-relation when it declares two or more; either way the runtime denies every read and refuses every write)". Every clause is what cbpMasterCandidates and resolveCbpRelation do on this head; the three-tier sentence is gone.
  2. packages/objectql/src/engine-unreadable-master-detail-header.test.ts — DONE, right. CBP_LINE_OBJECT now spreads REQ_LINE_OBJECT.fields and overrides invoice with { ...invoiceField, required: true }, on that object alone; the plain, requiredWhen and readonlyWhen fixtures keep the shared invoiceField unchanged. uh_cbp_line is a §1-legal shape, and the docblock says why.
  3. STEP18_RATIONALE fragment — DONE, right. Id cbp-master-relation-tiers-retired, order: 95 (the template's max on main is 94), inserted in id order ahead of chart-config-aria-retired — the shape step18-rationale-merge.test.ts pins (sorted by id, explicit order). The text states the one-master rule, the two retired tiers, the fail-closed runtime answer, the one converted shape and its producer, the retirement from the load path, and the three shapes not converted. Accurate to the code. (The two sibling cbp entries still carry no fragment; that is their pre-existing gap, not this PR's.)
  4. D2 for the omitted-required half — DONE. Judged in full in 5–9.

The new D2 conversion cbp-master-detail-required-defaulted, judged as a contract change.

  1. Lossless — RIGHT. The transform writes required: true onto the one master_detail of a controlled_by_parent object when the key is absent. That is byte-for-byte what ObjectSchema.create() has emitted for the same source since spec builder: force required: true on a master_detail reference under controlled_by_parent (ruled Direction 2 of #8772) #9138: forceCbpMasterDetailRequired on this head forces an omitted required to true, throws on an explicit false, and leaves true alone — so a stored Studio row and a builder-authored object with the same declaration now serve the same object. The master it resolves under 18 (the one required master_detail) is the master tier 2 resolved under 17 (any master_detail; with one there was no pick). Under 18 the un-converted shape does not parse at all (refuseControlledByParentWithoutOneMaster refuses it at sharingModel), so the replay is a rescue of a row that would otherwise register under [metadata_spec_invalid] and be denied every read — not a reinterpretation of a shape that still parses, which is the artifact door's DEFAULT_FLIPS_NOT_REPLAYED_HERE criterion; no row is owed there. No DDL path reads field.required (grep over the drivers' and objectql's schema paths on main: none). The one write whose answer changes is the subject of ③ item A: a by-id PATCH that explicitly nulls the master FK — accepted on a non-required master because the write gate reads the STORED row's FK on update and never the incoming value, refused 400 VALIDATION_FAILED by the record-validator's ADR-0113 check once required is true. Before, that write minted a detail row no controlled_by_parent read filter can ever match (the gate's own freeze note names it as the defect); the conversion closes that route for data at rest exactly as the builder's force closes it for authored objects. That is the contract's intent, not a capability lost, so lossless stands.
  2. Scope — exactly the one shape, RIGHT. Read line by line: obj.sharingModel !== 'controlled_by_parent' returns; fields must be a dict; masters are the fields with type === 'master_detail' irrespective of required, and masters.length !== 1 returns — so zero masters and two or more are untouched; 'required' in def returns — so an explicit false (and an explicit true) is untouched; a lookup is never a candidate, so the required-lookup tier is untouched, while a required lookup BESIDE the one master_detail does not block the conversion — right, because the parse accepts a required master beside lookups and tier 2 outranked tier 3, so the same master resolves. Other sharing models untouched. Copy-on-write; idempotent (a converted field carries the key). One notice at objects[N].fields.NAME.required, (omitted) to true. The fixture pair carries all five shapes with expectedNotices: 1, run by conversions.test.ts; the dedicated pin covers the stored Studio row (refused before replay, converts, parses whole, not mutated, second pass a no-op), the three untouched shapes each still refused by the parse at sharingModel, a private object untouched, and defineStack still throwing the prescription.
  3. Retired from the authoring load path — RIGHT. retiredFromLoadPath: true with retiredAfter: '17.7.0'. applyConversions skips a retired entry unless includeRetired is set; the authoring funnel (normalizeStackInput: defineStack, validate, lint, compile, info, doctor) never sets it, the stored-row seam pins it to true, the artifact door opens it for a floor at or below 17.7.0, and os migrate meta replays it by id through step 18's derived conversionIds. The save door (saveMetaItem) replays nothing by design, so a live Studio author who saves the shape is refused with the parse's prescription — pinned through defineStack in the new test. The stamp is right by the type's own rule: this head's @objectstack/spec label is the stable 17.7.0 (the newest published spec tag), so the stamp is the label; five other step-18 retirements carry the same value and the retired-after census pin is green on the head.
  4. Registration — RIGHT. MAJOR_18_CONVERSIONS gains { conversion: cbpMasterDetailRequiredDefaulted, order: 63 } in identifier order between apiEndpointCacheTtlToCacheTtlSeconds and chartConfigAriaRemoved, main's max order being 62 — the shape conversions-major18-merge.test.ts pins (sorted by identifier, explicit order; duplicate orders 35/55/57/61 already exist on main, so uniqueness is not a pin). Step 18's conversionIds is derived from CONVERSIONS_BY_MAJOR[18], so os migrate meta and the generated registry pick it up with no hand edit; ALL_CONVERSIONS membership and toMajor: 18 are pinned. check:spec-changes and check:upgrade-guide project it in memory inside the green Lint & Repo Gates.
  5. Changeset, D3 entry and docblocks — RIGHT, one sentence excepted. The adr-0087 marker names both ids, registered cbp-master-detail-required-defaulted, cbp-master-relation-tiers-retired, which is the gate's documented registered id[, id...] form; the dev quotes the gate naming both as new, and the gate is green on this head. The changeset's spec paragraph states the conversion, its one shape, its producer, the three seams it runs on and that a live parse still refuses; its "Unchanged" list separates the converted shape from the three refused ones. The D3 entry's reason, replacement and acceptanceCriteria name the D2 and the not-converted shapes; its os migrate meta sentence is in house form after 6582c1f0d (the Test Core shard that went red on dbd333828 is green on the head). The object.zod.ts docblock paragraph on metadata at rest is accurate. The one wrong sentence is in the conversion's own docblock in conversions/registry.ts — ③ item A.

The whole net diff, re-read on this head.

  1. Round 0's seams stand. The parse refinement counts type === 'master_detail' && required === true on the parsed map, reporting zero at sharingModel (naming each retired-tier field with its fix) and two or more at fields; the lint's cbpMasterCandidates reads master_detail, required === true, with a reference, and its resolveCbpRelation answers one candidate or nothing; the runtime's resolveCbpRelation filters the same predicate, resolves only a single candidate, and reportCbpMasterUnresolved fires on the cache-miss path gated on masterGovernsRowWrites, naming the retired shape. Three seams, one predicate. The two amended sibling step-18 entries, the error text, the explain-engine condition, the two docs pages and the fixture triage are as judged in 6139386784.
  2. Public surface. No export, signature, JSON-schema key or error code is added or removed; check:api-surface and check:generated are inside the green gates. The one new thing a consumer sees from round 1 is on data at rest: a stored controlled_by_parent row whose one master_detail omitted required is now served (GET /api/v1/meta/object/:name, the registry, the Studio) with required: true — the changeset says so.
  3. objectui at the pin — no sibling fix or pin bump owed. The Studio writer the dev measured at f0532d8a reads the same at the pinned 1fb1ced36 and at objectui origin/main cea429b98: newField emits { type, label }, the inspector's Required toggle writes required: v || undefined, ObjectSettingsPanel offers controlled_by_parent on an existing object, and none of those files changed between the pin and main. Console Pin Gate builds the console SPA at the pin and verifies the spec injection; it runs no objectui test, and this diff removes or renames nothing an objectui build reads (Post-Task Checklist 4 is satisfied by absence).
  4. HotCRM, re-confirmed on the clone at 0f84c9c. crm_campaign_member.crm_campaign and crm_event_attendee.crm_event are Field.lookup(…, { required: true }) with no deleteBehavior; crm_article_feedback.crm_knowledge_article is Field.lookup(…, { required: true, deleteBehavior: 'cascade' }); all three objects are controlled_by_parent. The changeset's FROM column is exact; the D2 converts none of them, as it must.

② Semver level

  • Changeset .changeset/23100-cbp-one-required-master.md: @objectstack/spec, @objectstack/lint, @objectstack/plugin-security at minor, title with the breaking bang, body headed BREAKING, exactly one adr-0087 marker now naming two ids, and the Clause-②: no (narrowing) line. Unchanged from round 0 and still what the diff publishes: a narrowed ObjectSchema accept-set, new lint verdicts, a narrowed runtime resolver, plus — new this round — a step-18 conversion on data at rest. check:changeset-no-major, check-adr-0087-registration and Check Changeset are green on the head. Matches.
  • Clause-②: no (narrowing) is right after round 1 too: the D2 writes an existing authorable key (required) onto an existing field; no key is added to any published payload, and no error code is minted. The served value of a stored row changes from omitted to true, which is the narrowing's data-at-rest half, and the changeset states it. The PR body carries the same line.
  • The FROM → TO table and the one-line remedy are unchanged and verified (① 13). No changeset is owed for @objectstack/verify or @objectstack/objectql (tests and a comment only), nor for the docs page.

③ Boundary flags

Every deviation and every out-of-scope finding in the dev's newest report 6141838120, answered or escalated. open_questions is empty.

Deviations:

  1. The D2 converts the OMITTED key only; an explicit required: false is not converted — answered, right as built. The losslessness argument IS the builder's force, and the force refuses an explicit false loudly rather than overruling it (ADR-0032); converting it would have the stored pass accept an authored contradiction the builder stops, and the measured writer never writes false. The coordinator's "required not true" wording was wider than the force; the dev's narrower reading is the correct one, and no predicate change is wanted.
  2. dbd333828 pushed red on Test Core 1/6 (the house-sentence pin on the D3 entry's os migrate meta --from 17 literal) — answered, fixed on the head: Test Core and all six shards success at 6582c1f0d. The lesson that spec's local test script runs --project local only is a toolchain note, not a contract item.
  3. Worktree recreated; three gates read PREREQUISITE NOT MET locally — answered: Lint & Repo Gates on the head runs every check:* gate and is green.
  4. Lock-queue time — process note, no contract effect.

Out-of-scope findings:

  • objectExtensions adding a second required master_detail to a controlled_by_parent base passes defineStack and the posture rules clean; the runtime then fails it closed with one warn — measured now, escalated with the carrier the earlier record named. The ruling named ObjectSchema, so this PR is not short; but an author or AI can ship a merged two-master object through the extension door and learn it only from denied reads. Carrier: the dispatching seat files it as a finding (the dev's probe evidence and dedupe words are in 6141838120), to decide whether the stack-level parse and the posture rules merge extensions before judging.
  • objectui's Studio is the measured producer of the omitted-required shape and will now meet a 422 at save when controlled_by_parent is chosen with the Required toggle off — the dev marks the carrier none; escalated with one: the refusal is loud and names the fix, so it is not a silent trap, but the one writer this D2 exists for keeps writing the shape until the designer forces or prompts Required when controlled_by_parent is selected (or when a master_detail is added to such an object). Carrier: the dispatching seat files it on objectui, together with item B below.

Items this review adds (none blocks the verdict; each has a carrier):

  • A. One docblock sentence in packages/spec/src/conversions/registry.ts — "the write the row could not legally take (an insert without its master, an update nulling it) was refused before as well, by the security gate". The insert half is true. The update half is not: assertControlledByParentWrite reads opCtx.data only on the insert leg and reads the stored row's FK on a by-id update, so an update that nulls a non-required master's FK was not refused by the gate; it is refused only by the record-validator's ADR-0113 check, which required: true arms. The conversion therefore closes a route that minted an unreadable orphan row — the right outcome, and the one the builder's force already has — but the sentence attributes the earlier refusal to the wrong seam. Comment text only; no contract text (changeset, D3 entry, fragment) makes the claim. Carrier: the dev, on this PR if it re-spins for any other reason, else a docs-only follow-up on this card.
  • B. objectui's deriveMasterObject (packages/app-shell/src/views/studio-design/owd-sharing.ts) returns the reference of the FIRST master_detail field by order, required unread — display-only, in PackageOwdOverviewPanel. After this PR it shows a master for a non-required-master_detail object the runtime derives nothing from, and picks by order where the runtime refuses two. Not this PR's surface, and no pin bump is owed for it. Carrier: the same objectui card as the Studio item above.

Implemented-by: claude/issue-23100-cbp-one-master
Reviewed-by: session_01BRQ9Mp1yH7R1az6MaUM18H

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 13, 2026 09:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 13, 2026 09:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 76815b9 Oct 13, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23100-cbp-one-master branch October 13, 2026 09:34
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/xl tests tooling

Projects

None yet

2 participants