@suss/adapter-python 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
@@ -754,7 +754,7 @@ function passesParametersThrough(def, inner) {
754
754
  }
755
755
 
756
756
  // src/discovery.ts
757
- import { dispatchByType, restBinding } from "@suss/behavioral-ir";
757
+ import { dispatchByType as dispatchByType2, restBinding } from "@suss/behavioral-ir";
758
758
  import {
759
759
  absentReading,
760
760
  ambiguousReading,
@@ -765,19 +765,399 @@ import {
765
765
  mapReading,
766
766
  unreadableReading,
767
767
  valueToReadFurtherFrom,
768
- walkDescendants,
768
+ walkDescendants as walkDescendants2,
769
769
  writtenReading
770
770
  } from "@suss/extractor";
771
771
 
772
+ // src/envReads.ts
773
+ import { runtimeConfigBinding } from "@suss/behavioral-ir";
774
+ import { SKIP_CHILDREN, walkDescendants } from "@suss/extractor";
775
+ var PYTHON_ENV_RECOGNITION = "python-env";
776
+ var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
777
+ function envReadEffects(root, module) {
778
+ const effects = [];
779
+ const startScope = module.scopeFor.get(root.id) ?? module.moduleScope;
780
+ walkDescendants(root, startScope, {
781
+ at: (node, scope) => {
782
+ const read = envReadAt(node, scope);
783
+ if (read !== null) {
784
+ effects.push(configReadEffect(read));
785
+ }
786
+ },
787
+ into: (node, scope) => {
788
+ if (DEFERRED_BODY_TYPES.has(node.type)) {
789
+ return SKIP_CHILDREN;
790
+ }
791
+ return module.scopeFor.get(node.id) ?? scope;
792
+ }
793
+ });
794
+ return effects;
795
+ }
796
+ function configReadEffect(read) {
797
+ return {
798
+ type: "interaction",
799
+ binding: runtimeConfigBinding({
800
+ recognition: PYTHON_ENV_RECOGNITION,
801
+ deploymentTarget: "lambda",
802
+ instanceName: "<unknown>"
803
+ }),
804
+ callee: `os.environ["${read.name}"]`,
805
+ interaction: {
806
+ class: "config-read",
807
+ name: read.name,
808
+ defaulted: read.defaulted
809
+ }
810
+ };
811
+ }
812
+ function envReadAt(node, scope) {
813
+ if (node.type === "subscript") {
814
+ return subscriptRead(node, scope);
815
+ }
816
+ if (node.type === "call") {
817
+ return callRead(node, scope);
818
+ }
819
+ return null;
820
+ }
821
+ function subscriptRead(node, scope) {
822
+ const value = field(node, "value");
823
+ const index = field(node, "subscript");
824
+ if (value === null || index === null || !isEnviron(value, scope)) {
825
+ return null;
826
+ }
827
+ const name = stringLiteralValue(index);
828
+ if (name === null || isAssignedTo(node)) {
829
+ return null;
830
+ }
831
+ return { name, defaulted: isDefaultedAt(node) };
832
+ }
833
+ function isAssignedTo(node) {
834
+ const parent = node.parent;
835
+ if (parent === null) {
836
+ return false;
837
+ }
838
+ if (parent.type === "delete_statement") {
839
+ return true;
840
+ }
841
+ return (parent.type === "assignment" || parent.type === "augmented_assignment") && field(parent, "left")?.id === node.id;
842
+ }
843
+ function callRead(node, scope) {
844
+ const callee = field(node, "function");
845
+ const args = field(node, "arguments");
846
+ if (callee === null || args === null || !isEnvGetter(callee, scope)) {
847
+ return null;
848
+ }
849
+ const positional = args.namedChildren.filter(
850
+ (arg) => arg !== null && arg.type !== "keyword_argument"
851
+ );
852
+ const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
853
+ if (name === null) {
854
+ return null;
855
+ }
856
+ const keywords = args.namedChildren.filter(
857
+ (arg) => arg !== null && arg.type === "keyword_argument"
858
+ );
859
+ const hasDefault = positional.length > 1 || keywords.some((arg) => field(arg, "name")?.text === "default");
860
+ return { name, defaulted: hasDefault || isDefaultedAt(node) };
861
+ }
862
+ function isEnvGetter(callee, scope) {
863
+ if (callee.type === "attribute") {
864
+ const object2 = field(callee, "object");
865
+ const attribute = field(callee, "attribute")?.text;
866
+ if (object2 === null) {
867
+ return false;
868
+ }
869
+ if (attribute === "get") {
870
+ return isEnviron(object2, scope);
871
+ }
872
+ return attribute === "getenv" && isOsModule(object2, scope);
873
+ }
874
+ return isImportedFromOs(callee, scope, "getenv");
875
+ }
876
+ function isEnviron(node, scope) {
877
+ if (node.type === "attribute") {
878
+ const object2 = field(node, "object");
879
+ return object2 !== null && field(node, "attribute")?.text === "environ" && isOsModule(object2, scope);
880
+ }
881
+ return isImportedFromOs(node, scope, "environ");
882
+ }
883
+ function isOsModule(node, scope) {
884
+ if (node.type !== "identifier") {
885
+ return false;
886
+ }
887
+ const binding = resolveName(scope, node.text);
888
+ return binding?.kind === "import" && binding.module === "os" && binding.relativeLevel === 0;
889
+ }
890
+ function isImportedFromOs(node, scope, name) {
891
+ if (node.type !== "identifier") {
892
+ return false;
893
+ }
894
+ const binding = resolveName(scope, node.text);
895
+ return binding?.kind === "importFrom" && binding.module === "os" && binding.relativeLevel === 0 && binding.importedName === name;
896
+ }
897
+ function isDefaultedAt(node) {
898
+ let child = node;
899
+ let parent = node.parent;
900
+ while (parent !== null) {
901
+ if (parent.type === "parenthesized_expression") {
902
+ child = parent;
903
+ parent = parent.parent;
904
+ continue;
905
+ }
906
+ if (parent.type !== "boolean_operator" || field(parent, "operator")?.text !== "or") {
907
+ return false;
908
+ }
909
+ if (field(parent, "left")?.id === child.id) {
910
+ return true;
911
+ }
912
+ child = parent;
913
+ parent = parent.parent;
914
+ }
915
+ return false;
916
+ }
917
+
772
918
  // src/facts/resolve.ts
773
- import { deriveOnDemand, evaluate } from "@suss/datalog";
919
+ import { deriveOnDemand, evaluate as evaluate2 } from "@suss/datalog";
774
920
 
775
921
  // ../../resolution/dist/index.js
776
922
  import { ruleLabel } from "@suss/datalog";
923
+ import { Database, evaluate, proofOf, witnesses } from "@suss/datalog";
777
924
  import { constant, lit, rule, variable as v } from "@suss/datalog";
778
- function joinMountedPath(prefix, path4) {
925
+ function exportTrail(proof, describe3) {
926
+ if (proof.kind !== "derived") {
927
+ return [];
928
+ }
929
+ const name = ruleLabel(proof.rule);
930
+ if (name === "re-export") {
931
+ const [m, n, m2, n2] = proof.premises[0].tuple;
932
+ return [
933
+ `${describe3(m)} takes ${String(n)} from ${describe3(m2)}, where it is called ${String(n2)}`,
934
+ ...exportTrail(proof.premises[1], describe3)
935
+ ];
936
+ }
937
+ if (name === "re-export all") {
938
+ const [m, m2] = proof.premises[0].tuple;
939
+ return [
940
+ `${describe3(m)} forwards everything ${describe3(m2)} exports`,
941
+ ...exportTrail(proof.premises[1], describe3)
942
+ ];
943
+ }
944
+ return [];
945
+ }
946
+ function argumentReason(proof, describe3) {
947
+ const [, p, a] = proof.tuple;
948
+ if (proof.kind !== "derived") {
949
+ return `${describe3(p)} is a parameter, and a call passes it ${describe3(a)}`;
950
+ }
951
+ const f = proof.premises[0].tuple[0];
952
+ return `${describe3(p)} is a parameter of ${describe3(f)}, and a call passes it ${describe3(a)}`;
953
+ }
954
+ var DEFAULT_PHRASES = {
955
+ alias: ({ tuple: tuple2, describe: describe3 }) => ({
956
+ reason: `${describe3(tuple2[0])} is declared as ${describe3(tuple2[1])}`
957
+ }),
958
+ "last write": ({ tuple: tuple2, describe: describe3 }) => ({
959
+ reason: `${describe3(tuple2[0])} is written more than once, and the last write leaves it as ${describe3(tuple2[1])}`
960
+ }),
961
+ fallback: ({ tuple: tuple2, describe: describe3 }) => ({
962
+ reason: `${describe3(tuple2[0])} is a fallback expression, and ${describe3(tuple2[1])} is the branch that resolves`
963
+ }),
964
+ import: ({ tuple: tuple2, premises, describe: describe3 }) => {
965
+ const [, module, name] = premises[0].tuple;
966
+ return {
967
+ reason: `${describe3(tuple2[0])} is imported from ${describe3(module)} under the name ${String(name)}`,
968
+ notes: exportTrail(premises[1], describe3)
969
+ };
970
+ },
971
+ argument: ({ premises, describe: describe3 }) => ({
972
+ reason: argumentReason(premises[0], describe3)
973
+ }),
974
+ "property read": ({ tuple: tuple2, premises, describe: describe3 }) => {
975
+ const [, object2, name] = premises[0].tuple;
976
+ const contains = premises[2];
977
+ const inherited = contains.kind === "derived" && contains.premises.length > 1;
978
+ return {
979
+ reason: `${describe3(tuple2[0])} reads ${String(name)} off ${describe3(object2)}, which contains ${describe3(tuple2[1])}`,
980
+ notes: inherited ? [`${String(name)} comes from a base class`] : []
981
+ };
982
+ },
983
+ "class instance": ({ tuple: tuple2, premises, describe: describe3, inline }) => ({
984
+ reason: `${describe3(tuple2[0])} makes an instance of ${describe3(tuple2[1])}`,
985
+ notes: inline(premises[1])
986
+ }),
987
+ "factory unwrap": ({ tuple: tuple2, premises, describe: describe3, inline }) => {
988
+ const factory = premises[1].tuple[1];
989
+ return {
990
+ reason: `${describe3(tuple2[0])} calls ${describe3(factory)}, a factory that passes its argument through, so it comes down to ${describe3(tuple2[1])}`,
991
+ notes: inline(premises[1])
992
+ };
993
+ },
994
+ "declared wrapper": ({ tuple: tuple2, premises, describe: describe3 }) => {
995
+ const callee = String(premises[0].tuple[1]);
996
+ const module = String(premises[2].tuple[1]);
997
+ const argument = String(premises[1].tuple[1]);
998
+ return {
999
+ reason: `${describe3(tuple2[0])} calls ${callee} from ${module}, which passes argument ${argument} through, so it comes down to ${describe3(tuple2[1])}`,
1000
+ assumptions: [
1001
+ `a pack declares that ${callee} from ${module} passes argument ${argument} through to its result`
1002
+ ]
1003
+ };
1004
+ },
1005
+ "call result": ({ tuple: tuple2, premises, describe: describe3, inline }) => {
1006
+ const invokes = premises[0];
1007
+ const invoked = invokes.tuple[1];
1008
+ const calleeWalks = invokes.kind === "derived" ? invokes.premises.filter(
1009
+ (premise) => premise.kind === "derived" && (premise.relation === "comesTo" || premise.relation === "givesBack")
1010
+ ) : [];
1011
+ return {
1012
+ reason: `${describe3(tuple2[0])} runs ${describe3(invoked)}, which returns ${describe3(tuple2[1])}`,
1013
+ notes: calleeWalks.flatMap((premise) => inline(premise))
1014
+ };
1015
+ }
1016
+ };
1017
+ var CHAIN_RELATIONS = /* @__PURE__ */ new Set([
1018
+ "resolves",
1019
+ "comesTo",
1020
+ "givesBack",
1021
+ "isWrittenAs",
1022
+ "comesFrom",
1023
+ "reaches",
1024
+ "stepsTo"
1025
+ ]);
1026
+ function inlineChain(proof, state) {
1027
+ const explained = explainResolutionProof(proof, {
1028
+ describe: state.describe,
1029
+ phrases: state.phrases
1030
+ });
1031
+ if (explained === null || explained.steps.length === 0) {
1032
+ return [];
1033
+ }
1034
+ if (explained.truncated) {
1035
+ state.truncated = true;
1036
+ }
1037
+ return explained.steps.map((step) => step.reason);
1038
+ }
1039
+ function stepFrom(proof, state) {
1040
+ const name = ruleLabel(proof.rule);
1041
+ const phrase = state.phrases[name];
1042
+ const from = proof.tuple[0];
1043
+ const to = proof.tuple[1];
1044
+ if (phrase === void 0) {
1045
+ return {
1046
+ from,
1047
+ to,
1048
+ rule: name,
1049
+ reason: `${state.describe(from)} steps to ${state.describe(to)} (${name})`,
1050
+ notes: [],
1051
+ assumptions: []
1052
+ };
1053
+ }
1054
+ const said = phrase({
1055
+ tuple: proof.tuple,
1056
+ premises: proof.premises,
1057
+ describe: state.describe,
1058
+ inline: (nested) => inlineChain(nested, state)
1059
+ });
1060
+ return {
1061
+ from,
1062
+ to,
1063
+ rule: name,
1064
+ reason: said.reason,
1065
+ notes: said.notes ?? [],
1066
+ assumptions: said.assumptions ?? []
1067
+ };
1068
+ }
1069
+ function importStep(proof, state) {
1070
+ const [x, module, name] = proof.tuple;
1071
+ return {
1072
+ from: x,
1073
+ to: module,
1074
+ rule: "import",
1075
+ reason: `${state.describe(x)} is imported from ${state.describe(module)} under the name ${String(name)}`,
1076
+ notes: [],
1077
+ assumptions: []
1078
+ };
1079
+ }
1080
+ var flattenInto = (proof, state) => {
1081
+ if (proof.kind === "truncated") {
1082
+ state.truncated = true;
1083
+ return;
1084
+ }
1085
+ if (proof.kind !== "derived") {
1086
+ return;
1087
+ }
1088
+ const relation = proof.relation;
1089
+ if (relation === "stepsTo") {
1090
+ state.steps.push(stepFrom(proof, state));
1091
+ return;
1092
+ }
1093
+ if (relation === "reaches") {
1094
+ for (const p of proof.premises) {
1095
+ flattenInto(p, state);
1096
+ }
1097
+ return;
1098
+ }
1099
+ if (relation === "comesFrom") {
1100
+ if (proof.premises.length === 2) {
1101
+ flattenInto(proof.premises[0], state);
1102
+ state.steps.push(importStep(proof.premises[1], state));
1103
+ } else {
1104
+ state.steps.push(importStep(proof.premises[0], state));
1105
+ }
1106
+ return;
1107
+ }
1108
+ for (const p of proof.premises) {
1109
+ if (p.kind === "derived" && (p.relation === "reaches" || p.relation === "comesTo")) {
1110
+ flattenInto(p, state);
1111
+ }
1112
+ if (p.kind === "truncated") {
1113
+ state.truncated = true;
1114
+ }
1115
+ }
1116
+ };
1117
+ function explainResolutionProof(proof, options) {
1118
+ if (proof.kind === "absence") {
1119
+ return null;
1120
+ }
1121
+ if (proof.kind === "derived" && !CHAIN_RELATIONS.has(proof.relation)) {
1122
+ return null;
1123
+ }
1124
+ const state = {
1125
+ steps: [],
1126
+ truncated: proof.kind === "truncated",
1127
+ describe: options.describe,
1128
+ phrases: { ...DEFAULT_PHRASES, ...options.phrases }
1129
+ };
1130
+ flattenInto(proof, state);
1131
+ const atoms = proof.kind === "fact" || proof.kind === "truncated" ? [proof.tuple[0]] : [proof.tuple[0], ...state.steps.map((step) => step.to)];
1132
+ return {
1133
+ atoms,
1134
+ steps: state.steps,
1135
+ assumptions: state.steps.flatMap((step) => step.assumptions),
1136
+ truncated: state.truncated
1137
+ };
1138
+ }
1139
+ function renderExplanation(explanation, describe3) {
1140
+ const lines = [];
1141
+ if (explanation.steps.length > 0) {
1142
+ lines.push(explanation.atoms.map((atom) => describe3(atom)).join(" -> "));
1143
+ }
1144
+ for (const step of explanation.steps) {
1145
+ lines.push(` ${step.reason}`);
1146
+ for (const note of step.notes) {
1147
+ lines.push(` ${note}`);
1148
+ }
1149
+ }
1150
+ for (const assumption of explanation.assumptions) {
1151
+ lines.push(` assuming ${assumption}`);
1152
+ }
1153
+ if (explanation.truncated) {
1154
+ lines.push(" the chain goes on past the proof depth cap");
1155
+ }
1156
+ return lines;
1157
+ }
1158
+ function joinMountedPath(prefix, path6) {
779
1159
  const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
780
- return trimmed === "" ? path4 : trimmed + path4;
1160
+ return trimmed === "" ? path6 : trimmed + path6;
781
1161
  }
782
1162
  function mountPathsOf(edges, childId) {
783
1163
  return pathsOf(edges, /* @__PURE__ */ new Map(), childId, /* @__PURE__ */ new Set());
@@ -814,6 +1194,88 @@ function pathsOf(edges, memo, childId, visiting) {
814
1194
  memo.set(childId, result);
815
1195
  return result;
816
1196
  }
1197
+ function explainResolvedKey(options) {
1198
+ const { db, rules, key, locate, displayPath } = options;
1199
+ const proofDb = new Database();
1200
+ let baseFacts = 0;
1201
+ for (const relation of db.relationNames()) {
1202
+ for (const tuple2 of db.facts(relation)) {
1203
+ proofDb.add(relation, tuple2);
1204
+ baseFacts++;
1205
+ }
1206
+ }
1207
+ const started = performance.now();
1208
+ evaluate(proofDb, rules, witnesses);
1209
+ const evaluateMs = performance.now() - started;
1210
+ const derivedFacts = proofDb.relationNames().reduce((count, relation) => count + proofDb.size(relation), 0) - baseFacts;
1211
+ const targets = new Set(
1212
+ proofDb.lookup("resolves", 0, key).map((tuple2) => String(tuple2[1]))
1213
+ );
1214
+ if (targets.size !== 1) {
1215
+ return null;
1216
+ }
1217
+ const target = [...targets][0];
1218
+ const proof = proofOf(
1219
+ proofDb,
1220
+ "resolves",
1221
+ [key, target],
1222
+ options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth }
1223
+ );
1224
+ const describe3 = (atom) => {
1225
+ const location = locate(String(atom));
1226
+ return location === null ? displayPath(String(atom)) : `${location.name} (${location.file}:${location.line})`;
1227
+ };
1228
+ const explanation = explainResolutionProof(proof, {
1229
+ describe: describe3,
1230
+ ...options.phrases === void 0 ? {} : { phrases: options.phrases }
1231
+ });
1232
+ if (explanation === null) {
1233
+ return null;
1234
+ }
1235
+ const targetLocation = locate(target);
1236
+ if (targetLocation === null) {
1237
+ return null;
1238
+ }
1239
+ return {
1240
+ explanation,
1241
+ chain: explanation.atoms.map(describe3),
1242
+ lines: renderExplanation(explanation, describe3),
1243
+ target: targetLocation,
1244
+ stats: { baseFacts, derivedFacts, evaluateMs }
1245
+ };
1246
+ }
1247
+ var NO_PLACEHOLDERS = /* @__PURE__ */ new Set();
1248
+ function singleAnswers(rows, placeholders = NO_PLACEHOLDERS) {
1249
+ const candidates = /* @__PURE__ */ new Map();
1250
+ for (const row of rows) {
1251
+ const key = String(row[0]);
1252
+ const answer = String(row[1]);
1253
+ if (answer === key) {
1254
+ continue;
1255
+ }
1256
+ const set2 = candidates.get(key) ?? /* @__PURE__ */ new Set();
1257
+ set2.add(answer);
1258
+ candidates.set(key, set2);
1259
+ }
1260
+ const settled = /* @__PURE__ */ new Map();
1261
+ for (const [key, set2] of candidates) {
1262
+ const answers = withoutPlaceholders(set2, placeholders);
1263
+ if (answers.length === 1) {
1264
+ settled.set(key, answers[0]);
1265
+ }
1266
+ }
1267
+ return settled;
1268
+ }
1269
+ function placeholderValues(db) {
1270
+ return new Set(db.facts("placeholderValue").map((row) => String(row[0])));
1271
+ }
1272
+ function withoutPlaceholders(answers, placeholders) {
1273
+ const kept = [...answers].filter((answer) => !placeholders.has(answer));
1274
+ if (kept.length === 0) {
1275
+ return [...answers];
1276
+ }
1277
+ return kept;
1278
+ }
817
1279
  var VALUE_STEP = constant("value");
818
1280
  var NAMESPACE_IMPORT_NAME = "*";
819
1281
  var NAMESPACE_IMPORT = constant(NAMESPACE_IMPORT_NAME);
@@ -927,9 +1389,9 @@ var RESOLUTION_RULES = [
927
1389
  [lit("invokes", v("r"), v("f")), lit("returnsValue", v("f"), v("ret"))],
928
1390
  "call result"
929
1391
  ),
930
- // Where a walk gets to, and whether it ran a call on the way. A value
931
- // step keeps whatever the rest of the walk was, and a result step
932
- // makes the whole walk one that ran a call.
1392
+ // Where a walk gets to, and whether it ran a call on the way. The walk
1393
+ // so far comes first, so demand stays on the value asked about; the
1394
+ // README says what the other order cost.
933
1395
  rule(
934
1396
  "reaches",
935
1397
  [v("x"), v("z"), v("kind")],
@@ -939,27 +1401,27 @@ var RESOLUTION_RULES = [
939
1401
  "reaches",
940
1402
  [v("x"), v("z"), v("kind")],
941
1403
  [
942
- lit("stepsTo", v("x"), v("y"), VALUE_STEP),
943
- lit("reaches", v("y"), v("z"), v("kind"))
1404
+ lit("reaches", v("x"), v("y"), VALUE_STEP),
1405
+ lit("stepsTo", v("y"), v("z"), v("kind"))
944
1406
  ]
945
1407
  ),
946
- // Two rules rather than one leaving the rest of the walk's kind free.
947
- // A free kind is a second question about the same relation, and a
1408
+ // Two rules rather than one leaving the next step's kind free. A free
1409
+ // kind is a second question about the same relation, and a
948
1410
  // demand-driven run then derives both to answer either.
949
1411
  rule(
950
1412
  "reaches",
951
1413
  [v("x"), v("z"), RESULT_STEP],
952
1414
  [
953
- lit("stepsTo", v("x"), v("y"), RESULT_STEP),
954
- lit("reaches", v("y"), v("z"), VALUE_STEP)
1415
+ lit("reaches", v("x"), v("y"), RESULT_STEP),
1416
+ lit("stepsTo", v("y"), v("z"), VALUE_STEP)
955
1417
  ]
956
1418
  ),
957
1419
  rule(
958
1420
  "reaches",
959
1421
  [v("x"), v("z"), RESULT_STEP],
960
1422
  [
961
- lit("stepsTo", v("x"), v("y"), RESULT_STEP),
962
- lit("reaches", v("y"), v("z"), RESULT_STEP)
1423
+ lit("reaches", v("x"), v("y"), RESULT_STEP),
1424
+ lit("stepsTo", v("y"), v("z"), RESULT_STEP)
963
1425
  ]
964
1426
  ),
965
1427
  // What a value comes down to: the walk that never ran a call, stopped
@@ -1005,6 +1467,18 @@ var RESOLUTION_RULES = [
1005
1467
  [v("x"), v("z")],
1006
1468
  [lit("reaches", v("x"), v("z"), VALUE_STEP), lit("objectValue", v("z"))]
1007
1469
  ),
1470
+ // A call is written as whatever its callee's return value is written
1471
+ // as. Unlike `comesTo`, this has nothing to protect: which expression
1472
+ // a value was written as does not compete with unwrapping it.
1473
+ rule(
1474
+ "isWrittenAs",
1475
+ [v("x"), v("z")],
1476
+ [
1477
+ lit("invokes", v("x"), v("f")),
1478
+ lit("returnsValue", v("f"), v("ret")),
1479
+ lit("isWrittenAs", v("ret"), v("z"))
1480
+ ]
1481
+ ),
1008
1482
  // What one call site put in a parameter, told apart from what the
1009
1483
  // other callers passed.
1010
1484
  rule(
@@ -1305,29 +1779,14 @@ var RESOLUTION_QUESTIONS = [
1305
1779
  // here demands reaches or callsInto. The chains are closures with
1306
1780
  // no depth bound: an alias run of any length resolves.
1307
1781
  rule("callOriginChain", [v("x"), v("x")], [lit("wantedCallOrigin", v("x"))]),
1782
+ // The chain with at least one hop taken, which is what tells a
1783
+ // destructured binding apart from asking about a node directly. The
1784
+ // whole chain is the seed plus those hops.
1308
1785
  rule(
1309
1786
  "callOriginChain",
1310
1787
  [v("x"), v("z")],
1311
- [lit("callOriginChain", v("x"), v("y")), lit("binds", v("y"), v("z"))]
1312
- ),
1313
- rule(
1314
- "callOriginChain",
1315
- [v("x"), v("z")],
1316
- [
1317
- lit("callOriginChain", v("x"), v("y")),
1318
- lit("endsHolding", v("y"), v("z"))
1319
- ]
1320
- ),
1321
- rule(
1322
- "callOriginChain",
1323
- [v("x"), v("z")],
1324
- [
1325
- lit("callOriginChain", v("x"), v("y")),
1326
- lit("fallbackBranch", v("y"), v("z"))
1327
- ]
1788
+ [lit("callOriginChainStepped", v("x"), v("z"))]
1328
1789
  ),
1329
- // The chain with at least one hop taken, which is what tells a
1330
- // destructured binding apart from asking about a node directly.
1331
1790
  rule(
1332
1791
  "callOriginChainStepped",
1333
1792
  [v("x"), v("z")],
@@ -1567,23 +2026,33 @@ function resolveCalls(db, callKeys) {
1567
2026
  for (const callKey of callKeys) {
1568
2027
  db.add("wanted", [callKey]);
1569
2028
  }
1570
- evaluate(db, RESOLUTION_PROGRAM.rules);
2029
+ evaluate2(db, RESOLUTION_PROGRAM.rules);
1571
2030
  }
1572
2031
  function subjectConstructions(db, valueKeys) {
1573
2032
  const found = /* @__PURE__ */ new Map();
1574
2033
  if (valueKeys.length === 0) {
1575
2034
  return found;
1576
2035
  }
1577
- for (const valueKey2 of valueKeys) {
1578
- db.add("wantedSubject", [valueKey2]);
1579
- }
1580
- evaluate(db, RESOLUTION_PROGRAM.rules);
1581
- const writtenAs = /* @__PURE__ */ new Map();
1582
- for (const row of db.facts("wantedSubjectWritten")) {
1583
- const written = writtenAs.get(String(row[0])) ?? /* @__PURE__ */ new Set();
1584
- written.add(String(row[1]));
1585
- writtenAs.set(String(row[0]), written);
2036
+ const askSubjects = (keys) => {
2037
+ for (const key of keys) {
2038
+ db.add("wantedSubject", [key]);
2039
+ }
2040
+ evaluate2(db, RESOLUTION_PROGRAM.rules);
2041
+ };
2042
+ askSubjects(valueKeys);
2043
+ const placeholders = placeholderValues(db);
2044
+ const written = singleAnswers(db.facts("wantedSubjectWritten"), placeholders);
2045
+ const calls = new Set(db.facts("call").map((row) => String(row[0])));
2046
+ const throughCalls = new Set(
2047
+ [...written.values()].filter((candidate) => calls.has(candidate))
2048
+ );
2049
+ if (throughCalls.size > 0) {
2050
+ askSubjects(throughCalls);
1586
2051
  }
2052
+ const writtenThroughCall = singleAnswers(
2053
+ db.facts("wantedSubjectWritten"),
2054
+ placeholders
2055
+ );
1587
2056
  const origins = /* @__PURE__ */ new Map();
1588
2057
  for (const row of db.facts("wantedSubjectConstruction")) {
1589
2058
  const key = `${String(row[0])}|${String(row[1])}`;
@@ -1592,12 +2061,14 @@ function subjectConstructions(db, valueKeys) {
1592
2061
  origins.set(key, listed);
1593
2062
  }
1594
2063
  for (const valueKey2 of valueKeys) {
1595
- const written = writtenAs.get(valueKey2);
1596
- if (written === void 0 || written.size !== 1) {
2064
+ const direct = written.get(valueKey2);
2065
+ if (direct === void 0) {
1597
2066
  continue;
1598
2067
  }
1599
- const constructionKey = [...written][0];
1600
- const from = origins.get(`${valueKey2}|${constructionKey}`);
2068
+ const deeper = calls.has(direct) ? writtenThroughCall.get(direct) : void 0;
2069
+ const constructionKey = deeper ?? direct;
2070
+ const originOf = deeper === void 0 ? valueKey2 : direct;
2071
+ const from = origins.get(`${originOf}|${constructionKey}`);
1601
2072
  if (from !== void 0) {
1602
2073
  found.set(valueKey2, { constructionKey, origins: from });
1603
2074
  }
@@ -1793,6 +2264,9 @@ function emitExpressionFact(emitter, child) {
1793
2264
  if (WRITTEN_VALUE_TYPES.has(child.type)) {
1794
2265
  add(emitter, "writtenValue", nodeId(emitter.filePath, child));
1795
2266
  }
2267
+ if (child.type === "none") {
2268
+ add(emitter, "placeholderValue", nodeId(emitter.filePath, child));
2269
+ }
1796
2270
  }
1797
2271
  function emitExpressionFacts(emitter, node) {
1798
2272
  walkExpressions(emitter, node, (child) => {
@@ -2251,7 +2725,7 @@ function predicateOf(node) {
2251
2725
  }
2252
2726
 
2253
2727
  // src/paths/effects.ts
2254
- var NESTED_FUNCTION_TYPES2 = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
2728
+ var NESTED_DEFINITION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
2255
2729
  var LITERAL_ARGS = {
2256
2730
  string: (node) => ({ kind: "string", value: node.text.slice(1, -1) }),
2257
2731
  integer: (node) => {
@@ -2267,7 +2741,7 @@ var LITERAL_ARGS = {
2267
2741
  };
2268
2742
  function bodyCalls(node, found = []) {
2269
2743
  for (const child of node.namedChildren) {
2270
- if (child === null || NESTED_FUNCTION_TYPES2.has(child.type)) {
2744
+ if (child === null || NESTED_DEFINITION_TYPES.has(child.type)) {
2271
2745
  continue;
2272
2746
  }
2273
2747
  if (child.type === "call") {
@@ -2309,26 +2783,12 @@ function argsOf(call) {
2309
2783
  function calleeText(call) {
2310
2784
  return field(call, "function")?.text ?? call.text;
2311
2785
  }
2312
- function withoutChainLinks(calls) {
2313
- const isLink = /* @__PURE__ */ new Set();
2314
- for (const call of calls) {
2315
- const callee = field(call, "function");
2316
- if (callee === null || callee.type !== "attribute") {
2317
- continue;
2318
- }
2319
- const object2 = field(callee, "object");
2320
- if (object2 !== null && object2.type === "call") {
2321
- isLink.add(object2.id);
2322
- }
2323
- }
2324
- return calls.filter((call) => !isLink.has(call.id));
2325
- }
2326
2786
  function invocationEffects(definitionNode) {
2327
2787
  const body = field(definitionNode, "body");
2328
2788
  if (body === null) {
2329
2789
  return [];
2330
2790
  }
2331
- const calls = withoutChainLinks(bodyCalls(body));
2791
+ const calls = bodyCalls(body).sort((a, b) => a.endIndex - b.endIndex);
2332
2792
  if (calls.length === 0) {
2333
2793
  return [];
2334
2794
  }
@@ -3206,10 +3666,10 @@ function mergeDefs(...defs) {
3206
3666
  function cloneDef(schema) {
3207
3667
  return mergeDefs(schema._zod.def);
3208
3668
  }
3209
- function getElementAtPath(obj, path4) {
3210
- if (!path4)
3669
+ function getElementAtPath(obj, path6) {
3670
+ if (!path6)
3211
3671
  return obj;
3212
- return path4.reduce((acc, key) => acc?.[key], obj);
3672
+ return path6.reduce((acc, key) => acc?.[key], obj);
3213
3673
  }
3214
3674
  function promiseAllObject(promisesObj) {
3215
3675
  const keys = Object.keys(promisesObj);
@@ -3618,11 +4078,11 @@ function explicitlyAborted(x, startIndex = 0) {
3618
4078
  }
3619
4079
  return false;
3620
4080
  }
3621
- function prefixIssues(path4, issues) {
4081
+ function prefixIssues(path6, issues) {
3622
4082
  return issues.map((iss) => {
3623
4083
  var _a3;
3624
4084
  (_a3 = iss).path ?? (_a3.path = []);
3625
- iss.path.unshift(path4);
4085
+ iss.path.unshift(path6);
3626
4086
  return iss;
3627
4087
  });
3628
4088
  }
@@ -3769,16 +4229,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
3769
4229
  }
3770
4230
  function formatError(error51, mapper = (issue2) => issue2.message) {
3771
4231
  const fieldErrors = { _errors: [] };
3772
- const processError = (error52, path4 = []) => {
4232
+ const processError = (error52, path6 = []) => {
3773
4233
  for (const issue2 of error52.issues) {
3774
4234
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3775
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
4235
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
3776
4236
  } else if (issue2.code === "invalid_key") {
3777
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4237
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3778
4238
  } else if (issue2.code === "invalid_element") {
3779
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4239
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3780
4240
  } else {
3781
- const fullpath = [...path4, ...issue2.path];
4241
+ const fullpath = [...path6, ...issue2.path];
3782
4242
  if (fullpath.length === 0) {
3783
4243
  fieldErrors._errors.push(mapper(issue2));
3784
4244
  } else {
@@ -3805,17 +4265,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
3805
4265
  }
3806
4266
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
3807
4267
  const result = { errors: [] };
3808
- const processError = (error52, path4 = []) => {
4268
+ const processError = (error52, path6 = []) => {
3809
4269
  var _a3, _b;
3810
4270
  for (const issue2 of error52.issues) {
3811
4271
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3812
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
4272
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
3813
4273
  } else if (issue2.code === "invalid_key") {
3814
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4274
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3815
4275
  } else if (issue2.code === "invalid_element") {
3816
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4276
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3817
4277
  } else {
3818
- const fullpath = [...path4, ...issue2.path];
4278
+ const fullpath = [...path6, ...issue2.path];
3819
4279
  if (fullpath.length === 0) {
3820
4280
  result.errors.push(mapper(issue2));
3821
4281
  continue;
@@ -3847,8 +4307,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
3847
4307
  }
3848
4308
  function toDotPath(_path) {
3849
4309
  const segs = [];
3850
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3851
- for (const seg of path4) {
4310
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4311
+ for (const seg of path6) {
3852
4312
  if (typeof seg === "number")
3853
4313
  segs.push(`[${seg}]`);
3854
4314
  else if (typeof seg === "symbol")
@@ -16540,13 +17000,13 @@ function resolveRef(ref, ctx) {
16540
17000
  if (!ref.startsWith("#")) {
16541
17001
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
16542
17002
  }
16543
- const path4 = ref.slice(1).split("/").filter(Boolean);
16544
- if (path4.length === 0) {
17003
+ const path6 = ref.slice(1).split("/").filter(Boolean);
17004
+ if (path6.length === 0) {
16545
17005
  return ctx.rootSchema;
16546
17006
  }
16547
17007
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
16548
- if (path4[0] === defsKey) {
16549
- const key = path4[1];
17008
+ if (path6[0] === defsKey) {
17009
+ const key = path6[1];
16550
17010
  if (!key || !ctx.defs[key]) {
16551
17011
  throw new Error(`Reference not found: ${ref}`);
16552
17012
  }
@@ -16954,7 +17414,7 @@ function date4(params) {
16954
17414
  // ../../../node_modules/zod/v4/classic/external.js
16955
17415
  config(en_default());
16956
17416
 
16957
- // ../../ir-core/dist/chunk-QHFMZPEG.js
17417
+ // ../../ir-core/dist/chunk-PPQWK62O.js
16958
17418
  function gqlIdentityKey(typeName, fieldName) {
16959
17419
  return `gql:${typeName}.${fieldName}`;
16960
17420
  }
@@ -17240,6 +17700,42 @@ var metricSemantics = defineBoundarySemantics({
17240
17700
  }
17241
17701
  }
17242
17702
  });
17703
+ var HOLE_SPLIT = /(\{[^}]*\})/;
17704
+ function isHole(piece) {
17705
+ return piece.startsWith("{") && piece.endsWith("}");
17706
+ }
17707
+ function parseBoundaryName(name) {
17708
+ const parts = name.split(HOLE_SPLIT).filter((piece) => piece !== "").map(
17709
+ (piece) => isHole(piece) ? { type: "hole", label: piece.slice(1, -1) } : { type: "text", text: piece }
17710
+ );
17711
+ const only = parts.length === 1 ? parts[0] : void 0;
17712
+ if (only !== void 0 && only.type === "hole") {
17713
+ return { type: "reference", path: only.label.split(".") };
17714
+ }
17715
+ if (parts.some((part) => part.type === "hole")) {
17716
+ return { type: "pattern", parts };
17717
+ }
17718
+ return { type: "literal", value: name };
17719
+ }
17720
+ function patternHole(label) {
17721
+ return `{${label}}`;
17722
+ }
17723
+ function namesNothing(name) {
17724
+ return parseBoundaryName(name).type === "reference";
17725
+ }
17726
+ function referenceOf(name) {
17727
+ if (name.type !== "reference") {
17728
+ return null;
17729
+ }
17730
+ const root = name.path[0];
17731
+ if (root === void 0 || name.path.some((part) => part === "")) {
17732
+ return null;
17733
+ }
17734
+ return { root, fields: name.path.slice(1) };
17735
+ }
17736
+ function referenceFromName(name) {
17737
+ return referenceOf(parseBoundaryName(name));
17738
+ }
17243
17739
  var SCHEME_ORIGIN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]*/;
17244
17740
  var PROTOCOL_RELATIVE_ORIGIN = /^\/\/[^/]*/;
17245
17741
  function pathAfterOrigin(text) {
@@ -17266,12 +17762,12 @@ var RestSemanticsSchema = external_exports.object({
17266
17762
  declaredResponses: external_exports.array(external_exports.number()).optional()
17267
17763
  });
17268
17764
  var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
17269
- function variableOf(label) {
17270
- const parts = label.split(".");
17271
- return parts[parts.length - 1] ?? label;
17765
+ function baseUrlReference(semantics) {
17766
+ const label = OPENING_HOLE.exec(semantics.path ?? "")?.[1];
17767
+ return label === void 0 ? null : referenceFromName(patternHole(label));
17272
17768
  }
17273
- function normalizePath(path4) {
17274
- let normalized = path4.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
17769
+ function normalizePath(path6) {
17770
+ let normalized = path6.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
17275
17771
  if (normalized.length > 1 && normalized.endsWith("/")) {
17276
17772
  normalized = normalized.slice(0, -1);
17277
17773
  }
@@ -17281,8 +17777,8 @@ function normalizePath(path4) {
17281
17777
  );
17282
17778
  return normalized;
17283
17779
  }
17284
- function pathShape(path4) {
17285
- return normalizePath(path4).replace(/\{[^}]*\}/g, "{}");
17780
+ function pathShape(path6) {
17781
+ return normalizePath(path6).replace(/\{[^}]*\}/g, "{}");
17286
17782
  }
17287
17783
  function methodsAgree(a, b) {
17288
17784
  if (a === null || b === null) {
@@ -17360,8 +17856,8 @@ var restSemantics = defineBoundarySemantics({
17360
17856
  return null;
17361
17857
  }
17362
17858
  const method = semantics.method === null ? "ANY" : semantics.method.toUpperCase();
17363
- const path4 = semantics.path === null ? "?" : normalizePath(semantics.path);
17364
- return `${method} ${path4}`;
17859
+ const path6 = semantics.path === null ? "?" : normalizePath(semantics.path);
17860
+ return `${method} ${path6}`;
17365
17861
  },
17366
17862
  /**
17367
17863
  * A route with both halves stated decides for itself: the method
@@ -17370,14 +17866,14 @@ var restSemantics = defineBoundarySemantics({
17370
17866
  * half might still be the one that handles the request, and nothing
17371
17867
  * here can settle that, so it abstains instead of refusing.
17372
17868
  */
17373
- servesRequest(semantics, method, path4) {
17869
+ servesRequest(semantics, method, path6) {
17374
17870
  if (semantics.method === null || semantics.path === null) {
17375
17871
  return "unknown";
17376
17872
  }
17377
17873
  if (!methodsAgree(semantics.method, method.toUpperCase())) {
17378
17874
  return "nomatch";
17379
17875
  }
17380
- return routePathAdmits(semantics.path, path4) ? "match" : "nomatch";
17876
+ return routePathAdmits(semantics.path, path6) ? "match" : "nomatch";
17381
17877
  },
17382
17878
  /**
17383
17879
  * A route the registration pattern admits is inside it. A route
@@ -17402,22 +17898,20 @@ var restSemantics = defineBoundarySemantics({
17402
17898
  * route parameter, and `/orders/{id}` means every id rather than a
17403
17899
  * variable somebody set.
17404
17900
  */
17405
- groundName(semantics, deployedAs) {
17406
- if (semantics.path === null) {
17407
- return null;
17408
- }
17409
- const opening = OPENING_HOLE.exec(semantics.path);
17410
- if (opening === null) {
17901
+ groundName(semantics, deployment) {
17902
+ const reference = baseUrlReference(semantics);
17903
+ if (reference === null) {
17411
17904
  return null;
17412
17905
  }
17413
- const [, label, rest] = opening;
17414
- const base = deployedAs(variableOf(label));
17906
+ const base = deployment.setTo(reference);
17415
17907
  if (base === null) {
17416
17908
  return null;
17417
17909
  }
17910
+ const rest = OPENING_HOLE.exec(semantics.path ?? "")?.[2] ?? "";
17418
17911
  const grounded = pathAfterOrigin(`${base}${rest}`);
17419
17912
  return { ...semantics, path: grounded === "" ? "/" : grounded };
17420
17913
  },
17914
+ nameReference: baseUrlReference,
17421
17915
  ruleBoundary: {
17422
17916
  // "METHOD /path": one leading token, then an absolute path.
17423
17917
  claims(raw) {
@@ -17493,6 +17987,9 @@ var StorageSemanticsSchema = external_exports.object({
17493
17987
  */
17494
17988
  accessPath: external_exports.string().nullable()
17495
17989
  });
17990
+ function containerReference(semantics) {
17991
+ return semantics.container === null ? null : referenceFromName(semantics.container);
17992
+ }
17496
17993
  var storageSemantics = defineBoundarySemantics({
17497
17994
  name: "storage",
17498
17995
  schema: StorageSemanticsSchema,
@@ -17509,7 +18006,21 @@ var storageSemantics = defineBoundarySemantics({
17509
18006
  exchangesHttpResponses: false,
17510
18007
  reportsUnpairedItself: false,
17511
18008
  identityKey: () => null,
17512
- displayLabel: storageLabel
18009
+ displayLabel: storageLabel,
18010
+ /**
18011
+ * A container the source states as a variable is whatever the
18012
+ * deployment sets that variable to. `{SUBSCRIBERS_TABLE}` in the
18013
+ * code and `prod-subscribers-v1` in the manifest are one table.
18014
+ */
18015
+ groundName(semantics, deployment) {
18016
+ const reference = containerReference(semantics);
18017
+ if (reference === null) {
18018
+ return null;
18019
+ }
18020
+ const name = deployment.setTo(reference);
18021
+ return name === null ? null : { ...semantics, container: name };
18022
+ },
18023
+ nameReference: containerReference
17513
18024
  }
17514
18025
  });
17515
18026
  function storageLabel(semantics) {
@@ -17524,6 +18035,9 @@ var UnitInvocationSemanticsSchema = DeployableUnitSchema.extend({
17524
18035
  name: external_exports.literal("unit-invocation"),
17525
18036
  instanceName: DeployableUnitSchema.shape.instanceName.nullable()
17526
18037
  });
18038
+ function calleeReference(semantics) {
18039
+ return semantics.instanceName === null ? null : referenceFromName(semantics.instanceName);
18040
+ }
17527
18041
  var unitInvocationSemantics = defineBoundarySemantics({
17528
18042
  name: "unit-invocation",
17529
18043
  schema: UnitInvocationSemanticsSchema,
@@ -17546,8 +18060,13 @@ var unitInvocationSemantics = defineBoundarySemantics({
17546
18060
  * being reported as a problem here.
17547
18061
  */
17548
18062
  reportsUnpairedItself: false,
18063
+ /**
18064
+ * A callee the source states only as a variable is not a name for
18065
+ * anything until the deployment fills it in, so it keys as nothing
18066
+ * rather than as a bucket that would agree with itself alone.
18067
+ */
17549
18068
  identityKey(semantics) {
17550
- if (semantics.instanceName === null) {
18069
+ if (semantics.instanceName === null || namesNothing(semantics.instanceName)) {
17551
18070
  return null;
17552
18071
  }
17553
18072
  return unitIdentityKey(
@@ -17563,7 +18082,23 @@ var unitInvocationSemantics = defineBoundarySemantics({
17563
18082
  semantics.deploymentTarget,
17564
18083
  semantics.instanceName
17565
18084
  );
17566
- }
18085
+ },
18086
+ /**
18087
+ * A call that reads its callee's name out of the environment
18088
+ * reaches whichever resource the template wires that variable to.
18089
+ * The logical id is the answer rather than the deployed name,
18090
+ * because the invoked unit's own summary is keyed by the logical
18091
+ * id and neither side ever writes the other's string.
18092
+ */
18093
+ groundName(semantics, deployment) {
18094
+ const reference = calleeReference(semantics);
18095
+ if (reference === null) {
18096
+ return null;
18097
+ }
18098
+ const logicalId = deployment.pointsAt(reference);
18099
+ return logicalId === null ? null : { ...semantics, instanceName: logicalId };
18100
+ },
18101
+ nameReference: calleeReference
17567
18102
  }
17568
18103
  });
17569
18104
  var SemanticsSchema = external_exports.discriminatedUnion("name", [
@@ -17975,71 +18510,10 @@ function returnTypeName(node) {
17975
18510
  }
17976
18511
  return annotation.type === "subscript" ? field(annotation, "value")?.text ?? null : annotation.text;
17977
18512
  }
17978
- function callsIn(node, found = []) {
17979
- for (const child of node.namedChildren) {
17980
- if (child === null) {
17981
- continue;
17982
- }
17983
- if (child.type === "call") {
17984
- found.push(child);
17985
- }
17986
- callsIn(child, found);
17987
- }
17988
- return found;
18513
+ function storageCallees(calls, filePath, couldMatch) {
18514
+ return chainsIn(calls).filter((chain) => couldMatch.has(methodName(chain.root))).map((chain) => field(chain.root, "function")).filter((callee) => callee !== null).map((callee) => nodeId(filePath, callee));
17989
18515
  }
17990
18516
  function storageEffects(calls, options) {
17991
- const seen = /* @__PURE__ */ new Set();
17992
- const asked = new Set(
17993
- options.facts.facts("wanted").map((row) => String(row[0]))
17994
- );
17995
- const gather = (inCalls, filePath) => {
17996
- const found = [...directStorageEffects(inCalls, { ...options, filePath })];
17997
- resolveCalls(
17998
- options.facts,
17999
- inCalls.filter((call) => options.leadsToStorage.has(methodName(call))).map((call) => field(call, "function")).filter((callee) => callee !== null).map((callee) => calleeKeyOf(filePath, callee)).filter((key) => {
18000
- const fresh = !asked.has(key);
18001
- asked.add(key);
18002
- return fresh;
18003
- })
18004
- );
18005
- for (const call of inCalls) {
18006
- const callee = field(call, "function");
18007
- if (callee === null || !options.leadsToStorage.has(methodName(call))) {
18008
- continue;
18009
- }
18010
- const key = resolvedOnce(options, calleeKeyOf(filePath, callee));
18011
- const body = key === void 0 ? void 0 : options.definitionAt(key);
18012
- if (key === void 0 || body === void 0 || seen.has(key)) {
18013
- continue;
18014
- }
18015
- seen.add(key);
18016
- const name = field(body, "name")?.text;
18017
- const origin = {
18018
- file: fileOf(key),
18019
- line: body.startPosition.row + 1,
18020
- ...name === void 0 ? {} : { function: name }
18021
- };
18022
- found.push(
18023
- ...gather(callsIn(body), fileOf(key)).map(
18024
- (effect) => effect.type === "interaction" && effect.origin === void 0 ? { ...effect, origin } : effect
18025
- )
18026
- );
18027
- }
18028
- return found;
18029
- };
18030
- return gather(calls, options.filePath);
18031
- }
18032
- function calleeKeyOf(filePath, callee) {
18033
- return callee.type === "identifier" ? `${filePath}#${callee.text}` : nodeId(filePath, callee);
18034
- }
18035
- function resolvedOnce(options, calleeKey) {
18036
- const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === calleeKey).map((row) => String(row[1]));
18037
- return resolved.length === 1 ? resolved[0] : void 0;
18038
- }
18039
- function storageCallees(calls, filePath, couldMatch) {
18040
- return chainsIn(calls).filter((chain) => couldMatch.has(methodName(chain.root))).map((chain) => field(chain.root, "function")).filter((callee) => callee !== null).map((callee) => nodeId(filePath, callee));
18041
- }
18042
- function directStorageEffects(calls, options) {
18043
18517
  if (options.patterns.length === 0) {
18044
18518
  return [];
18045
18519
  }
@@ -18097,7 +18571,7 @@ function directStorageEffects(calls, options) {
18097
18571
  // src/discovery.ts
18098
18572
  function decoratedStatements(root, module) {
18099
18573
  const found = [];
18100
- walkDescendants(
18574
+ walkDescendants2(
18101
18575
  root,
18102
18576
  { scope: module.moduleScope, enclosingFunction: null },
18103
18577
  {
@@ -18136,21 +18610,21 @@ function builtSubjects(decorated, options) {
18136
18610
  }
18137
18611
  const factsPath = options.absoluteFile ?? options.filePath;
18138
18612
  const asked = /* @__PURE__ */ new Set();
18139
- for (const { enclosingFunction, decorators } of decorated) {
18613
+ for (const { enclosingFunction: enclosingFunction2, decorators } of decorated) {
18140
18614
  for (const { node, direct } of decorators) {
18141
18615
  if (direct.module !== null && acceptedByAnyPattern(direct.module, options)) {
18142
18616
  continue;
18143
18617
  }
18144
18618
  const written = decoratorReceiver(node);
18145
18619
  if (written !== null) {
18146
- asked.add(readKey(factsPath, written.object, enclosingFunction));
18620
+ asked.add(readKey(factsPath, written.object, enclosingFunction2));
18147
18621
  }
18148
18622
  }
18149
18623
  }
18150
18624
  return subjectConstructions(facts, [...asked]);
18151
18625
  }
18152
18626
  function decoratedUnits(decoratedStatement, module, options, subjects) {
18153
- const { stmt, scope, enclosingFunction, decorators } = decoratedStatement;
18627
+ const { stmt, scope, enclosingFunction: enclosingFunction2, decorators } = decoratedStatement;
18154
18628
  const units = [];
18155
18629
  const { definition } = stripDecorators(stmt);
18156
18630
  for (const { node: decoratorNode, direct } of decorators) {
@@ -18166,7 +18640,7 @@ function decoratedUnits(decoratedStatement, module, options, subjects) {
18166
18640
  ) ?? null
18167
18641
  ) ?? builtSubjectClassification(
18168
18642
  decoratorNode,
18169
- enclosingFunction,
18643
+ enclosingFunction2,
18170
18644
  subjects,
18171
18645
  options
18172
18646
  )
@@ -18202,14 +18676,14 @@ function acceptedByAnyPattern(module, options) {
18202
18676
  (pack) => pack.discovery.some((pattern) => pattern.importModule.includes(module))
18203
18677
  );
18204
18678
  }
18205
- function builtSubjectClassification(decoratorNode, enclosingFunction, subjects, options) {
18679
+ function builtSubjectClassification(decoratorNode, enclosingFunction2, subjects, options) {
18206
18680
  const written = decoratorReceiver(decoratorNode);
18207
18681
  if (written === null) {
18208
18682
  return null;
18209
18683
  }
18210
18684
  const factsPath = options.absoluteFile ?? options.filePath;
18211
18685
  const built = subjects.get(
18212
- readKey(factsPath, written.object, enclosingFunction)
18686
+ readKey(factsPath, written.object, enclosingFunction2)
18213
18687
  );
18214
18688
  const origin = built?.origins.find(
18215
18689
  (candidate) => acceptedByAnyPattern(candidate.module, options)
@@ -18268,7 +18742,7 @@ function unitsFor(pattern, pack, decoratorModule, classification, definition, mo
18268
18742
  );
18269
18743
  }
18270
18744
  };
18271
- return dispatchByType(table, pattern);
18745
+ return dispatchByType2(table, pattern);
18272
18746
  }
18273
18747
  function readPathArgument(classification) {
18274
18748
  const first = classification.args[0];
@@ -18282,9 +18756,9 @@ function readPathArgument(classification) {
18282
18756
  }
18283
18757
  var NO_PATH_PARAMS = /* @__PURE__ */ new Set();
18284
18758
  function templateReader(template) {
18285
- return (path4) => {
18759
+ return (path6) => {
18286
18760
  const names = [];
18287
- const canonical = path4.replace(template, (_match, name) => {
18761
+ const canonical = path6.replace(template, (_match, name) => {
18288
18762
  names.push(name);
18289
18763
  return `{${name}}`;
18290
18764
  });
@@ -18297,10 +18771,10 @@ var PATH_TEMPLATE_READERS = {
18297
18771
  /<(?:[A-Za-z_]\w*(?:\(.*?\))?:)?([A-Za-z_]\w*)>/g
18298
18772
  )
18299
18773
  };
18300
- function readPathTemplate(pattern, path4, range) {
18774
+ function readPathTemplate(pattern, path6, range) {
18301
18775
  const syntax = pattern.pathParamSyntax;
18302
18776
  if (syntax === void 0) {
18303
- return writtenReading({ path: path4, paramNames: NO_PATH_PARAMS }, range);
18777
+ return writtenReading({ path: path6, paramNames: NO_PATH_PARAMS }, range);
18304
18778
  }
18305
18779
  const reader = PATH_TEMPLATE_READERS[syntax];
18306
18780
  if (reader === void 0) {
@@ -18309,7 +18783,7 @@ function readPathTemplate(pattern, path4, range) {
18309
18783
  range
18310
18784
  );
18311
18785
  }
18312
- return writtenReading(reader(path4), range);
18786
+ return writtenReading(reader(path6), range);
18313
18787
  }
18314
18788
  function classRouteUnits(pattern, pack, classification, classNode, module, options) {
18315
18789
  const pathArgument = readPathArgument(classification);
@@ -18379,7 +18853,7 @@ function readRoutePath(pattern, pathArgument, classification, module, options) {
18379
18853
  );
18380
18854
  return andThenReading(
18381
18855
  composed,
18382
- (path4, range) => readPathTemplate(pattern, path4, range)
18856
+ (path6, range) => readPathTemplate(pattern, path6, range)
18383
18857
  );
18384
18858
  }
18385
18859
  function readRoutePaths(pattern, pathArgument, classification, module, options) {
@@ -18408,7 +18882,7 @@ function readRoutePaths(pattern, pathArgument, classification, module, options)
18408
18882
  pathArgument,
18409
18883
  (literal2, range) => writtenReading(servedSpelling(pattern, prefix + literal2), range)
18410
18884
  ),
18411
- (path4, range) => readPathTemplate(pattern, path4, range)
18885
+ (path6, range) => readPathTemplate(pattern, path6, range)
18412
18886
  ),
18413
18887
  mount: { siblings: resolution.values.length, prefix }
18414
18888
  }));
@@ -18450,11 +18924,11 @@ function composeRoutePath(pattern, classification, module, options, literal2, ra
18450
18924
  );
18451
18925
  }
18452
18926
  var REPEATED_SLASH_READERS = {
18453
- kept: (path4) => path4,
18454
- merged: (path4) => path4.replace(/\/{2,}/g, "/")
18927
+ kept: (path6) => path6,
18928
+ merged: (path6) => path6.replace(/\/{2,}/g, "/")
18455
18929
  };
18456
- function servedSpelling(pattern, path4) {
18457
- return REPEATED_SLASH_READERS[pattern.pathRepeatedSlashes ?? "kept"](path4);
18930
+ function servedSpelling(pattern, path6) {
18931
+ return REPEATED_SLASH_READERS[pattern.pathRepeatedSlashes ?? "kept"](path6);
18458
18932
  }
18459
18933
  function readRouterPrefix(pattern, classification, module, options, range) {
18460
18934
  const resolution = routerResolutionOf(
@@ -18566,8 +19040,8 @@ function branchesFromReturns(readsReturnedStatus, definitionNode, effects, respo
18566
19040
  "This return gives a status this reading cannot resolve to a number, so this outcome claims none",
18567
19041
  range
18568
19042
  );
18569
- for (const path4 of paths) {
18570
- const conditions = path4.map((condition) => ({
19043
+ for (const path6 of paths) {
19044
+ const conditions = path6.map((condition) => ({
18571
19045
  sourceText: condition.sourceText,
18572
19046
  structured: condition.expression === null ? null : predicateOf(condition.expression),
18573
19047
  polarity: condition.polarity,
@@ -18596,7 +19070,7 @@ function branchesFromReturns(readsReturnedStatus, definitionNode, effects, respo
18596
19070
  },
18597
19071
  effects: effectsReaching(effects, conditions),
18598
19072
  location: range,
18599
- isDefault: path4.length === 0
19073
+ isDefault: path6.length === 0
18600
19074
  });
18601
19075
  }
18602
19076
  }
@@ -18807,17 +19281,7 @@ function buildRouteUnit(options) {
18807
19281
  () => readReturnAnnotation(definitionNode, enclosingScope, ctx)
18808
19282
  );
18809
19283
  const effects = invocationEffects(definitionNode);
18810
- const storage = storageLookup === void 0 ? [] : [
18811
- ...storageEffects(bodyCalls(definitionNode), {
18812
- ...storageLookup,
18813
- filePath: storageLookup.factsPath
18814
- }),
18815
- ...rawSqlEffects(bodyCalls(definitionNode), {
18816
- facts: storageLookup.facts,
18817
- filePath: storageLookup.factsPath,
18818
- patterns: storageLookup.rawSql ?? []
18819
- })
18820
- ];
19284
+ const extra = recognizedBodyEffects(definitionNode, module, storageLookup);
18821
19285
  const perReturn = branchesFromReturns(
18822
19286
  readsReturnedStatus,
18823
19287
  definitionNode,
@@ -18826,9 +19290,9 @@ function buildRouteUnit(options) {
18826
19290
  statusCode
18827
19291
  );
18828
19292
  const branches = (perReturn ?? []).map(
18829
- (branch) => storage.length === 0 ? branch : { ...branch, extraEffects: storage }
19293
+ (branch) => extra.length === 0 ? branch : { ...branch, extraEffects: extra }
18830
19294
  );
18831
- if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0 || effects.length > 0)) {
19295
+ if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || extra.length > 0 || effects.length > 0)) {
18832
19296
  branches.push({
18833
19297
  conditions: [],
18834
19298
  terminal: {
@@ -18848,7 +19312,7 @@ function buildRouteUnit(options) {
18848
19312
  statusCodeReading: statusCode,
18849
19313
  bodyShapeReading: { reading: responseShape.reading },
18850
19314
  effects,
18851
- ...storage.length === 0 ? {} : { extraEffects: storage },
19315
+ ...extra.length === 0 ? {} : { extraEffects: extra },
18852
19316
  location: rangeOf(definitionNode),
18853
19317
  isDefault: true
18854
19318
  });
@@ -18896,6 +19360,20 @@ function unreadRoleReadings(parameters, range) {
18896
19360
  )
18897
19361
  ];
18898
19362
  }
19363
+ function recognizedBodyEffects(definitionNode, module, storageLookup) {
19364
+ const storage = storageLookup === void 0 ? [] : [
19365
+ ...storageEffects(bodyCalls(definitionNode), {
19366
+ ...storageLookup,
19367
+ filePath: storageLookup.factsPath
19368
+ }),
19369
+ ...rawSqlEffects(bodyCalls(definitionNode), {
19370
+ facts: storageLookup.facts,
19371
+ filePath: storageLookup.factsPath,
19372
+ patterns: storageLookup.rawSql ?? []
19373
+ })
19374
+ ];
19375
+ return [...envReadEffects(definitionNode, module), ...storage];
19376
+ }
18899
19377
  function bodyContentOf(bodyNode) {
18900
19378
  for (const stmt of bodyStatements(bodyNode)) {
18901
19379
  if (stmt.type === "pass_statement") {
@@ -19031,6 +19509,11 @@ function parameterNameAndType(param) {
19031
19509
  return null;
19032
19510
  }
19033
19511
 
19512
+ // src/facts/why.ts
19513
+ import fs4 from "fs";
19514
+ import path4 from "path";
19515
+ import { Database as Database4 } from "@suss/datalog";
19516
+
19034
19517
  // src/moduleResolver.ts
19035
19518
  import fs from "fs";
19036
19519
  import path from "path";
@@ -19123,43 +19606,38 @@ function importedModuleText(module, relativeLevel) {
19123
19606
  return relativeLevel > 0 ? `${".".repeat(relativeLevel)}${module}` : module;
19124
19607
  }
19125
19608
  function emitModuleImportFacts(db, filePath, module, resolverOptions) {
19126
- const everyBinding = [...module.scopeFor.values()].flatMap((scope) => [
19127
- ...scope.bindings
19128
- ]);
19129
- for (const [localName, binding] of everyBinding) {
19130
- if (binding.kind !== "import" && binding.kind !== "importFrom") {
19131
- continue;
19132
- }
19133
- const moduleText = importedModuleText(
19134
- binding.module,
19135
- binding.relativeLevel
19136
- );
19137
- const resolution = resolveModule(
19138
- filePath,
19139
- { module: binding.module, relativeLevel: binding.relativeLevel },
19140
- resolverOptions
19141
- );
19142
- db.add("pyImport", [
19143
- filePath,
19144
- moduleText,
19145
- resolution.status === "resolved" ? "resolved" : resolution.reason
19146
- ]);
19147
- const importedName = binding.kind === "import" ? binding.localName : binding.importedName;
19148
- db.add("pyImportedName", [
19149
- `${filePath}#${localName}`,
19150
- moduleText,
19151
- importedName
19152
- ]);
19153
- if (resolution.status === "resolved") {
19154
- db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
19155
- db.add("imports", [
19156
- `${filePath}#${localName}`,
19157
- resolution.file,
19158
- importedName
19609
+ for (const scope of module.scopeFor.values()) {
19610
+ for (const [localName, binding] of scope.bindings) {
19611
+ if (binding.kind !== "import" && binding.kind !== "importFrom") {
19612
+ continue;
19613
+ }
19614
+ const nameKey = `${filePath}#${localName}`;
19615
+ const moduleText = importedModuleText(
19616
+ binding.module,
19617
+ binding.relativeLevel
19618
+ );
19619
+ const resolution = resolveModule(
19620
+ filePath,
19621
+ { module: binding.module, relativeLevel: binding.relativeLevel },
19622
+ resolverOptions
19623
+ );
19624
+ db.add("pyImport", [
19625
+ filePath,
19626
+ moduleText,
19627
+ resolution.status === "resolved" ? "resolved" : resolution.reason
19159
19628
  ]);
19160
- continue;
19629
+ const importedName = binding.kind === "import" ? binding.localName : binding.importedName;
19630
+ db.add("pyImportedName", [nameKey, moduleText, importedName]);
19631
+ if (scope.kind === "module") {
19632
+ db.add("exportsAs", [filePath, localName, nameKey]);
19633
+ }
19634
+ if (resolution.status === "resolved") {
19635
+ db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
19636
+ db.add("imports", [nameKey, resolution.file, importedName]);
19637
+ continue;
19638
+ }
19639
+ db.add("imports", [nameKey, moduleText, importedName]);
19161
19640
  }
19162
- db.add("imports", [`${filePath}#${localName}`, moduleText, importedName]);
19163
19641
  }
19164
19642
  for (const openModule of module.openImports) {
19165
19643
  db.add("pyOpenImport", [filePath, openModule]);
@@ -19178,6 +19656,7 @@ var GRAMMAR_PATH = path2.join(
19178
19656
  "tree-sitter-python.wasm"
19179
19657
  );
19180
19658
  var languagePromise = null;
19659
+ var loadedLanguage = null;
19181
19660
  function loadLanguage() {
19182
19661
  languagePromise ??= (async () => {
19183
19662
  if (!fs2.existsSync(GRAMMAR_PATH)) {
@@ -19186,12 +19665,28 @@ function loadLanguage() {
19186
19665
  );
19187
19666
  }
19188
19667
  await Parser.init();
19189
- return Language.load(GRAMMAR_PATH);
19668
+ const language = await Language.load(GRAMMAR_PATH);
19669
+ loadedLanguage = language;
19670
+ return language;
19190
19671
  })();
19191
19672
  return languagePromise;
19192
19673
  }
19193
19674
  async function parsePython(source) {
19194
19675
  const language = await loadLanguage();
19676
+ return parseWith(language, source);
19677
+ }
19678
+ async function preloadPythonGrammar() {
19679
+ await loadLanguage();
19680
+ }
19681
+ function parsePythonSync(source) {
19682
+ if (loadedLanguage === null) {
19683
+ throw new Error(
19684
+ "the Python grammar has not been loaded yet; call preloadPythonGrammar first"
19685
+ );
19686
+ }
19687
+ return parseWith(loadedLanguage, source);
19688
+ }
19689
+ function parseWith(language, source) {
19195
19690
  const parser = new Parser();
19196
19691
  parser.setLanguage(language);
19197
19692
  try {
@@ -19208,8 +19703,705 @@ async function parsePython(source) {
19208
19703
  // src/project.ts
19209
19704
  import fs3 from "fs";
19210
19705
  import path3 from "path";
19211
- import { Database, evaluate as evaluate2, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
19212
- import { assembleSummary } from "@suss/extractor";
19706
+ import {
19707
+ disambiguateSummaryIds,
19708
+ linkCallsToSummaries,
19709
+ placeArgTargets as placeArgTargets2,
19710
+ placeCalleeParameters as placeCalleeParameters2,
19711
+ placeCalls as placeCalls2,
19712
+ recordParameterGaps as recordParameterGaps2,
19713
+ summaryIdFromParts,
19714
+ unfollowedCallGap as unfollowedCallGap2
19715
+ } from "@suss/behavioral-ir";
19716
+ import { Database as Database3 } from "@suss/datalog";
19717
+ import {
19718
+ assembleSummary as assembleSummary2,
19719
+ createCacheLayer,
19720
+ createTimer,
19721
+ moduleInitStructure,
19722
+ noopTimer,
19723
+ runDigest,
19724
+ stampModuleImports
19725
+ } from "@suss/extractor";
19726
+
19727
+ // src/diagnostics.ts
19728
+ import {
19729
+ buildUngatedExtractionReport,
19730
+ createPackTallies,
19731
+ tallyUnit
19732
+ } from "@suss/extractor";
19733
+ function buildPythonExtractionReport(args) {
19734
+ return buildUngatedExtractionReport({
19735
+ packs: args.packs.map((pack) => ({
19736
+ name: pack.name,
19737
+ version: pack.version ?? null,
19738
+ discovers: pack.discovery.length > 0
19739
+ })),
19740
+ tallies: args.tallies,
19741
+ filesWalked: args.filesWalked,
19742
+ summaries: args.summaries
19743
+ });
19744
+ }
19745
+
19746
+ // src/reach/closure.ts
19747
+ import {
19748
+ functionCallBinding,
19749
+ placeArgTargets,
19750
+ placeCalleeParameters,
19751
+ placeCalls,
19752
+ recordParameterGaps,
19753
+ TargetPlacements,
19754
+ unfollowedCallGap,
19755
+ worthRecording
19756
+ } from "@suss/behavioral-ir";
19757
+ import { Database as Database2, evaluate as evaluate3, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
19758
+ import {
19759
+ assembleSummary,
19760
+ SKIP_CHILDREN as SKIP_CHILDREN2,
19761
+ walkDescendants as walkDescendants3
19762
+ } from "@suss/extractor";
19763
+
19764
+ // src/reach/resolveCallee.ts
19765
+ var stop = (reason) => ({ kind: "stop", reason });
19766
+ var MODULE_STOP = {
19767
+ ambiguous: "multipleSources",
19768
+ outsideRoots: "outsideRun"
19769
+ };
19770
+ function resolveCallee2(call, site, ctx) {
19771
+ const callee = field(call, "function");
19772
+ if (callee === null) {
19773
+ return { kind: "stopped", reason: "noDeclaration" };
19774
+ }
19775
+ return asCallee(resolveExpression(callee, site, ctx), ctx);
19776
+ }
19777
+ function resolveNamedFunctionArgument(name, site, ctx) {
19778
+ if (site.rebound.has(name)) {
19779
+ return null;
19780
+ }
19781
+ const binding = resolveName(site.scope, name);
19782
+ if (binding !== null && binding.kind === "assignment") {
19783
+ return null;
19784
+ }
19785
+ const resolved = binding === null ? throughOpenImports(site.file, name, ctx, /* @__PURE__ */ new Set()) : resolveBinding(binding, site.file, site.scope, ctx, /* @__PURE__ */ new Set());
19786
+ return resolved.kind === "function" ? resolved.target : null;
19787
+ }
19788
+ function asCallee(resolved, ctx) {
19789
+ if (resolved.kind === "function") {
19790
+ return { kind: "followed", target: resolved.target };
19791
+ }
19792
+ if (resolved.kind === "class") {
19793
+ return asCallee(
19794
+ memberOfClass(resolved.file, resolved.node, "__init__", ctx),
19795
+ ctx
19796
+ );
19797
+ }
19798
+ if (resolved.kind === "stop") {
19799
+ return { kind: "stopped", reason: resolved.reason };
19800
+ }
19801
+ if (resolved.kind === "package") {
19802
+ return { kind: "stopped", reason: "outsideRun" };
19803
+ }
19804
+ return { kind: "stopped", reason: "noDeclaration" };
19805
+ }
19806
+ var EXPRESSION_RESOLVERS = {
19807
+ identifier: (node, site, ctx) => resolveIdentifier(node.text, site, ctx),
19808
+ attribute: (node, site, ctx) => {
19809
+ const object2 = field(node, "object");
19810
+ const attribute = field(node, "attribute");
19811
+ if (object2 === null || attribute === null) {
19812
+ return stop("noDeclaration");
19813
+ }
19814
+ return memberOf(resolveExpression(object2, site, ctx), attribute.text, ctx);
19815
+ },
19816
+ // `Service().run()` runs a method on whatever the constructor built.
19817
+ call: (node, site, ctx) => {
19818
+ const callee = field(node, "function");
19819
+ if (callee === null) {
19820
+ return stop("noDeclaration");
19821
+ }
19822
+ const built = resolveExpression(callee, site, ctx);
19823
+ return built.kind === "class" ? built : stop("noDeclaration");
19824
+ },
19825
+ parenthesized_expression: (node, site, ctx) => {
19826
+ const inner = node.namedChildren[0];
19827
+ return inner === null || inner === void 0 ? stop("noDeclaration") : resolveExpression(inner, site, ctx);
19828
+ }
19829
+ };
19830
+ function resolveExpression(node, site, ctx) {
19831
+ const resolver = EXPRESSION_RESOLVERS[node.type];
19832
+ return resolver === void 0 ? stop("noDeclaration") : resolver(node, site, ctx);
19833
+ }
19834
+ function resolveIdentifier(name, site, ctx) {
19835
+ if (site.rebound.has(name)) {
19836
+ return stop("unsettledValue");
19837
+ }
19838
+ if (isReceiver(name, site.scope)) {
19839
+ return receiverClass(site);
19840
+ }
19841
+ const binding = resolveName(site.scope, name);
19842
+ if (binding === null) {
19843
+ return throughOpenImports(site.file, name, ctx, /* @__PURE__ */ new Set());
19844
+ }
19845
+ return resolveBinding(binding, site.file, site.scope, ctx, /* @__PURE__ */ new Set());
19846
+ }
19847
+ function isReceiver(name, scope) {
19848
+ return (name === "self" || name === "cls") && scope.kind === "function" && scope.parent?.kind === "class";
19849
+ }
19850
+ function receiverClass(site) {
19851
+ const classScope = site.scope.parent;
19852
+ if (classScope === null || classScope === void 0) {
19853
+ return stop("noDeclaration");
19854
+ }
19855
+ return { kind: "class", file: site.file, node: classScope.node };
19856
+ }
19857
+ var BINDING_RESOLVERS = {
19858
+ functionDef: (binding, file2) => ({
19859
+ kind: "function",
19860
+ target: reachedFunction(file2, binding.node)
19861
+ }),
19862
+ classDef: (binding, file2) => ({ kind: "class", file: file2, node: binding.node }),
19863
+ parameter: () => stop("callerSupplied"),
19864
+ // `import a.b.c` binds `a`, and `import a.b.c as m` binds `m` to `a.b.c`.
19865
+ import: (binding, file2, _scope, ctx) => moduleNamed(
19866
+ file2,
19867
+ binding.localName === binding.module.split(".")[0] ? binding.localName : binding.module,
19868
+ 0,
19869
+ ctx
19870
+ ),
19871
+ importFrom: (binding, file2, _scope, ctx, visited) => memberOf(
19872
+ moduleNamed(file2, binding.module, binding.relativeLevel, ctx),
19873
+ binding.importedName,
19874
+ ctx,
19875
+ visited
19876
+ ),
19877
+ // `run = load` is an alias worth one hop; anything else is a value.
19878
+ assignment: (binding, file2, scope, ctx, visited) => aliasedValue(binding.value, file2, scope, ctx, visited),
19879
+ global: () => stop("unsettledValue"),
19880
+ nonlocal: () => stop("unsettledValue")
19881
+ };
19882
+ function resolveBinding(binding, file2, scope, ctx, visited) {
19883
+ const resolver = BINDING_RESOLVERS[binding.kind];
19884
+ return resolver(binding, file2, scope, ctx, visited);
19885
+ }
19886
+ function aliasedValue(value, file2, scope, ctx, visited) {
19887
+ if (value === null) {
19888
+ return stop("unsettledValue");
19889
+ }
19890
+ if (value.type === "identifier") {
19891
+ const binding = resolveName(scope, value.text);
19892
+ if (binding === null || binding.kind === "assignment") {
19893
+ return stop("unsettledValue");
19894
+ }
19895
+ return resolveBinding(binding, file2, scope, ctx, visited);
19896
+ }
19897
+ if (value.type === "call") {
19898
+ const callee = field(value, "function");
19899
+ const built = callee === null ? stop("unsettledValue") : aliasedValue(callee, file2, scope, ctx, visited);
19900
+ return built.kind === "class" ? built : stop("unsettledValue");
19901
+ }
19902
+ if (value.type === "attribute") {
19903
+ const object2 = field(value, "object");
19904
+ const attribute = field(value, "attribute");
19905
+ if (object2 === null || attribute === null) {
19906
+ return stop("unsettledValue");
19907
+ }
19908
+ return memberOf(
19909
+ aliasedValue(object2, file2, scope, ctx, visited),
19910
+ attribute.text,
19911
+ ctx
19912
+ );
19913
+ }
19914
+ return stop("unsettledValue");
19915
+ }
19916
+ function reachedFunction(file2, node, owner) {
19917
+ const name = field(node, "name")?.text ?? "<anon>";
19918
+ const ownerName = owner === void 0 ? null : field(owner, "name")?.text;
19919
+ return {
19920
+ file: file2,
19921
+ node,
19922
+ name,
19923
+ exportPath: ownerName === null || ownerName === void 0 ? [name] : [ownerName, name]
19924
+ };
19925
+ }
19926
+ function moduleNamed(from, module, relativeLevel, ctx) {
19927
+ const resolution = resolveModule(
19928
+ from.file,
19929
+ { module, relativeLevel },
19930
+ { roots: ctx.roots }
19931
+ );
19932
+ if (resolution.status !== "resolved") {
19933
+ return resolution.reason === "external" ? { kind: "package", dotted: module, relativeLevel, from } : stop(MODULE_STOP[resolution.reason]);
19934
+ }
19935
+ const file2 = ctx.filesByPath.get(resolution.file);
19936
+ return file2 === void 0 ? stop("outsideRun") : { kind: "module", file: file2 };
19937
+ }
19938
+ function memberOf(base, name, ctx, visited = /* @__PURE__ */ new Set()) {
19939
+ if (base.kind === "module") {
19940
+ return memberOfModule(base.file, name, ctx, visited);
19941
+ }
19942
+ if (base.kind === "package") {
19943
+ return moduleNamed(
19944
+ base.from,
19945
+ base.dotted === "" ? name : `${base.dotted}.${name}`,
19946
+ base.relativeLevel,
19947
+ ctx
19948
+ );
19949
+ }
19950
+ if (base.kind === "class") {
19951
+ return memberOfClass(base.file, base.node, name, ctx);
19952
+ }
19953
+ if (base.kind === "stop") {
19954
+ return base;
19955
+ }
19956
+ return stop("noDeclaration");
19957
+ }
19958
+ function memberOfClass(file2, classNode, name, ctx) {
19959
+ const classScope = file2.module.scopeFor.get(classNode.id);
19960
+ const binding = classScope?.bindings.get(name);
19961
+ if (binding === void 0) {
19962
+ return stop("noDeclaration");
19963
+ }
19964
+ if (binding.kind === "functionDef") {
19965
+ return {
19966
+ kind: "function",
19967
+ target: reachedFunction(file2, binding.node, classNode)
19968
+ };
19969
+ }
19970
+ return resolveBinding(binding, file2, classScope, ctx, /* @__PURE__ */ new Set());
19971
+ }
19972
+ function memberOfModule(file2, name, ctx, visited) {
19973
+ const key = `${file2.file}#${name}`;
19974
+ if (visited.has(key)) {
19975
+ return stop("unsettledValue");
19976
+ }
19977
+ visited.add(key);
19978
+ const binding = file2.module.moduleScope.bindings.get(name);
19979
+ if (binding !== void 0) {
19980
+ return resolveBinding(binding, file2, file2.module.moduleScope, ctx, visited);
19981
+ }
19982
+ if (file2.file.endsWith("__init__.py")) {
19983
+ const submodule = moduleNamed(file2, name, 1, ctx);
19984
+ if (submodule.kind === "module") {
19985
+ return submodule;
19986
+ }
19987
+ }
19988
+ return throughOpenImports(file2, name, ctx, visited);
19989
+ }
19990
+ function throughOpenImports(file2, name, ctx, visited) {
19991
+ const found = [];
19992
+ for (const spec of file2.module.openImports) {
19993
+ const dots = spec.length - spec.replace(/^\.+/, "").length;
19994
+ const opened = moduleNamed(file2, spec.slice(dots), dots, ctx);
19995
+ if (opened.kind !== "module") {
19996
+ continue;
19997
+ }
19998
+ const member = memberOfModule(opened.file, name, ctx, visited);
19999
+ if (member.kind !== "stop") {
20000
+ found.push(member);
20001
+ }
20002
+ }
20003
+ if (found.length > 1) {
20004
+ return stop("multipleSources");
20005
+ }
20006
+ return found[0] ?? stop("noDeclaration");
20007
+ }
20008
+
20009
+ // src/reach/closure.ts
20010
+ var REACHABLE_RULES = [
20011
+ rule2("reachable", [v2("f")], [lit2("entry", v2("f"))]),
20012
+ rule2(
20013
+ "reachable",
20014
+ [v2("g")],
20015
+ [lit2("reachable", v2("f")), lit2("calls", v2("f"), v2("g"))]
20016
+ )
20017
+ ];
20018
+ function reachedFunctions(seeds, options) {
20019
+ const ctx = {
20020
+ filesByPath: new Map(options.files.map((file2) => [file2.file, file2])),
20021
+ roots: options.roots
20022
+ };
20023
+ const db = new Database2();
20024
+ const functionByKey = /* @__PURE__ */ new Map();
20025
+ const seedKeys = /* @__PURE__ */ new Set();
20026
+ const scanned = /* @__PURE__ */ new Set();
20027
+ const targetsByKey = /* @__PURE__ */ new Map();
20028
+ const stopsByKey = /* @__PURE__ */ new Map();
20029
+ const argTargetsByKey = /* @__PURE__ */ new Map();
20030
+ const parameterCallsByKey = /* @__PURE__ */ new Map();
20031
+ const passedPositions = /* @__PURE__ */ new Set();
20032
+ for (const seed of seeds) {
20033
+ seedKeys.add(seed.key);
20034
+ functionByKey.set(seed.key, {
20035
+ file: seed.file,
20036
+ node: seed.node,
20037
+ name: field(seed.node, "name")?.text ?? "<anon>",
20038
+ exportPath: []
20039
+ });
20040
+ db.add("entry", [seed.key]);
20041
+ }
20042
+ for (; ; ) {
20043
+ evaluate3(db, REACHABLE_RULES);
20044
+ const frontier = db.facts("reachable").map(([key]) => String(key)).filter((key) => !scanned.has(key));
20045
+ if (frontier.length === 0) {
20046
+ break;
20047
+ }
20048
+ for (const key of frontier) {
20049
+ scanned.add(key);
20050
+ const source = functionByKey.get(key);
20051
+ if (source === void 0) {
20052
+ continue;
20053
+ }
20054
+ const scan = scanBody(source, ctx);
20055
+ if (scan.stops.length > 0) {
20056
+ stopsByKey.set(key, scan.stops);
20057
+ }
20058
+ targetsByKey.set(key, scan.targets);
20059
+ argTargetsByKey.set(key, scan.argTargets);
20060
+ if (scan.parameterCalls.length > 0) {
20061
+ parameterCallsByKey.set(key, scan.parameterCalls);
20062
+ }
20063
+ for (const position of scan.passedPositions) {
20064
+ passedPositions.add(position);
20065
+ }
20066
+ for (const target of scan.followed) {
20067
+ const calleeKey = keyOf(target);
20068
+ if (!functionByKey.has(calleeKey)) {
20069
+ functionByKey.set(calleeKey, target);
20070
+ }
20071
+ db.add("calls", [key, calleeKey]);
20072
+ }
20073
+ }
20074
+ }
20075
+ const summaries = [];
20076
+ const summariesByKey = /* @__PURE__ */ new Map();
20077
+ for (const [keyAtom] of db.facts("reachable")) {
20078
+ const key = String(keyAtom);
20079
+ const target = functionByKey.get(key);
20080
+ if (seedKeys.has(key) || target === void 0) {
20081
+ continue;
20082
+ }
20083
+ const summary = assembleSummary(libraryUnit(target, options), {
20084
+ gapHandling: options.gapHandling
20085
+ });
20086
+ summary.confidence = { source: "inferred_static", level: "low" };
20087
+ if (options.gapHandling !== "silent") {
20088
+ summary.gaps.push(...(stopsByKey.get(key) ?? []).map(unfollowedCallGap));
20089
+ }
20090
+ placeCalls(summary, targetsByKey.get(key));
20091
+ placeArgTargets(summary, argTargetsByKey.get(key));
20092
+ placeCalleeParameters(summary, parameterCallsByKey.get(key));
20093
+ summariesByKey.set(key, [summary]);
20094
+ summaries.push(summary);
20095
+ }
20096
+ if (options.gapHandling !== "silent") {
20097
+ recordParameterGaps(parameterCallsByKey, summariesByKey, passedPositions);
20098
+ }
20099
+ return {
20100
+ summaries,
20101
+ targetsByKey,
20102
+ stopsByKey,
20103
+ argTargetsByKey,
20104
+ parameterCallsByKey,
20105
+ passedPositions
20106
+ };
20107
+ }
20108
+ function keyOf(target) {
20109
+ return nodeId(target.file.file, target.node);
20110
+ }
20111
+ var EMPTY_SCAN = {
20112
+ followed: [],
20113
+ stops: [],
20114
+ targets: /* @__PURE__ */ new Map(),
20115
+ argTargets: /* @__PURE__ */ new Map(),
20116
+ parameterCalls: [],
20117
+ passedPositions: /* @__PURE__ */ new Set()
20118
+ };
20119
+ function scanBody(source, ctx) {
20120
+ const followed = [];
20121
+ const stops = [];
20122
+ const placements = new TargetPlacements();
20123
+ const parameterCalls = [];
20124
+ const passedPositions = /* @__PURE__ */ new Set();
20125
+ const seen = /* @__PURE__ */ new Set();
20126
+ const parameterCallsSeen = /* @__PURE__ */ new Set();
20127
+ const { file: file2, node } = source;
20128
+ const body = field(node, "body");
20129
+ if (body === null) {
20130
+ return EMPTY_SCAN;
20131
+ }
20132
+ const ownParameters = callParameterNames(node, source.exportPath.length > 1);
20133
+ const recordPassedArgs = (call, callee, calleeKey, where) => {
20134
+ const args = field(call, "arguments");
20135
+ if (args === null) {
20136
+ return;
20137
+ }
20138
+ args.namedChildren.forEach((arg, position) => {
20139
+ if (arg === null || arg.type !== "identifier") {
20140
+ return;
20141
+ }
20142
+ const resolved = resolveNamedFunctionArgument(
20143
+ arg.text,
20144
+ { file: file2, scope: where.scope, rebound: where.rebound },
20145
+ ctx
20146
+ );
20147
+ if (resolved === null) {
20148
+ return;
20149
+ }
20150
+ const key = keyOf(resolved);
20151
+ if (!seen.has(key)) {
20152
+ seen.add(key);
20153
+ followed.push(resolved);
20154
+ }
20155
+ placements.placeArg(callee, position, {
20156
+ file: resolved.file.displayPath,
20157
+ span: spanOf(resolved.node)
20158
+ });
20159
+ if (calleeKey !== null) {
20160
+ passedPositions.add(`${calleeKey}#${position}`);
20161
+ }
20162
+ });
20163
+ };
20164
+ const record2 = (call, where) => {
20165
+ const callee = calleeText(call);
20166
+ const outcome = resolveCallee2(
20167
+ call,
20168
+ { file: file2, scope: where.scope, rebound: where.rebound },
20169
+ ctx
20170
+ );
20171
+ const placed = outcome.kind === "followed" ? {
20172
+ file: outcome.target.file.displayPath,
20173
+ span: spanOf(outcome.target.node)
20174
+ } : outcome.reason === "noDeclaration" ? null : { file: file2.displayPath, span: spanOf(call) };
20175
+ placements.place(callee, placed);
20176
+ recordPassedArgs(
20177
+ call,
20178
+ callee,
20179
+ outcome.kind === "followed" ? keyOf(outcome.target) : null,
20180
+ where
20181
+ );
20182
+ if (outcome.kind === "stopped") {
20183
+ const stopKey = `${outcome.reason}:${callee}`;
20184
+ if (!seen.has(stopKey) && worthRecording(outcome.reason)) {
20185
+ seen.add(stopKey);
20186
+ stops.push({ callee, reason: outcome.reason });
20187
+ }
20188
+ if (outcome.reason === "callerSupplied" && !parameterCallsSeen.has(callee)) {
20189
+ const parameterIndex = ownParameters.indexOf(callee);
20190
+ if (parameterIndex !== -1) {
20191
+ parameterCallsSeen.add(callee);
20192
+ parameterCalls.push({ callee, parameterIndex });
20193
+ }
20194
+ }
20195
+ return;
20196
+ }
20197
+ const key = keyOf(outcome.target);
20198
+ if (!seen.has(key)) {
20199
+ seen.add(key);
20200
+ followed.push(outcome.target);
20201
+ }
20202
+ };
20203
+ walkDescendants3(
20204
+ body,
20205
+ { scope: scopeAt(file2, node), rebound: reboundNames(body) },
20206
+ {
20207
+ at: (child, where) => {
20208
+ if (child.type === "call") {
20209
+ record2(child, where);
20210
+ }
20211
+ },
20212
+ into: (child, where) => {
20213
+ if (child.type === "function_definition") {
20214
+ return SKIP_CHILDREN2;
20215
+ }
20216
+ if (child.type === "lambda") {
20217
+ return {
20218
+ scope: where.scope,
20219
+ rebound: /* @__PURE__ */ new Set([...where.rebound, ...lambdaParameters(child)])
20220
+ };
20221
+ }
20222
+ return {
20223
+ scope: file2.module.scopeFor.get(child.id) ?? where.scope,
20224
+ rebound: where.rebound
20225
+ };
20226
+ }
20227
+ }
20228
+ );
20229
+ return {
20230
+ followed,
20231
+ stops,
20232
+ targets: placements.targets,
20233
+ argTargets: placements.argTargets,
20234
+ parameterCalls,
20235
+ passedPositions
20236
+ };
20237
+ }
20238
+ function scopeAt(file2, node) {
20239
+ let current = node;
20240
+ while (current !== null) {
20241
+ const scope = file2.module.scopeFor.get(current.id);
20242
+ if (scope !== void 0) {
20243
+ return scope;
20244
+ }
20245
+ current = current.parent;
20246
+ }
20247
+ return file2.module.moduleScope;
20248
+ }
20249
+ var REBINDING_FIELDS = {
20250
+ assignment: "left",
20251
+ augmented_assignment: "left",
20252
+ named_expression: "name",
20253
+ for_statement: "left",
20254
+ for_in_clause: "left",
20255
+ as_pattern: "alias"
20256
+ };
20257
+ function reboundNames(body) {
20258
+ const names = /* @__PURE__ */ new Set();
20259
+ walkDescendants3(body, null, {
20260
+ at: (node) => {
20261
+ const target = REBINDING_FIELDS[node.type];
20262
+ if (target === void 0 || boundByBinder(node, body)) {
20263
+ return;
20264
+ }
20265
+ const written = field(node, target);
20266
+ if (written !== null) {
20267
+ for (const name of identifiersUnder(written)) {
20268
+ names.add(name);
20269
+ }
20270
+ }
20271
+ },
20272
+ into: (node) => node.type === "function_definition" ? SKIP_CHILDREN2 : null
20273
+ });
20274
+ return names;
20275
+ }
20276
+ function boundByBinder(node, body) {
20277
+ if (node.type !== "assignment") {
20278
+ return false;
20279
+ }
20280
+ const statement = node.parent;
20281
+ return field(node, "left")?.type === "identifier" && statement?.type === "expression_statement" && statement.parent?.id === body.id;
20282
+ }
20283
+ function identifiersUnder(node, found = []) {
20284
+ if (node.type === "identifier") {
20285
+ found.push(node.text);
20286
+ return found;
20287
+ }
20288
+ for (const child of node.namedChildren) {
20289
+ if (child !== null && child.type !== "attribute" && child.type !== "subscript") {
20290
+ identifiersUnder(child, found);
20291
+ }
20292
+ }
20293
+ return found;
20294
+ }
20295
+ function lambdaParameters(lambda) {
20296
+ const parameters = field(lambda, "parameters");
20297
+ return parameters === null ? [] : identifiersUnder(parameters);
20298
+ }
20299
+ function libraryUnit(target, options) {
20300
+ const { file: file2, node, name, exportPath } = target;
20301
+ const body = field(node, "body");
20302
+ const extra = recognizedBodyEffects(
20303
+ node,
20304
+ file2.module,
20305
+ options.storageFor(file2)
20306
+ );
20307
+ const range = rangeOf(node);
20308
+ return {
20309
+ identity: {
20310
+ name,
20311
+ nameKind: "binding",
20312
+ kind: "library",
20313
+ file: file2.displayPath,
20314
+ range,
20315
+ span: spanOf(node),
20316
+ exportName: exportPath[0] ?? name,
20317
+ exportPath
20318
+ },
20319
+ boundaryBinding: functionCallBinding({
20320
+ transport: "in-process",
20321
+ recognition: "reachable"
20322
+ }),
20323
+ parameters: positionalParameters(node, exportPath.length > 1),
20324
+ branches: [
20325
+ {
20326
+ conditions: [],
20327
+ terminal: {
20328
+ kind: "return",
20329
+ statusCode: null,
20330
+ body: null,
20331
+ exceptionType: null,
20332
+ message: null,
20333
+ component: null,
20334
+ renderTree: null,
20335
+ delegateTarget: null,
20336
+ emitEvent: null,
20337
+ location: range
20338
+ },
20339
+ effects: invocationEffects(node),
20340
+ ...extra.length === 0 ? {} : { extraEffects: extra },
20341
+ location: range,
20342
+ isDefault: true
20343
+ }
20344
+ ],
20345
+ bodyContent: body === null ? "absent" : bodyContentOf(body),
20346
+ dependencyCalls: [],
20347
+ declaredContract: null
20348
+ };
20349
+ }
20350
+ function positionalParameters(node, isMethod) {
20351
+ const parameters = field(node, "parameters");
20352
+ if (parameters === null) {
20353
+ return [];
20354
+ }
20355
+ const out = [];
20356
+ let position = 0;
20357
+ for (const param of parameters.namedChildren) {
20358
+ if (param === null) {
20359
+ continue;
20360
+ }
20361
+ const name = parameterName3(param);
20362
+ if (name === null) {
20363
+ continue;
20364
+ }
20365
+ if (isMethod && position === 0 && (name === "self" || name === "cls")) {
20366
+ position += 1;
20367
+ continue;
20368
+ }
20369
+ out.push({ name, position, role: name, typeText: null });
20370
+ position += 1;
20371
+ }
20372
+ return out;
20373
+ }
20374
+ function callParameterNames(node, isMethod) {
20375
+ const parameters = field(node, "parameters");
20376
+ if (parameters === null) {
20377
+ return [];
20378
+ }
20379
+ const names = [];
20380
+ for (const param of parameters.namedChildren) {
20381
+ if (param === null) {
20382
+ continue;
20383
+ }
20384
+ const name = parameterName3(param);
20385
+ if (name === null) {
20386
+ continue;
20387
+ }
20388
+ if (isMethod && names.length === 0 && (name === "self" || name === "cls")) {
20389
+ continue;
20390
+ }
20391
+ names.push(name);
20392
+ }
20393
+ return names;
20394
+ }
20395
+ function parameterName3(param) {
20396
+ if (param.type === "identifier") {
20397
+ return param.text;
20398
+ }
20399
+ const named = field(param, "name");
20400
+ if (named !== null && named.type === "identifier") {
20401
+ return named.text;
20402
+ }
20403
+ return identifiersUnder(param)[0] ?? null;
20404
+ }
19213
20405
 
19214
20406
  // src/routers.ts
19215
20407
  var UNSTATED_PREFIX = { kind: "unstated" };
@@ -19484,6 +20676,9 @@ function buildPatternIndex(files, importModule, composition, resolverOptions) {
19484
20676
  for (const bound of files) {
19485
20677
  collectConstructions(bound, importModule, composition, index);
19486
20678
  }
20679
+ for (const bound of files) {
20680
+ collectReturnedConstructions(bound, importModule, composition, index);
20681
+ }
19487
20682
  if (objectPrefix?.carrier !== void 0) {
19488
20683
  const carrier = objectPrefix.carrier;
19489
20684
  for (const bound of files) {
@@ -19659,6 +20854,37 @@ function collectConstructions(bound, importModule, composition, index) {
19659
20854
  );
19660
20855
  }
19661
20856
  }
20857
+ function collectReturnedConstructions(bound, importModule, composition, index) {
20858
+ for (const stmt of bodyStatements(bound.root)) {
20859
+ const definition = stripDecorators(stmt).definition;
20860
+ if (definition.type !== "function_definition") {
20861
+ continue;
20862
+ }
20863
+ const scope = bound.module.scopeFor.get(definition.id);
20864
+ for (const inner of nestedStatements(definition)) {
20865
+ if (inner.type !== "return_statement" || scope === void 0) {
20866
+ continue;
20867
+ }
20868
+ const returned = inner.namedChildren[0];
20869
+ if (returned?.type !== "call") {
20870
+ continue;
20871
+ }
20872
+ const callee = field(returned, "function");
20873
+ const constructorName = callee === null ? null : importedConstructorName(callee, scope, importModule);
20874
+ if (constructorName !== composition.routerConstructorName) {
20875
+ continue;
20876
+ }
20877
+ const { keywordArgs } = readCallArguments(field(returned, "arguments"));
20878
+ const valueKey2 = nodeId(bound.file, returned);
20879
+ index.byValueKey.set(valueKey2, {
20880
+ constructorName,
20881
+ valueKey: valueKey2,
20882
+ prefix: constructorPrefix(keywordArgs, composition, scope),
20883
+ reassigned: false
20884
+ });
20885
+ }
20886
+ }
20887
+ }
19662
20888
  function collectCarrierConstructions(carrier, composition, scan) {
19663
20889
  walkStatements(
19664
20890
  bodyStatements(scan.bound.root),
@@ -19717,6 +20943,21 @@ function constructionNamed(name, scope, scan, constructions = scan.index.constru
19717
20943
  };
19718
20944
  return resolvers[binding.kind]?.() ?? null;
19719
20945
  }
20946
+ function constructionThroughFacts(name, scope, scan) {
20947
+ const facts = scan.index.facts;
20948
+ const binding = resolveName(scope, name);
20949
+ if (facts === void 0 || binding === null) {
20950
+ return null;
20951
+ }
20952
+ if (binding.kind !== "assignment" && binding.kind !== "importFrom") {
20953
+ return null;
20954
+ }
20955
+ const nameKey = `${scan.bound.file}#${name}`;
20956
+ const constructionKey = subjectConstructions(facts, [nameKey]).get(
20957
+ nameKey
20958
+ )?.constructionKey;
20959
+ return constructionKey === void 0 ? null : scan.index.byValueKey.get(constructionKey) ?? null;
20960
+ }
19720
20961
  var MOUNT_STATE_BY_EFFECT = {
19721
20962
  prefixes: (statedPrefix, site) => ({
19722
20963
  kind: "mounted",
@@ -19886,10 +21127,10 @@ function constructionsReturnedBy(target, index) {
19886
21127
  return { kind: "unread" };
19887
21128
  }
19888
21129
  const contained = containedValues(facts, returned);
19889
- resolveCalls(facts, contained);
21130
+ const settled = subjectConstructions(facts, contained);
19890
21131
  const found = [];
19891
21132
  for (const value of contained) {
19892
- const resolved = resolvedValueKey(facts, value);
21133
+ const resolved = settled.get(value)?.constructionKey ?? value;
19893
21134
  const construction = index.byValueKey.get(resolved);
19894
21135
  if (construction === void 0) {
19895
21136
  return {
@@ -19904,10 +21145,6 @@ function constructionsReturnedBy(target, index) {
19904
21145
  }
19905
21146
  return { kind: "settled", found };
19906
21147
  }
19907
- function resolvedValueKey(facts, value) {
19908
- const row = facts.facts("wantedIsWrittenAs").find((entry) => String(entry[0]) === value);
19909
- return row === void 0 ? value : String(row[1]);
19910
- }
19911
21148
  function mountedConstructions(arg, position, scan) {
19912
21149
  if (arg?.kind === "attribute") {
19913
21150
  const construction = constructionThroughModule(arg, position.scope, scan);
@@ -19917,7 +21154,7 @@ function mountedConstructions(arg, position, scan) {
19917
21154
  return [];
19918
21155
  }
19919
21156
  const named = (name) => {
19920
- const construction = constructionNamed(name, position.scope, scan);
21157
+ const construction = constructionNamed(name, position.scope, scan) ?? constructionThroughFacts(name, position.scope, scan);
19921
21158
  return construction === null ? [] : [construction];
19922
21159
  };
19923
21160
  const target = position.loopBindings.get(arg.name);
@@ -20304,36 +21541,69 @@ function carrierPrefix(carrier, constructorArg, objectName, scope, scan) {
20304
21541
  return readings[construction.prefix.kind]();
20305
21542
  }
20306
21543
 
21544
+ // src/version.ts
21545
+ import { createAdapterStamp } from "@suss/extractor";
21546
+ var ADAPTER_VERSION = "0.1.0";
21547
+ var adapterStamp = createAdapterStamp({
21548
+ moduleUrl: import.meta.url,
21549
+ version: ADAPTER_VERSION
21550
+ });
21551
+
20307
21552
  // src/project.ts
20308
- function reportUnresolvedProjectModules(options) {
21553
+ function reportUnresolvedProjectModules(options, db) {
21554
+ const imported = new Set(db.facts("pyImport").map((row) => String(row[1])));
20309
21555
  for (const pack of options.packs) {
20310
21556
  for (const module of pack.projectModules ?? []) {
20311
- const resolved = resolveAbsoluteModule(module, { roots: options.roots });
20312
- if (resolved.status === "resolved") {
21557
+ if (imported.has(module)) {
20313
21558
  continue;
20314
21559
  }
20315
21560
  process.stderr.write(
20316
- `[suss] ${pack.name}: the configured module ${module} does not resolve under ${options.roots.join(", ")}, so nothing it wraps will be discovered.
21561
+ `[suss] ${pack.name}: no file under ${options.roots.join(", ")} imports ${module}, so the stub for it changes nothing.
20317
21562
  `
20318
21563
  );
20319
21564
  }
20320
21565
  }
20321
21566
  }
20322
21567
  async function extractPythonProject(options) {
20323
- const db = new Database();
21568
+ const timer = options.onTiming !== void 0 ? createTimer() : noopTimer();
21569
+ const cacheDir = adapterStamp.declineWhenRunFromSource(
21570
+ options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? path3.join(options.projectRoot, ".suss", "cache") : null)
21571
+ );
21572
+ const cache = createCacheLayer(cacheDir);
21573
+ const packsDigest = adapterStamp.packsDigest(
21574
+ options.packs.map(
21575
+ (pack) => pack.version !== void 0 ? { name: pack.name, version: pack.version } : { name: pack.name }
21576
+ )
21577
+ );
21578
+ const cacheInput = {
21579
+ files: cacheDir === null ? [] : options.files,
21580
+ adapterPacksDigest: cacheDir === null ? packsDigest : runDigest(packsDigest, options.packs, options.files)
21581
+ };
21582
+ const lookup = await timer.timeAsync(
21583
+ "cache.lookup",
21584
+ () => cache.lookup(cacheInput)
21585
+ );
21586
+ options.onCacheDiagnostic?.(lookup.diagnostic);
21587
+ if (lookup.kind === "hit") {
21588
+ options.onTiming?.(timer.report());
21589
+ return { summaries: lookup.summaries, facts: new Database3() };
21590
+ }
21591
+ const db = new Database3();
20324
21592
  const summaries = [];
20325
21593
  const gapHandling = options.gapHandling ?? "permissive";
21594
+ const tallies = createPackTallies(options.packs);
20326
21595
  const displayPathOf = (file2) => options.workspaceRoot !== void 0 ? path3.relative(options.workspaceRoot, file2) : file2;
20327
- reportUnresolvedProjectModules(options);
20328
21596
  const bound = [];
20329
21597
  for (const file2 of options.files) {
20330
- const source = fs3.readFileSync(file2, "utf8");
20331
- const tree = await parsePython(source);
20332
- bound.push({
20333
- file: file2,
20334
- displayPath: displayPathOf(file2),
20335
- root: tree.rootNode,
20336
- module: bindModule(tree.rootNode)
21598
+ await timer.timeAsync("parse", async () => {
21599
+ const source = fs3.readFileSync(file2, "utf8");
21600
+ const tree = await parsePython(source);
21601
+ bound.push({
21602
+ file: file2,
21603
+ displayPath: displayPathOf(file2),
21604
+ root: tree.rootNode,
21605
+ module: bindModule(tree.rootNode)
21606
+ });
20337
21607
  });
20338
21608
  }
20339
21609
  const mountsRouters = options.packs.some(
@@ -20344,81 +21614,172 @@ async function extractPythonProject(options) {
20344
21614
  const discovers = options.packs.some((pack) => pack.discovery.length > 0);
20345
21615
  const needsValues = discovers || mountsRouters || storagePatterns.length > 0;
20346
21616
  const definitions = /* @__PURE__ */ new Map();
20347
- for (const { file: file2, root, module: moduleBinding } of bound) {
20348
- emitModuleImportFacts(db, file2, moduleBinding, { roots: options.roots });
20349
- if (needsValues) {
20350
- emitValueFacts(db, file2, root);
20351
- }
20352
- if (storagePatterns.length > 0) {
21617
+ timer.time("discover", () => {
21618
+ for (const { file: file2, root, module: moduleBinding } of bound) {
21619
+ emitModuleImportFacts(db, file2, moduleBinding, { roots: options.roots });
21620
+ if (needsValues) {
21621
+ emitValueFacts(db, file2, root);
21622
+ }
20353
21623
  indexDefinitions(definitions, file2, root);
20354
21624
  }
20355
- }
20356
- const couldMatch = methodsDeclaredNear(db, storagePatterns, definitions);
20357
- const startsAQuery = /* @__PURE__ */ new Set([
20358
- ...couldMatch,
20359
- ...storagePatterns.flatMap((pattern) => pattern.queryFunctions ?? [])
20360
- ]);
20361
- const leadsToStorage = functionsReachingStorage(
20362
- db,
20363
- definitions,
20364
- startsAQuery
20365
- );
20366
- const routerIndex = buildRouterIndex(bound, options.packs, {
20367
- roots: options.roots,
20368
- ...mountsRouters ? { facts: db } : {}
20369
21625
  });
20370
- for (const { file: file2, root, module: moduleBinding } of bound) {
21626
+ reportUnresolvedProjectModules(options, db);
21627
+ const couldMatch = timer.time(
21628
+ "discover",
21629
+ () => methodsDeclaredNear(db, storagePatterns, definitions)
21630
+ );
21631
+ const routerIndex = timer.time(
21632
+ "discover",
21633
+ () => buildRouterIndex(bound, options.packs, {
21634
+ roots: options.roots,
21635
+ ...mountsRouters ? { facts: db } : {}
21636
+ })
21637
+ );
21638
+ const storageFor = (file2) => storagePatterns.length > 0 || rawSqlPatterns.length > 0 ? {
21639
+ facts: db,
21640
+ factsPath: file2.file,
21641
+ patterns: storagePatterns,
21642
+ definitionAt: (key) => definitions.get(key),
21643
+ couldMatch,
21644
+ rawSql: rawSqlPatterns
21645
+ } : void 0;
21646
+ const seeds = [];
21647
+ const summariesBySeed = /* @__PURE__ */ new Map();
21648
+ for (const boundFile of bound) {
21649
+ const { file: file2, root, module: moduleBinding } = boundFile;
20371
21650
  const displayPath = displayPathOf(file2);
20372
- const rawUnits = discoverUnits(root, moduleBinding, {
20373
- packs: options.packs,
20374
- filePath: displayPath,
20375
- absoluteFile: file2,
20376
- routerIndex,
20377
- gapHandling,
20378
- ...needsValues ? { facts: db } : {},
20379
- ...storagePatterns.length > 0 || rawSqlPatterns.length > 0 ? {
20380
- storage: {
20381
- facts: db,
20382
- factsPath: file2,
20383
- patterns: storagePatterns,
20384
- definitionAt: (key) => definitions.get(key),
20385
- couldMatch,
20386
- leadsToStorage,
20387
- rawSql: rawSqlPatterns
20388
- }
20389
- } : {}
20390
- });
21651
+ const storage = storageFor(boundFile);
21652
+ const rawUnits = timer.time(
21653
+ "discover",
21654
+ () => discoverUnits(root, moduleBinding, {
21655
+ packs: options.packs,
21656
+ filePath: displayPath,
21657
+ absoluteFile: file2,
21658
+ routerIndex,
21659
+ gapHandling,
21660
+ ...needsValues ? { facts: db } : {},
21661
+ ...storage === void 0 ? {} : { storage }
21662
+ })
21663
+ );
20391
21664
  for (const raw of rawUnits) {
20392
- const summary = assembleSummary(raw, { gapHandling });
21665
+ const summary = timer.time(
21666
+ "summarize",
21667
+ () => assembleSummary2(raw, { gapHandling })
21668
+ );
20393
21669
  summary.confidence = { source: "inferred_static", level: "low" };
20394
21670
  summaries.push(summary);
20395
21671
  emitEntryFact(db, file2, raw.identity.range, raw.identity.name);
21672
+ tallyUnit(tallies, raw.boundaryBinding?.recognition);
21673
+ const span = raw.identity.span;
21674
+ const key = span === void 0 ? null : `${file2}:${span.start}-${span.end}`;
21675
+ const node = key === null ? void 0 : definitions.get(key);
21676
+ if (key === null || node === void 0) {
21677
+ continue;
21678
+ }
21679
+ const sharing = summariesBySeed.get(key);
21680
+ if (sharing === void 0) {
21681
+ seeds.push({ key, file: boundFile, node });
21682
+ summariesBySeed.set(key, [summary]);
21683
+ } else {
21684
+ sharing.push(summary);
21685
+ }
20396
21686
  }
21687
+ const loadTimeReads = timer.time(
21688
+ "discover",
21689
+ () => envReadEffects(root, moduleBinding)
21690
+ );
21691
+ if (loadTimeReads.length > 0) {
21692
+ const summary = timer.time(
21693
+ "summarize",
21694
+ () => assembleSummary2(
21695
+ moduleInitStructure({
21696
+ name: path3.basename(displayPath),
21697
+ file: displayPath,
21698
+ range: rangeOf(root),
21699
+ effects: loadTimeReads
21700
+ }),
21701
+ { gapHandling }
21702
+ )
21703
+ );
21704
+ summary.confidence = { source: "inferred_static", level: "low" };
21705
+ summaries.push(summary);
21706
+ }
21707
+ }
21708
+ const reached = timer.time(
21709
+ "summarize",
21710
+ () => reachedFunctions(seeds, {
21711
+ files: bound,
21712
+ roots: options.roots,
21713
+ gapHandling,
21714
+ storageFor
21715
+ })
21716
+ );
21717
+ for (const [key, owners] of summariesBySeed) {
21718
+ for (const summary of owners) {
21719
+ if (gapHandling !== "silent") {
21720
+ summary.gaps.push(
21721
+ ...(reached.stopsByKey.get(key) ?? []).map(unfollowedCallGap2)
21722
+ );
21723
+ }
21724
+ placeCalls2(summary, reached.targetsByKey.get(key));
21725
+ placeArgTargets2(summary, reached.argTargetsByKey.get(key));
21726
+ placeCalleeParameters2(summary, reached.parameterCallsByKey.get(key));
21727
+ }
21728
+ }
21729
+ if (gapHandling !== "silent") {
21730
+ recordParameterGaps2(
21731
+ reached.parameterCallsByKey,
21732
+ summariesBySeed,
21733
+ reached.passedPositions
21734
+ );
21735
+ }
21736
+ summaries.push(...reached.summaries);
21737
+ const resolvedImports = resolvedImportsOf(db, displayPathOf);
21738
+ stampModuleImports(summaries, (file2) => resolvedImports.get(file2) ?? []);
21739
+ const idRoot = options.projectRoot ?? options.workspaceRoot;
21740
+ for (const summary of summaries) {
21741
+ const absoluteFile = options.workspaceRoot === void 0 ? summary.location.file : path3.resolve(options.workspaceRoot, summary.location.file);
21742
+ summary.identity.id = summaryIdFromParts({
21743
+ workspace: void 0,
21744
+ file: idRoot === void 0 ? absoluteFile : path3.relative(idRoot, absoluteFile),
21745
+ name: summary.identity.name,
21746
+ exportPath: summary.identity.exportPath
21747
+ });
20397
21748
  }
20398
- stampModuleImports(summaries, db, displayPathOf);
21749
+ disambiguateSummaryIds(summaries);
21750
+ linkCallsToSummaries(summaries);
21751
+ await timer.timeAsync("cache.write", async () => {
21752
+ if (cacheDir === null || summaries.length === 0) {
21753
+ return;
21754
+ }
21755
+ try {
21756
+ await cache.write(cacheInput, summaries);
21757
+ } catch {
21758
+ }
21759
+ });
21760
+ options.onExtractionReport?.(
21761
+ buildPythonExtractionReport({
21762
+ packs: options.packs,
21763
+ tallies,
21764
+ filesWalked: options.files.length,
21765
+ summaries
21766
+ })
21767
+ );
21768
+ options.onTiming?.(timer.report());
20399
21769
  return { summaries, facts: db };
20400
21770
  }
20401
- function stampModuleImports(summaries, db, displayPathOf) {
21771
+ function resolvedImportsOf(db, displayPathOf) {
20402
21772
  const importsByFile = /* @__PURE__ */ new Map();
20403
21773
  for (const [from, , to] of db.facts("pyImportResolved")) {
20404
21774
  if (typeof from !== "string" || typeof to !== "string") {
20405
21775
  continue;
20406
21776
  }
20407
21777
  const key = displayPathOf(from);
20408
- const seen = importsByFile.get(key) ?? /* @__PURE__ */ new Set();
20409
- seen.add(displayPathOf(to));
21778
+ const seen = importsByFile.get(key) ?? [];
21779
+ seen.push(displayPathOf(to));
20410
21780
  importsByFile.set(key, seen);
20411
21781
  }
20412
- for (const summary of summaries) {
20413
- const imports = importsByFile.get(summary.location.file);
20414
- if (imports === void 0 || imports.size === 0) {
20415
- continue;
20416
- }
20417
- summary.metadata = {
20418
- ...summary.metadata ?? {},
20419
- moduleImports: [...imports].sort()
20420
- };
20421
- }
21782
+ return importsByFile;
20422
21783
  }
20423
21784
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
20424
21785
  "__pycache__",
@@ -20473,54 +21834,271 @@ function methodsDeclaredNear(db, patterns, definitions) {
20473
21834
  }
20474
21835
  return found;
20475
21836
  }
20476
- function calledName(call) {
20477
- const callee = field(call, "function");
20478
- if (callee === null) {
20479
- return "";
21837
+
21838
+ // src/facts/why.ts
21839
+ var FUNCTION_TYPES2 = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
21840
+ var IMPORT_TYPES = /* @__PURE__ */ new Set(["import_statement", "import_from_statement"]);
21841
+ function namedChildrenOf(node) {
21842
+ return node.namedChildren.filter((child) => child !== null);
21843
+ }
21844
+ function indexImportNames(file2, stmt, locations) {
21845
+ for (const nameNode of fields(stmt, "name")) {
21846
+ if (nameNode.type === "aliased_import") {
21847
+ const alias = field(nameNode, "alias");
21848
+ if (alias !== null) {
21849
+ locations.set(`${file2}#${alias.text}`, { file: file2, node: alias });
21850
+ }
21851
+ continue;
21852
+ }
21853
+ const local = nameNode.type === "dotted_name" ? nameNode.namedChild(0) : nameNode;
21854
+ if (local !== null) {
21855
+ locations.set(`${file2}#${local.text}`, { file: file2, node: local });
21856
+ }
20480
21857
  }
20481
- return callee.type === "attribute" ? field(callee, "attribute")?.text ?? "" : callee.text;
20482
21858
  }
20483
- function callsUnder(node, found = []) {
20484
- for (const child of node.namedChildren) {
20485
- if (child === null) {
21859
+ function enclosingFunction(node) {
21860
+ let current = node.parent;
21861
+ while (current !== null) {
21862
+ if (FUNCTION_TYPES2.has(current.type)) {
21863
+ return current;
21864
+ }
21865
+ current = current.parent;
21866
+ }
21867
+ return null;
21868
+ }
21869
+ function indexFile(file2, root, locations) {
21870
+ const walk = (node) => {
21871
+ locations.set(nodeId(file2, node), { file: file2, node });
21872
+ if (IMPORT_TYPES.has(node.type)) {
21873
+ indexImportNames(file2, node, locations);
21874
+ }
21875
+ if (FUNCTION_TYPES2.has(node.type) || node.type === "class_definition") {
21876
+ const name = field(node, "name");
21877
+ if (name !== null) {
21878
+ locations.set(`${file2}#${name.text}`, { file: file2, node: name });
21879
+ }
21880
+ }
21881
+ if (node.type === "assignment") {
21882
+ const left = field(node, "left");
21883
+ if (left !== null && left.type === "identifier") {
21884
+ locations.set(`${file2}#${left.text}`, { file: file2, node: left });
21885
+ }
21886
+ }
21887
+ for (const child of namedChildrenOf(node)) {
21888
+ walk(child);
21889
+ }
21890
+ };
21891
+ walk(root);
21892
+ }
21893
+ function displayNameOf(node) {
21894
+ if (node.type === "identifier") {
21895
+ return node.text;
21896
+ }
21897
+ if (node.type === "call") {
21898
+ const callee = field(node, "function");
21899
+ return `${callee?.text ?? "call"}(...)`;
21900
+ }
21901
+ if (node.type === "function_definition" || node.type === "class_definition") {
21902
+ return field(node, "name")?.text ?? node.text;
21903
+ }
21904
+ const text = node.text.split("\n")[0].trim();
21905
+ return text.length > 40 ? `${text.slice(0, 40)}...` : text;
21906
+ }
21907
+ function width(node) {
21908
+ return node.endIndex - node.startIndex;
21909
+ }
21910
+ var PythonWhySession = class {
21911
+ root;
21912
+ db = new Database4();
21913
+ locations = /* @__PURE__ */ new Map();
21914
+ trees = /* @__PURE__ */ new Map();
21915
+ constructor(options) {
21916
+ this.root = path4.resolve(options.dir);
21917
+ const roots = [this.root];
21918
+ for (const file2 of findPythonFiles(this.root)) {
21919
+ const source = fs4.readFileSync(file2, "utf8");
21920
+ const root = parsePythonSync(source).rootNode;
21921
+ this.trees.set(file2, root);
21922
+ emitModuleImportFacts(this.db, file2, bindModule(root), { roots });
21923
+ emitValueFacts(this.db, file2, root);
21924
+ indexFile(file2, root, this.locations);
21925
+ }
21926
+ }
21927
+ /**
21928
+ * The smallest expression on `line` of `file` whose text is exactly
21929
+ * `text`, or null.
21930
+ */
21931
+ findExpression(file2, line, text) {
21932
+ const root = this.rootOf(file2);
21933
+ if (root === null) {
21934
+ return null;
21935
+ }
21936
+ let found = null;
21937
+ const visit = (node) => {
21938
+ if (node.startPosition.row + 1 === line && node.text === text) {
21939
+ if (found === null || width(node) <= width(found)) {
21940
+ found = node;
21941
+ }
21942
+ }
21943
+ for (const child of namedChildrenOf(node)) {
21944
+ visit(child);
21945
+ }
21946
+ };
21947
+ visit(root);
21948
+ return found === null ? null : { file: this.pathOf(file2), node: found };
21949
+ }
21950
+ /**
21951
+ * The callee of the call written as `calleeText` between `startLine`
21952
+ * and `endLine` of `file`, or null.
21953
+ */
21954
+ findCallee(file2, startLine, endLine, calleeText2) {
21955
+ const root = this.rootOf(file2);
21956
+ if (root === null) {
21957
+ return null;
21958
+ }
21959
+ let found = null;
21960
+ const visit = (node) => {
21961
+ if (node.type === "call") {
21962
+ const line = node.startPosition.row + 1;
21963
+ const callee = field(node, "function");
21964
+ if (line >= startLine && line <= endLine && callee !== null && callee.text === calleeText2) {
21965
+ found = callee;
21966
+ }
21967
+ }
21968
+ for (const child of namedChildrenOf(node)) {
21969
+ visit(child);
21970
+ }
21971
+ };
21972
+ visit(root);
21973
+ return found === null ? null : { file: this.pathOf(file2), node: found };
21974
+ }
21975
+ /**
21976
+ * Why `value` resolves to what it does: the witness proof, flattened
21977
+ * to the chain and rendered. Null when the value does not resolve,
21978
+ * which the caller says in its own words.
21979
+ */
21980
+ explain(value, options = {}) {
21981
+ return explainResolvedKey({
21982
+ db: this.db,
21983
+ rules: RESOLUTION_RULES,
21984
+ key: readKey(value.file, value.node, enclosingFunction(value.node)),
21985
+ locate: (key) => this.locate(key),
21986
+ displayPath: (key) => this.displayPath(key),
21987
+ ...options
21988
+ });
21989
+ }
21990
+ locate(key) {
21991
+ const located = this.locations.get(key);
21992
+ if (located === void 0) {
21993
+ return null;
21994
+ }
21995
+ return {
21996
+ name: displayNameOf(located.node),
21997
+ file: this.displayPath(located.file),
21998
+ line: located.node.startPosition.row + 1
21999
+ };
22000
+ }
22001
+ rootOf(file2) {
22002
+ return this.trees.get(this.pathOf(file2)) ?? null;
22003
+ }
22004
+ pathOf(file2) {
22005
+ return path4.isAbsolute(file2) ? file2 : path4.resolve(this.root, file2);
22006
+ }
22007
+ /** A file path said relative to the root, or as given when it is outside the root. */
22008
+ displayPath(key) {
22009
+ if (key.startsWith(this.root)) {
22010
+ return path4.relative(this.root, key);
22011
+ }
22012
+ return key;
22013
+ }
22014
+ };
22015
+
22016
+ // src/stubEvidence.ts
22017
+ import fs5 from "fs";
22018
+ import path5 from "path";
22019
+ function importsSubmoduleOf(module, packageName) {
22020
+ return module === packageName || module.startsWith(`${packageName}.`);
22021
+ }
22022
+ function importStatementModules(stmt) {
22023
+ const modules = [];
22024
+ for (const nameNode of fields(stmt, "name")) {
22025
+ if (nameNode.type === "aliased_import") {
22026
+ const dotted = field(nameNode, "name");
22027
+ if (dotted !== null) {
22028
+ modules.push(dotted.text);
22029
+ }
20486
22030
  continue;
20487
22031
  }
20488
- if (child.type === "call") {
20489
- found.push(child);
22032
+ if (nameNode.type === "dotted_name") {
22033
+ modules.push(nameNode.text);
20490
22034
  }
20491
- callsUnder(child, found);
20492
22035
  }
20493
- return found;
22036
+ return modules;
20494
22037
  }
20495
- var REACHES_STORAGE_RULES = [
20496
- rule2(
20497
- "reachesStorage",
20498
- [v2("f")],
20499
- [lit2("defCallsName", v2("f"), v2("m")), lit2("queryStart", v2("m"))]
20500
- ),
20501
- rule2(
20502
- "reachesStorage",
20503
- [v2("f")],
20504
- [lit2("defCallsName", v2("f"), v2("m")), lit2("reachesStorage", v2("m"))]
20505
- )
20506
- ];
20507
- function functionsReachingStorage(db, definitions, couldMatch) {
20508
- for (const node of definitions.values()) {
20509
- const name = field(node, "name");
20510
- if (name === null) {
22038
+ function importFromStatementEntries(stmt) {
22039
+ const moduleNode = field(stmt, "module_name");
22040
+ if (moduleNode === null || moduleNode.type === "relative_import") {
22041
+ return [];
22042
+ }
22043
+ const module = moduleNode.text;
22044
+ const entries = [];
22045
+ for (const nameNode of fields(stmt, "name")) {
22046
+ if (nameNode.type === "aliased_import") {
22047
+ const original = field(nameNode, "name");
22048
+ if (original !== null) {
22049
+ entries.push({ module, name: original.text });
22050
+ }
20511
22051
  continue;
20512
22052
  }
20513
- for (const called of callsUnder(node).map(calledName)) {
20514
- db.add("defCallsName", [name.text, called]);
22053
+ if (nameNode.type === "dotted_name") {
22054
+ entries.push({ module, name: nameNode.text });
20515
22055
  }
20516
22056
  }
20517
- for (const name of couldMatch) {
20518
- db.add("queryStart", [name]);
22057
+ return entries;
22058
+ }
22059
+ function importSitesIn(root) {
22060
+ const found = [];
22061
+ const visit = (node) => {
22062
+ if (node.type === "import_statement") {
22063
+ const line = rangeOf(node).start;
22064
+ for (const module of importStatementModules(node)) {
22065
+ found.push({ module, name: null, line });
22066
+ }
22067
+ } else if (node.type === "import_from_statement") {
22068
+ const line = rangeOf(node).start;
22069
+ for (const entry of importFromStatementEntries(node)) {
22070
+ found.push({ module: entry.module, name: entry.name, line });
22071
+ }
22072
+ }
22073
+ for (const child of node.namedChildren) {
22074
+ if (child !== null) {
22075
+ visit(child);
22076
+ }
22077
+ }
22078
+ };
22079
+ visit(root);
22080
+ return found;
22081
+ }
22082
+ async function pythonImportEvidence(options) {
22083
+ const byModule = /* @__PURE__ */ new Map();
22084
+ for (const file2 of findPythonFiles(options.directory)) {
22085
+ const source = fs5.readFileSync(file2, "utf8");
22086
+ const tree = await parsePython(source);
22087
+ const displayPath = path5.relative(options.directory, file2);
22088
+ for (const site of importSitesIn(tree.rootNode)) {
22089
+ if (!importsSubmoduleOf(site.module, options.packageName)) {
22090
+ continue;
22091
+ }
22092
+ const sites = byModule.get(site.module) ?? [];
22093
+ sites.push({ name: site.name, file: displayPath, line: site.line });
22094
+ byModule.set(site.module, sites);
22095
+ }
20519
22096
  }
20520
- evaluate2(db, REACHES_STORAGE_RULES);
20521
- return new Set(db.facts("reachesStorage").map((row) => String(row[0])));
22097
+ return [...byModule.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([module, sites]) => ({ module, sites }));
20522
22098
  }
20523
22099
  export {
22100
+ ADAPTER_VERSION,
22101
+ PythonWhySession,
20524
22102
  annotationToShape,
20525
22103
  bindModule,
20526
22104
  buildRouterIndex,
@@ -20535,6 +22113,10 @@ export {
20535
22113
  nodeId,
20536
22114
  objectReturnedBy,
20537
22115
  parsePython,
22116
+ parsePythonSync,
22117
+ preloadPythonGrammar,
22118
+ pythonImportEvidence,
22119
+ readKey,
20538
22120
  resolveAbsoluteModule,
20539
22121
  resolveCalls,
20540
22122
  resolveModule,