@suss/adapter-ruby 0.22.4 → 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.cjs CHANGED
@@ -288,9 +288,21 @@ var PATH_CONVENTIONS = {
288
288
  railsUnderscore: underscoreConstantPath
289
289
  };
290
290
  function resolveConstantFile(root, qualifiedName, convention) {
291
- const relative = PATH_CONVENTIONS[convention](qualifiedName);
292
- const file = import_node_path.default.join(root, `${relative}.rb`);
293
- return import_node_fs.default.existsSync(file) ? file : null;
291
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
292
+ for (const candidate of [root, ...autoloadDirectories(root)]) {
293
+ const file = import_node_path.default.join(candidate, relative);
294
+ if (import_node_fs.default.existsSync(file)) {
295
+ return file;
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ function autoloadDirectories(root) {
301
+ try {
302
+ return import_node_fs.default.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => import_node_path.default.join(root, entry.name)).sort();
303
+ } catch {
304
+ return [];
305
+ }
294
306
  }
295
307
 
296
308
  // src/scope.ts
@@ -595,112 +607,15 @@ function inheritedStatements(ancestry, blockConfigures) {
595
607
  }
596
608
 
597
609
  // src/discovery.ts
598
- var import_behavioral_ir2 = require("@suss/behavioral-ir");
610
+ var import_behavioral_ir3 = require("@suss/behavioral-ir");
599
611
  var import_extractor5 = require("@suss/extractor");
600
612
 
601
- // src/envReads.ts
613
+ // src/clientCalls.ts
602
614
  var import_behavioral_ir = require("@suss/behavioral-ir");
603
- var import_extractor2 = require("@suss/extractor");
604
- var RUBY_ENV_RECOGNITION = "ruby-env";
605
- var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
606
- function envReadEffects(root) {
607
- const effects = [];
608
- (0, import_extractor2.walkDescendants)(root, null, {
609
- at: (node) => {
610
- const read = envReadAt(node);
611
- if (read !== null) {
612
- effects.push(configReadEffect(read));
613
- }
614
- },
615
- into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? import_extractor2.SKIP_CHILDREN : null
616
- });
617
- return effects;
618
- }
619
- function configReadEffect(read) {
620
- return {
621
- type: "interaction",
622
- binding: (0, import_behavioral_ir.runtimeConfigBinding)({
623
- recognition: RUBY_ENV_RECOGNITION,
624
- deploymentTarget: "lambda",
625
- instanceName: "<unknown>"
626
- }),
627
- callee: `ENV["${read.name}"]`,
628
- interaction: {
629
- class: "config-read",
630
- name: read.name,
631
- defaulted: read.defaulted
632
- }
633
- };
634
- }
635
- function envReadAt(node) {
636
- if (node.type === "element_reference") {
637
- return elementRead(node);
638
- }
639
- if (node.type === "call") {
640
- return fetchRead(node);
641
- }
642
- return null;
643
- }
644
- function elementRead(node) {
645
- const object = field(node, "object");
646
- if (object === null || !isEnv(object) || isAssignedTo(node)) {
647
- return null;
648
- }
649
- const index = node.namedChildren.find(
650
- (child) => child !== null && child.id !== object.id
651
- );
652
- const name = index === void 0 ? null : stringLiteralValue(index);
653
- if (name === null) {
654
- return null;
655
- }
656
- return { name, defaulted: isDefaultedAt(node) };
657
- }
658
- function fetchRead(node) {
659
- const receiver = field(node, "receiver");
660
- if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
661
- return null;
662
- }
663
- const { positional } = readCallArgs(field(node, "arguments"));
664
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
665
- if (name === null) {
666
- return null;
667
- }
668
- const hasDefault = positional.length > 1 || field(node, "block") !== null;
669
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
670
- }
671
- function isEnv(node) {
672
- if (node.type === "constant") {
673
- return node.text === "ENV";
674
- }
675
- return node.type === "scope_resolution" && field(node, "scope") === null && field(node, "name")?.text === "ENV";
676
- }
677
- function isAssignedTo(node) {
678
- const parent = node.parent;
679
- return parent !== null && (parent.type === "assignment" || parent.type === "operator_assignment") && field(parent, "left")?.id === node.id;
680
- }
681
- function isDefaultedAt(node) {
682
- let child = node;
683
- let parent = node.parent;
684
- while (parent !== null) {
685
- if (parent.type === "parenthesized_statements") {
686
- child = parent;
687
- parent = parent.parent;
688
- continue;
689
- }
690
- if (parent.type !== "binary" || field(parent, "operator")?.text !== "||") {
691
- return false;
692
- }
693
- if (field(parent, "left")?.id === child.id) {
694
- return true;
695
- }
696
- child = parent;
697
- parent = parent.parent;
698
- }
699
- return false;
700
- }
615
+ var import_values5 = require("@suss/values");
701
616
 
702
617
  // src/paths/effects.ts
703
- var import_extractor3 = require("@suss/extractor");
618
+ var import_extractor2 = require("@suss/extractor");
704
619
 
705
620
  // src/paths/bareCalls.ts
706
621
  var PARAMETER_LIST_TYPES = [
@@ -1219,7 +1134,7 @@ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
1219
1134
  statements.push(statement);
1220
1135
  }
1221
1136
  const lowered = lowerRubyBody(body, statements);
1222
- const enumerated = (0, import_extractor3.enumerateOrDegrade)(
1137
+ const enumerated = (0, import_extractor2.enumerateOrDegrade)(
1223
1138
  {
1224
1139
  statements: lowered.statements,
1225
1140
  terminalsByStmt: lowered.terminalsByStmt
@@ -1228,7 +1143,7 @@ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
1228
1143
  );
1229
1144
  return calls.map((call) => {
1230
1145
  const statement = statementOf2.get(call.id);
1231
- const conditions = (0, import_extractor3.sharedGatingConditions)(
1146
+ const conditions = (0, import_extractor2.sharedGatingConditions)(
1232
1147
  statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
1233
1148
  );
1234
1149
  return {
@@ -1242,9 +1157,120 @@ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS) {
1242
1157
  }
1243
1158
 
1244
1159
  // src/responseStatus.ts
1245
- var import_extractor4 = require("@suss/extractor");
1160
+ var import_extractor3 = require("@suss/extractor");
1246
1161
  var import_values4 = require("@suss/values");
1247
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;
1272
+ }
1273
+
1248
1274
  // src/values/evaluator.ts
1249
1275
  var import_resolution2 = require("@suss/resolution");
1250
1276
  var import_values2 = require("@suss/values");
@@ -2561,6 +2587,18 @@ var calleeRows = [
2561
2587
  kind: "callee",
2562
2588
  origin: { module: "Kernel", name: "String" },
2563
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])]) })
2564
2602
  }
2565
2603
  ];
2566
2604
  var rubyRows = [
@@ -2726,20 +2764,20 @@ function collectReturns(node, found) {
2726
2764
  function readingOfCall(call, declaration, names) {
2727
2765
  const argument = statusArgumentOf(call, declaration);
2728
2766
  if (argument === null) {
2729
- return declaration.defaultStatusCode === void 0 ? import_extractor4.absentReading : (0, import_extractor4.writtenReading)(declaration.defaultStatusCode, rangeOf(call));
2767
+ return declaration.defaultStatusCode === void 0 ? import_extractor3.absentReading : (0, import_extractor3.writtenReading)(declaration.defaultStatusCode, rangeOf(call));
2730
2768
  }
2731
2769
  const status = statusNumberOf(argument, names);
2732
2770
  if (status === null) {
2733
- return (0, import_extractor4.unreadableReading)(
2771
+ return (0, import_extractor3.unreadableReading)(
2734
2772
  "This response writes a status that does not settle on a number here, so this outcome claims none",
2735
2773
  rangeOf(argument)
2736
2774
  );
2737
2775
  }
2738
- return (0, import_extractor4.writtenReading)(status, rangeOf(argument));
2776
+ return (0, import_extractor3.writtenReading)(status, rangeOf(argument));
2739
2777
  }
2740
2778
  function effectsReaching(effects, conditions) {
2741
2779
  return effects.filter(
2742
- (effect) => (0, import_extractor4.guardsHoldOn)(
2780
+ (effect) => (0, import_extractor3.guardsHoldOn)(
2743
2781
  effect.type === "invocation" ? effect.preconditions : void 0,
2744
2782
  conditions
2745
2783
  )
@@ -2748,12 +2786,15 @@ function effectsReaching(effects, conditions) {
2748
2786
  function conditionOf(condition) {
2749
2787
  return {
2750
2788
  sourceText: condition.sourceText,
2751
- structured: null,
2789
+ structured: condition.expression === null ? null : predicateOf(condition.expression),
2752
2790
  polarity: condition.polarity,
2753
2791
  source: condition.source
2754
2792
  };
2755
2793
  }
2756
- function branchOf(outcome, pattern, effects, extraEffects) {
2794
+ function branchOf(outcome, pattern, effects, extraEffects, options) {
2795
+ if (outcome.fellThrough === true && options.fallthrough === "handOn") {
2796
+ return handsOnBranch(outcome, effects, extraEffects);
2797
+ }
2757
2798
  return {
2758
2799
  conditions: outcome.conditions,
2759
2800
  terminal: {
@@ -2778,7 +2819,72 @@ function branchOf(outcome, pattern, effects, extraEffects) {
2778
2819
  isDefault: outcome.conditions.length === 0
2779
2820
  };
2780
2821
  }
2781
- function responseBranches(method, pattern, effects, extraEffects) {
2822
+ function handsOnBranch(outcome, effects, extraEffects) {
2823
+ return {
2824
+ conditions: outcome.conditions,
2825
+ terminal: {
2826
+ kind: "delegate",
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: outcome.location
2836
+ },
2837
+ effects: effectsReaching(effects, outcome.conditions),
2838
+ ...extraEffects === void 0 ? {} : { extraEffects },
2839
+ location: outcome.location,
2840
+ isDefault: outcome.conditions.length === 0
2841
+ };
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
+ }
2887
+ function responseBranches(method, pattern, effects, extraEffects, options = {}) {
2782
2888
  const declarations = pattern.responseStatusCalls ?? [];
2783
2889
  const body = field(method, "body");
2784
2890
  if (declarations.length === 0 || body === null) {
@@ -2793,7 +2899,7 @@ function responseBranches(method, pattern, effects, extraEffects) {
2793
2899
  (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
2794
2900
  );
2795
2901
  const terminals = [...responses, ...bareReturns];
2796
- const enumerated = (0, import_extractor4.enumerateOrDegrade)(
2902
+ const enumerated = (0, import_extractor3.enumerateOrDegrade)(
2797
2903
  {
2798
2904
  statements: lowered.statements,
2799
2905
  terminalsByStmt: lowered.terminalsByStmt
@@ -2804,7 +2910,7 @@ function responseBranches(method, pattern, effects, extraEffects) {
2804
2910
  const outcomes = [];
2805
2911
  for (const terminal of terminals) {
2806
2912
  const declaration = declarationOf(terminal, byName, locals);
2807
- const reading = declaration === void 0 ? import_extractor4.absentReading : readingOfCall(terminal, declaration, statusNames);
2913
+ const reading = declaration === void 0 ? import_extractor3.absentReading : readingOfCall(terminal, declaration, statusNames);
2808
2914
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
2809
2915
  outcomes.push({
2810
2916
  conditions: path7.map(conditionOf),
@@ -2816,40 +2922,541 @@ function responseBranches(method, pattern, effects, extraEffects) {
2816
2922
  for (const path7 of enumerated.fallthrough) {
2817
2923
  outcomes.push({
2818
2924
  conditions: path7.map(conditionOf),
2819
- reading: import_extractor4.absentReading,
2820
- location: rangeOf(method)
2925
+ reading: import_extractor3.absentReading,
2926
+ location: rangeOf(method),
2927
+ fellThrough: true
2821
2928
  });
2822
2929
  }
2823
2930
  if (outcomes.length === 0) {
2824
2931
  return null;
2825
2932
  }
2826
2933
  return outcomes.map(
2827
- (outcome) => branchOf(outcome, pattern, effects, extraEffects)
2934
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2828
2935
  );
2829
2936
  }
2830
2937
 
2831
- // src/storage.ts
2832
- var import_datalog2 = require("@suss/datalog");
2833
- var import_ir_core = require("@suss/ir-core");
2834
- var import_resolution3 = require("@suss/resolution");
2835
- function children3(node) {
2836
- return node.namedChildren.filter((child) => child !== null);
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;
2837
2955
  }
2838
- function receiverOf(node) {
2839
- return node.type === "call" ? field(node, "receiver") : null;
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;
2840
2967
  }
2841
- function methodOf(node) {
2842
- return field(node, "method")?.text ?? "";
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;
2843
2979
  }
2844
- function rootConstant(node) {
2845
- const receiver = receiverOf(node);
2846
- if (receiver === null) {
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) {
2847
2984
  return null;
2848
2985
  }
2849
- if (receiver.type === "constant" || receiver.type === "scope_resolution") {
2850
- return receiver;
2986
+ const prefix = receiverPrefix(receiver, method, pattern, options);
2987
+ if (prefix === null) {
2988
+ return null;
2851
2989
  }
2852
- return rootConstant(receiver);
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
3202
+ );
3203
+ const name = index === void 0 ? null : stringLiteralValue(index);
3204
+ if (name === null) {
3205
+ return null;
3206
+ }
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;
3213
+ }
3214
+ const { positional } = readCallArgs(field(node, "arguments"));
3215
+ const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
3216
+ if (name === null) {
3217
+ return null;
3218
+ }
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;
3251
+ }
3252
+
3253
+ // src/filters.ts
3254
+ function controllerFilters(pattern, ancestry) {
3255
+ const forms = pattern.filters ?? [];
3256
+ if (forms.length === 0) {
3257
+ return [];
3258
+ }
3259
+ const byName = new Map(forms.map((form) => [form.name, form]));
3260
+ const skipNames = /* @__PURE__ */ new Map();
3261
+ for (const form of forms) {
3262
+ if (form.skippedBy !== void 0) {
3263
+ skipNames.set(form.skippedBy, form.name);
3264
+ }
3265
+ }
3266
+ const declared = [];
3267
+ const skips = [];
3268
+ for (const { statement } of inheritedStatements(ancestry)) {
3269
+ const called = calledName(statement);
3270
+ if (called === null) {
3271
+ continue;
3272
+ }
3273
+ const form = byName.get(called);
3274
+ if (form !== void 0) {
3275
+ declared.push(...declarationsOf(statement, form));
3276
+ continue;
3277
+ }
3278
+ const skipped = skipNames.get(called);
3279
+ if (skipped !== void 0) {
3280
+ skips.push(...skipsOf(statement, skipped));
3281
+ }
3282
+ }
3283
+ const resolved = [];
3284
+ for (const declaration of applySkips(declared, skips)) {
3285
+ const found = methodInAncestry(ancestry, declaration.methodName);
3286
+ if (found.type !== "found") {
3287
+ continue;
3288
+ }
3289
+ resolved.push({
3290
+ filter: declaration.filter,
3291
+ methodName: declaration.methodName,
3292
+ method: found.method,
3293
+ file: found.block.file,
3294
+ enclosingQualifiedName: found.block.info.qualifiedName,
3295
+ only: declaration.only,
3296
+ except: declaration.except
3297
+ });
3298
+ }
3299
+ return [
3300
+ ...resolved.filter((one) => one.filter.onThrow !== true),
3301
+ ...resolved.filter((one) => one.filter.onThrow === true)
3302
+ ];
3303
+ }
3304
+ function filterCoversAction(filter, actionName) {
3305
+ if (filter.except.has(actionName)) {
3306
+ return false;
3307
+ }
3308
+ return filter.only === null || filter.only.has(actionName);
3309
+ }
3310
+ function filterReference(filter, displayPath) {
3311
+ return {
3312
+ file: displayPath,
3313
+ name: filter.methodName,
3314
+ ...filter.filter.onThrow === true ? { onThrow: true } : {}
3315
+ };
3316
+ }
3317
+ function filterUnit(filter, pattern, displayPath, body) {
3318
+ const range = rangeOf(filter.method);
3319
+ const branches = responseBranches(
3320
+ filter.method,
3321
+ pattern,
3322
+ body.effects ?? [],
3323
+ body.extraEffects,
3324
+ { fallthrough: "handOn" }
3325
+ );
3326
+ return {
3327
+ identity: {
3328
+ name: filter.methodName,
3329
+ nameKind: "binding",
3330
+ kind: "middleware",
3331
+ file: displayPath,
3332
+ range,
3333
+ span: spanOf(filter.method),
3334
+ exportName: filter.methodName,
3335
+ exportPath: [filter.enclosingQualifiedName, filter.methodName]
3336
+ },
3337
+ boundaryBinding: null,
3338
+ parameters: [],
3339
+ branches: branches ?? [handsOn(range, body)],
3340
+ bodyContent: body.bodyContent ?? "absent",
3341
+ dependencyCalls: [],
3342
+ declaredContract: null
3343
+ };
3344
+ }
3345
+ function handsOn(range, body) {
3346
+ return {
3347
+ conditions: [],
3348
+ terminal: {
3349
+ kind: "delegate",
3350
+ statusCode: null,
3351
+ body: null,
3352
+ exceptionType: null,
3353
+ message: null,
3354
+ component: null,
3355
+ renderTree: null,
3356
+ delegateTarget: null,
3357
+ emitEvent: null,
3358
+ location: range
3359
+ },
3360
+ effects: body.effects ?? [],
3361
+ ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
3362
+ location: range,
3363
+ isDefault: true
3364
+ };
3365
+ }
3366
+ function calledName(statement) {
3367
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
3368
+ return null;
3369
+ }
3370
+ return field(statement, "method")?.text ?? null;
3371
+ }
3372
+ function declarationsOf(statement, filter) {
3373
+ const args = readCallArgs(field(statement, "arguments"));
3374
+ const keywords = filter.actionKeywords;
3375
+ const only = keywords === void 0 ? null : namesUnder(args.keyword[keywords.include]);
3376
+ const except = keywords === void 0 ? null : namesUnder(args.keyword[keywords.exclude]);
3377
+ return methodNamesOf(args, filter).map((methodName) => ({
3378
+ filter,
3379
+ methodName,
3380
+ only,
3381
+ except: except ?? /* @__PURE__ */ new Set()
3382
+ }));
3383
+ }
3384
+ function methodNamesOf(args, filter) {
3385
+ if (filter.methodFrom === "withKeyword") {
3386
+ const named2 = args.keyword.with;
3387
+ const value = named2 === void 0 ? null : symbolValue(named2);
3388
+ return value === null ? [] : [value];
3389
+ }
3390
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null);
3391
+ }
3392
+ function skipsOf(statement, filterName) {
3393
+ const args = readCallArgs(field(statement, "arguments"));
3394
+ const actions = namesUnder(args.keyword.only);
3395
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3396
+ }
3397
+ function applySkips(declared, skips) {
3398
+ const left = [];
3399
+ for (const declaration of declared) {
3400
+ const reaching = skips.filter(
3401
+ (skip) => skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
3402
+ );
3403
+ if (reaching.some((skip) => skip.actions === null)) {
3404
+ continue;
3405
+ }
3406
+ const except = new Set(declaration.except);
3407
+ for (const skip of reaching) {
3408
+ for (const action of skip.actions ?? []) {
3409
+ except.add(action);
3410
+ }
3411
+ }
3412
+ left.push({ ...declaration, except });
3413
+ }
3414
+ return left;
3415
+ }
3416
+ function namesUnder(node) {
3417
+ if (node === void 0) {
3418
+ return null;
3419
+ }
3420
+ const single = symbolValue(node);
3421
+ if (single !== null) {
3422
+ return /* @__PURE__ */ new Set([single]);
3423
+ }
3424
+ if (node.type !== "array" && node.type !== "symbol_array") {
3425
+ return null;
3426
+ }
3427
+ const names = /* @__PURE__ */ new Set();
3428
+ for (const child of bodyStatements(node)) {
3429
+ const bare = child.type === "bare_symbol" ? child.text : null;
3430
+ const name = symbolValue(child) ?? bare;
3431
+ if (name !== null) {
3432
+ names.add(name);
3433
+ }
3434
+ }
3435
+ return names;
3436
+ }
3437
+
3438
+ // src/storage.ts
3439
+ var import_datalog2 = require("@suss/datalog");
3440
+ var import_ir_core = require("@suss/ir-core");
3441
+ var import_resolution3 = require("@suss/resolution");
3442
+ function children3(node) {
3443
+ return node.namedChildren.filter((child) => child !== null);
3444
+ }
3445
+ function receiverOf(node) {
3446
+ return node.type === "call" ? field(node, "receiver") : null;
3447
+ }
3448
+ function methodOf(node) {
3449
+ return field(node, "method")?.text ?? "";
3450
+ }
3451
+ function rootConstant(node) {
3452
+ const receiver = receiverOf(node);
3453
+ if (receiver === null) {
3454
+ return null;
3455
+ }
3456
+ if (receiver.type === "constant" || receiver.type === "scope_resolution") {
3457
+ return receiver;
3458
+ }
3459
+ return rootConstant(receiver);
2853
3460
  }
2854
3461
  var ANCESTRY_PROGRAM = (0, import_datalog2.deriveOnDemand)(
2855
3462
  [...import_resolution3.RESOLUTION_RULES, ...import_resolution3.RESOLUTION_QUESTIONS],
@@ -3046,6 +3653,16 @@ async function discoverUnits(root, options) {
3046
3653
  file: options.absoluteFile ?? options.filePath
3047
3654
  }));
3048
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
+ }
3049
3666
  for (const info of classes) {
3050
3667
  const ownBlocks = fileBlocks.filter(
3051
3668
  (block) => block.info.qualifiedName === info.qualifiedName
@@ -3077,7 +3694,7 @@ function unitsFor(pattern, pack, info, ownBlocks, fileBlocks, options) {
3077
3694
  graphqlObjectFields: (p) => graphqlObjectFieldUnits(p, pack, info, ownBlocks, fileBlocks, options),
3078
3695
  controllerActions: (p) => controllerActionUnits(p, pack, info, ownBlocks, fileBlocks, options)
3079
3696
  };
3080
- return (0, import_behavioral_ir2.dispatchByType)(table, pattern);
3697
+ return (0, import_behavioral_ir3.dispatchByType)(table, pattern);
3081
3698
  }
3082
3699
  async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlocks, options) {
3083
3700
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
@@ -3127,7 +3744,23 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3127
3744
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3128
3745
  return [];
3129
3746
  }
3747
+ const filters = controllerFilters(pattern, ancestry);
3130
3748
  const units = [];
3749
+ for (const filter of filters) {
3750
+ const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
3751
+ const raw = filterUnit(
3752
+ filter,
3753
+ pattern,
3754
+ displayPath,
3755
+ bodyOfMethod(filter.method, options)
3756
+ );
3757
+ units.push(raw);
3758
+ options.onReachSeed?.(raw, {
3759
+ file: filter.file,
3760
+ node: filter.method,
3761
+ enclosingQualifiedName: filter.enclosingQualifiedName
3762
+ });
3763
+ }
3131
3764
  for (const block of ownBlocks) {
3132
3765
  if (block.info.bodyNode === null) {
3133
3766
  continue;
@@ -3139,6 +3772,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3139
3772
  if ((visibility.get(actionName) ?? "public") !== "public") {
3140
3773
  continue;
3141
3774
  }
3775
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3776
+ (filter) => filterReference(
3777
+ filter,
3778
+ options.displayPathOf?.(filter.file) ?? filter.file
3779
+ )
3780
+ );
3142
3781
  const raw = buildControllerActionUnit(
3143
3782
  pack,
3144
3783
  pattern,
@@ -3146,7 +3785,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3146
3785
  actionName,
3147
3786
  method,
3148
3787
  options.filePath,
3149
- options
3788
+ options,
3789
+ around
3150
3790
  );
3151
3791
  units.push(raw);
3152
3792
  options.onReachSeed?.(raw, {
@@ -3158,7 +3798,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3158
3798
  }
3159
3799
  return units;
3160
3800
  }
3161
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead) {
3801
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead, wrappers = []) {
3162
3802
  const range = rangeOf(method);
3163
3803
  const route = pattern.routeFor(controllerQualifiedName, actionName);
3164
3804
  const body = bodyOfMethod(method, bodyRead);
@@ -3179,7 +3819,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3179
3819
  exportName: actionName,
3180
3820
  exportPath: [controllerQualifiedName, actionName]
3181
3821
  },
3182
- boundaryBinding: route === null ? null : (0, import_behavioral_ir2.restBinding)({
3822
+ boundaryBinding: route === null ? null : (0, import_behavioral_ir3.restBinding)({
3183
3823
  transport: pack.protocol,
3184
3824
  method: route.method,
3185
3825
  path: route.path,
@@ -3213,7 +3853,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3213
3853
  ],
3214
3854
  bodyContent: body.bodyContent ?? "absent",
3215
3855
  dependencyCalls: [],
3216
- declaredContract: null
3856
+ declaredContract: null,
3857
+ ...wrappers.length > 0 ? { wrappers: [...wrappers] } : {}
3217
3858
  };
3218
3859
  }
3219
3860
  function routingGapUnit(pattern, gaps) {
@@ -3296,7 +3937,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3296
3937
  const storage = bodyRead.storage;
3297
3938
  const extra = [
3298
3939
  ...envReadEffects(method),
3299
- ...storage === void 0 ? [] : storageEffects(callsUnder(method), storage)
3940
+ ...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
3300
3941
  ];
3301
3942
  return {
3302
3943
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -3305,7 +3946,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3305
3946
  ...extra.length > 0 ? { extraEffects: extra } : {}
3306
3947
  };
3307
3948
  }
3308
- function callsUnder(node, found = []) {
3949
+ function callsUnder2(node, found = []) {
3309
3950
  for (const child of node.namedChildren) {
3310
3951
  if (child === null) {
3311
3952
  continue;
@@ -3313,7 +3954,7 @@ function callsUnder(node, found = []) {
3313
3954
  if (child.type === "call") {
3314
3955
  found.push(child);
3315
3956
  }
3316
- callsUnder(child, found);
3957
+ callsUnder2(child, found);
3317
3958
  }
3318
3959
  return found;
3319
3960
  }
@@ -3340,7 +3981,7 @@ function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
3340
3981
  ),
3341
3982
  none: () => nothingThere
3342
3983
  };
3343
- return (0, import_behavioral_ir2.dispatchByType)(table, found);
3984
+ return (0, import_behavioral_ir3.dispatchByType)(table, found);
3344
3985
  }
3345
3986
  async function readFieldCall(stmt, scope, ctx, ancestry) {
3346
3987
  if (stmt.type !== "call" || field(stmt, "receiver") !== null) {
@@ -3609,7 +4250,7 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
3609
4250
  exportName: null,
3610
4251
  exportPath: null
3611
4252
  },
3612
- boundaryBinding: decl.namedOnTheWire ? (0, import_behavioral_ir2.graphqlResolverBinding)({
4253
+ boundaryBinding: decl.namedOnTheWire ? (0, import_behavioral_ir3.graphqlResolverBinding)({
3613
4254
  transport: pack.protocol,
3614
4255
  recognition: pack.name,
3615
4256
  typeName,
@@ -3807,7 +4448,7 @@ function parseWith(language, source) {
3807
4448
  // src/project.ts
3808
4449
  var import_node_fs3 = __toESM(require("fs"), 1);
3809
4450
  var import_node_path4 = __toESM(require("path"), 1);
3810
- var import_behavioral_ir4 = require("@suss/behavioral-ir");
4451
+ var import_behavioral_ir5 = require("@suss/behavioral-ir");
3811
4452
  var import_datalog4 = require("@suss/datalog");
3812
4453
  var import_extractor10 = require("@suss/extractor");
3813
4454
 
@@ -3862,7 +4503,7 @@ function requiredRelativePath(node) {
3862
4503
  }
3863
4504
 
3864
4505
  // src/reach/closure.ts
3865
- var import_behavioral_ir3 = require("@suss/behavioral-ir");
4506
+ var import_behavioral_ir4 = require("@suss/behavioral-ir");
3866
4507
  var import_datalog3 = require("@suss/datalog");
3867
4508
  var import_extractor8 = require("@suss/extractor");
3868
4509
 
@@ -4086,14 +4727,14 @@ async function reachedFunctions(seeds, options) {
4086
4727
  gapHandling: "permissive"
4087
4728
  });
4088
4729
  summary.confidence = { source: "inferred_static", level: "low" };
4089
- summary.gaps.push(...(stopsByKey.get(key) ?? []).map(import_behavioral_ir3.unfollowedCallGap));
4090
- (0, import_behavioral_ir3.placeCalls)(summary, targetsByKey.get(key));
4091
- (0, import_behavioral_ir3.placeArgTargets)(summary, argTargetsByKey.get(key));
4092
- (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));
4093
4734
  summariesByKey.set(key, [summary]);
4094
4735
  summaries.push(summary);
4095
4736
  }
4096
- (0, import_behavioral_ir3.recordParameterGaps)(parameterCallsByKey, summariesByKey, passedPositions);
4737
+ (0, import_behavioral_ir4.recordParameterGaps)(parameterCallsByKey, summariesByKey, passedPositions);
4097
4738
  return {
4098
4739
  summaries,
4099
4740
  targetsByKey,
@@ -4203,7 +4844,7 @@ async function scanBody(source, ctx, options) {
4203
4844
  };
4204
4845
  const followed2 = [];
4205
4846
  const stops = [];
4206
- const placements = new import_behavioral_ir3.TargetPlacements();
4847
+ const placements = new import_behavioral_ir4.TargetPlacements();
4207
4848
  const parameterCalls = [];
4208
4849
  const passedPositions = /* @__PURE__ */ new Set();
4209
4850
  const seen = /* @__PURE__ */ new Set();
@@ -4256,7 +4897,7 @@ async function scanBody(source, ctx, options) {
4256
4897
  );
4257
4898
  if (outcome.kind === "stopped") {
4258
4899
  const stopKey = `${outcome.reason}:${callee}`;
4259
- if (!seen.has(stopKey) && (0, import_behavioral_ir3.worthRecording)(outcome.reason)) {
4900
+ if (!seen.has(stopKey) && (0, import_behavioral_ir4.worthRecording)(outcome.reason)) {
4260
4901
  seen.add(stopKey);
4261
4902
  stops.push({ callee, reason: outcome.reason });
4262
4903
  }
@@ -4300,7 +4941,7 @@ function libraryUnit(target, options) {
4300
4941
  exportName: exportPath[0] ?? name,
4301
4942
  exportPath
4302
4943
  },
4303
- boundaryBinding: (0, import_behavioral_ir3.functionCallBinding)({
4944
+ boundaryBinding: (0, import_behavioral_ir4.functionCallBinding)({
4304
4945
  transport: "in-process",
4305
4946
  recognition: "reachable"
4306
4947
  }),
@@ -4379,6 +5020,14 @@ function inheritedMethodsIn(packs) {
4379
5020
  }
4380
5021
  return found;
4381
5022
  }
5023
+ function alreadyDiscovered(seen, raw) {
5024
+ const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
5025
+ if (seen.has(key)) {
5026
+ return true;
5027
+ }
5028
+ seen.add(key);
5029
+ return false;
5030
+ }
4382
5031
  async function extractRubyProject(options) {
4383
5032
  const timer = options.onTiming !== void 0 ? (0, import_extractor10.createTimer)() : (0, import_extractor10.noopTimer)();
4384
5033
  const cacheDir = adapterStamp.declineWhenRunFromSource(
@@ -4440,6 +5089,7 @@ async function extractRubyProject(options) {
4440
5089
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
4441
5090
  const seeds = [];
4442
5091
  const summariesBySeed = /* @__PURE__ */ new Map();
5092
+ const discovered = /* @__PURE__ */ new Set();
4443
5093
  for (const { file, root } of parsed) {
4444
5094
  const displayPath = displayPathOf(file);
4445
5095
  const seedByRaw = /* @__PURE__ */ new Map();
@@ -4452,10 +5102,15 @@ async function extractRubyProject(options) {
4452
5102
  cache,
4453
5103
  ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
4454
5104
  inheritedMethods,
5105
+ displayPathOf,
5106
+ facts: db,
4455
5107
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
4456
5108
  })
4457
5109
  );
4458
5110
  for (const raw of rawUnits) {
5111
+ if (alreadyDiscovered(discovered, raw)) {
5112
+ continue;
5113
+ }
4459
5114
  const summary = timer.time(
4460
5115
  "summarize",
4461
5116
  () => (0, import_extractor10.assembleSummary)(raw, { gapHandling: "permissive" })
@@ -4531,14 +5186,14 @@ async function extractRubyProject(options) {
4531
5186
  for (const [key, owners] of summariesBySeed) {
4532
5187
  for (const summary of owners) {
4533
5188
  summary.gaps.push(
4534
- ...(reached.stopsByKey.get(key) ?? []).map(import_behavioral_ir4.unfollowedCallGap)
5189
+ ...(reached.stopsByKey.get(key) ?? []).map(import_behavioral_ir5.unfollowedCallGap)
4535
5190
  );
4536
- (0, import_behavioral_ir4.placeCalls)(summary, reached.targetsByKey.get(key));
4537
- (0, import_behavioral_ir4.placeArgTargets)(summary, reached.argTargetsByKey.get(key));
4538
- (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));
4539
5194
  }
4540
5195
  }
4541
- (0, import_behavioral_ir4.recordParameterGaps)(
5196
+ (0, import_behavioral_ir5.recordParameterGaps)(
4542
5197
  reached.parameterCallsByKey,
4543
5198
  summariesBySeed,
4544
5199
  reached.passedPositions
@@ -4549,21 +5204,27 @@ async function extractRubyProject(options) {
4549
5204
  const idRoot = options.projectRoot ?? options.workspaceRoot;
4550
5205
  for (const summary of summaries) {
4551
5206
  const absoluteFile = options.workspaceRoot === void 0 ? summary.location.file : import_node_path4.default.resolve(options.workspaceRoot, summary.location.file);
4552
- summary.identity.id = (0, import_behavioral_ir4.summaryIdFromParts)({
5207
+ summary.identity.id = (0, import_behavioral_ir5.summaryIdFromParts)({
4553
5208
  workspace: void 0,
4554
5209
  file: idRoot === void 0 ? absoluteFile : import_node_path4.default.relative(idRoot, absoluteFile),
4555
5210
  name: summary.identity.name,
4556
5211
  exportPath: summary.identity.exportPath
4557
5212
  });
4558
5213
  }
4559
- (0, import_behavioral_ir4.disambiguateSummaryIds)(summaries);
4560
- (0, import_behavioral_ir4.linkCallsToSummaries)(summaries);
5214
+ (0, import_behavioral_ir5.disambiguateSummaryIds)(summaries);
5215
+ (0, import_behavioral_ir5.linkCallsToSummaries)(summaries);
5216
+ const composed = timer.time(
5217
+ "summarize",
5218
+ () => (0, import_extractor10.composeWrappers)(summaries, {
5219
+ gapHandling: options.gapHandling ?? "permissive"
5220
+ })
5221
+ );
4561
5222
  await timer.timeAsync("cache.write", async () => {
4562
- if (cacheDir === null || summaries.length === 0) {
5223
+ if (cacheDir === null || composed.length === 0) {
4563
5224
  return;
4564
5225
  }
4565
5226
  try {
4566
- await extractionCache.write(cacheInput, summaries);
5227
+ await extractionCache.write(cacheInput, composed);
4567
5228
  } catch {
4568
5229
  }
4569
5230
  });
@@ -4572,11 +5233,11 @@ async function extractRubyProject(options) {
4572
5233
  packs: options.packs,
4573
5234
  tallies,
4574
5235
  filesWalked: options.files.length,
4575
- summaries
5236
+ summaries: composed
4576
5237
  })
4577
5238
  );
4578
5239
  options.onTiming?.(timer.report());
4579
- return { summaries, facts: db };
5240
+ return { summaries: composed, facts: db };
4580
5241
  }
4581
5242
  function fileDependenciesOf(db, displayPathOf) {
4582
5243
  const byFile = /* @__PURE__ */ new Map();