@suss/extractor 0.33.1 → 0.34.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.cjs CHANGED
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ EnclosingNodes: () => EnclosingNodes,
33
34
  IdMap: () => IdMap,
34
35
  IdSet: () => IdSet,
36
+ KeptParses: () => KeptParses,
35
37
  MAX_ENTRIES: () => MAX_ENTRIES,
36
38
  MAX_PATHS: () => MAX_PATHS,
39
+ MODULE_SURFACE_RECOGNITION: () => MODULE_SURFACE_RECOGNITION,
37
40
  PathBudgetExceeded: () => PathBudgetExceeded,
38
41
  SKIP_CHILDREN: () => SKIP_CHILDREN,
39
42
  UnmodeledFlow: () => UnmodeledFlow,
@@ -58,13 +61,18 @@ __export(index_exports, {
58
61
  enumerateOrDegrade: () => enumerateOrDegrade,
59
62
  enumerateStructuredPaths: () => enumerateStructuredPaths,
60
63
  evaluatePackHealth: () => evaluatePackHealth,
64
+ extractionConfigStamp: () => extractionConfigStamp,
61
65
  firstWrittenReading: () => firstWrittenReading,
62
66
  formatPackHealth: () => formatPackHealth,
63
67
  guardsHoldOn: () => guardsHoldOn,
64
68
  httpRouteDiscovery: () => httpRouteDiscovery,
69
+ isListedTestFile: () => isListedTestFile,
65
70
  makeTransitionId: () => makeTransitionId,
66
71
  mapReading: () => mapReading,
72
+ markReturnsAsExitCode: () => markReturnsAsExitCode,
73
+ matchesTestFileName: () => matchesTestFileName,
67
74
  moduleInitStructure: () => moduleInitStructure,
75
+ moduleOfFile: () => moduleOfFile,
68
76
  noopTimer: () => noopTimer,
69
77
  packGradients: () => packGradients,
70
78
  paramToInput: () => paramToInput,
@@ -76,8 +84,12 @@ __export(index_exports, {
76
84
  runDigest: () => runDigest,
77
85
  runsBefore: () => runsBefore,
78
86
  scopeOption: () => scopeOption,
87
+ settleModules: () => settleModules,
79
88
  sharedGatingConditions: () => sharedGatingConditions,
89
+ sourceRefsOf: () => sourceRefsOf,
90
+ stampMayMissAWrite: () => stampMayMissAWrite,
80
91
  stampModuleImports: () => stampModuleImports,
92
+ stampModules: () => stampModules,
81
93
  storageSystemOption: () => storageSystemOption,
82
94
  summaryCountsByPack: () => summaryCountsByPack,
83
95
  tallyUnit: () => tallyUnit,
@@ -90,8 +102,8 @@ __export(index_exports, {
90
102
  writtenReading: () => writtenReading
91
103
  });
92
104
  module.exports = __toCommonJS(index_exports);
93
- var import_node_crypto3 = require("crypto");
94
- var import_behavioral_ir5 = require("@suss/behavioral-ir");
105
+ var import_node_crypto4 = require("crypto");
106
+ var import_behavioral_ir6 = require("@suss/behavioral-ir");
95
107
 
96
108
  // src/contractStatusGaps.ts
97
109
  var import_behavioral_ir = require("@suss/behavioral-ir");
@@ -354,6 +366,56 @@ function withDistinctIds(transitions) {
354
366
  });
355
367
  }
356
368
 
369
+ // src/declaredModules.ts
370
+ var import_node_fs = __toESM(require("fs"), 1);
371
+ var import_node_path = __toESM(require("path"), 1);
372
+ var MODULE_SURFACE_RECOGNITION = "modules";
373
+ function settleModules(modules, candidates) {
374
+ return (modules ?? []).map((module2) => ({
375
+ ...module2,
376
+ public: module2.public ?? candidates.map(
377
+ (one) => import_node_path.default.join(module2.root, one.replace("{name}", module2.name))
378
+ ).filter((file) => import_node_fs.default.existsSync(file))
379
+ }));
380
+ }
381
+ function isInside(file, root) {
382
+ const relative = import_node_path.default.relative(root, file);
383
+ return relative !== "" && !relative.startsWith("..") && !import_node_path.default.isAbsolute(relative);
384
+ }
385
+ function moduleOfFile(modules, file) {
386
+ const byPublicFile = modules.find((module2) => module2.public.includes(file));
387
+ if (byPublicFile !== void 0) {
388
+ return byPublicFile;
389
+ }
390
+ let deepest;
391
+ for (const module2 of modules) {
392
+ if (isInside(file, module2.root) && (deepest === void 0 || module2.root.length > deepest.root.length)) {
393
+ deepest = module2;
394
+ }
395
+ }
396
+ return deepest;
397
+ }
398
+ function stampModules(summaries, modules, root) {
399
+ if (modules.length === 0) {
400
+ return;
401
+ }
402
+ for (const summary of summaries) {
403
+ const file = import_node_path.default.resolve(root ?? "", summary.location.file);
404
+ const module2 = moduleOfFile(modules, file);
405
+ if (module2 !== void 0) {
406
+ summary.location.module = module2.name;
407
+ }
408
+ }
409
+ }
410
+ function modulesStamp(modules) {
411
+ if (modules === void 0 || modules.length === 0) {
412
+ return void 0;
413
+ }
414
+ return JSON.stringify(
415
+ modules.map((module2) => [module2.name, module2.root, [...module2.public]])
416
+ );
417
+ }
418
+
357
419
  // src/effectGuards.ts
358
420
  var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
359
421
  var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
@@ -370,6 +432,41 @@ function runsBefore(effectLine, terminalEndLine) {
370
432
  return effectLine <= terminalEndLine;
371
433
  }
372
434
 
435
+ // src/exitCode.ts
436
+ var import_node_path2 = __toESM(require("path"), 1);
437
+ var import_behavioral_ir3 = require("@suss/behavioral-ir");
438
+ function markReturnsAsExitCode(summaries, returnsTheCode, workspaceRoot) {
439
+ for (const summary of summaries) {
440
+ const span = summary.location.span;
441
+ const file = workspaceRoot === void 0 ? summary.location.file : import_node_path2.default.resolve(workspaceRoot, summary.location.file);
442
+ markExitCodeFrom(
443
+ summary,
444
+ span !== void 0 && returnsTheCode.has((0, import_behavioral_ir3.declarationKey)(file, span))
445
+ );
446
+ }
447
+ }
448
+ function markExitCodeFrom(summary, marked) {
449
+ const process2 = summary.metadata?.process;
450
+ if (marked) {
451
+ summary.metadata = {
452
+ ...summary.metadata ?? {},
453
+ process: { ...process2 ?? {}, exitCodeFrom: "return" }
454
+ };
455
+ return;
456
+ }
457
+ if (process2 === void 0 || !("exitCodeFrom" in process2)) {
458
+ return;
459
+ }
460
+ const { exitCodeFrom: _dropped, ...rest } = process2;
461
+ const { process: _process, ...others } = summary.metadata ?? {};
462
+ const left = Object.keys(rest).length === 0 ? others : { ...others, process: rest };
463
+ if (Object.keys(left).length === 0) {
464
+ Reflect.deleteProperty(summary, "metadata");
465
+ return;
466
+ }
467
+ summary.metadata = left;
468
+ }
469
+
373
470
  // src/moduleImports.ts
374
471
  function stampModuleImports(summaries, importsOf) {
375
472
  const byFile = /* @__PURE__ */ new Map();
@@ -446,8 +543,8 @@ function httpRouteDiscovery(opts) {
446
543
  registrationChain: [...opts.methods]
447
544
  },
448
545
  bindingExtraction: {
449
- // `.all` registers every method, so it records "*", which the pairing
450
- // engine treats as agreeing with any method at all.
546
+ // `.all` registers every method, so it records "*", which pairs with
547
+ // any method.
451
548
  method: {
452
549
  type: "fromRegistration",
453
550
  position: "methodName",
@@ -463,10 +560,9 @@ function httpRouteDiscovery(opts) {
463
560
  }
464
561
  return [
465
562
  ...calls,
466
- // Routes registered in a loop over an array of specs, a shape
467
- // registration-call discovery cannot see. The receiver comes from
468
- // the same import declaration as the calls above, so a loop that
469
- // never touches this library's routable is left alone.
563
+ // Routes registered in a loop over an array of specs, which the calls
564
+ // above cannot see. The receiver must come from the same import, so a
565
+ // loop that never touches this library's routable is left alone.
470
566
  {
471
567
  kind,
472
568
  match: {
@@ -613,7 +709,7 @@ var configuredCallOption = import_zod.z.object({
613
709
  receiver: import_zod.z.string(),
614
710
  /** Method that performs the send. */
615
711
  method: import_zod.z.string(),
616
- /** Argument index carrying the subject. */
712
+ /** Index of the argument that is the subject. */
617
713
  subjectArg: import_zod.z.number(),
618
714
  /**
619
715
  * Which argument the message body is. Left out when the method has no
@@ -624,7 +720,7 @@ var configuredCallOption = import_zod.z.object({
624
720
  }).strict();
625
721
 
626
722
  // src/paths/enumeratePaths.ts
627
- var import_behavioral_ir3 = require("@suss/behavioral-ir");
723
+ var import_behavioral_ir4 = require("@suss/behavioral-ir");
628
724
  var MAX_PATHS = 256;
629
725
  var UnmodeledFlow = class extends Error {
630
726
  };
@@ -668,12 +764,12 @@ var loopCompletedCond = (headerText, exit) => fixedCond(
668
764
  exit === "throw" ? "earlyThrow" : "earlyReturn"
669
765
  );
670
766
  var catchEntryCond = () => fixedCond(
671
- { sourceText: import_behavioral_ir3.CATCH_ENTRY_TEXT, expression: null },
767
+ { sourceText: import_behavioral_ir4.CATCH_ENTRY_TEXT, expression: null },
672
768
  "positive",
673
769
  "catchBlock"
674
770
  );
675
- function classify(path4, home, isAncestorOrSelf) {
676
- return path4.map((cond) => {
771
+ function classify(path6, home, isAncestorOrSelf) {
772
+ return path6.map((cond) => {
677
773
  if (cond.branchStmt === null) {
678
774
  return cond.info;
679
775
  }
@@ -738,9 +834,9 @@ function chargeBudget(state) {
738
834
  );
739
835
  }
740
836
  }
741
- function recordTerminal(ctx, terminal, home, path4) {
837
+ function recordTerminal(ctx, terminal, home, path6) {
742
838
  chargeBudget(ctx.state);
743
- const info = classify(path4, home, ctx.isAncestorOrSelf);
839
+ const info = classify(path6, home, ctx.isAncestorOrSelf);
744
840
  const existing = ctx.state.byTerminal.get(terminal) ?? [];
745
841
  existing.push(info);
746
842
  ctx.state.byTerminal.set(terminal, existing);
@@ -751,15 +847,15 @@ function containsTerminal(ctx, stmt) {
751
847
  }
752
848
  return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
753
849
  }
754
- function stepIf(stmt, path4, ctx) {
850
+ function stepIf(stmt, path6, ctx) {
755
851
  const thenExit = exitKindOfList(stmt.thenBody);
756
852
  const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
757
853
  const thenPath = [
758
- ...path4,
854
+ ...path6,
759
855
  branchCond(stmt, stmt.condition, "positive", elseExit)
760
856
  ];
761
857
  const elsePath = [
762
- ...path4,
858
+ ...path6,
763
859
  branchCond(
764
860
  stmt,
765
861
  stmt.condition,
@@ -776,7 +872,7 @@ function stepIf(stmt, path4, ctx) {
776
872
  }
777
873
  return out;
778
874
  }
779
- function stepSwitch(stmt, path4, ctx) {
875
+ function stepSwitch(stmt, path6, ctx) {
780
876
  const out = [];
781
877
  const negations = [];
782
878
  let defaultRanBody = false;
@@ -786,7 +882,7 @@ function stepSwitch(stmt, path4, ctx) {
786
882
  if (hasStrayBreak(group.body)) {
787
883
  throw new UnmodeledFlow("non-trailing break in switch clause");
788
884
  }
789
- const groupPath = group.condition === null ? [...path4, ...negations] : [...path4, fixedCond(group.condition, "positive", "explicit")];
885
+ const groupPath = group.condition === null ? [...path6, ...negations] : [...path6, fixedCond(group.condition, "positive", "explicit")];
790
886
  const conts = enumerate(ctx, group.body, groupPath);
791
887
  if (conts.length > 0) {
792
888
  if (!group.hasTrailingBreak && !fallthroughSafe) {
@@ -803,24 +899,24 @@ function stepSwitch(stmt, path4, ctx) {
803
899
  }
804
900
  }
805
901
  if (!defaultRanBody) {
806
- out.push([...path4, ...negations]);
902
+ out.push([...path6, ...negations]);
807
903
  }
808
904
  return out;
809
905
  }
810
- function stepLoop(stmt, path4, ctx) {
906
+ function stepLoop(stmt, path6, ctx) {
811
907
  const bodyExit = exitKindOfList(stmt.body);
812
908
  if (containsTerminal(ctx, stmt)) {
813
909
  enumerate(ctx, stmt.body, [
814
- ...path4,
910
+ ...path6,
815
911
  loopIterationCond(stmt.condition.sourceText)
816
912
  ]);
817
913
  }
818
914
  if (bodyExit !== null) {
819
- return [[...path4, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
915
+ return [[...path6, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
820
916
  }
821
- return [path4];
917
+ return [path6];
822
918
  }
823
- function stepTry(stmt, path4, ctx) {
919
+ function stepTry(stmt, path6, ctx) {
824
920
  if (stmt.finallyBody !== null) {
825
921
  const finallyHasTerminal = stmt.finallyBody.some(
826
922
  (s) => containsTerminal(ctx, s)
@@ -830,22 +926,22 @@ function stepTry(stmt, path4, ctx) {
830
926
  }
831
927
  }
832
928
  const out = [];
833
- out.push(...enumerate(ctx, stmt.tryBody, path4));
929
+ out.push(...enumerate(ctx, stmt.tryBody, path6));
834
930
  if (stmt.catchBody !== null) {
835
- const catchPath = [...path4, catchEntryCond()];
931
+ const catchPath = [...path6, catchEntryCond()];
836
932
  out.push(...enumerate(ctx, stmt.catchBody, catchPath));
837
933
  }
838
934
  return out;
839
935
  }
840
- function stepExit(stmt, path4, ctx) {
936
+ function stepExit(stmt, path6, ctx) {
841
937
  const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
842
938
  if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
843
- collector.push(path4);
939
+ collector.push(path6);
844
940
  }
845
941
  return [];
846
942
  }
847
- function stepOpaque(_stmt, path4, _ctx) {
848
- return [path4];
943
+ function stepOpaque(_stmt, path6, _ctx) {
944
+ return [path6];
849
945
  }
850
946
  var stepHandlers = {
851
947
  if: stepIf,
@@ -855,20 +951,20 @@ var stepHandlers = {
855
951
  exit: stepExit,
856
952
  opaque: stepOpaque
857
953
  };
858
- function stepCallback(ctx, body, path4) {
954
+ function stepCallback(ctx, body, path6) {
859
955
  const exits = [];
860
956
  ctx.state.callbackExits.push(exits);
861
957
  let fellThrough;
862
958
  try {
863
- fellThrough = enumerate(ctx, body, path4);
959
+ fellThrough = enumerate(ctx, body, path6);
864
960
  } finally {
865
961
  ctx.state.callbackExits.pop();
866
962
  }
867
963
  const out = [...fellThrough, ...exits];
868
- return out.length === 0 ? [path4] : out;
964
+ return out.length === 0 ? [path6] : out;
869
965
  }
870
- function stepStatement(ctx, stmt, path4) {
871
- let paths = [path4];
966
+ function stepStatement(ctx, stmt, path6) {
967
+ let paths = [path6];
872
968
  for (const body of stmt.callbacks ?? []) {
873
969
  paths = paths.flatMap((each) => stepCallback(ctx, body, each));
874
970
  }
@@ -949,8 +1045,8 @@ function enumerate(ctx, stmts, prefix) {
949
1045
  let frontiers = [[...prefix]];
950
1046
  for (const stmt of stmts) {
951
1047
  const nextFrontiers = [];
952
- for (const path4 of mergeRejoined(frontiers)) {
953
- nextFrontiers.push(...stepStatement(ctx, stmt, path4));
1048
+ for (const path6 of mergeRejoined(frontiers)) {
1049
+ nextFrontiers.push(...stepStatement(ctx, stmt, path6));
954
1050
  }
955
1051
  frontiers = nextFrontiers;
956
1052
  if (frontiers.length > MAX_PATHS) {
@@ -1001,9 +1097,9 @@ function enumerateStructuredPaths(input) {
1001
1097
  isAncestorOrSelf
1002
1098
  };
1003
1099
  const continuations = withRejoined(enumerate(ctx, input.statements, []));
1004
- for (const path4 of continuations) {
1100
+ for (const path6 of continuations) {
1005
1101
  chargeBudget(state);
1006
- state.fallthrough.push(classify(path4, null, isAncestorOrSelf));
1102
+ state.fallthrough.push(classify(path6, null, isAncestorOrSelf));
1007
1103
  }
1008
1104
  return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
1009
1105
  }
@@ -1046,8 +1142,8 @@ function sharedGatingConditions(paths, readPredicate) {
1046
1142
  return [];
1047
1143
  }
1048
1144
  const shared = new Set(first.map(keyOf2));
1049
- for (const path4 of rest) {
1050
- const here = new Set(path4.map(keyOf2));
1145
+ for (const path6 of rest) {
1146
+ const here = new Set(path6.map(keyOf2));
1051
1147
  for (const value of shared) {
1052
1148
  if (!here.has(value)) {
1053
1149
  shared.delete(value);
@@ -1077,7 +1173,7 @@ var IdSet = class {
1077
1173
  has(node) {
1078
1174
  return this.byKey.has(node.id);
1079
1175
  }
1080
- /** The node this set was built with, which is the caller's own handle for it. */
1176
+ /** The wrapper that was added, which may be a different object from `node`. */
1081
1177
  get(node) {
1082
1178
  return this.byKey.get(node.id);
1083
1179
  }
@@ -1108,6 +1204,51 @@ var IdMap = class {
1108
1204
  }
1109
1205
  };
1110
1206
 
1207
+ // src/paths/nodeReads.ts
1208
+ var EnclosingNodes = class {
1209
+ constructor(types) {
1210
+ this.types = types;
1211
+ }
1212
+ types;
1213
+ byTree = /* @__PURE__ */ new WeakMap();
1214
+ /**
1215
+ * The nearest ancestor of one of the types, or null when there is none.
1216
+ * Every node passed on the way up has the same result, so each one is
1217
+ * kept, and a later read from a sibling stops one step up.
1218
+ */
1219
+ of(node) {
1220
+ const kept = this.keptFor(node.tree);
1221
+ const known = kept.get(node.id);
1222
+ if (known !== void 0) {
1223
+ return known;
1224
+ }
1225
+ const passed = [node.id];
1226
+ let current = node.parent;
1227
+ while (current !== null && !this.types.has(current.type)) {
1228
+ const above = kept.get(current.id);
1229
+ if (above !== void 0) {
1230
+ current = above;
1231
+ break;
1232
+ }
1233
+ passed.push(current.id);
1234
+ current = current.parent;
1235
+ }
1236
+ for (const id of passed) {
1237
+ kept.set(id, current);
1238
+ }
1239
+ return current;
1240
+ }
1241
+ keptFor(tree) {
1242
+ const known = this.byTree.get(tree);
1243
+ if (known !== void 0) {
1244
+ return known;
1245
+ }
1246
+ const fresh = /* @__PURE__ */ new Map();
1247
+ this.byTree.set(tree, fresh);
1248
+ return fresh;
1249
+ }
1250
+ };
1251
+
1111
1252
  // src/paths/nodeWalk.ts
1112
1253
  var SKIP_CHILDREN = /* @__PURE__ */ Symbol("skipChildren");
1113
1254
  function walkDescendants(root, carried, visitor) {
@@ -1124,23 +1265,23 @@ function walkDescendants(root, carried, visitor) {
1124
1265
  }
1125
1266
 
1126
1267
  // src/projectRoot.ts
1127
- var import_node_path = __toESM(require("path"), 1);
1268
+ var import_node_path3 = __toESM(require("path"), 1);
1128
1269
  function commonDirectoryOf(files) {
1129
- const absolute = files.filter((f) => import_node_path.default.isAbsolute(f));
1270
+ const absolute = files.filter((f) => import_node_path3.default.isAbsolute(f));
1130
1271
  const first = absolute[0];
1131
1272
  if (first === void 0) {
1132
1273
  return void 0;
1133
1274
  }
1134
- let shared = import_node_path.default.dirname(first).split(import_node_path.default.sep);
1275
+ let shared = import_node_path3.default.dirname(first).split(import_node_path3.default.sep);
1135
1276
  for (const file of absolute.slice(1)) {
1136
- const parts = import_node_path.default.dirname(file).split(import_node_path.default.sep);
1277
+ const parts = import_node_path3.default.dirname(file).split(import_node_path3.default.sep);
1137
1278
  let i = 0;
1138
1279
  while (i < shared.length && i < parts.length && shared[i] === parts[i]) {
1139
1280
  i += 1;
1140
1281
  }
1141
1282
  shared = shared.slice(0, i);
1142
1283
  }
1143
- const joined = shared.join(import_node_path.default.sep);
1284
+ const joined = shared.join(import_node_path3.default.sep);
1144
1285
  return joined.length > 1 ? joined : void 0;
1145
1286
  }
1146
1287
 
@@ -1197,11 +1338,79 @@ function valueToReadFurtherFrom(reading) {
1197
1338
  return reading.kind === "written" ? reading.value : null;
1198
1339
  }
1199
1340
 
1341
+ // src/testFiles.ts
1342
+ function isListedTestFile(file, listed) {
1343
+ if (listed === void 0) {
1344
+ return true;
1345
+ }
1346
+ const whole = withForwardSlashes(file);
1347
+ return listed.some((entry) => {
1348
+ const tail = withForwardSlashes(entry).replace(/^\.\//, "");
1349
+ return whole === tail || whole.endsWith(`/${tail}`);
1350
+ });
1351
+ }
1352
+ function matchesTestFileName(file, patterns) {
1353
+ const base = withForwardSlashes(file).split("/").pop() ?? "";
1354
+ return patterns.some((pattern) => globToRegExp(pattern).test(base));
1355
+ }
1356
+ function globToRegExp(pattern) {
1357
+ const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1358
+ return new RegExp(`^${escaped}$`);
1359
+ }
1360
+ function withForwardSlashes(file) {
1361
+ return file.replace(/\\/g, "/");
1362
+ }
1363
+
1364
+ // src/valueSources.ts
1365
+ var QUOTED_LENGTH = 80;
1366
+ function sourceRefsOf(leaves, spelling) {
1367
+ const refs = [];
1368
+ const said = /* @__PURE__ */ new Set();
1369
+ for (const leaf of leaves) {
1370
+ const ref = convertedFrom(refOf(leaf, spelling), leaf.conversions);
1371
+ const spelled = JSON.stringify(ref);
1372
+ if (!said.has(spelled)) {
1373
+ said.add(spelled);
1374
+ refs.push(ref);
1375
+ }
1376
+ }
1377
+ return refs;
1378
+ }
1379
+ function convertedFrom(ref, conversions) {
1380
+ return conversions.reduceRight(
1381
+ (from, method) => ({
1382
+ type: "derived",
1383
+ from,
1384
+ derivation: { type: "methodCall", method, args: [] }
1385
+ }),
1386
+ ref
1387
+ );
1388
+ }
1389
+ function refOf(leaf, spelling) {
1390
+ const whole = leaf.computedAt === null;
1391
+ const input = whole ? spelling.inputOf(leaf) : null;
1392
+ if (input !== null) {
1393
+ return input;
1394
+ }
1395
+ if (whole && leaf.end.is === "written" && leaf.path.length === 0) {
1396
+ const literal = spelling.literalAt(leaf.key);
1397
+ if (literal !== void 0) {
1398
+ return { type: "literal", value: literal };
1399
+ }
1400
+ }
1401
+ const at = leaf.computedAt ?? leaf.key;
1402
+ return { type: "unresolved", sourceText: quoted(spelling.textAt(at), at) };
1403
+ }
1404
+ function quoted(text, key) {
1405
+ const written = (text ?? key.slice(key.lastIndexOf("#") + 1)).replace(/\s+/g, " ").trim();
1406
+ return written.length <= QUOTED_LENGTH ? written : `${written.slice(0, QUOTED_LENGTH - 3)}...`;
1407
+ }
1408
+
1200
1409
  // src/adapterStamp.ts
1201
1410
  var import_node_crypto = require("crypto");
1202
- var import_node_fs = __toESM(require("fs"), 1);
1411
+ var import_node_fs2 = __toESM(require("fs"), 1);
1203
1412
  var import_node_module = require("module");
1204
- var import_node_path2 = __toESM(require("path"), 1);
1413
+ var import_node_path4 = __toESM(require("path"), 1);
1205
1414
  var import_node_url = require("url");
1206
1415
  var import_meta = {};
1207
1416
  var ANALYSIS_PACKAGES = [
@@ -1244,16 +1453,16 @@ function createAdapterStamp(config) {
1244
1453
  function readAdapterCodeStamp(moduleUrl) {
1245
1454
  try {
1246
1455
  const selfPath = (0, import_node_url.fileURLToPath)(moduleUrl);
1247
- const hash = computeDistHashFrom(import_node_path2.default.dirname(selfPath));
1456
+ const hash = computeDistHashFrom(import_node_path4.default.dirname(selfPath));
1248
1457
  return hash.length > 0 ? { kind: "bundle", hash } : SOURCE_STAMP;
1249
1458
  } catch {
1250
1459
  return SOURCE_STAMP;
1251
1460
  }
1252
1461
  }
1253
1462
  function computeDistHashFrom(dir) {
1254
- const candidates = [import_node_path2.default.join(dir, "index.js"), import_node_path2.default.join(dir, "index.cjs")];
1463
+ const candidates = [import_node_path4.default.join(dir, "index.js"), import_node_path4.default.join(dir, "index.cjs")];
1255
1464
  for (const candidate of candidates) {
1256
- if (!import_node_fs.default.existsSync(candidate)) {
1465
+ if (!import_node_fs2.default.existsSync(candidate)) {
1257
1466
  continue;
1258
1467
  }
1259
1468
  return computeContentHash([candidate, ...analysisBundles()]);
@@ -1267,7 +1476,7 @@ function computeContentHash(paths) {
1267
1476
  const hash = (0, import_node_crypto.createHash)("sha256");
1268
1477
  for (const file of paths) {
1269
1478
  try {
1270
- hash.update(import_node_fs.default.readFileSync(file));
1479
+ hash.update(import_node_fs2.default.readFileSync(file));
1271
1480
  } catch {
1272
1481
  return "";
1273
1482
  }
@@ -1283,13 +1492,29 @@ function projectFileStamp(paths) {
1283
1492
  hash.update(file);
1284
1493
  hash.update("\0");
1285
1494
  try {
1286
- hash.update(import_node_fs.default.readFileSync(file));
1495
+ hash.update(import_node_fs2.default.readFileSync(file));
1287
1496
  } catch {
1288
1497
  hash.update("absent");
1289
1498
  }
1290
1499
  }
1291
1500
  return hash.digest("hex").slice(0, 16);
1292
1501
  }
1502
+ function extractionConfigStamp(config) {
1503
+ const parts = [
1504
+ ["gapHandling", config.gapHandling ?? "permissive"],
1505
+ ["includeReachable", optionalText(config.includeReachable)],
1506
+ ["workspaceRoot", config.workspaceRoot],
1507
+ ["projectRoot", config.projectRoot],
1508
+ ["importRoots", config.importRoots?.join(import_node_path4.default.delimiter)],
1509
+ ["modules", modulesStamp(config.modules)]
1510
+ ];
1511
+ return parts.flatMap(
1512
+ ([name, value]) => value === void 0 ? [] : [`${name}=${value}`]
1513
+ ).join(",");
1514
+ }
1515
+ function optionalText(value) {
1516
+ return value === void 0 ? void 0 : String(value);
1517
+ }
1293
1518
  function runDigest(packsDigest, packs, files) {
1294
1519
  const inputs = packs.flatMap((pack) => pack.discoveryInputs?.(files) ?? []);
1295
1520
  return `${packsDigest}|reads:${projectFileStamp(inputs)}`;
@@ -1307,10 +1532,118 @@ function analysisBundles() {
1307
1532
  }
1308
1533
 
1309
1534
  // src/cache.ts
1535
+ var import_node_crypto3 = require("crypto");
1536
+ var import_promises2 = __toESM(require("fs/promises"), 1);
1537
+ var import_node_path5 = __toESM(require("path"), 1);
1538
+
1539
+ // src/cacheStamps.ts
1310
1540
  var import_node_crypto2 = require("crypto");
1311
1541
  var import_promises = __toESM(require("fs/promises"), 1);
1312
- var import_node_path3 = __toESM(require("path"), 1);
1313
- var SCHEMA_VERSION = "7";
1542
+ var RECENT_CHANGE_MS = 2e3;
1543
+ function stampMayMissAWrite(stat, statAt) {
1544
+ return Math.max(stat.mtimeMs, stat.ctimeMs) > statAt - RECENT_CHANGE_MS;
1545
+ }
1546
+ async function startRun(files, configPath) {
1547
+ const startedAt = Date.now();
1548
+ const config = configPath === void 0 ? null : await statAtStart(configPath);
1549
+ const stats = await Promise.all(files.map(statAtStart));
1550
+ stats.sort(
1551
+ (a, b) => a.stamp.path < b.stamp.path ? -1 : a.stamp.path > b.stamp.path ? 1 : 0
1552
+ );
1553
+ return {
1554
+ startedAt,
1555
+ config: config?.ctimeMs === null ? null : config,
1556
+ files: stats,
1557
+ hashes: /* @__PURE__ */ new Map()
1558
+ };
1559
+ }
1560
+ async function statAtStart(filePath) {
1561
+ try {
1562
+ const stat = await import_promises.default.stat(filePath);
1563
+ return {
1564
+ stamp: { path: filePath, mtimeMs: stat.mtimeMs, size: stat.size },
1565
+ ctimeMs: stat.ctimeMs
1566
+ };
1567
+ } catch {
1568
+ return { stamp: { path: filePath, mtimeMs: -1, size: -1 }, ctimeMs: null };
1569
+ }
1570
+ }
1571
+ async function hashOf(run, filePath) {
1572
+ const known = run.hashes.get(filePath);
1573
+ if (known !== void 0) {
1574
+ return known;
1575
+ }
1576
+ const hash = await hashFile(filePath);
1577
+ run.hashes.set(filePath, hash);
1578
+ return hash;
1579
+ }
1580
+ async function hashRecentChanges(run) {
1581
+ const recent = [...run.config === null ? [] : [run.config], ...run.files].filter(
1582
+ (file) => file.ctimeMs !== null && stampMayMissAWrite(
1583
+ { mtimeMs: file.stamp.mtimeMs, ctimeMs: file.ctimeMs },
1584
+ run.startedAt
1585
+ )
1586
+ ).map((file) => file.stamp.path);
1587
+ await Promise.all(recent.map((filePath) => hashOf(run, filePath)));
1588
+ }
1589
+ async function stillMatches(stored, found, run) {
1590
+ if (!fileStampEquals(stored, found)) {
1591
+ return false;
1592
+ }
1593
+ if (stored === null || stored.verifyHash !== true) {
1594
+ return true;
1595
+ }
1596
+ return await hashOf(run, stored.path) === stored.contentHash;
1597
+ }
1598
+ async function filesStillMatch(stored, run) {
1599
+ if (stored.length !== run.files.length || !stored.every((s, i) => fileStampEquals(s, run.files[i]?.stamp ?? null))) {
1600
+ return false;
1601
+ }
1602
+ const checks = await Promise.all(
1603
+ stored.map((s, i) => stillMatches(s, run.files[i]?.stamp ?? null, run))
1604
+ );
1605
+ return checks.every(Boolean);
1606
+ }
1607
+ async function recordedStamp(start, run, prior) {
1608
+ if (start.ctimeMs === null) {
1609
+ return start.stamp;
1610
+ }
1611
+ const filePath = start.stamp.path;
1612
+ const now = await import_promises.default.stat(filePath).catch(() => null);
1613
+ if (now === null || now.mtimeMs !== start.stamp.mtimeMs || now.size !== start.stamp.size || now.ctimeMs !== start.ctimeMs) {
1614
+ return null;
1615
+ }
1616
+ const atStart = { mtimeMs: start.stamp.mtimeMs, ctimeMs: start.ctimeMs };
1617
+ if (stampMayMissAWrite(atStart, run.startedAt)) {
1618
+ const readBefore = run.hashes.get(filePath);
1619
+ return readBefore === void 0 || readBefore === null ? null : { ...start.stamp, contentHash: readBefore, verifyHash: true };
1620
+ }
1621
+ const hash = run.hashes.get(filePath) ?? reusableHash(prior, start.stamp) ?? await hashFile(filePath);
1622
+ return hash === null ? start.stamp : { ...start.stamp, contentHash: hash };
1623
+ }
1624
+ function reusableHash(prior, stamp) {
1625
+ return prior !== void 0 && prior.verifyHash !== true && fileStampEquals(prior, stamp) ? prior.contentHash : void 0;
1626
+ }
1627
+ async function hashFile(filePath) {
1628
+ try {
1629
+ const content = await import_promises.default.readFile(filePath);
1630
+ return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").slice(0, 16);
1631
+ } catch {
1632
+ return null;
1633
+ }
1634
+ }
1635
+ function fileStampEquals(a, b) {
1636
+ if (a === null && b === null) {
1637
+ return true;
1638
+ }
1639
+ if (a === null || b === null) {
1640
+ return false;
1641
+ }
1642
+ return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
1643
+ }
1644
+
1645
+ // src/cache.ts
1646
+ var SCHEMA_VERSION = "9";
1314
1647
  var MAX_ENTRIES = 2;
1315
1648
  var ENTRY_PREFIX = "key-";
1316
1649
  var ENTRY_DIR_NAME = new RegExp(`^${ENTRY_PREFIX}[0-9a-f]{16}$`);
@@ -1327,56 +1660,88 @@ function createCacheLayer(cacheDir) {
1327
1660
  }
1328
1661
  };
1329
1662
  }
1663
+ const manifests = new ManifestReads();
1664
+ const runs = /* @__PURE__ */ new WeakMap();
1665
+ const runFor = async (input) => runs.get(input) ?? await startRun(input.files, input.configPath);
1666
+ const lookupWith = async (input, run) => {
1667
+ const entryDir = entryDirFor(cacheDir, input);
1668
+ const manifest = await manifests.readAtRunStart(
1669
+ import_node_path5.default.join(entryDir, "manifest.json")
1670
+ );
1671
+ if (manifest === null) {
1672
+ return missDiag(await describeAbsentEntry(cacheDir));
1673
+ }
1674
+ if (!await stillMatches(
1675
+ manifest.configStamp,
1676
+ run.config?.stamp ?? null,
1677
+ run
1678
+ )) {
1679
+ return missDiag("config-changed");
1680
+ }
1681
+ if (!await filesStillMatch(manifest.files, run)) {
1682
+ return missDiag("files-changed");
1683
+ }
1684
+ await markUsed(entryDir);
1685
+ manifests.forget();
1686
+ return {
1687
+ kind: "hit",
1688
+ summaries: manifest.summaries,
1689
+ diagnostic: { kind: "hit" }
1690
+ };
1691
+ };
1330
1692
  return {
1331
1693
  async tryHit(input) {
1332
1694
  const result = await this.lookup(input);
1333
1695
  return result.kind === "hit" ? result.summaries : null;
1334
1696
  },
1335
1697
  async lookup(input) {
1336
- const entryDir = entryDirFor(cacheDir, input);
1337
- const manifest = await readManifest(
1338
- import_node_path3.default.join(entryDir, "manifest.json")
1339
- );
1340
- if (manifest === null) {
1341
- return missDiag(await describeAbsentEntry(cacheDir));
1698
+ const run = await startRun(input.files, input.configPath);
1699
+ runs.set(input, run);
1700
+ const result = await lookupWith(input, run);
1701
+ if (result.kind === "miss") {
1702
+ await hashRecentChanges(run);
1342
1703
  }
1343
- const currentConfigStamp = await stampConfigFile(input.configPath);
1344
- if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
1345
- return missDiag("config-changed");
1346
- }
1347
- const currentFiles = await resolveFileStamps(input);
1348
- if (!fileStampsEqual(manifest.files, currentFiles)) {
1349
- return missDiag("files-changed");
1350
- }
1351
- await markUsed(entryDir);
1352
- return {
1353
- kind: "hit",
1354
- summaries: manifest.summaries,
1355
- diagnostic: { kind: "hit" }
1356
- };
1704
+ return result;
1357
1705
  },
1358
1706
  async plan(input) {
1359
1707
  const entryDir = entryDirFor(cacheDir, input);
1360
- const manifest = await readManifest(
1361
- import_node_path3.default.join(entryDir, "manifest.json")
1708
+ const manifest = await manifests.readAgain(
1709
+ import_node_path5.default.join(entryDir, "manifest.json")
1362
1710
  );
1363
1711
  if (manifest === null || manifest.roots === void 0 || manifest.rootMeta === void 0 || manifest.owners === void 0) {
1364
1712
  return null;
1365
1713
  }
1366
- const currentConfigStamp = await stampConfigFile(input.configPath);
1367
- if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
1714
+ const run = await runFor(input);
1715
+ if (!await stillMatches(
1716
+ manifest.configStamp,
1717
+ run.config?.stamp ?? null,
1718
+ run
1719
+ )) {
1368
1720
  return null;
1369
1721
  }
1370
1722
  await markUsed(entryDir);
1371
- return buildPlan(manifest, await resolveFileStamps(input));
1723
+ return buildPlan(manifest, run);
1372
1724
  },
1373
1725
  async write(input, summaries, attribution) {
1374
1726
  const entryDir = entryDirFor(cacheDir, input);
1375
- const previous = await readManifest(
1376
- import_node_path3.default.join(entryDir, "manifest.json")
1727
+ const previous = await manifests.readAgain(
1728
+ import_node_path5.default.join(entryDir, "manifest.json")
1377
1729
  );
1378
- const configStamp = await stampConfigFile(input.configPath);
1379
- const files = await hashStamps(await resolveFileStamps(input), previous);
1730
+ manifests.forget();
1731
+ const known = runs.get(input);
1732
+ const run = known ?? await runFor(input);
1733
+ if (known === void 0) {
1734
+ await hashRecentChanges(run);
1735
+ }
1736
+ runs.delete(input);
1737
+ const configStamp = run.config === null ? null : await recordedStamp(run.config, run, void 0);
1738
+ const prior = new Map((previous?.files ?? []).map((f) => [f.path, f]));
1739
+ const recorded = await Promise.all(
1740
+ run.files.map(
1741
+ (file) => recordedStamp(file, run, prior.get(file.stamp.path))
1742
+ )
1743
+ );
1744
+ const files = recorded.filter((stamp) => stamp !== null);
1380
1745
  const manifest = {
1381
1746
  schemaVersion: SCHEMA_VERSION,
1382
1747
  adapterPacksDigest: input.adapterPacksDigest,
@@ -1385,19 +1750,20 @@ function createCacheLayer(cacheDir) {
1385
1750
  summaries,
1386
1751
  ...attribution === void 0 ? {} : encodeAttribution(attribution)
1387
1752
  };
1388
- await import_promises.default.mkdir(entryDir, { recursive: true });
1389
- await import_promises.default.writeFile(
1390
- import_node_path3.default.join(entryDir, "manifest.json"),
1753
+ await import_promises2.default.mkdir(entryDir, { recursive: true });
1754
+ await import_promises2.default.writeFile(
1755
+ import_node_path5.default.join(entryDir, "manifest.json"),
1391
1756
  JSON.stringify(manifest)
1392
1757
  );
1393
1758
  await evictOldEntries(cacheDir, entryDir);
1394
1759
  }
1395
1760
  };
1396
1761
  }
1397
- async function buildPlan(manifest, currentStamps) {
1762
+ async function buildPlan(manifest, run) {
1398
1763
  const stored = new Map(manifest.files.map((f) => [f.path, f]));
1399
- const current = new Map(currentStamps.map((f) => [f.path, f]));
1764
+ const current = new Map(run.files.map((f) => [f.stamp.path, f.stamp]));
1400
1765
  const changed = /* @__PURE__ */ new Set();
1766
+ const added = /* @__PURE__ */ new Set();
1401
1767
  const removed = /* @__PURE__ */ new Set();
1402
1768
  for (const p of stored.keys()) {
1403
1769
  if (!current.has(p)) {
@@ -1409,17 +1775,20 @@ async function buildPlan(manifest, currentStamps) {
1409
1775
  const before = stored.get(p);
1410
1776
  if (before === void 0) {
1411
1777
  changed.add(p);
1778
+ added.add(p);
1412
1779
  } else if (!fileStampEquals(before, stamp)) {
1413
1780
  if (before.contentHash === void 0) {
1414
1781
  changed.add(p);
1415
1782
  } else {
1416
1783
  toVerify.push(p);
1417
1784
  }
1785
+ } else if (before.verifyHash === true) {
1786
+ toVerify.push(p);
1418
1787
  }
1419
1788
  }
1420
1789
  await Promise.all(
1421
1790
  toVerify.map(async (p) => {
1422
- const hash = await hashFile(p);
1791
+ const hash = await hashOf(run, p);
1423
1792
  if (hash === null || hash !== stored.get(p)?.contentHash) {
1424
1793
  changed.add(p);
1425
1794
  }
@@ -1428,6 +1797,10 @@ async function buildPlan(manifest, currentStamps) {
1428
1797
  const roots = /* @__PURE__ */ new Map();
1429
1798
  const rootNames = manifest.roots ?? [];
1430
1799
  const depPaths = manifest.depPaths ?? [];
1800
+ const pathsOf = (ids) => ids.flatMap((d) => {
1801
+ const p = depPaths[d];
1802
+ return p === void 0 ? [] : [p];
1803
+ });
1431
1804
  let rootsDeclined = 0;
1432
1805
  rootNames.forEach((rootPath, i) => {
1433
1806
  const meta = manifest.rootMeta?.[i];
@@ -1440,35 +1813,54 @@ async function buildPlan(manifest, currentStamps) {
1440
1813
  roots.set(rootPath, {
1441
1814
  path: rootPath,
1442
1815
  cacheable: meta.cacheable,
1443
- deps: meta.deps.flatMap((d) => {
1444
- const p = depPaths[d];
1445
- return p === void 0 ? [] : [p];
1446
- }),
1816
+ deps: pathsOf(meta.deps),
1447
1817
  claims: meta.claims,
1448
1818
  meta: meta.meta,
1449
- packs: meta.packs
1819
+ packs: meta.packs,
1820
+ ...meta.imports === void 0 ? {} : { imports: pathsOf(meta.imports) }
1450
1821
  });
1451
1822
  });
1823
+ const unchanged = (filePath, deps) => current.has(filePath) && !changed.has(filePath) && !deps.some((d) => changed.has(d) || removed.has(d));
1452
1824
  const validRoots = /* @__PURE__ */ new Set();
1453
1825
  for (const [rootPath, record] of roots) {
1454
- if (!record.cacheable || !current.has(rootPath)) {
1455
- continue;
1826
+ if (record.cacheable && unchanged(rootPath, record.deps)) {
1827
+ validRoots.add(rootPath);
1456
1828
  }
1457
- if (changed.has(rootPath)) {
1458
- continue;
1829
+ }
1830
+ const units = (manifest.units ?? []).flatMap((stored2) => {
1831
+ const file = depPaths[stored2.file];
1832
+ return file === void 0 ? [] : [
1833
+ {
1834
+ key: stored2.key,
1835
+ file,
1836
+ deps: pathsOf(stored2.deps),
1837
+ data: stored2.data
1838
+ }
1839
+ ];
1840
+ });
1841
+ const validUnits = /* @__PURE__ */ new Map();
1842
+ for (const unit of units) {
1843
+ if (unchanged(unit.file, unit.deps)) {
1844
+ validUnits.set(unit.key, unit);
1459
1845
  }
1460
- const depMoved = record.deps.some((d) => changed.has(d) || removed.has(d));
1461
- if (!depMoved) {
1462
- validRoots.add(rootPath);
1846
+ }
1847
+ const resolvedImports = /* @__PURE__ */ new Map();
1848
+ if (added.size === 0 && removed.size === 0) {
1849
+ for (const [rootPath, record] of roots) {
1850
+ if (record.imports !== void 0 && !changed.has(rootPath)) {
1851
+ resolvedImports.set(rootPath, record.imports);
1852
+ }
1463
1853
  }
1464
1854
  }
1465
1855
  const owners = manifest.owners ?? [];
1466
1856
  return {
1467
1857
  changed,
1468
1858
  removed,
1859
+ resolvedImports,
1469
1860
  roots,
1470
1861
  validRoots,
1471
1862
  rootsDeclined,
1863
+ validUnits,
1472
1864
  reuse(valid) {
1473
1865
  const summaries = [];
1474
1866
  const reusedOwners = [];
@@ -1495,7 +1887,8 @@ async function buildPlan(manifest, currentStamps) {
1495
1887
  const p = rootNames[o];
1496
1888
  return p === void 0 ? [] : [p];
1497
1889
  })
1498
- )
1890
+ ),
1891
+ units
1499
1892
  };
1500
1893
  }
1501
1894
  };
@@ -1520,7 +1913,8 @@ function encodeAttribution(attribution) {
1520
1913
  deps: r.deps.map(depIdOf),
1521
1914
  claims: r.claims,
1522
1915
  meta: r.meta,
1523
- packs: r.packs
1916
+ packs: r.packs,
1917
+ ...r.imports === void 0 ? {} : { imports: r.imports.map(depIdOf) }
1524
1918
  }));
1525
1919
  const owners = attribution.owners.map(
1526
1920
  (ownerPaths) => ownerPaths.flatMap((p) => {
@@ -1528,32 +1922,23 @@ function encodeAttribution(attribution) {
1528
1922
  return i === void 0 ? [] : [i];
1529
1923
  })
1530
1924
  );
1531
- return { roots, rootMeta, depPaths, owners };
1532
- }
1533
- async function hashStamps(stamps, previous) {
1534
- const before = new Map((previous?.files ?? []).map((f) => [f.path, f]));
1535
- return Promise.all(
1536
- stamps.map(async (stamp) => {
1537
- const prior = before.get(stamp.path);
1538
- if (prior !== void 0 && prior.contentHash !== void 0 && fileStampEquals(prior, stamp)) {
1539
- return { ...stamp, contentHash: prior.contentHash };
1540
- }
1541
- const contentHash = await hashFile(stamp.path);
1542
- return contentHash === null ? stamp : { ...stamp, contentHash };
1543
- })
1544
- );
1545
- }
1546
- async function hashFile(filePath) {
1547
- try {
1548
- const content = await import_promises.default.readFile(filePath);
1549
- return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").slice(0, 16);
1550
- } catch {
1551
- return null;
1552
- }
1925
+ const units = attribution.units?.map((unit) => ({
1926
+ key: unit.key,
1927
+ file: depIdOf(unit.file),
1928
+ deps: unit.deps.map(depIdOf),
1929
+ data: unit.data
1930
+ }));
1931
+ return {
1932
+ roots,
1933
+ rootMeta,
1934
+ depPaths,
1935
+ owners,
1936
+ ...units === void 0 ? {} : { units }
1937
+ };
1553
1938
  }
1554
1939
  async function describeAbsentEntry(cacheDir) {
1555
1940
  try {
1556
- const entries = await import_promises.default.readdir(cacheDir, { withFileTypes: true });
1941
+ const entries = await import_promises2.default.readdir(cacheDir, { withFileTypes: true });
1557
1942
  return entries.some((e) => e.isDirectory() && isEntryDir(e.name)) ? "key-changed" : "no-manifest";
1558
1943
  } catch {
1559
1944
  return "no-manifest";
@@ -1565,8 +1950,8 @@ function entryDirFor(cacheDir, input) {
1565
1950
  input.adapterPacksDigest,
1566
1951
  input.configPath ?? ""
1567
1952
  ].join(" ");
1568
- const name = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex").slice(0, 16);
1569
- return import_node_path3.default.join(cacheDir, `${ENTRY_PREFIX}${name}`);
1953
+ const name = (0, import_node_crypto3.createHash)("sha256").update(key).digest("hex").slice(0, 16);
1954
+ return import_node_path5.default.join(cacheDir, `${ENTRY_PREFIX}${name}`);
1570
1955
  }
1571
1956
  function isEntryDir(name) {
1572
1957
  return ENTRY_DIR_NAME.test(name);
@@ -1574,24 +1959,24 @@ function isEntryDir(name) {
1574
1959
  async function markUsed(entryDir) {
1575
1960
  const now = /* @__PURE__ */ new Date();
1576
1961
  try {
1577
- await import_promises.default.utimes(entryDir, now, now);
1962
+ await import_promises2.default.utimes(entryDir, now, now);
1578
1963
  } catch {
1579
1964
  }
1580
1965
  }
1581
1966
  async function evictOldEntries(cacheDir, keepDir) {
1582
1967
  try {
1583
- await import_promises.default.rm(import_node_path3.default.join(cacheDir, "manifest.json"), { force: true });
1584
- const entries = await import_promises.default.readdir(cacheDir, { withFileTypes: true });
1968
+ await import_promises2.default.rm(import_node_path5.default.join(cacheDir, "manifest.json"), { force: true });
1969
+ const entries = await import_promises2.default.readdir(cacheDir, { withFileTypes: true });
1585
1970
  const dirs = await Promise.all(
1586
1971
  entries.filter((e) => e.isDirectory() && isEntryDir(e.name)).map(async (e) => {
1587
- const dir = import_node_path3.default.join(cacheDir, e.name);
1588
- const stat = await import_promises.default.stat(dir);
1972
+ const dir = import_node_path5.default.join(cacheDir, e.name);
1973
+ const stat = await import_promises2.default.stat(dir);
1589
1974
  return { dir, mtimeMs: stat.mtimeMs };
1590
1975
  })
1591
1976
  );
1592
1977
  const doomed = dirs.filter((d) => d.dir !== keepDir).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_ENTRIES - 1);
1593
1978
  await Promise.all(
1594
- doomed.map((d) => import_promises.default.rm(d.dir, { recursive: true, force: true }))
1979
+ doomed.map((d) => import_promises2.default.rm(d.dir, { recursive: true, force: true }))
1595
1980
  );
1596
1981
  } catch {
1597
1982
  }
@@ -1602,54 +1987,44 @@ function missDiag(reason) {
1602
1987
  diagnostic: { kind: "miss", missReason: reason }
1603
1988
  };
1604
1989
  }
1605
- async function readManifest(manifestPath) {
1990
+ var ManifestReads = class {
1991
+ last = null;
1992
+ async readAtRunStart(manifestPath) {
1993
+ const stamp = await manifestStamp(manifestPath);
1994
+ const manifest = await readManifest(manifestPath);
1995
+ this.last = { path: manifestPath, stamp, manifest };
1996
+ return manifest;
1997
+ }
1998
+ async readAgain(manifestPath) {
1999
+ const last = this.last;
2000
+ if (last !== null && last.path === manifestPath && last.stamp !== null && last.stamp === await manifestStamp(manifestPath)) {
2001
+ return last.manifest;
2002
+ }
2003
+ return await this.readAtRunStart(manifestPath);
2004
+ }
2005
+ forget() {
2006
+ this.last = null;
2007
+ }
2008
+ };
2009
+ async function manifestStamp(manifestPath) {
1606
2010
  try {
1607
- const raw = await import_promises.default.readFile(manifestPath, "utf-8");
1608
- return JSON.parse(raw);
2011
+ const stat = await import_promises2.default.stat(manifestPath);
2012
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
1609
2013
  } catch {
1610
2014
  return null;
1611
2015
  }
1612
2016
  }
1613
- async function stampConfigFile(configPath) {
1614
- if (configPath === void 0) {
1615
- return null;
1616
- }
2017
+ async function readManifest(manifestPath) {
1617
2018
  try {
1618
- const stat = await import_promises.default.stat(configPath);
1619
- return { path: configPath, mtimeMs: stat.mtimeMs, size: stat.size };
2019
+ const raw = await import_promises2.default.readFile(manifestPath, "utf-8");
2020
+ return JSON.parse(raw);
1620
2021
  } catch {
1621
2022
  return null;
1622
2023
  }
1623
2024
  }
1624
- async function resolveFileStamps(input) {
1625
- const stamped = await Promise.all(
1626
- input.files.map(async (p) => {
1627
- try {
1628
- const stat = await import_promises.default.stat(p);
1629
- return { path: p, mtimeMs: stat.mtimeMs, size: stat.size };
1630
- } catch {
1631
- return { path: p, mtimeMs: -1, size: -1 };
1632
- }
1633
- })
1634
- );
1635
- stamped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1636
- return stamped;
1637
- }
1638
- function fileStampsEqual(a, b) {
1639
- return a.length === b.length && a.every((s, i) => fileStampEquals(s, b[i]));
1640
- }
1641
- function fileStampEquals(a, b) {
1642
- if (a === null && b === null) {
1643
- return true;
1644
- }
1645
- if (a === null || b === null) {
1646
- return false;
1647
- }
1648
- return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
1649
- }
1650
2025
 
1651
2026
  // src/extractionReport.ts
1652
- var import_behavioral_ir4 = require("@suss/behavioral-ir");
2027
+ var import_behavioral_ir5 = require("@suss/behavioral-ir");
1653
2028
  var emptyTally = () => ({
1654
2029
  candidateFiles: 0,
1655
2030
  unitsDiscovered: 0,
@@ -1699,7 +2074,7 @@ function summaryCountsByPack(summaries) {
1699
2074
  }
1700
2075
  const entry = counts.get(recognition) ?? emptyCounts();
1701
2076
  entry.bound += 1;
1702
- if (import_behavioral_ir4.BOUNDARY_ROLE[summary.kind] === "provider") {
2077
+ if (import_behavioral_ir5.BOUNDARY_ROLE[summary.kind] === "provider") {
1703
2078
  entry.providers += 1;
1704
2079
  }
1705
2080
  if (summary.transitions.length > 0) {
@@ -1755,6 +2130,38 @@ function firstEmptyUngatedStage(packs, filesWalked) {
1755
2130
  return "assembly";
1756
2131
  }
1757
2132
 
2133
+ // src/keptParses.ts
2134
+ var KeptParses = class {
2135
+ constructor(release = () => {
2136
+ }) {
2137
+ this.release = release;
2138
+ }
2139
+ release;
2140
+ byFile = /* @__PURE__ */ new Map();
2141
+ async parse(file, source, make) {
2142
+ const kept = this.byFile.get(file);
2143
+ if (kept !== void 0 && kept.source === source) {
2144
+ return kept.value;
2145
+ }
2146
+ const value = await make(source);
2147
+ if (kept !== void 0) {
2148
+ this.release(kept.value);
2149
+ }
2150
+ this.byFile.set(file, { source, value });
2151
+ return value;
2152
+ }
2153
+ /** Lets go of every file not in this run. */
2154
+ keepOnly(files) {
2155
+ const wanted = new Set(files);
2156
+ for (const [file, kept] of this.byFile) {
2157
+ if (!wanted.has(file)) {
2158
+ this.release(kept.value);
2159
+ this.byFile.delete(file);
2160
+ }
2161
+ }
2162
+ }
2163
+ };
2164
+
1758
2165
  // src/packHealth.ts
1759
2166
  function stagesOf(funnel) {
1760
2167
  const stages = [];
@@ -1801,8 +2208,8 @@ function funnelDrops(packs) {
1801
2208
  function recognizersWithNoUnits(packs) {
1802
2209
  return packs.filter(
1803
2210
  (funnel) => funnel.recognizes && !funnel.discovers && funnel.gates.length > 0 && funnel.unresolvedGates.length === 0 && funnel.candidateFiles > 0 && funnel.unitsInGatedFiles === 0 && // The closure walks a recognizer-only pack's gated exports as
1804
- // roots, so effects recognized there mean the run worked and
1805
- // the missing framework pack costs attribution, not existence.
2211
+ // roots, so effects recognized there mean the run worked and only
2212
+ // the framework pack's attribution is missing.
1806
2213
  funnel.effectsRecognized === 0
1807
2214
  ).map((funnel) => ({
1808
2215
  label: funnel.pack,
@@ -2044,7 +2451,7 @@ function makeTransitionId(functionName, branch) {
2044
2451
  const { terminal } = branch;
2045
2452
  const statusKey = terminal.statusCode === null ? "none" : terminal.statusCode.type === "literal" ? String(terminal.statusCode.value) : `dyn:${terminal.statusCode.sourceText}`;
2046
2453
  const conditionSig = branch.conditions.map((c) => `${c.polarity}:${c.sourceText}`).join(";");
2047
- const conditionHash = (0, import_node_crypto3.createHash)("sha1").update(conditionSig).digest("hex").slice(0, 7);
2454
+ const conditionHash = (0, import_node_crypto4.createHash)("sha1").update(conditionSig).digest("hex").slice(0, 7);
2048
2455
  return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
2049
2456
  }
2050
2457
  function collapseReading(table, reading) {
@@ -2112,7 +2519,40 @@ function effectsOfBranch(branch) {
2112
2519
  ...branch.effects.map(effectToIR),
2113
2520
  ...branch.extraEffects ?? []
2114
2521
  ];
2115
- return (0, import_behavioral_ir5.foldRepeatedEffects)(effects.map(withNormalizedCallee));
2522
+ return (0, import_behavioral_ir6.foldRepeatedEffects)(effects.map(withNormalizedCallee));
2523
+ }
2524
+ function provenanceOf(raw, effects) {
2525
+ if (raw.length === 0) {
2526
+ return [];
2527
+ }
2528
+ const positionOf = new Map(effects.map((one, at) => [(0, import_behavioral_ir6.effectKey)(one), at]));
2529
+ const bySlot = /* @__PURE__ */ new Map();
2530
+ const said = /* @__PURE__ */ new Map();
2531
+ for (const one of raw) {
2532
+ const effect = positionOf.get((0, import_behavioral_ir6.effectKey)(withNormalizedCallee(one.effect)));
2533
+ if (effect === void 0) {
2534
+ continue;
2535
+ }
2536
+ const slotKey = `${effect}\0${one.slot}\0${one.name}`;
2537
+ let entry = bySlot.get(slotKey);
2538
+ let seen = said.get(slotKey);
2539
+ if (entry === void 0 || seen === void 0) {
2540
+ entry = { at: { slot: one.slot, effect, name: one.name }, from: [] };
2541
+ seen = /* @__PURE__ */ new Set();
2542
+ bySlot.set(slotKey, entry);
2543
+ said.set(slotKey, seen);
2544
+ }
2545
+ for (const source of one.from) {
2546
+ const spelled = JSON.stringify(source);
2547
+ if (!seen.has(spelled)) {
2548
+ seen.add(spelled);
2549
+ entry.from.push(source);
2550
+ }
2551
+ }
2552
+ }
2553
+ return [...bySlot.values()].sort(
2554
+ (a, b) => a.at.effect - b.at.effect || a.at.slot.localeCompare(b.at.slot) || a.at.name.localeCompare(b.at.name)
2555
+ );
2116
2556
  }
2117
2557
  function withNormalizedCallee(effect) {
2118
2558
  if (effect.type !== "invocation" && effect.type !== "interaction") {
@@ -2121,7 +2561,7 @@ function withNormalizedCallee(effect) {
2121
2561
  if (effect.callee === void 0) {
2122
2562
  return effect;
2123
2563
  }
2124
- return { ...effect, callee: (0, import_behavioral_ir5.normalizeCalleeText)(effect.callee) };
2564
+ return { ...effect, callee: (0, import_behavioral_ir6.normalizeCalleeText)(effect.callee) };
2125
2565
  }
2126
2566
  function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2127
2567
  const transitions = raw.branches.map((rawBranch) => {
@@ -2129,17 +2569,22 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2129
2569
  const conditions = branch.conditions.map(
2130
2570
  rawConditionToPredicate
2131
2571
  );
2572
+ const effects = effectsOfBranch(branch);
2132
2573
  const transition = {
2133
2574
  id: makeTransitionId(raw.identity.name, branch),
2134
2575
  conditions,
2135
2576
  output: terminalToOutput(branch.terminal),
2136
- effects: effectsOfBranch(branch),
2577
+ effects,
2137
2578
  location: branch.location,
2138
2579
  isDefault: branch.isDefault
2139
2580
  };
2140
2581
  if (branch.expectedInput != null) {
2141
2582
  transition.expectedInput = branch.expectedInput;
2142
2583
  }
2584
+ const provenance = provenanceOf(branch.provenance ?? [], effects);
2585
+ if (provenance.length > 0) {
2586
+ transition.provenance = provenance;
2587
+ }
2143
2588
  return transition;
2144
2589
  });
2145
2590
  const gaps = detectGaps(raw, transitions, options);
@@ -2181,25 +2626,28 @@ function buildMetadata(raw) {
2181
2626
  let metadata = {};
2182
2627
  const http = buildHttpMetadataValue(raw);
2183
2628
  if (http !== null) {
2184
- metadata = (0, import_behavioral_ir5.withHttpMetadata)(metadata, http);
2629
+ metadata = (0, import_behavioral_ir6.withHttpMetadata)(metadata, http);
2185
2630
  }
2186
2631
  const graphql = buildGraphqlMetadataValue(raw);
2187
2632
  if (graphql !== null) {
2188
- metadata = (0, import_behavioral_ir5.withGraphqlMetadata)(metadata, graphql);
2633
+ metadata = (0, import_behavioral_ir6.withGraphqlMetadata)(metadata, graphql);
2189
2634
  }
2190
2635
  if (raw.sourceDocumentLabel !== void 0) {
2191
- metadata = (0, import_behavioral_ir5.withSourceDocumentMetadata)(metadata, {
2636
+ metadata = (0, import_behavioral_ir6.withSourceDocumentMetadata)(metadata, {
2192
2637
  label: raw.sourceDocumentLabel
2193
2638
  });
2194
2639
  }
2195
2640
  if (raw.mount !== void 0) {
2196
- metadata = (0, import_behavioral_ir5.withMountMetadata)(metadata, raw.mount);
2641
+ metadata = (0, import_behavioral_ir6.withMountMetadata)(metadata, raw.mount);
2197
2642
  }
2198
2643
  if (raw.wrappers !== void 0 && raw.wrappers.length > 0) {
2199
- metadata = (0, import_behavioral_ir5.withWrapperMetadata)(metadata, { applied: raw.wrappers });
2644
+ metadata = (0, import_behavioral_ir6.withWrapperMetadata)(metadata, { applied: raw.wrappers });
2200
2645
  }
2201
2646
  if (raw.requestSpelling !== void 0) {
2202
- metadata = (0, import_behavioral_ir5.withRequestSpellingMetadata)(metadata, raw.requestSpelling);
2647
+ metadata = (0, import_behavioral_ir6.withRequestSpellingMetadata)(metadata, raw.requestSpelling);
2648
+ }
2649
+ if (raw.test !== void 0) {
2650
+ metadata = (0, import_behavioral_ir6.withTestMetadata)(metadata, raw.test);
2203
2651
  }
2204
2652
  return Object.keys(metadata).length > 0 ? metadata : null;
2205
2653
  }
@@ -2252,7 +2700,7 @@ function answersWithHttpResponses(raw) {
2252
2700
  if (raw.boundaryBinding === null || raw.boundaryBinding === void 0) {
2253
2701
  return true;
2254
2702
  }
2255
- return (0, import_behavioral_ir5.exchangesHttpResponses)(raw.boundaryBinding);
2703
+ return (0, import_behavioral_ir6.exchangesHttpResponses)(raw.boundaryBinding);
2256
2704
  }
2257
2705
  function detectGaps(raw, transitions, options) {
2258
2706
  if (options.gapHandling === "silent") {
@@ -2344,18 +2792,27 @@ function bodyToShape(body) {
2344
2792
  }
2345
2793
  return null;
2346
2794
  }
2795
+ function statusValueRef(code) {
2796
+ if (code === null) {
2797
+ return null;
2798
+ }
2799
+ return code.type === "literal" ? { type: "literal", value: code.value } : { type: "unresolved", sourceText: code.sourceText };
2800
+ }
2347
2801
  var terminalConverters = {
2348
2802
  response: (t) => {
2349
- const statusCode = t.statusCode ? t.statusCode.type === "literal" ? { type: "literal", value: t.statusCode.value } : { type: "unresolved", sourceText: t.statusCode.sourceText } : null;
2350
2803
  const body = bodyToShape(t.body);
2351
- return { type: "response", statusCode, body, headers: {} };
2804
+ return {
2805
+ type: "response",
2806
+ statusCode: statusValueRef(t.statusCode),
2807
+ body,
2808
+ headers: {}
2809
+ };
2352
2810
  },
2353
2811
  throw: (t) => {
2354
2812
  if (t.statusCode && t.producesResponse === true) {
2355
- const statusCode = t.statusCode.type === "literal" ? { type: "literal", value: t.statusCode.value } : { type: "unresolved", sourceText: t.statusCode.sourceText };
2356
2813
  return {
2357
2814
  type: "response",
2358
- statusCode,
2815
+ statusCode: statusValueRef(t.statusCode),
2359
2816
  body: bodyToShape(t.body),
2360
2817
  headers: {}
2361
2818
  };
@@ -2383,7 +2840,8 @@ var terminalConverters = {
2383
2840
  type: "return",
2384
2841
  value: bodyToShape(t.body)
2385
2842
  }),
2386
- void: (_t) => ({ type: "void" })
2843
+ void: (_t) => ({ type: "void" }),
2844
+ exit: (t) => ({ type: "exit", code: statusValueRef(t.statusCode) })
2387
2845
  };
2388
2846
  function terminalToOutput(terminal) {
2389
2847
  return terminalConverters[terminal.kind](terminal);
@@ -2426,15 +2884,19 @@ function paramToInput(param) {
2426
2884
  name: param.name,
2427
2885
  position: param.position,
2428
2886
  role: param.role,
2887
+ ...param.field === void 0 ? {} : { field: param.field },
2429
2888
  shape: param.typeText ? { type: "ref", name: param.typeText } : null
2430
2889
  };
2431
2890
  }
2432
2891
  // Annotate the CommonJS export names for ESM import in node:
2433
2892
  0 && (module.exports = {
2893
+ EnclosingNodes,
2434
2894
  IdMap,
2435
2895
  IdSet,
2896
+ KeptParses,
2436
2897
  MAX_ENTRIES,
2437
2898
  MAX_PATHS,
2899
+ MODULE_SURFACE_RECOGNITION,
2438
2900
  PathBudgetExceeded,
2439
2901
  SKIP_CHILDREN,
2440
2902
  UnmodeledFlow,
@@ -2459,13 +2921,18 @@ function paramToInput(param) {
2459
2921
  enumerateOrDegrade,
2460
2922
  enumerateStructuredPaths,
2461
2923
  evaluatePackHealth,
2924
+ extractionConfigStamp,
2462
2925
  firstWrittenReading,
2463
2926
  formatPackHealth,
2464
2927
  guardsHoldOn,
2465
2928
  httpRouteDiscovery,
2929
+ isListedTestFile,
2466
2930
  makeTransitionId,
2467
2931
  mapReading,
2932
+ markReturnsAsExitCode,
2933
+ matchesTestFileName,
2468
2934
  moduleInitStructure,
2935
+ moduleOfFile,
2469
2936
  noopTimer,
2470
2937
  packGradients,
2471
2938
  paramToInput,
@@ -2477,8 +2944,12 @@ function paramToInput(param) {
2477
2944
  runDigest,
2478
2945
  runsBefore,
2479
2946
  scopeOption,
2947
+ settleModules,
2480
2948
  sharedGatingConditions,
2949
+ sourceRefsOf,
2950
+ stampMayMissAWrite,
2481
2951
  stampModuleImports,
2952
+ stampModules,
2482
2953
  storageSystemOption,
2483
2954
  summaryCountsByPack,
2484
2955
  tallyUnit,