@suss/adapter-typescript 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -67,7 +67,7 @@ __export(index_exports, {
67
67
  module.exports = __toCommonJS(index_exports);
68
68
 
69
69
  // src/adapter.ts
70
- var import_node_path9 = __toESM(require("path"), 1);
70
+ var import_node_path10 = __toESM(require("path"), 1);
71
71
  var import_ts_morph60 = require("ts-morph");
72
72
  var import_behavioral_ir8 = require("@suss/behavioral-ir");
73
73
  var import_datalog6 = require("@suss/datalog");
@@ -1864,8 +1864,8 @@ function loadImportGraphsDepthFirstFromPaths(project, rootPaths) {
1864
1864
  stack.pop();
1865
1865
  loadOrder.push(top.path);
1866
1866
  let deepest = 0;
1867
- for (const path12 of top.deps) {
1868
- deepest = Math.max(deepest, chainDepth.get(path12) ?? 0);
1867
+ for (const path13 of top.deps) {
1868
+ deepest = Math.max(deepest, chainDepth.get(path13) ?? 0);
1869
1869
  }
1870
1870
  chainDepth.set(top.path, deepest + 1);
1871
1871
  }
@@ -2210,11 +2210,11 @@ function walkAndWarm(root, reach) {
2210
2210
  if (warmedFiles.get(target) !== void 0) {
2211
2211
  continue;
2212
2212
  }
2213
- const path12 = target.getFilePath();
2214
- if (visited.has(path12)) {
2213
+ const path13 = target.getFilePath();
2214
+ if (visited.has(path13)) {
2215
2215
  continue;
2216
2216
  }
2217
- visited.add(path12);
2217
+ visited.add(path13);
2218
2218
  stack.push(frameOf(target, reach));
2219
2219
  continue;
2220
2220
  }
@@ -2231,7 +2231,7 @@ function sitsOnWarmChains(file) {
2231
2231
  return false;
2232
2232
  }
2233
2233
  const imported = importedFilePathsOf(project, file.getFilePath());
2234
- return imported.length > 0 && imported.every((path12) => warmed.has(path12));
2234
+ return imported.length > 0 && imported.every((path13) => warmed.has(path13));
2235
2235
  }
2236
2236
  function markWarmed(file) {
2237
2237
  warmedFiles.set(file, true);
@@ -5848,9 +5848,9 @@ function createSourceFileLookup(project) {
5848
5848
  if (sf === null) {
5849
5849
  return null;
5850
5850
  }
5851
- const path12 = sf.getFilePath();
5852
- const index = functionsByFile.get(path12) ?? indexFunctions(sf);
5853
- functionsByFile.set(path12, index);
5851
+ const path13 = sf.getFilePath();
5852
+ const index = functionsByFile.get(path13) ?? indexFunctions(sf);
5853
+ functionsByFile.set(path13, index);
5854
5854
  return index.get(rangeKey(location.range)) ?? null;
5855
5855
  }
5856
5856
  };
@@ -6377,7 +6377,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6377
6377
  }
6378
6378
  const responses = [];
6379
6379
  let method;
6380
- let path12;
6380
+ let path13;
6381
6381
  for (const prop of endpointNode.getProperties()) {
6382
6382
  if (!import_ts_morph28.Node.isPropertyAssignment(prop)) {
6383
6383
  continue;
@@ -6392,7 +6392,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6392
6392
  if (propName === pattern.pathProperty) {
6393
6393
  const val = prop.getInitializer();
6394
6394
  if (val !== void 0 && import_ts_morph28.Node.isStringLiteral(val)) {
6395
- path12 = val.getLiteralValue();
6395
+ path13 = val.getLiteralValue();
6396
6396
  }
6397
6397
  }
6398
6398
  if (propName === pattern.responseExtraction.property) {
@@ -6416,10 +6416,10 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6416
6416
  if (responses.length === 0) {
6417
6417
  return null;
6418
6418
  }
6419
- const boundaryBinding = method !== void 0 || path12 !== void 0 ? (0, import_behavioral_ir2.restBinding)({
6419
+ const boundaryBinding = method !== void 0 || path13 !== void 0 ? (0, import_behavioral_ir2.restBinding)({
6420
6420
  transport: "http",
6421
6421
  method: method ?? null,
6422
- path: path12 ?? null,
6422
+ path: path13 ?? null,
6423
6423
  recognition: framework
6424
6424
  }) : null;
6425
6425
  return {
@@ -8334,7 +8334,34 @@ function joinRoutePath(prefix, suffix) {
8334
8334
  }
8335
8335
  return `/${segments.join("/")}`;
8336
8336
  }
8337
- function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8337
+ function declaredChannelOf(source, decorator, resolution) {
8338
+ if (source.from === "literal") {
8339
+ return source.value;
8340
+ }
8341
+ if (source.from === "unstated") {
8342
+ return null;
8343
+ }
8344
+ if (!import_ts_morph38.Node.isDecorator(decorator)) {
8345
+ return null;
8346
+ }
8347
+ const arg = decorator.getArguments()[source.position];
8348
+ if (arg === void 0) {
8349
+ return null;
8350
+ }
8351
+ const text = literalStringOf(arg, resolution);
8352
+ return text === null || text === "" ? null : text;
8353
+ }
8354
+ function literalStringOf(node, resolution) {
8355
+ if (import_ts_morph38.Node.isStringLiteral(node) || import_ts_morph38.Node.isNoSubstitutionTemplateLiteral(node)) {
8356
+ return node.getLiteralValue();
8357
+ }
8358
+ const written = resolution?.resolveWrittenValue(node) ?? null;
8359
+ if (written === null || written === node) {
8360
+ return null;
8361
+ }
8362
+ return literalStringOf(written, resolution);
8363
+ }
8364
+ function discoverDecoratedRoutes(sourceFile, match, kind, resolution, binding) {
8338
8365
  const acceptedModules = Array.isArray(match.importModule) ? match.importModule : [match.importModule];
8339
8366
  const routeDecoratorNames = Object.keys(match.methodDecoratorRouteMap);
8340
8367
  const importedRouteDecorators = /* @__PURE__ */ new Set();
@@ -8366,6 +8393,22 @@ function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8366
8393
  const pathPrefix = routePathOf(marker.args, resolution);
8367
8394
  const className = cls.getName() ?? "<anon-class>";
8368
8395
  for (const handler of decoratedCallablesOf(cls, routeDecoratorNames)) {
8396
+ if (binding !== void 0) {
8397
+ results.push({
8398
+ func: handler.func,
8399
+ kind,
8400
+ name: `${className}.${handler.name}`,
8401
+ channelInfo: {
8402
+ messageBus: binding.messageBus,
8403
+ channel: declaredChannelOf(
8404
+ binding.channel,
8405
+ handler.decorator,
8406
+ resolution
8407
+ )
8408
+ }
8409
+ });
8410
+ continue;
8411
+ }
8369
8412
  const httpMethod = match.methodDecoratorRouteMap[handler.standsFor];
8370
8413
  const pathSuffix = resolveRoutePathArg(handler.decorator, resolution);
8371
8414
  const routePath = joinRoutePath(pathPrefix, pathSuffix);
@@ -9379,7 +9422,7 @@ function attributeValueOf(node, name) {
9379
9422
  }
9380
9423
  return null;
9381
9424
  }
9382
- function literalStringOf(value) {
9425
+ function literalStringOf2(value) {
9383
9426
  const unwrapped2 = import_ts_morph43.Node.isJsxExpression(value) ? value.getExpression() ?? value : value;
9384
9427
  if (import_ts_morph43.Node.isStringLiteral(unwrapped2) || import_ts_morph43.Node.isNoSubstitutionTemplateLiteral(unwrapped2)) {
9385
9428
  return unwrapped2.getLiteralValue();
@@ -9389,7 +9432,7 @@ function literalStringOf(value) {
9389
9432
  function ownPathOfJsx(node, match) {
9390
9433
  const pathValue = attributeValueOf(node, match.pathAttribute);
9391
9434
  if (pathValue !== null) {
9392
- const literal = literalStringOf(pathValue);
9435
+ const literal = literalStringOf2(pathValue);
9393
9436
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9394
9437
  }
9395
9438
  if (match.indexAttribute !== void 0 && attributeValueOf(node, match.indexAttribute) !== null) {
@@ -9480,14 +9523,14 @@ function unitForJsxRoute(node, match, routeNames, kind, resolution) {
9480
9523
  }
9481
9524
  return routeUnit(node, kind, match.method, resolved.path, target, resolution);
9482
9525
  }
9483
- function routeUnit(at, kind, method, path12, target, resolution) {
9484
- const routeInfo = { method, path: path12 };
9526
+ function routeUnit(at, kind, method, path13, target, resolution) {
9527
+ const routeInfo = { method, path: path13 };
9485
9528
  if (target.kind === "unreadable") {
9486
9529
  return {
9487
9530
  func: null,
9488
9531
  announcedAt: at,
9489
9532
  kind,
9490
- name: path12,
9533
+ name: path13,
9491
9534
  nameKind: "label",
9492
9535
  routeInfo
9493
9536
  };
@@ -9657,7 +9700,7 @@ function unitsUnderRouteObject(object, match, kind, resolution, prefix, ancestry
9657
9700
  function ownPathOfObject(object, match) {
9658
9701
  const pathValue = routePropertyOf(object, match.pathAttribute);
9659
9702
  if (pathValue !== null) {
9660
- const literal = literalStringOf(pathValue);
9703
+ const literal = literalStringOf2(pathValue);
9661
9704
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9662
9705
  }
9663
9706
  if (match.indexAttribute !== void 0) {
@@ -9917,6 +9960,9 @@ function resolvePackageExportsCached(packageJsonPath) {
9917
9960
  return fresh;
9918
9961
  }
9919
9962
  function discoverPackageExports(sourceFile, match, kind) {
9963
+ if (match.packageJsonPath === void 0) {
9964
+ return [];
9965
+ }
9920
9966
  const { entries } = resolvePackageExportsCached(match.packageJsonPath);
9921
9967
  const filePath = sourceFile.getFilePath();
9922
9968
  const matching = [];
@@ -10304,12 +10350,12 @@ function originEndOf(flattened) {
10304
10350
  const slash = flattened.indexOf("/", opener[0].length);
10305
10351
  return slash === -1 ? flattened.length : slash;
10306
10352
  }
10307
- function appendPathText(path12, text) {
10353
+ function appendPathText(path13, text) {
10308
10354
  const idx = text.search(/[?#]/);
10309
10355
  if (idx === -1) {
10310
- return { path: path12 + text, stop: false };
10356
+ return { path: path13 + text, stop: false };
10311
10357
  }
10312
- return { path: path12 + text.slice(0, idx), stop: true };
10358
+ return { path: path13 + text.slice(0, idx), stop: true };
10313
10359
  }
10314
10360
  function parseAbsoluteUrl(text) {
10315
10361
  try {
@@ -10333,8 +10379,8 @@ function pathnameOfAbsoluteLiteral(text) {
10333
10379
  return stripQueryAndFragment(stripOriginManually(text));
10334
10380
  }
10335
10381
  function pathFromLiteralUrl(text) {
10336
- const path12 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10337
- return path12 === "" ? void 0 : path12;
10382
+ const path13 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10383
+ return path13 === "" ? void 0 : path13;
10338
10384
  }
10339
10385
  function pathFromTemplateUrl(arg, resolution) {
10340
10386
  const headText = arg.getHead().getLiteralText();
@@ -10342,7 +10388,7 @@ function pathFromTemplateUrl(arg, resolution) {
10342
10388
  const tails = spans.map((span) => span.getLiteral().getLiteralText());
10343
10389
  const originEnd = originEndOf([headText, ...tails].join(SUBSTITUTION));
10344
10390
  const head = appendPathText("", headText.slice(originEnd));
10345
- let path12 = head.path;
10391
+ let path13 = head.path;
10346
10392
  let stop = head.stop;
10347
10393
  let at = headText.length;
10348
10394
  for (let i = 0; i < spans.length && !stop; i++) {
@@ -10352,18 +10398,18 @@ function pathFromTemplateUrl(arg, resolution) {
10352
10398
  continue;
10353
10399
  }
10354
10400
  if (at >= originEnd) {
10355
- path12 += holeText(span.getExpression(), resolution);
10401
+ path13 += holeText(span.getExpression(), resolution);
10356
10402
  }
10357
10403
  at += SUBSTITUTION.length;
10358
10404
  const appended = appendPathText(
10359
- path12,
10405
+ path13,
10360
10406
  tailText.slice(Math.max(0, originEnd - at))
10361
10407
  );
10362
- path12 = appended.path;
10408
+ path13 = appended.path;
10363
10409
  stop = appended.stop;
10364
10410
  at += tailText.length;
10365
10411
  }
10366
- return path12 === "" ? void 0 : path12;
10412
+ return path13 === "" ? void 0 : path13;
10367
10413
  }
10368
10414
  function pathFromUrlNode(node, resolution) {
10369
10415
  if (import_ts_morph48.Node.isStringLiteral(node)) {
@@ -10530,9 +10576,9 @@ function withMountPrefix(routeInfo, subjectNode, mountPrefixes) {
10530
10576
  const prefix = mountPrefixes.effectivePrefixFor(subjectNode);
10531
10577
  return prefix === "" ? routeInfo : { ...routeInfo, path: joinMountedPath(prefix, routeInfo.path) };
10532
10578
  }
10533
- function joinMountedPath(prefix, path12) {
10579
+ function joinMountedPath(prefix, path13) {
10534
10580
  const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
10535
- return trimmed === "" ? path12 : trimmed + path12;
10581
+ return trimmed === "" ? path13 : trimmed + path13;
10536
10582
  }
10537
10583
  function registrationSubjectsOf(sourceFile, importModule, importName) {
10538
10584
  let importedLocalName = null;
@@ -10696,11 +10742,11 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10696
10742
  return null;
10697
10743
  }
10698
10744
  const method2 = stringProperty(routeObject, binding.method.property);
10699
- const path13 = stringProperty(routeObject, binding.path.property);
10700
- if (method2 === null || path13 === null) {
10745
+ const path14 = stringProperty(routeObject, binding.path.property);
10746
+ if (method2 === null || path14 === null) {
10701
10747
  return null;
10702
10748
  }
10703
- return { method: method2.toUpperCase(), path: path13 };
10749
+ return { method: method2.toUpperCase(), path: path14 };
10704
10750
  }
10705
10751
  if (binding.method.type !== "fromRegistration" || binding.path.type !== "fromArgument") {
10706
10752
  return null;
@@ -10717,8 +10763,8 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10717
10763
  method = arg.getLiteralValue().toUpperCase();
10718
10764
  }
10719
10765
  const pathArg = call.getArguments()[binding.path.position];
10720
- const path12 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10721
- return path12 === void 0 ? null : { method, path: path12 };
10766
+ const path13 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10767
+ return path13 === void 0 ? null : { method, path: path13 };
10722
10768
  }
10723
10769
 
10724
10770
  // src/discovery/registrationLoop.ts
@@ -10841,7 +10887,7 @@ function bodyCallsOn(loop, routables) {
10841
10887
  }
10842
10888
  function readRouteSpec(element, shape, resolution) {
10843
10889
  let method = null;
10844
- let path12 = null;
10890
+ let path13 = null;
10845
10891
  let handler = null;
10846
10892
  for (const prop of element.getProperties()) {
10847
10893
  if (import_ts_morph50.Node.isMethodDeclaration(prop) && prop.getName() === shape.handlerKey) {
@@ -10859,16 +10905,16 @@ function readRouteSpec(element, shape, resolution) {
10859
10905
  if (name === shape.methodKey) {
10860
10906
  method = readStringLiteralValue(init);
10861
10907
  } else if (name === shape.pathKey) {
10862
- path12 = readStringLiteralValue(init);
10908
+ path13 = readStringLiteralValue(init);
10863
10909
  } else if (name === shape.handlerKey) {
10864
10910
  const func = functionValueOf(init, resolution);
10865
10911
  handler = func === null ? null : { func, name: handlerName(init) };
10866
10912
  }
10867
10913
  }
10868
- if (method === null || path12 === null || handler === null) {
10914
+ if (method === null || path13 === null || handler === null) {
10869
10915
  return null;
10870
10916
  }
10871
- return { method, path: path12, handler: handler.func, handlerName: handler.name };
10917
+ return { method, path: path13, handler: handler.func, handlerName: handler.name };
10872
10918
  }
10873
10919
  function readStringLiteralValue(node) {
10874
10920
  if (import_ts_morph50.Node.isStringLiteral(node) || import_ts_morph50.Node.isNoSubstitutionTemplateLiteral(node)) {
@@ -10897,8 +10943,8 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10897
10943
  }
10898
10944
  const args = node.getArguments();
10899
10945
  for (const reg of match.registrations) {
10900
- const path12 = substitutePath(reg.pathTemplate, args);
10901
- if (path12 === null) {
10946
+ const path13 = substitutePath(reg.pathTemplate, args);
10947
+ if (path13 === null) {
10902
10948
  continue;
10903
10949
  }
10904
10950
  const handler = resolveHandler(reg.handlerArg, args, resolution);
@@ -10911,7 +10957,7 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10911
10957
  name: handler.name,
10912
10958
  routeInfo: {
10913
10959
  method: reg.method.toUpperCase(),
10914
- path: path12
10960
+ path: path13
10915
10961
  }
10916
10962
  });
10917
10963
  }
@@ -11237,7 +11283,8 @@ function runPattern(sourceFile, pattern, resolution, mountPrefixes) {
11237
11283
  sourceFile,
11238
11284
  pattern.match,
11239
11285
  pattern.kind,
11240
- resolution
11286
+ resolution,
11287
+ pattern.binding
11241
11288
  );
11242
11289
  }
11243
11290
  if (pattern.match.type === "jsxElementRoute") {
@@ -12437,19 +12484,35 @@ function resolveCallee(call, calleeName, scan) {
12437
12484
  }
12438
12485
  };
12439
12486
  }
12487
+ function resolveJsxReference(node) {
12488
+ if (!import_ts_morph56.Node.isJsxOpeningElement(node) && !import_ts_morph56.Node.isJsxSelfClosingElement(node)) {
12489
+ return null;
12490
+ }
12491
+ const tag = node.getTagNameNode();
12492
+ if (!import_ts_morph56.Node.isIdentifier(tag) || !/^[A-Z]/.test(tag.getText())) {
12493
+ return null;
12494
+ }
12495
+ const name = tag.getText();
12496
+ const declarations = declarationsBehind(tag.getSymbol());
12497
+ for (const decl of declarations) {
12498
+ const resolved = resolveDecl(decl, name);
12499
+ if (resolved !== null) {
12500
+ return { tag: name, outcome: { kind: "followed", candidate: resolved } };
12501
+ }
12502
+ }
12503
+ return {
12504
+ tag: name,
12505
+ outcome: {
12506
+ kind: "stopped",
12507
+ stop: { callee: name, reason: classifyStop(declarations) }
12508
+ }
12509
+ };
12510
+ }
12440
12511
  function collectReachable(func, scan) {
12441
12512
  const candidates = [];
12442
12513
  const stops = [];
12443
12514
  const seen = /* @__PURE__ */ new Set();
12444
- func.forEachDescendant((node) => {
12445
- if (!import_ts_morph56.Node.isCallExpression(node)) {
12446
- return;
12447
- }
12448
- const calleeText = normalizeCallee(node.getExpression().getText());
12449
- const outcome = resolveCallee(node, calleeText, scan);
12450
- if (outcome === null) {
12451
- return;
12452
- }
12515
+ const record = (outcome) => {
12453
12516
  if (outcome.kind === "stopped") {
12454
12517
  const stopKey = `${outcome.stop.reason}:${outcome.stop.callee}`;
12455
12518
  if (!seen.has(stopKey) && worthRecording(outcome.stop.reason)) {
@@ -12464,6 +12527,22 @@ function collectReachable(func, scan) {
12464
12527
  }
12465
12528
  seen.add(key);
12466
12529
  candidates.push(outcome.candidate);
12530
+ };
12531
+ func.forEachDescendant((node) => {
12532
+ const jsx = resolveJsxReference(node);
12533
+ if (jsx !== null) {
12534
+ record(jsx.outcome);
12535
+ return;
12536
+ }
12537
+ if (!import_ts_morph56.Node.isCallExpression(node)) {
12538
+ return;
12539
+ }
12540
+ const calleeText = normalizeCallee(node.getExpression().getText());
12541
+ const outcome = resolveCallee(node, calleeText, scan);
12542
+ if (outcome === null) {
12543
+ return;
12544
+ }
12545
+ record(outcome);
12467
12546
  });
12468
12547
  return { candidates, stops };
12469
12548
  }
@@ -12502,7 +12581,38 @@ var REACHABLE_RULES = [
12502
12581
  )
12503
12582
  ];
12504
12583
  var NO_RECOGNIZERS = { invocation: [], access: [] };
12505
- function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = []) {
12584
+ function recognizerOnlyRoots(packsByFile) {
12585
+ const roots = [];
12586
+ for (const [sourceFile, packs] of packsByFile) {
12587
+ const recognizerOnly = packs.some(
12588
+ (pack) => pack.discovery.length === 0 && pack.discoverUnits === void 0 && ((pack.invocationRecognizers?.length ?? 0) > 0 || (pack.accessRecognizers?.length ?? 0) > 0)
12589
+ );
12590
+ if (!recognizerOnly) {
12591
+ continue;
12592
+ }
12593
+ for (const declaration of sourceFile.getFunctions()) {
12594
+ if (declaration.isExported() && hasBody(declaration)) {
12595
+ roots.push({
12596
+ func: declaration,
12597
+ name: declaration.getName() ?? "<anon>"
12598
+ });
12599
+ }
12600
+ }
12601
+ for (const variableStatement of sourceFile.getVariableStatements()) {
12602
+ if (!variableStatement.isExported()) {
12603
+ continue;
12604
+ }
12605
+ for (const declaration of variableStatement.getDeclarations()) {
12606
+ const init = declaration.getInitializer();
12607
+ if (init !== void 0 && (import_ts_morph56.Node.isArrowFunction(init) || import_ts_morph56.Node.isFunctionExpression(init)) && hasBody(init)) {
12608
+ roots.push({ func: init, name: declaration.getName() });
12609
+ }
12610
+ }
12611
+ }
12612
+ }
12613
+ return roots;
12614
+ }
12615
+ function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = [], extraRoots = []) {
12506
12616
  const lookup = createSourceFileLookup(project);
12507
12617
  const db = facts?.db ?? new import_datalog4.Database();
12508
12618
  const functionByKey = /* @__PURE__ */ new Map();
@@ -12522,6 +12632,13 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
12522
12632
  db.add("entry", [key]);
12523
12633
  }
12524
12634
  }
12635
+ for (const root of extraRoots) {
12636
+ const key = nodeKey2(root.func);
12637
+ if (!functionByKey.has(key)) {
12638
+ functionByKey.set(key, root);
12639
+ }
12640
+ db.add("entry", [key]);
12641
+ }
12525
12642
  const knownKeys = /* @__PURE__ */ new Set();
12526
12643
  for (const summary of alreadySummarized) {
12527
12644
  const func = lookup.functionAt(summary.location);
@@ -13583,6 +13700,185 @@ function computeAdapterPacksDigest(packVersions) {
13583
13700
  return `${adapterStamp}|${sortedPacks.join(",")}`;
13584
13701
  }
13585
13702
 
13703
+ // src/workspacePatterns.ts
13704
+ var import_node_crypto4 = require("crypto");
13705
+ var import_node_fs6 = __toESM(require("fs"), 1);
13706
+ var import_node_path9 = __toESM(require("path"), 1);
13707
+ function readJson(filePath) {
13708
+ try {
13709
+ return JSON.parse(import_node_fs6.default.readFileSync(filePath, "utf8"));
13710
+ } catch {
13711
+ return void 0;
13712
+ }
13713
+ }
13714
+ function pnpmWorkspaceGlobs(yamlText) {
13715
+ const globs = [];
13716
+ let inPackages = false;
13717
+ for (const line of yamlText.split("\n")) {
13718
+ if (/^packages\s*:/.test(line)) {
13719
+ inPackages = true;
13720
+ continue;
13721
+ }
13722
+ if (inPackages) {
13723
+ const item = line.match(/^\s+-\s+["']?([^"'#\s]+)["']?/);
13724
+ if (item !== null) {
13725
+ globs.push(item[1]);
13726
+ continue;
13727
+ }
13728
+ if (/^\S/.test(line)) {
13729
+ inPackages = false;
13730
+ }
13731
+ }
13732
+ }
13733
+ return globs;
13734
+ }
13735
+ function workspaceGlobsAt(dir) {
13736
+ const pkg = readJson(import_node_path9.default.join(dir, "package.json"));
13737
+ if (typeof pkg === "object" && pkg !== null && "workspaces" in pkg) {
13738
+ const ws = pkg.workspaces;
13739
+ if (Array.isArray(ws)) {
13740
+ return ws.filter((g) => typeof g === "string");
13741
+ }
13742
+ if (typeof ws === "object" && ws !== null && Array.isArray(ws.packages)) {
13743
+ return ws.packages.filter(
13744
+ (g) => typeof g === "string"
13745
+ );
13746
+ }
13747
+ }
13748
+ const pnpmPath = import_node_path9.default.join(dir, "pnpm-workspace.yaml");
13749
+ if (import_node_fs6.default.existsSync(pnpmPath)) {
13750
+ try {
13751
+ return pnpmWorkspaceGlobs(import_node_fs6.default.readFileSync(pnpmPath, "utf8"));
13752
+ } catch {
13753
+ return null;
13754
+ }
13755
+ }
13756
+ return null;
13757
+ }
13758
+ function isDirectory(candidate) {
13759
+ try {
13760
+ return import_node_fs6.default.statSync(candidate).isDirectory();
13761
+ } catch {
13762
+ return false;
13763
+ }
13764
+ }
13765
+ function subdirectoriesOf(dir) {
13766
+ try {
13767
+ return import_node_fs6.default.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && name !== "node_modules");
13768
+ } catch {
13769
+ return [];
13770
+ }
13771
+ }
13772
+ function segmentToRegExp(segment) {
13773
+ const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13774
+ return new RegExp(`^${escaped.replace(/\*/g, "[^/]*")}$`);
13775
+ }
13776
+ function directoriesMatchingGlob(root, glob) {
13777
+ let current2 = [root];
13778
+ for (const segment of glob.split("/").filter((s) => s.length > 0)) {
13779
+ const next = [];
13780
+ if (segment.includes("*")) {
13781
+ const pattern = segmentToRegExp(segment);
13782
+ for (const dir of current2) {
13783
+ for (const entry of subdirectoriesOf(dir)) {
13784
+ if (pattern.test(entry)) {
13785
+ next.push(import_node_path9.default.join(dir, entry));
13786
+ }
13787
+ }
13788
+ }
13789
+ } else {
13790
+ for (const dir of current2) {
13791
+ next.push(import_node_path9.default.join(dir, segment));
13792
+ }
13793
+ }
13794
+ current2 = next.filter(isDirectory);
13795
+ }
13796
+ return current2;
13797
+ }
13798
+ function packageAt(dir) {
13799
+ const packageJsonPath = import_node_path9.default.join(dir, "package.json");
13800
+ const pkg = readJson(packageJsonPath);
13801
+ if (typeof pkg === "object" && pkg !== null && typeof pkg.name === "string") {
13802
+ return { name: pkg.name, packageJsonPath };
13803
+ }
13804
+ return null;
13805
+ }
13806
+ function workspacePackagesFrom(anchor) {
13807
+ let dir = import_node_path9.default.resolve(anchor);
13808
+ for (; ; ) {
13809
+ const globs = workspaceGlobsAt(dir);
13810
+ if (globs !== null) {
13811
+ const found = /* @__PURE__ */ new Map();
13812
+ for (const glob of globs) {
13813
+ if (glob.startsWith("!")) {
13814
+ continue;
13815
+ }
13816
+ for (const match of directoriesMatchingGlob(dir, glob)) {
13817
+ const pkg = packageAt(match);
13818
+ if (pkg !== null) {
13819
+ found.set(pkg.packageJsonPath, pkg);
13820
+ }
13821
+ }
13822
+ }
13823
+ return [...found.values()].sort(
13824
+ (a, b) => a.packageJsonPath.localeCompare(b.packageJsonPath)
13825
+ );
13826
+ }
13827
+ const parent = import_node_path9.default.dirname(dir);
13828
+ if (parent === dir) {
13829
+ return [];
13830
+ }
13831
+ dir = parent;
13832
+ }
13833
+ }
13834
+ function isWorkspaceMarked(pattern) {
13835
+ const match = pattern.match;
13836
+ return (match.type === "packageExports" || match.type === "packageImport") && match.workspaces === true;
13837
+ }
13838
+ function expandPattern(pattern, packages) {
13839
+ const match = pattern.match;
13840
+ if (match.type === "packageExports" && match.workspaces === true) {
13841
+ const { workspaces: _workspaces, ...rest } = match;
13842
+ return packages.map((pkg) => ({
13843
+ ...pattern,
13844
+ match: { ...rest, packageJsonPath: pkg.packageJsonPath }
13845
+ }));
13846
+ }
13847
+ if (match.type === "packageImport" && match.workspaces === true) {
13848
+ const listed = match.packages ?? [];
13849
+ const names = [.../* @__PURE__ */ new Set([...listed, ...packages.map((p) => p.name)])];
13850
+ if (names.length === 0) {
13851
+ return [];
13852
+ }
13853
+ return [{ ...pattern, match: { type: "packageImport", packages: names } }];
13854
+ }
13855
+ return [pattern];
13856
+ }
13857
+ function workspaceExpansionStamp(frameworks) {
13858
+ const paths = frameworks.flatMap((pack) => pack.discovery).flatMap(
13859
+ (pattern) => pattern.match.type === "packageExports" && pattern.match.packageJsonPath !== void 0 ? [pattern.match.packageJsonPath] : []
13860
+ ).sort();
13861
+ if (paths.length === 0) {
13862
+ return "none";
13863
+ }
13864
+ return (0, import_node_crypto4.createHash)("sha256").update(paths.join("\n")).digest("hex").slice(0, 12);
13865
+ }
13866
+ function expandWorkspacePatterns(frameworks, anchor) {
13867
+ const marked = frameworks.some(
13868
+ (pack) => pack.discovery.some(isWorkspaceMarked)
13869
+ );
13870
+ if (!marked) {
13871
+ return frameworks;
13872
+ }
13873
+ const packages = anchor !== void 0 ? workspacePackagesFrom(anchor) : [];
13874
+ return frameworks.map((pack) => ({
13875
+ ...pack,
13876
+ discovery: pack.discovery.flatMap(
13877
+ (pattern) => expandPattern(pattern, packages)
13878
+ )
13879
+ }));
13880
+ }
13881
+
13586
13882
  // src/adapter.ts
13587
13883
  var raise = (msg) => {
13588
13884
  throw new Error(msg);
@@ -13993,11 +14289,11 @@ function extractConsumerBinding(unit, pattern, pack, resolution) {
13993
14289
  return null;
13994
14290
  }
13995
14291
  const method = extractBindingMethod(binding, callSite, pack, resolution);
13996
- const path12 = extractBindingPath(binding, callSite, pack, resolution);
14292
+ const path13 = extractBindingPath(binding, callSite, pack, resolution);
13997
14293
  return (0, import_behavioral_ir8.restBinding)({
13998
14294
  transport: pack.protocol,
13999
14295
  method: method ?? null,
14000
- path: path12 ?? null,
14296
+ path: path13 ?? null,
14001
14297
  recognition: pack.name
14002
14298
  });
14003
14299
  }
@@ -14122,8 +14418,8 @@ function collectAccessRecognizers(frameworks, tallies) {
14122
14418
  }
14123
14419
  return out;
14124
14420
  }
14125
- function computeSubUnitBarriers(unit, pack, ctx, tally, file) {
14126
- if (pack.subUnits === void 0 || unit.func === null) {
14421
+ function computeSubUnitBarriers(unit, packs, ctx, tallies, file) {
14422
+ if (unit.func === null) {
14127
14423
  return NO_BARRIERS;
14128
14424
  }
14129
14425
  const parentHandle = {
@@ -14131,27 +14427,27 @@ function computeSubUnitBarriers(unit, pack, ctx, tally, file) {
14131
14427
  name: unit.name,
14132
14428
  kind: unit.kind
14133
14429
  };
14134
- try {
14135
- const subUnits = pack.subUnits(parentHandle, ctx);
14136
- if (subUnits.length === 0) {
14137
- return NO_BARRIERS;
14430
+ const barriers = /* @__PURE__ */ new Set();
14431
+ for (const pack of packs) {
14432
+ if (pack.subUnits === void 0) {
14433
+ continue;
14138
14434
  }
14139
- const barriers = /* @__PURE__ */ new Set();
14140
- for (const su of subUnits) {
14141
- barriers.add(su.func);
14435
+ try {
14436
+ for (const su of pack.subUnits(parentHandle, ctx)) {
14437
+ barriers.add(su.func);
14438
+ }
14439
+ } catch (err) {
14440
+ process.stderr.write(
14441
+ recordPackFailure(tallies?.get(pack.name), {
14442
+ pack: pack.name,
14443
+ hook: "subUnits",
14444
+ file,
14445
+ error: err
14446
+ })
14447
+ );
14142
14448
  }
14143
- return barriers;
14144
- } catch (err) {
14145
- process.stderr.write(
14146
- recordPackFailure(tally, {
14147
- pack: pack.name,
14148
- hook: "subUnits",
14149
- file,
14150
- error: err
14151
- })
14152
- );
14153
- return NO_BARRIERS;
14154
14449
  }
14450
+ return barriers.size === 0 ? NO_BARRIERS : barriers;
14155
14451
  }
14156
14452
  function extractFromSourceFile(sourceFile, frameworks, claimedUnits, options, tallies, resolution, mountPrefixes) {
14157
14453
  const summaries = [];
@@ -14233,9 +14529,9 @@ function extractFromSourceFile(sourceFile, frameworks, claimedUnits, options, ta
14233
14529
  }
14234
14530
  const barriers = computeSubUnitBarriers(
14235
14531
  unit,
14236
- pack,
14532
+ frameworks,
14237
14533
  subUnitCtx,
14238
- tally,
14534
+ tallies,
14239
14535
  sourceFile.getFilePath()
14240
14536
  );
14241
14537
  const raw = extractCodeStructure(
@@ -14609,8 +14905,8 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14609
14905
  if (pathArg === void 0) {
14610
14906
  continue;
14611
14907
  }
14612
- const path12 = pathFromArgument(pathArg, resolution);
14613
- if (path12 === void 0) {
14908
+ const path13 = pathFromArgument(pathArg, resolution);
14909
+ if (path13 === void 0) {
14614
14910
  continue;
14615
14911
  }
14616
14912
  const callerFunc = enclosingFunction(callExpr);
@@ -14622,7 +14918,7 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14622
14918
  continue;
14623
14919
  }
14624
14920
  seen.add(dedupKey);
14625
- out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path12, options));
14921
+ out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path13, options));
14626
14922
  }
14627
14923
  return out;
14628
14924
  }
@@ -14679,7 +14975,7 @@ function callerName(func) {
14679
14975
  }
14680
14976
  return "anonymous";
14681
14977
  }
14682
- function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14978
+ function buildCallerSummary(wrapper, callerFunc, callExpr, path13, options) {
14683
14979
  const wrapperBinding = wrapper.summary.identity.boundaryBinding;
14684
14980
  const wrapperRest = wrapperBinding?.semantics.name === "rest" ? wrapperBinding.semantics : null;
14685
14981
  const syntheticPack = {
@@ -14711,7 +15007,7 @@ function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14711
15007
  raw.boundaryBinding = (0, import_behavioral_ir8.restBinding)({
14712
15008
  transport: wrapperBinding?.transport ?? "http",
14713
15009
  method: wrapperRest?.method ?? null,
14714
- path: path12,
15010
+ path: path13,
14715
15011
  recognition: wrapperBinding?.recognition ?? "unknown"
14716
15012
  });
14717
15013
  const summary = (0, import_extractor5.assembleSummary)(raw, options);
@@ -14777,25 +15073,32 @@ function loadRunFiles(project, candidatePaths, timer) {
14777
15073
  );
14778
15074
  return { sourceFiles, deep };
14779
15075
  }
14780
- function createTypeScriptAdapter(config) {
15076
+ function createTypeScriptAdapter(suppliedConfig) {
15077
+ const config = {
15078
+ ...suppliedConfig,
15079
+ frameworks: expandWorkspacePatterns(
15080
+ suppliedConfig.frameworks,
15081
+ suppliedConfig.projectRoot ?? (suppliedConfig.tsConfigFilePath !== void 0 ? import_node_path10.default.dirname(suppliedConfig.tsConfigFilePath) : void 0)
15082
+ )
15083
+ };
14781
15084
  const project = config.project ?? new import_ts_morph60.Project(
14782
15085
  config.tsConfigFilePath !== void 0 ? {
14783
15086
  tsConfigFilePath: config.tsConfigFilePath,
14784
15087
  skipAddingFilesFromTsConfig: true
14785
15088
  } : { skipAddingFilesFromTsConfig: true }
14786
15089
  );
14787
- const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(import_node_path9.default.dirname(config.tsConfigFilePath)) : void 0);
15090
+ const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(import_node_path10.default.dirname(config.tsConfigFilePath)) : void 0);
14788
15091
  let lazyBootstrapped = false;
14789
15092
  let projectFileSet;
14790
15093
  const cacheDir = declineWhenRunFromSource(
14791
- config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? import_node_path9.default.join(import_node_path9.default.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
15094
+ config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? import_node_path10.default.join(import_node_path10.default.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
14792
15095
  );
14793
15096
  const cache = createCacheLayer(cacheDir);
14794
15097
  const adapterPacksDigest = `${computeAdapterPacksDigest(
14795
15098
  config.frameworks.map(
14796
15099
  (p) => p.version !== void 0 ? { name: p.name, version: p.version } : { name: p.name }
14797
15100
  )
14798
- )}|${extractionConfigStamp(config)}`;
15101
+ )}|${extractionConfigStamp(config)}|ws:${workspaceExpansionStamp(config.frameworks)}`;
14799
15102
  const packWrappers = config.frameworks.flatMap(
14800
15103
  (pack) => pack.transparentWrappers ?? []
14801
15104
  );
@@ -14997,6 +15300,7 @@ function createTypeScriptAdapter(config) {
14997
15300
  pipeline,
14998
15301
  project,
14999
15302
  config.frameworks,
15303
+ packsByFile,
15000
15304
  config.extractorOptions,
15001
15305
  tallies,
15002
15306
  (created, parent) => {
@@ -15031,7 +15335,12 @@ function createTypeScriptAdapter(config) {
15031
15335
  },
15032
15336
  // Reached units the cache already serves emit nothing,
15033
15337
  // the way a cold run's seeds do not.
15034
- reused?.summaries ?? []
15338
+ reused?.summaries ?? [],
15339
+ // A recognizer-only pack's effects need a function to
15340
+ // live on even when nothing discovers units in its
15341
+ // files, so those files' exports join the walk as
15342
+ // roots.
15343
+ recognizerOnlyRoots(packsByFile)
15035
15344
  )
15036
15345
  ) : withSubUnits;
15037
15346
  const closureOwnership = caching && config.includeReachable !== false ? timer.time(
@@ -15332,11 +15641,15 @@ function buildCacheAttribution(args) {
15332
15641
  ];
15333
15642
  return { roots, owners };
15334
15643
  }
15335
- function synthesizeSubUnits(summaries, project, frameworks, options, tallies, onCreated) {
15644
+ function synthesizeSubUnits(summaries, project, frameworks, packsByFile, options, tallies, onCreated) {
15336
15645
  const packByRecognition = /* @__PURE__ */ new Map();
15337
15646
  for (const pack of frameworks) {
15338
15647
  packByRecognition.set(pack.name, pack);
15339
15648
  }
15649
+ const packsByPath = /* @__PURE__ */ new Map();
15650
+ for (const [sf, packs] of packsByFile) {
15651
+ packsByPath.set(sf.getFilePath(), packs);
15652
+ }
15340
15653
  const allInvocationRecognizers = collectInvocationRecognizers(
15341
15654
  frameworks,
15342
15655
  tallies
@@ -15350,8 +15663,15 @@ function synthesizeSubUnits(summaries, project, frameworks, options, tallies, on
15350
15663
  if (binding === null) {
15351
15664
  continue;
15352
15665
  }
15353
- const pack = packByRecognition.get(binding.recognition);
15354
- if (pack?.subUnits === void 0) {
15666
+ const discovering = packByRecognition.get(binding.recognition);
15667
+ const applicable = packsByPath.get(parent.location.file) ?? (discovering !== void 0 ? [discovering] : []);
15668
+ const hooked = [
15669
+ ...discovering?.subUnits !== void 0 ? [discovering] : [],
15670
+ ...applicable.filter(
15671
+ (one) => one.subUnits !== void 0 && one !== discovering
15672
+ )
15673
+ ];
15674
+ if (hooked.length === 0) {
15355
15675
  continue;
15356
15676
  }
15357
15677
  lookup ??= createSourceFileLookup(project);
@@ -15364,7 +15684,17 @@ function synthesizeSubUnits(summaries, project, frameworks, options, tallies, on
15364
15684
  name: parent.identity.name,
15365
15685
  kind: parent.kind
15366
15686
  };
15367
- const subUnits = pack.subUnits(parentHandle, subUnitCtx);
15687
+ const claimedFuncs = /* @__PURE__ */ new Set();
15688
+ const subUnits = [];
15689
+ for (const pack of hooked) {
15690
+ for (const subUnit of pack.subUnits?.(parentHandle, subUnitCtx) ?? []) {
15691
+ if (claimedFuncs.has(subUnit.func)) {
15692
+ continue;
15693
+ }
15694
+ claimedFuncs.add(subUnit.func);
15695
+ subUnits.push(subUnit);
15696
+ }
15697
+ }
15368
15698
  for (const subUnit of subUnits) {
15369
15699
  const summary = buildSubUnitSummary(
15370
15700
  subUnit,
@@ -15428,8 +15758,8 @@ function buildSubUnitSummary(subUnit, parent, invocationRecognizers, accessRecog
15428
15758
  }
15429
15759
 
15430
15760
  // src/bootstrap/noTsconfigProject.ts
15431
- var import_node_fs6 = __toESM(require("fs"), 1);
15432
- var import_node_path10 = __toESM(require("path"), 1);
15761
+ var import_node_fs7 = __toESM(require("fs"), 1);
15762
+ var import_node_path11 = __toESM(require("path"), 1);
15433
15763
  var import_ts_morph61 = require("ts-morph");
15434
15764
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
15435
15765
  ".ts",
@@ -15460,7 +15790,7 @@ function findSourceFiles(rootDir, depth = 0) {
15460
15790
  const found = [];
15461
15791
  let entries;
15462
15792
  try {
15463
- entries = import_node_fs6.default.readdirSync(rootDir, { withFileTypes: true });
15793
+ entries = import_node_fs7.default.readdirSync(rootDir, { withFileTypes: true });
15464
15794
  } catch {
15465
15795
  return [];
15466
15796
  }
@@ -15468,10 +15798,10 @@ function findSourceFiles(rootDir, depth = 0) {
15468
15798
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name)) {
15469
15799
  continue;
15470
15800
  }
15471
- const full = import_node_path10.default.join(rootDir, entry.name);
15801
+ const full = import_node_path11.default.join(rootDir, entry.name);
15472
15802
  if (entry.isDirectory()) {
15473
15803
  found.push(...findSourceFiles(full, depth + 1));
15474
- } else if (SOURCE_EXTENSIONS.has(import_node_path10.default.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15804
+ } else if (SOURCE_EXTENSIONS.has(import_node_path11.default.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15475
15805
  found.push(full);
15476
15806
  }
15477
15807
  }
@@ -15506,15 +15836,15 @@ function createProjectWithoutTsconfig(rootDir) {
15506
15836
  return { project, files };
15507
15837
  }
15508
15838
  function findNearestTsconfig(startDir) {
15509
- let dir = import_node_path10.default.resolve(startDir);
15839
+ let dir = import_node_path11.default.resolve(startDir);
15510
15840
  while (true) {
15511
15841
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
15512
- const candidate = import_node_path10.default.join(dir, name);
15513
- if (import_node_fs6.default.existsSync(candidate)) {
15842
+ const candidate = import_node_path11.default.join(dir, name);
15843
+ if (import_node_fs7.default.existsSync(candidate)) {
15514
15844
  return candidate;
15515
15845
  }
15516
15846
  }
15517
- const parent = import_node_path10.default.dirname(dir);
15847
+ const parent = import_node_path11.default.dirname(dir);
15518
15848
  if (parent === dir) {
15519
15849
  return null;
15520
15850
  }
@@ -15523,7 +15853,7 @@ function findNearestTsconfig(startDir) {
15523
15853
  }
15524
15854
 
15525
15855
  // src/facts/why.ts
15526
- var import_node_path11 = __toESM(require("path"), 1);
15856
+ var import_node_path12 = __toESM(require("path"), 1);
15527
15857
  var import_ts_morph62 = require("ts-morph");
15528
15858
  var import_resolution2 = require("@suss/resolution");
15529
15859
  var JS_PHRASES = {
@@ -15536,7 +15866,7 @@ var WhySession = class {
15536
15866
  store;
15537
15867
  root;
15538
15868
  constructor(options) {
15539
- this.root = import_node_path11.default.resolve(options.dir);
15869
+ this.root = import_node_path12.default.resolve(options.dir);
15540
15870
  const tsconfig = findNearestTsconfig(this.root);
15541
15871
  this.project = tsconfig === null ? createProjectWithoutTsconfig(this.root).project : new import_ts_morph62.Project({ tsConfigFilePath: tsconfig });
15542
15872
  this.store = new ResolutionStore();
@@ -15625,7 +15955,7 @@ var WhySession = class {
15625
15955
  };
15626
15956
  }
15627
15957
  sourceFileAt(file) {
15628
- const absolute = import_node_path11.default.resolve(this.root, file);
15958
+ const absolute = import_node_path12.default.resolve(this.root, file);
15629
15959
  const loaded = this.project.getSourceFile(absolute);
15630
15960
  if (loaded !== void 0) {
15631
15961
  return loaded;
@@ -15639,7 +15969,7 @@ var WhySession = class {
15639
15969
  /** A module key: a file path said relative to the root, or a package name. */
15640
15970
  displayPath(key) {
15641
15971
  if (key.startsWith(this.root)) {
15642
- return import_node_path11.default.relative(this.root, key);
15972
+ return import_node_path12.default.relative(this.root, key);
15643
15973
  }
15644
15974
  return key;
15645
15975
  }
@@ -15710,6 +16040,17 @@ function funnelDrops(packs) {
15710
16040
  }
15711
16041
  return violations;
15712
16042
  }
16043
+ function recognizersWithNoUnits(packs) {
16044
+ return packs.filter(
16045
+ (funnel) => funnel.recognizes && !funnel.discovers && funnel.gates.length > 0 && funnel.unresolvedGates.length === 0 && funnel.candidateFiles > 0 && funnel.unitsInGatedFiles === 0 && // The closure walks a recognizer-only pack's gated exports as
16046
+ // roots, so effects recognized there mean the run worked and
16047
+ // the missing framework pack costs attribution, not existence.
16048
+ funnel.effectsRecognized === 0
16049
+ ).map((funnel) => ({
16050
+ label: funnel.pack,
16051
+ detail: `${funnel.candidateFiles} gated files, and no pack in this run discovered a unit in them. ${funnel.pack} reads calls inside units another pack finds, so add the pack that finds this project's handlers (-f express, -f fastify, ...), or run suss init to work out which.`
16052
+ }));
16053
+ }
15713
16054
  function unversionedPacks(packs) {
15714
16055
  return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
15715
16056
  label: funnel.pack,
@@ -15804,6 +16145,12 @@ function evaluatePackHealth(report) {
15804
16145
  audience: "run",
15805
16146
  violations: selfCollisions(report.packs)
15806
16147
  },
16148
+ {
16149
+ name: "every recognizer had units to look inside",
16150
+ code: "no-units",
16151
+ audience: "run",
16152
+ violations: recognizersWithNoUnits(report.packs)
16153
+ },
15807
16154
  {
15808
16155
  name: "every pack declares a version",
15809
16156
  code: "no-version",