ucn 5.2.2 → 5.3.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.
- package/.claude/skills/ucn/SKILL.md +49 -3
- package/.claude/skills/ucn/references/commands.md +3 -1
- package/README.md +158 -533
- package/cli/index.js +71 -10
- package/core/cache.js +22 -13
- package/core/callers.js +51 -52
- package/core/execute.js +24 -6
- package/core/graph.js +167 -35
- package/core/index-ir.js +12 -9
- package/core/output/graph.js +60 -11
- package/core/output/lines.js +259 -0
- package/core/output/public.js +15 -0
- package/core/output/reporting.js +9 -2
- package/core/output-budget.js +7 -4
- package/core/project.js +15 -2
- package/core/registry.js +7 -6
- package/core/reporting.js +159 -14
- package/languages/javascript.js +213 -6
- package/languages/python.js +115 -12
- package/mcp/server.js +3 -1
- package/package.json +2 -2
- package/assets/demo.svg +0 -31
package/languages/python.js
CHANGED
|
@@ -2936,20 +2936,123 @@ function findImportsInCode(code, parser) {
|
|
|
2936
2936
|
// module initialization. Preserve that AST fact so dependency-cycle
|
|
2937
2937
|
// reporting can distinguish an eager import loop from a deliberate lazy
|
|
2938
2938
|
// edge without deleting either edge from the graph.
|
|
2939
|
-
|
|
2939
|
+
// fix #338: `if TYPE_CHECKING:` (bare, `t.TYPE_CHECKING`,
|
|
2940
|
+
// `typing.TYPE_CHECKING`) consequence blocks never execute at runtime —
|
|
2941
|
+
// the import exists for the type checker only. Only the consequence
|
|
2942
|
+
// branch is guarded: `else:` and `if not TYPE_CHECKING:` bodies DO run.
|
|
2943
|
+
let typingBindings = null;
|
|
2944
|
+
let typingWildcard = false;
|
|
2945
|
+
const isNestedScope = node => {
|
|
2946
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
2947
|
+
if (p.type === 'function_definition' || p.type === 'class_definition' || p.type === 'lambda') return true;
|
|
2948
|
+
}
|
|
2949
|
+
return false;
|
|
2950
|
+
};
|
|
2951
|
+
// Which local names prove a runtime-false guard, and where. A guard at
|
|
2952
|
+
// module level reads the module binding: only module-level rebindings
|
|
2953
|
+
// (assignments, loops, def/class names, walrus, `as` targets) and a
|
|
2954
|
+
// second import of the same name can disturb it — a function parameter
|
|
2955
|
+
// or a nested import lives in its own scope. A guard INSIDE a function
|
|
2956
|
+
// can additionally be shadowed by that function's locals, so nested
|
|
2957
|
+
// rebindings poison nested guards (conservative: any nested scope, not
|
|
2958
|
+
// just the enclosing one). Same-spelled user flags/attributes can be
|
|
2959
|
+
// true; only a unique `typing` binding proves the guard false.
|
|
2960
|
+
const typingBindingKind = (name, guardNested) => {
|
|
2961
|
+
if (!typingBindings) {
|
|
2962
|
+
typingBindings = new Map();
|
|
2963
|
+
const entry = local => {
|
|
2964
|
+
let e = typingBindings.get(local);
|
|
2965
|
+
if (!e) { e = { kind: null, imports: 0, moduleShadow: false, nestedShadow: false }; typingBindings.set(local, e); }
|
|
2966
|
+
return e;
|
|
2967
|
+
};
|
|
2968
|
+
const shadow = (pattern, nested) => {
|
|
2969
|
+
if (!pattern) return;
|
|
2970
|
+
traverseTree(pattern, child => {
|
|
2971
|
+
if (child.type === 'identifier') {
|
|
2972
|
+
const e = entry(child.text);
|
|
2973
|
+
if (nested) e.nestedShadow = true; else e.moduleShadow = true;
|
|
2974
|
+
}
|
|
2975
|
+
return true;
|
|
2976
|
+
});
|
|
2977
|
+
};
|
|
2978
|
+
traverseTree(tree.rootNode, n => {
|
|
2979
|
+
const nested = isNestedScope(n);
|
|
2980
|
+
if (n.type === 'import_statement' || n.type === 'import_from_statement') {
|
|
2981
|
+
if (n.namedChildren.some(item => item.type === 'wildcard_import')) typingWildcard = true;
|
|
2982
|
+
const moduleNode = n.childForFieldName('module_name');
|
|
2983
|
+
for (const item of n.namedChildren) {
|
|
2984
|
+
if (moduleNode && sameNode(moduleNode, item)) continue;
|
|
2985
|
+
const imported = item.type === 'aliased_import' ? item.childForFieldName('name')?.text : item.text;
|
|
2986
|
+
const local = item.type === 'aliased_import' ? item.childForFieldName('alias')?.text : imported?.split('.')[0];
|
|
2987
|
+
if (!local) continue;
|
|
2988
|
+
const e = entry(local);
|
|
2989
|
+
if (nested) { e.nestedShadow = true; continue; }
|
|
2990
|
+
const kind = n.type === 'import_statement'
|
|
2991
|
+
? (imported === 'typing' ? 'module' : null)
|
|
2992
|
+
: (moduleNode?.text === 'typing' && imported === 'TYPE_CHECKING' ? 'flag' : null);
|
|
2993
|
+
e.imports++;
|
|
2994
|
+
e.kind = e.imports === 1 ? kind : null;
|
|
2995
|
+
}
|
|
2996
|
+
} else if (n.type === 'assignment' || n.type === 'augmented_assignment' || n.type === 'for_statement' || n.type === 'for_in_clause') {
|
|
2997
|
+
shadow(n.childForFieldName('left'), nested);
|
|
2998
|
+
} else if (n.type === 'parameters' || n.type === 'lambda_parameters') {
|
|
2999
|
+
// Parameter NAMES rebind; annotations (`x: t.Any`) and
|
|
3000
|
+
// default values are expressions that READ the outer
|
|
3001
|
+
// binding. Walking the whole subtree treated every
|
|
3002
|
+
// `t.`-annotated parameter as a rebinding of `t` and
|
|
3003
|
+
// reverted click's TYPE_CHECKING guards to eager.
|
|
3004
|
+
for (const param of n.namedChildren) {
|
|
3005
|
+
if (param.type === 'default_parameter' || param.type === 'typed_default_parameter') {
|
|
3006
|
+
shadow(param.childForFieldName('name'), true);
|
|
3007
|
+
} else if (param.type === 'typed_parameter') {
|
|
3008
|
+
shadow(param.namedChild(0), true);
|
|
3009
|
+
} else if (param.type !== 'keyword_separator' && param.type !== 'positional_separator') {
|
|
3010
|
+
shadow(param, true);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
} else if (n.type === 'named_expression') shadow(n.childForFieldName('name'), nested);
|
|
3014
|
+
else if (n.type === 'function_definition' || n.type === 'class_definition') shadow(n.childForFieldName('name'), nested);
|
|
3015
|
+
else if (n.type === 'as_pattern') shadow(n.childForFieldName('alias'), nested);
|
|
3016
|
+
return true;
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
if (typingWildcard) return null;
|
|
3020
|
+
const e = typingBindings.get(name);
|
|
3021
|
+
if (!e || e.imports !== 1 || e.moduleShadow) return null;
|
|
3022
|
+
if (guardNested && e.nestedShadow) return null;
|
|
3023
|
+
return e.kind;
|
|
3024
|
+
};
|
|
3025
|
+
const isTypeCheckingGuard = (condition, guardNested = true) => {
|
|
3026
|
+
if (!condition) return false;
|
|
3027
|
+
if (condition.type === 'parenthesized_expression') return isTypeCheckingGuard(condition.namedChild(0), guardNested);
|
|
3028
|
+
if (condition.type === 'identifier') return typingBindingKind(condition.text, guardNested) === 'flag';
|
|
3029
|
+
if (condition.type === 'attribute') {
|
|
3030
|
+
const object = condition.childForFieldName('object');
|
|
3031
|
+
return condition.childForFieldName('attribute')?.text === 'TYPE_CHECKING' &&
|
|
3032
|
+
object?.type === 'identifier' && typingBindingKind(object.text, guardNested) === 'module';
|
|
3033
|
+
}
|
|
3034
|
+
return false;
|
|
3035
|
+
};
|
|
3036
|
+
const importDeferral = (node) => {
|
|
2940
3037
|
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
2941
3038
|
if (parent.type === 'function_definition' || parent.type === 'lambda') {
|
|
2942
|
-
return
|
|
3039
|
+
return 'function-local';
|
|
3040
|
+
}
|
|
3041
|
+
if (parent.type === 'block' && parent.parent &&
|
|
3042
|
+
(parent.parent.type === 'if_statement' || parent.parent.type === 'elif_clause') &&
|
|
3043
|
+
sameNode(parent.parent.childForFieldName('consequence'), parent) &&
|
|
3044
|
+
isTypeCheckingGuard(parent.parent.childForFieldName('condition'), isNestedScope(parent.parent))) {
|
|
3045
|
+
return 'type-checking';
|
|
2943
3046
|
}
|
|
2944
3047
|
}
|
|
2945
|
-
return
|
|
3048
|
+
return null;
|
|
2946
3049
|
};
|
|
2947
3050
|
|
|
2948
3051
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2949
3052
|
// import statement: import os, import sys as system
|
|
2950
3053
|
if (node.type === 'import_statement') {
|
|
2951
3054
|
const line = node.startPosition.row + 1;
|
|
2952
|
-
const
|
|
3055
|
+
const deferral = importDeferral(node);
|
|
2953
3056
|
|
|
2954
3057
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2955
3058
|
const child = node.namedChild(i);
|
|
@@ -2966,14 +3069,14 @@ function findImportsInCode(code, parser) {
|
|
|
2966
3069
|
names: [parts[0]],
|
|
2967
3070
|
type: 'import',
|
|
2968
3071
|
line,
|
|
2969
|
-
...(
|
|
3072
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2970
3073
|
});
|
|
2971
3074
|
imports.push({
|
|
2972
3075
|
module: child.text,
|
|
2973
3076
|
names: [],
|
|
2974
3077
|
type: 'import-submodule',
|
|
2975
3078
|
line,
|
|
2976
|
-
...(
|
|
3079
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2977
3080
|
});
|
|
2978
3081
|
} else {
|
|
2979
3082
|
imports.push({
|
|
@@ -2981,7 +3084,7 @@ function findImportsInCode(code, parser) {
|
|
|
2981
3084
|
names: [child.text],
|
|
2982
3085
|
type: 'import',
|
|
2983
3086
|
line,
|
|
2984
|
-
...(
|
|
3087
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2985
3088
|
});
|
|
2986
3089
|
}
|
|
2987
3090
|
} else if (child.type === 'aliased_import') {
|
|
@@ -2994,7 +3097,7 @@ function findImportsInCode(code, parser) {
|
|
|
2994
3097
|
names: [aliasNode ? aliasNode.text : nameNode.text.split('.').pop()],
|
|
2995
3098
|
type: 'import',
|
|
2996
3099
|
line,
|
|
2997
|
-
...(
|
|
3100
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
2998
3101
|
});
|
|
2999
3102
|
if (aliasNode && aliasNode.text !== nameNode.text) {
|
|
3000
3103
|
if (!importAliases) importAliases = [];
|
|
@@ -3009,7 +3112,7 @@ function findImportsInCode(code, parser) {
|
|
|
3009
3112
|
// from ... import statement
|
|
3010
3113
|
if (node.type === 'import_from_statement') {
|
|
3011
3114
|
const line = node.startPosition.row + 1;
|
|
3012
|
-
const
|
|
3115
|
+
const deferral = importDeferral(node);
|
|
3013
3116
|
let modulePath = '';
|
|
3014
3117
|
const names = [];
|
|
3015
3118
|
|
|
@@ -3043,7 +3146,7 @@ function findImportsInCode(code, parser) {
|
|
|
3043
3146
|
names,
|
|
3044
3147
|
type: isRelative ? 'relative' : 'from',
|
|
3045
3148
|
line,
|
|
3046
|
-
...(
|
|
3149
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
3047
3150
|
});
|
|
3048
3151
|
}
|
|
3049
3152
|
return true;
|
|
@@ -3058,7 +3161,7 @@ function findImportsInCode(code, parser) {
|
|
|
3058
3161
|
const firstArg = argsNode.namedChild(0);
|
|
3059
3162
|
if ((funcName === 'importlib.import_module' || funcName === '__import__') && firstArg) {
|
|
3060
3163
|
const line = node.startPosition.row + 1;
|
|
3061
|
-
const
|
|
3164
|
+
const deferral = importDeferral(node);
|
|
3062
3165
|
const isLiteral = firstArg.type === 'string';
|
|
3063
3166
|
imports.push({
|
|
3064
3167
|
module: isLiteral ? firstArg.text.replace(/^['"]|['"]$/g, '') : firstArg.text,
|
|
@@ -3066,7 +3169,7 @@ function findImportsInCode(code, parser) {
|
|
|
3066
3169
|
type: 'dynamic',
|
|
3067
3170
|
line,
|
|
3068
3171
|
dynamic: !isLiteral,
|
|
3069
|
-
...(
|
|
3172
|
+
...(deferral && { deferred: true, deferredReason: deferral })
|
|
3070
3173
|
});
|
|
3071
3174
|
}
|
|
3072
3175
|
}
|
package/mcp/server.js
CHANGED
|
@@ -271,7 +271,7 @@ const INPUT_SHAPE = {
|
|
|
271
271
|
include_exported: booleanParam('Include exported symbols in deadcode results'),
|
|
272
272
|
include_decorated: booleanParam('Include decorated/annotated symbols in deadcode results'),
|
|
273
273
|
calls_only: booleanParam('tests: retain direct calls and test-case matches only.'),
|
|
274
|
-
max_lines: integerParam('source: maximum lines
|
|
274
|
+
max_lines: integerParam('source: maximum lines per declaration; raw mode discloses any truncation.', { exclusiveMinimum: 0, maximum: 1000000 }),
|
|
275
275
|
direction: stringParam('trace: callees/callers. deps: imports/importers/both.', { enum: ['callees', 'callers', 'imports', 'importers', 'both'] }),
|
|
276
276
|
to: stringParam('trace with direction=callers: continue toward entry points.', { enum: ['entrypoints'] }),
|
|
277
277
|
cycles: booleanParam('deps: report circular imports instead of a file graph.'),
|
|
@@ -280,6 +280,8 @@ const INPUT_SHAPE = {
|
|
|
280
280
|
functions: booleanParam('repo stats: include per-function line counts sorted by size.'),
|
|
281
281
|
hot: booleanParam('repo stats: include the top N most-called functions.'),
|
|
282
282
|
diverse: booleanParam('show example: return representatives from distinct argument shapes.'),
|
|
283
|
+
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.'),
|
|
284
|
+
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.'),
|
|
283
285
|
git: booleanParam('show summary: attach last-modified, author, and recent-change metadata.'),
|
|
284
286
|
add_param: stringParam('Parameter name to add (plan command)'),
|
|
285
287
|
remove_param: stringParam('Parameter name to remove (plan command)'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"version": "node scripts/sync-server-version.js && git add server.json",
|
|
13
|
-
"test": "node --test 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",
|
|
13
|
+
"test": "node --test 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",
|
|
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",
|
package/assets/demo.svg
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 453" font-family="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace" font-size="12">
|
|
2
|
-
<!-- Terminal window -->
|
|
3
|
-
<rect x="0.5" y="0.5" width="999" height="452" rx="10" fill="#0d1117" stroke="#30363d"/>
|
|
4
|
-
<!-- Title bar -->
|
|
5
|
-
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h979 a10 10 0 0 1 10 10 v23 h-999 z" fill="#161b22"/>
|
|
6
|
-
<line x1="0.5" y1="34" x2="999.5" y2="34" stroke="#30363d"/>
|
|
7
|
-
<circle cx="20" cy="17" r="6" fill="#ff5f57"/>
|
|
8
|
-
<circle cx="40" cy="17" r="6" fill="#febc2e"/>
|
|
9
|
-
<circle cx="60" cy="17" r="6" fill="#28c840"/>
|
|
10
|
-
<text x="500" y="21" text-anchor="middle" fill="#8b949e" font-size="11.5">ucn - ripgrep @ 82313cf9 (pinned oracle commit) - output abridged</text>
|
|
11
|
-
<!-- Session -->
|
|
12
|
-
<g fill="#c9d1d9">
|
|
13
|
-
<text x="24" y="62"><tspan fill="#8b949e">$ </tspan><tspan fill="#7ee787">ucn show Searcher.search_reader --sections=summary,callers --compact</tspan></text>
|
|
14
|
-
<text x="24" y="100">SUMMARY</text>
|
|
15
|
-
<text x="24" y="119">───────</text>
|
|
16
|
-
<text x="24" y="138">pub search_reader<M, R, S>(&mut self, matcher: M, read_from: R, write_to: S): Result<(), S::Error></text>
|
|
17
|
-
<text x="39" y="157">crates/searcher/src/searcher/mod.rs:727-765 (39 lines)</text>
|
|
18
|
-
<text x="39" y="176">handle: crates/searcher/src/searcher/mod.rs:727:search_reader</text>
|
|
19
|
-
<text x="39" y="195">"Execute a search over any implementation of `std::io::Read` and write"</text>
|
|
20
|
-
<text x="24" y="233">CALLERS — CONFIRMED (123):</text>
|
|
21
|
-
<text x="39" y="252">evidence: 2 exact-binding, 121 receiver-hint</text>
|
|
22
|
-
<text x="39" y="271">[1] crates/core/search.rs:426 [search_reader]: searcher.search_reader(&matcher, &mut rdr, &mut sink)?;</text>
|
|
23
|
-
<text x="39" y="290">[4] crates/printer/src/json.rs:940 [binary_detection]: .search_reader(&matcher, BINARY, printer.sink(&matcher))</text>
|
|
24
|
-
<text x="39" y="309">[11] crates/printer/src/standard.rs:1788 [reports_match]: .search_reader(&matcher, SHERLOCK.as_bytes(), &mut sink)</text>
|
|
25
|
-
<text x="69" y="328" fill="#8b949e">… 120 more callers</text>
|
|
26
|
-
<text x="24" y="366">ACCOUNT: "search_reader" occurs on 136 lines in 8 files: 123 confirmed, 1 unverified,</text>
|
|
27
|
-
<text x="39" y="385">6 non-call, 6 other-target, 0 unaccounted</text>
|
|
28
|
-
<text x="24" y="404">CONTRACT: literal-name text partition complete; semantic completeness is not claimed.</text>
|
|
29
|
-
<text x="24" y="433"><tspan fill="#8b949e">$ </tspan><tspan fill="#c9d1d9">▍</tspan></text>
|
|
30
|
-
</g>
|
|
31
|
-
</svg>
|