ucn 5.5.0 → 5.5.1
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/README.md +11 -6
- package/cli/index.js +5 -0
- package/core/account.js +32 -11
- package/core/analysis.js +9 -2
- package/core/cache.js +8 -1
- package/core/callers.js +378 -77
- package/core/project.js +15 -6
- package/core/rust-macro-expansion.js +1061 -148
- package/core/rust-modules.js +20 -2
- package/core/type-denotation.js +33 -9
- package/languages/rust.js +38 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UCN - Universal Code Navigator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Auditable answers, with every match counted, unsure ones marked and blind spots named.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/ucn)
|
|
6
6
|
[](https://github.com/mleoca/ucn/actions/workflows/ci.yml)
|
|
@@ -8,7 +8,9 @@ See what code does before you touch it.
|
|
|
8
8
|
|
|
9
9
|
If you work with AI Agents, add UCN as a [Skill or MCP tool](#ai-setup). One tool
|
|
10
10
|
gives the agent compact, source-linked answers to caller, impact, and test
|
|
11
|
-
questions, with uncertainty labeled instead of guessed.
|
|
11
|
+
questions, with uncertainty labeled instead of guessed. The answer depends on
|
|
12
|
+
the indexed source, not on who asks: a person at a terminal and an agent over
|
|
13
|
+
MCP receive the same records for the same question.
|
|
12
14
|
|
|
13
15
|
<img src="assets/readme/ucn-connected-scope.gif" width="736" alt="Conceptual overview of UCN's relationship views, multi-hop exploration, visible uncertainty, index reuse, and refresh after edits. Nodes and timing are illustrative, not a captured query or benchmark.">
|
|
14
16
|
|
|
@@ -103,10 +105,11 @@ Captured findings · [Still image](assets/readme/ucn-evidence-graph.png) · [Dat
|
|
|
103
105
|
|
|
104
106
|
The ACCOUNT line reconciles the observed name occurrences: confirmed calls,
|
|
105
107
|
unverified candidates, non-call occurrences, and matches attributed to another
|
|
106
|
-
target.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
target. Each occurrence is counted in exactly one category, so the totals can
|
|
109
|
+
be checked line by line rather than taken on trust. CONTRACT describes the
|
|
110
|
+
scope of that accounting. Warnings identify source the index could not cover.
|
|
111
|
+
These details survive text truncation for an agent's output budget; a budget
|
|
112
|
+
too small to hold them withholds the answer and names the budget that would.
|
|
110
113
|
|
|
111
114
|
An empty result therefore means something specific about the inspected code.
|
|
112
115
|
It cannot establish that reflection, generated code, runtime registration, or
|
|
@@ -210,6 +213,8 @@ exits 2, and a successful listing exits 0. `--lines` supports `find`, `show`,
|
|
|
210
213
|
`usages`, `search`, and `impact`; `show --lines` lists callers by default.
|
|
211
214
|
Use `--json` when the script needs structured fields or a tree result.
|
|
212
215
|
|
|
216
|
+
Listings are deterministic for a given source tree: a fresh build, a cache
|
|
217
|
+
load, and an incremental rebuild return the same records in the same order.
|
|
213
218
|
Listings have no default row cap. Explicit limits disclose what they omit,
|
|
214
219
|
and a shell-mode character budget fails before writing partial output.
|
|
215
220
|
`source --raw` extracts complete functions and classes unless an explicit
|
package/cli/index.js
CHANGED
|
@@ -843,6 +843,11 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
843
843
|
// On cache-hit runs, only re-save if callsCache was mutated OR
|
|
844
844
|
// reachability was computed (MED-1: persists the BFS result so
|
|
845
845
|
// subsequent cold invocations don't repeat the 7-11s tax).
|
|
846
|
+
// Files whose macro-expansion calls the command derived in part are
|
|
847
|
+
// derived whole first, so the next invocation reads them persisted.
|
|
848
|
+
if (flags.cache) {
|
|
849
|
+
try { index.completeMacroCallDerivations(); } catch (e) { /* best-effort */ }
|
|
850
|
+
}
|
|
846
851
|
if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty || index.macroExpansionDirty)) {
|
|
847
852
|
try { index.saveCache(); } catch (e) { /* best-effort */ }
|
|
848
853
|
}
|
package/core/account.js
CHANGED
|
@@ -276,6 +276,23 @@ function scanUnsupportedFiles(index, name, opts = {}) {
|
|
|
276
276
|
return unsupported;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* A set of (file, line) keys held per file: `key(file, line)` names one,
|
|
281
|
+
* and `add`/`has` take a key or a (file, line) pair. Lines compare by their
|
|
282
|
+
* text, as `${file}:${line}` strings do.
|
|
283
|
+
*/
|
|
284
|
+
class LineKeys {
|
|
285
|
+
constructor() { this.byFile = new Map(); }
|
|
286
|
+
key(file, line) { return { file: String(file), line: String(line) }; }
|
|
287
|
+
add(file, line) {
|
|
288
|
+
const key = line === undefined ? file : this.key(file, line);
|
|
289
|
+
let lines = this.byFile.get(key.file);
|
|
290
|
+
if (!lines) this.byFile.set(key.file, lines = new Set());
|
|
291
|
+
lines.add(key.line);
|
|
292
|
+
}
|
|
293
|
+
has(key) { return this.byFile.get(key.file)?.has(key.line) === true; }
|
|
294
|
+
}
|
|
295
|
+
|
|
279
296
|
/**
|
|
280
297
|
* Classify ground lines not claimed by engine results.
|
|
281
298
|
*
|
|
@@ -289,7 +306,7 @@ function scanUnsupportedFiles(index, name, opts = {}) {
|
|
|
289
306
|
* @param {object} index - ProjectIndex
|
|
290
307
|
* @param {string} name - Symbol name
|
|
291
308
|
* @param {object} groundSet - from computeGroundSet
|
|
292
|
-
* @param {
|
|
309
|
+
* @param {LineKeys} claimedKeys - (absPath, line) keys already claimed
|
|
293
310
|
* (confirmed + unverified + excluded engine lines)
|
|
294
311
|
* @returns {{
|
|
295
312
|
* nonCall: { imports: number, definitions: number, references: number, unclassifiedText: number, total: number },
|
|
@@ -311,12 +328,12 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
|
|
|
311
328
|
if (!defLinesByFile.has(def.file)) defLinesByFile.set(def.file, new Set());
|
|
312
329
|
defLinesByFile.get(def.file).add(def.startLine);
|
|
313
330
|
}
|
|
314
|
-
let
|
|
331
|
+
let getCachedCallsBearing = null; // lazy require to avoid cycle at module load
|
|
315
332
|
|
|
316
333
|
for (const [filePath, lineNumbers] of groundSet.perFile) {
|
|
317
334
|
let unclaimed = null;
|
|
318
335
|
for (const line of lineNumbers) {
|
|
319
|
-
if (claimedKeys.has(
|
|
336
|
+
if (claimedKeys.has(claimedKeys.key(filePath, line))) continue;
|
|
320
337
|
(unclaimed || (unclaimed = [])).push(line);
|
|
321
338
|
}
|
|
322
339
|
if (!unclaimed) continue;
|
|
@@ -324,9 +341,10 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
|
|
|
324
341
|
const fileEntry = index.files.get(filePath);
|
|
325
342
|
const relativePath = fileEntry ? fileEntry.relativePath : path.relative(index.root, filePath);
|
|
326
343
|
|
|
327
|
-
// Cheap pass: calls cache + symbol table
|
|
328
|
-
|
|
329
|
-
|
|
344
|
+
// Cheap pass: calls cache + symbol table (only records bearing the
|
|
345
|
+
// name on unclaimed lines are read below).
|
|
346
|
+
if (!getCachedCallsBearing) getCachedCallsBearing = require('./callers').getCachedCallsBearing;
|
|
347
|
+
const cachedCalls = getCachedCallsBearing(index, filePath, name, new Set(unclaimed));
|
|
330
348
|
const callLines = new Set();
|
|
331
349
|
if (Array.isArray(cachedCalls)) {
|
|
332
350
|
for (const c of cachedCalls) {
|
|
@@ -419,18 +437,21 @@ function buildAccount(index, name, parts) {
|
|
|
419
437
|
const unverifiedEntries = parts.unverifiedEntries || [];
|
|
420
438
|
const excludedEntries = parts.excludedEntries || [];
|
|
421
439
|
|
|
422
|
-
|
|
440
|
+
// (file, line) keys, kept per file (fix #405b: one composed string per
|
|
441
|
+
// ground line and per entry); a line is keyed by its text, as the
|
|
442
|
+
// composed `${file}:${line}` keys were.
|
|
443
|
+
const groundKeys = new LineKeys();
|
|
423
444
|
for (const [filePath, lineNumbers] of groundSet.perFile) {
|
|
424
|
-
for (const line of lineNumbers) groundKeys.add(
|
|
445
|
+
for (const line of lineNumbers) groundKeys.add(filePath, line);
|
|
425
446
|
}
|
|
426
447
|
|
|
427
|
-
const claimedKeys = new
|
|
448
|
+
const claimedKeys = new LineKeys();
|
|
428
449
|
const beyondText = { count: 0, sample: [] };
|
|
429
450
|
let confirmed = 0;
|
|
430
451
|
let unverified = 0;
|
|
431
452
|
|
|
432
453
|
const claim = (entry, bucket) => {
|
|
433
|
-
const key =
|
|
454
|
+
const key = claimedKeys.key(entry.file, entry.line);
|
|
434
455
|
if (claimedKeys.has(key)) return; // one bucket per line; first claim wins
|
|
435
456
|
if (!groundKeys.has(key)) {
|
|
436
457
|
// Engine found a call site grep would miss (alias / indirect name).
|
|
@@ -454,7 +475,7 @@ function buildAccount(index, name, parts) {
|
|
|
454
475
|
const excludedByReason = {};
|
|
455
476
|
let excludedTotal = 0;
|
|
456
477
|
for (const e of excludedEntries) {
|
|
457
|
-
const key =
|
|
478
|
+
const key = claimedKeys.key(e.file, e.line);
|
|
458
479
|
if (claimedKeys.has(key)) continue;
|
|
459
480
|
claimedKeys.add(key);
|
|
460
481
|
if (!groundKeys.has(key)) continue; // excluded non-ground line: irrelevant to both grep and display
|
package/core/analysis.js
CHANGED
|
@@ -2509,8 +2509,15 @@ function diffImpact(index, options = {}) {
|
|
|
2509
2509
|
const { getCachedCalls } = require('./callers');
|
|
2510
2510
|
for (const del of deletedFunctions) {
|
|
2511
2511
|
const remaining = [];
|
|
2512
|
-
|
|
2513
|
-
|
|
2512
|
+
// A snapshot: deriving a file's macro-expansion calls re-lists it
|
|
2513
|
+
// in the callee index. Files whose expansions' calls are not
|
|
2514
|
+
// derived yet and whose expanded text holds the name join it
|
|
2515
|
+
// (fix #405).
|
|
2516
|
+
const calleeFiles = new Set(index.getCalleeFiles(del.name) || []);
|
|
2517
|
+
for (const f of require('./rust-macro-expansion').rustPendingFilesBearing(index, [del.name])) {
|
|
2518
|
+
calleeFiles.add(f);
|
|
2519
|
+
}
|
|
2520
|
+
if (calleeFiles.size > 0) {
|
|
2514
2521
|
for (const f of calleeFiles) {
|
|
2515
2522
|
const calls = getCachedCalls(index, f);
|
|
2516
2523
|
if (!Array.isArray(calls)) continue;
|
package/core/cache.js
CHANGED
|
@@ -918,7 +918,14 @@ function clearAllCaches() {
|
|
|
918
918
|
// `passedAsArgument`.
|
|
919
919
|
// v274 (fix #403): a Python reference inside a lambda whose enclosing def
|
|
920
920
|
// binds the name records that def as its `localShadowScope`.
|
|
921
|
-
|
|
921
|
+
// v275 (fix #405): a Rust file whose macro-expansion calls are pending
|
|
922
|
+
// records every expansion's hygiene start and expanded length
|
|
923
|
+
// (`rustMacroExpansion.expansions`); its `words` leave out the text of
|
|
924
|
+
// literals and comments.
|
|
925
|
+
// v276 (fix #405b): a pending Rust file whose expansions the calls pass
|
|
926
|
+
// reads by groups records each group's lines, expanded length and the words
|
|
927
|
+
// its expansions output and its headers hold (`rustMacroExpansion.groups`).
|
|
928
|
+
const CACHE_FORMAT_VERSION = 276;
|
|
922
929
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
923
930
|
|
|
924
931
|
/**
|