Skip to content

refactor(sections): read every schema through one public-API adapter instead of three private walkers - #442

Merged
Vivswan merged 2 commits into
mainfrom
zod/schema-node
Oct 5, 2026
Merged

Vivswan merged 2 commits into
mainfrom
zod/schema-node

Conversation

@Vivswan

@Vivswan Vivswan commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Before

// src/sections/shared/snapshot-helpers.ts, one of four walkers reaching into zod's private internals
const def = defOf(schema);          // (schema as unknown as { _zod: { def: ZodDef } })._zod.def
switch (def.type) {
  case "optional": case "nullable": case "default":
    return project(def.innerType as z.ZodType, live);
  // ...
  default:
    if (!LEAF_TYPES.has(def.type)) throw new Error(`BUG: projectOntoSchema(): unhandled schema type ...`);

After

// every reader goes through one adapter over zod's public .def
const node = schemaNode(schema);
switch (node.kind) {
  case "wrapper": return project(node.inner, live);
  // ...
  case "leaf": return live;
}

How

  • One adapter, schemaNode(), maps every schema into a closed union (object, array, record, wrapper, union, leaf) from zod's public .def and class checks; the one unhandled-type invariant and the one leaf whitelist live there.
  • Four walkers fold onto it: snapshot projection, the replace sweep, canonical ordering, and loosen(), which now writes through public z.clone with a typed def.
  • Four test-side walkers and the schema generator read the adapter too, so no private access remains anywhere in the repository.
  • Behavior unchanged: the published schema is byte-identical and every existing expectation holds; one BUG message string moved to the adapter's wording.

Proof

  • Census: _zod references 6 to 0 across source, scripts, and tests.
  • Tests: 2174 green across the engine, sections, slices, and e2e oracle suites; scripts and lint 367; the full e2e run 370 of 370.
  • Gates: typecheck, biome, both lint plugins, lint:arch, knip clean; the cross-model review converged.
Technical details
  • Step 4 of the zod public-API plan, built on step 1 (refactor(sections): gate every schema rule with zod's when instead of cloning its checks #440, merged) and restacked onto main; step 2 (knob routing by typed options) and step 3 (open() replacing loosen()) follow.
  • Line accounting: source +211 -172, tests +134 -182, scripts +4 -8: the adapter and its header replace four walkers and two whitelists.
  • Deliberate readings: the adapter looks through prefault, catch, nonoptional, and readonly wrappers too, since an existing list-section test draws through .catch() and the old walkers disagreed on them; open means a catchall is declared and not never, which every old walker tested; the unreachable "int" leaf entry is dropped (z.int().type is "number"), with the slice walk census in the commit body.
  • One direct downcast from zod's core supertype to the classic type, because zod's generics type every child as the core supertype; no cast through unknown.
  • Recorded, not built: an instanceof check plus BUG in place of that downcast; refusing non-classic toJSONSchema callbacks instead of the generator override's null arm.
  • Files: src/sections/shared/{schema-node,snapshot-helpers,schema-helpers}.ts, src/engine/canonical.ts, src/sections/contract/module.ts, .github/scripts/gen-settings-schema.ts, test/{engine/canonical,schema-slices,sections/raw-sibling,sections/loosen}.test.ts, test/e2e/gen-support.ts.

…instead of three private walkers

Four walkers each reached into zod's private `_zod.def` through a hand-mirrored def view, carried their own "unhandled schema type" BUG throw and leaf whitelist, and cast through `unknown`: `projectOntoSchema` and `replaceSweep` (snapshot-helpers), `canonicalNode`/`admits` (engine/canonical), and `loosen`/`detectKnobUnion`/`cloneWith` (contract/module). Four test-side copies and the JSON schema generator read the same internals.

One adapter now does the reading, through zod's public classes and `.def`: `schemaNode(schema: z.ZodType): SchemaNode` in src/sections/shared/schema-node.ts. Its closed union:

- object: shape, catchall, open (a catchall other than never; a strip object stays closed to the walks)
- array: element
- record: value
- wrapper: inner plus the kind (optional, nullable, default, prefault, catch, nonoptional, readonly)
- union: options plus knob, the knobbed()/layeredList() pair when the union is one (same detection as the deleted detectKnobUnion)
- leaf: string, number, boolean, null, unknown, never, enum, literal, with an enum's or literal's values (duplicates kept, so a seeded draw is stable)

Every walker consumes SchemaNode: the two snapshot helpers, the two canonical walkers, loosen (which now writes through public `z.clone(node.schema, { ...node.schema.def, patch })`, typed against the narrowed class), the canonical completeness test, the slice identity test, the raw-sibling walk, the fuzz generator, and the generator override. `defOf`, `ZodDef`, `cloneWith`, `detectKnobUnion`, and the four test-side def views are deleted. The one BUG throw and the one leaf whitelist live in the adapter; `loosen` keeps only its own-refinements-on-a-knob BUG.

Behavior on the shipped schemas is unchanged: 2174 tests pass, the e2e suite passes 370/370, and lib/settings.schema.json built from this tree is byte-identical (cmp) to a build at the base branch. The one test edit is the expected text of the moved unhandled-type BUG in test/sections/loosen.test.ts.

Two deliberate readings the old walkers did not share. The "int" leaf entry is gone: `z.int().type` is "number" in zod 4, and a walk over every node of SettingsFile met exactly these types: array, boolean, default, enum, literal, never, null, nullable, number, object, optional, record, string, union, unknown. The four extra wrappers (prefault, catch, nonoptional, readonly), absent from that census, are looked through by every walk where before two walkers threw, two fell through, and the generator looked through.

`_zod` census over src/, .github/scripts/, and test/: 6 hits before, 0 after.
schemaNode() in src/sections/shared/schema-node.ts is now the one place that recognizes the knobbed()/layeredList() union by the wrapper's entries; loosen() and engine/canonical.ts consume its union.knob instead of detecting the pair themselves.
Copilot AI balanced review requested due to automatic review settings October 5, 2026 00:30
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 33 warning(s).

File Size Tier Cap
.github/actions/setup/action.yml:7 169 chars warn 150
.github/scripts/gen-inputs-table.ts:54 11 comment lines warn 10
.github/scripts/generated.ts:28 191 chars warn 150
.github/scripts/release-pipeline.ts:7 156 chars warn 150
.github/scripts/release-pipeline.ts:458 151 chars warn 150
.github/scripts/release-pipeline.ts:1252 153 chars warn 150
.github/scripts/release-pipeline.ts:1 35 comment lines (header) warn 25
.github/scripts/release-pipeline.ts:454 14 comment lines warn 10
.github/workflows/post-green.yml:29 153 chars warn 150
.github/workflows/update-release-pr.yml:110 14 comment lines warn 10
docs/upgrading/v2-to-v3.md 1276 lines warn 1040
src/engine/layers.ts:75 157 chars warn 150
src/engine/layers.ts:95 167 chars warn 150
src/flows/settings-write.ts:137 159 chars warn 150
src/github/secret-scan.ts:43 11 comment lines warn 10
src/schema.ts:186 153 chars warn 150
src/schema.ts:197 176 chars warn 150
src/sections/contract/errors.ts:14 14 comment lines warn 10
src/sections/secret_scanning_custom_patterns/compilable-form.ts:382 161 chars warn 150
src/sections/shared/roles.ts:43 13 comment lines warn 10
src/sections/shared/schema-helpers.ts:12 153 chars warn 150
src/types.ts:16 156 chars warn 150
test/docs/guides.test.ts:451 155 chars warn 150
test/docs/nightly-workflows.test.ts:206 160 chars warn 150
test/e2e/constants.ts:9 197 chars warn 150
test/e2e/generators.ts 2634 lines warn 2560
test/e2e/generators.ts:1704 166 chars warn 150
test/e2e/generators.ts:1858 161 chars warn 150
test/e2e/generators.ts:1667 12 comment lines warn 10
test/engine/execute.test.ts:617 152 chars warn 150
test/flows/merge-parity.test.ts:63 164 chars warn 150
test/scripts/auto-fix-allowlist.test.ts:12 12 comment lines warn 10
test/sections/refusal-messages.test.ts:61 19 comment lines warn 10

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The adapter migration is consistent across production, generation, and test paths with no unresolved defects found.

Review effort: Balanced
Findings: None

What changed in this PR

Centralizes Zod schema traversal behind the public schemaNode() adapter while preserving schema generation and runtime behavior.

Changes:

  • Adds a closed schema-node representation.
  • Migrates projection, canonicalization, loosening, generation, and test walkers.
  • Removes private _zod access.
File Description
.github/​scripts/​gen-settings-schema.ts Uses the adapter during JSON Schema generation.
src/​engine/​canonical.ts Migrates canonical traversal.
src/​sections/​contract/​module.ts Reworks loosen() around public cloning.
src/​sections/​shared/​schema-helpers.ts Updates adapter-related documentation.
src/​sections/​shared/​schema-node.ts Adds the centralized schema adapter.
src/​sections/​shared/​snapshot-helpers.ts Migrates projection and replace-sweep traversal.
test/​e2e/​gen-support.ts Migrates slice-based generators.
test/​engine/​canonical.test.ts Migrates canonical schema census helpers.
test/​schema-slices.test.ts Validates slice composition through the adapter.
test/​sections/​loosen.test.ts Updates the centralized invariant expectation.
test/​sections/​raw-sibling.test.ts Migrates raw-sibling schema walkers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@Vivswan
Vivswan marked this pull request as ready for review October 5, 2026 00:59
@Vivswan
Vivswan merged commit 4ac778b into main Oct 5, 2026
30 checks passed
@Vivswan
Vivswan deleted the zod/schema-node branch October 5, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants