Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/24198-blueprint-option-kind-stage-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/spec': minor
---

feat(spec/ai): a solution blueprint's select options declare their lifecycle `kind`, and a blueprint object names its `stageField`

Clause-②: yes

The object schema already requires a lifecycle `kind` (`open` / `won` / `lost`) on every option of the field an object's `stageField` names. The AI blueprint had no slot for either key, so a model could not say which field was an object's stage or which option ended its lifecycle, and that rule never applied to an AI-built object. Both blueprint shapes now carry both keys.

- **Option `kind`**, on `BlueprintFieldSchema.options[]` and on the strict structured-output mirror's (`SolutionBlueprintStrictSchema`) field options. It is `SelectOption.kind`'s own enum, taken by reference, so the blueprint accepts exactly the kinds an object field's option accepts. Lenient: optional. Strict: required and nullable (null = no kind stated).
- **Object `stageField`**, on `BlueprintObjectSchema` and the strict mirror's objects: a snake_case name of one of the object's own fields, mapping onto `ObjectSchema.stageField`. Lenient: optional. Strict: required and nullable (null = no stage field). The object schema's `false` value ("no linear lifecycle") has no blueprint spelling.
- Both describes state what the object schema refuses: the field `stageField` names must be a single-value `select` or `radio` whose inline options each declare `kind`. A multiselect / checkboxes / tags field that carries options is refused as a stage field, and `kind` on such a field's options is refused by the field schema. The blueprint does not check these rules itself; the object schema does, when the applier writes the object.
- Additive only: every blueprint that parsed before still parses with the lenient schema. A producer of the STRICT shape must now send `stageField` on each object and `kind` on each option (null when it has none), as with every other key in that mirror.
15 changes: 13 additions & 2 deletions content/docs/references/ai/solution-blueprint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const result = BlueprintAppSchema.parse(data);
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | ✅ | Field data type |
| **required** | `boolean` | optional | Whether the field is required |
| **reference** | `string` | optional | Target object name for lookup / master_detail relationship fields |
| **options** | `{ label: string; value: string }[]` | optional | Choices for select / multiselect / radio fields |
| **options** | `{ label: string; value: string; kind?: Enum<'open' \| 'won' \| 'lost'> }[]` | optional | Choices for select / multiselect / radio fields |
| **summaryOperations** | `{ object: string; function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>; field?: string; relationshipField?: string; … }` | optional | REQUIRED when `type` is "summary" (a roll-up of child records: 任务总数 / 报名人数 / 合计金额 / 已完成任务数). Names the child object, the aggregation, and — for a qualified count/sum — the condition. A "summary" field without it materializes runtime-dead. |
| **expression** | `string` | optional | REQUIRED when `type` is "formula" — the CEL body the field computes, e.g. "record.quantity * record.unit_price", or "record.order_no + ' · ' + record.customer" for a composed title. A "formula" field without it materializes runtime-dead: the engine builds its formula plan only from fields that HAVE an expression, so the field reads null everywhere, forever. Same failure shape as a "summary" with no `summaryOperations`. Note `nameField` on the object recommends a formula for numbered entities (invoice/ticket) — that formula needs THIS key, or the record title is blank on every card, lookup chip and breadcrumb. |

Expand Down Expand Up @@ -165,6 +165,14 @@ const result = BlueprintAppSchema.parse(data);
* `tags`
* `vector`

### Nested Shape: `BlueprintField.options[number]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | ✅ | |
| **value** | `string` | ✅ | |
| **kind** | `Enum<'open' \| 'won' \| 'lost'>` | optional | The option's lifecycle category, REQUIRED on EVERY option of the field the object's `stageField` names. The object schema refuses the object unless the field `stageField` names is a single-value `select` or `radio` whose inline options EACH declare `kind`: "open" for a step on the way, "won" for the successful end (a deal's Closed Won, a task's Done, a case's Resolved), "lost" for the unsuccessful end (Closed Lost, Cancelled, Unqualified). Naming a multiselect / checkboxes / tags field that carries options is refused too: a record stands at one stage. Only a single-value `select` / `radio` field's options take it: omit it on a multiselect / checkboxes / tags field's options, where the field schema refuses it. Absent on any other select option reads as "open". |

### Nested Shape: `BlueprintField.summaryOperations`

| Property | Type | Required | Description |
Expand Down Expand Up @@ -206,6 +214,7 @@ const result = BlueprintAppSchema.parse(data);
| **fields** | `{ name: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; required?: boolean; … }[]` | ✅ | Fields to create on the object |
| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users — the deliberate sharing choice for this object (ADR-0090). Canonical four only: private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record — ONLY for an object whose fields include a master_detail reference). SET it when the user's description implies a visibility intent — personal/private data (HR, 绩效, salary, 个人隐私) → "private"; shared reference data everyone edits → "public_read_write". Omit to accept the platform's deterministic default (business object → public_read_write; master-detail child → controlled_by_parent) — omitting on privacy-sensitive data silently over-shares it. |
| **nameField** | `string` | optional | The record title field — which field holds the human-readable name shown on cards, lookup chips, breadcrumbs and search (ADR-0079). Set it to the object's text label field (e.g. "product_name"). For a numbered entity (invoice/ticket), set it to a formula field that composes number + name (e.g. "`{order_no}` · `{customer}`"). Omitting it lets the platform auto-pick a text field, but declaring it is strongly preferred. |
| **stageField** | `string` | optional | The lifecycle stage field (ADR-0085) — which `select` (or `radio`) field of THIS object holds the ordered steps a record moves through (a deal's stage, a ticket's status), drawn as the record path. snake_case, naming one of this object's own fields. The object schema refuses the object unless the field `stageField` names is a single-value `select` or `radio` whose inline options EACH declare `kind`: "open" for a step on the way, "won" for the successful end (a deal's Closed Won, a task's Done, a case's Resolved), "lost" for the unsuccessful end (Closed Lost, Cancelled, Unqualified). Naming a multiselect / checkboxes / tags field that carries options is refused too: a record stands at one stage. SET it whenever the object has such a status/stage field, and give every one of that field's options its `kind`. Omitting it leaves stage detection to a guess from field names (status / stage / state / phase). |

### Nested Shape: `BlueprintObject.fields[number]`

Expand All @@ -216,7 +225,7 @@ const result = BlueprintAppSchema.parse(data);
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field data type |
| **required** | `boolean` | optional | Whether the field is required |
| **reference** | `string` | optional | Target object name for lookup / master_detail relationship fields |
| **options** | `{ label: string; value: string }[]` | optional | Choices for select / multiselect / radio fields |
| **options** | `{ label: string; value: string; kind?: Enum<'open' \| 'won' \| 'lost'> }[]` | optional | Choices for select / multiselect / radio fields |
| **summaryOperations** | `{ object: string; function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>; field?: string; relationshipField?: string; … }` | optional | REQUIRED when `type` is "summary" (a roll-up of child records: 任务总数 / 报名人数 / 合计金额 / 已完成任务数). Names the child object, the aggregation, and — for a qualified count/sum — the condition. A "summary" field without it materializes runtime-dead. |
| **expression** | `string` | optional | REQUIRED when `type` is "formula" — the CEL body the field computes, e.g. "record.quantity * record.unit_price", or "record.order_no + ' · ' + record.customer" for a composed title. A "formula" field without it materializes runtime-dead: the engine builds its formula plan only from fields that HAVE an expression, so the field reads null everywhere, forever. Same failure shape as a "summary" with no `summaryOperations`. Note `nameField` on the object recommends a formula for numbered entities (invoice/ticket) — that formula needs THIS key, or the record title is blank on every card, lookup chip and breadcrumb. |

Expand Down Expand Up @@ -334,6 +343,7 @@ Sort field and direction pair
| **fields** | `{ name: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; required?: boolean; … }[]` | ✅ | Fields to create on the object |
| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users — the deliberate sharing choice for this object (ADR-0090). Canonical four only: private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record — ONLY for an object whose fields include a master_detail reference). SET it when the user's description implies a visibility intent — personal/private data (HR, 绩效, salary, 个人隐私) → "private"; shared reference data everyone edits → "public_read_write". Omit to accept the platform's deterministic default (business object → public_read_write; master-detail child → controlled_by_parent) — omitting on privacy-sensitive data silently over-shares it. |
| **nameField** | `string` | optional | The record title field — which field holds the human-readable name shown on cards, lookup chips, breadcrumbs and search (ADR-0079). Set it to the object's text label field (e.g. "product_name"). For a numbered entity (invoice/ticket), set it to a formula field that composes number + name (e.g. "`{order_no}` · `{customer}`"). Omitting it lets the platform auto-pick a text field, but declaring it is strongly preferred. |
| **stageField** | `string` | optional | The lifecycle stage field (ADR-0085) — which `select` (or `radio`) field of THIS object holds the ordered steps a record moves through (a deal's stage, a ticket's status), drawn as the record path. snake_case, naming one of this object's own fields. The object schema refuses the object unless the field `stageField` names is a single-value `select` or `radio` whose inline options EACH declare `kind`: "open" for a step on the way, "won" for the successful end (a deal's Closed Won, a task's Done, a case's Resolved), "lost" for the unsuccessful end (Closed Lost, Cancelled, Unqualified). Naming a multiselect / checkboxes / tags field that carries options is refused too: a record stands at one stage. SET it whenever the object has such a status/stage field, and give every one of that field's options its `kind`. Omitting it leaves stage detection to a guess from field names (status / stage / state / phase). |

### Nested Shape: `SolutionBlueprint.views[number]`

Expand Down Expand Up @@ -399,6 +409,7 @@ Sort field and direction pair
| **fields** | `{ name: string; label: string \| null; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; required: boolean \| null; … }[]` | ✅ | Fields to create on the object |
| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'> \| null` | ✅ | Org-Wide Default record visibility (OWD) for INTERNAL users (ADR-0090), or null to accept the platform default (business object → public_read_write; master-detail child → controlled_by_parent). SET it when the user's description implies a visibility intent: personal/private data (HR, 绩效, salary, 个人隐私) → "private" (owner-only); "public_read" = everyone reads, owner writes; "public_read_write" = everyone reads+writes; "controlled_by_parent" ONLY for an object with a master_detail reference field. Null on privacy-sensitive data silently over-shares it. |
| **nameField** | `string \| null` | ✅ | The record title field — which field holds the human-readable name shown on cards, lookup chips, breadcrumbs and search (ADR-0079), or null to let the platform auto-pick a text field. Set it to the object's text label field (e.g. "product_name") — snake_case. For a numbered entity (invoice/ticket), set it to a formula field that composes number + name (e.g. "`{order_no}` · `{customer}`"). Declaring it is strongly preferred over null. |
| **stageField** | `string \| null` | ✅ | The lifecycle stage field (ADR-0085) — which `select` (or `radio`) field of THIS object holds the ordered steps a record moves through (a deal's stage, a ticket's status), drawn as the record path — or null. snake_case, naming one of this object's own fields. The object schema refuses the object unless the field `stageField` names is a single-value `select` or `radio` whose inline options EACH declare `kind`: "open" for a step on the way, "won" for the successful end (a deal's Closed Won, a task's Done, a case's Resolved), "lost" for the unsuccessful end (Closed Lost, Cancelled, Unqualified). Naming a multiselect / checkboxes / tags field that carries options is refused too: a record stands at one stage. SET it whenever the object has such a status/stage field, and give every one of that field's options a non-null `kind`. Null leaves stage detection to a guess from field names (status / stage / state / phase). |

### Nested Shape: `SolutionBlueprintStrict.views[number]`

Expand Down
1 change: 1 addition & 0 deletions packages/spec/authorable-surface/ai.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"ai/BlueprintObject:name",
"ai/BlueprintObject:nameField",
"ai/BlueprintObject:sharingModel",
"ai/BlueprintObject:stageField",
"ai/BlueprintSeed:object",
"ai/BlueprintSeed:records",
"ai/BlueprintSummaryOperations:conditions",
Expand Down
Loading
Loading