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/account.js
CHANGED
|
@@ -16,11 +16,18 @@
|
|
|
16
16
|
* strings, and scanner-skipped tokens such as JS builtins —
|
|
17
17
|
* deliberately named "unclassified", not "comment")
|
|
18
18
|
* unparsed - line in a file that failed to parse (still readable text)
|
|
19
|
+
* unsupported - line in a source file whose language UCN cannot parse
|
|
20
|
+
* (index.unsupportedFiles — e.g. .rb in a mixed repo).
|
|
21
|
+
* Text-scanned only, never analyzed; sites are listed so the
|
|
22
|
+
* answer is a superset of grep, and the CONTRACT sentence
|
|
23
|
+
* degrades whenever this bucket is non-zero. Without it a
|
|
24
|
+
* mixed-language repo got "partition complete" over a ground
|
|
25
|
+
* set that silently excluded whole languages.
|
|
19
26
|
* unaccounted - residual; 0 when the arithmetic is conserved
|
|
20
27
|
*
|
|
21
28
|
* Conservation invariant:
|
|
22
29
|
* groundTotal === confirmed + unverified + nonCall.total + excluded.total
|
|
23
|
-
* + unparsed.lines + unaccounted
|
|
30
|
+
* + unparsed.lines + unsupported.lines + unaccounted
|
|
24
31
|
*
|
|
25
32
|
* Engine finds that grep would MISS (alias-resolved call sites whose line does
|
|
26
33
|
* not word-boundary-match the name) are reported in `beyondText` — additive
|
|
@@ -43,7 +50,12 @@
|
|
|
43
50
|
|
|
44
51
|
const fs = require('fs');
|
|
45
52
|
const path = require('path');
|
|
46
|
-
const { escapeRegExp } = require('./shared');
|
|
53
|
+
const { escapeRegExp, codeUnitCompare } = require('./shared');
|
|
54
|
+
|
|
55
|
+
// Unsupported-language site listings are capped so a Rails-sized repo cannot
|
|
56
|
+
// flood the account object; the counts always cover the full set.
|
|
57
|
+
const UNSUPPORTED_SITE_CAP = 50;
|
|
58
|
+
const UNSUPPORTED_SITE_TEXT_MAX = 160;
|
|
47
59
|
|
|
48
60
|
/**
|
|
49
61
|
* Compute the text-occurrence ground set for a symbol name.
|
|
@@ -51,10 +63,14 @@ const { escapeRegExp } = require('./shared');
|
|
|
51
63
|
* @param {object} index - ProjectIndex
|
|
52
64
|
* @param {string} name - Symbol name (matched with \b word boundaries)
|
|
53
65
|
* @returns {{
|
|
54
|
-
* total: number, // matching lines incl. unparsed files
|
|
55
|
-
* fileCount: number, // files (indexed + unparsed) with >= 1 matching line
|
|
66
|
+
* total: number, // matching lines incl. unparsed + unsupported files
|
|
67
|
+
* fileCount: number, // files (indexed + unparsed + unsupported) with >= 1 matching line
|
|
56
68
|
* perFile: Map<string, number[]>, // absPath -> sorted 1-indexed line numbers (indexed files only)
|
|
57
69
|
* unparsed: { fileCount: number, lines: number, files: string[] }, // relative paths
|
|
70
|
+
* unsupported: { fileCount: number, lines: number, files: string[],
|
|
71
|
+
* languages: Object<string, number>,
|
|
72
|
+
* sites: Array<{file: string, line: number, text: string}>,
|
|
73
|
+
* sitesTruncated: boolean },
|
|
58
74
|
* unreadableFiles: string[] // relative paths; OUTSIDE the arithmetic
|
|
59
75
|
* }}
|
|
60
76
|
*/
|
|
@@ -88,43 +104,126 @@ function computeGroundSet(index, name) {
|
|
|
88
104
|
|
|
89
105
|
// Failed-to-parse files are still text: their matching lines are part of
|
|
90
106
|
// the ground set, classified as `unparsed` (loud degradation, not silence).
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
if (!content.includes(name)) continue;
|
|
104
|
-
const lines = content.split('\n');
|
|
105
|
-
let matched = 0;
|
|
106
|
-
for (let i = 0; i < lines.length; i++) {
|
|
107
|
-
if (wordRe.test(lines[i])) matched++;
|
|
108
|
-
}
|
|
109
|
-
if (matched > 0) {
|
|
110
|
-
unparsed.fileCount++;
|
|
111
|
-
unparsed.lines += matched;
|
|
112
|
-
unparsed.files.push(path.relative(index.root, failedPath));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
unparsed.files.sort();
|
|
116
|
-
}
|
|
107
|
+
const { unparsed, unreadableFiles } = scanFailedFiles(index, name);
|
|
108
|
+
|
|
109
|
+
// Unsupported-language source files (discovery classified them as source,
|
|
110
|
+
// the parser registry cannot read them) are still text the contract must
|
|
111
|
+
// cover: a literal `processPayment(order)` in a .rb file is exactly what
|
|
112
|
+
// grep would show. Their matching lines join the ground set as
|
|
113
|
+
// `unsupported`, with sites listed so the caller answer strictly
|
|
114
|
+
// dominates grep instead of silently under-reporting it.
|
|
115
|
+
const unsupported = scanUnsupportedFiles(index, name, { unreadableFiles });
|
|
116
|
+
const skippedSources = Array.isArray(index.discoveryIssues)
|
|
117
|
+
? index.discoveryIssues.map(issue => ({ ...issue })) : [];
|
|
117
118
|
unreadableFiles.sort();
|
|
118
119
|
|
|
119
120
|
return {
|
|
120
|
-
total: total + unparsed.lines,
|
|
121
|
-
fileCount: fileCount + unparsed.fileCount,
|
|
121
|
+
total: total + unparsed.lines + unsupported.lines,
|
|
122
|
+
fileCount: fileCount + unparsed.fileCount + unsupported.fileCount,
|
|
122
123
|
perFile,
|
|
123
124
|
unparsed,
|
|
125
|
+
unsupported,
|
|
124
126
|
unreadableFiles,
|
|
127
|
+
skippedSources,
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
/** Scan only files the parser/index could not ingest. */
|
|
132
|
+
function scanFailedFiles(index, name) {
|
|
133
|
+
const unparsed = { fileCount: 0, lines: 0, files: [] };
|
|
134
|
+
const unreadableFiles = [];
|
|
135
|
+
if (!index.failedFiles || index.failedFiles.size === 0) {
|
|
136
|
+
return { unparsed, unreadableFiles };
|
|
137
|
+
}
|
|
138
|
+
const wordRe = new RegExp('\\b' + escapeRegExp(name) + '\\b');
|
|
139
|
+
for (const failedPath of index.failedFiles) {
|
|
140
|
+
if (index.files.has(failedPath)) continue;
|
|
141
|
+
let content;
|
|
142
|
+
try {
|
|
143
|
+
content = fs.readFileSync(failedPath, 'utf-8');
|
|
144
|
+
} catch (_) {
|
|
145
|
+
unreadableFiles.push(path.relative(index.root, failedPath));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!content.includes(name)) continue;
|
|
149
|
+
let matched = 0;
|
|
150
|
+
for (const line of content.split('\n')) {
|
|
151
|
+
if (wordRe.test(line)) matched++;
|
|
152
|
+
}
|
|
153
|
+
if (matched > 0) {
|
|
154
|
+
unparsed.fileCount++;
|
|
155
|
+
unparsed.lines += matched;
|
|
156
|
+
unparsed.files.push(path.relative(index.root, failedPath));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
unparsed.files.sort(codeUnitCompare);
|
|
160
|
+
unreadableFiles.sort(codeUnitCompare);
|
|
161
|
+
return { unparsed, unreadableFiles };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Word-boundary-scan index.unsupportedFiles for `name`. Returns the
|
|
166
|
+
* `unsupported` account bucket ({ fileCount, lines, files, languages, sites,
|
|
167
|
+
* sitesTruncated }). Shared by computeGroundSet and by commands whose result
|
|
168
|
+
* is not account-shaped (tests, usages) but still must not stay silent when
|
|
169
|
+
* a mixed-language repo has occurrences UCN cannot analyze.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} index - ProjectIndex
|
|
172
|
+
* @param {string} name - Symbol name (matched with \b word boundaries)
|
|
173
|
+
* @param {object} [opts]
|
|
174
|
+
* @param {string[]} [opts.unreadableFiles] - collector for read failures
|
|
175
|
+
*/
|
|
176
|
+
function scanUnsupportedFiles(index, name, opts = {}) {
|
|
177
|
+
const unsupported = {
|
|
178
|
+
fileCount: 0, lines: 0, files: [], languages: {}, sites: [],
|
|
179
|
+
sitesTruncated: false,
|
|
180
|
+
};
|
|
181
|
+
if (!Array.isArray(index.unsupportedFiles) || index.unsupportedFiles.length === 0) {
|
|
182
|
+
return unsupported;
|
|
183
|
+
}
|
|
184
|
+
const wordRe = new RegExp('\\b' + escapeRegExp(name) + '\\b');
|
|
185
|
+
const languageCounts = new Map();
|
|
186
|
+
for (const skipped of index.unsupportedFiles) {
|
|
187
|
+
const absPath = path.join(index.root, skipped.relativePath);
|
|
188
|
+
let content;
|
|
189
|
+
try {
|
|
190
|
+
content = index._readFile(absPath);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (opts.unreadableFiles) opts.unreadableFiles.push(skipped.relativePath);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!content.includes(name)) continue;
|
|
196
|
+
const lines = content.split('\n');
|
|
197
|
+
let matched = 0;
|
|
198
|
+
for (let i = 0; i < lines.length; i++) {
|
|
199
|
+
if (!wordRe.test(lines[i])) continue;
|
|
200
|
+
matched++;
|
|
201
|
+
if (unsupported.sites.length < UNSUPPORTED_SITE_CAP) {
|
|
202
|
+
unsupported.sites.push({
|
|
203
|
+
file: skipped.relativePath,
|
|
204
|
+
line: i + 1,
|
|
205
|
+
text: lines[i].trim().slice(0, UNSUPPORTED_SITE_TEXT_MAX),
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
unsupported.sitesTruncated = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (matched > 0) {
|
|
212
|
+
unsupported.fileCount++;
|
|
213
|
+
unsupported.lines += matched;
|
|
214
|
+
unsupported.files.push(skipped.relativePath);
|
|
215
|
+
const lang = skipped.language || skipped.extension || 'unknown';
|
|
216
|
+
languageCounts.set(lang, (languageCounts.get(lang) || 0) + matched);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
unsupported.files.sort(codeUnitCompare);
|
|
220
|
+
unsupported.sites.sort((a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line);
|
|
221
|
+
for (const lang of [...languageCounts.keys()].sort(codeUnitCompare)) {
|
|
222
|
+
unsupported.languages[lang] = languageCounts.get(lang);
|
|
223
|
+
}
|
|
224
|
+
return unsupported;
|
|
225
|
+
}
|
|
226
|
+
|
|
128
227
|
/**
|
|
129
228
|
* Classify ground lines not claimed by engine results.
|
|
130
229
|
*
|
|
@@ -313,12 +412,19 @@ function buildAccount(index, name, parts) {
|
|
|
313
412
|
// visible entries instead of silent gaps.
|
|
314
413
|
unverified += callNotResolved.length;
|
|
315
414
|
|
|
316
|
-
const
|
|
415
|
+
const unsupported = groundSet.unsupported ||
|
|
416
|
+
{ fileCount: 0, lines: 0, files: [], languages: {}, sites: [], sitesTruncated: false };
|
|
417
|
+
const accountedTotal = confirmed + unverified + nonCall.total + excludedTotal +
|
|
418
|
+
groundSet.unparsed.lines + unsupported.lines;
|
|
317
419
|
const unaccounted = groundSet.total - accountedTotal;
|
|
318
420
|
|
|
421
|
+
// Occurrences in unsupported-language files break the whole-text claim:
|
|
422
|
+
// UCN cannot adjudicate them, so "partition complete" would be false.
|
|
319
423
|
const textComplete = unaccounted === 0 &&
|
|
320
424
|
groundSet.unparsed.fileCount === 0 &&
|
|
321
|
-
|
|
425
|
+
unsupported.lines === 0 &&
|
|
426
|
+
groundSet.unreadableFiles.length === 0 &&
|
|
427
|
+
(!groundSet.skippedSources || groundSet.skippedSources.length === 0);
|
|
322
428
|
const observedTextZero = textComplete && confirmed === 0 && unverified === 0 &&
|
|
323
429
|
beyondText.count === 0;
|
|
324
430
|
|
|
@@ -331,7 +437,9 @@ function buildAccount(index, name, parts) {
|
|
|
331
437
|
nonCall,
|
|
332
438
|
excluded: { total: excludedTotal, byReason: excludedByReason },
|
|
333
439
|
unparsed: groundSet.unparsed,
|
|
440
|
+
unsupported,
|
|
334
441
|
unreadableFiles: groundSet.unreadableFiles,
|
|
442
|
+
skippedSources: groundSet.skippedSources || [],
|
|
335
443
|
beyondText,
|
|
336
444
|
unaccounted,
|
|
337
445
|
conserved: unaccounted === 0,
|
|
@@ -371,4 +479,6 @@ module.exports = {
|
|
|
371
479
|
computeGroundSet,
|
|
372
480
|
classifyGroundLines,
|
|
373
481
|
buildAccount,
|
|
482
|
+
scanUnsupportedFiles,
|
|
483
|
+
scanFailedFiles,
|
|
374
484
|
};
|