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
18 changes: 18 additions & 0 deletions .changeset/30584-blueprint-field-default-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/spec': minor
---

feat(spec): a blueprint field carries a typed `defaultValue`, so an AI-built boolean flag defaults to false

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — no file under `skills/**` restates a blueprint field key (`skills/objectstack-ai/references/_index.md` only points at `solution-blueprint.zod.ts`); the reference page `content/docs/references/ai/solution-blueprint.mdx` is regenerated from the new describe.

The AI solution blueprint had no slot for a field's default. A blueprint that wrote `{ type: 'boolean', defaultValue: false }` for a 是否完成 / 已付款 / `is_active` flag, as the build guidance teaches, parsed with `success: true` and lost the key, so the built field had no default and every record created without the flag read empty instead of false. A filter on false missed those records.

- **`defaultValue` on `BlueprintFieldSchema` and on the strict structured-output mirror's fields (`SolutionBlueprintStrictSchema.objects[].fields[]`).** Both sides declare it as one scalar union, `boolean | number | string`. Lenient: optional. Strict: required and nullable (null = no default), as with every other key in that mirror. The describe says the value becomes the built field's `defaultValue`, and states the boolean-flag rule: a yes/no flag is a `boolean` field that is never `required` and carries `defaultValue: false`.
- **The value is checked against the field's `type`, on the lenient side.** The check is the field schema's own literal contract (`checkLiteralDefaultValue`, the one `FieldSchema` runs on a built field's `defaultValue`), so a default the blueprint admits is one the built field admits. These types take a default: `boolean` / `toggle` take true or false; `number` / `currency` / `percent` / `rating` / `slider` / `progress` take a number; `text` / `textarea` / `email` / `url` / `phone` / `markdown` / `html` / `richtext` / `code` / `color` take a string; `time` takes `"HH:MM"`; `select` / `radio` take the `value` of one of the field's own `options`, never a label.
- **Every other type is refused by name, never stripped.** The refusal is a `custom` issue at the field's `defaultValue` that names the field and its type, says why, and lists the types that take one: a `date` / `datetime` literal is frozen at design time; a reference or file type names a record or file that does not exist yet; a `multiselect` / `checkboxes` / `tags` field stores a list; a `formula` / `summary` / `autonumber` field is computed; `password` / `secret`, `signature` / `qrcode` and the structured JSON types have no one value right for every record. A runtime token (`NOW()`, `current_user`) is refused too: a blueprint default is a literal.
- The strict mirror cannot read `type` while it judges this key, so it can generate a value the lenient schema then refuses. `apply_blueprint` parses the lenient schema, and that refusal names the type at the key.

Additive only: every blueprint that parsed before still parses with the lenient schema. A producer of the STRICT shape must now send `defaultValue` on each field (null when it has none). The cloud build applier is what carries the value into the built field; it moves on its own pin.
2 changes: 2 additions & 0 deletions content/docs/references/ai/solution-blueprint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const result = BlueprintAppSchema.parse(data);
| **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.name" for a composed title. A formula reads a lookup / master_detail field's related record ONE hop through it — `record.<lookup>.<field>`, as `record.customer.name` above; the bare `record.customer` is the stored id, and a second hop (`record.customer.owner.name`) is refused at build. 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. |
| **defaultValue** | `boolean \| number \| string` | optional | The built field's default: the value a new record gets when it is created without one, carried into the object field as its `defaultValue` and stored as written. It must fit `type`, and only these types take one: "boolean" / "toggle" → true or false; "number" / "currency" / "percent" / "rating" / "slider" / "progress" → a number; "text" / "textarea" / "email" / "url" / "phone" / "markdown" / "html" / "richtext" / "code" / "color" → a string; "time" → a time of day, "HH:MM"; "select" / "radio" → the `value` of one of the field's own `options`, never its label. Every other type is refused by name: a date is fixed at design time, a reference or file names no record yet, a multi-value field stores a list, a formula / summary / autonumber is computed, and a credential, signature or structured value has no one value right for every record. A yes/no flag (是否完成 / 已付款 / is_active) is a "boolean" field that is never `required` (omit `required` or set it false) and carries defaultValue false: a required flag refuses the unchecked switch, and a flag with no default is empty, not false, on every record created without it, so a filter on false misses those records. Omit it for a field with no default. |

### Allowed Values: `BlueprintField.type`

Expand Down Expand Up @@ -229,6 +230,7 @@ const result = BlueprintAppSchema.parse(data);
| **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.name" for a composed title. A formula reads a lookup / master_detail field's related record ONE hop through it — `record.<lookup>.<field>`, as `record.customer.name` above; the bare `record.customer` is the stored id, and a second hop (`record.customer.owner.name`) is refused at build. 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. |
| **defaultValue** | `boolean \| number \| string` | optional | The built field's default: the value a new record gets when it is created without one, carried into the object field as its `defaultValue` and stored as written. It must fit `type`, and only these types take one: "boolean" / "toggle" → true or false; "number" / "currency" / "percent" / "rating" / "slider" / "progress" → a number; "text" / "textarea" / "email" / "url" / "phone" / "markdown" / "html" / "richtext" / "code" / "color" → a string; "time" → a time of day, "HH:MM"; "select" / "radio" → the `value` of one of the field's own `options`, never its label. Every other type is refused by name: a date is fixed at design time, a reference or file names no record yet, a multi-value field stores a list, a formula / summary / autonumber is computed, and a credential, signature or structured value has no one value right for every record. A yes/no flag (是否完成 / 已付款 / is_active) is a "boolean" field that is never `required` (omit `required` or set it false) and carries defaultValue false: a required flag refuses the unchecked switch, and a flag with no default is empty, not false, on every record created without it, so a filter on false misses those records. Omit it for a field with no default. |


---
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 @@ -46,6 +46,7 @@
"ai/BlueprintDashboard:label",
"ai/BlueprintDashboard:name",
"ai/BlueprintDashboard:widgets",
"ai/BlueprintField:defaultValue",
"ai/BlueprintField:expression",
"ai/BlueprintField:label",
"ai/BlueprintField:name",
Expand Down
5 changes: 4 additions & 1 deletion packages/spec/dropped-refinements.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"measured": {
"zod": "4.4.3",
"publishedSchemasWithDroppedRefinements": 252,
"droppedRefinementSites": 1020,
"droppedRefinementSites": 1023,
"refinementSitesThatDidProject": 369,
"refinementSitesWithNoJsonFormToCompare": 0
},
Expand All @@ -16,11 +16,13 @@
},
"ai/BlueprintField": {
"sites": [
"",
"summaryOperations.filter.lazy"
]
},
"ai/BlueprintObject": {
"sites": [
"fields.element",
"fields.element.summaryOperations.filter.lazy"
]
},
Expand All @@ -46,6 +48,7 @@
},
"ai/SolutionBlueprint": {
"sites": [
"objects.element.fields.element",
"objects.element.fields.element.summaryOperations.filter.lazy",
"views.element.filters.element"
]
Expand Down
Loading
Loading