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.
- package/.claude/skills/ucn/SKILL.md +35 -9
- package/.claude/skills/ucn/references/commands.md +40 -18
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +32 -21
- package/cli/index.js +2 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/bridge.js +15 -65
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/client-receivers.js +189 -0
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +61 -16
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +2 -0
- package/core/output/reporting.js +12 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +270 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/request-helpers.js +63 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +3 -3
package/core/output/analysis.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
919
|
-
//
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
};
|
package/core/output/graph.js
CHANGED
|
@@ -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
|
-
|
|
458
|
-
|
|
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
|
}
|
package/core/output/lines.js
CHANGED
|
@@ -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,
|
|
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};
|
|
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}
|
|
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));
|
package/core/output/public.js
CHANGED
|
@@ -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
|
package/core/output/reporting.js
CHANGED
|
@@ -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 }),
|
package/core/output/shared.js
CHANGED
|
@@ -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
|
}
|
package/core/output/tracing.js
CHANGED
|
@@ -39,18 +39,31 @@ function renderFrontier(lines, frontier, options = {}, expanded = false) {
|
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
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
|
package/core/output-budget.js
CHANGED
|
@@ -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
|
|
package/core/parallel-build.js
CHANGED
|
@@ -275,4 +275,55 @@ function parallelBuild(index, files, options = {}) {
|
|
|
275
275
|
return changed;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
|
|
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 };
|