Skip to content

Commit f3135a4

Browse files
feat(types): declare node-level responsiveStyles on flex, object-grid and object-chart, by reference to the spec (objectui#10872, batch 9) (#11273)
Part of #10872 Clause-②: yes Batch 9 of objectui#10872. It declares the node-level `responsiveStyles` that `@objectstack/spec`'s `PageComponentSchema` declares on every page component on the three arms outside the public-block set that a producer writes it on: `flex`, `object-grid` and `object-chart`. The declaration is by reference to the spec, from batch 8's fragment. Claim `5918816527`, seat `domain:ui#1`, session `https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ`. **Clause-② yes (widening).** The strict authoring face (`StrictAnyComponentSchema`) now accepts a spec-valid node-level `responsiveStyles` on `FlexSchema`, `ObjectGridSchema` and `ObjectChartSchema`. Before, it refused the key by name. Changeset: `.changeset/10872-flat-arm-responsive-styles.md`, `'@object-ui/types': minor`. Unlike batch 8's arms, these three were already published, so the changeset spells out the narrowings too (see Acceptance notes). ## What changed - **zod.** `FlexSchema` (`layout.zod.ts`), `ObjectGridSchema` and `ObjectChartSchema` (`objectql.zod.ts`) spread the same fragment the 30 public-block arms spread. The member is ONE object on all 33 arms: the spec's `ResponsiveStylesSchema` through the objectui#8317 boundary. `BaseSchema` is not touched, and neither is any other arm. - **The fragment is renamed from `PUBLIC_BLOCK_ENVELOPE` to `NODE_ENVELOPE`.** It stays in `public-blocks.zod.ts`, because that module imports only `base.zod.ts`, `imported-defaults.ts` and `tombstone.zod.ts`, so neither import makes a cycle. With `flex` spreading it, the old name would tell a reader that `flex` is a public block. The rename is mechanical: 30 occurrences in `public-blocks.zod.ts`, 5 in `objectql.zod.ts`, plus one census row key, one test comment and one README line. On-disk counts were 0 old / 38 new after the rename. - **TypeScript twins.** `FlexSchema` (`layout.ts`), `ObjectGridSchema` and `ObjectChartSchema` (`objectql.ts`) declare `responsiveStyles?: ResponsiveStyles`, the spec's type by reference (type-only import, as `dataSource?: ElementDataSource` already is). It is not on `FlexLayoutProps`, so `stack` does not gain it. - **`object-view`'s `table` slot does not gain the key.** The slot is built from `ObjectGridSchema`'s shape, so the zod change alone would have made `table.responsiveStyles` accepted and judged, where nothing compiles it: `ObjectView` draws its grid as a component. The slot now refuses it by name, as it refuses the other node-level keys (objectui#10976). The reason is its own, not the shared node-level one, because that one points the author at the `object-view` node, whose arm does not declare the key either. - **Pins.** New: `packages/types/src/__tests__/flat-arm-responsive-styles-10872.test.ts`, with 37 runtime rows and 3 type-level assertion groups. Forced edits to existing pins: - batch 8's pin: its boundary control moves from `flex` to `grid`; - `object-view-slot-key-lists.test.ts`: the grid's member count goes from 63 to 64, and `responsiveStyles` joins the `nodeLevel` withheld set; - `stack-schema-emitted-members.test.ts`: `stack` declares exactly `flex`'s members less the named `responsiveStyles`; - census text in `zod-mirror-parity.test.ts` and `imported-defaults-8317.test.ts`: the row key follows the rename, and the reason names the three arms. The census reads presence, not words. - One zod `README.md` line plus the rename in the existing line, and one changeset. ## The order's mechanism assumptions, measured 1. **The pull, re-driven.** objectui `54a78308a3` was read from its tracked files. objectstack `aaad682dbc` (main at 2026-09-30T19:33Z) was read from a depth-1 scratch clone; the shared checkout was not fetched or edited. The instrument is `scripts/body-dialect-census.mjs` (`scanNodes`, `keepFencedCodeOnly`, `bucketOf`) under an uncommitted scratch driver. It counts NODES carrying a node-level `responsiveStyles`, and it reads the arm population at run time from `AnyComponentSchema` (189 literals, 30 of them public-block literals). - Non-test producers on arms outside the public-block set: `flex` 16 (showcase `command-center.page.ts` 8 and `styling-gallery.page.ts` 7, plus skill `rules/pages.md` 1), `object-chart` 1 and `object-grid` 1 (both in `command-center.page.ts`). Every other non-public arm: 0. objectui non-test: 0. - Lit control: the detector fires on the test buckets, on the same types (objectstack `flex` 11; objectui `flex` 1, `object-chart` 1). - **Confirmed**, and the population is exactly these three arms. 2. **The faces at base.** For all three arms, the strict face refused a valid map as `unrecognized_keys` naming `responsiveStyles`. The tolerant face ACCEPTED and kept `7` and `{ md: … }` unjudged. `grid`, `stack` and `container` read the same. **Confirmed.** 3. **Reuse the fragment.** There is no cycle, so the fragment was not moved. It was renamed because the name would lie (see What changed). There is no second copy of the member: the pin asserts identity with `PageSectionBlockSchema.shape.responsiveStyles`. 4. **Flat arms and the TypeScript twins.** Measured with the zod member alone, before any TypeScript edit, `tsc -p tsconfig.test.json` failed on two census assertions: - `assertionMirroredUndeclaredMatchesLedger` named exactly the three pairs (`layout.zod.ts#FlexSchema`, `objectql.zod.ts#ObjectChartSchema`, `objectql.zod.ts#ObjectGridSchema`); - `assertionMirroredUndeclaredRecordsEveryKey` named `responsiveStyles`. Two edits turn it green: three new `MirroredUndeclared` rows, or the declaration on the twins. That ledger is a floor that "can only shrink", so the declaration was taken, by reference to the spec's type, as the order's item 4 says. It is part of the same widening. Two more consumers read the twins through the compiler and were forced as listed above. 5. **`BaseSchema` stays untouched.** It is not in the diff; the pin asserts that `BaseSchema.shape` lacks the key and that every other arm refuses it. There is no `needs_decision`. ## Evidence - **Red first**, at base `54a78308a3` with the pin added and the source unmodified. `pnpm exec vitest run --maxWorkers=2 …/flat-arm-responsive-styles-10872.test.ts` gave `Tests 17 failed | 20 passed (37)`. The 17 reds: - 3 member-identity rows, the spec-map row and the arm-population row; - 3 valid-node rows (strict face `unrecognized_keys`); - 9 invalid-value rows (tolerant face accepted, expected the spec's issue). The 20 controls (population, lit control, `grid` / `stack` / `container`, and the other five envelope keys) were green. `pnpm --filter @object-ui/types type-check` at base gave 8 errors: TS2339 on the three zod shapes, and TS2344 `Type 'false' does not satisfy the constraint 'true'` on the three TypeScript-twin assertions. - **Ablation**, at `c4edd1d106` with the implementation committed first. It ran three single-arm legs through objectstack's `scripts/ablation-replace.mjs` in WRAP mode, plus a shell trap restoring both zod files by absolute path. Each leg deleted one arm's spread, and each anchor hit exactly once: - `flex` leg: blob `797a9c0b4e63` to `6011a4533631`, `Tests 7 failed | 30 passed (37)`; - `object-grid` leg: `2aa44c282213` to `1e00d88afe2c`, `6 failed | 31 passed`; - `object-chart` leg: `2aa44c282213` to `673ec48085df`, `6 failed | 31 passed`. The union of the three is exactly the 17 red-first rows. Every restore read `ok restored: blob == HEAD … git diff HEAD is empty`, and the final check read both blobs equal to HEAD with `git diff HEAD` at 0 bytes. The pin imports `../zod/index.zod.js` from source, so there is no dist leg. - **Reverse type check**, through the rebuilt `dist`. A scratch consumer in `packages/components` resolved `@object-ui/types` to `packages/types/dist/index.d.ts` (`--traceResolution`). It got exactly 3 errors, one per bad line: TS2353 `'md' does not exist`, TS2559 for a number, and TS2322 for a boolean style value. The three valid lines got none. The scratch files were deleted. - **Runtime probe** (scratch, deleted). Each type was mounted through the real `SchemaRenderer` and registry, and each emitted a `style[data-os-scope]` with the base block and the `@media` override: - `flex`: the scope class is on the flex root `div`; - `object-grid`: on the grid's container `div`, which holds the rows; - `object-chart`: on the chart container (`[data-slot="chart"]`), which holds the SVG. In each control without the key, 0 style tags were emitted. Declared is enforced on these arms. - **Final head `c31dff6c1f`**, clean tree, one os-verify-lock hold per step, every VERDICT command-exit 0: - `pnpm --filter @object-ui/types build` gave `dist completeness: 1 package(s) complete`; - `pnpm --filter @object-ui/types type-check` passed, and `tsc -p tsconfig.test.json --listFilesOnly` lists both 10872 pins; - `pnpm exec vitest run packages/types/` gave `Test Files 296 passed (296)` and `Tests 7412 passed (7412)`; - `pnpm exec vitest run packages/cli/` gave `23 passed (23)` and `335 passed (335)`; the registered-types ratchet is unchanged; - **the arm-list consumer sweep** ran 162 test files outside `packages/types` and `packages/cli`: every file naming the zod node union, the strict face, `safeValidateSchema`, the `@object-ui/types/zod` entry, an arm `.shape`, or `FlexSchema` / `StackSchema` / `ObjectGridSchema` / `ObjectChartSchema` / `ObjectViewSchema` / `responsiveStyles` / the fragment. It gave `79 passed` with 1537 tests and `83 passed` with 1196 tests; - the 24 `scripts/__tests__` files that read `.changeset/` gave `24 passed (24)` and `914 passed (914)`. ## Gates at `c31dff6c1f` objectui has no `scripts/pm/dispatch-gates.mjs`. The families were derived by hand from the root `package.json` for this diff's paths (`packages/types/src/**`, one README, one changeset). - `pnpm check:handler-key-reads`: exit 0, `OK 140 arm(s), 212 registration(s)`. - `pnpm check:spec-symbols`: exit 0. - `pnpm check:new-line-citations`: exit 0, `VERDICT new-cross-file-line-citations: 0 new citation(s)`. - `node scripts/check-changeset-presence.mjs`: exit 0, `12 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`. - `pnpm changeset:check`: exit 0, `No changeset declares a major bump`. - `pnpm check:control-bytes`: exit 0. A `grep -P` control-byte scan of the 13 changed files found 0. - Exit 0 each: `check:changeset-claims` (report-only), `check:pending-changeset-literals`, `check:component-surface-parity` (report-only; no row names these keys), `check:test-path-roots`, `check:element-data-source-declaration`, `check:esm-specifiers`, and `markdown-test-inputs --audit`. - `check-governed-queue-guard.mjs --test` on the 13 paths: NOT GOVERNED. - ESLint on the 11 changed TS files, `--no-inline-config --format json`: 11 files, 0 errors. All 37 warnings are `no-explicit-any` on lines this diff does not add (checked line by line against the diff). `--print-config` shows no `project` / `projectService`, so linting is not type-aware and an untouched file's verdict cannot move. Repo-wide `pnpm lint` is CI's. - NOT MEASURED: `check:readme-exports`. Its population collapsed (`packagesRead: found 3, floor is 25`) because the other packages' `dist` is not built in this worktree. That is a prerequisite, not a verdict. The README lines edited here carry no fenced block. ## Acceptance notes - **The narrowings on already-published arms**, disclosed in the changeset. The tolerant face now refuses a spec-refused `responsiveStyles` value on the three arms, and `table.responsiveStyles` by name. The three TypeScript interfaces type the key as the spec's type, where it was `any` through the index signature. Level: `minor`, as the ruling set it; `check-changeset-no-major` forbids `major`. - **The 18 producer nodes still do not validate on objectui's faces after this batch, for a reason outside this card's key.** The showcase writes them in the spec's page-component form, with props under `properties`. The three flat arms do not declare `properties`, so the strict face also refuses them by name. On the tolerant face the `object-grid` and `object-chart` nodes are refused too (`objectName` required; no chart family), because the arms read those keys at node level. `SchemaRenderer` hoists `properties` and renders the nodes, and the spec accepts them. This is reported to the seat as a finding, and is relevant to PR objectui#11069's un-park census. - `object-chart`'s scope class lands on the chart container, not the `ObjectChart` root. A first probe on a node with no series binding drew the explanatory placeholder, and that placeholder did not carry the class. - Downstream consumers' type-check is declared to CI's `Type Check`. Outside `packages/types`, no TypeScript source writes `responsiveStyles` on a node typed as one of these interfaces: the only hits are comments, `identity-probe` fixtures and `SchemaRenderer`'s own read. The one derived type, the `table` slot's `Pick`, is pinned by `object-view-slot-key-lists`. - `origin/main` moved to `f4ed2387e9` (objectui#10188, objectui#11163) with zero file overlap with this branch, so it was not merged; the queue rebuilds on `main`. ## Deviations - The fragment was renamed without being moved. The order tied the rename to a move, but the name would lie either way. - The PR's reach is wider than the claim's file surface: `public-blocks.zod.ts` (rename only), `layout.ts` / `objectql.ts` (the twins, forced by the census), and two existing type-reading pins (forced). Each is listed above with its reason. --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1cb3732 commit f3135a4

13 files changed

Lines changed: 498 additions & 55 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
`flex`, `object-grid` and `object-chart` accept the node-level `responsiveStyles` that `@objectstack/spec`'s `PageComponentSchema` declares on every page component, and judge it as the spec does (objectui#10872, batch 9).
6+
7+
**Clause-②: yes (widening)**: the strict authoring face (`StrictAnyComponentSchema`) now accepts a node-level `responsiveStyles` on `FlexSchema`, `ObjectGridSchema` and `ObjectChartSchema`, where it refused the key by name. Batch 8 declared the same key on the 30 public-block arms; these are the three arms outside that set whose nodes the objectstack showcase and UI skill write it on.
8+
9+
**What changed, in observable terms.**
10+
11+
- The three arms declare `responsiveStyles` from the same shared declaration as the public-block arms: the spec's `ResponsiveStylesSchema`, by reference. Its breakpoints (`large`, `medium`, `small`, `xsmall`), its strictness and its unknown-breakpoint message come from the spec.
12+
- A node such as `{ "type": "flex", "direction": "col", "responsiveStyles": { "large": { "display": "block", "minWidth": "0" }, "small": { "padding": "12px" } } }` parses on both faces, and the map comes back unchanged. The same holds for `object-grid` and `object-chart` nodes.
13+
- The published TypeScript interfaces `FlexSchema`, `ObjectGridSchema` and `ObjectChartSchema` declare `responsiveStyles?: ResponsiveStyles`, the spec's type. Before, the key was typed `any` through `BaseSchema`'s index signature.
14+
- Nothing else widens. `BaseSchema` still does not declare `responsiveStyles`, and neither does any other arm (`stack`, `grid` and `container` included), so the strict face still refuses it there by name. The other node-level keys the spec declares (`events`, `aria`, `visibility`, `responsive`, and `dataSource` on `flex` and `object-chart`) stay undeclared.
15+
16+
**Narrowing, on already-published arms.** Unlike the public-block arms, these three were published before this change, and two things now refuse what used to pass:
17+
18+
- The tolerant face (`safeValidateSchema`, the face `objectui validate` runs) used to keep any node-level `responsiveStyles` value on these arms unjudged. It now refuses a value the spec refuses, at `responsiveStyles`, with the spec's own issue: a breakpoint the spec does not have (`md`), a number instead of a map, or a style value that is neither a string nor a number. `SchemaRenderer` applied nothing for a number or for a map with no spec breakpoint. It emitted a non-CSS value inside a valid breakpoint verbatim, where the browser dropped it, and it applied the valid breakpoints of a mixed map.
19+
- In TypeScript, a `responsiveStyles` value that is not the spec's `ResponsiveStyles` no longer type-checks on these three interfaces.
20+
- An `object-view`'s `table` slot, which takes `ObjectGridSchema`'s members, does not gain the key. `ObjectView` draws its grid as a component, not as a schema node, so nothing compiles a `responsiveStyles` map written in `table`. The slot therefore refuses `table.responsiveStyles` by name, with that reason, on both faces, as it refuses the other node-level keys (objectui#10976). The tolerant face used to keep it unjudged, and the TypeScript slot never declared it.
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* `flex`, `object-grid` and `object-chart` declare the node-level
11+
* `responsiveStyles` that `@objectstack/spec`'s `PageComponentSchema` declares
12+
* on every page component, by reference, from the same fragment as the
13+
* public-block arms (objectui#10872, batch 9).
14+
*
15+
* ## The defect these pin
16+
*
17+
* Batch 8 declared the key on the 30 public-block arms. Objectstack's showcase
18+
* and its UI skill also write it on nodes of three arms outside that set:
19+
* `flex`, `object-grid` and `object-chart`. That reading is recorded on
20+
* objectui#10872, and nothing here re-derives it. `SchemaRenderer` compiles
21+
* the key to id-scoped CSS on every node (ADR-0065), but these three arms did
22+
* not declare it, so:
23+
*
24+
* - the STRICT authoring face refused a spec-valid node by name
25+
* (`unrecognized_keys`), and
26+
* - the TOLERANT face passed it unjudged: `responsiveStyles: 7`, or a
27+
* breakpoint the spec does not have (`md`), parsed clean and styled
28+
* nothing.
29+
*
30+
* ## What is and is not widened
31+
*
32+
* The three arms spread the ONE shared fragment the public-block arms spread,
33+
* `PAGE_COMPONENT_ENVELOPE`, so the member is the same object on all 33 arms.
34+
* The key is NOT declared on `BaseSchema` and NOT on any arm without a measured
35+
* producer: `grid`, `stack` and `container` below are the control, and the
36+
* whole arm list is read at run time so an arm that gains the member silently
37+
* turns this file red. The published TypeScript twins declare it too, as the
38+
* spec's `ResponsiveStyles` type by reference, so the mirror-parity census
39+
* measures no mirrored-but-undeclared key on these pairs.
40+
*
41+
* Every verdict is read against the INSTALLED spec at run time: the member is
42+
* compared with the spec's own `ResponsiveStylesSchema`, and a refusal is
43+
* compared with the issue the spec itself emits for the same value.
44+
*/
45+
46+
import { describe, expect, it } from 'vitest';
47+
import type { z } from 'zod';
48+
import {
49+
PageComponentSchema,
50+
ResponsiveStylesSchema as SpecResponsiveStylesSchema,
51+
type ResponsiveStyles as SpecResponsiveStyles,
52+
} from '@objectstack/spec/ui';
53+
54+
import {
55+
AnyComponentSchema,
56+
BaseSchema,
57+
FlexSchema,
58+
ObjectChartSchema,
59+
ObjectGridSchema,
60+
ObjectQLPublicBlockComponentSchema,
61+
PageSectionBlockSchema,
62+
PublicBlockComponentSchema,
63+
StrictAnyComponentSchema,
64+
safeValidateSchema,
65+
} from '../zod/index.zod.js';
66+
import type { FlexSchema as TsFlexSchema, StackSchema as TsStackSchema } from '../layout';
67+
import type { ObjectChartSchema as TsObjectChartSchema, ObjectGridSchema as TsObjectGridSchema } from '../objectql';
68+
69+
/* ── Type-level pins (compiled by `tsc -p tsconfig.test.json`) ─────────────── */
70+
71+
type Equal<A, B> =
72+
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
73+
type Expect<T extends true> = T;
74+
type ShapeOf<M> = M extends { shape: infer S } ? S : never;
75+
type InputOf<T> = T extends z.ZodType ? z.input<T> : never;
76+
77+
/** Each zod arm's member accepts exactly the spec's published type, absent allowed. */
78+
export type assertionZodMemberIsTheSpecType = [
79+
Expect<Equal<InputOf<ShapeOf<typeof FlexSchema>['responsiveStyles']>, SpecResponsiveStyles | undefined>>,
80+
Expect<Equal<InputOf<ShapeOf<typeof ObjectGridSchema>['responsiveStyles']>, SpecResponsiveStyles | undefined>>,
81+
Expect<Equal<InputOf<ShapeOf<typeof ObjectChartSchema>['responsiveStyles']>, SpecResponsiveStyles | undefined>>,
82+
];
83+
/**
84+
* Each published TypeScript twin declares the member as the spec's type. Without
85+
* the declaration the member resolves through `BaseSchema`'s index signature to
86+
* `any`, which `Equal` refuses.
87+
*/
88+
export type assertionTsTwinIsTheSpecType = [
89+
Expect<Equal<TsFlexSchema['responsiveStyles'], SpecResponsiveStyles | undefined>>,
90+
Expect<Equal<TsObjectGridSchema['responsiveStyles'], SpecResponsiveStyles | undefined>>,
91+
Expect<Equal<TsObjectChartSchema['responsiveStyles'], SpecResponsiveStyles | undefined>>,
92+
];
93+
/**
94+
* The helper can FAIL, on the exact shape a missing declaration takes: `stack`
95+
* declares no `responsiveStyles`, so the member resolves through the index
96+
* signature, and `Equal` refuses it.
97+
*/
98+
export type assertionUndeclaredTwinIsRefused = Expect<
99+
Equal<Equal<TsStackSchema['responsiveStyles'], SpecResponsiveStyles | undefined>, false>
100+
>;
101+
102+
/* ── The population ───────────────────────────────────────────────────────── */
103+
104+
type Arm = z.ZodObject;
105+
const optionsOf = (union: unknown): Arm[] => (union as { options: Arm[] }).options;
106+
const literalOf = (arm: Arm): string => (arm.shape.type as z.ZodLiteral).value as string;
107+
108+
/** Every arm of the node union with its `type` literals, read off the live union. */
109+
function armsByLiteral(): Map<string, Arm> {
110+
const out = new Map<string, Arm>();
111+
const walk = (schema: unknown): void => {
112+
const options = (schema as { options?: unknown[] }).options;
113+
if (options) {
114+
for (const option of options) walk(option);
115+
return;
116+
}
117+
const type = (schema as Arm).shape?.type as { _zod?: { values?: Set<unknown> } } | undefined;
118+
for (const value of type?._zod?.values ?? []) out.set(String(value), schema as Arm);
119+
};
120+
walk(AnyComponentSchema);
121+
return out;
122+
}
123+
124+
/**
125+
* The three arms this batch widens: the arms outside the public-block set that
126+
* a producer writes `responsiveStyles` on (objectui#10872's batch-9 pull). NAMED,
127+
* not derived, because the population is a measurement of producers.
128+
*/
129+
const FLAT_ARMS = [
130+
['flex', FlexSchema],
131+
['object-grid', ObjectGridSchema],
132+
['object-chart', ObjectChartSchema],
133+
] as const;
134+
135+
/** Arms with no producer: the strict face still refuses the key on them. */
136+
const CONTROL_TYPES = ['grid', 'stack', 'container'] as const;
137+
138+
/** The arm that already declared `dataSource` before this batch (objectui#11070). */
139+
const DECLARES_DATA_SOURCE: ReadonlySet<string> = new Set(['object-grid']);
140+
141+
/** The five envelope keys the spec declares and this batch leaves undeclared. */
142+
const UNDECLARED_ENVELOPE = ['events', 'dataSource', 'aria', 'visibility', 'responsive'] as const;
143+
144+
/** A valid flat node per arm, shaped like the producer's nodes, `responsiveStyles` included. */
145+
const VALID_NODES = {
146+
flex: {
147+
type: 'flex',
148+
id: 'cc_panel',
149+
direction: 'col',
150+
responsiveStyles: {
151+
large: { display: 'block', minWidth: '0', padding: '15px 17px 17px', borderRadius: '16px' },
152+
small: { padding: '12px', minHeight: '200px' },
153+
},
154+
// A nested node carrying the key: judged at depth, through the node recursion.
155+
children: [{ type: 'flex', id: 'cc_spacer', responsiveStyles: { large: { flex: '1 1 auto' } } }],
156+
},
157+
'object-grid': {
158+
type: 'object-grid',
159+
id: 'cc_queue_g',
160+
objectName: 'showcase_task',
161+
columns: ['title', 'status'],
162+
responsiveStyles: { large: { minWidth: '0', display: 'block' } },
163+
},
164+
'object-chart': {
165+
type: 'object-chart',
166+
id: 'cc_status_c',
167+
dataset: 'showcase_task_metrics',
168+
dimensions: ['status'],
169+
values: ['task_count'],
170+
chartType: 'bar',
171+
responsiveStyles: { large: { width: '100%', minWidth: '0' }, xsmall: { display: 'none' } },
172+
},
173+
} as const;
174+
175+
/** Values the spec refuses: a breakpoint it does not have, a non-object, a non-CSS value. */
176+
const INVALID_STYLES: ReadonlyArray<readonly [string, unknown]> = [
177+
['a breakpoint the spec does not declare (`md`)', { md: { padding: '8px' } }],
178+
['a number instead of a map', 7],
179+
['a style value that is neither string nor number', { large: { padding: true } }],
180+
];
181+
182+
type Result = { success: boolean; error?: { issues: z.core.$ZodIssue[] } };
183+
184+
/** Every issue as `{ code, path, message }`. */
185+
const issuesOf = (result: Result) =>
186+
result.success
187+
? []
188+
: result.error!.issues.map((issue) => ({ code: issue.code, path: issue.path.join('.'), message: issue.message }));
189+
190+
/** The keys an `unrecognized_keys` issue names, at any depth of a union. */
191+
function refusedKeys(result: Result): string[] {
192+
if (result.success) return [];
193+
const collect = (issues: readonly z.core.$ZodIssue[]): string[] =>
194+
issues.flatMap((issue) => {
195+
if (issue.code === 'unrecognized_keys') return issue.keys;
196+
if (issue.code === 'invalid_union') return issue.errors.flatMap((arm) => collect(arm));
197+
return [];
198+
});
199+
return collect(result.error!.issues);
200+
}
201+
202+
describe('the population (objectui#10872 batch 9)', () => {
203+
it('each named arm is the arm the node union selects for its literal', () => {
204+
const arms = armsByLiteral();
205+
// Non-vacuity: the union is read, and it is the whole node vocabulary.
206+
expect(arms.size).toBeGreaterThan(100);
207+
for (const [type, arm] of FLAT_ARMS) {
208+
expect(literalOf(arm)).toBe(type);
209+
expect(arms.get(type)).toBe(arm);
210+
}
211+
for (const type of CONTROL_TYPES) expect(arms.has(type)).toBe(true);
212+
});
213+
214+
it('the spec declares `responsiveStyles` on a page component of each type (lit control)', () => {
215+
for (const [type] of FLAT_ARMS) {
216+
const node = { type, responsiveStyles: VALID_NODES[type].responsiveStyles };
217+
expect(PageComponentSchema.safeParse(node).success).toBe(true);
218+
expect(SpecResponsiveStylesSchema.safeParse(node.responsiveStyles).success).toBe(true);
219+
}
220+
});
221+
});
222+
223+
describe('`responsiveStyles` is declared on the three arms, from the public blocks\' fragment (objectui#10872 batch 9)', () => {
224+
it.each(FLAT_ARMS)('%s carries the member, and it is the SAME object the public-block arms carry', (_type, arm) => {
225+
const shared = PageSectionBlockSchema.shape.responsiveStyles;
226+
expect(shared).toBeDefined();
227+
expect(arm.shape.responsiveStyles).toBe(shared);
228+
});
229+
230+
it('the member is the spec\'s own map: the same breakpoints, each the spec\'s own value schema', () => {
231+
const member = (FlexSchema.shape.responsiveStyles as z.ZodOptional).unwrap() as z.ZodObject;
232+
const spec = SpecResponsiveStylesSchema as unknown as z.ZodObject;
233+
expect(Object.keys(member.shape).sort()).toEqual(Object.keys(spec.shape).sort());
234+
for (const key of Object.keys(spec.shape)) expect(member.shape[key]).toBe(spec.shape[key]);
235+
});
236+
237+
it('exactly the public-block arms and these three declare it: no other arm, and not `BaseSchema`', () => {
238+
expect(Object.keys(BaseSchema.shape)).not.toContain('responsiveStyles');
239+
const publicBlocks = [
240+
...optionsOf(PublicBlockComponentSchema),
241+
...optionsOf(ObjectQLPublicBlockComponentSchema),
242+
].map(literalOf);
243+
const expected = [...publicBlocks, ...FLAT_ARMS.map(([type]) => type)].sort();
244+
const declaring = [...armsByLiteral()]
245+
.filter(([, arm]) => 'responsiveStyles' in arm.shape)
246+
.map(([type]) => type)
247+
.sort();
248+
expect(declaring).toEqual(expected);
249+
});
250+
251+
it.each(CONTROL_TYPES)('%s (no producer) still refuses it on the strict face, by name', (type) => {
252+
const node = { type, responsiveStyles: VALID_NODES.flex.responsiveStyles };
253+
expect(refusedKeys(StrictAnyComponentSchema.safeParse(node))).toEqual(['responsiveStyles']);
254+
});
255+
});
256+
257+
describe('a valid `responsiveStyles` is accepted on BOTH faces (objectui#10872 batch 9)', () => {
258+
it.each(FLAT_ARMS.map(([type]) => type))('%s: the node parses on both faces, and the map survives unchanged', (type) => {
259+
const node = VALID_NODES[type];
260+
const tolerant = safeValidateSchema(node);
261+
expect(issuesOf(tolerant)).toEqual([]);
262+
expect((tolerant.data as { responsiveStyles?: unknown }).responsiveStyles).toEqual(node.responsiveStyles);
263+
const strict = StrictAnyComponentSchema.safeParse(node);
264+
expect(issuesOf(strict)).toEqual([]);
265+
expect((strict.data as { responsiveStyles?: unknown }).responsiveStyles).toEqual(node.responsiveStyles);
266+
});
267+
});
268+
269+
describe('an invalid `responsiveStyles` is refused on BOTH faces, with the spec\'s own issue (objectui#10872 batch 9)', () => {
270+
const cases = FLAT_ARMS.flatMap(([type]) => INVALID_STYLES.map(([why, value]) => [type, why, value] as const));
271+
272+
it.each(cases)('%s — %s', (type, _why, value) => {
273+
// The spec's verdict on the same value at a page component of this type,
274+
// read at run time. The spec node carries only `type` and the key, so every
275+
// issue it reports is about the key.
276+
const spec = issuesOf(PageComponentSchema.safeParse({ type, responsiveStyles: value }));
277+
expect(spec.length, 'lit control: the spec refuses this value').toBeGreaterThan(0);
278+
expect(spec.every((issue) => issue.path.startsWith('responsiveStyles'))).toBe(true);
279+
const node = { ...VALID_NODES[type], responsiveStyles: value };
280+
expect(issuesOf(safeValidateSchema(node))).toEqual(spec);
281+
expect(issuesOf(StrictAnyComponentSchema.safeParse(node))).toEqual(spec);
282+
});
283+
});
284+
285+
describe('the other five envelope keys stay undeclared on the three arms (objectui#10872 batch 9)', () => {
286+
it('no arm gained any of them: `dataSource` only where objectui#11070 declared it', () => {
287+
const declared = FLAT_ARMS.flatMap(([type, arm]) =>
288+
UNDECLARED_ENVELOPE.filter((key) => key in arm.shape).map((key) => `${type}#${key}`),
289+
).sort();
290+
expect(declared).toEqual([...DECLARES_DATA_SOURCE].map((type) => `${type}#dataSource`));
291+
});
292+
293+
const cases = FLAT_ARMS.flatMap(([type]) =>
294+
UNDECLARED_ENVELOPE.filter((key) => !(key === 'dataSource' && DECLARES_DATA_SOURCE.has(type))).map(
295+
(key) => [type, key] as const,
296+
),
297+
);
298+
299+
it.each(cases)('%s: a node-level `%s` is still refused on the strict face, by name', (type, key) => {
300+
// The valid node without the key under test and without a nested child, so
301+
// the only refusal left is the one this row asks about.
302+
const { responsiveStyles: _styles, ...rest } = VALID_NODES[type] as Record<string, unknown>;
303+
const { children: _children, ...bare } = rest;
304+
const node = { ...bare, [key]: { x: 1 } };
305+
expect(refusedKeys(StrictAnyComponentSchema.safeParse(node))).toEqual([key]);
306+
});
307+
});

‎packages/types/src/__tests__/imported-defaults-8317.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ const IMPORTED: Array<readonly [string, z.ZodType]> = [
301301
['ElementDataSourceSchema', SpecElementDataSourceSchema],
302302
// objectui#10872 batch 8: every public-block arm declares the node-level
303303
// `responsiveStyles` as the spec's `ResponsiveStylesSchema` (one shared
304-
// `PUBLIC_BLOCK_ENVELOPE`), crossed through this boundary like every other
304+
// `NODE_ENVELOPE`, which batch 9 also spread into `flex`, `object-grid` and
305+
// `object-chart`), crossed through this boundary like every other
305306
// read. It carries no default and reaches no `z.lazy`, so the strip is the
306307
// identity function — the row is here because the census below requires
307308
// every imported symbol to be measured, not because the strip does work.

0 commit comments

Comments
 (0)