carrick 0.3.109 → 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.
@@ -24,8 +24,11 @@ import { validateInferRequestItem } from './validators.js';
24
24
  import { notePrintedType, PrintedTypes } from './printed-names.js';
25
25
  import { externalImportsOf, isExternalOrigin } from './origin.js';
26
26
  import { reachedOnlyOnFailure } from './failure-path.js';
27
+ import { readResponseModes } from './response-modes.js';
27
28
  import { functionAtLine } from './function-line-index.js';
29
+ import { endLineOf, startLineOf } from './line-index.js';
28
30
  import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
31
+ import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
29
32
  import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
30
33
  import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
31
34
  /**
@@ -169,11 +172,19 @@ const RESPONSE_HELPER_MAX_DEPTH = 4;
169
172
  * the branch an error path, whose shape is not the endpoint's contract.
170
173
  */
171
174
  const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
172
- /** `value` when it is an integer in the HTTP status range, else `undefined`. */
173
- function httpStatus(value) {
174
- return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599
175
- ? value
176
- : 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
+ });
177
188
  }
178
189
  /**
179
190
  * The status codes a TYPE fixes: a numeric literal, or a union made only of
@@ -193,16 +204,6 @@ function statusCodesOfType(type) {
193
204
  }
194
205
  return codes.length > 0 ? codes : undefined;
195
206
  }
196
- /** One verdict for a set of status codes, or `mixed` when they disagree. */
197
- function classifyStatusCodes(codes) {
198
- if (codes.every((code) => code >= 400))
199
- return 'error';
200
- if (codes.every((code) => code >= 300 && code < 400))
201
- return 'redirect';
202
- if (codes.every((code) => code < 300))
203
- return 'success';
204
- return 'mixed';
205
- }
206
207
  /**
207
208
  * The parts of a request that ARE the body, as validator middleware names them
208
209
  * (`validate('json', Schema)`). HTTP vocabulary for how a body is carried — the
@@ -973,7 +974,7 @@ export class TypeInferrer {
973
974
  Node.isArrowFunction(ancestor) ||
974
975
  Node.isFunctionExpression(ancestor) ||
975
976
  Node.isMethodDeclaration(ancestor)) &&
976
- ancestor.getStartLineNumber() === request.line_number);
977
+ startLineOf(ancestor) === request.line_number);
977
978
  const declaredHandler = insideFunctionOnLine
978
979
  ? this.handlerDeclaredAtLine(sourceFile, request.line_number)
979
980
  : undefined;
@@ -1130,7 +1131,7 @@ export class TypeInferrer {
1130
1131
  */
1131
1132
  exportedFunctionOpeningOn(sourceFile, line) {
1132
1133
  const declared = this.findFunctionByLine(sourceFile, line);
1133
- if (!declared || declared.getStartLineNumber() !== line)
1134
+ if (!declared || startLineOf(declared) !== line)
1134
1135
  return undefined;
1135
1136
  if (Node.isFunctionDeclaration(declared)) {
1136
1137
  return Node.isSourceFile(declared.getParent()) && declared.isExported()
@@ -1299,7 +1300,10 @@ export class TypeInferrer {
1299
1300
  const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
1300
1301
  ? this.primaryTypeSymbolSource(recoveredAnchor.element)
1301
1302
  : undefined;
1302
- 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;
1303
1307
  }
1304
1308
  /**
1305
1309
  * carrick#1161: type a route response from every send in the handler the
@@ -1472,7 +1476,9 @@ export class TypeInferrer {
1472
1476
  // carrier read gives its own. A result that is no carrier keeps the
1473
1477
  // answer it had.
1474
1478
  const where = `${request.file_path}:${request.line_number}`;
1475
- 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;
1476
1482
  // carrick#1877: the terminal holds a thenable the names do not peel, a
1477
1483
  // subclass of `Promise` or a class with a `then` of its own. What a
1478
1484
  // caller receives is what `await` yields for it, as it is for the
@@ -1521,12 +1527,22 @@ export class TypeInferrer {
1521
1527
  const readThroughCarrier = unwrapResult.wasUnwrapped &&
1522
1528
  resultIsCarrier &&
1523
1529
  !!afterRules &&
1524
- this.resultCarrierArguments(afterRules, terminalNode) === undefined;
1525
- const carrierCandidate = explicitType || !afterRules
1530
+ this.resultCarrierArguments(afterRules, terminalNode, callArgs) === undefined;
1531
+ const carrierRead = explicitType || !afterRules
1526
1532
  ? undefined
1527
1533
  : readThroughCarrier
1528
- ? afterRules
1529
- : 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;
1530
1546
  // carrick#1841: what the carrier holds goes through the service's wrapper
1531
1547
  // rules before it is published, as the call's own result did. The carrier
1532
1548
  // is found by its shape, so what it holds can still be a library's
@@ -1658,7 +1674,7 @@ export class TypeInferrer {
1658
1674
  const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
1659
1675
  const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
1660
1676
  !!callAfterRules &&
1661
- this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
1677
+ this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
1662
1678
  const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
1663
1679
  let anchor = anchorSource
1664
1680
  ? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
@@ -2466,13 +2482,57 @@ export class TypeInferrer {
2466
2482
  * Where neither decides, the carrier keeps its own answer and the limit is
2467
2483
  * logged: a coin flip published as a contract is worse than an envelope a
2468
2484
  * reader can see is an envelope.
2469
- */
2470
- resultCarrierPayload(type, at, projections, where) {
2471
- 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);
2472
2498
  if (!shape) {
2473
2499
  return undefined;
2474
2500
  }
2475
- 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
+ };
2476
2536
  const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
2477
2537
  const read = new Set();
2478
2538
  for (const projection of projections) {
@@ -2491,27 +2551,69 @@ export class TypeInferrer {
2491
2551
  // argument out than the shape test picked, the two disagree about which
2492
2552
  // side is which and nothing here knows better than the source does.
2493
2553
  if (byShape && byRead && byShape !== byRead) {
2494
- this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose error ` +
2495
- `shape names '${typeText(byShape, at)}' as the payload while the source reads ` +
2496
- `'${typeText(byRead, at)}' out of it. Publishing the carrier as written rather ` +
2497
- '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)
2498
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
+ }
2499
2605
  }
2500
- if (byShape ?? byRead) {
2501
- return byShape ?? byRead;
2606
+ catch {
2607
+ // A signature the checker cannot resolve states no arguments.
2502
2608
  }
2503
- this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose success ` +
2504
- 'side cannot be told from its failure side: no single argument is the only ' +
2505
- 'non-error one and the source reads none of them out. Publishing the carrier as ' +
2506
- 'written rather than guessing which argument is the payload');
2507
- return undefined;
2609
+ return [];
2508
2610
  }
2509
2611
  /**
2510
2612
  * The shape test of `resultCarrierPayload`: the carrier `type` is, once a
2511
2613
  * promise-like around it is peeled, and the type arguments a branch of it
2512
2614
  * holds as a member. `undefined` when `type` is not a carrier.
2513
2615
  */
2514
- resultCarrierArguments(type, at) {
2616
+ resultCarrierArguments(type, at, callArgs = []) {
2515
2617
  const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
2516
2618
  if (!carrier.isUnion()) {
2517
2619
  return undefined;
@@ -2520,26 +2622,80 @@ export class TypeInferrer {
2520
2622
  if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
2521
2623
  return undefined;
2522
2624
  }
2523
- const args = [
2524
- ...carrier.getAliasTypeArguments(),
2525
- ...carrier.getTypeArguments(),
2526
- ];
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;
2527
2630
  if (args.length < 2) {
2528
2631
  return undefined;
2529
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();
2530
2649
  // A type argument only names a payload when a branch actually holds it:
2531
2650
  // a generic that parameterises a status code or a key carries nothing.
2532
- const carried = args.filter((arg) => branches.some((branch) => branch
2533
- .getProperties()
2534
- .some((property) => {
2651
+ const carried = args.filter((arg) => branches.some((branch) => branch.getProperties().some((property) => {
2652
+ if (transport.has(property.getName()))
2653
+ return false;
2535
2654
  try {
2536
- 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;
2537
2667
  }
2538
2668
  catch {
2539
2669
  return false;
2540
2670
  }
2541
2671
  })));
2542
- 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;
2543
2699
  }
2544
2700
  /**
2545
2701
  * The decided abstain of a call whose result carries transport the
@@ -2549,16 +2705,16 @@ export class TypeInferrer {
2549
2705
  * re-reading the raw call (`inference_decided_no_contract`,
2550
2706
  * engine/type_compat_v2.rs).
2551
2707
  */
2552
- 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') {
2553
2711
  const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
2554
2712
  abstain.any_provenance = [
2555
2713
  {
2556
2714
  path: '',
2557
2715
  kind: 'unknown',
2558
2716
  reason: 'machinery_envelope',
2559
- detail: "what this call's result carries is transport that the service's wrapper rules " +
2560
- 'verify and read no payload out of (a library response object around the body), ' +
2561
- 'so this site states no response contract',
2717
+ detail,
2562
2718
  },
2563
2719
  ];
2564
2720
  return abstain;
@@ -3918,17 +4074,13 @@ export class TypeInferrer {
3918
4074
  const isEmptyObject = (text) => text.replace(/\s+/g, '') === '{}';
3919
4075
  const withMembers = candidates.filter((c) => !isEmptyObject(c.typeString));
3920
4076
  const kept = withMembers.length > 0 ? withMembers : candidates;
3921
- // Dedupe on the whitespace-collapsed text, preserving source order.
3922
- const seen = new Set();
3923
- const distinct = kept.filter((c) => {
3924
- const key = c.typeString.replace(/\s+/g, ' ').trim();
3925
- if (seen.has(key))
3926
- return false;
3927
- seen.add(key);
3928
- return true;
3929
- });
4077
+ const distinct = distinctByText(kept, (c) => c.typeString);
3930
4078
  const typeString = distinct.map((c) => c.typeString).join(' | ');
3931
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,
3932
4084
  typeString,
3933
4085
  // Every payload node that fed the union, deduped ones included: a
3934
4086
  // caller asks whether the expression it located is one of them.
@@ -3942,6 +4094,20 @@ export class TypeInferrer {
3942
4094
  statedTypeNode: distinct.length === 1 ? distinct[0].statedTypeNode : undefined,
3943
4095
  };
3944
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
+ }
3945
4111
  /**
3946
4112
  * The expressions a function returns, excluding returns belonging to nested
3947
4113
  * functions (a `.map(x => …)` callback returns a row, not the response).
@@ -4822,7 +4988,7 @@ export class TypeInferrer {
4822
4988
  for (const node of sourceFile.getDescendants()) {
4823
4989
  if ((Node.isCallExpression(node) ||
4824
4990
  Node.isObjectLiteralExpression(node)) &&
4825
- node.getStartLineNumber() === line) {
4991
+ startLineOf(node) === line) {
4826
4992
  const handler = this.resolveRegisteredHandler(node);
4827
4993
  if (handler) {
4828
4994
  return { registration: node, handler };
@@ -5963,14 +6129,9 @@ export class TypeInferrer {
5963
6129
  if (!Node.isPropertyAssignment(prop)) {
5964
6130
  continue;
5965
6131
  }
5966
- const name = prop.getName().replace(/['"`]/g, '').toLowerCase();
5967
- // `2xx` is a status RANGE key; treat it as the bottom of its range.
5968
- const code = /^\d{3}$/.test(name)
5969
- ? Number(name)
5970
- : /^\dxx$/.test(name)
5971
- ? Number(name[0]) * 100
5972
- : NaN;
5973
- 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) {
5974
6135
  continue;
5975
6136
  }
5976
6137
  const initializer = prop.getInitializer();
@@ -6418,7 +6579,7 @@ export class TypeInferrer {
6418
6579
  // Filter to nodes within the search window and pre-compute normalized text
6419
6580
  const candidates = (lineNumber
6420
6581
  ? nodes.filter((node) => {
6421
- const nodeLine = node.getStartLineNumber();
6582
+ const nodeLine = startLineOf(node);
6422
6583
  return nodeLine >= lineNumber - searchRadius && nodeLine <= lineNumber + searchRadius;
6423
6584
  })
6424
6585
  : nodes).map((node) => ({ node, text: this.normalizeWhitespace(node.getText()) }));
@@ -6476,8 +6637,8 @@ export class TypeInferrer {
6476
6637
  }
6477
6638
  // Tie-break by proximity to target line
6478
6639
  if (targetLine !== undefined) {
6479
- const bestDist = Math.abs(best.getStartLineNumber() - targetLine);
6480
- const currentDist = Math.abs(current.getStartLineNumber() - targetLine);
6640
+ const bestDist = Math.abs(startLineOf(best) - targetLine);
6641
+ const currentDist = Math.abs(startLineOf(current) - targetLine);
6481
6642
  return currentDist < bestDist ? current : best;
6482
6643
  }
6483
6644
  return best;
@@ -6693,8 +6854,8 @@ export class TypeInferrer {
6693
6854
  // Result Building
6694
6855
  // ===========================================================================
6695
6856
  getNodeLocation(node) {
6696
- const startLinePos = node.getStartLineNumber();
6697
- const endLinePos = node.getEndLineNumber();
6857
+ const startLinePos = startLineOf(node);
6858
+ const endLinePos = endLineOf(node);
6698
6859
  const location = {
6699
6860
  file_path: node.getSourceFile().getFilePath(),
6700
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
  */
@@ -496,6 +497,12 @@ export interface SymbolRequest {
496
497
  * symbol as-is (the HTTP/socket/consumer default).
497
498
  */
498
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;
499
506
  }
500
507
  /**
501
508
  * Request for type inference at a specific location
@@ -947,6 +954,15 @@ export interface InferredType {
947
954
  * Absent when the body is not a raw-text read.
948
955
  */
949
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;
950
966
  }
951
967
  /**
952
968
  * What the source states a body read to be (carrick#1749). The scanner