Skip to content

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

Closed

Description

@objectstack-fleet

Source

Read on 2026-10-16 in the console (objectui main 8415d9f11) with HotCRM 63db83e2. The lead record's stage path draws "New ✓ → Contacted → Qualified → Converted → Unqualified": the lead's status options (src/sales/objects/lead.object.ts) are a flat list, and Unqualified, which ends a lead's life, is painted as a fifth step after Converted. The opportunity record's path draws "Prospecting … Negotiation · Closed Won · ✗ Closed Lost" with the two closed states set apart, because the console recognises the values closed_won / closed_lost by name. packages/spec/src/data/field.zod.ts (around line 1935) says it itself: no select option carries a terminal marker, so the renderer guesses from value spelling and every other object gets a path that treats its lost state as a step.

Target

  1. The select option shape gains kind, a small closed enum of lifecycle categories: "a step on the way", "the successful end", "the unsuccessful end". The spelling is the spec seat's to settle, choosing one vocabulary for the whole platform between the sales pair open | won | lost and the neutral open | done | cancelled (Jira's status-category shape), so that a case's Resolved / Cancelled and a task's Completed fit the same three words as a deal's Closed Won / Closed Lost. Declared once on the option, read by every renderer of a stageField: the record path, the kanban column order, the "closed" filters, the dashboard "won / lost" measures.
    • On a plain select the key is optional and an absent kind reads as the open category.
    • When an object's stageField names the field, every option of that field must declare kind explicitly: publish refuses a stage field with any option whose kind is missing, naming the field and the option. No default there, because a default would hand an AI that forgot the key the same wrong path drawn today, only declared. Maintainer ruling 2026-10-16 (agreeing in chat to these two tightenings, the mandatory kind under stageField and the vocabulary choice), verbatim: 「同意」.
  2. The console's name-based guess (closed_won / closed_lost) stays as the fallback for packages that do not declare the kind, and is retired in the next major.
  3. HotCRM declares kind: 'lost' on the lead's unqualified and kind: 'won' on converted, and the same on the opportunity's two closed stages, in the same change set (a hotcrm follow-up card references this one).
  4. The field inspector in Studio shows the kind as a third column of the picklist options editor (the objectui follow-up).

File surface

packages/spec/src/data/field.zod.ts (the option schema and its docblock), the spec's generated docs and metadata forms, packages/spec contract tests; the objectui path renderer and Studio inspector follow in their own cards once the field is published.

Acceptance

  1. Spec tests: an option with a terminal kind parses; an unknown kind is refused; a plain select without kinds parses and reads as open; an object whose stageField names a select with one option lacking kind is refused at publish with a message that names the field and the option; kind on a non-select field is refused.
  2. The generated metadata forms list the field; pnpm --filter @objectstack/spec test and the spec gates green.
  3. The four-axis note on this card (business pull: two HotCRM objects already encode the semantics by naming; AI-safety: a declared kind beats a spelling convention) is read by the spec seat before it lands.

Constraints

  • Contract-first: the spec declares before any consumer reads; no console-only workaround.

Evidence

Screenshots on the temporary branch claude/console-home-login-design-20261016 of objectstack-ai/objectui (commit b72c97985, never merged): hotcrm-lead-record-header-and-stage.jpg · hotcrm-opportunity-record.jpg.

Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff from the 2026-10-16 walk of HotCRM's core pages as a user: objectui main 8415d9f11, HotCRM 63db83e2, the vite console on :5180 against objectstack serve --dev on :4001, the maintainer's browser pane at 1024 × 768 (en). The maintainer asked for the findings to be filed on the repository each belongs to, verbatim: 「相关问题也在对应的项目上创建任务」.

Body refreshed 2026-10-16T04:10Z: Target 1 and Acceptance 1 tightened per the maintainer's 2026-10-16 ruling (kind mandatory under stageField, vocabulary settled by the spec seat); the rest unchanged.

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 12 (second shift, batch 1) · 2026-10-16T06:41Z · the select option's lifecycle kind, with its required form under stageField
    Session: session_01LFqdRsuM5nkqoJdHd5ztVk (the seat; the dev is a cloud session whose draft PR carries its own session link)
    Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23809-select-option-kind
    Worktree: objectstack-issue-23809
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface (paths on origin/main 1ecb08a1cc or later; stop on a breach and explain it in the report):

  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23809,
      "status": "done",
      "branch": "claude/issue-23809-select-option-kind",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23846",
      "session": "session_017qmjVjz61scwQFLfspdBX6",
      "premise_still_valid": true,
      "summary": "SelectOptionSchema gains kind: open | won | lost (optional; absent stays absent, reads as open). FieldSchema refuses kind off a single-value select/radio, and ObjectSchema refuses a stageField whose options do not each declare kind (one located issue per option, naming field and option), plus a picklist-backed or multi-value stage field. FormSelectOptionSchema omits kind with guidance; forms offer kind; field/options liveness drilled (kind planned); D3 entry stage-field-option-kind-required; showcase semantic-zoo options declare kind; platform-objects form bundles regenerated and translated. Vocabulary premise measured true: objectui main @63d00da record-path.tsx classify returns s.terminal ('won' | 'lost') first.",
      "tests": "head 4274c652. @objectstack/spec full vitest: 21996 passed / 2 failed (field-rows-option-description and form-select-option pins, both then updated); re-run of those 2 + select-option-kind.test.ts + src/migrations: 279/279; spec typecheck exit 0; check:generated all 14 up to date. Ablation via scripts/ablation-replace.mjs (mutation on disk anchor 1->0, restore blob == HEAD, git diff HEAD empty): delete refuseStageFieldWithoutKinds call -> 6 failed / 15 passed; FieldSchema off-type guard -> if (false) -> 7 failed / 14 passed; both green after restore (src-resolved suite, no dist alias involved). Downstream: rest FLS/save-gate 125/125, runtime meta-object-fls 85/85, metadata-core object-schema-fls-references 53/53; examples/app-showcase validate/build/typecheck exit 0.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack: 116 derived, 116 run, reconciled by --ran (0 NOT-MEASURED, 0 UNRUN). Exit codes recorded before any pipe: 112 exit 0; check:i18n exit 1 (real: platform-objects bundles drifted) -> fixed (--write + zh-CN/ja-JP/es-ES translations) -> 0; check:skill-examples exit 3 and check:i18n exit 3 (dist prerequisite) -> rerun 0, check:i18n-coverage 0, check:i18n-stale-fill 0. NOT MEASURED, exit 3 prerequisite, left to CI: node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone cannot reach pinned fixture), pnpm check:dual-build-cjs-loads (whole workspace unbuilt). PR check-runs: in_progress at report time (not waited on, per os-dev).",
      "line_budget": "PR additions 558 / deletions 27 over 29 files (generated: authorable-surface, api-surface, export-origins, json-schema.manifest, reference docs, liveness state-counts, i18n bundles). No skills/** or governed surface touched.",
      "deviations": [
        "Worktree: the session's primary checkout already held the branch, so it was detached and the worktree objectstack-issue-23809 created on the existing branch (no new branch).",
        "Files beyond the claim's surface, each forced by measurement: packages/spec/src/ui/view.zod.ts + form-select-option.test.ts (FormSelectOptionSchema derives from SelectOptionSchema.shape, so kind would leak onto form-view options with no reader); packages/spec/liveness/picklist.json and scripts/liveness/undrilled-containers.baseline.json (the walk demands picklist/options.kind, and drilling field/options retires its debt row); packages/spec/src/data/field-rows-option-description.test.ts (pins the repeater list); packages/platform-objects translations (check:i18n); packages/spec/src/migrations/entries/semantic/18.stage-field-option-kind-required.ts (ADR-0087 disposition for the narrowing).",
        "No exported SelectOptionKindSchema: an export above CurrencyValueSchema rewrote five skills/**/references/_index.md (governed Tier H, and off-limits for this card), and one placed below broke the OS_EAGER_SCHEMAS evaluation; the enum is inlined and only the type SelectOptionKind is exported.",
        "One single-file vitest run (the first select-option-kind run) went outside os-verify-lock.",
        "Clause-② line in PR body and changeset carries the (narrowing) arm; the claim line reads 'Clause-②: yes' without an arm.",
        "No label-write: this dispatch's write budget names push, pr_create and this comment only."
      ],
      "files_changed": [
        ".changeset/23809-select-option-kind.md",
        "examples/app-showcase/src/data/objects/semantic-zoo.object.ts",
        "packages/spec/src/data/field.zod.ts",
        "packages/spec/src/data/object.zod.ts",
        "packages/spec/src/data/object.form.ts",
        "packages/spec/src/data/field.form.ts",
        "packages/spec/src/ui/view.zod.ts",
        "packages/spec/src/data/select-option-kind.test.ts",
        "packages/spec/src/data/field-rows-option-description.test.ts",
        "packages/spec/src/ui/form-select-option.test.ts",
        "packages/spec/src/migrations/entries/semantic/18.stage-field-option-kind-required.ts",
        "packages/spec/liveness/field.json",
        "packages/spec/liveness/picklist.json",
        "packages/spec/liveness/state-counts/field.md",
        "packages/spec/liveness/state-counts/picklist.md",
        "packages/spec/scripts/liveness/undrilled-containers.baseline.json",
        "packages/spec/authorable-surface/data.json",
        "packages/spec/api-surface/data.json",
        "packages/spec/export-origins/data.json",
        "content/docs/references/{api/metadata,data/field,data/object,data/picklist,system/migration,ui/view}.mdx (gen:docs)",
        "packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts"
      ],
      "mcp_calls": "4 read-only: issue_read get, issue_read get_comments, pull_request_read get; plus add_repo (read lane, nothing attached). No MCP write tool.",
      "api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs: pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST .../issues/23846/assignees), comment (POST .../issues/23809/comments, this report). Plus git push.",
      "open_questions": [
        {
          "question": "A stageField naming a picklist-backed select: refuse at parse (shipped) or judge the resolved list's kinds?",
          "options": [
            "A (shipped): ObjectSchema refuses it at stageField. Every door, including the save door; zero corpus instances; widening later is non-breaking.",
            "B: accept it at parse and judge the resolved picklist (plus picklistExtensions) in the CLI picklist-references walk. Shared stage lists become possible, but only os validate / os build judge them, and the save door lets them through unjudged."
          ],
          "recommendation": "A. Business pull: none measured (zero picklist-backed stage fields). AI-safety: a parse refusal is the loud door every write passes. Startup focus: no new CLI rule. Long term: widen to B only when a named producer needs shared stage lists."
        },
        {
          "question": "Stage fields carrying options on multiselect or multiple: true are now refused at stageField; kind is refused on those types. Keep it?",
          "options": [
            "A (shipped): refuse. A record stands at one stage.",
            "B: leave non-single-value stage fields alone."
          ],
          "recommendation": "A. Without it such a stage field slips the mandatory-kind rule silently. Zero corpus instances."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted in PR Acceptance notes, not filed: the object designer's options repeater (visible for multiselect/checkboxes) offers kind, which the door refuses there, and the form DSL has no parent-scoped row predicate. Help text states the scope.",
        "carrier: none · noted, not filed: metadata-core FLS_CONTRACT_OBJECT names stageField over kind-less options. No door parses it in its suites."
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23809,
      "status": "done",
      "branch": "claude/issue-23809-select-option-kind",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23846",
      "session": "session_017qmjVjz61scwQFLfspdBX6",
      "premise_still_valid": true,
      "summary": "Patch round on contract review 6211666660 (FAIL), head 44754694. (1) The stageField .describe() and its [#23809] TSDoc now state only what refuseStageFieldWithoutKinds refuses (a field that carries options must be a single-value select/radio with inline options each declaring kind; picklist-backed and multi-value stage fields refused; an unresolved name or an option-less field not judged at parse); reference docs regenerated via check:generated --fix (object.mdx, api/metadata.mdx, system/migration.mdx). (2) The changeset adds '@objectstack/platform-objects': patch and '@objectstack/metadata-core': patch, each with one sentence. (3) FLS_CONTRACT_OBJECT's salary_grade options declare kind: 'open', pinned in metadata-core. Also found and fixed a CI red the first round missed: Test Core (3/6) failed on three platform-objects catalog count pins that the new kind rows move (10->11, 116->118, 672->674).",
      "tests": "head 44754694. spec: check:generated all 14 up to date after --fix; select-option-kind + object.test 232/232. metadata-core full suite 735/735, typecheck exit 0. The new pin asserts no ObjectSchema issue at stageField or under fields.salary_grade.options, with a positive control: deleting one option's kind raises exactly fields.salary_grade.options.1.kind. A whole-object ObjectSchema.parse pin was not cheap, because the fixture is deliberately not spec-valid elsewhere (its script actions carry no body/target, measured: actions.0.body / actions.1.body issues), so the pin is scoped to the stage-field rule. Downstream after rebuilding metadata-core: rest FLS/save-gate 125/125, runtime meta-object-fls 85/85. platform-objects: 3 failed / 1089 passed before (field-panel-echo-decisions ROW_PROPERTIES 11 vs 10, object-collapsed-sections OPEN_LEAVES 118 vs 116, object-lifecycle-panel translated 674 vs 672 per locale), 1092/1092 after, typecheck exit 0.",
      "gates": "dispatch-gates --commands on head ec9f775f: 116 derived, 116 run, --ran reconciled (0 NOT-MEASURED, 0 UNRUN). Exit codes before any pipe: 111 exit 0. check:skill-examples, check:i18n and check:i18n-coverage exited 3 (spec dist prerequisite, as in round 1); after a spec rebuild all three re-ran 0. NOT MEASURED locally (exit 3 prerequisite, left to CI): check-plugin-teardown-shape --self-test (shallow clone) and check:dual-build-cjs-loads (workspace unbuilt). Head 44754694 adds only three platform-objects test edits, and the derived list is byte-identical. The families that read them were re-run, all exit 0: type-check-coverage, type-check-debt, test-source-alias, cross-package-test-inputs, issue-citations, nul-bytes, doc-authoring, i18n, adr-0087-registration, changeset-no-major, empty-changeset. CI as read on the ec9f775f-era run (38877404174 / 38877404281): Test Core (3/6) failure, which is the platform-objects count pins above, now fixed; Test Core (2/6) and (4/6) in_progress; everything else success or skipped. CI on 44754694 is in_progress.",
      "line_budget": "This round: spec describe/TSDoc (2 hunks), changeset (+2 frontmatter lines, +4 body lines), metadata-core fixture (+3 lines) and pin (+25), platform-objects count pins (3 hunks, +10), regenerated reference docs. No skills/** or governed surface.",
      "deviations": [
        "Round-1 miss, now closed: the first report did not run @objectstack/platform-objects' own suite after changing its bundles, so the CI red on three count pins went unreported there.",
        "metadata-core pin is scoped to the stage-field rule rather than a whole-object parse (see tests).",
        "PR body not edited (os-dev: the seat writes it); replacement paragraphs are below."
      ],
      "files_changed": [
        "packages/spec/src/data/object.zod.ts",
        ".changeset/23809-select-option-kind.md",
        "packages/metadata-core/src/object-schema-fls-contract.ts",
        "packages/metadata-core/src/object-schema-fls-references.test.ts",
        "packages/platform-objects/src/apps/translations/field-panel-echo-decisions.test.ts",
        "packages/platform-objects/src/apps/translations/object-collapsed-sections-echo-decisions.test.ts",
        "packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts",
        "content/docs/references/{api/metadata,data/object,system/migration}.mdx (check:generated --fix)"
      ],
      "sweep": "git grep -n 'stageField:' -- 'packages/**/src/**' examples on 44754694. Parsed by ObjectSchema and pointing at kind-less options: none left. Showcase semantic-zoo was fixed in round 1; FLS_CONTRACT_OBJECT is fixed now. Not parsed by ObjectSchema: lint validate-semantic-roles.test.ts and validate-object-field-refs.test.ts (validateSemanticRoles / validateObjectFieldRefs over raw stacks); metadata-core object-schema-fls-references.test.ts:73 (applyObjectSchemaMask input, and secret_score is a number field with no options); metadata-core object-schema-fls-references.ts:738 (a mask position table, not metadata); spec object.test.ts:1766 (fields: {}, so the target is undeclared and not judged) and :1334 (retired detail block); e2e detail-shapes.spec.ts (comments and stageField:false tests). stageField: false sites are untouched by the rule.",
      "mcp_calls": "7 read-only this round: pull_request_read get_comments, get_check_runs; get_job_logs x4 (job 116896497288); plus the round-1 reads. No MCP write tool.",
      "api_writes": "1 this round: comment (POST /repos/objectstack-ai/objectstack/issues/23809/comments, this report). Plus three plain git pushes (no force).",
      "pr_body_patch": [
        "In ## What, replace the object.zod.ts bullet's last sentence with: 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.",
        "In ## What, replace the changeset bullet with: 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.",
        "In ## What, add a bullet: `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.",
        "In ## What, add a bullet: 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.",
        "Replace the ## Verification heading line with: ## Verification (head 44754694), and append: platform-objects 1092/1092; metadata-core 735/735; rest FLS 125/125; runtime meta-object-fls 85/85.",
        "In ## Acceptance notes, delete the FLS_CONTRACT_OBJECT bullet (now fixed in this PR)."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23809,
      "status": "done",
      "branch": "claude/issue-23809-select-option-kind",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23846",
      "session": "session_017qmjVjz61scwQFLfspdBX6",
      "premise_still_valid": true,
      "summary": "Patch round 2 on delta contract review 6213027069, head ef945519. One sentence in .changeset/23809-select-option-kind.md (the metadata-core line) was false: it said the fixture still parses through ObjectSchema. It now says what the pin asserts: the two options declare kind: 'open', and ObjectSchema raises no issue at stageField or under fields.salary_grade.options. It also states that the fixture is not otherwise spec-valid (its script actions carry no body or target) and that this change leaves that as it was. No other file touched.",
      "tests": "No code change; no test run this round (the pin the sentence now describes ran 735/735 in round 1 of the patch, on 44754694).",
      "gates": "Changeset families on head ef945519, exit codes before any pipe, all 0: check-empty-changeset --base origin/main and --self-test, check-adr-0087-registration --base origin/main and --self-test, check-changeset-no-major --base origin/main and --self-test, pnpm check:changeset-gate-self-tests, check:nul-bytes, check:issue-citations, check:doc-authoring. CI on ef945519 not yet concluded; not waited on.",
      "line_budget": "1 file, +1 / -1.",
      "deviations": [],
      "files_changed": [".changeset/23809-select-option-kind.md"],
      "mcp_calls": "0 this round.",
      "api_writes": "1 this round: comment (POST /repos/objectstack-ai/objectstack/issues/23809/comments, this report). Plus one plain git push.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23846 comment 6213634298 · domain:spec seat 2 · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-16T10:37Z · on contract review PASS 6213609812 (after FAIL 6211666660 and FAIL 6213027069). Ready + auto-merge in this act; Fixes #23809 closes this card on merge.

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23846 → adad73f714 (Fixes #23809) · domain:spec seat 2 · os-bill · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-16T11:23Z

    probe reading
    PR object merged 2026-10-16T11:15Z out of the merge queue, as squash adad73f714 on parent c5cf546a6f
    origin/main git merge-base --is-ancestor adad73f714 origin/main → exit 0
    merged vs reviewed head git diff c5cf546a6f adad73f714 and the reviewed head's net diff (ef94551925 over its merge base with that parent) share git patch-id --stable 3f0bdeb0
    queue branch no gh-readonly-queue/main/pr-23846-* ref remains
    card closed completed by Fixes #23809; pm:dispatched cleared in this act

    What holds on main now.

    • A select option declares its lifecycle kind (open / won / lost): won is the successful end and lost the unsuccessful end, on every object. That is the vocabulary record:path stages[].terminal already ships.
    • An absent kind reads as open. kind is refused outside a single-value select / radio.
    • On every option of the field an object's stageField names, kind is mandatory. ObjectSchema refuses a missing kind at every door, with one located issue per option. It also refuses a stage field that is picklist-backed or multi-value.
    • os migrate meta lists the D3 entry stage-field-option-kind-required.

    Chain:

    • claim 6209728672
    • dev reports 6211151563, 6212681414 and the round-3 report
    • contract reviews FAIL 6211666660, FAIL 6213027069, then PASS 6213609812
    • ACCEPT 6213634298

    Readers to follow, in their own cards (the card's Target 2–4):

    • objectui's record path and kanban read the option kind;
    • the Studio inspector shows a third column;
    • HotCRM declares kind on its lead and opportunity stages.

    Read at this record: no objectstack-ai/objectui issue cross-references #23809, by #23809's timeline and a search of that repository. The HotCRM repository is outside this seat's reach, so it was not checked. The card's Target 3 says that a HotCRM follow-up references this card.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions