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
@@ -19,12 +19,16 @@
19
19
  */
20
20
  import * as path from 'node:path';
21
21
  import { Node, SyntaxKind, ts, } from 'ts-morph';
22
+ import { typeToStringAsDeclared } from './print-type.js';
22
23
  import { validateInferRequestItem } from './validators.js';
23
24
  import { notePrintedType, PrintedTypes } from './printed-names.js';
24
25
  import { externalImportsOf, isExternalOrigin } from './origin.js';
25
26
  import { reachedOnlyOnFailure } from './failure-path.js';
27
+ import { readResponseModes } from './response-modes.js';
26
28
  import { functionAtLine } from './function-line-index.js';
29
+ import { endLineOf, startLineOf } from './line-index.js';
27
30
  import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
31
+ import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
28
32
  import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
29
33
  import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
30
34
  /**
@@ -168,11 +172,19 @@ const RESPONSE_HELPER_MAX_DEPTH = 4;
168
172
  * the branch an error path, whose shape is not the endpoint's contract.
169
173
  */
170
174
  const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
171
- /** `value` when it is an integer in the HTTP status range, else `undefined`. */
172
- function httpStatus(value) {
173
- return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599
174
- ? value
175
- : undefined;
175
+ /**
176
+ * The items whose whitespace-collapsed texts differ, first of each kept, in
177
+ * order: how a joined union drops a member it already has.
178
+ */
179
+ function distinctByText(items, text) {
180
+ const seen = new Set();
181
+ return items.filter((item) => {
182
+ const key = text(item).replace(/\s+/g, ' ').trim();
183
+ if (seen.has(key))
184
+ return false;
185
+ seen.add(key);
186
+ return true;
187
+ });
176
188
  }
177
189
  /**
178
190
  * The status codes a TYPE fixes: a numeric literal, or a union made only of
@@ -192,16 +204,6 @@ function statusCodesOfType(type) {
192
204
  }
193
205
  return codes.length > 0 ? codes : undefined;
194
206
  }
195
- /** One verdict for a set of status codes, or `mixed` when they disagree. */
196
- function classifyStatusCodes(codes) {
197
- if (codes.every((code) => code >= 400))
198
- return 'error';
199
- if (codes.every((code) => code >= 300 && code < 400))
200
- return 'redirect';
201
- if (codes.every((code) => code < 300))
202
- return 'success';
203
- return 'mixed';
204
- }
205
207
  /**
206
208
  * The parts of a request that ARE the body, as validator middleware names them
207
209
  * (`validate('json', Schema)`). HTTP vocabulary for how a body is carried — the
@@ -242,8 +244,12 @@ const RAW_TEXT_FORMAT = 'text';
242
244
  const TYPE_TEXT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias;
243
245
  function typeText(type, enclosingNode) {
244
246
  // Timed as the print (carrick#1985): the type is already computed when it
245
- // gets here, so this is what its length costs.
246
- const text = timedPhase('print', () => type.getText(enclosingNode, TYPE_TEXT_FLAGS));
247
+ // gets here, so this is what its length costs. A module the print names is
248
+ // written as its declaring file, whichever file it is printed from
249
+ // (carrick#2019).
250
+ const text = timedPhase('print', () => enclosingNode
251
+ ? typeToStringAsDeclared(enclosingNode.getProject().getTypeChecker().compilerObject, type.compilerType, enclosingNode.compilerNode, TYPE_TEXT_FLAGS)
252
+ : type.getText(undefined, TYPE_TEXT_FLAGS));
247
253
  notePrintedType(type, enclosingNode, text);
248
254
  return text;
249
255
  }
@@ -968,7 +974,7 @@ export class TypeInferrer {
968
974
  Node.isArrowFunction(ancestor) ||
969
975
  Node.isFunctionExpression(ancestor) ||
970
976
  Node.isMethodDeclaration(ancestor)) &&
971
- ancestor.getStartLineNumber() === request.line_number);
977
+ startLineOf(ancestor) === request.line_number);
972
978
  const declaredHandler = insideFunctionOnLine
973
979
  ? this.handlerDeclaredAtLine(sourceFile, request.line_number)
974
980
  : undefined;
@@ -1125,7 +1131,7 @@ export class TypeInferrer {
1125
1131
  */
1126
1132
  exportedFunctionOpeningOn(sourceFile, line) {
1127
1133
  const declared = this.findFunctionByLine(sourceFile, line);
1128
- if (!declared || declared.getStartLineNumber() !== line)
1134
+ if (!declared || startLineOf(declared) !== line)
1129
1135
  return undefined;
1130
1136
  if (Node.isFunctionDeclaration(declared)) {
1131
1137
  return Node.isSourceFile(declared.getParent()) && declared.isExported()
@@ -1294,7 +1300,10 @@ export class TypeInferrer {
1294
1300
  const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
1295
1301
  ? this.primaryTypeSymbolSource(recoveredAnchor.element)
1296
1302
  : undefined;
1297
- return this.createInferredType(request, recovered.typeString, recovered.isExplicit, this.getNodeLocation(recovered.node), undefined, resolvedSymbol ?? writtenAnchor?.symbol, writtenAnchor ? writtenAnchor.depth : recoveredAnchor?.depth, writtenAnchor ? writtenAnchor.source : resolvedSource);
1303
+ const inferred = this.createInferredType(request, recovered.typeString, recovered.isExplicit, this.getNodeLocation(recovered.node), undefined, resolvedSymbol ?? writtenAnchor?.symbol, writtenAnchor ? writtenAnchor.depth : recoveredAnchor?.depth, writtenAnchor ? writtenAnchor.source : resolvedSource);
1304
+ if (recovered.responseModes)
1305
+ inferred.response_modes = recovered.responseModes;
1306
+ return inferred;
1298
1307
  }
1299
1308
  /**
1300
1309
  * carrick#1161: type a route response from every send in the handler the
@@ -1467,7 +1476,9 @@ export class TypeInferrer {
1467
1476
  // carrier read gives its own. A result that is no carrier keeps the
1468
1477
  // answer it had.
1469
1478
  const where = `${request.file_path}:${request.line_number}`;
1470
- const resultIsCarrier = !explicitType && this.resultCarrierArguments(returnType, terminalNode) !== undefined;
1479
+ const callArgs = this.callResultArguments(callExpr);
1480
+ const resultIsCarrier = !explicitType &&
1481
+ this.resultCarrierArguments(returnType, terminalNode, callArgs) !== undefined;
1471
1482
  // carrick#1877: the terminal holds a thenable the names do not peel, a
1472
1483
  // subclass of `Promise` or a class with a `then` of its own. What a
1473
1484
  // caller receives is what `await` yields for it, as it is for the
@@ -1516,12 +1527,22 @@ export class TypeInferrer {
1516
1527
  const readThroughCarrier = unwrapResult.wasUnwrapped &&
1517
1528
  resultIsCarrier &&
1518
1529
  !!afterRules &&
1519
- this.resultCarrierArguments(afterRules, terminalNode) === undefined;
1520
- const carrierCandidate = explicitType || !afterRules
1530
+ this.resultCarrierArguments(afterRules, terminalNode, callArgs) === undefined;
1531
+ const carrierRead = explicitType || !afterRules
1521
1532
  ? undefined
1522
1533
  : readThroughCarrier
1523
- ? afterRules
1524
- : this.resultCarrierPayload(afterRules, terminalNode, use.projections, where);
1534
+ ? { payload: afterRules }
1535
+ : this.resultCarrierPayload(afterRules, terminalNode, use.projections, where, callArgs);
1536
+ // carrick#1841: a carrier built from a response table whose success side
1537
+ // states no body (a `204` row), or whose side cannot be told, states no
1538
+ // response contract. The abstain is decided for the same reason as the
1539
+ // transport abstain above: left undecided, the capture's own locator
1540
+ // re-reads the raw call and publishes the carrier.
1541
+ if (carrierRead && 'abstain' in carrierRead) {
1542
+ this.log(`Call result at ${where}: ${carrierRead.abstain}`);
1543
+ return this.transportAbstain(request, callExpr, carrierRead.abstain);
1544
+ }
1545
+ const carrierCandidate = carrierRead?.payload;
1525
1546
  // carrick#1841: what the carrier holds goes through the service's wrapper
1526
1547
  // rules before it is published, as the call's own result did. The carrier
1527
1548
  // is found by its shape, so what it holds can still be a library's
@@ -1653,7 +1674,7 @@ export class TypeInferrer {
1653
1674
  const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
1654
1675
  const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
1655
1676
  !!callAfterRules &&
1656
- this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
1677
+ this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
1657
1678
  const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
1658
1679
  let anchor = anchorSource
1659
1680
  ? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
@@ -2461,13 +2482,57 @@ export class TypeInferrer {
2461
2482
  * Where neither decides, the carrier keeps its own answer and the limit is
2462
2483
  * logged: a coin flip published as a contract is worse than an envelope a
2463
2484
  * reader can see is an envelope.
2464
- */
2465
- resultCarrierPayload(type, at, projections, where) {
2466
- const shape = this.resultCarrierArguments(type, at);
2485
+ *
2486
+ * carrick#1841: an argument that is a RESPONSE TABLE (`{ 200: Item }`, every
2487
+ * key a status code) says which side it is by its keys, and that decides
2488
+ * first: the one table whose keys are all 2xx is the success side, and what
2489
+ * a caller receives is the table's body, the member the branch holds, never
2490
+ * the table. A success table whose rows state no body (`{ 204: void }`)
2491
+ * states no response contract. A carrier built from tables, or found
2492
+ * through the call's arguments, that nothing decides states none either:
2493
+ * the union is the client's bookkeeping, not a body, so it is not published
2494
+ * as written.
2495
+ */
2496
+ resultCarrierPayload(type, at, projections, where, callArgs) {
2497
+ const shape = this.resultCarrierArguments(type, at, callArgs);
2467
2498
  if (!shape) {
2468
2499
  return undefined;
2469
2500
  }
2470
- const { carrier, carried } = shape;
2501
+ const { carrier, carried, bodies, readOffCall } = shape;
2502
+ const checker = this.project.getTypeChecker().compilerObject;
2503
+ const tables = carried.map((arg) => {
2504
+ const entries = statusTableOf(checker, arg.compilerType);
2505
+ return {
2506
+ arg,
2507
+ entries,
2508
+ side: entries ? classifyStatusCodes(entries.map((entry) => entry.code)) : undefined,
2509
+ };
2510
+ });
2511
+ const successTables = tables.filter((table) => table.side === 'success');
2512
+ if (successTables.length === 1) {
2513
+ const { arg, entries } = successTables[0];
2514
+ const body = bodies.get(arg) ?? this.singleSuccessValue(arg, entries ?? [], at);
2515
+ if (body && statesNoBody(body.compilerType)) {
2516
+ return {
2517
+ abstain: `the call answers a carrier whose success side is the response table ` +
2518
+ `'${typeText(arg, at)}', and its success rows state no body, so this site ` +
2519
+ 'states no response contract',
2520
+ };
2521
+ }
2522
+ if (body) {
2523
+ return { payload: body };
2524
+ }
2525
+ }
2526
+ const builtFromTables = tables.some((table) => table.side !== undefined);
2527
+ const undecided = (detail) => {
2528
+ this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}): ${detail}`);
2529
+ return builtFromTables || readOffCall
2530
+ ? {
2531
+ abstain: `the call answers a carrier (${typeText(carrier, at)}) whose success side ` +
2532
+ 'cannot be told from its failure side, so this site states no response contract',
2533
+ }
2534
+ : undefined;
2535
+ };
2471
2536
  const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
2472
2537
  const read = new Set();
2473
2538
  for (const projection of projections) {
@@ -2486,27 +2551,69 @@ export class TypeInferrer {
2486
2551
  // argument out than the shape test picked, the two disagree about which
2487
2552
  // side is which and nothing here knows better than the source does.
2488
2553
  if (byShape && byRead && byShape !== byRead) {
2489
- this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose error ` +
2490
- `shape names '${typeText(byShape, at)}' as the payload while the source reads ` +
2491
- `'${typeText(byRead, at)}' out of it. Publishing the carrier as written rather ` +
2492
- 'than picking one');
2554
+ return undecided(`its error shape names '${typeText(byShape, at)}' as the payload while the source ` +
2555
+ `reads '${typeText(byRead, at)}' out of it. Not picking one`);
2556
+ }
2557
+ const decided = byShape ?? byRead;
2558
+ if (decided) {
2559
+ return { payload: bodies.get(decided) ?? decided };
2560
+ }
2561
+ return undecided('its success side cannot be told from its failure side: no single argument is the ' +
2562
+ 'only non-error one and the source reads none of them out. Not guessing which ' +
2563
+ 'argument is the payload');
2564
+ }
2565
+ /**
2566
+ * The value of a response table's one success row, for a table a carrier
2567
+ * holds as itself rather than through its body. `undefined` where the table
2568
+ * has more than one success row: their union has no single type here.
2569
+ */
2570
+ singleSuccessValue(table, entries, at) {
2571
+ const success = successEntries(entries);
2572
+ if (success.length !== 1)
2493
2573
  return undefined;
2574
+ return table.getProperty(success[0].key)?.getTypeAtLocation(at);
2575
+ }
2576
+ /**
2577
+ * carrick#1841: the type arguments a call's result was built from, for a
2578
+ * result that no longer carries them. A client whose result type is a
2579
+ * conditional alias (`Result<TData, TError, ThrowOnError>`) resolves to its
2580
+ * branch at the call, and the branch is an anonymous union with no
2581
+ * arguments of its own. The arguments are still stated in two places the
2582
+ * compiler keeps:
2583
+ *
2584
+ * - the call's own explicit type arguments (`client.get<Table, Errors>(…)`);
2585
+ * - the alias the callee's declared return type was written with, where
2586
+ * the callee is a function wrapping such a call
2587
+ * (`getItem<ThrowOnError>(…)` returning `Result<Table, Errors, ThrowOnError>`).
2588
+ *
2589
+ * An argument read here names a payload only where a branch of the result
2590
+ * holds it (`resultCarrierArguments`), so a type parameter of the callee or
2591
+ * a style flag carries nothing.
2592
+ */
2593
+ callResultArguments(callExpr) {
2594
+ const explicit = callExpr.getTypeArguments().map((node) => node.getType());
2595
+ if (explicit.length > 0)
2596
+ return explicit;
2597
+ try {
2598
+ const declaration = this.project
2599
+ .getTypeChecker()
2600
+ .getResolvedSignature(callExpr)
2601
+ ?.getDeclaration();
2602
+ if (declaration && Node.isReturnTyped(declaration)) {
2603
+ return declaration.getReturnType().getAliasTypeArguments();
2604
+ }
2494
2605
  }
2495
- if (byShape ?? byRead) {
2496
- return byShape ?? byRead;
2606
+ catch {
2607
+ // A signature the checker cannot resolve states no arguments.
2497
2608
  }
2498
- this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose success ` +
2499
- 'side cannot be told from its failure side: no single argument is the only ' +
2500
- 'non-error one and the source reads none of them out. Publishing the carrier as ' +
2501
- 'written rather than guessing which argument is the payload');
2502
- return undefined;
2609
+ return [];
2503
2610
  }
2504
2611
  /**
2505
2612
  * The shape test of `resultCarrierPayload`: the carrier `type` is, once a
2506
2613
  * promise-like around it is peeled, and the type arguments a branch of it
2507
2614
  * holds as a member. `undefined` when `type` is not a carrier.
2508
2615
  */
2509
- resultCarrierArguments(type, at) {
2616
+ resultCarrierArguments(type, at, callArgs = []) {
2510
2617
  const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
2511
2618
  if (!carrier.isUnion()) {
2512
2619
  return undefined;
@@ -2515,26 +2622,80 @@ export class TypeInferrer {
2515
2622
  if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
2516
2623
  return undefined;
2517
2624
  }
2518
- const args = [
2519
- ...carrier.getAliasTypeArguments(),
2520
- ...carrier.getTypeArguments(),
2521
- ];
2625
+ const own = [...carrier.getAliasTypeArguments(), ...carrier.getTypeArguments()];
2626
+ // carrick#1841: a carrier with no arguments of its own is read through the
2627
+ // arguments its call was built from (`callResultArguments`).
2628
+ const readOffCall = own.length === 0;
2629
+ const args = readOffCall ? callArgs : own;
2522
2630
  if (args.length < 2) {
2523
2631
  return undefined;
2524
2632
  }
2633
+ // carrick#1841: on a carrier read off the call, a member every branch
2634
+ // holds with one type (the request and response objects a client hands
2635
+ // back beside the result) is the transport's, not a side of the result.
2636
+ const transport = readOffCall ? this.membersCommonToEveryBranch(branches, at) : new Set();
2637
+ const checker = this.project.getTypeChecker().compilerObject;
2638
+ // What a branch member holding a response table's body looks like: the
2639
+ // table indexed by its own keys (`Table[keyof Table]`), which the compiler
2640
+ // resolves to the union of the table's values.
2641
+ const tableValues = new Map();
2642
+ for (const arg of args) {
2643
+ const entries = statusTableOf(checker, arg.compilerType);
2644
+ if (entries) {
2645
+ tableValues.set(arg, new Set(entries.flatMap((entry) => constituents(checker.getTypeOfSymbolAtLocation(entry.symbol, at.compilerNode)))));
2646
+ }
2647
+ }
2648
+ const bodies = new Map();
2525
2649
  // A type argument only names a payload when a branch actually holds it:
2526
2650
  // a generic that parameterises a status code or a key carries nothing.
2527
- const carried = args.filter((arg) => branches.some((branch) => branch
2528
- .getProperties()
2529
- .some((property) => {
2651
+ const carried = args.filter((arg) => branches.some((branch) => branch.getProperties().some((property) => {
2652
+ if (transport.has(property.getName()))
2653
+ return false;
2530
2654
  try {
2531
- return property.getTypeAtLocation(at).getText() === arg.getText();
2655
+ const held = property.getTypeAtLocation(at);
2656
+ if (held.getText() === arg.getText())
2657
+ return true;
2658
+ const values = tableValues.get(arg);
2659
+ const heldTypes = constituents(held.compilerType);
2660
+ if (values &&
2661
+ heldTypes.length === values.size &&
2662
+ heldTypes.every((member) => values.has(member))) {
2663
+ bodies.set(arg, held);
2664
+ return true;
2665
+ }
2666
+ return false;
2532
2667
  }
2533
2668
  catch {
2534
2669
  return false;
2535
2670
  }
2536
2671
  })));
2537
- return carried.length === 0 ? undefined : { carrier, carried };
2672
+ return carried.length === 0 ? undefined : { carrier, carried, bodies, readOffCall };
2673
+ }
2674
+ /** The member names every branch holds, each with one and the same type. */
2675
+ membersCommonToEveryBranch(branches, at) {
2676
+ const [first, ...rest] = branches;
2677
+ const common = new Set();
2678
+ for (const property of first.getProperties()) {
2679
+ const name = property.getName();
2680
+ let held;
2681
+ try {
2682
+ held = property.getTypeAtLocation(at);
2683
+ }
2684
+ catch {
2685
+ continue;
2686
+ }
2687
+ const same = rest.every((branch) => {
2688
+ try {
2689
+ return branch.getProperty(name)?.getTypeAtLocation(at).compilerType === held.compilerType;
2690
+ }
2691
+ catch {
2692
+ return false;
2693
+ }
2694
+ });
2695
+ if (same)
2696
+ common.add(name);
2697
+ }
2698
+ return common;
2538
2699
  }
2539
2700
  /**
2540
2701
  * The decided abstain of a call whose result carries transport the
@@ -2544,16 +2705,16 @@ export class TypeInferrer {
2544
2705
  * re-reading the raw call (`inference_decided_no_contract`,
2545
2706
  * engine/type_compat_v2.rs).
2546
2707
  */
2547
- transportAbstain(request, callExpr) {
2708
+ transportAbstain(request, callExpr, detail = "what this call's result carries is transport that the service's wrapper " +
2709
+ 'rules verify and read no payload out of (a library response object around the body), ' +
2710
+ 'so this site states no response contract') {
2548
2711
  const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
2549
2712
  abstain.any_provenance = [
2550
2713
  {
2551
2714
  path: '',
2552
2715
  kind: 'unknown',
2553
2716
  reason: 'machinery_envelope',
2554
- detail: "what this call's result carries is transport that the service's wrapper rules " +
2555
- 'verify and read no payload out of (a library response object around the body), ' +
2556
- 'so this site states no response contract',
2717
+ detail,
2557
2718
  },
2558
2719
  ];
2559
2720
  return abstain;
@@ -3913,17 +4074,13 @@ export class TypeInferrer {
3913
4074
  const isEmptyObject = (text) => text.replace(/\s+/g, '') === '{}';
3914
4075
  const withMembers = candidates.filter((c) => !isEmptyObject(c.typeString));
3915
4076
  const kept = withMembers.length > 0 ? withMembers : candidates;
3916
- // Dedupe on the whitespace-collapsed text, preserving source order.
3917
- const seen = new Set();
3918
- const distinct = kept.filter((c) => {
3919
- const key = c.typeString.replace(/\s+/g, ' ').trim();
3920
- if (seen.has(key))
3921
- return false;
3922
- seen.add(key);
3923
- return true;
3924
- });
4077
+ const distinct = distinctByText(kept, (c) => c.typeString);
3925
4078
  const typeString = distinct.map((c) => c.typeString).join(' | ');
3926
4079
  return {
4080
+ // carrick#2054: which request field picks each member of the union.
4081
+ responseModes: distinct.length >= 2
4082
+ ? this.responseModesOf(kept.map((c) => ({ node: c.node, type_string: c.typeString })))
4083
+ : undefined,
3927
4084
  typeString,
3928
4085
  // Every payload node that fed the union, deduped ones included: a
3929
4086
  // caller asks whether the expression it located is one of them.
@@ -3937,6 +4094,20 @@ export class TypeInferrer {
3937
4094
  statedTypeNode: distinct.length === 1 ? distinct[0].statedTypeNode : undefined,
3938
4095
  };
3939
4096
  }
4097
+ /**
4098
+ * The request field each member of a joined union answers
4099
+ * (`response-modes.ts`). Case texts are joined by the published union's
4100
+ * rule. A reading that fails is no reading: it must never cost the union.
4101
+ */
4102
+ responseModesOf(bodies) {
4103
+ try {
4104
+ return readResponseModes(bodies, (texts) => distinctByText(texts, (text) => text).join(' | '), (symbol) => this.symbolIsLibOrExternalOrigin(symbol));
4105
+ }
4106
+ catch (error) {
4107
+ this.log(`Response modes not read: ${error instanceof Error ? error.message : String(error)}`);
4108
+ return undefined;
4109
+ }
4110
+ }
3940
4111
  /**
3941
4112
  * The expressions a function returns, excluding returns belonging to nested
3942
4113
  * functions (a `.map(x => …)` callback returns a row, not the response).
@@ -4625,7 +4796,7 @@ export class TypeInferrer {
4625
4796
  */
4626
4797
  declaringPackageOf(type) {
4627
4798
  const symbol = type.getSymbol() ?? type.getAliasSymbol();
4628
- const declaration = symbol?.getDeclarations()?.[0];
4799
+ const declaration = this.attributedDeclaration(symbol?.getDeclarations() ?? []);
4629
4800
  if (!declaration) {
4630
4801
  return undefined;
4631
4802
  }
@@ -4648,6 +4819,28 @@ export class TypeInferrer {
4648
4819
  }
4649
4820
  return rest[0];
4650
4821
  }
4822
+ /**
4823
+ * The declaration a symbol is attributed to (carrick#2031). One declaration
4824
+ * is itself. Where several files merge one symbol (a global interface the
4825
+ * compiler's library declares and a typings package augments), their order
4826
+ * is the order the checker merged them in, and on TypeScript 6 that changes
4827
+ * once the program is rebuilt, so the first one is not an answer. The
4828
+ * compiler's default library wins where it declares the symbol; otherwise,
4829
+ * and among the library's own files, the declaring file whose path sorts
4830
+ * first, then the earliest declaration in it.
4831
+ */
4832
+ attributedDeclaration(declarations) {
4833
+ if (declarations.length <= 1)
4834
+ return declarations[0];
4835
+ const program = this.project.getProgram().compilerObject;
4836
+ const fromLibrary = declarations.filter((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile().compilerNode));
4837
+ const candidates = fromLibrary.length > 0 ? fromLibrary : declarations;
4838
+ return [...candidates].sort((a, b) => {
4839
+ const pathA = a.getSourceFile().getFilePath();
4840
+ const pathB = b.getSourceFile().getFilePath();
4841
+ return pathA < pathB ? -1 : pathA > pathB ? 1 : a.getStart() - b.getStart();
4842
+ })[0];
4843
+ }
4651
4844
  /**
4652
4845
  * The awaited return type of the member invoked on the receiver. Reported as
4653
4846
  * a fact next to the receiver; a caller that classified on it alone would be
@@ -4795,7 +4988,7 @@ export class TypeInferrer {
4795
4988
  for (const node of sourceFile.getDescendants()) {
4796
4989
  if ((Node.isCallExpression(node) ||
4797
4990
  Node.isObjectLiteralExpression(node)) &&
4798
- node.getStartLineNumber() === line) {
4991
+ startLineOf(node) === line) {
4799
4992
  const handler = this.resolveRegisteredHandler(node);
4800
4993
  if (handler) {
4801
4994
  return { registration: node, handler };
@@ -5936,14 +6129,9 @@ export class TypeInferrer {
5936
6129
  if (!Node.isPropertyAssignment(prop)) {
5937
6130
  continue;
5938
6131
  }
5939
- const name = prop.getName().replace(/['"`]/g, '').toLowerCase();
5940
- // `2xx` is a status RANGE key; treat it as the bottom of its range.
5941
- const code = /^\d{3}$/.test(name)
5942
- ? Number(name)
5943
- : /^\dxx$/.test(name)
5944
- ? Number(name[0]) * 100
5945
- : NaN;
5946
- if (!Number.isFinite(code) || code < 200 || code > 299) {
6132
+ // `2xx` is a status RANGE key; it reads as the bottom of its range.
6133
+ const code = statusKeyCode(prop.getName());
6134
+ if (code === undefined || code < 200 || code > 299) {
5947
6135
  continue;
5948
6136
  }
5949
6137
  const initializer = prop.getInitializer();
@@ -6391,7 +6579,7 @@ export class TypeInferrer {
6391
6579
  // Filter to nodes within the search window and pre-compute normalized text
6392
6580
  const candidates = (lineNumber
6393
6581
  ? nodes.filter((node) => {
6394
- const nodeLine = node.getStartLineNumber();
6582
+ const nodeLine = startLineOf(node);
6395
6583
  return nodeLine >= lineNumber - searchRadius && nodeLine <= lineNumber + searchRadius;
6396
6584
  })
6397
6585
  : nodes).map((node) => ({ node, text: this.normalizeWhitespace(node.getText()) }));
@@ -6449,8 +6637,8 @@ export class TypeInferrer {
6449
6637
  }
6450
6638
  // Tie-break by proximity to target line
6451
6639
  if (targetLine !== undefined) {
6452
- const bestDist = Math.abs(best.getStartLineNumber() - targetLine);
6453
- const currentDist = Math.abs(current.getStartLineNumber() - targetLine);
6640
+ const bestDist = Math.abs(startLineOf(best) - targetLine);
6641
+ const currentDist = Math.abs(startLineOf(current) - targetLine);
6454
6642
  return currentDist < bestDist ? current : best;
6455
6643
  }
6456
6644
  return best;
@@ -6666,8 +6854,8 @@ export class TypeInferrer {
6666
6854
  // Result Building
6667
6855
  // ===========================================================================
6668
6856
  getNodeLocation(node) {
6669
- const startLinePos = node.getStartLineNumber();
6670
- const endLinePos = node.getEndLineNumber();
6857
+ const startLinePos = startLineOf(node);
6858
+ const endLinePos = endLineOf(node);
6671
6859
  const location = {
6672
6860
  file_path: node.getSourceFile().getFilePath(),
6673
6861
  start_line: startLinePos,
@@ -2,6 +2,7 @@
2
2
  * Type definitions for the sidecar message protocol
3
3
  * These types define the JSON messages exchanged between Rust and the Node.js sidecar
4
4
  */
5
+ import type { ResponseModes } from './response-modes.js';
5
6
  /**
6
7
  * The kind of type inference to perform
7
8
  */
@@ -206,6 +207,22 @@ export interface HealthRequest extends BaseRequest {
206
207
  export interface ShutdownRequest extends BaseRequest {
207
208
  action: 'shutdown';
208
209
  }
210
+ /**
211
+ * List the root files the init'd project's program was built from, in order
212
+ * (carrick#2027).
213
+ */
214
+ export interface ListProgramFilesRequest extends BaseRequest {
215
+ action: 'list_program_files';
216
+ }
217
+ /**
218
+ * Add files to the programs that type them, in the given order
219
+ * (carrick#2027).
220
+ */
221
+ export interface AddProgramFilesRequest extends BaseRequest {
222
+ action: 'add_program_files';
223
+ /** Absolute paths, or relative to the init'd root. */
224
+ files: string[];
225
+ }
209
226
  /**
210
227
  * Request to resolve type definitions from a v2 capture stub package.
211
228
  * Returns both the original declaration and the compiler-expanded form for
@@ -461,7 +478,7 @@ export interface ListLibrarySurfaceRequest extends BaseRequest {
461
478
  /**
462
479
  * Union type for all possible sidecar requests
463
480
  */
464
- export type SidecarRequest = RetypeCheckRequest | VerifyClientSemanticsRequest | VerifyLibraryClaimsRequest | ListLibrarySurfaceRequest | InitRequest | BundleRequest | CaptureV2Request | CheckV2Request | InferRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
481
+ export type SidecarRequest = RetypeCheckRequest | VerifyClientSemanticsRequest | VerifyLibraryClaimsRequest | ListLibrarySurfaceRequest | InitRequest | BundleRequest | CaptureV2Request | CheckV2Request | InferRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest | ListProgramFilesRequest | AddProgramFilesRequest;
465
482
  /**
466
483
  * Request for a specific symbol to be bundled
467
484
  */
@@ -480,6 +497,12 @@ export interface SymbolRequest {
480
497
  * symbol as-is (the HTTP/socket/consumer default).
481
498
  */
482
499
  array_depth?: number;
500
+ /**
501
+ * carrick#1841: the symbol names what a consumer receives. A symbol that is
502
+ * a response table keyed by status code (`{ 200: Item }`) then bundles as
503
+ * its 2xx body, never as the table.
504
+ */
505
+ consumer_response?: boolean;
483
506
  }
484
507
  /**
485
508
  * Request for type inference at a specific location
@@ -645,6 +668,22 @@ export interface HealthResponse extends BaseResponse {
645
668
  export interface ShutdownResponse extends BaseResponse {
646
669
  status: 'success';
647
670
  }
671
+ /**
672
+ * Response for list_program_files action
673
+ */
674
+ export interface ListProgramFilesResponse extends BaseResponse {
675
+ /** The program's root files, as absolute paths, in order. */
676
+ files?: string[];
677
+ errors?: string[];
678
+ }
679
+ /**
680
+ * Response for add_program_files action
681
+ */
682
+ export interface AddProgramFilesResponse extends BaseResponse {
683
+ /** How many of the given files were added. */
684
+ added?: number;
685
+ errors?: string[];
686
+ }
648
687
  /**
649
688
  * Error response
650
689
  */
@@ -785,7 +824,7 @@ export interface ListLibrarySurfaceResponse extends BaseResponse {
785
824
  /**
786
825
  * Union type for all possible sidecar responses
787
826
  */
788
- export type SidecarResponse = RetypeCheckResponse | VerifyClientSemanticsResponse | VerifyLibraryClaimsResponse | ListLibrarySurfaceResponse | InitResponse | BundleResponse | CaptureV2Response | CheckV2Response | InferResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
827
+ export type SidecarResponse = RetypeCheckResponse | VerifyClientSemanticsResponse | VerifyLibraryClaimsResponse | ListLibrarySurfaceResponse | InitResponse | BundleResponse | CaptureV2Response | CheckV2Response | InferResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ListProgramFilesResponse | AddProgramFilesResponse | ErrorResponse;
789
828
  /**
790
829
  * An entry in the type manifest
791
830
  */
@@ -915,6 +954,15 @@ export interface InferredType {
915
954
  * Absent when the body is not a raw-text read.
916
955
  */
917
956
  raw_text_read?: true;
957
+ /**
958
+ * carrick#2054, a response joined from two or more success bodies: the
959
+ * request fields whose values pick among them (`reads`), and, for one
960
+ * query parameter or body field whose every test the reading follows, the
961
+ * union each stated value receives (`cases`). `type_string` is unchanged.
962
+ * Absent when no request read separates the bodies, or when every value
963
+ * receives the same union.
964
+ */
965
+ response_modes?: ResponseModes;
918
966
  }
919
967
  /**
920
968
  * What the source states a body read to be (carrick#1749). The scanner