refactor: monorepo split + honest compat layer + honest benchmarks + honest docs [Phase 0-4] - #37
refactor: monorepo split + honest compat layer + honest benchmarks + honest docs [Phase 0-4]#37Isqanderm wants to merge 91 commits into
Conversation
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
Also fixes a latent cross-package import break in 3 test files (branch-coverage-boost, complex-combinations, final-coverage-gaps) left over from Task 4's class-transformer extraction: they imported Type from the old packages/core/src/compat/class-transformer path, which no longer exists. Repointed to ../../../../../class-transformer/src.
…e test imports Replaces the deep relative import (../../../../../class-transformer/src) in 3 class-validator tests with a proper dependency-graph-recorded approach: @om-data-mapper/class-transformer as a workspace devDependency, resolved in tests via a vitest resolve.alias pointing at the sibling package's src (same pattern Task 6 will use for the meta-package).
…chmarks until Phase 3
…sing ambient types
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…o pnpm; drop dead .npmignore Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
There was a problem hiding this comment.
💡 Codex Review
data-mapper/examples/ergonomic-api.ts
Line 20 in dd2cf7b
This example still imports from ../src/decorators, but this commit moved the root src tree under packages/core/src, so the documented npx tsx examples/ergonomic-api.ts flow now fails with module-not-found before users can run it. Update the examples to import from the public package/workspace entry point (or the new package source path) along with the other stale example imports found by repo-wide search.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "types": "build/cjs/index.d.ts", | ||
| "sideEffects": false, | ||
| "exports": { | ||
| ".": { |
There was a problem hiding this comment.
Export the decorators subpath used by consumers
Because this package now has an exports map but only declares the root entry, any real Node/npm consumer importing the subpath used in the moved meta tests (@om-data-mapper/class-validator/decorators) will get ERR_PACKAGE_PATH_NOT_EXPORTED; the Vitest alias masks this locally by rewriting to ../class-validator/src. Add an explicit ./decorators (and any intended nested subpaths such as ./decorators/custom) export, or change those imports to the package root.
Useful? React with 👍 / 👎.
…ties/skipUndefinedProperties Add runtime skipProp guard to generated per-property validation code in generatePropertyValidation and generateAsyncPropertyValidation. Every constraint check is wrapped in `if (!skipProp)` except isDefined, which class-validator always evaluates regardless of skip* options.
Wrap every constraint check (sync and async, including isDefined) in an `if (!(opts.stopAtFirstError && Object.keys(propertyErrors).length > 0))` guard so at most one constraint is recorded per property when the option is set. Composes inside the existing skipProp guard from Task 2.
…settle Fired-and-forgotten async constraint tasks (validateBy/custom, no intermediate awaits) could still add a second constraints entry under stopAtFirstError: true when they resolved after a later sync check on the same property had already recorded its own failure. Add a post-trim in generateAsyncPropertyValidation, run after Promise.all(propertyAsyncTasks) and before error assembly, that keeps only the first failing constraint in declaration order (codegen-computed order array) and deletes the rest. The existing synchronous per-check guard is unchanged — it already handles the common sync-only case. Regression test: a bottom (registered-first) async ValidateBy that always fails, combined with a sync MinLength on the same property, reproduces two constraint keys pre-fix and one post-fix.
Add forbidUnknownValues option support to validate/validateSync functions. When enabled, returns an unknownValue error for objects without validation metadata, maintaining backward compatibility with default value of false. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code
…; fix ValidationError.target docs Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…rrorFields When validationError stripping options are combined with forbidUnknownValues, early-return branches bypassed stripErrorFields, leaving target/value fields in unknownValue errors. Now properly strips fields in all code paths. Adds regression test: forbidUnknownValues + validationError stripping combined. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…ompat APIs registerDecorator lets consumers build custom TC39-decorator validators without a @ValidatorConstraint class, dispatching to the existing 'custom'/'validateBy' compiled constraint paths. Guards against re-registering the same logical constraint on repeated addInitializer runs (one per instantiation) via an early existence check. getMetadataStorage exposes a minimal read-only facade (getTargetValidationMetadatas) over the Symbol-based metadata storage. Also hardens compiler.ts: validateBy constraint names are interpolated as JS identifiers in generated code, so both the sync and async validateBy branches now fall back to 'custom' when the name isn't a valid identifier.
…d ClassTransformOptions fields Coerce primitives via @type(() => Number|String|Boolean|Date) during plainToClass-family transforms when enableImplicitConversion is true; null/undefined pass through untouched. Without the flag, behavior is unchanged (TC39 decorators have no reflected type metadata, so conversion requires an explicit @type). Removed enableCircularCheck, exposeUnsetFields, targetMaps, and enableValidation from ClassTransformOptions - all four were declared but never read anywhere in src, tests, or the meta package. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…ngesets for Phase 2
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…red it but no file existed Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…nly subpaths The /decorators subpath isn't declared in class-validator's exports map and only resolved through a vitest alias; real consumers get ERR_PACKAGE_PATH_NOT_EXPORTED. Point the three test files at the package root, which re-exports all decorators. Swept tests, examples, and benchmarks for any other subpath not present in a package's exports map - none found. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
… current generator compat-class-validator.md (both mirrors): the claim that hoisting the validator object out of addInitializer makes dedup "exact (by reference) rather than source-text-based" oversold a mechanism with no observable effect - the reference check is a fast path only, since equal references always imply equal source text. Rewrite to say what's actually true. transformer-jit-internals.md (both mirrors): commit 5a219d4 changed the core JIT generator so every key and path is JSON-escaped bracket access (target["email"], source?.["user"]?.["profile"]?.["email"]) instead of dot access. Update every stale dot-notation sample to the current bracket-access shape, including the generateSafePropertyAccess listing itself. Illustrative "slow"/hypothetical comparison lines that were never generated code are left as dot-notation on purpose. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
Adds the one case missing from codegen-escaping.test.ts (added in 5a219d4): a target property name containing a double quote, the only surface where a key lands inside a string literal in generated source (cache lookups via cacheKey(), and the _wrapInTryCatch error message). The existing kebab-case target case only proves property-access syntax is safe, not string-literal escaping - so the _wrapInTryCatch fix previously shipped with zero coverage. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…undle-size claim Review round 1 fixes: 1. transformer-jit-internals.md (both mirrors): prettier's singleQuote rule was silently rewriting the "Generated code" samples to single quotes, while the generateSafePropertyAccess "Examples:" comment (untouched by prettier, since it's inside //) correctly kept double quotes - the same document contradicted itself about what generateSafePropertyAccess/prop/cacheKey actually emit. All three build brackets with JSON.stringify, which always emits double quotes. Added <!-- prettier-ignore --> above every affected fenced block (12 per mirror) and restored the literal double-quoted output, including cache['__defValues'] staying single-quoted (it's a hardcoded literal in the generator template, not JSON.stringify'd) and catch(error) with no space (matches _wrapInTryCatch verbatim). Verified with `pnpm exec prettier --write` (reports "unchanged") and `prettier --check .` (no unrelated regressions). 2. packages/core/src/index.ts: dropped the "70% smaller bundle size compared to class-transformer" bullet - an unverifiable figure of the same fabricated-benchmark class as the ones already purged from this file, missed because the Task 9 grep pattern didn't cover it. Its only checkable content (no runtime deps, tree-shakeable) already restates the "Zero Dependencies" bullet directly above it, so it's dropped as redundant rather than rewritten. Ran a wider sweep of packages/*/src for any other unverifiable quantitative/benchmark-shaped claim: `[0-9]+(\.[0-9]+)?[xX]\b`, `[0-9]+(\.[0-9]+)?%`, `[0-9]+ *times`, `compared to`, `outperform`, `beats`, `reduce(s|d)? .* by [0-9]`, `improve(s|d)? .* by [0-9]`, `up to [0-9]`, plus `faster than|slower than|fastest|smaller than|larger than`, `blazing|lightning fast|ultra-fast|ultra fast|best-in-class|industry-leading`, and `benchmark`. Only hits: three false positives in class-validator's ISBN-10 (`[0-9X]`) and Ethereum-address (`0x...`) regex literals - unrelated to performance claims. No further fabricated figures found. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…ion use real package resolution
`packages/core/src/index.ts` did `export * from './core/Mapper'` (the legacy
class with a static `.create()`) alongside `export { Mapper } from './decorators'`.
Under ES module semantics the explicit named export wins, so the legacy class
was never reachable from the published surface. Removing the star re-export
leaves the public surface byte-identical (verified against the built ESM index)
while retiring dead code from the export map.
The ESM post-install simulation reached `Mapper.create()` through a raw
node_modules file path that deliberately bypassed the very exports map it
claimed to validate, then printed "Package is ready for npm publication".
Rewritten so every scenario imports by package name only — `om-data-mapper`
and its declared subpaths — with a scenario asserting undeclared subpaths are
blocked, and a success message that claims only what was tested.
Also records a real behavior change in the migration guides (EN + RU):
`@Map('items[0].name')` worked by accident under the old codegen and no longer
resolves now that path segments are emitted as escaped literal keys; and warns
at the legacy class that its own generator still interpolates keys and paths
unescaped and must be fixed before the class is ever exported again.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…v4 examples decay unnoticed benchmarks/tsconfig.json failed tsc --noEmit with 4x TS2339 in the transformation honesty guards, and nothing ran it. The root cause is not overload inference (omPlainToInstance/ctPlainToInstance already infer the exact class correctly) - it's that TS narrows `if (!(x instanceof X))` to `never` when x's static type is already exactly X, which is precisely the scenario these guards exist to check at runtime. Route the diagnostic `.constructor.name` reads through an `unknown`-typed helper instead of relying on narrowing to prove the negated branch is unreachable. Wire `pnpm --filter examples run typecheck` and `pnpm --filter benchmarks run typecheck` into CI, after build (both check against built package output). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…e() ship broken packages/core and packages/om-data-mapper each execute their built ESM output in a real Node process via test:esm; class-transformer had nothing equivalent. That's exactly why a require() call inside TransformClassToPlain/TransformClassToClass/TransformPlainToClass survived into the ESM build and threw ReferenceError for every consumer, and no test noticed (fixed in 0f4e567). A plain .mjs file can't use decorator syntax, so this manually applies the TC39-shaped decorator functions (value, context) => replacement the way a decorator transform would, then actually calls the decorated methods - the require() bug only broke at call time, not at decoration time. Verified by temporarily reintroducing require() into the built output: the test fails hard (module-format ambiguity / ReferenceError) instead of passing silently. Also verified the root `pnpm run test:esm` (pnpm -r --workspace-concurrency=1 run test:esm) picks the new script up automatically. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…overclaims "Every entry point above was reached" was untrue of Scenario 5, whose two subpaths are deliberately NOT reached - that's the assertion. "the same resolution a consumer of the published package gets" glossed two things: this is Node self-reference, not an installed node_modules lookup, and nothing here runs `npm pack` or exercises the `files` array, so it doesn't verify the published tarball actually ships build/. This file's entire subject is claim accuracy (it was rewritten for exactly that reason), so its own summary needs to be exact too. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…guidance SECURITY.md's "Safe Usage" section documented Mapper.create() - an API no consumer can import (0da556d removed its re-export; the decorator Mapper shadows it) whose code generator carries an unescaped-interpolation injection hazard regardless of config trust level (see the @internal warning in packages/core/src/core/Mapper.ts). Rewrite the section around the Decorator API, which is the actual published surface, and add an explicit note that Mapper.create() is neither reachable nor safe. Also fixes a pre-existing dead link (docs/DECORATOR_API.md, which doesn't exist) to docs/transformer-usage.md. CONTRIBUTING.md's test template did `import { Mapper } from '../src'` then `Mapper.create(...)` - broken against the current tree, since `../src` no longer re-exports the legacy class and the root `Mapper` is the decorator. Replaced with a working Decorator API example; verified by running it as a real vitest test (temporarily, not committed) before writing it into the doc. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
The plan document was committed unformatted and fails the repo's prettier --check CI gate. Formatting only; no content change. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…ucible fast-check drew fresh inputs on every run, so a failing property failed on inputs nobody could reproduce - the next run was green and the evidence was gone. That is exactly how this suite behaved: the URL property failed once under load and never again in ~65 subsequent runs, including 20 under full CPU saturation. Pin all 25 properties to one shared seed via a runs(n) helper. Breadth is unchanged - each property still explores 10-100 distinct generated inputs - but the same ones every time, so CI stops reddening at random and a real failure can be investigated instead of vanishing. A determinism guard test fails if the seed is ever dropped: it draws twice and asserts the draws are identical (verified red before this change, producing two entirely different URL sets). Investigated and ruled out as causes of the observed failure: the wall-clock assertions in regression.test.ts (0.0020ms against a 0.5ms baseline - 250x headroom even with every core saturated), CPU contention (6/6 clean under load), concurrent vitest runs sharing the coverage dir (3/3 clean), and a gap in @isurl itself (20,000 fc.webUrl draws, zero rejections). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
…ention
global.gc was never available: forceGC() checked `if (global.gc)` and did
nothing, so every 'Force GC before measurement' comment described something
that never happened. Without a collection, the heapUsed delta conflates
retained memory with garbage V8 had not gotten around to freeing - the tests
measured noise. Evidence: for the same operation, growth swung from -1.48MB
to +4.09MB across five runs. With a real gc it is +0.02..+0.09MB, and each
test varies by at most 0.02MB run to run.
- Expose gc via the ROOT vitest config. pool/poolOptions are root-only in
Vitest; setting them in the project config is silently ignored, which cost
a round of debugging.
- forceGC() now throws instead of no-oping. A missing --expose-gc must fail
loudly, not leave the suite green while measuring nothing.
- Thresholds 10/15MB -> 1MB, sized from the measured 0.09MB worst case with
~10x headroom. The old numbers were sized for the multi-megabyte noise and
would have passed a 9MB leak: verified by probe - a deliberate retention
reads as 3.99MB growth, which trips 1MB and would NOT have tripped 10MB.
- Drop Math.abs from the assertions. It contradicted the comment beside it
('negative growth is good - it means GC is working') by failing the test
when the heap ended smaller, which is the healthiest outcome.
Full suite 584/584 green.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
The README said nothing about performance, which is not useful to someone deciding whether to adopt the library - but v4's failure was publishing numbers nobody could reproduce, so the snapshot is bound by rules rather than just added: - every figure comes from `pnpm bench:compat` / `pnpm bench:core`, named in the section, and the suite's honesty guards abort the run if either engine is not doing the same real work on the same fixtures; - the environment is stamped (CPU, OS, Node, upstream versions, date) so a reader can tell whether it applies to them, and the wide relative margin of error on some samples is disclosed; - the losses are published alongside the wins. Hand-written JavaScript beats the core mapper in three of four scenarios, and the section says so, with the honest conclusion: JIT pays off against interpreting metadata per call, not against direct property access. benchmarks/README.md previously argued that numbers in documentation are always wrong, which the new section would have contradicted. It now states the split explicitly - none here, one dated snapshot in the root README - and records the three rules above so the next person publishing a figure has them. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
The three internal packages move from the unregistered `@om-data-mapper` scope to `@tech-pioneer`, an npm namespace that already exists: @om-data-mapper/core -> @tech-pioneer/data-mapper-core @om-data-mapper/class-transformer -> @tech-pioneer/data-mapper-class-transformer @om-data-mapper/class-validator -> @tech-pioneer/data-mapper-class-validator The meta-package keeps its published name, `om-data-mapper`, so the existing 4.x installs upgrade to 5.0.0 under the name they already use and the v4->v5 migration guide's import mapping stays correct. Its `./class-transformer-compat` / `./class-validator-compat` subpaths are untouched. Directories are deliberately not renamed: `packages/core` still holds `@tech-pioneer/data-mapper-core`. Only registry names change. This removes the "register the om-data-mapper npm organization" manual prerequisite that Phase 5 was blocked on. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
The five pending changesets name their packages in YAML frontmatter, which Changesets resolves against the workspace: left at the old scope, `changeset version` would fail on unknown packages and the Phase 2/4.5 release notes would never reach the renamed packages. Verified with `changeset status`: all four packages resolve (two minor, two patch), unchanged from before the rename. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
Root and per-package READMEs, docs/, docs-ru/, examples/, benchmarks/, and the CHANGELOG's [Unreleased] entry now name the packages as they will actually be published. Prettier re-wrapped six files whose markdown tables and prose lines no longer fit the longer names. The design spec is amended rather than rewritten: an amendment note at the top records the namespace decision, the Phase 1 step no longer says to register an org, and the "npm scope" risk is marked resolved with what genuinely remains manual before publishing (`npm login`, an NPM_TOKEN with publish rights for @Tech-Pioneer). The Phase 0-4 plans under docs/superpowers/plans/ keep the old scope: they record what was executed at the time, not what ships. The meta-package's npm badges and links are untouched — they point at `om-data-mapper`, which does not move. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
…ection
`transformValue` recursed only when `transformationType === 'plainToClass'`,
so on the way out a nested class instance was copied into the result as-is.
Its own metadata never ran: an `@Exclude()`d field one level down reached
`classToPlain`, `instanceToPlain` and `serialize` output, and a nested
`@Expose({ name })` rename was silently dropped. The same decorators worked
on a top-level property, which is what kept this hidden.
Recursion is deliberately not gated on `typeFunction`. In this direction the
class is known from the value itself, and class-transformer 0.5.1 recurses
whether or not the property carries a `@Type` — verified by running both
implementations over the same fixtures.
Only the `classToPlain` direction is touched. `classToClass` passes its own
transformation type through both legs and keeps its current shallow
behavior; that is a separate defect, fixed separately.
Boundaries verified against upstream: `Date` passes through instead of being
expanded, plain object literals are copied key-for-key including undecorated
keys, primitives and arrays of primitives are untouched, and a self-referential
graph throws `RangeError` on both sides.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
`each` was declared nowhere — not in `ValidationDecoratorOptions`, not in
the constraint metadata, and not in the compiler. `@IsString({ each: true })`
type-checked, ran, and validated nothing per element: the constraint was
applied to the array itself, which for most predicates silently passes. That
made it the widest silent gap in the package, since `each` is the most-used
decorator option upstream.
The generated check was already parameterised by the name of the value
variable, so per-element validation needed no changes to the ~90 constraint
branches: `emitConstraintCheck` and its async twin wrap the existing output in
a loop over the collection and emit the check against the element instead.
Errors stay keyed by constraint name, so a failing element writes the same key
as any other and the property ends up with one error, matching upstream rather
than one error per element.
Behaviour verified by running this package and class-validator 0.14.4 over the
same fixtures: element counts, Sets, empty collections, non-collections,
custom messages and the reported `value` all agree. The default-message prefix
deliberately differs: upstream's "each value in " reads on into the property
name it puts in every message, and this package's messages carry no property
name, so the prefix stops at "each value".
Deduplication now compares `each`. Without that, `@MinLength(5)` and
`@MinLength(5, { each: true })` on one property collapse into a single
constraint and one of the two checks disappears silently.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
`each` appeared in neither the supported nor the missing list, so the tables gave no signal either way about the most-used decorator option upstream. Both mirrors now carry a row, including the deliberate difference in the default message prefix. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
`getValidationMetadata` tested `target[VALIDATION_METADATA]` for truthiness. That lookup walks the static prototype chain, so `class Child extends Parent` found Parent's metadata object and never created its own — every constraint Child declared was written into Parent's map. Compiled validators are cached by `metadata.target`, which for that shared object is Parent, so validating a Child instance returned Parent's validator and Child's own constraints were never checked. Nothing failed; the fields were simply not validated. The outcome depended on which class was constructed first. Subclass first: it created the map, and both sets of constraints ran. Parent first: Parent owned the map and the subclass's fields went unvalidated. A base DTO carrying shared fields — the ordinary NestJS arrangement — is the broken order. Both lookups now test for an own property. No merging from the parent is needed: TC39 field initializers run for the whole hierarchy when a subclass is constructed, with `this.constructor` pointing at the subclass, so inherited constraints are already registered against it. Verified against class-validator 0.14.4 for both orders, a three-deep chain, a subclass that declares nothing, a property decorated at two levels, and a subclass reached through `@ValidateNested`. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
Prettier normalises `*own*` to `_own_` in markdown emphasis. The file was written after the local format check ran, so CI caught it instead. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
…type
BREAKING CHANGE: a decorated property whose value is of the wrong type — or
absent — is now reported as invalid rather than passing silently.
The generated checks read "if the type fits and the value is bad, report an
error". A value of the wrong type fell out of the condition entirely, so
`@IsEmail()` passed on `5`, `@Min(5)` passed on `'a'`, `@ArrayMinSize(2)`
passed on `'ab'`, and all of them passed on `undefined` and `null`. Upstream's
predicates return false for a value they cannot measure, which is an error.
Two shapes had to change. Fifteen checks guarded inline (`typeof v === 'x' &&
bad`) and were inverted to `typeof v !== 'x' || bad`. Fifty opened a block
(`if (typeof v === 'x') { ... }`) and now report the constraint's own error in
the mismatch branch and validate in the `else`. `@IsLatitude` and
`@IsLongitude` accept a number or a numeric string, so their two-branch chains
were rewritten by hand rather than mechanically; the inline `@IsEmpty` and
`@IsNotEmptyObject` guards were left alone, their semantics being the inverse.
Verified against class-validator 0.14.4 over 46 decorators × six value shapes:
321 of 322 comparisons agree. The one that does not is unrelated to types —
this package's `@IsBase64` regex accepts a string upstream rejects.
The escape hatches for absent values are upstream's and already work here:
`@IsOptional()`, `skipUndefinedProperties`, `skipNullProperties`,
`skipMissingProperties`. Tests cover each of them alongside the new behaviour.
No existing test changed. All 726 pass, and the benchmark honesty guards still
hold.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
BREAKING CHANGE: error keys and what `validateOrReject` rejects with have
changed to match class-validator 0.14.4.
Both are read directly by consumers — `error.constraints.isUrl` and
`catch (errors) { errors.map(...) }` — and both were spelled differently here,
so migrated code read `undefined` and failed later, away from the cause.
Measured across 48 decorators: 14 reported a key upstream does not use. Ten
were acronym casing (isURL/isUUID/isJSON/isIP/isFQDN/isISO8601/isJWT/
isMACAddress/isISIN/isISBN). Four reported a different constraint's key:
- `@Length` emitted separate minLength and maxLength constraints, so it
reported the wrong key and, when both ends failed, two of them. It is now a
single `isLength` constraint with an optional `max`, as upstream has it.
- `@IsDateString` delegated to `@IsISO8601` and inherited `isIso8601`.
- `@IsPositive` was `min: 0.000001` reporting `min`, `@IsNegative` was
`max: -0.000001` reporting `max`. They now test the sign, which also fixes
the range: 5e-7 is positive and was rejected.
`validateOrReject` rejects with the error array; `validateOrRejectSync` throws
it too, so the twins agree. `ValidationFailedError` stays exported for anyone
importing it, but nothing throws it now.
21 existing tests asserted the old keys — they encoded the divergence rather
than catching it — and were updated to the upstream spelling. No assertion was
weakened: each still requires the same error, under the name upstream gives it.
All 750 tests pass, and a fresh comparison across the 48 decorators reports
zero key differences.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
BREAKING CHANGE: which constraints run under a group filter has changed to match class-validator 0.14.4. The filter was inverted in both directions. A constraint carrying `groups` ran only when a matching group was passed, so a plain `validate(dto)` skipped it — upstream treats an absent filter as no filtering and runs everything. An ungrouped constraint ran even under a filter selecting other groups, where upstream skips it. `always` — the upstream escape hatch for precisely that situation — was declared on every decorator and in ValidatorOptions, stored in metadata, and never read by the compiler. It is honoured now, per decorator and as a call-wide default via `ValidatorOptions.always`. `@IsOptional`'s `groups` and `always` used the same dead paths and are fixed alongside, on the sync and async generators both; the async optional branch had kept the old rule after the sync one was corrected, which is what the async tests caught. All three gates now share one `groupGateExpression` helper rather than repeating the condition at four sites. 13 existing tests asserted the inverted behaviour — one was even named "should validate all constraints when no groups specified" while asserting that grouped constraints do not run. Their expectations were corrected to what upstream does; none was weakened. A fresh comparison across the four filter shapes × three constraint shapes reports zero differences from upstream. All 768 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
Same slip as the inheritance changeset: the file was written after the local format check ran, so CI caught it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
Two parity gaps left from the audit.
`IsUrl` is upstream's spelling and the implementation was already present —
only the export name was missing, so `import { IsUrl }` failed for anyone
migrating. Both names now point at the same decorator; the error key was
already `isUrl`.
`@IsBase64` accepted `'zz'` and `'abc'`: its pattern ended with an optional
group of two or three unpadded characters, which let any length through.
validator.js requires a multiple of four with padding on, which is its
default. Checked against upstream over twelve inputs — padded, unpadded and
malformed — with no differences left.
One existing test asserted a 15-character string is valid base64. Upstream
rejects it unless `IsBase64Options.padding: false` is passed, which this
package does not accept, so the test now asserts the rejection.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
BREAKING CHANGE: default validation messages now begin with the property name. `@MinLength(3)` on `username` produced `must be at least 3 characters`. Any surface that shows `error.constraints` to a person had to re-attach `error.property` by hand to say which field the sentence was about. Upstream writes `username must be longer than or equal to 3 characters`; the subject now matches, even where the wording does not. Under `each` the property follows upstream's preposition: `each value in tags must be ...`. It previously stopped at `each value`, since there was no property name for `in` to lead into. Caller-supplied messages, string or function, are untouched. Five defaults opened with their own noun and would have read as `lat latitude must be ...` once prefixed, so they were reworded: @IsLatitude, @IsLongitude, @ArrayUnique, @mindate and @MaxDate. Upstream embeds the property mid-sentence for the last three (`All tags's elements must be unique`); rather than special- case them, they were rewritten to read after a prefix. Of six messages compared with upstream, all six now share the subject and three are identical word for word. No existing test needed changing. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
BREAKING CHANGE: classToClass / instanceToInstance now clone nested values instead of sharing them, and @Transform runs once rather than twice. It was a classToPlain -> plainToClass round trip carrying the 'classToClass' type through both legs. Neither leg recursed — each descends only for its own direction — so nested instances and arrays came out shared with the source, and mutating the clone mutated the original. @Transform ran on both legs, so a `value + 1` transform produced `value + 2`. Replaced with one recursive pass. A nested value's class comes from the value rather than from @type, matching upstream, which restores a nested instance to its own class with or without the decorator. Date is cloned instead of being walked as a plain object, and Map / Set keep their type instead of collapsing to `{}` through the plain round trip. Two behaviours that look like bugs and are not — both measured against class-transformer 0.5.1: @exclude() does not remove a property in this direction, and the target is built with `new`, so an excluded property is never copied onto it and keeps its field initializer's value rather than the source's. Seven comparisons against upstream — nested cloning, class preservation, arrays, Dates, @Transform call count and result, excluded-property value — report no differences. All 810 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
…ckage did not IsAscii, IsBase32, IsBase58, IsBooleanString, IsByteLength, IsFirebasePushId, IsFullWidth, IsHSL, IsHalfWidth, IsHash, IsHexadecimal, IsISRC, IsISSN, IsMilitaryTime, IsMultibyte, IsNumberString, IsOctal, IsRgbColor, IsSurrogatePair, IsTaxId, IsVariableWidth. Each gets a decorator, a codegen branch and the same treatment as the rest: upstream's error key, the package's type rules (a non-string fails rather than passing), and support for each / groups / always / a custom message. Comparing the two packages' runtime exports, nothing upstream exports is missing here now. Every expectation came from running class-validator 0.14.4 rather than from reading its source, which is the only reason these match: IsFirebasePushId reports under a capitalised key, IsRgbColor rejects spaces between components, IsByteLength counts UTF-8 bytes, IsBase58 accepts 'abc', IsAscii rejects the empty string, IsNumberString rejects exponent notation, and IsISSN verifies its mod-11 check digit. The last two surfaced only in the wide comparison after the first implementation passed the narrower tests. IsTaxId implements the en-US format only and IsHash takes the algorithm name without the further options; both are marked in the compat table, whose "Missing vs upstream" section is now empty and no longer hedged as written "from memory". 58 comparisons against upstream over the new decorators report no differences in outcome or error key. All 901 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T9KKEkbFmzqeFcrv18auEB
Summary
Phases 0-4 of the om-data-mapper v5 revival: monorepo split (Phase 0-1), the compat-honesty pass (Phase 2), honest benchmarks (Phase 3), and the documentation rewrite (Phase 4).
Phase 0-1 — monorepo split
@tech-pioneer/data-mapper-core(1.0.0),@tech-pioneer/data-mapper-class-transformer(1.0.0),@tech-pioneer/data-mapper-class-validator(1.0.0), and a meta-packageom-data-mapper(5.0.0) that re-exports the v4 surface (including theclass-transformer-compat/class-validator-compatsubpaths).Phase 0-1 post-review fixes (whole-branch review)
fix: add @types/node devDependency— pnpm's isolated linker exposed that@types/nodewas never a direct dependency (npm hoisting masked it); CI caught it, local runs did not.style: format repository with prettier— the newformat:checkCI gate required a one-time repo-wide format (style-only, verified with a whitespace-ignoring diff).fix: disarm release publish path until Phase 5— with zero changeset files,changesets/action+publish:input would have published all 4 unpublished versions to npm on merge to main. Thepublish:input andNPM_TOKENenv are removed until Phase 5; merging this PR cannot trigger a release.Phase 2 — compat honesty (13 commits, TDD, per-task subagent review)
Every declared compat option now either works or is removed, with honest compat tables.
class-validator (
packages/class-validator):ValidatorOptionsare implemented in the JIT compiler as runtime checks (sync + async codegen):whitelist(now actually strips undecorated properties — recorded in the changeset as a behavior change under the 5.0.0 major),forbidNonWhitelisted,skipMissingProperties/skipNullProperties/skipUndefinedProperties(with the upstream@IsDefinedexemption),stopAtFirstError(per-property; async path enforces via post-trim in constraint declaration order),forbidUnknownValues(defaultfalse— documented divergence from upstream ≥0.14).message— called at runtime withValidationArguments; explicit message wins over a validator'sdefaultMessage; composite constraint envelopes are unwrapped soargs.constraintscarries the original decorator args.validationError: { target, value }stripping (recursive, all return paths); fixed the wrong doc comment onValidationError.target.registerDecorator(TC39addInitializermigration pattern, documented with a working example) +getMetadataStorage(minimal facade:getTargetValidationMetadatas).ValidateBynames are sanitized before being interpolated as identifiers.class-transformer (
packages/class-transformer):enableImplicitConversionimplemented:@Type(() => Number|String|Boolean|Date)-gated primitive coercion (honest limitation documented: no reflect-metadata under TC39 decorators, so@Typeis required).ClassTransformOptionsfields:enableCircularCheck,exposeUnsetFields,targetMaps,enableValidation.Docs & release:
docs/compat-class-validator.mdanddocs/compat-class-transformer.md— per-option/per-decorator tables, every row verified against the shipped code (including the honest ❌ rows:strictGroups,dismissDefaultMessages,$property-templating,ValidationError.contexts,@Typediscriminator).minorfor both compat packages; meta bumps viaupdateInternalDependencies).Process: 9 plan tasks executed by fresh implementer subagents with per-task spec+quality review, two fix rounds, and a final whole-branch review (verdict "with fixes" — both findings fixed and re-review-verified). Known parked residual:
@Matchesfunction messages still receive the internal{pattern, modifiers}shape inargs.constraints(follow-up for Phase 4).Phase 3 — honest benchmarks (7 commits, per-task subagent review)
The v4 benchmark suite is deleted and rebuilt from scratch as a private
benchmarksworkspace package:RESULTS.md("20,000–60,000% faster" rows marked "(estimated)"), the no-op validation comparison (fed class-validator metadata to om's engine, which returned[]— the timed no-op produced the headline numbers), the dead prebuilt-JS pipeline, and 4 dead devDeps (@types/benchmark,chalk,ts-node,tinybench).passwordnever leaks). A guard failure throws and fails the run; the v4 no-op failure mode is structurally impossible.pnpm bench/bench:core/bench:compat(build packages first, then vitest bench). Core benches measure om's decorator API vs hand-written vanilla baselines (vanilla honestly wins 3/4). Compat benches measure om vs the realclass-validator/class-transformer(upstream legacy decorators applied programmatically; metadata pre-warmed on both sides).benchmark.ymlrebuilt (setup mirrors ci.yml, frozen lockfile, 30-min timeout, no|| echo, nocontinue-on-error) — a red bench run fails red.benchmarks/README.mdcontains zero performance numbers, names the v4 failure explicitly, and documents the fairness rules.Known carry-forward to Phase 4 (recorded in the final review): fabricated v4 numbers still live in the root
README.md,CHANGELOG.md,SECURITY.mdanddocs//docs-ru/— the Phase 4 docs rewrite must purge them all before Phase 5 publishes anything.Phase 4 — documentation (13 commits, per-task subagent review)
The docs now tell one honest, consistent story across English, Russian, and npm surfaces:
README.md: 1,474 → 178 lines. All fabricated numbers gone (17.28x, 42.7x, the self-contradicting "vs Vanilla" table, "98% coverage"), the class-validator adapter finally has a section (v4's README never mentioned the largest module), and the only numbers left are repo-regenerable (549 tests / 38 files, zero runtime deps, ~90 validator decorators). Performance section states outright: this project publishes no benchmark numbers it cannot regenerate in CI.filesalready ships them at publish).docs/troubleshooting.mdextracted from the README's 590-line troubleshooting section, curated to the real v5 API (one factually wrong claim about@Defaultordering caught and fixed against the test suite).docs/migration-v4-to-v5.md— package layout, import mapping (v4 subpaths still work), and every behavior change from the Phase-2 changesets, with thewhitelist-now-actually-strips breaking change called out redundantly (NestJSValidationPipenote included).docs/honesty pass — complete index (compat tables and migration guides were unlisted), stalesrc/…paths → realpackages/…paths, non-existent@MapNested/@Whenexamples rewritten to the real API (verified against core's tests and codegen), every fabricated "10x" table deleted, both "100% API compatible" claims qualified.examples/fixed — every example imported a deleted pre-monoreposrc/; now a private workspace package (benchmarks pattern) importing published names, type-checked AND executed under node. Bonus finding: the legacyMapper.create()class API is unreachable from the published packages (export { Mapper } from './decorators'shadowsexport * from './core/Mapper') — examples rewritten to the decorator API, docs don't advertise the legacy class; Phase 5 must decide: fix the export or drop the legacy API.docs-ru/synced — 5 missing files translated (both compat tables, both migration guides, troubleshooting), honesty fixes mirrored into the 4 existing RU guides, index rebuilt; heading-parity and byte-identical code blocks verified for all 10 file pairs.package.jsondescription.SECURITY.md's live fabricated figure ("112-474% faster") purged;CHANGELOG.mdgot an[Unreleased]pointer to the migration guide (historical 4.x entries with their old numbers left untouched as historical record).Post-phase docs-vs-code re-audit (4 parallel verifiers: READMEs, docs/, benchmarks, docs-ru): READMEs, benchmarks (incl. a live
pnpm bench:corerun — all honesty guards pass, vanilla honestly wins 2/4 core scenarios), and docs-ru came back clean;docs/had 7 defects, all fixed and re-verified against source: the deadalwaysValidatorOption honestly marked ❌ (stored in metadata, never read by the compiler) andvalidationErroradded to the options listing;validation-jit-internals.mdcodegen examples corrected to the real generated code (bracket access not optional chaining,targetfield present, custom-validator key iscustom, Debugging section rewritten to the public API only);transformer-jit-internals.mdmetadata-storage claim fixed (both APIs use WeakMap, not Symbols) and the "Both APIs compile a specialized transform function once" paragraph split into the honest per-API story (all EN + RU mirrors).Known follow-ups: CHANGELOG historical entries link three deleted docs; two RU guides retain pre-existing partially-translated prose (full re-translation is a future pass); the
alwaysoption is a candidate for actual implementation in a future code change.Out of scope (tracked in the design spec)
Phase 5 (publish, dependabot PRs, PR #21 decision). See
docs/superpowers/specs/2026-08-24-monorepo-v5-design.md.No manual prerequisite remains for Phase 5's package names — see the namespace section at the end of this description. What is still manual:
npm login, and anNPM_TOKENwith publish rights for@tech-pioneer.Test plan
rm -rf node_modules+ package build dirs,pnpm install --frozen-lockfile)pnpm run build— all 4 packages build cleanpnpm lint— cleanpnpm exec prettier --check .— cleanpnpm test— 38 files / 549 tests passing (518 baseline + 31 new Phase 2 tests)pnpm bench— full suite runs end-to-end, all honesty guards passpnpm run test:esm— 12/12 ESM smoke scenarios passingpnpm --filter examples run typecheck— all 9 examples type-check against the built packages (Phase 4)plainToInstance,Expose,validateSync,registerDecorator,getMetadataStorageall resolve via theexport *re-export)🤖 Generated with Claude Code
https://claude.ai/code/session_01E2BunJE88yu6YcADceinHe
Phase 4.5 — post-review fixes (27 commits)
The whole-branch adversarial review of this PR confirmed 20 code defects that the Phase 0-4 honesty pass had missed — they were in the code, not the prose. All 20 are fixed here, plan-driven (
docs/superpowers/plans/2026-08-25-review-fixes.md), one task per finding, each task independently reviewed.Two further defects surfaced afterwards, in the test suite itself: both the property tests and the memory-leak tests were non-deterministic, and the memory tests turned out to be incapable of detecting the leaks they claimed to guard against. Both are fixed below.
Finally, with the measurement infrastructure trustworthy, the README publishes performance numbers again — this time bound by rules (see the last section).
Silent-failure bugs (class-validator)
stopAtFirstErrorcould drop every error. The generated async trim resolved the first failing constraint withk in propertyErrors—inwalksObject.prototype, so a validator namedtoString/constructormatched an inherited key, deleted every real error, and the invalid payload passedvalidate(). NowObject.hasOwn.__proto__as an error key silently passed invalid data.propertyErrors.__proto__ = msghits the prototype setter, creates no own property, and the failing constraint looked like a passing one.sanitizeValidatorNamenow rejects it.registerDecoratorsilently dropped registrations. A second unnamed inline validator, a constraint class re-registered with differentconstraints, and — worst — two registrations differing only byoptions.groupsall collapsed into one, so validating with the dropped group ran no check at all. The dedup predicate now compares constraints,message,alwaysandgroups, and prefers validator reference identity.@Matches/@Validate/@ValidateBybuilt a freshvalueobject insideaddInitializer, which runs on everynew Dto(), so the identity-based dedup never matched and the constraints array grew without bound. One constraint object per decorator application now.unknownValueresult.Symbol— two installed copies of the package validated nothing. NowSymbol.for.Upstream-fidelity fix (class-validator)
Class-based custom validators reported errors under a hard-coded
customkey, so consumers migrating from upstream readerrors[0].constraints.isLongerThanand gotundefined. They now report under the registered name, with upstream's precedence (@ValidatorConstraint({name})wins over an explicitnameargument — verified against class-validator 0.14.4'sValidationExecutor.getConstraintType). Behavior change, recorded in a changeset.class-transformer
enableImplicitConversionreturnedNaNfor arrays.@Type(() => Number) scores!: number[]with['1','2']producedNumber(['1','2'])=NaN; upstream yields[1, 2]. Array elements are now coerced individually.@TransformClassToPlain/@TransformClassToClass/@TransformPlainToClassused a literalrequire('./functions')that survived intobuild/esm/("type": "module") —ReferenceError: require is not defined in ES module scopefor every ESM consumer. No test invoked them, so CI stayed green. Static imports now (verified acyclic), plus the package's first ESM smoke test, which executes the built output in a real Node process.core (JIT codegen)
Field names and
@Map()paths were interpolated verbatim intonew Functionsource.@Map('content-type')emittedsource?.content-type— aSyntaxErrorthat killed the whole mapper class at first instantiation, so kebab-case API keys were simply unusable. A quote-bearing key escaped thecache['...']literal and executed arbitrary code inside the compiled function; the regression test proves the injection ran against the old code. Every emission site now usesJSON.stringify-escaped bracket access — including one inside_wrapInTryCatchthat the original audit missed. Side effect: numeric path segments (@Map('items.0.name')) work now instead of throwing.Packaging and honesty
"files": [..., "LICENSE"]and no such file existed. npm ignores missing entries silently.Mapper.create()API was unreachable (the decoratorMappershadows it under ES module semantics) yet the ESM "post-install simulation" reached it through a rawnode_modulespath that bypasses the veryexportsmap it claimed to validate, then printed "Package is ready for npm publication". The dead re-export is removed, the simulation now resolves everything by package name, and its summary claims only what it tests.SECURITY.mdno longer presents that API under "✅ Safe Usage", andCONTRIBUTING.md's test template works again.@tech-pioneer/data-mapper-class-validator/decorators, which is not in the exports map and resolved only through a vitest alias — real consumers getERR_PACKAGE_PATH_NOT_EXPORTED.42.7x,17.28x,112-474%,70% smaller bundle). Purged; the completeness grep returns nothing. (Numbers do return to the README at the end of this PR — measured ones, under rules. See the last section.)docs/transformer-jit-internals.md(both mirrors) were stale after the escaping change and are now byte-accurate, withprettier-ignoreso the formatter cannot re-break them.CI holes that let all of this ship green
examples/andbenchmarks/are now typechecked in CI — the exact rot class that let v4's examples decay unnoticed.benchmarks/tsconfig.jsonhad been failingtsc --noEmitwith 4 errors that nothing ran.packages/class-transformergained atest:esmscript; the recursive root script picks it up automatically.fast-checkproperties drew fresh inputs every run, so a failure landed on inputs nobody could reproduce — one URL property failed once and never again in ~65 subsequent runs, including 20 under full CPU saturation. Pinned to a shared seed, with a guard test that fails if the seed is dropped. Investigated and ruled out first: the wall-clock assertions inregression.test.ts(250x headroom even with every core busy), CPU contention, concurrent vitest runs, and a gap in@IsURLitself (20,000 draws, zero rejections).Known follow-ups from Phase 3, now resolved
The fabricated figures Phase 3 flagged as "still living in the root README, CHANGELOG, SECURITY.md and docs" are gone, and the Phase 4 question "fix the export or drop the legacy API" is answered: dropped, with a warning comment left on the class recording that its own codegen is unescaped and must be fixed before anyone re-exports it.
forceGC()wasif (global.gc) global.gc()andglobal.gcisundefinedunder vitest, so every "Force GC before measurement" comment described an action that never happened. Without a collection theheapUseddelta conflates retained memory with garbage V8 had not freed yet: the same operation swung from −1.48MB to +4.09MB across runs. Consequence — the suite could not detect a leak at all, and a genuine 9MB retention passed the 10MB threshold silently.--expose-gcis now supplied via the root vitest config (pool/poolOptionsare root-only in Vitest and are silently ignored in a project config),forceGC()throws instead of no-opping so a missing flag fails loudly, and the thresholds drop from 10/15MB to 1MB — sized from the measured 0.09MB worst case with ~10x headroom, not guessed. Verified the guard can still fail: a deliberate retention reads as 3.99MB, which trips 1MB and would not have tripped 10MB. Also removedMath.absfrom the assertions, which contradicted the comment beside it ("negative growth is good — it means GC is working") by failing the test when the heap ended smaller.Deliberately not fixed
BaseMapper's codegen carries the same unescaped-interpolation defect. It is unreachable from the published surface, the decorator API uses it only in type positions, and it is now marked with a warning — escaping dead code would be busywork.Performance numbers return to the README — under rules
With the benchmark suite trustworthy (honesty guards that abort if either engine is not doing the same real work) and the test suite deterministic, the README can finally answer "is this faster?" without lying. A README that says nothing about performance is not useful to someone deciding whether to adopt the library; the v4 failure was not publishing numbers, it was publishing numbers nobody could reproduce.
The new section is bound by three rules, and
benchmarks/README.mdnow records them for whoever publishes the next figure:pnpm bench:compat/pnpm bench:core.benchmarks/README.mdhad argued that numbers in documentation are always stale and were what caused the v4 failure, which the new section would have flatly contradicted. Rather than leave two documents arguing with each other, it now states the split explicitly — no figures in the benchmark package, one dated snapshot in the root README — and carries the rules above.Test plan (Phase 4.5)
pnpm run build— 4 packages cleanpnpm lintandpnpm exec prettier --check .— cleanpnpm test— 44 files / 584 tests, 10 consecutive full-suite runs with zero failures after each of the two determinism fixespnpm run test:esm— all 3 packages, including class-transformer's new smoke testpnpm --filter examples run typecheckandpnpm --filter benchmarks run typecheck— cleanpnpm bench— honesty guards pass (verified they can still fail: the TypeScript fix touched only the diagnostic string inside eachthrow)Phase 4.6 — npm namespace (3 commits)
The scoped packages move to
@tech-pioneer, an npm namespace that already exists, instead of the@om-data-mapperorg that Phase 0-1 left as a manual prerequisite for Phase 5:The meta-package keeps its published name.
om-data-mapperhas version history through 4.2.1 and live installs; keeping it unscoped means existing users upgrade to v5 under the name they already depend on, and the import mapping indocs/migration-v4-to-v5.md(om-data-mapper,./class-transformer-compat,./class-validator-compat) stays correct as written.Directories are deliberately not renamed —
packages/corestill holds@tech-pioneer/data-mapper-core. Only registry names change.Beyond the manifests, three things had to move with them or the release would have broken:
changeset versionfails on unknown packages and the Phase 2 / 4.5 release notes never reach the renamed packages.changeset statusnow resolves all four (2 minor, 2 patch) — unchanged from before.pnpm install; the clean-room--frozen-lockfilerun below is what proves it matches the new manifests.docs/superpowers/specs/2026-08-24-monorepo-v5-design.mdcarries an amendment note, the Phase 1 step no longer says to register an org, and the "npm scope" risk is marked resolved. The Phase 0-4 plans underdocs/superpowers/plans/keep the old scope — they record what was executed at the time, not what ships.Prettier re-wrapped six markdown files whose tables and prose lines no longer fit the longer names (verified line-by-line: table alignment only, no code block touched).
Test plan (Phase 4.6)
rm -rf node_modules packages/*/build+pnpm install --frozen-lockfile— lockfile matches the renamed manifestspnpm run build— 4 packages cleanpnpm lintandpnpm exec prettier --check .— cleanpnpm test— 44 files / 584 tests, identical to the pre-rename baselinepnpm run test:esm— all 3 packagespnpm --filter examples run typecheckandpnpm --filter benchmarks run typecheck— cleanpnpm bench— full suite, all honesty guards passpnpm exec changeset status— all 4 packages resolvegrep -r "@om-data-mapper"— onlydocs/superpowers/plans/(historical) and the spec's amendment note, where it is named deliberately