ucn 5.5.2 → 5.6.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.
Files changed (74) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -12
  2. package/.claude/skills/ucn/references/commands.md +21 -17
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +474 -0
  6. package/README.md +71 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +138 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +273 -107
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +29 -65
  13. package/core/cache.js +101 -16
  14. package/core/callers.js +352 -70
  15. package/core/capture.js +331 -0
  16. package/core/change-kinds.js +888 -0
  17. package/core/check.js +1117 -139
  18. package/core/client-receivers.js +189 -0
  19. package/core/command-contracts.js +7 -6
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +388 -85
  23. package/core/deletion.js +401 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoints.js +12 -6
  26. package/core/evidence-bands.js +68 -0
  27. package/core/evidence-bands.json +44 -0
  28. package/core/execute.js +172 -161
  29. package/core/hot-summary.js +241 -0
  30. package/core/index-ir.js +3 -1
  31. package/core/inprocess-clients.js +41 -15
  32. package/core/ir.js +5 -1
  33. package/core/member-signatures.js +789 -0
  34. package/core/output/analysis-ext.js +20 -10
  35. package/core/output/analysis.js +98 -60
  36. package/core/output/check.js +98 -37
  37. package/core/output/doctor.js +34 -1
  38. package/core/output/find.js +2 -1
  39. package/core/output/lines.js +63 -3
  40. package/core/output/public.js +30 -11
  41. package/core/output/refactoring.js +32 -0
  42. package/core/output/reporting.js +21 -2
  43. package/core/output/search.js +11 -2
  44. package/core/output/tracing.js +35 -7
  45. package/core/output-budget.js +124 -57
  46. package/core/parallel-build.js +3 -3
  47. package/core/project.js +106 -23
  48. package/core/provenance-type.js +12 -5
  49. package/core/public-command.js +35 -9
  50. package/core/python-modules.js +6 -0
  51. package/core/reflection.js +166 -23
  52. package/core/registry.js +623 -72
  53. package/core/reporting.js +69 -34
  54. package/core/request-helpers.js +63 -7
  55. package/core/review-items.js +75 -0
  56. package/core/search.js +89 -128
  57. package/core/shared.js +0 -45
  58. package/core/snapshot.js +350 -0
  59. package/core/source-universe.js +707 -0
  60. package/core/test-units.js +3624 -0
  61. package/core/tracing.js +150 -96
  62. package/core/verify.js +27 -11
  63. package/languages/adapter.js +8 -3
  64. package/languages/c-family.js +12 -0
  65. package/languages/csharp.js +634 -0
  66. package/languages/go.js +120 -0
  67. package/languages/index.js +422 -4
  68. package/languages/java.js +712 -0
  69. package/languages/javascript.js +1460 -2
  70. package/languages/lexical-scope.js +68 -0
  71. package/languages/python.js +1672 -1
  72. package/languages/rust.js +91 -0
  73. package/mcp/server.js +44 -110
  74. package/package.json +11 -6
package/core/reporting.js CHANGED
@@ -11,6 +11,7 @@ const fs = require('fs');
11
11
  const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
12
12
  const path = require('path');
13
13
  const { isTestFile } = require('./discovery');
14
+ const { paramFact } = require('./registry');
14
15
  const { summarizeCommandTrust } = require('./trust-matrix');
15
16
  const { projectComputedDispatch } = require('./ast-analysis');
16
17
  const { langTraits } = require('../languages');
@@ -123,8 +124,37 @@ function getStats(index, options = {}) {
123
124
  // positive integer, 0 (show nothing), or undefined (default 10).
124
125
  const top = options.top === 0
125
126
  ? 0
126
- : ((options.top != null && Number(options.top) > 0) ? Number(options.top) : 10);
127
+ : ((options.top != null && Number(options.top) > 0) ? Number(options.top) : paramFact('top', 'hot'));
127
128
  const FUNCTION_TYPES = CALLABLE_SYMBOL_KINDS;
129
+ const maxRefine = Number.isInteger(options.maxRefine) && options.maxRefine > 0
130
+ ? options.maxRefine : Infinity;
131
+ // Refinement work is bounded by call records, not wall time, so the
132
+ // answer is the same on every run and machine (fix #382). One
133
+ // refinement scans the call records of its name in the countable
134
+ // files; `workBudget` caps their sum. Exact answers on real projects
135
+ // stay far below it; a very large repository (kubernetes: ~68M
136
+ // records under its candidate ceilings) stops and says so.
137
+ const workBudget = Number.isFinite(options.workBudget) && options.workBudget > 0
138
+ ? options.workBudget : STATS_HOT_WORK_BUDGET;
139
+
140
+ // The ranking is a function of the index and of these options, so an
141
+ // answer already computed for this index state is the answer
142
+ // (core/hot-summary.js). Nothing below runs for it.
143
+ const hotSummary = require('./hot-summary');
144
+ const summaryKey = hotSummary.keyOf({
145
+ top, productionCallsOnly: options.productionCallsOnly, maxRefine, workBudget,
146
+ file: options.file, in: options.in,
147
+ exclude: Array.isArray(options.exclude)
148
+ ? options.exclude
149
+ : (options.exclude
150
+ ? String(options.exclude).split(',').map(s => s.trim()).filter(Boolean)
151
+ : []),
152
+ });
153
+ const keptSummary = hotSummary.get(index, summaryKey);
154
+ if (keptSummary) {
155
+ stats.hot = keptSummary;
156
+ return stats;
157
+ }
128
158
 
129
159
  // Ensure the calls cache is fully populated before counting.
130
160
  // First-time stats --hot may need to parse files to extract calls;
@@ -374,16 +404,6 @@ function getStats(index, options = {}) {
374
404
  }
375
405
  }
376
406
  }
377
- const maxRefine = Number.isInteger(options.maxRefine) && options.maxRefine > 0
378
- ? options.maxRefine : Infinity;
379
- // Refinement work is bounded by call records, not wall time, so the
380
- // answer is the same on every run and machine (fix #382). One
381
- // refinement scans the call records of its name in the countable
382
- // files; `workBudget` caps their sum. Exact answers on real projects
383
- // stay far below it; a very large repository (kubernetes: ~68M
384
- // records under its candidate ceilings) stops and says so.
385
- const workBudget = Number.isFinite(options.workBudget) && options.workBudget > 0
386
- ? options.workBudget : STATS_HOT_WORK_BUDGET;
387
407
  const workOf = candidate => recordsByName.get(candidate.name) || 0;
388
408
  // Ceilings in descending order: the early stop fires as soon as no
389
409
  // unseen ceiling can reach the current Nth count, so exactly the
@@ -510,7 +530,7 @@ function getStats(index, options = {}) {
510
530
  (a.startLine || 0) - (b.startLine || 0)
511
531
  );
512
532
 
513
- stats.hot = {
533
+ stats.hot = hotSummary.put(index, summaryKey, {
514
534
  top,
515
535
  total: refined === candidates.length ? hotList.length : candidates.length,
516
536
  totalKind: refined === candidates.length ? 'confirmed' : 'raw-call-candidates',
@@ -523,7 +543,7 @@ function getStats(index, options = {}) {
523
543
  note: refined === candidates.length
524
544
  ? 'Counts are confirmed caller-engine edges pinned to each displayed definition; unverified dispatch is excluded.'
525
545
  : `Displayed counts are exact confirmed caller-engine edges; ${candidates.length} raw candidates were bounded and ${refined} required exact refinement.`,
526
- };
546
+ });
527
547
  }
528
548
 
529
549
  return stats;
@@ -747,11 +767,6 @@ function doctor(index, options = {}) {
747
767
  skippedSources: { count: 0, fileCount: 0, files: [], reasons: {} },
748
768
  };
749
769
 
750
- // Reflection/eval signals come from the shared text-blind-spot counter
751
- // (core/shared.js) — the SAME routine detectCompleteness uses for the about
752
- // footer, so the two never drift (field-report #2). Occurrence counts.
753
- const { hasTextBlindspots, countTextBlindspots } = lazySharedModule();
754
-
755
770
  const computedDispatch = projectComputedDispatch(index);
756
771
  for (const [filePath, fe] of index.files) {
757
772
  fileCounts.total++;
@@ -781,14 +796,12 @@ function doctor(index, options = {}) {
781
796
  if (fe.parseError) recordBlind(blindSpots.parseFailures, 1);
782
797
  if (fe.parseRecovery) recordBlind(blindSpots.parseRecoveries, 1);
783
798
 
784
- // Read file once for eval/reflection signals (shared counter).
785
- if (hasTextBlindspots(lang)) {
786
- try {
787
- const bs = countTextBlindspots(fs.readFileSync(filePath, 'utf-8'), lang);
788
- recordBlind(blindSpots.evalCalls, bs.eval);
789
- recordBlind(blindSpots.reflection, bs.reflection);
790
- } catch (e) { /* ignore read errors */ }
791
- }
799
+ // Code execution and reflection: call sites of each language's
800
+ // vocabulary (langTraits().reflectionApi), counted in the file's
801
+ // tree when it was indexed. The completeness footer reads the same
802
+ // two numbers, so the two never drift, and no file is read here.
803
+ recordBlind(blindSpots.evalCalls, fe.evalUses || 0);
804
+ recordBlind(blindSpots.reflection, fe.reflectionUses || 0);
792
805
  }
793
806
 
794
807
  // Files that failed before a fileEntry could be created are absent from
@@ -825,16 +838,30 @@ function doctor(index, options = {}) {
825
838
  }
826
839
  fileCounts.unsupported = blindSpots.unsupportedSources.fileCount;
827
840
 
828
- for (const issue of index.discoveryIssues || []) {
829
- const rel = issue.relativePath || '.';
830
- if (!matchInFilter(rel)) continue;
841
+ // Source that is not indexed, from the one model every claim uses
842
+ // (core/source-universe.js): files searched as text only (a bundle, an
843
+ // excluded path, an oversized file; unsupported languages are counted
844
+ // above) and paths nothing can be read from. `unsearchable` is the part
845
+ // no name can be cleared against.
846
+ const sourceUniverse = require('./source-universe').universe(index);
847
+ let unsearchablePaths = 0;
848
+ const noteSkipped = (rel, reason) => {
849
+ if (!matchInFilter(rel)) return false;
831
850
  const skipped = blindSpots.skippedSources;
832
851
  skipped.count++;
833
852
  skipped.fileCount++;
834
- skipped.reasons[issue.reason || 'unknown'] =
835
- (skipped.reasons[issue.reason || 'unknown'] || 0) + 1;
853
+ skipped.reasons[reason] = (skipped.reasons[reason] || 0) + 1;
836
854
  if (skipped.files.length < BLINDSPOT_FILE_CAP) skipped.files.push(rel);
855
+ return true;
856
+ };
857
+ for (const entry of sourceUniverse.textOnly) {
858
+ if (entry.reason === 'unsupported-language') continue;
859
+ noteSkipped(entry.relativePath, entry.reason);
860
+ }
861
+ for (const entry of sourceUniverse.opaque) {
862
+ if (noteSkipped(entry.relativePath, entry.reason || 'unknown')) unsearchablePaths++;
837
863
  }
864
+ blindSpots.skippedSources.unsearchable = unsearchablePaths;
838
865
  fileCounts.skipped = blindSpots.skippedSources.fileCount;
839
866
 
840
867
  // C/C++ token-pasting macro invocations whose expansion cannot be
@@ -910,7 +937,7 @@ function doctor(index, options = {}) {
910
937
  if (blindSpots.skippedSources.count > 0) {
911
938
  const reasons = Object.entries(blindSpots.skippedSources.reasons)
912
939
  .map(([reason, count]) => `${reason} ${count}`).join(', ');
913
- blindSignals.push(`${blindSpots.skippedSources.count} source discovery gap(s): ${reasons}`);
940
+ blindSignals.push(`${blindSpots.skippedSources.count} source path(s) not indexed: ${reasons}`);
914
941
  }
915
942
 
916
943
  // Trust is task-specific. A healthy index can be excellent for navigation
@@ -932,8 +959,10 @@ function doctor(index, options = {}) {
932
959
  ? `${blindSpots.parseRecoveries.count} file(s) required parser recovery; indexed results may be partial`
933
960
  : cache.fresh === false ? 'index cache is stale'
934
961
  : index.truncated ? `file discovery stopped at maxFiles=${index.truncated.maxFiles}`
962
+ : unsearchablePaths > 0
963
+ ? `${unsearchablePaths} path(s) could not be searched; no caller account is complete until they can`
935
964
  : blindSpots.skippedSources.count > 0
936
- ? `${blindSpots.skippedSources.count} source path(s) were not indexed; inspect discovery reasons`
965
+ ? `${blindSpots.skippedSources.count} source file(s) are searched as text only; a name that occurs in them is listed, not analyzed`
937
966
  : unsupportedCount > 0
938
967
  ? `${fileCounts.scanned} supported file(s) indexed; ${unsupportedCount} unsupported source file(s) require grep/ripgrep`
939
968
  : 'fresh index; no parse failures';
@@ -965,8 +994,11 @@ function doctor(index, options = {}) {
965
994
  : 'no known runtime blind spots detected; alias/dynamic completeness is not compiler-verified locally';
966
995
 
967
996
  const navigationLevel = indexLevel;
997
+ // A path nothing can be read from leaves every caller account
998
+ // incomplete, which is what blocks `check`: the two must agree.
968
999
  const hardIndexRisk = blindSpots.parseFailures.count > 0 ||
969
- blindSpots.parseRecoveries.count > 0 || cache.fresh === false || !!index.truncated;
1000
+ blindSpots.parseRecoveries.count > 0 || cache.fresh === false || !!index.truncated ||
1001
+ unsearchablePaths > 0;
970
1002
  const refactorLevel = indexLevel === 'UNKNOWN' ? 'UNKNOWN'
971
1003
  : indexLevel === 'UNSUPPORTED' ? 'UNSUPPORTED'
972
1004
  : hardIndexRisk ? 'LOW'
@@ -1005,6 +1037,9 @@ function doctor(index, options = {}) {
1005
1037
  symbols: totalSymbols,
1006
1038
  languages: langs,
1007
1039
  blindSpots,
1040
+ // What completeness claims range over: text-only, unsearchable and
1041
+ // declared-outside paths, stated here once.
1042
+ scope: require('./source-universe').scopeSummary(index),
1008
1043
  evidenceProfile,
1009
1044
  cache,
1010
1045
  commandTrust: summarizeCommandTrust(),
@@ -43,6 +43,8 @@ const JS_HTTP_PACKAGES = [
43
43
  ];
44
44
  const JS_HTTP_GLOBAL_CALLS = new Set(['fetch']);
45
45
  const JS_HTTP_GLOBAL_CTORS = new Set(['XMLHttpRequest', 'EventSource']);
46
+ const JS_HTTP_CLIENT_TYPES = new Set(['AxiosInstance', 'AxiosStatic', 'KyInstance',
47
+ 'Got', 'GotInstance', 'HttpClient', 'Dispatcher', 'Agent']);
46
48
  const PY_HTTP_MODULES = [
47
49
  'requests', 'httpx', 'aiohttp', 'urllib3', 'urllib.request', 'http.client', 'niquests', 'treq',
48
50
  ];
@@ -173,7 +175,7 @@ class RequestHelperProof {
173
175
  }
174
176
 
175
177
  /** Proof for the function expression being called. */
176
- jsCallee(file, fn, depth, factory = false) {
178
+ jsCallee(file, fn, depth) {
177
179
  if (!fn || depth > MAX_RESOLVE) return unknown('depth');
178
180
  if (fn.type === 'identifier') {
179
181
  const decl = findDecl(this.sess, file, fn.text, fn);
@@ -201,9 +203,7 @@ class RequestHelperProof {
201
203
  // the method reads as one (`client.post`, `instance.request`);
202
204
  // a server built on an HTTP stack (`app.route(...)`) is not a
203
205
  // client.
204
- // A factory call on a client value (`axios.create()`) yields a
205
- // client value too.
206
- if (!factory && (!prop || !CLIENT_METHOD.test(prop))) return unknown('method-not-request');
206
+ if (!prop || !CLIENT_METHOD.test(prop)) return unknown('method-not-request');
207
207
  return this.jsValue(file, obj, depth + 1);
208
208
  }
209
209
  if (fn.type === 'call_expression') return this.jsValue(file, fn, depth + 1);
@@ -296,7 +296,7 @@ class RequestHelperProof {
296
296
  const spec = this.requireSpecOf({ valueNode: e });
297
297
  return isHttpPackage(spec, JS_HTTP_PACKAGES) ? PROVEN : unknown('receiver-unproven');
298
298
  }
299
- return this.jsCallee(file, callee, depth + 1, true);
299
+ return this.jsFactory(file, callee, depth + 1);
300
300
  }
301
301
  default:
302
302
  return unknown('receiver-unproven');
@@ -314,6 +314,59 @@ class RequestHelperProof {
314
314
  return last;
315
315
  }
316
316
 
317
+ /** A request result or server is not a client just because the function
318
+ * that produced it uses an HTTP package. Follow factory return values. */
319
+ jsFactory(file, callee, depth) {
320
+ if (!callee || depth > MAX_RESOLVE) return unknown('receiver-unproven');
321
+ if (callee.type === 'member_expression') {
322
+ const name = field(callee, 'property')?.text;
323
+ if (name === 'create' || name === 'extend') {
324
+ return this.jsValue(file, unwrap(field(callee, 'object')), depth + 1);
325
+ }
326
+ return unknown('factory-unproven');
327
+ }
328
+ if (callee.type !== 'identifier') return unknown('factory-unproven');
329
+ const decl = findDecl(this.sess, file, callee.text, callee);
330
+ let target = decl ? { file, value: decl.count === 1 ? decl.valueNode : null } : null;
331
+ if (!decl) {
332
+ const binding = this.jsBinding(file, callee.text);
333
+ if (binding) {
334
+ if (binding.binding.module?.startsWith('@hey-api/client-') ||
335
+ binding.binding.module === 'openapi-fetch') return PROVEN;
336
+ const rec = this.jsImportRecord(file, binding.binding, binding.original);
337
+ if (rec) target = { file: rec.file, value: rec.decl?.valueNode || rec.valueNode };
338
+ }
339
+ }
340
+ const fn = unwrap(target?.value);
341
+ if (!fn || !JS_FUNCTION_TYPES.has(fn.type)) return unknown('factory-unproven');
342
+ const body = field(fn, 'body');
343
+ if (!body) return unknown('factory-unproven');
344
+ const values = [];
345
+ const visit = node => {
346
+ if (node.type === 'return_statement') {
347
+ values.push(node.namedChild(0));
348
+ return;
349
+ }
350
+ for (const child of namedChildren(node)) {
351
+ if (!JS_FUNCTION_TYPES.has(child.type) && child.type !== 'class_declaration') visit(child);
352
+ }
353
+ };
354
+ if (body.type === 'statement_block') visit(body);
355
+ else values.push(body);
356
+ if (!values.length) return unknown('factory-unproven');
357
+ const proven = values.every(value => {
358
+ const v = unwrap(value);
359
+ if (v?.type !== 'object') return this.jsValue(target.file, v, depth + 1).proven;
360
+ const methods = namedChildren(v).filter(member =>
361
+ CLIENT_METHOD.test((field(member, 'key') || field(member, 'name'))?.text || ''));
362
+ return methods.length > 0 && methods.every(member => {
363
+ const impl = member.type === 'method_definition' ? member : unwrap(field(member, 'value'));
364
+ return impl && JS_FUNCTION_TYPES.has(impl.type) && this.jsFunction(target.file, impl, 0).proven;
365
+ });
366
+ });
367
+ return proven ? PROVEN : unknown('factory-unproven');
368
+ }
369
+
317
370
  /** A parameter proves a client only through its declared type or default. */
318
371
  jsParam(file, decl, depth) {
319
372
  const holder = decl.nameNode.parent;
@@ -321,7 +374,8 @@ class RequestHelperProof {
321
374
  if (typeNode) {
322
375
  const m = typeNode.text.replace(/^:\s*/, '').match(/^([A-Za-z_$][\w$]*)/);
323
376
  const b = m ? this.jsBinding(file, m[1]) : null;
324
- if (b && isHttpPackage(b.binding.module, JS_HTTP_PACKAGES)) return PROVEN;
377
+ if (b && isHttpPackage(b.binding.module, JS_HTTP_PACKAGES) &&
378
+ JS_HTTP_CLIENT_TYPES.has(b.original)) return PROVEN;
325
379
  }
326
380
  const dflt = holder ? (field(holder, 'value') || field(holder, 'right')) : null;
327
381
  if (dflt) return this.jsValue(file, dflt, depth + 1);
@@ -374,6 +428,8 @@ class RequestHelperProof {
374
428
  return !!b && isHttpPackage(b.binding.module, JS_HTTP_PACKAGES);
375
429
  }
376
430
  if (callee.type === 'member_expression') {
431
+ if (CLIENT_METHOD.test(field(callee, 'property')?.text || '') &&
432
+ this.jsValue(file, unwrap(field(callee, 'object')), 0).proven) return true;
377
433
  let root = unwrap(field(callee, 'object'));
378
434
  while (root && root.type === 'member_expression') root = unwrap(field(root, 'object'));
379
435
  if (!root || root.type !== 'identifier') return false;
@@ -558,4 +614,4 @@ function locateCall(root, start, end, type) {
558
614
  return node;
559
615
  }
560
616
 
561
- module.exports = { RequestHelperProof, JS_HTTP_PACKAGES, PY_HTTP_MODULES };
617
+ module.exports = { RequestHelperProof, JS_HTTP_PACKAGES, PY_HTTP_MODULES, pyImportedFunction };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * core/review-items.js - How much is there to look at?
3
+ *
4
+ * Unverified caller candidates are not all the same amount of work. A site
5
+ * the engine could not bind is one thing to check. A dispatch family (an
6
+ * interface call that may reach any implementation, a template the
7
+ * compiler instantiates) is ONE boundary with a known reason, however many
8
+ * sites it lists. A reader reviews the boundary, not each site.
9
+ *
10
+ * One counter, shared by the pre-commit check, the caller formatter and
11
+ * the oracle evaluation, so the three cannot disagree about the burden.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ /**
17
+ * Why a caller candidate is unverified, as one of three classes:
18
+ * 'runtime-dispatch' (the implementation is selected at runtime through a
19
+ * named boundary), 'compile-time-dispatch' (the compiler selects it),
20
+ * 'actionable-ambiguity' (something a reader can settle). The class the
21
+ * engine recorded wins; a possible-dispatch site with a named boundary is
22
+ * runtime dispatch.
23
+ */
24
+ function uncertaintyClassOf(site) {
25
+ return site.uncertaintyClass ||
26
+ (site.reason === 'possible-dispatch' && site.dispatchVia ? 'runtime-dispatch' : 'actionable-ambiguity');
27
+ }
28
+
29
+ /** Unverified entries by class: { actionable, runtime, compileTime }. */
30
+ function partitionUnverified(unverified) {
31
+ const parts = { actionable: [], runtime: [], compileTime: [] };
32
+ for (const site of Array.isArray(unverified) ? unverified : []) {
33
+ const kind = uncertaintyClassOf(site);
34
+ if (kind === 'runtime-dispatch') parts.runtime.push(site);
35
+ else if (kind === 'compile-time-dispatch') parts.compileTime.push(site);
36
+ else parts.actionable.push(site);
37
+ }
38
+ return parts;
39
+ }
40
+
41
+ /** Key of the family a runtime-dispatch site belongs to. */
42
+ function runtimeFamilyKey(site) {
43
+ return `${site.dispatchVia || ''}\0${site.dispatchCandidates ?? ''}\0` +
44
+ `${site.externalContract ? 'external' : 'project'}`;
45
+ }
46
+
47
+ /** Key of the family a compile-time-dispatch site belongs to. */
48
+ function compileTimeFamilyKey(site) {
49
+ return `${site.dispatchFamily || ''}\0${site.dispatchCandidates ?? ''}`;
50
+ }
51
+
52
+ /**
53
+ * @param {object[]} unverified - unverified caller entries
54
+ * @returns {{actionable, runtimeDispatchSites, runtimeDispatchGroups,
55
+ * compileTimeDispatchSites, compileTimeDispatchGroups, items}}
56
+ * `items` = actionable sites + one per dispatch family.
57
+ */
58
+ function reviewItemsOf(unverified) {
59
+ const { actionable: open, runtime, compileTime } = partitionUnverified(unverified);
60
+ const actionable = open.length;
61
+ const runtimeDispatchGroups = new Set(runtime.map(runtimeFamilyKey)).size;
62
+ const compileTimeDispatchGroups = new Set(compileTime.map(compileTimeFamilyKey)).size;
63
+ return {
64
+ actionable,
65
+ runtimeDispatchSites: runtime.length,
66
+ runtimeDispatchGroups,
67
+ compileTimeDispatchSites: compileTime.length,
68
+ compileTimeDispatchGroups,
69
+ items: actionable + runtimeDispatchGroups + compileTimeDispatchGroups,
70
+ };
71
+ }
72
+
73
+ module.exports = {
74
+ reviewItemsOf, uncertaintyClassOf, partitionUnverified, runtimeFamilyKey, compileTimeFamilyKey,
75
+ };