Skip to content

feat(spec,lint): pin <ObjectChart> aggregate result-column naming and validate its axes (#3701) - #3725

Merged
os-zhuang merged 1 commit into
mainfrom
claude/objectchart-aggregate-naming-nb642o
Jul 28, 2026
Merged

feat(spec,lint): pin <ObjectChart> aggregate result-column naming and validate its axes (#3701)#3725
os-zhuang merged 1 commit into
mainfrom
claude/objectchart-aggregate-naming-nb642o

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3701.

#3684 extended ADR-0021 axis checking to report charts, list-view charts and dataset-bound page chart components, but had to leave the react <ObjectChart> block out: it is OBJECT-bound (objectName + an inline aggregate), aggregate existed in the contract only as the description string '{ field, function, groupBy }', and nothing in the repo said what the aggregated result columns were called. With no columns to resolve against, its axis bindings could not be validated — and inventing a convention would have manufactured false positives (ADR-0072 D1).

1. The convention, recorded rather than invented

Every path that can serve an object-bound chart already agreed; it was simply never written down:

path category column value column
engine aggregate() w/ structured groupBy (objectui ObjectChart) groupBy.alias ?? groupBy.field the aggregation alias, set to field || function
legacy analytics/cube query groupBy remapped back to field
client-side fallback (aggregateRecords) groupBy field
the console's own object chart-view wiring (ObjectView) xAxisKey: groupBy series[].dataKey: field

So: an object-bound aggregate returns rows keyed by the RAW FIELD NAMESgroupBy for the category column, field for the value column, the literal count for a fieldless count, plus <field>__comparison under a comparison overlay. That is the deliberate opposite of the dataset path, whose rows are keyed by the declared measure name (sum_amount) — the trap chart-measure-unknown catches. Only the dataset path has an author-chosen name to key by.

  • chart.zod.tsChartAggregateSchema / ChartGroupBySchema / ChartAggregateFunctionSchema replace the description string, and reject a non-count function with no field (which reached the renderer as sum(undefined) and drew a blank chart).
  • chart-aggregate.ts — records the convention and exports the derivations (chartAggregateCategoryKey / chartAggregateValueKey / chartAggregateResultKeys) so producers and checkers cannot drift apart by re-deriving the rule separately.

2. <ObjectChart>'s contract now names the props the block actually reads

ChartConfig's xAxis / yAxis / series shapes reached this block and were silently dropped — it consumes xAxisKey and series[].dataKey. Advertising them violated ADR-0078 (a prop the author writes is honored or rejected, never silently dropped), and validating yAxis[].field would have meant validating a prop that does nothing. They leave dataProps; chartType, xAxisKey and series join the React overlay where the other bindings live, per ADR-0082 decision 3.

(chartType was not published at all before, so an author following the contract had no way to pick anything but the default bar chart.)

3. The prop gate now reads attribute VALUES

validate-react-page-props previously only checked prop names. For <ObjectChart> it now evaluates static literal values:

rule severity what it catches
react-chart-field-unknown error aggregate.field / aggregate.groupBy naming a field the bound object does not declare
react-chart-aggregate-invalid error an unimplemented aggregation function, or a non-count function with nothing to aggregate
react-chart-axis-unknown error an axis naming a column the aggregate never returns (incl. a dataset-style sum_total), or a category axis bound to the value column
react-chart-axis-inert warning the xAxis / yAxis shapes this block never reads

Value reading is opt-in per block and evaluates only static literals. Skipped silently, to keep false positives at zero: a prop driven by React state or a variable, an object literal with a shorthand or spread property, a usage carrying {...spread}, a chart given inline data (its columns are the author's own), and objects another package defines.

Verification

  • @objectstack/spec 6702 tests, @objectstack/lint 467 tests — all pass.
  • All seven generated-artifact gates in sync: check:react-blocks, check:api-surface, check:skill-refs, check:skill-docs, check:docs, check:spec-changes, check:skill-examples.
  • The golden page (renewals-pipeline.page.ts) now binds its axes explicitly, so the ADR-0082 chain is proven end to end — swapping total for sum_total there fails os validate:
✗ React-source page prop check failed (1 issue)
  • page "showcase_renewals_pipeline" › <ObjectChart>: "sum_total" is not a column this
    aggregate returns, so the axis plots nothing. Object-bound aggregate rows are keyed by
    the RAW FIELD NAMES (unlike a dataset, whose rows are keyed by measure name).
  • Showcase os validate otherwise passes clean (remaining warnings are pre-existing and unrelated).

Companion PR

objectstack-ai/objectui — the convention exposed a real bug: a fieldless count keyed its value column undefined in three of the four runtime paths, and the legacy analytics path additionally deleted the count the server had returned. Since this PR's lint and skill docs now recommend the fieldless form, that had to be fixed rather than advertised (Prime Directive #10).

Out of scope — follow-ups worth filing

Found while auditing what this block actually honors; not fixed here to keep the change proportionate (Prime Directive #10 says file, don't silently expand):

  1. <ObjectChart>'s remaining inert config props — showLegend is read by nothing, and title is only used for the drill-down drawer title, never rendered as a chart title.
  2. validate-chart-bindings §3 validates properties.yAxis[].field for dataset-bound page chart components, but the same renderer ignores yAxis there too — so that rule checks a no-op prop.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMHZBHTjH4rw8xmDYirFC7


Generated by Claude Code

… validate its axes (#3701)

#3684 extended ADR-0021 axis checking to report charts, list-view charts and
dataset-bound page chart components, but had to leave the react <ObjectChart>
block out: it is OBJECT-bound (objectName + an inline aggregate), `aggregate`
existed in the contract only as the description string '{ field, function,
groupBy }', and nothing in the repo said what the aggregated result columns were
called. With no columns to resolve against, its axis bindings could not be
validated, and inventing a convention would have manufactured false positives
(ADR-0072 D1).

Record the convention rather than invent one. Every path that can serve an
object-bound chart already agreed: the engine's structured-groupBy aggregate
(whose alias objectui sets to `field || function`), the legacy analytics query
(which remaps its measure key back to `field`), the client-side fallback, and
the console's own chart-view wiring (`xAxisKey: groupBy`, `series[].dataKey:
field`). An object-bound aggregate returns rows keyed by the RAW FIELD NAMES —
`groupBy` for the category column, `field` for the value column, the literal
`count` for a fieldless count, plus `<field>__comparison` under a comparison
overlay. That is the deliberate opposite of the dataset path, whose rows are
keyed by the declared measure `name`; only the dataset path has an
author-chosen name to key by.

spec:
  - ChartAggregateSchema / ChartGroupBySchema / ChartAggregateFunctionSchema in
    chart.zod.ts replace the description string, and reject a non-count
    function with no field (which reached the renderer as sum(undefined) and
    drew a blank chart).
  - chart-aggregate.ts records the convention and exports the derivations —
    chartAggregateCategoryKey / chartAggregateValueKey /
    chartAggregateResultKeys — so producers and checkers cannot drift apart by
    re-deriving the rule separately.
  - <ObjectChart>'s contract now names the props the block actually reads.
    ChartConfig's xAxis/yAxis/series shapes reached it and were silently
    dropped, which ADR-0078 forbids; they leave dataProps, and chartType,
    xAxisKey and series join the React overlay where the other bindings live.

lint: validate-react-page-props now reads attribute VALUES, not just names, for
<ObjectChart> — react-chart-field-unknown (aggregate.field/groupBy off the
object), react-chart-aggregate-invalid (unimplemented function, or a non-count
function with nothing to aggregate), react-chart-axis-unknown (an axis naming a
column the aggregate never returns, incl. a dataset-style sum_total, or a
category axis bound to the value column), react-chart-axis-inert (the axis
shapes this block never reads). Value reading evaluates only static literals: a
prop driven by React state, a usage carrying a spread, a chart given inline
data, and objects another package defines are skipped — an unresolvable binding
is not a wrong one.

The golden page binds its axes explicitly so the chain is proven end to end:
swapping `total` for `sum_total` there fails `os validate` with the new rule.

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

vercel Bot commented Jul 28, 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 28, 2026 12:27am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec.

104 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 @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @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/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • 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/lint, @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/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 @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/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/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.

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.

<ObjectChart> aggregate result-column naming is undefined, so its axis bindings cannot be validated

2 participants