فهرست منبع

perf(typert): materialize generated schemas on first use

imccyu 4 روز پیش
والد
کامیت
e459e32637
38فایلهای تغییر یافته به همراه205 افزوده شده و 162 حذف شده
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.i18n.yaml
  2. 3 3
      .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md
  3. 3 3
      .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md
  7. 2 2
      docs/subsystems/typert.i18n.yaml
  8. 6 5
      docs/subsystems/typert.md
  9. 6 5
      docs/subsystems/typert.zh.md
  10. 1 1
      packages/api/gateway/src/client/index.ts
  11. 1 1
      packages/api/gateway/src/index.ts
  12. 3 3
      packages/api/gateway/tests/gateway-stream.host.spec.ts
  13. 12 12
      packages/api/gateway/tests/gateway.client.spec.ts
  14. 1 1
      packages/api/gateway/tests/gateway.host.spec.ts
  15. 14 6
      packages/extensions/tool-cordis/src/api-catalog.ts
  16. 2 2
      packages/typert/generator/README.i18n.yaml
  17. 2 2
      packages/typert/generator/README.md
  18. 2 2
      packages/typert/generator/README.zh.md
  19. 7 6
      packages/typert/generator/src/emitter.ts
  20. 5 4
      packages/typert/generator/tests/__snapshots__/type-model.spec.ts.snap
  21. 15 15
      packages/typert/generator/tests/remote-model.spec.ts
  22. 2 2
      packages/typert/generator/tests/schema-emitter.spec.ts
  23. 7 6
      packages/typert/generator/tests/type-model.spec.ts
  24. 2 2
      packages/typert/loader/README.i18n.yaml
  25. 3 3
      packages/typert/loader/README.md
  26. 3 3
      packages/typert/loader/README.zh.md
  27. 4 7
      packages/typert/loader/src/index.ts
  28. 17 17
      packages/typert/loader/tests/loader.spec.ts
  29. 2 2
      packages/typert/protocol/README.i18n.yaml
  30. 1 1
      packages/typert/protocol/README.md
  31. 1 1
      packages/typert/protocol/README.zh.md
  32. 2 1
      packages/typert/protocol/src/types.ts
  33. 2 2
      packages/typert/registry/README.i18n.yaml
  34. 4 4
      packages/typert/registry/README.md
  35. 4 4
      packages/typert/registry/README.zh.md
  36. 34 11
      packages/typert/registry/src/service.ts
  37. 8 5
      packages/typert/registry/src/types.ts
  38. 18 12
      packages/typert/registry/tests/typert.spec.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md
-2026-07-27-compiler-independent-typert-model.md: c9e7ec0e471c9deabcffda69346077a2a10ac42a
-2026-07-27-compiler-independent-typert-model.zh.md: 9bd419f39c9d8a490dfb0baf12a9852fdbb817a8
+2026-07-27-compiler-independent-typert-model.md: 5083c55176b2f4d342d14d0c1cf2a7f0c2c14b3c
+2026-07-27-compiler-independent-typert-model.zh.md: 70d6954fae0fcb48e76137231168d5f2417668bf

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md

@@ -20,7 +20,7 @@ Each face independently owns a PackageModel and TypeGraph. Direct project refere
 
 PackageModel recognizes Cordis services, events, `@typert object` reference objects, and `@typert schema` data roots. Services and objects expose only public instance members, excluding constructors and static, private, and protected members; inheritance edges remain in TypeGraph instead of being copied into flattened members. When a public property, parameter, or return type lacks an annotation, `check` mode reports an error, while `write` mode writes the checker-inferred result, rebuilds the project, and analyzes it again in strict mode.
 
-[`dsh-typert-registry`](../../../../packages/typert/registry/README.md) provides `ctx.typert` and handles runtime registration only: one contribution atomically carries package-face reflection and an optional Zod schema, and Cordis effect disposal revokes it. The registry neither analyzes TypeScript nor merges the two faces. JSON Schema is an on-demand projection of registered Zod schemas.
+[`dsh-typert-registry`](../../../../packages/typert/registry/README.md) provides `ctx.typert` and handles runtime registration only: one contribution atomically carries package-face reflection and optional Zod schema factories, and Cordis effect disposal revokes it. The registry validates factories without invoking them, then materializes and caches each schema on its first `get()`, `resolve()`, `list()`, or JSON Schema projection. It neither analyzes TypeScript nor merges the two faces.
 
 Package artifact publication remains explicit opt-in through package exports. When invoked, `WorkspaceTypertGenerator` validates that each requested host face exposes the user-facing subpath `package/typert` from the root artifact `package/lib/typert.host.{js,d.ts}`, or that each requested client face exposes `package/client/typert` from `package/lib/typert.client.{js,d.ts}`; it never edits those exports. The later [Typert Remote design](2026-08-02-typert-remote-method-calls.md) adds a whole-workspace Host contract pass to root build, typecheck, lint, and documentation typecheck. For opted-in Host packages, that pass emits both local reflection and strict Host-for-Client `/remote` contracts before consumers resolve them. Generated local declarations keep `TYPERT` typed as `unknown`, so business packages do not depend on the registry.
 
@@ -34,7 +34,7 @@ For every property in `SyntaxZoo`, the TypeScript printer normalizes the source
 
 Boundary cases pin explicit package imports within and across faces, cross-face named re-exports, exact export aliases, qualified `import()` links, and the External classification of global `@types` declarations; they reject TypeScript diagnostics originating in package-owned files, relative-path boundary crossings, references outside `package.json#exports`, and cross-face namespace re-exports without a model target. Interface declaration merging explicitly preserves every authored part; other merges that cannot be represented losslessly fail.
 
-For each supported node kind and literal category, Zod emitter tests run both successful and failing parses; for each unsupported kind, they assert an explicit `TypertEmitError`. Emitter fixtures snapshot generated Zod JavaScript and `.d.ts` text, execute the JavaScript, and typecheck the declarations. `dsh-typert-registry` tests pin atomic registration, queries, JSON Schema, and effect disposal; `dsh-typert-loader` tests also prove delayed mounting, unloading, and disposal while a dynamic import remains pending. A real `dsh-tools` vertical slice generates a contribution from the model, loads it through the runtime registry, and compares its service, event, and related-type records with the committed static `SERVICE_API`, `EVENT_API`, and `TYPE_API`. A full-workspace projector test regenerates the two Cordis catalog documents and the `tool-cordis` API catalog and requires all three texts to be byte-for-byte identical to the committed artifacts.
+For each supported node kind and literal category, Zod emitter tests run both successful and failing parses; for each unsupported kind, they assert an explicit `TypertEmitError`. Emitter fixtures snapshot generated Zod JavaScript and `.d.ts` text, execute each schema factory, and typecheck the declarations. `dsh-typert-registry` tests pin atomic registration, first-use materialization, successful-result caching, retry after factory failure, queries, JSON Schema, and effect disposal; `dsh-typert-loader` tests also prove delayed mounting, unloading, and disposal while a dynamic import remains pending. A real `dsh-tools` vertical slice generates a contribution from the model, loads it through the runtime registry, and compares its service, event, and related-type records with the committed static `SERVICE_API`, `EVENT_API`, and `TYPE_API`. A full-workspace projector test regenerates the two Cordis catalog documents and the `tool-cordis` API catalog and requires all three texts to be byte-for-byte identical to the committed artifacts.
 
 ## Alternatives considered
 
@@ -50,4 +50,4 @@ For each supported node kind and literal category, Zod emitter tests run both su
 
 New generation targets and static checks can reuse the same TypeGraph, and business categories can extend PackageModel without parsing the AST again. Preserving pre-evaluation types and independent faces makes the model more complex than a flattened schema; emitters must explicitly declare their supported scope and fail on missing capabilities.
 
-Explicit package opt-in keeps artifact publication and exports under package ownership. Repository orchestration may still run the whole-workspace Host contract pass for every opted-in package; that pass remains owned by the later Remote Gateway Agent Note. The static Cordis catalogs remain reproducible from the canonical model without coupling `tool-cordis` to runtime registry state. `ctx.typert` reflects only artifacts mounted in the current runtime, and unloading does not control Zod instances that consumers retain after importing them directly.
+Explicit package opt-in keeps artifact publication and exports under package ownership. Repository orchestration may still run the whole-workspace Host contract pass for every opted-in package; that pass remains owned by the later Remote Gateway Agent Note. The static Cordis catalogs remain reproducible from the canonical model without coupling `tool-cordis` to runtime registry state. `ctx.typert` reflects only artifacts mounted in the current runtime, and unloading does not control Zod instances that consumers retain after materializing them.

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md

@@ -20,7 +20,7 @@ TypeGraph 保存开发者写下的计算前类型结构,包括泛型参数与
 
 PackageModel 识别 Cordis service、event、`@typert object` 引用对象和 `@typert schema` 数据根。service 与 object 只暴露 public instance member,排除 constructor、static、private 和 protected;继承边保留在 TypeGraph 中,不复制为扁平成员。缺少 public property、parameter 或 return 类型标注时,`check` 模式报错,`write` 模式写入 checker 推断结果后重建 project 并再次以严格模式分析。
 
-[`dsh-typert-registry`](../../../../packages/typert/registry/README.zh.md) 提供 `ctx.typert`,且只负责运行时注册:一个 contribution 原子携带 package-face reflection 与可选 Zod schema,并随 Cordis effect 撤销。注册表不分析 TypeScript,也不合并两个 face。JSON Schema 是对已注册 Zod schema 的按需投影
+[`dsh-typert-registry`](../../../../packages/typert/registry/README.zh.md) 提供 `ctx.typert`,且只负责运行时注册:一个 contribution 原子携带 package-face reflection 与可选 Zod schema factory,并随 Cordis effect 撤销。注册表校验 factory 时不会调用它;首次 `get()`、`resolve()`、`list()` 或 JSON Schema 投影才会物化并缓存各 schema。注册表不分析 TypeScript,也不合并两个 face
 
 包产物发布仍通过 package exports 采用显式 opt-in。`WorkspaceTypertGenerator` 仅在被调用时校验所请求 face 的根目录产物协议:host face 必须通过面向用户的 subpath `package/typert` 暴露 `package/lib/typert.host.{js,d.ts}`,client face 必须通过 `package/client/typert` 暴露 `package/lib/typert.client.{js,d.ts}`;它不会修改这些 exports。后续的 [Typert Remote 设计](2026-08-02-typert-remote-method-calls.zh.md) 为根目录 build、typecheck、lint 与文档类型检查增加了全仓 Host 约定 pass。对于已 opt-in 的 Host 包,该 pass 会在消费方解析两者之前生成本地反射产物与严格的 Host-for-Client `/remote` 约定。生成的本地声明将 `TYPERT` 类型保持为 `unknown`,因此业务包不依赖注册表。
 
@@ -34,7 +34,7 @@ PackageModel 识别 Cordis service、event、`@typert object` 引用对象和 `@
 
 边界用例固定同 face 与跨 face 的显式包导入、跨 face 命名 re-export、精确 export alias、qualified `import()` link 和全局 `@types` External 归属,并拒绝 package 自有 TypeScript 诊断、相对路径越界、`package.json#exports` 之外的引用,以及尚无模型 target 的跨 face namespace re-export。interface declaration merging 显式保留每个 authored part,无法无损表示的其他 merge 失败。
 
-Zod emitter 对支持的节点和各类 literal 逐类执行成功与失败 parse,对不支持的节点逐类断言明确的 `TypertEmitError`。Emitter fixture 对生成的 Zod JavaScript 与 `.d.ts` 文本做快照,执行 JavaScript,并对声明做类型检查。`dsh-typert-registry` 测试固定原子注册、查询、JSON Schema 和 effect 撤销,`dsh-typert-loader` 测试还证明延迟挂载、卸载及未完成 dynamic import 的释放行为。真实 `dsh-tools` 纵切从模型生成 contribution,经运行时注册表加载后,将其服务、事件与关联类型记录同已提交的静态 `SERVICE_API`、`EVENT_API` 和 `TYPE_API` 对照。全仓 projector 测试重新生成两份 Cordis catalog 文档与 `tool-cordis` API catalog,并要求三份文本同已提交产物逐字节一致。
+Zod emitter 对支持的节点和各类 literal 逐类执行成功与失败 parse,对不支持的节点逐类断言明确的 `TypertEmitError`。Emitter fixture 对生成的 Zod JavaScript 与 `.d.ts` 文本做快照,执行每个 schema factory,并对声明做类型检查。`dsh-typert-registry` 测试固定原子注册、首次使用物化、成功结果缓存、factory 失败后重试、查询、JSON Schema 和 effect 撤销,`dsh-typert-loader` 测试还证明延迟挂载、卸载及未完成 dynamic import 的释放行为。真实 `dsh-tools` 纵切从模型生成 contribution,经运行时注册表加载后,将其服务、事件与关联类型记录同已提交的静态 `SERVICE_API`、`EVENT_API` 和 `TYPE_API` 对照。全仓 projector 测试重新生成两份 Cordis catalog 文档与 `tool-cordis` API catalog,并要求三份文本同已提交产物逐字节一致。
 
 ## Alternatives considered
 
@@ -50,4 +50,4 @@ Zod emitter 对支持的节点和各类 literal 逐类执行成功与失败 pars
 
 新增生成目标或静态检查可复用同一 TypeGraph,业务类目也可在 PackageModel 上扩展,而无需再次解析 AST。保留计算前类型和独立 face 的代价是模型比打平后的 schema 更复杂,emitter 必须显式声明支持范围并对缺失能力失败。
 
-包级显式 opt-in 使产物发布与 exports 由各包自行管理。仓库编排仍可为每个已 opt-in 的包运行全仓 Host 约定 pass;该 pass 仍由后续 Remote Gateway Agent Note 负责说明。静态 Cordis catalog 可从标准模型复现,同时不把 `tool-cordis` 与运行时注册表状态耦合。`ctx.typert` 只反映当前运行时中已挂载的产物;对于消费方直接导入后仍持有的 Zod 实例,卸载流程无法控制。
+包级显式 opt-in 使产物发布与 exports 由各包自行管理。仓库编排仍可为每个已 opt-in 的包运行全仓 Host 约定 pass;该 pass 仍由后续 Remote Gateway Agent Note 负责说明。静态 Cordis catalog 可从标准模型复现,同时不把 `tool-cordis` 与运行时注册表状态耦合。`ctx.typert` 只反映当前运行时中已挂载的产物;对于消费方物化后仍持有的 Zod 实例,卸载流程无法控制。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md
-2026-08-02-typert-remote-method-calls.md: 73ab996d408c71ab70d25058677d0d02efe05804
-2026-08-02-typert-remote-method-calls.zh.md: 06b3f9ad454ca905d33e8d08dde51e6c4e99427e
+2026-08-02-typert-remote-method-calls.md: b6551e1c7f8c94fb02a788aa62cb4acef1addffe
+2026-08-02-typert-remote-method-calls.zh.md: 058ec47e6749ee7576fd84fdcacfda350eec3fb8

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md

@@ -147,7 +147,7 @@ The strict generator writes `scope` only when a direct method has exactly one lo
 
 Parameter order comes from the method signature. HTTP fields come from parameter names or lookup declarations. A cancellation descriptor reserves only the final `signal` position and keeps it outside named `args`; Connection or a direct Gateway caller supplies the actual signal. The Gateway does not infer optional fields, Context types, lookup types, or missing arguments from request contents, and it does not synthesize business defaults.
 
-A LIB codec contains a Zod schema and a canonical `typeSymbol` consisting of "package + public subpath + export name." An SRC codec is marked only as `src-json`. When the Host and consumer run in different JavaScript realms, each holds its own Zod instances, but both sets are generated from the same Typert model and symbol keys.
+A LIB codec contains a success-cached Zod schema factory and a canonical `typeSymbol` consisting of "package + public subpath + export name." Host and Client gateways invoke the factory only when that boundary first encodes or decodes a value. An SRC codec is marked only as `src-json`. When the Host and consumer run in different JavaScript realms, each holds its own Zod instances, but both sets are generated from the same Typert model and symbol keys.
 
 Descriptors exist only in the local registry on each side. The wire carries only the `/api` channel, endpoint, and `{ args }` payload. The Host uses its descriptor to decode and invoke the method, while the Client uses its corresponding descriptor to encode arguments and validate the result.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md

@@ -147,7 +147,7 @@ InvocationDescriptor {
 
 参数顺序来自方法签名,HTTP 字段来自参数名或 lookup 声明。取消 descriptor 只保留最后一个 `signal` 位置,并使其不进入具名 `args`;实际 signal 由 Connection 或直接调用 Gateway 的调用方提供。Gateway 不根据请求内容推断可选字段、Context 类型、lookup 类型或缺失参数,也不会合成业务默认值。
 
-LIB codec 带有 Zod schema 和「package + 公共 subpath + export name」的规范 `typeSymbol`;SRC codec 只标记 `src-json`。Host 和消费端运行在不同 JavaScript realm 时会各自持有 Zod 实例,但这些实例由同一 Typert 模型和 symbol key 生成。
+LIB codec 带有只缓存成功结果的 Zod schema factory 和「package + 公共 subpath + export name」的规范 `typeSymbol`;Host 与 Client gateway 只在该边界首次编码或解码值时调用 factory。SRC codec 只标记 `src-json`。Host 和消费端运行在不同 JavaScript realm 时会各自持有 Zod 实例,但这些实例由同一 Typert 模型和 symbol key 生成。
 
 descriptor 只存在于两端本地 registry。wire 上只有 `/api` channel、endpoint 和 `{ args }` payload;Host 用自己的 descriptor 解码和调用,Client 用自己的对应 descriptor 编码参数和验证结果。
 

+ 2 - 2
docs/subsystems/typert.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/typert.md
-typert.md: 0f3d2b1afdc1b7713402abc5885dc9551e16ac14
-typert.zh.md: a3c06489707e18433ed9018363c57ec76732e606
+typert.md: 954c3dfb7e66b100132c6d4604c28f06fba04916
+typert.zh.md: dd2becdc43871ddbecdfb45909e076435ab5a447

+ 6 - 5
docs/subsystems/typert.md

@@ -38,7 +38,7 @@ interface TypertLookupDefinition {
 
 ## Invocation descriptors
 
-An `InvocationDescriptor` is local reflection, not a wire message. Host and consumer builds generate corresponding descriptors; the request sends only the endpoint and named `args`. Strict codecs carry generated schemas, while SRC codecs enforce JSON-safe values without structural type recovery. Cancellation is an out-of-band carrier signal injected after business parameters and never enters `args`.
+An `InvocationDescriptor` is local reflection, not a wire message. Host and consumer builds generate corresponding descriptors; the request sends only the endpoint and named `args`. Strict codecs carry generated schema factories, while SRC codecs enforce JSON-safe values without structural type recovery. Cancellation is an out-of-band carrier signal injected after business parameters and never enters `args`.
 
 ```ts type-equiv
 /** Codec attached to one invocation parameter or result. */
@@ -46,7 +46,8 @@ type TypertCodec =
   | {
     readonly mode: 'strict'
     readonly typeSymbol: string
-    readonly schema: TypertSchema
+    /** Materialize and return the process-realm schema on first boundary use. */
+    readonly create: () => TypertSchema
   }
   | {
     readonly mode: 'src-json'
@@ -261,14 +262,14 @@ register(contribution: TypertContribution): TypertDisposer
 /**
  * Look up one schema by `<package>#<name>`.
  * @param key - global schema key.
- * @returns the live schema record, or `undefined` when absent.
+ * @returns a record containing the cached schema, or `undefined` when absent.
  */
 get(key: string): TypertSchemaRecord | undefined
 
 /**
  * Resolve one required schema.
  * @param key - global schema key.
- * @returns the live schema record.
+ * @returns a record containing the cached schema.
  * @throws when the key is malformed, the package face is absent, or the schema is not contributed.
  */
 resolve(key: string): TypertSchemaRecord
@@ -276,7 +277,7 @@ resolve(key: string): TypertSchemaRecord
 /**
  * Enumerate live schemas in registration order.
  * @param filter - optional package and face restriction.
- * @returns matching schema records.
+ * @returns matching records containing the cached schemas.
  */
 list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[]
 

+ 6 - 5
docs/subsystems/typert.zh.md

@@ -38,7 +38,7 @@ interface TypertLookupDefinition {
 
 ## 调用 descriptor
 
-`InvocationDescriptor` 是本地反射信息,不是 wire message。Host 与消费方构建会生成彼此对应的 descriptor;请求只发送 endpoint 与具名 `args`。strict codec 携带生成的 schema,SRC codec 则在不恢复结构类型的前提下强制要求 JSON 安全值。取消通过带外 carrier signal 表达:它在业务参数之后注入,绝不进入 `args`。
+`InvocationDescriptor` 是本地反射信息,不是 wire message。Host 与消费方构建会生成彼此对应的 descriptor;请求只发送 endpoint 与具名 `args`。strict codec 携带生成的 schema factory,SRC codec 则在不恢复结构类型的前提下强制要求 JSON 安全值。取消通过带外 carrier signal 表达:它在业务参数之后注入,绝不进入 `args`。
 
 ```ts type-equiv
 /** Codec attached to one invocation parameter or result. */
@@ -46,7 +46,8 @@ type TypertCodec =
   | {
     readonly mode: 'strict'
     readonly typeSymbol: string
-    readonly schema: TypertSchema
+    /** Materialize and return the process-realm schema on first boundary use. */
+    readonly create: () => TypertSchema
   }
   | {
     readonly mode: 'src-json'
@@ -261,14 +262,14 @@ register(contribution: TypertContribution): TypertDisposer
 /**
  * Look up one schema by `<package>#<name>`.
  * @param key - global schema key.
- * @returns the live schema record, or `undefined` when absent.
+ * @returns a record containing the cached schema, or `undefined` when absent.
  */
 get(key: string): TypertSchemaRecord | undefined
 
 /**
  * Resolve one required schema.
  * @param key - global schema key.
- * @returns the live schema record.
+ * @returns a record containing the cached schema.
  * @throws when the key is malformed, the package face is absent, or the schema is not contributed.
  */
 resolve(key: string): TypertSchemaRecord
@@ -276,7 +277,7 @@ resolve(key: string): TypertSchemaRecord
 /**
  * Enumerate live schemas in registration order.
  * @param filter - optional package and face restriction.
- * @returns matching schema records.
+ * @returns matching records containing the cached schemas.
  */
 list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[]
 

+ 1 - 1
packages/api/gateway/src/client/index.ts

@@ -717,7 +717,7 @@ function parseInput(codec: TypertCodec, value: unknown, endpoint: string, field:
     throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`)
   }
   try {
-    return codec.schema.parse(value)
+    return codec.create().parse(value)
   } catch (cause) {
     throw new Error(`client api: ${endpoint} rejected ${JSON.stringify(field)}`, { cause })
   }

+ 1 - 1
packages/api/gateway/src/index.ts

@@ -1140,7 +1140,7 @@ function decode(
 ): unknown {
   try {
     if (codec.mode === 'strict') {
-      value = codec.schema.parse(value)
+      value = codec.create().parse(value)
       /* v8 ignore next -- generated optional-input codecs are the only strict codecs that return undefined. */
       if (value === undefined) return value
     }

+ 3 - 3
packages/api/gateway/tests/gateway-stream.host.spec.ts

@@ -954,7 +954,7 @@ function descriptors(): InvocationDescriptor[] {
     name: 'label',
     wire: 'label',
     source: 'json' as const,
-    codec: { mode: 'strict' as const, typeSymbol: '@fixture/feed#Label', schema: z.string() },
+    codec: { mode: 'strict' as const, typeSymbol: '@fixture/feed#Label', create: () => z.string() },
   }
   const stream = (method: string, parameters: InvocationDescriptor['parameters'], schema: z.ZodType): InvocationDescriptor => ({
     id: `@fixture/feed#feed/${method}`,
@@ -964,7 +964,7 @@ function descriptors(): InvocationDescriptor[] {
     mode: 'stream',
     invocation: { kind: 'direct' },
     parameters,
-    result: { mode: 'strict', typeSymbol: '@fixture/feed#Item', schema },
+    result: { mode: 'strict', typeSymbol: '@fixture/feed#Item', create: () => schema },
   })
   return [
     { ...stream('follow', [label], z.string()), cancellation: { parameter: 'signal' } },
@@ -982,7 +982,7 @@ function descriptors(): InvocationDescriptor[] {
       method: 'unary',
       invocation: { kind: 'direct' },
       parameters: [label],
-      result: { mode: 'strict', typeSymbol: '@fixture/feed#Item', schema: z.string() },
+      result: { mode: 'strict', typeSymbol: '@fixture/feed#Item', create: () => z.string() },
     },
   ]
 }

+ 12 - 12
packages/api/gateway/tests/gateway.client.spec.ts

@@ -152,15 +152,15 @@ function directDescriptor(): InvocationDescriptor {
       wire: 'agentId',
       source: 'lookup',
       lookup: 'fixture',
-      codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', schema: idSchema },
+      codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', create: () => idSchema },
     }, {
       name: 'request',
       wire: 'request',
       source: 'json',
-      codec: { mode: 'strict', typeSymbol: '@fixture#CreateRequest', schema: requestSchema },
+      codec: { mode: 'strict', typeSymbol: '@fixture#CreateRequest', create: () => requestSchema },
     }],
     cancellation: { parameter: 'signal' },
-    result: { mode: 'strict', typeSymbol: '@fixture#CreateResult', schema: createResultSchema },
+    result: { mode: 'strict', typeSymbol: '@fixture#CreateResult', create: () => createResultSchema },
   }
 }
 
@@ -174,15 +174,15 @@ function contextDescriptor(): InvocationDescriptor {
       kind: 'context',
       context: 'fixture',
       wire: 'agentId',
-      codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', schema: idSchema },
+      codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', create: () => idSchema },
     },
     parameters: [{
       name: 'request',
       wire: 'request',
       source: 'json',
-      codec: { mode: 'strict', typeSymbol: '@fixture#RenameRequest', schema: requestSchema },
+      codec: { mode: 'strict', typeSymbol: '@fixture#RenameRequest', create: () => requestSchema },
     }],
-    result: { mode: 'strict', typeSymbol: '@fixture#RenameResult', schema: renameResultSchema },
+    result: { mode: 'strict', typeSymbol: '@fixture#RenameResult', create: () => renameResultSchema },
   }
 }
 
@@ -199,9 +199,9 @@ function maybeDescriptor(): InvocationDescriptor {
       wire: 'value',
       source: 'json',
       acceptsUndefined: true,
-      codec: { mode: 'strict', typeSymbol: '@fixture#MaybeValue', schema },
+      codec: { mode: 'strict', typeSymbol: '@fixture#MaybeValue', create: () => schema },
     }],
-    result: { mode: 'strict', typeSymbol: '@fixture#MaybeValue', schema },
+    result: { mode: 'strict', typeSymbol: '@fixture#MaybeValue', create: () => schema },
   }
 }
 
@@ -217,10 +217,10 @@ function streamDescriptor(): InvocationDescriptor {
       name: 'topic',
       wire: 'topic',
       source: 'json',
-      codec: { mode: 'strict', typeSymbol: '@fixture#Topic', schema: z.string().min(1) },
+      codec: { mode: 'strict', typeSymbol: '@fixture#Topic', create: () => z.string().min(1) },
     }],
     cancellation: { parameter: 'signal' },
-    result: { mode: 'strict', typeSymbol: '@fixture#WatchItem', schema: z.string().min(1) },
+    result: { mode: 'strict', typeSymbol: '@fixture#WatchItem', create: () => z.string().min(1) },
   }
 }
 
@@ -1155,7 +1155,7 @@ describe('Client Typert API', () => {
         ...direct,
         parameters: [...direct.parameters, {
           name: 'other', wire: 'otherId', source: 'lookup', lookup: 'fixture',
-          codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', schema: idSchema },
+          codec: { mode: 'strict', typeSymbol: '@fixture#AgentId', create: () => idSchema },
         }],
       }],
     })).rejects.toThrow('scope must select its only lookup parameter')
@@ -1271,7 +1271,7 @@ describe('Client Typert API', () => {
         name: 'value',
         wire: '__proto__',
         source: 'json',
-        codec: { mode: 'strict', typeSymbol: '@fixture#PrototypeValue', schema: z.string() },
+        codec: { mode: 'strict', typeSymbol: '@fixture#PrototypeValue', create: () => z.string() },
       }],
     }
     const dispose = await ctx.remote.$mount({ package: '@fixture/prototype', descriptors: [descriptor] })

+ 1 - 1
packages/api/gateway/tests/gateway.host.spec.ts

@@ -1349,7 +1349,7 @@ function contextProvider(context: Context) {
 }
 
 function strictCodec(typeSymbol: string, schema: z.ZodType): InvocationDescriptor['result'] {
-  return { mode: 'strict', typeSymbol, schema }
+  return { mode: 'strict', typeSymbol, create: () => schema }
 }
 
 function createDescriptor(): InvocationDescriptor {

+ 14 - 6
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -2807,20 +2807,20 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         signature: 'get(key: string): TypertSchemaRecord | undefined',
         description: 'Look up one schema by `<package>#<name>`.',
         parameters: [{ name: 'key', description: 'global schema key.' }],
-        returns: 'the live schema record, or `undefined` when absent.',
+        returns: 'a record containing the cached schema, or `undefined` when absent.',
       },
       {
         signature: 'resolve(key: string): TypertSchemaRecord',
         description: 'Resolve one required schema.',
         parameters: [{ name: 'key', description: 'global schema key.' }],
-        returns: 'the live schema record.',
+        returns: 'a record containing the cached schema.',
         throws: ['when the key is malformed, the package face is absent, or the schema is not contributed.'],
       },
       {
         signature: 'list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[]',
         description: 'Enumerate live schemas in registration order.',
         parameters: [{ name: 'filter', description: 'optional package and face restriction.' }],
-        returns: 'matching schema records.',
+        returns: 'matching records containing the cached schemas.',
       },
       {
         signature: 'getPackage(packageName: string, face: TypertFace = \'host\'): TypertPackageRecord | undefined',
@@ -6356,11 +6356,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'TypertCodec',
-    declaration: 'export type TypertCodec = {\n    readonly mode: \'strict\';\n    readonly typeSymbol: string;\n    readonly schema: TypertSchema;\n} | {\n    readonly mode: \'src-json\';\n};',
+    declaration: 'export type TypertCodec = {\n    readonly mode: \'strict\';\n    readonly typeSymbol: string;\n    readonly create: () => TypertSchema;\n} | {\n    readonly mode: \'src-json\';\n};',
   },
   {
     name: 'TypertContribution',
-    declaration: 'export interface TypertContribution {\n    readonly package: string;\n    readonly face: TypertFace;\n    readonly schemas: readonly TypertSchema[];\n    readonly model: TypertPackageModel;\n    readonly invocations: readonly InvocationDescriptor[];\n}',
+    declaration: 'export interface TypertContribution {\n    readonly package: string;\n    readonly face: TypertFace;\n    readonly schemas: readonly TypertSchemaFactory[];\n    readonly model: TypertPackageModel;\n    readonly invocations: readonly InvocationDescriptor[];\n}',
   },
   {
     name: 'TypertDisposer',
@@ -6434,13 +6434,21 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'TypertRemoteService',
     declaration: 'export abstract class TypertRemoteService<out T = never> extends Service<T> {\n    readonly typertRemote: TypertGatewayBinding<this>;\n}',
   },
+  {
+    name: 'TypertSchema',
+    declaration: 'export interface TypertSchema<Output = unknown> {\n    parse(value: unknown): Output;\n}',
+  },
+  {
+    name: 'TypertSchemaFactory',
+    declaration: 'export interface TypertSchemaFactory {\n    readonly name: string;\n    readonly create: () => z.ZodType;\n}',
+  },
   {
     name: 'TypertSchemaFilter',
     declaration: 'export interface TypertSchemaFilter {\n    readonly package?: string;\n    readonly face?: TypertFace;\n}',
   },
   {
     name: 'TypertSchemaRecord',
-    declaration: 'export interface TypertSchemaRecord extends TypertSchema {\n    readonly package: string;\n    readonly face: TypertFace;\n    readonly key: string;\n}',
+    declaration: 'export interface TypertSchemaRecord {\n    readonly name: string;\n    readonly schema: z.ZodType;\n    readonly package: string;\n    readonly face: TypertFace;\n    readonly key: string;\n}',
   },
   {
     name: 'TypertServiceModel',

+ 2 - 2
packages/typert/generator/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/typert/generator/README.md
-README.md: 7177e6a34520ab07645727edb15c5a726b9a2a4c
-README.zh.md: c1add1907c7c14ee7dd440dfaceee0ece0aa036e
+README.md: bcbde084425a3a61deb8c9a596dca808f144c1da
+README.zh.md: 450573b57d9236f9f64ad37a339afdb6e428655b

+ 2 - 2
packages/typert/generator/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-typert-generator` lets maintainers turn public TypeScript types into build artifacts and compiler-independent models. Packages opt in through the `./typert` and optional `./client/typert` exports, and generation rejects declarations, publish lists, Remote exports, or Zod projections that it cannot represent correctly. Repository builds can emit executable schemas and matching declarations, while tools can call `WorkspaceAnalyzer` for inspection or catalog generation without publishing artifacts. Generation runs only at build time and never in a live agent session.
+`dsh-typert-generator` lets maintainers turn public TypeScript types into build artifacts and compiler-independent models. Packages opt in through the `./typert` and optional `./client/typert` exports, and generation rejects declarations, publish lists, Remote exports, or Zod projections that it cannot represent correctly. Repository builds emit executable schema factories and matching declarations, while tools can call `WorkspaceAnalyzer` for inspection or catalog generation without publishing artifacts. Generation runs only at build time and never in a live agent session.
 
 ## Table of Contents
 
@@ -83,7 +83,7 @@ Host and Client are independent TypeScript programs. Direct project references e
 
 ### Emission and publication contract
 
-`FaceModelEmitter` emits executable JavaScript containing supported Zod schemas and the `TYPERT` contribution, plus a declaration file whose schemas are typed `z.ZodType<SourceType>` through the package's public export; unsupported Zod projections fail. The Host face with Remote methods additionally emits `typert.remote-client.*` projections of Host Remote contracts for the Client. `WorkspaceTypertGenerator` validates each contributor's `package.json`: `./typert` and `./client/typert` (and `./remote` when Remote methods exist) must point at the exact generated files, and the `files` list must include them.
+`FaceModelEmitter` emits executable JavaScript containing success-cached Zod schema factories and the `TYPERT` contribution, plus a declaration file whose factories return `z.ZodType<SourceType>` through the package's public export; unsupported Zod projections fail. The Host face with Remote methods additionally emits `typert.remote-client.*` projections of Host Remote contracts for the Client. `WorkspaceTypertGenerator` validates each contributor's `package.json`: `./typert` and `./client/typert` (and `./remote` when Remote methods exist) must point at the exact generated files, and the `files` list must include them.
 
 ### Catalog projection
 

+ 2 - 2
packages/typert/generator/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-library"
 
 ## 概述
 
-`dsh-typert-generator` 让维护者把公开的 TypeScript 类型转换为构建产物和与编译器无关的模型。包通过 `./typert` 和可选的 `./client/typert` 导出选择加入;如果声明、发布清单、Remote 导出或 Zod 投影无法被正确表示,生成过程就会失败。仓库构建可以生成可执行 schema 与配套声明,工具也可以调用 `WorkspaceAnalyzer` 完成检查或目录生成而不发布产物。生成过程只在构建时运行,绝不会进入实时 agent(智能体)会话。
+`dsh-typert-generator` 让维护者把公开的 TypeScript 类型转换为构建产物和与编译器无关的模型。包通过 `./typert` 和可选的 `./client/typert` 导出选择加入;如果声明、发布清单、Remote 导出或 Zod 投影无法被正确表示,生成过程就会失败。仓库构建会生成可执行 schema factory 与配套声明,工具也可以调用 `WorkspaceAnalyzer` 完成检查或目录生成而不发布产物。生成过程只在构建时运行,绝不会进入实时 agent(智能体)会话。
 
 ## 目录
 
@@ -83,7 +83,7 @@ Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定
 
 ### 生成与发布约定
 
-`FaceModelEmitter` 输出包含受支持 Zod schema 与 `TYPERT` 贡献的可执行 JavaScript,以及把 schema 通过包的公开导出标注为 `z.ZodType<SourceType>` 的声明文件;不支持的 Zod 投影会失败。含 Remote 方法的 Host face 还会额外为 Client 生成 Host Remote 约定的 `typert.remote-client.*` 投影。`WorkspaceTypertGenerator` 校验每个贡献方的 `package.json`:`./typert` 与 `./client/typert`(存在 Remote 方法时还有 `./remote`)必须指向精确的生成文件,且 `files` 清单必须包含它们。
+`FaceModelEmitter` 输出包含只缓存成功结果的 Zod schema factory 与 `TYPERT` 贡献的可执行 JavaScript,以及把 factory 通过包的公开导出标注为返回 `z.ZodType<SourceType>` 的声明文件;不支持的 Zod 投影会失败。含 Remote 方法的 Host face 还会额外为 Client 生成 Host Remote 约定的 `typert.remote-client.*` 投影。`WorkspaceTypertGenerator` 校验每个贡献方的 `package.json`:`./typert` 与 `./client/typert`(存在 Remote 方法时还有 `./remote`)必须指向精确的生成文件,且 `files` 清单必须包含它们。
 
 ### 目录投影
 

+ 7 - 6
packages/typert/generator/src/emitter.ts

@@ -202,7 +202,7 @@ export class FaceModelEmitter {
     lines.push(`  face: ${quote(this.face.face)},`)
     lines.push('  schemas: [')
     for (const schema of schemas.exports) {
-      lines.push(`    { name: ${quote(schema.exportName)}, schema: ${schema.exportName} },`)
+      lines.push(`    { name: ${quote(schema.exportName)}, create: ${schema.exportName} },`)
     }
     lines.push('  ],')
     lines.push('  invocations: [')
@@ -232,7 +232,7 @@ export class FaceModelEmitter {
     }
     lines.push('')
     for (const schema of schemas.exports) {
-      lines.push(`export declare const ${schema.exportName}: z.ZodType<${schema.exportName}$source>`)
+      lines.push(`export declare const ${schema.exportName}: () => z.ZodType<${schema.exportName}$source>`)
     }
     if (schemas.exports.length > 0) lines.push('')
     // The Loader validates and narrows this generated module boundary before
@@ -556,7 +556,8 @@ class SchemaEmitter {
   emit(): SchemaArtifact {
     const definitions = this.declarations.map(declaration => this.declarationDefinition(declaration))
     for (const boundary of this.boundaries) {
-      definitions.push(`const ${this.boundaryName(boundary.key)} = ${this.typeSchema(boundary.type)}`)
+      const name = this.boundaryName(boundary.key)
+      definitions.push(`let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.typeSchema(boundary.type)})`)
     }
     const exports = this.schemas.map((model): SchemaExport => ({
       model,
@@ -573,7 +574,7 @@ class SchemaEmitter {
   private declarationDefinition(declaration: TypeDeclarationModel): string {
     const name = this.schemaName(declaration.id)
     if (declaration.typeParameters.length === 0) {
-      return `const ${name} = ${this.declarationSchema(declaration, new Map())}`
+      return `let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.declarationSchema(declaration, new Map())})`
     }
     const parameters = declaration.typeParameters.map((parameter, index) =>
       [`type${String(index)}$schema`, parameter.id] as const)
@@ -655,7 +656,7 @@ class SchemaEmitter {
         if (node.arguments.length > 0) {
           this.fail(node.name, `non-generic declaration received ${String(node.arguments.length)} type arguments`)
         }
-        return `z.lazy(() => ${name})`
+        return `z.lazy(() => ${name}())`
       }
       const arguments_ = this.declarationArguments(node, declaration, substitutions)
       return `z.lazy(() => ${name}(${arguments_.join(', ')}))`
@@ -873,7 +874,7 @@ function strictCodec(boundary: RemoteBoundaryModel, schema: string): string {
     '{',
     '  mode: \'strict\',',
     `  typeSymbol: ${quote(boundary.typeSymbol)},`,
-    `  schema: ${schema},`,
+    `  create: ${schema},`,
     '}',
   ].join('\n')
 }

+ 5 - 4
packages/typert/generator/tests/__snapshots__/type-model.spec.ts.snap

@@ -4,10 +4,11 @@ exports[`FaceModelEmitter > emits runnable Zod JavaScript, precise declarations,
 "/* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */
 import { z } from 'zod'
 
-const Payload$schema = z.object({
+let Payload$schema$value
+const Payload$schema = () => (Payload$schema$value ??= z.object({
   'name': z.string(),
   'count': z.number().optional(),
-}).describe('Runtime-validating data root.')
+}).describe('Runtime-validating data root.'))
 
 export const Payload = Payload$schema
 
@@ -15,7 +16,7 @@ export const TYPERT = {
   package: '@fixture/host',
   face: 'host',
   schemas: [
-    { name: 'Payload', schema: Payload },
+    { name: 'Payload', create: Payload },
   ],
   invocations: [
   ],
@@ -321,7 +322,7 @@ exports[`FaceModelEmitter > emits runnable Zod JavaScript, precise declarations,
 import type { z } from 'zod'
 import type { Payload as Payload$source } from '@fixture/host'
 
-export declare const Payload: z.ZodType<Payload$source>
+export declare const Payload: () => z.ZodType<Payload$source>
 
 export declare const TYPERT: unknown
 "

+ 15 - 15
packages/typert/generator/tests/remote-model.spec.ts

@@ -25,9 +25,9 @@ interface RuntimeDescriptor {
   readonly parameters: readonly {
     readonly wire: string
     readonly acceptsUndefined?: true
-    readonly codec: { readonly schema: RuntimeSchema }
+    readonly codec: { readonly create: () => RuntimeSchema }
   }[]
-  readonly result: { readonly schema: RuntimeSchema }
+  readonly result: { readonly create: () => RuntimeSchema }
 }
 
 interface RuntimeRemoteModule {
@@ -152,10 +152,10 @@ describe('Remote model generation', { timeout: 60_000 }, () => {
     expect(generated.TYPERT_REMOTE.package).toBe('@fixture/remote')
     const create = generated.TYPERT_REMOTE.descriptors[0]
     expect(create?.cancellation).toEqual({ parameter: 'signal' })
-    expect(create?.parameters[1]?.codec.schema.safeParse({ title: 'ship' }).success).toBe(true)
-    expect(create?.parameters[1]?.codec.schema.safeParse({ title: 1 }).success).toBe(false)
-    expect(create?.result.schema.safeParse({ ref: 'goal-1' }).success).toBe(true)
-    expect(create?.result.schema.safeParse({ ref: 1 }).success).toBe(false)
+    expect(create?.parameters[1]?.codec.create().safeParse({ title: 'ship' }).success).toBe(true)
+    expect(create?.parameters[1]?.codec.create().safeParse({ title: 1 }).success).toBe(false)
+    expect(create?.result.create().safeParse({ ref: 'goal-1' }).success).toBe(true)
+    expect(create?.result.create().safeParse({ ref: 1 }).success).toBe(false)
     expect(generated.TYPERT_REMOTE.descriptors[2]?.mode).toBe('stream')
 
     const declarationMap = JSON.parse(artifact?.remote?.dtsMap ?? '') as RemoteDeclarationMap
@@ -208,15 +208,15 @@ export type {`,
     const maybe = generated.TYPERT_REMOTE.descriptors.find(descriptor => descriptor.id.endsWith('/maybe'))
     const clear = generated.TYPERT_REMOTE.descriptors.find(descriptor => descriptor.id.endsWith('/clear'))
     expect(maybe?.parameters[0]?.acceptsUndefined).toBe(true)
-    expect(maybe?.parameters[0]?.codec.schema.safeParse(undefined).success).toBe(true)
-    expect(maybe?.result.schema.safeParse(undefined).success).toBe(true)
-    expect(clear?.result.schema.safeParse(undefined).success).toBe(true)
-    expect(clear?.result.schema.safeParse(null).success).toBe(false)
+    expect(maybe?.parameters[0]?.codec.create().safeParse(undefined).success).toBe(true)
+    expect(maybe?.result.create().safeParse(undefined).success).toBe(true)
+    expect(clear?.result.create().safeParse(undefined).success).toBe(true)
+    expect(clear?.result.create().safeParse(null).success).toBe(false)
     const labelled = generated.TYPERT_REMOTE.descriptors.find(descriptor => descriptor.id.endsWith('/labelled'))
     expect(labelled?.parameters[0]?.acceptsUndefined).toBeUndefined()
     expect(labelled?.parameters[1]?.acceptsUndefined).toBe(true)
-    expect(labelled?.parameters[1]?.codec.schema.safeParse(undefined).success).toBe(true)
-    expect(labelled?.parameters[1]?.codec.schema.safeParse(7).success).toBe(false)
+    expect(labelled?.parameters[1]?.codec.create().safeParse(undefined).success).toBe(true)
+    expect(labelled?.parameters[1]?.codec.create().safeParse(7).success).toBe(false)
   })
 
   it('evaluates declaration-merged mapped and conditional boundaries for codecs without widening consumer types', async () => {
@@ -282,13 +282,13 @@ export type GenericResult = {
     const executable = remoteJs.replace("from 'zod'", `from ${JSON.stringify(import.meta.resolve('zod'))}`)
     const generated = await import(`data:text/javascript,${encodeURIComponent(executable)}`) as RuntimeRemoteModule
     const dispatch = generated.TYPERT_REMOTE.descriptors.find(descriptor => descriptor.id.endsWith('/dispatch'))
-    const schema = dispatch?.parameters[0]?.codec.schema
+    const schema = dispatch?.parameters[0]?.codec.create()
     expect(schema?.safeParse({ kind: 'ship', payload: { count: 2, meta: { nested: [true, null] } } }).success).toBe(true)
     expect(schema?.safeParse({ kind: 'ship', payload: { count: '2', meta: {} } }).success).toBe(false)
     expect(schema?.safeParse({ kind: 'cancel', payload: { reason: 'obsolete' } }).success).toBe(true)
     expect(schema?.safeParse({ kind: 'unknown', payload: {} }).success).toBe(false)
-    expect(dispatch?.result.schema.safeParse({ kind: 'ship', value: { accepted: true } }).success).toBe(true)
-    expect(dispatch?.result.schema.safeParse({ kind: 'ship', value: { cancelled: true } }).success).toBe(false)
+    expect(dispatch?.result.create().safeParse({ kind: 'ship', value: { accepted: true } }).success).toBe(true)
+    expect(dispatch?.result.create().safeParse({ kind: 'ship', value: { cancelled: true } }).success).toBe(false)
   })
 
   it('imports public type arguments nested under a named generic boundary', () => {

+ 2 - 2
packages/typert/generator/tests/schema-emitter.spec.ts

@@ -964,9 +964,9 @@ async function loadSchema(source: string): Promise<{ safeParse(value: unknown):
   const path = join(root, 'schema.mjs')
   writeFileSync(path, source)
   const generated = await import(`${pathToFileURL(path).href}?test=${Date.now()}-${String(temporaryRoots.length)}`) as {
-    Root: { safeParse(value: unknown): { success: boolean } }
+    Root: () => { safeParse(value: unknown): { success: boolean } }
   }
-  return generated.Root
+  return generated.Root()
 }
 
 function distinct(values: readonly string[]): string[] {

+ 7 - 6
packages/typert/generator/tests/type-model.spec.ts

@@ -1605,18 +1605,19 @@ describe('FaceModelEmitter', { timeout: 60_000 }, () => {
     const modulePath = join(root, 'host.mjs')
     writeFileSync(modulePath, artifact.js)
     const generated = await import(`${pathToFileURL(modulePath).href}?test=${Date.now()}`) as {
-      Payload: { safeParse(value: unknown): { success: boolean } }
+      Payload: () => { safeParse(value: unknown): { success: boolean } }
       TYPERT: {
         package: string
         face: string
-        schemas: { name: string; schema: unknown }[]
+        schemas: { name: string; create: () => unknown }[]
         model: { services: { key: string; members: { signature: string }[] }[] }
       }
     }
-    expect(generated.Payload.safeParse({ name: 'ready', count: 2 }).success).toBe(true)
-    expect(generated.Payload.safeParse({ name: 'ready', count: 'two' }).success).toBe(false)
+    expect(generated.Payload().safeParse({ name: 'ready', count: 2 }).success).toBe(true)
+    expect(generated.Payload().safeParse({ name: 'ready', count: 'two' }).success).toBe(false)
+    expect(generated.Payload()).toBe(generated.Payload())
     expect(generated.TYPERT).toMatchObject({ package: '@fixture/host', face: 'host' })
-    expect(generated.TYPERT.schemas[0]?.schema).toBe(generated.Payload)
+    expect(generated.TYPERT.schemas[0]?.create).toBe(generated.Payload)
     const demo = generated.TYPERT.model.services.find(service => service.key === 'demo')
     expect(demo).toMatchObject({ key: 'demo' })
     expect(demo?.members.map(member => member.signature)).toContain(
@@ -1631,7 +1632,7 @@ describe('FaceModelEmitter', { timeout: 60_000 }, () => {
       'import { Payload } from \'./host.js\'',
       'import type { Payload as SourcePayload } from \'@fixture/host\'',
       'import type { z } from \'zod\'',
-      'const precise: z.ZodType<SourcePayload> = Payload',
+      'const precise: () => z.ZodType<SourcePayload> = Payload',
       'void precise',
       '',
     ].join('\n'))

+ 2 - 2
packages/typert/loader/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/typert/loader/README.md
-README.md: 378be98dea5f47e4d6b0a0c70600e2a4c221c80b
-README.zh.md: e598127bba12e4057405231fe09199db18f9604d
+README.md: c45fbdebe7c2a05491515c3679ca2a3338285ca1
+README.zh.md: 9cb6ae6da5da30292d74e05932cd147090aa4dc8

+ 3 - 3
packages/typert/loader/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-With `dsh-typert-loader` mounted, every package that mounts in a Loader composition automatically contributes its generated Typert reflection and schemas to the runtime registry — and withdraws them when the package or the plugin unmounts. Packages without the generated export are skipped, so adding the plugin to any composition is safe. An explicit `packages` list covers plugins nested behind another Loader entry, whose fibers carry no resolvable package specifier. It is a Node-only plugin and needs the config-tree resolution anchor to resolve packages.
+With `dsh-typert-loader` mounted, every package that mounts in a Loader composition automatically contributes its generated Typert reflection and schema factories to the runtime registry — and withdraws them when the package or the plugin unmounts. Packages without the generated export are skipped, so adding the plugin to any composition is safe. An explicit `packages` list covers plugins nested behind another Loader entry, whose fibers carry no resolvable package specifier. It is a Node-only plugin and needs the config-tree resolution anchor to resolve packages.
 
 ## Table of Contents
 
@@ -44,7 +44,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
 
 ### What gets registered
 
-Each qualifying Loader entry contributes its generated host-face reflection and schemas to the runtime registry. Registration follows the entry lifecycle: it is withdrawn when the entry or the plugin unmounts, and an import that settles after either owner is gone is discarded.
+Each qualifying Loader entry contributes its generated host-face reflection and schema factories to the runtime registry. Registration follows the entry lifecycle: it is withdrawn when the entry or the plugin unmounts, and an import that settles after either owner is gone is discarded.
 
 ### Observable behavior and failures
 
@@ -66,7 +66,7 @@ The plugin is an incremental scanner mirroring the client-modules node half: eve
 
 ### Manifest validation
 
-`validateTypertManifest()` is the module/file boundary: the manifest crosses from a build artifact into the typed registry, so every field is checked. The manifest must name the package that exports it, carry face `host`, hold zod v4 schema instances, and keep well-formed service, event, object, member, type, and documentation records; invocation descriptors must use strict codecs. Every failure names the package and the defect.
+`validateTypertManifest()` is the module/file boundary: the manifest crosses from a build artifact into the typed registry, so every field is checked. The manifest must name the package that exports it, carry face `host`, hold schema factories, and keep well-formed service, event, object, member, type, and documentation records; invocation descriptors must use strict codecs with factories. Every failure names the package and the defect.
 
 ### Caching and ownership
 

+ 3 - 3
packages/typert/loader/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-挂载 `dsh-typert-loader` 后,Loader 组合中每个挂载的包都会自动把其生成的 Typert 反射与 schema 贡献给运行时注册表——并在包或本插件卸载时自动撤销。没有该导出的包会被跳过,因此在任何 Loader 组合中挂载它都是安全的。显式 `packages` 用于覆盖嵌套在另一 Loader 配置项之下的插件,这些插件的 fiber 不携带可解析的包说明符。它是仅支持 Node 的插件,需要配置树解析锚点才能解析包。
+挂载 `dsh-typert-loader` 后,Loader 组合中每个挂载的包都会自动把其生成的 Typert 反射与 schema factory 贡献给运行时注册表——并在包或本插件卸载时自动撤销。没有该导出的包会被跳过,因此在任何 Loader 组合中挂载它都是安全的。显式 `packages` 用于覆盖嵌套在另一 Loader 配置项之下的插件,这些插件的 fiber 不携带可解析的包说明符。它是仅支持 Node 的插件,需要配置树解析锚点才能解析包。
 
 ## 目录
 
@@ -44,7 +44,7 @@ kind: "package-reference"
 
 ### 注册什么
 
-每个符合条件的 Loader 配置项都会把其生成的宿主侧反射与 schema 贡献给运行时注册表。注册跟随配置项生命周期:配置项或本插件卸载时撤销;在配置项或本插件任一方卸载后才结束的导入操作会被丢弃。
+每个符合条件的 Loader 配置项都会把其生成的宿主侧反射与 schema factory 贡献给运行时注册表。注册跟随配置项生命周期:配置项或本插件卸载时撤销;在配置项或本插件任一方卸载后才结束的导入操作会被丢弃。
 
 ### 可观察行为与失败
 
@@ -66,7 +66,7 @@ kind: "package-reference"
 
 ### Manifest 校验
 
-`validateTypertManifest()` 是模块/文件边界:manifest 从构建产物进入类型化注册表,因此每个字段都会被检查。manifest 必须指名导出它的包、携带 `host` face、持有 zod v4 schema 实例,并保持服务、事件、对象、成员、类型与文档记录格式正确;调用描述符必须使用严格编解码器。每次失败都会指名包与缺陷。
+`validateTypertManifest()` 是模块/文件边界:manifest 从构建产物进入类型化注册表,因此每个字段都会被检查。manifest 必须指名导出它的包、携带 `host` face、持有 schema factory,并保持服务、事件、对象、成员、类型与文档记录格式正确;调用描述符必须使用带 factory 的严格编解码器。每次失败都会指名包与缺陷。
 
 ### 缓存与归属
 

+ 4 - 7
packages/typert/loader/src/index.ts

@@ -108,8 +108,8 @@ export function validateTypertManifest(pkgName: string, exported: unknown): Type
     }
     const schema = value as Record<string, unknown>
     requireString(pkgName, schema, 'name', 'schema')
-    if (typeof schema.schema !== 'object' || schema.schema === null || !('_zod' in schema.schema)) {
-      throw new Error(`typert-loader: ${pkgName} TYPERT schema "${schema.name as string}" is not a zod v4 schema instance`)
+    if (typeof schema.create !== 'function') {
+      throw new Error(`typert-loader: ${pkgName} TYPERT schema "${schema.name as string}" has no create() factory`)
     }
   }
   const model = requireObject(pkgName, manifest.model, 'TYPERT.model')
@@ -273,11 +273,8 @@ function requireStrictCodec(pkgName: string, value: unknown, subject: string): v
     throw new Error(`typert-loader: ${pkgName} ${subject} must use a strict codec`)
   }
   requireString(pkgName, codec, 'typeSymbol', subject)
-  if (typeof codec.schema !== 'object'
-    || codec.schema === null
-    || !('_zod' in codec.schema)
-    || typeof (codec.schema as { parse?: unknown }).parse !== 'function') {
-    throw new Error(`typert-loader: ${pkgName} ${subject} is not backed by a zod v4 schema`)
+  if (typeof codec.create !== 'function') {
+    throw new Error(`typert-loader: ${pkgName} ${subject} has no create() factory`)
   }
 }
 

+ 17 - 17
packages/typert/loader/tests/loader.spec.ts

@@ -58,11 +58,11 @@ async function writePackage(
 function typertSource(pkgName: string, entryName: string): string {
   return [
     'import { z } from \'zod\'',
-    `export const ${entryName} = z.object({ id: z.string() })`,
+    `export const ${entryName} = () => z.object({ id: z.string() })`,
     'export const TYPERT = {',
     `  package: '${pkgName}',`,
     '  face: \'host\',',
-    `  schemas: [{ name: '${entryName}', schema: ${entryName} }],`,
+    `  schemas: [{ name: '${entryName}', create: ${entryName} }],`,
     '  model: { services: [], events: [], objects: [] },',
     '  invocations: [],',
     '}',
@@ -73,7 +73,7 @@ function typertSource(pkgName: string, entryName: string): string {
 function invocationTypertSource(pkgName: string): string {
   return [
     'import { z } from \'zod\'',
-    'const Text = z.string()',
+    'const Text = () => z.string()',
     'export const TYPERT = {',
     `  package: '${pkgName}',`,
     '  face: \'host\',',
@@ -85,10 +85,10 @@ function invocationTypertSource(pkgName: string): string {
     '    invocation: { kind: \'direct\' },',
     '    parameters: [{',
     '      name: \'request\', wire: \'request\', source: \'json\',',
-    `      codec: { mode: 'strict', typeSymbol: '${pkgName}/types#Request', schema: Text },`,
+    `      codec: { mode: 'strict', typeSymbol: '${pkgName}/types#Request', create: Text },`,
     '    }],',
     "    cancellation: { parameter: 'signal' },",
-    `    result: { mode: 'strict', typeSymbol: '${pkgName}/types#Result', schema: Text },`,
+    `    result: { mode: 'strict', typeSymbol: '${pkgName}/types#Result', create: Text },`,
     '    sourceLocation: { file: \'src/index.ts\', line: 8, column: 3 },',
     '  }],',
     '}',
@@ -168,7 +168,7 @@ describe('typert loader', () => {
     })
     expect(descriptor?.parameters[0]?.codec.mode).toBe('strict')
     if (descriptor?.parameters[0]?.codec.mode === 'strict') {
-      expect(descriptor.parameters[0].codec.schema.parse('request')).toBe('request')
+      expect(descriptor.parameters[0].codec.create().parse('request')).toBe('request')
     }
 
     await fiber.dispose()
@@ -289,11 +289,11 @@ describe('typert loader', () => {
         'import { z } from \'zod\'',
         'globalThis.__dshTypertLoaderGate.started()',
         'await globalThis.__dshTypertLoaderGate.wait',
-        'export const Pending = z.object({ id: z.string() })',
+        'export const Pending = () => z.object({ id: z.string() })',
         'export const TYPERT = {',
         '  package: \'@fixture/pending\',',
         '  face: \'host\',',
-        '  schemas: [{ name: \'Pending\', schema: Pending }],',
+        '  schemas: [{ name: \'Pending\', create: Pending }],',
         '  model: { services: [], events: [], objects: [] },',
         '  invocations: [],',
         '}',
@@ -329,7 +329,7 @@ describe('typert loader', () => {
     root = await mkdtemp(join(tmpdir(), 'dsh-typert-loader-'))
     await linkZod(root)
     await writePackage(root, '@fixture/broken', {
-      typertSource: 'export const TYPERT = { package: \'@fixture/broken\', face: \'host\', schemas: [{ name: \'\', schema: {} }], model: { services: [], events: [], objects: [] }, invocations: [] }\n',
+      typertSource: 'export const TYPERT = { package: \'@fixture/broken\', face: \'host\', schemas: [{ name: \'\', create: {} }], model: { services: [], events: [], objects: [] }, invocations: [] }\n',
     })
     const ctx = await boot()
     await ctx.loader.create({ name: '@fixture/broken' })
@@ -443,7 +443,7 @@ describe('validateTypertManifest', () => {
     expect(validateTypertManifest('pkg', {
       package: 'pkg',
       face: 'host',
-      schemas: [{ name: 'A', schema: zodish }],
+      schemas: [{ name: 'A', create: () => zodish }],
       model: { services: [], events: [], objects: [] },
       invocations: [],
     }).schemas).toHaveLength(1)
@@ -453,10 +453,10 @@ describe('validateTypertManifest', () => {
     expect(() => validateTypertManifest('pkg', { package: 'pkg', face: 'client' })).toThrow('TYPERT.face is not "host"')
     expect(() => validateTypertManifest('pkg', { package: 'pkg', face: 'host', schemas: 'x' })).toThrow('schemas must be an array')
     expect(() => validateTypertManifest('pkg', { package: 'pkg', face: 'host', schemas: [null] })).toThrow('non-object schema')
-    expect(() => validateTypertManifest('pkg', { package: 'pkg', face: 'host', schemas: [{ name: '', schema: zodish }] }))
+    expect(() => validateTypertManifest('pkg', { package: 'pkg', face: 'host', schemas: [{ name: '', create: () => zodish }] }))
       .toThrow('missing or empty name')
-    expect(() => validateTypertManifest('pkg', { package: 'pkg', face: 'host', schemas: [{ name: 'A', schema: {} }] }))
-      .toThrow('not a zod v4 schema instance')
+    expect(() => validateTypertManifest('pkg', { package: 'pkg', face: 'host', schemas: [{ name: 'A', create: {} }] }))
+      .toThrow('has no create() factory')
     expect(() => validateTypertManifest('pkg', {
       package: 'pkg',
       face: 'host',
@@ -572,8 +572,8 @@ describe('validateTypertManifest', () => {
     })).toThrow('cancellation parameter must be "signal"')
     expect(() => validateTypertManifest('pkg', {
       ...base,
-      invocations: [{ ...descriptor, result: { mode: 'strict', typeSymbol: 'pkg#Result', schema: zodish } }],
-    })).toThrow('result codec is not backed by a zod v4 schema')
+      invocations: [{ ...descriptor, result: { mode: 'strict', typeSymbol: 'pkg#Result', create: zodish } }],
+    })).toThrow('result codec has no create() factory')
     expect(() => validateTypertManifest('pkg', {
       ...base,
       invocations: [{
@@ -667,7 +667,7 @@ describe('validateTypertManifest', () => {
 })
 
 function strictCodec(typeSymbol: string) {
-  return { mode: 'strict', typeSymbol, schema: z.string() }
+  return { mode: 'strict', typeSymbol, create: () => z.string() }
 }
 
 function strictInvocation() {
@@ -694,7 +694,7 @@ function completeManifest(zodish: object) {
   return {
     package: 'pkg',
     face: 'host',
-    schemas: [{ name: 'Schema', schema: zodish }],
+    schemas: [{ name: 'Schema', create: () => zodish }],
     invocations: [],
     model: {
       services: [{

+ 2 - 2
packages/typert/protocol/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/typert/protocol/README.md
-README.md: 43147c2c2f1746ff0c806c0842b5dba65457775a
-README.zh.md: c88432ff3587cba1a3222a3a4d75cd8fe0e2d514
+README.md: 99883e322bf6e528f8fd4effe9c4fc1e418abbdb
+README.zh.md: 429c08587a5297e254ac4c0f3fb9d82fc680a455

+ 1 - 1
packages/typert/protocol/README.md

@@ -91,7 +91,7 @@ The merge-extensible protocol maps keep static associations in the type system,
 
 ### Wire identity grammar
 
-Every namespace, method, lookup, and Context segment must satisfy `isTypertRemoteSegment()`, so generated names cross the shared RPC carrier unchanged. Strict codecs carry generated schemas; `src-json` codecs identify the weaker source-launch path.
+Every namespace, method, lookup, and Context segment must satisfy `isTypertRemoteSegment()`, so generated names cross the shared RPC carrier unchanged. Strict codecs carry generated schema factories; `src-json` codecs identify the weaker source-launch path.
 
 ### Source map
 

+ 1 - 1
packages/typert/protocol/README.zh.md

@@ -91,7 +91,7 @@ Host 装配以转发给消费方的 Cordis 事件扩展 `TypertRemoteEventSelect
 
 ### Wire 标识文法
 
-每个命名空间、方法、查找与 Context 段都必须满足 `isTypertRemoteSegment()`,生成的名字才能原样跨共享 RPC 载体传输。严格编解码器携带生成的 schema;`src-json` 编解码器标识约束更弱的源码启动路径。
+每个命名空间、方法、查找与 Context 段都必须满足 `isTypertRemoteSegment()`,生成的名字才能原样跨共享 RPC 载体传输。严格编解码器携带生成的 schema factory;`src-json` 编解码器标识约束更弱的源码启动路径。
 
 ### 源码地图
 

+ 2 - 1
packages/typert/protocol/src/types.ts

@@ -223,7 +223,8 @@ export type TypertCodec =
   | {
     readonly mode: 'strict'
     readonly typeSymbol: string
-    readonly schema: TypertSchema
+    /** Materialize and return the process-realm schema on first boundary use. */
+    readonly create: () => TypertSchema
   }
   | {
     readonly mode: 'src-json'

+ 2 - 2
packages/typert/registry/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/typert/registry/README.md
-README.md: c1ab98ae8ebe720a29eb4fb339f39e00ce593120
-README.zh.md: 7bf90ff8fd02c4fb17c4f57d1af251a7a4bd3116
+README.md: 75a3d2bc495db97067818fe9c16e835e8e1f98bc
+README.zh.md: b47d735c17ca4e51f41c82916a4b402ddf84e853

+ 4 - 4
packages/typert/registry/README.md

@@ -1,5 +1,5 @@
 ---
-description: "The runtime Typert registry: stores generated package reflection, live Zod schemas, and Remote invocation descriptors, and resolves them for consumers."
+description: "The runtime Typert registry: stores generated package reflection, lazy Zod schema factories, and Remote invocation descriptors, and resolves them for consumers."
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-typert-registry` makes generated Typert artifacts queryable at runtime: each package's reflection — services, events, and objects — its live Zod schemas, and Remote invocation descriptors live under stable keys that consumers can query or resolve on demand. Registrations are atomic and fiber-scoped: a contribution lands whole or not at all and is withdrawn automatically when the registering component unloads. The same service hosts the lookup and scoped-Context provider registries that Remote calls resolve through. It performs no TypeScript analysis and generates no schemas; the generator and the loader handle those.
+`dsh-typert-registry` makes generated Typert artifacts queryable at runtime: each package's reflection, lazy Zod schema factories, and Remote invocation descriptors live under stable keys. A schema is materialized and cached when a consumer first requests it. Registrations are atomic and fiber-scoped: a contribution lands whole or not at all and is withdrawn automatically when the registering component unloads. The same service hosts the lookup and scoped-Context provider registries that Remote calls resolve through. It performs no TypeScript analysis and generates no schemas; the generator and the loader handle those.
 
 ## Table of Contents
 
@@ -37,7 +37,7 @@ Load the registry plugin; the Client face is installed the same way by the Clien
 
 ### Querying schemas and reflection
 
-Consumers read schemas with `get(key)`, `resolve(key)`, or `list(filter?)` and package reflection with `getPackage(name, face?)` or `listPackages(filter?)`. `resolve()` distinguishes a malformed key, an absent package, and a registered package that contributes no schema under that name, each with its own error. `toJSONSchema(key)` projects a live Zod schema to JSON Schema without caching.
+Consumers read schemas with `get(key)`, `resolve(key)`, or `list(filter?)` and package reflection with `getPackage(name, face?)` or `listPackages(filter?)`. The first schema read materializes one process-realm instance; later reads reuse it. `resolve()` distinguishes a malformed key, an absent package, and a registered package that contributes no schema under that name, each with its own error. `toJSONSchema(key)` projects the materialized Zod schema to JSON Schema without caching the projection.
 
 ### Registering a contribution
 
@@ -72,7 +72,7 @@ Each sub-registry publishes `TypertRegistryChange` events to subscribed listener
 
 ### Identity and validation
 
-Keys are stable: `<package>#<face>` for reflection, `<package>#<name>` for schemas, and `<namespace>/<method>` for endpoints. Validation rejects names containing `#`, wire names outside the RPC segment grammar, duplicate keys, and lookup definitions whose wire declaration changes during the registry lifetime; strict codecs must carry a parseable schema.
+Keys are stable: `<package>#<face>` for reflection, `<package>#<name>` for schemas, and `<namespace>/<method>` for endpoints. Validation rejects names containing `#`, wire names outside the RPC segment grammar, duplicate keys, and lookup definitions whose wire declaration changes during the registry lifetime; schema entries and strict codecs must carry a factory.
 
 ### Source map
 

+ 4 - 4
packages/typert/registry/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "运行时 Typert 注册表:保存生成的包反射、实时 Zod schema 与 Remote 调用描述符,并按需为消费方解析。"
+description: "运行时 Typert 注册表:保存生成的包反射、惰性 Zod schema factory 与 Remote 调用描述符,并按需为消费方解析。"
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-typert-registry` 让生成的 Typert 产物在运行时可按需查询:每个包的反射——服务、事件与对象——其实时 Zod schema 与 Remote 调用描述符都保存在稳定键下,消费方可以按需查询或解析。注册是原子且按 fiber 作用域的:贡献要么整体落地要么完全不落地,并在注册组件卸载时自动撤销。同一服务还托管 Remote 调用所经由的 lookup 与作用域 Context 提供方注册表。它不执行 TypeScript 分析,也不生成 schema;这些由生成器与 loader 负责。
+`dsh-typert-registry` 让生成的 Typert 产物在运行时可按需查询:每个包的反射、惰性 Zod schema factory 与 Remote 调用描述符都保存在稳定键下。消费方首次请求 schema 时才会物化并缓存它。注册是原子且按 fiber 作用域的:贡献要么整体落地要么完全不落地,并在注册组件卸载时自动撤销。同一服务还托管 Remote 调用所经由的 lookup 与作用域 Context 提供方注册表。它不执行 TypeScript 分析,也不生成 schema;这些由生成器与 loader 负责。
 
 ## 目录
 
@@ -37,7 +37,7 @@ kind: "package-reference"
 
 ### 查询 schema 与反射
 
-消费方用 `get(key)`、`resolve(key)` 或 `list(filter?)` 读取 schema,用 `getPackage(name, face?)` 或 `listPackages(filter?)` 读取包反射。`resolve()` 能区分格式错误的键、未注册的包,以及已注册但未以该名称提供 schema 的包,各自给出不同的错误。`toJSONSchema(key)` 把实时 Zod schema 投影为 JSON Schema,且不缓存结果
+消费方用 `get(key)`、`resolve(key)` 或 `list(filter?)` 读取 schema,用 `getPackage(name, face?)` 或 `listPackages(filter?)` 读取包反射。首次读取会物化一个进程 realm 内的 schema 实例,后续读取复用该实例。`resolve()` 能区分格式错误的键、未注册的包,以及已注册但未以该名称提供 schema 的包,各自给出不同的错误。`toJSONSchema(key)` 把已物化的 Zod schema 投影为 JSON Schema,但不缓存该投影
 
 ### 注册贡献
 
@@ -72,7 +72,7 @@ Remote 调用通过 `ctx.typert.lookups` 与 `ctx.typert.contexts` 解析 Host 
 
 ### 标识与校验
 
-键是稳定的:反射用 `<package>#<face>`,schema 用 `<package>#<name>`,端点用 `<namespace>/<method>`。校验会拒绝含 `#` 的名称、超出 RPC 端点段文法的 wire 名称、重复键,以及在其注册表生命周期内改变 wire 声明的 lookup 定义;严格编解码器必须携带可解析的 schema
+键是稳定的:反射用 `<package>#<face>`,schema 用 `<package>#<name>`,端点用 `<namespace>/<method>`。校验会拒绝含 `#` 的名称、超出 RPC 端点段文法的 wire 名称、重复键,以及在其注册表生命周期内改变 wire 声明的 lookup 定义;schema 条目与严格编解码器必须携带 factory
 
 ### 源码地图
 

+ 34 - 11
packages/typert/registry/src/service.ts

@@ -35,6 +35,7 @@ import type {
   TypertFace,
   TypertPackageFilter,
   TypertPackageRecord,
+  TypertSchemaFactory,
   TypertSchemaFilter,
   TypertSchemaRecord,
 } from './types.ts'
@@ -444,7 +445,7 @@ interface HostContextResolverEntry {
  * @typert service typert
  */
 export class TypertRegistry extends Service implements TypertRegistryContract {
-  private readonly schemas = new Map<string, TypertSchemaRecord>()
+  private readonly schemas = new Map<string, TypertSchemaFactoryRecord>()
   private readonly packages = new Map<string, TypertPackageRecord>()
   private readonly localStore: DescriptorStore
   private readonly remoteStore: RemoteStore
@@ -522,21 +523,22 @@ export class TypertRegistry extends Service implements TypertRegistryContract {
   /**
    * Look up one schema by `<package>#<name>`.
    * @param key - global schema key.
-   * @returns the live schema record, or `undefined` when absent.
+   * @returns a record containing the cached schema, or `undefined` when absent.
    */
   get(key: string): TypertSchemaRecord | undefined {
-    return this.schemas.get(key)
+    const record = this.schemas.get(key)
+    return record === undefined ? undefined : materializeSchema(record)
   }
 
   /**
    * Resolve one required schema.
    * @param key - global schema key.
-   * @returns the live schema record.
+   * @returns a record containing the cached schema.
    * @throws when the key is malformed, the package face is absent, or the schema is not contributed.
    */
   resolve(key: string): TypertSchemaRecord {
     const record = this.schemas.get(key)
-    if (record !== undefined) return record
+    if (record !== undefined) return materializeSchema(record)
     const hash = key.indexOf('#')
     if (hash <= 0 || hash === key.length - 1) {
       throw new Error(`typert: invalid schema key "${key}" — expected "<package>#<name>"`)
@@ -553,10 +555,10 @@ export class TypertRegistry extends Service implements TypertRegistryContract {
   /**
    * Enumerate live schemas in registration order.
    * @param filter - optional package and face restriction.
-   * @returns matching schema records.
+   * @returns matching records containing the cached schemas.
    */
   list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[] {
-    return [...this.schemas.values()].filter(record => matches(record, filter))
+    return [...this.schemas.values()].filter(record => matches(record, filter)).map(materializeSchema)
   }
 
   /**
@@ -606,11 +608,14 @@ export class TypertRegistry extends Service implements TypertRegistryContract {
     }
   }
 
-  private validateSchemas(contribution: TypertContribution): TypertSchemaRecord[] {
-    const records: TypertSchemaRecord[] = []
+  private validateSchemas(contribution: TypertContribution): TypertSchemaFactoryRecord[] {
+    const records: TypertSchemaFactoryRecord[] = []
     const batch = new Set<string>()
     for (const schema of contribution.schemas) {
       validateSegment('schema name', schema.name)
+      if (typeof schema.create !== 'function') {
+        throw new Error(`typert: schema "${schema.name}" has no create() factory`)
+      }
       const key = typertKey(contribution.package, schema.name)
       if (batch.has(key) || this.schemas.has(key)) {
         throw new Error(`typert: schema "${key}" is already registered`)
@@ -627,6 +632,24 @@ export class TypertRegistry extends Service implements TypertRegistryContract {
   }
 }
 
+interface TypertSchemaFactoryRecord extends TypertSchemaFactory {
+  readonly package: string
+  readonly face: TypertFace
+  readonly key: string
+  value?: z.ZodType
+}
+
+function materializeSchema(record: TypertSchemaFactoryRecord): TypertSchemaRecord {
+  const schema = record.value ??= record.create()
+  return {
+    name: record.name,
+    schema,
+    package: record.package,
+    face: record.face,
+    key: record.key,
+  }
+}
+
 function matches(
   record: { readonly package: string; readonly face: TypertFace },
   filter: { readonly package?: string; readonly face?: TypertFace },
@@ -697,8 +720,8 @@ function validateInvocation(descriptor: InvocationDescriptor): void {
 function validateCodec(codec: InvocationDescriptor['result'], subject: string): void {
   if (codec.mode === 'src-json') return
   validateNonempty(`${subject} type symbol`, codec.typeSymbol)
-  if (typeof codec.schema.parse !== 'function') {
-    throw new Error(`typert: ${subject} strict codec has no parse() method`)
+  if (typeof codec.create !== 'function') {
+    throw new Error(`typert: ${subject} strict codec has no create() factory`)
   }
 }
 

+ 8 - 5
packages/typert/registry/src/types.ts

@@ -71,24 +71,27 @@ export interface TypertPackageModel {
   readonly objects: readonly TypertObjectModel[]
 }
 
-/** One generated live Zod schema. */
-export interface TypertSchema {
+/** One generated Zod schema factory. */
+export interface TypertSchemaFactory {
   readonly name: string
-  readonly schema: z.ZodType
+  /** Materialize and return the process-realm schema on first use. */
+  readonly create: () => z.ZodType
 }
 
 /** One generated package contribution registered and withdrawn atomically. */
 export interface TypertContribution {
   readonly package: string
   readonly face: TypertFace
-  readonly schemas: readonly TypertSchema[]
+  readonly schemas: readonly TypertSchemaFactory[]
   readonly model: TypertPackageModel
   /** Host invocation definitions, empty when the package exports no Remote methods. */
   readonly invocations: readonly InvocationDescriptor[]
 }
 
 /** A live schema plus its contribution identity. */
-export interface TypertSchemaRecord extends TypertSchema {
+export interface TypertSchemaRecord {
+  readonly name: string
+  readonly schema: z.ZodType
   readonly package: string
   readonly face: TypertFace
   readonly key: string

+ 18 - 12
packages/typert/registry/tests/typert.spec.ts

@@ -1,4 +1,4 @@
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { z } from 'zod'
 import TypertRegistry, {
@@ -36,7 +36,7 @@ function toolsContribution(schema: z.ZodType = z.object({ name: z.string() })):
   return {
     package: '@deepseek-ai/dsh-tools',
     face: 'host',
-    schemas: [{ name: 'ToolInput', schema }],
+    schemas: [{ name: 'ToolInput', create: () => schema }],
     invocations: [],
     model: {
       services: [{
@@ -111,7 +111,7 @@ describe('TypertRegistry', () => {
       face: 'host',
       name: 'ToolInput',
     })
-    expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')?.schema).toBe(contribution.schemas[0]?.schema)
+    expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')?.schema).toBe(contribution.schemas[0]?.create())
     expect(ctx.typert.getPackage('@deepseek-ai/dsh-tools', 'host')).toMatchObject({
       key: '@deepseek-ai/dsh-tools#host',
       model: { services: [{ key: 'tools' }] },
@@ -153,14 +153,14 @@ describe('TypertRegistry', () => {
     ctx.typert.register(original)
 
     expect(() => ctx.typert.register(toolsContribution(z.never()))).toThrow('package face')
-    expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')?.schema).toBe(original.schemas[0]?.schema)
+    expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')?.schema).toBe(original.schemas[0]?.create())
 
     const duplicateBatch: TypertContribution = {
       ...toolsContribution(),
       package: '@fixture/duplicate',
       schemas: [
-        { name: 'Same', schema: z.string() },
-        { name: 'Same', schema: z.number() },
+        { name: 'Same', create: () => z.string() },
+        { name: 'Same', create: () => z.number() },
       ],
     }
     expect(() => ctx.typert.register(duplicateBatch)).toThrow('schema "@fixture/duplicate#Same" is already registered')
@@ -180,7 +180,7 @@ describe('TypertRegistry', () => {
     expect(() => ctx.typert.register({
       ...toolsContribution(),
       package: '@fixture/schema-name',
-      schemas: [{ name: 'bad#name', schema: z.string() }],
+      schemas: [{ name: 'bad#name', create: () => z.string() }],
     })).toThrow('invalid schema name')
 
     expect(ctx.typert.list({ package: '@fixture/absent' })).toEqual([])
@@ -191,9 +191,15 @@ describe('TypertRegistry', () => {
 
   it('resolves required schemas and projects fresh JSON Schema documents', async () => {
     const ctx = await makeCtx()
-    ctx.typert.register(toolsContribution())
+    const schema = z.object({ name: z.string() })
+    const create = vi.fn(() => schema)
+    ctx.typert.register({ ...toolsContribution(), schemas: [{ name: 'ToolInput', create }] })
+    expect(create).not.toHaveBeenCalled()
 
     expect(ctx.typert.resolve('@deepseek-ai/dsh-tools#ToolInput').name).toBe('ToolInput')
+    expect(create).toHaveBeenCalledOnce()
+    expect(ctx.typert.resolve('@deepseek-ai/dsh-tools#ToolInput').schema).toBe(schema)
+    expect(create).toHaveBeenCalledOnce()
     expect(() => ctx.typert.resolve('@deepseek-ai/dsh-tools#Missing')).toThrow('contributes no schema named "Missing"')
     expect(() => ctx.typert.resolve('@fixture/absent#Value')).toThrow('has no registered contribution')
     expect(() => ctx.typert.resolve('invalid')).toThrow('expected "<package>#<name>"')
@@ -483,7 +489,7 @@ describe('TypertRegistry', () => {
     const strict = {
       mode: 'strict' as const,
       typeSymbol: '@fixture#Value',
-      schema: z.string(),
+      create: () => z.string(),
     }
     const strictInvocation: InvocationDescriptor = {
       ...invocation('@fixture/remote#strict'),
@@ -539,12 +545,12 @@ describe('TypertRegistry', () => {
       }, 'repeats wire field'],
       [{
         ...invocation(),
-        result: { mode: 'strict', typeSymbol: '', schema: z.string() },
+        result: { mode: 'strict', typeSymbol: '', create: () => z.string() },
       }, 'type symbol'],
       [{
         ...invocation(),
-        result: { mode: 'strict', typeSymbol: '@fixture#Broken', schema: {} as z.ZodType },
-      }, 'has no parse'],
+        result: { mode: 'strict', typeSymbol: '@fixture#Broken', create: {} as () => z.ZodType },
+      }, 'has no create'],
     ]
     for (const [index, [descriptor, message]] of malformed.entries()) {
       expect(() => ctx.typert.remotes.register({