Skip to content

πŸŽ™οΈ task - fix(deps): zod is a hard dep but leaks through the public type signature β€” pin-skew forces a 2nd ZodType identity and OOMs consumer tscΒ #25

Description

@uladkasach

πŸ¦«πŸŽ™οΈ dispatch to foreman

πŸ’§ task enqueued
   β”œβ”€ priority = ?
   β”œβ”€ yieldage = ?
   └─ leverage = ?

title
fix(deps): zod is a hard dep but leaks through the public type signature β€” pin-skew forces a 2nd ZodType identity and OOMs consumer tsc
description

fix(deps): zod is a hard dependency but leaks through the public type signature β€” a pin-skew forces a 2nd ZodType identity and OOMs consumer tsc

.what

sdk-aws-lambda declares zod as an exact hard dependency, while zod types appear
throughout its public .d.ts surface. any consumer that resolves a different zod version
gets a second ZodType identity, and tsc must reconcile two ~120k-line, near-identical
type graphs to infer TInput from genLambdaEndpoint. that reconciliation is super-linear and
exhausts the default ~4GB node heap.

ask: move zod to a peerDependency with a broad range (^4), keep an exact pin in
devDependencies for the sdk's own build.

.evidence β€” measured in ahbode/svc-notifications across the declapract upgrade

the pin, today (0.3.0)

// sdk-aws-lambda/package.json
"dependencies": {
  "zod": "4.4.3"        // exact, hard dep
}
// no peerDependencies entry for zod

zod leaks through the PUBLIC type surface

dist/domain.operations/genLambdaEndpoint/genLambdaEndpoint.forAskEndpoint.d.ts:4
  import type { ZodSchema } from 'zod';
dist/domain.operations/genLambdaEndpoint/genLambdaEndpoint.forApiGateway.d.ts:4
  import type { ZodSchema } from 'zod';
dist/domain.operations/genLambdaEndpoint/middleware/getValidatedOutput.d.ts:1
  import type { ZodType } from 'zod';
dist/domain.operations/genLambdaEndpoint/middleware/getValidationError.d.ts:2
  import type { ZodError } from 'zod';
dist/domain.objects/LambdaEndpointSchema.d.ts:1
  import type { JSONSchema } from 'zod/v4/core/json-schema';

ZodSchema sits directly in the signature that infers:

genLambdaEndpoint<TInput, TOutput>(input: { schema: { input: ZodSchema<TInput> } })

so TInput inference is exactly where the cross-copy reconciliation happens.

the skew is live in the ehmpathy tree right now

sdk-aws-lambda 0.3.0  dependencies.zod = "4.4.3"    ← hard dep
sdk-config     0.2.3  dependencies.zod = "4.3.6"    ← hard dep, DIFFERENT exact pin

two ehmpathy sdks, two exact hard pins, both leak zod types. a consumer that installs both gets
two copies unless it hand-writes an override. we measured a third copy (4.3.4) in the same
tree before we collapsed it.

the failure mode we measured

scope with 3 zod copies after collapse to one
one trivial genLambdaEndpoint call OOM at ~225s / 4GB 6.4s, 188MB, exit 0
full src build (tsconfig.build.json) OOM at ~760s / 4GB 22.95s, 738MB, exit 0
full test:types OOM exit 0

the OOM masks the real error, which is:

_zod.version.minor: Type '4' is not assignable to type '3'

the versions genuinely are incompatible identities; the OOM is tsc's effort to prove otherwise.

the discriminator that isolates the cause

explicit type args at the call site skip inference:

genLambdaEndpoint<unknown, unknown>({ ... })   // OOM vanishes, seconds not minutes

this proves 100% of the cost is TInput inference, which unifies two ZodType graphs β€” not
code volume, not any single complex type, not the return type.

.why the current shape is wrong

rule.require.pinned-versions is written for a consumer app β€” pin exactly, eliminate
surprise. a library that surfaces a foreign type in its own public signature is the opposite
case: an exact hard pin imposes a duplicate identity on every consumer that resolves any other
version. the ecosystem convention for exactly this case is a peer range.

the tree already demonstrates the correct shape β€” every package that peer-declares zod dedupes
cleanly:

sdk-aws-lambda 0.3.0  └── zod 4.4.3           ← hard dep (the hazard)
sdk-config     0.2.3  └── zod 4.3.6           ← hard dep (the hazard)
rhachet        1.44.4 └── zod 4.4.3 peer      ← correct
openai         5.8.2  └── zod 4.4.3 peer      ← correct
@anthropic-ai/sdk     └── zod 4.4.3 peer      ← correct

.the ask

{
  "peerDependencies": { "zod": "^4" },
  "devDependencies":  { "zod": "4.4.3" }
}
  • one identity across the whole tree; consumers no longer need pnpm.overrides
  • a real zod-4 major break still surfaces at install as a peer-range violation, loud and early
  • consider peerDependenciesMeta if any zod-bound export should stay optional

domain-objects deserves the same audit β€” it also leaks types and is pinned inconsistently
(0.33.0 in sdk-aws-lambda vs 0.31.9 in sdk-config).

.scope note

this is a twin defect to sdk-config's identical shape (dispatched separately). the general
rule both sdks drifted past:

a package that surfaces a third-party type in its public .d.ts must declare that package as
a peer with a broad range β€” never as an exact hard dependency.

that rule is written down nowhere today, which is why both sdks reached the same shape
independently.

.workaround in place downstream (not a fix)

// ahbode/svc-notifications/package.json
"pnpm": { "overrides": { "zod": "4.4.3" } }

every consumer has to rediscover this. the fix belongs upstream.

.refs

  • durable lesson (written across this migration):
    ahbode/svc-notifications:.agent/repo=.this/role=any/briefs/lesson.tsc-oom-from-multi-version-schema-lib.md
  • full bisection audit trail:
    ahbode/svc-notifications:.route/v2026_07_28.declapract.upgrade/PERF.tsc-inference-fix.ledger.md
  • a prior instance of the same single-identity class used joi instead of zod β€” it recurs per
    schema library until the peer-range convention is adopted

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions