@suss/adapter-ruby 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
29
29
  "module",
30
30
  "singleton_class"
31
31
  ]);
32
+ var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
32
33
  var ARGUMENT_LIST_TYPE = "argument_list";
33
34
  function runStatements(body, blockConfigures = () => false) {
34
35
  const found = [];
@@ -46,6 +47,22 @@ function runStatements(body, blockConfigures = () => false) {
46
47
  });
47
48
  return found;
48
49
  }
50
+ function stringLiteralValue(node) {
51
+ if (node.type !== "string") {
52
+ return null;
53
+ }
54
+ let content = "";
55
+ for (const child of bodyStatements(node)) {
56
+ if (child.type === "string_content") {
57
+ content += child.text;
58
+ continue;
59
+ }
60
+ if (child.type === "interpolation") {
61
+ return null;
62
+ }
63
+ }
64
+ return content;
65
+ }
49
66
  function symbolValue(node) {
50
67
  return node.type === "simple_symbol" ? node.text.slice(1) : null;
51
68
  }
@@ -62,6 +79,71 @@ function instanceMethodsByName(body) {
62
79
  }
63
80
  return methods;
64
81
  }
82
+ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
83
+ "private",
84
+ "protected",
85
+ "public"
86
+ ]);
87
+ function visibilityKeyword(text) {
88
+ return VISIBILITY_KEYWORDS.has(text) ? text : null;
89
+ }
90
+ function setVisibility(visibility, name, keyword) {
91
+ if (keyword === "public") {
92
+ visibility.delete(name);
93
+ return;
94
+ }
95
+ visibility.set(name, keyword);
96
+ }
97
+ function markCalledOutMethods(call, keyword, visibility) {
98
+ const argumentList = field(call, "arguments");
99
+ if (argumentList === null) {
100
+ return;
101
+ }
102
+ for (const arg of bodyStatements(argumentList)) {
103
+ const name = arg.type === "method" ? field(arg, "name")?.text : symbolValue(arg);
104
+ if (name !== void 0 && name !== null) {
105
+ setVisibility(visibility, name, keyword);
106
+ }
107
+ }
108
+ }
109
+ function instanceMethodVisibility(body) {
110
+ const visibility = /* @__PURE__ */ new Map();
111
+ let mode = "public";
112
+ for (const stmt of bodyStatements(body)) {
113
+ if (stmt.type === "method") {
114
+ const name = field(stmt, "name")?.text;
115
+ if (name !== void 0) {
116
+ setVisibility(visibility, name, mode);
117
+ }
118
+ continue;
119
+ }
120
+ if (stmt.type === "identifier") {
121
+ mode = visibilityKeyword(stmt.text) ?? mode;
122
+ continue;
123
+ }
124
+ if (stmt.type !== "call" || field(stmt, "receiver") !== null) {
125
+ continue;
126
+ }
127
+ const keyword = visibilityKeyword(field(stmt, "method")?.text ?? "");
128
+ if (keyword !== null) {
129
+ markCalledOutMethods(stmt, keyword, visibility);
130
+ }
131
+ }
132
+ return visibility;
133
+ }
134
+ function singletonMethodsByName(body) {
135
+ const methods = /* @__PURE__ */ new Map();
136
+ for (const stmt of runStatements(body)) {
137
+ if (stmt.type !== "singleton_method") {
138
+ continue;
139
+ }
140
+ const name = field(stmt, "name")?.text;
141
+ if (name !== void 0) {
142
+ methods.set(name, stmt);
143
+ }
144
+ }
145
+ return methods;
146
+ }
65
147
  function bareCallArgumentGroups(body, name) {
66
148
  const groups = [];
67
149
  for (const stmt of runStatements(body)) {
@@ -249,7 +331,10 @@ async function reachDefinition(qualifiedName, lookup) {
249
331
  qualifiedName,
250
332
  lookup.pathConvention
251
333
  );
252
- const fileRoot = filePath === null ? null : await lookup.parsedFile(filePath);
334
+ if (filePath === null) {
335
+ return null;
336
+ }
337
+ const fileRoot = await lookup.parsedFile(filePath);
253
338
  if (fileRoot === null) {
254
339
  return null;
255
340
  }
@@ -260,7 +345,7 @@ async function reachDefinition(qualifiedName, lookup) {
260
345
  return null;
261
346
  }
262
347
  const knownClasses = new Set(all.map((info) => info.qualifiedName));
263
- return matches.map((info) => ({ info, knownClasses }));
348
+ return matches.map((info) => ({ info, knownClasses, file: filePath }));
264
349
  }
265
350
  function ancestryOf(name, blocks, lookup) {
266
351
  return chainOf(
@@ -352,17 +437,19 @@ function methodInAncestry(ancestry, name) {
352
437
  if (entry.type === "unfollowed") {
353
438
  return {
354
439
  type: "unsettled",
355
- reason: `inherited from ${entry.name}, which this run did not read`
440
+ reason: `inherited from ${entry.name}, which this run did not read`,
441
+ cause: "unreadAncestor"
356
442
  };
357
443
  }
358
444
  const found = definitionIn(entry.blocks, name);
359
- if (found.method !== null) {
360
- return { type: "found", method: found.method };
445
+ if (found.method !== null && found.block !== null) {
446
+ return { type: "found", method: found.method, block: found.block };
361
447
  }
362
448
  if (found.dynamic) {
363
449
  return {
364
450
  type: "unsettled",
365
- reason: "defined with define_method, which this reader does not follow"
451
+ reason: "defined with define_method, which this reader does not follow",
452
+ cause: "dynamicDefine"
366
453
  };
367
454
  }
368
455
  }
@@ -370,16 +457,21 @@ function methodInAncestry(ancestry, name) {
370
457
  }
371
458
  function definitionIn(blocks, name) {
372
459
  let method = null;
460
+ let block = null;
373
461
  let dynamic = false;
374
- for (const block of blocks) {
375
- const body = block.info.bodyNode;
462
+ for (const candidate of blocks) {
463
+ const body = candidate.info.bodyNode;
376
464
  if (body === null) {
377
465
  continue;
378
466
  }
379
- method = instanceMethodsByName(body).get(name) ?? method;
467
+ const found = instanceMethodsByName(body).get(name);
468
+ if (found !== void 0) {
469
+ method = found;
470
+ block = candidate;
471
+ }
380
472
  dynamic ||= bareCallArgumentGroups(body, DEFINE_METHOD_CALL).length > 0;
381
473
  }
382
- return { method, dynamic };
474
+ return { method, block, dynamic };
383
475
  }
384
476
  function inheritedStatements(ancestry, blockConfigures) {
385
477
  const statements = [];
@@ -401,9 +493,114 @@ function inheritedStatements(ancestry, blockConfigures) {
401
493
  }
402
494
 
403
495
  // src/discovery.ts
404
- import { dispatchByType, graphqlResolverBinding } from "@suss/behavioral-ir";
496
+ import {
497
+ dispatchByType as dispatchByType2,
498
+ graphqlResolverBinding,
499
+ restBinding
500
+ } from "@suss/behavioral-ir";
405
501
  import { unreadableReading } from "@suss/extractor";
406
502
 
503
+ // src/envReads.ts
504
+ import { runtimeConfigBinding } from "@suss/behavioral-ir";
505
+ import { SKIP_CHILDREN as SKIP_CHILDREN2, walkDescendants as walkDescendants2 } from "@suss/extractor";
506
+ var RUBY_ENV_RECOGNITION = "ruby-env";
507
+ var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
508
+ function envReadEffects(root) {
509
+ const effects = [];
510
+ walkDescendants2(root, null, {
511
+ at: (node) => {
512
+ const read = envReadAt(node);
513
+ if (read !== null) {
514
+ effects.push(configReadEffect(read));
515
+ }
516
+ },
517
+ into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? SKIP_CHILDREN2 : null
518
+ });
519
+ return effects;
520
+ }
521
+ function configReadEffect(read) {
522
+ return {
523
+ type: "interaction",
524
+ binding: runtimeConfigBinding({
525
+ recognition: RUBY_ENV_RECOGNITION,
526
+ deploymentTarget: "lambda",
527
+ instanceName: "<unknown>"
528
+ }),
529
+ callee: `ENV["${read.name}"]`,
530
+ interaction: {
531
+ class: "config-read",
532
+ name: read.name,
533
+ defaulted: read.defaulted
534
+ }
535
+ };
536
+ }
537
+ function envReadAt(node) {
538
+ if (node.type === "element_reference") {
539
+ return elementRead(node);
540
+ }
541
+ if (node.type === "call") {
542
+ return fetchRead(node);
543
+ }
544
+ return null;
545
+ }
546
+ function elementRead(node) {
547
+ const object2 = field(node, "object");
548
+ if (object2 === null || !isEnv(object2) || isAssignedTo(node)) {
549
+ return null;
550
+ }
551
+ const index = node.namedChildren.find(
552
+ (child) => child !== null && child.id !== object2.id
553
+ );
554
+ const name = index === void 0 ? null : stringLiteralValue(index);
555
+ if (name === null) {
556
+ return null;
557
+ }
558
+ return { name, defaulted: isDefaultedAt(node) };
559
+ }
560
+ function fetchRead(node) {
561
+ const receiver = field(node, "receiver");
562
+ if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
563
+ return null;
564
+ }
565
+ const { positional } = readCallArgs(field(node, "arguments"));
566
+ const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
567
+ if (name === null) {
568
+ return null;
569
+ }
570
+ const hasDefault = positional.length > 1 || field(node, "block") !== null;
571
+ return { name, defaulted: hasDefault || isDefaultedAt(node) };
572
+ }
573
+ function isEnv(node) {
574
+ if (node.type === "constant") {
575
+ return node.text === "ENV";
576
+ }
577
+ return node.type === "scope_resolution" && field(node, "scope") === null && field(node, "name")?.text === "ENV";
578
+ }
579
+ function isAssignedTo(node) {
580
+ const parent = node.parent;
581
+ return parent !== null && (parent.type === "assignment" || parent.type === "operator_assignment") && field(parent, "left")?.id === node.id;
582
+ }
583
+ function isDefaultedAt(node) {
584
+ let child = node;
585
+ let parent = node.parent;
586
+ while (parent !== null) {
587
+ if (parent.type === "parenthesized_statements") {
588
+ child = parent;
589
+ parent = parent.parent;
590
+ continue;
591
+ }
592
+ if (parent.type !== "binary" || field(parent, "operator")?.text !== "||") {
593
+ return false;
594
+ }
595
+ if (field(parent, "left")?.id === child.id) {
596
+ return true;
597
+ }
598
+ child = parent;
599
+ parent = parent.parent;
600
+ }
601
+ return false;
602
+ }
603
+
407
604
  // src/paths/effects.ts
408
605
  import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
409
606
 
@@ -656,8 +853,18 @@ function withoutChainLinks(calls) {
656
853
  }
657
854
  return calls.filter((call) => !isLink.has(call.id));
658
855
  }
856
+ function calleeMethodName(call) {
857
+ const method = field(call, "method");
858
+ if (method !== null) {
859
+ return method.text;
860
+ }
861
+ return field(call, "receiver") !== null ? "call" : children(call)[0]?.text;
862
+ }
659
863
  function isPropertyRead(node) {
660
- return field(node, "receiver") !== null && field(node, "arguments") === null;
864
+ if (field(node, "receiver") === null || field(node, "arguments") !== null) {
865
+ return false;
866
+ }
867
+ return calleeMethodName(node) !== "call";
661
868
  }
662
869
  function isRaise2(node) {
663
870
  const method = field(node, "method") ?? children(node)[0] ?? null;
@@ -677,11 +884,11 @@ function bodyCalls(node, found = []) {
677
884
  }
678
885
  function calleeText(call) {
679
886
  const receiver = field(call, "receiver");
680
- const method = field(call, "method") ?? children(call)[0];
887
+ const method = calleeMethodName(call);
681
888
  if (method === void 0) {
682
889
  return call.text;
683
890
  }
684
- return receiver === null ? method.text : `${receiver.text}.${method.text}`;
891
+ return receiver === null ? method : `${receiver.text}.${method}`;
685
892
  }
686
893
  function argOf(node) {
687
894
  const literal2 = LITERAL_ARGS[node.type]?.(node);
@@ -752,7 +959,7 @@ function invocationEffects(definitionNode) {
752
959
  }
753
960
 
754
961
  // src/storage.ts
755
- import { deriveOnDemand, evaluate } from "@suss/datalog";
962
+ import { deriveOnDemand, evaluate as evaluate2 } from "@suss/datalog";
756
963
 
757
964
  // ../../../node_modules/zod/v4/classic/external.js
758
965
  var external_exports = {};
@@ -1434,8 +1641,8 @@ function assertNever(_x) {
1434
1641
  function assert(_) {
1435
1642
  }
1436
1643
  function getEnumValues(entries) {
1437
- const numericValues = Object.values(entries).filter((v2) => typeof v2 === "number");
1438
- const values = Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v2]) => v2);
1644
+ const numericValues = Object.values(entries).filter((v4) => typeof v4 === "number");
1645
+ const values = Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v4]) => v4);
1439
1646
  return values;
1440
1647
  }
1441
1648
  function joinValues(array2, separator = "|") {
@@ -1489,9 +1696,9 @@ function defineLazy(object2, key, getter) {
1489
1696
  }
1490
1697
  return value;
1491
1698
  },
1492
- set(v2) {
1699
+ set(v4) {
1493
1700
  Object.defineProperty(object2, key, {
1494
- value: v2
1701
+ value: v4
1495
1702
  // configurable: true,
1496
1703
  });
1497
1704
  },
@@ -1520,10 +1727,10 @@ function mergeDefs(...defs) {
1520
1727
  function cloneDef(schema) {
1521
1728
  return mergeDefs(schema._zod.def);
1522
1729
  }
1523
- function getElementAtPath(obj, path4) {
1524
- if (!path4)
1730
+ function getElementAtPath(obj, path7) {
1731
+ if (!path7)
1525
1732
  return obj;
1526
- return path4.reduce((acc, key) => acc?.[key], obj);
1733
+ return path7.reduce((acc, key) => acc?.[key], obj);
1527
1734
  }
1528
1735
  function promiseAllObject(promisesObj) {
1529
1736
  const keys = Object.keys(promisesObj);
@@ -1932,11 +2139,11 @@ function explicitlyAborted(x, startIndex = 0) {
1932
2139
  }
1933
2140
  return false;
1934
2141
  }
1935
- function prefixIssues(path4, issues) {
2142
+ function prefixIssues(path7, issues) {
1936
2143
  return issues.map((iss) => {
1937
2144
  var _a3;
1938
2145
  (_a3 = iss).path ?? (_a3.path = []);
1939
- iss.path.unshift(path4);
2146
+ iss.path.unshift(path7);
1940
2147
  return iss;
1941
2148
  });
1942
2149
  }
@@ -2083,16 +2290,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2083
2290
  }
2084
2291
  function formatError(error51, mapper = (issue2) => issue2.message) {
2085
2292
  const fieldErrors = { _errors: [] };
2086
- const processError = (error52, path4 = []) => {
2293
+ const processError = (error52, path7 = []) => {
2087
2294
  for (const issue2 of error52.issues) {
2088
2295
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2089
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
2296
+ issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
2090
2297
  } else if (issue2.code === "invalid_key") {
2091
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2298
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
2092
2299
  } else if (issue2.code === "invalid_element") {
2093
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2300
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
2094
2301
  } else {
2095
- const fullpath = [...path4, ...issue2.path];
2302
+ const fullpath = [...path7, ...issue2.path];
2096
2303
  if (fullpath.length === 0) {
2097
2304
  fieldErrors._errors.push(mapper(issue2));
2098
2305
  } else {
@@ -2119,17 +2326,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2119
2326
  }
2120
2327
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2121
2328
  const result = { errors: [] };
2122
- const processError = (error52, path4 = []) => {
2329
+ const processError = (error52, path7 = []) => {
2123
2330
  var _a3, _b;
2124
2331
  for (const issue2 of error52.issues) {
2125
2332
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2126
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
2333
+ issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
2127
2334
  } else if (issue2.code === "invalid_key") {
2128
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2335
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
2129
2336
  } else if (issue2.code === "invalid_element") {
2130
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2337
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
2131
2338
  } else {
2132
- const fullpath = [...path4, ...issue2.path];
2339
+ const fullpath = [...path7, ...issue2.path];
2133
2340
  if (fullpath.length === 0) {
2134
2341
  result.errors.push(mapper(issue2));
2135
2342
  continue;
@@ -2161,8 +2368,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2161
2368
  }
2162
2369
  function toDotPath(_path) {
2163
2370
  const segs = [];
2164
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2165
- for (const seg of path4) {
2371
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2372
+ for (const seg of path7) {
2166
2373
  if (typeof seg === "number")
2167
2374
  segs.push(`[${seg}]`);
2168
2375
  else if (typeof seg === "symbol")
@@ -3178,10 +3385,10 @@ var $ZodUUID = /* @__PURE__ */ $constructor("$ZodUUID", (inst, def) => {
3178
3385
  v7: 7,
3179
3386
  v8: 8
3180
3387
  };
3181
- const v2 = versionMap[def.version];
3182
- if (v2 === void 0)
3388
+ const v4 = versionMap[def.version];
3389
+ if (v4 === void 0)
3183
3390
  throw new Error(`Invalid UUID version: "${def.version}"`);
3184
- def.pattern ?? (def.pattern = uuid(v2));
3391
+ def.pattern ?? (def.pattern = uuid(v4));
3185
3392
  } else
3186
3393
  def.pattern ?? (def.pattern = uuid());
3187
3394
  $ZodStringFormat.init(inst, def);
@@ -3786,8 +3993,8 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3786
3993
  const field2 = shape[key]._zod;
3787
3994
  if (field2.values) {
3788
3995
  propValues[key] ?? (propValues[key] = /* @__PURE__ */ new Set());
3789
- for (const v2 of field2.values)
3790
- propValues[key].add(v2);
3996
+ for (const v4 of field2.values)
3997
+ propValues[key].add(v4);
3791
3998
  }
3792
3999
  }
3793
4000
  return propValues;
@@ -4082,10 +4289,10 @@ var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnio
4082
4289
  const pv = option._zod.propValues;
4083
4290
  if (!pv || Object.keys(pv).length === 0)
4084
4291
  throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
4085
- for (const [k, v2] of Object.entries(pv)) {
4292
+ for (const [k, v4] of Object.entries(pv)) {
4086
4293
  if (!propValues[k])
4087
4294
  propValues[k] = /* @__PURE__ */ new Set();
4088
- for (const val of v2) {
4295
+ for (const val of v4) {
4089
4296
  propValues[k].add(val);
4090
4297
  }
4091
4298
  }
@@ -4099,11 +4306,11 @@ var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnio
4099
4306
  const values = o._zod.propValues?.[def.discriminator];
4100
4307
  if (!values || values.size === 0)
4101
4308
  throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o)}"`);
4102
- for (const v2 of values) {
4103
- if (map2.has(v2)) {
4104
- throw new Error(`Duplicate discriminator value "${String(v2)}"`);
4309
+ for (const v4 of values) {
4310
+ if (map2.has(v4)) {
4311
+ throw new Error(`Duplicate discriminator value "${String(v4)}"`);
4105
4312
  }
4106
- map2.set(v2, o);
4313
+ map2.set(v4, o);
4107
4314
  }
4108
4315
  }
4109
4316
  return map2;
@@ -4742,8 +4949,8 @@ var $ZodPrefault = /* @__PURE__ */ $constructor("$ZodPrefault", (inst, def) => {
4742
4949
  var $ZodNonOptional = /* @__PURE__ */ $constructor("$ZodNonOptional", (inst, def) => {
4743
4950
  $ZodType.init(inst, def);
4744
4951
  defineLazy(inst._zod, "values", () => {
4745
- const v2 = def.innerType._zod.values;
4746
- return v2 ? new Set([...v2].filter((x) => x !== void 0)) : void 0;
4952
+ const v4 = def.innerType._zod.values;
4953
+ return v4 ? new Set([...v4].filter((x) => x !== void 0)) : void 0;
4747
4954
  });
4748
4955
  inst._zod.parse = (payload, ctx) => {
4749
4956
  const result = def.innerType._zod.run(payload, ctx);
@@ -7140,7 +7347,7 @@ var error17 = () => {
7140
7347
  if (issue2.values.length === 1) {
7141
7348
  return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF: \u05D4\u05E2\u05E8\u05DA \u05D7\u05D9\u05D9\u05D1 \u05DC\u05D4\u05D9\u05D5\u05EA ${stringifyPrimitive(issue2.values[0])}`;
7142
7349
  }
7143
- const stringified = issue2.values.map((v2) => stringifyPrimitive(v2));
7350
+ const stringified = issue2.values.map((v4) => stringifyPrimitive(v4));
7144
7351
  if (issue2.values.length === 2) {
7145
7352
  return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF: \u05D4\u05D0\u05E4\u05E9\u05E8\u05D5\u05D9\u05D5\u05EA \u05D4\u05DE\u05EA\u05D0\u05D9\u05DE\u05D5\u05EA \u05D4\u05DF ${stringified[0]} \u05D0\u05D5 ${stringified[1]}`;
7146
7353
  }
@@ -11895,7 +12102,7 @@ function _set(Class2, valueType, params) {
11895
12102
  }
11896
12103
  // @__NO_SIDE_EFFECTS__
11897
12104
  function _enum(Class2, values, params) {
11898
- const entries = Array.isArray(values) ? Object.fromEntries(values.map((v2) => [v2, v2])) : values;
12105
+ const entries = Array.isArray(values) ? Object.fromEntries(values.map((v4) => [v4, v4])) : values;
11899
12106
  return new Class2({
11900
12107
  type: "enum",
11901
12108
  entries,
@@ -12100,8 +12307,8 @@ function _stringbool(Classes, _params) {
12100
12307
  let truthyArray = params.truthy ?? ["true", "1", "yes", "on", "y", "enabled"];
12101
12308
  let falsyArray = params.falsy ?? ["false", "0", "no", "off", "n", "disabled"];
12102
12309
  if (params.case !== "sensitive") {
12103
- truthyArray = truthyArray.map((v2) => typeof v2 === "string" ? v2.toLowerCase() : v2);
12104
- falsyArray = falsyArray.map((v2) => typeof v2 === "string" ? v2.toLowerCase() : v2);
12310
+ truthyArray = truthyArray.map((v4) => typeof v4 === "string" ? v4.toLowerCase() : v4);
12311
+ falsyArray = falsyArray.map((v4) => typeof v4 === "string" ? v4.toLowerCase() : v4);
12105
12312
  }
12106
12313
  const truthySet = new Set(truthyArray);
12107
12314
  const falsySet = new Set(falsyArray);
@@ -12643,9 +12850,9 @@ var dateProcessor = (_schema, ctx, _json, _params) => {
12643
12850
  var enumProcessor = (schema, _ctx, json2, _params) => {
12644
12851
  const def = schema._zod.def;
12645
12852
  const values = getEnumValues(def.entries);
12646
- if (values.every((v2) => typeof v2 === "number"))
12853
+ if (values.every((v4) => typeof v4 === "number"))
12647
12854
  json2.type = "number";
12648
- if (values.every((v2) => typeof v2 === "string"))
12855
+ if (values.every((v4) => typeof v4 === "string"))
12649
12856
  json2.type = "string";
12650
12857
  json2.enum = values;
12651
12858
  };
@@ -12678,13 +12885,13 @@ var literalProcessor = (schema, ctx, json2, _params) => {
12678
12885
  json2.const = val;
12679
12886
  }
12680
12887
  } else {
12681
- if (vals.every((v2) => typeof v2 === "number"))
12888
+ if (vals.every((v4) => typeof v4 === "number"))
12682
12889
  json2.type = "number";
12683
- if (vals.every((v2) => typeof v2 === "string"))
12890
+ if (vals.every((v4) => typeof v4 === "string"))
12684
12891
  json2.type = "string";
12685
- if (vals.every((v2) => typeof v2 === "boolean"))
12892
+ if (vals.every((v4) => typeof v4 === "boolean"))
12686
12893
  json2.type = "boolean";
12687
- if (vals.every((v2) => v2 === null))
12894
+ if (vals.every((v4) => v4 === null))
12688
12895
  json2.type = "null";
12689
12896
  json2.enum = vals;
12690
12897
  }
@@ -12782,11 +12989,11 @@ var objectProcessor = (schema, ctx, _json, params) => {
12782
12989
  }
12783
12990
  const allKeys = new Set(Object.keys(shape));
12784
12991
  const requiredKeys = new Set([...allKeys].filter((key) => {
12785
- const v2 = def.shape[key]._zod;
12992
+ const v4 = def.shape[key]._zod;
12786
12993
  if (ctx.io === "input") {
12787
- return v2.optin === void 0;
12994
+ return v4.optin === void 0;
12788
12995
  } else {
12789
- return v2.optout === void 0;
12996
+ return v4.optout === void 0;
12790
12997
  }
12791
12998
  }));
12792
12999
  if (requiredKeys.size > 0) {
@@ -12906,7 +13113,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
12906
13113
  }
12907
13114
  const keyValues = keyType._zod.values;
12908
13115
  if (keyValues) {
12909
- const validKeyValues = [...keyValues].filter((v2) => typeof v2 === "string" || typeof v2 === "number");
13116
+ const validKeyValues = [...keyValues].filter((v4) => typeof v4 === "string" || typeof v4 === "number");
12910
13117
  if (validKeyValues.length > 0) {
12911
13118
  json2.required = validKeyValues;
12912
13119
  }
@@ -13471,12 +13678,12 @@ function _installLazyMethods(inst, group, methods) {
13471
13678
  });
13472
13679
  return bound;
13473
13680
  },
13474
- set(v2) {
13681
+ set(v4) {
13475
13682
  Object.defineProperty(this, key, {
13476
13683
  configurable: true,
13477
13684
  writable: true,
13478
13685
  enumerable: true,
13479
- value: v2
13686
+ value: v4
13480
13687
  });
13481
13688
  }
13482
13689
  });
@@ -14364,7 +14571,7 @@ var ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => {
14364
14571
  };
14365
14572
  });
14366
14573
  function _enum2(values, params) {
14367
- const entries = Array.isArray(values) ? Object.fromEntries(values.map((v2) => [v2, v2])) : values;
14574
+ const entries = Array.isArray(values) ? Object.fromEntries(values.map((v4) => [v4, v4])) : values;
14368
14575
  return new ZodEnum({
14369
14576
  type: "enum",
14370
14577
  entries,
@@ -14854,13 +15061,13 @@ function resolveRef(ref, ctx) {
14854
15061
  if (!ref.startsWith("#")) {
14855
15062
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14856
15063
  }
14857
- const path4 = ref.slice(1).split("/").filter(Boolean);
14858
- if (path4.length === 0) {
15064
+ const path7 = ref.slice(1).split("/").filter(Boolean);
15065
+ if (path7.length === 0) {
14859
15066
  return ctx.rootSchema;
14860
15067
  }
14861
15068
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14862
- if (path4[0] === defsKey) {
14863
- const key = path4[1];
15069
+ if (path7[0] === defsKey) {
15070
+ const key = path7[1];
14864
15071
  if (!key || !ctx.defs[key]) {
14865
15072
  throw new Error(`Reference not found: ${ref}`);
14866
15073
  }
@@ -14918,10 +15125,10 @@ function convertBaseSchema(schema, ctx) {
14918
15125
  if (enumValues.length === 1) {
14919
15126
  return z.literal(enumValues[0]);
14920
15127
  }
14921
- if (enumValues.every((v2) => typeof v2 === "string")) {
15128
+ if (enumValues.every((v4) => typeof v4 === "string")) {
14922
15129
  return z.enum(enumValues);
14923
15130
  }
14924
- const literalSchemas = enumValues.map((v2) => z.literal(v2));
15131
+ const literalSchemas = enumValues.map((v4) => z.literal(v4));
14925
15132
  if (literalSchemas.length < 2) {
14926
15133
  return literalSchemas[0];
14927
15134
  }
@@ -15268,7 +15475,7 @@ function date4(params) {
15268
15475
  // ../../../node_modules/zod/v4/classic/external.js
15269
15476
  config(en_default());
15270
15477
 
15271
- // ../../ir-core/dist/chunk-QHFMZPEG.js
15478
+ // ../../ir-core/dist/chunk-PPQWK62O.js
15272
15479
  function gqlIdentityKey(typeName, fieldName) {
15273
15480
  return `gql:${typeName}.${fieldName}`;
15274
15481
  }
@@ -15554,6 +15761,42 @@ var metricSemantics = defineBoundarySemantics({
15554
15761
  }
15555
15762
  }
15556
15763
  });
15764
+ var HOLE_SPLIT = /(\{[^}]*\})/;
15765
+ function isHole(piece) {
15766
+ return piece.startsWith("{") && piece.endsWith("}");
15767
+ }
15768
+ function parseBoundaryName(name) {
15769
+ const parts = name.split(HOLE_SPLIT).filter((piece) => piece !== "").map(
15770
+ (piece) => isHole(piece) ? { type: "hole", label: piece.slice(1, -1) } : { type: "text", text: piece }
15771
+ );
15772
+ const only = parts.length === 1 ? parts[0] : void 0;
15773
+ if (only !== void 0 && only.type === "hole") {
15774
+ return { type: "reference", path: only.label.split(".") };
15775
+ }
15776
+ if (parts.some((part) => part.type === "hole")) {
15777
+ return { type: "pattern", parts };
15778
+ }
15779
+ return { type: "literal", value: name };
15780
+ }
15781
+ function patternHole(label) {
15782
+ return `{${label}}`;
15783
+ }
15784
+ function namesNothing(name) {
15785
+ return parseBoundaryName(name).type === "reference";
15786
+ }
15787
+ function referenceOf(name) {
15788
+ if (name.type !== "reference") {
15789
+ return null;
15790
+ }
15791
+ const root = name.path[0];
15792
+ if (root === void 0 || name.path.some((part) => part === "")) {
15793
+ return null;
15794
+ }
15795
+ return { root, fields: name.path.slice(1) };
15796
+ }
15797
+ function referenceFromName(name) {
15798
+ return referenceOf(parseBoundaryName(name));
15799
+ }
15557
15800
  var SCHEME_ORIGIN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]*/;
15558
15801
  var PROTOCOL_RELATIVE_ORIGIN = /^\/\/[^/]*/;
15559
15802
  function pathAfterOrigin(text) {
@@ -15580,12 +15823,12 @@ var RestSemanticsSchema = external_exports.object({
15580
15823
  declaredResponses: external_exports.array(external_exports.number()).optional()
15581
15824
  });
15582
15825
  var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
15583
- function variableOf(label) {
15584
- const parts = label.split(".");
15585
- return parts[parts.length - 1] ?? label;
15826
+ function baseUrlReference(semantics) {
15827
+ const label = OPENING_HOLE.exec(semantics.path ?? "")?.[1];
15828
+ return label === void 0 ? null : referenceFromName(patternHole(label));
15586
15829
  }
15587
- function normalizePath(path4) {
15588
- let normalized = path4.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
15830
+ function normalizePath(path7) {
15831
+ let normalized = path7.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
15589
15832
  if (normalized.length > 1 && normalized.endsWith("/")) {
15590
15833
  normalized = normalized.slice(0, -1);
15591
15834
  }
@@ -15595,8 +15838,8 @@ function normalizePath(path4) {
15595
15838
  );
15596
15839
  return normalized;
15597
15840
  }
15598
- function pathShape(path4) {
15599
- return normalizePath(path4).replace(/\{[^}]*\}/g, "{}");
15841
+ function pathShape(path7) {
15842
+ return normalizePath(path7).replace(/\{[^}]*\}/g, "{}");
15600
15843
  }
15601
15844
  function methodsAgree(a, b) {
15602
15845
  if (a === null || b === null) {
@@ -15674,8 +15917,8 @@ var restSemantics = defineBoundarySemantics({
15674
15917
  return null;
15675
15918
  }
15676
15919
  const method = semantics.method === null ? "ANY" : semantics.method.toUpperCase();
15677
- const path4 = semantics.path === null ? "?" : normalizePath(semantics.path);
15678
- return `${method} ${path4}`;
15920
+ const path7 = semantics.path === null ? "?" : normalizePath(semantics.path);
15921
+ return `${method} ${path7}`;
15679
15922
  },
15680
15923
  /**
15681
15924
  * A route with both halves stated decides for itself: the method
@@ -15684,14 +15927,14 @@ var restSemantics = defineBoundarySemantics({
15684
15927
  * half might still be the one that handles the request, and nothing
15685
15928
  * here can settle that, so it abstains instead of refusing.
15686
15929
  */
15687
- servesRequest(semantics, method, path4) {
15930
+ servesRequest(semantics, method, path7) {
15688
15931
  if (semantics.method === null || semantics.path === null) {
15689
15932
  return "unknown";
15690
15933
  }
15691
15934
  if (!methodsAgree(semantics.method, method.toUpperCase())) {
15692
15935
  return "nomatch";
15693
15936
  }
15694
- return routePathAdmits(semantics.path, path4) ? "match" : "nomatch";
15937
+ return routePathAdmits(semantics.path, path7) ? "match" : "nomatch";
15695
15938
  },
15696
15939
  /**
15697
15940
  * A route the registration pattern admits is inside it. A route
@@ -15716,22 +15959,20 @@ var restSemantics = defineBoundarySemantics({
15716
15959
  * route parameter, and `/orders/{id}` means every id rather than a
15717
15960
  * variable somebody set.
15718
15961
  */
15719
- groundName(semantics, deployedAs) {
15720
- if (semantics.path === null) {
15962
+ groundName(semantics, deployment) {
15963
+ const reference = baseUrlReference(semantics);
15964
+ if (reference === null) {
15721
15965
  return null;
15722
15966
  }
15723
- const opening = OPENING_HOLE.exec(semantics.path);
15724
- if (opening === null) {
15725
- return null;
15726
- }
15727
- const [, label, rest] = opening;
15728
- const base = deployedAs(variableOf(label));
15967
+ const base = deployment.setTo(reference);
15729
15968
  if (base === null) {
15730
15969
  return null;
15731
15970
  }
15971
+ const rest = OPENING_HOLE.exec(semantics.path ?? "")?.[2] ?? "";
15732
15972
  const grounded = pathAfterOrigin(`${base}${rest}`);
15733
15973
  return { ...semantics, path: grounded === "" ? "/" : grounded };
15734
15974
  },
15975
+ nameReference: baseUrlReference,
15735
15976
  ruleBoundary: {
15736
15977
  // "METHOD /path": one leading token, then an absolute path.
15737
15978
  claims(raw) {
@@ -15807,6 +16048,9 @@ var StorageSemanticsSchema = external_exports.object({
15807
16048
  */
15808
16049
  accessPath: external_exports.string().nullable()
15809
16050
  });
16051
+ function containerReference(semantics) {
16052
+ return semantics.container === null ? null : referenceFromName(semantics.container);
16053
+ }
15810
16054
  var storageSemantics = defineBoundarySemantics({
15811
16055
  name: "storage",
15812
16056
  schema: StorageSemanticsSchema,
@@ -15823,7 +16067,21 @@ var storageSemantics = defineBoundarySemantics({
15823
16067
  exchangesHttpResponses: false,
15824
16068
  reportsUnpairedItself: false,
15825
16069
  identityKey: () => null,
15826
- displayLabel: storageLabel
16070
+ displayLabel: storageLabel,
16071
+ /**
16072
+ * A container the source states as a variable is whatever the
16073
+ * deployment sets that variable to. `{SUBSCRIBERS_TABLE}` in the
16074
+ * code and `prod-subscribers-v1` in the manifest are one table.
16075
+ */
16076
+ groundName(semantics, deployment) {
16077
+ const reference = containerReference(semantics);
16078
+ if (reference === null) {
16079
+ return null;
16080
+ }
16081
+ const name = deployment.setTo(reference);
16082
+ return name === null ? null : { ...semantics, container: name };
16083
+ },
16084
+ nameReference: containerReference
15827
16085
  }
15828
16086
  });
15829
16087
  function storageLabel(semantics) {
@@ -15838,6 +16096,9 @@ var UnitInvocationSemanticsSchema = DeployableUnitSchema.extend({
15838
16096
  name: external_exports.literal("unit-invocation"),
15839
16097
  instanceName: DeployableUnitSchema.shape.instanceName.nullable()
15840
16098
  });
16099
+ function calleeReference(semantics) {
16100
+ return semantics.instanceName === null ? null : referenceFromName(semantics.instanceName);
16101
+ }
15841
16102
  var unitInvocationSemantics = defineBoundarySemantics({
15842
16103
  name: "unit-invocation",
15843
16104
  schema: UnitInvocationSemanticsSchema,
@@ -15860,8 +16121,13 @@ var unitInvocationSemantics = defineBoundarySemantics({
15860
16121
  * being reported as a problem here.
15861
16122
  */
15862
16123
  reportsUnpairedItself: false,
16124
+ /**
16125
+ * A callee the source states only as a variable is not a name for
16126
+ * anything until the deployment fills it in, so it keys as nothing
16127
+ * rather than as a bucket that would agree with itself alone.
16128
+ */
15863
16129
  identityKey(semantics) {
15864
- if (semantics.instanceName === null) {
16130
+ if (semantics.instanceName === null || namesNothing(semantics.instanceName)) {
15865
16131
  return null;
15866
16132
  }
15867
16133
  return unitIdentityKey(
@@ -15877,7 +16143,23 @@ var unitInvocationSemantics = defineBoundarySemantics({
15877
16143
  semantics.deploymentTarget,
15878
16144
  semantics.instanceName
15879
16145
  );
15880
- }
16146
+ },
16147
+ /**
16148
+ * A call that reads its callee's name out of the environment
16149
+ * reaches whichever resource the template wires that variable to.
16150
+ * The logical id is the answer rather than the deployed name,
16151
+ * because the invoked unit's own summary is keyed by the logical
16152
+ * id and neither side ever writes the other's string.
16153
+ */
16154
+ groundName(semantics, deployment) {
16155
+ const reference = calleeReference(semantics);
16156
+ if (reference === null) {
16157
+ return null;
16158
+ }
16159
+ const logicalId = deployment.pointsAt(reference);
16160
+ return logicalId === null ? null : { ...semantics, instanceName: logicalId };
16161
+ },
16162
+ nameReference: calleeReference
15881
16163
  }
15882
16164
  });
15883
16165
  var SemanticsSchema = external_exports.discriminatedUnion("name", [
@@ -16071,7 +16353,291 @@ function storageBinding(opts) {
16071
16353
 
16072
16354
  // ../../resolution/dist/index.js
16073
16355
  import { ruleLabel } from "@suss/datalog";
16356
+ import { Database, evaluate, proofOf, witnesses } from "@suss/datalog";
16074
16357
  import { constant, lit, rule, variable as v } from "@suss/datalog";
16358
+ function exportTrail(proof, describe3) {
16359
+ if (proof.kind !== "derived") {
16360
+ return [];
16361
+ }
16362
+ const name = ruleLabel(proof.rule);
16363
+ if (name === "re-export") {
16364
+ const [m, n, m2, n2] = proof.premises[0].tuple;
16365
+ return [
16366
+ `${describe3(m)} takes ${String(n)} from ${describe3(m2)}, where it is called ${String(n2)}`,
16367
+ ...exportTrail(proof.premises[1], describe3)
16368
+ ];
16369
+ }
16370
+ if (name === "re-export all") {
16371
+ const [m, m2] = proof.premises[0].tuple;
16372
+ return [
16373
+ `${describe3(m)} forwards everything ${describe3(m2)} exports`,
16374
+ ...exportTrail(proof.premises[1], describe3)
16375
+ ];
16376
+ }
16377
+ return [];
16378
+ }
16379
+ function argumentReason(proof, describe3) {
16380
+ const [, p, a] = proof.tuple;
16381
+ if (proof.kind !== "derived") {
16382
+ return `${describe3(p)} is a parameter, and a call passes it ${describe3(a)}`;
16383
+ }
16384
+ const f = proof.premises[0].tuple[0];
16385
+ return `${describe3(p)} is a parameter of ${describe3(f)}, and a call passes it ${describe3(a)}`;
16386
+ }
16387
+ var DEFAULT_PHRASES = {
16388
+ alias: ({ tuple: tuple2, describe: describe3 }) => ({
16389
+ reason: `${describe3(tuple2[0])} is declared as ${describe3(tuple2[1])}`
16390
+ }),
16391
+ "last write": ({ tuple: tuple2, describe: describe3 }) => ({
16392
+ reason: `${describe3(tuple2[0])} is written more than once, and the last write leaves it as ${describe3(tuple2[1])}`
16393
+ }),
16394
+ fallback: ({ tuple: tuple2, describe: describe3 }) => ({
16395
+ reason: `${describe3(tuple2[0])} is a fallback expression, and ${describe3(tuple2[1])} is the branch that resolves`
16396
+ }),
16397
+ import: ({ tuple: tuple2, premises, describe: describe3 }) => {
16398
+ const [, module, name] = premises[0].tuple;
16399
+ return {
16400
+ reason: `${describe3(tuple2[0])} is imported from ${describe3(module)} under the name ${String(name)}`,
16401
+ notes: exportTrail(premises[1], describe3)
16402
+ };
16403
+ },
16404
+ argument: ({ premises, describe: describe3 }) => ({
16405
+ reason: argumentReason(premises[0], describe3)
16406
+ }),
16407
+ "property read": ({ tuple: tuple2, premises, describe: describe3 }) => {
16408
+ const [, object2, name] = premises[0].tuple;
16409
+ const contains = premises[2];
16410
+ const inherited = contains.kind === "derived" && contains.premises.length > 1;
16411
+ return {
16412
+ reason: `${describe3(tuple2[0])} reads ${String(name)} off ${describe3(object2)}, which contains ${describe3(tuple2[1])}`,
16413
+ notes: inherited ? [`${String(name)} comes from a base class`] : []
16414
+ };
16415
+ },
16416
+ "class instance": ({ tuple: tuple2, premises, describe: describe3, inline }) => ({
16417
+ reason: `${describe3(tuple2[0])} makes an instance of ${describe3(tuple2[1])}`,
16418
+ notes: inline(premises[1])
16419
+ }),
16420
+ "factory unwrap": ({ tuple: tuple2, premises, describe: describe3, inline }) => {
16421
+ const factory = premises[1].tuple[1];
16422
+ return {
16423
+ reason: `${describe3(tuple2[0])} calls ${describe3(factory)}, a factory that passes its argument through, so it comes down to ${describe3(tuple2[1])}`,
16424
+ notes: inline(premises[1])
16425
+ };
16426
+ },
16427
+ "declared wrapper": ({ tuple: tuple2, premises, describe: describe3 }) => {
16428
+ const callee = String(premises[0].tuple[1]);
16429
+ const module = String(premises[2].tuple[1]);
16430
+ const argument = String(premises[1].tuple[1]);
16431
+ return {
16432
+ reason: `${describe3(tuple2[0])} calls ${callee} from ${module}, which passes argument ${argument} through, so it comes down to ${describe3(tuple2[1])}`,
16433
+ assumptions: [
16434
+ `a pack declares that ${callee} from ${module} passes argument ${argument} through to its result`
16435
+ ]
16436
+ };
16437
+ },
16438
+ "call result": ({ tuple: tuple2, premises, describe: describe3, inline }) => {
16439
+ const invokes = premises[0];
16440
+ const invoked = invokes.tuple[1];
16441
+ const calleeWalks = invokes.kind === "derived" ? invokes.premises.filter(
16442
+ (premise) => premise.kind === "derived" && (premise.relation === "comesTo" || premise.relation === "givesBack")
16443
+ ) : [];
16444
+ return {
16445
+ reason: `${describe3(tuple2[0])} runs ${describe3(invoked)}, which returns ${describe3(tuple2[1])}`,
16446
+ notes: calleeWalks.flatMap((premise) => inline(premise))
16447
+ };
16448
+ }
16449
+ };
16450
+ var CHAIN_RELATIONS = /* @__PURE__ */ new Set([
16451
+ "resolves",
16452
+ "comesTo",
16453
+ "givesBack",
16454
+ "isWrittenAs",
16455
+ "comesFrom",
16456
+ "reaches",
16457
+ "stepsTo"
16458
+ ]);
16459
+ function inlineChain(proof, state) {
16460
+ const explained = explainResolutionProof(proof, {
16461
+ describe: state.describe,
16462
+ phrases: state.phrases
16463
+ });
16464
+ if (explained === null || explained.steps.length === 0) {
16465
+ return [];
16466
+ }
16467
+ if (explained.truncated) {
16468
+ state.truncated = true;
16469
+ }
16470
+ return explained.steps.map((step) => step.reason);
16471
+ }
16472
+ function stepFrom(proof, state) {
16473
+ const name = ruleLabel(proof.rule);
16474
+ const phrase = state.phrases[name];
16475
+ const from = proof.tuple[0];
16476
+ const to = proof.tuple[1];
16477
+ if (phrase === void 0) {
16478
+ return {
16479
+ from,
16480
+ to,
16481
+ rule: name,
16482
+ reason: `${state.describe(from)} steps to ${state.describe(to)} (${name})`,
16483
+ notes: [],
16484
+ assumptions: []
16485
+ };
16486
+ }
16487
+ const said = phrase({
16488
+ tuple: proof.tuple,
16489
+ premises: proof.premises,
16490
+ describe: state.describe,
16491
+ inline: (nested) => inlineChain(nested, state)
16492
+ });
16493
+ return {
16494
+ from,
16495
+ to,
16496
+ rule: name,
16497
+ reason: said.reason,
16498
+ notes: said.notes ?? [],
16499
+ assumptions: said.assumptions ?? []
16500
+ };
16501
+ }
16502
+ function importStep(proof, state) {
16503
+ const [x, module, name] = proof.tuple;
16504
+ return {
16505
+ from: x,
16506
+ to: module,
16507
+ rule: "import",
16508
+ reason: `${state.describe(x)} is imported from ${state.describe(module)} under the name ${String(name)}`,
16509
+ notes: [],
16510
+ assumptions: []
16511
+ };
16512
+ }
16513
+ var flattenInto = (proof, state) => {
16514
+ if (proof.kind === "truncated") {
16515
+ state.truncated = true;
16516
+ return;
16517
+ }
16518
+ if (proof.kind !== "derived") {
16519
+ return;
16520
+ }
16521
+ const relation = proof.relation;
16522
+ if (relation === "stepsTo") {
16523
+ state.steps.push(stepFrom(proof, state));
16524
+ return;
16525
+ }
16526
+ if (relation === "reaches") {
16527
+ for (const p of proof.premises) {
16528
+ flattenInto(p, state);
16529
+ }
16530
+ return;
16531
+ }
16532
+ if (relation === "comesFrom") {
16533
+ if (proof.premises.length === 2) {
16534
+ flattenInto(proof.premises[0], state);
16535
+ state.steps.push(importStep(proof.premises[1], state));
16536
+ } else {
16537
+ state.steps.push(importStep(proof.premises[0], state));
16538
+ }
16539
+ return;
16540
+ }
16541
+ for (const p of proof.premises) {
16542
+ if (p.kind === "derived" && (p.relation === "reaches" || p.relation === "comesTo")) {
16543
+ flattenInto(p, state);
16544
+ }
16545
+ if (p.kind === "truncated") {
16546
+ state.truncated = true;
16547
+ }
16548
+ }
16549
+ };
16550
+ function explainResolutionProof(proof, options) {
16551
+ if (proof.kind === "absence") {
16552
+ return null;
16553
+ }
16554
+ if (proof.kind === "derived" && !CHAIN_RELATIONS.has(proof.relation)) {
16555
+ return null;
16556
+ }
16557
+ const state = {
16558
+ steps: [],
16559
+ truncated: proof.kind === "truncated",
16560
+ describe: options.describe,
16561
+ phrases: { ...DEFAULT_PHRASES, ...options.phrases }
16562
+ };
16563
+ flattenInto(proof, state);
16564
+ const atoms = proof.kind === "fact" || proof.kind === "truncated" ? [proof.tuple[0]] : [proof.tuple[0], ...state.steps.map((step) => step.to)];
16565
+ return {
16566
+ atoms,
16567
+ steps: state.steps,
16568
+ assumptions: state.steps.flatMap((step) => step.assumptions),
16569
+ truncated: state.truncated
16570
+ };
16571
+ }
16572
+ function renderExplanation(explanation, describe3) {
16573
+ const lines = [];
16574
+ if (explanation.steps.length > 0) {
16575
+ lines.push(explanation.atoms.map((atom) => describe3(atom)).join(" -> "));
16576
+ }
16577
+ for (const step of explanation.steps) {
16578
+ lines.push(` ${step.reason}`);
16579
+ for (const note of step.notes) {
16580
+ lines.push(` ${note}`);
16581
+ }
16582
+ }
16583
+ for (const assumption of explanation.assumptions) {
16584
+ lines.push(` assuming ${assumption}`);
16585
+ }
16586
+ if (explanation.truncated) {
16587
+ lines.push(" the chain goes on past the proof depth cap");
16588
+ }
16589
+ return lines;
16590
+ }
16591
+ function explainResolvedKey(options) {
16592
+ const { db, rules, key, locate, displayPath } = options;
16593
+ const proofDb = new Database();
16594
+ let baseFacts = 0;
16595
+ for (const relation of db.relationNames()) {
16596
+ for (const tuple2 of db.facts(relation)) {
16597
+ proofDb.add(relation, tuple2);
16598
+ baseFacts++;
16599
+ }
16600
+ }
16601
+ const started = performance.now();
16602
+ evaluate(proofDb, rules, witnesses);
16603
+ const evaluateMs = performance.now() - started;
16604
+ const derivedFacts = proofDb.relationNames().reduce((count, relation) => count + proofDb.size(relation), 0) - baseFacts;
16605
+ const targets = new Set(
16606
+ proofDb.lookup("resolves", 0, key).map((tuple2) => String(tuple2[1]))
16607
+ );
16608
+ if (targets.size !== 1) {
16609
+ return null;
16610
+ }
16611
+ const target = [...targets][0];
16612
+ const proof = proofOf(
16613
+ proofDb,
16614
+ "resolves",
16615
+ [key, target],
16616
+ options.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth }
16617
+ );
16618
+ const describe3 = (atom) => {
16619
+ const location = locate(String(atom));
16620
+ return location === null ? displayPath(String(atom)) : `${location.name} (${location.file}:${location.line})`;
16621
+ };
16622
+ const explanation = explainResolutionProof(proof, {
16623
+ describe: describe3,
16624
+ ...options.phrases === void 0 ? {} : { phrases: options.phrases }
16625
+ });
16626
+ if (explanation === null) {
16627
+ return null;
16628
+ }
16629
+ const targetLocation = locate(target);
16630
+ if (targetLocation === null) {
16631
+ return null;
16632
+ }
16633
+ return {
16634
+ explanation,
16635
+ chain: explanation.atoms.map(describe3),
16636
+ lines: renderExplanation(explanation, describe3),
16637
+ target: targetLocation,
16638
+ stats: { baseFacts, derivedFacts, evaluateMs }
16639
+ };
16640
+ }
16075
16641
  var VALUE_STEP = constant("value");
16076
16642
  var NAMESPACE_IMPORT_NAME = "*";
16077
16643
  var NAMESPACE_IMPORT = constant(NAMESPACE_IMPORT_NAME);
@@ -16185,9 +16751,9 @@ var RESOLUTION_RULES = [
16185
16751
  [lit("invokes", v("r"), v("f")), lit("returnsValue", v("f"), v("ret"))],
16186
16752
  "call result"
16187
16753
  ),
16188
- // Where a walk gets to, and whether it ran a call on the way. A value
16189
- // step keeps whatever the rest of the walk was, and a result step
16190
- // makes the whole walk one that ran a call.
16754
+ // Where a walk gets to, and whether it ran a call on the way. The walk
16755
+ // so far comes first, so demand stays on the value asked about; the
16756
+ // README says what the other order cost.
16191
16757
  rule(
16192
16758
  "reaches",
16193
16759
  [v("x"), v("z"), v("kind")],
@@ -16197,27 +16763,27 @@ var RESOLUTION_RULES = [
16197
16763
  "reaches",
16198
16764
  [v("x"), v("z"), v("kind")],
16199
16765
  [
16200
- lit("stepsTo", v("x"), v("y"), VALUE_STEP),
16201
- lit("reaches", v("y"), v("z"), v("kind"))
16766
+ lit("reaches", v("x"), v("y"), VALUE_STEP),
16767
+ lit("stepsTo", v("y"), v("z"), v("kind"))
16202
16768
  ]
16203
16769
  ),
16204
- // Two rules rather than one leaving the rest of the walk's kind free.
16205
- // A free kind is a second question about the same relation, and a
16770
+ // Two rules rather than one leaving the next step's kind free. A free
16771
+ // kind is a second question about the same relation, and a
16206
16772
  // demand-driven run then derives both to answer either.
16207
16773
  rule(
16208
16774
  "reaches",
16209
16775
  [v("x"), v("z"), RESULT_STEP],
16210
16776
  [
16211
- lit("stepsTo", v("x"), v("y"), RESULT_STEP),
16212
- lit("reaches", v("y"), v("z"), VALUE_STEP)
16777
+ lit("reaches", v("x"), v("y"), RESULT_STEP),
16778
+ lit("stepsTo", v("y"), v("z"), VALUE_STEP)
16213
16779
  ]
16214
16780
  ),
16215
16781
  rule(
16216
16782
  "reaches",
16217
16783
  [v("x"), v("z"), RESULT_STEP],
16218
16784
  [
16219
- lit("stepsTo", v("x"), v("y"), RESULT_STEP),
16220
- lit("reaches", v("y"), v("z"), RESULT_STEP)
16785
+ lit("reaches", v("x"), v("y"), RESULT_STEP),
16786
+ lit("stepsTo", v("y"), v("z"), RESULT_STEP)
16221
16787
  ]
16222
16788
  ),
16223
16789
  // What a value comes down to: the walk that never ran a call, stopped
@@ -16263,6 +16829,18 @@ var RESOLUTION_RULES = [
16263
16829
  [v("x"), v("z")],
16264
16830
  [lit("reaches", v("x"), v("z"), VALUE_STEP), lit("objectValue", v("z"))]
16265
16831
  ),
16832
+ // A call is written as whatever its callee's return value is written
16833
+ // as. Unlike `comesTo`, this has nothing to protect: which expression
16834
+ // a value was written as does not compete with unwrapping it.
16835
+ rule(
16836
+ "isWrittenAs",
16837
+ [v("x"), v("z")],
16838
+ [
16839
+ lit("invokes", v("x"), v("f")),
16840
+ lit("returnsValue", v("f"), v("ret")),
16841
+ lit("isWrittenAs", v("ret"), v("z"))
16842
+ ]
16843
+ ),
16266
16844
  // What one call site put in a parameter, told apart from what the
16267
16845
  // other callers passed.
16268
16846
  rule(
@@ -16563,29 +17141,14 @@ var RESOLUTION_QUESTIONS = [
16563
17141
  // here demands reaches or callsInto. The chains are closures with
16564
17142
  // no depth bound: an alias run of any length resolves.
16565
17143
  rule("callOriginChain", [v("x"), v("x")], [lit("wantedCallOrigin", v("x"))]),
17144
+ // The chain with at least one hop taken, which is what tells a
17145
+ // destructured binding apart from asking about a node directly. The
17146
+ // whole chain is the seed plus those hops.
16566
17147
  rule(
16567
17148
  "callOriginChain",
16568
17149
  [v("x"), v("z")],
16569
- [lit("callOriginChain", v("x"), v("y")), lit("binds", v("y"), v("z"))]
16570
- ),
16571
- rule(
16572
- "callOriginChain",
16573
- [v("x"), v("z")],
16574
- [
16575
- lit("callOriginChain", v("x"), v("y")),
16576
- lit("endsHolding", v("y"), v("z"))
16577
- ]
16578
- ),
16579
- rule(
16580
- "callOriginChain",
16581
- [v("x"), v("z")],
16582
- [
16583
- lit("callOriginChain", v("x"), v("y")),
16584
- lit("fallbackBranch", v("y"), v("z"))
16585
- ]
17150
+ [lit("callOriginChainStepped", v("x"), v("z"))]
16586
17151
  ),
16587
- // The chain with at least one hop taken, which is what tells a
16588
- // destructured binding apart from asking about a node directly.
16589
17152
  rule(
16590
17153
  "callOriginChainStepped",
16591
17154
  [v("x"), v("z")],
@@ -16839,11 +17402,11 @@ var ANCESTRY_PROGRAM = deriveOnDemand(
16839
17402
  );
16840
17403
  function reachesBase(facts, classKey, bases) {
16841
17404
  facts.add("wantedAncestry", [classKey]);
16842
- evaluate(facts, ANCESTRY_PROGRAM.rules);
17405
+ evaluate2(facts, ANCESTRY_PROGRAM.rules);
16843
17406
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
16844
17407
  }
16845
- function classBehind(facts, constant2) {
16846
- const wanted = `#${constant2.text}`;
17408
+ function classBehind(facts, constant3) {
17409
+ const wanted = `#${constant3.text}`;
16847
17410
  const bound = new Set(
16848
17411
  facts.facts("binds").filter((row) => String(row[0]).endsWith(wanted)).map((row) => String(row[1]))
16849
17412
  );
@@ -16886,11 +17449,11 @@ function storageEffects(calls, options) {
16886
17449
  if (partOfOne.has(call.id)) {
16887
17450
  continue;
16888
17451
  }
16889
- const constant2 = rootConstant(call);
16890
- if (constant2 === null) {
17452
+ const constant3 = rootConstant(call);
17453
+ if (constant3 === null) {
16891
17454
  continue;
16892
17455
  }
16893
- const classKey = classBehind(options.facts, constant2);
17456
+ const classKey = classBehind(options.facts, constant3);
16894
17457
  if (classKey === void 0) {
16895
17458
  continue;
16896
17459
  }
@@ -16908,7 +17471,7 @@ function storageEffects(calls, options) {
16908
17471
  recognition: "ruby-storage",
16909
17472
  storageSystem: pattern.storageSystem,
16910
17473
  scope: pattern.baseClasses[0] ?? "",
16911
- container: constant2.text
17474
+ container: constant3.text
16912
17475
  }),
16913
17476
  callee: call.text,
16914
17477
  interaction: {
@@ -17024,7 +17587,8 @@ async function discoverUnits(root, options) {
17024
17587
  const knownClasses = new Set(classes.map((info) => info.qualifiedName));
17025
17588
  const fileBlocks = classes.map((info) => ({
17026
17589
  info,
17027
- knownClasses
17590
+ knownClasses,
17591
+ file: options.absoluteFile ?? options.filePath
17028
17592
  }));
17029
17593
  const units = [];
17030
17594
  for (const info of classes) {
@@ -17048,16 +17612,17 @@ async function discoverUnits(root, options) {
17048
17612
  }
17049
17613
  return units;
17050
17614
  }
17051
- function reachesConfiguredBase(ancestry, self, pattern) {
17615
+ function reachesConfiguredBase(ancestry, self, baseClassNames) {
17052
17616
  return ancestry.some(
17053
- (entry) => entry.name !== self && pattern.baseClassNames.includes(entry.name)
17617
+ (entry) => entry.name !== self && baseClassNames.includes(entry.name)
17054
17618
  );
17055
17619
  }
17056
17620
  function unitsFor(pattern, pack, info, ownBlocks, fileBlocks, options) {
17057
17621
  const table = {
17058
- graphqlObjectFields: (p) => graphqlObjectFieldUnits(p, pack, info, ownBlocks, fileBlocks, options)
17622
+ graphqlObjectFields: (p) => graphqlObjectFieldUnits(p, pack, info, ownBlocks, fileBlocks, options),
17623
+ controllerActions: (p) => controllerActionUnits(p, pack, info, ownBlocks, fileBlocks, options)
17059
17624
  };
17060
- return dispatchByType(table, pattern);
17625
+ return dispatchByType2(table, pattern);
17061
17626
  }
17062
17627
  async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlocks, options) {
17063
17628
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
@@ -17070,7 +17635,7 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
17070
17635
  options.storage
17071
17636
  );
17072
17637
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
17073
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern)) {
17638
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
17074
17639
  return [];
17075
17640
  }
17076
17641
  const typeName = graphqlTypeNameFromQualified(
@@ -17089,19 +17654,155 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
17089
17654
  declsByName.set(decl.fieldName, decl);
17090
17655
  }
17091
17656
  }
17092
- return [...declsByName.values()].map(
17093
- (decl) => buildFieldUnit(pack, typeName, decl, options.filePath)
17094
- );
17657
+ return [...declsByName.values()].map((decl) => {
17658
+ const raw = buildFieldUnit(pack, typeName, decl, options.filePath);
17659
+ if (decl.body.reachSeed !== void 0) {
17660
+ options.onReachSeed?.(raw, decl.body.reachSeed);
17661
+ }
17662
+ return raw;
17663
+ });
17095
17664
  }
17096
- function blockConfiguresCall(pattern) {
17097
- const names = /* @__PURE__ */ new Set([
17098
- pattern.fieldCallName,
17099
- pattern.typeCallName,
17100
- pattern.argumentCallName
17101
- ]);
17102
- return (call) => {
17103
- const method = field(call, "method")?.text;
17104
- return field(call, "receiver") === null && method !== void 0 && names.has(method);
17665
+ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks, options) {
17666
+ if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
17667
+ return [];
17668
+ }
17669
+ const lookup = {
17670
+ root: pattern.root,
17671
+ pathConvention: pattern.pathConvention,
17672
+ ancestryRootClassNames: pattern.ancestryRootClassNames,
17673
+ parsedFile: (absPath) => options.cache.get(absPath),
17674
+ localDefinition: (name) => sameFileBlocks(name, fileBlocks)
17675
+ };
17676
+ const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, lookup);
17677
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
17678
+ return [];
17679
+ }
17680
+ const units = [];
17681
+ for (const block of ownBlocks) {
17682
+ if (block.info.bodyNode === null) {
17683
+ continue;
17684
+ }
17685
+ const visibility = instanceMethodVisibility(block.info.bodyNode);
17686
+ for (const [actionName, method] of instanceMethodsByName(
17687
+ block.info.bodyNode
17688
+ )) {
17689
+ if ((visibility.get(actionName) ?? "public") !== "public") {
17690
+ continue;
17691
+ }
17692
+ const raw = buildControllerActionUnit(
17693
+ pack,
17694
+ pattern,
17695
+ info.qualifiedName,
17696
+ actionName,
17697
+ method,
17698
+ options.filePath,
17699
+ options.storage
17700
+ );
17701
+ units.push(raw);
17702
+ options.onReachSeed?.(raw, {
17703
+ file: block.file,
17704
+ node: method,
17705
+ enclosingQualifiedName: info.qualifiedName
17706
+ });
17707
+ }
17708
+ }
17709
+ return units;
17710
+ }
17711
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, filePath, storage) {
17712
+ const range = rangeOf(method);
17713
+ const route = pattern.routeFor(controllerQualifiedName, actionName);
17714
+ const body = bodyOfMethod(method, storage);
17715
+ return {
17716
+ identity: {
17717
+ name: actionName,
17718
+ nameKind: "binding",
17719
+ kind: "handler",
17720
+ file: filePath,
17721
+ range,
17722
+ span: spanOf(method),
17723
+ exportName: actionName,
17724
+ exportPath: [controllerQualifiedName, actionName]
17725
+ },
17726
+ boundaryBinding: route === null ? null : restBinding({
17727
+ transport: pack.protocol,
17728
+ method: route.method,
17729
+ path: route.path,
17730
+ recognition: pack.name
17731
+ }),
17732
+ parameters: [],
17733
+ branches: [
17734
+ {
17735
+ conditions: [],
17736
+ terminal: {
17737
+ kind: "response",
17738
+ statusCode: { type: "literal", value: pattern.defaultStatusCode },
17739
+ body: null,
17740
+ exceptionType: null,
17741
+ message: null,
17742
+ component: null,
17743
+ renderTree: null,
17744
+ delegateTarget: null,
17745
+ emitEvent: null,
17746
+ location: range
17747
+ },
17748
+ effects: body.effects ?? [],
17749
+ ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
17750
+ location: range,
17751
+ isDefault: true
17752
+ }
17753
+ ],
17754
+ bodyContent: body.bodyContent ?? "absent",
17755
+ dependencyCalls: [],
17756
+ declaredContract: null
17757
+ };
17758
+ }
17759
+ function routingGapUnit(pattern, gaps) {
17760
+ const range = { start: 1, end: 1 };
17761
+ return {
17762
+ identity: {
17763
+ name: "routes",
17764
+ kind: "module-init",
17765
+ file: pattern.routesFile,
17766
+ range,
17767
+ exportName: null,
17768
+ exportPath: null
17769
+ },
17770
+ boundaryBinding: null,
17771
+ parameters: [],
17772
+ branches: [
17773
+ {
17774
+ conditions: [],
17775
+ terminal: {
17776
+ kind: "void",
17777
+ statusCode: null,
17778
+ body: null,
17779
+ exceptionType: null,
17780
+ message: null,
17781
+ component: null,
17782
+ renderTree: null,
17783
+ delegateTarget: null,
17784
+ emitEvent: null,
17785
+ location: range
17786
+ },
17787
+ effects: [],
17788
+ location: range,
17789
+ isDefault: true
17790
+ }
17791
+ ],
17792
+ readings: gaps.map((gap) => unreadableReading(gap, range)),
17793
+ dependencyCalls: [],
17794
+ declaredContract: null
17795
+ };
17796
+ }
17797
+ function blockConfiguresCall(pattern) {
17798
+ const names = /* @__PURE__ */ new Set([
17799
+ pattern.fieldCallName,
17800
+ pattern.typeCallName,
17801
+ pattern.argumentCallName
17802
+ ]);
17803
+ return (call) => {
17804
+ const method = field(call, "method")?.text;
17805
+ return field(call, "receiver") === null && method !== void 0 && names.has(method);
17105
17806
  };
17106
17807
  }
17107
17808
  function branchesFor(body, range) {
@@ -17132,12 +17833,15 @@ function branchesFor(body, range) {
17132
17833
  }
17133
17834
  function bodyOfMethod(method, storage) {
17134
17835
  const effects = invocationEffects(method);
17135
- const database = storage === void 0 ? [] : storageEffects(callsUnder(method), storage);
17836
+ const extra = [
17837
+ ...envReadEffects(method),
17838
+ ...storage === void 0 ? [] : storageEffects(callsUnder(method), storage)
17839
+ ];
17136
17840
  return {
17137
17841
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
17138
17842
  readings: [],
17139
17843
  ...effects.length > 0 ? { effects } : {},
17140
- ...database.length > 0 ? { extraEffects: database } : {}
17844
+ ...extra.length > 0 ? { extraEffects: extra } : {}
17141
17845
  };
17142
17846
  }
17143
17847
  function callsUnder(node, found = []) {
@@ -17161,14 +17865,21 @@ function methodNotSettled(reason, range) {
17161
17865
  }
17162
17866
  function bodyFromLookup(found, range, subject, nothingThere, storage) {
17163
17867
  const table = {
17164
- found: (lookup) => bodyOfMethod(lookup.method, storage),
17868
+ found: (lookup) => ({
17869
+ ...bodyOfMethod(lookup.method, storage),
17870
+ reachSeed: {
17871
+ file: lookup.block.file,
17872
+ node: lookup.method,
17873
+ enclosingQualifiedName: lookup.block.info.qualifiedName
17874
+ }
17875
+ }),
17165
17876
  unsettled: (lookup) => methodNotSettled(
17166
17877
  `${subject} could be answered by a method ${lookup.reason}, so whether one exists was not settled here`,
17167
17878
  range
17168
17879
  ),
17169
17880
  none: () => nothingThere
17170
17881
  };
17171
- return dispatchByType(table, found);
17882
+ return dispatchByType2(table, found);
17172
17883
  }
17173
17884
  async function readFieldCall(stmt, scope, ctx, ancestry) {
17174
17885
  if (stmt.type !== "call" || field(stmt, "receiver") !== null) {
@@ -17453,59 +18164,6 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
17453
18164
  };
17454
18165
  }
17455
18166
 
17456
- // src/facts.ts
17457
- function unitKey(filePath, range, name) {
17458
- return `${filePath}:${range.start}-${range.end}#${name}`;
17459
- }
17460
- function emitEntryFact(db, filePath, range, name) {
17461
- db.add("entry", [unitKey(filePath, range, name)]);
17462
- }
17463
-
17464
- // src/parser.ts
17465
- import fs2 from "fs";
17466
- import path2 from "path";
17467
- import { fileURLToPath } from "url";
17468
- import { Language, Parser } from "web-tree-sitter";
17469
- var GRAMMAR_PATH = path2.join(
17470
- path2.dirname(fileURLToPath(import.meta.url)),
17471
- "..",
17472
- "grammar",
17473
- "tree-sitter-ruby.wasm"
17474
- );
17475
- var languagePromise = null;
17476
- function loadLanguage() {
17477
- languagePromise ??= (async () => {
17478
- if (!fs2.existsSync(GRAMMAR_PATH)) {
17479
- throw new Error(
17480
- `Ruby grammar not found at ${GRAMMAR_PATH}. See grammar/README.md.`
17481
- );
17482
- }
17483
- await Parser.init();
17484
- return Language.load(GRAMMAR_PATH);
17485
- })();
17486
- return languagePromise;
17487
- }
17488
- async function parseRuby(source) {
17489
- const language = await loadLanguage();
17490
- const parser = new Parser();
17491
- parser.setLanguage(language);
17492
- try {
17493
- const tree = parser.parse(source);
17494
- if (tree === null) {
17495
- throw new Error("tree-sitter produced no tree for this source");
17496
- }
17497
- return tree;
17498
- } finally {
17499
- parser.delete();
17500
- }
17501
- }
17502
-
17503
- // src/project.ts
17504
- import fs3 from "fs";
17505
- import path3 from "path";
17506
- import { Database } from "@suss/datalog";
17507
- import { assembleSummary } from "@suss/extractor";
17508
-
17509
18167
  // src/facts/values.ts
17510
18168
  function nodeId(filePath, node) {
17511
18169
  return `${filePath}:${node.startIndex}-${node.endIndex}`;
@@ -17513,6 +18171,33 @@ function nodeId(filePath, node) {
17513
18171
  function nameId(filePath, name) {
17514
18172
  return `${filePath}#${name}`;
17515
18173
  }
18174
+ function paramNameOf(param) {
18175
+ return param.type === "identifier" ? param : field(param, "name") ?? null;
18176
+ }
18177
+ function declaredParamNames(method) {
18178
+ const params = field(method, "parameters");
18179
+ const declared = /* @__PURE__ */ new Set();
18180
+ for (const param of params === null ? [] : children3(params)) {
18181
+ const name = paramNameOf(param);
18182
+ if (name !== null) {
18183
+ declared.add(name.text);
18184
+ }
18185
+ }
18186
+ return declared;
18187
+ }
18188
+ function readKey(filePath, node, enclosing) {
18189
+ if (node.type !== "identifier" && node.type !== "constant") {
18190
+ return nodeId(filePath, node);
18191
+ }
18192
+ const parent = node.parent;
18193
+ if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
18194
+ return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
18195
+ }
18196
+ if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
18197
+ return `${nodeId(filePath, enclosing)}#${node.text}`;
18198
+ }
18199
+ return nameId(filePath, node.text);
18200
+ }
17516
18201
  var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
17517
18202
  "string",
17518
18203
  "integer",
@@ -17673,6 +18358,9 @@ function emitExpressionFacts(emitter, node) {
17673
18358
  if (WRITTEN_VALUE_TYPES.has(child.type)) {
17674
18359
  add(emitter, "writtenValue", nodeId(emitter.filePath, child));
17675
18360
  }
18361
+ if (child.type === "nil") {
18362
+ add(emitter, "placeholderValue", nodeId(emitter.filePath, child));
18363
+ }
17676
18364
  });
17677
18365
  }
17678
18366
  function implicitReturn(body) {
@@ -17689,7 +18377,7 @@ function emitMethodFacts(emitter, method) {
17689
18377
  const declared = /* @__PURE__ */ new Set();
17690
18378
  let position = 0;
17691
18379
  for (const param of params === null ? [] : children3(params)) {
17692
- const paramName = param.type === "identifier" ? param : field(param, "name") ?? null;
18380
+ const paramName = paramNameOf(param);
17693
18381
  if (paramName !== null) {
17694
18382
  declared.add(paramName.text);
17695
18383
  const paramKey = `${funcKey}#${paramName.text}`;
@@ -17784,6 +18472,10 @@ function emitClassFacts(emitter, cls) {
17784
18472
  }
17785
18473
  continue;
17786
18474
  }
18475
+ if (NESTING_TYPES.has(statement.type)) {
18476
+ emitClassFacts(emitter, statement);
18477
+ continue;
18478
+ }
17787
18479
  if (!METHOD_TYPES.has(statement.type)) {
17788
18480
  continue;
17789
18481
  }
@@ -17807,7 +18499,7 @@ function emitValueFacts(db, filePath, root) {
17807
18499
  };
17808
18500
  const walk = (node) => {
17809
18501
  for (const child of children3(node)) {
17810
- if (child.type === "class") {
18502
+ if (NESTING_TYPES.has(child.type)) {
17811
18503
  declaresName(child, emitClassFacts(emitter, child));
17812
18504
  continue;
17813
18505
  }
@@ -17825,7 +18517,6 @@ function emitValueFacts(db, filePath, root) {
17825
18517
  }
17826
18518
 
17827
18519
  // src/facts/constants.ts
17828
- var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
17829
18520
  function children4(node) {
17830
18521
  return node.namedChildren.filter((child) => child !== null);
17831
18522
  }
@@ -17903,16 +18594,18 @@ function collectFileConstants(filePath, root) {
17903
18594
  }
17904
18595
  };
17905
18596
  walk(root, null, []);
17906
- return { definitions, references };
18597
+ return { filePath, definitions, references };
17907
18598
  }
17908
18599
  function emitConstantBindings(db, perFile) {
17909
18600
  const byName = /* @__PURE__ */ new Map();
18601
+ const fileOfDefinition = /* @__PURE__ */ new Map();
17910
18602
  const files = [...perFile];
17911
18603
  for (const file2 of files) {
17912
18604
  for (const definition of file2.definitions) {
17913
18605
  const found = byName.get(definition.qualifiedName) ?? [];
17914
18606
  found.push(definition.key);
17915
18607
  byName.set(definition.qualifiedName, found);
18608
+ fileOfDefinition.set(definition.key, file2.filePath);
17916
18609
  }
17917
18610
  }
17918
18611
  const settles = (candidate) => {
@@ -17929,6 +18622,10 @@ function emitConstantBindings(db, perFile) {
17929
18622
  const key = settles(candidate);
17930
18623
  if (key !== null) {
17931
18624
  db.add("binds", [reference.key, key]);
18625
+ const definedIn = fileOfDefinition.get(key);
18626
+ if (definedIn !== void 0 && definedIn !== file2.filePath) {
18627
+ db.add("rbConstantFrom", [file2.filePath, definedIn]);
18628
+ }
17932
18629
  break;
17933
18630
  }
17934
18631
  }
@@ -17936,10 +18633,682 @@ function emitConstantBindings(db, perFile) {
17936
18633
  }
17937
18634
  }
17938
18635
 
18636
+ // src/facts/why.ts
18637
+ import fs4 from "fs";
18638
+ import path5 from "path";
18639
+ import { Database as Database4 } from "@suss/datalog";
18640
+
18641
+ // src/parser.ts
18642
+ import fs2 from "fs";
18643
+ import path2 from "path";
18644
+ import { fileURLToPath } from "url";
18645
+ import { Language, Parser } from "web-tree-sitter";
18646
+ var GRAMMAR_PATH = path2.join(
18647
+ path2.dirname(fileURLToPath(import.meta.url)),
18648
+ "..",
18649
+ "grammar",
18650
+ "tree-sitter-ruby.wasm"
18651
+ );
18652
+ var languagePromise = null;
18653
+ var loadedLanguage = null;
18654
+ function loadLanguage() {
18655
+ languagePromise ??= (async () => {
18656
+ if (!fs2.existsSync(GRAMMAR_PATH)) {
18657
+ throw new Error(
18658
+ `Ruby grammar not found at ${GRAMMAR_PATH}. See grammar/README.md.`
18659
+ );
18660
+ }
18661
+ await Parser.init();
18662
+ const language = await Language.load(GRAMMAR_PATH);
18663
+ loadedLanguage = language;
18664
+ return language;
18665
+ })();
18666
+ return languagePromise;
18667
+ }
18668
+ async function parseRuby(source) {
18669
+ const language = await loadLanguage();
18670
+ return parseWith(language, source);
18671
+ }
18672
+ async function preloadRubyGrammar() {
18673
+ await loadLanguage();
18674
+ }
18675
+ function parseRubySync(source) {
18676
+ if (loadedLanguage === null) {
18677
+ throw new Error(
18678
+ "the Ruby grammar has not been loaded yet; call preloadRubyGrammar first"
18679
+ );
18680
+ }
18681
+ return parseWith(loadedLanguage, source);
18682
+ }
18683
+ function parseWith(language, source) {
18684
+ const parser = new Parser();
18685
+ parser.setLanguage(language);
18686
+ try {
18687
+ const tree = parser.parse(source);
18688
+ if (tree === null) {
18689
+ throw new Error("tree-sitter produced no tree for this source");
18690
+ }
18691
+ return tree;
18692
+ } finally {
18693
+ parser.delete();
18694
+ }
18695
+ }
18696
+
18697
+ // src/project.ts
18698
+ import fs3 from "fs";
18699
+ import path4 from "path";
18700
+ import {
18701
+ disambiguateSummaryIds,
18702
+ linkCallsToSummaries,
18703
+ placeArgTargets as placeArgTargets2,
18704
+ placeCalleeParameters as placeCalleeParameters2,
18705
+ placeCalls as placeCalls2,
18706
+ recordParameterGaps as recordParameterGaps2,
18707
+ summaryIdFromParts,
18708
+ unfollowedCallGap as unfollowedCallGap2
18709
+ } from "@suss/behavioral-ir";
18710
+ import { Database as Database3 } from "@suss/datalog";
18711
+ import {
18712
+ assembleSummary as assembleSummary2,
18713
+ createCacheLayer,
18714
+ createTimer,
18715
+ moduleInitStructure,
18716
+ noopTimer,
18717
+ runDigest,
18718
+ stampModuleImports
18719
+ } from "@suss/extractor";
18720
+
18721
+ // src/diagnostics.ts
18722
+ import {
18723
+ buildUngatedExtractionReport,
18724
+ createPackTallies,
18725
+ tallyUnit
18726
+ } from "@suss/extractor";
18727
+ function buildRubyExtractionReport(args) {
18728
+ return buildUngatedExtractionReport({
18729
+ packs: args.packs.map((pack) => ({
18730
+ name: pack.name,
18731
+ version: pack.version ?? null,
18732
+ discovers: pack.discovery.length > 0
18733
+ })),
18734
+ tallies: args.tallies,
18735
+ filesWalked: args.filesWalked,
18736
+ summaries: args.summaries
18737
+ });
18738
+ }
18739
+
18740
+ // src/facts.ts
18741
+ import path3 from "path";
18742
+ import { SKIP_CHILDREN as SKIP_CHILDREN3, walkDescendants as walkDescendants3 } from "@suss/extractor";
18743
+ function unitKey(filePath, range, name) {
18744
+ return `${filePath}:${range.start}-${range.end}#${name}`;
18745
+ }
18746
+ function emitEntryFact(db, filePath, range, name) {
18747
+ db.add("entry", [unitKey(filePath, range, name)]);
18748
+ }
18749
+ function emitRequireFacts(db, filePath, root, known) {
18750
+ walkDescendants3(root, null, {
18751
+ at: (node) => {
18752
+ const target = requiredRelativePath(node);
18753
+ if (target === null) {
18754
+ return;
18755
+ }
18756
+ const resolved = path3.resolve(
18757
+ path3.dirname(filePath),
18758
+ target.endsWith(".rb") ? target : `${target}.rb`
18759
+ );
18760
+ if (known.has(resolved)) {
18761
+ db.add("rbRequires", [filePath, resolved]);
18762
+ }
18763
+ },
18764
+ into: (node) => node.type === "argument_list" ? SKIP_CHILDREN3 : null
18765
+ });
18766
+ }
18767
+ function requiredRelativePath(node) {
18768
+ if (node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== "require_relative") {
18769
+ return null;
18770
+ }
18771
+ const { positional } = readCallArgs(field(node, "arguments"));
18772
+ return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
18773
+ }
18774
+
18775
+ // src/reach/closure.ts
18776
+ import {
18777
+ functionCallBinding,
18778
+ placeArgTargets,
18779
+ placeCalleeParameters,
18780
+ placeCalls,
18781
+ recordParameterGaps,
18782
+ TargetPlacements,
18783
+ unfollowedCallGap,
18784
+ worthRecording
18785
+ } from "@suss/behavioral-ir";
18786
+ import { Database as Database2, evaluate as evaluate3, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
18787
+ import { assembleSummary } from "@suss/extractor";
18788
+
18789
+ // src/reach/resolveCallee.ts
18790
+ var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
18791
+ var stop = (reason) => ({
18792
+ kind: "stopped",
18793
+ reason
18794
+ });
18795
+ function followed(target) {
18796
+ return { kind: "followed", target };
18797
+ }
18798
+ async function resolveCallee(call, site, ctx) {
18799
+ const methodName = calleeMethodName(call);
18800
+ if (methodName === void 0) {
18801
+ return stop("noDeclaration");
18802
+ }
18803
+ if (DYNAMIC_SEND_NAMES.has(methodName)) {
18804
+ return stop("unsettledValue");
18805
+ }
18806
+ const receiver = field(call, "receiver");
18807
+ if (receiver === null || receiver.type === "self") {
18808
+ return resolveImplicitSelf(methodName, site, ctx);
18809
+ }
18810
+ if (receiver.type === "identifier" && methodName === "call" && site.ownParameters.includes(receiver.text)) {
18811
+ return stop("callerSupplied");
18812
+ }
18813
+ return resolveOnReceiver(receiver, methodName, site, ctx);
18814
+ }
18815
+ async function resolveMethodReference(name, site, ctx) {
18816
+ const resolved = await resolveImplicitSelf(name, site, ctx);
18817
+ return resolved.kind === "followed" ? resolved.target : null;
18818
+ }
18819
+ function leavesRoomForATopLevelMethod(reason) {
18820
+ return reason === "noDeclaration" || reason === "outsideRun";
18821
+ }
18822
+ async function resolveImplicitSelf(methodName, site, ctx) {
18823
+ if (site.enclosingQualifiedName !== null) {
18824
+ const onClass = await methodOnAncestryOf(
18825
+ site.enclosingQualifiedName,
18826
+ methodName,
18827
+ ctx
18828
+ );
18829
+ if (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason)) {
18830
+ return onClass;
18831
+ }
18832
+ }
18833
+ return resolveTopLevelName(methodName, ctx);
18834
+ }
18835
+ function resolveTopLevelName(methodName, ctx) {
18836
+ const candidates = ctx.topLevelMethods.get(methodName);
18837
+ if (candidates === void 0 || candidates.length === 0) {
18838
+ return stop("noDeclaration");
18839
+ }
18840
+ if (candidates.length > 1) {
18841
+ return stop("multipleSources");
18842
+ }
18843
+ return followed(candidates[0]);
18844
+ }
18845
+ async function resolveOnReceiver(receiver, methodName, site, ctx) {
18846
+ if (receiver.type === "call") {
18847
+ const innerName = calleeMethodName(receiver);
18848
+ const innerReceiver = field(receiver, "receiver");
18849
+ if (innerName === "new" && innerReceiver !== null) {
18850
+ const classRef = classRefOf(innerReceiver, site, ctx);
18851
+ return classRef === null ? stop("unsettledValue") : methodOnAncestryOf(classRef, methodName, ctx);
18852
+ }
18853
+ const root = rootConstantOf(receiver);
18854
+ if (root === null) {
18855
+ return stop("unsettledValue");
18856
+ }
18857
+ return classRefOf(root, site, ctx) === null ? stop("outsideRun") : stop("unsettledValue");
18858
+ }
18859
+ if (receiver.type === "constant" || receiver.type === "scope_resolution") {
18860
+ const classRef = classRefOf(receiver, site, ctx);
18861
+ return classRef === null ? stop("outsideRun") : singletonMethodOn(classRef, methodName, ctx);
18862
+ }
18863
+ return stop("unsettledValue");
18864
+ }
18865
+ function rootConstantOf(node) {
18866
+ if (node.type === "constant" || node.type === "scope_resolution") {
18867
+ return node;
18868
+ }
18869
+ if (node.type !== "call") {
18870
+ return null;
18871
+ }
18872
+ const receiver = field(node, "receiver");
18873
+ return receiver === null ? null : rootConstantOf(receiver);
18874
+ }
18875
+ function classRefOf(node, site, ctx) {
18876
+ if (node.type === "constant") {
18877
+ const shadow = shadowingClassFor(node, site.nesting, ctx.knownClasses);
18878
+ if (shadow !== null) {
18879
+ return shadow;
18880
+ }
18881
+ const qualified = qualifyConstantRef(node, site.nesting);
18882
+ return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
18883
+ }
18884
+ if (node.type === "scope_resolution") {
18885
+ const qualified = qualifyConstantRef(node, site.nesting);
18886
+ return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
18887
+ }
18888
+ return null;
18889
+ }
18890
+ async function methodOnAncestryOf(qualifiedName, methodName, ctx) {
18891
+ const ownBlocks = ctx.lookup.localDefinition?.(qualifiedName) ?? null;
18892
+ if (ownBlocks === null) {
18893
+ return stop("outsideRun");
18894
+ }
18895
+ const ancestry = await ancestryOf(qualifiedName, ownBlocks, ctx.lookup);
18896
+ const found = methodInAncestry(ancestry, methodName);
18897
+ if (found.type === "found") {
18898
+ return followed(reachedMethod(found.method, found.block, methodName));
18899
+ }
18900
+ if (found.type === "unsettled") {
18901
+ return stop(
18902
+ found.cause === "dynamicDefine" ? "unsettledValue" : "outsideRun"
18903
+ );
18904
+ }
18905
+ return stop("noDeclaration");
18906
+ }
18907
+ function reachedMethod(node, block, name) {
18908
+ return {
18909
+ file: block.file,
18910
+ node,
18911
+ name,
18912
+ exportPath: [block.info.qualifiedName, name],
18913
+ enclosingQualifiedName: block.info.qualifiedName
18914
+ };
18915
+ }
18916
+ function singletonMethodOn(qualifiedName, methodName, ctx) {
18917
+ const ownBlocks = ctx.lookup.localDefinition?.(qualifiedName) ?? null;
18918
+ if (ownBlocks === null) {
18919
+ return stop("outsideRun");
18920
+ }
18921
+ for (const block of ownBlocks) {
18922
+ if (block.info.bodyNode === null) {
18923
+ continue;
18924
+ }
18925
+ const found = singletonMethodsByName(block.info.bodyNode).get(methodName);
18926
+ if (found !== void 0) {
18927
+ return followed(reachedMethod(found, block, methodName));
18928
+ }
18929
+ }
18930
+ return stop("noDeclaration");
18931
+ }
18932
+
18933
+ // src/reach/closure.ts
18934
+ var REACHABLE_RULES = [
18935
+ rule2("reachable", [v2("f")], [lit2("entry", v2("f"))]),
18936
+ rule2(
18937
+ "reachable",
18938
+ [v2("g")],
18939
+ [lit2("reachable", v2("f")), lit2("calls", v2("f"), v2("g"))]
18940
+ )
18941
+ ];
18942
+ async function reachedFunctions(seeds, options) {
18943
+ const ctx = buildReachContext(options.files);
18944
+ const db = new Database2();
18945
+ const functionByKey = /* @__PURE__ */ new Map();
18946
+ const seedKeys = /* @__PURE__ */ new Set();
18947
+ const scanned = /* @__PURE__ */ new Set();
18948
+ const targetsByKey = /* @__PURE__ */ new Map();
18949
+ const stopsByKey = /* @__PURE__ */ new Map();
18950
+ const argTargetsByKey = /* @__PURE__ */ new Map();
18951
+ const parameterCallsByKey = /* @__PURE__ */ new Map();
18952
+ const passedPositions = /* @__PURE__ */ new Set();
18953
+ for (const seed of seeds) {
18954
+ seedKeys.add(seed.key);
18955
+ functionByKey.set(seed.key, {
18956
+ file: seed.file,
18957
+ node: seed.node,
18958
+ name: field(seed.node, "name")?.text ?? "<anon>",
18959
+ exportPath: [],
18960
+ enclosingQualifiedName: seed.enclosingQualifiedName
18961
+ });
18962
+ db.add("entry", [seed.key]);
18963
+ }
18964
+ for (; ; ) {
18965
+ evaluate3(db, REACHABLE_RULES);
18966
+ const frontier = db.facts("reachable").map(([key]) => String(key)).filter((key) => !scanned.has(key));
18967
+ if (frontier.length === 0) {
18968
+ break;
18969
+ }
18970
+ for (const key of frontier) {
18971
+ scanned.add(key);
18972
+ const source = functionByKey.get(key);
18973
+ if (source === void 0) {
18974
+ continue;
18975
+ }
18976
+ const scan = await scanBody(source, ctx, options.displayPathOf);
18977
+ if (scan.stops.length > 0) {
18978
+ stopsByKey.set(key, scan.stops);
18979
+ }
18980
+ targetsByKey.set(key, scan.targets);
18981
+ argTargetsByKey.set(key, scan.argTargets);
18982
+ if (scan.parameterCalls.length > 0) {
18983
+ parameterCallsByKey.set(key, scan.parameterCalls);
18984
+ }
18985
+ for (const position of scan.passedPositions) {
18986
+ passedPositions.add(position);
18987
+ }
18988
+ for (const target of scan.followed) {
18989
+ const calleeKey = keyOf(target);
18990
+ if (!functionByKey.has(calleeKey)) {
18991
+ functionByKey.set(calleeKey, target);
18992
+ }
18993
+ db.add("calls", [key, calleeKey]);
18994
+ }
18995
+ }
18996
+ }
18997
+ const summaries = [];
18998
+ const summariesByKey = /* @__PURE__ */ new Map();
18999
+ for (const [keyAtom] of db.facts("reachable")) {
19000
+ const key = String(keyAtom);
19001
+ const target = functionByKey.get(key);
19002
+ if (seedKeys.has(key) || target === void 0) {
19003
+ continue;
19004
+ }
19005
+ const summary = assembleSummary(libraryUnit(target, options), {
19006
+ gapHandling: "permissive"
19007
+ });
19008
+ summary.confidence = { source: "inferred_static", level: "low" };
19009
+ summary.gaps.push(...(stopsByKey.get(key) ?? []).map(unfollowedCallGap));
19010
+ placeCalls(summary, targetsByKey.get(key));
19011
+ placeArgTargets(summary, argTargetsByKey.get(key));
19012
+ placeCalleeParameters(summary, parameterCallsByKey.get(key));
19013
+ summariesByKey.set(key, [summary]);
19014
+ summaries.push(summary);
19015
+ }
19016
+ recordParameterGaps(parameterCallsByKey, summariesByKey, passedPositions);
19017
+ return {
19018
+ summaries,
19019
+ targetsByKey,
19020
+ stopsByKey,
19021
+ argTargetsByKey,
19022
+ parameterCallsByKey,
19023
+ passedPositions
19024
+ };
19025
+ }
19026
+ function keyOf(target) {
19027
+ return nodeId(target.file, target.node);
19028
+ }
19029
+ function buildReachContext(files) {
19030
+ const blocksByQualifiedName = /* @__PURE__ */ new Map();
19031
+ const classes = [];
19032
+ for (const { file: file2, root } of files) {
19033
+ walkDefinitions(root, (info) => classes.push({ file: file2, info }));
19034
+ }
19035
+ const knownClasses = new Set(classes.map(({ info }) => info.qualifiedName));
19036
+ for (const { file: file2, info } of classes) {
19037
+ const list = blocksByQualifiedName.get(info.qualifiedName) ?? [];
19038
+ list.push({ info, knownClasses, file: file2 });
19039
+ blocksByQualifiedName.set(info.qualifiedName, list);
19040
+ }
19041
+ const topLevelMethods = /* @__PURE__ */ new Map();
19042
+ for (const { file: file2, root } of files) {
19043
+ for (const method of topLevelMethodNodes(root)) {
19044
+ const name = field(method, "name")?.text;
19045
+ if (name === void 0) {
19046
+ continue;
19047
+ }
19048
+ const list = topLevelMethods.get(name) ?? [];
19049
+ list.push({
19050
+ file: file2,
19051
+ node: method,
19052
+ name,
19053
+ exportPath: [name],
19054
+ enclosingQualifiedName: null
19055
+ });
19056
+ topLevelMethods.set(name, list);
19057
+ }
19058
+ }
19059
+ const lookup = {
19060
+ root: "",
19061
+ pathConvention: "railsUnderscore",
19062
+ ancestryRootClassNames: [],
19063
+ // Every class the run defines is already in `blocksByQualifiedName`
19064
+ // above, so a name that misses there is outside the run and this
19065
+ // never has a file on disk to read.
19066
+ parsedFile: async () => null,
19067
+ localDefinition: (name) => blocksByQualifiedName.get(name) ?? null
19068
+ };
19069
+ return { lookup, knownClasses, topLevelMethods };
19070
+ }
19071
+ function topLevelMethodNodes(root, found = []) {
19072
+ for (const child of bodyStatements(root)) {
19073
+ if (child.type === "method") {
19074
+ found.push(child);
19075
+ continue;
19076
+ }
19077
+ if (OWN_BODY_TYPES.has(child.type)) {
19078
+ continue;
19079
+ }
19080
+ topLevelMethodNodes(child, found);
19081
+ }
19082
+ return found;
19083
+ }
19084
+ function nestingOf(qualifiedName) {
19085
+ const parts = qualifiedName.split("::");
19086
+ const out = [];
19087
+ for (let depth = parts.length; depth >= 1; depth--) {
19088
+ out.push(parts.slice(0, depth).join("::"));
19089
+ }
19090
+ return out;
19091
+ }
19092
+ var EMPTY_SCAN = {
19093
+ followed: [],
19094
+ stops: [],
19095
+ targets: /* @__PURE__ */ new Map(),
19096
+ argTargets: /* @__PURE__ */ new Map(),
19097
+ parameterCalls: [],
19098
+ passedPositions: /* @__PURE__ */ new Set()
19099
+ };
19100
+ function methodReferenceSymbol(node) {
19101
+ const target = node.type === "block_argument" ? bodyStatements(node)[0] : node;
19102
+ if (target === void 0 || target.type !== "call" || field(target, "receiver") !== null || field(target, "method")?.text !== "method") {
19103
+ return null;
19104
+ }
19105
+ const args = field(target, "arguments");
19106
+ const argChildren = args === null ? [] : bodyStatements(args);
19107
+ return argChildren.length === 1 && argChildren[0]?.type === "simple_symbol" ? argChildren[0] : null;
19108
+ }
19109
+ async function scanBody(source, ctx, displayPathOf) {
19110
+ const body = field(source.node, "body");
19111
+ if (body === null) {
19112
+ return EMPTY_SCAN;
19113
+ }
19114
+ const calls = withoutChainLinks(bodyCalls(body));
19115
+ const ownParameters = positionalParameters(source.node).map((p) => p.name);
19116
+ const site = {
19117
+ enclosingQualifiedName: source.enclosingQualifiedName,
19118
+ nesting: source.enclosingQualifiedName === null ? [] : nestingOf(source.enclosingQualifiedName),
19119
+ ownParameters
19120
+ };
19121
+ const followed2 = [];
19122
+ const stops = [];
19123
+ const placements = new TargetPlacements();
19124
+ const parameterCalls = [];
19125
+ const passedPositions = /* @__PURE__ */ new Set();
19126
+ const seen = /* @__PURE__ */ new Set();
19127
+ const parameterCallsSeen = /* @__PURE__ */ new Set();
19128
+ const recordPassedArgs = async (call, callee, calleeKey) => {
19129
+ const args = field(call, "arguments");
19130
+ if (args === null) {
19131
+ return;
19132
+ }
19133
+ const argNodes = bodyStatements(args);
19134
+ for (let position = 0; position < argNodes.length; position += 1) {
19135
+ const symbol2 = methodReferenceSymbol(argNodes[position]);
19136
+ if (symbol2 === null) {
19137
+ continue;
19138
+ }
19139
+ const resolved = await resolveMethodReference(
19140
+ symbol2.text.slice(1),
19141
+ site,
19142
+ ctx
19143
+ );
19144
+ if (resolved === null) {
19145
+ continue;
19146
+ }
19147
+ const key = keyOf(resolved);
19148
+ if (!seen.has(key)) {
19149
+ seen.add(key);
19150
+ followed2.push(resolved);
19151
+ }
19152
+ placements.placeArg(callee, position, {
19153
+ file: displayPathOf(resolved.file),
19154
+ span: spanOf(resolved.node)
19155
+ });
19156
+ if (calleeKey !== null) {
19157
+ passedPositions.add(`${calleeKey}#${position}`);
19158
+ }
19159
+ }
19160
+ };
19161
+ for (const call of calls) {
19162
+ const callee = calleeText(call);
19163
+ const outcome = await resolveCallee(call, site, ctx);
19164
+ const placed = outcome.kind === "followed" ? {
19165
+ file: displayPathOf(outcome.target.file),
19166
+ span: spanOf(outcome.target.node)
19167
+ } : outcome.reason === "noDeclaration" ? null : { file: displayPathOf(source.file), span: spanOf(call) };
19168
+ placements.place(callee, placed);
19169
+ await recordPassedArgs(
19170
+ call,
19171
+ callee,
19172
+ outcome.kind === "followed" ? keyOf(outcome.target) : null
19173
+ );
19174
+ if (outcome.kind === "stopped") {
19175
+ const stopKey = `${outcome.reason}:${callee}`;
19176
+ if (!seen.has(stopKey) && worthRecording(outcome.reason)) {
19177
+ seen.add(stopKey);
19178
+ stops.push({ callee, reason: outcome.reason });
19179
+ }
19180
+ if (outcome.reason === "callerSupplied" && !parameterCallsSeen.has(callee)) {
19181
+ const receiverName = field(call, "receiver")?.text;
19182
+ const parameterIndex = receiverName === void 0 ? -1 : ownParameters.indexOf(receiverName);
19183
+ if (parameterIndex !== -1) {
19184
+ parameterCallsSeen.add(callee);
19185
+ parameterCalls.push({ callee, parameterIndex });
19186
+ }
19187
+ }
19188
+ continue;
19189
+ }
19190
+ const key = keyOf(outcome.target);
19191
+ if (!seen.has(key)) {
19192
+ seen.add(key);
19193
+ followed2.push(outcome.target);
19194
+ }
19195
+ }
19196
+ return {
19197
+ followed: followed2,
19198
+ stops,
19199
+ targets: placements.targets,
19200
+ argTargets: placements.argTargets,
19201
+ parameterCalls,
19202
+ passedPositions
19203
+ };
19204
+ }
19205
+ function libraryUnit(target, options) {
19206
+ const { file: file2, node, name, exportPath } = target;
19207
+ const body = bodyOfMethod(node, options.storage);
19208
+ const range = rangeOf(node);
19209
+ return {
19210
+ identity: {
19211
+ name,
19212
+ nameKind: "binding",
19213
+ kind: "library",
19214
+ file: options.displayPathOf(file2),
19215
+ range,
19216
+ span: spanOf(node),
19217
+ exportName: exportPath[0] ?? name,
19218
+ exportPath
19219
+ },
19220
+ boundaryBinding: functionCallBinding({
19221
+ transport: "in-process",
19222
+ recognition: "reachable"
19223
+ }),
19224
+ parameters: positionalParameters(node),
19225
+ branches: [
19226
+ {
19227
+ conditions: [],
19228
+ terminal: {
19229
+ kind: "return",
19230
+ statusCode: null,
19231
+ body: null,
19232
+ exceptionType: null,
19233
+ message: null,
19234
+ component: null,
19235
+ renderTree: null,
19236
+ delegateTarget: null,
19237
+ emitEvent: null,
19238
+ location: range
19239
+ },
19240
+ effects: body.effects ?? [],
19241
+ ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
19242
+ location: range,
19243
+ isDefault: true
19244
+ }
19245
+ ],
19246
+ bodyContent: body.bodyContent ?? "absent",
19247
+ dependencyCalls: [],
19248
+ declaredContract: null
19249
+ };
19250
+ }
19251
+ function positionalParameters(node) {
19252
+ const parameters = field(node, "parameters");
19253
+ if (parameters === null) {
19254
+ return [];
19255
+ }
19256
+ const out = [];
19257
+ let position = 0;
19258
+ for (const param of bodyStatements(parameters)) {
19259
+ const name = parameterName(param);
19260
+ if (name === null) {
19261
+ continue;
19262
+ }
19263
+ out.push({ name, position, role: name, typeText: null });
19264
+ position += 1;
19265
+ }
19266
+ return out;
19267
+ }
19268
+ function parameterName(param) {
19269
+ if (param.type === "identifier") {
19270
+ return param.text;
19271
+ }
19272
+ const named = field(param, "name");
19273
+ return named !== null && named.type === "identifier" ? named.text : null;
19274
+ }
19275
+
19276
+ // src/version.ts
19277
+ import { createAdapterStamp } from "@suss/extractor";
19278
+ var ADAPTER_VERSION = "0.1.0";
19279
+ var adapterStamp = createAdapterStamp({
19280
+ moduleUrl: import.meta.url,
19281
+ version: ADAPTER_VERSION
19282
+ });
19283
+
17939
19284
  // src/project.ts
17940
19285
  async function extractRubyProject(options) {
17941
- const db = new Database();
19286
+ const timer = options.onTiming !== void 0 ? createTimer() : noopTimer();
19287
+ const cacheDir = adapterStamp.declineWhenRunFromSource(
19288
+ options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? path4.join(options.projectRoot, ".suss", "cache") : null)
19289
+ );
19290
+ const extractionCache = createCacheLayer(cacheDir);
19291
+ const packsDigest = adapterStamp.packsDigest(
19292
+ options.packs.map(
19293
+ (pack) => pack.version !== void 0 ? { name: pack.name, version: pack.version } : { name: pack.name }
19294
+ )
19295
+ );
19296
+ const cacheInput = {
19297
+ files: cacheDir === null ? [] : options.files,
19298
+ adapterPacksDigest: cacheDir === null ? packsDigest : runDigest(packsDigest, options.packs, options.files)
19299
+ };
19300
+ const lookup = await timer.timeAsync(
19301
+ "cache.lookup",
19302
+ () => extractionCache.lookup(cacheInput)
19303
+ );
19304
+ options.onCacheDiagnostic?.(lookup.diagnostic);
19305
+ if (lookup.kind === "hit") {
19306
+ options.onTiming?.(timer.report());
19307
+ return { summaries: lookup.summaries, facts: new Database3() };
19308
+ }
19309
+ const db = new Database3();
17942
19310
  const summaries = [];
19311
+ const tallies = createPackTallies(options.packs);
17943
19312
  const constants = [];
17944
19313
  const cache = createFileCache(
17945
19314
  (source) => parseRuby(source).then((tree) => tree.rootNode),
@@ -17947,33 +19316,178 @@ async function extractRubyProject(options) {
17947
19316
  );
17948
19317
  const parsed = [];
17949
19318
  for (const file2 of options.files) {
17950
- const root = await cache.get(file2);
17951
- if (root === null) {
17952
- continue;
17953
- }
17954
- parsed.push({ file: file2, root });
17955
- emitValueFacts(db, file2, root);
17956
- constants.push(collectFileConstants(file2, root));
19319
+ await timer.timeAsync("parse", async () => {
19320
+ const root = await cache.get(file2);
19321
+ if (root === null) {
19322
+ return;
19323
+ }
19324
+ parsed.push({ file: file2, root });
19325
+ emitValueFacts(db, file2, root);
19326
+ constants.push(collectFileConstants(file2, root));
19327
+ });
17957
19328
  }
17958
- emitConstantBindings(db, constants);
19329
+ timer.time("discover", () => {
19330
+ emitConstantBindings(db, constants);
19331
+ const known = new Set(parsed.map(({ file: file2 }) => file2));
19332
+ for (const { file: file2, root } of parsed) {
19333
+ emitRequireFacts(db, file2, root, known);
19334
+ }
19335
+ });
17959
19336
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
19337
+ const displayPathOf = (file2) => options.workspaceRoot !== void 0 ? path4.relative(options.workspaceRoot, file2) : file2;
19338
+ const seeds = [];
19339
+ const summariesBySeed = /* @__PURE__ */ new Map();
17960
19340
  for (const { file: file2, root } of parsed) {
17961
- const displayPath = options.workspaceRoot !== void 0 ? path3.relative(options.workspaceRoot, file2) : file2;
17962
- const rawUnits = await discoverUnits(root, {
17963
- packs: options.packs,
17964
- filePath: displayPath,
17965
- cache,
17966
- ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {}
17967
- });
19341
+ const displayPath = displayPathOf(file2);
19342
+ const seedByRaw = /* @__PURE__ */ new Map();
19343
+ const rawUnits = await timer.timeAsync(
19344
+ "discover",
19345
+ () => discoverUnits(root, {
19346
+ packs: options.packs,
19347
+ filePath: displayPath,
19348
+ absoluteFile: file2,
19349
+ cache,
19350
+ ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {},
19351
+ onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
19352
+ })
19353
+ );
17968
19354
  for (const raw of rawUnits) {
17969
- const summary = assembleSummary(raw, { gapHandling: "permissive" });
19355
+ const summary = timer.time(
19356
+ "summarize",
19357
+ () => assembleSummary2(raw, { gapHandling: "permissive" })
19358
+ );
17970
19359
  summary.confidence = { source: "inferred_static", level: "low" };
17971
19360
  summaries.push(summary);
17972
19361
  emitEntryFact(db, file2, raw.identity.range, raw.identity.name);
19362
+ tallyUnit(tallies, raw.boundaryBinding?.recognition);
19363
+ const seed = seedByRaw.get(raw);
19364
+ if (seed === void 0) {
19365
+ continue;
19366
+ }
19367
+ const key = nodeId(seed.file, seed.node);
19368
+ const sharing = summariesBySeed.get(key);
19369
+ if (sharing === void 0) {
19370
+ seeds.push({
19371
+ key,
19372
+ file: seed.file,
19373
+ node: seed.node,
19374
+ enclosingQualifiedName: seed.enclosingQualifiedName
19375
+ });
19376
+ summariesBySeed.set(key, [summary]);
19377
+ } else {
19378
+ sharing.push(summary);
19379
+ }
19380
+ }
19381
+ const loadTimeReads = timer.time("discover", () => envReadEffects(root));
19382
+ if (loadTimeReads.length > 0) {
19383
+ const summary = timer.time(
19384
+ "summarize",
19385
+ () => assembleSummary2(
19386
+ moduleInitStructure({
19387
+ name: path4.basename(displayPath),
19388
+ file: displayPath,
19389
+ range: rangeOf(root),
19390
+ effects: loadTimeReads
19391
+ }),
19392
+ { gapHandling: "permissive" }
19393
+ )
19394
+ );
19395
+ summary.confidence = { source: "inferred_static", level: "low" };
19396
+ summaries.push(summary);
17973
19397
  }
17974
19398
  }
19399
+ for (const pack of options.packs) {
19400
+ for (const pattern of pack.discovery) {
19401
+ if (pattern.type !== "controllerActions") {
19402
+ continue;
19403
+ }
19404
+ const gaps = pattern.routingGaps?.() ?? [];
19405
+ if (gaps.length === 0) {
19406
+ continue;
19407
+ }
19408
+ const summary = timer.time(
19409
+ "summarize",
19410
+ () => assembleSummary2(routingGapUnit(pattern, gaps), {
19411
+ gapHandling: "permissive"
19412
+ })
19413
+ );
19414
+ summary.confidence = { source: "inferred_static", level: "low" };
19415
+ summaries.push(summary);
19416
+ }
19417
+ }
19418
+ const reached = await timer.timeAsync(
19419
+ "summarize",
19420
+ () => reachedFunctions(seeds, {
19421
+ files: parsed,
19422
+ displayPathOf,
19423
+ ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {}
19424
+ })
19425
+ );
19426
+ for (const [key, owners] of summariesBySeed) {
19427
+ for (const summary of owners) {
19428
+ summary.gaps.push(
19429
+ ...(reached.stopsByKey.get(key) ?? []).map(unfollowedCallGap2)
19430
+ );
19431
+ placeCalls2(summary, reached.targetsByKey.get(key));
19432
+ placeArgTargets2(summary, reached.argTargetsByKey.get(key));
19433
+ placeCalleeParameters2(summary, reached.parameterCallsByKey.get(key));
19434
+ }
19435
+ }
19436
+ recordParameterGaps2(
19437
+ reached.parameterCallsByKey,
19438
+ summariesBySeed,
19439
+ reached.passedPositions
19440
+ );
19441
+ summaries.push(...reached.summaries);
19442
+ const dependencies = fileDependenciesOf(db, displayPathOf);
19443
+ stampModuleImports(summaries, (file2) => dependencies.get(file2) ?? []);
19444
+ const idRoot = options.projectRoot ?? options.workspaceRoot;
19445
+ for (const summary of summaries) {
19446
+ const absoluteFile = options.workspaceRoot === void 0 ? summary.location.file : path4.resolve(options.workspaceRoot, summary.location.file);
19447
+ summary.identity.id = summaryIdFromParts({
19448
+ workspace: void 0,
19449
+ file: idRoot === void 0 ? absoluteFile : path4.relative(idRoot, absoluteFile),
19450
+ name: summary.identity.name,
19451
+ exportPath: summary.identity.exportPath
19452
+ });
19453
+ }
19454
+ disambiguateSummaryIds(summaries);
19455
+ linkCallsToSummaries(summaries);
19456
+ await timer.timeAsync("cache.write", async () => {
19457
+ if (cacheDir === null || summaries.length === 0) {
19458
+ return;
19459
+ }
19460
+ try {
19461
+ await extractionCache.write(cacheInput, summaries);
19462
+ } catch {
19463
+ }
19464
+ });
19465
+ options.onExtractionReport?.(
19466
+ buildRubyExtractionReport({
19467
+ packs: options.packs,
19468
+ tallies,
19469
+ filesWalked: options.files.length,
19470
+ summaries
19471
+ })
19472
+ );
19473
+ options.onTiming?.(timer.report());
17975
19474
  return { summaries, facts: db };
17976
19475
  }
19476
+ function fileDependenciesOf(db, displayPathOf) {
19477
+ const byFile = /* @__PURE__ */ new Map();
19478
+ for (const relation of ["rbRequires", "rbConstantFrom"]) {
19479
+ for (const [from, to] of db.facts(relation)) {
19480
+ if (typeof from !== "string" || typeof to !== "string") {
19481
+ continue;
19482
+ }
19483
+ const key = displayPathOf(from);
19484
+ const seen = byFile.get(key) ?? [];
19485
+ seen.push(displayPathOf(to));
19486
+ byFile.set(key, seen);
19487
+ }
19488
+ }
19489
+ return byFile;
19490
+ }
17977
19491
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
17978
19492
  function findRubyFiles(root) {
17979
19493
  const found = [];
@@ -17981,32 +19495,306 @@ function findRubyFiles(root) {
17981
19495
  for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
17982
19496
  if (entry.isDirectory()) {
17983
19497
  if (!SKIPPED_DIRECTORIES.has(entry.name)) {
17984
- walk(path3.join(dir, entry.name));
19498
+ walk(path4.join(dir, entry.name));
17985
19499
  }
17986
19500
  continue;
17987
19501
  }
17988
19502
  if (entry.isFile() && entry.name.endsWith(".rb")) {
17989
- found.push(path3.join(dir, entry.name));
19503
+ found.push(path4.join(dir, entry.name));
17990
19504
  }
17991
19505
  }
17992
19506
  };
17993
19507
  walk(root);
17994
19508
  return found.sort();
17995
19509
  }
19510
+
19511
+ // src/facts/resolve.ts
19512
+ import {
19513
+ constant as constant2,
19514
+ deriveOnDemand as deriveOnDemand2,
19515
+ evaluate as evaluate4,
19516
+ lit as lit3,
19517
+ rule as rule3,
19518
+ variable as v3
19519
+ } from "@suss/datalog";
19520
+ var RUBY_RULES = [
19521
+ // `Loader.new` makes one of the class, which the shared rules already say
19522
+ // about calling a class. Ruby writes it as a method read off the constant
19523
+ // instead, so the read is what steps to the class here.
19524
+ rule3(
19525
+ "stepsTo",
19526
+ [v3("x"), v3("cls"), VALUE_STEP],
19527
+ [
19528
+ lit3("readsProperty", v3("x"), v3("o"), constant2("new")),
19529
+ lit3("comesTo", v3("o"), v3("cls")),
19530
+ lit3("objectValue", v3("cls"))
19531
+ ]
19532
+ )
19533
+ ];
19534
+ var RESOLUTION_PROGRAM = deriveOnDemand2(
19535
+ [...RESOLUTION_RULES, ...RUBY_RULES, ...RESOLUTION_QUESTIONS],
19536
+ ANSWER_RELATIONS
19537
+ );
19538
+
19539
+ // src/facts/why.ts
19540
+ var WITNESS_RULES = [...RESOLUTION_RULES, ...RUBY_RULES];
19541
+ var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
19542
+ function namedChildrenOf(node) {
19543
+ return node.namedChildren.filter((child) => child !== null);
19544
+ }
19545
+ function enclosingMethod(node) {
19546
+ let current = node.parent;
19547
+ while (current !== null) {
19548
+ if (METHOD_TYPES2.has(current.type)) {
19549
+ return current;
19550
+ }
19551
+ current = current.parent;
19552
+ }
19553
+ return null;
19554
+ }
19555
+ function indexFile(file2, root, locations) {
19556
+ const walk = (node) => {
19557
+ locations.set(nodeId(file2, node), { file: file2, node });
19558
+ if (node.type === "constant") {
19559
+ locations.set(`${file2}#${node.text}`, { file: file2, node });
19560
+ }
19561
+ if (node.type === "assignment") {
19562
+ const left = field(node, "left");
19563
+ if (left !== null && left.type === "identifier") {
19564
+ locations.set(`${file2}#${left.text}`, { file: file2, node: left });
19565
+ }
19566
+ }
19567
+ for (const child of namedChildrenOf(node)) {
19568
+ walk(child);
19569
+ }
19570
+ };
19571
+ walk(root);
19572
+ }
19573
+ function displayNameOf(node) {
19574
+ if (node.type === "identifier" || node.type === "constant") {
19575
+ return node.text;
19576
+ }
19577
+ if (node.type === "method" || node.type === "singleton_method") {
19578
+ return field(node, "name")?.text ?? node.text;
19579
+ }
19580
+ if (node.type === "class" || node.type === "module") {
19581
+ return field(node, "name")?.text ?? node.text;
19582
+ }
19583
+ const text = node.text.split("\n")[0].trim();
19584
+ return text.length > 40 ? `${text.slice(0, 40)}...` : text;
19585
+ }
19586
+ function width(node) {
19587
+ return node.endIndex - node.startIndex;
19588
+ }
19589
+ var RubyWhySession = class {
19590
+ root;
19591
+ db = new Database4();
19592
+ locations = /* @__PURE__ */ new Map();
19593
+ trees = /* @__PURE__ */ new Map();
19594
+ constructor(options) {
19595
+ this.root = path5.resolve(options.dir);
19596
+ const constants = [];
19597
+ for (const file2 of findRubyFiles(this.root)) {
19598
+ const source = fs4.readFileSync(file2, "utf8");
19599
+ const root = parseRubySync(source).rootNode;
19600
+ this.trees.set(file2, root);
19601
+ emitValueFacts(this.db, file2, root);
19602
+ constants.push(collectFileConstants(file2, root));
19603
+ indexFile(file2, root, this.locations);
19604
+ }
19605
+ emitConstantBindings(this.db, constants);
19606
+ }
19607
+ /**
19608
+ * The smallest expression on `line` of `file` whose text is exactly
19609
+ * `text`, or null.
19610
+ */
19611
+ findExpression(file2, line, text) {
19612
+ const root = this.rootOf(file2);
19613
+ if (root === null) {
19614
+ return null;
19615
+ }
19616
+ let found = null;
19617
+ const visit = (node) => {
19618
+ if (node.startPosition.row + 1 === line && node.text === text) {
19619
+ if (found === null || width(node) <= width(found)) {
19620
+ found = node;
19621
+ }
19622
+ }
19623
+ for (const child of namedChildrenOf(node)) {
19624
+ visit(child);
19625
+ }
19626
+ };
19627
+ visit(root);
19628
+ return found === null ? null : { file: this.pathOf(file2), node: found };
19629
+ }
19630
+ /**
19631
+ * The callee of the call written as `calleeText` between `startLine`
19632
+ * and `endLine` of `file`, or null.
19633
+ */
19634
+ findCallee(file2, startLine, endLine, calleeText2) {
19635
+ const root = this.rootOf(file2);
19636
+ if (root === null) {
19637
+ return null;
19638
+ }
19639
+ let found = null;
19640
+ const visit = (node) => {
19641
+ if (node.type === "call") {
19642
+ const line = node.startPosition.row + 1;
19643
+ const callee = field(node, "method");
19644
+ if (line >= startLine && line <= endLine && callee !== null && callee.text === calleeText2) {
19645
+ found = callee;
19646
+ }
19647
+ }
19648
+ for (const child of namedChildrenOf(node)) {
19649
+ visit(child);
19650
+ }
19651
+ };
19652
+ visit(root);
19653
+ return found === null ? null : { file: this.pathOf(file2), node: found };
19654
+ }
19655
+ /**
19656
+ * Why `value` resolves to what it does: the witness proof, flattened
19657
+ * to the chain and rendered. Null when the value does not resolve,
19658
+ * which the caller says in its own words.
19659
+ */
19660
+ explain(value, options = {}) {
19661
+ return explainResolvedKey({
19662
+ db: this.db,
19663
+ rules: WITNESS_RULES,
19664
+ key: readKey(value.file, value.node, enclosingMethod(value.node)),
19665
+ locate: (key) => this.locate(key),
19666
+ displayPath: (key) => this.displayPath(key),
19667
+ ...options
19668
+ });
19669
+ }
19670
+ locate(key) {
19671
+ const located = this.locations.get(key);
19672
+ if (located === void 0) {
19673
+ return null;
19674
+ }
19675
+ return {
19676
+ name: displayNameOf(located.node),
19677
+ file: this.displayPath(located.file),
19678
+ line: located.node.startPosition.row + 1
19679
+ };
19680
+ }
19681
+ rootOf(file2) {
19682
+ return this.trees.get(this.pathOf(file2)) ?? null;
19683
+ }
19684
+ pathOf(file2) {
19685
+ return path5.isAbsolute(file2) ? file2 : path5.resolve(this.root, file2);
19686
+ }
19687
+ /** A file path said relative to the root, or as given when it is outside the root. */
19688
+ displayPath(key) {
19689
+ if (key.startsWith(this.root)) {
19690
+ return path5.relative(this.root, key);
19691
+ }
19692
+ return key;
19693
+ }
19694
+ };
19695
+
19696
+ // src/stubEvidence.ts
19697
+ import fs5 from "fs";
19698
+ import path6 from "path";
19699
+ import { SKIP_CHILDREN as SKIP_CHILDREN4, walkDescendants as walkDescendants4 } from "@suss/extractor";
19700
+ function bareLetters(name) {
19701
+ return name.replaceAll(/[-_]/g, "").toLowerCase();
19702
+ }
19703
+ function isSpelledFrom(qualifiedName, packageName) {
19704
+ const wanted = bareLetters(packageName);
19705
+ const constants = qualifiedName.split("::");
19706
+ return constants.some(
19707
+ (_, end) => bareLetters(constants.slice(0, end + 1).join("")) === wanted
19708
+ );
19709
+ }
19710
+ function isPackageRequire(target, packageName) {
19711
+ const bundlerName = packageName.replaceAll("-", "_");
19712
+ return target === bundlerName || target.startsWith(`${bundlerName}/`) || target === packageName || target.startsWith(`${packageName}/`);
19713
+ }
19714
+ function requireTarget(node) {
19715
+ if (node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== "require" && field(node, "method")?.text !== "require_relative") {
19716
+ return null;
19717
+ }
19718
+ const { positional } = readCallArgs(field(node, "arguments"));
19719
+ return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
19720
+ }
19721
+ function requireSitesIn(root, file2) {
19722
+ const found = [];
19723
+ walkDescendants4(root, null, {
19724
+ at: (node) => {
19725
+ const target = requireTarget(node);
19726
+ if (target !== null) {
19727
+ found.push({ target, file: file2, line: rangeOf(node).start });
19728
+ }
19729
+ },
19730
+ into: (node) => node.type === "argument_list" ? SKIP_CHILDREN4 : null
19731
+ });
19732
+ return found;
19733
+ }
19734
+ function extendsSitesIn(root, file2, packageName) {
19735
+ const found = [];
19736
+ walkClasses(root, (info) => {
19737
+ if (info.superclassQualifiedName !== null && isSpelledFrom(info.superclassQualifiedName, packageName)) {
19738
+ found.push({
19739
+ className: info.qualifiedName,
19740
+ superclassName: info.superclassQualifiedName,
19741
+ file: file2,
19742
+ line: rangeOf(info.node).start
19743
+ });
19744
+ }
19745
+ });
19746
+ return found;
19747
+ }
19748
+ async function rubyStubEvidence(options) {
19749
+ const requires = [];
19750
+ const extendsSites = [];
19751
+ for (const file2 of findRubyFiles(options.directory)) {
19752
+ const source = fs5.readFileSync(file2, "utf8");
19753
+ const tree = await parseRuby(source);
19754
+ const displayPath = path6.relative(options.directory, file2);
19755
+ for (const site of requireSitesIn(tree.rootNode, displayPath)) {
19756
+ if (isPackageRequire(site.target, options.packageName)) {
19757
+ requires.push(site);
19758
+ }
19759
+ }
19760
+ extendsSites.push(
19761
+ ...extendsSitesIn(tree.rootNode, displayPath, options.packageName)
19762
+ );
19763
+ }
19764
+ return { requires, extendsSites };
19765
+ }
17996
19766
  export {
19767
+ ADAPTER_VERSION,
19768
+ RubyWhySession,
17997
19769
  ancestryOf,
19770
+ bodyStatements,
19771
+ booleanLiteralValue,
19772
+ collectFileConstants,
17998
19773
  createFileCache,
17999
19774
  discoverUnits,
19775
+ emitConstantBindings,
18000
19776
  emitEntryFact,
19777
+ emitValueFacts,
18001
19778
  extractRubyProject,
19779
+ field,
18002
19780
  findRubyFiles,
18003
19781
  graphqlTypeNameFromQualified,
19782
+ hashKeySymbolName,
18004
19783
  inheritedStatements,
18005
19784
  methodInAncestry,
19785
+ nodeId,
18006
19786
  parseRuby,
19787
+ parseRubySync,
19788
+ preloadRubyGrammar,
18007
19789
  qualifyConstantRef,
18008
19790
  reachDefinition,
19791
+ readCallArgs,
19792
+ readKey,
18009
19793
  resolveConstantFile,
19794
+ rubyStubEvidence,
19795
+ runStatements,
19796
+ stringLiteralValue,
19797
+ symbolValue,
18010
19798
  typeShapeFromNode,
18011
19799
  underscoreConstantPath,
18012
19800
  unitKey,