carrick 0.3.101 → 0.3.103

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.
Files changed (39) hide show
  1. package/package.json +6 -6
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/sidecar/dist/src/bundler.d.ts +3 -92
  4. package/sidecar/dist/src/bundler.js +5 -265
  5. package/sidecar/dist/src/capture/anchors.js +113 -8
  6. package/sidecar/dist/src/capture/check-fields.js +19 -5
  7. package/sidecar/dist/src/capture/check-probe.d.ts +5 -0
  8. package/sidecar/dist/src/capture/check-probe.js +31 -2
  9. package/sidecar/dist/src/capture/check-workspace.d.ts +3 -0
  10. package/sidecar/dist/src/capture/check-workspace.js +13 -16
  11. package/sidecar/dist/src/capture/check.js +5 -5
  12. package/sidecar/dist/src/capture/deep-walk.js +45 -12
  13. package/sidecar/dist/src/capture/deno-project.d.ts +2 -1
  14. package/sidecar/dist/src/capture/deno-project.js +22 -18
  15. package/sidecar/dist/src/capture/guarded-fs.d.ts +76 -0
  16. package/sidecar/dist/src/capture/guarded-fs.js +182 -0
  17. package/sidecar/dist/src/capture/index.js +60 -26
  18. package/sidecar/dist/src/capture/outside-root.d.ts +41 -0
  19. package/sidecar/dist/src/capture/outside-root.js +101 -0
  20. package/sidecar/dist/src/capture/paths-rewrite.d.ts +8 -0
  21. package/sidecar/dist/src/capture/paths-rewrite.js +9 -7
  22. package/sidecar/dist/src/capture/repair-dangling.d.ts +11 -1
  23. package/sidecar/dist/src/capture/repair-dangling.js +15 -4
  24. package/sidecar/dist/src/capture/self-check.d.ts +3 -0
  25. package/sidecar/dist/src/capture/self-check.js +3 -3
  26. package/sidecar/dist/src/index.d.ts +1 -1
  27. package/sidecar/dist/src/index.js +2 -115
  28. package/sidecar/dist/src/origin.d.ts +49 -8
  29. package/sidecar/dist/src/origin.js +81 -8
  30. package/sidecar/dist/src/project-loader.js +15 -5
  31. package/sidecar/dist/src/type-inferrer.d.ts +72 -0
  32. package/sidecar/dist/src/type-inferrer.js +248 -8
  33. package/sidecar/dist/src/type-structural-expander.d.ts +5 -1
  34. package/sidecar/dist/src/type-structural-expander.js +1 -1
  35. package/sidecar/dist/src/types.d.ts +31 -178
  36. package/sidecar/dist/src/validators.d.ts +58 -914
  37. package/sidecar/dist/src/validators.js +0 -55
  38. package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
  39. package/sidecar/dist/src/monorepo-builder.js +0 -584
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.101",
3
+ "version": "0.3.103",
4
4
  "description": "Maps your entire TypeScript codebase across services and repositories, giving AI agents full context on existing types, routes, and function behaviours over MCP before they write duplicate or breaking code.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -58,11 +58,11 @@
58
58
  "zod": "^3.23.0"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@carrick-tools/cli-darwin-arm64": "0.3.101",
62
- "@carrick-tools/cli-darwin-x64": "0.3.101",
63
- "@carrick-tools/cli-linux-arm64": "0.3.101",
64
- "@carrick-tools/cli-linux-x64": "0.3.101",
65
- "@carrick-tools/cli-win32-x64": "0.3.101"
61
+ "@carrick-tools/cli-darwin-arm64": "0.3.103",
62
+ "@carrick-tools/cli-darwin-x64": "0.3.103",
63
+ "@carrick-tools/cli-linux-arm64": "0.3.103",
64
+ "@carrick-tools/cli-linux-x64": "0.3.103",
65
+ "@carrick-tools/cli-win32-x64": "0.3.103"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^24.13.3",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "carrick",
3
3
  "description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
4
- "version": "0.3.101",
4
+ "version": "0.3.103",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -1,27 +1,9 @@
1
1
  /**
2
- * Surface Emitter - Emits .d.ts surface files with rewritten module specifiers
3
- *
4
- * This module replaces the previous dts-bundle-generator approach with a
5
- * ts-morph-based AST transformation and emission pipeline.
6
- *
7
- * Key features:
8
- * - Does NOT bundle dependencies (unlike dts-bundle-generator)
9
- * - Rewrites module specifiers to repo-specific aliases (@carrick/{repoName}/{spec})
10
- * - Handles ImportDeclaration, ImportTypeNode, and ExportDeclaration
11
- * - Supports scoped packages and subpaths
12
- * - Includes a safety pass for any compiler-introduced raw specifiers
2
+ * TypeBundler - the `bundle` action: each requested symbol's declaration text,
3
+ * read from the init'd ts-morph project.
13
4
  */
14
5
  import { Project } from 'ts-morph';
15
- import type { PayloadDefinition, SurfaceEmitResult, SymbolRequest, BundleResult } from './types.js';
16
- /**
17
- * Options for SurfaceEmitter construction
18
- */
19
- export interface SurfaceEmitterOptions {
20
- /** The ts-morph Project instance */
21
- project: Project;
22
- /** The repository root directory (absolute path) */
23
- repoRoot: string;
24
- }
6
+ import type { SymbolRequest, BundleResult } from './types.js';
25
7
  /**
26
8
  * Options for TypeBundler construction (legacy)
27
9
  */
@@ -31,82 +13,11 @@ export interface TypeBundlerOptions {
31
13
  /** The repository root directory (absolute path) */
32
14
  repoRoot: string;
33
15
  }
34
- /**
35
- * SurfaceEmitter - Generates .d.ts surface files with rewritten imports
36
- *
37
- * Usage:
38
- * const emitter = new SurfaceEmitter({ project, repoRoot });
39
- * const result = emitter.emit(repoName, payloads, outputPath);
40
- */
41
- export declare class SurfaceEmitter {
42
- private readonly project;
43
- private readonly repoRoot;
44
- constructor(options: SurfaceEmitterOptions);
45
- /**
46
- * Emit a surface .d.ts file containing payload type declarations
47
- *
48
- * @param repoName - Repository name for specifier rewriting
49
- * @param payloads - Payload definitions to include
50
- * @param outputPath - Path to write the surface file
51
- * @returns SurfaceEmitResult
52
- */
53
- emit(repoName: string, payloads: PayloadDefinition[], outputPath: string): SurfaceEmitResult;
54
- /**
55
- * Extract potential import references from a type string.
56
- * These are module specifiers that might need rewriting.
57
- */
58
- private extractImportReferences;
59
- /**
60
- * Rewrite all module specifiers in a source file to use repo-scoped aliases.
61
- *
62
- * Targets:
63
- * 1. ImportDeclaration.moduleSpecifier
64
- * 2. ImportTypeNode argument (import("..."))
65
- * 3. ExportDeclaration.moduleSpecifier
66
- *
67
- * @returns Array of rewritten specifiers
68
- */
69
- private rewriteModuleSpecifiers;
70
- /**
71
- * Determine if a module specifier should be rewritten.
72
- * Only non-relative specifiers should be rewritten.
73
- */
74
- private shouldRewriteSpecifier;
75
- /**
76
- * Rewrite a module specifier to the repo-scoped format.
77
- *
78
- * Format: @carrick/{repoName}/{originalSpecifier}
79
- *
80
- * Preserves:
81
- * - Scoped packages: "@scope/pkg" → "@carrick/repo-a/@scope/pkg"
82
- * - Subpaths: "zod/lib/helpers" → "@carrick/repo-a/zod/lib/helpers"
83
- * - Combined: "@scope/pkg/subpath" → "@carrick/repo-a/@scope/pkg/subpath"
84
- */
85
- private rewriteSpecifier;
86
- /**
87
- * Safety pass: Scan emitted content for any raw specifiers that might have
88
- * been introduced by the TypeScript compiler and rewrite them.
89
- *
90
- * This catches edge cases where the compiler emits types with their original
91
- * module specifiers that weren't caught by AST traversal.
92
- */
93
- private safetyRewritePass;
94
- /**
95
- * Log a message to stderr
96
- */
97
- private log;
98
- /**
99
- * Log an error to stderr
100
- */
101
- private logError;
102
- }
103
16
  /**
104
17
  * TypeBundler - Legacy bundler using simpler approach
105
18
  *
106
19
  * This is a simplified replacement that doesn't use dts-bundle-generator.
107
20
  * It extracts type definitions directly using ts-morph.
108
- *
109
- * @deprecated Use SurfaceEmitter instead for the new architecture
110
21
  */
111
22
  export declare class TypeBundler {
112
23
  private readonly project;
@@ -1,20 +1,12 @@
1
1
  /**
2
- * Surface Emitter - Emits .d.ts surface files with rewritten module specifiers
3
- *
4
- * This module replaces the previous dts-bundle-generator approach with a
5
- * ts-morph-based AST transformation and emission pipeline.
6
- *
7
- * Key features:
8
- * - Does NOT bundle dependencies (unlike dts-bundle-generator)
9
- * - Rewrites module specifiers to repo-specific aliases (@carrick/{repoName}/{spec})
10
- * - Handles ImportDeclaration, ImportTypeNode, and ExportDeclaration
11
- * - Supports scoped packages and subpaths
12
- * - Includes a safety pass for any compiler-introduced raw specifiers
2
+ * TypeBundler - the `bundle` action: each requested symbol's declaration text,
3
+ * read from the init'd ts-morph project.
13
4
  */
14
5
  import { Node, ts, } from 'ts-morph';
15
6
  import * as path from 'node:path';
16
7
  import * as fs from 'node:fs';
17
8
  import { expandTypeStructural, } from './type-structural-expander.js';
9
+ import { externalImportsOf } from './origin.js';
18
10
  /**
19
11
  * #248: upper bound on `SymbolRequest.array_depth`. SDL list nesting is
20
12
  * realistically 1-3 levels (`[Order!]!`, at most `[[Order!]!]!`); this is a
@@ -116,267 +108,14 @@ function declarationSite(sourceFile, name, label) {
116
108
  }
117
109
  return { reason: `Symbol '${name}' not found in ${label}` };
118
110
  }
119
- /**
120
- * SurfaceEmitter - Generates .d.ts surface files with rewritten imports
121
- *
122
- * Usage:
123
- * const emitter = new SurfaceEmitter({ project, repoRoot });
124
- * const result = emitter.emit(repoName, payloads, outputPath);
125
- */
126
- export class SurfaceEmitter {
127
- project;
128
- repoRoot;
129
- constructor(options) {
130
- this.project = options.project;
131
- this.repoRoot = options.repoRoot;
132
- }
133
- /**
134
- * Emit a surface .d.ts file containing payload type declarations
135
- *
136
- * @param repoName - Repository name for specifier rewriting
137
- * @param payloads - Payload definitions to include
138
- * @param outputPath - Path to write the surface file
139
- * @returns SurfaceEmitResult
140
- */
141
- emit(repoName, payloads, outputPath) {
142
- const errors = [];
143
- const manifest = [];
144
- try {
145
- // Phase 1: Generate surface content as a string
146
- const surfaceLines = [
147
- '// Generated surface file for type compatibility checking',
148
- '// Do not edit - this file is auto-generated by Carrick',
149
- '',
150
- ];
151
- // Collect all import specifiers we need to track for rewriting
152
- const importSpecifiers = new Set();
153
- // Phase 2: Add payload type declarations
154
- for (const payload of payloads) {
155
- const { alias, type_string, source_file } = payload;
156
- // Analyze type string for import references
157
- const detectedImports = this.extractImportReferences(type_string);
158
- for (const imp of detectedImports) {
159
- importSpecifiers.add(imp);
160
- }
161
- // Add type alias declaration
162
- surfaceLines.push(`export type ${alias} = ${type_string};`);
163
- surfaceLines.push('');
164
- manifest.push({
165
- alias,
166
- type_string,
167
- rewritten_imports: [],
168
- });
169
- }
170
- // Phase 3: Create a temporary source file for AST manipulation
171
- const tempFileName = `__carrick_surface_${repoName}_${Date.now()}.d.ts`;
172
- const tempFilePath = path.join(this.repoRoot, tempFileName);
173
- const initialContent = surfaceLines.join('\n');
174
- const tempSourceFile = this.project.createSourceFile(tempFilePath, initialContent, { overwrite: true });
175
- // Phase 4: Rewrite module specifiers in the AST
176
- const rewrittenImports = this.rewriteModuleSpecifiers(tempSourceFile, repoName);
177
- // Update manifest with rewritten imports
178
- for (const entry of manifest) {
179
- entry.rewritten_imports = rewrittenImports;
180
- }
181
- // Phase 5: Get the transformed content
182
- let surfaceContent = tempSourceFile.getFullText();
183
- // Phase 6: Safety pass - catch any raw specifiers the compiler might have added
184
- surfaceContent = this.safetyRewritePass(surfaceContent, repoName);
185
- // Phase 7: Clean up temporary source file from project
186
- this.project.removeSourceFile(tempSourceFile);
187
- // Phase 8: Write output file
188
- const absoluteOutputPath = path.isAbsolute(outputPath)
189
- ? outputPath
190
- : path.join(this.repoRoot, outputPath);
191
- // Ensure directory exists
192
- const outputDir = path.dirname(absoluteOutputPath);
193
- if (!fs.existsSync(outputDir)) {
194
- fs.mkdirSync(outputDir, { recursive: true });
195
- }
196
- fs.writeFileSync(absoluteOutputPath, surfaceContent, 'utf-8');
197
- this.log(`Emitted surface file: ${absoluteOutputPath}`);
198
- return {
199
- success: true,
200
- surface_content: surfaceContent,
201
- output_path: absoluteOutputPath,
202
- manifest,
203
- };
204
- }
205
- catch (err) {
206
- const error = err instanceof Error ? err.message : String(err);
207
- this.logError(`Surface emission failed: ${error}`);
208
- errors.push(error);
209
- return {
210
- success: false,
211
- errors,
212
- };
213
- }
214
- }
215
- /**
216
- * Extract potential import references from a type string.
217
- * These are module specifiers that might need rewriting.
218
- */
219
- extractImportReferences(typeString) {
220
- const imports = [];
221
- // Match import("...") syntax
222
- const importTypeRegex = /import\s*\(\s*["']([^"']+)["']\s*\)/g;
223
- let match;
224
- while ((match = importTypeRegex.exec(typeString)) !== null) {
225
- imports.push(match[1]);
226
- }
227
- // Match from "..." syntax (in case of inline imports)
228
- const fromRegex = /from\s+["']([^"']+)["']/g;
229
- while ((match = fromRegex.exec(typeString)) !== null) {
230
- imports.push(match[1]);
231
- }
232
- return imports;
233
- }
234
- /**
235
- * Rewrite all module specifiers in a source file to use repo-scoped aliases.
236
- *
237
- * Targets:
238
- * 1. ImportDeclaration.moduleSpecifier
239
- * 2. ImportTypeNode argument (import("..."))
240
- * 3. ExportDeclaration.moduleSpecifier
241
- *
242
- * @returns Array of rewritten specifiers
243
- */
244
- rewriteModuleSpecifiers(sourceFile, repoName) {
245
- const rewrittenSpecifiers = [];
246
- // 1. Rewrite ImportDeclaration specifiers
247
- const importDecls = sourceFile.getImportDeclarations();
248
- for (const importDecl of importDecls) {
249
- const specifier = importDecl.getModuleSpecifierValue();
250
- if (this.shouldRewriteSpecifier(specifier)) {
251
- const newSpecifier = this.rewriteSpecifier(specifier, repoName);
252
- importDecl.setModuleSpecifier(newSpecifier);
253
- rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
254
- }
255
- }
256
- // 2. Rewrite ExportDeclaration specifiers
257
- const exportDecls = sourceFile.getExportDeclarations();
258
- for (const exportDecl of exportDecls) {
259
- const specifier = exportDecl.getModuleSpecifierValue();
260
- if (specifier && this.shouldRewriteSpecifier(specifier)) {
261
- const newSpecifier = this.rewriteSpecifier(specifier, repoName);
262
- exportDecl.setModuleSpecifier(newSpecifier);
263
- rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
264
- }
265
- }
266
- // 3. Rewrite ImportTypeNode arguments
267
- // These appear in type positions: import("module").Type
268
- sourceFile.forEachDescendant((node) => {
269
- if (Node.isImportTypeNode(node)) {
270
- const arg = node.getArgument();
271
- if (Node.isLiteralTypeNode(arg)) {
272
- const literal = arg.getLiteral();
273
- if (Node.isStringLiteral(literal)) {
274
- const specifier = literal.getLiteralValue();
275
- if (this.shouldRewriteSpecifier(specifier)) {
276
- const newSpecifier = this.rewriteSpecifier(specifier, repoName);
277
- literal.setLiteralValue(newSpecifier);
278
- rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
279
- }
280
- }
281
- }
282
- }
283
- });
284
- return rewrittenSpecifiers;
285
- }
286
- /**
287
- * Determine if a module specifier should be rewritten.
288
- * Only non-relative specifiers should be rewritten.
289
- */
290
- shouldRewriteSpecifier(specifier) {
291
- // Don't rewrite relative imports
292
- if (specifier.startsWith('./') || specifier.startsWith('../')) {
293
- return false;
294
- }
295
- // Don't rewrite already-rewritten specifiers
296
- if (specifier.startsWith('@carrick/')) {
297
- return false;
298
- }
299
- // Don't rewrite node built-ins
300
- if (specifier.startsWith('node:')) {
301
- return false;
302
- }
303
- // Some common node built-ins without prefix
304
- const nodeBuiltins = [
305
- 'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https',
306
- 'url', 'querystring', 'crypto', 'buffer', 'child_process', 'cluster',
307
- 'dgram', 'dns', 'net', 'readline', 'tls', 'tty', 'zlib', 'assert',
308
- 'async_hooks', 'console', 'constants', 'domain', 'inspector', 'module',
309
- 'perf_hooks', 'process', 'punycode', 'string_decoder', 'timers',
310
- 'trace_events', 'v8', 'vm', 'worker_threads'
311
- ];
312
- if (nodeBuiltins.includes(specifier) || nodeBuiltins.includes(specifier.split('/')[0])) {
313
- return false;
314
- }
315
- return true;
316
- }
317
- /**
318
- * Rewrite a module specifier to the repo-scoped format.
319
- *
320
- * Format: @carrick/{repoName}/{originalSpecifier}
321
- *
322
- * Preserves:
323
- * - Scoped packages: "@scope/pkg" → "@carrick/repo-a/@scope/pkg"
324
- * - Subpaths: "zod/lib/helpers" → "@carrick/repo-a/zod/lib/helpers"
325
- * - Combined: "@scope/pkg/subpath" → "@carrick/repo-a/@scope/pkg/subpath"
326
- */
327
- rewriteSpecifier(specifier, repoName) {
328
- return `@carrick/${repoName}/${specifier}`;
329
- }
330
- /**
331
- * Safety pass: Scan emitted content for any raw specifiers that might have
332
- * been introduced by the TypeScript compiler and rewrite them.
333
- *
334
- * This catches edge cases where the compiler emits types with their original
335
- * module specifiers that weren't caught by AST traversal.
336
- */
337
- safetyRewritePass(content, repoName) {
338
- // Common patterns to catch:
339
- // - from "package"
340
- // - import("package")
341
- // - export * from "package"
342
- // We need to be careful not to double-rewrite @carrick/ specifiers
343
- // Pattern for import("...") that's not already @carrick
344
- const importTypePattern = /import\s*\(\s*["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']\s*\)/g;
345
- content = content.replace(importTypePattern, (match, specifier) => {
346
- const newSpecifier = this.rewriteSpecifier(specifier, repoName);
347
- return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
348
- });
349
- // Pattern for from "..." that's not already @carrick
350
- const fromPattern = /from\s+["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']/g;
351
- content = content.replace(fromPattern, (match, specifier) => {
352
- const newSpecifier = this.rewriteSpecifier(specifier, repoName);
353
- return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
354
- });
355
- return content;
356
- }
357
- /**
358
- * Log a message to stderr
359
- */
360
- log(message) {
361
- console.error(`[sidecar:surface-emitter] ${message}`);
362
- }
363
- /**
364
- * Log an error to stderr
365
- */
366
- logError(message) {
367
- console.error(`[sidecar:surface-emitter:error] ${message}`);
368
- }
369
- }
370
111
  // =============================================================================
371
- // Legacy TypeBundler (kept for backwards compatibility during migration)
112
+ // TypeBundler
372
113
  // =============================================================================
373
114
  /**
374
115
  * TypeBundler - Legacy bundler using simpler approach
375
116
  *
376
117
  * This is a simplified replacement that doesn't use dts-bundle-generator.
377
118
  * It extracts type definitions directly using ts-morph.
378
- *
379
- * @deprecated Use SurfaceEmitter instead for the new architecture
380
119
  */
381
120
  export class TypeBundler {
382
121
  project;
@@ -558,6 +297,7 @@ export class TypeBundler {
558
297
  return {
559
298
  program: this.project.getProgram().compilerObject,
560
299
  repoRoot: this.repoRoot,
300
+ imports: externalImportsOf(this.project),
561
301
  };
562
302
  }
563
303
  extractTypeDefinition(symbol) {
@@ -54,13 +54,20 @@ export function resolveAnchor(program, request, args) {
54
54
  // (a generated model that was never generated). The stub then self-checks
55
55
  // such a name as an error placeholder, which no walk flags. A name a
56
56
  // sibling symbol anchor imports is resolved by that import.
57
+ // carrick#1774: the text names types as they read where it was printed,
58
+ // so a name its source file means by a repo module's export is imported
59
+ // from that module (a string-union alias printed bare read `any`).
60
+ const scoped = siblingSpec
61
+ ? undefined
62
+ : qualifyNamesFromSource(text, program, request.source_file, args);
63
+ const scopedText = scoped ?? text;
57
64
  // carrick#1377: rewrite what nothing declares to `unknown` in place, so
58
65
  // one member typed by a module the checkout does not have stops taking
59
66
  // every member around it down with it.
60
67
  const rewritten = siblingSpec || !args.placeholder
61
68
  ? undefined
62
- : substituteUndeclaredNamesInText(text, program, args.placeholder);
63
- const aliasBody = rewritten?.text ?? text;
69
+ : substituteUndeclaredNamesInText(scopedText, program, args.placeholder);
70
+ const aliasBody = rewritten?.text ?? scopedText;
64
71
  const undeclaredNames = siblingSpec || !args.placeholder
65
72
  ? []
66
73
  : undeclaredNamesInText(aliasBody, program, args.placeholder);
@@ -349,6 +356,95 @@ function substituteUndeclaredNamesInText(text, program, destination) {
349
356
  paths: rewritten.substitutions.map((entry) => entry.path),
350
357
  };
351
358
  }
359
+ /**
360
+ * carrick#1774: literal text with every type name read where it was printed.
361
+ *
362
+ * The text was printed in `sourceFileRel` (the v1 inferrer prints at the node,
363
+ * where the module's own declarations and imports are in scope), so a type
364
+ * reference in it means what its leftmost name resolves to in that file. The
365
+ * surface entry is another scope: there such a name names nothing (TS2304, a
366
+ * member that reads `any`) or a global of the same name (`Notification`).
367
+ * Each reference whose name the source resolves to a type that a module
368
+ * inside the repo exports becomes `import('<module>').<export>`, with its
369
+ * qualifier and type arguments kept. Anything else is left as written: a
370
+ * global, a name only a function body declares, an unexported local, and a
371
+ * type declared outside the repo or under `node_modules`, which the stub does
372
+ * not ship.
373
+ *
374
+ * Returns the rewritten text, or undefined when nothing was rewritten, so a
375
+ * text with no such name stays byte-identical.
376
+ */
377
+ function qualifyNamesFromSource(text, program, sourceFileRel, args) {
378
+ if (!sourceFileRel)
379
+ return undefined;
380
+ const source = program.getSourceFile(path.join(args.repoRoot, sourceFileRel));
381
+ const parsed = parseLiteralAnchor(text);
382
+ if (!source || !parsed)
383
+ return undefined;
384
+ const checker = program.getTypeChecker();
385
+ const meaning = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias;
386
+ // Names the text declares itself (`[K in ...]`, `<T>(...)`, `infer U`).
387
+ const typeParameters = new Set();
388
+ const collect = (node) => {
389
+ if (ts.isTypeParameterDeclaration(node))
390
+ typeParameters.add(node.name.text);
391
+ ts.forEachChild(node, collect);
392
+ };
393
+ collect(parsed);
394
+ const imports = new Map();
395
+ const importFor = (name) => {
396
+ if (imports.has(name))
397
+ return imports.get(name);
398
+ let found;
399
+ const atSource = checker.resolveName(name, source, meaning, false);
400
+ const target = atSource && resolveSymbolAliases(checker, atSource);
401
+ const declaringFile = target?.declarations?.[0]?.getSourceFile();
402
+ const rel = declaringFile && path.relative(args.repoRoot, declaringFile.fileName);
403
+ if (target &&
404
+ declaringFile &&
405
+ rel &&
406
+ !rel.startsWith('..') &&
407
+ !rel.split(path.sep).includes('node_modules')) {
408
+ const moduleSymbol = checker.getSymbolAtLocation(declaringFile);
409
+ const exported = moduleSymbol
410
+ ? checker
411
+ .getExportsOfModule(moduleSymbol)
412
+ .filter((candidate) => resolveSymbolAliases(checker, candidate) === target)
413
+ : [];
414
+ const chosen = exported.find((candidate) => candidate.getName() === name) ?? exported[0];
415
+ if (chosen) {
416
+ found = {
417
+ spec: entryRelativeSpecifier(args.entryDir, args.repoRoot, rel.split(path.sep).join('/')),
418
+ exportName: chosen.getName(),
419
+ };
420
+ }
421
+ }
422
+ imports.set(name, found);
423
+ return found;
424
+ };
425
+ const leftmost = (name) => ts.isIdentifier(name) ? name : leftmost(name.left);
426
+ const renameLeftmost = (name, to) => ts.isIdentifier(name)
427
+ ? ts.factory.createIdentifier(to)
428
+ : ts.factory.createQualifiedName(renameLeftmost(name.left, to), name.right);
429
+ let rewrites = 0;
430
+ const rewrite = (node) => {
431
+ if (ts.isTypeReferenceNode(node)) {
432
+ const name = leftmost(node.typeName).text;
433
+ const target = typeParameters.has(name) ? undefined : importFor(name);
434
+ if (target) {
435
+ rewrites += 1;
436
+ return ts.factory.createImportTypeNode(ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(target.spec)), undefined, renameLeftmost(node.typeName, target.exportName), node.typeArguments?.map((argument) => rewrite(argument)), false);
437
+ }
438
+ }
439
+ return ts.visitEachChild(node, rewrite, /* context */ undefined);
440
+ };
441
+ const rewritten = rewrite(parsed);
442
+ if (rewrites === 0)
443
+ return undefined;
444
+ return ts
445
+ .createPrinter({ removeComments: true })
446
+ .printNode(ts.EmitHint.Unspecified, rewritten, parsed.getSourceFile());
447
+ }
352
448
  /** The type node of `type __LiteralAnchor = <text>;`, or undefined. */
353
449
  function parseLiteralAnchor(text) {
354
450
  const parsed = ts.createSourceFile('literal-anchor.ts', `type __LiteralAnchor = ${text};`, ts.ScriptTarget.Latest, true);
@@ -839,17 +935,26 @@ function isPreferredTarget(node) {
839
935
  ts.isParameter(node) ||
840
936
  ts.isPropertyAssignment(node));
841
937
  }
938
+ /**
939
+ * The deepest node a span names: a value-space target covering it, or a TYPE
940
+ * node it covers exactly (carrick#1775). A span is the scanner naming a node,
941
+ * and a declared type is named by its type node: the property type a generated
942
+ * GraphQL document declaration states for a field sits inside a type alias,
943
+ * where no value-space node covers it. Exactness keeps a span that names no
944
+ * node from being read as whatever type happens to enclose it.
945
+ */
842
946
  function tightestCoveringNode(sourceFile, start, end) {
843
947
  let best;
844
948
  const visit = (node) => {
845
- if (node.getStart(sourceFile) <= start && node.getEnd() >= end) {
846
- if (isPreferredTarget(node))
949
+ const nodeStart = node.getStart(sourceFile);
950
+ const nodeEnd = node.getEnd();
951
+ if (nodeStart <= start && nodeEnd >= end) {
952
+ if (isPreferredTarget(node) ||
953
+ (ts.isTypeNode(node) && nodeStart === start && nodeEnd === end)) {
847
954
  best = node;
848
- node.forEachChild(visit);
849
- }
850
- else {
851
- node.forEachChild(visit);
955
+ }
852
956
  }
957
+ node.forEachChild(visit);
853
958
  };
854
959
  visit(sourceFile);
855
960
  return best;
@@ -40,6 +40,15 @@ const MAX_TYPE_TEXT = 80;
40
40
  /** Depth cap on the structural descent. Deeper differences are reported at the
41
41
  * deepest ancestor the walk reached, never dropped. */
42
42
  const MAX_FIELD_DEPTH = 4;
43
+ /**
44
+ * Members a GraphQL server adds to every object a selection asks for
45
+ * (carrick#1759). The probe reads them as optional on the consumer, which is
46
+ * the probe's reading and not the consumer's source, so the walk states no
47
+ * optionality gap for them and does not count one the producer omits as a
48
+ * field the consumer is waiting on. A type they disagree on is still named.
49
+ */
50
+ const GRAPHQL_SERVER_SUPPLIED = new Set(['__typename']);
51
+ const NONE_SERVER_SUPPLIED = new Set();
43
52
  function assignabilityOf(checker) {
44
53
  const fn = checker.isTypeAssignableTo;
45
54
  return typeof fn === 'function' ? fn.bind(checker) : undefined;
@@ -89,7 +98,7 @@ export function pairFieldReports(opened, plans) {
89
98
  // did change the type the wire form is a mapped type with real members,
90
99
  // and it stays the thing compared, because that is what the judge judged.
91
100
  const compared = wireChanges ? wire.type : declared.type;
92
- const report = diffReport(compared, expected.type, checker, isAssignableTo, expected.node);
101
+ const report = diffReport(compared, expected.type, checker, isAssignableTo, expected.node, plan.spec.protocol === 'graphql' ? GRAPHQL_SERVER_SUPPLIED : NONE_SERVER_SUPPLIED);
93
102
  report.wireApplied = wireChanges;
94
103
  results.set(plan.pairId, report);
95
104
  }
@@ -111,9 +120,9 @@ function declaredConstType(file, checker, name) {
111
120
  }
112
121
  return undefined;
113
122
  }
114
- function diffReport(sent, expected, checker, isAssignableTo, at) {
123
+ function diffReport(sent, expected, checker, isAssignableTo, at, serverSupplied) {
115
124
  const found = [];
116
- walk(sent, expected, '', 0, { checker, isAssignableTo, at, found });
125
+ walk(sent, expected, '', 0, { checker, isAssignableTo, at, found, serverSupplied });
117
126
  found.sort((a, b) => a.path === b.path ? compareText(a.nature, b.nature) : compareText(a.path, b.path));
118
127
  return {
119
128
  differences: found.slice(0, MAX_NAMED_FIELDS),
@@ -165,7 +174,11 @@ function walk(sent, expected, path, depth, ctx) {
165
174
  for (const [name, expectedProp] of expectedProps) {
166
175
  const at = join(path, name);
167
176
  const sentProp = sentProps.get(name);
177
+ const supplied = ctx.serverSupplied.has(name);
168
178
  if (!sentProp) {
179
+ // The server sends it whether or not the producer's type states it.
180
+ if (supplied)
181
+ continue;
169
182
  absent += 1;
170
183
  // An optional member the sender omits is what optional MEANS. Naming it
171
184
  // would state that the receiver requires it, which is false, and it is
@@ -180,9 +193,10 @@ function walk(sent, expected, path, depth, ctx) {
180
193
  if (sentOptional && !expectedOptional) {
181
194
  ctx.found.push({ path: at, nature: 'optional_in_sent' });
182
195
  }
183
- else if (!sentOptional && expectedOptional) {
196
+ else if (!sentOptional && expectedOptional && !supplied) {
184
197
  // No assignment error exists for this direction, which is exactly why
185
- // the judge cannot report it and this walk must.
198
+ // the judge cannot report it and this walk must. A server-supplied
199
+ // member is optional only in the probe's reading, so no gap is stated.
186
200
  ctx.found.push({ path: at, nature: 'optional_in_expected' });
187
201
  }
188
202
  const sentType = memberType(sentProp, ctx);
@@ -23,6 +23,11 @@
23
23
  * at compare time (ts_check type-checker `unwrapGraphqlPayload`, deleted in
24
24
  * WP8); the type-level port here is its v2-native equivalent.
25
25
  *
26
+ * GraphQL pairs also read the consumer's `__typename` as optional, at any depth
27
+ * (carrick#1759): the server adds that meta-field to every object a selection
28
+ * asks for, so a resolver's return type never states it, while a consumer type
29
+ * generated from a document that selects it declares it required.
30
+ *
26
31
  * Seam: node builtins + `typescript` + this bundle only. No imports needed here.
27
32
  */
28
33
  import type { CheckPairEndpoint, CheckPairSpec, ProbeProtocol, ProbeTypeKind } from './api.js';