@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.js CHANGED
@@ -209,9 +209,21 @@ var PATH_CONVENTIONS = {
209
209
  railsUnderscore: underscoreConstantPath
210
210
  };
211
211
  function resolveConstantFile(root, qualifiedName, convention) {
212
- const relative = PATH_CONVENTIONS[convention](qualifiedName);
213
- const file = path.join(root, `${relative}.rb`);
214
- return fs.existsSync(file) ? file : null;
212
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
213
+ for (const candidate of [root, ...autoloadDirectories(root)]) {
214
+ const file = path.join(candidate, relative);
215
+ if (fs.existsSync(file)) {
216
+ return file;
217
+ }
218
+ }
219
+ return null;
220
+ }
221
+ function autoloadDirectories(root) {
222
+ try {
223
+ return fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(root, entry.name)).sort();
224
+ } catch {
225
+ return [];
226
+ }
215
227
  }
216
228
 
217
229
  // src/scope.ts
@@ -519,110 +531,13 @@ function inheritedStatements(ancestry, blockConfigures) {
519
531
  import {
520
532
  dispatchByType,
521
533
  graphqlResolverBinding,
522
- restBinding
534
+ restBinding as restBinding2
523
535
  } from "@suss/behavioral-ir";
524
536
  import { absentReading as absentReading2, unreadableReading as unreadableReading2 } from "@suss/extractor";
525
537
 
526
- // src/envReads.ts
527
- import { runtimeConfigBinding } from "@suss/behavioral-ir";
528
- import { SKIP_CHILDREN as SKIP_CHILDREN2, walkDescendants as walkDescendants2 } from "@suss/extractor";
529
- var RUBY_ENV_RECOGNITION = "ruby-env";
530
- var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
531
- function envReadEffects(root) {
532
- const effects = [];
533
- walkDescendants2(root, null, {
534
- at: (node) => {
535
- const read = envReadAt(node);
536
- if (read !== null) {
537
- effects.push(configReadEffect(read));
538
- }
539
- },
540
- into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? SKIP_CHILDREN2 : null
541
- });
542
- return effects;
543
- }
544
- function configReadEffect(read) {
545
- return {
546
- type: "interaction",
547
- binding: runtimeConfigBinding({
548
- recognition: RUBY_ENV_RECOGNITION,
549
- deploymentTarget: "lambda",
550
- instanceName: "<unknown>"
551
- }),
552
- callee: `ENV["${read.name}"]`,
553
- interaction: {
554
- class: "config-read",
555
- name: read.name,
556
- defaulted: read.defaulted
557
- }
558
- };
559
- }
560
- function envReadAt(node) {
561
- if (node.type === "element_reference") {
562
- return elementRead(node);
563
- }
564
- if (node.type === "call") {
565
- return fetchRead(node);
566
- }
567
- return null;
568
- }
569
- function elementRead(node) {
570
- const object = field(node, "object");
571
- if (object === null || !isEnv(object) || isAssignedTo(node)) {
572
- return null;
573
- }
574
- const index = node.namedChildren.find(
575
- (child) => child !== null && child.id !== object.id
576
- );
577
- const name = index === void 0 ? null : stringLiteralValue(index);
578
- if (name === null) {
579
- return null;
580
- }
581
- return { name, defaulted: isDefaultedAt(node) };
582
- }
583
- function fetchRead(node) {
584
- const receiver = field(node, "receiver");
585
- if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
586
- return null;
587
- }
588
- const { positional } = readCallArgs(field(node, "arguments"));
589
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
590
- if (name === null) {
591
- return null;
592
- }
593
- const hasDefault = positional.length > 1 || field(node, "block") !== null;
594
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
595
- }
596
- function isEnv(node) {
597
- if (node.type === "constant") {
598
- return node.text === "ENV";
599
- }
600
- return node.type === "scope_resolution" && field(node, "scope") === null && field(node, "name")?.text === "ENV";
601
- }
602
- function isAssignedTo(node) {
603
- const parent = node.parent;
604
- return parent !== null && (parent.type === "assignment" || parent.type === "operator_assignment") && field(parent, "left")?.id === node.id;
605
- }
606
- function isDefaultedAt(node) {
607
- let child = node;
608
- let parent = node.parent;
609
- while (parent !== null) {
610
- if (parent.type === "parenthesized_statements") {
611
- child = parent;
612
- parent = parent.parent;
613
- continue;
614
- }
615
- if (parent.type !== "binary" || field(parent, "operator")?.text !== "||") {
616
- return false;
617
- }
618
- if (field(parent, "left")?.id === child.id) {
619
- return true;
620
- }
621
- child = parent;
622
- parent = parent.parent;
623
- }
624
- return false;
625
- }
538
+ // src/clientCalls.ts
539
+ import { namesNothing, restBinding } from "@suss/behavioral-ir";
540
+ import { pathOf } from "@suss/values";
626
541
 
627
542
  // src/paths/effects.ts
628
543
  import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
@@ -1174,11 +1089,122 @@ import {
1174
1089
  unreadableReading,
1175
1090
  writtenReading
1176
1091
  } from "@suss/extractor";
1177
- import { constantOf, literalOf as literalOf2 } from "@suss/values";
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
+ }
1178
1204
 
1179
1205
  // src/values/evaluator.ts
1180
1206
  import { nodeOfKey } from "@suss/resolution";
1181
- import { Evaluator, force, literalOf } from "@suss/values";
1207
+ import { Evaluator, force, literalOf as literalOf2 } from "@suss/values";
1182
1208
 
1183
1209
  // src/facts/resolve.ts
1184
1210
  import {
@@ -2523,6 +2549,18 @@ var calleeRows = [
2523
2549
  kind: "callee",
2524
2550
  origin: { module: "Kernel", name: "String" },
2525
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])]) })
2526
2564
  }
2527
2565
  ];
2528
2566
  var rubyRows = [
@@ -2548,7 +2586,7 @@ function evaluatedValue(node, db) {
2548
2586
  return force(evaluatorFor(node, db).evaluate(node));
2549
2587
  }
2550
2588
  function stringValueOf(node, db) {
2551
- return literalOf(evaluatedValue(node, db));
2589
+ return literalOf2(evaluatedValue(node, db));
2552
2590
  }
2553
2591
  function methodDefinitionsIn(file, root) {
2554
2592
  const found = /* @__PURE__ */ new Map();
@@ -2641,7 +2679,7 @@ function statusNumberOf(node, names) {
2641
2679
  if (typeof constant2 === "number") {
2642
2680
  return constant2;
2643
2681
  }
2644
- const name = literalOf2(value);
2682
+ const name = literalOf3(value);
2645
2683
  return name === null ? null : names[name] ?? null;
2646
2684
  }
2647
2685
  function declarationsByName(declarations) {
@@ -2710,12 +2748,15 @@ function effectsReaching(effects, conditions) {
2710
2748
  function conditionOf(condition) {
2711
2749
  return {
2712
2750
  sourceText: condition.sourceText,
2713
- structured: null,
2751
+ structured: condition.expression === null ? null : predicateOf(condition.expression),
2714
2752
  polarity: condition.polarity,
2715
2753
  source: condition.source
2716
2754
  };
2717
2755
  }
2718
- function branchOf(outcome, pattern, effects, extraEffects) {
2756
+ function branchOf(outcome, pattern, effects, extraEffects, options) {
2757
+ if (outcome.fellThrough === true && options.fallthrough === "handOn") {
2758
+ return handsOnBranch(outcome, effects, extraEffects);
2759
+ }
2719
2760
  return {
2720
2761
  conditions: outcome.conditions,
2721
2762
  terminal: {
@@ -2740,7 +2781,72 @@ function branchOf(outcome, pattern, effects, extraEffects) {
2740
2781
  isDefault: outcome.conditions.length === 0
2741
2782
  };
2742
2783
  }
2743
- function responseBranches(method, pattern, effects, extraEffects) {
2784
+ function handsOnBranch(outcome, effects, extraEffects) {
2785
+ return {
2786
+ conditions: outcome.conditions,
2787
+ terminal: {
2788
+ kind: "delegate",
2789
+ statusCode: null,
2790
+ body: null,
2791
+ exceptionType: null,
2792
+ message: null,
2793
+ component: null,
2794
+ renderTree: null,
2795
+ delegateTarget: null,
2796
+ emitEvent: null,
2797
+ location: outcome.location
2798
+ },
2799
+ effects: effectsReaching(effects, outcome.conditions),
2800
+ ...extraEffects === void 0 ? {} : { extraEffects },
2801
+ location: outcome.location,
2802
+ isDefault: outcome.conditions.length === 0
2803
+ };
2804
+ }
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 = {}) {
2744
2850
  const declarations = pattern.responseStatusCalls ?? [];
2745
2851
  const body = field(method, "body");
2746
2852
  if (declarations.length === 0 || body === null) {
@@ -2779,43 +2885,544 @@ function responseBranches(method, pattern, effects, extraEffects) {
2779
2885
  outcomes.push({
2780
2886
  conditions: path7.map(conditionOf),
2781
2887
  reading: absentReading,
2782
- location: rangeOf(method)
2888
+ location: rangeOf(method),
2889
+ fellThrough: true
2783
2890
  });
2784
2891
  }
2785
2892
  if (outcomes.length === 0) {
2786
2893
  return null;
2787
2894
  }
2788
2895
  return outcomes.map(
2789
- (outcome) => branchOf(outcome, pattern, effects, extraEffects)
2896
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
2790
2897
  );
2791
2898
  }
2792
2899
 
2793
- // src/storage.ts
2794
- import { deriveOnDemand as deriveOnDemand2, evaluate as evaluate2 } from "@suss/datalog";
2795
- import { storageBinding } from "@suss/ir-core";
2796
- import {
2797
- ANSWER_RELATIONS as ANSWER_RELATIONS2,
2798
- RESOLUTION_QUESTIONS as RESOLUTION_QUESTIONS2,
2799
- RESOLUTION_RULES as RESOLUTION_RULES2
2800
- } from "@suss/resolution";
2801
- function children3(node) {
2802
- return node.namedChildren.filter((child) => child !== null);
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;
2803
2917
  }
2804
- function receiverOf(node) {
2805
- return node.type === "call" ? field(node, "receiver") : null;
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;
2806
2929
  }
2807
- function methodOf(node) {
2808
- return field(node, "method")?.text ?? "";
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;
2809
2941
  }
2810
- function rootConstant(node) {
2811
- const receiver = receiverOf(node);
2812
- if (receiver === null) {
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) {
2813
2946
  return null;
2814
2947
  }
2815
- if (receiver.type === "constant" || receiver.type === "scope_resolution") {
2816
- return receiver;
2948
+ const prefix = receiverPrefix(receiver, method, pattern, options);
2949
+ if (prefix === null) {
2950
+ return null;
2817
2951
  }
2818
- return rootConstant(receiver);
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)) {
3160
+ return null;
3161
+ }
3162
+ const index = node.namedChildren.find(
3163
+ (child) => child !== null && child.id !== object.id
3164
+ );
3165
+ const name = index === void 0 ? null : stringLiteralValue(index);
3166
+ if (name === null) {
3167
+ return null;
3168
+ }
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;
3175
+ }
3176
+ const { positional } = readCallArgs(field(node, "arguments"));
3177
+ const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
3178
+ if (name === null) {
3179
+ return null;
3180
+ }
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;
3213
+ }
3214
+
3215
+ // src/filters.ts
3216
+ function controllerFilters(pattern, ancestry) {
3217
+ const forms = pattern.filters ?? [];
3218
+ if (forms.length === 0) {
3219
+ return [];
3220
+ }
3221
+ const byName = new Map(forms.map((form) => [form.name, form]));
3222
+ const skipNames = /* @__PURE__ */ new Map();
3223
+ for (const form of forms) {
3224
+ if (form.skippedBy !== void 0) {
3225
+ skipNames.set(form.skippedBy, form.name);
3226
+ }
3227
+ }
3228
+ const declared = [];
3229
+ const skips = [];
3230
+ for (const { statement } of inheritedStatements(ancestry)) {
3231
+ const called = calledName(statement);
3232
+ if (called === null) {
3233
+ continue;
3234
+ }
3235
+ const form = byName.get(called);
3236
+ if (form !== void 0) {
3237
+ declared.push(...declarationsOf(statement, form));
3238
+ continue;
3239
+ }
3240
+ const skipped = skipNames.get(called);
3241
+ if (skipped !== void 0) {
3242
+ skips.push(...skipsOf(statement, skipped));
3243
+ }
3244
+ }
3245
+ const resolved = [];
3246
+ for (const declaration of applySkips(declared, skips)) {
3247
+ const found = methodInAncestry(ancestry, declaration.methodName);
3248
+ if (found.type !== "found") {
3249
+ continue;
3250
+ }
3251
+ resolved.push({
3252
+ filter: declaration.filter,
3253
+ methodName: declaration.methodName,
3254
+ method: found.method,
3255
+ file: found.block.file,
3256
+ enclosingQualifiedName: found.block.info.qualifiedName,
3257
+ only: declaration.only,
3258
+ except: declaration.except
3259
+ });
3260
+ }
3261
+ return [
3262
+ ...resolved.filter((one) => one.filter.onThrow !== true),
3263
+ ...resolved.filter((one) => one.filter.onThrow === true)
3264
+ ];
3265
+ }
3266
+ function filterCoversAction(filter, actionName) {
3267
+ if (filter.except.has(actionName)) {
3268
+ return false;
3269
+ }
3270
+ return filter.only === null || filter.only.has(actionName);
3271
+ }
3272
+ function filterReference(filter, displayPath) {
3273
+ return {
3274
+ file: displayPath,
3275
+ name: filter.methodName,
3276
+ ...filter.filter.onThrow === true ? { onThrow: true } : {}
3277
+ };
3278
+ }
3279
+ function filterUnit(filter, pattern, displayPath, body) {
3280
+ const range = rangeOf(filter.method);
3281
+ const branches = responseBranches(
3282
+ filter.method,
3283
+ pattern,
3284
+ body.effects ?? [],
3285
+ body.extraEffects,
3286
+ { fallthrough: "handOn" }
3287
+ );
3288
+ return {
3289
+ identity: {
3290
+ name: filter.methodName,
3291
+ nameKind: "binding",
3292
+ kind: "middleware",
3293
+ file: displayPath,
3294
+ range,
3295
+ span: spanOf(filter.method),
3296
+ exportName: filter.methodName,
3297
+ exportPath: [filter.enclosingQualifiedName, filter.methodName]
3298
+ },
3299
+ boundaryBinding: null,
3300
+ parameters: [],
3301
+ branches: branches ?? [handsOn(range, body)],
3302
+ bodyContent: body.bodyContent ?? "absent",
3303
+ dependencyCalls: [],
3304
+ declaredContract: null
3305
+ };
3306
+ }
3307
+ function handsOn(range, body) {
3308
+ return {
3309
+ conditions: [],
3310
+ terminal: {
3311
+ kind: "delegate",
3312
+ statusCode: null,
3313
+ body: null,
3314
+ exceptionType: null,
3315
+ message: null,
3316
+ component: null,
3317
+ renderTree: null,
3318
+ delegateTarget: null,
3319
+ emitEvent: null,
3320
+ location: range
3321
+ },
3322
+ effects: body.effects ?? [],
3323
+ ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
3324
+ location: range,
3325
+ isDefault: true
3326
+ };
3327
+ }
3328
+ function calledName(statement) {
3329
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
3330
+ return null;
3331
+ }
3332
+ return field(statement, "method")?.text ?? null;
3333
+ }
3334
+ function declarationsOf(statement, filter) {
3335
+ const args = readCallArgs(field(statement, "arguments"));
3336
+ const keywords = filter.actionKeywords;
3337
+ const only = keywords === void 0 ? null : namesUnder(args.keyword[keywords.include]);
3338
+ const except = keywords === void 0 ? null : namesUnder(args.keyword[keywords.exclude]);
3339
+ return methodNamesOf(args, filter).map((methodName) => ({
3340
+ filter,
3341
+ methodName,
3342
+ only,
3343
+ except: except ?? /* @__PURE__ */ new Set()
3344
+ }));
3345
+ }
3346
+ function methodNamesOf(args, filter) {
3347
+ if (filter.methodFrom === "withKeyword") {
3348
+ const named2 = args.keyword.with;
3349
+ const value = named2 === void 0 ? null : symbolValue(named2);
3350
+ return value === null ? [] : [value];
3351
+ }
3352
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null);
3353
+ }
3354
+ function skipsOf(statement, filterName) {
3355
+ const args = readCallArgs(field(statement, "arguments"));
3356
+ const actions = namesUnder(args.keyword.only);
3357
+ return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3358
+ }
3359
+ function applySkips(declared, skips) {
3360
+ const left = [];
3361
+ for (const declaration of declared) {
3362
+ const reaching = skips.filter(
3363
+ (skip) => skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
3364
+ );
3365
+ if (reaching.some((skip) => skip.actions === null)) {
3366
+ continue;
3367
+ }
3368
+ const except = new Set(declaration.except);
3369
+ for (const skip of reaching) {
3370
+ for (const action of skip.actions ?? []) {
3371
+ except.add(action);
3372
+ }
3373
+ }
3374
+ left.push({ ...declaration, except });
3375
+ }
3376
+ return left;
3377
+ }
3378
+ function namesUnder(node) {
3379
+ if (node === void 0) {
3380
+ return null;
3381
+ }
3382
+ const single = symbolValue(node);
3383
+ if (single !== null) {
3384
+ return /* @__PURE__ */ new Set([single]);
3385
+ }
3386
+ if (node.type !== "array" && node.type !== "symbol_array") {
3387
+ return null;
3388
+ }
3389
+ const names = /* @__PURE__ */ new Set();
3390
+ for (const child of bodyStatements(node)) {
3391
+ const bare = child.type === "bare_symbol" ? child.text : null;
3392
+ const name = symbolValue(child) ?? bare;
3393
+ if (name !== null) {
3394
+ names.add(name);
3395
+ }
3396
+ }
3397
+ return names;
3398
+ }
3399
+
3400
+ // src/storage.ts
3401
+ import { deriveOnDemand as deriveOnDemand2, evaluate as evaluate2 } from "@suss/datalog";
3402
+ import { storageBinding } from "@suss/ir-core";
3403
+ import {
3404
+ ANSWER_RELATIONS as ANSWER_RELATIONS2,
3405
+ RESOLUTION_QUESTIONS as RESOLUTION_QUESTIONS2,
3406
+ RESOLUTION_RULES as RESOLUTION_RULES2
3407
+ } from "@suss/resolution";
3408
+ function children3(node) {
3409
+ return node.namedChildren.filter((child) => child !== null);
3410
+ }
3411
+ function receiverOf(node) {
3412
+ return node.type === "call" ? field(node, "receiver") : null;
3413
+ }
3414
+ function methodOf(node) {
3415
+ return field(node, "method")?.text ?? "";
3416
+ }
3417
+ function rootConstant(node) {
3418
+ const receiver = receiverOf(node);
3419
+ if (receiver === null) {
3420
+ return null;
3421
+ }
3422
+ if (receiver.type === "constant" || receiver.type === "scope_resolution") {
3423
+ return receiver;
3424
+ }
3425
+ return rootConstant(receiver);
2819
3426
  }
2820
3427
  var ANCESTRY_PROGRAM = deriveOnDemand2(
2821
3428
  [...RESOLUTION_RULES2, ...RESOLUTION_QUESTIONS2],
@@ -3012,6 +3619,16 @@ async function discoverUnits(root, options) {
3012
3619
  file: options.absoluteFile ?? options.filePath
3013
3620
  }));
3014
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
+ }
3015
3632
  for (const info of classes) {
3016
3633
  const ownBlocks = fileBlocks.filter(
3017
3634
  (block) => block.info.qualifiedName === info.qualifiedName
@@ -3093,7 +3710,23 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3093
3710
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
3094
3711
  return [];
3095
3712
  }
3713
+ const filters = controllerFilters(pattern, ancestry);
3096
3714
  const units = [];
3715
+ for (const filter of filters) {
3716
+ const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
3717
+ const raw = filterUnit(
3718
+ filter,
3719
+ pattern,
3720
+ displayPath,
3721
+ bodyOfMethod(filter.method, options)
3722
+ );
3723
+ units.push(raw);
3724
+ options.onReachSeed?.(raw, {
3725
+ file: filter.file,
3726
+ node: filter.method,
3727
+ enclosingQualifiedName: filter.enclosingQualifiedName
3728
+ });
3729
+ }
3097
3730
  for (const block of ownBlocks) {
3098
3731
  if (block.info.bodyNode === null) {
3099
3732
  continue;
@@ -3105,6 +3738,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3105
3738
  if ((visibility.get(actionName) ?? "public") !== "public") {
3106
3739
  continue;
3107
3740
  }
3741
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3742
+ (filter) => filterReference(
3743
+ filter,
3744
+ options.displayPathOf?.(filter.file) ?? filter.file
3745
+ )
3746
+ );
3108
3747
  const raw = buildControllerActionUnit(
3109
3748
  pack,
3110
3749
  pattern,
@@ -3112,7 +3751,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3112
3751
  actionName,
3113
3752
  method,
3114
3753
  options.filePath,
3115
- options
3754
+ options,
3755
+ around
3116
3756
  );
3117
3757
  units.push(raw);
3118
3758
  options.onReachSeed?.(raw, {
@@ -3124,7 +3764,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3124
3764
  }
3125
3765
  return units;
3126
3766
  }
3127
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead) {
3767
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, bodyRead, wrappers = []) {
3128
3768
  const range = rangeOf(method);
3129
3769
  const route = pattern.routeFor(controllerQualifiedName, actionName);
3130
3770
  const body = bodyOfMethod(method, bodyRead);
@@ -3145,7 +3785,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3145
3785
  exportName: actionName,
3146
3786
  exportPath: [controllerQualifiedName, actionName]
3147
3787
  },
3148
- boundaryBinding: route === null ? null : restBinding({
3788
+ boundaryBinding: route === null ? null : restBinding2({
3149
3789
  transport: pack.protocol,
3150
3790
  method: route.method,
3151
3791
  path: route.path,
@@ -3179,7 +3819,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
3179
3819
  ],
3180
3820
  bodyContent: body.bodyContent ?? "absent",
3181
3821
  dependencyCalls: [],
3182
- declaredContract: null
3822
+ declaredContract: null,
3823
+ ...wrappers.length > 0 ? { wrappers: [...wrappers] } : {}
3183
3824
  };
3184
3825
  }
3185
3826
  function routingGapUnit(pattern, gaps) {
@@ -3262,7 +3903,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3262
3903
  const storage = bodyRead.storage;
3263
3904
  const extra = [
3264
3905
  ...envReadEffects(method),
3265
- ...storage === void 0 ? [] : storageEffects(callsUnder(method), storage)
3906
+ ...storage === void 0 ? [] : storageEffects(callsUnder2(method), storage)
3266
3907
  ];
3267
3908
  return {
3268
3909
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -3271,7 +3912,7 @@ function bodyOfMethod(method, bodyRead = {}) {
3271
3912
  ...extra.length > 0 ? { extraEffects: extra } : {}
3272
3913
  };
3273
3914
  }
3274
- function callsUnder(node, found = []) {
3915
+ function callsUnder2(node, found = []) {
3275
3916
  for (const child of node.namedChildren) {
3276
3917
  if (child === null) {
3277
3918
  continue;
@@ -3279,7 +3920,7 @@ function callsUnder(node, found = []) {
3279
3920
  if (child.type === "call") {
3280
3921
  found.push(child);
3281
3922
  }
3282
- callsUnder(child, found);
3923
+ callsUnder2(child, found);
3283
3924
  }
3284
3925
  return found;
3285
3926
  }
@@ -3786,6 +4427,7 @@ import {
3786
4427
  import { Database as Database2 } from "@suss/datalog";
3787
4428
  import {
3788
4429
  assembleSummary as assembleSummary2,
4430
+ composeWrappers,
3789
4431
  createCacheLayer,
3790
4432
  createTimer,
3791
4433
  moduleInitStructure,
@@ -4375,6 +5017,14 @@ function inheritedMethodsIn(packs) {
4375
5017
  }
4376
5018
  return found;
4377
5019
  }
5020
+ function alreadyDiscovered(seen, raw) {
5021
+ const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
5022
+ if (seen.has(key)) {
5023
+ return true;
5024
+ }
5025
+ seen.add(key);
5026
+ return false;
5027
+ }
4378
5028
  async function extractRubyProject(options) {
4379
5029
  const timer = options.onTiming !== void 0 ? createTimer() : noopTimer();
4380
5030
  const cacheDir = adapterStamp.declineWhenRunFromSource(
@@ -4436,6 +5086,7 @@ async function extractRubyProject(options) {
4436
5086
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? path4.relative(options.workspaceRoot, file) : file;
4437
5087
  const seeds = [];
4438
5088
  const summariesBySeed = /* @__PURE__ */ new Map();
5089
+ const discovered = /* @__PURE__ */ new Set();
4439
5090
  for (const { file, root } of parsed) {
4440
5091
  const displayPath = displayPathOf(file);
4441
5092
  const seedByRaw = /* @__PURE__ */ new Map();
@@ -4448,10 +5099,15 @@ async function extractRubyProject(options) {
4448
5099
  cache,
4449
5100
  ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
4450
5101
  inheritedMethods,
5102
+ displayPathOf,
5103
+ facts: db,
4451
5104
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
4452
5105
  })
4453
5106
  );
4454
5107
  for (const raw of rawUnits) {
5108
+ if (alreadyDiscovered(discovered, raw)) {
5109
+ continue;
5110
+ }
4455
5111
  const summary = timer.time(
4456
5112
  "summarize",
4457
5113
  () => assembleSummary2(raw, { gapHandling: "permissive" })
@@ -4554,12 +5210,18 @@ async function extractRubyProject(options) {
4554
5210
  }
4555
5211
  disambiguateSummaryIds(summaries);
4556
5212
  linkCallsToSummaries(summaries);
5213
+ const composed = timer.time(
5214
+ "summarize",
5215
+ () => composeWrappers(summaries, {
5216
+ gapHandling: options.gapHandling ?? "permissive"
5217
+ })
5218
+ );
4557
5219
  await timer.timeAsync("cache.write", async () => {
4558
- if (cacheDir === null || summaries.length === 0) {
5220
+ if (cacheDir === null || composed.length === 0) {
4559
5221
  return;
4560
5222
  }
4561
5223
  try {
4562
- await extractionCache.write(cacheInput, summaries);
5224
+ await extractionCache.write(cacheInput, composed);
4563
5225
  } catch {
4564
5226
  }
4565
5227
  });
@@ -4568,11 +5230,11 @@ async function extractRubyProject(options) {
4568
5230
  packs: options.packs,
4569
5231
  tallies,
4570
5232
  filesWalked: options.files.length,
4571
- summaries
5233
+ summaries: composed
4572
5234
  })
4573
5235
  );
4574
5236
  options.onTiming?.(timer.report());
4575
- return { summaries, facts: db };
5237
+ return { summaries: composed, facts: db };
4576
5238
  }
4577
5239
  function fileDependenciesOf(db, displayPathOf) {
4578
5240
  const byFile = /* @__PURE__ */ new Map();