fix(types): narrow ObjectCalendarSchema.data to the protocol's array arm - #9348
Conversation
`ComponentPropsMap['object-calendar'].data` on `@objectstack/spec` declares
`z.array(z.unknown()).optional()` — "Pre-fetched records — skips the internal
fetch". Both published faces of `@object-ui/types` declared `ViewData` on the
same key: a `{ provider, items }` config object, which that row refuses by kind.
After objectui#8348 put the renderer on the protocol's side (decision batch #83,
maintainer verbatim 8348 uses the protocol as the authority), this mirror was the
lone published face still teaching the config-object spelling, so an author who
validated against `@object-ui/types` got a green verdict for metadata the
renderer ignores, `os validate` refuses and the save gate rejects.
- TS face: `data?: ViewData` -> `data?: SpecObjectCalendarProps['data']`,
DERIVED from the protocol's own row so the key cannot drift from it again.
- Zod mirror: `ViewDataSchema.optional()` -> `z.array(z.unknown()).optional()`.
- Requiredness unchanged on both faces, so the zod-mirror-parity ratchet is
unmoved (verified with `tsc -p tsconfig.test.json`, which is the only
instrument that sees that ledger's type-level reconciliation).
- The `data` equality with `TsObjectGanttSchema['data']` is INVERTED rather than
deleted, and both sides are named; the `staticData` and `objectName`
equalities were re-checked individually and still hold.
- `requireRecordSource`'s docblock says what `data` means per member: the ladder
is shared, the arm is not.
`object-map` / `object-gantt` keep the `ViewData` arm — neither has a
`ComponentPropsMap` row, so the row governing them is this package's own.
`@objectstack/spec` is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ 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 — PASSHead reviewed: ⛔ Not SELF-REVIEW: the diff came from a Tier — MEASURED. This seat is ① Derived judgments
② Semver
③ Boundary flags
⭐ The sharpest question, and it was measured properly
⇒ the green is a measurement, not a skip. ⛔
|
Provenance — carriers cleared, pre-landing checks passedHead: Dual carrier CLEARED, ⛔ not stripped. Both removals in one act: this PR and card objectui#9239. ⭐ That distinction is the whole reason the second carrier exists — a legitimate clear leaves two removals seconds apart, a strip leaves one, and since 「闸门被剥不是红灯是放行」 the two are otherwise indistinguishable in the evidence.
落地前检 — all three, run BEFORE the ready flip
|
Fixes #9239
What this changes
ComponentPropsMap['object-calendar'].dataon@objectstack/specdeclaresz.array(z.unknown()).optional()— "Pre-fetched records — skips the internal fetch". Both published faces of@object-ui/typesdeclaredViewDataon the same key: a{ provider, items }config object, which that row refuses by kind. This PR brings both faces onto the protocol's array arm.This is a breaking narrowing of a published authoring type — the accept set genuinely shrinks, which is the point.
needs:contract-reviewis hung on this PR for that reason.packages/types/src/objectql.tsdata?: ViewDatadata?: SpecObjectCalendarProps['data'](derives the protocol's row; resolves to an optional array of UNKNOWN)packages/types/src/zod/objectql.zod.tsViewDataSchema.optional()z.array(z.unknown()).optional()Requiredness is unchanged (optional on both faces), which is the
zod-mirror-parityratchet's criterion.Premise 1 — re-measured first-hand, not inherited
The card, triage and the dispatch order all carried this reading second-hand; triage said so explicitly (
@objectstack/specwas not installed in its container). Measured here from the installed artifact in this worktree,@objectstack/spec17.4.0, resolved atnode_modules/.pnpm/@objectstack+spec@17.4.0_ai@7.0.65_zod@4.4.3_/node_modules/@objectstack/spec, imported through the published@objectstack/spec/uientry point:⇒ Premise confirmed. The protocol says exactly what all three readings said it says.
Two things the same probe turned up, both load-bearing for the acceptance list:
ComponentPropsMaphas 45 keys and neitherobject-ganttnorobject-mapis among them (object-*keys:object-grid,object-metric,object-kanban,object-calendar,object-form,object-master-detail-form). So the published row governing those two blocks is this repo's ownViewDataSchema.optional()— the reason they keep the object arm is measured, not assumed.ComponentPropsMap['object-calendar']is spelledObjectCalendarPropsSchemain the spec's own.d.ts, withtype ObjectCalendarProps = z.input of that schema. The TS face derives it rather than re-spellingunknown[], so this key cannot drift from the protocol a second time — which is the defect this card exists for.⭐ The parity ledger did NOT move
packages/types/src/__tests__/zod-mirror-parity.test.tsis held by PR #9343 this round and was not edited — but that file is a type map over every mirror, so the question is whether a row moved inside it. Instrument used:pnpm --filter @object-ui/types type-check, all three projects (tsc --noEmit+tsconfig.examples.json+tsconfig.test.json). exit 0.Proof that the instrument actually covers that ledger rather than skipping it —
tsc -p tsconfig.test.json --listFilesresolves 649 files, and the ledger is among them:object-calendarhas noKnownDriftentry and still has none: both faces narrowed together at equal requiredness, and the zod input face ofz.array(z.unknown())is exactly whatSpecObjectCalendarProps['data']resolves to. ⛔ A greenviteston that file would have proved nothing here, and neither would re-running only the buildtscproject; both are recorded false-green paths and neither was used as evidence.The
:102/:104/:106equalities — each re-checked individually:102Equal of the calendar's data and the gantt's data— inverted, not deleted. A bare deletion would have left the divergence unwitnessed (nothing would notice the two keys silently converging again), so the row now pins that they DIFFER, and a second row names the other side explicitly:TsObjectGanttSchema['data']is stillViewData or undefined. ⛔ The pin cannot be satisfied by moving the gantt.:104staticData— re-checked on its own:any[] | undefinedon both members. Still true, kept.:106objectName— re-checked on its own:string | undefinedon both members. Still true, kept.⭐ The trap in this very file
PR #9234's report recorded that its "the renderer still calls the ladder" row matched a bare
resolveRecordSourceConfig(schema)whichObjectCalendar.tsxsatisfied from a docblock line that merely names the function — a name occurring in a file is not a call site. Every new assertion in this PR is a verdict, not a text match:safeParseresults compared against the protocol's ownsafeParseresults on the same documents, plus type-levelEqualrows. The one pre-existing text-matching row in the file (resolveRecordSourceConfig(schema, 'array'), already hardened by #9234) is untouched.What else moved, and why
:165dataOnlyfixture is array-shaped now. Consequence handled rather than papered over: the gantt parity block feeds each member the arm its own published row declares (withArm), because that block measures the ladder — had it kept feeding one arm to both, the gantt would refusedataOnlyby kind and the comparison would read as a ladder divergence that does not exist.DATA_DOCUMENT, the typed literal, is array-shaped; the provider-block spelling is now a compile error there, which is the declaration half of the fix.success: truefordata: { provider: 'object', object: 'events' }now asserts the refusal, at the key. That line was the mirror's half of the divergence.requireRecordSource's docblock says whatdatameans per member: the ladder is shared (presence,!== undefined, whatever the kind); the arm is not. On the calendar,datais pre-fetched records, ⛔ not a source to fetch from.content/docs/plugins/plugin-calendar.mdx— its "refused on this block" section listedos validate, the save gate and the renderer;@object-ui/typeswas missing from that list precisely because it was the face that still accepted the document. It is listed now, with the breaking-narrowing note. Thecheck:doc-*gates and the registered audit pass.Verification
All exit codes captured by redirect before any pipe.
pnpm --filter @object-ui/types type-check(3 projects)tsc -p tsconfig.test.json --listFilespnpm --filter @object-ui/types buildpnpm exec vitest run packages/types/pnpm exec eslint . --no-inline-config --format jsonno-explicit-anywarnings, none introduced)pnpm --filter @object-ui/types lintcheck:spec-symbolscheck:control-bytescheck:new-line-citationscheck:entry-guardcheck:changeset-claimscheck:phantom-depscheck:unused-depscheck:doc-typescheck:doc-fencescheck:doc-example-idscheck:doc-example-readerscheck-changeset-presence.mjs,check-changeset-no-major.mjsGate list derived from
.github/workflows/lint.yml's andci.yml's step lists, ⛔ not frompackage.json—check:entry-guardis in that list and was run.Corpus sweep
⛔ No authored document anywhere in the repo carries the retired spelling — re-measured rather than inherited from PR #9234's report:
object-calendarappears in zero*.json/*.yml/*.yamlfiles and zero files underexamples/, and no literal annotated: ObjectCalendarSchemasetsdataat all. Sopnpm check(the CLI self-check over this repository) has no calendar document to refuse, and nothing that renders today stops rendering because of this PR — objectui#8348 is where runtime behaviour moved.Out of scope
⛔
object-gantt/object-mapobject arms untouched. ⛔@objectstack/specuntouched. ⛔content/docs/releases/untouched. ⛔zod-mirror-parity.test.tsuntouched (PR #9343 holds it).Acceptance notes
Noted while working, not filed and not changed here:
packages/plugin-calendar/README.mdteachesobject-calendarwithobjectNameonly and never authorsdata, so it needed no edit — but it is the one calendar doc surface that does not carry the "refused on this block" note the.mdxcarries. An observation about coverage, not a defect: nothing it teaches is wrong.REFUSAL_MESSAGEthe record-source refinement emits is member-parameterised and still reads "declare one ofdata,staticDataorobjectName" for all three members, while whatdataadmits is now member-specific. The message is not wrong (presence is what the refinement checks) and it is pinned by two tests; the docblock now carries the distinction instead. Style, not a defect.Session, as prose so it survives a body rewrite: this PR was produced by an ObjectUI development agent running Claude Code in session
session_01L5xpA5q533BgTTNADibEFt.Generated by Claude Code