ucn 4.0.1 → 4.0.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/core/analysis.js +13 -13
- package/core/output/analysis.js +15 -0
- package/core/output/doctor.js +13 -3
- package/core/reporting.js +89 -71
- package/core/shared.js +45 -0
- package/mcp/server.js +3 -1
- package/package.json +1 -1
package/core/analysis.js
CHANGED
|
@@ -12,7 +12,7 @@ const path = require('path');
|
|
|
12
12
|
const { execFileSync } = require('child_process');
|
|
13
13
|
const { parse } = require('./parser');
|
|
14
14
|
const { detectLanguage, langTraits } = require('../languages');
|
|
15
|
-
const { NON_CALLABLE_TYPES, addTestExclusions } = require('./shared');
|
|
15
|
+
const { NON_CALLABLE_TYPES, addTestExclusions, countTextBlindspots } = require('./shared');
|
|
16
16
|
const { computeReachability, symbolKey } = require('./entrypoints');
|
|
17
17
|
const { getLanguageModule } = require('../languages');
|
|
18
18
|
|
|
@@ -600,20 +600,20 @@ function detectCompleteness(index) {
|
|
|
600
600
|
const content = index._readFile(filePath);
|
|
601
601
|
|
|
602
602
|
if (langTraits(fileEntry.language)?.hasDynamicImports) {
|
|
603
|
-
// Dynamic imports:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
evalUsage += (content.match(/new\s+Function\s*\(/g) || []).length;
|
|
603
|
+
// Dynamic imports: use the parser's structural count — the SAME
|
|
604
|
+
// source `doctor` uses — instead of a text regex. The old
|
|
605
|
+
// /import\s*\(/ matched Python grouped imports `from x import
|
|
606
|
+
// (...)`, flashing a false "N dynamic imports" incompleteness
|
|
607
|
+
// warning on essentially every Python project (field-report #2,
|
|
608
|
+
// reviewer-confirmed: doctor and about now agree on one count).
|
|
609
|
+
dynamicImports += fileEntry.dynamicImports || 0;
|
|
611
610
|
}
|
|
612
611
|
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
612
|
+
// eval/exec and reflection: the SAME shared counter doctor uses, so
|
|
613
|
+
// the about footer and the trust report never diverge (field-report #2).
|
|
614
|
+
const bs = countTextBlindspots(content, fileEntry.language);
|
|
615
|
+
evalUsage += bs.eval;
|
|
616
|
+
reflectionUsage += bs.reflection;
|
|
617
617
|
} catch (e) {
|
|
618
618
|
// Skip unreadable files
|
|
619
619
|
}
|
package/core/output/analysis.js
CHANGED
|
@@ -732,6 +732,21 @@ function formatAbout(about, options = {}) {
|
|
|
732
732
|
for (const c of testTop) renderAboutCaller(c);
|
|
733
733
|
}
|
|
734
734
|
if (aboutCallerReach.note) lines.push(aboutCallerReach.note);
|
|
735
|
+
|
|
736
|
+
// Field-report #5: when every CONFIRMED caller is a test and the
|
|
737
|
+
// production call sites are method-style (landed in UNVERIFIED as
|
|
738
|
+
// method-ambiguous — e.g. a module function sharing a name with a
|
|
739
|
+
// method), the bare "0 prod" count reads like dead code. Flag it so the
|
|
740
|
+
// empty prod count isn't misread; the real calls are listed below.
|
|
741
|
+
if (prodTop.length === 0 && testTop.length > 0) {
|
|
742
|
+
const uv = about.callers.unverified;
|
|
743
|
+
const methodStyle = uv && uv.top
|
|
744
|
+
? uv.top.some(u => u.reason === 'method-ambiguous' || u.reason === 'possible-dispatch')
|
|
745
|
+
: false;
|
|
746
|
+
if (uv && uv.total > 0 && methodStyle) {
|
|
747
|
+
lines.push(` Note: 0 production callers CONFIRMED — the ${uv.total} call site(s) under UNVERIFIED below include method-style calls that may bind to this or a same-name method, so this is not dead code.`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
735
750
|
}
|
|
736
751
|
|
|
737
752
|
// Callers — UNVERIFIED tier (always visible; the contract forbids hiding)
|
package/core/output/doctor.js
CHANGED
|
@@ -13,6 +13,7 @@ function formatDoctor(result) {
|
|
|
13
13
|
const lines = [];
|
|
14
14
|
lines.push(`UCN Trust Report — ${result.root}`);
|
|
15
15
|
lines.push('═'.repeat(60));
|
|
16
|
+
if (result.version) lines.push(`Version: ucn ${result.version}`);
|
|
16
17
|
lines.push(`Index: ${result.files.scanned} file${result.files.scanned === 1 ? '' : 's'}, ${result.symbols} symbol${result.symbols === 1 ? '' : 's'}`);
|
|
17
18
|
|
|
18
19
|
if (result.filter) lines.push(`Filter: ${result.filter}`);
|
|
@@ -60,13 +61,22 @@ function formatDoctor(result) {
|
|
|
60
61
|
['Reflection', bs.reflection],
|
|
61
62
|
['Parse failures', bs.parseFailures],
|
|
62
63
|
];
|
|
64
|
+
const unitFor = { 'Dynamic imports': 'import', 'Eval/exec calls': 'use', 'Reflection': 'use', 'Parse failures': 'failure' };
|
|
63
65
|
let anyBlindSpot = false;
|
|
64
66
|
for (const [label, info] of bsLines) {
|
|
65
67
|
if (info && info.count > 0) {
|
|
66
68
|
anyBlindSpot = true;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// fileCount is the TRUE (uncapped) number of files; info.files is a
|
|
70
|
+
// capped display sample. Show "N use(s) in M file(s)" and, when the
|
|
71
|
+
// sample is truncated, "... and K more file(s)" against the true M —
|
|
72
|
+
// never present the display cap as the population (field-report #2).
|
|
73
|
+
const fileCount = info.fileCount != null ? info.fileCount : info.files.length;
|
|
74
|
+
const unit = unitFor[label] || 'use';
|
|
75
|
+
lines.push(` ${label}: ${info.count} ${unit}${info.count === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`);
|
|
76
|
+
const shownFiles = info.files.slice(0, 3);
|
|
77
|
+
const sample = shownFiles.map(f => ` - ${f}`).join('\n');
|
|
78
|
+
const moreFiles = fileCount - shownFiles.length;
|
|
79
|
+
const more = moreFiles > 0 ? `\n ... and ${moreFiles} more file${moreFiles === 1 ? '' : 's'}` : '';
|
|
70
80
|
if (sample) lines.push(sample + more);
|
|
71
81
|
}
|
|
72
82
|
}
|
package/core/reporting.js
CHANGED
|
@@ -538,28 +538,23 @@ function doctor(index, options = {}) {
|
|
|
538
538
|
const fileCounts = { total: 0, scanned: 0 };
|
|
539
539
|
const langs = {};
|
|
540
540
|
let totalSymbols = 0; // counted post-filter for accuracy when --in is set
|
|
541
|
+
// Each category tracks: count = total OCCURRENCES (uses), fileCount = TRUE
|
|
542
|
+
// number of files affected (uncapped), files = a capped sample for display.
|
|
543
|
+
// Keeping count and fileCount distinct is what lets the formatter say
|
|
544
|
+
// "481 uses in 121 files" instead of mislabeling a file count as uses or
|
|
545
|
+
// presenting the 10-file display cap as the population (field-report #2).
|
|
546
|
+
const BLINDSPOT_FILE_CAP = 10;
|
|
541
547
|
const blindSpots = {
|
|
542
|
-
dynamicImports: { count: 0, files: [] },
|
|
543
|
-
evalCalls: { count: 0, files: [] },
|
|
544
|
-
reflection: { count: 0, files: [] },
|
|
545
|
-
parseFailures: { count: 0, files: [] },
|
|
548
|
+
dynamicImports: { count: 0, fileCount: 0, files: [] },
|
|
549
|
+
evalCalls: { count: 0, fileCount: 0, files: [] },
|
|
550
|
+
reflection: { count: 0, fileCount: 0, files: [] },
|
|
551
|
+
parseFailures: { count: 0, fileCount: 0, files: [] },
|
|
546
552
|
};
|
|
547
553
|
|
|
548
|
-
// Reflection signals
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
javascript: /\bnew Function\s*\(|\bReflect\.\w+\s*\(/,
|
|
553
|
-
typescript: /\bnew Function\s*\(|\bReflect\.\w+\s*\(/,
|
|
554
|
-
go: /"reflect"|reflect\.\w+\s*\(/,
|
|
555
|
-
java: /\.getDeclaredMethod\b|\.getMethod\b|\.getDeclaredField\b|Class\.forName\b/,
|
|
556
|
-
rust: /\bAny::downcast/,
|
|
557
|
-
};
|
|
558
|
-
const EVAL_PATTERNS = {
|
|
559
|
-
python: /\b(eval|exec)\s*\(/,
|
|
560
|
-
javascript: /\beval\s*\(/,
|
|
561
|
-
typescript: /\beval\s*\(/,
|
|
562
|
-
};
|
|
554
|
+
// Reflection/eval signals come from the shared text-blind-spot counter
|
|
555
|
+
// (core/shared.js) — the SAME routine detectCompleteness uses for the about
|
|
556
|
+
// footer, so the two never drift (field-report #2). Occurrence counts.
|
|
557
|
+
const { hasTextBlindspots, countTextBlindspots } = require('./shared');
|
|
563
558
|
|
|
564
559
|
for (const [filePath, fe] of index.files) {
|
|
565
560
|
fileCounts.total++;
|
|
@@ -574,29 +569,22 @@ function doctor(index, options = {}) {
|
|
|
574
569
|
langs[lang].lines += fe.lines || 0;
|
|
575
570
|
totalSymbols += (fe.symbols || []).length;
|
|
576
571
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
572
|
+
const recordBlind = (cat, occurrences) => {
|
|
573
|
+
if (occurrences <= 0) return;
|
|
574
|
+
cat.count += occurrences;
|
|
575
|
+
cat.fileCount++;
|
|
576
|
+
if (cat.files.length < BLINDSPOT_FILE_CAP) cat.files.push(rel);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
if (fe.dynamicImports && fe.dynamicImports > 0) recordBlind(blindSpots.dynamicImports, fe.dynamicImports);
|
|
580
|
+
if (fe.parseError) recordBlind(blindSpots.parseFailures, 1);
|
|
585
581
|
|
|
586
|
-
// Read file once for eval/reflection signals
|
|
587
|
-
|
|
588
|
-
const reflRe = REFLECTION_PATTERNS[lang];
|
|
589
|
-
if (evalRe || reflRe) {
|
|
582
|
+
// Read file once for eval/reflection signals (shared counter).
|
|
583
|
+
if (hasTextBlindspots(lang)) {
|
|
590
584
|
try {
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (blindSpots.evalCalls.files.length < 10) blindSpots.evalCalls.files.push(rel);
|
|
595
|
-
}
|
|
596
|
-
if (reflRe && reflRe.test(content)) {
|
|
597
|
-
blindSpots.reflection.count++;
|
|
598
|
-
if (blindSpots.reflection.files.length < 10) blindSpots.reflection.files.push(rel);
|
|
599
|
-
}
|
|
585
|
+
const bs = countTextBlindspots(fs.readFileSync(filePath, 'utf-8'), lang);
|
|
586
|
+
recordBlind(blindSpots.evalCalls, bs.eval);
|
|
587
|
+
recordBlind(blindSpots.reflection, bs.reflection);
|
|
600
588
|
} catch (e) { /* ignore read errors */ }
|
|
601
589
|
}
|
|
602
590
|
}
|
|
@@ -620,57 +608,87 @@ function doctor(index, options = {}) {
|
|
|
620
608
|
|
|
621
609
|
// Compute trust verdict.
|
|
622
610
|
//
|
|
623
|
-
// 1
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
611
|
+
// Field-report #1: the old logic dropped the tier by one PER blind-spot
|
|
612
|
+
// category present, so any non-trivial Python/TS project (all of which have
|
|
613
|
+
// some getattr/eval/dynamic import) was forced to LOW even when --deep
|
|
614
|
+
// measured ~99% of edges at confidence ≥ 0.5 — a self-contradicting verdict
|
|
615
|
+
// ("99.1% ... LOW") that trains agents to distrust a healthy index. The fix:
|
|
616
|
+
// - When --deep coverage exists it drives the tier. Coverage measures the
|
|
617
|
+
// CONFIDENCE of edges UCN FOUND, NOT completeness — a reflection-hidden
|
|
618
|
+
// edge is absent from the sample, never a low-confidence edge dragging
|
|
619
|
+
// the % down — so sparse blind spots are a CAVEAT, while PERVASIVE ones
|
|
620
|
+
// (a large share of files) can hide edges the sample can't see and cap
|
|
621
|
+
// the verdict at MEDIUM (density, not mere presence; see below).
|
|
622
|
+
// - Parse failures are a separate exception: a file UCN couldn't parse is
|
|
623
|
+
// not in the sample at all, a genuine uncounted hole → cap at MEDIUM.
|
|
624
|
+
// - Without --deep there is no measurement, so blind spots are the only
|
|
625
|
+
// signal — but bounded to ONE tier total (not one per category), so a
|
|
626
|
+
// handful of getattr doesn't read as untrustworthy.
|
|
629
627
|
let trust = 'UNKNOWN';
|
|
630
628
|
let trustReason = '';
|
|
631
|
-
const
|
|
629
|
+
const tier = ['HIGH', 'MEDIUM', 'LOW'];
|
|
630
|
+
|
|
631
|
+
const blindSignals = [];
|
|
632
|
+
if (blindSpots.parseFailures.count > 0) blindSignals.push(`${blindSpots.parseFailures.count} parse failure(s)`);
|
|
633
|
+
if (blindSpots.evalCalls.count > 0) blindSignals.push(`${blindSpots.evalCalls.count} eval/exec use(s) in ${blindSpots.evalCalls.fileCount} file(s)`);
|
|
634
|
+
if (blindSpots.reflection.count > 0) blindSignals.push(`${blindSpots.reflection.count} reflection use(s) in ${blindSpots.reflection.fileCount} file(s)`);
|
|
635
|
+
if (blindSpots.dynamicImports.count > 0) blindSignals.push(`${blindSpots.dynamicImports.count} dynamic import(s) in ${blindSpots.dynamicImports.fileCount} file(s)`);
|
|
632
636
|
|
|
633
637
|
if (coverage && coverage.total > 0) {
|
|
634
638
|
const safe = coverage.high + coverage.medium;
|
|
635
639
|
const safePct = safe / coverage.total;
|
|
636
|
-
let
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
640
|
+
let idx = safePct >= 0.85 ? 0 : safePct >= 0.6 ? 1 : 2;
|
|
641
|
+
// Parse failures: unparsed files aren't in the sample at all.
|
|
642
|
+
if (blindSpots.parseFailures.count > 0) idx = Math.max(idx, 1);
|
|
643
|
+
// Coverage measures the CONFIDENCE of edges UCN found, NOT completeness:
|
|
644
|
+
// a call hidden behind reflection/dynamic dispatch is simply absent from
|
|
645
|
+
// findCallers' result, never a low-confidence edge that drags the % down.
|
|
646
|
+
// So when blind spots are PERVASIVE — affecting a large share of files —
|
|
647
|
+
// they can hide a real fraction of the call graph that the sample can't
|
|
648
|
+
// see, and the verdict is capped at MEDIUM. Density, not mere presence:
|
|
649
|
+
// a handful of getattr stays a caveat (the old code dropped a tier per
|
|
650
|
+
// category, forcing every project to LOW); reflection across half the
|
|
651
|
+
// files does cap. Gated on a file-count floor — file share is meaningless
|
|
652
|
+
// for a handful of files, so small projects ride on coverage alone.
|
|
653
|
+
const scanned = fileCounts.scanned || 1;
|
|
654
|
+
const share = (fc) => fc / scanned;
|
|
655
|
+
const pervasiveBlindSpot = scanned >= 10 && (
|
|
656
|
+
share(blindSpots.reflection.fileCount) >= 0.5 ||
|
|
657
|
+
share(blindSpots.dynamicImports.fileCount) >= 0.4 ||
|
|
658
|
+
share(blindSpots.evalCalls.fileCount) >= 0.15
|
|
659
|
+
);
|
|
660
|
+
const baseIdx = idx;
|
|
661
|
+
if (pervasiveBlindSpot) idx = Math.max(idx, 1);
|
|
662
|
+
const capped = idx > baseIdx;
|
|
650
663
|
trust = tier[idx];
|
|
651
|
-
|
|
664
|
+
const reasons = [`${(safePct * 100).toFixed(1)}% of found edges have confidence ≥ 0.5`];
|
|
665
|
+
if (blindSignals.length) {
|
|
666
|
+
reasons.push(capped
|
|
667
|
+
? `capped at MEDIUM — pervasive blind spots may hide edges the sample can't see: ${blindSignals.join(', ')}`
|
|
668
|
+
: `blind spots (caveat — coverage measures found edges, not completeness): ${blindSignals.join(', ')}`);
|
|
669
|
+
}
|
|
652
670
|
trustReason = reasons.join('; ');
|
|
653
671
|
} else if (coverage) {
|
|
654
672
|
// Sampled but zero edges — can't say anything about confidence.
|
|
655
673
|
trust = 'UNKNOWN';
|
|
656
674
|
trustReason = 'no edges sampled (empty scope or filter matched nothing)';
|
|
657
675
|
} else if (fileCounts.scanned > 0) {
|
|
658
|
-
// Cheap path (no --deep):
|
|
659
|
-
|
|
676
|
+
// Cheap path (no --deep): no measurement, so blind spots are the only
|
|
677
|
+
// signal — bounded to one tier total. Run --deep for a measured verdict.
|
|
660
678
|
let idx = 0;
|
|
661
|
-
|
|
662
|
-
if (blindSpots.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (blindSpots.dynamicImports.count > 0) { idx = Math.min(2, idx + 1); blindSignals.push(`${blindSpots.dynamicImports.count} dynamic import(s)`); }
|
|
679
|
+
if (blindSpots.parseFailures.count > 0) idx = Math.max(idx, 1);
|
|
680
|
+
if (blindSpots.evalCalls.count + blindSpots.reflection.count + blindSpots.dynamicImports.count > 0) {
|
|
681
|
+
idx = Math.min(2, idx + 1);
|
|
682
|
+
}
|
|
666
683
|
trust = tier[idx];
|
|
667
684
|
trustReason = blindSignals.length
|
|
668
|
-
? `coverage not deep-checked; blind spots: ${blindSignals.join(', ')}`
|
|
669
|
-
: 'no parse failures; coverage not deep-checked';
|
|
685
|
+
? `coverage not deep-checked (run --deep); blind spots: ${blindSignals.join(', ')}`
|
|
686
|
+
: 'no parse failures; coverage not deep-checked (run --deep)';
|
|
670
687
|
}
|
|
671
688
|
|
|
672
689
|
return {
|
|
673
690
|
root: index.root,
|
|
691
|
+
version: require('../package.json').version, // running ucn version — surfaces MCP/CLI drift (field-report #3)
|
|
674
692
|
files: fileCounts,
|
|
675
693
|
symbols: totalSymbols,
|
|
676
694
|
languages: langs,
|
package/core/shared.js
CHANGED
|
@@ -159,6 +159,49 @@ function isOverrideMarked(def) {
|
|
|
159
159
|
return false;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Per-language text patterns for the "blind spots" UCN's AST can't follow:
|
|
163
|
+
// eval/exec-style code execution and reflection (dynamic attribute access /
|
|
164
|
+
// dynamic dispatch). ONE source of truth so doctor's trust scan and
|
|
165
|
+
// detectCompleteness's about-footer warning count identically (field-report #2:
|
|
166
|
+
// they used to diverge — doctor 497 reflection vs footer 194, eval 3 vs 2 —
|
|
167
|
+
// because each kept its own regex set). Dynamic imports are NOT here: those are
|
|
168
|
+
// structural (fileEntry.dynamicImports), the AST-accurate count both paths share.
|
|
169
|
+
// `new Function(...)` is categorized as eval (code execution), not reflection.
|
|
170
|
+
const BLINDSPOT_TEXT_PATTERNS = {
|
|
171
|
+
reflection: {
|
|
172
|
+
python: /\b(getattr|hasattr|setattr|__import__|importlib\.import_module)\s*\(/g,
|
|
173
|
+
javascript: /\bReflect\.\w+\s*\(/g,
|
|
174
|
+
typescript: /\bReflect\.\w+\s*\(/g,
|
|
175
|
+
go: /\breflect\.\w+\s*\(/g,
|
|
176
|
+
java: /\.getDeclaredMethod\b|\.getMethod\b|\.getDeclaredField\b|Class\.forName\b/g,
|
|
177
|
+
rust: /\bAny::downcast/g,
|
|
178
|
+
},
|
|
179
|
+
eval: {
|
|
180
|
+
python: /\b(eval|exec)\s*\(/g,
|
|
181
|
+
javascript: /\beval\s*\(|\bnew\s+Function\s*\(/g,
|
|
182
|
+
typescript: /\beval\s*\(|\bnew\s+Function\s*\(/g,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** True when a language has any text-blind-spot pattern (so callers can skip the file read otherwise). */
|
|
187
|
+
function hasTextBlindspots(language) {
|
|
188
|
+
return !!(BLINDSPOT_TEXT_PATTERNS.reflection[language] || BLINDSPOT_TEXT_PATTERNS.eval[language]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Count text-detected blind spots (eval/exec, reflection) in one file's source.
|
|
193
|
+
* Returns { eval, reflection } OCCURRENCE counts (global match). Shared by doctor
|
|
194
|
+
* and detectCompleteness so both report the same numbers (field-report #2).
|
|
195
|
+
*/
|
|
196
|
+
function countTextBlindspots(content, language) {
|
|
197
|
+
const reRe = BLINDSPOT_TEXT_PATTERNS.reflection[language];
|
|
198
|
+
const evRe = BLINDSPOT_TEXT_PATTERNS.eval[language];
|
|
199
|
+
return {
|
|
200
|
+
eval: evRe ? (content.match(evRe) || []).length : 0,
|
|
201
|
+
reflection: reRe ? (content.match(reRe) || []).length : 0,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
162
205
|
module.exports = {
|
|
163
206
|
pickBestDefinition,
|
|
164
207
|
addTestExclusions,
|
|
@@ -169,4 +212,6 @@ module.exports = {
|
|
|
169
212
|
looksLikeHandle,
|
|
170
213
|
isTestPath,
|
|
171
214
|
isOverrideMarked,
|
|
215
|
+
hasTextBlindspots,
|
|
216
|
+
countTextBlindspots,
|
|
172
217
|
};
|
package/mcp/server.js
CHANGED
|
@@ -791,7 +791,9 @@ server.registerTool(
|
|
|
791
791
|
async function main() {
|
|
792
792
|
const transport = new StdioServerTransport();
|
|
793
793
|
await server.connect(transport);
|
|
794
|
-
|
|
794
|
+
// Print the running version so MCP-vs-CLI drift is visible (field-report #3:
|
|
795
|
+
// a stale `npx -y ucn` cache can silently run an older engine than the CLI).
|
|
796
|
+
console.error(`UCN MCP server v${require('../package.json').version} running on stdio`);
|
|
795
797
|
}
|
|
796
798
|
|
|
797
799
|
main().catch(e => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|