Skip to content

refactor(sections): gate every schema rule with zod's when instead of cloning its checks - #440

Merged
Vivswan merged 3 commits into
mainfrom
zod/rule-gate
Oct 5, 2026
Merged

Vivswan merged 3 commits into
mainfrom
zod/rule-gate

Conversation

@Vivswan

@Vivswan Vivswan commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Before

// src/sections/contract/module.ts: clone zod's private check objects so a node's own rule runs beside a nested failure
function reportingBesideFailures(check: z.core.$ZodCheck): z.core.$ZodCheck {
  const { when, ...def } = check._zod.def;
  const inner = check._zod.check;
  const clone = { _zod: { def: { ...def, when: (payload) => (when?.(payload) ?? true) && !failedPaths(payload.issues).some((p) => p.length === 0) },
    check: (payload) => { try { inner(payload); } catch (error) { swallowedThrowObserver?.(error); } } } };
  return clone;
}
// applied by cloneWith() to every clone and by checksReportingBesideFailures() in engine/validate.ts

After

// src/sections/shared/schema-helpers.ts: zod's public superRefine with its documented when gate
export function rule<T>(fn, params = {}) {
  return z.superRefine<T>(
    (value, ctx) => { /* run fn; drop the findings it appended under an already-failed path */ },
    { ...params, when: (payload) => notRefusedItself(payload) && (params.when?.(payload) ?? true) },
  );
}
// a site: z.object({ ... }).check(rule((entry, ctx) => { ... }))

How

  • One helper gates every schema rule with zod's documented when; the check-cloning, the swallowed-throw path, and its mutable test seam are deleted.
  • 26 sites convert to .check(rule(...)); three length bounds become gated checks of zod's own class, so the published schema and issue codes are unchanged.
  • A lint retires the class: a Biome plugin fails any bare .superRefine or .refine outside the helper, with a test pinning its verdicts and the fixture list derived from biome.json.
  • One behavior deleted on purpose: a rule that throws now surfaces; no rule throws, and the registry sweep proves it.

Proof

  • Schema: lib/settings.schema.json byte-identical to a build at main.
  • Tests: 1545 green across the lint, sections, and validate suites; the eight pinned behaviors unchanged except the one deleted swallow.
  • Gates: typecheck, lint, lint:arch, knip clean; the lint's negative control yields exactly one diagnostic; the cross-model review converged.
Technical details
  • Line accounting: source +559 -504, tests +320 -174, lint and config +18. Source grew by the helper, the gated length checks, and the conversions' wrapping; the deleted cloning was 80 lines.
  • Behaviors kept (each pinned): a node's rule runs beside a nested failure and keeps its path; a node refused whole runs no rule; a finding under a failed path is dropped; a check's own when ANDs with the gate; a rule composed after loosen() reports beside failures; a rule's own continue-true finding is not a failure; a leaf bound is suppressed when the leaf's type failed.
  • Numeric bounds left as zod's own: greater_than and less_than carry no default when, so a type failure already skips them; only the length checks defaulted to "when has length".
  • zod 4.6.5: its typed .min/.max params still omit when (a probe fails TS2353), so the length checks are constructed directly, the form rulesets already used.
  • Reviewer note (Copilot): the only lint carve-out is schema-helpers.ts, where rule() itself calls superRefine.
  • Recorded, not built: an aliased import of zod's superRefine called bare is outside the syntactic lint.
  • Design: step 1 of 4 in the zod public-API plan; steps 2 (knob routing), 4 (one schema-node adapter), and 3 (open() replacing loosen()) follow.
  • Files: src/sections/shared/schema-helpers.ts, src/sections/contract/module.ts, src/engine/validate.ts, 20 section schemas, lint/rule-gate.grit, biome.json, test/lint/{rule-gate.test.ts,fixture-lint.ts,never-throw.test.ts}, test/sections/{rule,loosen,raw-sibling}.test.ts, src/sections/shared/raw-values.ts.

… cloning its checks

Every section refinement is now authored as `.check(rule(fn))`, where rule() in src/sections/shared/schema-helpers.ts is `z.superRefine(wrapped, { when })` through zod's public API. src/sections/contract/module.ts no longer clones zod's private check objects (reportingBesideFailures), cloneWith no longer remaps checks, validate.ts parses the section shape directly, and the swallowed-throw path with its module-level test seam (observeSwallowedThrows) is gone.

Behaviors kept, each pinned by a test (test/sections/rule.test.ts, test/engine/validate.test.ts, test/sections/raw-sibling.test.ts):

1. a node's own rule runs although a nested value failed, and its finding keeps its path
2. a node refused as a whole (an aborting issue with an empty path) runs no rule
3. a rule's finding under a failed path is dropped; the shape's issue stands there
5. a rule's own `when` still decides, ANDed with the gate
6. a rule composed after loosen() (the routed list shape) reports beside a failed entry, in both container forms
7. a rule's own finding (continue: true) is not a failure; a list-level rule still runs
8. a leaf's length bound is suppressed when the leaf's type failed, so a wrong-typed value reports exactly one issue

Deleted on purpose (behavior 4): a rule that throws beside a nested failure is no longer swallowed. No registered rule throws on a raw value (the raw-sibling sweep proves it across the registry), so a throw is a bug that must surface; the sweep's control now expects the throw.

Counts: 26 rule() sites (22 in the section schemas and branches/index.ts, 2 in repository/schema.ts, 1 in webhooks/schema.ts, requirePlainMapping in module.ts, boundedString in schema-helpers.ts) and 3 length bounds (environments reviewers .max, secret_scanning start/end_delimiter .min) rebuilt as gated $ZodCheckMaxLength/$ZodCheckMinLength through minLength()/maxLength(). Numeric bounds stay zod's own: greater_than/less_than carry no default `when`, so a type failure already skips them. rulesets/schema.ts imports the helper as gatedRule, since its local rule() builds GitHub ruleset rules.

Lint: lint/rule-gate.grit, registered in biome.json for src/** except schema-helpers.ts (where rule() itself is the one superRefine), fails on any bare .refine()/.superRefine(), with or without params or type arguments. The module.ts exemption first proposed was dropped; requirePlainMapping is converted too. test/lint/rule-gate.test.ts pins the verdicts on a fixture tree, and test/lint/fixture-lint.ts (shared with never-throw.test.ts) copies every plugin biome.json names, so the next plugin cannot break the fixture.

Controls: lib/settings.schema.json built here is byte-identical (cmp) to the build at origin/main 674e8c9 and to the pre-change build; every user-visible refusal message is unchanged (test/sections pins them all).
The header named reportingBesideFailures in ../contract/module.ts as the owner of the raw-sibling contract; the previous commit deleted it, and rule() in ./schema-helpers.ts now states the contract. zod 4.6.5 still omits `when` from $ZodCheckMinLengthParams and $ZodCheckMaxLengthParams, so minLength()/maxLength() keep constructing the check objects directly.
Copilot AI balanced review requested due to automatic review settings October 4, 2026 23:41
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 36 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:142 159 chars warn 150
src/flows/settings-write.ts:30 11 comment lines warn 10
src/flows/snapshot.ts:189 13 comment lines warn 10
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/contract/module.ts:885 185 chars warn 150
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 refactor preserves validation semantics with comprehensive focused tests and lint enforcement.

Review effort: Balanced
Findings: None

What changed in this PR

Replaces private Zod check cloning with a shared, gated rule() helper while preserving validation behavior.

Changes:

  • Migrates section refinements to gated checks.
  • Removes check cloning and swallowed-error handling.
  • Adds lint enforcement and focused validation tests.
File Description
biome.json Enables the rule-gate plugin.
lint/​rule-gate.grit Rejects bare Zod refinements.
src/​engine/​validate.ts Parses section shapes directly.
src/​sections/​actions/​schema.ts Migrates action rules.
src/​sections/​branches/​index.ts Gates wildcard validation.
src/​sections/​branches/​schema.ts Migrates branch rules.
src/​sections/​check_suite_preferences/​schema.ts Gates duplicate-app validation.
src/​sections/​code_quality_setup/​schema.ts Gates setup validation.
src/​sections/​code_scanning_default_setup/​schema.ts Gates setup validation.
src/​sections/​contract/​module.ts Removes check cloning and throw swallowing.
src/​sections/​deploy_keys/​schema.ts Gates key validation.
src/​sections/​environments/​schema.ts Migrates environment rules and bounds.
src/​sections/​interaction_limits/​schema.ts Gates interaction-limit validation.
src/​sections/​pages/​schema.ts Gates read-only-field validation.
src/​sections/​repository/​schema.ts Migrates repository rules.
src/​sections/​rulesets/​schema.ts Migrates ruleset refinements.
src/​sections/​secret_scanning_custom_patterns/​schema.ts Gates regex and length checks.
src/​sections/​shared/​raw-values.ts Updates rule contract documentation.
src/​sections/​shared/​schema-helpers.ts Adds gated rule and length helpers.
src/​sections/​shared/​setup-schema.ts Updates setup-rule documentation.
src/​sections/​webhooks/​schema.ts Gates webhook validation.
test/​lint/​fixture-lint.ts Centralizes fixture lint execution.
test/​lint/​never-throw.test.ts Reuses the lint fixture helper.
test/​lint/​rule-gate.test.ts Tests refinement lint enforcement.
test/​sections/​loosen.test.ts Removes obsolete cloning tests.
test/​sections/​raw-sibling.test.ts Verifies rule exceptions propagate.
test/​sections/​rule.test.ts Tests gated rule semantics.

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

The platform's typos checker reads "ANDed" as a misspelling of "Ded" and fails standard-checks; the title now says the same thing without that word.
Copilot AI balanced review requested due to automatic review settings October 4, 2026 23:55

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 mechanical migration preserves documented validation behavior and includes comprehensive regression and lint coverage.

Review effort: Balanced
Findings: None

@Vivswan
Vivswan marked this pull request as ready for review October 5, 2026 00:03
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Oct 5, 2026
@Vivswan
Vivswan merged commit 0168a94 into main Oct 5, 2026
32 checks passed
@Vivswan
Vivswan deleted the zod/rule-gate branch October 5, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-green Owner approved: merge once every gate is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants