Skip to content

feat(spec): a report declares its period (dateRange) and the export of its table (exportOptions) - #26628

Merged
os-zhuang merged 12 commits into
mainfrom
claude/project-thread-xsyisg
Oct 27, 2026
Merged

os-zhuang merged 12 commits into
mainfrom
claude/project-thread-xsyisg

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26548

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — skills/objectstack-ui/evals/analytics-inline-vs-dataset.json

维护者速读

  • 改了什么: 报表元数据新增两个可选键。dateRange 是报表覆盖的时间段,预设表、默认值和改名提示与仪表盘逐字相同,作用于数据集里一个 type: 'date' 的维度;exportOptions 是报表表格的下载(csv / xlsx,默认开启,formats: [] 关闭)。另加一条 lint 规则(判定维度是否存在、是否日期、省略时是否唯一)、表单「时间段与导出」分区、文档和三种语言的翻译。
  • 为什么改: 裁决 B(6408567749,[Decision] The report page offers only "Edit": should a shipped report get a time-range filter, export and subscription, and in which order #25873)要求报表有时间范围和导出,并复用现有的导出权限,不做订阅。这是规格的一半;Console 报表页的读取在 [Decision] The report page offers only "Edit": should a shipped report get a time-range filter, export and subscription, and in which order #25873。
  • 风险与回滚: 只加可选键,不声明这两个键的报表解析和渲染都不变。渲染器落地前两个键没有读取方,作者写了会收到 planned 警告,说明暂时不生效。回滚就是 revert 本 PR。
  • 先看: packages/spec/src/ui/report.zod.ts(两个新键和 joined 报表的拒绝),packages/lint/src/validate-report-date-range.ts(四种判定,也接在报表发布门上),packages/spec/src/ui/list-view-export-options.ts(ReportExportOptionsSchema,只留 csv / xlsx)。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/ui/report-date-range-export.test.ts,预期 48 个用例全部通过,其中一条断言报表的 defaultRange / allowCustomRange 与仪表盘的声明逐字相等。
  • 需要你做的: 本 PR 改到 skills/**,属受管面:席位合同评审通过后由你合并。

What changes

ReportSchema gains two optional top-level keys (packages/spec/src/ui/report.zod.ts). A report that declares neither parses exactly as before, with neither key in its output.

  • dateRange: { dimension?, defaultRange, allowCustomRange }, a closed block. defaultRange and allowCustomRange are the dashboard's declaration: the same DATE_RANGE_DEFAULT_RANGES import (no second preset table), the same defaults (this_month, true), the same describes and the same rename hints (preset / range / default, allowCustom / custom). report-date-range-export.test.ts holds the two declarations equal through strictObjectDeclarations() and their JSON Schema, so dashboard.zod.ts is untouched. dimension names a dataset dimension that declares type: 'date' (ADR-0021: a report selects by dimension name), with the dashboard compareTo hints field / dateField / fieldName / timeDimension renamed onto it. The TSDoc states the renderer contract: the window rides selection.timeDimensions: [{ dimension, dateRange }] with no granularity, a filter window the analytics service already accepts. Top-level aliases dateFilter / timeRange → dateRange (the dashboard's).
  • exportOptions: { formats?, includeHeaders?, fileNamePrefix? }, declared as ReportExportOptionsSchema beside its list sibling in list-view-export-options.ts. includeHeaders and fileNamePrefix are the list block's members by identity; formats is ListViewExportFormatSchema.exclude(['json']), so csv / xlsx. maxRecords / streaming are answered as list-only. No permission key: the TSDoc and describe name ObjectPermission.allowExport on the dataset base object, the predicate the list toolbar uses. Default on (both formats), formats: [] off.
  • Joined container. A joined report refuses both keys, in the superRefine arm that already refuses the container chart, with the same no-pointer style. JoinedReportBlockSchema gets guidance for both keys.
  • Guidance on ReportSchema for allowExport (points at the permission-set axis and exportOptions: { formats: [] }), subscription and schedule (no scheduled delivery of a report).
  • Lint rule validateReportDateRange (packages/lint/src/validate-report-date-range.ts), in the reference-integrity suite beside validateChartBindings, so it runs on os validate / os lint / os build and at the report publish door (runtimeTypes: ['flow', 'report']). Four error verdicts, each with an os explain entry: report-date-range-dimension-unknown, -not-date, -ambiguous (omitted, two or more date dimensions) and -none (omitted, none). The omitted-dimension rule is the dashboard executor's compareTo rule: exactly one candidate is used.
  • Liveness. liveness/report.json rows for both keys, planned, authorWarn with an author hint, notes naming the renderer read point in DatasetReportRenderer.tsx and [Decision] The report page offers only "Edit": should a shipped report get a time-range filter, export and subscription, and in which order #25873 as carrier. Both containers are drilled (children, six child rows) so the gate's undeclared-inheritance check is satisfied. lint-liveness-properties now walks reports, so the warning reaches authors.
  • Form. report.form.ts gets a collapsed "Period & export" section, visibleWhen: "data.type != 'joined'", with both composites. Translated in @objectstack/platform-objects for zh-CN, ja-JP and es-ES.
  • Docs. content/docs/ui/reports.mdx § "Period and export" (an os:check example, the dimension rule, the window lowering, the export permission axis, a "declared, not drawn yet" callout, what is not offered) and a paragraph under "Permissions and protection". skills/objectstack-ui/rules/dashboards.md § Report Configuration gets the dateRange example.
  • Generated: content/docs/references/ui/report.mdx, authorable-surface/ui.json, the strictness-ledger counts and the liveness state counts.
  • Changeset: @objectstack/spec minor, @objectstack/lint minor, @objectstack/platform-objects patch, with both declaration lines.

Skills readings (skills/** touched):

Reading Before After
skills/objectstack-ui/rules/dashboards.md lines 464 463
skills/objectstack-ui/rules/dashboards.md tokens (ratchet, ceiling 6239) 6239 6236
skills/objectstack-ui/** lines 2260 2259
all SKILL.md lines 4600 4600
bundle total tokens 213407 213404

The three-line order comment becomes a trailing comment on order, and the freed lines carry a one-line period comment plus the dateRange example, so the file still shrinks by one line and 3 tokens. Before is origin/main at the round-3 merge. The eval is the owed half (filed as #26633): both skills/objectstack-ui/evals/*.json files sit at their token ceiling (headroom 0), so a report-period prompt cannot be added here without deleting another prompt.

Tests

  • packages/spec/src/ui/report-date-range-export.test.ts (new, 48 cases): untouched reports, dateRange: {} defaults, every preset accepted, last_6_months refused, every rename hint, unknown member, dateFilter / timeRange, joined refusal on container and block, declaration parity with the dashboard, exportOptions accepts and refusals (json, pdf, docx, maxRecords, streaming, bare array, joined, block), allowExport / subscription / schedule guidance, defineReport and getMetadataTypeSchema('report').
  • packages/lint/src/validate-report-date-range.test.ts (new): all four verdicts on the app-todo task-dataset shape, nearest-name suggestion among date dimensions only, silence on unbound / joined / malformed input, suite integration, verdict length and os explain per id.
  • runtime-gate.inert-type-writes.test.ts: a new block proving the report publish door judges a date range against the dataset it binds (four cases and a control); the door's member pin is now two members.
  • lint-liveness-properties.test.ts: both keys warn with the authored hint; a report without them is silent.
  • Pins moved by the new form section: report-form-echo-decisions.test.ts (population 44 → 50), the catalog-wide positive control in object-lifecycle-panel-echo-decisions.test.ts (683 → 686), validate-predicate-path-refs.test.ts predicate census (88 → 89, literal comparisons 63 → 64). Each carries a dated comment.
  • packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts: the served /meta/types census pins report at 23 top-level properties (was 21), with a JSDoc line saying why. The round-3 merge of main kept main's lowered counts for the other types (feat(spec)!: retirement kits whose conversion retired at or below 16.x age out; the strict unknown-key refusal names the protocol upgrade guide #26599).

Acceptance notes

Read against the card's Acceptance list. Deviations, each a choice made here:

  1. exportOptions is also refused on a joined container. The card decides the joined case for dateRange only. A joined report draws one table per block, so "the table the report draws" has no single answer. Widening later is non-breaking. Raised as an open question in the report.
  2. json and pdf get report-side messages. The card says pdf keeps the list's retirement prescription, but that text sends the author to list behaviour that is false for a report. The report message for pdf points at a page with print.
  3. The subscription / schedule guidance cites no ruling. Runtime strings carry no tracker numbers in this repo, so the text says what is true (no scheduled delivery) without the ruling reference.
  4. The lint rule sits on the report publish door as well as flow (the chart-bindings precedent), so publishing a report with a bad dimension is refused, as the docs say.
  5. The skills example uses this_year, not this_quarter. The example's matrix buckets close_date by quarter, so a quarter window would draw one column.
  6. Translations. Only @objectstack/platform-objects bundles metadata forms, so the six new leaves landed there (three locales, authored, not filled); pnpm check:i18n reports all ten packages in sync.
  7. No new public export. ReportExportOptionsSchema lives in list-view-export-options.ts, which, like its list sibling, is outside the ui barrel; check:api-surface is unchanged.

Out of scope, untouched: ui/ (the renderer is #25873), any server export route, dashboard.zod.ts.

Verification

All readings local, final head be4a0204cdad. It merges origin/main at 86ae47e13 (no rebase); main moved 6 commits during the test run, none touching a file this PR changes, and git merge-tree against 8222fa805 is clean. CI check-runs are not cited here: they report after this push.

  • Package suites (round 1, at 0c548c59a8ca): @objectstack/spec 729 files / 22095 tests passed (1 skipped, 1 todo); @objectstack/lint 167 files / 7411 passed; @objectstack/platform-objects 96 files / 1361 passed. Typecheck of all three: exit 0. eslint on the 15 changed source files: exit 0.
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands named 113 families at 4dca57f78; all were run on a full workspace build (118 turbo tasks) with each exit code recorded before any pipe. One was red: check:empty-state (exit 1), because the dateRange TSDoc says an absent window shows every period and nothing classified it. Fixed in 8c03abecb: the sentence now sits beside the key and dateRange is registered as a scope empty state (it widens the period, never the access). That commit derived one more family (check:pnpm-flag-spellings). Re-run after it: check:empty-state, check:pnpm-flag-spellings, check:spec-docblock-symbol-anchors, check:liveness, check:docs, check:api-surface, check:nul-bytes, all exit 0. dispatch-gates --ran: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN.
  • Round 3, on the merged tree (be4a0204cdad): the merge conflicted only in CARD_PROPERTY_COUNTS (protocol.meta-types-degenerate-derivation.test.ts), resolved to main's lowered counts with report: 23 and both comment blocks. pnpm install, then check:generated --fix regenerated authorable-surface/ui.json (main's file plus ui/Report:dateRange and ui/Report:exportOptions), then 0 stale. check:empty-state and check:liveness exit 0. Suites, each exit 0: @objectstack/spec 732 files / 21956 passed (1 todo); @objectstack/lint 167 / 7411; @objectstack/platform-objects 96 / 1361; @objectstack/metadata-protocol 269 files (3 skipped) / 21580 passed (19 skipped), census file 18 / 18; @objectstack/plugin-security 230 / 4991 passed (45 skipped).
  • Local-only families: check:dev-prereqs exit 0, check:skill-frame-freshness exit 0.
check:liveness (report rows)
report      27 classified (live 21, planned 6)
✓ every governed-type property, at every depth the ledger drills, is classified, … every container inheritance is declared, …
total across the shards: 1061 live · 1 experimental · 1 live-elsewhere · 113 dead · 29 planned = 1205 classified.
check:i18n and the translation ledgers
check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys).
platform-objects src/apps/translations: Test Files 25 passed (25), Tests 438 passed (438)
Skills token ratchet
✓ check-skills-token-ratchet: 81 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted.
skills/objectstack-ui/rules/dashboards.md is 6236 tokens (ceiling 6239; headroom 3)
Focused tests
packages/spec  src/ui/report-date-range-export.test.ts           Tests 48 passed (48)
packages/spec  report.test.ts, report-date-range-export, alias-integrity,
               metadata-form-zod-reconciliation                     Test Files 4 passed, Tests 216 passed
packages/lint  validate-report-date-range, runtime-gate.inert-type-writes, lint-liveness-properties,
               reference-integrity-suite, rule-explanations                          Tests 191 passed (191)

Generated by Claude Code

…ess counts; drill the two report containers

Claude-Session: https://claude.ai/code/session_01WGufzhMJrMdCkQS33DYtmu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/platform-objects, @objectstack/spec, touching 39 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/authorable-surface/ui.json, packages/spec/liveness/report.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))
  • content/docs/api/error-catalog.mdx (via invalid_type (literal, a string literal in ReportExportOptionsSchema))
  • content/docs/api/error-handling-server.mdx (via invalid_type (literal, a string literal in ReportExportOptionsSchema))
  • content/docs/automation/flows.mdx (via invalid_type (literal, a string literal in ReportExportOptionsSchema))
  • content/docs/data-modeling/analytics.mdx (via ReportSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx (via dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections), invalid_type (literal, a string literal in ReportExportOptionsSchema), this_month (literal, a string literal in ReportSchema))
  • content/docs/deployment/validating-metadata.mdx (via dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))
  • content/docs/protocol/objectui/concept.mdx (via invalid_type (literal, a string literal in ReportExportOptionsSchema))
  • content/docs/ui/dashboards.mdx (via allowCustomRange (literal, a string literal in ReportSchema), dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections), defaultRange (literal, a string literal in ReportSchema), this_month (literal, a string literal in ReportSchema))
  • content/docs/ui/reports.mdx (via fileNamePrefix (symbol, a field of const object REPORT_EXPORT_OPTIONS_SHAPE), includeHeaders (symbol, a field of const object REPORT_EXPORT_OPTIONS_SHAPE), allowCustomRange (literal, a string literal in ReportSchema), dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections), defaultRange (literal, a string literal in ReportSchema), exportOptions (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections), this_month (literal, a string literal in ReportSchema))
  • content/docs/ui/views.mdx (via fileNamePrefix (symbol, a field of const object REPORT_EXPORT_OPTIONS_SHAPE), includeHeaders (symbol, a field of const object REPORT_EXPORT_OPTIONS_SHAPE), exportOptions (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))
  • content/docs/releases/v16.mdx (via dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))
  • content/docs/releases/v17/17-0.mdx (via exportOptions (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections), invalid_type (literal, a string literal in ReportExportOptionsSchema))
  • content/docs/releases/v17/17-4.mdx (via dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))
  • content/docs/releases/v17/17-5.mdx (via dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))
  • content/docs/releases/v17/17-6.mdx (via invalid_type (literal, a string literal in ReportExportOptionsSchema))
  • content/docs/releases/v17/17-7.mdx (via ReportSchema (symbol, a top-level const), dateRange (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections), exportOptions (literal, a string literal in BLOCK_NON_JOINED_REPORT_KEY; a string literal in JoinedReportBlockSchema; a string literal in ReportSchema; a string literal in sections))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/authorable-surface/ui.json, packages/spec/liveness/report.json, …) — pages documenting those are invisible to this run
  • 12 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 147 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 8222fa805d0e288fec470e47e374e408b4925745 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from dd34d9461ef9c38e21e66497c4aa106ba72d2023 — the merge of head be4a0204cdad1918fcf4a6571c7e87d8faabf2b5 into base 8222fa805d0e288fec470e47e374e408b4925745, 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 dd34d9461ef9c38e21e66497c4aa106ba72d2023 && git checkout dd34d9461ef9c38e21e66497c4aa106ba72d2023
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8222fa805d0e288fec470e47e374e408b4925745 be4a0204cdad1918fcf4a6571c7e87d8faabf2b5 && git checkout -B drift-repro 8222fa805d0e288fec470e47e374e408b4925745 && git merge --no-ff be4a0204cdad1918fcf4a6571c7e87d8faabf2b5

node scripts/docs-audit/affected-docs.mjs --json 8222fa805d0e288fec470e47e374e408b4925745

⚠️ 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 8222fa805d0e288fec470e47e374e408b4925745 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0c548c59a8cac03bf6db83253d12b4bbc437b6a7
Local-runs: none
Scope: packages/spec/src/ui/list-view-export-options.ts, packages/spec/src/ui/report.form.ts, packages/spec/src/ui/report.zod.ts, skills/objectstack-ui/rules/dashboards.md + packages/spec/src/ui/dashboard.zod.ts, packages/spec/src/ui/view.zod.ts, packages/spec/src/ui/index.ts, packages/spec/src/ui/dataset.zod.ts, packages/spec/src/data/analytics.zod.ts, packages/spec/src/shared/strict-object.ts, packages/services/service-analytics/src/analytics-service.ts, packages/services/service-analytics/src/dataset-compiler.ts, packages/lint/src/validate-report-date-range.ts, packages/lint/src/reference-integrity-suite.ts, packages/lint/src/index.ts, packages/lint/src/lint-liveness-properties.ts, packages/lint/src/runtime-gate.inert-type-writes.test.ts, packages/spec/liveness/report.json, packages/spec/scripts/liveness/empty-state-registry.mts, packages/spec/src/ui/report-date-range-export.test.ts, packages/spec/src/ui/report.test.ts, docs/adr/0021-analytics-dataset-semantic-layer.md, content/docs/ui/reports.mdx, .changeset/26548-report-date-range-export.md, .github/workflows/lint.yml, .github/workflows/spec-liveness-check.yml

Inputs read: card #26548 (body and all three comments: triage 6436657773, claim 6437381453, os-dev report 6440254029), PR #26628 (body, 29-file list, net diff against origin/main at the head), and the check-runs on the head. Every file the hunks cite was read at the head tree.

Gate state at the time of this review. Complete and green: Governed Surface Queue Guard, Check Changeset, Check PR Size, PR-body guards, Spec property liveness (this is the job that runs check:liveness and check:empty-state), Type Check · source gates (this job runs check:generated --reconcile-only, check:skill-docs, check:authorable-surface, check:docs and the skills token ratchet), Lint · ESLint, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · scoped · query-options erasure, Lint · scoped · tooling self-tests, Dogfood Regression Gate, Flag docs affected by code changes, Check Documentation Links, Plan Test Core shards, Auto Label, filter. Still running: Build Core, Build Docs, Build UI Deps, Test Core (all eight shards), Type Check · workspace, Type Check · debt ledger, Type Check · consumer gates (this job runs check:api-surface and check:i18n), Lint · scoped · slot lookup & corpus. The aggregate required context Lint & Repo Gates had not reported yet. Skipped by design: the opt-in harness, the tarball smoke, the Dogfood matrix shards and Temporal Conformance. No gate was red. The gate verdicts for the running jobs are still owed; this record does not claim them.

① Derived judgments

  1. ReportSchema accept-set widens by two optional top-level keys, dateRange and exportOptions. A report declaring neither parses to the same output as before. Right: the new test pins the parsed object equal to the input plus the existing drilldown: true default, with neither key present.
  2. dateRange is a closed block { dimension?, defaultRange, allowCustomRange }. defaultRange and allowCustomRange are the dashboard's declaration: the same DATE_RANGE_DEFAULT_RANGES import from dashboard.zod.ts, the same defaults, the same describes. Right. The test compares the two live declarations by describe and by JSON Schema, and carries every dashboard alias for those two members. dashboard.zod.ts is untouched. No import cycle: dashboard.zod.ts does not import report.zod.ts.
  3. dateRange.dimension names a dataset dimension that declares type: 'date'. The dashboard routes field / dateField / fieldName onto its raw-field field; the report routes those words plus timeDimension onto dimension. Right against ADR-0021 line 273 ("bind to dimension names, not raw fields") and line 392, and against dataset-compiler.ts dimensionType, which types only type: 'date' as a time axis. The card's risk (1), that the dashboard's shipped field is a raw object field, stands as the card left it: a maintainer rename at review, not a defect in this PR.
  4. The lowering stated in the TSDoc (selection.timeDimensions: [{ dimension, dateRange }] with no granularity, a filter window) is what the analytics service does. Right: analytics-service.ts 612-614 says an entry carrying only a dateRange is a window and must stay one, and the wire timeDimensions[].dateRange accepts a preset name or a [start, end] pair.
  5. Top-level aliases dateFilter / timeRange onto dateRange. Right, the dashboard's spellings at dashboard.zod.ts:1778. The block-to-report parity test runs one way (every block alias the report can honour is routed on the report), so a report-only alias does not break it.
  6. A joined container refuses dateRange and exportOptions in the existing superRefine arm, at the key's own path, with no pointer onto blocks[], {} included. JoinedReportBlockSchema answers both keys with guidance. Right, the container-chart precedent in the same arm. The form hides the new section with visibleWhen: "data.type != 'joined'", so the three surfaces agree.
  7. exportOptions is ReportExportOptionsSchema, declared in list-view-export-options.ts: formats is the list enum with json excluded, includeHeaders is the list member by identity, fileNamePrefix is the list member re-described. maxRecords and streaming are refused by guidance. A bare array is answered by name. Right. ListViewExportOptionsSchema and ListViewExportFormatSchema are unchanged, and the test pins the list block still accepting all five members and json.
  8. No new public export. Right: list-view-export-options.ts is outside the ui barrel (only view.zod.ts and component.zod.ts import it), the api-surface shards carry neither export schema today, and the diff touches no api-surface file. The check:api-surface gate that proves this in CI is in the still-running consumer-gates job.
  9. Guidance entries allowExport, subscription, schedule on ReportSchema. These keys were already refused as unknown; the change is the sentence an author sees. Right.
  10. @objectstack/lint gains validateReportDateRange with four error ids and os explain entries, exported from the package index, registered in REFERENCE_INTEGRITY_RULES with runtimeTypes: ['flow', 'report']. A report write at the publish door now runs two members. Right: the door test pins the member list, four lit refusals and one clean control. The rule finds reports the way its sibling does (recordsOf(stack.reports)), skips joined and unbound reports, and leaves an unknown dataset to chart-dataset-unknown.
  11. lint-liveness-properties now walks reports. Only the two new rows carry authorWarn; every other report row is live (27 classified: 21 live, 6 planned, the six being the two keys and their children). So the new warning channel reaches only the planned keys. Right.
  12. Liveness rows: both keys planned, authorWarn, drilled children with evidence, a carrier ([Decision] The report page offers only "Edit": should a shipped report get a time-range filter, export and subscription, and in which order #25873) and the flip instruction. dateRange is registered as a scope empty state. Right. The liveness job is green.
  13. Form: a collapsed "Period & export" section with two composites. Section-level visibleWhen is a declared member of FormSectionSchema ("hides the whole section when false"). Six translated leaves in four bundles. Right.
  14. Generated: authorable-surface/ui.json gains ui/Report:dateRange and ui/Report:exportOptions; the reference docs page gains both keys; the strictness-ledger counts move. Right, and the source-gates job that checks them is green.
  15. Skills: the example gains one dateRange line and loses a three-line order comment; the file shrinks from 6239 to 6224 tokens under a ceiling of 6239. Right. The ratchet step runs in the green source-gates job.

② Semver level

The changeset is @objectstack/spec minor, @objectstack/lint minor, @objectstack/platform-objects patch. Its body carries Clause-②: yes (widening) and Metadata-protocol: added as line-initial declarations, and the PR body carries the same two lines. The diff moves exactly those three published packages. added takes at least minor on spec: right. Lint publishes a new rule and five new index exports and widens a refusal on a new key only: minor is right. Platform-objects moves only translation leaves: patch is right. Nothing is removed, tombstoned or narrowed, so no ADR-0087 disposition is owed and no migration text is owed. Check Changeset is green.

Clause-②: yes (widening)

③ Boundary flags

The dev's os-dev report (6440254029) raises one open question and seven deviations. Each is answered here.

Open question: should a joined report refuse exportOptions on the container as well as dateRange? Answer: A, keep the refusal. Three reasons. First, the key's own contract: exportOptions is "what the report's export control offers" over "the table the report draws", and a joined report draws one table per block, so the referent does not exist. A key that parsed would promise a download whose file shape nobody has decided (one file per block, or one workbook). That is a capability declared and not delivered, which Prime Directive 10 forbids, and it is the same reason the container already refuses chart. Second, nothing narrows: the key is new, no stored report carries it, and the card itself decides nothing for exportOptions on a joined container, so refusing is the conservative reading of the ruling. Third, direction of change: lifting the refusal later, once #25873 or a successor decides the file shape, is a pure widening; accepting now and reshaping later would be a semantic change on a published key. The form already hides the section for joined, and the block guidance already says "a joined container refuses it as well", so the three surfaces are consistent only under A. Resolved as A; no escalation.

Deviations, in the dev's order:

  1. exportOptions refused on a joined container. Accepted, as above.
  2. json and pdf get report-side messages. Accepted. The list's pdf sentence says the surviving formats are csv, xlsx and json, which is false for a report; the report message points at a page with print, the same destination the list message names.
  3. The subscription / schedule guidance cites no ruling. Accepted. Runtime strings carry no tracker number here (check:doc-authoring); the ruling is cited in the code comment above the constant, where a citation belongs.
  4. The lint rule also sits on the report publish door. Accepted. validateChartBindings is the precedent for the same report against the same datasets; the dev measured the shipped corpus (showcase 4, todo 5) and no report declares the key, so the door refuses only lit probes. The docs page says the door refuses it, and it does.
  5. The skills example uses this_year, not the card's this_quarter. Accepted. The example's matrix puts close_date in columns with dateGranularity: 'quarter'; a quarter window would draw a single column. The service fills the granularity of a grouped time dimension from the dimension, so the example windows a year and buckets by quarter, which is the sensible picture.
  6. Translations landed in @objectstack/platform-objects. Accepted; that package is the one that bundles metadata forms, and it carries a patch changeset.
  7. No new public export. Accepted and verified in ① item 8.

Skills-docs follow-up owed. The PR's Skills-docs: line names skills/objectstack-ui/evals/analytics-inline-vs-dataset.json; both ui eval files sit at their token ceiling with zero headroom, so no report-period prompt was added. The triage (6436657773) says skills, docs and eval are owed and that a follow-up card may share #26475. Escalated as a follow-up card to file, not a defect in this PR: the ruling's wording is satisfied by the written Skills-docs: line, and an eval cannot be added without deleting another prompt, which is its own decision.

Governed surface. The diff touches skills/**, a Tier H register row. This record is the contract-tier review it asks for, and it lifts nothing: the landing is the maintainer's merge or enqueue, after an authorized review and with every check green. The queue guard is green on the draft.

④ Design judgment

packages/spec/src/ui/report.zod.ts. The design holds against its first consumer and its execution plan. The first consumer is #25873's Console report page; its contract is written in the TSDoc (the timeDimensions window, the apiOperations export predicate, what the file holds), and each sentence was checked against the code it names: the wire schema at data/analytics.zod.ts, the window rule at analytics-service.ts 612-614, the dimensionType switch in the compiler, and the list toolbar predicate the card measured. I tried to break it four ways. A report with dateRange and no dataset: the existing non-joined arm already refuses "a report needs dataset + values", so the lint rule never sees an unbound window. A dashboard author copying field: 'created_at': the alias answers "rename to dimension", and the lint rule then says unknown or not-date against the dataset, so the raw-field habit fails loudly at authoring, not silently at render. A joined report with dateRange: {} and no blocks: two issues at two paths, each correct. defaultRange: 'custom': accepted, as on the dashboard, and the TSDoc explains the sentinel. The omitted-dimension rule (exactly one type: 'date' dimension, else a named error) is the dashboard executor's compareTo rule, so the two surfaces resolve "which date column" the same way. No finding.

packages/spec/src/ui/list-view-export-options.ts. ReportExportOptionsSchema is a sibling of the list block, not a third carrier: includeHeaders by identity, fileNamePrefix re-described because the list sentence names labels a report lacks, formats as exclude(['json']) on the list enum with a report-side error map keyed on the input. The list block and its enum are byte-unchanged, and the test pins the list still accepting json, maxRecords and streaming. The closed-object construction mirrors the list's exactly, prime forwarded, so the unknown-key map is built on the refusal path. I tried ['csv'] (answered by name), docx (the enum message lists only the two members), and {} (parses, meaning default on). The one soft spot is that "default on" is stated in a describe while nothing renders it yet; the liveness row carries authorWarn with a hint that says exactly that, and the docs page has a "declared, not drawn yet" callout, which is this repo's sanctioned shape for a planned key whose renderer is the next card in the ruling's execution order. No finding.

packages/spec/src/ui/report.form.ts. One collapsed section, section-level visibleWhen on the same predicate the existing chart field uses, two composites whose names are schema keys, help texts that state the dimension rule and the permission axis. FormSectionSchema declares section visibleWhen and says it hides the whole section when false. The predicate census moved by one and the echo-decision pins moved by six, each with a dated comment. No finding.

skills/objectstack-ui/rules/dashboards.md. The hunk adds one dateRange line to the defineReport example and removes the three-line comment on order. The example is correct: close_date is a type: 'date' dimension of opportunity_metrics in that file's own dataset sketch, this_year draws four quarter columns, and the comment says "ONE type: 'date' dataset dimension". The removed sentence (a selected date dimension is already chronological; declare order only to change that) survives verbatim in content/docs/ui/reports.mdx:168 and in the report.zod.ts docblock at line 540, so no rule was lost from the catalog, only from this file. Two should-fix notes, neither a must-fix:

  • should-fix: the dashboard example at line 166 of the same file writes dateRange: { field: 'close_date', ... } and the report example 236 lines later writes dimension: 'close_date'. One clause on the new comment line, "dimension, a dataset dimension, not the dashboard's field", would stop an agent copying field from the dashboard block. The ratchet has 15 tokens of headroom, enough for the clause.
  • should-fix: the order line in the example now has no comment. A four-word trailer ("by a measure, descending") keeps the reason visible without re-adding the sentence that moved to the docs.

Implemented-by: claude/project-thread-xsyisg
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

… the served-schema census; skills: name the report dimension rule and the order default

Claude-Session: https://claude.ai/code/session_01WGufzhMJrMdCkQS33DYtmu
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5dde3fbd6d9bfafe8e03ae3fffef53d574eca6bc
Local-runs: none
Scope: packages/spec/src/ui/list-view-export-options.ts, packages/spec/src/ui/report.form.ts, packages/spec/src/ui/report.zod.ts, skills/objectstack-ui/rules/dashboards.md + packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts, packages/spec/src/ui/dashboard.zod.ts, packages/spec/src/kernel/metadata-protection.zod.ts, packages/spec/src/shared/strict-object.ts, scripts/check-skills-token-ratchet.mjs, content/docs/ui/reports.mdx

This is the delta review of 0c548c59..5dde3fbd. The full review 6440484161 (PASS at 0c548c59a8cac03bf6db83253d12b4bbc437b6a7) stands for everything else.

Inputs read: card #26548 (body and its three comments; nothing was added after the full review), PR #26628 (body, 30-file list, git diff 0c548c59 5dde3fbd), and the check-runs, check-suites, workflow runs and commit status on the new head. The old head's two red Test Core shards were read only for their failure annotations, so the pin move could be judged against what actually failed.

Delta shape. One commit, 5dde3fbd, parent 0c548c59. Two files: the census test (+4 -2) and the skills file (+2 -2). The merge-base with origin/main is fb91e979 at both ends, so no main merge rides in the delta and nothing was separated out. The commit message carries the two required trailers and no model id.

Gate state at the time of this review. The new head has no check-runs: total_count 0. Five check-suites exist (vercel, fly-io, claude, cloudflare-workers-and-pages, objectstack-fleet), all queued, and none of them is GitHub Actions. actions/runs?head_sha=5dde3fbd returns 0 runs; the same query on the old head returns 10, so the query works. The commit status is pending. The push landed at 11:21:24Z and the reading was taken at 12:05:56Z, 44 minutes later. The reason is not a slow queue: the PR reports mergeable: false, mergeable_state: dirty. main commit dbf7e04a (#26599, landed after the merge-base) rewrote the same CARD_PROPERTY_COUNTS block the delta edits, and a pull_request workflow does not start while GitHub cannot build the merge ref. So nothing is complete, nothing is green and nothing is running on this head. Every check is still owed.

Test Core on the old head, for the record. Test Core (7/8) failed in packages/metadata-protocol, the package of the census test, which is what the pin move addresses. Test Core (6/8) failed in packages/plugins/plugin-security, a package this PR does not touch, and its cause is still unestablished. On the new head there is no reading of either shard, because no run exists.

① Derived judgments

  1. The delta changes the accept set of nothing. report.zod.ts, list-view-export-options.ts, report.form.ts and every other spec file are byte-identical between 0c548c59 and 5dde3fbd. The public surface is unchanged. Right.
  2. The pin move is honest. ReportSchema at origin/main declares 14 keys of its own (name, label, description, type, dataset, rows, columns, values, runtimeFilter, order, drilldown, chart, blocks, protection) plus the 7 keys of the spread MetadataProtectionFields (_lock, _lockReason, _lockSource, _provenance, _packageId, _packageVersion, _lockDocsUrl): 21, the old pin. The head adds exactly dateRange and exportOptions to that shape and nothing else: 23. retiredKey() appears zero times in the file at either ref, so retiredTopLevelCount('report') adds the same 0 on both sides. The new aliases and guidance entries do not enter the shape: strict-object.ts says both run only from the unrecognized_keys path. So the served count rises by exactly the two new keys. Right.
  3. The test's comment names the move the same way its siblings do (type, old count, new count, the keys, the card). Right.
  4. Should-fix (a) landed. The report dateRange line in the skills example now reads "Period: ONE type: 'date' dataset dimension, not a dashboard-style raw field." This is what report.zod.ts says at the head: dimension names a dataset dimension that must declare type: 'date', and "the dashboard's field names a raw object FIELD". dashboard.zod.ts at the head still binds its dateRange.field as a raw field (filterBindings doc: dateRange.field ?? 'created_at'). Right.
  5. Should-fix (b) landed. The order line now carries "by measure; dates default to chronological". by: 'amount_sum' is a values measure, and the order docblock at the head (lines 539 to 543) says a selected date dimension already defaults to ascending, chronological, server-side, and order is declared to sort by a measure or reverse a time axis. Right.
  6. The old comment's parenthesis "(dashboard presets)" was dropped to pay for the clause. The fact survives in content/docs/ui/reports.mdx line 236 ("the dashboard's own: the same preset names"), so nothing left the catalog. Right.
  7. Token ratchet. The script prices a rules/*.md file as ceil(utf8 bytes / 4) with no stamp deduction. The file is 24944 bytes at the head: 6236 tokens, under the 6239 ceiling with 3 tokens of headroom. At 0c548c59 it was 24895 bytes, 6224 tokens, so the delta spends 12 of the 15 tokens the full review named. Right.

② Semver level

Unchanged by the delta. No published package moves: a test file and a skills file. The changeset and the PR body's declaration lines still read Clause-②: yes (widening) and Metadata-protocol: added, and both still describe the diff. No ADR-0087 disposition is owed.

Clause-②: yes (widening)

③ Boundary flags

The head cannot land as it stands, and this record covers no later head. The PR is in conflict with main on packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts. A local git merge-tree --write-tree origin/main 5dde3fbd names that one file as a content conflict (the other three overlapping paths auto-merge; authorable-surface/ui.json is routed to the os-regen driver locally, so that reading is not GitHub's, but GitHub's own dirty state agrees with the one named conflict). The resolution the dev owes is mechanical: take main's lowered counts (agent 24, app 22, dashboard 20, field 79, flow 22, skill 16, from #25767's aged-out tombstones) and keep report: 23 from this branch, with both comment blocks. That merge produces a new head, and the new head needs its own delta review and its own green checks before any landing. The merge should also take the §9 refresh (install, build, check:generated) because main moved packages/spec on its side.

Governed surface. The diff still touches skills/**, Tier H. This record lifts nothing: the landing is the maintainer's merge or enqueue, after an authorized review and with every check green. Today no check has run.

Skills-docs follow-up named in the full review (an eval prompt in analytics-inline-vs-dataset.json, blocked on a zero-headroom ceiling) is unchanged by the delta and still owed as a follow-up card.

④ Design judgment

skills/objectstack-ui/rules/dashboards.md. The two changed lines are the two should-fixes of the full review, in the form that review asked for, and each sentence was checked against the code it describes (items 4 to 6 above). The dashboard example at line 166 of the same file still writes field: 'close_date' and the report example now says in its own comment that a report takes a dataset dimension, not that field, so an agent copying from one block to the other is told the difference at the point of copying. The file sits 3 tokens under its ceiling; the next addition to this file pays by deletion. No must-fix.

  • should-fix: the PR body is stale on this head. Its Verification section names "final head 0c548c59a8ca" and its skills table reads 6224 tokens after; the head is 5dde3fbd and the true reading is 6236 (ceiling 6239). One edit to the body when the main merge lands.

packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (cited, not a hits file). The count moves by exactly what the served schema gained, and the comment records why. No finding on the delta; the conflict with main on this file is the ③ item above.

Implemented-by: claude/project-thread-xsyisg
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

Claude-Session: https://claude.ai/code/session_01WGufzhMJrMdCkQS33DYtmu
Co-authored-by: Claude <noreply@anthropic.com>

# Conflicts:
#	packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts
@os-zhuang
os-zhuang marked this pull request as ready for review October 27, 2026 12:34
@os-zhuang
os-zhuang enabled auto-merge October 27, 2026 12:34
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T13:11Z

Head be4a0204cdad1918fcf4a6571c7e87d8faabf2b5. Card #26548 (ruling 6408567749 letter B on #25873), claim 6437381453, dev report 6440254029.

  • Contract review: owed at CONTRACT_REVIEW_TIER for the published schema (report.zod.ts, list-view-export-options.ts, report.form.ts) and the governed skills/** file. The full review PASSed at 0c548c59 (6440484161), and the delta review of 0c548c59..5dde3fbd PASSed (6441526283). The two commits since (bb03d269, a main merge, and be4a0204, regenerating the authorable surface) carry no PR content in a reviewed file. The one hits-file line that moved, the LIST_VIEW_EXPORT_PDF_RETIRED sentence in list-view-export-options.ts, is main's own hunk, byte for byte. The census conflict was resolved as asked: main's lowered counts, plus report: 23.
  • Open question: ruled A. A joined container refuses exportOptions as well as dateRange. Lifting the refusal later is a pure widening.
  • Carried: the eval prompt (both ui eval files are at their token ceiling) is skills(objectstack-ui): no eval teaches a report's dateRange / exportOptions, because both ui eval files sit at their token ceiling (follow-up owed by #26548) #26633. The renderer is [Decision] The report page offers only "Edit": should a shipped report get a time-range filter, export and subscription, and in which order #25873.
  • Shape: Fixes #26548. @objectstack/spec and @objectstack/lint minor, @objectstack/platform-objects patch. Clause-②: yes (widening) and Metadata-protocol: added.
  • Gates: check-runs on the head are 42 success, 9 skipped and 0 red. Test Core is green on this head, including the 6/8 shard that was red on 0c548c59.
  • Landing: Tier H (skills/**). The maintainer approved and enqueued it. This seat arms nothing.

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:ui size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(report): a report declares a dataset-dimension date range and export options

3 participants