@suss/adapter-ruby 0.23.0 → 0.24.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.cjs CHANGED
@@ -607,113 +607,15 @@ function inheritedStatements(ancestry, blockConfigures) {
607
607
  }
608
608
 
609
609
  // src/discovery.ts
610
- var import_behavioral_ir2 = require("@suss/behavioral-ir");
610
+ var import_behavioral_ir3 = require("@suss/behavioral-ir");
611
611
  var import_extractor5 = require("@suss/extractor");
612
612
 
613
- // src/envReads.ts
613
+ // src/clientCalls.ts
614
614
  var import_behavioral_ir = require("@suss/behavioral-ir");
615
- var import_extractor2 = require("@suss/extractor");
616
- var RUBY_ENV_RECOGNITION = "ruby-env";
617
- var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
618
- function envReadEffects(root) {
619
- const effects = [];
620
- (0, import_extractor2.walkDescendants)(root, null, {
621
- at: (node) => {
622
- const read = envReadAt(node);
623
- if (read !== null) {
624
- effects.push(configReadEffect(read));
625
- }
626
- },
627
- into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? import_extractor2.SKIP_CHILDREN : null
628
- });
629
- return effects;
630
- }
631
- function configReadEffect(read) {
632
- return {
633
- type: "interaction",
634
- binding: (0, import_behavioral_ir.runtimeConfigBinding)({
635
- recognition: RUBY_ENV_RECOGNITION,
636
- deploymentTarget: "lambda",
637
- instanceName: "<unknown>"
638
- }),
639
- callee: `ENV["${read.name}"]`,
640
- interaction: {
641
- class: "config-read",
642
- name: read.name,
643
- defaulted: read.defaulted
644
- }
645
- };
646
- }
647
- function envReadAt(node) {
648
- if (node.type === "element_reference") {
649
- return elementRead(node);
650
- }
651
- if (node.type === "call") {
652
- return fetchRead(node);
653
- }
654
- return null;
655
- }
656
- function elementRead(node) {
657
- const object = field(node, "object");
658
- if (object === null || !isEnv(object) || isAssignedTo(node)) {
659
- return null;
660
- }
661
- const index = node.namedChildren.find(
662
- (child) => child !== null && child.id !== object.id
663
- );
664
- const name = index === void 0 ? null : stringLiteralValue(index);
665
- if (name === null) {
666
- return null;
667
- }
668
- return { name, defaulted: isDefaultedAt(node) };
669
- }
670
- function fetchRead(node) {
671
- const receiver = field(node, "receiver");
672
- if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
673
- return null;
674
- }
675
- const { positional } = readCallArgs(field(node, "arguments"));
676
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
677
- if (name === null) {
678
- return null;
679
- }
680
- const hasDefault = positional.length > 1 || field(node, "block") !== null;
681
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
682
- }
683
- function isEnv(node) {
684
- if (node.type === "constant") {
685
- return node.text === "ENV";
686
- }
687
- return node.type === "scope_resolution" && field(node, "scope") === null && field(node, "name")?.text === "ENV";
688
- }
689
- function isAssignedTo(node) {
690
- const parent = node.parent;
691
- return parent !== null && (parent.type === "assignment" || parent.type === "operator_assignment") && field(parent, "left")?.id === node.id;
692
- }
693
- function isDefaultedAt(node) {
694
- let child = node;
695
- let parent = node.parent;
696
- while (parent !== null) {
697
- if (parent.type === "parenthesized_statements") {
698
- child = parent;
699
- parent = parent.parent;
700
- continue;
701
- }
702
- if (parent.type !== "binary" || field(parent, "operator")?.text !== "||") {
703
- return false;
704
- }
705
- if (field(parent, "left")?.id === child.id) {
706
- return true;
707
- }
708
- child = parent;
709
- parent = parent.parent;
710
- }
711
- return false;
712
- }
615
+ var import_values5 = require("@suss/values");
713
616
 
714
- // src/responseStatus.ts
715
- var import_extractor3 = require("@suss/extractor");
716
- var import_values4 = require("@suss/values");
617
+ // src/paths/effects.ts
618
+ var import_extractor2 = require("@suss/extractor");
717
619
 
718
620
  // src/paths/bareCalls.ts
719
621
  var PARAMETER_LIST_TYPES = [
@@ -1076,17 +978,297 @@ var Lowerer = class {
1076
978
  }
1077
979
  return this.attach({ kind: "opaque", exitKind: exitKindOf(node) }, [node]);
1078
980
  }
1079
- };
1080
- function lowerRubyBody(body, terminals, responseCalls = []) {
1081
- const lowerer = new Lowerer(
1082
- new Map(terminals.map((terminal) => [terminal.id, terminal])),
1083
- new NodeSet(responseCalls)
1084
- );
1085
- return {
1086
- statements: lowerer.lowerBlock(body),
1087
- terminalsByStmt: lowerer.terminalsByStmt,
1088
- terminalHome: lowerer.terminalHome
1089
- };
981
+ };
982
+ function lowerRubyBody(body, terminals, responseCalls = []) {
983
+ const lowerer = new Lowerer(
984
+ new Map(terminals.map((terminal) => [terminal.id, terminal])),
985
+ new NodeSet(responseCalls)
986
+ );
987
+ return {
988
+ statements: lowerer.lowerBlock(body),
989
+ terminalsByStmt: lowerer.terminalsByStmt,
990
+ terminalHome: lowerer.terminalHome
991
+ };
992
+ }
993
+
994
+ // src/paths/effects.ts
995
+ var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
996
+ var BLOCK_TYPES = /* @__PURE__ */ new Set([
997
+ "body_statement",
998
+ "then",
999
+ "else",
1000
+ "do_block",
1001
+ "block"
1002
+ ]);
1003
+ var LITERAL_ARGS = {
1004
+ string: (node) => ({
1005
+ kind: "string",
1006
+ value: node.text.replace(/^["']|["']$/g, "")
1007
+ }),
1008
+ integer: (node) => {
1009
+ const value = Number.parseInt(node.text, 10);
1010
+ return Number.isNaN(value) ? null : { kind: "number", value };
1011
+ },
1012
+ float: (node) => {
1013
+ const value = Number.parseFloat(node.text);
1014
+ return Number.isNaN(value) ? null : { kind: "number", value };
1015
+ },
1016
+ true: () => ({ kind: "boolean", value: true }),
1017
+ false: () => ({ kind: "boolean", value: false }),
1018
+ simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
1019
+ };
1020
+ function children(node) {
1021
+ return node.namedChildren.filter((child) => child !== null);
1022
+ }
1023
+ function withoutChainLinks(calls) {
1024
+ const isLink = /* @__PURE__ */ new Set();
1025
+ for (const call of calls) {
1026
+ const receiver = field(call, "receiver");
1027
+ if (receiver !== null && receiver.type === "call") {
1028
+ isLink.add(receiver.id);
1029
+ }
1030
+ }
1031
+ return calls.filter((call) => !isLink.has(call.id));
1032
+ }
1033
+ function calleeMethodName(call) {
1034
+ if (call.type === "identifier") {
1035
+ return call.text;
1036
+ }
1037
+ const method = field(call, "method");
1038
+ if (method !== null) {
1039
+ return method.text;
1040
+ }
1041
+ return field(call, "receiver") !== null ? "call" : children(call)[0]?.text;
1042
+ }
1043
+ function isPropertyRead(node) {
1044
+ if (field(node, "receiver") === null || field(node, "arguments") !== null) {
1045
+ return false;
1046
+ }
1047
+ return calleeMethodName(node) !== "call";
1048
+ }
1049
+ function isRaise2(node) {
1050
+ const method = field(node, "method") ?? children(node)[0] ?? null;
1051
+ return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
1052
+ }
1053
+ var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
1054
+ function isInherited(node, inherited) {
1055
+ const method = field(node, "method");
1056
+ return field(node, "receiver") === null && method !== null && inherited.has(method.text);
1057
+ }
1058
+ function isCall(node, locals, inherited) {
1059
+ if (node.type === "call") {
1060
+ return !isPropertyRead(node) && !isRaise2(node) && !isInherited(node, inherited);
1061
+ }
1062
+ return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
1063
+ }
1064
+ function collectCalls(node, locals, inherited, found) {
1065
+ for (const child of children(node)) {
1066
+ if (OWN_BODY_TYPES.has(child.type)) {
1067
+ continue;
1068
+ }
1069
+ if (isCall(child, locals, inherited)) {
1070
+ found.push(child);
1071
+ }
1072
+ collectCalls(child, locals, inherited, found);
1073
+ }
1074
+ return found;
1075
+ }
1076
+ function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
1077
+ const body = field(definitionNode, "body");
1078
+ if (body === null) {
1079
+ return [];
1080
+ }
1081
+ return collectCalls(body, localNamesIn(definitionNode), inherited, []);
1082
+ }
1083
+ function calleeText(call) {
1084
+ const receiver = field(call, "receiver");
1085
+ const method = calleeMethodName(call);
1086
+ if (method === void 0) {
1087
+ return call.text;
1088
+ }
1089
+ return receiver === null ? method : `${receiver.text}.${method}`;
1090
+ }
1091
+ function argOf(node) {
1092
+ const literal = LITERAL_ARGS[node.type]?.(node);
1093
+ if (literal !== null && literal !== void 0) {
1094
+ return literal;
1095
+ }
1096
+ if (node.type === "call" && !isPropertyRead(node)) {
1097
+ return { kind: "call", callee: calleeText(node), args: argsOf(node) };
1098
+ }
1099
+ return { kind: "identifier", name: node.text };
1100
+ }
1101
+ function argsOf(call) {
1102
+ const args = field(call, "arguments");
1103
+ if (args === null) {
1104
+ return [];
1105
+ }
1106
+ return children(args).map(
1107
+ (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
1108
+ );
1109
+ }
1110
+ function enclosingStatement(call, body) {
1111
+ let current = call;
1112
+ while (current.parent !== null) {
1113
+ if (BLOCK_TYPES.has(current.parent.type) || current.parent.id === body.id) {
1114
+ return current;
1115
+ }
1116
+ current = current.parent;
1117
+ }
1118
+ return current;
1119
+ }
1120
+ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
1121
+ const body = field(definitionNode, "body");
1122
+ if (body === null) {
1123
+ return [];
1124
+ }
1125
+ const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
1126
+ if (calls.length === 0) {
1127
+ return [];
1128
+ }
1129
+ const statementOf2 = /* @__PURE__ */ new Map();
1130
+ const statements = [];
1131
+ for (const call of calls) {
1132
+ const statement = enclosingStatement(call, body);
1133
+ statementOf2.set(call.id, statement);
1134
+ statements.push(statement);
1135
+ }
1136
+ const lowered = lowerRubyBody(body, statements);
1137
+ const enumerated = (0, import_extractor2.enumerateOrDegrade)(
1138
+ {
1139
+ statements: lowered.statements,
1140
+ terminalsByStmt: lowered.terminalsByStmt
1141
+ },
1142
+ statements
1143
+ );
1144
+ return calls.map((call) => {
1145
+ const statement = statementOf2.get(call.id);
1146
+ const conditions = (0, import_extractor2.sharedGatingConditions)(
1147
+ statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
1148
+ );
1149
+ return {
1150
+ type: "invocation",
1151
+ callee: calleeText(call),
1152
+ args: argsOf(call),
1153
+ async: false,
1154
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
1155
+ };
1156
+ });
1157
+ }
1158
+
1159
+ // src/responseStatus.ts
1160
+ var import_extractor3 = require("@suss/extractor");
1161
+ var import_values4 = require("@suss/values");
1162
+
1163
+ // src/paths/predicates.ts
1164
+ var OPERATORS = {
1165
+ "==": "eq",
1166
+ "!=": "neq",
1167
+ ">": "gt",
1168
+ ">=": "gte",
1169
+ "<": "lt",
1170
+ "<=": "lte"
1171
+ };
1172
+ var CONVERSIONS = /* @__PURE__ */ new Set(["to_i", "to_s", "to_sym"]);
1173
+ var opaqueOf = (node) => ({
1174
+ type: "opaque",
1175
+ sourceText: node.text,
1176
+ reason: "complexExpression"
1177
+ });
1178
+ function predicateOf(node) {
1179
+ if (node.type === "parenthesized_statements") {
1180
+ const inner = node.namedChildren[0];
1181
+ return inner == null ? opaqueOf(node) : predicateOf(inner);
1182
+ }
1183
+ if (node.type === "unary" && field(node, "operator")?.text === "!") {
1184
+ const operand2 = field(node, "operand") ?? node.namedChildren[0];
1185
+ return operand2 == null ? opaqueOf(node) : { type: "negation", operand: predicateOf(operand2) };
1186
+ }
1187
+ if (node.type === "binary") {
1188
+ return binaryOf(node);
1189
+ }
1190
+ const asked = calledMethod(node);
1191
+ if (asked === "nil?") {
1192
+ const receiver = field(node, "receiver");
1193
+ return receiver === null ? opaqueOf(node) : { type: "nullCheck", subject: valueRefOf(receiver), negated: false };
1194
+ }
1195
+ if (node.type === "identifier" || memberChain(node) !== null) {
1196
+ return {
1197
+ type: "truthinessCheck",
1198
+ subject: valueRefOf(node),
1199
+ negated: false
1200
+ };
1201
+ }
1202
+ return opaqueOf(node);
1203
+ }
1204
+ function binaryOf(node) {
1205
+ const operator = field(node, "operator")?.text ?? "";
1206
+ const left = field(node, "left");
1207
+ const right = field(node, "right");
1208
+ if (left === null || right === null) {
1209
+ return opaqueOf(node);
1210
+ }
1211
+ const op = OPERATORS[operator];
1212
+ if (op !== void 0) {
1213
+ return {
1214
+ type: "comparison",
1215
+ left: valueRefOf(left),
1216
+ op,
1217
+ right: valueRefOf(right)
1218
+ };
1219
+ }
1220
+ if (operator === "&&" || operator === "||" || operator === "and" || operator === "or") {
1221
+ return {
1222
+ type: "compound",
1223
+ op: operator === "&&" || operator === "and" ? "and" : "or",
1224
+ operands: [predicateOf(left), predicateOf(right)]
1225
+ };
1226
+ }
1227
+ return opaqueOf(node);
1228
+ }
1229
+ function valueRefOf(node) {
1230
+ const literal = literalOf(node);
1231
+ if (literal !== null) {
1232
+ return literal;
1233
+ }
1234
+ const chain = memberChain(node);
1235
+ if (chain !== null) {
1236
+ const [name, ...accessChain] = chain;
1237
+ if (name !== void 0 && accessChain.length > 0) {
1238
+ return { type: "dependency", name, accessChain };
1239
+ }
1240
+ }
1241
+ return { type: "unresolved", sourceText: node.text };
1242
+ }
1243
+ function literalOf(node) {
1244
+ if (node.type === "integer") {
1245
+ const value = Number.parseInt(node.text, 10);
1246
+ return Number.isNaN(value) ? null : { type: "literal", value };
1247
+ }
1248
+ const string = stringLiteralValue(node);
1249
+ if (string !== null) {
1250
+ return { type: "literal", value: string };
1251
+ }
1252
+ const boolean = booleanLiteralValue(node);
1253
+ return boolean === null ? null : { type: "literal", value: boolean };
1254
+ }
1255
+ function memberChain(node, tail = []) {
1256
+ if (node.type === "identifier") {
1257
+ return [node.text, ...tail];
1258
+ }
1259
+ const called = calledMethod(node);
1260
+ const receiver = field(node, "receiver");
1261
+ if (called === null || receiver === null || field(node, "arguments") !== null) {
1262
+ return null;
1263
+ }
1264
+ const rest = CONVERSIONS.has(called) && tail.length === 0 ? tail : [called, ...tail];
1265
+ return memberChain(receiver, rest);
1266
+ }
1267
+ function calledMethod(node) {
1268
+ if (node.type !== "call" || field(node, "block") !== null) {
1269
+ return null;
1270
+ }
1271
+ return field(node, "method")?.text ?? null;
1090
1272
  }
1091
1273
 
1092
1274
  // src/values/evaluator.ts
@@ -1144,7 +1326,7 @@ function paramNameOf(param) {
1144
1326
  function declaredParamNames(method) {
1145
1327
  const params = field(method, "parameters");
1146
1328
  const declared = /* @__PURE__ */ new Set();
1147
- for (const param of params === null ? [] : children(params)) {
1329
+ for (const param of params === null ? [] : children2(params)) {
1148
1330
  const name = paramNameOf(param);
1149
1331
  if (name !== null) {
1150
1332
  declared.add(name.text);
@@ -1158,7 +1340,7 @@ function readKey(filePath, node, enclosing) {
1158
1340
  }
1159
1341
  const parent = node.parent;
1160
1342
  if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
1161
- return isPropertyRead(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1343
+ return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1162
1344
  }
1163
1345
  if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
1164
1346
  return `${nodeId(filePath, enclosing)}#${node.text}`;
@@ -1186,7 +1368,7 @@ var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1186
1368
  "parenthesized_statements"
1187
1369
  ]);
1188
1370
  var ARRAY_TYPES = /* @__PURE__ */ new Set(["array", "string_array", "symbol_array"]);
1189
- function children(node) {
1371
+ function children2(node) {
1190
1372
  return node.namedChildren.filter((child) => child !== null);
1191
1373
  }
1192
1374
  function add(emitter, relation, ...tuple) {
@@ -1214,8 +1396,8 @@ function pairKeyText(key) {
1214
1396
  }
1215
1397
  return null;
1216
1398
  }
1217
- function isPropertyRead(node) {
1218
- return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children(node).every(
1399
+ function isPropertyRead2(node) {
1400
+ return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children2(node).every(
1219
1401
  (child) => child.type !== "do_block" && child.type !== "block"
1220
1402
  );
1221
1403
  }
@@ -1254,7 +1436,7 @@ function emitCall(emitter, call) {
1254
1436
  }
1255
1437
  const args = field(call, "arguments");
1256
1438
  let position = 0;
1257
- for (const argument of args === null ? [] : children(args)) {
1439
+ for (const argument of args === null ? [] : children2(args)) {
1258
1440
  if (argument.type === "pair") {
1259
1441
  const key = field(argument, "key");
1260
1442
  const value = field(argument, "value");
@@ -1284,7 +1466,7 @@ function emitArray(emitter, array) {
1284
1466
  const objectKey = nodeId(emitter.filePath, array);
1285
1467
  add(emitter, "objectValue", objectKey);
1286
1468
  let position = 0;
1287
- for (const element of children(array)) {
1469
+ for (const element of children2(array)) {
1288
1470
  add(
1289
1471
  emitter,
1290
1472
  "holdsProperty",
@@ -1298,7 +1480,7 @@ function emitArray(emitter, array) {
1298
1480
  function emitHash(emitter, hash) {
1299
1481
  const objectKey = nodeId(emitter.filePath, hash);
1300
1482
  add(emitter, "objectValue", objectKey);
1301
- for (const pair of children(hash)) {
1483
+ for (const pair of children2(hash)) {
1302
1484
  if (pair.type !== "pair") {
1303
1485
  continue;
1304
1486
  }
@@ -1312,7 +1494,7 @@ function emitHash(emitter, hash) {
1312
1494
  }
1313
1495
  }
1314
1496
  function walkExpressions(node, visit) {
1315
- for (const child of children(node)) {
1497
+ for (const child of children2(node)) {
1316
1498
  if (OWN_BODY_TYPES.has(child.type)) {
1317
1499
  continue;
1318
1500
  }
@@ -1322,7 +1504,7 @@ function walkExpressions(node, visit) {
1322
1504
  }
1323
1505
  function emitExpressionFacts(emitter, node) {
1324
1506
  walkExpressions(node, (child) => {
1325
- if (isPropertyRead(child)) {
1507
+ if (isPropertyRead2(child)) {
1326
1508
  emitPropertyRead(emitter, child);
1327
1509
  } else if (child.type === "call") {
1328
1510
  emitCall(emitter, child);
@@ -1342,7 +1524,7 @@ function emitExpressionFacts(emitter, node) {
1342
1524
  });
1343
1525
  }
1344
1526
  function implicitReturn(body) {
1345
- const statements = children(body).filter(
1527
+ const statements = children2(body).filter(
1346
1528
  (child) => child.type !== "rescue" && child.type !== "ensure"
1347
1529
  );
1348
1530
  const last = statements[statements.length - 1];
@@ -1354,7 +1536,7 @@ function emitMethodFacts(emitter, method) {
1354
1536
  const params = field(method, "parameters");
1355
1537
  const declared = /* @__PURE__ */ new Set();
1356
1538
  let position = 0;
1357
- for (const param of params === null ? [] : children(params)) {
1539
+ for (const param of params === null ? [] : children2(params)) {
1358
1540
  const paramName = paramNameOf(param);
1359
1541
  if (paramName !== null) {
1360
1542
  declared.add(paramName.text);
@@ -1373,7 +1555,7 @@ function emitMethodFacts(emitter, method) {
1373
1555
  return funcKey;
1374
1556
  }
1375
1557
  const recordNested = (node) => {
1376
- for (const child of children(node)) {
1558
+ for (const child of children2(node)) {
1377
1559
  if (OWN_BODY_TYPES.has(child.type)) {
1378
1560
  add(emitter, "containsFn", funcKey, nodeId(emitter.filePath, child));
1379
1561
  continue;
@@ -1384,8 +1566,8 @@ function emitMethodFacts(emitter, method) {
1384
1566
  recordNested(body);
1385
1567
  walkExpressions(body, (child) => {
1386
1568
  if (child.type === "return") {
1387
- const first = children(child)[0];
1388
- const returned = first?.type === "argument_list" ? children(first)[0] : first;
1569
+ const first = children2(child)[0];
1570
+ const returned = first?.type === "argument_list" ? children2(first)[0] : first;
1389
1571
  if (returned !== void 0) {
1390
1572
  add(inside, "returnsValue", funcKey, valueKey(inside, returned));
1391
1573
  }
@@ -1429,13 +1611,13 @@ function emitClassFacts(emitter, cls) {
1429
1611
  const classKey = nodeId(emitter.filePath, cls);
1430
1612
  add(emitter, "objectValue", classKey);
1431
1613
  const superclass = field(cls, "superclass");
1432
- const base = superclass === null ? null : children(superclass)[0] ?? null;
1614
+ const base = superclass === null ? null : children2(superclass)[0] ?? null;
1433
1615
  if (base !== null) {
1434
1616
  add(emitter, "extends", classKey, valueKey(emitter, base));
1435
1617
  add(emitter, "extendsNamed", classKey, base.text);
1436
1618
  }
1437
1619
  const body = field(cls, "body");
1438
- for (const statement of body === null ? [] : children(body)) {
1620
+ for (const statement of body === null ? [] : children2(body)) {
1439
1621
  if (statement.type === "assignment") {
1440
1622
  const left = field(statement, "left");
1441
1623
  const right = field(statement, "right");
@@ -1477,7 +1659,7 @@ function emitValueFacts(db, filePath, root) {
1477
1659
  add(emitter, "exportsAs", filePath, name.text, key);
1478
1660
  };
1479
1661
  const walk = (node) => {
1480
- for (const child of children(node)) {
1662
+ for (const child of children2(node)) {
1481
1663
  if (NESTING_TYPES.has(child.type)) {
1482
1664
  declaresName(child, emitClassFacts(emitter, child));
1483
1665
  continue;
@@ -1508,7 +1690,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1508
1690
  "module",
1509
1691
  "singleton_class"
1510
1692
  ]);
1511
- var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
1693
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1512
1694
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1513
1695
  var NAME_TYPES = /* @__PURE__ */ new Set([
1514
1696
  "identifier",
@@ -2014,7 +2196,7 @@ function isTailExpression(node) {
2014
2196
  if (owner.type === "block" && owner.parent?.type === "lambda") {
2015
2197
  return true;
2016
2198
  }
2017
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES.has(owner.type)) {
2199
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
2018
2200
  return true;
2019
2201
  }
2020
2202
  if (owner.type === "when") {
@@ -2033,7 +2215,7 @@ function isRoot(node) {
2033
2215
  if (OWN_ROOT_TYPES.has(node.type)) {
2034
2216
  return true;
2035
2217
  }
2036
- return BLOCK_TYPES.has(node.type) && node.parent?.type === "call";
2218
+ return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
2037
2219
  }
2038
2220
  function isPathStatement(node, parent) {
2039
2221
  if (node.type === "elsif") {
@@ -2087,7 +2269,7 @@ function functionOf(node) {
2087
2269
  body: bodyOf(block)
2088
2270
  };
2089
2271
  }
2090
- if (BLOCK_TYPES.has(node.type)) {
2272
+ if (BLOCK_TYPES2.has(node.type)) {
2091
2273
  return {
2092
2274
  parameters: parametersOf(field(node, "parameters")),
2093
2275
  body: bodyOf(node)
@@ -2405,6 +2587,18 @@ var calleeRows = [
2405
2587
  kind: "callee",
2406
2588
  origin: { module: "Kernel", name: "String" },
2407
2589
  apply: ({ args }) => ({ result: (0, import_values.concat)([(0, import_values.operand)(args[0])]) })
2590
+ },
2591
+ // A URI is the string it was parsed from, as far as a reader of paths
2592
+ // is concerned, and both spellings are the standard library's own.
2593
+ {
2594
+ kind: "callee",
2595
+ origin: { module: "Kernel", name: "URI" },
2596
+ apply: ({ args }) => ({ result: (0, import_values.concat)([(0, import_values.operand)(args[0])]) })
2597
+ },
2598
+ {
2599
+ kind: "callee",
2600
+ origin: { module: "URI", name: "parse" },
2601
+ apply: ({ args }) => ({ result: (0, import_values.concat)([(0, import_values.operand)(args[0])]) })
2408
2602
  }
2409
2603
  ];
2410
2604
  var rubyRows = [
@@ -2592,7 +2786,7 @@ function effectsReaching(effects, conditions) {
2592
2786
  function conditionOf(condition) {
2593
2787
  return {
2594
2788
  sourceText: condition.sourceText,
2595
- structured: null,
2789
+ structured: condition.expression === null ? null : predicateOf(condition.expression),
2596
2790
  polarity: condition.polarity,
2597
2791
  source: condition.source
2598
2792
  };
@@ -2646,55 +2840,414 @@ function handsOnBranch(outcome, effects, extraEffects) {
2646
2840
  isDefault: outcome.conditions.length === 0
2647
2841
  };
2648
2842
  }
2843
+ function returnPathBranches(method, effects) {
2844
+ const body = field(method, "body");
2845
+ if (body === null) {
2846
+ return null;
2847
+ }
2848
+ const returns = collectReturns(body, []);
2849
+ const lowered = lowerRubyBody(body, returns, []);
2850
+ const enumerated = (0, import_extractor3.enumerateOrDegrade)(
2851
+ {
2852
+ statements: lowered.statements,
2853
+ terminalsByStmt: lowered.terminalsByStmt
2854
+ },
2855
+ returns
2856
+ );
2857
+ const branches = [];
2858
+ const push = (paths, at) => {
2859
+ for (const path7 of paths) {
2860
+ const conditions = path7.map(conditionOf);
2861
+ branches.push({
2862
+ conditions,
2863
+ terminal: {
2864
+ kind: "return",
2865
+ statusCode: null,
2866
+ body: null,
2867
+ exceptionType: null,
2868
+ message: null,
2869
+ component: null,
2870
+ renderTree: null,
2871
+ delegateTarget: null,
2872
+ emitEvent: null,
2873
+ location: at
2874
+ },
2875
+ effects: effectsReaching(effects, conditions),
2876
+ location: at,
2877
+ isDefault: conditions.length === 0
2878
+ });
2879
+ }
2880
+ };
2881
+ for (const statement of returns) {
2882
+ push(enumerated.byTerminal.get(statement) ?? [], rangeOf(statement));
2883
+ }
2884
+ push(enumerated.fallthrough, rangeOf(method));
2885
+ return branches.length === 0 ? null : branches;
2886
+ }
2649
2887
  function responseBranches(method, pattern, effects, extraEffects, options = {}) {
2650
2888
  const declarations = pattern.responseStatusCalls ?? [];
2651
2889
  const body = field(method, "body");
2652
2890
  if (declarations.length === 0 || body === null) {
2653
2891
  return null;
2654
2892
  }
2655
- const byName = declarationsByName(declarations);
2656
- const locals = localNamesIn(method);
2657
- const responses = collectResponseCalls(body, byName, locals, []);
2658
- const returns = collectReturns(body, []);
2659
- const lowered = lowerRubyBody(body, returns, responses);
2660
- const bareReturns = returns.filter(
2661
- (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
2662
- );
2663
- const terminals = [...responses, ...bareReturns];
2664
- const enumerated = (0, import_extractor3.enumerateOrDegrade)(
2665
- {
2666
- statements: lowered.statements,
2667
- terminalsByStmt: lowered.terminalsByStmt
2668
- },
2669
- terminals
2893
+ const byName = declarationsByName(declarations);
2894
+ const locals = localNamesIn(method);
2895
+ const responses = collectResponseCalls(body, byName, locals, []);
2896
+ const returns = collectReturns(body, []);
2897
+ const lowered = lowerRubyBody(body, returns, responses);
2898
+ const bareReturns = returns.filter(
2899
+ (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
2900
+ );
2901
+ const terminals = [...responses, ...bareReturns];
2902
+ const enumerated = (0, import_extractor3.enumerateOrDegrade)(
2903
+ {
2904
+ statements: lowered.statements,
2905
+ terminalsByStmt: lowered.terminalsByStmt
2906
+ },
2907
+ terminals
2908
+ );
2909
+ const statusNames = pattern.statusCodeNames ?? {};
2910
+ const outcomes = [];
2911
+ for (const terminal of terminals) {
2912
+ const declaration = declarationOf(terminal, byName, locals);
2913
+ const reading = declaration === void 0 ? import_extractor3.absentReading : readingOfCall(terminal, declaration, statusNames);
2914
+ for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
2915
+ outcomes.push({
2916
+ conditions: path7.map(conditionOf),
2917
+ reading,
2918
+ location: rangeOf(terminal)
2919
+ });
2920
+ }
2921
+ }
2922
+ for (const path7 of enumerated.fallthrough) {
2923
+ outcomes.push({
2924
+ conditions: path7.map(conditionOf),
2925
+ reading: import_extractor3.absentReading,
2926
+ location: rangeOf(method),
2927
+ fellThrough: true
2928
+ });
2929
+ }
2930
+ if (outcomes.length === 0) {
2931
+ return null;
2932
+ }
2933
+ return outcomes.map(
2934
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2935
+ );
2936
+ }
2937
+
2938
+ // src/clientCalls.ts
2939
+ function clientCallUnits(root, pack, pattern, options) {
2940
+ const units = [];
2941
+ for (const method of methodDefinitions(root)) {
2942
+ const name = field(method, "name")?.text;
2943
+ if (name === void 0) {
2944
+ continue;
2945
+ }
2946
+ for (const call of callsUnder(method)) {
2947
+ const request = requestCall(call, method, pattern, options);
2948
+ if (request === null) {
2949
+ continue;
2950
+ }
2951
+ units.push(clientUnit(method, name, request, pattern, pack, options));
2952
+ }
2953
+ }
2954
+ return units;
2955
+ }
2956
+ function methodDefinitions(node, found = []) {
2957
+ for (const child of node.namedChildren) {
2958
+ if (child === null) {
2959
+ continue;
2960
+ }
2961
+ if (child.type === "method" || child.type === "singleton_method") {
2962
+ found.push(child);
2963
+ }
2964
+ methodDefinitions(child, found);
2965
+ }
2966
+ return found;
2967
+ }
2968
+ function callsUnder(node, found = []) {
2969
+ for (const child of node.namedChildren) {
2970
+ if (child === null) {
2971
+ continue;
2972
+ }
2973
+ if (child.type === "call") {
2974
+ found.push(child);
2975
+ }
2976
+ callsUnder(child, found);
2977
+ }
2978
+ return found;
2979
+ }
2980
+ function requestCall(call, method, pattern, options) {
2981
+ const receiver = field(call, "receiver");
2982
+ const called = field(call, "method")?.text;
2983
+ if (receiver === null || called === void 0) {
2984
+ return null;
2985
+ }
2986
+ const prefix = receiverPrefix(receiver, method, pattern, options);
2987
+ if (prefix === null) {
2988
+ return null;
2989
+ }
2990
+ const args = readCallArgs(field(call, "arguments"));
2991
+ const verb = pattern.verbMethodNames[called];
2992
+ if (verb !== void 0) {
2993
+ const path8 = urlIn(args, pattern, options);
2994
+ return path8 === null ? null : { method: verb, path: prefix + path8 };
2995
+ }
2996
+ const sent = pattern.requestObject;
2997
+ if (sent === void 0 || sent.attribute !== called) {
2998
+ return null;
2999
+ }
3000
+ const built = requestBuilt(args.positional[0], method, sent);
3001
+ if (built === null) {
3002
+ return null;
3003
+ }
3004
+ const path7 = pathAt(built.url, options);
3005
+ return path7 === null ? null : { method: built.method, path: prefix + path7 };
3006
+ }
3007
+ function requestBuilt(argument, method, sent) {
3008
+ const written = argument === void 0 ? null : assignedCall(argument, method) ?? argument;
3009
+ if (written === null || written.type !== "call") {
3010
+ return null;
3011
+ }
3012
+ const requestClass = field(written, "receiver")?.text;
3013
+ const verb = requestClass === void 0 ? void 0 : sent.constructors[requestClass];
3014
+ if (verb === void 0) {
3015
+ return null;
3016
+ }
3017
+ const args = readCallArgs(field(written, "arguments"));
3018
+ return { method: verb, url: args.positional[sent.urlPosition] };
3019
+ }
3020
+ function assignedCall(node, method) {
3021
+ if (node.type !== "identifier") {
3022
+ return null;
3023
+ }
3024
+ const value = assignedValue(node.text, method);
3025
+ return value !== null && value.type === "call" ? value : null;
3026
+ }
3027
+ function receiverPrefix(receiver, method, pattern, options) {
3028
+ if (namesConstant(receiver, pattern.constantName)) {
3029
+ return "";
3030
+ }
3031
+ if (receiver.type !== "identifier") {
3032
+ return null;
3033
+ }
3034
+ const built = builtBy(receiver.text, method, pattern);
3035
+ if (built === null) {
3036
+ return null;
3037
+ }
3038
+ const keyword = pattern.builderUrlKeyword;
3039
+ if (keyword === void 0) {
3040
+ return "";
3041
+ }
3042
+ const base = readCallArgs(field(built, "arguments")).keyword[keyword];
3043
+ const path7 = base === void 0 ? null : (0, import_values5.pathOf)(evaluatedValue(base, options.facts));
3044
+ return path7 === void 0 || path7 === null ? "" : trimmed(path7);
3045
+ }
3046
+ function trimmed(path7) {
3047
+ return path7 === "/" ? "" : path7.replace(/\/+$/, "");
3048
+ }
3049
+ function namesConstant(receiver, constantName) {
3050
+ return (receiver.type === "constant" || receiver.type === "scope_resolution") && receiver.text === constantName;
3051
+ }
3052
+ function builtBy(name, method, pattern) {
3053
+ const builders = pattern.receiverBuilders ?? [];
3054
+ const value = builders.length === 0 ? null : assignedValue(name, method);
3055
+ if (value === null || value.type !== "call" || !builders.includes(field(value, "method")?.text ?? "")) {
3056
+ return null;
3057
+ }
3058
+ const receiver = field(value, "receiver");
3059
+ return receiver !== null && namesConstant(receiver, pattern.constantName) ? value : null;
3060
+ }
3061
+ function assignedValue(name, method) {
3062
+ const body = field(method, "body");
3063
+ if (body === null) {
3064
+ return null;
3065
+ }
3066
+ for (const statement of runStatements(body)) {
3067
+ if (statement.type !== "assignment") {
3068
+ continue;
3069
+ }
3070
+ if (field(statement, "left")?.text === name) {
3071
+ return field(statement, "right");
3072
+ }
3073
+ }
3074
+ return null;
3075
+ }
3076
+ function urlIn(args, pattern, options) {
3077
+ const keyword = pattern.url.keyword;
3078
+ const written = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
3079
+ return pathAt(written, options);
3080
+ }
3081
+ function pathAt(written, options) {
3082
+ if (written === void 0) {
3083
+ return null;
3084
+ }
3085
+ const path7 = (0, import_values5.pathOf)(evaluatedValue(written, options.facts));
3086
+ return path7 === void 0 || (0, import_behavioral_ir.namesNothing)(path7) ? null : path7;
3087
+ }
3088
+ function clientUnit(method, name, request, pattern, pack, options) {
3089
+ const range = rangeOf(method);
3090
+ return {
3091
+ identity: {
3092
+ name,
3093
+ nameKind: "binding",
3094
+ kind: "client",
3095
+ file: options.filePath,
3096
+ range,
3097
+ span: spanOf(method),
3098
+ exportName: name,
3099
+ exportPath: [name]
3100
+ },
3101
+ boundaryBinding: (0, import_behavioral_ir.restBinding)({
3102
+ transport: pack.protocol,
3103
+ method: request.method,
3104
+ path: request.path,
3105
+ recognition: pack.name
3106
+ }),
3107
+ parameters: [],
3108
+ branches: callerBranches(method, range),
3109
+ ...responseAccessors(pattern),
3110
+ bodyContent: "statements",
3111
+ dependencyCalls: [],
3112
+ declaredContract: null
3113
+ };
3114
+ }
3115
+ function callerBranches(method, range) {
3116
+ const effects = invocationEffects(method);
3117
+ return returnPathBranches(method, effects) ?? [returnBranch(range, effects)];
3118
+ }
3119
+ function responseAccessors(pattern) {
3120
+ const response = pattern.response;
3121
+ if (response === void 0) {
3122
+ return {};
3123
+ }
3124
+ return {
3125
+ ...response.body === void 0 ? {} : { bodyAccessors: response.body },
3126
+ ...response.statusCode === void 0 ? {} : { statusAccessors: response.statusCode },
3127
+ ...response.success === void 0 ? {} : { successAccessors: response.success },
3128
+ ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery }
3129
+ };
3130
+ }
3131
+ function returnBranch(range, effects) {
3132
+ return {
3133
+ conditions: [],
3134
+ terminal: {
3135
+ kind: "return",
3136
+ statusCode: null,
3137
+ body: null,
3138
+ exceptionType: null,
3139
+ message: null,
3140
+ component: null,
3141
+ renderTree: null,
3142
+ delegateTarget: null,
3143
+ emitEvent: null,
3144
+ location: range
3145
+ },
3146
+ effects,
3147
+ location: range,
3148
+ isDefault: true
3149
+ };
3150
+ }
3151
+
3152
+ // src/envReads.ts
3153
+ var import_behavioral_ir2 = require("@suss/behavioral-ir");
3154
+ var import_extractor4 = require("@suss/extractor");
3155
+ var RUBY_ENV_RECOGNITION = "ruby-env";
3156
+ var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
3157
+ function envReadEffects(root) {
3158
+ const effects = [];
3159
+ (0, import_extractor4.walkDescendants)(root, null, {
3160
+ at: (node) => {
3161
+ const read = envReadAt(node);
3162
+ if (read !== null) {
3163
+ effects.push(configReadEffect(read));
3164
+ }
3165
+ },
3166
+ into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? import_extractor4.SKIP_CHILDREN : null
3167
+ });
3168
+ return effects;
3169
+ }
3170
+ function configReadEffect(read) {
3171
+ return {
3172
+ type: "interaction",
3173
+ binding: (0, import_behavioral_ir2.runtimeConfigBinding)({
3174
+ recognition: RUBY_ENV_RECOGNITION,
3175
+ deploymentTarget: "lambda",
3176
+ instanceName: "<unknown>"
3177
+ }),
3178
+ callee: `ENV["${read.name}"]`,
3179
+ interaction: {
3180
+ class: "config-read",
3181
+ name: read.name,
3182
+ defaulted: read.defaulted
3183
+ }
3184
+ };
3185
+ }
3186
+ function envReadAt(node) {
3187
+ if (node.type === "element_reference") {
3188
+ return elementRead(node);
3189
+ }
3190
+ if (node.type === "call") {
3191
+ return fetchRead(node);
3192
+ }
3193
+ return null;
3194
+ }
3195
+ function elementRead(node) {
3196
+ const object = field(node, "object");
3197
+ if (object === null || !isEnv(object) || isAssignedTo(node)) {
3198
+ return null;
3199
+ }
3200
+ const index = node.namedChildren.find(
3201
+ (child) => child !== null && child.id !== object.id
2670
3202
  );
2671
- const statusNames = pattern.statusCodeNames ?? {};
2672
- const outcomes = [];
2673
- for (const terminal of terminals) {
2674
- const declaration = declarationOf(terminal, byName, locals);
2675
- const reading = declaration === void 0 ? import_extractor3.absentReading : readingOfCall(terminal, declaration, statusNames);
2676
- for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
2677
- outcomes.push({
2678
- conditions: path7.map(conditionOf),
2679
- reading,
2680
- location: rangeOf(terminal)
2681
- });
2682
- }
3203
+ const name = index === void 0 ? null : stringLiteralValue(index);
3204
+ if (name === null) {
3205
+ return null;
2683
3206
  }
2684
- for (const path7 of enumerated.fallthrough) {
2685
- outcomes.push({
2686
- conditions: path7.map(conditionOf),
2687
- reading: import_extractor3.absentReading,
2688
- location: rangeOf(method),
2689
- fellThrough: true
2690
- });
3207
+ return { name, defaulted: isDefaultedAt(node) };
3208
+ }
3209
+ function fetchRead(node) {
3210
+ const receiver = field(node, "receiver");
3211
+ if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
3212
+ return null;
2691
3213
  }
2692
- if (outcomes.length === 0) {
3214
+ const { positional } = readCallArgs(field(node, "arguments"));
3215
+ const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
3216
+ if (name === null) {
2693
3217
  return null;
2694
3218
  }
2695
- return outcomes.map(
2696
- (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2697
- );
3219
+ const hasDefault = positional.length > 1 || field(node, "block") !== null;
3220
+ return { name, defaulted: hasDefault || isDefaultedAt(node) };
3221
+ }
3222
+ function isEnv(node) {
3223
+ if (node.type === "constant") {
3224
+ return node.text === "ENV";
3225
+ }
3226
+ return node.type === "scope_resolution" && field(node, "scope") === null && field(node, "name")?.text === "ENV";
3227
+ }
3228
+ function isAssignedTo(node) {
3229
+ const parent = node.parent;
3230
+ return parent !== null && (parent.type === "assignment" || parent.type === "operator_assignment") && field(parent, "left")?.id === node.id;
3231
+ }
3232
+ function isDefaultedAt(node) {
3233
+ let child = node;
3234
+ let parent = node.parent;
3235
+ while (parent !== null) {
3236
+ if (parent.type === "parenthesized_statements") {
3237
+ child = parent;
3238
+ parent = parent.parent;
3239
+ continue;
3240
+ }
3241
+ if (parent.type !== "binary" || field(parent, "operator")?.text !== "||") {
3242
+ return false;
3243
+ }
3244
+ if (field(parent, "left")?.id === child.id) {
3245
+ return true;
3246
+ }
3247
+ child = parent;
3248
+ parent = parent.parent;
3249
+ }
3250
+ return false;
2698
3251
  }
2699
3252
 
2700
3253
  // src/filters.ts
@@ -2882,172 +3435,6 @@ function namesUnder(node) {
2882
3435
  return names;
2883
3436
  }
2884
3437
 
2885
- // src/paths/effects.ts
2886
- var import_extractor4 = require("@suss/extractor");
2887
- var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
2888
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set([
2889
- "body_statement",
2890
- "then",
2891
- "else",
2892
- "do_block",
2893
- "block"
2894
- ]);
2895
- var LITERAL_ARGS = {
2896
- string: (node) => ({
2897
- kind: "string",
2898
- value: node.text.replace(/^["']|["']$/g, "")
2899
- }),
2900
- integer: (node) => {
2901
- const value = Number.parseInt(node.text, 10);
2902
- return Number.isNaN(value) ? null : { kind: "number", value };
2903
- },
2904
- float: (node) => {
2905
- const value = Number.parseFloat(node.text);
2906
- return Number.isNaN(value) ? null : { kind: "number", value };
2907
- },
2908
- true: () => ({ kind: "boolean", value: true }),
2909
- false: () => ({ kind: "boolean", value: false }),
2910
- simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
2911
- };
2912
- function children2(node) {
2913
- return node.namedChildren.filter((child) => child !== null);
2914
- }
2915
- function withoutChainLinks(calls) {
2916
- const isLink = /* @__PURE__ */ new Set();
2917
- for (const call of calls) {
2918
- const receiver = field(call, "receiver");
2919
- if (receiver !== null && receiver.type === "call") {
2920
- isLink.add(receiver.id);
2921
- }
2922
- }
2923
- return calls.filter((call) => !isLink.has(call.id));
2924
- }
2925
- function calleeMethodName(call) {
2926
- if (call.type === "identifier") {
2927
- return call.text;
2928
- }
2929
- const method = field(call, "method");
2930
- if (method !== null) {
2931
- return method.text;
2932
- }
2933
- return field(call, "receiver") !== null ? "call" : children2(call)[0]?.text;
2934
- }
2935
- function isPropertyRead2(node) {
2936
- if (field(node, "receiver") === null || field(node, "arguments") !== null) {
2937
- return false;
2938
- }
2939
- return calleeMethodName(node) !== "call";
2940
- }
2941
- function isRaise2(node) {
2942
- const method = field(node, "method") ?? children2(node)[0] ?? null;
2943
- return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
2944
- }
2945
- var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
2946
- function isInherited(node, inherited) {
2947
- const method = field(node, "method");
2948
- return field(node, "receiver") === null && method !== null && inherited.has(method.text);
2949
- }
2950
- function isCall(node, locals, inherited) {
2951
- if (node.type === "call") {
2952
- return !isPropertyRead2(node) && !isRaise2(node) && !isInherited(node, inherited);
2953
- }
2954
- return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
2955
- }
2956
- function collectCalls(node, locals, inherited, found) {
2957
- for (const child of children2(node)) {
2958
- if (OWN_BODY_TYPES.has(child.type)) {
2959
- continue;
2960
- }
2961
- if (isCall(child, locals, inherited)) {
2962
- found.push(child);
2963
- }
2964
- collectCalls(child, locals, inherited, found);
2965
- }
2966
- return found;
2967
- }
2968
- function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
2969
- const body = field(definitionNode, "body");
2970
- if (body === null) {
2971
- return [];
2972
- }
2973
- return collectCalls(body, localNamesIn(definitionNode), inherited, []);
2974
- }
2975
- function calleeText(call) {
2976
- const receiver = field(call, "receiver");
2977
- const method = calleeMethodName(call);
2978
- if (method === void 0) {
2979
- return call.text;
2980
- }
2981
- return receiver === null ? method : `${receiver.text}.${method}`;
2982
- }
2983
- function argOf(node) {
2984
- const literal = LITERAL_ARGS[node.type]?.(node);
2985
- if (literal !== null && literal !== void 0) {
2986
- return literal;
2987
- }
2988
- if (node.type === "call" && !isPropertyRead2(node)) {
2989
- return { kind: "call", callee: calleeText(node), args: argsOf(node) };
2990
- }
2991
- return { kind: "identifier", name: node.text };
2992
- }
2993
- function argsOf(call) {
2994
- const args = field(call, "arguments");
2995
- if (args === null) {
2996
- return [];
2997
- }
2998
- return children2(args).map(
2999
- (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
3000
- );
3001
- }
3002
- function enclosingStatement(call, body) {
3003
- let current = call;
3004
- while (current.parent !== null) {
3005
- if (BLOCK_TYPES2.has(current.parent.type) || current.parent.id === body.id) {
3006
- return current;
3007
- }
3008
- current = current.parent;
3009
- }
3010
- return current;
3011
- }
3012
- function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
3013
- const body = field(definitionNode, "body");
3014
- if (body === null) {
3015
- return [];
3016
- }
3017
- const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
3018
- if (calls.length === 0) {
3019
- return [];
3020
- }
3021
- const statementOf2 = /* @__PURE__ */ new Map();
3022
- const statements = [];
3023
- for (const call of calls) {
3024
- const statement = enclosingStatement(call, body);
3025
- statementOf2.set(call.id, statement);
3026
- statements.push(statement);
3027
- }
3028
- const lowered = lowerRubyBody(body, statements);
3029
- const enumerated = (0, import_extractor4.enumerateOrDegrade)(
3030
- {
3031
- statements: lowered.statements,
3032
- terminalsByStmt: lowered.terminalsByStmt
3033
- },
3034
- statements
3035
- );
3036
- return calls.map((call) => {
3037
- const statement = statementOf2.get(call.id);
3038
- const conditions = (0, import_extractor4.sharedGatingConditions)(
3039
- statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
3040
- );
3041
- return {
3042
- type: "invocation",
3043
- callee: calleeText(call),
3044
- args: argsOf(call),
3045
- async: false,
3046
- ...conditions.length > 0 ? { preconditions: conditions } : {}
3047
- };
3048
- });
3049
- }
3050
-
3051
3438
  // src/storage.ts
3052
3439
  var import_datalog2 = require("@suss/datalog");
3053
3440
  var import_ir_core = require("@suss/ir-core");
@@ -3266,6 +3653,16 @@ async function discoverUnits(root, options) {
3266
3653
  file: options.absoluteFile ?? options.filePath
3267
3654
  }));
3268
3655
  const units = [];
3656
+ for (const pack of options.packs) {
3657
+ for (const pattern of pack.clients ?? []) {
3658
+ units.push(
3659
+ ...clientCallUnits(root, pack, pattern, {
3660
+ filePath: options.filePath,
3661
+ ...options.facts === void 0 ? {} : { facts: options.facts }
3662
+ })
3663
+ );
3664
+ }
3665
+ }
3269
3666
  for (const info of classes) {
3270
3667
  const ownBlocks = fileBlocks.filter(
3271
3668
  (block) => block.info.qualifiedName === info.qualifiedName
@@ -3297,7 +3694,7 @@ function unitsFor(pattern, pack, info, ownBlocks, fileBlocks, options) {
3297
3694
  graphqlObjectFields: (p) => graphqlObjectFieldUnits(p, pack, info, ownBlocks, fileBlocks, options),
3298
3695
  controllerActions: (p) => controllerActionUnits(p, pack, info, ownBlocks, fileBlocks, options)
3299
3696
  };
3300
- return (0, import_behavioral_ir2.dispatchByType)(table, pattern);
3697
+ return (0, import_behavioral_ir3.dispatchByType)(table, pattern);
3301
3698
  }
3302
3699
  async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlocks, options) {
3303
3700
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
@@ -3422,7 +3819,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3422
3819
  exportName: actionName,
3423
3820
  exportPath: [controllerQualifiedName, actionName]
3424
3821
  },
3425
- boundaryBinding: route === null ? null : (0, import_behavioral_ir2.restBinding)({
3822
+ boundaryBinding: route === null ? null : (0, import_behavioral_ir3.restBinding)({
3426
3823
  transport: pack.protocol,
3427
3824
  method: route.method,
3428
3825
  path: route.path,
@@ -3540,7 +3937,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3540
3937
  const storage = bodyRead.storage;
3541
3938
  const extra = [
3542
3939
  ...envReadEffects(method),
3543
- ...storage === void 0 ? [] : storageEffects(callsUnder(method), storage)
3940
+ ...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
3544
3941
  ];
3545
3942
  return {
3546
3943
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -3549,7 +3946,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3549
3946
  ...extra.length > 0 ? { extraEffects: extra } : {}
3550
3947
  };
3551
3948
  }
3552
- function callsUnder(node, found = []) {
3949
+ function callsUnder2(node, found = []) {
3553
3950
  for (const child of node.namedChildren) {
3554
3951
  if (child === null) {
3555
3952
  continue;
@@ -3557,7 +3954,7 @@ function callsUnder(node, found = []) {
3557
3954
  if (child.type === "call") {
3558
3955
  found.push(child);
3559
3956
  }
3560
- callsUnder(child, found);
3957
+ callsUnder2(child, found);
3561
3958
  }
3562
3959
  return found;
3563
3960
  }
@@ -3584,7 +3981,7 @@ function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
3584
3981
  ),
3585
3982
  none: () => nothingThere
3586
3983
  };
3587
- return (0, import_behavioral_ir2.dispatchByType)(table, found);
3984
+ return (0, import_behavioral_ir3.dispatchByType)(table, found);
3588
3985
  }
3589
3986
  async function readFieldCall(stmt, scope, ctx, ancestry) {
3590
3987
  if (stmt.type !== "call" || field(stmt, "receiver") !== null) {
@@ -3853,7 +4250,7 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
3853
4250
  exportName: null,
3854
4251
  exportPath: null
3855
4252
  },
3856
- boundaryBinding: decl.namedOnTheWire ? (0, import_behavioral_ir2.graphqlResolverBinding)({
4253
+ boundaryBinding: decl.namedOnTheWire ? (0, import_behavioral_ir3.graphqlResolverBinding)({
3857
4254
  transport: pack.protocol,
3858
4255
  recognition: pack.name,
3859
4256
  typeName,
@@ -4051,7 +4448,7 @@ function parseWith(language, source) {
4051
4448
  // src/project.ts
4052
4449
  var import_node_fs3 = __toESM(require("fs"), 1);
4053
4450
  var import_node_path4 = __toESM(require("path"), 1);
4054
- var import_behavioral_ir4 = require("@suss/behavioral-ir");
4451
+ var import_behavioral_ir5 = require("@suss/behavioral-ir");
4055
4452
  var import_datalog4 = require("@suss/datalog");
4056
4453
  var import_extractor10 = require("@suss/extractor");
4057
4454
 
@@ -4106,7 +4503,7 @@ function requiredRelativePath(node) {
4106
4503
  }
4107
4504
 
4108
4505
  // src/reach/closure.ts
4109
- var import_behavioral_ir3 = require("@suss/behavioral-ir");
4506
+ var import_behavioral_ir4 = require("@suss/behavioral-ir");
4110
4507
  var import_datalog3 = require("@suss/datalog");
4111
4508
  var import_extractor8 = require("@suss/extractor");
4112
4509
 
@@ -4330,14 +4727,14 @@ async function reachedFunctions(seeds, options) {
4330
4727
  gapHandling: "permissive"
4331
4728
  });
4332
4729
  summary.confidence = { source: "inferred_static", level: "low" };
4333
- summary.gaps.push(...(stopsByKey.get(key) ?? []).map(import_behavioral_ir3.unfollowedCallGap));
4334
- (0, import_behavioral_ir3.placeCalls)(summary, targetsByKey.get(key));
4335
- (0, import_behavioral_ir3.placeArgTargets)(summary, argTargetsByKey.get(key));
4336
- (0, import_behavioral_ir3.placeCalleeParameters)(summary, parameterCallsByKey.get(key));
4730
+ summary.gaps.push(...(stopsByKey.get(key) ?? []).map(import_behavioral_ir4.unfollowedCallGap));
4731
+ (0, import_behavioral_ir4.placeCalls)(summary, targetsByKey.get(key));
4732
+ (0, import_behavioral_ir4.placeArgTargets)(summary, argTargetsByKey.get(key));
4733
+ (0, import_behavioral_ir4.placeCalleeParameters)(summary, parameterCallsByKey.get(key));
4337
4734
  summariesByKey.set(key, [summary]);
4338
4735
  summaries.push(summary);
4339
4736
  }
4340
- (0, import_behavioral_ir3.recordParameterGaps)(parameterCallsByKey, summariesByKey, passedPositions);
4737
+ (0, import_behavioral_ir4.recordParameterGaps)(parameterCallsByKey, summariesByKey, passedPositions);
4341
4738
  return {
4342
4739
  summaries,
4343
4740
  targetsByKey,
@@ -4447,7 +4844,7 @@ async function scanBody(source, ctx, options) {
4447
4844
  };
4448
4845
  const followed2 = [];
4449
4846
  const stops = [];
4450
- const placements = new import_behavioral_ir3.TargetPlacements();
4847
+ const placements = new import_behavioral_ir4.TargetPlacements();
4451
4848
  const parameterCalls = [];
4452
4849
  const passedPositions = /* @__PURE__ */ new Set();
4453
4850
  const seen = /* @__PURE__ */ new Set();
@@ -4500,7 +4897,7 @@ async function scanBody(source, ctx, options) {
4500
4897
  );
4501
4898
  if (outcome.kind === "stopped") {
4502
4899
  const stopKey = `${outcome.reason}:${callee}`;
4503
- if (!seen.has(stopKey) && (0, import_behavioral_ir3.worthRecording)(outcome.reason)) {
4900
+ if (!seen.has(stopKey) && (0, import_behavioral_ir4.worthRecording)(outcome.reason)) {
4504
4901
  seen.add(stopKey);
4505
4902
  stops.push({ callee, reason: outcome.reason });
4506
4903
  }
@@ -4544,7 +4941,7 @@ function libraryUnit(target, options) {
4544
4941
  exportName: exportPath[0] ?? name,
4545
4942
  exportPath
4546
4943
  },
4547
- boundaryBinding: (0, import_behavioral_ir3.functionCallBinding)({
4944
+ boundaryBinding: (0, import_behavioral_ir4.functionCallBinding)({
4548
4945
  transport: "in-process",
4549
4946
  recognition: "reachable"
4550
4947
  }),
@@ -4706,6 +5103,7 @@ async function extractRubyProject(options) {
4706
5103
  ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
4707
5104
  inheritedMethods,
4708
5105
  displayPathOf,
5106
+ facts: db,
4709
5107
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
4710
5108
  })
4711
5109
  );
@@ -4788,14 +5186,14 @@ async function extractRubyProject(options) {
4788
5186
  for (const [key, owners] of summariesBySeed) {
4789
5187
  for (const summary of owners) {
4790
5188
  summary.gaps.push(
4791
- ...(reached.stopsByKey.get(key) ?? []).map(import_behavioral_ir4.unfollowedCallGap)
5189
+ ...(reached.stopsByKey.get(key) ?? []).map(import_behavioral_ir5.unfollowedCallGap)
4792
5190
  );
4793
- (0, import_behavioral_ir4.placeCalls)(summary, reached.targetsByKey.get(key));
4794
- (0, import_behavioral_ir4.placeArgTargets)(summary, reached.argTargetsByKey.get(key));
4795
- (0, import_behavioral_ir4.placeCalleeParameters)(summary, reached.parameterCallsByKey.get(key));
5191
+ (0, import_behavioral_ir5.placeCalls)(summary, reached.targetsByKey.get(key));
5192
+ (0, import_behavioral_ir5.placeArgTargets)(summary, reached.argTargetsByKey.get(key));
5193
+ (0, import_behavioral_ir5.placeCalleeParameters)(summary, reached.parameterCallsByKey.get(key));
4796
5194
  }
4797
5195
  }
4798
- (0, import_behavioral_ir4.recordParameterGaps)(
5196
+ (0, import_behavioral_ir5.recordParameterGaps)(
4799
5197
  reached.parameterCallsByKey,
4800
5198
  summariesBySeed,
4801
5199
  reached.passedPositions
@@ -4806,15 +5204,15 @@ async function extractRubyProject(options) {
4806
5204
  const idRoot = options.projectRoot ?? options.workspaceRoot;
4807
5205
  for (const summary of summaries) {
4808
5206
  const absoluteFile = options.workspaceRoot === void 0 ? summary.location.file : import_node_path4.default.resolve(options.workspaceRoot, summary.location.file);
4809
- summary.identity.id = (0, import_behavioral_ir4.summaryIdFromParts)({
5207
+ summary.identity.id = (0, import_behavioral_ir5.summaryIdFromParts)({
4810
5208
  workspace: void 0,
4811
5209
  file: idRoot === void 0 ? absoluteFile : import_node_path4.default.relative(idRoot, absoluteFile),
4812
5210
  name: summary.identity.name,
4813
5211
  exportPath: summary.identity.exportPath
4814
5212
  });
4815
5213
  }
4816
- (0, import_behavioral_ir4.disambiguateSummaryIds)(summaries);
4817
- (0, import_behavioral_ir4.linkCallsToSummaries)(summaries);
5214
+ (0, import_behavioral_ir5.disambiguateSummaryIds)(summaries);
5215
+ (0, import_behavioral_ir5.linkCallsToSummaries)(summaries);
4818
5216
  const composed = timer.time(
4819
5217
  "summarize",
4820
5218
  () => (0, import_extractor10.composeWrappers)(summaries, {