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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # UCN - Universal Code Navigator
2
2
 
3
- See what code does before you touch it.
3
+ Auditable answers, with every match counted, unsure ones marked and blind spots named.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/ucn)](https://www.npmjs.com/package/ucn)
6
6
  [![tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/mleoca/0e10a790e16ab61ddd233e05645e203e/raw/ucn-tests.json)](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. CONTRACT describes the scope of that accounting. Warnings identify
107
- source the index could not cover. These details survive text truncation for
108
- an agent's output budget; a budget too small to hold them withholds the answer
109
- and names the budget that would.
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 {Set<string>} claimedKeys - `${absPath}:${line}` keys already claimed
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 getCachedCalls = null; // lazy require to avoid cycle at module load
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(`${filePath}:${line}`)) continue;
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
- if (!getCachedCalls) getCachedCalls = require('./callers').getCachedCalls;
329
- const cachedCalls = getCachedCalls(index, filePath);
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
- const groundKeys = new Set();
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(`${filePath}:${line}`);
445
+ for (const line of lineNumbers) groundKeys.add(filePath, line);
425
446
  }
426
447
 
427
- const claimedKeys = new Set();
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 = `${entry.file}:${entry.line}`;
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 = `${e.file}:${e.line}`;
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
- const calleeFiles = index.getCalleeFiles(del.name);
2513
- if (calleeFiles) {
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
- const CACHE_FORMAT_VERSION = 274;
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
  /**