Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/patterns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ See {@link PatternMatchers} for more on `M.splitRecord()`, `M.number()`, and oth

`M` also has {@link GuardMakers} methods to make {@link InterfaceGuard}s that use Patterns to characterize dynamic behavior such as method argument/response signatures and promise awaiting. The {@link @endo/exo!} package uses `InterfaceGuard`s as the first level of defense for Exo objects against malformed input.

If you need to translate between Endo Patterns, Zod schemas, and TypeScript declarations, start with the [Rosetta guide](./docs/rosetta/index.md). It collects executable examples together with notes about fidelity gaps and recommended work-arounds.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest making Zod a link. Also would be handy to call-out which Rosetta this is referring to.


_For best rendering, use the [Endo reference docs](https://endojs.github.io/endo) site._

## Key Equality, Containers
Expand Down
76 changes: 76 additions & 0 deletions packages/patterns/docs/rosetta/examples/advanced.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Far } from '@endo/marshal';
import { passStyleOf } from '@endo/pass-style';
import {
M,
matches,
mustMatch,
} from '../../../src/patterns/patternMatchers.js';
import { makeCopySet } from '../../../src/keys/checkKey.js';

export const featureFlagsPattern = M.setOf(M.string());

export const featureFlagsSpecimen = makeCopySet(['alpha', 'beta']);

export const featureFlagsInvalid = harden(['alpha', 'alpha']);

export const makeFeatureFlagsZodSchema = z =>
z.array(z.string()).superRefine((value, ctx) => {
const seen = new Set();
for (const entry of value) {
if (seen.has(entry)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Feature flags must be unique',
});
return;
}
seen.add(entry);
}
});

export const remotablePattern = M.remotable('FlagService');

export const remoteServiceSpecimen = Far('FlagService', {
async getFlag(name) {
return name === 'alpha';
},
});

export const remoteServiceInvalid = harden({
async getFlag(name) {
return name === 'alpha';
},
});

export const makeRemoteServiceZodSchema = z =>
z.object({
getFlag: z
.function()
.args(z.string())
.returns(z.union([z.boolean(), z.promise(z.boolean())])),
});

export const promisePattern = M.promise();

export const promiseSpecimen = harden(Promise.resolve('ok'));

export const promiseInvalid = harden({ then: () => undefined });

export const makePromiseZodSchema = z =>
z.custom(value => value instanceof Promise, {
message: 'Must be a real Promise',
});

export const validateFeatureFlagsWithPattern = specimen =>
mustMatch(specimen, featureFlagsPattern);

export const validateRemotableWithPattern = specimen =>
mustMatch(specimen, remotablePattern);

export const validatePromiseWithPattern = specimen =>
mustMatch(specimen, promisePattern);

export const describePassStyle = specimen => passStyleOf(specimen);

export const matchesPromisePattern = specimen =>
matches(specimen, promisePattern);
20 changes: 20 additions & 0 deletions packages/patterns/docs/rosetta/examples/advanced.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod';

Check failure on line 1 in packages/patterns/docs/rosetta/examples/advanced.ts

View workflow job for this annotation

GitHub Actions / lint

'zod' should be listed in the project's dependencies, not devDependencies
import {
makeFeatureFlagsZodSchema,
makePromiseZodSchema,
makeRemoteServiceZodSchema,
} from './advanced.js';

export const featureFlagsSchema = makeFeatureFlagsZodSchema(z);

export type FeatureFlags = readonly string[];

export const remoteServiceSchema = makeRemoteServiceZodSchema(z);

export interface RemoteService {
getFlag(name: string): boolean | Promise<boolean>;
}

export const promiseSchema = makePromiseZodSchema(z);

export type KnownPromise = Promise<unknown>;
48 changes: 48 additions & 0 deletions packages/patterns/docs/rosetta/examples/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
M,
matches,
mustMatch,
} from '../../../src/patterns/patternMatchers.js';

/**
* Pattern describing a simple user record. Required `id` and `handle`, optional
* `email` and `note`. `id` uses `M.nat()` so the value must be a non-negative
* bigint.
*/
export const userProfilePattern = M.splitRecord(
{ id: M.nat(), handle: M.string() },
{ email: M.string(), note: M.string() },
);

export const goodUserProfile = harden({
id: 42n,
handle: 'querycat',
email: '[email protected]',
});

export const badUserProfile = harden({
id: -3n,
handle: 'querycat',
});

export const makeUserProfileZodSchema = z =>
z
.object({
id: z.bigint().refine(value => value >= 0n, {
message: 'id must be a non-negative bigint',
}),
handle: z.string(),
})
.extend({
email: z.string().email().optional(),
note: z.string().optional(),
});

export const validateWithPattern = specimen =>
mustMatch(specimen, userProfilePattern);

export const matchesWithPattern = specimen =>
matches(specimen, userProfilePattern);

export const explainZodFailure = (schema, specimen) =>
schema.safeParse(specimen).error;
18 changes: 18 additions & 0 deletions packages/patterns/docs/rosetta/examples/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from 'zod';

Check failure on line 1 in packages/patterns/docs/rosetta/examples/basic.ts

View workflow job for this annotation

GitHub Actions / lint

'zod' should be listed in the project's dependencies, not devDependencies
import { mustMatch } from '../../../src/patterns/patternMatchers.js';
import { makeUserProfileZodSchema, userProfilePattern } from './basic.js';

export const userProfileSchema = makeUserProfileZodSchema(z);

export type UserProfile = {
id: bigint;
handle: string;
email?: string;
note?: string;
};

export const assertUserProfile = (value: unknown): UserProfile => {
const runtimeChecked = userProfileSchema.parse(value);
mustMatch(runtimeChecked, userProfilePattern);
return runtimeChecked;
};
54 changes: 54 additions & 0 deletions packages/patterns/docs/rosetta/gaps-and-workarounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Gaps and Work-arounds

Some Endo Pattern features have no direct equivalent in Zod or TypeScript. This document highlights the most important gaps and suggests practical mitigations. See [`examples/advanced.ts`](./examples/advanced.ts) for code that exercises each scenario, together with [`../../test/rosetta/examples.test.js`](../../test/rosetta/examples.test.js).

## Pass Styles

Endo Patterns rely on pass-style metadata (e.g., `remotable`, `promise`) to differentiate between values that would otherwise look identical to structured-clone-based checkers. Zod and TypeScript do not carry pass-style information. Recommended practice:

- Use `M.remotable()` / `M.interface()` for runtime enforcement.
- Document the remotable contract and expose TypeScript interfaces for call signatures.
- In Zod, combine `z.any()` with `.refine(isRemotable, 'remotable expected')` where `isRemotable` delegates to `@endo/pass-style` checks.

## Copy Collections

Zod does not currently expose dedicated schema helpers for CopySet, CopyBag, or CopyMap. Suggested work-arounds:

- CopySet: validate with `z.array(schema)` and `.superRefine()` to ensure uniqueness, then coerce to `CopySet` in application code.
- CopyBag: validate with `z.array(z.tuple([schema, z.bigint().nonnegative()]))`.
- CopyMap: use `z.array(z.tuple([keySchema, valueSchema]))` when ordered entries are acceptable, or a custom validator wrapping `M.copyMapOf()`.

TypeScript can express these shapes using readonly arrays of tuples. The runtime guard remains authoritative.

## Promise Turnstyle

`M.promise()` distinguishes fulfilled vs. unresolved promises via pass-style, but Zod only sees "any object with a `then` method". When you need promise-aware checks:

1. Use Endo Pattern validation first.
2. In Zod, wrap the value in `z.custom()` and delegate to `passStyleOf(value) === 'promise'`.
3. Re-document that TypeScript types (`PromiseLike<unknown>`) do not catch handled vs unhandled promise differences.

## Branded Naturals

`M.nat()` guarantees non-negative bigints. TypeScript cannot encode this constraint. Introduce a branded type:

```ts
// docs/rosetta/examples/nat-brand.ts (conceptual)
export type Nat = bigint & { readonly __kind: 'Nat' };
```

Provide a guard function that runs `mustMatch(n, M.nat())` before returning `n as Nat`.

## Interface Guards

Endo InterfaceGuards (e.g., `M.interface`) validate method availability, argument patterns, and eventual-send behaviour. Zod has no equivalent concept. Model your service API as a TypeScript interface and use the guard to ensure remote callers respect the contract at runtime.

## SES & Hardened Data

Endo assumes harden()ed pass-by-copy data. Zod cannot enforce hardening. For cross-tooling compatibility:

- Harden specimens before validation in example code.
- In Zod, use `.transform(value => harden(value))` when safe.
- Document that TypeScript types describe structure only; the caller must still freeze the data.

These patterns give authors the vocabulary to explain when conversions stop being lossless. LLMs can rely on this page to decide whether to fall back to higher-level guards, branded types, or documentation notes.
16 changes: 16 additions & 0 deletions packages/patterns/docs/rosetta/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Rosetta Guide: Endo Patterns, Zod, and TypeScript

This guide collects idiomatic translations between [`@endo/patterns`](../..), [Zod](https://github.com/colinhacks/zod), and TypeScript type declarations. It is designed for humans and LLMs that need to understand how a data contract expressed in one system maps onto the others, what fidelity gaps exist, and which work-arounds are recommended.

The bundle is organised as follows:

- [`patterns-to-zod.md`](./patterns-to-zod.md) — start with an Endo Pattern and look up the closest Zod schema.
- [`zod-to-patterns.md`](./zod-to-patterns.md) — start with Zod and find the Endo Pattern (or guard) that delivers comparable runtime guarantees.
- [`patterns-to-typescript.md`](./patterns-to-typescript.md) — understand which static TypeScript declarations correspond to a given Pattern.
- [`typescript-to-patterns.md`](./typescript-to-patterns.md) — start from a TypeScript type and locate the appropriate Endo Pattern or guard.
- [`gaps-and-workarounds.md`](./gaps-and-workarounds.md) — catalogue of features that do not translate cleanly together with practical mitigations.
- [`examples/`](./examples) — executable snippets that the tests exercise to make sure the documentation stays in sync.

The examples demonstrate both successful and unsuccessful conversions. The tests in [`../../test/rosetta/examples.test.js`](../../test/rosetta/examples.test.js) exercise these snippets. They prove the conversions that work today and highlight the known mismatches when a translation is not possible.

> **Note:** Zod is an optional development dependency. Install it within this repository (`yarn workspace @endo/patterns add --dev zod`) before running the Rosetta tests locally.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this somehow more optional than other devDependencies in package.json? For other devDependencies, I just depend on normal yarn && yarn build to install so I can use them for local development. What makes this one different.

(I'm not opposed to this one being more optional. I just don't understand what the mechanism is.)

27 changes: 27 additions & 0 deletions packages/patterns/docs/rosetta/patterns-to-typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Relating Endo Patterns to TypeScript Types
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these relate to the types you already infer from patterns and guards?


TypeScript captures static structure while Endo Patterns provide runtime validation. This document shows how to author a type declaration that matches a given Pattern. The examples reference [`examples/basic.ts`](./examples/basic.ts) and [`examples/advanced.ts`](./examples/advanced.ts).

| Pattern | Suggested TypeScript | Notes |
| --- | --- | --- |
| `M.string()` | `type T = string;` | Strings translate directly. |
| `M.number()` | `type T = number;` | TypeScript cannot exclude `NaN` or `Infinity` at the type level; rely on runtime validation. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M.number() does no range checking. Did you mean

Suggested change
| `M.number()` | `type T = number;` | TypeScript cannot exclude `NaN` or `Infinity` at the type level; rely on runtime validation. |
| `M.number()` | `type T = number;` | |
| `M.nat()` | `type T = number;` | TypeScript cannot exclude `NaN` or `Infinity` at the type level; rely on runtime validation. |

| `M.bigint()` | `type T = bigint;` | |
| `M.splitRecord({ id: M.nat() }, { note: M.string() })` | ```ts
interface RecordShape {
id: bigint;
note?: string;
}
Comment on lines +10 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how markdown tables relate to markdown triple backtick. But this is not rendering correctly.

Ignoring the backtick issue I don't know how to solve, you could also extend this in place, or perhaps include as a distinct line, for some suitable T

Suggested change
| `M.splitRecord({ id: M.nat() }, { note: M.string() })` | ```ts
interface RecordShape {
id: bigint;
note?: string;
}
| `M.splitRecord({ id: M.nat() }, { note: M.string() }, T)` | ```ts
interface RecordShape {
id: bigint;
note?: string;
...T;
}

``` | TypeScript `bigint` does not encode non-negativity. Convey the constraint in docs or branded types. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue. M.bigint() also does not encode non-negativity

| `M.arrayOf(M.string())` | `type T = string[];` | Combine with tuple types if `arrayLengthLimit` is finite. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "Combine with tuple types" mean here?

| `M.arrayOf(M.string(), harden({ arrayLengthLimit: 2 }))` | `type T = readonly [string?, string?];` | Endo can enforce maximum length at runtime. A tuple with optional slots expresses the same upper bound statically. |
| `M.setOf(M.string())` | `type T = ReadonlyArray<string>;` | TypeScript lacks a native `CopySet`. Express as `ReadonlyArray` with documentation noting deduplication. |
| `M.bagOf(M.string())` | `type T = ReadonlyArray<[string, bigint]>;` | A bag is best expressed as entries with counts. |
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For setOf, bagOf, and the missing mapOf, typescript can type the enclosing envelope record, including the literal tag value ('set', 'bag', or 'map'). In fact, I thought you already created the typescript types for these.

| `M.promise()` | `type T = Promise<unknown>;` | Add generics for resolved types if known; runtime checks must ensure the promise is handled according to Endo’s pass-style rules. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why statements about runtime checks are relevant to patterns/guard and to zod. But what does this mean as applied to TypeScript?

| `M.interface({ getValue: M.callWhen([M.number()], M.number()) })` | ```ts
interface ValueService {
getValue(input: number): number;
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `M.interface({ getValue: M.callWhen([M.number()], M.number()) })` | ```ts
interface ValueService {
getValue(input: number): number;
| `M.interface({ getValue: M.callWhen([M.number()], M.number()) })` | ```ts
interface ValueService {
async getValue(input: number): number;

A callWhen corresponds to an async method declaration.

}
``` | TypeScript describes the shape but cannot enforce async/await behaviour. The Endo guard still provides runtime enforcement. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, what does it mean to even talk about enforcement when talking about TypeScript (as opposed to patterns/guards and to zod)?

M.await(M.number()) still has the typing issue that the external type should be Promise<number> while the internal type should be number

When a runtime constraint has no native static representation, consider branded types: e.g., `type Nat = bigint & { readonly brand: unique symbol; }`. Guard constructors can cast the value after validation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this so a non-TS expert like me can understand this?

28 changes: 28 additions & 0 deletions packages/patterns/docs/rosetta/patterns-to-zod.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Mapping Endo Patterns to Zod

This table-driven guide focuses on starting from an Endo Pattern expressed with the `M` helper and finding the most faithful Zod schema. Each entry contains three parts:

1. The Endo Pattern
2. The closest Zod schema (when one exists)
3. Notes describing the quality of the mapping

| Endo Pattern | Zod Schema | Notes |
| --- | --- | --- |
| `M.string()` | `z.string()` | 1:1 translation. Example in [`examples/basic.ts`](./examples/basic.ts). |
| `M.number()` | `z.number()` | 1:1 translation for finite numbers. Zod allows `NaN` by default; use `.finite()` to align with Endo which rejects `NaN` and infinities. |
| `M.bigint()` | `z.bigint()` | 1:1 translation. |
| `M.boolean()` | `z.boolean()` | 1:1 translation. |
| `M.undefined()` | `z.undefined()` | Exact match. |
| `M.null()` | `z.null()` | Exact match. |
| `M.arrayOf(M.string())` | `z.array(z.string())` | Exact match when Element pattern maps cleanly. |
| `M.array(harden({ arrayLengthLimit: 2 }))` | `z.array(z.any()).max(2)` | Zod has upper/lower bounds. Endo can express additional constraints (e.g., pass-by-copy) that Zod cannot. |
| `M.splitRecord({ id: M.nat() }, { note: M.string() })` | `z.object({ id: z.bigint().refine((value) => value >= 0n, 'non-negative bigint') }).extend({ note: z.string().optional() })` | Use `.refine` because BigInt schemas lack `.nonnegative()`. |
| `M.mapOf(M.string(), M.number())` | `z.map(z.string(), z.number())` | Zod only ensures entry types; it does not enforce pass-by-copy vs remotable keys. |
| `M.setOf(M.string())` | *(no direct Zod schema)* | Zod lacks passable `CopySet`. Use `z.array(schema).transform(...)` with deduplication. See [`examples/advanced.ts`](./examples/advanced.ts) and [`gaps-and-workarounds.md`](./gaps-and-workarounds.md#copy-collections). |
| `M.remotable('FlagService')` | `z.object({ getFlag: z.function().args(z.string()).returns(z.union([z.boolean(), z.promise(z.boolean())])) })` | Zod can require a callable property but cannot enforce remotable pass-style. |
| `M.promise()` | `z.custom((value) => value instanceof Promise)` | Zod has no built-in promise schema. A custom refinement approximates the check but loses pass-style information. |
| `M.or(M.string(), M.number())` | `z.union([z.string(), z.number()])` | 1:1 translation. |
| `M.and(M.pattern(), M.array())` | `z.array(z.any()).superRefine(...)` | Zod lacks an intersection helper with structural constraints. A `.superRefine` step can simulate additional predicates. |
| `M.interface('ValueService', { getValue: M.callWhen(M.number()).returns(M.number()) })` | *(no direct Zod schema)* | InterfaceGuards validate remotable behaviour; Zod remains focused on structural data. Pair with documentation or higher-level guards. |

See [`examples/basic.ts`](./examples/basic.ts) and [`examples/advanced.ts`](./examples/advanced.ts) for runnable conversions. The Ava test validates the entries labelled “1:1 translation” and demonstrates the gaps for non-expressible cases.
33 changes: 33 additions & 0 deletions packages/patterns/docs/rosetta/typescript-to-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Mapping TypeScript Declarations to Endo Patterns

When you begin with a TypeScript type and need the corresponding runtime guard, use this table to find the closest Endo Pattern (or guard) and any extra checks required. Examples come from [`examples/basic.ts`](./examples/basic.ts) and [`examples/advanced.ts`](./examples/advanced.ts).

| TypeScript | Endo Pattern | Notes |
| --- | --- | --- |
| `type Name = string;` | `M.string()` | Direct translation. |
| `type Count = number;` | `M.number()` | Add runtime checks to reject `NaN`/`Infinity`. |
| `type Amount = bigint;` | `M.bigint()` | |
| ```ts
interface UserProfile {
id: bigint;
handle: string;
email?: string;
note?: string;
}
``` | `M.splitRecord({ id: M.nat(), handle: M.string() }, { email: M.string(), note: M.string() })` | Endo enforces the `M.nat()` constraint and forbids undeclared properties. |
| `type Tags = string[];` | `M.arrayOf(M.string())` | Harden arrays before validation to meet pass-by-copy expectations. |
| `type Pair = readonly [string, number];` | `harden([M.string(), M.number()])` | Tuples become hardened arrays of element patterns. |
| ```ts
interface FeatureFlags {
readonly flags: readonly string[];
}
``` | `M.splitRecord({ flags: M.arrayOf(M.string()) })` | To enforce uniqueness, compose with `mustMatch` and a custom predicate or use `M.and`. |
| ```ts
interface RemoteService {
getFlag(name: string): boolean | Promise<boolean>;
}
``` | `M.interface('FlagService', harden({ getFlag: M.callWhen(M.string()).returns(M.boolean()) }))` | InterfaceGuards ensure remotable behaviour; TypeScript cannot express pass-style. |
| `type KnownPromise<T> = Promise<T>;` | `M.promise()` | Distinguish from plain thenables; Endo checks pass-style. |
| `type Nat = bigint & { readonly __brand: 'Nat' };` | Guard constructor: `spec => (mustMatch(spec, M.nat()), spec as Nat)` | Branded types need runtime validation to enforce structural promises. |

If a TypeScript type uses index signatures (`Record<string, T>`) or structural openness (e.g., `interface Foo { [key: string]: string }`), prefer translating it to CopyMap patterns (`M.mapOf`) or document the mismatch explicitly. See [`gaps-and-workarounds.md`](./gaps-and-workarounds.md) for more guidance.
20 changes: 20 additions & 0 deletions packages/patterns/docs/rosetta/zod-to-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Mapping Zod Schemas to Endo Patterns

When you begin from Zod, the goal is to locate the closest Endo Pattern and understand whether the conversion is exact or if additional guards are required. This guide mirrors [`patterns-to-zod.md`](./patterns-to-zod.md) but starts from the Zod surface area most commonly produced by LLMs.

| Zod Schema | Endo Pattern | Conversion Quality | Notes |
| --- | --- | --- | --- |
| `z.string()` | `M.string()` | Exact | Both enforce JavaScript strings. |
| `z.number().finite()` | `M.number()` | Exact | Endo rejects `NaN` and infinite values, so `.finite()` is required on the Zod side to avoid mismatches. |
| `z.bigint()` | `M.bigint()` | Exact | |
| `z.boolean()` | `M.boolean()` | Exact | |
| `z.literal('ready')` | `'ready'` | Exact | Literal patterns are the literal passable themselves. |
| `z.union([z.string(), z.number()])` | `M.or(M.string(), M.number())` | Exact | Endo preserves tagged error messages for each option. |
| `z.intersection(z.object({ id: z.string() }), z.object({ label: z.string() }))` | `M.splitRecord({ id: M.string(), label: M.string() })` | Better | Intersections of `z.object` can be represented with `M.splitRecord`; keep optional properties in the second argument. |
| `z.array(z.string()).min(1).max(3)` | `M.arrayOf(M.string(), harden({ arrayLengthLimit: 3 }))` + manual length check | Partial | Endo exposes upper limit, but `.min()` requires an extra `specimen.length >= 1` assertion before calling `mustMatch`. |
| `z.map(z.string(), z.number())` | `M.mapOf(M.string(), M.number())` | Partial | Endo enforces copy-map semantics; Zod only inspects entries. |
| `z.record(z.string())` | `M.splitRecord({}, { [M.symbol()] : ??? })` | Not directly expressible | Endo Patterns require property names to be known or pattern-matched via `M.guard`. Prefer rephrasing with `CopyMap`. |
| `z.object({ meta: z.any().optional() }).passthrough()` | *(no Endo Pattern)* | Not supported | Endo forbids extra properties unless they are described. Use `M.splitRecord` and explicit optional entries. |
| `z.custom((value) => value instanceof Promise, { message: 'Promise' })` | `M.promise()` | Partial | Endo distinguishes handled/unhandled Promise pass-styles. Zod custom checks cannot reach pass-style metadata. |

For Zod declarations with no Endo equivalent, see [`gaps-and-workarounds.md`](./gaps-and-workarounds.md) for defensive fallback strategies.
Loading
Loading