carrick 0.3.97 → 0.3.99

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 (58) hide show
  1. package/bin/carrick.mjs +19 -14
  2. package/dist/auth/run.d.ts +1 -1
  3. package/dist/auth/run.js +1 -1
  4. package/dist/contract.d.ts +15 -0
  5. package/dist/contract.js.map +1 -1
  6. package/dist/global-install.js +6 -5
  7. package/dist/global-install.js.map +1 -1
  8. package/dist/hook/reuse.d.ts +8 -3
  9. package/dist/hook/reuse.js +15 -3
  10. package/dist/hook/reuse.js.map +1 -1
  11. package/dist/init/connect.js +1 -1
  12. package/dist/init/connect.js.map +1 -1
  13. package/dist/init/doctor.js +5 -2
  14. package/dist/init/doctor.js.map +1 -1
  15. package/dist/init/hosted.d.ts +18 -1
  16. package/dist/init/hosted.js +84 -16
  17. package/dist/init/hosted.js.map +1 -1
  18. package/dist/init/install-id.d.ts +2 -2
  19. package/dist/init/install-id.js +3 -3
  20. package/dist/init/install-id.js.map +1 -1
  21. package/dist/init/mcp.d.ts +2 -2
  22. package/dist/init/mcp.js +2 -2
  23. package/dist/init/mcp.js.map +1 -1
  24. package/dist/init/outdated.d.ts +1 -1
  25. package/dist/init/outdated.js +1 -1
  26. package/dist/init/outdated.js.map +1 -1
  27. package/dist/init/output.d.ts +18 -1
  28. package/dist/init/output.js +40 -1
  29. package/dist/init/output.js.map +1 -1
  30. package/dist/init/remove.d.ts +71 -2
  31. package/dist/init/remove.js +474 -145
  32. package/dist/init/remove.js.map +1 -1
  33. package/dist/init/repo-copies.d.ts +16 -4
  34. package/dist/init/repo-copies.js +20 -6
  35. package/dist/init/repo-copies.js.map +1 -1
  36. package/dist/init/run.d.ts +12 -16
  37. package/dist/init/run.js +25 -42
  38. package/dist/init/run.js.map +1 -1
  39. package/dist/update.js +1 -0
  40. package/dist/update.js.map +1 -1
  41. package/package.json +6 -6
  42. package/plugin/.claude-plugin/plugin.json +1 -1
  43. package/sidecar/dist/src/bundler.d.ts +2 -0
  44. package/sidecar/dist/src/bundler.js +115 -39
  45. package/sidecar/dist/src/capture/index.d.ts +1 -0
  46. package/sidecar/dist/src/capture/index.js +91 -10
  47. package/sidecar/dist/src/capture/project-references.d.ts +67 -0
  48. package/sidecar/dist/src/capture/project-references.js +157 -0
  49. package/sidecar/dist/src/client-semantics.d.ts +36 -0
  50. package/sidecar/dist/src/client-semantics.js +1008 -0
  51. package/sidecar/dist/src/index.js +110 -10
  52. package/sidecar/dist/src/module-format.d.ts +32 -0
  53. package/sidecar/dist/src/module-format.js +102 -0
  54. package/sidecar/dist/src/project-loader.d.ts +24 -0
  55. package/sidecar/dist/src/project-loader.js +53 -5
  56. package/sidecar/dist/src/types.d.ts +86 -4
  57. package/sidecar/dist/src/validators.d.ts +463 -0
  58. package/sidecar/dist/src/validators.js +50 -0
@@ -11,6 +11,7 @@
11
11
  * - stderr is for logging
12
12
  * - Process stays alive between requests (warm standby)
13
13
  */
14
+ import * as path from 'node:path';
14
15
  import * as readline from 'node:readline';
15
16
  import { parseRequest } from './validators.js';
16
17
  import { ProjectLoader } from './project-loader.js';
@@ -20,29 +21,36 @@ import { MonorepoBuilder } from './monorepo-builder.js';
20
21
  import { DefinitionResolver } from './definition-resolver.js';
21
22
  import { captureStub, findDisqualifyingTopTypes, jsonWireDeclarations, runCheck, } from './capture/index.js';
22
23
  import { Retyper } from './retype.js';
24
+ import { ClientSemanticsVerifier } from './client-semantics.js';
23
25
  // ===========================================================================
24
26
  // Module-level state
25
27
  // ===========================================================================
26
28
  let projectLoader = null;
27
29
  let monorepoBuilder = null;
28
30
  let initTimeMs = null;
29
- let components = null;
31
+ /**
32
+ * Components by project key (`ProjectLoader.projectKeyFor`): `''` is the
33
+ * default project, any other key the program of a project that owns some of
34
+ * the service's files (carrick#1604).
35
+ */
36
+ let components = new Map();
30
37
  /**
31
38
  * Get the project-backed components, building the project if this is the
32
39
  * first request that needs it.
33
40
  *
34
41
  * @throws if init has not run, or if the project cannot be built
35
42
  */
36
- function projectComponents() {
43
+ function projectComponents(key = '') {
37
44
  if (!projectLoader?.isInitialized()) {
38
45
  throw new Error('Sidecar not initialized. Call init first.');
39
46
  }
40
- if (!components) {
47
+ let built = components.get(key);
48
+ if (!built) {
41
49
  // Bound here rather than read from the module slot inside the components:
42
50
  // a re-init drops `components` and points the slot at another service, and
43
51
  // nothing built over this project may follow it there.
44
52
  const loader = projectLoader;
45
- const project = loader.getProject();
53
+ const project = loader.getProjectFor(key);
46
54
  const repoRoot = loader.getRepoRoot();
47
55
  // The module graph, where the project resolved through one, is the only
48
56
  // thing that can name the package a file belongs to: a Deno service
@@ -52,7 +60,7 @@ function projectComponents() {
52
60
  repoRoot,
53
61
  packageOf: (filePath) => loader.packageOf(filePath),
54
62
  });
55
- components = {
63
+ built = {
56
64
  typeBundler: new TypeBundler({ project, repoRoot }),
57
65
  surfaceEmitter: new SurfaceEmitter({ project, repoRoot }),
58
66
  typeInferrer,
@@ -66,9 +74,43 @@ function projectComponents() {
66
74
  // (`npm/carrick/package.json`) resolves `typescript ^5.8.0` unpinned,
67
75
  // so an install can pair ts-morph's copy with a newer 5.x.
68
76
  retyper: new Retyper(project, typeInferrer, jsonWireDeclarations, findDisqualifyingTopTypes),
77
+ semanticsVerifier: new ClientSemanticsVerifier(project),
69
78
  };
79
+ components.set(key, built);
80
+ }
81
+ return built;
82
+ }
83
+ /** One bundle answer from the answers of several programs. */
84
+ function mergeBundles(results) {
85
+ const symbol_failures = results.flatMap((r) => r.symbol_failures ?? []);
86
+ const answered = results.filter((r) => r.success);
87
+ if (answered.length === 0) {
88
+ return {
89
+ success: false,
90
+ symbol_failures,
91
+ errors: [...new Set(results.flatMap((r) => r.errors ?? []))],
92
+ };
93
+ }
94
+ return {
95
+ success: true,
96
+ dts_content: answered.map((r) => r.dts_content ?? '').join('\n'),
97
+ manifest: answered.flatMap((r) => r.manifest ?? []),
98
+ symbol_failures: symbol_failures.length > 0 ? symbol_failures : undefined,
99
+ };
100
+ }
101
+ /**
102
+ * Split a request's items by the project that types their file, keeping each
103
+ * group in request order. One group, the default project's, for a service
104
+ * whose tsconfig references nothing.
105
+ */
106
+ function byProject(items, fileOf) {
107
+ // An empty request is still answered by the default project, as before.
108
+ const groups = new Map(items.length === 0 ? [['', []]] : []);
109
+ for (const item of items) {
110
+ const key = projectLoader?.projectKeyFor(fileOf(item)) ?? '';
111
+ groups.set(key, [...(groups.get(key) ?? []), item]);
70
112
  }
71
- return components;
113
+ return groups;
72
114
  }
73
115
  // ===========================================================================
74
116
  // Request Handlers
@@ -82,7 +124,7 @@ function handleInit(request) {
82
124
  log(`Initializing with repo_root: ${request.repo_root}`);
83
125
  // Re-init re-scopes the sidecar to another root: drop everything built
84
126
  // over the previous project before resolving the new one.
85
- components = null;
127
+ components = new Map();
86
128
  projectLoader = new ProjectLoader({
87
129
  repoRoot: request.repo_root,
88
130
  tsconfigPath: request.tsconfig_path,
@@ -128,7 +170,10 @@ function handleInit(request) {
128
170
  function handleBundle(request) {
129
171
  try {
130
172
  log(`Bundling ${request.symbols.length} symbol(s)`);
131
- const result = projectComponents().typeBundler.bundle(request.symbols);
173
+ // A symbol named by a path is bundled from the program of the project
174
+ // that owns that file (carrick#1604).
175
+ const results = [...byProject(request.symbols, (symbol) => symbol.source_file)].map(([key, symbols]) => projectComponents(key).typeBundler.bundle(symbols));
176
+ const result = results.length === 1 ? results[0] : mergeBundles(results);
132
177
  if (!result.success) {
133
178
  return {
134
179
  request_id: request.request_id,
@@ -274,7 +319,18 @@ async function handleCheckV2Async(request) {
274
319
  function handleInfer(request) {
275
320
  try {
276
321
  log(`Inferring ${request.requests.length} type(s)`);
277
- const result = projectComponents().typeInferrer.infer(request.requests, request.extraction_config);
322
+ const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config));
323
+ const result = results.length === 1
324
+ ? results[0]
325
+ : (() => {
326
+ const inferred_types = results.flatMap((r) => r.inferred_types ?? []);
327
+ const errors = results.flatMap((r) => r.errors ?? []);
328
+ return {
329
+ success: errors.length === 0 || inferred_types.length > 0,
330
+ inferred_types,
331
+ errors: errors.length > 0 ? errors : undefined,
332
+ };
333
+ })();
278
334
  return {
279
335
  request_id: request.request_id,
280
336
  status: result.success ? 'success' : 'error',
@@ -304,7 +360,19 @@ const RETYPE_BUDGET_MS = 600_000;
304
360
  function handleRetypeCheck(request) {
305
361
  try {
306
362
  log(`Retyping ${request.items.length} consumer call(s)`);
307
- const outcomes = projectComponents().retyper.run(request.items, request.budget_ms ?? RETYPE_BUDGET_MS);
363
+ const budget = request.budget_ms ?? RETYPE_BUDGET_MS;
364
+ const groups = byProject(request.items.map((item, index) => ({ item, index })), ({ item }) => item.file_path);
365
+ const deadline = performance.now() + budget;
366
+ const outcomes = new Array(request.items.length);
367
+ for (const [key, entries] of groups) {
368
+ // One budget for the request, whichever programs it spans.
369
+ const remaining = groups.size === 1 ? budget : Math.max(0, deadline - performance.now());
370
+ projectComponents(key)
371
+ .retyper.run(entries.map(({ item }) => item), remaining)
372
+ .forEach((outcome, position) => {
373
+ outcomes[entries[position].index] = outcome;
374
+ });
375
+ }
308
376
  return { request_id: request.request_id, status: 'success', outcomes };
309
377
  }
310
378
  catch (err) {
@@ -313,6 +381,36 @@ function handleRetypeCheck(request) {
313
381
  return { request_id: request.request_id, status: 'error', errors: [error] };
314
382
  }
315
383
  }
384
+ /**
385
+ * How long one verify_client_semantics request may spend before the checks it
386
+ * has not reached come back `unchecked` with reason `budget`. The first request
387
+ * on a service may pay the program build, which counts against it; the checks
388
+ * themselves are cheap. Well inside the scanner's 900s read deadline.
389
+ */
390
+ const SEMANTICS_BUDGET_MS = 600_000;
391
+ /**
392
+ * Handle the 'verify_client_semantics' action - check library-semantics
393
+ * claims against each package's type declarations (carrick#1564)
394
+ */
395
+ function handleVerifyClientSemantics(request) {
396
+ try {
397
+ const { semanticsVerifier } = projectComponents();
398
+ const fromDir = path.resolve(projectLoader.getRepoRoot(), request.from_dir);
399
+ log(`Verifying ${request.checks.length} client-semantics claim(s) from ${fromDir}`);
400
+ const { semantics, modules } = semanticsVerifier.run(fromDir, request.checks, request.budget_ms ?? SEMANTICS_BUDGET_MS);
401
+ return {
402
+ request_id: request.request_id,
403
+ status: 'success',
404
+ semantics,
405
+ semantics_modules: modules,
406
+ };
407
+ }
408
+ catch (err) {
409
+ const error = err instanceof Error ? err.message : String(err);
410
+ logError(`Client-semantics check failed: ${error}`);
411
+ return { request_id: request.request_id, status: 'error', errors: [error] };
412
+ }
413
+ }
316
414
  /**
317
415
  * Handle the 'build_workspace' action - build synthetic monorepo workspace
318
416
  */
@@ -463,6 +561,8 @@ function handleRequest(request) {
463
561
  return handleResolveDefinitions(request);
464
562
  case 'retype_check':
465
563
  return handleRetypeCheck(request);
564
+ case 'verify_client_semantics':
565
+ return handleVerifyClientSemantics(request);
466
566
  case 'health':
467
567
  return handleHealth(request);
468
568
  case 'shutdown':
@@ -0,0 +1,32 @@
1
+ /**
2
+ * How the init'd project resolves an import: in the mode the compiler picks
3
+ * for the file that imports it (carrick#1619).
4
+ *
5
+ * Under `module` `node16`..`nodenext` the compiler reads each file's own
6
+ * format (its nearest `package.json` `"type"`, or its extension) to decide
7
+ * whether an import resolves with the `import` or the `require` export
8
+ * conditions. The program asks its host to create each file with that format,
9
+ * but ts-morph's host creates every file with the script target only, so no
10
+ * file had a format and every import resolved as a `require`. A package whose
11
+ * `exports` entry only has an `import` branch did not resolve at all, while
12
+ * the capture, which uses the compiler directly, resolved the same import.
13
+ *
14
+ * This resolution host gives the importing file its format before its imports
15
+ * are resolved, then resolves each import as the compiler's default does:
16
+ * `ts.resolveModuleName` in the mode `ts.getModeForUsageLocation` reads off
17
+ * that import. The format is only set under `node16`..`nodenext`. Under other
18
+ * `module` settings the compiler still takes an import's mode from its syntax
19
+ * (`import`, `require`, a `resolution-mode` attribute) and reads a file's
20
+ * format only in narrow cases; setting it there made answers worse, because
21
+ * ts-morph's TypeScript copy shares one resolution-cache entry across modes
22
+ * under Bundler (carrick#1632), so those settings keep their old modes.
23
+ *
24
+ * The hook receives one entry per import, by module name, so one name
25
+ * imported twice in different modes (a `require` beside an `import`) needs
26
+ * each entry matched to its own import. It is, by position, when every import
27
+ * of the name is being resolved; when only some of them are (the compiler
28
+ * reusing earlier answers) and their modes differ, the entry cannot be placed
29
+ * and is left unresolved rather than given another import's mode.
30
+ */
31
+ import { type ResolutionHostFactory } from 'ts-morph';
32
+ export declare const moduleFormatResolutionHost: ResolutionHostFactory;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * How the init'd project resolves an import: in the mode the compiler picks
3
+ * for the file that imports it (carrick#1619).
4
+ *
5
+ * Under `module` `node16`..`nodenext` the compiler reads each file's own
6
+ * format (its nearest `package.json` `"type"`, or its extension) to decide
7
+ * whether an import resolves with the `import` or the `require` export
8
+ * conditions. The program asks its host to create each file with that format,
9
+ * but ts-morph's host creates every file with the script target only, so no
10
+ * file had a format and every import resolved as a `require`. A package whose
11
+ * `exports` entry only has an `import` branch did not resolve at all, while
12
+ * the capture, which uses the compiler directly, resolved the same import.
13
+ *
14
+ * This resolution host gives the importing file its format before its imports
15
+ * are resolved, then resolves each import as the compiler's default does:
16
+ * `ts.resolveModuleName` in the mode `ts.getModeForUsageLocation` reads off
17
+ * that import. The format is only set under `node16`..`nodenext`. Under other
18
+ * `module` settings the compiler still takes an import's mode from its syntax
19
+ * (`import`, `require`, a `resolution-mode` attribute) and reads a file's
20
+ * format only in narrow cases; setting it there made answers worse, because
21
+ * ts-morph's TypeScript copy shares one resolution-cache entry across modes
22
+ * under Bundler (carrick#1632), so those settings keep their old modes.
23
+ *
24
+ * The hook receives one entry per import, by module name, so one name
25
+ * imported twice in different modes (a `require` beside an `import`) needs
26
+ * each entry matched to its own import. It is, by position, when every import
27
+ * of the name is being resolved; when only some of them are (the compiler
28
+ * reusing earlier answers) and their modes differ, the entry cannot be placed
29
+ * and is left unresolved rather than given another import's mode.
30
+ */
31
+ import { ts } from 'ts-morph';
32
+ /** Whether the compiler decides an import's mode from the importing file's format. */
33
+ function formatDecidesMode(options) {
34
+ const kind = options.module;
35
+ return kind !== undefined && kind >= ts.ModuleKind.Node16 && kind <= ts.ModuleKind.NodeNext;
36
+ }
37
+ /**
38
+ * The literals in `file` the program resolves, by module name and in source
39
+ * order: its imports, then its module augmentations. Neither list is in the
40
+ * public declaration file.
41
+ */
42
+ function usagesOf(file) {
43
+ const internal = file;
44
+ const usages = new Map();
45
+ const add = (literal) => {
46
+ usages.set(literal.text, [...(usages.get(literal.text) ?? []), literal]);
47
+ };
48
+ for (const literal of internal.imports ?? [])
49
+ add(literal);
50
+ for (const literal of internal.moduleAugmentations ?? []) {
51
+ if (ts.isStringLiteral(literal))
52
+ add(literal);
53
+ }
54
+ return usages;
55
+ }
56
+ /** Why a module name cannot be given one mode: see the header. */
57
+ const UNPLACED = Symbol('unplaced');
58
+ export const moduleFormatResolutionHost = (host, getOptions) => {
59
+ let cache;
60
+ let cacheOptions;
61
+ const cacheFor = (options) => {
62
+ if (!cache || cacheOptions !== options) {
63
+ cache = ts.createModuleResolutionCache(process.cwd(), (fileName) => fileName, options);
64
+ cacheOptions = options;
65
+ }
66
+ return cache;
67
+ };
68
+ return {
69
+ resolveModuleNames: (moduleNames, containingFile, _reusedNames, redirectedReference, options, containingSourceFile) => {
70
+ const compilerOptions = options ?? getOptions();
71
+ const resolutionCache = cacheFor(compilerOptions);
72
+ if (containingSourceFile &&
73
+ containingSourceFile.impliedNodeFormat === undefined &&
74
+ formatDecidesMode(compilerOptions)) {
75
+ containingSourceFile.impliedNodeFormat =
76
+ ts.getImpliedNodeFormatForFile(containingFile, resolutionCache.getPackageJsonInfoCache(), host, compilerOptions);
77
+ }
78
+ const usages = containingSourceFile ? usagesOf(containingSourceFile) : undefined;
79
+ const asked = new Map();
80
+ for (const name of moduleNames)
81
+ asked.set(name, (asked.get(name) ?? 0) + 1);
82
+ const seen = new Map();
83
+ const modeOf = (name) => {
84
+ const occurrence = seen.get(name) ?? 0;
85
+ seen.set(name, occurrence + 1);
86
+ const literals = usages?.get(name) ?? [];
87
+ if (!containingSourceFile || literals.length === 0)
88
+ return undefined;
89
+ const modes = literals.map((literal) => ts.getModeForUsageLocation(containingSourceFile, literal, compilerOptions));
90
+ if (new Set(modes).size === 1)
91
+ return modes[0];
92
+ return asked.get(name) === literals.length ? modes[occurrence] : UNPLACED;
93
+ };
94
+ return moduleNames.map((name) => {
95
+ const mode = modeOf(name);
96
+ if (mode === UNPLACED)
97
+ return undefined;
98
+ return ts.resolveModuleName(name, containingFile, compilerOptions, host, resolutionCache, redirectedReference, mode).resolvedModule;
99
+ });
100
+ },
101
+ };
102
+ };
@@ -61,6 +61,12 @@ export declare class ProjectLoader {
61
61
  * build, so it is there from the first `getProject()` onwards.
62
62
  */
63
63
  private denoProject;
64
+ /** The tsconfig the service names, when the project is built from one. */
65
+ private namedTsconfigPath;
66
+ /** Each file's owning config (carrick#1604), resolved on first use. */
67
+ private configPathFor;
68
+ /** Programs built for owning projects other than the named tsconfig. */
69
+ private readonly ownerProjects;
64
70
  private readonly repoRoot;
65
71
  private readonly tsconfigPath;
66
72
  private readonly tsconfigSnapshot;
@@ -82,6 +88,24 @@ export declare class ProjectLoader {
82
88
  * Convert a TsconfigSnapshot to ts-morph CompilerOptions
83
89
  */
84
90
  private snapshotToCompilerOptions;
91
+ /** A ts-morph project built from one tsconfig and the files it lists. */
92
+ private projectFromConfig;
93
+ /**
94
+ * Which project types `file` (carrick#1604): the key of its owning
95
+ * project's program, or `''` for the default one. A tsconfig that
96
+ * references other projects owns only the files it lists itself; any other
97
+ * file belongs to the first project it references (depth-first, in
98
+ * declared order) whose file list includes it, and is typed under that
99
+ * project's options. A file no project includes, and every request that
100
+ * names no file, uses the default project, built from the named tsconfig
101
+ * as before.
102
+ */
103
+ projectKeyFor(file?: string): string;
104
+ /**
105
+ * The project behind a key from `projectKeyFor`, built on first use and
106
+ * kept: a service whose files all belong to one project builds one.
107
+ */
108
+ getProjectFor(key: string): Project;
85
109
  /**
86
110
  * Get the ts-morph Project, building it on first call.
87
111
  *
@@ -12,7 +12,8 @@
12
12
  import { Project } from 'ts-morph';
13
13
  import * as path from 'node:path';
14
14
  import * as fs from 'node:fs';
15
- import { DenoProject, findDenoConfig } from './capture/index.js';
15
+ import { DenoProject, findDenoConfig, serviceConfigPath } from './capture/index.js';
16
+ import { moduleFormatResolutionHost } from './module-format.js';
16
17
  /**
17
18
  * Source-file patterns used when the repo declares no tsconfig, relative to
18
19
  * the repo root. `node_modules` is excluded explicitly: a glob that matches
@@ -115,6 +116,12 @@ export class ProjectLoader {
115
116
  * build, so it is there from the first `getProject()` onwards.
116
117
  */
117
118
  denoProject;
119
+ /** The tsconfig the service names, when the project is built from one. */
120
+ namedTsconfigPath;
121
+ /** Each file's owning config (carrick#1604), resolved on first use. */
122
+ configPathFor;
123
+ /** Programs built for owning projects other than the named tsconfig. */
124
+ ownerProjects = new Map();
118
125
  repoRoot;
119
126
  tsconfigPath;
120
127
  tsconfigSnapshot;
@@ -200,10 +207,8 @@ export class ProjectLoader {
200
207
  }
201
208
  else if (tsconfigPath) {
202
209
  this.log(`Project will load with tsconfig: ${tsconfigPath}`);
203
- this.buildProject = () => new Project({
204
- tsConfigFilePath: tsconfigPath,
205
- skipAddingFilesFromTsConfig: false,
206
- });
210
+ this.namedTsconfigPath = tsconfigPath;
211
+ this.buildProject = () => this.projectFromConfig(tsconfigPath);
207
212
  }
208
213
  else {
209
214
  this.log('No tsconfig.json found, using default compiler options');
@@ -306,6 +311,49 @@ export class ProjectLoader {
306
311
  }
307
312
  return result;
308
313
  }
314
+ /** A ts-morph project built from one tsconfig and the files it lists. */
315
+ projectFromConfig(configPath) {
316
+ return new Project({
317
+ tsConfigFilePath: configPath,
318
+ skipAddingFilesFromTsConfig: false,
319
+ // Each import resolves in its file's own module format (carrick#1619).
320
+ resolutionHost: moduleFormatResolutionHost,
321
+ });
322
+ }
323
+ /**
324
+ * Which project types `file` (carrick#1604): the key of its owning
325
+ * project's program, or `''` for the default one. A tsconfig that
326
+ * references other projects owns only the files it lists itself; any other
327
+ * file belongs to the first project it references (depth-first, in
328
+ * declared order) whose file list includes it, and is typed under that
329
+ * project's options. A file no project includes, and every request that
330
+ * names no file, uses the default project, built from the named tsconfig
331
+ * as before.
332
+ */
333
+ projectKeyFor(file) {
334
+ if (!this.namedTsconfigPath || file === undefined)
335
+ return '';
336
+ this.configPathFor ??= serviceConfigPath(this.namedTsconfigPath, (d) => this.logError(d));
337
+ const config = this.configPathFor(path.resolve(this.repoRoot, file));
338
+ return config === this.namedTsconfigPath ? '' : config;
339
+ }
340
+ /**
341
+ * The project behind a key from `projectKeyFor`, built on first use and
342
+ * kept: a service whose files all belong to one project builds one.
343
+ */
344
+ getProjectFor(key) {
345
+ if (key === '')
346
+ return this.getProject();
347
+ let project = this.ownerProjects.get(key);
348
+ if (!project) {
349
+ const startTime = performance.now();
350
+ project = this.projectFromConfig(key);
351
+ this.ownerProjects.set(key, project);
352
+ this.log(`Project for ${key}, the project that owns the requested files, built in ` +
353
+ `${Math.round(performance.now() - startTime)}ms (${project.getSourceFiles().length} source files)`);
354
+ }
355
+ return project;
356
+ }
309
357
  /**
310
358
  * Get the ts-morph Project, building it on first call.
311
359
  *
@@ -320,10 +320,64 @@ export interface RetypeCheckRequest extends BaseRequest {
320
320
  /** Time the request may spend; items it does not reach abstain. */
321
321
  budget_ms?: number;
322
322
  }
323
+ /**
324
+ * One library-semantics claim to check against the package's own type
325
+ * declarations, on one receiver (carrick#1564). The unit of verification is
326
+ * the pair `(claim_id, receiver)`.
327
+ */
328
+ export interface SemanticsCheck {
329
+ claim_id: string;
330
+ /** Module specifier, resolved from the request's `from_dir`. */
331
+ package: string;
332
+ /** `"default"` or a named export. */
333
+ export: string;
334
+ /** `"export"` or `"instance:<factory member>"`. */
335
+ receiver: string;
336
+ claim: {
337
+ kind: 'factory';
338
+ member: string;
339
+ base_url_key: string;
340
+ } | {
341
+ kind: 'verb';
342
+ member: string;
343
+ method: string;
344
+ } | {
345
+ kind: 'verb_body';
346
+ member: string;
347
+ args: 'path_body' | 'path_options';
348
+ body_key?: string;
349
+ } | {
350
+ kind: 'request';
351
+ member: string | null;
352
+ args: 'config' | 'path_options';
353
+ url_key?: string;
354
+ method_key: string;
355
+ } | {
356
+ kind: 'request_body';
357
+ member: string | null;
358
+ args: 'config' | 'path_options';
359
+ url_key?: string;
360
+ method_key: string;
361
+ body_key: string;
362
+ };
363
+ }
364
+ /**
365
+ * Check library-semantics claims against each package's type declarations.
366
+ * Needs `init`: module resolution runs under the service's compiler options,
367
+ * in the service's program.
368
+ */
369
+ export interface VerifyClientSemanticsRequest extends BaseRequest {
370
+ action: 'verify_client_semantics';
371
+ /** Absolute service root; module resolution starts here. */
372
+ from_dir: string;
373
+ checks: SemanticsCheck[];
374
+ /** Checks not reached in time come back `unchecked` with reason `budget`. */
375
+ budget_ms?: number;
376
+ }
323
377
  /**
324
378
  * Union type for all possible sidecar requests
325
379
  */
326
- export type SidecarRequest = RetypeCheckRequest | InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
380
+ export type SidecarRequest = RetypeCheckRequest | VerifyClientSemanticsRequest | InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
327
381
  /**
328
382
  * Request for a specific symbol to be bundled
329
383
  */
@@ -532,10 +586,37 @@ export interface RetypeCheckResponse extends BaseResponse {
532
586
  outcomes?: RetypeOutcome[];
533
587
  errors?: string[];
534
588
  }
589
+ /**
590
+ * The verdict on one `(claim_id, receiver)` pair.
591
+ *
592
+ * - `verified`: the declarations satisfy the claim.
593
+ * - `failed`: the declarations resolved and contradict it.
594
+ * - `unchecked`: the declarations could not be read.
595
+ */
596
+ export interface SemanticsResult {
597
+ claim_id: string;
598
+ receiver: string;
599
+ verdict: 'verified' | 'failed' | 'unchecked';
600
+ /** Absent exactly when verified. */
601
+ reason?: string;
602
+ }
603
+ /** How one package of the request resolved, for logs. */
604
+ export interface SemanticsModule {
605
+ package: string;
606
+ resolved_file?: string;
607
+ installed_version?: string;
608
+ reason?: string;
609
+ }
610
+ export interface VerifyClientSemanticsResponse extends BaseResponse {
611
+ /** Exactly one per check, in request order. */
612
+ semantics?: SemanticsResult[];
613
+ semantics_modules?: SemanticsModule[];
614
+ errors?: string[];
615
+ }
535
616
  /**
536
617
  * Union type for all possible sidecar responses
537
618
  */
538
- export type SidecarResponse = RetypeCheckResponse | InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
619
+ export type SidecarResponse = RetypeCheckResponse | VerifyClientSemanticsResponse | InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
539
620
  /**
540
621
  * An entry in the type manifest
541
622
  */
@@ -604,8 +685,9 @@ export interface InferredType {
604
685
  * anchor symbol has a resolvable source declaration. Lets the scanner's
605
686
  * pub/sub two-anchor arbitration (carrick#413) re-aim a demoted explicit
606
687
  * `SymbolRequest` at the tsc-witnessed payload type: the bundler requires
607
- * the symbol to be DECLARED in the request's `source_file`, and the
608
- * inference is the only party that knows where that is. Reported only by
688
+ * the symbol to be declared in, or re-exported by, the request's
689
+ * `source_file`, and the inference is the only party that knows where that
690
+ * is. Reported only by
609
691
  * the pub/sub infer kinds (`function_param`, `expression`); other kinds
610
692
  * omit it.
611
693
  */