Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions .changeset/8505-grid-columns-breakpoint-narrowing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ from a computed `Record<string, number>` still type-checks, and only fresh objec
LITERALS meet excess-property checking. The narrowing is therefore a check on the
authoring spelling, which is where the defect was authored.

**The zod mirror is deliberately NOT narrowed here.** `zod/layout.zod.ts` still
validates `columns` as `z.record(z.string(), z.number())`, so the JSON authoring
face — `os-ui validate` / `check` in `@object-ui/cli`, the real consumer of these
mirrors — still admits `{ xxl: 6 }`. That is reported for its own card, on a
measurement: closing it ships runtime bytes into the console's `framework` chunk
(`packages/(core|react|types)`), which measured 70,999 gzip bytes against its
71,000 ceiling on this branch's base — one byte of headroom. The current reading is
held visible by a handoff assertion in the new test, with a lit control next to it,
so it flips to a refusal when that card lands rather than rotting into an
assumption that both faces closed together.
**The zod mirror WAS deliberately not narrowed here — corrected in #8573.** This
paragraph said `zod/layout.zod.ts` still validates `columns` as
`z.record(z.string(), z.number())`, so the JSON authoring face — `os-ui validate`
/ `check` in `@object-ui/cli`, the real consumer of these mirrors — still admits
`{ xxl: 6 }`. #8573 narrowed that mirror to a partial record over the six
breakpoints (objectui#8516), so the JSON face now refuses `{ xxl: 6 }` exactly as
the declaration above does. Its two supporting statements went with it, and #8573
quotes each in its current state rather than re-arguing it: the byte measurement
that deferred the narrowing — 70,999 gzip bytes against a 71,000 `framework`
ceiling, one byte of headroom — is retired, because PR #8550 landed the
maintainer's raise to `PER_CHUNK_GZIP_CEILINGS.framework` `100_000` against a
`PER_CHUNK_BASELINE.framework` of `72_245`; and the handoff assertion described
here as holding the current reading visible is flipped to a refusal in #8573, so
it no longer asserts acceptance.
64 changes: 64 additions & 0 deletions .changeset/8516-8556-mirror-partial-record-narrowing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
'@object-ui/types': minor
---

**Breaking for authored metadata, at validation time:** two zod mirrors in
`@object-ui/types/zod` now state their own declaration's key vocabulary instead
of `string` (objectui#8516, objectui#8556).

| key | mirror was | mirror is |
| :-- | :--------- | :-------- |
| `GridSchema.columns` (`zod/layout.zod.ts`) | `z.record(z.string(), z.number())` | a PARTIAL record over the six breakpoints |
| `ReportComponentSchema.exportConfigs` (`zod/reports.zod.ts`) | `z.record(z.string(), ReportExportConfigSchema)` | a PARTIAL record over `ReportExportFormat` |

Both declarations already stated the closed set — `columns` since objectui#8505,
`exportConfigs` since objectui#6121 — so each mirror was accepting a spelling its
own published type refuses. This is a pull-back to the declaration, not a new
constraint: `os-ui validate` / `check` in `@object-ui/cli` are the real consumers
of these mirrors, and they were passing documents `tsc` rejects.

**What now fails that used to pass.** A `grid` whose responsive map is keyed
outside `xs` `sm` `md` `lg` `xl` `2xl` — `{ xxl: 6 }` is the one to expect,
because it is the Bootstrap/Ant spelling an author reaches for first — and a
`report` whose `exportConfigs` is keyed outside `pdf` `excel` `csv` `json`
`html`. Both were ALREADY broken at runtime: the grid renderer reads exactly the
six keys and the export engine exactly the five formats, so a document the
mirror used to accept rendered at the default column count, or exported nothing,
with no error and no warning. The narrowing refuses documents that were already
being silently ignored; it refuses nothing that works today.

**Migration:** `xxl` is `2xl`; `XL` and `2XL` are `xl` and `2xl`. The refusal
names the offending key — `Path: columns → xxl`, `Code: invalid_key` — through
`@object-ui/cli`'s union-arm expansion.

**Measured accept set in the corpus, before grading this.** Counted over every
tracked file in this repository and in the `objectstack` sibling checkout, with
test fixtures and changeset prose separated out rather than folded in:

- grid nodes carrying an object-valued `columns` — **10 authored sites** (nine in
`content/docs`, `examples/schema-catalog` and `skills/objectui`, one in
`apps/site`), **all keyed inside the six breakpoints**. Every out-of-vocabulary
key in either tree sits in a test fixture written to document the defect, or in
objectui#8505's changeset quoting it.
- `exportConfigs` — **one authored site**, `content/docs/core/report-schema.mdx`,
keyed `pdf` / `excel` / `csv`, all **in vocabulary**. None in `objectstack`.

So this narrowing refuses **zero** documents that exist today, which is why it is
graded `minor` with the break spelled out rather than escalated.

**What it does NOT close.** These mirrors judge the document they are handed. A
`grid` nested under another node's `children` still reaches
`SchemaNodeSchema`, a lazy union over the passthrough base that does not re-enter
the per-type arms, so `{ type: 'container', children: [{ type: 'grid', columns:
{ xxl: 6 } }] }` still validates green. That is pre-existing and untouched here —
named so this change is not read as closing the nested case.

**The spelling is `z.partialRecord`, and that is load-bearing.** ⛔ Not
`z.record(z.enum([…]), …)`: measured on zod 4.4.3, the plain record over an enum
key REQUIRES every member, so `{ md: 2 }` stops parsing — it would trade this
divergence for its exact opposite, and on `exportConfigs` it would re-impose the
total-`Record` authoring face objectui#6121's maintainer ruling removed. That
measurement is pinned executably in
`__tests__/mirror-partial-record-narrowing-8516.test.ts`, at compile time (the
inferred map is `Partial<Record<…>>`, not `Record<…>`) and at run time (one
accepting row per member), so it cannot rot into folklore.
Original file line number Diff line number Diff line change
Expand Up @@ -252,28 +252,28 @@ describe('objectui#8505 — the narrowing broke no producer, and this is why', (
});
});

/* ── (e) the other authoring face, REPORTED and left open by this card ────── */
/* ── (e) the other authoring face — the handoff, now COLLECTED ────────────── */

describe('objectui#8505 — the zod mirror still admits an open record, deliberately', () => {
it('the JSON face still accepts `{ xxl: 6 }` — the gap this card did not close', () => {
// NOT the desired end state. `GridSchema` in `zod/layout.zod.ts` is
// `z.record(z.string(), z.number())`, so `os-ui validate` / `check`
// (`@object-ui/cli`, the real consumer of these mirrors) still passes a
// grid document whose map is keyed by a spelling no renderer reads.
describe('objectui#8505 — the zod mirror now refuses what the declaration refuses', () => {
it('the JSON face refuses `{ xxl: 6 }` — the gap objectui#8516 closed', () => {
// THE HANDOFF PIN, FLIPPED (objectui#8516, the objectui#7070 convention).
// It asserted `success: true` for the whole of objectui#8505's life, with
// the reason stated rather than assumed: that card narrowed the TypeScript
// face only, and this block held the mirror's remaining reading visible so
// it could not rot into a silent assumption that both faces closed
// together. objectui#8516 narrowed the mirror, so the block is collected
// here rather than deleted — the flip IS the record that the handoff was
// honoured, and a deletion would leave nothing that fails if the mirror is
// ever widened back.
//
// Left open on a measurement, not a preference: closing it ships runtime
// bytes into the console's `framework` chunk (`packages/(core|react|types)`),
// which measured 70,999 gzip bytes against its 71,000 ceiling on this
// branch's base — one byte of headroom. A zod narrowing here would turn
// `Bundle Analysis` red and make its own fix a byte-hunt through two other
// packages, which is a different card. The TypeScript narrowing this card
// ships costs zero runtime bytes.
//
// This assertion is the HANDOFF (the objectui#7070 convention): when that
// card lands, this block flips to a refusal rather than quietly agreeing
// with whatever the mirror ends up doing.
// ⚠️ The blocker this block used to cite is GONE, and the obsolete number
// is left named on purpose: it said a narrowing was unaffordable because
// the console's `framework` chunk measured 70,999 gzip bytes against a
// 71,000 ceiling. PR #8550 landed the maintainer's raise — that ceiling is
// 100,000 against a re-derived 72,245 baseline — so the narrowing cost no
// byte-hunt at all.
const r = GridZodMirror.safeParse({ type: 'grid', columns: { xxl: 6 } });
expect(r.success).toBe(true);
expect(r.success).toBe(false);
});

it('CONTROL — the mirror is live: it refuses a non-numeric column count', () => {
Expand All @@ -282,4 +282,15 @@ describe('objectui#8505 — the zod mirror still admits an open record, delibera
const r = GridZodMirror.safeParse({ type: 'grid', columns: { xs: 'two' } });
expect(r.success).toBe(false);
});

it('CONTROL — and it still ACCEPTS a partial breakpoint map', () => {
// ⚠️ Added with the flip, and load-bearing because of it. Both readings
// above are now `false`, so on their own they are equally green against a
// mirror that refuses EVERYTHING — the caricature the header's table warns
// about, one authoring face down. This is also the overshoot guard: the
// `z.record(z.enum([…]), …)` spelling objectui#8516 refused reddens exactly
// here, because zod 4 makes it require all six members.
const r = GridZodMirror.safeParse({ type: 'grid', columns: { md: 2 } });
expect(r.success).toBe(true);
});
});
Loading
Loading