ucn 5.5.2 → 5.6.0

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 (74) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -12
  2. package/.claude/skills/ucn/references/commands.md +21 -17
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +474 -0
  6. package/README.md +71 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +138 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +273 -107
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +29 -65
  13. package/core/cache.js +101 -16
  14. package/core/callers.js +352 -70
  15. package/core/capture.js +331 -0
  16. package/core/change-kinds.js +888 -0
  17. package/core/check.js +1117 -139
  18. package/core/client-receivers.js +189 -0
  19. package/core/command-contracts.js +7 -6
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +388 -85
  23. package/core/deletion.js +401 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoints.js +12 -6
  26. package/core/evidence-bands.js +68 -0
  27. package/core/evidence-bands.json +44 -0
  28. package/core/execute.js +172 -161
  29. package/core/hot-summary.js +241 -0
  30. package/core/index-ir.js +3 -1
  31. package/core/inprocess-clients.js +41 -15
  32. package/core/ir.js +5 -1
  33. package/core/member-signatures.js +789 -0
  34. package/core/output/analysis-ext.js +20 -10
  35. package/core/output/analysis.js +98 -60
  36. package/core/output/check.js +98 -37
  37. package/core/output/doctor.js +34 -1
  38. package/core/output/find.js +2 -1
  39. package/core/output/lines.js +63 -3
  40. package/core/output/public.js +30 -11
  41. package/core/output/refactoring.js +32 -0
  42. package/core/output/reporting.js +21 -2
  43. package/core/output/search.js +11 -2
  44. package/core/output/tracing.js +35 -7
  45. package/core/output-budget.js +124 -57
  46. package/core/parallel-build.js +3 -3
  47. package/core/project.js +106 -23
  48. package/core/provenance-type.js +12 -5
  49. package/core/public-command.js +35 -9
  50. package/core/python-modules.js +6 -0
  51. package/core/reflection.js +166 -23
  52. package/core/registry.js +623 -72
  53. package/core/reporting.js +69 -34
  54. package/core/request-helpers.js +63 -7
  55. package/core/review-items.js +75 -0
  56. package/core/search.js +89 -128
  57. package/core/shared.js +0 -45
  58. package/core/snapshot.js +350 -0
  59. package/core/source-universe.js +707 -0
  60. package/core/test-units.js +3624 -0
  61. package/core/tracing.js +150 -96
  62. package/core/verify.js +27 -11
  63. package/languages/adapter.js +8 -3
  64. package/languages/c-family.js +12 -0
  65. package/languages/csharp.js +634 -0
  66. package/languages/go.js +120 -0
  67. package/languages/index.js +422 -4
  68. package/languages/java.js +712 -0
  69. package/languages/javascript.js +1460 -2
  70. package/languages/lexical-scope.js +68 -0
  71. package/languages/python.js +1672 -1
  72. package/languages/rust.js +91 -0
  73. package/mcp/server.js +44 -110
  74. package/package.json +11 -6
package/core/account.js CHANGED
@@ -16,13 +16,17 @@
16
16
  * strings, and scanner-skipped tokens such as JS builtins —
17
17
  * deliberately named "unclassified", not "comment")
18
18
  * unparsed - line in a file that failed to parse (still readable text)
19
- * unsupported - line in a source file whose language UCN cannot parse
20
- * (index.unsupportedFiles — e.g. .rb in a mixed repo).
21
- * Text-scanned only, never analyzed; sites are listed so the
22
- * answer is a superset of grep, and the CONTRACT sentence
23
- * degrades whenever this bucket is non-zero. Without it a
24
- * mixed-language repo got "partition complete" over a ground
25
- * set that silently excluded whole languages.
19
+ * unsupported - line in TEXT-ONLY source: readable source UCN does not
20
+ * analyze (an unsupported language such as .rb in a mixed
21
+ * repo, a bundled/minified file, a file over the size or
22
+ * file limit, a path `.ucn.json` excludes from indexing —
23
+ * core/source-universe.js). Text-scanned only; sites are
24
+ * listed so the answer is a superset of grep, and the
25
+ * CONTRACT degrades for THIS name whenever the bucket is
26
+ * non-zero. Without it a mixed-language repo got "partition
27
+ * complete" over a ground set that silently excluded whole
28
+ * languages; with a repo-wide withdrawal instead, one
29
+ * vendored bundle degraded every answer in the repository.
26
30
  * unaccounted - residual; 0 when the arithmetic is conserved
27
31
  *
28
32
  * Conservation invariant:
@@ -50,15 +54,10 @@
50
54
 
51
55
  'use strict';
52
56
 
53
- const fs = require('fs');
54
57
  const path = require('path');
55
58
  const { codeUnitCompare, literalNameRegex } = require('./shared');
56
59
  const { compareNames } = require('./discovery');
57
-
58
- // Unsupported-language site listings are capped so a Rails-sized repo cannot
59
- // flood the account object; the counts always cover the full set.
60
- const UNSUPPORTED_SITE_CAP = 50;
61
- const UNSUPPORTED_SITE_TEXT_MAX = 160;
60
+ const { scanTextOnly, countNameLines, opaqueSummary } = require('./source-universe');
62
61
 
63
62
  /**
64
63
  * Compute the text-occurrence ground set for a symbol name.
@@ -75,7 +74,9 @@ const UNSUPPORTED_SITE_TEXT_MAX = 160;
75
74
  * languages: Object<string, number>,
76
75
  * sites: Array<{file: string, line: number, text: string}>,
77
76
  * sitesTruncated: boolean },
78
- * unreadableFiles: string[] // relative paths; OUTSIDE the arithmetic
77
+ * unreadableFiles: string[], // relative paths; OUTSIDE the arithmetic
78
+ * opaque: { count: number, reasons: Object<string, number>,
79
+ * sample: Array<{path: string, reason: string}> } // paths no name can be checked against
79
80
  * }}
80
81
  */
81
82
  function computeGroundSet(index, name) {
@@ -115,9 +116,12 @@ function computeGroundSet(index, name) {
115
116
  // grep would show. Their matching lines join the ground set as
116
117
  // `unsupported`, with sites listed so the caller answer strictly
117
118
  // dominates grep instead of silently under-reporting it.
118
- const unsupported = scanUnsupportedFiles(index, name, { unreadableFiles });
119
- const skippedSources = Array.isArray(index.discoveryIssues)
120
- ? index.discoveryIssues.map(issue => ({ ...issue })) : [];
119
+ const unsupported = scanUnsupportedFiles(index, name);
120
+ // Paths whose contents cannot be read name by name (an unreadable
121
+ // directory, a failed git listing, text beyond the scan budget). They
122
+ // say nothing about this name in particular, so they weaken the claim
123
+ // for every name; everything readable was scanned above.
124
+ const opaque = opaqueSummary(index);
121
125
  unreadableFiles.sort();
122
126
 
123
127
  // fix #367e: ground lines inside a parser-recovery region of an indexed
@@ -155,7 +159,7 @@ function computeGroundSet(index, name) {
155
159
  recovered,
156
160
  unsupported,
157
161
  unreadableFiles,
158
- skippedSources,
162
+ opaque,
159
163
  };
160
164
  if (index._groundSetCache) {
161
165
  const weight = result.total + result.fileCount;
@@ -209,21 +213,15 @@ function scanFailedFiles(index, name) {
209
213
  if (!index.failedFiles || index.failedFiles.size === 0) {
210
214
  return { unparsed, unreadableFiles };
211
215
  }
212
- const wordRe = literalNameRegex(name);
213
216
  for (const failedPath of index.failedFiles) {
214
217
  if (index.files.has(failedPath)) continue;
215
- let content;
218
+ let matched;
216
219
  try {
217
- content = fs.readFileSync(failedPath, 'utf-8');
220
+ matched = countNameLines(failedPath, name);
218
221
  } catch (_) {
219
222
  unreadableFiles.push(path.relative(index.root, failedPath));
220
223
  continue;
221
224
  }
222
- if (!content.includes(name)) continue;
223
- let matched = 0;
224
- for (const line of content.split('\n')) {
225
- if (line.includes(name) && wordRe.test(line)) matched++;
226
- }
227
225
  if (matched > 0) {
228
226
  unparsed.fileCount++;
229
227
  unparsed.lines += matched;
@@ -236,66 +234,22 @@ function scanFailedFiles(index, name) {
236
234
  }
237
235
 
238
236
  /**
239
- * Word-boundary-scan index.unsupportedFiles for `name`. Returns the
240
- * `unsupported` account bucket ({ fileCount, lines, files, languages, sites,
241
- * sitesTruncated }). Shared by computeGroundSet and by commands whose result
242
- * is not account-shaped (tests, usages) but still must not stay silent when
243
- * a mixed-language repo has occurrences UCN cannot analyze.
237
+ * Word-boundary-scan the project's text-only source for `name`. Returns the
238
+ * `unsupported` account bucket ({ fileCount, lines, files, languages,
239
+ * reasons, sites, sitesTruncated }). Shared by computeGroundSet and by
240
+ * commands whose result is not account-shaped (tests, usages) but still
241
+ * must not stay silent when a repo has occurrences UCN cannot analyze. The
242
+ * scan itself belongs to core/source-universe.js.
243
+ *
244
+ * A file that cannot be read while it is scanned becomes an unsearchable
245
+ * path of the universe (reported through `opaqueSummary` / `unsearchedNote`),
246
+ * never a silently shorter answer.
244
247
  *
245
248
  * @param {object} index - ProjectIndex
246
- * @param {string} name - Symbol name (matched with \b word boundaries)
247
- * @param {object} [opts]
248
- * @param {string[]} [opts.unreadableFiles] - collector for read failures
249
+ * @param {string} name - Symbol name (matched with word boundaries)
249
250
  */
250
- function scanUnsupportedFiles(index, name, opts = {}) {
251
- const unsupported = {
252
- fileCount: 0, lines: 0, files: [], languages: {}, sites: [],
253
- sitesTruncated: false,
254
- };
255
- if (!Array.isArray(index.unsupportedFiles) || index.unsupportedFiles.length === 0) {
256
- return unsupported;
257
- }
258
- const wordRe = literalNameRegex(name);
259
- const languageCounts = new Map();
260
- for (const skipped of index.unsupportedFiles) {
261
- const absPath = path.join(index.root, skipped.relativePath);
262
- let content;
263
- try {
264
- content = index._readFile(absPath);
265
- } catch (e) {
266
- if (opts.unreadableFiles) opts.unreadableFiles.push(skipped.relativePath);
267
- continue;
268
- }
269
- if (!content.includes(name)) continue;
270
- const lines = content.split('\n');
271
- let matched = 0;
272
- for (let i = 0; i < lines.length; i++) {
273
- if (!lines[i].includes(name) || !wordRe.test(lines[i])) continue;
274
- matched++;
275
- if (unsupported.sites.length < UNSUPPORTED_SITE_CAP) {
276
- unsupported.sites.push({
277
- file: skipped.relativePath,
278
- line: i + 1,
279
- text: lines[i].trim().slice(0, UNSUPPORTED_SITE_TEXT_MAX),
280
- });
281
- } else {
282
- unsupported.sitesTruncated = true;
283
- }
284
- }
285
- if (matched > 0) {
286
- unsupported.fileCount++;
287
- unsupported.lines += matched;
288
- unsupported.files.push(skipped.relativePath);
289
- const lang = skipped.language || skipped.extension || 'unknown';
290
- languageCounts.set(lang, (languageCounts.get(lang) || 0) + matched);
291
- }
292
- }
293
- unsupported.files.sort(codeUnitCompare);
294
- unsupported.sites.sort((a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line);
295
- for (const lang of [...languageCounts.keys()].sort(codeUnitCompare)) {
296
- unsupported.languages[lang] = languageCounts.get(lang);
297
- }
298
- return unsupported;
251
+ function scanUnsupportedFiles(index, name) {
252
+ return scanTextOnly(index, name);
299
253
  }
300
254
 
301
255
  /**
@@ -544,13 +498,16 @@ function buildAccount(index, name, parts) {
544
498
  groundSet.unparsed.lines + unsupported.lines;
545
499
  const unaccounted = groundSet.total - accountedTotal;
546
500
 
547
- // Occurrences in unsupported-language files break the whole-text claim:
548
- // UCN cannot adjudicate them, so "partition complete" would be false.
501
+ // The claim is about THIS name. An occurrence in text-only source
502
+ // breaks it (UCN cannot adjudicate the line); a path nothing can be
503
+ // read from breaks it for every name; unindexed source the name does
504
+ // not occur in leaves it standing.
505
+ const opaque = groundSet.opaque || { count: 0, reasons: {}, sample: [] };
549
506
  const textComplete = unaccounted === 0 &&
550
507
  groundSet.unparsed.fileCount === 0 &&
551
508
  unsupported.lines === 0 &&
552
509
  groundSet.unreadableFiles.length === 0 &&
553
- (!groundSet.skippedSources || groundSet.skippedSources.length === 0);
510
+ opaque.count === 0;
554
511
  const observedTextZero = textComplete && confirmed === 0 && unverified === 0 &&
555
512
  beyondText.count === 0;
556
513
 
@@ -570,7 +527,7 @@ function buildAccount(index, name, parts) {
570
527
  ...(groundSet.recovered?.lines > 0 && { recovered: groundSet.recovered }),
571
528
  unsupported,
572
529
  unreadableFiles: groundSet.unreadableFiles,
573
- skippedSources: groundSet.skippedSources || [],
530
+ opaque,
574
531
  beyondText,
575
532
  unaccounted,
576
533
  conserved: unaccounted === 0,