ucn 5.5.0 → 5.5.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 +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +41 -27
- package/cli/index.js +6 -1
- package/core/accessors.js +341 -54
- package/core/account.js +106 -31
- package/core/analysis.js +266 -127
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +144 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +5111 -1024
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +228 -114
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +1202 -157
- package/core/rust-modules.js +21 -3
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +157 -46
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +548 -89
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/core/account.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* Ground-set semantics are grep `-n -w`: unit is the (file, line) pair, each
|
|
37
37
|
* line with >= 1 word-boundary match counts once, case-sensitive.
|
|
38
38
|
*
|
|
39
|
-
* Performance: the first ground scan is one
|
|
39
|
+
* Performance: the first ground scan is one literal-name-gated read per project
|
|
40
40
|
* file — the same I/O profile as the existing `search`/`usages` commands.
|
|
41
41
|
* Exact results are retained in a small LRU for the lifetime of one built index
|
|
42
42
|
* so context/about/impact projections of the same symbol do not rescan the
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
const fs = require('fs');
|
|
54
54
|
const path = require('path');
|
|
55
55
|
const { codeUnitCompare, literalNameRegex } = require('./shared');
|
|
56
|
+
const { compareNames } = require('./discovery');
|
|
56
57
|
|
|
57
58
|
// Unsupported-language site listings are capped so a Rails-sized repo cannot
|
|
58
59
|
// flood the account object; the counts always cover the full set.
|
|
@@ -84,7 +85,7 @@ function computeGroundSet(index, name) {
|
|
|
84
85
|
index._groundSetCache.set(name, cached);
|
|
85
86
|
return cached.result;
|
|
86
87
|
}
|
|
87
|
-
const wordRe = literalNameRegex(name);
|
|
88
|
+
const wordRe = literalNameRegex(name, name ? 'y' : '');
|
|
88
89
|
const perFile = new Map();
|
|
89
90
|
let total = 0;
|
|
90
91
|
let fileCount = 0;
|
|
@@ -96,14 +97,7 @@ function computeGroundSet(index, name) {
|
|
|
96
97
|
} catch (e) {
|
|
97
98
|
continue; // deleted since indexing; not part of the universe anymore
|
|
98
99
|
}
|
|
99
|
-
|
|
100
|
-
// Shared read-only lines array — split once per file per operation,
|
|
101
|
-
// reused across the symbols of a multi-symbol command (diff-impact).
|
|
102
|
-
const lines = index._getFileLines(filePath);
|
|
103
|
-
const matched = [];
|
|
104
|
-
for (let i = 0; i < lines.length; i++) {
|
|
105
|
-
if (wordRe.test(lines[i])) matched.push(i + 1);
|
|
106
|
-
}
|
|
100
|
+
const matched = matchingGroundLines(content, name, wordRe);
|
|
107
101
|
if (matched.length > 0) {
|
|
108
102
|
perFile.set(filePath, matched);
|
|
109
103
|
total += matched.length;
|
|
@@ -180,6 +174,34 @@ function computeGroundSet(index, name) {
|
|
|
180
174
|
return result;
|
|
181
175
|
}
|
|
182
176
|
|
|
177
|
+
// Find literal candidates natively, checking the same Unicode boundaries
|
|
178
|
+
// at their exact offsets. Count line breaks only up to matching positions;
|
|
179
|
+
// once a line matches, its remaining occurrences cannot change the account.
|
|
180
|
+
function matchingGroundLines(content, name, wordRe) {
|
|
181
|
+
if (name.includes('\n')) return [];
|
|
182
|
+
if (!name) return content.split('\n').flatMap((line, i) => wordRe.test(line) ? [i + 1] : []);
|
|
183
|
+
const matched = [];
|
|
184
|
+
let at = content.indexOf(name);
|
|
185
|
+
if (at === -1) return matched;
|
|
186
|
+
let line = 1, end = content.indexOf('\n');
|
|
187
|
+
while (at !== -1) {
|
|
188
|
+
wordRe.lastIndex = at;
|
|
189
|
+
if (wordRe.test(content)) {
|
|
190
|
+
while (end !== -1 && end < at) {
|
|
191
|
+
line++;
|
|
192
|
+
end = content.indexOf('\n', end + 1);
|
|
193
|
+
}
|
|
194
|
+
matched.push(line);
|
|
195
|
+
if (end === -1) break;
|
|
196
|
+
at = content.indexOf(name, end + 1);
|
|
197
|
+
} else {
|
|
198
|
+
// A rejected candidate can overlap a later valid occurrence.
|
|
199
|
+
at = content.indexOf(name, at + 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return matched;
|
|
203
|
+
}
|
|
204
|
+
|
|
183
205
|
/** Scan only files the parser/index could not ingest. */
|
|
184
206
|
function scanFailedFiles(index, name) {
|
|
185
207
|
const unparsed = { fileCount: 0, lines: 0, files: [] };
|
|
@@ -200,7 +222,7 @@ function scanFailedFiles(index, name) {
|
|
|
200
222
|
if (!content.includes(name)) continue;
|
|
201
223
|
let matched = 0;
|
|
202
224
|
for (const line of content.split('\n')) {
|
|
203
|
-
if (wordRe.test(line)) matched++;
|
|
225
|
+
if (line.includes(name) && wordRe.test(line)) matched++;
|
|
204
226
|
}
|
|
205
227
|
if (matched > 0) {
|
|
206
228
|
unparsed.fileCount++;
|
|
@@ -248,7 +270,7 @@ function scanUnsupportedFiles(index, name, opts = {}) {
|
|
|
248
270
|
const lines = content.split('\n');
|
|
249
271
|
let matched = 0;
|
|
250
272
|
for (let i = 0; i < lines.length; i++) {
|
|
251
|
-
if (!wordRe.test(lines[i])) continue;
|
|
273
|
+
if (!lines[i].includes(name) || !wordRe.test(lines[i])) continue;
|
|
252
274
|
matched++;
|
|
253
275
|
if (unsupported.sites.length < UNSUPPORTED_SITE_CAP) {
|
|
254
276
|
unsupported.sites.push({
|
|
@@ -276,6 +298,23 @@ function scanUnsupportedFiles(index, name, opts = {}) {
|
|
|
276
298
|
return unsupported;
|
|
277
299
|
}
|
|
278
300
|
|
|
301
|
+
/**
|
|
302
|
+
* A set of (file, line) keys held per file: `key(file, line)` names one,
|
|
303
|
+
* and `add`/`has` take a key or a (file, line) pair. Lines compare by their
|
|
304
|
+
* text, as `${file}:${line}` strings do.
|
|
305
|
+
*/
|
|
306
|
+
class LineKeys {
|
|
307
|
+
constructor() { this.byFile = new Map(); }
|
|
308
|
+
key(file, line) { return { file: String(file), line: String(line) }; }
|
|
309
|
+
add(file, line) {
|
|
310
|
+
const key = line === undefined ? file : this.key(file, line);
|
|
311
|
+
let lines = this.byFile.get(key.file);
|
|
312
|
+
if (!lines) this.byFile.set(key.file, lines = new Set());
|
|
313
|
+
lines.add(key.line);
|
|
314
|
+
}
|
|
315
|
+
has(key) { return this.byFile.get(key.file)?.has(key.line) === true; }
|
|
316
|
+
}
|
|
317
|
+
|
|
279
318
|
/**
|
|
280
319
|
* Classify ground lines not claimed by engine results.
|
|
281
320
|
*
|
|
@@ -289,7 +328,7 @@ function scanUnsupportedFiles(index, name, opts = {}) {
|
|
|
289
328
|
* @param {object} index - ProjectIndex
|
|
290
329
|
* @param {string} name - Symbol name
|
|
291
330
|
* @param {object} groundSet - from computeGroundSet
|
|
292
|
-
* @param {
|
|
331
|
+
* @param {LineKeys} claimedKeys - (absPath, line) keys already claimed
|
|
293
332
|
* (confirmed + unverified + excluded engine lines)
|
|
294
333
|
* @returns {{
|
|
295
334
|
* nonCall: { imports: number, definitions: number, references: number, unclassifiedText: number, total: number },
|
|
@@ -311,12 +350,12 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
|
|
|
311
350
|
if (!defLinesByFile.has(def.file)) defLinesByFile.set(def.file, new Set());
|
|
312
351
|
defLinesByFile.get(def.file).add(def.startLine);
|
|
313
352
|
}
|
|
314
|
-
let
|
|
353
|
+
let getCachedCallsBearing = null; // lazy require to avoid cycle at module load
|
|
315
354
|
|
|
316
355
|
for (const [filePath, lineNumbers] of groundSet.perFile) {
|
|
317
356
|
let unclaimed = null;
|
|
318
357
|
for (const line of lineNumbers) {
|
|
319
|
-
if (claimedKeys.has(
|
|
358
|
+
if (claimedKeys.has(claimedKeys.key(filePath, line))) continue;
|
|
320
359
|
(unclaimed || (unclaimed = [])).push(line);
|
|
321
360
|
}
|
|
322
361
|
if (!unclaimed) continue;
|
|
@@ -324,20 +363,34 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
|
|
|
324
363
|
const fileEntry = index.files.get(filePath);
|
|
325
364
|
const relativePath = fileEntry ? fileEntry.relativePath : path.relative(index.root, filePath);
|
|
326
365
|
|
|
327
|
-
// Cheap pass: calls cache + symbol table
|
|
328
|
-
|
|
329
|
-
|
|
366
|
+
// Cheap pass: calls cache + symbol table (only records bearing the
|
|
367
|
+
// name on unclaimed lines are read below).
|
|
368
|
+
if (!getCachedCallsBearing) getCachedCallsBearing = require('./callers').getCachedCallsBearing;
|
|
369
|
+
const unclaimedLines = new Set(unclaimed);
|
|
370
|
+
const cachedCalls = getCachedCallsBearing(index, filePath, name, unclaimedLines);
|
|
371
|
+
const recordIsNoCall = require('./callers')._recordIsNoCall;
|
|
330
372
|
const callLines = new Set();
|
|
373
|
+
// `(T)(x)` casts: uses of the type, not calls (fix #427).
|
|
374
|
+
let castLines = null;
|
|
331
375
|
if (Array.isArray(cachedCalls)) {
|
|
332
376
|
for (const c of cachedCalls) {
|
|
377
|
+
// The ordinary cache can return the whole file. Only these
|
|
378
|
+
// name-bearing, unclaimed lines can affect this account.
|
|
379
|
+
if (!(c.name === name || c.resolvedName === name ||
|
|
380
|
+
(c.resolvedNames && c.resolvedNames.includes(name))) || !unclaimedLines.has(c.line)) continue;
|
|
333
381
|
// Unclaimed callback/type references have no invocation
|
|
334
382
|
// syntax. Let the usage AST classify them as references;
|
|
335
383
|
// merely entering the candidate cache is not a call fact.
|
|
336
|
-
|
|
337
|
-
if (c.
|
|
338
|
-
|
|
339
|
-
|
|
384
|
+
// An implicit initializer has no written invocation either.
|
|
385
|
+
if (c.isFunctionReference || c.isTypeReference || c.constructorSyntax === 'implicit') continue;
|
|
386
|
+
// Nor a data member's mem-initializer or a default
|
|
387
|
+
// initialization of no class (fix #421).
|
|
388
|
+
if ((c.memberInitializer || c.constructorSyntax === 'default') && recordIsNoCall(index, filePath, c)) continue;
|
|
389
|
+
if (c.parenthesizedCallee && c.name === name && recordIsNoCall(index, filePath, c)) {
|
|
390
|
+
(castLines ||= new Set()).add(c.line);
|
|
391
|
+
continue;
|
|
340
392
|
}
|
|
393
|
+
callLines.add(c.line);
|
|
341
394
|
}
|
|
342
395
|
}
|
|
343
396
|
const defLines = defLinesByFile.get(filePath);
|
|
@@ -346,6 +399,8 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
|
|
|
346
399
|
for (const line of unclaimed) {
|
|
347
400
|
if (callLines.has(line)) {
|
|
348
401
|
callNotResolved.push({ file: filePath, relativePath, line });
|
|
402
|
+
} else if (castLines?.has(line)) {
|
|
403
|
+
nonCall.references++;
|
|
349
404
|
} else if (defLines && defLines.has(line)) {
|
|
350
405
|
nonCall.definitions++;
|
|
351
406
|
} else {
|
|
@@ -359,8 +414,11 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
|
|
|
359
414
|
// Call lines were classified from the complete calls cache above.
|
|
360
415
|
// Language adapters may skip usage-only call recovery here; notably,
|
|
361
416
|
// C/C++ avoids reparsing every matching macro replacement list.
|
|
417
|
+
// Only occurrence kinds are consumed below, so receiver ownership
|
|
418
|
+
// is optional on this path too.
|
|
362
419
|
const usages = index._getCachedUsages(filePath, name, {
|
|
363
420
|
skipCallRecovery: true,
|
|
421
|
+
usageKindsOnly: true,
|
|
364
422
|
});
|
|
365
423
|
const byLine = new Map();
|
|
366
424
|
if (Array.isArray(usages)) {
|
|
@@ -415,22 +473,29 @@ const RANK = { call: 0, import: 1, definition: 2, reference: 3 };
|
|
|
415
473
|
*/
|
|
416
474
|
function buildAccount(index, name, parts) {
|
|
417
475
|
const { groundSet } = parts;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
476
|
+
// Entries in (file, line) order, stable (fix #407): samples, the evidence
|
|
477
|
+
// list and the reason of a line excluded twice must not follow the order
|
|
478
|
+
// the engine visited files in, which a warm session's derivation history
|
|
479
|
+
// changes.
|
|
480
|
+
const confirmedEntries = sortedSites(parts.confirmedEntries || []);
|
|
481
|
+
const unverifiedEntries = sortedSites(parts.unverifiedEntries || []);
|
|
482
|
+
const excludedEntries = sortedSites(parts.excludedEntries || []);
|
|
483
|
+
|
|
484
|
+
// (file, line) keys, kept per file (fix #405b: one composed string per
|
|
485
|
+
// ground line and per entry); a line is keyed by its text, as the
|
|
486
|
+
// composed `${file}:${line}` keys were.
|
|
487
|
+
const groundKeys = new LineKeys();
|
|
423
488
|
for (const [filePath, lineNumbers] of groundSet.perFile) {
|
|
424
|
-
for (const line of lineNumbers) groundKeys.add(
|
|
489
|
+
for (const line of lineNumbers) groundKeys.add(filePath, line);
|
|
425
490
|
}
|
|
426
491
|
|
|
427
|
-
const claimedKeys = new
|
|
492
|
+
const claimedKeys = new LineKeys();
|
|
428
493
|
const beyondText = { count: 0, sample: [] };
|
|
429
494
|
let confirmed = 0;
|
|
430
495
|
let unverified = 0;
|
|
431
496
|
|
|
432
497
|
const claim = (entry, bucket) => {
|
|
433
|
-
const key =
|
|
498
|
+
const key = claimedKeys.key(entry.file, entry.line);
|
|
434
499
|
if (claimedKeys.has(key)) return; // one bucket per line; first claim wins
|
|
435
500
|
if (!groundKeys.has(key)) {
|
|
436
501
|
// Engine found a call site grep would miss (alias / indirect name).
|
|
@@ -454,7 +519,7 @@ function buildAccount(index, name, parts) {
|
|
|
454
519
|
const excludedByReason = {};
|
|
455
520
|
let excludedTotal = 0;
|
|
456
521
|
for (const e of excludedEntries) {
|
|
457
|
-
const key =
|
|
522
|
+
const key = claimedKeys.key(e.file, e.line);
|
|
458
523
|
if (claimedKeys.has(key)) continue;
|
|
459
524
|
claimedKeys.add(key);
|
|
460
525
|
if (!groundKeys.has(key)) continue; // excluded non-ground line: irrelevant to both grep and display
|
|
@@ -541,6 +606,16 @@ function relPath(index, filePath) {
|
|
|
541
606
|
return fileEntry ? fileEntry.relativePath : path.relative(index.root, filePath);
|
|
542
607
|
}
|
|
543
608
|
|
|
609
|
+
/** Sites by (file in the index's canonical order, line), stable; the input
|
|
610
|
+
* itself when already in that order, else a sorted copy (fix #407). */
|
|
611
|
+
function sortedSites(entries) {
|
|
612
|
+
const before = (a, b) => compareNames(String(a.file), String(b.file)) || ((a.line || 0) - (b.line || 0));
|
|
613
|
+
for (let i = 1; i < entries.length; i++) {
|
|
614
|
+
if (before(entries[i - 1], entries[i]) > 0) return [...entries].sort(before);
|
|
615
|
+
}
|
|
616
|
+
return entries;
|
|
617
|
+
}
|
|
618
|
+
|
|
544
619
|
module.exports = {
|
|
545
620
|
computeGroundSet,
|
|
546
621
|
classifyGroundLines,
|