carrick 0.3.84 → 0.3.86

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 (47) hide show
  1. package/README.md +15 -0
  2. package/bin/carrick.mjs +89 -0
  3. package/dist/auth/credentials.d.ts +9 -0
  4. package/dist/auth/credentials.js +13 -2
  5. package/dist/auth/credentials.js.map +1 -1
  6. package/dist/auth/read.d.ts +4 -4
  7. package/dist/global-install.d.ts +183 -0
  8. package/dist/global-install.js +393 -0
  9. package/dist/global-install.js.map +1 -0
  10. package/dist/hook/post-edit.js +10 -0
  11. package/dist/hook/post-edit.js.map +1 -1
  12. package/dist/hook/session-start.js +34 -0
  13. package/dist/hook/session-start.js.map +1 -1
  14. package/dist/hook/stop.js +18 -4
  15. package/dist/hook/stop.js.map +1 -1
  16. package/dist/hook/user-prompt.js +16 -4
  17. package/dist/hook/user-prompt.js.map +1 -1
  18. package/dist/init/doctor.d.ts +40 -0
  19. package/dist/init/doctor.js +125 -0
  20. package/dist/init/doctor.js.map +1 -1
  21. package/dist/init/outdated.d.ts +47 -0
  22. package/dist/init/outdated.js +104 -0
  23. package/dist/init/outdated.js.map +1 -1
  24. package/dist/init/projects.d.ts +2 -2
  25. package/dist/init/run.d.ts +9 -0
  26. package/dist/init/run.js +57 -12
  27. package/dist/init/run.js.map +1 -1
  28. package/dist/scan.d.ts +26 -0
  29. package/dist/scan.js +92 -0
  30. package/dist/scan.js.map +1 -1
  31. package/dist/update-check.d.ts +1 -0
  32. package/dist/update-check.js +25 -0
  33. package/dist/update-check.js.map +1 -0
  34. package/dist/update.d.ts +128 -0
  35. package/dist/update.js +398 -0
  36. package/dist/update.js.map +1 -0
  37. package/package.json +7 -7
  38. package/sidecar/dist/src/capture/anchors.js +69 -8
  39. package/sidecar/dist/src/capture/api.d.ts +4 -1
  40. package/sidecar/dist/src/capture/deep-walk.js +4 -1
  41. package/sidecar/dist/src/capture/node-builder.d.ts +28 -1
  42. package/sidecar/dist/src/capture/node-builder.js +89 -3
  43. package/sidecar/dist/src/capture/unresolved.d.ts +7 -0
  44. package/sidecar/dist/src/capture/unresolved.js +1 -1
  45. package/sidecar/dist/src/type-inferrer.d.ts +128 -5
  46. package/sidecar/dist/src/type-inferrer.js +444 -17
  47. package/templates/skills/carrick-census.md +3 -1
@@ -865,7 +865,7 @@ export class TypeInferrer {
865
865
  const serialisers = this.calleesProvenSerialiser(branches);
866
866
  const isSend = (candidate) => this.isResponseSend(candidate, extractionConfig, serialisers);
867
867
  const peeled = this.peelTransparentExpression(located);
868
- const site = isSend(peeled) ? peeled : this.sendReceivingArgument(located, isSend);
868
+ const site = isSend(peeled) ? peeled : this.receivingCallOf(located, isSend);
869
869
  if (!site)
870
870
  return undefined;
871
871
  const contains = (outer, inner) => outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
@@ -938,12 +938,16 @@ export class TypeInferrer {
938
938
  this.typeIsOrContainsResponseMachinery(result));
939
939
  }
940
940
  /**
941
- * The send `node` is an argument of, looking through the wrappers that do
941
+ * The call `node` is an ARGUMENT of, looking through the wrappers that do
942
942
  * not change a payload (parentheses, `as`, `satisfies`, `!`, `await`) and a
943
- * `JSON.stringify` around the body. `undefined` when the parent call is not
944
- * a send or `node` is its callee.
943
+ * `JSON.stringify` around the body. `undefined` when `accept` rejects that
944
+ * call or `node` is its callee.
945
+ *
946
+ * Two readings use it: a value handed to a response send is the payload that
947
+ * send transmits, and a body read handed to a call that states what it
948
+ * returns is a better statement of that body than the read (carrick#1382).
945
949
  */
946
- sendReceivingArgument(node, isSend) {
950
+ receivingCallOf(node, accept) {
947
951
  let current = node;
948
952
  let parent = current.getParent();
949
953
  while (parent &&
@@ -961,7 +965,7 @@ export class TypeInferrer {
961
965
  }
962
966
  if (!parent.getArguments().includes(current))
963
967
  return undefined;
964
- return isSend(parent) ? parent : undefined;
968
+ return accept(parent) ? parent : undefined;
965
969
  }
966
970
  inferCallResult(sourceFile, request, extractionConfig) {
967
971
  const callExpr = this.resolveTargetCallExpression(sourceFile, request);
@@ -970,7 +974,8 @@ export class TypeInferrer {
970
974
  }
971
975
  // Walk up from the already-found call expression instead of re-searching
972
976
  const func = this.findContainingFunctionForNode(callExpr);
973
- const terminalNode = this.resolveCallResultTerminalNode(callExpr, func);
977
+ const use = this.resolveCallResultTerminalNode(callExpr, func);
978
+ const terminalNode = use.terminal;
974
979
  const returnType = terminalNode.getType();
975
980
  let typeString = typeText(returnType, terminalNode);
976
981
  let isExplicit = false;
@@ -984,6 +989,76 @@ export class TypeInferrer {
984
989
  typeString = explicitType;
985
990
  isExplicit = true;
986
991
  }
992
+ // carrick#1376: the call answers a RESULT CARRIER — a generic union whose
993
+ // branches say whether the call worked and carry, on the success side, the
994
+ // value it produced. The carrier is the transport's own bookkeeping; the
995
+ // payload a caller receives is the success type argument, and publishing
996
+ // the carrier instead reports an envelope as a wire contract, which is the
997
+ // same class of answer the machinery guard refuses on the producer side.
998
+ //
999
+ // A wrapper rule that already unwrapped, and a type the source itself
1000
+ // states, both outrank this: they are what the service's own config and
1001
+ // its own author said.
1002
+ const carrierCandidate = unwrapResult.wasUnwrapped || explicitType
1003
+ ? undefined
1004
+ : this.resultCarrierPayload(returnType, terminalNode, use.projections, `${request.file_path}:${request.line_number}`);
1005
+ // The payload rides the row as its own MEMBERS, never as its bare name
1006
+ // (#257): `derive_capture_anchors` turns a usable inference into a literal
1007
+ // capture anchor, and a bare name is out of scope where the surface
1008
+ // declares the alias, so it would decay to a top type and publish nothing
1009
+ // — trading a wrong answer for no answer. Where the payload carries no
1010
+ // member shape to print, the carrier keeps its own answer.
1011
+ const carrierText = carrierCandidate
1012
+ ? this.structuralTextFromType(carrierCandidate, terminalNode)
1013
+ : null;
1014
+ const carrierPayload = carrierText ? carrierCandidate : undefined;
1015
+ if (carrierPayload && carrierText) {
1016
+ typeString = carrierText;
1017
+ }
1018
+ else if (carrierCandidate) {
1019
+ this.log(`Call result at ${request.file_path}:${request.line_number} carries a payload with no ` +
1020
+ 'printable member shape; publishing the carrier as written');
1021
+ }
1022
+ // carrick#1375: the source never read this call's result whole — every
1023
+ // read took a member out of it, and one of those members is the generic
1024
+ // this result was instantiated with. That is a source unwrapping an
1025
+ // envelope by hand, so both answers available here are wrong: the
1026
+ // projection is a fragment of the payload, and the envelope around it is
1027
+ // machinery. A wrapper rule that DID unwrap the envelope, or a type the
1028
+ // source itself states, is a contract and keeps its answer.
1029
+ //
1030
+ // A single explicit call generic (`client.get<Order[]>(url)`) is the
1031
+ // caller's own payload claim and states the contract as plainly as an
1032
+ // `as` does — it is what the anchor below falls back to on a checkout
1033
+ // with no node_modules. Several generics claim nothing unambiguously.
1034
+ //
1035
+ // The decision has to ride the row: a plain `null` is re-read by the
1036
+ // capture's own locator, which is kind-blind and would publish the
1037
+ // projection this just declined to publish (`inference_decided_no_contract`,
1038
+ // engine/type_compat_v2.rs). No anchor and no depth go with it, so the
1039
+ // row also reads as blind rather than as a sighted answer.
1040
+ if (use.projectionOnly &&
1041
+ this.projectionReadsGenericPayload(callExpr, use.projections) &&
1042
+ !unwrapResult.wasUnwrapped &&
1043
+ !explicitType &&
1044
+ !carrierPayload &&
1045
+ callExpr.getTypeArguments().length !== 1) {
1046
+ this.log(`Call result at ${request.file_path}:${request.line_number} is only ever read as ` +
1047
+ `members of ${typeText(callExpr.getType(), callExpr)}; a part of a payload is not ` +
1048
+ 'a payload, so this site states no response contract');
1049
+ const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
1050
+ abstain.any_provenance = [
1051
+ {
1052
+ path: '',
1053
+ kind: 'unknown',
1054
+ reason: 'projected_value_only',
1055
+ detail: "every read of this call's result takes a member out of it and none reads the " +
1056
+ 'value itself, so what this site states is a part of a payload rather than the ' +
1057
+ 'payload a caller receives',
1058
+ },
1059
+ ];
1060
+ return abstain;
1061
+ }
987
1062
  typeString = this.unwrapPromise(typeString, returnType);
988
1063
  // #336: consumer analogue of the #306 producer anchor. Anchor on the
989
1064
  // CALL's own payload (the rule-extracted Type, else the awaited call
@@ -1001,9 +1076,17 @@ export class TypeInferrer {
1001
1076
  // anchor via that path.
1002
1077
  const callPayloadType = this.unwrapPromiseType(callExpr.getType());
1003
1078
  const callUnwrap = this.unwrapTypeWithConfig(callPayloadType, callExpr, extractionConfig);
1004
- const anchorSource = callUnwrap.wasUnwrapped
1005
- ? callUnwrap.payloadType
1006
- : callPayloadType;
1079
+ // carrick#1376: the carrier's own symbol must never anchor either. The
1080
+ // surface pre-claims the alias from the anchor, so anchoring on the
1081
+ // carrier makes the capture emit the transport's bookkeeping as the
1082
+ // operation's declaration — and where the carrier is a local interface the
1083
+ // service does not export, nothing is emitted at all and the row reads
1084
+ // null. The payload the carrier was found to hold is the anchor.
1085
+ const anchorSource = carrierPayload
1086
+ ? carrierPayload
1087
+ : callUnwrap.wasUnwrapped
1088
+ ? callUnwrap.payloadType
1089
+ : callPayloadType;
1007
1090
  let anchor = anchorSource
1008
1091
  ? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
1009
1092
  : undefined;
@@ -1314,13 +1397,17 @@ export class TypeInferrer {
1314
1397
  if (returnStmt) {
1315
1398
  const returnExpr = returnStmt.getExpression();
1316
1399
  if (returnExpr) {
1317
- return returnExpr;
1400
+ return { terminal: returnExpr, projectionOnly: false, projections: [] };
1318
1401
  }
1319
1402
  }
1320
1403
  const binding = this.extractBindingFromCall(callExpr);
1321
1404
  if (binding && func) {
1322
1405
  let currentNames = binding.names;
1323
1406
  let lastNode = binding.node;
1407
+ /** The member reads taken OUT of the tracked value (carrick#1375). */
1408
+ const projections = [];
1409
+ /** The value itself was read: returned, passed, aliased, or body-read. */
1410
+ let sawWholeRead = false;
1324
1411
  const startPos = callExpr.getStart();
1325
1412
  const candidates = this.collectDefUseNodes(func);
1326
1413
  for (const expr of candidates) {
@@ -1344,9 +1431,27 @@ export class TypeInferrer {
1344
1431
  if (Node.isIdentifier(expr) && this.isIdentifierUsage(expr, currentNames)) {
1345
1432
  const bodyRead = this.bodyReadOnReceiver(expr);
1346
1433
  if (bodyRead) {
1347
- lastNode = bodyRead;
1434
+ sawWholeRead = true;
1435
+ // carrick#1382: the read is a FLOOR, not a ceiling. Where the
1436
+ // source hands the body it read to a call that states what it
1437
+ // returns, that call says strictly more about the payload than
1438
+ // the untyped read does, and it must not lose to the read just
1439
+ // because the walk reaches the read second (candidates come in
1440
+ // pre-order, so the declaration of the parsed value is visited
1441
+ // before the identifier inside its own initializer).
1442
+ lastNode = this.statedPayloadAroundBodyRead(bodyRead) ?? bodyRead;
1348
1443
  continue;
1349
1444
  }
1445
+ // carrick#1375: how the source reads the value decides whether this
1446
+ // site states a payload at all. A member read takes a PART of it; a
1447
+ // return, an argument, an alias or a parse reads the value itself.
1448
+ const projection = this.projectionOnReceiver(expr);
1449
+ if (projection) {
1450
+ projections.push(projection);
1451
+ }
1452
+ else {
1453
+ sawWholeRead = true;
1454
+ }
1350
1455
  }
1351
1456
  if (Node.isVariableDeclaration(expr)) {
1352
1457
  const initializer = expr.getInitializer();
@@ -1370,13 +1475,326 @@ export class TypeInferrer {
1370
1475
  }
1371
1476
  }
1372
1477
  }
1373
- if (this.expressionUsesNames(expr, currentNames)) {
1478
+ // The last expression the value flows into is the call's payload — a
1479
+ // cast, a parse, an alias. An expression that only reads MEMBERS out
1480
+ // of it is a projection and states a part of the payload, never the
1481
+ // payload: the hook that derives `query.data?.flags` off a query
1482
+ // result published a boolean map as the expected response of a call
1483
+ // whose envelope the client declares in full (carrick#1375).
1484
+ if (this.expressionUsesNames(expr, currentNames) &&
1485
+ !this.usesNamesOnlyByProjection(expr, currentNames)) {
1374
1486
  lastNode = expr;
1375
1487
  }
1376
1488
  }
1377
- return lastNode;
1489
+ return {
1490
+ terminal: lastNode,
1491
+ projectionOnly: projections.length > 0 && !sawWholeRead,
1492
+ projections,
1493
+ };
1378
1494
  }
1379
- return callExpr;
1495
+ return { terminal: callExpr, projectionOnly: false, projections: [] };
1496
+ }
1497
+ /**
1498
+ * True when a member read on the call's result resolves to one of that
1499
+ * result's own TYPE ARGUMENTS — the source is unwrapping a generic envelope
1500
+ * by hand (`state.data` off a `ResourceState<Envelope>`), and the payload it
1501
+ * carries is the instantiation, not the envelope (carrick#1375).
1502
+ *
1503
+ * The generic is what tells the two apart. A call that answers its payload
1504
+ * directly is read member by member too, and its declared result IS the
1505
+ * contract; abstaining there would throw away the type the request boundary
1506
+ * states, which a replay over a real repo's consumer rows showed on a
1507
+ * `{ ok: true } | { ok: false; reason: string }` result read as `sent.ok`.
1508
+ */
1509
+ projectionReadsGenericPayload(callExpr, projections) {
1510
+ if (projections.length === 0)
1511
+ return false;
1512
+ const result = this.unwrapPromiseType(callExpr.getType());
1513
+ const args = [...result.getTypeArguments(), ...result.getAliasTypeArguments()];
1514
+ if (args.length === 0)
1515
+ return false;
1516
+ const argTexts = new Set(args.map((arg) => arg.getText()));
1517
+ return projections.some((projection) => {
1518
+ const read = this.unwrapPromiseType(projection.getType());
1519
+ const parts = [read, read.getNonNullableType()];
1520
+ for (const part of [...parts]) {
1521
+ if (part.isUnion())
1522
+ parts.push(...part.getUnionTypes());
1523
+ }
1524
+ return parts.some((part) => argTexts.has(part.getText()));
1525
+ });
1526
+ }
1527
+ /**
1528
+ * The payload a RESULT CARRIER carries, or `undefined` when `type` is not
1529
+ * one or its success side cannot be told from its failure side
1530
+ * (carrick#1376).
1531
+ *
1532
+ * A carrier is recognised by its shape, never by a name: a union of object
1533
+ * branches, instantiated with two or more type arguments, at least one of
1534
+ * which a branch holds as a member. `Result<T, E>`, `Either<L, R>` and a
1535
+ * hand-rolled `{ ok: true; value: T } | { ok: false; error: E }` are all the
1536
+ * same shape, and a promise-like around one is peeled first through the
1537
+ * language's own await protocol. A single generic object — a resource state,
1538
+ * a query result — is NOT a union and is left to carrick#1375, which
1539
+ * abstains on it so a sibling site can answer.
1540
+ *
1541
+ * Which argument is the payload is decided twice over, and never guessed:
1542
+ *
1543
+ * 1. the platform's error shape. Exactly one argument that is not
1544
+ * error-shaped, beside at least one that is, is the success side.
1545
+ * 2. what the source reads. Where every argument looks alike — `Pair<A,
1546
+ * string>` — a member read of the carrier that resolves to exactly one
1547
+ * of the arguments names the side this call site takes.
1548
+ *
1549
+ * Where neither decides, the carrier keeps its own answer and the limit is
1550
+ * logged: a coin flip published as a contract is worse than an envelope a
1551
+ * reader can see is an envelope.
1552
+ */
1553
+ resultCarrierPayload(type, at, projections, where) {
1554
+ const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
1555
+ if (!carrier.isUnion()) {
1556
+ return undefined;
1557
+ }
1558
+ const branches = carrier.getUnionTypes();
1559
+ if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
1560
+ return undefined;
1561
+ }
1562
+ const args = [
1563
+ ...carrier.getAliasTypeArguments(),
1564
+ ...carrier.getTypeArguments(),
1565
+ ];
1566
+ if (args.length < 2) {
1567
+ return undefined;
1568
+ }
1569
+ // A type argument only names a payload when a branch actually holds it:
1570
+ // a generic that parameterises a status code or a key carries nothing.
1571
+ const carried = args.filter((arg) => branches.some((branch) => branch
1572
+ .getProperties()
1573
+ .some((property) => {
1574
+ try {
1575
+ return property.getTypeAtLocation(at).getText() === arg.getText();
1576
+ }
1577
+ catch {
1578
+ return false;
1579
+ }
1580
+ })));
1581
+ if (carried.length === 0) {
1582
+ return undefined;
1583
+ }
1584
+ const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
1585
+ const read = new Set();
1586
+ for (const projection of projections) {
1587
+ const projected = this.unwrapPromiseType(projection.getType()).getNonNullableType();
1588
+ if (argTexts.has(projected.getText())) {
1589
+ read.add(projected.getText());
1590
+ }
1591
+ }
1592
+ const byRead = read.size === 1 ? argTexts.get([...read][0]) : undefined;
1593
+ const succeeded = carried.filter((arg) => !this.isErrorShaped(arg, at));
1594
+ const byShape = succeeded.length === 1 && succeeded.length < carried.length ? succeeded[0] : undefined;
1595
+ // The two tests must agree where both answer. An error shape is a shape,
1596
+ // and a payload is free to have one — a contact form declares a `name` and
1597
+ // a `message` too, and a service whose failure side is `{ code: number }`
1598
+ // would then read as the success side. Where the source reads a DIFFERENT
1599
+ // argument out than the shape test picked, the two disagree about which
1600
+ // side is which and nothing here knows better than the source does.
1601
+ if (byShape && byRead && byShape !== byRead) {
1602
+ this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose error ` +
1603
+ `shape names '${typeText(byShape, at)}' as the payload while the source reads ` +
1604
+ `'${typeText(byRead, at)}' out of it. Publishing the carrier as written rather ` +
1605
+ 'than picking one');
1606
+ return undefined;
1607
+ }
1608
+ if (byShape ?? byRead) {
1609
+ return byShape ?? byRead;
1610
+ }
1611
+ this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose success ` +
1612
+ 'side cannot be told from its failure side: no single argument is the only ' +
1613
+ 'non-error one and the source reads none of them out. Publishing the carrier as ' +
1614
+ 'written rather than guessing which argument is the payload');
1615
+ return undefined;
1616
+ }
1617
+ /**
1618
+ * `Future<T>` -> `T` for a promise-like of the source's own making, read off
1619
+ * the await protocol rather than a name: a `then` whose first parameter is a
1620
+ * callback, whose own first parameter is the value awaiting it yields.
1621
+ * `Promise` and `PromiseLike` are peeled by `unwrapPromiseType` before this.
1622
+ */
1623
+ unwrapThenableType(type) {
1624
+ let current = type;
1625
+ for (let depth = 0; depth < 8; depth++) {
1626
+ const then = current.getProperty('then');
1627
+ const declaration = then?.getDeclarations()[0];
1628
+ if (!then || !declaration)
1629
+ return current;
1630
+ let onValue;
1631
+ try {
1632
+ onValue = then
1633
+ .getTypeAtLocation(declaration)
1634
+ .getCallSignatures()[0]
1635
+ ?.getParameters()[0]
1636
+ ?.getTypeAtLocation(declaration);
1637
+ }
1638
+ catch {
1639
+ return current;
1640
+ }
1641
+ const value = onValue
1642
+ ?.getCallSignatures()[0]
1643
+ ?.getParameters()[0]
1644
+ ?.getTypeAtLocation(declaration);
1645
+ if (!value || value === current)
1646
+ return current;
1647
+ current = value;
1648
+ }
1649
+ return current;
1650
+ }
1651
+ /**
1652
+ * The platform's error shape, in full: `name` and `message` strings AND a
1653
+ * `stack`, which is what the `Error` interface declares and every subclass
1654
+ * of it inherits.
1655
+ *
1656
+ * `stack` is what makes the test a test. A name and a message alone are a
1657
+ * shape a PAYLOAD can have — a contact form declares both — and reading such
1658
+ * a payload as the failure side would publish the other argument, which is
1659
+ * the concrete-but-wrong answer this whole rule exists to avoid. A union is
1660
+ * error-shaped when every member of it is.
1661
+ */
1662
+ isErrorShaped(type, at) {
1663
+ if (type.isUnion()) {
1664
+ return type.getUnionTypes().every((part) => this.isErrorShaped(part, at));
1665
+ }
1666
+ const typeOf = (property) => {
1667
+ if (!property)
1668
+ return undefined;
1669
+ try {
1670
+ return property.getTypeAtLocation(at);
1671
+ }
1672
+ catch {
1673
+ return undefined;
1674
+ }
1675
+ };
1676
+ const stringy = (property) => typeOf(property)?.getNonNullableType().getText() === 'string';
1677
+ return (stringy(type.getProperty('name')) &&
1678
+ stringy(type.getProperty('message')) &&
1679
+ stringy(type.getProperty('stack')));
1680
+ }
1681
+ /**
1682
+ * The member read that takes `identifier` as its RECEIVER — `query` in
1683
+ * `query.data`, `envelope` in `envelope.list[0]` — or `undefined` when the
1684
+ * identifier names the value itself.
1685
+ *
1686
+ * A member CALL is not a projection: `res.text()` yields a body rather than
1687
+ * a part of one, and what it returns stays the walk's business. The
1688
+ * zero-argument json body read has its own branch and is taken before this
1689
+ * is asked.
1690
+ */
1691
+ projectionOnReceiver(identifier) {
1692
+ const access = identifier.getParent();
1693
+ if (!access ||
1694
+ (!Node.isPropertyAccessExpression(access) &&
1695
+ !Node.isElementAccessExpression(access)) ||
1696
+ access.getExpression() !== identifier) {
1697
+ return undefined;
1698
+ }
1699
+ const parent = access.getParent();
1700
+ if (parent &&
1701
+ Node.isCallExpression(parent) &&
1702
+ parent.getExpression() === access) {
1703
+ return undefined;
1704
+ }
1705
+ return access;
1706
+ }
1707
+ /**
1708
+ * The call that CONSUMES this json body read and states what the body is —
1709
+ * `parseEnvelope(await response.json())` — or `undefined` when nothing
1710
+ * downstream of the read says more about it than the read itself does
1711
+ * (carrick#1382).
1712
+ *
1713
+ * Three conditions, all shapes of the language rather than names:
1714
+ *
1715
+ * - the read reaches the call as an ARGUMENT, through the wrappers that do
1716
+ * not change a value (`await`, parentheses, `as`, `satisfies`, `!`). A
1717
+ * cast with no call around it therefore keeps the read as the terminal,
1718
+ * so `(await res.json()) as Entry` is still read off the read itself;
1719
+ * - the call's own result is BOUND — declared into a variable, returned, or
1720
+ * assigned — so a call the source made for its side effect
1721
+ * (`store(await res.json())`) states nothing about the payload;
1722
+ * - that result is an OBJECT shape. A validator answering `boolean` or a
1723
+ * serialiser answering `string` describes what the caller did with the
1724
+ * body, not what the body is, and publishing it would be a
1725
+ * concrete-but-wrong contract where the honest `any` of the read is
1726
+ * merely unresolved.
1727
+ */
1728
+ statedPayloadAroundBodyRead(bodyRead) {
1729
+ return this.receivingCallOf(bodyRead, (candidate) => Node.isCallExpression(candidate) &&
1730
+ this.callResultIsBound(candidate) &&
1731
+ this.isObjectShape(this.unwrapPromiseType(candidate.getType())));
1732
+ }
1733
+ /**
1734
+ * The source keeps this call's result: it initializes a declaration, is
1735
+ * returned, is assigned, or is an arrow's expression body. A result that is
1736
+ * kept is one the source has a use for; a discarded one is a side effect.
1737
+ */
1738
+ callResultIsBound(call) {
1739
+ let current = call;
1740
+ let parent = current.getParent();
1741
+ while (parent &&
1742
+ (Node.isParenthesizedExpression(parent) ||
1743
+ Node.isAwaitExpression(parent) ||
1744
+ Node.isAsExpression(parent) ||
1745
+ Node.isSatisfiesExpression(parent) ||
1746
+ Node.isNonNullExpression(parent)) &&
1747
+ parent.getExpression() === current) {
1748
+ current = parent;
1749
+ parent = current.getParent();
1750
+ }
1751
+ if (!parent)
1752
+ return false;
1753
+ if (Node.isVariableDeclaration(parent)) {
1754
+ return parent.getInitializer() === current;
1755
+ }
1756
+ if (Node.isReturnStatement(parent)) {
1757
+ return parent.getExpression() === current;
1758
+ }
1759
+ if (Node.isArrowFunction(parent)) {
1760
+ return parent.getBody() === current;
1761
+ }
1762
+ if (Node.isBinaryExpression(parent)) {
1763
+ return (parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken &&
1764
+ parent.getRight() === current);
1765
+ }
1766
+ return false;
1767
+ }
1768
+ /**
1769
+ * A shape a JSON body can be: an object, an array, or a union of them.
1770
+ * Top types, primitives, `void` and callables are not.
1771
+ */
1772
+ isObjectShape(type) {
1773
+ if (type.isAny() || type.isUnknown())
1774
+ return false;
1775
+ if (type.isUnion()) {
1776
+ const parts = type.getUnionTypes().filter((part) => !part.isUndefined() && !part.isNull());
1777
+ return parts.length > 0 && parts.every((part) => this.isObjectShape(part));
1778
+ }
1779
+ if (type.isIntersection()) {
1780
+ return type.getIntersectionTypes().every((part) => this.isObjectShape(part));
1781
+ }
1782
+ return type.isObject() && !this.isCallableType(type);
1783
+ }
1784
+ /**
1785
+ * Every use of a tracked name inside `expr` reads a member out of the
1786
+ * tracked value, so the expression's type describes a PART of the payload.
1787
+ * False when the expression uses no tracked name at all, so a caller can
1788
+ * read it as "this is a projection" rather than "this is not a use".
1789
+ */
1790
+ usesNamesOnlyByProjection(expr, names) {
1791
+ const uses = expr
1792
+ .getDescendantsOfKind(SyntaxKind.Identifier)
1793
+ .filter((id) => this.isIdentifierUsage(id, names));
1794
+ if (uses.length === 0) {
1795
+ return false;
1796
+ }
1797
+ return uses.every((id) => this.projectionOnReceiver(id) !== undefined);
1380
1798
  }
1381
1799
  extractBindingFromCall(callExpr) {
1382
1800
  const varDecl = callExpr.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
@@ -2611,7 +3029,16 @@ export class TypeInferrer {
2611
3029
  * True when an object literal argument is response INIT rather than a body:
2612
3030
  * every property it declares is one the standard `ResponseInit` declares
2613
3031
  * (`status`, `statusText`, `headers`), and it states at least one of them as
2614
- * init really does — a numeric status in the HTTP range, or headers.
3032
+ * init really does — a status in the HTTP range, or headers.
3033
+ *
3034
+ * The status does NOT have to be a literal code. A route that carries its
3035
+ * outcome in a value writes `new Response(body, { status: result.status })`,
3036
+ * where the source fixes no code and `statedStatusCodes` answers
3037
+ * `'variable'` — status-shaped, just not pinned. Requiring a literal there
3038
+ * left the whole init object reading as a body, so an endpoint whose payload
3039
+ * this layer does not publish (a string body) published `{ status: number }`
3040
+ * as its response contract instead: a wrong contract, served, where an
3041
+ * abstention was the honest answer.
2615
3042
  *
2616
3043
  * A payload that merely has a `status` member of its own (`{ status: "ok",
2617
3044
  * service: "ledger" }`) declares members init does not, or states `status` as
@@ -2640,7 +3067,7 @@ export class TypeInferrer {
2640
3067
  statesInit = true;
2641
3068
  continue;
2642
3069
  }
2643
- if (name === 'status' && Array.isArray(this.statedStatusCodes(value))) {
3070
+ if (name === 'status' && this.statedStatusCodes(value) !== undefined) {
2644
3071
  statesInit = true;
2645
3072
  }
2646
3073
  }
@@ -54,7 +54,9 @@ Report these numbers before the list, per query where the field is per query:
54
54
  removed, and `best` where it names the closest of them. A count above zero is
55
55
  the case for one more search at a lower `similarity_threshold`;
56
56
  - `total_without_intent`, which is index-wide: functions carrying no intent
57
- text, which no search looked at;
57
+ text, which the search ranked on their name, signature and body tokens
58
+ alone, so a query that names one finds it and a query that describes what
59
+ it does without naming it does not;
58
60
  - `total_intent_carried_forward` where the response states it, which are intents
59
61
  describing the code as of an earlier scan.
60
62