carrick 0.3.106 → 0.3.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/hook/refresh.js +8 -1
  2. package/dist/hook/refresh.js.map +1 -1
  3. package/package.json +6 -6
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/sidecar/dist/src/capture/anchors.d.ts +15 -2
  6. package/sidecar/dist/src/capture/anchors.js +28 -11
  7. package/sidecar/dist/src/capture/api.d.ts +15 -0
  8. package/sidecar/dist/src/capture/check-classify.d.ts +9 -0
  9. package/sidecar/dist/src/capture/check-classify.js +20 -0
  10. package/sidecar/dist/src/capture/check-fields.d.ts +24 -0
  11. package/sidecar/dist/src/capture/check-fields.js +54 -36
  12. package/sidecar/dist/src/capture/check-poison.js +4 -14
  13. package/sidecar/dist/src/capture/check-union.d.ts +40 -0
  14. package/sidecar/dist/src/capture/check-union.js +92 -0
  15. package/sidecar/dist/src/capture/check.js +5 -0
  16. package/sidecar/dist/src/capture/index.js +138 -80
  17. package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
  18. package/sidecar/dist/src/capture/installed-package.js +2 -1
  19. package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
  20. package/sidecar/dist/src/capture/outside-root.js +39 -2
  21. package/sidecar/dist/src/capture/self-check.js +3 -13
  22. package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
  23. package/sidecar/dist/src/capture/specifiers.js +25 -0
  24. package/sidecar/dist/src/definition-resolver.d.ts +5 -8
  25. package/sidecar/dist/src/definition-resolver.js +5 -7
  26. package/sidecar/dist/src/function-line-index.d.ts +30 -0
  27. package/sidecar/dist/src/function-line-index.js +162 -0
  28. package/sidecar/dist/src/index.d.ts +5 -0
  29. package/sidecar/dist/src/index.js +62 -9
  30. package/sidecar/dist/src/infer-timing.d.ts +54 -0
  31. package/sidecar/dist/src/infer-timing.js +124 -0
  32. package/sidecar/dist/src/line-index.d.ts +9 -0
  33. package/sidecar/dist/src/line-index.js +26 -0
  34. package/sidecar/dist/src/progress.d.ts +22 -0
  35. package/sidecar/dist/src/progress.js +31 -0
  36. package/sidecar/dist/src/retype.d.ts +4 -2
  37. package/sidecar/dist/src/retype.js +10 -23
  38. package/sidecar/dist/src/type-inferrer.d.ts +238 -12
  39. package/sidecar/dist/src/type-inferrer.js +772 -168
  40. package/sidecar/dist/src/types.d.ts +83 -6
  41. package/sidecar/dist/src/validators.d.ts +54 -16
  42. package/sidecar/dist/src/validators.js +4 -0
  43. package/templates/skills/carrick-reuse.md +6 -5
@@ -132,7 +132,14 @@ export function refreshInBackground(root, status, options = {}) {
132
132
  return null;
133
133
  if (!claimRefresh(marker, now, cooldown))
134
134
  return null;
135
- (options.start ?? startDetached)(binary(env), ["refresh", "--workspace", root], env);
135
+ // Nobody started this refresh and nobody is reading it, so it does not keep
136
+ // the machine awake: that hold is for a scan a person is waiting on, and it
137
+ // comes with a line saying so that this one has nowhere to print
138
+ // (carrick#1889).
139
+ (options.start ?? startDetached)(binary(env), ["refresh", "--workspace", root], {
140
+ ...env,
141
+ CARRICK_NO_KEEP_AWAKE: "1",
142
+ });
136
143
  return "Carrick: a repo here is connected with no hosted index yet, so a refresh is running in the background. Its rows arrive in the next session.";
137
144
  }
138
145
  //# sourceMappingURL=refresh.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../src/hook/refresh.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../src/hook/refresh.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.106",
3
+ "version": "0.3.108",
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",
@@ -58,11 +58,11 @@
58
58
  "zod": "^3.23.0"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@carrick-tools/cli-darwin-arm64": "0.3.106",
62
- "@carrick-tools/cli-darwin-x64": "0.3.106",
63
- "@carrick-tools/cli-linux-arm64": "0.3.106",
64
- "@carrick-tools/cli-linux-x64": "0.3.106",
65
- "@carrick-tools/cli-win32-x64": "0.3.106"
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"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^24.13.3",
@@ -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.106",
4
+ "version": "0.3.108",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -56,8 +56,21 @@ export interface ResolvedAnchor {
56
56
  */
57
57
  namesUnemittedModule?: true;
58
58
  }
59
- /** Repo-root-relative source file -> extensionless specifier from entryDir. */
60
- export declare function entryRelativeSpecifier(entryDir: string, repoRoot: string, sourceFile: string): string;
59
+ /**
60
+ * Repo-root-relative source file -> the specifier the surface entry imports
61
+ * it by: its path from `entryDir`, in the form the entry resolves.
62
+ *
63
+ * With the extension removed (`./a`), where `resolveFromEntry` says the entry
64
+ * resolves that to the module. It does not always (carrick#1911): under
65
+ * `module` `node16`..`nodenext` an entry in a `"type": "module"` package is
66
+ * an ES module, which must name the file the module compiles to, and a `.mts`
67
+ * or `.cts` module has no extensionless form under any setting. There, and
68
+ * with no resolver to ask, the specifier is that output name: `./a.js` for
69
+ * `.ts` and `.tsx`, `./a.mjs` for `.mts`, `./a.cjs` for `.cts`, which every
70
+ * module setting maps back to the source and, in the stub, to its
71
+ * declaration.
72
+ */
73
+ export declare function entryRelativeSpecifier(entryDir: string, repoRoot: string, sourceFile: string, resolveFromEntry?: (specifier: string) => string | undefined): string;
61
74
  /**
62
75
  * Resolve one anchor against the analysis program. `placeholder` is the
63
76
  * anchor's own placeholder alias declaration in the entry file -- the
@@ -11,15 +11,32 @@ import { typeIsOrContainsMachinery } from './machinery.js';
11
11
  import { installedPackageSpecifier } from './installed-package.js';
12
12
  import { realPath } from './service-config.js';
13
13
  import { unresolvedAtAnchor, unresolvedSpecifiersReachableFrom, } from './unresolved.js';
14
- /** Repo-root-relative source file -> extensionless specifier from entryDir. */
15
- export function entryRelativeSpecifier(entryDir, repoRoot, sourceFile) {
16
- const target = path
17
- .join(repoRoot, sourceFile)
18
- .replace(/\.(ts|tsx|mts|cts)$/, '');
19
- let rel = path.relative(entryDir, target).split(path.sep).join('/');
20
- if (!rel.startsWith('.'))
21
- rel = `./${rel}`;
22
- return rel;
14
+ /**
15
+ * Repo-root-relative source file -> the specifier the surface entry imports
16
+ * it by: its path from `entryDir`, in the form the entry resolves.
17
+ *
18
+ * With the extension removed (`./a`), where `resolveFromEntry` says the entry
19
+ * resolves that to the module. It does not always (carrick#1911): under
20
+ * `module` `node16`..`nodenext` an entry in a `"type": "module"` package is
21
+ * an ES module, which must name the file the module compiles to, and a `.mts`
22
+ * or `.cts` module has no extensionless form under any setting. There, and
23
+ * with no resolver to ask, the specifier is that output name: `./a.js` for
24
+ * `.ts` and `.tsx`, `./a.mjs` for `.mts`, `./a.cjs` for `.cts`, which every
25
+ * module setting maps back to the source and, in the stub, to its
26
+ * declaration.
27
+ */
28
+ export function entryRelativeSpecifier(entryDir, repoRoot, sourceFile, resolveFromEntry) {
29
+ const target = path.join(repoRoot, sourceFile);
30
+ const from = (file) => {
31
+ const rel = path.relative(entryDir, file).split(path.sep).join('/');
32
+ return rel.startsWith('.') ? rel : `./${rel}`;
33
+ };
34
+ const bare = from(target.replace(/\.(ts|tsx|mts|cts)$/, ''));
35
+ const resolved = resolveFromEntry?.(bare);
36
+ if (resolved !== undefined && realPath(resolved) === realPath(target))
37
+ return bare;
38
+ const kind = /(?:\.d)?\.([cm]?)[jt]sx?$/.exec(target);
39
+ return kind ? from(`${target.slice(0, kind.index)}.${kind[1]}js`) : bare;
23
40
  }
24
41
  function moduleExport(checker, sourceFile, name) {
25
42
  const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
@@ -100,7 +117,7 @@ export function resolveAnchor(program, request, args) {
100
117
  if (!sourceFile) {
101
118
  return demote(`source file not in program: ${request.source_file}`);
102
119
  }
103
- const spec = entryRelativeSpecifier(args.entryDir, args.repoRoot, request.source_file);
120
+ const spec = entryRelativeSpecifier(args.entryDir, args.repoRoot, request.source_file, args.resolveFromEntry);
104
121
  if (request.kind === 'symbol') {
105
122
  const exported = moduleExport(checker, sourceFile, request.symbol_name);
106
123
  if (!exported) {
@@ -436,7 +453,7 @@ function qualifyNamesFromSource(text, program, sourceFileRel, printed, args) {
436
453
  return undefined;
437
454
  if (rel.split(path.sep).includes('node_modules'))
438
455
  return undefined;
439
- return entryRelativeSpecifier(args.entryDir, args.repoRoot, rel.split(path.sep).join('/'));
456
+ return entryRelativeSpecifier(args.entryDir, args.repoRoot, rel.split(path.sep).join('/'), args.resolveFromEntry);
440
457
  };
441
458
  const imports = new Map();
442
459
  const importFor = (name) => {
@@ -346,6 +346,14 @@ export interface CaptureStubResult {
346
346
  ts_version: string;
347
347
  errors: string[];
348
348
  }
349
+ /**
350
+ * The stages of a capture that it reports as it reaches them (carrick#1916),
351
+ * in order: the program the anchors are read in is being built; the anchors
352
+ * are being resolved in it; the declarations are being emitted; the stub is
353
+ * being checked. A capture whose anchors belong to several projects goes
354
+ * through the first two once per project.
355
+ */
356
+ export type CapturePhase = 'program' | 'anchors' | 'emit' | 'self-check';
349
357
  export interface CaptureStubOptions {
350
358
  repoRoot: string;
351
359
  serviceName: string;
@@ -360,6 +368,13 @@ export interface CaptureStubOptions {
360
368
  * lie inside it only beneath a `.carrick` directory (carrick#1768).
361
369
  */
362
370
  scanRoot?: string;
371
+ /**
372
+ * Told each stage as the capture reaches it, and each anchor as it is
373
+ * resolved (`message` is then `<done> of <total>`). Called by the work
374
+ * itself, between units, so a capture that stops finishing them stops
375
+ * reporting. It changes nothing the capture writes.
376
+ */
377
+ onProgress?: (phase: CapturePhase, message: string) => void;
363
378
  }
364
379
  /** Wire protocol of a matched pair (drives the direction table). */
365
380
  export type ProbeProtocol = 'http' | 'graphql' | 'socket' | 'pubsub';
@@ -14,6 +14,8 @@
14
14
  * IsFormBody gate fired (TS2344) -> unverifiable (form-encoded body)
15
15
  * IsByteBody gate fired (TS2344) -> unverifiable (bytes, even agreeing)
16
16
  * raw-text read marked on a side -> unverifiable (text, even agreeing)
17
+ * assignment error, sent union taken in part by a weak expected type
18
+ * -> unverifiable (carrick#1995)
17
19
  * assignment-class error -> incompatible
18
20
  * no diagnostics -> compatible [lowest precedence]
19
21
  *
@@ -27,6 +29,7 @@ import type { CheckVerdict } from './api.js';
27
29
  import { type ProbePlan, type Side } from './check-probe.js';
28
30
  import { type ScrubContext } from './check-scrub.js';
29
31
  import type { PairDeepFindings } from './check-deep.js';
32
+ import type { DispatchUnion } from './check-union.js';
30
33
  import { type PairFieldReport } from './check-fields.js';
31
34
  export interface RawDiagnostic {
32
35
  /** Workspace-relative, forward-slash file path (empty for global errors). */
@@ -66,6 +69,12 @@ export interface ClassifyInput {
66
69
  * run, in which case the tsc text stands alone.
67
70
  */
68
71
  fieldReport?: PairFieldReport;
72
+ /**
73
+ * Set when the sent side is a union the expected side takes only in part,
74
+ * every other member failing by nothing but the compiler's weak-type check
75
+ * (carrick#1995). Read only where the decisive assignment failed.
76
+ */
77
+ dispatchUnion?: DispatchUnion;
69
78
  /**
70
79
  * The side whose capture record says it reads the body as raw text
71
80
  * (carrick#1842), the sent side when both do. Read from the record, not the
@@ -14,6 +14,8 @@
14
14
  * IsFormBody gate fired (TS2344) -> unverifiable (form-encoded body)
15
15
  * IsByteBody gate fired (TS2344) -> unverifiable (bytes, even agreeing)
16
16
  * raw-text read marked on a side -> unverifiable (text, even agreeing)
17
+ * assignment error, sent union taken in part by a weak expected type
18
+ * -> unverifiable (carrick#1995)
17
19
  * assignment-class error -> incompatible
18
20
  * no diagnostics -> compatible [lowest precedence]
19
21
  *
@@ -220,6 +222,24 @@ export function classifyPair(input) {
220
222
  // the `string` it becomes) has no error on this line and is not a drift.
221
223
  const decisiveLine = decisiveAssignmentLine(plan);
222
224
  const assignDiag = probeDiags.find((d) => d.line === decisiveLine && ASSIGNMENT_CODES.has(d.code));
225
+ // 5a. Unless the mismatch is a sent union the expected type takes in part,
226
+ // the members it rejects sharing no field with a type whose every field
227
+ // is optional (carrick#1995). That is a route answering a body per
228
+ // branch read by a caller of one branch: no member breaks a reader whose
229
+ // every read allows absence, and which member the call receives is not
230
+ // in the types. Not a fact either way, and no side to retype.
231
+ if (assignDiag && input.dispatchUnion) {
232
+ const { members, agreeing } = input.dispatchUnion;
233
+ const sent = plan.direction.sent;
234
+ const expected = plan.direction.expected;
235
+ return {
236
+ ...base,
237
+ bucket: 'unverifiable',
238
+ gate: `${sent}:union`,
239
+ diagnostic: `the ${sent} type is a union of ${members} bodies; the ${expected} type, every field optional, accepts ${agreeing} of them and shares no field with the other ${members - agreeing}, so the call reads one of the bodies the route answers and the types do not say which.`,
240
+ ...notAFact(`the ${sent} type is a union of bodies and the ${expected} type, every field optional, accepts only some of them`),
241
+ };
242
+ }
223
243
  if (assignDiag) {
224
244
  const text = scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias);
225
245
  return {
@@ -28,6 +28,7 @@
28
28
  *
29
29
  * Seam: node builtins + `typescript` + this bundle only.
30
30
  */
31
+ import ts from 'typescript';
31
32
  import type { ProbePlan, Side } from './check-probe.js';
32
33
  import type { ProbeProgram } from './check-deep.js';
33
34
  /** What kind of difference one field path carries. */
@@ -69,6 +70,29 @@ export interface PairFieldReport {
69
70
  * how many more there are rather than pretending the list is complete.
70
71
  */
71
72
  export declare const MAX_NAMED_FIELDS = 8;
73
+ /**
74
+ * The two types the judge's decisive assignment compared for one plan, read in
75
+ * the probe program, with the compiler's own assignability relation.
76
+ * `undefined` when the probe or either alias cannot be read, or the compiler
77
+ * build exposes no relation: every statement made without them would be a
78
+ * guess.
79
+ *
80
+ * The sent side is whatever that assignment actually sent: the GraphQL
81
+ * comparand where one exists, then the JSON wire form where serialising
82
+ * changes the type. Reading `sent` there instead would describe a type the
83
+ * judge did not compare, which is the one way a reader of this pair could
84
+ * contradict it.
85
+ */
86
+ export declare function comparedTypes(opened: ProbeProgram, plan: ProbePlan): {
87
+ compared: ts.Type;
88
+ expected: {
89
+ type: ts.Type;
90
+ node: ts.Node;
91
+ };
92
+ wireApplied: boolean;
93
+ checker: ts.TypeChecker;
94
+ isAssignableTo: (source: ts.Type, target: ts.Type) => boolean;
95
+ } | undefined;
72
96
  /**
73
97
  * Field reports for every plan whose probe the program could read, keyed by
74
98
  * pair id. A plan with no entry has no report, which is not a claim that its
@@ -54,6 +54,56 @@ function assignabilityOf(checker) {
54
54
  const fn = checker.isTypeAssignableTo;
55
55
  return typeof fn === 'function' ? fn.bind(checker) : undefined;
56
56
  }
57
+ /**
58
+ * The two types the judge's decisive assignment compared for one plan, read in
59
+ * the probe program, with the compiler's own assignability relation.
60
+ * `undefined` when the probe or either alias cannot be read, or the compiler
61
+ * build exposes no relation: every statement made without them would be a
62
+ * guess.
63
+ *
64
+ * The sent side is whatever that assignment actually sent: the GraphQL
65
+ * comparand where one exists, then the JSON wire form where serialising
66
+ * changes the type. Reading `sent` there instead would describe a type the
67
+ * judge did not compare, which is the one way a reader of this pair could
68
+ * contradict it.
69
+ */
70
+ export function comparedTypes(opened, plan) {
71
+ const { program, checker, probesDir } = opened;
72
+ const isAssignableTo = assignabilityOf(checker);
73
+ if (!isAssignableTo)
74
+ return undefined;
75
+ const file = program.getSourceFile(`${probesDir}/probes/${plan.fileName}`.split('\\').join('/'));
76
+ const source = file ??
77
+ program.getSourceFiles().find((sf) => sf.fileName.endsWith(`/probes/${plan.fileName}`));
78
+ if (!source)
79
+ return undefined;
80
+ const declared = declaredConstType(source, checker, 'sentComparand') ??
81
+ declaredConstType(source, checker, 'sent');
82
+ const expected = declaredConstType(source, checker, 'expected');
83
+ if (!declared || !expected)
84
+ return undefined;
85
+ const wire = declaredConstType(source, checker, 'sentWire');
86
+ // Whether serialising changes anything observable about the sent type.
87
+ const wireApplied = wire !== undefined &&
88
+ !(isAssignableTo(wire.type, declared.type) && isAssignableTo(declared.type, wire.type));
89
+ // The DECLARED type unless serialising really changed it.
90
+ //
91
+ // The probe declares its wire comparand through a conditional alias that
92
+ // short-circuits back to the declared type whenever that already assigns,
93
+ // and a conditional the checker has not had to resolve carries no members
94
+ // to walk. Reading it unconditionally therefore emptied the field report on
95
+ // exactly the pairs that AGREE — the ones whose only statement is an
96
+ // optionality gap or the wire note (carrick#1341). Where serialisation did
97
+ // change the type the wire form is a mapped type with real members, and it
98
+ // stays the thing compared, because that is what the judge judged.
99
+ return {
100
+ compared: wireApplied ? wire.type : declared.type,
101
+ expected,
102
+ wireApplied,
103
+ checker,
104
+ isAssignableTo,
105
+ };
106
+ }
57
107
  /**
58
108
  * Field reports for every plan whose probe the program could read, keyed by
59
109
  * pair id. A plan with no entry has no report, which is not a claim that its
@@ -63,44 +113,12 @@ export function pairFieldReports(opened, plans) {
63
113
  const results = new Map();
64
114
  if (!opened)
65
115
  return results;
66
- const { program, checker, probesDir } = opened;
67
- const isAssignableTo = assignabilityOf(checker);
68
- if (!isAssignableTo)
69
- return results;
70
116
  for (const plan of plans) {
71
- const file = program.getSourceFile(`${probesDir}/probes/${plan.fileName}`.split('\\').join('/'));
72
- const source = file ??
73
- program
74
- .getSourceFiles()
75
- .find((sf) => sf.fileName.endsWith(`/probes/${plan.fileName}`));
76
- if (!source)
77
- continue;
78
- // Whatever the judge's decisive assignment actually sent: the GraphQL
79
- // comparand where one exists, then the JSON wire form where one exists.
80
- // Reading `sent` there instead would describe a type the judge did not
81
- // compare, which is the one way this walk could contradict it.
82
- const declared = declaredConstType(source, checker, 'sentComparand') ??
83
- declaredConstType(source, checker, 'sent');
84
- const expected = declaredConstType(source, checker, 'expected');
85
- if (!declared || !expected)
117
+ const pair = comparedTypes(opened, plan);
118
+ if (!pair)
86
119
  continue;
87
- const wire = declaredConstType(source, checker, 'sentWire');
88
- // Whether serialising changes anything observable about the sent type.
89
- const wireChanges = wire !== undefined &&
90
- !(isAssignableTo(wire.type, declared.type) && isAssignableTo(declared.type, wire.type));
91
- // Walk the DECLARED type unless serialising really changed it.
92
- //
93
- // The probe declares its wire comparand through a conditional alias that
94
- // short-circuits back to the declared type whenever that already assigns,
95
- // and a conditional the checker has not had to resolve carries no members
96
- // to walk. Reading it unconditionally therefore emptied the report on
97
- // exactly the pairs that AGREE — the ones whose only statement is an
98
- // optionality gap or the wire note (carrick#1341). Where serialisation
99
- // did change the type the wire form is a mapped type with real members,
100
- // and it stays the thing compared, because that is what the judge judged.
101
- const compared = wireChanges ? wire.type : declared.type;
102
- const report = diffReport(compared, expected.type, checker, isAssignableTo, expected.node, plan.spec.protocol === 'graphql' ? GRAPHQL_SERVER_SUPPLIED : NONE_SERVER_SUPPLIED);
103
- report.wireApplied = wireChanges;
120
+ const report = diffReport(pair.compared, pair.expected.type, pair.checker, pair.isAssignableTo, pair.expected.node, plan.spec.protocol === 'graphql' ? GRAPHQL_SERVER_SUPPLIED : NONE_SERVER_SUPPLIED);
121
+ report.wireApplied = pair.wireApplied;
104
122
  results.set(plan.pairId, report);
105
123
  }
106
124
  return results;
@@ -27,7 +27,7 @@
27
27
  import ts from 'typescript';
28
28
  import * as fs from 'node:fs';
29
29
  import * as path from 'node:path';
30
- import { collectSpecifiers, isRelative } from './specifiers.js';
30
+ import { collectSpecifiers, declarationCandidates, isDeclarationFileName, isRelative, } from './specifiers.js';
31
31
  /** One index per assembled stub, keyed by service name. */
32
32
  export function buildPoisonIndexes(ws) {
33
33
  const indexes = new Map();
@@ -45,14 +45,14 @@ function buildOne(ws, serviceName, packageDir) {
45
45
  return undefined;
46
46
  const keyPrefix = `packages/${packageDir}/types`;
47
47
  const surfaceFile = `${keyPrefix}/surface.d.ts`;
48
- // Collect the whole `.d.ts` tree, keyed as `packages/<dir>/types/<rel>`.
48
+ // Collect the whole declaration tree, keyed as `packages/<dir>/types/<rel>`.
49
49
  const treeAbs = [];
50
50
  const walk = (dir) => {
51
51
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
52
52
  const p = path.join(dir, entry.name);
53
53
  if (entry.isDirectory())
54
54
  walk(p);
55
- else if (entry.name.endsWith('.d.ts'))
55
+ else if (isDeclarationFileName(entry.name))
56
56
  treeAbs.push(p);
57
57
  }
58
58
  };
@@ -63,17 +63,7 @@ function buildOne(ws, serviceName, packageDir) {
63
63
  if (!isRelative(spec))
64
64
  return undefined;
65
65
  const base = path.resolve(path.dirname(fromAbs), spec);
66
- const candidates = [
67
- `${base}.d.ts`,
68
- path.join(base, 'index.d.ts'),
69
- base.endsWith('.js') ? `${base.slice(0, -3)}.d.ts` : undefined,
70
- base, // already `.d.ts`
71
- ].filter((c) => c !== undefined);
72
- for (const c of candidates) {
73
- if (treeAbsSet.has(path.resolve(c)))
74
- return c;
75
- }
76
- return undefined;
66
+ return declarationCandidates(base).find((candidate) => treeAbsSet.has(candidate));
77
67
  };
78
68
  // Per-file relative-import adjacency, over the fileKey namespace.
79
69
  const adjacency = new Map();
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A sent union whose members the expected type takes only in part, where the
3
+ * rest fail by nothing but the compiler's weak-type check (carrick#1995).
4
+ *
5
+ * A handler that answers a different body per branch (an action that switches
6
+ * on a field of the request) publishes the union of those bodies. A caller of
7
+ * one branch reads the response with a type whose every field is optional. The
8
+ * compiler then rejects each member that shares no field with that type
9
+ * (TS2559, "has no properties in common"): a type whose every property is
10
+ * optional is a "weak type", and the check exists to catch a misspelt property
11
+ * bag. It is not a soundness rule. At runtime every member satisfies a type
12
+ * whose reads all allow absence, and the call only ever receives the body of
13
+ * the branch it asked for, which the types do not say.
14
+ *
15
+ * So the pair is neither compatible nor incompatible: the judge cannot say
16
+ * which member the call receives. Kept narrow on purpose:
17
+ * - the sent type must be a union with at least one member that assigns;
18
+ * - the expected type must be one object type whose every property is
19
+ * optional, with no index or call signatures;
20
+ * - every member that does not assign must be an object with properties, none
21
+ * of which the expected type names.
22
+ * A sent type that is not a union and shares no field with a weak expected
23
+ * type is the misspelling the check exists for, and stays a mismatch.
24
+ *
25
+ * Read in the probe program, over the two types the decisive assignment
26
+ * compared, with the compiler's own relation. Seam: typescript + this bundle.
27
+ */
28
+ import ts from 'typescript';
29
+ import type { ProbePlan } from './check-probe.js';
30
+ import type { ProbeProgram } from './check-deep.js';
31
+ export interface DispatchUnion {
32
+ /** Members of the sent union. */
33
+ members: number;
34
+ /** Members the expected type takes. */
35
+ agreeing: number;
36
+ }
37
+ /** The finding for every plan whose compared types have this shape, by pair id. */
38
+ export declare function pairDispatchUnions(opened: ProbeProgram | undefined, plans: ProbePlan[]): Map<string, DispatchUnion>;
39
+ /** The shape above for one compared pair, or `undefined` when it does not hold. */
40
+ export declare function dispatchUnion(sent: ts.Type, expected: ts.Type, checker: ts.TypeChecker, isAssignableTo: (source: ts.Type, target: ts.Type) => boolean): DispatchUnion | undefined;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * A sent union whose members the expected type takes only in part, where the
3
+ * rest fail by nothing but the compiler's weak-type check (carrick#1995).
4
+ *
5
+ * A handler that answers a different body per branch (an action that switches
6
+ * on a field of the request) publishes the union of those bodies. A caller of
7
+ * one branch reads the response with a type whose every field is optional. The
8
+ * compiler then rejects each member that shares no field with that type
9
+ * (TS2559, "has no properties in common"): a type whose every property is
10
+ * optional is a "weak type", and the check exists to catch a misspelt property
11
+ * bag. It is not a soundness rule. At runtime every member satisfies a type
12
+ * whose reads all allow absence, and the call only ever receives the body of
13
+ * the branch it asked for, which the types do not say.
14
+ *
15
+ * So the pair is neither compatible nor incompatible: the judge cannot say
16
+ * which member the call receives. Kept narrow on purpose:
17
+ * - the sent type must be a union with at least one member that assigns;
18
+ * - the expected type must be one object type whose every property is
19
+ * optional, with no index or call signatures;
20
+ * - every member that does not assign must be an object with properties, none
21
+ * of which the expected type names.
22
+ * A sent type that is not a union and shares no field with a weak expected
23
+ * type is the misspelling the check exists for, and stays a mismatch.
24
+ *
25
+ * Read in the probe program, over the two types the decisive assignment
26
+ * compared, with the compiler's own relation. Seam: typescript + this bundle.
27
+ */
28
+ import ts from 'typescript';
29
+ import { comparedTypes } from './check-fields.js';
30
+ /** The finding for every plan whose compared types have this shape, by pair id. */
31
+ export function pairDispatchUnions(opened, plans) {
32
+ const results = new Map();
33
+ if (!opened)
34
+ return results;
35
+ for (const plan of plans) {
36
+ const pair = comparedTypes(opened, plan);
37
+ if (!pair)
38
+ continue;
39
+ const finding = dispatchUnion(pair.compared, pair.expected.type, pair.checker, pair.isAssignableTo);
40
+ if (finding)
41
+ results.set(plan.pairId, finding);
42
+ }
43
+ return results;
44
+ }
45
+ /** The shape above for one compared pair, or `undefined` when it does not hold. */
46
+ export function dispatchUnion(sent, expected, checker, isAssignableTo) {
47
+ if (!sent.isUnion())
48
+ return undefined;
49
+ const names = weakPropertyNames(expected, checker);
50
+ if (!names)
51
+ return undefined;
52
+ let agreeing = 0;
53
+ for (const member of sent.types) {
54
+ if (isAssignableTo(member, expected)) {
55
+ agreeing += 1;
56
+ continue;
57
+ }
58
+ if (!sharesNoProperty(member, names, checker))
59
+ return undefined;
60
+ }
61
+ if (agreeing === 0 || agreeing === sent.types.length)
62
+ return undefined;
63
+ return { members: sent.types.length, agreeing };
64
+ }
65
+ /**
66
+ * The property names of a weak object type: one object type, at least one
67
+ * property, every property optional, no index signature and no call or
68
+ * construct signature. `undefined` for any other type.
69
+ */
70
+ function weakPropertyNames(type, checker) {
71
+ if (!(type.flags & ts.TypeFlags.Object))
72
+ return undefined;
73
+ if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 ||
74
+ checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0 ||
75
+ checker.getIndexInfosOfType(type).length > 0) {
76
+ return undefined;
77
+ }
78
+ const properties = checker.getPropertiesOfType(type);
79
+ if (properties.length === 0)
80
+ return undefined;
81
+ if (!properties.every((property) => (property.flags & ts.SymbolFlags.Optional) !== 0)) {
82
+ return undefined;
83
+ }
84
+ return new Set(properties.map((property) => property.name));
85
+ }
86
+ /** Whether `member` is an object with properties, none of them in `names`. */
87
+ function sharesNoProperty(member, names, checker) {
88
+ if (!(member.flags & ts.TypeFlags.Object))
89
+ return false;
90
+ const properties = checker.getPropertiesOfType(member);
91
+ return properties.length > 0 && properties.every((property) => !names.has(property.name));
92
+ }
@@ -26,6 +26,7 @@ import { assembleWorkspace, writeProbes, } from './check-workspace.js';
26
26
  import { buildPoisonIndexes } from './check-poison.js';
27
27
  import { openProbeProgram, probeDeepFindings } from './check-deep.js';
28
28
  import { pairFieldReports } from './check-fields.js';
29
+ import { pairDispatchUnions } from './check-union.js';
29
30
  function runProcess(command, args, cwd) {
30
31
  return new Promise((resolve, reject) => {
31
32
  const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
@@ -262,6 +263,9 @@ export async function runCheck(opts, onProgress) {
262
263
  // incompatible (carrick-tools/carrick-cloud#1118). It names what the verdict
263
264
  // is about; it never decides one.
264
265
  const fieldsByPair = pairFieldReports(probeProgram, probing);
266
+ // And which pairs compared a union the expected side takes only in part,
267
+ // the rest failing by nothing but the weak-type check (carrick#1995).
268
+ const unionsByPair = pairDispatchUnions(probeProgram, probing);
265
269
  const verdicts = sortVerdicts([
266
270
  ...probing.map((plan) => classifyPair({
267
271
  plan,
@@ -270,6 +274,7 @@ export async function runCheck(opts, onProgress) {
270
274
  scrubCtx,
271
275
  deepFindings: deepByPair.get(plan.pairId),
272
276
  fieldReport: fieldsByPair.get(plan.pairId),
277
+ dispatchUnion: unionsByPair.get(plan.pairId),
273
278
  rawTextSide: rawTextSideOf(plan, aliasRecords),
274
279
  })),
275
280
  ...preGated,