Skip to content

docs(spec): name the mechanism each of two docblocks describes - #24290

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24187-docblock-mechanisms
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24187-docblock-mechanisms

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24187
Clause-②: no

What changes

Two docblocks in packages/spec named the wrong mechanism. This PR rewrites both and adds a prose-only patch changeset. Nothing else changes.

  • packages/spec/src/ui/action.zod.ts, the visibleWhen bullet of the ActionParam options docblock. It quoted objectui's inline param branch as options: param.options. At the console pin, that branch reads options: authoredOptions. The bullet now quotes that, names the pin, and says what authoredOptions is. Its point is unchanged: an inline param's options reach the widget as authored.
  • packages/spec/src/conversions/registry.ts, the docblock of the cbp-master-detail-required-defaulted conversion. It said that two writes the row could not legally take ("an insert without its master, an update nulling it") were "refused before as well, by the security gate". The insert half is true and stays. The update half was wrong in two ways. The security gate never reads the incoming FK on an update. So before the conversion, that update was not refused at all. The sentence now names both seams by symbol anchor. It says the update was accepted before, and that its refusal now comes from the record validator's ADR-0113 check, which only required: true arms.
  • .changeset/24187-docblock-mechanisms.md: '@objectstack/spec': patch, prose only, with Clause-②: no.

No key, schema, export, accepted value, guidance string or message text changes. All 27 changed lines in packages/spec are * comment lines (see the mechanical check under Verification).

Item 1: what the pin reads

.objectui-sha = 5e15928b4631a9dfd0d9bdd7282d1b746cdbaa68. I read every file below with git show PIN:PATH from the local objectui object store. The objectui tree was not checked out or edited.

packages/fields/src/resolveActionParams.ts at the pin:

  • :538 declares export function resolveActionParam(, singular, so the docblock's name is right. resolveActionParams (plural, :873) is the list wrapper.
  • :565 is const authoredOptions = resolveOptionLabels(param.options, ctx.locale);.
  • :574 (the inline branch, no param.field) and :634 (field-backed, but the field is not in the metadata) both read options: authoredOptions,.
  • resolveOptionLabels (:246) returns the authored array itself when everyLabelResolved(options) holds, that is, when every label is already a string. This is an identity fast path. Its comment says objectui fbc23e094's toBe pin depends on it. Otherwise it maps the list. An entry with a string label comes back as it is. An entry whose label is an inline per-locale map is re-spread as { ...option, label: resolveInlineI18nLabel(option.label, locale) ?? option.value }.

So visibleWhen, and every other key, survives on both paths. Only a label written as a map is resolved. That is the bullet's point, so the bullet keeps the word VERBATIM, and its parenthesis now says exactly what happens to label. The map path can be reached from spec-valid metadata, because the spec's option label is I18nLabelSchema (the option strictObject in action.zod.ts).

I re-read the bullet's other links at the same pin:

  • packages/app-shell/src/views/ActionParamDialog.tsx:536 re-spreads each option to localise label.
  • packages/app-shell/src/utils/paramToField.ts:102 passes options: param.options on.
  • SelectField, MultiSelectField, RadioField and CheckboxesField each reference useCascadingOptions.
  • resolveCascadingOptions is exported from packages/core/src/evaluator/optionRules.ts:217.

resolveActionParams.ts, ActionParamDialog.tsx and paramToField.ts are byte-identical between this pin and the previous pin f8267df93aba, which the docblock's next paragraph names. git diff --stat between the two pins over those three paths prints nothing. So that paragraph's :650-:651 and :515 readings still hold.

Item 2: the two seams, read on this branch's base 93b4acb57

  • The security gate: packages/plugins/plugin-security/src/security-plugin.ts#assertControlledByParentWrite.
    • On operation === 'insert' it takes the master id from the incoming body, opCtx.data[rel.fk].
    • On any other single-id write it reads the stored row as system (readRowById(object, targetId, { isSystem: true })) and takes row[rel.fk]. It never reads the incoming body's FK there.
    • A null master id throws MasterReferenceMissingError (422). The one exception is an insert where rel.omissionRefusedByValidation holds (master_detail + required, and neither readonly nor system). There the gate stands down and leaves the refusal to the validator.
  • The record validator: packages/objectql/src/validation/record-validator.ts#validateRecordInScope, which the exported validateRecord calls.
    • Its update branch walks only the keys the caller supplied.
    • Its check commented "ADR-0113 non-regression: a PATCH may not null OUT a required field" is def.required && isEmptyForRequired(def, value) && def.type !== 'autonumber'. It pushes a required error with message key required_cleared.
    • On a field without required, the check does not fire.

"Before" in the docblock means the runtime before protocol 18. Then, a row with required omitted derived its master through the second tier. I re-read both seams at 76815b901^, the parent of the #23179 merge that retired that tier. All four readings are the same there:

  • masterId = data … [rel.fk] on the insert leg;
  • masterId = row[rel.fk] otherwise;
  • the stand-down predicate def?.type === 'master_detail' && !!def?.required && !def?.readonly && !def?.system;
  • the same def.required && isEmptyForRequired(…) update check.

So before the conversion:

  • An insert without the master: the stand-down predicate is false for a non-required master, so the gate threw. The security gate refused it, as the docblock says.
  • An update nulling the master: the gate read the stored FK, which was not null, and passed. The validator's check was not armed. The write was accepted.

This matches contract review 6142098559 on PR #23179 (items 5 and A). I read the card's summary, "refused by the record validator's ADR-0113 check, which required: true arms", in the same sense: the refusal exists only once the conversion writes required: true. So the new sentence does not say the update "was refused before". It says:

  • the update was accepted before;
  • it left a detail row with a null FK, which the read filter masterFK IN (accessible master ids) never matches;
  • it is now refused by the validator, because required: true arms that check.

That is the same refusal the builder's force gives an authored object. So the paragraph's "Lossless" claim still holds, as the contract review judged. The filter is spelled the way src/migrations/registry.ts and the lint rule already spell it.

Changeset

@objectstack/spec's files array ships src/**/*.zod.ts, so src/ui/action.zod.ts goes into the tarball as written. After the build, the new bullet is also in the dist bundles: 28 dist files carry options: authoredOptions, and 0 carry options: param.options. src/conversions/registry.ts is not in files, and its new text reaches no dist file: 0 files carry The update nulling it was not. So the changeset describes item 1 only. It is a patch with Clause-②: no, following PR #24072.

Verification

All readings below are on the final head c239951c2 (the merge of main at 1e001c7df) unless marked otherwise. 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: lock verdict command-exit 0. The output was All 14 generated artifacts are up to date. The comment-only edit regenerates nothing, and no artefact was touched.

  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: Test Files 694 passed (694), Tests 21027 passed | 1 todo (21028). On the pre-merge head fe8a88886 it was 693 files and 21002 tests, all passing.

  • pnpm --filter @objectstack/spec typecheck: exit 0, ending check:test-typecheck: OK.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 83 commands from the 3 changed paths, against merge base 1e001c7df. 82 exit 0. --ran reconciliation: 83 derived, 82 run, 1 NOT-MEASURED, 0 UNRUN.

    • check:spec-docblock-symbol-anchors: 4971 anchors across 2121 spec sources resolve — 12 symbol (12 declaration, 0 literal).
    • check:nul-bytes: OK.
    • check-adr-0087-registration: this PR adds no declared-breaking changeset.
    • check-empty-changeset and check-changeset-no-major are green.
  • NOT MEASURED: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET). It reads the built output of every workspace package, and 92 had no dist. I did not run a whole-repo build locally; that is a declared narrowing. CI's Lint & Repo Gates runs this gate.

  • On the pre-merge head, three more commands exited 3 at first. Each was re-run after its prerequisite was met:

    • check-plugin-teardown-shape.mjs --self-test needed a pinned commit missing from the shallow clone (see Deviations);
    • check:doc-formula-expressions and check:lean-entry-closure needed @objectstack/formula, @objectstack/lint and @objectstack/objectql built.

    On the final head, all three exit 0 in the first pass, after those three packages were rebuilt on the merged tree.

  • The symbol-anchor gate judges both new anchors (a one-off reverse check on fe8a88886; the merge did not touch registry.ts).

    • I misspelled each anchor's symbol in turn, through scripts/ablation-replace.mjs. Each time the gate went red with exit 1: [unresolved-symbol] packages/spec/src/conversions/registry.ts:14475 for #validateRecordInScope, and :14467 for #assertControlledByParentWrite.
    • Both restores are proven: the blob equals HEAD's (5dacc0aa3dc2), git diff HEAD is empty, and git status is clean.
    • No test file was added or kept.
  • Comment-only check: git diff -U0 origin/main...HEAD -- packages/spec has 27 changed lines. None starts with anything other than *. A control-byte scan of the three changed files finds nothing.

  • Local-only family node scripts/check-dev-prereqs.mjs: NOT MEASURED. It exits 1 with "The workspace is not built" (56 of 70 packages have no dist), so it never reached the spec-dist freshness check. Spec's own dist freshness is covered by check:generated's declaration stamp match on this head.

  • No ablation: the diff changes no tested behaviour.

  • Not run locally, left to CI: test:repo (Test Core), pnpm lint, the type-check lanes, and the families dispatch-gates lists outside its runnable set.

Acceptance notes

  • packages/spec/src/data/object.zod.ts, in the forceCbpMasterDetailRequired docblock: the "Why" paragraph says "Today only the security gate (assertControlledByParentWrite) closes that shape". Since protocol 18, the parse also refuses that shape (refuseControlledByParentWithoutOneMaster). The same docblock says so two paragraphs later, so a reader is not misled. It is present-tense drift, and outside this card's file surface. Carrier: none.
  • After the conversion, an insert without the master is also refused by the record validator, not the gate. The gate stands down for a required: true master, which is the 2026-08-15 ruling behind omissionRefusedByValidation. The envelope is then 400 VALIDATION_FAILED with fields[], where it was the gate's 422 MISSING_REQUIRED_FIELD. The docblock's sentence is about "before", so it stays true, and I did not widen it to describe the insert's current envelope. This is an observation, not a defect. Carrier: none.
  • Three other prose sites name assertControlledByParentWrite: packages/lint/src/data-model-rules.ts, packages/lint/src/rule-explanations.ts and that same object.zod.ts docblock. Each speaks of the insert leg only. None repeats the wrong update-half claim.

Deviations


Generated by Claude Code

The ActionParam options docblock's visibleWhen bullet quoted objectui's
inline param branch as `options: param.options`; at the console pin it
reads `options: authoredOptions` (resolveOptionLabels over the authored
list). The cbp-master-detail-required-defaulted conversion's docblock
said an update nulling the master was refused before by the security
gate; that gate reads the stored row's FK on a by-id update, and the
refusal is the record validator's ADR-0113 check, which only
`required: true` arms.

Comment text only, plus a patch changeset for @objectstack/spec.

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

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

⛔ 2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ActionParamSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-0.mdx (via ActionParamSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7a8733f1c77681137d25abb1c02b5c889b71c01e

⚠️ 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 7a8733f1c77681137d25abb1c02b5c889b71c01e → 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

席位核对 (seat check) · PR #24290 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T09:03Z

ACCEPT at head c239951c267fcc596fba3d871bba712ef20b687c (card #24187, claim 6247320472, report 6247975508). It lands only after a CONTRACT_REVIEW_TIER PASS on this head and every check green: the diff touches packages/spec/src/** outside tests, which is the schema face (record-recognisers --paths → CONTRACT_REVIEW_TIER). The review is running 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 #24187, line 2 is Clause-②: no, and the body has no other closing keyword. Assignee zhuangjianguo = the card's. 3 files, +36/-5: the changeset, conversions/registry.ts, ui/action.zod.ts. No content/docs/releases/.
Premises, re-measured by the seat Item 1, objectui at the pin 5e15928b4631: resolveActionParam (:538); authoredOptions = resolveOptionLabels(param.options, ctx.locale) (:565); options: authoredOptions (:574); resolveOptionLabels (:246) returns options untouched when everyLabelResolved, and otherwise spreads only the map-label entries. Item 2, on origin/main: assertControlledByParentWrite reads opCtx.data[rel.fk] on operation === 'insert', and row[rel.fk] from the stored row otherwise. validateRecordInScope's update branch pushes required_cleared only under def.required && isEmptyForRequired(…). Both new sentences match.
Comment-only The diff's two packages/spec/src hunks change only * docblock lines. No key, export, accepted value or message text moves.
Changeset, sentence by sentence .changeset/24187-docblock-mechanisms.md: '@objectstack/spec': patch and Clause-②: no, both right for a prose-only fix to a file that ships (src/**/*.zod.ts is in files[]). Its four sentences each match the pin reading above: "no longer quotes … options: param.options"; "reads options: authoredOptions, where … resolveOptionLabels(param.options, ctx.locale)"; "the authored array itself … otherwise only the entries with an inline per-locale label re-spread"; "No key, schema, export, accepted value or message text changes".
Gates (check-runs on this head, read now) 38 runs: 19 success, 6 skipped, 13 in progress, 0 failure. Not converged, so it does not land yet.
Accepted deviations (1) One git fetch --depth=1 of the pinned commit 621a4876… for check-plugin-teardown-shape --self-test. (2) One main merge before the PR opened. (3) Two declared NOT MEASURED narrowings (check:dual-build-cjs-loads, check:dev-prereqs); CI's Lint & Repo Gates runs the first.
Out of scope The PR's three Acceptance notes stay notes. The object.zod.ts forceCbpMasterDetailRequired "Today only the security gate…" sentence is auxiliary-face drift that does not mislead: the same docblock states the parse refusal. The packages/lint prose names only the insert leg, which is true. Each line: Acceptance notes.
Landing After the PASS on this head and a fully green head: pr_ready then automerge_enable, through the relay. Not a governed surface; under 3,000 changed lines.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c239951c267fcc596fba3d871bba712ef20b687c
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/ui/action.zod.ts + packages/plugins/plugin-security/src/security-plugin.ts, packages/objectql/src/validation/record-validator.ts, packages/core/src/utils/required-emptiness.ts, scripts/symbol-anchors.mjs, scripts/check-spec-docblock-symbol-anchors.mjs, and at the objectui pin 5e15928b4631: packages/fields/src/resolveActionParams.ts, packages/app-shell/src/views/ActionParamDialog.tsx, packages/app-shell/src/utils/paramToField.ts, packages/core/src/evaluator/optionRules.ts, the four option widgets
Remainder, the lane seat's ACCEPT at TIER_DEFAULT: .changeset/24187-docblock-mechanisms.md

Isolated read-only review, rendered 2026-10-18T09:12Z as a subagent of the seat session named in Reviewed-by:. Inputs: card #24187 (body and its three comments, the triage, the claim and the os-dev report), PR #24290 (body, file list, and the net diff against the merge base 1e001c7dfcdf864bcc48fe7c1f5b2b11568e13b5, which git merge-base origin/main HEAD confirms), and the head's check-runs. The shared checkout was read with git show; objectui with git -C ../objectui show at the pin; nothing was checked out, built, run or re-run.

① Derived judgments

Net diff: 3 files, +36 / −5, identical on GitHub and locally. The 27 changed lines under packages/spec all match ^[+-]\s*\*: comment lines only, zero code lines, zero control bytes in the three files at the head. Every judgment below was taken by reading the cited code, not the PR body.

  • D1 — accept set: unchanged. Right. No key, schema, export, accepted value, guidance string or message text moves. conversions/registry.ts is the tracked hand-written file (blob in the merge-base tree), not the gitignored generated src/migrations/registry.ts.
  • D2 — public surface: item 1 ships, item 2 does not. Right. @objectstack/spec files[] carries src/**/*.zod.ts, so the action.zod.ts docblock is tarball content; src/conversions/registry.ts is not in files[]. No checked-in generated artefact carried the old prose (git grep over content/docs, json-schema, api-surface, liveness, prompts, llms.txt at the merge base: no hit), so nothing was owed a regeneration and none appears in the diff.
  • D3 — item 1, the quoted expression. Right. At the pin, resolveActionParams.ts:538 declares export function resolveActionParam( (singular); :565 is const authoredOptions = resolveOptionLabels(param.options, ctx.locale); :574 (inline branch, no param.field) and :634 (field named but absent from metadata) both read options: authoredOptions. resolveOptionLabels (:246) returns the authored array itself when everyLabelResolved holds (every label a string), else maps the list, returning string-label entries as they are and re-spreading only the entries whose label is a map as { ...option, label: resolveInlineI18nLabel(option.label, locale) ?? option.value }. The bullet's parenthesis states exactly that, visibleWhen survives on both paths, and its point (VERBATIM) is kept.
  • D4 — item 1, the bullet's unchanged context links still hold at the pin. Right. ActionParamDialog.tsx:536 re-spreads each option with a localised label; paramToField.ts:102 passes options: param.options; SelectField, MultiSelectField, RadioField, CheckboxesField each reference useCascadingOptions; resolveCascadingOptions is exported at packages/core/src/evaluator/optionRules.ts:217.
  • D5 — item 1, the docblock's next paragraph still names the previous pin f8267df93aba (:650–:651, :515). The three objectui files are blob-identical between f8267df93aba and 5e15928b4631 (blob ids 69e460dbab, 80ce5490cd, 70b35303d5 on both), so those readings hold at the current pin and nothing in the diff is wrong. Observation, not a finding: the docblock now names two different shas as "the console pin" in consecutive paragraphs; outside the card's file surface, and the PR body says so. Carrier: the next edit of that paragraph moves it to the current pin.
  • D6 — item 2, the security gate half. Right. At the merge base, assertControlledByParentWrite (security-plugin.ts:8536) takes the master id from opCtx.data[rel.fk] only when operation === 'insert' (:8593); on any other single-id write it reads the stored row as system and takes row[rel.fk] (:8598–:8600), never the incoming body; a bulk write returns early (:8596). A null master id throws MasterReferenceMissingError (:8717) except on an insert where rel.omissionRefusedByValidation holds (:8716), that predicate being master_detail && required && !readonly && !system (:7953–:7954). So an update nulling a non-required master's FK read a non-null stored FK and passed: "The update nulling it was not [refused]" is right, and the old sentence was wrong.
  • D7 — item 2, the record validator half. Right. validateRecordInScope (record-validator.ts:1959, an export function, called by the exported validateRecord at :1938) walks only the supplied keys on update (:1989) and refuses with required_cleared on def.required && isEmptyForRequired(def, value) && def.type !== 'autonumber' (:2007–:2011); isEmptyForRequired (packages/core/src/utils/required-emptiness.ts:60) is true for a null value. Without required the check never fires, so "only required: true arms it" is right.
  • D8 — item 2, the "before" claim. Right. Both seams read the same at 76815b901^, the parent of the merge that retired the second tier: masterId = data[rel.fk] on insert, masterId = row[rel.fk] otherwise, the same stand-down predicate, the same def.required && isEmptyForRequired update check. The update was accepted before protocol 18, exactly as the new sentence says.
  • D9 — item 2, the read-filter clause. Right. The gate's derived filter is { [rel.fk]: { $in: masterIds } } (:8244), which a null FK never matches; the gate's own freeze note says the same. The spelling masterFK IN (accessible master ids) already stands in data/object.zod.ts:3481, the two migrations/entries/semantic/18.cbp-* entries and packages/lint/src/data-model-rules.ts:291, so the hunk adds no second dialect. (The PR body attributes the precedent to src/migrations/registry.ts, the generated file built from those entries; harmless, and not in the diff.)
  • D10 — the "Lossless" paragraph still holds. Right. The rewritten sentence no longer claims the update was refused before; it says the converted row gains the refusal the builder's force already gives an authored object, which is the conversion's stated contract.
  • D11 — the two new symbol anchors and the gate that reads them. Right. security-plugin.ts#assertControlledByParentWrite and record-validator.ts#validateRecordInScope name tracked repo-root paths, so check-spec-docblock-symbol-anchors judges them (it declines only sibling-repo and abbreviated paths). The shared resolver's member regex takes private async NAME( and its function regex takes export function NAME, so both resolve as declaration, matching the dev's reverse check (each anchor red when misspelled). Lint & Repo Gates is success on this head.
  • D12 — acceptance note on the insert's current envelope. Right. After the conversion the gate stands down for a required: true master (:8716) and the validator answers 400 VALIDATION_FAILED; the docblock's insert sentence speaks of "before" and stays true. The dev did not widen it, correctly.

② Semver level

.changeset/24187-docblock-mechanisms.md: '@objectstack/spec': patch, prose only, and its text matches the code read in D3 (the pin, the identity fast path, the map-only re-spread). The PR body and the changeset each carry the line Clause-②: no at the start of a line, with no arm. Right on every count: the diff publishes changed TSDoc inside a shipped src/**/*.zod.ts file, so skip-changeset would be wrong; it widens no accept set, so minor is not owed; it narrows nothing and removes nothing authorable, so no (narrowing) arm, no major, no BREAKING marker and no ADR-0087 disposition is owed. This follows the PR #24072 precedent the card names. Check Changeset is success on this head; Lint · CI, release & engine gates (home of check-adr-0087-registration and check-changeset-no-major) is success.

Tier: the schema face (packages/spec/src/**, published) maps the review to CONTRACT_REVIEW_TIER; no governed surface is touched (Governed Surface Queue Guard success); head repo equals base repo; 41 changed lines; the card's Claim: names this branch and the The card this PR closes must claim this branch check is success.

③ Boundary flags

  • Dev flag: check:dual-build-cjs-loads NOT MEASURED locally (92 packages without dist). Answered: it runs in Lint & Repo Gates, success on this head.
  • Dev flag: local-only check-dev-prereqs NOT MEASURED. Answered: a local prerequisite family, not a CI gate; the dev's check:generated stamp match and D2 (no artefact carried the old prose) cover spec-dist freshness for a comment-only diff.
  • Dev deviation: one git fetch --depth=1 of a pinned commit into the shared object store so a --self-test could find its positive control. Answered: adds an object, moves no ref and no checkout; benign.
  • Dev deviation: main merged once before the PR opened (head is the merge of 1e001c7df). Answered: section 10 of AGENTS.md asks for it; the two incoming commits touch none of the three files, and the net diff against the merge base is exactly the three files.
  • open_questions: empty. Nothing to answer.
  • out_of_scope_findings 1, carrier none: packages/spec/src/data/object.zod.ts:3483, the forceCbpMasterDetailRequired docblock, reads "Today only the security gate … closes that shape", and :3499 names refuseControlledByParentWithoutOneMaster in the same docblock. Verified. Escalated to the seat rather than left as none: that file ships in the tarball (src/**/*.zod.ts) and the sentence is a published docblock naming the mechanism in the wrong tense, the same family this card was filed for under its own filing gate ①. Recommend a sibling p3 documentation card at the seat's discretion; it is outside this card's file surface and does not hold this PR.
  • out_of_scope_findings 2, carrier none: the insert's current 400 envelope after conversion. Answered in D12; an observation, consistent with the code, nothing owed.
  • Two pins in one docblock (D5): observation, carrier the next edit of that paragraph; nothing owed here.

Check-runs on c239951c267fcc596fba3d871bba712ef20b687c, read twice. At 2026-10-18T09:04:16Z: 39 runs, 24 success, 6 skipped, 9 in_progress, 0 failure. At 2026-10-18T09:11:06Z: 43 runs, 27 success, 9 skipped, 7 in_progress, 0 failure. Of the five required contexts, Lint & Repo Gates, Build Core and Governed Surface Queue Guard are success; TypeScript Type Check is not yet posted (its Type Check · workspace lane still running; source gates, consumer gates and debt ledger are success); Test Core shards 1–6 are still running. in_progress is not a pass: the queue waits on those two contexts' success, which is the owning seat's read before it arms anything. No conclusion read on this head is anything but success or skipped.

Implemented-by: claude/issue-24187-docblock-mechanisms
Reviewed-by: session_019oFY6Hi35J7atvgVFnmoaK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review adopted, plus one correction to the seat's ACCEPT · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T09:15Z


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/s tooling

Projects

None yet

2 participants