carrick 0.3.102 → 0.3.104

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 (45) 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/api.d.ts +6 -0
  7. package/sidecar/dist/src/capture/check-classify.js +27 -0
  8. package/sidecar/dist/src/capture/check-fields.js +19 -5
  9. package/sidecar/dist/src/capture/check-probe.d.ts +6 -1
  10. package/sidecar/dist/src/capture/check-probe.js +45 -2
  11. package/sidecar/dist/src/capture/check-workspace.d.ts +3 -0
  12. package/sidecar/dist/src/capture/check-workspace.js +13 -16
  13. package/sidecar/dist/src/capture/check.js +5 -5
  14. package/sidecar/dist/src/capture/deep-walk.js +45 -12
  15. package/sidecar/dist/src/capture/deno-project.d.ts +2 -1
  16. package/sidecar/dist/src/capture/deno-project.js +22 -18
  17. package/sidecar/dist/src/capture/guarded-fs.d.ts +76 -0
  18. package/sidecar/dist/src/capture/guarded-fs.js +182 -0
  19. package/sidecar/dist/src/capture/index.d.ts +1 -0
  20. package/sidecar/dist/src/capture/index.js +65 -27
  21. package/sidecar/dist/src/capture/outside-root.d.ts +41 -0
  22. package/sidecar/dist/src/capture/outside-root.js +101 -0
  23. package/sidecar/dist/src/capture/paths-rewrite.d.ts +8 -0
  24. package/sidecar/dist/src/capture/paths-rewrite.js +9 -7
  25. package/sidecar/dist/src/capture/repair-dangling.d.ts +11 -1
  26. package/sidecar/dist/src/capture/repair-dangling.js +15 -4
  27. package/sidecar/dist/src/capture/self-check.d.ts +3 -0
  28. package/sidecar/dist/src/capture/self-check.js +3 -3
  29. package/sidecar/dist/src/capture/service-config.d.ts +19 -0
  30. package/sidecar/dist/src/capture/service-config.js +57 -0
  31. package/sidecar/dist/src/index.d.ts +1 -1
  32. package/sidecar/dist/src/index.js +4 -115
  33. package/sidecar/dist/src/origin.d.ts +49 -8
  34. package/sidecar/dist/src/origin.js +81 -8
  35. package/sidecar/dist/src/project-loader.d.ts +10 -2
  36. package/sidecar/dist/src/project-loader.js +22 -21
  37. package/sidecar/dist/src/type-inferrer.d.ts +72 -0
  38. package/sidecar/dist/src/type-inferrer.js +248 -8
  39. package/sidecar/dist/src/type-structural-expander.d.ts +5 -1
  40. package/sidecar/dist/src/type-structural-expander.js +1 -1
  41. package/sidecar/dist/src/types.d.ts +39 -178
  42. package/sidecar/dist/src/validators.d.ts +70 -914
  43. package/sidecar/dist/src/validators.js +2 -55
  44. package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
  45. 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.102",
3
+ "version": "0.3.104",
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.102",
62
- "@carrick-tools/cli-darwin-x64": "0.3.102",
63
- "@carrick-tools/cli-linux-arm64": "0.3.102",
64
- "@carrick-tools/cli-linux-x64": "0.3.102",
65
- "@carrick-tools/cli-win32-x64": "0.3.102"
61
+ "@carrick-tools/cli-darwin-arm64": "0.3.104",
62
+ "@carrick-tools/cli-darwin-x64": "0.3.104",
63
+ "@carrick-tools/cli-linux-arm64": "0.3.104",
64
+ "@carrick-tools/cli-linux-x64": "0.3.104",
65
+ "@carrick-tools/cli-win32-x64": "0.3.104"
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.102",
4
+ "version": "0.3.104",
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;
@@ -292,6 +292,12 @@ export interface CaptureStubOptions {
292
292
  /** Directory the stub package is written into (created if missing). */
293
293
  outDir: string;
294
294
  tsconfigPath?: string;
295
+ /**
296
+ * The scanned repo's root, the upper bound of the search for a tsconfig
297
+ * above `repoRoot` when none is named (carrick#1776). Without it only
298
+ * `repoRoot` is searched.
299
+ */
300
+ scanRoot?: string;
295
301
  }
296
302
  /** Wire protocol of a matched pair (drives the direction table). */
297
303
  export type ProbeProtocol = 'http' | 'graphql' | 'socket' | 'pubsub';
@@ -171,6 +171,29 @@ export function classifyPair(input) {
171
171
  ...notAFact(`the ${side} sends a form-encoded body`, side),
172
172
  };
173
173
  }
174
+ // 4c. A body of bytes (carrick#1793): a blob, a buffer or a stream has no JSON
175
+ // shape, so a MISMATCH between the containers two sides hold the bytes
176
+ // in (a `Uint8Array` sent, read with `.blob()`) is not a drift. It
177
+ // overrides a mismatch only: bytes that assign to bytes (a stream sent
178
+ // where a stream is read) agree, and that verdict stands. When both
179
+ // sides are bytes the sent side is named, whatever order the
180
+ // diagnostics came in.
181
+ const firedGates = new Set(gateDiags.map((d) => plan.gateLines.get(d.line)));
182
+ const bytesGate = firedGates.has('sent:bytes')
183
+ ? 'sent:bytes'
184
+ : firedGates.has('expected:bytes')
185
+ ? 'expected:bytes'
186
+ : undefined;
187
+ const bytesVerdict = () => {
188
+ const { side } = sideForGate(bytesGate, plan);
189
+ return {
190
+ ...base,
191
+ bucket: 'unverifiable',
192
+ gate: `${side}:bytes`,
193
+ diagnostic: `the ${side} body is bytes (a blob, a buffer or a stream), which has no JSON shape to compare with the other side.`,
194
+ ...notAFact(`the ${side} body is bytes`, side),
195
+ };
196
+ };
174
197
  // 5. Assignment-class error on the DECISIVE assignment line -> incompatible.
175
198
  //
176
199
  // On an `http` pair that line is the JSON wire assignment, not the declared
@@ -183,6 +206,8 @@ export function classifyPair(input) {
183
206
  const decisiveLine = decisiveAssignmentLine(plan);
184
207
  const assignDiag = probeDiags.find((d) => d.line === decisiveLine && ASSIGNMENT_CODES.has(d.code));
185
208
  if (assignDiag) {
209
+ if (bytesGate)
210
+ return bytesVerdict();
186
211
  const text = scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias);
187
212
  return {
188
213
  ...base,
@@ -218,6 +243,8 @@ export function classifyPair(input) {
218
243
  if (wireOther) {
219
244
  const declaredMismatch = probeDiags.find((d) => d.line === plan.assignmentLine && ASSIGNMENT_CODES.has(d.code));
220
245
  if (declaredMismatch) {
246
+ if (bytesGate)
247
+ return bytesVerdict();
221
248
  return {
222
249
  ...base,
223
250
  bucket: 'incompatible',