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
@@ -1722,11 +1722,25 @@ function inReadUnit(ctx, record) {
1722
1722
  * with the facts the file entry derives from them.
1723
1723
  */
1724
1724
  function composeCalls(index, ctx) {
1725
+ const calls = composedCalls(ctx);
1726
+ installComposedCalls(index, ctx.filePath, calls, ctx.original);
1727
+ return calls;
1728
+ }
1729
+
1730
+ /** The calls a derivation context composes, stable by line (not installed). */
1731
+ function composedCalls(ctx) {
1725
1732
  readGroups(ctx, ctx.groups.map((group, i) => i));
1726
- const fileEntry = index.files.get(ctx.filePath);
1727
1733
  const calls = ctx.original.filter(record => !inReadUnit(ctx, record));
1728
1734
  for (const group of ctx.groups) for (const record of group.records) calls.push(record);
1729
1735
  for (const record of invocationRecords(ctx)) calls.push(record);
1736
+ // Stable by line: records keep document order within a line.
1737
+ calls.sort((a, b) => a.line - b.line);
1738
+ return calls;
1739
+ }
1740
+
1741
+ /** Install a file's composed calls in the calls cache, with the facts the file entry derives from them. */
1742
+ function installComposedCalls(index, filePath, calls, original) {
1743
+ const fileEntry = index.files.get(filePath);
1730
1744
  // Shapes of generated calls whose value may be lost (audit-async).
1731
1745
  const { openCallShape } = require('../languages/rust-value-flow');
1732
1746
  const shapes = calls.filter(call => call.macroExpansion && call.name && !call.inMacro && !call.valueConsumed)
@@ -1734,18 +1748,15 @@ function composeCalls(index, ctx) {
1734
1748
  if (shapes.length > 0) {
1735
1749
  fileEntry.openCalls = [...new Set([...(fileEntry.openCalls || []), ...shapes])].sort(codeUnitCompare);
1736
1750
  }
1737
- // Stable by line: records keep document order within a line.
1738
- calls.sort((a, b) => a.line - b.line);
1739
- const cached = index.callsCache.get(ctx.filePath);
1751
+ const cached = index.callsCache.get(filePath);
1740
1752
  if (cached) {
1741
- if (index.calleeIndex) index._removeFromCalleeIndex(ctx.filePath, ctx.original);
1753
+ if (index.calleeIndex) index._removeFromCalleeIndex(filePath, original);
1742
1754
  cached.calls = calls;
1743
- if (index.calleeIndex) index._addToCalleeIndex(ctx.filePath, calls);
1755
+ if (index.calleeIndex) index._addToCalleeIndex(filePath, calls);
1744
1756
  } else {
1745
- index.callsCache.set(ctx.filePath, { mtime: fileEntry.mtime, hash: fileEntry.hash, calls });
1757
+ index.callsCache.set(filePath, { mtime: fileEntry.mtime, hash: fileEntry.hash, calls });
1746
1758
  }
1747
1759
  index.callsCacheDirty = true;
1748
- return calls;
1749
1760
  }
1750
1761
 
1751
1762
  const CALL_NODES = new Set(['call_expression', 'macro_invocation', 'struct_expression', 'generic_function']);
@@ -2156,11 +2167,62 @@ function startRustMacroExpansion(index, pending, options = {}) {
2156
2167
  const files = invokingFiles(index, pending, inventory, names);
2157
2168
  let bytes = 0;
2158
2169
  for (const file of files) bytes += index.files.get(file)?.size || 0;
2159
- const run = files.length >= PARALLEL_MIN_FILES && bytes >= PARALLEL_MIN_BYTES
2160
- ? startWorkers(index, files, { ...options, inventory }) : null;
2170
+ // A sequential build has no parse workers to keep (fix #422): when the
2171
+ // planned expansion is large - a crate whose API macros generate most of
2172
+ // its code has little source to parse - fresh workers expand it.
2173
+ let pool = options.pool?.available.length > 0 ? options.pool : null;
2174
+ let ownPool = null;
2175
+ if (!pool && freshExpansionWorthwhile(index, files, inventory)) {
2176
+ ownPool = require('./parallel-build').spawnExpansionPool(index,
2177
+ freshWorkerCount(index, Math.ceil(files.length / FRESH_FILES_PER_WORKER), files.length));
2178
+ pool = ownPool;
2179
+ }
2180
+ const run = files.length >= PARALLEL_MIN_FILES && (bytes >= PARALLEL_MIN_BYTES || ownPool)
2181
+ ? startWorkers(index, files, { ...options, pool, inventory }) : null;
2182
+ if (ownPool) {
2183
+ if (run) {
2184
+ const dispose = run.dispose;
2185
+ run.dispose = () => { dispose(); ownPool.dispose(); };
2186
+ } else {
2187
+ ownPool.dispose();
2188
+ }
2189
+ }
2161
2190
  return { inventory, files, names, results: run };
2162
2191
  }
2163
2192
 
2193
+ // Fresh expansion workers start cold (module load, parser warm-up): they
2194
+ // pay for themselves from this much planned expansion.
2195
+ const FRESH_MIN_SITES = 48;
2196
+ const FRESH_MIN_FILES = 12;
2197
+ const FRESH_FILES_PER_WORKER = 6;
2198
+
2199
+ /**
2200
+ * How many fresh threads to start for `wanted` workers' worth of work over
2201
+ * `files` files: 2 to EXPANSION_WORKERS, never more than an explicit build
2202
+ * worker count (a request for that execution shape) and none when the build
2203
+ * disabled workers; 0 when fewer than two would run.
2204
+ */
2205
+ function freshWorkerCount(index, wanted, files) {
2206
+ const limit = index.lastBuildWorkerLimit ?? EXPANSION_WORKERS;
2207
+ const count = Math.min(EXPANSION_WORKERS, limit, files, Math.max(2, wanted));
2208
+ return count >= 2 ? count : 0;
2209
+ }
2210
+
2211
+ /** Is the planned expansion of a build without kept parse workers worth fresh workers? */
2212
+ function freshExpansionWorthwhile(index, files, inventory) {
2213
+ const env = process.env.UCN_FRESH_EXPANSION;
2214
+ if (env === '0' || freshWorkerCount(index, 2, files.length) === 0) return false;
2215
+ if (env === '1') return true;
2216
+ if (files.length < FRESH_MIN_FILES) return false;
2217
+ let sites = 0;
2218
+ for (const file of files) {
2219
+ for (const call of index.callsCache.get(file)?.calls || []) {
2220
+ if (call.isMacro && inventory.has(call.name)) sites++;
2221
+ }
2222
+ }
2223
+ return sites >= FRESH_MIN_SITES;
2224
+ }
2225
+
2164
2226
  /**
2165
2227
  * Build-time pass, phase 2b (after the import graph resolved `mod`
2166
2228
  * declarations): apply the expansions of the planned files, expanding in
@@ -2235,7 +2297,7 @@ const EXPANSION_WORKERS = 3;
2235
2297
  * to expand their import facts, impl/trait spans and macro invocation
2236
2298
  * records. Also every name a project callable bears.
2237
2299
  */
2238
- function workerSnapshot(index, workload, inventory) {
2300
+ function workerSnapshot(index, workload, inventory, fullCalls = false) {
2239
2301
  const expanding = new Set(workload);
2240
2302
  // Invocations resolve at the invoking file, and nested ones also at the
2241
2303
  // defining file.
@@ -2266,7 +2328,10 @@ function workerSnapshot(index, workload, inventory) {
2266
2328
  record.importBindings = (entry.importBindings || [])
2267
2329
  .filter(binding => inventory.has(binding.alias || binding.name));
2268
2330
  }
2269
- if (full) {
2331
+ if (full && fullCalls) {
2332
+ // A calls derivation reads every parsed record of the file.
2333
+ record.calls = index.callsCache.get(file)?.calls || [];
2334
+ } else if (full) {
2270
2335
  record.calls = (index.callsCache.get(file)?.calls || [])
2271
2336
  .filter(call => call.isMacro && !call.macroExpansion && inventory.has(call.name))
2272
2337
  .map(call => ({
@@ -2337,7 +2402,7 @@ function snapshotIndex(snapshot) {
2337
2402
  * `signal[queueIndex]`), posting each record to `port`; `signal[workerIndex]`
2338
2403
  * is set when this worker is done.
2339
2404
  */
2340
- function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex }, port) {
2405
+ async function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex, mode }, port) {
2341
2406
  const signalArray = new Int32Array(signal);
2342
2407
  try {
2343
2408
  const state = createState(snapshotIndex(JSON.parse(Buffer.from(snapshot).toString('utf8'))));
@@ -2347,11 +2412,15 @@ function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex },
2347
2412
  const file = files[next];
2348
2413
  let result;
2349
2414
  try {
2350
- result = { file, record: expandForWorker(state, file) };
2415
+ result = { file, record: mode === 'calls' ? deriveForWorker(state, file) : expandForWorker(state, file) };
2351
2416
  } catch (error) {
2352
2417
  result = { file, error: error.message };
2353
2418
  }
2354
2419
  port.postMessage([result]);
2420
+ // Yield between files, as the parse loop does (fix #365): the
2421
+ // native trees of an expansion are freed only when this thread
2422
+ // returns to its event loop (fix #422).
2423
+ await new Promise(resolve => setImmediate(resolve));
2355
2424
  }
2356
2425
  } finally {
2357
2426
  Atomics.store(signalArray, workerIndex, 1);
@@ -2359,6 +2428,12 @@ function runExpansionQueue({ snapshot, files, signal, workerIndex, queueIndex },
2359
2428
  }
2360
2429
  }
2361
2430
 
2431
+ /** Worker-side: the whole-file calls derivation of one file (fix #422). */
2432
+ function deriveForWorker(state, filePath) {
2433
+ const ctx = expandFile(state, filePath, 'calls')?.context || null;
2434
+ return ctx ? { calls: composedCalls(ctx) } : { none: true };
2435
+ }
2436
+
2362
2437
  /** Worker-side: symbols-mode expansion of one file, as plain records. */
2363
2438
  function expandForWorker(state, filePath) {
2364
2439
  const rec = expandFile(state, filePath) || { sites: 0, blind: [], used: new Map(), expanded: false };
@@ -2391,7 +2466,8 @@ function startWorkers(index, files, options = {}) {
2391
2466
  codeUnitCompare(a, b));
2392
2467
  // One serialization shared by every worker (a structured clone per
2393
2468
  // worker cost more than the expansion on large workspaces).
2394
- const encoded = Buffer.from(JSON.stringify(workerSnapshot(index, files, options.inventory)), 'utf8');
2469
+ const encoded = Buffer.from(JSON.stringify(workerSnapshot(index, files, options.inventory,
2470
+ options.mode === 'calls')), 'utf8');
2395
2471
  const snapshot = new SharedArrayBuffer(encoded.length);
2396
2472
  encoded.copy(Buffer.from(snapshot));
2397
2473
  const sab = new SharedArrayBuffer(4 * (count + 1));
@@ -2400,7 +2476,8 @@ function startWorkers(index, files, options = {}) {
2400
2476
  for (let i = 0; i < count; i++) {
2401
2477
  const worker = pool.available[i];
2402
2478
  ports.push(pool.ports[worker]);
2403
- pool.post(worker, { snapshot, files: ordered, signal: sab, workerIndex: i, queueIndex: count });
2479
+ pool.post(worker, { snapshot, files: ordered, signal: sab, workerIndex: i, queueIndex: count,
2480
+ ...(options.mode === 'calls' && { mode: 'calls' }) });
2404
2481
  }
2405
2482
  // Kept workers this job does not need end now.
2406
2483
  for (const worker of pool.available.slice(count)) pool.retire(worker);
@@ -2451,12 +2528,16 @@ function rustExpandedCalls(index, filePath, rawCalls, { materialize = true } = {
2451
2528
  rec.callsPending = true;
2452
2529
  throw error;
2453
2530
  }
2454
- index._rustMacroState.contexts.delete(filePath);
2531
+ finishDerivation(index, filePath, rec);
2532
+ return index.callsCache.get(filePath)?.calls || rawCalls;
2533
+ }
2534
+
2535
+ function finishDerivation(index, filePath, rec) {
2536
+ index._rustMacroState?.contexts.delete(filePath);
2455
2537
  delete rec.words;
2456
2538
  delete rec.groups;
2457
2539
  delete rec.expansions;
2458
2540
  index.macroExpansionDirty = true;
2459
- return index.callsCache.get(filePath)?.calls || rawCalls;
2460
2541
  }
2461
2542
 
2462
2543
  // Derivation contexts whose trees are kept (fix #405); a context dropped
@@ -2976,13 +3057,64 @@ function completeRustMacroCalls(index) {
2976
3057
 
2977
3058
  /** Derive every pending file's expansion calls (whole-program consumers). */
2978
3059
  function materializeRustMacroCalls(index) {
3060
+ const pending = [];
3061
+ for (const [filePath, entry] of index.files) {
3062
+ if (!entry.rustMacroExpansion?.callsPending || !index.callsCache.get(filePath)) continue;
3063
+ // A file a query derived in part keeps its context's records.
3064
+ if (index._rustMacroState?.contexts.has(filePath)) continue;
3065
+ pending.push(filePath);
3066
+ }
3067
+ // Many files to derive at once (an orientation ranking, a deadcode
3068
+ // audit): fresh workers derive them (fix #422), each whole in one pass
3069
+ // exactly as below.
3070
+ const derived = deriveInWorkers(index, pending);
2979
3071
  for (const [filePath, entry] of index.files) {
2980
3072
  if (!entry.rustMacroExpansion?.callsPending) continue;
2981
3073
  const cached = index.callsCache.get(filePath);
2982
- if (cached) rustExpandedCalls(index, filePath, cached.calls);
3074
+ if (!cached) continue;
3075
+ const result = derived?.get(filePath);
3076
+ if (result) installDerived(index, filePath, entry.rustMacroExpansion, cached, result);
3077
+ else rustExpandedCalls(index, filePath, cached.calls);
2983
3078
  }
2984
3079
  }
2985
3080
 
3081
+ // Expanded text a materialization derives in fresh workers from, and per
3082
+ // worker (the build recorded each expansion's length): below it, starting
3083
+ // threads costs more than deriving in place.
3084
+ const DERIVE_IN_WORKERS_MIN_BYTES = 192 * 1024;
3085
+ const DERIVE_BYTES_PER_WORKER = 192 * 1024;
3086
+
3087
+ /** Whole-file calls derivations of `files` in fresh workers: Map<file, { calls } | { none }>, or null. */
3088
+ function deriveInWorkers(index, files) {
3089
+ const env = process.env.UCN_FRESH_EXPANSION;
3090
+ if (env === '0' || freshWorkerCount(index, 2, files.length) === 0) return null;
3091
+ let bytes = 0;
3092
+ for (const file of files) {
3093
+ const recorded = index.files.get(file)?.rustMacroExpansion?.expansions || [];
3094
+ for (let i = 2; i < recorded.length; i += 3) if (recorded[i] > 0) bytes += recorded[i];
3095
+ }
3096
+ if (env !== '1' && bytes < DERIVE_IN_WORKERS_MIN_BYTES) return null;
3097
+ const inventory = index._rustMacroState?.inventory || macroInventory(index);
3098
+ if (inventory.size === 0) return null;
3099
+ const count = freshWorkerCount(index, Math.ceil(bytes / DERIVE_BYTES_PER_WORKER), files.length);
3100
+ const pool = require('./parallel-build').spawnExpansionPool(index, count);
3101
+ if (!pool) return null;
3102
+ try {
3103
+ const run = startWorkers(index, files, { pool, inventory, mode: 'calls' });
3104
+ return run ? run.collect() : null;
3105
+ } finally {
3106
+ pool.dispose();
3107
+ }
3108
+ }
3109
+
3110
+ function installDerived(index, filePath, rec, cached, result) {
3111
+ const hints = (rec.calleeNames || []).map(name => ({ name, line: 0, calleeHint: true }));
3112
+ if (index.calleeIndex && hints.length > 0) index._removeFromCalleeIndex(filePath, hints);
3113
+ rec.callsPending = false;
3114
+ if (result.calls) installComposedCalls(index, filePath, result.calls, cached.calls);
3115
+ finishDerivation(index, filePath, rec);
3116
+ }
3117
+
2986
3118
  /** Expanded and blind invocation sites, for health and deadcode disclosure. */
2987
3119
  function rustMacroExpansionSummary(index) {
2988
3120
  const summary = { sites: 0, files: 0, blind: { count: 0, fileCount: 0, files: [], sample: [], reasons: {} } };
@@ -200,4 +200,4 @@ function rustItemAccessible(index, modifiers, file, line, siteFile, siteLine) {
200
200
  return scope.length <= site.segs.length && scope.every((seg, i) => site.segs[i] === seg);
201
201
  }
202
202
 
203
- module.exports = { rustPathModule, rustModuleItems, rustModulePathAt, rustItemAccessible };
203
+ module.exports = { rustPathModule, rustModuleItems, rustModulePathAt, rustItemAccessible, moduleTree };