carrick 0.3.106 → 0.3.108

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 (43) hide show
  1. package/dist/hook/refresh.js +8 -1
  2. package/dist/hook/refresh.js.map +1 -1
  3. package/package.json +6 -6
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/sidecar/dist/src/capture/anchors.d.ts +15 -2
  6. package/sidecar/dist/src/capture/anchors.js +28 -11
  7. package/sidecar/dist/src/capture/api.d.ts +15 -0
  8. package/sidecar/dist/src/capture/check-classify.d.ts +9 -0
  9. package/sidecar/dist/src/capture/check-classify.js +20 -0
  10. package/sidecar/dist/src/capture/check-fields.d.ts +24 -0
  11. package/sidecar/dist/src/capture/check-fields.js +54 -36
  12. package/sidecar/dist/src/capture/check-poison.js +4 -14
  13. package/sidecar/dist/src/capture/check-union.d.ts +40 -0
  14. package/sidecar/dist/src/capture/check-union.js +92 -0
  15. package/sidecar/dist/src/capture/check.js +5 -0
  16. package/sidecar/dist/src/capture/index.js +138 -80
  17. package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
  18. package/sidecar/dist/src/capture/installed-package.js +2 -1
  19. package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
  20. package/sidecar/dist/src/capture/outside-root.js +39 -2
  21. package/sidecar/dist/src/capture/self-check.js +3 -13
  22. package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
  23. package/sidecar/dist/src/capture/specifiers.js +25 -0
  24. package/sidecar/dist/src/definition-resolver.d.ts +5 -8
  25. package/sidecar/dist/src/definition-resolver.js +5 -7
  26. package/sidecar/dist/src/function-line-index.d.ts +30 -0
  27. package/sidecar/dist/src/function-line-index.js +162 -0
  28. package/sidecar/dist/src/index.d.ts +5 -0
  29. package/sidecar/dist/src/index.js +62 -9
  30. package/sidecar/dist/src/infer-timing.d.ts +54 -0
  31. package/sidecar/dist/src/infer-timing.js +124 -0
  32. package/sidecar/dist/src/line-index.d.ts +9 -0
  33. package/sidecar/dist/src/line-index.js +26 -0
  34. package/sidecar/dist/src/progress.d.ts +22 -0
  35. package/sidecar/dist/src/progress.js +31 -0
  36. package/sidecar/dist/src/retype.d.ts +4 -2
  37. package/sidecar/dist/src/retype.js +10 -23
  38. package/sidecar/dist/src/type-inferrer.d.ts +238 -12
  39. package/sidecar/dist/src/type-inferrer.js +772 -168
  40. package/sidecar/dist/src/types.d.ts +83 -6
  41. package/sidecar/dist/src/validators.d.ts +54 -16
  42. package/sidecar/dist/src/validators.js +4 -0
  43. package/templates/skills/carrick-reuse.md +6 -5
@@ -34,13 +34,13 @@ import { entryRelativeSpecifier, resolveAnchor } from './anchors.js';
34
34
  import { findAugmentationFiles } from './augmentations.js';
35
35
  import { installedVersions, lockfileVersions } from './lockfile.js';
36
36
  import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
37
- import { typesPackageOf, withInstalledPackages } from './installed-package.js';
37
+ import { installedPackageSpecifier, typesPackageOf, withInstalledPackages } from './installed-package.js';
38
38
  import { selfCheckStub } from './self-check.js';
39
39
  import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
40
40
  import { DenoProject, findDenoConfig } from './deno-project.js';
41
41
  import { emitsAlike, ProjectGraph } from './project-references.js';
42
42
  import { findServiceTsconfig } from './service-config.js';
43
- import { placeEmittedTree } from './outside-root.js';
43
+ import { placeEmittedTree, surfaceModuleInTree } from './outside-root.js';
44
44
  import { WriteGuard } from './guarded-fs.js';
45
45
  export { DenoProject, findDenoConfig } from './deno-project.js';
46
46
  export { serviceConfigPath } from './project-references.js';
@@ -256,7 +256,8 @@ export function captureStub(opts) {
256
256
  }
257
257
  // ---- Phase A: analysis program over placeholder entry + anchor sources ----
258
258
  let resolved;
259
- const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath, guard };
259
+ const progress = opts.onProgress ?? (() => { });
260
+ const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath, guard, progress };
260
261
  try {
261
262
  resolved = ownerGroups && emitProject
262
263
  ? resolveAnchorsByOwner(opts, ownerGroups, emitProject, analysisCtx, errors)
@@ -277,14 +278,9 @@ export function captureStub(opts) {
277
278
  }
278
279
  const scratch = WriteGuard.scratch('carrick-capture-v2-');
279
280
  const staging = scratch.dir;
280
- const emitted = new Map();
281
- // Input .d.ts files (ambient stubs, augmentation declarations, local
282
- // hand-written declarations in the import closure) are never re-emitted by
283
- // tsc; they must ship verbatim or the tree's references to them dangle.
284
- const declarationSources = new Map();
285
- const sourceByEmitted = new Map();
286
- let emitPartial = false;
281
+ let written;
287
282
  try {
283
+ progress('emit', 'emitting declarations');
288
284
  guard.writeFile(entryPath, entryLines.join('\n') + '\n');
289
285
  const emitOptions = {
290
286
  ...parsed.options,
@@ -300,35 +296,13 @@ export function captureStub(opts) {
300
296
  outDir: staging,
301
297
  rootDir: entryDir,
302
298
  };
303
- const program = ts.createProgram([entryPath, ...augmentationSources], emitOptions, deno?.host(emitOptions));
304
- const emitResult = program.emit(undefined, (fileName, text, _bom, _error, sources) => {
305
- emitted.set(fileName, text);
306
- if (sources?.[0])
307
- sourceByEmitted.set(path.relative(staging, fileName).split(path.sep).join('/'), sources[0].fileName);
308
- }, undefined,
309
- /* emitOnlyDtsFiles */ true);
310
- // emitSkipped is PER-PROGRAM even when only one file's declaration emit
311
- // failed (e.g. TS4023 from a hand-rolled ambient stub shadowing a real
312
- // package): every other file's .d.ts was still written to the callback.
313
- // Fail wholesale only when nothing at all emitted; otherwise keep the
314
- // emitted subset and demote exactly the aliases it cannot support.
315
- if (emitResult.emitSkipped && emitted.size === 0) {
316
- return fail(stubDir, packageName, ['declaration emit was skipped']);
317
- }
318
- emitPartial = emitResult.emitSkipped;
319
- for (const d of emitResult.diagnostics) {
320
- errors.push(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
321
- }
322
- for (const sourceFile of program.getSourceFiles()) {
323
- if (!sourceFile.isDeclarationFile)
324
- continue;
325
- const abs = path.resolve(sourceFile.fileName);
326
- const rel = path.relative(entryDir, abs).split(path.sep).join('/');
327
- if (rel.startsWith('..') || rel.includes('node_modules/'))
328
- continue;
329
- declarationSources.set(rel, sourceFile.getFullText());
330
- sourceByEmitted.set(rel, abs);
331
- }
299
+ written = emitDeclarations({
300
+ rootNames: [entryPath, ...augmentationSources],
301
+ options: emitOptions,
302
+ host: deno?.host(emitOptions),
303
+ staging,
304
+ entryDir,
305
+ });
332
306
  }
333
307
  catch (err) {
334
308
  return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
@@ -338,6 +312,9 @@ export function captureStub(opts) {
338
312
  guard.unlink(entryPath);
339
313
  scratch.guard.remove(staging);
340
314
  }
315
+ const { emitted, declarationSources, sourceByEmitted } = written;
316
+ const emitPartial = written.partial;
317
+ errors.push(...written.diagnostics);
341
318
  // ---- Partial-emit recovery ----
342
319
  // The corpus-2 notifications-svc shape: one file's declaration emit was
343
320
  // skipped but the rest of the tree emitted fine. Keep the tree; demote any
@@ -351,7 +328,14 @@ export function captureStub(opts) {
351
328
  if (emitPartial) {
352
329
  errors.push(`declaration emit was partial: kept ${emitted.size} emitted file(s); ` +
353
330
  'aliases referencing unemitted modules are demoted to structural_fallback');
354
- resolved = demoteDanglingAliases({ resolved, emitted, declarationSources, staging, surfaceDeclaration });
331
+ resolved = demoteDanglingAliases({
332
+ resolved,
333
+ emitted,
334
+ declarationSources,
335
+ staging,
336
+ entryDir,
337
+ surfaceDeclaration,
338
+ });
355
339
  }
356
340
  // ---- Relocate the emitted tree into the stub package ----
357
341
  const typesDir = path.join(stubDir, 'types');
@@ -483,6 +467,7 @@ export function captureStub(opts) {
483
467
  target: parsed.options.target !== undefined ? ts.ScriptTarget[parsed.options.target] : undefined,
484
468
  }, null, 2) + '\n');
485
469
  // ---- Capture-time self-check (per-alias closure attribution) ----
470
+ progress('self-check', 'checking the stub');
486
471
  const aliases = selfCheckStub({
487
472
  guard: stubGuard,
488
473
  stubDir,
@@ -520,43 +505,99 @@ export function captureStub(opts) {
520
505
  };
521
506
  }
522
507
  /**
523
- * Partial-emit demotion: with the set of modules that DID reach the tree
524
- * (emitted .d.ts plus verbatim declaration sources), demote every anchor
525
- * whose alias text references a relative module absent from that set, and
508
+ * Phase B: build the program of the final entry and emit its declarations.
509
+ *
510
+ * A function of its own so that its program dies with it (carrick#1916). The
511
+ * program is as large as the service, and nothing after the emit reads it:
512
+ * what the capture needs is the text returned here. While the emit ran inside
513
+ * `captureStub`, that one long function's frame went on holding the program
514
+ * for as long as the capture ran, so the self-check loaded its own program
515
+ * beside it; on a 2,345-file service that was 1,290 MB of a 2,741 MB peak.
516
+ *
517
+ * The caller builds the options and the host. A host is held by its program
518
+ * and holds none itself, so the caller keeping one keeps no program alive.
519
+ *
520
+ * Throws when the emit was skipped and wrote nothing.
521
+ */
522
+ function emitDeclarations(args) {
523
+ const emitted = new Map();
524
+ const declarationSources = new Map();
525
+ const sourceByEmitted = new Map();
526
+ const program = ts.createProgram(args.rootNames, args.options, args.host);
527
+ const emitResult = program.emit(undefined, (fileName, text, _bom, _error, sources) => {
528
+ emitted.set(fileName, text);
529
+ if (sources?.[0])
530
+ sourceByEmitted.set(path.relative(args.staging, fileName).split(path.sep).join('/'), sources[0].fileName);
531
+ }, undefined,
532
+ /* emitOnlyDtsFiles */ true);
533
+ // emitSkipped is PER-PROGRAM even when only one file's declaration emit
534
+ // failed (e.g. TS4023 from a hand-rolled ambient stub shadowing a real
535
+ // package): every other file's .d.ts was still written to the callback.
536
+ // Fail wholesale only when nothing at all emitted; otherwise keep the
537
+ // emitted subset and demote exactly the aliases it cannot support.
538
+ if (emitResult.emitSkipped && emitted.size === 0) {
539
+ throw new Error('declaration emit was skipped');
540
+ }
541
+ for (const sourceFile of program.getSourceFiles()) {
542
+ if (!sourceFile.isDeclarationFile)
543
+ continue;
544
+ const abs = path.resolve(sourceFile.fileName);
545
+ const rel = path.relative(args.entryDir, abs).split(path.sep).join('/');
546
+ if (rel.startsWith('..') || rel.includes('node_modules/'))
547
+ continue;
548
+ declarationSources.set(rel, sourceFile.getFullText());
549
+ sourceByEmitted.set(rel, abs);
550
+ }
551
+ return {
552
+ emitted,
553
+ declarationSources,
554
+ sourceByEmitted,
555
+ partial: emitResult.emitSkipped,
556
+ diagnostics: emitResult.diagnostics.map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n')),
557
+ };
558
+ }
559
+ /**
560
+ * Partial-emit demotion: demote every anchor whose alias text names, by a
561
+ * relative or an absolute path, a module the stub will not resolve, and
526
562
  * rewrite the demoted aliases' lines in the emitted surface to `unknown`.
563
+ *
564
+ * A path resolves when the tree holds its module (`surfaceModuleInTree`: an
565
+ * emitted declaration inside rootDir or outside it, or a verbatim declaration
566
+ * source), read from where the entry was written, not from where the surface
567
+ * sits in the tree. An absolute path into an installed package resolves too
568
+ * (carrick#1773): no emit writes that module, and the specifier rewrite turns
569
+ * the path into the package's bare specifier and a pin, as it does when the
570
+ * emit is whole. A relative path into a package is not rewritten, so it stays
571
+ * demoted (carrick#1857).
572
+ *
527
573
  * Anchors already demoted stay as they are; anchors whose text is
528
574
  * self-contained (node-builder structural prints, literal object text) are
529
575
  * untouched even when their source file failed to emit — their surface line
530
576
  * references nothing that can dangle.
531
577
  */
532
578
  function demoteDanglingAliases(args) {
533
- // Extensionless, entryDir-relative POSIX module ids present in the tree.
534
- const treeModules = new Set();
535
- let surfaceKey;
536
- for (const fileName of args.emitted.keys()) {
537
- const rel = path.relative(args.staging, fileName).split(path.sep).join('/');
538
- if (path.basename(rel) === args.surfaceDeclaration)
539
- surfaceKey = fileName;
540
- if (rel.endsWith('.d.ts'))
541
- treeModules.add(rel.slice(0, -'.d.ts'.length));
542
- }
543
- for (const rel of args.declarationSources.keys()) {
544
- if (rel.endsWith('.d.ts'))
545
- treeModules.add(rel.slice(0, -'.d.ts'.length));
546
- }
547
- // Deno's temporary surface lives in the cache inside the emitted tree.
548
- const surfaceDir = surfaceKey ? path.posix.dirname(path.relative(args.staging, surfaceKey).split(path.sep).join('/')) : '.';
549
- const moduleInTree = (spec) => {
550
- const id = path.posix.normalize(path.posix.join(surfaceDir, spec));
551
- if (id.startsWith('..'))
552
- return false;
553
- return treeModules.has(id) || treeModules.has(`${id}/index`);
579
+ const surfaceKey = [...args.emitted.keys()].find((fileName) => path.basename(fileName) === args.surfaceDeclaration);
580
+ const inTree = surfaceModuleInTree({
581
+ emitted: args.emitted.keys(),
582
+ declarationSources: args.declarationSources.keys(),
583
+ staging: args.staging,
584
+ entryDir: args.entryDir,
585
+ surfaceDeclaration: args.surfaceDeclaration,
586
+ });
587
+ const resolves = new Map();
588
+ const moduleResolves = (spec) => {
589
+ let answer = resolves.get(spec);
590
+ if (answer === undefined) {
591
+ answer = inTree(spec) || (spec.startsWith('/') && installedPackageSpecifier(spec) !== undefined);
592
+ resolves.set(spec, answer);
593
+ }
594
+ return answer;
554
595
  };
555
596
  const demoted = new Set();
556
597
  const next = args.resolved.map((anchor) => {
557
598
  if (anchor.failureReason !== undefined)
558
599
  return anchor;
559
- const dangling = [...collectSpecifiers(anchor.aliasText)].find((spec) => isRelative(spec) && !moduleInTree(spec));
600
+ const dangling = [...collectSpecifiers(anchor.aliasText)].find((spec) => isRelative(spec) && !moduleResolves(spec));
560
601
  if (dangling === undefined)
561
602
  return anchor;
562
603
  demoted.add(anchor.request.alias);
@@ -644,6 +685,7 @@ function resolveAnchors(opts, parsed, ctx, deno) {
644
685
  }
645
686
  ctx.guard.writeFile(ctx.entryPath, placeholderLines.join('\n') + '\n');
646
687
  try {
688
+ ctx.progress('program', 'building the program the anchors are read in');
647
689
  const anchorSources = [
648
690
  ...new Set(opts.anchors
649
691
  .flatMap((a) => (a.source_file ? [path.join(ctx.repoRoot, a.source_file)] : []))),
@@ -653,6 +695,11 @@ function resolveAnchors(opts, parsed, ctx, deno) {
653
695
  noEmit: true,
654
696
  };
655
697
  const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options, deno?.host(options));
698
+ // The checker binds every file of the program when it is first asked for,
699
+ // which the first anchor would otherwise do: asked for here, so that the
700
+ // report below says the program is whole before any anchor is read.
701
+ program.getTypeChecker();
702
+ ctx.progress('anchors', `0 of ${opts.anchors.length}`);
656
703
  const entrySource = program.getSourceFile(ctx.entryPath);
657
704
  const placeholders = new Map();
658
705
  if (entrySource) {
@@ -661,6 +708,20 @@ function resolveAnchors(opts, parsed, ctx, deno) {
661
708
  placeholders.set(stmt.name.text, stmt);
662
709
  }
663
710
  }
711
+ // A module specifier as the entry resolves it, under the program's own
712
+ // resolution (the Deno graph's, for a Deno service). Asked once per
713
+ // specifier: every anchor of a module asks for the same one.
714
+ const entryMode = entrySource?.impliedNodeFormat;
715
+ const fromEntry = new Map();
716
+ const resolveFromEntry = (specifier) => {
717
+ if (!fromEntry.has(specifier)) {
718
+ fromEntry.set(specifier, (deno
719
+ ? deno.resolve(specifier, ctx.entryPath, options)
720
+ : ts.resolveModuleName(specifier, ctx.entryPath, options, ts.sys, undefined, undefined, entryMode)
721
+ .resolvedModule)?.resolvedFileName);
722
+ }
723
+ return fromEntry.get(specifier);
724
+ };
664
725
  // A literal anchor whose text is a bare identifier resolves through a
665
726
  // sibling symbol anchor's module when one names the same symbol.
666
727
  const siblingSymbolSpecs = new Map();
@@ -668,23 +729,20 @@ function resolveAnchors(opts, parsed, ctx, deno) {
668
729
  if (anchor.kind !== 'symbol')
669
730
  continue;
670
731
  if (!siblingSymbolSpecs.has(anchor.symbol_name)) {
671
- siblingSymbolSpecs.set(anchor.symbol_name, entryRelativeSpecifier(ctx.entryDir, ctx.repoRoot, anchor.source_file));
732
+ siblingSymbolSpecs.set(anchor.symbol_name, entryRelativeSpecifier(ctx.entryDir, ctx.repoRoot, anchor.source_file, resolveFromEntry));
672
733
  }
673
734
  }
674
- // A module specifier as the entry resolves it, under the program's own
675
- // resolution (the Deno graph's, for a Deno service).
676
- const entryMode = entrySource?.impliedNodeFormat;
677
- const resolveFromEntry = (specifier) => (deno
678
- ? deno.resolve(specifier, ctx.entryPath, options)
679
- : ts.resolveModuleName(specifier, ctx.entryPath, options, ts.sys, undefined, undefined, entryMode)
680
- .resolvedModule)?.resolvedFileName;
681
- return opts.anchors.map((request) => resolveAnchor(program, request, {
682
- repoRoot: ctx.repoRoot,
683
- entryDir: ctx.entryDir,
684
- placeholder: placeholders.get(request.alias),
685
- siblingSymbolSpecs,
686
- resolveFromEntry,
687
- }));
735
+ return opts.anchors.map((request, index) => {
736
+ const anchor = resolveAnchor(program, request, {
737
+ repoRoot: ctx.repoRoot,
738
+ entryDir: ctx.entryDir,
739
+ placeholder: placeholders.get(request.alias),
740
+ siblingSymbolSpecs,
741
+ resolveFromEntry,
742
+ });
743
+ ctx.progress('anchors', `${index + 1} of ${opts.anchors.length}`);
744
+ return anchor;
745
+ });
688
746
  }
689
747
  finally {
690
748
  if (fs.existsSync(ctx.entryPath))
@@ -41,6 +41,8 @@ export interface InstalledPackageSpecifier {
41
41
  * deciding whether a runtime name is covered by a pinned types package.
42
42
  */
43
43
  export declare function typesPackageOf(name: string): string;
44
+ /** A path or specifier without its script, source or declaration extension. */
45
+ export declare function withoutExtension(subpath: string): string;
44
46
  /**
45
47
  * A compiler host that also resolves the packages an absolute specifier was
46
48
  * rewritten into, each from its own install directory. The capture's
@@ -101,7 +101,8 @@ function existingFile(spec) {
101
101
  }
102
102
  return undefined;
103
103
  }
104
- function withoutExtension(subpath) {
104
+ /** A path or specifier without its script, source or declaration extension. */
105
+ export function withoutExtension(subpath) {
105
106
  return subpath.replace(/(?:\.d)?\.(?:ts|mts|cts|js|mjs|cjs|tsx|jsx)$/, '');
106
107
  }
107
108
  /** Every string target an exports value names, whatever its conditions. */
@@ -39,3 +39,22 @@ export declare function placeEmittedTree(args: {
39
39
  entryDir: string;
40
40
  surfaceDeclaration: string;
41
41
  }): PlacedTree;
42
+ /**
43
+ * A test for whether the tree holds the module a specifier in the surface
44
+ * entry names (carrick#1773).
45
+ *
46
+ * The specifier is read the way the placement above reads one: resolved from
47
+ * the surface's SOURCE-side directory, so an absolute path stays what it is
48
+ * and `../` leaves rootDir, then looked up among the source-side paths of the
49
+ * declarations the tree holds. Those are every emitted declaration, inside
50
+ * rootDir or placed under `OUTSIDE_DIR`, and the declaration sources shipped
51
+ * verbatim (`declarationSources`, relative to rootDir).
52
+ */
53
+ export declare function surfaceModuleInTree(args: {
54
+ /** tsc's file name for each emitted declaration. */
55
+ emitted: Iterable<string>;
56
+ declarationSources: Iterable<string>;
57
+ staging: string;
58
+ entryDir: string;
59
+ surfaceDeclaration: string;
60
+ }): (spec: string) => boolean;
@@ -15,6 +15,7 @@
15
15
  * A capture whose program stays inside rootDir is placed exactly as before.
16
16
  */
17
17
  import * as path from 'node:path';
18
+ import { withoutExtension } from './installed-package.js';
18
19
  import { rewriteSpecifiers } from './specifiers.js';
19
20
  /** Tree directory holding declarations of sources outside rootDir. */
20
21
  export const OUTSIDE_DIR = '__outside__';
@@ -31,13 +32,12 @@ export function placeEmittedTree(args) {
31
32
  const sourceSideOf = new Map();
32
33
  const outsideFiles = [];
33
34
  for (const fileName of args.emitted.keys()) {
35
+ sourceSideOf.set(fileName, sourceSidePath(fileName, args.staging, args.entryDir));
34
36
  const rel = posix(path.relative(args.staging, fileName));
35
37
  if (escapes(rel)) {
36
38
  outsideFiles.push(fileName);
37
- sourceSideOf.set(fileName, path.resolve(fileName));
38
39
  continue;
39
40
  }
40
- sourceSideOf.set(fileName, path.join(args.entryDir, rel));
41
41
  relOf.set(fileName, path.basename(rel) === args.surfaceDeclaration ? 'surface.d.ts' : rel);
42
42
  }
43
43
  const outside = new Map();
@@ -82,6 +82,43 @@ export function placeEmittedTree(args) {
82
82
  }
83
83
  return { relOf, textOf, outside, rewrites };
84
84
  }
85
+ /**
86
+ * Where tsc would have written an emitted declaration in the source tree. A
87
+ * file under the staging dir mirrors rootDir; one outside it arrived at its
88
+ * source's own path.
89
+ */
90
+ function sourceSidePath(fileName, staging, entryDir) {
91
+ const rel = posix(path.relative(staging, fileName));
92
+ return escapes(rel) ? path.resolve(fileName) : path.join(entryDir, rel);
93
+ }
94
+ /**
95
+ * A test for whether the tree holds the module a specifier in the surface
96
+ * entry names (carrick#1773).
97
+ *
98
+ * The specifier is read the way the placement above reads one: resolved from
99
+ * the surface's SOURCE-side directory, so an absolute path stays what it is
100
+ * and `../` leaves rootDir, then looked up among the source-side paths of the
101
+ * declarations the tree holds. Those are every emitted declaration, inside
102
+ * rootDir or placed under `OUTSIDE_DIR`, and the declaration sources shipped
103
+ * verbatim (`declarationSources`, relative to rootDir).
104
+ */
105
+ export function surfaceModuleInTree(args) {
106
+ const held = new Set();
107
+ let surfaceDir = args.entryDir;
108
+ for (const fileName of args.emitted) {
109
+ const sourceSide = sourceSidePath(fileName, args.staging, args.entryDir);
110
+ if (path.basename(sourceSide) === args.surfaceDeclaration)
111
+ surfaceDir = path.dirname(sourceSide);
112
+ held.add(sourceSide.replace(DECLARATION_EXT, ''));
113
+ }
114
+ for (const rel of args.declarationSources) {
115
+ held.add(path.join(args.entryDir, rel).replace(DECLARATION_EXT, ''));
116
+ }
117
+ return (spec) => {
118
+ const target = path.resolve(surfaceDir, withoutExtension(spec));
119
+ return held.has(target) || held.has(path.join(target, 'index'));
120
+ };
121
+ }
85
122
  function posix(p) {
86
123
  return p.split(path.sep).join('/');
87
124
  }
@@ -32,7 +32,7 @@
32
32
  import ts from 'typescript';
33
33
  import * as fs from 'node:fs';
34
34
  import * as path from 'node:path';
35
- import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
35
+ import { collectSpecifiers, declarationCandidates, isDeclarationFileName, isRelative, packageNameOf, } from './specifiers.js';
36
36
  import { repairDanglingImports } from './repair-dangling.js';
37
37
  import { findDisqualifyingTopTypes, findUnresolvedPlaceholders, isErrorPlaceholder, provenanceOf, unemittedModuleProvenance, unresolvedInTreeProvenance, } from './deep-walk.js';
38
38
  export function selfCheckStub(args) {
@@ -43,7 +43,7 @@ export function selfCheckStub(args) {
43
43
  const p = path.join(dir, entry.name);
44
44
  if (entry.isDirectory())
45
45
  walk(p);
46
- else if (entry.name.endsWith('.d.ts'))
46
+ else if (isDeclarationFileName(entry.name))
47
47
  treeFiles.push(p);
48
48
  }
49
49
  };
@@ -464,15 +464,5 @@ function resolveTreeSpecifier(fromAbs, spec, tree) {
464
464
  if (!isRelative(spec))
465
465
  return undefined;
466
466
  const base = path.resolve(path.dirname(fromAbs), spec);
467
- const candidates = [
468
- `${base}.d.ts`,
469
- path.join(base, 'index.d.ts'),
470
- base.endsWith('.js') ? `${base.slice(0, -3)}.d.ts` : undefined,
471
- base, // already .d.ts
472
- ].filter((c) => c !== undefined);
473
- for (const candidate of candidates) {
474
- if (tree.has(candidate))
475
- return candidate;
476
- }
477
- return undefined;
467
+ return declarationCandidates(base).find((candidate) => tree.has(candidate));
478
468
  }
@@ -4,6 +4,20 @@
4
4
  * pattern matching for the post-emit rewrite pass.
5
5
  */
6
6
  export declare function isRelative(spec: string): boolean;
7
+ /** A declaration file of any module format: `.d.ts`, `.d.mts`, `.d.cts`. */
8
+ export declare function isDeclarationFileName(name: string): boolean;
9
+ /**
10
+ * The declaration files a relative specifier can name in a stub's tree, in
11
+ * the order tried, given the path it resolves to from the file that holds it.
12
+ *
13
+ * A specifier names its module with no extension (`./a`: `a.d.ts`, or the
14
+ * directory's `index.d.ts`), by the file the module compiles to (`./a.js`:
15
+ * `a.d.ts`, `./a.mjs`: `a.d.mts`, `./a.cjs`: `a.d.cts`), or by the
16
+ * declaration file itself. The surface names an anchor's module the second
17
+ * way where the first does not resolve (carrick#1911), and so does any source
18
+ * written for `node16`..`nodenext`.
19
+ */
20
+ export declare function declarationCandidates(resolved: string): string[];
7
21
  /** zod -> zod, @scope/pkg/sub -> @scope/pkg, pkg/sub -> pkg */
8
22
  export declare function packageNameOf(spec: string): string;
9
23
  /** Extract every module specifier mentioned in a .d.ts text: `from "x"`,
@@ -3,9 +3,34 @@
3
3
  * declaration text, external/internal classification, and tsconfig-`paths`
4
4
  * pattern matching for the post-emit rewrite pass.
5
5
  */
6
+ import * as path from 'node:path';
6
7
  export function isRelative(spec) {
7
8
  return spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/');
8
9
  }
10
+ /** A declaration file of any module format: `.d.ts`, `.d.mts`, `.d.cts`. */
11
+ export function isDeclarationFileName(name) {
12
+ return /\.d\.[cm]?ts$/.test(name);
13
+ }
14
+ /**
15
+ * The declaration files a relative specifier can name in a stub's tree, in
16
+ * the order tried, given the path it resolves to from the file that holds it.
17
+ *
18
+ * A specifier names its module with no extension (`./a`: `a.d.ts`, or the
19
+ * directory's `index.d.ts`), by the file the module compiles to (`./a.js`:
20
+ * `a.d.ts`, `./a.mjs`: `a.d.mts`, `./a.cjs`: `a.d.cts`), or by the
21
+ * declaration file itself. The surface names an anchor's module the second
22
+ * way where the first does not resolve (carrick#1911), and so does any source
23
+ * written for `node16`..`nodenext`.
24
+ */
25
+ export function declarationCandidates(resolved) {
26
+ const output = /\.([cm]?)jsx?$/.exec(resolved);
27
+ return [
28
+ `${resolved}.d.ts`,
29
+ path.join(resolved, 'index.d.ts'),
30
+ ...(output ? [`${resolved.slice(0, output.index)}.d.${output[1]}ts`] : []),
31
+ resolved,
32
+ ];
33
+ }
9
34
  /** zod -> zod, @scope/pkg/sub -> @scope/pkg, pkg/sub -> pkg */
10
35
  export function packageNameOf(spec) {
11
36
  const parts = spec.split('/');
@@ -26,11 +26,12 @@
26
26
  * inference path in `type-inferrer.ts`), which walks the resolved `Type` and
27
27
  * rebuilds the inlined text.
28
28
  *
29
- * Each resolve call builds its own throwaway in-memory project over the stub
30
- * tree, so the warm sidecar's long-lived project never sees stub files and
31
- * cannot accumulate stale trees across requests.
29
+ * Each resolve call builds its own throwaway project over the stub tree and
30
+ * reads nothing else: not the service's project, and not whether the sidecar
31
+ * was ever initialised. So the request costs what the stub costs, in a process
32
+ * that has built nothing as in one that has (carrick#1927), and the sidecar's
33
+ * long-lived project never sees stub files.
32
34
  */
33
- import { Project } from 'ts-morph';
34
35
  export interface ResolvedDefinition {
35
36
  type_alias: string;
36
37
  /** Original declaration text as written (preserves named types) */
@@ -39,10 +40,6 @@ export interface ResolvedDefinition {
39
40
  expanded: string;
40
41
  }
41
42
  export declare class DefinitionResolver {
42
- private readonly project;
43
- constructor(options: {
44
- project: Project;
45
- });
46
43
  /**
47
44
  * Resolve surface aliases from a capture stub package directory
48
45
  * (`<stub_dir>/types/surface.d.ts` + its declaration tree).
@@ -26,19 +26,17 @@
26
26
  * inference path in `type-inferrer.ts`), which walks the resolved `Type` and
27
27
  * rebuilds the inlined text.
28
28
  *
29
- * Each resolve call builds its own throwaway in-memory project over the stub
30
- * tree, so the warm sidecar's long-lived project never sees stub files and
31
- * cannot accumulate stale trees across requests.
29
+ * Each resolve call builds its own throwaway project over the stub tree and
30
+ * reads nothing else: not the service's project, and not whether the sidecar
31
+ * was ever initialised. So the request costs what the stub costs, in a process
32
+ * that has built nothing as in one that has (carrick#1927), and the sidecar's
33
+ * long-lived project never sees stub files.
32
34
  */
33
35
  import * as path from 'node:path';
34
36
  import * as fs from 'node:fs';
35
37
  import { Project, Node } from 'ts-morph';
36
38
  import { expandTypeStructural, } from './type-structural-expander.js';
37
39
  export class DefinitionResolver {
38
- project;
39
- constructor(options) {
40
- this.project = options.project;
41
- }
42
40
  /**
43
41
  * Resolve surface aliases from a capture stub package directory
44
42
  * (`<stub_dir>/types/surface.d.ts` + its declaration tree).
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The function a line names, from an index built once per source file
3
+ * (carrick#1915).
4
+ *
5
+ * A signature request carries only the line its function starts on, and a
6
+ * signature pass sends one request per unannotated slot: thousands of
7
+ * lookups, several per function and many per file. Answering each by walking
8
+ * the file's every node cost the file's size per slot, and was 81% of the
9
+ * pass on a 2,345-file program. Here the file is walked once, over the
10
+ * compiler's own nodes, and each lookup reads the few entries near its line.
11
+ *
12
+ * The index belongs to the compiler's source file node, not to the file's
13
+ * name: replacing a file's text gives it a new node, so a file rewritten for
14
+ * one reading (the unwidened reading, the retype check) and then restored is
15
+ * indexed again each time, and never answered from positions it no longer has.
16
+ */
17
+ import { type ArrowFunction, type FunctionDeclaration, type FunctionExpression, type MethodDeclaration, type SourceFile } from 'ts-morph';
18
+ /** The declarations a line can name. */
19
+ export type LineFunction = FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration;
20
+ /**
21
+ * The function whose declaration starts at, or within `LINE_TOLERANCE` lines
22
+ * of, the given line: the closest, then the smallest, then the first in the
23
+ * file. A function starting after the line is passed over when a statement
24
+ * that opens on the line or after it, on a line before the function's, does
25
+ * not contain the function. Undefined when no function is left.
26
+ *
27
+ * These are `TypeInferrer.findFunctionByLine`'s rules, which says why each is
28
+ * there; this is where they are computed.
29
+ */
30
+ export declare function functionAtLine(sourceFile: SourceFile, line: number): LineFunction | undefined;