@suss/adapter-ruby 0.21.1 → 0.22.1

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 as dispatchByType2, 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
  }
@@ -15620,8 +15827,8 @@ function baseUrlReference(semantics) {
15620
15827
  const label = OPENING_HOLE.exec(semantics.path ?? "")?.[1];
15621
15828
  return label === void 0 ? null : referenceFromName(patternHole(label));
15622
15829
  }
15623
- function normalizePath(path4) {
15624
- 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}");
15625
15832
  if (normalized.length > 1 && normalized.endsWith("/")) {
15626
15833
  normalized = normalized.slice(0, -1);
15627
15834
  }
@@ -15631,8 +15838,8 @@ function normalizePath(path4) {
15631
15838
  );
15632
15839
  return normalized;
15633
15840
  }
15634
- function pathShape(path4) {
15635
- return normalizePath(path4).replace(/\{[^}]*\}/g, "{}");
15841
+ function pathShape(path7) {
15842
+ return normalizePath(path7).replace(/\{[^}]*\}/g, "{}");
15636
15843
  }
15637
15844
  function methodsAgree(a, b) {
15638
15845
  if (a === null || b === null) {
@@ -15710,8 +15917,8 @@ var restSemantics = defineBoundarySemantics({
15710
15917
  return null;
15711
15918
  }
15712
15919
  const method = semantics.method === null ? "ANY" : semantics.method.toUpperCase();
15713
- const path4 = semantics.path === null ? "?" : normalizePath(semantics.path);
15714
- return `${method} ${path4}`;
15920
+ const path7 = semantics.path === null ? "?" : normalizePath(semantics.path);
15921
+ return `${method} ${path7}`;
15715
15922
  },
15716
15923
  /**
15717
15924
  * A route with both halves stated decides for itself: the method
@@ -15720,14 +15927,14 @@ var restSemantics = defineBoundarySemantics({
15720
15927
  * half might still be the one that handles the request, and nothing
15721
15928
  * here can settle that, so it abstains instead of refusing.
15722
15929
  */
15723
- servesRequest(semantics, method, path4) {
15930
+ servesRequest(semantics, method, path7) {
15724
15931
  if (semantics.method === null || semantics.path === null) {
15725
15932
  return "unknown";
15726
15933
  }
15727
15934
  if (!methodsAgree(semantics.method, method.toUpperCase())) {
15728
15935
  return "nomatch";
15729
15936
  }
15730
- return routePathAdmits(semantics.path, path4) ? "match" : "nomatch";
15937
+ return routePathAdmits(semantics.path, path7) ? "match" : "nomatch";
15731
15938
  },
15732
15939
  /**
15733
15940
  * A route the registration pattern admits is inside it. A route
@@ -16146,7 +16353,291 @@ function storageBinding(opts) {
16146
16353
 
16147
16354
  // ../../resolution/dist/index.js
16148
16355
  import { ruleLabel } from "@suss/datalog";
16356
+ import { Database, evaluate, proofOf, witnesses } from "@suss/datalog";
16149
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
+ }
16150
16641
  var VALUE_STEP = constant("value");
16151
16642
  var NAMESPACE_IMPORT_NAME = "*";
16152
16643
  var NAMESPACE_IMPORT = constant(NAMESPACE_IMPORT_NAME);
@@ -16338,6 +16829,18 @@ var RESOLUTION_RULES = [
16338
16829
  [v("x"), v("z")],
16339
16830
  [lit("reaches", v("x"), v("z"), VALUE_STEP), lit("objectValue", v("z"))]
16340
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
+ ),
16341
16844
  // What one call site put in a parameter, told apart from what the
16342
16845
  // other callers passed.
16343
16846
  rule(
@@ -16899,11 +17402,11 @@ var ANCESTRY_PROGRAM = deriveOnDemand(
16899
17402
  );
16900
17403
  function reachesBase(facts, classKey, bases) {
16901
17404
  facts.add("wantedAncestry", [classKey]);
16902
- evaluate(facts, ANCESTRY_PROGRAM.rules);
17405
+ evaluate2(facts, ANCESTRY_PROGRAM.rules);
16903
17406
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
16904
17407
  }
16905
- function classBehind(facts, constant2) {
16906
- const wanted = `#${constant2.text}`;
17408
+ function classBehind(facts, constant3) {
17409
+ const wanted = `#${constant3.text}`;
16907
17410
  const bound = new Set(
16908
17411
  facts.facts("binds").filter((row) => String(row[0]).endsWith(wanted)).map((row) => String(row[1]))
16909
17412
  );
@@ -16946,11 +17449,11 @@ function storageEffects(calls, options) {
16946
17449
  if (partOfOne.has(call.id)) {
16947
17450
  continue;
16948
17451
  }
16949
- const constant2 = rootConstant(call);
16950
- if (constant2 === null) {
17452
+ const constant3 = rootConstant(call);
17453
+ if (constant3 === null) {
16951
17454
  continue;
16952
17455
  }
16953
- const classKey = classBehind(options.facts, constant2);
17456
+ const classKey = classBehind(options.facts, constant3);
16954
17457
  if (classKey === void 0) {
16955
17458
  continue;
16956
17459
  }
@@ -16968,7 +17471,7 @@ function storageEffects(calls, options) {
16968
17471
  recognition: "ruby-storage",
16969
17472
  storageSystem: pattern.storageSystem,
16970
17473
  scope: pattern.baseClasses[0] ?? "",
16971
- container: constant2.text
17474
+ container: constant3.text
16972
17475
  }),
16973
17476
  callee: call.text,
16974
17477
  interaction: {
@@ -17084,7 +17587,8 @@ async function discoverUnits(root, options) {
17084
17587
  const knownClasses = new Set(classes.map((info) => info.qualifiedName));
17085
17588
  const fileBlocks = classes.map((info) => ({
17086
17589
  info,
17087
- knownClasses
17590
+ knownClasses,
17591
+ file: options.absoluteFile ?? options.filePath
17088
17592
  }));
17089
17593
  const units = [];
17090
17594
  for (const info of classes) {
@@ -17108,14 +17612,15 @@ async function discoverUnits(root, options) {
17108
17612
  }
17109
17613
  return units;
17110
17614
  }
17111
- function reachesConfiguredBase(ancestry, self, pattern) {
17615
+ function reachesConfiguredBase(ancestry, self, baseClassNames) {
17112
17616
  return ancestry.some(
17113
- (entry) => entry.name !== self && pattern.baseClassNames.includes(entry.name)
17617
+ (entry) => entry.name !== self && baseClassNames.includes(entry.name)
17114
17618
  );
17115
17619
  }
17116
17620
  function unitsFor(pattern, pack, info, ownBlocks, fileBlocks, options) {
17117
17621
  const table = {
17118
- 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)
17119
17624
  };
17120
17625
  return dispatchByType2(table, pattern);
17121
17626
  }
@@ -17130,7 +17635,7 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
17130
17635
  options.storage
17131
17636
  );
17132
17637
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
17133
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern)) {
17638
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
17134
17639
  return [];
17135
17640
  }
17136
17641
  const typeName = graphqlTypeNameFromQualified(
@@ -17149,9 +17654,145 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
17149
17654
  declsByName.set(decl.fieldName, decl);
17150
17655
  }
17151
17656
  }
17152
- return [...declsByName.values()].map(
17153
- (decl) => buildFieldUnit(pack, typeName, decl, options.filePath)
17154
- );
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
+ });
17664
+ }
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
+ };
17155
17796
  }
17156
17797
  function blockConfiguresCall(pattern) {
17157
17798
  const names = /* @__PURE__ */ new Set([
@@ -17192,12 +17833,15 @@ function branchesFor(body, range) {
17192
17833
  }
17193
17834
  function bodyOfMethod(method, storage) {
17194
17835
  const effects = invocationEffects(method);
17195
- const database = storage === void 0 ? [] : storageEffects(callsUnder(method), storage);
17836
+ const extra = [
17837
+ ...envReadEffects(method),
17838
+ ...storage === void 0 ? [] : storageEffects(callsUnder(method), storage)
17839
+ ];
17196
17840
  return {
17197
17841
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
17198
17842
  readings: [],
17199
17843
  ...effects.length > 0 ? { effects } : {},
17200
- ...database.length > 0 ? { extraEffects: database } : {}
17844
+ ...extra.length > 0 ? { extraEffects: extra } : {}
17201
17845
  };
17202
17846
  }
17203
17847
  function callsUnder(node, found = []) {
@@ -17221,7 +17865,14 @@ function methodNotSettled(reason, range) {
17221
17865
  }
17222
17866
  function bodyFromLookup(found, range, subject, nothingThere, storage) {
17223
17867
  const table = {
17224
- 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
+ }),
17225
17876
  unsettled: (lookup) => methodNotSettled(
17226
17877
  `${subject} could be answered by a method ${lookup.reason}, so whether one exists was not settled here`,
17227
17878
  range
@@ -17513,65 +18164,39 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
17513
18164
  };
17514
18165
  }
17515
18166
 
17516
- // src/facts.ts
17517
- function unitKey(filePath, range, name) {
17518
- return `${filePath}:${range.start}-${range.end}#${name}`;
18167
+ // src/facts/values.ts
18168
+ function nodeId(filePath, node) {
18169
+ return `${filePath}:${node.startIndex}-${node.endIndex}`;
17519
18170
  }
17520
- function emitEntryFact(db, filePath, range, name) {
17521
- db.add("entry", [unitKey(filePath, range, name)]);
18171
+ function nameId(filePath, name) {
18172
+ return `${filePath}#${name}`;
17522
18173
  }
17523
-
17524
- // src/parser.ts
17525
- import fs2 from "fs";
17526
- import path2 from "path";
17527
- import { fileURLToPath } from "url";
17528
- import { Language, Parser } from "web-tree-sitter";
17529
- var GRAMMAR_PATH = path2.join(
17530
- path2.dirname(fileURLToPath(import.meta.url)),
17531
- "..",
17532
- "grammar",
17533
- "tree-sitter-ruby.wasm"
17534
- );
17535
- var languagePromise = null;
17536
- function loadLanguage() {
17537
- languagePromise ??= (async () => {
17538
- if (!fs2.existsSync(GRAMMAR_PATH)) {
17539
- throw new Error(
17540
- `Ruby grammar not found at ${GRAMMAR_PATH}. See grammar/README.md.`
17541
- );
17542
- }
17543
- await Parser.init();
17544
- return Language.load(GRAMMAR_PATH);
17545
- })();
17546
- return languagePromise;
18174
+ function paramNameOf(param) {
18175
+ return param.type === "identifier" ? param : field(param, "name") ?? null;
17547
18176
  }
17548
- async function parseRuby(source) {
17549
- const language = await loadLanguage();
17550
- const parser = new Parser();
17551
- parser.setLanguage(language);
17552
- try {
17553
- const tree = parser.parse(source);
17554
- if (tree === null) {
17555
- throw new Error("tree-sitter produced no tree for this source");
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);
17556
18184
  }
17557
- return tree;
17558
- } finally {
17559
- parser.delete();
17560
18185
  }
18186
+ return declared;
17561
18187
  }
17562
-
17563
- // src/project.ts
17564
- import fs3 from "fs";
17565
- import path3 from "path";
17566
- import { Database } from "@suss/datalog";
17567
- import { assembleSummary } from "@suss/extractor";
17568
-
17569
- // src/facts/values.ts
17570
- function nodeId(filePath, node) {
17571
- return `${filePath}:${node.startIndex}-${node.endIndex}`;
17572
- }
17573
- function nameId(filePath, name) {
17574
- return `${filePath}#${name}`;
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);
17575
18200
  }
17576
18201
  var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
17577
18202
  "string",
@@ -17733,6 +18358,9 @@ function emitExpressionFacts(emitter, node) {
17733
18358
  if (WRITTEN_VALUE_TYPES.has(child.type)) {
17734
18359
  add(emitter, "writtenValue", nodeId(emitter.filePath, child));
17735
18360
  }
18361
+ if (child.type === "nil") {
18362
+ add(emitter, "placeholderValue", nodeId(emitter.filePath, child));
18363
+ }
17736
18364
  });
17737
18365
  }
17738
18366
  function implicitReturn(body) {
@@ -17749,7 +18377,7 @@ function emitMethodFacts(emitter, method) {
17749
18377
  const declared = /* @__PURE__ */ new Set();
17750
18378
  let position = 0;
17751
18379
  for (const param of params === null ? [] : children3(params)) {
17752
- const paramName = param.type === "identifier" ? param : field(param, "name") ?? null;
18380
+ const paramName = paramNameOf(param);
17753
18381
  if (paramName !== null) {
17754
18382
  declared.add(paramName.text);
17755
18383
  const paramKey = `${funcKey}#${paramName.text}`;
@@ -17844,6 +18472,10 @@ function emitClassFacts(emitter, cls) {
17844
18472
  }
17845
18473
  continue;
17846
18474
  }
18475
+ if (NESTING_TYPES.has(statement.type)) {
18476
+ emitClassFacts(emitter, statement);
18477
+ continue;
18478
+ }
17847
18479
  if (!METHOD_TYPES.has(statement.type)) {
17848
18480
  continue;
17849
18481
  }
@@ -17867,7 +18499,7 @@ function emitValueFacts(db, filePath, root) {
17867
18499
  };
17868
18500
  const walk = (node) => {
17869
18501
  for (const child of children3(node)) {
17870
- if (child.type === "class") {
18502
+ if (NESTING_TYPES.has(child.type)) {
17871
18503
  declaresName(child, emitClassFacts(emitter, child));
17872
18504
  continue;
17873
18505
  }
@@ -17885,7 +18517,6 @@ function emitValueFacts(db, filePath, root) {
17885
18517
  }
17886
18518
 
17887
18519
  // src/facts/constants.ts
17888
- var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
17889
18520
  function children4(node) {
17890
18521
  return node.namedChildren.filter((child) => child !== null);
17891
18522
  }
@@ -17963,16 +18594,18 @@ function collectFileConstants(filePath, root) {
17963
18594
  }
17964
18595
  };
17965
18596
  walk(root, null, []);
17966
- return { definitions, references };
18597
+ return { filePath, definitions, references };
17967
18598
  }
17968
18599
  function emitConstantBindings(db, perFile) {
17969
18600
  const byName = /* @__PURE__ */ new Map();
18601
+ const fileOfDefinition = /* @__PURE__ */ new Map();
17970
18602
  const files = [...perFile];
17971
18603
  for (const file2 of files) {
17972
18604
  for (const definition of file2.definitions) {
17973
18605
  const found = byName.get(definition.qualifiedName) ?? [];
17974
18606
  found.push(definition.key);
17975
18607
  byName.set(definition.qualifiedName, found);
18608
+ fileOfDefinition.set(definition.key, file2.filePath);
17976
18609
  }
17977
18610
  }
17978
18611
  const settles = (candidate) => {
@@ -17989,6 +18622,10 @@ function emitConstantBindings(db, perFile) {
17989
18622
  const key = settles(candidate);
17990
18623
  if (key !== null) {
17991
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
+ }
17992
18629
  break;
17993
18630
  }
17994
18631
  }
@@ -17996,10 +18633,682 @@ function emitConstantBindings(db, perFile) {
17996
18633
  }
17997
18634
  }
17998
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
+
17999
19284
  // src/project.ts
18000
19285
  async function extractRubyProject(options) {
18001
- 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();
18002
19310
  const summaries = [];
19311
+ const tallies = createPackTallies(options.packs);
18003
19312
  const constants = [];
18004
19313
  const cache = createFileCache(
18005
19314
  (source) => parseRuby(source).then((tree) => tree.rootNode),
@@ -18007,33 +19316,178 @@ async function extractRubyProject(options) {
18007
19316
  );
18008
19317
  const parsed = [];
18009
19318
  for (const file2 of options.files) {
18010
- const root = await cache.get(file2);
18011
- if (root === null) {
18012
- continue;
18013
- }
18014
- parsed.push({ file: file2, root });
18015
- emitValueFacts(db, file2, root);
18016
- 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
+ });
18017
19328
  }
18018
- 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
+ });
18019
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();
18020
19340
  for (const { file: file2, root } of parsed) {
18021
- const displayPath = options.workspaceRoot !== void 0 ? path3.relative(options.workspaceRoot, file2) : file2;
18022
- const rawUnits = await discoverUnits(root, {
18023
- packs: options.packs,
18024
- filePath: displayPath,
18025
- cache,
18026
- ...storagePatterns.length > 0 ? { storage: { facts: db, patterns: storagePatterns } } : {}
18027
- });
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
+ );
18028
19354
  for (const raw of rawUnits) {
18029
- const summary = assembleSummary(raw, { gapHandling: "permissive" });
19355
+ const summary = timer.time(
19356
+ "summarize",
19357
+ () => assembleSummary2(raw, { gapHandling: "permissive" })
19358
+ );
18030
19359
  summary.confidence = { source: "inferred_static", level: "low" };
18031
19360
  summaries.push(summary);
18032
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);
19397
+ }
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);
18033
19416
  }
18034
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());
18035
19474
  return { summaries, facts: db };
18036
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
+ }
18037
19491
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
18038
19492
  function findRubyFiles(root) {
18039
19493
  const found = [];
@@ -18041,32 +19495,306 @@ function findRubyFiles(root) {
18041
19495
  for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
18042
19496
  if (entry.isDirectory()) {
18043
19497
  if (!SKIPPED_DIRECTORIES.has(entry.name)) {
18044
- walk(path3.join(dir, entry.name));
19498
+ walk(path4.join(dir, entry.name));
18045
19499
  }
18046
19500
  continue;
18047
19501
  }
18048
19502
  if (entry.isFile() && entry.name.endsWith(".rb")) {
18049
- found.push(path3.join(dir, entry.name));
19503
+ found.push(path4.join(dir, entry.name));
18050
19504
  }
18051
19505
  }
18052
19506
  };
18053
19507
  walk(root);
18054
19508
  return found.sort();
18055
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
+ }
18056
19766
  export {
19767
+ ADAPTER_VERSION,
19768
+ RubyWhySession,
18057
19769
  ancestryOf,
19770
+ bodyStatements,
19771
+ booleanLiteralValue,
19772
+ collectFileConstants,
18058
19773
  createFileCache,
18059
19774
  discoverUnits,
19775
+ emitConstantBindings,
18060
19776
  emitEntryFact,
19777
+ emitValueFacts,
18061
19778
  extractRubyProject,
19779
+ field,
18062
19780
  findRubyFiles,
18063
19781
  graphqlTypeNameFromQualified,
19782
+ hashKeySymbolName,
18064
19783
  inheritedStatements,
18065
19784
  methodInAncestry,
19785
+ nodeId,
18066
19786
  parseRuby,
19787
+ parseRubySync,
19788
+ preloadRubyGrammar,
18067
19789
  qualifyConstantRef,
18068
19790
  reachDefinition,
19791
+ readCallArgs,
19792
+ readKey,
18069
19793
  resolveConstantFile,
19794
+ rubyStubEvidence,
19795
+ runStatements,
19796
+ stringLiteralValue,
19797
+ symbolValue,
18070
19798
  typeShapeFromNode,
18071
19799
  underscoreConstantPath,
18072
19800
  unitKey,