@withtruing/companion 0.0.0-stage → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,644 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { lstat, readFile } from 'node:fs/promises';
4
+ import { extname, posix, relative, resolve, sep } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { builtinModules } from 'node:module';
7
+ import ts from 'typescript';
8
+ import { hasLinkedPathComponent, isTrackedRegularFileMode, safeSelectedPath } from './investigation-reader.js';
9
+ const execFileAsync = promisify(execFile);
10
+ const SCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
11
+ const JS_TO_TS = {
12
+ '.js': ['.ts'],
13
+ '.jsx': ['.tsx'],
14
+ '.mjs': ['.mts'],
15
+ '.cjs': ['.cts'],
16
+ };
17
+ const BLOCKING_V1 = new Set([
18
+ 'unresolved_internal',
19
+ 'unresolved_dynamic',
20
+ 'declaration_only',
21
+ 'parse_failed',
22
+ 'unsupported_resolution',
23
+ 'unsupported_exports',
24
+ 'unsupported_workspace_config',
25
+ 'unsafe_path',
26
+ 'named_path_unverifiable',
27
+ 'limit_depth',
28
+ 'limit_files',
29
+ 'limit_bytes',
30
+ ]);
31
+ /** Under @2, `limit_bytes` is a deliberate trim (nearest files kept), not a block. */
32
+ const BLOCKING_V2 = new Set([...BLOCKING_V1].filter((code) => code !== 'limit_bytes'));
33
+ /** Every mapper version this companion can produce; advertised as capabilities. */
34
+ export const SUPPORTED_MAPPER_VERSIONS = [
35
+ 'auto-surface-mapper@1',
36
+ 'auto-surface-mapper@2',
37
+ ];
38
+ export function isSupportedMapperVersion(value) {
39
+ return SUPPORTED_MAPPER_VERSIONS.includes(value);
40
+ }
41
+ export class MappingFailure extends Error {
42
+ kind;
43
+ code;
44
+ constructor(kind, code, detail) {
45
+ super(detail);
46
+ this.kind = kind;
47
+ this.code = code;
48
+ }
49
+ }
50
+ class WorkspaceInputError extends Error {
51
+ code;
52
+ constructor(code, detail) {
53
+ super(detail);
54
+ this.code = code;
55
+ }
56
+ }
57
+ export const DEFAULT_MAPPER_SETTINGS = {
58
+ maxDepth: 8,
59
+ maxFiles: 120,
60
+ maxSourceBytes: 1_048_576,
61
+ conditions: ['import', 'require', 'node', 'default'],
62
+ extensionOrder: [
63
+ 'exact', '.js->.ts', '.jsx->.tsx', '.mjs->.mts', '.cjs->.cts',
64
+ '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json',
65
+ '/index.ts', '/index.tsx', '/index.js',
66
+ ],
67
+ };
68
+ const hash = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
69
+ const compareDiagnostic = (a, b) => a.code.localeCompare(b.code) || a.path.localeCompare(b.path) || a.detail.localeCompare(b.detail);
70
+ const repoPath = (value) => value.replaceAll('\\', '/').replace(/^\.\//, '');
71
+ function isSafeRepoPath(value) {
72
+ if (!value || value.startsWith('/') || value.includes('\\') || /^[A-Za-z]:/.test(value))
73
+ return false;
74
+ const segments = value.split('/');
75
+ return !segments.some((segment) => !segment || segment === '..' || segment === '.');
76
+ }
77
+ export async function enumerateTrackedFiles(sandboxPath, commit) {
78
+ let stdout;
79
+ try {
80
+ ({ stdout } = await execFileAsync('git', ['ls-tree', '-r', '-z', '--full-tree', commit], { cwd: sandboxPath, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, windowsHide: true }));
81
+ }
82
+ catch (error) {
83
+ throw new MappingFailure('transient', 'enumeration_failed', error instanceof Error ? error.message : String(error));
84
+ }
85
+ const entries = new Map();
86
+ for (const record of stdout.split('\0').filter(Boolean)) {
87
+ const tab = record.indexOf('\t');
88
+ if (tab < 0)
89
+ throw new MappingFailure('transient', 'enumeration_failed', 'Malformed git ls-tree output');
90
+ const [mode, type, object] = record.slice(0, tab).split(' ');
91
+ const path = record.slice(tab + 1);
92
+ entries.set(path, { path, mode, type, object });
93
+ }
94
+ return { entries, complete: true };
95
+ }
96
+ function regular(tree, path) {
97
+ const entry = tree.entries.get(path);
98
+ return !!entry && isTrackedRegularFileMode(entry.mode, entry.type);
99
+ }
100
+ async function safeRead(root, tree, path) {
101
+ if (!isSafeRepoPath(path) || !regular(tree, path))
102
+ throw new Error(`not a tracked regular file: ${path}`);
103
+ const absolute = safeSelectedPath(root, path);
104
+ if (await hasLinkedPathComponent(root, path))
105
+ throw new Error(`symlinked path component: ${path}`);
106
+ const stat = await lstat(absolute);
107
+ if (!stat.isFile())
108
+ throw new Error(`not an ordinary file: ${path}`);
109
+ return readFile(absolute);
110
+ }
111
+ function parseWorkspaceGlobs(text) {
112
+ const globs = [];
113
+ const unsupported = [];
114
+ let inPackages = false;
115
+ for (const raw of text.split(/\r?\n/)) {
116
+ const line = raw.replace(/\s+#.*$/, '');
117
+ if (/^packages\s*:\s*$/.test(line.trim())) {
118
+ inPackages = true;
119
+ continue;
120
+ }
121
+ if (/^packages\s*:/.test(line.trim())) {
122
+ unsupported.push(line.trim());
123
+ inPackages = false;
124
+ continue;
125
+ }
126
+ if (inPackages && /^\S/.test(line) && !/^\s*-/.test(line))
127
+ break;
128
+ if (!inPackages)
129
+ continue;
130
+ const match = line.match(/^\s*-\s*['"]?([^'"\s]+)['"]?\s*$/);
131
+ if (!match) {
132
+ if (/^\s*-/.test(line))
133
+ unsupported.push(line.trim());
134
+ continue;
135
+ }
136
+ if (/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*(?:\/\*)?$/.test(match[1]))
137
+ globs.push(match[1]);
138
+ else
139
+ unsupported.push(match[1]);
140
+ }
141
+ return { globs, unsupported };
142
+ }
143
+ function workspaceDirsForGlob(glob, tree) {
144
+ if (!glob.endsWith('/*'))
145
+ return [glob];
146
+ const parent = glob.slice(0, -2);
147
+ const dirs = new Set();
148
+ for (const path of tree.entries.keys()) {
149
+ if (!path.startsWith(`${parent}/`))
150
+ continue;
151
+ const rest = path.slice(parent.length + 1);
152
+ const first = rest.split('/')[0];
153
+ if (first)
154
+ dirs.add(`${parent}/${first}`);
155
+ }
156
+ return [...dirs].sort();
157
+ }
158
+ async function readJsonInput(root, tree, path) {
159
+ if (!tree.entries.has(path))
160
+ return null;
161
+ if (!regular(tree, path))
162
+ throw new WorkspaceInputError('unsafe_path', 'not a tracked regular file');
163
+ let bytes;
164
+ try {
165
+ bytes = await safeRead(root, tree, path);
166
+ }
167
+ catch (error) {
168
+ throw new WorkspaceInputError('unsafe_path', error instanceof Error ? error.message : String(error));
169
+ }
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(bytes.toString('utf8'));
173
+ }
174
+ catch (error) {
175
+ throw new WorkspaceInputError('unsupported_workspace_config', error instanceof Error ? error.message : String(error));
176
+ }
177
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
178
+ throw new WorkspaceInputError('unsupported_workspace_config', 'expected a JSON object');
179
+ }
180
+ return { json: parsed, sha256: hash(bytes) };
181
+ }
182
+ async function checkedJsonInput(root, tree, path, diagnostics) {
183
+ try {
184
+ return await readJsonInput(root, tree, path);
185
+ }
186
+ catch (error) {
187
+ const checked = error instanceof WorkspaceInputError
188
+ ? error
189
+ : new WorkspaceInputError('unsafe_path', error instanceof Error ? error.message : String(error));
190
+ diagnostics.push({ code: checked.code, path, detail: checked.message });
191
+ return null;
192
+ }
193
+ }
194
+ async function discoverWorkspace(root, tree) {
195
+ const diagnostics = [];
196
+ const resolutionInputs = [];
197
+ const globs = [];
198
+ const rootRead = await checkedJsonInput(root, tree, 'package.json', diagnostics);
199
+ const rootPackage = rootRead
200
+ ? { dir: '', path: 'package.json', json: rootRead.json, name: typeof rootRead.json.name === 'string' ? rootRead.json.name : undefined }
201
+ : null;
202
+ if (rootRead) {
203
+ resolutionInputs.push({ path: 'package.json', sha256: rootRead.sha256 });
204
+ const workspaces = rootRead.json.workspaces;
205
+ if (Array.isArray(workspaces)) {
206
+ for (const value of workspaces) {
207
+ if (typeof value === 'string' && /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*(?:\/\*)?$/.test(value))
208
+ globs.push(value);
209
+ else
210
+ diagnostics.push({ code: 'unsupported_workspace_config', path: 'package.json', detail: String(value) });
211
+ }
212
+ }
213
+ else if (workspaces !== undefined) {
214
+ diagnostics.push({ code: 'unsupported_workspace_config', path: 'package.json', detail: 'workspaces must be an array' });
215
+ }
216
+ }
217
+ if (tree.entries.has('pnpm-workspace.yaml')) {
218
+ if (!regular(tree, 'pnpm-workspace.yaml')) {
219
+ diagnostics.push({ code: 'unsafe_path', path: 'pnpm-workspace.yaml', detail: 'not a tracked regular file' });
220
+ }
221
+ else {
222
+ try {
223
+ const bytes = await safeRead(root, tree, 'pnpm-workspace.yaml');
224
+ resolutionInputs.push({ path: 'pnpm-workspace.yaml', sha256: hash(bytes) });
225
+ const parsed = parseWorkspaceGlobs(bytes.toString('utf8'));
226
+ globs.push(...parsed.globs);
227
+ for (const unsupported of parsed.unsupported) {
228
+ diagnostics.push({ code: 'unsupported_workspace_config', path: 'pnpm-workspace.yaml', detail: unsupported });
229
+ }
230
+ }
231
+ catch (error) {
232
+ diagnostics.push({
233
+ code: 'unsafe_path',
234
+ path: 'pnpm-workspace.yaml',
235
+ detail: error instanceof Error ? error.message : String(error),
236
+ });
237
+ }
238
+ }
239
+ }
240
+ const packageDirs = new Set(globs.flatMap((glob) => workspaceDirsForGlob(glob, tree)));
241
+ const packages = new Map();
242
+ const packageByDir = [];
243
+ for (const dir of [...packageDirs].sort()) {
244
+ const path = `${dir}/package.json`;
245
+ const read = await checkedJsonInput(root, tree, path, diagnostics);
246
+ if (!read)
247
+ continue;
248
+ resolutionInputs.push({ path, sha256: read.sha256 });
249
+ const info = {
250
+ dir,
251
+ path,
252
+ json: read.json,
253
+ name: typeof read.json.name === 'string' ? read.json.name : undefined,
254
+ };
255
+ packageByDir.push(info);
256
+ if (info.name)
257
+ packages.set(info.name, info);
258
+ }
259
+ packageByDir.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
260
+ const packageScopes = new Set([...packages.keys()]
261
+ .filter((name) => name.startsWith('@'))
262
+ .map((name) => name.split('/')[0]));
263
+ return {
264
+ packages,
265
+ packageScopes,
266
+ packageByDir,
267
+ rootPackage,
268
+ workspaceDirs: [...packageDirs].sort((a, b) => b.length - a.length || a.localeCompare(b)),
269
+ resolutionInputs: resolutionInputs.sort((a, b) => a.path.localeCompare(b.path)),
270
+ diagnostics,
271
+ };
272
+ }
273
+ function scriptKind(path) {
274
+ const extension = extname(path);
275
+ if (extension === '.tsx')
276
+ return ts.ScriptKind.TSX;
277
+ if (extension === '.jsx')
278
+ return ts.ScriptKind.JSX;
279
+ if (extension === '.js' || extension === '.mjs' || extension === '.cjs')
280
+ return ts.ScriptKind.JS;
281
+ if (extension === '.json')
282
+ return ts.ScriptKind.JSON;
283
+ return ts.ScriptKind.TS;
284
+ }
285
+ export function parseImportEdges(path, source) {
286
+ const file = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, scriptKind(path));
287
+ const parseFailed = (file.parseDiagnostics?.length ?? 0) > 0;
288
+ const edges = [];
289
+ const literal = (node) => ts.isStringLiteralLike(node) ? node.text : null;
290
+ const add = (node, specifier, edge, detail) => {
291
+ edges.push({ position: node.getStart(file), specifier, edge, ...(detail ? { detail } : {}) });
292
+ };
293
+ const visit = (node) => {
294
+ if (ts.isImportDeclaration(node)) {
295
+ add(node, literal(node.moduleSpecifier), node.importClause ? 'import' : 'side_effect');
296
+ }
297
+ else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
298
+ add(node, literal(node.moduleSpecifier), 'reexport');
299
+ }
300
+ else if (ts.isImportEqualsDeclaration(node)
301
+ && ts.isExternalModuleReference(node.moduleReference)
302
+ && node.moduleReference.expression) {
303
+ add(node, literal(node.moduleReference.expression), 'require');
304
+ }
305
+ else if (ts.isCallExpression(node)) {
306
+ const dynamic = node.expression.kind === ts.SyntaxKind.ImportKeyword;
307
+ const required = ts.isIdentifier(node.expression) && node.expression.text === 'require';
308
+ if (dynamic || required) {
309
+ const specifier = node.arguments.length === 1 ? literal(node.arguments[0]) : null;
310
+ add(node, specifier, dynamic ? 'dynamic' : 'require', specifier === null ? node.getText(file).slice(0, 160) : undefined);
311
+ }
312
+ }
313
+ ts.forEachChild(node, visit);
314
+ };
315
+ visit(file);
316
+ return {
317
+ parseFailed,
318
+ edges: edges.sort((a, b) => a.position - b.position || (a.specifier ?? '').localeCompare(b.specifier ?? '')),
319
+ };
320
+ }
321
+ function relativeCandidates(importer, specifier) {
322
+ const base = repoPath(posix.normalize(posix.join(posix.dirname(importer), specifier)));
323
+ const extension = extname(base);
324
+ const candidates = [];
325
+ if (JS_TO_TS[extension]) {
326
+ const stem = base.slice(0, -extension.length);
327
+ candidates.push(...JS_TO_TS[extension].map((candidate) => `${stem}${candidate}`), base);
328
+ }
329
+ else if (extension) {
330
+ candidates.push(base);
331
+ }
332
+ candidates.push(`${base}.ts`, `${base}.tsx`, `${base}.mts`, `${base}.cts`, `${base}.js`, `${base}.jsx`, `${base}.mjs`, `${base}.cjs`, `${base}.json`, `${base}/index.ts`, `${base}/index.tsx`, `${base}/index.js`);
333
+ return [...new Set(candidates)].filter(isSafeRepoPath);
334
+ }
335
+ function firstResolution(tree, candidates) {
336
+ for (const path of candidates) {
337
+ const entry = tree.entries.get(path);
338
+ if (entry && !isTrackedRegularFileMode(entry.mode, entry.type))
339
+ return { unsafe: path };
340
+ if (entry && !path.endsWith('.d.ts'))
341
+ return { path };
342
+ }
343
+ const declaration = candidates.map((candidate) => candidate.endsWith('.d.ts') ? candidate : `${candidate.replace(/\.[^.\/]+$/, '')}.d.ts`)
344
+ .find((candidate) => regular(tree, candidate));
345
+ return declaration ? { declarationOnly: declaration } : {};
346
+ }
347
+ function packageNameAndSubpath(specifier) {
348
+ const parts = specifier.split('/');
349
+ const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
350
+ return { name, subpath: parts.slice(specifier.startsWith('@') ? 2 : 1).join('/') };
351
+ }
352
+ function chooseConditional(value, conditions) {
353
+ if (typeof value === 'string')
354
+ return { target: value };
355
+ if (!value || typeof value !== 'object' || Array.isArray(value))
356
+ return { unsupported: true };
357
+ const record = value;
358
+ for (const condition of conditions) {
359
+ if (condition in record)
360
+ return chooseConditional(record[condition], conditions);
361
+ }
362
+ if ('types' in record)
363
+ return { typesOnly: true };
364
+ return { unsupported: true };
365
+ }
366
+ function exportTarget(exportsValue, subpath, conditions) {
367
+ if (typeof exportsValue === 'string')
368
+ return subpath ? { unsupported: true } : { target: exportsValue };
369
+ if (!exportsValue || typeof exportsValue !== 'object' || Array.isArray(exportsValue))
370
+ return { unsupported: true };
371
+ const record = exportsValue;
372
+ const keys = Object.keys(record);
373
+ if (!keys.some((key) => key.startsWith('.')))
374
+ return subpath ? { unsupported: true } : chooseConditional(record, conditions);
375
+ const requested = subpath ? `./${subpath}` : '.';
376
+ if (requested in record)
377
+ return chooseConditional(record[requested], conditions);
378
+ for (const key of keys.sort()) {
379
+ const stars = [...key.matchAll(/\*/g)].length;
380
+ if (stars > 1 || (key.includes('*') && stars !== 1))
381
+ return { unsupported: true };
382
+ if (stars !== 1)
383
+ continue;
384
+ const [prefix, suffix] = key.split('*');
385
+ if (!requested.startsWith(prefix) || !requested.endsWith(suffix))
386
+ continue;
387
+ const capture = requested.slice(prefix.length, requested.length - suffix.length);
388
+ const selected = chooseConditional(record[key], conditions);
389
+ if (selected.target) {
390
+ if ([...selected.target.matchAll(/\*/g)].length !== 1)
391
+ return { unsupported: true };
392
+ selected.target = selected.target.replace('*', capture);
393
+ }
394
+ return selected;
395
+ }
396
+ return { unsupported: true };
397
+ }
398
+ function declaredDependencies(info) {
399
+ const names = new Set();
400
+ if (!info)
401
+ return names;
402
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
403
+ const value = info.json[field];
404
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
405
+ for (const name of Object.keys(value))
406
+ names.add(name);
407
+ }
408
+ }
409
+ return names;
410
+ }
411
+ function nearestPackage(importer, workspace) {
412
+ return workspace.packageByDir.find((candidate) => importer.startsWith(`${candidate.dir}/`))
413
+ ?? workspace.rootPackage;
414
+ }
415
+ function packageTargetCandidates(dir, target) {
416
+ const cleaned = target.replace(/^\.\//, '');
417
+ return relativeCandidates(`${dir}/__package__.ts`, `./${cleaned}`);
418
+ }
419
+ function sourceFallbackCandidates(pkg, target) {
420
+ const normalized = repoPath(posix.normalize(posix.join(pkg.dir, target.replace(/^\.\//, ''))));
421
+ const prefixes = [`${pkg.dir}/dist/`, `${pkg.dir}/build/`];
422
+ const prefix = prefixes.find((candidate) => normalized.startsWith(candidate));
423
+ if (!prefix)
424
+ return [];
425
+ const relativeStem = normalized.slice(prefix.length).replace(/\.[^.\/]+$/, '');
426
+ return relativeCandidates(`${pkg.dir}/src/__fallback__.ts`, `./${relativeStem}`);
427
+ }
428
+ function resolveSpecifier(tree, workspace, importer, specifier, settings) {
429
+ if (specifier.startsWith('./') || specifier.startsWith('../')) {
430
+ const found = firstResolution(tree, relativeCandidates(importer, specifier));
431
+ if (found.path)
432
+ return { path: found.path };
433
+ if (found.unsafe)
434
+ return { diagnostic: { code: 'unsafe_path', path: found.unsafe, detail: `${importer}: ${specifier}` } };
435
+ if (found.declarationOnly)
436
+ return { diagnostic: { code: 'declaration_only', path: importer, detail: `${specifier} -> ${found.declarationOnly}` } };
437
+ return { diagnostic: { code: 'unresolved_internal', path: importer, detail: specifier } };
438
+ }
439
+ if (specifier.startsWith('#')) {
440
+ return { diagnostic: { code: 'unsupported_resolution', path: importer, detail: specifier } };
441
+ }
442
+ const { name, subpath } = packageNameAndSubpath(specifier);
443
+ const pkg = workspace.packages.get(name);
444
+ if (pkg) {
445
+ let target;
446
+ if (pkg.json.exports !== undefined) {
447
+ const selected = exportTarget(pkg.json.exports, subpath, settings.conditions);
448
+ if (selected.typesOnly)
449
+ return { diagnostic: { code: 'declaration_only', path: importer, detail: specifier } };
450
+ if (selected.unsupported || !selected.target)
451
+ return { diagnostic: { code: 'unsupported_exports', path: pkg.path, detail: specifier } };
452
+ target = selected.target;
453
+ }
454
+ else if (!subpath) {
455
+ target = typeof pkg.json.main === 'string'
456
+ ? pkg.json.main
457
+ : typeof pkg.json.module === 'string'
458
+ ? pkg.json.module
459
+ : undefined;
460
+ }
461
+ else {
462
+ target = subpath;
463
+ }
464
+ const candidates = target
465
+ ? packageTargetCandidates(pkg.dir, target)
466
+ : [`${pkg.dir}/src/index.ts`, `${pkg.dir}/src/index.tsx`, `${pkg.dir}/index.ts`, `${pkg.dir}/index.js`];
467
+ const found = firstResolution(tree, candidates);
468
+ if (found.path)
469
+ return { path: found.path };
470
+ if (found.unsafe)
471
+ return { diagnostic: { code: 'unsafe_path', path: found.unsafe, detail: `${importer}: ${specifier}` } };
472
+ if (found.declarationOnly)
473
+ return { diagnostic: { code: 'declaration_only', path: importer, detail: `${specifier} -> ${found.declarationOnly}` } };
474
+ if (target) {
475
+ const fallback = firstResolution(tree, sourceFallbackCandidates(pkg, target));
476
+ if (fallback.path) {
477
+ return {
478
+ path: fallback.path,
479
+ informational: { code: 'source_fallback', path: fallback.path, detail: specifier },
480
+ };
481
+ }
482
+ if (fallback.unsafe)
483
+ return { diagnostic: { code: 'unsafe_path', path: fallback.unsafe, detail: specifier } };
484
+ }
485
+ return { diagnostic: { code: 'unresolved_internal', path: importer, detail: specifier } };
486
+ }
487
+ if (builtinModules.includes(specifier) || specifier.startsWith('node:'))
488
+ return { external: true };
489
+ const dependencyNames = new Set([
490
+ ...declaredDependencies(nearestPackage(importer, workspace)),
491
+ ...declaredDependencies(workspace.rootPackage),
492
+ ]);
493
+ if (dependencyNames.has(name))
494
+ return { external: true };
495
+ if (name.startsWith('@') && workspace.packageScopes.has(name.split('/')[0])) {
496
+ return { diagnostic: { code: 'unresolved_internal', path: importer, detail: specifier } };
497
+ }
498
+ return { diagnostic: { code: 'unsupported_resolution', path: importer, detail: specifier } };
499
+ }
500
+ function seedRoots(seeds, workspace) {
501
+ return seeds.map((seed) => {
502
+ const matched = workspace.workspaceDirs.find((dir) => seed.path.startsWith(`${dir}/`));
503
+ if (matched)
504
+ return matched;
505
+ const segments = seed.path.split('/');
506
+ if ((segments[0] === 'apps' || segments[0] === 'packages') && segments.length > 2) {
507
+ return `${segments[0]}/${segments[1]}`;
508
+ }
509
+ return '';
510
+ });
511
+ }
512
+ export async function expandImports(input) {
513
+ const settings = input.settings ?? DEFAULT_MAPPER_SETTINGS;
514
+ const mapperVersion = input.mapperVersion ?? 'auto-surface-mapper@1';
515
+ const blocking = mapperVersion === 'auto-surface-mapper@2' ? BLOCKING_V2 : BLOCKING_V1;
516
+ const workspace = await discoverWorkspace(input.sandboxPath, input.tree);
517
+ const diagnostics = [...workspace.diagnostics];
518
+ const externals = [];
519
+ const roots = seedRoots(input.seeds, workspace);
520
+ const viaByPath = new Map();
521
+ const depthByPath = new Map();
522
+ const queue = [];
523
+ for (const seed of [...input.seeds].sort((a, b) => a.path.localeCompare(b.path))) {
524
+ if (!depthByPath.has(seed.path)) {
525
+ depthByPath.set(seed.path, 0);
526
+ queue.push(seed.path);
527
+ }
528
+ const via = viaByPath.get(seed.path) ?? [];
529
+ via.push({ from: '', specifier: seed.path, edge: 'seed' });
530
+ viaByPath.set(seed.path, via);
531
+ }
532
+ const files = [];
533
+ let includedBytes = 0;
534
+ let byteLimitHit = false;
535
+ while (queue.length > 0) {
536
+ const path = queue.shift();
537
+ const depth = depthByPath.get(path);
538
+ if (byteLimitHit) {
539
+ diagnostics.push({ code: 'limit_bytes', path, detail: `maxSourceBytes=${settings.maxSourceBytes}` });
540
+ continue;
541
+ }
542
+ if (files.length >= settings.maxFiles) {
543
+ diagnostics.push({ code: 'limit_files', path, detail: `maxFiles=${settings.maxFiles}` });
544
+ for (const omitted of queue)
545
+ diagnostics.push({ code: 'limit_files', path: omitted, detail: `maxFiles=${settings.maxFiles}` });
546
+ break;
547
+ }
548
+ let bytes;
549
+ try {
550
+ bytes = await safeRead(input.sandboxPath, input.tree, path);
551
+ }
552
+ catch (error) {
553
+ diagnostics.push({ code: 'unsafe_path', path, detail: error instanceof Error ? error.message : String(error) });
554
+ continue;
555
+ }
556
+ if (includedBytes + bytes.byteLength > settings.maxSourceBytes) {
557
+ diagnostics.push({ code: 'limit_bytes', path, detail: `maxSourceBytes=${settings.maxSourceBytes}` });
558
+ byteLimitHit = true;
559
+ continue;
560
+ }
561
+ includedBytes += bytes.byteLength;
562
+ const extension = extname(path);
563
+ const parsed = SCRIPT_EXTENSIONS.has(extension);
564
+ files.push({
565
+ path,
566
+ sha256: hash(bytes),
567
+ bytes: bytes.byteLength,
568
+ depth,
569
+ shared: roots.every((root) => root !== '' && path !== root && !path.startsWith(`${root}/`)),
570
+ kind: parsed ? 'parsed' : 'opaque',
571
+ via: (viaByPath.get(path) ?? []).sort((a, b) => a.from.localeCompare(b.from) || a.specifier.localeCompare(b.specifier) || a.edge.localeCompare(b.edge)),
572
+ });
573
+ if (!parsed)
574
+ continue;
575
+ const parsedFile = parseImportEdges(path, bytes.toString('utf8'));
576
+ if (parsedFile.parseFailed) {
577
+ diagnostics.push({ code: 'parse_failed', path, detail: 'TypeScript syntax parse failed' });
578
+ continue;
579
+ }
580
+ for (const edge of parsedFile.edges) {
581
+ if (edge.specifier === null) {
582
+ diagnostics.push({ code: 'unresolved_dynamic', path, detail: edge.detail ?? edge.edge });
583
+ continue;
584
+ }
585
+ const resolved = resolveSpecifier(input.tree, workspace, path, edge.specifier, settings);
586
+ if (resolved.external) {
587
+ externals.push({ from: path, specifier: edge.specifier });
588
+ diagnostics.push({ code: 'external', path, detail: edge.specifier });
589
+ continue;
590
+ }
591
+ if (resolved.diagnostic)
592
+ diagnostics.push(resolved.diagnostic);
593
+ if (resolved.informational)
594
+ diagnostics.push(resolved.informational);
595
+ if (!resolved.path)
596
+ continue;
597
+ const target = resolved.path;
598
+ const targetDepth = depth + 1;
599
+ const via = viaByPath.get(target) ?? [];
600
+ via.push({ from: path, specifier: edge.specifier, edge: edge.edge });
601
+ viaByPath.set(target, via);
602
+ const knownDepth = depthByPath.get(target);
603
+ if (knownDepth !== undefined && knownDepth <= targetDepth)
604
+ continue;
605
+ depthByPath.set(target, targetDepth);
606
+ if (targetDepth > settings.maxDepth) {
607
+ diagnostics.push({ code: 'limit_depth', path: target, detail: `maxDepth=${settings.maxDepth}` });
608
+ }
609
+ else {
610
+ queue.push(target);
611
+ }
612
+ }
613
+ }
614
+ for (const file of files) {
615
+ file.via = (viaByPath.get(file.path) ?? []).sort((a, b) => a.from.localeCompare(b.from) || a.specifier.localeCompare(b.specifier) || a.edge.localeCompare(b.edge));
616
+ }
617
+ files.sort((a, b) => a.path.localeCompare(b.path));
618
+ const uniqueDiagnostics = [...new Map(diagnostics.map((entry) => [`${entry.code}\0${entry.path}\0${entry.detail}`, entry])).values()].sort(compareDiagnostic);
619
+ const uniqueExternals = [...new Map(externals.map((entry) => [`${entry.from}\0${entry.specifier}`, entry])).values()].sort((a, b) => a.from.localeCompare(b.from) || a.specifier.localeCompare(b.specifier));
620
+ return {
621
+ mapper: { version: mapperVersion, settings },
622
+ resolutionInputs: workspace.resolutionInputs,
623
+ files,
624
+ externals: uniqueExternals,
625
+ status: uniqueDiagnostics.some((diagnostic) => blocking.has(diagnostic.code)) ? 'incomplete' : 'complete',
626
+ diagnostics: uniqueDiagnostics,
627
+ };
628
+ }
629
+ export function trackedNamedPathStatus(tree, path) {
630
+ if (!isSafeRepoPath(path))
631
+ return 'unverifiable';
632
+ const entry = tree.entries.get(path);
633
+ if (!entry)
634
+ return 'missing';
635
+ return isTrackedRegularFileMode(entry.mode, entry.type) ? 'present' : 'unverifiable';
636
+ }
637
+ export function absoluteIsContained(root, path) {
638
+ const absoluteRoot = resolve(root);
639
+ const absolute = resolve(root, path);
640
+ return absolute === absoluteRoot || absolute.startsWith(`${absoluteRoot}${sep}`);
641
+ }
642
+ export function relativeRepoPath(root, path) {
643
+ return repoPath(relative(root, path));
644
+ }