refactor: domain-layer restructure + correctness fixes & collection-validate perf - #39
Merged
Merged
Conversation
Structure-first plan (triple-reviewed). Each domain owns its types+enums+impl; higher layers import downward. No 'core kernel'; bottom leaves = symbols/errors/utils only. RAW metadata IR is a layer ABOVE the author primitives (it aggregates their types). Single documented upward edge: type-only rules->seal (EmitContext.addExecutor: SealedExecutors). Phases: A compile-cache extraction, snapshot harness, B skeleton, C dissolve types/enums/interfaces, D builder decomposition, E defer string.ts, F close-out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the (class,config) executor cache (compileCache WeakMap + configFingerprint/ getCached/setCached/clearCached/clearAllCached) out of seal.ts into seal/compile-cache.ts. seal.ts imports configFingerprint/getCached/setCached from it; the spec and unseal helper repoint their imports. Self-contained, zero codegen change. tsc 0, 2335 pass/0 fail, coverage 0, lint 0, knip clean, no circular deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Triple-reviewed. Add a common/ bottom leaf for ownerless cross-cutting symbols, with an explicit membership test (semantic owner, not fewest-importers): - common/: errors, utils, Direction, CacheKey, ClassCtor, RuntimeOptions(seam); symbols root-pinned. - domains own their types: CollectionType/MessageArgs->metadata, rule types->rules, Transformer*->transformers, SealedExecutors/SealOptions->seal, ExcludeMode->decorators. - Fix RuntimeOptions (was wrongly under seal — seal only threads it; common avoids a seal->runtime cycle). Fix MessageArgs rationale (owned via RuleDef membership). Complete the public re-export repoint list (BakerConfig, RuntimeOptions). common/ verified a true acyclic leaf (all members import-free). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Snapshots the generated source (deserialize/validate/serialize .toString()) for a representative DTO x config matrix (15 snapshots), reachable via getCached(Cls, configFingerprint(opts)). Locks codegen byte-identity so the structural moves in the seal/builder phases are machine-checked, not eyeballed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…config (Phase B) Pure moves (git rename) + import repoints, no logic/codegen change: - src/functions/ -> src/runtime/ - src/errors.ts, utils.ts -> src/common/ - src/collect.ts, meta-access.ts -> src/metadata/ - src/configure.ts -> src/config/ symbols.ts/baker.ts and the types/enums/interfaces/rule-machinery roots stay (move in Phase C). tsc 0, 2350 pass/0 fail, codegen snapshot unchanged (15), deps:check clean, knip clean, lint 0, build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (Phase C1a) Declarations moved verbatim into domain files (common/, rules/, transformers/, metadata/, decorators/, seal/) per the semantic-owner placement; root types.ts/enums.ts/interfaces.ts become thin re-export shims so all importers stay green. The one upward edge is the type-only rules/types -> seal/types (EmitContext.addExecutor: SealedExecutors). C1b will repoint importers off the shims and delete them. tsc 0, 2350 pass/0 fail, codegen snapshot unchanged (15), deps:check clean, knip/lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ims (Phase C1b) Repoint 49 files off the root types.ts/enums.ts/interfaces.ts re-export shims to the domain homes, then delete the three shims. types.ts/enums.ts/interfaces.ts are gone; each symbol is now imported directly from its owning domain. No declaration/name change. tsc 0, 2350 pass/0 fail, codegen snapshot unchanged (15), deps:check clean, knip/lint clean, build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…laceholder (Phase C2) Move analyzeAsync+nestedClassesOf -> seal/async-analysis.ts, mergeInheritance -> seal/merge-inheritance.ts, circularPlaceholder -> seal/circular-placeholder.ts, and the shared PRIMITIVE_CTORS -> seal/constants.ts. seal.ts is now a slim orchestrator; each module owns its own test surface (seal.spec repoints). Verbatim moves, no codegen change. tsc 0, 2350 pass/0 fail, codegen snapshot unchanged (15), deps:check clean, knip/lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ase D) DeserializeBuilder + SerializeBuilder classes hold codegen state (regexes/refs/execs/ options/resolve/...) as fields; the ~20 ctx-threaded functions become methods calling each other via this. Eliminates the FieldCodeContext threading, the fragment re-return, and the field<->nested-validate cycle-break callback. Inline-nested = a child builder sharing the parent's ref arrays (identical executor indices). buildDeserializeCode/buildValidateCode/ buildSerializeCode kept as thin exported wrappers; pure stateless helpers stay module-level. Structural only — generated new Function bodies BYTE-IDENTICAL (codegen snapshot unchanged). No new any/unknown. tsc 0, 2350 pass/0 fail, deps/knip/lint clean, build ok, memory pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, rule-metadata) git mv create-rule.ts/rule-plan.ts/rule-metadata.ts (+create-rule.spec) into src/rules/ and repoint importers (rules/* -> ./rule-plan, seal/deserialize-builder -> ../rules/rule-plan, index.ts public createRule -> ./src/rules/create-rule, root specs). Verbatim moves. tsc 0, 2350 pass/0 fail, codegen snapshot unchanged, deps/knip/lint clean, build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add index.ts barrels for common/metadata/config/seal/runtime (strict named exports, only what's consumed cross-dir — knip-clean). Route cross-directory imports through the barrels (22 files). One import kept deep by design: rules/types -> seal/types (SealedExecutors), the documented type-only visitor edge — barreling it would risk a runtime cycle. Update REFACTORING.md status (A-F done; E string-split deferred). tsc 0, 2350 pass/0 fail, codegen snapshot unchanged, deps:check clean (no cycles), knip/lint clean, build ok, memory pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uilder; tidy root - createChild now constructs the child via a real constructor 'scope' argument (shared reference arrays + circular-tracking set as the single mutable accumulator) instead of Object.create(prototype) + a readonly-bypass MutableBuilderState cast. No hack, no any/unknown. - Move orphan src/error-system.spec.ts -> src/common/ (error domain); src/ root now holds only baker.ts + symbols.ts (+ their co-located specs). tsc 0, 2350 pass/0 fail, codegen snapshot unchanged, deps/knip/lint clean, build ok, memory pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the stateless module-level codegen helpers + data tables (GEN, nestedErr*, guard strategies, generateConversionCode, categorizeRules, nested-result emitters, type-hint/asserter tables) out of deserialize-builder.ts into a dedicated seal/deserialize-codegen.ts. DeserializeBuilder (the stateful builder class) now imports them — SRP split between 'pure codegen utilities' and 'the builder'. deserialize-builder.ts 2003 -> 1624 lines; deserialize-codegen.ts 404 lines. tsc 0, 2350 pass/0 fail, codegen snapshot byte-identical (15/0), deps/knip/lint clean, build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…source of truth getDeserializeExtractKey/getSerializeOutputKey and getDeserializeExposeGroups/ getSerializeExposeGroups were direction-mirror copies (differing only in which directional @expose flag they honour). Collapse each pair into one Direction- parameterized helper (resolveExposeName / resolveExposeGroups) in codegen-utils.ts, the shared codegen source of truth. Both builders call them with their Direction. Removes 4 near-duplicate functions -> 2. tsc 0, 2350 pass/0 fail, codegen snapshot byte-identical (15/0), deps acyclic, lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the 2526-line flat string-rule list into a shared factory module plus six concern modules (basic/width/encoding/format/identifier/finance); string.ts becomes a pure re-export barrel preserving every export name and the original ordering, so the published ./rules surface is byte-stable. Every regex, data constant (incl. the ISO 3166-1 alpha-2/alpha-3 and ISO 4217 sets), checksum helper, and embedded codegen string moved verbatim — declarations are byte-identical to the original. Verified: tsc 0; codegen snapshot 15/0 (byte-identical); 2350 pass/0 fail; declaration hash identical (2154 lines); ./rules export set identical (83 exports); deps acyclic; knip/lint clean; build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n state-threading) sealOne/sealRegistry threaded executors/fp/options/sealed through every recursive call — the exact pattern the builders were classed to remove. Make a SealRun class holding those as fields; sealOne becomes a private method, recursion is this.sealOne(x). sealRegistry stays the thin module entry point (new SealRun(...).run()), mirroring how buildDeserializeCode fronts DeserializeBuilder, so seal/index.ts and baker.ts are unchanged. tsc 0, 2350 pass/0 fail, codegen snapshot byte-identical (15/0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… datetime spec per-file Match the repo-wide kebab file convention (collection/date/luxon/moment/number/string.ts); only transformers/index.ts referenced them. Split datetime-transformer.spec.ts into luxon.spec.ts + moment.spec.ts so each transformer source has its co-located unit spec. tsc 0, transformer specs 8 pass, lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… source split
Relocate all 440 tests verbatim into string-{basic,width,encoding,format,identifier,
finance}.spec.ts co-located with their source modules; delete the monolithic string.spec.ts.
Test titles are an exact verbatim partition (0 lost / 0 added); sum 440 = original 440.
tsc 0, full suite 2350 pass/0 fail, lint/knip clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ain imports through barrels Eliminate cross-domain deep imports. Each published dir (rules/transformers/decorators) now has public.ts (the curated public surface) + index.ts (the FULL internal barrel that also re-exports the internal symbols other domains need: EmitContext/InternalRule/emitRulePlan/RequiredType, Transformer/TransformFunction, ExcludeMode). package.json ./rules ./transformers ./decorators point at public.ts, so internals never leak; src modules import everything via '../<dir>' (index barrel). Repointed: decorators/field, metadata/types, seal/deserialize-builder, seal/deserialize-codegen, root index.ts. The ONLY remaining cross-dir deep import is rules/types -> seal/types (documented type-only cycle-break; routing it via the barrel would form a rules<->seal module cycle). Verified: tsc 0, no circular deps, codegen snapshot 15/0 byte-identical, 2350 pass/0 fail, knip/lint clean, build ok; ./rules ./transformers ./decorators public surfaces byte-identical to the old barrels; root '.' re-exported names unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…plit, public/internal barrels) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t fixes DI extraction (behavior-preserving, codegen byte-identical): - Lift seal/metadata/config stage logic into constructor-injected classes that own collaborators/state as private #fields: MetaStore (RAW key now injected), InheritanceMerger, CircularAnalyzer, AsyncAnalyzer, MetaValidator, ConfigNormalizer, CircularPlaceholder. SealRun's constructor wires the graph by injection; pure stateless helpers (codegen emitters, runtime dispatchers, validateExposeStacks) stay functions. - Smear cleanup: builder/codegen types -> seal/types.ts + seal/interfaces.ts; codegen data consts -> seal/constants.ts as distinct DES_GEN/SER_GEN (alias-imported as GEN, byte-identical). EmitContext-coupled GuardParams/TypeGateConfig stay internal to deserialize-codegen.ts to avoid a rules<->seal cycle. - Delete dissolved modules: collect, meta-access, merge-inheritance, validate-meta, async-analysis, config/configure. Audit bug fixes (package-wide line-by-line review; see .changeset/audit-bugfixes.md): - isEnum: numeric-enum reverse-map key names no longer accepted as valid values. - luxonTransformer: unparseable date passes through instead of laundering into an Invalid DateTime that serialized to null/"Invalid DateTime". - momentTransformer: parse in UTC (moment.utc) so zoneless strings are machine-independent. - checkCallOptions: validate per-call `groups` is string[] at the untyped call boundary. Consistency / dead-code / docs / coverage: - isISSN/isIBAN store resolved constraint booleans; emitGeneralRules markVar uses varPrefix; MetaStore test-only methods labeled + require throws BakerError; fix stale comments. - Add transformers/date.spec.ts (cover non-number/invalid pass-through branches); give CompileCache/MetaStore explicit constructors so coverage counts them (bun marks the synthesized constructor uncovered, which tripped the per-file 90% threshold). tsc clean; 2397 pass / 0 fail; 15 codegen snapshots byte-identical; lint/knip/deps clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
inheritance-merger, meta-validator, async-analyzer, and circular-placeholder were tested only transitively through seal.spec, while their siblings (circular-analyzer, compile-cache, expose-validator) each have an isolated spec. Add per-unit specs to make the testing axis consistent and to lock each collaborator's contract in isolation: - inheritance-merger.spec: own-meta passthrough, validation union-merge + dedup-by-ruleName, transform/exclude/type child-priority-else-inherit, flag supplementing, deep-copy isolation. - meta-validator.spec: every discriminator-shape rejection + Set value-class @field check. - async-analyzer.spec: sync/async rule + transform detection, direction filtering, nested-DTO flag propagation via the resolver, and nestedClassesOf resolution. - circular-placeholder.spec: non-async flags, BakerError-throwing members, writable own fields. tsc clean; 2427 pass / 0 fail; 15 snapshots byte-identical; lint/knip/deps clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ervable behavior) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… bugs
Reproduced five bugs (RED test first for each), plus type-organization/dedup cleanups.
Bugs:
- seal: discriminator + `type: () => [Base]` now dispatches per element in deserialize/validate
(was reading the discriminator off the array → invalidDiscriminator on valid input)
- seal: serialize throws BakerError on an instance matching no discriminator subtype instead of
leaking the raw, un-serialized object
- seal: `each` rule message/context functions receive the failing element as `value` (not the
whole collection), matching the element-level path
- rules: isDateString / isISO8601({strict}) use the proleptic Gregorian leap rule for years 0–99
(new Date(0..99,…) remapped to 1900–1999, so 0000-02-29 was wrongly rejected); no Date alloc
- rules: isHash / isTaxId throw at construction on an unknown algorithm/locale (was a runtime
always-fail), matching the locale rules' fail-fast model
Cleanups (no behavior change):
- decorators: split FieldOptions/ArrayOfMarker → interfaces.ts, RuleArg/FieldDecorator → types.ts,
ARRAY_OF/FIELD_OPTION_KEYS → constants.ts (field.ts −79 lines); FIELD_OPTION_KEYS is drift-safe
- metadata: extract shared DiscriminatorDef/DiscriminatorSubType (FieldOptions + TypeDef)
- rules: RulePlanCache single-sourced in rules/types.ts; isNumber maxDecimal codegen → helper
- seal: compile-cache fingerprint derived from Record<keyof SealOptions> so a new option is a
compile error until covered
typecheck/lint/knip clean, no circular deps, 2451 tests pass, coverage 99.8%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…odegen
Pure refactors, no behavior change — generated code is byte-identical for every shape covered by the
codegen snapshot; full suite (2451) and snapshots unchanged.
- type-resolver.ts: single `classifyTypeResult` reads the `@Type` thunk's Map/Set marker + array
unwrap once, replacing three near-identical copies in seal normalization, circular analysis, and
async analysis (the file comments flagged the drift risk)
- deserialize-codegen: `generateNestedEachResultCode` / `generateValidateNestedEachResultCode`
single-source the per-element `if (isErr/!=null) { re-path nested errors } else { success }` block
that the Set / Map / array / discriminator loops each repeated (deserialize + validate sides,
8 sites) — the exact divergence class behind the each-message bug; nestedErrPush/nestedErrReturn
are now fully encapsulated in those helpers
typecheck/lint/knip clean, no circular deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `await` branch of the discriminator-each codegen (added with the discriminator-array fix) was only exercised by sync DTOs; add an async-transform DTO so the awaited per-element dispatch and the async invalidDiscriminator path are covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dit residuals Ground-truth verified each remaining deep-review item; fixed the real ones (RED test first where observable), pinned the intentional one, and confirmed two agent-flagged "bugs" were non-issues. Fixes: - rules: isURL with default protocols copies into `constraints` instead of sharing the frozen module-level array — one rule mutating its constraints can no longer corrupt every other isURL rule - runtime: SEAL_TIME_KEYS (per-call rejection) now derives from a new single-source SEAL_OPTION_KEYS (Record<keyof SealOptions>) shared with the compile-cache fingerprint, instead of a hand-maintained partial alias list that would silently fall through on a future renamed/added option - decorators: ARRAY_OF is a `unique symbol`, so ArrayOfMarker is keyed precisely (`[ARRAY_OF]: true`) instead of a catch-all `[key: symbol]: true` index signature Tests: - pin unixSecondsTransformer's whole-second floor as intentional (standard Unix-timestamp convention, not the round-trip "bug" an audit flagged): -500ms -> -1s, 1500ms -> 1s - cover async deserialize/validate of discriminator arrays (the `await` codegen branch) Verified NON-issues (no change): per-rule message/context copy is required to pass each rule's own constraints to a message fn (not redundant with the field-level copy); serialize's `out[key]=undefined` for a non-optional field is consistent with flat fields, not a nested-only asymmetry. typecheck/lint/knip clean, no circular deps, 2456 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The benchmark suite referenced a removed global API (Recipe/seal/deserialize/
serialize/validate/configure as named exports of the package root), so the whole
suite failed to type-check and run. Rewire every bench to the current
`new Baker()` instance API (configure({...}) → new Baker({...})). Also fix a
collateral clobber where a blanket rename had rewritten ajv's local validate()
call in cold.bench.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llection validate Headline bug fix + a collection-validate perf win + an internal layering cleanup (full detail in the changeset). - Declared @type(() => Set/Map) collections now validate their elements. The declared-collection codegen hand-rolled its per-element loop apart from the canonical path and (a) dropped every each-rule on a Map, (b) ignored the runtime groups filter, (c) passed the whole collection (not the failing element) to a function message. All four Set/Map x deserialize/validate sites now route through one shared emitter with canonical rule-major order, group filtering, per-element value binding, and field[i] paths. RED tests added first. - Collection validate is ~4.7x faster on large arrays: the inline-nested validate path no longer eagerly allocates a per-element error-path string on the happy path; it is built only at the cold error-push sites. deserialize and all error paths are byte-identical (codegen snapshot updated). - createRule is now also exported from the ./rules subpath. - luxon/moment peer-dep error narrowed to ERR_MODULE_NOT_FOUND so a peer that is installed but throws during evaluation surfaces its real error. Internal-only: extracted TypeDef normalization out of the sealOne god-function, split large static lookup tables and the string-format validators into cohesive modules, simplified stateless helpers (config/type/expose normalizers as plain functions), and ran an oxfmt pass over the source. Public surface unchanged except the createRule subpath export (verified by export-diff + three adversarial reviews). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Restructures the package into clean per-domain layers and lands a batch of correctness fixes and one performance win. Three changesets (all minor) describe the consumer-facing changes:
Consumer-facing (see
.changeset/)audit-bugfixes—@IsEnumnumeric-enum lookup,momentTransformerUTC parsing,luxonTransformerinvalid-date passthrough, per-callgroupsvalidation. (2 observable behavior changes — see changeset.)discriminator-array-and-validator-fixes— discriminated arrays now work, serialize throws on unmatched discriminator subtype,eachmessages get the failing element,isDateString/isISO8601leap-year for years 0–99,isHash/isTaxIdreject unknown key at construction,isURLconstraint-array isolation.declared-collection-validation-and-perf— declared@Type(() => Set/Map)now validate their elements (Map previously dropped element rules; groups were ignored; function-messagevaluewas wrong); collectionvalidate~4.7× faster on large arrays (lazy error-path);createRuleexported from the./rulessubpath; precise luxon/moment peer-dep error.Internal (no API change)
Per-directory layering (
common/config/metadata/decorators/rules/transformers/seal/runtime), published barrels split intopublic.ts, strict per-scopetypes/interfaces/constants/enums, DI-class seal pipeline (SealRun), extractedTypeNormalizer/codegen utilities, large static tables →constants.ts,string-formatsplit into cohesive modules, benchmarks migrated to theBakerAPI.Verification
typecheck,typecheck:bench,build,test(2469 pass / 0 fail),test:coverage(per-file ≥90%),test:memory— all green locally.lint(oxlint 0/0),format:check,deps:check(0 circular),knip(0 dead).createRulesubpath addition.🤖 Generated with Claude Code