ucn 5.6.0 → 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.
package/languages/rust.js CHANGED
@@ -287,7 +287,10 @@ const RUST_TRANSPARENT_FN_ATTRIBUTES = new Set([
287
287
  ]);
288
288
 
289
289
  function rustAttributePath(node) {
290
- return node && ['identifier', 'scoped_identifier'].includes(node.type) ? node.text.replace(/\s+/g, '') : null;
290
+ // Raw identifiers change the spelling, not the attribute's identity.
291
+ return node && ['identifier', 'scoped_identifier'].includes(node.type)
292
+ ? node.text.replace(/\s+/g, '').split('::').map(part => part.startsWith('r#') ? part.slice(2) : part).join('::')
293
+ : null;
291
294
  }
292
295
 
293
296
  /** Outer attribute paths of an item; `cfg_attr(pred, a, b)` yields a, b. */
@@ -314,7 +317,7 @@ function rustOuterAttributePaths(itemNode) {
314
317
  continue;
315
318
  }
316
319
  if (group === 0 || !expectPath) continue;
317
- if (token.type === 'identifier' || token.type === '::') current += token.text;
320
+ if (token.type === 'identifier' || token.type === '::') current += rustAttributePath(token) || token.text;
318
321
  else { if (current) paths.push(current); current = ''; expectPath = false; }
319
322
  }
320
323
  if (current) paths.push(current);
@@ -602,93 +605,42 @@ function extractVisibility(text) {
602
605
  /**
603
606
  * Extract attributes from a function node (e.g., #[test], #[tokio::main])
604
607
  * @param {Node} node - AST node
605
- * @param {string} code - Source code
606
608
  * @returns {string[]} Array of attribute names
607
609
  */
608
- function extractAttributes(node, codeOrLines) {
610
+ function outerAttributeNodes(node) {
609
611
  const attributes = [];
610
- const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
611
-
612
- // Look at lines before the function for attributes
613
- const startLine = node.startPosition.row;
614
- for (let i = startLine - 1; i >= 0 && i >= startLine - 5; i--) {
615
- const line = lines[i]?.trim();
616
- if (!line) break;
617
- if (line.startsWith('#[')) {
618
- // Extract attribute name (e.g., #[test] -> test, #[tokio::main] -> tokio::main)
619
- const match = line.match(/#\[([^\]]+)\]/);
620
- if (match) {
621
- const attrContent = match[1];
622
- // Get just the attribute name (without arguments)
623
- const attrName = attrContent.split('(')[0].trim();
624
- // Skip compiler hint attributes that aren't semantically meaningful for display
625
- const SKIP_ATTRS = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
626
- if (!SKIP_ATTRS.has(attrName)) {
627
- attributes.push(attrName);
628
- }
629
- }
630
- } else if (!line.startsWith('//')) {
631
- // Stop at non-comment, non-attribute lines
632
- break;
633
- }
612
+ for (let sibling = node.previousNamedSibling; sibling; sibling = sibling.previousNamedSibling) {
613
+ if (sibling.type === 'line_comment' || sibling.type === 'block_comment') continue;
614
+ if (sibling.type !== 'attribute_item') break;
615
+ const attribute = sibling.namedChildren.find(child => child.type === 'attribute');
616
+ if (attribute) attributes.unshift(attribute);
634
617
  }
635
-
636
618
  return attributes;
637
619
  }
638
620
 
639
- /**
640
- * Extract attributes WITH their argument tokens (for routing decorator detection).
641
- * Returns array of { name, args: rawArgString } objects.
642
- * #[get("/users")] → [{ name: 'get', args: '"/users"' }]
643
- * #[tokio::main] → [{ name: 'tokio::main', args: null }]
644
- *
645
- * @param {Node} node - Function AST node
646
- * @param {string|string[]} codeOrLines - Source code or pre-split lines
647
- * @returns {Array<{name: string, args: string|null}>}
648
- */
649
- function extractAttributesWithArgs(node, codeOrLines) {
650
- const result = [];
651
- const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
652
-
653
- const startLine = node.startPosition.row;
654
- for (let i = startLine - 1; i >= 0 && i >= startLine - 5; i--) {
655
- const line = lines[i]?.trim();
656
- if (!line) break;
657
- if (line.startsWith('#[')) {
658
- // Match #[name(...args...)] or #[name]
659
- // Need to handle nested parens; use a simple bracket-matching approach.
660
- // (The line is trimmed: only a line ending in ']' can match; a
661
- // macro expansion's generated line can be very long.)
662
- const m = line.charCodeAt(line.length - 1) === 93 ? line.match(/^#\[(.+)\]\s*$/) : null;
663
- if (m) {
664
- const attrContent = m[1];
665
- const parenIdx = attrContent.indexOf('(');
666
- if (parenIdx === -1) {
667
- result.unshift({ name: attrContent.trim(), args: null });
668
- } else {
669
- const name = attrContent.slice(0, parenIdx).trim();
670
- // Extract content within outer parens (find matching close)
671
- let depth = 0;
672
- let endIdx = -1;
673
- for (let k = parenIdx; k < attrContent.length; k++) {
674
- const ch = attrContent[k];
675
- if (ch === '(') depth++;
676
- else if (ch === ')') {
677
- depth--;
678
- if (depth === 0) { endIdx = k; break; }
679
- }
680
- }
681
- const args = endIdx > parenIdx
682
- ? attrContent.slice(parenIdx + 1, endIdx).trim()
683
- : attrContent.slice(parenIdx + 1).trim();
684
- result.unshift({ name, args });
685
- }
686
- }
687
- } else if (!line.startsWith('//')) {
688
- break;
621
+ function extractAttributes(node) {
622
+ const skip = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
623
+ // Preserve the established nearest-first modifier ordering. Ownership
624
+ // comes from adjacent AST siblings, never preceding source lines.
625
+ return extractAttributesWithArgs(node).reverse().map(attribute => attribute.name)
626
+ .filter(name => !skip.has(name));
627
+ }
628
+
629
+ /** Written outer attributes attached to this item, in source order. */
630
+ function extractAttributesWithArgs(node) {
631
+ const attributes = [];
632
+ for (const attribute of outerAttributeNodes(node)) {
633
+ let name = rustAttributePath(attribute.namedChild(0));
634
+ if (!name) continue;
635
+ const argumentsNode = attribute.childForFieldName('arguments');
636
+ // The existing attribute record keeps name-value attributes whole
637
+ // (notably path = "module.rs", which module resolution consumes).
638
+ if (!argumentsNode && attribute.children.some(child => child.type === '=')) {
639
+ name += attribute.text.slice(attribute.namedChild(0).endIndex - attribute.startIndex).trimEnd();
689
640
  }
641
+ attributes.push({ name, args: argumentsNode ? argumentsNode.text.slice(1, -1).trim() : null });
690
642
  }
691
- return result;
643
+ return attributes;
692
644
  }
693
645
 
694
646
  // --- Module-scope constants for state object detection ---
@@ -701,30 +653,14 @@ function extractAttributesWithArgs(node, codeOrLines) {
701
653
  * functions inside a `mod tests` block as test entry points even when
702
654
  * they don't carry a direct `#[test]` attribute (BUG-CY).
703
655
  */
704
- function _isInsideCfgTestModule(node, lines) {
705
- let parent = node.parent;
706
- while (parent) {
707
- if (parent.type === 'mod_item') {
708
- const startRow = parent.startPosition.row;
709
- // Look at preceding lines for #[cfg(test)] or #[cfg(any(test,...))] / #[cfg(all(...,test,...))]
710
- for (let i = startRow - 1; i >= 0 && i >= startRow - 5; i--) {
711
- const line = lines[i]?.trim();
712
- if (!line) break;
713
- if (line.startsWith('#[')) {
714
- // Match #[cfg(...)] forms that include a `test` predicate.
715
- // Conservatively look for the literal token `test` inside the cfg(...) args.
716
- const m = line.match(/#\[\s*cfg\s*\(([^\]]*)\)\s*\]/);
717
- if (m) {
718
- const args = m[1];
719
- // Word-boundary match for `test` to avoid matching e.g. `testing_module`.
720
- if (/\btest\b/.test(args)) return true;
721
- }
722
- } else if (!line.startsWith('//')) {
723
- break;
724
- }
725
- }
726
- }
727
- parent = parent.parent;
656
+ function _isInsideCfgTestModule(node) {
657
+ const namesTest = part => part.type === 'identifier' && rustAttributePath(part) === 'test' ||
658
+ part.namedChildren.some(namesTest);
659
+ for (let parent = node.parent; parent; parent = parent.parent) {
660
+ if (parent.type !== 'mod_item') continue;
661
+ if (outerAttributeNodes(parent).some(attribute =>
662
+ rustAttributePath(attribute.namedChild(0)) === 'cfg' &&
663
+ attribute.childForFieldName('arguments')?.namedChildren.some(namesTest))) return true;
728
664
  }
729
665
  return false;
730
666
  }
@@ -6404,6 +6340,67 @@ const RUST_INERT_ATTRIBUTES = new Set(['test', 'ignore', 'should_panic', 'cfg',
6404
6340
  'forbid', 'expect', 'doc', 'inline', 'cold', 'must_use', 'deprecated', 'track_caller', 'no_mangle', 'case',
6405
6341
  'values', 'timeout']);
6406
6342
 
6343
+ /** The comma-separated syntax groups of an attribute's token tree. */
6344
+ function rustAttributeGroups(argumentsNode) {
6345
+ const groups = [[]];
6346
+ for (const child of argumentsNode?.children || []) {
6347
+ if (['(', ')', '[', ']', '{', '}', 'line_comment', 'block_comment'].includes(child.type)) continue;
6348
+ if (child.type === ',') groups.push([]);
6349
+ else groups[groups.length - 1].push(child);
6350
+ }
6351
+ return groups.filter(group => group.length > 0);
6352
+ }
6353
+
6354
+ /** An attribute written inside cfg_attr is token-tree syntax, not an attribute node. */
6355
+ function rustNestedAttribute(nodes) {
6356
+ let name = '';
6357
+ let args = null;
6358
+ for (const node of nodes) {
6359
+ if (node.type === 'token_tree') { args = node; break; }
6360
+ if (['identifier', 'scoped_identifier', '::'].includes(node.type)) name += rustAttributePath(node) || node.text;
6361
+ else break;
6362
+ }
6363
+ return { name, args };
6364
+ }
6365
+
6366
+ // Only configuration facts intrinsic to a test build are decided here.
6367
+ // Feature, target and user cfg values remain unknown, including their negations.
6368
+ function rustTestCfgValue(nodes) {
6369
+ if (nodes.length === 1 && nodes[0].type === 'identifier' && rustAttributePath(nodes[0]) === 'test') return true;
6370
+ const { name, args } = rustNestedAttribute(nodes);
6371
+ if (!args) return null;
6372
+ const values = rustAttributeGroups(args).map(rustTestCfgValue);
6373
+ if (name === 'all') return values.includes(false) ? false : values.includes(null) ? null : true;
6374
+ if (name === 'any') return values.includes(true) ? true : values.includes(null) ? null : false;
6375
+ if (name === 'not' && values.length === 1) return values[0] === null ? null : !values[0];
6376
+ return null;
6377
+ }
6378
+
6379
+ /** Possible attributes cfg_attr adds to this exact declaration, never a neighbor. */
6380
+ function rustConditionalTestAttributes(node) {
6381
+ const attributes = [];
6382
+ const expand = (name, args, active) => {
6383
+ if (active === false) return;
6384
+ if (name !== 'cfg_attr') {
6385
+ if (name) attributes.push({ name, certain: active === true });
6386
+ return;
6387
+ }
6388
+ const [condition, ...nested] = rustAttributeGroups(args);
6389
+ const value = condition ? rustTestCfgValue(condition) : null;
6390
+ const enabled = value === false ? false : active === null || value === null ? null : true;
6391
+ for (const group of nested) {
6392
+ const attribute = rustNestedAttribute(group);
6393
+ expand(attribute.name, attribute.args, enabled);
6394
+ }
6395
+ };
6396
+ for (const attribute of outerAttributeNodes(node)) {
6397
+ if (rustAttributePath(attribute.namedChild(0)) === 'cfg_attr') {
6398
+ expand('cfg_attr', attribute.childForFieldName('arguments'), true);
6399
+ }
6400
+ }
6401
+ return attributes;
6402
+ }
6403
+
6407
6404
  /**
6408
6405
  * The tests a Rust file declares, as the test harness lists them: a
6409
6406
  * function carrying #[test] or an attribute whose path ends in ::test
@@ -6415,11 +6412,26 @@ const RUST_INERT_ATTRIBUTES = new Set(['test', 'ignore', 'should_panic', 'cfg',
6415
6412
  function testUnits(fileEntry, helpers) {
6416
6413
  const units = [];
6417
6414
  const holding = new Set();
6415
+ const attributesOf = symbol => symbol.attributesWithArgs || [];
6416
+ let conditionalAttributes;
6417
+ const conditionalAttributesOf = symbol => {
6418
+ if (!attributesOf(symbol).some(attribute => attribute.name === 'cfg_attr')) return [];
6419
+ if (!conditionalAttributes) {
6420
+ conditionalAttributes = new Map();
6421
+ for (const node of helpers.tree()?.rootNode.descendantsOfType('function_item') || []) {
6422
+ conditionalAttributes.set(node.startIndex, rustConditionalTestAttributes(node));
6423
+ }
6424
+ }
6425
+ return conditionalAttributes.get(symbol.sourceSpan?.start) || [];
6426
+ };
6418
6427
  for (const symbol of fileEntry.symbols || []) {
6419
- if (symbol.type !== 'function') continue;
6420
- const attributes = symbol.attributesWithArgs || [];
6428
+ if (symbol.type !== 'function' || symbol.macroExpansion) continue;
6429
+ const attributes = attributesOf(symbol);
6421
6430
  // The attribute's path: `ignore = "why"` is the attribute `ignore`.
6422
- const names = attributes.map(attribute => String(attribute.name).split(/[\s=(]/)[0]);
6431
+ const directNames = attributes.map(attribute => String(attribute.name).split(/[\s=(]/)[0]);
6432
+ const conditional = conditionalAttributesOf(symbol);
6433
+ const names = [...directNames, ...conditional.map(attribute => attribute.name)];
6434
+ const ignored = directNames.includes('ignore') || conditional.some(attribute => attribute.name === 'ignore' && attribute.certain);
6423
6435
  const plain = names.some(name => /(^|::)test$/.test(name));
6424
6436
  const cases = names.some(name => RUST_CASE_ATTRIBUTES.has(name.split('::').pop()));
6425
6437
  if (!plain && !cases) continue;
@@ -6428,6 +6440,8 @@ function testUnits(fileEntry, helpers) {
6428
6440
  const gates = chain.filter(outer => (outer.modifiers || []).includes('cfg')).length;
6429
6441
  const ownGate = attributes.some(attribute => /^cfg(_attr)?$/.test(String(attribute.name)) &&
6430
6442
  String(attribute.args || '').replace(/[\s()]/g, '') !== 'test');
6443
+ const outerGate = chain.some(outer => attributesOf(outer).some(attribute =>
6444
+ /^cfg(_attr)?$/.test(String(attribute.name)) && String(attribute.args || '').replace(/[\s()]/g, '') !== 'test'));
6431
6445
  const inTestModule = (symbol.modifiers || []).includes('cfg_test_module');
6432
6446
  // An attribute that is none of the language's is a macro that rewrites the function.
6433
6447
  const rewritten = names.some(name => !/(^|::)test$/.test(name) &&
@@ -6437,11 +6451,11 @@ function testUnits(fileEntry, helpers) {
6437
6451
  units.push({
6438
6452
  kind: 'test', name: symbol.name, suite: chain.map(outer => outer.name),
6439
6453
  line: symbol.nameLine || symbol.startLine, startLine: symbol.startLine,
6440
- endLine: symbol.endLine ?? symbol.startLine,
6454
+ endLine: symbol.endLine ?? symbol.startLine, sourceSpan: symbol.sourceSpan,
6441
6455
  // 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 }),
6456
+ ...(ignored && { ignored: true, skipped: true }),
6457
+ ...((ownGate || outerGate || gates > (inTestModule ? 1 : 0)) && { conditional: true, notInEveryBuild: true }),
6458
+ ...(!(ownGate || outerGate || gates > (inTestModule ? 1 : 0)) && rewritten && { conditional: true }),
6445
6459
  // What a macro makes of the function is not read: none, one
6446
6460
  // or several tests.
6447
6461
  ...(rewritten && { several: true }),
@@ -6452,7 +6466,7 @@ function testUnits(fileEntry, helpers) {
6452
6466
  units.push({
6453
6467
  kind: 'suite', name: outer.name, suite: helpers.enclosing(outer).map(up => up.name),
6454
6468
  line: outer.nameLine || outer.startLine, startLine: outer.startLine,
6455
- endLine: outer.endLine ?? outer.startLine,
6469
+ endLine: outer.endLine ?? outer.startLine, sourceSpan: outer.sourceSpan,
6456
6470
  });
6457
6471
  }
6458
6472
  // Tests a macro makes are not listed: a function a macro of the
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { sameNode } = require('./utils');
4
+ const SCOPES = new Set(['program', 'statement_block', 'switch_body', 'for_statement', 'for_in_statement',
5
+ 'function_declaration', 'generator_function_declaration', 'function_expression', 'generator_function',
6
+ 'arrow_function', 'method_definition', 'catch_clause', 'class', 'class_declaration']);
7
+ const FUNCTIONS = new Set(['function_declaration', 'generator_function_declaration', 'function_expression',
8
+ 'generator_function', 'arrow_function', 'method_definition']);
9
+
10
+ function hasName(pattern, name) {
11
+ return !!pattern && (pattern.text === name ||
12
+ pattern.descendantsOfType(['identifier', 'shorthand_property_identifier_pattern']).some(id => id.text === name));
13
+ }
14
+
15
+ /** Decode a JavaScript literal without evaluating project code. */
16
+ function jsString(node) {
17
+ if (!node || !['string', 'template_string'].includes(node.type) ||
18
+ node.namedChildren.some(child => child.type === 'template_substitution')) return null;
19
+ const escapes = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', 0: '\0' };
20
+ return node.text.slice(1, -1).replace(/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0-3][0-7]{0,2}|[4-7][0-7]?|\r?\n|[\s\S])/g, (whole, escape) => {
21
+ if (/^[0-7]+$/.test(escape)) return String.fromCharCode(parseInt(escape, 8));
22
+ if (escape[0] === 'u' || escape[0] === 'x') {
23
+ const code = parseInt(escape.replace(/^[ux]\{?|\}$/g, ''), 16);
24
+ return code <= 0x10ffff ? String.fromCodePoint(code) : whole;
25
+ }
26
+ if (escape === '\n' || escape === '\r\n') return '';
27
+ return Object.hasOwn(escapes, escape) ? escapes[escape] : escape;
28
+ });
29
+ }
30
+
31
+ /** One stable const binding, with block and parameter shadowing. */
32
+ function jsConstValue(node, constructedMutable = false) {
33
+ if (node?.type !== 'identifier') return null;
34
+ const name = node.text;
35
+ for (let scope = node.parent; scope; scope = scope.parent) {
36
+ if (scope.type === 'with_statement') return null;
37
+ if (!SCOPES.has(scope.type)) continue;
38
+ // Named function/generator/class expressions bind their own name in
39
+ // their body; it never denotes an outer const of the same spelling.
40
+ if (['function_expression', 'generator_function', 'function_declaration',
41
+ 'generator_function_declaration', 'class', 'class_declaration'].includes(scope.type) &&
42
+ scope.childForFieldName('name')?.text === name) return null;
43
+ const params = scope.childForFieldName('parameters') || scope.childForFieldName('parameter');
44
+ if (hasName(params, name) || (scope.type === 'for_in_statement' && hasName(scope.childForFieldName('left'), name))) return null;
45
+ if (FUNCTIONS.has(scope.type)) {
46
+ let shadow = false;
47
+ const scanVars = current => {
48
+ for (const child of current.namedChildren) {
49
+ if (FUNCTIONS.has(child.type)) continue;
50
+ if (child.type === 'variable_declarator' && child.parent?.type === 'variable_declaration' &&
51
+ hasName(child.childForFieldName('name'), name)) shadow = true;
52
+ scanVars(child);
53
+ }
54
+ };
55
+ scanVars(scope);
56
+ if (shadow) return null;
57
+ }
58
+ const bindings = [];
59
+ const walk = current => {
60
+ for (const child of current.namedChildren) {
61
+ if (['function_declaration', 'generator_function_declaration', 'class_declaration'].includes(child.type) && child.childForFieldName('name')?.text === name) bindings.push(child);
62
+ if (SCOPES.has(child.type) && !sameNode(child, scope)) continue;
63
+ if (child.type === 'variable_declarator' && hasName(child.childForFieldName('name'), name)) bindings.push(child);
64
+ else if (child.type === 'import_statement' && hasName(child, name)) bindings.push(child);
65
+ walk(child);
66
+ }
67
+ };
68
+ walk(scope);
69
+ if (bindings.length === 0) continue;
70
+ if (bindings.length !== 1) return null;
71
+ const binding = bindings[0];
72
+ if (binding.type !== 'variable_declarator' || binding.childForFieldName('name')?.type !== 'identifier' ||
73
+ binding.endIndex > node.startIndex) return null;
74
+ const value = binding.childForFieldName('value');
75
+ if (binding.parent?.type === 'lexical_declaration' && binding.parent.child(0)?.type === 'const') return value;
76
+ if (!constructedMutable || value?.type !== 'new_expression') return null;
77
+ const writes = node.tree.rootNode.descendantsOfType(['assignment_expression', 'augmented_assignment_expression', 'update_expression']);
78
+ if (writes.some(write => hasName(write.childForFieldName('left') || write.childForFieldName('argument'), name))) return null;
79
+ return value;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function jsStaticKey(node) {
85
+ return jsString(node) ?? jsString(jsConstValue(node));
86
+ }
87
+
88
+ module.exports = { jsString, jsConstValue, jsStaticKey };
package/mcp/server.js CHANGED
@@ -247,7 +247,7 @@ Use UCN for semantic questions: exact definitions, symbol-aware callers/callees,
247
247
  - impact: symbol impact when name is set; Git-diff impact when it is omitted.
248
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.
249
249
  - deps/api/entrypoints/endpoints: architecture and public surfaces.
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.
250
+ - check: symbol signature check when name is set; precommit check when omitted (per changed symbol: BLOCKED = a break was observed, REVIEW = change exposure or a changed call cannot be settled, READY = no unresolved exposure in the modeled checks, not compiler or runtime proof; 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.
251
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.
252
252
 
253
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "5.6.0",
3
+ "version": "5.6.1",
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",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
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",
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/check-outgoing.test.js test/check-performance.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,8 @@
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/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",
24
+ "profile:check": "node eval/profile-check.js",
25
+ "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/check-outgoing-cases.js eval/test-unit-matrix-cases.js eval/profile-callers.js eval/profile-check.js eval/oracles/*.js",
25
26
  "verify": "npm run lint && npm test",
26
27
  "trust:gate:semantic": "node eval/run-oracle-eval.js --release --min-precision 0.98 --max-unscored-ratio 0.10",
27
28
  "trust:gate:deadcode": "node eval/run-deadcode-eval.js --release --sample 100 --arm default",