ucn 5.5.1 → 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.
Files changed (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +30 -21
  6. package/cli/index.js +1 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +137 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +4726 -940
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +217 -112
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +151 -19
  63. package/core/rust-modules.js +1 -1
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +124 -37
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +510 -82
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. 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 `includes()`-gated read per project
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
- if (!content.includes(name)) continue;
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({
@@ -344,18 +366,31 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
344
366
  // Cheap pass: calls cache + symbol table (only records bearing the
345
367
  // name on unclaimed lines are read below).
346
368
  if (!getCachedCallsBearing) getCachedCallsBearing = require('./callers').getCachedCallsBearing;
347
- const cachedCalls = getCachedCallsBearing(index, filePath, name, new Set(unclaimed));
369
+ const unclaimedLines = new Set(unclaimed);
370
+ const cachedCalls = getCachedCallsBearing(index, filePath, name, unclaimedLines);
371
+ const recordIsNoCall = require('./callers')._recordIsNoCall;
348
372
  const callLines = new Set();
373
+ // `(T)(x)` casts: uses of the type, not calls (fix #427).
374
+ let castLines = null;
349
375
  if (Array.isArray(cachedCalls)) {
350
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;
351
381
  // Unclaimed callback/type references have no invocation
352
382
  // syntax. Let the usage AST classify them as references;
353
383
  // merely entering the candidate cache is not a call fact.
354
- if (c.isFunctionReference || c.isTypeReference) continue;
355
- if (c.name === name || c.resolvedName === name ||
356
- (c.resolvedNames && c.resolvedNames.includes(name))) {
357
- callLines.add(c.line);
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;
358
392
  }
393
+ callLines.add(c.line);
359
394
  }
360
395
  }
361
396
  const defLines = defLinesByFile.get(filePath);
@@ -364,6 +399,8 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
364
399
  for (const line of unclaimed) {
365
400
  if (callLines.has(line)) {
366
401
  callNotResolved.push({ file: filePath, relativePath, line });
402
+ } else if (castLines?.has(line)) {
403
+ nonCall.references++;
367
404
  } else if (defLines && defLines.has(line)) {
368
405
  nonCall.definitions++;
369
406
  } else {
@@ -377,8 +414,11 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
377
414
  // Call lines were classified from the complete calls cache above.
378
415
  // Language adapters may skip usage-only call recovery here; notably,
379
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.
380
419
  const usages = index._getCachedUsages(filePath, name, {
381
420
  skipCallRecovery: true,
421
+ usageKindsOnly: true,
382
422
  });
383
423
  const byLine = new Map();
384
424
  if (Array.isArray(usages)) {
@@ -433,9 +473,13 @@ const RANK = { call: 0, import: 1, definition: 2, reference: 3 };
433
473
  */
434
474
  function buildAccount(index, name, parts) {
435
475
  const { groundSet } = parts;
436
- const confirmedEntries = parts.confirmedEntries || [];
437
- const unverifiedEntries = parts.unverifiedEntries || [];
438
- const excludedEntries = parts.excludedEntries || [];
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 || []);
439
483
 
440
484
  // (file, line) keys, kept per file (fix #405b: one composed string per
441
485
  // ground line and per entry); a line is keyed by its text, as the
@@ -562,6 +606,16 @@ function relPath(index, filePath) {
562
606
  return fileEntry ? fileEntry.relativePath : path.relative(index.root, filePath);
563
607
  }
564
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
+
565
619
  module.exports = {
566
620
  computeGroundSet,
567
621
  classifyGroundLines,