Skip to content

feat(spec): a select option declares its lifecycle kind (open / won / lost); every stage-field option must declare it - #23846

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23809-select-option-kind
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23809-select-option-kind

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23809
Clause-②: yes (narrowing)

What

A select option declares its lifecycle kind, one closed vocabulary for every object: open (a step on the way), won (the successful end), lost (the unsuccessful end). Every option of the field an object's stageField names must declare it.

  • packages/spec/src/data/field.zod.ts: SelectOptionSchema gains kind: z.enum(['open', 'won', 'lost']).optional(), documented with the definition (a task's Done and a case's Resolved are won; Cancelled / Unqualified / Closed Lost are lost). Absent stays absent at parse (reads as open; no default is written in). FieldSchema refuses kind at options.N.kind on any field that is not a single-value select / radio. Exported type SelectOptionKind. The dueLike / settledWhen docblock now says options carry a kind but overdue is still judged by settledWhen alone (objectui#11815 ruling D unchanged; nothing is wired).
  • packages/spec/src/data/object.zod.ts: refuseStageFieldWithoutKinds, an ObjectSchema parse-time check beside refuseDishonestReferenceDeletedWhen. One located issue per option without kind at fields.FIELD.options.N.kind, message naming the field and the option. Also refused at stageField: a picklist-backed stage field, and a stage field that has options but holds no single option code per record (multiselect, multiple: true). Left as they were: stageField: false, a pointer to no declared field (lint semantic-roles owns it), a field with no options. The stageField describe states exactly this scope.
  • packages/spec/src/ui/view.zod.ts: FormSelectOptionSchema is derived from the option shape, so it now leaves out kind as well as default, with a guidance line pointing at the object field. Without that, kind would have been accepted on form-view options with nothing reading it.
  • Forms: the object designer's options repeater offers kind (a select of the three values); the field form's repeater lists it. The platform-objects metadata-form bundles carry the new row, translated into zh-CN / ja-JP / es-ES.
  • Liveness: field/options is now drilled. Six children are live with their read points, and kind is planned (no authorWarn, because the key is mandatory under stageField). The field/options row leaves undrilled-containers.baseline.json. picklist/options.kind is planned.
  • ADR-0087: the D3 entry stage-field-option-kind-required. It has no conversion because the ruling withholds a default.
  • examples/app-showcase/.../semantic-zoo.object.ts: the only stageField: 'status' in the corpus now declares open / open / won.
  • packages/metadata-core/src/object-schema-fls-contract.ts: the published contract fixture FLS_CONTRACT_OBJECT names salary_grade as its stageField, so its two options declare kind: 'open'. It is pinned in object-schema-fls-references.test.ts, which asserts no stage-field issue and has a positive control.
  • platform-objects catalog count pins follow the new kind rows: field-panel-echo-decisions 10 to 11, object-collapsed-sections-echo-decisions 116 to 118, object-lifecycle-panel-echo-decisions 672 to 674.
  • Changeset .changeset/23809-select-option-kind.md: @objectstack/spec minor, @objectstack/platform-objects patch (metadata-form bundles), @objectstack/metadata-core patch (contract fixture), Clause-②: yes (narrowing), with the migration.

Vocabulary ruling: premise reading

The seat ruled open | won | lost on the premise that record:path stages[].terminal: z.enum(['won', 'lost']) is a live platform vocabulary. Measured at objectui main @63d00da: packages/plugin-detail/src/renderers/record-path.tsx declares PathStage.terminal?: 'won' | 'lost', and its classify returns s.terminal first (line 221), ahead of the token heuristic. The premise holds. component.zod.ts is untouched.

Acceptance pins (packages/spec/src/data/select-option-kind.test.ts)

  1. An option with a terminal kind parses: an option with a terminal kind parses, and keeps it.
  2. An unknown kind is refused: an unknown kind is refused at the option, located (done, cancelled, closed, WON, empty).
  3. A plain select without kinds parses and reads as open: a plain select without kinds parses, and an absent kind stays absent (reads as open).
  4. A stage field with one option lacking kind is refused, naming the field and option: one option lacking kind is refused at publish, naming the field and the option (path fields.status.options.3.kind), plus each option lacking kind is its own located issue, create() refuses the same object.
  5. kind on a non-select field is refused: is refused on a \%s` fieldover multiselect, checkboxes, tags,multiple: true` select/radio, lookup, text.

Updated pins: field-rows-option-description.test.ts (the repeater's input list gains kind), form-select-option.test.ts (Omit minus default and kind; the kind refusal names the object field).

Verification (round 1 at head 4274c65; patch round at head 4475469)

  • @objectstack/spec full vitest: 21996 passed, 2 failed. Both failures were the two pins above, updated afterwards. The re-run of those files, the new file and src/migrations: 279/279. pnpm typecheck exit 0. check:generated: all 14 artifacts up to date.
  • Ablation (scripts/ablation-replace.mjs, mutation proved on disk, restored blob == HEAD with an empty git diff HEAD): deleting the refuseStageFieldWithoutKinds(...) call turns 6 stage pins red. Turning the FieldSchema off-type guard into if (false) turns 7 off-type pins red. Both are green again after restore.
  • Downstream: rest FLS / save-gate suites 125/125, runtime meta-object-fls 85/85, metadata-core object-schema-fls-references 53/53. Showcase validate, build and typecheck all exit 0.
  • Patch round, head 4475469 (contract review FAIL 6211666660, three items closed): spec check:generated all 14 up to date after --fix; select-option-kind + object.test 232/232. platform-objects 1092/1092 (3 count pins updated, as above); metadata-core 735/735 and typecheck exit 0; rest FLS 125/125; runtime meta-object-fls 85/85. dispatch-gates --commands: the derived list is byte-identical to round 1. 116 run, all exit 0 after the spec-dist prerequisite re-runs, with the same 2 NOT MEASURED left to CI.
  • dispatch-gates --commands (round 1): 116 families derived, 116 run. All exit 0 except: check:i18n exit 1, a real finding (bundles drifted), fixed by regenerating and translating, now 0. check:skill-examples, check:i18n, check:i18n-coverage re-run green after a dist rebuild. Two families are NOT MEASURED locally, exit 3 (prerequisite): check-plugin-teardown-shape --self-test (the shallow clone cannot reach its pinned fixture) and check:dual-build-cjs-loads (needs the whole workspace built). Both are left to CI.

Acceptance notes

  • Picklist-backed stage field: refused at stageField rather than judged, because ObjectSchema cannot see a picklist's options. Zero instances in the corpus. The alternative, judging the resolved list where os validate resolves picklist references, would cover only the CLI doors and not the save door. This is recorded in the report's open questions.
  • The object designer's options repeater is shown for multiselect / checkboxes too, and the form DSL has no parent-scoped row predicate. So kind is offered there and the publish door refuses it. The help text says "single-value select / radio only".
  • Readers follow in their own cards: the objectui record path / kanban, the Studio inspector column, and the HotCRM declarations.

🤖 Generated with Claude Code


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/platform-objects, @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json f7e836da1ed344aaa6936b9de8b173d0f326b9ec.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/data.json, …) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 142 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 f7e836da1ed344aaa6936b9de8b173d0f326b9ec → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json f7e836da1ed344aaa6936b9de8b173d0f326b9ec

⚠️ 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 f7e836da1ed344aaa6936b9de8b173d0f326b9ec → 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: 4274c652f7bfeff1c33f7f73ac97370f4f5f8d24
Local-runs: none

Inputs: card #23809 (body, the dispatch claim 6209728672 and the os-dev-report 6211151563), PR #23846 (body, its 29-file list, the net diff against main at 3fed1eb00af6520f1f26e774195b0819d94fb254), and the head's check-runs. Face: published schema (packages/spec/src/data/field.zod.ts, object.zod.ts, ui/view.zod.ts); no governed path in the file list (Governed Surface Queue Guard green); 558 / 27 lines.

① Derived judgments

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

  1. SelectOptionSchema gains kind: z.enum(['open', 'won', 'lost']).optional(), no default, absent stays absent at parse — RIGHT. Card Target 1 reads an absent kind as the open category (a reading, never a written default) and the maintainer's ruling withholds a default under stageField; pinned by a plain select without kinds parses, and an absent kind stays absent.
  2. The vocabulary open | won | lost — RIGHT. packages/spec/src/ui/component.zod.ts on main already ships record:path stages[].terminal: z.enum(['won', 'lost']), so the data layer and the page say one thing one way, no conversion. The card left the spelling to the spec seat. The dev's premise reading (the objectui renderer honours terminal first) is a measurement reported in the PR body, outside this record's inputs and not re-measured here; the spec-side half holds on main.
  3. FieldSchema refuses kind at options.N.kind on any field that is not a single-value select / radio (SINGLE_OPTION_TYPES on main is exactly select, radio; multiple: true excluded) — RIGHT. A lifecycle is one value per record; a key no renderer could read is refused, not parsed (ADR-0049). Not itself an accept-set narrowing: strictObject refused the unknown key before. Pinned over seven types.
  4. ObjectSchema refuses, at parse beside refuseDishonestReferenceDeletedWhen, every option of the stageField field that lacks kind, one located issue per option at fields.FIELD.options.N.kind naming the field and the option — RIGHT. This is the card's Target 1 second bullet verbatim and the accept-set NARROWING the Clause-② arm declares; reaches every door (create() pinned).
  5. ObjectSchema refuses at stageField a picklist-backed stage field, and a multi-value (multiselect, multiple: true) stage field that carries options — RIGHT, with a note: both go beyond the card's wording; both are declared in the changeset and in the D3 entry; zero corpus instances (git grep of stageField: string authors on main: the showcase semantic-zoo and the metadata-core fixture named under ③). The open questions are answered under ③.
  6. The three unchanged postures — stageField: false, a pointer to an undeclared field (lint semantic-roles owns it), a field with no option source — RIGHT as implemented and as pinned (leaves the existing postures alone).
  7. The stageField .describe() — WRONG. The new sentence reads: "The named field is a single-value select / radio with inline options, and every option declares its kind ('open' / 'won' / 'lost'); refused otherwise." The parse does not refuse "otherwise": a stageField naming a text field, or a select with neither options nor picklist, parses — item 6, which the function docblock calls "unchanged, on purpose" and the PR's own pin asserts (ObjectSchema.safeParse(lead({ type: 'text' })).success is true). That describe is the JSON-schema description every AI author reads, regenerated into content/docs/references/data/object.mdx in this very diff, and it advertises a refusal the runtime does not deliver — Prime Directive chore: version packages #10, "keep the claim as narrow as the enforcement". Fix: narrow the sentence to what refuseStageFieldWithoutKinds enforces (a stage field that carries options is refused unless it is a single-value select / radio with inline options each declaring kind; a picklist-backed one is refused; false, an unresolved pointer and an option-less field are left to lint and the heuristics), then pnpm --filter @objectstack/spec build and check:generated --fix so object.mdx and whatever else reports stale move with it.
  8. Exported type SelectOptionKind (api-surface and export-origins, one row each); no SelectOptionKindSchema export — RIGHT. The dev measured that a schema export rewrote five skills/**/references/_index.md files (Tier H governed, outside the card) and stopped; the type carries the three literals a consumer needs. A runtime-enum export, if a consumer ever needs one, is its own card.
  9. FormSelectOptionSchema (view.zod.ts) now omits kind as well as default, with guidance pointing at the object field — RIGHT. It is derived structurally from SelectOptionSchema.shape, so without the omit kind would have parsed on form-view options with no reader. Outside the claim's listed surface but forced by the derivation, and not a ⛔ path (component.zod.ts is untouched). Pinned in form-select-option.test.ts.
  10. PicklistSchema options accept kind (the option shape is reused verbatim on main) — RIGHT as a consequence; the picklist ledger row options.kind: planned records that nothing reads it and that no stage use exists (item 5 refuses it).
  11. Forms: the object.form.ts repeater offers kind as a three-value select with help text, field.form.ts lists it, and the en / zh-CN / ja-JP / es-ES metadata-form bundles are regenerated and translated — RIGHT. The repeater is also shown for multi-value types where the door refuses kind: loud at publish (a located issue) and the help text scopes it; acceptable as noted, not an authoring trap.
  12. Liveness: field/options drilled (six children live with read points, kind graded planned without authorWarn), the debt row leaves undrilled-containers.baseline.json, the state-counts move — RIGHT. planned is the honest grade: the refusals are live at parse, the value is read by nothing until the objectui follow-up (card Target 4; Constraints: the spec declares before any consumer reads). No authorWarn is right: the author-side lint would warn on every compliant stage field, where the key is mandatory. Spec property liveness is green.
  13. D3 entry 18.stage-field-option-kind-required.ts (surface, replacement, reason, acceptanceCriteria; no conversionIds) under the open protocol major 18 (PROTOCOL_VERSION 18.0.0) — RIGHT. No D2 conversion is correct: the ruling withholds a default, so no lossless mechanical edit exists, and ADR-0087 sends a non-convertible break to D3. The registry is generated and gitignored, nothing to commit; the step-18 rationale is a fragment list that no gate pairs to entries (123 fragments against 387 entries on main), so no fragment is owed.
  14. Showcase semantic-zoo.object.ts options declare open / open / won — RIGHT (the one stageField: 'status' in the corpus; SemanticZooLegacy is stageField: false, untouched by the rule).
  15. Reference docs (content/docs/references/**, six files) regenerated by gen:docs — a prose face outside this review; named only because item 7's fix moves object.mdx again.

Check-runs on the head, read last at 2026-10-16T08:41Z: 33 success, 5 skipped (the full-battery jobs a PR run does not carry), 2 still in progress — Test Core (3/6) and Test Core (4/6). Green: Lint & Repo Gates, Type Check · workspace / source gates / consumer gates / debt ledger, Build Core, Test Core (1/6, 2/6, 5/6, 6/6), Check Changeset, Spec property liveness, Governed Surface Queue Guard. The two shards are named as in progress rather than waited on; no landing before they conclude green.

② Semver level

  • .changeset/23809-select-option-kind.md: '@objectstack/spec': minor, summary feat(spec)!: …, Clause-②: yes (narrowing), a FROM → TO migration with the one-line fix, and the adr-0087 registered stage-field-option-kind-required marker — level, arm and disposition are RIGHT. yes: the option shape widens by a new authorable key, at least minor. (narrowing): ObjectSchema's accept set shrinks (items 4 and 5), BREAKING, and it carries its migration and its ADR-0087 marker as the Post-Task Checklist demands. minor, not major: the launch-window convention in scripts/check-changeset-no-major.mjs ships a breaking change as minor in the lockstep fixed group until GA, and .changeset/pre.json is in pre mode (tag next). The PR body's Clause-②: yes (narrowing) agrees with the changeset; the claim's bare Clause-②: yes predates the measured shape, and the arm is the dev's correct refinement (③).
  • Package set — WRONG. The diff publishes a changed @objectstack/platform-objects: the four src/apps/translations/*.metadata-forms.generated.ts bundles gain the fields.options.kind and options.kind rows, and they are reached from the published ./metadata-translations entry (src/metadata-translations/index.ts, a tsup entry, files: dist). The changeset names @objectstack/spec alone. "Add a changeset for anything that publishes" — and every one of the last six landings on main that moved en.metadata-forms.generated.ts names '@objectstack/platform-objects' (changesets 23808, 23735, 23543, 23488, 23395 and 15204-s8: patch or minor). The fixed group bumps it either way; what is missing is its CHANGELOG line, the text an upgrading agent greps. Fix: add '@objectstack/platform-objects': patch to the changeset frontmatter (the dev's own report lists the bundles under files_changed). Check Changeset is green on this head, so this is not something CI catches here.
  • Clause-②: yes (narrowing) — the published option shape widens and ObjectSchema's accept set narrows.

③ Boundary flags

The dev's deviations and open_questions on the os-dev-report, each answered or escalated:

  • OQ1, a picklist-backed stage field refused at parse (A) or judged on the resolved list (B): A, accepted. The parse is the one door every save passes; B would judge only the CLI doors and let the save door through unjudged (declared ≠ enforced). Zero corpus instances; widening to B later is non-breaking; the narrowing is declared in the changeset and the D3 entry (item 5). Not escalated.
  • OQ2, a multi-value stage field carrying options refused at stageField: A, accepted. A record stands at one stage; without it multiple: true would slip the mandatory-kind rule silently. Zero corpus instances; declared in the changeset and the D3 entry. Not escalated.
  • Files beyond the claim's surface (view.zod.ts and its pin, liveness/picklist.json, undrilled-containers.baseline.json, field-rows-option-description.test.ts, the platform-objects bundles, the D3 entry): each forced by a measurement the dev names; none touches a ⛔ path of the claim (component.zod.ts, skills/**, content/docs/releases/**, objectui and HotCRM are all untouched). Accepted; the platform-objects bundles are what ② charges to the changeset.
  • No SelectOptionKindSchema export: right to stop (item 8); the governed skills/** rewrite was outside the card.
  • The (narrowing) arm added where the claim read a bare yes: right (②).
  • A worktree created on the pre-existing branch, one vitest run outside os-verify-lock, no label-write: process flags outside the contract faces; noted, nothing to answer here.
  • out_of_scope_findings 1, the options repeater offering kind on multi-value types: accepted as noted (item 11); loud at publish, not a trap.
  • out_of_scope_findings 2 — escalated to the owning seat. FLS_CONTRACT_OBJECT in packages/metadata-core/src/object-schema-fls-contract.ts, exported from the published @objectstack/metadata-core/testing, declares stageField: 'salary_grade' over two kind-less select options. No door in its suites parses it through ObjectSchema (the dev's rest, runtime and metadata-core runs are green), so nothing is red — but a published contract fixture now fails ObjectSchema.parse under the spec it ships beside, a trap for the next consumer that feeds it to a parsing door. Two answers, either acceptable: add kind: 'open' to its two options on the push item 7 already demands (two lines outside the claim surface, so say so in the PR body), or file a card naming the trap. This record does not choose between them.
  • On main, the only stageField string authors outside tests are semantic-zoo and that fixture; lint's stageField: 'status' fixtures run validateSemanticRoles on raw stacks and never through ObjectSchema, so the in-progress Test Core shards carry no known red from this rule.

Implemented-by: claude/issue-23809-select-option-kind
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: FAIL

Two edits re-earn a PASS on a new head: ① item 7 (narrow the stageField describe and regenerate) and ② (add '@objectstack/platform-objects': patch); ③'s fixture escalation is the seat's choice of an in-PR edit or a card. Everything else on the three faces is right.


Generated by Claude Code

…angeset names platform-objects and metadata-core; FLS contract fixture declares kind

Claude-Session: https://claude.ai/code/session_017qmjVjz61scwQFLfspdBX6
Co-authored-by: Claude <noreply@anthropic.com>
…e-field kind rule; regenerate reference docs

Claude-Session: https://claude.ai/code/session_017qmjVjz61scwQFLfspdBX6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 447546949874cd7706decaac1833b07c21b2ecbc
Local-runs: none

DELTA record on contract review 6211666660 (FAIL at 4274c652f7bfeff1c33f7f73ac97370f4f5f8d24). Inputs: card #23809 (body, the dispatch claim 6209728672, the os-dev-reports 6211151563 and 6212681414), PR #23846 (body, its 34-file list, the docs-drift comment 6211145627, the prior record, and the net diff against main at 3fed1eb00af6520f1f26e774195b0819d94fb254, merge-base 1ecb08a1cc), and the head's check-runs. The delta git diff 4274c652 44754694 is three commits over 10 files, +63 / -14: the stageField TSDoc and .describe() in packages/spec/src/data/object.zod.ts (the function body of refuseStageFieldWithoutKinds is untouched), the changeset frontmatter and one new body paragraph, the metadata-core fixture and a new pin, three platform-objects count pins, and three regenerated reference pages. Face: published schema in the file list; no governed path (Governed Surface Queue Guard green on the head).

① Derived judgments

Items 1 to 6 and 8 to 15 of record 6211666660 stand unchanged: the delta does not touch field.zod.ts, ui/view.zod.ts, the two forms, the liveness ledgers, the D3 entry, the showcase object, or the refusal function's code. Judged on this head:

  • Item 7, prior FAIL ground (1) — CLOSED. The new stageField describe reads: "When the named field carries options, it must be a single-value select / radio with inline options, each declaring its kind ('open' / 'won' / 'lost'); a picklist-backed or multi-value stage field is refused. A name that resolves to no declared field, or a field with no options, is not judged at parse." Read clause by clause against refuseStageFieldWithoutKinds on the head: a non-string stageField or a name absent from fields returns without an issue; a type outside SINGLE_OPTION_TYPES (exactly select, radio on the head) or multiple: true is refused at stageField only when the field carries options or picklist, and returns otherwise (so stageField: 'a_text_field' still parses, as the PR's own pin asserts); a single-value field with picklist and no options is refused at stageField; an options array raises one located issue per option lacking kind. FieldSchema refuses options and picklist together ("a field takes its options from exactly one source"), so no coexistence edge lets a picklist-backed stage field through the options branch. The claim is now exactly as wide as the enforcement (Prime Directive chore: version packages #10). The [#23809] TSDoc above it says the same, and the describe is regenerated into content/docs/references/data/object.mdx, api/metadata.mdx and system/migration.mdx (two tables); Type Check · source gates, the job that runs check:authorable-surface and check:docs, is green on the head.
  • Item 16 (new): FLS_CONTRACT_OBJECT in packages/metadata-core/src/object-schema-fls-contract.ts declares kind: 'open' on both salary_grade options — RIGHT. The fixture is published: src/testing.ts re-exports the module and package.json serves ./testing from dist. open on both is the right reading (a salary band is a step, never an end). It is pinned in object-schema-fls-references.test.ts by a block that collects ObjectSchema.safeParse issues at stageField or under fields.salary_grade.options, asserts none, and has a positive control that strips one kind and asserts exactly fields.salary_grade.options.1.kind. The pin's scope is honest and necessary: the fixture is deliberately not a whole-object valid document — its four script actions carry neither body nor target nor operation: 'update', which ActionSchema refuses at actions.N.body ("A 'script' action requires either an inline body ... or a target"), and the pin's comment says so. Prior ③ escalation closed by the in-PR edit.
  • Item 17 (new): the three platform-objects count pins — RIGHT as read against the bundles. field-panel-echo-decisions ROW_PROPERTIES 10 to 11 is the field form's options.kind row; object-collapsed-sections-echo-decisions OPEN_LEAVES 116 to 118 is the object form's fields.options.kind label plus its help text; object-lifecycle-panel-echo-decisions 672 to 674 per locale is one row label on each form. All four bundles (en, zh-CN, ja-JP, es-ES) carry both keys with the same shape. Test-side pins only; the dev names Test Core (3/6) as the shard that was red on the intermediate head ec9f775f for exactly these three counts, and that shard judges them on this head (below).

Check-runs on the head, read last at 2026-10-16T09:59Z: 37 success, 7 skipped (the full-battery jobs a PR run does not carry, plus the Auto Label / Check PR Size duplicates of the 09:54Z re-trigger), 3 still in progress: Test Core (3/6), Test Core (4/6), Test Core (6/6). Green: Lint & Repo Gates and every Lint · job, Type Check · workspace / source gates / consumer gates / debt ledger, Build Core, Build Docs, Test Core (1/6, 2/6, 5/6), Check Changeset (both runs), Spec property liveness, Governed Surface Queue Guard, the four card-claim guards. The three shards are named as in progress rather than waited on; no landing before they conclude green, and (3/6) is the one that carries the count pins.

② Semver level

  • Level, arm and disposition unchanged from the prior record and still RIGHT: '@objectstack/spec': minor, feat(spec)!: summary, Clause-②: yes (narrowing), the FROM → TO migration with the one-line fix, the adr-0087: registered stage-field-option-kind-required marker; .changeset/pre.json in pre mode, the launch-window minor for a breaking change in the fixed group. Check Changeset green twice on the head. The PR body's Clause-②: yes (narrowing) agrees.
  • Package set, prior FAIL ground (2) — CLOSED. The frontmatter now carries '@objectstack/platform-objects': patch (the ./metadata-translations bundles gain the two kind rows) and '@objectstack/metadata-core': patch (the ./testing fixture), each with its own sentence in the body. patch is right for both: a bundle row and a fixture value, no API widening; the fixed group bumps them either way and what was missing was the CHANGELOG line.
  • One clause of the new metadata-core sentence is WRONG, and it is the reason for the verdict. The body reads: "The published contract fixture FLS_CONTRACT_OBJECT (@objectstack/metadata-core/testing) names salary_grade as its stageField, so its two options now declare kind: 'open' and the fixture still parses through ObjectSchema." It does not, and it never did: ActionSchema refuses each of the fixture's four script actions at actions.N.body (no body, no target, no operation: 'update'), so ObjectSchema.safeParse(FLS_CONTRACT_OBJECT).success is false on this head. The dev measured exactly that (report 6212681414: "its script actions carry no body/target, measured: actions.0.body / actions.1.body issues") and scoped the pin to the stage-field rule for that reason; the pin's own comment says "The fixture is not a whole spec-valid object". The changeset body is the text changeset version compiles into packages/metadata-core/CHANGELOG.md, shipped inside the npm tarball as what an upgrading agent greps (AGENTS.md, Post-Task Checklist step 3 and the Documentation Guardrails row for packages/*/CHANGELOG.md: a factual error in a released entry is correctable only in a later docs-only PR the reader never reaches). This sentence tells that agent the fixture is safe to feed to a parsing door, which is the trap record 6211666660 escalated under ③, now advertised as absent. Same class as prior ground (1): a claim wider than what the runtime delivers (Prime Directive chore: version packages #10). Fix, one line and no regeneration: replace "and the fixture still parses through ObjectSchema" with what the pin asserts, for example "and ObjectSchema raises no stage-field issue on it (the fixture is not a whole spec-valid object by design: its script actions carry no body or target)", or delete the clause.
  • Clause-②: yes (narrowing) — the published option shape widens and ObjectSchema's accept set narrows; unchanged by the delta.

③ Boundary flags

The round-2 os-dev-report 6212681414 carries open_questions: [] and out_of_scope_findings: []; its deviations, each answered:

  • Round-1 miss (the platform-objects suite was not run after its bundles moved, so the Test Core (3/6) red on ec9f775f went unreported): closed by the three count pins (item 17); the head's Test Core (3/6) is the measurement, in progress at the time of this record. Noted, nothing further owed.
  • The metadata-core pin scoped to the stage-field rule rather than a whole-object parse: accepted as the right scope for a fixture that is deliberately not whole-object valid (item 16), with the one consequence ② names: the changeset must say the same thing the pin does.
  • PR body not edited, pr_body_patch supplied: the body on this head still says the changeset names @objectstack/spec alone and, under Acceptance notes, that FLS_CONTRACT_OBJECT "is left as it is"; both are false on this head. A body edit moves no head, so this is not a FAIL ground; the owning seat applies the dev's six-item patch (its text is accurate as read against the diff) before landing, and the next record need not re-judge it.
  • The prior record's answers to OQ1 (picklist-backed stage field refused at parse: A) and OQ2 (multi-value stage field with options refused: A) stand; nothing in the delta reopens them, and the narrowed describe now states both refusals and the three unjudged postures exactly.
  • The prior ③ escalation on the fixture: resolved by the in-PR edit (item 16), declared in the changeset and in the pr_body_patch.
  • Files beyond the claim's surface in the delta (the metadata-core fixture and its pin, three platform-objects test files): each forced by a measurement the dev names; none is a ⛔ path of the claim (component.zod.ts, skills/**, content/docs/releases/**, objectui and HotCRM all untouched).

Implemented-by: claude/issue-23809-select-option-kind
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: FAIL

All three grounds of record 6211666660 are closed on this head, and the delta breaks nothing the prior record judged right. One edit re-earns a PASS on a new head: the metadata-core sentence in .changeset/23809-select-option-kind.md must claim only what the pin asserts (②). The PR body patch is the seat's, outside the head.

…sserts, not a whole-object parse

Claude-Session: https://claude.ai/code/session_017qmjVjz61scwQFLfspdBX6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ef94551925f2cdf22a074003509ee81e42efd6ab
Local-runs: none

DELTA record 2 on contract review 6213027069 (FAIL at 447546949874cd7706decaac1833b07c21b2ecbc), whose only open ground was ②: the metadata-core sentence of the changeset. Inputs: card #23809 (body, the dispatch claim 6209728672, the os-dev-reports 6211151563, 6212681414 and 6213094599), PR #23846 (body, its 34-file list, the docs-drift comment 6211145627, the records 6211666660 and 6213027069, no inline review comments and no reviews, and the net diff against main at 3fed1eb00af6520f1f26e774195b0819d94fb254, merge-base 1ecb08a1ccb8f574ec4df8d4a65e1354ca55596c: 34 files, +611 / -31), and the head's check-runs. The delta git diff 4475469498 ef94551925 is ONE commit (ef94551925 docs(changeset): the metadata-core line states what the fixture pin asserts, not a whole-object parse) over ONE file, .changeset/23809-select-option-kind.md, +1 / -1: the @objectstack/metadata-core bullet in the body, and nothing else. No source, test, fixture, form, ledger or generated file moves. Face: published schema in the file list; no governed path (Governed Surface Queue Guard green on the head); head repo is the base repo, so not a fork proposal.

① Derived judgments

Every item of records 6211666660 (1 to 6, 8 to 15) and 6213027069 (7, 16, 17) stands unchanged: the delta touches none of the files they judged. Judged on this head:

  • Item 18 (new): the rewritten metadata-core sentence — RIGHT, clause by clause. It now reads: "The published contract fixture FLS_CONTRACT_OBJECT (@objectstack/metadata-core/testing) names salary_grade as its stageField, so its two options now declare kind: 'open', and the fixture meets the stage-field rule: ObjectSchema raises no issue at stageField or under fields.salary_grade.options. The fixture is not otherwise a spec-valid object (its script actions carry no body or target), and this change leaves that as it was."
    • Published: packages/metadata-core/src/testing.ts:13 re-exports ./object-schema-fls-contract.js; package.json serves ./testing from dist/testing.js with files: ['dist', ...]. Unchanged from item 16.
    • Names salary_grade as its stageField: the fixture declares stageField: 'salary_grade'; its two options are { value: 'band_a', label: 'Band A', kind: 'open' } and { value: 'band_b', label: 'Band B', kind: 'open' }. The fixture's net diff against main is exactly that options line plus three comment lines above it.
    • "ObjectSchema raises no issue at stageField or under fields.salary_grade.options" is verbatim what the pin asserts: object-schema-fls-references.test.ts collects ObjectSchema.safeParse issues whose path is stageField or starts with fields.salary_grade.options and expects []. Read against refuseStageFieldWithoutKinds on the head (object.zod.ts:1970, wired into ObjectSchema's .superRefine beside refuseDishonestReferenceDeletedWhen): stageField is a string, fields owns salary_grade, its type select is in SINGLE_OPTION_TYPES (field-value.zod.ts:141: exactly select, radio), multiple is absent, options is an array, and every option's kind is defined, so the function returns with no issue; SelectOptionSchema accepts open, and FieldSchema's off-type guard does not fire on a single-value select, so no issue lands under fields.salary_grade.options from either schema. The positive control (strip options[1].kind, expect exactly fields.salary_grade.options.1.kind) is what makes the assertion non-vacuous: it shows the object-level refinement does run on this fixture despite the action issues below.
    • "not otherwise a spec-valid object (its script actions carry no body or target)": all four fixture actions (regrade, rename, invite, escalate) are type: 'script' with no body, no target and no operation: 'update'; ActionSchema (packages/spec/src/ui/action.zod.ts:1916, the schema ObjectSchema.actions is an array of) refuses exactly that shape at path ['body'] ("A 'script' action requires either an inline body ... or a target"), so each lands at actions.N.body. The sentence names a true cause and claims it as nothing more; the pin's comment says the same.
    • "this change leaves that as it was": the actions block is byte-identical on the merge-base; the net diff on the fixture is confined to the salary_grade options.
    • The false clause ("the fixture still parses through ObjectSchema") is gone. The claim is now exactly as wide as the pin and the runtime (Prime Directive chore: version packages #10), which is what the prior record's ② asked for.

Check-runs on the head, read last at 2026-10-16T10:34Z: 34 success, 5 skipped (Dogfood Verify CLI, Temporal Conformance, the Dogfood Regression Gate matrix leg, Console Pin Gate, Packed-tarball smoke: the opt-in and full-battery jobs a PR run does not carry), 1 still in progress: Test Core (4/6) (started 10:05:57Z; the five finished shards took 16 to 29 minutes). Green: Lint & Repo Gates and every Lint · job (ESLint, repo & tooling gates, docs & code guards, scoped · tooling self-tests, CI, release & engine gates, scoped · query-options erasure, scoped · slot lookup & corpus, PM dispatch-gates), Type Check · workspace / source gates / consumer gates / debt ledger, TypeScript Type Check, Build Core, Build Docs, Test Core (1/6, 2/6, 3/6, 5/6, 6/6), Check Changeset (the gate that reads the one changed file; green at 10:06Z), Spec property liveness, Governed Surface Queue Guard, the four card-claim guards, Check PR Size, Auto Label, Flag docs affected by code changes, Check Documentation Links, Dogfood Regression Gate, filter. The bare Test Core gate context (ci.yml, the job that carries the branch-protection name after the matrix) has not been published yet and follows the last shard. The delta moves no file any Test Core shard reads, so the shard judges the same test tree the prior head's shards did; it is named as in progress rather than waited on, and no landing before it and the bare Test Core context conclude green.

② Semver level

  • Level, arm and disposition unchanged by the delta and still RIGHT: frontmatter '@objectstack/spec': minor, '@objectstack/platform-objects': patch, '@objectstack/metadata-core': patch; feat(spec)!: summary; Clause-②: yes (narrowing); the FROM → TO migration with the one-line fix; the adr-0087: registered stage-field-option-kind-required marker. .changeset/pre.json in pre mode, the launch-window minor for a breaking change in the fixed group. Check Changeset green on the head. The PR body's Clause-②: yes (narrowing) agrees with the changeset.
  • Prior FAIL ground ② — CLOSED. The metadata-core sentence, the text changeset version compiles into packages/metadata-core/CHANGELOG.md and ships in the tarball, now claims only what the pin asserts and what the runtime delivers (item 18); it no longer tells an upgrading agent the fixture is safe to feed to a parsing door. patch for @objectstack/metadata-core stays right: a fixture value, no API widening.
  • The changeset matches what the diff publishes: the three packages whose published outputs move (@objectstack/spec schemas, forms, ledgers and types; @objectstack/platform-objects ./metadata-translations bundles; @objectstack/metadata-core ./testing fixture) are the three it names, unchanged from the prior record's reading of the same 34-file list.
  • Clause-②: yes (narrowing) — the published option shape widens and ObjectSchema's accept set narrows; unchanged by the delta.

③ Boundary flags

The round-3 os-dev-report 6213094599 carries deviations: [], open_questions: [] and out_of_scope_findings: []; files_changed names the one changeset and the delta agrees. Judged:

  • No test run this round: right for a prose-only head; the pin the sentence now describes is unchanged since 4475469498, and the head's check-runs are the measurement. The dev's local changeset-family exit codes are its claim, not re-measured here; Check Changeset on the head is the gate verdict.
  • The prior record's ③ note on the stale PR body is closed on this head: the body now names the three changeset packages, carries the FLS_CONTRACT_OBJECT bullet and the platform-objects count-pin bullet, heads Verification with both earlier heads, and no longer says the fixture "is left as it is" under Acceptance notes. The body's Verification section does not name ef94551925; nothing is owed for a head that changes one prose line.
  • The prior records' answers to OQ1 (picklist-backed stage field refused at parse) and OQ2 (multi-value stage field with options refused) stand; nothing in the delta reopens them.
  • No ⛔ path of the claim is touched on this head (component.zod.ts, skills/**, content/docs/releases/**, objectui and HotCRM all untouched); no governed path in the file list; the PR is still a draft with auto-merge off, as it should be until the two remaining contexts conclude.

Implemented-by: claude/issue-23809-select-option-kind
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: PASS

The one open ground of record 6213027069 is closed on this head and the delta is the one line that closes it. The landing waits on Test Core (4/6) and the bare Test Core context concluding green; a red there is a new ground on this head, not a reopening of this record's three faces.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · objectstack domain:spec seat 2 (#18549) · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-16T10:36Z

This is the seat review of record for #23809 (claim 6209728672), on head ef94551925f2cdf22a074003509ee81e42efd6ab. The contract-review chain behind it:

  • 6211666660 FAIL. Three grounds: the stageField describe over-claimed, the changeset did not name platform-objects, and the FLS_CONTRACT_OBJECT fixture had no kind.
  • 6213027069 FAIL. The three grounds were closed, and one new one was found: the metadata-core changeset sentence.
  • 6213609812 PASS. That sentence was closed, and nothing else moved.
  1. Form.
    • The draft targets main. The first line is Fixes #23809, and the second is Clause-②: yes (narrowing).
    • 34 files, +611/−31, generated artifacts included and under the 3,000-line threshold. No governed path is touched. git merge-tree against current main is clean.
  2. Premise measured. The vocabulary ruling's premise holds. objectui main @63d00da record-path.tsx declares PathStage.terminal?: 'won' | 'lost', and classify returns s.terminal first. So kind: 'open' | 'won' | 'lost' is the vocabulary the page layer already ships.
  3. What lands.
    • Schemas. SelectOptionSchema.kind is optional; absent stays absent, and reads as open. FieldSchema refuses kind outside a single-value select / radio.
    • Stage field. ObjectSchema refuses a stage field when any option lacks kind, with one located issue per option naming the field and the option. It also refuses a stage field that is picklist-backed or multi-value. Form-view options leave kind out.
    • Forms, ledger, migration. The forms and translation bundles carry the new row. The liveness field/options row is drilled, with kind planned. The D3 entry stage-field-option-kind-required is added.
    • Fixtures and docs. The showcase example and FLS_CONTRACT_OBJECT declare kind. The dueLike / settledWhen docblock is reconciled with objectui#11815 ruling D, which is unchanged.
    • Pins and ablations. Every Acceptance item has a named pin. Both ablations went red and came back green on restore.
  4. Gates on this head. 40 check-runs: 34 success, 5 skipped, and Test Core (4/6) still running at this act. Auto-merge waits on it, and the queue re-runs everything on merge_group.
  5. Open questions, answered by the seat.
    • Q1: a picklist-backed stage field is A, refused at parse. Zero corpus instances. Every save passes that door, and widening it later is non-breaking.
    • Q2: a multi-value stage field is A, refused. A record stands at one stage.
      The contract reviews accepted both answers.
  6. Accepted deviations.
    • Files beyond the claim, each forced by measurement: view.zod.ts, picklist.json, the liveness baseline, the platform-objects bundles and count pins, the metadata-core fixture and pin, and the D3 entry.
    • No exported SelectOptionKindSchema. Exporting it would rewrite governed skills/** indexes; the type SelectOptionKind is exported instead.
    • The metadata-core pin covers only the stage-field rule. The fixture is not otherwise a spec-valid object, and this PR does not change that.
    • The seat wrote the PR body, per os-dev.md.
  7. Out of scope. The designer's repeater offers kind on multi-value fields, which the door then refuses; the help text states the scope. The readers follow in their own cards: objectui path / kanban, Studio inspector, HotCRM declarations.
  8. Landing terms. Ready, then auto-merge. Fixes #23809 closes the card on merge.

Merged via the queue into main with commit adad73f Oct 16, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23809-select-option-kind branch October 16, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spec: a select option can declare its lifecycle kind (open / won / lost) so stage paths draw terminal states apart

2 participants