Skip to content

feat(spec): blueprint dashboard widgets say how they aggregate, and calendar views which date field they schedule on - #30484

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-yz95oc
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-yz95oc

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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 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; 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.
  • 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).
Reverse verification (two mutations through scripts/ablation-replace.mjs, each restored to the HEAD blob 1d11f8e14420)
  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.

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 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 621a4876… 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.

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

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (an enum tuple, describe strings and schemas).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (CI-owned run).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block excepted, per project convention).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzhYPN1JqnojTxfq2Pc

…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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ai tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 0c7e95aff5996de2313b4668760eafee2e50172d.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0c7e95aff5996de2313b4668760eafee2e50172d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8f5ff65b5fad97d9109a3ad2f58c0025b5d0c3ae — the merge of head e16fea955b42696bbbe6258047ad8aaf57767e8c into base 0c7e95aff5996de2313b4668760eafee2e50172d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0c7e95aff5996de2313b4668760eafee2e50172d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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 CONTRACT_REVIEW_TIER (a published-schema widening: an optional aggregate on the blueprint dashboard widget and an optional calendar date key on the blueprint view, executing objectstack-ai/cloud#3607's rulings).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4960c1d3e5c8ade07432fdb50ddb7047ff82b19e
Local-runs: none
Scope: the PR diff against merge-base c58008187f (all 5 files, 2 commits); packages/spec/src/ai/solution-blueprint.zod.ts at head (SNAKE_CASE :27, the three new constants :148-176, BlueprintViewSchema :192-215 with startDateField :212-213, the widget :236-250 with measure :241-242 and aggregate :243-244, the cloud#1967 notes :270-278 and :376-384, strictIdent / strictIdentOrNull :385-388, StrictView :474-494 with startDateField :493, StrictDashboard widget :496-512 with measure :504 and aggregate :508-509, SolutionBlueprintSchema :324, SolutionBlueprintStrictSchema :561); packages/spec/src/ai/solution-blueprint.test.ts at head (view parity :898-911, dashboard and widget parity :913-930, VALUE-parity walker :1051-1118, the new describe :1173-1296); packages/spec/authorable-surface/ai.json (:46-48, :65, :86-88); content/docs/references/ai/solution-blueprint.mdx at head (:86-97, :272-300, strict views and dashboards sections); .changeset/30430-blueprint-widget-aggregate-calendar-date.md; packages/spec/liveness/README.md :27-70; packages/spec/scripts/liveness/check-liveness.mts (GOVERNED :281, SPEC_ONLY_SCHEMAS :481-484); packages/spec/dropped-refinements.baseline.json (ai/BlueprintView :32-36); packages/spec/scripts/check-llms-txt.ts (count semantics :56, :179); scripts/check-changeset-no-major.mjs :905-930; AGENTS.md Prime Directives #10 and #12 (:264-283); the order /mnt/project-files/seat3-orders/30430.md; card #30430 body and 4 comments (os-dev-report 6658975093 included); PR #30484 body, 2 comments, 0 reviews; check-runs on the head.

① Derived judgments

  1. Value parity holds and is pinned. Both sides spell one module-local tuple BLUEPRINT_WIDGET_AGGREGATES (:156) through z.enum(...), .optional() on the lenient widget (:243) and .nullable() on the strict widget (:508), so the enum values cannot differ; startDateField is z.string().regex(SNAKE_CASE).optional() on the lenient view (:212) and strictIdentOrNull(...) on the strict view (:493), the same SNAKE_CASE pattern the VALUE-parity walker indexes on both sides (walker :1072-1104 treats optional / nullable / lazy as transparent and records string leaves by path, so views[].startDateField is compared). The walker returns on enums (:1099-1100), so aggregate was outside it; the new widget-parity pin closes that with an explicit options comparison (:929-930). Key parity: the VIEW pin (:909-910) now requires startDateField on both sides, and the new DASHBOARD and WIDGET pin (:913-930) is the first widget-level key-parity pin this file has had (git diff confirms none existed at the merge-base). Optional versus nullable is the mirror's standing spelling (:386 "this mirror's spelling of optional"), and the strict fixtures in the file gained both keys (diff :77-80, :88-91, :100).
  2. The measure describes no longer claim automatic aggregation and carry no title heuristic. Lenient (:241-242): "The field this widget aggregates ... 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 ... Omit for a count widget." Strict (:504): the same rule with "or null for a count widget". Gone: "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), all three visible as deletions in the diff. The aggregate describe (WIDGET_AGGREGATE_MEANING :159) names each value's meaning (sum = total of measure, avg, min / max, count = number of records after condition), states that sum / avg / min / max need a numeric measure, and states the count rule ("count" needs no measure field ... a field named in measure beside it is not read), on both sides through the shared constant. The pin at :1250-1259 reads both describes on both sides and refuses /infer/i and /chosen automatically/i; it does not refuse /title/i, so the rejected option C could return under another verb (should-fix 1). Pre-existing title heuristics on sibling keys remain: widget groupBy (:245-246 "the title and this field MUST name the SAME field") and condition (:247-248 "when its title implies a threshold or status"); the ruling rejected inferring the AGGREGATION from the title, and the order's ⛔ names "title-based inference" for that slot, so these sit outside the card and are flagged under ③, not owed here.
  3. "Ignored, not refused" against Prime Directive chore: version packages #10. The directive's corollary is "never advertise or demo a capability the runtime doesn't actually deliver (declared ≠ enforced) ... keep the claim as narrow as the enforcement", and Add comprehensive test suite for Zod schema validation #12 asks that off-spec metadata be rejected at authoring or publish so the error surfaces loudly. Judged acceptable here, for three reasons the code and body give and one they do not. (a) The claim is as narrow as the enforcement: the describe on both sides says "For type:"calendar" only" and "Ignored on the other view types" (:175, :493 adds "Null on every view that is not a calendar"), so nothing is advertised that the reader does not deliver; the silent no-op is declared, which is the narrowing chore: version packages #10 asks for. (b) The strict mirror is per-leaf JSON Schema (:270-278, :376-384) and cannot state "startDateField only when type is calendar"; a lenient-side .superRefine would run at apply only, after the model generated against a mirror that allowed the pairing, which is the cloud#1967 failure this file documents (a blueprint the user approved refused wholesale at apply), and the published JSON Schema would carry it as a dropped refinement (dropped-refinements.baseline.json already lists ai/BlueprintView for filters.element). (c) The same shape already carries a kind-scoped key other kinds ignore: strict groupBy :480 "Null for list/form/calendar/gallery", lenient groupBy :200-201 "REQUIRED for kanban ... Optional for gantt". (d) What is missing is the loud signal Add comprehensive test suite for Zod schema validation #12 wants at the one place a cross-key rule can be stated, the reader (cloud#3607's compile): the dev recorded both pairings as carrier notes in the os-dev-report's out_of_scope_findings (sum / avg / min / max without a numeric measure; startDateField on a non-calendar view) but the PR body's acceptance notes do not carry them and nothing names them on cloud#3607 yet (should-fix 4). Same verdict for measure beside aggregate: 'count': the describe says the field is not read (:159), the per-leaf limit is the same, and the reader is where a refusal belongs. Refusing on the lenient side would make a generated proposal illegal to apply, the worse failure; ignoring with a stated scope is the right choice at this layer.
  4. Liveness: outside the ledger, as claimed. The README's denominator is the registered kinds (BUILTIN_METADATA_TYPE_SCHEMAS) union listUnregisteredKindSchemaTypes() union the SPEC_ONLY_SCHEMAS overrides (:27-70); GOVERNED (check-liveness.mts :281) and SPEC_ONLY_SCHEMAS (:481-484: webhook, query, validation) name no blueprint, git grep -i blueprint packages/spec/src/kernel/ and grep -ril blueprint packages/spec/liveness/ both return nothing. No row is owed; the order said to say so in the body and add none, and the body does (§ Liveness). Nearest precedent is query, a request surface governed by override (README :62-70); the blueprint is cloud's I/O contract with no reader in this repo (git grep at head outside the schema and its test hits only PROTOCOL_MAP.md, generated ledgers and ai/index.ts), so an override would govern a surface whose only consumer is in another repo; not owed by the order.
  5. Declarations and ripples. PR body and changeset both carry Clause-②: yes (widening: ...) and Metadata-protocol: added; the changeset grades @objectstack/spec minor, the level added owes (check-changeset-no-major.mjs :909 "added / narrowed / semantic -> at least one moved package minor+"), and Check Changeset is green on the head. Ripples: authorable-surface/ai.json gains exactly ai/BlueprintView:startDateField (:65); the widget is an inline z.object under ai/BlueprintDashboard:widgets (:48) and the ledger lists no nested widget keys at the merge-base either (BlueprintWidgetCondition has rows :86-88 because it is an exported schema), so no aggregate row is expected. content/docs/references/ai/solution-blueprint.mdx carries the three new or changed rows with describe text byte-equal to the zod constants (lenient widget :94-95, lenient view :300, strict view :429); the strict dashboards section renders widgets as a truncated inline type, so no strict widget row is expected there. api-surface/ and llms.txt are untouched and should be: the tuple and the three describe constants are not exported, and check-llms-txt.ts re-derives per-module counts over *.zod.ts (:179), and no module was added. Lint · docs & code guards and Lint · ESLint are green on the head; check:docs and check:authorable-surface exit 0 by the dev's gates report (102 run, 0 NOT-MEASURED).
  6. Pins are real by reading. Ablation 1 (strict aggregate renamed): strictWidget(...) writes aggregate, so every strict parse either strips it (the .toBe(aggregate) expectations at :1217 and :1220 fail) or reports the renamed required key missing (the single-issue expectations at :1233, :1235 and :1247 fail); the widget key-parity pin fails on key sets; the no-title pin throws reading .description of a missing key. Eleven reds of 85 is consistent with that count. Ablation 2 (lenient startDateField renamed): the VIEW parity pin (:909-910), the calendar accept pin (:1263-1266 reads .startDateField off a lenient parse that now strips it) and the wrong-type pin (:1274-1277 expects an issue at startDateField, none now) go red; the non-calendar pin stays green because an unknown key is stripped, which the body states. Three reds, as reported. The unknown-value pin covers average, median, count_distinct, AVG and '' on both sides with exactly one issue at the key (:1230-1237); absent / null / missing are each pinned (:1239-1248); the five non-calendar types are each pinned on both sides (:1285-1295). Gap: the no-title pin's vocabulary (should-fix 1).
  7. Body matches code and the order. Every describe the body quotes is in the file verbatim ("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."); the count rule and the non-calendar choice are stated with reasons in the describe, the file's docblocks (:148-176) and the body, as the order required; strictIdentOrNull is what the strict view uses (:493); the five values are the ruling's five and count_distinct is named as not offered. One body-versus-code nit: the acceptance notes say the describe "does not describe cloud's default table, which this repo cannot read", while the strict aggregate describe (:509) and the lenient one (:244) both assert the fallback "(a numeric field sums)" (should-fix 3). Scope: the four work items of the order are all delivered and nothing outside them moved (5 files, 219 added, 10 deleted, no cloud, no runtime, no ADR); ADR-0087 not needed, as the order said; the Prior rulings read: line and Skills-docs: line are present; the PR is a draft with no assignees or reviewers.

② Semver level

@objectstack/spec minor. Two optional keys are added to a published authorable shape (one new authorable-surface row), every existing blueprint parses unchanged, and no value, default or refusal narrows. The strict mirror now requires the two keys of a model generating against it (nullable), which is the mirror's standing contract, not a published-metadata narrowing. Clause-②: yes (widening) with Metadata-protocol: added is the right pair and the changeset grades the one moved package minor.

③ Boundary flags

  • Two spellings of a count widget now coexist on the lenient and strict measure describes ('or "count" to count records' at the start, 'Omit for a count widget' / 'or null for a count widget' at the end). measure: "count" is kept for existing blueprints and cloud's reader; retiring the literal is a later narrowing with its own ADR-0087 disposition, not this PR's. Until then the describe should say which spelling to write (should-fix 2).
  • The reader-side signal for a misplaced startDateField or a sum / avg / min / max with no numeric measure lives in cloud#3607's compile. The dev filed both as carrier notes on the os-dev-report only; they are not on cloud#3607 (should-fix 4).
  • Sibling widget keys groupBy (:245-246) and condition (:247-248) still carry title-reading guidance that predates this PR and sits outside ruling C's slot (aggregation). No change owed by this card; the seat may want a card if the maintainer reads option C's rejection as shape-wide.
  • The BlueprintViewParsed and BlueprintViewSchema.type default (list) are untouched; a lenient view with startDateField and no type parses as a list that ignores the key, which the describe covers ("Ignored on the other view types").
  • Required CI contexts (Build Core, Test Core 1-8, Type Check, the remaining Lint shards) were in_progress at review time; Check Changeset, Lint · docs & code guards, Lint · ESLint, Lint · PM dispatch-gates, Spec property liveness and Governed Surface Queue Guard are green. No governed surface in the file list.

④ Design judgment

The 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 /title/i on the measure and aggregate describes of both sides, so the rejected option C cannot return under another verb; (2) in WIDGET_MEASURE_RULE or the lenient / strict measure describe, mark measure: "count" as the legacy spelling kept for existing blueprints and aggregate: "count" as the one to write, and say what a reader does with measure: "count" beside sum / avg / min / max (no field to aggregate, refused or reported by the reader), so the model has one target; (3) either drop "(a numeric field sums)" from the two aggregate describes (:244, :509) or cite it as measured on cloud#3607 (defaultAggregateFor, card body), so the describe does not assert a reader behaviour this repo cannot verify and the body's "does not describe cloud's default table" becomes true; (4) carry the two carrier notes from os-dev-report 6658975093 onto objectstack-ai/cloud#3607 as the cloud half's acceptance (a per-widget diagnostic for sum / avg / min / max without a numeric measure, and a per-view diagnostic for startDateField on a non-calendar view or naming a field that is not date / datetime), which is the loud reader-side refusal Prime Directive #12 asks for and the only place the cross-key rule can be stated. None blocks landing; landing waits on the running required contexts going green.

Implemented-by: claude/project-thread-yz95oc
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

  • 改了什么: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 被接受而不读)。
  • 需要你做的:无,席位落地。

席位意见:fable 契约复审一轮 PASS,无必改;四条建议全部采纳。宽松侧不对「非日历视图写 startDateField」或「sum/avg/min/max 缺数字 measure」报错,因为严格镜像是逐叶 JSON Schema 表达不了跨键规则,宽松侧拒会让用户已批准的蓝图在应用时整体失败(cloud#1967 那类);这两条改由 cloud#3607 的编译阶段按组件 / 视图报错,已写进 PR 正文的 Carried 行。

席位核对 — ACCEPT · domain:spec#3 · head e16fea955b42696bbbe6258047ad8aaf57767e8c

  • Shape: 5 files (+220 −10), all under packages/spec plus the regenerated reference page, on merge-base c5800818: BLUEPRINT_WIDGET_AGGREGATES and the shared describe constants (WIDGET_AGGREGATE_MEANING, WIDGET_MEASURE_RULE, VIEW_START_DATE_FIELD_RULE) read by both the lenient schema and the strict mirror; optional aggregate on the widget and startDateField on the view (nullable on the strict side); key-parity and enum-option-parity pins; a spec minor changeset.
  • Premise measured: at merge-base a widget could only write measure and the reader chose the aggregation from the field type, and a blueprint view had no calendar date key (cloud#3607's rulings: row 2 A, calendar key B).
  • Gates: at e16fea95, full build 119/119, spec 814 files (25610 passed), typecheck exit 0, blueprint pins 85/85, 102/102 derived gates exit 0 and --ran reconciles with 0 NOT-MEASURED, check-changeset-no-major exit 0 with this body (the none control exits 1) (dev report). Check-runs on this head: 57 success, 10 skipped, 0 red; the body edit re-runs the body guards.
  • Contract review: 6659170858 on 4960c1d3: PASS, no must-fix. The seat read the round-1 delta (3 files, +6 −5, describes and one pin line only): should-fix 1, the no-title pin now refuses /title/i on the measure and aggregate describes of both sides; should-fix 2, WIDGET_MEASURE_RULE names measure:"count" as the legacy spelling, says to write aggregate:"count", and says a reader refuses measure:"count" beside sum/avg/min/max; should-fix 3, both aggregate describes drop "(a numeric field sums)" and say only "the reader's own default"; should-fix 4, the PR body's Carried: line gives cloud#3607's cloud half its two acceptance items.
  • Declarations: Clause-②: yes (widening: …), Metadata-protocol: added, @objectstack/spec minor. ADR-0087 not needed.
  • Accepted deviations: startDateField on a non-calendar view and a sum/avg/min/max widget without a numeric measure are declared as ignored / incomplete in the describes, not refused by the schema; the refusal belongs to the reader (cloud#3607), stated in the body.
  • Carried: objectstack-ai/cloud#3607 (cloud half): (1) a sum/avg/min/max widget whose measure is absent, "count" or non-numeric is refused per widget, never summed as a record count; (2) startDateField is read on calendar views only, and one naming no date/datetime field is refused, never replaced by the first date field.
  • Landing: not governed (check-governed-merges --pr 30484: 230 lines). The hand-off is pr_ready plus automerge_enable, through the merge queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ai size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant