ucn 5.5.1 → 5.5.2

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 (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  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 +30 -21
  6. package/cli/index.js +1 -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/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +137 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +4726 -940
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +217 -112
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +151 -19
  63. package/core/rust-modules.js +1 -1
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +124 -37
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +510 -82
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. package/package.json +2 -2
@@ -384,6 +384,37 @@ function macroBodyTokens(index, def) {
384
384
  return expansionState(index).tokensOf(def);
385
385
  }
386
386
 
387
+ class DeclarationChoice extends Error {
388
+ constructor(name, definitions) { super(name); this.definitions = definitions; }
389
+ }
390
+
391
+ /** Expand every bounded macro configuration of a declaration fragment. */
392
+ function expandDeclaration(index, file, line, source) {
393
+ const state = expansionState(index);
394
+ const pending = [new Map()], variants = [];
395
+ while (pending.length) {
396
+ const choices = pending.pop();
397
+ const site = { file, line, lines: index._getFileLines(file), fileState: {},
398
+ lookupMemo: new Map(), usedDefs: new Set(), budget: MAX_TOKENS, ambiguous: false,
399
+ declarationChoices: choices };
400
+ try {
401
+ const tokens = expandTokens(index, state, site, lexPP(source), new Set(), 0);
402
+ const text = tokens.map(token => token.v).join(' ');
403
+ const complete = !site.ambiguous && !tokens.some(token => token.opaque || token.noexpand);
404
+ if (!variants.some(variant => variant.text === text)) variants.push({ text, complete });
405
+ } catch (error) {
406
+ if (error instanceof DeclarationChoice) {
407
+ if (pending.length + variants.length + error.definitions.length > MAX_VARIANTS) {
408
+ variants.push({ text: source, complete: false }); break;
409
+ }
410
+ for (const definition of error.definitions) pending.push(new Map(choices).set(error.message, definition));
411
+ } else if (error instanceof BlindExpansion) variants.push({ text: source, complete: false });
412
+ else throw error;
413
+ }
414
+ }
415
+ return { ...variants[0], complete: variants.length === 1 && variants[0].complete, variants };
416
+ }
417
+
387
418
  /** Does an invocation with `args` fit the macro's parameter list? */
388
419
  function arityFits(def, args) {
389
420
  const params = def.ppParams || [];
@@ -508,7 +539,12 @@ function expandTokens(index, state, site, tokens, hide, depth) {
508
539
  out.push({ ...t, noexpand: true });
509
540
  continue;
510
541
  }
511
- const scope = definitionsInScope(index, state, site, t.v);
542
+ let scope = definitionsInScope(index, state, site, t.v);
543
+ if (site.declarationChoices && scope.defs.length > 1 && !scope.truncated) {
544
+ const chosen = site.declarationChoices.get(t.v);
545
+ if (!chosen) throw new DeclarationChoice(t.v, scope.defs);
546
+ scope = { defs: [chosen], ambiguous: false, truncated: false };
547
+ }
512
548
  let def = scope.defs[0];
513
549
  if (!def) {
514
550
  out.push(t);
@@ -1413,4 +1449,4 @@ function macroExpansionSummary(index) {
1413
1449
  }
1414
1450
 
1415
1451
  module.exports = { withMacroExpansion, macroExpansionSummary, expansionState, persistableMacroExpansion,
1416
- macroDefinitionsAt, macroBodyTokens, codeTokens, pastedNameSites };
1452
+ macroDefinitionsAt, macroBodyTokens, expandDeclaration, codeTokens, pastedNameSites };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * core/out-of-class-members.js - constructors defined outside their class
5
+ * body (fix #421): the C++ header + source layout, where `W::W(int)` in
6
+ * w.cpp defines the `W(int)` its class declares in w.h (trait
7
+ * `outOfClassMemberDefinitions`).
8
+ *
9
+ * The constructor family of a class is its in-class declarations; an
10
+ * out-of-class definition holds the body of exactly one of them. Linkage is
11
+ * by the owner's scope path, textual visibility of the class (same file or
12
+ * include closure) and the declaration's parameter signature. Anything less
13
+ * certain links nothing: the caller keeps its review result.
14
+ */
15
+
16
+ const { langTraits } = require('../languages');
17
+
18
+ const OWNER_KINDS = new Set(['class', 'struct', 'union']);
19
+
20
+ const scopePath = def => [def.namespace, def.enclosingType].filter(Boolean).join('::');
21
+
22
+ const outOfClassLanguage = (index, file) => !!langTraits(index.files.get(file)?.language)?.outOfClassMemberDefinitions;
23
+ const applies = (index, def) => !!def?.className && outOfClassLanguage(index, def.file);
24
+
25
+ function memo(index, name) {
26
+ return index.isProvenanceFactIndex ? null : index._opMemo?.(name, () => new WeakMap());
27
+ }
28
+
29
+ /** A definition outside every class body (its symbol's span lies in no owner). */
30
+ function isOutOfClass(index, def) {
31
+ const { ownerDefinitionOf } = require('./class-identity');
32
+ return !def.isSignature && !ownerDefinitionOf(index, def);
33
+ }
34
+
35
+ /**
36
+ * The class an out-of-class constructor definition belongs to: the one
37
+ * class of its name whose scope path equals the definition's qualifier and
38
+ * that the definition's file sees. Null when none or several qualify.
39
+ */
40
+ function definitionOwner(index, def) {
41
+ if (!applies(index, def) || !isOutOfClass(index, def)) return null;
42
+ const saved = memo(index, 'outOfClassOwner');
43
+ if (saved?.has(def)) return saved.get(def);
44
+ const visible = require('./cpp-scope').includeClosure(index, def.file);
45
+ const path = scopePath(def);
46
+ const owners = (index.symbols.get(def.className) || []).filter(owner => OWNER_KINDS.has(owner.type) &&
47
+ !owner.forwardDeclaration && scopePath(owner) === path && visible.has(owner.file));
48
+ const owner = owners.length === 1 ? owners[0] : null;
49
+ saved?.set(def, owner);
50
+ return owner;
51
+ }
52
+
53
+ /** The in-class constructor declaration an out-of-class definition defines. */
54
+ function declarationOf(index, def) {
55
+ const owner = definitionOwner(index, def);
56
+ if (!owner) return null;
57
+ const { ownConstructors } = require('./constructor-calls');
58
+ const { _cFamilySignatureKey } = require('./callers');
59
+ const key = _cFamilySignatureKey(def);
60
+ const declared = ownConstructors(index, owner).filter(member => member.isSignature &&
61
+ _cFamilySignatureKey(member) === key);
62
+ return declared.length === 1 ? declared[0] : null;
63
+ }
64
+
65
+ /**
66
+ * The definition holding the body of an in-class constructor declaration:
67
+ * the declaration itself when it has a body, else its one linked
68
+ * out-of-class definition (null when there is none or several).
69
+ */
70
+ function bodyDefinitionOf(index, declaration) {
71
+ if (!declaration?.isSignature || !applies(index, declaration)) return declaration;
72
+ const saved = memo(index, 'outOfClassBody');
73
+ if (saved?.has(declaration)) return saved.get(declaration);
74
+ const definitions = (index.symbols.get(declaration.name) || []).filter(def => def !== declaration &&
75
+ def.type === declaration.type && def.className === declaration.className && !def.isSignature &&
76
+ declarationOf(index, def) === declaration);
77
+ const body = definitions.length === 1 ? definitions[0] : null;
78
+ saved?.set(declaration, body);
79
+ return body;
80
+ }
81
+
82
+ /** The out-of-class constructor definition enclosing a call record of `file`. */
83
+ function enclosingDefinition(index, file, call, owner) {
84
+ const enclosing = call.enclosingFunction;
85
+ if (!enclosing || !outOfClassLanguage(index, file)) return null;
86
+ const matches = (index.files.get(file)?.symbols || []).filter(def => def.name === owner.name &&
87
+ def.className === owner.name && !def.isSignature && def.startLine === enclosing.startLine &&
88
+ def.endLine === enclosing.endLine && require('./constructor-calls').isConstructor(def, index));
89
+ return matches.length === 1 && definitionOwner(index, matches[0]) === owner ? matches[0] : null;
90
+ }
91
+
92
+ /**
93
+ * Does a mem-initializer of an out-of-class constructor definition
94
+ * (parser `memberInitializer`) initialize a data member rather than
95
+ * construct a base (fix #421)? A name that is no project type, or a field
96
+ * of the definition's class, is a member; the record is then no call.
97
+ */
98
+ function initializesMember(index, file, call, namesType) {
99
+ if (!call.memberInitializer) return false;
100
+ if (!namesType(call.name)) return true;
101
+ const enclosing = call.enclosingFunction;
102
+ if (!enclosing) return false;
103
+ const { isConstructor } = require('./constructor-calls');
104
+ const definitions = (index.files.get(file)?.symbols || []).filter(def => !def.isSignature &&
105
+ def.startLine === enclosing.startLine && def.endLine === enclosing.endLine && isConstructor(def, index));
106
+ const owner = definitions.length === 1 ? definitionOwner(index, definitions[0]) : null;
107
+ return !!owner && (index.symbols.get(call.name) || []).some(def => def.className === owner.name &&
108
+ def.file === owner.file && def.startLine >= owner.startLine && def.endLine <= owner.endLine &&
109
+ (def.type === 'field' || def.memberType === 'field'));
110
+ }
111
+
112
+ /**
113
+ * The project bases an out-of-class constructor definition leaves to
114
+ * implicit construction (fix #421): its class's written bases that none of
115
+ * its mem-initializers names, none when it delegates. `isProjectType(name)`
116
+ * keeps the bases the project declares.
117
+ */
118
+ function omittedBases(index, def, isProjectType) {
119
+ const owner = definitionOwner(index, def);
120
+ if (!owner?.extends) return [];
121
+ const { getCachedCalls } = require('./callers');
122
+ const { splitParentList } = require('./graph-build');
123
+ const initializers = (getCachedCalls(index, def.file) || []).filter(call => call.constructorSyntax === 'delegation' &&
124
+ call.line >= def.startLine && call.line <= def.endLine &&
125
+ call.enclosingFunction?.startLine === def.startLine && call.enclosingFunction?.endLine === def.endLine);
126
+ if (initializers.some(call => call.ctorDelegation === 'this')) return [];
127
+ const initialized = new Set(initializers.map(call => call.name));
128
+ return splitParentList(owner.extends).map(spelling => {
129
+ const parts = String(spelling).replace(/<.*$/s, '').trim().split('::');
130
+ const name = parts.pop().trim();
131
+ return { name, qualifier: parts.join('::') || null };
132
+ }).filter(base => base.name && !initialized.has(base.name) && isProjectType(base.name));
133
+ }
134
+
135
+ /**
136
+ * Call records for the omitted base initializers of an out-of-class
137
+ * constructor definition (fix #421): the base's default constructor runs
138
+ * there, read from the class declaration in another place, so the site is
139
+ * a visible unverified candidate (`implicitOutOfClass`), never a confirmed
140
+ * edge.
141
+ */
142
+ function implicitBaseRecords(index, def, isProjectType) {
143
+ if (!applies(index, def) || def.isSignature || !require('./constructor-calls').isConstructor(def, index)) return [];
144
+ const saved = memo(index, 'outOfClassImplicitBases');
145
+ if (saved?.has(def)) return saved.get(def);
146
+ const records = omittedBases(index, def, isProjectType).map(base => ({
147
+ name: base.name, line: def.startLine, isMethod: false, isConstructor: true,
148
+ constructorSyntax: 'implicit', argCount: 0, argKinds: [],
149
+ ...(base.qualifier && { constructorQualifier: base.qualifier }),
150
+ implicitOutOfClass: { owner: def.className },
151
+ enclosingFunction: { name: def.name, startLine: def.startLine, endLine: def.endLine, className: def.className },
152
+ }));
153
+ saved?.set(def, records);
154
+ return records;
155
+ }
156
+
157
+ /**
158
+ * The out-of-class constructor definitions of classes derived from `base`
159
+ * that construct it implicitly (fix #421), as unverified caller rows.
160
+ */
161
+ function implicitBaseCallerRows(index, base, isProjectType) {
162
+ if (!outOfClassLanguage(index, base.file)) return [];
163
+ const rows = [];
164
+ const derived = new Set((index.extendedByGraph?.get(base.name) || [])
165
+ .map(child => typeof child === 'string' ? child : child.name));
166
+ for (const name of [...derived].sort()) {
167
+ for (const def of index.symbols.get(name) || []) {
168
+ if (def.className !== name || def.isSignature || !require('./constructor-calls').isConstructor(def, index)) continue;
169
+ const owner = definitionOwner(index, def);
170
+ if (!owner || !implicitBaseRecords(index, def, isProjectType).some(record => record.name === base.name)) continue;
171
+ // The derived class's written base must be this class.
172
+ const { resolveClassRef } = require('./class-identity');
173
+ const ref = resolveClassRef(index, base.name, owner.file);
174
+ if (ref?.def && ref.def !== base) continue;
175
+ rows.push(def);
176
+ }
177
+ }
178
+ return rows;
179
+ }
180
+
181
+ module.exports = { definitionOwner, declarationOf, bodyDefinitionOf, enclosingDefinition, initializesMember,
182
+ implicitBaseRecords, implicitBaseCallerRows };
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  const { dynamicImportsNote, formatLineRanges, unverifiedReasonLabel, advisoryLine } = require('./shared');
6
- const { formatAccountLines, formatCalleeAccountLine, unverifiedCalleeLines } = require('./analysis');
6
+ const { formatAccountLines, formatCalleeAccountLine, unverifiedCalleeLines, initializerReviewLines } = require('./analysis');
7
7
 
8
8
  /**
9
9
  * Format related command output - text
@@ -146,6 +146,7 @@ function formatSmart(smart, options = {}) {
146
146
  // v4 callee contract: unresolved calls inside the target — visible with
147
147
  // reasons (they may be dependencies smart could not inline).
148
148
  lines.push(...unverifiedCalleeLines(smart.unverifiedCallees, false));
149
+ lines.push(...initializerReviewLines(smart.initializerReviews, false));
149
150
  const smartCalleeAcct = formatCalleeAccountLine(smart.meta && smart.meta.calleeAccount);
150
151
  if (smartCalleeAcct) lines.push(`\n${smartCalleeAcct}`);
151
152
 
@@ -185,7 +186,8 @@ function formatSmartJson(result) {
185
186
  types: result.types || [],
186
187
  // v4 callee contract: visible unresolved-call entries, reconciled
187
188
  // by meta.calleeAccount.
188
- unverifiedCallees: result.unverifiedCallees || []
189
+ unverifiedCallees: result.unverifiedCallees || [],
190
+ ...(result.initializerReviews?.length && { initializerReviews: result.initializerReviews }),
189
191
  }
190
192
  });
191
193
  }
@@ -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: [] };
@@ -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
  }
@@ -366,6 +370,7 @@ function formatDeadcodeJson(results) {
366
370
  ...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
367
371
  ...(results.excludedExternalContract > 0 && { excludedExternalContract: results.excludedExternalContract }),
368
372
  ...(results.excludedRuntimeContract > 0 && { excludedRuntimeContract: results.excludedRuntimeContract }),
373
+ ...(results.excludedConstructors > 0 && { excludedConstructors: results.excludedConstructors }),
369
374
  ...(results.pythonImplicitExportFiles > 0 && { pythonImplicitExportFiles: results.pythonImplicitExportFiles }),
370
375
  ...(results.excludedDynamicDispatch > 0 && { excludedDynamicDispatch: results.excludedDynamicDispatch }),
371
376
  ...(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
  }