@deepseek-ai/dsh-typert-generator 0.2.1-alpha.2 → 0.2.1-alpha.4

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.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Detection of workspace build outputs loaded into an analyzed face program,
3
+ * and of workspace imports that resolve to nothing. Typert reads workspace
4
+ * packages only from source when a caller rejects build outputs, so its
5
+ * result cannot depend on whether, or when, package compilation ran: an
6
+ * import that reaches `lib/` on a compiled tree resolves to nothing on a
7
+ * clean one, and both are rejected.
8
+ * @module @deepseek-ai/dsh-typert-generator/build-output-guard
9
+ */
10
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
11
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
12
+ import ts from 'typescript';
13
+ /**
14
+ * List the program files that a workspace package holds under its `lib/`
15
+ * directory. A package is the nearest directory with a `package.json` at or
16
+ * below `root`; files under `node_modules` are external. Importers are found
17
+ * by resolving each program file's import specifiers again, so this pass
18
+ * costs extra only when an output is present.
19
+ * @param program - the analyzed face program.
20
+ * @param root - workspace root.
21
+ * @param host - module resolution host of the program.
22
+ * @returns loads in file-path order; empty when every workspace file comes from source.
23
+ */
24
+ export function findBuildOutputLoads(program, root, host) {
25
+ const classifier = new BuildOutputClassifier(root);
26
+ const outputs = new Map();
27
+ for (const sourceFile of program.getSourceFiles()) {
28
+ if (classifier.isBuildOutput(sourceFile.fileName))
29
+ outputs.set(classifier.canonical(sourceFile.fileName), []);
30
+ }
31
+ if (outputs.size === 0)
32
+ return [];
33
+ const options = program.getCompilerOptions();
34
+ for (const sourceFile of program.getSourceFiles()) {
35
+ if (!classifier.isWorkspaceFile(sourceFile.fileName) || classifier.isBuildOutput(sourceFile.fileName))
36
+ continue;
37
+ for (const { fileName: specifier } of ts.preProcessFile(sourceFile.text, true, true).importedFiles) {
38
+ const resolved = ts.resolveModuleName(specifier, sourceFile.fileName, options, host).resolvedModule;
39
+ const importers = resolved === undefined ? undefined : outputs.get(classifier.canonical(resolved.resolvedFileName));
40
+ importers?.push({ file: classifier.display(sourceFile.fileName), specifier });
41
+ }
42
+ }
43
+ return [...outputs]
44
+ .sort(([left], [right]) => left.localeCompare(right))
45
+ .map(([file, importers]) => ({ file: classifier.display(file), importers, source: classifier.sourceFor(file) }));
46
+ }
47
+ /**
48
+ * List the imports in workspace program files that name a workspace package
49
+ * and resolve to nothing. A workspace package is one whose `node_modules` link,
50
+ * found from the importing file's directories, leads inside `root` outside any
51
+ * `node_modules`; without that link or a `paths` alias the package cannot be
52
+ * identified, and type checking reports the import. The source of a subpath is
53
+ * derived from the package's `exports` `types` condition under `lib/types/`.
54
+ * @param program - the analyzed face program.
55
+ * @param root - workspace root.
56
+ * @param host - module resolution host of the program.
57
+ * @returns unresolved imports in file and specifier order.
58
+ */
59
+ export function findUnresolvedWorkspaceImports(program, root, host) {
60
+ const classifier = new BuildOutputClassifier(root);
61
+ const options = program.getCompilerOptions();
62
+ const cache = ts.createModuleResolutionCache(root, file => file, options);
63
+ const found = [];
64
+ for (const sourceFile of program.getSourceFiles()) {
65
+ if (!classifier.isWorkspaceFile(sourceFile.fileName) || classifier.isBuildOutput(sourceFile.fileName))
66
+ continue;
67
+ for (const { fileName: specifier } of ts.preProcessFile(sourceFile.text, true, true).importedFiles) {
68
+ if (specifier.startsWith('.') || isAbsolute(specifier) || specifier.includes(':'))
69
+ continue;
70
+ if (ts.resolveModuleName(specifier, sourceFile.fileName, options, host, cache).resolvedModule !== undefined)
71
+ continue;
72
+ const packageRoot = classifier.linkedWorkspacePackage(sourceFile.fileName, packageNameOf(specifier));
73
+ if (packageRoot === undefined)
74
+ continue;
75
+ const source = exportSource(packageRoot, specifier);
76
+ found.push({
77
+ file: classifier.display(sourceFile.fileName),
78
+ specifier,
79
+ source: source === undefined ? undefined : classifier.display(source),
80
+ });
81
+ }
82
+ }
83
+ return found.sort((left, right) => left.file.localeCompare(right.file) || left.specifier.localeCompare(right.specifier));
84
+ }
85
+ function packageNameOf(specifier) {
86
+ const parts = specifier.split('/');
87
+ return parts.slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
88
+ }
89
+ function exportSource(packageRoot, specifier) {
90
+ const packageName = packageNameOf(specifier);
91
+ const subpath = specifier === packageName ? '.' : `.${specifier.slice(packageName.length)}`;
92
+ let manifest;
93
+ try {
94
+ manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
95
+ }
96
+ catch (_unreadableManifest) {
97
+ // The error names the import without a suggested source path.
98
+ return undefined;
99
+ }
100
+ const entry = property(property(manifest, 'exports'), subpath);
101
+ const types = typeof entry === 'string' ? entry : property(entry, 'types');
102
+ if (typeof types !== 'string' || !types.startsWith('./lib/types/'))
103
+ return undefined;
104
+ const stem = join(packageRoot, 'src', types.slice('./lib/types/'.length).replace(/\.d\.([cm]?)ts$/u, '.$1ts'));
105
+ return [stem, `${stem}x`].find(candidate => existsSync(candidate));
106
+ }
107
+ function property(value, key) {
108
+ return typeof value === 'object' && value !== null ? value[key] : undefined;
109
+ }
110
+ /**
111
+ * Classifies program files by their real paths, because module resolution
112
+ * reports symbolic-link targets and Windows long names while the workspace
113
+ * root may be spelled through a link or an 8.3 short name; reported paths are
114
+ * spelled under the caller's root.
115
+ */
116
+ class BuildOutputClassifier {
117
+ root;
118
+ packageRoots = new Map();
119
+ realPaths = new Map();
120
+ realRoot;
121
+ constructor(root) {
122
+ this.root = root;
123
+ this.realRoot = this.canonical(root);
124
+ }
125
+ canonical(file) {
126
+ const absolute = resolve(file);
127
+ const cached = this.realPaths.get(absolute);
128
+ if (cached !== undefined)
129
+ return cached;
130
+ let real = absolute;
131
+ try {
132
+ real = realpathSync.native(absolute);
133
+ }
134
+ catch (_missingPath) {
135
+ // A path that does not exist, such as an unresolved candidate, keeps its resolved spelling.
136
+ }
137
+ this.realPaths.set(absolute, real);
138
+ return real;
139
+ }
140
+ display(file) {
141
+ const path = relative(this.realRoot, this.canonical(file));
142
+ return path.startsWith('..') || isAbsolute(path) ? resolve(file) : join(this.root, path);
143
+ }
144
+ isWorkspaceFile(file) {
145
+ const path = relative(this.realRoot, this.canonical(file));
146
+ return path !== '' && !path.startsWith('..') && !isAbsolute(path) && !path.split(sep).includes('node_modules');
147
+ }
148
+ isBuildOutput(file) {
149
+ if (!this.isWorkspaceFile(file))
150
+ return false;
151
+ const real = this.canonical(file);
152
+ const packageRoot = this.packageRoot(dirname(real));
153
+ return packageRoot !== undefined && relative(packageRoot, real).split(sep)[0] === 'lib';
154
+ }
155
+ sourceFor(file) {
156
+ const real = this.canonical(file);
157
+ const packageRoot = this.packageRoot(dirname(real));
158
+ if (packageRoot === undefined)
159
+ return undefined;
160
+ const [lib, types, ...rest] = relative(packageRoot, real).split(sep);
161
+ const last = rest.at(-1);
162
+ if (lib !== 'lib' || types !== 'types' || last === undefined)
163
+ return undefined;
164
+ const stem = join(packageRoot, 'src', ...rest.slice(0, -1), last.replace(/\.d\.([cm]?)ts$/u, '.$1ts'));
165
+ const source = [stem, `${stem}x`].find(candidate => existsSync(candidate));
166
+ return source === undefined ? undefined : this.display(source);
167
+ }
168
+ linkedWorkspacePackage(importer, packageName) {
169
+ let directory = dirname(resolve(importer));
170
+ for (;;) {
171
+ const link = join(directory, 'node_modules', packageName);
172
+ if (existsSync(link)) {
173
+ const target = this.canonical(link);
174
+ return this.isWorkspaceFile(join(target, 'package.json')) ? target : undefined;
175
+ }
176
+ if (this.canonical(directory) === this.realRoot || dirname(directory) === directory)
177
+ return undefined;
178
+ directory = dirname(directory);
179
+ }
180
+ }
181
+ packageRoot(directory) {
182
+ if (this.packageRoots.has(directory))
183
+ return this.packageRoots.get(directory);
184
+ const found = existsSync(join(directory, 'package.json'))
185
+ ? directory
186
+ : directory === this.realRoot || dirname(directory) === directory
187
+ ? undefined
188
+ : this.packageRoot(dirname(directory));
189
+ this.packageRoots.set(directory, found);
190
+ return found;
191
+ }
192
+ }
193
+ //# sourceMappingURL=build-output-guard.js.map
@@ -44,6 +44,9 @@ export declare class FaceModelEmitter {
44
44
  private renderJs;
45
45
  private renderDts;
46
46
  private emitRemote;
47
+ private remoteMethodTypes;
48
+ private remoteInputType;
49
+ private remoteInfoLiteral;
47
50
  private invocationLiteral;
48
51
  private renderRemoteDts;
49
52
  private pushRemoteSignature;
@@ -8,6 +8,7 @@ import { posix } from 'node:path';
8
8
  import { GenMapping, addMapping, toEncodedMap } from '@jridgewell/gen-mapping';
9
9
  import { TypeGraphRenderer } from "./renderer.js";
10
10
  import { childTypeNodeIds } from "./model.js";
11
+ import { projectToolSchema, ToolSchemaError } from "./tool-schema.js";
11
12
  /** Failure to project a modeled construct into an emitted artifact. */
12
13
  export class TypertEmitError extends Error {
13
14
  name = 'TypertEmitError';
@@ -34,8 +35,7 @@ export class FaceModelEmitter {
34
35
  if (packageModel === undefined) {
35
36
  throw new TypertEmitError(`typert emitter(${this.face.face}): package ${packageName} is not modeled on this face`);
36
37
  }
37
- const schemas = new SchemaEmitter(this.renderer, packageModel.schemas, invocationBoundaryRoots(packageModel.invocations));
38
- const schemaArtifact = schemas.emit();
38
+ const schemaArtifact = invocationSchemas(this.renderer, packageModel.schemas, invocationBoundaryRoots(packageModel.invocations));
39
39
  const runtimeModel = this.runtimeModel(packageModel);
40
40
  const js = this.renderJs(packageModel, schemaArtifact, runtimeModel);
41
41
  const dts = this.renderDts(packageModel, schemaArtifact);
@@ -109,6 +109,9 @@ export class FaceModelEmitter {
109
109
  ];
110
110
  if (schemas.definitions.length > 0)
111
111
  lines.push('import { z } from \'zod\'', '');
112
+ if (packageModel.invocations.length > 0) {
113
+ lines.push("import { createTypertRemoteInfo } from '@deepseek-ai/dsh-typert-protocol/remote-info'", '');
114
+ }
112
115
  lines.push(...schemas.definitions);
113
116
  if (schemas.definitions.length > 0)
114
117
  lines.push('');
@@ -116,6 +119,8 @@ export class FaceModelEmitter {
116
119
  lines.push(`export const ${schema.exportName} = ${schema.internalName}`);
117
120
  if (schemas.exports.length > 0)
118
121
  lines.push('');
122
+ if (packageModel.invocations.length > 0)
123
+ lines.push(`const $remote = ${this.remoteInfoLiteral(packageModel, schemas)}`, '');
119
124
  const model = JSON.stringify(runtimeModel, null, 2);
120
125
  lines.push('export const TYPERT = {');
121
126
  lines.push(` package: ${quote(packageModel.name)},`);
@@ -125,11 +130,10 @@ export class FaceModelEmitter {
125
130
  lines.push(` { name: ${quote(schema.exportName)}, create: ${schema.exportName} },`);
126
131
  }
127
132
  lines.push(' ],');
128
- lines.push(' invocations: [');
129
- for (const invocation of packageModel.invocations) {
130
- lines.push(`${indent(this.invocationLiteral(invocation, schemas), 4)},`);
131
- }
132
- lines.push(' ],');
133
+ if (packageModel.invocations.length > 0)
134
+ lines.push(' get invocations() { return $remote.descriptors },');
135
+ else
136
+ lines.push(' invocations: [', ' ],');
133
137
  lines.push(` model: ${indent(model, 2).trimStart()},`);
134
138
  lines.push('}');
135
139
  return `${lines.join('\n')}\n`;
@@ -163,38 +167,53 @@ export class FaceModelEmitter {
163
167
  return `${lines.join('\n')}\n`;
164
168
  }
165
169
  emitRemote(packageModel) {
166
- const schemas = new SchemaEmitter(this.renderer, [], invocationBoundaryRoots(packageModel.invocations)).emit();
170
+ const schemas = invocationSchemas(this.renderer, [], invocationBoundaryRoots(packageModel.invocations));
167
171
  const lines = [
168
172
  '/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */',
169
173
  ];
170
174
  if (schemas.definitions.length > 0)
171
175
  lines.push('import { z } from \'zod\'', '');
176
+ lines.push("import { createTypertRemoteInfo } from '@deepseek-ai/dsh-typert-protocol/remote-info'", '');
172
177
  lines.push(...schemas.definitions);
173
178
  if (schemas.definitions.length > 0)
174
179
  lines.push('');
175
- lines.push('export const TYPERT_REMOTE = {');
176
- lines.push(` package: ${quote(packageModel.name)},`);
177
- lines.push(' descriptors: [');
178
- for (const invocation of packageModel.invocations) {
179
- lines.push(`${indent(this.invocationLiteral(invocation, schemas), 4)},`);
180
- }
181
- lines.push(' ],');
182
- lines.push('}');
183
- lines.push('');
180
+ lines.push(`export const TYPERT_REMOTE = ${this.remoteInfoLiteral(packageModel, schemas)}`, '');
184
181
  lines.push('export default TYPERT_REMOTE');
185
- const declaration = this.renderRemoteDts(packageModel);
186
182
  return {
187
183
  js: `${lines.join('\n')}\n`,
188
- ...declaration,
184
+ ...this.renderRemoteDts(packageModel),
189
185
  };
190
186
  }
187
+ remoteMethodTypes(packageModel) {
188
+ const names = allocateRemoteImportNames(remoteImports(packageModel.invocations));
189
+ const entries = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'direct').map((invocation) => {
190
+ const input = this.remoteInputType(invocation, names);
191
+ const result = this.renderer.renderType(invocation.result.type, names);
192
+ const uplink = invocation.uplink === undefined ? '' : `, ${this.renderer.renderType(invocation.uplink.boundary.type, names)}`;
193
+ const mode = invocation.mode === 'stream' ? `, ${quote('stream')}${uplink}`
194
+ : containsBytes(this.renderer, invocation.result.codecType) ? `, 'unary', never, RemoteDecoded<${result}>` : '';
195
+ return ` readonly ${quote(`${invocation.namespace}/${invocation.method}`)}: TypertRemoteMethod<${input}, ${result}${mode}>`;
196
+ });
197
+ return `{\n${entries.join('\n')}\n}`;
198
+ }
199
+ remoteInputType(invocation, names) {
200
+ return `{ ${invocation.parameters.map(parameter => `readonly ${quote(parameter.wire)}${parameter.boundary.acceptsUndefined ? '?' : ''}: ${this.renderer.renderType(parameter.boundary.type, names)}`).join('; ')} }`;
201
+ }
202
+ remoteInfoLiteral(packageModel, schemas) {
203
+ return [
204
+ 'createTypertRemoteInfo({',
205
+ ` package: ${quote(packageModel.name)},`,
206
+ ' methods: {',
207
+ ...packageModel.invocations.map(invocation => ` ${quote(`${invocation.namespace}/${invocation.method}`)}: ${indent(this.invocationLiteral(invocation, schemas), 4).trimStart()},`),
208
+ ' },',
209
+ '}, z)',
210
+ ].join('\n');
211
+ }
191
212
  invocationLiteral(invocation, schemas) {
192
213
  const lines = [
193
214
  '{',
194
- ` id: ${quote(invocation.id)},`,
195
215
  ` service: ${quote(invocation.service)},`,
196
- ` namespace: ${quote(invocation.namespace)},`,
197
- ` method: ${quote(invocation.method)},`,
216
+ ...(invocation.description === undefined ? [] : [` description: ${quote(invocation.description)},`]),
198
217
  ];
199
218
  if (invocation.implementation !== undefined) {
200
219
  lines.push(` implementation: ${quote(invocation.implementation)},`);
@@ -222,13 +241,18 @@ export class FaceModelEmitter {
222
241
  invocation.parameters.forEach((parameter, index) => {
223
242
  lines.push(' {');
224
243
  lines.push(` name: ${quote(parameter.name)},`);
225
- lines.push(` wire: ${quote(parameter.wire)},`);
244
+ if (parameter.wire !== parameter.name)
245
+ lines.push(` wire: ${quote(parameter.wire)},`);
246
+ if (parameter.description !== undefined)
247
+ lines.push(` description: ${quote(parameter.description)},`);
226
248
  lines.push(` source: ${quote(parameter.source)},`);
227
249
  if (parameter.lookup !== undefined)
228
250
  lines.push(` lookup: ${quote(parameter.lookup)},`);
229
251
  if (parameter.boundary.acceptsUndefined)
230
252
  lines.push(' acceptsUndefined: true,');
231
- lines.push(` codec: ${indent(strictCodec(parameter.boundary, schemas.boundary(parameterBoundaryKey(invocation, index))), 6).trimStart()},`);
253
+ for (const strict of [false, true]) {
254
+ lines.push(` ${strict ? 'strictCodec' : 'codec'}: ${indent(strictCodec(parameter.boundary, schemas.boundary(parameterBoundaryKey(invocation, index, strict)), undefined, toolProjection(() => projectToolSchema(this.renderer, parameter.boundary.codecType, strict, parameter.boundary.acceptsUndefined))), 6).trimStart()},`);
255
+ }
232
256
  lines.push(' },');
233
257
  });
234
258
  lines.push(' ],');
@@ -240,7 +264,8 @@ export class FaceModelEmitter {
240
264
  if (invocation.cancellation !== undefined) {
241
265
  lines.push(" cancellation: { parameter: 'signal' },");
242
266
  }
243
- lines.push(` result: ${indent(strictCodec(invocation.result, schemas.boundary(resultBoundaryKey(invocation)), schemas.encoder(resultBoundaryKey(invocation))), 2).trimStart()},`);
267
+ const returns = invocation.tags.find(tag => tag.name === 'returns' || tag.name === 'return')?.comment?.replace(/^[-—–]\s*/, '');
268
+ 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()},`);
244
269
  lines.push(` sourceLocation: ${JSON.stringify(invocation.location)},`);
245
270
  lines.push('}');
246
271
  return lines.join('\n');
@@ -262,9 +287,11 @@ export class FaceModelEmitter {
262
287
  const lines = [
263
288
  '/* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */',
264
289
  'import type {',
290
+ ...packageModel.invocations.some(invocation => containsBytes(this.renderer, invocation.result.codecType)) ? [' RemoteDecoded,'] : [],
265
291
  ' RemoteResult,',
266
292
  ...packageModel.invocations.some(invocation => invocation.mode === 'stream') ? [' RemoteStreamHandle,'] : [],
267
- ' TypertRemoteContribution,',
293
+ ' TypertRemoteInfo,',
294
+ ...packageModel.invocations.some(invocation => invocation.invocation.kind === 'direct') ? [' TypertRemoteMethod,'] : [],
268
295
  '} from \'@deepseek-ai/dsh-typert-protocol\'',
269
296
  ];
270
297
  const sourceMap = new GenMapping({ file: 'typert.remote-client.d.ts' });
@@ -273,12 +300,6 @@ export class FaceModelEmitter {
273
300
  lines.push(`import type { ${names.join(', ')} } from ${quote(specifier)}`);
274
301
  }
275
302
  lines.push('');
276
- if (packageModel.invocations.some(invocation => containsBytes(this.renderer, invocation.result.codecType))) {
277
- lines.push('type RemoteDecoded<Value> = Value extends Uint8Array ? Uint8Array<ArrayBuffer>');
278
- lines.push(' : Value extends string | number | boolean | null | undefined ? Value');
279
- lines.push(' : { [Key in keyof Value]: RemoteDecoded<Value[Key]> }');
280
- lines.push('');
281
- }
282
303
  lines.push('declare module \'@deepseek-ai/dsh-typert-protocol\' {');
283
304
  const direct = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'direct');
284
305
  const scoped = packageModel.invocations.filter(invocation => invocation.invocation.kind === 'context' || invocation.scope !== undefined);
@@ -310,7 +331,7 @@ export class FaceModelEmitter {
310
331
  }
311
332
  lines.push('}');
312
333
  lines.push('');
313
- lines.push('export declare const TYPERT_REMOTE: TypertRemoteContribution');
334
+ lines.push(`export declare const TYPERT_REMOTE: TypertRemoteInfo<${this.remoteMethodTypes(packageModel)}>`);
314
335
  lines.push('export default TYPERT_REMOTE');
315
336
  lines.push('//# sourceMappingURL=typert.remote-client.d.ts.map');
316
337
  return {
@@ -385,6 +406,27 @@ function uniqueNamespaces(invocations) {
385
406
  function remoteNamespaceInterface(namespace) {
386
407
  return `TypertRemoteNamespace$${Buffer.from(namespace, 'utf8').toString('hex')}`;
387
408
  }
409
+ function toolProjection(project) {
410
+ try {
411
+ return `jsonSchema: ${JSON.stringify(project())}`;
412
+ }
413
+ catch (error) {
414
+ if (!(error instanceof ToolSchemaError))
415
+ throw error;
416
+ return `jsonSchemaError: ${quote(error.message)}`;
417
+ }
418
+ }
419
+ function invocationSchemas(renderer, schemas, roots) {
420
+ const identifiers = new Set();
421
+ const ordinary = new SchemaEmitter(renderer, schemas, roots.filter(root => !root.closedObjects), {}, identifiers).emit();
422
+ const closedRoots = roots.filter(root => root.closedObjects);
423
+ const closed = new SchemaEmitter(renderer, [], closedRoots, { closedObjects: true }, identifiers).emit();
424
+ const keys = new Set(closedRoots.map(root => root.key));
425
+ return { definitions: [...ordinary.definitions, ...closed.definitions], exports: ordinary.exports,
426
+ boundary: key => keys.has(key) ? closed.boundary(key) : ordinary.boundary(key),
427
+ encoder: key => keys.has(key) ? closed.encoder(key) : ordinary.encoder(key),
428
+ };
429
+ }
388
430
  class ResultEncoderEmitter {
389
431
  renderer;
390
432
  names = new Map();
@@ -504,13 +546,15 @@ class SchemaEmitter {
504
546
  renderer;
505
547
  schemas;
506
548
  boundaries;
549
+ options;
507
550
  names = new Map();
508
551
  boundaryNames = new Map();
509
552
  declarations;
510
- constructor(renderer, schemas, boundaries) {
553
+ constructor(renderer, schemas, boundaries, options = {}, identifiers = new Set()) {
511
554
  this.renderer = renderer;
512
555
  this.schemas = schemas;
513
556
  this.boundaries = boundaries;
557
+ this.options = options;
514
558
  const declarations = new Map();
515
559
  for (const schema of schemas) {
516
560
  for (const declaration of renderer.declarationClosureForTypes([schema.type])) {
@@ -523,9 +567,8 @@ class SchemaEmitter {
523
567
  }
524
568
  }
525
569
  this.declarations = renderer.graph.declarations.filter(declaration => declarations.has(declaration.id));
526
- const identifiers = new Set();
527
570
  for (const declaration of this.declarations) {
528
- const base = `${safeIdentifier(declaration.name)}$schema`;
571
+ const base = `${options.closedObjects ? 'closed$' : ''}${safeIdentifier(declaration.name)}$schema`;
529
572
  let name = base;
530
573
  let suffix = 2;
531
574
  while (identifiers.has(name))
@@ -552,7 +595,7 @@ class SchemaEmitter {
552
595
  if (encode !== undefined)
553
596
  encoders.set(boundary.key, encode);
554
597
  const name = this.boundaryName(boundary.key);
555
- definitions.push(`let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.typeSchema(boundary.type)})`);
598
+ definitions.push(this.cachedFactory(name, this.typeSchema(boundary.type)));
556
599
  }
557
600
  definitions.push(...encoder.definitions());
558
601
  const exports = this.schemas.map((model) => ({
@@ -570,12 +613,15 @@ class SchemaEmitter {
570
613
  declarationDefinition(declaration) {
571
614
  const name = this.schemaName(declaration.id);
572
615
  if (declaration.typeParameters.length === 0) {
573
- return `let ${name}$value\nconst ${name} = () => (${name}$value ??= ${this.declarationSchema(declaration, new Map())})`;
616
+ return this.cachedFactory(name, this.declarationSchema(declaration, new Map()));
574
617
  }
575
618
  const parameters = declaration.typeParameters.map((parameter, index) => [`type${String(index)}$schema`, parameter.id]);
576
619
  const substitutions = new Map(parameters.map(([schema, id]) => [id, schema]));
577
620
  return `const ${name} = (${parameters.map(([schema]) => schema).join(', ')}) => ${this.declarationSchema(declaration, substitutions)}`;
578
621
  }
622
+ cachedFactory(name, expression) {
623
+ return `let ${name}$value\nconst ${name} = () => (${name}$value ??= ${expression})`;
624
+ }
579
625
  declarationSchema(declaration, substitutions) {
580
626
  if (declaration.kind === 'enum') {
581
627
  this.fail(declaration.name, 'enum declarations have no Zod projection');
@@ -594,6 +640,8 @@ class SchemaEmitter {
594
640
  }
595
641
  typeSchema(id, substitutions = new Map()) {
596
642
  const node = this.renderer.node(id);
643
+ if (node.json)
644
+ return 'z.json()';
597
645
  switch (node.kind) {
598
646
  case 'keyword': return this.keywordSchema(node.name);
599
647
  case 'literal': return `z.literal(${node.text})`;
@@ -715,6 +763,8 @@ class SchemaEmitter {
715
763
  objectSchema(members, subject, substitutions) {
716
764
  const properties = [];
717
765
  const indices = [];
766
+ let stringIndexValue;
767
+ let stringIndexType;
718
768
  let symbolMembers = 0;
719
769
  for (const member of members) {
720
770
  if (member.static || member.visibility !== 'public')
@@ -731,29 +781,75 @@ class SchemaEmitter {
731
781
  if (member.signature.parameters.length !== 1 || parameter === undefined) {
732
782
  this.fail(subject, 'index signature must have exactly one key parameter');
733
783
  }
734
- indices.push(this.readonly(`z.record(${this.typeSchema(parameter.type, substitutions)}, ${this.typeSchema(member.signature.returns, substitutions)})`, member.readonly));
784
+ const key = this.typeSchema(parameter.type, substitutions);
785
+ const value = this.typeSchema(member.signature.returns, substitutions);
786
+ if (key === 'z.string()') {
787
+ stringIndexValue = value;
788
+ stringIndexType = member.signature.returns;
789
+ }
790
+ indices.push(this.readonly(`z.record(${key}, ${value})`, member.readonly));
735
791
  continue;
736
792
  }
737
793
  if (member.kind !== 'property')
738
794
  this.fail(subject, `${member.kind} member ${member.name} is not data-schema projectable`);
739
- // Freezing a union may freeze its populated Uint8Array member.
740
- const schema = this.readonly(this.typeSchema(member.type, substitutions), member.readonly && !containsBytes(this.renderer, member.type, false));
741
- const property = this.describe(this.optional(schema, member.optional), member);
742
- properties.push(`${quote(member.jsonName ?? member.name)}: ${property}`);
795
+ properties.push(member);
743
796
  }
744
797
  if (indices.length > 1)
745
798
  this.fail(subject, 'object type has more than one JSON index signature');
746
799
  // A unique-symbol-only object is a compile-time marker and imposes no JSON shape.
747
800
  if (properties.length === 0 && indices.length === 0 && symbolMembers > 0)
748
801
  return 'z.unknown()';
749
- const object = `z.object({${properties.length === 0 ? '' : `\n${properties.map(property => ` ${property},`).join('\n')}\n`}})`;
802
+ const fields = properties.map((member) => {
803
+ // Freezing a union may freeze its populated Uint8Array member.
804
+ const type = this.options.closedObjects && stringIndexType !== undefined
805
+ ? this.indexedPropertySchema(member.type, stringIndexType, substitutions)
806
+ : this.typeSchema(member.type, substitutions);
807
+ const schema = this.readonly(type, member.readonly && !containsBytes(this.renderer, member.type, false));
808
+ return `${quote(member.jsonName ?? member.name)}: ${this.describe(this.optional(schema, member.optional), member)}`;
809
+ });
810
+ const object = `z.${this.options.closedObjects && indices.length === 0 ? 'strictObject' : 'object'}({${fields.length === 0 ? '' : `\n${fields.map(field => ` ${field},`).join('\n')}\n`}})`;
750
811
  const index = indices[0];
751
812
  if (index === undefined)
752
813
  return object;
753
814
  if (properties.length === 0)
754
815
  return index;
816
+ // Named keys enforce both their declared type and the index value; the catchall checks other keys.
817
+ if (this.options.closedObjects && stringIndexValue !== undefined)
818
+ return `${object}.catchall(${stringIndexValue})`;
755
819
  return `z.intersection(${object}, ${index})`;
756
820
  }
821
+ indexedPropertySchema(declared, indexed, substitutions) {
822
+ if (declared === indexed)
823
+ return this.typeSchema(declared, substitutions);
824
+ const own = this.renderer.node(declared);
825
+ const value = this.renderer.node(indexed);
826
+ if (own.kind === 'union') {
827
+ return `z.union([${own.types.map(type => this.indexedPropertySchema(type, indexed, substitutions)).join(', ')}])`;
828
+ }
829
+ if (value.kind === 'union') {
830
+ return `z.union([${value.types.map(type => this.indexedPropertySchema(declared, type, substitutions)).join(', ')}])`;
831
+ }
832
+ const ownProperties = own.kind === 'object'
833
+ ? own.members.filter((member) => member.kind === 'property') : [];
834
+ const valueProperties = value.kind === 'object'
835
+ ? value.members.filter((member) => member.kind === 'property') : [];
836
+ if (own.kind === 'object' && value.kind === 'object' && !own.json && !value.json
837
+ && ownProperties.length === own.members.length && valueProperties.length === value.members.length
838
+ && [...ownProperties, ...valueProperties].every(member => !member.static
839
+ && member.visibility === 'public' && member.computed === undefined)) {
840
+ const indexedFields = new Map(valueProperties.map(member => [member.jsonName ?? member.name, member]));
841
+ const overlaps = ownProperties.flatMap((member) => {
842
+ const inherited = indexedFields.get(member.jsonName ?? member.name);
843
+ if (inherited === undefined)
844
+ return [];
845
+ const combined = this.indexedPropertySchema(member.type, inherited.type, substitutions);
846
+ const schema = this.readonly(this.optional(combined, member.optional && inherited.optional), (member.readonly || inherited.readonly) && !containsBytes(this.renderer, member.type, false));
847
+ return [`${quote(member.jsonName ?? member.name)}: ${this.describe(schema, member)}`];
848
+ });
849
+ return `z.strictObject({ ...${this.typeSchema(indexed, substitutions)}.shape, ...${this.typeSchema(declared, substitutions)}.shape${overlaps.length === 0 ? '' : `, ${overlaps.join(', ')}`} })`;
850
+ }
851
+ return `z.intersection(${this.typeSchema(declared, substitutions)}, ${this.typeSchema(indexed, substitutions)})`;
852
+ }
757
853
  exportSchemaName(model) {
758
854
  const name = this.schemaName(model.symbol);
759
855
  const declaration = this.renderer.declaration(model.symbol);
@@ -822,6 +918,7 @@ function invocationBoundaryRoots(invocations) {
822
918
  }
823
919
  invocation.parameters.forEach((parameter, index) => {
824
920
  result.push({ key: parameterBoundaryKey(invocation, index), type: parameter.boundary.codecType });
921
+ result.push({ key: parameterBoundaryKey(invocation, index, true), type: parameter.boundary.codecType, closedObjects: true });
825
922
  });
826
923
  if (invocation.uplink !== undefined) {
827
924
  result.push({ key: uplinkBoundaryKey(invocation), type: invocation.uplink.boundary.codecType });
@@ -836,8 +933,8 @@ function contextBoundaryKey(invocation) {
836
933
  function uplinkBoundaryKey(invocation) {
837
934
  return `${invocation.id}:uplink`;
838
935
  }
839
- function parameterBoundaryKey(invocation, index) {
840
- return `${invocation.id}:parameter:${String(index)}`;
936
+ function parameterBoundaryKey(invocation, index, strict = false) {
937
+ return `${invocation.id}:parameter:${String(index)}${strict ? ':strict' : ''}`;
841
938
  }
842
939
  function resultBoundaryKey(invocation) {
843
940
  return `${invocation.id}:result`;
@@ -865,12 +962,14 @@ function containsBytes(renderer, id, nested = true, visited = new Set()) {
865
962
  return true;
866
963
  return childTypeNodeIds(node).some(child => containsBytes(renderer, child, nested, visited));
867
964
  }
868
- function strictCodec(boundary, schema, encoder) {
965
+ function strictCodec(boundary, schema, encoder, projection, description) {
869
966
  return [
870
967
  '{',
871
968
  ' mode: \'strict\',',
872
969
  ` typeSymbol: ${quote(boundary.typeSymbol)},`,
873
970
  ` create: ${schema},`,
971
+ ...(description === undefined ? [] : [` description: ${quote(description)},`]),
972
+ ...(projection === undefined ? [] : [` ${projection},`]),
874
973
  ...(encoder === undefined ? [] : [
875
974
  ` decode: value => ${schema}().parse(value),`,
876
975
  ` encode: (value, writeBytes) => ${encoder}(value, writeBytes, [], new Map()),`,
@@ -906,7 +1005,8 @@ function allocateRemoteImportNames(imports) {
906
1005
  'RemoteDecoded',
907
1006
  'RemoteResult',
908
1007
  'RemoteStreamHandle',
909
- 'TypertRemoteContribution',
1008
+ 'TypertRemoteInfo',
1009
+ 'TypertRemoteMethod',
910
1010
  'TYPERT_REMOTE',
911
1011
  ]);
912
1012
  const names = new Map();
@@ -10,7 +10,8 @@ export { FaceModelEmitter, TypertEmitError } from './emitter.ts';
10
10
  export type { ModelEmitResult } from './emitter.ts';
11
11
  export * from './cordis-catalog.ts';
12
12
  export { TypeGraphRenderer, TypeGraphRenderError } from './renderer.ts';
13
- export { WorkspaceTypertGenerator } from './workspace.ts';
14
- export type { WorkspaceEmitResult } from './workspace.ts';
13
+ export { findWorkspaceRoot, WorkspaceTypertGenerator } from './workspace.ts';
14
+ export type { WorkspaceEmitResult, WorkspaceTypertGeneratorOptions } from './workspace.ts';
15
+ export { writePackageArtifacts, writeWorkspaceArtifacts } from './artifact-files.ts';
15
16
  export type * from './model.ts';
16
17
  //# sourceMappingURL=index.d.ts.map
@@ -8,5 +8,6 @@ export { WorkspaceAnalyzer, WorkspaceCaches, TypertAnalysisError } from "./analy
8
8
  export { FaceModelEmitter, TypertEmitError } from "./emitter.js";
9
9
  export * from "./cordis-catalog.js";
10
10
  export { TypeGraphRenderer, TypeGraphRenderError } from "./renderer.js";
11
- export { WorkspaceTypertGenerator } from "./workspace.js";
11
+ export { findWorkspaceRoot, WorkspaceTypertGenerator } from "./workspace.js";
12
+ export { writePackageArtifacts, writeWorkspaceArtifacts } from "./artifact-files.js";
12
13
  //# sourceMappingURL=index.js.map