carrick 0.3.108 → 0.3.110

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 (44) hide show
  1. package/package.json +9 -10
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/sidecar/dist/src/bundler.d.ts +10 -0
  4. package/sidecar/dist/src/bundler.js +42 -0
  5. package/sidecar/dist/src/capture/anchors.js +32 -0
  6. package/sidecar/dist/src/capture/api.d.ts +5 -0
  7. package/sidecar/dist/src/capture/check-deep.js +2 -1
  8. package/sidecar/dist/src/capture/check-workspace.d.ts +11 -0
  9. package/sidecar/dist/src/capture/check-workspace.js +81 -8
  10. package/sidecar/dist/src/capture/compiler-options.d.ts +39 -0
  11. package/sidecar/dist/src/capture/compiler-options.js +100 -0
  12. package/sidecar/dist/src/capture/deno-project.js +3 -2
  13. package/sidecar/dist/src/capture/index.d.ts +2 -0
  14. package/sidecar/dist/src/capture/index.js +25 -8
  15. package/sidecar/dist/src/capture/installed-package-directory.d.ts +22 -0
  16. package/sidecar/dist/src/capture/installed-package-directory.js +30 -0
  17. package/sidecar/dist/src/capture/installed-package.js +3 -2
  18. package/sidecar/dist/src/capture/resolution-edges.d.ts +36 -0
  19. package/sidecar/dist/src/capture/resolution-edges.js +150 -0
  20. package/sidecar/dist/src/capture/self-check.js +3 -2
  21. package/sidecar/dist/src/capture/status-table.d.ts +62 -0
  22. package/sidecar/dist/src/capture/status-table.js +104 -0
  23. package/sidecar/dist/src/definition-resolver.js +3 -2
  24. package/sidecar/dist/src/failure-path.d.ts +42 -2
  25. package/sidecar/dist/src/failure-path.js +38 -13
  26. package/sidecar/dist/src/index.js +91 -7
  27. package/sidecar/dist/src/library-claims.js +2 -0
  28. package/sidecar/dist/src/line-index.d.ts +9 -0
  29. package/sidecar/dist/src/line-index.js +24 -0
  30. package/sidecar/dist/src/print-type.d.ts +48 -0
  31. package/sidecar/dist/src/print-type.js +71 -0
  32. package/sidecar/dist/src/printed-names.js +2 -1
  33. package/sidecar/dist/src/program-files.d.ts +44 -0
  34. package/sidecar/dist/src/program-files.js +64 -0
  35. package/sidecar/dist/src/project-loader.d.ts +5 -0
  36. package/sidecar/dist/src/project-loader.js +14 -5
  37. package/sidecar/dist/src/response-modes.d.ts +77 -0
  38. package/sidecar/dist/src/response-modes.js +459 -0
  39. package/sidecar/dist/src/retype.js +7 -1
  40. package/sidecar/dist/src/type-inferrer.d.ts +53 -0
  41. package/sidecar/dist/src/type-inferrer.js +267 -79
  42. package/sidecar/dist/src/types.d.ts +50 -2
  43. package/sidecar/dist/src/validators.d.ts +159 -88
  44. package/sidecar/dist/src/validators.js +13 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.108",
3
+ "version": "0.3.110",
4
4
  "description": "Maps your entire TypeScript codebase across services and repositories, giving AI agents full context on existing types, routes, and function behaviours over MCP before they write duplicate or breaking code.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -53,20 +53,19 @@
53
53
  "@clack/prompts": "1.8.1",
54
54
  "picocolors": "1.1.1",
55
55
  "pnpm": "10.34.5",
56
- "ts-morph": "^25.0.1",
57
- "typescript": "^5.8.0",
56
+ "ts-morph": "28.0.0",
57
+ "typescript": "6.0.2",
58
58
  "zod": "^3.23.0"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@carrick-tools/cli-darwin-arm64": "0.3.108",
62
- "@carrick-tools/cli-darwin-x64": "0.3.108",
63
- "@carrick-tools/cli-linux-arm64": "0.3.108",
64
- "@carrick-tools/cli-linux-x64": "0.3.108",
65
- "@carrick-tools/cli-win32-x64": "0.3.108"
61
+ "@carrick-tools/cli-darwin-arm64": "0.3.110",
62
+ "@carrick-tools/cli-darwin-x64": "0.3.110",
63
+ "@carrick-tools/cli-linux-arm64": "0.3.110",
64
+ "@carrick-tools/cli-linux-x64": "0.3.110",
65
+ "@carrick-tools/cli-win32-x64": "0.3.110"
66
66
  },
67
67
  "devDependencies": {
68
- "@types/node": "^24.13.3",
69
- "typescript": "^5.9.2"
68
+ "@types/node": "^24.13.3"
70
69
  },
71
70
  "publishConfig": {
72
71
  "access": "public",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "carrick",
3
3
  "description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
4
- "version": "0.3.108",
4
+ "version": "0.3.110",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -50,6 +50,16 @@ export declare class TypeBundler {
50
50
  */
51
51
  private expandOrigin;
52
52
  private extractTypeDefinition;
53
+ /**
54
+ * carrick#1841: what a consumer receives when the symbol the model named for
55
+ * its response is a response table keyed by status code (`{ 200: Item;
56
+ * 404: Problem }`): the table's 2xx values, expanded like any other body.
57
+ * `unknown` where the table states no success body (no 2xx row, or only
58
+ * rows such as `204: void`). `undefined` when the symbol is not a table, so
59
+ * the caller bundles it as written. The capture's symbol anchor reads the
60
+ * same rule (`capture/anchors.ts`), so both answers agree.
61
+ */
62
+ private statusTableBodyText;
53
63
  private elementNeedsArrayParens;
54
64
  private extractTypeDefinitionBase;
55
65
  private isPathSource;
@@ -7,6 +7,7 @@ import * as path from 'node:path';
7
7
  import * as fs from 'node:fs';
8
8
  import { expandTypeStructural, } from './type-structural-expander.js';
9
9
  import { externalImportsOf } from './origin.js';
10
+ import { statusTableBody } from './capture/index.js';
10
11
  /**
11
12
  * #248: upper bound on `SymbolRequest.array_depth`. SDL list nesting is
12
13
  * realistically 1-3 levels (`[Order!]!`, at most `[[Order!]!]!`); this is a
@@ -301,6 +302,17 @@ export class TypeBundler {
301
302
  };
302
303
  }
303
304
  extractTypeDefinition(symbol) {
305
+ const tableBody = symbol.consumer_response ? this.statusTableBodyText(symbol) : undefined;
306
+ if (tableBody !== undefined) {
307
+ const alias = symbol.alias || symbol.symbol_name;
308
+ const depth = symbol.array_depth ?? 0;
309
+ if (depth > MAX_ARRAY_DEPTH)
310
+ return null;
311
+ const typeString = depth > 0 && tableBody !== 'unknown'
312
+ ? `(${tableBody})${'[]'.repeat(depth)}`
313
+ : tableBody;
314
+ return { definition: `export type ${alias} = ${typeString};`, typeString };
315
+ }
304
316
  const base = this.extractTypeDefinitionBase(symbol);
305
317
  if (!base)
306
318
  return null;
@@ -338,6 +350,36 @@ export class TypeBundler {
338
350
  const typeString = `${element}${'[]'.repeat(depth)}`;
339
351
  return { definition: `export type ${alias} = ${typeString};`, typeString };
340
352
  }
353
+ /**
354
+ * carrick#1841: what a consumer receives when the symbol the model named for
355
+ * its response is a response table keyed by status code (`{ 200: Item;
356
+ * 404: Problem }`): the table's 2xx values, expanded like any other body.
357
+ * `unknown` where the table states no success body (no 2xx row, or only
358
+ * rows such as `204: void`). `undefined` when the symbol is not a table, so
359
+ * the caller bundles it as written. The capture's symbol anchor reads the
360
+ * same rule (`capture/anchors.ts`), so both answers agree.
361
+ */
362
+ statusTableBodyText(symbol) {
363
+ const site = this.siteOf(symbol);
364
+ if (!site)
365
+ return undefined;
366
+ const declaration = site.sourceFile.getTypeAlias(site.name) ?? site.sourceFile.getInterface(site.name);
367
+ if (!declaration)
368
+ return undefined;
369
+ const table = declaration.getType();
370
+ const checker = this.project.getTypeChecker().compilerObject;
371
+ const read = statusTableBody(checker, table.compilerType, declaration.compilerNode);
372
+ if (!read)
373
+ return undefined;
374
+ if (read.kind === 'no_body')
375
+ return 'unknown';
376
+ const bodies = read.entries.map((entry) => {
377
+ const value = table.getProperty(entry.key)?.getTypeAtLocation(declaration);
378
+ return value ? expandTypeStructural(value, this.expandOrigin()) : 'unknown';
379
+ });
380
+ const distinct = [...new Set(bodies)];
381
+ return distinct.length === 1 ? distinct[0] : distinct.map((body) => `(${body})`).join(' | ');
382
+ }
341
383
  /// Whether the element type must be parenthesised before an `[]` suffix.
342
384
  /// Interfaces/classes/enums are always object/nominal (never a top-level
343
385
  /// union), so only type aliases and typed variables can carry a bare
@@ -10,6 +10,7 @@ import { printTypeForDestination, substituteUndeclaredNames, undeclaredNamesIn,
10
10
  import { typeIsOrContainsMachinery } from './machinery.js';
11
11
  import { installedPackageSpecifier } from './installed-package.js';
12
12
  import { realPath } from './service-config.js';
13
+ import { statusTableBody } from './status-table.js';
13
14
  import { unresolvedAtAnchor, unresolvedSpecifiersReachableFrom, } from './unresolved.js';
14
15
  /**
15
16
  * Repo-root-relative source file -> the specifier the surface entry imports
@@ -155,6 +156,37 @@ export function resolveAnchor(program, request, args) {
155
156
  }
156
157
  const arrayDepth = Math.max(0, request.array_depth ?? 0);
157
158
  const declared = checker.getDeclaredTypeOfSymbol(resolvedExport);
159
+ // carrick#1841: a symbol named for what a consumer receives that is a
160
+ // response table keyed by status code (`{ 200: Item; 404: Problem }`) is
161
+ // not the body. The body is its 2xx rows, indexed out of the table so the
162
+ // compiler still emits it (`import('./m').Table[200]`). A table with no
163
+ // success body abstains, decided. The v1 bundle reads the same rule
164
+ // (`TypeBundler.statusTableBodyText`).
165
+ if (request.consumer_response) {
166
+ const table = statusTableBody(checker, declared, resolvedExport.declarations?.[0] ?? sourceFile);
167
+ if (table?.kind === 'no_body') {
168
+ return {
169
+ request,
170
+ aliasText: 'unknown',
171
+ serialization: 'structural_fallback',
172
+ abstainReason: `'${request.symbol_name}' is a response table keyed by status code with no ` +
173
+ 'success row that states a body, so this consumer states no response contract',
174
+ };
175
+ }
176
+ if (table?.kind === 'body') {
177
+ const rows = [...new Set(table.entries.map((entry) => entry.key))].map((key) => /^\d+$/.test(key)
178
+ ? `import('${spec}').${request.symbol_name}[${key}]`
179
+ : `import('${spec}').${request.symbol_name}[${JSON.stringify(key)}]`);
180
+ const body = rows.join(' | ');
181
+ const unresolvedBody = unresolvedAtAnchor(program, sourceFile, declared, resolvedExport.declarations?.[0] ?? sourceFile, '<0>'.repeat(arrayDepth));
182
+ return {
183
+ request,
184
+ aliasText: arraySuffix && rows.length > 1 ? `(${body})${arraySuffix}` : `${body}${arraySuffix}`,
185
+ serialization: 'emitted',
186
+ ...(unresolvedBody ? { unresolved: unresolvedBody } : {}),
187
+ };
188
+ }
189
+ }
158
190
  const unresolved = unresolvedAtAnchor(program, sourceFile, declared, resolvedExport.declarations?.[0] ?? sourceFile, '<0>'.repeat(arrayDepth));
159
191
  return {
160
192
  request,
@@ -50,6 +50,11 @@ export interface SymbolAnchorRequest {
50
50
  * `import('./m').Sym[]`. Omitted/0 captures the symbol as-is.
51
51
  */
52
52
  array_depth?: number;
53
+ /**
54
+ * carrick#1841: the symbol names what a consumer receives, so a response
55
+ * table keyed by status code captures as its 2xx body, not as the table.
56
+ */
57
+ consumer_response?: boolean;
53
58
  }
54
59
  /**
55
60
  * Inline literal type text with no addressable symbol (the v1 inline-alias
@@ -18,6 +18,7 @@
18
18
  import ts from 'typescript';
19
19
  import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
+ import { sidecarCompilerOptions } from './compiler-options.js';
21
22
  import { findDisqualifyingTopTypes, provenanceOf } from './deep-walk.js';
22
23
  export function openProbeProgram(probesDir, plans) {
23
24
  if (plans.length === 0)
@@ -35,7 +36,7 @@ export function openProbeProgram(probesDir, plans) {
35
36
  .filter((f) => fs.existsSync(f));
36
37
  if (fileNames.length === 0)
37
38
  return undefined;
38
- const program = ts.createProgram(fileNames, { ...parsed.options, noEmit: true });
39
+ const program = ts.createProgram(fileNames, sidecarCompilerOptions({ ...parsed.options, noEmit: true }));
39
40
  return { program, checker: program.getTypeChecker(), probesDir };
40
41
  }
41
42
  catch {
@@ -15,6 +15,7 @@
15
15
  import type { CheckStubInput } from './api.js';
16
16
  import type { ProbePlan } from './check-probe.js';
17
17
  import { WriteGuard } from './guarded-fs.js';
18
+ import { type ResolutionEdges } from './resolution-edges.js';
18
19
  export interface AssembledStub {
19
20
  serviceName: string;
20
21
  /** Workspace package dir under packages/ (== the @carrick/<dir> suffix). */
@@ -45,6 +46,15 @@ export interface AssembledWorkspace {
45
46
  export declare function computeDedupeOverrides(stubs: {
46
47
  dependencies: Record<string, string>;
47
48
  }[]): Record<string, string>;
49
+ /**
50
+ * pnpm parent-scoped overrides (`<parent>@<version>><child>`) that install
51
+ * each edge the stubs recorded at the version the scanned repo installed
52
+ * (carrick#2091). A parent-scoped override wins over a generic one, so an
53
+ * edge's version first passes through the dedupe table: one physical copy per
54
+ * compat group still holds. An edge two stubs record at versions the dedupe
55
+ * does not reconcile is left to the resolver.
56
+ */
57
+ export declare function computeResolutionOverrides(edgeSets: ResolutionEdges[], dedupe: Record<string, string>): Record<string, string>;
48
58
  export interface AssembleOptions {
49
59
  stubs: CheckStubInput[];
50
60
  workspaceRoot?: string;
@@ -53,3 +63,4 @@ export interface AssembleOptions {
53
63
  export declare function assembleWorkspace(opts: AssembleOptions): AssembledWorkspace;
54
64
  /** Write the generated probe files into the assembled probes package. */
55
65
  export declare function writeProbes(ws: AssembledWorkspace, plans: ProbePlan[]): void;
66
+ export declare const CHECKER_TSCONFIG: string;
@@ -15,6 +15,7 @@
15
15
  import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import { WriteGuard } from './guarded-fs.js';
18
+ import { RESOLUTION_FILE } from './resolution-edges.js';
18
19
  const PROBES_PACKAGE = 'carrick-probes';
19
20
  function parseVersion(v) {
20
21
  const core = v.replace(/^[^\d]*/, '').split('-')[0].split('+')[0];
@@ -90,6 +91,57 @@ export function computeDedupeOverrides(stubs) {
90
91
  .sort()
91
92
  .map((k) => [k, overrides[k]]));
92
93
  }
94
+ /** A stub's recorded edges; a stub stored without the file records none. */
95
+ function readStubResolution(stubDir) {
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(fs.readFileSync(path.join(stubDir, RESOLUTION_FILE), 'utf8'));
99
+ }
100
+ catch {
101
+ return {};
102
+ }
103
+ const edges = parsed?.edges;
104
+ if (!edges || typeof edges !== 'object')
105
+ return {};
106
+ const out = {};
107
+ for (const [parent, children] of Object.entries(edges)) {
108
+ if (!children || typeof children !== 'object')
109
+ continue;
110
+ for (const [child, version] of Object.entries(children)) {
111
+ if (typeof version === 'string')
112
+ (out[parent] ??= {})[child] = version;
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ /**
118
+ * pnpm parent-scoped overrides (`<parent>@<version>><child>`) that install
119
+ * each edge the stubs recorded at the version the scanned repo installed
120
+ * (carrick#2091). A parent-scoped override wins over a generic one, so an
121
+ * edge's version first passes through the dedupe table: one physical copy per
122
+ * compat group still holds. An edge two stubs record at versions the dedupe
123
+ * does not reconcile is left to the resolver.
124
+ */
125
+ export function computeResolutionOverrides(edgeSets, dedupe) {
126
+ const versionsOf = new Map();
127
+ for (const edges of edgeSets) {
128
+ for (const [parent, children] of Object.entries(edges)) {
129
+ for (const [child, version] of Object.entries(children)) {
130
+ const key = `${parent}>${child}`;
131
+ if (!versionsOf.has(key))
132
+ versionsOf.set(key, new Set());
133
+ versionsOf.get(key).add(dedupe[`${child}@${version}`] ?? version);
134
+ }
135
+ }
136
+ }
137
+ const overrides = {};
138
+ for (const key of [...versionsOf.keys()].sort()) {
139
+ const versions = versionsOf.get(key);
140
+ if (versions.size === 1)
141
+ overrides[key] = [...versions][0];
142
+ }
143
+ return overrides;
144
+ }
93
145
  function readStubPackageName(stubDir) {
94
146
  const pkgPath = path.join(stubDir, 'package.json');
95
147
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
@@ -116,6 +168,7 @@ export function assembleWorkspace(opts) {
116
168
  guard.writeFile(path.join(workspaceDir, 'pnpm-workspace.yaml'), 'packages:\n - "packages/*"\n');
117
169
  const assembled = [];
118
170
  const dependencySets = [];
171
+ const edgeSets = [];
119
172
  const svcToPkg = new Map();
120
173
  const dirToPkg = new Map();
121
174
  const dirToSvc = new Map();
@@ -126,12 +179,20 @@ export function assembleWorkspace(opts) {
126
179
  guard.copyTree(stub.stub_dir, dest, (src) => !src.split(path.sep).includes('node_modules'));
127
180
  assembled.push({ serviceName: stub.service_name, packageDir, packageName });
128
181
  dependencySets.push({ dependencies: readStubDependencies(stub.stub_dir) });
182
+ edgeSets.push(readStubResolution(stub.stub_dir));
129
183
  svcToPkg.set(stub.service_name, packageName);
130
184
  dirToPkg.set(packageDir, packageName);
131
185
  dirToSvc.set(packageDir, stub.service_name);
132
186
  }
133
- // Root manifest carries the semver-dedupe overrides.
134
- const overrides = computeDedupeOverrides(dependencySets);
187
+ // Root manifest carries the semver-dedupe overrides and the recorded edges.
188
+ const dedupe = computeDedupeOverrides(dependencySets);
189
+ const merged = {
190
+ ...dedupe,
191
+ ...computeResolutionOverrides(edgeSets, dedupe),
192
+ };
193
+ const overrides = Object.fromEntries(Object.keys(merged)
194
+ .sort()
195
+ .map((k) => [k, merged[k]]));
135
196
  guard.writeFile(path.join(workspaceDir, 'package.json'), JSON.stringify({
136
197
  name: 'carrick-check-workspace',
137
198
  version: '0.0.0',
@@ -181,12 +242,13 @@ const NPMRC = [
181
242
  'strict-peer-dependencies=false',
182
243
  // Determinism: install exactly the pinned closure, no implicit peer pull-in.
183
244
  'auto-install-peers=false',
184
- // Determinism: the scratch workspace has no committed lockfile, so the
185
- // transitive closure would otherwise be a pure function of live registry
186
- // state. prefer-offline resolves from the local store/metadata cache
245
+ // Determinism: the scratch workspace has no committed lockfile. Direct deps
246
+ // are exact-pinned by the stubs, and every transitive edge a stub recorded
247
+ // from the scanned repo's installed tree is pinned by a parent-scoped
248
+ // override in the root manifest. Only unrecorded edges resolve, and for
249
+ // those prefer-offline resolves from the local store/metadata cache
187
250
  // whenever possible (byte-stable across runs on a host), and the explicit
188
251
  // resolution-mode pins pnpm's resolver behavior across pnpm versions.
189
- // Direct deps are exact-pinned by the stubs; only transitives resolve.
190
252
  'prefer-offline=true',
191
253
  'resolution-mode=highest',
192
254
  '',
@@ -195,8 +257,11 @@ const NPMRC = [
195
257
  // TS2717 collisions so the poison rule can convert them to honest
196
258
  // unverifiables). noUnusedLocals/Parameters:false keep TS6133/6196 off the
197
259
  // gate/assignment lines. moduleResolution bundler accepts both node16
198
- // `.js`-suffixed and extensionless specifiers in one program.
199
- const CHECKER_TSCONFIG = JSON.stringify({
260
+ // `.js`-suffixed and extensionless specifiers in one program. The rest is
261
+ // what `sidecarCompilerOptions` sets on every other program (carrick#2019):
262
+ // `tsc` reads this file itself, so each value is written out, and a test
263
+ // holds the file to what that function would make of it.
264
+ export const CHECKER_TSCONFIG = JSON.stringify({
200
265
  compilerOptions: {
201
266
  strict: true,
202
267
  exactOptionalPropertyTypes: false,
@@ -210,6 +275,14 @@ const CHECKER_TSCONFIG = JSON.stringify({
210
275
  types: [],
211
276
  noEmit: true,
212
277
  forceConsistentCasingInFileNames: true,
278
+ esModuleInterop: false,
279
+ allowSyntheticDefaultImports: true,
280
+ resolveJsonModule: true,
281
+ alwaysStrict: true,
282
+ noUncheckedSideEffectImports: false,
283
+ libReplacement: true,
284
+ stableTypeOrdering: true,
285
+ ignoreDeprecations: '6.0',
213
286
  },
214
287
  include: ['probes/**/*.ts'],
215
288
  }, null, 2) + '\n';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The options every program the sidecar builds carries (carrick#2019).
3
+ *
4
+ * Every `ts.createProgram`, every ts-morph `Project` and the `tsconfig.json`
5
+ * the check hands to `tsc` goes through `sidecarCompilerOptions`, whatever
6
+ * its options came from: a project's tsconfig, a snapshot of one, a Deno
7
+ * config, or the sidecar's own literal set.
8
+ *
9
+ * Two things are added:
10
+ *
11
+ * - `stableTypeOrdering`. Without it the compiler prints a union's members in
12
+ * the order the checker first met them, and an object made by a mapped type
13
+ * lists its properties in the order its keys were created, so the same type
14
+ * printed text that depended on what the process had read before. With it
15
+ * the checker orders types by their content, and one process prints the
16
+ * same text whatever it did first.
17
+ * - TypeScript 5's effective value for every option whose default TypeScript 6
18
+ * changed, where the options leave it unset (`typescript5Defaults`). A
19
+ * project written against TypeScript 5 is read as its own compiler reads
20
+ * it, and as the sidecar read it before it moved to 6. A project that sets
21
+ * an option keeps its own value. Several of those values are deprecated in
22
+ * TypeScript 6 (target ES5, moduleResolution node10, esModuleInterop false),
23
+ * and so are options many projects still set themselves (baseUrl), so
24
+ * `ignoreDeprecations` is set too: a deprecated option still works in 6, and
25
+ * the error it would raise is not the project's.
26
+ */
27
+ import ts from 'typescript';
28
+ /**
29
+ * For each option whose default TypeScript 6 changed and `options` leaves
30
+ * unset, the value TypeScript 5 (5.9, its last minor) computes for it from
31
+ * the options that are set. Only unset options appear in the result.
32
+ */
33
+ export declare function typescript5Defaults(options: ts.CompilerOptions): ts.CompilerOptions;
34
+ /**
35
+ * `options` with TypeScript 5's defaults filled in and the sidecar's own set.
36
+ * Takes ts-morph's `CompilerOptions` as well as the compiler's: the two
37
+ * declare the same option enums separately, and the values are the same.
38
+ */
39
+ export declare function sidecarCompilerOptions<O extends object>(options: O): O;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The options every program the sidecar builds carries (carrick#2019).
3
+ *
4
+ * Every `ts.createProgram`, every ts-morph `Project` and the `tsconfig.json`
5
+ * the check hands to `tsc` goes through `sidecarCompilerOptions`, whatever
6
+ * its options came from: a project's tsconfig, a snapshot of one, a Deno
7
+ * config, or the sidecar's own literal set.
8
+ *
9
+ * Two things are added:
10
+ *
11
+ * - `stableTypeOrdering`. Without it the compiler prints a union's members in
12
+ * the order the checker first met them, and an object made by a mapped type
13
+ * lists its properties in the order its keys were created, so the same type
14
+ * printed text that depended on what the process had read before. With it
15
+ * the checker orders types by their content, and one process prints the
16
+ * same text whatever it did first.
17
+ * - TypeScript 5's effective value for every option whose default TypeScript 6
18
+ * changed, where the options leave it unset (`typescript5Defaults`). A
19
+ * project written against TypeScript 5 is read as its own compiler reads
20
+ * it, and as the sidecar read it before it moved to 6. A project that sets
21
+ * an option keeps its own value. Several of those values are deprecated in
22
+ * TypeScript 6 (target ES5, moduleResolution node10, esModuleInterop false),
23
+ * and so are options many projects still set themselves (baseUrl), so
24
+ * `ignoreDeprecations` is set too: a deprecated option still works in 6, and
25
+ * the error it would raise is not the project's.
26
+ */
27
+ import ts from 'typescript';
28
+ /** Options the sidecar sets on every program, over anything else. */
29
+ const ALWAYS = {
30
+ stableTypeOrdering: true,
31
+ ignoreDeprecations: '6.0',
32
+ };
33
+ /**
34
+ * For each option whose default TypeScript 6 changed and `options` leaves
35
+ * unset, the value TypeScript 5 (5.9, its last minor) computes for it from
36
+ * the options that are set. Only unset options appear in the result.
37
+ */
38
+ export function typescript5Defaults(options) {
39
+ const fill = {};
40
+ const { ModuleKind: M, ModuleResolutionKind: R, ScriptTarget: T } = ts;
41
+ // 5.x ignores ES3 as it ignores no target: both take the default.
42
+ const statedTarget = options.target === T.ES3 ? undefined : options.target;
43
+ const target = statedTarget ??
44
+ (options.module === M.Node16 || options.module === M.Node18
45
+ ? T.ES2022
46
+ : options.module === M.Node20
47
+ ? T.ES2023
48
+ : options.module === M.NodeNext
49
+ ? T.ESNext
50
+ : T.ES5);
51
+ if (statedTarget === undefined)
52
+ fill.target = target;
53
+ const module = options.module ?? (target >= T.ES2015 ? M.ES2015 : M.CommonJS);
54
+ if (options.module === undefined)
55
+ fill.module = module;
56
+ const moduleResolution = options.moduleResolution ??
57
+ (module === M.CommonJS
58
+ ? R.Node10
59
+ : module === M.Node16 || module === M.Node18 || module === M.Node20
60
+ ? R.Node16
61
+ : module === M.NodeNext
62
+ ? R.NodeNext
63
+ : module === M.Preserve
64
+ ? R.Bundler
65
+ : R.Classic);
66
+ if (options.moduleResolution === undefined)
67
+ fill.moduleResolution = moduleResolution;
68
+ const nodeFamily = module === M.Node16 || module === M.Node18 || module === M.Node20 || module === M.NodeNext;
69
+ const esModuleInterop = options.esModuleInterop ?? (nodeFamily || module === M.Preserve);
70
+ if (options.esModuleInterop === undefined)
71
+ fill.esModuleInterop = esModuleInterop;
72
+ if (options.allowSyntheticDefaultImports === undefined) {
73
+ fill.allowSyntheticDefaultImports = esModuleInterop || module === M.System || moduleResolution === R.Bundler;
74
+ }
75
+ if (options.resolveJsonModule === undefined) {
76
+ fill.resolveJsonModule = module === M.Node20 || module === M.NodeNext || moduleResolution === R.Bundler;
77
+ }
78
+ // Each strict flag a project leaves unset follows `strict`, which was off
79
+ // unless set; `alwaysStrict` followed it too.
80
+ if (options.strict === undefined)
81
+ fill.strict = false;
82
+ if (options.alwaysStrict === undefined)
83
+ fill.alwaysStrict = options.strict ?? false;
84
+ // Every package under each `typeRoots` directory (`@types/*`), not none.
85
+ if (options.types === undefined)
86
+ fill.types = ['*'];
87
+ if (options.noUncheckedSideEffectImports === undefined)
88
+ fill.noUncheckedSideEffectImports = false;
89
+ if (options.libReplacement === undefined)
90
+ fill.libReplacement = true;
91
+ return fill;
92
+ }
93
+ /**
94
+ * `options` with TypeScript 5's defaults filled in and the sidecar's own set.
95
+ * Takes ts-morph's `CompilerOptions` as well as the compiler's: the two
96
+ * declare the same option enums separately, and the values are the same.
97
+ */
98
+ export function sidecarCompilerOptions(options) {
99
+ return { ...options, ...typescript5Defaults(options), ...ALWAYS };
100
+ }
@@ -5,6 +5,7 @@ import * as path from 'node:path';
5
5
  import { pathToFileURL, fileURLToPath } from 'node:url';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { execFileSync } from 'node:child_process';
8
+ import { sidecarCompilerOptions } from './compiler-options.js';
8
9
  import { rewriteSpecifiers } from './specifiers.js';
9
10
  import { WriteGuard } from './guarded-fs.js';
10
11
  function readConfig(file) {
@@ -452,11 +453,11 @@ export class DenoProject {
452
453
  /** Runtime declarations belong to their producer, not the checker's globals. */
453
454
  function isolateRuntime(guard, typesDir, files, runtimeRel) {
454
455
  const runtimePath = path.join(typesDir, runtimeRel);
455
- const program = ts.createProgram(files.map(file => path.join(typesDir, file)), {
456
+ const program = ts.createProgram(files.map(file => path.join(typesDir, file)), sidecarCompilerOptions({
456
457
  strict: true, skipLibCheck: true, target: ts.ScriptTarget.ESNext,
457
458
  module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler,
458
459
  types: [],
459
- });
460
+ }));
460
461
  const checker = program.getTypeChecker();
461
462
  const runtime = program.getSourceFile(runtimePath);
462
463
  const retained = new Set();
@@ -30,6 +30,7 @@
30
30
  import type { CaptureStubOptions, CaptureStubResult } from './api.js';
31
31
  export type { CaptureStubOptions, CaptureStubResult } from './api.js';
32
32
  export { DenoProject, findDenoConfig } from './deno-project.js';
33
+ export { sidecarCompilerOptions } from './compiler-options.js';
33
34
  export { serviceConfigPath } from './project-references.js';
34
35
  export { findServiceTsconfig } from './service-config.js';
35
36
  export { runCheck } from './check.js';
@@ -58,3 +59,4 @@ export declare function surfaceEntryFileName(): string;
58
59
  /** Same normalization intent as bundle_file_stems on the Rust side. */
59
60
  export declare function sanitizeServiceName(name: string): string;
60
61
  export declare function captureStub(opts: CaptureStubOptions): CaptureStubResult;
62
+ export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';
@@ -33,6 +33,7 @@ import * as path from 'node:path';
33
33
  import { entryRelativeSpecifier, resolveAnchor } from './anchors.js';
34
34
  import { findAugmentationFiles } from './augmentations.js';
35
35
  import { installedVersions, lockfileVersions } from './lockfile.js';
36
+ import { installedResolutionEdges, RESOLUTION_FILE } from './resolution-edges.js';
36
37
  import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
37
38
  import { installedPackageSpecifier, typesPackageOf, withInstalledPackages } from './installed-package.js';
38
39
  import { selfCheckStub } from './self-check.js';
@@ -42,7 +43,9 @@ import { emitsAlike, ProjectGraph } from './project-references.js';
42
43
  import { findServiceTsconfig } from './service-config.js';
43
44
  import { placeEmittedTree, surfaceModuleInTree } from './outside-root.js';
44
45
  import { WriteGuard } from './guarded-fs.js';
46
+ import { sidecarCompilerOptions } from './compiler-options.js';
45
47
  export { DenoProject, findDenoConfig } from './deno-project.js';
48
+ export { sidecarCompilerOptions } from './compiler-options.js';
46
49
  export { serviceConfigPath } from './project-references.js';
47
50
  export { findServiceTsconfig } from './service-config.js';
48
51
  // v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
@@ -282,7 +285,7 @@ export function captureStub(opts) {
282
285
  try {
283
286
  progress('emit', 'emitting declarations');
284
287
  guard.writeFile(entryPath, entryLines.join('\n') + '\n');
285
- const emitOptions = {
288
+ const emitOptions = sidecarCompilerOptions({
286
289
  ...parsed.options,
287
290
  // The load-bearing trio: emit declarations without checking, so
288
291
  // type-error-laden and bare (no node_modules) checkouts still emit.
@@ -295,7 +298,7 @@ export function captureStub(opts) {
295
298
  incremental: false,
296
299
  outDir: staging,
297
300
  rootDir: entryDir,
298
- };
301
+ });
299
302
  written = emitDeclarations({
300
303
  rootNames: [entryPath, ...augmentationSources],
301
304
  options: emitOptions,
@@ -426,9 +429,8 @@ export function captureStub(opts) {
426
429
  // repos whose lockfiles we do not parse (yarn classic v1, binary
427
430
  // bun.lockb). The parsed lockfile (npm, pnpm, yarn-berry, text bun.lock)
428
431
  // remains the bare-checkout fallback. Both paths pin only
429
- // the directly-referenced externals; transitives resolve at check-install
430
- // (check-workspace NPMRC: "Direct deps are exact-pinned by the stubs;
431
- // only transitives resolve").
432
+ // the directly-referenced externals; what those depend on is recorded
433
+ // from the installed tree below (RESOLUTION_FILE).
432
434
  const installed = installedVersions(repoRoot, externalSpecs);
433
435
  const lockVersions = lockfileVersions(repoRoot);
434
436
  for (const name of Object.keys(deno?.pinned ?? {}))
@@ -449,6 +451,18 @@ export function captureStub(opts) {
449
451
  else if (!rewritten.pins[typesPackageOf(name)])
450
452
  unpinned.push(name);
451
453
  }
454
+ // ---- Record how the installed tree resolved what the pins depend on ----
455
+ // The check pins each recorded edge, so it installs what the repo
456
+ // installed rather than the newest version in range (carrick#2091).
457
+ const resolution = deno
458
+ ? { edges: {}, unrecorded: 0 }
459
+ : installedResolutionEdges(repoRoot, pinned);
460
+ const edgeCount = Object.values(resolution.edges).reduce((n, children) => n + Object.keys(children).length, 0);
461
+ if (edgeCount > 0) {
462
+ stubGuard.writeFile(path.join(stubDir, RESOLUTION_FILE), JSON.stringify({ edges: resolution.edges }, null, 2) + '\n');
463
+ }
464
+ console.error(`[sidecar] capture ${packageName}: ${edgeCount} dependency edge(s) recorded from the installed tree, ` +
465
+ `${resolution.unrecorded} left to the registry`);
452
466
  const dependencyRoot = deno?.config.workspaceRoot ?? repoRoot;
453
467
  const bareCheckout = !deno && !fs.existsSync(path.join(dependencyRoot, 'node_modules'));
454
468
  stubGuard.writeFile(path.join(stubDir, 'package.json'), JSON.stringify({
@@ -523,7 +537,7 @@ function emitDeclarations(args) {
523
537
  const emitted = new Map();
524
538
  const declarationSources = new Map();
525
539
  const sourceByEmitted = new Map();
526
- const program = ts.createProgram(args.rootNames, args.options, args.host);
540
+ const program = ts.createProgram(args.rootNames, sidecarCompilerOptions(args.options), args.host);
527
541
  const emitResult = program.emit(undefined, (fileName, text, _bom, _error, sources) => {
528
542
  emitted.set(fileName, text);
529
543
  if (sources?.[0])
@@ -690,10 +704,10 @@ function resolveAnchors(opts, parsed, ctx, deno) {
690
704
  ...new Set(opts.anchors
691
705
  .flatMap((a) => (a.source_file ? [path.join(ctx.repoRoot, a.source_file)] : []))),
692
706
  ].filter((f) => fs.existsSync(f));
693
- const options = {
707
+ const options = sidecarCompilerOptions({
694
708
  ...parsed.options,
695
709
  noEmit: true,
696
- };
710
+ });
697
711
  const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options, deno?.host(options));
698
712
  // The checker binds every file of the program when it is first asked for,
699
713
  // which the first anchor would otherwise do: asked for here, so that the
@@ -749,3 +763,6 @@ function resolveAnchors(opts, parsed, ctx, deno) {
749
763
  ctx.guard.unlink(ctx.entryPath);
750
764
  }
751
765
  }
766
+ // carrick#1841: the response-table reading the inferrer and the v1 bundle
767
+ // share with the capture's symbol anchor.
768
+ export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';