carrick 0.3.53

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 (114) hide show
  1. package/LICENSE.md +99 -0
  2. package/README.md +79 -0
  3. package/bin/carrick.mjs +168 -0
  4. package/dist/channel.d.ts +16 -0
  5. package/dist/channel.js +37 -0
  6. package/dist/channel.js.map +1 -0
  7. package/dist/cli.d.ts +29 -0
  8. package/dist/cli.js +60 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/contract.d.ts +154 -0
  11. package/dist/contract.js +141 -0
  12. package/dist/contract.js.map +1 -0
  13. package/dist/diagnostics.d.ts +54 -0
  14. package/dist/diagnostics.js +199 -0
  15. package/dist/diagnostics.js.map +1 -0
  16. package/dist/hook/post-edit.d.ts +13 -0
  17. package/dist/hook/post-edit.js +76 -0
  18. package/dist/hook/post-edit.js.map +1 -0
  19. package/dist/hook/session-start.d.ts +2 -0
  20. package/dist/hook/session-start.js +44 -0
  21. package/dist/hook/session-start.js.map +1 -0
  22. package/dist/init/identity.d.ts +20 -0
  23. package/dist/init/identity.js +60 -0
  24. package/dist/init/identity.js.map +1 -0
  25. package/dist/init/repos.d.ts +19 -0
  26. package/dist/init/repos.js +46 -0
  27. package/dist/init/repos.js.map +1 -0
  28. package/dist/init/run.d.ts +11 -0
  29. package/dist/init/run.js +228 -0
  30. package/dist/init/run.js.map +1 -0
  31. package/dist/init/settings.d.ts +50 -0
  32. package/dist/init/settings.js +138 -0
  33. package/dist/init/settings.js.map +1 -0
  34. package/dist/log.d.ts +2 -0
  35. package/dist/log.js +35 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/native.d.ts +48 -0
  38. package/dist/native.js +127 -0
  39. package/dist/native.js.map +1 -0
  40. package/dist/render.d.ts +65 -0
  41. package/dist/render.js +334 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/root.d.ts +30 -0
  44. package/dist/root.js +82 -0
  45. package/dist/root.js.map +1 -0
  46. package/dist/server.d.ts +2 -0
  47. package/dist/server.js +255 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/templates.d.ts +18 -0
  50. package/dist/templates.js +66 -0
  51. package/dist/templates.js.map +1 -0
  52. package/package.json +72 -0
  53. package/plugin/.claude-plugin/plugin.json +6 -0
  54. package/plugin/.lsp.json +14 -0
  55. package/plugin/hooks/hooks.json +27 -0
  56. package/sidecar/dist/src/bundler.d.ts +141 -0
  57. package/sidecar/dist/src/bundler.js +680 -0
  58. package/sidecar/dist/src/capture/anchors.d.ts +61 -0
  59. package/sidecar/dist/src/capture/anchors.js +1132 -0
  60. package/sidecar/dist/src/capture/api.d.ts +378 -0
  61. package/sidecar/dist/src/capture/api.js +10 -0
  62. package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
  63. package/sidecar/dist/src/capture/augmentations.js +60 -0
  64. package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
  65. package/sidecar/dist/src/capture/check-classify.js +189 -0
  66. package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
  67. package/sidecar/dist/src/capture/check-deep.js +91 -0
  68. package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
  69. package/sidecar/dist/src/capture/check-poison.js +155 -0
  70. package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
  71. package/sidecar/dist/src/capture/check-probe.js +147 -0
  72. package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
  73. package/sidecar/dist/src/capture/check-scrub.js +71 -0
  74. package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
  75. package/sidecar/dist/src/capture/check-workspace.js +218 -0
  76. package/sidecar/dist/src/capture/check.d.ts +39 -0
  77. package/sidecar/dist/src/capture/check.js +450 -0
  78. package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
  79. package/sidecar/dist/src/capture/deep-walk.js +243 -0
  80. package/sidecar/dist/src/capture/index.d.ts +36 -0
  81. package/sidecar/dist/src/capture/index.js +477 -0
  82. package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
  83. package/sidecar/dist/src/capture/lockfile.js +490 -0
  84. package/sidecar/dist/src/capture/machinery.d.ts +59 -0
  85. package/sidecar/dist/src/capture/machinery.js +160 -0
  86. package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
  87. package/sidecar/dist/src/capture/node-builder.js +123 -0
  88. package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
  89. package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
  90. package/sidecar/dist/src/capture/self-check.d.ts +38 -0
  91. package/sidecar/dist/src/capture/self-check.js +317 -0
  92. package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
  93. package/sidecar/dist/src/capture/specifiers.js +56 -0
  94. package/sidecar/dist/src/definition-resolver.d.ts +57 -0
  95. package/sidecar/dist/src/definition-resolver.js +153 -0
  96. package/sidecar/dist/src/index.d.ts +14 -0
  97. package/sidecar/dist/src/index.js +564 -0
  98. package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
  99. package/sidecar/dist/src/monorepo-builder.js +584 -0
  100. package/sidecar/dist/src/project-loader.d.ts +130 -0
  101. package/sidecar/dist/src/project-loader.js +399 -0
  102. package/sidecar/dist/src/type-inferrer.d.ts +940 -0
  103. package/sidecar/dist/src/type-inferrer.js +3540 -0
  104. package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
  105. package/sidecar/dist/src/type-structural-expander.js +283 -0
  106. package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
  107. package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
  108. package/sidecar/dist/src/types.d.ts +669 -0
  109. package/sidecar/dist/src/types.js +5 -0
  110. package/sidecar/dist/src/validators.d.ts +2214 -0
  111. package/sidecar/dist/src/validators.js +336 -0
  112. package/sidecar/package.json +6 -0
  113. package/templates/carrick.json +7 -0
  114. package/templates/carrick.yml +45 -0
@@ -0,0 +1,680 @@
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
13
+ */
14
+ import { Node, } from 'ts-morph';
15
+ import * as path from 'node:path';
16
+ import * as fs from 'node:fs';
17
+ import { expandTypeStructural } from './type-structural-expander.js';
18
+ /**
19
+ * #248: upper bound on `SymbolRequest.array_depth`. SDL list nesting is
20
+ * realistically 1-3 levels (`[Order!]!`, at most `[[Order!]!]!`); this is a
21
+ * generous ceiling, not a realistic value. `array_depth` wraps via string
22
+ * repetition (`'[]'.repeat(depth)`), so an unbounded value from the model
23
+ * would let a single symbol blow up the emitted `.d.ts` (and the memory/CPU
24
+ * spent producing it). Depths above this are treated as unresolvable, same
25
+ * as any other symbol the bundler can't extract a type for.
26
+ */
27
+ const MAX_ARRAY_DEPTH = 10;
28
+ /**
29
+ * SurfaceEmitter - Generates .d.ts surface files with rewritten imports
30
+ *
31
+ * Usage:
32
+ * const emitter = new SurfaceEmitter({ project, repoRoot });
33
+ * const result = emitter.emit(repoName, payloads, outputPath);
34
+ */
35
+ export class SurfaceEmitter {
36
+ project;
37
+ repoRoot;
38
+ constructor(options) {
39
+ this.project = options.project;
40
+ this.repoRoot = options.repoRoot;
41
+ }
42
+ /**
43
+ * Emit a surface .d.ts file containing payload type declarations
44
+ *
45
+ * @param repoName - Repository name for specifier rewriting
46
+ * @param payloads - Payload definitions to include
47
+ * @param outputPath - Path to write the surface file
48
+ * @returns SurfaceEmitResult
49
+ */
50
+ emit(repoName, payloads, outputPath) {
51
+ const errors = [];
52
+ const manifest = [];
53
+ try {
54
+ // Phase 1: Generate surface content as a string
55
+ const surfaceLines = [
56
+ '// Generated surface file for type compatibility checking',
57
+ '// Do not edit - this file is auto-generated by Carrick',
58
+ '',
59
+ ];
60
+ // Collect all import specifiers we need to track for rewriting
61
+ const importSpecifiers = new Set();
62
+ // Phase 2: Add payload type declarations
63
+ for (const payload of payloads) {
64
+ const { alias, type_string, source_file } = payload;
65
+ // Analyze type string for import references
66
+ const detectedImports = this.extractImportReferences(type_string);
67
+ for (const imp of detectedImports) {
68
+ importSpecifiers.add(imp);
69
+ }
70
+ // Add type alias declaration
71
+ surfaceLines.push(`export type ${alias} = ${type_string};`);
72
+ surfaceLines.push('');
73
+ manifest.push({
74
+ alias,
75
+ type_string,
76
+ rewritten_imports: [],
77
+ });
78
+ }
79
+ // Phase 3: Create a temporary source file for AST manipulation
80
+ const tempFileName = `__carrick_surface_${repoName}_${Date.now()}.d.ts`;
81
+ const tempFilePath = path.join(this.repoRoot, tempFileName);
82
+ const initialContent = surfaceLines.join('\n');
83
+ const tempSourceFile = this.project.createSourceFile(tempFilePath, initialContent, { overwrite: true });
84
+ // Phase 4: Rewrite module specifiers in the AST
85
+ const rewrittenImports = this.rewriteModuleSpecifiers(tempSourceFile, repoName);
86
+ // Update manifest with rewritten imports
87
+ for (const entry of manifest) {
88
+ entry.rewritten_imports = rewrittenImports;
89
+ }
90
+ // Phase 5: Get the transformed content
91
+ let surfaceContent = tempSourceFile.getFullText();
92
+ // Phase 6: Safety pass - catch any raw specifiers the compiler might have added
93
+ surfaceContent = this.safetyRewritePass(surfaceContent, repoName);
94
+ // Phase 7: Clean up temporary source file from project
95
+ this.project.removeSourceFile(tempSourceFile);
96
+ // Phase 8: Write output file
97
+ const absoluteOutputPath = path.isAbsolute(outputPath)
98
+ ? outputPath
99
+ : path.join(this.repoRoot, outputPath);
100
+ // Ensure directory exists
101
+ const outputDir = path.dirname(absoluteOutputPath);
102
+ if (!fs.existsSync(outputDir)) {
103
+ fs.mkdirSync(outputDir, { recursive: true });
104
+ }
105
+ fs.writeFileSync(absoluteOutputPath, surfaceContent, 'utf-8');
106
+ this.log(`Emitted surface file: ${absoluteOutputPath}`);
107
+ return {
108
+ success: true,
109
+ surface_content: surfaceContent,
110
+ output_path: absoluteOutputPath,
111
+ manifest,
112
+ };
113
+ }
114
+ catch (err) {
115
+ const error = err instanceof Error ? err.message : String(err);
116
+ this.logError(`Surface emission failed: ${error}`);
117
+ errors.push(error);
118
+ return {
119
+ success: false,
120
+ errors,
121
+ };
122
+ }
123
+ }
124
+ /**
125
+ * Extract potential import references from a type string.
126
+ * These are module specifiers that might need rewriting.
127
+ */
128
+ extractImportReferences(typeString) {
129
+ const imports = [];
130
+ // Match import("...") syntax
131
+ const importTypeRegex = /import\s*\(\s*["']([^"']+)["']\s*\)/g;
132
+ let match;
133
+ while ((match = importTypeRegex.exec(typeString)) !== null) {
134
+ imports.push(match[1]);
135
+ }
136
+ // Match from "..." syntax (in case of inline imports)
137
+ const fromRegex = /from\s+["']([^"']+)["']/g;
138
+ while ((match = fromRegex.exec(typeString)) !== null) {
139
+ imports.push(match[1]);
140
+ }
141
+ return imports;
142
+ }
143
+ /**
144
+ * Rewrite all module specifiers in a source file to use repo-scoped aliases.
145
+ *
146
+ * Targets:
147
+ * 1. ImportDeclaration.moduleSpecifier
148
+ * 2. ImportTypeNode argument (import("..."))
149
+ * 3. ExportDeclaration.moduleSpecifier
150
+ *
151
+ * @returns Array of rewritten specifiers
152
+ */
153
+ rewriteModuleSpecifiers(sourceFile, repoName) {
154
+ const rewrittenSpecifiers = [];
155
+ // 1. Rewrite ImportDeclaration specifiers
156
+ const importDecls = sourceFile.getImportDeclarations();
157
+ for (const importDecl of importDecls) {
158
+ const specifier = importDecl.getModuleSpecifierValue();
159
+ if (this.shouldRewriteSpecifier(specifier)) {
160
+ const newSpecifier = this.rewriteSpecifier(specifier, repoName);
161
+ importDecl.setModuleSpecifier(newSpecifier);
162
+ rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
163
+ }
164
+ }
165
+ // 2. Rewrite ExportDeclaration specifiers
166
+ const exportDecls = sourceFile.getExportDeclarations();
167
+ for (const exportDecl of exportDecls) {
168
+ const specifier = exportDecl.getModuleSpecifierValue();
169
+ if (specifier && this.shouldRewriteSpecifier(specifier)) {
170
+ const newSpecifier = this.rewriteSpecifier(specifier, repoName);
171
+ exportDecl.setModuleSpecifier(newSpecifier);
172
+ rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
173
+ }
174
+ }
175
+ // 3. Rewrite ImportTypeNode arguments
176
+ // These appear in type positions: import("module").Type
177
+ sourceFile.forEachDescendant((node) => {
178
+ if (Node.isImportTypeNode(node)) {
179
+ const arg = node.getArgument();
180
+ if (Node.isLiteralTypeNode(arg)) {
181
+ const literal = arg.getLiteral();
182
+ if (Node.isStringLiteral(literal)) {
183
+ const specifier = literal.getLiteralValue();
184
+ if (this.shouldRewriteSpecifier(specifier)) {
185
+ const newSpecifier = this.rewriteSpecifier(specifier, repoName);
186
+ literal.setLiteralValue(newSpecifier);
187
+ rewrittenSpecifiers.push(`${specifier} → ${newSpecifier}`);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ });
193
+ return rewrittenSpecifiers;
194
+ }
195
+ /**
196
+ * Determine if a module specifier should be rewritten.
197
+ * Only non-relative specifiers should be rewritten.
198
+ */
199
+ shouldRewriteSpecifier(specifier) {
200
+ // Don't rewrite relative imports
201
+ if (specifier.startsWith('./') || specifier.startsWith('../')) {
202
+ return false;
203
+ }
204
+ // Don't rewrite already-rewritten specifiers
205
+ if (specifier.startsWith('@carrick/')) {
206
+ return false;
207
+ }
208
+ // Don't rewrite node built-ins
209
+ if (specifier.startsWith('node:')) {
210
+ return false;
211
+ }
212
+ // Some common node built-ins without prefix
213
+ const nodeBuiltins = [
214
+ 'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https',
215
+ 'url', 'querystring', 'crypto', 'buffer', 'child_process', 'cluster',
216
+ 'dgram', 'dns', 'net', 'readline', 'tls', 'tty', 'zlib', 'assert',
217
+ 'async_hooks', 'console', 'constants', 'domain', 'inspector', 'module',
218
+ 'perf_hooks', 'process', 'punycode', 'string_decoder', 'timers',
219
+ 'trace_events', 'v8', 'vm', 'worker_threads'
220
+ ];
221
+ if (nodeBuiltins.includes(specifier) || nodeBuiltins.includes(specifier.split('/')[0])) {
222
+ return false;
223
+ }
224
+ return true;
225
+ }
226
+ /**
227
+ * Rewrite a module specifier to the repo-scoped format.
228
+ *
229
+ * Format: @carrick/{repoName}/{originalSpecifier}
230
+ *
231
+ * Preserves:
232
+ * - Scoped packages: "@scope/pkg" → "@carrick/repo-a/@scope/pkg"
233
+ * - Subpaths: "zod/lib/helpers" → "@carrick/repo-a/zod/lib/helpers"
234
+ * - Combined: "@scope/pkg/subpath" → "@carrick/repo-a/@scope/pkg/subpath"
235
+ */
236
+ rewriteSpecifier(specifier, repoName) {
237
+ return `@carrick/${repoName}/${specifier}`;
238
+ }
239
+ /**
240
+ * Safety pass: Scan emitted content for any raw specifiers that might have
241
+ * been introduced by the TypeScript compiler and rewrite them.
242
+ *
243
+ * This catches edge cases where the compiler emits types with their original
244
+ * module specifiers that weren't caught by AST traversal.
245
+ */
246
+ safetyRewritePass(content, repoName) {
247
+ // Common patterns to catch:
248
+ // - from "package"
249
+ // - import("package")
250
+ // - export * from "package"
251
+ // We need to be careful not to double-rewrite @carrick/ specifiers
252
+ // Pattern for import("...") that's not already @carrick
253
+ const importTypePattern = /import\s*\(\s*["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']\s*\)/g;
254
+ content = content.replace(importTypePattern, (match, specifier) => {
255
+ const newSpecifier = this.rewriteSpecifier(specifier, repoName);
256
+ return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
257
+ });
258
+ // Pattern for from "..." that's not already @carrick
259
+ const fromPattern = /from\s+["'](?!@carrick\/|\.\.?\/|node:)([^"']+)["']/g;
260
+ content = content.replace(fromPattern, (match, specifier) => {
261
+ const newSpecifier = this.rewriteSpecifier(specifier, repoName);
262
+ return match.replace(`"${specifier}"`, `"${newSpecifier}"`).replace(`'${specifier}'`, `'${newSpecifier}'`);
263
+ });
264
+ return content;
265
+ }
266
+ /**
267
+ * Log a message to stderr
268
+ */
269
+ log(message) {
270
+ console.error(`[sidecar:surface-emitter] ${message}`);
271
+ }
272
+ /**
273
+ * Log an error to stderr
274
+ */
275
+ logError(message) {
276
+ console.error(`[sidecar:surface-emitter:error] ${message}`);
277
+ }
278
+ }
279
+ // =============================================================================
280
+ // Legacy TypeBundler (kept for backwards compatibility during migration)
281
+ // =============================================================================
282
+ /**
283
+ * TypeBundler - Legacy bundler using simpler approach
284
+ *
285
+ * This is a simplified replacement that doesn't use dts-bundle-generator.
286
+ * It extracts type definitions directly using ts-morph.
287
+ *
288
+ * @deprecated Use SurfaceEmitter instead for the new architecture
289
+ */
290
+ export class TypeBundler {
291
+ project;
292
+ repoRoot;
293
+ constructor(options) {
294
+ this.project = options.project;
295
+ this.repoRoot = options.repoRoot;
296
+ }
297
+ /**
298
+ * Bundle the requested symbols into type definitions
299
+ *
300
+ * @param symbols - Array of symbol requests to bundle
301
+ * @returns BundleResult with the bundled content or errors
302
+ */
303
+ bundle(symbols) {
304
+ const manifest = [];
305
+ const symbolFailures = [];
306
+ const errors = [];
307
+ // Dedupe symbols
308
+ const uniqueSymbols = this.dedupeSymbols(symbols);
309
+ // Validate all symbols exist
310
+ const validatedSymbols = this.validateSymbols(uniqueSymbols, symbolFailures);
311
+ if (validatedSymbols.length === 0) {
312
+ return {
313
+ success: false,
314
+ symbol_failures: symbolFailures,
315
+ errors: symbolFailures.length > 0
316
+ ? ['All requested symbols failed to resolve']
317
+ : ['No symbols provided'],
318
+ };
319
+ }
320
+ try {
321
+ // Generate type definitions directly
322
+ const dtsLines = [
323
+ '// Generated by Carrick TypeBundler',
324
+ '// This file contains extracted type definitions',
325
+ '',
326
+ ];
327
+ for (const symbol of validatedSymbols) {
328
+ const typeInfo = this.extractTypeDefinition(symbol);
329
+ if (typeInfo) {
330
+ dtsLines.push(typeInfo.definition);
331
+ dtsLines.push('');
332
+ manifest.push({
333
+ alias: symbol.alias || symbol.symbol_name,
334
+ original_name: symbol.symbol_name,
335
+ source_file: symbol.source_file,
336
+ type_string: typeInfo.typeString,
337
+ is_explicit: true,
338
+ });
339
+ }
340
+ else {
341
+ symbolFailures.push({
342
+ symbol_name: symbol.symbol_name,
343
+ source_file: symbol.source_file,
344
+ reason: `Could not extract type definition for '${symbol.symbol_name}'`,
345
+ });
346
+ }
347
+ }
348
+ const dtsContent = dtsLines.join('\n');
349
+ return {
350
+ success: true,
351
+ dts_content: dtsContent,
352
+ manifest,
353
+ symbol_failures: symbolFailures.length > 0 ? symbolFailures : undefined,
354
+ };
355
+ }
356
+ catch (err) {
357
+ const error = err instanceof Error ? err.message : String(err);
358
+ this.logError(`Bundle generation failed: ${error}`);
359
+ errors.push(error);
360
+ return {
361
+ success: false,
362
+ symbol_failures: symbolFailures.length > 0 ? symbolFailures : undefined,
363
+ errors,
364
+ };
365
+ }
366
+ }
367
+ dedupeSymbols(symbols) {
368
+ const seen = new Set();
369
+ const unique = [];
370
+ for (const symbol of symbols) {
371
+ const key = `${symbol.source_file}::${symbol.symbol_name}::${symbol.alias || ''}`;
372
+ if (seen.has(key)) {
373
+ continue;
374
+ }
375
+ seen.add(key);
376
+ unique.push(symbol);
377
+ }
378
+ return unique;
379
+ }
380
+ validateSymbols(symbols, failures) {
381
+ const validated = [];
382
+ for (const symbol of symbols) {
383
+ const result = this.validateSymbol(symbol);
384
+ if (result.valid) {
385
+ validated.push(symbol);
386
+ }
387
+ else {
388
+ failures.push({
389
+ symbol_name: symbol.symbol_name,
390
+ source_file: symbol.source_file,
391
+ reason: result.reason || 'Unknown error',
392
+ });
393
+ }
394
+ }
395
+ return validated;
396
+ }
397
+ validateSymbol(symbol) {
398
+ if (this.isPlaceholderSource(symbol.source_file)) {
399
+ return {
400
+ valid: false,
401
+ reason: `Unsupported source placeholder: ${symbol.source_file}`,
402
+ };
403
+ }
404
+ if (!this.isPathSource(symbol.source_file)) {
405
+ return { valid: true };
406
+ }
407
+ // Resolve the source file path
408
+ const absolutePath = path.isAbsolute(symbol.source_file)
409
+ ? symbol.source_file
410
+ : path.resolve(this.repoRoot, symbol.source_file);
411
+ // Check if file exists
412
+ if (!fs.existsSync(absolutePath)) {
413
+ return {
414
+ valid: false,
415
+ reason: `Source file not found: ${symbol.source_file}`,
416
+ };
417
+ }
418
+ // Get or add the source file to the project
419
+ let sourceFile;
420
+ try {
421
+ sourceFile =
422
+ this.project.getSourceFile(absolutePath) ||
423
+ this.project.addSourceFileAtPath(absolutePath);
424
+ }
425
+ catch (err) {
426
+ return {
427
+ valid: false,
428
+ reason: `Failed to load source file: ${err instanceof Error ? err.message : String(err)}`,
429
+ };
430
+ }
431
+ // Look for the symbol in the file
432
+ const symbolName = symbol.symbol_name;
433
+ // Check interfaces
434
+ if (sourceFile.getInterface(symbolName))
435
+ return { valid: true };
436
+ // Check type aliases
437
+ if (sourceFile.getTypeAlias(symbolName))
438
+ return { valid: true };
439
+ // Check classes
440
+ if (sourceFile.getClass(symbolName))
441
+ return { valid: true };
442
+ // Check enums
443
+ if (sourceFile.getEnum(symbolName))
444
+ return { valid: true };
445
+ // Check exported variables
446
+ if (sourceFile.getVariableDeclaration(symbolName))
447
+ return { valid: true };
448
+ // Check functions
449
+ if (sourceFile.getFunction(symbolName))
450
+ return { valid: true };
451
+ // Symbol not found
452
+ return {
453
+ valid: false,
454
+ reason: `Symbol '${symbolName}' not found in ${symbol.source_file}`,
455
+ };
456
+ }
457
+ /**
458
+ * Structural body (`{ ... }`) for an interface/object type, with every nested
459
+ * named member inlined recursively. Returns null when the expansion is not a
460
+ * usable object body (e.g. the expander fell back to a bare name), so the
461
+ * caller can keep the verbatim declaration instead of emitting an invalid
462
+ * interface. The expander already guards recursion (depth + per-branch cycle
463
+ * set) and keeps builtins/`node_modules` types by name.
464
+ */
465
+ expandObjectBody(type) {
466
+ try {
467
+ const expanded = expandTypeStructural(type);
468
+ return expanded.startsWith('{') ? expanded : null;
469
+ }
470
+ catch {
471
+ return null;
472
+ }
473
+ }
474
+ extractTypeDefinition(symbol) {
475
+ const base = this.extractTypeDefinitionBase(symbol);
476
+ if (!base)
477
+ return null;
478
+ // #248: wrap the resolved element type in the SDL list depth. A GraphQL
479
+ // producer field `[Order!]!` backed by `interface Order` bundles `Order`
480
+ // with `array_depth: 1`; the alias becomes `Order[]` (arrays can't be
481
+ // interfaces, so it is always emitted as a type alias). The element's
482
+ // structurally-expanded body carries the shape, so downstream resolution
483
+ // and the eval's whitespace-collapsed `{...}[]` comparison both hold.
484
+ const depth = symbol.array_depth ?? 0;
485
+ if (depth <= 0)
486
+ return base;
487
+ // Reject rather than clamp: silently clamping would emit a type at a
488
+ // depth the model never asked for, which is wrong in a different way.
489
+ // Falling through to the symbol's normal "could not extract" failure
490
+ // keeps this on the existing per-symbol failure path (`symbol_failures`)
491
+ // instead of rejecting the whole batch.
492
+ if (depth > MAX_ARRAY_DEPTH)
493
+ return null;
494
+ // The wrap suffixes `typeString` with `[]`, which is only valid when it is
495
+ // a standalone type EXPRESSION. The base's fallback branches (interface
496
+ // whose expansion failed, class/enum/function) return declaration text,
497
+ // and `export type A = enum X {...}[]` is not parseable .d.ts — fail the
498
+ // symbol instead, same per-symbol path as an over-depth request.
499
+ if (!base.typeStringIsExpression)
500
+ return null;
501
+ const alias = symbol.alias || symbol.symbol_name;
502
+ // Parenthesise a union/intersection/function element so `(A | B)[]` doesn't
503
+ // misparse as `A | B[]` and `(() => T)[]` as `() => T[]`. Decide from the
504
+ // TYPE (not the string): a single object literal `{ a: A | B }` is NOT a
505
+ // top-level union and must stay unparenthesised (`{ a: A | B }[]`).
506
+ const element = this.elementNeedsArrayParens(symbol)
507
+ ? `(${base.typeString})`
508
+ : base.typeString;
509
+ const typeString = `${element}${'[]'.repeat(depth)}`;
510
+ return { definition: `export type ${alias} = ${typeString};`, typeString };
511
+ }
512
+ /// Whether the element type must be parenthesised before an `[]` suffix.
513
+ /// Interfaces/classes/enums are always object/nominal (never a top-level
514
+ /// union), so only type aliases and typed variables can carry a bare
515
+ /// union/intersection/function that would misparse; those are re-resolved
516
+ /// from the source Type here (this runs only on the rare array_depth path).
517
+ elementNeedsArrayParens(symbol) {
518
+ const absolutePath = path.isAbsolute(symbol.source_file)
519
+ ? symbol.source_file
520
+ : path.resolve(this.repoRoot, symbol.source_file);
521
+ const sourceFile = this.project.getSourceFile(absolutePath);
522
+ if (!sourceFile)
523
+ return false;
524
+ const name = symbol.symbol_name;
525
+ const decl = sourceFile.getTypeAlias(name) ?? sourceFile.getVariableDeclaration(name);
526
+ if (!decl)
527
+ return false;
528
+ const t = decl.getType();
529
+ return (t.isUnion() ||
530
+ t.isIntersection() ||
531
+ t.getCallSignatures().length > 0 ||
532
+ t.getConstructSignatures().length > 0);
533
+ }
534
+ // `typeStringIsExpression`: whether `typeString` is a standalone type
535
+ // expression (an object body, alias RHS, or `Type#getText()`), as opposed to
536
+ // verbatim declaration text from a fallback branch. Only expressions can be
537
+ // array-wrapped by `extractTypeDefinition`.
538
+ extractTypeDefinitionBase(symbol) {
539
+ const absolutePath = path.isAbsolute(symbol.source_file)
540
+ ? symbol.source_file
541
+ : path.resolve(this.repoRoot, symbol.source_file);
542
+ const sourceFile = this.project.getSourceFile(absolutePath);
543
+ if (!sourceFile) {
544
+ return null;
545
+ }
546
+ const symbolName = symbol.symbol_name;
547
+ const alias = symbol.alias || symbolName;
548
+ // Try interface
549
+ const iface = sourceFile.getInterface(symbolName);
550
+ if (iface) {
551
+ // Inline nested named members structurally (`total: { amountCents: number;
552
+ // currency: string; }`, not `total: MoneyView`). The bundle carries only
553
+ // the alias declaration — never the source decls of nested types — so a
554
+ // bare named reference is dangling and resolves to `any` downstream. The
555
+ // expansion happens here, in the real source project, where the nested
556
+ // symbol still resolves. Falls back to the verbatim rename if the type
557
+ // can't be safely rendered as an object body.
558
+ const expanded = this.expandObjectBody(iface.getType());
559
+ const text = iface.getText();
560
+ if (expanded) {
561
+ const definition = `export interface ${alias} ${expanded}`;
562
+ return { definition, typeString: expanded, typeStringIsExpression: true };
563
+ }
564
+ const typeString = text;
565
+ const alreadyExported = text.trimStart().startsWith('export');
566
+ const definition = alias !== symbolName
567
+ ? (alreadyExported
568
+ ? text.replace(`interface ${symbolName}`, `interface ${alias}`)
569
+ : text.replace(`interface ${symbolName}`, `export interface ${alias}`))
570
+ : alreadyExported
571
+ ? text
572
+ : `export ${text}`;
573
+ return { definition, typeString, typeStringIsExpression: false };
574
+ }
575
+ // Try type alias
576
+ const typeAlias = sourceFile.getTypeAlias(symbolName);
577
+ if (typeAlias) {
578
+ // Same nested-member inlining as the interface branch above. A type alias
579
+ // can resolve to any shape (object, union, array, primitive); the
580
+ // structural expander renders each correctly and keeps builtins/library
581
+ // types by name, so the alias RHS is always emittable.
582
+ const text = typeAlias.getText();
583
+ const expanded = expandTypeStructural(typeAlias.getType());
584
+ if (expanded && expanded !== 'unknown') {
585
+ const definition = `export type ${alias} = ${expanded};`;
586
+ return { definition, typeString: expanded, typeStringIsExpression: true };
587
+ }
588
+ const typeString = typeAlias.getType().getText();
589
+ const alreadyExported = text.trimStart().startsWith('export');
590
+ const definition = alias !== symbolName
591
+ ? (alreadyExported
592
+ ? text.replace(`type ${symbolName}`, `type ${alias}`)
593
+ : text.replace(`type ${symbolName}`, `export type ${alias}`))
594
+ : alreadyExported
595
+ ? text
596
+ : `export ${text}`;
597
+ return { definition, typeString, typeStringIsExpression: true };
598
+ }
599
+ // Try class
600
+ const classDecl = sourceFile.getClass(symbolName);
601
+ if (classDecl) {
602
+ const text = classDecl.getText();
603
+ const typeString = text;
604
+ const alreadyExported = text.trimStart().startsWith('export');
605
+ const definition = alias !== symbolName
606
+ ? (alreadyExported
607
+ ? text.replace(`class ${symbolName}`, `declare class ${alias}`)
608
+ : text.replace(`class ${symbolName}`, `export declare class ${alias}`))
609
+ : alreadyExported
610
+ ? text
611
+ : `export declare ${text}`;
612
+ return { definition, typeString, typeStringIsExpression: false };
613
+ }
614
+ // Try enum
615
+ const enumDecl = sourceFile.getEnum(symbolName);
616
+ if (enumDecl) {
617
+ const text = enumDecl.getText();
618
+ const typeString = text;
619
+ const alreadyExported = text.trimStart().startsWith('export');
620
+ const definition = alias !== symbolName
621
+ ? (alreadyExported
622
+ ? text.replace(`enum ${symbolName}`, `enum ${alias}`)
623
+ : text.replace(`enum ${symbolName}`, `export enum ${alias}`))
624
+ : alreadyExported
625
+ ? text
626
+ : `export ${text}`;
627
+ return { definition, typeString, typeStringIsExpression: false };
628
+ }
629
+ // Try variable (const type definitions)
630
+ const varDecl = sourceFile.getVariableDeclaration(symbolName);
631
+ if (varDecl) {
632
+ const type = varDecl.getType();
633
+ const typeString = type.getText();
634
+ const definition = `export type ${alias} = ${typeString};`;
635
+ return { definition, typeString, typeStringIsExpression: true };
636
+ }
637
+ // Try function
638
+ const funcDecl = sourceFile.getFunction(symbolName);
639
+ if (funcDecl) {
640
+ const text = funcDecl.getText();
641
+ const typeString = text;
642
+ const alreadyExported = text.trimStart().startsWith('export');
643
+ const definition = alias !== symbolName
644
+ ? (alreadyExported
645
+ ? text.replace(`function ${symbolName}`, `declare function ${alias}`)
646
+ : text.replace(`function ${symbolName}`, `export declare function ${alias}`))
647
+ : alreadyExported
648
+ ? text
649
+ : `export declare ${text}`;
650
+ return { definition, typeString, typeStringIsExpression: false };
651
+ }
652
+ return null;
653
+ }
654
+ isPathSource(sourceFile) {
655
+ if (sourceFile.startsWith('.') ||
656
+ sourceFile.startsWith('/') ||
657
+ path.isAbsolute(sourceFile)) {
658
+ return true;
659
+ }
660
+ const basePath = path.resolve(this.repoRoot, sourceFile);
661
+ if (fs.existsSync(basePath)) {
662
+ return true;
663
+ }
664
+ if (path.extname(sourceFile) !== '') {
665
+ return false;
666
+ }
667
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.d.ts'];
668
+ return extensions.some((ext) => fs.existsSync(basePath + ext));
669
+ }
670
+ isPlaceholderSource(sourceFile) {
671
+ const trimmed = sourceFile.trim();
672
+ return trimmed === '' || trimmed === '%none%' || trimmed === '%inline%';
673
+ }
674
+ log(message) {
675
+ console.error(`[sidecar:bundler] ${message}`);
676
+ }
677
+ logError(message) {
678
+ console.error(`[sidecar:bundler:error] ${message}`);
679
+ }
680
+ }