@suss/adapter-typescript 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/adapter.ts
2
- import path11 from "path";
2
+ import path10 from "path";
3
3
  import {
4
4
  Node as Node64,
5
5
  Project,
@@ -21,7 +21,10 @@ import {
21
21
  import { Database as Database5 } from "@suss/datalog";
22
22
  import {
23
23
  assembleSummary as assembleSummary3,
24
- composeWrappers
24
+ composeWrappers,
25
+ createCacheLayer,
26
+ runDigest,
27
+ stampModuleImports
25
28
  } from "@suss/extractor";
26
29
 
27
30
  // src/assembly.ts
@@ -40,6 +43,7 @@ import { Node as Node5, SyntaxKind as SyntaxKind2 } from "ts-morph";
40
43
  import { Node as Node2, SyntaxKind } from "ts-morph";
41
44
 
42
45
  // src/walk/nodeKeys.ts
46
+ import { declarationKey } from "@suss/behavioral-ir";
43
47
  function offsetKeyOf(node) {
44
48
  return offsetKeyFor(node.getSourceFile().getFilePath(), {
45
49
  start: node.getStart(),
@@ -47,7 +51,7 @@ function offsetKeyOf(node) {
47
51
  });
48
52
  }
49
53
  function offsetKeyFor(file, span) {
50
- return `${file}:${span.start}-${span.end}`;
54
+ return declarationKey(file, span);
51
55
  }
52
56
  function lineRangeKey(file, startLine, endLine) {
53
57
  return `${file}:${startLine}-${endLine}`;
@@ -1867,8 +1871,8 @@ function loadImportGraphsDepthFirstFromPaths(project, rootPaths) {
1867
1871
  stack.pop();
1868
1872
  loadOrder.push(top.path);
1869
1873
  let deepest = 0;
1870
- for (const path15 of top.deps) {
1871
- deepest = Math.max(deepest, chainDepth.get(path15) ?? 0);
1874
+ for (const path14 of top.deps) {
1875
+ deepest = Math.max(deepest, chainDepth.get(path14) ?? 0);
1872
1876
  }
1873
1877
  chainDepth.set(top.path, deepest + 1);
1874
1878
  }
@@ -2220,11 +2224,11 @@ function walkAndWarm(root, reach) {
2220
2224
  if (warmedFiles.get(target) !== void 0) {
2221
2225
  continue;
2222
2226
  }
2223
- const path15 = target.getFilePath();
2224
- if (visited.has(path15)) {
2227
+ const path14 = target.getFilePath();
2228
+ if (visited.has(path14)) {
2225
2229
  continue;
2226
2230
  }
2227
- visited.add(path15);
2231
+ visited.add(path14);
2228
2232
  stack.push(frameOf(target, reach));
2229
2233
  continue;
2230
2234
  }
@@ -2241,7 +2245,7 @@ function sitsOnWarmChains(file) {
2241
2245
  return false;
2242
2246
  }
2243
2247
  const imported = importedFilePathsOf(project, file.getFilePath());
2244
- return imported.length > 0 && imported.every((path15) => warmed.has(path15));
2248
+ return imported.length > 0 && imported.every((path14) => warmed.has(path14));
2245
2249
  }
2246
2250
  function markWarmed(file) {
2247
2251
  warmedFiles.set(file, true);
@@ -2558,6 +2562,32 @@ function writesToBindingUncached(declaration) {
2558
2562
  }
2559
2563
  return { values, inOrder: writesRunInOrder(declaration, writes) };
2560
2564
  }
2565
+ function sameConstructionAcrossWrites(values) {
2566
+ const candidates = values.filter((value) => !isNotYetPlaceholder(value));
2567
+ if (candidates.length === 0) {
2568
+ return null;
2569
+ }
2570
+ let construction = null;
2571
+ for (const value of candidates) {
2572
+ if (!isConstruction(value)) {
2573
+ return null;
2574
+ }
2575
+ if (construction !== null && sourceOf(value) !== sourceOf(construction)) {
2576
+ return null;
2577
+ }
2578
+ construction = value;
2579
+ }
2580
+ return construction;
2581
+ }
2582
+ function isNotYetPlaceholder(value) {
2583
+ return value.getKind() === SyntaxKind3.NullKeyword || Node13.isIdentifier(value) && value.getText() === "undefined";
2584
+ }
2585
+ function isConstruction(value) {
2586
+ return Node13.isCallExpression(value) || Node13.isNewExpression(value);
2587
+ }
2588
+ function sourceOf(node) {
2589
+ return node.getText().replace(/\s+/g, " ").trim();
2590
+ }
2561
2591
  function writesToField(declaration) {
2562
2592
  const remembered = byField.get(declaration);
2563
2593
  if (remembered !== void 0) {
@@ -2648,6 +2678,9 @@ function assignmentsInFile(sourceFile) {
2648
2678
  function isAssignmentOperator(kind) {
2649
2679
  return kind >= SyntaxKind3.FirstAssignment && kind <= SyntaxKind3.LastAssignment;
2650
2680
  }
2681
+ function writesItsWholeValue(kind) {
2682
+ return kind === SyntaxKind3.EqualsToken || kind === SyntaxKind3.QuestionQuestionEqualsToken || kind === SyntaxKind3.BarBarEqualsToken;
2683
+ }
2651
2684
  function writeAt(node) {
2652
2685
  if (Node13.isBinaryExpression(node)) {
2653
2686
  const operator = node.getOperatorToken().getKind();
@@ -2655,7 +2688,7 @@ function writeAt(node) {
2655
2688
  if (!isAssignmentOperator(operator) || !(Node13.isIdentifier(target) || Node13.isPropertyAccessExpression(target))) {
2656
2689
  return null;
2657
2690
  }
2658
- const value = operator === SyntaxKind3.EqualsToken ? node.getRight() : null;
2691
+ const value = writesItsWholeValue(operator) ? node.getRight() : null;
2659
2692
  return { target, write: { value, node } };
2660
2693
  }
2661
2694
  if (Node13.isPrefixUnaryExpression(node) || Node13.isPostfixUnaryExpression(node)) {
@@ -3046,11 +3079,16 @@ function emitBindingValues(db, table, declaration) {
3046
3079
  }
3047
3080
  const { values, inOrder } = writesToBinding(declaration);
3048
3081
  const last = values[values.length - 1];
3049
- if (!inOrder || last === void 0) {
3050
- reassignedUnstated.add(declarationId);
3082
+ if (inOrder && last !== void 0) {
3083
+ fact(db, "endsHolding", declarationId, emitValue(db, table, last));
3084
+ return;
3085
+ }
3086
+ const construction = sameConstructionAcrossWrites(values);
3087
+ if (construction !== null) {
3088
+ fact(db, "endsHolding", declarationId, emitValue(db, table, construction));
3051
3089
  return;
3052
3090
  }
3053
- fact(db, "endsHolding", declarationId, emitValue(db, table, last));
3091
+ reassignedUnstated.add(declarationId);
3054
3092
  }
3055
3093
  var reassignedUnstated = /* @__PURE__ */ new Set();
3056
3094
  function reassignedNamesUnstated() {
@@ -3062,23 +3100,31 @@ function forgetReassignedNamesUnstated() {
3062
3100
  function emitFieldValues(db, table, declaration) {
3063
3101
  const { values, inOrder } = writesToField(declaration);
3064
3102
  const last = values[values.length - 1];
3065
- if (!inOrder || last === void 0 || !Node14.isExpression(last)) {
3103
+ if (inOrder && last !== void 0 && Node14.isExpression(last)) {
3104
+ fact(db, "binds", nodeId(declaration), emitValue(db, table, last));
3066
3105
  return;
3067
3106
  }
3068
- fact(db, "binds", nodeId(declaration), emitValue(db, table, last));
3107
+ const construction = sameConstructionAcrossWrites(values);
3108
+ if (construction !== null) {
3109
+ fact(db, "binds", nodeId(declaration), emitValue(db, table, construction));
3110
+ }
3069
3111
  }
3070
3112
  function emitParameterPropertyRead(db, table, referenceId, declaration) {
3071
3113
  const { values, inOrder } = writesToField(declaration);
3072
3114
  const last = values[values.length - 1];
3073
- if (!inOrder || last === void 0) {
3115
+ if (inOrder && last !== void 0 && Node14.isExpression(last)) {
3116
+ fact(db, "binds", referenceId, emitValue(db, table, last));
3074
3117
  return;
3075
3118
  }
3076
- if (!Node14.isExpression(last)) {
3119
+ if (inOrder && last !== void 0) {
3077
3120
  table.byId.set(nodeId(last), last);
3078
3121
  fact(db, "binds", referenceId, nodeId(last));
3079
3122
  return;
3080
3123
  }
3081
- fact(db, "binds", referenceId, emitValue(db, table, last));
3124
+ const construction = sameConstructionAcrossWrites(values);
3125
+ if (construction !== null) {
3126
+ fact(db, "binds", referenceId, emitValue(db, table, construction));
3127
+ }
3082
3128
  }
3083
3129
  function emitBindingElementFacts(db, table, element) {
3084
3130
  const id = nodeId(element);
@@ -3542,7 +3588,8 @@ var JS_RULES = [
3542
3588
  ];
3543
3589
  var RESOLUTION_PROGRAM = process.env.SUSS_RESOLUTION_ON_DEMAND === "0" ? {
3544
3590
  rules: [...SHARED_RULES, ...JS_RULES, ...RESOLUTION_QUESTIONS],
3545
- demandDriven: []
3591
+ demandDriven: [],
3592
+ demands: []
3546
3593
  } : deriveOnDemand(
3547
3594
  [...SHARED_RULES, ...JS_RULES, ...RESOLUTION_QUESTIONS],
3548
3595
  ANSWER_RELATIONS
@@ -3569,7 +3616,9 @@ var QUERY_FACTS = RESOLUTION_PROGRAM.demandDriven.length === 0 ? [] : [
3569
3616
  "wantedAnchor",
3570
3617
  "wantedSubject"
3571
3618
  ];
3572
- var MAX_MODULE_HOPS = 6;
3619
+ var MODULE_DEMANDS = RESOLUTION_PROGRAM.demands.filter(
3620
+ (one) => (one.relation === "moduleExport" || one.relation === "moduleForwards") && one.bound[0] === true
3621
+ ).map((one) => one.demand);
3573
3622
  var ResolutionStore = class {
3574
3623
  db = new Database2();
3575
3624
  table = createNodeTable();
@@ -3585,7 +3634,7 @@ var ResolutionStore = class {
3585
3634
  * extraction elsewhere cannot change an entry.
3586
3635
  */
3587
3636
  exportTables = /* @__PURE__ */ new Map();
3588
- /** Files the most recent wave walk entered, for the memo to keep. */
3637
+ /** Files the most recent query read, for the memo to keep. */
3589
3638
  lastQueryWalked = [];
3590
3639
  declarations = /* @__PURE__ */ new Map();
3591
3640
  graph = new ModuleGraph();
@@ -3616,19 +3665,19 @@ var ResolutionStore = class {
3616
3665
  */
3617
3666
  resolveCallableSources(value, alsoFrom) {
3618
3667
  const target = factKeyOf(value);
3619
- return this.resolveByWaves(
3668
+ return this.askAbout(
3620
3669
  target,
3621
3670
  "wanted",
3622
3671
  () => this.lookupSources(target),
3623
3672
  alsoFrom
3624
- ) ?? [];
3673
+ );
3625
3674
  }
3626
3675
  /**
3627
3676
  * The witness proof behind `resolveCallable`'s answer. Resolves the
3628
- * value first, which walks files and extracts facts in the usual
3629
- * waves, then re-evaluates the rules over those base facts under the
3630
- * witness algebra and rebuilds the proof of the answer. Null when
3631
- * the value does not resolve at all.
3677
+ * value first, which extracts the files the question demands, then
3678
+ * re-evaluates the rules over those base facts under the witness
3679
+ * algebra and rebuilds the proof of the answer. Null when the value
3680
+ * does not resolve at all.
3632
3681
  */
3633
3682
  explainCallable(value, options = {}) {
3634
3683
  const resolved = this.resolveCallable(value, options.alsoFrom);
@@ -3665,11 +3714,7 @@ var ResolutionStore = class {
3665
3714
  }
3666
3715
  resolveObject(value) {
3667
3716
  const target = factKeyOf(value);
3668
- return this.resolveByWaves(
3669
- target,
3670
- "wanted",
3671
- () => this.lookupObject(target)
3672
- );
3717
+ return this.askAbout(target, "wanted", () => this.lookupObject(target));
3673
3718
  }
3674
3719
  /**
3675
3720
  * For a value that is neither a function nor an object, such as a
@@ -3685,17 +3730,17 @@ var ResolutionStore = class {
3685
3730
  }
3686
3731
  return cached.written;
3687
3732
  }
3688
- const settled2 = this.resolveByWaves(
3733
+ const written = this.askAbout(
3689
3734
  target,
3690
3735
  "wanted",
3691
3736
  () => this.lookupWritten(target)
3692
3737
  );
3693
3738
  this.writtenValues.set(key, {
3694
- written: settled2?.written ?? null,
3739
+ written,
3695
3740
  walked: [...this.lastQueryWalked],
3696
3741
  extractedAt: this.fullyExtracted.size
3697
3742
  });
3698
- return settled2?.written ?? null;
3743
+ return written;
3699
3744
  }
3700
3745
  /**
3701
3746
  * The construction behind a registration subject: the call or `new`
@@ -3715,18 +3760,29 @@ var ResolutionStore = class {
3715
3760
  }
3716
3761
  return cached.construction;
3717
3762
  }
3718
- const settled2 = this.resolveByWaves(
3763
+ const settled2 = this.askAbout(
3719
3764
  target,
3720
3765
  "wantedSubject",
3721
3766
  () => this.lookupSubjectConstruction(target, importModule, importName)
3722
3767
  );
3768
+ const walked = [...this.lastQueryWalked];
3769
+ let resolved = settled2;
3770
+ if (settled2.construction === null && settled2.candidate !== null && Node15.isCallExpression(settled2.candidate) && this.resolveCallable(settled2.candidate.getExpression()) !== null) {
3771
+ const call = settled2.candidate;
3772
+ resolved = this.askAbout(
3773
+ call,
3774
+ "wantedSubject",
3775
+ () => this.lookupSubjectConstruction(call, importModule, importName)
3776
+ );
3777
+ walked.push(...this.lastQueryWalked);
3778
+ }
3723
3779
  this.subjectConstructions.set(key, {
3724
- construction: settled2?.construction ?? null,
3725
- candidates: settled2?.candidates ?? 0,
3726
- walked: [...this.lastQueryWalked],
3780
+ construction: resolved.construction,
3781
+ candidates: resolved.candidates,
3782
+ walked,
3727
3783
  extractedAt: this.fullyExtracted.size
3728
3784
  });
3729
- return settled2?.construction ?? null;
3785
+ return resolved.construction;
3730
3786
  }
3731
3787
  /**
3732
3788
  * How many constructions this value was written as, which is what
@@ -3740,10 +3796,8 @@ var ResolutionStore = class {
3740
3796
  )?.candidates ?? 0;
3741
3797
  }
3742
3798
  /**
3743
- * The single-answer policy over the written-value walk, then the
3744
- * origin check on the one candidate. A candidate failing the check
3745
- * is settled refusal rather than a reason to widen: its own file's
3746
- * import facts are already in, and more files only add candidates.
3799
+ * The single-answer policy over the written-value chain, then the
3800
+ * origin check on the one candidate.
3747
3801
  */
3748
3802
  lookupSubjectConstruction(value, importModule, importName) {
3749
3803
  this.derive();
@@ -3759,6 +3813,7 @@ var ResolutionStore = class {
3759
3813
  if (candidates.size > 1) {
3760
3814
  return {
3761
3815
  construction: null,
3816
+ candidate: null,
3762
3817
  candidates: [...candidates].filter(
3763
3818
  (one) => this.constructionComesFrom(valueId, one, importModule, importName)
3764
3819
  ).length
@@ -3766,7 +3821,7 @@ var ResolutionStore = class {
3766
3821
  }
3767
3822
  const single = [...candidates][0];
3768
3823
  if (single === void 0) {
3769
- return neverWritable(value) ? { construction: null, candidates: 0 } : null;
3824
+ return { construction: null, candidate: null, candidates: 0 };
3770
3825
  }
3771
3826
  return {
3772
3827
  construction: this.constructionComesFrom(
@@ -3775,6 +3830,7 @@ var ResolutionStore = class {
3775
3830
  importModule,
3776
3831
  importName
3777
3832
  ) ? single : null,
3833
+ candidate: single,
3778
3834
  candidates: 1
3779
3835
  };
3780
3836
  }
@@ -3834,11 +3890,11 @@ var ResolutionStore = class {
3834
3890
  return cached.names;
3835
3891
  }
3836
3892
  const target = factKeyOf(value);
3837
- const found = this.resolveByWaves(
3893
+ const found = this.askAbout(
3838
3894
  target,
3839
3895
  "wantedOrigin",
3840
3896
  () => this.lookupImportedNames(target, modules)
3841
- ) ?? [];
3897
+ );
3842
3898
  this.importedNames.set(declaration, {
3843
3899
  names: found,
3844
3900
  walked: [...this.lastQueryWalked]
@@ -3857,8 +3913,8 @@ var ResolutionStore = class {
3857
3913
  return this.importOriginsOfMany([value], modules).get(value) ?? [];
3858
3914
  }
3859
3915
  /**
3860
- * The batched form: one demand set, one derivation, one widening
3861
- * walk shared by every value. A discovery pass asks about every
3916
+ * The batched form: one demand set, one derivation, one round of
3917
+ * extraction shared by every value. A discovery pass asks about every
3862
3918
  * callee in a file, and per-value queries would re-pay demand
3863
3919
  * clearing and re-derivation once per call site.
3864
3920
  */
@@ -3886,49 +3942,12 @@ var ResolutionStore = class {
3886
3942
  this.wantValue("wantedCallOrigin", one.target);
3887
3943
  this.seedValue(one.target);
3888
3944
  }
3889
- const walked = /* @__PURE__ */ new Set();
3890
- this.lastQueryWalked = [];
3891
- let frontier = [];
3945
+ this.extractDemanded(pending.map((one) => one.target.getSourceFile()));
3892
3946
  for (const one of pending) {
3893
- const file = one.target.getSourceFile();
3894
- if (!walked.has(file.getFilePath()) && !frontier.includes(file)) {
3895
- frontier.push(file);
3896
- }
3897
- }
3898
- const open = new Set(pending);
3899
- for (let hop = 0; hop <= MAX_MODULE_HOPS && open.size > 0; hop++) {
3900
- const next = [];
3901
- for (const sourceFile of frontier) {
3902
- if (walked.has(sourceFile.getFilePath())) {
3903
- continue;
3904
- }
3905
- walked.add(sourceFile.getFilePath());
3906
- this.lastQueryWalked.push(sourceFile.getFilePath());
3907
- recordFileDependency(sourceFile.getFilePath());
3908
- this.extractFile(sourceFile);
3909
- next.push(...this.graph.importedFilesOf(sourceFile));
3910
- }
3911
- for (const one of [...open]) {
3912
- const found = this.lookupImportOrigins(one.target, modules);
3913
- if (found !== null) {
3914
- results.set(one.value, found);
3915
- this.importOrigins.set(one.key, {
3916
- origins: found,
3917
- walked: [...this.lastQueryWalked],
3918
- extractedAt: this.fullyExtracted.size
3919
- });
3920
- open.delete(one);
3921
- }
3922
- }
3923
- if (next.length === 0) {
3924
- break;
3925
- }
3926
- frontier = next;
3927
- }
3928
- for (const one of open) {
3929
- results.set(one.value, []);
3947
+ const origins = this.lookupImportOrigins(one.target, modules);
3948
+ results.set(one.value, origins);
3930
3949
  this.importOrigins.set(one.key, {
3931
- origins: [],
3950
+ origins,
3932
3951
  walked: [...this.lastQueryWalked],
3933
3952
  extractedAt: this.fullyExtracted.size
3934
3953
  });
@@ -3987,67 +4006,89 @@ var ResolutionStore = class {
3987
4006
  const refined = new Set(
3988
4007
  [...byPath.values()].filter((one) => one.path.length > 1).map((one) => tupleKey(one.path.slice(0, -1)))
3989
4008
  );
3990
- const origins = [...byPath.values()].filter((one) => !refined.has(tupleKey(one.path))).sort((a, b) => a.path.join(".").localeCompare(b.path.join(".")));
3991
- if (origins.length > 0) {
3992
- return origins;
3993
- }
3994
- return all.some((one) => namesAPackage(one.module)) ? [] : null;
4009
+ return [...byPath.values()].filter((one) => !refined.has(tupleKey(one.path))).sort((a, b) => a.path.join(".").localeCompare(b.path.join(".")));
3995
4010
  }
3996
- /**
3997
- * Null tells the walk to keep widening. A value that landed in a
3998
- * package returns empty instead, because a library's own body is not
3999
- * here to read and widening would never find anything.
4000
- */
4001
4011
  lookupImportedNames(value, modules) {
4002
4012
  this.derive();
4003
- const origins = this.answerPairsFor("wantedComesFrom", nodeId(value));
4004
- const reached = new Set(origins);
4013
+ const reached = new Set(
4014
+ this.answerPairsFor("wantedComesFrom", nodeId(value))
4015
+ );
4005
4016
  for (const target of this.answersFor("wantedComesTo", nodeId(value))) {
4006
4017
  for (const entry of this.answerPairsFor("wantedCallsInto", target)) {
4007
4018
  reached.add(entry);
4008
4019
  }
4009
4020
  }
4010
- const names = namesFrom([...reached], modules);
4011
- if (names.length > 0) {
4012
- return names;
4013
- }
4014
- return origins.some((pair) => namesAPackage(pairHalves(pair).module)) ? [] : null;
4021
+ return namesFrom([...reached], modules);
4015
4022
  }
4016
- resolveByWaves(value, question, ask, alsoFrom) {
4023
+ askAbout(value, question, read2, alsoFrom) {
4017
4024
  try {
4018
- return this.walkForAnswer(value, question, ask, alsoFrom);
4025
+ this.wantValue(question, value);
4026
+ this.seedValue(value);
4027
+ this.extractDemanded(
4028
+ alsoFrom === void 0 ? [value.getSourceFile()] : [value.getSourceFile(), alsoFrom]
4029
+ );
4030
+ return read2();
4019
4031
  } finally {
4020
4032
  this.forgetQuery();
4021
4033
  }
4022
4034
  }
4023
- walkForAnswer(value, question, ask, alsoFrom) {
4024
- this.wantValue(question, value);
4025
- this.seedValue(value);
4026
- const walked = /* @__PURE__ */ new Set();
4035
+ /**
4036
+ * Extract the seed files, then every module the rules ask for, until
4037
+ * they ask for nothing new. The files a question reads are then fixed
4038
+ * by the question alone, so its answer does not depend on what an
4039
+ * earlier question left in the store.
4040
+ */
4041
+ extractDemanded(seeds) {
4042
+ const project = seeds[0]?.getProject();
4043
+ if (project === void 0) {
4044
+ return;
4045
+ }
4046
+ const read2 = /* @__PURE__ */ new Set();
4047
+ const considered = /* @__PURE__ */ new Set();
4027
4048
  this.lastQueryWalked = [];
4028
- let frontier = alsoFrom === void 0 ? [value.getSourceFile()] : [value.getSourceFile(), alsoFrom];
4029
- for (let hop = 0; hop <= MAX_MODULE_HOPS; hop++) {
4030
- const next = [];
4031
- for (const sourceFile of frontier) {
4032
- if (walked.has(sourceFile.getFilePath())) {
4049
+ let pending = [...seeds];
4050
+ while (pending.length > 0) {
4051
+ const readThisRound = [];
4052
+ for (const sourceFile of pending) {
4053
+ const filePath = sourceFile.getFilePath();
4054
+ if (read2.has(filePath)) {
4033
4055
  continue;
4034
4056
  }
4035
- walked.add(sourceFile.getFilePath());
4036
- this.lastQueryWalked.push(sourceFile.getFilePath());
4037
- recordFileDependency(sourceFile.getFilePath());
4057
+ read2.add(filePath);
4058
+ readThisRound.push(filePath);
4059
+ this.lastQueryWalked.push(filePath);
4060
+ recordFileDependency(filePath);
4038
4061
  this.extractFile(sourceFile);
4039
- next.push(...this.graph.importedFilesOf(sourceFile));
4040
- }
4041
- const found = ask();
4042
- if (found !== null) {
4043
- return found;
4044
4062
  }
4045
- if (next.length === 0) {
4046
- return null;
4063
+ this.derive();
4064
+ pending = [];
4065
+ for (const moduleKey of this.demandedModules(readThisRound)) {
4066
+ if (considered.has(moduleKey)) {
4067
+ continue;
4068
+ }
4069
+ considered.add(moduleKey);
4070
+ const sourceFile = sourceFileFor(project, moduleKey);
4071
+ if (sourceFile !== void 0 && !read2.has(sourceFile.getFilePath())) {
4072
+ pending.push(sourceFile);
4073
+ }
4047
4074
  }
4048
- frontier = next;
4049
4075
  }
4050
- return null;
4076
+ }
4077
+ /**
4078
+ * Module keys the rules are waiting on. Demand facts are cleared
4079
+ * between queries, so the demand relations contain this query's
4080
+ * alone; the unrestricted program has none, and follows the imports
4081
+ * of the files read this round instead.
4082
+ */
4083
+ demandedModules(readThisRound) {
4084
+ if (RESOLUTION_PROGRAM.demands.length === 0) {
4085
+ return readThisRound.flatMap(
4086
+ (filePath) => this.db.lookup("importsModule", 0, filePath).map((tuple) => String(tuple[1]))
4087
+ );
4088
+ }
4089
+ return MODULE_DEMANDS.flatMap(
4090
+ (relation) => this.db.facts(relation).map((tuple) => String(tuple[0]))
4091
+ );
4051
4092
  }
4052
4093
  /**
4053
4094
  * The question and the chain that settled it are dropped. The files
@@ -4066,6 +4107,27 @@ var ResolutionStore = class {
4066
4107
  this.stale = true;
4067
4108
  }
4068
4109
  }
4110
+ wantExportsOf(filePath) {
4111
+ if (this.db.add("wantedExportsOf", [filePath]) === "added") {
4112
+ this.stale = true;
4113
+ }
4114
+ }
4115
+ /**
4116
+ * Discovery walks the table in order, and the rounds derive it in
4117
+ * the order files arrived. Deriving it again from the demand alone,
4118
+ * over the loaded closure, orders it by the file's own statements.
4119
+ */
4120
+ rederiveExportsOf(filePath) {
4121
+ if (QUERY_FACTS.length === 0) {
4122
+ return;
4123
+ }
4124
+ this.db.retract("wantedModuleExport", [
4125
+ ...this.db.lookup("wantedModuleExport", 0, filePath)
4126
+ ]);
4127
+ this.forgetQuery();
4128
+ this.wantExportsOf(filePath);
4129
+ this.derive();
4130
+ }
4069
4131
  /**
4070
4132
  * File extraction only reaches values hanging off exports, and a query
4071
4133
  * can be rooted anywhere, so the queried value gets its own facts.
@@ -4115,24 +4177,18 @@ var ResolutionStore = class {
4115
4177
  }
4116
4178
  candidates.add(node);
4117
4179
  }
4118
- if (candidates.size === 1) {
4119
- return { written: [...candidates][0] };
4120
- }
4121
- if (candidates.size > 1) {
4122
- return { written: null };
4123
- }
4124
- if (neverWritable(value)) {
4125
- return { written: null };
4180
+ if (candidates.size !== 1) {
4181
+ return null;
4126
4182
  }
4127
- return null;
4183
+ return [...candidates][0];
4128
4184
  }
4129
4185
  /**
4130
4186
  * Read these files' facts now, before anything asks a question.
4131
4187
  *
4132
- * A query widens along the imports of the file it starts in, which
4133
- * is where a value's definition is. What a caller passed is the other
4188
+ * A query follows the imports of the file it starts in, which is
4189
+ * where a value's definition is. What a caller passed is the other
4134
4190
  * direction, so a question about a parameter is answered only by
4135
- * files the walk never reaches on its own.
4191
+ * files the query never reaches on its own.
4136
4192
  */
4137
4193
  extractFiles(sourceFiles) {
4138
4194
  for (const sourceFile of sourceFiles) {
@@ -4148,15 +4204,14 @@ var ResolutionStore = class {
4148
4204
  return this.graph.filesReachingAnyPackage(fileSets);
4149
4205
  }
4150
4206
  /**
4151
- * The calls behind a receiver that `matches` accepts, walked out in
4152
- * waves the way every question is. The wave ends as soon as one file
4153
- * yields a match, and the caller applies the single-answer policy to
4154
- * the set; the resolution README's anchor section says why the rules
4155
- * cannot rank a nearer call above a farther one.
4207
+ * The calls behind a receiver that `matches` accepts. The caller
4208
+ * applies the single-answer policy to the set; the resolution
4209
+ * README's anchor section says why the rules cannot rank a nearer
4210
+ * call above a farther one.
4156
4211
  */
4157
4212
  anchorCallsOf(value, matches) {
4158
4213
  const target = factKeyOf(value);
4159
- return this.resolveByWaves(target, "wantedAnchor", () => {
4214
+ return this.askAbout(target, "wantedAnchor", () => {
4160
4215
  this.derive();
4161
4216
  const found = [];
4162
4217
  for (const id of this.answersFor("wantedAnchorCall", nodeId(target))) {
@@ -4165,12 +4220,12 @@ var ResolutionStore = class {
4165
4220
  found.push(node);
4166
4221
  }
4167
4222
  }
4168
- return found.length === 0 ? null : found;
4169
- }) ?? [];
4223
+ return found;
4224
+ });
4170
4225
  }
4171
4226
  /**
4172
4227
  * Every name a module exports and the values behind each, with
4173
- * re-export chains flattened by the rules. The frontier follows
4228
+ * re-export chains flattened by the rules. The demand follows
4174
4229
  * re-export targets only, so a barrel of barrels extracts its own
4175
4230
  * chain and nothing beside it, to any depth.
4176
4231
  */
@@ -4190,33 +4245,10 @@ var ResolutionStore = class {
4190
4245
  }
4191
4246
  }
4192
4247
  collectExports(sourceFile) {
4193
- const project = sourceFile.getProject();
4194
- const walked = /* @__PURE__ */ new Set();
4195
- let frontier = [sourceFile];
4196
- while (frontier.length > 0) {
4197
- const next = [];
4198
- for (const file of frontier) {
4199
- const filePath2 = file.getFilePath();
4200
- if (walked.has(filePath2)) {
4201
- continue;
4202
- }
4203
- walked.add(filePath2);
4204
- recordFileDependency(filePath2);
4205
- this.extractFile(file);
4206
- for (const target of this.reExportTargetsOf(filePath2)) {
4207
- const targetFile = sourceFileFor(project, target);
4208
- if (targetFile !== void 0) {
4209
- next.push(targetFile);
4210
- }
4211
- }
4212
- }
4213
- frontier = next;
4214
- }
4215
4248
  const filePath = sourceFile.getFilePath();
4216
- if (this.db.add("wantedExportsOf", [filePath]) === "added") {
4217
- this.stale = true;
4218
- }
4219
- this.derive();
4249
+ this.wantExportsOf(filePath);
4250
+ this.extractDemanded([sourceFile]);
4251
+ this.rederiveExportsOf(filePath);
4220
4252
  const exports = /* @__PURE__ */ new Map();
4221
4253
  for (const tuple of this.db.lookup("wantedModuleExport", 0, filePath)) {
4222
4254
  const node = this.table.byId.get(String(tuple[2]));
@@ -4233,22 +4265,9 @@ var ResolutionStore = class {
4233
4265
  }
4234
4266
  return exports;
4235
4267
  }
4236
- /** Module keys this file re-exports from, read off its own facts. */
4237
- reExportTargetsOf(filePath) {
4238
- const targets = /* @__PURE__ */ new Set();
4239
- for (const tuple of this.db.lookup("reExports", 0, filePath)) {
4240
- targets.add(String(tuple[2]));
4241
- }
4242
- for (const tuple of this.db.lookup("reExportsAll", 0, filePath)) {
4243
- targets.add(String(tuple[1]));
4244
- }
4245
- return [...targets];
4246
- }
4247
4268
  /**
4248
- * Null keeps the wave walk widening; any candidate at all ends it,
4249
- * since more files can only add candidates, never take one away. The
4250
- * single-answer policy stays with the callers: picking one of two
4251
- * would make the result depend on the order the facts came in.
4269
+ * The single-answer policy stays with the callers: a value with two
4270
+ * sources is something to say at the site, not something to pick from.
4252
4271
  */
4253
4272
  lookupSources(value) {
4254
4273
  this.derive();
@@ -4262,7 +4281,7 @@ var ResolutionStore = class {
4262
4281
  candidates.add(resolved);
4263
4282
  }
4264
4283
  }
4265
- return candidates.size === 0 ? null : [...candidates];
4284
+ return [...candidates];
4266
4285
  }
4267
4286
  derive() {
4268
4287
  if (!this.stale) {
@@ -4321,18 +4340,6 @@ function pairHalves(pair) {
4321
4340
  const [module = "", name = ""] = tupleKeyParts(pair);
4322
4341
  return { module, name };
4323
4342
  }
4324
- function namesAPackage(moduleKey) {
4325
- return !moduleKey.startsWith("/") || moduleKey.includes("/node_modules/");
4326
- }
4327
- function neverWritable(value) {
4328
- if (!Node15.isIdentifier(value)) {
4329
- return false;
4330
- }
4331
- const declarations = value.getSymbol()?.getDeclarations() ?? [];
4332
- return declarations.some(
4333
- (declaration) => declaration.getSourceFile().getFilePath().includes("/node_modules/")
4334
- );
4335
- }
4336
4343
  function namesPackage2(moduleKey, packages) {
4337
4344
  return namesAnyPackage(
4338
4345
  [moduleKey, ...packagesDeclaring(moduleKey)],
@@ -4353,35 +4360,12 @@ import path3 from "path";
4353
4360
  // src/diagnostics.ts
4354
4361
  import path2 from "path";
4355
4362
  import { ts as ts3 } from "ts-morph";
4356
- import { BOUNDARY_ROLE } from "@suss/behavioral-ir";
4357
- var emptyTally = () => ({
4358
- candidateFiles: 0,
4359
- unitsDiscovered: 0,
4360
- unitsInGatedFiles: 0,
4361
- effectsRecognized: 0,
4362
- unitsClaimed: 0,
4363
- selfCollisions: 0,
4364
- summariesProduced: 0,
4365
- failures: [],
4366
- helpersMatched: /* @__PURE__ */ new Set()
4367
- });
4368
- function recordPackFailure(tally, failure) {
4369
- const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
4370
- tally?.failures.push({
4371
- hook: failure.hook,
4372
- file: failure.file,
4373
- message
4374
- });
4375
- return `[suss] pack "${failure.pack}" threw from ${failure.hook} while reading ${failure.file}: ${message}
4376
- `;
4377
- }
4378
- function createPackTallies(packs) {
4379
- const tallies = /* @__PURE__ */ new Map();
4380
- for (const pack of packs) {
4381
- tallies.set(pack.name, emptyTally());
4382
- }
4383
- return tallies;
4384
- }
4363
+ import {
4364
+ createPackTallies,
4365
+ emptyTally,
4366
+ recordPackFailure,
4367
+ summaryCountsByPack
4368
+ } from "@suss/extractor";
4385
4369
  function commonDirectoryOf(files) {
4386
4370
  const absolute = files.filter((f) => path2.isAbsolute(f));
4387
4371
  const first = absolute[0];
@@ -4458,30 +4442,6 @@ function unresolvedGatesFor(gates, args) {
4458
4442
  }
4459
4443
  return unresolved;
4460
4444
  }
4461
- var emptyCounts = () => ({
4462
- bound: 0,
4463
- providers: 0,
4464
- withBehavior: 0
4465
- });
4466
- function summaryCountsByPack(summaries) {
4467
- const counts = /* @__PURE__ */ new Map();
4468
- for (const summary of summaries) {
4469
- const recognition = summary.identity.boundaryBinding?.recognition;
4470
- if (recognition === void 0 || recognition === null) {
4471
- continue;
4472
- }
4473
- const entry = counts.get(recognition) ?? emptyCounts();
4474
- entry.bound += 1;
4475
- if (BOUNDARY_ROLE[summary.kind] === "provider") {
4476
- entry.providers += 1;
4477
- }
4478
- if (summary.transitions.length > 0) {
4479
- entry.withBehavior += 1;
4480
- }
4481
- counts.set(recognition, entry);
4482
- }
4483
- return counts;
4484
- }
4485
4445
  function helpersWithNoCall(patterns, matched, projectRoot) {
4486
4446
  const missing = [];
4487
4447
  for (const pattern of patterns) {
@@ -4506,7 +4466,11 @@ function buildExtractionReport(args) {
4506
4466
  const packFunnels = args.packs.map((pack) => {
4507
4467
  const gates = packIsUngated(pack) ? [] : collectPackGates(pack);
4508
4468
  const tally = args.tallies.get(pack.name) ?? emptyTally();
4509
- const counted = bySummary.get(pack.name) ?? emptyCounts();
4469
+ const counted = bySummary.get(pack.name) ?? {
4470
+ bound: 0,
4471
+ providers: 0,
4472
+ withBehavior: 0
4473
+ };
4510
4474
  return {
4511
4475
  pack: pack.name,
4512
4476
  version: pack.version ?? null,
@@ -4735,13 +4699,26 @@ function propertiesReached(object, resolution, seen) {
4735
4699
  }
4736
4700
  function writtenNodeOf(value, resolution) {
4737
4701
  const written = factKeyOf(value);
4738
- if (Node16.isCallExpression(written) || Node16.isNewExpression(written)) {
4702
+ if (Node16.isNewExpression(written)) {
4739
4703
  return written;
4740
4704
  }
4705
+ if (Node16.isCallExpression(written)) {
4706
+ return writtenThroughCall(written, resolution);
4707
+ }
4741
4708
  if (resolution === void 0 || !couldNameAValue(written)) {
4742
4709
  return null;
4743
4710
  }
4744
- return resolution.resolveWrittenValue(written);
4711
+ const resolved = resolution.resolveWrittenValue(written);
4712
+ return resolved !== null && Node16.isCallExpression(resolved) ? writtenThroughCall(resolved, resolution) : resolved;
4713
+ }
4714
+ function writtenThroughCall(call, resolution) {
4715
+ if (resolution !== void 0 && resolution.resolveCallable(call.getExpression()) !== null) {
4716
+ const resolved = resolution.resolveWrittenValue(call);
4717
+ if (resolved !== null) {
4718
+ return resolved;
4719
+ }
4720
+ }
4721
+ return call;
4745
4722
  }
4746
4723
  function stringValueOf(value, resolution) {
4747
4724
  const written = factKeyOf(value);
@@ -4803,7 +4780,7 @@ function discoverClientCalls(sourceFile, match, kind, resolution) {
4803
4780
  }
4804
4781
  } else if (Node17.isPropertyAccessExpression(callee)) {
4805
4782
  const subject = callee.getExpression();
4806
- if (Node17.isIdentifier(subject) && (isClientImport(subject, match, resolution, true) || clientVarNames.has(subject.getText()) || resolvesToKnownInstance(subject, match, resolution))) {
4783
+ if (Node17.isIdentifier(subject) && (isClientImport(subject, match, resolution, true) || clientVarNames.has(subject.getText())) || resolvesToKnownInstance(subject, match, resolution)) {
4807
4784
  methodName = callee.getName();
4808
4785
  if (methodFilter === null || methodFilter.has(methodName)) {
4809
4786
  matched = true;
@@ -6413,14 +6390,14 @@ function unitForJsxRoute(node, match, routeNames, kind, resolution) {
6413
6390
  }
6414
6391
  return routeUnit(node, kind, match.method, resolved.path, target, resolution);
6415
6392
  }
6416
- function routeUnit(at, kind, method, path15, target, resolution) {
6417
- const routeInfo = { method, path: path15 };
6393
+ function routeUnit(at, kind, method, path14, target, resolution) {
6394
+ const routeInfo = { method, path: path14 };
6418
6395
  if (target.kind === "unreadable") {
6419
6396
  return {
6420
6397
  func: null,
6421
6398
  announcedAt: at,
6422
6399
  kind,
6423
- name: path15,
6400
+ name: path14,
6424
6401
  nameKind: "label",
6425
6402
  routeInfo
6426
6403
  };
@@ -6619,7 +6596,6 @@ function labelOfExpression(node) {
6619
6596
  }
6620
6597
 
6621
6598
  // src/discovery/packageExports.ts
6622
- import fs4 from "fs";
6623
6599
  import { Node as Node29 } from "ts-morph";
6624
6600
 
6625
6601
  // src/packageExports.ts
@@ -6648,7 +6624,8 @@ function resolvePackageExports(packageJsonPath, opts = {}) {
6648
6624
  packageName,
6649
6625
  subPath,
6650
6626
  exportPathPrefix: subPathToPrefix(subPath),
6651
- sourceFile: src
6627
+ sourceFile: src,
6628
+ publishedFile: path5.resolve(pkgDir, distPath)
6652
6629
  });
6653
6630
  };
6654
6631
  const exportsField = pkg.exports;
@@ -6668,6 +6645,53 @@ function resolvePackageExports(packageJsonPath, opts = {}) {
6668
6645
  }
6669
6646
  return { packageName, entries, warnings };
6670
6647
  }
6648
+ var packageExportsCache = /* @__PURE__ */ new Map();
6649
+ var PATH_STAMP_SEPARATOR = "";
6650
+ function packageJsonStamp(packageJsonPath) {
6651
+ try {
6652
+ const stat = fs3.statSync(packageJsonPath);
6653
+ return `${stat.mtimeMs}:${stat.size}`;
6654
+ } catch {
6655
+ return "unreadable";
6656
+ }
6657
+ }
6658
+ function resolvePackageExportsCached(packageJsonPath) {
6659
+ const stamp2 = packageJsonStamp(packageJsonPath);
6660
+ const key = `${packageJsonPath}${PATH_STAMP_SEPARATOR}${stamp2}`;
6661
+ const cached = packageExportsCache.get(key);
6662
+ if (cached !== void 0) {
6663
+ return cached;
6664
+ }
6665
+ const fresh = resolvePackageExports(packageJsonPath);
6666
+ packageExportsCache.set(key, fresh);
6667
+ return fresh;
6668
+ }
6669
+ function namedPackageDirAbove(start) {
6670
+ let at = path5.resolve(start);
6671
+ for (let up = 0; up < 12; up += 1) {
6672
+ if (packageNameAt(at) !== null) {
6673
+ return at;
6674
+ }
6675
+ const parent = path5.dirname(at);
6676
+ if (parent === at) {
6677
+ break;
6678
+ }
6679
+ at = parent;
6680
+ }
6681
+ return null;
6682
+ }
6683
+ function packageNameAt(dir) {
6684
+ try {
6685
+ const manifest = JSON.parse(
6686
+ fs3.readFileSync(path5.join(dir, "package.json"), "utf8")
6687
+ );
6688
+ if (typeof manifest.name === "string" && manifest.name.length > 0) {
6689
+ return manifest.name;
6690
+ }
6691
+ } catch {
6692
+ }
6693
+ return null;
6694
+ }
6671
6695
  function pickConditional(value, warnings, key) {
6672
6696
  if (value === void 0 || value === null) {
6673
6697
  if (value === null) {
@@ -6717,8 +6741,55 @@ function resolveSourceFor(pkgDir, distPath, srcRoot) {
6717
6741
  return null;
6718
6742
  }
6719
6743
 
6744
+ // src/resolve/sourceDeclaration.ts
6745
+ import path6 from "path";
6746
+ function sourceDeclarationsBehind(decl, resolution, visited = /* @__PURE__ */ new Set()) {
6747
+ const file = decl.getSourceFile();
6748
+ if (!file.isDeclarationFile() || file.isInNodeModules()) {
6749
+ return [decl];
6750
+ }
6751
+ const packageDir = namedPackageDirAbove(path6.dirname(file.getFilePath()));
6752
+ if (packageDir === null || visited.has(packageDir)) {
6753
+ return [decl];
6754
+ }
6755
+ visited.add(packageDir);
6756
+ const project = file.getProject();
6757
+ const { entries } = resolvePackageExportsCached(
6758
+ path6.join(packageDir, "package.json")
6759
+ );
6760
+ const found = [];
6761
+ for (const entry of entries) {
6762
+ const published = sourceFileFor(project, entry.publishedFile);
6763
+ const source = sourceFileFor(project, entry.sourceFile);
6764
+ if (published === void 0 || source === void 0) {
6765
+ continue;
6766
+ }
6767
+ for (const [name, nodes] of resolution.exportsOf(published)) {
6768
+ if (!nodes.some((node) => sameDeclaration(node, decl))) {
6769
+ continue;
6770
+ }
6771
+ for (const behind of resolution.exportsOf(source).get(name) ?? []) {
6772
+ for (const candidate of sourceDeclarationsBehind(
6773
+ behind,
6774
+ resolution,
6775
+ visited
6776
+ )) {
6777
+ if (!found.includes(candidate)) {
6778
+ found.push(candidate);
6779
+ }
6780
+ }
6781
+ }
6782
+ }
6783
+ }
6784
+ return found.length > 0 ? found : [decl];
6785
+ }
6786
+ function sameDeclaration(a, b) {
6787
+ return a === b || a.getSourceFile().getFilePath() === b.getSourceFile().getFilePath() && a.getStart() === b.getStart() && a.getEnd() === b.getEnd();
6788
+ }
6789
+
6720
6790
  // src/discovery/factorySurface.ts
6721
6791
  import { Node as Node28, SyntaxKind as SyntaxKind6 } from "ts-morph";
6792
+ var MAX_BUILDER_HOPS = 2;
6722
6793
  function surfaceMethods(decl) {
6723
6794
  if (Node28.isClassDeclaration(decl)) {
6724
6795
  return surfaceClassMethods(decl);
@@ -6758,16 +6829,20 @@ function surfaceClassMethods(cls) {
6758
6829
  function surfaceFactoryReturnMethods(fn) {
6759
6830
  const out = [];
6760
6831
  const seen = /* @__PURE__ */ new Set();
6832
+ collectFromFunctionReturns(fn, out, seen, 0);
6833
+ return out;
6834
+ }
6835
+ function collectFromFunctionReturns(fn, out, seen, depth) {
6761
6836
  if (Node28.isArrowFunction(fn)) {
6762
6837
  const body2 = fn.getBody();
6763
6838
  if (Node28.isExpression(body2)) {
6764
- collectFromObjectLiteral(body2, out, seen);
6765
- return out;
6839
+ collectFromReturnValue(body2, out, seen, depth);
6840
+ return;
6766
6841
  }
6767
6842
  }
6768
6843
  const body = fn.getBody?.();
6769
6844
  if (body === void 0) {
6770
- return out;
6845
+ return;
6771
6846
  }
6772
6847
  const returns = [];
6773
6848
  body.forEachDescendant((node, traversal) => {
@@ -6784,9 +6859,53 @@ function surfaceFactoryReturnMethods(fn) {
6784
6859
  if (expr === void 0) {
6785
6860
  continue;
6786
6861
  }
6862
+ collectFromReturnValue(expr, out, seen, depth);
6863
+ }
6864
+ }
6865
+ function collectFromReturnValue(node, out, seen, depth) {
6866
+ const expr = peelParens(node);
6867
+ if (Node28.isObjectLiteralExpression(expr)) {
6787
6868
  collectFromObjectLiteral(expr, out, seen);
6869
+ return;
6788
6870
  }
6789
- return out;
6871
+ if (depth >= MAX_BUILDER_HOPS || !Node28.isCallExpression(expr)) {
6872
+ return;
6873
+ }
6874
+ const callee = expr.getExpression();
6875
+ if (!Node28.isIdentifier(callee)) {
6876
+ return;
6877
+ }
6878
+ const helper = sameFileFunctionBehind(callee);
6879
+ if (helper !== null) {
6880
+ collectFromFunctionReturns(helper, out, seen, depth + 1);
6881
+ }
6882
+ }
6883
+ function sameFileFunctionBehind(id) {
6884
+ const symbol = id.getSymbol();
6885
+ if (symbol === void 0) {
6886
+ return null;
6887
+ }
6888
+ return functionAmong(
6889
+ symbol.getDeclarations(),
6890
+ id.getSourceFile().getFilePath()
6891
+ );
6892
+ }
6893
+ function functionAmong(declarations, filePath) {
6894
+ for (const decl of declarations) {
6895
+ if (decl.getSourceFile().getFilePath() !== filePath) {
6896
+ continue;
6897
+ }
6898
+ if (Node28.isFunctionDeclaration(decl)) {
6899
+ return decl;
6900
+ }
6901
+ if (Node28.isVariableDeclaration(decl)) {
6902
+ const init = decl.getInitializer();
6903
+ if (init !== void 0 && (Node28.isArrowFunction(init) || Node28.isFunctionExpression(init))) {
6904
+ return init;
6905
+ }
6906
+ }
6907
+ }
6908
+ return null;
6790
6909
  }
6791
6910
  function collectFromObjectLiteral(node, out, seen) {
6792
6911
  const e = peelParens(node);
@@ -6821,31 +6940,31 @@ function collectFromObjectLiteral(node, out, seen) {
6821
6940
  out.push({ func: v3, name });
6822
6941
  }
6823
6942
  }
6943
+ if (Node28.isShorthandPropertyAssignment(prop)) {
6944
+ collectShorthandFunction(prop, out, seen);
6945
+ }
6824
6946
  }
6825
6947
  }
6826
-
6827
- // src/discovery/packageExports.ts
6828
- var packageExportsCache = /* @__PURE__ */ new Map();
6829
- var PATH_STAMP_SEPARATOR = "";
6830
- function packageJsonStamp(packageJsonPath) {
6831
- try {
6832
- const stat = fs4.statSync(packageJsonPath);
6833
- return `${stat.mtimeMs}:${stat.size}`;
6834
- } catch {
6835
- return "unreadable";
6948
+ function collectShorthandFunction(prop, out, seen) {
6949
+ const name = prop.getName();
6950
+ if (seen.has(name)) {
6951
+ return;
6836
6952
  }
6837
- }
6838
- function resolvePackageExportsCached(packageJsonPath) {
6839
- const stamp = packageJsonStamp(packageJsonPath);
6840
- const key = `${packageJsonPath}${PATH_STAMP_SEPARATOR}${stamp}`;
6841
- const cached = packageExportsCache.get(key);
6842
- if (cached !== void 0) {
6843
- return cached;
6953
+ const symbol = prop.getValueSymbol();
6954
+ if (symbol === void 0) {
6955
+ return;
6956
+ }
6957
+ const fn = functionAmong(
6958
+ symbol.getDeclarations(),
6959
+ prop.getSourceFile().getFilePath()
6960
+ );
6961
+ if (fn !== null) {
6962
+ seen.add(name);
6963
+ out.push({ func: fn, name });
6844
6964
  }
6845
- const fresh = resolvePackageExports(packageJsonPath);
6846
- packageExportsCache.set(key, fresh);
6847
- return fresh;
6848
6965
  }
6966
+
6967
+ // src/discovery/packageExports.ts
6849
6968
  function discoverPackageExports(sourceFile, match, kind, resolution) {
6850
6969
  if (match.packageJsonPath === void 0) {
6851
6970
  return [];
@@ -6880,7 +6999,10 @@ function discoverPackageExports(sourceFile, match, kind, resolution) {
6880
6999
  if (seenNames.has(key)) {
6881
7000
  continue;
6882
7001
  }
6883
- for (const decl of decls) {
7002
+ const candidates = decls.flatMap(
7003
+ (decl) => sourceDeclarationsBehind(decl, resolution)
7004
+ );
7005
+ for (const decl of candidates) {
6884
7006
  if (Node29.isVariableDeclaration(decl)) {
6885
7007
  const init = decl.getInitializer();
6886
7008
  if (init !== void 0 && (Node29.isArrowFunction(init) || Node29.isFunctionExpression(init))) {
@@ -7035,7 +7157,7 @@ function attributedCalls(sourceFile, packages, resolution) {
7035
7157
  return null;
7036
7158
  }
7037
7159
  if (!methodComesFromSource(callee)) {
7038
- return subject;
7160
+ return null;
7039
7161
  }
7040
7162
  return {
7041
7163
  packageName: subject.packageName,
@@ -7149,12 +7271,12 @@ function originEndOf(flattened) {
7149
7271
  const slash = flattened.indexOf("/", opener[0].length);
7150
7272
  return slash === -1 ? flattened.length : slash;
7151
7273
  }
7152
- function appendPathText(path15, text) {
7274
+ function appendPathText(path14, text) {
7153
7275
  const idx = text.search(/[?#]/);
7154
7276
  if (idx === -1) {
7155
- return { path: path15 + text, stop: false };
7277
+ return { path: path14 + text, stop: false };
7156
7278
  }
7157
- return { path: path15 + text.slice(0, idx), stop: true };
7279
+ return { path: path14 + text.slice(0, idx), stop: true };
7158
7280
  }
7159
7281
  function parseAbsoluteUrl(text) {
7160
7282
  try {
@@ -7178,8 +7300,8 @@ function pathnameOfAbsoluteLiteral(text) {
7178
7300
  return stripQueryAndFragment(stripOriginManually(text));
7179
7301
  }
7180
7302
  function pathFromLiteralUrl(text) {
7181
- const path15 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
7182
- return path15 === "" ? void 0 : path15;
7303
+ const path14 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
7304
+ return path14 === "" ? void 0 : path14;
7183
7305
  }
7184
7306
  function pathFromTemplateUrl(arg, resolution) {
7185
7307
  const headText = arg.getHead().getLiteralText();
@@ -7187,7 +7309,7 @@ function pathFromTemplateUrl(arg, resolution) {
7187
7309
  const tails = spans.map((span) => span.getLiteral().getLiteralText());
7188
7310
  const originEnd = originEndOf([headText, ...tails].join(SUBSTITUTION));
7189
7311
  const head = appendPathText("", headText.slice(originEnd));
7190
- let path15 = head.path;
7312
+ let path14 = head.path;
7191
7313
  let stop = head.stop;
7192
7314
  let at = headText.length;
7193
7315
  for (let i = 0; i < spans.length && !stop; i++) {
@@ -7197,18 +7319,18 @@ function pathFromTemplateUrl(arg, resolution) {
7197
7319
  continue;
7198
7320
  }
7199
7321
  if (at >= originEnd) {
7200
- path15 += holeText(span.getExpression(), resolution);
7322
+ path14 += holeText(span.getExpression(), resolution);
7201
7323
  }
7202
7324
  at += SUBSTITUTION.length;
7203
7325
  const appended = appendPathText(
7204
- path15,
7326
+ path14,
7205
7327
  tailText.slice(Math.max(0, originEnd - at))
7206
7328
  );
7207
- path15 = appended.path;
7329
+ path14 = appended.path;
7208
7330
  stop = appended.stop;
7209
7331
  at += tailText.length;
7210
7332
  }
7211
- return path15 === "" ? void 0 : path15;
7333
+ return path14 === "" ? void 0 : path14;
7212
7334
  }
7213
7335
  function pathFromUrlNode(node, resolution) {
7214
7336
  if (Node31.isStringLiteral(node)) {
@@ -7278,12 +7400,12 @@ function discoverRegistrationCalls(sourceFile, match, kind, bindingExtraction, r
7278
7400
  match.importName,
7279
7401
  resolution
7280
7402
  );
7281
- if (registrationSubjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution)) {
7282
- return results;
7283
- }
7284
7403
  const registrationMethods = match.registrationChain.map(
7285
7404
  (c) => c.startsWith(".") ? c.slice(1) : c
7286
7405
  );
7406
+ if (registrationSubjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution, registrationMethods)) {
7407
+ return results;
7408
+ }
7287
7409
  sourceFile.forEachDescendant((node) => {
7288
7410
  if (!Node32.isCallExpression(node)) {
7289
7411
  return;
@@ -7477,8 +7599,54 @@ function receiverOf(subject, subjects, match, resolution) {
7477
7599
  );
7478
7600
  return candidates > 1 ? { kind: "several", candidates } : NOT_A_ROUTABLE;
7479
7601
  }
7480
- function storeCanFindSubjects(sourceFile, match, resolution) {
7481
- return resolution !== void 0 && importDeclarationsOf(sourceFile, [match.importModule]).length > 0;
7602
+ function storeCanFindSubjects(sourceFile, match, resolution, methods = []) {
7603
+ if (resolution === void 0) {
7604
+ return false;
7605
+ }
7606
+ if (importDeclarationsOf(sourceFile, [match.importModule]).length > 0) {
7607
+ return true;
7608
+ }
7609
+ return methods.length > 0 && callsOnCallResult(sourceFile, methods);
7610
+ }
7611
+ function callsOnCallResult(sourceFile, methods) {
7612
+ let called = methodsCalledOnCallResults.get(sourceFile);
7613
+ if (called === void 0) {
7614
+ called = collectMethodsCalledOnCallResults(sourceFile);
7615
+ methodsCalledOnCallResults.set(sourceFile, called);
7616
+ }
7617
+ return methods.some((method) => called.has(method));
7618
+ }
7619
+ var methodsCalledOnCallResults = /* @__PURE__ */ new WeakMap();
7620
+ function collectMethodsCalledOnCallResults(sourceFile) {
7621
+ const setToCalls = /* @__PURE__ */ new Set();
7622
+ const calledOn = /* @__PURE__ */ new Map();
7623
+ sourceFile.forEachDescendant((node) => {
7624
+ if (Node32.isVariableDeclaration(node)) {
7625
+ const init = node.getInitializer();
7626
+ if (init !== void 0 && Node32.isCallExpression(init)) {
7627
+ setToCalls.add(node.getName());
7628
+ }
7629
+ return;
7630
+ }
7631
+ if (!Node32.isCallExpression(node)) {
7632
+ return;
7633
+ }
7634
+ const callee = node.getExpression();
7635
+ if (!Node32.isPropertyAccessExpression(callee) || !Node32.isIdentifier(callee.getExpression())) {
7636
+ return;
7637
+ }
7638
+ const receiver = callee.getExpression().getText();
7639
+ const methods = calledOn.get(receiver) ?? /* @__PURE__ */ new Set();
7640
+ methods.add(callee.getName());
7641
+ calledOn.set(receiver, methods);
7642
+ });
7643
+ const called = /* @__PURE__ */ new Set();
7644
+ for (const name of setToCalls) {
7645
+ for (const method of calledOn.get(name) ?? []) {
7646
+ called.add(method);
7647
+ }
7648
+ }
7649
+ return called;
7482
7650
  }
7483
7651
  function registrationSubjectsOf(sourceFile, importModule, importName, resolution) {
7484
7652
  const importedLocalName = resolveImportedLocalName(
@@ -7543,9 +7711,33 @@ function registrationSubjectIdsOf(sourceFile, matches, resolution) {
7543
7711
  ).values()) {
7544
7712
  ids.add(nodeId(node));
7545
7713
  }
7714
+ for (const node of unboundConstructionsOf(
7715
+ sourceFile,
7716
+ match.importModule,
7717
+ match.importName
7718
+ )) {
7719
+ ids.add(nodeId(node));
7720
+ }
7546
7721
  }
7547
7722
  return ids;
7548
7723
  }
7724
+ function unboundConstructionsOf(sourceFile, importModule, importName) {
7725
+ const importedLocalName = resolveImportedLocalName(
7726
+ sourceFile,
7727
+ importModule,
7728
+ importName
7729
+ );
7730
+ if (importedLocalName === null) {
7731
+ return [];
7732
+ }
7733
+ const constructions = [];
7734
+ sourceFile.forEachDescendant((node) => {
7735
+ if ((Node32.isCallExpression(node) || Node32.isNewExpression(node)) && node.getExpression().getText() === importedLocalName && !Node32.isVariableDeclaration(node.getParent())) {
7736
+ constructions.push(node);
7737
+ }
7738
+ });
7739
+ return constructions;
7740
+ }
7549
7741
  function discoverMountEdges(sourceFile, match, mount, knownSubjectIds, resolution) {
7550
7742
  const subjects = registrationSubjectsOf(
7551
7743
  sourceFile,
@@ -7553,7 +7745,7 @@ function discoverMountEdges(sourceFile, match, mount, knownSubjectIds, resolutio
7553
7745
  match.importName,
7554
7746
  resolution
7555
7747
  );
7556
- if (subjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution)) {
7748
+ if (subjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution, [mount.method])) {
7557
7749
  return [];
7558
7750
  }
7559
7751
  const edges = [];
@@ -7634,11 +7826,11 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
7634
7826
  return null;
7635
7827
  }
7636
7828
  const method2 = stringProperty(routeObject, binding.method.property);
7637
- const path16 = stringProperty(routeObject, binding.path.property);
7638
- if (method2 === null || path16 === null) {
7829
+ const path15 = stringProperty(routeObject, binding.path.property);
7830
+ if (method2 === null || path15 === null) {
7639
7831
  return null;
7640
7832
  }
7641
- return { method: method2.toUpperCase(), path: path16 };
7833
+ return { method: method2.toUpperCase(), path: path15 };
7642
7834
  }
7643
7835
  if (binding.method.type !== "fromRegistration" || binding.path.type !== "fromArgument") {
7644
7836
  return null;
@@ -7655,8 +7847,8 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
7655
7847
  method = arg.getLiteralValue().toUpperCase();
7656
7848
  }
7657
7849
  const pathArg = call.getArguments()[binding.path.position];
7658
- const path15 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
7659
- return path15 === void 0 ? null : { method, path: path15 };
7850
+ const path14 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
7851
+ return path14 === void 0 ? null : { method, path: path14 };
7660
7852
  }
7661
7853
 
7662
7854
  // src/discovery/registrationLoop.ts
@@ -7788,7 +7980,7 @@ function bodyCallsOnRoutable(loop, guard, resolution) {
7788
7980
  }
7789
7981
  function readRouteSpec(element, shape, resolution) {
7790
7982
  let method = null;
7791
- let path15 = null;
7983
+ let path14 = null;
7792
7984
  let handler = null;
7793
7985
  for (const prop of element.getProperties()) {
7794
7986
  if (Node33.isMethodDeclaration(prop) && prop.getName() === shape.handlerKey) {
@@ -7806,16 +7998,16 @@ function readRouteSpec(element, shape, resolution) {
7806
7998
  if (name === shape.methodKey) {
7807
7999
  method = readStringLiteralValue(init);
7808
8000
  } else if (name === shape.pathKey) {
7809
- path15 = readStringLiteralValue(init);
8001
+ path14 = readStringLiteralValue(init);
7810
8002
  } else if (name === shape.handlerKey) {
7811
8003
  const func = functionValueOf(init, resolution);
7812
8004
  handler = func === null ? null : { func, name: handlerName(init) };
7813
8005
  }
7814
8006
  }
7815
- if (method === null || path15 === null || handler === null) {
8007
+ if (method === null || path14 === null || handler === null) {
7816
8008
  return null;
7817
8009
  }
7818
- return { method, path: path15, handler: handler.func, handlerName: handler.name };
8010
+ return { method, path: path14, handler: handler.func, handlerName: handler.name };
7819
8011
  }
7820
8012
  function readStringLiteralValue(node) {
7821
8013
  if (Node33.isStringLiteral(node) || Node33.isNoSubstitutionTemplateLiteral(node)) {
@@ -7828,7 +8020,7 @@ function handlerName(value) {
7828
8020
  }
7829
8021
 
7830
8022
  // src/discovery/registrationTemplate.ts
7831
- import path6 from "path";
8023
+ import path7 from "path";
7832
8024
  import { Node as Node35 } from "ts-morph";
7833
8025
  import { joinMountedPath as joinMountedPath2 } from "@suss/resolution";
7834
8026
 
@@ -7837,6 +8029,10 @@ import {
7837
8029
  Node as Node34,
7838
8030
  SyntaxKind as SyntaxKind7
7839
8031
  } from "ts-morph";
8032
+ import {
8033
+ unfollowedCallGap,
8034
+ worthRecording
8035
+ } from "@suss/behavioral-ir";
7840
8036
  function isInExternalCode(sourceFile) {
7841
8037
  if (sourceFile.isDeclarationFile()) {
7842
8038
  return true;
@@ -7890,6 +8086,19 @@ function declarationsBehind(symbol) {
7890
8086
  function isParameterOf(declaration, scanning) {
7891
8087
  return Node34.isParameterDeclaration(declaration) && declaration.getParent() === scanning;
7892
8088
  }
8089
+ function parameterIndexOf(declarations, scanning) {
8090
+ if (scanning === void 0) {
8091
+ return void 0;
8092
+ }
8093
+ const params = scanning.getParameters();
8094
+ for (const declaration of declarations) {
8095
+ const index = params.findIndex((param) => param === declaration);
8096
+ if (index !== -1) {
8097
+ return index;
8098
+ }
8099
+ }
8100
+ return void 0;
8101
+ }
7893
8102
  function classifyStop(declarations, scanning) {
7894
8103
  if (declarations.length === 0) {
7895
8104
  return "noDeclaration";
@@ -7910,36 +8119,6 @@ function classifyStop(declarations, scanning) {
7910
8119
  }
7911
8120
  return "unsettledValue";
7912
8121
  }
7913
- var RECORDED = {
7914
- noBody: true,
7915
- unsettledValue: true,
7916
- multipleSources: true,
7917
- outsideRun: false,
7918
- noDeclaration: false,
7919
- callerSupplied: false,
7920
- multipleReceivers: true
7921
- };
7922
- function worthRecording(reason) {
7923
- return RECORDED[reason];
7924
- }
7925
- var STOP_SENTENCE = {
7926
- noBody: ({ callee }) => `The call to ${callee} lands on a declaration with no body, so whatever runs there is missing from this summary`,
7927
- unsettledValue: ({ callee }) => `The call to ${callee} goes through a value this run could not settle, so whatever runs there is missing from this summary`,
7928
- multipleSources: ({ callee }) => `The call to ${callee} reaches a value with more than one possible source, so whatever runs there is missing from this summary`,
7929
- outsideRun: ({ callee }) => `The call to ${callee} lands in a package whose source is not in this run, so whatever runs there is missing from this summary`,
7930
- noDeclaration: ({ callee }) => `The call to ${callee} has no declaration this run could find, so whatever runs there is missing from this summary`,
7931
- callerSupplied: ({ callee }) => `The call to ${callee} runs the function this unit's caller passed in, so what happens there is decided at the call site`,
7932
- multipleReceivers: ({ callee, candidates }) => `The call to ${callee} is made on a receiver this run reads as ${candidates ?? "several"} different values, so nothing says which one it registers on and the registration is left out`
7933
- };
7934
- function unfollowedCallGap(stop) {
7935
- return {
7936
- type: "unfollowedCall",
7937
- conditions: [],
7938
- consequence: "unknown",
7939
- description: STOP_SENTENCE[stop.reason](stop),
7940
- callee: stop.callee
7941
- };
7942
- }
7943
8122
 
7944
8123
  // src/discovery/registrationTemplate.ts
7945
8124
  function discoverRegistrationTemplates(sourceFile, match, kind, resolution, mountPrefixes) {
@@ -7949,8 +8128,8 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution, moun
7949
8128
  const subject = subjectOfCall(sourceFile, args, match, resolution);
7950
8129
  const prefix = subject === void 0 ? "" : mountPrefixes?.effectivePrefixFor(subject) ?? "";
7951
8130
  for (const reg of match.registrations) {
7952
- const path15 = substitutePath(reg.pathTemplate, args);
7953
- if (path15 === null) {
8131
+ const path14 = substitutePath(reg.pathTemplate, args);
8132
+ if (path14 === null) {
7954
8133
  continue;
7955
8134
  }
7956
8135
  const handler = resolveHandler(reg.handlerArg, args, resolution);
@@ -7964,7 +8143,7 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution, moun
7964
8143
  ...subject === void 0 ? {} : { registrationSubjectId: nodeId(subject) },
7965
8144
  routeInfo: {
7966
8145
  method: reg.method.toUpperCase(),
7967
- path: prefix === "" ? path15 : joinMountedPath2(prefix, path15)
8146
+ path: prefix === "" ? path14 : joinMountedPath2(prefix, path14)
7968
8147
  }
7969
8148
  });
7970
8149
  }
@@ -7997,7 +8176,7 @@ function subjectOfCall(sourceFile, args, match, resolution) {
7997
8176
  }
7998
8177
  function helperCallsOf(sourceFile, match, resolution) {
7999
8178
  const module = match.importModule;
8000
- if (module !== void 0 && !path6.isAbsolute(module)) {
8179
+ if (module !== void 0 && !path7.isAbsolute(module)) {
8001
8180
  const store = resolution ?? new ResolutionStore();
8002
8181
  return callsResolvingTo(sourceFile, store, {
8003
8182
  module,
@@ -8443,24 +8622,24 @@ function declName(decl) {
8443
8622
 
8444
8623
  // src/parameterReads.ts
8445
8624
  function pathAbove(reference) {
8446
- const path15 = [];
8625
+ const path14 = [];
8447
8626
  let current2 = reference;
8448
8627
  for (; ; ) {
8449
8628
  const parent = current2.getParent();
8450
8629
  if (parent !== void 0 && Node38.isPropertyAccessExpression(parent) && parent.getExpression() === current2) {
8451
- path15.push(parent.getName());
8630
+ path14.push(parent.getName());
8452
8631
  current2 = parent;
8453
8632
  continue;
8454
8633
  }
8455
8634
  if (parent !== void 0 && Node38.isElementAccessExpression(parent) && parent.getExpression() === current2) {
8456
8635
  const argument = parent.getArgumentExpression();
8457
8636
  if (argument !== void 0 && Node38.isStringLiteral(argument)) {
8458
- path15.push(argument.getLiteralValue());
8637
+ path14.push(argument.getLiteralValue());
8459
8638
  current2 = parent;
8460
8639
  continue;
8461
8640
  }
8462
8641
  }
8463
- return path15;
8642
+ return path14;
8464
8643
  }
8465
8644
  }
8466
8645
  function declaresParameterOf(node, func) {
@@ -9159,142 +9338,40 @@ function importSpecifierMatches(decl, expectedModule) {
9159
9338
  return Node41.isImportDeclaration(importDecl) && importDecl.getModuleSpecifierValue() === expectedModule;
9160
9339
  }
9161
9340
  function extractInvocationEffects(func, barriers = NO_BARRIERS) {
9162
- const results = [];
9341
+ const calls = [];
9163
9342
  func.forEachDescendant((node, traversal) => {
9164
- if (isDescentStop(node, func, barriers)) {
9343
+ if (isDescentStop(node, func, barriers) || Node41.isDecorator(node)) {
9165
9344
  traversal.skip();
9166
9345
  return;
9167
9346
  }
9168
- if (Node41.isExpressionStatement(node)) {
9169
- const { call, async } = unwrapCall(node.getExpression());
9170
- if (call !== null) {
9171
- const preconditions = collectPreconditions(node, func);
9172
- results.push({
9173
- effect: {
9174
- type: "invocation",
9175
- callee: call.getExpression().getText(),
9176
- args: extractArgs(call),
9177
- async,
9178
- ...preconditions.length > 0 ? { preconditions } : {}
9179
- },
9180
- line: startLineOf(node),
9181
- neverTerminal: false,
9182
- alwaysRuns: runsOnEveryPath(node, func)
9183
- });
9184
- }
9185
- return;
9186
- }
9187
- if (Node41.isSpreadElement(node)) {
9188
- const parent = node.getParent();
9189
- if (parent !== void 0 && (Node41.isArrayLiteralExpression(parent) || Node41.isObjectLiteralExpression(parent))) {
9190
- const { call, async } = unwrapCall(node.getExpression());
9191
- if (call !== null) {
9192
- const preconditions = collectPreconditions(node, func);
9193
- results.push({
9194
- effect: {
9195
- type: "invocation",
9196
- callee: call.getExpression().getText(),
9197
- args: extractArgs(call),
9198
- async,
9199
- ...preconditions.length > 0 ? { preconditions } : {}
9200
- },
9201
- line: enclosingStatementLine(node),
9202
- neverTerminal: true,
9203
- alwaysRuns: runsOnEveryPath(node, func)
9204
- });
9205
- }
9206
- }
9207
- return;
9208
- }
9209
- if (Node41.isVariableDeclaration(node)) {
9210
- const initializer = node.getInitializer();
9211
- if (initializer !== void 0) {
9212
- const { call, async } = unwrapCall(initializer);
9213
- if (call !== null) {
9214
- const preconditions = collectPreconditions(node, func);
9215
- results.push({
9216
- effect: {
9217
- type: "invocation",
9218
- callee: call.getExpression().getText(),
9219
- args: extractArgs(call),
9220
- async,
9221
- ...preconditions.length > 0 ? { preconditions } : {}
9222
- },
9223
- line: enclosingStatementLine(node),
9224
- neverTerminal: true,
9225
- alwaysRuns: runsOnEveryPath(node, func)
9226
- });
9227
- }
9228
- }
9229
- return;
9230
- }
9231
- if (Node41.isReturnStatement(node)) {
9232
- const returned = node.getExpression();
9233
- const { call, async } = returned === void 0 ? { call: null, async: false } : unwrapCall(returned);
9234
- if (call !== null) {
9235
- const preconditions = collectPreconditions(node, func);
9236
- results.push({
9237
- effect: {
9238
- type: "invocation",
9239
- callee: call.getExpression().getText(),
9240
- args: extractArgs(call),
9241
- async,
9242
- ...preconditions.length > 0 ? { preconditions } : {}
9243
- },
9244
- line: startLineOf(node),
9245
- neverTerminal: false,
9246
- alwaysRuns: runsOnEveryPath(node, func),
9247
- node: call
9248
- });
9249
- }
9250
- return;
9251
- }
9252
- if (Node41.isArrowFunction(node)) {
9253
- const body = node.getBody();
9254
- const { call, async } = Node41.isExpression(body) ? unwrapCall(body) : { call: null, async: false };
9255
- if (call !== null) {
9256
- const preconditions = collectPreconditions(node, func);
9257
- results.push({
9258
- effect: {
9259
- type: "invocation",
9260
- callee: call.getExpression().getText(),
9261
- args: extractArgs(call),
9262
- async: async || node.isAsync(),
9263
- ...preconditions.length > 0 ? { preconditions } : {}
9264
- },
9265
- line: startLineOf(node),
9266
- neverTerminal: false,
9267
- alwaysRuns: runsOnEveryPath(node, func),
9268
- node: call
9269
- });
9270
- }
9271
- return;
9272
- }
9273
9347
  if (Node41.isCallExpression(node)) {
9274
- const parent = node.getParent();
9275
- if (parent === void 0) {
9276
- return;
9277
- }
9278
- const isArrayElement = Node41.isArrayLiteralExpression(parent);
9279
- const isPropertyValue = Node41.isPropertyAssignment(parent) && parent.getInitializer() === node;
9280
- if (isArrayElement || isPropertyValue) {
9281
- const preconditions = collectPreconditions(node, func);
9282
- results.push({
9283
- effect: {
9284
- type: "invocation",
9285
- callee: node.getExpression().getText(),
9286
- args: extractArgs(node),
9287
- async: false,
9288
- ...preconditions.length > 0 ? { preconditions } : {}
9289
- },
9290
- line: enclosingStatementLine(node),
9291
- neverTerminal: true,
9292
- alwaysRuns: runsOnEveryPath(node, func)
9293
- });
9294
- }
9348
+ calls.push(node);
9295
9349
  }
9296
9350
  });
9297
- return results;
9351
+ calls.sort((a, b) => a.getEnd() - b.getEnd());
9352
+ return calls.map((call) => invocationAt(call, func));
9353
+ }
9354
+ function invocationAt(call, func) {
9355
+ const preconditions = collectPreconditions(call, func);
9356
+ return {
9357
+ effect: {
9358
+ type: "invocation",
9359
+ callee: call.getExpression().getText(),
9360
+ args: extractArgs(call),
9361
+ async: isAwaited(call),
9362
+ ...preconditions.length > 0 ? { preconditions } : {}
9363
+ },
9364
+ node: call,
9365
+ line: enclosingStatementLine(call),
9366
+ alwaysRuns: runsOnEveryPath(call, func)
9367
+ };
9368
+ }
9369
+ function isAwaited(call) {
9370
+ let current2 = call.getParent();
9371
+ while (current2 !== void 0 && Node41.isParenthesizedExpression(current2)) {
9372
+ current2 = current2.getParent();
9373
+ }
9374
+ return current2 !== void 0 && Node41.isAwaitExpression(current2);
9298
9375
  }
9299
9376
  function runInvocationRecognizers(func, recognizers, barriers = NO_BARRIERS, resolveWrittenValue, originatesFrom, anchorCallsOf) {
9300
9377
  if (recognizers.length === 0) {
@@ -9637,22 +9714,6 @@ function enclosingStatementLine(node) {
9637
9714
  }
9638
9715
  return startLineOf(node);
9639
9716
  }
9640
- function unwrapCall(node) {
9641
- if (Node41.isAwaitExpression(node)) {
9642
- const inner = node.getExpression();
9643
- if (Node41.isCallExpression(inner)) {
9644
- return { call: inner, async: true };
9645
- }
9646
- return { call: null, async: false };
9647
- }
9648
- if (Node41.isParenthesizedExpression(node)) {
9649
- return unwrapCall(node.getExpression());
9650
- }
9651
- if (Node41.isCallExpression(node)) {
9652
- return { call: node, async: false };
9653
- }
9654
- return { call: null, async: false };
9655
- }
9656
9717
 
9657
9718
  // src/terminals/index.ts
9658
9719
  import { Node as Node51 } from "ts-morph";
@@ -11694,6 +11755,22 @@ function firesOn(site, branch) {
11694
11755
  }
11695
11756
  return site.alwaysRuns || runsBefore(site.line, branch.terminal.location.end);
11696
11757
  }
11758
+ function writesTerminal(call, terminalNodes) {
11759
+ let current2 = call;
11760
+ for (; ; ) {
11761
+ if (terminalNodes.has(current2)) {
11762
+ return true;
11763
+ }
11764
+ const parent = current2.getParent();
11765
+ if (parent === void 0 || !isReceiverOf(current2, parent)) {
11766
+ return false;
11767
+ }
11768
+ current2 = parent;
11769
+ }
11770
+ }
11771
+ function isReceiverOf(node, parent) {
11772
+ return (Node52.isPropertyAccessExpression(parent) || Node52.isElementAccessExpression(parent) || Node52.isCallExpression(parent)) && parent.getExpression() === node;
11773
+ }
11697
11774
  var isDefaultConditionList = (conditions) => conditions.length === 0 || conditions.every(
11698
11775
  (c) => c.source === "earlyReturn" || c.source === "earlyThrow"
11699
11776
  );
@@ -11832,11 +11909,8 @@ function extractRawBranches(func, terminalPatterns, invocationRecognizers = [],
11832
11909
  });
11833
11910
  if (invocations.length > 0) {
11834
11911
  const terminalNodes = new Set(terminals.map(({ node }) => node));
11835
- const terminalLines = new Set(
11836
- distinctBranches.map((b) => b.terminal.location.start)
11837
- );
11838
11912
  const sideEffects = invocations.filter(
11839
- (i) => i.node !== void 0 ? !terminalNodes.has(i.node) : i.neverTerminal || !terminalLines.has(i.line)
11913
+ (i) => !writesTerminal(i.node, terminalNodes)
11840
11914
  );
11841
11915
  for (const branch of distinctBranches) {
11842
11916
  branch.effects = sideEffects.filter(
@@ -11882,9 +11956,9 @@ function createSourceFileLookup(project) {
11882
11956
  if (sf === null) {
11883
11957
  return null;
11884
11958
  }
11885
- const path15 = sf.getFilePath();
11886
- const index = functionsByFile.get(path15) ?? indexFunctions(sf);
11887
- functionsByFile.set(path15, index);
11959
+ const path14 = sf.getFilePath();
11960
+ const index = functionsByFile.get(path14) ?? indexFunctions(sf);
11961
+ functionsByFile.set(path14, index);
11888
11962
  if (location.span !== void 0) {
11889
11963
  const bySpan = index.get(spanKey(location.span));
11890
11964
  if (bySpan !== void 0) {
@@ -11929,356 +12003,6 @@ function indexFunctions(sf) {
11929
12003
  return index;
11930
12004
  }
11931
12005
 
11932
- // src/cache.ts
11933
- import { createHash as createHash2 } from "crypto";
11934
- import fs5 from "fs/promises";
11935
- import path7 from "path";
11936
- var SCHEMA_VERSION = "6";
11937
- var MAX_ENTRIES = 2;
11938
- var ENTRY_PREFIX = "key-";
11939
- var ENTRY_DIR_NAME = new RegExp(`^${ENTRY_PREFIX}[0-9a-f]{16}$`);
11940
- function createCacheLayer(cacheDir) {
11941
- if (cacheDir === null) {
11942
- return {
11943
- tryHit: async () => null,
11944
- lookup: async () => ({
11945
- kind: "miss",
11946
- diagnostic: { kind: "miss", missReason: "no-manifest" }
11947
- }),
11948
- plan: async () => null,
11949
- write: async () => {
11950
- }
11951
- };
11952
- }
11953
- return {
11954
- async tryHit(input) {
11955
- const result = await this.lookup(input);
11956
- return result.kind === "hit" ? result.summaries : null;
11957
- },
11958
- async lookup(input) {
11959
- const entryDir = entryDirFor(cacheDir, input);
11960
- const manifest = await readManifest(path7.join(entryDir, "manifest.json"));
11961
- if (manifest === null) {
11962
- return missDiag(await describeAbsentEntry(cacheDir));
11963
- }
11964
- const currentTsconfigStamp = await stampTsconfig(input.tsconfigPath);
11965
- if (!fileStampEquals(manifest.tsconfigStamp, currentTsconfigStamp)) {
11966
- return missDiag("tsconfig-changed");
11967
- }
11968
- const currentFiles = await resolveFileStamps(input);
11969
- if (!fileStampsEqual(manifest.files, currentFiles)) {
11970
- return missDiag("files-changed");
11971
- }
11972
- await markUsed(entryDir);
11973
- return {
11974
- kind: "hit",
11975
- summaries: manifest.summaries,
11976
- diagnostic: { kind: "hit" }
11977
- };
11978
- },
11979
- async plan(input) {
11980
- const entryDir = entryDirFor(cacheDir, input);
11981
- const manifest = await readManifest(path7.join(entryDir, "manifest.json"));
11982
- if (manifest === null || manifest.roots === void 0 || manifest.rootMeta === void 0 || manifest.owners === void 0) {
11983
- return null;
11984
- }
11985
- const currentTsconfigStamp = await stampTsconfig(input.tsconfigPath);
11986
- if (!fileStampEquals(manifest.tsconfigStamp, currentTsconfigStamp)) {
11987
- return null;
11988
- }
11989
- await markUsed(entryDir);
11990
- return buildPlan(manifest, await resolveFileStamps(input));
11991
- },
11992
- async write(input, summaries, attribution) {
11993
- const entryDir = entryDirFor(cacheDir, input);
11994
- const previous = await readManifest(path7.join(entryDir, "manifest.json"));
11995
- const tsconfigStamp = await stampTsconfig(input.tsconfigPath);
11996
- const files = await hashStamps(await resolveFileStamps(input), previous);
11997
- const manifest = {
11998
- schemaVersion: SCHEMA_VERSION,
11999
- adapterPacksDigest: input.adapterPacksDigest,
12000
- tsconfigStamp,
12001
- files,
12002
- summaries,
12003
- ...attribution === void 0 ? {} : encodeAttribution(attribution)
12004
- };
12005
- await fs5.mkdir(entryDir, { recursive: true });
12006
- await fs5.writeFile(
12007
- path7.join(entryDir, "manifest.json"),
12008
- JSON.stringify(manifest)
12009
- );
12010
- await evictOldEntries(cacheDir, entryDir);
12011
- }
12012
- };
12013
- }
12014
- async function buildPlan(manifest, currentStamps) {
12015
- const stored = new Map(manifest.files.map((f) => [f.path, f]));
12016
- const current2 = new Map(currentStamps.map((f) => [f.path, f]));
12017
- const changed = /* @__PURE__ */ new Set();
12018
- const removed = /* @__PURE__ */ new Set();
12019
- for (const p of stored.keys()) {
12020
- if (!current2.has(p)) {
12021
- removed.add(p);
12022
- }
12023
- }
12024
- const toVerify = [];
12025
- for (const [p, stamp] of current2) {
12026
- const before = stored.get(p);
12027
- if (before === void 0) {
12028
- changed.add(p);
12029
- } else if (!fileStampEquals(before, stamp)) {
12030
- if (before.contentHash === void 0) {
12031
- changed.add(p);
12032
- } else {
12033
- toVerify.push(p);
12034
- }
12035
- }
12036
- }
12037
- await Promise.all(
12038
- toVerify.map(async (p) => {
12039
- const hash = await hashFile(p);
12040
- if (hash === null || hash !== stored.get(p)?.contentHash) {
12041
- changed.add(p);
12042
- }
12043
- })
12044
- );
12045
- const roots = /* @__PURE__ */ new Map();
12046
- const rootNames = manifest.roots ?? [];
12047
- const depPaths = manifest.depPaths ?? [];
12048
- let rootsDeclined = 0;
12049
- rootNames.forEach((rootPath, i) => {
12050
- const meta = manifest.rootMeta?.[i];
12051
- if (meta === void 0) {
12052
- return;
12053
- }
12054
- if (!meta.cacheable) {
12055
- rootsDeclined += 1;
12056
- }
12057
- roots.set(rootPath, {
12058
- path: rootPath,
12059
- cacheable: meta.cacheable,
12060
- deps: meta.deps.flatMap((d) => {
12061
- const p = depPaths[d];
12062
- return p === void 0 ? [] : [p];
12063
- }),
12064
- claims: meta.claims,
12065
- mountPrefixes: meta.mountPrefixes,
12066
- packs: meta.packs
12067
- });
12068
- });
12069
- const validRoots = /* @__PURE__ */ new Set();
12070
- for (const [rootPath, record2] of roots) {
12071
- if (!record2.cacheable || !current2.has(rootPath)) {
12072
- continue;
12073
- }
12074
- if (changed.has(rootPath)) {
12075
- continue;
12076
- }
12077
- const depMoved = record2.deps.some((d) => changed.has(d) || removed.has(d));
12078
- if (!depMoved) {
12079
- validRoots.add(rootPath);
12080
- }
12081
- }
12082
- const owners = manifest.owners ?? [];
12083
- return {
12084
- changed,
12085
- removed,
12086
- roots,
12087
- validRoots,
12088
- rootsDeclined,
12089
- reuse(valid) {
12090
- const summaries = [];
12091
- const reusedOwners = [];
12092
- manifest.summaries.forEach((summary, i) => {
12093
- const ownerPaths = (owners[i] ?? []).flatMap((o) => {
12094
- const p = rootNames[o];
12095
- return p === void 0 ? [] : [p];
12096
- });
12097
- if (ownerPaths.some((p) => valid.has(p))) {
12098
- summaries.push(summary);
12099
- reusedOwners.push(ownerPaths.filter((p) => valid.has(p)));
12100
- }
12101
- });
12102
- return { summaries, owners: reusedOwners };
12103
- },
12104
- allSummaries() {
12105
- return manifest.summaries;
12106
- },
12107
- attribution() {
12108
- return {
12109
- roots: [...roots.values()],
12110
- owners: owners.map(
12111
- (ownerIds) => ownerIds.flatMap((o) => {
12112
- const p = rootNames[o];
12113
- return p === void 0 ? [] : [p];
12114
- })
12115
- )
12116
- };
12117
- }
12118
- };
12119
- }
12120
- function encodeAttribution(attribution) {
12121
- const roots = attribution.roots.map((r) => r.path);
12122
- const rootIndex = new Map(roots.map((p, i) => [p, i]));
12123
- const depIndex = /* @__PURE__ */ new Map();
12124
- const depPaths = [];
12125
- const depIdOf = (p) => {
12126
- const existing = depIndex.get(p);
12127
- if (existing !== void 0) {
12128
- return existing;
12129
- }
12130
- const id = depPaths.length;
12131
- depPaths.push(p);
12132
- depIndex.set(p, id);
12133
- return id;
12134
- };
12135
- const rootMeta = attribution.roots.map((r) => ({
12136
- cacheable: r.cacheable,
12137
- deps: r.deps.map(depIdOf),
12138
- claims: r.claims,
12139
- mountPrefixes: r.mountPrefixes,
12140
- packs: r.packs
12141
- }));
12142
- const owners = attribution.owners.map(
12143
- (ownerPaths) => ownerPaths.flatMap((p) => {
12144
- const i = rootIndex.get(p);
12145
- return i === void 0 ? [] : [i];
12146
- })
12147
- );
12148
- return { roots, rootMeta, depPaths, owners };
12149
- }
12150
- async function hashStamps(stamps, previous) {
12151
- const before = new Map((previous?.files ?? []).map((f) => [f.path, f]));
12152
- return Promise.all(
12153
- stamps.map(async (stamp) => {
12154
- const prior = before.get(stamp.path);
12155
- if (prior !== void 0 && prior.contentHash !== void 0 && fileStampEquals(prior, stamp)) {
12156
- return { ...stamp, contentHash: prior.contentHash };
12157
- }
12158
- const contentHash = await hashFile(stamp.path);
12159
- return contentHash === null ? stamp : { ...stamp, contentHash };
12160
- })
12161
- );
12162
- }
12163
- async function hashFile(filePath) {
12164
- try {
12165
- const content = await fs5.readFile(filePath);
12166
- return createHash2("sha256").update(content).digest("hex").slice(0, 16);
12167
- } catch {
12168
- return null;
12169
- }
12170
- }
12171
- async function describeAbsentEntry(cacheDir) {
12172
- try {
12173
- const entries = await fs5.readdir(cacheDir, { withFileTypes: true });
12174
- return entries.some((e) => e.isDirectory() && isEntryDir(e.name)) ? "key-changed" : "no-manifest";
12175
- } catch {
12176
- return "no-manifest";
12177
- }
12178
- }
12179
- function entryDirFor(cacheDir, input) {
12180
- const key = [
12181
- SCHEMA_VERSION,
12182
- input.adapterPacksDigest,
12183
- input.tsconfigPath ?? ""
12184
- ].join(" ");
12185
- const name = createHash2("sha256").update(key).digest("hex").slice(0, 16);
12186
- return path7.join(cacheDir, `${ENTRY_PREFIX}${name}`);
12187
- }
12188
- function isEntryDir(name) {
12189
- return ENTRY_DIR_NAME.test(name);
12190
- }
12191
- async function markUsed(entryDir) {
12192
- const now = /* @__PURE__ */ new Date();
12193
- try {
12194
- await fs5.utimes(entryDir, now, now);
12195
- } catch {
12196
- }
12197
- }
12198
- async function evictOldEntries(cacheDir, keepDir) {
12199
- try {
12200
- await fs5.rm(path7.join(cacheDir, "manifest.json"), { force: true });
12201
- const entries = await fs5.readdir(cacheDir, { withFileTypes: true });
12202
- const dirs = await Promise.all(
12203
- entries.filter((e) => e.isDirectory() && isEntryDir(e.name)).map(async (e) => {
12204
- const dir = path7.join(cacheDir, e.name);
12205
- const stat = await fs5.stat(dir);
12206
- return { dir, mtimeMs: stat.mtimeMs };
12207
- })
12208
- );
12209
- const doomed = dirs.filter((d) => d.dir !== keepDir).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_ENTRIES - 1);
12210
- await Promise.all(
12211
- doomed.map((d) => fs5.rm(d.dir, { recursive: true, force: true }))
12212
- );
12213
- } catch {
12214
- }
12215
- }
12216
- function missDiag(reason) {
12217
- return {
12218
- kind: "miss",
12219
- diagnostic: { kind: "miss", missReason: reason }
12220
- };
12221
- }
12222
- async function readManifest(manifestPath) {
12223
- try {
12224
- const raw = await fs5.readFile(manifestPath, "utf-8");
12225
- return JSON.parse(raw);
12226
- } catch {
12227
- return null;
12228
- }
12229
- }
12230
- async function stampTsconfig(tsconfigPath) {
12231
- if (tsconfigPath === void 0) {
12232
- return null;
12233
- }
12234
- try {
12235
- const stat = await fs5.stat(tsconfigPath);
12236
- return { path: tsconfigPath, mtimeMs: stat.mtimeMs, size: stat.size };
12237
- } catch {
12238
- return null;
12239
- }
12240
- }
12241
- async function resolveFileStamps(input) {
12242
- if (input.files !== void 0) {
12243
- return stampPaths(input.files);
12244
- }
12245
- if (input.project !== void 0) {
12246
- return stampProjectFiles(input.project);
12247
- }
12248
- return [];
12249
- }
12250
- async function stampProjectFiles(project) {
12251
- const files = project.getSourceFiles().filter((sf) => !sf.isDeclarationFile()).map((sf) => sf.getFilePath());
12252
- return stampPaths(files);
12253
- }
12254
- async function stampPaths(paths) {
12255
- const files = paths;
12256
- const stamped = await Promise.all(
12257
- files.map(async (p) => {
12258
- try {
12259
- const stat = await fs5.stat(p);
12260
- return { path: p, mtimeMs: stat.mtimeMs, size: stat.size };
12261
- } catch {
12262
- return { path: p, mtimeMs: -1, size: -1 };
12263
- }
12264
- })
12265
- );
12266
- stamped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
12267
- return stamped;
12268
- }
12269
- function fileStampsEqual(a, b) {
12270
- return a.length === b.length && a.every((s, i) => fileStampEquals(s, b[i]));
12271
- }
12272
- function fileStampEquals(a, b) {
12273
- if (a === null && b === null) {
12274
- return true;
12275
- }
12276
- if (a === null || b === null) {
12277
- return false;
12278
- }
12279
- return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
12280
- }
12281
-
12282
12006
  // src/contract.ts
12283
12007
  import { Node as Node54 } from "ts-morph";
12284
12008
  import { restBinding } from "@suss/behavioral-ir";
@@ -12421,7 +12145,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
12421
12145
  }
12422
12146
  const responses = [];
12423
12147
  let method;
12424
- let path15;
12148
+ let path14;
12425
12149
  for (const prop of endpointNode.getProperties()) {
12426
12150
  if (!Node54.isPropertyAssignment(prop)) {
12427
12151
  continue;
@@ -12436,7 +12160,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
12436
12160
  if (propName === pattern.pathProperty) {
12437
12161
  const val = prop.getInitializer();
12438
12162
  if (val !== void 0 && Node54.isStringLiteral(val)) {
12439
- path15 = val.getLiteralValue();
12163
+ path14 = val.getLiteralValue();
12440
12164
  }
12441
12165
  }
12442
12166
  if (propName === pattern.responseExtraction.property) {
@@ -12460,10 +12184,10 @@ function extractEndpointContract(endpointNode, pattern, framework) {
12460
12184
  if (responses.length === 0) {
12461
12185
  return null;
12462
12186
  }
12463
- const boundaryBinding = method !== void 0 || path15 !== void 0 ? restBinding({
12187
+ const boundaryBinding = method !== void 0 || path14 !== void 0 ? restBinding({
12464
12188
  transport: "http",
12465
12189
  method: method ?? null,
12466
- path: path15 ?? null,
12190
+ path: path14 ?? null,
12467
12191
  recognition: framework
12468
12192
  }) : null;
12469
12193
  return {
@@ -13102,8 +12826,8 @@ function noteExpanded(expanded, readings, discovery) {
13102
12826
  const paths = new Set(match.registrations.map((one) => one.pathTemplate));
13103
12827
  const methods = expanded.get(reading.declarationId) ?? /* @__PURE__ */ new Set();
13104
12828
  for (const sink of reading.helper.sinks) {
13105
- const path15 = sink.arguments[0];
13106
- if (sink.method !== null && path15?.as === "text" && paths.has(path15.text)) {
12829
+ const path14 = sink.arguments[0];
12830
+ if (sink.method !== null && path14?.as === "text" && paths.has(path14.text)) {
13107
12831
  methods.add(sink.method);
13108
12832
  }
13109
12833
  }
@@ -13697,7 +13421,7 @@ function scalarOrRef(node) {
13697
13421
  }
13698
13422
 
13699
13423
  // src/moduleInit.ts
13700
- import { assembleSummary } from "@suss/extractor";
13424
+ import { assembleSummary, moduleInitStructure } from "@suss/extractor";
13701
13425
  function moduleInitName(sourceFile) {
13702
13426
  return sourceFile.getBaseName();
13703
13427
  }
@@ -13705,44 +13429,15 @@ function moduleInitSummary(sourceFile, effects, options) {
13705
13429
  if (effects.length === 0) {
13706
13430
  return null;
13707
13431
  }
13708
- const end = sourceFile.getEnd();
13709
- const raw = {
13710
- identity: {
13432
+ return assembleSummary(
13433
+ moduleInitStructure({
13711
13434
  name: moduleInitName(sourceFile),
13712
- kind: "module-init",
13713
13435
  file: sourceFile.getFilePath(),
13714
- range: { start: 0, end },
13715
- exportName: null,
13716
- exportPath: null
13717
- },
13718
- boundaryBinding: null,
13719
- parameters: [],
13720
- branches: [
13721
- {
13722
- conditions: [],
13723
- terminal: {
13724
- kind: "void",
13725
- statusCode: null,
13726
- body: null,
13727
- exceptionType: null,
13728
- message: null,
13729
- component: null,
13730
- renderTree: null,
13731
- delegateTarget: null,
13732
- emitEvent: null,
13733
- location: { start: 0, end }
13734
- },
13735
- effects: [],
13736
- extraEffects: effects,
13737
- location: { start: 0, end },
13738
- isDefault: true
13739
- }
13740
- ],
13741
- unmatchedReturns: 0,
13742
- dependencyCalls: [],
13743
- declaredContract: null
13744
- };
13745
- return assembleSummary(raw, options);
13436
+ range: { start: 0, end: sourceFile.getEnd() },
13437
+ effects
13438
+ }),
13439
+ options
13440
+ );
13746
13441
  }
13747
13442
 
13748
13443
  // src/referencedFiles.ts
@@ -13890,7 +13585,7 @@ function deriveBoundaryEffects(summaries, facts) {
13890
13585
 
13891
13586
  // src/resolve/reachableClosure.ts
13892
13587
  import { Node as Node60 } from "ts-morph";
13893
- import { functionCallBinding } from "@suss/behavioral-ir";
13588
+ import { functionCallBinding, TargetPlacements } from "@suss/behavioral-ir";
13894
13589
  import { Database as Database3, evaluate as evaluate4, lit as lit4, rule as rule4, variable as variable2 } from "@suss/datalog";
13895
13590
  import { assembleSummary as assembleSummary2 } from "@suss/extractor";
13896
13591
  var reachablePack = {
@@ -13914,6 +13609,12 @@ var reachablePack = {
13914
13609
  function nodeKey(func) {
13915
13610
  return offsetKeyOf(func);
13916
13611
  }
13612
+ function targetOf(node) {
13613
+ return {
13614
+ file: node.getSourceFile().getFilePath(),
13615
+ span: { start: node.getStart(), end: node.getEnd() }
13616
+ };
13617
+ }
13917
13618
  function resolveCallee(call, calleeName, scan) {
13918
13619
  if (!Node60.isCallExpression(call)) {
13919
13620
  return null;
@@ -13938,19 +13639,42 @@ function resolveCallee(call, calleeName, scan) {
13938
13639
  if (sources.length > 1) {
13939
13640
  return {
13940
13641
  kind: "stopped",
13941
- stop: { callee: calleeName, reason: "multipleSources" }
13642
+ stop: { callee: calleeName, reason: "multipleSources" },
13643
+ declaration: declarationToReport(declarations, calleeName, scan)
13942
13644
  };
13943
13645
  }
13944
13646
  }
13647
+ const reason = classifyStop(declarations, scan.scanning);
13648
+ const parameterIndex = reason === "callerSupplied" ? parameterIndexOf(declarations, scan.scanning) : void 0;
13945
13649
  return {
13946
13650
  kind: "stopped",
13947
- stop: {
13948
- callee: calleeName,
13949
- reason: classifyStop(declarationsBehind(symbol), scan.scanning)
13950
- }
13651
+ stop: { callee: calleeName, reason },
13652
+ declaration: declarationToReport(declarations, calleeName, scan),
13653
+ ...parameterIndex === void 0 ? {} : { parameterIndex }
13951
13654
  };
13952
13655
  }
13953
- function resolveJsxReference(node) {
13656
+ function declarationToReport(declarations, calleeName, scan) {
13657
+ const first = declarations[0];
13658
+ if (first === void 0) {
13659
+ return null;
13660
+ }
13661
+ const sources = scan.sourceDeclarationsBehind?.(first) ?? [first];
13662
+ const only = sources.length === 1 ? sources[0] : void 0;
13663
+ if (only === void 0 || only === first) {
13664
+ return first;
13665
+ }
13666
+ return resolveDecl(only, calleeName)?.func ?? only;
13667
+ }
13668
+ function declaredAtOf(outcome) {
13669
+ if (outcome.kind === "followed") {
13670
+ return targetOf(outcome.candidate.func);
13671
+ }
13672
+ return outcome.declaration === null ? null : targetOf(outcome.declaration);
13673
+ }
13674
+ function passedPositionKey(target, position) {
13675
+ return `${offsetKeyFor(target.file, target.span)}#${position}`;
13676
+ }
13677
+ function resolveJsxReference(node, scan) {
13954
13678
  if (!Node60.isJsxOpeningElement(node) && !Node60.isJsxSelfClosingElement(node)) {
13955
13679
  return null;
13956
13680
  }
@@ -13970,7 +13694,8 @@ function resolveJsxReference(node) {
13970
13694
  tag: name,
13971
13695
  outcome: {
13972
13696
  kind: "stopped",
13973
- stop: { callee: name, reason: classifyStop(declarations) }
13697
+ stop: { callee: name, reason: classifyStop(declarations) },
13698
+ declaration: declarationToReport(declarations, name, scan)
13974
13699
  }
13975
13700
  };
13976
13701
  }
@@ -13978,7 +13703,11 @@ function collectReachable(func, scan) {
13978
13703
  const inFunc = { ...scan, scanning: func };
13979
13704
  const candidates = [];
13980
13705
  const stops = [];
13706
+ const placements = new TargetPlacements();
13707
+ const parameterCalls = [];
13708
+ const passedPositions = /* @__PURE__ */ new Set();
13981
13709
  const seen = /* @__PURE__ */ new Set();
13710
+ const parameterCallsSeen = /* @__PURE__ */ new Set();
13982
13711
  const record2 = (outcome) => {
13983
13712
  if (outcome.kind === "stopped") {
13984
13713
  const stopKey = `${outcome.stop.reason}:${outcome.stop.callee}`;
@@ -13995,8 +13724,28 @@ function collectReachable(func, scan) {
13995
13724
  seen.add(key);
13996
13725
  candidates.push(outcome.candidate);
13997
13726
  };
13727
+ const recordPassedArgs = (call, calleeText, calleeOutcome) => {
13728
+ if (!Node60.isCallExpression(call)) {
13729
+ return;
13730
+ }
13731
+ const calleeTarget = declaredAtOf(calleeOutcome);
13732
+ call.getArguments().forEach((arg, position) => {
13733
+ if (!Node60.isIdentifier(arg)) {
13734
+ return;
13735
+ }
13736
+ const resolved = functionTargetOf(arg);
13737
+ if (resolved === null) {
13738
+ return;
13739
+ }
13740
+ record2({ kind: "followed", candidate: resolved });
13741
+ placements.placeArg(calleeText, position, targetOf(resolved.func));
13742
+ if (calleeTarget !== null) {
13743
+ passedPositions.add(passedPositionKey(calleeTarget, position));
13744
+ }
13745
+ });
13746
+ };
13998
13747
  func.forEachDescendant((node) => {
13999
- const jsx = resolveJsxReference(node);
13748
+ const jsx = resolveJsxReference(node, inFunc);
14000
13749
  if (jsx !== null) {
14001
13750
  record2(jsx.outcome);
14002
13751
  return;
@@ -14010,8 +13759,24 @@ function collectReachable(func, scan) {
14010
13759
  return;
14011
13760
  }
14012
13761
  record2(outcome);
13762
+ placements.place(calleeText, declaredAtOf(outcome));
13763
+ recordPassedArgs(node, calleeText, outcome);
13764
+ if (outcome.kind === "stopped" && outcome.stop.reason === "callerSupplied" && outcome.parameterIndex !== void 0 && !parameterCallsSeen.has(calleeText)) {
13765
+ parameterCallsSeen.add(calleeText);
13766
+ parameterCalls.push({
13767
+ callee: calleeText,
13768
+ parameterIndex: outcome.parameterIndex
13769
+ });
13770
+ }
14013
13771
  });
14014
- return { candidates, stops };
13772
+ return {
13773
+ candidates,
13774
+ stops,
13775
+ targets: placements.targets,
13776
+ argTargets: placements.argTargets,
13777
+ parameterCalls,
13778
+ passedPositions
13779
+ };
14015
13780
  }
14016
13781
  function normalizeCallee(text) {
14017
13782
  return text.replace(/\s+/g, " ").trim();
@@ -14086,6 +13851,10 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14086
13851
  const seedKeys = /* @__PURE__ */ new Set();
14087
13852
  const scanned = /* @__PURE__ */ new Set();
14088
13853
  const stopsByKey = /* @__PURE__ */ new Map();
13854
+ const targetsByKey = /* @__PURE__ */ new Map();
13855
+ const argTargetsByKey = /* @__PURE__ */ new Map();
13856
+ const parameterCallsByKey = /* @__PURE__ */ new Map();
13857
+ const passedPositions = /* @__PURE__ */ new Set();
14089
13858
  const summariesByKey = /* @__PURE__ */ new Map();
14090
13859
  const reachedFrom = /* @__PURE__ */ new Map();
14091
13860
  for (const seed of seeds) {
@@ -14130,9 +13899,17 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14130
13899
  const cameFrom = reachedFrom.get(key);
14131
13900
  const scan = {
14132
13901
  ...recognizers.resolveCallableSources === void 0 ? {} : { resolveCallableSources: recognizers.resolveCallableSources },
13902
+ ...recognizers.sourceDeclarationsBehind === void 0 ? {} : { sourceDeclarationsBehind: recognizers.sourceDeclarationsBehind },
14133
13903
  ...cameFrom === void 0 ? {} : { reachedFrom: cameFrom }
14134
13904
  };
14135
- const { candidates, stops } = scanWithRecording(
13905
+ const {
13906
+ candidates,
13907
+ stops,
13908
+ targets,
13909
+ argTargets,
13910
+ parameterCalls,
13911
+ passedPositions: scanPassedPositions
13912
+ } = scanWithRecording(
14136
13913
  key,
14137
13914
  facts,
14138
13915
  () => collectReachable(source.func, scan)
@@ -14140,6 +13917,14 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14140
13917
  if (stops.length > 0) {
14141
13918
  stopsByKey.set(key, stops);
14142
13919
  }
13920
+ targetsByKey.set(key, targets);
13921
+ argTargetsByKey.set(key, argTargets);
13922
+ if (parameterCalls.length > 0) {
13923
+ parameterCallsByKey.set(key, parameterCalls);
13924
+ }
13925
+ for (const position of scanPassedPositions) {
13926
+ passedPositions.add(position);
13927
+ }
14143
13928
  for (const candidate of candidates) {
14144
13929
  const calleeKey = nodeKey(candidate.func);
14145
13930
  if (!functionByKey.has(calleeKey)) {
@@ -14179,6 +13964,15 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14179
13964
  reached.push(summary);
14180
13965
  }
14181
13966
  recordStops(stopsByKey, summariesByKey, options);
13967
+ recordTargets(targetsByKey, summariesByKey);
13968
+ recordArgTargets(argTargetsByKey, summariesByKey);
13969
+ recordCalleeParameters(parameterCallsByKey, summariesByKey);
13970
+ recordParameterGaps(
13971
+ parameterCallsByKey,
13972
+ summariesByKey,
13973
+ passedPositions,
13974
+ options
13975
+ );
14182
13976
  return [...seeds, ...reached];
14183
13977
  }
14184
13978
  function scanWithRecording(key, facts, fn) {
@@ -14210,6 +14004,90 @@ function recordStops(stopsByKey, summariesByKey, options) {
14210
14004
  }
14211
14005
  }
14212
14006
  }
14007
+ function recordTargets(targetsByKey, summariesByKey) {
14008
+ for (const [key, targets] of targetsByKey) {
14009
+ for (const summary of summariesByKey.get(key) ?? []) {
14010
+ for (const transition of summary.transitions) {
14011
+ for (const effect of transition.effects) {
14012
+ if (effect.type !== "invocation") {
14013
+ continue;
14014
+ }
14015
+ const target = targets.get(normalizeCallee(effect.callee));
14016
+ if (target !== void 0) {
14017
+ effect.declaredAt = target;
14018
+ }
14019
+ }
14020
+ }
14021
+ }
14022
+ }
14023
+ }
14024
+ function recordArgTargets(argTargetsByKey, summariesByKey) {
14025
+ for (const [key, argTargets] of argTargetsByKey) {
14026
+ for (const summary of summariesByKey.get(key) ?? []) {
14027
+ for (const transition of summary.transitions) {
14028
+ for (const effect of transition.effects) {
14029
+ if (effect.type !== "invocation") {
14030
+ continue;
14031
+ }
14032
+ const byPosition = argTargets.get(normalizeCallee(effect.callee));
14033
+ if (byPosition === void 0) {
14034
+ continue;
14035
+ }
14036
+ const argsDeclaredAt = {};
14037
+ for (const [position, target] of byPosition) {
14038
+ argsDeclaredAt[position] = target;
14039
+ }
14040
+ if (Object.keys(argsDeclaredAt).length > 0) {
14041
+ effect.argsDeclaredAt = argsDeclaredAt;
14042
+ }
14043
+ }
14044
+ }
14045
+ }
14046
+ }
14047
+ }
14048
+ function recordCalleeParameters(parameterCallsByKey, summariesByKey) {
14049
+ for (const [key, parameterCalls] of parameterCallsByKey) {
14050
+ const byCallee = new Map(
14051
+ parameterCalls.map(({ callee, parameterIndex }) => [
14052
+ callee,
14053
+ parameterIndex
14054
+ ])
14055
+ );
14056
+ for (const summary of summariesByKey.get(key) ?? []) {
14057
+ for (const transition of summary.transitions) {
14058
+ for (const effect of transition.effects) {
14059
+ if (effect.type !== "invocation") {
14060
+ continue;
14061
+ }
14062
+ const parameterIndex = byCallee.get(normalizeCallee(effect.callee));
14063
+ if (parameterIndex !== void 0) {
14064
+ effect.calleeParameter = parameterIndex;
14065
+ }
14066
+ }
14067
+ }
14068
+ }
14069
+ }
14070
+ }
14071
+ function recordParameterGaps(parameterCallsByKey, summariesByKey, passedPositions, options) {
14072
+ if (options?.gapHandling === "silent") {
14073
+ return;
14074
+ }
14075
+ for (const [key, parameterCalls] of parameterCallsByKey) {
14076
+ const unbound = parameterCalls.filter(
14077
+ ({ parameterIndex }) => !passedPositions.has(`${key}#${parameterIndex}`)
14078
+ );
14079
+ if (unbound.length === 0) {
14080
+ continue;
14081
+ }
14082
+ for (const summary of summariesByKey.get(key) ?? []) {
14083
+ summary.gaps.push(
14084
+ ...unbound.map(
14085
+ ({ callee }) => unfollowedCallGap({ callee, reason: "unboundParameter" })
14086
+ )
14087
+ );
14088
+ }
14089
+ }
14090
+ }
14213
14091
 
14214
14092
  // src/resolve/rethrowEnrichment.ts
14215
14093
  import { Node as Node61 } from "ts-morph";
@@ -14945,10 +14823,10 @@ function readArrayLiteralText(node) {
14945
14823
  }
14946
14824
 
14947
14825
  // src/summaryIdentity.ts
14948
- import fs6 from "fs";
14949
14826
  import path8 from "path";
14950
14827
  import {
14951
14828
  disambiguateSummaryIds,
14829
+ linkCallsToSummaries,
14952
14830
  summaryIdFromParts
14953
14831
  } from "@suss/behavioral-ir";
14954
14832
  function workspaceRootFor(anchor) {
@@ -14965,32 +14843,6 @@ function workspaceNameFor(root) {
14965
14843
  const folder = path8.basename(path8.resolve(root));
14966
14844
  return folder.length > 0 ? folder : null;
14967
14845
  }
14968
- function namedPackageDirAbove(start) {
14969
- let at = path8.resolve(start);
14970
- for (let up = 0; up < 12; up += 1) {
14971
- if (packageNameAt(at) !== null) {
14972
- return at;
14973
- }
14974
- const parent = path8.dirname(at);
14975
- if (parent === at) {
14976
- break;
14977
- }
14978
- at = parent;
14979
- }
14980
- return null;
14981
- }
14982
- function packageNameAt(dir) {
14983
- try {
14984
- const manifest = JSON.parse(
14985
- fs6.readFileSync(path8.join(dir, "package.json"), "utf8")
14986
- );
14987
- if (typeof manifest.name === "string" && manifest.name.length > 0) {
14988
- return manifest.name;
14989
- }
14990
- } catch {
14991
- }
14992
- return null;
14993
- }
14994
14846
  function nameSummaries(summaries, args) {
14995
14847
  for (const summary of summaries) {
14996
14848
  const file = relativeFile(summary.location.file, args.projectRoot);
@@ -15000,7 +14852,7 @@ function nameSummaries(summaries, args) {
15000
14852
  summary.identity.id = idFor(args.workspace, file, summary);
15001
14853
  }
15002
14854
  disambiguateSummaryIds(summaries);
15003
- nameWhatEachCallReaches(summaries);
14855
+ linkCallsToSummaries(summaries);
15004
14856
  }
15005
14857
  function relativeFile(file, projectRoot) {
15006
14858
  if (projectRoot === void 0 || !path8.isAbsolute(file)) {
@@ -15017,185 +14869,35 @@ function idFor(workspace, file, summary) {
15017
14869
  exportPath: summary.identity.exportPath
15018
14870
  });
15019
14871
  }
15020
- function nameWhatEachCallReaches(summaries) {
15021
- const byName = /* @__PURE__ */ new Map();
15022
- const byFileAndName = /* @__PURE__ */ new Map();
15023
- const remember = (index, key, summary) => {
15024
- const found = index.get(key);
15025
- if (found === void 0) {
15026
- index.set(key, [summary]);
15027
- return;
15028
- }
15029
- found.push(summary);
15030
- };
15031
- for (const summary of summaries) {
15032
- if (summary.identity.nameKind === "label") {
15033
- continue;
15034
- }
15035
- remember(byName, summary.identity.name, summary);
15036
- remember(
15037
- byFileAndName,
15038
- `${summary.location.file}::${summary.identity.name}`,
15039
- summary
15040
- );
15041
- }
15042
- const onlyAnswer = (index, key) => {
15043
- const found = index.get(key) ?? [];
15044
- return found.length === 1 ? found[0] : null;
15045
- };
15046
- for (const summary of summaries) {
15047
- for (const transition of summary.transitions) {
15048
- for (const effect of transition.effects) {
15049
- if (effect.type !== "invocation") {
15050
- continue;
15051
- }
15052
- const called = effect.callee.split(".").pop() ?? effect.callee;
15053
- const reached = onlyAnswer(byFileAndName, `${summary.location.file}::${called}`) ?? onlyAnswer(byName, called);
15054
- if (reached?.identity.id !== void 0) {
15055
- effect.summary = reached.identity.id;
15056
- }
15057
- }
15058
- }
15059
- }
15060
- }
15061
14872
 
15062
14873
  // src/timing.ts
15063
- function createTimer() {
15064
- const phases = /* @__PURE__ */ new Map();
15065
- const start = performance.now();
15066
- function record2(label, deltaMs) {
15067
- const existing = phases.get(label);
15068
- if (existing === void 0) {
15069
- phases.set(label, { durationMs: deltaMs, calls: 1 });
15070
- } else {
15071
- existing.durationMs += deltaMs;
15072
- existing.calls += 1;
15073
- }
15074
- }
15075
- return {
15076
- time(label, fn) {
15077
- const t0 = performance.now();
15078
- try {
15079
- return fn();
15080
- } finally {
15081
- record2(label, performance.now() - t0);
15082
- }
15083
- },
15084
- async timeAsync(label, fn) {
15085
- const t0 = performance.now();
15086
- try {
15087
- return await fn();
15088
- } finally {
15089
- record2(label, performance.now() - t0);
15090
- }
15091
- },
15092
- report() {
15093
- const totalMs = performance.now() - start;
15094
- const ordered = [...phases.entries()].map(([label, s]) => ({
15095
- label,
15096
- durationMs: s.durationMs,
15097
- calls: s.calls
15098
- })).sort((a, b) => b.durationMs - a.durationMs);
15099
- return { totalMs, phases: ordered };
15100
- }
15101
- };
15102
- }
14874
+ import { createTimer, noopTimer } from "@suss/extractor";
15103
14875
 
15104
14876
  // src/version.ts
15105
- import { createHash as createHash3 } from "crypto";
15106
- import fs7 from "fs";
15107
- import { createRequire } from "module";
15108
- import path9 from "path";
15109
- import { fileURLToPath } from "url";
14877
+ import { createAdapterStamp } from "@suss/extractor";
14878
+ import {
14879
+ computeContentHash,
14880
+ computeDistHashFrom,
14881
+ projectFileStamp
14882
+ } from "@suss/extractor";
15110
14883
  var ADAPTER_VERSION = "0.2.1";
15111
- var ANALYSIS_PACKAGES = [
15112
- "@suss/extractor",
15113
- "@suss/resolution",
15114
- "@suss/datalog",
15115
- "@suss/behavioral-ir"
15116
- ];
15117
- var SOURCE_STAMP = { kind: "source" };
15118
- var cachedCodeStamp = null;
15119
- function adapterCodeStamp() {
15120
- if (cachedCodeStamp !== null) {
15121
- return cachedCodeStamp;
15122
- }
15123
- cachedCodeStamp = readAdapterCodeStamp();
15124
- return cachedCodeStamp;
15125
- }
15126
- function readAdapterCodeStamp() {
15127
- try {
15128
- const selfPath = fileURLToPath(import.meta.url);
15129
- const hash = computeDistHashFrom(path9.dirname(selfPath));
15130
- return hash.length > 0 ? { kind: "bundle", hash } : SOURCE_STAMP;
15131
- } catch {
15132
- return SOURCE_STAMP;
15133
- }
15134
- }
15135
- function computeDistHashFrom(dir) {
15136
- const candidates = [path9.join(dir, "index.js"), path9.join(dir, "index.cjs")];
15137
- for (const candidate of candidates) {
15138
- if (!fs7.existsSync(candidate)) {
15139
- continue;
15140
- }
15141
- return computeContentHash([candidate, ...analysisBundles()]);
15142
- }
15143
- return "";
15144
- }
15145
- function computeContentHash(paths) {
15146
- if (paths.length === 0) {
15147
- return "";
15148
- }
15149
- const hash = createHash3("sha256");
15150
- for (const file of paths) {
15151
- try {
15152
- hash.update(fs7.readFileSync(file));
15153
- } catch {
15154
- return "";
15155
- }
15156
- }
15157
- return hash.digest("hex").slice(0, 16);
15158
- }
15159
- function projectFileStamp(paths) {
15160
- if (paths.length === 0) {
15161
- return "none";
15162
- }
15163
- const hash = createHash3("sha256");
15164
- for (const file of [...paths].sort()) {
15165
- hash.update(file);
15166
- hash.update("\0");
15167
- try {
15168
- hash.update(fs7.readFileSync(file));
15169
- } catch {
15170
- hash.update("absent");
15171
- }
15172
- }
15173
- return hash.digest("hex").slice(0, 16);
15174
- }
15175
- function analysisBundles() {
15176
- const require2 = createRequire(import.meta.url);
15177
- const found = [];
15178
- for (const name of ANALYSIS_PACKAGES) {
15179
- try {
15180
- found.push(require2.resolve(name));
15181
- } catch {
15182
- }
15183
- }
15184
- return found;
15185
- }
14884
+ var stamp = createAdapterStamp({
14885
+ moduleUrl: import.meta.url,
14886
+ version: ADAPTER_VERSION
14887
+ });
15186
14888
  function computeAdapterPacksDigest(packVersions) {
15187
- const sortedPacks = [...packVersions].map((p) => `${p.name}@${p.version ?? "unset"}`).sort();
15188
- const stamp = adapterCodeStamp();
15189
- const adapterStamp = stamp.kind === "bundle" ? `adapter@${ADAPTER_VERSION}+${stamp.hash}` : `adapter@${ADAPTER_VERSION}+source`;
15190
- return `${adapterStamp}|${sortedPacks.join(",")}`;
14889
+ return stamp.packsDigest(packVersions);
14890
+ }
14891
+ function declineWhenRunFromSource(cacheDir) {
14892
+ return stamp.declineWhenRunFromSource(cacheDir);
15191
14893
  }
15192
14894
 
15193
14895
  // src/workspacePatterns.ts
15194
- import fs8 from "fs";
15195
- import path10 from "path";
14896
+ import fs4 from "fs";
14897
+ import path9 from "path";
15196
14898
  function readJson(filePath) {
15197
14899
  try {
15198
- return JSON.parse(fs8.readFileSync(filePath, "utf8"));
14900
+ return JSON.parse(fs4.readFileSync(filePath, "utf8"));
15199
14901
  } catch {
15200
14902
  return void 0;
15201
14903
  }
@@ -15222,7 +14924,7 @@ function pnpmWorkspaceGlobs(yamlText) {
15222
14924
  return globs;
15223
14925
  }
15224
14926
  function workspaceGlobsAt(dir) {
15225
- const pkg = readJson(path10.join(dir, "package.json"));
14927
+ const pkg = readJson(path9.join(dir, "package.json"));
15226
14928
  if (typeof pkg === "object" && pkg !== null && "workspaces" in pkg) {
15227
14929
  const ws = pkg.workspaces;
15228
14930
  if (Array.isArray(ws)) {
@@ -15234,10 +14936,10 @@ function workspaceGlobsAt(dir) {
15234
14936
  );
15235
14937
  }
15236
14938
  }
15237
- const pnpmPath = path10.join(dir, "pnpm-workspace.yaml");
15238
- if (fs8.existsSync(pnpmPath)) {
14939
+ const pnpmPath = path9.join(dir, "pnpm-workspace.yaml");
14940
+ if (fs4.existsSync(pnpmPath)) {
15239
14941
  try {
15240
- return pnpmWorkspaceGlobs(fs8.readFileSync(pnpmPath, "utf8"));
14942
+ return pnpmWorkspaceGlobs(fs4.readFileSync(pnpmPath, "utf8"));
15241
14943
  } catch {
15242
14944
  return null;
15243
14945
  }
@@ -15246,14 +14948,14 @@ function workspaceGlobsAt(dir) {
15246
14948
  }
15247
14949
  function isDirectory(candidate) {
15248
14950
  try {
15249
- return fs8.statSync(candidate).isDirectory();
14951
+ return fs4.statSync(candidate).isDirectory();
15250
14952
  } catch {
15251
14953
  return false;
15252
14954
  }
15253
14955
  }
15254
14956
  function subdirectoriesOf(dir) {
15255
14957
  try {
15256
- return fs8.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && name !== "node_modules");
14958
+ return fs4.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && name !== "node_modules");
15257
14959
  } catch {
15258
14960
  return [];
15259
14961
  }
@@ -15271,13 +14973,13 @@ function directoriesMatchingGlob(root, glob) {
15271
14973
  for (const dir of current2) {
15272
14974
  for (const entry of subdirectoriesOf(dir)) {
15273
14975
  if (pattern.test(entry)) {
15274
- next.push(path10.join(dir, entry));
14976
+ next.push(path9.join(dir, entry));
15275
14977
  }
15276
14978
  }
15277
14979
  }
15278
14980
  } else {
15279
14981
  for (const dir of current2) {
15280
- next.push(path10.join(dir, segment));
14982
+ next.push(path9.join(dir, segment));
15281
14983
  }
15282
14984
  }
15283
14985
  current2 = next.filter(isDirectory);
@@ -15285,7 +14987,7 @@ function directoriesMatchingGlob(root, glob) {
15285
14987
  return current2;
15286
14988
  }
15287
14989
  function packageAt(dir) {
15288
- const packageJsonPath = path10.join(dir, "package.json");
14990
+ const packageJsonPath = path9.join(dir, "package.json");
15289
14991
  const pkg = readJson(packageJsonPath);
15290
14992
  if (typeof pkg === "object" && pkg !== null && typeof pkg.name === "string") {
15291
14993
  return { name: pkg.name, packageJsonPath };
@@ -15293,7 +14995,7 @@ function packageAt(dir) {
15293
14995
  return null;
15294
14996
  }
15295
14997
  function workspacePackagesFrom(anchor) {
15296
- let dir = path10.resolve(anchor);
14998
+ let dir = path9.resolve(anchor);
15297
14999
  for (; ; ) {
15298
15000
  const globs = workspaceGlobsAt(dir);
15299
15001
  if (globs !== null) {
@@ -15313,7 +15015,7 @@ function workspacePackagesFrom(anchor) {
15313
15015
  (a, b) => a.packageJsonPath.localeCompare(b.packageJsonPath)
15314
15016
  );
15315
15017
  }
15316
- const parent = path10.dirname(dir);
15018
+ const parent = path9.dirname(dir);
15317
15019
  if (parent === dir) {
15318
15020
  return [];
15319
15021
  }
@@ -15855,11 +15557,11 @@ function extractConsumerBinding(unit, pattern, pack, resolution) {
15855
15557
  return null;
15856
15558
  }
15857
15559
  const method = extractBindingMethod(binding, callSite, pack, resolution);
15858
- const path15 = extractBindingPath(binding, callSite, pack, resolution);
15560
+ const path14 = extractBindingPath(binding, callSite, pack, resolution);
15859
15561
  return restBinding2({
15860
15562
  transport: pack.protocol,
15861
15563
  method: method ?? null,
15862
- path: path15 ?? null,
15564
+ path: path14 ?? null,
15863
15565
  recognition: pack.name
15864
15566
  });
15865
15567
  }
@@ -16335,24 +16037,6 @@ function stampWrappers(raw, unit, wrappers) {
16335
16037
  recordFileDependency(wrapper.file);
16336
16038
  }
16337
16039
  }
16338
- function stampModuleImports(summaries, project) {
16339
- const byFile2 = /* @__PURE__ */ new Map();
16340
- for (const summary of summaries) {
16341
- const file = summary.location.file;
16342
- let imports = byFile2.get(file);
16343
- if (imports === void 0) {
16344
- imports = internalImportsOf(project, file);
16345
- byFile2.set(file, imports);
16346
- }
16347
- if (imports.length === 0) {
16348
- continue;
16349
- }
16350
- summary.metadata = {
16351
- ...summary.metadata ?? {},
16352
- moduleImports: imports
16353
- };
16354
- }
16355
- }
16356
16040
  function emitLibraryEnvReadMarkers(summaries, project, frameworks) {
16357
16041
  for (const pack of frameworks) {
16358
16042
  for (const declared of pack.libraryEnvVars ?? []) {
@@ -16527,8 +16211,8 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
16527
16211
  if (pathArg === void 0) {
16528
16212
  continue;
16529
16213
  }
16530
- const path15 = pathFromArgument(pathArg, resolution);
16531
- if (path15 === void 0) {
16214
+ const path14 = pathFromArgument(pathArg, resolution);
16215
+ if (path14 === void 0) {
16532
16216
  continue;
16533
16217
  }
16534
16218
  const callerFunc = enclosingFunction(callExpr);
@@ -16540,7 +16224,7 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
16540
16224
  continue;
16541
16225
  }
16542
16226
  seen.add(dedupKey);
16543
- out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path15, options));
16227
+ out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path14, options));
16544
16228
  }
16545
16229
  return out;
16546
16230
  }
@@ -16597,7 +16281,7 @@ function callerName(func) {
16597
16281
  }
16598
16282
  return "anonymous";
16599
16283
  }
16600
- function buildCallerSummary(wrapper, callerFunc, callExpr, path15, options) {
16284
+ function buildCallerSummary(wrapper, callerFunc, callExpr, path14, options) {
16601
16285
  const wrapperBinding = wrapper.summary.identity.boundaryBinding;
16602
16286
  const wrapperRest = wrapperBinding?.semantics.name === "rest" ? wrapperBinding.semantics : null;
16603
16287
  const syntheticPack = {
@@ -16629,7 +16313,7 @@ function buildCallerSummary(wrapper, callerFunc, callExpr, path15, options) {
16629
16313
  raw.boundaryBinding = restBinding2({
16630
16314
  transport: wrapperBinding?.transport ?? "http",
16631
16315
  method: wrapperRest?.method ?? null,
16632
- path: path15,
16316
+ path: path14,
16633
16317
  recognition: wrapperBinding?.recognition ?? "unknown"
16634
16318
  });
16635
16319
  const summary = assembleSummary3(raw, options);
@@ -16651,25 +16335,12 @@ function named(summaries, workspace, runRoot) {
16651
16335
  });
16652
16336
  return summaries;
16653
16337
  }
16654
- var saidWhyNoCache = false;
16655
16338
  function extractionConfigStamp(config) {
16656
16339
  return [
16657
16340
  `includeReachable=${config.includeReachable !== false}`,
16658
16341
  `gapHandling=${config.extractorOptions?.gapHandling ?? "default"}`
16659
16342
  ].join(",");
16660
16343
  }
16661
- function declineWhenRunFromSource(cacheDir) {
16662
- if (cacheDir === null || adapterCodeStamp().kind === "bundle") {
16663
- return cacheDir;
16664
- }
16665
- if (!saidWhyNoCache) {
16666
- saidWhyNoCache = true;
16667
- process.stderr.write(
16668
- "[suss] extraction cache off: this process loaded the adapter from source, where nothing can tell one build of it from another. Run the built adapter to cache.\n"
16669
- );
16670
- }
16671
- return null;
16672
- }
16673
16344
  function loadRunFiles(project, candidatePaths, timer) {
16674
16345
  if (candidatePaths !== null) {
16675
16346
  const deep2 = timer.time(
@@ -16700,7 +16371,7 @@ function createTypeScriptAdapter(suppliedConfig) {
16700
16371
  ...suppliedConfig,
16701
16372
  frameworks: expandWorkspacePatterns(
16702
16373
  suppliedConfig.frameworks,
16703
- suppliedConfig.projectRoot ?? (suppliedConfig.tsConfigFilePath !== void 0 ? path11.dirname(suppliedConfig.tsConfigFilePath) : void 0)
16374
+ suppliedConfig.projectRoot ?? (suppliedConfig.tsConfigFilePath !== void 0 ? path10.dirname(suppliedConfig.tsConfigFilePath) : void 0)
16704
16375
  )
16705
16376
  };
16706
16377
  const project = config.project ?? new Project(
@@ -16709,11 +16380,11 @@ function createTypeScriptAdapter(suppliedConfig) {
16709
16380
  skipAddingFilesFromTsConfig: true
16710
16381
  } : { skipAddingFilesFromTsConfig: true }
16711
16382
  );
16712
- const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(path11.dirname(config.tsConfigFilePath)) : void 0);
16383
+ const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(path10.dirname(config.tsConfigFilePath)) : void 0);
16713
16384
  let lazyBootstrapped = false;
16714
16385
  let projectFileSet;
16715
16386
  const cacheDir = declineWhenRunFromSource(
16716
- config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? path11.join(path11.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
16387
+ config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? path10.join(path10.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
16717
16388
  );
16718
16389
  const cache = createCacheLayer(cacheDir);
16719
16390
  const packsDigest = `${computeAdapterPacksDigest(
@@ -16721,12 +16392,6 @@ function createTypeScriptAdapter(suppliedConfig) {
16721
16392
  (p) => p.version !== void 0 ? { name: p.name, version: p.version } : { name: p.name }
16722
16393
  )
16723
16394
  )}|${extractionConfigStamp(config)}|ws:${workspaceExpansionStamp(config.frameworks)}`;
16724
- const digestFor = (files) => {
16725
- const inputs = config.frameworks.flatMap(
16726
- (pack) => pack.discoveryInputs?.(files) ?? []
16727
- );
16728
- return `${packsDigest}|reads:${projectFileStamp(inputs)}`;
16729
- };
16730
16395
  const packWrappers = config.frameworks.flatMap(
16731
16396
  (pack) => pack.transparentWrappers ?? []
16732
16397
  );
@@ -16770,20 +16435,17 @@ function createTypeScriptAdapter(suppliedConfig) {
16770
16435
  config.tsConfigFilePath ?? raise("lazy bootstrap requires tsConfigFilePath")
16771
16436
  )
16772
16437
  ) : null;
16773
- const adapterPacksDigest = cacheDir === null ? packsDigest : timer.time(
16774
- "cache.digest",
16775
- () => digestFor(
16776
- tsconfigFileList ?? project.getSourceFiles().map((sf) => sf.getFilePath())
16777
- )
16778
- );
16779
- const cacheInput = tsconfigFileList !== null ? {
16780
- files: tsconfigFileList,
16781
- adapterPacksDigest,
16782
- tsconfigPath: config.tsConfigFilePath ?? raise("lazy bootstrap requires tsConfigFilePath")
16783
- } : {
16784
- project,
16438
+ let cacheFiles = null;
16439
+ const adapterPacksDigest = cacheDir === null ? packsDigest : timer.time("cache.digest", () => {
16440
+ cacheFiles = tsconfigFileList ?? project.getSourceFiles().map((sf) => sf.getFilePath());
16441
+ return runDigest(packsDigest, config.frameworks, cacheFiles);
16442
+ });
16443
+ const cacheInput = {
16444
+ // Empty when cacheDir is null: a no-op cache layer never reads
16445
+ // the file list, so there is nothing worth resolving early.
16446
+ files: cacheFiles ?? [],
16785
16447
  adapterPacksDigest,
16786
- ...config.tsConfigFilePath !== void 0 ? { tsconfigPath: config.tsConfigFilePath } : {}
16448
+ ...config.tsConfigFilePath !== void 0 ? { configPath: config.tsConfigFilePath } : {}
16787
16449
  };
16788
16450
  const lookup = await timer.timeAsync(
16789
16451
  "cache.lookup",
@@ -16989,7 +16651,8 @@ function createTypeScriptAdapter(suppliedConfig) {
16989
16651
  ],
16990
16652
  access: collectAccessRecognizers(config.frameworks, tallies),
16991
16653
  resolveWrittenValue: (value) => resolution.resolveWrittenValue(value),
16992
- resolveCallableSources: (value, alsoFrom) => resolution.resolveCallableSources(value, alsoFrom)
16654
+ resolveCallableSources: (value, alsoFrom) => resolution.resolveCallableSources(value, alsoFrom),
16655
+ sourceDeclarationsBehind: (declaration) => sourceDeclarationsBehind(declaration, resolution)
16993
16656
  },
16994
16657
  // Reached units the cache already serves emit nothing,
16995
16658
  // the way a cold run's seeds do not.
@@ -17026,7 +16689,10 @@ function createTypeScriptAdapter(suppliedConfig) {
17026
16689
  }
17027
16690
  timer.time(
17028
16691
  "stampModuleImports",
17029
- () => stampModuleImports(enriched, project)
16692
+ () => stampModuleImports(enriched, (file) => {
16693
+ const imports = internalImportsOf(project, file);
16694
+ return imports.length === 0 ? void 0 : imports;
16695
+ })
17030
16696
  );
17031
16697
  timer.time(
17032
16698
  "emitLibraryEnvReadMarkers",
@@ -17117,7 +16783,7 @@ function createTypeScriptAdapter(suppliedConfig) {
17117
16783
  };
17118
16784
  }
17119
16785
  function mountAssumptionsAgree(record2, index) {
17120
- return Object.entries(record2.mountPrefixes).every(
16786
+ return Object.entries(record2.meta.mountPrefixes).every(
17121
16787
  ([childId, prefix]) => (index.prefixForId?.(childId) ?? "") === prefix
17122
16788
  );
17123
16789
  }
@@ -17277,7 +16943,7 @@ function buildCacheAttribution(args) {
17277
16943
  cacheable,
17278
16944
  deps: [...deps].sort(),
17279
16945
  claims: sink.claims,
17280
- mountPrefixes: Object.fromEntries(sink.mountPrefixes),
16946
+ meta: { mountPrefixes: Object.fromEntries(sink.mountPrefixes) },
17281
16947
  packs: packNamesByPath.get(rootPath) ?? []
17282
16948
  });
17283
16949
  }
@@ -17422,8 +17088,8 @@ function buildSubUnitSummary(subUnit, parent, invocationRecognizers, accessRecog
17422
17088
  }
17423
17089
 
17424
17090
  // src/bootstrap/noTsconfigProject.ts
17425
- import fs9 from "fs";
17426
- import path12 from "path";
17091
+ import fs5 from "fs";
17092
+ import path11 from "path";
17427
17093
  import {
17428
17094
  ModuleKind,
17429
17095
  ModuleResolutionKind,
@@ -17459,7 +17125,7 @@ function findSourceFiles(rootDir, depth = 0) {
17459
17125
  const found = [];
17460
17126
  let entries;
17461
17127
  try {
17462
- entries = fs9.readdirSync(rootDir, { withFileTypes: true });
17128
+ entries = fs5.readdirSync(rootDir, { withFileTypes: true });
17463
17129
  } catch {
17464
17130
  return [];
17465
17131
  }
@@ -17467,10 +17133,10 @@ function findSourceFiles(rootDir, depth = 0) {
17467
17133
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name)) {
17468
17134
  continue;
17469
17135
  }
17470
- const full = path12.join(rootDir, entry.name);
17136
+ const full = path11.join(rootDir, entry.name);
17471
17137
  if (entry.isDirectory()) {
17472
17138
  found.push(...findSourceFiles(full, depth + 1));
17473
- } else if (SOURCE_EXTENSIONS.has(path12.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
17139
+ } else if (SOURCE_EXTENSIONS.has(path11.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
17474
17140
  found.push(full);
17475
17141
  }
17476
17142
  }
@@ -17505,15 +17171,15 @@ function createProjectWithoutTsconfig(rootDir) {
17505
17171
  return { project, files };
17506
17172
  }
17507
17173
  function findNearestTsconfig(startDir) {
17508
- let dir = path12.resolve(startDir);
17174
+ let dir = path11.resolve(startDir);
17509
17175
  while (true) {
17510
17176
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
17511
- const candidate = path12.join(dir, name);
17512
- if (fs9.existsSync(candidate)) {
17177
+ const candidate = path11.join(dir, name);
17178
+ if (fs5.existsSync(candidate)) {
17513
17179
  return candidate;
17514
17180
  }
17515
17181
  }
17516
- const parent = path12.dirname(dir);
17182
+ const parent = path11.dirname(dir);
17517
17183
  if (parent === dir) {
17518
17184
  return null;
17519
17185
  }
@@ -17522,7 +17188,7 @@ function findNearestTsconfig(startDir) {
17522
17188
  }
17523
17189
 
17524
17190
  // src/facts/why.ts
17525
- import path13 from "path";
17191
+ import path12 from "path";
17526
17192
  import { Node as Node65, Project as Project3, SyntaxKind as SyntaxKind12 } from "ts-morph";
17527
17193
  import { explainResolutionProof, renderExplanation } from "@suss/resolution";
17528
17194
  var JS_PHRASES = {
@@ -17530,12 +17196,12 @@ var JS_PHRASES = {
17530
17196
  reason: `${describe(tuple[0])} is a .bind of ${describe(tuple[1])}, which runs the same function`
17531
17197
  })
17532
17198
  };
17533
- var WhySession = class {
17199
+ var TypeScriptWhySession = class {
17534
17200
  project;
17535
17201
  store;
17536
17202
  root;
17537
17203
  constructor(options) {
17538
- this.root = path13.resolve(options.dir);
17204
+ this.root = path12.resolve(options.dir);
17539
17205
  const tsconfig = findNearestTsconfig(this.root);
17540
17206
  this.project = tsconfig === null ? createProjectWithoutTsconfig(this.root).project : new Project3({ tsConfigFilePath: tsconfig });
17541
17207
  this.store = new ResolutionStore();
@@ -17624,7 +17290,7 @@ var WhySession = class {
17624
17290
  };
17625
17291
  }
17626
17292
  sourceFileAt(file) {
17627
- const absolute = path13.resolve(this.root, file);
17293
+ const absolute = path12.resolve(this.root, file);
17628
17294
  const loaded = this.project.getSourceFile(absolute);
17629
17295
  if (loaded !== void 0) {
17630
17296
  return loaded;
@@ -17638,7 +17304,7 @@ var WhySession = class {
17638
17304
  /** A module key: a file path said relative to the root, or a package name. */
17639
17305
  displayPath(key) {
17640
17306
  if (key.startsWith(this.root)) {
17641
- return path13.relative(this.root, key);
17307
+ return path12.relative(this.root, key);
17642
17308
  }
17643
17309
  return key;
17644
17310
  }
@@ -17666,228 +17332,8 @@ function displayNameOf(node) {
17666
17332
  return text.length > 40 ? `${text.slice(0, 40)}...` : text;
17667
17333
  }
17668
17334
 
17669
- // src/packHealth.ts
17670
- function stagesOf(funnel) {
17671
- const stages = [];
17672
- const gateSaysSomething = funnel.gates.length > 0 && funnel.unresolvedGates.length === 0;
17673
- if (funnel.recognizes && !funnel.discovers && gateSaysSomething) {
17674
- stages.push({
17675
- from: { count: funnel.unitsInGatedFiles, name: "unit bodies" },
17676
- to: { count: funnel.effectsRecognized, name: "effects" }
17677
- });
17678
- }
17679
- if (funnel.discovers && gateSaysSomething) {
17680
- stages.push({
17681
- from: { count: funnel.candidateFiles, name: "source files" },
17682
- to: { count: funnel.unitsDiscovered, name: "units" }
17683
- });
17684
- }
17685
- stages.push(
17686
- {
17687
- from: { count: funnel.unitsClaimed, name: "units" },
17688
- to: { count: funnel.summariesBound, name: "summaries" }
17689
- },
17690
- {
17691
- from: { count: funnel.summariesBound, name: "summaries" },
17692
- to: { count: funnel.summariesWithBehavior, name: "transitions" }
17693
- }
17694
- );
17695
- return stages;
17696
- }
17697
- function funnelDrops(packs) {
17698
- const violations = [];
17699
- for (const funnel of packs) {
17700
- for (const stage of stagesOf(funnel)) {
17701
- if (stage.from.count === 0 || stage.to.count > 0) {
17702
- continue;
17703
- }
17704
- violations.push({
17705
- label: funnel.pack,
17706
- detail: `${stage.from.count} ${stage.from.name} -> 0 ${stage.to.name}`
17707
- });
17708
- }
17709
- }
17710
- return violations;
17711
- }
17712
- function recognizersWithNoUnits(packs) {
17713
- return packs.filter(
17714
- (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
17715
- // roots, so effects recognized there mean the run worked and
17716
- // the missing framework pack costs attribution, not existence.
17717
- funnel.effectsRecognized === 0
17718
- ).map((funnel) => ({
17719
- label: funnel.pack,
17720
- detail: `${funnel.candidateFiles} gated files, and no pack in this run discovered a unit in them. ${funnel.pack} reads calls inside units another pack finds, so add the pack that finds this project's handlers (-f express, -f fastify, ...), or run suss init to work out which.`
17721
- }));
17722
- }
17723
- function helpersThatMatchedNothing(packs) {
17724
- return packs.flatMap(
17725
- (funnel) => funnel.helpersUnmatched.map((helper) => ({
17726
- label: funnel.pack,
17727
- detail: `${helper} matched no call in this run, so whatever it registers is missing. suss read that helper out of the project and then failed to match a call to it, which is a bug in suss rather than in your code.`
17728
- }))
17729
- );
17730
- }
17731
- function unversionedPacks(packs) {
17732
- return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
17733
- label: funnel.pack,
17734
- detail: "no version declared"
17735
- }));
17736
- }
17737
- function selfCollisions(packs) {
17738
- return packs.filter((funnel) => funnel.selfCollisions > 0).map((funnel) => ({
17739
- label: funnel.pack,
17740
- detail: `${funnel.selfCollisions} ${funnel.selfCollisions === 1 ? "unit" : "units"} matched twice, second dropped`
17741
- }));
17742
- }
17743
- function threwWhileReading(packs) {
17744
- return packs.filter((funnel) => funnel.failures.length > 0).map((funnel) => {
17745
- const first = funnel.failures[0];
17746
- const rest = funnel.failures.length - 1;
17747
- const alsoIn = rest > 0 ? ` (+${rest} more)` : "";
17748
- return {
17749
- label: funnel.pack,
17750
- detail: `${first.hook} on ${first.file}${alsoIn}: ${first.message}`
17751
- };
17752
- });
17753
- }
17754
- function packGradients(report) {
17755
- const gradients = [];
17756
- for (const funnel of report.packs) {
17757
- if (funnel.declarations === null) {
17758
- continue;
17759
- }
17760
- gradients.push(gradientOf(funnel.pack, funnel.declarations.declarations));
17761
- }
17762
- return gradients;
17763
- }
17764
- function gradientOf(pack, declarations) {
17765
- const gradient = {
17766
- pack,
17767
- dataLinks: 0,
17768
- functionLinks: [],
17769
- astLinks: [],
17770
- withoutExample: []
17771
- };
17772
- for (const declaration of declarations) {
17773
- gradient.dataLinks += declaration.dataLinks;
17774
- for (const question of declaration.functionLinks) {
17775
- gradient.functionLinks.push(`${declaration.name}.${question}`);
17776
- }
17777
- for (const question of declaration.astLinks) {
17778
- gradient.astLinks.push(`${declaration.name}.${question}`);
17779
- }
17780
- if (declaration.example === null) {
17781
- gradient.withoutExample.push(declaration.name);
17782
- }
17783
- }
17784
- return gradient;
17785
- }
17786
- function opaqueLinks(gradients) {
17787
- return gradients.filter((gradient) => gradient.functionLinks.length > 0).map((gradient) => ({
17788
- label: gradient.pack,
17789
- detail: `${gradient.functionLinks.join(", ")} (${gradient.dataLinks} data)`
17790
- }));
17791
- }
17792
- function reachesTheSyntaxTree(gradients) {
17793
- return gradients.filter((gradient) => gradient.astLinks.length > 0).map((gradient) => ({
17794
- label: gradient.pack,
17795
- detail: gradient.astLinks.join(", ")
17796
- }));
17797
- }
17798
- function undocumentedDeclarations(gradients) {
17799
- return gradients.filter((gradient) => gradient.withoutExample.length > 0).map((gradient) => ({
17800
- label: gradient.pack,
17801
- detail: gradient.withoutExample.join(", ")
17802
- }));
17803
- }
17804
- function evaluatePackHealth(report) {
17805
- const gradients = packGradients(report);
17806
- return [
17807
- {
17808
- name: "no pack throws while it reads",
17809
- code: "threw",
17810
- audience: "run",
17811
- violations: threwWhileReading(report.packs)
17812
- },
17813
- {
17814
- name: "no pack finds something and records nothing",
17815
- code: "no-output",
17816
- audience: "run",
17817
- violations: funnelDrops(report.packs)
17818
- },
17819
- {
17820
- name: "no pack collides with itself",
17821
- code: "double-match",
17822
- audience: "run",
17823
- violations: selfCollisions(report.packs)
17824
- },
17825
- {
17826
- name: "every recognizer had units to look inside",
17827
- code: "no-units",
17828
- audience: "run",
17829
- violations: recognizersWithNoUnits(report.packs)
17830
- },
17831
- {
17832
- name: "every registration helper the run read matched a call",
17833
- code: "no-helper",
17834
- audience: "run",
17835
- violations: helpersThatMatchedNothing(report.packs)
17836
- },
17837
- {
17838
- name: "every pack declares a version",
17839
- code: "no-version",
17840
- audience: "pack",
17841
- violations: unversionedPacks(report.packs)
17842
- },
17843
- {
17844
- name: "every link a declared pack states is data",
17845
- code: "fn-link",
17846
- audience: "pack",
17847
- violations: opaqueLinks(gradients)
17848
- },
17849
- {
17850
- name: "no declared pack reads the syntax tree",
17851
- code: "ast-link",
17852
- audience: "pack",
17853
- violations: reachesTheSyntaxTree(gradients)
17854
- },
17855
- {
17856
- name: "every declaration states an example",
17857
- code: "no-example",
17858
- audience: "pack",
17859
- violations: undocumentedDeclarations(gradients)
17860
- }
17861
- ];
17862
- }
17863
- function formatPackHealth(checks, audiences) {
17864
- const fired = checks.filter(
17865
- (check) => check.violations.length > 0 && audiences.includes(check.audience)
17866
- );
17867
- if (fired.length === 0) {
17868
- return "";
17869
- }
17870
- const rows = fired.flatMap(
17871
- (check) => check.violations.map((violation) => ({
17872
- code: check.code,
17873
- pack: violation.label,
17874
- detail: violation.detail
17875
- }))
17876
- );
17877
- const codeWidth = Math.max(...rows.map((row) => row.code.length));
17878
- const packWidth = Math.max(...rows.map((row) => row.pack.length));
17879
- const lines = ["", `Pack health (${rows.length}):`];
17880
- for (const row of rows) {
17881
- lines.push(
17882
- ` ${row.code.padEnd(codeWidth)} ${row.pack.padEnd(packWidth)} ${row.detail}`
17883
- );
17884
- }
17885
- return `${lines.join("\n")}
17886
- `;
17887
- }
17888
-
17889
17335
  // src/stubEvidence.ts
17890
- import path14 from "path";
17336
+ import path13 from "path";
17891
17337
  import { Project as Project4 } from "ts-morph";
17892
17338
  function stubEvidenceIn(project, packageName, root) {
17893
17339
  const resolution = new ResolutionStore();
@@ -17903,7 +17349,7 @@ function stubEvidenceIn(project, packageName, root) {
17903
17349
  calls: []
17904
17350
  };
17905
17351
  entry.calls.push({
17906
- file: path14.relative(root, sourceFile.getFilePath()).split(path14.sep).join("/"),
17352
+ file: path13.relative(root, sourceFile.getFilePath()).split(path13.sep).join("/"),
17907
17353
  line: one.call.getStartLineNumber(),
17908
17354
  args: one.call.getArguments().map((arg) => effectArgOf(arg))
17909
17355
  });
@@ -17918,7 +17364,7 @@ function stubEvidence(options) {
17918
17364
  const project = new Project4(
17919
17365
  options.tsConfigFilePath !== void 0 ? { tsConfigFilePath: options.tsConfigFilePath } : { compilerOptions: { allowJs: true } }
17920
17366
  );
17921
- const root = options.tsConfigFilePath !== void 0 ? path14.dirname(options.tsConfigFilePath) : path14.resolve(options.directory ?? process.cwd());
17367
+ const root = options.tsConfigFilePath !== void 0 ? path13.dirname(options.tsConfigFilePath) : path13.resolve(options.directory ?? process.cwd());
17922
17368
  if (options.tsConfigFilePath === void 0) {
17923
17369
  project.addSourceFilesAtPaths([
17924
17370
  `${root}/**/*.{ts,tsx,js,jsx}`,
@@ -17930,7 +17376,7 @@ function stubEvidence(options) {
17930
17376
  export {
17931
17377
  ADAPTER_VERSION,
17932
17378
  ResolutionStore,
17933
- WhySession,
17379
+ TypeScriptWhySession,
17934
17380
  accessContextFor,
17935
17381
  buildProjectHelperIndex,
17936
17382
  callOpsFor,
@@ -17943,13 +17389,11 @@ export {
17943
17389
  createTsSubUnitContext,
17944
17390
  createTypeScriptAdapter,
17945
17391
  discoverUnits,
17946
- evaluatePackHealth,
17947
17392
  exportedDeclarationsOf,
17948
17393
  extractCodeStructure,
17949
17394
  extractRawBranches,
17950
17395
  findNearestTsconfig,
17951
17396
  findTerminals,
17952
- formatPackHealth,
17953
17397
  functionTargetOf,
17954
17398
  importDeclarationsOf,
17955
17399
  importedNamesOf,