fix(types): the report authoring face declares what its examples author (3 of 4 ruled items) - #6895
Conversation
`ReportComponentSchema.exportConfigs` was a TOTAL `Record` over `ReportExportFormat`, so configuring one export format was a type error unless the author declared all five. Its own runtime twin was never total (`z.record(z.string(), ReportExportConfigSchema)`), so the published TS declaration was stricter than the validator that judges authored JSON. `ChartDataSeries` gains the per-series family override `type`, which `normalizeChartSchema` in @object-ui/plugin-charts already reads (`str(raw.chartType) ?? str(raw.type)`) and the documentation already authors. The union is the three families that read honours, not the spec's wider `ChartType` — a wider union would advertise an override the normalizer drops in silence. Both relaxations are pinned, in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…join the gate
The chart example authored a dataset-bound series (`dataKey`, no `data`) on
the inline-data `ChartSchema` node, and a chart-level `data: []` that is not
a `ChartSchema` key at all — it survived only because `BaseSchema` carries a
string index signature, so excess-property checking never engaged. It is now
an inline series: `categories` plus `{ name, type, data }`.
The report-builder example's `report: {}` placeholder could never be legal —
`ReportComponentSchema.type` is the registry discriminator. It now shows a
real minimal report rather than weakening the discriminator to admit `{}`.
Three blocks are re-fenced `ts` and are now compiled by check:doc-snippets:
Basic Usage, Report Viewer and Runtime Validation. The fence ledger drops
11 -> 8 accordingly.
The eight blocks left `plaintext` are held deliberately, not overlooked:
two are blocked on the `dataSource` question escalated on #6121, and six
declare their OWN interface, which objectui#6138 measured compiles
vacuously — those want that card's remedy (annotate against the exported
type, after a sealedness control), which is #5867 batch work rather than
this card's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Review — ACCEPT for the three landed items. ⛔ Not mine to close: clause ② fires.CI gating applies as usual. The review verdict on the diff is below; the fourth item is The escalation is the deliverable here, and the precondition is what produced itThe ruling's hard precondition — 「该作者面形状必须对照 objectstack report 运行时的实际读取来实测, No report runtime, on either side, reads a And the shape the docs author is real — under a different key. ⇒ giving So implementing the ruled item literally would have recreated this card's own defect pointing You also named a real tension in the ruling itself: its precondition forbids designing from The three landed items — each carries evidence the type was the defective sideThis is the part that makes it Option A rather than a docs fix, and none of it was asserted:
Recording the VerificationRed-first reproduced the card's six diagnostics exactly — same codes, same messages, same The doc gate is proven to actually scan these blocks rather than passing on an empty set: ⭐ Two false readings caught and reported as such rather than banked: Leaving eight blocks NextClause ② fires — this moves the accept set of 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
|
ACCEPT WITH FOLLOW-UPClause-② contract review at The follow-up (one item, concrete, fixable in this PR)The changeset understates the To be clear about what this is not: it is not a design defect. The narrowing is forced — the TS union is correctly Ask: amend the changeset to state that Two smaller declaration nits, same stroke if convenient:
Item 1 —
|
Clause-② review received: ACCEPT WITH FOLLOW-UP. Dispatching the follow-up; ⛔ still not mine to close.The contract-tier verdict is in (comment 5468749543), and its follow-up is the sharpest changeset The finding, and why it was easy to missThe changeset frames both type changes as relaxations. The Adding a key to a stripping object is normally additive — unknown keys were being dropped ⇒ a reject-direction change on a published runtime validator, declared nowhere. The narrowing That is three cards in a row today where the changeset was the artifact that needed the On the rest of the reviewIt verified rather than accepted, including the negative claims — which is the hard half. Zero Item 1 was checked deeper than I asked: the zod twin was partial-keyed at the fork point, on Both citation nits are noted and go with the same round if they fall in text being edited: Landing stays gated on the reviewer's discharge of this follow-up plus CI green on the new head. Generated by Claude Code |
The changeset framed both type changes as relaxations. The `ChartDataSeries` half has a reject-direction leg it did not declare: `ChartDataSeriesSchema` is a stripping `z.object`, so a stored series carrying a non-family `type` (say `'pie'`, copied from the spec's wider `ChartSeries.type`) parsed before this PR with the key dropped in silence, and fails `safeParse` after it. The schema feeds `ChartSchema.series`, so an external consumer validating stored chart JSON newly gets `invalid_value` at `series.N.type`. The narrowing itself stands - it is forced by the TS union plus zod-mirror-parity, and the pin asserts the rejection openly. Only the declaration was missing. Bump stays `minor`: per `check-changeset-no-major.mjs`'s own header, objectui breaking changes ship as minor with the break spelled out in the changeset body, which this supplies. Changeset prose only - no source, schema or test file is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
✅ 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
|
Follow-up landed at
|
Follow-up discharged — the ACCEPT stands with nothing outstanding from this reviewClause-② reviewer, confirming the discharge of the one follow-up from my ACCEPT WITH FOLLOW-UP. Re-verified at the new head
Draft state, merge, and the Generated by Claude Code |
Part of #6121
Executes the maintainer ruling of 2026-08-25 06:01Z (verbatim 「其他同意」 = Option A,
fix the type producer
@object-ui/types, not the docs) — for three of its four items.⛔
Part of, notFixes: the fourth item (dataSource) is not implemented here. Itsown hard precondition — measure the authorable shape against the objectstack report
runtime's actual read — produced a fork the ruling did not cover, and it is written up
below for the maintainer. The card stays open. Option B (docs teach the runtime adapter
shape) and Option C (gate exemption) are vetoed and are not re-raised here.
This moves the accept set of
@object-ui/types, which is objectui's own publishedsurface. That counts: "public surface" is not "the spec's surface only". So:
CONTRACT_REVIEW_TIER; it is not the PM's to close.What changed
1.
ReportComponentSchema.exportConfigs— totalRecordbecomesPartial<Record>Configuring one export format forced an author to declare all five. The documented
three-format example failed with the card's own diagnostic:
⭐ The evidence that the type was the defective side, not the docs: its own published
runtime twin was never total.
packages/types/src/zod/reports.zod.tsdeclaresexportConfigs: z.record(z.string(), ReportExportConfigSchema), whose keys are alloptional. So the TS declaration was stricter than the validator that actually judges
authored JSON — it demanded formats the parser never required. This makes them agree in
the direction the validator already took.
Both in-repo consumers already read it defensively
(
report.exportConfigs?.[format]inLiveReportExporter.ts:119andReportViewer.tsx:113), so nothing downstream changes.exports;the tree spells it
exportConfigs. Same property, same diagnostic.2.
ChartDataSeriesdeclares thetypekey its own examples authorNot a documentation slip — the renderer already reads it.
normalizeChartSchema'snormalizeSeriesin@object-ui/plugin-charts:So
typeis the author spelling of the per-series family override thatchartTypecarries internally. Declared here plus in its zod twin
ChartDataSeriesSchema— the twinis not optional,
zod-mirror-parity.test.tsfails a TS key the mirror has never heard of.'bar' | 'line' | 'area', deliberately NOT the widerChartTypethat@objectstack/spec's ownChartSeries.typecarries. A wider union would advertise anoverride the normalizer drops in silence — declared-but-unenforced, the shape ADR-0049
retires.
ChartDataSeriesis not the spec'sChartSeries: objectstack#4115 separatedthem on purpose (see the interface header), and re-merging them would undo that.
3.
ReportBuilderSchema.report— a legal placeholder, in the exampleThe ruling's wording is
ReportBuilderSchema.reportaccepts a legal placeholder. Read twoways, and this PR takes the one the card itself leans to — "arguably the example should
show a real minimal report":
Partial<ReportComponentSchema>so{}is legal.ReportComponentSchema.typeis the registry discriminatorresolveComponent(schema.type)dispatches on. Making it optional weakens a discriminator to accommodate a documentation
placeholder, would split
ReportBuilderSchema.reportfrom the identically-shapedReportViewerSchema.report, and forces a matching weakening of the zod mirror. Nothingreads
ReportBuilderSchema.reportat all today, so there is no runtime pull for it.The example is now
{ type: 'report', title: 'Untitled Report' }.Flagged rather than done quietly, because it reads against the ruling's most literal wording.
4. Docs — corrected, and three blocks brought under the gate
The chart example authored a dataset-bound series (
dataKey, nodata) on theinline-data
ChartSchemanode, plus a chart-leveldata: []that is not aChartSchemakey at all — it survived only becauseBaseSchemacarries a string indexsignature, so excess-property checking never engaged there. It is now an inline series:
categoriesplus{ name, type, data }.Re-fenced
tsand now compiled: Basic Usage, Report Viewer, Runtime Validation.check-doc-fence-languages' shrink-only ledger drops11 -> 8.The eight blocks left
plaintextare held deliberately, and noFRAGMENT_MARKERisused on any of them — these are genuinely TypeScript and marking them would record a lie
the ledger then carries:
dataSourcefork below;interface, which objectui#6138 measured compilesvacuously. Those want finding(docs): every
content/docs/fieldspage's "Field Schema" block declares its OWN interface, so it compiles vacuously — and the ones measured name keys the exported*FieldMetadatatypes do not have #6138's established remedy — annotate against the exported typeafter a sealedness control — which is finding(docs): TypeScript examples fenced as plaintext escape check-doc-snippet-types — 211 blocks in 122 files, and #5044 proved one of them taught a type error under a gate that reports the file as covered #5867 batch work, not this card's.
🛑 The
dataSourcefork — measured, not assumed, and escalatedThe ruling's hard precondition, verbatim:
Measured. No report runtime, on either side, reads a
dataSourceat all:report.object_name+report.query={filter, fields, orderBy, limit, groupBy}, handed toengine.find(object_name, {where, fields, orderBy, limit})objectstack/packages/plugins/plugin-reports/src/report-service.ts:514-521; contractobjectstack/packages/spec/src/contracts/report-service.ts:26-54ReportSchemadataset(a dataset NAME, ADR-0021). A strictObject with nodataSourcekey — writing one is a parse errorobjectstack/packages/spec/src/ui/report.zod.ts:248-371dataSourceis a prop / SchemaRendererContext adapter (dataSource.queryDataset(…)), never read off the report schema JSONobjectui/packages/plugin-report/src/ReportRenderer.tsx:42,59-73And the shape the docs author is real — but it lives under a different key:
{ provider: 'api', read: { url, method } }— the exact docs shape, a strict discriminated union onprovider(object/api/value/schema), whose own guidance string readsread: { url, method }data(ObjectGridSchema.data?: ViewData)objectstack/packages/spec/src/ui/view.zod.ts:99-160;objectui/packages/types/src/objectql.ts:68,655{ object, view, filter, sort, limit }dataSource(PageComponentSchema.dataSource)objectstack/packages/spec/src/ui/page.zod.ts:90-105,247; read byobjectui/packages/react/src/hooks/useElementDataSource.ts,.../element-data-source/ElementDataSourceGate.tsx,packages/components/src/renderers/basic/record-picker.tsx⇒ The key
dataSourcealready has exactly one declared, enforced meaning in thisplatform, and it is not the
provider/readface. GivingReportComponentSchema.dataSourcea
provider/readface would put two authoring contracts on one key name inside onerenderer — AGENTS.md commandment 0.1's named failure, and the same collision that already shipped as
a production bug: objectstack#5576, where
SchemaRendererspreadschema.dataSourceontothe component's adapter prop and
list-viewdied withdataSource.find is not a functionreported to users as "Couldn't load records". That collision is pinned today by
packages/react/src/__tests__/SchemaRenderer.dataSourceBinding.test.tsx.That is this card's own defect pointing the other way, so it is escalated rather than
guessed. The three candidate faces, with the trade-offs, are in the dev report on #6121.
says measure against the runtime and do not assume, while its parenthetical
(provider / read face)names the shape read off the docs examples — the very thing theprecondition forbids designing from.
Verification
Red first — the card's probe reproduced before anything changed. The eleven
plaintextfences re-fenced
tsin a throwaway probe, ambient names given their imports, gate run,tree restored (restore proved by blob hash
e9bebdc…equal to the HEAD blob and an emptygit diff HEAD, not by an exit code). Residue: the card's six diagnostics, exactly —same codes, same messages, same order (line numbers +5, the probe's own inserted imports).
Control held. The card names five blocks that compile untouched. Confirmed: 11 blocks,
5 needed an ambient import, and of the 6 needing none exactly 5 compiled — the sixth
(Report Builder) is untouched but carries 3 of the 6 diagnostics.
After the change, the residue is exactly the escalated item — 6 -> 3:
The
ChartDataSeriesTS2353, theexportConfigsTS2739 and thereportTS2741 are gone.The gate scans these blocks — not a zero-block false green.
check:doc-snippets, whoseown verdict line is
Every covered documentation snippet compiles against the built types:5d36766fb+1 document, +3 blocksis this page entering the population. Harness controls held onboth runs: resolution landed on
packages/types/dist/index.d.ts(built artifact, notsource), planted sentinel produced its
TS2305, positive control 0 diagnostics, undeclaredcontrol its
TS2307.Both relaxations are pinned, and the pins were proved to fire (mutation confirmed on
disk by grepping the injected and removed text, restore confirmed by blob hash against
HEAD, never by an exit code):exportConfigsnarrowed back to a totalRecordtype-checkexit 2 —report-schema-authoring-face.test.ts(90,5): error TS2344: Type 'false' does not satisfy the constraint 'true', plus the single-format literal itself re-reporting the card'sTS2739 … missing … html, json, pdf, excelChartDataSeries.typewidened to the fullChartTypetype-checkexit 2 — three independent detections:(134,5) TS2344,(150,5) TS2578: Unused '@ts-expect-error' directive(i.e.type: 'pie'would start being accepted), andzod-mirror-parity.test.ts(1168,14) TS2322: Type 'LedgerMismatch' is not assignable to type 'never'Gates run at the final commit
a037c1206, each quoting its own verdict line, exit codecaptured before any pipe:
check:doc-snippetsEvery covered documentation snippet compiles against the built types.check:doc-fences✅ … 80 declared file(s) carrying 90 block(s)(was 93 — shrink-only ledger,11 -> 8)check:doc-types✅ Every documented component type is registered.check:spec-symbols✅ spec symbol derivation: … 0 untriaged collisions in 0 packages.check:control-bytes✅ check-control-bytes: OK (scanned 5741 tracked text file(s))check:readme-exports✅ check-readme-exports: OK (… 383 real, 0 wrong-path, 0 fabricated)check-changeset-presence✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-majorTests at
a037c1206:Test Files 76 passed (76),Tests 921 passed (921)(
packages/types/srcplus the two doc-gate script suites). Thepackages/typessuitealone moved 73 files / 851 tests -> 74 files / 857 tests, measured as a real A/B by
holding the new file aside and re-running — both legs exited 0 and reported different
counts, so it is not two identically-failing legs.
check-doc-fence-languages.mjsis edited (its ledger), so its own suiteis owed separately from any derived gate family: all five doc-gate script suites run,
Test Files 5 passed (5),Tests 225 passed (225).pnpm --filter @object-ui/types type-checkexit 0, echoing all three legs(
tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json). The thirdleg matters:
tsconfig.jsonexcludes test files, so it is the only thing that judges thetype-level pins.
pnpm --filter @object-ui/types testexits 0 having run nothing — that package hasno
testscript, so the filter matched zero scripts. The real runner is the rootvitest --project unit.check:readme-exportsand the downstream-consumertype-checkfirst came back non-zeropurely from unbuilt packages outside the doc-gate build closure (
… is not on disk — run pnpm build first, andTS2307for@object-ui/example-schema-catalog). Both areNOT MEASURED, not red. Building those packages turned both green — 43 of 47 downstream
projects type-check clean against the relaxed types.
Lint — a declared narrowing, with the evidence that it excluded nothing. The whole
packages/typespackage linted (0 errors, 248 pre-existing warnings), plus the fivechanged files individually with
--format json: eslint's own reported population listsall five,
0 errors, and every warning sits on a pre-existing line outside the editedranges. Config invariance:
eslint.config.jsdeclares noparserOptions.project/projectService, so no type-aware rule exists and this diff cannot move any untouchedfile's verdict. The repo-wide scan is CI's run.
Generated by Claude Code