@suss/adapter-ruby 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -531,120 +531,16 @@ function inheritedStatements(ancestry, blockConfigures) {
531
531
  import {
532
532
  dispatchByType,
533
533
  graphqlResolverBinding,
534
- restBinding
534
+ restBinding as restBinding2
535
535
  } from "@suss/behavioral-ir";
536
536
  import { absentReading as absentReading2, unreadableReading as unreadableReading2 } from "@suss/extractor";
537
537
 
538
- // src/envReads.ts
539
- import { runtimeConfigBinding } from "@suss/behavioral-ir";
540
- import { SKIP_CHILDREN as SKIP_CHILDREN2, walkDescendants as walkDescendants2 } from "@suss/extractor";
541
- var RUBY_ENV_RECOGNITION = "ruby-env";
542
- var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
543
- function envReadEffects(root) {
544
- const effects = [];
545
- walkDescendants2(root, null, {
546
- at: (node) => {
547
- const read = envReadAt(node);
548
- if (read !== null) {
549
- effects.push(configReadEffect(read));
550
- }
551
- },
552
- into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? SKIP_CHILDREN2 : null
553
- });
554
- return effects;
555
- }
556
- function configReadEffect(read) {
557
- return {
558
- type: "interaction",
559
- binding: runtimeConfigBinding({
560
- recognition: RUBY_ENV_RECOGNITION,
561
- deploymentTarget: "lambda",
562
- instanceName: "<unknown>"
563
- }),
564
- callee: `ENV["${read.name}"]`,
565
- interaction: {
566
- class: "config-read",
567
- name: read.name,
568
- defaulted: read.defaulted
569
- }
570
- };
571
- }
572
- function envReadAt(node) {
573
- if (node.type === "element_reference") {
574
- return elementRead(node);
575
- }
576
- if (node.type === "call") {
577
- return fetchRead(node);
578
- }
579
- return null;
580
- }
581
- function elementRead(node) {
582
- const object = field(node, "object");
583
- if (object === null || !isEnv(object) || isAssignedTo(node)) {
584
- return null;
585
- }
586
- const index = node.namedChildren.find(
587
- (child) => child !== null && child.id !== object.id
588
- );
589
- const name = index === void 0 ? null : stringLiteralValue(index);
590
- if (name === null) {
591
- return null;
592
- }
593
- return { name, defaulted: isDefaultedAt(node) };
594
- }
595
- function fetchRead(node) {
596
- const receiver = field(node, "receiver");
597
- if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
598
- return null;
599
- }
600
- const { positional } = readCallArgs(field(node, "arguments"));
601
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
602
- if (name === null) {
603
- return null;
604
- }
605
- const hasDefault = positional.length > 1 || field(node, "block") !== null;
606
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
607
- }
608
- function isEnv(node) {
609
- if (node.type === "constant") {
610
- return node.text === "ENV";
611
- }
612
- return node.type === "scope_resolution" && field(node, "scope") === null && field(node, "name")?.text === "ENV";
613
- }
614
- function isAssignedTo(node) {
615
- const parent = node.parent;
616
- return parent !== null && (parent.type === "assignment" || parent.type === "operator_assignment") && field(parent, "left")?.id === node.id;
617
- }
618
- function isDefaultedAt(node) {
619
- let child = node;
620
- let parent = node.parent;
621
- while (parent !== null) {
622
- if (parent.type === "parenthesized_statements") {
623
- child = parent;
624
- parent = parent.parent;
625
- continue;
626
- }
627
- if (parent.type !== "binary" || field(parent, "operator")?.text !== "||") {
628
- return false;
629
- }
630
- if (field(parent, "left")?.id === child.id) {
631
- return true;
632
- }
633
- child = parent;
634
- parent = parent.parent;
635
- }
636
- return false;
637
- }
538
+ // src/clientCalls.ts
539
+ import { namesNothing, restBinding } from "@suss/behavioral-ir";
540
+ import { pathOf } from "@suss/values";
638
541
 
639
- // src/responseStatus.ts
640
- import {
641
- absentReading,
642
- enumerateOrDegrade,
643
- guardsHoldOn,
644
- unreadableReading,
645
- writtenReading
646
- } from "@suss/extractor";
647
- import { constantOf, literalOf as literalOf2 } from "@suss/values";
542
+ // src/paths/effects.ts
543
+ import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
648
544
 
649
545
  // src/paths/bareCalls.ts
650
546
  var PARAMETER_LIST_TYPES = [
@@ -1020,9 +916,295 @@ function lowerRubyBody(body, terminals, responseCalls = []) {
1020
916
  };
1021
917
  }
1022
918
 
919
+ // src/paths/effects.ts
920
+ var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
921
+ var BLOCK_TYPES = /* @__PURE__ */ new Set([
922
+ "body_statement",
923
+ "then",
924
+ "else",
925
+ "do_block",
926
+ "block"
927
+ ]);
928
+ var LITERAL_ARGS = {
929
+ string: (node) => ({
930
+ kind: "string",
931
+ value: node.text.replace(/^["']|["']$/g, "")
932
+ }),
933
+ integer: (node) => {
934
+ const value = Number.parseInt(node.text, 10);
935
+ return Number.isNaN(value) ? null : { kind: "number", value };
936
+ },
937
+ float: (node) => {
938
+ const value = Number.parseFloat(node.text);
939
+ return Number.isNaN(value) ? null : { kind: "number", value };
940
+ },
941
+ true: () => ({ kind: "boolean", value: true }),
942
+ false: () => ({ kind: "boolean", value: false }),
943
+ simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
944
+ };
945
+ function children(node) {
946
+ return node.namedChildren.filter((child) => child !== null);
947
+ }
948
+ function withoutChainLinks(calls) {
949
+ const isLink = /* @__PURE__ */ new Set();
950
+ for (const call of calls) {
951
+ const receiver = field(call, "receiver");
952
+ if (receiver !== null && receiver.type === "call") {
953
+ isLink.add(receiver.id);
954
+ }
955
+ }
956
+ return calls.filter((call) => !isLink.has(call.id));
957
+ }
958
+ function calleeMethodName(call) {
959
+ if (call.type === "identifier") {
960
+ return call.text;
961
+ }
962
+ const method = field(call, "method");
963
+ if (method !== null) {
964
+ return method.text;
965
+ }
966
+ return field(call, "receiver") !== null ? "call" : children(call)[0]?.text;
967
+ }
968
+ function isPropertyRead(node) {
969
+ if (field(node, "receiver") === null || field(node, "arguments") !== null) {
970
+ return false;
971
+ }
972
+ return calleeMethodName(node) !== "call";
973
+ }
974
+ function isRaise2(node) {
975
+ const method = field(node, "method") ?? children(node)[0] ?? null;
976
+ return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
977
+ }
978
+ var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
979
+ function isInherited(node, inherited) {
980
+ const method = field(node, "method");
981
+ return field(node, "receiver") === null && method !== null && inherited.has(method.text);
982
+ }
983
+ function isCall(node, locals, inherited) {
984
+ if (node.type === "call") {
985
+ return !isPropertyRead(node) && !isRaise2(node) && !isInherited(node, inherited);
986
+ }
987
+ return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
988
+ }
989
+ function collectCalls(node, locals, inherited, found) {
990
+ for (const child of children(node)) {
991
+ if (OWN_BODY_TYPES.has(child.type)) {
992
+ continue;
993
+ }
994
+ if (isCall(child, locals, inherited)) {
995
+ found.push(child);
996
+ }
997
+ collectCalls(child, locals, inherited, found);
998
+ }
999
+ return found;
1000
+ }
1001
+ function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
1002
+ const body = field(definitionNode, "body");
1003
+ if (body === null) {
1004
+ return [];
1005
+ }
1006
+ return collectCalls(body, localNamesIn(definitionNode), inherited, []);
1007
+ }
1008
+ function calleeText(call) {
1009
+ const receiver = field(call, "receiver");
1010
+ const method = calleeMethodName(call);
1011
+ if (method === void 0) {
1012
+ return call.text;
1013
+ }
1014
+ return receiver === null ? method : `${receiver.text}.${method}`;
1015
+ }
1016
+ function argOf(node) {
1017
+ const literal = LITERAL_ARGS[node.type]?.(node);
1018
+ if (literal !== null && literal !== void 0) {
1019
+ return literal;
1020
+ }
1021
+ if (node.type === "call" && !isPropertyRead(node)) {
1022
+ return { kind: "call", callee: calleeText(node), args: argsOf(node) };
1023
+ }
1024
+ return { kind: "identifier", name: node.text };
1025
+ }
1026
+ function argsOf(call) {
1027
+ const args = field(call, "arguments");
1028
+ if (args === null) {
1029
+ return [];
1030
+ }
1031
+ return children(args).map(
1032
+ (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
1033
+ );
1034
+ }
1035
+ function enclosingStatement(call, body) {
1036
+ let current = call;
1037
+ while (current.parent !== null) {
1038
+ if (BLOCK_TYPES.has(current.parent.type) || current.parent.id === body.id) {
1039
+ return current;
1040
+ }
1041
+ current = current.parent;
1042
+ }
1043
+ return current;
1044
+ }
1045
+ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
1046
+ const body = field(definitionNode, "body");
1047
+ if (body === null) {
1048
+ return [];
1049
+ }
1050
+ const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
1051
+ if (calls.length === 0) {
1052
+ return [];
1053
+ }
1054
+ const statementOf2 = /* @__PURE__ */ new Map();
1055
+ const statements = [];
1056
+ for (const call of calls) {
1057
+ const statement = enclosingStatement(call, body);
1058
+ statementOf2.set(call.id, statement);
1059
+ statements.push(statement);
1060
+ }
1061
+ const lowered = lowerRubyBody(body, statements);
1062
+ const enumerated = enumerateOrDegrade(
1063
+ {
1064
+ statements: lowered.statements,
1065
+ terminalsByStmt: lowered.terminalsByStmt
1066
+ },
1067
+ statements
1068
+ );
1069
+ return calls.map((call) => {
1070
+ const statement = statementOf2.get(call.id);
1071
+ const conditions = sharedGatingConditions(
1072
+ statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
1073
+ );
1074
+ return {
1075
+ type: "invocation",
1076
+ callee: calleeText(call),
1077
+ args: argsOf(call),
1078
+ async: false,
1079
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
1080
+ };
1081
+ });
1082
+ }
1083
+
1084
+ // src/responseStatus.ts
1085
+ import {
1086
+ absentReading,
1087
+ enumerateOrDegrade as enumerateOrDegrade2,
1088
+ guardsHoldOn,
1089
+ unreadableReading,
1090
+ writtenReading
1091
+ } from "@suss/extractor";
1092
+ import { constantOf, literalOf as literalOf3 } from "@suss/values";
1093
+
1094
+ // src/paths/predicates.ts
1095
+ var OPERATORS = {
1096
+ "==": "eq",
1097
+ "!=": "neq",
1098
+ ">": "gt",
1099
+ ">=": "gte",
1100
+ "<": "lt",
1101
+ "<=": "lte"
1102
+ };
1103
+ var CONVERSIONS = /* @__PURE__ */ new Set(["to_i", "to_s", "to_sym"]);
1104
+ var opaqueOf = (node) => ({
1105
+ type: "opaque",
1106
+ sourceText: node.text,
1107
+ reason: "complexExpression"
1108
+ });
1109
+ function predicateOf(node) {
1110
+ if (node.type === "parenthesized_statements") {
1111
+ const inner = node.namedChildren[0];
1112
+ return inner == null ? opaqueOf(node) : predicateOf(inner);
1113
+ }
1114
+ if (node.type === "unary" && field(node, "operator")?.text === "!") {
1115
+ const operand2 = field(node, "operand") ?? node.namedChildren[0];
1116
+ return operand2 == null ? opaqueOf(node) : { type: "negation", operand: predicateOf(operand2) };
1117
+ }
1118
+ if (node.type === "binary") {
1119
+ return binaryOf(node);
1120
+ }
1121
+ const asked = calledMethod(node);
1122
+ if (asked === "nil?") {
1123
+ const receiver = field(node, "receiver");
1124
+ return receiver === null ? opaqueOf(node) : { type: "nullCheck", subject: valueRefOf(receiver), negated: false };
1125
+ }
1126
+ if (node.type === "identifier" || memberChain(node) !== null) {
1127
+ return {
1128
+ type: "truthinessCheck",
1129
+ subject: valueRefOf(node),
1130
+ negated: false
1131
+ };
1132
+ }
1133
+ return opaqueOf(node);
1134
+ }
1135
+ function binaryOf(node) {
1136
+ const operator = field(node, "operator")?.text ?? "";
1137
+ const left = field(node, "left");
1138
+ const right = field(node, "right");
1139
+ if (left === null || right === null) {
1140
+ return opaqueOf(node);
1141
+ }
1142
+ const op = OPERATORS[operator];
1143
+ if (op !== void 0) {
1144
+ return {
1145
+ type: "comparison",
1146
+ left: valueRefOf(left),
1147
+ op,
1148
+ right: valueRefOf(right)
1149
+ };
1150
+ }
1151
+ if (operator === "&&" || operator === "||" || operator === "and" || operator === "or") {
1152
+ return {
1153
+ type: "compound",
1154
+ op: operator === "&&" || operator === "and" ? "and" : "or",
1155
+ operands: [predicateOf(left), predicateOf(right)]
1156
+ };
1157
+ }
1158
+ return opaqueOf(node);
1159
+ }
1160
+ function valueRefOf(node) {
1161
+ const literal = literalOf(node);
1162
+ if (literal !== null) {
1163
+ return literal;
1164
+ }
1165
+ const chain = memberChain(node);
1166
+ if (chain !== null) {
1167
+ const [name, ...accessChain] = chain;
1168
+ if (name !== void 0 && accessChain.length > 0) {
1169
+ return { type: "dependency", name, accessChain };
1170
+ }
1171
+ }
1172
+ return { type: "unresolved", sourceText: node.text };
1173
+ }
1174
+ function literalOf(node) {
1175
+ if (node.type === "integer") {
1176
+ const value = Number.parseInt(node.text, 10);
1177
+ return Number.isNaN(value) ? null : { type: "literal", value };
1178
+ }
1179
+ const string = stringLiteralValue(node);
1180
+ if (string !== null) {
1181
+ return { type: "literal", value: string };
1182
+ }
1183
+ const boolean = booleanLiteralValue(node);
1184
+ return boolean === null ? null : { type: "literal", value: boolean };
1185
+ }
1186
+ function memberChain(node, tail = []) {
1187
+ if (node.type === "identifier") {
1188
+ return [node.text, ...tail];
1189
+ }
1190
+ const called = calledMethod(node);
1191
+ const receiver = field(node, "receiver");
1192
+ if (called === null || receiver === null || field(node, "arguments") !== null) {
1193
+ return null;
1194
+ }
1195
+ const rest = CONVERSIONS.has(called) && tail.length === 0 ? tail : [called, ...tail];
1196
+ return memberChain(receiver, rest);
1197
+ }
1198
+ function calledMethod(node) {
1199
+ if (node.type !== "call" || field(node, "block") !== null) {
1200
+ return null;
1201
+ }
1202
+ return field(node, "method")?.text ?? null;
1203
+ }
1204
+
1023
1205
  // src/values/evaluator.ts
1024
1206
  import { nodeOfKey } from "@suss/resolution";
1025
- import { Evaluator, force, literalOf } from "@suss/values";
1207
+ import { Evaluator, force, literalOf as literalOf2 } from "@suss/values";
1026
1208
 
1027
1209
  // src/facts/resolve.ts
1028
1210
  import {
@@ -1088,7 +1270,7 @@ function paramNameOf(param) {
1088
1270
  function declaredParamNames(method) {
1089
1271
  const params = field(method, "parameters");
1090
1272
  const declared = /* @__PURE__ */ new Set();
1091
- for (const param of params === null ? [] : children(params)) {
1273
+ for (const param of params === null ? [] : children2(params)) {
1092
1274
  const name = paramNameOf(param);
1093
1275
  if (name !== null) {
1094
1276
  declared.add(name.text);
@@ -1102,7 +1284,7 @@ function readKey(filePath, node, enclosing) {
1102
1284
  }
1103
1285
  const parent = node.parent;
1104
1286
  if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
1105
- return isPropertyRead(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1287
+ return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1106
1288
  }
1107
1289
  if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
1108
1290
  return `${nodeId(filePath, enclosing)}#${node.text}`;
@@ -1130,7 +1312,7 @@ var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1130
1312
  "parenthesized_statements"
1131
1313
  ]);
1132
1314
  var ARRAY_TYPES = /* @__PURE__ */ new Set(["array", "string_array", "symbol_array"]);
1133
- function children(node) {
1315
+ function children2(node) {
1134
1316
  return node.namedChildren.filter((child) => child !== null);
1135
1317
  }
1136
1318
  function add(emitter, relation, ...tuple) {
@@ -1158,8 +1340,8 @@ function pairKeyText(key) {
1158
1340
  }
1159
1341
  return null;
1160
1342
  }
1161
- function isPropertyRead(node) {
1162
- return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children(node).every(
1343
+ function isPropertyRead2(node) {
1344
+ return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children2(node).every(
1163
1345
  (child) => child.type !== "do_block" && child.type !== "block"
1164
1346
  );
1165
1347
  }
@@ -1198,7 +1380,7 @@ function emitCall(emitter, call) {
1198
1380
  }
1199
1381
  const args = field(call, "arguments");
1200
1382
  let position = 0;
1201
- for (const argument of args === null ? [] : children(args)) {
1383
+ for (const argument of args === null ? [] : children2(args)) {
1202
1384
  if (argument.type === "pair") {
1203
1385
  const key = field(argument, "key");
1204
1386
  const value = field(argument, "value");
@@ -1228,7 +1410,7 @@ function emitArray(emitter, array) {
1228
1410
  const objectKey = nodeId(emitter.filePath, array);
1229
1411
  add(emitter, "objectValue", objectKey);
1230
1412
  let position = 0;
1231
- for (const element of children(array)) {
1413
+ for (const element of children2(array)) {
1232
1414
  add(
1233
1415
  emitter,
1234
1416
  "holdsProperty",
@@ -1242,7 +1424,7 @@ function emitArray(emitter, array) {
1242
1424
  function emitHash(emitter, hash) {
1243
1425
  const objectKey = nodeId(emitter.filePath, hash);
1244
1426
  add(emitter, "objectValue", objectKey);
1245
- for (const pair of children(hash)) {
1427
+ for (const pair of children2(hash)) {
1246
1428
  if (pair.type !== "pair") {
1247
1429
  continue;
1248
1430
  }
@@ -1256,7 +1438,7 @@ function emitHash(emitter, hash) {
1256
1438
  }
1257
1439
  }
1258
1440
  function walkExpressions(node, visit) {
1259
- for (const child of children(node)) {
1441
+ for (const child of children2(node)) {
1260
1442
  if (OWN_BODY_TYPES.has(child.type)) {
1261
1443
  continue;
1262
1444
  }
@@ -1266,7 +1448,7 @@ function walkExpressions(node, visit) {
1266
1448
  }
1267
1449
  function emitExpressionFacts(emitter, node) {
1268
1450
  walkExpressions(node, (child) => {
1269
- if (isPropertyRead(child)) {
1451
+ if (isPropertyRead2(child)) {
1270
1452
  emitPropertyRead(emitter, child);
1271
1453
  } else if (child.type === "call") {
1272
1454
  emitCall(emitter, child);
@@ -1286,7 +1468,7 @@ function emitExpressionFacts(emitter, node) {
1286
1468
  });
1287
1469
  }
1288
1470
  function implicitReturn(body) {
1289
- const statements = children(body).filter(
1471
+ const statements = children2(body).filter(
1290
1472
  (child) => child.type !== "rescue" && child.type !== "ensure"
1291
1473
  );
1292
1474
  const last = statements[statements.length - 1];
@@ -1298,7 +1480,7 @@ function emitMethodFacts(emitter, method) {
1298
1480
  const params = field(method, "parameters");
1299
1481
  const declared = /* @__PURE__ */ new Set();
1300
1482
  let position = 0;
1301
- for (const param of params === null ? [] : children(params)) {
1483
+ for (const param of params === null ? [] : children2(params)) {
1302
1484
  const paramName = paramNameOf(param);
1303
1485
  if (paramName !== null) {
1304
1486
  declared.add(paramName.text);
@@ -1317,7 +1499,7 @@ function emitMethodFacts(emitter, method) {
1317
1499
  return funcKey;
1318
1500
  }
1319
1501
  const recordNested = (node) => {
1320
- for (const child of children(node)) {
1502
+ for (const child of children2(node)) {
1321
1503
  if (OWN_BODY_TYPES.has(child.type)) {
1322
1504
  add(emitter, "containsFn", funcKey, nodeId(emitter.filePath, child));
1323
1505
  continue;
@@ -1328,8 +1510,8 @@ function emitMethodFacts(emitter, method) {
1328
1510
  recordNested(body);
1329
1511
  walkExpressions(body, (child) => {
1330
1512
  if (child.type === "return") {
1331
- const first = children(child)[0];
1332
- const returned = first?.type === "argument_list" ? children(first)[0] : first;
1513
+ const first = children2(child)[0];
1514
+ const returned = first?.type === "argument_list" ? children2(first)[0] : first;
1333
1515
  if (returned !== void 0) {
1334
1516
  add(inside, "returnsValue", funcKey, valueKey(inside, returned));
1335
1517
  }
@@ -1373,13 +1555,13 @@ function emitClassFacts(emitter, cls) {
1373
1555
  const classKey = nodeId(emitter.filePath, cls);
1374
1556
  add(emitter, "objectValue", classKey);
1375
1557
  const superclass = field(cls, "superclass");
1376
- const base = superclass === null ? null : children(superclass)[0] ?? null;
1558
+ const base = superclass === null ? null : children2(superclass)[0] ?? null;
1377
1559
  if (base !== null) {
1378
1560
  add(emitter, "extends", classKey, valueKey(emitter, base));
1379
1561
  add(emitter, "extendsNamed", classKey, base.text);
1380
1562
  }
1381
1563
  const body = field(cls, "body");
1382
- for (const statement of body === null ? [] : children(body)) {
1564
+ for (const statement of body === null ? [] : children2(body)) {
1383
1565
  if (statement.type === "assignment") {
1384
1566
  const left = field(statement, "left");
1385
1567
  const right = field(statement, "right");
@@ -1421,7 +1603,7 @@ function emitValueFacts(db, filePath, root) {
1421
1603
  add(emitter, "exportsAs", filePath, name.text, key);
1422
1604
  };
1423
1605
  const walk = (node) => {
1424
- for (const child of children(node)) {
1606
+ for (const child of children2(node)) {
1425
1607
  if (NESTING_TYPES.has(child.type)) {
1426
1608
  declaresName(child, emitClassFacts(emitter, child));
1427
1609
  continue;
@@ -1452,7 +1634,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1452
1634
  "module",
1453
1635
  "singleton_class"
1454
1636
  ]);
1455
- var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
1637
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1456
1638
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1457
1639
  var NAME_TYPES = /* @__PURE__ */ new Set([
1458
1640
  "identifier",
@@ -1958,7 +2140,7 @@ function isTailExpression(node) {
1958
2140
  if (owner.type === "block" && owner.parent?.type === "lambda") {
1959
2141
  return true;
1960
2142
  }
1961
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES.has(owner.type)) {
2143
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
1962
2144
  return true;
1963
2145
  }
1964
2146
  if (owner.type === "when") {
@@ -1977,7 +2159,7 @@ function isRoot(node) {
1977
2159
  if (OWN_ROOT_TYPES.has(node.type)) {
1978
2160
  return true;
1979
2161
  }
1980
- return BLOCK_TYPES.has(node.type) && node.parent?.type === "call";
2162
+ return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
1981
2163
  }
1982
2164
  function isPathStatement(node, parent) {
1983
2165
  if (node.type === "elsif") {
@@ -2031,7 +2213,7 @@ function functionOf(node) {
2031
2213
  body: bodyOf(block)
2032
2214
  };
2033
2215
  }
2034
- if (BLOCK_TYPES.has(node.type)) {
2216
+ if (BLOCK_TYPES2.has(node.type)) {
2035
2217
  return {
2036
2218
  parameters: parametersOf(field(node, "parameters")),
2037
2219
  body: bodyOf(node)
@@ -2367,6 +2549,18 @@ var calleeRows = [
2367
2549
  kind: "callee",
2368
2550
  origin: { module: "Kernel", name: "String" },
2369
2551
  apply: ({ args }) => ({ result: concat([operand(args[0])]) })
2552
+ },
2553
+ // A URI is the string it was parsed from, as far as a reader of paths
2554
+ // is concerned, and both spellings are the standard library's own.
2555
+ {
2556
+ kind: "callee",
2557
+ origin: { module: "Kernel", name: "URI" },
2558
+ apply: ({ args }) => ({ result: concat([operand(args[0])]) })
2559
+ },
2560
+ {
2561
+ kind: "callee",
2562
+ origin: { module: "URI", name: "parse" },
2563
+ apply: ({ args }) => ({ result: concat([operand(args[0])]) })
2370
2564
  }
2371
2565
  ];
2372
2566
  var rubyRows = [
@@ -2392,7 +2586,7 @@ function evaluatedValue(node, db) {
2392
2586
  return force(evaluatorFor(node, db).evaluate(node));
2393
2587
  }
2394
2588
  function stringValueOf(node, db) {
2395
- return literalOf(evaluatedValue(node, db));
2589
+ return literalOf2(evaluatedValue(node, db));
2396
2590
  }
2397
2591
  function methodDefinitionsIn(file, root) {
2398
2592
  const found = /* @__PURE__ */ new Map();
@@ -2485,7 +2679,7 @@ function statusNumberOf(node, names) {
2485
2679
  if (typeof constant2 === "number") {
2486
2680
  return constant2;
2487
2681
  }
2488
- const name = literalOf2(value);
2682
+ const name = literalOf3(value);
2489
2683
  return name === null ? null : names[name] ?? null;
2490
2684
  }
2491
2685
  function declarationsByName(declarations) {
@@ -2554,7 +2748,7 @@ function effectsReaching(effects, conditions) {
2554
2748
  function conditionOf(condition) {
2555
2749
  return {
2556
2750
  sourceText: condition.sourceText,
2557
- structured: null,
2751
+ structured: condition.expression === null ? null : predicateOf(condition.expression),
2558
2752
  polarity: condition.polarity,
2559
2753
  source: condition.source
2560
2754
  };
@@ -2608,55 +2802,414 @@ function handsOnBranch(outcome, effects, extraEffects) {
2608
2802
  isDefault: outcome.conditions.length === 0
2609
2803
  };
2610
2804
  }
2611
- function responseBranches(method, pattern, effects, extraEffects, options = {}) {
2612
- const declarations = pattern.responseStatusCalls ?? [];
2613
- const body = field(method, "body");
2614
- if (declarations.length === 0 || body === null) {
2805
+ function returnPathBranches(method, effects) {
2806
+ const body = field(method, "body");
2807
+ if (body === null) {
2808
+ return null;
2809
+ }
2810
+ const returns = collectReturns(body, []);
2811
+ const lowered = lowerRubyBody(body, returns, []);
2812
+ const enumerated = enumerateOrDegrade2(
2813
+ {
2814
+ statements: lowered.statements,
2815
+ terminalsByStmt: lowered.terminalsByStmt
2816
+ },
2817
+ returns
2818
+ );
2819
+ const branches = [];
2820
+ const push = (paths, at) => {
2821
+ for (const path7 of paths) {
2822
+ const conditions = path7.map(conditionOf);
2823
+ branches.push({
2824
+ conditions,
2825
+ terminal: {
2826
+ kind: "return",
2827
+ statusCode: null,
2828
+ body: null,
2829
+ exceptionType: null,
2830
+ message: null,
2831
+ component: null,
2832
+ renderTree: null,
2833
+ delegateTarget: null,
2834
+ emitEvent: null,
2835
+ location: at
2836
+ },
2837
+ effects: effectsReaching(effects, conditions),
2838
+ location: at,
2839
+ isDefault: conditions.length === 0
2840
+ });
2841
+ }
2842
+ };
2843
+ for (const statement of returns) {
2844
+ push(enumerated.byTerminal.get(statement) ?? [], rangeOf(statement));
2845
+ }
2846
+ push(enumerated.fallthrough, rangeOf(method));
2847
+ return branches.length === 0 ? null : branches;
2848
+ }
2849
+ function responseBranches(method, pattern, effects, extraEffects, options = {}) {
2850
+ const declarations = pattern.responseStatusCalls ?? [];
2851
+ const body = field(method, "body");
2852
+ if (declarations.length === 0 || body === null) {
2853
+ return null;
2854
+ }
2855
+ const byName = declarationsByName(declarations);
2856
+ const locals = localNamesIn(method);
2857
+ const responses = collectResponseCalls(body, byName, locals, []);
2858
+ const returns = collectReturns(body, []);
2859
+ const lowered = lowerRubyBody(body, returns, responses);
2860
+ const bareReturns = returns.filter(
2861
+ (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
2862
+ );
2863
+ const terminals = [...responses, ...bareReturns];
2864
+ const enumerated = enumerateOrDegrade2(
2865
+ {
2866
+ statements: lowered.statements,
2867
+ terminalsByStmt: lowered.terminalsByStmt
2868
+ },
2869
+ terminals
2870
+ );
2871
+ const statusNames = pattern.statusCodeNames ?? {};
2872
+ const outcomes = [];
2873
+ for (const terminal of terminals) {
2874
+ const declaration = declarationOf(terminal, byName, locals);
2875
+ const reading = declaration === void 0 ? absentReading : readingOfCall(terminal, declaration, statusNames);
2876
+ for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
2877
+ outcomes.push({
2878
+ conditions: path7.map(conditionOf),
2879
+ reading,
2880
+ location: rangeOf(terminal)
2881
+ });
2882
+ }
2883
+ }
2884
+ for (const path7 of enumerated.fallthrough) {
2885
+ outcomes.push({
2886
+ conditions: path7.map(conditionOf),
2887
+ reading: absentReading,
2888
+ location: rangeOf(method),
2889
+ fellThrough: true
2890
+ });
2891
+ }
2892
+ if (outcomes.length === 0) {
2893
+ return null;
2894
+ }
2895
+ return outcomes.map(
2896
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2897
+ );
2898
+ }
2899
+
2900
+ // src/clientCalls.ts
2901
+ function clientCallUnits(root, pack, pattern, options) {
2902
+ const units = [];
2903
+ for (const method of methodDefinitions(root)) {
2904
+ const name = field(method, "name")?.text;
2905
+ if (name === void 0) {
2906
+ continue;
2907
+ }
2908
+ for (const call of callsUnder(method)) {
2909
+ const request = requestCall(call, method, pattern, options);
2910
+ if (request === null) {
2911
+ continue;
2912
+ }
2913
+ units.push(clientUnit(method, name, request, pattern, pack, options));
2914
+ }
2915
+ }
2916
+ return units;
2917
+ }
2918
+ function methodDefinitions(node, found = []) {
2919
+ for (const child of node.namedChildren) {
2920
+ if (child === null) {
2921
+ continue;
2922
+ }
2923
+ if (child.type === "method" || child.type === "singleton_method") {
2924
+ found.push(child);
2925
+ }
2926
+ methodDefinitions(child, found);
2927
+ }
2928
+ return found;
2929
+ }
2930
+ function callsUnder(node, found = []) {
2931
+ for (const child of node.namedChildren) {
2932
+ if (child === null) {
2933
+ continue;
2934
+ }
2935
+ if (child.type === "call") {
2936
+ found.push(child);
2937
+ }
2938
+ callsUnder(child, found);
2939
+ }
2940
+ return found;
2941
+ }
2942
+ function requestCall(call, method, pattern, options) {
2943
+ const receiver = field(call, "receiver");
2944
+ const called = field(call, "method")?.text;
2945
+ if (receiver === null || called === void 0) {
2946
+ return null;
2947
+ }
2948
+ const prefix = receiverPrefix(receiver, method, pattern, options);
2949
+ if (prefix === null) {
2950
+ return null;
2951
+ }
2952
+ const args = readCallArgs(field(call, "arguments"));
2953
+ const verb = pattern.verbMethodNames[called];
2954
+ if (verb !== void 0) {
2955
+ const path8 = urlIn(args, pattern, options);
2956
+ return path8 === null ? null : { method: verb, path: prefix + path8 };
2957
+ }
2958
+ const sent = pattern.requestObject;
2959
+ if (sent === void 0 || sent.attribute !== called) {
2960
+ return null;
2961
+ }
2962
+ const built = requestBuilt(args.positional[0], method, sent);
2963
+ if (built === null) {
2964
+ return null;
2965
+ }
2966
+ const path7 = pathAt(built.url, options);
2967
+ return path7 === null ? null : { method: built.method, path: prefix + path7 };
2968
+ }
2969
+ function requestBuilt(argument, method, sent) {
2970
+ const written = argument === void 0 ? null : assignedCall(argument, method) ?? argument;
2971
+ if (written === null || written.type !== "call") {
2972
+ return null;
2973
+ }
2974
+ const requestClass = field(written, "receiver")?.text;
2975
+ const verb = requestClass === void 0 ? void 0 : sent.constructors[requestClass];
2976
+ if (verb === void 0) {
2977
+ return null;
2978
+ }
2979
+ const args = readCallArgs(field(written, "arguments"));
2980
+ return { method: verb, url: args.positional[sent.urlPosition] };
2981
+ }
2982
+ function assignedCall(node, method) {
2983
+ if (node.type !== "identifier") {
2984
+ return null;
2985
+ }
2986
+ const value = assignedValue(node.text, method);
2987
+ return value !== null && value.type === "call" ? value : null;
2988
+ }
2989
+ function receiverPrefix(receiver, method, pattern, options) {
2990
+ if (namesConstant(receiver, pattern.constantName)) {
2991
+ return "";
2992
+ }
2993
+ if (receiver.type !== "identifier") {
2994
+ return null;
2995
+ }
2996
+ const built = builtBy(receiver.text, method, pattern);
2997
+ if (built === null) {
2998
+ return null;
2999
+ }
3000
+ const keyword = pattern.builderUrlKeyword;
3001
+ if (keyword === void 0) {
3002
+ return "";
3003
+ }
3004
+ const base = readCallArgs(field(built, "arguments")).keyword[keyword];
3005
+ const path7 = base === void 0 ? null : pathOf(evaluatedValue(base, options.facts));
3006
+ return path7 === void 0 || path7 === null ? "" : trimmed(path7);
3007
+ }
3008
+ function trimmed(path7) {
3009
+ return path7 === "/" ? "" : path7.replace(/\/+$/, "");
3010
+ }
3011
+ function namesConstant(receiver, constantName) {
3012
+ return (receiver.type === "constant" || receiver.type === "scope_resolution") && receiver.text === constantName;
3013
+ }
3014
+ function builtBy(name, method, pattern) {
3015
+ const builders = pattern.receiverBuilders ?? [];
3016
+ const value = builders.length === 0 ? null : assignedValue(name, method);
3017
+ if (value === null || value.type !== "call" || !builders.includes(field(value, "method")?.text ?? "")) {
3018
+ return null;
3019
+ }
3020
+ const receiver = field(value, "receiver");
3021
+ return receiver !== null && namesConstant(receiver, pattern.constantName) ? value : null;
3022
+ }
3023
+ function assignedValue(name, method) {
3024
+ const body = field(method, "body");
3025
+ if (body === null) {
3026
+ return null;
3027
+ }
3028
+ for (const statement of runStatements(body)) {
3029
+ if (statement.type !== "assignment") {
3030
+ continue;
3031
+ }
3032
+ if (field(statement, "left")?.text === name) {
3033
+ return field(statement, "right");
3034
+ }
3035
+ }
3036
+ return null;
3037
+ }
3038
+ function urlIn(args, pattern, options) {
3039
+ const keyword = pattern.url.keyword;
3040
+ const written = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
3041
+ return pathAt(written, options);
3042
+ }
3043
+ function pathAt(written, options) {
3044
+ if (written === void 0) {
3045
+ return null;
3046
+ }
3047
+ const path7 = pathOf(evaluatedValue(written, options.facts));
3048
+ return path7 === void 0 || namesNothing(path7) ? null : path7;
3049
+ }
3050
+ function clientUnit(method, name, request, pattern, pack, options) {
3051
+ const range = rangeOf(method);
3052
+ return {
3053
+ identity: {
3054
+ name,
3055
+ nameKind: "binding",
3056
+ kind: "client",
3057
+ file: options.filePath,
3058
+ range,
3059
+ span: spanOf(method),
3060
+ exportName: name,
3061
+ exportPath: [name]
3062
+ },
3063
+ boundaryBinding: restBinding({
3064
+ transport: pack.protocol,
3065
+ method: request.method,
3066
+ path: request.path,
3067
+ recognition: pack.name
3068
+ }),
3069
+ parameters: [],
3070
+ branches: callerBranches(method, range),
3071
+ ...responseAccessors(pattern),
3072
+ bodyContent: "statements",
3073
+ dependencyCalls: [],
3074
+ declaredContract: null
3075
+ };
3076
+ }
3077
+ function callerBranches(method, range) {
3078
+ const effects = invocationEffects(method);
3079
+ return returnPathBranches(method, effects) ?? [returnBranch(range, effects)];
3080
+ }
3081
+ function responseAccessors(pattern) {
3082
+ const response = pattern.response;
3083
+ if (response === void 0) {
3084
+ return {};
3085
+ }
3086
+ return {
3087
+ ...response.body === void 0 ? {} : { bodyAccessors: response.body },
3088
+ ...response.statusCode === void 0 ? {} : { statusAccessors: response.statusCode },
3089
+ ...response.success === void 0 ? {} : { successAccessors: response.success },
3090
+ ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery }
3091
+ };
3092
+ }
3093
+ function returnBranch(range, effects) {
3094
+ return {
3095
+ conditions: [],
3096
+ terminal: {
3097
+ kind: "return",
3098
+ statusCode: null,
3099
+ body: null,
3100
+ exceptionType: null,
3101
+ message: null,
3102
+ component: null,
3103
+ renderTree: null,
3104
+ delegateTarget: null,
3105
+ emitEvent: null,
3106
+ location: range
3107
+ },
3108
+ effects,
3109
+ location: range,
3110
+ isDefault: true
3111
+ };
3112
+ }
3113
+
3114
+ // src/envReads.ts
3115
+ import { runtimeConfigBinding } from "@suss/behavioral-ir";
3116
+ import { SKIP_CHILDREN as SKIP_CHILDREN2, walkDescendants as walkDescendants2 } from "@suss/extractor";
3117
+ var RUBY_ENV_RECOGNITION = "ruby-env";
3118
+ var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
3119
+ function envReadEffects(root) {
3120
+ const effects = [];
3121
+ walkDescendants2(root, null, {
3122
+ at: (node) => {
3123
+ const read = envReadAt(node);
3124
+ if (read !== null) {
3125
+ effects.push(configReadEffect(read));
3126
+ }
3127
+ },
3128
+ into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? SKIP_CHILDREN2 : null
3129
+ });
3130
+ return effects;
3131
+ }
3132
+ function configReadEffect(read) {
3133
+ return {
3134
+ type: "interaction",
3135
+ binding: runtimeConfigBinding({
3136
+ recognition: RUBY_ENV_RECOGNITION,
3137
+ deploymentTarget: "lambda",
3138
+ instanceName: "<unknown>"
3139
+ }),
3140
+ callee: `ENV["${read.name}"]`,
3141
+ interaction: {
3142
+ class: "config-read",
3143
+ name: read.name,
3144
+ defaulted: read.defaulted
3145
+ }
3146
+ };
3147
+ }
3148
+ function envReadAt(node) {
3149
+ if (node.type === "element_reference") {
3150
+ return elementRead(node);
3151
+ }
3152
+ if (node.type === "call") {
3153
+ return fetchRead(node);
3154
+ }
3155
+ return null;
3156
+ }
3157
+ function elementRead(node) {
3158
+ const object = field(node, "object");
3159
+ if (object === null || !isEnv(object) || isAssignedTo(node)) {
2615
3160
  return null;
2616
3161
  }
2617
- const byName = declarationsByName(declarations);
2618
- const locals = localNamesIn(method);
2619
- const responses = collectResponseCalls(body, byName, locals, []);
2620
- const returns = collectReturns(body, []);
2621
- const lowered = lowerRubyBody(body, returns, responses);
2622
- const bareReturns = returns.filter(
2623
- (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
2624
- );
2625
- const terminals = [...responses, ...bareReturns];
2626
- const enumerated = enumerateOrDegrade(
2627
- {
2628
- statements: lowered.statements,
2629
- terminalsByStmt: lowered.terminalsByStmt
2630
- },
2631
- terminals
3162
+ const index = node.namedChildren.find(
3163
+ (child) => child !== null && child.id !== object.id
2632
3164
  );
2633
- const statusNames = pattern.statusCodeNames ?? {};
2634
- const outcomes = [];
2635
- for (const terminal of terminals) {
2636
- const declaration = declarationOf(terminal, byName, locals);
2637
- const reading = declaration === void 0 ? absentReading : readingOfCall(terminal, declaration, statusNames);
2638
- for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
2639
- outcomes.push({
2640
- conditions: path7.map(conditionOf),
2641
- reading,
2642
- location: rangeOf(terminal)
2643
- });
2644
- }
3165
+ const name = index === void 0 ? null : stringLiteralValue(index);
3166
+ if (name === null) {
3167
+ return null;
2645
3168
  }
2646
- for (const path7 of enumerated.fallthrough) {
2647
- outcomes.push({
2648
- conditions: path7.map(conditionOf),
2649
- reading: absentReading,
2650
- location: rangeOf(method),
2651
- fellThrough: true
2652
- });
3169
+ return { name, defaulted: isDefaultedAt(node) };
3170
+ }
3171
+ function fetchRead(node) {
3172
+ const receiver = field(node, "receiver");
3173
+ if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
3174
+ return null;
2653
3175
  }
2654
- if (outcomes.length === 0) {
3176
+ const { positional } = readCallArgs(field(node, "arguments"));
3177
+ const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
3178
+ if (name === null) {
2655
3179
  return null;
2656
3180
  }
2657
- return outcomes.map(
2658
- (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2659
- );
3181
+ const hasDefault = positional.length > 1 || field(node, "block") !== null;
3182
+ return { name, defaulted: hasDefault || isDefaultedAt(node) };
3183
+ }
3184
+ function isEnv(node) {
3185
+ if (node.type === "constant") {
3186
+ return node.text === "ENV";
3187
+ }
3188
+ return node.type === "scope_resolution" && field(node, "scope") === null && field(node, "name")?.text === "ENV";
3189
+ }
3190
+ function isAssignedTo(node) {
3191
+ const parent = node.parent;
3192
+ return parent !== null && (parent.type === "assignment" || parent.type === "operator_assignment") && field(parent, "left")?.id === node.id;
3193
+ }
3194
+ function isDefaultedAt(node) {
3195
+ let child = node;
3196
+ let parent = node.parent;
3197
+ while (parent !== null) {
3198
+ if (parent.type === "parenthesized_statements") {
3199
+ child = parent;
3200
+ parent = parent.parent;
3201
+ continue;
3202
+ }
3203
+ if (parent.type !== "binary" || field(parent, "operator")?.text !== "||") {
3204
+ return false;
3205
+ }
3206
+ if (field(parent, "left")?.id === child.id) {
3207
+ return true;
3208
+ }
3209
+ child = parent;
3210
+ parent = parent.parent;
3211
+ }
3212
+ return false;
2660
3213
  }
2661
3214
 
2662
3215
  // src/filters.ts
@@ -2844,172 +3397,6 @@ function namesUnder(node) {
2844
3397
  return names;
2845
3398
  }
2846
3399
 
2847
- // src/paths/effects.ts
2848
- import { enumerateOrDegrade as enumerateOrDegrade2, sharedGatingConditions } from "@suss/extractor";
2849
- var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
2850
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set([
2851
- "body_statement",
2852
- "then",
2853
- "else",
2854
- "do_block",
2855
- "block"
2856
- ]);
2857
- var LITERAL_ARGS = {
2858
- string: (node) => ({
2859
- kind: "string",
2860
- value: node.text.replace(/^["']|["']$/g, "")
2861
- }),
2862
- integer: (node) => {
2863
- const value = Number.parseInt(node.text, 10);
2864
- return Number.isNaN(value) ? null : { kind: "number", value };
2865
- },
2866
- float: (node) => {
2867
- const value = Number.parseFloat(node.text);
2868
- return Number.isNaN(value) ? null : { kind: "number", value };
2869
- },
2870
- true: () => ({ kind: "boolean", value: true }),
2871
- false: () => ({ kind: "boolean", value: false }),
2872
- simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
2873
- };
2874
- function children2(node) {
2875
- return node.namedChildren.filter((child) => child !== null);
2876
- }
2877
- function withoutChainLinks(calls) {
2878
- const isLink = /* @__PURE__ */ new Set();
2879
- for (const call of calls) {
2880
- const receiver = field(call, "receiver");
2881
- if (receiver !== null && receiver.type === "call") {
2882
- isLink.add(receiver.id);
2883
- }
2884
- }
2885
- return calls.filter((call) => !isLink.has(call.id));
2886
- }
2887
- function calleeMethodName(call) {
2888
- if (call.type === "identifier") {
2889
- return call.text;
2890
- }
2891
- const method = field(call, "method");
2892
- if (method !== null) {
2893
- return method.text;
2894
- }
2895
- return field(call, "receiver") !== null ? "call" : children2(call)[0]?.text;
2896
- }
2897
- function isPropertyRead2(node) {
2898
- if (field(node, "receiver") === null || field(node, "arguments") !== null) {
2899
- return false;
2900
- }
2901
- return calleeMethodName(node) !== "call";
2902
- }
2903
- function isRaise2(node) {
2904
- const method = field(node, "method") ?? children2(node)[0] ?? null;
2905
- return field(node, "receiver") === null && method !== null && RAISE_NAMES2.has(method.text);
2906
- }
2907
- var NO_INHERITED_METHODS = /* @__PURE__ */ new Set();
2908
- function isInherited(node, inherited) {
2909
- const method = field(node, "method");
2910
- return field(node, "receiver") === null && method !== null && inherited.has(method.text);
2911
- }
2912
- function isCall(node, locals, inherited) {
2913
- if (node.type === "call") {
2914
- return !isPropertyRead2(node) && !isRaise2(node) && !isInherited(node, inherited);
2915
- }
2916
- return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
2917
- }
2918
- function collectCalls(node, locals, inherited, found) {
2919
- for (const child of children2(node)) {
2920
- if (OWN_BODY_TYPES.has(child.type)) {
2921
- continue;
2922
- }
2923
- if (isCall(child, locals, inherited)) {
2924
- found.push(child);
2925
- }
2926
- collectCalls(child, locals, inherited, found);
2927
- }
2928
- return found;
2929
- }
2930
- function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
2931
- const body = field(definitionNode, "body");
2932
- if (body === null) {
2933
- return [];
2934
- }
2935
- return collectCalls(body, localNamesIn(definitionNode), inherited, []);
2936
- }
2937
- function calleeText(call) {
2938
- const receiver = field(call, "receiver");
2939
- const method = calleeMethodName(call);
2940
- if (method === void 0) {
2941
- return call.text;
2942
- }
2943
- return receiver === null ? method : `${receiver.text}.${method}`;
2944
- }
2945
- function argOf(node) {
2946
- const literal = LITERAL_ARGS[node.type]?.(node);
2947
- if (literal !== null && literal !== void 0) {
2948
- return literal;
2949
- }
2950
- if (node.type === "call" && !isPropertyRead2(node)) {
2951
- return { kind: "call", callee: calleeText(node), args: argsOf(node) };
2952
- }
2953
- return { kind: "identifier", name: node.text };
2954
- }
2955
- function argsOf(call) {
2956
- const args = field(call, "arguments");
2957
- if (args === null) {
2958
- return [];
2959
- }
2960
- return children2(args).map(
2961
- (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
2962
- );
2963
- }
2964
- function enclosingStatement(call, body) {
2965
- let current = call;
2966
- while (current.parent !== null) {
2967
- if (BLOCK_TYPES2.has(current.parent.type) || current.parent.id === body.id) {
2968
- return current;
2969
- }
2970
- current = current.parent;
2971
- }
2972
- return current;
2973
- }
2974
- function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
2975
- const body = field(definitionNode, "body");
2976
- if (body === null) {
2977
- return [];
2978
- }
2979
- const calls = withoutChainLinks(bodyCalls(definitionNode, inherited));
2980
- if (calls.length === 0) {
2981
- return [];
2982
- }
2983
- const statementOf2 = /* @__PURE__ */ new Map();
2984
- const statements = [];
2985
- for (const call of calls) {
2986
- const statement = enclosingStatement(call, body);
2987
- statementOf2.set(call.id, statement);
2988
- statements.push(statement);
2989
- }
2990
- const lowered = lowerRubyBody(body, statements);
2991
- const enumerated = enumerateOrDegrade2(
2992
- {
2993
- statements: lowered.statements,
2994
- terminalsByStmt: lowered.terminalsByStmt
2995
- },
2996
- statements
2997
- );
2998
- return calls.map((call) => {
2999
- const statement = statementOf2.get(call.id);
3000
- const conditions = sharedGatingConditions(
3001
- statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
3002
- );
3003
- return {
3004
- type: "invocation",
3005
- callee: calleeText(call),
3006
- args: argsOf(call),
3007
- async: false,
3008
- ...conditions.length > 0 ? { preconditions: conditions } : {}
3009
- };
3010
- });
3011
- }
3012
-
3013
3400
  // src/storage.ts
3014
3401
  import { deriveOnDemand as deriveOnDemand2, evaluate as evaluate2 } from "@suss/datalog";
3015
3402
  import { storageBinding } from "@suss/ir-core";
@@ -3232,6 +3619,16 @@ async function discoverUnits(root, options) {
3232
3619
  file: options.absoluteFile ?? options.filePath
3233
3620
  }));
3234
3621
  const units = [];
3622
+ for (const pack of options.packs) {
3623
+ for (const pattern of pack.clients ?? []) {
3624
+ units.push(
3625
+ ...clientCallUnits(root, pack, pattern, {
3626
+ filePath: options.filePath,
3627
+ ...options.facts === void 0 ? {} : { facts: options.facts }
3628
+ })
3629
+ );
3630
+ }
3631
+ }
3235
3632
  for (const info of classes) {
3236
3633
  const ownBlocks = fileBlocks.filter(
3237
3634
  (block) => block.info.qualifiedName === info.qualifiedName
@@ -3388,7 +3785,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3388
3785
  exportName: actionName,
3389
3786
  exportPath: [controllerQualifiedName, actionName]
3390
3787
  },
3391
- boundaryBinding: route === null ? null : restBinding({
3788
+ boundaryBinding: route === null ? null : restBinding2({
3392
3789
  transport: pack.protocol,
3393
3790
  method: route.method,
3394
3791
  path: route.path,
@@ -3506,7 +3903,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3506
3903
  const storage = bodyRead.storage;
3507
3904
  const extra = [
3508
3905
  ...envReadEffects(method),
3509
- ...storage === void 0 ? [] : storageEffects(callsUnder(method), storage)
3906
+ ...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
3510
3907
  ];
3511
3908
  return {
3512
3909
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -3515,7 +3912,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3515
3912
  ...extra.length > 0 ? { extraEffects: extra } : {}
3516
3913
  };
3517
3914
  }
3518
- function callsUnder(node, found = []) {
3915
+ function callsUnder2(node, found = []) {
3519
3916
  for (const child of node.namedChildren) {
3520
3917
  if (child === null) {
3521
3918
  continue;
@@ -3523,7 +3920,7 @@ function callsUnder(node, found = []) {
3523
3920
  if (child.type === "call") {
3524
3921
  found.push(child);
3525
3922
  }
3526
- callsUnder(child, found);
3923
+ callsUnder2(child, found);
3527
3924
  }
3528
3925
  return found;
3529
3926
  }
@@ -4703,6 +5100,7 @@ async function extractRubyProject(options) {
4703
5100
  ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
4704
5101
  inheritedMethods,
4705
5102
  displayPathOf,
5103
+ facts: db,
4706
5104
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
4707
5105
  })
4708
5106
  );