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
12 changes: 12 additions & 0 deletions .changeset/30430-blueprint-widget-aggregate-calendar-date.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/spec': minor
---

feat(spec): a blueprint dashboard widget says how it aggregates, and a blueprint calendar view says which date field it schedules on

Clause-②: yes (widening: an optional `aggregate` on the blueprint dashboard widget and an optional `startDateField` on the blueprint view)
Metadata-protocol: added

- **Widget `aggregate`.** `BlueprintDashboardSchema` widgets and the strict mirror `SolutionBlueprintStrictSchema.dashboards[].widgets[]` gain `aggregate: 'sum' | 'avg' | 'min' | 'max' | 'count'` (optional on the lenient side, required-but-nullable on the mirror). Before this, a widget could only name its `measure` and the reader picked the aggregation from the field type, so a widget meant to average a number field summed it. `sum`, `avg`, `min` and `max` aggregate the `measure` field; `count` counts records and needs no `measure` field (a field named beside it is not read). `measure`'s describe no longer says the aggregation is automatic, and neither side tells the model to infer anything from the widget title.
- **View `startDateField`.** `BlueprintViewSchema` and the strict mirror's `views[]` gain `startDateField`, a snake_case field name, carried into the built view as `calendar.startDateField`. It is read on a `calendar` view and ignored on the other view types, not refused: the strict mirror the design model writes through cannot state a cross-key rule, so a refusal on the lenient side alone would refuse at apply a proposal the model was allowed to generate.
- Both keys are optional on the lenient schema, so every existing blueprint parses unchanged. The strict mirror is only the design model's structured-output contract; a model generating against it now states both keys, `null` when it has nothing to say.
6 changes: 5 additions & 1 deletion content/docs/references/ai/solution-blueprint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ const result = BlueprintAppSchema.parse(data);
| **title** | `string` | optional | Widget title |
| **object** | `string` | optional | Source object for the widget |
| **chart** | `Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'>` | optional | Widget visualization |
| **measure** | `string` | optional | The field this widget aggregates (e.g. "amount", "probability"), or "count" to count records. The aggregation is chosen automatically from the field type — a money field SUMs, a percentage/rate AVERAGEs — so name the FIELD, not "total_amount". A "total revenue" widget sets measure:"amount"; an "average win rate" widget sets measure:"win_rate"; a "number of deals" widget sets measure:"count". Omit to let the builder infer from the title. |
| **measure** | `string` | optional | The field this widget aggregates (e.g. "amount", "score"), or "count" to count records. Name the FIELD, not "total_amount" or "avg_score": how it is aggregated is what `aggregate` says, never this key. A "total revenue" widget sets measure:"amount" with aggregate:"sum"; an "average interview score" widget sets measure:"score" with aggregate:"avg"; a "number of deals" widget sets aggregate:"count" and needs no measure. measure:"count" is the legacy spelling of a record count: write aggregate:"count" instead. Beside "sum", "avg", "min" or "max", measure:"count" names no field to aggregate, so that widget is incomplete: a reader must refuse it, never sum, average or rank a record count. Omit for a count widget. |
| **aggregate** | `Enum<'sum' \| 'avg' \| 'min' \| 'max' \| 'count'>` | optional | How the widget aggregates its records: "sum" = the total of the `measure` field; "avg" = its average; "min" / "max" = its smallest / largest value; "count" = the number of records the widget covers (after its `condition`). "sum", "avg", "min" and "max" need `measure` to name a numeric field of the widget's object. "count" needs no `measure` field: it counts records, and a field named in `measure` beside it is not read. Say what the widget computes: an average → "avg", a total → "sum", a highest / lowest value → "max" / "min", a number of records → "count". SET it on every widget that aggregates; omitted, the reader falls back to a default of its own, which may not be the average or extreme a widget means. |
| **groupBy** | `string` | optional | The field to break the widget down by — the category or time axis (e.g. "stage", "created_at"). A "by status" chart MUST set this to the status field; the title and this field MUST name the SAME field. Omit for a single-number metric. |
| **condition** | `{ field: string; op: Enum<'lt' \| 'lte' \| 'gt' \| 'gte' \| 'eq' \| 'ne'>; value: number \| string \| boolean }` | optional | Restrict WHICH records the widget counts/aggregates when its title implies a threshold or status (e.g. "stock below 10" → `{field:"stock_quantity", op:"lt", value:10}`; "open tickets" → `{field:"status", op:"eq", value:"open"}`). Without it the widget covers ALL records — so a "低于10的备件预警" / "overdue" card would wrongly count everything. Omit when the widget genuinely spans every record. |

Expand Down Expand Up @@ -282,6 +283,7 @@ const result = BlueprintAppSchema.parse(data);
| **groupBy** | `string` | optional | REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows). |
| **filters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Which records this view shows: the list-view filter rules [`{ field, operator, value }`] the built view carries, ANDed. REQUIRED whenever the view's name or label states a condition — an 「overdue / 逾期」, 「my open / 我的未完成」, 「待审批」 or 「本月」 view MUST carry the rules that make its name true, e.g. overdue → [`{ field: "due_date", operator: "less_than", value: "{today}" }`, `{ field: "status", operator: "not_equals", value: "completed" }`]; my open → [`{ field: "owner", operator: "equals", value: "{current_user_id}" }`, `{ field: "status", operator: "equals", value: "open" }`]. WITHOUT it the view shows EVERY record under a name that promises a subset. For a select field compare its option VALUE, never its label. Omit for a view that genuinely spans every record. |
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Default record order: [`{ field, order }`] with order "asc" or "desc"; the first entry sorts first (e.g. an overdue list → [`{ field: "due_date", order: "asc" }`]). Omit for the platform default order. |
| **startDateField** | `string` | optional | For type:"calendar" only: the date or datetime field of THIS view's object that places each record on the calendar (e.g. "interview_date", "due_date"), carried into the built view as `calendar.startDateField`. snake_case, naming one of the object's own fields. SET it on every calendar view; without it the builder binds a date field of its own choosing, which may not be the one meant. Ignored on the other view types (a gantt view takes its dates from `columns`, start before end). |

### Nested Shape: `BlueprintView.filters[number]`

Expand Down Expand Up @@ -357,6 +359,7 @@ Sort field and direction pair
| **groupBy** | `string` | optional | REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows). |
| **filters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Which records this view shows: the list-view filter rules [`{ field, operator, value }`] the built view carries, ANDed. REQUIRED whenever the view's name or label states a condition — an 「overdue / 逾期」, 「my open / 我的未完成」, 「待审批」 or 「本月」 view MUST carry the rules that make its name true, e.g. overdue → [`{ field: "due_date", operator: "less_than", value: "{today}" }`, `{ field: "status", operator: "not_equals", value: "completed" }`]; my open → [`{ field: "owner", operator: "equals", value: "{current_user_id}" }`, `{ field: "status", operator: "equals", value: "open" }`]. WITHOUT it the view shows EVERY record under a name that promises a subset. For a select field compare its option VALUE, never its label. Omit for a view that genuinely spans every record. |
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Default record order: [`{ field, order }`] with order "asc" or "desc"; the first entry sorts first (e.g. an overdue list → [`{ field: "due_date", order: "asc" }`]). Omit for the platform default order. |
| **startDateField** | `string` | optional | For type:"calendar" only: the date or datetime field of THIS view's object that places each record on the calendar (e.g. "interview_date", "due_date"), carried into the built view as `calendar.startDateField`. snake_case, naming one of the object's own fields. SET it on every calendar view; without it the builder binds a date field of its own choosing, which may not be the one meant. Ignored on the other view types (a gantt view takes its dates from `columns`, start before end). |

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

Expand Down Expand Up @@ -423,6 +426,7 @@ Sort field and direction pair
| **groupBy** | `string \| null` | ✅ | REQUIRED for kanban: the select/status field whose options become the board columns (e.g. "stage"). Optional for gantt (groups leaf tasks). Null for list/form/calendar/gallery. |
| **filters** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value: string \| number \| boolean \| (string \| number)[] \| null }[] \| null` | ✅ | Which records this view shows: list-view filter rules [`{field, operator, value}`], ANDed, or null for a view over EVERY record. REQUIRED whenever the view's name or label states a condition — an 「overdue / 逾期」, 「my open / 我的未完成」, 「待审批」 or 「本月」 view MUST carry the rules that make its name true, e.g. overdue → [`{field:"due_date",operator:"less_than",value:"{today}"}`,`{field:"status",operator:"not_equals",value:"completed"}`]; my open → [`{field:"owner",operator:"equals",value:"{current_user_id}"}`,`{field:"status",operator:"equals",value:"open"}`]. Null under such a name ships a view that shows every record while its name promises a subset. |
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[] \| null` | ✅ | Default record order: [`{field, order}`] with order "asc" or "desc"; the first entry sorts first (e.g. an overdue list → [`{field:"due_date",order:"asc"}`]), or null for the platform default order. |
| **startDateField** | `string \| null` | ✅ | For type:"calendar" only: the date or datetime field of THIS view's object that places each record on the calendar (e.g. "interview_date", "due_date"), carried into the built view as `calendar.startDateField`. snake_case, naming one of the object's own fields. SET it on every calendar view; without it the builder binds a date field of its own choosing, which may not be the one meant. Ignored on the other view types (a gantt view takes its dates from `columns`, start before end). Null on every view that is not a calendar. |

### Nested Shape: `SolutionBlueprintStrict.dashboards[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 @@ -81,6 +81,7 @@
"ai/BlueprintView:name",
"ai/BlueprintView:object",
"ai/BlueprintView:sort",
"ai/BlueprintView:startDateField",
"ai/BlueprintView:type",
"ai/BlueprintWidgetCondition:field",
"ai/BlueprintWidgetCondition:op",
Expand Down
Loading
Loading