@suss/extractor 0.33.1 → 0.34.1

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,14 @@ 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,
38
+ MAX_HELPER_PATHS: () => MAX_HELPER_PATHS,
36
39
  MAX_PATHS: () => MAX_PATHS,
40
+ MODULE_SURFACE_RECOGNITION: () => MODULE_SURFACE_RECOGNITION,
37
41
  PathBudgetExceeded: () => PathBudgetExceeded,
38
42
  SKIP_CHILDREN: () => SKIP_CHILDREN,
39
43
  UnmodeledFlow: () => UnmodeledFlow,
@@ -58,30 +62,45 @@ __export(index_exports, {
58
62
  enumerateOrDegrade: () => enumerateOrDegrade,
59
63
  enumerateStructuredPaths: () => enumerateStructuredPaths,
60
64
  evaluatePackHealth: () => evaluatePackHealth,
65
+ extractionConfigStamp: () => extractionConfigStamp,
61
66
  firstWrittenReading: () => firstWrittenReading,
62
67
  formatPackHealth: () => formatPackHealth,
63
68
  guardsHoldOn: () => guardsHoldOn,
69
+ helperPathsOnVariables: () => helperPathsOnVariables,
64
70
  httpRouteDiscovery: () => httpRouteDiscovery,
71
+ isListedTestFile: () => isListedTestFile,
72
+ makeIgnoredDirectory: () => makeIgnoredDirectory,
65
73
  makeTransitionId: () => makeTransitionId,
66
74
  mapReading: () => mapReading,
75
+ markReturnsAsExitCode: () => markReturnsAsExitCode,
76
+ matchesTestFileName: () => matchesTestFileName,
77
+ mergeReadShapes: () => mergeReadShapes,
67
78
  moduleInitStructure: () => moduleInitStructure,
79
+ moduleOfFile: () => moduleOfFile,
68
80
  noopTimer: () => noopTimer,
69
81
  packGradients: () => packGradients,
70
82
  paramToInput: () => paramToInput,
71
83
  projectFileStamp: () => projectFileStamp,
72
84
  rawConditionToPredicate: () => rawConditionToPredicate,
73
85
  recordPackFailure: () => recordPackFailure,
86
+ redirectDeliveryWhenSet: () => redirectDeliveryWhenSet,
74
87
  registrationHelperDiscovery: () => registrationHelperDiscovery,
75
88
  routeHelperIndex: () => routeHelperIndex,
76
89
  runDigest: () => runDigest,
77
90
  runsBefore: () => runsBefore,
78
91
  scopeOption: () => scopeOption,
92
+ settleModules: () => settleModules,
79
93
  sharedGatingConditions: () => sharedGatingConditions,
94
+ sourceRefsOf: () => sourceRefsOf,
95
+ splitAtHandOffs: () => splitAtHandOffs,
96
+ stampMayMissAWrite: () => stampMayMissAWrite,
80
97
  stampModuleImports: () => stampModuleImports,
98
+ stampModules: () => stampModules,
81
99
  storageSystemOption: () => storageSystemOption,
82
100
  summaryCountsByPack: () => summaryCountsByPack,
83
101
  tallyUnit: () => tallyUnit,
84
102
  terminalToOutput: () => terminalToOutput,
103
+ throughHelper: () => throughHelper,
85
104
  unreadableReading: () => unreadableReading,
86
105
  unwrapJsonStringify: () => unwrapJsonStringify,
87
106
  valueToReadFurtherFrom: () => valueToReadFurtherFrom,
@@ -90,8 +109,8 @@ __export(index_exports, {
90
109
  writtenReading: () => writtenReading
91
110
  });
92
111
  module.exports = __toCommonJS(index_exports);
93
- var import_node_crypto3 = require("crypto");
94
- var import_behavioral_ir5 = require("@suss/behavioral-ir");
112
+ var import_node_crypto4 = require("crypto");
113
+ var import_behavioral_ir7 = require("@suss/behavioral-ir");
95
114
 
96
115
  // src/contractStatusGaps.ts
97
116
  var import_behavioral_ir = require("@suss/behavioral-ir");
@@ -270,29 +289,82 @@ function composeOne(summary, chain, keepGaps) {
270
289
  if (wrappers.length === 0) {
271
290
  return narrowed;
272
291
  }
273
- const responses = respondedInstead(wrappers);
274
- const handled = handledThrows(wrappers);
275
- const transitions = beside(summary, responses, handled);
292
+ const responses = settledUncaught(respondedInstead(wrappers), wrappers);
293
+ const unitOwn = settledUncaught(summary.transitions, wrappers);
294
+ const own = [...responses, ...unitOwn];
295
+ const handled = handledThrows(wrappers, thrownBy(own));
296
+ const transitions = beside(summary, unitOwn, responses, handled);
276
297
  if (!keepGaps) {
277
298
  return transitions === summary.transitions ? narrowed : { ...narrowed, transitions };
278
299
  }
279
300
  const gaps = withContractStatusGaps(
280
301
  summary,
281
- [...responses, ...summary.transitions],
282
- handled
302
+ own,
303
+ everyHandlerOutcome(wrappers)
283
304
  );
284
305
  if (transitions === summary.transitions && gaps === summary.gaps) {
285
306
  return narrowed;
286
307
  }
287
308
  return { ...narrowed, transitions, gaps };
288
309
  }
289
- function beside(summary, responses, handled) {
290
- const throws = summary.transitions.some((t) => t.output.type === "throw");
291
- const onThrow = throws ? handled : [];
292
- if (responses.length === 0 && onThrow.length === 0) {
310
+ function beside(summary, unitOwn, responses, handled) {
311
+ if (responses.length === 0 && handled.length === 0 && unitOwn === summary.transitions) {
293
312
  return summary.transitions;
294
313
  }
295
- return withDistinctIds([...responses, ...summary.transitions, ...onThrow]);
314
+ return withDistinctIds([...responses, ...unitOwn, ...handled]);
315
+ }
316
+ function settledUncaught(transitions, wrappers) {
317
+ const handlers = wrappers.filter(
318
+ (wrapper) => wrapper.reference.onThrow === true
319
+ );
320
+ let changed = false;
321
+ const settled = transitions.flatMap((transition) => {
322
+ const { output } = transition;
323
+ if (output.type !== "throw") {
324
+ return [transition];
325
+ }
326
+ const caught = caughtOnRoute(handlers, output);
327
+ if (caught === "sure") {
328
+ changed = true;
329
+ return [markedCaught(transition)];
330
+ }
331
+ if (output.statusWhenUncaught === void 0) {
332
+ return [transition];
333
+ }
334
+ changed = true;
335
+ const response = {
336
+ ...transition,
337
+ output: {
338
+ type: "response",
339
+ statusCode: { type: "literal", value: output.statusWhenUncaught },
340
+ body: null,
341
+ headers: {}
342
+ }
343
+ };
344
+ return caught === "uncertain" ? [transition, response] : [response];
345
+ });
346
+ return changed ? settled : transitions;
347
+ }
348
+ function markedCaught(transition) {
349
+ return {
350
+ ...transition,
351
+ metadata: (0, import_behavioral_ir2.withWrapperMetadata)(transition.metadata, {
352
+ ...(0, import_behavioral_ir2.readWrapperMetadata)(transition),
353
+ caught: true
354
+ })
355
+ };
356
+ }
357
+ function caughtOnRoute(handlers, thrown) {
358
+ if (handlers.some((handler) => handler.reference.catches === void 0)) {
359
+ return "sure";
360
+ }
361
+ const matches = handlers.map(
362
+ (handler) => catchMatch(handler.reference, thrown)
363
+ );
364
+ if (matches.includes("yes")) {
365
+ return "sure";
366
+ }
367
+ return matches.includes("maybe") ? "uncertain" : "no";
296
368
  }
297
369
  function respondedInstead(wrappers) {
298
370
  return wrappers.flatMap(
@@ -304,11 +376,91 @@ function respondedInstead(wrappers) {
304
376
  )
305
377
  );
306
378
  }
307
- function handledThrows(wrappers) {
379
+ function thrownBy(transitions) {
380
+ return transitions.flatMap(
381
+ (t) => t.output.type === "throw" ? [t.output] : []
382
+ );
383
+ }
384
+ function everyHandlerOutcome(wrappers) {
308
385
  return wrappers.flatMap(
309
386
  (wrapper) => wrapper.reference.onThrow === true ? attribute(wrapper.summary.transitions, wrapper.reference) : []
310
387
  );
311
388
  }
389
+ function handledThrows(wrappers, thrown) {
390
+ const caught = catchesAny(wrappers, thrown);
391
+ return [...oncePerHandler(wrappers, caught)].flatMap(
392
+ ([wrapper, how]) => attribute(
393
+ wrapper.summary.transitions,
394
+ wrapper.reference,
395
+ how === "uncertain"
396
+ )
397
+ );
398
+ }
399
+ function oncePerHandler(wrappers, caught) {
400
+ const byHandler = /* @__PURE__ */ new Map();
401
+ for (const wrapper of wrappers) {
402
+ const how = caught.get(wrapper);
403
+ if (how === void 0) {
404
+ continue;
405
+ }
406
+ const earlier = byHandler.get(wrapper.summary);
407
+ if (earlier === void 0 || earlier[1] === "uncertain" && how === "sure") {
408
+ byHandler.set(wrapper.summary, [wrapper, how]);
409
+ }
410
+ }
411
+ return new Map(byHandler.values());
412
+ }
413
+ function catchesAny(wrappers, thrown) {
414
+ const caught = /* @__PURE__ */ new Map();
415
+ if (thrown.length === 0) {
416
+ return caught;
417
+ }
418
+ const handlers = wrappers.filter(
419
+ (wrapper) => wrapper.reference.onThrow === true
420
+ );
421
+ for (const handler of handlers) {
422
+ if (handler.reference.catches === void 0) {
423
+ caught.set(handler, "sure");
424
+ }
425
+ }
426
+ const listing = handlers.filter(
427
+ (handler) => handler.reference.catches !== void 0
428
+ );
429
+ for (const one of thrown) {
430
+ for (const [handler, how] of catchersOf(listing, one)) {
431
+ if (caught.get(handler) !== "sure") {
432
+ caught.set(handler, how);
433
+ }
434
+ }
435
+ }
436
+ return caught;
437
+ }
438
+ function catchersOf(handlers, thrown) {
439
+ const found = [];
440
+ for (const handler of handlers) {
441
+ const match = catchMatch(handler.reference, thrown);
442
+ if (match === "yes") {
443
+ found.push([handler, found.length === 0 ? "sure" : "uncertain"]);
444
+ return found;
445
+ }
446
+ if (match === "maybe") {
447
+ found.push([handler, "uncertain"]);
448
+ }
449
+ }
450
+ return found;
451
+ }
452
+ function catchMatch(reference, thrown) {
453
+ const ancestors = thrown.exceptionAncestors;
454
+ if (thrown.exceptionType === null || ancestors === void 0) {
455
+ return "maybe";
456
+ }
457
+ const classes = /* @__PURE__ */ new Set([thrown.exceptionType, ...ancestors]);
458
+ if ((reference.catches ?? []).some((name) => classes.has(name))) {
459
+ return "yes";
460
+ }
461
+ const unread = thrown.ancestryIncomplete === true && reference.mayCatchUnreadClasses === true;
462
+ return unread || reference.mayCatchAny === true ? "maybe" : "no";
463
+ }
312
464
  function withContractStatusGaps(summary, composed, handled) {
313
465
  const contract = (0, import_behavioral_ir2.readHttpMetadata)(summary)?.declaredContract;
314
466
  const binding = summary.identity.boundaryBinding;
@@ -339,10 +491,13 @@ function coversUnit(reference, summary) {
339
491
  const binding = summary.identity.boundaryBinding;
340
492
  return binding !== null && (0, import_behavioral_ir2.withinScope)(binding, reference.scope);
341
493
  }
342
- function attribute(transitions, reference) {
494
+ function attribute(transitions, reference, catchUncertain = false) {
343
495
  return transitions.map((transition) => ({
344
496
  ...transition,
345
- metadata: (0, import_behavioral_ir2.withWrapperMetadata)(transition.metadata, { from: reference })
497
+ metadata: (0, import_behavioral_ir2.withWrapperMetadata)(transition.metadata, {
498
+ from: reference,
499
+ ...catchUncertain ? { catchUncertain } : {}
500
+ })
346
501
  }));
347
502
  }
348
503
  function withDistinctIds(transitions) {
@@ -354,6 +509,56 @@ function withDistinctIds(transitions) {
354
509
  });
355
510
  }
356
511
 
512
+ // src/declaredModules.ts
513
+ var import_node_fs = __toESM(require("fs"), 1);
514
+ var import_node_path = __toESM(require("path"), 1);
515
+ var MODULE_SURFACE_RECOGNITION = "modules";
516
+ function settleModules(modules, candidates) {
517
+ return (modules ?? []).map((module2) => ({
518
+ ...module2,
519
+ public: module2.public ?? candidates.map(
520
+ (one) => import_node_path.default.join(module2.root, one.replace("{name}", module2.name))
521
+ ).filter((file) => import_node_fs.default.existsSync(file))
522
+ }));
523
+ }
524
+ function isInside(file, root) {
525
+ const relative = import_node_path.default.relative(root, file);
526
+ return relative !== "" && !relative.startsWith("..") && !import_node_path.default.isAbsolute(relative);
527
+ }
528
+ function moduleOfFile(modules, file) {
529
+ const byPublicFile = modules.find((module2) => module2.public.includes(file));
530
+ if (byPublicFile !== void 0) {
531
+ return byPublicFile;
532
+ }
533
+ let deepest;
534
+ for (const module2 of modules) {
535
+ if (isInside(file, module2.root) && (deepest === void 0 || module2.root.length > deepest.root.length)) {
536
+ deepest = module2;
537
+ }
538
+ }
539
+ return deepest;
540
+ }
541
+ function stampModules(summaries, modules, root) {
542
+ if (modules.length === 0) {
543
+ return;
544
+ }
545
+ for (const summary of summaries) {
546
+ const file = import_node_path.default.resolve(root ?? "", summary.location.file);
547
+ const module2 = moduleOfFile(modules, file);
548
+ if (module2 !== void 0) {
549
+ summary.location.module = module2.name;
550
+ }
551
+ }
552
+ }
553
+ function modulesStamp(modules) {
554
+ if (modules === void 0 || modules.length === 0) {
555
+ return void 0;
556
+ }
557
+ return JSON.stringify(
558
+ modules.map((module2) => [module2.name, module2.root, [...module2.public]])
559
+ );
560
+ }
561
+
357
562
  // src/effectGuards.ts
358
563
  var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
359
564
  var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
@@ -370,6 +575,139 @@ function runsBefore(effectLine, terminalEndLine) {
370
575
  return effectLine <= terminalEndLine;
371
576
  }
372
577
 
578
+ // src/exitCode.ts
579
+ var import_node_path2 = __toESM(require("path"), 1);
580
+ var import_behavioral_ir3 = require("@suss/behavioral-ir");
581
+ function markReturnsAsExitCode(summaries, returnsTheCode, workspaceRoot) {
582
+ for (const summary of summaries) {
583
+ const span = summary.location.span;
584
+ const file = workspaceRoot === void 0 ? summary.location.file : import_node_path2.default.resolve(workspaceRoot, summary.location.file);
585
+ markExitCodeFrom(
586
+ summary,
587
+ span !== void 0 && returnsTheCode.has((0, import_behavioral_ir3.declarationKey)(file, span))
588
+ );
589
+ }
590
+ }
591
+ function markExitCodeFrom(summary, marked) {
592
+ const process2 = summary.metadata?.process;
593
+ if (marked) {
594
+ summary.metadata = {
595
+ ...summary.metadata ?? {},
596
+ process: { ...process2 ?? {}, exitCodeFrom: "return" }
597
+ };
598
+ return;
599
+ }
600
+ if (process2 === void 0 || !("exitCodeFrom" in process2)) {
601
+ return;
602
+ }
603
+ const { exitCodeFrom: _dropped, ...rest } = process2;
604
+ const { process: _process, ...others } = summary.metadata ?? {};
605
+ const left = Object.keys(rest).length === 0 ? others : { ...others, process: rest };
606
+ if (Object.keys(left).length === 0) {
607
+ Reflect.deleteProperty(summary, "metadata");
608
+ return;
609
+ }
610
+ summary.metadata = left;
611
+ }
612
+
613
+ // src/helperHandOff.ts
614
+ var import_behavioral_ir4 = require("@suss/behavioral-ir");
615
+ var MAX_HELPER_PATHS = 16;
616
+ function throughHelper(branch, at, helperBranches) {
617
+ return helperBranches.map((helper) => {
618
+ const conditions = [...branch.conditions, ...helper.conditions];
619
+ const expectedInput = mergeReadShapes(
620
+ branch.expectedInput ?? null,
621
+ helper.expectedInput ?? null
622
+ );
623
+ const reads = expectedInput === null ? {} : { expectedInput };
624
+ if (helper.terminal.kind !== "throw" && helper.terminal.kind !== "exit") {
625
+ return {
626
+ ...branch,
627
+ ...reads,
628
+ conditions,
629
+ isDefault: branch.isDefault && helper.isDefault
630
+ };
631
+ }
632
+ return {
633
+ ...branch,
634
+ ...reads,
635
+ conditions,
636
+ terminal: { ...helper.terminal, location: at },
637
+ location: at,
638
+ isDefault: false
639
+ };
640
+ });
641
+ }
642
+ function mergeReadShapes(a, b) {
643
+ if (a === null || b === null) {
644
+ return a ?? b;
645
+ }
646
+ if (a.type !== "record" || b.type !== "record") {
647
+ return a.type === "record" ? a : b;
648
+ }
649
+ const properties = { ...a.properties };
650
+ for (const [name, shape] of Object.entries(b.properties)) {
651
+ const known = properties[name];
652
+ properties[name] = known === void 0 ? shape : mergeReadShapes(known, shape) ?? shape;
653
+ }
654
+ return { ...a, properties };
655
+ }
656
+ function splitAtHandOffs(branch, sites, from = 0) {
657
+ for (let index = from; index < sites.length; index++) {
658
+ const site = sites[index];
659
+ const reached = site.alwaysRuns === true || runsBefore(site.line, branch.terminal.location.end);
660
+ if (!reached || !guardsHoldOn(site.preconditions, branch.conditions)) {
661
+ continue;
662
+ }
663
+ return throughHelper(branch, site.at, site.helper).flatMap(
664
+ (path7) => path7.terminal === branch.terminal ? splitAtHandOffs(path7, sites, index + 1) : [path7]
665
+ );
666
+ }
667
+ return [branch];
668
+ }
669
+ function helperPathsOnVariables(helperBranches, passed) {
670
+ const renamed = helperBranches.map((branch) => ({
671
+ ...branch,
672
+ conditions: branch.conditions.map((condition) => ({
673
+ ...condition,
674
+ structured: condition.structured === null ? null : onCallerVariables(condition.structured, passed)
675
+ }))
676
+ }));
677
+ const variables = new Set(passed.values());
678
+ const testsWhatItGot = renamed.some(
679
+ (branch) => branch.conditions.some(
680
+ (condition) => condition.structured !== null && (0, import_behavioral_ir4.predicateRefs)(condition.structured).some(
681
+ (ref) => variables.has(rootName(ref) ?? "")
682
+ )
683
+ )
684
+ );
685
+ return testsWhatItGot && renamed.length <= MAX_HELPER_PATHS ? renamed : null;
686
+ }
687
+ function onCallerVariables(predicate, passed) {
688
+ const rename = (ref) => {
689
+ if (ref.type === "derived") {
690
+ return { ...ref, from: rename(ref.from) };
691
+ }
692
+ if (ref.type === "input") {
693
+ const name = passed.get(ref.inputRef);
694
+ return name === void 0 ? ref : { type: "dependency", name, accessChain: ref.path };
695
+ }
696
+ if (ref.type === "dependency") {
697
+ const name = passed.get(ref.name);
698
+ return name === void 0 ? ref : { ...ref, name };
699
+ }
700
+ return ref;
701
+ };
702
+ return (0, import_behavioral_ir4.replacePredicateRefs)(predicate, rename);
703
+ }
704
+ function rootName(ref) {
705
+ if (ref.type === "derived") {
706
+ return rootName(ref.from);
707
+ }
708
+ return ref.type === "dependency" ? ref.name : null;
709
+ }
710
+
373
711
  // src/moduleImports.ts
374
712
  function stampModuleImports(summaries, importsOf) {
375
713
  const byFile = /* @__PURE__ */ new Map();
@@ -446,8 +784,8 @@ function httpRouteDiscovery(opts) {
446
784
  registrationChain: [...opts.methods]
447
785
  },
448
786
  bindingExtraction: {
449
- // `.all` registers every method, so it records "*", which the pairing
450
- // engine treats as agreeing with any method at all.
787
+ // `.all` registers every method, so it records "*", which pairs with
788
+ // any method.
451
789
  method: {
452
790
  type: "fromRegistration",
453
791
  position: "methodName",
@@ -463,10 +801,9 @@ function httpRouteDiscovery(opts) {
463
801
  }
464
802
  return [
465
803
  ...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.
804
+ // Routes registered in a loop over an array of specs, which the calls
805
+ // above cannot see. The receiver must come from the same import, so a
806
+ // loop that never touches this library's routable is left alone.
470
807
  {
471
808
  kind,
472
809
  match: {
@@ -613,7 +950,7 @@ var configuredCallOption = import_zod.z.object({
613
950
  receiver: import_zod.z.string(),
614
951
  /** Method that performs the send. */
615
952
  method: import_zod.z.string(),
616
- /** Argument index carrying the subject. */
953
+ /** Index of the argument that is the subject. */
617
954
  subjectArg: import_zod.z.number(),
618
955
  /**
619
956
  * Which argument the message body is. Left out when the method has no
@@ -624,7 +961,7 @@ var configuredCallOption = import_zod.z.object({
624
961
  }).strict();
625
962
 
626
963
  // src/paths/enumeratePaths.ts
627
- var import_behavioral_ir3 = require("@suss/behavioral-ir");
964
+ var import_behavioral_ir5 = require("@suss/behavioral-ir");
628
965
  var MAX_PATHS = 256;
629
966
  var UnmodeledFlow = class extends Error {
630
967
  };
@@ -668,12 +1005,12 @@ var loopCompletedCond = (headerText, exit) => fixedCond(
668
1005
  exit === "throw" ? "earlyThrow" : "earlyReturn"
669
1006
  );
670
1007
  var catchEntryCond = () => fixedCond(
671
- { sourceText: import_behavioral_ir3.CATCH_ENTRY_TEXT, expression: null },
1008
+ { sourceText: import_behavioral_ir5.CATCH_ENTRY_TEXT, expression: null },
672
1009
  "positive",
673
1010
  "catchBlock"
674
1011
  );
675
- function classify(path4, home, isAncestorOrSelf) {
676
- return path4.map((cond) => {
1012
+ function classify(path7, home, isAncestorOrSelf) {
1013
+ return path7.map((cond) => {
677
1014
  if (cond.branchStmt === null) {
678
1015
  return cond.info;
679
1016
  }
@@ -738,9 +1075,16 @@ function chargeBudget(state) {
738
1075
  );
739
1076
  }
740
1077
  }
741
- function recordTerminal(ctx, terminal, home, path4) {
1078
+ function limitOpenPaths(paths) {
1079
+ if (paths.length > MAX_PATHS) {
1080
+ throw new PathBudgetExceeded(
1081
+ `path budget exceeded, more than ${MAX_PATHS} paths`
1082
+ );
1083
+ }
1084
+ }
1085
+ function recordTerminal(ctx, terminal, home, path7) {
742
1086
  chargeBudget(ctx.state);
743
- const info = classify(path4, home, ctx.isAncestorOrSelf);
1087
+ const info = classify(path7, home, ctx.isAncestorOrSelf);
744
1088
  const existing = ctx.state.byTerminal.get(terminal) ?? [];
745
1089
  existing.push(info);
746
1090
  ctx.state.byTerminal.set(terminal, existing);
@@ -751,15 +1095,15 @@ function containsTerminal(ctx, stmt) {
751
1095
  }
752
1096
  return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
753
1097
  }
754
- function stepIf(stmt, path4, ctx) {
1098
+ function stepIf(stmt, path7, ctx) {
755
1099
  const thenExit = exitKindOfList(stmt.thenBody);
756
1100
  const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
757
1101
  const thenPath = [
758
- ...path4,
1102
+ ...path7,
759
1103
  branchCond(stmt, stmt.condition, "positive", elseExit)
760
1104
  ];
761
1105
  const elsePath = [
762
- ...path4,
1106
+ ...path7,
763
1107
  branchCond(
764
1108
  stmt,
765
1109
  stmt.condition,
@@ -776,7 +1120,7 @@ function stepIf(stmt, path4, ctx) {
776
1120
  }
777
1121
  return out;
778
1122
  }
779
- function stepSwitch(stmt, path4, ctx) {
1123
+ function stepSwitch(stmt, path7, ctx) {
780
1124
  const out = [];
781
1125
  const negations = [];
782
1126
  let defaultRanBody = false;
@@ -786,7 +1130,7 @@ function stepSwitch(stmt, path4, ctx) {
786
1130
  if (hasStrayBreak(group.body)) {
787
1131
  throw new UnmodeledFlow("non-trailing break in switch clause");
788
1132
  }
789
- const groupPath = group.condition === null ? [...path4, ...negations] : [...path4, fixedCond(group.condition, "positive", "explicit")];
1133
+ const groupPath = group.condition === null ? [...path7, ...negations] : [...path7, fixedCond(group.condition, "positive", "explicit")];
790
1134
  const conts = enumerate(ctx, group.body, groupPath);
791
1135
  if (conts.length > 0) {
792
1136
  if (!group.hasTrailingBreak && !fallthroughSafe) {
@@ -803,24 +1147,24 @@ function stepSwitch(stmt, path4, ctx) {
803
1147
  }
804
1148
  }
805
1149
  if (!defaultRanBody) {
806
- out.push([...path4, ...negations]);
1150
+ out.push([...path7, ...negations]);
807
1151
  }
808
1152
  return out;
809
1153
  }
810
- function stepLoop(stmt, path4, ctx) {
1154
+ function stepLoop(stmt, path7, ctx) {
811
1155
  const bodyExit = exitKindOfList(stmt.body);
812
1156
  if (containsTerminal(ctx, stmt)) {
813
1157
  enumerate(ctx, stmt.body, [
814
- ...path4,
1158
+ ...path7,
815
1159
  loopIterationCond(stmt.condition.sourceText)
816
1160
  ]);
817
1161
  }
818
1162
  if (bodyExit !== null) {
819
- return [[...path4, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
1163
+ return [[...path7, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
820
1164
  }
821
- return [path4];
1165
+ return [path7];
822
1166
  }
823
- function stepTry(stmt, path4, ctx) {
1167
+ function stepTry(stmt, path7, ctx) {
824
1168
  if (stmt.finallyBody !== null) {
825
1169
  const finallyHasTerminal = stmt.finallyBody.some(
826
1170
  (s) => containsTerminal(ctx, s)
@@ -830,22 +1174,22 @@ function stepTry(stmt, path4, ctx) {
830
1174
  }
831
1175
  }
832
1176
  const out = [];
833
- out.push(...enumerate(ctx, stmt.tryBody, path4));
1177
+ out.push(...enumerate(ctx, stmt.tryBody, path7));
834
1178
  if (stmt.catchBody !== null) {
835
- const catchPath = [...path4, catchEntryCond()];
1179
+ const catchPath = [...path7, catchEntryCond()];
836
1180
  out.push(...enumerate(ctx, stmt.catchBody, catchPath));
837
1181
  }
838
1182
  return out;
839
1183
  }
840
- function stepExit(stmt, path4, ctx) {
1184
+ function stepExit(stmt, path7, ctx) {
841
1185
  const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
842
1186
  if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
843
- collector.push(path4);
1187
+ collector.push(path7);
844
1188
  }
845
1189
  return [];
846
1190
  }
847
- function stepOpaque(_stmt, path4, _ctx) {
848
- return [path4];
1191
+ function stepOpaque(_stmt, path7, _ctx) {
1192
+ return [path7];
849
1193
  }
850
1194
  var stepHandlers = {
851
1195
  if: stepIf,
@@ -855,22 +1199,23 @@ var stepHandlers = {
855
1199
  exit: stepExit,
856
1200
  opaque: stepOpaque
857
1201
  };
858
- function stepCallback(ctx, body, path4) {
1202
+ function stepCallback(ctx, body, path7) {
859
1203
  const exits = [];
860
1204
  ctx.state.callbackExits.push(exits);
861
1205
  let fellThrough;
862
1206
  try {
863
- fellThrough = enumerate(ctx, body, path4);
1207
+ fellThrough = enumerate(ctx, body, path7);
864
1208
  } finally {
865
1209
  ctx.state.callbackExits.pop();
866
1210
  }
867
1211
  const out = [...fellThrough, ...exits];
868
- return out.length === 0 ? [path4] : out;
1212
+ return out.length === 0 ? [path7] : out;
869
1213
  }
870
- function stepStatement(ctx, stmt, path4) {
871
- let paths = [path4];
1214
+ function stepStatement(ctx, stmt, path7) {
1215
+ let paths = [path7];
872
1216
  for (const body of stmt.callbacks ?? []) {
873
1217
  paths = paths.flatMap((each) => stepCallback(ctx, body, each));
1218
+ limitOpenPaths(paths);
874
1219
  }
875
1220
  const ownTerminals = ctx.terminalsByStmt.get(stmt) ?? [];
876
1221
  for (const each of withRejoined(paths)) {
@@ -887,7 +1232,7 @@ function stepStatement(ctx, stmt, path4) {
887
1232
  )
888
1233
  );
889
1234
  }
890
- function soleDisagreement(left, right) {
1235
+ function soleDisagreement(left, right, keepsArms) {
891
1236
  if (left.length !== right.length) {
892
1237
  return null;
893
1238
  }
@@ -906,9 +1251,10 @@ function soleDisagreement(left, right) {
906
1251
  }
907
1252
  found = at;
908
1253
  }
909
- return found;
1254
+ const test = found === null ? null : left[found]?.info.expression ?? null;
1255
+ return test !== null && keepsArms(test) ? null : found;
910
1256
  }
911
- function mergeRejoined(paths) {
1257
+ function mergeRejoined(paths, keepsArms = KEEPS_NO_ARMS) {
912
1258
  let current = paths;
913
1259
  for (let round = 0; round < current.length; round++) {
914
1260
  const out = [];
@@ -923,7 +1269,11 @@ function mergeRejoined(paths) {
923
1269
  if (taken.has(j)) {
924
1270
  continue;
925
1271
  }
926
- const at = soleDisagreement(combined, current[j]);
1272
+ const at = soleDisagreement(
1273
+ combined,
1274
+ current[j],
1275
+ keepsArms
1276
+ );
927
1277
  if (at === null) {
928
1278
  continue;
929
1279
  }
@@ -949,21 +1299,18 @@ function enumerate(ctx, stmts, prefix) {
949
1299
  let frontiers = [[...prefix]];
950
1300
  for (const stmt of stmts) {
951
1301
  const nextFrontiers = [];
952
- for (const path4 of mergeRejoined(frontiers)) {
953
- nextFrontiers.push(...stepStatement(ctx, stmt, path4));
1302
+ for (const path7 of mergeRejoined(frontiers, ctx.keepsArms)) {
1303
+ nextFrontiers.push(...stepStatement(ctx, stmt, path7));
954
1304
  }
955
1305
  frontiers = nextFrontiers;
956
- if (frontiers.length > MAX_PATHS) {
957
- throw new PathBudgetExceeded(
958
- `path budget exceeded, more than ${MAX_PATHS} paths`
959
- );
960
- }
1306
+ limitOpenPaths(frontiers);
961
1307
  if (frontiers.length === 0) {
962
1308
  break;
963
1309
  }
964
1310
  }
965
1311
  return frontiers;
966
1312
  }
1313
+ var KEEPS_NO_ARMS = () => false;
967
1314
  function buildParentMap(statements) {
968
1315
  const parentOf = /* @__PURE__ */ new Map();
969
1316
  const visit = (stmt, parent) => {
@@ -997,13 +1344,14 @@ function enumerateStructuredPaths(input) {
997
1344
  };
998
1345
  const ctx = {
999
1346
  terminalsByStmt: input.terminalsByStmt,
1347
+ keepsArms: input.keepsArms ?? KEEPS_NO_ARMS,
1000
1348
  state,
1001
1349
  isAncestorOrSelf
1002
1350
  };
1003
1351
  const continuations = withRejoined(enumerate(ctx, input.statements, []));
1004
- for (const path4 of continuations) {
1352
+ for (const path7 of continuations) {
1005
1353
  chargeBudget(state);
1006
- state.fallthrough.push(classify(path4, null, isAncestorOrSelf));
1354
+ state.fallthrough.push(classify(path7, null, isAncestorOrSelf));
1007
1355
  }
1008
1356
  return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
1009
1357
  }
@@ -1046,8 +1394,8 @@ function sharedGatingConditions(paths, readPredicate) {
1046
1394
  return [];
1047
1395
  }
1048
1396
  const shared = new Set(first.map(keyOf2));
1049
- for (const path4 of rest) {
1050
- const here = new Set(path4.map(keyOf2));
1397
+ for (const path7 of rest) {
1398
+ const here = new Set(path7.map(keyOf2));
1051
1399
  for (const value of shared) {
1052
1400
  if (!here.has(value)) {
1053
1401
  shared.delete(value);
@@ -1077,7 +1425,7 @@ var IdSet = class {
1077
1425
  has(node) {
1078
1426
  return this.byKey.has(node.id);
1079
1427
  }
1080
- /** The node this set was built with, which is the caller's own handle for it. */
1428
+ /** The wrapper that was added, which may be a different object from `node`. */
1081
1429
  get(node) {
1082
1430
  return this.byKey.get(node.id);
1083
1431
  }
@@ -1108,39 +1456,81 @@ var IdMap = class {
1108
1456
  }
1109
1457
  };
1110
1458
 
1459
+ // src/paths/nodeReads.ts
1460
+ var EnclosingNodes = class {
1461
+ constructor(types) {
1462
+ this.types = types;
1463
+ }
1464
+ types;
1465
+ byTree = /* @__PURE__ */ new WeakMap();
1466
+ /**
1467
+ * The nearest ancestor of one of the types, or null when there is none.
1468
+ * Every node passed on the way up has the same result, so each one is
1469
+ * kept, and a later read from a sibling stops one step up.
1470
+ */
1471
+ of(node) {
1472
+ const kept = this.keptFor(node.tree);
1473
+ const known = kept.get(node.id);
1474
+ if (known !== void 0) {
1475
+ return known;
1476
+ }
1477
+ const passed = [node.id];
1478
+ let current = node.parent;
1479
+ while (current !== null && !this.types.has(current.type)) {
1480
+ const above = kept.get(current.id);
1481
+ if (above !== void 0) {
1482
+ current = above;
1483
+ break;
1484
+ }
1485
+ passed.push(current.id);
1486
+ current = current.parent;
1487
+ }
1488
+ for (const id of passed) {
1489
+ kept.set(id, current);
1490
+ }
1491
+ return current;
1492
+ }
1493
+ keptFor(tree) {
1494
+ const known = this.byTree.get(tree);
1495
+ if (known !== void 0) {
1496
+ return known;
1497
+ }
1498
+ const fresh = /* @__PURE__ */ new Map();
1499
+ this.byTree.set(tree, fresh);
1500
+ return fresh;
1501
+ }
1502
+ };
1503
+
1111
1504
  // src/paths/nodeWalk.ts
1112
1505
  var SKIP_CHILDREN = /* @__PURE__ */ Symbol("skipChildren");
1113
- function walkDescendants(root, carried, visitor) {
1114
- for (const child of root.namedChildren) {
1115
- if (child === null) {
1116
- continue;
1117
- }
1506
+ function walkDescendants(root, carried, childrenOf2, visitor) {
1507
+ for (const child of childrenOf2(root)) {
1118
1508
  visitor.at(child, carried);
1119
1509
  const inner = visitor.into(child, carried);
1120
1510
  if (inner !== SKIP_CHILDREN) {
1121
- walkDescendants(child, inner, visitor);
1511
+ walkDescendants(child, inner, childrenOf2, visitor);
1122
1512
  }
1123
1513
  }
1124
1514
  }
1125
1515
 
1126
1516
  // src/projectRoot.ts
1127
- var import_node_path = __toESM(require("path"), 1);
1517
+ var import_node_path3 = __toESM(require("path"), 1);
1128
1518
  function commonDirectoryOf(files) {
1129
- const absolute = files.filter((f) => import_node_path.default.isAbsolute(f));
1519
+ const absolute = files.filter((f) => import_node_path3.default.isAbsolute(f));
1130
1520
  const first = absolute[0];
1131
1521
  if (first === void 0) {
1132
1522
  return void 0;
1133
1523
  }
1134
- let shared = import_node_path.default.dirname(first).split(import_node_path.default.sep);
1524
+ let shared = import_node_path3.default.dirname(first).split(import_node_path3.default.sep);
1135
1525
  for (const file of absolute.slice(1)) {
1136
- const parts = import_node_path.default.dirname(file).split(import_node_path.default.sep);
1526
+ const parts = import_node_path3.default.dirname(file).split(import_node_path3.default.sep);
1137
1527
  let i = 0;
1138
1528
  while (i < shared.length && i < parts.length && shared[i] === parts[i]) {
1139
1529
  i += 1;
1140
1530
  }
1141
1531
  shared = shared.slice(0, i);
1142
1532
  }
1143
- const joined = shared.join(import_node_path.default.sep);
1533
+ const joined = shared.join(import_node_path3.default.sep);
1144
1534
  return joined.length > 1 ? joined : void 0;
1145
1535
  }
1146
1536
 
@@ -1197,11 +1587,84 @@ function valueToReadFurtherFrom(reading) {
1197
1587
  return reading.kind === "written" ? reading.value : null;
1198
1588
  }
1199
1589
 
1590
+ // src/redirectOption.ts
1591
+ function redirectDeliveryWhenSet(option, value) {
1592
+ return option.handsBack.includes(value) ? "response" : "followed";
1593
+ }
1594
+
1595
+ // src/testFiles.ts
1596
+ function isListedTestFile(file, listed) {
1597
+ if (listed === void 0) {
1598
+ return true;
1599
+ }
1600
+ const whole = withForwardSlashes(file);
1601
+ return listed.some((entry) => {
1602
+ const tail = withForwardSlashes(entry).replace(/^\.\//, "");
1603
+ return whole === tail || whole.endsWith(`/${tail}`);
1604
+ });
1605
+ }
1606
+ function matchesTestFileName(file, patterns) {
1607
+ const base = withForwardSlashes(file).split("/").pop() ?? "";
1608
+ return patterns.some((pattern) => globToRegExp(pattern).test(base));
1609
+ }
1610
+ function globToRegExp(pattern) {
1611
+ const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1612
+ return new RegExp(`^${escaped}$`);
1613
+ }
1614
+ function withForwardSlashes(file) {
1615
+ return file.replace(/\\/g, "/");
1616
+ }
1617
+
1618
+ // src/valueSources.ts
1619
+ var QUOTED_LENGTH = 80;
1620
+ function sourceRefsOf(leaves, spelling) {
1621
+ const refs = [];
1622
+ const said = /* @__PURE__ */ new Set();
1623
+ for (const leaf of leaves) {
1624
+ const ref = convertedFrom(refOf(leaf, spelling), leaf.conversions);
1625
+ const spelled = JSON.stringify(ref);
1626
+ if (!said.has(spelled)) {
1627
+ said.add(spelled);
1628
+ refs.push(ref);
1629
+ }
1630
+ }
1631
+ return refs;
1632
+ }
1633
+ function convertedFrom(ref, conversions) {
1634
+ return conversions.reduceRight(
1635
+ (from, method) => ({
1636
+ type: "derived",
1637
+ from,
1638
+ derivation: { type: "methodCall", method, args: [] }
1639
+ }),
1640
+ ref
1641
+ );
1642
+ }
1643
+ function refOf(leaf, spelling) {
1644
+ const whole = leaf.computedAt === null;
1645
+ const input = whole ? spelling.inputOf(leaf) : null;
1646
+ if (input !== null) {
1647
+ return input;
1648
+ }
1649
+ if (whole && leaf.end.is === "written" && leaf.path.length === 0) {
1650
+ const literal = spelling.literalAt(leaf.key);
1651
+ if (literal !== void 0) {
1652
+ return { type: "literal", value: literal };
1653
+ }
1654
+ }
1655
+ const at = leaf.computedAt ?? leaf.key;
1656
+ return { type: "unresolved", sourceText: quoted(spelling.textAt(at), at) };
1657
+ }
1658
+ function quoted(text, key) {
1659
+ const written = (text ?? key.slice(key.lastIndexOf("#") + 1)).replace(/\s+/g, " ").trim();
1660
+ return written.length <= QUOTED_LENGTH ? written : `${written.slice(0, QUOTED_LENGTH - 3)}...`;
1661
+ }
1662
+
1200
1663
  // src/adapterStamp.ts
1201
1664
  var import_node_crypto = require("crypto");
1202
- var import_node_fs = __toESM(require("fs"), 1);
1665
+ var import_node_fs2 = __toESM(require("fs"), 1);
1203
1666
  var import_node_module = require("module");
1204
- var import_node_path2 = __toESM(require("path"), 1);
1667
+ var import_node_path4 = __toESM(require("path"), 1);
1205
1668
  var import_node_url = require("url");
1206
1669
  var import_meta = {};
1207
1670
  var ANALYSIS_PACKAGES = [
@@ -1244,16 +1707,16 @@ function createAdapterStamp(config) {
1244
1707
  function readAdapterCodeStamp(moduleUrl) {
1245
1708
  try {
1246
1709
  const selfPath = (0, import_node_url.fileURLToPath)(moduleUrl);
1247
- const hash = computeDistHashFrom(import_node_path2.default.dirname(selfPath));
1710
+ const hash = computeDistHashFrom(import_node_path4.default.dirname(selfPath));
1248
1711
  return hash.length > 0 ? { kind: "bundle", hash } : SOURCE_STAMP;
1249
1712
  } catch {
1250
1713
  return SOURCE_STAMP;
1251
1714
  }
1252
1715
  }
1253
1716
  function computeDistHashFrom(dir) {
1254
- const candidates = [import_node_path2.default.join(dir, "index.js"), import_node_path2.default.join(dir, "index.cjs")];
1717
+ const candidates = [import_node_path4.default.join(dir, "index.js"), import_node_path4.default.join(dir, "index.cjs")];
1255
1718
  for (const candidate of candidates) {
1256
- if (!import_node_fs.default.existsSync(candidate)) {
1719
+ if (!import_node_fs2.default.existsSync(candidate)) {
1257
1720
  continue;
1258
1721
  }
1259
1722
  return computeContentHash([candidate, ...analysisBundles()]);
@@ -1267,7 +1730,7 @@ function computeContentHash(paths) {
1267
1730
  const hash = (0, import_node_crypto.createHash)("sha256");
1268
1731
  for (const file of paths) {
1269
1732
  try {
1270
- hash.update(import_node_fs.default.readFileSync(file));
1733
+ hash.update(import_node_fs2.default.readFileSync(file));
1271
1734
  } catch {
1272
1735
  return "";
1273
1736
  }
@@ -1283,13 +1746,30 @@ function projectFileStamp(paths) {
1283
1746
  hash.update(file);
1284
1747
  hash.update("\0");
1285
1748
  try {
1286
- hash.update(import_node_fs.default.readFileSync(file));
1749
+ hash.update(import_node_fs2.default.readFileSync(file));
1287
1750
  } catch {
1288
1751
  hash.update("absent");
1289
1752
  }
1290
1753
  }
1291
1754
  return hash.digest("hex").slice(0, 16);
1292
1755
  }
1756
+ function extractionConfigStamp(config) {
1757
+ const parts = [
1758
+ ["gapHandling", config.gapHandling ?? "permissive"],
1759
+ ["includeReachable", optionalText(config.includeReachable)],
1760
+ ["workspaceRoot", config.workspaceRoot],
1761
+ ["projectRoot", config.projectRoot],
1762
+ ["importRoots", config.importRoots?.join(import_node_path4.default.delimiter)],
1763
+ ["modules", modulesStamp(config.modules)],
1764
+ ["walkedDirectory", config.walkedDirectory]
1765
+ ];
1766
+ return parts.flatMap(
1767
+ ([name, value]) => value === void 0 ? [] : [`${name}=${value}`]
1768
+ ).join(",");
1769
+ }
1770
+ function optionalText(value) {
1771
+ return value === void 0 ? void 0 : String(value);
1772
+ }
1293
1773
  function runDigest(packsDigest, packs, files) {
1294
1774
  const inputs = packs.flatMap((pack) => pack.discoveryInputs?.(files) ?? []);
1295
1775
  return `${packsDigest}|reads:${projectFileStamp(inputs)}`;
@@ -1307,10 +1787,130 @@ function analysisBundles() {
1307
1787
  }
1308
1788
 
1309
1789
  // src/cache.ts
1790
+ var import_node_crypto3 = require("crypto");
1791
+ var import_promises2 = __toESM(require("fs/promises"), 1);
1792
+ var import_node_path6 = __toESM(require("path"), 1);
1793
+ var import_files = require("@suss/behavioral-ir/files");
1794
+
1795
+ // src/cacheStamps.ts
1310
1796
  var import_node_crypto2 = require("crypto");
1311
1797
  var import_promises = __toESM(require("fs/promises"), 1);
1312
- var import_node_path3 = __toESM(require("path"), 1);
1313
- var SCHEMA_VERSION = "7";
1798
+ var RECENT_CHANGE_MS = 2e3;
1799
+ function stampMayMissAWrite(stat, statAt) {
1800
+ return Math.max(stat.mtimeMs, stat.ctimeMs) > statAt - RECENT_CHANGE_MS;
1801
+ }
1802
+ async function startRun(files, configPath) {
1803
+ const startedAt = Date.now();
1804
+ const config = configPath === void 0 ? null : await statAtStart(configPath);
1805
+ const stats = await Promise.all(files.map(statAtStart));
1806
+ stats.sort(
1807
+ (a, b) => a.stamp.path < b.stamp.path ? -1 : a.stamp.path > b.stamp.path ? 1 : 0
1808
+ );
1809
+ return {
1810
+ startedAt,
1811
+ config: config?.ctimeMs === null ? null : config,
1812
+ files: stats,
1813
+ hashes: /* @__PURE__ */ new Map()
1814
+ };
1815
+ }
1816
+ async function statAtStart(filePath) {
1817
+ try {
1818
+ const stat = await import_promises.default.stat(filePath);
1819
+ return {
1820
+ stamp: { path: filePath, mtimeMs: stat.mtimeMs, size: stat.size },
1821
+ ctimeMs: stat.ctimeMs
1822
+ };
1823
+ } catch {
1824
+ return { stamp: { path: filePath, mtimeMs: -1, size: -1 }, ctimeMs: null };
1825
+ }
1826
+ }
1827
+ async function hashOf(run, filePath) {
1828
+ const known = run.hashes.get(filePath);
1829
+ if (known !== void 0) {
1830
+ return known;
1831
+ }
1832
+ const hash = await hashFile(filePath);
1833
+ run.hashes.set(filePath, hash);
1834
+ return hash;
1835
+ }
1836
+ async function hashRecentChanges(run) {
1837
+ const recent = [...run.config === null ? [] : [run.config], ...run.files].filter(
1838
+ (file) => file.ctimeMs !== null && stampMayMissAWrite(
1839
+ { mtimeMs: file.stamp.mtimeMs, ctimeMs: file.ctimeMs },
1840
+ run.startedAt
1841
+ )
1842
+ ).map((file) => file.stamp.path);
1843
+ await Promise.all(recent.map((filePath) => hashOf(run, filePath)));
1844
+ }
1845
+ async function stillMatches(stored, found, run) {
1846
+ if (!fileStampEquals(stored, found)) {
1847
+ return false;
1848
+ }
1849
+ if (stored === null || stored.verifyHash !== true) {
1850
+ return true;
1851
+ }
1852
+ return await hashOf(run, stored.path) === stored.contentHash;
1853
+ }
1854
+ async function filesStillMatch(stored, run) {
1855
+ if (stored.length !== run.files.length || !stored.every((s, i) => fileStampEquals(s, run.files[i]?.stamp ?? null))) {
1856
+ return false;
1857
+ }
1858
+ const checks = await Promise.all(
1859
+ stored.map((s, i) => stillMatches(s, run.files[i]?.stamp ?? null, run))
1860
+ );
1861
+ return checks.every(Boolean);
1862
+ }
1863
+ async function recordedStamp(start, run, prior) {
1864
+ if (start.ctimeMs === null) {
1865
+ return start.stamp;
1866
+ }
1867
+ const filePath = start.stamp.path;
1868
+ const now = await import_promises.default.stat(filePath).catch(() => null);
1869
+ if (now === null || now.mtimeMs !== start.stamp.mtimeMs || now.size !== start.stamp.size || now.ctimeMs !== start.ctimeMs) {
1870
+ return null;
1871
+ }
1872
+ const atStart = { mtimeMs: start.stamp.mtimeMs, ctimeMs: start.ctimeMs };
1873
+ if (stampMayMissAWrite(atStart, run.startedAt)) {
1874
+ const readBefore = run.hashes.get(filePath);
1875
+ return readBefore === void 0 || readBefore === null ? null : { ...start.stamp, contentHash: readBefore, verifyHash: true };
1876
+ }
1877
+ const hash = run.hashes.get(filePath) ?? reusableHash(prior, start.stamp) ?? await hashFile(filePath);
1878
+ return hash === null ? start.stamp : { ...start.stamp, contentHash: hash };
1879
+ }
1880
+ function reusableHash(prior, stamp) {
1881
+ return prior !== void 0 && prior.verifyHash !== true && fileStampEquals(prior, stamp) ? prior.contentHash : void 0;
1882
+ }
1883
+ async function hashFile(filePath) {
1884
+ try {
1885
+ const content = await import_promises.default.readFile(filePath);
1886
+ return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").slice(0, 16);
1887
+ } catch {
1888
+ return null;
1889
+ }
1890
+ }
1891
+ function fileStampEquals(a, b) {
1892
+ if (a === null && b === null) {
1893
+ return true;
1894
+ }
1895
+ if (a === null || b === null) {
1896
+ return false;
1897
+ }
1898
+ return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
1899
+ }
1900
+
1901
+ // src/ignoredDirectory.ts
1902
+ var import_node_fs3 = __toESM(require("fs"), 1);
1903
+ var import_node_path5 = __toESM(require("path"), 1);
1904
+ function makeIgnoredDirectory(dir) {
1905
+ import_node_fs3.default.mkdirSync(dir, { recursive: true });
1906
+ const ignore = import_node_path5.default.join(dir, ".gitignore");
1907
+ if (!import_node_fs3.default.existsSync(ignore)) {
1908
+ import_node_fs3.default.writeFileSync(ignore, "*\n");
1909
+ }
1910
+ }
1911
+
1912
+ // src/cache.ts
1913
+ var SCHEMA_VERSION = "9";
1314
1914
  var MAX_ENTRIES = 2;
1315
1915
  var ENTRY_PREFIX = "key-";
1316
1916
  var ENTRY_DIR_NAME = new RegExp(`^${ENTRY_PREFIX}[0-9a-f]{16}$`);
@@ -1327,56 +1927,88 @@ function createCacheLayer(cacheDir) {
1327
1927
  }
1328
1928
  };
1329
1929
  }
1930
+ const manifests = new ManifestReads();
1931
+ const runs = /* @__PURE__ */ new WeakMap();
1932
+ const runFor = async (input) => runs.get(input) ?? await startRun(input.files, input.configPath);
1933
+ const lookupWith = async (input, run) => {
1934
+ const entryDir = entryDirFor(cacheDir, input);
1935
+ const manifest = await manifests.readAtRunStart(
1936
+ import_node_path6.default.join(entryDir, "manifest.json")
1937
+ );
1938
+ if (manifest === null) {
1939
+ return missDiag(await describeAbsentEntry(cacheDir));
1940
+ }
1941
+ if (!await stillMatches(
1942
+ manifest.configStamp,
1943
+ run.config?.stamp ?? null,
1944
+ run
1945
+ )) {
1946
+ return missDiag("config-changed");
1947
+ }
1948
+ if (!await filesStillMatch(manifest.files, run)) {
1949
+ return missDiag("files-changed");
1950
+ }
1951
+ await markUsed(entryDir);
1952
+ manifests.forget();
1953
+ return {
1954
+ kind: "hit",
1955
+ summaries: manifest.summaries,
1956
+ diagnostic: { kind: "hit" }
1957
+ };
1958
+ };
1330
1959
  return {
1331
1960
  async tryHit(input) {
1332
1961
  const result = await this.lookup(input);
1333
1962
  return result.kind === "hit" ? result.summaries : null;
1334
1963
  },
1335
1964
  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));
1965
+ const run = await startRun(input.files, input.configPath);
1966
+ runs.set(input, run);
1967
+ const result = await lookupWith(input, run);
1968
+ if (result.kind === "miss") {
1969
+ await hashRecentChanges(run);
1342
1970
  }
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
- };
1971
+ return result;
1357
1972
  },
1358
1973
  async plan(input) {
1359
1974
  const entryDir = entryDirFor(cacheDir, input);
1360
- const manifest = await readManifest(
1361
- import_node_path3.default.join(entryDir, "manifest.json")
1975
+ const manifest = await manifests.readAgain(
1976
+ import_node_path6.default.join(entryDir, "manifest.json")
1362
1977
  );
1363
1978
  if (manifest === null || manifest.roots === void 0 || manifest.rootMeta === void 0 || manifest.owners === void 0) {
1364
1979
  return null;
1365
1980
  }
1366
- const currentConfigStamp = await stampConfigFile(input.configPath);
1367
- if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
1981
+ const run = await runFor(input);
1982
+ if (!await stillMatches(
1983
+ manifest.configStamp,
1984
+ run.config?.stamp ?? null,
1985
+ run
1986
+ )) {
1368
1987
  return null;
1369
1988
  }
1370
1989
  await markUsed(entryDir);
1371
- return buildPlan(manifest, await resolveFileStamps(input));
1990
+ return buildPlan(manifest, run);
1372
1991
  },
1373
1992
  async write(input, summaries, attribution) {
1374
1993
  const entryDir = entryDirFor(cacheDir, input);
1375
- const previous = await readManifest(
1376
- import_node_path3.default.join(entryDir, "manifest.json")
1994
+ const previous = await manifests.readAgain(
1995
+ import_node_path6.default.join(entryDir, "manifest.json")
1377
1996
  );
1378
- const configStamp = await stampConfigFile(input.configPath);
1379
- const files = await hashStamps(await resolveFileStamps(input), previous);
1997
+ manifests.forget();
1998
+ const known = runs.get(input);
1999
+ const run = known ?? await runFor(input);
2000
+ if (known === void 0) {
2001
+ await hashRecentChanges(run);
2002
+ }
2003
+ runs.delete(input);
2004
+ const configStamp = run.config === null ? null : await recordedStamp(run.config, run, void 0);
2005
+ const prior = new Map((previous?.files ?? []).map((f) => [f.path, f]));
2006
+ const recorded = await Promise.all(
2007
+ run.files.map(
2008
+ (file) => recordedStamp(file, run, prior.get(file.stamp.path))
2009
+ )
2010
+ );
2011
+ const files = recorded.filter((stamp) => stamp !== null);
1380
2012
  const manifest = {
1381
2013
  schemaVersion: SCHEMA_VERSION,
1382
2014
  adapterPacksDigest: input.adapterPacksDigest,
@@ -1385,19 +2017,17 @@ function createCacheLayer(cacheDir) {
1385
2017
  summaries,
1386
2018
  ...attribution === void 0 ? {} : encodeAttribution(attribution)
1387
2019
  };
1388
- await import_promises.default.mkdir(entryDir, { recursive: true });
1389
- await import_promises.default.writeFile(
1390
- import_node_path3.default.join(entryDir, "manifest.json"),
1391
- JSON.stringify(manifest)
1392
- );
2020
+ makeIgnoredDirectory(cacheDir);
2021
+ (0, import_files.writeDocumentFile)(import_node_path6.default.join(entryDir, "manifest.json"), manifest);
1393
2022
  await evictOldEntries(cacheDir, entryDir);
1394
2023
  }
1395
2024
  };
1396
2025
  }
1397
- async function buildPlan(manifest, currentStamps) {
2026
+ async function buildPlan(manifest, run) {
1398
2027
  const stored = new Map(manifest.files.map((f) => [f.path, f]));
1399
- const current = new Map(currentStamps.map((f) => [f.path, f]));
2028
+ const current = new Map(run.files.map((f) => [f.stamp.path, f.stamp]));
1400
2029
  const changed = /* @__PURE__ */ new Set();
2030
+ const added = /* @__PURE__ */ new Set();
1401
2031
  const removed = /* @__PURE__ */ new Set();
1402
2032
  for (const p of stored.keys()) {
1403
2033
  if (!current.has(p)) {
@@ -1409,17 +2039,20 @@ async function buildPlan(manifest, currentStamps) {
1409
2039
  const before = stored.get(p);
1410
2040
  if (before === void 0) {
1411
2041
  changed.add(p);
2042
+ added.add(p);
1412
2043
  } else if (!fileStampEquals(before, stamp)) {
1413
2044
  if (before.contentHash === void 0) {
1414
2045
  changed.add(p);
1415
2046
  } else {
1416
2047
  toVerify.push(p);
1417
2048
  }
2049
+ } else if (before.verifyHash === true) {
2050
+ toVerify.push(p);
1418
2051
  }
1419
2052
  }
1420
2053
  await Promise.all(
1421
2054
  toVerify.map(async (p) => {
1422
- const hash = await hashFile(p);
2055
+ const hash = await hashOf(run, p);
1423
2056
  if (hash === null || hash !== stored.get(p)?.contentHash) {
1424
2057
  changed.add(p);
1425
2058
  }
@@ -1428,6 +2061,10 @@ async function buildPlan(manifest, currentStamps) {
1428
2061
  const roots = /* @__PURE__ */ new Map();
1429
2062
  const rootNames = manifest.roots ?? [];
1430
2063
  const depPaths = manifest.depPaths ?? [];
2064
+ const pathsOf = (ids) => ids.flatMap((d) => {
2065
+ const p = depPaths[d];
2066
+ return p === void 0 ? [] : [p];
2067
+ });
1431
2068
  let rootsDeclined = 0;
1432
2069
  rootNames.forEach((rootPath, i) => {
1433
2070
  const meta = manifest.rootMeta?.[i];
@@ -1440,35 +2077,54 @@ async function buildPlan(manifest, currentStamps) {
1440
2077
  roots.set(rootPath, {
1441
2078
  path: rootPath,
1442
2079
  cacheable: meta.cacheable,
1443
- deps: meta.deps.flatMap((d) => {
1444
- const p = depPaths[d];
1445
- return p === void 0 ? [] : [p];
1446
- }),
2080
+ deps: pathsOf(meta.deps),
1447
2081
  claims: meta.claims,
1448
2082
  meta: meta.meta,
1449
- packs: meta.packs
2083
+ packs: meta.packs,
2084
+ ...meta.imports === void 0 ? {} : { imports: pathsOf(meta.imports) }
1450
2085
  });
1451
2086
  });
2087
+ const unchanged = (filePath, deps) => current.has(filePath) && !changed.has(filePath) && !deps.some((d) => changed.has(d) || removed.has(d));
1452
2088
  const validRoots = /* @__PURE__ */ new Set();
1453
2089
  for (const [rootPath, record] of roots) {
1454
- if (!record.cacheable || !current.has(rootPath)) {
1455
- continue;
2090
+ if (record.cacheable && unchanged(rootPath, record.deps)) {
2091
+ validRoots.add(rootPath);
1456
2092
  }
1457
- if (changed.has(rootPath)) {
1458
- continue;
2093
+ }
2094
+ const units = (manifest.units ?? []).flatMap((stored2) => {
2095
+ const file = depPaths[stored2.file];
2096
+ return file === void 0 ? [] : [
2097
+ {
2098
+ key: stored2.key,
2099
+ file,
2100
+ deps: pathsOf(stored2.deps),
2101
+ data: stored2.data
2102
+ }
2103
+ ];
2104
+ });
2105
+ const validUnits = /* @__PURE__ */ new Map();
2106
+ for (const unit of units) {
2107
+ if (unchanged(unit.file, unit.deps)) {
2108
+ validUnits.set(unit.key, unit);
1459
2109
  }
1460
- const depMoved = record.deps.some((d) => changed.has(d) || removed.has(d));
1461
- if (!depMoved) {
1462
- validRoots.add(rootPath);
2110
+ }
2111
+ const resolvedImports = /* @__PURE__ */ new Map();
2112
+ if (added.size === 0 && removed.size === 0) {
2113
+ for (const [rootPath, record] of roots) {
2114
+ if (record.imports !== void 0 && !changed.has(rootPath)) {
2115
+ resolvedImports.set(rootPath, record.imports);
2116
+ }
1463
2117
  }
1464
2118
  }
1465
2119
  const owners = manifest.owners ?? [];
1466
2120
  return {
1467
2121
  changed,
1468
2122
  removed,
2123
+ resolvedImports,
1469
2124
  roots,
1470
2125
  validRoots,
1471
2126
  rootsDeclined,
2127
+ validUnits,
1472
2128
  reuse(valid) {
1473
2129
  const summaries = [];
1474
2130
  const reusedOwners = [];
@@ -1495,7 +2151,8 @@ async function buildPlan(manifest, currentStamps) {
1495
2151
  const p = rootNames[o];
1496
2152
  return p === void 0 ? [] : [p];
1497
2153
  })
1498
- )
2154
+ ),
2155
+ units
1499
2156
  };
1500
2157
  }
1501
2158
  };
@@ -1520,7 +2177,8 @@ function encodeAttribution(attribution) {
1520
2177
  deps: r.deps.map(depIdOf),
1521
2178
  claims: r.claims,
1522
2179
  meta: r.meta,
1523
- packs: r.packs
2180
+ packs: r.packs,
2181
+ ...r.imports === void 0 ? {} : { imports: r.imports.map(depIdOf) }
1524
2182
  }));
1525
2183
  const owners = attribution.owners.map(
1526
2184
  (ownerPaths) => ownerPaths.flatMap((p) => {
@@ -1528,32 +2186,23 @@ function encodeAttribution(attribution) {
1528
2186
  return i === void 0 ? [] : [i];
1529
2187
  })
1530
2188
  );
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
- }
2189
+ const units = attribution.units?.map((unit) => ({
2190
+ key: unit.key,
2191
+ file: depIdOf(unit.file),
2192
+ deps: unit.deps.map(depIdOf),
2193
+ data: unit.data
2194
+ }));
2195
+ return {
2196
+ roots,
2197
+ rootMeta,
2198
+ depPaths,
2199
+ owners,
2200
+ ...units === void 0 ? {} : { units }
2201
+ };
1553
2202
  }
1554
2203
  async function describeAbsentEntry(cacheDir) {
1555
2204
  try {
1556
- const entries = await import_promises.default.readdir(cacheDir, { withFileTypes: true });
2205
+ const entries = await import_promises2.default.readdir(cacheDir, { withFileTypes: true });
1557
2206
  return entries.some((e) => e.isDirectory() && isEntryDir(e.name)) ? "key-changed" : "no-manifest";
1558
2207
  } catch {
1559
2208
  return "no-manifest";
@@ -1565,8 +2214,8 @@ function entryDirFor(cacheDir, input) {
1565
2214
  input.adapterPacksDigest,
1566
2215
  input.configPath ?? ""
1567
2216
  ].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}`);
2217
+ const name = (0, import_node_crypto3.createHash)("sha256").update(key).digest("hex").slice(0, 16);
2218
+ return import_node_path6.default.join(cacheDir, `${ENTRY_PREFIX}${name}`);
1570
2219
  }
1571
2220
  function isEntryDir(name) {
1572
2221
  return ENTRY_DIR_NAME.test(name);
@@ -1574,24 +2223,24 @@ function isEntryDir(name) {
1574
2223
  async function markUsed(entryDir) {
1575
2224
  const now = /* @__PURE__ */ new Date();
1576
2225
  try {
1577
- await import_promises.default.utimes(entryDir, now, now);
2226
+ await import_promises2.default.utimes(entryDir, now, now);
1578
2227
  } catch {
1579
2228
  }
1580
2229
  }
1581
2230
  async function evictOldEntries(cacheDir, keepDir) {
1582
2231
  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 });
2232
+ await import_promises2.default.rm(import_node_path6.default.join(cacheDir, "manifest.json"), { force: true });
2233
+ const entries = await import_promises2.default.readdir(cacheDir, { withFileTypes: true });
1585
2234
  const dirs = await Promise.all(
1586
2235
  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);
2236
+ const dir = import_node_path6.default.join(cacheDir, e.name);
2237
+ const stat = await import_promises2.default.stat(dir);
1589
2238
  return { dir, mtimeMs: stat.mtimeMs };
1590
2239
  })
1591
2240
  );
1592
2241
  const doomed = dirs.filter((d) => d.dir !== keepDir).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_ENTRIES - 1);
1593
2242
  await Promise.all(
1594
- doomed.map((d) => import_promises.default.rm(d.dir, { recursive: true, force: true }))
2243
+ doomed.map((d) => import_promises2.default.rm(d.dir, { recursive: true, force: true }))
1595
2244
  );
1596
2245
  } catch {
1597
2246
  }
@@ -1602,54 +2251,43 @@ function missDiag(reason) {
1602
2251
  diagnostic: { kind: "miss", missReason: reason }
1603
2252
  };
1604
2253
  }
1605
- async function readManifest(manifestPath) {
2254
+ var ManifestReads = class {
2255
+ last = null;
2256
+ async readAtRunStart(manifestPath) {
2257
+ const stamp = await manifestStamp(manifestPath);
2258
+ const manifest = await readManifest(manifestPath);
2259
+ this.last = { path: manifestPath, stamp, manifest };
2260
+ return manifest;
2261
+ }
2262
+ async readAgain(manifestPath) {
2263
+ const last = this.last;
2264
+ if (last !== null && last.path === manifestPath && last.stamp !== null && last.stamp === await manifestStamp(manifestPath)) {
2265
+ return last.manifest;
2266
+ }
2267
+ return await this.readAtRunStart(manifestPath);
2268
+ }
2269
+ forget() {
2270
+ this.last = null;
2271
+ }
2272
+ };
2273
+ async function manifestStamp(manifestPath) {
1606
2274
  try {
1607
- const raw = await import_promises.default.readFile(manifestPath, "utf-8");
1608
- return JSON.parse(raw);
2275
+ const stat = await import_promises2.default.stat(manifestPath);
2276
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
1609
2277
  } catch {
1610
2278
  return null;
1611
2279
  }
1612
2280
  }
1613
- async function stampConfigFile(configPath) {
1614
- if (configPath === void 0) {
1615
- return null;
1616
- }
2281
+ async function readManifest(manifestPath) {
1617
2282
  try {
1618
- const stat = await import_promises.default.stat(configPath);
1619
- return { path: configPath, mtimeMs: stat.mtimeMs, size: stat.size };
2283
+ return (0, import_files.readJsonFile)(manifestPath);
1620
2284
  } catch {
1621
2285
  return null;
1622
2286
  }
1623
2287
  }
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
2288
 
1651
2289
  // src/extractionReport.ts
1652
- var import_behavioral_ir4 = require("@suss/behavioral-ir");
2290
+ var import_behavioral_ir6 = require("@suss/behavioral-ir");
1653
2291
  var emptyTally = () => ({
1654
2292
  candidateFiles: 0,
1655
2293
  unitsDiscovered: 0,
@@ -1699,7 +2337,7 @@ function summaryCountsByPack(summaries) {
1699
2337
  }
1700
2338
  const entry = counts.get(recognition) ?? emptyCounts();
1701
2339
  entry.bound += 1;
1702
- if (import_behavioral_ir4.BOUNDARY_ROLE[summary.kind] === "provider") {
2340
+ if (import_behavioral_ir6.BOUNDARY_ROLE[summary.kind] === "provider") {
1703
2341
  entry.providers += 1;
1704
2342
  }
1705
2343
  if (summary.transitions.length > 0) {
@@ -1755,6 +2393,38 @@ function firstEmptyUngatedStage(packs, filesWalked) {
1755
2393
  return "assembly";
1756
2394
  }
1757
2395
 
2396
+ // src/keptParses.ts
2397
+ var KeptParses = class {
2398
+ constructor(release = () => {
2399
+ }) {
2400
+ this.release = release;
2401
+ }
2402
+ release;
2403
+ byFile = /* @__PURE__ */ new Map();
2404
+ async parse(file, source, make) {
2405
+ const kept = this.byFile.get(file);
2406
+ if (kept !== void 0 && kept.source === source) {
2407
+ return kept.value;
2408
+ }
2409
+ const value = await make(source);
2410
+ if (kept !== void 0) {
2411
+ this.release(kept.value);
2412
+ }
2413
+ this.byFile.set(file, { source, value });
2414
+ return value;
2415
+ }
2416
+ /** Lets go of every file not in this run. */
2417
+ keepOnly(files) {
2418
+ const wanted = new Set(files);
2419
+ for (const [file, kept] of this.byFile) {
2420
+ if (!wanted.has(file)) {
2421
+ this.release(kept.value);
2422
+ this.byFile.delete(file);
2423
+ }
2424
+ }
2425
+ }
2426
+ };
2427
+
1758
2428
  // src/packHealth.ts
1759
2429
  function stagesOf(funnel) {
1760
2430
  const stages = [];
@@ -1801,8 +2471,8 @@ function funnelDrops(packs) {
1801
2471
  function recognizersWithNoUnits(packs) {
1802
2472
  return packs.filter(
1803
2473
  (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.
2474
+ // roots, so effects recognized there mean the run worked and only
2475
+ // the framework pack's attribution is missing.
1806
2476
  funnel.effectsRecognized === 0
1807
2477
  ).map((funnel) => ({
1808
2478
  label: funnel.pack,
@@ -2044,7 +2714,7 @@ function makeTransitionId(functionName, branch) {
2044
2714
  const { terminal } = branch;
2045
2715
  const statusKey = terminal.statusCode === null ? "none" : terminal.statusCode.type === "literal" ? String(terminal.statusCode.value) : `dyn:${terminal.statusCode.sourceText}`;
2046
2716
  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);
2717
+ const conditionHash = (0, import_node_crypto4.createHash)("sha1").update(conditionSig).digest("hex").slice(0, 7);
2048
2718
  return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
2049
2719
  }
2050
2720
  function collapseReading(table, reading) {
@@ -2103,6 +2773,7 @@ function unreadSentences(raw) {
2103
2773
  ...raw.branches.flatMap(readingsOfBranch)
2104
2774
  ];
2105
2775
  return [
2776
+ ...raw.readFailure !== void 0 ? [raw.readFailure] : [],
2106
2777
  ...raw.unreadBinding !== void 0 ? [raw.unreadBinding] : [],
2107
2778
  ...handedOver.map(unreadReasonOf).filter((reason) => reason !== null)
2108
2779
  ];
@@ -2112,7 +2783,40 @@ function effectsOfBranch(branch) {
2112
2783
  ...branch.effects.map(effectToIR),
2113
2784
  ...branch.extraEffects ?? []
2114
2785
  ];
2115
- return (0, import_behavioral_ir5.foldRepeatedEffects)(effects.map(withNormalizedCallee));
2786
+ return (0, import_behavioral_ir7.foldRepeatedEffects)(effects.map(withNormalizedCallee));
2787
+ }
2788
+ function provenanceOf(raw, effects) {
2789
+ if (raw.length === 0) {
2790
+ return [];
2791
+ }
2792
+ const positionOf = new Map(effects.map((one, at) => [(0, import_behavioral_ir7.effectKey)(one), at]));
2793
+ const bySlot = /* @__PURE__ */ new Map();
2794
+ const said = /* @__PURE__ */ new Map();
2795
+ for (const one of raw) {
2796
+ const effect = positionOf.get((0, import_behavioral_ir7.effectKey)(withNormalizedCallee(one.effect)));
2797
+ if (effect === void 0) {
2798
+ continue;
2799
+ }
2800
+ const slotKey = `${effect}\0${one.slot}\0${one.name}`;
2801
+ let entry = bySlot.get(slotKey);
2802
+ let seen = said.get(slotKey);
2803
+ if (entry === void 0 || seen === void 0) {
2804
+ entry = { at: { slot: one.slot, effect, name: one.name }, from: [] };
2805
+ seen = /* @__PURE__ */ new Set();
2806
+ bySlot.set(slotKey, entry);
2807
+ said.set(slotKey, seen);
2808
+ }
2809
+ for (const source of one.from) {
2810
+ const spelled = JSON.stringify(source);
2811
+ if (!seen.has(spelled)) {
2812
+ seen.add(spelled);
2813
+ entry.from.push(source);
2814
+ }
2815
+ }
2816
+ }
2817
+ return [...bySlot.values()].sort(
2818
+ (a, b) => a.at.effect - b.at.effect || a.at.slot.localeCompare(b.at.slot) || a.at.name.localeCompare(b.at.name)
2819
+ );
2116
2820
  }
2117
2821
  function withNormalizedCallee(effect) {
2118
2822
  if (effect.type !== "invocation" && effect.type !== "interaction") {
@@ -2121,7 +2825,7 @@ function withNormalizedCallee(effect) {
2121
2825
  if (effect.callee === void 0) {
2122
2826
  return effect;
2123
2827
  }
2124
- return { ...effect, callee: (0, import_behavioral_ir5.normalizeCalleeText)(effect.callee) };
2828
+ return { ...effect, callee: (0, import_behavioral_ir7.normalizeCalleeText)(effect.callee) };
2125
2829
  }
2126
2830
  function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2127
2831
  const transitions = raw.branches.map((rawBranch) => {
@@ -2129,17 +2833,22 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2129
2833
  const conditions = branch.conditions.map(
2130
2834
  rawConditionToPredicate
2131
2835
  );
2836
+ const effects = effectsOfBranch(branch);
2132
2837
  const transition = {
2133
2838
  id: makeTransitionId(raw.identity.name, branch),
2134
2839
  conditions,
2135
2840
  output: terminalToOutput(branch.terminal),
2136
- effects: effectsOfBranch(branch),
2841
+ effects,
2137
2842
  location: branch.location,
2138
2843
  isDefault: branch.isDefault
2139
2844
  };
2140
2845
  if (branch.expectedInput != null) {
2141
2846
  transition.expectedInput = branch.expectedInput;
2142
2847
  }
2848
+ const provenance = provenanceOf(branch.provenance ?? [], effects);
2849
+ if (provenance.length > 0) {
2850
+ transition.provenance = provenance;
2851
+ }
2143
2852
  return transition;
2144
2853
  });
2145
2854
  const gaps = detectGaps(raw, transitions, options);
@@ -2181,25 +2890,28 @@ function buildMetadata(raw) {
2181
2890
  let metadata = {};
2182
2891
  const http = buildHttpMetadataValue(raw);
2183
2892
  if (http !== null) {
2184
- metadata = (0, import_behavioral_ir5.withHttpMetadata)(metadata, http);
2893
+ metadata = (0, import_behavioral_ir7.withHttpMetadata)(metadata, http);
2185
2894
  }
2186
2895
  const graphql = buildGraphqlMetadataValue(raw);
2187
2896
  if (graphql !== null) {
2188
- metadata = (0, import_behavioral_ir5.withGraphqlMetadata)(metadata, graphql);
2897
+ metadata = (0, import_behavioral_ir7.withGraphqlMetadata)(metadata, graphql);
2189
2898
  }
2190
2899
  if (raw.sourceDocumentLabel !== void 0) {
2191
- metadata = (0, import_behavioral_ir5.withSourceDocumentMetadata)(metadata, {
2900
+ metadata = (0, import_behavioral_ir7.withSourceDocumentMetadata)(metadata, {
2192
2901
  label: raw.sourceDocumentLabel
2193
2902
  });
2194
2903
  }
2195
2904
  if (raw.mount !== void 0) {
2196
- metadata = (0, import_behavioral_ir5.withMountMetadata)(metadata, raw.mount);
2905
+ metadata = (0, import_behavioral_ir7.withMountMetadata)(metadata, raw.mount);
2197
2906
  }
2198
2907
  if (raw.wrappers !== void 0 && raw.wrappers.length > 0) {
2199
- metadata = (0, import_behavioral_ir5.withWrapperMetadata)(metadata, { applied: raw.wrappers });
2908
+ metadata = (0, import_behavioral_ir7.withWrapperMetadata)(metadata, { applied: raw.wrappers });
2200
2909
  }
2201
2910
  if (raw.requestSpelling !== void 0) {
2202
- metadata = (0, import_behavioral_ir5.withRequestSpellingMetadata)(metadata, raw.requestSpelling);
2911
+ metadata = (0, import_behavioral_ir7.withRequestSpellingMetadata)(metadata, raw.requestSpelling);
2912
+ }
2913
+ if (raw.test !== void 0) {
2914
+ metadata = (0, import_behavioral_ir7.withTestMetadata)(metadata, raw.test);
2203
2915
  }
2204
2916
  return Object.keys(metadata).length > 0 ? metadata : null;
2205
2917
  }
@@ -2224,6 +2936,12 @@ function buildHttpMetadataValue(raw) {
2224
2936
  if (raw.failureDelivery !== void 0) {
2225
2937
  http.failureDelivery = raw.failureDelivery;
2226
2938
  }
2939
+ if (raw.redirectDelivery !== void 0) {
2940
+ http.redirectDelivery = raw.redirectDelivery;
2941
+ }
2942
+ if (raw.declaredStatuses !== void 0 && raw.declaredStatuses.length > 0) {
2943
+ http.declaredStatuses = raw.declaredStatuses;
2944
+ }
2227
2945
  return Object.keys(http).length > 0 ? http : null;
2228
2946
  }
2229
2947
  function buildGraphqlMetadataValue(raw) {
@@ -2252,7 +2970,7 @@ function answersWithHttpResponses(raw) {
2252
2970
  if (raw.boundaryBinding === null || raw.boundaryBinding === void 0) {
2253
2971
  return true;
2254
2972
  }
2255
- return (0, import_behavioral_ir5.exchangesHttpResponses)(raw.boundaryBinding);
2973
+ return (0, import_behavioral_ir7.exchangesHttpResponses)(raw.boundaryBinding);
2256
2974
  }
2257
2975
  function detectGaps(raw, transitions, options) {
2258
2976
  if (options.gapHandling === "silent") {
@@ -2291,13 +3009,16 @@ function detectGaps(raw, transitions, options) {
2291
3009
  return gaps;
2292
3010
  }
2293
3011
  function bodyWentUnread(raw) {
3012
+ if (raw.readFailure !== void 0) {
3013
+ return true;
3014
+ }
2294
3015
  if (raw.bodyContent === "absent" || raw.bodyContent === "elsewhere") {
2295
3016
  return true;
2296
3017
  }
2297
3018
  return raw.bodyContent === "statements" && raw.branches.length === 0;
2298
3019
  }
2299
3020
  function describeUnreadBody(raw) {
2300
- if (!bodyWentUnread(raw)) {
3021
+ if (!bodyWentUnread(raw) || raw.readFailure !== void 0) {
2301
3022
  return null;
2302
3023
  }
2303
3024
  if (raw.bodyContent === "absent") {
@@ -2344,26 +3065,41 @@ function bodyToShape(body) {
2344
3065
  }
2345
3066
  return null;
2346
3067
  }
3068
+ function statusValueRef(code) {
3069
+ if (code === null) {
3070
+ return null;
3071
+ }
3072
+ return code.type === "literal" ? { type: "literal", value: code.value } : { type: "unresolved", sourceText: code.sourceText };
3073
+ }
2347
3074
  var terminalConverters = {
2348
3075
  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
3076
  const body = bodyToShape(t.body);
2351
- return { type: "response", statusCode, body, headers: {} };
3077
+ return {
3078
+ type: "response",
3079
+ statusCode: statusValueRef(t.statusCode),
3080
+ body,
3081
+ headers: {}
3082
+ };
2352
3083
  },
2353
3084
  throw: (t) => {
2354
3085
  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
3086
  return {
2357
3087
  type: "response",
2358
- statusCode,
3088
+ statusCode: statusValueRef(t.statusCode),
2359
3089
  body: bodyToShape(t.body),
2360
3090
  headers: {}
2361
3091
  };
2362
3092
  }
3093
+ const ancestry = t.exceptionAncestry;
2363
3094
  return {
2364
3095
  type: "throw",
2365
3096
  exceptionType: t.exceptionType,
2366
- message: t.message
3097
+ message: t.message,
3098
+ ...ancestry === void 0 ? {} : {
3099
+ exceptionAncestors: ancestry.ancestors,
3100
+ ...ancestry.incomplete ? { ancestryIncomplete: true } : {}
3101
+ },
3102
+ ...t.statusWhenUncaught === void 0 ? {} : { statusWhenUncaught: t.statusWhenUncaught }
2367
3103
  };
2368
3104
  },
2369
3105
  render: (t) => ({
@@ -2383,7 +3119,8 @@ var terminalConverters = {
2383
3119
  type: "return",
2384
3120
  value: bodyToShape(t.body)
2385
3121
  }),
2386
- void: (_t) => ({ type: "void" })
3122
+ void: (_t) => ({ type: "void" }),
3123
+ exit: (t) => ({ type: "exit", code: statusValueRef(t.statusCode) })
2387
3124
  };
2388
3125
  function terminalToOutput(terminal) {
2389
3126
  return terminalConverters[terminal.kind](terminal);
@@ -2426,15 +3163,20 @@ function paramToInput(param) {
2426
3163
  name: param.name,
2427
3164
  position: param.position,
2428
3165
  role: param.role,
3166
+ ...param.field === void 0 ? {} : { field: param.field },
2429
3167
  shape: param.typeText ? { type: "ref", name: param.typeText } : null
2430
3168
  };
2431
3169
  }
2432
3170
  // Annotate the CommonJS export names for ESM import in node:
2433
3171
  0 && (module.exports = {
3172
+ EnclosingNodes,
2434
3173
  IdMap,
2435
3174
  IdSet,
3175
+ KeptParses,
2436
3176
  MAX_ENTRIES,
3177
+ MAX_HELPER_PATHS,
2437
3178
  MAX_PATHS,
3179
+ MODULE_SURFACE_RECOGNITION,
2438
3180
  PathBudgetExceeded,
2439
3181
  SKIP_CHILDREN,
2440
3182
  UnmodeledFlow,
@@ -2459,30 +3201,45 @@ function paramToInput(param) {
2459
3201
  enumerateOrDegrade,
2460
3202
  enumerateStructuredPaths,
2461
3203
  evaluatePackHealth,
3204
+ extractionConfigStamp,
2462
3205
  firstWrittenReading,
2463
3206
  formatPackHealth,
2464
3207
  guardsHoldOn,
3208
+ helperPathsOnVariables,
2465
3209
  httpRouteDiscovery,
3210
+ isListedTestFile,
3211
+ makeIgnoredDirectory,
2466
3212
  makeTransitionId,
2467
3213
  mapReading,
3214
+ markReturnsAsExitCode,
3215
+ matchesTestFileName,
3216
+ mergeReadShapes,
2468
3217
  moduleInitStructure,
3218
+ moduleOfFile,
2469
3219
  noopTimer,
2470
3220
  packGradients,
2471
3221
  paramToInput,
2472
3222
  projectFileStamp,
2473
3223
  rawConditionToPredicate,
2474
3224
  recordPackFailure,
3225
+ redirectDeliveryWhenSet,
2475
3226
  registrationHelperDiscovery,
2476
3227
  routeHelperIndex,
2477
3228
  runDigest,
2478
3229
  runsBefore,
2479
3230
  scopeOption,
3231
+ settleModules,
2480
3232
  sharedGatingConditions,
3233
+ sourceRefsOf,
3234
+ splitAtHandOffs,
3235
+ stampMayMissAWrite,
2481
3236
  stampModuleImports,
3237
+ stampModules,
2482
3238
  storageSystemOption,
2483
3239
  summaryCountsByPack,
2484
3240
  tallyUnit,
2485
3241
  terminalToOutput,
3242
+ throughHelper,
2486
3243
  unreadableReading,
2487
3244
  unwrapJsonStringify,
2488
3245
  valueToReadFurtherFrom,