ucn 5.4.2 → 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.
Files changed (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +529 -31
  16. package/core/callers.js +12228 -921
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +472 -66
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +3026 -0
  62. package/core/rust-modules.js +203 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1185 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
package/core/account.js CHANGED
@@ -69,6 +69,7 @@ const UNSUPPORTED_SITE_TEXT_MAX = 160;
69
69
  * fileCount: number, // files (indexed + unparsed + unsupported) with >= 1 matching line
70
70
  * perFile: Map<string, number[]>, // absPath -> sorted 1-indexed line numbers (indexed files only)
71
71
  * unparsed: { fileCount: number, lines: number, files: string[] }, // relative paths
72
+ * recovered: { fileCount, lines, files, sites }, // matched lines inside parse-error recovery regions (subset of perFile)
72
73
  * unsupported: { fileCount: number, lines: number, files: string[],
73
74
  * languages: Object<string, number>,
74
75
  * sites: Array<{file: string, line: number, text: string}>,
@@ -125,11 +126,39 @@ function computeGroundSet(index, name) {
125
126
  ? index.discoveryIssues.map(issue => ({ ...issue })) : [];
126
127
  unreadableFiles.sort();
127
128
 
129
+ // fix #367e: ground lines inside a parser-recovery region of an indexed
130
+ // file. They are classified like any other line, but from a partially
131
+ // parsed tree - disclosed, never silently trusted.
132
+ const recovered = { fileCount: 0, lines: 0, files: [], sites: [] };
133
+ for (const [filePath, matchedLines] of perFile) {
134
+ const regions = index.files.get(filePath)?.parseErrorRegions;
135
+ if (!regions || regions.length === 0) continue;
136
+ let inFile = 0;
137
+ let r = 0;
138
+ for (const line of matchedLines) {
139
+ while (r < regions.length && regions[r][1] < line) r++;
140
+ if (r < regions.length && regions[r][0] <= line) {
141
+ inFile++;
142
+ if (recovered.sites.length < 10) {
143
+ recovered.sites.push({ file: path.relative(index.root, filePath), line });
144
+ }
145
+ }
146
+ }
147
+ if (inFile > 0) {
148
+ recovered.fileCount++;
149
+ recovered.lines += inFile;
150
+ recovered.files.push(path.relative(index.root, filePath));
151
+ }
152
+ }
153
+ recovered.files.sort(codeUnitCompare);
154
+ recovered.sites.sort((a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line);
155
+
128
156
  const result = {
129
157
  total: total + unparsed.lines + unsupported.lines,
130
158
  fileCount: fileCount + unparsed.fileCount + unsupported.fileCount,
131
159
  perFile,
132
160
  unparsed,
161
+ recovered,
133
162
  unsupported,
134
163
  unreadableFiles,
135
164
  skippedSources,
@@ -247,6 +276,23 @@ function scanUnsupportedFiles(index, name, opts = {}) {
247
276
  return unsupported;
248
277
  }
249
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
+
250
296
  /**
251
297
  * Classify ground lines not claimed by engine results.
252
298
  *
@@ -260,7 +306,7 @@ function scanUnsupportedFiles(index, name, opts = {}) {
260
306
  * @param {object} index - ProjectIndex
261
307
  * @param {string} name - Symbol name
262
308
  * @param {object} groundSet - from computeGroundSet
263
- * @param {Set<string>} claimedKeys - `${absPath}:${line}` keys already claimed
309
+ * @param {LineKeys} claimedKeys - (absPath, line) keys already claimed
264
310
  * (confirmed + unverified + excluded engine lines)
265
311
  * @returns {{
266
312
  * nonCall: { imports: number, definitions: number, references: number, unclassifiedText: number, total: number },
@@ -282,12 +328,12 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
282
328
  if (!defLinesByFile.has(def.file)) defLinesByFile.set(def.file, new Set());
283
329
  defLinesByFile.get(def.file).add(def.startLine);
284
330
  }
285
- let getCachedCalls = null; // lazy require to avoid cycle at module load
331
+ let getCachedCallsBearing = null; // lazy require to avoid cycle at module load
286
332
 
287
333
  for (const [filePath, lineNumbers] of groundSet.perFile) {
288
334
  let unclaimed = null;
289
335
  for (const line of lineNumbers) {
290
- if (claimedKeys.has(`${filePath}:${line}`)) continue;
336
+ if (claimedKeys.has(claimedKeys.key(filePath, line))) continue;
291
337
  (unclaimed || (unclaimed = [])).push(line);
292
338
  }
293
339
  if (!unclaimed) continue;
@@ -295,9 +341,10 @@ function classifyGroundLines(index, name, groundSet, claimedKeys) {
295
341
  const fileEntry = index.files.get(filePath);
296
342
  const relativePath = fileEntry ? fileEntry.relativePath : path.relative(index.root, filePath);
297
343
 
298
- // Cheap pass: calls cache + symbol table
299
- if (!getCachedCalls) getCachedCalls = require('./callers').getCachedCalls;
300
- 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));
301
348
  const callLines = new Set();
302
349
  if (Array.isArray(cachedCalls)) {
303
350
  for (const c of cachedCalls) {
@@ -390,18 +437,21 @@ function buildAccount(index, name, parts) {
390
437
  const unverifiedEntries = parts.unverifiedEntries || [];
391
438
  const excludedEntries = parts.excludedEntries || [];
392
439
 
393
- 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();
394
444
  for (const [filePath, lineNumbers] of groundSet.perFile) {
395
- for (const line of lineNumbers) groundKeys.add(`${filePath}:${line}`);
445
+ for (const line of lineNumbers) groundKeys.add(filePath, line);
396
446
  }
397
447
 
398
- const claimedKeys = new Set();
448
+ const claimedKeys = new LineKeys();
399
449
  const beyondText = { count: 0, sample: [] };
400
450
  let confirmed = 0;
401
451
  let unverified = 0;
402
452
 
403
453
  const claim = (entry, bucket) => {
404
- const key = `${entry.file}:${entry.line}`;
454
+ const key = claimedKeys.key(entry.file, entry.line);
405
455
  if (claimedKeys.has(key)) return; // one bucket per line; first claim wins
406
456
  if (!groundKeys.has(key)) {
407
457
  // Engine found a call site grep would miss (alias / indirect name).
@@ -425,7 +475,7 @@ function buildAccount(index, name, parts) {
425
475
  const excludedByReason = {};
426
476
  let excludedTotal = 0;
427
477
  for (const e of excludedEntries) {
428
- const key = `${e.file}:${e.line}`;
478
+ const key = claimedKeys.key(e.file, e.line);
429
479
  if (claimedKeys.has(key)) continue;
430
480
  claimedKeys.add(key);
431
481
  if (!groundKeys.has(key)) continue; // excluded non-ground line: irrelevant to both grep and display
@@ -473,6 +523,7 @@ function buildAccount(index, name, parts) {
473
523
  line: e.line, reason: e.reason, provenance: e.provenance })) }),
474
524
  },
475
525
  unparsed: groundSet.unparsed,
526
+ ...(groundSet.recovered?.lines > 0 && { recovered: groundSet.recovered }),
476
527
  unsupported,
477
528
  unreadableFiles: groundSet.unreadableFiles,
478
529
  skippedSources: groundSet.skippedSources || [],