feat(types,plugin-charts): anchor ObjectChart props to ObjectChartSchema and declare the four keys its producers write (#7946) - #8884
Conversation
…ema (#7946) `ObjectChart` was published as `(props: any)`, so `ObjectChartSchema` anchored nothing and four keys its producers write and its renderer reads — `xAxisKey`, `series`, `aggregate`, `filter` — were declared on neither published copy. Maintainer ruling 2026-09-09 option A (director seat, summon #20, decision batch #108 item 3), applying objectui#6576's gallery treatment to the chart: - `ObjectChartProps.schema` is `ObjectChartSchema`; the published `.d.ts` no longer carries `props: any`. `ObjectChartProps` is exported. - The four keys are declared on BOTH copies, typed from their READ sites (`ChartRendererProps` for `xAxisKey`/`series`, `ObjectChart.tsx` for `aggregate`/`filter`), not copied from any producer's literal. - `colors` converges — the mirror declared it, the interface did not. - Read-census + compile-time pins join the `widget-schema-anchors` family. The anchor found a real defect it was pointed at: `groupByField` used the raw `groupBy` union as a row index, a field name and a drill-filter key, while its twin 200 lines up normalised the structured node. Both now share one normalisation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
|
Contract review at
|
| Face | Before | After |
|---|---|---|
ObjectChart props |
(props: any) |
ObjectChartProps { schema: ObjectChartSchema; dataSource?: any; onSegmentClick? } — ObjectChart.tsx:417-439; exported index.tsx:20 |
filter |
undeclared (rode index signature / passthrough) | any[] | Record<string, any> — TS :3146; zod objectql.zod.ts:1271-1274 |
aggregate |
undeclared | { field?; function?: enum5; groupBy?: string | { field?; dateGranularity?; alias? } } — TS :3163-3181; zod :1275-1286 (z.object, strip posture) |
xAxisKey |
undeclared | string — TS :3191; zod :1287 |
series |
undeclared | Array<{ dataKey: string; label?; variant?: 'current'|'comparison'; opacity?; dashArray?; chartType?; stack?; yAxis?; color? }> — TS :3204-3214; zod :1288-1298 |
colors |
zod only (since #3913) | both faces — TS :3216 |
aggregateRecords |
function: string |
function?: string — ObjectChart.tsx:167 (param widening, non-breaking) |
Verbatim check holds: series element and xAxisKey are ChartRendererProps.schema's internal arm byte-for-byte (ChartRenderer.tsx:53,72). aggregate.function enum and dateGranularity enum equal the spec's (chart.zod.ts:752-754, ChartGroupBySchema).
Now refused (TS, pinned): xAxisKey: 0, series: 'amount', series: [{ name }], function: 'average', dateGranularity beside groupBy, filter: 'stage=won', colors: 42, missing type, type: 'chart', chartType: 'radar', visible: 42. Zod refusals pinned: xAxisKey: 0, series: [{ label }], function: 'average' only. Still accepted, honestly pinned: unknown keys (xAxisKy) via [key: string]: any / .passthrough().
Findings
F1 — aggregate is a local near-copy wider than the by-reference spec symbol (§0.1; contract line needed). The spec requires function and groupBy, and field inside the structured node (objectstack/packages/spec/src/ui/chart.zod.ts:877-882; ChartGroupBySchema field: z.string(); react-blocks.ts ObjectChart.aggregate prop table). The PR makes all three optional on both published faces. Consequences: (a) the TS face now publishes aggregate: {} / { field: 'amount' } as legal; (b) the zod mirror's nested z.object (zod 4, strip posture) accepts aggregate: { groupby: 'stage', function: 'count' } and silently drops groupby — the exact failure the spec's strictObject history text was written to stop; (c) no typed in-tree producer needs the relaxation — every live aggregate forward is any (DashboardRenderer.tsx:591 (widget as any).data; app-shell viewDef: any); (d) objectql.ts:23-75 already imports @objectstack/spec/ui types, and the sibling #8895 declares its three keys by reference on the same interface. The PR's defence ("renderer accept set; every read is guarded") is coherent — ObjectChart.absentCategoryAxisRefusal-8168 deliberately feeds aggregate without a category and expects the refusal screen — so this is a genuine fork between renderer accept set and authoring door on a published face, not a slip. Recommendation: aggregate?: ChartAggregate / stripImportedDefaults(ChartAggregateSchema).optional() (the #8895 pattern); the #8168 test keeps its Partial<…>. Tightening later is a major; doing it now is free. Director's line decides; if the accept-set reading is ruled intended, F1 downgrades to a note.
F2 — filter record arm is a second dialect the spec does not declare (§0.1; non-blocking, prose must change). Spec FilterArray is array-only (data/filter.zod.ts:2384, 2440-2449; react-blocks ObjectChart.filter: FilterArray). Six sibling widgets declare filter?: any[] (objectql.ts:686, 2036, 2347, 2732, 2853, 2992); ObjectChartSchema:3146 becomes the only object-* widget with a record arm. In-tree provenance for the record form is test-only (compareTo*.test.tsx, DatasetWidget*.test.tsx, this PR's own pin) plus the drill spread at ObjectChart.tsx:982 — which itself miscomposes the spec's array arm into index keys. The docblock's claim that narrowing is "a contract decision across every object-* widget's filter" is not supported: siblings are already array-only; the decision is local to this card. Amend the :3124-3145 docblock and changeset to say so and name the successor (:982 array-arm composition + narrowing); keep the union for now.
F3 — Behaviour fix declared, not pinned (REWORK). The changeset's "BEHAVIOUR" paragraph and PR body claim a date-bucketed chart regained option-colour resolution, label→raw map and drill filter. Both new test files are compile-time/census; no runtime case exercises aggregate.groupBy: { field, dateGranularity, alias } through :700-702 or :950. Add one pin (structured groupBy → drill filter keyed by alias) or drop the behaviour claim from the changeset.
F4 — Changeset omits the narrowing that actually bit (REWORK, one sentence). minor on both published packages (private unset, files: dist) is correct per the ruling. The stated consequence is "a wrong VALUE TYPE is now a compile error"; what the eight edited test files show is that (a) type: 'object-chart' is now required on the literal, (b) chartType must be the declared union — a non-const object widens to string and fails, (c) series entries must be dataKey-shaped. Name these as the migration for TS consumers of <ObjectChart schema={…}>.
F5 — "Both sites now share one normalisation" is not what the diff does (REWORK or reword). ObjectChart.tsx:781-784 still carries the inline schema.aggregate?.groupBy as any normalisation and :654 casts the same read; only :950 uses aggregateGroupByKey. Route :781-784 through the helper (the PR's own rationale for hoisting it) or correct the PR body/changeset.
F6 — Zod refusal gaps (minor). No mirror refusal case for filter ('stage=won') or colors (42); widget-schema-anchors-7946.test.ts:153 only checks presence.
F7 — Parked items (verified). ① 'drawer' | 'dialog' prose ×3 is OUT: byte-identical at merge-base :844/:1368/:1380, origin/main, and PR head :934/:1464/:1476; none of the six hunks touches them. It is stale against spec 17.3.0 (chart.zod.ts:516 ['drawer','dialog','navigate']; react-blocks advertises 'navigate'). The PM parked it "at landing"; this PR owns the file — decide in-PR-before-ready or successor, and record which. ② #8895 reconciliation is OUT (correctly; #8895 has not landed) — see acceptance notes.
F8 — Prose inaccuracy (trivial). fieldOptionLabelRefetch.test.tsx:124 says tick "moves to a wrapper element that really takes it"; the code puts data-tick on <ObjectChart> itself and compiles only because TS skips hyphenated JSX attributes.
Verified correct: per-key AUTHORABLE/INTERNAL grounds all check out against the spec (ChartSeriesSchema:229 aliases dataKey→name; ChartAggregateSchema:868-869 names <ObjectChart filter>; validate-react-page-props.ts:463 parses ChartAggregateSchema); all eight producers compute xAxisKey/series; ObjectChartBlock (props: any) at :1427 correctly left to #8885; BaseSchema has no title (ledger claim holds); Fixes #7946 is the only closing keyword and is appropriate (parts 1-5 delivered, part 6 moot with an honest premise correction); no content/docs/releases/ edits; docs contain nothing this PR makes false; scope of the in-place fix meets all four conditions.
Acceptance notes
- Implemented-by:
session_01611D6ZaRaMmwTNQmSbk8MH· branchclaude/issue-7946-objectchart-schema-anchor(newestClaim:5606402447, PM dispatch byos-zhuang; matches PRhead_refand body link). Reviewed-by: this seat, independent session. - Overlap with feat(types): declare
drillDown/title/compareToon both published copies of ObjectChartSchema (objectui#8885) #8895 (d553e5d1b7, measuredgit merge-tree --write-tree): one conflicted file,packages/types/src/objectql.ts, two blocks — docblock header:3058-3149and interface-body tail:3171-3329;objectql.zod.tsauto-merges with zero markers — confirms the sibling reviewer. No same-name keys (feat(types): declaredrillDown/title/compareToon both published copies of ObjectChartSchema (objectui#8885) #8895:drillDown/title/compareTo; feat(types,plugin-charts): anchor ObjectChart props to ObjectChartSchema and declare the four keys its producers write (#7946) #8884:filter/aggregate/xAxisKey/series/colors). Both ledgers assert only "still read" (feat(types): declaredrillDown/title/compareToon both published copies of ObjectChartSchema (objectui#8885) #8895object-chart-undeclared-keys-8885.test.ts:271; feat(types,plugin-charts): anchor ObjectChart props to ObjectChartSchema and declare the four keys its producers write (#7946) #8884widget-schema-anchors-7946.test.tscensus) — no post-merge red; only feat(types,plugin-charts): anchor ObjectChart props to ObjectChartSchema and declare the four keys its producers write (#7946) #8884'sLEDGERED_UNDECLARED_READSprose ("declared on neither published copy") goes stale, for the later lander. Method divergence to reconcile at merge: feat(types): declaredrillDown/title/compareToon both published copies of ObjectChartSchema (objectui#8885) #8895 declares by reference, feat(types,plugin-charts): anchor ObjectChart props to ObjectChartSchema and declare the four keys its producers write (#7946) #8884 by local copy (F1). - The zod mirror is consumed by no validation gate outside
packages/types(ObjectChartSchemaused only as a TS type inObjectChart.tsx:10/:148/:421), so F1(b) is a published-shape defect, not a runtime one in this repo.
Generated by Claude Code
|
Handoff provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
|
Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published For this PR: its Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Head moved on this PR — a sync merge, no content change. Flagging it because a REWORK verdict is anchored to the previous head. Collision-resolution seat (session What I pushed:
⛔ I have not touched any label, assignee, claim or draft state, and I did not queue anything. Reverting the head would need a force-push, which this repo forbids, so it stands as pushed. One finding this PR's rework round may want to absorb. This PR and objectui#8895 are each green alone, but their union does not compile: Generated by Claude Code |
… the drill-title read (objectui#7946 rework)
Contract-review REWORK round on PR objectui#8884.
F1 — `ObjectChartSchema.aggregate` is `@objectstack/spec`'s `ChartAggregate` /
`ChartAggregateSchema` BY REFERENCE on both published faces, replacing the local
all-optional near-copy. The copy advertised `aggregate: {}` as legal authoring
and, being strip-postured, dropped a mis-cased member silently; no typed in-tree
producer needed the relaxation, and tightening later would be a `major`.
Read-site repair — the drill drawer's heading fallback resolves `schema.title`
through `pickLocalized` (`@object-ui/i18n`) instead of using it as a bare
string. The spec types that slot as `I18nLabel`, so the inline-locale-map arm
used to reach the heading as an object. Union-only defect with objectui#8895.
F3 — new runtime pin for the behaviour the changeset claims:
`ObjectChart.structuredGroupBy-7946.test.tsx` (drill filter keyed by the
projected column, alias and field arms, and the label→raw recovery).
F5 — the comparison-merge leg now routes through `aggregateGroupByKey`, the same
spelling the drill leg uses; the `as any` on the raw-node read is gone too.
F2/F4/F8 — prose and changeset corrections: the `filter` narrowing is local to
this node, the migration names the three narrowings that actually bite, and the
`data-tick` note states why a hyphenated JSX attribute compiles.
F6 — zod refusal cases for `filter` and `colors`, plus the strict-posture and
by-reference differentials for `aggregate`.
F7① — the stale `'drawer' | 'dialog'` claim about the spec is corrected in all
three prose sites; the palette advertisement itself is left to objectui#8885.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…er's declaration Second union-only defect with objectui#8895, measured on the merge of the two heads and NOT previously reported: `renderChart(compareTo: unknown, …)` stops compiling once #8895 declares `ObjectChartSchema.compareTo` as `DashboardWidget['compareTo']`, because on this branch the key still rides `BaseSchema`'s index signature. It was masked rather than absent: `type-check` is `tsc --noEmit && tsc -p tsconfig.test.json`, so the drill-title error in `ObjectChart.tsx` short-circuited the test project and hid every error it would have reported. Running the test project alone on the pre-rework union shows both. Typed at `DashboardWidget['compareTo']` — the symbol #8895 binds to and the declaration the value actually comes from (`DashboardRenderer` forwards `widget.compareTo` verbatim). All four call sites already pass that shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
The rework adds one import line to `packages/types/src/objectql.ts` (`ChartAggregate`), which moves the ledgered `@example` block from :1614 to :1615. `check:doc-examples` reports that as STALE LEDGER ROW + UNDECLARED FAILURE; the row's `codes` and `reason` are unchanged.⚠️ objectui#8895 moves the same row to :1617 (it adds three import lines). Neither number is right in the union — the later lander must RE-RUN the gate and take the number it prints, not pick a side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
落地前状态记录(ui@objectui 派发席)返工轮已交付,头 一、
|
…atch `main` moved and objectui#7946 (PR #8884) landed, editing the same `ObjectChartSchema` on both published faces. The previous round's stashed resolution was built on #8884's PRE-REWORK tree and is not replayed. 5 files, 7 hunks, re-derived against origin/main aeaa0f6: - `objectql.ts` docblock: both sides rewrote the same title line into different full docblocks. BOTH narratives kept; the two near-identical "ceiling" paragraphs folded into ONE section carrying both cards' examples and both counter-probe filenames; this branch's section "Four keys are still undeclared here, deliberately" had its premise falsified by the merge and is re-derived. - `objectql.ts` interface body: double insertion at one anchor, both kept. All 8 keys now declared exactly once on both faces. - `objectql.zod.ts` / `imported-defaults-8317.test.ts`: additive on both sides, both kept. - `zod-mirror-parity.test.ts`: BOTH sides add the SAME `SPEC_DERIVED_PAIRS` entry with different grounds. One entry, both grounds recorded. - `check-doc-example-types.mjs`: the `UNGATED_EXAMPLES` key embeds a line number and each side repaired it to a different one. Re-derived BY ANCHOR against the merged tree (the `@example` tag on `ObjectFormSchema.mobile`, byte-identical to the branch point): :1618. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Fixes #7946
Clause-②: yes— given by the ruling's execution note, not judged here: 「已发布类型契约从any收紧 + 声明面加键」.needs:contract-reviewis on the card and on this PR.Ruling: maintainer, director seat, summon #20, decision batch #108 item 3, 2026-09-09, option A. Verbatim, untranslated: 「7424 查 「什么在飞」 就应该查open的呀,直接关闭。 其他同意。」 — item 3 = A. B (keep
any) and C (declare without anchoring) were refused.The six ruled parts, and where each landed
ObjectChart's props typed byObjectChartSchema; noprops: anyon the published.d.tsObjectChartPropsinpackages/plugin-charts/src/ObjectChart.tsx, exported from the barrel. Builtdist/ObjectChart.d.ts:158now readsexport declare const ObjectChart: (props: ObjectChartProps) => React.JSX.ElementxAxisKey·series·aggregate·filterdeclared on both copiespackages/types/src/objectql.tsandpackages/types/src/zod/objectql.zod.tsChartRenderer's reads, never copied fromObjectView's literalsxAxisKey/seriesareChartRendererProps.schema's own declarations (the internaldataKeyarm, verbatim);aggregate/filtercome fromObjectChart.tsx's read sites. See per-key section belowcolorsdrift convergeswidget-schema-anchorsfamilypackages/types/src/__tests__/widget-schema-anchors-7946.test.ts, plus the compile-side pin beside the widget,packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.tsas anycasts inObjectViewremoved in this PRorigin/main— see the premise correction belowPremise correction on part 6
The casts are not in this diff because they are not on
main. objectui#7891 / PR objectui#7948 removed them when it landed, and left the reason in the file (packages/app-shell/src/views/ObjectView.tsx, the block above the chart branch): "Theas anycasts that used to sit on both literals were measured INERT ... so they are gone too". So part 6's deletion was already satisfied; what was still missing is the half that gives it meaning — the anchor that makes their absence measurable rather than incidental. That half is here, and the ablation below is the measurement.The four keys — AUTHORABLE or INTERNAL, per key
The 速读 (comment 5596356234) ended by flagging its own gap: 「四个键的精确值类型需从渲染器读法推导,本席未逐键核。」 Read per key, with ground:
aggregate— AUTHORABLE.@objectstack/spec'sChartAggregateSchemacalls itself "Inline aggregation for an OBJECT-bound chart", and its own file header names the carrier: "a real authorable prop on the react tier'sObjectChart objectName aggregate(ADR-0081), it is published in the generated react-blocks contract". objectstack#5020 wired the publish gate —packages/lint/src/validate-react-page-props.tscallsChartAggregateSchema.safeParse()on a staticaggregateliteral. This component's registryinputsadvertises it too. ⭐ Declared BY REFERENCE as the spec's ownChartAggregate/ChartAggregateSchema— see the rework section below. The first cut declared a local near-copy with every member optional, reasoning from this renderer's accept set (every read here is guarded); the contract review measured what that PUBLISHES, and it was an authoring door wider than the spec's on a key the publish gate parses.filter— AUTHORABLE. The spec spells the carrier literally, inChartAggregateSchema's own guidance for a misplaced key: "filteris a prop on the chart itself (ObjectChart filter=...), not part of the aggregate."@objectstack/spec/data'sFilterArraydocblock listsObjectChart.filteramong the React block props declaredFilterArrayinui/react-blocks.ts, and the registryinputscarries{ name: 'filter', type: 'array' }.xAxisKey— INTERNAL (relay-composed).ChartRendererPropsdeclares it and calls it, verbatim, "Internal binding. Authors write the specxAxis: { field }(or the report surface's bare string)". The author-facing spelling on this node isxAxisField, already declared. All five producers of anobject-chartnode compute it —dims[0]in the twoObjectViewrelays andListView,chartCategoryKey(effectiveAggregate, xAxisKey)in the two dashboard relays — and none forwards an authored value. It is absent from the registryinputs.series— INTERNAL (relay-composed). The{ dataKey }shape is the renderer's internal contract. The spec's author-facingChartSeriesSchemaREFUSESdataKeyby name — it is an alias entry that renames toname— and that file callschartType, the sibling internal spelling, "the INTERNAL spelling that is deliberately not part of the author contract". Every producer composes it (vals.map(...),[{ dataKey: effectiveYField }]), which is exactly the suspicion the card raised.Why the two internal keys are still declared, rather than withheld. Withholding is not neutral here.
BaseSchemais.passthrough()on the mirror and carries[key: string]: anyon the interface, soxAxisKey: 42already rides both published faces unvalidated today; not declaring keeps it that way, and additionally puts the pair inzod-mirror-parity'sUnmirroredDeclaredledger, which that file describes as a real defect in the pair rather than a neutral state. Declaring buys the VALUE check without minting authorable vocabulary, and each.describe()and TSDoc says which verdict the key carries so no author or generator reads them as authoring surface. ⛔ Neither was added to the registryinputs.Measured, not asserted — the probe MOVES
The card's before-state: deleting both
as anycasts lefttsc --noEmitgreen, over a program--listFilesconfirmed contained the file — meaningless, because the consumer wasany. Here is the same probe after the anchor, from the committed tree, with the mutation proved on disk (marker counts and blob hash) before anything ran:Leg 3 is the control the acceptance asked for: it moves when the change is wrong, not when the subject is broken. The identical mutation is silent without the anchor and loud with it, so the redness is attributable to this PR and to nothing else.
Ablation on one key's declaration, same discipline, red by test-case name on two independent instruments:
The second instrument matters: the parity guard's unmirrored-keys half is a compile-time assertion, so
vitestalone cannot see it. A run that only read vitest would have reported one instrument where there are two.The defect the anchor found
Typing
schematurned four expressions insideObjectChart.tsxinto compile errors thatprops: anyhad been absorbing. One root cause, and it is a real runtime bug rather than a typing nuisance:groupByis a union — a bare field name, or the structured date-bucketing node{ field, dateGranularity, alias }. The file resolved the group column twice. The comparison-merge leg normalises ("Normalise to the underlying string field name so all column lookups work"); the drill/label leg 200 lines below did not, and used the raw union as a row index (row[groupByField]), as a field name (fieldOptionLabel) and as a drill-filter key. On a date-bucketed chart every one of those resolves against the node's stringification, so option-colour resolution, the label→raw reverse map and the drill filter all silently miss. Both sites now share one module-local normalisation, andaggregateRecords'groupBy: stringparameter is fed the projected column rather than the union. A second, smaller one: the metadata-label leg passedschema.objectName— optional since ADR-0021 — into a resolver typedstring; the object-bound precondition it already assumed is now written down in its guard.This is a bounded in-place fix, and it is declared rather than folded in quietly: same defect class as the card (an unchecked schema key reaching a reader that cannot handle its declared shape), mechanical, with its shape already pinned by its own twin in the same file, no new gate surface. The alternative was to reintroduce an
as anycast at each site — the exact anti-pattern this ruling removed.What the anchor does NOT buy
BaseSchemastill carries[key: string]: any(objectui#5155), so a misspelled key still compiles:xAxisKy: 'stage'is accepted. That is the same ceiling objectui#6576 accepted knowingly for the gallery, and both new pin files carry a counter-probe for it so nobody reads the anchor as more than it is. What moved is that a wrong value type is now refused loudly.⭐ One exception, and it arrived with the rework:
aggregate. Bound to the spec's own object, which isstrictObject, so a mis-cased member INSIDE it —aggregate: { groupby, function }— is refused by name rather than dropped. That is a property of the spec's schema, not of the anchor; it is why the by-reference form is worth more than a structurally identical local copy.返工轮 — contract-review REWORK (2026-09-10)
Review of record: Verdict REWORK, findings F1–F8, at
#8884 (comment) (anchored to head
09f8ffaa33; the head then moved by a diff-neutral sync merge to7e7134160, flagged in comment5613430888). The contract-review-TIER requirement was separately lifted for this PR by comment
5612089561 — that changes the reviewing seat, not the findings, and every finding below is worked as
written. Two lines were decided by the director before this round and are executed rather than
re-opened: F1 takes the by-reference form, and the drill-title read site is repaired here.
① F1 —
aggregateis now the spec symbol, by referenceObjectChartSchema.aggregatewas a local near-copy with all three members optional. It is nowChartAggregate(TS) /stripImportedDefaults(SpecChartAggregateSchema)(zod) — the sameChartAggregateSchemathe react-page publish gate parses, and the pattern the sibling PRobjectui#8895 already uses on this interface.
What changed on each published face, measured:
aggregatefield?·function?·groupBy?, structuredgroupBynode withfield?functionandgroupByREQUIRED,fieldoptional, structured node'sfieldREQUIREDaggregate: {}and{ field: 'amount' }compiled{ function: 'count', groupBy: 'stage' }still acceptedz.object— strip:{ groupby: 'stage', function: 'count' }parsed CLEAN and lostgroupbystrictObject— refused BY NAME (unrecognized_keysnamesgroupby)ChartAggregateSchemaitselfThree type pins now say this in a way a look-alike cannot satisfy: an invariant
EqualagainstChartAggregateitself, plusfunction/groupByrequired andfieldoptional read off THISinterface. On the mirror, a probe-for-probe differential against the raw spec schema, with the
verdict list asserted to contain both answers so agreement is not agreement-on-refusing-everything.
The renderer's accept set is deliberately unchanged and stays wider: untyped producers forward
aggregateasany, so out-of-contract documents still arrive, and objectui#8168's named refusalscreen is what they get.
ObjectChart.absentCategoryAxisRefusal-8168.test.tsxkeeps feeding exactlythose documents through a named
UNAUTHORABLE(...)helper, so the file says which literals aredeliberately out of contract instead of relaxing its parameter type and unchecking every other one.
② The read-site repair — the union-only drill-title defect
resolveDrillTitle(drillDown, drillEvent, schema.title || 'Details')passed a schema title into aparameter typed
string. objectui#8895 declares that title asI18nLabel— a plain string or aninline locale map — so an author writing the map arm got an OBJECT rendered as the drill drawer's
heading. Repaired through
pickLocalized(@object-ui/i18n), the repository's publishedlocale-aware resolver, pinned as the twin of the spec's own
resolveI18nLabel; the module-privatelabel helper in
plugin-chartswas NOT used, because it is not locale-aware and would mint a seconddialect disagreeing with the published one.
Measured on the real union, not argued — the merge of this head with objectui#8895's resolved
tree, built and type-checked:
plugin-chartstsc -p tsconfig.test.json11ddbab62)typesbuild,typestype-check andplugin-chartstype-check all 0, and 220 files / 3741 tests green⭐ Two, not one — and the second was measured for the first time here. The pre-rework union also
fails at
ObjectChart.compareTo.test.tsx:100, whererenderChart(compareTo: unknown, …)stopscompiling once objectui#8895 declares
compareToon the schema. It was masked, not absent:type-checkistsc --noEmit && tsc -p tsconfig.test.json, and the&&short-circuits — thedrill-title error in
ObjectChart.tsx(main project) hid every error the TEST project would havereported. Running the test project alone on
11ddbab62shows both. The parameter is now typed atDashboardWidget['compareTo']— the symbol objectui#8895 binds to and the declaration the valueactually comes from (
DashboardRendererforwardswidget.compareToverbatim); all four call sitesalready passed that shape.
pickLocalizedneeds a current locale, so the filenow reads
useObjectTranslation().language. The drill heading becomes locale-aware while the chartheading beside it is not —
normalizeChartSchema'slabel()picks the FIRST string in the map,which follows key order rather than the user's language. That asymmetry is pre-existing and is NOT
widened here; it is filed as objectui#8943.
③ F7① — the parked
'drawer' | 'dialog'item: decided, and which half went whereDecision: the prose is corrected in-PR; the palette advertisement is a named successor. They are
not one item.
ChartDrillDownSchemadeclares the chart drill target as two arms, and that advertising'navigate'would hand an author a value the publish gate rejects. Measured on@objectstack/spec17.4.0, both halves are FALSE:ChartDrillDownSchema.targetis['drawer','dialog','navigate'], andpackages/lint'svalidate-react-page-props— which parsesthat very schema — depends on
@objectstack/spec: workspace:*, so the gate accepts the valuetoday. Corrected in the two
ObjectChart.tsxcomment blocks and inplugin-charts/src/index.test.ts, which carried a fourth copy of the same stale rationale that thereview's count of three did not include. Correcting one copy and leaving its twin is worse than
leaving both.
descriptionstring and its pin). Adding'navigate'to the registryinputsdescription WIDENS an advertised authoring vocabulary and requires flipping a behaviour pin
(
index.test.tsasserts'navigate'is absent). That is a contract decision aboutdrillDown— akey this card declares on neither published face and objectui#8885 declares on both, and whose
PR objectui#8895 already records these very sites in its acceptance notes. Taking it here would
split one key's story across two PRs with two rulings. ⇒ 承接者: objectui#8885. Recorded there
and here rather than filed as a third card; the reason is now written into the code beside both the
description and the pin, so the next reader finds it instead of re-deriving a claim that has
already gone stale twice.
④ The rest of the verdict
aggregatewider than the specfilterrecord arm is a second dialect; the "contract decision across everyobject-*widget" claim is unsupportedobject-*widgets declarefilteron this interface and all six are array-only, so the decision is LOCAL to this node. Successor named on the member doc and filed as objectui#8944 — the drill spread mis-composes the array arm into index keys, which is what blocks the narrowing. Union keptplugin-charts/src/__tests__/ObjectChart.structuredGroupBy-7946.test.tsx. Drill filter keyed by the projected column (alias arm and field-fallback arm), the label→raw reverse map recovered through it, and a bare-string controltypenow required on the literal;chartTypemust be the declared union (a non-constholder widens tostring);seriesentries must bedataKey-shaped — plus theaggregaterequiredness from F1aggregateGroupByKey, the same expression the drill leg uses. Theas anyon the raw-node read inrunAggregateis gone too — that read is the one site that must stay raw (the node goes to the server verbatim; normalising it would dropdateGranularity), and the comment now says sofilter: 'stage=won'andcolors: 42now have mirror refusal cases, alongside an accepting control asserted issue-free so a mirror that refused everything could not pass'drawer' | 'dialog'fieldOptionLabelRefetch.test.tsxprose inaccuratedata-tickstays ONObjectChartand compiles because TypeScript does not check hyphenated JSX attribute names against the prop type — it does not "move to a wrapper"⑤ The acceptance instrument, stated separately
⭐ Both census pins compute their declared key set from
Object.keysof the zod mirror. A changethat drops an entire TypeScript member block runs the vitest suite fully green.
pnpm testalone istherefore not an acceptance instrument for this file, and the two verdicts are reported apart:
e805024d7pnpm --filter @object-ui/types type-check(tsc -p tsconfig.test.json)pnpm --filter @object-ui/plugin-charts type-checkpnpm exec vitest run packages/types/ packages/plugin-charts/The type-check is also what enforces every
@ts-expect-errorin the two compile-side pins: anexpectation that stops being needed fails as TS2578, which vitest cannot see either.
⑥ Ablation — the new runtime pin can fail, and fails for the right reason
From the committed tree, with the mutation proved on disk by marker count AND blob hash before
anything ran (the drill leg's normalisation reverted to the raw union; the anchor is the one-line,
two-space-indented form, distinct from the comparison leg's annotated two-line form):
Three of four turn red and the fourth — the bare-string control — stays green, which is exactly
what it is there for: the ablation reports that the structured-node arm broke, not that the file
broke. The failure text is the defect itself, verbatim.
⑦ Collision with objectui#8895 — re-measured on this head, for whoever lands second
Agreed order is this PR first.
refs/os-dev-collide/pr8895-resolvedwas read but not touched(still
11ddbab62); the union above was built on a throwaway local branch which was deleted, sonothing new is pushed. Re-derived against this head:
packages/types/src/objectql.tsfilter·aggregate·xAxisKey·series·colors+drillDown·title·compareTo)packages/types/src/zod/objectql.zod.tspackages/types/src/__tests__/imported-defaults-8317.test.tsIMPORTEDdifferential list. Keep both entriespackages/types/src/__tests__/zod-mirror-parity.test.ts'objectql.zod.ts#ObjectChartSchema'with different comments. Keep one entry and both commentsscripts/check-doc-example-types.mjsObjectFormSchemaledger row because both add import lines toobjectql.ts— this PR to:1615(one line), objectui#8895 to:1617(three). Neither number is right in the union: re-runpnpm check:doc-examplesand take the number it printsPost-merge the union is green: type-checks EXIT 0 and 220 files / 3741 tests. Only the prose in this
PR's
LEDGERED_UNDECLARED_READSgoes stale (its three keys become declared) — the assertions allsurvive, because both ledgers assert only that the key is still READ.
Verification
All at the final commit
e805024d7.pnpm --filter @object-ui/types buildpnpm --filter '@object-ui/plugin-charts^...' build(dependency closure)pnpm --filter @object-ui/types type-checkpnpm --filter @object-ui/plugin-charts type-checkpnpm exec vitest run packages/types/ packages/plugin-charts/pnpm exec vitest run packages/plugin-dashboard/ packages/plugin-list/ packages/plugin-view/ ObjectView.chartRelay-7823types,plugin-charts,plugin-dashboard,plugin-list,plugin-view,app-shell,react,corescripts/check-doc-example-types.mjs)check-doc-example-types.test.ts+check-doc-example-shared-reader.test.ts— 73 tests passeddoc-examples·doc-types·doc-snippets·doc-fences·doc-example-readers·spec-symbols·spec-floors·control-bytes·handler-key-reads·element-data-source-declaration·vi-mock-specifiers·vi-mock-inherit·comment-mask-corpus·self-import·esm-specifiers·unreferenced-sources·readme-exports·side-effects-array·phantom-deps·unused-deps·published-tsconfig-exclude·entry-guard·lint-rule-coverage·i18n-keys·i18n-dead-keyscheck:sdui-registration-pinsapps/console/dist/assetsand there is no console build here. Neither pass nor fail. This diff changes nosideEffectsarray and no registration key (the edit inside that block is comment-only), and CI builds the console — the Console Performance Budget comment on this PR is the evidence it doesLint — the FULL population, not a narrowing.
eslint . --format jsonfrom the repo root judged4698 files, 0 errors, 12562 warnings, EXIT 0. Per package:
types235 files / 0 errors / 278warnings;
plugin-charts65 files / 0 errors / 357 warnings (358 before — one fewer, theas anyF5 removed).
calculateConfigForFilereturnslanguageOptions.parserOptions={}for both editedsources, so type-aware linting is not enabled and no declaration in this diff can move a verdict on a
file it did not touch — recorded because that is the invariance a narrowing WOULD have needed, and
it also explains why the two package runs and the repo-wide run agree.
验收备注
Measured while doing this. None is fixed here.
normalizeChartSchema'slabel()picks the FIRST string in an inlinelocale map, so a chart heading follows key insertion order rather than the user's language. This is
the successor for the asymmetry ② introduces, and the repository already publishes the right answer
(
pickLocalized).filterARRAY arm into indexkeys, silently dropping the widget's own filter on the arm the spec and the registry
inputsadvertise. F2's named successor, and the prerequisite for narrowing
filterto one arm.inputsdescriptionwithholdstarget: 'navigate'. See ③ for why the prose and the advertisement parted company.403, "sessions arebound to their configured repositories — use repository-scoped endpoints"), and one targeted MCP
search_issuesreturned empty but its control query could not run (API rate limit already exceeded), so that empty is NOT a reading. The dedup that DID hold is repository-scoped andlabel-scoped: the four open issues carrying
package: plugin-charts, with objectui#7946 itself asthe control that must appear, and it did.
appear in it.
ObjectChartSchemaleavesundeclared (
compareTo/drillDown/title) andObjectChartBlock'sprops: any. Both areobjectui#8885's, which is open and dispatched.
🤖 Generated with Claude Code
https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
Generated by Claude Code
Generated by Claude Code