carrick 0.3.133 → 0.3.135

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 (70) hide show
  1. package/dist/contract.d.ts +6 -0
  2. package/dist/contract.js.map +1 -1
  3. package/dist/hook/post-edit.d.ts +10 -5
  4. package/dist/hook/post-edit.js +80 -23
  5. package/dist/hook/post-edit.js.map +1 -1
  6. package/dist/hook/refresh.d.ts +23 -3
  7. package/dist/hook/refresh.js +48 -6
  8. package/dist/hook/refresh.js.map +1 -1
  9. package/dist/hook/reuse.d.ts +7 -21
  10. package/dist/hook/reuse.js +23 -36
  11. package/dist/hook/reuse.js.map +1 -1
  12. package/dist/hook/session-start.js +15 -2
  13. package/dist/hook/session-start.js.map +1 -1
  14. package/dist/hook/stop.js.map +1 -1
  15. package/dist/init/agents-section.js +1 -1
  16. package/dist/init/agents-section.js.map +1 -1
  17. package/dist/init/doctor.js +4 -0
  18. package/dist/init/doctor.js.map +1 -1
  19. package/dist/init/hosted.d.ts +1 -0
  20. package/dist/init/hosted.js +4 -1
  21. package/dist/init/hosted.js.map +1 -1
  22. package/dist/init/remove.js +7 -1
  23. package/dist/init/remove.js.map +1 -1
  24. package/dist/init/repo-copies.js +3 -1
  25. package/dist/init/repo-copies.js.map +1 -1
  26. package/dist/init/run.d.ts +1 -1
  27. package/dist/init/run.js +10 -2
  28. package/dist/init/run.js.map +1 -1
  29. package/dist/init/task-skills.d.ts +22 -1
  30. package/dist/init/task-skills.js +65 -1
  31. package/dist/init/task-skills.js.map +1 -1
  32. package/dist/render.d.ts +41 -16
  33. package/dist/render.js +91 -102
  34. package/dist/render.js.map +1 -1
  35. package/package.json +10 -9
  36. package/plugin/.claude-plugin/plugin.json +1 -1
  37. package/sidecar/dist/src/capture/anchors.js +18 -1
  38. package/sidecar/dist/src/capture/api.d.ts +17 -4
  39. package/sidecar/dist/src/capture/check-workspace.js +1 -1
  40. package/sidecar/dist/src/capture/check.d.ts +44 -0
  41. package/sidecar/dist/src/capture/check.js +112 -10
  42. package/sidecar/dist/src/capture/index.js +18 -0
  43. package/sidecar/dist/src/capture/installed-package.d.ts +5 -0
  44. package/sidecar/dist/src/capture/installed-package.js +64 -6
  45. package/sidecar/dist/src/capture/machine-paths.d.ts +44 -0
  46. package/sidecar/dist/src/capture/machine-paths.js +97 -0
  47. package/sidecar/dist/src/capture/node-builder.d.ts +2 -0
  48. package/sidecar/dist/src/capture/node-builder.js +1 -1
  49. package/sidecar/dist/src/capture/paths-rewrite.js +3 -3
  50. package/sidecar/dist/src/capture/result-label.d.ts +42 -0
  51. package/sidecar/dist/src/capture/result-label.js +91 -0
  52. package/sidecar/dist/src/capture/self-check.js +13 -2
  53. package/sidecar/dist/src/index.js +117 -5
  54. package/sidecar/dist/src/infer-cache.d.ts +125 -0
  55. package/sidecar/dist/src/infer-cache.js +244 -0
  56. package/sidecar/dist/src/library-claims.d.ts +35 -3
  57. package/sidecar/dist/src/library-claims.js +417 -61
  58. package/sidecar/dist/src/node-index.d.ts +53 -0
  59. package/sidecar/dist/src/node-index.js +100 -0
  60. package/sidecar/dist/src/project-loader.d.ts +11 -0
  61. package/sidecar/dist/src/project-loader.js +15 -0
  62. package/sidecar/dist/src/result-candidates.d.ts +28 -0
  63. package/sidecar/dist/src/result-candidates.js +209 -0
  64. package/sidecar/dist/src/type-inferrer.d.ts +27 -0
  65. package/sidecar/dist/src/type-inferrer.js +175 -43
  66. package/sidecar/dist/src/types.d.ts +77 -1
  67. package/sidecar/dist/src/unwidened.js +10 -1
  68. package/sidecar/dist/src/validators.d.ts +931 -197
  69. package/sidecar/dist/src/validators.js +34 -0
  70. package/templates/skills/carrick-reuse.md +11 -0
@@ -61,6 +61,31 @@ export function carriesHeapLimitLine(stderr) {
61
61
  .some((line) => line.includes(HEAP_OUT_OF_MEMORY) &&
62
62
  !HARD_SIZE_LIMITS.some((limit) => line.includes(limit)));
63
63
  }
64
+ /**
65
+ * Whether the native compiler (TypeScript 7, a Go program) died out of
66
+ * memory. The Go runtime prints `fatal error: runtime: out of memory` and
67
+ * exits 2; V8's line above never appears, and a heap cap passed through
68
+ * `NODE_OPTIONS` reaches only the launcher, not the compiler.
69
+ */
70
+ export function carriesNativeOutOfMemoryLine(output) {
71
+ return output.split('\n').some((line) => line.includes('fatal error: runtime: out of memory'));
72
+ }
73
+ /**
74
+ * Whether a diagnostic is about the compiler's configuration rather than a
75
+ * file it checked: one placed in the tsconfig the judge wrote, or "no inputs"
76
+ * (TS18003). Keyed on the file, not on the TS5xxx option range: some of those
77
+ * codes sit in a source file (TS5097, an import ending in `.ts`) and poison
78
+ * only the stub that holds it.
79
+ */
80
+ export function isConfigDiagnostic(d) {
81
+ return path.posix.basename(d.file) === 'tsconfig.json' || d.code === 18003;
82
+ }
83
+ /** Why the compiler ended abnormally, when its output names a known cause. */
84
+ export function abnormalCause(output) {
85
+ if (carriesNativeOutOfMemoryLine(output))
86
+ return 'the type checker ran out of memory';
87
+ return undefined;
88
+ }
64
89
  /** V8's default old-space ceiling on a 64-bit host, in MB. */
65
90
  const V8_DEFAULT_HEAP_CEILING_MB = 4096;
66
91
  /**
@@ -119,9 +144,16 @@ function sidecarRoot() {
119
144
  * which is the caller's "not found" signal and the most useful thing to name.
120
145
  */
121
146
  export function resolveVendoredBin(name, from = sidecarRoot()) {
122
- const local = path.join(from, 'node_modules', '.bin', name);
147
+ return resolveVendored(path.join('.bin', name), from);
148
+ }
149
+ /**
150
+ * The first `node_modules/<rel>` at or above `from`, the same walk as
151
+ * {@link resolveVendoredBin}; the sidecar-local path when none exists.
152
+ */
153
+ export function resolveVendored(rel, from = sidecarRoot()) {
154
+ const local = path.join(from, 'node_modules', rel);
123
155
  for (let dir = from;; dir = path.dirname(dir)) {
124
- const candidate = path.join(dir, 'node_modules', '.bin', name);
156
+ const candidate = path.join(dir, 'node_modules', rel);
125
157
  if (fs.existsSync(candidate))
126
158
  return candidate;
127
159
  if (path.dirname(dir) === dir)
@@ -131,6 +163,58 @@ export function resolveVendoredBin(name, from = sidecarRoot()) {
131
163
  function binPath(name) {
132
164
  return resolveVendoredBin(name);
133
165
  }
166
+ /**
167
+ * The package that brings the judge's compiler: TypeScript 7 under an alias
168
+ * beside the TypeScript 6 that capture and inference read through ts-morph.
169
+ * Its platform package holds the native executable the judge runs. Both
170
+ * packages declare a `tsc` bin, so `node_modules/.bin/tsc` may be either; the
171
+ * judge never reads it.
172
+ */
173
+ export const JUDGE_COMPILER_PACKAGE = 'typescript-native';
174
+ /**
175
+ * The judge's tsc: the host's native executable, from the platform package
176
+ * the compiler installs as an optional dependency (`@typescript/
177
+ * typescript-<platform>-<arch>`), found by the same walk as the bins. Where it
178
+ * is not installed (an install that omits optional dependencies, such as the
179
+ * cloud runner's, or a pnpm store the walk does not reach), TypeScript 6's own
180
+ * tsc, which ships for ts-morph anyway. The two read every probe the same
181
+ * (measured on 1,018 real probes; the one wording difference moves only a
182
+ * verdict's `codes`), so the fallback changes no verdict, only the time.
183
+ */
184
+ export function judgeTscPath(from = sidecarRoot(), platform = process.platform, arch = process.arch) {
185
+ const exe = platform === 'win32' ? 'tsc.exe' : 'tsc';
186
+ const native = path.join('@typescript', `typescript-${platform}-${arch}`, 'lib', exe);
187
+ const found = resolveVendored(native, from);
188
+ if (fs.existsSync(found))
189
+ return found;
190
+ return resolveVendored(path.join('typescript', 'bin', 'tsc'), from);
191
+ }
192
+ /** Which compiler a tsc path is, in words, for the run log and the result. */
193
+ export function judgeLabel(tscPath) {
194
+ const version = compilerVersion(tscPath);
195
+ const parts = tscPath.split(/[\\/]/);
196
+ if (parts.some((p) => p.startsWith('typescript-') && p !== JUDGE_COMPILER_PACKAGE)) {
197
+ return `TypeScript ${version} (native)`;
198
+ }
199
+ if (parts.includes('typescript') && parts[parts.length - 2] === 'bin') {
200
+ return `TypeScript ${version} (native compiler not installed)`;
201
+ }
202
+ return `a stand-in compiler at ${tscPath}`;
203
+ }
204
+ /** The version of the compiler a tsc path belongs to, read from the package
205
+ * that holds it; the compiler-API version for a stand-in with no package. */
206
+ function compilerVersion(tscPath) {
207
+ try {
208
+ const manifest = JSON.parse(fs.readFileSync(path.join(path.dirname(tscPath), '..', 'package.json'), 'utf8'));
209
+ const compiler = manifest.name === 'typescript' || manifest.name?.startsWith('@typescript/typescript-');
210
+ if (compiler && manifest.version)
211
+ return manifest.version;
212
+ }
213
+ catch {
214
+ // A stand-in compiler (tests) sits in no package.
215
+ }
216
+ return ts.version;
217
+ }
134
218
  function unverifiableAll(plans, gate, diagnostic) {
135
219
  return plans.map((plan) => ({
136
220
  pair_id: plan.pairId,
@@ -155,8 +239,9 @@ export async function runCheck(opts, onProgress) {
155
239
  const progress = onProgress ?? (() => { });
156
240
  const cleanup = opts.cleanup !== false;
157
241
  const pnpmPath = opts.pnpmPath ?? binPath('pnpm');
158
- const tscPath = opts.tscPath ?? binPath('tsc');
159
- const tsVersion = ts.version;
242
+ const tscPath = opts.tscPath ?? judgeTscPath();
243
+ const tsVersion = compilerVersion(tscPath);
244
+ const judge = judgeLabel(tscPath);
160
245
  // Isolation is non-negotiable: without the vendored pnpm we would fall back
161
246
  // to a flat npm install that manufactures nominal false-incompatibles. Fail
162
247
  // explicitly instead (design Check step 2).
@@ -169,6 +254,7 @@ export async function runCheck(opts, onProgress) {
169
254
  isolation: 'unavailable',
170
255
  install_ok: false,
171
256
  ts_version: tsVersion,
257
+ judge,
172
258
  verdicts: sortVerdicts([
173
259
  ...unverifiableAll(probing, 'isolation:unavailable', 'type isolation unavailable (pnpm missing); compatibility cannot be verified.'),
174
260
  ...preGated,
@@ -248,13 +334,14 @@ export async function runCheck(opts, onProgress) {
248
334
  isolation: 'pnpm',
249
335
  install_ok: false,
250
336
  ts_version: tsVersion,
337
+ judge,
251
338
  verdicts,
252
339
  degraded_services: degraded,
253
340
  errors,
254
341
  };
255
342
  }
256
343
  // ---- Judge (async, off the event loop) ----------------------------------
257
- progress('checking', 'type-checking probes');
344
+ progress('checking', `type-checking probes (judge: ${judge})`);
258
345
  const heap = childHeap(process.execArgv, process.env.NODE_OPTIONS, process.memoryUsage().rss / (1024 * 1024));
259
346
  const tsc = await runProcess(tscPath, ['--noEmit', '--pretty', 'false', '-p', path.join(ws.probesRel, 'tsconfig.json')], ws.workspaceDir, { ...process.env, NODE_OPTIONS: heap.nodeOptions });
260
347
  const diagnostics = parseTscOutput(tsc.stdout + '\n' + tsc.stderr);
@@ -267,8 +354,20 @@ export async function runCheck(opts, onProgress) {
267
354
  // `code === 0` guard.
268
355
  // A compiler a signal ended never finished, so a pair with no diagnostic
269
356
  // was never cleared, whatever it printed first (carrick#2209).
270
- if (tsc.signal !== null || (tsc.code !== 0 && diagnostics.length === 0)) {
271
- const excerpt = scrubPaths((tsc.stderr || tsc.stdout).trim().slice(0, 2000), scrubCtx);
357
+ //
358
+ // A diagnostic about the configuration (a tsconfig the compiler refuses,
359
+ // an option it does not know) is printed WITH a position, in the tsconfig,
360
+ // and belongs to no pair: TypeScript 7 refuses an option and checks
361
+ // nothing, so without this every pair read compatible. Such a run checked
362
+ // no pair, whatever else it printed.
363
+ const configError = diagnostics.find(isConfigDiagnostic);
364
+ if (tsc.signal !== null ||
365
+ (tsc.code !== 0 && diagnostics.length === 0) ||
366
+ configError !== undefined) {
367
+ const excerpt = scrubPaths((configError
368
+ ? `${configError.file}(${configError.line},${configError.col}): error TS${configError.code}: ${configError.message}`
369
+ : (tsc.stderr || tsc.stdout).trim()).slice(0, 2000), scrubCtx);
370
+ const cause = abnormalCause(tsc.stderr + '\n' + tsc.stdout);
272
371
  errors.push(`tsc terminated abnormally (exit code ${tsc.code ?? 'null'}) ` +
273
372
  `with no parseable diagnostics${excerpt ? `: ${excerpt}` : ''}`);
274
373
  for (const s of opts.stubs) {
@@ -278,8 +377,9 @@ export async function runCheck(opts, onProgress) {
278
377
  });
279
378
  }
280
379
  const verdicts = sortVerdicts([
281
- ...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'}); ` +
282
- 'compatibility cannot be verified.' +
380
+ ...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'})` +
381
+ (cause ? ` because ${cause}` : '') +
382
+ '; compatibility cannot be verified.' +
283
383
  (excerpt ? ` Compiler output: ${excerpt}` : '')),
284
384
  ...preGated,
285
385
  ...unresolved,
@@ -290,7 +390,7 @@ export async function runCheck(opts, onProgress) {
290
390
  const compilerEnd = {
291
391
  exit_code: tsc.code,
292
392
  signal: tsc.signal,
293
- heap_limit: carriesHeapLimitLine(tsc.stderr),
393
+ heap_limit: carriesHeapLimitLine(tsc.stderr) || carriesNativeOutOfMemoryLine(tsc.stderr + '\n' + tsc.stdout),
294
394
  heap_cap_mb: heap.heapCapMb,
295
395
  };
296
396
  if (cleanup)
@@ -301,6 +401,7 @@ export async function runCheck(opts, onProgress) {
301
401
  isolation: 'pnpm',
302
402
  install_ok: true,
303
403
  ts_version: tsVersion,
404
+ judge,
304
405
  verdicts,
305
406
  degraded_services: dedupeDegraded(degraded),
306
407
  errors,
@@ -359,6 +460,7 @@ export async function runCheck(opts, onProgress) {
359
460
  isolation: 'pnpm',
360
461
  install_ok: true,
361
462
  ts_version: tsVersion,
463
+ judge,
362
464
  verdicts,
363
465
  degraded_services: dedupeDegraded(degraded),
364
466
  errors,
@@ -35,6 +35,7 @@ import { findAugmentationFiles } from './augmentations.js';
35
35
  import { installedVersions, lockfileVersions } from './lockfile.js';
36
36
  import { installedResolutionEdges, RESOLUTION_FILE } from './resolution-edges.js';
37
37
  import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
38
+ import { substituteMachinePathImports } from './machine-paths.js';
38
39
  import { installedPackageSpecifier, typesPackageOf, withInstalledPackages } from './installed-package.js';
39
40
  import { selfCheckStub } from './self-check.js';
40
41
  import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
@@ -448,6 +449,23 @@ export function captureStub(opts) {
448
449
  entryDir,
449
450
  });
450
451
  const specifierRewrites = denoRewrites + placed.rewrites + rewritten.rewrites;
452
+ // What the rewrite could not map still names a machine path (carrick#2943).
453
+ const machinePaths = substituteMachinePathImports({
454
+ files: emittedFiles.map((rel) => path.join(typesDir, rel)),
455
+ surfaceAbsPath,
456
+ tree: stubGuard.narrow(typesDir),
457
+ labelRoot: opts.scanRoot ?? repoRoot,
458
+ });
459
+ if (machinePaths.size > 0) {
460
+ resolved = resolved.map((anchor) => {
461
+ const found = machinePaths.get(anchor.request.alias);
462
+ if (!found)
463
+ return anchor;
464
+ const paths = [...new Set([...(anchor.unresolved?.paths ?? []), ...found.paths])];
465
+ const specifiers = [...new Set([...found.modules, ...(anchor.unresolved?.specifiers ?? [])])];
466
+ return { ...anchor, unresolved: { paths, specifiers } };
467
+ });
468
+ }
451
469
  // ---- Pin external deps: installed node_modules first, lockfile fallback ----
452
470
  // Externals are collected AFTER the rewrite pass: a rewritten paths
453
471
  // specifier is internal, not a dependency.
@@ -35,6 +35,11 @@ export interface InstalledPackageSpecifier {
35
35
  name: string;
36
36
  version: string;
37
37
  };
38
+ /**
39
+ * The name the entry exports the declaration under, set only when it differs
40
+ * from the name the path's declaration carries (`export { O as OmitKeys }`).
41
+ */
42
+ exportName?: string;
38
43
  }
39
44
  /**
40
45
  * The `@types/*` package that serves a bare name's declarations, for callers
@@ -245,29 +245,78 @@ function entrySubpaths(manifest) {
245
245
  }
246
246
  return [''];
247
247
  }
248
+ /**
249
+ * Declaration keys (`file\0declaredName`) of what `file` exports as `name`.
250
+ * A chunk can declare a type under its public name and export it under a
251
+ * short one (`export { type OmitKeyof as O }`), so the name read off the
252
+ * import is not the name the declaration carries.
253
+ */
254
+ function declarationKeysOfExport(file, name) {
255
+ const program = ts.createProgram([file], RESOLUTION_OPTIONS);
256
+ const checker = program.getTypeChecker();
257
+ const source = program.getSourceFile(file);
258
+ const moduleSymbol = source && checker.getSymbolAtLocation(source);
259
+ const symbol = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((s) => s.getName() === name);
260
+ if (!symbol)
261
+ return [];
262
+ let target = symbol;
263
+ for (let hops = 0; target.flags & ts.SymbolFlags.Alias && hops < 16; hops++) {
264
+ const next = checker.getAliasedSymbol(target);
265
+ if (next === target)
266
+ break;
267
+ target = next;
268
+ }
269
+ const keys = [];
270
+ for (const declaration of target.declarations ?? []) {
271
+ try {
272
+ keys.push(`${fs.realpathSync(declaration.getSourceFile().fileName)}\0${target.getName()}`);
273
+ }
274
+ catch {
275
+ // declaration outside the real file system: ignore
276
+ }
277
+ }
278
+ return keys;
279
+ }
248
280
  const moduleExportsCache = new Map();
249
- /** Exported names of `entryFile`, each with the real files declaring it. */
281
+ /**
282
+ * Exports of `entryFile`: by exported name (with the real files declaring it)
283
+ * and by declaration identity (the file and the name the declaration carries
284
+ * there, whatever name the entry exports it under).
285
+ */
250
286
  function exportsOf(entryFile) {
251
287
  const cached = moduleExportsCache.get(entryFile);
252
288
  if (cached)
253
289
  return cached;
254
- const exported = new Map();
290
+ const exported = { byName: new Map(), byDeclaration: new Map() };
255
291
  const program = ts.createProgram([entryFile], RESOLUTION_OPTIONS);
256
292
  const checker = program.getTypeChecker();
257
293
  const source = program.getSourceFile(entryFile);
258
294
  const moduleSymbol = source && checker.getSymbolAtLocation(source);
259
295
  for (const symbol of moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : []) {
260
- const target = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
296
+ let target = symbol;
297
+ // Follow the alias chain until it stops (an entry can re-export a re-export).
298
+ for (let hops = 0; target.flags & ts.SymbolFlags.Alias && hops < 16; hops++) {
299
+ const next = checker.getAliasedSymbol(target);
300
+ if (next === target)
301
+ break;
302
+ target = next;
303
+ }
261
304
  const files = new Set();
262
305
  for (const declaration of target.declarations ?? []) {
263
306
  try {
264
- files.add(fs.realpathSync(declaration.getSourceFile().fileName));
307
+ const real = fs.realpathSync(declaration.getSourceFile().fileName);
308
+ files.add(real);
309
+ const key = `${real}\0${target.getName()}`;
310
+ // The first export of a declaration wins; its own name wins over a rename.
311
+ if (!exported.byDeclaration.has(key) || symbol.getName() === target.getName()) {
312
+ exported.byDeclaration.set(key, symbol.getName());
313
+ }
265
314
  }
266
315
  catch {
267
316
  // declaration outside the real file system: ignore
268
317
  }
269
318
  }
270
- exported.set(symbol.getName(), files);
319
+ exported.byName.set(symbol.getName(), files);
271
320
  }
272
321
  moduleExportsCache.set(entryFile, exported);
273
322
  return exported;
@@ -304,9 +353,18 @@ export function installedPackageSpecifier(absoluteSpecifier, importedName) {
304
353
  const entryFile = resolveAgainstInstall(publicName + entry, root, manifest.name)?.resolvedFileName;
305
354
  if (!entryFile)
306
355
  continue;
307
- if (exportsOf(entryFile).get(importedName)?.has(file)) {
356
+ const entryExports = exportsOf(entryFile);
357
+ if (entryExports.byName.get(importedName)?.has(file)) {
308
358
  return withPin(publicName + entry);
309
359
  }
360
+ // The entry may export the declaration under another name.
361
+ const renamed = entryExports.byDeclaration.get(`${file}\0${importedName}`) ??
362
+ declarationKeysOfExport(file, importedName)
363
+ .map((key) => entryExports.byDeclaration.get(key))
364
+ .find((name) => name !== undefined);
365
+ if (renamed !== undefined) {
366
+ return { ...withPin(publicName + entry), exportName: renamed };
367
+ }
310
368
  }
311
369
  }
312
370
  return withPin(publicName + (candidates[0] ?? `/${withoutExtension(subpath)}`));
@@ -0,0 +1,44 @@
1
+ /**
2
+ * No stub names a module by a machine path (carrick#2943).
3
+ *
4
+ * The specifier rewrite maps an absolute specifier onto a file of the stub's
5
+ * tree or onto an installed package. What it cannot map stays as printed: a
6
+ * type that a printer could name only by where its module sits on this
7
+ * machine, such as a workspace sibling's source reached through a
8
+ * `node_modules` link, which the declaration emit does not carry. That path
9
+ * resolves on the machine that ran the capture, to a source file outside the
10
+ * stub, and nowhere else: the upload rewrites it to `<checkout>/...`, so the
11
+ * stored stub and a fresh one disagree about the same alias.
12
+ *
13
+ * Each such reference becomes `unknown` at its own position. In the surface
14
+ * the position is reported per alias, so the self-check labels the finding
15
+ * there as a reference that did not resolve rather than as declared; the rest
16
+ * of the alias's type survives. Any other tree file gets the dangling-import
17
+ * repair (carrick#1397), which writes `unknown` where the import's names were
18
+ * used. The stub then reads the same on every machine.
19
+ *
20
+ * Seam: node builtins + `typescript`, like the rest of this directory.
21
+ */
22
+ import type { WriteGuard } from './guarded-fs.js';
23
+ /** A specifier that names a module by where it sits on one machine. */
24
+ export declare function isMachinePath(specifier: string): boolean;
25
+ /** What the surface substitution replaced in one alias. */
26
+ export interface MachinePathPositions {
27
+ /** Member paths now `unknown`, in the deep walk's notation (`''` = root). */
28
+ paths: string[];
29
+ /** The modules they named, relative to `labelRoot` (never absolute). */
30
+ modules: string[];
31
+ }
32
+ /**
33
+ * Replace every machine-path import in the stub's tree with `unknown`.
34
+ * `files` are absolute tree paths; `surfaceAbsPath` is one of them. Returns
35
+ * the positions replaced in the surface, by alias.
36
+ */
37
+ export declare function substituteMachinePathImports(args: {
38
+ files: readonly string[];
39
+ surfaceAbsPath: string;
40
+ /** A guard over the stub's types tree. */
41
+ tree: WriteGuard;
42
+ /** The root a module is named from in a label (the scanned repo's). */
43
+ labelRoot: string;
44
+ }): Map<string, MachinePathPositions>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * No stub names a module by a machine path (carrick#2943).
3
+ *
4
+ * The specifier rewrite maps an absolute specifier onto a file of the stub's
5
+ * tree or onto an installed package. What it cannot map stays as printed: a
6
+ * type that a printer could name only by where its module sits on this
7
+ * machine, such as a workspace sibling's source reached through a
8
+ * `node_modules` link, which the declaration emit does not carry. That path
9
+ * resolves on the machine that ran the capture, to a source file outside the
10
+ * stub, and nowhere else: the upload rewrites it to `<checkout>/...`, so the
11
+ * stored stub and a fresh one disagree about the same alias.
12
+ *
13
+ * Each such reference becomes `unknown` at its own position. In the surface
14
+ * the position is reported per alias, so the self-check labels the finding
15
+ * there as a reference that did not resolve rather than as declared; the rest
16
+ * of the alias's type survives. Any other tree file gets the dangling-import
17
+ * repair (carrick#1397), which writes `unknown` where the import's names were
18
+ * used. The stub then reads the same on every machine.
19
+ *
20
+ * Seam: node builtins + `typescript`, like the rest of this directory.
21
+ */
22
+ import ts from 'typescript';
23
+ import * as fs from 'node:fs';
24
+ import * as path from 'node:path';
25
+ import { childPath } from './node-builder.js';
26
+ import { repairDanglingImports } from './repair-dangling.js';
27
+ import { collectSpecifiers } from './specifiers.js';
28
+ /** A specifier that names a module by where it sits on one machine. */
29
+ export function isMachinePath(specifier) {
30
+ return path.posix.isAbsolute(specifier) || /^[A-Za-z]:[\\/]/.test(specifier) || specifier.startsWith('\\\\');
31
+ }
32
+ /**
33
+ * Replace every machine-path import in the stub's tree with `unknown`.
34
+ * `files` are absolute tree paths; `surfaceAbsPath` is one of them. Returns
35
+ * the positions replaced in the surface, by alias.
36
+ */
37
+ export function substituteMachinePathImports(args) {
38
+ const failing = new Map();
39
+ for (const file of args.files) {
40
+ if (path.resolve(file) === path.resolve(args.surfaceAbsPath))
41
+ continue;
42
+ const machine = [...collectSpecifiers(fs.readFileSync(file, 'utf8'))].filter(isMachinePath);
43
+ if (machine.length > 0)
44
+ failing.set(file, new Set(machine));
45
+ }
46
+ if (failing.size > 0)
47
+ repairDanglingImports(failing, args.tree);
48
+ return substituteInSurface(args.surfaceAbsPath, args.tree, args.labelRoot);
49
+ }
50
+ function substituteInSurface(surfaceAbsPath, tree, labelRoot) {
51
+ const byAlias = new Map();
52
+ const text = fs.readFileSync(surfaceAbsPath, 'utf8');
53
+ if (!/import\s*\(/.test(text))
54
+ return byAlias;
55
+ const source = ts.createSourceFile(surfaceAbsPath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
56
+ const edits = [];
57
+ for (const statement of source.statements) {
58
+ if (!ts.isTypeAliasDeclaration(statement))
59
+ continue;
60
+ const found = { paths: [], modules: [] };
61
+ const visit = (node, at) => {
62
+ const specifier = importTypeSpecifier(node);
63
+ if (specifier !== undefined && isMachinePath(specifier)) {
64
+ edits.push({ start: node.getStart(source), end: node.getEnd() });
65
+ if (!found.paths.includes(at))
66
+ found.paths.push(at);
67
+ const module = moduleLabel(specifier, labelRoot);
68
+ if (!found.modules.includes(module))
69
+ found.modules.push(module);
70
+ return;
71
+ }
72
+ node.forEachChild((child) => visit(child, childPath(node, child, at)));
73
+ };
74
+ visit(statement.type, '');
75
+ if (found.paths.length > 0)
76
+ byAlias.set(statement.name.text, found);
77
+ }
78
+ if (edits.length === 0)
79
+ return byAlias;
80
+ edits.sort((a, b) => b.start - a.start);
81
+ let out = text;
82
+ for (const edit of edits)
83
+ out = `${out.slice(0, edit.start)}unknown${out.slice(edit.end)}`;
84
+ tree.writeFile(surfaceAbsPath, out);
85
+ return byAlias;
86
+ }
87
+ /** The string an `import("...")` type names, when `node` is one. */
88
+ function importTypeSpecifier(node) {
89
+ if (!ts.isImportTypeNode(node) || !ts.isLiteralTypeNode(node.argument))
90
+ return undefined;
91
+ const literal = node.argument.literal;
92
+ return ts.isStringLiteral(literal) ? literal.text : undefined;
93
+ }
94
+ /** `specifier` relative to `root`, with forward slashes. */
95
+ function moduleLabel(specifier, root) {
96
+ return path.relative(path.resolve(root), path.resolve(specifier)).split(path.sep).join('/');
97
+ }
@@ -67,6 +67,8 @@ export declare function substituteUndeclaredNames(node: ts.TypeNode, program: ts
67
67
  node: ts.TypeNode;
68
68
  substitutions: UndeclaredSubstitution[];
69
69
  };
70
+ /** The deep walk's path for `child` inside `parent`, extending `path`. */
71
+ export declare function childPath(parent: ts.Node, child: ts.Node, path: string): string;
70
72
  /**
71
73
  * Bare names in a printed type node that nothing in the producer's program
72
74
  * declares (carrick#1165).
@@ -174,7 +174,7 @@ export function substituteUndeclaredNames(node, program, destination) {
174
174
  return { node: rewritten, substitutions };
175
175
  }
176
176
  /** The deep walk's path for `child` inside `parent`, extending `path`. */
177
- function childPath(parent, child, path) {
177
+ export function childPath(parent, child, path) {
178
178
  if (ts.isPropertySignature(parent) && parent.type === child) {
179
179
  const name = ts.isIdentifier(parent.name) || ts.isStringLiteral(parent.name)
180
180
  ? parent.name.text
@@ -82,7 +82,7 @@ export function rewriteEmittedSpecifiers(args) {
82
82
  installs[mapped.install.name] = mapped.install.root;
83
83
  if (mapped.pin)
84
84
  pins[mapped.pin.name] = mapped.pin.version;
85
- return mapped.specifier;
85
+ return { specifier: mapped.specifier, exportName: mapped.exportName };
86
86
  };
87
87
  for (const file of args.files) {
88
88
  const absFile = path.join(args.typesDir, file);
@@ -97,13 +97,13 @@ export function rewriteEmittedSpecifiers(args) {
97
97
  if (replacement === undefined)
98
98
  return whole;
99
99
  importTypeRewrites++;
100
- return `${open}${quote}${replacement}${quote}${close}${dot}${name}`;
100
+ return `${open}${quote}${replacement.specifier}${quote}${close}${dot}${replacement.exportName ?? name}`;
101
101
  });
102
102
  const { text: rewritten, rewrites } = rewriteSpecifiers(text, (spec) => {
103
103
  // Absolute paths: an emitted tree file, else an installed package.
104
104
  if (spec.startsWith('/')) {
105
105
  const target = treeFileFor(spec, args.entryDir, emitted, args.outside);
106
- return target ? relativeSpecifier(file, target) : installed(spec);
106
+ return target ? relativeSpecifier(file, target) : installed(spec)?.specifier;
107
107
  }
108
108
  if (isRelative(spec))
109
109
  return undefined;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Reading a labelled call's result (carrick#2818).
3
+ *
4
+ * A request library's call rarely returns the response body itself: the body
5
+ * sits in a member of what comes back (`data`), behind a parse method
6
+ * (`json()`), or is the whole value. The library store labels which, per
7
+ * operation, and the scanner sends the label on the consumer's infer anchor.
8
+ * This reads the labelled part of the located call's type, so the contract the
9
+ * consumer states is the body and never the library's envelope.
10
+ *
11
+ * The label is one of:
12
+ * - `{ whole: true }`: the awaited type;
13
+ * - `{ member: [m] }`: `m` on every non-nullish constituent of the awaited
14
+ * type, `undefined` and `null` removed from what is read;
15
+ * - `{ parse: p }`: the awaited return of calling `p()`, read off the type as
16
+ * the call returns it (a promise-like with a `json()` method is a thenable,
17
+ * and awaiting it first would drop the method).
18
+ *
19
+ * A label the type cannot satisfy abstains with `result_member_missing`.
20
+ */
21
+ import ts from 'typescript';
22
+ /** The wire form of a result label. Exactly one key is set. */
23
+ export type ResultLabel = {
24
+ whole: true;
25
+ } | {
26
+ member: [string];
27
+ } | {
28
+ parse: string;
29
+ };
30
+ export type ResultRead = {
31
+ type: ts.Type;
32
+ } | {
33
+ abstain: string;
34
+ };
35
+ /** The constituents of a union, or the type alone. */
36
+ export declare function constituentsOf(type: ts.Type): readonly ts.Type[];
37
+ /**
38
+ * Read `label` off a call's type. `raw` is the type as the call returns it and
39
+ * `awaited` the type after the transport unwrap; `at` is the node properties
40
+ * are instantiated at.
41
+ */
42
+ export declare function readResultLabel(checker: ts.TypeChecker, raw: ts.Type, awaited: ts.Type, label: ResultLabel, at: ts.Node): ResultRead;