ucn 4.1.3 → 4.2.1
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 +80 -353
- package/.claude/skills/ucn/references/commands.md +93 -0
- package/.claude/skills/ucn/references/trust-contract.md +63 -0
- package/README.md +67 -44
- package/cli/index.js +24 -19
- package/core/account.js +18 -0
- package/core/analysis.js +27 -7
- package/core/bridge.js +0 -1
- package/core/brief.js +2 -3
- package/core/build-worker.js +5 -0
- package/core/cache.js +51 -6
- package/core/callers.js +1709 -162
- package/core/check.js +107 -19
- package/core/confidence.js +29 -16
- package/core/deadcode.js +1 -2
- package/core/entrypoints.js +32 -27
- package/core/execute.js +19 -3
- package/core/graph-build.js +33 -0
- package/core/output/analysis.js +53 -14
- package/core/output/check.js +11 -0
- package/core/output/doctor.js +40 -19
- package/core/output/endpoints.js +0 -2
- package/core/output/search.js +26 -1
- package/core/parser.js +1 -1
- package/core/project.js +25 -4
- package/core/registry.js +2 -2
- package/core/reporting.js +166 -108
- package/core/search.js +271 -49
- package/core/tracing.js +2 -2
- package/core/trust-matrix.js +158 -0
- package/core/verify.js +0 -1
- package/eslint.config.js +2 -2
- package/languages/go.js +143 -13
- package/languages/html.js +5 -0
- package/languages/index.js +5 -0
- package/languages/java.js +61 -4
- package/languages/javascript.js +233 -42
- package/languages/python.js +54 -15
- package/languages/rust.js +180 -22
- package/languages/utils.js +0 -1
- package/mcp/server.js +118 -38
- package/package.json +11 -4
package/core/output/analysis.js
CHANGED
|
@@ -75,9 +75,9 @@ function isTestEntry(entry) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* Render the conservation contract lines: ACCOUNT (always),
|
|
79
|
-
* files containing the symbol), FILTERED (display-filter
|
|
80
|
-
* when no account is present (e.g. class-type context).
|
|
78
|
+
* Render the conservation contract lines: ACCOUNT and CONTRACT (always),
|
|
79
|
+
* WARNING (unparsed files containing the symbol), FILTERED (display-filter
|
|
80
|
+
* hides). Returns [] when no account is present (e.g. class-type context).
|
|
81
81
|
*/
|
|
82
82
|
function formatAccountLines(account) {
|
|
83
83
|
if (!account) return [];
|
|
@@ -93,6 +93,18 @@ function formatAccountLines(account) {
|
|
|
93
93
|
line += ` (+${account.beyondText.count} beyond-text caller${account.beyondText.count === 1 ? '' : 's'})`;
|
|
94
94
|
}
|
|
95
95
|
lines.push(line);
|
|
96
|
+
const contract = account.contract || {};
|
|
97
|
+
const textComplete = contract.textComplete !== undefined
|
|
98
|
+
? contract.textComplete
|
|
99
|
+
: Boolean(account.conserved) && !(account.unparsed && account.unparsed.fileCount > 0) &&
|
|
100
|
+
!(account.unreadableFiles && account.unreadableFiles.length > 0);
|
|
101
|
+
if (!textComplete) {
|
|
102
|
+
lines.push('CONTRACT: literal-name text partition is DEGRADED; review WARNING and unaccounted lines before acting. Semantic completeness is not claimed.');
|
|
103
|
+
} else if (contract.observedTextZero) {
|
|
104
|
+
lines.push('CONTRACT: observed-text zero only; every literal-name line was classified, but aliases, indirect calls, generated code, and runtime dispatch may exist. Not safe-delete proof.');
|
|
105
|
+
} else {
|
|
106
|
+
lines.push('CONTRACT: literal-name text partition complete; semantic completeness is not claimed (aliases, indirect calls, generated code, and runtime dispatch may exist).');
|
|
107
|
+
}
|
|
96
108
|
if (account.unparsed && account.unparsed.fileCount > 0) {
|
|
97
109
|
lines.push(`WARNING: ${account.unparsed.fileCount} unparsed file${account.unparsed.fileCount === 1 ? '' : 's'} ` +
|
|
98
110
|
`contain${account.unparsed.fileCount === 1 ? 's' : ''} "${account.symbol}" ` +
|
|
@@ -184,6 +196,8 @@ function formatContextJson(context) {
|
|
|
184
196
|
// Tier parity with the function-path callers list: class
|
|
185
197
|
// usages are the confirmed-tier answer for type symbols.
|
|
186
198
|
...(c.confidence !== undefined && { confidence: c.confidence }),
|
|
199
|
+
...(c.evidenceScore !== undefined && { evidenceScore: c.evidenceScore }),
|
|
200
|
+
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
187
201
|
...(c.resolution && { resolution: c.resolution }),
|
|
188
202
|
...(c.tier && { tier: c.tier })
|
|
189
203
|
})),
|
|
@@ -193,6 +207,10 @@ function formatContextJson(context) {
|
|
|
193
207
|
expression: c.content,
|
|
194
208
|
callerName: c.callerName ?? null,
|
|
195
209
|
tier: 'unverified',
|
|
210
|
+
...(c.confidence !== undefined && { confidence: c.confidence }),
|
|
211
|
+
...(c.evidenceScore !== undefined && { evidenceScore: c.evidenceScore }),
|
|
212
|
+
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
213
|
+
...(c.resolution && { resolution: c.resolution }),
|
|
196
214
|
...(c.reason && { reason: c.reason }),
|
|
197
215
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
198
216
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
@@ -225,6 +243,8 @@ function formatContextJson(context) {
|
|
|
225
243
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
226
244
|
...(c.isFunctionReference && { functionReference: true }),
|
|
227
245
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
246
|
+
...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
|
|
247
|
+
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
228
248
|
...(c.tier && { tier: c.tier }),
|
|
229
249
|
...(c.reachable !== undefined && { reachable: c.reachable }),
|
|
230
250
|
})),
|
|
@@ -236,6 +256,8 @@ function formatContextJson(context) {
|
|
|
236
256
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
237
257
|
...(c.isFunctionReference && { functionReference: true }),
|
|
238
258
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
259
|
+
...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
|
|
260
|
+
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
239
261
|
tier: 'unverified',
|
|
240
262
|
...(c.reason && { reason: c.reason }),
|
|
241
263
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
@@ -250,6 +272,8 @@ function formatContextJson(context) {
|
|
|
250
272
|
params: c.params, // FULL params
|
|
251
273
|
weight: c.weight || 'normal', // Dependency weight: core, setup, utility
|
|
252
274
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
275
|
+
...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
|
|
276
|
+
...(c.scoreKind && { scoreKind: c.scoreKind }),
|
|
253
277
|
...(c.tier && { tier: c.tier }),
|
|
254
278
|
...(c.sites && { sites: c.sites }),
|
|
255
279
|
...(c.functionReference && { functionReference: true }),
|
|
@@ -377,7 +401,7 @@ function formatContext(ctx, options = {}) {
|
|
|
377
401
|
if (ctx.meta) {
|
|
378
402
|
const notes = [];
|
|
379
403
|
if (ctx.meta.dynamicImports) { const dn = dynamicImportsNote(ctx.meta.dynamicImports, ctx.meta); if (dn) notes.push(dn); }
|
|
380
|
-
if (ctx.meta.confidenceFiltered) notes.push(`${ctx.meta.confidenceFiltered} edge(s) below
|
|
404
|
+
if (ctx.meta.confidenceFiltered) notes.push(`${ctx.meta.confidenceFiltered} edge(s) below evidence threshold hidden`);
|
|
381
405
|
if (notes.length) {
|
|
382
406
|
lines.push(` Note: ${notes.join(', ')}`);
|
|
383
407
|
}
|
|
@@ -401,7 +425,7 @@ function formatContext(ctx, options = {}) {
|
|
|
401
425
|
: `CALLERS — CONFIRMED (${callers.length}):`;
|
|
402
426
|
lines.push(`${compact ? '' : '\n'}${tierHeader}`);
|
|
403
427
|
const callerEvidence = options.showConfidence !== false ? formatEvidenceLine(callers) : null;
|
|
404
|
-
if (callerEvidence
|
|
428
|
+
if (callerEvidence) lines.push(callerEvidence);
|
|
405
429
|
const callerReach = reachabilityDisplay(callers, hasEntrypoints, 'caller');
|
|
406
430
|
const renderCaller = (c) => {
|
|
407
431
|
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
@@ -470,7 +494,7 @@ function formatContext(ctx, options = {}) {
|
|
|
470
494
|
const callees = ctx.callees || [];
|
|
471
495
|
lines.push(`${compact ? '' : '\n'}CALLEES (${callees.length}):`);
|
|
472
496
|
const calleeEvidence = options.showConfidence !== false ? formatEvidenceLine(callees) : null;
|
|
473
|
-
if (calleeEvidence
|
|
497
|
+
if (calleeEvidence) lines.push(calleeEvidence);
|
|
474
498
|
const calleeReach = reachabilityDisplay(callees, hasEntrypoints, 'callee');
|
|
475
499
|
for (const c of callees) {
|
|
476
500
|
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
@@ -709,14 +733,18 @@ function formatAbout(about, options = {}) {
|
|
|
709
733
|
}
|
|
710
734
|
}
|
|
711
735
|
if (about.confidenceFiltered) {
|
|
712
|
-
lines.push(` Note: ${about.confidenceFiltered} edge(s) below
|
|
736
|
+
lines.push(` Note: ${about.confidenceFiltered} edge(s) below evidence threshold hidden`);
|
|
713
737
|
}
|
|
714
738
|
|
|
715
739
|
// Usage summary (fast-path approximation; ACCOUNT below is the exact
|
|
716
740
|
// text-ground truth — both are labeled to avoid confusion)
|
|
717
741
|
lines.push('');
|
|
718
|
-
|
|
719
|
-
|
|
742
|
+
if (compact) {
|
|
743
|
+
lines.push(`USAGES: ${about.totalUsages} total (${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references)`);
|
|
744
|
+
} else {
|
|
745
|
+
lines.push(`USAGES: ${about.totalUsages} total`);
|
|
746
|
+
lines.push(` ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references`);
|
|
747
|
+
}
|
|
720
748
|
|
|
721
749
|
// Callers — CONFIRMED tier, prod before test
|
|
722
750
|
const hasEntrypoints = about.hasEntrypoints !== false;
|
|
@@ -740,7 +768,7 @@ function formatAbout(about, options = {}) {
|
|
|
740
768
|
const caller = c.callerName ? `[${c.callerName}]` : '';
|
|
741
769
|
const unreachableMark = (aboutCallerReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
|
|
742
770
|
lines.push(` ${c.file}:${c.line} ${caller}${unreachableMark}`);
|
|
743
|
-
lines.push(` ${c.expression}`);
|
|
771
|
+
if (!compact && c.expression) lines.push(` ${c.expression}`);
|
|
744
772
|
};
|
|
745
773
|
for (const c of prodTop) renderAboutCaller(c);
|
|
746
774
|
if (testTop.length > 0) {
|
|
@@ -773,7 +801,7 @@ function formatAbout(about, options = {}) {
|
|
|
773
801
|
for (const u of aboutUnverified.top) {
|
|
774
802
|
const caller = u.callerName ? ` [${u.callerName}]` : '';
|
|
775
803
|
const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
|
|
776
|
-
const expr = u.expression ? `: ${u.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
804
|
+
const expr = !compact && u.expression ? `: ${u.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
777
805
|
lines.push(` ${u.file}:${u.line}${caller}${expr}${reason}`);
|
|
778
806
|
}
|
|
779
807
|
if (aboutUnverified.total > aboutUnverified.top.length) {
|
|
@@ -799,13 +827,13 @@ function formatAbout(about, options = {}) {
|
|
|
799
827
|
const sideEffects = (c.sideEffects && c.sideEffects.length) ? ` {${c.sideEffects.join(',')}}` : '';
|
|
800
828
|
const unreachableMark = (aboutCalleeReach.perLine && c.reachable === false) ? ' [unreachable]' : '';
|
|
801
829
|
lines.push(` ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.file}:${c.line} (${c.callCount}x)${unreachableMark}`);
|
|
802
|
-
if (c.docstring) {
|
|
830
|
+
if (!compact && c.docstring) {
|
|
803
831
|
const snip = calleeDocstringSnippet(c.docstring);
|
|
804
832
|
if (snip) lines.push(` "${snip}"`);
|
|
805
833
|
}
|
|
806
834
|
|
|
807
835
|
// Inline expansion: show first 3 lines of callee code
|
|
808
|
-
if (expand && root && c.file && c.startLine) {
|
|
836
|
+
if (!compact && expand && root && c.file && c.startLine) {
|
|
809
837
|
try {
|
|
810
838
|
const filePath = path.isAbsolute(c.file) ? c.file : path.join(root, c.file);
|
|
811
839
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -840,6 +868,13 @@ function formatAbout(about, options = {}) {
|
|
|
840
868
|
// Tests
|
|
841
869
|
if (about.tests.totalMatches > 0) {
|
|
842
870
|
lines.push('');
|
|
871
|
+
if (compact) {
|
|
872
|
+
const shown = about.tests.files.length > 0 ? `: ${about.tests.files.join(', ')}` : '';
|
|
873
|
+
const partial = about.tests.fileCount > about.tests.files.length
|
|
874
|
+
? ` (showing ${about.tests.files.length} of ${about.tests.fileCount} files)`
|
|
875
|
+
: '';
|
|
876
|
+
lines.push(`TESTS: ${about.tests.totalMatches} matches${partial}${shown}`);
|
|
877
|
+
} else {
|
|
843
878
|
if (about.tests.fileCount > about.tests.files.length) {
|
|
844
879
|
lines.push(`TESTS: ${about.tests.totalMatches} matches in ${about.tests.fileCount} file(s), showing ${about.tests.files.length}:`);
|
|
845
880
|
aboutTruncated = true;
|
|
@@ -849,6 +884,7 @@ function formatAbout(about, options = {}) {
|
|
|
849
884
|
for (const f of about.tests.files) {
|
|
850
885
|
lines.push(` ${f}`);
|
|
851
886
|
}
|
|
887
|
+
}
|
|
852
888
|
}
|
|
853
889
|
|
|
854
890
|
// Other definitions
|
|
@@ -881,10 +917,13 @@ function formatAbout(about, options = {}) {
|
|
|
881
917
|
}
|
|
882
918
|
|
|
883
919
|
// Code
|
|
884
|
-
if (about.code) {
|
|
920
|
+
if (about.code && !compact) {
|
|
885
921
|
lines.push('');
|
|
886
922
|
lines.push('─── CODE ───');
|
|
887
923
|
lines.push(about.code);
|
|
924
|
+
} else if (about.code && compact) {
|
|
925
|
+
lines.push('');
|
|
926
|
+
lines.push(`SOURCE: omitted in compact mode; use fn ${sym.handle || sym.name} to extract it.`);
|
|
888
927
|
}
|
|
889
928
|
|
|
890
929
|
if (aboutTruncated) {
|
package/core/output/check.js
CHANGED
|
@@ -13,6 +13,16 @@ function formatCheck(result) {
|
|
|
13
13
|
const lines = [];
|
|
14
14
|
lines.push(`Pre-commit Check vs ${result.base}${result.staged ? ' (staged)' : ''}`);
|
|
15
15
|
lines.push('═'.repeat(60));
|
|
16
|
+
if (result.trust) {
|
|
17
|
+
lines.push(`TRUST: ${result.trust.status} — UCN evidence is not semantic proof; compiler and tests are required`);
|
|
18
|
+
const trustDetails = [];
|
|
19
|
+
if (result.trust.incompleteAccounts) trustDetails.push(`${result.trust.incompleteAccounts} incomplete account(s)`);
|
|
20
|
+
if (result.trust.unverifiedCallSites) trustDetails.push(`${result.trust.unverifiedCallSites} unverified caller(s)`);
|
|
21
|
+
if (result.trust.signatureMismatches) trustDetails.push(`${result.trust.signatureMismatches} signature mismatch(es)`);
|
|
22
|
+
if (result.trust.filteredEdges) trustDetails.push(`${result.trust.filteredEdges} filtered edge(s)`);
|
|
23
|
+
if (result.trust.usageReviewSymbols) trustDetails.push(`${result.trust.usageReviewSymbols} symbol(s) need usages review`);
|
|
24
|
+
if (trustDetails.length > 0) lines.push(` ${trustDetails.join(' · ')}`);
|
|
25
|
+
}
|
|
16
26
|
|
|
17
27
|
// Changed functions section
|
|
18
28
|
const items = result.changed || [];
|
|
@@ -29,6 +39,7 @@ function formatCheck(result) {
|
|
|
29
39
|
if (it.kind && it.kind !== 'changed') tags.push(it.kind.toUpperCase());
|
|
30
40
|
if (it.signatureMismatches > 0) tags.push(`SIG-DRIFT(${it.signatureMismatches})`);
|
|
31
41
|
if (it.orphan) tags.push('ORPHAN');
|
|
42
|
+
if (it.account && !it.account.textComplete) tags.push('ACCOUNT-INCOMPLETE');
|
|
32
43
|
const tagStr = tags.length ? ' [' + tags.join(', ') + ']' : '';
|
|
33
44
|
let callers = it.callerCount != null ? `${it.callerCount} caller${it.callerCount === 1 ? '' : 's'}` : '';
|
|
34
45
|
if (it.unverifiedCallerCount > 0) {
|
package/core/output/doctor.js
CHANGED
|
@@ -4,10 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
-
function pad(n, width = 4) {
|
|
8
|
-
return String(n).padStart(width);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
7
|
function formatDoctor(result) {
|
|
12
8
|
if (!result) return 'No project to analyze.';
|
|
13
9
|
const lines = [];
|
|
@@ -32,23 +28,31 @@ function formatDoctor(result) {
|
|
|
32
28
|
const buildHint = result.cache.buildMs ? `, ${result.cache.buildMs}ms build` : '';
|
|
33
29
|
lines.push(`Cache: ${state}${buildHint}`);
|
|
34
30
|
}
|
|
31
|
+
if (result.commandTrust) {
|
|
32
|
+
const c = result.commandTrust;
|
|
33
|
+
lines.push(`Command proofs: ${c.classified}/${c.commands} classified, ${c.oracleBacked} external-oracle-backed, ${c.unclassified} unclassified`);
|
|
34
|
+
lines.push(' Classification describes shipped proof coverage, not this repository\'s runtime accuracy.');
|
|
35
|
+
}
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
|
|
37
|
+
// Evidence profile (if computed). Confidence scores are rule labels, not
|
|
38
|
+
// empirically calibrated probabilities, so never call this accuracy.
|
|
39
|
+
const profile = result.evidenceProfile || result.coverage;
|
|
40
|
+
if (profile && profile.total > 0) {
|
|
38
41
|
lines.push('');
|
|
39
|
-
lines.push('Resolution
|
|
40
|
-
const c =
|
|
42
|
+
lines.push('Resolution evidence profile (sampled; not semantic accuracy):');
|
|
43
|
+
const c = profile;
|
|
41
44
|
const total = c.total || 1;
|
|
42
|
-
lines.push(`
|
|
43
|
-
lines.push(`
|
|
44
|
-
lines.push(`
|
|
45
|
-
lines.push(` Sampled ${c.sampled}
|
|
46
|
-
|
|
45
|
+
lines.push(` Confirmed evidence: ${c.confirmed} (${(c.confirmed / total * 100).toFixed(1)}%)`);
|
|
46
|
+
lines.push(` Unverified: ${c.unverified} (${(c.unverified / total * 100).toFixed(1)}%)`);
|
|
47
|
+
lines.push(` Score bands: high ${c.high}, medium ${c.medium}, low ${c.low}`);
|
|
48
|
+
lines.push(` Sampled ${c.sampled}/${c.candidateSymbols} pinned definitions → ${c.total} unique edge candidates`);
|
|
49
|
+
lines.push(` Sample quality: ${c.adequate ? 'adequate' : 'insufficient'}${c.representative ? ', all indexed languages represented' : ''}`);
|
|
50
|
+
} else if (profile) {
|
|
47
51
|
lines.push('');
|
|
48
|
-
lines.push('Resolution
|
|
52
|
+
lines.push('Resolution evidence profile: no caller edges in the stratified sample.');
|
|
49
53
|
} else {
|
|
50
54
|
lines.push('');
|
|
51
|
-
lines.push('Resolution
|
|
55
|
+
lines.push('Resolution evidence profile: not computed (use --deep)');
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
// Blind spots
|
|
@@ -60,8 +64,12 @@ function formatDoctor(result) {
|
|
|
60
64
|
['Eval/exec calls', bs.evalCalls],
|
|
61
65
|
['Reflection', bs.reflection],
|
|
62
66
|
['Parse failures', bs.parseFailures],
|
|
67
|
+
['Parser recovery', bs.parseRecoveries],
|
|
63
68
|
];
|
|
64
|
-
const unitFor = {
|
|
69
|
+
const unitFor = {
|
|
70
|
+
'Dynamic imports': 'import', 'Eval/exec calls': 'use', Reflection: 'use',
|
|
71
|
+
'Parse failures': 'failure',
|
|
72
|
+
};
|
|
65
73
|
let anyBlindSpot = false;
|
|
66
74
|
for (const [label, info] of bsLines) {
|
|
67
75
|
if (info && info.count > 0) {
|
|
@@ -72,7 +80,11 @@ function formatDoctor(result) {
|
|
|
72
80
|
// never present the display cap as the population (field-report #2).
|
|
73
81
|
const fileCount = info.fileCount != null ? info.fileCount : info.files.length;
|
|
74
82
|
const unit = unitFor[label] || 'use';
|
|
75
|
-
|
|
83
|
+
if (label === 'Parser recovery') {
|
|
84
|
+
lines.push(` ${label}: ${fileCount} recovered file${fileCount === 1 ? '' : 's'} (results may be partial)`);
|
|
85
|
+
} else {
|
|
86
|
+
lines.push(` ${label}: ${info.count} ${unit}${info.count === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`);
|
|
87
|
+
}
|
|
76
88
|
const shownFiles = info.files.slice(0, 3);
|
|
77
89
|
const sample = shownFiles.map(f => ` - ${f}`).join('\n');
|
|
78
90
|
const moreFiles = fileCount - shownFiles.length;
|
|
@@ -82,9 +94,18 @@ function formatDoctor(result) {
|
|
|
82
94
|
}
|
|
83
95
|
if (!anyBlindSpot) lines.push(' (none detected)');
|
|
84
96
|
|
|
85
|
-
//
|
|
97
|
+
// Task-specific readiness. One scalar cannot honestly describe navigation,
|
|
98
|
+
// refactoring, and deletion risk.
|
|
86
99
|
lines.push('');
|
|
87
|
-
|
|
100
|
+
if (result.dimensions) {
|
|
101
|
+
lines.push('Readiness:');
|
|
102
|
+
for (const key of ['navigation', 'refactor', 'deletion']) {
|
|
103
|
+
const d = result.dimensions[key];
|
|
104
|
+
if (d) lines.push(` ${key}: ${d.level} — ${d.reason}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push(` semantic recall: ${result.dimensions.semanticRecall.level} — ${result.dimensions.semanticRecall.reason}`);
|
|
107
|
+
}
|
|
108
|
+
lines.push(`Overall (${result.trustScope || 'legacy'}): ${result.trust}${result.trustReason ? ' — ' + result.trustReason : ''}`);
|
|
88
109
|
return lines.join('\n');
|
|
89
110
|
}
|
|
90
111
|
|
package/core/output/endpoints.js
CHANGED
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
const { advisoryLine } = require('./shared');
|
|
16
16
|
const { codeUnitCompare } = require('../shared');
|
|
17
17
|
|
|
18
|
-
const SEP_TIER = { exact: 'EXACT', partial: 'PARTIAL', uncertain: 'UNCERTAIN' };
|
|
19
|
-
|
|
20
18
|
function pad(s, n) {
|
|
21
19
|
if (typeof s !== 'string') s = String(s);
|
|
22
20
|
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
package/core/output/search.js
CHANGED
|
@@ -151,6 +151,10 @@ function formatStructuralSearchJson(result) {
|
|
|
151
151
|
* Format example result as text
|
|
152
152
|
*/
|
|
153
153
|
function formatExample(result, name) {
|
|
154
|
+
if (result && !result.best && result.unverifiedCalls > 0) {
|
|
155
|
+
const n = result.unverifiedCalls;
|
|
156
|
+
return `No confirmed call example for "${name}". ${n} unverified candidate${n === 1 ? '' : 's'} require receiver/binding review (use impact or usages).`;
|
|
157
|
+
}
|
|
154
158
|
// MEDIUM-8: when only test-file callers exist and the user didn't ask
|
|
155
159
|
// for them, surface that fact explicitly instead of saying nothing was
|
|
156
160
|
// found.
|
|
@@ -172,6 +176,7 @@ function formatExample(result, name) {
|
|
|
172
176
|
lines.push(`${best.relativePath}:${best.line}`);
|
|
173
177
|
const exAdvisory = advisoryLine(result.advisory);
|
|
174
178
|
if (exAdvisory) lines.push(exAdvisory);
|
|
179
|
+
lines.push(`Evidence: ${best.evidenceTier || 'unverified'}${best.resolution ? ` (${best.resolution})` : ''}`);
|
|
175
180
|
lines.push('');
|
|
176
181
|
|
|
177
182
|
if (best.before) {
|
|
@@ -224,6 +229,7 @@ function formatExampleDiverse(result, name) {
|
|
|
224
229
|
lines.push(`[${i + 1}] ${shape} — ${c.count} call${c.count === 1 ? '' : 's'} in this cluster`);
|
|
225
230
|
if (!rep) continue;
|
|
226
231
|
lines.push(` ${rep.relativePath || rep.file}:${rep.line}`);
|
|
232
|
+
lines.push(` evidence: ${rep.evidenceTier || 'unverified'}${rep.resolution ? ` (${rep.resolution})` : ''}`);
|
|
227
233
|
|
|
228
234
|
if (rep.before) {
|
|
229
235
|
for (let j = 0; j < rep.before.length; j++) {
|
|
@@ -256,10 +262,17 @@ function formatExampleJson(result, name) {
|
|
|
256
262
|
// Surface the excluded-test-usages count in JSON too (fix #237 — the
|
|
257
263
|
// handler note that used to carry it duplicated the text body).
|
|
258
264
|
const excluded = result?.excludedTestCalls || 0;
|
|
265
|
+
const unverified = result?.unverifiedCalls || 0;
|
|
259
266
|
return JSON.stringify({
|
|
260
267
|
found: false, query: name,
|
|
261
268
|
...(excluded > 0 && { excludedTestCalls: excluded }),
|
|
262
|
-
|
|
269
|
+
...(unverified > 0 && {
|
|
270
|
+
unverifiedCalls: unverified,
|
|
271
|
+
unverifiedCandidates: result.unverifiedCandidates || [],
|
|
272
|
+
}),
|
|
273
|
+
error: unverified > 0
|
|
274
|
+
? `No confirmed call example for "${name}". ${unverified} unverified candidate${unverified === 1 ? '' : 's'} require receiver/binding review.`
|
|
275
|
+
: excluded > 0
|
|
263
276
|
? `No call examples found for "${name}" (excluded ${excluded} test-file usage${excluded === 1 ? '' : 's'} — pass --include-tests to include them)`
|
|
264
277
|
: `No call examples found for "${name}"`,
|
|
265
278
|
}, null, 2);
|
|
@@ -271,12 +284,17 @@ function formatExampleJson(result, name) {
|
|
|
271
284
|
query: name,
|
|
272
285
|
...(result.advisory && { advisory: result.advisory }),
|
|
273
286
|
totalCalls: result.totalCalls,
|
|
287
|
+
confirmedCalls: result.confirmedCalls || 0,
|
|
288
|
+
unverifiedCalls: result.unverifiedCalls || 0,
|
|
274
289
|
best: {
|
|
275
290
|
file: best.relativePath || best.file,
|
|
276
291
|
line: best.line,
|
|
277
292
|
content: best.content,
|
|
278
293
|
score: best.score,
|
|
279
294
|
reasons: best.reasons || [],
|
|
295
|
+
evidenceTier: best.evidenceTier || 'unverified',
|
|
296
|
+
...(best.resolution && { resolution: best.resolution }),
|
|
297
|
+
...(best.reason && { reason: best.reason }),
|
|
280
298
|
...(best.before && best.before.length > 0 && { before: best.before }),
|
|
281
299
|
...(best.after && best.after.length > 0 && { after: best.after })
|
|
282
300
|
}
|
|
@@ -383,6 +401,11 @@ function formatTests(tests, name) {
|
|
|
383
401
|
} else {
|
|
384
402
|
const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
|
|
385
403
|
lines.push(`Found ${totalMatches} matches in ${tests.length} test file(s):\n`);
|
|
404
|
+
const unverified = tests.reduce((sum, t) => sum +
|
|
405
|
+
t.matches.filter(m => ['unverified-call', 'unverified-reference'].includes(m.matchType)).length, 0);
|
|
406
|
+
if (unverified > 0) {
|
|
407
|
+
lines.push(`UNVERIFIED: ${unverified} possible test call${unverified === 1 ? '' : 's'} need receiver/binding review.\n`);
|
|
408
|
+
}
|
|
386
409
|
|
|
387
410
|
for (const testFile of tests) {
|
|
388
411
|
lines.push(testFile.file);
|
|
@@ -390,6 +413,8 @@ function formatTests(tests, name) {
|
|
|
390
413
|
const typeLabel = match.matchType === 'test-case' ? '[test]' :
|
|
391
414
|
match.matchType === 'import' ? '[import]' :
|
|
392
415
|
match.matchType === 'call' ? '[call]' :
|
|
416
|
+
match.matchType === 'unverified-call' ? '[unverified-call]' :
|
|
417
|
+
match.matchType === 'unverified-reference' ? '[unverified-ref]' :
|
|
393
418
|
match.matchType === 'string-ref' ? '[string]' : '[ref]';
|
|
394
419
|
lines.push(` ${match.line}: ${typeLabel} ${match.content}`);
|
|
395
420
|
}
|
package/core/parser.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
9
|
const { detectLanguage, getParser, getLanguageModule, isSupported } = require('../languages');
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -58,6 +57,7 @@ const { detectLanguage, getParser, getLanguageModule, isSupported } = require('.
|
|
|
58
57
|
* @property {StateDef[]} stateObjects - All state objects
|
|
59
58
|
* @property {Array} imports - Import statements (from imports.js)
|
|
60
59
|
* @property {Array} exports - Export statements (from imports.js)
|
|
60
|
+
* @property {boolean} [parseRecovery] - Tree-sitter recovered from syntax errors; results may be partial
|
|
61
61
|
*/
|
|
62
62
|
|
|
63
63
|
/**
|
package/core/project.js
CHANGED
|
@@ -9,7 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const { expandGlob, findProjectRoot, detectProjectPattern, isTestFile, parseGitignore, DEFAULT_IGNORES, compareNames } = require('./discovery');
|
|
12
|
-
const { extractImports, extractExports
|
|
12
|
+
const { extractImports, extractExports } = require('./imports');
|
|
13
13
|
const { parse, cleanHtmlScriptTags } = require('./parser');
|
|
14
14
|
const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits, PARSE_OPTIONS } = require('../languages');
|
|
15
15
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
@@ -54,6 +54,8 @@ class ProjectIndex {
|
|
|
54
54
|
this._opUsagesCache = null; // per-operation findUsagesInCode cache (Map<"file:name", usages[]>)
|
|
55
55
|
this._opTreeCache = null; // per-operation parsed-tree cache (Map<filePath, tree|null>, bounded FIFO)
|
|
56
56
|
this._opLinesCache = null; // per-operation split-lines cache (Map<filePath, string[]>, bounded FIFO)
|
|
57
|
+
this._opInnerSymbolRangesCache = null; // per-operation sorted class-method ranges by file
|
|
58
|
+
this._opFlowTypeOriginCache = null; // per-operation annotation type identity results
|
|
57
59
|
this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -83,6 +85,15 @@ class ProjectIndex {
|
|
|
83
85
|
this._opEnclosingFnCache = new Map();
|
|
84
86
|
this._opTreeCache = new Map();
|
|
85
87
|
this._opLinesCache = new Map();
|
|
88
|
+
// Query-time semantic derivations that depend only on one file's
|
|
89
|
+
// immutable call records. Reachability may ask findCallees() for
|
|
90
|
+
// thousands of symbols in the same few large files; rebuilding
|
|
91
|
+
// these structures per symbol turns the walk quadratic.
|
|
92
|
+
this._opReturnTypeFlowCache = new Map();
|
|
93
|
+
this._opCallsByLineCache = new Map();
|
|
94
|
+
this._opInnerSymbolRangesCache = new Map();
|
|
95
|
+
this._opFlowTypeOriginCache = new Map();
|
|
96
|
+
this._opImportReachCache = new Map();
|
|
86
97
|
this._opDepth = 0;
|
|
87
98
|
}
|
|
88
99
|
this._opDepth++;
|
|
@@ -99,6 +110,11 @@ class ProjectIndex {
|
|
|
99
110
|
this._opEnclosingFnCache = null;
|
|
100
111
|
this._opTreeCache = null;
|
|
101
112
|
this._opLinesCache = null;
|
|
113
|
+
this._opReturnTypeFlowCache = null;
|
|
114
|
+
this._opCallsByLineCache = null;
|
|
115
|
+
this._opInnerSymbolRangesCache = null;
|
|
116
|
+
this._opFlowTypeOriginCache = null;
|
|
117
|
+
this._opImportReachCache = null;
|
|
102
118
|
// Free cached file content from callsCache entries (retained during
|
|
103
119
|
// operation for _readFile caching, not needed between operations)
|
|
104
120
|
for (const entry of this.callsCache.values()) {
|
|
@@ -120,7 +136,7 @@ class ProjectIndex {
|
|
|
120
136
|
if (!this._opTreeCache) return null;
|
|
121
137
|
const cached = this._opTreeCache.get(filePath);
|
|
122
138
|
if (cached !== undefined) return cached;
|
|
123
|
-
let tree
|
|
139
|
+
let tree;
|
|
124
140
|
try {
|
|
125
141
|
const parser = getParser(language);
|
|
126
142
|
tree = parser ? safeParse(parser, content, undefined, PARSE_OPTIONS) : null;
|
|
@@ -495,6 +511,7 @@ class ProjectIndex {
|
|
|
495
511
|
exportDetails: exports,
|
|
496
512
|
symbols: [],
|
|
497
513
|
bindings: [],
|
|
514
|
+
...(parsed.parseRecovery && { parseRecovery: true }),
|
|
498
515
|
...(importAliases && { importAliases }),
|
|
499
516
|
// Module-scope assignment targets (fix #217): names a module can
|
|
500
517
|
// expose WITHOUT a def/class/import binding (`render = impl`,
|
|
@@ -518,6 +535,8 @@ class ProjectIndex {
|
|
|
518
535
|
params: item.params,
|
|
519
536
|
paramsStructured: item.paramsStructured,
|
|
520
537
|
returnType: item.returnType,
|
|
538
|
+
...(item.returnedFunctionResult && { returnedFunctionResult: item.returnedFunctionResult }),
|
|
539
|
+
...(item.isFunctionVariable && { isFunctionVariable: true }),
|
|
521
540
|
...(item.paramTypes && { paramTypes: item.paramTypes }),
|
|
522
541
|
...(item.isAsync && { isAsync: true }),
|
|
523
542
|
...(item.isGenerator && { isGenerator: true }),
|
|
@@ -550,7 +569,9 @@ class ProjectIndex {
|
|
|
550
569
|
// just the traitImpl flag (fix #210).
|
|
551
570
|
...(item.traitName && { traitName: item.traitName }),
|
|
552
571
|
...(item.isSignature && { isSignature: true }),
|
|
553
|
-
...(item.memberAssigned && { memberAssigned: true })
|
|
572
|
+
...(item.memberAssigned && { memberAssigned: true }),
|
|
573
|
+
...(item.registryMember && { registryMember: true }),
|
|
574
|
+
...(item.registryContainer && { registryContainer: item.registryContainer })
|
|
554
575
|
};
|
|
555
576
|
fileEntry.symbols.push(symbol);
|
|
556
577
|
// Property-assignment defs (fix #269: Reply.prototype.serialize
|
|
@@ -1418,7 +1439,7 @@ class ProjectIndex {
|
|
|
1418
1439
|
// Match both "TypeName" and "*TypeName" receivers (for Go/Rust pointer receivers)
|
|
1419
1440
|
const baseTypeName = typeName.replace(/^\*/, '');
|
|
1420
1441
|
|
|
1421
|
-
for (const [
|
|
1442
|
+
for (const [, symbols] of this.symbols) {
|
|
1422
1443
|
for (const symbol of symbols) {
|
|
1423
1444
|
// Skip non-method types (fields, properties, etc.)
|
|
1424
1445
|
if (symbol.type === 'field' || symbol.type === 'property') {
|
package/core/registry.js
CHANGED
|
@@ -125,7 +125,7 @@ const FLAG_APPLICABILITY = {
|
|
|
125
125
|
reverseTrace: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence', 'expandUnverified'],
|
|
126
126
|
smart: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'withTypes', 'minConfidence'],
|
|
127
127
|
trace: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
|
|
128
|
-
example: ['name', 'file', 'className', 'diverse', 'top', 'includeTests'],
|
|
128
|
+
example: ['name', 'file', 'className', 'line', 'diverse', 'top', 'includeTests'],
|
|
129
129
|
related: ['name', 'file', 'className', 'line', 'top', 'all'],
|
|
130
130
|
brief: ['name', 'file', 'className', 'line', 'git'],
|
|
131
131
|
// Finding code
|
|
@@ -162,7 +162,7 @@ const FLAG_APPLICABILITY = {
|
|
|
162
162
|
stacktrace: ['stack'],
|
|
163
163
|
api: ['file', 'limit'],
|
|
164
164
|
stats: ['functions', 'hot', 'top'],
|
|
165
|
-
doctor: ['file', 'in', '
|
|
165
|
+
doctor: ['file', 'in', 'deep'],
|
|
166
166
|
orient: ['top'],
|
|
167
167
|
auditAsync: ['file', 'exclude', 'limit'],
|
|
168
168
|
};
|