You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec): a blueprint formula field can state its expression (#4577)
* feat(spec): a blueprint formula field can state its expression
`BlueprintFieldSchema.type` is the FULL `FieldType` enum, so the AI-build
design step could always NAME a `formula` field — but neither the lenient
schema nor the OpenAI-strict mirror the model generates against had any key for
the body. There was no way, anywhere on that surface, to say what the formula
computed. It materialized bare, and cloud graph-lint then correctly reported
`formula_without_expression` with the fix "Set field expression to a CEL
formula" — which the agent could not write in the blueprint it was holding.
Detected, but unfixable on the surface that produced it.
This is the same hole `summaryOperations` closed for roll-ups in cloud#970;
`formula` was left behind. It bites hardest through `nameField`, whose own
guidance tells the model to use a formula for numbered entities (invoice /
ticket) composing "number · name" — following that advice produced a record
title blank on every card, lookup chip and breadcrumb.
The PIN matters more than the key. The root cause is not a forgotten property:
two schemas describe the same shape and nothing forced them to agree. The mirror
is what the model may EMIT, the lenient schema is what downstream READS, and
drift either way silently drops authored config. A new test asserts the two
field schemas carry exactly the same keys, so the next key added to one cannot
go missing from the other.
`authorable-surface.json` records exactly one new key
(`ai/BlueprintField:expression`). The API surface is unchanged — a Zod field
adds no exports.
Verification: blueprint tests 29/29; full suite 132/132; check:api-surface
unchanged; check:authorable-surface green after gen:schema.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
* docs(spec): regenerate the blueprint reference for the new `expression` key
`content/docs/references/ai/solution-blueprint.mdx` is generated from the
schema and enumerates BlueprintField's keys in a table, so adding `expression`
makes it stale. Regenerated with `gen:docs`; the diff is exactly the one new
row. `check:docs` reports 259 generated files in sync.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
---------
Co-authored-by: Claude <noreply@anthropic.com>
A blueprint `formula` field can finally say what it computes: `BlueprintFieldSchema` and its OpenAI-strict mirror both gain `expression`.
6
+
7
+
`BlueprintFieldSchema.type` is the **full**`FieldType` enum, so the AI-build design step could always NAME a `formula` field — but neither the lenient schema nor the strict mirror the model generates against had any key for the body. There was no way, anywhere on that surface, to state what the formula computed. It materialized bare, and cloud's graph-lint then correctly reported `formula_without_expression` with the fix *"Set field expression to a CEL formula"* — a fix the agent could not write in the blueprint it was holding. Detected, but unfixable on the surface that produced it.
8
+
9
+
This is the exact hole `summaryOperations` closed for roll-ups in cloud#970 (see this file's own test: *"z.object STRIPS unknown keys, so before this slot existed a blueprint that correctly declared `{ type:'summary', summaryOperations:{…} }` lost the config at the parse waist and materialized runtime-dead"*). `formula` was simply left behind — the same defect, one field type over.
10
+
11
+
It bites hardest through `nameField`, whose own guidance tells the model to point at a formula for numbered entities (invoice/ticket) that compose `number · name`. Without an expression slot, following that advice produces a record title that is blank on every card, lookup chip and breadcrumb.
12
+
13
+
**The pin matters more than the key.** A1's root cause is not a forgotten property — it is that two schemas describe the same shape and nothing forced them to agree. The mirror is what the model may EMIT; the lenient schema is what downstream READS. Drift in either direction silently drops authored config. A new test asserts the two field schemas carry **exactly** the same keys, so the next key added to one cannot go missing from the other.
14
+
15
+
Cloud's `objectBody` carries the value through to materialization (companion change in the `cloud` repo); it reads the key via cast, as it already does for `defaultValue`, so it is inert against an older spec and live as soon as this ships.
Copy file name to clipboardExpand all lines: content/docs/references/ai/solution-blueprint.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -96,6 +96,7 @@ const result = BlueprintApp.parse(data);
96
96
|**reference**|`string`| optional | Target object name for lookup / master_detail relationship fields |
97
97
|**options**|`{ label: string; value: string }[]`| optional | Choices for select / multiselect / radio fields |
98
98
|**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. |
99
+
|**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. |
.describe('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.'),
93
+
expression: z.string().optional()
94
+
.describe('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.'),
.describe('REQUIRED when type is "summary" (a roll-up of child records onto this parent: 任务总数 / 报名人数 / 合计金额 / 已完成任务数); null for every other field type. A "summary" field without it is runtime-dead — it reads 0/empty everywhere.'),
276
+
expression: z.string().nullable()
277
+
.describe('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 record title; null for every other field type. A "formula" field without it is runtime-dead — it reads null everywhere, forever. This is the formula analogue of summaryOperations: name the type and you must supply the body.'),
0 commit comments