@deepseek-ai/dsh-typert-generator 0.1.1-rc.2 → 0.1.2-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/typert/generator/README.md
5
- README.md: 06fcc69ce860a7cd0e893289ba59835ef3e160a7
6
- README.zh.md: 66b90c946157154741248d50fe95a194e5d39ad3
5
+ README.md: c6a419467f8f9da71c789dee2b969f69841b57cc
6
+ README.zh.md: 196af16d97c9c6eb2774e615840bfaed2454e7f2
package/README.md CHANGED
@@ -1,41 +1,139 @@
1
+ ---
2
+ description: "The build-time Typert generator: source type analysis, compiler-independent models, and artifact emission for maintainers wiring Typert publication or consuming generated artifacts."
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-typert-generator
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- TypeScript project analyzer and model-driven Typert generator. It converts the developer-authored source type tree into compiler-independent `FaceModel` and `TypeGraph` data before any artifact is rendered. Static analysis can consume that model without Cordis; emitters never receive TypeScript AST or checker objects.
10
+ ## Summary
11
+
12
+ `dsh-typert-generator` turns source TypeScript into compiler-independent data and runnable artifacts at build time: it analyzes a workspace's package type trees, produces a `FaceModel` and type graph, and emits executable JavaScript with supported Zod schemas and a `TYPERT` reflection contribution, plus matching declarations. It is a build-time library, not a plugin — it never runs inside a live agent session. The repository's Host tsdown runs it automatically; a business package opts in by exporting `./typert` and `./client/typert` entries, and the generator validates those exports and published file lists. Static consumers can also call the analyzer directly for type inspection or catalog generation without publishing anything.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ This package is for package and repository maintainers who wire Typert generation into a build or consume generated artifacts. Publication is opt-in: declare the export entries, run the build, and the artifacts appear in `lib/`; static analysis needs no artifact.
29
+
30
+ ### Publishing Typert artifacts from a package
31
+
32
+ A contributing package declares the host-face artifact export in `package.json`; packages that contribute both faces also declare `./client/typert`, and packages with Remote methods declare `./remote`:
33
+
34
+ ```yaml
35
+ exports:
36
+ "./typert":
37
+ types: "./lib/typert.host.d.ts"
38
+ default: "./lib/typert.host.js"
39
+ files:
40
+ - "lib/typert.host.js"
41
+ - "lib/typert.host.d.ts"
42
+ ```
43
+
44
+ After the build, `lib/typert.host.js` and `lib/typert.host.d.ts` exist and the [loader](../loader/README.md) registers the contribution in Loader compositions. The generated declaration file exposes `TYPERT` as `unknown`, so contributing packages never depend on the runtime registry. The generator fails the build when a declaration is missing, points at the wrong file, or publishes Remote artifacts without Remote methods; unsupported Zod projections fail with a `TypertEmitError` naming the construct instead of flattening or weakening the source type.
45
+
46
+ ### Analyzing a workspace statically
47
+
48
+ Static consumers call `WorkspaceAnalyzer` directly against the workspace's `tsconfig.host.json` and `tsconfig.client.json` aggregates, select a face and package subset, and read the resulting `FaceModel` and type graph without emitting or loading runtime artifacts. `analyzeInBatches()` processes a large package selection through bounded compiler programs with the same model shape, and `discoverPackages()` finds contributing packages without building a type-checker program.
49
+
50
+ ### Running generation inside a tsdown build
51
+
52
+ The package's `./tsdown` subpath provides `typertPlugin()` for the root tsdown config: it lowers standard decorators in TypeScript dependencies before bundling and emits the model-driven face artifacts at the package output root. In `package` mode it emits only the bundled package; in `workspace` mode it emits every explicit contributor once.
53
+
54
+ -----
55
+
56
+ <a id="understand-the-implementation"></a>
57
+ ## Understand the implementation
6
58
 
7
- The analyzer can use independent `ts.Program` instances seeded from `tsconfig.host.json` or `tsconfig.client.json`. Direct project references establish compiler-face membership, while package subpaths establish Typert runtime-face contributions: an ordinary single-project package declaring `dsh.client` may contribute both Host and Client runtime models, and only a split project explicitly referenced through `tsconfig.host.json` or `tsconfig.client.json` is restricted to that corresponding face. `package.json#exports` establishes every cross-package public boundary, and source imports or re-exports are the only allowed cross-face edges. Types owned by NPM dependencies, including global declarations from `@types` packages, remain `external` references instead of being expanded.
59
+ <details>
60
+ <summary>Implementation internals — click to expand</summary>
8
61
 
9
- ## Analysis Model
62
+ This section explains how the generator reaches a compiler-independent model and what it emits; the observable build behavior is covered in [Use this package](#use-this-package).
10
63
 
11
- Each face contains package exports, Cordis services and events, explicitly tagged objects and schemas, and a type graph for their reachable declarations. The graph preserves declaration identity, generic parameters and applications, explicit inheritance, conditional and mapped types, import attributes, abstract modifiers, and source JSDoc. Service and `@typert object` APIs expose public instance members only; constructors, static members, and non-public members are excluded.
64
+ ### Design concept
12
65
 
13
- `WorkspaceAnalyzer` defaults to `check` mode and fails on TypeScript syntax or semantic diagnostics, missing reachable public annotations, private cross-package references, and reachable declaration merges that the model cannot retain losslessly. `write` mode inserts checker-derived annotations, rebuilds the program, and returns a clean check-mode model.
66
+ The generator is built on one separation: extraction and emission are decoupled through the compiler-independent model. `WorkspaceAnalyzer` reads TypeScript programs seeded from the face aggregate tsconfigs and produces `FaceModel` and `TypeGraph` data; `FaceModelEmitter` consumes only that model and never receives compiler nodes. The model retains declaration identity, generic parameters and applications, explicit inheritance, conditional and mapped types, import attributes, abstract modifiers, and source JSDoc, and excludes constructors, static members, and non-public members.
14
67
 
15
- ## Emission and Opt-in Publication
68
+ ### Source map
16
69
 
17
- `FaceModelEmitter` consumes only the model. It emits executable JavaScript containing supported Zod schemas and a `TYPERT` contribution, plus a declaration file whose schemas are typed as `z.ZodType<SourceType>` through the package's public export. Unsupported Zod projections fail instead of flattening or weakening the source type.
70
+ | File | Role |
71
+ |---|---|
72
+ | [`src/index.ts`](src/index.ts) | Public API: analyzer, emitter, workspace generator, renderer, catalog projection |
73
+ | [`src/analyzer.ts`](src/analyzer.ts) | `WorkspaceAnalyzer`: face programs, check/write modes, batching, discovery, source index |
74
+ | [`src/model.ts`](src/model.ts) | Compiler-independent model types |
75
+ | [`src/emitter.ts`](src/emitter.ts) | `FaceModelEmitter`: Zod schema and declaration emission, Remote declarations |
76
+ | [`src/workspace.ts`](src/workspace.ts) | `WorkspaceTypertGenerator`: discovery, generation, export and files validation |
77
+ | [`src/tsdown-plugin.ts`](src/tsdown-plugin.ts) | tsdown plugin face: decorator lowering and artifact emission |
78
+ | [`src/cordis-catalog.ts`](src/cordis-catalog.ts) | Catalog projection used by the generated Cordis catalogs |
18
79
 
19
- `WorkspaceTypertGenerator` discovers contributors by walking package public exports reachable from Cordis `Context` or `Events` augmentations and explicit `@typert` declarations. When invoked for artifact publication, it requires host artifacts at `lib/typert.host.{js,d.ts}` exposed as `package/typert`, and client artifacts at `lib/typert.client.{js,d.ts}` exposed as `package/client/typert`. Generated declarations expose `TYPERT` as `unknown`, so contributing business packages do not depend on the runtime registry.
80
+ ### Analysis and faces
20
81
 
21
- Publication is package opt-in, and business packages without the corresponding public entry do not need Typert artifacts. The repository's Host tsdown runs workspace Typert generation with `tsconfig.host.json` as its only program seed; it produces both Host reflection artifacts and the `typert.remote-client.*` projection of Host Remote contracts for the Client. The subsequent Client tsdown neither starts Typert nor analyzes `tsconfig.client.json`. Static consumers can still call `WorkspaceAnalyzer` directly, explicitly select a face and package subset, and process packages in batches without publishing or loading runtime artifacts.
82
+ Host and client are independent TypeScript programs. Direct project references establish compiler-face membership, while `dsh.client` package subpaths establish runtime-face contribution; `package.json#exports` marks every cross-package public boundary, and imports or re-exports are the only cross-face edges. `check` mode fails on syntax or semantic diagnostics, missing public annotations, private cross-package references, and reachable declaration merges the model cannot retain losslessly; `write` mode inserts checker-derived annotations and returns a clean check-mode model. Types owned by NPM dependencies remain `external` references instead of being expanded.
22
83
 
23
- ## Repository-specific Cordis projection
84
+ ### Emission and publication contract
24
85
 
25
- The root package export includes the model-driven extraction, completeness checks, and deterministic text renderers used by this repository's Cordis catalogs. They accept a `CordisCatalogPolicy`; repository-owned type links, foundation/exemption classifications, and inherited Cordis entries remain in `scripts/gen-cordis-catalog.ts` and are passed in explicitly. The generator package therefore contains projection mechanics, not a hidden copy of this repository's documentation taxonomy.
86
+ `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.
26
87
 
88
+ ### Catalog projection
89
+
90
+ The root export includes the model-driven extraction, completeness checks, and deterministic text renderers used by this repository's Cordis catalogs. They accept a `CordisCatalogPolicy`; repository-owned type links, foundation and exemption classifications, and inherited Cordis entries stay in `scripts/gen-cordis-catalog.ts` and are passed in explicitly, so this package contains projection mechanics, not a hidden copy of the repository's documentation taxonomy.
91
+
92
+ </details>
93
+
94
+ -----
95
+
96
+ <a id="further-exploration"></a>
97
+ ## Further Exploration
98
+
99
+ Read these pages when the package-level contract is not enough; they move from the generated model to the runtime and the Remote-call path.
100
+
101
+ - [Typert subsystem reference](../../../docs/subsystems/typert.md) — the Remote contracts and registry interfaces the generator models.
102
+ - [Typert protocol](../protocol/README.md) — the declarations generated artifacts extend and consume.
103
+ - [Typert registry](../registry/README.md) — the runtime store the emitted artifacts feed.
104
+ - [API Gateway reference](../../../docs/api-gateway.md) — how generated Remote descriptors are invoked end to end.
105
+ - [Compiler-independent model Agent Note](../../../.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md) — the model design, alternatives, and consequences.
106
+
107
+ -----
108
+
109
+ <a id="model-experience"></a>
27
110
  ## Model Experience
28
111
 
29
- None, as this package runs at build or test time and never contributes to a model request.
112
+ None, as the build-time generator runs outside any agent runtime and touches no model request.
30
113
 
31
114
  #### KV Cache effect
32
115
 
33
- None.
116
+ No direct effect; generated artifacts reach a request only when a consumer places them in one.
34
117
 
35
118
  ## Known Limitations and Deferred Work
36
119
 
37
- - Package export patterns are skipped; contributing packages need concrete export targets.
38
- - Cross-face named and star re-exports produce links; namespace re-exports fail until `TypeTargetModel` can represent a module namespace without flattening it.
39
- - The Zod emitter supports a deliberate subset of the modeled TypeScript graph. Generic schema declarations and computed constructs such as conditional or mapped schema roots fail until a concrete schema-factory policy exists.
40
- - Cross-face links are represented for analysis, but no generated schema currently requires a runtime cross-face Zod import.
41
- - Discovery follows source files reachable from concrete public exports; declarations that are neither exported nor imported by that graph are intentionally outside the package model.
120
+ <a id="known-limitations-and-deferred-work"></a>
121
+
122
+
123
+ These limits define what the generator cannot model or emit; they are current package constraints, not a task backlog.
124
+
125
+ - **Package export patterns are skipped** — contributing packages need concrete export targets; wildcard export patterns are not analyzed.
126
+ - **Namespace re-exports across faces fail** — named and star re-exports produce links, but a namespace re-export cannot be represented until `TypeTargetModel` can model a module namespace without flattening it.
127
+ - **The Zod emitter supports a deliberate subset** — generic schema declarations and computed constructs such as conditional or mapped schema roots fail until a concrete schema-factory policy exists.
128
+ - **No generated schema imports across faces** — cross-face links are represented for analysis, but no generated schema requires a runtime cross-face Zod import.
129
+ - **Discovery covers concrete public exports only** — declarations neither exported nor imported by the reachable graph are intentionally outside the package model.
130
+
131
+ <a id="dev-note"></a>
132
+ ### Dev Note
133
+
134
+ <details>
135
+ <summary>Working context for maintainers — click to expand</summary>
136
+
137
+ None.
138
+
139
+ </details>
package/README.zh.md CHANGED
@@ -1,41 +1,139 @@
1
+ ---
2
+ description: "构建时 Typert 生成器:源代码类型分析、与编译器无关的模型与产物生成,供接入 Typert 发布或消费生成产物的维护者阅读。"
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-typert-generator
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- TypeScript 项目分析器和模型驱动的 Typert 生成器。在生成任何产物之前,它会先将开发者编写的源类型树转换为独立于编译器的 `FaceModel` 和 `TypeGraph` 数据。静态分析无需 Cordis 即可消费该模型;各产物生成组件均不会接收 TypeScript 抽象语法树(AST)或类型检查器对象。
10
+ ## 概述
11
+
12
+ `dsh-typert-generator` 在构建时把源代码 TypeScript 转换为与编译器无关的数据与可运行产物:它分析工作区各包的类型树,生成 `FaceModel` 与类型图,并输出包含受支持 Zod schema 与 `TYPERT` 反射贡献的可执行 JavaScript,以及配套声明文件。它是构建时库而非插件——绝不会在实时 agent 会话中运行。仓库的 Host tsdown 会自动运行它;业务包通过导出 `./typert` 与 `./client/typert` 入口选择加入,生成器会校验这些导出与发布文件清单。静态消费方也可以直接调用分析器进行类型检查或目录生成,无需发布任何内容。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 本包供把 Typert 生成接入构建或消费生成产物的包维护者与仓库维护者使用。发布是选择加入的:声明导出入口、运行构建,产物即出现在 `lib/` 中;静态分析则不需要产物。
29
+
30
+ ### 从包中发布 Typert 产物
31
+
32
+ 参与贡献的包在 `package.json` 中声明宿主侧产物导出;同时贡献两侧的包还要声明 `./client/typert`,含 Remote 方法的包还要声明 `./remote`:
33
+
34
+ ```yaml
35
+ exports:
36
+ "./typert":
37
+ types: "./lib/typert.host.d.ts"
38
+ default: "./lib/typert.host.js"
39
+ files:
40
+ - "lib/typert.host.js"
41
+ - "lib/typert.host.d.ts"
42
+ ```
43
+
44
+ 构建完成后,`lib/typert.host.js` 与 `lib/typert.host.d.ts` 即存在,[loader](../loader/README.zh.md) 会在 Loader 组合中注册该贡献。生成的声明文件把 `TYPERT` 暴露为 `unknown`,因此参与贡献的包永远不会依赖运行时注册表。当声明缺失、指向错误文件,或在没有 Remote 方法的情况下发布 Remote 产物时,生成器会使构建失败;不支持的 Zod 投影会以 `TypertEmitError` 指明具体构造并失败,而不会展平或弱化源类型。
45
+
46
+ ### 静态分析工作区
47
+
48
+ 静态消费方直接以工作区的 `tsconfig.host.json` 与 `tsconfig.client.json` aggregate 调用 `WorkspaceAnalyzer`,选择 face 与包子集,并在不生成或加载运行时产物的前提下读取生成的 `FaceModel` 与类型图。`analyzeInBatches()` 通过有界的编译器程序处理大批量包选择,模型形态保持一致;`discoverPackages()` 无需构建类型检查程序即可找出参与贡献的包。
49
+
50
+ ### 在 tsdown 构建中运行生成
51
+
52
+ 包的 `./tsdown` 子路径为根 tsdown 配置提供 `typertPlugin()`:它在打包前降低 TypeScript 依赖中的标准装饰器,并在包输出根目录生成模型驱动的 face 产物。`package` 模式只生成当前打包的包;`workspace` 模式对每个显式贡献方各生成一次。
53
+
54
+ -----
55
+
56
+ <a id="understand-the-implementation"></a>
57
+ ## 理解实现
6
58
 
7
- 分析器可以分别使用由 `tsconfig.host.json` 或 `tsconfig.client.json` 初始化的独立 `ts.Program`。直接项目引用确定编译器 face 的成员归属,而包子路径确定 Typert 运行时 face 的贡献:声明 `dsh.client` 的普通单项目包可以同时贡献 Host 与 Client 运行时模型;只有通过 `tsconfig.host.json` 或 `tsconfig.client.json` 显式引用的拆分项目,才会被限制在相应 face。`package.json#exports` 确定所有跨包公开边界,跨 face 的边只能来自源码导入或重新导出。NPM 依赖拥有的类型(包括 `@types` 包中的全局声明)继续以 `external` 引用表示,不会被展开。
59
+ <details>
60
+ <summary>实现细节——点击展开</summary>
8
61
 
9
- ## 分析模型
62
+ 本节解释生成器如何得到与编译器无关的模型以及它生成什么;可观察的构建行为已在[使用本包](#use-this-package)中说明。
10
63
 
11
- 每个 face 包含包导出、Cordis 服务与事件、显式标记的对象与 schema,以及涵盖其可达声明的类型图。类型图保留声明标识、泛型参数及应用、显式继承、条件类型与映射类型、导入属性、abstract 修饰符和源码 JSDoc。服务和 `@typert object` 对外接口仅暴露公共实例成员;构造函数、静态成员与非公共成员均被排除。
64
+ ### 设计理念
12
65
 
13
- `WorkspaceAnalyzer` 默认采用 `check` 模式,遇到 TypeScript 语法或语义诊断、可达公开声明缺少类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时,分析会失败。`write` 模式会插入类型检查器推导出的类型标注,重建该程序,并返回无诊断的检查模式模型。
66
+ 生成器建立在一个分离之上:提取与生成通过与编译器无关的模型解耦。`WorkspaceAnalyzer` 读取以 face aggregate tsconfig 为种子的 TypeScript 程序,产出 `FaceModel` 与 `TypeGraph` 数据;`FaceModelEmitter` 只消费该模型,绝不接收编译器节点。模型保留声明标识、泛型参数及应用、显式继承、条件类型与映射类型、导入属性、abstract 修饰符与源码 JSDoc,并排除构造函数、静态成员与非公共成员。
14
67
 
15
- ## 产物生成与选择性发布
68
+ ### 源码地图
16
69
 
17
- `FaceModelEmitter` 只消费模型。它会生成可执行 JavaScript,其中包含受支持的 Zod schema 和一个 `TYPERT` contribution;同时生成声明文件,通过包的公开导出将其中的 schema 标注为 `z.ZodType<SourceType>`。遇到不支持的 Zod 投影时,生成会失败,不会展平或弱化源类型。
70
+ | 文件 | 职责 |
71
+ |---|---|
72
+ | [`src/index.ts`](src/index.ts) | 公共 API:分析器、生成器、工作区生成器、渲染器、目录投影 |
73
+ | [`src/analyzer.ts`](src/analyzer.ts) | `WorkspaceAnalyzer`:face 程序、check/write 模式、分批、发现、源码索引 |
74
+ | [`src/model.ts`](src/model.ts) | 与编译器无关的模型类型 |
75
+ | [`src/emitter.ts`](src/emitter.ts) | `FaceModelEmitter`:Zod schema 与声明生成、Remote 声明 |
76
+ | [`src/workspace.ts`](src/workspace.ts) | `WorkspaceTypertGenerator`:发现、生成、导出与文件清单校验 |
77
+ | [`src/tsdown-plugin.ts`](src/tsdown-plugin.ts) | tsdown 插件面:装饰器降低与产物生成 |
78
+ | [`src/cordis-catalog.ts`](src/cordis-catalog.ts) | 生成 Cordis 目录所用的目录投影 |
18
79
 
19
- `WorkspaceTypertGenerator` 会遍历从 Cordis `Context` 或 `Events` 扩充声明及显式 `@typert` 声明可达的包公开导出,以发现贡献方。发布产物时,它要求宿主侧产物位于 `lib/typert.host.{js,d.ts}` 并以 `package/typert` 暴露,客户端侧产物位于 `lib/typert.client.{js,d.ts}` 并以 `package/client/typert` 暴露。生成的声明将 `TYPERT` 暴露为 `unknown`,因此参与贡献的业务包无需依赖运行时注册表。
80
+ ### 分析与 face
20
81
 
21
- 各包可自行选择是否发布,未提供对应公开入口的业务包无需生成 Typert 产物。仓库的 Host tsdown 会以 `tsconfig.host.json` 为唯一 program 种子运行 workspace Typert 生成;它既生成 Host 反射产物,也把 Host Remote 约定投影为 Client 使用的 `typert.remote-client.*`。后续 Client tsdown 不启动 Typert,也不分析 `tsconfig.client.json`。静态消费方仍可直接调用 `WorkspaceAnalyzer`,显式选择 face 与包子集,并在不发布或加载运行时产物的情况下分批处理包。
82
+ Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face 的边只能来自导入或重新导出。`check` 模式遇到语法或语义诊断、缺失的公开类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时都会失败;`write` 模式插入类型检查器推导出的标注,并返回无诊断的 check 模式模型。NPM 依赖拥有的类型继续以 `external` 引用表示,不会被展开。
22
83
 
23
- ## 本仓库的 Cordis 投影
84
+ ### 生成与发布约定
24
85
 
25
- 包根导出中包含本仓库 Cordis 目录使用的模型驱动提取逻辑、完整性检查和确定性文本渲染器。它们接受 `CordisCatalogPolicy`;由仓库持有的类型链接、基础类型/豁免类型分类和继承的 Cordis 条目仍位于 `scripts/gen-cordis-catalog.ts`,并由调用方显式传入。因此,生成器包只包含投影机制,不会隐式复制本仓库的文档分类体系。
86
+ `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` 清单必须包含它们。
26
87
 
88
+ ### 目录投影
89
+
90
+ 根导出包含本仓库 Cordis 目录使用的模型驱动提取逻辑、完整性检查与确定性文本渲染器。它们接受 `CordisCatalogPolicy`;由仓库持有的类型链接、基础类型/豁免分类与继承的 Cordis 条目仍位于 `scripts/gen-cordis-catalog.ts`,由调用方显式传入,因此本包只包含投影机制,不会隐式复制仓库的文档分类体系。
91
+
92
+ </details>
93
+
94
+ -----
95
+
96
+ <a id="further-exploration"></a>
97
+ ## 进一步探索
98
+
99
+ 当包级约定不够用时阅读以下页面;它们从生成模型逐步进入运行时与 Remote 调用路径。
100
+
101
+ - [Typert 子系统参考](../../../docs/subsystems/typert.zh.md)——生成器建模的 Remote 约定与注册表接口。
102
+ - [Typert 协议](../protocol/README.zh.md)——生成产物所扩展并消费的声明。
103
+ - [Typert 注册表](../registry/README.zh.md)——生成产物所供给的运行时存储。
104
+ - [API Gateway 参考](../../../docs/api-gateway.zh.md)——生成的 Remote 描述符如何端到端被调用。
105
+ - [Compiler-independent model Agent Note](../../../.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md)——模型设计、备选方案与后果。
106
+
107
+ -----
108
+
109
+ <a id="model-experience"></a>
27
110
  ## 模型体验
28
111
 
29
- 无。该包仅在构建或测试时运行,不会向模型请求添加任何内容。
112
+ 无,因为构建时生成器在任何 agent 运行时之外运行,不触及任何模型请求。
30
113
 
31
114
  #### KV Cache 影响
32
115
 
33
- 无。
116
+ 无直接影响;生成产物只有在消费方将其放入请求时才会触及请求。
34
117
 
35
- ## 已知限制与暂缓事项
118
+ ## 已知限制与延期工作
119
+
120
+ <a id="known-limitations-and-deferred-work"></a>
121
+
122
+
123
+ 这些限制说明生成器无法建模或生成的构造;它们是当前包约束,不是任务积压。
124
+
125
+ - **包导出模式会被跳过**——参与贡献的包需要具体的导出目标;通配符导出模式不会被分析。
126
+ - **跨 face 的命名空间重新导出会失败**——具名与星号重新导出会生成链接,但在 `TypeTargetModel` 能够不经展平表示模块命名空间之前,命名空间重新导出无法表示。
127
+ - **Zod 生成器只支持有意限定的子集**——泛型 schema 声明,以及以条件类型或映射类型为 schema 根的计算构造,都会失败,直到存在明确的 schema 工厂策略。
128
+ - **没有生成的 schema 跨 face 导入**——跨 face 链接会在模型中表示以供分析,但生成的 schema 均不需要跨 face 的运行时 Zod 导入。
129
+ - **发现范围只覆盖具体公开导出**——既未导出、也未由可达图导入的声明按设计排除在包模型之外。
130
+
131
+ <a id="dev-note"></a>
132
+ ### 开发备注
133
+
134
+ <details>
135
+ <summary>维护者的工作上下文——点击展开</summary>
136
+
137
+ 无。
36
138
 
37
- - 系统会跳过包导出中的模式匹配;参与贡献的包需要具体的导出目标。
38
- - 跨 face 的具名重新导出和星号重新导出会生成链接;在 `TypeTargetModel` 能够不经展平便表示模块命名空间之前,命名空间重新导出会失败。
39
- - Zod 产物生成组件仅支持 TypeScript 类型图中有意限定的部分。泛型 schema 声明,以及以条件类型或映射类型为 schema 根的计算构造,都会失败,直到存在明确的 schema 工厂策略。
40
- - 跨 face 链接会在模型中表示以供分析,但当前生成的 schema 均不需要跨 face 的运行时 Zod 导入。
41
- - 发现过程会遍历从具体公开导出可达的源文件;既未导出、也未由该图导入的声明会按设计排除在包模型之外。
139
+ </details>
package/lib/index.js CHANGED
@@ -10,6 +10,7 @@ import { GenMapping, addMapping, toEncodedMap } from "@jridgewell/gen-mapping";
10
10
  * details; callers receive only the model declared in {@link ./model.ts}.
11
11
  * @module @deepseek-ai/dsh-typert-generator/analyzer
12
12
  */
13
+ const PUBLIC_REMOTE_TYPE_ROOTS = new Set(["@deepseek-ai/dsh-util-values"]);
13
14
  /** Analysis failure with a source-oriented diagnostic. */
14
15
  var TypertAnalysisError = class extends Error {
15
16
  name = "TypertAnalysisError";
@@ -705,13 +706,15 @@ var FaceAnalyzer = class {
705
706
  };
706
707
  }
707
708
  }
708
- const resultType = this.remoteResultType(method);
709
+ const mode = invocation.kind === "direct" ? invocation.mode : void 0;
710
+ const resultType = this.remoteResultType(method, mode);
709
711
  return {
710
712
  id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
711
713
  service: binding.service,
712
714
  namespace: binding.namespace,
713
715
  method: exportedMethod,
714
716
  ...exportedMethod === methodName ? {} : { implementation: methodName },
717
+ ...mode === void 0 ? {} : { mode },
715
718
  invocation: receiver,
716
719
  ...scope === void 0 ? {} : { scope },
717
720
  parameters,
@@ -781,13 +784,26 @@ var FaceAnalyzer = class {
781
784
  let marker;
782
785
  if (this.isTypeMetaSymbol(expression, "Remote")) marker = { kind: "direct" };
783
786
  else if (ts.isCallExpression(expression) && this.isTypeMetaSymbol(expression.expression, "Remote")) {
784
- if (expression.arguments.length !== 1) this.fail(expression, "Remote() requires one exported method name");
785
- const exportName = stringLiteralValue(expression.arguments[0]);
786
- if (exportName === void 0 || !isRemoteSegment(exportName)) this.fail(expression.arguments[0] ?? expression, "Remote() name must be a string literal containing only RPC endpoint segment characters");
787
- marker = {
788
- kind: "direct",
789
- exportName
790
- };
787
+ if (expression.arguments.length !== 1) this.fail(expression, "Remote() requires one name or options object");
788
+ const argument = expression.arguments[0];
789
+ if (argument === void 0) this.fail(expression, "Remote() requires one name or options object");
790
+ const exportName = stringLiteralValue(argument);
791
+ if (exportName !== void 0) {
792
+ if (!isRemoteSegment(exportName)) this.fail(argument, "Remote() name must contain only RPC endpoint segment characters");
793
+ marker = {
794
+ kind: "direct",
795
+ exportName
796
+ };
797
+ } else {
798
+ if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
799
+ const [property] = argument.properties;
800
+ if (property === void 0) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
801
+ if (!ts.isPropertyAssignment(property) || memberName(property.name) !== "mode" || stringLiteralValue(property.initializer) !== "stream") this.fail(property, "Remote() options must contain exactly mode: \"stream\"");
802
+ marker = {
803
+ kind: "direct",
804
+ mode: "stream"
805
+ };
806
+ }
791
807
  } else if (ts.isCallExpression(expression) && this.isTypeMetaSymbol(expression.expression, "RemoteScope")) {
792
808
  if (expression.arguments.length < 1 || expression.arguments.length > 2) this.fail(expression, "RemoteScope() requires a Context key and optional exported method name");
793
809
  const context = stringLiteralValue(expression.arguments[0]);
@@ -806,16 +822,18 @@ var FaceAnalyzer = class {
806
822
  }
807
823
  return found;
808
824
  }
809
- remoteResultType(method) {
825
+ remoteResultType(method, mode) {
810
826
  const authored = this.requiredType(method, method.type, "return");
811
- if (!ts.isTypeReferenceNode(authored)) return authored;
812
- const symbol = this.checker.getSymbolAtLocation(authored.typeName);
813
- const resolved = symbol === void 0 ? void 0 : this.resolveSymbol(symbol);
814
- const resultType = authored.typeArguments?.[0];
815
- if (resolved?.name !== "Promise" || resultType === void 0 || authored.typeArguments?.length !== 1) return authored;
816
- const declaration = preferredDeclaration(resolved);
817
- if (declaration === void 0 || !isStandardLibraryFile(declaration.getSourceFile().fileName)) return authored;
818
- return resultType;
827
+ if (ts.isTypeReferenceNode(authored)) {
828
+ const symbol = this.checker.getSymbolAtLocation(authored.typeName);
829
+ const resolved = symbol === void 0 ? void 0 : this.resolveSymbol(symbol);
830
+ const resultType = authored.typeArguments?.[0];
831
+ const wrappers = mode === "stream" ? ["Iterable", "AsyncIterable"] : ["Promise"];
832
+ const declaration = resolved === void 0 ? void 0 : preferredDeclaration(resolved);
833
+ if (resolved !== void 0 && wrappers.includes(resolved.name) && resultType !== void 0 && authored.typeArguments?.length === 1 && declaration !== void 0 && isStandardLibraryFile(declaration.getSourceFile().fileName)) return resultType;
834
+ }
835
+ if (mode === "stream") this.fail(method, "stream Remote methods must return Iterable<T> or AsyncIterable<T>");
836
+ return authored;
819
837
  }
820
838
  isGlobalAbortSignal(type) {
821
839
  const symbol = this.symbolAtType(type);
@@ -1247,7 +1265,7 @@ var FaceAnalyzer = class {
1247
1265
  if (registration === void 0) this.fail(site, `type ${symbol.name} is not owned by a workspace package`);
1248
1266
  const candidates = [];
1249
1267
  for (const [subpath, target] of packageExportTargets(registration.manifest)) {
1250
- if (subpath === "." || subpath === "./package.json" || subpath === "./typert" || subpath === "./client/typert" || subpath === "./remote" || target.includes("*")) continue;
1268
+ if (subpath === "." && !PUBLIC_REMOTE_TYPE_ROOTS.has(registration.name) || subpath === "./package.json" || subpath === "./typert" || subpath === "./client/typert" || subpath === "./remote" || target.includes("*")) continue;
1251
1269
  const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
1252
1270
  if (sourceFile === void 0) continue;
1253
1271
  const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
@@ -2783,6 +2801,7 @@ var FaceModelEmitter = class {
2783
2801
  ` method: ${quote$1(invocation.method)},`
2784
2802
  ];
2785
2803
  if (invocation.implementation !== void 0) lines.push(` implementation: ${quote$1(invocation.implementation)},`);
2804
+ if (invocation.mode !== void 0) lines.push(` mode: ${quote$1(invocation.mode)},`);
2786
2805
  if (invocation.invocation.kind === "direct") lines.push(" invocation: { kind: 'direct' },");
2787
2806
  else {
2788
2807
  lines.push(" invocation: {");
@@ -2912,6 +2931,7 @@ var FaceModelEmitter = class {
2912
2931
  const parameters = invocation.parameters.filter((parameter) => !scoped || invocation.invocation.kind === "context" || parameter.wire !== invocation.scope?.wire).map((parameter) => `${safeIdentifier(parameter.wire)}${parameter.optional === true ? "?" : ""}: ${this.renderer.renderType(parameter.boundary.type, referenceNames)}`);
2913
2932
  if (invocation.cancellation !== void 0) parameters.push("signal?: AbortSignal");
2914
2933
  const result = this.renderer.renderType(invocation.result.type, referenceNames);
2934
+ if (invocation.mode === "stream") return `(${parameters.join(", ")}) => AsyncIterable<${result}>`;
2915
2935
  return `(${parameters.join(", ")}) => Promise<RemoteResult<${result}>>`;
2916
2936
  }
2917
2937
  };
@@ -3824,7 +3844,7 @@ function renderRuntimeApi(services, events, types, inheritedServices) {
3824
3844
  }
3825
3845
  lines.push("]", "", "/** The inherited `ctx` API (cordis core + loader/hmr/timer), in curated order. */", "export const INHERITED_CTX_API: readonly InheritedApiEntry[] = [");
3826
3846
  for (const inherited of inheritedServices) lines.push(` { name: ${quote(inherited.name)}, summary: ${quote(inherited.summary)} },`);
3827
- lines.push("]", "", "function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {", " const included = new Set<string>()", " let frontier = [...seeds]", " while (frontier.length > 0) {", " const next: string[] = []", " for (const entry of TYPE_API) {", " if (included.has(entry.name)) continue", " const pattern = new RegExp(`\\b${entry.name}\\b`)", " if (!frontier.some(text => pattern.test(text))) continue", " included.add(entry.name)", " next.push(entry.declaration)", " }", " frontier = next", " }", " return TYPE_API.filter(entry => included.has(entry.name))", "}", "", "function contextProperty(key: string): string {", " return /^[A-Za-z_$][\\w$]*$/.test(key) ? `ctx.${key}` : `ctx[${JSON.stringify(key)}]`", "}", "", "/**", " * Project the Service Catalog as a compact directory or one exact coding contract.", " * @param key - exact Service key; omit it to list all Services and method signatures.", " * @param services - platform-specific visible Service entries.", " * @returns compact navigation data or one detailed Service with its referenced type closure.", " */", "export function queryServiceApi(key?: string, services: readonly ServiceApiEntry[] = SERVICE_API): object {", " if (key === undefined) {", " return {", " mode: 'catalog',", " services: services.map(service => ({", " key: service.key,", " description: service.summary,", " methods: service.methods.map(method => ({ signature: method.signature })),", " })),", " }", " }", " const service = services.find(candidate => candidate.key === key)", " if (service === undefined) throw new Error(`no catalogued Service named \"${key}\"`)", " return {", " mode: 'service',", " service: {", " key: service.key,", " description: service.description,", " access: {", " optional: { expression: `ctx.get(${JSON.stringify(service.key)})`, requiresUndefinedCheck: true },", " hardDependency: { inject: [service.key], expression: contextProperty(service.key) },", " },", " methods: service.methods,", " },", " referencedTypes: referencedTypeClosure(service.methods.map(method => method.signature)),", " }", "}", "", "/**", " * Project the Event Catalog as a compact directory or one exact listener contract.", " * @param name - exact Event name; omit it to list all Events and listener signatures.", " * @param events - platform-specific visible Event entries.", " * @returns compact navigation data or one detailed Event with its referenced type closure.", " */", "export function queryEventApi(name?: string, events: readonly EventApiEntry[] = EVENT_API): object {", " if (name === undefined) {", " return {", " mode: 'catalog',", " events: events.map(event => ({", " name: event.name,", " description: event.summary,", " mode: event.mode,", " signature: event.signature,", " })),", " }", " }", " const event = events.find(candidate => candidate.name === name)", " if (event === undefined) throw new Error(`no catalogued Event named \"${name}\"`)", " return {", " mode: 'event',", " event: {", " name: event.name,", " description: event.description,", " mode: event.mode,", " signature: event.signature,", " parameters: event.parameters,", " },", " referencedTypes: referencedTypeClosure([event.signature]),", " }", "}", "/* jscpd:ignore-end */", "");
3847
+ lines.push("]", "", "function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {", " const included = new Set<string>()", " let frontier = [...seeds]", " while (frontier.length > 0) {", " const next: string[] = []", " for (const entry of TYPE_API) {", " if (included.has(entry.name)) continue", " const pattern = new RegExp(`\\\\b${entry.name}\\\\b`)", " if (!frontier.some(text => pattern.test(text))) continue", " included.add(entry.name)", " next.push(entry.declaration)", " }", " frontier = next", " }", " return TYPE_API.filter(entry => included.has(entry.name))", "}", "", "function contextProperty(key: string): string {", " return /^[A-Za-z_$][\\w$]*$/.test(key) ? `ctx.${key}` : `ctx[${JSON.stringify(key)}]`", "}", "", "/**", " * Project the Service Catalog as a compact directory or one exact coding contract.", " * @param key - exact Service key; omit it to list all Services and method signatures.", " * @param services - platform-specific visible Service entries.", " * @returns compact navigation data or one detailed Service with its referenced type closure.", " */", "export function queryServiceApi(key?: string, services: readonly ServiceApiEntry[] = SERVICE_API): object {", " if (key === undefined) {", " return {", " mode: 'catalog',", " services: services.map(service => ({", " key: service.key,", " description: service.summary,", " methods: service.methods.map(method => ({ signature: method.signature })),", " })),", " }", " }", " const service = services.find(candidate => candidate.key === key)", " if (service === undefined) throw new Error(`no catalogued Service named \"${key}\"`)", " return {", " mode: 'service',", " service: {", " key: service.key,", " description: service.description,", " access: {", " optional: { expression: `ctx.get(${JSON.stringify(service.key)})`, requiresUndefinedCheck: true },", " hardDependency: { inject: [service.key], expression: contextProperty(service.key) },", " },", " methods: service.methods,", " },", " referencedTypes: referencedTypeClosure(service.methods.map(method => method.signature)),", " }", "}", "", "/**", " * Project the Event Catalog as a compact directory or one exact listener contract.", " * @param name - exact Event name; omit it to list all Events and listener signatures.", " * @param events - platform-specific visible Event entries.", " * @returns compact navigation data or one detailed Event with its referenced type closure.", " */", "export function queryEventApi(name?: string, events: readonly EventApiEntry[] = EVENT_API): object {", " if (name === undefined) {", " return {", " mode: 'catalog',", " events: events.map(event => ({", " name: event.name,", " description: event.summary,", " mode: event.mode,", " signature: event.signature,", " })),", " }", " }", " const event = events.find(candidate => candidate.name === name)", " if (event === undefined) throw new Error(`no catalogued Event named \"${name}\"`)", " return {", " mode: 'event',", " event: {", " name: event.name,", " description: event.description,", " mode: event.mode,", " signature: event.signature,", " parameters: event.parameters,", " },", " referencedTypes: referencedTypeClosure([event.signature]),", " }", "}", "/* jscpd:ignore-end */", "");
3828
3848
  return lines.join("\n");
3829
3849
  }
3830
3850
  /** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */
@@ -3972,12 +3992,17 @@ function renderInheritedPage(policy) {
3972
3992
  /** Discover, analyze, and emit package reflection from independent faces. */
3973
3993
  var WorkspaceTypertGenerator = class {
3974
3994
  root;
3995
+ options;
3996
+ /** Parsed-config and program-host state shared by every analyzer this generator creates. */
3997
+ caches = new WorkspaceCaches();
3975
3998
  /**
3976
3999
  * Bind generation to one workspace root.
3977
4000
  * @param root - directory containing face aggregate tsconfigs.
4001
+ * @param options - behavior switches applied to every pass of this generator.
3978
4002
  */
3979
- constructor(root) {
4003
+ constructor(root, options = {}) {
3980
4004
  this.root = root;
4005
+ this.options = options;
3981
4006
  }
3982
4007
  /**
3983
4008
  * Find public package faces that contribute Cordis services/events or
@@ -3988,6 +4013,7 @@ var WorkspaceTypertGenerator = class {
3988
4013
  discover(faces) {
3989
4014
  return new WorkspaceAnalyzer({
3990
4015
  root: this.root,
4016
+ caches: this.caches,
3991
4017
  ...faces === void 0 ? {} : { faces }
3992
4018
  }).discoverPackages();
3993
4019
  }
@@ -4002,7 +4028,9 @@ var WorkspaceTypertGenerator = class {
4002
4028
  const workspace = new WorkspaceAnalyzer({
4003
4029
  root: this.root,
4004
4030
  packages: selected,
4005
- ...faces === void 0 ? {} : { faces }
4031
+ caches: this.caches,
4032
+ ...faces === void 0 ? {} : { faces },
4033
+ ...this.options.checkDiagnostics === void 0 ? {} : { checkDiagnostics: this.options.checkDiagnostics }
4006
4034
  }).analyze();
4007
4035
  const artifacts = [];
4008
4036
  for (const face of workspace.faces) {
@@ -7,6 +7,9 @@
7
7
  import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
8
8
  import { dirname, extname, join, relative, resolve, sep } from 'node:path';
9
9
  import ts from 'typescript';
10
+ const PUBLIC_REMOTE_TYPE_ROOTS = new Set([
11
+ '@deepseek-ai/dsh-util-values',
12
+ ]);
10
13
  /** Analysis failure with a source-oriented diagnostic. */
11
14
  export class TypertAnalysisError extends Error {
12
15
  name = 'TypertAnalysisError';
@@ -895,13 +898,15 @@ class FaceAnalyzer {
895
898
  scope = { context: context.key, wire: parameter.wire };
896
899
  }
897
900
  }
898
- const resultType = this.remoteResultType(method);
901
+ const mode = invocation.kind === 'direct' ? invocation.mode : undefined;
902
+ const resultType = this.remoteResultType(method, mode);
899
903
  return {
900
904
  id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
901
905
  service: binding.service,
902
906
  namespace: binding.namespace,
903
907
  method: exportedMethod,
904
908
  ...(exportedMethod === methodName ? {} : { implementation: methodName }),
909
+ ...(mode === undefined ? {} : { mode }),
905
910
  invocation: receiver,
906
911
  ...(scope === undefined ? {} : { scope }),
907
912
  parameters,
@@ -1009,12 +1014,31 @@ class FaceAnalyzer {
1009
1014
  else if (ts.isCallExpression(expression)
1010
1015
  && this.isTypeMetaSymbol(expression.expression, 'Remote')) {
1011
1016
  if (expression.arguments.length !== 1)
1012
- this.fail(expression, 'Remote() requires one exported method name');
1013
- const exportName = stringLiteralValue(expression.arguments[0]);
1014
- if (exportName === undefined || !isRemoteSegment(exportName)) {
1015
- this.fail(expression.arguments[0] ?? expression, 'Remote() name must be a string literal containing only RPC endpoint segment characters');
1017
+ this.fail(expression, 'Remote() requires one name or options object');
1018
+ const argument = expression.arguments[0];
1019
+ if (argument === undefined)
1020
+ this.fail(expression, 'Remote() requires one name or options object');
1021
+ const exportName = stringLiteralValue(argument);
1022
+ if (exportName !== undefined) {
1023
+ if (!isRemoteSegment(exportName)) {
1024
+ this.fail(argument, 'Remote() name must contain only RPC endpoint segment characters');
1025
+ }
1026
+ marker = { kind: 'direct', exportName };
1027
+ }
1028
+ else {
1029
+ if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) {
1030
+ this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
1031
+ }
1032
+ const [property] = argument.properties;
1033
+ if (property === undefined)
1034
+ this.fail(argument, 'Remote() options must contain exactly mode: "stream"');
1035
+ if (!ts.isPropertyAssignment(property)
1036
+ || memberName(property.name) !== 'mode'
1037
+ || stringLiteralValue(property.initializer) !== 'stream') {
1038
+ this.fail(property, 'Remote() options must contain exactly mode: "stream"');
1039
+ }
1040
+ marker = { kind: 'direct', mode: 'stream' };
1016
1041
  }
1017
- marker = { kind: 'direct', exportName };
1018
1042
  }
1019
1043
  else if (ts.isCallExpression(expression)
1020
1044
  && this.isTypeMetaSymbol(expression.expression, 'RemoteScope')) {
@@ -1041,19 +1065,27 @@ class FaceAnalyzer {
1041
1065
  }
1042
1066
  return found;
1043
1067
  }
1044
- remoteResultType(method) {
1068
+ remoteResultType(method, mode) {
1045
1069
  const authored = this.requiredType(method, method.type, 'return');
1046
- if (!ts.isTypeReferenceNode(authored))
1047
- return authored;
1048
- const symbol = this.checker.getSymbolAtLocation(authored.typeName);
1049
- const resolved = symbol === undefined ? undefined : this.resolveSymbol(symbol);
1050
- const resultType = authored.typeArguments?.[0];
1051
- if (resolved?.name !== 'Promise' || resultType === undefined || authored.typeArguments?.length !== 1)
1052
- return authored;
1053
- const declaration = preferredDeclaration(resolved);
1054
- if (declaration === undefined || !isStandardLibraryFile(declaration.getSourceFile().fileName))
1055
- return authored;
1056
- return resultType;
1070
+ if (ts.isTypeReferenceNode(authored)) {
1071
+ const symbol = this.checker.getSymbolAtLocation(authored.typeName);
1072
+ const resolved = symbol === undefined ? undefined : this.resolveSymbol(symbol);
1073
+ const resultType = authored.typeArguments?.[0];
1074
+ const wrappers = mode === 'stream' ? ['Iterable', 'AsyncIterable'] : ['Promise'];
1075
+ const declaration = resolved === undefined ? undefined : preferredDeclaration(resolved);
1076
+ if (resolved !== undefined
1077
+ && wrappers.includes(resolved.name)
1078
+ && resultType !== undefined
1079
+ && authored.typeArguments?.length === 1
1080
+ && declaration !== undefined
1081
+ && isStandardLibraryFile(declaration.getSourceFile().fileName)) {
1082
+ return resultType;
1083
+ }
1084
+ }
1085
+ if (mode === 'stream') {
1086
+ this.fail(method, 'stream Remote methods must return Iterable<T> or AsyncIterable<T>');
1087
+ }
1088
+ return authored;
1057
1089
  }
1058
1090
  isGlobalAbortSignal(type) {
1059
1091
  const symbol = this.symbolAtType(type);
@@ -1542,7 +1574,8 @@ class FaceAnalyzer {
1542
1574
  this.fail(site, `type ${symbol.name} is not owned by a workspace package`);
1543
1575
  const candidates = [];
1544
1576
  for (const [subpath, target] of packageExportTargets(registration.manifest)) {
1545
- if (subpath === '.' || subpath === './package.json' || subpath === './typert'
1577
+ if ((subpath === '.' && !PUBLIC_REMOTE_TYPE_ROOTS.has(registration.name))
1578
+ || subpath === './package.json' || subpath === './typert'
1546
1579
  || subpath === './client/typert' || subpath === './remote' || target.includes('*'))
1547
1580
  continue;
1548
1581
  const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
@@ -663,7 +663,7 @@ function renderRuntimeApi(services, events, types, inheritedServices) {
663
663
  for (const inherited of inheritedServices) {
664
664
  lines.push(` { name: ${quote(inherited.name)}, summary: ${quote(inherited.summary)} },`);
665
665
  }
666
- lines.push(']', '', 'function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {', ' const included = new Set<string>()', ' let frontier = [...seeds]', ' while (frontier.length > 0) {', ' const next: string[] = []', ' for (const entry of TYPE_API) {', ' if (included.has(entry.name)) continue', ' const pattern = new RegExp(`\\b${entry.name}\\b`)', ' if (!frontier.some(text => pattern.test(text))) continue', ' included.add(entry.name)', ' next.push(entry.declaration)', ' }', ' frontier = next', ' }', ' return TYPE_API.filter(entry => included.has(entry.name))', '}', '', 'function contextProperty(key: string): string {', ' return /^[A-Za-z_$][\\w$]*$/.test(key) ? `ctx.${key}` : `ctx[${JSON.stringify(key)}]`', '}', '', '/**', ' * Project the Service Catalog as a compact directory or one exact coding contract.', ' * @param key - exact Service key; omit it to list all Services and method signatures.', ' * @param services - platform-specific visible Service entries.', ' * @returns compact navigation data or one detailed Service with its referenced type closure.', ' */', 'export function queryServiceApi(key?: string, services: readonly ServiceApiEntry[] = SERVICE_API): object {', ' if (key === undefined) {', ' return {', " mode: 'catalog',", ' services: services.map(service => ({', ' key: service.key,', ' description: service.summary,', ' methods: service.methods.map(method => ({ signature: method.signature })),', ' })),', ' }', ' }', ' const service = services.find(candidate => candidate.key === key)', ' if (service === undefined) throw new Error(`no catalogued Service named "${key}"`)', ' return {', " mode: 'service',", ' service: {', ' key: service.key,', ' description: service.description,', ' access: {', ' optional: { expression: `ctx.get(${JSON.stringify(service.key)})`, requiresUndefinedCheck: true },', ' hardDependency: { inject: [service.key], expression: contextProperty(service.key) },', ' },', ' methods: service.methods,', ' },', ' referencedTypes: referencedTypeClosure(service.methods.map(method => method.signature)),', ' }', '}', '', '/**', ' * Project the Event Catalog as a compact directory or one exact listener contract.', ' * @param name - exact Event name; omit it to list all Events and listener signatures.', ' * @param events - platform-specific visible Event entries.', ' * @returns compact navigation data or one detailed Event with its referenced type closure.', ' */', 'export function queryEventApi(name?: string, events: readonly EventApiEntry[] = EVENT_API): object {', ' if (name === undefined) {', ' return {', " mode: 'catalog',", ' events: events.map(event => ({', ' name: event.name,', ' description: event.summary,', ' mode: event.mode,', ' signature: event.signature,', ' })),', ' }', ' }', ' const event = events.find(candidate => candidate.name === name)', ' if (event === undefined) throw new Error(`no catalogued Event named "${name}"`)', ' return {', " mode: 'event',", ' event: {', ' name: event.name,', ' description: event.description,', ' mode: event.mode,', ' signature: event.signature,', ' parameters: event.parameters,', ' },', ' referencedTypes: referencedTypeClosure([event.signature]),', ' }', '}', '/* jscpd:ignore-end */', '');
666
+ lines.push(']', '', 'function referencedTypeClosure(seeds: readonly string[]): TypeApiEntry[] {', ' const included = new Set<string>()', ' let frontier = [...seeds]', ' while (frontier.length > 0) {', ' const next: string[] = []', ' for (const entry of TYPE_API) {', ' if (included.has(entry.name)) continue', ' const pattern = new RegExp(`\\\\b${entry.name}\\\\b`)', ' if (!frontier.some(text => pattern.test(text))) continue', ' included.add(entry.name)', ' next.push(entry.declaration)', ' }', ' frontier = next', ' }', ' return TYPE_API.filter(entry => included.has(entry.name))', '}', '', 'function contextProperty(key: string): string {', ' return /^[A-Za-z_$][\\w$]*$/.test(key) ? `ctx.${key}` : `ctx[${JSON.stringify(key)}]`', '}', '', '/**', ' * Project the Service Catalog as a compact directory or one exact coding contract.', ' * @param key - exact Service key; omit it to list all Services and method signatures.', ' * @param services - platform-specific visible Service entries.', ' * @returns compact navigation data or one detailed Service with its referenced type closure.', ' */', 'export function queryServiceApi(key?: string, services: readonly ServiceApiEntry[] = SERVICE_API): object {', ' if (key === undefined) {', ' return {', " mode: 'catalog',", ' services: services.map(service => ({', ' key: service.key,', ' description: service.summary,', ' methods: service.methods.map(method => ({ signature: method.signature })),', ' })),', ' }', ' }', ' const service = services.find(candidate => candidate.key === key)', ' if (service === undefined) throw new Error(`no catalogued Service named "${key}"`)', ' return {', " mode: 'service',", ' service: {', ' key: service.key,', ' description: service.description,', ' access: {', ' optional: { expression: `ctx.get(${JSON.stringify(service.key)})`, requiresUndefinedCheck: true },', ' hardDependency: { inject: [service.key], expression: contextProperty(service.key) },', ' },', ' methods: service.methods,', ' },', ' referencedTypes: referencedTypeClosure(service.methods.map(method => method.signature)),', ' }', '}', '', '/**', ' * Project the Event Catalog as a compact directory or one exact listener contract.', ' * @param name - exact Event name; omit it to list all Events and listener signatures.', ' * @param events - platform-specific visible Event entries.', ' * @returns compact navigation data or one detailed Event with its referenced type closure.', ' */', 'export function queryEventApi(name?: string, events: readonly EventApiEntry[] = EVENT_API): object {', ' if (name === undefined) {', ' return {', " mode: 'catalog',", ' events: events.map(event => ({', ' name: event.name,', ' description: event.summary,', ' mode: event.mode,', ' signature: event.signature,', ' })),', ' }', ' }', ' const event = events.find(candidate => candidate.name === name)', ' if (event === undefined) throw new Error(`no catalogued Event named "${name}"`)', ' return {', " mode: 'event',", ' event: {', ' name: event.name,', ' description: event.description,', ' mode: event.mode,', ' signature: event.signature,', ' parameters: event.parameters,', ' },', ' referencedTypes: referencedTypeClosure([event.signature]),', ' }', '}', '/* jscpd:ignore-end */', '');
667
667
  return lines.join('\n');
668
668
  }
669
669
  /** Opening region delimiter; injected content lives between the pair and the page owns everything outside. */
@@ -198,6 +198,8 @@ export class FaceModelEmitter {
198
198
  if (invocation.implementation !== undefined) {
199
199
  lines.push(` implementation: ${quote(invocation.implementation)},`);
200
200
  }
201
+ if (invocation.mode !== undefined)
202
+ lines.push(` mode: ${quote(invocation.mode)},`);
201
203
  if (invocation.invocation.kind === 'direct') {
202
204
  lines.push(' invocation: { kind: \'direct\' },');
203
205
  }
@@ -341,9 +343,11 @@ export class FaceModelEmitter {
341
343
  if (invocation.cancellation !== undefined)
342
344
  parameters.push('signal?: AbortSignal');
343
345
  const result = this.renderer.renderType(invocation.result.type, referenceNames);
344
- // The Client Remote face delivers the carrier's outcome, so every generated
345
- // consumer signature resolves to a result the caller reads instead of a
346
- // value it must guard with its own try/catch.
346
+ if (invocation.mode === 'stream') {
347
+ return `(${parameters.join(', ')}) => AsyncIterable<${result}>`;
348
+ }
349
+ // The unary Client Remote face delivers the carrier's outcome, so every
350
+ // generated consumer signature resolves to a result the caller reads.
347
351
  return `(${parameters.join(', ')}) => Promise<RemoteResult<${result}>>`;
348
352
  }
349
353
  }
@@ -103,6 +103,7 @@ export interface InvocationModel {
103
103
  readonly namespace: string;
104
104
  readonly method: string;
105
105
  readonly implementation?: string;
106
+ readonly mode?: 'stream';
106
107
  readonly invocation: {
107
108
  readonly kind: 'direct';
108
109
  } | {
@@ -10,6 +10,10 @@ import { dirname, join, resolve } from 'node:path';
10
10
  import ts from 'typescript';
11
11
  import { WorkspaceTypertGenerator } from "./workspace.js";
12
12
  const DECORATOR_SYNTAX = /^\s*@[A-Za-z_$][\w$]*/m;
13
+ // This plugin consumes tsc-emitted `lib/types` output, so every project it
14
+ // would re-diagnose has already passed the workspace tsc build in the same
15
+ // orchestration; the generator skips its per-package diagnostic pass here.
16
+ const TSC_VERIFIED_INPUT = { checkDiagnostics: false };
13
17
  /**
14
18
  * Create the decorator-lowering and typert-generation plugin for the root tsdown config.
15
19
  * @param pluginOptions - package/workspace emission mode and independent program faces.
@@ -60,7 +64,7 @@ export function typertPlugin(pluginOptions = {}) {
60
64
  return;
61
65
  let artifacts = artifactsByRoot.get(root);
62
66
  if (artifacts === undefined) {
63
- const generator = new WorkspaceTypertGenerator(root);
67
+ const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT);
64
68
  artifacts = pluginOptions.faces === undefined
65
69
  ? generator.generate()
66
70
  : generator.generate(undefined, pluginOptions.faces);
@@ -70,7 +74,7 @@ export function typertPlugin(pluginOptions = {}) {
70
74
  },
71
75
  };
72
76
  function emitWorkspace(root, faces) {
73
- const generator = new WorkspaceTypertGenerator(root);
77
+ const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT);
74
78
  const packages = generator.discover(faces)
75
79
  .filter(candidate => hasTypertExport(readManifest(join(root, candidate.root)).exports))
76
80
  .map(candidate => candidate.package);
@@ -9,14 +9,29 @@ import type { TypertFace } from './model.ts';
9
9
  export interface WorkspaceEmitResult extends ModelEmitResult {
10
10
  readonly packageRoot: string;
11
11
  }
12
+ /** Behavior switches for one {@link WorkspaceTypertGenerator}. */
13
+ export interface WorkspaceTypertGeneratorOptions {
14
+ /**
15
+ * Run the per-package syntactic/semantic diagnostic pass before analysis
16
+ * (default true). Pass false only when the same orchestration already
17
+ * verified the workspace with tsc; the Typert-specific analysis checks
18
+ * (annotation coverage, private cross-package references, unretainable
19
+ * merges) run regardless.
20
+ */
21
+ readonly checkDiagnostics?: boolean;
22
+ }
12
23
  /** Discover, analyze, and emit package reflection from independent faces. */
13
24
  export declare class WorkspaceTypertGenerator {
14
25
  private readonly root;
26
+ private readonly options;
27
+ /** Parsed-config and program-host state shared by every analyzer this generator creates. */
28
+ private readonly caches;
15
29
  /**
16
30
  * Bind generation to one workspace root.
17
31
  * @param root - directory containing face aggregate tsconfigs.
32
+ * @param options - behavior switches applied to every pass of this generator.
18
33
  */
19
- constructor(root: string);
34
+ constructor(root: string, options?: WorkspaceTypertGeneratorOptions);
20
35
  /**
21
36
  * Find public package faces that contribute Cordis services/events or
22
37
  * explicitly tagged Typert roots.
@@ -4,17 +4,22 @@
4
4
  */
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { resolve } from 'node:path';
7
- import { TypertAnalysisError, WorkspaceAnalyzer } from "./analyzer.js";
7
+ import { TypertAnalysisError, WorkspaceAnalyzer, WorkspaceCaches } from "./analyzer.js";
8
8
  import { FaceModelEmitter } from "./emitter.js";
9
9
  /** Discover, analyze, and emit package reflection from independent faces. */
10
10
  export class WorkspaceTypertGenerator {
11
11
  root;
12
+ options;
13
+ /** Parsed-config and program-host state shared by every analyzer this generator creates. */
14
+ caches = new WorkspaceCaches();
12
15
  /**
13
16
  * Bind generation to one workspace root.
14
17
  * @param root - directory containing face aggregate tsconfigs.
18
+ * @param options - behavior switches applied to every pass of this generator.
15
19
  */
16
- constructor(root) {
20
+ constructor(root, options = {}) {
17
21
  this.root = root;
22
+ this.options = options;
18
23
  }
19
24
  /**
20
25
  * Find public package faces that contribute Cordis services/events or
@@ -25,6 +30,7 @@ export class WorkspaceTypertGenerator {
25
30
  discover(faces) {
26
31
  return new WorkspaceAnalyzer({
27
32
  root: this.root,
33
+ caches: this.caches,
28
34
  ...(faces === undefined ? {} : { faces }),
29
35
  }).discoverPackages();
30
36
  }
@@ -39,7 +45,9 @@ export class WorkspaceTypertGenerator {
39
45
  const workspace = new WorkspaceAnalyzer({
40
46
  root: this.root,
41
47
  packages: selected,
48
+ caches: this.caches,
42
49
  ...(faces === undefined ? {} : { faces }),
50
+ ...(this.options.checkDiagnostics === undefined ? {} : { checkDiagnostics: this.options.checkDiagnostics }),
43
51
  }).analyze();
44
52
  const artifacts = [];
45
53
  for (const face of workspace.faces) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-typert-generator",
3
3
  "description": "TypeScript project analyzer and model-driven Typert artifact generator",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -41,14 +41,14 @@
41
41
  "typescript": "^6.0.3"
42
42
  },
43
43
  "peerDependencies": {
44
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
- "@deepseek-ai/cordis": "^4.0.1"
44
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
45
+ "@deepseek-ai/cordis": "^4.0.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "zod": "^4.4.3",
49
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
50
- "@deepseek-ai/dsh-tool-cordis": "^0.1.1-rc.2",
51
- "@deepseek-ai/dsh-typert-registry": "^0.1.1-rc.2",
52
- "@deepseek-ai/cordis": "^4.0.1"
49
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
50
+ "@deepseek-ai/dsh-tool-cordis": "^0.1.2-alpha.3",
51
+ "@deepseek-ai/dsh-typert-registry": "^0.1.2-alpha.3",
52
+ "@deepseek-ai/cordis": "^4.0.2"
53
53
  }
54
54
  }