carrick 0.3.102 → 0.3.103

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 (39) hide show
  1. package/package.json +6 -6
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/sidecar/dist/src/bundler.d.ts +3 -92
  4. package/sidecar/dist/src/bundler.js +5 -265
  5. package/sidecar/dist/src/capture/anchors.js +113 -8
  6. package/sidecar/dist/src/capture/check-fields.js +19 -5
  7. package/sidecar/dist/src/capture/check-probe.d.ts +5 -0
  8. package/sidecar/dist/src/capture/check-probe.js +31 -2
  9. package/sidecar/dist/src/capture/check-workspace.d.ts +3 -0
  10. package/sidecar/dist/src/capture/check-workspace.js +13 -16
  11. package/sidecar/dist/src/capture/check.js +5 -5
  12. package/sidecar/dist/src/capture/deep-walk.js +45 -12
  13. package/sidecar/dist/src/capture/deno-project.d.ts +2 -1
  14. package/sidecar/dist/src/capture/deno-project.js +22 -18
  15. package/sidecar/dist/src/capture/guarded-fs.d.ts +76 -0
  16. package/sidecar/dist/src/capture/guarded-fs.js +182 -0
  17. package/sidecar/dist/src/capture/index.js +60 -26
  18. package/sidecar/dist/src/capture/outside-root.d.ts +41 -0
  19. package/sidecar/dist/src/capture/outside-root.js +101 -0
  20. package/sidecar/dist/src/capture/paths-rewrite.d.ts +8 -0
  21. package/sidecar/dist/src/capture/paths-rewrite.js +9 -7
  22. package/sidecar/dist/src/capture/repair-dangling.d.ts +11 -1
  23. package/sidecar/dist/src/capture/repair-dangling.js +15 -4
  24. package/sidecar/dist/src/capture/self-check.d.ts +3 -0
  25. package/sidecar/dist/src/capture/self-check.js +3 -3
  26. package/sidecar/dist/src/index.d.ts +1 -1
  27. package/sidecar/dist/src/index.js +2 -115
  28. package/sidecar/dist/src/origin.d.ts +49 -8
  29. package/sidecar/dist/src/origin.js +81 -8
  30. package/sidecar/dist/src/project-loader.js +15 -5
  31. package/sidecar/dist/src/type-inferrer.d.ts +72 -0
  32. package/sidecar/dist/src/type-inferrer.js +248 -8
  33. package/sidecar/dist/src/type-structural-expander.d.ts +5 -1
  34. package/sidecar/dist/src/type-structural-expander.js +1 -1
  35. package/sidecar/dist/src/types.d.ts +31 -178
  36. package/sidecar/dist/src/validators.d.ts +58 -914
  37. package/sidecar/dist/src/validators.js +0 -55
  38. package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
  39. package/sidecar/dist/src/monorepo-builder.js +0 -584
@@ -37,17 +37,28 @@ import * as fs from 'node:fs';
37
37
  * `failing` maps an absolute file path to the specifiers the stub could not
38
38
  * resolve from it. Returns the files actually rewritten; a file whose names
39
39
  * cannot all be replaced by `unknown` is left exactly as it was.
40
+ *
41
+ * Only a file `tree` (a guard over the stub's types tree) lets it write is
42
+ * repaired (carrick#1742). The self-check program also loads files the stub
43
+ * merely reaches: the repo's own sources, through a workspace package linked
44
+ * into the `node_modules` the stub borrows, or a runtime's dependency cache.
45
+ * Those report their missing modules exactly as an emitted declaration does,
46
+ * and they belong to the user: a scan reads them and never writes them. The
47
+ * guard decides on the resolved path, so a file reached through the link is
48
+ * the repo's, whatever path the program loaded it by.
40
49
  */
41
- export function repairDanglingImports(failing) {
50
+ export function repairDanglingImports(failing, tree) {
42
51
  const repaired = new Map();
43
52
  for (const [file, specifiers] of failing) {
44
- const result = repairFile(file, specifiers);
53
+ if (!tree.allowsWrite(file))
54
+ continue;
55
+ const result = repairFile(file, specifiers, tree);
45
56
  if (result)
46
57
  repaired.set(file, result);
47
58
  }
48
59
  return repaired;
49
60
  }
50
- function repairFile(file, specifiers) {
61
+ function repairFile(file, specifiers, tree) {
51
62
  let text;
52
63
  try {
53
64
  text = fs.readFileSync(file, 'utf8');
@@ -116,7 +127,7 @@ function repairFile(file, specifiers) {
116
127
  for (const edit of edits) {
117
128
  out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
118
129
  }
119
- fs.writeFileSync(file, out);
130
+ tree.writeFile(file, out);
120
131
  return { specifiers: [...removedSpecifiers].sort(), names: [...names].sort() };
121
132
  }
122
133
  /** The module specifier an import STATEMENT names, if it names one. */
@@ -32,7 +32,10 @@
32
32
  import ts from 'typescript';
33
33
  import type { CaptureAliasRecord } from './api.js';
34
34
  import type { ResolvedAnchor } from './anchors.js';
35
+ import type { WriteGuard } from './guarded-fs.js';
35
36
  export interface SelfCheckArgs {
37
+ /** Writes are held to the stub (carrick#1748). */
38
+ guard: WriteGuard;
36
39
  stubDir: string;
37
40
  surfaceAbsPath: string;
38
41
  resolved: ResolvedAnchor[];
@@ -53,7 +53,7 @@ export function selfCheckStub(args) {
53
53
  const linkPath = path.join(args.stubDir, 'node_modules');
54
54
  let linked = false;
55
55
  if (!args.bareCheckout && fs.existsSync(repoNodeModules) && !fs.existsSync(linkPath)) {
56
- fs.symlinkSync(repoNodeModules, linkPath, 'dir');
56
+ args.guard.symlink(repoNodeModules, linkPath, 'dir');
57
57
  linked = true;
58
58
  }
59
59
  try {
@@ -65,7 +65,7 @@ export function selfCheckStub(args) {
65
65
  // the verdict: it reads the repaired text, and a name the rewrite did not
66
66
  // reach comes back as a `Cannot find name` diagnostic that
67
67
  // `repairedNameFailures` folds back into the same dangling specifier.
68
- const repaired = repairDanglingImports(first.internalFailuresByFile);
68
+ const repaired = repairDanglingImports(first.internalFailuresByFile, args.guard.narrow(typesDir));
69
69
  if (repaired.size === 0)
70
70
  return first.records;
71
71
  return runSelfCheck(args, treeFiles, repaired).records;
@@ -74,7 +74,7 @@ export function selfCheckStub(args) {
74
74
  // unlinkSync, not rmSync: the link target is a directory and rmSync
75
75
  // refuses symlinks-to-directories with EISDIR.
76
76
  if (linked)
77
- fs.unlinkSync(linkPath);
77
+ args.guard.unlink(linkPath);
78
78
  }
79
79
  }
80
80
  function runSelfCheck(args, treeFiles, repaired) {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This module implements a message loop that:
5
5
  * 1. Listens on stdin for JSON requests
6
- * 2. Processes each request (init, bundle, emit_surface, infer, build_workspace, check_compatibility, health, shutdown)
6
+ * 2. Processes each request (see handleRequest for the actions)
7
7
  * 3. Writes JSON responses to stdout
8
8
  *
9
9
  * IMPORTANT:
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This module implements a message loop that:
5
5
  * 1. Listens on stdin for JSON requests
6
- * 2. Processes each request (init, bundle, emit_surface, infer, build_workspace, check_compatibility, health, shutdown)
6
+ * 2. Processes each request (see handleRequest for the actions)
7
7
  * 3. Writes JSON responses to stdout
8
8
  *
9
9
  * IMPORTANT:
@@ -15,9 +15,8 @@ import * as path from 'node:path';
15
15
  import * as readline from 'node:readline';
16
16
  import { parseRequest } from './validators.js';
17
17
  import { ProjectLoader } from './project-loader.js';
18
- import { TypeBundler, SurfaceEmitter } from './bundler.js';
18
+ import { TypeBundler } from './bundler.js';
19
19
  import { TypeInferrer } from './type-inferrer.js';
20
- import { MonorepoBuilder } from './monorepo-builder.js';
21
20
  import { DefinitionResolver } from './definition-resolver.js';
22
21
  import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
23
22
  import { Retyper } from './retype.js';
@@ -26,7 +25,6 @@ import { LibraryClaimsVerifier, httpCheck } from './library-claims.js';
26
25
  // Module-level state
27
26
  // ===========================================================================
28
27
  let projectLoader = null;
29
- let monorepoBuilder = null;
30
28
  let initTimeMs = null;
31
29
  /**
32
30
  * Components by project key (`ProjectLoader.projectKeyFor`): `''` is the
@@ -62,7 +60,6 @@ function projectComponents(key = '') {
62
60
  });
63
61
  built = {
64
62
  typeBundler: new TypeBundler({ project, repoRoot }),
65
- surfaceEmitter: new SurfaceEmitter({ project, repoRoot }),
66
63
  typeInferrer,
67
64
  definitionResolver: new DefinitionResolver({ project }),
68
65
  // Locates calls exactly as `infer` does, so it rewrites the node the
@@ -143,8 +140,6 @@ function handleInit(request) {
143
140
  // The project and everything that reads it are built by the first request
144
141
  // that needs them, so readiness costs the same on a bare checkout as on
145
142
  // one with its dependencies installed (carrick#749).
146
- // Initialize monorepo builder (doesn't need project)
147
- monorepoBuilder = new MonorepoBuilder();
148
143
  initTimeMs = result.initTimeMs || Math.round(performance.now() - startTime);
149
144
  log(`Initialization complete in ${initTimeMs}ms`);
150
145
  return {
@@ -202,38 +197,6 @@ function handleBundle(request) {
202
197
  };
203
198
  }
204
199
  }
205
- /**
206
- * Handle the 'emit_surface' action - emit a surface .d.ts with rewritten specifiers
207
- */
208
- function handleEmitSurface(request) {
209
- try {
210
- log(`Emitting surface for repo '${request.repo_name}' with ${request.payloads.length} payload(s)`);
211
- const result = projectComponents().surfaceEmitter.emit(request.repo_name, request.payloads, request.output_path);
212
- if (!result.success) {
213
- return {
214
- request_id: request.request_id,
215
- status: 'error',
216
- errors: result.errors,
217
- };
218
- }
219
- return {
220
- request_id: request.request_id,
221
- status: 'success',
222
- output_path: result.output_path,
223
- surface_content: result.surface_content,
224
- manifest: result.manifest,
225
- };
226
- }
227
- catch (err) {
228
- const error = err instanceof Error ? err.message : String(err);
229
- logError(`Surface emission failed: ${error}`);
230
- return {
231
- request_id: request.request_id,
232
- status: 'error',
233
- errors: [error],
234
- };
235
- }
236
- }
237
200
  /**
238
201
  * Handle the 'capture_v2' action - v2 "tsc as serializer" capture.
239
202
  * Stateless by design: unlike bundle/infer it needs no init'd ts-morph
@@ -461,76 +424,6 @@ function handleListLibrarySurface(request) {
461
424
  return { request_id: request.request_id, status: 'error', errors: [error] };
462
425
  }
463
426
  }
464
- /**
465
- * Handle the 'build_workspace' action - build synthetic monorepo workspace
466
- */
467
- function handleBuildWorkspace(request) {
468
- // MonorepoBuilder doesn't require project initialization
469
- if (!monorepoBuilder) {
470
- monorepoBuilder = new MonorepoBuilder();
471
- }
472
- try {
473
- log(`Building synthetic workspace with ${request.repos.length} repo(s)`);
474
- const result = monorepoBuilder.build(request.repos, request.workspace_root);
475
- if (!result.success) {
476
- return {
477
- request_id: request.request_id,
478
- status: 'error',
479
- errors: result.errors,
480
- };
481
- }
482
- return {
483
- request_id: request.request_id,
484
- status: 'success',
485
- workspace_path: result.workspace_path,
486
- stub_packages: result.stub_packages,
487
- checker_path: result.checker_path,
488
- };
489
- }
490
- catch (err) {
491
- const error = err instanceof Error ? err.message : String(err);
492
- logError(`Workspace build failed: ${error}`);
493
- return {
494
- request_id: request.request_id,
495
- status: 'error',
496
- errors: [error],
497
- };
498
- }
499
- }
500
- /**
501
- * Handle the 'check_compatibility' action - run type compatibility checks
502
- */
503
- function handleCheckCompatibility(request) {
504
- if (!monorepoBuilder) {
505
- monorepoBuilder = new MonorepoBuilder();
506
- }
507
- try {
508
- log(`Running compatibility checks: ${request.checks.length} check(s)`);
509
- const result = monorepoBuilder.checkCompatibility(request.workspace_root, request.checks);
510
- if (!result.success) {
511
- return {
512
- request_id: request.request_id,
513
- status: 'error',
514
- errors: result.errors,
515
- };
516
- }
517
- return {
518
- request_id: request.request_id,
519
- status: 'success',
520
- results: result.results,
521
- diagnostics: result.diagnostics,
522
- };
523
- }
524
- catch (err) {
525
- const error = err instanceof Error ? err.message : String(err);
526
- logError(`Compatibility check failed: ${error}`);
527
- return {
528
- request_id: request.request_id,
529
- status: 'error',
530
- errors: [error],
531
- };
532
- }
533
- }
534
427
  /**
535
428
  * Handle the 'resolve_definitions' action - resolve surface aliases from a
536
429
  * v2 capture stub package's declaration tree.
@@ -597,16 +490,10 @@ function handleRequest(request) {
597
490
  return handleInit(request);
598
491
  case 'bundle':
599
492
  return handleBundle(request);
600
- case 'emit_surface':
601
- return handleEmitSurface(request);
602
493
  case 'capture_v2':
603
494
  return handleCaptureV2(request);
604
495
  case 'infer':
605
496
  return handleInfer(request);
606
- case 'build_workspace':
607
- return handleBuildWorkspace(request);
608
- case 'check_compatibility':
609
- return handleCheckCompatibility(request);
610
497
  case 'resolve_definitions':
611
498
  return handleResolveDefinitions(request);
612
499
  case 'retype_check':
@@ -13,7 +13,7 @@
13
13
  * bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
14
14
  * rather than by sharing this module.
15
15
  */
16
- import type { ts } from 'ts-morph';
16
+ import type { Project, ResolutionHostFactory, ts } from 'ts-morph';
17
17
  /**
18
18
  * True when a declaration's source file is runtime/library origin rather than
19
19
  * user source. Four answers, in order:
@@ -32,14 +32,55 @@ import type { ts } from 'ts-morph';
32
32
  * types from its own cache, a path with no `node_modules` segment, and
33
33
  * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
34
34
  * the graph, which the compiler records on the file. Nothing crosses the
35
- * capture seam; both programs are built with that host. One exclusion: a
36
- * workspace package reached through a `node_modules` symlink is also
37
- * marked external by the compiler but is the user's own source, so a file
38
- * inside the checkout (the nearest `.git` above the service root) that
39
- * carries no `node_modules` segment stays user source.
35
+ * capture seam; both programs are built with that host. The compiler's
36
+ * record does not survive an edit to a ts-morph project, so `imports`, the
37
+ * resolver's answers kept for the project's life, is read beside it
38
+ * (`ExternalImports`, carrick#1731). One exclusion: a workspace package
39
+ * reached through a `node_modules` symlink is also marked external by the
40
+ * compiler but is the user's own source, so a file inside the checkout
41
+ * (the nearest `.git` above the service root) that carries no
42
+ * `node_modules` segment stays user source.
40
43
  *
41
44
  * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
42
45
  * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
43
- * the pair on a real program.
46
+ * the pair on a real program. The capture builds each program once and never
47
+ * edits it, so it has no `imports` to read.
44
48
  */
45
- export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
49
+ export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string, imports?: ExternalImports): boolean;
50
+ /**
51
+ * Every file a ts-morph project's resolver reached as an external-library
52
+ * import, recorded as the resolver answers (carrick#1731).
53
+ *
54
+ * The compiler keeps that verdict on each program (rule 4 of
55
+ * `isExternalOrigin`), but ts-morph rebuilds the program after any edit to the
56
+ * project, such as a probe file created and removed or a file rewritten and
57
+ * restored, and passes every file it has loaded as a ROOT file. The compiler
58
+ * marks no root file as external, so after the first edit no dependency is.
59
+ * Where a path test cannot answer (a Deno project serves npm types from its own
60
+ * cache), every library type then read as the user's: framework machinery was
61
+ * published as a contract and the printer inlined library types it keeps by
62
+ * name. The resolver's answer for a file does not change with the project's
63
+ * history, so it is kept here for as long as the project lives.
64
+ *
65
+ * The compiler's verdict also carries down: a file a library imports, even by
66
+ * a relative path the resolver answers as not external, is reached while the
67
+ * compiler is inside an external import and is marked external too. A Deno
68
+ * npm package's own relative imports are answered that way, so an answer for
69
+ * an import made from a recorded file is recorded as well.
70
+ */
71
+ export declare class ExternalImports {
72
+ private readonly files;
73
+ /** `factory` as it was, with every answer it gives recorded. */
74
+ recording(factory: ResolutionHostFactory): ResolutionHostFactory;
75
+ /** Whether a resolution reached `fileName` as an external-library import. */
76
+ has(fileName: string): boolean;
77
+ /** Record the external answers to `containingFile`'s imports. */
78
+ private record;
79
+ }
80
+ /** Keep `imports` as the record of `project`, which resolves through it. */
81
+ export declare function registerExternalImports(project: Project, imports: ExternalImports): void;
82
+ /**
83
+ * The record of a project the loader built with a recording resolver, or
84
+ * `undefined` for any other project, which reads the compiler's flag alone.
85
+ */
86
+ export declare function externalImportsOf(project: Project): ExternalImports | undefined;
@@ -33,17 +33,21 @@ import * as path from 'node:path';
33
33
  * types from its own cache, a path with no `node_modules` segment, and
34
34
  * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
35
35
  * the graph, which the compiler records on the file. Nothing crosses the
36
- * capture seam; both programs are built with that host. One exclusion: a
37
- * workspace package reached through a `node_modules` symlink is also
38
- * marked external by the compiler but is the user's own source, so a file
39
- * inside the checkout (the nearest `.git` above the service root) that
40
- * carries no `node_modules` segment stays user source.
36
+ * capture seam; both programs are built with that host. The compiler's
37
+ * record does not survive an edit to a ts-morph project, so `imports`, the
38
+ * resolver's answers kept for the project's life, is read beside it
39
+ * (`ExternalImports`, carrick#1731). One exclusion: a workspace package
40
+ * reached through a `node_modules` symlink is also marked external by the
41
+ * compiler but is the user's own source, so a file inside the checkout
42
+ * (the nearest `.git` above the service root) that carries no
43
+ * `node_modules` segment stays user source.
41
44
  *
42
45
  * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
43
46
  * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
44
- * the pair on a real program.
47
+ * the pair on a real program. The capture builds each program once and never
48
+ * edits it, so it has no `imports` to read.
45
49
  */
46
- export function isExternalOrigin(program, sourceFile, repoRoot) {
50
+ export function isExternalOrigin(program, sourceFile, repoRoot, imports) {
47
51
  const file = sourceFile.fileName.replace(/\\/g, '/');
48
52
  if (file.includes('/.carrick/deno/')) {
49
53
  return true;
@@ -54,7 +58,76 @@ export function isExternalOrigin(program, sourceFile, repoRoot) {
54
58
  if (file.includes('/node_modules/')) {
55
59
  return true;
56
60
  }
57
- return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
61
+ const external = program.isSourceFileFromExternalLibrary(sourceFile) || imports?.has(file) === true;
62
+ return external && !isInsideCheckout(file, repoRoot);
63
+ }
64
+ /**
65
+ * Every file a ts-morph project's resolver reached as an external-library
66
+ * import, recorded as the resolver answers (carrick#1731).
67
+ *
68
+ * The compiler keeps that verdict on each program (rule 4 of
69
+ * `isExternalOrigin`), but ts-morph rebuilds the program after any edit to the
70
+ * project, such as a probe file created and removed or a file rewritten and
71
+ * restored, and passes every file it has loaded as a ROOT file. The compiler
72
+ * marks no root file as external, so after the first edit no dependency is.
73
+ * Where a path test cannot answer (a Deno project serves npm types from its own
74
+ * cache), every library type then read as the user's: framework machinery was
75
+ * published as a contract and the printer inlined library types it keeps by
76
+ * name. The resolver's answer for a file does not change with the project's
77
+ * history, so it is kept here for as long as the project lives.
78
+ *
79
+ * The compiler's verdict also carries down: a file a library imports, even by
80
+ * a relative path the resolver answers as not external, is reached while the
81
+ * compiler is inside an external import and is marked external too. A Deno
82
+ * npm package's own relative imports are answered that way, so an answer for
83
+ * an import made from a recorded file is recorded as well.
84
+ */
85
+ export class ExternalImports {
86
+ files = new Set();
87
+ /** `factory` as it was, with every answer it gives recorded. */
88
+ recording(factory) {
89
+ return (moduleResolutionHost, getCompilerOptions) => {
90
+ const host = factory(moduleResolutionHost, getCompilerOptions);
91
+ return {
92
+ ...host,
93
+ ...(host.resolveModuleNames && {
94
+ resolveModuleNames: (...args) => this.record(args[1], host.resolveModuleNames(...args)),
95
+ }),
96
+ ...(host.resolveTypeReferenceDirectives && {
97
+ resolveTypeReferenceDirectives: (...args) => this.record(args[1], host.resolveTypeReferenceDirectives(...args)),
98
+ }),
99
+ };
100
+ };
101
+ }
102
+ /** Whether a resolution reached `fileName` as an external-library import. */
103
+ has(fileName) {
104
+ return this.files.has(normalised(fileName));
105
+ }
106
+ /** Record the external answers to `containingFile`'s imports. */
107
+ record(containingFile, answers) {
108
+ const fromLibrary = this.has(containingFile);
109
+ for (const answer of answers) {
110
+ if (answer?.resolvedFileName && (fromLibrary || answer.isExternalLibraryImport)) {
111
+ this.files.add(normalised(answer.resolvedFileName));
112
+ }
113
+ }
114
+ return answers;
115
+ }
116
+ }
117
+ function normalised(fileName) {
118
+ return path.resolve(fileName).replace(/\\/g, '/');
119
+ }
120
+ const externalImportsByProject = new WeakMap();
121
+ /** Keep `imports` as the record of `project`, which resolves through it. */
122
+ export function registerExternalImports(project, imports) {
123
+ externalImportsByProject.set(project, imports);
124
+ }
125
+ /**
126
+ * The record of a project the loader built with a recording resolver, or
127
+ * `undefined` for any other project, which reads the compiler's flag alone.
128
+ */
129
+ export function externalImportsOf(project) {
130
+ return externalImportsByProject.get(project);
58
131
  }
59
132
  const checkoutRoots = new Map();
60
133
  /** The checkout the service root sits in: the nearest ancestor holding a
@@ -14,6 +14,7 @@ import * as path from 'node:path';
14
14
  import * as fs from 'node:fs';
15
15
  import { DenoProject, findDenoConfig, serviceConfigPath } from './capture/index.js';
16
16
  import { moduleFormatResolutionHost } from './module-format.js';
17
+ import { ExternalImports, registerExternalImports } from './origin.js';
17
18
  /**
18
19
  * Source-file patterns used when the repo declares no tsconfig, relative to
19
20
  * the repo root. `node_modules` is excluded explicitly: a glob that matches
@@ -190,14 +191,18 @@ export class ProjectLoader {
190
191
  this.buildProject = () => {
191
192
  const deno = new DenoProject(denoConfig, this.repoRoot);
192
193
  this.denoProject = deno;
194
+ // The graph's external-library verdicts, kept past the program
195
+ // rebuilds that drop them (carrick#1731).
196
+ const imports = new ExternalImports();
193
197
  const project = new Project({
194
198
  compilerOptions: deno.parsed.options,
195
199
  skipAddingFilesFromTsConfig: true,
196
- resolutionHost: (host, getOptions) => ({
200
+ resolutionHost: imports.recording((host, getOptions) => ({
197
201
  resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
198
202
  resolveTypeReferenceDirectives: (names, from) => names.map(name => deno.resolveTypeReference(typeof name === 'string' ? name : name.fileName, from, getOptions(), host)),
199
- }),
203
+ })),
200
204
  });
205
+ registerExternalImports(project, imports);
201
206
  for (const file of deno.parsed.fileNames)
202
207
  project.addSourceFileAtPath(file);
203
208
  for (const diagnostic of deno.diagnostics)
@@ -313,12 +318,17 @@ export class ProjectLoader {
313
318
  }
314
319
  /** A ts-morph project built from one tsconfig and the files it lists. */
315
320
  projectFromConfig(configPath) {
316
- return new Project({
321
+ const imports = new ExternalImports();
322
+ const project = new Project({
317
323
  tsConfigFilePath: configPath,
318
324
  skipAddingFilesFromTsConfig: false,
319
- // Each import resolves in its file's own module format (carrick#1619).
320
- resolutionHost: moduleFormatResolutionHost,
325
+ // Each import resolves in its file's own module format (carrick#1619),
326
+ // and every external-library answer is kept past the program rebuilds
327
+ // that drop it (carrick#1731).
328
+ resolutionHost: imports.recording(moduleFormatResolutionHost),
321
329
  });
330
+ registerExternalImports(project, imports);
331
+ return project;
322
332
  }
323
333
  /**
324
334
  * Which project types `file` (carrick#1604): the key of its owning
@@ -182,6 +182,25 @@ export declare class TypeInferrer {
182
182
  */
183
183
  private resolveParamTarget;
184
184
  private inferResponseBody;
185
+ /**
186
+ * True when a located call's own result is the route's payload, so the
187
+ * transitional drill into its first argument must not run (carrick#1732).
188
+ *
189
+ * `res.json(users)` reached that drill because nothing above it recognised
190
+ * the send: its result reads `void`, `any` or `unknown`, and the payload is
191
+ * the argument. `toPublicView(row)` is the opposite case: a mapper building
192
+ * the object the route sends. Its first argument is the row it was built
193
+ * FROM, which carries columns the route never sends.
194
+ *
195
+ * The call's result decides, not where its callee is declared. It is the
196
+ * payload when it reads as one by the rule a response helper's argument is
197
+ * read with (`nodeCarriesPayloadContract`: object-shaped, not machinery,
198
+ * not `void`/`any`/`unknown`) and the object is not a library's own: a
199
+ * codec's writer from `encode(message)` or a reply builder from a send is
200
+ * the library describing itself, and keeps the drill. A library call that
201
+ * returns the repo's own type (`toInstance(View, plain)`) is the payload.
202
+ */
203
+ private callResultIsPayload;
185
204
  /**
186
205
  * The wire representation a request's printed type takes. A route response
187
206
  * is serialised as JSON by every sender this layer reads a payload out of,
@@ -238,6 +257,30 @@ export declare class TypeInferrer {
238
257
  */
239
258
  private receivingCallOf;
240
259
  private inferCallResult;
260
+ /**
261
+ * What the source states the body read at `terminal` to be, when the type
262
+ * `extractExplicitTypeFromAncestor` printed for it is stated AT the read
263
+ * (carrick#1749): the read is the operand of that cast, or the initializer
264
+ * of that annotated declaration, through wrappers that leave a value as it
265
+ * is (parentheses, `await`, `!`, another cast). An annotation further out —
266
+ * the declared type of the function the read sits in — describes something
267
+ * else, and is not reported.
268
+ */
269
+ private statedBodyAtRead;
270
+ /**
271
+ * A stated type with `any` or `unknown` written anywhere in it (`unknown`,
272
+ * `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
273
+ * is a placeholder the source narrows later (`const data: unknown = await
274
+ * res.json()`, then `data as Entry[]`), not its statement of the body.
275
+ */
276
+ private leavesAPositionOpen;
277
+ /**
278
+ * The named root of a stated body type: `Promise<...>` and `PromiseLike`
279
+ * (the language's await protocol), array levels, parentheses, `readonly`
280
+ * and `| null`/`| undefined` are peeled, and what is left is either a type
281
+ * reference, whose name is the root, or anything else, which has none.
282
+ */
283
+ private statedRoot;
241
284
  private inferVariable;
242
285
  private inferExpression;
243
286
  private inferRequestBody;
@@ -361,6 +404,33 @@ export declare class TypeInferrer {
361
404
  * HTTP response is read exactly this way whatever produced the response, so
362
405
  * the shape is structural, not a framework's name.
363
406
  */
407
+ /**
408
+ * The body read inside a chain that starts at `callExpr` (carrick#1749):
409
+ * `request(url).check(ok).mapOk(response => { const data = response as
410
+ * SearchResponse; ... })`.
411
+ *
412
+ * The chain is the run of member calls whose receiver is the call, then
413
+ * that call's result, and so on. A callback passed to one of them whose
414
+ * first parameter the compiler types `unknown` is handed a value nothing
415
+ * has typed yet, which is what a parsed body is; where the source casts
416
+ * that parameter, the cast is what the caller says the body is. The value
417
+ * the chain ends in is what the caller computed from it, and publishing
418
+ * that as the body is the false mismatch this rule exists for.
419
+ *
420
+ * Exactly one such cast across the whole chain answers. None, or more than
421
+ * one (a callback on the failure side can be handed an `unknown` too), and
422
+ * the walk carries on as before. A parameter typed `any` is not read: an
423
+ * unresolved library types every callback that way, success and failure
424
+ * alike, so it says nothing about which one is the body.
425
+ */
426
+ private bodyReadInCallbackChain;
427
+ /**
428
+ * The casts of a callback's first parameter, when the compiler types that
429
+ * parameter `unknown`: `response as T` and `<T>response`, the operand being
430
+ * the parameter itself. A cast that leaves a position open states nothing
431
+ * about the body and is skipped.
432
+ */
433
+ private castsOfUnreadParameter;
364
434
  private bodyReadOnReceiver;
365
435
  private collectDefUseNodes;
366
436
  private expressionUsesNames;
@@ -539,6 +609,8 @@ export declare class TypeInferrer {
539
609
  */
540
610
  private unwrapJsonStringifyArg;
541
611
  private extractExplicitTypeFromAncestor;
612
+ /** The annotation `extractExplicitTypeFromAncestor` prints, as a node. */
613
+ private explicitTypeNodeFromAncestor;
542
614
  /**
543
615
  * Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
544
616
  * fully-structural text.