@deepseek-ai/dsh-typert-generator 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2723 @@
1
+ /**
2
+ * TypeScript project analyzer for the compiler-independent Typert model.
3
+ * Programs, symbols, and syntax nodes remain extraction-only implementation
4
+ * details; callers receive only the model declared in {@link ./model.ts}.
5
+ * @module @deepseek-ai/dsh-typert-generator/analyzer
6
+ */
7
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
8
+ import { dirname, extname, join, relative, resolve, sep } from 'node:path';
9
+ import ts from 'typescript';
10
+ /** Analysis failure with a source-oriented diagnostic. */
11
+ export class TypertAnalysisError extends Error {
12
+ name = 'TypertAnalysisError';
13
+ }
14
+ class SourceEditQueued extends Error {
15
+ }
16
+ const EMPTY_DOCUMENTATION = { tags: [] };
17
+ /**
18
+ * Process-wide parse cache for the bundled TypeScript default libraries.
19
+ * `typescript/lib/lib.*.d.ts` content is immutable for the process lifetime,
20
+ * so parses are shared across every {@link WorkspaceCaches} instance; the key
21
+ * carries the parse-affecting settings, keeping reuse exact.
22
+ */
23
+ const defaultLibraryParses = new Map();
24
+ function defaultLibraryKey(fileName, languageVersionOrOptions) {
25
+ const options = typeof languageVersionOrOptions === 'object'
26
+ ? languageVersionOrOptions
27
+ : { languageVersion: languageVersionOrOptions };
28
+ return [
29
+ fileName,
30
+ String(options.languageVersion),
31
+ String(options.impliedNodeFormat ?? ''),
32
+ String(options.jsDocParsingMode ?? ''),
33
+ ].join('\0');
34
+ }
35
+ /**
36
+ * Shared memo over one immutable workspace snapshot. Passing one instance to
37
+ * several analyzers (the batched and write-mode children reuse their parent's
38
+ * automatically) reuses parsed tsconfigs, the registration inventory, and
39
+ * per-face compiler hosts whose parsed and bound source files and module
40
+ * resolutions carry across programs. Callers that mutate workspace files
41
+ * between analyses must start from a fresh instance; write-mode source edits
42
+ * invalidate themselves through {@link invalidate}.
43
+ */
44
+ export class WorkspaceCaches {
45
+ /** Parsed tsconfig files by absolute config path. */
46
+ configs = new Map();
47
+ /** Registration inventories keyed by root and aggregate config paths. */
48
+ registrations = new Map();
49
+ hosts = new Map();
50
+ /**
51
+ * Parse one tsconfig once per workspace snapshot.
52
+ * @param path - absolute config path.
53
+ * @returns the memoized parse result.
54
+ */
55
+ config(path) {
56
+ let parsed = this.configs.get(path);
57
+ if (parsed === undefined) {
58
+ parsed = parseConfig(path);
59
+ this.configs.set(path, parsed);
60
+ }
61
+ return parsed;
62
+ }
63
+ /**
64
+ * Return the shared compiler host for one face. Every program of one face
65
+ * is built from the same aggregate compiler options (the first call wins),
66
+ * so parsed source files, binder state, and module resolutions are safe to
67
+ * reuse across the face's batched programs.
68
+ * @param face - the face whose programs share this host.
69
+ * @param options - the face's effective compiler options.
70
+ * @returns a compiler host with source-file and module-resolution caches.
71
+ */
72
+ programHost(face, options) {
73
+ let entry = this.hosts.get(face);
74
+ if (entry === undefined) {
75
+ const host = ts.createCompilerHost(options);
76
+ const files = new Map();
77
+ const resolutionCache = ts.createModuleResolutionCache(host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName), options);
78
+ const base = host.getSourceFile.bind(host);
79
+ // The snapshot contract makes shouldCreateNewSourceFile irrelevant: it
80
+ // only fires under oldProgram reuse, which these fresh programs never
81
+ // request, and invalidate() is the one supported re-read path.
82
+ host.getSourceFile = (fileName, languageVersionOrOptions, onError) => {
83
+ if (isStandardLibraryFile(fileName)) {
84
+ const key = defaultLibraryKey(fileName, languageVersionOrOptions);
85
+ if (!defaultLibraryParses.has(key)) {
86
+ defaultLibraryParses.set(key, base(fileName, languageVersionOrOptions, onError));
87
+ }
88
+ return defaultLibraryParses.get(key);
89
+ }
90
+ if (!files.has(fileName))
91
+ files.set(fileName, base(fileName, languageVersionOrOptions, onError));
92
+ return files.get(fileName);
93
+ };
94
+ host.getModuleResolutionCache = () => resolutionCache;
95
+ entry = { host, files };
96
+ this.hosts.set(face, entry);
97
+ }
98
+ return entry.host;
99
+ }
100
+ /**
101
+ * Drop cached parses of one edited source file so the next analysis reads
102
+ * the written content.
103
+ * @param file - path of the edited file.
104
+ */
105
+ invalidate(file) {
106
+ const target = realPath(file);
107
+ for (const { files } of this.hosts.values()) {
108
+ for (const key of [...files.keys()]) {
109
+ if (realPath(key) === target)
110
+ files.delete(key);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ /** Analyze host and client as independent TypeScript programs. */
116
+ export class WorkspaceAnalyzer {
117
+ options;
118
+ queuedEdit;
119
+ crossFaceLinks = new Map();
120
+ checkedProjects = new Set();
121
+ registrations = [];
122
+ caches;
123
+ constructor(options) {
124
+ this.options = {
125
+ root: realPath(options.root),
126
+ hostConfig: options.hostConfig ?? 'tsconfig.host.json',
127
+ clientConfig: options.clientConfig ?? 'tsconfig.client.json',
128
+ faces: options.faces ?? ['host', 'client'],
129
+ checkDiagnostics: options.checkDiagnostics ?? true,
130
+ mode: options.mode ?? 'check',
131
+ ...(options.packages === undefined ? {} : { packages: options.packages }),
132
+ };
133
+ this.caches = options.caches ?? new WorkspaceCaches();
134
+ }
135
+ /**
136
+ * Build the workspace model. Write mode applies inferred annotations and then
137
+ * returns a fresh check-mode analysis of the edited projects.
138
+ * @returns the independent face models and their explicit cross-face links.
139
+ */
140
+ analyze() {
141
+ this.registrations = this.loadRegistrations();
142
+ const selected = this.options.packages === undefined
143
+ ? undefined
144
+ : new Set(this.options.packages);
145
+ const faces = [];
146
+ try {
147
+ for (const face of this.options.faces) {
148
+ const registrations = this.registrations.filter(registration => registration.face === face && (selected === undefined || selected.has(registration.name)));
149
+ if (registrations.length === 0)
150
+ continue;
151
+ if (this.options.checkDiagnostics) {
152
+ for (const registration of registrations)
153
+ this.checkProject(registration);
154
+ }
155
+ const aggregatePath = resolve(this.options.root, face === 'host' ? this.options.hostConfig : this.options.clientConfig);
156
+ const aggregate = this.caches.config(aggregatePath);
157
+ const rootNames = [...new Set(registrations.flatMap(registration => registration.config.parsed.fileNames))];
158
+ const options = {
159
+ ...aggregate.parsed.options,
160
+ composite: false,
161
+ incremental: false,
162
+ noEmit: true,
163
+ };
164
+ const program = ts.createProgram({
165
+ rootNames,
166
+ options,
167
+ host: this.caches.programHost(face, options),
168
+ });
169
+ faces.push(new FaceAnalyzer({
170
+ root: this.options.root,
171
+ face,
172
+ program,
173
+ registrations,
174
+ allRegistrations: this.registrations,
175
+ mode: this.options.mode,
176
+ queueEdit: (edit) => { this.queueEdit(edit); },
177
+ crossFaceLinks: this.crossFaceLinks,
178
+ }).analyze());
179
+ }
180
+ }
181
+ catch (error) {
182
+ if (!(error instanceof SourceEditQueued) || this.options.mode !== 'write' || this.queuedEdit === undefined)
183
+ throw error;
184
+ }
185
+ if (this.queuedEdit !== undefined) {
186
+ this.applyEdit(this.queuedEdit);
187
+ return new WorkspaceAnalyzer({ ...this.options, caches: this.caches, mode: 'write' }).analyze();
188
+ }
189
+ if (this.options.mode === 'write') {
190
+ return new WorkspaceAnalyzer({ ...this.options, caches: this.caches, mode: 'check' }).analyze();
191
+ }
192
+ return {
193
+ faces,
194
+ crossFaceLinks: [...this.crossFaceLinks.values()].sort(compareCrossFaceLinks),
195
+ };
196
+ }
197
+ /**
198
+ * Analyze an explicit package selection through bounded compiler programs.
199
+ * The resulting model is identical in shape to {@link analyze}; stable graph
200
+ * ids let repeated dependency declarations merge without flattening types.
201
+ * @param batchSize - maximum selected packages in one face program.
202
+ * @returns one merged workspace model.
203
+ */
204
+ analyzeInBatches(batchSize = 8) {
205
+ if (this.options.packages === undefined) {
206
+ throw new TypertAnalysisError('typert: batched analysis requires an explicit package selection');
207
+ }
208
+ if (!Number.isInteger(batchSize) || batchSize < 1) {
209
+ throw new TypertAnalysisError(`typert: batch size must be a positive integer, received ${String(batchSize)}`);
210
+ }
211
+ const batches = [];
212
+ for (let index = 0; index < this.options.packages.length; index += batchSize) {
213
+ batches.push(new WorkspaceAnalyzer({
214
+ ...this.options,
215
+ caches: this.caches,
216
+ packages: this.options.packages.slice(index, index + batchSize),
217
+ }).analyze());
218
+ }
219
+ return mergeWorkspaceModels(batches);
220
+ }
221
+ /**
222
+ * Discover package faces from public-export-reachable Cordis augmentations
223
+ * and explicit `@typert` roots without constructing a type-checker program.
224
+ * @returns contributors grouped by package with deterministic face order.
225
+ */
226
+ discoverPackages() {
227
+ const registrations = this.loadRegistrations()
228
+ .filter(registration => this.options.faces.includes(registration.face))
229
+ .filter(registration => this.registrationHasSurface(registration));
230
+ const packages = new Map();
231
+ for (const registration of registrations) {
232
+ const current = packages.get(registration.name) ?? {
233
+ root: slash(relative(this.options.root, registration.root)),
234
+ faces: new Set(),
235
+ };
236
+ current.faces.add(registration.face);
237
+ packages.set(registration.name, current);
238
+ }
239
+ return [...packages]
240
+ .map(([packageName, value]) => ({
241
+ package: packageName,
242
+ root: value.root,
243
+ faces: [...value.faces].sort(),
244
+ }))
245
+ .sort((left, right) => left.package.localeCompare(right.package));
246
+ }
247
+ /**
248
+ * Index top-level exported type declarations without promoting them to graph
249
+ * roots. Consumers use this lexical index for ambiguity checks while all
250
+ * semantic traversal continues through {@link TypeGraph}.
251
+ * @returns declarations from the selected faces and package projects.
252
+ */
253
+ indexSourceDeclarations() {
254
+ const selected = this.options.packages === undefined ? undefined : new Set(this.options.packages);
255
+ const declarations = [];
256
+ for (const registration of this.loadRegistrations()) {
257
+ if (!this.options.faces.includes(registration.face)
258
+ || (selected !== undefined && !selected.has(registration.name)))
259
+ continue;
260
+ for (const file of registration.config.parsed.fileNames) {
261
+ const relativeFile = slash(relative(this.options.root, file));
262
+ if (!existsSync(file)
263
+ || !isWithin(realPath(file), join(registration.root, 'src'))
264
+ || !/\.(?:cts|mts|ts)$/.test(file))
265
+ continue;
266
+ const sourceFile = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
267
+ for (const statement of sourceFile.statements) {
268
+ if (!isTypeDeclaration(statement)
269
+ || statement.name === undefined
270
+ || !hasModifier(statement, ts.SyntaxKind.ExportKeyword))
271
+ continue;
272
+ const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
273
+ declarations.push({
274
+ face: registration.face,
275
+ package: registration.name,
276
+ name: statement.name.text,
277
+ kind: ts.isClassDeclaration(statement)
278
+ ? 'class'
279
+ : ts.isInterfaceDeclaration(statement)
280
+ ? 'interface'
281
+ : ts.isTypeAliasDeclaration(statement)
282
+ ? 'alias'
283
+ : 'enum',
284
+ location: {
285
+ file: relativeFile,
286
+ line: position.line + 1,
287
+ column: position.character + 1,
288
+ },
289
+ text: declarationText(statement),
290
+ });
291
+ }
292
+ }
293
+ }
294
+ return uniqueBy(declarations, declaration => `${declaration.face}\0${declaration.location.file}\0${String(declaration.location.line)}\0${declaration.name}`)
295
+ .sort((left, right) => left.face.localeCompare(right.face)
296
+ || left.location.file.localeCompare(right.location.file)
297
+ || left.location.line - right.location.line);
298
+ }
299
+ loadRegistrations() {
300
+ const inventoryKey = `${this.options.root}\0${this.options.hostConfig}\0${this.options.clientConfig}`;
301
+ const cached = this.caches.registrations.get(inventoryKey);
302
+ if (cached !== undefined)
303
+ return cached;
304
+ const registrations = [];
305
+ for (const face of ['host', 'client']) {
306
+ const aggregatePath = resolve(this.options.root, face === 'host' ? this.options.hostConfig : this.options.clientConfig);
307
+ if (!existsSync(aggregatePath))
308
+ continue;
309
+ const aggregate = this.caches.config(aggregatePath);
310
+ for (const reference of aggregate.parsed.projectReferences ?? []) {
311
+ const configPath = projectConfigPath(reference.path);
312
+ const packageRoot = dirname(configPath);
313
+ if (!isWithin(realPath(packageRoot), join(this.options.root, 'packages')))
314
+ continue;
315
+ const manifestPath = join(packageRoot, 'package.json');
316
+ if (!existsSync(manifestPath))
317
+ continue;
318
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
319
+ if (typeof manifest.name !== 'string')
320
+ continue;
321
+ const registration = {
322
+ face,
323
+ name: manifest.name,
324
+ root: realPath(packageRoot),
325
+ config: this.caches.config(configPath),
326
+ manifest,
327
+ };
328
+ if (!isDualFacePackage(manifest)) {
329
+ registrations.push(registration);
330
+ }
331
+ else if (configPath === join(packageRoot, 'tsconfig.json')) {
332
+ registrations.push({ ...registration, face: 'host', exportSubpaths: hostExportSubpaths(manifest) }, { ...registration, face: 'client', exportSubpaths: clientExportSubpaths(manifest) });
333
+ }
334
+ else {
335
+ registrations.push({
336
+ ...registration,
337
+ exportSubpaths: face === 'host'
338
+ ? hostExportSubpaths(manifest)
339
+ : clientExportSubpaths(manifest),
340
+ });
341
+ }
342
+ }
343
+ }
344
+ const inventory = uniqueBy(registrations, registration => `${registration.face}\0${registration.name}`)
345
+ .sort((left, right) => left.face.localeCompare(right.face) || left.name.localeCompare(right.name));
346
+ this.caches.registrations.set(inventoryKey, inventory);
347
+ return inventory;
348
+ }
349
+ entrySourcePaths(registration) {
350
+ return packageExportTargets(registration.manifest)
351
+ .filter(([subpath, target]) => (registration.exportSubpaths === undefined
352
+ || registration.exportSubpaths.includes(subpath))
353
+ && !target.includes('*')
354
+ && subpath !== './package.json'
355
+ && subpath !== './typert'
356
+ && subpath !== './client/typert'
357
+ && subpath !== './remote'
358
+ && !target.endsWith('.json'))
359
+ .map(([, target]) => sourcePathForExport(registration.root, target))
360
+ .filter(existsSync);
361
+ }
362
+ registrationHasSurface(registration) {
363
+ const seen = new Set();
364
+ const queue = this.entrySourcePaths(registration);
365
+ while (queue.length > 0) {
366
+ const file = realPath(queue.shift());
367
+ if (seen.has(file) || !isWithin(file, registration.root))
368
+ continue;
369
+ seen.add(file);
370
+ const source = readFileSync(file, 'utf8');
371
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
372
+ if (sourceFileHasSurface(sourceFile))
373
+ return true;
374
+ for (const imported of ts.preProcessFile(source).importedFiles) {
375
+ const resolved = ts.resolveModuleName(imported.fileName, file, registration.config.parsed.options, ts.sys).resolvedModule;
376
+ if (resolved !== undefined && isWithin(resolved.resolvedFileName, registration.root)) {
377
+ queue.push(resolved.resolvedFileName);
378
+ }
379
+ }
380
+ }
381
+ return false;
382
+ }
383
+ checkProject(registration) {
384
+ if (this.checkedProjects.has(registration.config.path))
385
+ return;
386
+ this.checkedProjects.add(registration.config.path);
387
+ const program = ts.createProgram({
388
+ rootNames: registration.config.parsed.fileNames,
389
+ options: {
390
+ ...registration.config.parsed.options,
391
+ composite: false,
392
+ incremental: false,
393
+ noEmit: true,
394
+ // Source-plane workspace aliases resolve referenced packages to source.
395
+ // Widen only this diagnostic program's root so those imports do not
396
+ // produce an artificial TS6059 before Typert checks the public edge.
397
+ rootDir: this.options.root,
398
+ },
399
+ });
400
+ const diagnostics = [
401
+ ...program.getSyntacticDiagnostics(),
402
+ ...program.getSemanticDiagnostics(),
403
+ ].filter((diagnostic) => diagnostic.file !== undefined
404
+ && diagnostic.start !== undefined
405
+ && isWithin(diagnostic.file.fileName, registration.root));
406
+ if (diagnostics.length === 0)
407
+ return;
408
+ throw new TypertAnalysisError(diagnostics
409
+ .map(diagnostic => formatProgramDiagnostic(this.options.root, registration.face, diagnostic))
410
+ .join('\n'));
411
+ }
412
+ queueEdit(edit) {
413
+ this.queuedEdit = edit;
414
+ }
415
+ applyEdit(edit) {
416
+ const source = readFileSync(edit.file, 'utf8');
417
+ writeFileSync(edit.file, source.slice(0, edit.position) + edit.text + source.slice(edit.position));
418
+ this.caches.invalidate(edit.file);
419
+ }
420
+ }
421
+ class FaceAnalyzer {
422
+ root;
423
+ face;
424
+ program;
425
+ checker;
426
+ registrations;
427
+ allRegistrations;
428
+ mode;
429
+ queueEdit;
430
+ crossFaceLinks;
431
+ sourceFiles = new Map();
432
+ declarations = new Map();
433
+ declarationStates = new Set();
434
+ nodes = new Map();
435
+ exportsByPackage = new Map();
436
+ nodeOrdinals = new Map();
437
+ staticLookups;
438
+ staticContexts;
439
+ constructor(options) {
440
+ this.root = options.root;
441
+ this.face = options.face;
442
+ this.program = options.program;
443
+ this.checker = options.program.getTypeChecker();
444
+ this.registrations = options.registrations;
445
+ this.allRegistrations = options.allRegistrations;
446
+ this.mode = options.mode;
447
+ this.queueEdit = options.queueEdit;
448
+ this.crossFaceLinks = options.crossFaceLinks;
449
+ for (const sourceFile of this.program.getSourceFiles()) {
450
+ this.sourceFiles.set(realPath(sourceFile.fileName), sourceFile);
451
+ }
452
+ }
453
+ analyze() {
454
+ for (const registration of this.registrations) {
455
+ this.exportsByPackage.set(registration.name, this.collectExports(registration));
456
+ }
457
+ const packages = this.registrations
458
+ .map(registration => this.analyzePackage(registration))
459
+ .filter(hasPackageSurface);
460
+ this.validateInvocationIdentity(packages);
461
+ return {
462
+ face: this.face,
463
+ packages,
464
+ graph: {
465
+ declarations: [...this.declarations.values()].sort((left, right) => left.id.localeCompare(right.id)),
466
+ nodes: [...this.nodes.values()].sort((left, right) => left.id.localeCompare(right.id)),
467
+ },
468
+ };
469
+ }
470
+ analyzePackage(registration) {
471
+ const records = this.exportsByPackage.get(registration.name);
472
+ const reachable = this.reachableFiles(registration, records.map(record => record.sourceFile));
473
+ const services = [];
474
+ const events = [];
475
+ for (const sourceFile of reachable) {
476
+ for (const statement of sourceFile.statements) {
477
+ if (!ts.isModuleDeclaration(statement)
478
+ || !ts.isStringLiteral(statement.name)
479
+ || statement.name.text !== '@deepseek-ai/cordis'
480
+ || statement.body === undefined
481
+ || !ts.isModuleBlock(statement.body))
482
+ continue;
483
+ for (const member of statement.body.statements) {
484
+ if (!ts.isInterfaceDeclaration(member))
485
+ continue;
486
+ if (member.name.text === 'Context') {
487
+ services.push(...this.collectServices(member, records));
488
+ }
489
+ else if (member.name.text === 'Events') {
490
+ events.push(...this.collectEvents(member));
491
+ }
492
+ }
493
+ }
494
+ }
495
+ const explicitServices = this.collectExplicitServices(records);
496
+ const objects = [];
497
+ const schemas = [];
498
+ const seenBusinessSymbols = new Set();
499
+ for (const record of records) {
500
+ const declaration = record.declaration;
501
+ if (!isTypeDeclaration(declaration))
502
+ continue;
503
+ if (this.registrationForFile(declaration.getSourceFile().fileName) === undefined)
504
+ continue;
505
+ const symbol = this.resolveSymbol(record.symbol);
506
+ const symbolId = this.symbolId(symbol);
507
+ if (seenBusinessSymbols.has(symbolId))
508
+ continue;
509
+ const mode = typertMode(declaration);
510
+ if (mode !== 'object' && mode !== 'schema')
511
+ continue;
512
+ seenBusinessSymbols.add(symbolId);
513
+ this.ensureDeclaration(symbol, declaration);
514
+ const documentation = documentationOf(declaration);
515
+ if (mode === 'object') {
516
+ objects.push({
517
+ ...documentation,
518
+ export: record.model,
519
+ symbol: symbolId,
520
+ passing: 'reference',
521
+ });
522
+ }
523
+ else {
524
+ schemas.push({
525
+ ...documentation,
526
+ export: record.model,
527
+ symbol: symbolId,
528
+ type: this.referenceNode(symbol, declaration),
529
+ });
530
+ }
531
+ }
532
+ return {
533
+ name: registration.name,
534
+ root: slash(relative(this.root, registration.root)),
535
+ exports: records.map(record => record.model)
536
+ .sort((left, right) => left.subpath.localeCompare(right.subpath) || left.name.localeCompare(right.name)),
537
+ services: uniqueBy([...explicitServices, ...services], service => service.key)
538
+ .sort((left, right) => left.key.localeCompare(right.key)),
539
+ events: uniqueBy(events, event => event.name).sort((left, right) => left.name.localeCompare(right.name)),
540
+ objects: objects.sort((left, right) => left.export.name.localeCompare(right.export.name)),
541
+ schemas: schemas.sort((left, right) => left.export.name.localeCompare(right.export.name)),
542
+ invocations: this.face === 'host'
543
+ ? this.collectInvocations(registration, reachable).sort((left, right) => left.id.localeCompare(right.id))
544
+ : [],
545
+ };
546
+ }
547
+ collectExports(registration) {
548
+ const targets = packageExportTargets(registration.manifest)
549
+ .filter(([subpath]) => registration.exportSubpaths === undefined
550
+ || registration.exportSubpaths.includes(subpath));
551
+ const records = [];
552
+ for (const [subpath, target] of targets) {
553
+ if (target.includes('*') || subpath === './package.json'
554
+ || subpath === './typert' || subpath === './client/typert' || subpath === './remote'
555
+ // Data exports (bundle patch lists, JSON manifests) carry no TypeScript API.
556
+ || target.endsWith('.json') || target.endsWith('.yml') || target.endsWith('.yaml'))
557
+ continue;
558
+ const sourcePath = sourcePathForExport(registration.root, target);
559
+ const sourceFile = this.sourceFiles.get(realPath(sourcePath));
560
+ if (sourceFile === undefined) {
561
+ throw new TypertAnalysisError(`typert(${this.face}): ${registration.name} export ${subpath} resolves to missing source ${sourcePath}`);
562
+ }
563
+ const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
564
+ if (moduleSymbol === undefined)
565
+ continue;
566
+ for (const exported of this.checker.getExportsOfModule(moduleSymbol)) {
567
+ const symbol = this.resolveSymbol(exported);
568
+ const declaration = preferredDeclaration(symbol);
569
+ const aliases = exported === symbol || exported.name === symbol.name
570
+ ? [exported.name]
571
+ : [exported.name, symbol.name];
572
+ records.push({
573
+ model: {
574
+ subpath,
575
+ name: exported.name,
576
+ symbol: this.symbolId(symbol),
577
+ aliases,
578
+ },
579
+ symbol,
580
+ declaration,
581
+ sourceFile,
582
+ });
583
+ }
584
+ }
585
+ const unique = uniqueBy(records, record => `${record.model.subpath}\0${record.model.name}`);
586
+ this.collectCrossFaceReExports(registration, unique);
587
+ return unique;
588
+ }
589
+ collectCrossFaceReExports(registration, records) {
590
+ const publicSymbols = new Set(records.map(record => record.symbol));
591
+ const entryFiles = uniqueBy(records, record => record.sourceFile.fileName).map(record => record.sourceFile);
592
+ for (const sourceFile of this.reachableFiles(registration, entryFiles)) {
593
+ for (const statement of sourceFile.statements) {
594
+ if (!ts.isExportDeclaration(statement)
595
+ || statement.moduleSpecifier === undefined
596
+ || !ts.isStringLiteral(statement.moduleSpecifier))
597
+ continue;
598
+ const module = moduleIdentity(statement.moduleSpecifier.text);
599
+ if (module === undefined)
600
+ continue;
601
+ const toFace = this.allRegistrations
602
+ .find(candidate => candidate.name === module.package && candidate.face !== this.face)?.face;
603
+ if (toFace === undefined)
604
+ continue;
605
+ if (statement.exportClause !== undefined && ts.isNamespaceExport(statement.exportClause)) {
606
+ const namespace = this.resolveSymbol(this.checker.getSymbolAtLocation(statement.exportClause.name));
607
+ if (publicSymbols.has(namespace)) {
608
+ this.fail(statement.exportClause, 'cross-face namespace re-exports are not supported');
609
+ }
610
+ continue;
611
+ }
612
+ const exports = statement.exportClause === undefined
613
+ ? this.moduleExports(statement.moduleSpecifier)
614
+ .map(symbol => ({ symbol: this.resolveSymbol(symbol), requestedName: symbol.name, site: statement }))
615
+ : statement.exportClause.elements.map(element => ({
616
+ symbol: this.resolveSymbol(this.checker.getSymbolAtLocation(element.name)),
617
+ requestedName: element.propertyName?.text ?? element.name.text,
618
+ site: element,
619
+ }));
620
+ for (const exported of exports) {
621
+ if (!publicSymbols.has(exported.symbol))
622
+ continue;
623
+ const name = this.packageExportName(module, exported.symbol, toFace, exported.requestedName);
624
+ if (name === undefined) {
625
+ this.fail(exported.site, `cross-face re-export ${exported.requestedName} is not exported by ${module.package} at ${module.subpath}`);
626
+ }
627
+ this.recordCrossFaceLink(registration.name, toFace, module, name);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ moduleExports(moduleSpecifier) {
633
+ /* v8 ignore next -- a semantically valid export declaration from a resolved module always has a module symbol. */
634
+ const moduleSymbol = this.checker.getSymbolAtLocation(moduleSpecifier);
635
+ return this.checker.getExportsOfModule(moduleSymbol);
636
+ }
637
+ reachableFiles(registration, entryFiles) {
638
+ const reachable = new Map();
639
+ const queue = [...entryFiles];
640
+ while (queue.length > 0) {
641
+ const sourceFile = queue.shift();
642
+ const fileName = realPath(sourceFile.fileName);
643
+ if (reachable.has(fileName) || !isWithin(fileName, registration.root))
644
+ continue;
645
+ reachable.set(fileName, sourceFile);
646
+ for (const statement of sourceFile.statements) {
647
+ if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
648
+ || statement.moduleSpecifier === undefined
649
+ || !ts.isStringLiteral(statement.moduleSpecifier))
650
+ continue;
651
+ const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, sourceFile.fileName, this.program.getCompilerOptions(), ts.sys).resolvedModule;
652
+ if (resolved === undefined)
653
+ continue;
654
+ const resolvedPath = realPath(resolved.resolvedFileName);
655
+ if (!isWithin(resolvedPath, registration.root))
656
+ continue;
657
+ queue.push(this.sourceFiles.get(resolvedPath));
658
+ }
659
+ }
660
+ return [...reachable.values()].sort((left, right) => left.fileName.localeCompare(right.fileName));
661
+ }
662
+ collectServices(context, records) {
663
+ const bySymbol = new Map();
664
+ for (const record of records) {
665
+ const id = this.symbolId(record.symbol);
666
+ const matches = bySymbol.get(id) ?? [];
667
+ matches.push(record);
668
+ bySymbol.set(id, matches);
669
+ }
670
+ const result = [];
671
+ for (const member of context.members) {
672
+ if (!ts.isPropertySignature(member) || member.type === undefined)
673
+ continue;
674
+ const symbol = this.symbolAtType(member.type);
675
+ if (symbol === undefined)
676
+ continue;
677
+ const symbolId = this.symbolId(symbol);
678
+ const exported = bySymbol.get(symbolId)?.find(record => record.model.name === symbol.name)
679
+ ?? bySymbol.get(symbolId)?.find(record => record.model.name !== 'default')
680
+ ?? bySymbol.get(symbolId)?.[0];
681
+ if (exported === undefined)
682
+ continue;
683
+ const declaration = preferredDeclaration(symbol);
684
+ if (declaration === undefined || (!ts.isClassDeclaration(declaration) && !ts.isInterfaceDeclaration(declaration))) {
685
+ this.fail(member, `service ${memberName(member.name)} does not resolve to an exported class or interface`);
686
+ }
687
+ const model = this.ensureDeclaration(symbol, declaration);
688
+ const exposed = model.members
689
+ .filter(exposableMember)
690
+ .map(publicMember => publicMember.id);
691
+ result.push({
692
+ ...documentationOf(declaration),
693
+ key: memberName(member.name),
694
+ symbol: symbolId,
695
+ export: exported.model,
696
+ members: exposed,
697
+ location: this.location(member),
698
+ });
699
+ }
700
+ return result;
701
+ }
702
+ collectExplicitServices(records) {
703
+ const result = [];
704
+ const seen = new Set();
705
+ for (const record of records) {
706
+ const tag = typertServiceTag(record.declaration);
707
+ if (tag === undefined)
708
+ continue;
709
+ const words = (ts.getTextOfJSDocComment(tag.comment) ?? '').trim().split(/\s+/);
710
+ if (words.length !== 2 || !isRemoteSegment(words[1] ?? '')) {
711
+ this.fail(tag, '@typert service requires exactly one nonempty Cordis service key without "/"');
712
+ }
713
+ if (!ts.isClassDeclaration(record.declaration)) {
714
+ this.fail(record.declaration, '@typert service requires an exported class');
715
+ }
716
+ const symbol = this.resolveSymbol(record.symbol);
717
+ const symbolId = this.symbolId(symbol);
718
+ if (seen.has(symbolId))
719
+ continue;
720
+ seen.add(symbolId);
721
+ const model = this.ensureDeclaration(symbol, record.declaration);
722
+ result.push({
723
+ ...documentationOf(record.declaration),
724
+ key: words[1],
725
+ symbol: symbolId,
726
+ export: record.model,
727
+ members: model.members.filter(exposableMember).map(member => member.id),
728
+ location: this.location(record.declaration),
729
+ });
730
+ }
731
+ return result;
732
+ }
733
+ collectInvocations(registration, reachable) {
734
+ const result = [];
735
+ for (const sourceFile of reachable) {
736
+ for (const statement of sourceFile.statements) {
737
+ if (!ts.isClassDeclaration(statement))
738
+ continue;
739
+ const marked = statement.members.flatMap((member) => {
740
+ const invocation = this.remoteMarker(member);
741
+ if (invocation === undefined)
742
+ return [];
743
+ if (!ts.isMethodDeclaration(member)) {
744
+ this.fail(member, 'Remote decorators require a public instance method');
745
+ }
746
+ return [{ method: member, invocation }];
747
+ });
748
+ const first = marked[0];
749
+ if (first === undefined)
750
+ continue;
751
+ const binding = this.gatewayBinding(statement);
752
+ if (binding === undefined) {
753
+ this.fail(first.method, 'Remote methods require GatewayService or readonly typertGateway = bindTypeRTGateway(this, serviceKey)');
754
+ }
755
+ for (const { method, invocation } of marked) {
756
+ result.push(this.invocationModel(registration, binding, method, invocation));
757
+ }
758
+ }
759
+ }
760
+ return result;
761
+ }
762
+ invocationModel(registration, binding, method, invocation) {
763
+ if (visibilityOf(method) !== 'public' || hasModifier(method, ts.SyntaxKind.StaticKeyword)) {
764
+ this.fail(method, 'Remote decorators require a public instance method');
765
+ }
766
+ if (hasModifier(method, ts.SyntaxKind.AbstractKeyword) || method.body === undefined) {
767
+ this.fail(method, 'Remote methods must have a concrete implementation');
768
+ }
769
+ if (!ts.isIdentifier(method.name)) {
770
+ this.fail(method, 'Remote method names must be identifiers');
771
+ }
772
+ if ((method.typeParameters?.length ?? 0) > 0) {
773
+ this.fail(method, 'generic Remote methods are not supported');
774
+ }
775
+ const methodName = method.name.text;
776
+ const exportedMethod = invocation.exportName ?? methodName;
777
+ const lookups = this.lookupDeclarations();
778
+ const lookupByHost = new Map(lookups.map(lookup => [lookup.hostSymbol, lookup]));
779
+ const parameters = [];
780
+ let cancellation;
781
+ const wires = new Set();
782
+ for (const [parameterIndex, parameter] of method.parameters.entries()) {
783
+ if (!ts.isIdentifier(parameter.name)) {
784
+ this.fail(parameter, 'Remote parameters must use identifier bindings');
785
+ }
786
+ if (parameter.dotDotDotToken !== undefined)
787
+ this.fail(parameter, 'Remote parameters cannot be rest parameters');
788
+ if (parameter.initializer !== undefined)
789
+ this.fail(parameter, 'Remote parameters cannot have default values');
790
+ if (parameter.questionToken !== undefined)
791
+ this.fail(parameter, 'Remote parameters cannot be optional');
792
+ if (parameter.name.text === 'this')
793
+ this.fail(parameter, 'Remote methods cannot declare an explicit this parameter');
794
+ const authoredType = this.requiredType(parameter, parameter.type, 'parameter');
795
+ const cancellationName = parameter.name.text === 'signal';
796
+ const cancellationType = this.isGlobalAbortSignal(authoredType);
797
+ if (cancellationName || cancellationType) {
798
+ if (!cancellationName || !cancellationType) {
799
+ this.fail(parameter, 'Remote cancellation must use a parameter named signal with the global AbortSignal type');
800
+ }
801
+ if (parameterIndex !== method.parameters.length - 1) {
802
+ this.fail(parameter, 'Remote cancellation signal must be the final parameter');
803
+ }
804
+ cancellation = { parameter: 'signal' };
805
+ continue;
806
+ }
807
+ const hostSymbol = this.symbolAtType(authoredType);
808
+ const lookup = hostSymbol === undefined ? undefined : lookupByHost.get(this.symbolId(hostSymbol));
809
+ let modeled;
810
+ if (lookup !== undefined) {
811
+ if (parameter.name.text !== lookup.key) {
812
+ this.fail(parameter, `lookup parameter for ${lookup.key} must also be named ${lookup.key}`);
813
+ }
814
+ const boundary = this.remoteBoundary(lookup.wireType, `${registration.name}#${binding.namespace}/${exportedMethod}:${lookup.key}Id`, true);
815
+ modeled = {
816
+ name: parameter.name.text,
817
+ wire: `${lookup.key}Id`,
818
+ source: 'lookup',
819
+ lookup: lookup.key,
820
+ boundary,
821
+ };
822
+ }
823
+ else {
824
+ if (hostSymbol !== undefined && this.isWorkspaceClass(hostSymbol)) {
825
+ this.fail(parameter, `non-JSON class parameter ${hostSymbol.name} requires a TypeRTLookupMap entry`);
826
+ }
827
+ modeled = {
828
+ name: parameter.name.text,
829
+ wire: parameter.name.text,
830
+ source: 'json',
831
+ boundary: this.remoteBoundary(authoredType, `${registration.name}#${binding.namespace}/${exportedMethod}:${parameter.name.text}`, false),
832
+ };
833
+ }
834
+ if (wires.has(modeled.wire))
835
+ this.fail(parameter, `duplicate Remote wire field ${modeled.wire}`);
836
+ wires.add(modeled.wire);
837
+ parameters.push(modeled);
838
+ }
839
+ let receiver = { kind: 'direct' };
840
+ if (invocation.kind === 'context') {
841
+ const context = this.contextDeclarations().get(invocation.context);
842
+ if (context === undefined) {
843
+ this.fail(method, `Remote Scope ${invocation.context} has no TypeRTContextMap entry`);
844
+ }
845
+ const wire = `${invocation.context}Id`;
846
+ if (wires.has(wire))
847
+ this.fail(method, `Remote Scope wire field ${wire} conflicts with a method parameter`);
848
+ receiver = {
849
+ kind: 'context',
850
+ context: invocation.context,
851
+ wire,
852
+ boundary: this.remoteBoundary(context.wireType, `${registration.name}#${binding.namespace}/${exportedMethod}:${wire}`, true),
853
+ };
854
+ }
855
+ let scope;
856
+ if (invocation.kind === 'direct') {
857
+ const lookupParameters = parameters.filter(parameter => parameter.source === 'lookup');
858
+ const parameter = lookupParameters.length === 1 ? lookupParameters[0] : undefined;
859
+ const context = parameter?.lookup === undefined
860
+ ? undefined
861
+ : this.contextDeclarations().get(parameter.lookup);
862
+ if (parameter !== undefined && context !== undefined) {
863
+ const contextBoundary = this.remoteBoundary(context.wireType, `${registration.name}#${binding.namespace}/${exportedMethod}:scope:${context.key}`, true);
864
+ if (contextBoundary.typeSymbol !== parameter.boundary.typeSymbol) {
865
+ this.fail(method, `Remote scope ${context.key} wire type ${contextBoundary.typeSymbol} does not match lookup wire type ${parameter.boundary.typeSymbol}`);
866
+ }
867
+ scope = { context: context.key, wire: parameter.wire };
868
+ }
869
+ }
870
+ const resultType = this.remoteResultType(method);
871
+ return {
872
+ id: `${registration.name}#${binding.namespace}/${exportedMethod}`,
873
+ service: binding.service,
874
+ namespace: binding.namespace,
875
+ method: exportedMethod,
876
+ ...(exportedMethod === methodName ? {} : { implementation: methodName }),
877
+ invocation: receiver,
878
+ ...(scope === undefined ? {} : { scope }),
879
+ parameters,
880
+ ...(cancellation === undefined ? {} : { cancellation }),
881
+ result: this.remoteBoundary(resultType, `${registration.name}#${binding.namespace}/${exportedMethod}:result`, false),
882
+ location: this.location(method.name),
883
+ };
884
+ }
885
+ gatewayBinding(declaration) {
886
+ const field = this.gatewayFieldBinding(declaration);
887
+ const base = this.gatewayServiceBinding(declaration);
888
+ if (field !== undefined && base !== undefined) {
889
+ this.fail(field.site, 'GatewayService subclasses must not declare a second typertGateway binding');
890
+ }
891
+ return field ?? base;
892
+ }
893
+ gatewayFieldBinding(declaration) {
894
+ const candidates = declaration.members.filter((member) => ts.isPropertyDeclaration(member) && memberName(member.name) === 'typertGateway');
895
+ const [property, duplicate] = candidates;
896
+ if (property === undefined)
897
+ return undefined;
898
+ if (duplicate !== undefined)
899
+ this.fail(duplicate, 'Service has more than one typertGateway field');
900
+ if (visibilityOf(property) !== 'public'
901
+ || hasModifier(property, ts.SyntaxKind.StaticKeyword)
902
+ || !hasModifier(property, ts.SyntaxKind.ReadonlyKeyword)) {
903
+ this.fail(property, 'typertGateway must be a public readonly instance field');
904
+ }
905
+ if (property.initializer === undefined
906
+ || !ts.isCallExpression(property.initializer)
907
+ || !this.isTypeMetaSymbol(property.initializer.expression, 'bindTypeRTGateway')) {
908
+ this.fail(property, 'typertGateway must call bindTypeRTGateway()');
909
+ }
910
+ const call = property.initializer;
911
+ if (call.arguments.length < 2 || call.arguments.length > 3) {
912
+ this.fail(call, 'bindTypeRTGateway() requires this, service key, and an optional options object');
913
+ }
914
+ if (call.arguments[0]?.kind !== ts.SyntaxKind.ThisKeyword) {
915
+ this.fail(call.arguments[0] ?? call, 'bindTypeRTGateway() first argument must be this');
916
+ }
917
+ return this.gatewayBindingArguments(call, property);
918
+ }
919
+ gatewayServiceBinding(declaration) {
920
+ const heritage = (declaration.heritageClauses ?? [])
921
+ .filter(clause => clause.token === ts.SyntaxKind.ExtendsKeyword)
922
+ .flatMap(clause => [...clause.types])
923
+ .find(type => this.isTypeMetaSymbol(type.expression, 'GatewayService'));
924
+ if (heritage === undefined)
925
+ return undefined;
926
+ const constructor = declaration.members.find(ts.isConstructorDeclaration);
927
+ if (constructor?.body === undefined) {
928
+ this.fail(heritage, 'GatewayService subclasses must declare a constructor with super(ctx, serviceKey)');
929
+ }
930
+ const call = constructor.body.statements.flatMap((statement) => {
931
+ if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression))
932
+ return [];
933
+ return statement.expression.expression.kind === ts.SyntaxKind.SuperKeyword ? [statement.expression] : [];
934
+ })[0];
935
+ if (call === undefined) {
936
+ this.fail(constructor, 'GatewayService constructor must call super(ctx, serviceKey) directly');
937
+ }
938
+ if (call.arguments.length < 2 || call.arguments.length > 3) {
939
+ this.fail(call, 'GatewayService super() requires context, service key, and an optional options object');
940
+ }
941
+ return this.gatewayBindingArguments(call, heritage);
942
+ }
943
+ gatewayBindingArguments(call, site) {
944
+ const serviceArgument = call.arguments[1];
945
+ if (serviceArgument === undefined)
946
+ this.fail(call, 'Gateway service key must be a string literal');
947
+ const service = stringLiteralValue(serviceArgument);
948
+ if (service === undefined)
949
+ this.fail(serviceArgument, 'Gateway service key must be a string literal');
950
+ let namespace = service;
951
+ const options = call.arguments[2];
952
+ if (options !== undefined) {
953
+ if (!ts.isObjectLiteralExpression(options)) {
954
+ this.fail(options, 'bindTypeRTGateway() options must be an object literal');
955
+ }
956
+ for (const propertyOption of options.properties) {
957
+ if (!ts.isPropertyAssignment(propertyOption)
958
+ || memberName(propertyOption.name) !== 'namespace') {
959
+ this.fail(propertyOption, 'bindTypeRTGateway() only supports a namespace option');
960
+ }
961
+ const value = stringLiteralValue(propertyOption.initializer);
962
+ if (value === undefined)
963
+ this.fail(propertyOption.initializer, 'Gateway namespace must be a string literal');
964
+ namespace = value;
965
+ }
966
+ }
967
+ if (!isRemoteSegment(service))
968
+ this.fail(serviceArgument, 'Gateway service key must contain only RPC endpoint segment characters');
969
+ if (!isRemoteSegment(namespace))
970
+ this.fail(options ?? call, 'Gateway namespace must contain only RPC endpoint segment characters');
971
+ return { service, namespace, site };
972
+ }
973
+ remoteMarker(member) {
974
+ let found;
975
+ for (const decorator of ts.canHaveDecorators(member) ? ts.getDecorators(member) ?? [] : []) {
976
+ const expression = decorator.expression;
977
+ let marker;
978
+ if (this.isTypeMetaSymbol(expression, 'Remote')) {
979
+ marker = { kind: 'direct' };
980
+ }
981
+ else if (ts.isCallExpression(expression)
982
+ && this.isTypeMetaSymbol(expression.expression, 'Remote')) {
983
+ if (expression.arguments.length !== 1)
984
+ this.fail(expression, 'Remote() requires one exported method name');
985
+ const exportName = stringLiteralValue(expression.arguments[0]);
986
+ if (exportName === undefined || !isRemoteSegment(exportName)) {
987
+ this.fail(expression.arguments[0] ?? expression, 'Remote() name must be a string literal containing only RPC endpoint segment characters');
988
+ }
989
+ marker = { kind: 'direct', exportName };
990
+ }
991
+ else if (ts.isCallExpression(expression)
992
+ && this.isTypeMetaSymbol(expression.expression, 'RemoteScope')) {
993
+ if (expression.arguments.length < 1 || expression.arguments.length > 2) {
994
+ this.fail(expression, 'RemoteScope() requires a Context key and optional exported method name');
995
+ }
996
+ const context = stringLiteralValue(expression.arguments[0]);
997
+ if (context === undefined || !isRemoteSegment(context)) {
998
+ this.fail(expression.arguments[0] ?? expression, 'RemoteScope() key must be a string literal containing only RPC endpoint segment characters');
999
+ }
1000
+ const exportArgument = expression.arguments[1];
1001
+ const exportName = exportArgument === undefined ? undefined : stringLiteralValue(exportArgument);
1002
+ if (exportArgument !== undefined && (exportName === undefined || !isRemoteSegment(exportName))) {
1003
+ this.fail(exportArgument, 'RemoteScope() name must be a string literal containing only RPC endpoint segment characters');
1004
+ }
1005
+ marker = { kind: 'context', context, ...exportName === undefined ? {} : { exportName } };
1006
+ }
1007
+ else {
1008
+ continue;
1009
+ }
1010
+ if (found !== undefined)
1011
+ this.fail(decorator, 'a method can have only one Remote invocation decorator');
1012
+ found = marker;
1013
+ }
1014
+ return found;
1015
+ }
1016
+ remoteResultType(method) {
1017
+ const authored = this.requiredType(method, method.type, 'return');
1018
+ if (!ts.isTypeReferenceNode(authored))
1019
+ return authored;
1020
+ const symbol = this.checker.getSymbolAtLocation(authored.typeName);
1021
+ const resolved = symbol === undefined ? undefined : this.resolveSymbol(symbol);
1022
+ const resultType = authored.typeArguments?.[0];
1023
+ if (resolved?.name !== 'Promise' || resultType === undefined || authored.typeArguments?.length !== 1)
1024
+ return authored;
1025
+ const declaration = preferredDeclaration(resolved);
1026
+ if (declaration === undefined || !isStandardLibraryFile(declaration.getSourceFile().fileName))
1027
+ return authored;
1028
+ return resultType;
1029
+ }
1030
+ isGlobalAbortSignal(type) {
1031
+ const symbol = this.symbolAtType(type);
1032
+ if (symbol?.name !== 'AbortSignal')
1033
+ return false;
1034
+ return symbol.declarations?.some(declaration => isStandardLibraryFile(declaration.getSourceFile().fileName)) === true;
1035
+ }
1036
+ lookupDeclarations() {
1037
+ if (this.staticLookups !== undefined)
1038
+ return this.staticLookups;
1039
+ const byKey = new Map();
1040
+ const byHost = new Map();
1041
+ for (const declaration of this.typeMetaMapMembers('TypeRTLookupMap')) {
1042
+ if (!ts.isPropertySignature(declaration) || declaration.type === undefined) {
1043
+ this.fail(declaration, 'TypeRTLookupMap entries must be required properties');
1044
+ }
1045
+ const key = memberName(declaration.name);
1046
+ if (!isRemoteSegment(key))
1047
+ this.fail(declaration.name, 'TypeRTLookupMap key must contain only RPC endpoint segment characters');
1048
+ if (!ts.isTypeReferenceNode(declaration.type)
1049
+ || !this.isTypeMetaSymbol(declaration.type.typeName, 'TypeRTLookup')
1050
+ || declaration.type.typeArguments?.length !== 2) {
1051
+ this.fail(declaration.type, 'TypeRTLookupMap values must be TypeRTLookup<Host, Wire>');
1052
+ }
1053
+ const hostType = declaration.type.typeArguments[0];
1054
+ const wireType = declaration.type.typeArguments[1];
1055
+ if (hostType === undefined || wireType === undefined) {
1056
+ this.fail(declaration.type, 'TypeRTLookupMap values must be TypeRTLookup<Host, Wire>');
1057
+ }
1058
+ const host = this.symbolAtType(hostType);
1059
+ if (host === undefined)
1060
+ this.fail(hostType, 'TypeRTLookup Host must be a named type');
1061
+ const entry = {
1062
+ key,
1063
+ hostSymbol: this.symbolId(host),
1064
+ wireType,
1065
+ site: declaration,
1066
+ };
1067
+ if (byKey.has(key))
1068
+ this.fail(declaration, `duplicate TypeRTLookupMap key ${key}`);
1069
+ if (byHost.has(entry.hostSymbol))
1070
+ this.fail(declaration, `Host type ${host.name} has more than one TypeRT lookup`);
1071
+ byKey.set(key, entry);
1072
+ byHost.set(entry.hostSymbol, entry);
1073
+ }
1074
+ this.staticLookups = [...byKey.values()];
1075
+ return this.staticLookups;
1076
+ }
1077
+ contextDeclarations() {
1078
+ if (this.staticContexts !== undefined)
1079
+ return this.staticContexts;
1080
+ const result = new Map();
1081
+ for (const declaration of this.typeMetaMapMembers('TypeRTContextMap')) {
1082
+ if (!ts.isPropertySignature(declaration) || declaration.type === undefined) {
1083
+ this.fail(declaration, 'TypeRTContextMap entries must be required properties');
1084
+ }
1085
+ const key = memberName(declaration.name);
1086
+ if (!isRemoteSegment(key))
1087
+ this.fail(declaration.name, 'TypeRTContextMap key must contain only RPC endpoint segment characters');
1088
+ if (!ts.isTypeReferenceNode(declaration.type)
1089
+ || !this.isTypeMetaSymbol(declaration.type.typeName, 'TypeRTContext')
1090
+ || declaration.type.typeArguments?.length !== 1) {
1091
+ this.fail(declaration.type, 'TypeRTContextMap values must be TypeRTContext<Wire>');
1092
+ }
1093
+ if (result.has(key))
1094
+ this.fail(declaration, `duplicate TypeRTContextMap key ${key}`);
1095
+ const wireType = declaration.type.typeArguments[0];
1096
+ if (wireType === undefined)
1097
+ this.fail(declaration.type, 'TypeRTContextMap values must be TypeRTContext<Wire>');
1098
+ result.set(key, {
1099
+ key,
1100
+ wireType,
1101
+ site: declaration,
1102
+ });
1103
+ }
1104
+ this.staticContexts = result;
1105
+ return result;
1106
+ }
1107
+ typeMetaMapMembers(name) {
1108
+ const result = [];
1109
+ for (const sourceFile of this.program.getSourceFiles()) {
1110
+ for (const statement of sourceFile.statements) {
1111
+ if (!ts.isModuleDeclaration(statement)
1112
+ || !ts.isStringLiteral(statement.name)
1113
+ || statement.name.text !== '@deepseek-ai/dsh-type-meta'
1114
+ || statement.body === undefined
1115
+ || !ts.isModuleBlock(statement.body))
1116
+ continue;
1117
+ for (const nested of statement.body.statements) {
1118
+ if (ts.isInterfaceDeclaration(nested) && nested.name.text === name)
1119
+ result.push(...nested.members);
1120
+ }
1121
+ }
1122
+ }
1123
+ return result;
1124
+ }
1125
+ remoteBoundary(authoredType, fallbackTypeSymbol, requireNamed) {
1126
+ const type = this.convertType(authoredType);
1127
+ const codecType = this.resolvedRemoteCodecType(authoredType);
1128
+ const rootSymbol = this.namedWorkspaceType(authoredType);
1129
+ const imports = new Map();
1130
+ const visit = (node) => {
1131
+ if ((ts.isTypeReferenceNode(node) || ts.isImportTypeNode(node))) {
1132
+ const symbol = ts.isTypeReferenceNode(node)
1133
+ ? this.checker.getSymbolAtLocation(node.typeName)
1134
+ : node.qualifier === undefined ? undefined : this.checker.getSymbolAtLocation(node.qualifier);
1135
+ if (symbol !== undefined) {
1136
+ const resolved = this.resolveSymbol(symbol);
1137
+ const declaration = preferredDeclaration(resolved);
1138
+ if (declaration !== undefined
1139
+ && !isStandardLibraryFile(declaration.getSourceFile().fileName)
1140
+ && this.registrationForFile(declaration.getSourceFile().fileName) !== undefined) {
1141
+ const imported = this.publicRemoteType(resolved, node);
1142
+ imports.set(imported.symbol, imported);
1143
+ }
1144
+ }
1145
+ }
1146
+ ts.forEachChild(node, visit);
1147
+ };
1148
+ visit(authoredType);
1149
+ if (rootSymbol !== undefined) {
1150
+ const imported = this.publicRemoteType(rootSymbol, authoredType);
1151
+ return {
1152
+ type,
1153
+ codecType,
1154
+ typeSymbol: `${imported.specifier}#${imported.name}`,
1155
+ imports: [...imports.values()].sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name)),
1156
+ };
1157
+ }
1158
+ if (requireNamed)
1159
+ this.fail(authoredType, 'lookup and Context wire types must be named public types');
1160
+ return {
1161
+ type,
1162
+ codecType,
1163
+ typeSymbol: fallbackTypeSymbol,
1164
+ imports: [...imports.values()].sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name)),
1165
+ };
1166
+ }
1167
+ /**
1168
+ * Project one authored Remote boundary through the complete face Program.
1169
+ * Consumer declarations retain the authored alias, while codecs use this
1170
+ * concrete graph so declaration-merged mapped and conditional types are
1171
+ * validated without teaching the compiler-independent emitter TypeScript's
1172
+ * type evaluator.
1173
+ */
1174
+ resolvedRemoteCodecType(authoredType) {
1175
+ const resolvedType = this.checker.getTypeFromTypeNode(authoredType);
1176
+ this.assertRemoteJsonType(resolvedType, authoredType, new Set(), false);
1177
+ const completed = new Map();
1178
+ const active = new Map();
1179
+ const recursiveDeclarations = new Map();
1180
+ const convert = (type) => {
1181
+ const cached = completed.get(type);
1182
+ if (cached !== undefined)
1183
+ return cached;
1184
+ const activeId = active.get(type);
1185
+ if (activeId !== undefined) {
1186
+ if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
1187
+ const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
1188
+ const elementId = element === undefined ? undefined : active.get(element);
1189
+ if (element !== undefined && elementId !== undefined) {
1190
+ return this.addNode(authoredType, {
1191
+ kind: 'array',
1192
+ element: this.resolvedCycleReference(element, authoredType, elementId, recursiveDeclarations),
1193
+ });
1194
+ }
1195
+ }
1196
+ return this.resolvedCycleReference(type, authoredType, activeId, recursiveDeclarations);
1197
+ }
1198
+ const id = this.allocateNodeId(authoredType);
1199
+ active.set(type, id);
1200
+ try {
1201
+ const add = (model) => {
1202
+ this.nodes.set(id, { id, ...model });
1203
+ completed.set(type, id);
1204
+ return id;
1205
+ };
1206
+ const flags = type.flags;
1207
+ if ((flags & ts.TypeFlags.Any) !== 0)
1208
+ return add({ kind: 'keyword', name: 'any' });
1209
+ if ((flags & ts.TypeFlags.Unknown) !== 0)
1210
+ return add({ kind: 'keyword', name: 'unknown' });
1211
+ if ((flags & ts.TypeFlags.Never) !== 0)
1212
+ return add({ kind: 'keyword', name: 'never' });
1213
+ if ((flags & ts.TypeFlags.String) !== 0)
1214
+ return add({ kind: 'keyword', name: 'string' });
1215
+ if ((flags & ts.TypeFlags.Number) !== 0)
1216
+ return add({ kind: 'keyword', name: 'number' });
1217
+ if ((flags & ts.TypeFlags.BigInt) !== 0)
1218
+ return add({ kind: 'keyword', name: 'bigint' });
1219
+ if ((flags & ts.TypeFlags.Boolean) !== 0)
1220
+ return add({ kind: 'keyword', name: 'boolean' });
1221
+ if ((flags & ts.TypeFlags.ESSymbol) !== 0)
1222
+ return add({ kind: 'keyword', name: 'symbol' });
1223
+ if ((flags & ts.TypeFlags.Undefined) !== 0)
1224
+ return add({ kind: 'keyword', name: 'undefined' });
1225
+ if ((flags & ts.TypeFlags.Void) !== 0)
1226
+ return add({ kind: 'keyword', name: 'void' });
1227
+ if ((flags & ts.TypeFlags.Null) !== 0)
1228
+ return add({ kind: 'literal', value: null, text: 'null' });
1229
+ if ((flags & ts.TypeFlags.StringLiteral) !== 0) {
1230
+ const value = type.value;
1231
+ return add({ kind: 'literal', value, text: JSON.stringify(value) });
1232
+ }
1233
+ if ((flags & ts.TypeFlags.NumberLiteral) !== 0) {
1234
+ const value = type.value;
1235
+ return add({ kind: 'literal', value, text: String(value) });
1236
+ }
1237
+ if ((flags & ts.TypeFlags.BigIntLiteral) !== 0) {
1238
+ const value = type.value;
1239
+ const text = `${value.negative ? '-' : ''}${value.base10Value}n`;
1240
+ return add({ kind: 'literal', value: BigInt(`${value.negative ? '-' : ''}${value.base10Value}`), text });
1241
+ }
1242
+ if ((flags & ts.TypeFlags.BooleanLiteral) !== 0) {
1243
+ const value = type.intrinsicName === 'true';
1244
+ return add({ kind: 'literal', value, text: String(value) });
1245
+ }
1246
+ if (type.isUnionOrIntersection()) {
1247
+ return add({
1248
+ kind: (flags & ts.TypeFlags.Union) !== 0 ? 'union' : 'intersection',
1249
+ types: type.types.map(convert),
1250
+ });
1251
+ }
1252
+ if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
1253
+ this.fail(authoredType, 'Remote codec contains an unresolved type parameter');
1254
+ }
1255
+ if ((flags & ts.TypeFlags.Object) === 0) {
1256
+ this.fail(authoredType, `Remote codec type ${this.checker.typeToString(type, authoredType, ts.TypeFormatFlags.NoTruncation)} has no concrete Zod projection`);
1257
+ }
1258
+ if (this.checker.isTupleType(type)) {
1259
+ const reference = type;
1260
+ const target = reference.target;
1261
+ const arguments_ = this.checker.getTypeArguments(reference);
1262
+ return add({
1263
+ kind: 'tuple',
1264
+ elements: arguments_.map((argument, index) => {
1265
+ const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
1266
+ return {
1267
+ type: convert(argument),
1268
+ optional: (elementFlags & ts.ElementFlags.Optional) !== 0,
1269
+ rest: (elementFlags & (ts.ElementFlags.Rest | ts.ElementFlags.Variadic)) !== 0,
1270
+ };
1271
+ }),
1272
+ });
1273
+ }
1274
+ if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
1275
+ const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
1276
+ if (element === undefined)
1277
+ this.fail(authoredType, 'Remote codec array has no element type');
1278
+ return add({ kind: 'array', element: convert(element) });
1279
+ }
1280
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
1281
+ this.fail(authoredType, 'Remote codec cannot contain callable or constructable values');
1282
+ }
1283
+ const members = [];
1284
+ for (const property of this.checker.getPropertiesOfType(type)) {
1285
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
1286
+ const propertyType = this.checker.getTypeOfSymbolAtLocation(property, declaration ?? authoredType);
1287
+ const symbolKey = property.getName();
1288
+ members.push({
1289
+ ...EMPTY_DOCUMENTATION,
1290
+ id: `${id}#${symbolKey}`,
1291
+ name: symbolKey,
1292
+ ...(symbolKey.startsWith('__@') ? { computed: 'symbol' } : {}),
1293
+ optional: (property.flags & ts.SymbolFlags.Optional) !== 0,
1294
+ readonly: declaration !== undefined && hasModifier(declaration, ts.SyntaxKind.ReadonlyKeyword),
1295
+ async: false,
1296
+ abstract: false,
1297
+ static: false,
1298
+ visibility: 'public',
1299
+ location: this.location(authoredType),
1300
+ text: '',
1301
+ kind: 'property',
1302
+ type: convert(propertyType),
1303
+ });
1304
+ }
1305
+ for (const [index, info] of this.checker.getIndexInfosOfType(type).entries()) {
1306
+ members.push({
1307
+ ...EMPTY_DOCUMENTATION,
1308
+ id: `${id}#index:${String(index)}`,
1309
+ name: '(index)',
1310
+ optional: false,
1311
+ readonly: info.isReadonly,
1312
+ async: false,
1313
+ abstract: false,
1314
+ static: false,
1315
+ visibility: 'public',
1316
+ location: this.location(authoredType),
1317
+ text: '',
1318
+ kind: 'index',
1319
+ signature: {
1320
+ typeParameters: [],
1321
+ parameters: [{
1322
+ name: 'key',
1323
+ binding: 'identifier',
1324
+ type: convert(info.keyType),
1325
+ optional: false,
1326
+ rest: false,
1327
+ receiver: false,
1328
+ }],
1329
+ returns: convert(info.type),
1330
+ },
1331
+ });
1332
+ }
1333
+ return add({ kind: 'object', members });
1334
+ }
1335
+ finally {
1336
+ active.delete(type);
1337
+ }
1338
+ };
1339
+ return convert(resolvedType);
1340
+ }
1341
+ assertRemoteJsonType(type, site, active, allowUndefined) {
1342
+ const flags = type.flags;
1343
+ if ((flags & ts.TypeFlags.Undefined) !== 0 && allowUndefined)
1344
+ return;
1345
+ if ((flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
1346
+ this.fail(site, `Remote boundary contains unconstrained ${this.checker.typeToString(type)} data`);
1347
+ }
1348
+ if ((flags & (ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0) {
1349
+ this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
1350
+ }
1351
+ if ((flags & (ts.TypeFlags.StringLike
1352
+ | ts.TypeFlags.NumberLike
1353
+ | ts.TypeFlags.BooleanLike
1354
+ | ts.TypeFlags.Null
1355
+ | ts.TypeFlags.Never)) !== 0)
1356
+ return;
1357
+ if (type.isUnion()) {
1358
+ for (const member of type.types)
1359
+ this.assertRemoteJsonType(member, site, active, allowUndefined);
1360
+ return;
1361
+ }
1362
+ if (type.isIntersection()) {
1363
+ const material = type.types.filter(member => !this.isRemotePhantomConstraint(member));
1364
+ if (material.length === 0)
1365
+ this.fail(site, 'Remote boundary contains a symbol-only object');
1366
+ for (const member of material)
1367
+ this.assertRemoteJsonType(member, site, active, false);
1368
+ return;
1369
+ }
1370
+ if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
1371
+ this.fail(site, 'Remote boundary contains an unresolved type parameter');
1372
+ }
1373
+ if ((flags & ts.TypeFlags.Object) === 0) {
1374
+ this.fail(site, `Remote boundary contains non-JSON type ${this.checker.typeToString(type)}`);
1375
+ }
1376
+ const symbol = type.getSymbol();
1377
+ const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
1378
+ if (declaration !== undefined && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration))) {
1379
+ this.fail(site, `Remote boundary contains class instance ${symbol?.name ?? this.checker.typeToString(type)}`);
1380
+ }
1381
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
1382
+ this.fail(site, 'Remote boundary contains callable or constructable data');
1383
+ }
1384
+ if (active.has(type))
1385
+ return;
1386
+ active.add(type);
1387
+ try {
1388
+ if (this.checker.isTupleType(type)) {
1389
+ const reference = type;
1390
+ const target = reference.target;
1391
+ const arguments_ = this.checker.getTypeArguments(reference);
1392
+ arguments_.forEach((argument, index) => {
1393
+ const elementFlags = target.elementFlags[index] ?? ts.ElementFlags.Required;
1394
+ this.assertRemoteJsonType(argument, site, active, (elementFlags & ts.ElementFlags.Optional) !== 0);
1395
+ });
1396
+ return;
1397
+ }
1398
+ if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
1399
+ const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number);
1400
+ if (element === undefined)
1401
+ this.fail(site, 'Remote boundary array has no element type');
1402
+ this.assertRemoteJsonType(element, site, active, false);
1403
+ return;
1404
+ }
1405
+ const properties = this.checker.getPropertiesOfType(type);
1406
+ if (properties.some(property => property.getName().startsWith('__@'))) {
1407
+ this.fail(site, 'Remote boundary contains a symbol-keyed property');
1408
+ }
1409
+ for (const property of properties) {
1410
+ const propertyDeclaration = property.valueDeclaration ?? property.declarations?.[0];
1411
+ const propertyType = this.checker.getTypeOfSymbolAtLocation(property, propertyDeclaration ?? site);
1412
+ this.assertRemoteJsonType(propertyType, site, active, (property.flags & ts.SymbolFlags.Optional) !== 0);
1413
+ }
1414
+ for (const info of this.checker.getIndexInfosOfType(type)) {
1415
+ if ((info.keyType.flags & ts.TypeFlags.ESSymbolLike) !== 0) {
1416
+ this.fail(site, 'Remote boundary contains a symbol index signature');
1417
+ }
1418
+ this.assertRemoteJsonType(info.type, site, active, false);
1419
+ }
1420
+ }
1421
+ finally {
1422
+ active.delete(type);
1423
+ }
1424
+ }
1425
+ isRemotePhantomConstraint(type) {
1426
+ if ((type.flags & ts.TypeFlags.Unknown) !== 0)
1427
+ return true;
1428
+ if ((type.flags & ts.TypeFlags.Any) !== 0 || (type.flags & ts.TypeFlags.Object) === 0)
1429
+ return false;
1430
+ if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0)
1431
+ return false;
1432
+ if (this.checker.getIndexInfosOfType(type).length > 0)
1433
+ return false;
1434
+ return this.checker.getPropertiesOfType(type).every(property => property.getName().startsWith('__@'));
1435
+ }
1436
+ resolvedCycleReference(type, site, resolvedType, recursiveDeclarations) {
1437
+ const symbol = type.aliasSymbol ?? type.getSymbol();
1438
+ if (symbol === undefined)
1439
+ this.fail(site, 'Remote codec contains an unnamed recursive type');
1440
+ const resolved = this.resolveSymbol(symbol);
1441
+ const declaration = preferredDeclaration(resolved);
1442
+ if (declaration === undefined || isStandardLibraryFile(declaration.getSourceFile().fileName)) {
1443
+ this.fail(site, `Remote codec recursive type ${resolved.name} has no workspace declaration`);
1444
+ }
1445
+ const owner = this.registrationForFile(declaration.getSourceFile().fileName);
1446
+ if (owner === undefined)
1447
+ this.fail(site, `Remote codec recursive type ${resolved.name} is not owned by this face`);
1448
+ let id = recursiveDeclarations.get(type);
1449
+ if (id === undefined) {
1450
+ id = `${this.symbolId(resolved)}#remote-codec:${resolvedType}`;
1451
+ recursiveDeclarations.set(type, id);
1452
+ this.declarations.set(id, {
1453
+ ...EMPTY_DOCUMENTATION,
1454
+ id,
1455
+ package: owner.name,
1456
+ name: `${resolved.name}RemoteCodec`,
1457
+ kind: 'alias',
1458
+ abstract: false,
1459
+ exported: false,
1460
+ location: this.location(declaration),
1461
+ text: '',
1462
+ typeParameters: [],
1463
+ extends: [],
1464
+ implements: [],
1465
+ members: [],
1466
+ type: resolvedType,
1467
+ });
1468
+ }
1469
+ return this.addNode(site, {
1470
+ kind: 'reference',
1471
+ name: `${resolved.name}RemoteCodec`,
1472
+ target: { kind: 'declaration', symbol: id },
1473
+ arguments: [],
1474
+ });
1475
+ }
1476
+ namedWorkspaceType(node) {
1477
+ if (!ts.isTypeReferenceNode(node) && !ts.isImportTypeNode(node))
1478
+ return undefined;
1479
+ const symbol = ts.isTypeReferenceNode(node)
1480
+ ? this.checker.getSymbolAtLocation(node.typeName)
1481
+ : node.qualifier === undefined ? undefined : this.checker.getSymbolAtLocation(node.qualifier);
1482
+ if (symbol === undefined)
1483
+ return undefined;
1484
+ const resolved = this.resolveSymbol(symbol);
1485
+ const declaration = preferredDeclaration(resolved);
1486
+ if (declaration === undefined
1487
+ || isStandardLibraryFile(declaration.getSourceFile().fileName)
1488
+ || this.registrationForFile(declaration.getSourceFile().fileName) === undefined)
1489
+ return undefined;
1490
+ return resolved;
1491
+ }
1492
+ publicRemoteType(symbol, site) {
1493
+ const declaration = preferredDeclaration(symbol);
1494
+ if (declaration === undefined)
1495
+ this.fail(site, `type ${symbol.name} has no declaration`);
1496
+ const registration = this.registrationForFile(declaration.getSourceFile().fileName);
1497
+ if (registration === undefined)
1498
+ this.fail(site, `type ${symbol.name} is not owned by a workspace package`);
1499
+ const candidates = [];
1500
+ for (const [subpath, target] of packageExportTargets(registration.manifest)) {
1501
+ if (subpath === '.' || subpath === './package.json' || subpath === './typert'
1502
+ || subpath === './client/typert' || subpath === './remote' || target.includes('*'))
1503
+ continue;
1504
+ const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
1505
+ if (sourceFile === undefined)
1506
+ continue;
1507
+ const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
1508
+ if (moduleSymbol === undefined)
1509
+ continue;
1510
+ for (const exported of this.checker.getExportsOfModule(moduleSymbol)) {
1511
+ if (this.resolveSymbol(exported) !== symbol)
1512
+ continue;
1513
+ candidates.push({
1514
+ symbol: this.symbolId(symbol),
1515
+ specifier: packageExportSpecifier(registration.name, subpath),
1516
+ name: exported.name,
1517
+ });
1518
+ }
1519
+ }
1520
+ const selected = candidates.sort((left, right) => left.specifier.localeCompare(right.specifier) || left.name.localeCompare(right.name))[0];
1521
+ if (selected === undefined) {
1522
+ this.fail(site, `Remote boundary type ${symbol.name} must be exported from a public non-root type subpath`);
1523
+ }
1524
+ return selected;
1525
+ }
1526
+ isWorkspaceClass(symbol) {
1527
+ const declaration = preferredDeclaration(symbol);
1528
+ return declaration !== undefined
1529
+ && ts.isClassDeclaration(declaration)
1530
+ && this.registrationForFile(declaration.getSourceFile().fileName) !== undefined;
1531
+ }
1532
+ isTypeMetaSymbol(node, name) {
1533
+ const symbol = this.checker.getSymbolAtLocation(node);
1534
+ if (symbol === undefined)
1535
+ return false;
1536
+ const resolved = this.resolveSymbol(symbol);
1537
+ if (resolved.name !== name)
1538
+ return false;
1539
+ const declaration = preferredDeclaration(resolved);
1540
+ if (declaration === undefined)
1541
+ return false;
1542
+ const registration = this.registrationForFile(declaration.getSourceFile().fileName);
1543
+ if (registration?.name === '@deepseek-ai/dsh-type-meta')
1544
+ return true;
1545
+ for (let current = declaration; current !== undefined; current = optionalParent(current)) {
1546
+ if (ts.isModuleDeclaration(current)
1547
+ && ts.isStringLiteral(current.name)
1548
+ && current.name.text === '@deepseek-ai/dsh-type-meta')
1549
+ return true;
1550
+ }
1551
+ return false;
1552
+ }
1553
+ validateInvocationIdentity(packages) {
1554
+ const endpoints = new Map();
1555
+ const ids = new Map();
1556
+ for (const invocation of packages.flatMap(packageModel => packageModel.invocations)) {
1557
+ const endpoint = `${invocation.namespace}/${invocation.method}`;
1558
+ const existingEndpoint = endpoints.get(endpoint);
1559
+ if (existingEndpoint !== undefined) {
1560
+ throw new TypertAnalysisError(`typert(${this.face}): ${invocation.location.file}:${String(invocation.location.line)}:${String(invocation.location.column)}: Remote endpoint ${endpoint} conflicts with ${existingEndpoint.id}`);
1561
+ }
1562
+ const existingId = ids.get(invocation.id);
1563
+ if (existingId !== undefined) {
1564
+ throw new TypertAnalysisError(`typert(${this.face}): ${invocation.location.file}:${String(invocation.location.line)}:${String(invocation.location.column)}: Remote invocation id ${invocation.id} conflicts with ${existingId.id}`);
1565
+ }
1566
+ endpoints.set(endpoint, invocation);
1567
+ ids.set(invocation.id, invocation);
1568
+ }
1569
+ }
1570
+ collectEvents(events) {
1571
+ const result = [];
1572
+ for (const member of events.members) {
1573
+ const documentation = documentationOf(member);
1574
+ const mode = documentation.tags.find(tag => tag.name === 'mode')?.comment?.trim();
1575
+ if (ts.isMethodSignature(member)) {
1576
+ const signature = this.signature(member, member.type);
1577
+ result.push({
1578
+ ...documentation,
1579
+ name: memberName(member.name),
1580
+ signature: this.addNode(member, { kind: 'function', signature }),
1581
+ text: memberText(member),
1582
+ ...(mode === undefined ? {} : { mode }),
1583
+ location: this.location(member),
1584
+ });
1585
+ }
1586
+ else if (ts.isPropertySignature(member) && member.type !== undefined) {
1587
+ result.push({
1588
+ ...documentation,
1589
+ name: memberName(member.name),
1590
+ signature: this.convertType(member.type),
1591
+ text: memberText(member),
1592
+ ...(mode === undefined ? {} : { mode }),
1593
+ location: this.location(member),
1594
+ });
1595
+ }
1596
+ }
1597
+ return result;
1598
+ }
1599
+ ensureDeclaration(symbol, selected) {
1600
+ const resolved = this.resolveSymbol(symbol);
1601
+ const id = this.symbolId(resolved);
1602
+ const existing = this.declarations.get(id);
1603
+ if (existing !== undefined)
1604
+ return existing;
1605
+ const declarationParts = resolved.declarations.filter(isTypeDeclaration);
1606
+ if (declarationParts.length > 1 && !declarationParts.every(ts.isInterfaceDeclaration)) {
1607
+ this.fail(selected, `merged ${ts.SyntaxKind[selected.kind]} declaration ${resolved.name} is not supported`);
1608
+ }
1609
+ if (selected.name === undefined) {
1610
+ this.fail(selected, `anonymous ${ts.SyntaxKind[selected.kind]} cannot be represented as a named type declaration`);
1611
+ }
1612
+ const owner = this.registrationForFile(selected.getSourceFile().fileName);
1613
+ this.declarationStates.add(id);
1614
+ if (declarationParts.length > 1) {
1615
+ const analyzedParts = declarationParts.map((declarationPart) => {
1616
+ const part = declarationPart;
1617
+ const partOwner = this.registrationForFile(part.getSourceFile().fileName);
1618
+ if (partOwner === undefined) {
1619
+ this.fail(part, `merged interface ${resolved.name} contains a declaration outside this face`);
1620
+ }
1621
+ const typeParameters = this.typeParameters(part.typeParameters);
1622
+ const heritage = this.heritage(part);
1623
+ const members = this.members(part.members, id);
1624
+ return {
1625
+ typeParameters,
1626
+ heritage,
1627
+ members,
1628
+ model: {
1629
+ ...documentationOf(part),
1630
+ package: partOwner.name,
1631
+ location: this.location(part),
1632
+ typeParameters,
1633
+ extends: heritage.extends,
1634
+ members: members.map(member => member.id),
1635
+ },
1636
+ };
1637
+ });
1638
+ const parameters = this.mergeTypeParameters(analyzedParts.map(part => part.typeParameters), selected, resolved.name);
1639
+ const model = {
1640
+ ...documentationOf(selected),
1641
+ id,
1642
+ package: owner.name,
1643
+ name: declarationName(selected),
1644
+ kind: 'interface',
1645
+ abstract: false,
1646
+ exported: hasModifier(selected, ts.SyntaxKind.ExportKeyword),
1647
+ location: this.location(selected),
1648
+ text: declarationText(selected),
1649
+ typeParameters: parameters,
1650
+ extends: analyzedParts.flatMap(part => part.heritage.extends),
1651
+ implements: [],
1652
+ members: analyzedParts.flatMap(part => part.members),
1653
+ parts: analyzedParts.map(part => part.model),
1654
+ };
1655
+ this.declarations.set(id, model);
1656
+ this.declarationStates.delete(id);
1657
+ return model;
1658
+ }
1659
+ const parameters = ts.isEnumDeclaration(selected) ? [] : this.typeParameters(selected.typeParameters);
1660
+ const heritage = ts.isTypeAliasDeclaration(selected) || ts.isEnumDeclaration(selected)
1661
+ ? { extends: [], implements: [] }
1662
+ : this.heritage(selected);
1663
+ const kind = ts.isClassDeclaration(selected)
1664
+ ? 'class'
1665
+ : ts.isInterfaceDeclaration(selected)
1666
+ ? 'interface'
1667
+ : ts.isTypeAliasDeclaration(selected)
1668
+ ? 'alias'
1669
+ : 'enum';
1670
+ const model = {
1671
+ ...documentationOf(selected),
1672
+ id,
1673
+ package: owner.name,
1674
+ name: declarationName(selected),
1675
+ kind,
1676
+ abstract: hasModifier(selected, ts.SyntaxKind.AbstractKeyword),
1677
+ exported: hasModifier(selected, ts.SyntaxKind.ExportKeyword),
1678
+ location: this.location(selected),
1679
+ text: declarationText(selected),
1680
+ typeParameters: parameters,
1681
+ extends: heritage.extends,
1682
+ implements: heritage.implements,
1683
+ members: ts.isTypeAliasDeclaration(selected) || ts.isEnumDeclaration(selected)
1684
+ ? []
1685
+ : this.members(selected.members, id),
1686
+ ...(ts.isTypeAliasDeclaration(selected) ? { type: this.convertType(selected.type) } : {}),
1687
+ ...(ts.isEnumDeclaration(selected) ? { enumMembers: this.enumMembers(selected) } : {}),
1688
+ };
1689
+ this.declarations.set(id, model);
1690
+ this.declarationStates.delete(id);
1691
+ return model;
1692
+ }
1693
+ enumMembers(declaration) {
1694
+ return declaration.members.map(member => ({
1695
+ ...documentationOf(member),
1696
+ name: memberName(member.name),
1697
+ ...(member.initializer === undefined ? {} : { initializer: member.initializer.getText() }),
1698
+ location: this.location(member),
1699
+ }));
1700
+ }
1701
+ heritage(declaration) {
1702
+ const result = { extends: [], implements: [] };
1703
+ for (const clause of declaration.heritageClauses ?? []) {
1704
+ const target = clause.token === ts.SyntaxKind.ExtendsKeyword ? result.extends : result.implements;
1705
+ for (const type of clause.types)
1706
+ target.push(this.convertHeritage(type));
1707
+ }
1708
+ return result;
1709
+ }
1710
+ convertHeritage(node) {
1711
+ const symbol = this.checker.getSymbolAtLocation(node.expression);
1712
+ return this.addNode(node, {
1713
+ kind: 'reference',
1714
+ name: node.expression.getText(),
1715
+ target: this.targetForReference(this.resolveSymbol(symbol), node),
1716
+ arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
1717
+ });
1718
+ }
1719
+ members(members, ownerId) {
1720
+ const result = [];
1721
+ for (const member of members) {
1722
+ if (ts.isPropertyDeclaration(member)
1723
+ && memberName(member.name) === 'typertGateway'
1724
+ && member.initializer !== undefined
1725
+ && ts.isCallExpression(member.initializer)
1726
+ && this.isTypeMetaSymbol(member.initializer.expression, 'bindTypeRTGateway'))
1727
+ continue;
1728
+ const visibility = visibilityOf(member);
1729
+ const isStatic = hasModifier(member, ts.SyntaxKind.StaticKeyword);
1730
+ if (visibility !== 'public' || isStatic || ts.isConstructorDeclaration(member))
1731
+ continue;
1732
+ const base = this.memberBase(member, ownerId, visibility, isStatic);
1733
+ if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
1734
+ const type = this.requiredType(member, member.type, 'property');
1735
+ result.push({ ...base, kind: 'property', type: this.convertType(type) });
1736
+ }
1737
+ else if (ts.isMethodSignature(member) || ts.isMethodDeclaration(member)) {
1738
+ result.push({ ...base, kind: 'method', signature: this.signature(member, member.type) });
1739
+ }
1740
+ else if (ts.isGetAccessorDeclaration(member)) {
1741
+ result.push({ ...base, kind: 'getter', signature: this.signature(member, member.type) });
1742
+ }
1743
+ else if (ts.isSetAccessorDeclaration(member)) {
1744
+ result.push({ ...base, kind: 'setter', signature: this.signature(member, member.type) });
1745
+ }
1746
+ else if (ts.isCallSignatureDeclaration(member)) {
1747
+ result.push({ ...base, kind: 'call', signature: this.signature(member, member.type) });
1748
+ }
1749
+ else if (ts.isConstructSignatureDeclaration(member)) {
1750
+ result.push({ ...base, kind: 'construct', signature: this.signature(member, member.type) });
1751
+ }
1752
+ else if (ts.isIndexSignatureDeclaration(member)) {
1753
+ result.push({ ...base, kind: 'index', signature: this.signature(member, member.type) });
1754
+ }
1755
+ }
1756
+ return result;
1757
+ }
1758
+ memberBase(member, ownerId, visibility, isStatic) {
1759
+ const identity = member.name !== undefined
1760
+ ? this.memberIdentity(member.name)
1761
+ : {
1762
+ name: ts.isCallSignatureDeclaration(member)
1763
+ ? '(call)'
1764
+ : ts.isConstructSignatureDeclaration(member)
1765
+ ? '(construct)'
1766
+ : '(index)',
1767
+ };
1768
+ return {
1769
+ ...documentationOf(member),
1770
+ id: `${ownerId}#${identity.name}@${String(member.getStart())}`,
1771
+ ...identity,
1772
+ optional: 'questionToken' in member && member.questionToken !== undefined,
1773
+ readonly: hasModifier(member, ts.SyntaxKind.ReadonlyKeyword),
1774
+ async: hasModifier(member, ts.SyntaxKind.AsyncKeyword),
1775
+ abstract: hasModifier(member, ts.SyntaxKind.AbstractKeyword),
1776
+ static: isStatic,
1777
+ visibility,
1778
+ location: this.location(member),
1779
+ text: memberText(member),
1780
+ };
1781
+ }
1782
+ memberIdentity(name) {
1783
+ if (!ts.isComputedPropertyName(name))
1784
+ return { name: memberName(name) };
1785
+ const expression = name.expression;
1786
+ if (ts.isStringLiteral(expression) || ts.isNumericLiteral(expression)
1787
+ || ts.isNoSubstitutionTemplateLiteral(expression)) {
1788
+ return { name: memberName(name), jsonName: expression.text };
1789
+ }
1790
+ const type = this.checker.getTypeAtLocation(expression);
1791
+ return {
1792
+ name: memberName(name),
1793
+ computed: (type.flags & ts.TypeFlags.UniqueESSymbol) !== 0 ? 'symbol' : 'dynamic',
1794
+ };
1795
+ }
1796
+ signature(node, explicitReturn) {
1797
+ const parameters = node.parameters.map(parameter => ({
1798
+ name: memberName(parameter.name),
1799
+ binding: ts.isIdentifier(parameter.name)
1800
+ ? 'identifier'
1801
+ : ts.isObjectBindingPattern(parameter.name)
1802
+ ? 'object'
1803
+ : 'array',
1804
+ type: this.convertType(this.requiredType(parameter, parameter.type, 'parameter')),
1805
+ optional: parameter.questionToken !== undefined || parameter.initializer !== undefined,
1806
+ rest: parameter.dotDotDotToken !== undefined,
1807
+ receiver: ts.isIdentifier(parameter.name) && parameter.name.text === 'this',
1808
+ ...(parameter.initializer === undefined ? {} : { initializer: parameter.initializer.getText() }),
1809
+ }));
1810
+ return {
1811
+ typeParameters: this.typeParameters(node.typeParameters),
1812
+ parameters,
1813
+ returns: ts.isSetAccessorDeclaration(node)
1814
+ ? this.addNode(node, { kind: 'keyword', name: 'void' })
1815
+ : this.convertType(this.requiredType(node, explicitReturn, 'return')),
1816
+ };
1817
+ }
1818
+ typeParameters(parameters) {
1819
+ return parameters?.map(parameter => ({
1820
+ id: `${this.locationKey(parameter)}#${parameter.name.text}`,
1821
+ name: parameter.name.text,
1822
+ const: hasModifier(parameter, ts.SyntaxKind.ConstKeyword),
1823
+ ...(parameter.constraint === undefined ? {} : { constraint: this.convertType(parameter.constraint) }),
1824
+ ...(parameter.default === undefined ? {} : { default: this.convertType(parameter.default) }),
1825
+ ...(hasModifier(parameter, ts.SyntaxKind.InKeyword) && hasModifier(parameter, ts.SyntaxKind.OutKeyword)
1826
+ ? { variance: 'in-out' }
1827
+ : hasModifier(parameter, ts.SyntaxKind.InKeyword)
1828
+ ? { variance: 'in' }
1829
+ : hasModifier(parameter, ts.SyntaxKind.OutKeyword)
1830
+ ? { variance: 'out' }
1831
+ : {}),
1832
+ })) ?? [];
1833
+ }
1834
+ mergeTypeParameters(parts, site, declarationName) {
1835
+ const first = parts[0];
1836
+ return first.map((parameter, index) => {
1837
+ const peers = parts.map(part => part[index]);
1838
+ const constraint = peers.find(peer => peer.constraint !== undefined)?.constraint;
1839
+ const fallback = peers.find(peer => peer.default !== undefined)?.default;
1840
+ const variances = [...new Set(peers.flatMap(peer => peer.variance === undefined ? [] : [peer.variance]))];
1841
+ if (variances.length > 1) {
1842
+ this.fail(site, `merged interface ${declarationName} has incompatible variance modifiers`);
1843
+ }
1844
+ return {
1845
+ id: parameter.id,
1846
+ name: parameter.name,
1847
+ const: peers.some(peer => peer.const),
1848
+ ...(constraint === undefined ? {} : { constraint }),
1849
+ ...(fallback === undefined ? {} : { default: fallback }),
1850
+ ...(variances[0] === undefined ? {} : { variance: variances[0] }),
1851
+ };
1852
+ });
1853
+ }
1854
+ requiredType(owner, type, purpose) {
1855
+ if (type !== undefined)
1856
+ return type;
1857
+ if (this.mode === 'check') {
1858
+ this.fail(owner, `public ${purpose} is missing an explicit type annotation`);
1859
+ }
1860
+ const inferred = this.inferType(owner, purpose);
1861
+ const rendered = ts.createPrinter().printNode(ts.EmitHint.Unspecified, inferred, owner.getSourceFile());
1862
+ const position = annotationPosition(owner, purpose);
1863
+ this.queueEdit({ file: realPath(owner.getSourceFile().fileName), position, text: `: ${rendered}` });
1864
+ throw new SourceEditQueued();
1865
+ }
1866
+ inferType(owner, purpose) {
1867
+ let type;
1868
+ if (purpose === 'return') {
1869
+ const signature = this.checker.getSignatureFromDeclaration(owner);
1870
+ type = this.checker.getReturnTypeOfSignature(signature);
1871
+ }
1872
+ else {
1873
+ type = this.checker.getTypeAtLocation(owner);
1874
+ }
1875
+ return this.checker.typeToTypeNode(type, owner, ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope);
1876
+ }
1877
+ convertType(node) {
1878
+ const id = this.allocateNodeId(node);
1879
+ const add = (model) => {
1880
+ this.nodes.set(id, { id, ...model });
1881
+ return id;
1882
+ };
1883
+ const keyword = keywordName(node.kind);
1884
+ if (keyword !== undefined)
1885
+ return add({ kind: 'keyword', name: keyword });
1886
+ if (ts.isParenthesizedTypeNode(node)) {
1887
+ return add({ kind: 'parenthesized', type: this.convertType(node.type) });
1888
+ }
1889
+ if (ts.isLiteralTypeNode(node))
1890
+ return add(literalModel(node));
1891
+ if (ts.isTypeReferenceNode(node)) {
1892
+ const symbol = this.checker.getSymbolAtLocation(node.typeName);
1893
+ return add({
1894
+ kind: 'reference',
1895
+ name: node.typeName.getText(),
1896
+ target: this.targetForReference(this.resolveSymbol(symbol), node),
1897
+ arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
1898
+ });
1899
+ }
1900
+ if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
1901
+ return add({
1902
+ kind: ts.isUnionTypeNode(node) ? 'union' : 'intersection',
1903
+ types: node.types.map(type => this.convertType(type)),
1904
+ });
1905
+ }
1906
+ if (ts.isArrayTypeNode(node))
1907
+ return add({ kind: 'array', element: this.convertType(node.elementType) });
1908
+ if (ts.isTupleTypeNode(node)) {
1909
+ return add({
1910
+ kind: 'tuple',
1911
+ elements: node.elements.map((element) => {
1912
+ const named = ts.isNamedTupleMember(element) ? element : undefined;
1913
+ const raw = named?.type ?? element;
1914
+ const optional = named?.questionToken !== undefined || ts.isOptionalTypeNode(raw);
1915
+ const rest = named?.dotDotDotToken !== undefined || ts.isRestTypeNode(raw);
1916
+ const type = ts.isOptionalTypeNode(raw) || ts.isRestTypeNode(raw) ? raw.type : raw;
1917
+ return {
1918
+ ...(named === undefined ? {} : { name: named.name.text }),
1919
+ type: this.convertType(type),
1920
+ optional,
1921
+ rest,
1922
+ };
1923
+ }),
1924
+ });
1925
+ }
1926
+ if (ts.isTypeLiteralNode(node))
1927
+ return add({ kind: 'object', members: this.members(node.members, id) });
1928
+ if (ts.isFunctionTypeNode(node)) {
1929
+ return add({ kind: 'function', signature: this.signature(node, node.type) });
1930
+ }
1931
+ if (ts.isConstructorTypeNode(node)) {
1932
+ return add({
1933
+ kind: 'constructor',
1934
+ abstract: hasModifier(node, ts.SyntaxKind.AbstractKeyword),
1935
+ signature: this.signature(node, node.type),
1936
+ });
1937
+ }
1938
+ if (ts.isIndexedAccessTypeNode(node)) {
1939
+ return add({
1940
+ kind: 'indexed-access',
1941
+ object: this.convertType(node.objectType),
1942
+ index: this.convertType(node.indexType),
1943
+ });
1944
+ }
1945
+ if (ts.isTypeOperatorNode(node)) {
1946
+ return add({
1947
+ kind: 'operator',
1948
+ operator: ts.tokenToString(node.operator),
1949
+ type: this.convertType(node.type),
1950
+ });
1951
+ }
1952
+ if (ts.isConditionalTypeNode(node)) {
1953
+ return add({
1954
+ kind: 'conditional',
1955
+ check: this.convertType(node.checkType),
1956
+ extends: this.convertType(node.extendsType),
1957
+ whenTrue: this.convertType(node.trueType),
1958
+ whenFalse: this.convertType(node.falseType),
1959
+ });
1960
+ }
1961
+ if (ts.isInferTypeNode(node)) {
1962
+ return add({ kind: 'infer', parameter: this.typeParameters(ts.factory.createNodeArray([node.typeParameter]))[0] });
1963
+ }
1964
+ if (ts.isMappedTypeNode(node)) {
1965
+ const parameter = this.typeParameters(ts.factory.createNodeArray([node.typeParameter]))[0];
1966
+ return add({
1967
+ kind: 'mapped',
1968
+ parameter,
1969
+ ...(node.nameType === undefined ? {} : { nameType: this.convertType(node.nameType) }),
1970
+ ...(node.type === undefined ? {} : { value: this.convertType(node.type) }),
1971
+ readonly: modifierMode(node.readonlyToken),
1972
+ optional: modifierMode(node.questionToken),
1973
+ });
1974
+ }
1975
+ if (ts.isTemplateLiteralTypeNode(node)) {
1976
+ return add({
1977
+ kind: 'template-literal',
1978
+ head: node.head.text,
1979
+ spans: node.templateSpans.map(span => ({ type: this.convertType(span.type), text: span.literal.text })),
1980
+ });
1981
+ }
1982
+ if (ts.isTypeQueryNode(node)) {
1983
+ return add({
1984
+ kind: 'type-query',
1985
+ expression: node.exprName.getText(),
1986
+ arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
1987
+ });
1988
+ }
1989
+ if (ts.isImportTypeNode(node)) {
1990
+ const argument = node.argument;
1991
+ const symbol = node.qualifier === undefined ? undefined : this.checker.getSymbolAtLocation(node.qualifier);
1992
+ return add({
1993
+ kind: 'import-type',
1994
+ module: argument.literal.text,
1995
+ ...(node.qualifier === undefined ? {} : { qualifier: node.qualifier.getText() }),
1996
+ arguments: node.typeArguments?.map(argument => this.convertType(argument)) ?? [],
1997
+ typeof: node.isTypeOf,
1998
+ ...(node.attributes === undefined ? {} : { attributes: importTypeAttributesText(node) }),
1999
+ ...(symbol === undefined ? {} : { target: this.targetForReference(this.resolveSymbol(symbol), node) }),
2000
+ });
2001
+ }
2002
+ if (ts.isTypePredicateNode(node)) {
2003
+ return add({
2004
+ kind: 'predicate',
2005
+ asserts: node.assertsModifier !== undefined,
2006
+ parameter: node.parameterName.getText(),
2007
+ ...(node.type === undefined ? {} : { type: this.convertType(node.type) }),
2008
+ });
2009
+ }
2010
+ /* v8 ignore else -- every source TypeNode kind accepted by TypeScript is handled above; this arm keeps
2011
+ * future compiler kinds fail-loud. */
2012
+ if (ts.isThisTypeNode(node))
2013
+ return add({ kind: 'this' });
2014
+ /* v8 ignore next -- paired with the exhaustive TypeNode guard above. */
2015
+ this.fail(node, `unsupported TypeScript type node ${ts.SyntaxKind[node.kind]}`);
2016
+ }
2017
+ addNode(site, model) {
2018
+ const id = this.allocateNodeId(site);
2019
+ this.nodes.set(id, { id, ...model });
2020
+ return id;
2021
+ }
2022
+ referenceNode(symbol, site) {
2023
+ return this.addNode(site, {
2024
+ kind: 'reference',
2025
+ name: symbol.name,
2026
+ target: { kind: 'declaration', symbol: this.symbolId(symbol) },
2027
+ arguments: [],
2028
+ });
2029
+ }
2030
+ targetForReference(symbol, site) {
2031
+ const declaration = preferredDeclaration(symbol);
2032
+ /* v8 ignore next -- a symbol from a semantically valid source type reference always has a declaration. */
2033
+ if (declaration === undefined)
2034
+ this.fail(site, `type symbol ${symbol.name} has no declaration`);
2035
+ if (ts.isTypeParameterDeclaration(declaration)) {
2036
+ return {
2037
+ kind: 'type-parameter',
2038
+ parameter: `${this.locationKey(declaration)}#${declaration.name.text}`,
2039
+ };
2040
+ }
2041
+ if (isStandardLibraryFile(declaration.getSourceFile().fileName)) {
2042
+ return { kind: 'standard', name: symbol.name };
2043
+ }
2044
+ const moduleSpecifier = moduleSpecifierOf(site);
2045
+ const module = moduleSpecifier === undefined ? undefined : moduleIdentity(moduleSpecifier);
2046
+ const from = this.registrationForFile(site.getSourceFile().fileName);
2047
+ const owner = this.registrationForFile(declaration.getSourceFile().fileName);
2048
+ if (owner !== undefined) {
2049
+ if (owner.name !== from.name) {
2050
+ if (module === undefined) {
2051
+ this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`);
2052
+ }
2053
+ const exportName = authoredExportName(site, moduleSpecifier);
2054
+ if (this.packageExportName(module, symbol, owner.face, exportName) === undefined) {
2055
+ this.fail(site, `package reference ${exportName} is not exported by ${module.package} at ${module.subpath}`);
2056
+ }
2057
+ }
2058
+ const typeDeclaration = declaration;
2059
+ if (!this.declarationStates.has(this.symbolId(symbol)))
2060
+ this.ensureDeclaration(symbol, typeDeclaration);
2061
+ return { kind: 'declaration', symbol: this.symbolId(symbol) };
2062
+ }
2063
+ const packageFaces = module === undefined
2064
+ ? []
2065
+ : [...new Set(this.allRegistrations.filter(candidate => candidate.name === module.package).map(candidate => candidate.face))];
2066
+ const otherFace = packageFaces.find(face => face !== this.face);
2067
+ if (otherFace !== undefined && module !== undefined) {
2068
+ const requestedName = authoredExportName(site, moduleSpecifier);
2069
+ const exportName = this.packageExportName(module, symbol, otherFace, requestedName);
2070
+ if (exportName === undefined) {
2071
+ this.fail(site, `cross-face reference ${requestedName} is not exported by ${module.package} at ${module.subpath}`);
2072
+ }
2073
+ this.recordCrossFaceLink(from.name, otherFace, module, exportName);
2074
+ return {
2075
+ kind: 'cross-face',
2076
+ face: otherFace,
2077
+ package: module.package,
2078
+ subpath: module.subpath,
2079
+ name: exportName,
2080
+ };
2081
+ }
2082
+ if (module !== undefined) {
2083
+ return {
2084
+ kind: 'external',
2085
+ module: module.package,
2086
+ subpath: module.subpath,
2087
+ name: symbol.name,
2088
+ };
2089
+ }
2090
+ const external = externalModuleIdentityForFile(declaration.getSourceFile().fileName);
2091
+ if (external !== undefined) {
2092
+ return {
2093
+ kind: 'external',
2094
+ module: external.package,
2095
+ subpath: external.subpath,
2096
+ name: symbol.name,
2097
+ };
2098
+ }
2099
+ this.fail(site, `reference to ${symbol.name} crosses a package or face without an explicit import`);
2100
+ }
2101
+ recordCrossFaceLink(fromPackage, toFace, module, name) {
2102
+ const link = {
2103
+ fromFace: this.face,
2104
+ fromPackage,
2105
+ toFace,
2106
+ toPackage: module.package,
2107
+ subpath: module.subpath,
2108
+ name,
2109
+ };
2110
+ const key = [
2111
+ link.fromFace,
2112
+ link.fromPackage,
2113
+ link.toFace,
2114
+ link.toPackage,
2115
+ link.subpath,
2116
+ link.name,
2117
+ ].join('\0');
2118
+ this.crossFaceLinks.set(key, link);
2119
+ }
2120
+ packageExportName(module, symbol, face, requestedName) {
2121
+ const registration = this.allRegistrations.find(candidate => candidate.face === face && candidate.name === module.package);
2122
+ const target = packageExportTargets(registration.manifest)
2123
+ .find(([subpath]) => subpath === module.subpath)?.[1];
2124
+ if (target === undefined)
2125
+ return undefined;
2126
+ const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
2127
+ const moduleSymbol = this.checker.getSymbolAtLocation(sourceFile);
2128
+ const exported = this.checker.getExportsOfModule(moduleSymbol)
2129
+ .find(candidate => candidate.name === requestedName && this.resolveSymbol(candidate) === symbol);
2130
+ return exported?.name;
2131
+ }
2132
+ symbolAtType(node) {
2133
+ if (ts.isTypeReferenceNode(node)) {
2134
+ return this.resolveSymbol(this.checker.getSymbolAtLocation(node.typeName));
2135
+ }
2136
+ const type = this.checker.getTypeAtLocation(node);
2137
+ const symbol = type.aliasSymbol ?? type.getSymbol();
2138
+ return symbol === undefined ? undefined : this.resolveSymbol(symbol);
2139
+ }
2140
+ resolveSymbol(symbol) {
2141
+ return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : this.checker.getAliasedSymbol(symbol);
2142
+ }
2143
+ symbolId(symbol) {
2144
+ const declaration = preferredDeclaration(symbol);
2145
+ if (declaration === undefined)
2146
+ return `symbol:${symbol.name}`;
2147
+ const location = this.location(declaration);
2148
+ return `${this.packageNameForFile(declaration.getSourceFile().fileName)}:${location.file}#${symbol.name}`;
2149
+ }
2150
+ registrationForFile(file) {
2151
+ const path = realPath(file);
2152
+ return this.allRegistrations
2153
+ .find(registration => registration.face === this.face && isWithin(path, registration.root));
2154
+ }
2155
+ packageNameForFile(file) {
2156
+ const path = realPath(file);
2157
+ return this.allRegistrations.find(registration => isWithin(path, registration.root))?.name ?? '<external>';
2158
+ }
2159
+ allocateNodeId(site) {
2160
+ const location = this.locationKey(site);
2161
+ const ordinal = (this.nodeOrdinals.get(location) ?? 0) + 1;
2162
+ this.nodeOrdinals.set(location, ordinal);
2163
+ return `type:${location}#${String(ordinal)}`;
2164
+ }
2165
+ locationKey(node) {
2166
+ const location = this.location(node);
2167
+ return `${location.file}:${String(location.line)}:${String(location.column)}`;
2168
+ }
2169
+ location(node) {
2170
+ const sourceFile = node.getSourceFile();
2171
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
2172
+ return {
2173
+ file: slash(relative(this.root, sourceFile.fileName)),
2174
+ line: position.line + 1,
2175
+ column: position.character + 1,
2176
+ };
2177
+ }
2178
+ fail(node, message) {
2179
+ const location = this.location(node);
2180
+ throw new TypertAnalysisError(`typert(${this.face}): ${location.file}:${String(location.line)}:${String(location.column)}: ${message}`);
2181
+ }
2182
+ }
2183
+ function mergeWorkspaceModels(models) {
2184
+ const faces = new Map();
2185
+ const links = new Map();
2186
+ for (const model of models) {
2187
+ for (const face of model.faces) {
2188
+ const merged = faces.get(face.face) ?? {
2189
+ packages: new Map(),
2190
+ declarations: new Map(),
2191
+ nodes: new Map(),
2192
+ };
2193
+ for (const packageModel of face.packages)
2194
+ merged.packages.set(packageModel.name, packageModel);
2195
+ for (const declaration of face.graph.declarations) {
2196
+ if (!merged.declarations.has(declaration.id))
2197
+ merged.declarations.set(declaration.id, declaration);
2198
+ }
2199
+ for (const node of face.graph.nodes) {
2200
+ if (!merged.nodes.has(node.id))
2201
+ merged.nodes.set(node.id, node);
2202
+ }
2203
+ faces.set(face.face, merged);
2204
+ }
2205
+ for (const link of model.crossFaceLinks) {
2206
+ links.set([
2207
+ link.fromFace,
2208
+ link.fromPackage,
2209
+ link.toFace,
2210
+ link.toPackage,
2211
+ link.subpath,
2212
+ link.name,
2213
+ ].join('\0'), link);
2214
+ }
2215
+ }
2216
+ return {
2217
+ faces: [...faces].sort(([left], [right]) => (left === 'host' ? 0 : 1) - (right === 'host' ? 0 : 1)).map(([face, model]) => ({
2218
+ face,
2219
+ packages: [...model.packages.values()].sort((left, right) => left.name.localeCompare(right.name)),
2220
+ graph: {
2221
+ declarations: [...model.declarations.values()].sort((left, right) => left.id.localeCompare(right.id)),
2222
+ nodes: [...model.nodes.values()].sort((left, right) => left.id.localeCompare(right.id)),
2223
+ },
2224
+ })),
2225
+ crossFaceLinks: [...links.values()].sort(compareCrossFaceLinks),
2226
+ };
2227
+ }
2228
+ function parseConfig(path) {
2229
+ const compilerPath = path.split(sep).join('/');
2230
+ const read = ts.readConfigFile(compilerPath, file => ts.sys.readFile(file));
2231
+ if (read.error !== undefined)
2232
+ throw new TypertAnalysisError(formatDiagnostic(read.error));
2233
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(compilerPath), undefined, compilerPath);
2234
+ if (parsed.errors.length > 0)
2235
+ throw new TypertAnalysisError(parsed.errors.map(formatDiagnostic).join('\n'));
2236
+ return { path, parsed };
2237
+ }
2238
+ function projectConfigPath(path) {
2239
+ if (extname(path) === '.json')
2240
+ return path;
2241
+ return join(path, 'tsconfig.json');
2242
+ }
2243
+ function sourceFileHasSurface(sourceFile) {
2244
+ for (const statement of sourceFile.statements) {
2245
+ if ((ts.isClassDeclaration(statement)
2246
+ || ts.isInterfaceDeclaration(statement)
2247
+ || ts.isTypeAliasDeclaration(statement)
2248
+ || ts.isEnumDeclaration(statement))
2249
+ && (typertMode(statement) !== undefined || typertServiceTag(statement) !== undefined))
2250
+ return true;
2251
+ if (ts.isClassDeclaration(statement)) {
2252
+ for (const member of statement.members) {
2253
+ if (ts.isPropertyDeclaration(member)
2254
+ && memberName(member.name) === 'typertGateway'
2255
+ && member.initializer !== undefined
2256
+ && ts.isCallExpression(member.initializer)
2257
+ && expressionName(member.initializer.expression) === 'bindTypeRTGateway')
2258
+ return true;
2259
+ for (const decorator of ts.canHaveDecorators(member) ? ts.getDecorators(member) ?? [] : []) {
2260
+ const expression = ts.isCallExpression(decorator.expression)
2261
+ ? decorator.expression.expression
2262
+ : decorator.expression;
2263
+ const name = expressionName(expression);
2264
+ if (name === 'Remote' || name === 'RemoteScope')
2265
+ return true;
2266
+ }
2267
+ }
2268
+ }
2269
+ if (!ts.isModuleDeclaration(statement)
2270
+ || !ts.isStringLiteral(statement.name)
2271
+ || statement.name.text !== '@deepseek-ai/cordis'
2272
+ || statement.body === undefined
2273
+ || !ts.isModuleBlock(statement.body))
2274
+ continue;
2275
+ if (statement.body.statements.some(member => ts.isInterfaceDeclaration(member)
2276
+ && (member.name.text === 'Context' || member.name.text === 'Events')
2277
+ && member.members.length > 0))
2278
+ return true;
2279
+ }
2280
+ return false;
2281
+ }
2282
+ function hasPackageSurface(model) {
2283
+ return model.services.length > 0
2284
+ || model.events.length > 0
2285
+ || model.objects.length > 0
2286
+ || model.schemas.length > 0
2287
+ || model.invocations.length > 0;
2288
+ }
2289
+ function isDualFacePackage(manifest) {
2290
+ const dsh = manifest.dsh;
2291
+ const client = dsh !== null && typeof dsh === 'object'
2292
+ ? dsh.client
2293
+ : undefined;
2294
+ return client !== null
2295
+ && typeof client === 'object'
2296
+ && clientExportSubpaths(manifest).length > 0;
2297
+ }
2298
+ function hostExportSubpaths(manifest) {
2299
+ return packageExportTargets(manifest)
2300
+ .map(([subpath]) => subpath)
2301
+ .filter(subpath => subpath !== './client'
2302
+ && !subpath.startsWith('./client/')
2303
+ && subpath !== './remote');
2304
+ }
2305
+ function clientExportSubpaths(manifest) {
2306
+ return packageExportTargets(manifest)
2307
+ .map(([subpath]) => subpath)
2308
+ .filter(subpath => subpath === './client' || subpath.startsWith('./client/'));
2309
+ }
2310
+ function packageExportTargets(manifest) {
2311
+ const exportsField = manifest.exports;
2312
+ if (typeof exportsField === 'string')
2313
+ return [['.', exportsField]];
2314
+ if (exportsField === null || typeof exportsField !== 'object') {
2315
+ const types = manifest.types;
2316
+ return typeof types === 'string' ? [['.', types]] : [];
2317
+ }
2318
+ if (Array.isArray(exportsField)
2319
+ || !Object.keys(exportsField).some(key => key.startsWith('.'))) {
2320
+ const target = exportTarget(exportsField);
2321
+ return target === undefined ? [] : [['.', target]];
2322
+ }
2323
+ const result = [];
2324
+ for (const [subpath, value] of Object.entries(exportsField)) {
2325
+ if (!subpath.startsWith('.'))
2326
+ continue;
2327
+ const target = exportTarget(value);
2328
+ if (target !== undefined)
2329
+ result.push([subpath, target]);
2330
+ }
2331
+ return result.sort(([left], [right]) => left.localeCompare(right));
2332
+ }
2333
+ function exportTarget(value) {
2334
+ if (typeof value === 'string')
2335
+ return value;
2336
+ if (Array.isArray(value)) {
2337
+ for (const candidate of value) {
2338
+ const target = exportTarget(candidate);
2339
+ if (target !== undefined)
2340
+ return target;
2341
+ }
2342
+ return undefined;
2343
+ }
2344
+ if (value === null || typeof value !== 'object')
2345
+ return undefined;
2346
+ const conditions = value;
2347
+ for (const key of ['types', 'import', 'default']) {
2348
+ const target = exportTarget(conditions[key]);
2349
+ if (target !== undefined)
2350
+ return target;
2351
+ }
2352
+ for (const candidate of Object.values(conditions)) {
2353
+ const target = exportTarget(candidate);
2354
+ if (target !== undefined)
2355
+ return target;
2356
+ }
2357
+ return undefined;
2358
+ }
2359
+ function sourcePathForExport(packageRoot, target) {
2360
+ const normalized = target.replace(/^\.\//, '');
2361
+ if (normalized.startsWith('lib/types/')) {
2362
+ return resolve(packageRoot, 'src', normalized.slice('lib/types/'.length).replace(/\.d\.(?:mts|cts|ts)$/, '.ts'));
2363
+ }
2364
+ if (normalized.startsWith('lib/')) {
2365
+ return resolve(packageRoot, 'src', normalized.slice('lib/'.length).replace(/\.(?:mjs|cjs|js|d\.ts)$/, '.ts'));
2366
+ }
2367
+ return resolve(packageRoot, normalized);
2368
+ }
2369
+ function preferredDeclaration(symbol) {
2370
+ return symbol.declarations?.find(isTypeDeclaration)
2371
+ ?? symbol.valueDeclaration
2372
+ ?? symbol.declarations?.[0];
2373
+ }
2374
+ function optionalParent(node) {
2375
+ return node.parent;
2376
+ }
2377
+ function isTypeDeclaration(node) {
2378
+ return ts.isClassDeclaration(node)
2379
+ || ts.isInterfaceDeclaration(node)
2380
+ || ts.isTypeAliasDeclaration(node)
2381
+ || ts.isEnumDeclaration(node);
2382
+ }
2383
+ function declarationName(declaration) {
2384
+ return declaration.name.text;
2385
+ }
2386
+ function memberText(member) {
2387
+ const sourceFile = member.getSourceFile();
2388
+ const full = member.getText(sourceFile);
2389
+ const body = member.body;
2390
+ const signature = body === undefined ? full : full.slice(0, full.length - body.getText(sourceFile).length);
2391
+ return signature.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim();
2392
+ }
2393
+ function declarationText(declaration) {
2394
+ const printer = ts.createPrinter({ removeComments: true });
2395
+ const projected = ts.isClassDeclaration(declaration) ? classShape(declaration) : declaration;
2396
+ return printer.printNode(ts.EmitHint.Unspecified, projected, declaration.getSourceFile()).replace(/\r/g, '');
2397
+ }
2398
+ function classShape(node) {
2399
+ const nonPublic = (member) => (ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined)?.some(modifier => modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword) ?? false;
2400
+ const members = node.members.flatMap((member) => {
2401
+ if (nonPublic(member) || (ts.isPropertyDeclaration(member) && ts.isPrivateIdentifier(member.name)))
2402
+ return [];
2403
+ if (ts.isMethodDeclaration(member)) {
2404
+ return [ts.factory.updateMethodDeclaration(member, member.modifiers, member.asteriskToken, member.name, member.questionToken, member.typeParameters, member.parameters, member.type, undefined)];
2405
+ }
2406
+ if (ts.isConstructorDeclaration(member)) {
2407
+ return [ts.factory.updateConstructorDeclaration(member, member.modifiers, member.parameters, undefined)];
2408
+ }
2409
+ if (ts.isGetAccessorDeclaration(member)) {
2410
+ return [ts.factory.updateGetAccessorDeclaration(member, member.modifiers, member.name, member.parameters, member.type, undefined)];
2411
+ }
2412
+ if (ts.isSetAccessorDeclaration(member)) {
2413
+ return [ts.factory.updateSetAccessorDeclaration(member, member.modifiers, member.name, member.parameters, undefined)];
2414
+ }
2415
+ if (ts.isPropertyDeclaration(member)) {
2416
+ return [ts.factory.updatePropertyDeclaration(member, member.modifiers, member.name, member.questionToken ?? member.exclamationToken, member.type, undefined)];
2417
+ }
2418
+ return [member];
2419
+ });
2420
+ return ts.factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, members);
2421
+ }
2422
+ function documentationOf(node) {
2423
+ const blocks = ts.getJSDocCommentsAndTags(node).filter(ts.isJSDoc);
2424
+ const block = blocks.at(-1);
2425
+ if (block === undefined)
2426
+ return EMPTY_DOCUMENTATION;
2427
+ const description = normalizedDocText(ts.getTextOfJSDocComment(block.comment));
2428
+ const tags = ts.getJSDocTags(node).map((tag) => {
2429
+ const named = tag;
2430
+ const comment = normalizedDocText(ts.getTextOfJSDocComment(tag.comment));
2431
+ return {
2432
+ name: tag.tagName.text,
2433
+ ...(named.name === undefined ? {} : { argument: named.name.getText() }),
2434
+ ...(comment === undefined ? {} : { comment }),
2435
+ text: tag.getText(tag.getSourceFile()).trim(),
2436
+ };
2437
+ });
2438
+ return {
2439
+ ...(description === undefined ? {} : {
2440
+ description,
2441
+ summary: firstSentence(description),
2442
+ }),
2443
+ tags,
2444
+ jsDoc: rawJsDoc(node),
2445
+ };
2446
+ }
2447
+ function normalizedDocText(value) {
2448
+ if (value === undefined)
2449
+ return undefined;
2450
+ const normalized = value.replace(/\s+/g, ' ').trim();
2451
+ /* v8 ignore next -- TypeScript represents whitespace-only JSDoc as undefined before this helper is called. */
2452
+ return normalized.length === 0 ? undefined : normalized;
2453
+ }
2454
+ function firstSentence(value) {
2455
+ return (/^(.*?[.!?])(?:\s|$)/.exec(value)?.[1] ?? value).trim();
2456
+ }
2457
+ function rawJsDoc(node) {
2458
+ const sourceFile = node.getSourceFile();
2459
+ const source = sourceFile.getFullText();
2460
+ const ranges = ts.getLeadingCommentRanges(source, node.getFullStart());
2461
+ const range = ranges.filter(candidate => source.slice(candidate.pos, candidate.pos + 3) === '/**').at(-1);
2462
+ const raw = source.slice(range.pos, range.end);
2463
+ const { line } = sourceFile.getLineAndCharacterOfPosition(range.pos);
2464
+ const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0);
2465
+ const indent = source.slice(lineStart, range.pos);
2466
+ return raw.split('\n')
2467
+ .map((text, index) => index > 0 && text.startsWith(indent) ? text.slice(indent.length) : text)
2468
+ .join('\n');
2469
+ }
2470
+ function typertMode(node) {
2471
+ for (const tag of ts.getJSDocTags(node)) {
2472
+ if (tag.tagName.text !== 'typert')
2473
+ continue;
2474
+ const mode = (ts.getTextOfJSDocComment(tag.comment) ?? '').trim().split(/\s+/, 1)[0];
2475
+ if (mode === 'object')
2476
+ return 'object';
2477
+ if (mode === '' || mode === 'schema' || mode === 'type')
2478
+ return 'schema';
2479
+ }
2480
+ return undefined;
2481
+ }
2482
+ function typertServiceTag(node) {
2483
+ return ts.getJSDocTags(node).find(tag => tag.tagName.text === 'typert'
2484
+ && (ts.getTextOfJSDocComment(tag.comment) ?? '').trim().split(/\s+/, 1)[0] === 'service');
2485
+ }
2486
+ function memberName(name) {
2487
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name)
2488
+ || ts.isNumericLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name))
2489
+ return name.text;
2490
+ if (ts.isComputedPropertyName(name))
2491
+ return `[${name.expression.getText()}]`;
2492
+ return name.getText();
2493
+ }
2494
+ function stringLiteralValue(node) {
2495
+ return node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
2496
+ ? node.text
2497
+ : undefined;
2498
+ }
2499
+ function isRemoteSegment(value) {
2500
+ // Generation bootstraps workspace artifacts before dsh-type-meta is built,
2501
+ // so this extraction-only copy must mirror isTypeRTRemoteSegment().
2502
+ return value !== '.' && value !== '..' && /^[A-Za-z0-9_$.-]+$/.test(value);
2503
+ }
2504
+ function expressionName(node) {
2505
+ if (ts.isIdentifier(node))
2506
+ return node.text;
2507
+ if (ts.isPropertyAccessExpression(node))
2508
+ return node.name.text;
2509
+ return undefined;
2510
+ }
2511
+ function packageExportSpecifier(packageName, subpath) {
2512
+ return subpath === '.' ? packageName : `${packageName}${subpath.slice(1)}`;
2513
+ }
2514
+ function visibilityOf(node) {
2515
+ if ('name' in node && node.name !== undefined && ts.isPrivateIdentifier(node.name))
2516
+ return 'private';
2517
+ if (hasModifier(node, ts.SyntaxKind.PrivateKeyword))
2518
+ return 'private';
2519
+ if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword))
2520
+ return 'protected';
2521
+ return 'public';
2522
+ }
2523
+ function hasModifier(node, kind) {
2524
+ return (ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined)?.some(modifier => modifier.kind === kind) ?? false;
2525
+ }
2526
+ function exposableMember(member) {
2527
+ return member.visibility === 'public' && !member.static;
2528
+ }
2529
+ function keywordName(kind) {
2530
+ switch (kind) {
2531
+ case ts.SyntaxKind.AnyKeyword: return 'any';
2532
+ case ts.SyntaxKind.BigIntKeyword: return 'bigint';
2533
+ case ts.SyntaxKind.BooleanKeyword: return 'boolean';
2534
+ case ts.SyntaxKind.NeverKeyword: return 'never';
2535
+ case ts.SyntaxKind.NumberKeyword: return 'number';
2536
+ case ts.SyntaxKind.ObjectKeyword: return 'object';
2537
+ case ts.SyntaxKind.StringKeyword: return 'string';
2538
+ case ts.SyntaxKind.SymbolKeyword: return 'symbol';
2539
+ case ts.SyntaxKind.UndefinedKeyword: return 'undefined';
2540
+ case ts.SyntaxKind.UnknownKeyword: return 'unknown';
2541
+ case ts.SyntaxKind.VoidKeyword: return 'void';
2542
+ default: return undefined;
2543
+ }
2544
+ }
2545
+ function literalModel(node) {
2546
+ const literal = node.literal;
2547
+ if (ts.isStringLiteral(literal))
2548
+ return { kind: 'literal', value: literal.text, text: literal.getText() };
2549
+ if (ts.isNoSubstitutionTemplateLiteral(literal)) {
2550
+ return { kind: 'literal', value: literal.text, text: literal.getText() };
2551
+ }
2552
+ if (ts.isNumericLiteral(literal))
2553
+ return { kind: 'literal', value: Number(literal.text), text: literal.getText() };
2554
+ if (ts.isBigIntLiteral(literal))
2555
+ return { kind: 'literal', value: BigInt(literal.text.slice(0, -1)), text: literal.getText() };
2556
+ if (literal.kind === ts.SyntaxKind.TrueKeyword)
2557
+ return { kind: 'literal', value: true, text: 'true' };
2558
+ if (literal.kind === ts.SyntaxKind.FalseKeyword)
2559
+ return { kind: 'literal', value: false, text: 'false' };
2560
+ if (literal.kind === ts.SyntaxKind.NullKeyword)
2561
+ return { kind: 'literal', value: null, text: 'null' };
2562
+ /* v8 ignore else -- all remaining LiteralTypeNode syntax is a signed numeric or bigint literal. */
2563
+ if (ts.isPrefixUnaryExpression(literal)
2564
+ && (ts.isNumericLiteral(literal.operand) || ts.isBigIntLiteral(literal.operand))) {
2565
+ return {
2566
+ kind: 'literal',
2567
+ value: ts.isBigIntLiteral(literal.operand)
2568
+ ? BigInt(literal.getText().slice(0, -1))
2569
+ : Number(literal.getText()),
2570
+ text: literal.getText(),
2571
+ };
2572
+ }
2573
+ /* v8 ignore next -- TypeScript's LiteralTypeNode grammar is exhausted above; this contains future compiler syntax. */
2574
+ throw new TypertAnalysisError(`typert: unsupported literal type ${literal.getText()}`);
2575
+ }
2576
+ function modifierMode(token) {
2577
+ if (token?.kind === ts.SyntaxKind.PlusToken)
2578
+ return 'add';
2579
+ if (token?.kind === ts.SyntaxKind.MinusToken)
2580
+ return 'remove';
2581
+ return token === undefined ? 'preserve' : 'add';
2582
+ }
2583
+ function annotationPosition(node, purpose) {
2584
+ if (purpose === 'return')
2585
+ return node.parameters.end + 1;
2586
+ return node.name.end;
2587
+ }
2588
+ function moduleSpecifierOf(node) {
2589
+ if (ts.isImportTypeNode(node)) {
2590
+ const argument = node.argument;
2591
+ return argument.literal.text;
2592
+ }
2593
+ const symbol = ts.isTypeReferenceNode(node)
2594
+ ? node.typeName
2595
+ : node.expression;
2596
+ const sourceFile = node.getSourceFile();
2597
+ const first = ts.isIdentifier(symbol) ? symbol.text : symbol.getFirstToken(sourceFile)?.getText(sourceFile);
2598
+ for (const statement of sourceFile.statements) {
2599
+ if (!ts.isImportDeclaration(statement) || statement.importClause === undefined
2600
+ || !ts.isStringLiteral(statement.moduleSpecifier))
2601
+ continue;
2602
+ if (statement.importClause.name?.text === first)
2603
+ return statement.moduleSpecifier.text;
2604
+ const bindings = statement.importClause.namedBindings;
2605
+ if (bindings !== undefined && ts.isNamespaceImport(bindings) && bindings.name.text === first) {
2606
+ return statement.moduleSpecifier.text;
2607
+ }
2608
+ if (bindings !== undefined && ts.isNamedImports(bindings)
2609
+ && bindings.elements.some(element => element.name.text === first))
2610
+ return statement.moduleSpecifier.text;
2611
+ }
2612
+ return undefined;
2613
+ }
2614
+ function authoredExportName(node, moduleSpecifier) {
2615
+ if (ts.isImportTypeNode(node))
2616
+ return node.qualifier.getText().split('.')[0];
2617
+ const referenced = ts.isTypeReferenceNode(node)
2618
+ ? node.typeName.getText().split('.')
2619
+ : node.expression.getText().split('.');
2620
+ const localName = referenced[0];
2621
+ for (const statement of node.getSourceFile().statements) {
2622
+ if (!ts.isImportDeclaration(statement)
2623
+ || statement.importClause === undefined
2624
+ || !ts.isStringLiteral(statement.moduleSpecifier)
2625
+ || statement.moduleSpecifier.text !== moduleSpecifier)
2626
+ continue;
2627
+ if (statement.importClause.name?.text === localName)
2628
+ return 'default';
2629
+ const bindings = statement.importClause.namedBindings;
2630
+ if (bindings !== undefined && ts.isNamedImports(bindings)) {
2631
+ const imported = bindings.elements.find(element => element.name.text === localName);
2632
+ if (imported !== undefined)
2633
+ return imported.propertyName?.text ?? imported.name.text;
2634
+ }
2635
+ if (bindings !== undefined && ts.isNamespaceImport(bindings) && bindings.name.text === localName) {
2636
+ return referenced[1];
2637
+ }
2638
+ }
2639
+ /* v8 ignore next -- moduleSpecifierOf returns only the matching import inspected by this loop. */
2640
+ throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
2641
+ }
2642
+ function importTypeAttributesText(node) {
2643
+ const sourceFile = node.getSourceFile();
2644
+ const children = node.getChildren(sourceFile);
2645
+ const comma = children.find(child => child.kind === ts.SyntaxKind.CommaToken);
2646
+ const close = children.find(child => child.kind === ts.SyntaxKind.CloseParenToken);
2647
+ return sourceFile.text.slice(comma.end, close.pos).trim();
2648
+ }
2649
+ function moduleIdentity(specifier) {
2650
+ if (specifier.startsWith('.') || specifier.startsWith('/'))
2651
+ return undefined;
2652
+ const parts = specifier.split('/');
2653
+ const packageLength = specifier.startsWith('@') ? 2 : 1;
2654
+ const packageName = parts.slice(0, packageLength).join('/');
2655
+ const rest = parts.slice(packageLength).join('/');
2656
+ return {
2657
+ package: packageName,
2658
+ subpath: rest.length === 0 ? '.' : `./${rest}`,
2659
+ };
2660
+ }
2661
+ function externalModuleIdentityForFile(file) {
2662
+ const normalized = slash(file);
2663
+ const marker = '/node_modules/';
2664
+ const index = normalized.lastIndexOf(marker);
2665
+ if (index < 0)
2666
+ return undefined;
2667
+ const parts = normalized.slice(index + marker.length).split('/');
2668
+ const packageLength = parts[0].startsWith('@') ? 2 : 1;
2669
+ const packageName = parts.slice(0, packageLength).join('/');
2670
+ return { package: packageName, subpath: '.' };
2671
+ }
2672
+ function isStandardLibraryFile(file) {
2673
+ const base = file.replaceAll('\\', '/');
2674
+ return /\/typescript\/lib\/lib\.[^/]+\.d\.ts$/.test(base);
2675
+ }
2676
+ function formatDiagnostic(diagnostic) {
2677
+ return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
2678
+ }
2679
+ function formatProgramDiagnostic(root, face, diagnostic) {
2680
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
2681
+ const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
2682
+ const file = slash(relative(root, diagnostic.file.fileName));
2683
+ return `typert(${face}): ${file}:${String(position.line + 1)}:${String(position.character + 1)}: TypeScript TS${String(diagnostic.code)}: ${message}`;
2684
+ }
2685
+ const realPathCache = new Map();
2686
+ function realPath(path) {
2687
+ const absolute = resolve(path);
2688
+ const cached = realPathCache.get(absolute);
2689
+ if (cached !== undefined)
2690
+ return cached;
2691
+ // Only existing paths are memoized: a path can come into existence later,
2692
+ // but an existing path's canonical form is stable for the process lifetime
2693
+ // (analysis edits rewrite file contents, never the directory tree).
2694
+ if (!existsSync(absolute))
2695
+ return absolute;
2696
+ const resolved = realpathSync(absolute);
2697
+ realPathCache.set(absolute, resolved);
2698
+ return resolved;
2699
+ }
2700
+ function isWithin(path, root) {
2701
+ const absolute = realPath(path);
2702
+ const parent = realPath(root);
2703
+ return absolute === parent || absolute.startsWith(parent + sep);
2704
+ }
2705
+ function slash(value) {
2706
+ return value.replaceAll('\\', '/');
2707
+ }
2708
+ function uniqueBy(values, key) {
2709
+ const result = new Map();
2710
+ for (const value of values)
2711
+ if (!result.has(key(value)))
2712
+ result.set(key(value), value);
2713
+ return [...result.values()];
2714
+ }
2715
+ function compareCrossFaceLinks(left, right) {
2716
+ return left.fromFace.localeCompare(right.fromFace)
2717
+ || left.fromPackage.localeCompare(right.fromPackage)
2718
+ || left.toFace.localeCompare(right.toFace)
2719
+ || left.toPackage.localeCompare(right.toPackage)
2720
+ || left.subpath.localeCompare(right.subpath)
2721
+ || left.name.localeCompare(right.name);
2722
+ }
2723
+ //# sourceMappingURL=analyzer.js.map