Repository navigation
feat(spec): blueprint dashboard widgets say how they aggregate, and calendar views which date field they schedule on - #30484
Conversation
…ar views a startDateField A blueprint widget could name only its measure, and the reader picked the aggregation from the field type, so a widget meant to average a number field summed it. The widget gains an optional aggregate (sum | avg | min | max | count) on the lenient schema and the strict mirror; measure's describe no longer says the aggregation is automatic or that the builder infers anything from the title. A blueprint calendar view could not say which date field it schedules on. The view gains an optional startDateField (the built view's calendar.startDateField), read on a calendar view and ignored on the other view types, because the strict mirror cannot state a cross-key rule. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhYPN1JqnojTxfq2Pc
…urface, add the changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhYPN1JqnojTxfq2Pc
📓 Docs Drift CheckThis PR changes 1 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8f5ff65b5fad97d9109a3ad2f58c0025b5d0c3ae && git checkout 8f5ff65b5fad97d9109a3ad2f58c0025b5d0c3ae
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0c7e95aff5996de2313b4668760eafee2e50172d e16fea955b42696bbbe6258047ad8aaf57767e8c && git checkout -B drift-repro 0c7e95aff5996de2313b4668760eafee2e50172d && git merge --no-ff e16fea955b42696bbbe6258047ad8aaf57767e8c
node scripts/docs-audit/affected-docs.mjs --json 0c7e95aff5996de2313b4668760eafee2e50172d
|
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 4960c1d · 2026-11-05T13:17Z Full review of the first head at |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe change is at the right layer and in the right shape: one tuple and three describe constants declared once and read by both the lenient schema and the strict mirror, so the two describes cannot drift; key parity and enum-option parity pinned where the existing VALUE walker is blind; the cross-key rules stated where the contract can state them (the describe and the docblocks) and left to the reader where it cannot (the per-leaf mirror), with the reasoning written beside the code. No must-fix. Should-fix, in order: (1) extend the no-title-inference pin (:1250-1259) to refuse Implemented-by: VERDICT: PASS |
…e legacy spelling, and the aggregate describes stop stating the reader's fallback measure:"count" is the legacy spelling of a record count; aggregate:"count" is the one to write, and beside sum/avg/min/max measure:"count" names no field, so a reader must refuse that widget. The aggregate describes no longer assert what the reader's own default is. The no-title-inference pin now also refuses "title" in both describes on both sides. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhYPN1JqnojTxfq2Pc
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审一轮 PASS,无必改;四条建议全部采纳。宽松侧不对「非日历视图写 席位核对 — ACCEPT · domain:spec#3 · head
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
Requested by Yibo Wang · project thread
Fixes #30430
Ruling-ref: objectstack-ai/cloud#3607 (row 2 A, record 6656298548; the calendar key B)
Clause-②: yes (widening: an optional
aggregateon the blueprint dashboard widget and an optionalstartDateFieldon 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'splanCardresult and teaches no widget or view key; controlgit grep -c "solution-blueprint" -- skills/objectstack-ai/references/_index.md: 1, an index pointer to the schema file). The generated referencecontent/docs/references/ai/solution-blueprint.mdxis 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
aggregatesum,avg,minormaxwhosemeasureis 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)startDateFieldis read on acalendarview only and ignored on every other view type; on a calendar view it binds the calendar'scalendar.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.维护者速读
aggregate(sum/avg/min/max/count),蓝图视图新增可选startDateField(日历视图按哪个日期字段排程,对应建出视图的calendar.startDateField)。宽松 schema 与给模型用的严格镜像两侧同时加,严格侧可为 null。measure的说明改为「只写字段,怎么聚合看aggregate」,删掉了「按字段类型自动选」和「从标题推断」两句,并标明measure: "count"是旧写法、应写aggregate: "count"。measure,平台按字段类型自动聚合,数字字段一律求和,「平均面试评分」显示成 12 个分数之和 908;日历视图也无处写日期字段。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).aggregate: 'sum' | 'avg' | 'min' | 'max' | 'count':.optional()on the lenientBlueprintDashboardSchemawidget,.nullable()(key required) on the strict mirrorSolutionBlueprintStrictSchema.dashboards[].widgets[]. Both sides spell one module-local tupleBLUEPRINT_WIDGET_AGGREGATESand read one meaning sentenceWIDGET_AGGREGATE_MEANING, so the two describes cannot drift.measure: both describes rewritten through one sharedWIDGET_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, nottotal_amount.measure: "count"is now marked as the legacy spelling of a record count, withaggregate: "count"the one to write, and the describe says what a reader does withmeasure: "count"besidesum/avg/min/max(refuses the widget).startDateField: a snake_case identifier,.optional()onBlueprintViewSchema,strictIdentOrNullon the strict view (so the VALUE-parity walker sees the sameSNAKE_CASEpattern on both sides). One shared describe,VIEW_START_DATE_FIELD_RULE.packages/spec/authorable-surface/ai.json(+ai/BlueprintView:startDateField; the widget is a nested shape underai/BlueprintDashboard:widgets, soaggregateadds no row) andcontent/docs/references/ai/solution-blueprint.mdx.api-surface/is unchanged: no export was added (the tuple and the three describe constants are module-local).@objectstack/specminor, with both declaration lines.The
countruleaggregate: 'count'counts the records the widget covers (after itscondition) and needs nomeasurefield. A field named inmeasurebeside 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 writeaggregate: "count". Besidesum,avg,minormax,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'needmeasureto name a numeric field of the widget's object; the describe says so. Withmeasureabsent 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.superRefinewould 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
startDateFieldis read on acalendarview and ignored onlist,form,kanban,galleryandgantt. 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 fromcolumns, 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 asgroupByis.Liveness
packages/spec/liveness/does not govern these keys, so no row is added. The ledger's denominator is the registered metadata kinds pluslistUnregisteredKindSchemaTypes()and theSPEC_ONLY_SCHEMASoverrides (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" packagesoutside 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:describe('blueprint widget \aggregate` and calendar view `startDateField`'): each of the five values accepted on both sides;countwith nomeasureon both sides;average,median,count_distinct,AVGand''refused on both sides with exactly one issue atdashboards.0.widgets.0.aggregate; absent parses on the lenient side,nullparses and a missing key is refused on the strict side; nomeasure/aggregatedescribe on either side says "infer", "chosen automatically" or "title";startDateFieldon 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.the DASHBOARD and WIDGET schemas carry exactly the same keyspin (no widget-level parity pin existed), with theaggregateenum's options compared across sides; the VIEW pin now namesstartDateField.aggregate,startDateField); the VALUE-parity test is unchanged and green with both new keys on both sides.Verification
Head
e16fea95(branch offorigin/mainc5800818; review round 1 changed only themeasure/aggregatedescribes, the no-title-inference pin and the regenerated reference page), underos-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.tsalone: 85 passed (85) ate16fea95. The reverse verification below ran at4960c1d3; round 1 touched neither ablated line.check-changeset-no-majorwith this body as the event: exit 0 onMetadata-protocol: added; the control withnoneexits 1 (the authorable-surface baseline moved).Reverse verification (two mutations through scripts/ablation-replace.mjs, each restored to the HEAD blob 1d11f8e14420)
aggregate: z.enum(BLUEPRINT_WIDGET_AGGREGATES).nullable()→aggregate_ablated: …, anchor 1 → 0, blob1d11f8e14420→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 HEADempty.startDateField: z.string().regex(SNAKE_CASE).optional()→startDateField_ablated: …, blob1d11f8e14420→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 HEADempty.The spec tests import the schema from
src, so no rebuild was needed for either leg.Gates: dispatch-gates --commands, 102 derived at e16fea9, 102 run, 0 NOT-MEASURED
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 lineexit 0, includingcheck-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-loadsandcheck:nul-bytes.4960c1d3one prerequisite exited 3:check-plugin-teardown-shape --self-teston the shallow clone (its pinned fixture commit621a4876absent); aftergit fetch origin 621a4876…it exits 0 (48 cases). Ate16fea95all 102 exited 0 on the first run.!in the title and no(narrowing)arm: no full dogfood run. CI owns the rest of the farm.Acceptance notes
aggregateleaves 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.count_distinct(inAggregationMetricTypeand ADR-0021 dataset measures) is not offered: the ruling names five values.Human Minimum
packages/specholds no runtime logic (an enum tuple, describe strings and schemas).pnpm lintpasses (CI-owned run).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzhYPN1JqnojTxfq2Pc