ucn 5.5.1 → 5.5.3

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 (97) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +40 -18
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +32 -21
  6. package/cli/index.js +2 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/bridge.js +15 -65
  12. package/core/brief.js +8 -4
  13. package/core/build-worker.js +2 -1
  14. package/core/cache.js +137 -6
  15. package/core/call-name-cache.js +134 -0
  16. package/core/callable-scope.js +21 -0
  17. package/core/callers.js +4726 -940
  18. package/core/check.js +22 -5
  19. package/core/class-identity.js +399 -12
  20. package/core/client-receivers.js +189 -0
  21. package/core/constructor-calls.js +831 -0
  22. package/core/constructor-hierarchy.js +147 -0
  23. package/core/constructor-protocol.js +196 -0
  24. package/core/contract-membership.js +346 -140
  25. package/core/cpp-conversion-constraints.js +123 -0
  26. package/core/cpp-conversion-syntax.js +216 -0
  27. package/core/cpp-conversions.js +414 -0
  28. package/core/cpp-scope.js +245 -44
  29. package/core/deadcode.js +61 -16
  30. package/core/decorator-binding.js +137 -3
  31. package/core/enum-reference-syntax.js +134 -0
  32. package/core/execute.js +17 -1
  33. package/core/external-macros.js +42 -10
  34. package/core/go-packages.js +171 -0
  35. package/core/graph-build.js +2 -0
  36. package/core/graph.js +75 -59
  37. package/core/ignore-rules.js +2 -1
  38. package/core/imports.js +145 -19
  39. package/core/index-ir.js +41 -4
  40. package/core/inprocess-clients.js +41 -15
  41. package/core/ir.js +27 -6
  42. package/core/macro-expansion.js +38 -2
  43. package/core/out-of-class-members.js +182 -0
  44. package/core/output/analysis-ext.js +4 -2
  45. package/core/output/analysis.js +89 -20
  46. package/core/output/graph.js +12 -3
  47. package/core/output/lines.js +4 -3
  48. package/core/output/public.js +2 -0
  49. package/core/output/reporting.js +12 -1
  50. package/core/output/shared.js +2 -0
  51. package/core/output/tracing.js +28 -22
  52. package/core/output-budget.js +2 -2
  53. package/core/parallel-build.js +52 -1
  54. package/core/project.js +217 -112
  55. package/core/provenance-constructor-base.js +161 -0
  56. package/core/provenance-facts.js +257 -27
  57. package/core/provenance-overload.js +458 -54
  58. package/core/provenance-type.js +487 -0
  59. package/core/provenance.js +234 -15
  60. package/core/public-command.js +17 -2
  61. package/core/query-bodies.js +159 -0
  62. package/core/reflection.js +270 -2
  63. package/core/rename-token-syntax.js +111 -0
  64. package/core/reporting.js +13 -7
  65. package/core/request-helpers.js +63 -7
  66. package/core/rust-macro-expansion.js +151 -19
  67. package/core/rust-modules.js +1 -1
  68. package/core/rust-names.js +607 -0
  69. package/core/search.js +70 -17
  70. package/core/shared.js +98 -1
  71. package/core/source-memo.js +201 -0
  72. package/core/text-usage-syntax.js +38 -0
  73. package/core/tracing.js +7 -1
  74. package/core/type-denotation.js +124 -37
  75. package/core/type-references.js +615 -246
  76. package/core/value-references.js +2777 -0
  77. package/core/verify.js +1097 -281
  78. package/languages/adapter.js +28 -4
  79. package/languages/c-family.js +1118 -164
  80. package/languages/constructor-bases.js +105 -0
  81. package/languages/cpp-platform.js +26 -0
  82. package/languages/cpp-site-syntax.js +83 -0
  83. package/languages/csharp-scope.js +439 -0
  84. package/languages/csharp.js +428 -68
  85. package/languages/go.js +385 -207
  86. package/languages/html.js +4 -2
  87. package/languages/index.js +397 -2
  88. package/languages/java-platform.js +226 -0
  89. package/languages/java-unicode.js +269 -0
  90. package/languages/java.js +461 -117
  91. package/languages/javascript.js +418 -66
  92. package/languages/lexical-scope.js +190 -23
  93. package/languages/python.js +404 -54
  94. package/languages/rust.js +510 -82
  95. package/languages/utils.js +67 -8
  96. package/mcp/server.js +8 -2
  97. package/package.json +3 -3
@@ -222,6 +222,33 @@ function unverifiedCalleeLines(entries, compact) {
222
222
  return lines;
223
223
  }
224
224
 
225
+ /**
226
+ * " (constructs X)" for a constructor body edge beside its class callee (fix
227
+ * #421); " (build alternative of F:L)" for a Go declaration of the callee in
228
+ * a file of another build constraint (fix #425).
229
+ */
230
+ function constructsSuffix(callee) {
231
+ if (callee?.initializerOf) return ` (constructs ${callee.initializerOf})`;
232
+ return callee?.configurationAlternativeOf ? ` (build alternative of ${callee.configurationAlternativeOf})` : '';
233
+ }
234
+
235
+ /**
236
+ * Constructions whose class edge is confirmed while the constructor body
237
+ * they run is undecided (fix #421). Their sites are in the class edge's
238
+ * account; these lines say which body is not proven.
239
+ */
240
+ function initializerReviewLines(entries, compact) {
241
+ if (!entries || entries.length === 0) return [];
242
+ const lines = [`${compact ? '' : '\n'}CONSTRUCTOR BODIES — UNDECIDED (${entries.length}) — class dependency confirmed above; which constructor runs is not proven:`];
243
+ for (const entry of entries) {
244
+ const candidates = entry.candidates > 1 ? ` (${entry.candidates} candidates)` : '';
245
+ const sites = entry.sites?.length ? ` L${entry.sites.join(',L')}` : '';
246
+ const label = entry.name === entry.initializerOf ? entry.name : `${entry.initializerOf}.${entry.name}`;
247
+ lines.push(` ${label} ×${entry.callCount} — ${entry.reason}${candidates}${sites}`);
248
+ }
249
+ return lines;
250
+ }
251
+
225
252
  /** "NON-CALL OCCURRENCES" summary line from the account. */
226
253
  function formatNonCallLine(account, hintName, usagesHint) {
227
254
  if (!account || !account.nonCall || account.nonCall.total === 0) return null;
@@ -388,11 +415,14 @@ function formatContextJson(context) {
388
415
  ...(c.tier && { tier: c.tier }),
389
416
  ...(c.sites && { sites: c.sites }),
390
417
  ...(c.functionReference && { functionReference: true }),
418
+ ...(c.initializerOf && { initializerOf: c.initializerOf }),
419
+ ...(c.configurationAlternativeOf && { configurationAlternativeOf: c.configurationAlternativeOf }),
391
420
  ...(c.reachable !== undefined && { reachable: c.reachable }),
392
421
  })),
393
422
  // v4 callee contract: visible unresolved-call entries (aggregated
394
423
  // name+reason), reconciled by meta.calleeAccount.
395
424
  unverifiedCallees: context.unverifiedCallees || [],
425
+ ...(context.initializerReviews?.length && { initializerReviews: context.initializerReviews }),
396
426
  ...(context.warnings && { warnings: context.warnings })
397
427
  }
398
428
  }, provenanceReplacer);
@@ -747,9 +777,9 @@ function formatContext(ctx, options = {}) {
747
777
  if (compact) {
748
778
  const snip = c.docstring ? calleeDocstringSnippet(c.docstring) : '';
749
779
  const docPart = snip ? `: ${snip}` : '';
750
- lines.push(` [${itemNum}] ${c.name}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${docPart}${unreachableMark}`);
780
+ lines.push(` [${itemNum}] ${c.name}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${constructsSuffix(c)}${docPart}${unreachableMark}`);
751
781
  } else {
752
- lines.push(` [${itemNum}] ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${unreachableMark}`);
782
+ lines.push(` [${itemNum}] ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${constructsSuffix(c)}${unreachableMark}`);
753
783
  if (c.docstring) {
754
784
  const snip = calleeDocstringSnippet(c.docstring);
755
785
  if (snip) lines.push(` "${snip}"`);
@@ -770,6 +800,7 @@ function formatContext(ctx, options = {}) {
770
800
  // Unverified callee band (v4): aggregated name+reason entries — visible,
771
801
  // never silently dropped. Not expandable (no definition resolved).
772
802
  lines.push(...unverifiedCalleeLines(ctx.unverifiedCallees, compact));
803
+ lines.push(...initializerReviewLines(ctx.initializerReviews, compact));
773
804
 
774
805
  // Conservation contract lines: non-call summary + ACCOUNT/WARNING/FILTERED
775
806
  const account = ctx.meta && ctx.meta.account;
@@ -793,6 +824,38 @@ function formatContext(ctx, options = {}) {
793
824
  }
794
825
 
795
826
  /** Format impact command output - text. Shows what would need updating if a function signature changes. */
827
+ /**
828
+ * The value band's token account (fix #420): every name token of the
829
+ * scanned lines is an access (confirmed, unverified, excluded by reason) or
830
+ * a declaration token.
831
+ */
832
+ function formatValueAccount(name, band) {
833
+ const account = band.account;
834
+ const reasons = Object.entries(band.excluded?.byReason || {})
835
+ .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
836
+ .slice(0, 4).map(([reason, count]) => `${reason} ${count}`).join(', ');
837
+ return `VALUE ACCOUNT: "${name}" is ${account.tokens} name token(s) on ${account.lines} line(s) in ` +
838
+ `${account.files} file(s): ${account.confirmed} confirmed, ${account.unverified} unverified, ` +
839
+ `${account.excluded} excluded${reasons ? ` (${reasons})` : ''}, ${account.declarations} declaration` +
840
+ `${account.textLines ? `; ${account.textLines} text-only line(s)` : ''}` +
841
+ `${account.conserved ? '' : ' [NOT CONSERVED]'}`;
842
+ }
843
+
844
+ function formatTypeAccount(name, band) {
845
+ const account = band.account;
846
+ const reasons = Object.entries(band.excluded?.byReason || {})
847
+ .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
848
+ .slice(0, 4).map(([reason, count]) => `${reason} ${count}`).join(', ');
849
+ return `TYPE ACCOUNT: "${name}" is ${account.tokens} name token(s) on ${account.lines} line(s) in ` +
850
+ `${account.files} file(s): ${account.confirmed} confirmed, ${account.unverified} unverified, ` +
851
+ `${account.excluded} excluded${reasons ? ` (${reasons})` : ''}, ${account.constructions} at call sites, ` +
852
+ `${account.declarations} declaration` +
853
+ `${account.attributeTokens ? ` (${account.attributeTokens} attribute short name(s))` : ''}` +
854
+ `${account.unparsedLines ? `; ${account.unparsedLines} unparsed line(s)` : ''}` +
855
+ `${account.textLines ? `; ${account.textLines} text-only line(s)` : ''}` +
856
+ `${account.conserved ? '' : ' [NOT CONSERVED]'}`;
857
+ }
858
+
796
859
  function formatImpact(impact, options = {}) {
797
860
  if (!impact) {
798
861
  return 'Function not found.';
@@ -814,8 +877,8 @@ function formatImpact(impact, options = {}) {
814
877
  // Summary (confirmed + unverified tiers reported separately)
815
878
  const impactUnverified = impact.unverifiedSites || [];
816
879
  const unverifiedSuffix = impactUnverified.length > 0 ? ` confirmed + ${impactUnverified.length} unverified` : '';
817
- if (impact.propertyAccesses || impact.typeReferences) {
818
- const pa = impact.propertyAccesses || impact.typeReferences;
880
+ if (impact.propertyAccesses || impact.valueAccesses || impact.typeReferences) {
881
+ const pa = impact.propertyAccesses || impact.valueAccesses || impact.typeReferences;
819
882
  const uv = pa.unverifiedCount ? ` + ${pa.unverifiedCount} unverified` : '';
820
883
  lines.push(`DEPENDENCY SITES: ${impact.totalDependencySites} confirmed${uv}`);
821
884
  }
@@ -891,9 +954,10 @@ function formatImpact(impact, options = {}) {
891
954
  }
892
955
  if (impactReach.note && !compact) lines.push(impactReach.note);
893
956
 
894
- if (impact.propertyAccesses) {
895
- const access = impact.propertyAccesses;
896
- lines.push(`${compact ? '' : '\n'}PROPERTY ACCESS SITES: ${access.confirmedCount} confirmed` +
957
+ if (impact.propertyAccesses || impact.valueAccesses) {
958
+ const access = impact.propertyAccesses || impact.valueAccesses;
959
+ const label = impact.propertyAccesses ? 'PROPERTY' : 'VALUE';
960
+ lines.push(`${compact ? '' : '\n'}${label} ACCESS SITES: ${access.confirmedCount} confirmed` +
897
961
  (access.unverifiedCount ? ` + ${access.unverifiedCount} unverified` : ''));
898
962
  for (const group of access.byFile) {
899
963
  for (const site of group.sites) {
@@ -903,44 +967,47 @@ function formatImpact(impact, options = {}) {
903
967
  }
904
968
  }
905
969
  if (access.unverifiedSites.length > 0) {
906
- lines.push(`${compact ? '' : '\n'}UNVERIFIED PROPERTY ACCESS CANDIDATES (${access.unverifiedSites.length}) — matching attribute syntax, receiver identity unresolved:`);
970
+ const reason = impact.propertyAccesses ? 'matching attribute syntax, receiver identity unresolved'
971
+ : 'matching value syntax, binding identity unresolved';
972
+ lines.push(`${compact ? '' : '\n'}UNVERIFIED ${label} ACCESS CANDIDATES (${access.unverifiedSites.length}) — ${reason}:`);
907
973
  for (const site of access.unverifiedSites.slice(0, 10)) {
908
974
  const caller = site.callerName ? ` [${site.callerName}]` : '';
909
975
  const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
910
- lines.push(` ${site.file}:${site.line}${caller} [${site.accessKind || 'access'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}]${expr}`);
976
+ const why = impact.valueAccesses && site.reason ? ` (${site.reason})` : '';
977
+ lines.push(` ${site.file}:${site.line}${caller} [${site.accessKind || 'access'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}]${expr}${why}`);
911
978
  }
912
979
  if (access.unverifiedSites.length > 10) {
913
980
  lines.push(` (+${access.unverifiedSites.length - 10} more unverified)`);
914
981
  }
915
982
  }
983
+ const valueAccount = impact.valueAccesses?.account;
984
+ if (valueAccount) lines.push(formatValueAccount(impact.function, impact.valueAccesses));
916
985
  }
917
986
 
918
- // fix #345: annotation sites of a type-kind definition, tiered like the
919
- // accessor band. The headline no longer says 0 for a type with dependents.
987
+ // fixes #345, #428: the tokens naming a type outside its call sites,
988
+ // tiered by the type lookup, with their own account.
920
989
  if (impact.typeReferences) {
921
990
  const refs = impact.typeReferences;
922
991
  lines.push(`${compact ? '' : '\n'}TYPE REFERENCE SITES: ${refs.confirmedCount} confirmed` +
923
- (refs.unverifiedCount ? ` + ${refs.unverifiedCount} unverified` : '') +
924
- (refs.excluded?.total ? ` (${refs.excluded.total} other-target)` : ''));
992
+ (refs.unverifiedCount ? ` + ${refs.unverifiedCount} unverified` : ''));
993
+ const position = site => `${site.kind || 'type'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}`;
925
994
  for (const group of refs.byFile) {
926
995
  for (const site of group.sites) {
927
996
  const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
928
- lines.push(` ${group.file}:${site.line}${expr}`);
997
+ lines.push(` ${group.file}:${site.line} [${position(site)}]${expr}`);
929
998
  }
930
999
  }
931
1000
  if (refs.unverifiedSites.length > 0) {
932
- lines.push(`${compact ? '' : '\n'}UNVERIFIED TYPE REFERENCE CANDIDATES (${refs.unverifiedSites.length}) — name matches, no import link to this definition:`);
1001
+ lines.push(`${compact ? '' : '\n'}UNVERIFIED TYPE REFERENCE CANDIDATES (${refs.unverifiedSites.length}) — the name, the type lookup cannot decide what it names:`);
933
1002
  for (const site of refs.unverifiedSites.slice(0, 10)) {
934
1003
  const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
935
- lines.push(` ${site.file}:${site.line}${expr} (${site.reason})`);
1004
+ lines.push(` ${site.file}:${site.line} [${position(site)}]${expr} (${site.reason})`);
936
1005
  }
937
1006
  if (refs.unverifiedSites.length > 10) {
938
1007
  lines.push(` (+${refs.unverifiedSites.length - 10} more unverified)`);
939
1008
  }
940
1009
  }
941
- if (refs.confirmedCount === 0 && refs.unverifiedCount === 0) {
942
- lines.push(' (no annotation sites outside the definition)');
943
- }
1010
+ if (refs.account) lines.push(formatTypeAccount(impact.function, refs));
944
1011
  }
945
1012
 
946
1013
  // Unverified tier: visible, capped at 10 one-liners
@@ -1132,7 +1199,7 @@ function formatAbout(about, options = {}) {
1132
1199
  const returnSuffix = c.returnType ? ` → ${c.returnType}` : '';
1133
1200
  const sideEffects = (c.sideEffects && c.sideEffects.length) ? ` {${c.sideEffects.join(',')}}` : '';
1134
1201
  const unreachableMark = (aboutCalleeReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
1135
- lines.push(` ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.file}:${c.line} (${c.callCount}x)${unreachableMark}`);
1202
+ lines.push(` ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.file}:${c.line} (${c.callCount}x)${constructsSuffix(c)}${unreachableMark}`);
1136
1203
  if (!compact && c.docstring) {
1137
1204
  const snip = calleeDocstringSnippet(c.docstring);
1138
1205
  if (snip) lines.push(` "${snip}"`);
@@ -1261,4 +1328,6 @@ module.exports = {
1261
1328
  // Shared with output/analysis-ext.js (smart): single-node callee contract.
1262
1329
  formatCalleeAccountLine,
1263
1330
  unverifiedCalleeLines,
1331
+ initializerReviewLines,
1332
+ constructsSuffix,
1264
1333
  };
@@ -443,8 +443,9 @@ function formatCircularDeps(result) {
443
443
  return lines.join('\n');
444
444
  }
445
445
 
446
- const eager = result.cycles.filter(cycle => cycle.classification !== 'deferred');
446
+ const eager = result.cycles.filter(cycle => cycle.classification !== 'deferred' && cycle.classification !== 'static');
447
447
  const deferred = result.cycles.filter(cycle => cycle.classification === 'deferred');
448
+ const staticCycles = result.cycles.filter(cycle => cycle.classification === 'static');
448
449
 
449
450
  // Groups first: a strongly connected file set is the unit a refactor has
450
451
  // to break, and a 14-file tangle can hold hundreds of elementary cycles.
@@ -454,8 +455,12 @@ function formatCircularDeps(result) {
454
455
  lines.push(`CYCLE GROUPS (${groups.length}) — every member reaches every other member:`);
455
456
  for (const group of groups) {
456
457
  const counts = [];
457
- if (group.eagerCycles != null) counts.push(`${group.eagerCycles} import-time`);
458
- if (group.deferredCycles != null) counts.push(`${group.deferredCycles} deferred`);
458
+ // A group of files that run no module code on import has only
459
+ // static cycles; the import-time/deferred split does not apply.
460
+ const onlyStatic = group.staticCycles > 0 && !group.eagerCycles && !group.deferredCycles;
461
+ if (group.eagerCycles != null && !onlyStatic) counts.push(`${group.eagerCycles} import-time`);
462
+ if (group.deferredCycles != null && !onlyStatic) counts.push(`${group.deferredCycles} deferred`);
463
+ if (group.staticCycles > 0) counts.push(`${group.staticCycles} static`);
459
464
  const suffix = counts.length > 0 ? ` [${counts.join(', ')}${result.summary.truncated ? '; enumerated counts only' : ''}]` : '';
460
465
  lines.push(` ${group.size} files: ${group.files.join(', ')}${suffix}`);
461
466
  }
@@ -489,6 +494,7 @@ function formatCircularDeps(result) {
489
494
  };
490
495
  renderGroup('IMPORT-TIME CYCLES', eager, false, EAGER_CYCLE_DISPLAY_LIMIT);
491
496
  renderGroup('DEFERRED CYCLES', deferred, true, DEFERRED_CYCLE_DISPLAY_LIMIT);
497
+ renderGroup('STATIC CYCLES', staticCycles, false, EAGER_CYCLE_DISPLAY_LIMIT);
492
498
 
493
499
  lines.push('');
494
500
  const { totalCycles, filesInCycles } = result.summary;
@@ -502,6 +508,9 @@ function formatCircularDeps(result) {
502
508
  }
503
509
  lines.push('The CYCLE GROUPS list and files-in-cycles count are complete; enumerated cycle counts are lower bounds.');
504
510
  }
511
+ if (staticCycles.length > 0) {
512
+ lines.push(`${staticCycles.length} chain${staticCycles.length === 1 ? ' links' : 's link'} only files whose imports and includes run no module code (Go, Rust, Java, C#, C, C++): static dependency cycles, not import-time cycles.`);
513
+ }
505
514
  if (deferred.length > 0) {
506
515
  lines.push(`${deferred.length} chain${deferred.length === 1 ? '' : 's'} close only through deferred edges (function-local, TYPE_CHECKING-only, or type-only imports); they are not unconditional import-time cycles, but a function-local edge may still matter if invoked during initialization.`);
507
516
  }
@@ -244,15 +244,16 @@ function impactRecords(result) {
244
244
  }
245
245
  if (result.typeReferences) {
246
246
  const refs = result.typeReferences;
247
+ const where = site => `type-reference: ${site.kind || 'type'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}`;
247
248
  for (const group of refs.byFile || []) {
248
249
  for (const site of group.sites || []) {
249
- out.push(record(group.file, site.line, site.expression, 'type-reference'));
250
+ out.push(record(group.file, site.line, site.expression, where(site)));
250
251
  }
251
252
  }
252
253
  for (const site of refs.unverifiedSites || []) {
253
- out.push(record(pathOf(site), site.line, site.expression, `unverified: ${site.reason}; type-reference`));
254
+ out.push(record(pathOf(site), site.line, site.expression, `unverified: ${site.reason}; ${where(site)}`));
254
255
  }
255
- notes.push(`# TYPE REFERENCE SITES: ${refs.confirmedCount} confirmed, ${refs.unverifiedCount} unverified, ${refs.excluded?.total || 0} other-target (separate from caller ACCOUNT).`);
256
+ notes.push(`# TYPE REFERENCE SITES: ${refs.confirmedCount} confirmed, ${refs.unverifiedCount} unverified, ${refs.excluded?.total || 0} excluded (separate from caller ACCOUNT).`);
256
257
  }
257
258
  notes.push(...accountComments(result.account));
258
259
  for (const warning of result.warnings || []) notes.push(...commentLines(warning.message));
@@ -410,6 +410,7 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
410
410
  commandMeta.excludedDecorated = result.excludedDecorated || 0;
411
411
  commandMeta.excludedExternalContract = result.excludedExternalContract || 0;
412
412
  commandMeta.excludedRuntimeContract = result.excludedRuntimeContract || 0;
413
+ commandMeta.excludedConstructors = result.excludedConstructors || 0;
413
414
  commandMeta.pythonImplicitExportFiles = result.pythonImplicitExportFiles || 0;
414
415
  commandMeta.excludedDynamicDispatch = result.excludedDynamicDispatch || 0;
415
416
  commandMeta.computedDispatch = result.computedDispatch || { count: 0, names: [] };
@@ -475,6 +476,7 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
475
476
  const envelope = {
476
477
  meta: {
477
478
  command: surfaceCommand,
479
+ ...(execution.surface === 'cli' && { maxChars: 'not-applied' }),
478
480
  ...(surfaceCommand !== command && { canonicalCommand: command }),
479
481
  // A result that self-reports it could not run (check with a bad
480
482
  // base ref / outside git) must be machine-distinguishable from a
@@ -214,8 +214,9 @@ function formatStatsJson(stats) {
214
214
  function formatDeadcode(results, options = {}) {
215
215
  if (results.length === 0 && !results.excludedDecorated &&
216
216
  !results.excludedExported && !results.excludedExternalContract &&
217
- !results.excludedRuntimeContract && !results.pythonImplicitExportFiles &&
217
+ !results.excludedRuntimeContract && !results.excludedConstructors && !results.pythonImplicitExportFiles &&
218
218
  !results.excludedDynamicDispatch && !results.computedDispatch?.count &&
219
+ !results.reflection?.withheld && !results.reflection?.dynamicCount && !results.reflection?.literalCount &&
219
220
  results.coverage?.complete !== false) {
220
221
  return 'No dead code found.';
221
222
  }
@@ -291,6 +292,9 @@ function formatDeadcode(results, options = {}) {
291
292
  if (results.excludedRuntimeContract > 0) {
292
293
  lines.push(`\n${results.excludedRuntimeContract} runtime callback(s) hidden (language/runtime registrations, not dead).`);
293
294
  }
295
+ if (results.excludedConstructors > 0) {
296
+ lines.push(`\n${results.excludedConstructors} constructor(s) not listed: a constructor belongs to its type's allocation protocol, so the type is audited instead.`);
297
+ }
294
298
  if (results.pythonImplicitExportFiles > 0) {
295
299
  lines.push(`\nPython public-surface rule active in ${results.pythonImplicitExportFiles} file(s) without __all__: top-level non-underscore names are treated as externally reachable.`);
296
300
  }
@@ -311,6 +315,12 @@ function formatDeadcode(results, options = {}) {
311
315
  if (results.reflection?.dynamicCount > 0) {
312
316
  lines.push(`\nWARNING: ${results.reflection.dynamicCount} dynamic reflection use(s) have no static member spelling. Dead-code results remain review candidates because runtime-selected members cannot be attributed.`);
313
317
  }
318
+ if (results.reflection?.withheldByTable?.length > 0) {
319
+ lines.push('\nConservatively withheld: exact string literals in a file with dynamic reflection (flow to the reflection call is unproven):');
320
+ for (const site of results.reflection.withheldByTable) {
321
+ lines.push(` ${site.name}: named in possible reflection table ${site.file}:${site.line}`);
322
+ }
323
+ }
314
324
  if (results.macroPaste) {
315
325
  const m = results.macroPaste;
316
326
  if (m.withheld > 0) {
@@ -366,6 +376,7 @@ function formatDeadcodeJson(results) {
366
376
  ...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
367
377
  ...(results.excludedExternalContract > 0 && { excludedExternalContract: results.excludedExternalContract }),
368
378
  ...(results.excludedRuntimeContract > 0 && { excludedRuntimeContract: results.excludedRuntimeContract }),
379
+ ...(results.excludedConstructors > 0 && { excludedConstructors: results.excludedConstructors }),
369
380
  ...(results.pythonImplicitExportFiles > 0 && { pythonImplicitExportFiles: results.pythonImplicitExportFiles }),
370
381
  ...(results.excludedDynamicDispatch > 0 && { excludedDynamicDispatch: results.excludedDynamicDispatch }),
371
382
  ...(results.computedDispatch?.count > 0 && { computedDispatch: results.computedDispatch }),
@@ -177,6 +177,8 @@ function formatClassSignature(cls) {
177
177
  if (cls.implements && cls.implements.length > 0) {
178
178
  parts.push(`implements ${cls.implements.join(', ')}`);
179
179
  }
180
+ // `class State;` declares a class defined elsewhere (fix #427).
181
+ if (cls.forwardDeclaration) parts.push('(forward declaration)');
180
182
 
181
183
  return parts.join(' ');
182
184
  }
@@ -39,18 +39,31 @@ function renderFrontier(lines, frontier, options = {}, expanded = false) {
39
39
  return true;
40
40
  }
41
41
 
42
- /** Render a node's unresolved callee calls as [unverified] leaves. */
43
- function renderUnverifiedCallees(lines, node, prefix, isParentLast) {
44
- if (!node.unverifiedCallees || node.unverifiedCallees.length === 0) return;
45
- const extension = isParentLast ? ' ' : '│ ';
46
- for (let i = 0; i < node.unverifiedCallees.length; i++) {
47
- const u = node.unverifiedCallees[i];
48
- const isLast = i === node.unverifiedCallees.length - 1;
49
- const connector = isLast ? '└── ' : '├── ';
42
+ /**
43
+ * A node's unresolved callee calls as [unverified] leaves, then the
44
+ * constructions whose constructor body is undecided (fix #421; their class
45
+ * edge is a child of the node).
46
+ */
47
+ function undecidedLeafLines(node) {
48
+ const out = [];
49
+ for (const u of node.unverifiedCallees || []) {
50
50
  const owners = u.ownerCount > 1 ? ` (${u.ownerCount} owners)` : '';
51
51
  const linesPart = u.sites && u.sites.length > 0 ? ` L${u.sites.join(',L')}` : '';
52
- lines.push(`${prefix}${extension}${connector}[unverified] ${u.name} — ${u.reason}${owners}${linesPart}`);
52
+ out.push(`[unverified] ${u.name} — ${u.reason}${owners}${linesPart}`);
53
+ }
54
+ for (const r of node.initializerReviews || []) {
55
+ const candidates = r.candidates > 1 ? ` (${r.candidates} candidates)` : '';
56
+ const linesPart = r.sites && r.sites.length > 0 ? ` L${r.sites.join(',L')}` : '';
57
+ const label = r.name === r.initializerOf ? r.name : `${r.initializerOf}.${r.name}`;
58
+ out.push(`[constructor undecided] ${label} — ${r.reason}${candidates}${linesPart}`);
53
59
  }
60
+ return out;
61
+ }
62
+
63
+ function renderUnverifiedCallees(lines, node, prefix, isParentLast) {
64
+ const leaves = undecidedLeafLines(node);
65
+ const extension = isParentLast ? ' ' : '│ ';
66
+ leaves.forEach((text, i) => lines.push(`${prefix}${extension}${i === leaves.length - 1 ? '└── ' : '├── '}${text}`));
54
67
  }
55
68
 
56
69
  /** TREE ACCOUNT line for caller-direction trees. */
@@ -129,6 +142,7 @@ function formatTrace(trace, options = {}) {
129
142
  } else if (node.file) {
130
143
  label += ` (${node.file}:${node.line})`;
131
144
  }
145
+ if (node.constructs) label += ` (constructs ${node.constructs})`;
132
146
  if (node.weight && node.weight !== 'normal') {
133
147
  label += ` [${node.weight}]`;
134
148
  }
@@ -143,7 +157,7 @@ function formatTrace(trace, options = {}) {
143
157
 
144
158
  if (node.children && !node.alreadyShown) {
145
159
  const hasMore = node.truncatedChildren > 0;
146
- const hasUnverified = node.unverifiedCallees && node.unverifiedCallees.length > 0;
160
+ const hasUnverified = node.unverifiedCallees?.length > 0 || node.initializerReviews?.length > 0;
147
161
  for (let i = 0; i < node.children.length; i++) {
148
162
  const isChildLast = !hasMore && !hasUnverified && i === node.children.length - 1;
149
163
  renderNode(node.children[i], prefix + extension, isChildLast);
@@ -160,7 +174,8 @@ function formatTrace(trace, options = {}) {
160
174
  lines.push(trace.root);
161
175
  if (trace.tree && trace.tree.children) {
162
176
  const rootHasMore = trace.tree.truncatedChildren > 0;
163
- const rootHasUnverified = trace.tree.unverifiedCallees && trace.tree.unverifiedCallees.length > 0;
177
+ const rootLeaves = undecidedLeafLines(trace.tree);
178
+ const rootHasUnverified = rootLeaves.length > 0;
164
179
  for (let i = 0; i < trace.tree.children.length; i++) {
165
180
  const isLast = !rootHasMore && !rootHasUnverified && i === trace.tree.children.length - 1;
166
181
  renderNode(trace.tree.children[i], '', isLast);
@@ -169,17 +184,8 @@ function formatTrace(trace, options = {}) {
169
184
  hasTruncation = true;
170
185
  lines.push((rootHasUnverified ? '├── ' : '└── ') + `... and ${trace.tree.truncatedChildren} more callees`);
171
186
  }
172
- if (rootHasUnverified) {
173
- // Root-level unverified callees: render with no prefix
174
- for (let i = 0; i < trace.tree.unverifiedCallees.length; i++) {
175
- const u = trace.tree.unverifiedCallees[i];
176
- const isLast = i === trace.tree.unverifiedCallees.length - 1;
177
- const connector = isLast ? '└── ' : '├── ';
178
- const owners = u.ownerCount > 1 ? ` (${u.ownerCount} owners)` : '';
179
- const linesPart = u.sites && u.sites.length > 0 ? ` L${u.sites.join(',L')}` : '';
180
- lines.push(`${connector}[unverified] ${u.name} — ${u.reason}${owners}${linesPart}`);
181
- }
182
- }
187
+ // Root-level unverified callees: render with no prefix
188
+ rootLeaves.forEach((text, i) => lines.push(`${i === rootLeaves.length - 1 ? '└── ' : '├── '}${text}`));
183
189
  }
184
190
 
185
191
  // Callers section
@@ -15,10 +15,10 @@ const BROAD_COMMANDS = new Set([
15
15
  ...[...BROAD_CANONICAL].map(toMcpName),
16
16
  ]);
17
17
 
18
- const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|Note):|\d+ test-file usage\(s\) hidden\b|Found \d+ (?:definitions|fuzzy matches)\b)/;
18
+ const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|VALUE ACCOUNT|TYPE ACCOUNT|Note):|\d+ test-file usage\(s\) hidden\b|Found \d+ (?:definitions|fuzzy matches)\b)/;
19
19
  // Trust lines: the accounting and contract lines that qualify a caller
20
20
  // answer. A truncated answer carries every one of them or none of its body.
21
- const TRUST_LINE_RE = /^(?:ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):/;
21
+ const TRUST_LINE_RE = /^(?:ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|VALUE ACCOUNT|TYPE ACCOUNT):/;
22
22
  const MAX_PRESERVED_CONTRACT_LINES = 24;
23
23
  const MAX_PRESERVED_CONTRACT_CHARS = 8000;
24
24
 
@@ -275,4 +275,55 @@ function parallelBuild(index, files, options = {}) {
275
275
  return changed;
276
276
  }
277
277
 
278
- module.exports = { parallelBuild, scheduleFiles };
278
+ /**
279
+ * Worker threads for a sequential build's Rust macro expansion (fix #422): a
280
+ * pool shaped like the one a parallel build keeps (`options.onParsed`), whose
281
+ * workers have no file to parse and wait for their expansion job. A crate
282
+ * whose API macros generate most of its code has little source to parse and
283
+ * much expansion work.
284
+ */
285
+ function spawnExpansionPool(index, count) {
286
+ if (!(count >= 1)) return null;
287
+ const sab = new SharedArrayBuffer(4 * (count + 1));
288
+ const control = new SharedArrayBuffer(4 * count);
289
+ const controlArray = new Int32Array(control);
290
+ const ports = [];
291
+ const workers = [];
292
+ for (let i = 0; i < count; i++) {
293
+ const { port1, port2 } = new MessageChannel();
294
+ ports.push(port1);
295
+ const worker = new Worker(path.join(__dirname, 'build-worker.js'), {
296
+ workerData: {
297
+ files: [], rootDir: index.root, existingHashes: {}, signal: sab, workerIndex: i,
298
+ queueIndex: count, port: port2, control, macroDictionary: null, dictionarySignal: null,
299
+ },
300
+ transferList: [port2],
301
+ resourceLimits: { maxYoungGenerationSizeMb: WORKER_YOUNG_GENERATION_MB },
302
+ });
303
+ worker.unref();
304
+ workers.push(worker);
305
+ }
306
+ const live = new Set(workers.map((_, i) => i));
307
+ const retire = i => {
308
+ if (!live.delete(i)) return;
309
+ Atomics.store(controlArray, i, 1);
310
+ Atomics.notify(controlArray, i);
311
+ ports[i].close();
312
+ workers[i].terminate();
313
+ };
314
+ return {
315
+ available: [...live],
316
+ ports,
317
+ post(i, job) {
318
+ ports[i].postMessage(job);
319
+ Atomics.store(controlArray, i, 2);
320
+ Atomics.notify(controlArray, i);
321
+ },
322
+ retire,
323
+ dispose() {
324
+ for (const i of [...live]) retire(i);
325
+ },
326
+ };
327
+ }
328
+
329
+ module.exports = { parallelBuild, scheduleFiles, spawnExpansionPool };