ucn 4.2.3 → 5.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/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read-only compile_commands.json support for C/C++ language and include-path
|
|
5
|
+
* resolution. This module interprets build metadata only; source analysis
|
|
6
|
+
* remains tree-sitter AST based.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const CPP_EXTENSIONS = new Set(['.cc', '.cpp', '.cxx', '.c++', '.cp', '.mm']);
|
|
13
|
+
const C_EXTENSIONS = new Set(['.c', '.m']);
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
const conventionCache = new Map();
|
|
16
|
+
const CONVENTION_IGNORES = new Set([
|
|
17
|
+
'.git', '.svn', 'node_modules', 'vendor', 'third_party',
|
|
18
|
+
'build', 'dist', 'out', 'target',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function splitCommandLine(command) {
|
|
22
|
+
const args = [];
|
|
23
|
+
let current = '';
|
|
24
|
+
let quote = null;
|
|
25
|
+
let escaped = false;
|
|
26
|
+
for (const char of String(command || '')) {
|
|
27
|
+
if (escaped) {
|
|
28
|
+
current += char;
|
|
29
|
+
escaped = false;
|
|
30
|
+
} else if (char === '\\' && quote !== "'") {
|
|
31
|
+
escaped = true;
|
|
32
|
+
} else if (quote) {
|
|
33
|
+
if (char === quote) quote = null;
|
|
34
|
+
else current += char;
|
|
35
|
+
} else if (char === '"' || char === "'") {
|
|
36
|
+
quote = char;
|
|
37
|
+
} else if (/\s/.test(char)) {
|
|
38
|
+
if (current) {
|
|
39
|
+
args.push(current);
|
|
40
|
+
current = '';
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
current += char;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (escaped) current += '\\';
|
|
47
|
+
if (current) args.push(current);
|
|
48
|
+
return args;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findCompilationDatabase(startDir, stopDir = null) {
|
|
52
|
+
let dir = path.resolve(startDir);
|
|
53
|
+
const boundary = stopDir ? path.resolve(stopDir) : null;
|
|
54
|
+
while (true) {
|
|
55
|
+
const candidate = path.join(dir, 'compile_commands.json');
|
|
56
|
+
try {
|
|
57
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
58
|
+
} catch { /* keep walking */ }
|
|
59
|
+
if (boundary && dir === boundary) break;
|
|
60
|
+
const parent = path.dirname(dir);
|
|
61
|
+
if (parent === dir || (boundary && !parent.startsWith(boundary))) break;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function languageFromArgs(file, args) {
|
|
68
|
+
for (let i = 0; i < args.length; i++) {
|
|
69
|
+
if (args[i] === '-x' && args[i + 1]) {
|
|
70
|
+
const value = args[i + 1].toLowerCase();
|
|
71
|
+
if (value.includes('c++') || value.includes('objective-c++')) return 'cpp';
|
|
72
|
+
if (value === 'c' || value.includes('objective-c')) return 'c';
|
|
73
|
+
}
|
|
74
|
+
if (args[i].startsWith('-x')) {
|
|
75
|
+
const value = args[i].slice(2).toLowerCase();
|
|
76
|
+
if (value.includes('c++') || value.includes('objective-c++')) return 'cpp';
|
|
77
|
+
if (value === 'c' || value.includes('objective-c')) return 'c';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const ext = path.extname(file).toLowerCase();
|
|
81
|
+
if (CPP_EXTENSIONS.has(ext)) return 'cpp';
|
|
82
|
+
if (C_EXTENSIONS.has(ext)) return 'c';
|
|
83
|
+
const compiler = path.basename(args[0] || '').toLowerCase();
|
|
84
|
+
if (/(^|-)c\+\+/.test(compiler) || /g\+\+|clang\+\+/.test(compiler)) return 'cpp';
|
|
85
|
+
return 'c';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function includeDirectories(directory, args) {
|
|
89
|
+
const result = [];
|
|
90
|
+
const add = value => {
|
|
91
|
+
if (!value) return;
|
|
92
|
+
const absolute = path.isAbsolute(value) ? value : path.resolve(directory, value);
|
|
93
|
+
if (!result.includes(absolute)) result.push(absolute);
|
|
94
|
+
};
|
|
95
|
+
for (let i = 0; i < args.length; i++) {
|
|
96
|
+
const arg = args[i];
|
|
97
|
+
if (arg === '-I' || arg === '-isystem' || arg === '-iquote') {
|
|
98
|
+
add(args[++i]);
|
|
99
|
+
} else if (arg.startsWith('-I') && arg.length > 2) {
|
|
100
|
+
add(arg.slice(2));
|
|
101
|
+
} else if (arg.startsWith('-isystem') && arg.length > 8) {
|
|
102
|
+
add(arg.slice(8));
|
|
103
|
+
} else if (arg.startsWith('-iquote') && arg.length > 7) {
|
|
104
|
+
add(arg.slice(7));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadCompilationDatabase(databasePath) {
|
|
111
|
+
if (!databasePath) return null;
|
|
112
|
+
let stat;
|
|
113
|
+
try { stat = fs.statSync(databasePath); }
|
|
114
|
+
catch { return null; }
|
|
115
|
+
const previous = cache.get(databasePath);
|
|
116
|
+
if (previous && previous.mtime === stat.mtimeMs && previous.size === stat.size) {
|
|
117
|
+
return previous.value;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const rows = JSON.parse(fs.readFileSync(databasePath, 'utf8'));
|
|
121
|
+
if (!Array.isArray(rows)) return null;
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
if (!row || typeof row.file !== 'string') continue;
|
|
125
|
+
const directory = path.resolve(row.directory || path.dirname(databasePath));
|
|
126
|
+
const file = path.isAbsolute(row.file) ? row.file : path.resolve(directory, row.file);
|
|
127
|
+
const args = Array.isArray(row.arguments)
|
|
128
|
+
? row.arguments.map(String)
|
|
129
|
+
: splitCommandLine(row.command);
|
|
130
|
+
entries.push({
|
|
131
|
+
file,
|
|
132
|
+
directory,
|
|
133
|
+
language: languageFromArgs(file, args),
|
|
134
|
+
includeDirs: includeDirectories(directory, args),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const value = { path: databasePath, root: path.dirname(databasePath), entries };
|
|
138
|
+
cache.set(databasePath, { mtime: stat.mtimeMs, size: stat.size, value });
|
|
139
|
+
return value;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function directoryDistance(fromDir, candidateDir) {
|
|
146
|
+
const relative = path.relative(candidateDir, fromDir);
|
|
147
|
+
if (relative === '') return 0;
|
|
148
|
+
return relative.split(path.sep).filter(part => part && part !== '.').length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function nearestEntries(database, filePath) {
|
|
152
|
+
if (!database) return [];
|
|
153
|
+
const directory = path.dirname(path.resolve(filePath));
|
|
154
|
+
return [...database.entries].sort((a, b) => {
|
|
155
|
+
const distance = directoryDistance(directory, path.dirname(a.file)) -
|
|
156
|
+
directoryDistance(directory, path.dirname(b.file));
|
|
157
|
+
// Code-unit tiebreak (rule 11): localeCompare is ICU-locale-dependent.
|
|
158
|
+
return distance || (a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function projectBoundary(startDir) {
|
|
163
|
+
let directory = path.resolve(startDir);
|
|
164
|
+
let fallback = directory;
|
|
165
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
166
|
+
fallback = directory;
|
|
167
|
+
if (fs.existsSync(path.join(directory, '.git')) ||
|
|
168
|
+
fs.existsSync(path.join(directory, 'CMakeLists.txt')) ||
|
|
169
|
+
fs.existsSync(path.join(directory, 'meson.build'))) {
|
|
170
|
+
return directory;
|
|
171
|
+
}
|
|
172
|
+
const parent = path.dirname(directory);
|
|
173
|
+
if (parent === directory) break;
|
|
174
|
+
directory = parent;
|
|
175
|
+
}
|
|
176
|
+
return fallback;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function projectTranslationUnitConvention(filePath, projectRoot = null) {
|
|
180
|
+
const root = projectRoot
|
|
181
|
+
? path.resolve(projectRoot)
|
|
182
|
+
: projectBoundary(path.dirname(filePath));
|
|
183
|
+
if (conventionCache.has(root)) return conventionCache.get(root);
|
|
184
|
+
let cpp = 0;
|
|
185
|
+
let c = 0;
|
|
186
|
+
let visited = 0;
|
|
187
|
+
const queue = [root];
|
|
188
|
+
while (queue.length > 0 && visited < 5000) {
|
|
189
|
+
const directory = queue.shift();
|
|
190
|
+
let entries;
|
|
191
|
+
try {
|
|
192
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
193
|
+
} catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
entries.sort((left, right) =>
|
|
197
|
+
left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (visited++ >= 5000) break;
|
|
200
|
+
if (entry.isDirectory()) {
|
|
201
|
+
if (!CONVENTION_IGNORES.has(entry.name) &&
|
|
202
|
+
!entry.name.startsWith('.')) {
|
|
203
|
+
queue.push(path.join(directory, entry.name));
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
208
|
+
if (CPP_EXTENSIONS.has(extension)) cpp++;
|
|
209
|
+
else if (C_EXTENSIONS.has(extension)) c++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const language = cpp === c ? null : cpp > c ? 'cpp' : 'c';
|
|
213
|
+
conventionCache.set(root, language);
|
|
214
|
+
return language;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function detectHeaderLanguage(filePath, projectRoot = null) {
|
|
218
|
+
const originalExt = path.extname(filePath);
|
|
219
|
+
if (originalExt === '.H' || originalExt === '.HPP') return 'cpp';
|
|
220
|
+
const stem = filePath.slice(0, -originalExt.length);
|
|
221
|
+
for (const ext of CPP_EXTENSIONS) {
|
|
222
|
+
try { if (fs.statSync(stem + ext).isFile()) return 'cpp'; } catch { /* next */ }
|
|
223
|
+
}
|
|
224
|
+
for (const ext of C_EXTENSIONS) {
|
|
225
|
+
try { if (fs.statSync(stem + ext).isFile()) return 'c'; } catch { /* next */ }
|
|
226
|
+
}
|
|
227
|
+
const dbPath = findCompilationDatabase(path.dirname(filePath), projectRoot);
|
|
228
|
+
const database = loadCompilationDatabase(dbPath);
|
|
229
|
+
const nearest = nearestEntries(database, filePath);
|
|
230
|
+
if (nearest.length > 0) {
|
|
231
|
+
const closestDistance = directoryDistance(
|
|
232
|
+
path.dirname(filePath), path.dirname(nearest[0].file));
|
|
233
|
+
const closest = nearest.filter(entry =>
|
|
234
|
+
directoryDistance(path.dirname(filePath), path.dirname(entry.file)) === closestDistance);
|
|
235
|
+
const cpp = closest.filter(entry => entry.language === 'cpp').length;
|
|
236
|
+
const c = closest.filter(entry => entry.language === 'c').length;
|
|
237
|
+
if (cpp !== c) return cpp > c ? 'cpp' : 'c';
|
|
238
|
+
}
|
|
239
|
+
// No database: use neighboring translation-unit extensions as a
|
|
240
|
+
// deterministic project convention, without reading source text.
|
|
241
|
+
try {
|
|
242
|
+
const names = fs.readdirSync(path.dirname(filePath));
|
|
243
|
+
const cpp = names.filter(name => CPP_EXTENSIONS.has(path.extname(name).toLowerCase())).length;
|
|
244
|
+
const c = names.filter(name => C_EXTENSIONS.has(path.extname(name).toLowerCase())).length;
|
|
245
|
+
if (cpp !== c) return cpp > c ? 'cpp' : 'c';
|
|
246
|
+
} catch { /* default below */ }
|
|
247
|
+
const projectConvention = projectTranslationUnitConvention(filePath, projectRoot);
|
|
248
|
+
if (projectConvention) return projectConvention;
|
|
249
|
+
return 'c';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function includeDirectoriesForFile(filePath, projectRoot = null) {
|
|
253
|
+
const dbPath = findCompilationDatabase(path.dirname(filePath), projectRoot);
|
|
254
|
+
const database = loadCompilationDatabase(dbPath);
|
|
255
|
+
if (!database) return [];
|
|
256
|
+
const exact = database.entries.find(entry => path.resolve(entry.file) === path.resolve(filePath));
|
|
257
|
+
const candidates = exact ? [exact] : nearestEntries(database, filePath).slice(0, 8);
|
|
258
|
+
const result = [];
|
|
259
|
+
for (const entry of candidates) {
|
|
260
|
+
for (const directory of entry.includeDirs) {
|
|
261
|
+
if (!result.includes(directory)) result.push(directory);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
CPP_EXTENSIONS,
|
|
269
|
+
C_EXTENSIONS,
|
|
270
|
+
splitCommandLine,
|
|
271
|
+
findCompilationDatabase,
|
|
272
|
+
loadCompilationDatabase,
|
|
273
|
+
detectHeaderLanguage,
|
|
274
|
+
projectTranslationUnitConvention,
|
|
275
|
+
includeDirectoriesForFile,
|
|
276
|
+
};
|
package/core/confidence.js
CHANGED
|
@@ -81,6 +81,7 @@ function scored(resolution, reasons) {
|
|
|
81
81
|
* @param {boolean} [evidence.resolvedByReceiverHint] - Receiver type narrowed via local hints
|
|
82
82
|
* @param {boolean} [evidence.hasImportEvidence] - File imports the target definition
|
|
83
83
|
* @param {boolean} [evidence.hasReceiverEvidence] - Receiver variable has binding in file scope
|
|
84
|
+
* @param {boolean} [evidence.hasSingleOwnerEvidence] - One eligible project type owns the method
|
|
84
85
|
* @param {boolean} [evidence.isUncertain] - Marked uncertain by resolution logic
|
|
85
86
|
* @param {boolean} [evidence.isFunctionReference] - Passed as callback argument
|
|
86
87
|
* @param {boolean} [evidence.hasReceiverType] - Go/Java/Rust parser-inferred receiverType
|
|
@@ -143,10 +144,12 @@ function scoreEdge(evidence) {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
// Scope/import-supported match
|
|
146
|
-
if (evidence.hasImportEvidence || evidence.hasReceiverEvidence ||
|
|
147
|
+
if (evidence.hasImportEvidence || evidence.hasReceiverEvidence ||
|
|
148
|
+
evidence.hasSamePackageEvidence || evidence.hasSingleOwnerEvidence) {
|
|
147
149
|
if (evidence.hasImportEvidence) reasons.push('import-supported');
|
|
148
150
|
if (evidence.hasReceiverEvidence) reasons.push('receiver binding in scope');
|
|
149
151
|
if (evidence.hasSamePackageEvidence) reasons.push('same package/module');
|
|
152
|
+
if (evidence.hasSingleOwnerEvidence) reasons.push('single project method owner');
|
|
150
153
|
return scored(RESOLUTION.SCOPE_MATCH, reasons);
|
|
151
154
|
}
|
|
152
155
|
|