@suss/adapter-typescript 0.21.0 → 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
  }
@@ -1915,6 +1919,20 @@ function importedFilePathsOf(project, filePath) {
1915
1919
  cache.set(filePath, paths);
1916
1920
  return paths;
1917
1921
  }
1922
+ function resolutionCacheFor(project, options) {
1923
+ const existing = resolutionCachesByProject.get(project);
1924
+ if (existing !== void 0) {
1925
+ return existing;
1926
+ }
1927
+ const fresh = ts.createModuleResolutionCache(
1928
+ project.getFileSystem().getCurrentDirectory(),
1929
+ (name) => name,
1930
+ options
1931
+ );
1932
+ resolutionCachesByProject.set(project, fresh);
1933
+ return fresh;
1934
+ }
1935
+ var resolutionCachesByProject = /* @__PURE__ */ new WeakMap();
1918
1936
  function specifierCacheFor(project) {
1919
1937
  const existing = specifiersByProject.get(project);
1920
1938
  if (existing !== void 0) {
@@ -1941,13 +1959,15 @@ function readSpecifierPaths(project, filePath, options, host) {
1941
1959
  return [];
1942
1960
  }
1943
1961
  const scanned = ts.preProcessFile(text, true, true);
1962
+ const cache = resolutionCacheFor(project, options);
1944
1963
  const paths = [];
1945
1964
  for (const imported of scanned.importedFiles) {
1946
1965
  const resolved = ts.resolveModuleName(
1947
1966
  imported.fileName,
1948
1967
  filePath,
1949
1968
  options,
1950
- host
1969
+ host,
1970
+ cache
1951
1971
  ).resolvedModule?.resolvedFileName;
1952
1972
  if (resolved !== void 0 && !resolved.includes("/node_modules/")) {
1953
1973
  paths.push(resolved);
@@ -2204,11 +2224,11 @@ function walkAndWarm(root, reach) {
2204
2224
  if (warmedFiles.get(target) !== void 0) {
2205
2225
  continue;
2206
2226
  }
2207
- const path15 = target.getFilePath();
2208
- if (visited.has(path15)) {
2227
+ const path14 = target.getFilePath();
2228
+ if (visited.has(path14)) {
2209
2229
  continue;
2210
2230
  }
2211
- visited.add(path15);
2231
+ visited.add(path14);
2212
2232
  stack.push(frameOf(target, reach));
2213
2233
  continue;
2214
2234
  }
@@ -2225,7 +2245,7 @@ function sitsOnWarmChains(file) {
2225
2245
  return false;
2226
2246
  }
2227
2247
  const imported = importedFilePathsOf(project, file.getFilePath());
2228
- return imported.length > 0 && imported.every((path15) => warmed.has(path15));
2248
+ return imported.length > 0 && imported.every((path14) => warmed.has(path14));
2229
2249
  }
2230
2250
  function markWarmed(file) {
2231
2251
  warmedFiles.set(file, true);
@@ -2542,6 +2562,32 @@ function writesToBindingUncached(declaration) {
2542
2562
  }
2543
2563
  return { values, inOrder: writesRunInOrder(declaration, writes) };
2544
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
+ }
2545
2591
  function writesToField(declaration) {
2546
2592
  const remembered = byField.get(declaration);
2547
2593
  if (remembered !== void 0) {
@@ -2632,6 +2678,9 @@ function assignmentsInFile(sourceFile) {
2632
2678
  function isAssignmentOperator(kind) {
2633
2679
  return kind >= SyntaxKind3.FirstAssignment && kind <= SyntaxKind3.LastAssignment;
2634
2680
  }
2681
+ function writesItsWholeValue(kind) {
2682
+ return kind === SyntaxKind3.EqualsToken || kind === SyntaxKind3.QuestionQuestionEqualsToken || kind === SyntaxKind3.BarBarEqualsToken;
2683
+ }
2635
2684
  function writeAt(node) {
2636
2685
  if (Node13.isBinaryExpression(node)) {
2637
2686
  const operator = node.getOperatorToken().getKind();
@@ -2639,7 +2688,7 @@ function writeAt(node) {
2639
2688
  if (!isAssignmentOperator(operator) || !(Node13.isIdentifier(target) || Node13.isPropertyAccessExpression(target))) {
2640
2689
  return null;
2641
2690
  }
2642
- const value = operator === SyntaxKind3.EqualsToken ? node.getRight() : null;
2691
+ const value = writesItsWholeValue(operator) ? node.getRight() : null;
2643
2692
  return { target, write: { value, node } };
2644
2693
  }
2645
2694
  if (Node13.isPrefixUnaryExpression(node) || Node13.isPostfixUnaryExpression(node)) {
@@ -3030,11 +3079,16 @@ function emitBindingValues(db, table, declaration) {
3030
3079
  }
3031
3080
  const { values, inOrder } = writesToBinding(declaration);
3032
3081
  const last = values[values.length - 1];
3033
- if (!inOrder || last === void 0) {
3034
- reassignedUnstated.add(declarationId);
3082
+ if (inOrder && last !== void 0) {
3083
+ fact(db, "endsHolding", declarationId, emitValue(db, table, last));
3035
3084
  return;
3036
3085
  }
3037
- fact(db, "endsHolding", declarationId, emitValue(db, table, last));
3086
+ const construction = sameConstructionAcrossWrites(values);
3087
+ if (construction !== null) {
3088
+ fact(db, "endsHolding", declarationId, emitValue(db, table, construction));
3089
+ return;
3090
+ }
3091
+ reassignedUnstated.add(declarationId);
3038
3092
  }
3039
3093
  var reassignedUnstated = /* @__PURE__ */ new Set();
3040
3094
  function reassignedNamesUnstated() {
@@ -3046,23 +3100,31 @@ function forgetReassignedNamesUnstated() {
3046
3100
  function emitFieldValues(db, table, declaration) {
3047
3101
  const { values, inOrder } = writesToField(declaration);
3048
3102
  const last = values[values.length - 1];
3049
- 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));
3050
3105
  return;
3051
3106
  }
3052
- 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
+ }
3053
3111
  }
3054
3112
  function emitParameterPropertyRead(db, table, referenceId, declaration) {
3055
3113
  const { values, inOrder } = writesToField(declaration);
3056
3114
  const last = values[values.length - 1];
3057
- if (!inOrder || last === void 0) {
3115
+ if (inOrder && last !== void 0 && Node14.isExpression(last)) {
3116
+ fact(db, "binds", referenceId, emitValue(db, table, last));
3058
3117
  return;
3059
3118
  }
3060
- if (!Node14.isExpression(last)) {
3119
+ if (inOrder && last !== void 0) {
3061
3120
  table.byId.set(nodeId(last), last);
3062
3121
  fact(db, "binds", referenceId, nodeId(last));
3063
3122
  return;
3064
3123
  }
3065
- 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
+ }
3066
3128
  }
3067
3129
  function emitBindingElementFacts(db, table, element) {
3068
3130
  const id = nodeId(element);
@@ -3526,7 +3588,8 @@ var JS_RULES = [
3526
3588
  ];
3527
3589
  var RESOLUTION_PROGRAM = process.env.SUSS_RESOLUTION_ON_DEMAND === "0" ? {
3528
3590
  rules: [...SHARED_RULES, ...JS_RULES, ...RESOLUTION_QUESTIONS],
3529
- demandDriven: []
3591
+ demandDriven: [],
3592
+ demands: []
3530
3593
  } : deriveOnDemand(
3531
3594
  [...SHARED_RULES, ...JS_RULES, ...RESOLUTION_QUESTIONS],
3532
3595
  ANSWER_RELATIONS
@@ -3553,7 +3616,9 @@ var QUERY_FACTS = RESOLUTION_PROGRAM.demandDriven.length === 0 ? [] : [
3553
3616
  "wantedAnchor",
3554
3617
  "wantedSubject"
3555
3618
  ];
3556
- 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);
3557
3622
  var ResolutionStore = class {
3558
3623
  db = new Database2();
3559
3624
  table = createNodeTable();
@@ -3569,7 +3634,7 @@ var ResolutionStore = class {
3569
3634
  * extraction elsewhere cannot change an entry.
3570
3635
  */
3571
3636
  exportTables = /* @__PURE__ */ new Map();
3572
- /** Files the most recent wave walk entered, for the memo to keep. */
3637
+ /** Files the most recent query read, for the memo to keep. */
3573
3638
  lastQueryWalked = [];
3574
3639
  declarations = /* @__PURE__ */ new Map();
3575
3640
  graph = new ModuleGraph();
@@ -3600,19 +3665,19 @@ var ResolutionStore = class {
3600
3665
  */
3601
3666
  resolveCallableSources(value, alsoFrom) {
3602
3667
  const target = factKeyOf(value);
3603
- return this.resolveByWaves(
3668
+ return this.askAbout(
3604
3669
  target,
3605
3670
  "wanted",
3606
3671
  () => this.lookupSources(target),
3607
3672
  alsoFrom
3608
- ) ?? [];
3673
+ );
3609
3674
  }
3610
3675
  /**
3611
3676
  * The witness proof behind `resolveCallable`'s answer. Resolves the
3612
- * value first, which walks files and extracts facts in the usual
3613
- * waves, then re-evaluates the rules over those base facts under the
3614
- * witness algebra and rebuilds the proof of the answer. Null when
3615
- * 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.
3616
3681
  */
3617
3682
  explainCallable(value, options = {}) {
3618
3683
  const resolved = this.resolveCallable(value, options.alsoFrom);
@@ -3649,11 +3714,7 @@ var ResolutionStore = class {
3649
3714
  }
3650
3715
  resolveObject(value) {
3651
3716
  const target = factKeyOf(value);
3652
- return this.resolveByWaves(
3653
- target,
3654
- "wanted",
3655
- () => this.lookupObject(target)
3656
- );
3717
+ return this.askAbout(target, "wanted", () => this.lookupObject(target));
3657
3718
  }
3658
3719
  /**
3659
3720
  * For a value that is neither a function nor an object, such as a
@@ -3669,17 +3730,17 @@ var ResolutionStore = class {
3669
3730
  }
3670
3731
  return cached.written;
3671
3732
  }
3672
- const settled2 = this.resolveByWaves(
3733
+ const written = this.askAbout(
3673
3734
  target,
3674
3735
  "wanted",
3675
3736
  () => this.lookupWritten(target)
3676
3737
  );
3677
3738
  this.writtenValues.set(key, {
3678
- written: settled2?.written ?? null,
3739
+ written,
3679
3740
  walked: [...this.lastQueryWalked],
3680
3741
  extractedAt: this.fullyExtracted.size
3681
3742
  });
3682
- return settled2?.written ?? null;
3743
+ return written;
3683
3744
  }
3684
3745
  /**
3685
3746
  * The construction behind a registration subject: the call or `new`
@@ -3699,18 +3760,29 @@ var ResolutionStore = class {
3699
3760
  }
3700
3761
  return cached.construction;
3701
3762
  }
3702
- const settled2 = this.resolveByWaves(
3763
+ const settled2 = this.askAbout(
3703
3764
  target,
3704
3765
  "wantedSubject",
3705
3766
  () => this.lookupSubjectConstruction(target, importModule, importName)
3706
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
+ }
3707
3779
  this.subjectConstructions.set(key, {
3708
- construction: settled2?.construction ?? null,
3709
- candidates: settled2?.candidates ?? 0,
3710
- walked: [...this.lastQueryWalked],
3780
+ construction: resolved.construction,
3781
+ candidates: resolved.candidates,
3782
+ walked,
3711
3783
  extractedAt: this.fullyExtracted.size
3712
3784
  });
3713
- return settled2?.construction ?? null;
3785
+ return resolved.construction;
3714
3786
  }
3715
3787
  /**
3716
3788
  * How many constructions this value was written as, which is what
@@ -3724,10 +3796,8 @@ var ResolutionStore = class {
3724
3796
  )?.candidates ?? 0;
3725
3797
  }
3726
3798
  /**
3727
- * The single-answer policy over the written-value walk, then the
3728
- * origin check on the one candidate. A candidate failing the check
3729
- * is settled refusal rather than a reason to widen: its own file's
3730
- * 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.
3731
3801
  */
3732
3802
  lookupSubjectConstruction(value, importModule, importName) {
3733
3803
  this.derive();
@@ -3743,6 +3813,7 @@ var ResolutionStore = class {
3743
3813
  if (candidates.size > 1) {
3744
3814
  return {
3745
3815
  construction: null,
3816
+ candidate: null,
3746
3817
  candidates: [...candidates].filter(
3747
3818
  (one) => this.constructionComesFrom(valueId, one, importModule, importName)
3748
3819
  ).length
@@ -3750,7 +3821,7 @@ var ResolutionStore = class {
3750
3821
  }
3751
3822
  const single = [...candidates][0];
3752
3823
  if (single === void 0) {
3753
- return neverWritable(value) ? { construction: null, candidates: 0 } : null;
3824
+ return { construction: null, candidate: null, candidates: 0 };
3754
3825
  }
3755
3826
  return {
3756
3827
  construction: this.constructionComesFrom(
@@ -3759,6 +3830,7 @@ var ResolutionStore = class {
3759
3830
  importModule,
3760
3831
  importName
3761
3832
  ) ? single : null,
3833
+ candidate: single,
3762
3834
  candidates: 1
3763
3835
  };
3764
3836
  }
@@ -3818,11 +3890,11 @@ var ResolutionStore = class {
3818
3890
  return cached.names;
3819
3891
  }
3820
3892
  const target = factKeyOf(value);
3821
- const found = this.resolveByWaves(
3893
+ const found = this.askAbout(
3822
3894
  target,
3823
3895
  "wantedOrigin",
3824
3896
  () => this.lookupImportedNames(target, modules)
3825
- ) ?? [];
3897
+ );
3826
3898
  this.importedNames.set(declaration, {
3827
3899
  names: found,
3828
3900
  walked: [...this.lastQueryWalked]
@@ -3841,8 +3913,8 @@ var ResolutionStore = class {
3841
3913
  return this.importOriginsOfMany([value], modules).get(value) ?? [];
3842
3914
  }
3843
3915
  /**
3844
- * The batched form: one demand set, one derivation, one widening
3845
- * 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
3846
3918
  * callee in a file, and per-value queries would re-pay demand
3847
3919
  * clearing and re-derivation once per call site.
3848
3920
  */
@@ -3870,49 +3942,12 @@ var ResolutionStore = class {
3870
3942
  this.wantValue("wantedCallOrigin", one.target);
3871
3943
  this.seedValue(one.target);
3872
3944
  }
3873
- const walked = /* @__PURE__ */ new Set();
3874
- this.lastQueryWalked = [];
3875
- let frontier = [];
3945
+ this.extractDemanded(pending.map((one) => one.target.getSourceFile()));
3876
3946
  for (const one of pending) {
3877
- const file = one.target.getSourceFile();
3878
- if (!walked.has(file.getFilePath()) && !frontier.includes(file)) {
3879
- frontier.push(file);
3880
- }
3881
- }
3882
- const open = new Set(pending);
3883
- for (let hop = 0; hop <= MAX_MODULE_HOPS && open.size > 0; hop++) {
3884
- const next = [];
3885
- for (const sourceFile of frontier) {
3886
- if (walked.has(sourceFile.getFilePath())) {
3887
- continue;
3888
- }
3889
- walked.add(sourceFile.getFilePath());
3890
- this.lastQueryWalked.push(sourceFile.getFilePath());
3891
- recordFileDependency(sourceFile.getFilePath());
3892
- this.extractFile(sourceFile);
3893
- next.push(...this.graph.importedFilesOf(sourceFile));
3894
- }
3895
- for (const one of [...open]) {
3896
- const found = this.lookupImportOrigins(one.target, modules);
3897
- if (found !== null) {
3898
- results.set(one.value, found);
3899
- this.importOrigins.set(one.key, {
3900
- origins: found,
3901
- walked: [...this.lastQueryWalked],
3902
- extractedAt: this.fullyExtracted.size
3903
- });
3904
- open.delete(one);
3905
- }
3906
- }
3907
- if (next.length === 0) {
3908
- break;
3909
- }
3910
- frontier = next;
3911
- }
3912
- for (const one of open) {
3913
- results.set(one.value, []);
3947
+ const origins = this.lookupImportOrigins(one.target, modules);
3948
+ results.set(one.value, origins);
3914
3949
  this.importOrigins.set(one.key, {
3915
- origins: [],
3950
+ origins,
3916
3951
  walked: [...this.lastQueryWalked],
3917
3952
  extractedAt: this.fullyExtracted.size
3918
3953
  });
@@ -3971,67 +4006,89 @@ var ResolutionStore = class {
3971
4006
  const refined = new Set(
3972
4007
  [...byPath.values()].filter((one) => one.path.length > 1).map((one) => tupleKey(one.path.slice(0, -1)))
3973
4008
  );
3974
- const origins = [...byPath.values()].filter((one) => !refined.has(tupleKey(one.path))).sort((a, b) => a.path.join(".").localeCompare(b.path.join(".")));
3975
- if (origins.length > 0) {
3976
- return origins;
3977
- }
3978
- 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(".")));
3979
4010
  }
3980
- /**
3981
- * Null tells the walk to keep widening. A value that landed in a
3982
- * package returns empty instead, because a library's own body is not
3983
- * here to read and widening would never find anything.
3984
- */
3985
4011
  lookupImportedNames(value, modules) {
3986
4012
  this.derive();
3987
- const origins = this.answerPairsFor("wantedComesFrom", nodeId(value));
3988
- const reached = new Set(origins);
4013
+ const reached = new Set(
4014
+ this.answerPairsFor("wantedComesFrom", nodeId(value))
4015
+ );
3989
4016
  for (const target of this.answersFor("wantedComesTo", nodeId(value))) {
3990
4017
  for (const entry of this.answerPairsFor("wantedCallsInto", target)) {
3991
4018
  reached.add(entry);
3992
4019
  }
3993
4020
  }
3994
- const names = namesFrom([...reached], modules);
3995
- if (names.length > 0) {
3996
- return names;
3997
- }
3998
- return origins.some((pair) => namesAPackage(pairHalves(pair).module)) ? [] : null;
4021
+ return namesFrom([...reached], modules);
3999
4022
  }
4000
- resolveByWaves(value, question, ask, alsoFrom) {
4023
+ askAbout(value, question, read2, alsoFrom) {
4001
4024
  try {
4002
- 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();
4003
4031
  } finally {
4004
4032
  this.forgetQuery();
4005
4033
  }
4006
4034
  }
4007
- walkForAnswer(value, question, ask, alsoFrom) {
4008
- this.wantValue(question, value);
4009
- this.seedValue(value);
4010
- 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();
4011
4048
  this.lastQueryWalked = [];
4012
- let frontier = alsoFrom === void 0 ? [value.getSourceFile()] : [value.getSourceFile(), alsoFrom];
4013
- for (let hop = 0; hop <= MAX_MODULE_HOPS; hop++) {
4014
- const next = [];
4015
- for (const sourceFile of frontier) {
4016
- 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)) {
4017
4055
  continue;
4018
4056
  }
4019
- walked.add(sourceFile.getFilePath());
4020
- this.lastQueryWalked.push(sourceFile.getFilePath());
4021
- recordFileDependency(sourceFile.getFilePath());
4057
+ read2.add(filePath);
4058
+ readThisRound.push(filePath);
4059
+ this.lastQueryWalked.push(filePath);
4060
+ recordFileDependency(filePath);
4022
4061
  this.extractFile(sourceFile);
4023
- next.push(...this.graph.importedFilesOf(sourceFile));
4024
4062
  }
4025
- const found = ask();
4026
- if (found !== null) {
4027
- return found;
4028
- }
4029
- if (next.length === 0) {
4030
- 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
+ }
4031
4074
  }
4032
- frontier = next;
4033
4075
  }
4034
- 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
+ );
4035
4092
  }
4036
4093
  /**
4037
4094
  * The question and the chain that settled it are dropped. The files
@@ -4050,6 +4107,27 @@ var ResolutionStore = class {
4050
4107
  this.stale = true;
4051
4108
  }
4052
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
+ }
4053
4131
  /**
4054
4132
  * File extraction only reaches values hanging off exports, and a query
4055
4133
  * can be rooted anywhere, so the queried value gets its own facts.
@@ -4099,24 +4177,18 @@ var ResolutionStore = class {
4099
4177
  }
4100
4178
  candidates.add(node);
4101
4179
  }
4102
- if (candidates.size === 1) {
4103
- return { written: [...candidates][0] };
4104
- }
4105
- if (candidates.size > 1) {
4106
- return { written: null };
4107
- }
4108
- if (neverWritable(value)) {
4109
- return { written: null };
4180
+ if (candidates.size !== 1) {
4181
+ return null;
4110
4182
  }
4111
- return null;
4183
+ return [...candidates][0];
4112
4184
  }
4113
4185
  /**
4114
4186
  * Read these files' facts now, before anything asks a question.
4115
4187
  *
4116
- * A query widens along the imports of the file it starts in, which
4117
- * 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
4118
4190
  * direction, so a question about a parameter is answered only by
4119
- * files the walk never reaches on its own.
4191
+ * files the query never reaches on its own.
4120
4192
  */
4121
4193
  extractFiles(sourceFiles) {
4122
4194
  for (const sourceFile of sourceFiles) {
@@ -4132,15 +4204,14 @@ var ResolutionStore = class {
4132
4204
  return this.graph.filesReachingAnyPackage(fileSets);
4133
4205
  }
4134
4206
  /**
4135
- * The calls behind a receiver that `matches` accepts, walked out in
4136
- * waves the way every question is. The wave ends as soon as one file
4137
- * yields a match, and the caller applies the single-answer policy to
4138
- * the set; the resolution README's anchor section says why the rules
4139
- * 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.
4140
4211
  */
4141
4212
  anchorCallsOf(value, matches) {
4142
4213
  const target = factKeyOf(value);
4143
- return this.resolveByWaves(target, "wantedAnchor", () => {
4214
+ return this.askAbout(target, "wantedAnchor", () => {
4144
4215
  this.derive();
4145
4216
  const found = [];
4146
4217
  for (const id of this.answersFor("wantedAnchorCall", nodeId(target))) {
@@ -4149,12 +4220,12 @@ var ResolutionStore = class {
4149
4220
  found.push(node);
4150
4221
  }
4151
4222
  }
4152
- return found.length === 0 ? null : found;
4153
- }) ?? [];
4223
+ return found;
4224
+ });
4154
4225
  }
4155
4226
  /**
4156
4227
  * Every name a module exports and the values behind each, with
4157
- * re-export chains flattened by the rules. The frontier follows
4228
+ * re-export chains flattened by the rules. The demand follows
4158
4229
  * re-export targets only, so a barrel of barrels extracts its own
4159
4230
  * chain and nothing beside it, to any depth.
4160
4231
  */
@@ -4174,33 +4245,10 @@ var ResolutionStore = class {
4174
4245
  }
4175
4246
  }
4176
4247
  collectExports(sourceFile) {
4177
- const project = sourceFile.getProject();
4178
- const walked = /* @__PURE__ */ new Set();
4179
- let frontier = [sourceFile];
4180
- while (frontier.length > 0) {
4181
- const next = [];
4182
- for (const file of frontier) {
4183
- const filePath2 = file.getFilePath();
4184
- if (walked.has(filePath2)) {
4185
- continue;
4186
- }
4187
- walked.add(filePath2);
4188
- recordFileDependency(filePath2);
4189
- this.extractFile(file);
4190
- for (const target of this.reExportTargetsOf(filePath2)) {
4191
- const targetFile = sourceFileFor(project, target);
4192
- if (targetFile !== void 0) {
4193
- next.push(targetFile);
4194
- }
4195
- }
4196
- }
4197
- frontier = next;
4198
- }
4199
4248
  const filePath = sourceFile.getFilePath();
4200
- if (this.db.add("wantedExportsOf", [filePath]) === "added") {
4201
- this.stale = true;
4202
- }
4203
- this.derive();
4249
+ this.wantExportsOf(filePath);
4250
+ this.extractDemanded([sourceFile]);
4251
+ this.rederiveExportsOf(filePath);
4204
4252
  const exports = /* @__PURE__ */ new Map();
4205
4253
  for (const tuple of this.db.lookup("wantedModuleExport", 0, filePath)) {
4206
4254
  const node = this.table.byId.get(String(tuple[2]));
@@ -4217,22 +4265,9 @@ var ResolutionStore = class {
4217
4265
  }
4218
4266
  return exports;
4219
4267
  }
4220
- /** Module keys this file re-exports from, read off its own facts. */
4221
- reExportTargetsOf(filePath) {
4222
- const targets = /* @__PURE__ */ new Set();
4223
- for (const tuple of this.db.lookup("reExports", 0, filePath)) {
4224
- targets.add(String(tuple[2]));
4225
- }
4226
- for (const tuple of this.db.lookup("reExportsAll", 0, filePath)) {
4227
- targets.add(String(tuple[1]));
4228
- }
4229
- return [...targets];
4230
- }
4231
4268
  /**
4232
- * Null keeps the wave walk widening; any candidate at all ends it,
4233
- * since more files can only add candidates, never take one away. The
4234
- * single-answer policy stays with the callers: picking one of two
4235
- * 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.
4236
4271
  */
4237
4272
  lookupSources(value) {
4238
4273
  this.derive();
@@ -4246,7 +4281,7 @@ var ResolutionStore = class {
4246
4281
  candidates.add(resolved);
4247
4282
  }
4248
4283
  }
4249
- return candidates.size === 0 ? null : [...candidates];
4284
+ return [...candidates];
4250
4285
  }
4251
4286
  derive() {
4252
4287
  if (!this.stale) {
@@ -4305,18 +4340,6 @@ function pairHalves(pair) {
4305
4340
  const [module = "", name = ""] = tupleKeyParts(pair);
4306
4341
  return { module, name };
4307
4342
  }
4308
- function namesAPackage(moduleKey) {
4309
- return !moduleKey.startsWith("/") || moduleKey.includes("/node_modules/");
4310
- }
4311
- function neverWritable(value) {
4312
- if (!Node15.isIdentifier(value)) {
4313
- return false;
4314
- }
4315
- const declarations = value.getSymbol()?.getDeclarations() ?? [];
4316
- return declarations.some(
4317
- (declaration) => declaration.getSourceFile().getFilePath().includes("/node_modules/")
4318
- );
4319
- }
4320
4343
  function namesPackage2(moduleKey, packages) {
4321
4344
  return namesAnyPackage(
4322
4345
  [moduleKey, ...packagesDeclaring(moduleKey)],
@@ -4337,35 +4360,12 @@ import path3 from "path";
4337
4360
  // src/diagnostics.ts
4338
4361
  import path2 from "path";
4339
4362
  import { ts as ts3 } from "ts-morph";
4340
- import { BOUNDARY_ROLE } from "@suss/behavioral-ir";
4341
- var emptyTally = () => ({
4342
- candidateFiles: 0,
4343
- unitsDiscovered: 0,
4344
- unitsInGatedFiles: 0,
4345
- effectsRecognized: 0,
4346
- unitsClaimed: 0,
4347
- selfCollisions: 0,
4348
- summariesProduced: 0,
4349
- failures: [],
4350
- helpersMatched: /* @__PURE__ */ new Set()
4351
- });
4352
- function recordPackFailure(tally, failure) {
4353
- const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
4354
- tally?.failures.push({
4355
- hook: failure.hook,
4356
- file: failure.file,
4357
- message
4358
- });
4359
- return `[suss] pack "${failure.pack}" threw from ${failure.hook} while reading ${failure.file}: ${message}
4360
- `;
4361
- }
4362
- function createPackTallies(packs) {
4363
- const tallies = /* @__PURE__ */ new Map();
4364
- for (const pack of packs) {
4365
- tallies.set(pack.name, emptyTally());
4366
- }
4367
- return tallies;
4368
- }
4363
+ import {
4364
+ createPackTallies,
4365
+ emptyTally,
4366
+ recordPackFailure,
4367
+ summaryCountsByPack
4368
+ } from "@suss/extractor";
4369
4369
  function commonDirectoryOf(files) {
4370
4370
  const absolute = files.filter((f) => path2.isAbsolute(f));
4371
4371
  const first = absolute[0];
@@ -4442,30 +4442,6 @@ function unresolvedGatesFor(gates, args) {
4442
4442
  }
4443
4443
  return unresolved;
4444
4444
  }
4445
- var emptyCounts = () => ({
4446
- bound: 0,
4447
- providers: 0,
4448
- withBehavior: 0
4449
- });
4450
- function summaryCountsByPack(summaries) {
4451
- const counts = /* @__PURE__ */ new Map();
4452
- for (const summary of summaries) {
4453
- const recognition = summary.identity.boundaryBinding?.recognition;
4454
- if (recognition === void 0 || recognition === null) {
4455
- continue;
4456
- }
4457
- const entry = counts.get(recognition) ?? emptyCounts();
4458
- entry.bound += 1;
4459
- if (BOUNDARY_ROLE[summary.kind] === "provider") {
4460
- entry.providers += 1;
4461
- }
4462
- if (summary.transitions.length > 0) {
4463
- entry.withBehavior += 1;
4464
- }
4465
- counts.set(recognition, entry);
4466
- }
4467
- return counts;
4468
- }
4469
4445
  function helpersWithNoCall(patterns, matched, projectRoot) {
4470
4446
  const missing = [];
4471
4447
  for (const pattern of patterns) {
@@ -4490,7 +4466,11 @@ function buildExtractionReport(args) {
4490
4466
  const packFunnels = args.packs.map((pack) => {
4491
4467
  const gates = packIsUngated(pack) ? [] : collectPackGates(pack);
4492
4468
  const tally = args.tallies.get(pack.name) ?? emptyTally();
4493
- const counted = bySummary.get(pack.name) ?? emptyCounts();
4469
+ const counted = bySummary.get(pack.name) ?? {
4470
+ bound: 0,
4471
+ providers: 0,
4472
+ withBehavior: 0
4473
+ };
4494
4474
  return {
4495
4475
  pack: pack.name,
4496
4476
  version: pack.version ?? null,
@@ -4719,13 +4699,26 @@ function propertiesReached(object, resolution, seen) {
4719
4699
  }
4720
4700
  function writtenNodeOf(value, resolution) {
4721
4701
  const written = factKeyOf(value);
4722
- if (Node16.isCallExpression(written) || Node16.isNewExpression(written)) {
4702
+ if (Node16.isNewExpression(written)) {
4723
4703
  return written;
4724
4704
  }
4705
+ if (Node16.isCallExpression(written)) {
4706
+ return writtenThroughCall(written, resolution);
4707
+ }
4725
4708
  if (resolution === void 0 || !couldNameAValue(written)) {
4726
4709
  return null;
4727
4710
  }
4728
- 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;
4729
4722
  }
4730
4723
  function stringValueOf(value, resolution) {
4731
4724
  const written = factKeyOf(value);
@@ -4787,7 +4780,7 @@ function discoverClientCalls(sourceFile, match, kind, resolution) {
4787
4780
  }
4788
4781
  } else if (Node17.isPropertyAccessExpression(callee)) {
4789
4782
  const subject = callee.getExpression();
4790
- 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)) {
4791
4784
  methodName = callee.getName();
4792
4785
  if (methodFilter === null || methodFilter.has(methodName)) {
4793
4786
  matched = true;
@@ -6397,14 +6390,14 @@ function unitForJsxRoute(node, match, routeNames, kind, resolution) {
6397
6390
  }
6398
6391
  return routeUnit(node, kind, match.method, resolved.path, target, resolution);
6399
6392
  }
6400
- function routeUnit(at, kind, method, path15, target, resolution) {
6401
- const routeInfo = { method, path: path15 };
6393
+ function routeUnit(at, kind, method, path14, target, resolution) {
6394
+ const routeInfo = { method, path: path14 };
6402
6395
  if (target.kind === "unreadable") {
6403
6396
  return {
6404
6397
  func: null,
6405
6398
  announcedAt: at,
6406
6399
  kind,
6407
- name: path15,
6400
+ name: path14,
6408
6401
  nameKind: "label",
6409
6402
  routeInfo
6410
6403
  };
@@ -6603,7 +6596,6 @@ function labelOfExpression(node) {
6603
6596
  }
6604
6597
 
6605
6598
  // src/discovery/packageExports.ts
6606
- import fs4 from "fs";
6607
6599
  import { Node as Node29 } from "ts-morph";
6608
6600
 
6609
6601
  // src/packageExports.ts
@@ -6632,7 +6624,8 @@ function resolvePackageExports(packageJsonPath, opts = {}) {
6632
6624
  packageName,
6633
6625
  subPath,
6634
6626
  exportPathPrefix: subPathToPrefix(subPath),
6635
- sourceFile: src
6627
+ sourceFile: src,
6628
+ publishedFile: path5.resolve(pkgDir, distPath)
6636
6629
  });
6637
6630
  };
6638
6631
  const exportsField = pkg.exports;
@@ -6652,6 +6645,53 @@ function resolvePackageExports(packageJsonPath, opts = {}) {
6652
6645
  }
6653
6646
  return { packageName, entries, warnings };
6654
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
+ }
6655
6695
  function pickConditional(value, warnings, key) {
6656
6696
  if (value === void 0 || value === null) {
6657
6697
  if (value === null) {
@@ -6701,8 +6741,55 @@ function resolveSourceFor(pkgDir, distPath, srcRoot) {
6701
6741
  return null;
6702
6742
  }
6703
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
+
6704
6790
  // src/discovery/factorySurface.ts
6705
6791
  import { Node as Node28, SyntaxKind as SyntaxKind6 } from "ts-morph";
6792
+ var MAX_BUILDER_HOPS = 2;
6706
6793
  function surfaceMethods(decl) {
6707
6794
  if (Node28.isClassDeclaration(decl)) {
6708
6795
  return surfaceClassMethods(decl);
@@ -6742,16 +6829,20 @@ function surfaceClassMethods(cls) {
6742
6829
  function surfaceFactoryReturnMethods(fn) {
6743
6830
  const out = [];
6744
6831
  const seen = /* @__PURE__ */ new Set();
6832
+ collectFromFunctionReturns(fn, out, seen, 0);
6833
+ return out;
6834
+ }
6835
+ function collectFromFunctionReturns(fn, out, seen, depth) {
6745
6836
  if (Node28.isArrowFunction(fn)) {
6746
6837
  const body2 = fn.getBody();
6747
6838
  if (Node28.isExpression(body2)) {
6748
- collectFromObjectLiteral(body2, out, seen);
6749
- return out;
6839
+ collectFromReturnValue(body2, out, seen, depth);
6840
+ return;
6750
6841
  }
6751
6842
  }
6752
6843
  const body = fn.getBody?.();
6753
6844
  if (body === void 0) {
6754
- return out;
6845
+ return;
6755
6846
  }
6756
6847
  const returns = [];
6757
6848
  body.forEachDescendant((node, traversal) => {
@@ -6768,9 +6859,53 @@ function surfaceFactoryReturnMethods(fn) {
6768
6859
  if (expr === void 0) {
6769
6860
  continue;
6770
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)) {
6771
6868
  collectFromObjectLiteral(expr, out, seen);
6869
+ return;
6772
6870
  }
6773
- 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;
6774
6909
  }
6775
6910
  function collectFromObjectLiteral(node, out, seen) {
6776
6911
  const e = peelParens(node);
@@ -6805,31 +6940,31 @@ function collectFromObjectLiteral(node, out, seen) {
6805
6940
  out.push({ func: v3, name });
6806
6941
  }
6807
6942
  }
6943
+ if (Node28.isShorthandPropertyAssignment(prop)) {
6944
+ collectShorthandFunction(prop, out, seen);
6945
+ }
6808
6946
  }
6809
6947
  }
6810
-
6811
- // src/discovery/packageExports.ts
6812
- var packageExportsCache = /* @__PURE__ */ new Map();
6813
- var PATH_STAMP_SEPARATOR = "";
6814
- function packageJsonStamp(packageJsonPath) {
6815
- try {
6816
- const stat = fs4.statSync(packageJsonPath);
6817
- return `${stat.mtimeMs}:${stat.size}`;
6818
- } catch {
6819
- return "unreadable";
6948
+ function collectShorthandFunction(prop, out, seen) {
6949
+ const name = prop.getName();
6950
+ if (seen.has(name)) {
6951
+ return;
6820
6952
  }
6821
- }
6822
- function resolvePackageExportsCached(packageJsonPath) {
6823
- const stamp = packageJsonStamp(packageJsonPath);
6824
- const key = `${packageJsonPath}${PATH_STAMP_SEPARATOR}${stamp}`;
6825
- const cached = packageExportsCache.get(key);
6826
- if (cached !== void 0) {
6827
- 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 });
6828
6964
  }
6829
- const fresh = resolvePackageExports(packageJsonPath);
6830
- packageExportsCache.set(key, fresh);
6831
- return fresh;
6832
6965
  }
6966
+
6967
+ // src/discovery/packageExports.ts
6833
6968
  function discoverPackageExports(sourceFile, match, kind, resolution) {
6834
6969
  if (match.packageJsonPath === void 0) {
6835
6970
  return [];
@@ -6864,7 +6999,10 @@ function discoverPackageExports(sourceFile, match, kind, resolution) {
6864
6999
  if (seenNames.has(key)) {
6865
7000
  continue;
6866
7001
  }
6867
- for (const decl of decls) {
7002
+ const candidates = decls.flatMap(
7003
+ (decl) => sourceDeclarationsBehind(decl, resolution)
7004
+ );
7005
+ for (const decl of candidates) {
6868
7006
  if (Node29.isVariableDeclaration(decl)) {
6869
7007
  const init = decl.getInitializer();
6870
7008
  if (init !== void 0 && (Node29.isArrowFunction(init) || Node29.isFunctionExpression(init))) {
@@ -7019,7 +7157,7 @@ function attributedCalls(sourceFile, packages, resolution) {
7019
7157
  return null;
7020
7158
  }
7021
7159
  if (!methodComesFromSource(callee)) {
7022
- return subject;
7160
+ return null;
7023
7161
  }
7024
7162
  return {
7025
7163
  packageName: subject.packageName,
@@ -7133,12 +7271,12 @@ function originEndOf(flattened) {
7133
7271
  const slash = flattened.indexOf("/", opener[0].length);
7134
7272
  return slash === -1 ? flattened.length : slash;
7135
7273
  }
7136
- function appendPathText(path15, text) {
7274
+ function appendPathText(path14, text) {
7137
7275
  const idx = text.search(/[?#]/);
7138
7276
  if (idx === -1) {
7139
- return { path: path15 + text, stop: false };
7277
+ return { path: path14 + text, stop: false };
7140
7278
  }
7141
- return { path: path15 + text.slice(0, idx), stop: true };
7279
+ return { path: path14 + text.slice(0, idx), stop: true };
7142
7280
  }
7143
7281
  function parseAbsoluteUrl(text) {
7144
7282
  try {
@@ -7162,8 +7300,8 @@ function pathnameOfAbsoluteLiteral(text) {
7162
7300
  return stripQueryAndFragment(stripOriginManually(text));
7163
7301
  }
7164
7302
  function pathFromLiteralUrl(text) {
7165
- const path15 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
7166
- return path15 === "" ? void 0 : path15;
7303
+ const path14 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
7304
+ return path14 === "" ? void 0 : path14;
7167
7305
  }
7168
7306
  function pathFromTemplateUrl(arg, resolution) {
7169
7307
  const headText = arg.getHead().getLiteralText();
@@ -7171,7 +7309,7 @@ function pathFromTemplateUrl(arg, resolution) {
7171
7309
  const tails = spans.map((span) => span.getLiteral().getLiteralText());
7172
7310
  const originEnd = originEndOf([headText, ...tails].join(SUBSTITUTION));
7173
7311
  const head = appendPathText("", headText.slice(originEnd));
7174
- let path15 = head.path;
7312
+ let path14 = head.path;
7175
7313
  let stop = head.stop;
7176
7314
  let at = headText.length;
7177
7315
  for (let i = 0; i < spans.length && !stop; i++) {
@@ -7181,18 +7319,18 @@ function pathFromTemplateUrl(arg, resolution) {
7181
7319
  continue;
7182
7320
  }
7183
7321
  if (at >= originEnd) {
7184
- path15 += holeText(span.getExpression(), resolution);
7322
+ path14 += holeText(span.getExpression(), resolution);
7185
7323
  }
7186
7324
  at += SUBSTITUTION.length;
7187
7325
  const appended = appendPathText(
7188
- path15,
7326
+ path14,
7189
7327
  tailText.slice(Math.max(0, originEnd - at))
7190
7328
  );
7191
- path15 = appended.path;
7329
+ path14 = appended.path;
7192
7330
  stop = appended.stop;
7193
7331
  at += tailText.length;
7194
7332
  }
7195
- return path15 === "" ? void 0 : path15;
7333
+ return path14 === "" ? void 0 : path14;
7196
7334
  }
7197
7335
  function pathFromUrlNode(node, resolution) {
7198
7336
  if (Node31.isStringLiteral(node)) {
@@ -7262,12 +7400,12 @@ function discoverRegistrationCalls(sourceFile, match, kind, bindingExtraction, r
7262
7400
  match.importName,
7263
7401
  resolution
7264
7402
  );
7265
- if (registrationSubjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution)) {
7266
- return results;
7267
- }
7268
7403
  const registrationMethods = match.registrationChain.map(
7269
7404
  (c) => c.startsWith(".") ? c.slice(1) : c
7270
7405
  );
7406
+ if (registrationSubjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution, registrationMethods)) {
7407
+ return results;
7408
+ }
7271
7409
  sourceFile.forEachDescendant((node) => {
7272
7410
  if (!Node32.isCallExpression(node)) {
7273
7411
  return;
@@ -7461,8 +7599,54 @@ function receiverOf(subject, subjects, match, resolution) {
7461
7599
  );
7462
7600
  return candidates > 1 ? { kind: "several", candidates } : NOT_A_ROUTABLE;
7463
7601
  }
7464
- function storeCanFindSubjects(sourceFile, match, resolution) {
7465
- 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;
7466
7650
  }
7467
7651
  function registrationSubjectsOf(sourceFile, importModule, importName, resolution) {
7468
7652
  const importedLocalName = resolveImportedLocalName(
@@ -7527,9 +7711,33 @@ function registrationSubjectIdsOf(sourceFile, matches, resolution) {
7527
7711
  ).values()) {
7528
7712
  ids.add(nodeId(node));
7529
7713
  }
7714
+ for (const node of unboundConstructionsOf(
7715
+ sourceFile,
7716
+ match.importModule,
7717
+ match.importName
7718
+ )) {
7719
+ ids.add(nodeId(node));
7720
+ }
7530
7721
  }
7531
7722
  return ids;
7532
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
+ }
7533
7741
  function discoverMountEdges(sourceFile, match, mount, knownSubjectIds, resolution) {
7534
7742
  const subjects = registrationSubjectsOf(
7535
7743
  sourceFile,
@@ -7537,7 +7745,7 @@ function discoverMountEdges(sourceFile, match, mount, knownSubjectIds, resolutio
7537
7745
  match.importName,
7538
7746
  resolution
7539
7747
  );
7540
- if (subjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution)) {
7748
+ if (subjects.size === 0 && !storeCanFindSubjects(sourceFile, match, resolution, [mount.method])) {
7541
7749
  return [];
7542
7750
  }
7543
7751
  const edges = [];
@@ -7618,11 +7826,11 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
7618
7826
  return null;
7619
7827
  }
7620
7828
  const method2 = stringProperty(routeObject, binding.method.property);
7621
- const path16 = stringProperty(routeObject, binding.path.property);
7622
- if (method2 === null || path16 === null) {
7829
+ const path15 = stringProperty(routeObject, binding.path.property);
7830
+ if (method2 === null || path15 === null) {
7623
7831
  return null;
7624
7832
  }
7625
- return { method: method2.toUpperCase(), path: path16 };
7833
+ return { method: method2.toUpperCase(), path: path15 };
7626
7834
  }
7627
7835
  if (binding.method.type !== "fromRegistration" || binding.path.type !== "fromArgument") {
7628
7836
  return null;
@@ -7639,8 +7847,8 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
7639
7847
  method = arg.getLiteralValue().toUpperCase();
7640
7848
  }
7641
7849
  const pathArg = call.getArguments()[binding.path.position];
7642
- const path15 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
7643
- 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 };
7644
7852
  }
7645
7853
 
7646
7854
  // src/discovery/registrationLoop.ts
@@ -7772,7 +7980,7 @@ function bodyCallsOnRoutable(loop, guard, resolution) {
7772
7980
  }
7773
7981
  function readRouteSpec(element, shape, resolution) {
7774
7982
  let method = null;
7775
- let path15 = null;
7983
+ let path14 = null;
7776
7984
  let handler = null;
7777
7985
  for (const prop of element.getProperties()) {
7778
7986
  if (Node33.isMethodDeclaration(prop) && prop.getName() === shape.handlerKey) {
@@ -7790,16 +7998,16 @@ function readRouteSpec(element, shape, resolution) {
7790
7998
  if (name === shape.methodKey) {
7791
7999
  method = readStringLiteralValue(init);
7792
8000
  } else if (name === shape.pathKey) {
7793
- path15 = readStringLiteralValue(init);
8001
+ path14 = readStringLiteralValue(init);
7794
8002
  } else if (name === shape.handlerKey) {
7795
8003
  const func = functionValueOf(init, resolution);
7796
8004
  handler = func === null ? null : { func, name: handlerName(init) };
7797
8005
  }
7798
8006
  }
7799
- if (method === null || path15 === null || handler === null) {
8007
+ if (method === null || path14 === null || handler === null) {
7800
8008
  return null;
7801
8009
  }
7802
- return { method, path: path15, handler: handler.func, handlerName: handler.name };
8010
+ return { method, path: path14, handler: handler.func, handlerName: handler.name };
7803
8011
  }
7804
8012
  function readStringLiteralValue(node) {
7805
8013
  if (Node33.isStringLiteral(node) || Node33.isNoSubstitutionTemplateLiteral(node)) {
@@ -7812,7 +8020,7 @@ function handlerName(value) {
7812
8020
  }
7813
8021
 
7814
8022
  // src/discovery/registrationTemplate.ts
7815
- import path6 from "path";
8023
+ import path7 from "path";
7816
8024
  import { Node as Node35 } from "ts-morph";
7817
8025
  import { joinMountedPath as joinMountedPath2 } from "@suss/resolution";
7818
8026
 
@@ -7821,6 +8029,10 @@ import {
7821
8029
  Node as Node34,
7822
8030
  SyntaxKind as SyntaxKind7
7823
8031
  } from "ts-morph";
8032
+ import {
8033
+ unfollowedCallGap,
8034
+ worthRecording
8035
+ } from "@suss/behavioral-ir";
7824
8036
  function isInExternalCode(sourceFile) {
7825
8037
  if (sourceFile.isDeclarationFile()) {
7826
8038
  return true;
@@ -7874,6 +8086,19 @@ function declarationsBehind(symbol) {
7874
8086
  function isParameterOf(declaration, scanning) {
7875
8087
  return Node34.isParameterDeclaration(declaration) && declaration.getParent() === scanning;
7876
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
+ }
7877
8102
  function classifyStop(declarations, scanning) {
7878
8103
  if (declarations.length === 0) {
7879
8104
  return "noDeclaration";
@@ -7894,36 +8119,6 @@ function classifyStop(declarations, scanning) {
7894
8119
  }
7895
8120
  return "unsettledValue";
7896
8121
  }
7897
- var RECORDED = {
7898
- noBody: true,
7899
- unsettledValue: true,
7900
- multipleSources: true,
7901
- outsideRun: false,
7902
- noDeclaration: false,
7903
- callerSupplied: false,
7904
- multipleReceivers: true
7905
- };
7906
- function worthRecording(reason) {
7907
- return RECORDED[reason];
7908
- }
7909
- var STOP_SENTENCE = {
7910
- noBody: ({ callee }) => `The call to ${callee} lands on a declaration with no body, so whatever runs there is missing from this summary`,
7911
- unsettledValue: ({ callee }) => `The call to ${callee} goes through a value this run could not settle, so whatever runs there is missing from this summary`,
7912
- multipleSources: ({ callee }) => `The call to ${callee} reaches a value with more than one possible source, so whatever runs there is missing from this summary`,
7913
- 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`,
7914
- noDeclaration: ({ callee }) => `The call to ${callee} has no declaration this run could find, so whatever runs there is missing from this summary`,
7915
- 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`,
7916
- 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`
7917
- };
7918
- function unfollowedCallGap(stop) {
7919
- return {
7920
- type: "unfollowedCall",
7921
- conditions: [],
7922
- consequence: "unknown",
7923
- description: STOP_SENTENCE[stop.reason](stop),
7924
- callee: stop.callee
7925
- };
7926
- }
7927
8122
 
7928
8123
  // src/discovery/registrationTemplate.ts
7929
8124
  function discoverRegistrationTemplates(sourceFile, match, kind, resolution, mountPrefixes) {
@@ -7933,8 +8128,8 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution, moun
7933
8128
  const subject = subjectOfCall(sourceFile, args, match, resolution);
7934
8129
  const prefix = subject === void 0 ? "" : mountPrefixes?.effectivePrefixFor(subject) ?? "";
7935
8130
  for (const reg of match.registrations) {
7936
- const path15 = substitutePath(reg.pathTemplate, args);
7937
- if (path15 === null) {
8131
+ const path14 = substitutePath(reg.pathTemplate, args);
8132
+ if (path14 === null) {
7938
8133
  continue;
7939
8134
  }
7940
8135
  const handler = resolveHandler(reg.handlerArg, args, resolution);
@@ -7948,7 +8143,7 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution, moun
7948
8143
  ...subject === void 0 ? {} : { registrationSubjectId: nodeId(subject) },
7949
8144
  routeInfo: {
7950
8145
  method: reg.method.toUpperCase(),
7951
- path: prefix === "" ? path15 : joinMountedPath2(prefix, path15)
8146
+ path: prefix === "" ? path14 : joinMountedPath2(prefix, path14)
7952
8147
  }
7953
8148
  });
7954
8149
  }
@@ -7981,7 +8176,7 @@ function subjectOfCall(sourceFile, args, match, resolution) {
7981
8176
  }
7982
8177
  function helperCallsOf(sourceFile, match, resolution) {
7983
8178
  const module = match.importModule;
7984
- if (module !== void 0 && !path6.isAbsolute(module)) {
8179
+ if (module !== void 0 && !path7.isAbsolute(module)) {
7985
8180
  const store = resolution ?? new ResolutionStore();
7986
8181
  return callsResolvingTo(sourceFile, store, {
7987
8182
  module,
@@ -8427,24 +8622,24 @@ function declName(decl) {
8427
8622
 
8428
8623
  // src/parameterReads.ts
8429
8624
  function pathAbove(reference) {
8430
- const path15 = [];
8625
+ const path14 = [];
8431
8626
  let current2 = reference;
8432
8627
  for (; ; ) {
8433
8628
  const parent = current2.getParent();
8434
8629
  if (parent !== void 0 && Node38.isPropertyAccessExpression(parent) && parent.getExpression() === current2) {
8435
- path15.push(parent.getName());
8630
+ path14.push(parent.getName());
8436
8631
  current2 = parent;
8437
8632
  continue;
8438
8633
  }
8439
8634
  if (parent !== void 0 && Node38.isElementAccessExpression(parent) && parent.getExpression() === current2) {
8440
8635
  const argument = parent.getArgumentExpression();
8441
8636
  if (argument !== void 0 && Node38.isStringLiteral(argument)) {
8442
- path15.push(argument.getLiteralValue());
8637
+ path14.push(argument.getLiteralValue());
8443
8638
  current2 = parent;
8444
8639
  continue;
8445
8640
  }
8446
8641
  }
8447
- return path15;
8642
+ return path14;
8448
8643
  }
8449
8644
  }
8450
8645
  function declaresParameterOf(node, func) {
@@ -9143,142 +9338,40 @@ function importSpecifierMatches(decl, expectedModule) {
9143
9338
  return Node41.isImportDeclaration(importDecl) && importDecl.getModuleSpecifierValue() === expectedModule;
9144
9339
  }
9145
9340
  function extractInvocationEffects(func, barriers = NO_BARRIERS) {
9146
- const results = [];
9341
+ const calls = [];
9147
9342
  func.forEachDescendant((node, traversal) => {
9148
- if (isDescentStop(node, func, barriers)) {
9343
+ if (isDescentStop(node, func, barriers) || Node41.isDecorator(node)) {
9149
9344
  traversal.skip();
9150
9345
  return;
9151
9346
  }
9152
- if (Node41.isExpressionStatement(node)) {
9153
- const { call, async } = unwrapCall(node.getExpression());
9154
- if (call !== null) {
9155
- const preconditions = collectPreconditions(node, func);
9156
- results.push({
9157
- effect: {
9158
- type: "invocation",
9159
- callee: call.getExpression().getText(),
9160
- args: extractArgs(call),
9161
- async,
9162
- ...preconditions.length > 0 ? { preconditions } : {}
9163
- },
9164
- line: startLineOf(node),
9165
- neverTerminal: false,
9166
- alwaysRuns: runsOnEveryPath(node, func)
9167
- });
9168
- }
9169
- return;
9170
- }
9171
- if (Node41.isSpreadElement(node)) {
9172
- const parent = node.getParent();
9173
- if (parent !== void 0 && (Node41.isArrayLiteralExpression(parent) || Node41.isObjectLiteralExpression(parent))) {
9174
- const { call, async } = unwrapCall(node.getExpression());
9175
- if (call !== null) {
9176
- const preconditions = collectPreconditions(node, func);
9177
- results.push({
9178
- effect: {
9179
- type: "invocation",
9180
- callee: call.getExpression().getText(),
9181
- args: extractArgs(call),
9182
- async,
9183
- ...preconditions.length > 0 ? { preconditions } : {}
9184
- },
9185
- line: enclosingStatementLine(node),
9186
- neverTerminal: true,
9187
- alwaysRuns: runsOnEveryPath(node, func)
9188
- });
9189
- }
9190
- }
9191
- return;
9192
- }
9193
- if (Node41.isVariableDeclaration(node)) {
9194
- const initializer = node.getInitializer();
9195
- if (initializer !== void 0) {
9196
- const { call, async } = unwrapCall(initializer);
9197
- if (call !== null) {
9198
- const preconditions = collectPreconditions(node, func);
9199
- results.push({
9200
- effect: {
9201
- type: "invocation",
9202
- callee: call.getExpression().getText(),
9203
- args: extractArgs(call),
9204
- async,
9205
- ...preconditions.length > 0 ? { preconditions } : {}
9206
- },
9207
- line: enclosingStatementLine(node),
9208
- neverTerminal: true,
9209
- alwaysRuns: runsOnEveryPath(node, func)
9210
- });
9211
- }
9212
- }
9213
- return;
9214
- }
9215
- if (Node41.isReturnStatement(node)) {
9216
- const returned = node.getExpression();
9217
- const { call, async } = returned === void 0 ? { call: null, async: false } : unwrapCall(returned);
9218
- if (call !== null) {
9219
- const preconditions = collectPreconditions(node, func);
9220
- results.push({
9221
- effect: {
9222
- type: "invocation",
9223
- callee: call.getExpression().getText(),
9224
- args: extractArgs(call),
9225
- async,
9226
- ...preconditions.length > 0 ? { preconditions } : {}
9227
- },
9228
- line: startLineOf(node),
9229
- neverTerminal: false,
9230
- alwaysRuns: runsOnEveryPath(node, func),
9231
- node: call
9232
- });
9233
- }
9234
- return;
9235
- }
9236
- if (Node41.isArrowFunction(node)) {
9237
- const body = node.getBody();
9238
- const { call, async } = Node41.isExpression(body) ? unwrapCall(body) : { call: null, async: false };
9239
- if (call !== null) {
9240
- const preconditions = collectPreconditions(node, func);
9241
- results.push({
9242
- effect: {
9243
- type: "invocation",
9244
- callee: call.getExpression().getText(),
9245
- args: extractArgs(call),
9246
- async: async || node.isAsync(),
9247
- ...preconditions.length > 0 ? { preconditions } : {}
9248
- },
9249
- line: startLineOf(node),
9250
- neverTerminal: false,
9251
- alwaysRuns: runsOnEveryPath(node, func),
9252
- node: call
9253
- });
9254
- }
9255
- return;
9256
- }
9257
9347
  if (Node41.isCallExpression(node)) {
9258
- const parent = node.getParent();
9259
- if (parent === void 0) {
9260
- return;
9261
- }
9262
- const isArrayElement = Node41.isArrayLiteralExpression(parent);
9263
- const isPropertyValue = Node41.isPropertyAssignment(parent) && parent.getInitializer() === node;
9264
- if (isArrayElement || isPropertyValue) {
9265
- const preconditions = collectPreconditions(node, func);
9266
- results.push({
9267
- effect: {
9268
- type: "invocation",
9269
- callee: node.getExpression().getText(),
9270
- args: extractArgs(node),
9271
- async: false,
9272
- ...preconditions.length > 0 ? { preconditions } : {}
9273
- },
9274
- line: enclosingStatementLine(node),
9275
- neverTerminal: true,
9276
- alwaysRuns: runsOnEveryPath(node, func)
9277
- });
9278
- }
9348
+ calls.push(node);
9279
9349
  }
9280
9350
  });
9281
- 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);
9282
9375
  }
9283
9376
  function runInvocationRecognizers(func, recognizers, barriers = NO_BARRIERS, resolveWrittenValue, originatesFrom, anchorCallsOf) {
9284
9377
  if (recognizers.length === 0) {
@@ -9621,22 +9714,6 @@ function enclosingStatementLine(node) {
9621
9714
  }
9622
9715
  return startLineOf(node);
9623
9716
  }
9624
- function unwrapCall(node) {
9625
- if (Node41.isAwaitExpression(node)) {
9626
- const inner = node.getExpression();
9627
- if (Node41.isCallExpression(inner)) {
9628
- return { call: inner, async: true };
9629
- }
9630
- return { call: null, async: false };
9631
- }
9632
- if (Node41.isParenthesizedExpression(node)) {
9633
- return unwrapCall(node.getExpression());
9634
- }
9635
- if (Node41.isCallExpression(node)) {
9636
- return { call: node, async: false };
9637
- }
9638
- return { call: null, async: false };
9639
- }
9640
9717
 
9641
9718
  // src/terminals/index.ts
9642
9719
  import { Node as Node51 } from "ts-morph";
@@ -11678,6 +11755,22 @@ function firesOn(site, branch) {
11678
11755
  }
11679
11756
  return site.alwaysRuns || runsBefore(site.line, branch.terminal.location.end);
11680
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
+ }
11681
11774
  var isDefaultConditionList = (conditions) => conditions.length === 0 || conditions.every(
11682
11775
  (c) => c.source === "earlyReturn" || c.source === "earlyThrow"
11683
11776
  );
@@ -11816,11 +11909,8 @@ function extractRawBranches(func, terminalPatterns, invocationRecognizers = [],
11816
11909
  });
11817
11910
  if (invocations.length > 0) {
11818
11911
  const terminalNodes = new Set(terminals.map(({ node }) => node));
11819
- const terminalLines = new Set(
11820
- distinctBranches.map((b) => b.terminal.location.start)
11821
- );
11822
11912
  const sideEffects = invocations.filter(
11823
- (i) => i.node !== void 0 ? !terminalNodes.has(i.node) : i.neverTerminal || !terminalLines.has(i.line)
11913
+ (i) => !writesTerminal(i.node, terminalNodes)
11824
11914
  );
11825
11915
  for (const branch of distinctBranches) {
11826
11916
  branch.effects = sideEffects.filter(
@@ -11866,9 +11956,9 @@ function createSourceFileLookup(project) {
11866
11956
  if (sf === null) {
11867
11957
  return null;
11868
11958
  }
11869
- const path15 = sf.getFilePath();
11870
- const index = functionsByFile.get(path15) ?? indexFunctions(sf);
11871
- functionsByFile.set(path15, index);
11959
+ const path14 = sf.getFilePath();
11960
+ const index = functionsByFile.get(path14) ?? indexFunctions(sf);
11961
+ functionsByFile.set(path14, index);
11872
11962
  if (location.span !== void 0) {
11873
11963
  const bySpan = index.get(spanKey(location.span));
11874
11964
  if (bySpan !== void 0) {
@@ -11913,397 +12003,47 @@ function indexFunctions(sf) {
11913
12003
  return index;
11914
12004
  }
11915
12005
 
11916
- // src/cache.ts
11917
- import { createHash as createHash2 } from "crypto";
11918
- import fs5 from "fs/promises";
11919
- import path7 from "path";
11920
- var SCHEMA_VERSION = "6";
11921
- var MAX_ENTRIES = 2;
11922
- var ENTRY_PREFIX = "key-";
11923
- var ENTRY_DIR_NAME = new RegExp(`^${ENTRY_PREFIX}[0-9a-f]{16}$`);
11924
- function createCacheLayer(cacheDir) {
11925
- if (cacheDir === null) {
11926
- return {
11927
- tryHit: async () => null,
11928
- lookup: async () => ({
11929
- kind: "miss",
11930
- diagnostic: { kind: "miss", missReason: "no-manifest" }
11931
- }),
11932
- plan: async () => null,
11933
- write: async () => {
11934
- }
11935
- };
11936
- }
11937
- return {
11938
- async tryHit(input) {
11939
- const result = await this.lookup(input);
11940
- return result.kind === "hit" ? result.summaries : null;
11941
- },
11942
- async lookup(input) {
11943
- const entryDir = entryDirFor(cacheDir, input);
11944
- const manifest = await readManifest(path7.join(entryDir, "manifest.json"));
11945
- if (manifest === null) {
11946
- return missDiag(await describeAbsentEntry(cacheDir));
11947
- }
11948
- const currentTsconfigStamp = await stampTsconfig(input.tsconfigPath);
11949
- if (!fileStampEquals(manifest.tsconfigStamp, currentTsconfigStamp)) {
11950
- return missDiag("tsconfig-changed");
11951
- }
11952
- const currentFiles = await resolveFileStamps(input);
11953
- if (!fileStampsEqual(manifest.files, currentFiles)) {
11954
- return missDiag("files-changed");
11955
- }
11956
- await markUsed(entryDir);
11957
- return {
11958
- kind: "hit",
11959
- summaries: manifest.summaries,
11960
- diagnostic: { kind: "hit" }
11961
- };
11962
- },
11963
- async plan(input) {
11964
- const entryDir = entryDirFor(cacheDir, input);
11965
- const manifest = await readManifest(path7.join(entryDir, "manifest.json"));
11966
- if (manifest === null || manifest.roots === void 0 || manifest.rootMeta === void 0 || manifest.owners === void 0) {
11967
- return null;
11968
- }
11969
- const currentTsconfigStamp = await stampTsconfig(input.tsconfigPath);
11970
- if (!fileStampEquals(manifest.tsconfigStamp, currentTsconfigStamp)) {
11971
- return null;
11972
- }
11973
- await markUsed(entryDir);
11974
- return buildPlan(manifest, await resolveFileStamps(input));
11975
- },
11976
- async write(input, summaries, attribution) {
11977
- const entryDir = entryDirFor(cacheDir, input);
11978
- const previous = await readManifest(path7.join(entryDir, "manifest.json"));
11979
- const tsconfigStamp = await stampTsconfig(input.tsconfigPath);
11980
- const files = await hashStamps(await resolveFileStamps(input), previous);
11981
- const manifest = {
11982
- schemaVersion: SCHEMA_VERSION,
11983
- adapterPacksDigest: input.adapterPacksDigest,
11984
- tsconfigStamp,
11985
- files,
11986
- summaries,
11987
- ...attribution === void 0 ? {} : encodeAttribution(attribution)
11988
- };
11989
- await fs5.mkdir(entryDir, { recursive: true });
11990
- await fs5.writeFile(
11991
- path7.join(entryDir, "manifest.json"),
11992
- JSON.stringify(manifest)
11993
- );
11994
- await evictOldEntries(cacheDir, entryDir);
11995
- }
11996
- };
11997
- }
11998
- async function buildPlan(manifest, currentStamps) {
11999
- const stored = new Map(manifest.files.map((f) => [f.path, f]));
12000
- const current2 = new Map(currentStamps.map((f) => [f.path, f]));
12001
- const changed = /* @__PURE__ */ new Set();
12002
- const removed = /* @__PURE__ */ new Set();
12003
- for (const p of stored.keys()) {
12004
- if (!current2.has(p)) {
12005
- removed.add(p);
12006
- }
12007
- }
12008
- const toVerify = [];
12009
- for (const [p, stamp] of current2) {
12010
- const before = stored.get(p);
12011
- if (before === void 0) {
12012
- changed.add(p);
12013
- } else if (!fileStampEquals(before, stamp)) {
12014
- if (before.contentHash === void 0) {
12015
- changed.add(p);
12016
- } else {
12017
- toVerify.push(p);
12018
- }
12019
- }
12006
+ // src/contract.ts
12007
+ import { Node as Node54 } from "ts-morph";
12008
+ import { restBinding } from "@suss/behavioral-ir";
12009
+ function extractDeclaredBody(node) {
12010
+ if (node === void 0) {
12011
+ return null;
12020
12012
  }
12021
- await Promise.all(
12022
- toVerify.map(async (p) => {
12023
- const hash = await hashFile(p);
12024
- if (hash === null || hash !== stored.get(p)?.contentHash) {
12025
- changed.add(p);
12026
- }
12027
- })
12028
- );
12029
- const roots = /* @__PURE__ */ new Map();
12030
- const rootNames = manifest.roots ?? [];
12031
- const depPaths = manifest.depPaths ?? [];
12032
- let rootsDeclined = 0;
12033
- rootNames.forEach((rootPath, i) => {
12034
- const meta = manifest.rootMeta?.[i];
12035
- if (meta === void 0) {
12036
- return;
12037
- }
12038
- if (!meta.cacheable) {
12039
- rootsDeclined += 1;
12040
- }
12041
- roots.set(rootPath, {
12042
- path: rootPath,
12043
- cacheable: meta.cacheable,
12044
- deps: meta.deps.flatMap((d) => {
12045
- const p = depPaths[d];
12046
- return p === void 0 ? [] : [p];
12047
- }),
12048
- claims: meta.claims,
12049
- mountPrefixes: meta.mountPrefixes,
12050
- packs: meta.packs
12051
- });
12052
- });
12053
- const validRoots = /* @__PURE__ */ new Set();
12054
- for (const [rootPath, record2] of roots) {
12055
- if (!record2.cacheable || !current2.has(rootPath)) {
12056
- continue;
12057
- }
12058
- if (changed.has(rootPath)) {
12059
- continue;
12060
- }
12061
- const depMoved = record2.deps.some((d) => changed.has(d) || removed.has(d));
12062
- if (!depMoved) {
12063
- validRoots.add(rootPath);
12064
- }
12013
+ if (!Node54.isCallExpression(node)) {
12014
+ return null;
12065
12015
  }
12066
- const owners = manifest.owners ?? [];
12067
- return {
12068
- changed,
12069
- removed,
12070
- roots,
12071
- validRoots,
12072
- rootsDeclined,
12073
- reuse(valid) {
12074
- const summaries = [];
12075
- const reusedOwners = [];
12076
- manifest.summaries.forEach((summary, i) => {
12077
- const ownerPaths = (owners[i] ?? []).flatMap((o) => {
12078
- const p = rootNames[o];
12079
- return p === void 0 ? [] : [p];
12080
- });
12081
- if (ownerPaths.some((p) => valid.has(p))) {
12082
- summaries.push(summary);
12083
- reusedOwners.push(ownerPaths.filter((p) => valid.has(p)));
12084
- }
12085
- });
12086
- return { summaries, owners: reusedOwners };
12087
- },
12088
- allSummaries() {
12089
- return manifest.summaries;
12090
- },
12091
- attribution() {
12092
- return {
12093
- roots: [...roots.values()],
12094
- owners: owners.map(
12095
- (ownerIds) => ownerIds.flatMap((o) => {
12096
- const p = rootNames[o];
12097
- return p === void 0 ? [] : [p];
12098
- })
12099
- )
12100
- };
12101
- }
12102
- };
12103
- }
12104
- function encodeAttribution(attribution) {
12105
- const roots = attribution.roots.map((r) => r.path);
12106
- const rootIndex = new Map(roots.map((p, i) => [p, i]));
12107
- const depIndex = /* @__PURE__ */ new Map();
12108
- const depPaths = [];
12109
- const depIdOf = (p) => {
12110
- const existing = depIndex.get(p);
12111
- if (existing !== void 0) {
12112
- return existing;
12113
- }
12114
- const id = depPaths.length;
12115
- depPaths.push(p);
12116
- depIndex.set(p, id);
12117
- return id;
12118
- };
12119
- const rootMeta = attribution.roots.map((r) => ({
12120
- cacheable: r.cacheable,
12121
- deps: r.deps.map(depIdOf),
12122
- claims: r.claims,
12123
- mountPrefixes: r.mountPrefixes,
12124
- packs: r.packs
12125
- }));
12126
- const owners = attribution.owners.map(
12127
- (ownerPaths) => ownerPaths.flatMap((p) => {
12128
- const i = rootIndex.get(p);
12129
- return i === void 0 ? [] : [i];
12130
- })
12131
- );
12132
- return { roots, rootMeta, depPaths, owners };
12133
- }
12134
- async function hashStamps(stamps, previous) {
12135
- const before = new Map((previous?.files ?? []).map((f) => [f.path, f]));
12136
- return Promise.all(
12137
- stamps.map(async (stamp) => {
12138
- const prior = before.get(stamp.path);
12139
- if (prior !== void 0 && prior.contentHash !== void 0 && fileStampEquals(prior, stamp)) {
12140
- return { ...stamp, contentHash: prior.contentHash };
12141
- }
12142
- const contentHash = await hashFile(stamp.path);
12143
- return contentHash === null ? stamp : { ...stamp, contentHash };
12144
- })
12145
- );
12146
- }
12147
- async function hashFile(filePath) {
12148
- try {
12149
- const content = await fs5.readFile(filePath);
12150
- return createHash2("sha256").update(content).digest("hex").slice(0, 16);
12151
- } catch {
12016
+ const typeArgs = node.getTypeArguments();
12017
+ if (typeArgs.length === 0) {
12152
12018
  return null;
12153
12019
  }
12020
+ return shapeFromNodeType(typeArgs[0]);
12154
12021
  }
12155
- async function describeAbsentEntry(cacheDir) {
12156
- try {
12157
- const entries = await fs5.readdir(cacheDir, { withFileTypes: true });
12158
- return entries.some((e) => e.isDirectory() && isEntryDir(e.name)) ? "key-changed" : "no-manifest";
12159
- } catch {
12160
- return "no-manifest";
12022
+ function resolveHandlerPropNode(func) {
12023
+ const parent = func.getParent();
12024
+ if (parent === void 0) {
12025
+ return null;
12161
12026
  }
12162
- }
12163
- function entryDirFor(cacheDir, input) {
12164
- const key = [
12165
- SCHEMA_VERSION,
12166
- input.adapterPacksDigest,
12167
- input.tsconfigPath ?? ""
12168
- ].join(" ");
12169
- const name = createHash2("sha256").update(key).digest("hex").slice(0, 16);
12170
- return path7.join(cacheDir, `${ENTRY_PREFIX}${name}`);
12171
- }
12172
- function isEntryDir(name) {
12173
- return ENTRY_DIR_NAME.test(name);
12174
- }
12175
- async function markUsed(entryDir) {
12176
- const now = /* @__PURE__ */ new Date();
12177
- try {
12178
- await fs5.utimes(entryDir, now, now);
12179
- } catch {
12027
+ if (Node54.isPropertyAssignment(parent)) {
12028
+ return parent;
12180
12029
  }
12181
- }
12182
- async function evictOldEntries(cacheDir, keepDir) {
12183
- try {
12184
- await fs5.rm(path7.join(cacheDir, "manifest.json"), { force: true });
12185
- const entries = await fs5.readdir(cacheDir, { withFileTypes: true });
12186
- const dirs = await Promise.all(
12187
- entries.filter((e) => e.isDirectory() && isEntryDir(e.name)).map(async (e) => {
12188
- const dir = path7.join(cacheDir, e.name);
12189
- const stat = await fs5.stat(dir);
12190
- return { dir, mtimeMs: stat.mtimeMs };
12191
- })
12192
- );
12193
- const doomed = dirs.filter((d) => d.dir !== keepDir).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_ENTRIES - 1);
12194
- await Promise.all(
12195
- doomed.map((d) => fs5.rm(d.dir, { recursive: true, force: true }))
12196
- );
12197
- } catch {
12030
+ if (Node54.isMethodDeclaration(func)) {
12031
+ return func;
12198
12032
  }
12033
+ return null;
12199
12034
  }
12200
- function missDiag(reason) {
12201
- return {
12202
- kind: "miss",
12203
- diagnostic: { kind: "miss", missReason: reason }
12204
- };
12205
- }
12206
- async function readManifest(manifestPath) {
12207
- try {
12208
- const raw = await fs5.readFile(manifestPath, "utf-8");
12209
- return JSON.parse(raw);
12210
- } catch {
12035
+ function findRouterCall(unit) {
12036
+ const func = unit.func;
12037
+ const propNode = func === null ? null : resolveHandlerPropNode(func);
12038
+ if (propNode === null) {
12211
12039
  return null;
12212
12040
  }
12213
- }
12214
- async function stampTsconfig(tsconfigPath) {
12215
- if (tsconfigPath === void 0) {
12041
+ const handlersObj = propNode.getParent();
12042
+ if (handlersObj === void 0 || !Node54.isObjectLiteralExpression(handlersObj)) {
12216
12043
  return null;
12217
12044
  }
12218
- try {
12219
- const stat = await fs5.stat(tsconfigPath);
12220
- return { path: tsconfigPath, mtimeMs: stat.mtimeMs, size: stat.size };
12221
- } catch {
12222
- return null;
12223
- }
12224
- }
12225
- async function resolveFileStamps(input) {
12226
- if (input.files !== void 0) {
12227
- return stampPaths(input.files);
12228
- }
12229
- if (input.project !== void 0) {
12230
- return stampProjectFiles(input.project);
12231
- }
12232
- return [];
12233
- }
12234
- async function stampProjectFiles(project) {
12235
- const files = project.getSourceFiles().filter((sf) => !sf.isDeclarationFile()).map((sf) => sf.getFilePath());
12236
- return stampPaths(files);
12237
- }
12238
- async function stampPaths(paths) {
12239
- const files = paths;
12240
- const stamped = await Promise.all(
12241
- files.map(async (p) => {
12242
- try {
12243
- const stat = await fs5.stat(p);
12244
- return { path: p, mtimeMs: stat.mtimeMs, size: stat.size };
12245
- } catch {
12246
- return { path: p, mtimeMs: -1, size: -1 };
12247
- }
12248
- })
12249
- );
12250
- stamped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
12251
- return stamped;
12252
- }
12253
- function fileStampsEqual(a, b) {
12254
- return a.length === b.length && a.every((s, i) => fileStampEquals(s, b[i]));
12255
- }
12256
- function fileStampEquals(a, b) {
12257
- if (a === null && b === null) {
12258
- return true;
12259
- }
12260
- if (a === null || b === null) {
12261
- return false;
12262
- }
12263
- return a.path === b.path && a.mtimeMs === b.mtimeMs && a.size === b.size;
12264
- }
12265
-
12266
- // src/contract.ts
12267
- import { Node as Node54 } from "ts-morph";
12268
- import { restBinding } from "@suss/behavioral-ir";
12269
- function extractDeclaredBody(node) {
12270
- if (node === void 0) {
12271
- return null;
12272
- }
12273
- if (!Node54.isCallExpression(node)) {
12274
- return null;
12275
- }
12276
- const typeArgs = node.getTypeArguments();
12277
- if (typeArgs.length === 0) {
12278
- return null;
12279
- }
12280
- return shapeFromNodeType(typeArgs[0]);
12281
- }
12282
- function resolveHandlerPropNode(func) {
12283
- const parent = func.getParent();
12284
- if (parent === void 0) {
12285
- return null;
12286
- }
12287
- if (Node54.isPropertyAssignment(parent)) {
12288
- return parent;
12289
- }
12290
- if (Node54.isMethodDeclaration(func)) {
12291
- return func;
12292
- }
12293
- return null;
12294
- }
12295
- function findRouterCall(unit) {
12296
- const func = unit.func;
12297
- const propNode = func === null ? null : resolveHandlerPropNode(func);
12298
- if (propNode === null) {
12299
- return null;
12300
- }
12301
- const handlersObj = propNode.getParent();
12302
- if (handlersObj === void 0 || !Node54.isObjectLiteralExpression(handlersObj)) {
12303
- return null;
12304
- }
12305
- const routerCall = handlersObj.getParent();
12306
- if (routerCall === void 0 || !Node54.isCallExpression(routerCall)) {
12045
+ const routerCall = handlersObj.getParent();
12046
+ if (routerCall === void 0 || !Node54.isCallExpression(routerCall)) {
12307
12047
  return null;
12308
12048
  }
12309
12049
  const args = routerCall.getArguments();
@@ -12405,7 +12145,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
12405
12145
  }
12406
12146
  const responses = [];
12407
12147
  let method;
12408
- let path15;
12148
+ let path14;
12409
12149
  for (const prop of endpointNode.getProperties()) {
12410
12150
  if (!Node54.isPropertyAssignment(prop)) {
12411
12151
  continue;
@@ -12420,7 +12160,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
12420
12160
  if (propName === pattern.pathProperty) {
12421
12161
  const val = prop.getInitializer();
12422
12162
  if (val !== void 0 && Node54.isStringLiteral(val)) {
12423
- path15 = val.getLiteralValue();
12163
+ path14 = val.getLiteralValue();
12424
12164
  }
12425
12165
  }
12426
12166
  if (propName === pattern.responseExtraction.property) {
@@ -12444,10 +12184,10 @@ function extractEndpointContract(endpointNode, pattern, framework) {
12444
12184
  if (responses.length === 0) {
12445
12185
  return null;
12446
12186
  }
12447
- const boundaryBinding = method !== void 0 || path15 !== void 0 ? restBinding({
12187
+ const boundaryBinding = method !== void 0 || path14 !== void 0 ? restBinding({
12448
12188
  transport: "http",
12449
12189
  method: method ?? null,
12450
- path: path15 ?? null,
12190
+ path: path14 ?? null,
12451
12191
  recognition: framework
12452
12192
  }) : null;
12453
12193
  return {
@@ -13086,8 +12826,8 @@ function noteExpanded(expanded, readings, discovery) {
13086
12826
  const paths = new Set(match.registrations.map((one) => one.pathTemplate));
13087
12827
  const methods = expanded.get(reading.declarationId) ?? /* @__PURE__ */ new Set();
13088
12828
  for (const sink of reading.helper.sinks) {
13089
- const path15 = sink.arguments[0];
13090
- 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)) {
13091
12831
  methods.add(sink.method);
13092
12832
  }
13093
12833
  }
@@ -13681,7 +13421,7 @@ function scalarOrRef(node) {
13681
13421
  }
13682
13422
 
13683
13423
  // src/moduleInit.ts
13684
- import { assembleSummary } from "@suss/extractor";
13424
+ import { assembleSummary, moduleInitStructure } from "@suss/extractor";
13685
13425
  function moduleInitName(sourceFile) {
13686
13426
  return sourceFile.getBaseName();
13687
13427
  }
@@ -13689,44 +13429,15 @@ function moduleInitSummary(sourceFile, effects, options) {
13689
13429
  if (effects.length === 0) {
13690
13430
  return null;
13691
13431
  }
13692
- const end = sourceFile.getEnd();
13693
- const raw = {
13694
- identity: {
13432
+ return assembleSummary(
13433
+ moduleInitStructure({
13695
13434
  name: moduleInitName(sourceFile),
13696
- kind: "module-init",
13697
13435
  file: sourceFile.getFilePath(),
13698
- range: { start: 0, end },
13699
- exportName: null,
13700
- exportPath: null
13701
- },
13702
- boundaryBinding: null,
13703
- parameters: [],
13704
- branches: [
13705
- {
13706
- conditions: [],
13707
- terminal: {
13708
- kind: "void",
13709
- statusCode: null,
13710
- body: null,
13711
- exceptionType: null,
13712
- message: null,
13713
- component: null,
13714
- renderTree: null,
13715
- delegateTarget: null,
13716
- emitEvent: null,
13717
- location: { start: 0, end }
13718
- },
13719
- effects: [],
13720
- extraEffects: effects,
13721
- location: { start: 0, end },
13722
- isDefault: true
13723
- }
13724
- ],
13725
- unmatchedReturns: 0,
13726
- dependencyCalls: [],
13727
- declaredContract: null
13728
- };
13729
- return assembleSummary(raw, options);
13436
+ range: { start: 0, end: sourceFile.getEnd() },
13437
+ effects
13438
+ }),
13439
+ options
13440
+ );
13730
13441
  }
13731
13442
 
13732
13443
  // src/referencedFiles.ts
@@ -13874,7 +13585,7 @@ function deriveBoundaryEffects(summaries, facts) {
13874
13585
 
13875
13586
  // src/resolve/reachableClosure.ts
13876
13587
  import { Node as Node60 } from "ts-morph";
13877
- import { functionCallBinding } from "@suss/behavioral-ir";
13588
+ import { functionCallBinding, TargetPlacements } from "@suss/behavioral-ir";
13878
13589
  import { Database as Database3, evaluate as evaluate4, lit as lit4, rule as rule4, variable as variable2 } from "@suss/datalog";
13879
13590
  import { assembleSummary as assembleSummary2 } from "@suss/extractor";
13880
13591
  var reachablePack = {
@@ -13898,6 +13609,12 @@ var reachablePack = {
13898
13609
  function nodeKey(func) {
13899
13610
  return offsetKeyOf(func);
13900
13611
  }
13612
+ function targetOf(node) {
13613
+ return {
13614
+ file: node.getSourceFile().getFilePath(),
13615
+ span: { start: node.getStart(), end: node.getEnd() }
13616
+ };
13617
+ }
13901
13618
  function resolveCallee(call, calleeName, scan) {
13902
13619
  if (!Node60.isCallExpression(call)) {
13903
13620
  return null;
@@ -13922,19 +13639,42 @@ function resolveCallee(call, calleeName, scan) {
13922
13639
  if (sources.length > 1) {
13923
13640
  return {
13924
13641
  kind: "stopped",
13925
- stop: { callee: calleeName, reason: "multipleSources" }
13642
+ stop: { callee: calleeName, reason: "multipleSources" },
13643
+ declaration: declarationToReport(declarations, calleeName, scan)
13926
13644
  };
13927
13645
  }
13928
13646
  }
13647
+ const reason = classifyStop(declarations, scan.scanning);
13648
+ const parameterIndex = reason === "callerSupplied" ? parameterIndexOf(declarations, scan.scanning) : void 0;
13929
13649
  return {
13930
13650
  kind: "stopped",
13931
- stop: {
13932
- callee: calleeName,
13933
- reason: classifyStop(declarationsBehind(symbol), scan.scanning)
13934
- }
13651
+ stop: { callee: calleeName, reason },
13652
+ declaration: declarationToReport(declarations, calleeName, scan),
13653
+ ...parameterIndex === void 0 ? {} : { parameterIndex }
13935
13654
  };
13936
13655
  }
13937
- 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) {
13938
13678
  if (!Node60.isJsxOpeningElement(node) && !Node60.isJsxSelfClosingElement(node)) {
13939
13679
  return null;
13940
13680
  }
@@ -13954,7 +13694,8 @@ function resolveJsxReference(node) {
13954
13694
  tag: name,
13955
13695
  outcome: {
13956
13696
  kind: "stopped",
13957
- stop: { callee: name, reason: classifyStop(declarations) }
13697
+ stop: { callee: name, reason: classifyStop(declarations) },
13698
+ declaration: declarationToReport(declarations, name, scan)
13958
13699
  }
13959
13700
  };
13960
13701
  }
@@ -13962,7 +13703,11 @@ function collectReachable(func, scan) {
13962
13703
  const inFunc = { ...scan, scanning: func };
13963
13704
  const candidates = [];
13964
13705
  const stops = [];
13706
+ const placements = new TargetPlacements();
13707
+ const parameterCalls = [];
13708
+ const passedPositions = /* @__PURE__ */ new Set();
13965
13709
  const seen = /* @__PURE__ */ new Set();
13710
+ const parameterCallsSeen = /* @__PURE__ */ new Set();
13966
13711
  const record2 = (outcome) => {
13967
13712
  if (outcome.kind === "stopped") {
13968
13713
  const stopKey = `${outcome.stop.reason}:${outcome.stop.callee}`;
@@ -13979,8 +13724,28 @@ function collectReachable(func, scan) {
13979
13724
  seen.add(key);
13980
13725
  candidates.push(outcome.candidate);
13981
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
+ };
13982
13747
  func.forEachDescendant((node) => {
13983
- const jsx = resolveJsxReference(node);
13748
+ const jsx = resolveJsxReference(node, inFunc);
13984
13749
  if (jsx !== null) {
13985
13750
  record2(jsx.outcome);
13986
13751
  return;
@@ -13994,8 +13759,24 @@ function collectReachable(func, scan) {
13994
13759
  return;
13995
13760
  }
13996
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
+ }
13997
13771
  });
13998
- return { candidates, stops };
13772
+ return {
13773
+ candidates,
13774
+ stops,
13775
+ targets: placements.targets,
13776
+ argTargets: placements.argTargets,
13777
+ parameterCalls,
13778
+ passedPositions
13779
+ };
13999
13780
  }
14000
13781
  function normalizeCallee(text) {
14001
13782
  return text.replace(/\s+/g, " ").trim();
@@ -14070,6 +13851,10 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14070
13851
  const seedKeys = /* @__PURE__ */ new Set();
14071
13852
  const scanned = /* @__PURE__ */ new Set();
14072
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();
14073
13858
  const summariesByKey = /* @__PURE__ */ new Map();
14074
13859
  const reachedFrom = /* @__PURE__ */ new Map();
14075
13860
  for (const seed of seeds) {
@@ -14114,9 +13899,17 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14114
13899
  const cameFrom = reachedFrom.get(key);
14115
13900
  const scan = {
14116
13901
  ...recognizers.resolveCallableSources === void 0 ? {} : { resolveCallableSources: recognizers.resolveCallableSources },
13902
+ ...recognizers.sourceDeclarationsBehind === void 0 ? {} : { sourceDeclarationsBehind: recognizers.sourceDeclarationsBehind },
14117
13903
  ...cameFrom === void 0 ? {} : { reachedFrom: cameFrom }
14118
13904
  };
14119
- const { candidates, stops } = scanWithRecording(
13905
+ const {
13906
+ candidates,
13907
+ stops,
13908
+ targets,
13909
+ argTargets,
13910
+ parameterCalls,
13911
+ passedPositions: scanPassedPositions
13912
+ } = scanWithRecording(
14120
13913
  key,
14121
13914
  facts,
14122
13915
  () => collectReachable(source.func, scan)
@@ -14124,6 +13917,14 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14124
13917
  if (stops.length > 0) {
14125
13918
  stopsByKey.set(key, stops);
14126
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
+ }
14127
13928
  for (const candidate of candidates) {
14128
13929
  const calleeKey = nodeKey(candidate.func);
14129
13930
  if (!functionByKey.has(calleeKey)) {
@@ -14163,6 +13964,15 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
14163
13964
  reached.push(summary);
14164
13965
  }
14165
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
+ );
14166
13976
  return [...seeds, ...reached];
14167
13977
  }
14168
13978
  function scanWithRecording(key, facts, fn) {
@@ -14194,6 +14004,90 @@ function recordStops(stopsByKey, summariesByKey, options) {
14194
14004
  }
14195
14005
  }
14196
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
+ }
14197
14091
 
14198
14092
  // src/resolve/rethrowEnrichment.ts
14199
14093
  import { Node as Node61 } from "ts-morph";
@@ -14929,10 +14823,10 @@ function readArrayLiteralText(node) {
14929
14823
  }
14930
14824
 
14931
14825
  // src/summaryIdentity.ts
14932
- import fs6 from "fs";
14933
14826
  import path8 from "path";
14934
14827
  import {
14935
14828
  disambiguateSummaryIds,
14829
+ linkCallsToSummaries,
14936
14830
  summaryIdFromParts
14937
14831
  } from "@suss/behavioral-ir";
14938
14832
  function workspaceRootFor(anchor) {
@@ -14949,32 +14843,6 @@ function workspaceNameFor(root) {
14949
14843
  const folder = path8.basename(path8.resolve(root));
14950
14844
  return folder.length > 0 ? folder : null;
14951
14845
  }
14952
- function namedPackageDirAbove(start) {
14953
- let at = path8.resolve(start);
14954
- for (let up = 0; up < 12; up += 1) {
14955
- if (packageNameAt(at) !== null) {
14956
- return at;
14957
- }
14958
- const parent = path8.dirname(at);
14959
- if (parent === at) {
14960
- break;
14961
- }
14962
- at = parent;
14963
- }
14964
- return null;
14965
- }
14966
- function packageNameAt(dir) {
14967
- try {
14968
- const manifest = JSON.parse(
14969
- fs6.readFileSync(path8.join(dir, "package.json"), "utf8")
14970
- );
14971
- if (typeof manifest.name === "string" && manifest.name.length > 0) {
14972
- return manifest.name;
14973
- }
14974
- } catch {
14975
- }
14976
- return null;
14977
- }
14978
14846
  function nameSummaries(summaries, args) {
14979
14847
  for (const summary of summaries) {
14980
14848
  const file = relativeFile(summary.location.file, args.projectRoot);
@@ -14984,7 +14852,7 @@ function nameSummaries(summaries, args) {
14984
14852
  summary.identity.id = idFor(args.workspace, file, summary);
14985
14853
  }
14986
14854
  disambiguateSummaryIds(summaries);
14987
- nameWhatEachCallReaches(summaries);
14855
+ linkCallsToSummaries(summaries);
14988
14856
  }
14989
14857
  function relativeFile(file, projectRoot) {
14990
14858
  if (projectRoot === void 0 || !path8.isAbsolute(file)) {
@@ -15001,185 +14869,35 @@ function idFor(workspace, file, summary) {
15001
14869
  exportPath: summary.identity.exportPath
15002
14870
  });
15003
14871
  }
15004
- function nameWhatEachCallReaches(summaries) {
15005
- const byName = /* @__PURE__ */ new Map();
15006
- const byFileAndName = /* @__PURE__ */ new Map();
15007
- const remember = (index, key, summary) => {
15008
- const found = index.get(key);
15009
- if (found === void 0) {
15010
- index.set(key, [summary]);
15011
- return;
15012
- }
15013
- found.push(summary);
15014
- };
15015
- for (const summary of summaries) {
15016
- if (summary.identity.nameKind === "label") {
15017
- continue;
15018
- }
15019
- remember(byName, summary.identity.name, summary);
15020
- remember(
15021
- byFileAndName,
15022
- `${summary.location.file}::${summary.identity.name}`,
15023
- summary
15024
- );
15025
- }
15026
- const onlyAnswer = (index, key) => {
15027
- const found = index.get(key) ?? [];
15028
- return found.length === 1 ? found[0] : null;
15029
- };
15030
- for (const summary of summaries) {
15031
- for (const transition of summary.transitions) {
15032
- for (const effect of transition.effects) {
15033
- if (effect.type !== "invocation") {
15034
- continue;
15035
- }
15036
- const called = effect.callee.split(".").pop() ?? effect.callee;
15037
- const reached = onlyAnswer(byFileAndName, `${summary.location.file}::${called}`) ?? onlyAnswer(byName, called);
15038
- if (reached?.identity.id !== void 0) {
15039
- effect.summary = reached.identity.id;
15040
- }
15041
- }
15042
- }
15043
- }
15044
- }
15045
14872
 
15046
14873
  // src/timing.ts
15047
- function createTimer() {
15048
- const phases = /* @__PURE__ */ new Map();
15049
- const start = performance.now();
15050
- function record2(label, deltaMs) {
15051
- const existing = phases.get(label);
15052
- if (existing === void 0) {
15053
- phases.set(label, { durationMs: deltaMs, calls: 1 });
15054
- } else {
15055
- existing.durationMs += deltaMs;
15056
- existing.calls += 1;
15057
- }
15058
- }
15059
- return {
15060
- time(label, fn) {
15061
- const t0 = performance.now();
15062
- try {
15063
- return fn();
15064
- } finally {
15065
- record2(label, performance.now() - t0);
15066
- }
15067
- },
15068
- async timeAsync(label, fn) {
15069
- const t0 = performance.now();
15070
- try {
15071
- return await fn();
15072
- } finally {
15073
- record2(label, performance.now() - t0);
15074
- }
15075
- },
15076
- report() {
15077
- const totalMs = performance.now() - start;
15078
- const ordered = [...phases.entries()].map(([label, s]) => ({
15079
- label,
15080
- durationMs: s.durationMs,
15081
- calls: s.calls
15082
- })).sort((a, b) => b.durationMs - a.durationMs);
15083
- return { totalMs, phases: ordered };
15084
- }
15085
- };
15086
- }
14874
+ import { createTimer, noopTimer } from "@suss/extractor";
15087
14875
 
15088
14876
  // src/version.ts
15089
- import { createHash as createHash3 } from "crypto";
15090
- import fs7 from "fs";
15091
- import { createRequire } from "module";
15092
- import path9 from "path";
15093
- import { fileURLToPath } from "url";
14877
+ import { createAdapterStamp } from "@suss/extractor";
14878
+ import {
14879
+ computeContentHash,
14880
+ computeDistHashFrom,
14881
+ projectFileStamp
14882
+ } from "@suss/extractor";
15094
14883
  var ADAPTER_VERSION = "0.2.1";
15095
- var ANALYSIS_PACKAGES = [
15096
- "@suss/extractor",
15097
- "@suss/resolution",
15098
- "@suss/datalog",
15099
- "@suss/behavioral-ir"
15100
- ];
15101
- var SOURCE_STAMP = { kind: "source" };
15102
- var cachedCodeStamp = null;
15103
- function adapterCodeStamp() {
15104
- if (cachedCodeStamp !== null) {
15105
- return cachedCodeStamp;
15106
- }
15107
- cachedCodeStamp = readAdapterCodeStamp();
15108
- return cachedCodeStamp;
15109
- }
15110
- function readAdapterCodeStamp() {
15111
- try {
15112
- const selfPath = fileURLToPath(import.meta.url);
15113
- const hash = computeDistHashFrom(path9.dirname(selfPath));
15114
- return hash.length > 0 ? { kind: "bundle", hash } : SOURCE_STAMP;
15115
- } catch {
15116
- return SOURCE_STAMP;
15117
- }
15118
- }
15119
- function computeDistHashFrom(dir) {
15120
- const candidates = [path9.join(dir, "index.js"), path9.join(dir, "index.cjs")];
15121
- for (const candidate of candidates) {
15122
- if (!fs7.existsSync(candidate)) {
15123
- continue;
15124
- }
15125
- return computeContentHash([candidate, ...analysisBundles()]);
15126
- }
15127
- return "";
15128
- }
15129
- function computeContentHash(paths) {
15130
- if (paths.length === 0) {
15131
- return "";
15132
- }
15133
- const hash = createHash3("sha256");
15134
- for (const file of paths) {
15135
- try {
15136
- hash.update(fs7.readFileSync(file));
15137
- } catch {
15138
- return "";
15139
- }
15140
- }
15141
- return hash.digest("hex").slice(0, 16);
15142
- }
15143
- function projectFileStamp(paths) {
15144
- if (paths.length === 0) {
15145
- return "none";
15146
- }
15147
- const hash = createHash3("sha256");
15148
- for (const file of [...paths].sort()) {
15149
- hash.update(file);
15150
- hash.update("\0");
15151
- try {
15152
- hash.update(fs7.readFileSync(file));
15153
- } catch {
15154
- hash.update("absent");
15155
- }
15156
- }
15157
- return hash.digest("hex").slice(0, 16);
15158
- }
15159
- function analysisBundles() {
15160
- const require2 = createRequire(import.meta.url);
15161
- const found = [];
15162
- for (const name of ANALYSIS_PACKAGES) {
15163
- try {
15164
- found.push(require2.resolve(name));
15165
- } catch {
15166
- }
15167
- }
15168
- return found;
15169
- }
14884
+ var stamp = createAdapterStamp({
14885
+ moduleUrl: import.meta.url,
14886
+ version: ADAPTER_VERSION
14887
+ });
15170
14888
  function computeAdapterPacksDigest(packVersions) {
15171
- const sortedPacks = [...packVersions].map((p) => `${p.name}@${p.version ?? "unset"}`).sort();
15172
- const stamp = adapterCodeStamp();
15173
- const adapterStamp = stamp.kind === "bundle" ? `adapter@${ADAPTER_VERSION}+${stamp.hash}` : `adapter@${ADAPTER_VERSION}+source`;
15174
- return `${adapterStamp}|${sortedPacks.join(",")}`;
14889
+ return stamp.packsDigest(packVersions);
14890
+ }
14891
+ function declineWhenRunFromSource(cacheDir) {
14892
+ return stamp.declineWhenRunFromSource(cacheDir);
15175
14893
  }
15176
14894
 
15177
14895
  // src/workspacePatterns.ts
15178
- import fs8 from "fs";
15179
- import path10 from "path";
14896
+ import fs4 from "fs";
14897
+ import path9 from "path";
15180
14898
  function readJson(filePath) {
15181
14899
  try {
15182
- return JSON.parse(fs8.readFileSync(filePath, "utf8"));
14900
+ return JSON.parse(fs4.readFileSync(filePath, "utf8"));
15183
14901
  } catch {
15184
14902
  return void 0;
15185
14903
  }
@@ -15206,7 +14924,7 @@ function pnpmWorkspaceGlobs(yamlText) {
15206
14924
  return globs;
15207
14925
  }
15208
14926
  function workspaceGlobsAt(dir) {
15209
- const pkg = readJson(path10.join(dir, "package.json"));
14927
+ const pkg = readJson(path9.join(dir, "package.json"));
15210
14928
  if (typeof pkg === "object" && pkg !== null && "workspaces" in pkg) {
15211
14929
  const ws = pkg.workspaces;
15212
14930
  if (Array.isArray(ws)) {
@@ -15218,10 +14936,10 @@ function workspaceGlobsAt(dir) {
15218
14936
  );
15219
14937
  }
15220
14938
  }
15221
- const pnpmPath = path10.join(dir, "pnpm-workspace.yaml");
15222
- if (fs8.existsSync(pnpmPath)) {
14939
+ const pnpmPath = path9.join(dir, "pnpm-workspace.yaml");
14940
+ if (fs4.existsSync(pnpmPath)) {
15223
14941
  try {
15224
- return pnpmWorkspaceGlobs(fs8.readFileSync(pnpmPath, "utf8"));
14942
+ return pnpmWorkspaceGlobs(fs4.readFileSync(pnpmPath, "utf8"));
15225
14943
  } catch {
15226
14944
  return null;
15227
14945
  }
@@ -15230,14 +14948,14 @@ function workspaceGlobsAt(dir) {
15230
14948
  }
15231
14949
  function isDirectory(candidate) {
15232
14950
  try {
15233
- return fs8.statSync(candidate).isDirectory();
14951
+ return fs4.statSync(candidate).isDirectory();
15234
14952
  } catch {
15235
14953
  return false;
15236
14954
  }
15237
14955
  }
15238
14956
  function subdirectoriesOf(dir) {
15239
14957
  try {
15240
- 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");
15241
14959
  } catch {
15242
14960
  return [];
15243
14961
  }
@@ -15255,13 +14973,13 @@ function directoriesMatchingGlob(root, glob) {
15255
14973
  for (const dir of current2) {
15256
14974
  for (const entry of subdirectoriesOf(dir)) {
15257
14975
  if (pattern.test(entry)) {
15258
- next.push(path10.join(dir, entry));
14976
+ next.push(path9.join(dir, entry));
15259
14977
  }
15260
14978
  }
15261
14979
  }
15262
14980
  } else {
15263
14981
  for (const dir of current2) {
15264
- next.push(path10.join(dir, segment));
14982
+ next.push(path9.join(dir, segment));
15265
14983
  }
15266
14984
  }
15267
14985
  current2 = next.filter(isDirectory);
@@ -15269,7 +14987,7 @@ function directoriesMatchingGlob(root, glob) {
15269
14987
  return current2;
15270
14988
  }
15271
14989
  function packageAt(dir) {
15272
- const packageJsonPath = path10.join(dir, "package.json");
14990
+ const packageJsonPath = path9.join(dir, "package.json");
15273
14991
  const pkg = readJson(packageJsonPath);
15274
14992
  if (typeof pkg === "object" && pkg !== null && typeof pkg.name === "string") {
15275
14993
  return { name: pkg.name, packageJsonPath };
@@ -15277,7 +14995,7 @@ function packageAt(dir) {
15277
14995
  return null;
15278
14996
  }
15279
14997
  function workspacePackagesFrom(anchor) {
15280
- let dir = path10.resolve(anchor);
14998
+ let dir = path9.resolve(anchor);
15281
14999
  for (; ; ) {
15282
15000
  const globs = workspaceGlobsAt(dir);
15283
15001
  if (globs !== null) {
@@ -15297,7 +15015,7 @@ function workspacePackagesFrom(anchor) {
15297
15015
  (a, b) => a.packageJsonPath.localeCompare(b.packageJsonPath)
15298
15016
  );
15299
15017
  }
15300
- const parent = path10.dirname(dir);
15018
+ const parent = path9.dirname(dir);
15301
15019
  if (parent === dir) {
15302
15020
  return [];
15303
15021
  }
@@ -15839,11 +15557,11 @@ function extractConsumerBinding(unit, pattern, pack, resolution) {
15839
15557
  return null;
15840
15558
  }
15841
15559
  const method = extractBindingMethod(binding, callSite, pack, resolution);
15842
- const path15 = extractBindingPath(binding, callSite, pack, resolution);
15560
+ const path14 = extractBindingPath(binding, callSite, pack, resolution);
15843
15561
  return restBinding2({
15844
15562
  transport: pack.protocol,
15845
15563
  method: method ?? null,
15846
- path: path15 ?? null,
15564
+ path: path14 ?? null,
15847
15565
  recognition: pack.name
15848
15566
  });
15849
15567
  }
@@ -16319,24 +16037,6 @@ function stampWrappers(raw, unit, wrappers) {
16319
16037
  recordFileDependency(wrapper.file);
16320
16038
  }
16321
16039
  }
16322
- function stampModuleImports(summaries, project) {
16323
- const byFile2 = /* @__PURE__ */ new Map();
16324
- for (const summary of summaries) {
16325
- const file = summary.location.file;
16326
- let imports = byFile2.get(file);
16327
- if (imports === void 0) {
16328
- imports = internalImportsOf(project, file);
16329
- byFile2.set(file, imports);
16330
- }
16331
- if (imports.length === 0) {
16332
- continue;
16333
- }
16334
- summary.metadata = {
16335
- ...summary.metadata ?? {},
16336
- moduleImports: imports
16337
- };
16338
- }
16339
- }
16340
16040
  function emitLibraryEnvReadMarkers(summaries, project, frameworks) {
16341
16041
  for (const pack of frameworks) {
16342
16042
  for (const declared of pack.libraryEnvVars ?? []) {
@@ -16511,8 +16211,8 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
16511
16211
  if (pathArg === void 0) {
16512
16212
  continue;
16513
16213
  }
16514
- const path15 = pathFromArgument(pathArg, resolution);
16515
- if (path15 === void 0) {
16214
+ const path14 = pathFromArgument(pathArg, resolution);
16215
+ if (path14 === void 0) {
16516
16216
  continue;
16517
16217
  }
16518
16218
  const callerFunc = enclosingFunction(callExpr);
@@ -16524,7 +16224,7 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
16524
16224
  continue;
16525
16225
  }
16526
16226
  seen.add(dedupKey);
16527
- out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path15, options));
16227
+ out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path14, options));
16528
16228
  }
16529
16229
  return out;
16530
16230
  }
@@ -16581,7 +16281,7 @@ function callerName(func) {
16581
16281
  }
16582
16282
  return "anonymous";
16583
16283
  }
16584
- function buildCallerSummary(wrapper, callerFunc, callExpr, path15, options) {
16284
+ function buildCallerSummary(wrapper, callerFunc, callExpr, path14, options) {
16585
16285
  const wrapperBinding = wrapper.summary.identity.boundaryBinding;
16586
16286
  const wrapperRest = wrapperBinding?.semantics.name === "rest" ? wrapperBinding.semantics : null;
16587
16287
  const syntheticPack = {
@@ -16613,7 +16313,7 @@ function buildCallerSummary(wrapper, callerFunc, callExpr, path15, options) {
16613
16313
  raw.boundaryBinding = restBinding2({
16614
16314
  transport: wrapperBinding?.transport ?? "http",
16615
16315
  method: wrapperRest?.method ?? null,
16616
- path: path15,
16316
+ path: path14,
16617
16317
  recognition: wrapperBinding?.recognition ?? "unknown"
16618
16318
  });
16619
16319
  const summary = assembleSummary3(raw, options);
@@ -16635,25 +16335,12 @@ function named(summaries, workspace, runRoot) {
16635
16335
  });
16636
16336
  return summaries;
16637
16337
  }
16638
- var saidWhyNoCache = false;
16639
16338
  function extractionConfigStamp(config) {
16640
16339
  return [
16641
16340
  `includeReachable=${config.includeReachable !== false}`,
16642
16341
  `gapHandling=${config.extractorOptions?.gapHandling ?? "default"}`
16643
16342
  ].join(",");
16644
16343
  }
16645
- function declineWhenRunFromSource(cacheDir) {
16646
- if (cacheDir === null || adapterCodeStamp().kind === "bundle") {
16647
- return cacheDir;
16648
- }
16649
- if (!saidWhyNoCache) {
16650
- saidWhyNoCache = true;
16651
- process.stderr.write(
16652
- "[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"
16653
- );
16654
- }
16655
- return null;
16656
- }
16657
16344
  function loadRunFiles(project, candidatePaths, timer) {
16658
16345
  if (candidatePaths !== null) {
16659
16346
  const deep2 = timer.time(
@@ -16684,7 +16371,7 @@ function createTypeScriptAdapter(suppliedConfig) {
16684
16371
  ...suppliedConfig,
16685
16372
  frameworks: expandWorkspacePatterns(
16686
16373
  suppliedConfig.frameworks,
16687
- 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)
16688
16375
  )
16689
16376
  };
16690
16377
  const project = config.project ?? new Project(
@@ -16693,11 +16380,11 @@ function createTypeScriptAdapter(suppliedConfig) {
16693
16380
  skipAddingFilesFromTsConfig: true
16694
16381
  } : { skipAddingFilesFromTsConfig: true }
16695
16382
  );
16696
- 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);
16697
16384
  let lazyBootstrapped = false;
16698
16385
  let projectFileSet;
16699
16386
  const cacheDir = declineWhenRunFromSource(
16700
- 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)
16701
16388
  );
16702
16389
  const cache = createCacheLayer(cacheDir);
16703
16390
  const packsDigest = `${computeAdapterPacksDigest(
@@ -16705,12 +16392,6 @@ function createTypeScriptAdapter(suppliedConfig) {
16705
16392
  (p) => p.version !== void 0 ? { name: p.name, version: p.version } : { name: p.name }
16706
16393
  )
16707
16394
  )}|${extractionConfigStamp(config)}|ws:${workspaceExpansionStamp(config.frameworks)}`;
16708
- const digestFor = (files) => {
16709
- const inputs = config.frameworks.flatMap(
16710
- (pack) => pack.discoveryInputs?.(files) ?? []
16711
- );
16712
- return `${packsDigest}|reads:${projectFileStamp(inputs)}`;
16713
- };
16714
16395
  const packWrappers = config.frameworks.flatMap(
16715
16396
  (pack) => pack.transparentWrappers ?? []
16716
16397
  );
@@ -16754,20 +16435,17 @@ function createTypeScriptAdapter(suppliedConfig) {
16754
16435
  config.tsConfigFilePath ?? raise("lazy bootstrap requires tsConfigFilePath")
16755
16436
  )
16756
16437
  ) : null;
16757
- const adapterPacksDigest = cacheDir === null ? packsDigest : timer.time(
16758
- "cache.digest",
16759
- () => digestFor(
16760
- tsconfigFileList ?? project.getSourceFiles().map((sf) => sf.getFilePath())
16761
- )
16762
- );
16763
- const cacheInput = tsconfigFileList !== null ? {
16764
- files: tsconfigFileList,
16765
- adapterPacksDigest,
16766
- tsconfigPath: config.tsConfigFilePath ?? raise("lazy bootstrap requires tsConfigFilePath")
16767
- } : {
16768
- 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 ?? [],
16769
16447
  adapterPacksDigest,
16770
- ...config.tsConfigFilePath !== void 0 ? { tsconfigPath: config.tsConfigFilePath } : {}
16448
+ ...config.tsConfigFilePath !== void 0 ? { configPath: config.tsConfigFilePath } : {}
16771
16449
  };
16772
16450
  const lookup = await timer.timeAsync(
16773
16451
  "cache.lookup",
@@ -16973,7 +16651,8 @@ function createTypeScriptAdapter(suppliedConfig) {
16973
16651
  ],
16974
16652
  access: collectAccessRecognizers(config.frameworks, tallies),
16975
16653
  resolveWrittenValue: (value) => resolution.resolveWrittenValue(value),
16976
- resolveCallableSources: (value, alsoFrom) => resolution.resolveCallableSources(value, alsoFrom)
16654
+ resolveCallableSources: (value, alsoFrom) => resolution.resolveCallableSources(value, alsoFrom),
16655
+ sourceDeclarationsBehind: (declaration) => sourceDeclarationsBehind(declaration, resolution)
16977
16656
  },
16978
16657
  // Reached units the cache already serves emit nothing,
16979
16658
  // the way a cold run's seeds do not.
@@ -17010,7 +16689,10 @@ function createTypeScriptAdapter(suppliedConfig) {
17010
16689
  }
17011
16690
  timer.time(
17012
16691
  "stampModuleImports",
17013
- () => stampModuleImports(enriched, project)
16692
+ () => stampModuleImports(enriched, (file) => {
16693
+ const imports = internalImportsOf(project, file);
16694
+ return imports.length === 0 ? void 0 : imports;
16695
+ })
17014
16696
  );
17015
16697
  timer.time(
17016
16698
  "emitLibraryEnvReadMarkers",
@@ -17101,7 +16783,7 @@ function createTypeScriptAdapter(suppliedConfig) {
17101
16783
  };
17102
16784
  }
17103
16785
  function mountAssumptionsAgree(record2, index) {
17104
- return Object.entries(record2.mountPrefixes).every(
16786
+ return Object.entries(record2.meta.mountPrefixes).every(
17105
16787
  ([childId, prefix]) => (index.prefixForId?.(childId) ?? "") === prefix
17106
16788
  );
17107
16789
  }
@@ -17261,7 +16943,7 @@ function buildCacheAttribution(args) {
17261
16943
  cacheable,
17262
16944
  deps: [...deps].sort(),
17263
16945
  claims: sink.claims,
17264
- mountPrefixes: Object.fromEntries(sink.mountPrefixes),
16946
+ meta: { mountPrefixes: Object.fromEntries(sink.mountPrefixes) },
17265
16947
  packs: packNamesByPath.get(rootPath) ?? []
17266
16948
  });
17267
16949
  }
@@ -17406,8 +17088,8 @@ function buildSubUnitSummary(subUnit, parent, invocationRecognizers, accessRecog
17406
17088
  }
17407
17089
 
17408
17090
  // src/bootstrap/noTsconfigProject.ts
17409
- import fs9 from "fs";
17410
- import path12 from "path";
17091
+ import fs5 from "fs";
17092
+ import path11 from "path";
17411
17093
  import {
17412
17094
  ModuleKind,
17413
17095
  ModuleResolutionKind,
@@ -17443,7 +17125,7 @@ function findSourceFiles(rootDir, depth = 0) {
17443
17125
  const found = [];
17444
17126
  let entries;
17445
17127
  try {
17446
- entries = fs9.readdirSync(rootDir, { withFileTypes: true });
17128
+ entries = fs5.readdirSync(rootDir, { withFileTypes: true });
17447
17129
  } catch {
17448
17130
  return [];
17449
17131
  }
@@ -17451,10 +17133,10 @@ function findSourceFiles(rootDir, depth = 0) {
17451
17133
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name)) {
17452
17134
  continue;
17453
17135
  }
17454
- const full = path12.join(rootDir, entry.name);
17136
+ const full = path11.join(rootDir, entry.name);
17455
17137
  if (entry.isDirectory()) {
17456
17138
  found.push(...findSourceFiles(full, depth + 1));
17457
- } 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")) {
17458
17140
  found.push(full);
17459
17141
  }
17460
17142
  }
@@ -17489,15 +17171,15 @@ function createProjectWithoutTsconfig(rootDir) {
17489
17171
  return { project, files };
17490
17172
  }
17491
17173
  function findNearestTsconfig(startDir) {
17492
- let dir = path12.resolve(startDir);
17174
+ let dir = path11.resolve(startDir);
17493
17175
  while (true) {
17494
17176
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
17495
- const candidate = path12.join(dir, name);
17496
- if (fs9.existsSync(candidate)) {
17177
+ const candidate = path11.join(dir, name);
17178
+ if (fs5.existsSync(candidate)) {
17497
17179
  return candidate;
17498
17180
  }
17499
17181
  }
17500
- const parent = path12.dirname(dir);
17182
+ const parent = path11.dirname(dir);
17501
17183
  if (parent === dir) {
17502
17184
  return null;
17503
17185
  }
@@ -17506,7 +17188,7 @@ function findNearestTsconfig(startDir) {
17506
17188
  }
17507
17189
 
17508
17190
  // src/facts/why.ts
17509
- import path13 from "path";
17191
+ import path12 from "path";
17510
17192
  import { Node as Node65, Project as Project3, SyntaxKind as SyntaxKind12 } from "ts-morph";
17511
17193
  import { explainResolutionProof, renderExplanation } from "@suss/resolution";
17512
17194
  var JS_PHRASES = {
@@ -17514,12 +17196,12 @@ var JS_PHRASES = {
17514
17196
  reason: `${describe(tuple[0])} is a .bind of ${describe(tuple[1])}, which runs the same function`
17515
17197
  })
17516
17198
  };
17517
- var WhySession = class {
17199
+ var TypeScriptWhySession = class {
17518
17200
  project;
17519
17201
  store;
17520
17202
  root;
17521
17203
  constructor(options) {
17522
- this.root = path13.resolve(options.dir);
17204
+ this.root = path12.resolve(options.dir);
17523
17205
  const tsconfig = findNearestTsconfig(this.root);
17524
17206
  this.project = tsconfig === null ? createProjectWithoutTsconfig(this.root).project : new Project3({ tsConfigFilePath: tsconfig });
17525
17207
  this.store = new ResolutionStore();
@@ -17608,7 +17290,7 @@ var WhySession = class {
17608
17290
  };
17609
17291
  }
17610
17292
  sourceFileAt(file) {
17611
- const absolute = path13.resolve(this.root, file);
17293
+ const absolute = path12.resolve(this.root, file);
17612
17294
  const loaded = this.project.getSourceFile(absolute);
17613
17295
  if (loaded !== void 0) {
17614
17296
  return loaded;
@@ -17622,7 +17304,7 @@ var WhySession = class {
17622
17304
  /** A module key: a file path said relative to the root, or a package name. */
17623
17305
  displayPath(key) {
17624
17306
  if (key.startsWith(this.root)) {
17625
- return path13.relative(this.root, key);
17307
+ return path12.relative(this.root, key);
17626
17308
  }
17627
17309
  return key;
17628
17310
  }
@@ -17650,228 +17332,8 @@ function displayNameOf(node) {
17650
17332
  return text.length > 40 ? `${text.slice(0, 40)}...` : text;
17651
17333
  }
17652
17334
 
17653
- // src/packHealth.ts
17654
- function stagesOf(funnel) {
17655
- const stages = [];
17656
- const gateSaysSomething = funnel.gates.length > 0 && funnel.unresolvedGates.length === 0;
17657
- if (funnel.recognizes && !funnel.discovers && gateSaysSomething) {
17658
- stages.push({
17659
- from: { count: funnel.unitsInGatedFiles, name: "unit bodies" },
17660
- to: { count: funnel.effectsRecognized, name: "effects" }
17661
- });
17662
- }
17663
- if (funnel.discovers && gateSaysSomething) {
17664
- stages.push({
17665
- from: { count: funnel.candidateFiles, name: "source files" },
17666
- to: { count: funnel.unitsDiscovered, name: "units" }
17667
- });
17668
- }
17669
- stages.push(
17670
- {
17671
- from: { count: funnel.unitsClaimed, name: "units" },
17672
- to: { count: funnel.summariesBound, name: "summaries" }
17673
- },
17674
- {
17675
- from: { count: funnel.summariesBound, name: "summaries" },
17676
- to: { count: funnel.summariesWithBehavior, name: "transitions" }
17677
- }
17678
- );
17679
- return stages;
17680
- }
17681
- function funnelDrops(packs) {
17682
- const violations = [];
17683
- for (const funnel of packs) {
17684
- for (const stage of stagesOf(funnel)) {
17685
- if (stage.from.count === 0 || stage.to.count > 0) {
17686
- continue;
17687
- }
17688
- violations.push({
17689
- label: funnel.pack,
17690
- detail: `${stage.from.count} ${stage.from.name} -> 0 ${stage.to.name}`
17691
- });
17692
- }
17693
- }
17694
- return violations;
17695
- }
17696
- function recognizersWithNoUnits(packs) {
17697
- return packs.filter(
17698
- (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
17699
- // roots, so effects recognized there mean the run worked and
17700
- // the missing framework pack costs attribution, not existence.
17701
- funnel.effectsRecognized === 0
17702
- ).map((funnel) => ({
17703
- label: funnel.pack,
17704
- 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.`
17705
- }));
17706
- }
17707
- function helpersThatMatchedNothing(packs) {
17708
- return packs.flatMap(
17709
- (funnel) => funnel.helpersUnmatched.map((helper) => ({
17710
- label: funnel.pack,
17711
- 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.`
17712
- }))
17713
- );
17714
- }
17715
- function unversionedPacks(packs) {
17716
- return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
17717
- label: funnel.pack,
17718
- detail: "no version declared"
17719
- }));
17720
- }
17721
- function selfCollisions(packs) {
17722
- return packs.filter((funnel) => funnel.selfCollisions > 0).map((funnel) => ({
17723
- label: funnel.pack,
17724
- detail: `${funnel.selfCollisions} ${funnel.selfCollisions === 1 ? "unit" : "units"} matched twice, second dropped`
17725
- }));
17726
- }
17727
- function threwWhileReading(packs) {
17728
- return packs.filter((funnel) => funnel.failures.length > 0).map((funnel) => {
17729
- const first = funnel.failures[0];
17730
- const rest = funnel.failures.length - 1;
17731
- const alsoIn = rest > 0 ? ` (+${rest} more)` : "";
17732
- return {
17733
- label: funnel.pack,
17734
- detail: `${first.hook} on ${first.file}${alsoIn}: ${first.message}`
17735
- };
17736
- });
17737
- }
17738
- function packGradients(report) {
17739
- const gradients = [];
17740
- for (const funnel of report.packs) {
17741
- if (funnel.declarations === null) {
17742
- continue;
17743
- }
17744
- gradients.push(gradientOf(funnel.pack, funnel.declarations.declarations));
17745
- }
17746
- return gradients;
17747
- }
17748
- function gradientOf(pack, declarations) {
17749
- const gradient = {
17750
- pack,
17751
- dataLinks: 0,
17752
- functionLinks: [],
17753
- astLinks: [],
17754
- withoutExample: []
17755
- };
17756
- for (const declaration of declarations) {
17757
- gradient.dataLinks += declaration.dataLinks;
17758
- for (const question of declaration.functionLinks) {
17759
- gradient.functionLinks.push(`${declaration.name}.${question}`);
17760
- }
17761
- for (const question of declaration.astLinks) {
17762
- gradient.astLinks.push(`${declaration.name}.${question}`);
17763
- }
17764
- if (declaration.example === null) {
17765
- gradient.withoutExample.push(declaration.name);
17766
- }
17767
- }
17768
- return gradient;
17769
- }
17770
- function opaqueLinks(gradients) {
17771
- return gradients.filter((gradient) => gradient.functionLinks.length > 0).map((gradient) => ({
17772
- label: gradient.pack,
17773
- detail: `${gradient.functionLinks.join(", ")} (${gradient.dataLinks} data)`
17774
- }));
17775
- }
17776
- function reachesTheSyntaxTree(gradients) {
17777
- return gradients.filter((gradient) => gradient.astLinks.length > 0).map((gradient) => ({
17778
- label: gradient.pack,
17779
- detail: gradient.astLinks.join(", ")
17780
- }));
17781
- }
17782
- function undocumentedDeclarations(gradients) {
17783
- return gradients.filter((gradient) => gradient.withoutExample.length > 0).map((gradient) => ({
17784
- label: gradient.pack,
17785
- detail: gradient.withoutExample.join(", ")
17786
- }));
17787
- }
17788
- function evaluatePackHealth(report) {
17789
- const gradients = packGradients(report);
17790
- return [
17791
- {
17792
- name: "no pack throws while it reads",
17793
- code: "threw",
17794
- audience: "run",
17795
- violations: threwWhileReading(report.packs)
17796
- },
17797
- {
17798
- name: "no pack finds something and records nothing",
17799
- code: "no-output",
17800
- audience: "run",
17801
- violations: funnelDrops(report.packs)
17802
- },
17803
- {
17804
- name: "no pack collides with itself",
17805
- code: "double-match",
17806
- audience: "run",
17807
- violations: selfCollisions(report.packs)
17808
- },
17809
- {
17810
- name: "every recognizer had units to look inside",
17811
- code: "no-units",
17812
- audience: "run",
17813
- violations: recognizersWithNoUnits(report.packs)
17814
- },
17815
- {
17816
- name: "every registration helper the run read matched a call",
17817
- code: "no-helper",
17818
- audience: "run",
17819
- violations: helpersThatMatchedNothing(report.packs)
17820
- },
17821
- {
17822
- name: "every pack declares a version",
17823
- code: "no-version",
17824
- audience: "pack",
17825
- violations: unversionedPacks(report.packs)
17826
- },
17827
- {
17828
- name: "every link a declared pack states is data",
17829
- code: "fn-link",
17830
- audience: "pack",
17831
- violations: opaqueLinks(gradients)
17832
- },
17833
- {
17834
- name: "no declared pack reads the syntax tree",
17835
- code: "ast-link",
17836
- audience: "pack",
17837
- violations: reachesTheSyntaxTree(gradients)
17838
- },
17839
- {
17840
- name: "every declaration states an example",
17841
- code: "no-example",
17842
- audience: "pack",
17843
- violations: undocumentedDeclarations(gradients)
17844
- }
17845
- ];
17846
- }
17847
- function formatPackHealth(checks, audiences) {
17848
- const fired = checks.filter(
17849
- (check) => check.violations.length > 0 && audiences.includes(check.audience)
17850
- );
17851
- if (fired.length === 0) {
17852
- return "";
17853
- }
17854
- const rows = fired.flatMap(
17855
- (check) => check.violations.map((violation) => ({
17856
- code: check.code,
17857
- pack: violation.label,
17858
- detail: violation.detail
17859
- }))
17860
- );
17861
- const codeWidth = Math.max(...rows.map((row) => row.code.length));
17862
- const packWidth = Math.max(...rows.map((row) => row.pack.length));
17863
- const lines = ["", `Pack health (${rows.length}):`];
17864
- for (const row of rows) {
17865
- lines.push(
17866
- ` ${row.code.padEnd(codeWidth)} ${row.pack.padEnd(packWidth)} ${row.detail}`
17867
- );
17868
- }
17869
- return `${lines.join("\n")}
17870
- `;
17871
- }
17872
-
17873
17335
  // src/stubEvidence.ts
17874
- import path14 from "path";
17336
+ import path13 from "path";
17875
17337
  import { Project as Project4 } from "ts-morph";
17876
17338
  function stubEvidenceIn(project, packageName, root) {
17877
17339
  const resolution = new ResolutionStore();
@@ -17887,7 +17349,7 @@ function stubEvidenceIn(project, packageName, root) {
17887
17349
  calls: []
17888
17350
  };
17889
17351
  entry.calls.push({
17890
- file: path14.relative(root, sourceFile.getFilePath()).split(path14.sep).join("/"),
17352
+ file: path13.relative(root, sourceFile.getFilePath()).split(path13.sep).join("/"),
17891
17353
  line: one.call.getStartLineNumber(),
17892
17354
  args: one.call.getArguments().map((arg) => effectArgOf(arg))
17893
17355
  });
@@ -17902,7 +17364,7 @@ function stubEvidence(options) {
17902
17364
  const project = new Project4(
17903
17365
  options.tsConfigFilePath !== void 0 ? { tsConfigFilePath: options.tsConfigFilePath } : { compilerOptions: { allowJs: true } }
17904
17366
  );
17905
- 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());
17906
17368
  if (options.tsConfigFilePath === void 0) {
17907
17369
  project.addSourceFilesAtPaths([
17908
17370
  `${root}/**/*.{ts,tsx,js,jsx}`,
@@ -17914,7 +17376,7 @@ function stubEvidence(options) {
17914
17376
  export {
17915
17377
  ADAPTER_VERSION,
17916
17378
  ResolutionStore,
17917
- WhySession,
17379
+ TypeScriptWhySession,
17918
17380
  accessContextFor,
17919
17381
  buildProjectHelperIndex,
17920
17382
  callOpsFor,
@@ -17927,13 +17389,11 @@ export {
17927
17389
  createTsSubUnitContext,
17928
17390
  createTypeScriptAdapter,
17929
17391
  discoverUnits,
17930
- evaluatePackHealth,
17931
17392
  exportedDeclarationsOf,
17932
17393
  extractCodeStructure,
17933
17394
  extractRawBranches,
17934
17395
  findNearestTsconfig,
17935
17396
  findTerminals,
17936
- formatPackHealth,
17937
17397
  functionTargetOf,
17938
17398
  importDeclarationsOf,
17939
17399
  importedNamesOf,