Repository navigation
spec(ai): the blueprint's select options declare their lifecycle kind and a blueprint object names its stageField, so an AI-built status field ships the kinds the object schema already requires #24198
Description
Source
objectstack-ai/cloud#2926 (the AI builder: a status field's options get their lifecycle kinds, so a loan's stage path stops drawing 逾期 as the step after 已还). Its Blocked-by: was #23809, which landed as PR #23846 (adad73f714). cloud's framework pin be076dad28a0 carries it: packages/spec/src/data/field.zod.ts:452 declares kind: 'open' | 'won' | 'lost' on a select option, and object.zod.ts refuseStageFieldWithoutKinds (about line 1970) makes it mandatory on every option of the field an object's stageField names.
The gap, measured at be076dad28a0
The AI builder streams the spec's own blueprint schema: SolutionBlueprintStrictSchema, extended in cloud only by plan-view-intent.ts. That schema has no slot for either key.
packages/spec/src/ai/solution-blueprint.zod.ts:88(BlueprintFieldSchema.options) and:320(StrictField.options) are{ label, value }only. The lenient side strips unknown keys on parse.BlueprintObjectSchema(:100) andStrictObject(:331) carrynameFieldbut nostageField.
So a model cannot state an option's kind or which field is the object's stage, and apply_blueprint has nothing to write. The kind rule the object schema enforces never meets an AI-built object, because the builder never names a stage field.
Scope
BlueprintFieldSchema.options[]andStrictField.options[]gainkind: the same enumSelectOption.kinddeclares, reused and not re-spelled. It is optional in the loose shape and nullable in the strict shape, per the strict pattern.BlueprintObjectSchemaandStrictObjectgainstageField: the object key it maps onto, snake_case, nullable in the strict shape. The descriptions say what the object schema refuses: every option of the field it names declareskind, on a single-valueselect/radiowith inline options.- The strict/loose key-set parity test (
solution-blueprint.test.ts) covers both keys; the published strict JSON schema regenerates. Clause-②: yes (widening); aminorchangeset for@objectstack/spec. The blueprint schema's only producer and consumer is cloud'spropose_blueprint/apply_blueprint(as measured for spec(ai):BlueprintViewSchemaandStrictViewdeclarefilters(ViewFilterRule[]) andsort, so an AI-built view carries its own filter (ruling A on cloud#2932) #24129).
The spec seat settles whether stageField rides this card. Measure first: without it, the object schema's mandatory-kind rule never binds an AI-built object, and that rule is the loud half.
Done when
- Both shapes declare both keys, the parity test is green, and the published strict JSON schema carries them.
- objectstack-ai/cloud#2926 can start once cloud's pin carries this card's merge. That route (spec first, then the pin) is the precedent of objectstack-ai/cloud#2150 and of spec(ai):
BlueprintViewSchemaandStrictViewdeclarefilters(ViewFilterRule[]) andsort, so an AI-built view carries its own filter (ruling A on cloud#2932) #24129 / PR feat(spec): BlueprintViewSchema and StrictView declare filters and sort, so an AI-built view carries its own filter #24152.
Why not a cloud-side extension first
The bounded exception of ruling A on objectstack-ai/cloud#2932 rested on a p1 defect users saw on staging. Here, nothing changes for a user until objectui's record path reads the option's kind: at objectui's pin ef71a09f9e5f, deriveStages (packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:391) maps { value, label } only. That reader is objectstack-ai/objectui's card, filed beside this one. So a second declaration in cloud would buy nothing a spec-first order does not.
Filed by the repo:cloud#2 seat (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX). ⛔ Not a claim. Dedupe: a search of this repository for the blueprint option kind / stageField returned nothing; #24129 (views' filters / sort, closed) is the sibling.
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: p1 ·
target:v18·area:ai·domain:spec·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T17:59ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/mainf963d8e2b3:packages/spec/src/ai/solution-blueprint.zod.tshas nokindon blueprint options and nostageFieldon a blueprint object (0 hits).target:v18: the protocol-first rule. It is a spec schema widening (Clause-②: yes,minor). The named producer and consumer is objectstack-ai/cloud#2926, whose route (spec first, then the pin) follows the spec(ai):BlueprintViewSchemaandStrictViewdeclarefilters(ViewFilterRule[]) andsort, so an AI-built view carries its own filter (ruling A on cloud#2932) #24129 / PR feat(spec): BlueprintViewSchema and StrictView declare filters and sort, so an AI-built view carries its own filter #24152 precedent.kindreusesSelectOption.kind's enum, ⛔ not re-spelled. WhetherstageFieldrides this card is thedomain:specseat's call after measuring, as the card says.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 2 · 2026-10-17T18:12Z · the AI blueprint's select options gain the lifecycle
kind(reusingSelectOption.kind's enum) and a blueprint object gainsstageField, in both the loose and the strict shapeSession:
session_01TyRhWbVbbQpQJTDbW8VahJ(the seat; the dev is anos-devsubagent of this session)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24198-blueprint-option-kind-stage-fieldWorktree:
objectstack-issue-24198Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface (read on
origin/mainf963d8e2b; line numbers move; stop on a breach and explain it in the report):packages/spec/src/ai/solution-blueprint.zod.ts:BlueprintFieldSchema.options(about:90),StrictField.options(about:340),BlueprintObjectSchema(about:100/:109) andStrictObject(about:331/:358).packages/spec/src/ai/solution-blueprint.test.ts(the strict/loose key-set parity test) and the published strict JSON schema / generated artifacts the build regenerates, by their generators only..changeset/24198-*.md(@objectstack/spec,minor).packages/spec/src/data/field.zod.tsandobject.zod.ts(read, not edited); cloud and objectui (objectstack-ai/cloud#2926, record path: a derived stage drops its option's declared lifecyclekind, so terminal stages are still guessed from spelling (deriveStagesmaps value and label only; spec 18 declareskind) objectui#12907).stageField(the card leaves it to this seat): it rides this card, on the premise thatObjectSchema.stageFieldis the only trigger ofrefuseStageFieldWithoutKinds(object.zod.ts, about:3160). If the dev measures another trigger, it reports the fork; it does not drop or widen silently.Container & model:
Streated asM(two keys across two parallel shapes, with describe text an AI model reads),mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate); review ceilingCONTRACT_REVIEW_TIER, reason:Clause-②: yeson a publishedpackages/spec/src/**schema (contract-review.md, 档随面定). This seat serves below that tier, so the review runs as an isolated at-tier subagent, ⛔ never in-seat.Clause-②: yes
Basis for the Clause-② line: two published schemas gain keys, which widens their accept sets.
Responsibility: n/a — not a defect card (a contract widening for a named producer, objectstack-ai/cloud#2926)
Thread-read: 6238179039
Ruling-ref: none — no maintainer ruling on this card. Its scope is triage's grade
6238179039.Serial constraints cleared:
solution-blueprint.*.os migrate metaand the upgrade guide #24179 (PR fix(spec): the step-18 upgrade rationale states the element data-binding ruling in words instead of a tracker id #24201,registry.ts.template), [finding] spec(migrations): the published entry page-template-closed-region-unplaced-refused says a stored page with an unplaced region or an unregistered template "renders as before"; objectui#12476 makes it render a refusal #24195 (this seat, a migration entry file), and the other seats' (lint: a locale pack'spicklists.<name>translations are never checked — an option key that is not in the list, or a list no package declares, passesos lint --strictand is dropped silently at runtime #23566, metadata-protocol: duplicatePackage renames a token that names both a prefixed package object and one of its fields as the object, so a copied view's column or form field names a field the copy does not have (family close-out) #23561, refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204). None names this file.solution-blueprint.zod.tswas last touched today (9f024d8a2, the spec(ai):BlueprintViewSchemaandStrictViewdeclarefilters(ViewFilterRule[]) andsort, so an AI-built view carries its own filter (ruling A on cloud#2932) #24129 sibling).objectstack-fleet commentedon Oct 17, 2026
os-dev-report
{ "issue": 24198, "status": "done", "branch": "claude/issue-24198-blueprint-option-kind-stage-field", "pr": "https://github.com/objectstack-ai/objectstack/pull/24213", "session": "session_01TyRhWbVbbQpQJTDbW8VahJ — the parent seat's id (this run is its os-dev subagent; the relay derived the same id from the container)", "premise_still_valid": true, "summary": "Both blueprint option shapes (lenient BlueprintFieldSchema.options[] optional, strict mirror required+nullable) gain `kind`, which is SelectOption.kind's own enum taken by identity (`SelectOptionSchema.shape.kind.unwrap()`, never re-spelled, field.zod.ts not edited). Both object shapes gain `stageField` beside `nameField` (lenient optional SNAKE_CASE, strict strictIdentOrNull). One shared describe sentence, written from refuseStageFieldWithoutKinds, states what the object schema refuses. The seat's stageField premise held: ObjectSchema.stageField is the only trigger of refuseStageFieldWithoutKinds (one definition object.zod.ts:1970, one call :3160; no other stageField declaration in packages/spec/src; the lint/metadata readers only resolve the pointer), so stageField rides the card. H1-H5 measured true: before the change there were 0 stageField hits and no `kind` key (control: nameField at :109/:358); there are 0 code consumers outside packages/spec (5 non-code hits: a changeset, the auto-generated reference page, ADR-0033; control: 70+11 hits inside). Draft PR #24213 opened as objectstack-fleet[bot], assignee os-project-manager, body read back byte-identical (8829/8829).", "tests": "At 5645c2cd9 (the merge of origin/main f7af83945): `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` gave 691 files, 20974 passed, 1 todo, exit 0. `pnpm --filter @objectstack/spec typecheck` exited 0 (the test-layer program compiles src/**/*.test.ts). `pnpm --filter @objectstack/spec build && check:generated`: all 14 artifacts up to date, exit 0. solution-blueprint.test.ts has 72 tests. New: a lenient carry/optional/identity/vocabulary/snake_case block, an ObjectSchema contract leg (a 1:1 copy parses; dropping one kind is refused at exactly fields.status.options.3.kind, custom), a strict carry/null/required-at-own-path/vocabulary/snake_case block, and an option-level key-parity pin. The object pin now names stageField. Existing strict fixtures gained stageField:null and kind:null, so the leading-digit pin still fails for the digit only. Ablation through scripts/ablation-replace.mjs in wrap mode (anchor x1 to x0, blob changed, restore proven blob==HEAD and git diff HEAD empty; the test reads src, so there is no dist step): strict stageField renamed to stageFieldAblated gave 20 failed / 52 passed (incl. 'the OBJECT schemas carry exactly the same keys'); lenient option kind renamed to kindAblated gave 5 failed / 67 passed (option key parity, carry, identity, vocabulary, ObjectSchema contract). Both went red, the predicted direction.", "gates": "Local, at 5645c2cd9: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 108 commands from merge-base f792290df, 5 paths. All were run sequentially, each exit captured before any pipe. `--ran` printed 'Run reconciliation — 108 derived, 107 run, 1 NOT-MEASURED, 0 UNRUN' and '✓ dispatch-gates --ran: 108 derived famil(ies) accounted for', RAN_EXIT=0. NOT MEASURED: check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET, it needs a whole-workspace pnpm build (declared narrowing; CI Lint & Repo Gates runs it). Six first readings (check:doc-formula-expressions 3, check:doc-security-posture 3, spec check:generated 1, check:skill-examples 3, check:lean-entry-closure 3, plus the dual-build one) were prerequisite refusals or a dist read mid-rebuild by my own concurrent turbo build of lint. After building lint/formula/objectql, all 13 gates in the overlap window were re-run, each exit 0. CI on PR #24213 head 5645c2cd9 at report time: 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, every other check-run in_progress (Check Changeset, Governed Surface Queue Guard, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, the Type Check lanes, Spec property liveness, The card this PR closes must claim this branch, …). Test Core and Build Core had not been listed yet: in_progress, not waited on.", "line_budget": "n/a", "files_changed": [ "packages/spec/src/ai/solution-blueprint.zod.ts", "packages/spec/src/ai/solution-blueprint.test.ts", "packages/spec/authorable-surface/ai.json (generated: +ai/BlueprintObject:stageField)", "content/docs/references/ai/solution-blueprint.mdx (generated: gen:docs)", ".changeset/24198-blueprint-option-kind-stage-field.md (@objectstack/spec minor)" ], "deviations": [ "Clause-② spelling: the card's body says 'Clause-②: yes (widening)', but the claim carries 'Clause-②: yes'. The PR body and the changeset both carry the claim's line verbatim, 'Clause-②: yes'; no arm was added on my own.", "check:dual-build-cjs-loads not measured locally (needs a whole-workspace pnpm build); left to CI.", "test:repo not run locally. The derived list did not name it, and the diff edits no repo-project test. scripts/solution-blueprint-header-row.test.ts reads solution-blueprint.zod.ts's header. The header block is unchanged (the new docblocks sit after SNAKE_CASE), and check:skill-refs, which republishes that row, passed. CI Test Core runs it.", "My own `turbo run build --filter=@objectstack/lint`, started to clear two prerequisite refusals, rebuilt packages/spec/dist while gates 055-063 ran. The 13 gates in that window were re-run after the build, all exit 0.", "The first attempt of ablation leg 2 did nothing: the log redirect named a nonexistent directory, so bash refused before anything ran (git diff HEAD stayed 0 bytes). It was re-run with the correct path, and that run is the one cited.", "origin/main moved 6 commits after the pre-PR merge (rest, service-settings, cloud-connection, metadata-protocol, spec api/protocol + liveness). Those paths do not overlap this diff, so no second merge (§10: merge only on a conflict, a main-caused red or a queue removal).", "4 git pushes (empty-branch probe, implementation commit, generated-artifacts+changeset commit, merge of origin/main). These are the push leg of the write budget, per the push-each-step rule." ], "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs (each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]). Stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24213/assignees (os-project-manager), run 39062176125 success. Stroke 2: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/24198/comments. No labels were written (no skip-changeset; a changeset ships).", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed — the blueprint carries only the string form of stageField. ObjectSchema.stageField also takes `false` (status field declared non-linear, stage heuristics suppressed). The card scopes a snake_case name, and no producer or consumer of the blueprint `false` form is named. Recorded in the PR's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
ACCEPT → PR #24213 comment 6240066851 · contract review PASS
6239776296· ready and auto-merge armed 2026-10-17T20:02Z ·domain:specseat 1 · sessionsession_01TyRhWbVbbQpQJTDbW8VahJGenerated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Landed · PR #24213 →
6ec0e12228(Fixes #24198, blueprint select options declare their lifecyclekindand a blueprint object names itsstageField) ·domain:specseat 1 ·os-project-manager· sessionsession_01TyRhWbVbbQpQJTDbW8VahJ· 2026-10-17T20:37Z6ec0e12228on parentb7f29ddfeorigin/maingit merge-base --is-ancestor 6ec0e12228 origin/main→ exit 0 (control: the PR head5645c2cd9f→ exit 1, a squash merge)git diff b7f29ddfe 6ec0e12228and the reviewed head5645c2cd9fover its merge basef7af83945sharegit patch-id --stablec330dc2e(ACCEPT6240066851, contract review PASS6239776296)gh-readonly-queue/main/pr-24213-*ref remainsFixes #24198is the squash message's only closing keyword. Its other references are objectstack-ai/objectui#12907 and objectstack-ai/cloud#2926, and neither is a closing keyword. This card closedcompletedby that PR alonepm:dispatchedis cleared in this act.Downstream: objectstack-ai/cloud#2926 (
apply_blueprint) can start once cloud's pin carries6ec0e12228. objectstack-ai/objectui#12907 is the renderer half.Generated by Claude Code