ucn 5.5.3 → 5.6.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 (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  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 +485 -0
  6. package/README.md +66 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
package/core/verify.js CHANGED
@@ -1215,7 +1215,7 @@ const GIT_REF_FORMAT = /^[a-zA-Z0-9._\-~\/^@{}:]+$/; // eslint-disable-line no-
1215
1215
  */
1216
1216
  let baseReadSerial = 0;
1217
1217
 
1218
- function startBaseRead(file, base) {
1218
+ function startBaseRead(file, base, repoFile = file) {
1219
1219
  if (process.platform === 'win32' || !file || !GIT_REF_FORMAT.test(base)) return null;
1220
1220
  const out = path.join(os.tmpdir(),
1221
1221
  `ucn-base-${process.pid}-${++baseReadSerial}-${Math.random().toString(36).slice(2, 10)}`);
@@ -1225,8 +1225,8 @@ function startBaseRead(file, base) {
1225
1225
  // marks a revision that does not hold the file.
1226
1226
  child = spawn('/bin/sh', ['-c',
1227
1227
  'git show "$1" > "$2.part" 2>/dev/null && mv "$2.part" "$2" || { rm -f "$2.part"; : > "$2.none"; }',
1228
- 'ucn-base', `${base}:./${path.basename(file)}`, out],
1229
- { cwd: path.dirname(file), stdio: 'ignore' });
1228
+ 'ucn-base', `${base}:./${path.basename(repoFile)}`, out],
1229
+ { cwd: path.dirname(repoFile), stdio: 'ignore' });
1230
1230
  } catch {
1231
1231
  child = null;
1232
1232
  }
@@ -1299,8 +1299,11 @@ function baseDeclarationChanges(index, name, def, base = 'HEAD', baseRead = null
1299
1299
  if (baseContent === null) continue;
1300
1300
  if (baseContent === undefined) {
1301
1301
  try {
1302
- baseContent = execFileSync('git', ['show', `${base}:./${path.basename(file)}`], {
1303
- cwd: path.dirname(file), encoding: 'utf-8', maxBuffer: 256 * 1024 * 1024,
1302
+ // The repository's copy of the file (a snapshot exported
1303
+ // from the git index is not a repository).
1304
+ const repoFile = index.repoPathOf ? index.repoPathOf(file) : file;
1305
+ baseContent = execFileSync('git', ['show', `${base}:./${path.basename(repoFile)}`], {
1306
+ cwd: path.dirname(repoFile), encoding: 'utf-8', maxBuffer: 256 * 1024 * 1024,
1304
1307
  stdio: ['ignore', 'pipe', 'ignore'],
1305
1308
  });
1306
1309
  } catch {
@@ -1774,10 +1777,19 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
1774
1777
  if (!argsNode) return { args: [], argCount: 0, isMethodCall, ...ctx };
1775
1778
 
1776
1779
  let args = [];
1780
+ const argumentFacts = [];
1777
1781
  for (let i = 0; i < argsNode.namedChildCount; i++) {
1778
1782
  const argNode = argsNode.namedChild(i);
1779
1783
  if (argNode.type.includes('comment')) continue;
1780
1784
  args.push(argNode.text.trim());
1785
+ let value = argNode.type === 'keyword_argument' ? argNode.childForFieldName('value') : argNode;
1786
+ // C# wraps each ordinary value in an argument node. Named or
1787
+ // ref/out arguments remain outside the literal proof domain.
1788
+ if (value?.type === 'argument' && value.namedChildCount === 1 && value.childCount === 1) {
1789
+ value = value.namedChild(0);
1790
+ }
1791
+ argumentFacts.push({ kind: classifyArgNode(value), nodeType: value?.type, text: value?.text,
1792
+ ...(argNode.type === 'keyword_argument' && { keyword: argNode.childForFieldName('name')?.text }) });
1781
1793
  }
1782
1794
 
1783
1795
  // Python argument structure (fix #281): keyword arguments bind by
@@ -1847,7 +1859,10 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
1847
1859
  }
1848
1860
  }
1849
1861
 
1850
- return {
1862
+ return Object.defineProperty({
1863
+ // Where the call is in the source: the same node, whoever reads
1864
+ // the file next (an alias, the second call on a line).
1865
+ span: { start: callNode.startIndex, end: callNode.endIndex },
1851
1866
  args,
1852
1867
  argCount: args.length,
1853
1868
  hasSpread: args.some(a => a.startsWith('...')) || unpackingArgs > 0,
@@ -1857,7 +1872,7 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
1857
1872
  isMethodCall,
1858
1873
  ...(indirectKind && { indirectKind }),
1859
1874
  ...ctx,
1860
- };
1875
+ }, 'argumentFacts', { value: argumentFacts });
1861
1876
  } catch (e) {
1862
1877
  return { args: null, argCount: 0 };
1863
1878
  }
@@ -1961,7 +1976,7 @@ function classifyArgNode(node) {
1961
1976
  }
1962
1977
  // Numbers
1963
1978
  if (t === 'number' || t === 'integer' || t === 'float' || t === 'number_literal' ||
1964
- t === 'integer_literal' || t === 'float_literal' || t === 'decimal_integer_literal' ||
1979
+ t === 'integer_literal' || t === 'int_literal' || t === 'float_literal' || t === 'decimal_integer_literal' ||
1965
1980
  t === 'hex_integer_literal' || t === 'real_literal') {
1966
1981
  return 'number_literal';
1967
1982
  }
@@ -2133,10 +2148,11 @@ function bindKeywordCall(params, analysis) {
2133
2148
  * @param {object} options - { file }
2134
2149
  * @returns {object} Verification results with mismatches
2135
2150
  */
2136
- function verify(index, name, options = {}) {
2151
+ function verify(index, name, options = {}, direct = null) {
2137
2152
  index._beginOp();
2138
2153
  try {
2139
- const { def, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
2154
+ const { def, warnings } = direct ? { def: direct.definition, warnings: [] }
2155
+ : index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
2140
2156
  if (!def) {
2141
2157
  return { found: false, function: name };
2142
2158
  }
@@ -2173,6 +2189,7 @@ function verify(index, name, options = {}) {
2173
2189
  const rawParamLists = ctorParamLists ||
2174
2190
  (callableIdentityParams?.length ? callableIdentityParams : null) ||
2175
2191
  [(arrowTypes?.paramsStructured) || def.paramsStructured || []];
2192
+ if (direct) direct.parameterLists = rawParamLists.map(stripSelf);
2176
2193
  const params = stripSelf(rawParamLists[0]);
2177
2194
  const arities = rawParamLists.map(l => {
2178
2195
  const list = stripSelf(l);
@@ -2199,11 +2216,13 @@ function verify(index, name, options = {}) {
2199
2216
  // A base the user names must be a commit: a misspelled ref would read as
2200
2217
  // a file absent at base and check nothing against the old declaration
2201
2218
  // (the target-less check already resolved its base through the diff).
2202
- if (options.base && options.checkBaseRef) assertGitCommit(def.file, baseRef);
2203
- const baseRead = startBaseRead(def.file, baseRef);
2219
+ const repoFile = index.repoPathOf ? index.repoPathOf(def.file) : def.file;
2220
+ if (options.base && options.checkBaseRef) assertGitCommit(repoFile, baseRef);
2221
+ const baseRead = direct ? null : startBaseRead(def.file, baseRef, repoFile);
2204
2222
  let sweep;
2205
2223
  try {
2206
- sweep = contractedCallerSweep(index, name, def, {
2224
+ sweep = direct ? { confirmed: direct.sites, unverified: [], account: null, rebound: [], baseChanges: [] }
2225
+ : contractedCallerSweep(index, name, def, {
2207
2226
  baseChanges: () => baseDeclarationChanges(index, name, def, baseRef, baseRead),
2208
2227
  });
2209
2228
  } finally {
@@ -2274,12 +2293,22 @@ function verify(index, name, options = {}) {
2274
2293
  };
2275
2294
  }
2276
2295
 
2296
+ // A mismatch remembers which call it is (not part of what is printed).
2297
+ const located = (record, analysis) => {
2298
+ if (analysis && analysis.span) Object.defineProperty(record, 'span', { value: analysis.span, enumerable: false });
2299
+ // `f.call(self, ..)`: the arguments start one slot later.
2300
+ if (analysis && analysis.indirectKind) {
2301
+ Object.defineProperty(record, 'indirect', { value: analysis.indirectKind, enumerable: false });
2302
+ }
2303
+ return record;
2304
+ };
2277
2305
  const verifyLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
2278
2306
  for (const call of calls) {
2279
2307
  const siteKey = `${call.file}:${call.line}:${call.calledAs || name}`;
2280
2308
  const occurrence = verifyLineSeen.get(siteKey) || 0;
2281
2309
  verifyLineSeen.set(siteKey, occurrence + 1);
2282
2310
  const analysis = analyzeCallSite(index, call, name, occurrence);
2311
+ if (direct) (direct.callShapes ||= []).push(analysis);
2283
2312
 
2284
2313
  // Carry callerFile/callerStartLine so tagInTestCase can resolve the
2285
2314
  // enclosing function in a later pass.
@@ -2340,9 +2369,10 @@ function verify(index, name, options = {}) {
2340
2369
  }
2341
2370
 
2342
2371
  // Check if arg count is valid
2343
- const countOk = hasRest
2344
- ? argCount >= minArgs
2345
- : (argCount >= minArgs && argCount <= expectedParamCount);
2372
+ const countOk = direct && !inheritedCtorOnly
2373
+ ? arities.some(arity => argCount >= arity.min && (arity.hasRest || argCount <= arity.max))
2374
+ : hasRest ? argCount >= minArgs
2375
+ : (argCount >= minArgs && argCount <= expectedParamCount);
2346
2376
  if (call.rebind && call.rebind.kind !== 'no-overload' && (countOk || call.rebind.kind !== 'count')) {
2347
2377
  // Bound the base declaration; the new one may still take it, or
2348
2378
  // another overload may: the compiler decides (fix #394).
@@ -2364,7 +2394,7 @@ function verify(index, name, options = {}) {
2364
2394
  continue;
2365
2395
  }
2366
2396
  if (!countOk || call.invalidOverload) {
2367
- mismatches.push({
2397
+ mismatches.push(located({
2368
2398
  file: call.relativePath,
2369
2399
  line: call.line,
2370
2400
  expression: call.content.trim(),
@@ -2378,7 +2408,7 @@ function verify(index, name, options = {}) {
2378
2408
  args: analysis.args,
2379
2409
  patterns: patternFlagsFrom(analysis),
2380
2410
  ...carry,
2381
- });
2411
+ }, analysis));
2382
2412
  continue;
2383
2413
  }
2384
2414
 
@@ -2399,7 +2429,7 @@ function verify(index, name, options = {}) {
2399
2429
  ...carry,
2400
2430
  });
2401
2431
  } else {
2402
- mismatches.push({
2432
+ mismatches.push(located({
2403
2433
  file: call.relativePath,
2404
2434
  line: call.line,
2405
2435
  expression: call.content.trim(),
@@ -2413,7 +2443,7 @@ function verify(index, name, options = {}) {
2413
2443
  problem: bindingProblems.join('; '),
2414
2444
  patterns: patternFlagsFrom(analysis),
2415
2445
  ...carry,
2416
- });
2446
+ }, analysis));
2417
2447
  }
2418
2448
  continue;
2419
2449
  }
@@ -2425,7 +2455,7 @@ function verify(index, name, options = {}) {
2425
2455
  ...carry,
2426
2456
  });
2427
2457
  }
2428
- clearTreeCache(index);
2458
+ if (!direct) clearTreeCache(index);
2429
2459
 
2430
2460
  // Feature A: tag each entry with `inTestCase` based on its enclosing function.
2431
2461
  // Done after the per-call loop because tagInTestCase prefers a single pass
@@ -6945,4 +6975,15 @@ function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
6945
6975
 
6946
6976
  const IDENTITY_TYPE_KINDS_FOR_CREF = new Set(['class', 'struct', 'interface', 'record', 'enum', 'trait', 'type']);
6947
6977
 
6948
- module.exports = { verify, plan, baseCounterpartOf, sitesReboundByChange, computePlanCallSites, analyzeCallSite, analyzeCallSiteAST, analyzeCallShape, classifyArgNode, findCallNode, clearTreeCache, identifyCallPatterns };
6978
+ /** Check already-resolved call sites using exactly the signature rules of verify. */
6979
+ function verifyCallSites(index, definition, sites) {
6980
+ const direct = { definition, sites };
6981
+ const result = verify(index, definition.name, {}, direct);
6982
+ Object.defineProperties(result, {
6983
+ parameterLists: { value: direct.parameterLists },
6984
+ callShapes: { value: direct.callShapes },
6985
+ });
6986
+ return result;
6987
+ }
6988
+
6989
+ module.exports = { verify, verifyCallSites, plan, baseCounterpartOf, sitesReboundByChange, computePlanCallSites, analyzeCallSite, analyzeCallSiteAST, analyzeCallShape, classifyArgNode, findCallNode, clearTreeCache, identifyCallPatterns };
@@ -147,13 +147,18 @@ function createLanguageAdapter(config) {
147
147
  // safeParse returns the tree the extractors above just built.
148
148
  if (config.traits?.reflectionApi && !parsed.reflectionSites) {
149
149
  const { safeParse } = require('./index');
150
- const { reflectionSitesInTree } = require('../core/reflection');
150
+ const { reflectionFactsInTree } = require('../core/reflection');
151
151
  // A language reading the file through its own view (C#
152
152
  // conditional compilation, fix #391) supplies that tree.
153
153
  const analysisTree = typeof languageModule.analysisTree === 'function'
154
154
  ? languageModule.analysisTree(code, parser) : safeParse(parser, code);
155
- const sites = reflectionSitesInTree(analysisTree, config.name, code);
156
- if (sites.length > 0) parsed.reflectionSites = sites;
155
+ const facts = reflectionFactsInTree(analysisTree, config.name, code, { imports });
156
+ if (facts.sites.length > 0) parsed.reflectionSites = facts.sites;
157
+ // How often the file uses the reflection and code-execution
158
+ // vocabulary: what `repo` and the completeness footer count,
159
+ // read from the index instead of from every file's text.
160
+ if (facts.reflectionUses > 0) parsed.reflectionUses = facts.reflectionUses;
161
+ if (facts.evalUses > 0) parsed.evalUses = facts.evalUses;
157
162
  }
158
163
  // Computed dispatch sites (`handlers[name]()`) of the literal
159
164
  // parse the doctor's blind-spot inventory reads (fix #436): a
@@ -3633,6 +3633,17 @@ function memberQualifiers(declarator) {
3633
3633
  return out;
3634
3634
  }
3635
3635
 
3636
+ /**
3637
+ * `virtual R f(...) = 0;`: a member function declaration whose initializer
3638
+ * is the literal 0 is pure virtual. A concrete derived class must override
3639
+ * it. (A data member `int x = 0;` has no function declarator and never
3640
+ * reaches the caller.)
3641
+ */
3642
+ function isPureVirtual(node) {
3643
+ const value = templatedDeclaration(node).childForFieldName('default_value');
3644
+ return !!value && value.type === 'number_literal' && value.text === '0';
3645
+ }
3646
+
3636
3647
  function memberFromNode(node, className, access, lines, mode) {
3637
3648
  const declarator = functionDeclarator(node);
3638
3649
  if (!declarator) return null;
@@ -3662,6 +3673,7 @@ function memberFromNode(node, className, access, lines, mode) {
3662
3673
  memberType: isConstructor ? 'constructor' : 'method',
3663
3674
  isMethod: true,
3664
3675
  isConstructor,
3676
+ ...(mode === 'cpp' && !isConstructor && isPureVirtual(node) && { requiredMember: true }),
3665
3677
  ...(mode === 'cpp' && memberQualifiers(declarator)),
3666
3678
  ...(enclosingNamespace(node) && {
3667
3679
  namespace: enclosingNamespace(node),