@suss/adapter-python 0.21.1 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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);
@@ -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(
@@ -1552,23 +2026,33 @@ function resolveCalls(db, callKeys) {
1552
2026
  for (const callKey of callKeys) {
1553
2027
  db.add("wanted", [callKey]);
1554
2028
  }
1555
- evaluate(db, RESOLUTION_PROGRAM.rules);
2029
+ evaluate2(db, RESOLUTION_PROGRAM.rules);
1556
2030
  }
1557
2031
  function subjectConstructions(db, valueKeys) {
1558
2032
  const found = /* @__PURE__ */ new Map();
1559
2033
  if (valueKeys.length === 0) {
1560
2034
  return found;
1561
2035
  }
1562
- for (const valueKey2 of valueKeys) {
1563
- db.add("wantedSubject", [valueKey2]);
1564
- }
1565
- evaluate(db, RESOLUTION_PROGRAM.rules);
1566
- const writtenAs = /* @__PURE__ */ new Map();
1567
- for (const row of db.facts("wantedSubjectWritten")) {
1568
- const written = writtenAs.get(String(row[0])) ?? /* @__PURE__ */ new Set();
1569
- written.add(String(row[1]));
1570
- 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);
1571
2051
  }
2052
+ const writtenThroughCall = singleAnswers(
2053
+ db.facts("wantedSubjectWritten"),
2054
+ placeholders
2055
+ );
1572
2056
  const origins = /* @__PURE__ */ new Map();
1573
2057
  for (const row of db.facts("wantedSubjectConstruction")) {
1574
2058
  const key = `${String(row[0])}|${String(row[1])}`;
@@ -1577,12 +2061,14 @@ function subjectConstructions(db, valueKeys) {
1577
2061
  origins.set(key, listed);
1578
2062
  }
1579
2063
  for (const valueKey2 of valueKeys) {
1580
- const written = writtenAs.get(valueKey2);
1581
- if (written === void 0 || written.size !== 1) {
2064
+ const direct = written.get(valueKey2);
2065
+ if (direct === void 0) {
1582
2066
  continue;
1583
2067
  }
1584
- const constructionKey = [...written][0];
1585
- 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}`);
1586
2072
  if (from !== void 0) {
1587
2073
  found.set(valueKey2, { constructionKey, origins: from });
1588
2074
  }
@@ -1778,6 +2264,9 @@ function emitExpressionFact(emitter, child) {
1778
2264
  if (WRITTEN_VALUE_TYPES.has(child.type)) {
1779
2265
  add(emitter, "writtenValue", nodeId(emitter.filePath, child));
1780
2266
  }
2267
+ if (child.type === "none") {
2268
+ add(emitter, "placeholderValue", nodeId(emitter.filePath, child));
2269
+ }
1781
2270
  }
1782
2271
  function emitExpressionFacts(emitter, node) {
1783
2272
  walkExpressions(emitter, node, (child) => {
@@ -2236,7 +2725,7 @@ function predicateOf(node) {
2236
2725
  }
2237
2726
 
2238
2727
  // src/paths/effects.ts
2239
- var NESTED_FUNCTION_TYPES2 = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
2728
+ var NESTED_DEFINITION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
2240
2729
  var LITERAL_ARGS = {
2241
2730
  string: (node) => ({ kind: "string", value: node.text.slice(1, -1) }),
2242
2731
  integer: (node) => {
@@ -2252,7 +2741,7 @@ var LITERAL_ARGS = {
2252
2741
  };
2253
2742
  function bodyCalls(node, found = []) {
2254
2743
  for (const child of node.namedChildren) {
2255
- if (child === null || NESTED_FUNCTION_TYPES2.has(child.type)) {
2744
+ if (child === null || NESTED_DEFINITION_TYPES.has(child.type)) {
2256
2745
  continue;
2257
2746
  }
2258
2747
  if (child.type === "call") {
@@ -2294,26 +2783,12 @@ function argsOf(call) {
2294
2783
  function calleeText(call) {
2295
2784
  return field(call, "function")?.text ?? call.text;
2296
2785
  }
2297
- function withoutChainLinks(calls) {
2298
- const isLink = /* @__PURE__ */ new Set();
2299
- for (const call of calls) {
2300
- const callee = field(call, "function");
2301
- if (callee === null || callee.type !== "attribute") {
2302
- continue;
2303
- }
2304
- const object2 = field(callee, "object");
2305
- if (object2 !== null && object2.type === "call") {
2306
- isLink.add(object2.id);
2307
- }
2308
- }
2309
- return calls.filter((call) => !isLink.has(call.id));
2310
- }
2311
2786
  function invocationEffects(definitionNode) {
2312
2787
  const body = field(definitionNode, "body");
2313
2788
  if (body === null) {
2314
2789
  return [];
2315
2790
  }
2316
- const calls = withoutChainLinks(bodyCalls(body));
2791
+ const calls = bodyCalls(body).sort((a, b) => a.endIndex - b.endIndex);
2317
2792
  if (calls.length === 0) {
2318
2793
  return [];
2319
2794
  }
@@ -3191,10 +3666,10 @@ function mergeDefs(...defs) {
3191
3666
  function cloneDef(schema) {
3192
3667
  return mergeDefs(schema._zod.def);
3193
3668
  }
3194
- function getElementAtPath(obj, path4) {
3195
- if (!path4)
3669
+ function getElementAtPath(obj, path6) {
3670
+ if (!path6)
3196
3671
  return obj;
3197
- return path4.reduce((acc, key) => acc?.[key], obj);
3672
+ return path6.reduce((acc, key) => acc?.[key], obj);
3198
3673
  }
3199
3674
  function promiseAllObject(promisesObj) {
3200
3675
  const keys = Object.keys(promisesObj);
@@ -3603,11 +4078,11 @@ function explicitlyAborted(x, startIndex = 0) {
3603
4078
  }
3604
4079
  return false;
3605
4080
  }
3606
- function prefixIssues(path4, issues) {
4081
+ function prefixIssues(path6, issues) {
3607
4082
  return issues.map((iss) => {
3608
4083
  var _a3;
3609
4084
  (_a3 = iss).path ?? (_a3.path = []);
3610
- iss.path.unshift(path4);
4085
+ iss.path.unshift(path6);
3611
4086
  return iss;
3612
4087
  });
3613
4088
  }
@@ -3754,16 +4229,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
3754
4229
  }
3755
4230
  function formatError(error51, mapper = (issue2) => issue2.message) {
3756
4231
  const fieldErrors = { _errors: [] };
3757
- const processError = (error52, path4 = []) => {
4232
+ const processError = (error52, path6 = []) => {
3758
4233
  for (const issue2 of error52.issues) {
3759
4234
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3760
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
4235
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
3761
4236
  } else if (issue2.code === "invalid_key") {
3762
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4237
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3763
4238
  } else if (issue2.code === "invalid_element") {
3764
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4239
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3765
4240
  } else {
3766
- const fullpath = [...path4, ...issue2.path];
4241
+ const fullpath = [...path6, ...issue2.path];
3767
4242
  if (fullpath.length === 0) {
3768
4243
  fieldErrors._errors.push(mapper(issue2));
3769
4244
  } else {
@@ -3790,17 +4265,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
3790
4265
  }
3791
4266
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
3792
4267
  const result = { errors: [] };
3793
- const processError = (error52, path4 = []) => {
4268
+ const processError = (error52, path6 = []) => {
3794
4269
  var _a3, _b;
3795
4270
  for (const issue2 of error52.issues) {
3796
4271
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3797
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
4272
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
3798
4273
  } else if (issue2.code === "invalid_key") {
3799
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4274
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3800
4275
  } else if (issue2.code === "invalid_element") {
3801
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
4276
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
3802
4277
  } else {
3803
- const fullpath = [...path4, ...issue2.path];
4278
+ const fullpath = [...path6, ...issue2.path];
3804
4279
  if (fullpath.length === 0) {
3805
4280
  result.errors.push(mapper(issue2));
3806
4281
  continue;
@@ -3832,8 +4307,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
3832
4307
  }
3833
4308
  function toDotPath(_path) {
3834
4309
  const segs = [];
3835
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3836
- for (const seg of path4) {
4310
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4311
+ for (const seg of path6) {
3837
4312
  if (typeof seg === "number")
3838
4313
  segs.push(`[${seg}]`);
3839
4314
  else if (typeof seg === "symbol")
@@ -16525,13 +17000,13 @@ function resolveRef(ref, ctx) {
16525
17000
  if (!ref.startsWith("#")) {
16526
17001
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
16527
17002
  }
16528
- const path4 = ref.slice(1).split("/").filter(Boolean);
16529
- if (path4.length === 0) {
17003
+ const path6 = ref.slice(1).split("/").filter(Boolean);
17004
+ if (path6.length === 0) {
16530
17005
  return ctx.rootSchema;
16531
17006
  }
16532
17007
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
16533
- if (path4[0] === defsKey) {
16534
- const key = path4[1];
17008
+ if (path6[0] === defsKey) {
17009
+ const key = path6[1];
16535
17010
  if (!key || !ctx.defs[key]) {
16536
17011
  throw new Error(`Reference not found: ${ref}`);
16537
17012
  }
@@ -17291,8 +17766,8 @@ function baseUrlReference(semantics) {
17291
17766
  const label = OPENING_HOLE.exec(semantics.path ?? "")?.[1];
17292
17767
  return label === void 0 ? null : referenceFromName(patternHole(label));
17293
17768
  }
17294
- function normalizePath(path4) {
17295
- 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}");
17296
17771
  if (normalized.length > 1 && normalized.endsWith("/")) {
17297
17772
  normalized = normalized.slice(0, -1);
17298
17773
  }
@@ -17302,8 +17777,8 @@ function normalizePath(path4) {
17302
17777
  );
17303
17778
  return normalized;
17304
17779
  }
17305
- function pathShape(path4) {
17306
- return normalizePath(path4).replace(/\{[^}]*\}/g, "{}");
17780
+ function pathShape(path6) {
17781
+ return normalizePath(path6).replace(/\{[^}]*\}/g, "{}");
17307
17782
  }
17308
17783
  function methodsAgree(a, b) {
17309
17784
  if (a === null || b === null) {
@@ -17381,8 +17856,8 @@ var restSemantics = defineBoundarySemantics({
17381
17856
  return null;
17382
17857
  }
17383
17858
  const method = semantics.method === null ? "ANY" : semantics.method.toUpperCase();
17384
- const path4 = semantics.path === null ? "?" : normalizePath(semantics.path);
17385
- return `${method} ${path4}`;
17859
+ const path6 = semantics.path === null ? "?" : normalizePath(semantics.path);
17860
+ return `${method} ${path6}`;
17386
17861
  },
17387
17862
  /**
17388
17863
  * A route with both halves stated decides for itself: the method
@@ -17391,14 +17866,14 @@ var restSemantics = defineBoundarySemantics({
17391
17866
  * half might still be the one that handles the request, and nothing
17392
17867
  * here can settle that, so it abstains instead of refusing.
17393
17868
  */
17394
- servesRequest(semantics, method, path4) {
17869
+ servesRequest(semantics, method, path6) {
17395
17870
  if (semantics.method === null || semantics.path === null) {
17396
17871
  return "unknown";
17397
17872
  }
17398
17873
  if (!methodsAgree(semantics.method, method.toUpperCase())) {
17399
17874
  return "nomatch";
17400
17875
  }
17401
- return routePathAdmits(semantics.path, path4) ? "match" : "nomatch";
17876
+ return routePathAdmits(semantics.path, path6) ? "match" : "nomatch";
17402
17877
  },
17403
17878
  /**
17404
17879
  * A route the registration pattern admits is inside it. A route
@@ -18035,71 +18510,10 @@ function returnTypeName(node) {
18035
18510
  }
18036
18511
  return annotation.type === "subscript" ? field(annotation, "value")?.text ?? null : annotation.text;
18037
18512
  }
18038
- function callsIn(node, found = []) {
18039
- for (const child of node.namedChildren) {
18040
- if (child === null) {
18041
- continue;
18042
- }
18043
- if (child.type === "call") {
18044
- found.push(child);
18045
- }
18046
- callsIn(child, found);
18047
- }
18048
- return found;
18049
- }
18050
- function storageEffects(calls, options) {
18051
- const seen = /* @__PURE__ */ new Set();
18052
- const asked = new Set(
18053
- options.facts.facts("wanted").map((row) => String(row[0]))
18054
- );
18055
- const gather = (inCalls, filePath) => {
18056
- const found = [...directStorageEffects(inCalls, { ...options, filePath })];
18057
- resolveCalls(
18058
- options.facts,
18059
- inCalls.filter((call) => options.leadsToStorage.has(methodName(call))).map((call) => field(call, "function")).filter((callee) => callee !== null).map((callee) => calleeKeyOf(filePath, callee)).filter((key) => {
18060
- const fresh = !asked.has(key);
18061
- asked.add(key);
18062
- return fresh;
18063
- })
18064
- );
18065
- for (const call of inCalls) {
18066
- const callee = field(call, "function");
18067
- if (callee === null || !options.leadsToStorage.has(methodName(call))) {
18068
- continue;
18069
- }
18070
- const key = resolvedOnce(options, calleeKeyOf(filePath, callee));
18071
- const body = key === void 0 ? void 0 : options.definitionAt(key);
18072
- if (key === void 0 || body === void 0 || seen.has(key)) {
18073
- continue;
18074
- }
18075
- seen.add(key);
18076
- const name = field(body, "name")?.text;
18077
- const origin = {
18078
- file: fileOf(key),
18079
- line: body.startPosition.row + 1,
18080
- ...name === void 0 ? {} : { function: name }
18081
- };
18082
- found.push(
18083
- ...gather(callsIn(body), fileOf(key)).map(
18084
- (effect) => effect.type === "interaction" && effect.origin === void 0 ? { ...effect, origin } : effect
18085
- )
18086
- );
18087
- }
18088
- return found;
18089
- };
18090
- return gather(calls, options.filePath);
18091
- }
18092
- function calleeKeyOf(filePath, callee) {
18093
- return callee.type === "identifier" ? `${filePath}#${callee.text}` : nodeId(filePath, callee);
18094
- }
18095
- function resolvedOnce(options, calleeKey) {
18096
- const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === calleeKey).map((row) => String(row[1]));
18097
- return resolved.length === 1 ? resolved[0] : void 0;
18098
- }
18099
18513
  function storageCallees(calls, filePath, couldMatch) {
18100
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));
18101
18515
  }
18102
- function directStorageEffects(calls, options) {
18516
+ function storageEffects(calls, options) {
18103
18517
  if (options.patterns.length === 0) {
18104
18518
  return [];
18105
18519
  }
@@ -18157,7 +18571,7 @@ function directStorageEffects(calls, options) {
18157
18571
  // src/discovery.ts
18158
18572
  function decoratedStatements(root, module) {
18159
18573
  const found = [];
18160
- walkDescendants(
18574
+ walkDescendants2(
18161
18575
  root,
18162
18576
  { scope: module.moduleScope, enclosingFunction: null },
18163
18577
  {
@@ -18196,21 +18610,21 @@ function builtSubjects(decorated, options) {
18196
18610
  }
18197
18611
  const factsPath = options.absoluteFile ?? options.filePath;
18198
18612
  const asked = /* @__PURE__ */ new Set();
18199
- for (const { enclosingFunction, decorators } of decorated) {
18613
+ for (const { enclosingFunction: enclosingFunction2, decorators } of decorated) {
18200
18614
  for (const { node, direct } of decorators) {
18201
18615
  if (direct.module !== null && acceptedByAnyPattern(direct.module, options)) {
18202
18616
  continue;
18203
18617
  }
18204
18618
  const written = decoratorReceiver(node);
18205
18619
  if (written !== null) {
18206
- asked.add(readKey(factsPath, written.object, enclosingFunction));
18620
+ asked.add(readKey(factsPath, written.object, enclosingFunction2));
18207
18621
  }
18208
18622
  }
18209
18623
  }
18210
18624
  return subjectConstructions(facts, [...asked]);
18211
18625
  }
18212
18626
  function decoratedUnits(decoratedStatement, module, options, subjects) {
18213
- const { stmt, scope, enclosingFunction, decorators } = decoratedStatement;
18627
+ const { stmt, scope, enclosingFunction: enclosingFunction2, decorators } = decoratedStatement;
18214
18628
  const units = [];
18215
18629
  const { definition } = stripDecorators(stmt);
18216
18630
  for (const { node: decoratorNode, direct } of decorators) {
@@ -18226,7 +18640,7 @@ function decoratedUnits(decoratedStatement, module, options, subjects) {
18226
18640
  ) ?? null
18227
18641
  ) ?? builtSubjectClassification(
18228
18642
  decoratorNode,
18229
- enclosingFunction,
18643
+ enclosingFunction2,
18230
18644
  subjects,
18231
18645
  options
18232
18646
  )
@@ -18262,14 +18676,14 @@ function acceptedByAnyPattern(module, options) {
18262
18676
  (pack) => pack.discovery.some((pattern) => pattern.importModule.includes(module))
18263
18677
  );
18264
18678
  }
18265
- function builtSubjectClassification(decoratorNode, enclosingFunction, subjects, options) {
18679
+ function builtSubjectClassification(decoratorNode, enclosingFunction2, subjects, options) {
18266
18680
  const written = decoratorReceiver(decoratorNode);
18267
18681
  if (written === null) {
18268
18682
  return null;
18269
18683
  }
18270
18684
  const factsPath = options.absoluteFile ?? options.filePath;
18271
18685
  const built = subjects.get(
18272
- readKey(factsPath, written.object, enclosingFunction)
18686
+ readKey(factsPath, written.object, enclosingFunction2)
18273
18687
  );
18274
18688
  const origin = built?.origins.find(
18275
18689
  (candidate) => acceptedByAnyPattern(candidate.module, options)
@@ -18342,9 +18756,9 @@ function readPathArgument(classification) {
18342
18756
  }
18343
18757
  var NO_PATH_PARAMS = /* @__PURE__ */ new Set();
18344
18758
  function templateReader(template) {
18345
- return (path4) => {
18759
+ return (path6) => {
18346
18760
  const names = [];
18347
- const canonical = path4.replace(template, (_match, name) => {
18761
+ const canonical = path6.replace(template, (_match, name) => {
18348
18762
  names.push(name);
18349
18763
  return `{${name}}`;
18350
18764
  });
@@ -18357,10 +18771,10 @@ var PATH_TEMPLATE_READERS = {
18357
18771
  /<(?:[A-Za-z_]\w*(?:\(.*?\))?:)?([A-Za-z_]\w*)>/g
18358
18772
  )
18359
18773
  };
18360
- function readPathTemplate(pattern, path4, range) {
18774
+ function readPathTemplate(pattern, path6, range) {
18361
18775
  const syntax = pattern.pathParamSyntax;
18362
18776
  if (syntax === void 0) {
18363
- return writtenReading({ path: path4, paramNames: NO_PATH_PARAMS }, range);
18777
+ return writtenReading({ path: path6, paramNames: NO_PATH_PARAMS }, range);
18364
18778
  }
18365
18779
  const reader = PATH_TEMPLATE_READERS[syntax];
18366
18780
  if (reader === void 0) {
@@ -18369,7 +18783,7 @@ function readPathTemplate(pattern, path4, range) {
18369
18783
  range
18370
18784
  );
18371
18785
  }
18372
- return writtenReading(reader(path4), range);
18786
+ return writtenReading(reader(path6), range);
18373
18787
  }
18374
18788
  function classRouteUnits(pattern, pack, classification, classNode, module, options) {
18375
18789
  const pathArgument = readPathArgument(classification);
@@ -18439,7 +18853,7 @@ function readRoutePath(pattern, pathArgument, classification, module, options) {
18439
18853
  );
18440
18854
  return andThenReading(
18441
18855
  composed,
18442
- (path4, range) => readPathTemplate(pattern, path4, range)
18856
+ (path6, range) => readPathTemplate(pattern, path6, range)
18443
18857
  );
18444
18858
  }
18445
18859
  function readRoutePaths(pattern, pathArgument, classification, module, options) {
@@ -18468,7 +18882,7 @@ function readRoutePaths(pattern, pathArgument, classification, module, options)
18468
18882
  pathArgument,
18469
18883
  (literal2, range) => writtenReading(servedSpelling(pattern, prefix + literal2), range)
18470
18884
  ),
18471
- (path4, range) => readPathTemplate(pattern, path4, range)
18885
+ (path6, range) => readPathTemplate(pattern, path6, range)
18472
18886
  ),
18473
18887
  mount: { siblings: resolution.values.length, prefix }
18474
18888
  }));
@@ -18510,11 +18924,11 @@ function composeRoutePath(pattern, classification, module, options, literal2, ra
18510
18924
  );
18511
18925
  }
18512
18926
  var REPEATED_SLASH_READERS = {
18513
- kept: (path4) => path4,
18514
- merged: (path4) => path4.replace(/\/{2,}/g, "/")
18927
+ kept: (path6) => path6,
18928
+ merged: (path6) => path6.replace(/\/{2,}/g, "/")
18515
18929
  };
18516
- function servedSpelling(pattern, path4) {
18517
- return REPEATED_SLASH_READERS[pattern.pathRepeatedSlashes ?? "kept"](path4);
18930
+ function servedSpelling(pattern, path6) {
18931
+ return REPEATED_SLASH_READERS[pattern.pathRepeatedSlashes ?? "kept"](path6);
18518
18932
  }
18519
18933
  function readRouterPrefix(pattern, classification, module, options, range) {
18520
18934
  const resolution = routerResolutionOf(
@@ -18626,8 +19040,8 @@ function branchesFromReturns(readsReturnedStatus, definitionNode, effects, respo
18626
19040
  "This return gives a status this reading cannot resolve to a number, so this outcome claims none",
18627
19041
  range
18628
19042
  );
18629
- for (const path4 of paths) {
18630
- const conditions = path4.map((condition) => ({
19043
+ for (const path6 of paths) {
19044
+ const conditions = path6.map((condition) => ({
18631
19045
  sourceText: condition.sourceText,
18632
19046
  structured: condition.expression === null ? null : predicateOf(condition.expression),
18633
19047
  polarity: condition.polarity,
@@ -18656,7 +19070,7 @@ function branchesFromReturns(readsReturnedStatus, definitionNode, effects, respo
18656
19070
  },
18657
19071
  effects: effectsReaching(effects, conditions),
18658
19072
  location: range,
18659
- isDefault: path4.length === 0
19073
+ isDefault: path6.length === 0
18660
19074
  });
18661
19075
  }
18662
19076
  }
@@ -18867,17 +19281,7 @@ function buildRouteUnit(options) {
18867
19281
  () => readReturnAnnotation(definitionNode, enclosingScope, ctx)
18868
19282
  );
18869
19283
  const effects = invocationEffects(definitionNode);
18870
- const storage = storageLookup === void 0 ? [] : [
18871
- ...storageEffects(bodyCalls(definitionNode), {
18872
- ...storageLookup,
18873
- filePath: storageLookup.factsPath
18874
- }),
18875
- ...rawSqlEffects(bodyCalls(definitionNode), {
18876
- facts: storageLookup.facts,
18877
- filePath: storageLookup.factsPath,
18878
- patterns: storageLookup.rawSql ?? []
18879
- })
18880
- ];
19284
+ const extra = recognizedBodyEffects(definitionNode, module, storageLookup);
18881
19285
  const perReturn = branchesFromReturns(
18882
19286
  readsReturnedStatus,
18883
19287
  definitionNode,
@@ -18886,9 +19290,9 @@ function buildRouteUnit(options) {
18886
19290
  statusCode
18887
19291
  );
18888
19292
  const branches = (perReturn ?? []).map(
18889
- (branch) => storage.length === 0 ? branch : { ...branch, extraEffects: storage }
19293
+ (branch) => extra.length === 0 ? branch : { ...branch, extraEffects: extra }
18890
19294
  );
18891
- 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)) {
18892
19296
  branches.push({
18893
19297
  conditions: [],
18894
19298
  terminal: {
@@ -18908,7 +19312,7 @@ function buildRouteUnit(options) {
18908
19312
  statusCodeReading: statusCode,
18909
19313
  bodyShapeReading: { reading: responseShape.reading },
18910
19314
  effects,
18911
- ...storage.length === 0 ? {} : { extraEffects: storage },
19315
+ ...extra.length === 0 ? {} : { extraEffects: extra },
18912
19316
  location: rangeOf(definitionNode),
18913
19317
  isDefault: true
18914
19318
  });
@@ -18956,6 +19360,20 @@ function unreadRoleReadings(parameters, range) {
18956
19360
  )
18957
19361
  ];
18958
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
+ }
18959
19377
  function bodyContentOf(bodyNode) {
18960
19378
  for (const stmt of bodyStatements(bodyNode)) {
18961
19379
  if (stmt.type === "pass_statement") {
@@ -19091,6 +19509,11 @@ function parameterNameAndType(param) {
19091
19509
  return null;
19092
19510
  }
19093
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
+
19094
19517
  // src/moduleResolver.ts
19095
19518
  import fs from "fs";
19096
19519
  import path from "path";
@@ -19183,43 +19606,38 @@ function importedModuleText(module, relativeLevel) {
19183
19606
  return relativeLevel > 0 ? `${".".repeat(relativeLevel)}${module}` : module;
19184
19607
  }
19185
19608
  function emitModuleImportFacts(db, filePath, module, resolverOptions) {
19186
- const everyBinding = [...module.scopeFor.values()].flatMap((scope) => [
19187
- ...scope.bindings
19188
- ]);
19189
- for (const [localName, binding] of everyBinding) {
19190
- if (binding.kind !== "import" && binding.kind !== "importFrom") {
19191
- continue;
19192
- }
19193
- const moduleText = importedModuleText(
19194
- binding.module,
19195
- binding.relativeLevel
19196
- );
19197
- const resolution = resolveModule(
19198
- filePath,
19199
- { module: binding.module, relativeLevel: binding.relativeLevel },
19200
- resolverOptions
19201
- );
19202
- db.add("pyImport", [
19203
- filePath,
19204
- moduleText,
19205
- resolution.status === "resolved" ? "resolved" : resolution.reason
19206
- ]);
19207
- const importedName = binding.kind === "import" ? binding.localName : binding.importedName;
19208
- db.add("pyImportedName", [
19209
- `${filePath}#${localName}`,
19210
- moduleText,
19211
- importedName
19212
- ]);
19213
- if (resolution.status === "resolved") {
19214
- db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
19215
- db.add("imports", [
19216
- `${filePath}#${localName}`,
19217
- resolution.file,
19218
- 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
19219
19628
  ]);
19220
- continue;
19221
- }
19222
- db.add("imports", [`${filePath}#${localName}`, moduleText, importedName]);
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]);
19640
+ }
19223
19641
  }
19224
19642
  for (const openModule of module.openImports) {
19225
19643
  db.add("pyOpenImport", [filePath, openModule]);
@@ -19238,6 +19656,7 @@ var GRAMMAR_PATH = path2.join(
19238
19656
  "tree-sitter-python.wasm"
19239
19657
  );
19240
19658
  var languagePromise = null;
19659
+ var loadedLanguage = null;
19241
19660
  function loadLanguage() {
19242
19661
  languagePromise ??= (async () => {
19243
19662
  if (!fs2.existsSync(GRAMMAR_PATH)) {
@@ -19246,12 +19665,28 @@ function loadLanguage() {
19246
19665
  );
19247
19666
  }
19248
19667
  await Parser.init();
19249
- return Language.load(GRAMMAR_PATH);
19668
+ const language = await Language.load(GRAMMAR_PATH);
19669
+ loadedLanguage = language;
19670
+ return language;
19250
19671
  })();
19251
19672
  return languagePromise;
19252
19673
  }
19253
19674
  async function parsePython(source) {
19254
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) {
19255
19690
  const parser = new Parser();
19256
19691
  parser.setLanguage(language);
19257
19692
  try {
@@ -19268,8 +19703,705 @@ async function parsePython(source) {
19268
19703
  // src/project.ts
19269
19704
  import fs3 from "fs";
19270
19705
  import path3 from "path";
19271
- import { Database, evaluate as evaluate2, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
19272
- 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
+ }
19273
20405
 
19274
20406
  // src/routers.ts
19275
20407
  var UNSTATED_PREFIX = { kind: "unstated" };
@@ -19544,6 +20676,9 @@ function buildPatternIndex(files, importModule, composition, resolverOptions) {
19544
20676
  for (const bound of files) {
19545
20677
  collectConstructions(bound, importModule, composition, index);
19546
20678
  }
20679
+ for (const bound of files) {
20680
+ collectReturnedConstructions(bound, importModule, composition, index);
20681
+ }
19547
20682
  if (objectPrefix?.carrier !== void 0) {
19548
20683
  const carrier = objectPrefix.carrier;
19549
20684
  for (const bound of files) {
@@ -19719,6 +20854,37 @@ function collectConstructions(bound, importModule, composition, index) {
19719
20854
  );
19720
20855
  }
19721
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
+ }
19722
20888
  function collectCarrierConstructions(carrier, composition, scan) {
19723
20889
  walkStatements(
19724
20890
  bodyStatements(scan.bound.root),
@@ -19777,6 +20943,21 @@ function constructionNamed(name, scope, scan, constructions = scan.index.constru
19777
20943
  };
19778
20944
  return resolvers[binding.kind]?.() ?? null;
19779
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
+ }
19780
20961
  var MOUNT_STATE_BY_EFFECT = {
19781
20962
  prefixes: (statedPrefix, site) => ({
19782
20963
  kind: "mounted",
@@ -19946,10 +21127,10 @@ function constructionsReturnedBy(target, index) {
19946
21127
  return { kind: "unread" };
19947
21128
  }
19948
21129
  const contained = containedValues(facts, returned);
19949
- resolveCalls(facts, contained);
21130
+ const settled = subjectConstructions(facts, contained);
19950
21131
  const found = [];
19951
21132
  for (const value of contained) {
19952
- const resolved = resolvedValueKey(facts, value);
21133
+ const resolved = settled.get(value)?.constructionKey ?? value;
19953
21134
  const construction = index.byValueKey.get(resolved);
19954
21135
  if (construction === void 0) {
19955
21136
  return {
@@ -19964,10 +21145,6 @@ function constructionsReturnedBy(target, index) {
19964
21145
  }
19965
21146
  return { kind: "settled", found };
19966
21147
  }
19967
- function resolvedValueKey(facts, value) {
19968
- const row = facts.facts("wantedIsWrittenAs").find((entry) => String(entry[0]) === value);
19969
- return row === void 0 ? value : String(row[1]);
19970
- }
19971
21148
  function mountedConstructions(arg, position, scan) {
19972
21149
  if (arg?.kind === "attribute") {
19973
21150
  const construction = constructionThroughModule(arg, position.scope, scan);
@@ -19977,7 +21154,7 @@ function mountedConstructions(arg, position, scan) {
19977
21154
  return [];
19978
21155
  }
19979
21156
  const named = (name) => {
19980
- const construction = constructionNamed(name, position.scope, scan);
21157
+ const construction = constructionNamed(name, position.scope, scan) ?? constructionThroughFacts(name, position.scope, scan);
19981
21158
  return construction === null ? [] : [construction];
19982
21159
  };
19983
21160
  const target = position.loopBindings.get(arg.name);
@@ -20364,36 +21541,69 @@ function carrierPrefix(carrier, constructorArg, objectName, scope, scan) {
20364
21541
  return readings[construction.prefix.kind]();
20365
21542
  }
20366
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
+
20367
21552
  // src/project.ts
20368
- function reportUnresolvedProjectModules(options) {
21553
+ function reportUnresolvedProjectModules(options, db) {
21554
+ const imported = new Set(db.facts("pyImport").map((row) => String(row[1])));
20369
21555
  for (const pack of options.packs) {
20370
21556
  for (const module of pack.projectModules ?? []) {
20371
- const resolved = resolveAbsoluteModule(module, { roots: options.roots });
20372
- if (resolved.status === "resolved") {
21557
+ if (imported.has(module)) {
20373
21558
  continue;
20374
21559
  }
20375
21560
  process.stderr.write(
20376
- `[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.
20377
21562
  `
20378
21563
  );
20379
21564
  }
20380
21565
  }
20381
21566
  }
20382
21567
  async function extractPythonProject(options) {
20383
- 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();
20384
21592
  const summaries = [];
20385
21593
  const gapHandling = options.gapHandling ?? "permissive";
21594
+ const tallies = createPackTallies(options.packs);
20386
21595
  const displayPathOf = (file2) => options.workspaceRoot !== void 0 ? path3.relative(options.workspaceRoot, file2) : file2;
20387
- reportUnresolvedProjectModules(options);
20388
21596
  const bound = [];
20389
21597
  for (const file2 of options.files) {
20390
- const source = fs3.readFileSync(file2, "utf8");
20391
- const tree = await parsePython(source);
20392
- bound.push({
20393
- file: file2,
20394
- displayPath: displayPathOf(file2),
20395
- root: tree.rootNode,
20396
- 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
+ });
20397
21607
  });
20398
21608
  }
20399
21609
  const mountsRouters = options.packs.some(
@@ -20404,81 +21614,172 @@ async function extractPythonProject(options) {
20404
21614
  const discovers = options.packs.some((pack) => pack.discovery.length > 0);
20405
21615
  const needsValues = discovers || mountsRouters || storagePatterns.length > 0;
20406
21616
  const definitions = /* @__PURE__ */ new Map();
20407
- for (const { file: file2, root, module: moduleBinding } of bound) {
20408
- emitModuleImportFacts(db, file2, moduleBinding, { roots: options.roots });
20409
- if (needsValues) {
20410
- emitValueFacts(db, file2, root);
20411
- }
20412
- 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
+ }
20413
21623
  indexDefinitions(definitions, file2, root);
20414
21624
  }
20415
- }
20416
- const couldMatch = methodsDeclaredNear(db, storagePatterns, definitions);
20417
- const startsAQuery = /* @__PURE__ */ new Set([
20418
- ...couldMatch,
20419
- ...storagePatterns.flatMap((pattern) => pattern.queryFunctions ?? [])
20420
- ]);
20421
- const leadsToStorage = functionsReachingStorage(
20422
- db,
20423
- definitions,
20424
- startsAQuery
20425
- );
20426
- const routerIndex = buildRouterIndex(bound, options.packs, {
20427
- roots: options.roots,
20428
- ...mountsRouters ? { facts: db } : {}
20429
21625
  });
20430
- 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;
20431
21650
  const displayPath = displayPathOf(file2);
20432
- const rawUnits = discoverUnits(root, moduleBinding, {
20433
- packs: options.packs,
20434
- filePath: displayPath,
20435
- absoluteFile: file2,
20436
- routerIndex,
20437
- gapHandling,
20438
- ...needsValues ? { facts: db } : {},
20439
- ...storagePatterns.length > 0 || rawSqlPatterns.length > 0 ? {
20440
- storage: {
20441
- facts: db,
20442
- factsPath: file2,
20443
- patterns: storagePatterns,
20444
- definitionAt: (key) => definitions.get(key),
20445
- couldMatch,
20446
- leadsToStorage,
20447
- rawSql: rawSqlPatterns
20448
- }
20449
- } : {}
20450
- });
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
+ );
20451
21664
  for (const raw of rawUnits) {
20452
- const summary = assembleSummary(raw, { gapHandling });
21665
+ const summary = timer.time(
21666
+ "summarize",
21667
+ () => assembleSummary2(raw, { gapHandling })
21668
+ );
20453
21669
  summary.confidence = { source: "inferred_static", level: "low" };
20454
21670
  summaries.push(summary);
20455
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
+ }
20456
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
+ });
20457
21748
  }
20458
- 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());
20459
21769
  return { summaries, facts: db };
20460
21770
  }
20461
- function stampModuleImports(summaries, db, displayPathOf) {
21771
+ function resolvedImportsOf(db, displayPathOf) {
20462
21772
  const importsByFile = /* @__PURE__ */ new Map();
20463
21773
  for (const [from, , to] of db.facts("pyImportResolved")) {
20464
21774
  if (typeof from !== "string" || typeof to !== "string") {
20465
21775
  continue;
20466
21776
  }
20467
21777
  const key = displayPathOf(from);
20468
- const seen = importsByFile.get(key) ?? /* @__PURE__ */ new Set();
20469
- seen.add(displayPathOf(to));
21778
+ const seen = importsByFile.get(key) ?? [];
21779
+ seen.push(displayPathOf(to));
20470
21780
  importsByFile.set(key, seen);
20471
21781
  }
20472
- for (const summary of summaries) {
20473
- const imports = importsByFile.get(summary.location.file);
20474
- if (imports === void 0 || imports.size === 0) {
20475
- continue;
20476
- }
20477
- summary.metadata = {
20478
- ...summary.metadata ?? {},
20479
- moduleImports: [...imports].sort()
20480
- };
20481
- }
21782
+ return importsByFile;
20482
21783
  }
20483
21784
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
20484
21785
  "__pycache__",
@@ -20533,54 +21834,271 @@ function methodsDeclaredNear(db, patterns, definitions) {
20533
21834
  }
20534
21835
  return found;
20535
21836
  }
20536
- function calledName(call) {
20537
- const callee = field(call, "function");
20538
- if (callee === null) {
20539
- 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
+ }
20540
21857
  }
20541
- return callee.type === "attribute" ? field(callee, "attribute")?.text ?? "" : callee.text;
20542
21858
  }
20543
- function callsUnder(node, found = []) {
20544
- for (const child of node.namedChildren) {
20545
- 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
+ }
20546
22030
  continue;
20547
22031
  }
20548
- if (child.type === "call") {
20549
- found.push(child);
22032
+ if (nameNode.type === "dotted_name") {
22033
+ modules.push(nameNode.text);
20550
22034
  }
20551
- callsUnder(child, found);
20552
22035
  }
20553
- return found;
22036
+ return modules;
20554
22037
  }
20555
- var REACHES_STORAGE_RULES = [
20556
- rule2(
20557
- "reachesStorage",
20558
- [v2("f")],
20559
- [lit2("defCallsName", v2("f"), v2("m")), lit2("queryStart", v2("m"))]
20560
- ),
20561
- rule2(
20562
- "reachesStorage",
20563
- [v2("f")],
20564
- [lit2("defCallsName", v2("f"), v2("m")), lit2("reachesStorage", v2("m"))]
20565
- )
20566
- ];
20567
- function functionsReachingStorage(db, definitions, couldMatch) {
20568
- for (const node of definitions.values()) {
20569
- const name = field(node, "name");
20570
- 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
+ }
20571
22051
  continue;
20572
22052
  }
20573
- for (const called of callsUnder(node).map(calledName)) {
20574
- db.add("defCallsName", [name.text, called]);
22053
+ if (nameNode.type === "dotted_name") {
22054
+ entries.push({ module, name: nameNode.text });
20575
22055
  }
20576
22056
  }
20577
- for (const name of couldMatch) {
20578
- 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
+ }
20579
22096
  }
20580
- evaluate2(db, REACHES_STORAGE_RULES);
20581
- 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 }));
20582
22098
  }
20583
22099
  export {
22100
+ ADAPTER_VERSION,
22101
+ PythonWhySession,
20584
22102
  annotationToShape,
20585
22103
  bindModule,
20586
22104
  buildRouterIndex,
@@ -20595,6 +22113,10 @@ export {
20595
22113
  nodeId,
20596
22114
  objectReturnedBy,
20597
22115
  parsePython,
22116
+ parsePythonSync,
22117
+ preloadPythonGrammar,
22118
+ pythonImportEvidence,
22119
+ readKey,
20598
22120
  resolveAbsoluteModule,
20599
22121
  resolveCalls,
20600
22122
  resolveModule,