Repository navigation
feat(spec/ai): blueprint select options declare their lifecycle kind and a blueprint object names its stageField - #24213
Conversation
…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>
…ueprint-option-kind-stage-field
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: Inputs: card #24198 (body; comments 6238179039 triage, 6238356000 dispatch claim, 6239509257 os-dev report), PR #24213 (body, file list, net diff against ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
③ Boundary flags
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: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · PR #24213 @
|
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'sstageFieldnames (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):SolutionBlueprintSchema, whatapply_blueprintparses)SolutionBlueprintStrictSchema, the structured-output contract)fields[].options[].kindobjects[].stageFieldSNAKE_CASESNAKE_CASE(strictIdentOrNull, the same asnameField)kindreusesSelectOption.kind's enum by identity. One shared reference,optionKindEnum(), readsSelectOptionSchema.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.tsis not edited, andSelectOption's accept set does not change. It is a function, not a module-level constant, so the lenient lazy shapes do not forceSelectOptionSchemaat import.stageFieldsits next tonameFieldin both object shapes and maps ontoObjectSchema.stageField. Only the string form is carried. The object schema'sfalse("no linear lifecycle") has no blueprint spelling (see Acceptance notes).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-valueselectorradiowhose inline options each declarekind, and that naming a multiselect / checkboxes / tags field that carries options is refused too. The option describes add theFieldSchemahalf:kindonly goes on a single-valueselect/radiofield's options.packages/spec/authorable-surface/ai.json(+ai/BlueprintObject:stageField) andcontent/docs/references/ai/solution-blueprint.mdx. The publishedjson-schema/ai/SolutionBlueprintStrict.jsonis gitignored and built bygen:schema. After the build it has both keys, and every object node'srequiredstill equals itspropertieswithadditionalProperties: false(checked by walking the built file)..changeset/24198-blueprint-option-kind-stage-field.md:@objectstack/specminor.Premise checks (on
origin/mainf792290df, this branch's base)grep -n "kind\|stageField\|nameField"oversolution-blueprint.zod.tsfoundstageField0 times and nokindkey. The onlykindhits were two describe strings that say "View kind". Control:nameFieldhit at:109and:358.ObjectSchema.stageFieldis the only trigger of the mandatory-kind rule.git grep refuseStageFieldWithoutKindsgives one definition (object.zod.ts:1970), one call (:3160, withobject.stageField), one docblock mention and the liveness note. NostageFieldis declared anywhere inpackages/spec/srcother thanobject.zod.ts:2897. The non-spec readers ofstageField(lintvalidate-semantic-roles,validate-object-field-refs, metadata-core FLS references, metadata-protocol reference sites) check that the pointer resolves. None of them requireskind. SostageFieldrides this card, as the seat's ruling said it would if this premise held.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 insolution-blueprint.test.tsand 11 times insolution-blueprint.zod.ts. The consumer is cloud'spropose_blueprint/apply_blueprint(objectstack-ai/cloud#2926).Tests
packages/spec/src/ai/solution-blueprint.test.ts, 72 tests:kindisSelectOptionSchema.shape.kind.unwrap()itself, on both sides (identity, not shape); akindoutside the vocabulary is refused atobjects.0.fields.1.options.2.kind; a non-snake_casestageFieldis refused atobjects.0.stageField. A contract leg copies a parsed blueprint object 1:1 intoObjectSchema. It parses, and once one option drops itskind, the object schema refuses it at exactlyfields.status.options.3.kind(custom).kindand a non-snake_casestageFieldare refused.['kind', 'label', 'value']). The object-level pin now namesstageField.stageField: nulland, 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 oforigin/mainf7af83945):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 compilessrc/**/*.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.mjsin wrap mode. Each anchor hit once, the blob changed, and the restore was proven (blob equals HEAD,git diff HEADempty). The test readssrcdirectly, so no build or dist step is involved.stageFieldrenamed tostageFieldAblated: 20 failed / 52 passed, includingthe OBJECT schemas carry exactly the same keys.kindrenamed tokindAblated: 5 failed / 67 passed. The failures are the option key-parity pin, the carry test, the identity test, the vocabulary refusal and theObjectSchemacontract leg.The direction was red both times, as predicted.
Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 108 commands for this diff, and all were run at5645c2cd9.--ranreconciliation: 108 derived, 107 run, 1 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loads. Family: dual-build CJS loads. Reason: exit 3, PREREQUISITE NOT MET, because it needs a whole-workspacepnpm build. This is a declared narrowing, and CI'sLint & Repo Gatesruns it.turbobuild of@objectstack/lintrebuiltpackages/spec/distwhile 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, speccheck: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-testsandcheck:lean-entry-closure.test:repo, the workspace and consumer type-check lanes, Test Core, Build Core and dogfood.Acceptance notes
stageField: falsehas no blueprint spelling.ObjectSchema.stageFieldalso takesfalse, which declares a status field non-linear and turns off stage heuristics. The card scopesstageFieldas a snake_case name, and no producer or consumer offalsefrom the blueprint is named, so only the string form is carried. Owner: none. Noted, not filed.stageFieldnames has a non-nullkind"). The refusal arrives, located, when the applier writes the object throughObjectSchema(pinned by the contract leg above). The describes tell the model the rule up front.apply_blueprintcopiesobjects[].stageFieldonto the object andoptions[].kindonto 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 lifecyclekind, so terminal stages are still guessed from spelling (deriveStagesmaps value and label only; spec 18 declareskind) objectui#12907.Generated by Claude Code