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
package/core/brief.js
CHANGED
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { detectLanguage } = require('../languages');
|
|
20
20
|
const { formatSymbolHandle } = require('./shared');
|
|
21
|
+
const { computeAstComplexity } = require('./ast-analysis');
|
|
21
22
|
|
|
22
23
|
// ============================================================================
|
|
23
24
|
// Side-effect signal sets (per-language, conservative)
|
|
@@ -114,6 +115,10 @@ function brief(index, name, options = {}) {
|
|
|
114
115
|
...(def.paramsStructured && { paramsStructured: def.paramsStructured }),
|
|
115
116
|
...(def.paramTypes && { paramTypes: def.paramTypes }),
|
|
116
117
|
...(def.returnType && { returnType: def.returnType }),
|
|
118
|
+
...(def.fieldType && { fieldType: def.fieldType }),
|
|
119
|
+
...(def.memberType && { memberType: def.memberType }),
|
|
120
|
+
...(def.generics && { generics: def.generics }),
|
|
121
|
+
...(def.functionLike !== undefined && { functionLike: def.functionLike }),
|
|
117
122
|
...(def.modifiers && def.modifiers.length && { modifiers: def.modifiers }),
|
|
118
123
|
...(def.decorators && def.decorators.length && { decorators: def.decorators }),
|
|
119
124
|
...(def.docstring && { docstring: firstSentence(def.docstring) }),
|
|
@@ -131,7 +136,8 @@ function brief(index, name, options = {}) {
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
// For non-callable types (class/struct/interface/type), most fields don't apply
|
|
134
|
-
if (['class', 'struct', 'interface', 'type', 'enum'
|
|
139
|
+
if (['class', 'struct', 'interface', 'type', 'enum', 'record',
|
|
140
|
+
'trait', 'namespace'].includes(def.type)) {
|
|
135
141
|
return {
|
|
136
142
|
symbol,
|
|
137
143
|
kind: 'type',
|
|
@@ -141,12 +147,25 @@ function brief(index, name, options = {}) {
|
|
|
141
147
|
};
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
const isData = def.type === 'field' || def.type === 'state' ||
|
|
151
|
+
def.memberType === 'field' || def.memberType === 'property' ||
|
|
152
|
+
(def.type === 'macro' && def.functionLike === false);
|
|
153
|
+
if (isData) {
|
|
154
|
+
return {
|
|
155
|
+
symbol,
|
|
156
|
+
kind: 'data',
|
|
157
|
+
lineCount: (def.endLine || def.startLine) - def.startLine + 1,
|
|
158
|
+
...(gitInfo && { git: gitInfo }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
// For callable symbols, scan the body
|
|
145
163
|
const filePath = path.isAbsolute(def.file) ? def.file : path.join(index.root, def.file);
|
|
146
164
|
let bodyText = '';
|
|
165
|
+
let fileContent = '';
|
|
147
166
|
try {
|
|
148
|
-
|
|
149
|
-
const lines =
|
|
167
|
+
fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
168
|
+
const lines = fileContent.split('\n');
|
|
150
169
|
const start = Math.max(0, (def.startLine || 1) - 1);
|
|
151
170
|
const end = Math.min(lines.length, def.endLine || def.startLine || 1);
|
|
152
171
|
bodyText = lines.slice(start, end).join('\n');
|
|
@@ -167,7 +186,10 @@ function brief(index, name, options = {}) {
|
|
|
167
186
|
const fileImports = collectImportNames(fileEntry);
|
|
168
187
|
|
|
169
188
|
const sideEffects = classifySideEffects(bodyText, language, fileImports);
|
|
170
|
-
const complexity =
|
|
189
|
+
const complexity = computeAstComplexity(fileContent, language, {
|
|
190
|
+
startLine: def.startLine || 1,
|
|
191
|
+
endLine: def.endLine || def.startLine || 1,
|
|
192
|
+
});
|
|
171
193
|
|
|
172
194
|
return {
|
|
173
195
|
symbol,
|
|
@@ -307,59 +329,6 @@ function escapeRegExp(s) {
|
|
|
307
329
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
308
330
|
}
|
|
309
331
|
|
|
310
|
-
/**
|
|
311
|
-
* Compute complexity metrics from a function body.
|
|
312
|
-
* Cheap, AST-free counts on tokenized source.
|
|
313
|
-
*/
|
|
314
|
-
function computeComplexity(bodyText, language) {
|
|
315
|
-
const lines = bodyText.split('\n');
|
|
316
|
-
const lineCount = lines.length;
|
|
317
|
-
|
|
318
|
-
// Branch count: count keywords that introduce a new branching path.
|
|
319
|
-
// We deliberately ignore final `else` (it's just the alternate of an `if`).
|
|
320
|
-
const branchPatterns = [
|
|
321
|
-
/\bif\s*\(/g, // JS/TS/Java/Rust/Go/C-like
|
|
322
|
-
/\bif\s+/g, // Python (if x:)
|
|
323
|
-
/\belif\b/g, // Python
|
|
324
|
-
/\belse\s+if\b/g, // JS/Java/etc.
|
|
325
|
-
/\bcase\b/g, // switch case
|
|
326
|
-
/\bwhen\b/g, // Rust match arms (and Kotlin/Scala but we don't support those)
|
|
327
|
-
/\bfor\s*\(/g, // C-like for
|
|
328
|
-
/\bfor\s+\w/g, // Python for x in
|
|
329
|
-
/\bwhile\s*\(/g, // C-like while
|
|
330
|
-
/\bwhile\s+/g, // Python while x:
|
|
331
|
-
/\?[^?]/g, // ternary (rough)
|
|
332
|
-
/\bcatch\s*\(/g, // catch
|
|
333
|
-
/\bexcept\b/g, // Python except
|
|
334
|
-
];
|
|
335
|
-
let branches = 0;
|
|
336
|
-
for (const re of branchPatterns) branches += (bodyText.match(re) || []).length;
|
|
337
|
-
|
|
338
|
-
// maxDepth: indent-based proxy. Fast, language-agnostic, off-by-one safe.
|
|
339
|
-
let maxDepth = 0;
|
|
340
|
-
let firstNonBlankIndent = -1;
|
|
341
|
-
for (const line of lines) {
|
|
342
|
-
if (!line.trim()) continue;
|
|
343
|
-
const m = line.match(/^(\s*)/);
|
|
344
|
-
const spaces = m ? expandIndent(m[1]) : 0;
|
|
345
|
-
if (firstNonBlankIndent === -1) firstNonBlankIndent = spaces;
|
|
346
|
-
// depth = (current - first) / unit; we don't know "unit", so just track
|
|
347
|
-
// raw delta and divide by 2 (conservative — most code is 2 or 4 space indented).
|
|
348
|
-
const rawDepth = Math.max(0, spaces - firstNonBlankIndent);
|
|
349
|
-
if (rawDepth > maxDepth) maxDepth = rawDepth;
|
|
350
|
-
}
|
|
351
|
-
// Translate raw spaces to depth levels (assume 2-space indent baseline)
|
|
352
|
-
const depth = Math.round(maxDepth / 2);
|
|
353
|
-
|
|
354
|
-
return { branches, maxDepth: depth, lineCount };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function expandIndent(s) {
|
|
358
|
-
let n = 0;
|
|
359
|
-
for (const c of s) n += (c === '\t') ? 4 : 1;
|
|
360
|
-
return n;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
332
|
/**
|
|
364
333
|
* Lazy classifier: side-effect tags for an arbitrary symbol record.
|
|
365
334
|
* Used by callee output (`context`, `about`) to surface [fs]/[net]/[proc] tags
|
|
@@ -402,6 +371,6 @@ module.exports = {
|
|
|
402
371
|
sideEffectsFor,
|
|
403
372
|
// exposed for tests
|
|
404
373
|
classifySideEffects,
|
|
405
|
-
computeComplexity,
|
|
374
|
+
computeComplexity: computeAstComplexity,
|
|
406
375
|
firstSentence,
|
|
407
376
|
};
|
package/core/build-worker.js
CHANGED
|
@@ -12,78 +12,13 @@ const { workerData } = require('worker_threads');
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const crypto = require('crypto');
|
|
15
|
-
const { detectLanguage, getParser,
|
|
16
|
-
const {
|
|
17
|
-
const {
|
|
15
|
+
const { detectLanguage, getParser, getLanguageAdapter } = require('../languages');
|
|
16
|
+
const { validateFileIR } = require('./ir');
|
|
17
|
+
const { createFileEntryFromIR, populateFileEntryFromIR } = require('./index-ir');
|
|
18
18
|
|
|
19
19
|
const { files, rootDir, existingHashes, signal, workerIndex, port } = workerData;
|
|
20
20
|
const signalArray = new Int32Array(signal);
|
|
21
21
|
|
|
22
|
-
function addSymbol(fileEntry, item, type) {
|
|
23
|
-
const symbol = {
|
|
24
|
-
name: item.name,
|
|
25
|
-
type,
|
|
26
|
-
file: fileEntry.path,
|
|
27
|
-
relativePath: fileEntry.relativePath,
|
|
28
|
-
startLine: item.startLine,
|
|
29
|
-
endLine: item.endLine,
|
|
30
|
-
params: item.params,
|
|
31
|
-
paramsStructured: item.paramsStructured,
|
|
32
|
-
returnType: item.returnType,
|
|
33
|
-
modifiers: item.modifiers,
|
|
34
|
-
docstring: item.docstring,
|
|
35
|
-
bindingId: `${fileEntry.relativePath}:${type}:${item.startLine}`,
|
|
36
|
-
};
|
|
37
|
-
// Field set MUST mirror project.js addSymbol exactly — a worker-side drop
|
|
38
|
-
// silently strips the field from every parallel-built index (>500 files).
|
|
39
|
-
// The cache.test.js / perf-optimizations.test.js indexSnapshot guards
|
|
40
|
-
// compare full symbol shapes, but only for shapes present in their
|
|
41
|
-
// fixtures — keep this list in sync by hand when addSymbol grows.
|
|
42
|
-
if (item.paramTypes) symbol.paramTypes = item.paramTypes;
|
|
43
|
-
if (item.returnedFunctionResult) symbol.returnedFunctionResult = item.returnedFunctionResult;
|
|
44
|
-
if (item.isFunctionVariable) symbol.isFunctionVariable = true;
|
|
45
|
-
if (item.isAsync) symbol.isAsync = true;
|
|
46
|
-
if (item.isGenerator) symbol.isGenerator = true;
|
|
47
|
-
if (item.generics) symbol.generics = item.generics;
|
|
48
|
-
if (item.extends) symbol.extends = item.extends;
|
|
49
|
-
if (item.implements) symbol.implements = item.implements;
|
|
50
|
-
if (item.indent !== undefined) symbol.indent = item.indent;
|
|
51
|
-
if (item.isNested) symbol.isNested = item.isNested;
|
|
52
|
-
if (item.enclosingType) symbol.enclosingType = item.enclosingType;
|
|
53
|
-
if (item.isMethod) symbol.isMethod = item.isMethod;
|
|
54
|
-
if (item.receiver) symbol.receiver = item.receiver;
|
|
55
|
-
if (item.className) symbol.className = item.className;
|
|
56
|
-
if (item.memberType) symbol.memberType = item.memberType;
|
|
57
|
-
if (item.fieldType) symbol.fieldType = item.fieldType;
|
|
58
|
-
if (item.aliasOf) symbol.aliasOf = item.aliasOf;
|
|
59
|
-
if (item.derefTarget) symbol.derefTarget = item.derefTarget;
|
|
60
|
-
if (item.decorators && item.decorators.length > 0) symbol.decorators = item.decorators;
|
|
61
|
-
if (item.decoratorsWithArgs && item.decoratorsWithArgs.length > 0) symbol.decoratorsWithArgs = item.decoratorsWithArgs;
|
|
62
|
-
if (item.annotationsWithArgs && item.annotationsWithArgs.length > 0) symbol.annotationsWithArgs = item.annotationsWithArgs;
|
|
63
|
-
if (item.attributesWithArgs && item.attributesWithArgs.length > 0) symbol.attributesWithArgs = item.attributesWithArgs;
|
|
64
|
-
if (item.nameLine) symbol.nameLine = item.nameLine;
|
|
65
|
-
if (item.traitImpl) symbol.traitImpl = true;
|
|
66
|
-
if (item.traitName) symbol.traitName = item.traitName;
|
|
67
|
-
if (item.isSignature) symbol.isSignature = true;
|
|
68
|
-
if (item.memberAssigned) symbol.memberAssigned = true;
|
|
69
|
-
if (item.bodyScopedName) symbol.bodyScopedName = true;
|
|
70
|
-
if (item.registryMember) symbol.registryMember = true;
|
|
71
|
-
if (item.registryContainer) symbol.registryContainer = item.registryContainer;
|
|
72
|
-
|
|
73
|
-
fileEntry.symbols.push(symbol);
|
|
74
|
-
// Property-assignment defs declare no lexical name (fix #269) — kept in
|
|
75
|
-
// lockstep with project.js addSymbol (the parallel≡sequential guard).
|
|
76
|
-
// Named function expressions bind only inside their own body.
|
|
77
|
-
if (!item.memberAssigned && !item.bodyScopedName) {
|
|
78
|
-
fileEntry.bindings.push({
|
|
79
|
-
id: symbol.bindingId,
|
|
80
|
-
name: symbol.name,
|
|
81
|
-
type: symbol.type,
|
|
82
|
-
startLine: symbol.startLine,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
22
|
function processFile(filePath) {
|
|
88
23
|
const stat = fs.statSync(filePath);
|
|
89
24
|
const existing = existingHashes[filePath];
|
|
@@ -101,25 +36,17 @@ function processFile(filePath) {
|
|
|
101
36
|
return { filePath, skipped: true, mtimeUpdate: stat.mtimeMs, sizeUpdate: stat.size };
|
|
102
37
|
}
|
|
103
38
|
|
|
104
|
-
const language = detectLanguage(filePath);
|
|
39
|
+
const language = detectLanguage(filePath, rootDir);
|
|
105
40
|
if (!language) return { filePath, skipped: true };
|
|
106
41
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const langModule = getLanguageModule(language);
|
|
116
|
-
if (langModule.findCallsInCode) {
|
|
117
|
-
const parser = getParser(language);
|
|
118
|
-
const callOpts = {};
|
|
119
|
-
if (langTraits(language)?.hasReceiverPackageCalls) {
|
|
120
|
-
callOpts.imports = imports.flatMap(i => i.names || []);
|
|
121
|
-
}
|
|
122
|
-
calls = langModule.findCallsInCode(content, parser, callOpts);
|
|
42
|
+
// One adapter analysis produces the complete, validated file IR consumed
|
|
43
|
+
// by both worker and sequential builds.
|
|
44
|
+
const adapter = getLanguageAdapter(language);
|
|
45
|
+
const parser = getParser(language);
|
|
46
|
+
const ir = adapter.analyze(content, parser, filePath);
|
|
47
|
+
const irFailures = validateFileIR(ir);
|
|
48
|
+
if (irFailures.length > 0) {
|
|
49
|
+
throw new Error(`Invalid ${language} IR: ${irFailures.join('; ')}`);
|
|
123
50
|
}
|
|
124
51
|
|
|
125
52
|
// Detect bundled/minified files (same logic as indexFile in project.js)
|
|
@@ -150,69 +77,23 @@ function processFile(filePath) {
|
|
|
150
77
|
|
|
151
78
|
const relativePath = path.relative(rootDir, filePath);
|
|
152
79
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
80
|
+
const fileEntry = createFileEntryFromIR({
|
|
81
|
+
ir,
|
|
82
|
+
filePath,
|
|
156
83
|
relativePath,
|
|
157
|
-
language,
|
|
158
|
-
lines: lineCount,
|
|
159
84
|
hash,
|
|
160
85
|
mtime: stat.mtimeMs,
|
|
161
86
|
size: stat.size,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
importBindings: imports.flatMap(i => (i.names || [])
|
|
168
|
-
.filter(n => n && n !== '*' && n !== '_' && n !== '.')
|
|
169
|
-
.map(n => {
|
|
170
|
-
// Rename pairing (fix #269) — lockstep with project.js.
|
|
171
|
-
const rn = (i.renames || []).find(r => r.original === n);
|
|
172
|
-
return {
|
|
173
|
-
name: n,
|
|
174
|
-
module: i.module,
|
|
175
|
-
...(rn && { alias: rn.local }),
|
|
176
|
-
...(i.defaultLike && { defaultLike: true }),
|
|
177
|
-
};
|
|
178
|
-
})),
|
|
179
|
-
exports: exports.map(e => e.name),
|
|
180
|
-
exportDetails: exports,
|
|
181
|
-
symbols: [],
|
|
182
|
-
bindings: [],
|
|
183
|
-
dynamicImports: dynamicCount || 0,
|
|
184
|
-
};
|
|
185
|
-
if (parsed.parseRecovery) fileEntry.parseRecovery = true;
|
|
186
|
-
if (importAliases) fileEntry.importAliases = importAliases;
|
|
187
|
-
if (parsed.moduleAssignedNames) fileEntry.moduleAssignedNames = parsed.moduleAssignedNames;
|
|
188
|
-
if (isBundled) fileEntry.isBundled = true;
|
|
189
|
-
if (isGenerated) fileEntry.isGenerated = true;
|
|
190
|
-
|
|
191
|
-
// Build symbols (mirrors indexFile)
|
|
192
|
-
for (const fn of parsed.functions) {
|
|
193
|
-
if (fn.receiver && !fn.className) {
|
|
194
|
-
fn.className = fn.receiver.replace(/^\*/, '');
|
|
195
|
-
}
|
|
196
|
-
addSymbol(fileEntry, fn, fn.isConstructor ? 'constructor' : 'function');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const cls of parsed.classes) {
|
|
200
|
-
addSymbol(fileEntry, cls, cls.type || 'class');
|
|
201
|
-
if (cls.members) {
|
|
202
|
-
for (const m of cls.members) {
|
|
203
|
-
addSymbol(fileEntry, { ...m, className: cls.name, ...(cls.traitName && { traitImpl: true, traitName: cls.traitName }) }, m.memberType || 'method');
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
for (const state of parsed.stateObjects) {
|
|
209
|
-
addSymbol(fileEntry, state, 'state');
|
|
210
|
-
}
|
|
87
|
+
lineCount,
|
|
88
|
+
isBundled,
|
|
89
|
+
isGenerated,
|
|
90
|
+
});
|
|
91
|
+
populateFileEntryFromIR(fileEntry, ir);
|
|
211
92
|
|
|
212
93
|
return {
|
|
213
94
|
filePath,
|
|
214
95
|
fileEntry,
|
|
215
|
-
calls,
|
|
96
|
+
calls: ir.calls,
|
|
216
97
|
callsMtime: stat.mtimeMs,
|
|
217
98
|
callsHash: hash,
|
|
218
99
|
hadExisting: !!existing,
|