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.
@@ -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
- const isDeferredImport = (node) => {
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 true;
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 false;
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 deferred = isDeferredImport(node);
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
- ...(deferred && { deferred: true })
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
- ...(deferred && { deferred: true })
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
- ...(deferred && { deferred: true })
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
- ...(deferred && { deferred: true })
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 deferred = isDeferredImport(node);
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
- ...(deferred && { deferred: true })
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 deferred = isDeferredImport(node);
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
- ...(deferred && { deferred: true })
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 for large class-like declarations.', { exclusiveMinimum: 0, maximum: 1000000 }),
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.2.2",
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&lt;M, R, S&gt;(&amp;mut self, matcher: M, read_from: R, write_to: S): Result&lt;(), S::Error&gt;</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(&amp;matcher, &amp;mut rdr, &amp;mut sink)?;</text>
23
- <text x="39" y="290">[4] crates/printer/src/json.rs:940 [binary_detection]: .search_reader(&amp;matcher, BINARY, printer.sink(&amp;matcher))</text>
24
- <text x="39" y="309">[11] crates/printer/src/standard.rs:1788 [reports_match]: .search_reader(&amp;matcher, SHERLOCK.as_bytes(), &amp;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>