Refactor union dispatch to use reservation-based target assignment#295
Refactor union dispatch to use reservation-based target assignment#295DZakh wants to merge 14 commits into
Conversation
…regression Source-union conversion runs the three-tier target-matching algorithm independently per source variant with no target consumption: several variants can resolve to the same target, and a variant with no tier-1/2 match still falls through to tier 3 instead of erroring. Surface this with S.string->S.to(S.option(S.string)): on encode the string variant maps via tier-1 identity while None falls to tier-3 and stringifies to "undefined" rather than failing with invalid type. Captured as a regression test and documented in both usage guides, with FIXMEs marking the surprising None -> "undefined" coercion for later revisit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
…users Add a regression for the tier-2 nullish bridge: S.option(S.string)-> S.to(S.null(S.string)) maps None <-> null in both directions instead of stringifying, complementing the tier-1/tier-3 option case. Rewrite the new union-conversion doc paragraphs in both usage guides to drop tier jargon and internal "no target consumption" framing, keeping the practical behavior a new user needs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
The None -> "undefined" encode is a bug to be fixed, not intended design, so remove it from the usage guides. Keep only the intended optional behavior: the nullish bridge (None <-> null) when the target has a null/undefined case. The FIXME-marked regression in S_to_test.res stays as a tripwire that will flag the change once the behavior is fixed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Rewrite the union conversion intro to describe the intended design: each source case maps to one target case, same-type matches win and claim their target exclusively, and the remaining cases resolve over the still-free targets via the nullish bridge or coercion (erroring if none is left). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Make the whole "Decoding into / out of a union" section consistent with the intended design: each source case maps to one target, resolved tier 1 -> 2 -> 3 across all cases, and tiers 1 and 2 reserve their target so no other case can reuse it (tier 3 coercion does not reserve). Update the worked examples to match: bigint -> error once string is reserved, and the stringified "null" -> error once the undefined -> null bridge reserves the null target. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Union `.to` conversion now resolves which target each source variant maps to with reservation, matching the documented "direct matches are exclusive" model: - unionAssignTargets walks reservable variants tier 1 (same-type, exact const preferred) then tier 2 (nullish bridge) — both claim their target so no other variant can reuse it — then tier 3 shares whatever targets remain free. A reservable variant with no target left maps to `never` and errors. - Only primitives/literals/null/undefined that pass their value through unchanged are reservable; objects, instances, arrays, refs, nested unions and transforming variants keep the full target and dispatch at runtime. - Reservation applies only to genuine alternative sources (activeKey === ""), not when a known single value is decoded into a union. Effects: `S.option(x)->to(string)` no longer stringifies None to "undefined" (it errors); nullish bridges (None <-> null) compile without a try/catch dispatch wrapper; a coercion can no longer steal a target an identity match reserved (e.g. bigint/bool/float into a claimed string target error). Tests and the dict->object "deferred tier fix" cases updated to the new behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Move pure resolution logic out of the unionDecoder codegen into named, testable functions, separating "which target/variant" decisions from emission: - unionActiveKey: tier-narrowing (known input type -> single dispatch key, incl. nullish bridge) - unionPrioritizeConst: const-input variant ordering - (alongside the existing unionReservable / unionAssignTargets reservation pass) Behavior-preserving refactor; generated code unchanged, all tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Move the tag -> canonical type-only schema mapping out of the decoder loop into a pure unionTypeValidationSchema helper, continuing the resolution/ emission separation. Behavior-preserving; all tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Fold the three scattered passthrough conditions (self-decode noop, wider-union input, already-output) out of unionDecoder into a single named, documented unionIsPassthrough helper. These guards keep generated code minimal (emit the input untouched instead of a full dispatch), so they are preserved and centralized as explicit planning logic rather than deleted. Behavior-preserving; all tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Lift the entire emission body out of unionDecoder into a dedicated unionEmit function. unionDecoder now reads as passthrough -> resolve -> emit: - passthrough: unionIsPassthrough - resolve/plan: unionActiveKey, unionPrioritizeConst, unionAssignTargets - emit: unionEmit (the per-schema codegen optimizer) Faithful extraction — generated code is byte-identical, all 1474 tests green, no snapshot churn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
📝 WalkthroughWalkthroughUnion conversion now reserves direct and nullish target cases, separates coercive fallback behavior, and updates generated decoder control flow. Documentation and tests reflect the new exclusivity rules, while shaped-schema helpers and internal formatting are reorganized. ChangesUnion conversion behavior
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant SourceUnion
participant unionAssignTargets
participant unionDecoder
participant TargetSchema
SourceUnion->>unionAssignTargets: provide source and target cases
unionAssignTargets->>TargetSchema: reserve direct or nullish targets
unionAssignTargets-->>unionDecoder: return target assignments
unionDecoder->>TargetSchema: decode assigned cases
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/sury/src/Sury.res`:
- Around line 3122-3142: Update unionIsSelfDecodeNoop to require both
schema.refiner and schema.inputRefiner to be absent, alongside the existing
passthrough conditions. This ensures unionIsPassthrough never returns refined
input unchanged and preserves required user refiner calls.
- Around line 3493-3524: Update unionRefinerAttacher’s attach caching so it does
not reuse complete refiner check arrays across union cases. Regenerate
fn(~input) for each case-specific val, while caching only predicate data that is
independent of the case schema, expected type, path, and variable allocation;
preserve the existing merge behavior for current and newly generated checks.
- Around line 3282-3311: Update the tier-2 nullish bridge logic in the pass
containing `claim` so a source variant may claim the opposite nullish target
only when `unionReservable` approves it. Apply this check before selecting or
reserving a target, while preserving the existing null/undefined pairing and
reservation behavior for eligible variants.
- Around line 3253-3274: The fallback matching loop in the union reservation
pass must not reserve a mismatched literal target. In the `chosen.contents ===
-1` fallback within the `unionReservable` handling, require the candidate target
to be non-literal while retaining the `unionToKey` match; leave exact constant
matching in the preceding pass unchanged so broad targets remain available for
unmatched source literals.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ad0dcade-7073-4370-80ef-271f5e04cb6e
📒 Files selected for processing (6)
docs/js-usage.mddocs/rescript-usage.mdpackages/sury/src/Sury.respackages/sury/src/Sury.res.mjspackages/sury/tests/S_to_dictToObject_test.respackages/sury/tests/S_to_test.res
| and unionIsSelfDecodeNoop = (schema: internal) => { | ||
| schema.to === None && | ||
| schema.parser === None && | ||
| !(schema.tag->TagFlag.get->Flag.unsafeHas(TagFlag.ref)) && | ||
| switch schema.anyOf { | ||
| | Some(anyOf) => anyOf->Array.every(unionIsSelfDecodeNoop) | ||
| | None => true | ||
| } && | ||
| switch schema.items { | ||
| | Some(items) => items->Array.every(unionIsSelfDecodeNoop) | ||
| | None => true | ||
| } && | ||
| switch schema.properties { | ||
| | Some(properties) => properties->Stdlib.Dict.valuesToArray->Array.every(unionIsSelfDecodeNoop) | ||
| | None => true | ||
| } && | ||
| switch schema.additionalItems { | ||
| | Some(Schema(s)) => s->castToInternal->unionIsSelfDecodeNoop | ||
| | _ => true | ||
| } | ||
| schema.to === None && | ||
| schema.parser === None && | ||
| !(schema.tag->TagFlag.get->Flag.unsafeHas(TagFlag.ref)) && | ||
| switch schema.anyOf { | ||
| | Some(anyOf) => anyOf->Array.every(unionIsSelfDecodeNoop) | ||
| | None => true | ||
| } && | ||
| switch schema.items { | ||
| | Some(items) => items->Array.every(unionIsSelfDecodeNoop) | ||
| | None => true | ||
| } && | ||
| switch schema.properties { | ||
| | Some(properties) => properties->Stdlib.Dict.valuesToArray->Array.every(unionIsSelfDecodeNoop) | ||
| | None => true | ||
| } && | ||
| switch schema.additionalItems { | ||
| | Some(Schema(s)) => s->castToInternal->unionIsSelfDecodeNoop | ||
| | _ => true | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not treat refined unions as passthroughs.
unionIsSelfDecodeNoop ignores refiner and inputRefiner. Consequently, unionIsPassthrough can return the input unchanged for a schema that still requires custom validation, silently skipping those refiners. Require both fields to be absent before classifying a schema as a self-decode noop.
As per coding guidelines: “Never skip this call when user refiners must be preserved.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/sury/src/Sury.res` around lines 3122 - 3142, Update
unionIsSelfDecodeNoop to require both schema.refiner and schema.inputRefiner to
be absent, alongside the existing passthrough conditions. This ensures
unionIsPassthrough never returns refined input unchanged and preserves required
user refiner calls.
Source: Coding guidelines
| // Pass 1: tier 1 — same-type match (exact const preferred), reserves it. | ||
| for si in 0 to sourceLen - 1 { | ||
| let s = sources->Array.getUnsafe(si) | ||
| if unionReservable(s) { | ||
| let sKey = unionToKey(s) | ||
| let chosen = ref(-1) | ||
| for ti in 0 to targetLen - 1 { | ||
| if chosen.contents === -1 && !(reserved->Array.getUnsafe(ti)) { | ||
| let t = targetCases->Array.getUnsafe(ti) | ||
| if unionToKey(t) === sKey && t.const === s.const { | ||
| chosen := ti | ||
| } | ||
| } | ||
| } | ||
| if chosen.contents === -1 { | ||
| for ti in 0 to targetLen - 1 { | ||
| if chosen.contents === -1 && !(reserved->Array.getUnsafe(ti)) { | ||
| if unionToKey(targetCases->Array.getUnsafe(ti)) === sKey { | ||
| chosen := ti | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not reserve mismatched literal targets.
The fallback tag match lets source literal "a" reserve target literal "b" when no exact constant matches. If a broad string target is also available, "a" is assigned to "b" and fails at runtime instead of using the broad target.
Restrict this fallback to non-literal targets; exact literal matches are already handled by the preceding pass.
Suggested fix
- if unionToKey(targetCases->Array.getUnsafe(ti)) === sKey {
+ let target = targetCases->Array.getUnsafe(ti)
+ if unionToKey(target) === sKey && !target->isLiteral {
chosen := ti
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Pass 1: tier 1 — same-type match (exact const preferred), reserves it. | |
| for si in 0 to sourceLen - 1 { | |
| let s = sources->Array.getUnsafe(si) | |
| if unionReservable(s) { | |
| let sKey = unionToKey(s) | |
| let chosen = ref(-1) | |
| for ti in 0 to targetLen - 1 { | |
| if chosen.contents === -1 && !(reserved->Array.getUnsafe(ti)) { | |
| let t = targetCases->Array.getUnsafe(ti) | |
| if unionToKey(t) === sKey && t.const === s.const { | |
| chosen := ti | |
| } | |
| } | |
| } | |
| if chosen.contents === -1 { | |
| for ti in 0 to targetLen - 1 { | |
| if chosen.contents === -1 && !(reserved->Array.getUnsafe(ti)) { | |
| if unionToKey(targetCases->Array.getUnsafe(ti)) === sKey { | |
| chosen := ti | |
| } | |
| } | |
| } | |
| // Pass 1: tier 1 — same-type match (exact const preferred), reserves it. | |
| for si in 0 to sourceLen - 1 { | |
| let s = sources->Array.getUnsafe(si) | |
| if unionReservable(s) { | |
| let sKey = unionToKey(s) | |
| let chosen = ref(-1) | |
| for ti in 0 to targetLen - 1 { | |
| if chosen.contents === -1 && !(reserved->Array.getUnsafe(ti)) { | |
| let t = targetCases->Array.getUnsafe(ti) | |
| if unionToKey(t) === sKey && t.const === s.const { | |
| chosen := ti | |
| } | |
| } | |
| } | |
| if chosen.contents === -1 { | |
| for ti in 0 to targetLen - 1 { | |
| if chosen.contents === -1 && !(reserved->Array.getUnsafe(ti)) { | |
| let target = targetCases->Array.getUnsafe(ti) | |
| if unionToKey(target) === sKey && !target->isLiteral { | |
| chosen := ti | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/sury/src/Sury.res` around lines 3253 - 3274, The fallback matching
loop in the union reservation pass must not reserve a mismatched literal target.
In the `chosen.contents === -1` fallback within the `unionReservable` handling,
require the candidate target to be non-literal while retaining the `unionToKey`
match; leave exact constant matching in the preceding pass unchanged so broad
targets remain available for unmatched source literals.
| // Pass 2: tier 2 — nullish bridge to the opposite nullish target, reserves it. | ||
| for si in 0 to sourceLen - 1 { | ||
| switch assigned->Array.getUnsafe(si) { | ||
| | Some(_) => () | ||
| | None => | ||
| let sTag = (sources->Array.getUnsafe(si)).tag | ||
| let oppositeTag = if sTag === nullTag { | ||
| undefinedTag | ||
| } else if sTag === undefinedTag { | ||
| nullTag | ||
| } else { | ||
| sTag | ||
| } | ||
| if oppositeTag !== sTag { | ||
| let chosen = ref(-1) | ||
| for ti in 0 to targetLen - 1 { | ||
| if ( | ||
| chosen.contents === -1 && | ||
| !(reserved->Array.getUnsafe(ti)) && | ||
| (targetCases->Array.getUnsafe(ti)).tag === oppositeTag | ||
| ) { | ||
| chosen := ti | ||
| } | ||
| } | ||
| if chosen.contents !== -1 { | ||
| claim(si, chosen.contents) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Only reservable variants may claim nullish bridges.
This pass does not call unionReservable, so a transformed null/undefined variant with .to, parser, or serializer can reserve the opposite nullish target. That violates the reservation contract and can force the transformed case into an incompatible target.
Suggested fix
| None =>
let sTag = (sources->Array.getUnsafe(si)).tag
- let oppositeTag = if sTag === nullTag {
+ if unionReservable(sources->Array.getUnsafe(si)) {
+ let oppositeTag = if sTag === nullTag {
undefinedTag
- } else if sTag === undefinedTag {
+ } else if sTag === undefinedTag {
nullTag
- } else {
+ } else {
sTag
- }
- if oppositeTag !== sTag {
+ }
+ if oppositeTag !== sTag {
...
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Pass 2: tier 2 — nullish bridge to the opposite nullish target, reserves it. | |
| for si in 0 to sourceLen - 1 { | |
| switch assigned->Array.getUnsafe(si) { | |
| | Some(_) => () | |
| | None => | |
| let sTag = (sources->Array.getUnsafe(si)).tag | |
| let oppositeTag = if sTag === nullTag { | |
| undefinedTag | |
| } else if sTag === undefinedTag { | |
| nullTag | |
| } else { | |
| sTag | |
| } | |
| if oppositeTag !== sTag { | |
| let chosen = ref(-1) | |
| for ti in 0 to targetLen - 1 { | |
| if ( | |
| chosen.contents === -1 && | |
| !(reserved->Array.getUnsafe(ti)) && | |
| (targetCases->Array.getUnsafe(ti)).tag === oppositeTag | |
| ) { | |
| chosen := ti | |
| } | |
| } | |
| if chosen.contents !== -1 { | |
| claim(si, chosen.contents) | |
| } | |
| } | |
| } | |
| } | |
| // Pass 2: tier 2 — nullish bridge to the opposite nullish target, reserves it. | |
| for si in 0 to sourceLen - 1 { | |
| switch assigned->Array.getUnsafe(si) { | |
| | Some(_) => () | |
| | None => | |
| let sTag = (sources->Array.getUnsafe(si)).tag | |
| if unionReservable(sources->Array.getUnsafe(si)) { | |
| let oppositeTag = if sTag === nullTag { | |
| undefinedTag | |
| } else if sTag === undefinedTag { | |
| nullTag | |
| } else { | |
| sTag | |
| } | |
| if oppositeTag !== sTag { | |
| let chosen = ref(-1) | |
| for ti in 0 to targetLen - 1 { | |
| if ( | |
| chosen.contents === -1 && | |
| !(reserved->Array.getUnsafe(ti)) && | |
| (targetCases->Array.getUnsafe(ti)).tag === oppositeTag | |
| ) { | |
| chosen := ti | |
| } | |
| } | |
| if chosen.contents !== -1 { | |
| claim(si, chosen.contents) | |
| } | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/sury/src/Sury.res` around lines 3282 - 3311, Update the tier-2
nullish bridge logic in the pass containing `claim` so a source variant may
claim the opposite nullish target only when `unionReservable` approves it. Apply
this check before selecting or reserving a target, while preserving the existing
null/undefined pairing and reservation behavior for eligible variants.
| and unionRefinerAttacher = (~selfSchema: internal): (internal => unit) => { | ||
| let unionRefiner = selfSchema.refiner | ||
| let unionInputRefiner = selfSchema.inputRefiner | ||
| // Call each source refiner at most once so its predicate is embedded | ||
| // in `input.global.embeded` once and every case references the same | ||
| // `e[N]`. `B.embed` is append-only, so a per-case call would duplicate. | ||
| let cachedRefinerChecks = ref(None) | ||
| let cachedInputRefinerChecks = ref(None) | ||
| let attach = (current, source, cache) => | ||
| switch source { | ||
| | None => current | ||
| | Some(fn) => | ||
| let getCached = (~input) => | ||
| switch cache.contents { | ||
| | Some(checks) => checks | ||
| | None => | ||
| let checks = fn(~input) | ||
| cache := Some(checks) | ||
| checks | ||
| } | ||
| switch current { | ||
| | None => Some(getCached) | ||
| | Some(existing) => | ||
| Some( | ||
| (~input) => { | ||
| let arr = existing(~input) | ||
| let next = getCached(~input) | ||
| for i in 0 to next->Array.length - 1 { | ||
| arr->Array.push(next->Array.getUnsafe(i))->ignore | ||
| } | ||
| arr | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Do not cache complete refiner checks across cases.
fn(~input) receives a val, so the generated condition and failure builder may depend on that case’s schema, expected type, path, or variable allocation. Caching the entire check array from the first case reuses those val-specific checks for every later case. Cache only reusable embedded predicate data, or regenerate the checks per case.
As per coding guidelines: “Treat val as the compile-time view of a runtime value and preserve its core fields.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/sury/src/Sury.res` around lines 3493 - 3524, Update
unionRefinerAttacher’s attach caching so it does not reuse complete refiner
check arrays across union cases. Regenerate fn(~input) for each case-specific
val, while caching only predicate data that is independent of the case schema,
expected type, path, and variable allocation; preserve the existing merge
behavior for current and newly generated checks.
Source: Coding guidelines
Summary
Refactors the union decoder to use a three-tier reservation algorithm for assigning source variants to target variants when compiling
source → targetUnion. This improves correctness by preventing unreachable variants and enables better code generation through per-case.toapplication.Key Changes
New
unionReservablefunction: Identifies variants that can participate in reservation (primitives, literals, null/undefined/NaN with no transformations).New
unionAssignTargetsfunction: Implements three-tier target assignment:neverand fail at runtimeNew helper functions:
unionTypeValidationSchema,unionPrioritizeConst,unionActiveKey,unionIsPassthrough,unionRefinerAttacher,unionFail— extracted from inline union decoder logic for clarity and reusability.Refactored
unionDecoder: Now delegates tounionEmitfor the dispatch generation, usingunionIsPassthroughto detect when input can be returned unchanged.Per-case
.toapplication: When a union has a.totransformation and active key narrowing isn't possible, each variant's schema is updated with its assigned target and union refiners are attached per-case viaunionRefinerAttacher.Code formatting: Fixed indentation and formatting throughout the union decoder section and related functions to match project conventions.
Implementation Details
The reservation algorithm ensures that when coercing
source → targetUnion:stringsource variant claims thestringtarget variant (tier 1), leavingunit/None unreachablenullsource can bridge toundefinedtarget if no same-type match exists (tier 2)This prevents silent data loss and makes union coercion semantics explicit and predictable. The refactored code is more maintainable with extracted helper functions that can be tested and reasoned about independently.
https://claude.ai/code/session_01CuMuwLb7tufLms3U3Gs3Kv
Summary by CodeRabbit
Documentation
Behavior Changes
"undefined"values now produce clear validation errors.Refactor