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/.claude/skills/ucn/SKILL.md +4 -2
- package/.claude/skills/ucn/references/commands.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -8
- package/core/analysis.js +3 -3
- package/core/cache.js +3 -1
- package/core/call-fit.js +130 -0
- package/core/callers.js +1 -1
- package/core/capture.js +40 -8
- package/core/change-kinds.js +221 -2
- package/core/check.js +48 -15
- package/core/command-contracts.js +2 -2
- package/core/deadcode.js +94 -23
- package/core/deletion.js +29 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/index-ir.js +2 -2
- package/core/ir.js +1 -1
- package/core/outgoing-check.js +116 -0
- package/core/output/check.js +5 -2
- package/core/output/reporting.js +4 -0
- package/core/output/search.js +3 -0
- package/core/output/tracing.js +1 -1
- package/core/output-budget.js +29 -11
- package/core/python-binding-identity.js +707 -0
- package/core/reflection.js +99 -10
- package/core/registry.js +2 -2
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +61 -38
- package/core/test-units.js +116 -42
- package/core/tracing.js +193 -635
- package/core/verify.js +37 -12
- package/languages/csharp.js +4 -4
- package/languages/go.js +1 -1
- package/languages/index.js +45 -1
- package/languages/java.js +4 -4
- package/languages/javascript.js +40 -6
- package/languages/python.js +49 -15
- package/languages/rust.js +126 -112
- package/languages/static-js.js +88 -0
- package/mcp/server.js +1 -1
- package/package.json +4 -3
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
|
-
|
|
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
|
|
610
|
+
function outerAttributeNodes(node) {
|
|
609
611
|
const attributes = [];
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (!
|
|
657
|
-
|
|
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
|
|
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
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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 =
|
|
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.
|
|
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
|
-
"
|
|
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",
|