Skip to content

docs(spec): put the forceCbpMasterDetailRequired "only the security gate" sentence in the past tense - #24310

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24291-cbp-docblock-tense
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24291-cbp-docblock-tense

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24291
Clause-②: no

What changes

This PR rewrites one sentence in the forceCbpMasterDetailRequired docblock and adds a prose-only patch changeset. Nothing else changes. The sentence is in the "Why" paragraph of packages/spec/src/data/object.zod.ts, now :3483–:3490.

Before:

Today only the security gate (assertControlledByParentWrite) closes that shape, and commit 75b7c24 records that the declaration and the enforcement disagree. This makes the unsafe shape impossible to NEWLY declare:

After:

Before commit 75b7c24 only the security gate (assertControlledByParentWrite) closed that shape, by refusing the insert at runtime, and that commit records that the declaration and the enforcement disagreed. Since protocol 18 the parse refuses the declaration itself: {@link refuseControlledByParentWithoutOneMaster} refuses a controlled_by_parent object none of whose master_detail fields declares required: true. The builder makes the unsafe shape impossible to NEWLY declare:

  • "This makes" became "The builder makes". The sentence before it is now about the parse, so "This" would point at the wrong mechanism.
  • .changeset/24291-cbp-docblock-tense.md: '@objectstack/spec': patch, prose only, with Clause-②: no.

No key, schema, export, accepted value or message text changes. All 12 changed lines in packages/spec are * comment lines.

What each claim rests on

The past half: before commit 75b7c24, only the security gate closed that shape, by refusing the insert.

The present half: since protocol 18, the parse refuses the declaration itself.

  • refuseControlledByParentWithoutOneMaster (object.zod.ts:2213) runs inside ObjectSchemaBase's .superRefine (:3143, called at :3167).
  • It reports the verdict of controlledByParentMasterRefusal (src/data/controlled-by-parent-master.ts). That function counts a field as a master only when type === 'master_detail' and required === true, and refuses unless exactly one field counts.
  • It is a protocol 18 change: the parse refusal is registered by src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts.
  • I ran a one-off probe against the built dist/data/index.mjs. No test file was added or kept. Each object below declares sharingModel: 'controlled_by_parent':
master_detail fields ObjectSchema.safeParse ObjectSchema.create
one, required omitted refused at ["sharingModel"], "but no required master_detail field" passes (forced)
one, required: false refused at ["sharingModel"] throws (the force's refusal)
one, required: true passes passes
one required, plus one with required omitted passes throws at ["fields"] (two required after the force)

The fourth row is why the new sentence states the exact predicate ("none of whose master_detail fields declares required: true"). It does not say "a master_detail without required: true", which would be wrong for that row.

Which door the sentence claims. It claims only the ObjectSchema parse. That means .parse(), .safeParse() and create(), which runs the force (:3677) and then ObjectSchemaBase.parse(withDefaults) (:3687).

The sentence says nothing about defineStack, os validate, the metadata save door or stored rows. The refinement's own docblock says stored rows still register, under a [metadata_spec_invalid] diagnostic. I did not read those doors for this PR, so the sentence does not name them.

The sentence also drops "only", and it does not say whether the gate still closes the shape. I read what the gate does today: it derives a master from exactly one required master_detail (security-plugin.ts:7923), and on any write to an object without one it throws MasterDetailRelationMissingError (:8585). That is not this sentence's subject, so the sentence makes no claim about it.

The symbol reference. The new text cites the symbol as {@link refuseControlledByParentWithoutOneMaster}. That is the form the same docblock already uses one paragraph below the list. It is not a path#symbol anchor, so check-spec-docblock-symbol-anchors does not judge it. That gate counts 12 symbol (12 declaration, 0 literal) on this head, the same as on PR #24290. The name has exactly one declaration in the file: git grep -c "^function refuseControlledByParentWithoutOneMaster(" gives 1.

Changeset

@objectstack/spec's files array ships src/**/*.zod.ts, so the docblock goes into the tarball as written. It does not reach dist. After the build, 0 dist files carry the docblock's unchanged phrase "arms the worst measured failure shape", and 0 carry the new "closed that shape, by refusing the". The function's name is in dist, which is the positive control. So the published surface is the source file, and the changeset is a patch with Clause-②: no. This follows PR #24290 and PR #24072.

Verification

The final head is 5d9d774b1, the merge of main at 21070a2f0. The two commits that merge brought in touch only packages/lint, docs/qa and one changeset. Build and test ran through scripts/pm/os-verify-lock.sh, and every exit code was captured before any pipe.

  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated: VERDICT command-exit 0, All 14 generated artifacts are up to date. Nothing was regenerated.
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: Test Files 694 passed (694), Tests 21040 passed | 1 todo (21041).
  • pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0, ending check:test-typecheck: OK.
  • The build, the tests and the typecheck ran on the pre-merge head 6abb5d815. The merge brought no packages/spec change.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 5d9d774b1 derived 81 commands from 2 paths against merge base 21070a2f0. 80 exit 0. The --ran reconciliation reads 81 derived, 80 run, 1 NOT-MEASURED, 0 UNRUN.
    • check-spec-docblock-symbol-anchors: 4972 anchors across 2121 spec sources resolve.
    • check:nul-bytes: OK.
    • check-adr-0087-registration: this PR adds no declared-breaking changeset.
    • check-empty-changeset: green.
    • check:doc-formula-expressions and check:lean-entry-closure read built output. Both exit 0, after I built @objectstack/formula, @objectstack/lint and @objectstack/objectql.
  • NOT MEASURED: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET). It reads every workspace package's dist, and 69 package entries had none. I did not run a whole-repo build locally; that is a declared narrowing. CI's Lint & Repo Gates runs this gate.
  • Comment-only check: the git diff -U0 of packages/spec against the base has 12 changed lines, and each one starts with *. Neither changed file contains a control byte.
  • git grep "Today only the security gate" -- packages: 0 hits on this head, 1 on the base ef6564684.
  • No ablation: the diff changes no tested behaviour.
  • Left to CI: test:repo (Test Core), pnpm lint, the type-check lanes, and the families outside the derived set.

Acceptance notes

  • The same docblock, outside this card's file surface. The paragraph after the list (:3498–:3507) says "so a raw parse of a master_detail without required: true is refused rather than forced". The probe's fourth row reads otherwise: next to a required master_detail, a second one without required parses green. (create() refuses that object, because the force makes both required.) Read as "the object's one master_detail", the sentence holds. This is an observation, not a defect. Carrier: none.
  • packages/lint/src/data-model-rules.ts:304, a code comment. It says "the security gate's fallbacks stay, so metadata already at rest keeps loading and is still guarded". Since protocol 18, the gate has no fallback: security-plugin.ts:7923, and the gate's own docblock says both tiers retired. This is the same family, a comment that names a retired mechanism in the present tense. It is not published: @objectstack/lint's files is dist, README.md and CHANGELOG.md. 0 dist files carry the phrase, while the neighbouring code string "readable by nobody" is in dist. Carrier: none.

Deviations

  • main was merged once before this PR opened (5d9d774b1), as AGENTS.md section 10 asks.
  • I read commit 75b7c240a twice through the REST API: the commit itself, and security-plugin.ts at that ref. Nothing was fetched into the shared object store.
  • Labels: none written.

Generated by Claude Code

…ate" sentence in the past tense

The docblock said that today only the security gate closes the null-master-FK
shape. That held before the builder's force landed; since protocol 18 the parse
refuses the declaration itself (refuseControlledByParentWithoutOneMaster).
Comment text and a patch changeset only.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/object.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/object.zod.ts) — pages documenting those are invisible to this run
  • 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 — 143 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 30da04f8e01758805121ff7a734a1b3472fc6192 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24310 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T11:04Z

ACCEPT at head 5d9d774b10695a1a7f2cb0b01d1e31f8846e925a (card #24291, claim 6248580616, report 6249053516). It lands only after a CONTRACT_REVIEW_TIER PASS on this head and every check green: the diff touches packages/spec/src/data/object.zod.ts (the schema face). That review starts now as an isolated subagent. needs:contract-review goes on this PR in the same act.

item reading
Shape Draft, base main. The body's line 1 is Fixes #24291, line 2 is Clause-②: no; no other closing keyword. Assignee = the card's. 2 files, +21/-4.
Comment-only The object.zod.ts hunk changes only * docblock lines in the "Why" paragraph of forceCbpMasterDetailRequired. No key, export, accepted value or message text moves.
The present half, read by the seat on origin/main refuseControlledByParentWithoutOneMaster (object.zod.ts:2213) adds an issue whenever controlledByParentMasterRefusal (data/controlled-by-parent-master.ts) answers. That function answers undefined only when exactly one field passes isControlledByParentMasterField, so zero required master_detail fields is refused. "refuses a controlled_by_parent object none of whose master_detail fields declares required: true" is true, and it claims only that one of its two cases, which the docblock does not overstate.
The past half "Before commit 75b7c24 only the security gate … closed that shape" rests on the dev's reading of security-plugin.ts at 75b7c240a and on that commit's own changeset. The contract review re-reads it.
Changeset '@objectstack/spec': patch, Clause-②: no. Its three sentences match the diff: "no longer says that today only the security gate …"; "now in the past tense"; "names what has refused the declaration at parse since protocol 18 …".
Accepted deviations One main merge before the PR opened (the incoming commits touch no file here). Base moved to ef6564684 before the worktree was cut. Three packages built for two derived gates. check:dual-build-cjs-loads NOT MEASURED locally, which CI's Lint & Repo Gates runs.
Out of scope Both Acceptance notes. (1) The same docblock's later "a raw parse of a master_detail without required: true is refused" holds for an object's one master. The second-master_detail case parses and create() refuses it, so it is imprecise, not false; the contract review may weigh it. (2) packages/lint/src/data-model-rules.ts:304 ("the security gate's fallbacks stay") is a present-tense comment that ships in no tarball (0 hits in lint dist, positive control present).
Landing After the PASS on this head and a fully green head: pr_ready then automerge_enable, through the relay.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5d9d774b10695a1a7f2cb0b01d1e31f8846e925a
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts + packages/spec/src/data/controlled-by-parent-master.ts, packages/plugins/plugin-security/src/security-plugin.ts (at 75b7c24, at its parent 88f97d5, and on the head), packages/spec/src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts, packages/spec/src/kernel/protocol-version.ts, .changeset/cbp-master-detail-required-forced.md (at 75b7c24), packages/lint/src/data-model-rules.ts

Inputs: card #24291 (body and all three comments: triage 6248485488, Claim 6248580616, os-dev-report 6249053516), PR #24310 (body, file list, net diff against merge base 21070a2), the check-runs on the head. Read-only: the head was fetched into a ref of my own (refs/review/pr-24310) and read with git show; the historical tree was read through the REST contents API into my scratch dir. Nothing built, run or re-run.

① Derived judgments

  • D1 Accept-set: unchanged — right. The net diff is 2 files: .changeset/24291-cbp-docblock-tense.md (+13) and packages/spec/src/data/object.zod.ts (+8/-4). Every changed line in the spec file is a * comment line inside the forceCbpMasterDetailRequired docblock. No schema, key, export, default, message text or .describe() moves. git grep -c "Today only the security gate": 1 on the merge base, 0 on the head.
  • D2 Public surface: the docblock IS published text — @objectstack/spec files[] on the head lists src/**/*.zod.ts — and nothing else changes. The docblock is TSDoc on a module-private function, so it is not projected into content/docs (0 hits for either of its anchor phrases on origin/main), the api-surface snapshot records exports only, and check:docs plus the check:generated ledger run in Type Check · source gates, which is green on this head. Right.
  • D3 The symbol reference: {@link refuseControlledByParentWithoutOneMaster} resolves — the function has exactly one declaration in the file (object.zod.ts:2213), and the same docblock already cites it in that form one paragraph below (:3503). Lint & Repo Gates (the symbol-anchor corpus) is green. Right.
  • D4 The past half — "Before commit 75b7c24 only the security gate (assertControlledByParentWrite) closed that shape, by refusing the insert at runtime": right. Commit 75b7c240a3eb has one parent, 88f97d5281, and its file list is the force, its test, the 18.cbp-master-detail-required-forced entry, the registry and a changeset — it does not touch security-plugin.ts, and I fetched that file at both refs: byte-identical (sha256 fd27d513f8f1…). In that file resolveCbpRelation picks a required master_detail, then ANY master_detail, then a required lookup (:4949–:4951), so a non-required master resolved; omissionRefusedByValidation requires !!def?.required (:4969–:4970), so it was false for that master; the insert leg takes the master id from the body (:5164–:5166), and a null id reaches throw new MasterReferenceMissingError (:5251) because the stand-down if (operation === 'insert' && rel.omissionRefusedByValidation) return; (:5250) does not fire. So the gate refused the insert, at runtime, and nothing before it did: the commit's own changeset says "A controlled_by_parent object may declare its master reference without required, so the master-access guard is the only thing preventing an unreachable orphan detail row #8772 measured that only the security gate closed that shape while the declaration surface accepted it" and keeps the lint rule at warning.
  • D5 "and that commit records that the declaration and the enforcement disagreed": right — the sentence quoted in D4 is that record, and the old text's "records" is preserved with only the tense moved.
  • D6 The present half — "Since protocol 18 the parse refuses the declaration itself: {@link refuseControlledByParentWithoutOneMaster} refuses a controlled_by_parent object none of whose master_detail fields declares required: true": right. The refinement (:2213) runs inside ObjectSchemaBase's .superRefine (:3143, call at :3167) and reports controlledByParentMasterRefusal (controlled-by-parent-master.ts), which counts a master only where type === 'master_detail' && required === true and, with zero such fields, returns the path: 'sharingModel' refusal — exactly the predicate the sentence states. create() reaches the same refinement through ObjectSchemaBase.parse(withDefaults) (:3687) after the force (:3677). "Protocol 18" is the repo's own anchor: PROTOCOL_VERSION = '18.0.0', the entry 18.cbp-master-relation-tiers-retired.ts registers the refusal, and the refusal message itself says "since @objectstack/spec 18". The sentence does not say "only", so it claims nothing about what the gate does today, which is right because the gate still refuses (security-plugin.ts:7923 resolves one required master_detail or nothing; :8585 throws MasterDetailRelationMissingError on any write without one).
  • D7 "The builder makes the unsafe shape impossible to NEWLY declare:" — right referent. The two list items that follow (omitted required forced; explicit required: false refused) are forceCbpMasterDetailRequired's behaviour at create(), not the parse's; "This makes" would have pointed at the parse sentence inserted before it.
  • D8 Chronology: the force (75b7c240a) and the parse refusal (finding(security): controlled_by_parent derives its master through a REQUIRED LOOKUP (runtime and lint, third tier) where ADR-0055 §1 requires exactly one required master_detail and fails closed — a widening no record states #23100) are both registered under protocol 18, so "before commit 75b7c24" and "since protocol 18" do not contradict — the interval between them is covered by the builder sentence (D7). Nothing in the new text claims the parse refused at 75b7c240a itself (it did not; that commit's message says raw parse stayed tolerant). Right.
  • D9 The changeset text restates D4–D6 ("That was true before the builder's force landed"; "what has refused the declaration at parse since protocol 18") and ends with "No key, schema, export, accepted value or message text changes" — consistent with the diff. Right.
  • D10 Check-runs on the head, read at 2026-10-18T11:10Z: 43 runs, 0 failure. Required contexts: Lint & Repo Gates success, Build Core success, Governed Surface Queue Guard success; Test Core shards 1/6–6/6 in_progress (the aggregate not yet posted); TypeScript Type Check aggregate not yet posted (Type Check · source gates and · debt ledger success, · workspace and · consumer gates in_progress). Also green: Check Changeset (twice), Check PR Size, the three claim guards, Spec property liveness, every Lint · lane. In_progress is not a pass: the seat arms nothing until Test Core and TypeScript Type Check post success.

② Semver level

'@objectstack/spec': patch, prose-only, Clause-②: no on the PR body and in the changeset body. Matches what the diff publishes: a changed docblock in a shipped src/**/*.zod.ts file and nothing else — no accept-set, export, default or message text moves, so no minor, no narrowing, and skip-changeset would be wrong because the tarball changes. Same shape as the .changeset/24187-docblock-mechanisms.md precedent on this head. check-adr-0087-registration and check-empty-changeset are inside the green Lint & Repo Gates. Clause-②: no — right.

③ Boundary flags

  • F1 (dev, out-of-scope note 1) The same docblock's later sentence (:3503–:3507): "so a raw parse of a master_detail without required: true is refused rather than forced". Judged: IMPRECISE, not false, and not made false by this PR. In controlledByParentMasterRefusal, masters.length === 1 returns undefined whatever else the field map holds, so an object with one required master_detail plus a second master_detail without required: true parses green (the dev's probe row D reads the same from the built dist; I read it from the source). The sentence's literal form over-reaches for that one shape; read with its own "so" clause — the RULE it names is "exactly one required master_detail", stated correctly one clause earlier — it describes the object's one master, where it holds (shape master-not-required, refused at sharingModel). It names the right mechanism, and this PR's new sentence now carries the exact predicate in the Why paragraph, so the docblock states the precise rule twice and the loose shorthand once. Answered: a note, not a card — nothing here is a false mechanism or a trap that leads an author into a wrong write (the over-strict reading only makes an author expect a refusal that, for the two-master_detail shape, comes from the builder instead of the parse). The lane may tighten the shorthand to "an object whose master is a master_detail without required: true" the next time this docblock is touched. The parse-accepts-what-create()-refuses asymmetry for that shape is declared by design in the refinement's own docblock ("one authored with two always has two required ones and is refused"), so it is not a finding either.
  • F2 (dev, out-of-scope note 2) packages/lint/src/data-model-rules.ts:304 "the security gate's fallbacks stay" — confirmed stale on the head (security-plugin.ts:7923 resolves one tier only) and confirmed unpublished (@objectstack/lint files[] is dist, README.md, CHANGELOG.md; the sentence is a source comment). Answered: carrier none is right; the lane carries it as a note until a lint PR touches that header.
  • F3 (dev, Verification) check:dual-build-cjs-loads NOT MEASURED locally (exit 3, no whole-repo dist) — answered by CI: it is a Lint & Repo Gates member, green on this head.
  • F4 (dev, Deviations) main merged once before the PR opened, incoming commits in packages/lint, docs/qa and one changeset — answered: that is the §10 pre-open pull; the net diff against the merge base is still the two files above and nothing of the merge leaks into it.
  • F5 open_questions: none in the os-dev-report; none raised here.
  • F6 Family check: this is the second member of spec: two docblocks name the wrong mechanism, ActionParam.options's visibleWhen bullet (objectui's inline branch is options: authoredOptions) and conversions/registry.ts's master-update refusal (the record validator, not the security gate) #24187's family (a published docblock naming a mechanism in the wrong tense) and the card already names it so; this PR closes it with the same shape of changeset. No third occurrence is opened by this diff.

Implemented-by: claude/issue-24291-cbp-docblock-tense
Reviewed-by: session_019oFY6Hi35J7atvgVFnmoaK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review adopted · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T11:13Z

  • Adopted: contract review 6249191577 (Served-tier: CONTRACT_REVIEW_TIER, Head-sha 5d9d774b10695a1a7f2cb0b01d1e31f8846e925a, the current head, PASS, no FAIL item). Its D6 reading of controlledByParentMasterRefusal matches the seat's in ACCEPT 6249076227. needs:contract-review is removed in this act.
  • Its F1, the later "a raw parse of a master_detail without required: true is refused" sentence (:3503–:3507): judged imprecise, not false, and not made false by this PR. It stays a note, as the ACCEPT had it.
  • Landing: once every check on this head is green: pr_ready then automerge_enable.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 18, 2026 11:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 18, 2026 11:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 2ce2e36 Oct 18, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24291-cbp-docblock-tense branch October 18, 2026 12:04
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/s tooling

Projects

None yet

2 participants