You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 1bbb7bf
Browse filesBrowse the repository at this point in the historyBrowse files
fix(types): the @object-ui/types/zod mirrors keep an unknown key instead of silently dropping it (#26019)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbNzn2LZs8bk8QMmkCUnkJdy)_
Part of #25923 — work item 2 (strip-mode mirrors) only. Items 1 (the
flat calendar / gantt keys) and 3 (the `options` bag, which needs the
stored-row census first) stay on the card for later dispatches, and
#25923 remains open after this merges.
Clause-②: yes
Metadata-protocol: none
## What changed
The `@object-ui/types/zod` mirrors held strip-mode objects (plain
`z.object()`): a misspelled key parsed green and was silently dropped,
so `FormFieldSchema.safeParse({ name, type, labell: 'oops' })` succeeded
with `labell` gone, while `@objectstack/spec` refuses it by name. Every
one of them is now `.passthrough()` (`z.looseObject`), like `BaseSchema`
already was. The strict authoring face (`StrictAnyComponentSchema` /
`deriveStrictAuthoringSchema`, what `objectui validate` uses) is
unchanged and still refuses an unknown key by name, so the two faces now
disagree only on keep-or-refuse, never on silently dropping.
### Re-measured with the card's probe
| reading | before (base `38050ced`) | after |
|---|---|---|
| `*Schema` exports of `index.zod.ts` in strip mode (the card's probe) |
**67** (the card said 74, the static scan 63) | **1** |
| strip-mode object nodes reachable from those exports, local to
`ui/packages/types` | 157 | 3 |
| `z.object(` call sites in `ui/packages/types/src/zod/*.zod.ts`
converted | — | 145 |
The one remaining strip export is `HttpRequestSchema`, and the three
remaining reachable nodes (`HttpRequestSchema`, the `$and`/`$or`/`$not`
filter group, a filter-field object) are all spec-derived clones made by
`stripImportedDefaults` from `@objectstack/spec` subschemas: they are
not authored in this package, `HttpRequestSchema` is already in the
parity file's `EXCLUSIONS`, and the new census reads source sites, which
they are not.
### Landing point
As expected by the card: `ui/packages/types/src/zod/*.zod.ts` (16 files)
and `ui/packages/types/src/__tests__/zod-mirror-parity.test.ts`. No
producer lives outside the package.
### The guard (beside `assertionNoVacuousEntry`)
A new `describe` in `zod-mirror-parity.test.ts`, "no object in ../zod/
strips an unknown key (objectstack#25923)":
- an AST census of every `z.object` / `z.looseObject` / `z.strictObject`
call in `../zod/`, classifying each by its method chain
(`.passthrough()`, `.strict()`, `.strip()`, `.catchall()`), fails on any
site whose mode is strip;
- a non-vacuity pin (more than 100 loose sites, one in `form.zod.ts`, at
least one strict site);
- a runtime pin that no registered `MIRRORS` value is an object with an
undefined catchall.
It is runtime, not a type-level `assertion…` like its neighbour, and the
docblock says why: zod 4 types `$strip` and `$strict` identically, so a
type-level "no strip" pin cannot tell them apart (measured: it flagged
exactly the five strict entries). This is the existing instrument the
card named, not a new gate.
### Type-surface consequence, and how the pins absorb it
zod 4 gives a passthrough object's `z.input` / `z.infer` a `[k: string]:
unknown` index signature. A declared `interface` is not assignable to a
type carrying one, so the parity file's narrower-direction pin read
every nested object member as narrower than its declaration (22 pins
went red). The fix is one test-only operator, `WithoutCatchall`
(`ui/packages/types/src/__tests__/zod-catchall-type.ts`), which removes
exactly that signature (an index signature of `unknown` on an object
that also has literal keys) and is the identity everywhere else. It is
applied to the narrower direction only (`AcceptedOf`), so the wider
direction still compares the raw input. `KnownDrift` moves from 55
entries / 93 keys to 53 / 90: `AppSchemaRendererNodeSchema` and
`ReportSectionSchema` leave the ledger, `DashboardWidgetSchema` and
`ReportComponentSchema` change their keys, each with a "LEFT / JOINED
under objectstack#25923" note.
Downstream: `pnpm exec turbo run type-check
--filter='...@object-ui/types'` was 112/112 tasks green with the
passthrough mirrors.
## Held back: the `FormField` index signature in `form.ts`
The card also recommends dropping `FormField`'s `[key: string]: any`. It
was measured on this branch and reverted, because it breaks consumers
outside this dispatch's file surface:
- `@object-ui/components` `src/renderers/form/form.tsx` lines 1884,
1959, 2055, 2195, 3228: `FormField` is not assignable to
`FieldWithDeclaredDefault` (index signature missing);
- `@object-ui/plugin-form` `src/ObjectForm.tsx:1068` writes `group`,
which `FormField` does not declare; `src/WizardForm.tsx:845`, the same
`FieldWithDeclaredDefault` error;
- `@object-ui/plugin-grid` `src/ObjectGrid.tsx` lines 5963 and 6016,
plus the type-check cascade of every package depending on those.
Removing a published index signature also narrows what a TypeScript
caller may write on `FormField`, so it is a `(narrowing)` Clause-②
change on its own. It is reported to the seat as an open question rather
than folded in here; `form.ts` is unchanged in this PR.
## File surface beyond the claim
The claim named the strip-mode zod files, `form.ts`, the parity test and
one changeset. The diff also touches:
- `ui/packages/types/src/__tests__/zod-catchall-type.ts` (new,
test-only; excluded from the build) and five sibling type tests that
compare `z.input` with a declaration and now read it through the same
operator (`ai-zod-arms-10859`, `designer-zod-arms-10859`,
`registered-type-arms-10859-b2`, `sort-string-arm-retired-8221`,
`base-schema-predicate-envelope-7530`);
- 18 `ui/packages/types` tests that pinned the old strip behaviour ("the
unknown key is dropped") and now pin passthrough ("the key is kept and
still not refused"), each with a comment citing objectstack#25923;
- two consumer tests that used the strip as their measuring instrument
("parse and see which keys survive"), now measured with the strict twin
(`deriveStrictAuthoringSchema`), which refuses an undeclared key by
name:
`ui/examples/schema-catalog/test/component-fixture-declared-keys.test.ts`
and
`ui/packages/plugin-kanban/src/__tests__/columnCollapsedHonoured-9628.test.tsx`;
- `scripts/sdui-manifest.record.json`: the registry-source digest moves
because the registry sources changed; `sdui.manifest.json` itself is
byte-identical.
Each of these is a consequence of the mirrors no longer stripping; none
changes runtime code outside `ui/packages/types/src/zod/`.
## Verification
All on the final head `0d70f7e3` (branch merged with `main` at
`6a6ebff9`); tests and type-checks ran under
`scripts/pm/os-verify-lock.sh`:
- `pnpm --dir ui exec vitest run --maxWorkers=2 packages/types/` plus
the 120 `ui/` test files that import `@object-ui/types/zod` or a mirror
schema: **510 files, 12867 tests, all passed**. Before the two
instrument tests moved to the strict twin, that consumer set read 118
passed, 2 failed (5 tests). The full consumer battery is CI's.
- `pnpm --filter @object-ui/types type-check`: exit 0. Downstream `turbo
run type-check --filter='...@object-ui/types'`: 112/112 tasks successful
(pre-merge head); `@object-ui/plugin-kanban` and
`@object-ui/example-schema-catalog` type-check clean after the test
edits.
- The derived gate union (`node scripts/pm/dispatch-gates.mjs
--commands`, 46 commands): all exit 0, reconciled with `--ran`: 46
derived, 46 run, 0 NOT-MEASURED, 0 UNRUN.
## Acceptance notes
- Spec-owned strip nodes: the reachability walk also counts 18
strip-mode object nodes inside `@objectstack/spec` subschemas that the
mirrors import by reference. They are the spec's, not this package's,
and are out of scope here.
- `exportConfigs` residue: `WithoutCatchall` does not walk into an
object that has no catchall of its own (a partial record keyed by export
format), because walking those was measured to hit TS2589 on the node
tree. The residue is ledgered in `KnownDrift` on
`ReportComponentSchema`, `ReportBuilderSchema` and `ReportViewerSchema`.
- `ObjectForm.tsx:1068` writing an undeclared `group` key on a
`FormField` is only visible once the index signature goes; it belongs to
the `FormField` follow-up above.
Serial note: #25926 waits for this PR, because both edit
`ui/packages/types/src/zod/base.zod.ts`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UDxCk99USU7v77odNZ66qE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(types): the `@object-ui/types/zod` mirrors keep an unknown key instead of silently dropping it
6
+
7
+
Clause-②: yes
8
+
Metadata-protocol: none
9
+
10
+
-**What changed.** 145 objects in the zod mirrors were strip-mode `z.object()`: a misspelled key parsed green and vanished, so `FormFieldSchema.safeParse({ name, type, labell: 'oops' })` succeeded with `labell` gone while `@objectstack/spec` refused it by name. Every one is now `.passthrough()` (`z.looseObject`), like `BaseSchema` already was. The tolerant face keeps the key; the strict authoring face (`StrictAnyComponentSchema`, used by `objectui validate`) still refuses it by name, so the two faces disagree only on keep-or-refuse, never on silently dropping.
11
+
-**What a caller sees.** Nothing new is refused: every document that parsed before parses now. A parsed result now carries the unknown keys its input carried, and the inferred `z.input` / `z.infer` types of those objects gain zod's `[k: string]: unknown` index signature. A declared `interface` is not assignable to a type with that signature; if you assign an interface-typed value to a variable typed by one of these mirrors' `z.input`, type it through the interface or pass it to `parse()`.
12
+
-**Guard.**`zod-mirror-parity.test.ts` now fails on any strip-mode object in `ui/packages/types/src/zod/`.
13
+
14
+
What to do: nothing, unless you relied on a mirror dropping keys from its parse result; strip them yourself or read only the keys you need.
0 commit comments