Skip to content

feat(analytics): order the time axis by default, give reports a sort declaration (#3916) - #3921

Merged
os-zhuang merged 2 commits into
mainfrom
claude/matrix-report-date-ordering-4nou69
Jul 29, 2026
Merged

feat(analytics): order the time axis by default, give reports a sort declaration (#3916)#3921
os-zhuang merged 2 commits into
mainfrom
claude/matrix-report-date-ordering-4nou69

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #3916.

The problem

A matrix report with a date dimension across rendered its columns in arbitrary order (2026-07-01, 2026-07-05, …, 2026-07-02). Declaring dateGranularity made the bucket keys sortable (2026-07, 2026-Q3) without making anything sort them.

Nothing in the chain supplied an order:

  • resolveOrdering returned undefined unless the selection carried order explicitly (dataset-executor.ts:318-344).
  • The ObjectQL aggregate path has no ordering grammar, so buckets came back in Map-insertion order (in-memory-aggregation.ts:84), and native SQL declines every granularity query (native-sql-strategy.ts:30) — so bucketed queries always land there.
  • The console pivot builds column headers in row-arrival order.

And the author could not ask: DatasetSelection.order existed on the wire, but ReportSchema had no ordering field at all. Dashboard widgets had their own options.sortBy channel; reports had nothing.

One correction to the issue's diagnosis: ObjectQLStrategy does render ORDER BY in its echoed SQL, but that string is documentation — execution goes through engine.aggregate(), which never receives an ordering. The practical gap is exactly as reported.

The fix

Server default — the symptom fix. When a selection states no order (and no limit, whose existing fallback already ordered by every dimension), each selected dimension the cube types as time now defaults to ascending, in selection order. Bucket keys are minted sort-stable (2026-07, 2026-Q3, 2026-W31) precisely so ascending is chronological. This lands on both strategy paths: a real ORDER BY where native SQL serves the query, the executor's post-pass where a bucketed query goes to the ObjectQL path. Null / empty buckets stay last, as everywhere else.

Deliberately narrow — only time dimensions get a default, so grids with nothing wrong with them are not reordered. For the reported matrix (rows: [owner], columns: [closed_month]) that makes the column dimension the sole sort key, so the pivot's arrival-order headers come out strictly chronological. Sorting the row dimension too would have put one owner's months first and appended everyone else's out of order.

Author surface. ReportSchema.order / blocks[].order — a list of { by, direction } sort keys, most significant first. An array rather than a Record because key order is the contract and JSON object key order should not have to be. by must name a dimension the report groups by (rows/columns) or a measure it displays (values); anything unknown, or a duplicate, fails at authoring time rather than becoming an ordering that silently does nothing. A joined report orders per block — declaring order on the container is an error. reportSelectionOrder() lowers the list into the DatasetSelection.order a renderer posts, returning undefined for an empty list so the runtime's own defaults still apply.

An explicit order still wins outright — the chronological default is a default, not a policy, so "newest month first" is one declaration away.

Liveness classification — please read

report.order ships as planned + authorWarn, not live. The framework half is complete and live (schema, reportSelectionOrder, executor), but objectui's DatasetReportRenderer builds the selection it posts and does not yet carry report.order into it. Marking it live would be the exact failure the gate exists to catch. This needs a follow-up objectui PR to flip.

The time-axis default needs no renderer change and is live now — it is what actually closes the reported bug.

Out of scope here

The console pivot (packages/console) ships as a prebuilt bundle with no source in this repo, and the report→selection lowering lives in objectui. Neither is reachable from this change.

Also

Corrected docs/audits/2026-06-reportschema-property-liveness.md:21 — the note claiming the retired sortOrder "now lives on the dataset" was wrong, as the issue observed. No dataset-level sort field ever existed.

Testing

  • 13 new executor cases (dataset-time-axis-order.test.ts) pinning the default and its boundaries: time-only, selection order, explicit-order precedence, the bare-limit fallback, native-SQL ORDER BY emission, quarter/year keys, nulls last.
  • 9 new schema cases covering order, its validation, and reportSelectionOrder.
  • Full suites green: service-analytics 312, spec 6832, downstream-contract 14.
  • Regenerated artifacts (json-schema.manifest.json, authorable-surface.json, api-surface.json, references/ui/report.mdx); check:liveness, check:spec-changes, check:upgrade-guide, check:skill-docs, check:skill-refs, check:skill-examples, check:docs, check:api-surface, check:react-blocks, check:doc-authoring all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CYJ5WSiCgd522s1zCWtmFT


Generated by Claude Code

…declaration (#3916)

A matrix report with a date dimension across rendered its columns in arbitrary
order. Declaring `dateGranularity` made the bucket keys sortable without making
anything sort them, and nothing in the chain supplied an order: `resolveOrdering`
returned undefined unless the selection carried one, the ObjectQL aggregate path
has no ordering grammar so buckets came back in Map-insertion order, and the
pivot builds its column headers in row-arrival order. The report author could not
ask either — `DatasetSelection.order` existed on the wire but `ReportSchema` had
no ordering field at all.

Server default: when a selection states no `order` (and no `limit`, whose own
fallback already ordered by every dimension), each selected dimension the cube
types as `time` defaults to ascending, in selection order. Bucket keys are minted
sort-stable precisely so this works. Lands on both strategy paths — a real ORDER
BY where native SQL serves the query, the executor's post-pass where a bucketed
query goes to the ObjectQL path. Deliberately narrow: only time dimensions get a
default, so grids with nothing wrong with them are not reordered.

Author surface: `ReportSchema.order` / `blocks[].order` is a list of
`{ by, direction }` keys, most significant first — an array because key order is
the contract. `by` must name a `rows`/`columns` dimension or a `values` measure
the report selects; anything else, or a duplicate, fails at authoring time.
A `joined` report orders per block. `reportSelectionOrder()` lowers the list into
`DatasetSelection.order`, returning undefined for an empty list so the runtime
defaults still apply.

Ships as `planned` + authorWarn in the liveness ledger: the framework half is
complete and live, but objectui's DatasetReportRenderer does not yet carry
`report.order` into the selection it posts. The time-axis default needs no
renderer change and is live now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYJ5WSiCgd522s1zCWtmFT
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 29, 2026 9:43am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling size/l labels Jul 29, 2026
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, packages/services, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…` field drifted

`check:i18n` failed on the PR: the `order` field added to `reportForm` carries a
label + helpText, and the platform-objects metadata-form bundles are GENERATED
from the form declarations, so all four locales drifted from a fresh extract.

Regenerated via `scripts/check-i18n-bundles.mjs --write`; the diff is exactly the
one new `report.order` entry per locale, nothing else.

The zh-CN / ja-JP / es-ES entries are hand-translated rather than left as the
merge-mode English filler. Not cosmetic: platform-objects sits at 0 in
scripts/i18n-coverage-baseline.json, so `check:i18n-coverage` is already the
strict gate for this package and a new untranslated declared label would have
turned the next run red. Verified both ways — `check:i18n` reports 8 bundles in
sync (merge mode preserves the hand translations), and `os lint --json` reports 0
i18n issues for the config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYJ5WSiCgd522s1zCWtmFT
@os-zhuang
os-zhuang merged commit f752ee3 into main Jul 29, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/matrix-report-date-ordering-4nou69 branch July 29, 2026 10:03
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/l tests tooling

Projects

None yet

2 participants