ucn 4.1.3 → 4.2.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.
- 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/reporting.js
CHANGED
|
@@ -12,6 +12,7 @@ const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
|
12
12
|
const { _declaredFieldType, _projectTopLevelNames } = require('./callers');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const { isTestFile } = require('./discovery');
|
|
15
|
+
const { summarizeCommandTrust } = require('./trust-matrix');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Get project statistics: file counts, symbol counts, LOC, language breakdown.
|
|
@@ -23,7 +24,7 @@ const { isTestFile } = require('./discovery');
|
|
|
23
24
|
function getStats(index, options = {}) {
|
|
24
25
|
// Count total symbols (not just unique names)
|
|
25
26
|
let totalSymbols = 0;
|
|
26
|
-
for (const [
|
|
27
|
+
for (const [, symbols] of index.symbols) {
|
|
27
28
|
totalSymbols += symbols.length;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -37,7 +38,7 @@ function getStats(index, options = {}) {
|
|
|
37
38
|
...(index.truncated && { truncated: index.truncated })
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
for (const [
|
|
41
|
+
for (const [, fileEntry] of index.files) {
|
|
41
42
|
const lang = fileEntry.language;
|
|
42
43
|
if (!stats.byLanguage[lang]) {
|
|
43
44
|
stats.byLanguage[lang] = { files: 0, lines: 0, symbols: 0 };
|
|
@@ -47,7 +48,7 @@ function getStats(index, options = {}) {
|
|
|
47
48
|
stats.byLanguage[lang].symbols += fileEntry.symbols.length;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
for (const [
|
|
51
|
+
for (const [, symbols] of index.symbols) {
|
|
51
52
|
for (const sym of symbols) {
|
|
52
53
|
if (!Object.hasOwn(stats.byType, sym.type)) {
|
|
53
54
|
stats.byType[sym.type] = 0;
|
|
@@ -67,7 +68,7 @@ function getStats(index, options = {}) {
|
|
|
67
68
|
// Per-function line counts for complexity audits
|
|
68
69
|
if (options.functions) {
|
|
69
70
|
const functions = [];
|
|
70
|
-
for (const [
|
|
71
|
+
for (const [, symbols] of index.symbols) {
|
|
71
72
|
for (const sym of symbols) {
|
|
72
73
|
if (CALLABLE_SYMBOL_KINDS.has(sym.type)) {
|
|
73
74
|
const lineCount = sym.endLine - sym.startLine + 1;
|
|
@@ -298,7 +299,7 @@ function getStats(index, options = {}) {
|
|
|
298
299
|
const isMethodRow = ownerClasses.size > 0 &&
|
|
299
300
|
(!representative || !!representative.className || !!representative.receiver);
|
|
300
301
|
|
|
301
|
-
let count
|
|
302
|
+
let count;
|
|
302
303
|
let approximate = false;
|
|
303
304
|
if (!isMethodRow) {
|
|
304
305
|
// Standalone function (or top-level package call): use bare-name calls
|
|
@@ -549,7 +550,7 @@ function getToc(index, options = {}) {
|
|
|
549
550
|
|
|
550
551
|
/**
|
|
551
552
|
* Project trust report. Tells the caller how much UCN itself trusts the index
|
|
552
|
-
* for this project: resolution
|
|
553
|
+
* for this project: sampled resolution evidence, blind spots (dynamic imports, eval,
|
|
553
554
|
* reflection), parse failures, and a quick verdict.
|
|
554
555
|
*
|
|
555
556
|
* Cheap-by-default: counts + blind-spot scan are O(files). The expensive
|
|
@@ -560,9 +561,6 @@ function getToc(index, options = {}) {
|
|
|
560
561
|
* @param {object} options - { deep, sampleSize, in, file }
|
|
561
562
|
*/
|
|
562
563
|
function doctor(index, options = {}) {
|
|
563
|
-
const { detectLanguage, langTraits } = require('../languages');
|
|
564
|
-
const path = require('path');
|
|
565
|
-
|
|
566
564
|
const inFilter = options.in || options.file || null;
|
|
567
565
|
const matchInFilter = (rel) => {
|
|
568
566
|
if (!inFilter) return true;
|
|
@@ -583,6 +581,7 @@ function doctor(index, options = {}) {
|
|
|
583
581
|
evalCalls: { count: 0, fileCount: 0, files: [] },
|
|
584
582
|
reflection: { count: 0, fileCount: 0, files: [] },
|
|
585
583
|
parseFailures: { count: 0, fileCount: 0, files: [] },
|
|
584
|
+
parseRecoveries:{ count: 0, fileCount: 0, files: [] },
|
|
586
585
|
};
|
|
587
586
|
|
|
588
587
|
// Reflection/eval signals come from the shared text-blind-spot counter
|
|
@@ -612,6 +611,7 @@ function doctor(index, options = {}) {
|
|
|
612
611
|
|
|
613
612
|
if (fe.dynamicImports && fe.dynamicImports > 0) recordBlind(blindSpots.dynamicImports, fe.dynamicImports);
|
|
614
613
|
if (fe.parseError) recordBlind(blindSpots.parseFailures, 1);
|
|
614
|
+
if (fe.parseRecovery) recordBlind(blindSpots.parseRecoveries, 1);
|
|
615
615
|
|
|
616
616
|
// Read file once for eval/reflection signals (shared counter).
|
|
617
617
|
if (hasTextBlindspots(lang)) {
|
|
@@ -623,12 +623,30 @@ function doctor(index, options = {}) {
|
|
|
623
623
|
}
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
-
//
|
|
627
|
-
|
|
626
|
+
// Files that failed before a fileEntry could be created are absent from
|
|
627
|
+
// index.files, so the loop above can never see them. They are the most
|
|
628
|
+
// important parse failures to surface: otherwise doctor can report a
|
|
629
|
+
// clean index precisely when entire files are missing from it.
|
|
630
|
+
const recordedParseFailureFiles = new Set(blindSpots.parseFailures.files);
|
|
631
|
+
for (const failedPath of index.failedFiles || []) {
|
|
632
|
+
const rel = path.relative(index.root, failedPath);
|
|
633
|
+
if (!matchInFilter(rel) || recordedParseFailureFiles.has(rel)) continue;
|
|
634
|
+
blindSpots.parseFailures.count++;
|
|
635
|
+
blindSpots.parseFailures.fileCount++;
|
|
636
|
+
fileCounts.failed = (fileCounts.failed || 0) + 1;
|
|
637
|
+
if (blindSpots.parseFailures.files.length < BLINDSPOT_FILE_CAP) {
|
|
638
|
+
blindSpots.parseFailures.files.push(rel);
|
|
639
|
+
}
|
|
640
|
+
recordedParseFailureFiles.add(rel);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Evidence profile — sampled only in deep mode. This is deliberately NOT
|
|
644
|
+
// called "accuracy" or "coverage": it describes how UCN classified edges
|
|
645
|
+
// it found. Compiler/LSP oracle evaluation is the accuracy measurement.
|
|
646
|
+
let evidenceProfile = null;
|
|
628
647
|
if (options.deep || options.sampleSize) {
|
|
629
|
-
|
|
648
|
+
evidenceProfile = computeEvidenceProfile(index, {
|
|
630
649
|
sampleSize: options.sampleSize || 200,
|
|
631
|
-
inFilter,
|
|
632
650
|
matchInFilter,
|
|
633
651
|
});
|
|
634
652
|
}
|
|
@@ -640,86 +658,72 @@ function doctor(index, options = {}) {
|
|
|
640
658
|
cache.buildMs = index.buildTime || null;
|
|
641
659
|
} catch (e) { /* ignore */ }
|
|
642
660
|
|
|
643
|
-
// Compute trust verdict.
|
|
644
|
-
//
|
|
645
|
-
// Field-report #1: the old logic dropped the tier by one PER blind-spot
|
|
646
|
-
// category present, so any non-trivial Python/TS project (all of which have
|
|
647
|
-
// some getattr/eval/dynamic import) was forced to LOW even when --deep
|
|
648
|
-
// measured ~99% of edges at confidence ≥ 0.5 — a self-contradicting verdict
|
|
649
|
-
// ("99.1% ... LOW") that trains agents to distrust a healthy index. The fix:
|
|
650
|
-
// - When --deep coverage exists it drives the tier. Coverage measures the
|
|
651
|
-
// CONFIDENCE of edges UCN FOUND, NOT completeness — a reflection-hidden
|
|
652
|
-
// edge is absent from the sample, never a low-confidence edge dragging
|
|
653
|
-
// the % down — so sparse blind spots are a CAVEAT, while PERVASIVE ones
|
|
654
|
-
// (a large share of files) can hide edges the sample can't see and cap
|
|
655
|
-
// the verdict at MEDIUM (density, not mere presence; see below).
|
|
656
|
-
// - Parse failures are a separate exception: a file UCN couldn't parse is
|
|
657
|
-
// not in the sample at all, a genuine uncounted hole → cap at MEDIUM.
|
|
658
|
-
// - Without --deep there is no measurement, so blind spots are the only
|
|
659
|
-
// signal — but bounded to ONE tier total (not one per category), so a
|
|
660
|
-
// handful of getattr doesn't read as untrustworthy.
|
|
661
|
-
let trust = 'UNKNOWN';
|
|
662
|
-
let trustReason = '';
|
|
663
|
-
const tier = ['HIGH', 'MEDIUM', 'LOW'];
|
|
664
|
-
|
|
665
661
|
const blindSignals = [];
|
|
666
662
|
if (blindSpots.parseFailures.count > 0) blindSignals.push(`${blindSpots.parseFailures.count} parse failure(s)`);
|
|
663
|
+
if (blindSpots.parseRecoveries.count > 0) blindSignals.push(`${blindSpots.parseRecoveries.count} parse-recovery file(s)`);
|
|
667
664
|
if (blindSpots.evalCalls.count > 0) blindSignals.push(`${blindSpots.evalCalls.count} eval/exec use(s) in ${blindSpots.evalCalls.fileCount} file(s)`);
|
|
668
665
|
if (blindSpots.reflection.count > 0) blindSignals.push(`${blindSpots.reflection.count} reflection use(s) in ${blindSpots.reflection.fileCount} file(s)`);
|
|
669
666
|
if (blindSpots.dynamicImports.count > 0) blindSignals.push(`${blindSpots.dynamicImports.count} dynamic import(s) in ${blindSpots.dynamicImports.fileCount} file(s)`);
|
|
670
667
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const capped = idx > baseIdx;
|
|
697
|
-
trust = tier[idx];
|
|
698
|
-
const reasons = [`${(safePct * 100).toFixed(1)}% of found edges have confidence ≥ 0.5`];
|
|
699
|
-
if (blindSignals.length) {
|
|
700
|
-
reasons.push(capped
|
|
701
|
-
? `capped at MEDIUM — pervasive blind spots may hide edges the sample can't see: ${blindSignals.join(', ')}`
|
|
702
|
-
: `blind spots (caveat — coverage measures found edges, not completeness): ${blindSignals.join(', ')}`);
|
|
703
|
-
}
|
|
704
|
-
trustReason = reasons.join('; ');
|
|
705
|
-
} else if (coverage) {
|
|
706
|
-
// Sampled but zero edges — can't say anything about confidence.
|
|
707
|
-
trust = 'UNKNOWN';
|
|
708
|
-
trustReason = 'no edges sampled (empty scope or filter matched nothing)';
|
|
709
|
-
} else if (fileCounts.scanned > 0) {
|
|
710
|
-
// Cheap path (no --deep): no measurement, so blind spots are the only
|
|
711
|
-
// signal — bounded to one tier total. Run --deep for a measured verdict.
|
|
712
|
-
let idx = 0;
|
|
713
|
-
if (blindSpots.parseFailures.count > 0) idx = Math.max(idx, 1);
|
|
714
|
-
if (blindSpots.evalCalls.count + blindSpots.reflection.count + blindSpots.dynamicImports.count > 0) {
|
|
715
|
-
idx = Math.min(2, idx + 1);
|
|
668
|
+
// Trust is task-specific. A healthy index can be excellent for navigation
|
|
669
|
+
// while still requiring review before a breaking refactor or deletion.
|
|
670
|
+
// Never infer semantic accuracy from rule-assigned confidence decimals.
|
|
671
|
+
const indexLevel = fileCounts.scanned === 0 ? 'UNKNOWN'
|
|
672
|
+
: blindSpots.parseFailures.count > 0 || blindSpots.parseRecoveries.count > 0 ||
|
|
673
|
+
cache.fresh === false ? 'MEDIUM' : 'HIGH';
|
|
674
|
+
const indexReason = fileCounts.scanned === 0 ? 'empty scope'
|
|
675
|
+
: blindSpots.parseFailures.count > 0
|
|
676
|
+
? `${blindSpots.parseFailures.count} file(s) failed to parse`
|
|
677
|
+
: blindSpots.parseRecoveries.count > 0
|
|
678
|
+
? `${blindSpots.parseRecoveries.count} file(s) required parser recovery; indexed results may be partial`
|
|
679
|
+
: cache.fresh === false ? 'index cache is stale' : 'fresh index; no parse failures';
|
|
680
|
+
|
|
681
|
+
let evidenceLevel = 'UNKNOWN';
|
|
682
|
+
let evidenceReason = 'not sampled; run --deep for a stratified evidence profile';
|
|
683
|
+
if (evidenceProfile) {
|
|
684
|
+
if (evidenceProfile.total === 0) {
|
|
685
|
+
evidenceReason = 'sample contained no caller edges';
|
|
686
|
+
} else {
|
|
687
|
+
const confirmedShare = evidenceProfile.confirmed / evidenceProfile.total;
|
|
688
|
+
evidenceLevel = !evidenceProfile.adequate ? 'UNKNOWN'
|
|
689
|
+
: confirmedShare >= 0.85 ? 'HIGH'
|
|
690
|
+
: confirmedShare >= 0.55 ? 'MEDIUM' : 'LOW';
|
|
691
|
+
evidenceReason = `${(confirmedShare * 100).toFixed(1)}% confirmed-evidence edges across ${evidenceProfile.sampled} pinned definitions`;
|
|
692
|
+
if (!evidenceProfile.adequate) evidenceReason += '; sample too small for a readiness decision';
|
|
716
693
|
}
|
|
717
|
-
trust = tier[idx];
|
|
718
|
-
trustReason = blindSignals.length
|
|
719
|
-
? `coverage not deep-checked (run --deep); blind spots: ${blindSignals.join(', ')}`
|
|
720
|
-
: 'no parse failures; coverage not deep-checked (run --deep)';
|
|
721
694
|
}
|
|
722
695
|
|
|
696
|
+
const dynamicCount = blindSpots.evalCalls.count + blindSpots.reflection.count + blindSpots.dynamicImports.count;
|
|
697
|
+
const semanticLevel = blindSpots.parseFailures.count > 0 || blindSpots.parseRecoveries.count > 0 ? 'LOW'
|
|
698
|
+
: dynamicCount > 0 ? 'MEDIUM' : 'UNKNOWN';
|
|
699
|
+
const semanticReason = blindSignals.length
|
|
700
|
+
? `semantic recall may miss runtime-resolved edges: ${blindSignals.join(', ')}`
|
|
701
|
+
: 'no known runtime blind spots detected; alias/dynamic completeness is not compiler-verified locally';
|
|
702
|
+
|
|
703
|
+
const navigationLevel = indexLevel;
|
|
704
|
+
const refactorLevel = indexLevel === 'UNKNOWN' || evidenceLevel === 'UNKNOWN' ? 'UNKNOWN'
|
|
705
|
+
: indexLevel === 'HIGH' && evidenceLevel === 'HIGH' && semanticLevel === 'UNKNOWN' ? 'MEDIUM'
|
|
706
|
+
: indexLevel === 'LOW' || evidenceLevel === 'LOW' || semanticLevel === 'LOW' ? 'LOW' : 'MEDIUM';
|
|
707
|
+
const deletionLevel = refactorLevel === 'LOW' ? 'LOW' : 'REVIEW';
|
|
708
|
+
const dimensions = {
|
|
709
|
+
index: { level: indexLevel, reason: indexReason },
|
|
710
|
+
evidence: { level: evidenceLevel, reason: evidenceReason },
|
|
711
|
+
semanticRecall: { level: semanticLevel, reason: semanticReason },
|
|
712
|
+
navigation: { level: navigationLevel, reason: indexReason },
|
|
713
|
+
refactor: {
|
|
714
|
+
level: refactorLevel,
|
|
715
|
+
reason: refactorLevel === 'UNKNOWN'
|
|
716
|
+
? 'run --deep; review unverified and non-call occurrences before changing code'
|
|
717
|
+
: 'text-ground accounting is available; aliases, reflection, and unverified edges still require review',
|
|
718
|
+
},
|
|
719
|
+
deletion: {
|
|
720
|
+
level: deletionLevel,
|
|
721
|
+
reason: 'deletion additionally requires usages/deadcode review and tests; caller accounting alone is insufficient',
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
const trust = refactorLevel;
|
|
725
|
+
const trustReason = dimensions.refactor.reason;
|
|
726
|
+
|
|
723
727
|
return {
|
|
724
728
|
root: index.root,
|
|
725
729
|
version: require('../package.json').version, // running ucn version — surfaces MCP/CLI drift (field-report #3)
|
|
@@ -727,10 +731,16 @@ function doctor(index, options = {}) {
|
|
|
727
731
|
symbols: totalSymbols,
|
|
728
732
|
languages: langs,
|
|
729
733
|
blindSpots,
|
|
730
|
-
|
|
734
|
+
evidenceProfile,
|
|
735
|
+
// Backward-compatible field name. `kind` prevents consumers from
|
|
736
|
+
// mistaking this for semantic coverage or measured accuracy.
|
|
737
|
+
coverage: evidenceProfile,
|
|
731
738
|
cache,
|
|
739
|
+
commandTrust: summarizeCommandTrust(),
|
|
732
740
|
trust,
|
|
733
741
|
trustReason,
|
|
742
|
+
trustScope: 'refactor-readiness',
|
|
743
|
+
dimensions,
|
|
734
744
|
...(inFilter && { filter: inFilter }),
|
|
735
745
|
};
|
|
736
746
|
}
|
|
@@ -739,35 +749,83 @@ function doctor(index, options = {}) {
|
|
|
739
749
|
* Sample-based coverage: pick up to N symbols, run findCallers, bucket confidence.
|
|
740
750
|
* Doesn't pretend to be exhaustive — meant for a fast trust signal, not an audit.
|
|
741
751
|
*/
|
|
742
|
-
function
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
752
|
+
function computeEvidenceProfile(index, { sampleSize, matchInFilter }) {
|
|
753
|
+
const profile = {
|
|
754
|
+
kind: 'evidence-profile-not-accuracy',
|
|
755
|
+
high: 0, medium: 0, low: 0,
|
|
756
|
+
confirmed: 0, unverified: 0, total: 0,
|
|
757
|
+
sampled: 0, candidateSymbols: 0,
|
|
758
|
+
adequate: false, representative: false,
|
|
759
|
+
byLanguage: Object.create(null), byKind: Object.create(null),
|
|
760
|
+
};
|
|
761
|
+
const groups = new Map();
|
|
762
|
+
const seenHandles = new Set();
|
|
763
|
+
for (const [, arr] of index.symbols) {
|
|
746
764
|
for (const sym of arr) {
|
|
747
|
-
if (!sym || !sym.relativePath) continue;
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
765
|
+
if (!sym || !sym.relativePath || !matchInFilter(sym.relativePath)) continue;
|
|
766
|
+
if (sym.type !== 'method' && sym.type !== 'function' && sym.type !== 'constructor') continue;
|
|
767
|
+
const handle = `${sym.relativePath}:${sym.startLine}:${sym.name}`;
|
|
768
|
+
if (seenHandles.has(handle)) continue;
|
|
769
|
+
seenHandles.add(handle);
|
|
770
|
+
const lang = index.files.get(sym.file)?.language || 'unknown';
|
|
771
|
+
const key = `${lang}:${sym.type}`;
|
|
772
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
773
|
+
groups.get(key).push({ sym, handle, lang });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
profile.candidateSymbols = seenHandles.size;
|
|
777
|
+
for (const items of groups.values()) {
|
|
778
|
+
items.sort((a, b) => codeUnitCompare(a.handle, b.handle));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Round-robin across language+kind buckets so discovery order, duplicate
|
|
782
|
+
// names, and one large language cannot dominate the verdict.
|
|
783
|
+
const orderedGroups = [...groups.entries()].sort((a, b) => codeUnitCompare(a[0], b[0]));
|
|
784
|
+
const sample = [];
|
|
785
|
+
for (let round = 0; sample.length < sampleSize; round++) {
|
|
786
|
+
let added = false;
|
|
787
|
+
for (const [, items] of orderedGroups) {
|
|
788
|
+
if (items[round]) {
|
|
789
|
+
sample.push(items[round]);
|
|
790
|
+
added = true;
|
|
791
|
+
if (sample.length >= sampleSize) break;
|
|
752
792
|
}
|
|
753
793
|
}
|
|
754
|
-
if (
|
|
794
|
+
if (!added) break;
|
|
755
795
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const callers = index.findCallers(name, {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
796
|
+
profile.sampled = sample.length;
|
|
797
|
+
|
|
798
|
+
for (const { sym, lang } of sample) {
|
|
799
|
+
profile.byLanguage[lang] = (profile.byLanguage[lang] || 0) + 1;
|
|
800
|
+
profile.byKind[sym.type] = (profile.byKind[sym.type] || 0) + 1;
|
|
801
|
+
const callers = index.findCallers(sym.name, {
|
|
802
|
+
includeMethods: true,
|
|
803
|
+
includeUncertain: true,
|
|
804
|
+
targetDefinitions: [sym],
|
|
805
|
+
collectAccount: true,
|
|
806
|
+
});
|
|
807
|
+
const allEdges = [...callers, ...(callers.unverifiedEntries || [])];
|
|
808
|
+
const seenSites = new Set();
|
|
809
|
+
for (const c of allEdges) {
|
|
810
|
+
const site = `${c.file || c.relativePath}:${c.line}:${c.tier || c.reason || ''}`;
|
|
811
|
+
if (seenSites.has(site)) continue;
|
|
812
|
+
seenSites.add(site);
|
|
813
|
+
const conf = c.confidence != null ? c.confidence : 0;
|
|
814
|
+
profile.total++;
|
|
815
|
+
if (c.tier === 'unverified' || c.reason) profile.unverified++;
|
|
816
|
+
else profile.confirmed++;
|
|
817
|
+
if (conf > 0.8) profile.high++;
|
|
818
|
+
else if (conf >= 0.5) profile.medium++;
|
|
819
|
+
else profile.low++;
|
|
768
820
|
}
|
|
769
821
|
}
|
|
770
|
-
|
|
822
|
+
const representedGroups = Object.keys(profile.byLanguage).length;
|
|
823
|
+
profile.representative = representedGroups === new Set(
|
|
824
|
+
[...groups.keys()].map(k => k.split(':')[0])
|
|
825
|
+
).size;
|
|
826
|
+
const minSymbols = Math.min(30, profile.candidateSymbols);
|
|
827
|
+
profile.adequate = profile.sampled >= minSymbols && profile.total >= 20 && profile.representative;
|
|
828
|
+
return profile;
|
|
771
829
|
}
|
|
772
830
|
|
|
773
831
|
/**
|