@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.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
 
@@ -197,29 +199,82 @@ function composeOne(summary, chain, keepGaps) {
197
199
  if (wrappers.length === 0) {
198
200
  return narrowed;
199
201
  }
200
- const responses = respondedInstead(wrappers);
201
- const handled = handledThrows(wrappers);
202
- const transitions = beside(summary, responses, handled);
202
+ const responses = settledUncaught(respondedInstead(wrappers), wrappers);
203
+ const unitOwn = settledUncaught(summary.transitions, wrappers);
204
+ const own = [...responses, ...unitOwn];
205
+ const handled = handledThrows(wrappers, thrownBy(own));
206
+ const transitions = beside(summary, unitOwn, responses, handled);
203
207
  if (!keepGaps) {
204
208
  return transitions === summary.transitions ? narrowed : { ...narrowed, transitions };
205
209
  }
206
210
  const gaps = withContractStatusGaps(
207
211
  summary,
208
- [...responses, ...summary.transitions],
209
- handled
212
+ own,
213
+ everyHandlerOutcome(wrappers)
210
214
  );
211
215
  if (transitions === summary.transitions && gaps === summary.gaps) {
212
216
  return narrowed;
213
217
  }
214
218
  return { ...narrowed, transitions, gaps };
215
219
  }
216
- function beside(summary, responses, handled) {
217
- const throws = summary.transitions.some((t) => t.output.type === "throw");
218
- const onThrow = throws ? handled : [];
219
- if (responses.length === 0 && onThrow.length === 0) {
220
+ function beside(summary, unitOwn, responses, handled) {
221
+ if (responses.length === 0 && handled.length === 0 && unitOwn === summary.transitions) {
220
222
  return summary.transitions;
221
223
  }
222
- return withDistinctIds([...responses, ...summary.transitions, ...onThrow]);
224
+ return withDistinctIds([...responses, ...unitOwn, ...handled]);
225
+ }
226
+ function settledUncaught(transitions, wrappers) {
227
+ const handlers = wrappers.filter(
228
+ (wrapper) => wrapper.reference.onThrow === true
229
+ );
230
+ let changed = false;
231
+ const settled = transitions.flatMap((transition) => {
232
+ const { output } = transition;
233
+ if (output.type !== "throw") {
234
+ return [transition];
235
+ }
236
+ const caught = caughtOnRoute(handlers, output);
237
+ if (caught === "sure") {
238
+ changed = true;
239
+ return [markedCaught(transition)];
240
+ }
241
+ if (output.statusWhenUncaught === void 0) {
242
+ return [transition];
243
+ }
244
+ changed = true;
245
+ const response = {
246
+ ...transition,
247
+ output: {
248
+ type: "response",
249
+ statusCode: { type: "literal", value: output.statusWhenUncaught },
250
+ body: null,
251
+ headers: {}
252
+ }
253
+ };
254
+ return caught === "uncertain" ? [transition, response] : [response];
255
+ });
256
+ return changed ? settled : transitions;
257
+ }
258
+ function markedCaught(transition) {
259
+ return {
260
+ ...transition,
261
+ metadata: withWrapperMetadata(transition.metadata, {
262
+ ...readWrapperMetadata2(transition),
263
+ caught: true
264
+ })
265
+ };
266
+ }
267
+ function caughtOnRoute(handlers, thrown) {
268
+ if (handlers.some((handler) => handler.reference.catches === void 0)) {
269
+ return "sure";
270
+ }
271
+ const matches = handlers.map(
272
+ (handler) => catchMatch(handler.reference, thrown)
273
+ );
274
+ if (matches.includes("yes")) {
275
+ return "sure";
276
+ }
277
+ return matches.includes("maybe") ? "uncertain" : "no";
223
278
  }
224
279
  function respondedInstead(wrappers) {
225
280
  return wrappers.flatMap(
@@ -231,11 +286,91 @@ function respondedInstead(wrappers) {
231
286
  )
232
287
  );
233
288
  }
234
- function handledThrows(wrappers) {
289
+ function thrownBy(transitions) {
290
+ return transitions.flatMap(
291
+ (t) => t.output.type === "throw" ? [t.output] : []
292
+ );
293
+ }
294
+ function everyHandlerOutcome(wrappers) {
235
295
  return wrappers.flatMap(
236
296
  (wrapper) => wrapper.reference.onThrow === true ? attribute(wrapper.summary.transitions, wrapper.reference) : []
237
297
  );
238
298
  }
299
+ function handledThrows(wrappers, thrown) {
300
+ const caught = catchesAny(wrappers, thrown);
301
+ return [...oncePerHandler(wrappers, caught)].flatMap(
302
+ ([wrapper, how]) => attribute(
303
+ wrapper.summary.transitions,
304
+ wrapper.reference,
305
+ how === "uncertain"
306
+ )
307
+ );
308
+ }
309
+ function oncePerHandler(wrappers, caught) {
310
+ const byHandler = /* @__PURE__ */ new Map();
311
+ for (const wrapper of wrappers) {
312
+ const how = caught.get(wrapper);
313
+ if (how === void 0) {
314
+ continue;
315
+ }
316
+ const earlier = byHandler.get(wrapper.summary);
317
+ if (earlier === void 0 || earlier[1] === "uncertain" && how === "sure") {
318
+ byHandler.set(wrapper.summary, [wrapper, how]);
319
+ }
320
+ }
321
+ return new Map(byHandler.values());
322
+ }
323
+ function catchesAny(wrappers, thrown) {
324
+ const caught = /* @__PURE__ */ new Map();
325
+ if (thrown.length === 0) {
326
+ return caught;
327
+ }
328
+ const handlers = wrappers.filter(
329
+ (wrapper) => wrapper.reference.onThrow === true
330
+ );
331
+ for (const handler of handlers) {
332
+ if (handler.reference.catches === void 0) {
333
+ caught.set(handler, "sure");
334
+ }
335
+ }
336
+ const listing = handlers.filter(
337
+ (handler) => handler.reference.catches !== void 0
338
+ );
339
+ for (const one of thrown) {
340
+ for (const [handler, how] of catchersOf(listing, one)) {
341
+ if (caught.get(handler) !== "sure") {
342
+ caught.set(handler, how);
343
+ }
344
+ }
345
+ }
346
+ return caught;
347
+ }
348
+ function catchersOf(handlers, thrown) {
349
+ const found = [];
350
+ for (const handler of handlers) {
351
+ const match = catchMatch(handler.reference, thrown);
352
+ if (match === "yes") {
353
+ found.push([handler, found.length === 0 ? "sure" : "uncertain"]);
354
+ return found;
355
+ }
356
+ if (match === "maybe") {
357
+ found.push([handler, "uncertain"]);
358
+ }
359
+ }
360
+ return found;
361
+ }
362
+ function catchMatch(reference, thrown) {
363
+ const ancestors = thrown.exceptionAncestors;
364
+ if (thrown.exceptionType === null || ancestors === void 0) {
365
+ return "maybe";
366
+ }
367
+ const classes = /* @__PURE__ */ new Set([thrown.exceptionType, ...ancestors]);
368
+ if ((reference.catches ?? []).some((name) => classes.has(name))) {
369
+ return "yes";
370
+ }
371
+ const unread = thrown.ancestryIncomplete === true && reference.mayCatchUnreadClasses === true;
372
+ return unread || reference.mayCatchAny === true ? "maybe" : "no";
373
+ }
239
374
  function withContractStatusGaps(summary, composed, handled) {
240
375
  const contract = readHttpMetadata(summary)?.declaredContract;
241
376
  const binding = summary.identity.boundaryBinding;
@@ -266,10 +401,13 @@ function coversUnit(reference, summary) {
266
401
  const binding = summary.identity.boundaryBinding;
267
402
  return binding !== null && withinScope(binding, reference.scope);
268
403
  }
269
- function attribute(transitions, reference) {
404
+ function attribute(transitions, reference, catchUncertain = false) {
270
405
  return transitions.map((transition) => ({
271
406
  ...transition,
272
- metadata: withWrapperMetadata(transition.metadata, { from: reference })
407
+ metadata: withWrapperMetadata(transition.metadata, {
408
+ from: reference,
409
+ ...catchUncertain ? { catchUncertain } : {}
410
+ })
273
411
  }));
274
412
  }
275
413
  function withDistinctIds(transitions) {
@@ -281,6 +419,56 @@ function withDistinctIds(transitions) {
281
419
  });
282
420
  }
283
421
 
422
+ // src/declaredModules.ts
423
+ import fs from "fs";
424
+ import path from "path";
425
+ var MODULE_SURFACE_RECOGNITION = "modules";
426
+ function settleModules(modules, candidates) {
427
+ return (modules ?? []).map((module) => ({
428
+ ...module,
429
+ public: module.public ?? candidates.map(
430
+ (one) => path.join(module.root, one.replace("{name}", module.name))
431
+ ).filter((file) => fs.existsSync(file))
432
+ }));
433
+ }
434
+ function isInside(file, root) {
435
+ const relative = path.relative(root, file);
436
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
437
+ }
438
+ function moduleOfFile(modules, file) {
439
+ const byPublicFile = modules.find((module) => module.public.includes(file));
440
+ if (byPublicFile !== void 0) {
441
+ return byPublicFile;
442
+ }
443
+ let deepest;
444
+ for (const module of modules) {
445
+ if (isInside(file, module.root) && (deepest === void 0 || module.root.length > deepest.root.length)) {
446
+ deepest = module;
447
+ }
448
+ }
449
+ return deepest;
450
+ }
451
+ function stampModules(summaries, modules, root) {
452
+ if (modules.length === 0) {
453
+ return;
454
+ }
455
+ for (const summary of summaries) {
456
+ const file = path.resolve(root ?? "", summary.location.file);
457
+ const module = moduleOfFile(modules, file);
458
+ if (module !== void 0) {
459
+ summary.location.module = module.name;
460
+ }
461
+ }
462
+ }
463
+ function modulesStamp(modules) {
464
+ if (modules === void 0 || modules.length === 0) {
465
+ return void 0;
466
+ }
467
+ return JSON.stringify(
468
+ modules.map((module) => [module.name, module.root, [...module.public]])
469
+ );
470
+ }
471
+
284
472
  // src/effectGuards.ts
285
473
  var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
286
474
  var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
@@ -297,6 +485,139 @@ function runsBefore(effectLine, terminalEndLine) {
297
485
  return effectLine <= terminalEndLine;
298
486
  }
299
487
 
488
+ // src/exitCode.ts
489
+ import path2 from "path";
490
+ import { declarationKey } from "@suss/behavioral-ir";
491
+ function markReturnsAsExitCode(summaries, returnsTheCode, workspaceRoot) {
492
+ for (const summary of summaries) {
493
+ const span = summary.location.span;
494
+ const file = workspaceRoot === void 0 ? summary.location.file : path2.resolve(workspaceRoot, summary.location.file);
495
+ markExitCodeFrom(
496
+ summary,
497
+ span !== void 0 && returnsTheCode.has(declarationKey(file, span))
498
+ );
499
+ }
500
+ }
501
+ function markExitCodeFrom(summary, marked) {
502
+ const process2 = summary.metadata?.process;
503
+ if (marked) {
504
+ summary.metadata = {
505
+ ...summary.metadata ?? {},
506
+ process: { ...process2 ?? {}, exitCodeFrom: "return" }
507
+ };
508
+ return;
509
+ }
510
+ if (process2 === void 0 || !("exitCodeFrom" in process2)) {
511
+ return;
512
+ }
513
+ const { exitCodeFrom: _dropped, ...rest } = process2;
514
+ const { process: _process, ...others } = summary.metadata ?? {};
515
+ const left = Object.keys(rest).length === 0 ? others : { ...others, process: rest };
516
+ if (Object.keys(left).length === 0) {
517
+ Reflect.deleteProperty(summary, "metadata");
518
+ return;
519
+ }
520
+ summary.metadata = left;
521
+ }
522
+
523
+ // src/helperHandOff.ts
524
+ import { predicateRefs, replacePredicateRefs } from "@suss/behavioral-ir";
525
+ var MAX_HELPER_PATHS = 16;
526
+ function throughHelper(branch, at, helperBranches) {
527
+ return helperBranches.map((helper) => {
528
+ const conditions = [...branch.conditions, ...helper.conditions];
529
+ const expectedInput = mergeReadShapes(
530
+ branch.expectedInput ?? null,
531
+ helper.expectedInput ?? null
532
+ );
533
+ const reads = expectedInput === null ? {} : { expectedInput };
534
+ if (helper.terminal.kind !== "throw" && helper.terminal.kind !== "exit") {
535
+ return {
536
+ ...branch,
537
+ ...reads,
538
+ conditions,
539
+ isDefault: branch.isDefault && helper.isDefault
540
+ };
541
+ }
542
+ return {
543
+ ...branch,
544
+ ...reads,
545
+ conditions,
546
+ terminal: { ...helper.terminal, location: at },
547
+ location: at,
548
+ isDefault: false
549
+ };
550
+ });
551
+ }
552
+ function mergeReadShapes(a, b) {
553
+ if (a === null || b === null) {
554
+ return a ?? b;
555
+ }
556
+ if (a.type !== "record" || b.type !== "record") {
557
+ return a.type === "record" ? a : b;
558
+ }
559
+ const properties = { ...a.properties };
560
+ for (const [name, shape] of Object.entries(b.properties)) {
561
+ const known = properties[name];
562
+ properties[name] = known === void 0 ? shape : mergeReadShapes(known, shape) ?? shape;
563
+ }
564
+ return { ...a, properties };
565
+ }
566
+ function splitAtHandOffs(branch, sites, from = 0) {
567
+ for (let index = from; index < sites.length; index++) {
568
+ const site = sites[index];
569
+ const reached = site.alwaysRuns === true || runsBefore(site.line, branch.terminal.location.end);
570
+ if (!reached || !guardsHoldOn(site.preconditions, branch.conditions)) {
571
+ continue;
572
+ }
573
+ return throughHelper(branch, site.at, site.helper).flatMap(
574
+ (path7) => path7.terminal === branch.terminal ? splitAtHandOffs(path7, sites, index + 1) : [path7]
575
+ );
576
+ }
577
+ return [branch];
578
+ }
579
+ function helperPathsOnVariables(helperBranches, passed) {
580
+ const renamed = helperBranches.map((branch) => ({
581
+ ...branch,
582
+ conditions: branch.conditions.map((condition) => ({
583
+ ...condition,
584
+ structured: condition.structured === null ? null : onCallerVariables(condition.structured, passed)
585
+ }))
586
+ }));
587
+ const variables = new Set(passed.values());
588
+ const testsWhatItGot = renamed.some(
589
+ (branch) => branch.conditions.some(
590
+ (condition) => condition.structured !== null && predicateRefs(condition.structured).some(
591
+ (ref) => variables.has(rootName(ref) ?? "")
592
+ )
593
+ )
594
+ );
595
+ return testsWhatItGot && renamed.length <= MAX_HELPER_PATHS ? renamed : null;
596
+ }
597
+ function onCallerVariables(predicate, passed) {
598
+ const rename = (ref) => {
599
+ if (ref.type === "derived") {
600
+ return { ...ref, from: rename(ref.from) };
601
+ }
602
+ if (ref.type === "input") {
603
+ const name = passed.get(ref.inputRef);
604
+ return name === void 0 ? ref : { type: "dependency", name, accessChain: ref.path };
605
+ }
606
+ if (ref.type === "dependency") {
607
+ const name = passed.get(ref.name);
608
+ return name === void 0 ? ref : { ...ref, name };
609
+ }
610
+ return ref;
611
+ };
612
+ return replacePredicateRefs(predicate, rename);
613
+ }
614
+ function rootName(ref) {
615
+ if (ref.type === "derived") {
616
+ return rootName(ref.from);
617
+ }
618
+ return ref.type === "dependency" ? ref.name : null;
619
+ }
620
+
300
621
  // src/moduleImports.ts
301
622
  function stampModuleImports(summaries, importsOf) {
302
623
  const byFile = /* @__PURE__ */ new Map();
@@ -373,8 +694,8 @@ function httpRouteDiscovery(opts) {
373
694
  registrationChain: [...opts.methods]
374
695
  },
375
696
  bindingExtraction: {
376
- // `.all` registers every method, so it records "*", which the pairing
377
- // engine treats as agreeing with any method at all.
697
+ // `.all` registers every method, so it records "*", which pairs with
698
+ // any method.
378
699
  method: {
379
700
  type: "fromRegistration",
380
701
  position: "methodName",
@@ -390,10 +711,9 @@ function httpRouteDiscovery(opts) {
390
711
  }
391
712
  return [
392
713
  ...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.
714
+ // Routes registered in a loop over an array of specs, which the calls
715
+ // above cannot see. The receiver must come from the same import, so a
716
+ // loop that never touches this library's routable is left alone.
397
717
  {
398
718
  kind,
399
719
  match: {
@@ -540,7 +860,7 @@ var configuredCallOption = z.object({
540
860
  receiver: z.string(),
541
861
  /** Method that performs the send. */
542
862
  method: z.string(),
543
- /** Argument index carrying the subject. */
863
+ /** Index of the argument that is the subject. */
544
864
  subjectArg: z.number(),
545
865
  /**
546
866
  * Which argument the message body is. Left out when the method has no
@@ -599,8 +919,8 @@ var catchEntryCond = () => fixedCond(
599
919
  "positive",
600
920
  "catchBlock"
601
921
  );
602
- function classify(path4, home, isAncestorOrSelf) {
603
- return path4.map((cond) => {
922
+ function classify(path7, home, isAncestorOrSelf) {
923
+ return path7.map((cond) => {
604
924
  if (cond.branchStmt === null) {
605
925
  return cond.info;
606
926
  }
@@ -665,9 +985,16 @@ function chargeBudget(state) {
665
985
  );
666
986
  }
667
987
  }
668
- function recordTerminal(ctx, terminal, home, path4) {
988
+ function limitOpenPaths(paths) {
989
+ if (paths.length > MAX_PATHS) {
990
+ throw new PathBudgetExceeded(
991
+ `path budget exceeded, more than ${MAX_PATHS} paths`
992
+ );
993
+ }
994
+ }
995
+ function recordTerminal(ctx, terminal, home, path7) {
669
996
  chargeBudget(ctx.state);
670
- const info = classify(path4, home, ctx.isAncestorOrSelf);
997
+ const info = classify(path7, home, ctx.isAncestorOrSelf);
671
998
  const existing = ctx.state.byTerminal.get(terminal) ?? [];
672
999
  existing.push(info);
673
1000
  ctx.state.byTerminal.set(terminal, existing);
@@ -678,15 +1005,15 @@ function containsTerminal(ctx, stmt) {
678
1005
  }
679
1006
  return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
680
1007
  }
681
- function stepIf(stmt, path4, ctx) {
1008
+ function stepIf(stmt, path7, ctx) {
682
1009
  const thenExit = exitKindOfList(stmt.thenBody);
683
1010
  const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
684
1011
  const thenPath = [
685
- ...path4,
1012
+ ...path7,
686
1013
  branchCond(stmt, stmt.condition, "positive", elseExit)
687
1014
  ];
688
1015
  const elsePath = [
689
- ...path4,
1016
+ ...path7,
690
1017
  branchCond(
691
1018
  stmt,
692
1019
  stmt.condition,
@@ -703,7 +1030,7 @@ function stepIf(stmt, path4, ctx) {
703
1030
  }
704
1031
  return out;
705
1032
  }
706
- function stepSwitch(stmt, path4, ctx) {
1033
+ function stepSwitch(stmt, path7, ctx) {
707
1034
  const out = [];
708
1035
  const negations = [];
709
1036
  let defaultRanBody = false;
@@ -713,7 +1040,7 @@ function stepSwitch(stmt, path4, ctx) {
713
1040
  if (hasStrayBreak(group.body)) {
714
1041
  throw new UnmodeledFlow("non-trailing break in switch clause");
715
1042
  }
716
- const groupPath = group.condition === null ? [...path4, ...negations] : [...path4, fixedCond(group.condition, "positive", "explicit")];
1043
+ const groupPath = group.condition === null ? [...path7, ...negations] : [...path7, fixedCond(group.condition, "positive", "explicit")];
717
1044
  const conts = enumerate(ctx, group.body, groupPath);
718
1045
  if (conts.length > 0) {
719
1046
  if (!group.hasTrailingBreak && !fallthroughSafe) {
@@ -730,24 +1057,24 @@ function stepSwitch(stmt, path4, ctx) {
730
1057
  }
731
1058
  }
732
1059
  if (!defaultRanBody) {
733
- out.push([...path4, ...negations]);
1060
+ out.push([...path7, ...negations]);
734
1061
  }
735
1062
  return out;
736
1063
  }
737
- function stepLoop(stmt, path4, ctx) {
1064
+ function stepLoop(stmt, path7, ctx) {
738
1065
  const bodyExit = exitKindOfList(stmt.body);
739
1066
  if (containsTerminal(ctx, stmt)) {
740
1067
  enumerate(ctx, stmt.body, [
741
- ...path4,
1068
+ ...path7,
742
1069
  loopIterationCond(stmt.condition.sourceText)
743
1070
  ]);
744
1071
  }
745
1072
  if (bodyExit !== null) {
746
- return [[...path4, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
1073
+ return [[...path7, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
747
1074
  }
748
- return [path4];
1075
+ return [path7];
749
1076
  }
750
- function stepTry(stmt, path4, ctx) {
1077
+ function stepTry(stmt, path7, ctx) {
751
1078
  if (stmt.finallyBody !== null) {
752
1079
  const finallyHasTerminal = stmt.finallyBody.some(
753
1080
  (s) => containsTerminal(ctx, s)
@@ -757,22 +1084,22 @@ function stepTry(stmt, path4, ctx) {
757
1084
  }
758
1085
  }
759
1086
  const out = [];
760
- out.push(...enumerate(ctx, stmt.tryBody, path4));
1087
+ out.push(...enumerate(ctx, stmt.tryBody, path7));
761
1088
  if (stmt.catchBody !== null) {
762
- const catchPath = [...path4, catchEntryCond()];
1089
+ const catchPath = [...path7, catchEntryCond()];
763
1090
  out.push(...enumerate(ctx, stmt.catchBody, catchPath));
764
1091
  }
765
1092
  return out;
766
1093
  }
767
- function stepExit(stmt, path4, ctx) {
1094
+ function stepExit(stmt, path7, ctx) {
768
1095
  const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
769
1096
  if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
770
- collector.push(path4);
1097
+ collector.push(path7);
771
1098
  }
772
1099
  return [];
773
1100
  }
774
- function stepOpaque(_stmt, path4, _ctx) {
775
- return [path4];
1101
+ function stepOpaque(_stmt, path7, _ctx) {
1102
+ return [path7];
776
1103
  }
777
1104
  var stepHandlers = {
778
1105
  if: stepIf,
@@ -782,22 +1109,23 @@ var stepHandlers = {
782
1109
  exit: stepExit,
783
1110
  opaque: stepOpaque
784
1111
  };
785
- function stepCallback(ctx, body, path4) {
1112
+ function stepCallback(ctx, body, path7) {
786
1113
  const exits = [];
787
1114
  ctx.state.callbackExits.push(exits);
788
1115
  let fellThrough;
789
1116
  try {
790
- fellThrough = enumerate(ctx, body, path4);
1117
+ fellThrough = enumerate(ctx, body, path7);
791
1118
  } finally {
792
1119
  ctx.state.callbackExits.pop();
793
1120
  }
794
1121
  const out = [...fellThrough, ...exits];
795
- return out.length === 0 ? [path4] : out;
1122
+ return out.length === 0 ? [path7] : out;
796
1123
  }
797
- function stepStatement(ctx, stmt, path4) {
798
- let paths = [path4];
1124
+ function stepStatement(ctx, stmt, path7) {
1125
+ let paths = [path7];
799
1126
  for (const body of stmt.callbacks ?? []) {
800
1127
  paths = paths.flatMap((each) => stepCallback(ctx, body, each));
1128
+ limitOpenPaths(paths);
801
1129
  }
802
1130
  const ownTerminals = ctx.terminalsByStmt.get(stmt) ?? [];
803
1131
  for (const each of withRejoined(paths)) {
@@ -814,7 +1142,7 @@ function stepStatement(ctx, stmt, path4) {
814
1142
  )
815
1143
  );
816
1144
  }
817
- function soleDisagreement(left, right) {
1145
+ function soleDisagreement(left, right, keepsArms) {
818
1146
  if (left.length !== right.length) {
819
1147
  return null;
820
1148
  }
@@ -833,9 +1161,10 @@ function soleDisagreement(left, right) {
833
1161
  }
834
1162
  found = at;
835
1163
  }
836
- return found;
1164
+ const test = found === null ? null : left[found]?.info.expression ?? null;
1165
+ return test !== null && keepsArms(test) ? null : found;
837
1166
  }
838
- function mergeRejoined(paths) {
1167
+ function mergeRejoined(paths, keepsArms = KEEPS_NO_ARMS) {
839
1168
  let current = paths;
840
1169
  for (let round = 0; round < current.length; round++) {
841
1170
  const out = [];
@@ -850,7 +1179,11 @@ function mergeRejoined(paths) {
850
1179
  if (taken.has(j)) {
851
1180
  continue;
852
1181
  }
853
- const at = soleDisagreement(combined, current[j]);
1182
+ const at = soleDisagreement(
1183
+ combined,
1184
+ current[j],
1185
+ keepsArms
1186
+ );
854
1187
  if (at === null) {
855
1188
  continue;
856
1189
  }
@@ -876,21 +1209,18 @@ function enumerate(ctx, stmts, prefix) {
876
1209
  let frontiers = [[...prefix]];
877
1210
  for (const stmt of stmts) {
878
1211
  const nextFrontiers = [];
879
- for (const path4 of mergeRejoined(frontiers)) {
880
- nextFrontiers.push(...stepStatement(ctx, stmt, path4));
1212
+ for (const path7 of mergeRejoined(frontiers, ctx.keepsArms)) {
1213
+ nextFrontiers.push(...stepStatement(ctx, stmt, path7));
881
1214
  }
882
1215
  frontiers = nextFrontiers;
883
- if (frontiers.length > MAX_PATHS) {
884
- throw new PathBudgetExceeded(
885
- `path budget exceeded, more than ${MAX_PATHS} paths`
886
- );
887
- }
1216
+ limitOpenPaths(frontiers);
888
1217
  if (frontiers.length === 0) {
889
1218
  break;
890
1219
  }
891
1220
  }
892
1221
  return frontiers;
893
1222
  }
1223
+ var KEEPS_NO_ARMS = () => false;
894
1224
  function buildParentMap(statements) {
895
1225
  const parentOf = /* @__PURE__ */ new Map();
896
1226
  const visit = (stmt, parent) => {
@@ -924,13 +1254,14 @@ function enumerateStructuredPaths(input) {
924
1254
  };
925
1255
  const ctx = {
926
1256
  terminalsByStmt: input.terminalsByStmt,
1257
+ keepsArms: input.keepsArms ?? KEEPS_NO_ARMS,
927
1258
  state,
928
1259
  isAncestorOrSelf
929
1260
  };
930
1261
  const continuations = withRejoined(enumerate(ctx, input.statements, []));
931
- for (const path4 of continuations) {
1262
+ for (const path7 of continuations) {
932
1263
  chargeBudget(state);
933
- state.fallthrough.push(classify(path4, null, isAncestorOrSelf));
1264
+ state.fallthrough.push(classify(path7, null, isAncestorOrSelf));
934
1265
  }
935
1266
  return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
936
1267
  }
@@ -973,8 +1304,8 @@ function sharedGatingConditions(paths, readPredicate) {
973
1304
  return [];
974
1305
  }
975
1306
  const shared = new Set(first.map(keyOf2));
976
- for (const path4 of rest) {
977
- const here = new Set(path4.map(keyOf2));
1307
+ for (const path7 of rest) {
1308
+ const here = new Set(path7.map(keyOf2));
978
1309
  for (const value of shared) {
979
1310
  if (!here.has(value)) {
980
1311
  shared.delete(value);
@@ -1004,7 +1335,7 @@ var IdSet = class {
1004
1335
  has(node) {
1005
1336
  return this.byKey.has(node.id);
1006
1337
  }
1007
- /** The node this set was built with, which is the caller's own handle for it. */
1338
+ /** The wrapper that was added, which may be a different object from `node`. */
1008
1339
  get(node) {
1009
1340
  return this.byKey.get(node.id);
1010
1341
  }
@@ -1035,39 +1366,81 @@ var IdMap = class {
1035
1366
  }
1036
1367
  };
1037
1368
 
1369
+ // src/paths/nodeReads.ts
1370
+ var EnclosingNodes = class {
1371
+ constructor(types) {
1372
+ this.types = types;
1373
+ }
1374
+ types;
1375
+ byTree = /* @__PURE__ */ new WeakMap();
1376
+ /**
1377
+ * The nearest ancestor of one of the types, or null when there is none.
1378
+ * Every node passed on the way up has the same result, so each one is
1379
+ * kept, and a later read from a sibling stops one step up.
1380
+ */
1381
+ of(node) {
1382
+ const kept = this.keptFor(node.tree);
1383
+ const known = kept.get(node.id);
1384
+ if (known !== void 0) {
1385
+ return known;
1386
+ }
1387
+ const passed = [node.id];
1388
+ let current = node.parent;
1389
+ while (current !== null && !this.types.has(current.type)) {
1390
+ const above = kept.get(current.id);
1391
+ if (above !== void 0) {
1392
+ current = above;
1393
+ break;
1394
+ }
1395
+ passed.push(current.id);
1396
+ current = current.parent;
1397
+ }
1398
+ for (const id of passed) {
1399
+ kept.set(id, current);
1400
+ }
1401
+ return current;
1402
+ }
1403
+ keptFor(tree) {
1404
+ const known = this.byTree.get(tree);
1405
+ if (known !== void 0) {
1406
+ return known;
1407
+ }
1408
+ const fresh = /* @__PURE__ */ new Map();
1409
+ this.byTree.set(tree, fresh);
1410
+ return fresh;
1411
+ }
1412
+ };
1413
+
1038
1414
  // src/paths/nodeWalk.ts
1039
1415
  var SKIP_CHILDREN = /* @__PURE__ */ Symbol("skipChildren");
1040
- function walkDescendants(root, carried, visitor) {
1041
- for (const child of root.namedChildren) {
1042
- if (child === null) {
1043
- continue;
1044
- }
1416
+ function walkDescendants(root, carried, childrenOf2, visitor) {
1417
+ for (const child of childrenOf2(root)) {
1045
1418
  visitor.at(child, carried);
1046
1419
  const inner = visitor.into(child, carried);
1047
1420
  if (inner !== SKIP_CHILDREN) {
1048
- walkDescendants(child, inner, visitor);
1421
+ walkDescendants(child, inner, childrenOf2, visitor);
1049
1422
  }
1050
1423
  }
1051
1424
  }
1052
1425
 
1053
1426
  // src/projectRoot.ts
1054
- import path from "path";
1427
+ import path3 from "path";
1055
1428
  function commonDirectoryOf(files) {
1056
- const absolute = files.filter((f) => path.isAbsolute(f));
1429
+ const absolute = files.filter((f) => path3.isAbsolute(f));
1057
1430
  const first = absolute[0];
1058
1431
  if (first === void 0) {
1059
1432
  return void 0;
1060
1433
  }
1061
- let shared = path.dirname(first).split(path.sep);
1434
+ let shared = path3.dirname(first).split(path3.sep);
1062
1435
  for (const file of absolute.slice(1)) {
1063
- const parts = path.dirname(file).split(path.sep);
1436
+ const parts = path3.dirname(file).split(path3.sep);
1064
1437
  let i = 0;
1065
1438
  while (i < shared.length && i < parts.length && shared[i] === parts[i]) {
1066
1439
  i += 1;
1067
1440
  }
1068
1441
  shared = shared.slice(0, i);
1069
1442
  }
1070
- const joined = shared.join(path.sep);
1443
+ const joined = shared.join(path3.sep);
1071
1444
  return joined.length > 1 ? joined : void 0;
1072
1445
  }
1073
1446
 
@@ -1124,11 +1497,84 @@ function valueToReadFurtherFrom(reading) {
1124
1497
  return reading.kind === "written" ? reading.value : null;
1125
1498
  }
1126
1499
 
1500
+ // src/redirectOption.ts
1501
+ function redirectDeliveryWhenSet(option, value) {
1502
+ return option.handsBack.includes(value) ? "response" : "followed";
1503
+ }
1504
+
1505
+ // src/testFiles.ts
1506
+ function isListedTestFile(file, listed) {
1507
+ if (listed === void 0) {
1508
+ return true;
1509
+ }
1510
+ const whole = withForwardSlashes(file);
1511
+ return listed.some((entry) => {
1512
+ const tail = withForwardSlashes(entry).replace(/^\.\//, "");
1513
+ return whole === tail || whole.endsWith(`/${tail}`);
1514
+ });
1515
+ }
1516
+ function matchesTestFileName(file, patterns) {
1517
+ const base = withForwardSlashes(file).split("/").pop() ?? "";
1518
+ return patterns.some((pattern) => globToRegExp(pattern).test(base));
1519
+ }
1520
+ function globToRegExp(pattern) {
1521
+ const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1522
+ return new RegExp(`^${escaped}$`);
1523
+ }
1524
+ function withForwardSlashes(file) {
1525
+ return file.replace(/\\/g, "/");
1526
+ }
1527
+
1528
+ // src/valueSources.ts
1529
+ var QUOTED_LENGTH = 80;
1530
+ function sourceRefsOf(leaves, spelling) {
1531
+ const refs = [];
1532
+ const said = /* @__PURE__ */ new Set();
1533
+ for (const leaf of leaves) {
1534
+ const ref = convertedFrom(refOf(leaf, spelling), leaf.conversions);
1535
+ const spelled = JSON.stringify(ref);
1536
+ if (!said.has(spelled)) {
1537
+ said.add(spelled);
1538
+ refs.push(ref);
1539
+ }
1540
+ }
1541
+ return refs;
1542
+ }
1543
+ function convertedFrom(ref, conversions) {
1544
+ return conversions.reduceRight(
1545
+ (from, method) => ({
1546
+ type: "derived",
1547
+ from,
1548
+ derivation: { type: "methodCall", method, args: [] }
1549
+ }),
1550
+ ref
1551
+ );
1552
+ }
1553
+ function refOf(leaf, spelling) {
1554
+ const whole = leaf.computedAt === null;
1555
+ const input = whole ? spelling.inputOf(leaf) : null;
1556
+ if (input !== null) {
1557
+ return input;
1558
+ }
1559
+ if (whole && leaf.end.is === "written" && leaf.path.length === 0) {
1560
+ const literal = spelling.literalAt(leaf.key);
1561
+ if (literal !== void 0) {
1562
+ return { type: "literal", value: literal };
1563
+ }
1564
+ }
1565
+ const at = leaf.computedAt ?? leaf.key;
1566
+ return { type: "unresolved", sourceText: quoted(spelling.textAt(at), at) };
1567
+ }
1568
+ function quoted(text, key) {
1569
+ const written = (text ?? key.slice(key.lastIndexOf("#") + 1)).replace(/\s+/g, " ").trim();
1570
+ return written.length <= QUOTED_LENGTH ? written : `${written.slice(0, QUOTED_LENGTH - 3)}...`;
1571
+ }
1572
+
1127
1573
  // src/adapterStamp.ts
1128
1574
  import { createHash } from "crypto";
1129
- import fs from "fs";
1575
+ import fs2 from "fs";
1130
1576
  import { createRequire } from "module";
1131
- import path2 from "path";
1577
+ import path4 from "path";
1132
1578
  import { fileURLToPath } from "url";
1133
1579
  var ANALYSIS_PACKAGES = [
1134
1580
  "@suss/extractor",
@@ -1170,16 +1616,16 @@ function createAdapterStamp(config) {
1170
1616
  function readAdapterCodeStamp(moduleUrl) {
1171
1617
  try {
1172
1618
  const selfPath = fileURLToPath(moduleUrl);
1173
- const hash = computeDistHashFrom(path2.dirname(selfPath));
1619
+ const hash = computeDistHashFrom(path4.dirname(selfPath));
1174
1620
  return hash.length > 0 ? { kind: "bundle", hash } : SOURCE_STAMP;
1175
1621
  } catch {
1176
1622
  return SOURCE_STAMP;
1177
1623
  }
1178
1624
  }
1179
1625
  function computeDistHashFrom(dir) {
1180
- const candidates = [path2.join(dir, "index.js"), path2.join(dir, "index.cjs")];
1626
+ const candidates = [path4.join(dir, "index.js"), path4.join(dir, "index.cjs")];
1181
1627
  for (const candidate of candidates) {
1182
- if (!fs.existsSync(candidate)) {
1628
+ if (!fs2.existsSync(candidate)) {
1183
1629
  continue;
1184
1630
  }
1185
1631
  return computeContentHash([candidate, ...analysisBundles()]);
@@ -1193,7 +1639,7 @@ function computeContentHash(paths) {
1193
1639
  const hash = createHash("sha256");
1194
1640
  for (const file of paths) {
1195
1641
  try {
1196
- hash.update(fs.readFileSync(file));
1642
+ hash.update(fs2.readFileSync(file));
1197
1643
  } catch {
1198
1644
  return "";
1199
1645
  }
@@ -1209,13 +1655,30 @@ function projectFileStamp(paths) {
1209
1655
  hash.update(file);
1210
1656
  hash.update("\0");
1211
1657
  try {
1212
- hash.update(fs.readFileSync(file));
1658
+ hash.update(fs2.readFileSync(file));
1213
1659
  } catch {
1214
1660
  hash.update("absent");
1215
1661
  }
1216
1662
  }
1217
1663
  return hash.digest("hex").slice(0, 16);
1218
1664
  }
1665
+ function extractionConfigStamp(config) {
1666
+ const parts = [
1667
+ ["gapHandling", config.gapHandling ?? "permissive"],
1668
+ ["includeReachable", optionalText(config.includeReachable)],
1669
+ ["workspaceRoot", config.workspaceRoot],
1670
+ ["projectRoot", config.projectRoot],
1671
+ ["importRoots", config.importRoots?.join(path4.delimiter)],
1672
+ ["modules", modulesStamp(config.modules)],
1673
+ ["walkedDirectory", config.walkedDirectory]
1674
+ ];
1675
+ return parts.flatMap(
1676
+ ([name, value]) => value === void 0 ? [] : [`${name}=${value}`]
1677
+ ).join(",");
1678
+ }
1679
+ function optionalText(value) {
1680
+ return value === void 0 ? void 0 : String(value);
1681
+ }
1219
1682
  function runDigest(packsDigest, packs, files) {
1220
1683
  const inputs = packs.flatMap((pack) => pack.discoveryInputs?.(files) ?? []);
1221
1684
  return `${packsDigest}|reads:${projectFileStamp(inputs)}`;
@@ -1233,10 +1696,130 @@ function analysisBundles() {
1233
1696
  }
1234
1697
 
1235
1698
  // src/cache.ts
1699
+ import { createHash as createHash3 } from "crypto";
1700
+ import fs5 from "fs/promises";
1701
+ import path6 from "path";
1702
+ import { readJsonFile, writeDocumentFile } from "@suss/behavioral-ir/files";
1703
+
1704
+ // src/cacheStamps.ts
1236
1705
  import { createHash as createHash2 } from "crypto";
1237
- import fs2 from "fs/promises";
1238
- import path3 from "path";
1239
- var SCHEMA_VERSION = "7";
1706
+ import fs3 from "fs/promises";
1707
+ var RECENT_CHANGE_MS = 2e3;
1708
+ function stampMayMissAWrite(stat, statAt) {
1709
+ return Math.max(stat.mtimeMs, stat.ctimeMs) > statAt - RECENT_CHANGE_MS;
1710
+ }
1711
+ async function startRun(files, configPath) {
1712
+ const startedAt = Date.now();
1713
+ const config = configPath === void 0 ? null : await statAtStart(configPath);
1714
+ const stats = await Promise.all(files.map(statAtStart));
1715
+ stats.sort(
1716
+ (a, b) => a.stamp.path < b.stamp.path ? -1 : a.stamp.path > b.stamp.path ? 1 : 0
1717
+ );
1718
+ return {
1719
+ startedAt,
1720
+ config: config?.ctimeMs === null ? null : config,
1721
+ files: stats,
1722
+ hashes: /* @__PURE__ */ new Map()
1723
+ };
1724
+ }
1725
+ async function statAtStart(filePath) {
1726
+ try {
1727
+ const stat = await fs3.stat(filePath);
1728
+ return {
1729
+ stamp: { path: filePath, mtimeMs: stat.mtimeMs, size: stat.size },
1730
+ ctimeMs: stat.ctimeMs
1731
+ };
1732
+ } catch {
1733
+ return { stamp: { path: filePath, mtimeMs: -1, size: -1 }, ctimeMs: null };
1734
+ }
1735
+ }
1736
+ async function hashOf(run, filePath) {
1737
+ const known = run.hashes.get(filePath);
1738
+ if (known !== void 0) {
1739
+ return known;
1740
+ }
1741
+ const hash = await hashFile(filePath);
1742
+ run.hashes.set(filePath, hash);
1743
+ return hash;
1744
+ }
1745
+ async function hashRecentChanges(run) {
1746
+ const recent = [...run.config === null ? [] : [run.config], ...run.files].filter(
1747
+ (file) => file.ctimeMs !== null && stampMayMissAWrite(
1748
+ { mtimeMs: file.stamp.mtimeMs, ctimeMs: file.ctimeMs },
1749
+ run.startedAt
1750
+ )
1751
+ ).map((file) => file.stamp.path);
1752
+ await Promise.all(recent.map((filePath) => hashOf(run, filePath)));
1753
+ }
1754
+ async function stillMatches(stored, found, run) {
1755
+ if (!fileStampEquals(stored, found)) {
1756
+ return false;
1757
+ }
1758
+ if (stored === null || stored.verifyHash !== true) {
1759
+ return true;
1760
+ }
1761
+ return await hashOf(run, stored.path) === stored.contentHash;
1762
+ }
1763
+ async function filesStillMatch(stored, run) {
1764
+ if (stored.length !== run.files.length || !stored.every((s, i) => fileStampEquals(s, run.files[i]?.stamp ?? null))) {
1765
+ return false;
1766
+ }
1767
+ const checks = await Promise.all(
1768
+ stored.map((s, i) => stillMatches(s, run.files[i]?.stamp ?? null, run))
1769
+ );
1770
+ return checks.every(Boolean);
1771
+ }
1772
+ async function recordedStamp(start, run, prior) {
1773
+ if (start.ctimeMs === null) {
1774
+ return start.stamp;
1775
+ }
1776
+ const filePath = start.stamp.path;
1777
+ const now = await fs3.stat(filePath).catch(() => null);
1778
+ if (now === null || now.mtimeMs !== start.stamp.mtimeMs || now.size !== start.stamp.size || now.ctimeMs !== start.ctimeMs) {
1779
+ return null;
1780
+ }
1781
+ const atStart = { mtimeMs: start.stamp.mtimeMs, ctimeMs: start.ctimeMs };
1782
+ if (stampMayMissAWrite(atStart, run.startedAt)) {
1783
+ const readBefore = run.hashes.get(filePath);
1784
+ return readBefore === void 0 || readBefore === null ? null : { ...start.stamp, contentHash: readBefore, verifyHash: true };
1785
+ }
1786
+ const hash = run.hashes.get(filePath) ?? reusableHash(prior, start.stamp) ?? await hashFile(filePath);
1787
+ return hash === null ? start.stamp : { ...start.stamp, contentHash: hash };
1788
+ }
1789
+ function reusableHash(prior, stamp) {
1790
+ return prior !== void 0 && prior.verifyHash !== true && fileStampEquals(prior, stamp) ? prior.contentHash : void 0;
1791
+ }
1792
+ async function hashFile(filePath) {
1793
+ try {
1794
+ const content = await fs3.readFile(filePath);
1795
+ return createHash2("sha256").update(content).digest("hex").slice(0, 16);
1796
+ } catch {
1797
+ return null;
1798
+ }
1799
+ }
1800
+ function fileStampEquals(a, b) {
1801
+ if (a === null && b === null) {
1802
+ return true;
1803
+ }
1804
+ if (a === null || b === null) {
1805
+ return false;
1806
+ }
1807
+ return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
1808
+ }
1809
+
1810
+ // src/ignoredDirectory.ts
1811
+ import fs4 from "fs";
1812
+ import path5 from "path";
1813
+ function makeIgnoredDirectory(dir) {
1814
+ fs4.mkdirSync(dir, { recursive: true });
1815
+ const ignore = path5.join(dir, ".gitignore");
1816
+ if (!fs4.existsSync(ignore)) {
1817
+ fs4.writeFileSync(ignore, "*\n");
1818
+ }
1819
+ }
1820
+
1821
+ // src/cache.ts
1822
+ var SCHEMA_VERSION = "9";
1240
1823
  var MAX_ENTRIES = 2;
1241
1824
  var ENTRY_PREFIX = "key-";
1242
1825
  var ENTRY_DIR_NAME = new RegExp(`^${ENTRY_PREFIX}[0-9a-f]{16}$`);
@@ -1253,56 +1836,88 @@ function createCacheLayer(cacheDir) {
1253
1836
  }
1254
1837
  };
1255
1838
  }
1839
+ const manifests = new ManifestReads();
1840
+ const runs = /* @__PURE__ */ new WeakMap();
1841
+ const runFor = async (input) => runs.get(input) ?? await startRun(input.files, input.configPath);
1842
+ const lookupWith = async (input, run) => {
1843
+ const entryDir = entryDirFor(cacheDir, input);
1844
+ const manifest = await manifests.readAtRunStart(
1845
+ path6.join(entryDir, "manifest.json")
1846
+ );
1847
+ if (manifest === null) {
1848
+ return missDiag(await describeAbsentEntry(cacheDir));
1849
+ }
1850
+ if (!await stillMatches(
1851
+ manifest.configStamp,
1852
+ run.config?.stamp ?? null,
1853
+ run
1854
+ )) {
1855
+ return missDiag("config-changed");
1856
+ }
1857
+ if (!await filesStillMatch(manifest.files, run)) {
1858
+ return missDiag("files-changed");
1859
+ }
1860
+ await markUsed(entryDir);
1861
+ manifests.forget();
1862
+ return {
1863
+ kind: "hit",
1864
+ summaries: manifest.summaries,
1865
+ diagnostic: { kind: "hit" }
1866
+ };
1867
+ };
1256
1868
  return {
1257
1869
  async tryHit(input) {
1258
1870
  const result = await this.lookup(input);
1259
1871
  return result.kind === "hit" ? result.summaries : null;
1260
1872
  },
1261
1873
  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));
1268
- }
1269
- const currentConfigStamp = await stampConfigFile(input.configPath);
1270
- if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
1271
- return missDiag("config-changed");
1874
+ const run = await startRun(input.files, input.configPath);
1875
+ runs.set(input, run);
1876
+ const result = await lookupWith(input, run);
1877
+ if (result.kind === "miss") {
1878
+ await hashRecentChanges(run);
1272
1879
  }
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
- };
1880
+ return result;
1283
1881
  },
1284
1882
  async plan(input) {
1285
1883
  const entryDir = entryDirFor(cacheDir, input);
1286
- const manifest = await readManifest(
1287
- path3.join(entryDir, "manifest.json")
1884
+ const manifest = await manifests.readAgain(
1885
+ path6.join(entryDir, "manifest.json")
1288
1886
  );
1289
1887
  if (manifest === null || manifest.roots === void 0 || manifest.rootMeta === void 0 || manifest.owners === void 0) {
1290
1888
  return null;
1291
1889
  }
1292
- const currentConfigStamp = await stampConfigFile(input.configPath);
1293
- if (!fileStampEquals(manifest.configStamp, currentConfigStamp)) {
1890
+ const run = await runFor(input);
1891
+ if (!await stillMatches(
1892
+ manifest.configStamp,
1893
+ run.config?.stamp ?? null,
1894
+ run
1895
+ )) {
1294
1896
  return null;
1295
1897
  }
1296
1898
  await markUsed(entryDir);
1297
- return buildPlan(manifest, await resolveFileStamps(input));
1899
+ return buildPlan(manifest, run);
1298
1900
  },
1299
1901
  async write(input, summaries, attribution) {
1300
1902
  const entryDir = entryDirFor(cacheDir, input);
1301
- const previous = await readManifest(
1302
- path3.join(entryDir, "manifest.json")
1903
+ const previous = await manifests.readAgain(
1904
+ path6.join(entryDir, "manifest.json")
1303
1905
  );
1304
- const configStamp = await stampConfigFile(input.configPath);
1305
- const files = await hashStamps(await resolveFileStamps(input), previous);
1906
+ manifests.forget();
1907
+ const known = runs.get(input);
1908
+ const run = known ?? await runFor(input);
1909
+ if (known === void 0) {
1910
+ await hashRecentChanges(run);
1911
+ }
1912
+ runs.delete(input);
1913
+ const configStamp = run.config === null ? null : await recordedStamp(run.config, run, void 0);
1914
+ const prior = new Map((previous?.files ?? []).map((f) => [f.path, f]));
1915
+ const recorded = await Promise.all(
1916
+ run.files.map(
1917
+ (file) => recordedStamp(file, run, prior.get(file.stamp.path))
1918
+ )
1919
+ );
1920
+ const files = recorded.filter((stamp) => stamp !== null);
1306
1921
  const manifest = {
1307
1922
  schemaVersion: SCHEMA_VERSION,
1308
1923
  adapterPacksDigest: input.adapterPacksDigest,
@@ -1311,19 +1926,17 @@ function createCacheLayer(cacheDir) {
1311
1926
  summaries,
1312
1927
  ...attribution === void 0 ? {} : encodeAttribution(attribution)
1313
1928
  };
1314
- await fs2.mkdir(entryDir, { recursive: true });
1315
- await fs2.writeFile(
1316
- path3.join(entryDir, "manifest.json"),
1317
- JSON.stringify(manifest)
1318
- );
1929
+ makeIgnoredDirectory(cacheDir);
1930
+ writeDocumentFile(path6.join(entryDir, "manifest.json"), manifest);
1319
1931
  await evictOldEntries(cacheDir, entryDir);
1320
1932
  }
1321
1933
  };
1322
1934
  }
1323
- async function buildPlan(manifest, currentStamps) {
1935
+ async function buildPlan(manifest, run) {
1324
1936
  const stored = new Map(manifest.files.map((f) => [f.path, f]));
1325
- const current = new Map(currentStamps.map((f) => [f.path, f]));
1937
+ const current = new Map(run.files.map((f) => [f.stamp.path, f.stamp]));
1326
1938
  const changed = /* @__PURE__ */ new Set();
1939
+ const added = /* @__PURE__ */ new Set();
1327
1940
  const removed = /* @__PURE__ */ new Set();
1328
1941
  for (const p of stored.keys()) {
1329
1942
  if (!current.has(p)) {
@@ -1335,17 +1948,20 @@ async function buildPlan(manifest, currentStamps) {
1335
1948
  const before = stored.get(p);
1336
1949
  if (before === void 0) {
1337
1950
  changed.add(p);
1951
+ added.add(p);
1338
1952
  } else if (!fileStampEquals(before, stamp)) {
1339
1953
  if (before.contentHash === void 0) {
1340
1954
  changed.add(p);
1341
1955
  } else {
1342
1956
  toVerify.push(p);
1343
1957
  }
1958
+ } else if (before.verifyHash === true) {
1959
+ toVerify.push(p);
1344
1960
  }
1345
1961
  }
1346
1962
  await Promise.all(
1347
1963
  toVerify.map(async (p) => {
1348
- const hash = await hashFile(p);
1964
+ const hash = await hashOf(run, p);
1349
1965
  if (hash === null || hash !== stored.get(p)?.contentHash) {
1350
1966
  changed.add(p);
1351
1967
  }
@@ -1354,6 +1970,10 @@ async function buildPlan(manifest, currentStamps) {
1354
1970
  const roots = /* @__PURE__ */ new Map();
1355
1971
  const rootNames = manifest.roots ?? [];
1356
1972
  const depPaths = manifest.depPaths ?? [];
1973
+ const pathsOf = (ids) => ids.flatMap((d) => {
1974
+ const p = depPaths[d];
1975
+ return p === void 0 ? [] : [p];
1976
+ });
1357
1977
  let rootsDeclined = 0;
1358
1978
  rootNames.forEach((rootPath, i) => {
1359
1979
  const meta = manifest.rootMeta?.[i];
@@ -1366,35 +1986,54 @@ async function buildPlan(manifest, currentStamps) {
1366
1986
  roots.set(rootPath, {
1367
1987
  path: rootPath,
1368
1988
  cacheable: meta.cacheable,
1369
- deps: meta.deps.flatMap((d) => {
1370
- const p = depPaths[d];
1371
- return p === void 0 ? [] : [p];
1372
- }),
1989
+ deps: pathsOf(meta.deps),
1373
1990
  claims: meta.claims,
1374
1991
  meta: meta.meta,
1375
- packs: meta.packs
1992
+ packs: meta.packs,
1993
+ ...meta.imports === void 0 ? {} : { imports: pathsOf(meta.imports) }
1376
1994
  });
1377
1995
  });
1996
+ const unchanged = (filePath, deps) => current.has(filePath) && !changed.has(filePath) && !deps.some((d) => changed.has(d) || removed.has(d));
1378
1997
  const validRoots = /* @__PURE__ */ new Set();
1379
1998
  for (const [rootPath, record] of roots) {
1380
- if (!record.cacheable || !current.has(rootPath)) {
1381
- continue;
1999
+ if (record.cacheable && unchanged(rootPath, record.deps)) {
2000
+ validRoots.add(rootPath);
1382
2001
  }
1383
- if (changed.has(rootPath)) {
1384
- continue;
2002
+ }
2003
+ const units = (manifest.units ?? []).flatMap((stored2) => {
2004
+ const file = depPaths[stored2.file];
2005
+ return file === void 0 ? [] : [
2006
+ {
2007
+ key: stored2.key,
2008
+ file,
2009
+ deps: pathsOf(stored2.deps),
2010
+ data: stored2.data
2011
+ }
2012
+ ];
2013
+ });
2014
+ const validUnits = /* @__PURE__ */ new Map();
2015
+ for (const unit of units) {
2016
+ if (unchanged(unit.file, unit.deps)) {
2017
+ validUnits.set(unit.key, unit);
1385
2018
  }
1386
- const depMoved = record.deps.some((d) => changed.has(d) || removed.has(d));
1387
- if (!depMoved) {
1388
- validRoots.add(rootPath);
2019
+ }
2020
+ const resolvedImports = /* @__PURE__ */ new Map();
2021
+ if (added.size === 0 && removed.size === 0) {
2022
+ for (const [rootPath, record] of roots) {
2023
+ if (record.imports !== void 0 && !changed.has(rootPath)) {
2024
+ resolvedImports.set(rootPath, record.imports);
2025
+ }
1389
2026
  }
1390
2027
  }
1391
2028
  const owners = manifest.owners ?? [];
1392
2029
  return {
1393
2030
  changed,
1394
2031
  removed,
2032
+ resolvedImports,
1395
2033
  roots,
1396
2034
  validRoots,
1397
2035
  rootsDeclined,
2036
+ validUnits,
1398
2037
  reuse(valid) {
1399
2038
  const summaries = [];
1400
2039
  const reusedOwners = [];
@@ -1421,7 +2060,8 @@ async function buildPlan(manifest, currentStamps) {
1421
2060
  const p = rootNames[o];
1422
2061
  return p === void 0 ? [] : [p];
1423
2062
  })
1424
- )
2063
+ ),
2064
+ units
1425
2065
  };
1426
2066
  }
1427
2067
  };
@@ -1446,7 +2086,8 @@ function encodeAttribution(attribution) {
1446
2086
  deps: r.deps.map(depIdOf),
1447
2087
  claims: r.claims,
1448
2088
  meta: r.meta,
1449
- packs: r.packs
2089
+ packs: r.packs,
2090
+ ...r.imports === void 0 ? {} : { imports: r.imports.map(depIdOf) }
1450
2091
  }));
1451
2092
  const owners = attribution.owners.map(
1452
2093
  (ownerPaths) => ownerPaths.flatMap((p) => {
@@ -1454,32 +2095,23 @@ function encodeAttribution(attribution) {
1454
2095
  return i === void 0 ? [] : [i];
1455
2096
  })
1456
2097
  );
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
- }
2098
+ const units = attribution.units?.map((unit) => ({
2099
+ key: unit.key,
2100
+ file: depIdOf(unit.file),
2101
+ deps: unit.deps.map(depIdOf),
2102
+ data: unit.data
2103
+ }));
2104
+ return {
2105
+ roots,
2106
+ rootMeta,
2107
+ depPaths,
2108
+ owners,
2109
+ ...units === void 0 ? {} : { units }
2110
+ };
1479
2111
  }
1480
2112
  async function describeAbsentEntry(cacheDir) {
1481
2113
  try {
1482
- const entries = await fs2.readdir(cacheDir, { withFileTypes: true });
2114
+ const entries = await fs5.readdir(cacheDir, { withFileTypes: true });
1483
2115
  return entries.some((e) => e.isDirectory() && isEntryDir(e.name)) ? "key-changed" : "no-manifest";
1484
2116
  } catch {
1485
2117
  return "no-manifest";
@@ -1491,8 +2123,8 @@ function entryDirFor(cacheDir, input) {
1491
2123
  input.adapterPacksDigest,
1492
2124
  input.configPath ?? ""
1493
2125
  ].join(" ");
1494
- const name = createHash2("sha256").update(key).digest("hex").slice(0, 16);
1495
- return path3.join(cacheDir, `${ENTRY_PREFIX}${name}`);
2126
+ const name = createHash3("sha256").update(key).digest("hex").slice(0, 16);
2127
+ return path6.join(cacheDir, `${ENTRY_PREFIX}${name}`);
1496
2128
  }
1497
2129
  function isEntryDir(name) {
1498
2130
  return ENTRY_DIR_NAME.test(name);
@@ -1500,24 +2132,24 @@ function isEntryDir(name) {
1500
2132
  async function markUsed(entryDir) {
1501
2133
  const now = /* @__PURE__ */ new Date();
1502
2134
  try {
1503
- await fs2.utimes(entryDir, now, now);
2135
+ await fs5.utimes(entryDir, now, now);
1504
2136
  } catch {
1505
2137
  }
1506
2138
  }
1507
2139
  async function evictOldEntries(cacheDir, keepDir) {
1508
2140
  try {
1509
- await fs2.rm(path3.join(cacheDir, "manifest.json"), { force: true });
1510
- const entries = await fs2.readdir(cacheDir, { withFileTypes: true });
2141
+ await fs5.rm(path6.join(cacheDir, "manifest.json"), { force: true });
2142
+ const entries = await fs5.readdir(cacheDir, { withFileTypes: true });
1511
2143
  const dirs = await Promise.all(
1512
2144
  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);
2145
+ const dir = path6.join(cacheDir, e.name);
2146
+ const stat = await fs5.stat(dir);
1515
2147
  return { dir, mtimeMs: stat.mtimeMs };
1516
2148
  })
1517
2149
  );
1518
2150
  const doomed = dirs.filter((d) => d.dir !== keepDir).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_ENTRIES - 1);
1519
2151
  await Promise.all(
1520
- doomed.map((d) => fs2.rm(d.dir, { recursive: true, force: true }))
2152
+ doomed.map((d) => fs5.rm(d.dir, { recursive: true, force: true }))
1521
2153
  );
1522
2154
  } catch {
1523
2155
  }
@@ -1528,51 +2160,40 @@ function missDiag(reason) {
1528
2160
  diagnostic: { kind: "miss", missReason: reason }
1529
2161
  };
1530
2162
  }
1531
- async function readManifest(manifestPath) {
2163
+ var ManifestReads = class {
2164
+ last = null;
2165
+ async readAtRunStart(manifestPath) {
2166
+ const stamp = await manifestStamp(manifestPath);
2167
+ const manifest = await readManifest(manifestPath);
2168
+ this.last = { path: manifestPath, stamp, manifest };
2169
+ return manifest;
2170
+ }
2171
+ async readAgain(manifestPath) {
2172
+ const last = this.last;
2173
+ if (last !== null && last.path === manifestPath && last.stamp !== null && last.stamp === await manifestStamp(manifestPath)) {
2174
+ return last.manifest;
2175
+ }
2176
+ return await this.readAtRunStart(manifestPath);
2177
+ }
2178
+ forget() {
2179
+ this.last = null;
2180
+ }
2181
+ };
2182
+ async function manifestStamp(manifestPath) {
1532
2183
  try {
1533
- const raw = await fs2.readFile(manifestPath, "utf-8");
1534
- return JSON.parse(raw);
2184
+ const stat = await fs5.stat(manifestPath);
2185
+ return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
1535
2186
  } catch {
1536
2187
  return null;
1537
2188
  }
1538
2189
  }
1539
- async function stampConfigFile(configPath) {
1540
- if (configPath === void 0) {
1541
- return null;
1542
- }
2190
+ async function readManifest(manifestPath) {
1543
2191
  try {
1544
- const stat = await fs2.stat(configPath);
1545
- return { path: configPath, mtimeMs: stat.mtimeMs, size: stat.size };
2192
+ return readJsonFile(manifestPath);
1546
2193
  } catch {
1547
2194
  return null;
1548
2195
  }
1549
2196
  }
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
2197
 
1577
2198
  // src/extractionReport.ts
1578
2199
  import { BOUNDARY_ROLE } from "@suss/behavioral-ir";
@@ -1681,6 +2302,38 @@ function firstEmptyUngatedStage(packs, filesWalked) {
1681
2302
  return "assembly";
1682
2303
  }
1683
2304
 
2305
+ // src/keptParses.ts
2306
+ var KeptParses = class {
2307
+ constructor(release = () => {
2308
+ }) {
2309
+ this.release = release;
2310
+ }
2311
+ release;
2312
+ byFile = /* @__PURE__ */ new Map();
2313
+ async parse(file, source, make) {
2314
+ const kept = this.byFile.get(file);
2315
+ if (kept !== void 0 && kept.source === source) {
2316
+ return kept.value;
2317
+ }
2318
+ const value = await make(source);
2319
+ if (kept !== void 0) {
2320
+ this.release(kept.value);
2321
+ }
2322
+ this.byFile.set(file, { source, value });
2323
+ return value;
2324
+ }
2325
+ /** Lets go of every file not in this run. */
2326
+ keepOnly(files) {
2327
+ const wanted = new Set(files);
2328
+ for (const [file, kept] of this.byFile) {
2329
+ if (!wanted.has(file)) {
2330
+ this.release(kept.value);
2331
+ this.byFile.delete(file);
2332
+ }
2333
+ }
2334
+ }
2335
+ };
2336
+
1684
2337
  // src/packHealth.ts
1685
2338
  function stagesOf(funnel) {
1686
2339
  const stages = [];
@@ -1727,8 +2380,8 @@ function funnelDrops(packs) {
1727
2380
  function recognizersWithNoUnits(packs) {
1728
2381
  return packs.filter(
1729
2382
  (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.
2383
+ // roots, so effects recognized there mean the run worked and only
2384
+ // the framework pack's attribution is missing.
1732
2385
  funnel.effectsRecognized === 0
1733
2386
  ).map((funnel) => ({
1734
2387
  label: funnel.pack,
@@ -1970,7 +2623,7 @@ function makeTransitionId(functionName, branch) {
1970
2623
  const { terminal } = branch;
1971
2624
  const statusKey = terminal.statusCode === null ? "none" : terminal.statusCode.type === "literal" ? String(terminal.statusCode.value) : `dyn:${terminal.statusCode.sourceText}`;
1972
2625
  const conditionSig = branch.conditions.map((c) => `${c.polarity}:${c.sourceText}`).join(";");
1973
- const conditionHash = createHash3("sha1").update(conditionSig).digest("hex").slice(0, 7);
2626
+ const conditionHash = createHash4("sha1").update(conditionSig).digest("hex").slice(0, 7);
1974
2627
  return `${functionName}:${terminal.kind}:${statusKey}:${conditionHash}`;
1975
2628
  }
1976
2629
  function collapseReading(table, reading) {
@@ -2029,6 +2682,7 @@ function unreadSentences(raw) {
2029
2682
  ...raw.branches.flatMap(readingsOfBranch)
2030
2683
  ];
2031
2684
  return [
2685
+ ...raw.readFailure !== void 0 ? [raw.readFailure] : [],
2032
2686
  ...raw.unreadBinding !== void 0 ? [raw.unreadBinding] : [],
2033
2687
  ...handedOver.map(unreadReasonOf).filter((reason) => reason !== null)
2034
2688
  ];
@@ -2040,6 +2694,39 @@ function effectsOfBranch(branch) {
2040
2694
  ];
2041
2695
  return foldRepeatedEffects(effects.map(withNormalizedCallee));
2042
2696
  }
2697
+ function provenanceOf(raw, effects) {
2698
+ if (raw.length === 0) {
2699
+ return [];
2700
+ }
2701
+ const positionOf = new Map(effects.map((one, at) => [effectKey(one), at]));
2702
+ const bySlot = /* @__PURE__ */ new Map();
2703
+ const said = /* @__PURE__ */ new Map();
2704
+ for (const one of raw) {
2705
+ const effect = positionOf.get(effectKey(withNormalizedCallee(one.effect)));
2706
+ if (effect === void 0) {
2707
+ continue;
2708
+ }
2709
+ const slotKey = `${effect}\0${one.slot}\0${one.name}`;
2710
+ let entry = bySlot.get(slotKey);
2711
+ let seen = said.get(slotKey);
2712
+ if (entry === void 0 || seen === void 0) {
2713
+ entry = { at: { slot: one.slot, effect, name: one.name }, from: [] };
2714
+ seen = /* @__PURE__ */ new Set();
2715
+ bySlot.set(slotKey, entry);
2716
+ said.set(slotKey, seen);
2717
+ }
2718
+ for (const source of one.from) {
2719
+ const spelled = JSON.stringify(source);
2720
+ if (!seen.has(spelled)) {
2721
+ seen.add(spelled);
2722
+ entry.from.push(source);
2723
+ }
2724
+ }
2725
+ }
2726
+ return [...bySlot.values()].sort(
2727
+ (a, b) => a.at.effect - b.at.effect || a.at.slot.localeCompare(b.at.slot) || a.at.name.localeCompare(b.at.name)
2728
+ );
2729
+ }
2043
2730
  function withNormalizedCallee(effect) {
2044
2731
  if (effect.type !== "invocation" && effect.type !== "interaction") {
2045
2732
  return effect;
@@ -2055,17 +2742,22 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2055
2742
  const conditions = branch.conditions.map(
2056
2743
  rawConditionToPredicate
2057
2744
  );
2745
+ const effects = effectsOfBranch(branch);
2058
2746
  const transition = {
2059
2747
  id: makeTransitionId(raw.identity.name, branch),
2060
2748
  conditions,
2061
2749
  output: terminalToOutput(branch.terminal),
2062
- effects: effectsOfBranch(branch),
2750
+ effects,
2063
2751
  location: branch.location,
2064
2752
  isDefault: branch.isDefault
2065
2753
  };
2066
2754
  if (branch.expectedInput != null) {
2067
2755
  transition.expectedInput = branch.expectedInput;
2068
2756
  }
2757
+ const provenance = provenanceOf(branch.provenance ?? [], effects);
2758
+ if (provenance.length > 0) {
2759
+ transition.provenance = provenance;
2760
+ }
2069
2761
  return transition;
2070
2762
  });
2071
2763
  const gaps = detectGaps(raw, transitions, options);
@@ -2127,6 +2819,9 @@ function buildMetadata(raw) {
2127
2819
  if (raw.requestSpelling !== void 0) {
2128
2820
  metadata = withRequestSpellingMetadata(metadata, raw.requestSpelling);
2129
2821
  }
2822
+ if (raw.test !== void 0) {
2823
+ metadata = withTestMetadata(metadata, raw.test);
2824
+ }
2130
2825
  return Object.keys(metadata).length > 0 ? metadata : null;
2131
2826
  }
2132
2827
  function buildHttpMetadataValue(raw) {
@@ -2150,6 +2845,12 @@ function buildHttpMetadataValue(raw) {
2150
2845
  if (raw.failureDelivery !== void 0) {
2151
2846
  http.failureDelivery = raw.failureDelivery;
2152
2847
  }
2848
+ if (raw.redirectDelivery !== void 0) {
2849
+ http.redirectDelivery = raw.redirectDelivery;
2850
+ }
2851
+ if (raw.declaredStatuses !== void 0 && raw.declaredStatuses.length > 0) {
2852
+ http.declaredStatuses = raw.declaredStatuses;
2853
+ }
2153
2854
  return Object.keys(http).length > 0 ? http : null;
2154
2855
  }
2155
2856
  function buildGraphqlMetadataValue(raw) {
@@ -2217,13 +2918,16 @@ function detectGaps(raw, transitions, options) {
2217
2918
  return gaps;
2218
2919
  }
2219
2920
  function bodyWentUnread(raw) {
2921
+ if (raw.readFailure !== void 0) {
2922
+ return true;
2923
+ }
2220
2924
  if (raw.bodyContent === "absent" || raw.bodyContent === "elsewhere") {
2221
2925
  return true;
2222
2926
  }
2223
2927
  return raw.bodyContent === "statements" && raw.branches.length === 0;
2224
2928
  }
2225
2929
  function describeUnreadBody(raw) {
2226
- if (!bodyWentUnread(raw)) {
2930
+ if (!bodyWentUnread(raw) || raw.readFailure !== void 0) {
2227
2931
  return null;
2228
2932
  }
2229
2933
  if (raw.bodyContent === "absent") {
@@ -2270,26 +2974,41 @@ function bodyToShape(body) {
2270
2974
  }
2271
2975
  return null;
2272
2976
  }
2977
+ function statusValueRef(code) {
2978
+ if (code === null) {
2979
+ return null;
2980
+ }
2981
+ return code.type === "literal" ? { type: "literal", value: code.value } : { type: "unresolved", sourceText: code.sourceText };
2982
+ }
2273
2983
  var terminalConverters = {
2274
2984
  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
2985
  const body = bodyToShape(t.body);
2277
- return { type: "response", statusCode, body, headers: {} };
2986
+ return {
2987
+ type: "response",
2988
+ statusCode: statusValueRef(t.statusCode),
2989
+ body,
2990
+ headers: {}
2991
+ };
2278
2992
  },
2279
2993
  throw: (t) => {
2280
2994
  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
2995
  return {
2283
2996
  type: "response",
2284
- statusCode,
2997
+ statusCode: statusValueRef(t.statusCode),
2285
2998
  body: bodyToShape(t.body),
2286
2999
  headers: {}
2287
3000
  };
2288
3001
  }
3002
+ const ancestry = t.exceptionAncestry;
2289
3003
  return {
2290
3004
  type: "throw",
2291
3005
  exceptionType: t.exceptionType,
2292
- message: t.message
3006
+ message: t.message,
3007
+ ...ancestry === void 0 ? {} : {
3008
+ exceptionAncestors: ancestry.ancestors,
3009
+ ...ancestry.incomplete ? { ancestryIncomplete: true } : {}
3010
+ },
3011
+ ...t.statusWhenUncaught === void 0 ? {} : { statusWhenUncaught: t.statusWhenUncaught }
2293
3012
  };
2294
3013
  },
2295
3014
  render: (t) => ({
@@ -2309,7 +3028,8 @@ var terminalConverters = {
2309
3028
  type: "return",
2310
3029
  value: bodyToShape(t.body)
2311
3030
  }),
2312
- void: (_t) => ({ type: "void" })
3031
+ void: (_t) => ({ type: "void" }),
3032
+ exit: (t) => ({ type: "exit", code: statusValueRef(t.statusCode) })
2313
3033
  };
2314
3034
  function terminalToOutput(terminal) {
2315
3035
  return terminalConverters[terminal.kind](terminal);
@@ -2352,14 +3072,19 @@ function paramToInput(param) {
2352
3072
  name: param.name,
2353
3073
  position: param.position,
2354
3074
  role: param.role,
3075
+ ...param.field === void 0 ? {} : { field: param.field },
2355
3076
  shape: param.typeText ? { type: "ref", name: param.typeText } : null
2356
3077
  };
2357
3078
  }
2358
3079
  export {
3080
+ EnclosingNodes,
2359
3081
  IdMap,
2360
3082
  IdSet,
3083
+ KeptParses,
2361
3084
  MAX_ENTRIES,
3085
+ MAX_HELPER_PATHS,
2362
3086
  MAX_PATHS,
3087
+ MODULE_SURFACE_RECOGNITION,
2363
3088
  PathBudgetExceeded,
2364
3089
  SKIP_CHILDREN,
2365
3090
  UnmodeledFlow,
@@ -2384,30 +3109,45 @@ export {
2384
3109
  enumerateOrDegrade,
2385
3110
  enumerateStructuredPaths,
2386
3111
  evaluatePackHealth,
3112
+ extractionConfigStamp,
2387
3113
  firstWrittenReading,
2388
3114
  formatPackHealth,
2389
3115
  guardsHoldOn,
3116
+ helperPathsOnVariables,
2390
3117
  httpRouteDiscovery,
3118
+ isListedTestFile,
3119
+ makeIgnoredDirectory,
2391
3120
  makeTransitionId,
2392
3121
  mapReading,
3122
+ markReturnsAsExitCode,
3123
+ matchesTestFileName,
3124
+ mergeReadShapes,
2393
3125
  moduleInitStructure,
3126
+ moduleOfFile,
2394
3127
  noopTimer,
2395
3128
  packGradients,
2396
3129
  paramToInput,
2397
3130
  projectFileStamp,
2398
3131
  rawConditionToPredicate,
2399
3132
  recordPackFailure,
3133
+ redirectDeliveryWhenSet,
2400
3134
  registrationHelperDiscovery,
2401
3135
  routeHelperIndex,
2402
3136
  runDigest,
2403
3137
  runsBefore,
2404
3138
  scopeOption,
3139
+ settleModules,
2405
3140
  sharedGatingConditions,
3141
+ sourceRefsOf,
3142
+ splitAtHandOffs,
3143
+ stampMayMissAWrite,
2406
3144
  stampModuleImports,
3145
+ stampModules,
2407
3146
  storageSystemOption,
2408
3147
  summaryCountsByPack,
2409
3148
  tallyUnit,
2410
3149
  terminalToOutput,
3150
+ throughHelper,
2411
3151
  unreadableReading,
2412
3152
  unwrapJsonStringify,
2413
3153
  valueToReadFurtherFrom,