@deepseek-ai/dsh-typert-generator 0.2.1-alpha.1 → 0.2.1-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/lib/index.js CHANGED
@@ -1,8 +1,177 @@
1
- import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
2
- import { dirname, extname, join, posix, relative, resolve, sep } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
3
  import ts from "typescript";
4
4
  import { Buffer } from "node:buffer";
5
5
  import { GenMapping, addMapping, toEncodedMap } from "@jridgewell/gen-mapping";
6
+ //#region lib/types/build-output-guard.js
7
+ /**
8
+ * Detection of workspace build outputs loaded into an analyzed face program,
9
+ * and of workspace imports that resolve to nothing. Typert reads workspace
10
+ * packages only from source when a caller rejects build outputs, so its
11
+ * result cannot depend on whether, or when, package compilation ran: an
12
+ * import that reaches `lib/` on a compiled tree resolves to nothing on a
13
+ * clean one, and both are rejected.
14
+ * @module @deepseek-ai/dsh-typert-generator/build-output-guard
15
+ */
16
+ /**
17
+ * List the program files that a workspace package holds under its `lib/`
18
+ * directory. A package is the nearest directory with a `package.json` at or
19
+ * below `root`; files under `node_modules` are external. Importers are found
20
+ * by resolving each program file's import specifiers again, so this pass
21
+ * costs extra only when an output is present.
22
+ * @param program - the analyzed face program.
23
+ * @param root - workspace root.
24
+ * @param host - module resolution host of the program.
25
+ * @returns loads in file-path order; empty when every workspace file comes from source.
26
+ */
27
+ function findBuildOutputLoads(program, root, host) {
28
+ const classifier = new BuildOutputClassifier(root);
29
+ const outputs = /* @__PURE__ */ new Map();
30
+ for (const sourceFile of program.getSourceFiles()) if (classifier.isBuildOutput(sourceFile.fileName)) outputs.set(classifier.canonical(sourceFile.fileName), []);
31
+ if (outputs.size === 0) return [];
32
+ const options = program.getCompilerOptions();
33
+ for (const sourceFile of program.getSourceFiles()) {
34
+ if (!classifier.isWorkspaceFile(sourceFile.fileName) || classifier.isBuildOutput(sourceFile.fileName)) continue;
35
+ for (const { fileName: specifier } of ts.preProcessFile(sourceFile.text, true, true).importedFiles) {
36
+ const resolved = ts.resolveModuleName(specifier, sourceFile.fileName, options, host).resolvedModule;
37
+ (resolved === void 0 ? void 0 : outputs.get(classifier.canonical(resolved.resolvedFileName)))?.push({
38
+ file: classifier.display(sourceFile.fileName),
39
+ specifier
40
+ });
41
+ }
42
+ }
43
+ return [...outputs].sort(([left], [right]) => left.localeCompare(right)).map(([file, importers]) => ({
44
+ file: classifier.display(file),
45
+ importers,
46
+ source: classifier.sourceFor(file)
47
+ }));
48
+ }
49
+ /**
50
+ * List the imports in workspace program files that name a workspace package
51
+ * and resolve to nothing. A workspace package is one whose `node_modules` link,
52
+ * found from the importing file's directories, leads inside `root` outside any
53
+ * `node_modules`; without that link or a `paths` alias the package cannot be
54
+ * identified, and type checking reports the import. The source of a subpath is
55
+ * derived from the package's `exports` `types` condition under `lib/types/`.
56
+ * @param program - the analyzed face program.
57
+ * @param root - workspace root.
58
+ * @param host - module resolution host of the program.
59
+ * @returns unresolved imports in file and specifier order.
60
+ */
61
+ function findUnresolvedWorkspaceImports(program, root, host) {
62
+ const classifier = new BuildOutputClassifier(root);
63
+ const options = program.getCompilerOptions();
64
+ const cache = ts.createModuleResolutionCache(root, (file) => file, options);
65
+ const found = [];
66
+ for (const sourceFile of program.getSourceFiles()) {
67
+ if (!classifier.isWorkspaceFile(sourceFile.fileName) || classifier.isBuildOutput(sourceFile.fileName)) continue;
68
+ for (const { fileName: specifier } of ts.preProcessFile(sourceFile.text, true, true).importedFiles) {
69
+ if (specifier.startsWith(".") || isAbsolute(specifier) || specifier.includes(":")) continue;
70
+ if (ts.resolveModuleName(specifier, sourceFile.fileName, options, host, cache).resolvedModule !== void 0) continue;
71
+ const packageRoot = classifier.linkedWorkspacePackage(sourceFile.fileName, packageNameOf(specifier));
72
+ if (packageRoot === void 0) continue;
73
+ const source = exportSource(packageRoot, specifier);
74
+ found.push({
75
+ file: classifier.display(sourceFile.fileName),
76
+ specifier,
77
+ source: source === void 0 ? void 0 : classifier.display(source)
78
+ });
79
+ }
80
+ }
81
+ return found.sort((left, right) => left.file.localeCompare(right.file) || left.specifier.localeCompare(right.specifier));
82
+ }
83
+ function packageNameOf(specifier) {
84
+ return specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
85
+ }
86
+ function exportSource(packageRoot, specifier) {
87
+ const packageName = packageNameOf(specifier);
88
+ const subpath = specifier === packageName ? "." : `.${specifier.slice(packageName.length)}`;
89
+ let manifest;
90
+ try {
91
+ manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
92
+ } catch (_unreadableManifest) {
93
+ return;
94
+ }
95
+ const entry = property(property(manifest, "exports"), subpath);
96
+ const types = typeof entry === "string" ? entry : property(entry, "types");
97
+ if (typeof types !== "string" || !types.startsWith("./lib/types/")) return void 0;
98
+ const stem = join(packageRoot, "src", types.slice(12).replace(/\.d\.([cm]?)ts$/u, ".$1ts"));
99
+ return [stem, `${stem}x`].find((candidate) => existsSync(candidate));
100
+ }
101
+ function property(value, key) {
102
+ return typeof value === "object" && value !== null ? value[key] : void 0;
103
+ }
104
+ /**
105
+ * Classifies program files by their real paths, because module resolution
106
+ * reports symbolic-link targets and Windows long names while the workspace
107
+ * root may be spelled through a link or an 8.3 short name; reported paths are
108
+ * spelled under the caller's root.
109
+ */
110
+ var BuildOutputClassifier = class {
111
+ root;
112
+ packageRoots = /* @__PURE__ */ new Map();
113
+ realPaths = /* @__PURE__ */ new Map();
114
+ realRoot;
115
+ constructor(root) {
116
+ this.root = root;
117
+ this.realRoot = this.canonical(root);
118
+ }
119
+ canonical(file) {
120
+ const absolute = resolve(file);
121
+ const cached = this.realPaths.get(absolute);
122
+ if (cached !== void 0) return cached;
123
+ let real = absolute;
124
+ try {
125
+ real = realpathSync.native(absolute);
126
+ } catch (_missingPath) {}
127
+ this.realPaths.set(absolute, real);
128
+ return real;
129
+ }
130
+ display(file) {
131
+ const path = relative(this.realRoot, this.canonical(file));
132
+ return path.startsWith("..") || isAbsolute(path) ? resolve(file) : join(this.root, path);
133
+ }
134
+ isWorkspaceFile(file) {
135
+ const path = relative(this.realRoot, this.canonical(file));
136
+ return path !== "" && !path.startsWith("..") && !isAbsolute(path) && !path.split(sep).includes("node_modules");
137
+ }
138
+ isBuildOutput(file) {
139
+ if (!this.isWorkspaceFile(file)) return false;
140
+ const real = this.canonical(file);
141
+ const packageRoot = this.packageRoot(dirname(real));
142
+ return packageRoot !== void 0 && relative(packageRoot, real).split(sep)[0] === "lib";
143
+ }
144
+ sourceFor(file) {
145
+ const real = this.canonical(file);
146
+ const packageRoot = this.packageRoot(dirname(real));
147
+ if (packageRoot === void 0) return void 0;
148
+ const [lib, types, ...rest] = relative(packageRoot, real).split(sep);
149
+ const last = rest.at(-1);
150
+ if (lib !== "lib" || types !== "types" || last === void 0) return void 0;
151
+ const stem = join(packageRoot, "src", ...rest.slice(0, -1), last.replace(/\.d\.([cm]?)ts$/u, ".$1ts"));
152
+ const source = [stem, `${stem}x`].find((candidate) => existsSync(candidate));
153
+ return source === void 0 ? void 0 : this.display(source);
154
+ }
155
+ linkedWorkspacePackage(importer, packageName) {
156
+ let directory = dirname(resolve(importer));
157
+ for (;;) {
158
+ const link = join(directory, "node_modules", packageName);
159
+ if (existsSync(link)) {
160
+ const target = this.canonical(link);
161
+ return this.isWorkspaceFile(join(target, "package.json")) ? target : void 0;
162
+ }
163
+ if (this.canonical(directory) === this.realRoot || dirname(directory) === directory) return void 0;
164
+ directory = dirname(directory);
165
+ }
166
+ }
167
+ packageRoot(directory) {
168
+ if (this.packageRoots.has(directory)) return this.packageRoots.get(directory);
169
+ const found = existsSync(join(directory, "package.json")) ? directory : directory === this.realRoot || dirname(directory) === directory ? void 0 : this.packageRoot(dirname(directory));
170
+ this.packageRoots.set(directory, found);
171
+ return found;
172
+ }
173
+ };
174
+ //#endregion
6
175
  //#region lib/types/analyzer.js
7
176
  /**
8
177
  * TypeScript project analyzer for the compiler-independent Typert model.
@@ -47,6 +216,7 @@ var WorkspaceCaches = class {
47
216
  configs = /* @__PURE__ */ new Map();
48
217
  /** Registration inventories keyed by root and aggregate config paths. */
49
218
  registrations = /* @__PURE__ */ new Map();
219
+ aggregates = /* @__PURE__ */ new Map();
50
220
  hosts = /* @__PURE__ */ new Map();
51
221
  /**
52
222
  * Parse one tsconfig once per workspace snapshot.
@@ -62,6 +232,26 @@ var WorkspaceCaches = class {
62
232
  return parsed;
63
233
  }
64
234
  /**
235
+ * Read one face aggregate tsconfig once per workspace snapshot. An aggregate
236
+ * that declares `references` is parsed without expanding its `include`
237
+ * patterns, which affect neither its options nor its references.
238
+ * @param path - absolute aggregate config path.
239
+ * @returns the memoized compiler options and project references.
240
+ */
241
+ aggregate(path) {
242
+ let aggregate = this.aggregates.get(path);
243
+ if (aggregate === void 0) {
244
+ const { parsed } = parseConfig(path, true);
245
+ aggregate = {
246
+ path,
247
+ options: parsed.options,
248
+ projectReferences: parsed.projectReferences ?? []
249
+ };
250
+ this.aggregates.set(path, aggregate);
251
+ }
252
+ return aggregate;
253
+ }
254
+ /**
65
255
  * Return the shared compiler host for one face. Every program of one face
66
256
  * is built from the same aggregate compiler options (the first call wins),
67
257
  * so parsed source files, binder state, and module resolutions are safe to
@@ -121,6 +311,7 @@ var WorkspaceAnalyzer = class WorkspaceAnalyzer {
121
311
  faces: options.faces ?? ["host", "client"],
122
312
  checkDiagnostics: options.checkDiagnostics ?? true,
123
313
  mode: options.mode ?? "check",
314
+ rejectBuildOutputs: options.rejectBuildOutputs ?? false,
124
315
  ...options.packages === void 0 ? {} : { packages: options.packages }
125
316
  };
126
317
  this.caches = options.caches ?? new WorkspaceCaches();
@@ -140,10 +331,10 @@ var WorkspaceAnalyzer = class WorkspaceAnalyzer {
140
331
  if (registrations.length === 0) continue;
141
332
  if (this.options.checkDiagnostics) for (const registration of registrations) this.checkProject(registration);
142
333
  const aggregatePath = resolve(this.options.root, face === "host" ? this.options.hostConfig : this.options.clientConfig);
143
- const aggregate = this.caches.config(aggregatePath);
334
+ const aggregate = this.caches.aggregate(aggregatePath);
144
335
  const rootNames = [...new Set(registrations.flatMap((registration) => registration.config.parsed.fileNames))];
145
336
  const options = {
146
- ...aggregate.parsed.options,
337
+ ...aggregate.options,
147
338
  composite: false,
148
339
  incremental: false,
149
340
  noEmit: true
@@ -154,6 +345,7 @@ var WorkspaceAnalyzer = class WorkspaceAnalyzer {
154
345
  options,
155
346
  host
156
347
  });
348
+ if (this.options.rejectBuildOutputs) rejectBuildOutputLoads(face, this.options.root, aggregatePath, findBuildOutputLoads(program, this.options.root, host), findUnresolvedWorkspaceImports(program, this.options.root, host));
157
349
  faces.push(new FaceAnalyzer({
158
350
  root: this.options.root,
159
351
  face,
@@ -210,10 +402,11 @@ var WorkspaceAnalyzer = class WorkspaceAnalyzer {
210
402
  /**
211
403
  * Discover package faces from public-export-reachable Cordis augmentations
212
404
  * and explicit `@typert` roots without constructing a type-checker program.
405
+ * @param includeManifest - optional `package.json` predicate; a rejected package's sources and project config are not read.
213
406
  * @returns contributors grouped by package with deterministic face order.
214
407
  */
215
- discoverPackages() {
216
- const registrations = this.loadRegistrations().filter((registration) => this.options.faces.includes(registration.face)).filter((registration) => this.registrationHasSurface(registration));
408
+ discoverPackages(includeManifest) {
409
+ const registrations = this.loadRegistrations().filter((registration) => this.options.faces.includes(registration.face) && (includeManifest === void 0 || includeManifest(registration.manifest))).filter((registration) => this.registrationHasSurface(registration));
217
410
  const packages = /* @__PURE__ */ new Map();
218
411
  for (const registration of registrations) {
219
412
  const current = packages.get(registration.name) ?? {
@@ -272,36 +465,31 @@ var WorkspaceAnalyzer = class WorkspaceAnalyzer {
272
465
  for (const face of ["host", "client"]) {
273
466
  const aggregatePath = resolve(this.options.root, face === "host" ? this.options.hostConfig : this.options.clientConfig);
274
467
  if (!existsSync(aggregatePath)) continue;
275
- const aggregate = this.caches.config(aggregatePath);
276
- for (const reference of aggregate.parsed.projectReferences ?? []) {
468
+ const aggregate = this.caches.aggregate(aggregatePath);
469
+ for (const reference of aggregate.projectReferences) {
277
470
  const configPath = projectConfigPath(reference.path);
278
471
  const packageRoot = dirname(configPath);
279
472
  if (!isWithin(realPath(packageRoot), join(this.options.root, "packages")) && !(includeVendor && isWithin(realPath(packageRoot), join(this.options.root, "vendor")))) continue;
280
473
  const manifestPath = join(packageRoot, "package.json");
281
474
  if (!existsSync(manifestPath)) continue;
282
475
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
283
- if (typeof manifest.name !== "string") continue;
284
- const registration = {
285
- face,
286
- name: manifest.name,
287
- root: realPath(packageRoot),
288
- config: this.caches.config(configPath),
289
- manifest
290
- };
291
- if (!isDualFacePackage(manifest)) registrations.push(registration);
292
- else if (configPath === join(packageRoot, "tsconfig.json")) registrations.push({
293
- ...registration,
294
- face: "host",
295
- exportSubpaths: hostExportSubpaths(manifest)
296
- }, {
297
- ...registration,
298
- face: "client",
299
- exportSubpaths: clientExportSubpaths(manifest)
300
- });
301
- else registrations.push({
302
- ...registration,
303
- exportSubpaths: face === "host" ? hostExportSubpaths(manifest) : clientExportSubpaths(manifest)
476
+ const name = manifest.name;
477
+ if (typeof name !== "string") continue;
478
+ const root = realPath(packageRoot);
479
+ const caches = this.caches;
480
+ const register = (registrationFace, exportSubpaths) => ({
481
+ face: registrationFace,
482
+ name,
483
+ root,
484
+ get config() {
485
+ return caches.config(configPath);
486
+ },
487
+ manifest,
488
+ ...exportSubpaths === void 0 ? {} : { exportSubpaths }
304
489
  });
490
+ if (!isDualFacePackage(manifest)) registrations.push(register(face));
491
+ else if (configPath === join(packageRoot, "tsconfig.json")) registrations.push(register("host", hostExportSubpaths(manifest)), register("client", clientExportSubpaths(manifest)));
492
+ else registrations.push(register(face, face === "host" ? hostExportSubpaths(manifest) : clientExportSubpaths(manifest)));
305
493
  }
306
494
  }
307
495
  const inventory = uniqueBy(registrations, (registration) => `${registration.face}\0${registration.name}`).sort((left, right) => left.face.localeCompare(right.face) || left.name.localeCompare(right.name));
@@ -370,6 +558,9 @@ var FaceAnalyzer = class {
370
558
  nodes = /* @__PURE__ */ new Map();
371
559
  exportsByPackage = /* @__PURE__ */ new Map();
372
560
  nodeOrdinals = /* @__PURE__ */ new Map();
561
+ /** Registration and package lookups per source path; the registration set is fixed per analyzer. */
562
+ registrationByFile = /* @__PURE__ */ new Map();
563
+ packageNameByFile = /* @__PURE__ */ new Map();
373
564
  staticLookups;
374
565
  staticContexts;
375
566
  constructor(options) {
@@ -696,6 +887,7 @@ var FaceAnalyzer = class {
696
887
  if (hasModifier(method, ts.SyntaxKind.AbstractKeyword) || method.body === void 0) this.fail(method, "Remote methods must have a concrete implementation");
697
888
  if (!ts.isIdentifier(method.name)) this.fail(method, "Remote method names must be identifiers");
698
889
  if ((method.typeParameters?.length ?? 0) > 0) this.fail(method, "generic Remote methods are not supported");
890
+ const documentation = documentationOf(method);
699
891
  const methodName = method.name.text;
700
892
  const exportedMethod = invocation.exportName ?? methodName;
701
893
  const lookups = this.lookupDeclarations();
@@ -744,7 +936,11 @@ var FaceAnalyzer = class {
744
936
  }
745
937
  if (wires.has(modeled.wire)) this.fail(parameter, `duplicate Remote wire field ${modeled.wire}`);
746
938
  wires.add(modeled.wire);
747
- parameters.push(modeled);
939
+ const description = documentation.tags.find((tag) => tag.name === "param" && tag.argument === modeled.name)?.comment?.replace(/^[-—–]\s*/, "");
940
+ parameters.push({
941
+ ...modeled,
942
+ ...description === void 0 ? {} : { description }
943
+ });
748
944
  }
749
945
  let receiver = { kind: "direct" };
750
946
  if (invocation.kind === "context") {
@@ -777,6 +973,7 @@ var FaceAnalyzer = class {
777
973
  const { result: resultType, uplink: uplinkType } = this.remoteResultType(method, mode);
778
974
  const uplink = uplinkType === void 0 ? void 0 : { boundary: this.remoteBoundary(uplinkType, `${registration.name}#${binding.namespace}/${exportedMethod}:uplink`, false, "undefined") };
779
975
  return {
976
+ ...documentation,
780
977
  id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
781
978
  service: binding.service,
782
979
  namespace: binding.namespace,
@@ -863,28 +1060,27 @@ var FaceAnalyzer = class {
863
1060
  kind: "direct",
864
1061
  exportName
865
1062
  };
866
- } else {
867
- if (!ts.isObjectLiteralExpression(argument) || argument.properties.length !== 1) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
868
- const [property] = argument.properties;
869
- if (property === void 0) this.fail(argument, "Remote() options must contain exactly mode: \"stream\"");
870
- const mode = ts.isPropertyAssignment(property) && memberName(property.name) === "mode" ? stringLiteralValue(property.initializer) : void 0;
871
- if (mode !== "stream") this.fail(property, "Remote() options must contain exactly mode: \"stream\"");
872
- marker = {
873
- kind: "direct",
874
- mode
875
- };
876
- }
1063
+ } else marker = {
1064
+ kind: "direct",
1065
+ ...this.remoteOptions(argument, "Remote")
1066
+ };
877
1067
  } else if (ts.isCallExpression(expression) && this.isTypeMetaSymbol(expression.expression, "RemoteScope")) {
878
- if (expression.arguments.length < 1 || expression.arguments.length > 2) this.fail(expression, "RemoteScope() requires a Context key and optional exported method name");
1068
+ if (expression.arguments.length < 1 || expression.arguments.length > 2) this.fail(expression, "RemoteScope() requires a Context key and an optional exported method name or options object");
879
1069
  const context = stringLiteralValue(expression.arguments[0]);
880
1070
  if (context === void 0 || !isRemoteSegment(context)) this.fail(expression.arguments[0] ?? expression, "RemoteScope() key must be a string literal containing only RPC endpoint segment characters");
881
- const exportArgument = expression.arguments[1];
882
- const exportName = exportArgument === void 0 ? void 0 : stringLiteralValue(exportArgument);
883
- if (exportArgument !== void 0 && (exportName === void 0 || !isRemoteSegment(exportName))) this.fail(exportArgument, "RemoteScope() name must be a string literal containing only RPC endpoint segment characters");
884
- marker = {
1071
+ const option = expression.arguments[1];
1072
+ const exportName = option === void 0 ? void 0 : stringLiteralValue(option);
1073
+ if (option === void 0 || exportName !== void 0) {
1074
+ if (exportName !== void 0 && !isRemoteSegment(exportName)) this.fail(option ?? expression, "RemoteScope() name must be a string literal containing only RPC endpoint segment characters");
1075
+ marker = {
1076
+ kind: "context",
1077
+ context,
1078
+ ...exportName === void 0 ? {} : { exportName }
1079
+ };
1080
+ } else marker = {
885
1081
  kind: "context",
886
1082
  context,
887
- ...exportName === void 0 ? {} : { exportName }
1083
+ ...this.remoteOptions(option, "RemoteScope")
888
1084
  };
889
1085
  } else continue;
890
1086
  if (found !== void 0) this.fail(decorator, "a method can have only one Remote invocation decorator");
@@ -893,6 +1089,34 @@ var FaceAnalyzer = class {
893
1089
  return found;
894
1090
  }
895
1091
  /**
1092
+ * Read the options object of `@Remote` or `@RemoteScope`. `resources`
1093
+ * accepts any expression: its readers run only at runtime, where the
1094
+ * Gateway reads them from the prototype descriptor, so generation records
1095
+ * nothing for them.
1096
+ */
1097
+ remoteOptions(argument, decorator) {
1098
+ const requirement = decorator === "Remote" ? "Remote() options require name, mode: \"stream\", or resources" : "RemoteScope() options require name or resources";
1099
+ if (!ts.isObjectLiteralExpression(argument) || argument.properties.length === 0) this.fail(argument, requirement);
1100
+ let options = {};
1101
+ for (const property of argument.properties) {
1102
+ if (!ts.isPropertyAssignment(property)) this.fail(property, `${decorator}() options must use named values`);
1103
+ const name = memberName(property.name);
1104
+ if (decorator === "Remote" && name === "mode" && stringLiteralValue(property.initializer) === "stream") options = {
1105
+ ...options,
1106
+ mode: "stream"
1107
+ };
1108
+ else if (name === "name") {
1109
+ const value = stringLiteralValue(property.initializer);
1110
+ if (value === void 0 || !isRemoteSegment(value)) this.fail(property, `${decorator}() name must be an endpoint segment`);
1111
+ options = {
1112
+ ...options,
1113
+ exportName: value
1114
+ };
1115
+ } else if (name !== "resources") this.fail(property, requirement);
1116
+ }
1117
+ return options;
1118
+ }
1119
+ /**
896
1120
  * The item types a Remote method's authored return type declares. Unary
897
1121
  * methods unwrap `Promise<T>`; stream methods unwrap `Iterable<Out>`,
898
1122
  * `AsyncIterable<Out>`, or the protocol's `RemoteStream<Out, In>`, whose
@@ -1061,6 +1285,13 @@ var FaceAnalyzer = class {
1061
1285
  completed.set(type, id);
1062
1286
  return id;
1063
1287
  };
1288
+ const alias = type.aliasSymbol;
1289
+ const aliasDeclaration = alias === void 0 ? void 0 : preferredDeclaration(this.resolveSymbol(alias));
1290
+ if (alias?.name === "JsonValue" && aliasDeclaration !== void 0 && this.registrationForFile(aliasDeclaration.getSourceFile().fileName)?.name === "@deepseek-ai/dsh-util-values") return add({
1291
+ kind: "keyword",
1292
+ name: "unknown",
1293
+ json: true
1294
+ });
1064
1295
  const flags = type.flags;
1065
1296
  if (this.isRemoteByteArray(type)) return add({
1066
1297
  kind: "reference",
@@ -1149,8 +1380,19 @@ var FaceAnalyzer = class {
1149
1380
  text: String(value)
1150
1381
  });
1151
1382
  }
1152
- if (type.isUnionOrIntersection()) return add({
1153
- kind: (flags & ts.TypeFlags.Union) !== 0 ? "union" : "intersection",
1383
+ if (type.isIntersection()) {
1384
+ const material = type.types.filter((member) => !this.isRemotePhantomConstraint(member));
1385
+ if (material.length === 1) {
1386
+ const projected = convert(material[0]);
1387
+ const { id: _projectedId, ...value } = this.nodes.get(projected);
1388
+ return add(value);
1389
+ }
1390
+ if (!material.every((member) => (member.flags & ts.TypeFlags.Object) !== 0 && !this.checker.isArrayType(member) && !this.checker.isTupleType(member))) return add({
1391
+ kind: "intersection",
1392
+ types: material.map(convert)
1393
+ });
1394
+ } else if (type.isUnion()) return add({
1395
+ kind: "union",
1154
1396
  types: type.types.map(convert)
1155
1397
  });
1156
1398
  if ((flags & ts.TypeFlags.TypeParameter) !== 0) this.fail(authoredType, "Remote codec contains an unresolved type parameter");
@@ -1188,7 +1430,7 @@ var FaceAnalyzer = class {
1188
1430
  const propertyType = this.checker.getTypeOfSymbolAtLocation(property, declaration ?? authoredType);
1189
1431
  const symbolKey = property.getName();
1190
1432
  members.push({
1191
- ...EMPTY_DOCUMENTATION,
1433
+ ...declaration === void 0 ? EMPTY_DOCUMENTATION : documentationOf(declaration),
1192
1434
  id: `${id}#${symbolKey}`,
1193
1435
  name: symbolKey,
1194
1436
  ...symbolKey.startsWith("__@") ? { computed: "symbol" } : {},
@@ -1954,12 +2196,19 @@ var FaceAnalyzer = class {
1954
2196
  return `${this.packageNameForFile(declaration.getSourceFile().fileName)}:${location.file}#${symbol.name}`;
1955
2197
  }
1956
2198
  registrationForFile(file) {
2199
+ if (this.registrationByFile.has(file)) return this.registrationByFile.get(file);
1957
2200
  const path = realPath(file);
1958
- return this.allRegistrations.find((registration) => registration.face === this.face && isWithin(path, registration.root));
2201
+ const registration = this.allRegistrations.find((registration) => registration.face === this.face && isWithin(path, registration.root));
2202
+ this.registrationByFile.set(file, registration);
2203
+ return registration;
1959
2204
  }
1960
2205
  packageNameForFile(file) {
2206
+ const cached = this.packageNameByFile.get(file);
2207
+ if (cached !== void 0) return cached;
1961
2208
  const path = realPath(file);
1962
- return this.allRegistrations.find((registration) => isWithin(path, registration.root))?.name ?? "<external>";
2209
+ const name = this.allRegistrations.find((registration) => isWithin(path, registration.root))?.name ?? "<external>";
2210
+ this.packageNameByFile.set(file, name);
2211
+ return name;
1963
2212
  }
1964
2213
  allocateNodeId(site) {
1965
2214
  const location = this.locationKey(site);
@@ -2021,11 +2270,20 @@ function mergeWorkspaceModels(models) {
2021
2270
  crossFaceLinks: [...links.values()].sort(compareCrossFaceLinks)
2022
2271
  };
2023
2272
  }
2024
- function parseConfig(path) {
2273
+ /** {@link ts.sys} config reads without directory listing, so `include` patterns match no files. */
2274
+ const UNLISTED_CONFIG_HOST = {
2275
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
2276
+ readDirectory: () => [],
2277
+ fileExists: (file) => ts.sys.fileExists(file),
2278
+ readFile: (file) => ts.sys.readFile(file)
2279
+ };
2280
+ function parseConfig(path, aggregate = false) {
2025
2281
  const compilerPath = path.split(sep).join("/");
2026
2282
  const read = ts.readConfigFile(compilerPath, (file) => ts.sys.readFile(file));
2027
2283
  if (read.error !== void 0) throw new TypertAnalysisError(formatDiagnostic(read.error));
2028
- const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(compilerPath), void 0, compilerPath);
2284
+ const raw = read.config;
2285
+ const host = aggregate && typeof raw === "object" && raw !== null && Object.hasOwn(raw, "references") ? UNLISTED_CONFIG_HOST : ts.sys;
2286
+ const parsed = ts.parseJsonConfigFileContent(read.config, host, dirname(compilerPath), void 0, compilerPath);
2029
2287
  if (parsed.errors.length > 0) throw new TypertAnalysisError(parsed.errors.map(formatDiagnostic).join("\n"));
2030
2288
  return {
2031
2289
  path,
@@ -2140,7 +2398,8 @@ function declarationText(declaration) {
2140
2398
  function classShape(node) {
2141
2399
  const nonPublic = (member) => (ts.canHaveModifiers(member) ? ts.getModifiers(member) : void 0)?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword) ?? false;
2142
2400
  const members = node.members.flatMap((member) => {
2143
- if (nonPublic(member) || ts.isPropertyDeclaration(member) && ts.isPrivateIdentifier(member.name)) return [];
2401
+ const name = ts.getNameOfDeclaration(member);
2402
+ if (ts.isClassStaticBlockDeclaration(member) || nonPublic(member) || name !== void 0 && ts.isPrivateIdentifier(name)) return [];
2144
2403
  if (ts.isMethodDeclaration(member)) return [ts.factory.updateMethodDeclaration(member, member.modifiers, member.asteriskToken, member.name, member.questionToken, member.typeParameters, member.parameters, member.type, void 0)];
2145
2404
  if (ts.isConstructorDeclaration(member)) return [ts.factory.updateConstructorDeclaration(member, member.modifiers, member.parameters, void 0)];
2146
2405
  if (ts.isGetAccessorDeclaration(member)) return [ts.factory.updateGetAccessorDeclaration(member, member.modifiers, member.name, member.parameters, member.type, void 0)];
@@ -2383,8 +2642,14 @@ function formatProgramDiagnostic(root, face, diagnostic) {
2383
2642
  return `typert(${face}): ${slash(relative(root, diagnostic.file.fileName))}:${String(position.line + 1)}:${String(position.character + 1)}: TypeScript TS${String(diagnostic.code)}: ${message}`;
2384
2643
  }
2385
2644
  const realPathCache = /* @__PURE__ */ new Map();
2645
+ /** Normalized form per requested path; `resolve` reads only the process working directory. */
2646
+ const absolutePathCache = /* @__PURE__ */ new Map();
2386
2647
  function realPath(path) {
2387
- const absolute = resolve(path);
2648
+ let absolute = absolutePathCache.get(path);
2649
+ if (absolute === void 0) {
2650
+ absolute = resolve(path);
2651
+ absolutePathCache.set(path, absolute);
2652
+ }
2388
2653
  const cached = realPathCache.get(absolute);
2389
2654
  if (cached !== void 0) return cached;
2390
2655
  if (!existsSync(absolute)) return absolute;
@@ -2400,6 +2665,33 @@ function isWithin(path, root) {
2400
2665
  function slash(value) {
2401
2666
  return value.replaceAll("\\", "/");
2402
2667
  }
2668
+ function rejectBuildOutputLoads(face, root, aggregatePath, loads, unresolved) {
2669
+ if (loads.length === 0 && unresolved.length === 0) return;
2670
+ const path = (file) => slash(relative(root, file));
2671
+ const config = path(aggregatePath);
2672
+ const lines = [];
2673
+ const addAlias = (specifier, source) => {
2674
+ lines.push(` add ${JSON.stringify(specifier)}: [${JSON.stringify(`./${path(source)}`)}] to the compilerOptions.paths of ${config} or the config it extends`);
2675
+ };
2676
+ if (unresolved.length > 0) {
2677
+ lines.push(`typert(${face}): the ${face} program cannot resolve workspace imports, which would analyze as unknown types; map each import below to source:`);
2678
+ for (const entry of unresolved) {
2679
+ lines.push(` '${entry.specifier}' imported by ${path(entry.file)}`);
2680
+ if (entry.source === void 0) lines.push(` add a compilerOptions.paths alias for it to ${config} or the config it extends`);
2681
+ else addAlias(entry.specifier, entry.source);
2682
+ }
2683
+ }
2684
+ if (loads.length > 0) lines.push(`typert(${face}): the ${face} program loads workspace build outputs instead of source; resolve each import below to source so that generation does not depend on package compilation:`);
2685
+ for (const load of loads) {
2686
+ lines.push(` ${path(load.file)}`);
2687
+ if (load.importers.length === 0) lines.push(" no program source imports it directly; another build output or a reference directive loads it");
2688
+ for (const importer of load.importers) lines.push(` imported as '${importer.specifier}' by ${path(importer.file)}`);
2689
+ const specifiers = [...new Set(load.importers.map((importer) => importer.specifier))];
2690
+ if (load.source === void 0) lines.push(" no workspace source compiles to it; import a source module instead");
2691
+ else for (const specifier of specifiers) addAlias(specifier, load.source);
2692
+ }
2693
+ throw new TypertAnalysisError(lines.join("\n"));
2694
+ }
2403
2695
  function uniqueBy(values, key) {
2404
2696
  const result = /* @__PURE__ */ new Map();
2405
2697
  for (const value of values) if (!result.has(key(value))) result.set(key(value), value);
@@ -2755,6 +3047,148 @@ function assertNever(value) {
2755
3047
  throw new TypeGraphRenderError(`unsupported model variant ${JSON.stringify(value)}`);
2756
3048
  }
2757
3049
  //#endregion
3050
+ //#region lib/types/tool-schema.js
3051
+ /** Lossless projection into the harness tool-schema subset. */
3052
+ /** A resolved codec whose type the tool-schema subset cannot represent without weakening constraints. */
3053
+ var ToolSchemaError = class extends Error {
3054
+ name = "ToolSchemaError";
3055
+ };
3056
+ /**
3057
+ * Project one resolved type into the tool-schema subset that `@deepseek-ai/dsh-tools`
3058
+ * enforces on tool parameters, tool output, and the generated PTC SDK. A type the
3059
+ * subset cannot express without weakening constraints raises {@link ToolSchemaError},
3060
+ * which callers report as an absent projection rather than a narrower schema.
3061
+ * @param renderer - complete analyzed type graph.
3062
+ * @param type - resolved wire type.
3063
+ * @param closedObjects - close declared request objects with `additionalProperties: false`.
3064
+ * @param optional - omit a top-level undefined alternative supplied by an optional argument.
3065
+ * @returns an acyclic schema in the tool-schema subset.
3066
+ */
3067
+ function projectToolSchema(renderer, type, closedObjects = false, optional = false) {
3068
+ const active = /* @__PURE__ */ new Set();
3069
+ const fail = (subject) => {
3070
+ throw new ToolSchemaError(`typert tool schema: ${subject}`);
3071
+ };
3072
+ const project = (id, optional = false) => {
3073
+ const node = renderer.node(id);
3074
+ if (node.json) return {};
3075
+ if (active.has(id)) return fail("recursive selected type requires a supported JSON value declaration");
3076
+ active.add(id);
3077
+ try {
3078
+ switch (node.kind) {
3079
+ case "keyword":
3080
+ if ([
3081
+ "string",
3082
+ "number",
3083
+ "boolean"
3084
+ ].includes(node.name)) return { type: node.name };
3085
+ return fail(`unsupported ${node.name} value`);
3086
+ case "literal":
3087
+ if (typeof node.value === "bigint") return fail("bigint is not JSON");
3088
+ return {
3089
+ type: node.value === null ? "null" : typeof node.value,
3090
+ const: node.value
3091
+ };
3092
+ case "parenthesized": return project(node.type, optional);
3093
+ case "array": return {
3094
+ type: "array",
3095
+ items: project(node.element)
3096
+ };
3097
+ case "object": return object(node.members);
3098
+ case "reference": {
3099
+ if (node.target.kind !== "declaration" || node.arguments.length !== 0) return fail(`unsupported selected reference ${node.name}`);
3100
+ const declaration = renderer.declaration(node.target.symbol);
3101
+ const schema = declaration.kind === "alias" && declaration.type !== void 0 ? project(declaration.type, optional) : object(declaration.members);
3102
+ if (declaration.extends.length > 0) return fail("unresolved inherited selected type");
3103
+ return declaration.description === void 0 ? schema : {
3104
+ ...schema,
3105
+ description: declaration.description
3106
+ };
3107
+ }
3108
+ case "union": return union(node.types.filter((item) => {
3109
+ const branch = renderer.node(item);
3110
+ return !(optional && branch.kind === "keyword" && branch.name === "undefined");
3111
+ }).map((item) => project(item)));
3112
+ default: return fail(`unsupported selected ${node.kind} type`);
3113
+ }
3114
+ } finally {
3115
+ active.delete(id);
3116
+ }
3117
+ };
3118
+ const object = (members) => {
3119
+ const properties = {};
3120
+ const required = [];
3121
+ let indexed = false;
3122
+ for (const member of members) {
3123
+ if (member.static || member.visibility !== "public" || member.computed === "symbol") continue;
3124
+ if (member.kind === "index") {
3125
+ const key = member.signature.parameters[0];
3126
+ if (key === void 0 || project(key.type).type !== "string" || Object.keys(project(member.signature.returns)).length !== 0) return fail("selected dictionaries require string keys and unconstrained JSON values");
3127
+ indexed = true;
3128
+ continue;
3129
+ }
3130
+ if (member.kind !== "property" || member.computed === "dynamic") return fail(`unsupported member ${member.name}`);
3131
+ const name = member.jsonName ?? member.name;
3132
+ const schema = project(member.type, member.optional);
3133
+ properties[name] = member.description === void 0 ? schema : {
3134
+ ...schema,
3135
+ description: member.description
3136
+ };
3137
+ if (!member.optional) required.push(name);
3138
+ }
3139
+ return {
3140
+ type: "object",
3141
+ properties,
3142
+ required,
3143
+ additionalProperties: indexed || !closedObjects
3144
+ };
3145
+ };
3146
+ const union = (input) => {
3147
+ if (input.some((branch) => Object.keys(branch).length === 0)) return {};
3148
+ const branches = [];
3149
+ for (const branch of input) {
3150
+ const previous = branch.type !== void 0 && !["object", "array"].includes(branch.type) ? branches.find((candidate) => candidate.type === branch.type) : void 0;
3151
+ if (previous === void 0) {
3152
+ branches.push(branch);
3153
+ continue;
3154
+ }
3155
+ const values = literals(previous);
3156
+ const next = literals(branch);
3157
+ if (values === void 0 || next === void 0) {
3158
+ delete previous.const;
3159
+ delete previous.enum;
3160
+ } else {
3161
+ delete previous.const;
3162
+ previous.enum = [...new Set([...values, ...next])];
3163
+ }
3164
+ }
3165
+ if (branches.length === 1) return branches[0];
3166
+ if (branches.length === 0) return fail("empty selected union");
3167
+ for (const [index, left] of branches.entries()) for (const right of branches.slice(index + 1)) if (!disjoint(left, right)) return fail("selected union branches overlap or have no required discriminator");
3168
+ return { oneOf: branches };
3169
+ };
3170
+ return project(type, optional);
3171
+ }
3172
+ function literals(schema) {
3173
+ if (schema.const !== void 0) return [schema.const];
3174
+ return schema.enum;
3175
+ }
3176
+ function disjoint(left, right) {
3177
+ if (left.oneOf !== void 0) return left.oneOf.every((branch) => disjoint(branch, right));
3178
+ if (right.oneOf !== void 0) return right.oneOf.every((branch) => disjoint(left, branch));
3179
+ if (left.type !== right.type) return left.type !== void 0 && right.type !== void 0;
3180
+ if (left.type !== "object" || right.type !== "object") return false;
3181
+ return (left.required ?? []).some((name) => {
3182
+ if (!right.required?.includes(name)) return false;
3183
+ const a = left.properties?.[name];
3184
+ const b = right.properties?.[name];
3185
+ if (a === void 0 || b === void 0) return false;
3186
+ const first = literals(a);
3187
+ const second = literals(b);
3188
+ return first !== void 0 && second !== void 0 && first.every((value) => !second.includes(value));
3189
+ });
3190
+ }
3191
+ //#endregion
2758
3192
  //#region lib/types/emitter.js
2759
3193
  /**
2760
3194
  * Model-driven Typert artifact emitter. It consumes only FaceModel and
@@ -2785,7 +3219,7 @@ var FaceModelEmitter = class {
2785
3219
  emit(packageName) {
2786
3220
  const packageModel = this.face.packages.find((candidate) => candidate.name === packageName);
2787
3221
  if (packageModel === void 0) throw new TypertEmitError(`typert emitter(${this.face.face}): package ${packageName} is not modeled on this face`);
2788
- const schemaArtifact = new SchemaEmitter(this.renderer, packageModel.schemas, invocationBoundaryRoots(packageModel.invocations)).emit();
3222
+ const schemaArtifact = invocationSchemas(this.renderer, packageModel.schemas, invocationBoundaryRoots(packageModel.invocations));
2789
3223
  const runtimeModel = this.runtimeModel(packageModel);
2790
3224
  const js = this.renderJs(packageModel, schemaArtifact, runtimeModel);
2791
3225
  const dts = this.renderDts(packageModel, schemaArtifact);
@@ -2850,10 +3284,12 @@ var FaceModelEmitter = class {
2850
3284
  renderJs(packageModel, schemas, runtimeModel) {
2851
3285
  const lines = ["/* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */"];
2852
3286
  if (schemas.definitions.length > 0) lines.push("import { z } from 'zod'", "");
3287
+ if (packageModel.invocations.length > 0) lines.push("import { createTypertRemoteInfo } from '@deepseek-ai/dsh-typert-protocol/remote-info'", "");
2853
3288
  lines.push(...schemas.definitions);
2854
3289
  if (schemas.definitions.length > 0) lines.push("");
2855
3290
  for (const schema of schemas.exports) lines.push(`export const ${schema.exportName} = ${schema.internalName}`);
2856
3291
  if (schemas.exports.length > 0) lines.push("");
3292
+ if (packageModel.invocations.length > 0) lines.push(`const $remote = ${this.remoteInfoLiteral(packageModel, schemas)}`, "");
2857
3293
  const model = JSON.stringify(runtimeModel, null, 2);
2858
3294
  lines.push("export const TYPERT = {");
2859
3295
  lines.push(` package: ${quote$1(packageModel.name)},`);
@@ -2861,9 +3297,8 @@ var FaceModelEmitter = class {
2861
3297
  lines.push(" schemas: [");
2862
3298
  for (const schema of schemas.exports) lines.push(` { name: ${quote$1(schema.exportName)}, create: ${schema.exportName} },`);
2863
3299
  lines.push(" ],");
2864
- lines.push(" invocations: [");
2865
- for (const invocation of packageModel.invocations) lines.push(`${indent(this.invocationLiteral(invocation, schemas), 4)},`);
2866
- lines.push(" ],");
3300
+ if (packageModel.invocations.length > 0) lines.push(" get invocations() { return $remote.descriptors },");
3301
+ else lines.push(" invocations: [", " ],");
2867
3302
  lines.push(` model: ${indent(model, 2).trimStart()},`);
2868
3303
  lines.push("}");
2869
3304
  return `${lines.join("\n")}\n`;
@@ -2886,32 +3321,47 @@ var FaceModelEmitter = class {
2886
3321
  return `${lines.join("\n")}\n`;
2887
3322
  }
2888
3323
  emitRemote(packageModel) {
2889
- const schemas = new SchemaEmitter(this.renderer, [], invocationBoundaryRoots(packageModel.invocations)).emit();
3324
+ const schemas = invocationSchemas(this.renderer, [], invocationBoundaryRoots(packageModel.invocations));
2890
3325
  const lines = ["/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */"];
2891
3326
  if (schemas.definitions.length > 0) lines.push("import { z } from 'zod'", "");
3327
+ lines.push("import { createTypertRemoteInfo } from '@deepseek-ai/dsh-typert-protocol/remote-info'", "");
2892
3328
  lines.push(...schemas.definitions);
2893
3329
  if (schemas.definitions.length > 0) lines.push("");
2894
- lines.push("export const TYPERT_REMOTE = {");
2895
- lines.push(` package: ${quote$1(packageModel.name)},`);
2896
- lines.push(" descriptors: [");
2897
- for (const invocation of packageModel.invocations) lines.push(`${indent(this.invocationLiteral(invocation, schemas), 4)},`);
2898
- lines.push(" ],");
2899
- lines.push("}");
2900
- lines.push("");
3330
+ lines.push(`export const TYPERT_REMOTE = ${this.remoteInfoLiteral(packageModel, schemas)}`, "");
2901
3331
  lines.push("export default TYPERT_REMOTE");
2902
- const declaration = this.renderRemoteDts(packageModel);
2903
3332
  return {
2904
3333
  js: `${lines.join("\n")}\n`,
2905
- ...declaration
3334
+ ...this.renderRemoteDts(packageModel)
2906
3335
  };
2907
3336
  }
3337
+ remoteMethodTypes(packageModel) {
3338
+ const names = allocateRemoteImportNames(remoteImports(packageModel.invocations));
3339
+ return `{\n${packageModel.invocations.filter((invocation) => invocation.invocation.kind === "direct").map((invocation) => {
3340
+ const input = this.remoteInputType(invocation, names);
3341
+ const result = this.renderer.renderType(invocation.result.type, names);
3342
+ const uplink = invocation.uplink === void 0 ? "" : `, ${this.renderer.renderType(invocation.uplink.boundary.type, names)}`;
3343
+ const mode = invocation.mode === "stream" ? `, ${quote$1("stream")}${uplink}` : containsBytes(this.renderer, invocation.result.codecType) ? `, 'unary', never, RemoteDecoded<${result}>` : "";
3344
+ return ` readonly ${quote$1(`${invocation.namespace}/${invocation.method}`)}: TypertRemoteMethod<${input}, ${result}${mode}>`;
3345
+ }).join("\n")}\n}`;
3346
+ }
3347
+ remoteInputType(invocation, names) {
3348
+ return `{ ${invocation.parameters.map((parameter) => `readonly ${quote$1(parameter.wire)}${parameter.boundary.acceptsUndefined ? "?" : ""}: ${this.renderer.renderType(parameter.boundary.type, names)}`).join("; ")} }`;
3349
+ }
3350
+ remoteInfoLiteral(packageModel, schemas) {
3351
+ return [
3352
+ "createTypertRemoteInfo({",
3353
+ ` package: ${quote$1(packageModel.name)},`,
3354
+ " methods: {",
3355
+ ...packageModel.invocations.map((invocation) => ` ${quote$1(`${invocation.namespace}/${invocation.method}`)}: ${indent(this.invocationLiteral(invocation, schemas), 4).trimStart()},`),
3356
+ " },",
3357
+ "}, z)"
3358
+ ].join("\n");
3359
+ }
2908
3360
  invocationLiteral(invocation, schemas) {
2909
3361
  const lines = [
2910
3362
  "{",
2911
- ` id: ${quote$1(invocation.id)},`,
2912
3363
  ` service: ${quote$1(invocation.service)},`,
2913
- ` namespace: ${quote$1(invocation.namespace)},`,
2914
- ` method: ${quote$1(invocation.method)},`
3364
+ ...invocation.description === void 0 ? [] : [` description: ${quote$1(invocation.description)},`]
2915
3365
  ];
2916
3366
  if (invocation.implementation !== void 0) lines.push(` implementation: ${quote$1(invocation.implementation)},`);
2917
3367
  if (invocation.mode !== void 0) lines.push(` mode: ${quote$1(invocation.mode)},`);
@@ -2934,11 +3384,12 @@ var FaceModelEmitter = class {
2934
3384
  invocation.parameters.forEach((parameter, index) => {
2935
3385
  lines.push(" {");
2936
3386
  lines.push(` name: ${quote$1(parameter.name)},`);
2937
- lines.push(` wire: ${quote$1(parameter.wire)},`);
3387
+ if (parameter.wire !== parameter.name) lines.push(` wire: ${quote$1(parameter.wire)},`);
3388
+ if (parameter.description !== void 0) lines.push(` description: ${quote$1(parameter.description)},`);
2938
3389
  lines.push(` source: ${quote$1(parameter.source)},`);
2939
3390
  if (parameter.lookup !== void 0) lines.push(` lookup: ${quote$1(parameter.lookup)},`);
2940
3391
  if (parameter.boundary.acceptsUndefined) lines.push(" acceptsUndefined: true,");
2941
- lines.push(` codec: ${indent(strictCodec(parameter.boundary, schemas.boundary(parameterBoundaryKey(invocation, index))), 6).trimStart()},`);
3392
+ for (const strict of [false, true]) lines.push(` ${strict ? "strictCodec" : "codec"}: ${indent(strictCodec(parameter.boundary, schemas.boundary(parameterBoundaryKey(invocation, index, strict)), void 0, toolProjection(() => projectToolSchema(this.renderer, parameter.boundary.codecType, strict, parameter.boundary.acceptsUndefined))), 6).trimStart()},`);
2942
3393
  lines.push(" },");
2943
3394
  });
2944
3395
  lines.push(" ],");
@@ -2948,7 +3399,8 @@ var FaceModelEmitter = class {
2948
3399
  lines.push(" },");
2949
3400
  }
2950
3401
  if (invocation.cancellation !== void 0) lines.push(" cancellation: { parameter: 'signal' },");
2951
- lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation)), schemas.encoder(resultBoundaryKey(invocation))), 2).trimStart()},`);
3402
+ const returns = invocation.tags.find((tag) => tag.name === "returns" || tag.name === "return")?.comment?.replace(/^[-—–]\s*/, "");
3403
+ lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation)), schemas.encoder(resultBoundaryKey(invocation)), toolProjection(() => projectToolSchema(this.renderer, invocation.result.codecType)), returns), 2).trimStart()},`);
2952
3404
  lines.push(` sourceLocation: ${JSON.stringify(invocation.location)},`);
2953
3405
  lines.push("}");
2954
3406
  return lines.join("\n");
@@ -2968,9 +3420,11 @@ var FaceModelEmitter = class {
2968
3420
  const lines = [
2969
3421
  "/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */",
2970
3422
  "import type {",
3423
+ ...packageModel.invocations.some((invocation) => containsBytes(this.renderer, invocation.result.codecType)) ? [" RemoteDecoded,"] : [],
2971
3424
  " RemoteResult,",
2972
3425
  ...packageModel.invocations.some((invocation) => invocation.mode === "stream") ? [" RemoteStreamHandle,"] : [],
2973
- " TypertRemoteContribution,",
3426
+ " TypertRemoteInfo,",
3427
+ ...packageModel.invocations.some((invocation) => invocation.invocation.kind === "direct") ? [" TypertRemoteMethod,"] : [],
2974
3428
  "} from '@deepseek-ai/dsh-typert-protocol'"
2975
3429
  ];
2976
3430
  const sourceMap = new GenMapping({ file: "typert.remote-client.d.ts" });
@@ -2979,12 +3433,6 @@ var FaceModelEmitter = class {
2979
3433
  lines.push(`import type { ${names.join(", ")} } from ${quote$1(specifier)}`);
2980
3434
  }
2981
3435
  lines.push("");
2982
- if (packageModel.invocations.some((invocation) => containsBytes(this.renderer, invocation.result.codecType))) {
2983
- lines.push("type RemoteDecoded<Value> = Value extends Uint8Array ? Uint8Array<ArrayBuffer>");
2984
- lines.push(" : Value extends string | number | boolean | null | undefined ? Value");
2985
- lines.push(" : { [Key in keyof Value]: RemoteDecoded<Value[Key]> }");
2986
- lines.push("");
2987
- }
2988
3436
  lines.push("declare module '@deepseek-ai/dsh-typert-protocol' {");
2989
3437
  const direct = packageModel.invocations.filter((invocation) => invocation.invocation.kind === "direct");
2990
3438
  const scoped = packageModel.invocations.filter((invocation) => invocation.invocation.kind === "context" || invocation.scope !== void 0);
@@ -3008,7 +3456,7 @@ var FaceModelEmitter = class {
3008
3456
  }
3009
3457
  lines.push("}");
3010
3458
  lines.push("");
3011
- lines.push("export declare const TYPERT_REMOTE: TypertRemoteContribution");
3459
+ lines.push(`export declare const TYPERT_REMOTE: TypertRemoteInfo<${this.remoteMethodTypes(packageModel)}>`);
3012
3460
  lines.push("export default TYPERT_REMOTE");
3013
3461
  lines.push("//# sourceMappingURL=typert.remote-client.d.ts.map");
3014
3462
  return {
@@ -3075,6 +3523,27 @@ function uniqueNamespaces(invocations) {
3075
3523
  function remoteNamespaceInterface(namespace) {
3076
3524
  return `TypertRemoteNamespace$${Buffer.from(namespace, "utf8").toString("hex")}`;
3077
3525
  }
3526
+ function toolProjection(project) {
3527
+ try {
3528
+ return `jsonSchema: ${JSON.stringify(project())}`;
3529
+ } catch (error) {
3530
+ if (!(error instanceof ToolSchemaError)) throw error;
3531
+ return `jsonSchemaError: ${quote$1(error.message)}`;
3532
+ }
3533
+ }
3534
+ function invocationSchemas(renderer, schemas, roots) {
3535
+ const identifiers = /* @__PURE__ */ new Set();
3536
+ const ordinary = new SchemaEmitter(renderer, schemas, roots.filter((root) => !root.closedObjects), {}, identifiers).emit();
3537
+ const closedRoots = roots.filter((root) => root.closedObjects);
3538
+ const closed = new SchemaEmitter(renderer, [], closedRoots, { closedObjects: true }, identifiers).emit();
3539
+ const keys = new Set(closedRoots.map((root) => root.key));
3540
+ return {
3541
+ definitions: [...ordinary.definitions, ...closed.definitions],
3542
+ exports: ordinary.exports,
3543
+ boundary: (key) => keys.has(key) ? closed.boundary(key) : ordinary.boundary(key),
3544
+ encoder: (key) => keys.has(key) ? closed.encoder(key) : ordinary.encoder(key)
3545
+ };
3546
+ }
3078
3547
  var ResultEncoderEmitter = class {
3079
3548
  renderer;
3080
3549
  names = /* @__PURE__ */ new Map();
@@ -3173,20 +3642,21 @@ var SchemaEmitter = class {
3173
3642
  renderer;
3174
3643
  schemas;
3175
3644
  boundaries;
3645
+ options;
3176
3646
  names = /* @__PURE__ */ new Map();
3177
3647
  boundaryNames = /* @__PURE__ */ new Map();
3178
3648
  declarations;
3179
- constructor(renderer, schemas, boundaries) {
3649
+ constructor(renderer, schemas, boundaries, options = {}, identifiers = /* @__PURE__ */ new Set()) {
3180
3650
  this.renderer = renderer;
3181
3651
  this.schemas = schemas;
3182
3652
  this.boundaries = boundaries;
3653
+ this.options = options;
3183
3654
  const declarations = /* @__PURE__ */ new Map();
3184
3655
  for (const schema of schemas) for (const declaration of renderer.declarationClosureForTypes([schema.type])) declarations.set(declaration.id, declaration);
3185
3656
  for (const boundary of boundaries) for (const declaration of renderer.declarationClosureForTypes([boundary.type])) declarations.set(declaration.id, declaration);
3186
3657
  this.declarations = renderer.graph.declarations.filter((declaration) => declarations.has(declaration.id));
3187
- const identifiers = /* @__PURE__ */ new Set();
3188
3658
  for (const declaration of this.declarations) {
3189
- const base = `${safeIdentifier(declaration.name)}$schema`;
3659
+ const base = `${options.closedObjects ? "closed$" : ""}${safeIdentifier(declaration.name)}$schema`;
3190
3660
  let name = base;
3191
3661
  let suffix = 2;
3192
3662
  while (identifiers.has(name)) name = `${base}${String(suffix++)}`;
@@ -3210,7 +3680,7 @@ var SchemaEmitter = class {
3210
3680
  const encode = encoder.reference(boundary.type);
3211
3681
  if (encode !== void 0) encoders.set(boundary.key, encode);
3212
3682
  const name = this.boundaryName(boundary.key);
3213
- definitions.push(`let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.typeSchema(boundary.type)})`);
3683
+ definitions.push(this.cachedFactory(name, this.typeSchema(boundary.type)));
3214
3684
  }
3215
3685
  definitions.push(...encoder.definitions());
3216
3686
  return {
@@ -3226,11 +3696,14 @@ var SchemaEmitter = class {
3226
3696
  }
3227
3697
  declarationDefinition(declaration) {
3228
3698
  const name = this.schemaName(declaration.id);
3229
- if (declaration.typeParameters.length === 0) return `let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.declarationSchema(declaration, /* @__PURE__ */ new Map())})`;
3699
+ if (declaration.typeParameters.length === 0) return this.cachedFactory(name, this.declarationSchema(declaration, /* @__PURE__ */ new Map()));
3230
3700
  const parameters = declaration.typeParameters.map((parameter, index) => [`type${String(index)}$schema`, parameter.id]);
3231
3701
  const substitutions = new Map(parameters.map(([schema, id]) => [id, schema]));
3232
3702
  return `const ${name} = (${parameters.map(([schema]) => schema).join(", ")}) => ${this.declarationSchema(declaration, substitutions)}`;
3233
3703
  }
3704
+ cachedFactory(name, expression) {
3705
+ return `let ${name}$value\nconst ${name} = () => (${name}$value ??= ${expression})`;
3706
+ }
3234
3707
  declarationSchema(declaration, substitutions) {
3235
3708
  if (declaration.kind === "enum") this.fail(declaration.name, "enum declarations have no Zod projection");
3236
3709
  if (declaration.kind === "alias") {
@@ -3243,6 +3716,7 @@ var SchemaEmitter = class {
3243
3716
  }
3244
3717
  typeSchema(id, substitutions = /* @__PURE__ */ new Map()) {
3245
3718
  const node = this.renderer.node(id);
3719
+ if (node.json) return "z.json()";
3246
3720
  switch (node.kind) {
3247
3721
  case "keyword": return this.keywordSchema(node.name);
3248
3722
  case "literal": return `z.literal(${node.text})`;
@@ -3340,6 +3814,8 @@ var SchemaEmitter = class {
3340
3814
  objectSchema(members, subject, substitutions) {
3341
3815
  const properties = [];
3342
3816
  const indices = [];
3817
+ let stringIndexValue;
3818
+ let stringIndexType;
3343
3819
  let symbolMembers = 0;
3344
3820
  for (const member of members) {
3345
3821
  if (member.static || member.visibility !== "public") continue;
@@ -3351,22 +3827,53 @@ var SchemaEmitter = class {
3351
3827
  if (member.kind === "index") {
3352
3828
  const parameter = member.signature.parameters[0];
3353
3829
  if (member.signature.parameters.length !== 1 || parameter === void 0) this.fail(subject, "index signature must have exactly one key parameter");
3354
- indices.push(this.readonly(`z.record(${this.typeSchema(parameter.type, substitutions)}, ${this.typeSchema(member.signature.returns, substitutions)})`, member.readonly));
3830
+ const key = this.typeSchema(parameter.type, substitutions);
3831
+ const value = this.typeSchema(member.signature.returns, substitutions);
3832
+ if (key === "z.string()") {
3833
+ stringIndexValue = value;
3834
+ stringIndexType = member.signature.returns;
3835
+ }
3836
+ indices.push(this.readonly(`z.record(${key}, ${value})`, member.readonly));
3355
3837
  continue;
3356
3838
  }
3357
3839
  if (member.kind !== "property") this.fail(subject, `${member.kind} member ${member.name} is not data-schema projectable`);
3358
- const schema = this.readonly(this.typeSchema(member.type, substitutions), member.readonly && !containsBytes(this.renderer, member.type, false));
3359
- const property = this.describe(this.optional(schema, member.optional), member);
3360
- properties.push(`${quote$1(member.jsonName ?? member.name)}: ${property}`);
3840
+ properties.push(member);
3361
3841
  }
3362
3842
  if (indices.length > 1) this.fail(subject, "object type has more than one JSON index signature");
3363
3843
  if (properties.length === 0 && indices.length === 0 && symbolMembers > 0) return "z.unknown()";
3364
- const object = `z.object({${properties.length === 0 ? "" : `\n${properties.map((property) => ` ${property},`).join("\n")}\n`}})`;
3844
+ const fields = properties.map((member) => {
3845
+ const type = this.options.closedObjects && stringIndexType !== void 0 ? this.indexedPropertySchema(member.type, stringIndexType, substitutions) : this.typeSchema(member.type, substitutions);
3846
+ const schema = this.readonly(type, member.readonly && !containsBytes(this.renderer, member.type, false));
3847
+ return `${quote$1(member.jsonName ?? member.name)}: ${this.describe(this.optional(schema, member.optional), member)}`;
3848
+ });
3849
+ const object = `z.${this.options.closedObjects && indices.length === 0 ? "strictObject" : "object"}({${fields.length === 0 ? "" : `\n${fields.map((field) => ` ${field},`).join("\n")}\n`}})`;
3365
3850
  const index = indices[0];
3366
3851
  if (index === void 0) return object;
3367
3852
  if (properties.length === 0) return index;
3853
+ if (this.options.closedObjects && stringIndexValue !== void 0) return `${object}.catchall(${stringIndexValue})`;
3368
3854
  return `z.intersection(${object}, ${index})`;
3369
3855
  }
3856
+ indexedPropertySchema(declared, indexed, substitutions) {
3857
+ if (declared === indexed) return this.typeSchema(declared, substitutions);
3858
+ const own = this.renderer.node(declared);
3859
+ const value = this.renderer.node(indexed);
3860
+ if (own.kind === "union") return `z.union([${own.types.map((type) => this.indexedPropertySchema(type, indexed, substitutions)).join(", ")}])`;
3861
+ if (value.kind === "union") return `z.union([${value.types.map((type) => this.indexedPropertySchema(declared, type, substitutions)).join(", ")}])`;
3862
+ const ownProperties = own.kind === "object" ? own.members.filter((member) => member.kind === "property") : [];
3863
+ const valueProperties = value.kind === "object" ? value.members.filter((member) => member.kind === "property") : [];
3864
+ if (own.kind === "object" && value.kind === "object" && !own.json && !value.json && ownProperties.length === own.members.length && valueProperties.length === value.members.length && [...ownProperties, ...valueProperties].every((member) => !member.static && member.visibility === "public" && member.computed === void 0)) {
3865
+ const indexedFields = new Map(valueProperties.map((member) => [member.jsonName ?? member.name, member]));
3866
+ const overlaps = ownProperties.flatMap((member) => {
3867
+ const inherited = indexedFields.get(member.jsonName ?? member.name);
3868
+ if (inherited === void 0) return [];
3869
+ const combined = this.indexedPropertySchema(member.type, inherited.type, substitutions);
3870
+ const schema = this.readonly(this.optional(combined, member.optional && inherited.optional), (member.readonly || inherited.readonly) && !containsBytes(this.renderer, member.type, false));
3871
+ return [`${quote$1(member.jsonName ?? member.name)}: ${this.describe(schema, member)}`];
3872
+ });
3873
+ return `z.strictObject({ ...${this.typeSchema(indexed, substitutions)}.shape, ...${this.typeSchema(declared, substitutions)}.shape${overlaps.length === 0 ? "" : `, ${overlaps.join(", ")}`} })`;
3874
+ }
3875
+ return `z.intersection(${this.typeSchema(declared, substitutions)}, ${this.typeSchema(indexed, substitutions)})`;
3876
+ }
3370
3877
  exportSchemaName(model) {
3371
3878
  const name = this.schemaName(model.symbol);
3372
3879
  if (this.renderer.declaration(model.symbol).typeParameters.length > 0) this.fail(model.export.name, "generic schema exports require a concrete declaration");
@@ -3434,6 +3941,11 @@ function invocationBoundaryRoots(invocations) {
3434
3941
  key: parameterBoundaryKey(invocation, index),
3435
3942
  type: parameter.boundary.codecType
3436
3943
  });
3944
+ result.push({
3945
+ key: parameterBoundaryKey(invocation, index, true),
3946
+ type: parameter.boundary.codecType,
3947
+ closedObjects: true
3948
+ });
3437
3949
  });
3438
3950
  if (invocation.uplink !== void 0) result.push({
3439
3951
  key: uplinkBoundaryKey(invocation),
@@ -3452,8 +3964,8 @@ function contextBoundaryKey(invocation) {
3452
3964
  function uplinkBoundaryKey(invocation) {
3453
3965
  return `${invocation.id}:uplink`;
3454
3966
  }
3455
- function parameterBoundaryKey(invocation, index) {
3456
- return `${invocation.id}:parameter:${String(index)}`;
3967
+ function parameterBoundaryKey(invocation, index, strict = false) {
3968
+ return `${invocation.id}:parameter:${String(index)}${strict ? ":strict" : ""}`;
3457
3969
  }
3458
3970
  function resultBoundaryKey(invocation) {
3459
3971
  return `${invocation.id}:result`;
@@ -3474,12 +3986,14 @@ function containsBytes(renderer, id, nested = true, visited = /* @__PURE__ */ ne
3474
3986
  if (node.kind === "object" && node.members.some((member) => containsBytes(renderer, member.kind === "property" ? member.type : member.signature.returns, nested, visited))) return true;
3475
3987
  return childTypeNodeIds(node).some((child) => containsBytes(renderer, child, nested, visited));
3476
3988
  }
3477
- function strictCodec(boundary, schema, encoder) {
3989
+ function strictCodec(boundary, schema, encoder, projection, description) {
3478
3990
  return [
3479
3991
  "{",
3480
3992
  " mode: 'strict',",
3481
3993
  ` typeSymbol: ${quote$1(boundary.typeSymbol)},`,
3482
3994
  ` create: ${schema},`,
3995
+ ...description === void 0 ? [] : [` description: ${quote$1(description)},`],
3996
+ ...projection === void 0 ? [] : [` ${projection},`],
3483
3997
  ...encoder === void 0 ? [] : [` decode: value => ${schema}().parse(value),`, ` encode: (value, writeBytes) => ${encoder}(value, writeBytes, [], new Map()),`],
3484
3998
  "}"
3485
3999
  ].join("\n");
@@ -3506,7 +4020,8 @@ function allocateRemoteImportNames(imports) {
3506
4020
  "RemoteDecoded",
3507
4021
  "RemoteResult",
3508
4022
  "RemoteStreamHandle",
3509
- "TypertRemoteContribution",
4023
+ "TypertRemoteInfo",
4024
+ "TypertRemoteMethod",
3510
4025
  "TYPERT_REMOTE"
3511
4026
  ]);
3512
4027
  const names = /* @__PURE__ */ new Map();
@@ -4274,34 +4789,38 @@ var WorkspaceTypertGenerator = class {
4274
4789
  * Find public package faces that contribute Cordis services/events or
4275
4790
  * explicitly tagged Typert roots.
4276
4791
  * @param faces - optional independent program faces to inspect.
4792
+ * @param includeManifest - optional `package.json` predicate; a rejected package's sources and project config are not read.
4277
4793
  * @returns discovered packages in stable package-name order.
4278
4794
  */
4279
- discover(faces) {
4795
+ discover(faces, includeManifest) {
4280
4796
  return new WorkspaceAnalyzer({
4281
4797
  root: this.root,
4282
4798
  caches: this.caches,
4283
4799
  ...faces === void 0 ? {} : { faces }
4284
- }).discoverPackages();
4800
+ }).discoverPackages(includeManifest);
4285
4801
  }
4286
4802
  /**
4287
4803
  * Generate all discovered contributors, or an explicit package subset.
4288
- * @param packages - optional exact package names for a focused pass.
4804
+ * @param packages - optional exact package names to emit; every pass includes the same face-wide declaration merges.
4289
4805
  * @param faces - optional independent program faces to analyze.
4290
4806
  * @returns one artifact per package face.
4291
4807
  */
4292
4808
  generate(packages, faces) {
4293
- const selected = packages ?? this.discover(faces).map((candidate) => candidate.package);
4809
+ const contributors = this.discover(faces).map((candidate) => candidate.package);
4810
+ const selected = new Set(packages ?? contributors);
4294
4811
  const workspace = new WorkspaceAnalyzer({
4295
4812
  root: this.root,
4296
- packages: selected,
4813
+ packages: contributors,
4297
4814
  caches: this.caches,
4298
4815
  ...faces === void 0 ? {} : { faces },
4299
- ...this.options.checkDiagnostics === void 0 ? {} : { checkDiagnostics: this.options.checkDiagnostics }
4816
+ ...this.options.checkDiagnostics === void 0 ? {} : { checkDiagnostics: this.options.checkDiagnostics },
4817
+ ...this.options.rejectBuildOutputs === void 0 ? {} : { rejectBuildOutputs: this.options.rejectBuildOutputs }
4300
4818
  }).analyze();
4301
4819
  const artifacts = [];
4302
4820
  for (const face of workspace.faces) {
4303
4821
  const emitter = new FaceModelEmitter(face);
4304
4822
  for (const packageModel of face.packages) {
4823
+ if (!selected.has(packageModel.name)) continue;
4305
4824
  const artifact = {
4306
4825
  ...emitter.emit(packageModel.name),
4307
4826
  packageRoot: packageModel.root
@@ -4312,6 +4831,16 @@ var WorkspaceTypertGenerator = class {
4312
4831
  }
4313
4832
  return artifacts;
4314
4833
  }
4834
+ /**
4835
+ * Generate every explicit contributor: a package whose manifest declares a
4836
+ * Typert artifact export and whose public graph has a Typert surface.
4837
+ * @param faces - optional independent program faces to analyze.
4838
+ * @returns one artifact per contributor face.
4839
+ */
4840
+ generateContributors(faces) {
4841
+ const packages = this.discover(faces, (manifest) => hasTypertExport(manifest.exports)).map((candidate) => candidate.package);
4842
+ return packages.length === 0 ? [] : this.generate(packages, faces);
4843
+ }
4315
4844
  validateExport(artifact) {
4316
4845
  const manifestPath = resolve(this.root, artifact.packageRoot, "package.json");
4317
4846
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -4338,10 +4867,90 @@ var WorkspaceTypertGenerator = class {
4338
4867
  for (const file of remoteFiles) if (!files.includes(file)) throw new TypertAnalysisError(`typert(host): ${artifact.package} package files must include ${file}`);
4339
4868
  }
4340
4869
  };
4870
+ /**
4871
+ * Find the workspace root that Typert generation runs against: the nearest
4872
+ * directory at or above `start` that holds both `tsconfig.host.json` and
4873
+ * `packages/`. The analyzer registers projects under `<root>/packages`, so a
4874
+ * package's own `tsconfig.host.json` face leaf does not qualify.
4875
+ * @param start - directory to search from, such as a bundle output or a working directory.
4876
+ * @returns the absolute workspace root.
4877
+ * @throws If no ancestor of `start` qualifies.
4878
+ */
4879
+ function findWorkspaceRoot(start) {
4880
+ let current = resolve(start);
4881
+ while (!existsSync(join(current, "tsconfig.host.json")) || !existsSync(join(current, "packages"))) {
4882
+ const parent = dirname(current);
4883
+ if (parent === current) throw new Error(`typert-generator: cannot find workspace root above ${start}`);
4884
+ current = parent;
4885
+ }
4886
+ return current;
4887
+ }
4888
+ /**
4889
+ * Whether a `package.json#exports` field declares a Typert artifact subpath
4890
+ * (`./typert`, `./client/typert`, or `./remote`).
4891
+ * @param exportsField - the manifest's `exports` value.
4892
+ * @returns true when the package opts in to Typert publication.
4893
+ */
4894
+ function hasTypertExport(exportsField) {
4895
+ if (exportsField === null || typeof exportsField !== "object" || Array.isArray(exportsField)) return false;
4896
+ return Object.hasOwn(exportsField, "./typert") || Object.hasOwn(exportsField, "./client/typert") || Object.hasOwn(exportsField, "./remote");
4897
+ }
4341
4898
  function sameExport(actual, expected) {
4342
4899
  if (actual === null || typeof actual !== "object" || Array.isArray(actual)) return false;
4343
4900
  const value = actual;
4344
4901
  return value.types === expected.types && value.default === expected.default;
4345
4902
  }
4346
4903
  //#endregion
4347
- export { CordisCatalogProjector, FaceModelEmitter, REGION_BEGIN, REGION_END, TypeGraphRenderError, TypeGraphRenderer, TypertAnalysisError, TypertEmitError, WorkspaceAnalyzer, WorkspaceCaches, WorkspaceTypertGenerator, collectEvents, collectServices, projectCordisCatalog, renderInheritedPage, renderPageRegion };
4904
+ //#region lib/types/artifact-files.js
4905
+ /**
4906
+ * Package-local files of generated Typert artifacts. The tsdown plugin and
4907
+ * standalone workspace generation both write through this module, so a pass
4908
+ * over the same artifacts leaves the same bytes on disk.
4909
+ * @module @deepseek-ai/dsh-typert-generator/artifact-files
4910
+ */
4911
+ const REMOTE_CLIENT_FILES = [
4912
+ "typert.remote-client.js",
4913
+ "typert.remote-client.d.ts",
4914
+ "typert.remote-client.d.ts.map"
4915
+ ];
4916
+ /**
4917
+ * Write one package's artifacts under `<packageDir>/lib`: `typert.<face>.js`
4918
+ * and `typert.<face>.d.ts` per artifact, plus the three
4919
+ * `typert.remote-client.*` files when an artifact carries Remote methods. A
4920
+ * Host artifact without Remote methods removes any Remote client files left
4921
+ * by an earlier pass. Files are written one by one, so a failed write can
4922
+ * leave this package's outputs partially replaced.
4923
+ * @param packageDir - absolute package root.
4924
+ * @param artifacts - artifacts of that package only.
4925
+ * @throws A file-system error when the output directory or a file cannot be written.
4926
+ */
4927
+ function writePackageArtifacts(packageDir, artifacts) {
4928
+ const output = join(packageDir, "lib");
4929
+ mkdirSync(output, { recursive: true });
4930
+ let emittedRemote = false;
4931
+ for (const artifact of artifacts) {
4932
+ writeFileSync(join(output, `typert.${artifact.face}.js`), artifact.js);
4933
+ writeFileSync(join(output, `typert.${artifact.face}.d.ts`), artifact.dts);
4934
+ if (artifact.remote !== void 0) {
4935
+ emittedRemote = true;
4936
+ writeFileSync(join(output, "typert.remote-client.js"), artifact.remote.js);
4937
+ writeFileSync(join(output, "typert.remote-client.d.ts"), artifact.remote.dts);
4938
+ writeFileSync(join(output, "typert.remote-client.d.ts.map"), artifact.remote.dtsMap);
4939
+ }
4940
+ }
4941
+ if (!emittedRemote && artifacts.some((artifact) => artifact.face === "host")) for (const file of REMOTE_CLIENT_FILES) rmSync(join(output, file), { force: true });
4942
+ }
4943
+ /**
4944
+ * Write every artifact of one workspace pass into its own package, in
4945
+ * artifact order, with {@link writePackageArtifacts}. A failed write stops
4946
+ * the pass; earlier packages keep their new files and later ones keep their
4947
+ * previous files.
4948
+ * @param root - workspace root that each artifact's `packageRoot` is relative to.
4949
+ * @param artifacts - artifacts returned by one `WorkspaceTypertGenerator` pass.
4950
+ * @throws A file-system error when an output directory or file cannot be written.
4951
+ */
4952
+ function writeWorkspaceArtifacts(root, artifacts) {
4953
+ for (const artifact of artifacts) writePackageArtifacts(join(root, artifact.packageRoot), [artifact]);
4954
+ }
4955
+ //#endregion
4956
+ export { CordisCatalogProjector, FaceModelEmitter, REGION_BEGIN, REGION_END, TypeGraphRenderError, TypeGraphRenderer, TypertAnalysisError, TypertEmitError, WorkspaceAnalyzer, WorkspaceCaches, WorkspaceTypertGenerator, collectEvents, collectServices, findWorkspaceRoot, projectCordisCatalog, renderInheritedPage, renderPageRegion, writePackageArtifacts, writeWorkspaceArtifacts };