@suss/adapter-ruby 0.35.1 → 0.36.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
@@ -785,6 +785,8 @@ var INVOKE_METHOD = "call";
785
785
  var BLOCK_WRAPPERS = /* @__PURE__ */ new Set(["proc", "lambda"]);
786
786
  var PROC_CLASS = "Proc";
787
787
  var CONSTRUCTOR = "new";
788
+ var INITIALIZE_METHOD = "initialize";
789
+ var CLASS_OF_INSTANCE = "class";
788
790
 
789
791
  // src/facts/resolve.ts
790
792
  var import_resolution2 = require("@suss/resolution");
@@ -812,6 +814,26 @@ var RUBY_RULES = (0, import_resolution.alsoSteps)([
812
814
  (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("o"), (0, import_datalog.constant)(CONSTRUCTOR)),
813
815
  (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c"))
814
816
  ]
817
+ ),
818
+ // `new` in a class method makes one of the class the method runs on,
819
+ // and `self.class` in an instance method is that class.
820
+ (0, import_datalog.rule)(
821
+ "selfBehind",
822
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("s"), import_resolution.SELF_AS_INSTANCE],
823
+ [
824
+ (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
825
+ (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("o"), (0, import_datalog.constant)(CONSTRUCTOR)),
826
+ (0, import_datalog.lit)("selfBehind", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("s"), import_resolution.SELF_AS_CLASS)
827
+ ]
828
+ ),
829
+ (0, import_datalog.rule)(
830
+ "selfBehind",
831
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("s"), import_resolution.SELF_AS_CLASS],
832
+ [
833
+ (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
834
+ (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("o"), (0, import_datalog.constant)(CLASS_OF_INSTANCE)),
835
+ (0, import_datalog.lit)("selfBehind", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("s"), import_resolution.SELF_AS_INSTANCE)
836
+ ]
815
837
  )
816
838
  ]);
817
839
  var RUBY_PROGRAM = (0, import_resolution.resolutionProgram)(RUBY_RULES);
@@ -2541,9 +2563,8 @@ function emitCandidates(emitter, key, group) {
2541
2563
  add(emitter, "writesAllStated", key);
2542
2564
  }
2543
2565
  }
2544
- var INITIALIZE_METHOD = "initialize";
2545
- function receiverKeyOf(emitter, method, classKey) {
2546
- return definesClassMethod(method, emitter.bodyBlocks) ? classKey : `${nodeId(emitter.filePath, method)}#self`;
2566
+ function receiverKeyOf(file, method) {
2567
+ return `${nodeId(file, method)}#self`;
2547
2568
  }
2548
2569
  var CONSTANT_REF_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
2549
2570
  function mixedInConstants(body, callName, statements) {
@@ -2563,11 +2584,20 @@ function emitMixinFacts(emitter, classKey, body) {
2563
2584
  }
2564
2585
  function emitMethodOfClass(within, classKey, method, offeredOnClass) {
2565
2586
  const stored = /* @__PURE__ */ new Map();
2566
- const receiverKey = receiverKeyOf(within, method, classKey);
2567
- const onInstance = receiverKey !== classKey;
2587
+ const receiverKey = receiverKeyOf(within.filePath, method);
2588
+ const onInstance = !definesClassMethod(method, within.bodyBlocks);
2568
2589
  if (onInstance) {
2569
2590
  add(within, "instanceOf", receiverKey, classKey);
2591
+ } else {
2592
+ add(within, "binds", receiverKey, classKey);
2570
2593
  }
2594
+ add(
2595
+ within,
2596
+ "selfReceiver",
2597
+ nodeId(within.filePath, method),
2598
+ receiverKey,
2599
+ onInstance ? import_resolution4.SELF_AS_INSTANCE_NAME : import_resolution4.SELF_AS_CLASS_NAME
2600
+ );
2571
2601
  const funcKey = emitMethodFacts(
2572
2602
  {
2573
2603
  ...within,
@@ -4250,7 +4280,10 @@ async function definedInFileOf(fileConstant, qualifiedName, lookup) {
4250
4280
  if (filePath === null) {
4251
4281
  return null;
4252
4282
  }
4253
- const fileRoot = await lookup.parsedFile(filePath);
4283
+ return blocksDefinedIn(filePath, qualifiedName, lookup.parsedFile);
4284
+ }
4285
+ async function blocksDefinedIn(filePath, qualifiedName, parsedFile) {
4286
+ const fileRoot = await parsedFile(filePath);
4254
4287
  if (fileRoot === null) {
4255
4288
  return null;
4256
4289
  }
@@ -4330,9 +4363,16 @@ function unreadConstant(candidates) {
4330
4363
  const bare = candidates.at(-1);
4331
4364
  return bare === void 0 ? [] : [{ type: "unfollowed", name: bare }];
4332
4365
  }
4333
- function definitionOf(qualifiedName, lookup) {
4366
+ async function definitionOf(qualifiedName, lookup) {
4334
4367
  const local = lookup.localDefinition?.(qualifiedName);
4335
- return local !== void 0 && local !== null ? Promise.resolve(local) : reachDefinition(qualifiedName, lookup);
4368
+ if (local !== void 0 && local !== null) {
4369
+ return local;
4370
+ }
4371
+ const inRun = await lookup.definedInRun?.(qualifiedName);
4372
+ if (inRun !== void 0 && inRun !== null) {
4373
+ return inRun;
4374
+ }
4375
+ return reachDefinition(qualifiedName, lookup);
4336
4376
  }
4337
4377
  async function mixinChain(self, callName, lookup, active, present) {
4338
4378
  let chain = [];
@@ -4473,7 +4513,69 @@ function inheritedStatements(ancestry, blockConfigures) {
4473
4513
 
4474
4514
  // src/discovery.ts
4475
4515
  var import_behavioral_ir6 = require("@suss/behavioral-ir");
4476
- var import_extractor11 = require("@suss/extractor");
4516
+ var import_extractor12 = require("@suss/extractor");
4517
+
4518
+ // src/baseClass.ts
4519
+ var import_resolution7 = require("@suss/resolution");
4520
+ function classBehind(facts, file, constant2) {
4521
+ const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
4522
+ const bound = new Set(
4523
+ facts.lookup("binds", 0, key).map((row) => String(row[1]))
4524
+ );
4525
+ return bound.size === 1 ? [...bound][0] : void 0;
4526
+ }
4527
+ function classSettledOn(facts, key) {
4528
+ (0, import_resolution7.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
4529
+ const objects = (0, import_resolution7.answersFor)(facts, "wantedObjectOf", key);
4530
+ return objects.length === 1 ? objects[0] : void 0;
4531
+ }
4532
+ function declaredName(facts, classKey) {
4533
+ const names = new Set(
4534
+ facts.lookup("rbConstantName", 0, classKey).map((row) => String(row[1]))
4535
+ );
4536
+ return names.size === 1 ? [...names][0] : void 0;
4537
+ }
4538
+ function classesDeclaredInRun(facts, parsedFile) {
4539
+ const known = /* @__PURE__ */ new Map();
4540
+ const read = async (qualifiedName) => {
4541
+ const files = /* @__PURE__ */ new Set();
4542
+ for (const row of facts.lookup("rbConstantName", 1, qualifiedName)) {
4543
+ const span = (0, import_resolution7.spanOfKey)(String(row[0]));
4544
+ if (span !== null) {
4545
+ files.add(span.file);
4546
+ }
4547
+ }
4548
+ const blocks = [];
4549
+ for (const file of files) {
4550
+ blocks.push(
4551
+ ...await blocksDefinedIn(file, qualifiedName, parsedFile) ?? []
4552
+ );
4553
+ }
4554
+ return blocks.length === 0 ? null : blocks;
4555
+ };
4556
+ return (qualifiedName) => {
4557
+ let found = known.get(qualifiedName);
4558
+ if (found === void 0) {
4559
+ found = read(qualifiedName);
4560
+ known.set(qualifiedName, found);
4561
+ }
4562
+ return found;
4563
+ };
4564
+ }
4565
+ function reachesBase(facts, classKey, bases) {
4566
+ (0, import_resolution7.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4567
+ return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4568
+ }
4569
+ function enclosingClassKey(node, file) {
4570
+ let current = node.parent;
4571
+ while (current !== null) {
4572
+ if (NESTING_TYPES.has(current.type)) {
4573
+ return nodeId(file, current);
4574
+ }
4575
+ current = current.parent;
4576
+ }
4577
+ return null;
4578
+ }
4477
4579
 
4478
4580
  // src/clientCalls.ts
4479
4581
  var import_behavioral_ir2 = require("@suss/behavioral-ir");
@@ -4481,7 +4583,7 @@ var import_values14 = require("@suss/values");
4481
4583
 
4482
4584
  // src/paths/effects.ts
4483
4585
  var import_extractor2 = require("@suss/extractor");
4484
- var import_values7 = require("@suss/values");
4586
+ var import_values8 = require("@suss/values");
4485
4587
 
4486
4588
  // src/paths/lowering.ts
4487
4589
  var RAISE_NAMES = /* @__PURE__ */ new Set(["raise", "fail"]);
@@ -4692,6 +4794,7 @@ var Lowerer = class {
4692
4794
  );
4693
4795
  return {
4694
4796
  kind: "try",
4797
+ lines: rangeOf(node),
4695
4798
  tryBody: tryBody.map((statement) => this.lower(statement)),
4696
4799
  catchBody: rescues2.length > 0 ? rescues2.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4697
4800
  finallyBody: ensureClause === null ? null : this.lowerBlock(ensureClause),
@@ -4783,7 +4886,8 @@ var Lowerer = class {
4783
4886
  kind: "loop",
4784
4887
  condition: handleOf(header),
4785
4888
  body: this.lowerBlock(field(node, "body")),
4786
- exitKind: exitKindOf(node)
4889
+ exitKind: exitKindOf(node),
4890
+ lines: rangeOf(node)
4787
4891
  },
4788
4892
  [header]
4789
4893
  );
@@ -4799,7 +4903,8 @@ var Lowerer = class {
4799
4903
  kind: "loop",
4800
4904
  condition: callHeaderOf(node, attachedBlock),
4801
4905
  body: this.lowerBlock(field(attachedBlock, "body") ?? attachedBlock),
4802
- exitKind: exitKindOf(node)
4906
+ exitKind: exitKindOf(node),
4907
+ lines: rangeOf(node)
4803
4908
  },
4804
4909
  [node]
4805
4910
  );
@@ -4843,12 +4948,15 @@ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
4843
4948
  "block"
4844
4949
  ]);
4845
4950
  function literalArgOf(node, facts) {
4951
+ if (node.type === "nil") {
4952
+ return { kind: "null" };
4953
+ }
4846
4954
  const value = evaluatedValue(node, facts);
4847
- const text3 = (0, import_values7.literalOf)(value);
4955
+ const text3 = (0, import_values8.literalOf)(value);
4848
4956
  if (text3 !== null) {
4849
4957
  return { kind: "string", value: text3 };
4850
4958
  }
4851
- const constant2 = (0, import_values7.constantOf)(value);
4959
+ const constant2 = (0, import_values8.constantOf)(value);
4852
4960
  if (typeof constant2 === "number") {
4853
4961
  return { kind: "number", value: constant2 };
4854
4962
  }
@@ -4980,13 +5088,21 @@ function argsOf(call, facts) {
4980
5088
  function enclosingStatement(call, body) {
4981
5089
  let current = call;
4982
5090
  while (current.parent !== null) {
4983
- if (BLOCK_TYPES5.has(current.parent.type) || current.parent.id === body.id) {
5091
+ if (listsStatements(current.parent, body)) {
4984
5092
  return current;
4985
5093
  }
4986
5094
  current = current.parent;
4987
5095
  }
4988
5096
  return current;
4989
5097
  }
5098
+ function listsStatements(node, body) {
5099
+ if (BLOCK_TYPES5.has(node.type) || node.id === body.id) {
5100
+ return true;
5101
+ }
5102
+ const begin = node.type === "ensure" ? node.parent : node;
5103
+ const around = begin?.type === "begin" ? begin.parent : null;
5104
+ return around !== null && around !== void 0 && (BLOCK_TYPES5.has(around.type) || around.id === body.id);
5105
+ }
4990
5106
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4991
5107
  return invocationSites(
4992
5108
  definitionNode,
@@ -5011,36 +5127,13 @@ function moduleScopeInvocationEffects(root, inherited = NO_INHERITED_METHODS, ke
5011
5127
  ).map((site) => site.effect);
5012
5128
  }
5013
5129
  function effectsOfBody(read, inherited, keepsArglessCall, facts) {
5014
- const body = read.body;
5015
5130
  const calls = callsReported(bodyCalls(read, inherited), keepsArglessCall);
5016
5131
  if (calls.length === 0) {
5017
5132
  return [];
5018
5133
  }
5019
- const statementOf2 = /* @__PURE__ */ new Map();
5020
- const byStatement = new NodeMap();
5021
- const statements = [];
5022
- for (const call of calls) {
5023
- const written = enclosingStatement(call, body);
5024
- const statement = byStatement.get(written);
5025
- if (statement === void 0) {
5026
- byStatement.set(written, written);
5027
- statements.push(written);
5028
- }
5029
- statementOf2.set(call.id, statement ?? written);
5030
- }
5031
- const lowered = lowerRubyBody(body, statements);
5032
- const enumerated = (0, import_extractor2.enumerateOrDegrade)(
5033
- {
5034
- statements: lowered.statements,
5035
- terminalsByStmt: lowered.terminalsByStmt
5036
- },
5037
- statements
5038
- );
5134
+ const sites = sitesIn(read.body, calls);
5039
5135
  return calls.map((call) => {
5040
- const statement = statementOf2.get(call.id);
5041
- const conditions = (0, import_extractor2.sharedGatingConditions)(
5042
- statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
5043
- );
5136
+ const conditions = (0, import_extractor2.sharedGatingConditions)(sites.pathsTo(call));
5044
5137
  return {
5045
5138
  call,
5046
5139
  effect: {
@@ -5048,11 +5141,26 @@ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
5048
5141
  callee: calleeText(call),
5049
5142
  args: argsOf(call, facts),
5050
5143
  async: false,
5051
- ...conditions.length > 0 ? { preconditions: conditions } : {}
5052
- }
5144
+ ...conditions.length > 0 ? { preconditions: conditions } : {},
5145
+ location: rangeOf(call)
5146
+ },
5147
+ site: sites.siteOf(call, conditions)
5053
5148
  };
5054
5149
  });
5055
5150
  }
5151
+ function sitesIn(body, nodes) {
5152
+ return (0, import_extractor2.statementSites)(nodes, {
5153
+ statementOf: (node) => enclosingStatement(node, body),
5154
+ lower: (statements) => lowerRubyBody(body, statements),
5155
+ firstLine: (node) => rangeOf(node).start
5156
+ });
5157
+ }
5158
+ function effectSitesIn(read, nodes) {
5159
+ if (read === null || nodes.length === 0) {
5160
+ return (node) => ({ line: rangeOf(node).start });
5161
+ }
5162
+ return sitesIn(read.body, nodes).plainSiteOf;
5163
+ }
5056
5164
 
5057
5165
  // src/paths/predicates.ts
5058
5166
  var OPERATORS = {
@@ -5439,15 +5547,10 @@ function raiseTerminal(call) {
5439
5547
  }
5440
5548
  function throughResponseHelpers(branches, sites, options) {
5441
5549
  const handOffs = [];
5442
- for (const { call, effect } of sites) {
5550
+ for (const { call, site } of sites) {
5443
5551
  const helper = helperBranchesAt(call, options);
5444
5552
  if (helper !== null) {
5445
- handOffs.push({
5446
- helper,
5447
- preconditions: effect.preconditions,
5448
- line: rangeOf(call).start,
5449
- at: rangeOf(call)
5450
- });
5553
+ handOffs.push({ ...site, helper, at: rangeOf(call) });
5451
5554
  }
5452
5555
  }
5453
5556
  return handOffs.length === 0 ? [...branches] : branches.flatMap((branch) => (0, import_extractor3.splitAtHandOffs)(branch, handOffs));
@@ -5519,30 +5622,6 @@ var import_values11 = require("@suss/values");
5519
5622
  var import_ir_core2 = require("@suss/ir-core");
5520
5623
  var import_resolution8 = require("@suss/resolution");
5521
5624
 
5522
- // src/baseClass.ts
5523
- var import_resolution7 = require("@suss/resolution");
5524
- function classBehind(facts, file, constant2) {
5525
- const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
5526
- const bound = new Set(
5527
- facts.lookup("binds", 0, key).map((row) => String(row[1]))
5528
- );
5529
- return bound.size === 1 ? [...bound][0] : void 0;
5530
- }
5531
- function reachesBase(facts, classKey, bases) {
5532
- (0, import_resolution7.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
5533
- return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
5534
- }
5535
- function enclosingClassKey(node, file) {
5536
- let current = node.parent;
5537
- while (current !== null) {
5538
- if (NESTING_TYPES.has(current.type)) {
5539
- return nodeId(file, current);
5540
- }
5541
- current = current.parent;
5542
- }
5543
- return null;
5544
- }
5545
-
5546
5625
  // src/loaders.ts
5547
5626
  function receiverOf(node) {
5548
5627
  return node.type === "call" ? field(node, "receiver") : null;
@@ -5686,7 +5765,8 @@ function effectOf(call, store, address, access) {
5686
5765
  fields: access.fields,
5687
5766
  operation,
5688
5767
  ...access.selector.length > 0 ? { selector: access.selector } : {}
5689
- }
5768
+ },
5769
+ location: rangeOf(call)
5690
5770
  };
5691
5771
  }
5692
5772
  function addressBehind(receiver, pattern, options, hops) {
@@ -5997,20 +6077,10 @@ function storageEffect(call, container, pattern, kind, selector, fields) {
5997
6077
  fields,
5998
6078
  operation,
5999
6079
  ...selector.length > 0 ? { selector } : {}
6000
- }
6080
+ },
6081
+ location: rangeOf(call)
6001
6082
  };
6002
6083
  }
6003
- function classSettledOn(facts, key) {
6004
- (0, import_resolution8.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
6005
- const objects = (0, import_resolution8.answersFor)(facts, "wantedObjectOf", key);
6006
- return objects.length === 1 ? objects[0] : void 0;
6007
- }
6008
- function declaredName(facts, classKey) {
6009
- const names = new Set(
6010
- facts.lookup("rbConstantName", 0, classKey).map((row) => String(row[1]))
6011
- );
6012
- return names.size === 1 ? [...names][0] : void 0;
6013
- }
6014
6084
  function projectDeclares(facts, classKey, method) {
6015
6085
  const spellings = /* @__PURE__ */ new Set([method, (0, import_resolution8.classMemberName)(method)]);
6016
6086
  return facts.lookup("wantedDeclaredName", 0, classKey).some((row) => spellings.has(String(row[1])));
@@ -6067,12 +6137,13 @@ function callbacksRunBy(facts, classKey, pattern, writeMethod) {
6067
6137
  }
6068
6138
  return [...found.values()];
6069
6139
  }
6070
- function callbackEffect(callback) {
6140
+ function callbackEffect(callback, write) {
6071
6141
  return {
6072
6142
  type: "invocation",
6073
6143
  callee: callback.name,
6074
6144
  args: [],
6075
- async: false
6145
+ async: false,
6146
+ location: rangeOf(write)
6076
6147
  };
6077
6148
  }
6078
6149
  function modelCallEffects(call, file, options, enclosing) {
@@ -6111,7 +6182,7 @@ function modelCallEffects(call, file, options, enclosing) {
6111
6182
  options.statesSlots(access, slots);
6112
6183
  }
6113
6184
  }
6114
- return [access, ...ran.map(callbackEffect)];
6185
+ return [access, ...ran.map((callback) => callbackEffect(callback, worked))];
6115
6186
  }
6116
6187
  return [];
6117
6188
  }
@@ -6249,11 +6320,12 @@ function storageEffects(calls, file, options, enclosing = null) {
6249
6320
  "wanted",
6250
6321
  RUBY_PROGRAM
6251
6322
  );
6252
- const effects = [];
6253
- for (const call of chains) {
6254
- effects.push(...effectsOfCall(call, file, options, enclosing));
6255
- }
6256
- return effects;
6323
+ return chains.flatMap(
6324
+ (call) => effectsOfCall(call, file, options, enclosing).map((effect) => ({
6325
+ effect,
6326
+ at: call
6327
+ }))
6328
+ );
6257
6329
  }
6258
6330
 
6259
6331
  // src/provenance.ts
@@ -6642,13 +6714,13 @@ function readingOfCall(call, declaration, names, options) {
6642
6714
  }
6643
6715
  return (0, import_extractor6.writtenReading)(status, rangeOf(argument));
6644
6716
  }
6645
- function effectsReaching(effects, conditions) {
6646
- return effects.filter(
6647
- (effect) => (0, import_extractor6.guardsHoldOn)(
6648
- effect.type === "invocation" ? effect.preconditions : void 0,
6649
- conditions
6650
- )
6651
- );
6717
+ function withEffectsThatRun(branch, body) {
6718
+ const extra = (0, import_extractor6.effectsFiringOn)(body.extraEffects ?? [], branch);
6719
+ return {
6720
+ ...branch,
6721
+ effects: (0, import_extractor6.effectsFiringOn)(body.effects, branch),
6722
+ ...extra.length === 0 ? {} : { extraEffects: extra }
6723
+ };
6652
6724
  }
6653
6725
  function conditionOf(condition, inputs) {
6654
6726
  return {
@@ -6708,57 +6780,66 @@ function helperResponse(helper, pattern, facts, boundStatuses = /* @__PURE__ */
6708
6780
  rangeOf(helper)
6709
6781
  );
6710
6782
  }
6711
- function branchOf(outcome, pattern, effects, extraEffects, options) {
6783
+ function branchOf(outcome, pattern, body, options) {
6712
6784
  if (outcome.fellThrough === true && options.fallthrough === "handOn") {
6713
- return handsOnBranch(outcome, effects, extraEffects);
6785
+ return handsOnBranch(outcome, body);
6714
6786
  }
6715
- return {
6716
- conditions: outcome.conditions,
6717
- terminal: {
6718
- kind: "response",
6719
- statusCode: null,
6720
- body: null,
6721
- exceptionType: null,
6722
- message: null,
6723
- component: null,
6724
- renderTree: null,
6725
- delegateTarget: null,
6726
- emitEvent: null,
6727
- location: outcome.location
6728
- },
6729
- statusCodeReading: {
6730
- reading: outcome.reading,
6731
- libraryDefault: pattern.defaultStatusCode
6787
+ return withEffectsThatRun(
6788
+ {
6789
+ conditions: outcome.conditions,
6790
+ terminal: {
6791
+ kind: "response",
6792
+ statusCode: null,
6793
+ body: null,
6794
+ exceptionType: null,
6795
+ message: null,
6796
+ component: null,
6797
+ renderTree: null,
6798
+ delegateTarget: null,
6799
+ emitEvent: null,
6800
+ location: outcome.location
6801
+ },
6802
+ statusCodeReading: {
6803
+ reading: outcome.reading,
6804
+ libraryDefault: pattern.defaultStatusCode
6805
+ },
6806
+ ...outcome.body === void 0 ? {} : { bodyShapeReading: { reading: outcome.body } },
6807
+ effects: [],
6808
+ location: outcome.location,
6809
+ ...loopAroundOf(outcome),
6810
+ isDefault: outcome.conditions.length === 0
6732
6811
  },
6733
- ...outcome.body === void 0 ? {} : { bodyShapeReading: { reading: outcome.body } },
6734
- effects: effectsReaching(effects, outcome.conditions),
6735
- ...extraEffects === void 0 ? {} : { extraEffects },
6736
- location: outcome.location,
6737
- isDefault: outcome.conditions.length === 0
6738
- };
6812
+ body
6813
+ );
6739
6814
  }
6740
- function handsOnBranch(outcome, effects, extraEffects) {
6741
- return {
6742
- conditions: outcome.conditions,
6743
- terminal: {
6744
- kind: "delegate",
6745
- statusCode: null,
6746
- body: null,
6747
- exceptionType: null,
6748
- message: null,
6749
- component: null,
6750
- renderTree: null,
6751
- delegateTarget: null,
6752
- emitEvent: null,
6753
- location: outcome.location
6815
+ function handsOnBranch(outcome, body) {
6816
+ return withEffectsThatRun(
6817
+ {
6818
+ conditions: outcome.conditions,
6819
+ terminal: {
6820
+ kind: "delegate",
6821
+ statusCode: null,
6822
+ body: null,
6823
+ exceptionType: null,
6824
+ message: null,
6825
+ component: null,
6826
+ renderTree: null,
6827
+ delegateTarget: null,
6828
+ emitEvent: null,
6829
+ location: outcome.location
6830
+ },
6831
+ effects: [],
6832
+ location: outcome.location,
6833
+ ...loopAroundOf(outcome),
6834
+ isDefault: outcome.conditions.length === 0
6754
6835
  },
6755
- effects: effectsReaching(effects, outcome.conditions),
6756
- ...extraEffects === void 0 ? {} : { extraEffects },
6757
- location: outcome.location,
6758
- isDefault: outcome.conditions.length === 0
6759
- };
6836
+ body
6837
+ );
6760
6838
  }
6761
- function returnPathBranches(method, effects, exits = [], facts, keepsArms, raises2 = []) {
6839
+ function loopAroundOf(placed) {
6840
+ return placed.loopAround === void 0 ? {} : { loopAround: placed.loopAround };
6841
+ }
6842
+ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raises2 = [], endingOfReturn = (_statement, location) => returnTerminal(location)) {
6762
6843
  const body = field(method, "body");
6763
6844
  if (body === null) {
6764
6845
  return null;
@@ -6785,30 +6866,42 @@ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raise
6785
6866
  facts
6786
6867
  );
6787
6868
  const branches = [];
6788
- const push = (paths, terminal) => {
6869
+ const push = (paths, terminal, at) => {
6870
+ const placement = at === null ? void 0 : enumerated.placement.get(at);
6789
6871
  for (const path7 of paths) {
6790
6872
  const conditions = path7.map(
6791
6873
  (condition) => conditionOf(condition, inputs)
6792
6874
  );
6793
- branches.push({
6794
- conditions,
6795
- terminal,
6796
- effects: effectsReaching(effects, conditions),
6797
- location: terminal.location,
6798
- isDefault: conditions.length === 0
6799
- });
6875
+ branches.push(
6876
+ withEffectsThatRun(
6877
+ {
6878
+ conditions,
6879
+ terminal,
6880
+ effects: [],
6881
+ location: terminal.location,
6882
+ ...loopAroundOf({ loopAround: placement?.loopAround }),
6883
+ isDefault: conditions.length === 0
6884
+ },
6885
+ { effects }
6886
+ )
6887
+ );
6800
6888
  }
6801
6889
  };
6802
6890
  for (const statement of returns) {
6803
6891
  push(
6804
6892
  enumerated.byTerminal.get(statement) ?? [],
6805
- returnTerminal(rangeOf(statement))
6893
+ endingOfReturn(statement, rangeOf(statement)),
6894
+ statement
6806
6895
  );
6807
6896
  }
6808
6897
  for (const ending of [...exits, ...raises2]) {
6809
- push(enumerated.byTerminal.get(ending.call) ?? [], ending.terminal);
6898
+ push(
6899
+ enumerated.byTerminal.get(ending.call) ?? [],
6900
+ ending.terminal,
6901
+ ending.call
6902
+ );
6810
6903
  }
6811
- push(enumerated.fallthrough, returnTerminal(rangeOf(method)));
6904
+ push(enumerated.fallthrough, endingOfReturn(null, rangeOf(method)), null);
6812
6905
  return branches.length === 0 ? null : branches;
6813
6906
  }
6814
6907
  function returnTerminal(location) {
@@ -6868,12 +6961,14 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
6868
6961
  const responder = responderOf(terminal, responders);
6869
6962
  const reading = responder === void 0 ? import_extractor6.absentReading : readingOfResponder(terminal, responder, statusNames, options);
6870
6963
  const body2 = responder?.kind === "declared" ? bodyReadingOfCall(terminal, responder.declaration, options.facts) : void 0;
6964
+ const loopAround = enumerated.placement.get(terminal)?.loopAround;
6871
6965
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
6872
6966
  outcomes.push({
6873
6967
  conditions: conditionsOf(path7),
6874
6968
  reading,
6875
6969
  ...body2 === void 0 ? {} : { body: body2 },
6876
6970
  location: rangeOf(terminal),
6971
+ loopAround,
6877
6972
  // A bare `return` wrote no response, so a filter that takes it
6878
6973
  // hands the request on the same as one that reaches its end.
6879
6974
  ...responder === void 0 ? { fellThrough: true } : {}
@@ -6889,6 +6984,7 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
6889
6984
  });
6890
6985
  }
6891
6986
  const read = options.raises;
6987
+ const placed = { effects, extraEffects };
6892
6988
  const raised = raises2.flatMap((call) => {
6893
6989
  const terminal = raisedTerminal(
6894
6990
  pattern,
@@ -6896,25 +6992,26 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
6896
6992
  read?.handlers ?? [],
6897
6993
  rangeOf(call)
6898
6994
  );
6899
- return (enumerated.byTerminal.get(call) ?? []).map((path7) => {
6900
- const conditions = conditionsOf(path7);
6901
- return {
6902
- conditions,
6903
- terminal,
6904
- effects: effectsReaching(effects, conditions),
6905
- ...extraEffects === void 0 ? {} : { extraEffects },
6906
- location: terminal.location,
6907
- isDefault: false
6908
- };
6909
- });
6995
+ const loopAround = enumerated.placement.get(call)?.loopAround;
6996
+ return (enumerated.byTerminal.get(call) ?? []).map(
6997
+ (path7) => withEffectsThatRun(
6998
+ {
6999
+ conditions: conditionsOf(path7),
7000
+ terminal,
7001
+ effects: [],
7002
+ location: terminal.location,
7003
+ ...loopAroundOf({ loopAround }),
7004
+ isDefault: false
7005
+ },
7006
+ placed
7007
+ )
7008
+ );
6910
7009
  });
6911
7010
  if (outcomes.length === 0 && raised.length === 0) {
6912
7011
  return null;
6913
7012
  }
6914
7013
  return [
6915
- ...outcomes.map(
6916
- (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
6917
- ),
7014
+ ...outcomes.map((outcome) => branchOf(outcome, pattern, placed, options)),
6918
7015
  ...raised
6919
7016
  ];
6920
7017
  }
@@ -7182,7 +7279,7 @@ function callerBranches(method, reading) {
7182
7279
  const keepsArms = (condition) => (0, import_behavioral_ir2.testsStatus)(predicateOf(condition), reading.statusMembers);
7183
7280
  const found = returnPathBranches(
7184
7281
  method,
7185
- effects,
7282
+ sites,
7186
7283
  [],
7187
7284
  reading.facts,
7188
7285
  keepsArms,
@@ -7641,7 +7738,7 @@ function envReadEffects(root, facts) {
7641
7738
  at: (node) => {
7642
7739
  const read = envReadAt(node);
7643
7740
  if (read !== null) {
7644
- slots.push({ read });
7741
+ slots.push({ read, at: node });
7645
7742
  return;
7646
7743
  }
7647
7744
  if (helpers !== null && node.type === "call") {
@@ -7654,9 +7751,12 @@ function envReadEffects(root, facts) {
7654
7751
  const throughHelpers = helpers === null ? new NodeMap() : helperReads(calls, helpers);
7655
7752
  return slots.flatMap((slot) => {
7656
7753
  if ("read" in slot) {
7657
- return [configReadEffect(slot.read)];
7754
+ return [{ effect: configReadEffect(slot.read, slot.at), at: slot.at }];
7658
7755
  }
7659
- return (throughHelpers.get(slot.call) ?? []).map(configReadEffect);
7756
+ return (throughHelpers.get(slot.call) ?? []).map((read) => ({
7757
+ effect: configReadEffect(read, slot.call),
7758
+ at: slot.call
7759
+ }));
7660
7760
  });
7661
7761
  }
7662
7762
  function helperFactsOf(facts) {
@@ -7812,16 +7912,17 @@ function readsAtCall(db, call, args) {
7812
7912
  }
7813
7913
  return [...byName].map(([name, defaulted]) => ({ name, defaulted }));
7814
7914
  }
7815
- function configReadEffect(read) {
7915
+ function configReadEffect(read, at) {
7816
7916
  return {
7817
7917
  type: "interaction",
7818
7918
  binding: (0, import_behavioral_ir3.runtimeConfigBinding)({ recognition: RUBY_ENV_RECOGNITION }),
7819
- callee: `ENV["${read.name}"]`,
7919
+ callee: read.name === null ? at.text : `ENV["${read.name}"]`,
7820
7920
  interaction: {
7821
7921
  class: "config-read",
7822
7922
  name: read.name,
7823
7923
  defaulted: read.defaulted
7824
- }
7924
+ },
7925
+ location: rangeOf(at)
7825
7926
  };
7826
7927
  }
7827
7928
  function envReadAt(node) {
@@ -7829,8 +7930,7 @@ function envReadAt(node) {
7829
7930
  if (site === null) {
7830
7931
  return null;
7831
7932
  }
7832
- const name = stringLiteralValue(site.name);
7833
- return name === null ? null : { name, defaulted: site.defaulted };
7933
+ return { name: stringLiteralValue(site.name), defaulted: site.defaulted };
7834
7934
  }
7835
7935
  function envSiteAt(node) {
7836
7936
  const spelling = envSpellingAt(node);
@@ -8026,25 +8126,27 @@ function filterUnit(filter, pattern, displayPath, body, reading = {}) {
8026
8126
  };
8027
8127
  }
8028
8128
  function handsOn(range, body) {
8029
- return {
8030
- conditions: [],
8031
- terminal: {
8032
- kind: "delegate",
8033
- statusCode: null,
8034
- body: null,
8035
- exceptionType: null,
8036
- message: null,
8037
- component: null,
8038
- renderTree: null,
8039
- delegateTarget: null,
8040
- emitEvent: null,
8041
- location: range
8129
+ return withEffectsThatRun(
8130
+ {
8131
+ conditions: [],
8132
+ terminal: {
8133
+ kind: "delegate",
8134
+ statusCode: null,
8135
+ body: null,
8136
+ exceptionType: null,
8137
+ message: null,
8138
+ component: null,
8139
+ renderTree: null,
8140
+ delegateTarget: null,
8141
+ emitEvent: null,
8142
+ location: range
8143
+ },
8144
+ effects: [],
8145
+ location: range,
8146
+ isDefault: true
8042
8147
  },
8043
- effects: body.effects ?? [],
8044
- ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
8045
- location: range,
8046
- isDefault: true
8047
- };
8148
+ { effects: body.effects ?? [], extraEffects: body.extraEffects }
8149
+ );
8048
8150
  }
8049
8151
  function calledName(statement) {
8050
8152
  if (statement.type !== "call" || field(statement, "receiver") !== null) {
@@ -8181,35 +8283,206 @@ function actionsUnder(node, facts) {
8181
8283
  return found === null ? null : new Set(found);
8182
8284
  }
8183
8285
 
8184
- // src/streamWrites.ts
8185
- var import_behavioral_ir4 = require("@suss/behavioral-ir");
8286
+ // src/resolverHooks.ts
8186
8287
  var import_extractor8 = require("@suss/extractor");
8187
- var RUBY_STREAM_RECOGNITION = "ruby-stdlib";
8188
- var KERNEL_WRITES = /* @__PURE__ */ new Map([
8189
- ["puts", "stdout"],
8190
- ["print", "stdout"],
8191
- ["warn", "stderr"],
8192
- ["abort", "stderr"]
8193
- ]);
8194
- var STREAM_RECEIVERS = /* @__PURE__ */ new Map([
8195
- ["$stdout", "stdout"],
8196
- ["STDOUT", "stdout"],
8197
- ["$stderr", "stderr"],
8198
- ["STDERR", "stderr"]
8199
- ]);
8200
- var STREAM_METHODS = /* @__PURE__ */ new Set(["puts", "print", "write", "<<"]);
8201
- var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
8202
- function streamWriteEffects(root) {
8203
- const found = [];
8204
- (0, import_extractor8.walkDescendants)(root, null, children, {
8205
- at: (node) => {
8206
- const write = node.type === "call" ? streamWriteAt(node) : null;
8207
- if (write !== null) {
8208
- found.push(write);
8209
- }
8210
- },
8211
- into: (node) => DEFERRED_BODY_TYPES2.has(node.type) ? import_extractor8.SKIP_CHILDREN : null
8212
- });
8288
+ var import_values17 = require("@suss/values");
8289
+ function hooksRun(ancestry, hooks, read) {
8290
+ const definitions = [];
8291
+ const unsettled = [];
8292
+ for (const name of hooks.methods) {
8293
+ const found = definitionsRun(ancestry, name, read);
8294
+ definitions.push(...found.definitions);
8295
+ if (found.unsettled !== null) {
8296
+ unsettled.push({ name, reason: found.unsettled });
8297
+ }
8298
+ }
8299
+ return { definitions, unsettled };
8300
+ }
8301
+ function definitionsRun(ancestry, name, read) {
8302
+ const definitions = [];
8303
+ let rest = ancestry;
8304
+ while (rest.length > 0) {
8305
+ const lookup = methodInAncestry(rest, name, read);
8306
+ if (lookup.type !== "found") {
8307
+ return { definitions, unsettled: unsettledReason(lookup) };
8308
+ }
8309
+ definitions.push({ name, method: lookup.method, block: lookup.block });
8310
+ if (!callsSuper(lookup.method)) {
8311
+ break;
8312
+ }
8313
+ const at = rest.findIndex(
8314
+ (entry) => entry.type === "bodies" && entry.blocks.includes(lookup.block)
8315
+ );
8316
+ rest = rest.slice(Math.max(at, 0) + 1);
8317
+ }
8318
+ return { definitions, unsettled: null };
8319
+ }
8320
+ function unsettledReason(lookup) {
8321
+ if (lookup.type !== "unsettled") {
8322
+ return null;
8323
+ }
8324
+ return lookup.cause === "dynamicDefine" || lookup.definedLater === true ? lookup.reason : null;
8325
+ }
8326
+ function callsSuper(method) {
8327
+ let found = false;
8328
+ (0, import_extractor8.walkDescendants)(method, null, children, {
8329
+ at: (node) => {
8330
+ found ||= isSuper(node);
8331
+ },
8332
+ into: (node) => node !== method && METHOD_TYPES.has(node.type) ? import_extractor8.SKIP_CHILDREN : null
8333
+ });
8334
+ return found;
8335
+ }
8336
+ function isSuper(node) {
8337
+ return node.type === "super" || node.type === "call" && field(node, "method")?.type === "super";
8338
+ }
8339
+ function hookUnit(hook, input) {
8340
+ const { body, facts } = input;
8341
+ const range = rangeOf(hook.method);
8342
+ const effects = body.effects ?? [];
8343
+ const branches = (returnPathBranches(
8344
+ hook.method,
8345
+ effects,
8346
+ [],
8347
+ facts,
8348
+ void 0,
8349
+ raiseEndings(hook.method),
8350
+ (statement, location) => hookEnding(hook.method, statement, location, input)
8351
+ ) ?? [emptyBodyBranch(range)]).map(
8352
+ (branch) => withEffectsThatRun(branch, {
8353
+ effects,
8354
+ extraEffects: body.extraEffects
8355
+ })
8356
+ );
8357
+ return {
8358
+ identity: {
8359
+ name: hook.name,
8360
+ nameKind: "binding",
8361
+ kind: "middleware",
8362
+ file: input.displayPath,
8363
+ range,
8364
+ span: spanOf(hook.method),
8365
+ exportName: hook.name,
8366
+ exportPath: [hook.block.info.qualifiedName, hook.name]
8367
+ },
8368
+ boundaryBinding: null,
8369
+ parameters: [],
8370
+ branches: withSlotSources(branches, body.provenance),
8371
+ bodyContent: body.bodyContent ?? "absent",
8372
+ dependencyCalls: [],
8373
+ declaredContract: null
8374
+ };
8375
+ }
8376
+ function hookReference(hook, displayPath) {
8377
+ return {
8378
+ file: displayPath,
8379
+ name: hook.name,
8380
+ line: rangeOf(hook.method).start
8381
+ };
8382
+ }
8383
+ function unsettledHookReading(className, unsettled, range) {
8384
+ return (0, import_extractor8.unreadableReading)(
8385
+ `This field's ${className} could be stopped by a ${unsettled.name} hook ${unsettled.reason}, so whether one runs was not settled here`,
8386
+ range
8387
+ );
8388
+ }
8389
+ function hookEnding(method, statement, location, input) {
8390
+ const values = statement === null ? lastStatement(method) : returnedValues(statement);
8391
+ return stopsTheField(values, input) ? endingTerminal("return", location) : goOn(location);
8392
+ }
8393
+ function lastStatement(method) {
8394
+ const body = field(method, "body");
8395
+ const last = body === null ? void 0 : bodyStatements(body).at(-1);
8396
+ return last === void 0 ? [] : [last];
8397
+ }
8398
+ function returnedValues(statement) {
8399
+ const list = children(statement)[0];
8400
+ if (list === void 0) {
8401
+ return [];
8402
+ }
8403
+ return list.type === "argument_list" ? children(list) : [list];
8404
+ }
8405
+ function stopsTheField(values, input) {
8406
+ const first = values[0];
8407
+ if (first === void 0) {
8408
+ return true;
8409
+ }
8410
+ const listHead = input.hooks.answerIsListHead === true;
8411
+ if (values.length > 1) {
8412
+ return listHead && isFalsy(evaluatedValue(first, input.facts));
8413
+ }
8414
+ const value = evaluatedValue(first, input.facts);
8415
+ if (value.kind !== "sequence") {
8416
+ return isFalsy(value);
8417
+ }
8418
+ const head = value.items[0];
8419
+ return listHead && head !== void 0 && isFalsy(head.value);
8420
+ }
8421
+ function isFalsy(value) {
8422
+ const settled = (0, import_values17.constantOf)(value);
8423
+ return settled === false || settled === null;
8424
+ }
8425
+ function emptyBodyBranch(location) {
8426
+ return {
8427
+ conditions: [],
8428
+ terminal: endingTerminal("return", location),
8429
+ effects: [],
8430
+ location,
8431
+ isDefault: true
8432
+ };
8433
+ }
8434
+ function goOn(location) {
8435
+ return endingTerminal("delegate", location);
8436
+ }
8437
+ function endingTerminal(kind, location) {
8438
+ return {
8439
+ kind,
8440
+ statusCode: null,
8441
+ body: null,
8442
+ exceptionType: null,
8443
+ message: null,
8444
+ component: null,
8445
+ renderTree: null,
8446
+ delegateTarget: null,
8447
+ emitEvent: null,
8448
+ location
8449
+ };
8450
+ }
8451
+
8452
+ // src/streamWrites.ts
8453
+ var import_behavioral_ir4 = require("@suss/behavioral-ir");
8454
+ var import_extractor9 = require("@suss/extractor");
8455
+ var RUBY_STREAM_RECOGNITION = "ruby-stdlib";
8456
+ var KERNEL_WRITES = /* @__PURE__ */ new Map([
8457
+ ["puts", "stdout"],
8458
+ ["print", "stdout"],
8459
+ ["warn", "stderr"],
8460
+ ["abort", "stderr"]
8461
+ ]);
8462
+ var STREAM_RECEIVERS = /* @__PURE__ */ new Map([
8463
+ ["$stdout", "stdout"],
8464
+ ["STDOUT", "stdout"],
8465
+ ["$stderr", "stderr"],
8466
+ ["STDERR", "stderr"]
8467
+ ]);
8468
+ var STREAM_METHODS = /* @__PURE__ */ new Set(["puts", "print", "write", "<<"]);
8469
+ var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
8470
+ function streamWriteEffects(root) {
8471
+ return (0, import_extractor9.numberRepeatedCallSites)(
8472
+ writesUnder2(root).map((write) => [write])
8473
+ ).flat();
8474
+ }
8475
+ function writesUnder2(root) {
8476
+ const found = [];
8477
+ (0, import_extractor9.walkDescendants)(root, null, children, {
8478
+ at: (node) => {
8479
+ const write = node.type === "call" ? streamWriteAt(node) : null;
8480
+ if (write !== null) {
8481
+ found.push({ effect: write, at: node });
8482
+ }
8483
+ },
8484
+ into: (node) => DEFERRED_BODY_TYPES2.has(node.type) ? import_extractor9.SKIP_CHILDREN : null
8485
+ });
8213
8486
  return found;
8214
8487
  }
8215
8488
  function streamWrittenBy(call) {
@@ -8237,13 +8510,14 @@ function streamWriteAt(call) {
8237
8510
  function writeEffect(call, target, args) {
8238
8511
  const receiver = field(call, "receiver");
8239
8512
  const method = field(call, "method")?.text ?? "";
8240
- return {
8513
+ const effect = {
8241
8514
  type: "interaction",
8242
8515
  binding: (0, import_behavioral_ir4.ioBinding)({ recognition: RUBY_STREAM_RECOGNITION, target }),
8243
8516
  callee: receiver === null ? method : `${receiver.text}.${method}`,
8244
- groupId: `${call.startPosition.row + 1}:${call.startPosition.column + 1}`,
8245
- interaction: { class: "stream-write", ...payloadOf(args) }
8517
+ interaction: { class: "stream-write", ...payloadOf(args) },
8518
+ location: rangeOf(call)
8246
8519
  };
8520
+ return { ...effect, groupId: (0, import_extractor9.callSiteKey)(effect) };
8247
8521
  }
8248
8522
  function payloadOf(args) {
8249
8523
  const only = args[0];
@@ -8321,9 +8595,9 @@ function keyName(key) {
8321
8595
 
8322
8596
  // src/testCases.ts
8323
8597
  var import_behavioral_ir5 = require("@suss/behavioral-ir");
8324
- var import_extractor9 = require("@suss/extractor");
8598
+ var import_extractor10 = require("@suss/extractor");
8325
8599
  var import_resolution10 = require("@suss/resolution");
8326
- var import_values17 = require("@suss/values");
8600
+ var import_values18 = require("@suss/values");
8327
8601
  var BLOCK_TYPES6 = /* @__PURE__ */ new Set(["block", "do_block"]);
8328
8602
  function testPatternsIn(packs) {
8329
8603
  return packs.flatMap((pack) => pack.tests ?? []);
@@ -8507,7 +8781,7 @@ function groupStatements(group) {
8507
8781
  }
8508
8782
  function patternForFile(file, patterns) {
8509
8783
  return patterns.find(
8510
- (pattern) => (0, import_extractor9.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor9.isListedTestFile)(file, pattern.files)
8784
+ (pattern) => (0, import_extractor10.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor10.isListedTestFile)(file, pattern.files)
8511
8785
  ) ?? null;
8512
8786
  }
8513
8787
  function groupNamesIn(pattern) {
@@ -8799,7 +9073,7 @@ function exampleCalls(run, inherited) {
8799
9073
  }
8800
9074
  function testCaseUnits(root, patterns, options) {
8801
9075
  return patterns.flatMap((pattern) => {
8802
- if (!(0, import_extractor9.matchesTestFileName)(options.absoluteFile, pattern.filePatterns) || !(0, import_extractor9.isListedTestFile)(options.absoluteFile, pattern.files)) {
9076
+ if (!(0, import_extractor10.matchesTestFileName)(options.absoluteFile, pattern.filePatterns) || !(0, import_extractor10.isListedTestFile)(options.absoluteFile, pattern.files)) {
8803
9077
  return [];
8804
9078
  }
8805
9079
  return examplesIn(root, options.absoluteFile, pattern, options.shared).map(
@@ -8918,7 +9192,7 @@ function titleOf(call, facts) {
8918
9192
  if (first.type === "constant" || first.type === "scope_resolution") {
8919
9193
  return { text: first.text, unresolved: false };
8920
9194
  }
8921
- const read = (0, import_values17.literalOf)(evaluatedValue(first, facts));
9195
+ const read = (0, import_values18.literalOf)(evaluatedValue(first, facts));
8922
9196
  return read === null ? { text: first.text, unresolved: true } : { text: read, unresolved: false };
8923
9197
  }
8924
9198
  function isSkipped(example, pattern) {
@@ -9059,7 +9333,7 @@ function moduleOfConstant(node, options) {
9059
9333
  const key = classBehind(facts, options.absoluteFile, node);
9060
9334
  return key === void 0 ? null : options.displayPathOf((0, import_resolution10.fileOfKey)(key));
9061
9335
  }
9062
- const name = (0, import_values17.literalOf)(evaluatedValue(node, facts));
9336
+ const name = (0, import_values18.literalOf)(evaluatedValue(node, facts));
9063
9337
  const keys = name === null ? [] : facts.lookup("rbConstantName", 1, name.replace(/^::/, "")).map((row) => String(row[0]));
9064
9338
  const files = [...new Set(keys.map(import_resolution10.fileOfKey))];
9065
9339
  return files.length === 1 ? options.displayPathOf(files[0]) : null;
@@ -9069,8 +9343,8 @@ function oneLine(text3) {
9069
9343
  }
9070
9344
 
9071
9345
  // src/testClasses.ts
9072
- var import_extractor10 = require("@suss/extractor");
9073
- var import_values19 = require("@suss/values");
9346
+ var import_extractor11 = require("@suss/extractor");
9347
+ var import_values20 = require("@suss/values");
9074
9348
  var BLOCK_TYPES7 = /* @__PURE__ */ new Set(["block", "do_block"]);
9075
9349
  function testClassPatternsIn(packs) {
9076
9350
  return packs.flatMap((pack) => pack.testClasses ?? []);
@@ -9113,7 +9387,7 @@ function testClassUnits(root, patterns, options) {
9113
9387
  });
9114
9388
  }
9115
9389
  function readsFile(file, pattern) {
9116
- return (0, import_extractor10.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor10.isListedTestFile)(file, pattern.files);
9390
+ return (0, import_extractor11.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor11.isListedTestFile)(file, pattern.files);
9117
9391
  }
9118
9392
  function testClassesIn(root, file, pattern, facts) {
9119
9393
  const found = [];
@@ -9159,7 +9433,7 @@ function testBlockOf(statement, pattern) {
9159
9433
  }
9160
9434
  function descriptionOf(call, facts) {
9161
9435
  const first = firstArgument(call);
9162
- return first === null ? null : (0, import_values19.literalOf)(evaluatedValue(first, facts));
9436
+ return first === null ? null : (0, import_values20.literalOf)(evaluatedValue(first, facts));
9163
9437
  }
9164
9438
  function anchorOf2(node) {
9165
9439
  return BLOCK_TYPES7.has(node.type) ? node.parent ?? node : node;
@@ -9323,16 +9597,19 @@ function createFileCache(parse, readFile) {
9323
9597
  }
9324
9598
  };
9325
9599
  }
9326
- function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
9600
+ function fieldReadContext(pattern, cache, fileBlocks, options) {
9601
+ const facts = options.facts;
9602
+ const parsedFile = (absPath) => cache.get(absPath);
9327
9603
  return {
9328
9604
  pattern,
9329
9605
  cache,
9330
- bodyRead,
9606
+ bodyRead: options,
9331
9607
  facts,
9608
+ file: options.absoluteFile ?? options.filePath,
9332
9609
  read: {
9333
9610
  facts,
9334
- bodyBlocks: bodyRead.bodyBlocks,
9335
- dynamicNames: bodyRead.dynamicNames
9611
+ bodyBlocks: options.bodyBlocks,
9612
+ dynamicNames: options.dynamicNames
9336
9613
  },
9337
9614
  lookup: {
9338
9615
  root: pattern.root,
@@ -9341,8 +9618,9 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
9341
9618
  acronyms: pattern.acronyms ?? [],
9342
9619
  ancestryRootClassNames: pattern.ancestryRootClassNames,
9343
9620
  constantFiles: cache.constantFiles,
9344
- parsedFile: (absPath) => cache.get(absPath),
9345
- localDefinition: (name) => sameFileBlocks(name, fileBlocks)
9621
+ parsedFile,
9622
+ localDefinition: (name) => sameFileBlocks(name, fileBlocks),
9623
+ ...facts === void 0 ? {} : { definedInRun: classesDeclaredInRun(facts, parsedFile) }
9346
9624
  }
9347
9625
  };
9348
9626
  }
@@ -9451,13 +9729,7 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
9451
9729
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
9452
9730
  return [];
9453
9731
  }
9454
- const ctx = fieldReadContext(
9455
- pattern,
9456
- options.cache,
9457
- fileBlocks,
9458
- options,
9459
- options.facts
9460
- );
9732
+ const ctx = fieldReadContext(pattern, options.cache, fileBlocks, options);
9461
9733
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
9462
9734
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
9463
9735
  return [];
@@ -9478,13 +9750,47 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
9478
9750
  declsByName.set(decl.fieldName, decl);
9479
9751
  }
9480
9752
  }
9481
- return [...declsByName.values()].map((decl) => {
9482
- const raw = buildFieldUnit(pack, typeName, decl, options.filePath);
9753
+ const decls = [...declsByName.values()];
9754
+ const fields = decls.map((decl) => {
9755
+ const raw = buildFieldUnit(pack, typeName, decl, options);
9483
9756
  if (decl.body.reachSeed !== void 0) {
9484
9757
  options.onReachSeed?.(raw, decl.body.reachSeed);
9485
9758
  }
9486
9759
  return raw;
9487
9760
  });
9761
+ return [...fields, ...hookUnitsOf(decls, pattern, options)];
9762
+ }
9763
+ function hookUnitsOf(decls, pattern, options) {
9764
+ const hooks = pattern.resolverHooks;
9765
+ if (hooks === void 0) {
9766
+ return [];
9767
+ }
9768
+ const units = /* @__PURE__ */ new Map();
9769
+ for (const hook of decls.flatMap((decl) => decl.hooks)) {
9770
+ const key = nodeId(hook.block.file, hook.method);
9771
+ if (units.has(key)) {
9772
+ continue;
9773
+ }
9774
+ const raw = hookUnit(hook, {
9775
+ hooks,
9776
+ body: bodyOfMethod(hook.method, hook.block.file, options),
9777
+ displayPath: displayPathIn(options, hook.block.file),
9778
+ facts: options.facts
9779
+ });
9780
+ units.set(key, raw);
9781
+ options.onReachSeed?.(raw, {
9782
+ file: hook.block.file,
9783
+ node: hook.method,
9784
+ enclosingQualifiedName: hook.block.info.qualifiedName
9785
+ });
9786
+ }
9787
+ return [...units.values()];
9788
+ }
9789
+ function displayPathIn(options, file) {
9790
+ if (file === (options.absoluteFile ?? options.filePath)) {
9791
+ return options.filePath;
9792
+ }
9793
+ return options.displayPathOf?.(file) ?? file;
9488
9794
  }
9489
9795
  async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks, options) {
9490
9796
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
@@ -9561,7 +9867,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9561
9867
  raised: raisedStatusBranches(
9562
9868
  pattern,
9563
9869
  options.storage?.patterns ?? [],
9564
- body.extraEffects,
9870
+ everyEffect(body.extraEffects),
9565
9871
  "eachRoute",
9566
9872
  rangeOf(filter.method)
9567
9873
  )
@@ -9594,10 +9900,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9594
9900
  actionName,
9595
9901
  route,
9596
9902
  method,
9597
- {
9598
- display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
9599
- absolute: block.file
9600
- },
9903
+ { display: displayPathIn(options, block.file), absolute: block.file },
9601
9904
  options,
9602
9905
  around,
9603
9906
  respondingHelper,
@@ -9763,11 +10066,11 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
9763
10066
  location: range
9764
10067
  },
9765
10068
  statusCodeReading: {
9766
- reading: import_extractor11.absentReading,
10069
+ reading: import_extractor12.absentReading,
9767
10070
  libraryDefault: pattern.defaultStatusCode
9768
10071
  },
9769
- effects: body.effects ?? [],
9770
- ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
10072
+ effects: everyEffect(body.effects),
10073
+ ...body.extraEffects === void 0 ? {} : { extraEffects: everyEffect(body.extraEffects) },
9771
10074
  location: range,
9772
10075
  isDefault: true
9773
10076
  }
@@ -9777,7 +10080,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
9777
10080
  raisedStatusBranches(
9778
10081
  pattern,
9779
10082
  bodyRead.storage?.patterns ?? [],
9780
- body.extraEffects,
10083
+ everyEffect(body.extraEffects),
9781
10084
  raises2.handlers,
9782
10085
  range
9783
10086
  )
@@ -9822,7 +10125,7 @@ function routingGapUnit(pattern, gaps) {
9822
10125
  isDefault: true
9823
10126
  }
9824
10127
  ],
9825
- readings: gaps.map((gap) => (0, import_extractor11.unreadableReading)(gap, range)),
10128
+ readings: gaps.map((gap) => (0, import_extractor12.unreadableReading)(gap, range)),
9826
10129
  dependencyCalls: [],
9827
10130
  declaredContract: null
9828
10131
  };
@@ -9856,15 +10159,18 @@ function branchesFor(body, range) {
9856
10159
  emitEvent: null,
9857
10160
  location: range
9858
10161
  },
9859
- effects: body.effects ?? [],
9860
- ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
10162
+ effects: everyEffect(body.effects),
10163
+ ...body.extraEffects === void 0 ? {} : { extraEffects: everyEffect(body.extraEffects) },
9861
10164
  location: range,
9862
10165
  isDefault: true
9863
10166
  };
9864
10167
  return withSlotSources([branch], body.provenance);
9865
10168
  }
10169
+ function everyEffect(placed) {
10170
+ return (placed ?? []).map((one) => one.effect);
10171
+ }
9866
10172
  function bodyOfMethod(method, file, bodyRead = {}) {
9867
- const effects = invocationEffects(
10173
+ const effects = invocationSites(
9868
10174
  method,
9869
10175
  bodyRead.inheritedMethods,
9870
10176
  EVERY_ARGLESS_CALL,
@@ -9873,7 +10179,7 @@ function bodyOfMethod(method, file, bodyRead = {}) {
9873
10179
  const storage = bodyRead.storage;
9874
10180
  const facts = bodyRead.facts;
9875
10181
  const database = storage === void 0 ? { effects: [], slots: [] } : methodStorage(method, file, storage);
9876
- const extra = [
10182
+ const found = [
9877
10183
  ...envReadEffects(
9878
10184
  method,
9879
10185
  facts === void 0 ? void 0 : { db: facts, file }
@@ -9881,6 +10187,11 @@ function bodyOfMethod(method, file, bodyRead = {}) {
9881
10187
  ...database.effects,
9882
10188
  ...streamWriteEffects(method)
9883
10189
  ];
10190
+ const siteOf3 = effectSitesIn(
10191
+ methodBody(method),
10192
+ found.map((one) => one.at)
10193
+ );
10194
+ const extra = found.map(({ effect, at }) => ({ effect, site: siteOf3(at) }));
9884
10195
  const provenance = database.slots.length === 0 || storage === void 0 ? [] : slotProvenance(database.slots, { facts: storage.facts, unit: method });
9885
10196
  return {
9886
10197
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -9895,7 +10206,7 @@ var NO_METHOD_BEHIND_IT = {
9895
10206
  readings: []
9896
10207
  };
9897
10208
  function methodNotSettled(reason, range) {
9898
- return { readings: [(0, import_extractor11.unreadableReading)(reason, range)] };
10209
+ return { readings: [(0, import_extractor12.unreadableReading)(reason, range)] };
9899
10210
  }
9900
10211
  function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
9901
10212
  const table = {
@@ -9944,7 +10255,8 @@ async function readFieldCall(stmt, scope, ctx, ancestry) {
9944
10255
  namedOnTheWire: true,
9945
10256
  node: stmt,
9946
10257
  contract: read.contract,
9947
- body: read.body
10258
+ body: read.body,
10259
+ hooks: read.hooks ?? []
9948
10260
  };
9949
10261
  }
9950
10262
  function computedNameDeclaration(stmt, nameArg) {
@@ -9954,9 +10266,10 @@ function computedNameDeclaration(stmt, nameArg) {
9954
10266
  namedOnTheWire: false,
9955
10267
  node: stmt,
9956
10268
  contract: null,
10269
+ hooks: [],
9957
10270
  body: {
9958
10271
  readings: [
9959
- (0, import_extractor11.unreadableReading)(
10272
+ (0, import_extractor12.unreadableReading)(
9960
10273
  `This field is named by ${nameArg.text}, which is worked out when the class body runs, so the name the schema exposes it under was not read here`,
9961
10274
  range
9962
10275
  )
@@ -9980,14 +10293,38 @@ async function readFieldShape(symbol, callArgs, scope, ctx, ancestry, range) {
9980
10293
  }
9981
10294
  return {
9982
10295
  contract: literalContract(callArgs, scope, ctx),
9983
- body: bodyFromLookup(
10296
+ body: methodBehindField(symbol, callArgs, ctx, ancestry, range)
10297
+ };
10298
+ }
10299
+ function methodBehindField(symbol, callArgs, ctx, ancestry, range) {
10300
+ const keyword = ctx.pattern.resolverMethodKeyword;
10301
+ const written = keyword === void 0 ? void 0 : callArgs.keyword[keyword];
10302
+ if (written === void 0) {
10303
+ return bodyFromLookup(
9984
10304
  methodInAncestry(ancestry, symbol, ctx.read),
9985
10305
  range,
9986
10306
  "This field",
9987
10307
  NO_METHOD_BEHIND_IT,
9988
10308
  ctx.bodyRead
9989
- )
9990
- };
10309
+ );
10310
+ }
10311
+ const name = stringValueOf(written, ctx.facts);
10312
+ if (name === null) {
10313
+ return methodNotSettled(
10314
+ `This field is resolved by the method ${written.text}, which is worked out at run time, so nothing about what it does was read here`,
10315
+ range
10316
+ );
10317
+ }
10318
+ return bodyFromLookup(
10319
+ methodInAncestry(ancestry, name, ctx.read),
10320
+ range,
10321
+ "This field",
10322
+ methodNotSettled(
10323
+ `This field is resolved by ${name}, which nothing in its type's ancestry defines, so nothing about what it does was read here`,
10324
+ range
10325
+ ),
10326
+ ctx.bodyRead
10327
+ );
9991
10328
  }
9992
10329
  function literalContract(callArgs, scope, ctx) {
9993
10330
  const typeArg = callArgs.positional[1];
@@ -10003,21 +10340,12 @@ function literalContract(callArgs, scope, ctx) {
10003
10340
  async function readWiredClass(ref, scope, ctx) {
10004
10341
  const range = rangeOf(ref);
10005
10342
  const candidates = constantRefCandidates(ref, scope.nesting);
10006
- if (candidates.length === 0) {
10007
- return {
10008
- contract: null,
10009
- body: methodNotSettled(
10010
- `This field is wired to ${ref.text}, which is not a constant path this reader follows, so nothing about what it does was read here`,
10011
- range
10012
- )
10013
- };
10014
- }
10015
- const reached = await reachConstant(candidates, ctx.lookup);
10343
+ const reached = await wiredClassInRun(ref, ctx) ?? (candidates.length === 0 ? null : await reachConstant(candidates, ctx.lookup));
10016
10344
  if (reached === null) {
10017
10345
  return {
10018
10346
  contract: null,
10019
10347
  body: methodNotSettled(
10020
- `This field is wired to ${ref.text}, which this run did not read, so nothing about what it does was read here`,
10348
+ candidates.length === 0 ? `This field is wired to ${ref.text}, which is not a constant path this reader follows, so nothing about what it does was read here` : `This field is wired to ${ref.text}, which this run did not read, so nothing about what it does was read here`,
10021
10349
  range
10022
10350
  )
10023
10351
  };
@@ -10028,20 +10356,44 @@ async function readWiredClass(ref, scope, ctx) {
10028
10356
  reached.blocks,
10029
10357
  ctx.lookup
10030
10358
  );
10359
+ const body = bodyFromLookup(
10360
+ methodInAncestry(ancestry, ctx.pattern.resolverMethodName, ctx.read),
10361
+ range,
10362
+ `This field's ${targetQualifiedName}`,
10363
+ methodNotSettled(
10364
+ `This field is wired to ${targetQualifiedName}, which defines no ${ctx.pattern.resolverMethodName} method anywhere in its ancestry, so nothing about what it does was read here`,
10365
+ range
10366
+ ),
10367
+ ctx.bodyRead
10368
+ );
10369
+ const hooks = ctx.pattern.resolverHooks === void 0 ? { definitions: [], unsettled: [] } : hooksRun(ancestry, ctx.pattern.resolverHooks, ctx.read);
10031
10370
  return {
10032
10371
  contract: readClassContract(ancestry, ctx.pattern),
10033
- body: bodyFromLookup(
10034
- methodInAncestry(ancestry, ctx.pattern.resolverMethodName, ctx.read),
10035
- range,
10036
- `This field's ${targetQualifiedName}`,
10037
- methodNotSettled(
10038
- `This field is wired to ${targetQualifiedName}, which defines no ${ctx.pattern.resolverMethodName} method anywhere in its ancestry, so nothing about what it does was read here`,
10039
- range
10040
- ),
10041
- ctx.bodyRead
10042
- )
10372
+ body: {
10373
+ ...body,
10374
+ readings: [
10375
+ ...body.readings,
10376
+ ...hooks.unsettled.map(
10377
+ (one) => unsettledHookReading(targetQualifiedName, one, range)
10378
+ )
10379
+ ]
10380
+ },
10381
+ hooks: hooks.definitions
10043
10382
  };
10044
10383
  }
10384
+ async function wiredClassInRun(ref, ctx) {
10385
+ const inRun = ctx.lookup.definedInRun;
10386
+ if (ctx.facts === void 0 || inRun === void 0) {
10387
+ return null;
10388
+ }
10389
+ const classKey = classSettledOn(ctx.facts, readKey(ctx.file, ref, null));
10390
+ const name = classKey === void 0 ? void 0 : declaredName(ctx.facts, classKey);
10391
+ if (name === void 0) {
10392
+ return null;
10393
+ }
10394
+ const blocks = await inRun(name);
10395
+ return blocks === null ? null : { name, blocks };
10396
+ }
10045
10397
  function readTypeCall(callArgs, scope, pattern, out) {
10046
10398
  const typeArg = callArgs.positional[0];
10047
10399
  if (typeArg !== void 0) {
@@ -10159,7 +10511,11 @@ function toCamelCase(name) {
10159
10511
  ...rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
10160
10512
  ].join("");
10161
10513
  }
10162
- function buildFieldUnit(pack, typeName, decl, filePath) {
10514
+ function buildFieldUnit(pack, typeName, decl, options) {
10515
+ const filePath = options.filePath;
10516
+ const wrappers = decl.hooks.map(
10517
+ (hook) => hookReference(hook, displayPathIn(options, hook.block.file))
10518
+ );
10163
10519
  const parameters = (decl.contract?.methodArgs ?? decl.contract?.args ?? []).map((arg, position) => ({
10164
10520
  name: arg.name,
10165
10521
  position,
@@ -10199,7 +10555,8 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
10199
10555
  dependencyCalls: [],
10200
10556
  declaredContract: null,
10201
10557
  ...decl.body.readings.length > 0 ? { readings: decl.body.readings } : {},
10202
- ...graphqlDeclaredContract !== void 0 ? { graphqlDeclaredContract } : {}
10558
+ ...graphqlDeclaredContract !== void 0 ? { graphqlDeclaredContract } : {},
10559
+ ...wrappers.length > 0 ? { wrappers } : {}
10203
10560
  };
10204
10561
  }
10205
10562
 
@@ -10605,13 +10962,13 @@ var import_node_fs3 = __toESM(require("fs"), 1);
10605
10962
  var import_node_path4 = __toESM(require("path"), 1);
10606
10963
  var import_behavioral_ir9 = require("@suss/behavioral-ir");
10607
10964
  var import_datalog3 = require("@suss/datalog");
10608
- var import_extractor19 = require("@suss/extractor");
10965
+ var import_extractor20 = require("@suss/extractor");
10609
10966
  var import_resolution16 = require("@suss/resolution");
10610
10967
 
10611
10968
  // src/diagnostics.ts
10612
- var import_extractor12 = require("@suss/extractor");
10969
+ var import_extractor13 = require("@suss/extractor");
10613
10970
  function buildRubyExtractionReport(args) {
10614
- return (0, import_extractor12.buildUngatedExtractionReport)({
10971
+ return (0, import_extractor13.buildUngatedExtractionReport)({
10615
10972
  packs: args.packs.map((pack) => ({
10616
10973
  name: pack.name,
10617
10974
  version: pack.version ?? null,
@@ -10623,10 +10980,16 @@ function buildRubyExtractionReport(args) {
10623
10980
  });
10624
10981
  }
10625
10982
 
10983
+ // src/reach/closure.ts
10984
+ var import_behavioral_ir7 = require("@suss/behavioral-ir");
10985
+ var import_datalog2 = require("@suss/datalog");
10986
+ var import_extractor16 = require("@suss/extractor");
10987
+ var import_resolution13 = require("@suss/resolution");
10988
+
10626
10989
  // src/exits.ts
10627
- var import_extractor13 = require("@suss/extractor");
10990
+ var import_extractor14 = require("@suss/extractor");
10628
10991
  var import_resolution11 = require("@suss/resolution");
10629
- var import_values22 = require("@suss/values");
10992
+ var import_values24 = require("@suss/values");
10630
10993
  var EXIT_METHODS = /* @__PURE__ */ new Map([
10631
10994
  ["exit", 0],
10632
10995
  ["exit!", 1],
@@ -10635,14 +10998,14 @@ var EXIT_METHODS = /* @__PURE__ */ new Map([
10635
10998
  var DEFERRED_BODY_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
10636
10999
  function exitSites(root) {
10637
11000
  const found = [];
10638
- (0, import_extractor13.walkDescendants)(root, null, children, {
11001
+ (0, import_extractor14.walkDescendants)(root, null, children, {
10639
11002
  at: (node) => {
10640
11003
  const site = node.type === "call" ? exitSiteAt(node) : null;
10641
11004
  if (site !== null) {
10642
11005
  found.push(site);
10643
11006
  }
10644
11007
  },
10645
- into: (node) => DEFERRED_BODY_TYPES3.has(node.type) ? import_extractor13.SKIP_CHILDREN : null
11008
+ into: (node) => DEFERRED_BODY_TYPES3.has(node.type) ? import_extractor14.SKIP_CHILDREN : null
10646
11009
  });
10647
11010
  return found;
10648
11011
  }
@@ -10679,7 +11042,7 @@ function exitCodeOf(site, facts) {
10679
11042
  if (written !== void 0) {
10680
11043
  return { type: "literal", value: written };
10681
11044
  }
10682
- const value = (0, import_values22.constantOf)(evaluatedValue(code, facts));
11045
+ const value = (0, import_values24.constantOf)(evaluatedValue(code, facts));
10683
11046
  return typeof value === "number" ? { type: "literal", value } : { type: "dynamic", sourceText: code.text };
10684
11047
  }
10685
11048
  function exitingBranches(method, effects, facts) {
@@ -10700,93 +11063,13 @@ function markExitCodeFunctions(summaries, exits, facts, workspaceRoot) {
10700
11063
  (site) => site.code === null ? [] : [nodeId(file, site.code)]
10701
11064
  )
10702
11065
  );
10703
- (0, import_extractor13.markReturnsAsExitCode)(
11066
+ (0, import_extractor14.markReturnsAsExitCode)(
10704
11067
  summaries,
10705
11068
  (0, import_resolution11.exitCodeFunctions)(facts, sinks),
10706
11069
  workspaceRoot
10707
11070
  );
10708
11071
  }
10709
11072
 
10710
- // src/facts/callbacks.ts
10711
- function callbacksIn(patterns) {
10712
- return patterns.flatMap(
10713
- (pattern) => pattern.callbacks === void 0 ? [] : [pattern.callbacks]
10714
- );
10715
- }
10716
- function emitClassCallbacks(db, file, classNode, declared) {
10717
- const body = field(classNode, "body");
10718
- if (body === null || declared.length === 0) {
10719
- return;
10720
- }
10721
- const classKey = nodeId(file, classNode);
10722
- for (const statement of runStatements(body)) {
10723
- if (statement.type !== "call" || field(statement, "receiver") !== null) {
10724
- continue;
10725
- }
10726
- const called = field(statement, "method")?.text ?? "";
10727
- const args = readCallArgs(field(statement, "arguments"));
10728
- for (const callbacks of declared) {
10729
- const events = eventsOf(args, called, callbacks, db);
10730
- for (const event of events) {
10731
- for (const method of symbolArgumentNames(args, db)) {
10732
- db.add("classCallback", [classKey, event, method]);
10733
- }
10734
- }
10735
- }
10736
- }
10737
- }
10738
- function eventsOf(args, called, callbacks, db) {
10739
- const covered = callbacks.registeredBy[called];
10740
- if (covered === void 0) {
10741
- return [];
10742
- }
10743
- const written = args.keyword[callbacks.eventKeyword];
10744
- const narrowed = written === void 0 ? null : stringValueOf(written, db);
10745
- if (narrowed === null) {
10746
- return covered;
10747
- }
10748
- return covered.filter((event) => event === narrowed);
10749
- }
10750
-
10751
- // src/facts.ts
10752
- var import_node_path3 = __toESM(require("path"), 1);
10753
- var import_extractor14 = require("@suss/extractor");
10754
- function emitRequireFacts(db, filePath, root, known) {
10755
- (0, import_extractor14.walkDescendants)(root, null, children, {
10756
- at: (node) => {
10757
- const target = requiredRelativePath(node);
10758
- if (target === null) {
10759
- return;
10760
- }
10761
- const resolved = import_node_path3.default.resolve(
10762
- import_node_path3.default.dirname(filePath),
10763
- target.endsWith(".rb") ? target : `${target}.rb`
10764
- );
10765
- if (known.has(resolved)) {
10766
- db.add("importsFile", [filePath, resolved]);
10767
- }
10768
- },
10769
- into: (node) => node.type === "argument_list" ? import_extractor14.SKIP_CHILDREN : null
10770
- });
10771
- }
10772
- function requiredRelativePath(node) {
10773
- if (node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== "require_relative") {
10774
- return null;
10775
- }
10776
- const { positional } = readCallArgs(field(node, "arguments"));
10777
- return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
10778
- }
10779
-
10780
- // src/moduleSurface.ts
10781
- var import_behavioral_ir8 = require("@suss/behavioral-ir");
10782
- var import_extractor17 = require("@suss/extractor");
10783
-
10784
- // src/reach/closure.ts
10785
- var import_behavioral_ir7 = require("@suss/behavioral-ir");
10786
- var import_datalog2 = require("@suss/datalog");
10787
- var import_extractor16 = require("@suss/extractor");
10788
- var import_resolution13 = require("@suss/resolution");
10789
-
10790
11073
  // src/flags.ts
10791
11074
  var import_extractor15 = require("@suss/extractor");
10792
11075
  var PARSE_METHODS = /* @__PURE__ */ new Set(["parse", "parse!", "permute", "permute!"]);
@@ -10887,6 +11170,9 @@ function parserCalledOn(receiver, parserNamed) {
10887
11170
 
10888
11171
  // src/reach/resolveCallee.ts
10889
11172
  var import_resolution12 = require("@suss/resolution");
11173
+ function selfClassOf(site) {
11174
+ return site.receiverClass?.name ?? site.enclosingQualifiedName;
11175
+ }
10890
11176
  var STOP_FOR = {
10891
11177
  severalSources: "multipleSources",
10892
11178
  outsideRun: "outsideRun",
@@ -11083,6 +11369,24 @@ function asCallee(spelling, outcome, site, ctx) {
11083
11369
  if (objects.length > 1) {
11084
11370
  return stop("multipleSources");
11085
11371
  }
11372
+ const fromSelf = ownReceiverForm(spelling.key, site, ctx);
11373
+ const selfClass = selfClassOf(site);
11374
+ if (fromSelf !== null && selfClass !== null) {
11375
+ return methodOfClass(
11376
+ { ...spelling, onClassItself: fromSelf === import_resolution12.SELF_AS_CLASS_NAME },
11377
+ selfClass,
11378
+ ctx
11379
+ );
11380
+ }
11381
+ const returned = (0, import_resolution12.selfResultOf)(ctx.facts, spelling.key);
11382
+ const sentTo = returned === null ? null : classSentTo(returned.receiver, site, ctx);
11383
+ if (returned !== null && sentTo !== null) {
11384
+ return methodOfClass(
11385
+ { ...spelling, onClassItself: returned.form === import_resolution12.SELF_AS_CLASS_NAME },
11386
+ sentTo,
11387
+ ctx
11388
+ );
11389
+ }
11086
11390
  const objectKey = objects[0] ?? (outcome.kind === "object" ? outcome.key : void 0);
11087
11391
  if (objectKey !== void 0) {
11088
11392
  return methodOnObject(spelling, objectKey, ctx);
@@ -11092,6 +11396,17 @@ function asCallee(spelling, outcome, site, ctx) {
11092
11396
  }
11093
11397
  return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
11094
11398
  }
11399
+ function ownReceiverForm(key, site, ctx) {
11400
+ return site.method === null ? null : (0, import_resolution12.selfFormOf)(ctx.facts, key, receiverKeyOf(site.file, site.method));
11401
+ }
11402
+ function classSentTo(receiver, site, ctx) {
11403
+ resolveValues(ctx.facts, [receiver]);
11404
+ if (ownReceiverForm(receiver, site, ctx) !== null) {
11405
+ return selfClassOf(site);
11406
+ }
11407
+ const [only, ...more] = objectsBehind(ctx.facts, receiver);
11408
+ return only === void 0 || more.length > 0 ? null : ctx.classNames.get(only) ?? null;
11409
+ }
11095
11410
  function objectsBehind(facts, key) {
11096
11411
  return (0, import_resolution12.answersFor)(facts, "wantedObjectOf", key);
11097
11412
  }
@@ -11109,11 +11424,18 @@ function methodOfClass(spelling, qualifiedName, ctx) {
11109
11424
  if (!spelling.onClassItself) {
11110
11425
  return methodOnAncestryOf(qualifiedName, spelling.method, ctx);
11111
11426
  }
11112
- if (spelling.method === "new") {
11113
- return methodOnAncestryOf(qualifiedName, "initialize", ctx);
11427
+ if (spelling.method === CONSTRUCTOR) {
11428
+ return methodOnAncestryOf(qualifiedName, INITIALIZE_METHOD, ctx);
11429
+ }
11430
+ return classMethodOn(qualifiedName, spelling.method, ctx);
11431
+ }
11432
+ function classMethodOn(qualifiedName, method, ctx) {
11433
+ const written = singletonMethodOn(qualifiedName, method, ctx);
11434
+ if (written.kind === "followed") {
11435
+ return written;
11114
11436
  }
11115
- const written = singletonMethodOn(qualifiedName, spelling.method, ctx);
11116
- return written.kind === "followed" ? written : scopeOn(qualifiedName, spelling.method, ctx) ?? written;
11437
+ const inherited = scopeOn(qualifiedName, method, ctx);
11438
+ return inherited === null ? written : followed(runningOn(qualifiedName, inherited, ctx));
11117
11439
  }
11118
11440
  function scopeOn(qualifiedName, method, ctx) {
11119
11441
  const spelled = (0, import_resolution12.classMemberName)(method);
@@ -11125,7 +11447,7 @@ function scopeOn(qualifiedName, method, ctx) {
11125
11447
  for (const row of ctx.facts.lookup("holdsProperty", 0, classKey)) {
11126
11448
  const target = row[1] === spelled ? ctx.definitions.get(String(row[2])) : void 0;
11127
11449
  if (target !== void 0) {
11128
- return followed(target);
11450
+ return target;
11129
11451
  }
11130
11452
  }
11131
11453
  }
@@ -11149,7 +11471,8 @@ function leavesRoomForATopLevelMethod(reason) {
11149
11471
  return reason === "noDeclaration" || reason === "outsideRun";
11150
11472
  }
11151
11473
  function resolveImplicitSelf(methodName, site, ctx) {
11152
- const onClass = site.enclosingQualifiedName === null ? null : methodOnSelf(site, site.enclosingQualifiedName, methodName, ctx);
11474
+ const selfClass = selfClassOf(site);
11475
+ const onClass = selfClass === null ? null : methodOnSelf(site, selfClass, methodName, ctx);
11153
11476
  if (onClass !== null && (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason))) {
11154
11477
  return onClass;
11155
11478
  }
@@ -11164,7 +11487,7 @@ function resolveImplicitSelf(methodName, site, ctx) {
11164
11487
  return readWholeAncestry && !OBJECT_METHODS.has(methodName) && inModuleInstanceMethod(site, ctx) ? SELF_CALL_IN_MIXIN : UNSETTLED_ON_SELF;
11165
11488
  }
11166
11489
  function inModuleInstanceMethod(site, ctx) {
11167
- const qualifiedName = site.enclosingQualifiedName;
11490
+ const qualifiedName = selfClassOf(site);
11168
11491
  if (qualifiedName === null || site.method === null || inClassMethod(site, ctx)) {
11169
11492
  return false;
11170
11493
  }
@@ -11172,7 +11495,7 @@ function inModuleInstanceMethod(site, ctx) {
11172
11495
  return own?.type === "bodies" && own.blocks.every((block) => block.info.kind === "module");
11173
11496
  }
11174
11497
  function declaredForTheOtherSelf(site, methodName, ctx) {
11175
- const qualifiedName = site.enclosingQualifiedName;
11498
+ const qualifiedName = selfClassOf(site);
11176
11499
  if (qualifiedName === null) {
11177
11500
  return false;
11178
11501
  }
@@ -11183,10 +11506,10 @@ function inClassMethod(site, ctx) {
11183
11506
  return site.method !== null && definesClassMethod(site.method, ctx.bodyBlocks);
11184
11507
  }
11185
11508
  function methodOnSelf(site, qualifiedName, methodName, ctx) {
11186
- if (inClassMethod(site, ctx)) {
11187
- return singletonMethodOn(qualifiedName, methodName, ctx);
11509
+ if (!inClassMethod(site, ctx)) {
11510
+ return methodOnAncestryOf(qualifiedName, methodName, ctx);
11188
11511
  }
11189
- return methodOnAncestryOf(qualifiedName, methodName, ctx);
11512
+ return methodName === CONSTRUCTOR ? methodOnAncestryOf(qualifiedName, INITIALIZE_METHOD, ctx) : classMethodOn(qualifiedName, methodName, ctx);
11190
11513
  }
11191
11514
  function resolveTopLevelName(methodName, ctx) {
11192
11515
  const candidates = ctx.topLevelMethods.get(methodName);
@@ -11209,14 +11532,37 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
11209
11532
  dynamicNames: ctx.dynamicNames
11210
11533
  });
11211
11534
  if (found.type === "found") {
11212
- return followed(reachedMethod(found.method, found.block, methodName));
11535
+ return followed(
11536
+ runningOn(
11537
+ qualifiedName,
11538
+ reachedMethod(found.method, found.block, methodName),
11539
+ ctx
11540
+ )
11541
+ );
11213
11542
  }
11214
11543
  if (found.type === "unsettled") {
11215
- return stop(unsettledReason(found));
11544
+ return stop(unsettledReason2(found));
11216
11545
  }
11217
11546
  return NO_DECLARATION;
11218
11547
  }
11219
- function unsettledReason(found) {
11548
+ function runningOn(qualifiedName, target, ctx) {
11549
+ if (target.enclosingQualifiedName === qualifiedName) {
11550
+ return target;
11551
+ }
11552
+ const own = (ctx.ancestries.get(qualifiedName) ?? []).find(
11553
+ (entry) => entry.type === "bodies" && entry.name === qualifiedName
11554
+ );
11555
+ const file = own?.type === "bodies" ? own.blocks[0]?.file : void 0;
11556
+ if (file === void 0) {
11557
+ return target;
11558
+ }
11559
+ return {
11560
+ ...target,
11561
+ exportPath: [qualifiedName, target.name],
11562
+ receiverClass: { name: qualifiedName, file }
11563
+ };
11564
+ }
11565
+ function unsettledReason2(found) {
11220
11566
  if (found.cause === "dynamicDefine") {
11221
11567
  return "definedAtLoadTime";
11222
11568
  }
@@ -11279,11 +11625,12 @@ async function reachedFunctions(seeds, options) {
11279
11625
  const charges = /* @__PURE__ */ new Map();
11280
11626
  const beforeGaps = /* @__PURE__ */ new Map();
11281
11627
  const functionFor = (target) => {
11282
- const node = replay?.nodeOf(target.key) ?? null;
11283
- if (node === null) {
11628
+ const node = replay?.nodeOf((0, import_resolution13.plainKeyOf)(target.key)) ?? null;
11629
+ const found = node === null ? null : { ...target, node };
11630
+ if (found === null || keyOf(found) !== target.key) {
11284
11631
  throw new ReplayFailed(target.key);
11285
11632
  }
11286
- return { ...target, node };
11633
+ return found;
11287
11634
  };
11288
11635
  for (const seed of seeds) {
11289
11636
  seedKeys.add(seed.key);
@@ -11343,7 +11690,10 @@ async function reachedFunctions(seeds, options) {
11343
11690
  if (scan.followed.length > 0) {
11344
11691
  followedKeys.add(key);
11345
11692
  }
11346
- for (const target of scan.followed) {
11693
+ const calledHere = scan.followed.flatMap(
11694
+ (target) => target.receiverClass === void 0 ? [target] : [plainOf(target), target]
11695
+ );
11696
+ for (const target of calledHere) {
11347
11697
  const calleeKey = keyOf(target);
11348
11698
  if (!functionByKey.has(calleeKey)) {
11349
11699
  functionByKey.set(calleeKey, target);
@@ -11364,12 +11714,22 @@ async function reachedFunctions(seeds, options) {
11364
11714
  options.storage
11365
11715
  );
11366
11716
  }
11717
+ const placementsOf = (key) => ({
11718
+ targets: targetsByKey.get(key),
11719
+ argTargets: argTargetsByKey.get(key),
11720
+ possibleTargets: possibleTargetsByKey.get(key)
11721
+ });
11722
+ const onSubclass = (0, import_resolution13.keptVariants)(
11723
+ [...functionByKey].flatMap(
11724
+ ([key, target]) => target.receiverClass === void 0 ? [] : [variantOf(key, scans, placementsOf)]
11725
+ )
11726
+ );
11367
11727
  const summaries = [];
11368
11728
  const summariesByKey = /* @__PURE__ */ new Map();
11369
11729
  for (const [keyAtom] of db.facts("reachable")) {
11370
11730
  const key = String(keyAtom);
11371
11731
  const target = functionByKey.get(key);
11372
- if (seedKeys.has(key) || target === void 0) {
11732
+ if (seedKeys.has(key) || target === void 0 || target.receiverClass !== void 0 && !onSubclass.has(key)) {
11373
11733
  continue;
11374
11734
  }
11375
11735
  const kept = replay?.summaryOf(key);
@@ -11384,6 +11744,12 @@ async function reachedFunctions(seeds, options) {
11384
11744
  const raw = libraryUnit(target, options);
11385
11745
  dropPropertyReads(raw, propertyReadsByKey.get(key));
11386
11746
  const summary2 = (0, import_extractor16.assembleSummary)(raw, { gapHandling });
11747
+ if (target.receiverClass !== void 0) {
11748
+ summary2.receiverClass = subclassAt(
11749
+ target.receiverClass,
11750
+ options.displayPathOf
11751
+ );
11752
+ }
11387
11753
  if (gapHandling !== "silent") {
11388
11754
  summary2.gaps.push(
11389
11755
  ...(stopsByKey.get(key) ?? []).map(import_behavioral_ir7.unfollowedCallGap)
@@ -11425,6 +11791,16 @@ async function reachedFunctions(seeds, options) {
11425
11791
  beforeGaps
11426
11792
  };
11427
11793
  }
11794
+ function variantOf(key, scans, placementsOf) {
11795
+ return {
11796
+ key,
11797
+ differs: !(0, import_behavioral_ir7.sameDeclarations)(
11798
+ placementsOf(key),
11799
+ placementsOf((0, import_resolution13.plainKeyOf)(key))
11800
+ ),
11801
+ calls: scans.get(key)?.followed.map((target) => target.key) ?? []
11802
+ };
11803
+ }
11428
11804
  var ReplayFailed = class extends Error {
11429
11805
  constructor(key) {
11430
11806
  super(`The cache stored a method at ${key}, and nothing is there now.`);
@@ -11515,7 +11891,8 @@ function storedScan(scan) {
11515
11891
  file: target.file,
11516
11892
  name: target.name,
11517
11893
  exportPath: target.exportPath,
11518
- enclosingQualifiedName: target.enclosingQualifiedName
11894
+ enclosingQualifiedName: target.enclosingQualifiedName,
11895
+ ...target.receiverClass === void 0 ? {} : { receiverClass: target.receiverClass }
11519
11896
  })),
11520
11897
  stops: scan.stops,
11521
11898
  targets: [...scan.targets],
@@ -11552,7 +11929,25 @@ function scanFromStored(stored, functionFor) {
11552
11929
  };
11553
11930
  }
11554
11931
  function keyOf(target) {
11555
- return nodeId(target.file, target.node);
11932
+ const key = nodeId(target.file, target.node);
11933
+ return target.receiverClass === void 0 ? key : (0, import_resolution13.variantKey)(key, target.receiverClass);
11934
+ }
11935
+ function plainOf(target) {
11936
+ const { receiverClass: _, ...plain } = target;
11937
+ return plain.enclosingQualifiedName === null ? plain : { ...plain, exportPath: [plain.enclosingQualifiedName, plain.name] };
11938
+ }
11939
+ function placeOf(target, displayPathOf) {
11940
+ const place = {
11941
+ file: displayPathOf(target.file),
11942
+ span: spanOf(target.node)
11943
+ };
11944
+ return target.receiverClass === void 0 ? place : {
11945
+ ...place,
11946
+ receiverClass: subclassAt(target.receiverClass, displayPathOf)
11947
+ };
11948
+ }
11949
+ function subclassAt(subclass, displayPathOf) {
11950
+ return { name: subclass.name, file: displayPathOf(subclass.file) };
11556
11951
  }
11557
11952
  function dropPropertyReads(raw, callees) {
11558
11953
  if (callees === void 0) {
@@ -11625,8 +12020,9 @@ function siteOf2(source) {
11625
12020
  return {
11626
12021
  file: source.file,
11627
12022
  method: localsOwnerOf(source.node),
11628
- owner: keyOf(source),
11629
- enclosingQualifiedName: source.enclosingQualifiedName
12023
+ owner: nodeId(source.file, source.node),
12024
+ enclosingQualifiedName: source.enclosingQualifiedName,
12025
+ receiverClass: source.receiverClass
11630
12026
  };
11631
12027
  }
11632
12028
  function localsOwnerOf(node) {
@@ -11669,17 +12065,21 @@ function scanBody(source, ctx, options, read) {
11669
12065
  const seen = /* @__PURE__ */ new Set();
11670
12066
  const parameterCallsSeen = /* @__PURE__ */ new Set();
11671
12067
  const keptArgless = /* @__PURE__ */ new Set();
11672
- const recordPassedArgs = (call, callee, calleeKey) => {
12068
+ const recordPassedArgs = (call, callee, target) => {
11673
12069
  const args = field(call, "arguments");
11674
12070
  if (args === null) {
11675
12071
  return;
11676
12072
  }
12073
+ const calleeKeys = target === null ? [] : [keyOf(target), keyOf(plainOf(target))];
12074
+ const passes = (position) => {
12075
+ for (const calleeKey of calleeKeys) {
12076
+ passedPositions.add(`${calleeKey}#${position}`);
12077
+ }
12078
+ };
11677
12079
  const argNodes = bodyStatements(args);
11678
12080
  for (let position = 0; position < argNodes.length; position += 1) {
11679
12081
  if (argNodes[position]?.type === LAMBDA_TYPE) {
11680
- if (calleeKey !== null) {
11681
- passedPositions.add(`${calleeKey}#${position}`);
11682
- }
12082
+ passes(position);
11683
12083
  continue;
11684
12084
  }
11685
12085
  const symbol = methodReferenceSymbol(argNodes[position]);
@@ -11695,13 +12095,8 @@ function scanBody(source, ctx, options, read) {
11695
12095
  seen.add(key);
11696
12096
  followed2.push(resolved);
11697
12097
  }
11698
- placements.placeArg(callee, position, {
11699
- file: displayPathOf(resolved.file),
11700
- span: spanOf(resolved.node)
11701
- });
11702
- if (calleeKey !== null) {
11703
- passedPositions.add(`${calleeKey}#${position}`);
11704
- }
12098
+ placements.placeArg(callee, position, placeOf(resolved, displayPathOf));
12099
+ passes(position);
11705
12100
  }
11706
12101
  };
11707
12102
  const followCallback = (name, key) => {
@@ -11737,10 +12132,7 @@ function scanBody(source, ctx, options, read) {
11737
12132
  dispatched.targets.forEach(follow);
11738
12133
  placements.placePossible(
11739
12134
  callee,
11740
- dispatched.targets.map((target) => ({
11741
- file: displayPathOf(target.file),
11742
- span: spanOf(target.node)
11743
- }))
12135
+ dispatched.targets.map((target) => placeOf(target, displayPathOf))
11744
12136
  );
11745
12137
  if (dispatched.unresolved > 0) {
11746
12138
  stops.add({ callee, reason: "unsettledValue" });
@@ -11772,7 +12164,7 @@ function scanBody(source, ctx, options, read) {
11772
12164
  recordPassedArgs(
11773
12165
  call,
11774
12166
  callee,
11775
- outcome.kind === "followed" ? keyOf(outcome.target) : null
12167
+ outcome.kind === "followed" ? outcome.target : null
11776
12168
  );
11777
12169
  if (outcome.kind === "stopped") {
11778
12170
  const claimed = options.storage !== void 0 && storageClaims(call, callSite.file, options.storage, callSite.method);
@@ -11813,10 +12205,7 @@ function scanBody(source, ctx, options, read) {
11813
12205
  function placeCallee(placements, outcome, call, where) {
11814
12206
  const callee = calleeText(call);
11815
12207
  if (outcome.kind === "followed") {
11816
- placements.place(callee, {
11817
- file: where.displayPathOf(outcome.target.file),
11818
- span: spanOf(outcome.target.node)
11819
- });
12208
+ placements.place(callee, placeOf(outcome.target, where.displayPathOf));
11820
12209
  return;
11821
12210
  }
11822
12211
  if (outcome.matchByName === true) {
@@ -11870,7 +12259,7 @@ function libraryUnit(target, options, binding = (0, import_behavioral_ir7.functi
11870
12259
  emitEvent: null,
11871
12260
  location: range
11872
12261
  },
11873
- effects,
12262
+ effects: [],
11874
12263
  location: range,
11875
12264
  isDefault: true
11876
12265
  }
@@ -11890,7 +12279,10 @@ function libraryUnit(target, options, binding = (0, import_behavioral_ir7.functi
11890
12279
  parameters,
11891
12280
  branches: withSlotSources(
11892
12281
  branches.map(
11893
- (branch) => body.extraEffects === void 0 ? branch : { ...branch, extraEffects: body.extraEffects }
12282
+ (branch) => withEffectsThatRun(branch, {
12283
+ effects,
12284
+ extraEffects: body.extraEffects
12285
+ })
11894
12286
  ),
11895
12287
  body.provenance
11896
12288
  ),
@@ -11925,12 +12317,205 @@ function parameterName(param) {
11925
12317
  return named2 !== null && named2.type === "identifier" ? named2.text : null;
11926
12318
  }
11927
12319
 
12320
+ // src/entryPoints.ts
12321
+ function declaredEntryUnits(root, file, options) {
12322
+ const units = [];
12323
+ for (const pack of options.packs) {
12324
+ for (const entry of pack.entryPoints?.(file) ?? []) {
12325
+ const method = entryMethod(root, entry.name, options);
12326
+ if (method === null) {
12327
+ continue;
12328
+ }
12329
+ for (const declared of entry.units) {
12330
+ const raw = libraryUnit(
12331
+ {
12332
+ file,
12333
+ node: method.node,
12334
+ name: declared.name,
12335
+ exportPath: method.exportPath
12336
+ },
12337
+ options,
12338
+ declared.binding
12339
+ );
12340
+ const unit = {
12341
+ ...raw,
12342
+ identity: { ...raw.identity, kind: declared.kind },
12343
+ deployableUnit: declared.deployableUnit,
12344
+ ...declared.metadata !== void 0 ? { packMetadata: declared.metadata } : {}
12345
+ };
12346
+ units.push(unit);
12347
+ options.onReachSeed(unit, {
12348
+ file,
12349
+ node: method.node,
12350
+ enclosingQualifiedName: method.enclosingQualifiedName
12351
+ });
12352
+ }
12353
+ }
12354
+ }
12355
+ return units;
12356
+ }
12357
+ function entryMethod(root, name, options) {
12358
+ const dot = name.lastIndexOf(".");
12359
+ if (dot === -1) {
12360
+ const node = instanceMethodsByName(root, options.bodyBlocks).get(name);
12361
+ return node === void 0 ? null : { node, exportPath: [name], enclosingQualifiedName: null };
12362
+ }
12363
+ const className = name.slice(0, dot);
12364
+ const methodName = name.slice(dot + 1);
12365
+ let found = null;
12366
+ walkDefinitions(root, (info) => {
12367
+ if (found !== null || info.qualifiedName !== className) {
12368
+ return;
12369
+ }
12370
+ const node = info.bodyNode === null ? void 0 : singletonMethodsByName(info.bodyNode, options.bodyBlocks).get(
12371
+ methodName
12372
+ );
12373
+ if (node !== void 0) {
12374
+ found = {
12375
+ node,
12376
+ exportPath: [className, methodName],
12377
+ enclosingQualifiedName: className
12378
+ };
12379
+ }
12380
+ });
12381
+ return found;
12382
+ }
12383
+
12384
+ // src/erbTemplate.ts
12385
+ var OPEN = "<%";
12386
+ var CLOSE = "%>";
12387
+ var LITERAL_OPEN = "<%%";
12388
+ var COMMENT_MARK = "#";
12389
+ var OUTPUT_MARK = "=";
12390
+ var TRIM_OPEN_MARK = "-";
12391
+ var TRIM_CLOSE_MARK = "-";
12392
+ var CODE_CLOSE = "; ";
12393
+ var OUTPUT_CLOSE = "];";
12394
+ var OUTPUT_OPEN = "[";
12395
+ function rubyInErbTemplate(template) {
12396
+ const parts = [];
12397
+ let at = 0;
12398
+ while (at < template.length) {
12399
+ const open = template.indexOf(OPEN, at);
12400
+ if (open === -1) {
12401
+ parts.push(blanked(template.slice(at)));
12402
+ break;
12403
+ }
12404
+ parts.push(blanked(template.slice(at, open)));
12405
+ if (template.startsWith(LITERAL_OPEN, open)) {
12406
+ parts.push(blanked(LITERAL_OPEN));
12407
+ at = open + LITERAL_OPEN.length;
12408
+ continue;
12409
+ }
12410
+ const close = template.indexOf(CLOSE, open + OPEN.length);
12411
+ if (close === -1) {
12412
+ parts.push(blanked(template.slice(open)));
12413
+ break;
12414
+ }
12415
+ parts.push(tagAsRuby(template.slice(open, close + CLOSE.length)));
12416
+ at = close + CLOSE.length;
12417
+ }
12418
+ return parts.join("");
12419
+ }
12420
+ function tagAsRuby(tag) {
12421
+ const mark = tag.charAt(OPEN.length);
12422
+ if (mark === COMMENT_MARK) {
12423
+ return blanked(tag);
12424
+ }
12425
+ const prints = mark === OUTPUT_MARK;
12426
+ const codeStart = OPEN.length + (prints || mark === TRIM_OPEN_MARK ? mark.length : 0);
12427
+ const beforeClose = tag.length - CLOSE.length;
12428
+ const codeEnd = beforeClose > codeStart && tag.charAt(beforeClose - 1) === TRIM_CLOSE_MARK ? beforeClose - 1 : beforeClose;
12429
+ const opening = blanked(tag.slice(0, codeStart));
12430
+ return [
12431
+ prints ? `${OUTPUT_OPEN}${opening.slice(OUTPUT_OPEN.length)}` : opening,
12432
+ tag.slice(codeStart, codeEnd),
12433
+ blanked(tag.slice(codeEnd, beforeClose)),
12434
+ prints ? OUTPUT_CLOSE : CODE_CLOSE
12435
+ ].join("");
12436
+ }
12437
+ function blanked(text3) {
12438
+ return text3.replace(/[^\r\n]/g, " ");
12439
+ }
12440
+
12441
+ // src/facts/callbacks.ts
12442
+ function callbacksIn(patterns) {
12443
+ return patterns.flatMap(
12444
+ (pattern) => pattern.callbacks === void 0 ? [] : [pattern.callbacks]
12445
+ );
12446
+ }
12447
+ function emitClassCallbacks(db, file, classNode, declared) {
12448
+ const body = field(classNode, "body");
12449
+ if (body === null || declared.length === 0) {
12450
+ return;
12451
+ }
12452
+ const classKey = nodeId(file, classNode);
12453
+ for (const statement of runStatements(body)) {
12454
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
12455
+ continue;
12456
+ }
12457
+ const called = field(statement, "method")?.text ?? "";
12458
+ const args = readCallArgs(field(statement, "arguments"));
12459
+ for (const callbacks of declared) {
12460
+ const events = eventsOf(args, called, callbacks, db);
12461
+ for (const event of events) {
12462
+ for (const method of symbolArgumentNames(args, db)) {
12463
+ db.add("classCallback", [classKey, event, method]);
12464
+ }
12465
+ }
12466
+ }
12467
+ }
12468
+ }
12469
+ function eventsOf(args, called, callbacks, db) {
12470
+ const covered = callbacks.registeredBy[called];
12471
+ if (covered === void 0) {
12472
+ return [];
12473
+ }
12474
+ const written = args.keyword[callbacks.eventKeyword];
12475
+ const narrowed = written === void 0 ? null : stringValueOf(written, db);
12476
+ if (narrowed === null) {
12477
+ return covered;
12478
+ }
12479
+ return covered.filter((event) => event === narrowed);
12480
+ }
12481
+
12482
+ // src/facts.ts
12483
+ var import_node_path3 = __toESM(require("path"), 1);
12484
+ var import_extractor17 = require("@suss/extractor");
12485
+ function emitRequireFacts(db, filePath, root, known) {
12486
+ (0, import_extractor17.walkDescendants)(root, null, children, {
12487
+ at: (node) => {
12488
+ const target = requiredRelativePath(node);
12489
+ if (target === null) {
12490
+ return;
12491
+ }
12492
+ const resolved = import_node_path3.default.resolve(
12493
+ import_node_path3.default.dirname(filePath),
12494
+ target.endsWith(".rb") ? target : `${target}.rb`
12495
+ );
12496
+ if (known.has(resolved)) {
12497
+ db.add("importsFile", [filePath, resolved]);
12498
+ }
12499
+ },
12500
+ into: (node) => node.type === "argument_list" ? import_extractor17.SKIP_CHILDREN : null
12501
+ });
12502
+ }
12503
+ function requiredRelativePath(node) {
12504
+ if (node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== "require_relative") {
12505
+ return null;
12506
+ }
12507
+ const { positional } = readCallArgs(field(node, "arguments"));
12508
+ return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
12509
+ }
12510
+
11928
12511
  // src/moduleSurface.ts
12512
+ var import_behavioral_ir8 = require("@suss/behavioral-ir");
12513
+ var import_extractor18 = require("@suss/extractor");
11929
12514
  function settleRubyModules(modules) {
11930
- return (0, import_extractor17.settleModules)(modules, ["{name}.rb", "../{name}.rb"]);
12515
+ return (0, import_extractor18.settleModules)(modules, ["{name}.rb", "../{name}.rb"]);
11931
12516
  }
11932
12517
  function moduleExportUnits(root, file, options) {
11933
- const module2 = (0, import_extractor17.moduleOfFile)(options.modules, file);
12518
+ const module2 = (0, import_extractor18.moduleOfFile)(options.modules, file);
11934
12519
  if (module2 === void 0 || !module2.public.includes(file)) {
11935
12520
  return [];
11936
12521
  }
@@ -11958,7 +12543,7 @@ function moduleExportUnits(root, file, options) {
11958
12543
  },
11959
12544
  options,
11960
12545
  (0, import_behavioral_ir8.moduleExportBinding)({
11961
- recognition: import_extractor17.MODULE_SURFACE_RECOGNITION,
12546
+ recognition: import_extractor18.MODULE_SURFACE_RECOGNITION,
11962
12547
  module: module2.name,
11963
12548
  exportName: name
11964
12549
  })
@@ -12266,7 +12851,7 @@ var WalkReplay = class {
12266
12851
  };
12267
12852
 
12268
12853
  // src/testFactories.ts
12269
- var import_values29 = require("@suss/values");
12854
+ var import_values31 = require("@suss/values");
12270
12855
  function factoriesIn(packs) {
12271
12856
  return packs.flatMap((pack) => pack.factories ?? []);
12272
12857
  }
@@ -12328,7 +12913,7 @@ function classRefOf(db, file, node) {
12328
12913
  if (node.type === "constant" || node.type === "scope_resolution") {
12329
12914
  return { key: readKey(file, node, null) };
12330
12915
  }
12331
- const written = (0, import_values29.literalOf)(evaluatedValue(node, db));
12916
+ const written = (0, import_values31.literalOf)(evaluatedValue(node, db));
12332
12917
  if (written === null) {
12333
12918
  return null;
12334
12919
  }
@@ -12337,7 +12922,7 @@ function classRefOf(db, file, node) {
12337
12922
  };
12338
12923
  }
12339
12924
  function nameOf(db, node) {
12340
- return symbolValue(node) ?? (0, import_values29.literalOf)(evaluatedValue(node, db));
12925
+ return symbolValue(node) ?? (0, import_values31.literalOf)(evaluatedValue(node, db));
12341
12926
  }
12342
12927
  var FactoryClasses = class {
12343
12928
  constructor(db, definitions, inflections) {
@@ -12409,7 +12994,7 @@ function builtAt(db, call, pattern) {
12409
12994
 
12410
12995
  // src/testSharedGroups.ts
12411
12996
  var import_resolution15 = require("@suss/resolution");
12412
- var import_values31 = require("@suss/values");
12997
+ var import_values33 = require("@suss/values");
12413
12998
  var ByNode = class {
12414
12999
  byTree = /* @__PURE__ */ new WeakMap();
12415
13000
  set(node, value) {
@@ -12513,7 +13098,7 @@ var SharedGroupIndex = class {
12513
13098
  }
12514
13099
  nameOf(call) {
12515
13100
  const first = firstArgument(call);
12516
- return first === null ? null : (0, import_values31.literalOf)(evaluatedValue(first, this.db));
13101
+ return first === null ? null : (0, import_values33.literalOf)(evaluatedValue(first, this.db));
12517
13102
  }
12518
13103
  noteRead(definition) {
12519
13104
  if (definition !== null) {
@@ -12523,12 +13108,12 @@ var SharedGroupIndex = class {
12523
13108
  };
12524
13109
  function emitSharedGroupBinds(db, index, files) {
12525
13110
  for (const at of files) {
12526
- for (const site of sitesIn(index, at)) {
13111
+ for (const site of sitesIn2(index, at)) {
12527
13112
  emitSiteBinds(db, index, site, at.pattern);
12528
13113
  }
12529
13114
  }
12530
13115
  }
12531
- function sitesIn(index, at) {
13116
+ function sitesIn2(index, at) {
12532
13117
  const found = [];
12533
13118
  const visit = (node) => {
12534
13119
  for (const child of children(node)) {
@@ -12590,16 +13175,16 @@ function visibleAt(index, site, pattern) {
12590
13175
  }
12591
13176
 
12592
13177
  // src/version.ts
12593
- var import_extractor18 = require("@suss/extractor");
13178
+ var import_extractor19 = require("@suss/extractor");
12594
13179
  var ADAPTER_VERSION = "0.1.0";
12595
- var adapterStamp = (0, import_extractor18.createAdapterStamp)({
13180
+ var adapterStamp = (0, import_extractor19.createAdapterStamp)({
12596
13181
  moduleUrl: importMetaUrl,
12597
13182
  version: ADAPTER_VERSION
12598
13183
  });
12599
13184
 
12600
13185
  // src/project.ts
12601
13186
  function keptRubyParses() {
12602
- return new import_extractor19.KeptParses((root) => root.tree.delete());
13187
+ return new import_extractor20.KeptParses((root) => root.tree.delete());
12603
13188
  }
12604
13189
  async function parseRubyAhead(files, kept) {
12605
13190
  for (const file of files) {
@@ -12704,7 +13289,7 @@ var RunFacts = class {
12704
13289
  ...this.testFiles.map(({ file }) => file),
12705
13290
  ...this.parsed.map(({ file }) => file).filter(
12706
13291
  (file) => classTests.some(
12707
- (one) => (0, import_extractor19.matchesTestFileName)(file, one.filePatterns)
13292
+ (one) => (0, import_extractor20.matchesTestFileName)(file, one.filePatterns)
12708
13293
  )
12709
13294
  )
12710
13295
  ]);
@@ -12739,7 +13324,7 @@ function factsForFile(options) {
12739
13324
  return facts.db;
12740
13325
  }
12741
13326
  function moduleInitUnit(options) {
12742
- const raw = (0, import_extractor19.moduleInitStructure)({
13327
+ const raw = (0, import_extractor20.moduleInitStructure)({
12743
13328
  name: options.name,
12744
13329
  file: options.file,
12745
13330
  range: options.range,
@@ -12769,31 +13354,49 @@ function replaysCleanly(units, discovered) {
12769
13354
  }
12770
13355
  return true;
12771
13356
  }
13357
+ function walkedFilesOf(options) {
13358
+ const given = new Set(options.files);
13359
+ const templates = [
13360
+ ...new Set(options.packs.flatMap((pack) => pack.bootTemplates?.() ?? []))
13361
+ ].filter((file) => !given.has(file)).sort();
13362
+ return {
13363
+ files: [...options.files, ...templates],
13364
+ templates: new Set(templates)
13365
+ };
13366
+ }
13367
+ function rubySourceOf(absPath, templates) {
13368
+ if (!import_node_fs3.default.existsSync(absPath)) {
13369
+ return null;
13370
+ }
13371
+ const text3 = import_node_fs3.default.readFileSync(absPath, "utf8");
13372
+ return templates.has(absPath) ? rubyInErbTemplate(text3) : text3;
13373
+ }
12772
13374
  async function extractRubyProject(options) {
12773
- const timer = options.onTiming !== void 0 ? (0, import_extractor19.createTimer)() : (0, import_extractor19.noopTimer)();
13375
+ const walked = walkedFilesOf(options);
13376
+ const timer = options.onTiming !== void 0 ? (0, import_extractor20.createTimer)() : (0, import_extractor20.noopTimer)();
12774
13377
  const cacheDir = adapterStamp.declineWhenRunFromSource(
12775
- options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? (0, import_extractor19.extractionCacheDirFor)(options.projectRoot) : null)
13378
+ options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? (0, import_extractor20.extractionCacheDirFor)(options.projectRoot) : null)
12776
13379
  );
12777
- const layer = (0, import_extractor19.createCacheLayer)(cacheDir);
13380
+ const layer = (0, import_extractor20.createCacheLayer)(cacheDir);
12778
13381
  const packsDigest = `${adapterStamp.packsDigest(
12779
13382
  options.packs.map(
12780
13383
  (pack) => pack.version !== void 0 ? { name: pack.name, version: pack.version } : { name: pack.name }
12781
13384
  )
12782
- )}|${(0, import_extractor19.extractionConfigStamp)({
13385
+ )}|${(0, import_extractor20.extractionConfigStamp)({
12783
13386
  gapHandling: options.gapHandling,
12784
13387
  workspaceRoot: options.workspaceRoot,
12785
13388
  projectRoot: options.projectRoot,
12786
13389
  modules: settleRubyModules(options.modules)
12787
13390
  })}`;
12788
13391
  const input = {
12789
- files: cacheDir === null ? [] : options.files,
12790
- adapterPacksDigest: cacheDir === null ? packsDigest : (0, import_extractor19.runDigest)(packsDigest, options.packs, options.files)
13392
+ files: cacheDir === null ? [] : walked.files,
13393
+ adapterPacksDigest: cacheDir === null ? packsDigest : (0, import_extractor20.runDigest)(packsDigest, options.packs, walked.files)
12791
13394
  };
12792
13395
  const lookup = await timer.timeAsync(
12793
13396
  "cache.lookup",
12794
13397
  () => layer.lookup(input)
12795
13398
  );
12796
- const composed = (stored) => (0, import_extractor19.composeWrappers)(stored, {
13399
+ const composed = (stored) => (0, import_extractor20.composeWrappers)(stored, {
12797
13400
  gapHandling: options.gapHandling ?? "permissive"
12798
13401
  });
12799
13402
  if (lookup.kind === "hit") {
@@ -12815,7 +13418,7 @@ async function extractRubyProject(options) {
12815
13418
  const records = plan === null ? null : (0, import_resolution16.recordsToReplay)(plan.roots, plan.changed, plan.removed);
12816
13419
  if (plan !== null && records !== null) {
12817
13420
  try {
12818
- return await runRuby(options, cache, { plan, records });
13421
+ return await runRuby(options, walked, cache, { plan, records });
12819
13422
  } catch (error) {
12820
13423
  if (!(error instanceof ReplayFailed)) {
12821
13424
  throw error;
@@ -12823,7 +13426,7 @@ async function extractRubyProject(options) {
12823
13426
  }
12824
13427
  }
12825
13428
  options.onCacheDiagnostic?.(lookup.diagnostic);
12826
- return runRuby(options, cache, null);
13429
+ return runRuby(options, walked, cache, null);
12827
13430
  }
12828
13431
  function seedFromStored(seed, nodeOf) {
12829
13432
  const node = nodeOf(seed.key);
@@ -12840,13 +13443,13 @@ function seedFromStored(seed, nodeOf) {
12840
13443
  function addForSeed(bySeed, seedKey, summary) {
12841
13444
  bySeed.set(seedKey, [...bySeed.get(seedKey) ?? [], summary]);
12842
13445
  }
12843
- async function runRuby(options, cache, previous) {
13446
+ async function runRuby(options, walked, cache, previous) {
12844
13447
  const { timer, cacheDir } = cache;
12845
13448
  const db = new import_datalog3.Database();
12846
13449
  const summaries = [];
12847
13450
  const gapHandling = options.gapHandling ?? "permissive";
12848
- const tallies = (0, import_extractor12.createPackTallies)(options.packs);
12849
- const ledger = cacheDir === null ? null : new import_resolution16.DependencyLedger(options.files);
13451
+ const tallies = (0, import_extractor13.createPackTallies)(options.packs);
13452
+ const ledger = cacheDir === null ? null : new import_resolution16.DependencyLedger(walked.files);
12850
13453
  const log = ledger === null ? null : new import_resolution16.FactLog(db, ledger);
12851
13454
  if (ledger !== null) {
12852
13455
  (0, import_resolution16.observeDemand)(db, ledger);
@@ -12854,12 +13457,12 @@ async function runRuby(options, cache, previous) {
12854
13457
  const parses = options.keptParses ?? keptRubyParses();
12855
13458
  const trees = createFileCache(
12856
13459
  (source, absPath) => parses.parse(absPath, source, parseRubyRoot),
12857
- (absPath) => import_node_fs3.default.existsSync(absPath) ? import_node_fs3.default.readFileSync(absPath, "utf8") : null
13460
+ (absPath) => rubySourceOf(absPath, walked.templates)
12858
13461
  );
12859
13462
  const fileCache = ledger === null ? trees : watchFileCache(trees, ledger);
12860
13463
  const facts = new RunFacts(db, options.packs);
12861
13464
  const { parsed, bodyBlocks, scopeCalls } = facts;
12862
- for (const file of options.files) {
13465
+ for (const file of walked.files) {
12863
13466
  await timer.timeAsync("parse", async () => {
12864
13467
  const root = await trees.get(file);
12865
13468
  if (root !== null) {
@@ -12869,7 +13472,7 @@ async function runRuby(options, cache, previous) {
12869
13472
  }
12870
13473
  });
12871
13474
  }
12872
- options.keptParses?.keepOnly(options.files);
13475
+ options.keptParses?.keepOnly(walked.files);
12873
13476
  const rootsByFile = new Map(parsed.map(({ file, root }) => [file, root]));
12874
13477
  const dynamicNames = timer.time("discover", () => {
12875
13478
  log?.startJoined();
@@ -12947,7 +13550,7 @@ async function runRuby(options, cache, previous) {
12947
13550
  continue;
12948
13551
  }
12949
13552
  discovered.add(unit.dedup);
12950
- (0, import_extractor12.tallyUnit)(tallies, unit.recognition);
13553
+ (0, import_extractor13.tallyUnit)(tallies, unit.recognition);
12951
13554
  found.push({
12952
13555
  replayed: unit.summary ?? null,
12953
13556
  seedKey: unit.seed?.key ?? null
@@ -12995,6 +13598,16 @@ async function runRuby(options, cache, previous) {
12995
13598
  displayPathOf,
12996
13599
  facts: db,
12997
13600
  onReachSeed
13601
+ }),
13602
+ ...declaredEntryUnits(root, file, {
13603
+ packs: options.packs,
13604
+ ...storage === void 0 ? {} : { storage },
13605
+ inheritedMethods,
13606
+ bodyBlocks,
13607
+ dynamicNames: watchedNames,
13608
+ displayPathOf,
13609
+ facts: db,
13610
+ onReachSeed
12998
13611
  })
12999
13612
  ];
13000
13613
  const rawUnits = await timer.timeAsync(
@@ -13028,7 +13641,7 @@ async function runRuby(options, cache, previous) {
13028
13641
  continue;
13029
13642
  }
13030
13643
  discovered.add(dedup);
13031
- (0, import_extractor12.tallyUnit)(tallies, recognition);
13644
+ (0, import_extractor13.tallyUnit)(tallies, recognition);
13032
13645
  found.push({ raw, seedKey, draft: units[units.length - 1] });
13033
13646
  if (seed !== void 0 && seedKey !== null && !seedKeys.has(seedKey)) {
13034
13647
  seedKeys.add(seedKey);
@@ -13043,7 +13656,7 @@ async function runRuby(options, cache, previous) {
13043
13656
  seedKeys.add(moduleKey);
13044
13657
  seeds.push(moduleSeed);
13045
13658
  const readLoadTime = () => ({
13046
- reads: envReadEffects(root, { db, file }),
13659
+ reads: envReadEffects(root, { db, file }).map((one) => one.effect),
13047
13660
  calls: moduleScopeInvocationEffects(
13048
13661
  root,
13049
13662
  inheritedMethods,
@@ -13091,7 +13704,7 @@ async function runRuby(options, cache, previous) {
13091
13704
  }
13092
13705
  const summary = timer.time(
13093
13706
  "summarize",
13094
- () => (0, import_extractor19.assembleSummary)(routingGapUnit(pattern, gaps), { gapHandling })
13707
+ () => (0, import_extractor20.assembleSummary)(routingGapUnit(pattern, gaps), { gapHandling })
13095
13708
  );
13096
13709
  summary.confidence = { source: "inferred_static", level: "low" };
13097
13710
  found.push({ summary });
@@ -13147,7 +13760,7 @@ async function runRuby(options, cache, previous) {
13147
13760
  }
13148
13761
  const summary = timer.time(
13149
13762
  "summarize",
13150
- () => (0, import_extractor19.assembleSummary)(raw, { gapHandling })
13763
+ () => (0, import_extractor20.assembleSummary)(raw, { gapHandling })
13151
13764
  );
13152
13765
  summaries.push(summary);
13153
13766
  assembledHere.add(summary);
@@ -13194,8 +13807,8 @@ async function runRuby(options, cache, previous) {
13194
13807
  throw new ReplayFailed("");
13195
13808
  }
13196
13809
  const dependencies = (0, import_resolution16.importedFilesByFile)(db, displayPathOf);
13197
- (0, import_extractor19.stampModuleImports)(summaries, (file) => dependencies.get(file) ?? []);
13198
- (0, import_extractor19.stampModules)(summaries, declaredModules, options.workspaceRoot);
13810
+ (0, import_extractor20.stampModuleImports)(summaries, (file) => dependencies.get(file) ?? []);
13811
+ (0, import_extractor20.stampModules)(summaries, declaredModules, options.workspaceRoot);
13199
13812
  const idRoot = options.projectRoot ?? options.workspaceRoot;
13200
13813
  for (const summary of summaries) {
13201
13814
  const absoluteFile = options.workspaceRoot === void 0 ? summary.location.file : import_node_path4.default.resolve(options.workspaceRoot, summary.location.file);
@@ -13215,7 +13828,7 @@ async function runRuby(options, cache, previous) {
13215
13828
  db,
13216
13829
  options.workspaceRoot
13217
13830
  );
13218
- return (0, import_extractor19.composeWrappers)(summaries, { gapHandling });
13831
+ return (0, import_extractor20.composeWrappers)(summaries, { gapHandling });
13219
13832
  });
13220
13833
  const walkedFiles = new Map(
13221
13834
  parsed.map(({ file }) => [displayPathOf(file), file])
@@ -13275,7 +13888,7 @@ async function runRuby(options, cache, previous) {
13275
13888
  buildRubyExtractionReport({
13276
13889
  packs: options.packs,
13277
13890
  tallies,
13278
- filesWalked: options.files.length,
13891
+ filesWalked: walked.files.length,
13279
13892
  summaries: composed
13280
13893
  })
13281
13894
  );
@@ -13307,7 +13920,7 @@ function rubyAttribution(args) {
13307
13920
  }));
13308
13921
  return {
13309
13922
  roots,
13310
- owners: (0, import_extractor19.ownersWhereWritten)(args.summaries, args.walkedFileOf)
13923
+ owners: (0, import_extractor20.ownersWhereWritten)(args.summaries, args.walkedFileOf)
13311
13924
  };
13312
13925
  }
13313
13926
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
@@ -13501,7 +14114,7 @@ var RubyWhySession = class {
13501
14114
  // src/stubEvidence.ts
13502
14115
  var import_node_fs5 = __toESM(require("fs"), 1);
13503
14116
  var import_node_path6 = __toESM(require("path"), 1);
13504
- var import_extractor20 = require("@suss/extractor");
14117
+ var import_extractor21 = require("@suss/extractor");
13505
14118
  function bareLetters(name) {
13506
14119
  return name.replaceAll(/[-_]/g, "").toLowerCase();
13507
14120
  }
@@ -13525,14 +14138,14 @@ function requireTarget(node) {
13525
14138
  }
13526
14139
  function requireSitesIn(root, file) {
13527
14140
  const found = [];
13528
- (0, import_extractor20.walkDescendants)(root, null, children, {
14141
+ (0, import_extractor21.walkDescendants)(root, null, children, {
13529
14142
  at: (node) => {
13530
14143
  const target = requireTarget(node);
13531
14144
  if (target !== null) {
13532
14145
  found.push({ target, file, line: rangeOf(node).start });
13533
14146
  }
13534
14147
  },
13535
- into: (node) => node.type === "argument_list" ? import_extractor20.SKIP_CHILDREN : null
14148
+ into: (node) => node.type === "argument_list" ? import_extractor21.SKIP_CHILDREN : null
13536
14149
  });
13537
14150
  return found;
13538
14151
  }