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/languages/rust.js CHANGED
@@ -1978,6 +1978,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
1978
1978
  sourceSpan,
1979
1979
  memberType: 'method',
1980
1980
  isMethod: true,
1981
+ // A trait method without a body is one every impl must
1982
+ // supply; one with a body is a provided default.
1983
+ ...(child.type === 'function_signature_item' && { requiredMember: true }),
1981
1984
  ...(qualifiers.async && { isAsync: true }),
1982
1985
  modifiers: [...(traitVisibility ? [traitVisibility] : []), ...(qualifiers.async ? ['async'] : [])],
1983
1986
  ...(futureReturn && { futureReturn }),
@@ -6389,6 +6392,93 @@ function rustMacroNamespaceName(node, parent, attributeNode) {
6389
6392
  return !next || next.type !== '::';
6390
6393
  }
6391
6394
 
6395
+
6396
+ // Attribute macros that declare one test per case; the names the harness
6397
+ // lists come from the expansion, not from the function.
6398
+ const RUST_CASE_ATTRIBUTES = new Set(['rstest', 'test_case', 'test_matrix', 'parameterized', 'test_casing',
6399
+ 'apply', 'quickcheck', 'proptest', 'test_resources', 'datatest']);
6400
+
6401
+ // Attributes of the language that leave a test as it is. Any other is a
6402
+ // macro that rewrites the function: what then runs is not read.
6403
+ const RUST_INERT_ATTRIBUTES = new Set(['test', 'ignore', 'should_panic', 'cfg', 'cfg_attr', 'allow', 'warn', 'deny',
6404
+ 'forbid', 'expect', 'doc', 'inline', 'cold', 'must_use', 'deprecated', 'track_caller', 'no_mangle', 'case',
6405
+ 'values', 'timeout']);
6406
+
6407
+ /**
6408
+ * The tests a Rust file declares, as the test harness lists them: a
6409
+ * function carrying #[test] or an attribute whose path ends in ::test
6410
+ * (tokio::test and the like), named by the modules it sits in. A function
6411
+ * carrying a case-generating attribute is a test whose listed names are
6412
+ * not its own. A test under a cfg other than cfg(test) is compiled only
6413
+ * sometimes.
6414
+ */
6415
+ function testUnits(fileEntry, helpers) {
6416
+ const units = [];
6417
+ const holding = new Set();
6418
+ for (const symbol of fileEntry.symbols || []) {
6419
+ if (symbol.type !== 'function') continue;
6420
+ const attributes = symbol.attributesWithArgs || [];
6421
+ // The attribute's path: `ignore = "why"` is the attribute `ignore`.
6422
+ const names = attributes.map(attribute => String(attribute.name).split(/[\s=(]/)[0]);
6423
+ const plain = names.some(name => /(^|::)test$/.test(name));
6424
+ const cases = names.some(name => RUST_CASE_ATTRIBUTES.has(name.split('::').pop()));
6425
+ if (!plain && !cases) continue;
6426
+ const chain = helpers.enclosing(symbol);
6427
+ if (chain.some(outer => outer.type !== 'module')) continue;
6428
+ const gates = chain.filter(outer => (outer.modifiers || []).includes('cfg')).length;
6429
+ const ownGate = attributes.some(attribute => /^cfg(_attr)?$/.test(String(attribute.name)) &&
6430
+ String(attribute.args || '').replace(/[\s()]/g, '') !== 'test');
6431
+ const inTestModule = (symbol.modifiers || []).includes('cfg_test_module');
6432
+ // An attribute that is none of the language's is a macro that rewrites the function.
6433
+ const rewritten = names.some(name => !/(^|::)test$/.test(name) &&
6434
+ !RUST_INERT_ATTRIBUTES.has(name) && !RUST_CASE_ATTRIBUTES.has(name.split('::').pop()) &&
6435
+ !/^(clippy|rustfmt|rustc|diagnostic)(::|_)/.test(name));
6436
+ for (const outer of chain) holding.add(outer);
6437
+ units.push({
6438
+ kind: 'test', name: symbol.name, suite: chain.map(outer => outer.name),
6439
+ line: symbol.nameLine || symbol.startLine, startLine: symbol.startLine,
6440
+ endLine: symbol.endLine ?? symbol.startLine,
6441
+ // Declared, and left out of a run unless asked for.
6442
+ ...(names.includes('ignore') && { ignored: true, skipped: true }),
6443
+ ...((ownGate || gates > (inTestModule ? 1 : 0)) && { conditional: true, notInEveryBuild: true }),
6444
+ ...(!(ownGate || gates > (inTestModule ? 1 : 0)) && rewritten && { conditional: true }),
6445
+ // What a macro makes of the function is not read: none, one
6446
+ // or several tests.
6447
+ ...(rewritten && { several: true }),
6448
+ ...(cases && { dynamic: 'generated-cases' }),
6449
+ });
6450
+ }
6451
+ for (const outer of holding) {
6452
+ units.push({
6453
+ kind: 'suite', name: outer.name, suite: helpers.enclosing(outer).map(up => up.name),
6454
+ line: outer.nameLine || outer.startLine, startLine: outer.startLine,
6455
+ endLine: outer.endLine ?? outer.startLine,
6456
+ });
6457
+ }
6458
+ // Tests a macro makes are not listed: a function a macro of the
6459
+ // project expands to (its attributes stand in the macro), and an
6460
+ // invocation among the items whose tokens name a test attribute
6461
+ // (proptest! { #[test] fn ... }).
6462
+ let more = (fileEntry.symbols || []).some(symbol => symbol.type === 'function' && symbol.macroExpansion &&
6463
+ !units.some(unit => unit.kind === 'test' && unit.startLine === symbol.startLine && unit.name === symbol.name));
6464
+ const tree = more ? null : helpers.tree();
6465
+ const namesTest = node => (node.type === 'identifier' && /(^|_)test$/.test(node.text)) || node.namedChildren.some(namesTest);
6466
+ const items = node => {
6467
+ for (const child of node.namedChildren) {
6468
+ if (more) return;
6469
+ if (child.type === 'macro_invocation') {
6470
+ if (namesTest(child)) more = true;
6471
+ } else if (child.type === 'expression_statement' && child.namedChildren[0]?.type === 'macro_invocation') {
6472
+ if (namesTest(child)) more = true;
6473
+ } else if (child.type === 'mod_item' || child.type === 'declaration_list') {
6474
+ items(child);
6475
+ }
6476
+ }
6477
+ };
6478
+ if (tree) items(tree.rootNode);
6479
+ return { units, inheriting: more };
6480
+ }
6481
+
6392
6482
  /**
6393
6483
  * Classify a Rust symbol as a runtime entry point of a specific kind.
6394
6484
  * Returns 'test' | 'main' | 'framework' | null.
@@ -6444,5 +6534,6 @@ module.exports = {
6444
6534
  findUsagesInCode,
6445
6535
  isEntryPoint,
6446
6536
  getEntryPointKind,
6537
+ testUnits,
6447
6538
  parse
6448
6539
  };
package/mcp/server.js CHANGED
@@ -29,13 +29,16 @@ const {
29
29
  FLAG_APPLICABILITY,
30
30
  REVERSE_PARAM_MAP,
31
31
  generateMcpParamSection,
32
+ mcpParamProperties,
33
+ paramValues,
34
+ GLOBAL_PARAMS,
32
35
  resolveCommand,
33
36
  suggestCommand,
34
37
  v4MigrationHint,
35
38
  formatSurfaceMessage,
36
39
  } = require('../core/registry');
37
40
  const { execute } = require('../core/execute');
38
- const { sourceRangeTarget } = require('../core/public-command');
41
+ const { sourceRangeTarget, noOpNotes } = require('../core/public-command');
39
42
  const { describeError, UcnError } = require('../core/errors');
40
43
  const { applyOutputBudget, MAX_OUTPUT_CHARS } = require('../core/output-budget');
41
44
 
@@ -124,13 +127,14 @@ const server = new StdioMcpServer({
124
127
  // TOOL HELPERS
125
128
  // ============================================================================
126
129
 
127
- function toolResult(text, command, maxChars, suffixNote, params = {}) {
130
+ function toolResult(text, command, maxChars, suffixNote, params = {}, peerText) {
128
131
  const suffix = suffixNote || '';
129
132
  const budget = applyOutputBudget(text + suffix, {
130
133
  command,
131
134
  maxChars,
132
135
  surface: 'mcp',
133
136
  params,
137
+ ...(typeof peerText === 'string' && { peerText: peerText + suffix }),
134
138
  });
135
139
  // The text block is the ONLY payload channel. Truncation facts live in the
136
140
  // text itself (the OUTPUT TRUNCATED notice carries full size, limit, and
@@ -236,107 +240,40 @@ const CONCISE_TOOL_DESCRIPTION = `Auditable AST code intelligence for JavaScript
236
240
  Use UCN for semantic questions: exact definitions, symbol-aware callers/callees, change impact, test selection, dependencies, APIs, entry points, and focused audits. Use grep/ripgrep for simple literals, error messages, configuration, filenames, Markdown, and unsupported languages. UCN search is worthwhile when AST structure, code-only filtering, or the shared agent contract matters.
237
241
 
238
242
  18 task-oriented commands:
239
- - repo: repository overview; sections=files,stats,health adds detail. Reports skipped unsupported source explicitly and hands it to grep/language-native tooling.
240
- - show: one symbol; sections=summary,callers,callees,source,dependencies,tests,types,example,related.
243
+ - repo: repository overview; sections=${paramValues('sections', 'repo').join(',')} selects detail. Reports skipped unsupported source explicitly and hands it to grep/language-native tooling.
244
+ - show: one symbol; sections=${paramValues('sections', 'show').join(',')}.
241
245
  - find/usages/search/source: locate definitions, literal-name occurrences, literal text (regex=true selects the linear-time RE2-compatible engine), AST structure, or exact source. Find activity is pinned confirmed + visible-unverified candidates; proved other-target calls are separate.
242
- - trace: direction=callees or callers; to=entrypoints follows callers to roots.
246
+ - trace: direction=${paramValues('direction', 'trace').join(' or ')}; to=${paramValues('to', 'trace').join('|')} follows callers to roots.
243
247
  - impact: symbol impact when name is set; Git-diff impact when it is omitted.
244
- - tests: static direct links by default; depth>0 includes transitive affected links. Empty results are not runtime coverage proof.
248
+ - tests: static direct links by default; depth>0 includes transitive affected links. Each test is named by its file, suites and name, with the command that runs it where the runner is proven. Empty results are not runtime coverage proof.
245
249
  - deps/api/entrypoints/endpoints: architecture and public surfaces.
246
- - check: symbol signature check when name is set; precommit check when omitted. plan previews the selected declaration plus indexed call/import/export edits.
250
+ - check: symbol signature check when name is set; precommit check when omitted (per changed symbol: BLOCKED = a break was observed, REVIEW = can reach existing code, READY = nothing existing depends on it; staged=true reads the git index, not the worktree). plan previews the selected declaration plus indexed call/import/export edits; with delete=true it assesses deleting the declaration (no-static-dependents, has-static-dependents, or review with the reason), by the rule deadcode applies to every name. A .ucn.json "kind": "application" lets exported symbols be assessed.
247
251
  - deadcode/audit_async/stacktrace: focused audits and runtime evidence. Computed dispatch is reported as a health/deletion blind spot; unknown decorators and member-assigned handlers are withheld from default dead-code claims.
248
252
 
249
- CONFIRMED carries target-identity evidence. UNVERIFIED is possible and requires review; when same-name definitions compete, show returns their stable handles once above the sites. ACCOUNT conserves observed literal-name lines; an observed-text zero never claims semantic completeness or safe deletion. Evidence weights are ordinal, not probabilities. Warnings, excluded reasons, and contract metadata remain visible when caller evidence is returned. Releases are gated on pinned real repositories: sampled callers, callees, value references, and dead-code claims are compared with ts-morph, Pyright, gopls, rust-analyzer, JDT LS, Roslyn, and clangd, and one handle's claims must agree across find, show, source, impact, tests, check, and caller trace.` + generateMcpParamSection();
253
+ CONFIRMED carries target-identity evidence. UNVERIFIED is possible and requires review; when same-name definitions compete, show returns their stable handles once above the sites. ACCOUNT conserves observed literal-name lines; an observed-text zero never claims semantic completeness or safe deletion. CONTRACT is a state: text-complete means every literal-name line of project source was classified, never that aliases, indirect calls, generated code or runtime dispatch are absent. Source UCN reads without analyzing (bundles, paths excluded in .ucn.json, oversized files, unsupported languages) is searched for each name and listed only when the name occurs there; a path that cannot be read degrades every account. repo health states the scope once. Evidence weights are ordinal, not probabilities. Warnings, excluded reasons, and contract metadata remain visible when caller evidence is returned. Releases are gated on pinned real repositories: sampled callers, callees, value references, and dead-code claims are compared with ts-morph, Pyright, gopls, rust-analyzer, JDT LS, Roslyn, and clangd, and one handle's claims must agree across find, show, source, impact, tests, check, and caller trace.` + generateMcpParamSection();
250
254
 
251
255
  // The default description is the only public contract. The former verbose
252
256
  // description is intentionally not selectable: it documented retired commands
253
257
  // and made the single MCP tool difficult for agents to parse.
254
258
  const TOOL_DESCRIPTION = CONCISE_TOOL_DESCRIPTION;
255
259
 
256
- const stringParam = (description, extra = {}) => ({ type: 'string', description, ...extra });
257
- const booleanParam = description => ({ type: 'boolean', description });
258
- const integerParam = (description, extra = {}) => ({ type: 'integer', description, ...extra });
259
- const numberParam = (description, extra = {}) => ({ type: 'number', description, ...extra });
260
-
261
- // Keep the public JSON Schema and runtime validation rules in one dependency-
262
- // free object. Unknown keys remain allowed so the handler can return typo and
263
- // applicability guidance instead of silently changing the requested answer.
260
+ // The public JSON Schema and the runtime validation rules are one object,
261
+ // written from the registry's parameter table: a type, a bound, a value list
262
+ // or a default stated here is the one the engine uses. Unknown keys remain
263
+ // allowed so the handler can return typo and applicability guidance instead
264
+ // of silently changing the requested answer.
264
265
  const INPUT_SHAPE = {
265
- command: stringParam(
266
- `UCN task command. One of: ${getMcpCommandEnum().join(', ')}.`,
267
- { enum: getMcpCommandEnum() },
268
- ),
269
- project_dir: stringParam('Non-empty absolute path, or a path relative to the MCP server process working directory, identifying the project to analyze.', { minLength: 1 }),
270
- name: stringParam('Symbol name or stable path:line:name handle. Used by show/find/usages/source/trace/impact/tests/check/plan.'),
271
- file: stringParam('File target for source/deps/api or a symbol-disambiguation filter.'),
272
- sections: stringParam('Comma-separated projection. show: summary,callers,callees,source,dependencies,tests,types,example,related. repo: summary,files,stats,health.'),
273
- exclude: stringParam('Comma-separated patterns to exclude (e.g. "test,mock,vendor")'),
274
- include_tests: booleanParam('Include test files in results (excluded by default)'),
275
- exclude_tests: booleanParam('Exclude test files: the default for entrypoints (include_tests=true lists them); usages and endpoints list test files unless exclude_tests=true.'),
276
- include_methods: booleanParam('Include method callees where receiver evidence permits. Caller-bearing views always tier method sites.'),
277
- expand_unverified: booleanParam('trace callers: follow unverified edges; downstream nodes remain marked possible, never confirmed.'),
278
- min_confidence: numberParam('Minimum ordinal evidence weight (legacy name; not a probability) for caller/callee edges', { minimum: 0, maximum: 1 }),
279
- show_confidence: booleanParam('show: resolution-evidence labels default to visible; set false to hide them. Numeric weights are ordinal, not probabilities.'),
280
- unreachable_only: booleanParam('show/impact: retain only relationships unreachable from detected entry points.'),
281
- with_types: booleanParam('show: include related type definitions.'),
282
- with_source: booleanParam('Attach exact source to find results.'),
283
- detailed: booleanParam('repo files: show symbols per file; deps: include import declarations and importers.'),
284
- exact: booleanParam('Exact name match only (no substring matching)'),
285
- in: stringParam('Only search in this directory path (e.g. "src/core")'),
286
- top: integerParam('Max results to show (default: 10). Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000 }),
287
- depth: integerParam('Max depth (default: 3 for trace, 2 for deps); expands all children. Non-negative integer.', { minimum: 0, maximum: 100 }),
288
- code_only: booleanParam('Exclude matches in comments and strings'),
289
- context: integerParam('Lines of context around each match. Non-negative integer.', { minimum: 0, maximum: 1000 }),
290
- include_exported: booleanParam('Include exported symbols in deadcode results'),
291
- include_decorated: booleanParam('Include decorated/annotated symbols in deadcode results'),
292
- calls_only: booleanParam('tests: retain direct calls and test-case matches only.'),
293
- max_lines: integerParam('source: maximum lines per declaration; raw mode discloses any truncation.', { exclusiveMinimum: 0, maximum: 1000000 }),
294
- direction: stringParam('trace: callees/callers. deps: imports/importers/both.', { enum: ['callees', 'callers', 'imports', 'importers', 'both'] }),
295
- to: stringParam('trace with direction=callers: continue toward entry points.', { enum: ['entrypoints'] }),
296
- cycles: booleanParam('deps: report circular imports instead of a file graph.'),
297
- term: stringParam('Literal search term by default. Set regex=true only for regular-expression syntax.'),
298
- regex: booleanParam('Treat search term as a regular expression (default: false/literal). Ordinary patterns run in an RE2-compatible linear-time engine; unsafe nested repetition is rejected.'),
299
- functions: booleanParam('repo stats: include per-function line counts sorted by size.'),
300
- hot: booleanParam('repo stats: include the top N most-called functions.'),
301
- diverse: booleanParam('show example: return representatives from distinct argument shapes.'),
302
- lines: booleanParam('find/usages/search/show/impact: path:line:text listings without default row caps; explicit top/limit still apply. show accepts callers/callees sections, default callers. Tier tags follow a tab; accounting and notes follow as "# " lines. MCP transport budgets still apply and preserve accounting.'),
303
- raw: booleanParam('source: code text without a header or line-number gutter, including full large classes. Notes follow as "# " lines; MCP transport budgets still apply.'),
304
- git: booleanParam('show summary: attach last-modified, author, and recent-change metadata.'),
305
- add_param: stringParam('Parameter name to add (plan command)'),
306
- remove_param: stringParam('Parameter name to remove (plan command)'),
307
- rename_to: stringParam('New name for the function, method, or type (plan command)'),
308
- default_value: stringParam('Default value for added parameter (plan command)'),
309
- stack: stringParam('The stack trace text to parse (stacktrace command)'),
310
- range: stringParam('source line range, e.g. "10-20" or "15", of the file given as file or name.'),
311
- base: stringParam('Git ref (default: HEAD). impact/check without a symbol: diff against it; check with a symbol: compare the declaration with its version there. E.g. "HEAD~3", "main", a commit SHA'),
312
- staged: booleanParam('impact/check without a symbol: analyze staged changes.'),
313
- deep: booleanParam('repo: include health and sample the ordinal resolution-evidence profile, not accuracy.'),
314
- compact: booleanParam('Compact output defaults to true for show/impact and false for usages; set the opposite value to change that command\'s presentation.'),
315
- case_sensitive: booleanParam('Case-sensitive search (default: false, case-insensitive)'),
316
- all: booleanParam('Lift formatter and result caps where supported.'),
317
- top_level: booleanParam('repo files: show only top-level functions.'),
318
- class_name: stringParam('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
319
- line: integerParam('Definition line pin. Resolves the symbol defined at this exact line (the middle component of a file:line:name handle). Disambiguates same-file same-name definitions.', { exclusiveMinimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
320
- limit: integerParam('Max results to return (default: 500; structural search: 50; usages and lines: uncapped). Caps find, usages, search, deadcode, api, and repo files. find text shows the top 5 in detail and counts the rest; limit raises both. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 1000000 }),
321
- max_files: integerParam('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000000 }),
322
- max_chars: integerParam('Max output characters, not UTF-8 bytes, including notes and preserved metadata. Broad sweep commands (repo, entrypoints, endpoints, deadcode, deps, check, audit_async) default to 3K; all other commands default to 10K. Maximum: 100K. all=true lifts formatter caps but keeps the 100K transport ceiling.', { exclusiveMinimum: 0, maximum: 100000 }),
323
- type: stringParam('Symbol type filter for structural search: function, class, call, method, type, state, field, constant, macro. Triggers index-based search.'),
324
- param: stringParam('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
325
- receiver: stringParam('Filter calls by receiver (structural search, type=call). E.g. "db", "http".'),
326
- returns: stringParam('Filter by return type (structural search). E.g. "Promise", "error".'),
327
- decorator: stringParam('Filter by decorator/annotation (structural search). E.g. "Route", "Test".'),
328
- exported: booleanParam('Only exported/public symbols (structural search).'),
329
- unused: booleanParam('Only symbols with zero callers (structural search).'),
330
- framework: stringParam('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.'),
331
- follow_symlinks: booleanParam('Follow symlinks during file discovery (default: true)'),
332
- include_bundled: booleanParam('Index *.min.js and *.bundle.js files (default: false). Source maps remain disclosed but unindexed. Bypasses the shared cache.'),
333
- bridge: booleanParam('Match server routes to client requests (endpoints command).'),
334
- server_only: booleanParam('Only list server routes (endpoints command).'),
335
- client_only: booleanParam('Only list client requests (endpoints command).'),
336
- unmatched: booleanParam('Only show unmatched routes/requests (endpoints command).'),
337
- method: stringParam('Filter by HTTP method (e.g. "GET", "POST") for endpoints.'),
338
- prefix: stringParam('Filter routes/requests by path prefix (endpoints command).'),
339
- hide_uncertain: booleanParam('Hide uncertain (interpolated-path) bridges (endpoints command).'),
266
+ command: {
267
+ type: 'string',
268
+ description: `UCN task command. One of: ${getMcpCommandEnum().join(', ')}.`,
269
+ enum: getMcpCommandEnum(),
270
+ },
271
+ project_dir: {
272
+ type: 'string',
273
+ description: 'Non-empty absolute path, or a path relative to the MCP server process working directory, identifying the project to analyze.',
274
+ minLength: 1,
275
+ },
276
+ ...mcpParamProperties(),
340
277
  };
341
278
 
342
279
  const INPUT_SCHEMA = {
@@ -415,7 +352,7 @@ server.registerTool(
415
352
  if (applicable) {
416
353
  // Truly global options — apply to all commands (build/display control).
417
354
  // Command-specific params (name, term, stack, range, etc.) are in FLAG_APPLICABILITY.
418
- const coreParams = new Set(['maxChars', 'maxFiles', 'followSymlinks', 'includeBundled']);
355
+ const coreParams = new Set(GLOBAL_PARAMS);
419
356
  for (const key of Object.keys(ep)) {
420
357
  if (coreParams.has(key)) continue;
421
358
  if (!applicable.includes(key) && ep[key] !== undefined &&
@@ -436,34 +373,23 @@ server.registerTool(
436
373
  if (strippedParams.length > 0) {
437
374
  noteParts.push(`${strippedParams.join(', ')} ignored (not applicable to ${command}).`);
438
375
  }
439
- if (ep.includeMethods && ['impact', 'trace', 'tests', 'check'].includes(canonicalCommand)) {
440
- noteParts.push(`include_methods=true has no effect on '${command}' — method calls are always tiered by receiver evidence.`);
441
- }
376
+ for (const note of noOpNotes(canonicalCommand, ep)) noteParts.push(formatSurfaceMessage(note, 'mcp'));
442
377
  if (projectScopeNote) noteParts.push(projectScopeNote);
443
378
  noteParts.push(...unknownParamNotes);
444
379
  const strippedNote = noteParts.length > 0
445
380
  ? '\n\n' + noteParts.map(part => `Note: ${part}`).join('\n')
446
381
  : '';
382
+ // In a line listing everything that is not a record is a "# " line.
383
+ const listingNote = noteParts.length > 0
384
+ ? '\n' + noteParts.map(part => `# Note: ${part}`).join('\n')
385
+ : '';
447
386
 
448
387
  // Wrap toolResult to auto-inject command + maxChars + stripping note
449
- const tr = (text) => toolResult(text, command, maxChars, strippedNote, ep);
388
+ const tr = (text, peerText) => toolResult(text, command, maxChars, ep.lines ? listingNote : strippedNote, ep, peerText);
450
389
  // Wrap toolError to include stripping note on error paths too
451
390
  const te = strippedNote
452
391
  ? (msg) => toolError(msg + strippedNote)
453
392
  : toolError;
454
- // Translate CLI flag syntax in execute-layer notes to MCP param
455
- // syntax — the deadcode exported/decorated hints were already
456
- // param-styled, but limit/depth/truncation notes leaked
457
- // '--limit N' / '--max-files N' / '--all' at this surface.
458
- const mn = (note) => note && note
459
- .replace(/--limit N\b/g, 'limit=<n>')
460
- .replace(/--max-files N\b/g, 'max_files=<n>')
461
- .replace(/--depth=N\b/g, 'depth=<n>')
462
- .replace(/--detailed\b/g, 'detailed=true')
463
- .replace(/--all\b/g, 'all=true')
464
- .replace(/--expand-unverified\b/g, 'expand_unverified=true')
465
- .replace(/--(\w[\w-]*)/g, (_m, f) => f.replace(/-/g, '_'));
466
-
467
393
  let index = null; // Track for post-command cache save
468
394
  try {
469
395
  // Public v5 has one MCP dispatch path. The canonical registry,
@@ -514,7 +440,6 @@ server.registerTool(
514
440
 
515
441
  const presentationExecution = {
516
442
  ...execution,
517
- note: mn(execution.note),
518
443
  surface: 'mcp',
519
444
  };
520
445
  return tr(output.formatPublicText(
@@ -522,6 +447,11 @@ server.registerTool(
522
447
  execution.result,
523
448
  ep,
524
449
  presentationExecution,
450
+ ), ep.lines || ep.raw ? undefined : output.formatPublicText(
451
+ canonicalCommand,
452
+ execution.result,
453
+ ep,
454
+ { ...execution, surface: 'cli' },
525
455
  ));
526
456
 
527
457
  } catch (e) {
@@ -540,6 +470,10 @@ server.registerTool(
540
470
  try { index.saveCache(); } catch (_) { /* best-effort */ }
541
471
  index.callsCacheDirty = false;
542
472
  }
473
+ // A HOT ranking this request computed, for the next session.
474
+ if (index && !ep.includeBundled && !ep.maxFiles && index.hotSummaryDirty) {
475
+ try { index.saveHotSummaries(); } catch (_) { /* best-effort */ }
476
+ }
543
477
  }
544
478
  }
545
479
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "5.5.2",
3
+ "version": "5.6.0",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
6
6
  "main": "index.js",
@@ -9,8 +9,8 @@
9
9
  "ucn-mcp": "mcp/server.js"
10
10
  },
11
11
  "scripts": {
12
- "version": "node scripts/sync-server-version.js && git add server.json",
13
- "test": "node --test test/audit-5.4.0.test.js test/audit-5.3.8.test.js test/audit-5.3.7.test.js test/audit-5.3.6.test.js test/evidence-provenance.test.js test/provenance-unit.test.js test/provenance-scope.test.js test/shell-output.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
12
+ "version": "node scripts/sync-server-version.js && node scripts/changelog.js && git add server.json CHANGELOG.md",
13
+ "test": "node --test test/audit-5.4.0.test.js test/audit-5.3.8.test.js test/audit-5.3.7.test.js test/audit-5.3.6.test.js test/evidence-provenance.test.js test/evidence-bands.test.js test/constructed-receiver.test.js test/provenance-unit.test.js test/provenance-scope.test.js test/shell-output.test.js test/param-table.test.js test/changelog.test.js test/repo-facts.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/outcome-mutations.test.js test/source-universe.test.js test/deletion.test.js test/check-change-kinds.test.js test/test-units.test.js test/check-staged.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
14
14
  "benchmark:agent": "node test/agent-public-surface-benchmark.js",
15
15
  "benchmark:agent:gate": "node test/agent-public-surface-benchmark.js --gate",
16
16
  "benchmark:agent:legacy": "node test/agent-understanding-benchmark.js",
@@ -21,7 +21,7 @@
21
21
  "eval:host-calibration": "node eval/lib/host-calibration.js --pin",
22
22
  "eval:consistency": "node eval/run-consistency-eval.js --project .",
23
23
  "profile:callers": "node eval/profile-callers.js",
24
- "lint": "eslint core/ cli/ mcp/ languages/ eval/run-*.js eval/*-gate-policy.js eval/outcome-policy.js eval/profile-callers.js eval/oracles/*.js",
24
+ "lint": "eslint core/ cli/ mcp/ languages/ eval/run-*.js eval/*-gate-policy.js eval/outcome-policy.js eval/outcome-mutations.js eval/deletion-matrix-cases.js eval/check-matrix-cases.js eval/test-unit-matrix-cases.js eval/profile-callers.js eval/oracles/*.js",
25
25
  "verify": "npm run lint && npm test",
26
26
  "trust:gate:semantic": "node eval/run-oracle-eval.js --release --min-precision 0.98 --max-unscored-ratio 0.10",
27
27
  "trust:gate:deadcode": "node eval/run-deadcode-eval.js --release --sample 100 --arm default",
@@ -30,8 +30,13 @@
30
30
  "trust:gate:consistency": "node eval/run-consistency-eval.js --release --sample 40",
31
31
  "trust:gate:agent": "node test/agent-public-surface-benchmark.js --gate",
32
32
  "trust:gate:fast": "node --expose-gc eval/run-performance-gate.js --repo preact-signals,httpx --queries 20 && node eval/run-oracle-eval.js --repo preact-signals,httpx --min-precision 0.98 && node eval/run-consistency-eval.js --repo preact-signals,httpx --sample 20 --gate",
33
- "trust:gate": "npm run trust:gate:performance && npm run trust:gate:semantic && npm run trust:gate:deadcode && npm run trust:gate:consistency && npm run trust:gate:agent",
33
+ "trust:gate:matrices": "node eval/run-deletion-matrix.js && node eval/run-check-matrix.js && node eval/run-test-unit-matrix.js",
34
+ "trust:gate:outcome": "node eval/run-outcome-eval.js --gate",
35
+ "trust:gate": "npm run trust:gate:performance && npm run trust:gate:semantic && npm run trust:gate:deadcode && npm run trust:gate:consistency && npm run trust:gate:agent && npm run trust:gate:matrices && npm run trust:gate:outcome",
34
36
  "eval:outcome": "node eval/run-outcome-eval.js",
37
+ "eval:deletion-matrix": "node eval/run-deletion-matrix.js",
38
+ "eval:check-matrix": "node eval/run-check-matrix.js",
39
+ "eval:test-unit-matrix": "node eval/run-test-unit-matrix.js",
35
40
  "eval:census": "node eval/run-unverified-census.js"
36
41
  },
37
42
  "keywords": [
@@ -87,7 +92,7 @@
87
92
  "dependencies": {
88
93
  "node-addon-api": "8.9.2",
89
94
  "node-gyp-build": "4.8.4",
90
- "re2js": "2.8.6",
95
+ "re2js": "2.8.7",
91
96
  "ret": "0.5.0",
92
97
  "safe-regex2": "5.1.1",
93
98
  "tree-sitter": "0.21.1",