Skip to content

Commit 73580e7

Browse files
os-zhuangclaude
andauthored
feat(lint): the react-page publish gate PARSES ChartAggregateSchema (#5020) (#5608)
`<ObjectChart aggregate={{…}}>` was judged by a gate that RE-DERIVED its schema: a local `CHART_FUNCTIONS` copy of the function vocabulary and a hand-written twin of the count/field refinement. Two implementations of one contract, free to drift — and since unknown-key handling is a property of a PARSE rather than of a list of `if`s, a gate with no unknown-key check at all. `validate-react-page-props` now calls `ChartAggregateSchema.safeParse()` on a statically resolvable literal, exactly as #5022 did for `ChartDrillDownSchema` one prop over, and both hand-derived copies are deleted. Newly reported, all previously silent: a missing `function`, a non-string `field`, a present-but- wrong `groupBy`, a non-object `aggregate`. Two messages are re-worded because they now come from the schema (FROM/TO in the changeset). zod 4 collapses a union's arm failures into one `invalid_union` whose message is the bare "Invalid input", so the arms are unpacked into the finding — otherwise `groupBy` rejections would say nothing an author can act on, and the same collapse will hide #5583's strict `unrecognized_keys`. An absent `groupBy` reports at `warning`, alone among the graded violations: the schema and the published react-blocks type declare it required while objectui's `ObjectChart` honours its absence (`|| schema.xAxisKey`) and `chartAggregateCategoryKey` documents the ungrouped single-row result. Gating it would enforce a declaration the platform does not itself keep; #5583 decides which side moves. The unknown-key hole is NOT closed by this commit and is pinned open on purpose: both schemas are still STRIP, so the parse drops `groupby` / `dateGranularty` instead of reporting them. Wiring the parse is the precondition, not the closing (#5583). Records updated in the three places the strictness ledger requires: `chart.zod.ts` header prose (schema body untouched), `chart.test.ts` pin comments (assertions unchanged — the posture did not move), and the ledger's two `chart.zod.ts` rows `no gate` → `authorable` with the counts artifact regenerated (`no gate` 31 → 29, `authorable` 11 → 13). Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE Co-authored-by: Claude <noreply@anthropic.com>
1 parent a287d1c commit 73580e7

7 files changed

Lines changed: 622 additions & 54 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): the react-page publish gate PARSES `ChartAggregateSchema` instead of re-deriving it (#5020)
6+
7+
`<ObjectChart aggregate={{…}}>` is judged at publish time by
8+
`validate-react-page-props`. That gate used to RE-DERIVE the aggregate's
9+
declaration: a local `CHART_FUNCTIONS` copy of the function vocabulary and a
10+
hand-written twin of the schema's count/field refinement. Two implementations of
11+
one contract, each free to drift — and, because unknown-key handling is a
12+
property of a **parse** rather than of a list of `if`s, a gate with no
13+
unknown-key check at all. The rule now calls `ChartAggregateSchema.safeParse()`
14+
on a statically resolvable literal, exactly as #5022 did for
15+
`ChartDrillDownSchema` beside it, and both hand-derived copies are deleted:
16+
`@objectstack/spec` is the single source of the vocabulary and the refinement
17+
again.
18+
19+
**Newly reported (all `error`, all previously silent).** These are shapes the
20+
schema, the published react-blocks type and objectui's renderer already agreed
21+
were wrong; the old gate simply could not see them:
22+
23+
| authored | before | after |
24+
|---|---|---|
25+
| `aggregate={{ field: 'total', groupBy: 'status' }}` (no `function`) | accepted | `aggregate.function: Invalid option: expected one of "count"\|"sum"\|"avg"\|"min"\|"max" (nothing is set there)` |
26+
| `aggregate={{ field: 42, function: 'sum', groupBy: 'status' }}` | accepted | `aggregate.field: Invalid input: expected string, received number` |
27+
| `aggregate={{ function: 'count', groupBy: 42 }}` | accepted | `aggregate.groupBy: Invalid input (received 42) — no accepted form matched: (1) … (2) …` |
28+
| `aggregate="count"` / `aggregate={[]}` | accepted | `aggregate must be a configuration object, not string.` |
29+
30+
**Re-worded, same verdict.** Two messages now arrive from the schema rather than
31+
from this rule's own copy. If you match on lint output, update the text:
32+
33+
- FROM `aggregate.function "median" is not an aggregation this chart can run.`
34+
(hint: `Use one of: count, sum, avg, min, max.`)
35+
TO `aggregate.function: Invalid option: expected one of "count"|"sum"|"avg"|"min"|"max" (received "median")`
36+
— the vocabulary is the enum's own, and the author's value is echoed back from
37+
the input (the one part zod does not put in the message).
38+
- FROM `aggregate.function "sum" has no "field" to aggregate.`
39+
TO `aggregate.field: aggregate.function "sum" needs a "field" to aggregate (only "count" may omit it).`
40+
— verbatim from the schema's refinement.
41+
42+
The rule id (`react-chart-aggregate-invalid`) and the severity are unchanged for
43+
both.
44+
45+
**`aggregate.groupBy` missing is a NEW `warning`, deliberately not an error.**
46+
It is the one violation the platform does not agree with itself about:
47+
`ChartAggregateSchema` and the published react-blocks type both declare `groupBy`
48+
**required**, while objectui's `ObjectChart` honours its absence
49+
(`schema.aggregate?.groupBy || schema.xAxisKey`) and this protocol's own
50+
`chartAggregateCategoryKey` documents the ungrouped single-row result. Gating it
51+
would break a working authoring shape to enforce a declaration the platform does
52+
not keep, so the finding explains the situation and does not fail
53+
`os lint`/`validate`/`compile`. Whether the schema loosens or the renderer
54+
tightens is decided on #5583.
55+
56+
**What this does NOT fix yet.** `ChartAggregateSchema` and `ChartGroupBySchema`'s
57+
object arm are still STRIP-posture, so the parse this gate now runs **drops** an
58+
unknown key rather than reporting it: `groupby` for `groupBy` and
59+
`dateGranularty` for `dateGranularity` still degrade a chart to one ungrouped
60+
point with the build green. Wiring the parse is the precondition for closing
61+
that, not the closing — `.strict()` is a property of a parse, and until now there
62+
was no parse to make strict. The spec-side tightening is **#5583**; the tolerance
63+
is pinned by name in this rule's tests so a wired gate cannot be mistaken for a
64+
closed one (#4583).

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ Remaining strip sites by class:
2929

3030
| Bucket | Sites |
3131
|---|---|
32-
| authorable — the ruling's forced scope | 11 |
32+
| authorable — the ruling's forced scope | 13 |
3333
| unresolved — needs a per-schema verdict | 33 |
3434
| wire / open — out of forced scope | 106 |
3535
| no door — no carrier, ADR-0049 territory | 14 |
36-
| no gate — carrier live, no parse | 31 |
36+
| no gate — carrier live, no parse | 29 |
3737

3838
## Posture, per triaged directory
3939

@@ -169,11 +169,11 @@ over it is here.
169169

170170
| Bucket | Sites |
171171
|---|---|
172-
| authorable — the ruling's forced scope | 2 |
172+
| authorable — the ruling's forced scope | 4 |
173173
| unresolved — needs a per-schema verdict | 0 |
174174
| wire / open — out of forced scope | 2 |
175175
| no door — no carrier, ADR-0049 territory | 14 |
176-
| no gate — carrier live, no parse | 31 |
176+
| no gate — carrier live, no parse | 29 |
177177

178178
### `data/` — open
179179

docs/audits/2026-07-unknown-key-strictness-ledger.md

Lines changed: 11 additions & 4 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)