@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/adapter.ts
2
- import path9 from "path";
2
+ import path10 from "path";
3
3
  import {
4
4
  Node as Node58,
5
5
  Project,
@@ -1827,8 +1827,8 @@ function loadImportGraphsDepthFirstFromPaths(project, rootPaths) {
1827
1827
  stack.pop();
1828
1828
  loadOrder.push(top.path);
1829
1829
  let deepest = 0;
1830
- for (const path12 of top.deps) {
1831
- deepest = Math.max(deepest, chainDepth.get(path12) ?? 0);
1830
+ for (const path13 of top.deps) {
1831
+ deepest = Math.max(deepest, chainDepth.get(path13) ?? 0);
1832
1832
  }
1833
1833
  chainDepth.set(top.path, deepest + 1);
1834
1834
  }
@@ -2173,11 +2173,11 @@ function walkAndWarm(root, reach) {
2173
2173
  if (warmedFiles.get(target) !== void 0) {
2174
2174
  continue;
2175
2175
  }
2176
- const path12 = target.getFilePath();
2177
- if (visited.has(path12)) {
2176
+ const path13 = target.getFilePath();
2177
+ if (visited.has(path13)) {
2178
2178
  continue;
2179
2179
  }
2180
- visited.add(path12);
2180
+ visited.add(path13);
2181
2181
  stack.push(frameOf(target, reach));
2182
2182
  continue;
2183
2183
  }
@@ -2194,7 +2194,7 @@ function sitsOnWarmChains(file) {
2194
2194
  return false;
2195
2195
  }
2196
2196
  const imported = importedFilePathsOf(project, file.getFilePath());
2197
- return imported.length > 0 && imported.every((path12) => warmed.has(path12));
2197
+ return imported.length > 0 && imported.every((path13) => warmed.has(path13));
2198
2198
  }
2199
2199
  function markWarmed(file) {
2200
2200
  warmedFiles.set(file, true);
@@ -5818,9 +5818,9 @@ function createSourceFileLookup(project) {
5818
5818
  if (sf === null) {
5819
5819
  return null;
5820
5820
  }
5821
- const path12 = sf.getFilePath();
5822
- const index = functionsByFile.get(path12) ?? indexFunctions(sf);
5823
- functionsByFile.set(path12, index);
5821
+ const path13 = sf.getFilePath();
5822
+ const index = functionsByFile.get(path13) ?? indexFunctions(sf);
5823
+ functionsByFile.set(path13, index);
5824
5824
  return index.get(rangeKey(location.range)) ?? null;
5825
5825
  }
5826
5826
  };
@@ -6347,7 +6347,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6347
6347
  }
6348
6348
  const responses = [];
6349
6349
  let method;
6350
- let path12;
6350
+ let path13;
6351
6351
  for (const prop of endpointNode.getProperties()) {
6352
6352
  if (!Node27.isPropertyAssignment(prop)) {
6353
6353
  continue;
@@ -6362,7 +6362,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6362
6362
  if (propName === pattern.pathProperty) {
6363
6363
  const val = prop.getInitializer();
6364
6364
  if (val !== void 0 && Node27.isStringLiteral(val)) {
6365
- path12 = val.getLiteralValue();
6365
+ path13 = val.getLiteralValue();
6366
6366
  }
6367
6367
  }
6368
6368
  if (propName === pattern.responseExtraction.property) {
@@ -6386,10 +6386,10 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6386
6386
  if (responses.length === 0) {
6387
6387
  return null;
6388
6388
  }
6389
- const boundaryBinding = method !== void 0 || path12 !== void 0 ? restBinding({
6389
+ const boundaryBinding = method !== void 0 || path13 !== void 0 ? restBinding({
6390
6390
  transport: "http",
6391
6391
  method: method ?? null,
6392
- path: path12 ?? null,
6392
+ path: path13 ?? null,
6393
6393
  recognition: framework
6394
6394
  }) : null;
6395
6395
  return {
@@ -8311,7 +8311,34 @@ function joinRoutePath(prefix, suffix) {
8311
8311
  }
8312
8312
  return `/${segments.join("/")}`;
8313
8313
  }
8314
- function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8314
+ function declaredChannelOf(source, decorator, resolution) {
8315
+ if (source.from === "literal") {
8316
+ return source.value;
8317
+ }
8318
+ if (source.from === "unstated") {
8319
+ return null;
8320
+ }
8321
+ if (!Node36.isDecorator(decorator)) {
8322
+ return null;
8323
+ }
8324
+ const arg = decorator.getArguments()[source.position];
8325
+ if (arg === void 0) {
8326
+ return null;
8327
+ }
8328
+ const text = literalStringOf(arg, resolution);
8329
+ return text === null || text === "" ? null : text;
8330
+ }
8331
+ function literalStringOf(node, resolution) {
8332
+ if (Node36.isStringLiteral(node) || Node36.isNoSubstitutionTemplateLiteral(node)) {
8333
+ return node.getLiteralValue();
8334
+ }
8335
+ const written = resolution?.resolveWrittenValue(node) ?? null;
8336
+ if (written === null || written === node) {
8337
+ return null;
8338
+ }
8339
+ return literalStringOf(written, resolution);
8340
+ }
8341
+ function discoverDecoratedRoutes(sourceFile, match, kind, resolution, binding) {
8315
8342
  const acceptedModules = Array.isArray(match.importModule) ? match.importModule : [match.importModule];
8316
8343
  const routeDecoratorNames = Object.keys(match.methodDecoratorRouteMap);
8317
8344
  const importedRouteDecorators = /* @__PURE__ */ new Set();
@@ -8343,6 +8370,22 @@ function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8343
8370
  const pathPrefix = routePathOf(marker.args, resolution);
8344
8371
  const className = cls.getName() ?? "<anon-class>";
8345
8372
  for (const handler of decoratedCallablesOf(cls, routeDecoratorNames)) {
8373
+ if (binding !== void 0) {
8374
+ results.push({
8375
+ func: handler.func,
8376
+ kind,
8377
+ name: `${className}.${handler.name}`,
8378
+ channelInfo: {
8379
+ messageBus: binding.messageBus,
8380
+ channel: declaredChannelOf(
8381
+ binding.channel,
8382
+ handler.decorator,
8383
+ resolution
8384
+ )
8385
+ }
8386
+ });
8387
+ continue;
8388
+ }
8346
8389
  const httpMethod = match.methodDecoratorRouteMap[handler.standsFor];
8347
8390
  const pathSuffix = resolveRoutePathArg(handler.decorator, resolution);
8348
8391
  const routePath = joinRoutePath(pathPrefix, pathSuffix);
@@ -9362,7 +9405,7 @@ function attributeValueOf(node, name) {
9362
9405
  }
9363
9406
  return null;
9364
9407
  }
9365
- function literalStringOf(value) {
9408
+ function literalStringOf2(value) {
9366
9409
  const unwrapped2 = Node41.isJsxExpression(value) ? value.getExpression() ?? value : value;
9367
9410
  if (Node41.isStringLiteral(unwrapped2) || Node41.isNoSubstitutionTemplateLiteral(unwrapped2)) {
9368
9411
  return unwrapped2.getLiteralValue();
@@ -9372,7 +9415,7 @@ function literalStringOf(value) {
9372
9415
  function ownPathOfJsx(node, match) {
9373
9416
  const pathValue = attributeValueOf(node, match.pathAttribute);
9374
9417
  if (pathValue !== null) {
9375
- const literal = literalStringOf(pathValue);
9418
+ const literal = literalStringOf2(pathValue);
9376
9419
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9377
9420
  }
9378
9421
  if (match.indexAttribute !== void 0 && attributeValueOf(node, match.indexAttribute) !== null) {
@@ -9463,14 +9506,14 @@ function unitForJsxRoute(node, match, routeNames, kind, resolution) {
9463
9506
  }
9464
9507
  return routeUnit(node, kind, match.method, resolved.path, target, resolution);
9465
9508
  }
9466
- function routeUnit(at, kind, method, path12, target, resolution) {
9467
- const routeInfo = { method, path: path12 };
9509
+ function routeUnit(at, kind, method, path13, target, resolution) {
9510
+ const routeInfo = { method, path: path13 };
9468
9511
  if (target.kind === "unreadable") {
9469
9512
  return {
9470
9513
  func: null,
9471
9514
  announcedAt: at,
9472
9515
  kind,
9473
- name: path12,
9516
+ name: path13,
9474
9517
  nameKind: "label",
9475
9518
  routeInfo
9476
9519
  };
@@ -9640,7 +9683,7 @@ function unitsUnderRouteObject(object, match, kind, resolution, prefix, ancestry
9640
9683
  function ownPathOfObject(object, match) {
9641
9684
  const pathValue = routePropertyOf(object, match.pathAttribute);
9642
9685
  if (pathValue !== null) {
9643
- const literal = literalStringOf(pathValue);
9686
+ const literal = literalStringOf2(pathValue);
9644
9687
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9645
9688
  }
9646
9689
  if (match.indexAttribute !== void 0) {
@@ -9900,6 +9943,9 @@ function resolvePackageExportsCached(packageJsonPath) {
9900
9943
  return fresh;
9901
9944
  }
9902
9945
  function discoverPackageExports(sourceFile, match, kind) {
9946
+ if (match.packageJsonPath === void 0) {
9947
+ return [];
9948
+ }
9903
9949
  const { entries } = resolvePackageExportsCached(match.packageJsonPath);
9904
9950
  const filePath = sourceFile.getFilePath();
9905
9951
  const matching = [];
@@ -10287,12 +10333,12 @@ function originEndOf(flattened) {
10287
10333
  const slash = flattened.indexOf("/", opener[0].length);
10288
10334
  return slash === -1 ? flattened.length : slash;
10289
10335
  }
10290
- function appendPathText(path12, text) {
10336
+ function appendPathText(path13, text) {
10291
10337
  const idx = text.search(/[?#]/);
10292
10338
  if (idx === -1) {
10293
- return { path: path12 + text, stop: false };
10339
+ return { path: path13 + text, stop: false };
10294
10340
  }
10295
- return { path: path12 + text.slice(0, idx), stop: true };
10341
+ return { path: path13 + text.slice(0, idx), stop: true };
10296
10342
  }
10297
10343
  function parseAbsoluteUrl(text) {
10298
10344
  try {
@@ -10316,8 +10362,8 @@ function pathnameOfAbsoluteLiteral(text) {
10316
10362
  return stripQueryAndFragment(stripOriginManually(text));
10317
10363
  }
10318
10364
  function pathFromLiteralUrl(text) {
10319
- const path12 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10320
- return path12 === "" ? void 0 : path12;
10365
+ const path13 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10366
+ return path13 === "" ? void 0 : path13;
10321
10367
  }
10322
10368
  function pathFromTemplateUrl(arg, resolution) {
10323
10369
  const headText = arg.getHead().getLiteralText();
@@ -10325,7 +10371,7 @@ function pathFromTemplateUrl(arg, resolution) {
10325
10371
  const tails = spans.map((span) => span.getLiteral().getLiteralText());
10326
10372
  const originEnd = originEndOf([headText, ...tails].join(SUBSTITUTION));
10327
10373
  const head = appendPathText("", headText.slice(originEnd));
10328
- let path12 = head.path;
10374
+ let path13 = head.path;
10329
10375
  let stop = head.stop;
10330
10376
  let at = headText.length;
10331
10377
  for (let i = 0; i < spans.length && !stop; i++) {
@@ -10335,18 +10381,18 @@ function pathFromTemplateUrl(arg, resolution) {
10335
10381
  continue;
10336
10382
  }
10337
10383
  if (at >= originEnd) {
10338
- path12 += holeText(span.getExpression(), resolution);
10384
+ path13 += holeText(span.getExpression(), resolution);
10339
10385
  }
10340
10386
  at += SUBSTITUTION.length;
10341
10387
  const appended = appendPathText(
10342
- path12,
10388
+ path13,
10343
10389
  tailText.slice(Math.max(0, originEnd - at))
10344
10390
  );
10345
- path12 = appended.path;
10391
+ path13 = appended.path;
10346
10392
  stop = appended.stop;
10347
10393
  at += tailText.length;
10348
10394
  }
10349
- return path12 === "" ? void 0 : path12;
10395
+ return path13 === "" ? void 0 : path13;
10350
10396
  }
10351
10397
  function pathFromUrlNode(node, resolution) {
10352
10398
  if (Node46.isStringLiteral(node)) {
@@ -10513,9 +10559,9 @@ function withMountPrefix(routeInfo, subjectNode, mountPrefixes) {
10513
10559
  const prefix = mountPrefixes.effectivePrefixFor(subjectNode);
10514
10560
  return prefix === "" ? routeInfo : { ...routeInfo, path: joinMountedPath(prefix, routeInfo.path) };
10515
10561
  }
10516
- function joinMountedPath(prefix, path12) {
10562
+ function joinMountedPath(prefix, path13) {
10517
10563
  const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
10518
- return trimmed === "" ? path12 : trimmed + path12;
10564
+ return trimmed === "" ? path13 : trimmed + path13;
10519
10565
  }
10520
10566
  function registrationSubjectsOf(sourceFile, importModule, importName) {
10521
10567
  let importedLocalName = null;
@@ -10679,11 +10725,11 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10679
10725
  return null;
10680
10726
  }
10681
10727
  const method2 = stringProperty(routeObject, binding.method.property);
10682
- const path13 = stringProperty(routeObject, binding.path.property);
10683
- if (method2 === null || path13 === null) {
10728
+ const path14 = stringProperty(routeObject, binding.path.property);
10729
+ if (method2 === null || path14 === null) {
10684
10730
  return null;
10685
10731
  }
10686
- return { method: method2.toUpperCase(), path: path13 };
10732
+ return { method: method2.toUpperCase(), path: path14 };
10687
10733
  }
10688
10734
  if (binding.method.type !== "fromRegistration" || binding.path.type !== "fromArgument") {
10689
10735
  return null;
@@ -10700,8 +10746,8 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10700
10746
  method = arg.getLiteralValue().toUpperCase();
10701
10747
  }
10702
10748
  const pathArg = call.getArguments()[binding.path.position];
10703
- const path12 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10704
- return path12 === void 0 ? null : { method, path: path12 };
10749
+ const path13 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10750
+ return path13 === void 0 ? null : { method, path: path13 };
10705
10751
  }
10706
10752
 
10707
10753
  // src/discovery/registrationLoop.ts
@@ -10824,7 +10870,7 @@ function bodyCallsOn(loop, routables) {
10824
10870
  }
10825
10871
  function readRouteSpec(element, shape, resolution) {
10826
10872
  let method = null;
10827
- let path12 = null;
10873
+ let path13 = null;
10828
10874
  let handler = null;
10829
10875
  for (const prop of element.getProperties()) {
10830
10876
  if (Node48.isMethodDeclaration(prop) && prop.getName() === shape.handlerKey) {
@@ -10842,16 +10888,16 @@ function readRouteSpec(element, shape, resolution) {
10842
10888
  if (name === shape.methodKey) {
10843
10889
  method = readStringLiteralValue(init);
10844
10890
  } else if (name === shape.pathKey) {
10845
- path12 = readStringLiteralValue(init);
10891
+ path13 = readStringLiteralValue(init);
10846
10892
  } else if (name === shape.handlerKey) {
10847
10893
  const func = functionValueOf(init, resolution);
10848
10894
  handler = func === null ? null : { func, name: handlerName(init) };
10849
10895
  }
10850
10896
  }
10851
- if (method === null || path12 === null || handler === null) {
10897
+ if (method === null || path13 === null || handler === null) {
10852
10898
  return null;
10853
10899
  }
10854
- return { method, path: path12, handler: handler.func, handlerName: handler.name };
10900
+ return { method, path: path13, handler: handler.func, handlerName: handler.name };
10855
10901
  }
10856
10902
  function readStringLiteralValue(node) {
10857
10903
  if (Node48.isStringLiteral(node) || Node48.isNoSubstitutionTemplateLiteral(node)) {
@@ -10880,8 +10926,8 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10880
10926
  }
10881
10927
  const args = node.getArguments();
10882
10928
  for (const reg of match.registrations) {
10883
- const path12 = substitutePath(reg.pathTemplate, args);
10884
- if (path12 === null) {
10929
+ const path13 = substitutePath(reg.pathTemplate, args);
10930
+ if (path13 === null) {
10885
10931
  continue;
10886
10932
  }
10887
10933
  const handler = resolveHandler(reg.handlerArg, args, resolution);
@@ -10894,7 +10940,7 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10894
10940
  name: handler.name,
10895
10941
  routeInfo: {
10896
10942
  method: reg.method.toUpperCase(),
10897
- path: path12
10943
+ path: path13
10898
10944
  }
10899
10945
  });
10900
10946
  }
@@ -11220,7 +11266,8 @@ function runPattern(sourceFile, pattern, resolution, mountPrefixes) {
11220
11266
  sourceFile,
11221
11267
  pattern.match,
11222
11268
  pattern.kind,
11223
- resolution
11269
+ resolution,
11270
+ pattern.binding
11224
11271
  );
11225
11272
  }
11226
11273
  if (pattern.match.type === "jsxElementRoute") {
@@ -12443,19 +12490,35 @@ function resolveCallee(call, calleeName, scan) {
12443
12490
  }
12444
12491
  };
12445
12492
  }
12493
+ function resolveJsxReference(node) {
12494
+ if (!Node54.isJsxOpeningElement(node) && !Node54.isJsxSelfClosingElement(node)) {
12495
+ return null;
12496
+ }
12497
+ const tag = node.getTagNameNode();
12498
+ if (!Node54.isIdentifier(tag) || !/^[A-Z]/.test(tag.getText())) {
12499
+ return null;
12500
+ }
12501
+ const name = tag.getText();
12502
+ const declarations = declarationsBehind(tag.getSymbol());
12503
+ for (const decl of declarations) {
12504
+ const resolved = resolveDecl(decl, name);
12505
+ if (resolved !== null) {
12506
+ return { tag: name, outcome: { kind: "followed", candidate: resolved } };
12507
+ }
12508
+ }
12509
+ return {
12510
+ tag: name,
12511
+ outcome: {
12512
+ kind: "stopped",
12513
+ stop: { callee: name, reason: classifyStop(declarations) }
12514
+ }
12515
+ };
12516
+ }
12446
12517
  function collectReachable(func, scan) {
12447
12518
  const candidates = [];
12448
12519
  const stops = [];
12449
12520
  const seen = /* @__PURE__ */ new Set();
12450
- func.forEachDescendant((node) => {
12451
- if (!Node54.isCallExpression(node)) {
12452
- return;
12453
- }
12454
- const calleeText = normalizeCallee(node.getExpression().getText());
12455
- const outcome = resolveCallee(node, calleeText, scan);
12456
- if (outcome === null) {
12457
- return;
12458
- }
12521
+ const record = (outcome) => {
12459
12522
  if (outcome.kind === "stopped") {
12460
12523
  const stopKey = `${outcome.stop.reason}:${outcome.stop.callee}`;
12461
12524
  if (!seen.has(stopKey) && worthRecording(outcome.stop.reason)) {
@@ -12470,6 +12533,22 @@ function collectReachable(func, scan) {
12470
12533
  }
12471
12534
  seen.add(key);
12472
12535
  candidates.push(outcome.candidate);
12536
+ };
12537
+ func.forEachDescendant((node) => {
12538
+ const jsx = resolveJsxReference(node);
12539
+ if (jsx !== null) {
12540
+ record(jsx.outcome);
12541
+ return;
12542
+ }
12543
+ if (!Node54.isCallExpression(node)) {
12544
+ return;
12545
+ }
12546
+ const calleeText = normalizeCallee(node.getExpression().getText());
12547
+ const outcome = resolveCallee(node, calleeText, scan);
12548
+ if (outcome === null) {
12549
+ return;
12550
+ }
12551
+ record(outcome);
12473
12552
  });
12474
12553
  return { candidates, stops };
12475
12554
  }
@@ -12508,7 +12587,38 @@ var REACHABLE_RULES = [
12508
12587
  )
12509
12588
  ];
12510
12589
  var NO_RECOGNIZERS = { invocation: [], access: [] };
12511
- function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = []) {
12590
+ function recognizerOnlyRoots(packsByFile) {
12591
+ const roots = [];
12592
+ for (const [sourceFile, packs] of packsByFile) {
12593
+ const recognizerOnly = packs.some(
12594
+ (pack) => pack.discovery.length === 0 && pack.discoverUnits === void 0 && ((pack.invocationRecognizers?.length ?? 0) > 0 || (pack.accessRecognizers?.length ?? 0) > 0)
12595
+ );
12596
+ if (!recognizerOnly) {
12597
+ continue;
12598
+ }
12599
+ for (const declaration of sourceFile.getFunctions()) {
12600
+ if (declaration.isExported() && hasBody(declaration)) {
12601
+ roots.push({
12602
+ func: declaration,
12603
+ name: declaration.getName() ?? "<anon>"
12604
+ });
12605
+ }
12606
+ }
12607
+ for (const variableStatement of sourceFile.getVariableStatements()) {
12608
+ if (!variableStatement.isExported()) {
12609
+ continue;
12610
+ }
12611
+ for (const declaration of variableStatement.getDeclarations()) {
12612
+ const init = declaration.getInitializer();
12613
+ if (init !== void 0 && (Node54.isArrowFunction(init) || Node54.isFunctionExpression(init)) && hasBody(init)) {
12614
+ roots.push({ func: init, name: declaration.getName() });
12615
+ }
12616
+ }
12617
+ }
12618
+ }
12619
+ return roots;
12620
+ }
12621
+ function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = [], extraRoots = []) {
12512
12622
  const lookup = createSourceFileLookup(project);
12513
12623
  const db = facts?.db ?? new Database3();
12514
12624
  const functionByKey = /* @__PURE__ */ new Map();
@@ -12528,6 +12638,13 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
12528
12638
  db.add("entry", [key]);
12529
12639
  }
12530
12640
  }
12641
+ for (const root of extraRoots) {
12642
+ const key = nodeKey2(root.func);
12643
+ if (!functionByKey.has(key)) {
12644
+ functionByKey.set(key, root);
12645
+ }
12646
+ db.add("entry", [key]);
12647
+ }
12531
12648
  const knownKeys = /* @__PURE__ */ new Set();
12532
12649
  for (const summary of alreadySummarized) {
12533
12650
  const func = lookup.functionAt(summary.location);
@@ -13595,6 +13712,185 @@ function computeAdapterPacksDigest(packVersions) {
13595
13712
  return `${adapterStamp}|${sortedPacks.join(",")}`;
13596
13713
  }
13597
13714
 
13715
+ // src/workspacePatterns.ts
13716
+ import { createHash as createHash4 } from "crypto";
13717
+ import fs8 from "fs";
13718
+ import path9 from "path";
13719
+ function readJson(filePath) {
13720
+ try {
13721
+ return JSON.parse(fs8.readFileSync(filePath, "utf8"));
13722
+ } catch {
13723
+ return void 0;
13724
+ }
13725
+ }
13726
+ function pnpmWorkspaceGlobs(yamlText) {
13727
+ const globs = [];
13728
+ let inPackages = false;
13729
+ for (const line of yamlText.split("\n")) {
13730
+ if (/^packages\s*:/.test(line)) {
13731
+ inPackages = true;
13732
+ continue;
13733
+ }
13734
+ if (inPackages) {
13735
+ const item = line.match(/^\s+-\s+["']?([^"'#\s]+)["']?/);
13736
+ if (item !== null) {
13737
+ globs.push(item[1]);
13738
+ continue;
13739
+ }
13740
+ if (/^\S/.test(line)) {
13741
+ inPackages = false;
13742
+ }
13743
+ }
13744
+ }
13745
+ return globs;
13746
+ }
13747
+ function workspaceGlobsAt(dir) {
13748
+ const pkg = readJson(path9.join(dir, "package.json"));
13749
+ if (typeof pkg === "object" && pkg !== null && "workspaces" in pkg) {
13750
+ const ws = pkg.workspaces;
13751
+ if (Array.isArray(ws)) {
13752
+ return ws.filter((g) => typeof g === "string");
13753
+ }
13754
+ if (typeof ws === "object" && ws !== null && Array.isArray(ws.packages)) {
13755
+ return ws.packages.filter(
13756
+ (g) => typeof g === "string"
13757
+ );
13758
+ }
13759
+ }
13760
+ const pnpmPath = path9.join(dir, "pnpm-workspace.yaml");
13761
+ if (fs8.existsSync(pnpmPath)) {
13762
+ try {
13763
+ return pnpmWorkspaceGlobs(fs8.readFileSync(pnpmPath, "utf8"));
13764
+ } catch {
13765
+ return null;
13766
+ }
13767
+ }
13768
+ return null;
13769
+ }
13770
+ function isDirectory(candidate) {
13771
+ try {
13772
+ return fs8.statSync(candidate).isDirectory();
13773
+ } catch {
13774
+ return false;
13775
+ }
13776
+ }
13777
+ function subdirectoriesOf(dir) {
13778
+ try {
13779
+ return fs8.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && name !== "node_modules");
13780
+ } catch {
13781
+ return [];
13782
+ }
13783
+ }
13784
+ function segmentToRegExp(segment) {
13785
+ const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13786
+ return new RegExp(`^${escaped.replace(/\*/g, "[^/]*")}$`);
13787
+ }
13788
+ function directoriesMatchingGlob(root, glob) {
13789
+ let current2 = [root];
13790
+ for (const segment of glob.split("/").filter((s) => s.length > 0)) {
13791
+ const next = [];
13792
+ if (segment.includes("*")) {
13793
+ const pattern = segmentToRegExp(segment);
13794
+ for (const dir of current2) {
13795
+ for (const entry of subdirectoriesOf(dir)) {
13796
+ if (pattern.test(entry)) {
13797
+ next.push(path9.join(dir, entry));
13798
+ }
13799
+ }
13800
+ }
13801
+ } else {
13802
+ for (const dir of current2) {
13803
+ next.push(path9.join(dir, segment));
13804
+ }
13805
+ }
13806
+ current2 = next.filter(isDirectory);
13807
+ }
13808
+ return current2;
13809
+ }
13810
+ function packageAt(dir) {
13811
+ const packageJsonPath = path9.join(dir, "package.json");
13812
+ const pkg = readJson(packageJsonPath);
13813
+ if (typeof pkg === "object" && pkg !== null && typeof pkg.name === "string") {
13814
+ return { name: pkg.name, packageJsonPath };
13815
+ }
13816
+ return null;
13817
+ }
13818
+ function workspacePackagesFrom(anchor) {
13819
+ let dir = path9.resolve(anchor);
13820
+ for (; ; ) {
13821
+ const globs = workspaceGlobsAt(dir);
13822
+ if (globs !== null) {
13823
+ const found = /* @__PURE__ */ new Map();
13824
+ for (const glob of globs) {
13825
+ if (glob.startsWith("!")) {
13826
+ continue;
13827
+ }
13828
+ for (const match of directoriesMatchingGlob(dir, glob)) {
13829
+ const pkg = packageAt(match);
13830
+ if (pkg !== null) {
13831
+ found.set(pkg.packageJsonPath, pkg);
13832
+ }
13833
+ }
13834
+ }
13835
+ return [...found.values()].sort(
13836
+ (a, b) => a.packageJsonPath.localeCompare(b.packageJsonPath)
13837
+ );
13838
+ }
13839
+ const parent = path9.dirname(dir);
13840
+ if (parent === dir) {
13841
+ return [];
13842
+ }
13843
+ dir = parent;
13844
+ }
13845
+ }
13846
+ function isWorkspaceMarked(pattern) {
13847
+ const match = pattern.match;
13848
+ return (match.type === "packageExports" || match.type === "packageImport") && match.workspaces === true;
13849
+ }
13850
+ function expandPattern(pattern, packages) {
13851
+ const match = pattern.match;
13852
+ if (match.type === "packageExports" && match.workspaces === true) {
13853
+ const { workspaces: _workspaces, ...rest } = match;
13854
+ return packages.map((pkg) => ({
13855
+ ...pattern,
13856
+ match: { ...rest, packageJsonPath: pkg.packageJsonPath }
13857
+ }));
13858
+ }
13859
+ if (match.type === "packageImport" && match.workspaces === true) {
13860
+ const listed = match.packages ?? [];
13861
+ const names = [.../* @__PURE__ */ new Set([...listed, ...packages.map((p) => p.name)])];
13862
+ if (names.length === 0) {
13863
+ return [];
13864
+ }
13865
+ return [{ ...pattern, match: { type: "packageImport", packages: names } }];
13866
+ }
13867
+ return [pattern];
13868
+ }
13869
+ function workspaceExpansionStamp(frameworks) {
13870
+ const paths = frameworks.flatMap((pack) => pack.discovery).flatMap(
13871
+ (pattern) => pattern.match.type === "packageExports" && pattern.match.packageJsonPath !== void 0 ? [pattern.match.packageJsonPath] : []
13872
+ ).sort();
13873
+ if (paths.length === 0) {
13874
+ return "none";
13875
+ }
13876
+ return createHash4("sha256").update(paths.join("\n")).digest("hex").slice(0, 12);
13877
+ }
13878
+ function expandWorkspacePatterns(frameworks, anchor) {
13879
+ const marked = frameworks.some(
13880
+ (pack) => pack.discovery.some(isWorkspaceMarked)
13881
+ );
13882
+ if (!marked) {
13883
+ return frameworks;
13884
+ }
13885
+ const packages = anchor !== void 0 ? workspacePackagesFrom(anchor) : [];
13886
+ return frameworks.map((pack) => ({
13887
+ ...pack,
13888
+ discovery: pack.discovery.flatMap(
13889
+ (pattern) => expandPattern(pattern, packages)
13890
+ )
13891
+ }));
13892
+ }
13893
+
13598
13894
  // src/adapter.ts
13599
13895
  var raise = (msg) => {
13600
13896
  throw new Error(msg);
@@ -14005,11 +14301,11 @@ function extractConsumerBinding(unit, pattern, pack, resolution) {
14005
14301
  return null;
14006
14302
  }
14007
14303
  const method = extractBindingMethod(binding, callSite, pack, resolution);
14008
- const path12 = extractBindingPath(binding, callSite, pack, resolution);
14304
+ const path13 = extractBindingPath(binding, callSite, pack, resolution);
14009
14305
  return restBinding2({
14010
14306
  transport: pack.protocol,
14011
14307
  method: method ?? null,
14012
- path: path12 ?? null,
14308
+ path: path13 ?? null,
14013
14309
  recognition: pack.name
14014
14310
  });
14015
14311
  }
@@ -14134,8 +14430,8 @@ function collectAccessRecognizers(frameworks, tallies) {
14134
14430
  }
14135
14431
  return out;
14136
14432
  }
14137
- function computeSubUnitBarriers(unit, pack, ctx, tally, file) {
14138
- if (pack.subUnits === void 0 || unit.func === null) {
14433
+ function computeSubUnitBarriers(unit, packs, ctx, tallies, file) {
14434
+ if (unit.func === null) {
14139
14435
  return NO_BARRIERS;
14140
14436
  }
14141
14437
  const parentHandle = {
@@ -14143,27 +14439,27 @@ function computeSubUnitBarriers(unit, pack, ctx, tally, file) {
14143
14439
  name: unit.name,
14144
14440
  kind: unit.kind
14145
14441
  };
14146
- try {
14147
- const subUnits = pack.subUnits(parentHandle, ctx);
14148
- if (subUnits.length === 0) {
14149
- return NO_BARRIERS;
14442
+ const barriers = /* @__PURE__ */ new Set();
14443
+ for (const pack of packs) {
14444
+ if (pack.subUnits === void 0) {
14445
+ continue;
14150
14446
  }
14151
- const barriers = /* @__PURE__ */ new Set();
14152
- for (const su of subUnits) {
14153
- barriers.add(su.func);
14447
+ try {
14448
+ for (const su of pack.subUnits(parentHandle, ctx)) {
14449
+ barriers.add(su.func);
14450
+ }
14451
+ } catch (err) {
14452
+ process.stderr.write(
14453
+ recordPackFailure(tallies?.get(pack.name), {
14454
+ pack: pack.name,
14455
+ hook: "subUnits",
14456
+ file,
14457
+ error: err
14458
+ })
14459
+ );
14154
14460
  }
14155
- return barriers;
14156
- } catch (err) {
14157
- process.stderr.write(
14158
- recordPackFailure(tally, {
14159
- pack: pack.name,
14160
- hook: "subUnits",
14161
- file,
14162
- error: err
14163
- })
14164
- );
14165
- return NO_BARRIERS;
14166
14461
  }
14462
+ return barriers.size === 0 ? NO_BARRIERS : barriers;
14167
14463
  }
14168
14464
  function extractFromSourceFile(sourceFile, frameworks, claimedUnits, options, tallies, resolution, mountPrefixes) {
14169
14465
  const summaries = [];
@@ -14245,9 +14541,9 @@ function extractFromSourceFile(sourceFile, frameworks, claimedUnits, options, ta
14245
14541
  }
14246
14542
  const barriers = computeSubUnitBarriers(
14247
14543
  unit,
14248
- pack,
14544
+ frameworks,
14249
14545
  subUnitCtx,
14250
- tally,
14546
+ tallies,
14251
14547
  sourceFile.getFilePath()
14252
14548
  );
14253
14549
  const raw = extractCodeStructure(
@@ -14621,8 +14917,8 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14621
14917
  if (pathArg === void 0) {
14622
14918
  continue;
14623
14919
  }
14624
- const path12 = pathFromArgument(pathArg, resolution);
14625
- if (path12 === void 0) {
14920
+ const path13 = pathFromArgument(pathArg, resolution);
14921
+ if (path13 === void 0) {
14626
14922
  continue;
14627
14923
  }
14628
14924
  const callerFunc = enclosingFunction(callExpr);
@@ -14634,7 +14930,7 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14634
14930
  continue;
14635
14931
  }
14636
14932
  seen.add(dedupKey);
14637
- out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path12, options));
14933
+ out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path13, options));
14638
14934
  }
14639
14935
  return out;
14640
14936
  }
@@ -14691,7 +14987,7 @@ function callerName(func) {
14691
14987
  }
14692
14988
  return "anonymous";
14693
14989
  }
14694
- function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14990
+ function buildCallerSummary(wrapper, callerFunc, callExpr, path13, options) {
14695
14991
  const wrapperBinding = wrapper.summary.identity.boundaryBinding;
14696
14992
  const wrapperRest = wrapperBinding?.semantics.name === "rest" ? wrapperBinding.semantics : null;
14697
14993
  const syntheticPack = {
@@ -14723,7 +15019,7 @@ function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14723
15019
  raw.boundaryBinding = restBinding2({
14724
15020
  transport: wrapperBinding?.transport ?? "http",
14725
15021
  method: wrapperRest?.method ?? null,
14726
- path: path12,
15022
+ path: path13,
14727
15023
  recognition: wrapperBinding?.recognition ?? "unknown"
14728
15024
  });
14729
15025
  const summary = assembleSummary3(raw, options);
@@ -14789,25 +15085,32 @@ function loadRunFiles(project, candidatePaths, timer) {
14789
15085
  );
14790
15086
  return { sourceFiles, deep };
14791
15087
  }
14792
- function createTypeScriptAdapter(config) {
15088
+ function createTypeScriptAdapter(suppliedConfig) {
15089
+ const config = {
15090
+ ...suppliedConfig,
15091
+ frameworks: expandWorkspacePatterns(
15092
+ suppliedConfig.frameworks,
15093
+ suppliedConfig.projectRoot ?? (suppliedConfig.tsConfigFilePath !== void 0 ? path10.dirname(suppliedConfig.tsConfigFilePath) : void 0)
15094
+ )
15095
+ };
14793
15096
  const project = config.project ?? new Project(
14794
15097
  config.tsConfigFilePath !== void 0 ? {
14795
15098
  tsConfigFilePath: config.tsConfigFilePath,
14796
15099
  skipAddingFilesFromTsConfig: true
14797
15100
  } : { skipAddingFilesFromTsConfig: true }
14798
15101
  );
14799
- const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(path9.dirname(config.tsConfigFilePath)) : void 0);
15102
+ const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(path10.dirname(config.tsConfigFilePath)) : void 0);
14800
15103
  let lazyBootstrapped = false;
14801
15104
  let projectFileSet;
14802
15105
  const cacheDir = declineWhenRunFromSource(
14803
- config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? path9.join(path9.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
15106
+ config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? path10.join(path10.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
14804
15107
  );
14805
15108
  const cache = createCacheLayer(cacheDir);
14806
15109
  const adapterPacksDigest = `${computeAdapterPacksDigest(
14807
15110
  config.frameworks.map(
14808
15111
  (p) => p.version !== void 0 ? { name: p.name, version: p.version } : { name: p.name }
14809
15112
  )
14810
- )}|${extractionConfigStamp(config)}`;
15113
+ )}|${extractionConfigStamp(config)}|ws:${workspaceExpansionStamp(config.frameworks)}`;
14811
15114
  const packWrappers = config.frameworks.flatMap(
14812
15115
  (pack) => pack.transparentWrappers ?? []
14813
15116
  );
@@ -15009,6 +15312,7 @@ function createTypeScriptAdapter(config) {
15009
15312
  pipeline,
15010
15313
  project,
15011
15314
  config.frameworks,
15315
+ packsByFile,
15012
15316
  config.extractorOptions,
15013
15317
  tallies,
15014
15318
  (created, parent) => {
@@ -15043,7 +15347,12 @@ function createTypeScriptAdapter(config) {
15043
15347
  },
15044
15348
  // Reached units the cache already serves emit nothing,
15045
15349
  // the way a cold run's seeds do not.
15046
- reused?.summaries ?? []
15350
+ reused?.summaries ?? [],
15351
+ // A recognizer-only pack's effects need a function to
15352
+ // live on even when nothing discovers units in its
15353
+ // files, so those files' exports join the walk as
15354
+ // roots.
15355
+ recognizerOnlyRoots(packsByFile)
15047
15356
  )
15048
15357
  ) : withSubUnits;
15049
15358
  const closureOwnership = caching && config.includeReachable !== false ? timer.time(
@@ -15344,11 +15653,15 @@ function buildCacheAttribution(args) {
15344
15653
  ];
15345
15654
  return { roots, owners };
15346
15655
  }
15347
- function synthesizeSubUnits(summaries, project, frameworks, options, tallies, onCreated) {
15656
+ function synthesizeSubUnits(summaries, project, frameworks, packsByFile, options, tallies, onCreated) {
15348
15657
  const packByRecognition = /* @__PURE__ */ new Map();
15349
15658
  for (const pack of frameworks) {
15350
15659
  packByRecognition.set(pack.name, pack);
15351
15660
  }
15661
+ const packsByPath = /* @__PURE__ */ new Map();
15662
+ for (const [sf, packs] of packsByFile) {
15663
+ packsByPath.set(sf.getFilePath(), packs);
15664
+ }
15352
15665
  const allInvocationRecognizers = collectInvocationRecognizers(
15353
15666
  frameworks,
15354
15667
  tallies
@@ -15362,8 +15675,15 @@ function synthesizeSubUnits(summaries, project, frameworks, options, tallies, on
15362
15675
  if (binding === null) {
15363
15676
  continue;
15364
15677
  }
15365
- const pack = packByRecognition.get(binding.recognition);
15366
- if (pack?.subUnits === void 0) {
15678
+ const discovering = packByRecognition.get(binding.recognition);
15679
+ const applicable = packsByPath.get(parent.location.file) ?? (discovering !== void 0 ? [discovering] : []);
15680
+ const hooked = [
15681
+ ...discovering?.subUnits !== void 0 ? [discovering] : [],
15682
+ ...applicable.filter(
15683
+ (one) => one.subUnits !== void 0 && one !== discovering
15684
+ )
15685
+ ];
15686
+ if (hooked.length === 0) {
15367
15687
  continue;
15368
15688
  }
15369
15689
  lookup ??= createSourceFileLookup(project);
@@ -15376,7 +15696,17 @@ function synthesizeSubUnits(summaries, project, frameworks, options, tallies, on
15376
15696
  name: parent.identity.name,
15377
15697
  kind: parent.kind
15378
15698
  };
15379
- const subUnits = pack.subUnits(parentHandle, subUnitCtx);
15699
+ const claimedFuncs = /* @__PURE__ */ new Set();
15700
+ const subUnits = [];
15701
+ for (const pack of hooked) {
15702
+ for (const subUnit of pack.subUnits?.(parentHandle, subUnitCtx) ?? []) {
15703
+ if (claimedFuncs.has(subUnit.func)) {
15704
+ continue;
15705
+ }
15706
+ claimedFuncs.add(subUnit.func);
15707
+ subUnits.push(subUnit);
15708
+ }
15709
+ }
15380
15710
  for (const subUnit of subUnits) {
15381
15711
  const summary = buildSubUnitSummary(
15382
15712
  subUnit,
@@ -15440,8 +15770,8 @@ function buildSubUnitSummary(subUnit, parent, invocationRecognizers, accessRecog
15440
15770
  }
15441
15771
 
15442
15772
  // src/bootstrap/noTsconfigProject.ts
15443
- import fs8 from "fs";
15444
- import path10 from "path";
15773
+ import fs9 from "fs";
15774
+ import path11 from "path";
15445
15775
  import {
15446
15776
  ModuleKind,
15447
15777
  ModuleResolutionKind,
@@ -15477,7 +15807,7 @@ function findSourceFiles(rootDir, depth = 0) {
15477
15807
  const found = [];
15478
15808
  let entries;
15479
15809
  try {
15480
- entries = fs8.readdirSync(rootDir, { withFileTypes: true });
15810
+ entries = fs9.readdirSync(rootDir, { withFileTypes: true });
15481
15811
  } catch {
15482
15812
  return [];
15483
15813
  }
@@ -15485,10 +15815,10 @@ function findSourceFiles(rootDir, depth = 0) {
15485
15815
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name)) {
15486
15816
  continue;
15487
15817
  }
15488
- const full = path10.join(rootDir, entry.name);
15818
+ const full = path11.join(rootDir, entry.name);
15489
15819
  if (entry.isDirectory()) {
15490
15820
  found.push(...findSourceFiles(full, depth + 1));
15491
- } else if (SOURCE_EXTENSIONS.has(path10.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15821
+ } else if (SOURCE_EXTENSIONS.has(path11.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15492
15822
  found.push(full);
15493
15823
  }
15494
15824
  }
@@ -15523,15 +15853,15 @@ function createProjectWithoutTsconfig(rootDir) {
15523
15853
  return { project, files };
15524
15854
  }
15525
15855
  function findNearestTsconfig(startDir) {
15526
- let dir = path10.resolve(startDir);
15856
+ let dir = path11.resolve(startDir);
15527
15857
  while (true) {
15528
15858
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
15529
- const candidate = path10.join(dir, name);
15530
- if (fs8.existsSync(candidate)) {
15859
+ const candidate = path11.join(dir, name);
15860
+ if (fs9.existsSync(candidate)) {
15531
15861
  return candidate;
15532
15862
  }
15533
15863
  }
15534
- const parent = path10.dirname(dir);
15864
+ const parent = path11.dirname(dir);
15535
15865
  if (parent === dir) {
15536
15866
  return null;
15537
15867
  }
@@ -15540,7 +15870,7 @@ function findNearestTsconfig(startDir) {
15540
15870
  }
15541
15871
 
15542
15872
  // src/facts/why.ts
15543
- import path11 from "path";
15873
+ import path12 from "path";
15544
15874
  import { Node as Node59, Project as Project3, SyntaxKind as SyntaxKind13 } from "ts-morph";
15545
15875
  import { explainResolutionProof, renderExplanation } from "@suss/resolution";
15546
15876
  var JS_PHRASES = {
@@ -15553,7 +15883,7 @@ var WhySession = class {
15553
15883
  store;
15554
15884
  root;
15555
15885
  constructor(options) {
15556
- this.root = path11.resolve(options.dir);
15886
+ this.root = path12.resolve(options.dir);
15557
15887
  const tsconfig = findNearestTsconfig(this.root);
15558
15888
  this.project = tsconfig === null ? createProjectWithoutTsconfig(this.root).project : new Project3({ tsConfigFilePath: tsconfig });
15559
15889
  this.store = new ResolutionStore();
@@ -15642,7 +15972,7 @@ var WhySession = class {
15642
15972
  };
15643
15973
  }
15644
15974
  sourceFileAt(file) {
15645
- const absolute = path11.resolve(this.root, file);
15975
+ const absolute = path12.resolve(this.root, file);
15646
15976
  const loaded = this.project.getSourceFile(absolute);
15647
15977
  if (loaded !== void 0) {
15648
15978
  return loaded;
@@ -15656,7 +15986,7 @@ var WhySession = class {
15656
15986
  /** A module key: a file path said relative to the root, or a package name. */
15657
15987
  displayPath(key) {
15658
15988
  if (key.startsWith(this.root)) {
15659
- return path11.relative(this.root, key);
15989
+ return path12.relative(this.root, key);
15660
15990
  }
15661
15991
  return key;
15662
15992
  }
@@ -15727,6 +16057,17 @@ function funnelDrops(packs) {
15727
16057
  }
15728
16058
  return violations;
15729
16059
  }
16060
+ function recognizersWithNoUnits(packs) {
16061
+ return packs.filter(
16062
+ (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
16063
+ // roots, so effects recognized there mean the run worked and
16064
+ // the missing framework pack costs attribution, not existence.
16065
+ funnel.effectsRecognized === 0
16066
+ ).map((funnel) => ({
16067
+ label: funnel.pack,
16068
+ 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.`
16069
+ }));
16070
+ }
15730
16071
  function unversionedPacks(packs) {
15731
16072
  return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
15732
16073
  label: funnel.pack,
@@ -15821,6 +16162,12 @@ function evaluatePackHealth(report) {
15821
16162
  audience: "run",
15822
16163
  violations: selfCollisions(report.packs)
15823
16164
  },
16165
+ {
16166
+ name: "every recognizer had units to look inside",
16167
+ code: "no-units",
16168
+ audience: "run",
16169
+ violations: recognizersWithNoUnits(report.packs)
16170
+ },
15824
16171
  {
15825
16172
  name: "every pack declares a version",
15826
16173
  code: "no-version",