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.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. 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'].includes(def.type)) {
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
- const content = fs.readFileSync(filePath, 'utf-8');
149
- const lines = content.split('\n');
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 = computeComplexity(bodyText, language);
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
  };
@@ -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, getLanguageModule, langTraits } = require('../languages');
16
- const { parse } = require('./parser');
17
- const { extractImports, extractExports } = require('./imports');
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
- // Parse content — safeParse cache ensures tree is shared across
108
- // parse()/extractImports()/extractExports()/findCallsInCode() (1 parse per file)
109
- const parsed = parse(content, language);
110
- const { imports, dynamicCount, importAliases } = extractImports(content, language);
111
- const { exports } = extractExports(content, language);
112
-
113
- // Extract calls in the same pass (eliminates buildCalleeIndex re-parsing)
114
- let calls = null;
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
- // Build fileEntry (mirrors indexFile)
154
- const fileEntry = {
155
- path: filePath,
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
- imports: imports.map(i => i.module),
163
- importNames: imports.flatMap(i => i.names || []),
164
- // Paired name↔module bindings (fix #209) — mirrors indexFile; was
165
- // missing here, so parallel-built indexes (>500 files) silently lost
166
- // the name-level scope/ownership discipline (found during fix #217).
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,