Skip to content

Commit 4bf9a2d

Browse files
feat(spec): a blueprint field carries a typed defaultValue, so an AI-built boolean flag defaults to false (#30655)
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa <!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuHuzzmQedufwxyR2BQdPjaR)_ Implemented by the card thread of claim 6668151062 (session_01L6bhgUCEUicLCQ4tkXayK2); round 1 adopted from contract review 6670092320. Fixes #30584 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. ## 维护者速读 - 改了什么:AI 搭建方案(蓝图)的字段可以写 `defaultValue` 了。宽松 schema 与 strict 结构化输出镜像都加了这个键,并按字段类型校验:布尔取 true/false,数字取数字,单选取本字段自己的选项值;21 种类型可写,其余 28 种写了就按类型名拒收,不再悄悄丢掉;`current_user`、`NOW()` 这类运行时令牌先按令牌拒收,理由说的是令牌,不借用类型的理由(第 1 轮)。 - 为什么改:AI 照搭建指引把「是否完成 / 已付款」写成 `defaultValue: false`,以前解析成功但这个键被剥掉,建出的字段没有默认值,新记录是空而不是「否」,按「否」筛选会漏掉;按 cloud#3619 裁决 B 补上 spec 这一半。 - 风险与回滚:只加不收,之前能解析的蓝图照旧能解析;strict 镜像的生产者每个字段要多发一个 `defaultValue`(无默认填 null)。回滚即还原 `solution-blueprint.zod.ts`、它的测试和台账三处站点,无数据迁移。 - 先看:`packages/spec/src/ai/solution-blueprint.zod.ts`(准入表 `BLUEPRINT_DEFAULT_VALUE_RULES` 与 `refuseUnfitDefaultValue`)、`packages/spec/src/ai/solution-blueprint.test.ts`(末尾 `blueprint field defaultValue` 一节)、`packages/spec/dropped-refinements.baseline.json`(新增三处站点)。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/ai/solution-blueprint.test.ts`,预期 97 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes - **`defaultValue` on `BlueprintFieldSchema` and on the strict mirror's fields** (`SolutionBlueprintStrictSchema.objects[].fields[]`, the `StrictField` shape). Both sides declare one scalar union, `boolean | number | string`. Lenient: `.optional()`. Strict: `.nullable()` and present on every field, as with every other key in that mirror; the blueprint tools strip the null. The key is appended last on both sides, so the model generates it after `type` and `options`. - **The type check lives on the lenient field**, as a `superRefine` (`refuseUnfitDefaultValue`) run with `when: () => true`. It reads one table, `BLUEPRINT_DEFAULT_VALUE_RULES`, typed as a record keyed by every `FieldType` member: a type added to `FieldType` does not compile until it is placed in the table, admitted or refused with a reason. An admitted value is judged by `checkLiteralDefaultValue` from `data/default-value-shape.ts`, the same literal contract `FieldSchema` runs on a built field's `defaultValue` (ADR-0104 D1, `valueSchemaFor(def, 'stored')`). A select / radio default must be one of THIS field's `options[].value`; with no options declared it is refused. A runtime token (`NOW()`, `current_user`) is refused on every type, and it is judged FIRST, before the type's own rule: the refusal gives the token's reason (a blueprint default is a literal), never a refused type's reason, which is false of a token. - **Every refusal is one `custom` issue at the field's `defaultValue`.** It names the field and its type (`Field "is_done" (boolean): …`), says why, and prescribes the fix. A non-admitted type's refusal lists the types that take a default. - **Both describes** say the value becomes the built field's `defaultValue`, give the admitted list (built from the table, so describe and refusal read the same list), and state the boolean-flag rule: a 是否完成 / 已付款 / `is_active` flag is a `boolean` field that is never `required` and carries `defaultValue: false`. Each side uses its own spelling of an unset `required` ("omit `required` or set it false" / "`required` null or false"). - **Generated artifacts:** `content/docs/references/ai/solution-blueprint.mdx` (`gen:docs`, two rows), `packages/spec/authorable-surface/ai.json` (one key). `packages/spec/dropped-refinements.baseline.json` gains three hand-edited sites (`ai/BlueprintField` root, `ai/BlueprintObject` `fields.element`, `ai/SolutionBlueprint` `objects.element.fields.element`) and its `droppedRefinementSites` total moves 1020 → 1023: the option-membership rule reads a sibling key, so it has no JSON Schema form. - **Changeset** `30584-blueprint-field-default-value.md`: `@objectstack/spec` minor. ## Tests A new `describe` block, "blueprint field `defaultValue`", at the end of `packages/spec/src/ai/solution-blueprint.test.ts`, 12 cases: - a boolean with `defaultValue: false` parses and the key survives the parse (own-property check); - the admitted set is exactly the declared one: every `FieldType` member is tried, the 21 admitted take their value, each of the 28 others is refused with one by-name issue; - a wrong-typed value on each admitted type is refused, one issue, by name (a string on a boolean, a number on text, `"9am"` on time, a label `"Open"` on a select); - a select default that is not one of its OWN option values is refused, including a value another field of the same object declares, and a radio with no options; - a runtime token on an admitted type is refused; - a runtime token on a type that takes no default (`user` / `lookup` + `current_user`, `date` + `NOW()`, `datetime` + `now()`) is refused with one issue that gives the token's reason, says the type takes no defaultValue in a blueprint and ends `Remove defaultValue from this field.`, and never the type's own reason (round 1); - an array default on a `multiselect` is refused by name (beside the key's own union issue); - an absent value parses (lenient); the strict mirror takes null and refuses the key missing, at the key's path; - the strict mirror accepts what the lenient schema accepts for each admitted type, and null; - VALUE parity for a key whose rule spans `type`: 49 types × 9 values the strict union accepts; the lenient schema either keeps the value byte-equal or refuses it with exactly one issue that names the type at this key, never strips it; - every value the blueprint admits is one `FieldSchema` admits on the built field; - both describes name every admitted type and state the boolean-flag rule. Seven existing strict fixtures gained `defaultValue: null` (strict mode needs every key), including the leading-digit pin, whose "the only defect left is the leading digit" claim needs it. ## Acceptance notes - **The strict mirror's shape: `z.union([z.boolean(), z.number(), z.string()]).nullable()`.** Strict structured output states a rule per key and cannot read `type` while it judges this one, so the mirror cannot say which arm a type takes. The per-type check lives on the lenient field, which `apply_blueprint` parses, following the precedent this file already sets for the view filter rule's operator ↔ value refinement. The mirror can therefore generate a value the lenient schema refuses (a string on a boolean), and the cross-product pin holds every such refusal to one by-name issue at the key. The existing identifier VALUE-parity walker does not walk union leaves and is unchanged and green. - **The lenient key is the same scalar union, not `z.unknown()`** like `FieldSchema.defaultValue`. The published JSON Schema then types the key as the scalars every admitted type draws from, rather than as anything. The price: the union's `invalid_union` aborts before an object refinement runs by default, so the refinement runs with `when: () => true` (the `view.zod.ts` / `analytics-carrier-filter.ts` precedent), and an array or object default gets the union's issue AND the by-name one. A lenient `null` is refused by the union, as on every other lenient key. - **The admitted types**, grounded in `data/field-value.zod.ts`'s value classes, the ones `valueSchemaFor` answers for a built field's literal default: `BOOLEAN_VALUE_TYPES` (boolean, toggle); `NUMERIC_VALUE_TYPES` minus `summary` (number, currency, percent, rating, slider, progress); `STRING_VALUE_TYPES` minus `password`, `secret`, `signature`, `qrcode` (text, textarea, email, url, phone, markdown, html, richtext, code, color); `CLOCK_TIME_TYPES` (time, `HH:MM[:SS]`, a wall-clock literal that does not age); `SINGLE_OPTION_TYPES` (select, radio), narrowed to the field's own option values (with no options, `valueSchemaFor` would take any string). - **Refused by name, even where `FieldSchema` takes a default:** `date` / `datetime` (a literal day is frozen at design time; "today" is the `NOW()` token, an instruction rather than a value); `lookup` / `master_detail` / `tree` / `user` and the five file types (the value is the id of a record or file that does not exist while the blueprint is designed; seed data is reported, never applied); `multiselect` / `checkboxes` / `tags` (an array, and this key carries one scalar); `formula` / `summary` / `autonumber` (computed, never read); `password` / `secret` (one credential on every record); `signature` / `qrcode` (made per record); the seven structured JSON types. The brief's example names a `checkbox` type: `FieldType` has none. `checkboxes` is the multi-option type and is refused with the other list types. Widening any of these later is additive; narrowing would be breaking, so the first cut is the narrow one. - **The boolean-flag sentence** lives in `defaultValue`'s describe on both sides. `required`'s describe is unchanged: no existing key's shape, default or describe moves. - **Liveness: no row, measured.** `check:liveness` at base `14c67d44` prints 43 governed types (33 authorable plus 10 via `SPEC_ONLY_SCHEMAS`); no blueprint shape is among them, because a blueprint is never persisted and is not a metadata type. The card's Acceptance asks for a `planned` row; that is a deviation with this reading. Carrier for "planned until the cloud half lands": cloud#3619 / cloud#3633 (the build applier that carries the value into the built field and admits the key in its undeclared-key refusal). Once carried, the value is the built field's `defaultValue`, governed by `liveness/field.json` `props.defaultValue` (`live`, `packages/objectql/src/engine.ts`). - **Consumers in this repo:** `packages/lint/src/validate-expressions.formula-one-hop.test.ts` reads `BlueprintFieldSchema.shape` and the strict field shape; the refined schema is still a `ZodObject`, and the file is 13/13 after a lint closure build. `ui/packages/plugin-chatbot/src/mapMessages.ts` names `BlueprintFieldSchema` only in a comment: it lifts each field's `name` / `label` from the plan envelope and imports no blueprint schema, so it neither reads nor drops the new key. `packages/spec/src/type-alias-convention.pin.test.ts` (input = output for both schemas) is green in the spec suite. The producers and readers that move on the new key (cloud's blueprint tools and build applier) are in objectstack-ai/cloud, outside this repo. - The ledger's `measured` header also carries `refinementSitesThatDidProject: 369` and `zod: "4.4.3"`, which already differ from what the build prints at base (714, zod 4.6.7). No test reads them; left alone. - **Round 1** (contract review 6670092320, PASS, one should-fix, adopted by the seat). `refuseUnfitDefaultValue` judged the type before the token, so `{ type: 'user', defaultValue: 'current_user' }` read "no record exists while the blueprint is designed" and `{ type: 'date', defaultValue: 'NOW()' }` read "a literal date is fixed when the blueprint is written", both false of a token. The `isRuntimeDefaultToken` check now runs first for every type in the table. An admitted type keeps its message (`… default is ${rule.takes}; write one, or remove defaultValue.`); a refused type says the value is a runtime token, that a blueprint default is a literal, that the type takes no defaultValue in a blueprint (with the admitted list), and ends `Remove defaultValue from this field.`; `rule.takes` is not read there. Still refused, no token admitted. One new pin; ablation 3 below. `origin/main` merged in (a merge commit, not a rebase). The describe's "carried into the object field" sentence stays as written: it waits on cloud#3633 by the ruled sequencing. - Branch is the harness-assigned `claude/project-thread-7e1clu`, not `claude/issue-30584-*`; claim 6668151062 names it. ## Verification Readings on `objectstack-ai/objectstack`. Round 1 head `32b39007` (fix `80d143dc`, then a merge of `origin/main` at `66a8822b`; merge base for the change set `66a8822b`). Round 0 head `13a078c8` (base `14c67d44`); its readings are kept below where marked. <details><summary>Repro at base 14c67d4</summary> `SolutionBlueprintSchema.safeParse` of a blueprint whose boolean field carries `defaultValue: false` → `success: true`, parsed field keys `[name, type]`, the key stripped. The strict mirror strips it too. </details> <details><summary>Ablation 1 (round 0, at 13a078c) — the type check detached</summary> `node scripts/ablation-replace.mjs --file packages/spec/src/ai/solution-blueprint.zod.ts --anchor '}).superRefine(refuseUnfitDefaultValue, { when: () => true }));' --replacement '})); // ABLATION: type check detached'`: anchor 1 → 0, blob 1663954 → cdf6df65. `src/ai/solution-blueprint.test.ts` → `Tests 5 failed | 91 passed (96)`: the admitted-set, wrong-type, own-option, token and multiselect pins red. Restored: blob == HEAD (1663954), `git diff HEAD` empty. (The never-stripped cross-product pin stays green here by design: with no check the lenient side keeps every value.) </details> <details><summary>Ablation 2 (round 0, at 13a078c) — the lenient key renamed (the strip)</summary> Same tool, anchor ` defaultValue: z.union([z.boolean(), z.number(), z.string()]).optional()` → ` ablationStripped: …`: blob 1663954 → f7c41b8f. → `Tests 9 failed | 87 passed (96)`: the field key-parity pin, the key-survives pin, the never-stripped cross-product pin and the describe pin red with the refusal pins. Restored: blob == HEAD, `git diff HEAD` empty. </details> <details><summary>Ablation 3 (round 1) — the old type-before-token order restored</summary> `node scripts/ablation-replace.mjs --file packages/spec/src/ai/solution-blueprint.zod.ts --anchor ' // A token is judged before the type. On a type that takes no default, that' --replacement` the round-0 `if (!('takes' in rule)) { … return; }` block placed back ahead of the token check: anchor 1 → 0, blob b490c57 → 234c4bbe. `src/ai/solution-blueprint.test.ts` → `Tests 1 failed | 96 passed (97)`: the new pin "refuses a runtime token on a type that takes no default with the token's reason, never the type's" red. Restored: blob == HEAD (b490c57), `git diff HEAD` empty. </details> <details><summary>Suites</summary> Round 1, at `32b39007`: - Full build after the merge, `turbo run build --filter=!@objectstack/docs --concurrency=3` under the lock: `118 successful, 118 total`, exit 0; `git status` clean afterwards. - `src/ai/solution-blueprint.test.ts`: 97 passed (97). - `pnpm --filter @objectstack/spec test` under the lock: `Test Files 817 passed (817)`, `Tests 25786 passed | 1 todo (25787)`, command-exit 0. - `pnpm --filter @objectstack/spec typecheck`: exit 0; `check:test-typecheck` OK, 52 file(s) / 240 error(s) / 131 pinned signature(s), unchanged. (A first run without a raised heap died on a JavaScript heap out-of-memory inside `tsc -p tsconfig.test.json`; rerun with `NODE_OPTIONS=--max-old-space-size=6144`, green.) - `pnpm --filter @objectstack/spec run check:generated`: exit 0, every artifact current (same heap note: the first run's only red was `check:test-typecheck` on that out-of-memory, not a stale artifact). - `eslint` on the two changed source files: exit 0, no output. `node scripts/check-nul-bytes.mjs`: OK. Round 0, at `13a078c8`: - `pnpm --filter @objectstack/spec test` (under `os-verify-lock.sh`) at `13a078c8`: `Test Files 817 passed (817)`, `Tests 25750 passed | 1 todo (25751)`, command-exit 0. The first run at `5d57ab6a` was 1 red: `scripts/dropped-refinements.test.ts` pins the ledger's `droppedRefinementSites` total (expected 1023, read 1020); `13a078c8` moves the total. - `src/ai/solution-blueprint.test.ts` alone: 96 passed (96). - `pnpm --filter @objectstack/spec typecheck`: exit 0; `check:test-typecheck` OK, 52 file(s) / 240 error(s) / 131 pinned signature(s), unchanged. - `pnpm --filter @objectstack/spec run check:generated`: exit 0, every entry current (the first run before the regen commit was red on `check:docs` only; `--fix` regenerated the reference page, committed in `5d57ab6a`). - `eslint` on the two changed source files: exit 0, no output. - `packages/lint/src/validate-expressions.formula-one-hop.test.ts` (reads both blueprint field shapes): 13 passed (13), after a lint closure build. - Full build, `turbo run build --filter=!@objectstack/docs --concurrency=3` under the lock: `118 successful, 118 total`, exit 0; `git status` clean afterwards (no generated file moved). </details> <details><summary>Derived gates</summary> Round 1: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `32b39007`: 102 derived families, the same list as round 0. Each was run on that head after the post-merge build, with its exit code written to disk before any pipe (`cmd :: exit N`), then reconciled: `node scripts/pm/dispatch-gates.mjs --ran r1-ran.list --repo objectstack-ai/objectstack` → `Run reconciliation — 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN`, exit 0. All 102 exited 0 on the first run; no prerequisite refusal. `check:adr-0087-registration --base origin/main` on the committed head: exit 0 (no declared-breaking changeset). Changed lines 450 (+441 / -9, 6 files) against merge base `66a8822b`, under the 3000-line threshold. Round 0: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `13a078c8`: 102 derived families. Each was run on that head with its exit code written to disk before any pipe (`cmd :: exit N`), then reconciled: `node scripts/pm/dispatch-gates.mjs --ran ran-final.list --repo objectstack-ai/objectstack` → `Run reconciliation — 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN`, exit 0. Every one of the 102 exited 0. Six of them first exited 3 (PREREQUISITE NOT MET, built output missing): `check:doc-formula-expressions`, `check:doc-security-posture` (lint dist), `check:skill-examples` (client-react / client declarations), `check:docs-transcript-drift` (lint dist), `check:dual-build-cjs-loads` (several dists), `check:lean-entry-closure` (objectql dist). After the full build above, all six reran with exit 0; those are the readings counted. `check:adr-0087-registration --base origin/main` ran on the committed head: exit 0. Changed lines 430 (+421 / -9, 6 files), under the 3000-line threshold. </details> Dogfood battery: n/a — no `!` in the title and no `(narrowing)` arm. --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgUCEUicLCQ4tkXayK2)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgUCEUicLCQ4tkXayK2 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 788715e commit 4bf9a2d

6 files changed

Lines changed: 441 additions & 9 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a blueprint field carries a typed `defaultValue`, so an AI-built boolean flag defaults to false
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
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.
10+
11+
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.
12+
13+
- **`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`.
14+
- **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.
15+
- **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.
16+
- 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.
17+
18+
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.

‎content/docs/references/ai/solution-blueprint.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ const result = BlueprintAppSchema.parse(data);
113113
| **options** | `{ label: string; value: string; kind?: Enum<'open' \| 'won' \| 'lost'> }[]` | optional | Choices for select / multiselect / radio fields |
114114
| **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. |
115115
| **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. |
116+
| **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. |
116117

117118
### Allowed Values: `BlueprintField.type`
118119

@@ -229,6 +230,7 @@ const result = BlueprintAppSchema.parse(data);
229230
| **options** | `{ label: string; value: string; kind?: Enum<'open' \| 'won' \| 'lost'> }[]` | optional | Choices for select / multiselect / radio fields |
230231
| **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. |
231232
| **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. |
233+
| **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. |
232234

233235

234236
---

‎packages/spec/authorable-surface/ai.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"ai/BlueprintDashboard:label",
4747
"ai/BlueprintDashboard:name",
4848
"ai/BlueprintDashboard:widgets",
49+
"ai/BlueprintField:defaultValue",
4950
"ai/BlueprintField:expression",
5051
"ai/BlueprintField:label",
5152
"ai/BlueprintField:name",

‎packages/spec/dropped-refinements.baseline.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"measured": {
44
"zod": "4.4.3",
55
"publishedSchemasWithDroppedRefinements": 252,
6-
"droppedRefinementSites": 1020,
6+
"droppedRefinementSites": 1023,
77
"refinementSitesThatDidProject": 369,
88
"refinementSitesWithNoJsonFormToCompare": 0
99
},
@@ -16,11 +16,13 @@
1616
},
1717
"ai/BlueprintField": {
1818
"sites": [
19+
"",
1920
"summaryOperations.filter.lazy"
2021
]
2122
},
2223
"ai/BlueprintObject": {
2324
"sites": [
25+
"fields.element",
2426
"fields.element.summaryOperations.filter.lazy"
2527
]
2628
},
@@ -46,6 +48,7 @@
4648
},
4749
"ai/SolutionBlueprint": {
4850
"sites": [
51+
"objects.element.fields.element",
4952
"objects.element.fields.element.summaryOperations.filter.lazy",
5053
"views.element.filters.element"
5154
]

0 commit comments

Comments
 (0)