Repository navigation
Commit d2e2f6f
feat(spec): blueprint dashboard widgets say how they aggregate, and calendar views which date field they schedule on (#30484)
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBUrCRf8HiH2JEanDW9gVbCZ)_
Fixes #30430
Ruling-ref: objectstack-ai/cloud#3607 (row 2 A, record 6656298548; the
calendar key B)
Clause-②: yes (widening: an optional `aggregate` on the blueprint
dashboard widget and an optional `startDateField` on the blueprint view)
Metadata-protocol: added
Skills-docs: none owed — no skill, eval or doc teaches a blueprint
widget or view key (`git grep -n
"BlueprintDashboard\|BlueprintView\|measure:\"count\"\|propose_blueprint"
-- skills content/docs ':!content/docs/references'
':!content/docs/releases'`: 1 hit,
`content/docs/frontend/plugins/plugin-chatbot.mdx:494`, which describes
the plan card's `planCard` result and teaches no widget or view key;
control `git grep -c "solution-blueprint" --
skills/objectstack-ai/references/_index.md`: 1, an index pointer to the
schema file). The generated reference
`content/docs/references/ai/solution-blueprint.mdx` is regenerated here.
The skills lane is paused; nothing is owed to it from this PR.
Prior rulings read: blueprint,aggregate,startdatefield → 17 hits;
ADR-0021 D3, ADR-0033 Decision §10, ADR-0033 Decision §5, ADR-0033
Decision §6, ADR-0033 Decision §8, ADR-0045 Decision §1, ADR-0045
Decision §4, ADR-0045 Decision §8, ADR-0060 D6; thread: none; repo:
objectstack-ai/objectstack
Carried: objectstack-ai/cloud#3607's cloud half takes two acceptance
items from this PR. (1) A widget with `aggregate` `sum`, `avg`, `min` or
`max` whose `measure` is absent, `"count"` or names a non-numeric field
is refused by the compile for that widget, naming it, and is never
summed, averaged or ranked as a record count. (2) `startDateField` is
read on a `calendar` view only and ignored on every other view type; on
a calendar view it binds the calendar's `calendar.startDateField`, and a
value that names no date or datetime field of the view's object is
refused naming the field, never silently replaced by the first declared
date field.
## 维护者速读
- 改了什么:AI 蓝图的仪表盘组件新增可选 `aggregate`(`sum` / `avg` / `min` / `max` /
`count`),蓝图视图新增可选 `startDateField`(日历视图按哪个日期字段排程,对应建出视图的
`calendar.startDateField`)。宽松 schema 与给模型用的严格镜像两侧同时加,严格侧可为
null。`measure` 的说明改为「只写字段,怎么聚合看 `aggregate`」,删掉了「按字段类型自动选」和「从标题推断」两句,并标明
`measure: "count"` 是旧写法、应写 `aggregate: "count"`。
- 为什么改:执行 cloud#3607 两条裁决(行 2 选 A、日历键选 B)。原来组件只能写
`measure`,平台按字段类型自动聚合,数字字段一律求和,「平均面试评分」显示成 12 个分数之和 908;日历视图也无处写日期字段。
- 风险与回滚:纯增可选键,旧蓝图原样解析;严格镜像只是模型的结构化输出契约,模型此后每个组件 / 视图都要给出这两个键(可为
null)。本仓无读者,读者是 cloud 半(cloud#3607 跟进)。回滚即还原本 PR。
- 先看:`packages/spec/src/ai/solution-blueprint.zod.ts` 顶部新增的
`WIDGET_AGGREGATE_MEANING` / `VIEW_START_DATE_FIELD_RULE`
两段说明文字(模型读的就是它们),以及组件 `measure` / `aggregate`
两行;`packages/spec/src/ai/solution-blueprint.test.ts` 末尾新增的 describe 块。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/ai/solution-blueprint.test.ts`,预期 85 例全绿(含五个聚合值两侧都收、未知值两侧都拒、非日历视图上的
`startDateField` 被接受而不读)。
- 需要你做的:无,席位落地。
## What changes
- **`@objectstack/spec`
(`packages/spec/src/ai/solution-blueprint.zod.ts`).**
- Widget `aggregate: 'sum' | 'avg' | 'min' | 'max' | 'count'`:
`.optional()` on the lenient `BlueprintDashboardSchema` widget,
`.nullable()` (key required) on the strict mirror
`SolutionBlueprintStrictSchema.dashboards[].widgets[]`. Both sides spell
one module-local tuple `BLUEPRINT_WIDGET_AGGREGATES` and read one
meaning sentence `WIDGET_AGGREGATE_MEANING`, so the two describes cannot
drift.
- Widget `measure`: both describes rewritten through one shared
`WIDGET_MEASURE_RULE`. Removed: "The aggregation is chosen automatically
from the field type", "Omit to let the builder infer from the title"
(lenient) and "or null to infer from the title" (strict). Kept: name the
FIELD, not `total_amount`. `measure: "count"` is now marked as the
legacy spelling of a record count, with `aggregate: "count"` the one to
write, and the describe says what a reader does with `measure: "count"`
beside `sum`/`avg`/`min`/`max` (refuses the widget).
- View `startDateField`: a snake_case identifier, `.optional()` on
`BlueprintViewSchema`, `strictIdentOrNull` on the strict view (so the
VALUE-parity walker sees the same `SNAKE_CASE` pattern on both sides).
One shared describe, `VIEW_START_DATE_FIELD_RULE`.
- **Regenerated:** `packages/spec/authorable-surface/ai.json`
(+`ai/BlueprintView:startDateField`; the widget is a nested shape under
`ai/BlueprintDashboard:widgets`, so `aggregate` adds no row) and
`content/docs/references/ai/solution-blueprint.mdx`. `api-surface/` is
unchanged: no export was added (the tuple and the three describe
constants are module-local).
- **Changeset:** `@objectstack/spec` minor, with both declaration lines.
### The `count` rule
`aggregate: 'count'` counts the records the widget covers (after its
`condition`) and needs no `measure` field. A field named in `measure`
beside it is **not read**: the count is of records, not of a field's
non-empty values. `measure: "count"` is the legacy spelling of a record
count and still means it; the describe marks it legacy and says to write
`aggregate: "count"`. Beside `sum`, `avg`, `min` or `max`, `measure:
"count"` names no field to aggregate, and the describe says a reader
must refuse that widget, never sum, average or rank a record count.
`aggregate: 'sum' | 'avg' | 'min' | 'max'` need `measure` to name a
numeric field of the widget's object; the describe says so. With
`measure` absent or `"count"` there is no field to aggregate, and **the
spec does not refuse that pairing**. Reason: the strict mirror the
design model generates against is per-leaf JSON Schema and cannot state
a cross-key rule, so a refusal on the lenient side alone would make a
proposal legal to generate and illegal to apply, which is the cloud#1967
failure this file already documents (an approved blueprint refused
wholesale at apply). A lenient-side `.superRefine` would also leave the
published JSON Schema without the rule (a dropped refinement). The
reader, cloud#3607's compile ("the compile requires it"), is where the
pairing is judged per widget.
### The non-calendar choice: ignored, not refused
`startDateField` is read on a `calendar` view and **ignored** on `list`,
`form`, `kanban`, `gallery` and `gantt`. Reasons: (1) the same
strict-mirror limit as above, since a type-scoped refusal is a cross-key
rule; (2) the view already has a kind-scoped key that other kinds
ignore, `groupBy` ("REQUIRED for kanban … Null for
list/form/calendar/gallery"), so this follows the shape's own precedent.
The describe states it ("Ignored on the other view types (a gantt view
takes its dates from `columns`, start before end)"), and the strict
describe adds "Null on every view that is not a calendar." The key name
is the target's own leaf, `calendar.startDateField`, flattened as
`groupBy` is.
### Liveness
`packages/spec/liveness/` does **not** govern these keys, so no row is
added. The ledger's denominator is the registered metadata kinds plus
`listUnregisteredKindSchemaTypes()` and the `SPEC_ONLY_SCHEMAS`
overrides (`packages/spec/liveness/README.md` § Source of truth); the
solution blueprint is none of them (`grep -il blueprint
packages/spec/liveness/*.json`: 0 files). It is the AI design tool's I/O
contract, not stored metadata. The consumer is cloud:
objectstack-ai/cloud#3607's cloud half (the designer emits both keys,
the compile reads them).
### Readers in this repo
None. `git grep -n
"BlueprintDashboard\|BlueprintView\|solution-blueprint" packages`
outside the schema file hits only its test, `ai/index.ts` (re-export),
generated ledgers, CHANGELOGs and one lint test that reads the strict
mirror's object shape
(`packages/lint/src/validate-expressions.formula-one-hop.test.ts:128`,
not the view or widget).
## Tests
`packages/spec/src/ai/solution-blueprint.test.ts`:
- New `describe('blueprint widget \`aggregate\` and calendar view
\`startDateField\`')`: each of the five values accepted on both sides;
`count` with no `measure` on both sides; `average`, `median`,
`count_distinct`, `AVG` and `''` refused on both sides with exactly one
issue at `dashboards.0.widgets.0.aggregate`; absent parses on the
lenient side, `null` parses and a missing key is refused on the strict
side; no `measure` / `aggregate` describe on either side says "infer",
"chosen automatically" or "title"; `startDateField` on a calendar view
accepted on both sides and absent still parses; `42`, `true`, an array,
an object and `'Interview Date'` refused at the key on both sides, and a
missing key refused on the strict side; the key accepted on each of the
five non-calendar types on both sides.
- Key parity: a new `the DASHBOARD and WIDGET schemas carry exactly the
same keys` pin (no widget-level parity pin existed), with the
`aggregate` enum's options compared across sides; the VIEW pin now names
`startDateField`.
- Existing strict fixtures gain the two required keys (`aggregate`,
`startDateField`); the VALUE-parity test is unchanged and green with
both new keys on both sides.
## Verification
Head `e16fea95` (branch off `origin/main` `c5800818`; review round 1
changed only the `measure` / `aggregate` describes, the
no-title-inference pin and the regenerated reference page), under
`os-verify-lock`, `NODE_OPTIONS=--max-old-space-size=4096`:
- `pnpm build` (full turbo build): Tasks 119 successful, 119 total,
VERDICT command-exit 0.
- `pnpm --filter @objectstack/spec test`: Test Files 814 passed (814),
Tests 25610 passed, 1 todo; `pnpm --filter @objectstack/spec typecheck`:
exit 0.
- `src/ai/solution-blueprint.test.ts` alone: 85 passed (85) at
`e16fea95`. The reverse verification below ran at `4960c1d3`; round 1
touched neither ablated line.
- `check-changeset-no-major` with this body as the event: exit 0 on
`Metadata-protocol: added`; the control with `none` exits 1 (the
authorable-surface baseline moved).
<details><summary>Reverse verification (two mutations through
scripts/ablation-replace.mjs, each restored to the HEAD blob
1d11f8e)</summary>
1. The strict widget key renamed (`aggregate:
z.enum(BLUEPRINT_WIDGET_AGGREGATES).nullable()` → `aggregate_ablated:
…`, anchor 1 → 0, blob `1d11f8e14420` → `82c3c8c15327`): **11 failed |
74 passed (85)**: the widget key-parity pin, the five accept cases, the
count, unknown-value, absent/null/missing and no-title-inference pins,
and the existing strict widget fixture. Restored: blob == HEAD, `git
diff HEAD` empty.
2. The lenient view key renamed (`startDateField:
z.string().regex(SNAKE_CASE).optional()` → `startDateField_ablated: …`,
blob `1d11f8e14420` → `45bbf04c1b37`): **3 failed | 82 passed (85)**:
the VIEW key-parity pin, the calendar accept pin and the wrong-type
refusal pin. The non-calendar pin stays green, as expected (an unknown
key is stripped, so the view still parses). Restored: blob == HEAD, `git
diff HEAD` empty.
The spec tests import the schema from `src`, so no rebuild was needed
for either leg.
</details>
<details><summary>Gates: dispatch-gates --commands, 102 derived at
e16fea9, 102 run, 0 NOT-MEASURED</summary>
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran gates-ran.list`: "102 derived famil(ies) accounted for — 102 run,
0 NOT-MEASURED", exit 0. Every line `exit 0`, including
`check-changeset-no-major`, `check:authorable-surface`,
`check:api-surface`, `check:generated`, `check:liveness`,
`check:empty-state`, `check:docs`, `check:llms-txt`,
`check:spec-parsed-alias`, `check:dual-build-cjs-loads` and
`check:nul-bytes`.
- At `4960c1d3` one prerequisite exited 3: `check-plugin-teardown-shape
--self-test` on the shallow clone (its pinned fixture commit `621a4876`
absent); after `git fetch origin 621a487…` it exits 0 (48 cases). At
`e16fea95` all 102 exited 0 on the first run.
- No `!` in the title and no `(narrowing)` arm: no full dogfood run. CI
owns the rest of the farm.
</details>
## Acceptance notes
- Absent `aggregate` leaves the reader's own default in place. Neither
describe states what that default is (lenient: "the reader falls back to
a default of its own, which may not be the average or extreme a widget
means"; strict: "or null for the reader's own default"), because this
repo cannot read cloud's default table. Both tell the model to set the
key on every widget that aggregates.
- Prior rulings: ADR-0021 D3 (dashboard widgets pick dataset measures by
name) governs the runtime dashboard, not the blueprint; ADR-0033
Decision §5 names the blueprint and rules nothing on widget aggregation
or a view date key. The rest of the hits are blueprint-process decisions
(confirm, iterate, open-core boundary, additive materialization).
- `count_distinct` (in `AggregationMetricType` and ADR-0021 dataset
measures) is not offered: the ruling names five values.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic
(an enum tuple, describe strings and schemas).
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (CI-owned run).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block
excepted, per project convention).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzhYPN1JqnojTxfq2Pc
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 86ae2ca commit d2e2f6f
5 files changed
Lines changed: 220 additions & 10 deletions
File tree
- .changeset
- content/docs/references/ai
- packages/spec
- authorable-surface
- src/ai
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | | - | |
| 94 | + | |
| 95 | + | |
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
| |||
282 | 283 | | |
283 | 284 | | |
284 | 285 | | |
| 286 | + | |
285 | 287 | | |
286 | 288 | | |
287 | 289 | | |
| |||
357 | 359 | | |
358 | 360 | | |
359 | 361 | | |
| 362 | + | |
360 | 363 | | |
361 | 364 | | |
362 | 365 | | |
| |||
423 | 426 | | |
424 | 427 | | |
425 | 428 | | |
| 429 | + | |
426 | 430 | | |
427 | 431 | | |
428 | 432 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
0 commit comments