@suss/extractor 0.35.1 → 0.37.0

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.
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  // src/index.ts
2
- import { createHash as createHash7 } from "crypto";
2
+ import { createHash as createHash8 } from "crypto";
3
3
  import {
4
- effectKey,
4
+ displayLabel as displayLabel2,
5
+ effectKey as effectKey2,
5
6
  exchangesHttpResponses as exchangesHttpResponses2,
6
7
  foldRepeatedEffects,
7
- normalizeCalleeText,
8
+ isUnsettledJob,
9
+ normalizeCalleeText as normalizeCalleeText2,
10
+ unfollowedCallGap,
8
11
  withGraphqlMetadata,
9
12
  withHttpMetadata,
10
13
  withMountMetadata,
@@ -165,6 +168,45 @@ function mergeInputReads(derived, syntactic) {
165
168
  return [...found.values()];
166
169
  }
167
170
 
171
+ // src/callSiteGroup.ts
172
+ import { createHash } from "crypto";
173
+ import { effectKey, normalizeCalleeText } from "@suss/behavioral-ir";
174
+ function callSiteKey(effect) {
175
+ const callee = calleeOf(effect);
176
+ const written = callee === void 0 ? effect : { ...effect, callee: normalizeCalleeText(callee) };
177
+ const hash = createHash("sha1").update(effectKey(written)).digest("hex").slice(0, 7);
178
+ return callee === void 0 ? hash : `${normalizeCalleeText(callee)}:${hash}`;
179
+ }
180
+ function calleeOf(effect) {
181
+ return effect.type === "interaction" || effect.type === "invocation" ? effect.callee : void 0;
182
+ }
183
+ function numberRepeatedCallSites(sites) {
184
+ const taken = /* @__PURE__ */ new Map();
185
+ return sites.map((entries) => {
186
+ const ids = /* @__PURE__ */ new Map();
187
+ for (const key of new Set(
188
+ entries.flatMap((entry) => groupIdOf(entry.effect))
189
+ )) {
190
+ const count = (taken.get(key) ?? 0) + 1;
191
+ taken.set(key, count);
192
+ ids.set(key, count === 1 ? key : `${key}#${count}`);
193
+ }
194
+ return entries.map((entry) => ({
195
+ ...entry,
196
+ effect: withGroupId(entry.effect, ids)
197
+ }));
198
+ });
199
+ }
200
+ function groupIdOf(effect) {
201
+ return effect.type === "interaction" && effect.groupId !== void 0 ? [effect.groupId] : [];
202
+ }
203
+ function withGroupId(effect, ids) {
204
+ if (effect.type !== "interaction" || effect.groupId === void 0) {
205
+ return effect;
206
+ }
207
+ return { ...effect, groupId: ids.get(effect.groupId) ?? effect.groupId };
208
+ }
209
+
168
210
  // src/composeWrappers.ts
169
211
  import {
170
212
  exchangesHttpResponses,
@@ -472,6 +514,22 @@ function modulesStamp(modules) {
472
514
  // src/effectGuards.ts
473
515
  var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
474
516
  var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
517
+ function firesOn(site, branch) {
518
+ const loop = branch.loopAround;
519
+ if (loop !== void 0 && site.line >= loop.start && site.line <= loop.end) {
520
+ return true;
521
+ }
522
+ if (!guardsHoldOn(site.preconditions, branch.conditions)) {
523
+ return false;
524
+ }
525
+ return runsBefore(
526
+ site.cleansUpFrom ?? site.line,
527
+ branch.terminal.location.end
528
+ );
529
+ }
530
+ function effectsFiringOn(placed, branch) {
531
+ return placed.filter((one) => firesOn(one.site, branch)).map((one) => one.effect);
532
+ }
475
533
  function guardsHoldOn(preconditions, conditions) {
476
534
  if (preconditions === void 0 || preconditions.length === 0) {
477
535
  return true;
@@ -539,16 +597,21 @@ function throughHelper(branch, at, helperBranches) {
539
597
  isDefault: branch.isDefault && helper.isDefault
540
598
  };
541
599
  }
600
+ const { loopAround, ...rest } = branch;
542
601
  return {
543
- ...branch,
602
+ ...rest,
544
603
  ...reads,
545
604
  conditions,
546
605
  terminal: { ...helper.terminal, location: at },
547
606
  location: at,
607
+ ...loopAround !== void 0 && within(at, loopAround) ? { loopAround } : {},
548
608
  isDefault: false
549
609
  };
550
610
  });
551
611
  }
612
+ function within(inner, outer) {
613
+ return inner.start >= outer.start && inner.end <= outer.end;
614
+ }
552
615
  function mergeReadShapes(a, b) {
553
616
  if (a === null || b === null) {
554
617
  return a ?? b;
@@ -566,12 +629,11 @@ function mergeReadShapes(a, b) {
566
629
  function splitAtHandOffs(branch, sites, from = 0) {
567
630
  for (let index = from; index < sites.length; index++) {
568
631
  const site = sites[index];
569
- const reached = site.alwaysRuns === true || runsBefore(site.line, branch.terminal.location.end);
570
- if (!reached || !guardsHoldOn(site.preconditions, branch.conditions)) {
632
+ if (!firesOn(site, branch)) {
571
633
  continue;
572
634
  }
573
635
  return throughHelper(branch, site.at, site.helper).flatMap(
574
- (path10) => path10.terminal === branch.terminal ? splitAtHandOffs(path10, sites, index + 1) : [path10]
636
+ (path11) => path11.terminal === branch.terminal ? splitAtHandOffs(path11, sites, index + 1) : [path11]
575
637
  );
576
638
  }
577
639
  return [branch];
@@ -618,6 +680,34 @@ function rootName(ref) {
618
680
  return ref.type === "dependency" ? ref.name : null;
619
681
  }
620
682
 
683
+ // src/libraryEnvReads.ts
684
+ import { functionCallBinding } from "@suss/behavioral-ir";
685
+ function libraryEnvReadMarker(recognition, declared, importer) {
686
+ return {
687
+ kind: "library",
688
+ location: { file: importer, range: { start: 0, end: 0 }, exportName: null },
689
+ identity: {
690
+ name: `${declared.module} env reads`,
691
+ exportPath: null,
692
+ boundaryBinding: functionCallBinding({
693
+ transport: "in-process",
694
+ recognition
695
+ })
696
+ },
697
+ inputs: [],
698
+ transitions: [],
699
+ gaps: [],
700
+ confidence: { source: "declared", level: "high" },
701
+ metadata: {
702
+ libraryEnvReads: {
703
+ module: declared.module,
704
+ ...declared.prefixes !== void 0 ? { prefixes: declared.prefixes } : {},
705
+ ...declared.names !== void 0 ? { names: declared.names } : {}
706
+ }
707
+ }
708
+ };
709
+ }
710
+
621
711
  // src/moduleImports.ts
622
712
  function stampModuleImports(summaries, importsOf2) {
623
713
  const byFile = /* @__PURE__ */ new Map();
@@ -919,8 +1009,8 @@ var catchEntryCond = () => fixedCond(
919
1009
  "positive",
920
1010
  "catchBlock"
921
1011
  );
922
- function classify(path10, home, isAncestorOrSelf) {
923
- return path10.map((cond) => {
1012
+ function classify(path11, home, isAncestorOrSelf) {
1013
+ return path11.map((cond) => {
924
1014
  if (cond.branchStmt === null) {
925
1015
  return cond.info;
926
1016
  }
@@ -992,9 +1082,9 @@ function limitOpenPaths(paths) {
992
1082
  );
993
1083
  }
994
1084
  }
995
- function recordTerminal(ctx, terminal, home, path10) {
1085
+ function recordTerminal(ctx, terminal, home, path11) {
996
1086
  chargeBudget(ctx.state);
997
- const info = classify(path10, home, ctx.isAncestorOrSelf);
1087
+ const info = classify(path11, home, ctx.isAncestorOrSelf);
998
1088
  const existing = ctx.state.byTerminal.get(terminal) ?? [];
999
1089
  existing.push(info);
1000
1090
  ctx.state.byTerminal.set(terminal, existing);
@@ -1005,15 +1095,15 @@ function containsTerminal(ctx, stmt) {
1005
1095
  }
1006
1096
  return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
1007
1097
  }
1008
- function stepIf(stmt, path10, ctx) {
1098
+ function stepIf(stmt, path11, ctx) {
1009
1099
  const thenExit = exitKindOfList(stmt.thenBody);
1010
1100
  const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
1011
1101
  const thenPath = [
1012
- ...path10,
1102
+ ...path11,
1013
1103
  branchCond(stmt, stmt.condition, "positive", elseExit)
1014
1104
  ];
1015
1105
  const elsePath = [
1016
- ...path10,
1106
+ ...path11,
1017
1107
  branchCond(
1018
1108
  stmt,
1019
1109
  stmt.condition,
@@ -1030,7 +1120,7 @@ function stepIf(stmt, path10, ctx) {
1030
1120
  }
1031
1121
  return out;
1032
1122
  }
1033
- function stepSwitch(stmt, path10, ctx) {
1123
+ function stepSwitch(stmt, path11, ctx) {
1034
1124
  const out = [];
1035
1125
  const negations = [];
1036
1126
  let defaultRanBody = false;
@@ -1040,7 +1130,7 @@ function stepSwitch(stmt, path10, ctx) {
1040
1130
  if (hasStrayBreak(group.body)) {
1041
1131
  throw new UnmodeledFlow("non-trailing break in switch clause");
1042
1132
  }
1043
- const groupPath = group.condition === null ? [...path10, ...negations] : [...path10, fixedCond(group.condition, "positive", "explicit")];
1133
+ const groupPath = group.condition === null ? [...path11, ...negations] : [...path11, fixedCond(group.condition, "positive", "explicit")];
1044
1134
  const conts = enumerate(ctx, group.body, groupPath);
1045
1135
  if (conts.length > 0) {
1046
1136
  if (!group.hasTrailingBreak && !fallthroughSafe) {
@@ -1057,24 +1147,24 @@ function stepSwitch(stmt, path10, ctx) {
1057
1147
  }
1058
1148
  }
1059
1149
  if (!defaultRanBody) {
1060
- out.push([...path10, ...negations]);
1150
+ out.push([...path11, ...negations]);
1061
1151
  }
1062
1152
  return out;
1063
1153
  }
1064
- function stepLoop(stmt, path10, ctx) {
1154
+ function stepLoop(stmt, path11, ctx) {
1065
1155
  const bodyExit = exitKindOfList(stmt.body);
1066
1156
  if (containsTerminal(ctx, stmt)) {
1067
1157
  enumerate(ctx, stmt.body, [
1068
- ...path10,
1158
+ ...path11,
1069
1159
  loopIterationCond(stmt.condition.sourceText)
1070
1160
  ]);
1071
1161
  }
1072
1162
  if (bodyExit !== null) {
1073
- return [[...path10, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
1163
+ return [[...path11, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
1074
1164
  }
1075
- return [path10];
1165
+ return [path11];
1076
1166
  }
1077
- function stepTry(stmt, path10, ctx) {
1167
+ function stepTry(stmt, path11, ctx) {
1078
1168
  if (stmt.finallyBody !== null) {
1079
1169
  const finallyHasTerminal = stmt.finallyBody.some(
1080
1170
  (s) => containsTerminal(ctx, s)
@@ -1084,22 +1174,22 @@ function stepTry(stmt, path10, ctx) {
1084
1174
  }
1085
1175
  }
1086
1176
  const out = [];
1087
- out.push(...enumerate(ctx, stmt.tryBody, path10));
1177
+ out.push(...enumerate(ctx, stmt.tryBody, path11));
1088
1178
  if (stmt.catchBody !== null) {
1089
- const catchPath = [...path10, catchEntryCond()];
1179
+ const catchPath = [...path11, catchEntryCond()];
1090
1180
  out.push(...enumerate(ctx, stmt.catchBody, catchPath));
1091
1181
  }
1092
1182
  return out;
1093
1183
  }
1094
- function stepExit(stmt, path10, ctx) {
1184
+ function stepExit(stmt, path11, ctx) {
1095
1185
  const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
1096
1186
  if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
1097
- collector.push(path10);
1187
+ collector.push(path11);
1098
1188
  }
1099
1189
  return [];
1100
1190
  }
1101
- function stepOpaque(_stmt, path10, _ctx) {
1102
- return [path10];
1191
+ function stepOpaque(_stmt, path11, _ctx) {
1192
+ return [path11];
1103
1193
  }
1104
1194
  var stepHandlers = {
1105
1195
  if: stepIf,
@@ -1109,20 +1199,20 @@ var stepHandlers = {
1109
1199
  exit: stepExit,
1110
1200
  opaque: stepOpaque
1111
1201
  };
1112
- function stepCallback(ctx, body, path10) {
1202
+ function stepCallback(ctx, body, path11) {
1113
1203
  const exits = [];
1114
1204
  ctx.state.callbackExits.push(exits);
1115
1205
  let fellThrough;
1116
1206
  try {
1117
- fellThrough = enumerate(ctx, body, path10);
1207
+ fellThrough = enumerate(ctx, body, path11);
1118
1208
  } finally {
1119
1209
  ctx.state.callbackExits.pop();
1120
1210
  }
1121
1211
  const out = [...fellThrough, ...exits];
1122
- return out.length === 0 ? [path10] : out;
1212
+ return out.length === 0 ? [path11] : out;
1123
1213
  }
1124
- function stepStatement(ctx, stmt, path10) {
1125
- let paths = [path10];
1214
+ function stepStatement(ctx, stmt, path11) {
1215
+ let paths = [path11];
1126
1216
  for (const body of stmt.callbacks ?? []) {
1127
1217
  paths = paths.flatMap((each) => stepCallback(ctx, body, each));
1128
1218
  limitOpenPaths(paths);
@@ -1209,8 +1299,8 @@ function enumerate(ctx, stmts, prefix) {
1209
1299
  let frontiers = [[...prefix]];
1210
1300
  for (const stmt of stmts) {
1211
1301
  const nextFrontiers = [];
1212
- for (const path10 of mergeRejoined(frontiers, ctx.keepsArms)) {
1213
- nextFrontiers.push(...stepStatement(ctx, stmt, path10));
1302
+ for (const path11 of mergeRejoined(frontiers, ctx.keepsArms)) {
1303
+ nextFrontiers.push(...stepStatement(ctx, stmt, path11));
1214
1304
  }
1215
1305
  frontiers = nextFrontiers;
1216
1306
  limitOpenPaths(frontiers);
@@ -1234,6 +1324,39 @@ function buildParentMap(statements) {
1234
1324
  }
1235
1325
  return parentOf;
1236
1326
  }
1327
+ function placeTerminals(input, parentOf = buildParentMap(input.statements)) {
1328
+ const placement = /* @__PURE__ */ new Map();
1329
+ for (const [home, terminals] of input.terminalsByStmt) {
1330
+ const placed = placementOf(home, parentOf);
1331
+ if (placed.loopAround === void 0 && placed.cleansUpFrom === void 0) {
1332
+ continue;
1333
+ }
1334
+ for (const terminal of terminals) {
1335
+ placement.set(terminal, placed);
1336
+ }
1337
+ }
1338
+ return placement;
1339
+ }
1340
+ function placementOf(home, parentOf) {
1341
+ let loopAround = home.kind === "loop" ? home.lines : void 0;
1342
+ let cleansUpFrom;
1343
+ let child = home;
1344
+ let parent = parentOf.get(home) ?? null;
1345
+ while (parent !== null) {
1346
+ if (parent.kind === "loop" && parent.lines !== void 0) {
1347
+ loopAround = parent.lines;
1348
+ }
1349
+ if (parent.kind === "try" && parent.lines !== void 0 && (parent.finallyBody ?? []).includes(child)) {
1350
+ cleansUpFrom = parent.lines.start;
1351
+ }
1352
+ child = parent;
1353
+ parent = parentOf.get(parent) ?? null;
1354
+ }
1355
+ return {
1356
+ ...loopAround === void 0 ? {} : { loopAround: { start: loopAround.start, end: loopAround.end } },
1357
+ ...cleansUpFrom === void 0 ? {} : { cleansUpFrom }
1358
+ };
1359
+ }
1237
1360
  function enumerateStructuredPaths(input) {
1238
1361
  const parentOf = buildParentMap(input.statements);
1239
1362
  const isAncestorOrSelf = (a, b) => {
@@ -1259,11 +1382,15 @@ function enumerateStructuredPaths(input) {
1259
1382
  isAncestorOrSelf
1260
1383
  };
1261
1384
  const continuations = withRejoined(enumerate(ctx, input.statements, []));
1262
- for (const path10 of continuations) {
1385
+ for (const path11 of continuations) {
1263
1386
  chargeBudget(state);
1264
- state.fallthrough.push(classify(path10, null, isAncestorOrSelf));
1387
+ state.fallthrough.push(classify(path11, null, isAncestorOrSelf));
1265
1388
  }
1266
- return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
1389
+ return {
1390
+ byTerminal: state.byTerminal,
1391
+ fallthrough: state.fallthrough,
1392
+ placement: placeTerminals(input, parentOf)
1393
+ };
1267
1394
  }
1268
1395
  function enumerateOrDegrade(input, terminals) {
1269
1396
  try {
@@ -1283,9 +1410,28 @@ function enumerateOrDegrade(input, terminals) {
1283
1410
  for (const terminal of terminals) {
1284
1411
  byTerminal.set(terminal, [[marker]]);
1285
1412
  }
1286
- return { byTerminal, fallthrough: [[marker]], degraded: reason };
1413
+ return {
1414
+ byTerminal,
1415
+ fallthrough: [[marker]],
1416
+ placement: placeTerminals(input),
1417
+ degraded: reason
1418
+ };
1287
1419
  }
1288
1420
  }
1421
+ function enumerateCallStatements(input, terminals) {
1422
+ const placement = placeTerminals(input);
1423
+ const enumerable = new Map(
1424
+ [...input.terminalsByStmt].filter(
1425
+ ([, found]) => found.every(
1426
+ (terminal) => placement.get(terminal)?.cleansUpFrom === void 0
1427
+ )
1428
+ )
1429
+ );
1430
+ return {
1431
+ ...enumerateOrDegrade({ ...input, terminalsByStmt: enumerable }, terminals),
1432
+ placement
1433
+ };
1434
+ }
1289
1435
  function reasonToDegrade(error) {
1290
1436
  if (error instanceof PathBudgetExceeded) {
1291
1437
  return error.message === "" ? "path budget exceeded" : error.message;
@@ -1304,8 +1450,8 @@ function sharedGatingConditions(paths, readPredicate) {
1304
1450
  return [];
1305
1451
  }
1306
1452
  const shared = new Set(first.map(keyOf2));
1307
- for (const path10 of rest) {
1308
- const here = new Set(path10.map(keyOf2));
1453
+ for (const path11 of rest) {
1454
+ const here = new Set(path11.map(keyOf2));
1309
1455
  for (const value of shared) {
1310
1456
  if (!here.has(value)) {
1311
1457
  shared.delete(value);
@@ -1423,6 +1569,43 @@ function walkDescendants(root, carried, childrenOf2, visitor) {
1423
1569
  }
1424
1570
  }
1425
1571
 
1572
+ // src/paths/statementSites.ts
1573
+ function statementSites(nodes, reading) {
1574
+ const statements = new IdMap();
1575
+ const statementOfNode = new IdMap();
1576
+ for (const node of nodes) {
1577
+ const written = reading.statementOf(node);
1578
+ if (written === null) {
1579
+ continue;
1580
+ }
1581
+ const statement = statements.get(written) ?? written;
1582
+ statements.set(statement, statement);
1583
+ statementOfNode.set(node, statement);
1584
+ }
1585
+ const terminals = [...statements].map(([statement]) => statement);
1586
+ const enumerated = enumerateCallStatements(
1587
+ reading.lower(terminals),
1588
+ terminals
1589
+ );
1590
+ const pathsTo = (node) => {
1591
+ const statement = statementOfNode.get(node);
1592
+ return statement === void 0 ? void 0 : enumerated.byTerminal.get(statement);
1593
+ };
1594
+ const siteOf = (node, preconditions) => {
1595
+ const statement = statementOfNode.get(node);
1596
+ return {
1597
+ line: reading.firstLine(statement ?? node),
1598
+ preconditions,
1599
+ cleansUpFrom: statement === void 0 ? void 0 : enumerated.placement.get(statement)?.cleansUpFrom
1600
+ };
1601
+ };
1602
+ return {
1603
+ pathsTo,
1604
+ siteOf,
1605
+ plainSiteOf: (node) => siteOf(node, sharedGatingConditions(pathsTo(node)))
1606
+ };
1607
+ }
1608
+
1426
1609
  // src/projectRoot.ts
1427
1610
  import path3 from "path";
1428
1611
  function commonDirectoryOf(files) {
@@ -1571,13 +1754,13 @@ function quoted(text, key) {
1571
1754
  }
1572
1755
 
1573
1756
  // src/adapterStamp.ts
1574
- import { createHash as createHash2 } from "crypto";
1757
+ import { createHash as createHash3 } from "crypto";
1575
1758
  import fs3 from "fs";
1576
1759
  import path5 from "path";
1577
1760
  import { fileURLToPath } from "url";
1578
1761
 
1579
1762
  // src/codeHash.ts
1580
- import { createHash } from "crypto";
1763
+ import { createHash as createHash2 } from "crypto";
1581
1764
  import fs2 from "fs";
1582
1765
  import { createRequire } from "module";
1583
1766
  import path4 from "path";
@@ -1623,7 +1806,7 @@ function createCodeHasher() {
1623
1806
  if (files.length === 0) {
1624
1807
  return "";
1625
1808
  }
1626
- const hash = createHash("sha256");
1809
+ const hash = createHash2("sha256");
1627
1810
  for (const file of files) {
1628
1811
  hash.update(read.get(file)?.digest ?? "");
1629
1812
  }
@@ -1640,7 +1823,7 @@ function readCodeFile(file) {
1640
1823
  return null;
1641
1824
  }
1642
1825
  return {
1643
- digest: createHash("sha256").update(text).digest("hex"),
1826
+ digest: createHash2("sha256").update(text).digest("hex"),
1644
1827
  imports: importsOf(file, text.toString("utf8"))
1645
1828
  };
1646
1829
  }
@@ -1798,7 +1981,7 @@ function computeContentHash(paths) {
1798
1981
  if (paths.length === 0) {
1799
1982
  return "";
1800
1983
  }
1801
- const hash = createHash2("sha256");
1984
+ const hash = createHash3("sha256");
1802
1985
  for (const file of paths) {
1803
1986
  try {
1804
1987
  hash.update(fs3.readFileSync(file));
@@ -1812,7 +1995,7 @@ function projectFileStamp(paths) {
1812
1995
  if (paths.length === 0) {
1813
1996
  return "none";
1814
1997
  }
1815
- const hash = createHash2("sha256");
1998
+ const hash = createHash3("sha256");
1816
1999
  for (const file of [...paths].sort()) {
1817
2000
  hash.update(file);
1818
2001
  hash.update("\0");
@@ -1847,7 +2030,7 @@ function runDigest(packsDigest, packs, files) {
1847
2030
  }
1848
2031
 
1849
2032
  // src/cache.ts
1850
- import { createHash as createHash5 } from "crypto";
2033
+ import { createHash as createHash6 } from "crypto";
1851
2034
  import fs7 from "fs/promises";
1852
2035
  import path8 from "path";
1853
2036
  import { readJsonFile as readJsonFile3, writeDocumentFile as writeDocumentFile2 } from "@suss/behavioral-ir/files";
@@ -1955,7 +2138,7 @@ function keyOf3(binding) {
1955
2138
  }
1956
2139
 
1957
2140
  // src/cacheShards.ts
1958
- import { createHash as createHash3 } from "crypto";
2141
+ import { createHash as createHash4 } from "crypto";
1959
2142
  import fs4 from "fs";
1960
2143
  import path6 from "path";
1961
2144
  import {
@@ -2021,7 +2204,7 @@ function namesIn(dir) {
2021
2204
  }
2022
2205
  function writeOnce(root, value) {
2023
2206
  const pieces = [...documentPieces(value)];
2024
- const hash = createHash3("sha256");
2207
+ const hash = createHash4("sha256");
2025
2208
  for (const piece of pieces) {
2026
2209
  hash.update(piece);
2027
2210
  }
@@ -2125,7 +2308,7 @@ function copyOfJson(value) {
2125
2308
  }
2126
2309
 
2127
2310
  // src/cacheStamps.ts
2128
- import { createHash as createHash4 } from "crypto";
2311
+ import { createHash as createHash5 } from "crypto";
2129
2312
  import fs5 from "fs/promises";
2130
2313
  var RECENT_CHANGE_MS = 2e3;
2131
2314
  function stampMayMissAWrite(stat, statAt) {
@@ -2215,7 +2398,7 @@ function reusableHash(prior, stamp) {
2215
2398
  async function hashFile(filePath) {
2216
2399
  try {
2217
2400
  const content = await fs5.readFile(filePath);
2218
- return createHash4("sha256").update(content).digest("hex").slice(0, 16);
2401
+ return createHash5("sha256").update(content).digest("hex").slice(0, 16);
2219
2402
  } catch {
2220
2403
  return null;
2221
2404
  }
@@ -2671,7 +2854,7 @@ function entryDirFor(cacheDir, input) {
2671
2854
  input.adapterPacksDigest,
2672
2855
  input.configPath ?? ""
2673
2856
  ].join(" ");
2674
- const name = createHash5("sha256").update(key).digest("hex").slice(0, 16);
2857
+ const name = createHash6("sha256").update(key).digest("hex").slice(0, 16);
2675
2858
  return path8.join(cacheDir, `${ENTRY_PREFIX}${name}`);
2676
2859
  }
2677
2860
  function isEntryDir(name) {
@@ -2745,7 +2928,7 @@ async function readManifest2(manifestPath) {
2745
2928
  }
2746
2929
 
2747
2930
  // src/cacheLocation.ts
2748
- import { createHash as createHash6 } from "crypto";
2931
+ import { createHash as createHash7 } from "crypto";
2749
2932
  import fs8 from "fs";
2750
2933
  import path9 from "path";
2751
2934
  var CACHE_DIR_ENV = "SUSS_CACHE_DIR";
@@ -2764,7 +2947,7 @@ function realPathOrSelf(dir) {
2764
2947
  }
2765
2948
  }
2766
2949
  function projectKey(projectDir) {
2767
- return createHash6("sha256").update(realPathOrSelf(path9.resolve(projectDir))).digest("hex").slice(0, 16);
2950
+ return createHash7("sha256").update(realPathOrSelf(path9.resolve(projectDir))).digest("hex").slice(0, 16);
2768
2951
  }
2769
2952
  function extractionCacheDirFor(projectDir) {
2770
2953
  const root = chosenCacheRoot();
@@ -2773,6 +2956,13 @@ function extractionCacheDirFor(projectDir) {
2773
2956
  }
2774
2957
  return path9.join(root, "cache", projectKey(projectDir));
2775
2958
  }
2959
+ function mapFolderFor(projectDir) {
2960
+ const root = chosenCacheRoot();
2961
+ if (root === null) {
2962
+ return path9.join(projectDir, ".suss");
2963
+ }
2964
+ return path9.join(root, "map", projectKey(projectDir));
2965
+ }
2776
2966
  function liveRecordFor(projectDir) {
2777
2967
  const root = chosenCacheRoot();
2778
2968
  if (root === null) {
@@ -2921,6 +3111,7 @@ var KeptParses = class {
2921
3111
  };
2922
3112
 
2923
3113
  // src/packHealth.ts
3114
+ import path10 from "path";
2924
3115
  function stagesOf(funnel) {
2925
3116
  const stages = [];
2926
3117
  const gateSaysSomething = funnel.gates.length > 0 && funnel.unresolvedGates.length === 0;
@@ -3068,6 +3259,30 @@ function packConfigCheck(needs) {
3068
3259
  }))
3069
3260
  };
3070
3261
  }
3262
+ function describeMissingPath(missing) {
3263
+ const where = `${missing.option} is ${missing.value} in ${missing.configFile}`;
3264
+ if (path10.isAbsolute(missing.value)) {
3265
+ return {
3266
+ problem: `${where}, and nothing exists there.`,
3267
+ fix: `Set ${missing.option} to a path that exists.`
3268
+ };
3269
+ }
3270
+ return {
3271
+ problem: `${where}, and nothing exists at ${missing.resolved}. A relative path is read against the config file, not the project.`,
3272
+ fix: missing.inProject === void 0 ? `Set ${missing.option} to an absolute path, or move the config file into the project.` : `Set ${missing.option} to ${missing.inProject} in the config file, or move the config file into the project.`
3273
+ };
3274
+ }
3275
+ function packPathCheck(missing) {
3276
+ return {
3277
+ name: "every path a pack config gives exists",
3278
+ code: "no-path",
3279
+ audience: "run",
3280
+ violations: missing.map((one) => {
3281
+ const { problem, fix } = describeMissingPath(one);
3282
+ return { label: one.pack, detail: `${problem} ${fix}` };
3283
+ })
3284
+ };
3285
+ }
3071
3286
  function evaluatePackHealth(report) {
3072
3287
  const gradients = packGradients(report);
3073
3288
  return [
@@ -3220,7 +3435,7 @@ function makeTransitionId(functionName, branch) {
3220
3435
  const { terminal } = branch;
3221
3436
  const statusKey = terminal.statusCode === null ? "none" : terminal.statusCode.type === "literal" ? String(terminal.statusCode.value) : `dyn:${terminal.statusCode.sourceText}`;
3222
3437
  const conditionSig = branch.conditions.map((c) => `${c.polarity}:${c.sourceText}`).join(";");
3223
- const conditionHash = createHash7("sha1").update(conditionSig).digest("hex").slice(0, 7);
3438
+ const conditionHash = createHash8("sha1").update(conditionSig).digest("hex").slice(0, 7);
3224
3439
  return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
3225
3440
  }
3226
3441
  function collapseReading(table, reading) {
@@ -3295,11 +3510,11 @@ function provenanceOf(raw, effects) {
3295
3510
  if (raw.length === 0) {
3296
3511
  return [];
3297
3512
  }
3298
- const positionOf = new Map(effects.map((one, at) => [effectKey(one), at]));
3513
+ const positionOf = new Map(effects.map((one, at) => [effectKey2(one), at]));
3299
3514
  const bySlot = /* @__PURE__ */ new Map();
3300
3515
  const said = /* @__PURE__ */ new Map();
3301
3516
  for (const one of raw) {
3302
- const effect = positionOf.get(effectKey(withNormalizedCallee(one.effect)));
3517
+ const effect = positionOf.get(effectKey2(withNormalizedCallee(one.effect)));
3303
3518
  if (effect === void 0) {
3304
3519
  continue;
3305
3520
  }
@@ -3331,7 +3546,7 @@ function withNormalizedCallee(effect) {
3331
3546
  if (effect.callee === void 0) {
3332
3547
  return effect;
3333
3548
  }
3334
- return { ...effect, callee: normalizeCalleeText(effect.callee) };
3549
+ return { ...effect, callee: normalizeCalleeText2(effect.callee) };
3335
3550
  }
3336
3551
  function assembleSummary(raw, options = DEFAULT_OPTIONS) {
3337
3552
  const transitions = raw.branches.map((rawBranch) => {
@@ -3419,6 +3634,9 @@ function buildMetadata(raw) {
3419
3634
  if (raw.test !== void 0) {
3420
3635
  metadata = withTestMetadata(metadata, raw.test);
3421
3636
  }
3637
+ if (raw.packMetadata !== void 0) {
3638
+ metadata = { ...metadata, ...raw.packMetadata };
3639
+ }
3422
3640
  return Object.keys(metadata).length > 0 ? metadata : null;
3423
3641
  }
3424
3642
  function buildHttpMetadataValue(raw) {
@@ -3470,6 +3688,12 @@ function buildGraphqlMetadataValue(raw) {
3470
3688
  if (raw.graphqlUnresolvedDocument !== void 0) {
3471
3689
  graphql.unresolvedDocument = raw.graphqlUnresolvedDocument;
3472
3690
  }
3691
+ if (raw.graphqlClient !== void 0) {
3692
+ graphql.client = raw.graphqlClient;
3693
+ }
3694
+ if (raw.graphqlUnresolvedClient !== void 0) {
3695
+ graphql.unresolvedClient = raw.graphqlUnresolvedClient;
3696
+ }
3473
3697
  return Object.keys(graphql).length > 0 ? graphql : null;
3474
3698
  }
3475
3699
  function answersWithHttpResponses(raw) {
@@ -3512,8 +3736,23 @@ function detectGaps(raw, transitions, options) {
3512
3736
  if (raw.declaredContract && answersWithHttpResponses(raw)) {
3513
3737
  gaps.push(...contractStatusGaps(raw.declaredContract, transitions));
3514
3738
  }
3739
+ gaps.push(...unsettledJobGaps(transitions));
3515
3740
  return gaps;
3516
3741
  }
3742
+ function unsettledJobGaps(transitions) {
3743
+ const callees = /* @__PURE__ */ new Set();
3744
+ for (const transition of transitions) {
3745
+ for (const effect of transition.effects) {
3746
+ if (effect.type !== "interaction" || effect.interaction.class !== "message-send" || !isUnsettledJob(effect.binding)) {
3747
+ continue;
3748
+ }
3749
+ callees.add(effect.callee ?? displayLabel2(effect.binding));
3750
+ }
3751
+ }
3752
+ return [...callees].map(
3753
+ (callee) => unfollowedCallGap({ callee, reason: "unsettledJob" })
3754
+ );
3755
+ }
3517
3756
  function bodyWentUnread(raw) {
3518
3757
  if (raw.readFailure !== void 0) {
3519
3758
  return true;
@@ -3661,7 +3900,10 @@ var effectConverters = {
3661
3900
  stateChange: (e) => ({ type: "stateChange", variable: e.variable })
3662
3901
  };
3663
3902
  function effectToIR(effect) {
3664
- return effectConverters[effect.type](effect);
3903
+ const converted = effectConverters[effect.type](
3904
+ effect
3905
+ );
3906
+ return effect.location === void 0 ? converted : { ...converted, location: effect.location };
3665
3907
  }
3666
3908
  function paramToInput(param) {
3667
3909
  return {
@@ -3692,6 +3934,7 @@ export {
3692
3934
  assembleSummary,
3693
3935
  assessConfidence,
3694
3936
  buildUngatedExtractionReport,
3937
+ callSiteKey,
3695
3938
  chosenCacheRoot,
3696
3939
  commonDirectoryOf,
3697
3940
  composeWrappers,
@@ -3703,23 +3946,28 @@ export {
3703
3946
  createCodeHasher,
3704
3947
  createPackTallies,
3705
3948
  createTimer,
3949
+ describeMissingPath,
3706
3950
  detectGaps,
3707
3951
  effectToIR,
3952
+ effectsFiringOn,
3708
3953
  emptyTally,
3709
3954
  enumerateOrDegrade,
3710
3955
  enumerateStructuredPaths,
3711
3956
  evaluatePackHealth,
3712
3957
  extractionCacheDirFor,
3713
3958
  extractionConfigStamp,
3959
+ firesOn,
3714
3960
  firstWrittenReading,
3715
3961
  formatPackHealth,
3716
3962
  guardsHoldOn,
3717
3963
  helperPathsOnVariables,
3718
3964
  httpRouteDiscovery,
3719
3965
  isListedTestFile,
3966
+ libraryEnvReadMarker,
3720
3967
  liveRecordFor,
3721
3968
  makeIgnoredDirectory,
3722
3969
  makeTransitionId,
3970
+ mapFolderFor,
3723
3971
  mapReading,
3724
3972
  markReturnsAsExitCode,
3725
3973
  matchesTestFileName,
@@ -3727,10 +3975,13 @@ export {
3727
3975
  moduleInitStructure,
3728
3976
  moduleOfFile,
3729
3977
  noopTimer,
3978
+ numberRepeatedCallSites,
3730
3979
  ownersWhereWritten,
3731
3980
  packConfigCheck,
3732
3981
  packGradients,
3982
+ packPathCheck,
3733
3983
  paramToInput,
3984
+ placeTerminals,
3734
3985
  processCodeHasher,
3735
3986
  projectFileStamp,
3736
3987
  rawConditionToPredicate,
@@ -3748,6 +3999,7 @@ export {
3748
3999
  stampMayMissAWrite,
3749
4000
  stampModuleImports,
3750
4001
  stampModules,
4002
+ statementSites,
3751
4003
  storageSystemOption,
3752
4004
  summaryCountsByPack,
3753
4005
  tallyUnit,