ucn 3.8.23 → 3.8.25
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 +114 -11
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +936 -32
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph.js +24 -2
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -10
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +9 -1
package/core/check.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/check.js — Pre-commit summary command.
|
|
3
|
+
*
|
|
4
|
+
* Composes diff-impact + verify + affected-tests into a single output.
|
|
5
|
+
* Tells the caller, in one shot:
|
|
6
|
+
* - which functions changed
|
|
7
|
+
* - which call sites might break (signature drift)
|
|
8
|
+
* - which tests are likely affected
|
|
9
|
+
* - which new functions look orphaned
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { diffImpact } = require('./analysis');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run the pre-commit check.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} index - ProjectIndex
|
|
20
|
+
* @param {object} options - { base, staged, file, limit }
|
|
21
|
+
* @returns {object}
|
|
22
|
+
*/
|
|
23
|
+
function check(index, options = {}) {
|
|
24
|
+
let dr;
|
|
25
|
+
try {
|
|
26
|
+
dr = diffImpact(index, {
|
|
27
|
+
base: options.base || 'HEAD',
|
|
28
|
+
staged: !!options.staged,
|
|
29
|
+
file: options.file,
|
|
30
|
+
});
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// Not a git repo, or git command failed — treat as empty
|
|
33
|
+
return {
|
|
34
|
+
base: options.base || 'HEAD',
|
|
35
|
+
staged: !!options.staged,
|
|
36
|
+
empty: true,
|
|
37
|
+
reason: e && e.message ? e.message : 'diff failed',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// diffImpact returns { base, functions, newFunctions, deletedFunctions }
|
|
42
|
+
const modified = (dr && Array.isArray(dr.functions)) ? dr.functions : [];
|
|
43
|
+
const added = (dr && Array.isArray(dr.newFunctions)) ? dr.newFunctions : [];
|
|
44
|
+
const deleted = (dr && Array.isArray(dr.deletedFunctions)) ? dr.deletedFunctions : [];
|
|
45
|
+
|
|
46
|
+
const allChanged = [
|
|
47
|
+
...modified.map(f => ({ ...f, _kind: 'modified' })),
|
|
48
|
+
...added.map(f => ({ ...f, _kind: 'added' })),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
if (!dr || (modified.length === 0 && added.length === 0 && deleted.length === 0)) {
|
|
52
|
+
return {
|
|
53
|
+
base: options.base || 'HEAD',
|
|
54
|
+
staged: !!options.staged,
|
|
55
|
+
empty: true,
|
|
56
|
+
reason: dr && dr.error ? dr.error : 'no changes detected',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const limit = options.limit && options.limit > 0 ? options.limit : null;
|
|
61
|
+
const changed = limit ? allChanged.slice(0, limit) : allChanged;
|
|
62
|
+
|
|
63
|
+
const items = [];
|
|
64
|
+
const reachable = computeReachableSet(index);
|
|
65
|
+
|
|
66
|
+
// For each changed function, run verify and gather caller summary
|
|
67
|
+
for (const fn of changed) {
|
|
68
|
+
const filePath = fn.relativePath || fn.file || '';
|
|
69
|
+
let verifyResult = null;
|
|
70
|
+
try {
|
|
71
|
+
verifyResult = index.verify(fn.name, { file: filePath });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
verifyResult = null;
|
|
74
|
+
}
|
|
75
|
+
// Note: verify() returns `mismatches` as a COUNT and `mismatchDetails` as the array.
|
|
76
|
+
const mismatches = verifyResult && Array.isArray(verifyResult.mismatchDetails)
|
|
77
|
+
? verifyResult.mismatchDetails
|
|
78
|
+
: [];
|
|
79
|
+
|
|
80
|
+
// For modified functions, the existing diffImpact result has `callers` already
|
|
81
|
+
let callers = Array.isArray(fn.callers) ? fn.callers : [];
|
|
82
|
+
if (callers.length === 0 && fn._kind === 'added') {
|
|
83
|
+
try {
|
|
84
|
+
callers = index.findCallers(fn.name, { includeMethods: true, includeUncertain: false }) || [];
|
|
85
|
+
} catch (e) { /* skip */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const item = {
|
|
89
|
+
name: fn.name,
|
|
90
|
+
file: filePath,
|
|
91
|
+
line: fn.startLine || fn.line,
|
|
92
|
+
kind: fn._kind,
|
|
93
|
+
callerCount: callers.length,
|
|
94
|
+
signatureMismatches: mismatches.length,
|
|
95
|
+
...(mismatches.length > 0 && { mismatches: mismatches.slice(0, 5) }),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Orphan = newly added with zero callers AND not detected as an entry point.
|
|
99
|
+
// (A new helper called by another new helper is still NOT orphan; reachability
|
|
100
|
+
// is rebuilt as the user iterates, and false positives here cause noise.)
|
|
101
|
+
if (item.kind === 'added' && callers.length === 0) {
|
|
102
|
+
// Check entry points: if the symbol is a known entry-point pattern, not orphan
|
|
103
|
+
let isEntry = false;
|
|
104
|
+
try {
|
|
105
|
+
const ep = require('./entrypoints');
|
|
106
|
+
if (typeof ep.detectEntrypoints === 'function') {
|
|
107
|
+
const eps = ep.detectEntrypoints(index) || [];
|
|
108
|
+
isEntry = eps.some(e => e.name === fn.name && (e.file === filePath || e.relativePath === filePath));
|
|
109
|
+
}
|
|
110
|
+
} catch (e) { /* skip */ }
|
|
111
|
+
item.orphan = !isEntry;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
items.push(item);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Surface deleted functions inline — they don't have line/file but still matter
|
|
118
|
+
for (const d of deleted) {
|
|
119
|
+
items.push({
|
|
120
|
+
name: d.name || '(unnamed)',
|
|
121
|
+
file: d.relativePath || d.file || '',
|
|
122
|
+
line: d.startLine || 0,
|
|
123
|
+
kind: 'deleted',
|
|
124
|
+
callerCount: 0,
|
|
125
|
+
signatureMismatches: 0,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Affected tests (top-level summary, capped)
|
|
130
|
+
let testFiles = [];
|
|
131
|
+
let testCount = 0;
|
|
132
|
+
for (const fn of changed.slice(0, 10)) {
|
|
133
|
+
try {
|
|
134
|
+
const t = index.affectedTests(fn.name, { depth: 2 });
|
|
135
|
+
if (t && t.testFiles) {
|
|
136
|
+
for (const tf of t.testFiles) {
|
|
137
|
+
if (!testFiles.find(x => x.file === tf.file)) {
|
|
138
|
+
testFiles.push(tf);
|
|
139
|
+
testCount += tf.testCount || 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (e) { /* skip */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Action items
|
|
147
|
+
const actions = [];
|
|
148
|
+
for (const it of items) {
|
|
149
|
+
if (it.signatureMismatches > 0) {
|
|
150
|
+
actions.push({
|
|
151
|
+
severity: 'warn',
|
|
152
|
+
kind: 'signature_drift',
|
|
153
|
+
message: `${it.name}: ${it.signatureMismatches} call site(s) need updating`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (it.orphan) {
|
|
157
|
+
actions.push({
|
|
158
|
+
severity: 'warn',
|
|
159
|
+
kind: 'orphan_new',
|
|
160
|
+
message: `${it.name} is new but has no callers and is not an entry point`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (testFiles.length > 0) {
|
|
165
|
+
const filesList = testFiles.slice(0, 5).map(t => t.file).join(' ');
|
|
166
|
+
actions.push({
|
|
167
|
+
severity: 'info',
|
|
168
|
+
kind: 'tests_to_run',
|
|
169
|
+
message: `Run tests: ${filesList}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
base: options.base || 'HEAD',
|
|
175
|
+
staged: !!options.staged,
|
|
176
|
+
changed: items,
|
|
177
|
+
totalChanged: allChanged.length + deleted.length,
|
|
178
|
+
truncated: !!(limit && allChanged.length > limit),
|
|
179
|
+
testFiles,
|
|
180
|
+
totalTestFiles: testFiles.length,
|
|
181
|
+
totalTests: testCount,
|
|
182
|
+
actions,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function symbolKey(file, line) {
|
|
187
|
+
return `${file}:${line}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function computeReachableSet(index) {
|
|
191
|
+
try {
|
|
192
|
+
const ep = require('./entrypoints');
|
|
193
|
+
if (typeof ep.computeReachability === 'function') {
|
|
194
|
+
return ep.computeReachability(index);
|
|
195
|
+
}
|
|
196
|
+
} catch (e) { /* fall through */ }
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { check };
|
package/core/discovery.js
CHANGED
|
@@ -393,43 +393,68 @@ function findProjectRoot(startDir) {
|
|
|
393
393
|
return path.resolve(startDir);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
// All file extensions for languages UCN supports as code analysis (excludes .rb/.php/.c/.cpp etc.
|
|
397
|
+
// which are extensions UCN scans but doesn't analyze). When build manifests can't tell us
|
|
398
|
+
// what's in a project, we scan all of these — the file extension alone determines language.
|
|
399
|
+
const ALL_SUPPORTED_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'py', 'go', 'java', 'rs', 'html', 'htm'];
|
|
400
|
+
|
|
401
|
+
// Build-manifest hints: when present, we know the project has files of that language
|
|
402
|
+
// regardless of whether sources are visible at the time of scan. Used as hints, not gates —
|
|
403
|
+
// any source file extension is included whether or not its manifest is present.
|
|
404
|
+
const MANIFEST_HINTS = {
|
|
405
|
+
'package.json': ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'html', 'htm'],
|
|
406
|
+
'pyproject.toml': ['py'],
|
|
407
|
+
'setup.py': ['py'],
|
|
408
|
+
'requirements.txt': ['py'],
|
|
409
|
+
'go.mod': ['go'],
|
|
410
|
+
'Cargo.toml': ['rs'],
|
|
411
|
+
'pom.xml': ['java'],
|
|
412
|
+
'build.gradle': ['java'],
|
|
413
|
+
'build.gradle.kts': ['java'],
|
|
414
|
+
};
|
|
415
|
+
|
|
396
416
|
/**
|
|
397
|
-
* Auto-detect the glob pattern for a project
|
|
398
|
-
*
|
|
417
|
+
* Auto-detect the glob pattern for a project.
|
|
418
|
+
*
|
|
419
|
+
* Discovery rule: build manifests are HINTS, not gates. We always scan ALL supported
|
|
420
|
+
* language extensions (JS/TS/Python/Go/Rust/Java/HTML). Manifests only inform metadata
|
|
421
|
+
* (e.g., flagging a project as "has Go") and never exclude files.
|
|
422
|
+
*
|
|
423
|
+
* This means a polyglot project with only `package.json` still discovers .py/.go/.rs
|
|
424
|
+
* files — language is determined by extension, not by manifest presence.
|
|
425
|
+
*
|
|
426
|
+
* Manifests are still useful for:
|
|
427
|
+
* - Project root detection (see findProjectRoot / PROJECT_MARKERS)
|
|
428
|
+
* - Conditional ignores (vendor/target/Pods/etc. — see CONDITIONAL_IGNORES)
|
|
429
|
+
* - Language hints in stats output
|
|
399
430
|
*/
|
|
400
431
|
function detectProjectPattern(projectRoot) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
406
|
-
extensions.push('js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'html', 'htm');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (fs.existsSync(path.join(dir, 'pyproject.toml')) ||
|
|
410
|
-
fs.existsSync(path.join(dir, 'setup.py')) ||
|
|
411
|
-
fs.existsSync(path.join(dir, 'requirements.txt'))) {
|
|
412
|
-
extensions.push('py');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (fs.existsSync(path.join(dir, 'go.mod'))) {
|
|
416
|
-
extensions.push('go');
|
|
417
|
-
}
|
|
432
|
+
// Always scan all supported language extensions. Build manifests no longer gate
|
|
433
|
+
// language inclusion — file extension alone determines what gets analyzed.
|
|
434
|
+
return `**/*.{${ALL_SUPPORTED_EXTENSIONS.join(',')}}`;
|
|
435
|
+
}
|
|
418
436
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Detect which manifest hints are present in a project root or immediate subdirectories.
|
|
439
|
+
* Returns array of detected language extension hints. Used purely informationally —
|
|
440
|
+
* not as a gate on which files get scanned.
|
|
441
|
+
*
|
|
442
|
+
* @param {string} projectRoot - Project root directory
|
|
443
|
+
* @returns {string[]} Array of language extension strings (e.g., ['js', 'py', 'go'])
|
|
444
|
+
*/
|
|
445
|
+
function detectManifestHints(projectRoot) {
|
|
446
|
+
const hints = new Set();
|
|
422
447
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
448
|
+
const checkDir = (dir) => {
|
|
449
|
+
for (const [marker, exts] of Object.entries(MANIFEST_HINTS)) {
|
|
450
|
+
if (fs.existsSync(path.join(dir, marker))) {
|
|
451
|
+
for (const ext of exts) hints.add(ext);
|
|
452
|
+
}
|
|
426
453
|
}
|
|
427
454
|
};
|
|
428
455
|
|
|
429
|
-
// Check project root
|
|
430
456
|
checkDir(projectRoot);
|
|
431
457
|
|
|
432
|
-
// Also check immediate subdirectories for multi-language projects (e.g., web/, frontend/, server/)
|
|
433
458
|
try {
|
|
434
459
|
const entries = fs.readdirSync(projectRoot, { withFileTypes: true });
|
|
435
460
|
for (const entry of entries) {
|
|
@@ -442,12 +467,7 @@ function detectProjectPattern(projectRoot) {
|
|
|
442
467
|
// Ignore errors reading directory
|
|
443
468
|
}
|
|
444
469
|
|
|
445
|
-
|
|
446
|
-
const unique = [...new Set(extensions)];
|
|
447
|
-
return `**/*.{${unique.join(',')}}`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return '**/*.{js,jsx,ts,tsx,py,go,java,rs,rb,php,c,cpp,h,hpp,html,htm}';
|
|
470
|
+
return [...hints];
|
|
451
471
|
}
|
|
452
472
|
|
|
453
473
|
/**
|
|
@@ -541,11 +561,14 @@ module.exports = {
|
|
|
541
561
|
shouldIgnore,
|
|
542
562
|
findProjectRoot,
|
|
543
563
|
detectProjectPattern,
|
|
564
|
+
detectManifestHints,
|
|
544
565
|
getFileStats,
|
|
545
566
|
isTestFile,
|
|
546
567
|
findTestFileFor,
|
|
547
568
|
parseGitignore,
|
|
548
569
|
DEFAULT_IGNORES,
|
|
549
570
|
PROJECT_MARKERS,
|
|
550
|
-
TEST_PATTERNS
|
|
571
|
+
TEST_PATTERNS,
|
|
572
|
+
ALL_SUPPORTED_EXTENSIONS,
|
|
573
|
+
MANIFEST_HINTS
|
|
551
574
|
};
|