@deepseek-ai/dsh-typert-generator 0.1.2-rc.1 → 0.1.5-alpha.1

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: a3deccbb9ba0847d02b532732a87ec52da1a2d7c
6
- README.zh.md: 5983a4d9ecc7914320718338931d354a8e85d032
5
+ README.md: 48d79dfc44f6c2f658e8aa13836e5044d6cbf25d
6
+ README.zh.md: e546661fe297d09367185585e43d486cd417090d
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
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.
12
+ `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.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -79,7 +79,7 @@ The generator is built on one separation: extraction and emission are decoupled
79
79
 
80
80
  ### Analysis and faces
81
81
 
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.
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. A relative import that resolves inside the referencing package is followed through that module's re-exports until a package specifier appears, so package-local forwarding modules keep their original declaration references; a relative import that resolves into another package fails. `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.
83
83
 
84
84
  ### Emission and publication contract
85
85
 
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-library"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-typert-generator` 在构建时把源代码 TypeScript 转换为与编译器无关的数据与可运行产物:它分析工作区各包的类型树,生成 `FaceModel` 与类型图,并输出包含受支持 Zod schema 与 `TYPERT` 反射贡献的可执行 JavaScript,以及配套声明文件。它是构建时库而非插件——绝不会在实时 agent 会话中运行。仓库的 Host tsdown 会自动运行它;业务包通过导出 `./typert` 与 `./client/typert` 入口选择加入,生成器会校验这些导出与发布文件清单。静态消费方也可以直接调用分析器进行类型检查或目录生成,无需发布任何内容。
12
+ `dsh-typert-generator` 让维护者把公开的 TypeScript 类型转换为构建产物和与编译器无关的模型。包通过 `./typert` 和可选的 `./client/typert` 导出选择加入;如果声明、发布清单、Remote 导出或 Zod 投影无法被正确表示,生成过程就会失败。仓库构建可以生成可执行 schema 与配套声明,工具也可以调用 `WorkspaceAnalyzer` 完成检查或目录生成而不发布产物。生成过程只在构建时运行,绝不会进入实时 agent 会话。
13
13
 
14
14
  ## 目录
15
15
 
@@ -79,7 +79,7 @@ files:
79
79
 
80
80
  ### 分析与 face
81
81
 
82
- Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face 的边只能来自导入或重新导出。`check` 模式遇到语法或语义诊断、缺失的公开类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时都会失败;`write` 模式插入类型检查器推导出的标注,并返回无诊断的 check 模式模型。NPM 依赖拥有的类型继续以 `external` 引用表示,不会被展开。
82
+ Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face 的边只能来自导入或重新导出。解析到本包内部模块的相对导入会沿该模块的重新导出继续追踪,直到出现包说明符,因此包内转发模块保留原始声明引用;解析到其他包的相对导入会失败。`check` 模式遇到语法或语义诊断、缺失的公开类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时都会失败;`write` 模式插入类型检查器推导出的标注,并返回无诊断的 check 模式模型。NPM 依赖拥有的类型继续以 `external` 引用表示,不会被展开。
83
83
 
84
84
  ### 生成与发布约定
85
85
 
package/lib/index.js CHANGED
@@ -148,15 +148,17 @@ var WorkspaceAnalyzer = class WorkspaceAnalyzer {
148
148
  incremental: false,
149
149
  noEmit: true
150
150
  };
151
+ const host = this.caches.programHost(face, options);
151
152
  const program = ts.createProgram({
152
153
  rootNames,
153
154
  options,
154
- host: this.caches.programHost(face, options)
155
+ host
155
156
  });
156
157
  faces.push(new FaceAnalyzer({
157
158
  root: this.options.root,
158
159
  face,
159
160
  program,
161
+ host,
160
162
  registrations,
161
163
  allRegistrations: this.registrations,
162
164
  mode: this.options.mode,
@@ -355,6 +357,7 @@ var FaceAnalyzer = class {
355
357
  root;
356
358
  face;
357
359
  program;
360
+ host;
358
361
  checker;
359
362
  registrations;
360
363
  allRegistrations;
@@ -373,6 +376,7 @@ var FaceAnalyzer = class {
373
376
  this.root = options.root;
374
377
  this.face = options.face;
375
378
  this.program = options.program;
379
+ this.host = options.host;
376
380
  this.checker = options.program.getTypeChecker();
377
381
  this.registrations = options.registrations;
378
382
  this.allRegistrations = options.allRegistrations;
@@ -524,15 +528,78 @@ var FaceAnalyzer = class {
524
528
  reachable.set(fileName, sourceFile);
525
529
  for (const statement of sourceFile.statements) {
526
530
  if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement) || statement.moduleSpecifier === void 0 || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
527
- const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, sourceFile.fileName, this.program.getCompilerOptions(), ts.sys).resolvedModule;
528
- if (resolved === void 0) continue;
529
- const resolvedPath = realPath(resolved.resolvedFileName);
530
- if (!isWithin(resolvedPath, registration.root)) continue;
531
+ const resolvedPath = this.resolveImport(statement.moduleSpecifier.text, sourceFile.fileName);
532
+ if (resolvedPath === void 0 || !isWithin(resolvedPath, registration.root)) continue;
531
533
  queue.push(this.sourceFiles.get(resolvedPath));
532
534
  }
533
535
  }
534
536
  return [...reachable.values()].sort((left, right) => left.fileName.localeCompare(right.fileName));
535
537
  }
538
+ resolveImport(specifier, fromFile) {
539
+ const resolved = ts.resolveModuleName(specifier, fromFile, this.program.getCompilerOptions(), this.host, this.host.getModuleResolutionCache?.()).resolvedModule;
540
+ return resolved === void 0 ? void 0 : realPath(resolved.resolvedFileName);
541
+ }
542
+ /**
543
+ * Follow the import that names `symbol` at `site` through modules of the
544
+ * referencing package until a package specifier appears. Each forwarding
545
+ * module and requested export name pair is entered once; its explicit export
546
+ * edges are tried before its star edges. A relative specifier that resolves
547
+ * outside `from`, a namespace hop, or a module with no edge leading to a
548
+ * package specifier yields undefined.
549
+ */
550
+ packageImportOf(site, moduleSpecifier, symbol, from) {
551
+ const visited = /* @__PURE__ */ new Map();
552
+ const walk = (sourceFile, specifier, name) => {
553
+ const module = moduleIdentity(specifier);
554
+ if (module !== void 0) return {
555
+ module,
556
+ name
557
+ };
558
+ const resolvedPath = this.resolveImport(specifier, sourceFile.fileName);
559
+ if (resolvedPath === void 0 || !isWithin(resolvedPath, from.root)) return void 0;
560
+ const names = visited.get(resolvedPath);
561
+ if (names?.has(name)) return void 0;
562
+ if (names === void 0) visited.set(resolvedPath, new Set([name]));
563
+ else names.add(name);
564
+ const forward = this.sourceFiles.get(resolvedPath);
565
+ for (const edge of this.forwardedExports(forward, name, symbol)) {
566
+ const found = walk(forward, edge.specifier, edge.name);
567
+ if (found !== void 0) return found;
568
+ }
569
+ };
570
+ return walk(site.getSourceFile(), moduleSpecifier, authoredExportName(site, moduleSpecifier));
571
+ }
572
+ /** Export edges of `sourceFile` that carry `name`: explicit edges in source order, then star edges exporting `symbol`. */
573
+ forwardedExports(sourceFile, name, symbol) {
574
+ const explicit = [];
575
+ const stars = [];
576
+ for (const statement of sourceFile.statements) {
577
+ if (!ts.isExportDeclaration(statement) || statement.exportClause === void 0 && statement.moduleSpecifier === void 0 || statement.exportClause !== void 0 && ts.isNamespaceExport(statement.exportClause)) continue;
578
+ if (statement.exportClause === void 0) {
579
+ const specifier = statement.moduleSpecifier.text;
580
+ if (this.moduleExports(statement.moduleSpecifier).find((candidate) => candidate.name === name && this.resolveSymbol(candidate) === symbol) !== void 0) stars.push({
581
+ specifier,
582
+ name
583
+ });
584
+ continue;
585
+ }
586
+ const element = statement.exportClause.elements.find((candidate) => candidate.name.text === name);
587
+ if (element === void 0) continue;
588
+ if (statement.moduleSpecifier !== void 0) {
589
+ explicit.push({
590
+ specifier: statement.moduleSpecifier.text,
591
+ name: element.propertyName?.text ?? name
592
+ });
593
+ continue;
594
+ }
595
+ const binding = importBindingOf(sourceFile, element.propertyName?.text ?? name);
596
+ if (binding?.name !== void 0) explicit.push({
597
+ specifier: binding.specifier,
598
+ name: binding.name
599
+ });
600
+ }
601
+ return [...explicit, ...stars];
602
+ }
536
603
  collectServices(context, records) {
537
604
  const bySymbol = /* @__PURE__ */ new Map();
538
605
  for (const record of records) {
@@ -1760,14 +1827,13 @@ var FaceAnalyzer = class {
1760
1827
  name: symbol.name
1761
1828
  };
1762
1829
  const moduleSpecifier = moduleSpecifierOf(site);
1763
- const module = moduleSpecifier === void 0 ? void 0 : moduleIdentity(moduleSpecifier);
1764
1830
  const from = this.registrationForFile(site.getSourceFile().fileName);
1765
1831
  const owner = this.registrationForFile(declaration.getSourceFile().fileName);
1766
1832
  if (owner !== void 0) {
1767
1833
  if (owner.name !== from.name) {
1768
- if (module === void 0) this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`);
1769
- const exportName = authoredExportName(site, moduleSpecifier);
1770
- if (this.packageExportName(module, symbol, owner.face, exportName) === void 0) this.fail(site, `package reference ${exportName} is not exported by ${module.package} at ${module.subpath}`);
1834
+ const imported = moduleSpecifier === void 0 ? void 0 : this.packageImportOf(site, moduleSpecifier, symbol, from);
1835
+ if (imported === void 0) this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`);
1836
+ if (this.packageExportName(imported.module, symbol, owner.face, imported.name) === void 0) this.fail(site, `package reference ${imported.name} is not exported by ${imported.module.package} at ${imported.module.subpath}`);
1771
1837
  }
1772
1838
  const typeDeclaration = declaration;
1773
1839
  if (!this.declarationStates.has(this.symbolId(symbol))) this.ensureDeclaration(symbol, typeDeclaration);
@@ -1776,11 +1842,12 @@ var FaceAnalyzer = class {
1776
1842
  symbol: this.symbolId(symbol)
1777
1843
  };
1778
1844
  }
1779
- const otherFace = (module === void 0 ? [] : [...new Set(this.allRegistrations.filter((candidate) => candidate.name === module.package).map((candidate) => candidate.face))]).find((face) => face !== this.face);
1780
- if (otherFace !== void 0 && module !== void 0) {
1781
- const requestedName = authoredExportName(site, moduleSpecifier);
1782
- const exportName = this.packageExportName(module, symbol, otherFace, requestedName);
1783
- if (exportName === void 0) this.fail(site, `cross-face reference ${requestedName} is not exported by ${module.package} at ${module.subpath}`);
1845
+ const imported = moduleSpecifier === void 0 ? void 0 : this.packageImportOf(site, moduleSpecifier, symbol, from);
1846
+ const otherFace = (imported === void 0 ? [] : [...new Set(this.allRegistrations.filter((candidate) => candidate.name === imported.module.package).map((candidate) => candidate.face))]).find((face) => face !== this.face);
1847
+ if (otherFace !== void 0 && imported !== void 0) {
1848
+ const { module } = imported;
1849
+ const exportName = this.packageExportName(module, symbol, otherFace, imported.name);
1850
+ if (exportName === void 0) this.fail(site, `cross-face reference ${imported.name} is not exported by ${module.package} at ${module.subpath}`);
1784
1851
  this.recordCrossFaceLink(from.name, otherFace, module, exportName);
1785
1852
  return {
1786
1853
  kind: "cross-face",
@@ -1790,10 +1857,10 @@ var FaceAnalyzer = class {
1790
1857
  name: exportName
1791
1858
  };
1792
1859
  }
1793
- if (module !== void 0) return {
1860
+ if (imported !== void 0) return {
1794
1861
  kind: "external",
1795
- module: module.package,
1796
- subpath: module.subpath,
1862
+ module: imported.module.package,
1863
+ subpath: imported.module.subpath,
1797
1864
  name: symbol.name
1798
1865
  };
1799
1866
  const external = externalModuleIdentityForFile(declaration.getSourceFile().fileName);
@@ -1826,6 +1893,7 @@ var FaceAnalyzer = class {
1826
1893
  }
1827
1894
  packageExportName(module, symbol, face, requestedName) {
1828
1895
  const registration = this.allRegistrations.find((candidate) => candidate.face === face && candidate.name === module.package);
1896
+ if (registration === void 0) return void 0;
1829
1897
  const target = packageExportTargets(registration.manifest).find(([subpath]) => subpath === module.subpath)?.[1];
1830
1898
  if (target === void 0) return void 0;
1831
1899
  const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
@@ -2203,30 +2271,37 @@ function moduleSpecifierOf(node) {
2203
2271
  const symbol = ts.isTypeReferenceNode(node) ? node.typeName : node.expression;
2204
2272
  const sourceFile = node.getSourceFile();
2205
2273
  const first = ts.isIdentifier(symbol) ? symbol.text : symbol.getFirstToken(sourceFile)?.getText(sourceFile);
2206
- for (const statement of sourceFile.statements) {
2207
- if (!ts.isImportDeclaration(statement) || statement.importClause === void 0 || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
2208
- if (statement.importClause.name?.text === first) return statement.moduleSpecifier.text;
2209
- const bindings = statement.importClause.namedBindings;
2210
- if (bindings !== void 0 && ts.isNamespaceImport(bindings) && bindings.name.text === first) return statement.moduleSpecifier.text;
2211
- if (bindings !== void 0 && ts.isNamedImports(bindings) && bindings.elements.some((element) => element.name.text === first)) return statement.moduleSpecifier.text;
2212
- }
2274
+ return first === void 0 ? void 0 : importBindingOf(sourceFile, first)?.specifier;
2213
2275
  }
2214
2276
  function authoredExportName(node, moduleSpecifier) {
2215
2277
  if (ts.isImportTypeNode(node)) return node.qualifier.getText().split(".")[0];
2216
2278
  const referenced = ts.isTypeReferenceNode(node) ? node.typeName.getText().split(".") : node.expression.getText().split(".");
2217
2279
  const localName = referenced[0];
2218
- for (const statement of node.getSourceFile().statements) {
2219
- if (!ts.isImportDeclaration(statement) || statement.importClause === void 0 || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleSpecifier) continue;
2220
- if (statement.importClause.name?.text === localName) return "default";
2280
+ const binding = importBindingOf(node.getSourceFile(), localName);
2281
+ /* v8 ignore next -- moduleSpecifierOf returns only the import inspected by importBindingOf. */
2282
+ if (binding === void 0) throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
2283
+ return binding.name ?? referenced[1];
2284
+ }
2285
+ function importBindingOf(sourceFile, localName) {
2286
+ for (const statement of sourceFile.statements) {
2287
+ if (!ts.isImportDeclaration(statement) || statement.importClause === void 0 || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
2288
+ const specifier = statement.moduleSpecifier.text;
2289
+ if (statement.importClause.name?.text === localName) return {
2290
+ specifier,
2291
+ name: "default"
2292
+ };
2221
2293
  const bindings = statement.importClause.namedBindings;
2222
- if (bindings !== void 0 && ts.isNamedImports(bindings)) {
2223
- const imported = bindings.elements.find((element) => element.name.text === localName);
2224
- if (imported !== void 0) return imported.propertyName?.text ?? imported.name.text;
2294
+ if (bindings === void 0) continue;
2295
+ if (ts.isNamespaceImport(bindings)) {
2296
+ if (bindings.name.text === localName) return { specifier };
2297
+ continue;
2225
2298
  }
2226
- if (bindings !== void 0 && ts.isNamespaceImport(bindings) && bindings.name.text === localName) return referenced[1];
2299
+ const element = bindings.elements.find((candidate) => candidate.name.text === localName);
2300
+ if (element !== void 0) return {
2301
+ specifier,
2302
+ name: element.propertyName?.text ?? element.name.text
2303
+ };
2227
2304
  }
2228
- /* v8 ignore next -- moduleSpecifierOf returns only the matching import inspected by this loop. */
2229
- throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
2230
2305
  }
2231
2306
  function importTypeAttributesText(node) {
2232
2307
  const sourceFile = node.getSourceFile();
@@ -164,15 +164,13 @@ export class WorkspaceAnalyzer {
164
164
  incremental: false,
165
165
  noEmit: true,
166
166
  };
167
- const program = ts.createProgram({
168
- rootNames,
169
- options,
170
- host: this.caches.programHost(face, options),
171
- });
167
+ const host = this.caches.programHost(face, options);
168
+ const program = ts.createProgram({ rootNames, options, host });
172
169
  faces.push(new FaceAnalyzer({
173
170
  root: this.options.root,
174
171
  face,
175
172
  program,
173
+ host,
176
174
  registrations,
177
175
  allRegistrations: this.registrations,
178
176
  mode: this.options.mode,
@@ -425,6 +423,7 @@ class FaceAnalyzer {
425
423
  root;
426
424
  face;
427
425
  program;
426
+ host;
428
427
  checker;
429
428
  registrations;
430
429
  allRegistrations;
@@ -443,6 +442,7 @@ class FaceAnalyzer {
443
442
  this.root = options.root;
444
443
  this.face = options.face;
445
444
  this.program = options.program;
445
+ this.host = options.host;
446
446
  this.checker = options.program.getTypeChecker();
447
447
  this.registrations = options.registrations;
448
448
  this.allRegistrations = options.allRegistrations;
@@ -651,17 +651,82 @@ class FaceAnalyzer {
651
651
  || statement.moduleSpecifier === undefined
652
652
  || !ts.isStringLiteral(statement.moduleSpecifier))
653
653
  continue;
654
- const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, sourceFile.fileName, this.program.getCompilerOptions(), ts.sys).resolvedModule;
655
- if (resolved === undefined)
656
- continue;
657
- const resolvedPath = realPath(resolved.resolvedFileName);
658
- if (!isWithin(resolvedPath, registration.root))
654
+ const resolvedPath = this.resolveImport(statement.moduleSpecifier.text, sourceFile.fileName);
655
+ if (resolvedPath === undefined || !isWithin(resolvedPath, registration.root))
659
656
  continue;
660
657
  queue.push(this.sourceFiles.get(resolvedPath));
661
658
  }
662
659
  }
663
660
  return [...reachable.values()].sort((left, right) => left.fileName.localeCompare(right.fileName));
664
661
  }
662
+ resolveImport(specifier, fromFile) {
663
+ const resolved = ts.resolveModuleName(specifier, fromFile, this.program.getCompilerOptions(), this.host, this.host.getModuleResolutionCache?.()).resolvedModule;
664
+ return resolved === undefined ? undefined : realPath(resolved.resolvedFileName);
665
+ }
666
+ /**
667
+ * Follow the import that names `symbol` at `site` through modules of the
668
+ * referencing package until a package specifier appears. Each forwarding
669
+ * module and requested export name pair is entered once; its explicit export
670
+ * edges are tried before its star edges. A relative specifier that resolves
671
+ * outside `from`, a namespace hop, or a module with no edge leading to a
672
+ * package specifier yields undefined.
673
+ */
674
+ packageImportOf(site, moduleSpecifier, symbol, from) {
675
+ const visited = new Map();
676
+ const walk = (sourceFile, specifier, name) => {
677
+ const module = moduleIdentity(specifier);
678
+ if (module !== undefined)
679
+ return { module, name };
680
+ const resolvedPath = this.resolveImport(specifier, sourceFile.fileName);
681
+ if (resolvedPath === undefined || !isWithin(resolvedPath, from.root))
682
+ return undefined;
683
+ const names = visited.get(resolvedPath);
684
+ if (names?.has(name))
685
+ return undefined;
686
+ if (names === undefined)
687
+ visited.set(resolvedPath, new Set([name]));
688
+ else
689
+ names.add(name);
690
+ const forward = this.sourceFiles.get(resolvedPath);
691
+ for (const edge of this.forwardedExports(forward, name, symbol)) {
692
+ const found = walk(forward, edge.specifier, edge.name);
693
+ if (found !== undefined)
694
+ return found;
695
+ }
696
+ return undefined;
697
+ };
698
+ return walk(site.getSourceFile(), moduleSpecifier, authoredExportName(site, moduleSpecifier));
699
+ }
700
+ /** Export edges of `sourceFile` that carry `name`: explicit edges in source order, then star edges exporting `symbol`. */
701
+ forwardedExports(sourceFile, name, symbol) {
702
+ const explicit = [];
703
+ const stars = [];
704
+ for (const statement of sourceFile.statements) {
705
+ if (!ts.isExportDeclaration(statement)
706
+ || (statement.exportClause === undefined && statement.moduleSpecifier === undefined)
707
+ || (statement.exportClause !== undefined && ts.isNamespaceExport(statement.exportClause)))
708
+ continue;
709
+ if (statement.exportClause === undefined) {
710
+ const specifier = statement.moduleSpecifier.text;
711
+ const exported = this.moduleExports(statement.moduleSpecifier)
712
+ .find(candidate => candidate.name === name && this.resolveSymbol(candidate) === symbol);
713
+ if (exported !== undefined)
714
+ stars.push({ specifier, name });
715
+ continue;
716
+ }
717
+ const element = statement.exportClause.elements.find(candidate => candidate.name.text === name);
718
+ if (element === undefined)
719
+ continue;
720
+ if (statement.moduleSpecifier !== undefined) {
721
+ explicit.push({ specifier: statement.moduleSpecifier.text, name: element.propertyName?.text ?? name });
722
+ continue;
723
+ }
724
+ const binding = importBindingOf(sourceFile, element.propertyName?.text ?? name);
725
+ if (binding?.name !== undefined)
726
+ explicit.push({ specifier: binding.specifier, name: binding.name });
727
+ }
728
+ return [...explicit, ...stars];
729
+ }
665
730
  collectServices(context, records) {
666
731
  const bySymbol = new Map();
667
732
  for (const record of records) {
@@ -2125,17 +2190,18 @@ class FaceAnalyzer {
2125
2190
  return { kind: 'standard', name: symbol.name };
2126
2191
  }
2127
2192
  const moduleSpecifier = moduleSpecifierOf(site);
2128
- const module = moduleSpecifier === undefined ? undefined : moduleIdentity(moduleSpecifier);
2129
2193
  const from = this.registrationForFile(site.getSourceFile().fileName);
2130
2194
  const owner = this.registrationForFile(declaration.getSourceFile().fileName);
2131
2195
  if (owner !== undefined) {
2132
2196
  if (owner.name !== from.name) {
2133
- if (module === undefined) {
2197
+ const imported = moduleSpecifier === undefined
2198
+ ? undefined
2199
+ : this.packageImportOf(site, moduleSpecifier, symbol, from);
2200
+ if (imported === undefined) {
2134
2201
  this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`);
2135
2202
  }
2136
- const exportName = authoredExportName(site, moduleSpecifier);
2137
- if (this.packageExportName(module, symbol, owner.face, exportName) === undefined) {
2138
- this.fail(site, `package reference ${exportName} is not exported by ${module.package} at ${module.subpath}`);
2203
+ if (this.packageExportName(imported.module, symbol, owner.face, imported.name) === undefined) {
2204
+ this.fail(site, `package reference ${imported.name} is not exported by ${imported.module.package} at ${imported.module.subpath}`);
2139
2205
  }
2140
2206
  }
2141
2207
  const typeDeclaration = declaration;
@@ -2143,15 +2209,18 @@ class FaceAnalyzer {
2143
2209
  this.ensureDeclaration(symbol, typeDeclaration);
2144
2210
  return { kind: 'declaration', symbol: this.symbolId(symbol) };
2145
2211
  }
2146
- const packageFaces = module === undefined
2212
+ const imported = moduleSpecifier === undefined
2213
+ ? undefined
2214
+ : this.packageImportOf(site, moduleSpecifier, symbol, from);
2215
+ const packageFaces = imported === undefined
2147
2216
  ? []
2148
- : [...new Set(this.allRegistrations.filter(candidate => candidate.name === module.package).map(candidate => candidate.face))];
2217
+ : [...new Set(this.allRegistrations.filter(candidate => candidate.name === imported.module.package).map(candidate => candidate.face))];
2149
2218
  const otherFace = packageFaces.find(face => face !== this.face);
2150
- if (otherFace !== undefined && module !== undefined) {
2151
- const requestedName = authoredExportName(site, moduleSpecifier);
2152
- const exportName = this.packageExportName(module, symbol, otherFace, requestedName);
2219
+ if (otherFace !== undefined && imported !== undefined) {
2220
+ const { module } = imported;
2221
+ const exportName = this.packageExportName(module, symbol, otherFace, imported.name);
2153
2222
  if (exportName === undefined) {
2154
- this.fail(site, `cross-face reference ${requestedName} is not exported by ${module.package} at ${module.subpath}`);
2223
+ this.fail(site, `cross-face reference ${imported.name} is not exported by ${module.package} at ${module.subpath}`);
2155
2224
  }
2156
2225
  this.recordCrossFaceLink(from.name, otherFace, module, exportName);
2157
2226
  return {
@@ -2162,11 +2231,11 @@ class FaceAnalyzer {
2162
2231
  name: exportName,
2163
2232
  };
2164
2233
  }
2165
- if (module !== undefined) {
2234
+ if (imported !== undefined) {
2166
2235
  return {
2167
2236
  kind: 'external',
2168
- module: module.package,
2169
- subpath: module.subpath,
2237
+ module: imported.module.package,
2238
+ subpath: imported.module.subpath,
2170
2239
  name: symbol.name,
2171
2240
  };
2172
2241
  }
@@ -2202,6 +2271,8 @@ class FaceAnalyzer {
2202
2271
  }
2203
2272
  packageExportName(module, symbol, face, requestedName) {
2204
2273
  const registration = this.allRegistrations.find(candidate => candidate.face === face && candidate.name === module.package);
2274
+ if (registration === undefined)
2275
+ return undefined;
2205
2276
  const target = packageExportTargets(registration.manifest)
2206
2277
  .find(([subpath]) => subpath === module.subpath)?.[1];
2207
2278
  if (target === undefined)
@@ -2678,21 +2749,7 @@ function moduleSpecifierOf(node) {
2678
2749
  : node.expression;
2679
2750
  const sourceFile = node.getSourceFile();
2680
2751
  const first = ts.isIdentifier(symbol) ? symbol.text : symbol.getFirstToken(sourceFile)?.getText(sourceFile);
2681
- for (const statement of sourceFile.statements) {
2682
- if (!ts.isImportDeclaration(statement) || statement.importClause === undefined
2683
- || !ts.isStringLiteral(statement.moduleSpecifier))
2684
- continue;
2685
- if (statement.importClause.name?.text === first)
2686
- return statement.moduleSpecifier.text;
2687
- const bindings = statement.importClause.namedBindings;
2688
- if (bindings !== undefined && ts.isNamespaceImport(bindings) && bindings.name.text === first) {
2689
- return statement.moduleSpecifier.text;
2690
- }
2691
- if (bindings !== undefined && ts.isNamedImports(bindings)
2692
- && bindings.elements.some(element => element.name.text === first))
2693
- return statement.moduleSpecifier.text;
2694
- }
2695
- return undefined;
2752
+ return first === undefined ? undefined : importBindingOf(sourceFile, first)?.specifier;
2696
2753
  }
2697
2754
  function authoredExportName(node, moduleSpecifier) {
2698
2755
  if (ts.isImportTypeNode(node))
@@ -2701,26 +2758,33 @@ function authoredExportName(node, moduleSpecifier) {
2701
2758
  ? node.typeName.getText().split('.')
2702
2759
  : node.expression.getText().split('.');
2703
2760
  const localName = referenced[0];
2704
- for (const statement of node.getSourceFile().statements) {
2705
- if (!ts.isImportDeclaration(statement)
2706
- || statement.importClause === undefined
2707
- || !ts.isStringLiteral(statement.moduleSpecifier)
2708
- || statement.moduleSpecifier.text !== moduleSpecifier)
2761
+ const binding = importBindingOf(node.getSourceFile(), localName);
2762
+ /* v8 ignore next -- moduleSpecifierOf returns only the import inspected by importBindingOf. */
2763
+ if (binding === undefined)
2764
+ throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
2765
+ return binding.name ?? referenced[1];
2766
+ }
2767
+ function importBindingOf(sourceFile, localName) {
2768
+ for (const statement of sourceFile.statements) {
2769
+ if (!ts.isImportDeclaration(statement) || statement.importClause === undefined
2770
+ || !ts.isStringLiteral(statement.moduleSpecifier))
2709
2771
  continue;
2772
+ const specifier = statement.moduleSpecifier.text;
2710
2773
  if (statement.importClause.name?.text === localName)
2711
- return 'default';
2774
+ return { specifier, name: 'default' };
2712
2775
  const bindings = statement.importClause.namedBindings;
2713
- if (bindings !== undefined && ts.isNamedImports(bindings)) {
2714
- const imported = bindings.elements.find(element => element.name.text === localName);
2715
- if (imported !== undefined)
2716
- return imported.propertyName?.text ?? imported.name.text;
2717
- }
2718
- if (bindings !== undefined && ts.isNamespaceImport(bindings) && bindings.name.text === localName) {
2719
- return referenced[1];
2776
+ if (bindings === undefined)
2777
+ continue;
2778
+ if (ts.isNamespaceImport(bindings)) {
2779
+ if (bindings.name.text === localName)
2780
+ return { specifier };
2781
+ continue;
2720
2782
  }
2783
+ const element = bindings.elements.find(candidate => candidate.name.text === localName);
2784
+ if (element !== undefined)
2785
+ return { specifier, name: element.propertyName?.text ?? element.name.text };
2721
2786
  }
2722
- /* v8 ignore next -- moduleSpecifierOf returns only the matching import inspected by this loop. */
2723
- throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
2787
+ return undefined;
2724
2788
  }
2725
2789
  function importTypeAttributesText(node) {
2726
2790
  const sourceFile = node.getSourceFile();
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.2-rc.1",
4
+ "version": "0.1.5-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "zod": "^4.4.3",
43
- "@deepseek-ai/dsh-tool-cordis": "^0.1.2-rc.1",
44
- "@deepseek-ai/dsh-typert-registry": "^0.1.2-rc.1",
43
+ "@deepseek-ai/dsh-tool-cordis": "^0.1.5-alpha.1",
44
+ "@deepseek-ai/dsh-typert-registry": "^0.1.5-alpha.1",
45
45
  "@deepseek-ai/cordis": "^4.0.2"
46
46
  }
47
47
  }