Skip to content

feat(spec/ai): blueprint select options declare their lifecycle kind and a blueprint object names its stageField - #24213

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24198-blueprint-option-kind-stage-field
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24198-blueprint-option-kind-stage-field

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24198

Clause-②: yes

What changed

The object schema already requires a lifecycle kind (open / won / lost) on every option of the field an object's stageField names (refuseStageFieldWithoutKinds, packages/spec/src/data/object.zod.ts). The AI blueprint had no slot for either key, so the design model could not say which field is an object's stage or which option ends its lifecycle, and that rule never applied to an AI-built object. Both blueprint shapes now carry both keys (packages/spec/src/ai/solution-blueprint.zod.ts):

Key Lenient (SolutionBlueprintSchema, what apply_blueprint parses) Strict mirror (SolutionBlueprintStrictSchema, the structured-output contract)
fields[].options[].kind optional required, nullable
objects[].stageField optional, SNAKE_CASE required, nullable, SNAKE_CASE (strictIdentOrNull, the same as nameField)
  • kind reuses SelectOption.kind's enum by identity. One shared reference, optionKindEnum(), reads SelectOptionSchema.shape.kind.unwrap(). Each side then adds its own form of "optional" (.optional() on the lenient side, .nullable() on the strict side). The three values are written nowhere in this file. field.zod.ts is not edited, and SelectOption's accept set does not change. It is a function, not a module-level constant, so the lenient lazy shapes do not force SelectOptionSchema at import.
  • stageField sits next to nameField in both object shapes and maps onto ObjectSchema.stageField. Only the string form is carried. The object schema's false ("no linear lifecycle") has no blueprint spelling (see Acceptance notes).
  • The describes are written from refuseStageFieldWithoutKinds, not from the card. One shared sentence (STAGE_FIELD_RULE) appears in all four describes. It says the object schema refuses the object unless the named field is a single-value select or radio whose inline options each declare kind, and that naming a multiselect / checkboxes / tags field that carries options is refused too. The option describes add the FieldSchema half: kind only goes on a single-value select / radio field's options.
  • Regenerated by their generators: packages/spec/authorable-surface/ai.json (+ai/BlueprintObject:stageField) and content/docs/references/ai/solution-blueprint.mdx. The published json-schema/ai/SolutionBlueprintStrict.json is gitignored and built by gen:schema. After the build it has both keys, and every object node's required still equals its properties with additionalProperties: false (checked by walking the built file).
  • Changeset .changeset/24198-blueprint-option-kind-stage-field.md: @objectstack/spec minor.

Premise checks (on origin/main f792290df, this branch's base)

  • No key existed before. grep -n "kind\|stageField\|nameField" over solution-blueprint.zod.ts found stageField 0 times and no kind key. The only kind hits were two describe strings that say "View kind". Control: nameField hit at :109 and :358.
  • ObjectSchema.stageField is the only trigger of the mandatory-kind rule. git grep refuseStageFieldWithoutKinds gives one definition (object.zod.ts:1970), one call (:3160, with object.stageField), one docblock mention and the liveness note. No stageField is declared anywhere in packages/spec/src other than object.zod.ts:2897. The non-spec readers of stageField (lint validate-semantic-roles, validate-object-field-refs, metadata-core FLS references, metadata-protocol reference sites) check that the pointer resolves. None of them requires kind. So stageField rides this card, as the seat's ruling said it would if this premise held.
  • No consumer outside packages/spec. git grep -E "SolutionBlueprintStrictSchema|SolutionBlueprintSchema" -- ':!packages/spec/**' gave 5 hits: a consumed changeset, the auto-generated reference page and ADR-0033. None is code. Control: the same names hit 70 times in solution-blueprint.test.ts and 11 times in solution-blueprint.zod.ts. The consumer is cloud's propose_blueprint / apply_blueprint (objectstack-ai/cloud#2926).

Tests

packages/spec/src/ai/solution-blueprint.test.ts, 72 tests:

  • Lenient: both keys survive the parse; both stay optional; kind is SelectOptionSchema.shape.kind.unwrap() itself, on both sides (identity, not shape); a kind outside the vocabulary is refused at objects.0.fields.1.options.2.kind; a non-snake_case stageField is refused at objects.0.stageField. A contract leg copies a parsed blueprint object 1:1 into ObjectSchema. It parses, and once one option drops its kind, the object schema refuses it at exactly fields.status.options.3.kind (custom).
  • Strict: both keys are carried; null is accepted for both; leaving either key out is refused at exactly its own path; an off-vocabulary kind and a non-snake_case stageField are refused.
  • Parity: a new option-level key-parity pin (both sides equal ['kind', 'label', 'value']). The object-level pin now names stageField.
  • Existing strict fixtures gained stageField: null and, where they carry options, kind: null. Without that, the leading-digit pin would have stayed red because a new key was missing rather than because of the digit.

Results at 5645c2cd9 (the merge of origin/main f7af83945):

  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 691 files, 20974 passed, 1 todo, exit 0.
  • pnpm --filter @objectstack/spec typecheck (tsc, scripts and the test-layer program, which compiles src/**/*.test.ts): exit 0.
  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date, exit 0.

Ablation, run through scripts/ablation-replace.mjs in wrap mode. Each anchor hit once, the blob changed, and the restore was proven (blob equals HEAD, git diff HEAD empty). The test reads src directly, so no build or dist step is involved.

  • Strict stageField renamed to stageFieldAblated: 20 failed / 52 passed, including the OBJECT schemas carry exactly the same keys.
  • Lenient option kind renamed to kindAblated: 5 failed / 67 passed. The failures are the option key-parity pin, the carry test, the identity test, the vocabulary refusal and the ObjectSchema contract leg.

The direction was red both times, as predicted.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 108 commands for this diff, and all were run at 5645c2cd9. --ran reconciliation: 108 derived, 107 run, 1 NOT-MEASURED, 0 UNRUN.

  • NOT MEASURED: pnpm check:dual-build-cjs-loads. Family: dual-build CJS loads. Reason: exit 3, PREREQUISITE NOT MET, because it needs a whole-workspace pnpm build. This is a declared narrowing, and CI's Lint & Repo Gates runs it.
  • Six gates first answered exit 3 or a stale reading. A concurrent turbo build of @objectstack/lint rebuilt packages/spec/dist while they ran, so their first answers measured nothing. Once lint, formula and objectql were built, all 13 gates in the overlap window were re-run, and all exited 0: check:doc-formula-expressions, check:doc-security-posture, spec check:exported-any, check:generated, check:liveness, check:llms-txt, check:skill-examples, check:skill-refs, check:strictness-ledger, check:variant-docs, check:yaml-examples, check:changeset-gate-self-tests and check:lean-entry-closure.
  • Not run locally because CI owns them: the repo-wide scans, test:repo, the workspace and consumer type-check lanes, Test Core, Build Core and dogfood.

Acceptance notes

  • stageField: false has no blueprint spelling. ObjectSchema.stageField also takes false, which declares a status field non-linear and turns off stage heuristics. The card scopes stageField as a snake_case name, and no producer or consumer of false from the blueprint is named, so only the string form is carried. Owner: none. Noted, not filed.
  • The blueprint does not judge kinds itself. Strict structured output cannot express a rule that spans keys ("every option of the field stageField names has a non-null kind"). The refusal arrives, located, when the applier writes the object through ObjectSchema (pinned by the contract leg above). The describes tell the model the rule up front.
  • Cloud seam (objectstack-ai/cloud#2926): apply_blueprint copies objects[].stageField onto the object and options[].kind onto each option, after stripping the strict mirror's nulls to absent, as it does for every other key. The renderer half is record path: a derived stage drops its option's declared lifecycle kind, so terminal stages are still guessed from spelling (deriveStages maps value and label only; spec 18 declares kind) objectui#12907.

Generated by Claude Code

…and a blueprint object names its stageField

Both option shapes (lenient BlueprintFieldSchema.options[] and the strict
mirror) gain kind, taken by reference from SelectOption.kind's enum; both
object shapes gain stageField. The describes state what the object schema
refuses about the field stageField names.

Claude-Session: https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ
Co-authored-by: Claude <noreply@anthropic.com>
…set for the blueprint kind / stageField keys

Claude-Session: https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ
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 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/objects.mdx (via stageField (symbol, a field of const object StrictObject))
  • content/docs/deployment/cli.mdx (via stageField (symbol, a field of const object StrictObject))
  • content/docs/ui/forms.mdx (via stageField (symbol, a field of const object StrictObject))

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

  • content/docs/releases/v15.mdx (via stageField (symbol, a field of const object StrictObject))

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/authorable-surface/ai.json) — 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 8259ea03e2959129c688639f69d0782c3a820f89 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8259ea03e2959129c688639f69d0782c3a820f89

⚠️ 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 8259ea03e2959129c688639f69d0782c3a820f89 → 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: 5645c2cd9f10036a302a2321ea42ad387129eb07
Local-runs: none
Scope: packages/spec/src/ai/solution-blueprint.zod.ts + packages/spec/src/ai/solution-blueprint.test.ts, .changeset/24198-blueprint-option-kind-stage-field.md, packages/spec/authorable-surface/ai.json, content/docs/references/ai/solution-blueprint.mdx (the whole diff — no remainder is left at TIER_DEFAULT), read beside the cited code on main 8259ea03e2: packages/spec/src/data/field.zod.ts (SelectOptionSchema.kind, the kind-on-a-multi-value-field refusal), packages/spec/src/data/object.zod.ts (refuseStageFieldWithoutKinds, ObjectSchema.stageField), packages/spec/src/data/field-value.zod.ts (SINGLE_OPTION_TYPES), packages/spec/src/shared/lazy-schema.ts, packages/spec/src/shared/strict-object.ts, packages/spec/scripts/solution-blueprint-header-row.test.ts, scripts/pm/clause2-line.mjs; precedent PR #24152 (card #24129) by its file list and changeset.

Inputs: card #24198 (body; comments 6238179039 triage, 6238356000 dispatch claim, 6239509257 os-dev report), PR #24213 (body, file list, net diff against main, its one bot comment — the docs-drift advisory), and the check-runs on the head. Reads only: a git fetch of main and the branch, git show on both, and record-recognisers --template --paths for the hits (offline, no board). Nothing was built, run or re-run; the ablation and gate readings below are the dev's, named as such.

① Derived judgments

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

  1. Lenient BlueprintFieldSchema.options[].kind — optionKindEnum().optional(). Widens: a key the plain z.object stripped is now carried, and only open / won / lost is accepted (pinned at objects.0.fields.1.options.2.kind, invalid_value). RIGHT. The corner where a document that smuggled an off-vocabulary kind through the strip is now refused is not a narrowing of any declared contract: the key was undeclared, and the strict mirror — the only producer path — forbade it as an additional property.
  2. Lenient BlueprintObjectSchema.stageField — z.string().regex(SNAKE_CASE).optional(), beside nameField, the string arm only. RIGHT: card scope item 2 names a snake_case field name; ObjectSchema.stageField is z.union([z.string(), z.literal(false)]) and the false arm is deliberately not carried (③ item 8). Non-snake_case is refused at objects.0.stageField, invalid_format — pinned.
  3. Strict StrictField.options[].kind — optionKindEnum().nullable(), present-required like every key in the mirror. Its JSON-Schema form is the nullable enum StrictView.type already uses, so the OpenAI-strict invariants (every key in required, additionalProperties: false) hold by the same construction. RIGHT. Pinned: null accepted, absence refused at exactly objects.0.fields.1.options.1.kind, off-vocabulary refused there too.
  4. Strict StrictObject.stageField — strictIdentOrNull(...), the helper nameField uses. RIGHT. Pinned: null accepted, absence refused at objects.0.stageField, non-snake refused.
  5. Enum reuse by identity, not by spelling — optionKindEnum is an arrow function returning SelectOptionSchema.shape.kind.unwrap(). SelectOptionSchema is a lazySchema wrapping a strictObject(...) factory, strictObject returns the ZodObject type, so .shape is typed and the proxy resolves it; kind there is z.enum([...]).optional().describe().meta(), so .unwrap() is the inner enum instance, and each side's .optional() / .nullable() / .describe() clones a wrapper around it without touching the shared enum or its registry metadata. field.zod.ts is not edited and SelectOption's accept set is unchanged. The identity test pins both sides toBe the same instance and the vocabulary ['open', 'won', 'lost']. RIGHT. One comment inaccuracy, non-blocking: the docblock says the function form keeps a module-level read from forcing SelectOptionSchema at import, but StrictField is an eager module-level z.object, so optionKindEnum() runs at import of this module regardless. Harmless — one small schema, and I found no pin on import-time laziness — and the function form costs nothing.
  6. The describes (public: they ship in both JSON Schemas and on the reference page), read against the code they cite. The per-option refusal at ['fields', f, 'options', i, 'kind'], the picklist refusal and the multi-value refusal at ['stageField'] (gated on hasOptionSource), SINGLE_OPTION_TYPES = {select, radio}, and the field schema's refusal of kind on any other field's options at ['options', i, 'kind'] — the four describes state these correctly, and "absent reads as open" is SelectOption.kind's own sentence. ADR-0085 is the right citation (it types stageField). One over-statement, non-blocking: "refuses the object unless the field stageField names is a single-value select or radio whose inline options EACH declare kind" — the parse does not refuse a stageField naming a field with no options at all, or a non-select field without options (the docblock lists both as unchanged on purpose; lint validate-semantic-roles reports the dangling pointer). No accept set turns on that sentence, and the stricter reading steers the model to the one shape that passes both parse and lint. Recorded, not a defect.
  7. The contract leg — a 1:1 copy of a parsed blueprint object into ObjectSchema parses, and dropping one option's kind is refused at exactly ['fields', 'status', 'options', 3, 'kind'] (custom). This pins that the blueprint spellings ARE the object schema's spellings, so apply_blueprint copies and converts nothing. RIGHT.
  8. Parity pins — a new option-level key pin (both sides ['kind', 'label', 'value']), the object pin now names stageField outright, and the strict fixtures gained stageField: null / kind: null so the leading-digit VALUE-parity pin fails for the digit alone. RIGHT.
  9. Generated artefacts, each against what the source change implies: authorable-surface/ai.json gains ai/BlueprintObject:stageField only — the option kind sits in an anonymous inline object under the existing ai/BlueprintField:options row, as the precedent's BlueprintView:filters did; authorable-surface.base.json is the deletion-gate anchor (--update-base), untouched by an addition — right. api-surface: optionKindEnum and STAGE_FIELD_RULE are module-private, no export moves — unchanged, right. json-schema.manifest: no new def — unchanged, right. dropped-refinements.baseline.json: no .refine() added (.regex projects as pattern, the enum projects) — unchanged, right; the precedent moved it only for its filters.element refinement. The strictness-ledger ai/ shard: z.object( sites 26 on main, 26 at the head — unchanged, right. content/docs/references/ai/solution-blueprint.mdx: regenerated, carries both keys in all four tables plus the new nested BlueprintField.options[number] table. The published json-schema/ai/SolutionBlueprintStrict.json is gitignored; its strict invariants are pinned at parse level (requires both keys to be present) and by construction (item 3).
  10. Header-row pin (scripts/solution-blueprint-header-row.test.ts): the module doc block and the blank line separating it from const SNAKE_CASE are unchanged — the only header-zone change is the import on line 5 — and the corpus row in skills/objectstack-ai/references/_index.md is unchanged. RIGHT.
  11. Boundaries: field.zod.ts and object.zod.ts read, not edited; no cloud or objectui path; no governed path in the file list; draft PR, assignee os-project-manager; the dispatch Claim: names this branch and the dev posted no second claim.

② Semver level

  • Changeset .changeset/24198-blueprint-option-kind-stage-field.md: @objectstack/spec minor, Clause-②: yes on its own line; the PR body carries the same line, key-initial, as its third line. Both read as a widening: clause2-line.mjs reads yes and yes (widening) as one declaration (an absent arm declares no direction; yes already means widening), so the dev's flagged mismatch with the card's yes (widening) is spelling, not substance.
  • What publishes: two lenient keys (the accept set grows) and two strict keys. The strict mirror formally loses documents lacking the two keys, but it is the generateObject output contract, regenerated per call and never persisted (module header), whose only producer generates against the schema it is handed; the changeset's last bullet says so ("a producer of the STRICT shape must now send stageField on each object and kind on each option"), exactly as precedent PR feat(spec): BlueprintViewSchema and StrictView declare filters and sort, so an AI-built view carries its own filter #24152's changeset did for filters / sort, which landed minor under Clause-②: yes (widening). No (narrowing) arm is owed; nothing is removed or renamed, so no migration text and no ADR-0087 marker.
  • minor matches the diff. Check Changeset is green on the head.

③ Boundary flags

open_questions is empty. Each deviations entry and the one out_of_scope_findings entry of report 6239509257, answered:

  1. Clause-②: yes vs the card's yes (widening) — equivalent readings (② above); no action.
  2. check:dual-build-cjs-loads not measured locally — it is hosted by ci.yml's Build Core, green on the head; answered by the gate.
  3. test:repo not run locally — the header-row test's two inputs are unchanged (① item 10); Test Core carries it, state recorded below.
  4. A concurrent turbo build rebuilt packages/spec/dist mid-gate and the 13 overlapped gates were re-run — a note about the dev's local readings; the gate verdicts of record are the head's check-runs; nothing to escalate.
  5. The first ablation leg was a no-op (bad log path) and was re-run — the cited run is the second; the ablation is the dev's measurement and was not re-run here (read-only). The predicted direction is consistent with the pins I read: renaming either key fails the key-parity pins by construction.
  6. origin/main moved six commits after the pre-PR merge and no second merge was made — the net diff here is computed against current main 8259ea03e2 and applies cleanly; mergeable_state: blocked is the draft flag plus unfinished required checks, not a conflict; §10 asks for a merge only on a conflict, a main-caused red or a queue removal. Right.
  7. Four pushes — the push-each-step rule; no flag.
  8. stageField: false has no blueprint spelling (noted, not filed) — accepted as noted. The card scopes a snake_case field name; a blueprint that omits stageField keeps the platform's heuristic detection, which is the behaviour before this PR; the named consumer (objectstack-ai/cloud#2926) asks for the kinds, not for "declared non-linear" from the design step. Not a defect, a contract violation or an authoring trap under Prime Directive 10, so no issue is owed; a producer wanting the false arm from the blueprint is a new card.
  9. The seat's stageField premise (it rides the card iff ObjectSchema.stageField is the only trigger of the mandatory-kind rule) — confirmed on main 8259ea03e2: one definition (object.zod.ts:1970), one call (:3160, with object.stageField), and no other stageField declaration in packages/spec/src besides object.zod.ts:2897. The fork the seat reserved did not open.
  10. Reviewer notes, both non-blocking, carried in ① items 5 and 6: the optionKindEnum function rationale is moot (the strict side is eager), and STAGE_FIELD_RULE over-states the parse refusal for an options-less target field. Neither moves an accept set; neither is a FAIL condition.
  11. Docs-drift advisory on the PR: three hand-written pages and one release page name stageField — the object's key, not the blueprint's; nothing in this diff touches them; advisory only.

Check-runs on the head, read at 2026-10-17T19:39Z: 39 check-runs; 27 success (the gate-carrying names among them: Build Core, Check Changeset, Governed Surface Queue Guard, Lint & Repo Gates, Spec property liveness, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates); 5 skipped (full-battery lanes not run on a PR: Console Pin Gate, Dogfood Regression Gate (the per-shard matrix row, named with a template placeholder), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL)); 7 in_progress: Test Core (1/6), Test Core (2/6), Test Core (3/6), Test Core (4/6), Test Core (5/6), Test Core (6/6), Type Check · workspace. The gate verdicts are the check-runs' own; the contract verdict below is mine. The unfinished lanes are recorded as unfinished: this record does not call them green, and the queue guard reads them at landing.

Implemented-by: claude/issue-24198-blueprint-option-kind-stage-field
Reviewed-by: session_01TyRhWbVbbQpQJTDbW8VahJ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #24213 @ 5645c2cd9f (#24198) · domain:spec seat 1 · os-project-manager · session session_01TyRhWbVbbQpQJTDbW8VahJ · 2026-10-17T20:00Z

Reviewer of record. Every item below was checked against GitHub and the code, not against the report (6239509257 on #24198).

  • Shape: draft, base main. The first line is Fixes #24198, the body's only closing keyword, then Clause-②: yes, matching claim 6238356000. 5 files, +241/−7, assignee os-project-manager. Not governed: check-governed-merges.mjs --pr 24213 reads 0 of 5 paths on the register, under 3000 lines.
  • Premise, measured by the seat before dispatch (f963d8e2b): neither blueprint shape had kind on its options or stageField on its objects; SelectOption.kind sits at field.zod.ts:452; refuseStageFieldWithoutKinds is called once, with object.stageField (object.zod.ts:3160).
  • The seat's scope call holds: stageField rides this card on the measured premise that ObjectSchema.stageField is the rule's only trigger. The dev and the contract review both confirmed that premise on main.
  • What it lands:
    • Lenient: options[].kind (optional, SelectOption.kind's enum by identity) and stageField (optional snake_case).
    • Strict mirror: both keys present-required and nullable, in the same form as StrictView.type and the feat(spec): BlueprintViewSchema and StrictView declare filters and sort, so an AI-built view carries its own filter #24152 precedent.
    • Parity and contract-leg pins; the authorable-surface row and the reference page regenerated; a minor changeset.
    • The changeset's sentences were read against the diff. "A producer of the STRICT shape must now send stageField on each object and kind on each option (null when it has none)" is the honest producer consequence of the strict pattern. "The object schema's false value has no blueprint spelling" matches the diff.
  • Contract review: PASS 6239776296 on this head (CONTRACT_REVIEW_TIER). It agrees with minor and Clause-②: yes. Its two non-blocking notes stand as notes: the optionKindEnum laziness comment is moot, and the stageField describe over-states the parse refusal for an options-less field (lint reports that case).
  • Gates: 41 check-runs on 5645c2cd9f, all completed: 36 success, 5 skipped (all in check-expected-skips.mjs's roster, matched by name). The five required contexts are success. Mergeable, state clean.
  • Deviations, accepted:
    • Clause-②: yes carried verbatim from the claim;
    • check:dual-build-cjs-loads declared to CI;
    • test:repo declared to CI;
    • an overlapping local build, with the 13 affected gates re-run green;
    • one ablation leg first ran as a no-op and was re-run;
    • no second main merge: no overlap.
  • Out-of-scope finding: the blueprint has no spelling for stageField: false: dropped — no producer or consumer of that form is named, and the PR's Acceptance notes record it.

The PR goes to the merge queue in this act. objectstack-ai/cloud#2926 can start once cloud's pin carries this merge.


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

Projects

None yet

2 participants