@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.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/index.ts
2
- import { createHash as createHash3 } from "crypto";
2
+ import { createHash as createHash4 } from "crypto";
3
3
  import {
4
+ effectKey,
4
5
  exchangesHttpResponses as exchangesHttpResponses2,
5
6
  foldRepeatedEffects,
6
7
  normalizeCalleeText,
@@ -9,6 +10,7 @@ import {
9
10
  withMountMetadata,
10
11
  withRequestSpellingMetadata,
11
12
  withSourceDocumentMetadata,
13
+ withTestMetadata,
12
14
  withWrapperMetadata as withWrapperMetadata2
13
15
  } from "@suss/behavioral-ir";
14
16
 
@@ -281,6 +283,56 @@ function withDistinctIds(transitions) {
281
283
  });
282
284
  }
283
285
 
286
+ // src/declaredModules.ts
287
+ import fs from "fs";
288
+ import path from "path";
289
+ var MODULE_SURFACE_RECOGNITION = "modules";
290
+ function settleModules(modules, candidates) {
291
+ return (modules ?? []).map((module) => ({
292
+ ...module,
293
+ public: module.public ?? candidates.map(
294
+ (one) => path.join(module.root, one.replace("{name}", module.name))
295
+ ).filter((file) => fs.existsSync(file))
296
+ }));
297
+ }
298
+ function isInside(file, root) {
299
+ const relative = path.relative(root, file);
300
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
301
+ }
302
+ function moduleOfFile(modules, file) {
303
+ const byPublicFile = modules.find((module) => module.public.includes(file));
304
+ if (byPublicFile !== void 0) {
305
+ return byPublicFile;
306
+ }
307
+ let deepest;
308
+ for (const module of modules) {
309
+ if (isInside(file, module.root) && (deepest === void 0 || module.root.length > deepest.root.length)) {
310
+ deepest = module;
311
+ }
312
+ }
313
+ return deepest;
314
+ }
315
+ function stampModules(summaries, modules, root) {
316
+ if (modules.length === 0) {
317
+ return;
318
+ }
319
+ for (const summary of summaries) {
320
+ const file = path.resolve(root ?? "", summary.location.file);
321
+ const module = moduleOfFile(modules, file);
322
+ if (module !== void 0) {
323
+ summary.location.module = module.name;
324
+ }
325
+ }
326
+ }
327
+ function modulesStamp(modules) {
328
+ if (modules === void 0 || modules.length === 0) {
329
+ return void 0;
330
+ }
331
+ return JSON.stringify(
332
+ modules.map((module) => [module.name, module.root, [...module.public]])
333
+ );
334
+ }
335
+
284
336
  // src/effectGuards.ts
285
337
  var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
286
338
  var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
@@ -297,6 +349,41 @@ function runsBefore(effectLine, terminalEndLine) {
297
349
  return effectLine <= terminalEndLine;
298
350
  }
299
351
 
352
+ // src/exitCode.ts
353
+ import path2 from "path";
354
+ import { declarationKey } from "@suss/behavioral-ir";
355
+ function markReturnsAsExitCode(summaries, returnsTheCode, workspaceRoot) {
356
+ for (const summary of summaries) {
357
+ const span = summary.location.span;
358
+ const file = workspaceRoot === void 0 ? summary.location.file : path2.resolve(workspaceRoot, summary.location.file);
359
+ markExitCodeFrom(
360
+ summary,
361
+ span !== void 0 && returnsTheCode.has(declarationKey(file, span))
362
+ );
363
+ }
364
+ }
365
+ function markExitCodeFrom(summary, marked) {
366
+ const process2 = summary.metadata?.process;
367
+ if (marked) {
368
+ summary.metadata = {
369
+ ...summary.metadata ?? {},
370
+ process: { ...process2 ?? {}, exitCodeFrom: "return" }
371
+ };
372
+ return;
373
+ }
374
+ if (process2 === void 0 || !("exitCodeFrom" in process2)) {
375
+ return;
376
+ }
377
+ const { exitCodeFrom: _dropped, ...rest } = process2;
378
+ const { process: _process, ...others } = summary.metadata ?? {};
379
+ const left = Object.keys(rest).length === 0 ? others : { ...others, process: rest };
380
+ if (Object.keys(left).length === 0) {
381
+ Reflect.deleteProperty(summary, "metadata");
382
+ return;
383
+ }
384
+ summary.metadata = left;
385
+ }
386
+
300
387
  // src/moduleImports.ts
301
388
  function stampModuleImports(summaries, importsOf) {
302
389
  const byFile = /* @__PURE__ */ new Map();
@@ -373,8 +460,8 @@ function httpRouteDiscovery(opts) {
373
460
  registrationChain: [...opts.methods]
374
461
  },
375
462
  bindingExtraction: {
376
- // `.all` registers every method, so it records "*", which the pairing
377
- // engine treats as agreeing with any method at all.
463
+ // `.all` registers every method, so it records "*", which pairs with
464
+ // any method.
378
465
  method: {
379
466
  type: "fromRegistration",
380
467
  position: "methodName",
@@ -390,10 +477,9 @@ function httpRouteDiscovery(opts) {
390
477
  }
391
478
  return [
392
479
  ...calls,
393
- // Routes registered in a loop over an array of specs, a shape
394
- // registration-call discovery cannot see. The receiver comes from
395
- // the same import declaration as the calls above, so a loop that
396
- // never touches this library's routable is left alone.
480
+ // Routes registered in a loop over an array of specs, which the calls
481
+ // above cannot see. The receiver must come from the same import, so a
482
+ // loop that never touches this library's routable is left alone.
397
483
  {
398
484
  kind,
399
485
  match: {
@@ -540,7 +626,7 @@ var configuredCallOption = z.object({
540
626
  receiver: z.string(),
541
627
  /** Method that performs the send. */
542
628
  method: z.string(),
543
- /** Argument index carrying the subject. */
629
+ /** Index of the argument that is the subject. */
544
630
  subjectArg: z.number(),
545
631
  /**
546
632
  * Which argument the message body is. Left out when the method has no
@@ -599,8 +685,8 @@ var catchEntryCond = () => fixedCond(
599
685
  "positive",
600
686
  "catchBlock"
601
687
  );
602
- function classify(path4, home, isAncestorOrSelf) {
603
- return path4.map((cond) => {
688
+ function classify(path6, home, isAncestorOrSelf) {
689
+ return path6.map((cond) => {
604
690
  if (cond.branchStmt === null) {
605
691
  return cond.info;
606
692
  }
@@ -665,9 +751,9 @@ function chargeBudget(state) {
665
751
  );
666
752
  }
667
753
  }
668
- function recordTerminal(ctx, terminal, home, path4) {
754
+ function recordTerminal(ctx, terminal, home, path6) {
669
755
  chargeBudget(ctx.state);
670
- const info = classify(path4, home, ctx.isAncestorOrSelf);
756
+ const info = classify(path6, home, ctx.isAncestorOrSelf);
671
757
  const existing = ctx.state.byTerminal.get(terminal) ?? [];
672
758
  existing.push(info);
673
759
  ctx.state.byTerminal.set(terminal, existing);
@@ -678,15 +764,15 @@ function containsTerminal(ctx, stmt) {
678
764
  }
679
765
  return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
680
766
  }
681
- function stepIf(stmt, path4, ctx) {
767
+ function stepIf(stmt, path6, ctx) {
682
768
  const thenExit = exitKindOfList(stmt.thenBody);
683
769
  const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
684
770
  const thenPath = [
685
- ...path4,
771
+ ...path6,
686
772
  branchCond(stmt, stmt.condition, "positive", elseExit)
687
773
  ];
688
774
  const elsePath = [
689
- ...path4,
775
+ ...path6,
690
776
  branchCond(
691
777
  stmt,
692
778
  stmt.condition,
@@ -703,7 +789,7 @@ function stepIf(stmt, path4, ctx) {
703
789
  }
704
790
  return out;
705
791
  }
706
- function stepSwitch(stmt, path4, ctx) {
792
+ function stepSwitch(stmt, path6, ctx) {
707
793
  const out = [];
708
794
  const negations = [];
709
795
  let defaultRanBody = false;
@@ -713,7 +799,7 @@ function stepSwitch(stmt, path4, ctx) {
713
799
  if (hasStrayBreak(group.body)) {
714
800
  throw new UnmodeledFlow("non-trailing break in switch clause");
715
801
  }
716
- const groupPath = group.condition === null ? [...path4, ...negations] : [...path4, fixedCond(group.condition, "positive", "explicit")];
802
+ const groupPath = group.condition === null ? [...path6, ...negations] : [...path6, fixedCond(group.condition, "positive", "explicit")];
717
803
  const conts = enumerate(ctx, group.body, groupPath);
718
804
  if (conts.length > 0) {
719
805
  if (!group.hasTrailingBreak && !fallthroughSafe) {
@@ -730,24 +816,24 @@ function stepSwitch(stmt, path4, ctx) {
730
816
  }
731
817
  }
732
818
  if (!defaultRanBody) {
733
- out.push([...path4, ...negations]);
819
+ out.push([...path6, ...negations]);
734
820
  }
735
821
  return out;
736
822
  }
737
- function stepLoop(stmt, path4, ctx) {
823
+ function stepLoop(stmt, path6, ctx) {
738
824
  const bodyExit = exitKindOfList(stmt.body);
739
825
  if (containsTerminal(ctx, stmt)) {
740
826
  enumerate(ctx, stmt.body, [
741
- ...path4,
827
+ ...path6,
742
828
  loopIterationCond(stmt.condition.sourceText)
743
829
  ]);
744
830
  }
745
831
  if (bodyExit !== null) {
746
- return [[...path4, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
832
+ return [[...path6, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
747
833
  }
748
- return [path4];
834
+ return [path6];
749
835
  }
750
- function stepTry(stmt, path4, ctx) {
836
+ function stepTry(stmt, path6, ctx) {
751
837
  if (stmt.finallyBody !== null) {
752
838
  const finallyHasTerminal = stmt.finallyBody.some(
753
839
  (s) => containsTerminal(ctx, s)
@@ -757,22 +843,22 @@ function stepTry(stmt, path4, ctx) {
757
843
  }
758
844
  }
759
845
  const out = [];
760
- out.push(...enumerate(ctx, stmt.tryBody, path4));
846
+ out.push(...enumerate(ctx, stmt.tryBody, path6));
761
847
  if (stmt.catchBody !== null) {
762
- const catchPath = [...path4, catchEntryCond()];
848
+ const catchPath = [...path6, catchEntryCond()];
763
849
  out.push(...enumerate(ctx, stmt.catchBody, catchPath));
764
850
  }
765
851
  return out;
766
852
  }
767
- function stepExit(stmt, path4, ctx) {
853
+ function stepExit(stmt, path6, ctx) {
768
854
  const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
769
855
  if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
770
- collector.push(path4);
856
+ collector.push(path6);
771
857
  }
772
858
  return [];
773
859
  }
774
- function stepOpaque(_stmt, path4, _ctx) {
775
- return [path4];
860
+ function stepOpaque(_stmt, path6, _ctx) {
861
+ return [path6];
776
862
  }
777
863
  var stepHandlers = {
778
864
  if: stepIf,
@@ -782,20 +868,20 @@ var stepHandlers = {
782
868
  exit: stepExit,
783
869
  opaque: stepOpaque
784
870
  };
785
- function stepCallback(ctx, body, path4) {
871
+ function stepCallback(ctx, body, path6) {
786
872
  const exits = [];
787
873
  ctx.state.callbackExits.push(exits);
788
874
  let fellThrough;
789
875
  try {
790
- fellThrough = enumerate(ctx, body, path4);
876
+ fellThrough = enumerate(ctx, body, path6);
791
877
  } finally {
792
878
  ctx.state.callbackExits.pop();
793
879
  }
794
880
  const out = [...fellThrough, ...exits];
795
- return out.length === 0 ? [path4] : out;
881
+ return out.length === 0 ? [path6] : out;
796
882
  }
797
- function stepStatement(ctx, stmt, path4) {
798
- let paths = [path4];
883
+ function stepStatement(ctx, stmt, path6) {
884
+ let paths = [path6];
799
885
  for (const body of stmt.callbacks ?? []) {
800
886
  paths = paths.flatMap((each) => stepCallback(ctx, body, each));
801
887
  }
@@ -876,8 +962,8 @@ function enumerate(ctx, stmts, prefix) {
876
962
  let frontiers = [[...prefix]];
877
963
  for (const stmt of stmts) {
878
964
  const nextFrontiers = [];
879
- for (const path4 of mergeRejoined(frontiers)) {
880
- nextFrontiers.push(...stepStatement(ctx, stmt, path4));
965
+ for (const path6 of mergeRejoined(frontiers)) {
966
+ nextFrontiers.push(...stepStatement(ctx, stmt, path6));
881
967
  }
882
968
  frontiers = nextFrontiers;
883
969
  if (frontiers.length > MAX_PATHS) {
@@ -928,9 +1014,9 @@ function enumerateStructuredPaths(input) {
928
1014
  isAncestorOrSelf
929
1015
  };
930
1016
  const continuations = withRejoined(enumerate(ctx, input.statements, []));
931
- for (const path4 of continuations) {
1017
+ for (const path6 of continuations) {
932
1018
  chargeBudget(state);
933
- state.fallthrough.push(classify(path4, null, isAncestorOrSelf));
1019
+ state.fallthrough.push(classify(path6, null, isAncestorOrSelf));
934
1020
  }
935
1021
  return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
936
1022
  }
@@ -973,8 +1059,8 @@ function sharedGatingConditions(paths, readPredicate) {
973
1059
  return [];
974
1060
  }
975
1061
  const shared = new Set(first.map(keyOf2));
976
- for (const path4 of rest) {
977
- const here = new Set(path4.map(keyOf2));
1062
+ for (const path6 of rest) {
1063
+ const here = new Set(path6.map(keyOf2));
978
1064
  for (const value of shared) {
979
1065
  if (!here.has(value)) {
980
1066
  shared.delete(value);
@@ -1004,7 +1090,7 @@ var IdSet = class {
1004
1090
  has(node) {
1005
1091
  return this.byKey.has(node.id);
1006
1092
  }
1007
- /** The node this set was built with, which is the caller's own handle for it. */
1093
+ /** The wrapper that was added, which may be a different object from `node`. */
1008
1094
  get(node) {
1009
1095
  return this.byKey.get(node.id);
1010
1096
  }
@@ -1035,6 +1121,51 @@ var IdMap = class {
1035
1121
  }
1036
1122
  };
1037
1123
 
1124
+ // src/paths/nodeReads.ts
1125
+ var EnclosingNodes = class {
1126
+ constructor(types) {
1127
+ this.types = types;
1128
+ }
1129
+ types;
1130
+ byTree = /* @__PURE__ */ new WeakMap();
1131
+ /**
1132
+ * The nearest ancestor of one of the types, or null when there is none.
1133
+ * Every node passed on the way up has the same result, so each one is
1134
+ * kept, and a later read from a sibling stops one step up.
1135
+ */
1136
+ of(node) {
1137
+ const kept = this.keptFor(node.tree);
1138
+ const known = kept.get(node.id);
1139
+ if (known !== void 0) {
1140
+ return known;
1141
+ }
1142
+ const passed = [node.id];
1143
+ let current = node.parent;
1144
+ while (current !== null && !this.types.has(current.type)) {
1145
+ const above = kept.get(current.id);
1146
+ if (above !== void 0) {
1147
+ current = above;
1148
+ break;
1149
+ }
1150
+ passed.push(current.id);
1151
+ current = current.parent;
1152
+ }
1153
+ for (const id of passed) {
1154
+ kept.set(id, current);
1155
+ }
1156
+ return current;
1157
+ }
1158
+ keptFor(tree) {
1159
+ const known = this.byTree.get(tree);
1160
+ if (known !== void 0) {
1161
+ return known;
1162
+ }
1163
+ const fresh = /* @__PURE__ */ new Map();
1164
+ this.byTree.set(tree, fresh);
1165
+ return fresh;
1166
+ }
1167
+ };
1168
+
1038
1169
  // src/paths/nodeWalk.ts
1039
1170
  var SKIP_CHILDREN = /* @__PURE__ */ Symbol("skipChildren");
1040
1171
  function walkDescendants(root, carried, visitor) {
@@ -1051,23 +1182,23 @@ function walkDescendants(root, carried, visitor) {
1051
1182
  }
1052
1183
 
1053
1184
  // src/projectRoot.ts
1054
- import path from "path";
1185
+ import path3 from "path";
1055
1186
  function commonDirectoryOf(files) {
1056
- const absolute = files.filter((f) => path.isAbsolute(f));
1187
+ const absolute = files.filter((f) => path3.isAbsolute(f));
1057
1188
  const first = absolute[0];
1058
1189
  if (first === void 0) {
1059
1190
  return void 0;
1060
1191
  }
1061
- let shared = path.dirname(first).split(path.sep);
1192
+ let shared = path3.dirname(first).split(path3.sep);
1062
1193
  for (const file of absolute.slice(1)) {
1063
- const parts = path.dirname(file).split(path.sep);
1194
+ const parts = path3.dirname(file).split(path3.sep);
1064
1195
  let i = 0;
1065
1196
  while (i < shared.length && i < parts.length && shared[i] === parts[i]) {
1066
1197
  i += 1;
1067
1198
  }
1068
1199
  shared = shared.slice(0, i);
1069
1200
  }
1070
- const joined = shared.join(path.sep);
1201
+ const joined = shared.join(path3.sep);
1071
1202
  return joined.length > 1 ? joined : void 0;
1072
1203
  }
1073
1204
 
@@ -1124,11 +1255,79 @@ function valueToReadFurtherFrom(reading) {
1124
1255
  return reading.kind === "written" ? reading.value : null;
1125
1256
  }
1126
1257
 
1258
+ // src/testFiles.ts
1259
+ function isListedTestFile(file, listed) {
1260
+ if (listed === void 0) {
1261
+ return true;
1262
+ }
1263
+ const whole = withForwardSlashes(file);
1264
+ return listed.some((entry) => {
1265
+ const tail = withForwardSlashes(entry).replace(/^\.\//, "");
1266
+ return whole === tail || whole.endsWith(`/${tail}`);
1267
+ });
1268
+ }
1269
+ function matchesTestFileName(file, patterns) {
1270
+ const base = withForwardSlashes(file).split("/").pop() ?? "";
1271
+ return patterns.some((pattern) => globToRegExp(pattern).test(base));
1272
+ }
1273
+ function globToRegExp(pattern) {
1274
+ const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1275
+ return new RegExp(`^${escaped}$`);
1276
+ }
1277
+ function withForwardSlashes(file) {
1278
+ return file.replace(/\\/g, "/");
1279
+ }
1280
+
1281
+ // src/valueSources.ts
1282
+ var QUOTED_LENGTH = 80;
1283
+ function sourceRefsOf(leaves, spelling) {
1284
+ const refs = [];
1285
+ const said = /* @__PURE__ */ new Set();
1286
+ for (const leaf of leaves) {
1287
+ const ref = convertedFrom(refOf(leaf, spelling), leaf.conversions);
1288
+ const spelled = JSON.stringify(ref);
1289
+ if (!said.has(spelled)) {
1290
+ said.add(spelled);
1291
+ refs.push(ref);
1292
+ }
1293
+ }
1294
+ return refs;
1295
+ }
1296
+ function convertedFrom(ref, conversions) {
1297
+ return conversions.reduceRight(
1298
+ (from, method) => ({
1299
+ type: "derived",
1300
+ from,
1301
+ derivation: { type: "methodCall", method, args: [] }
1302
+ }),
1303
+ ref
1304
+ );
1305
+ }
1306
+ function refOf(leaf, spelling) {
1307
+ const whole = leaf.computedAt === null;
1308
+ const input = whole ? spelling.inputOf(leaf) : null;
1309
+ if (input !== null) {
1310
+ return input;
1311
+ }
1312
+ if (whole && leaf.end.is === "written" && leaf.path.length === 0) {
1313
+ const literal = spelling.literalAt(leaf.key);
1314
+ if (literal !== void 0) {
1315
+ return { type: "literal", value: literal };
1316
+ }
1317
+ }
1318
+ const at = leaf.computedAt ?? leaf.key;
1319
+ return { type: "unresolved", sourceText: quoted(spelling.textAt(at), at) };
1320
+ }
1321
+ function quoted(text, key) {
1322
+ const written = (text ?? key.slice(key.lastIndexOf("#") + 1)).replace(/\s+/g, " ").trim();
1323
+ return written.length <= QUOTED_LENGTH ? written : `${written.slice(0, QUOTED_LENGTH - 3)}...`;
1324
+ }
1325
+
1127
1326
  // src/adapterStamp.ts
1128
1327
  import { createHash } from "crypto";
1129
- import fs from "fs";
1328
+ import fs2 from "fs";
1130
1329
  import { createRequire } from "module";
1131
- import path2 from "path";
1330
+ import path4 from "path";
1132
1331
  import { fileURLToPath } from "url";
1133
1332
  var ANALYSIS_PACKAGES = [
1134
1333
  "@suss/extractor",
@@ -1170,16 +1369,16 @@ function createAdapterStamp(config) {
1170
1369
  function readAdapterCodeStamp(moduleUrl) {
1171
1370
  try {
1172
1371
  const selfPath = fileURLToPath(moduleUrl);
1173
- const hash = computeDistHashFrom(path2.dirname(selfPath));
1372
+ const hash = computeDistHashFrom(path4.dirname(selfPath));
1174
1373
  return hash.length > 0 ? { kind: "bundle", hash } : SOURCE_STAMP;
1175
1374
  } catch {
1176
1375
  return SOURCE_STAMP;
1177
1376
  }
1178
1377
  }
1179
1378
  function computeDistHashFrom(dir) {
1180
- const candidates = [path2.join(dir, "index.js"), path2.join(dir, "index.cjs")];
1379
+ const candidates = [path4.join(dir, "index.js"), path4.join(dir, "index.cjs")];
1181
1380
  for (const candidate of candidates) {
1182
- if (!fs.existsSync(candidate)) {
1381
+ if (!fs2.existsSync(candidate)) {
1183
1382
  continue;
1184
1383
  }
1185
1384
  return computeContentHash([candidate, ...analysisBundles()]);
@@ -1193,7 +1392,7 @@ function computeContentHash(paths) {
1193
1392
  const hash = createHash("sha256");
1194
1393
  for (const file of paths) {
1195
1394
  try {
1196
- hash.update(fs.readFileSync(file));
1395
+ hash.update(fs2.readFileSync(file));
1197
1396
  } catch {
1198
1397
  return "";
1199
1398
  }
@@ -1209,13 +1408,29 @@ function projectFileStamp(paths) {
1209
1408
  hash.update(file);
1210
1409
  hash.update("\0");
1211
1410
  try {
1212
- hash.update(fs.readFileSync(file));
1411
+ hash.update(fs2.readFileSync(file));
1213
1412
  } catch {
1214
1413
  hash.update("absent");
1215
1414
  }
1216
1415
  }
1217
1416
  return hash.digest("hex").slice(0, 16);
1218
1417
  }
1418
+ function extractionConfigStamp(config) {
1419
+ const parts = [
1420
+ ["gapHandling", config.gapHandling ?? "permissive"],
1421
+ ["includeReachable", optionalText(config.includeReachable)],
1422
+ ["workspaceRoot", config.workspaceRoot],
1423
+ ["projectRoot", config.projectRoot],
1424
+ ["importRoots", config.importRoots?.join(path4.delimiter)],
1425
+ ["modules", modulesStamp(config.modules)]
1426
+ ];
1427
+ return parts.flatMap(
1428
+ ([name, value]) => value === void 0 ? [] : [`${name}=${value}`]
1429
+ ).join(",");
1430
+ }
1431
+ function optionalText(value) {
1432
+ return value === void 0 ? void 0 : String(value);
1433
+ }
1219
1434
  function runDigest(packsDigest, packs, files) {
1220
1435
  const inputs = packs.flatMap((pack) => pack.discoveryInputs?.(files) ?? []);
1221
1436
  return `${packsDigest}|reads:${projectFileStamp(inputs)}`;
@@ -1233,10 +1448,118 @@ function analysisBundles() {
1233
1448
  }
1234
1449
 
1235
1450
  // src/cache.ts
1451
+ import { createHash as createHash3 } from "crypto";
1452
+ import fs4 from "fs/promises";
1453
+ import path5 from "path";
1454
+
1455
+ // src/cacheStamps.ts
1236
1456
  import { createHash as createHash2 } from "crypto";
1237
- import fs2 from "fs/promises";
1238
- import path3 from "path";
1239
- var SCHEMA_VERSION = "7";
1457
+ import fs3 from "fs/promises";
1458
+ var RECENT_CHANGE_MS = 2e3;
1459
+ function stampMayMissAWrite(stat, statAt) {
1460
+ return Math.max(stat.mtimeMs, stat.ctimeMs) > statAt - RECENT_CHANGE_MS;
1461
+ }
1462
+ async function startRun(files, configPath) {
1463
+ const startedAt = Date.now();
1464
+ const config = configPath === void 0 ? null : await statAtStart(configPath);
1465
+ const stats = await Promise.all(files.map(statAtStart));
1466
+ stats.sort(
1467
+ (a, b) => a.stamp.path < b.stamp.path ? -1 : a.stamp.path > b.stamp.path ? 1 : 0
1468
+ );
1469
+ return {
1470
+ startedAt,
1471
+ config: config?.ctimeMs === null ? null : config,
1472
+ files: stats,
1473
+ hashes: /* @__PURE__ */ new Map()
1474
+ };
1475
+ }
1476
+ async function statAtStart(filePath) {
1477
+ try {
1478
+ const stat = await fs3.stat(filePath);
1479
+ return {
1480
+ stamp: { path: filePath, mtimeMs: stat.mtimeMs, size: stat.size },
1481
+ ctimeMs: stat.ctimeMs
1482
+ };
1483
+ } catch {
1484
+ return { stamp: { path: filePath, mtimeMs: -1, size: -1 }, ctimeMs: null };
1485
+ }
1486
+ }
1487
+ async function hashOf(run, filePath) {
1488
+ const known = run.hashes.get(filePath);
1489
+ if (known !== void 0) {
1490
+ return known;
1491
+ }
1492
+ const hash = await hashFile(filePath);
1493
+ run.hashes.set(filePath, hash);
1494
+ return hash;
1495
+ }
1496
+ async function hashRecentChanges(run) {
1497
+ const recent = [...run.config === null ? [] : [run.config], ...run.files].filter(
1498
+ (file) => file.ctimeMs !== null && stampMayMissAWrite(
1499
+ { mtimeMs: file.stamp.mtimeMs, ctimeMs: file.ctimeMs },
1500
+ run.startedAt
1501
+ )
1502
+ ).map((file) => file.stamp.path);
1503
+ await Promise.all(recent.map((filePath) => hashOf(run, filePath)));
1504
+ }
1505
+ async function stillMatches(stored, found, run) {
1506
+ if (!fileStampEquals(stored, found)) {
1507
+ return false;
1508
+ }
1509
+ if (stored === null || stored.verifyHash !== true) {
1510
+ return true;
1511
+ }
1512
+ return await hashOf(run, stored.path) === stored.contentHash;
1513
+ }
1514
+ async function filesStillMatch(stored, run) {
1515
+ if (stored.length !== run.files.length || !stored.every((s, i) => fileStampEquals(s, run.files[i]?.stamp ?? null))) {
1516
+ return false;
1517
+ }
1518
+ const checks = await Promise.all(
1519
+ stored.map((s, i) => stillMatches(s, run.files[i]?.stamp ?? null, run))
1520
+ );
1521
+ return checks.every(Boolean);
1522
+ }
1523
+ async function recordedStamp(start, run, prior) {
1524
+ if (start.ctimeMs === null) {
1525
+ return start.stamp;
1526
+ }
1527
+ const filePath = start.stamp.path;
1528
+ const now = await fs3.stat(filePath).catch(() => null);
1529
+ if (now === null || now.mtimeMs !== start.stamp.mtimeMs || now.size !== start.stamp.size || now.ctimeMs !== start.ctimeMs) {
1530
+ return null;
1531
+ }
1532
+ const atStart = { mtimeMs: start.stamp.mtimeMs, ctimeMs: start.ctimeMs };
1533
+ if (stampMayMissAWrite(atStart, run.startedAt)) {
1534
+ const readBefore = run.hashes.get(filePath);
1535
+ return readBefore === void 0 || readBefore === null ? null : { ...start.stamp, contentHash: readBefore, verifyHash: true };
1536
+ }
1537
+ const hash = run.hashes.get(filePath) ?? reusableHash(prior, start.stamp) ?? await hashFile(filePath);
1538
+ return hash === null ? start.stamp : { ...start.stamp, contentHash: hash };
1539
+ }
1540
+ function reusableHash(prior, stamp) {
1541
+ return prior !== void 0 && prior.verifyHash !== true && fileStampEquals(prior, stamp) ? prior.contentHash : void 0;
1542
+ }
1543
+ async function hashFile(filePath) {
1544
+ try {
1545
+ const content = await fs3.readFile(filePath);
1546
+ return createHash2("sha256").update(content).digest("hex").slice(0, 16);
1547
+ } catch {
1548
+ return null;
1549
+ }
1550
+ }
1551
+ function fileStampEquals(a, b) {
1552
+ if (a === null && b === null) {
1553
+ return true;
1554
+ }
1555
+ if (a === null || b === null) {
1556
+ return false;
1557
+ }
1558
+ return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
1559
+ }
1560
+
1561
+ // src/cache.ts
1562
+ var SCHEMA_VERSION = "9";
1240
1563
  var MAX_ENTRIES = 2;
1241
1564
  var ENTRY_PREFIX = "key-";
1242
1565
  var ENTRY_DIR_NAME = new RegExp(`^${ENTRY_PREFIX}[0-9a-f]{16}$`);
@@ -1253,56 +1576,88 @@ function createCacheLayer(cacheDir) {
1253
1576
  }
1254
1577
  };
1255
1578
  }
1579
+ const manifests = new ManifestReads();
1580
+ const runs = /* @__PURE__ */ new WeakMap();
1581
+ const runFor = async (input) => runs.get(input) ?? await startRun(input.files, input.configPath);
1582
+ const lookupWith = async (input, run) => {
1583
+ const entryDir = entryDirFor(cacheDir, input);
1584
+ const manifest = await manifests.readAtRunStart(
1585
+ path5.join(entryDir, "manifest.json")
1586
+ );
1587
+ if (manifest === null) {
1588
+ return missDiag(await describeAbsentEntry(cacheDir));
1589
+ }
1590
+ if (!await stillMatches(
1591
+ manifest.configStamp,
1592
+ run.config?.stamp ?? null,
1593
+ run
1594
+ )) {
1595
+ return missDiag("config-changed");
1596
+ }
1597
+ if (!await filesStillMatch(manifest.files, run)) {
1598
+ return missDiag("files-changed");
1599
+ }
1600
+ await markUsed(entryDir);
1601
+ manifests.forget();
1602
+ return {
1603
+ kind: "hit",
1604
+ summaries: manifest.summaries,
1605
+ diagnostic: { kind: "hit" }
1606
+ };
1607
+ };
1256
1608
  return {
1257
1609
  async tryHit(input) {
1258
1610
  const result = await this.lookup(input);
1259
1611
  return result.kind === "hit" ? result.summaries : null;
1260
1612
  },
1261
1613
  async lookup(input) {
1262
- const entryDir = entryDirFor(cacheDir, input);
1263
- const manifest = await readManifest(
1264
- path3.join(entryDir, "manifest.json")
1265
- );
1266
- if (manifest === null) {
1267
- return missDiag(await describeAbsentEntry(cacheDir));
1614
+ const run = await startRun(input.files, input.configPath);
1615
+ runs.set(input, run);
1616
+ const result = await lookupWith(input, run);
1617
+ if (result.kind === "miss") {
1618
+ await hashRecentChanges(run);
1268
1619
  }
1269
- const currentConfigStamp = await stampConfigFile(input.configPath);
1270
- if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
1271
- return missDiag("config-changed");
1272
- }
1273
- const currentFiles = await resolveFileStamps(input);
1274
- if (!fileStampsEqual(manifest.files, currentFiles)) {
1275
- return missDiag("files-changed");
1276
- }
1277
- await markUsed(entryDir);
1278
- return {
1279
- kind: "hit",
1280
- summaries: manifest.summaries,
1281
- diagnostic: { kind: "hit" }
1282
- };
1620
+ return result;
1283
1621
  },
1284
1622
  async plan(input) {
1285
1623
  const entryDir = entryDirFor(cacheDir, input);
1286
- const manifest = await readManifest(
1287
- path3.join(entryDir, "manifest.json")
1624
+ const manifest = await manifests.readAgain(
1625
+ path5.join(entryDir, "manifest.json")
1288
1626
  );
1289
1627
  if (manifest === null || manifest.roots === void 0 || manifest.rootMeta === void 0 || manifest.owners === void 0) {
1290
1628
  return null;
1291
1629
  }
1292
- const currentConfigStamp = await stampConfigFile(input.configPath);
1293
- if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
1630
+ const run = await runFor(input);
1631
+ if (!await stillMatches(
1632
+ manifest.configStamp,
1633
+ run.config?.stamp ?? null,
1634
+ run
1635
+ )) {
1294
1636
  return null;
1295
1637
  }
1296
1638
  await markUsed(entryDir);
1297
- return buildPlan(manifest, await resolveFileStamps(input));
1639
+ return buildPlan(manifest, run);
1298
1640
  },
1299
1641
  async write(input, summaries, attribution) {
1300
1642
  const entryDir = entryDirFor(cacheDir, input);
1301
- const previous = await readManifest(
1302
- path3.join(entryDir, "manifest.json")
1643
+ const previous = await manifests.readAgain(
1644
+ path5.join(entryDir, "manifest.json")
1303
1645
  );
1304
- const configStamp = await stampConfigFile(input.configPath);
1305
- const files = await hashStamps(await resolveFileStamps(input), previous);
1646
+ manifests.forget();
1647
+ const known = runs.get(input);
1648
+ const run = known ?? await runFor(input);
1649
+ if (known === void 0) {
1650
+ await hashRecentChanges(run);
1651
+ }
1652
+ runs.delete(input);
1653
+ const configStamp = run.config === null ? null : await recordedStamp(run.config, run, void 0);
1654
+ const prior = new Map((previous?.files ?? []).map((f) => [f.path, f]));
1655
+ const recorded = await Promise.all(
1656
+ run.files.map(
1657
+ (file) => recordedStamp(file, run, prior.get(file.stamp.path))
1658
+ )
1659
+ );
1660
+ const files = recorded.filter((stamp) => stamp !== null);
1306
1661
  const manifest = {
1307
1662
  schemaVersion: SCHEMA_VERSION,
1308
1663
  adapterPacksDigest: input.adapterPacksDigest,
@@ -1311,19 +1666,20 @@ function createCacheLayer(cacheDir) {
1311
1666
  summaries,
1312
1667
  ...attribution === void 0 ? {} : encodeAttribution(attribution)
1313
1668
  };
1314
- await fs2.mkdir(entryDir, { recursive: true });
1315
- await fs2.writeFile(
1316
- path3.join(entryDir, "manifest.json"),
1669
+ await fs4.mkdir(entryDir, { recursive: true });
1670
+ await fs4.writeFile(
1671
+ path5.join(entryDir, "manifest.json"),
1317
1672
  JSON.stringify(manifest)
1318
1673
  );
1319
1674
  await evictOldEntries(cacheDir, entryDir);
1320
1675
  }
1321
1676
  };
1322
1677
  }
1323
- async function buildPlan(manifest, currentStamps) {
1678
+ async function buildPlan(manifest, run) {
1324
1679
  const stored = new Map(manifest.files.map((f) => [f.path, f]));
1325
- const current = new Map(currentStamps.map((f) => [f.path, f]));
1680
+ const current = new Map(run.files.map((f) => [f.stamp.path, f.stamp]));
1326
1681
  const changed = /* @__PURE__ */ new Set();
1682
+ const added = /* @__PURE__ */ new Set();
1327
1683
  const removed = /* @__PURE__ */ new Set();
1328
1684
  for (const p of stored.keys()) {
1329
1685
  if (!current.has(p)) {
@@ -1335,17 +1691,20 @@ async function buildPlan(manifest, currentStamps) {
1335
1691
  const before = stored.get(p);
1336
1692
  if (before === void 0) {
1337
1693
  changed.add(p);
1694
+ added.add(p);
1338
1695
  } else if (!fileStampEquals(before, stamp)) {
1339
1696
  if (before.contentHash === void 0) {
1340
1697
  changed.add(p);
1341
1698
  } else {
1342
1699
  toVerify.push(p);
1343
1700
  }
1701
+ } else if (before.verifyHash === true) {
1702
+ toVerify.push(p);
1344
1703
  }
1345
1704
  }
1346
1705
  await Promise.all(
1347
1706
  toVerify.map(async (p) => {
1348
- const hash = await hashFile(p);
1707
+ const hash = await hashOf(run, p);
1349
1708
  if (hash === null || hash !== stored.get(p)?.contentHash) {
1350
1709
  changed.add(p);
1351
1710
  }
@@ -1354,6 +1713,10 @@ async function buildPlan(manifest, currentStamps) {
1354
1713
  const roots = /* @__PURE__ */ new Map();
1355
1714
  const rootNames = manifest.roots ?? [];
1356
1715
  const depPaths = manifest.depPaths ?? [];
1716
+ const pathsOf = (ids) => ids.flatMap((d) => {
1717
+ const p = depPaths[d];
1718
+ return p === void 0 ? [] : [p];
1719
+ });
1357
1720
  let rootsDeclined = 0;
1358
1721
  rootNames.forEach((rootPath, i) => {
1359
1722
  const meta = manifest.rootMeta?.[i];
@@ -1366,35 +1729,54 @@ async function buildPlan(manifest, currentStamps) {
1366
1729
  roots.set(rootPath, {
1367
1730
  path: rootPath,
1368
1731
  cacheable: meta.cacheable,
1369
- deps: meta.deps.flatMap((d) => {
1370
- const p = depPaths[d];
1371
- return p === void 0 ? [] : [p];
1372
- }),
1732
+ deps: pathsOf(meta.deps),
1373
1733
  claims: meta.claims,
1374
1734
  meta: meta.meta,
1375
- packs: meta.packs
1735
+ packs: meta.packs,
1736
+ ...meta.imports === void 0 ? {} : { imports: pathsOf(meta.imports) }
1376
1737
  });
1377
1738
  });
1739
+ const unchanged = (filePath, deps) => current.has(filePath) && !changed.has(filePath) && !deps.some((d) => changed.has(d) || removed.has(d));
1378
1740
  const validRoots = /* @__PURE__ */ new Set();
1379
1741
  for (const [rootPath, record] of roots) {
1380
- if (!record.cacheable || !current.has(rootPath)) {
1381
- continue;
1742
+ if (record.cacheable && unchanged(rootPath, record.deps)) {
1743
+ validRoots.add(rootPath);
1382
1744
  }
1383
- if (changed.has(rootPath)) {
1384
- continue;
1745
+ }
1746
+ const units = (manifest.units ?? []).flatMap((stored2) => {
1747
+ const file = depPaths[stored2.file];
1748
+ return file === void 0 ? [] : [
1749
+ {
1750
+ key: stored2.key,
1751
+ file,
1752
+ deps: pathsOf(stored2.deps),
1753
+ data: stored2.data
1754
+ }
1755
+ ];
1756
+ });
1757
+ const validUnits = /* @__PURE__ */ new Map();
1758
+ for (const unit of units) {
1759
+ if (unchanged(unit.file, unit.deps)) {
1760
+ validUnits.set(unit.key, unit);
1385
1761
  }
1386
- const depMoved = record.deps.some((d) => changed.has(d) || removed.has(d));
1387
- if (!depMoved) {
1388
- validRoots.add(rootPath);
1762
+ }
1763
+ const resolvedImports = /* @__PURE__ */ new Map();
1764
+ if (added.size === 0 && removed.size === 0) {
1765
+ for (const [rootPath, record] of roots) {
1766
+ if (record.imports !== void 0 && !changed.has(rootPath)) {
1767
+ resolvedImports.set(rootPath, record.imports);
1768
+ }
1389
1769
  }
1390
1770
  }
1391
1771
  const owners = manifest.owners ?? [];
1392
1772
  return {
1393
1773
  changed,
1394
1774
  removed,
1775
+ resolvedImports,
1395
1776
  roots,
1396
1777
  validRoots,
1397
1778
  rootsDeclined,
1779
+ validUnits,
1398
1780
  reuse(valid) {
1399
1781
  const summaries = [];
1400
1782
  const reusedOwners = [];
@@ -1421,7 +1803,8 @@ async function buildPlan(manifest, currentStamps) {
1421
1803
  const p = rootNames[o];
1422
1804
  return p === void 0 ? [] : [p];
1423
1805
  })
1424
- )
1806
+ ),
1807
+ units
1425
1808
  };
1426
1809
  }
1427
1810
  };
@@ -1446,7 +1829,8 @@ function encodeAttribution(attribution) {
1446
1829
  deps: r.deps.map(depIdOf),
1447
1830
  claims: r.claims,
1448
1831
  meta: r.meta,
1449
- packs: r.packs
1832
+ packs: r.packs,
1833
+ ...r.imports === void 0 ? {} : { imports: r.imports.map(depIdOf) }
1450
1834
  }));
1451
1835
  const owners = attribution.owners.map(
1452
1836
  (ownerPaths) => ownerPaths.flatMap((p) => {
@@ -1454,32 +1838,23 @@ function encodeAttribution(attribution) {
1454
1838
  return i === void 0 ? [] : [i];
1455
1839
  })
1456
1840
  );
1457
- return { roots, rootMeta, depPaths, owners };
1458
- }
1459
- async function hashStamps(stamps, previous) {
1460
- const before = new Map((previous?.files ?? []).map((f) => [f.path, f]));
1461
- return Promise.all(
1462
- stamps.map(async (stamp) => {
1463
- const prior = before.get(stamp.path);
1464
- if (prior !== void 0 && prior.contentHash !== void 0 && fileStampEquals(prior, stamp)) {
1465
- return { ...stamp, contentHash: prior.contentHash };
1466
- }
1467
- const contentHash = await hashFile(stamp.path);
1468
- return contentHash === null ? stamp : { ...stamp, contentHash };
1469
- })
1470
- );
1471
- }
1472
- async function hashFile(filePath) {
1473
- try {
1474
- const content = await fs2.readFile(filePath);
1475
- return createHash2("sha256").update(content).digest("hex").slice(0, 16);
1476
- } catch {
1477
- return null;
1478
- }
1841
+ const units = attribution.units?.map((unit) => ({
1842
+ key: unit.key,
1843
+ file: depIdOf(unit.file),
1844
+ deps: unit.deps.map(depIdOf),
1845
+ data: unit.data
1846
+ }));
1847
+ return {
1848
+ roots,
1849
+ rootMeta,
1850
+ depPaths,
1851
+ owners,
1852
+ ...units === void 0 ? {} : { units }
1853
+ };
1479
1854
  }
1480
1855
  async function describeAbsentEntry(cacheDir) {
1481
1856
  try {
1482
- const entries = await fs2.readdir(cacheDir, { withFileTypes: true });
1857
+ const entries = await fs4.readdir(cacheDir, { withFileTypes: true });
1483
1858
  return entries.some((e) => e.isDirectory() && isEntryDir(e.name)) ? "key-changed" : "no-manifest";
1484
1859
  } catch {
1485
1860
  return "no-manifest";
@@ -1491,8 +1866,8 @@ function entryDirFor(cacheDir, input) {
1491
1866
  input.adapterPacksDigest,
1492
1867
  input.configPath ?? ""
1493
1868
  ].join(" ");
1494
- const name = createHash2("sha256").update(key).digest("hex").slice(0, 16);
1495
- return path3.join(cacheDir, `${ENTRY_PREFIX}${name}`);
1869
+ const name = createHash3("sha256").update(key).digest("hex").slice(0, 16);
1870
+ return path5.join(cacheDir, `${ENTRY_PREFIX}${name}`);
1496
1871
  }
1497
1872
  function isEntryDir(name) {
1498
1873
  return ENTRY_DIR_NAME.test(name);
@@ -1500,24 +1875,24 @@ function isEntryDir(name) {
1500
1875
  async function markUsed(entryDir) {
1501
1876
  const now = /* @__PURE__ */ new Date();
1502
1877
  try {
1503
- await fs2.utimes(entryDir, now, now);
1878
+ await fs4.utimes(entryDir, now, now);
1504
1879
  } catch {
1505
1880
  }
1506
1881
  }
1507
1882
  async function evictOldEntries(cacheDir, keepDir) {
1508
1883
  try {
1509
- await fs2.rm(path3.join(cacheDir, "manifest.json"), { force: true });
1510
- const entries = await fs2.readdir(cacheDir, { withFileTypes: true });
1884
+ await fs4.rm(path5.join(cacheDir, "manifest.json"), { force: true });
1885
+ const entries = await fs4.readdir(cacheDir, { withFileTypes: true });
1511
1886
  const dirs = await Promise.all(
1512
1887
  entries.filter((e) => e.isDirectory() && isEntryDir(e.name)).map(async (e) => {
1513
- const dir = path3.join(cacheDir, e.name);
1514
- const stat = await fs2.stat(dir);
1888
+ const dir = path5.join(cacheDir, e.name);
1889
+ const stat = await fs4.stat(dir);
1515
1890
  return { dir, mtimeMs: stat.mtimeMs };
1516
1891
  })
1517
1892
  );
1518
1893
  const doomed = dirs.filter((d) => d.dir !== keepDir).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_ENTRIES - 1);
1519
1894
  await Promise.all(
1520
- doomed.map((d) => fs2.rm(d.dir, { recursive: true, force: true }))
1895
+ doomed.map((d) => fs4.rm(d.dir, { recursive: true, force: true }))
1521
1896
  );
1522
1897
  } catch {
1523
1898
  }
@@ -1528,51 +1903,41 @@ function missDiag(reason) {
1528
1903
  diagnostic: { kind: "miss", missReason: reason }
1529
1904
  };
1530
1905
  }
1531
- async function readManifest(manifestPath) {
1906
+ var ManifestReads = class {
1907
+ last = null;
1908
+ async readAtRunStart(manifestPath) {
1909
+ const stamp = await manifestStamp(manifestPath);
1910
+ const manifest = await readManifest(manifestPath);
1911
+ this.last = { path: manifestPath, stamp, manifest };
1912
+ return manifest;
1913
+ }
1914
+ async readAgain(manifestPath) {
1915
+ const last = this.last;
1916
+ if (last !== null && last.path === manifestPath && last.stamp !== null && last.stamp === await manifestStamp(manifestPath)) {
1917
+ return last.manifest;
1918
+ }
1919
+ return await this.readAtRunStart(manifestPath);
1920
+ }
1921
+ forget() {
1922
+ this.last = null;
1923
+ }
1924
+ };
1925
+ async function manifestStamp(manifestPath) {
1532
1926
  try {
1533
- const raw = await fs2.readFile(manifestPath, "utf-8");
1534
- return JSON.parse(raw);
1927
+ const stat = await fs4.stat(manifestPath);
1928
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
1535
1929
  } catch {
1536
1930
  return null;
1537
1931
  }
1538
1932
  }
1539
- async function stampConfigFile(configPath) {
1540
- if (configPath === void 0) {
1541
- return null;
1542
- }
1933
+ async function readManifest(manifestPath) {
1543
1934
  try {
1544
- const stat = await fs2.stat(configPath);
1545
- return { path: configPath, mtimeMs: stat.mtimeMs, size: stat.size };
1935
+ const raw = await fs4.readFile(manifestPath, "utf-8");
1936
+ return JSON.parse(raw);
1546
1937
  } catch {
1547
1938
  return null;
1548
1939
  }
1549
1940
  }
1550
- async function resolveFileStamps(input) {
1551
- const stamped = await Promise.all(
1552
- input.files.map(async (p) => {
1553
- try {
1554
- const stat = await fs2.stat(p);
1555
- return { path: p, mtimeMs: stat.mtimeMs, size: stat.size };
1556
- } catch {
1557
- return { path: p, mtimeMs: -1, size: -1 };
1558
- }
1559
- })
1560
- );
1561
- stamped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1562
- return stamped;
1563
- }
1564
- function fileStampsEqual(a, b) {
1565
- return a.length === b.length && a.every((s, i) => fileStampEquals(s, b[i]));
1566
- }
1567
- function fileStampEquals(a, b) {
1568
- if (a === null && b === null) {
1569
- return true;
1570
- }
1571
- if (a === null || b === null) {
1572
- return false;
1573
- }
1574
- return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
1575
- }
1576
1941
 
1577
1942
  // src/extractionReport.ts
1578
1943
  import { BOUNDARY_ROLE } from "@suss/behavioral-ir";
@@ -1681,6 +2046,38 @@ function firstEmptyUngatedStage(packs, filesWalked) {
1681
2046
  return "assembly";
1682
2047
  }
1683
2048
 
2049
+ // src/keptParses.ts
2050
+ var KeptParses = class {
2051
+ constructor(release = () => {
2052
+ }) {
2053
+ this.release = release;
2054
+ }
2055
+ release;
2056
+ byFile = /* @__PURE__ */ new Map();
2057
+ async parse(file, source, make) {
2058
+ const kept = this.byFile.get(file);
2059
+ if (kept !== void 0 && kept.source === source) {
2060
+ return kept.value;
2061
+ }
2062
+ const value = await make(source);
2063
+ if (kept !== void 0) {
2064
+ this.release(kept.value);
2065
+ }
2066
+ this.byFile.set(file, { source, value });
2067
+ return value;
2068
+ }
2069
+ /** Lets go of every file not in this run. */
2070
+ keepOnly(files) {
2071
+ const wanted = new Set(files);
2072
+ for (const [file, kept] of this.byFile) {
2073
+ if (!wanted.has(file)) {
2074
+ this.release(kept.value);
2075
+ this.byFile.delete(file);
2076
+ }
2077
+ }
2078
+ }
2079
+ };
2080
+
1684
2081
  // src/packHealth.ts
1685
2082
  function stagesOf(funnel) {
1686
2083
  const stages = [];
@@ -1727,8 +2124,8 @@ function funnelDrops(packs) {
1727
2124
  function recognizersWithNoUnits(packs) {
1728
2125
  return packs.filter(
1729
2126
  (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
1730
- // roots, so effects recognized there mean the run worked and
1731
- // the missing framework pack costs attribution, not existence.
2127
+ // roots, so effects recognized there mean the run worked and only
2128
+ // the framework pack's attribution is missing.
1732
2129
  funnel.effectsRecognized === 0
1733
2130
  ).map((funnel) => ({
1734
2131
  label: funnel.pack,
@@ -1970,7 +2367,7 @@ function makeTransitionId(functionName, branch) {
1970
2367
  const { terminal } = branch;
1971
2368
  const statusKey = terminal.statusCode === null ? "none" : terminal.statusCode.type === "literal" ? String(terminal.statusCode.value) : `dyn:${terminal.statusCode.sourceText}`;
1972
2369
  const conditionSig = branch.conditions.map((c) => `${c.polarity}:${c.sourceText}`).join(";");
1973
- const conditionHash = createHash3("sha1").update(conditionSig).digest("hex").slice(0, 7);
2370
+ const conditionHash = createHash4("sha1").update(conditionSig).digest("hex").slice(0, 7);
1974
2371
  return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
1975
2372
  }
1976
2373
  function collapseReading(table, reading) {
@@ -2040,6 +2437,39 @@ function effectsOfBranch(branch) {
2040
2437
  ];
2041
2438
  return foldRepeatedEffects(effects.map(withNormalizedCallee));
2042
2439
  }
2440
+ function provenanceOf(raw, effects) {
2441
+ if (raw.length === 0) {
2442
+ return [];
2443
+ }
2444
+ const positionOf = new Map(effects.map((one, at) => [effectKey(one), at]));
2445
+ const bySlot = /* @__PURE__ */ new Map();
2446
+ const said = /* @__PURE__ */ new Map();
2447
+ for (const one of raw) {
2448
+ const effect = positionOf.get(effectKey(withNormalizedCallee(one.effect)));
2449
+ if (effect === void 0) {
2450
+ continue;
2451
+ }
2452
+ const slotKey = `${effect}\0${one.slot}\0${one.name}`;
2453
+ let entry = bySlot.get(slotKey);
2454
+ let seen = said.get(slotKey);
2455
+ if (entry === void 0 || seen === void 0) {
2456
+ entry = { at: { slot: one.slot, effect, name: one.name }, from: [] };
2457
+ seen = /* @__PURE__ */ new Set();
2458
+ bySlot.set(slotKey, entry);
2459
+ said.set(slotKey, seen);
2460
+ }
2461
+ for (const source of one.from) {
2462
+ const spelled = JSON.stringify(source);
2463
+ if (!seen.has(spelled)) {
2464
+ seen.add(spelled);
2465
+ entry.from.push(source);
2466
+ }
2467
+ }
2468
+ }
2469
+ return [...bySlot.values()].sort(
2470
+ (a, b) => a.at.effect - b.at.effect || a.at.slot.localeCompare(b.at.slot) || a.at.name.localeCompare(b.at.name)
2471
+ );
2472
+ }
2043
2473
  function withNormalizedCallee(effect) {
2044
2474
  if (effect.type !== "invocation" && effect.type !== "interaction") {
2045
2475
  return effect;
@@ -2055,17 +2485,22 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2055
2485
  const conditions = branch.conditions.map(
2056
2486
  rawConditionToPredicate
2057
2487
  );
2488
+ const effects = effectsOfBranch(branch);
2058
2489
  const transition = {
2059
2490
  id: makeTransitionId(raw.identity.name, branch),
2060
2491
  conditions,
2061
2492
  output: terminalToOutput(branch.terminal),
2062
- effects: effectsOfBranch(branch),
2493
+ effects,
2063
2494
  location: branch.location,
2064
2495
  isDefault: branch.isDefault
2065
2496
  };
2066
2497
  if (branch.expectedInput != null) {
2067
2498
  transition.expectedInput = branch.expectedInput;
2068
2499
  }
2500
+ const provenance = provenanceOf(branch.provenance ?? [], effects);
2501
+ if (provenance.length > 0) {
2502
+ transition.provenance = provenance;
2503
+ }
2069
2504
  return transition;
2070
2505
  });
2071
2506
  const gaps = detectGaps(raw, transitions, options);
@@ -2127,6 +2562,9 @@ function buildMetadata(raw) {
2127
2562
  if (raw.requestSpelling !== void 0) {
2128
2563
  metadata = withRequestSpellingMetadata(metadata, raw.requestSpelling);
2129
2564
  }
2565
+ if (raw.test !== void 0) {
2566
+ metadata = withTestMetadata(metadata, raw.test);
2567
+ }
2130
2568
  return Object.keys(metadata).length > 0 ? metadata : null;
2131
2569
  }
2132
2570
  function buildHttpMetadataValue(raw) {
@@ -2270,18 +2708,27 @@ function bodyToShape(body) {
2270
2708
  }
2271
2709
  return null;
2272
2710
  }
2711
+ function statusValueRef(code) {
2712
+ if (code === null) {
2713
+ return null;
2714
+ }
2715
+ return code.type === "literal" ? { type: "literal", value: code.value } : { type: "unresolved", sourceText: code.sourceText };
2716
+ }
2273
2717
  var terminalConverters = {
2274
2718
  response: (t) => {
2275
- const statusCode = t.statusCode ? t.statusCode.type === "literal" ? { type: "literal", value: t.statusCode.value } : { type: "unresolved", sourceText: t.statusCode.sourceText } : null;
2276
2719
  const body = bodyToShape(t.body);
2277
- return { type: "response", statusCode, body, headers: {} };
2720
+ return {
2721
+ type: "response",
2722
+ statusCode: statusValueRef(t.statusCode),
2723
+ body,
2724
+ headers: {}
2725
+ };
2278
2726
  },
2279
2727
  throw: (t) => {
2280
2728
  if (t.statusCode && t.producesResponse === true) {
2281
- const statusCode = t.statusCode.type === "literal" ? { type: "literal", value: t.statusCode.value } : { type: "unresolved", sourceText: t.statusCode.sourceText };
2282
2729
  return {
2283
2730
  type: "response",
2284
- statusCode,
2731
+ statusCode: statusValueRef(t.statusCode),
2285
2732
  body: bodyToShape(t.body),
2286
2733
  headers: {}
2287
2734
  };
@@ -2309,7 +2756,8 @@ var terminalConverters = {
2309
2756
  type: "return",
2310
2757
  value: bodyToShape(t.body)
2311
2758
  }),
2312
- void: (_t) => ({ type: "void" })
2759
+ void: (_t) => ({ type: "void" }),
2760
+ exit: (t) => ({ type: "exit", code: statusValueRef(t.statusCode) })
2313
2761
  };
2314
2762
  function terminalToOutput(terminal) {
2315
2763
  return terminalConverters[terminal.kind](terminal);
@@ -2352,14 +2800,18 @@ function paramToInput(param) {
2352
2800
  name: param.name,
2353
2801
  position: param.position,
2354
2802
  role: param.role,
2803
+ ...param.field === void 0 ? {} : { field: param.field },
2355
2804
  shape: param.typeText ? { type: "ref", name: param.typeText } : null
2356
2805
  };
2357
2806
  }
2358
2807
  export {
2808
+ EnclosingNodes,
2359
2809
  IdMap,
2360
2810
  IdSet,
2811
+ KeptParses,
2361
2812
  MAX_ENTRIES,
2362
2813
  MAX_PATHS,
2814
+ MODULE_SURFACE_RECOGNITION,
2363
2815
  PathBudgetExceeded,
2364
2816
  SKIP_CHILDREN,
2365
2817
  UnmodeledFlow,
@@ -2384,13 +2836,18 @@ export {
2384
2836
  enumerateOrDegrade,
2385
2837
  enumerateStructuredPaths,
2386
2838
  evaluatePackHealth,
2839
+ extractionConfigStamp,
2387
2840
  firstWrittenReading,
2388
2841
  formatPackHealth,
2389
2842
  guardsHoldOn,
2390
2843
  httpRouteDiscovery,
2844
+ isListedTestFile,
2391
2845
  makeTransitionId,
2392
2846
  mapReading,
2847
+ markReturnsAsExitCode,
2848
+ matchesTestFileName,
2393
2849
  moduleInitStructure,
2850
+ moduleOfFile,
2394
2851
  noopTimer,
2395
2852
  packGradients,
2396
2853
  paramToInput,
@@ -2402,8 +2859,12 @@ export {
2402
2859
  runDigest,
2403
2860
  runsBefore,
2404
2861
  scopeOption,
2862
+ settleModules,
2405
2863
  sharedGatingConditions,
2864
+ sourceRefsOf,
2865
+ stampMayMissAWrite,
2406
2866
  stampModuleImports,
2867
+ stampModules,
2407
2868
  storageSystemOption,
2408
2869
  summaryCountsByPack,
2409
2870
  tallyUnit,