ucn 5.2.2 → 5.3.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.
@@ -1317,34 +1317,59 @@ function pythonTargetBindsName(left, name) {
1317
1317
  return false;
1318
1318
  }
1319
1319
 
1320
- function pythonScopeBindsName(scopeNode, name) {
1321
- for (let i = 0; i < scopeNode.namedChildCount; i++) {
1322
- const child = scopeNode.namedChild(i);
1323
- if (child.type === 'function_definition' ||
1324
- child.type === 'async_function_definition' ||
1325
- child.type === 'class_definition') {
1326
- // The nested body is a separate scope, but the declaration name
1327
- // binds in this scope.
1328
- if (child.childForFieldName('name')?.text === name) return true;
1329
- continue;
1320
+ // One walk per scope body collects every name it binds at this scope level
1321
+ // (declarations, assignment targets, for/with targets; nested def/class bodies
1322
+ // are separate scopes, lambdas too). Memoized per tree by native node id: the
1323
+ // caller loop asks the same body about many names, and a per-name walk made
1324
+ // the Python build cost functions x tracked names x body size (measured 10s of
1325
+ // a 60s sequential build on a 20MB Python repo).
1326
+ const scopeBoundNamesByTree = new WeakMap();
1327
+ function pythonScopeBoundNames(scopeNode) {
1328
+ let byId = scopeBoundNamesByTree.get(scopeNode.tree);
1329
+ if (!byId) { byId = new Map(); scopeBoundNamesByTree.set(scopeNode.tree, byId); }
1330
+ const cached = byId.get(scopeNode.id);
1331
+ if (cached) return cached;
1332
+ const names = new Set();
1333
+ const addTarget = (left) => {
1334
+ if (!left) return;
1335
+ if (left.type === 'identifier') names.add(left.text);
1336
+ else if (left.type === 'pattern_list' || left.type === 'tuple_pattern') {
1337
+ for (const item of left.namedChildren) {
1338
+ if (item.type === 'identifier') names.add(item.text);
1339
+ }
1330
1340
  }
1331
- if (child.type === 'lambda') continue;
1332
- if (child.type === 'assignment' ||
1333
- child.type === 'augmented_assignment' ||
1334
- child.type === 'named_expression') {
1335
- if (pythonTargetBindsName(
1336
- child.childForFieldName('left') || child.childForFieldName('name'),
1337
- name)) return true;
1338
- } else if (child.type === 'for_statement') {
1339
- if (pythonTargetBindsName(child.childForFieldName('left'), name)) return true;
1340
- } else if (child.type === 'with_statement') {
1341
- const text = child.namedChild(0)?.text || '';
1342
- const match = text.match(/\bas\s+([A-Za-z_][A-Za-z0-9_]*)/);
1343
- if (match && match[1] === name) return true;
1344
- }
1345
- if (pythonScopeBindsName(child, name)) return true;
1346
- }
1347
- return false;
1341
+ };
1342
+ const walk = (node) => {
1343
+ for (const child of node.namedChildren) {
1344
+ if (child.type === 'function_definition' ||
1345
+ child.type === 'async_function_definition' ||
1346
+ child.type === 'class_definition') {
1347
+ const declared = child.childForFieldName('name')?.text;
1348
+ if (declared) names.add(declared);
1349
+ continue;
1350
+ }
1351
+ if (child.type === 'lambda') continue;
1352
+ if (child.type === 'assignment' ||
1353
+ child.type === 'augmented_assignment' ||
1354
+ child.type === 'named_expression') {
1355
+ addTarget(child.childForFieldName('left') || child.childForFieldName('name'));
1356
+ } else if (child.type === 'for_statement') {
1357
+ addTarget(child.childForFieldName('left'));
1358
+ } else if (child.type === 'with_statement') {
1359
+ const text = child.namedChild(0)?.text || '';
1360
+ const match = text.match(/\bas\s+([A-Za-z_][A-Za-z0-9_]*)/);
1361
+ if (match) names.add(match[1]);
1362
+ }
1363
+ walk(child);
1364
+ }
1365
+ };
1366
+ walk(scopeNode);
1367
+ byId.set(scopeNode.id, names);
1368
+ return names;
1369
+ }
1370
+
1371
+ function pythonScopeBindsName(scopeNode, name) {
1372
+ return pythonScopeBoundNames(scopeNode).has(name);
1348
1373
  }
1349
1374
 
1350
1375
  const PY_COMPREHENSIONS = new Set([
@@ -2936,20 +2961,123 @@ function findImportsInCode(code, parser) {
2936
2961
  // module initialization. Preserve that AST fact so dependency-cycle
2937
2962
  // reporting can distinguish an eager import loop from a deliberate lazy
2938
2963
  // edge without deleting either edge from the graph.
2939
- const isDeferredImport = (node) => {
2964
+ // fix #338: `if TYPE_CHECKING:` (bare, `t.TYPE_CHECKING`,
2965
+ // `typing.TYPE_CHECKING`) consequence blocks never execute at runtime —
2966
+ // the import exists for the type checker only. Only the consequence
2967
+ // branch is guarded: `else:` and `if not TYPE_CHECKING:` bodies DO run.
2968
+ let typingBindings = null;
2969
+ let typingWildcard = false;
2970
+ const isNestedScope = node => {
2971
+ for (let p = node.parent; p; p = p.parent) {
2972
+ if (p.type === 'function_definition' || p.type === 'class_definition' || p.type === 'lambda') return true;
2973
+ }
2974
+ return false;
2975
+ };
2976
+ // Which local names prove a runtime-false guard, and where. A guard at
2977
+ // module level reads the module binding: only module-level rebindings
2978
+ // (assignments, loops, def/class names, walrus, `as` targets) and a
2979
+ // second import of the same name can disturb it — a function parameter
2980
+ // or a nested import lives in its own scope. A guard INSIDE a function
2981
+ // can additionally be shadowed by that function's locals, so nested
2982
+ // rebindings poison nested guards (conservative: any nested scope, not
2983
+ // just the enclosing one). Same-spelled user flags/attributes can be
2984
+ // true; only a unique `typing` binding proves the guard false.
2985
+ const typingBindingKind = (name, guardNested) => {
2986
+ if (!typingBindings) {
2987
+ typingBindings = new Map();
2988
+ const entry = local => {
2989
+ let e = typingBindings.get(local);
2990
+ if (!e) { e = { kind: null, imports: 0, moduleShadow: false, nestedShadow: false }; typingBindings.set(local, e); }
2991
+ return e;
2992
+ };
2993
+ const shadow = (pattern, nested) => {
2994
+ if (!pattern) return;
2995
+ traverseTree(pattern, child => {
2996
+ if (child.type === 'identifier') {
2997
+ const e = entry(child.text);
2998
+ if (nested) e.nestedShadow = true; else e.moduleShadow = true;
2999
+ }
3000
+ return true;
3001
+ });
3002
+ };
3003
+ traverseTree(tree.rootNode, n => {
3004
+ const nested = isNestedScope(n);
3005
+ if (n.type === 'import_statement' || n.type === 'import_from_statement') {
3006
+ if (n.namedChildren.some(item => item.type === 'wildcard_import')) typingWildcard = true;
3007
+ const moduleNode = n.childForFieldName('module_name');
3008
+ for (const item of n.namedChildren) {
3009
+ if (moduleNode && sameNode(moduleNode, item)) continue;
3010
+ const imported = item.type === 'aliased_import' ? item.childForFieldName('name')?.text : item.text;
3011
+ const local = item.type === 'aliased_import' ? item.childForFieldName('alias')?.text : imported?.split('.')[0];
3012
+ if (!local) continue;
3013
+ const e = entry(local);
3014
+ if (nested) { e.nestedShadow = true; continue; }
3015
+ const kind = n.type === 'import_statement'
3016
+ ? (imported === 'typing' ? 'module' : null)
3017
+ : (moduleNode?.text === 'typing' && imported === 'TYPE_CHECKING' ? 'flag' : null);
3018
+ e.imports++;
3019
+ e.kind = e.imports === 1 ? kind : null;
3020
+ }
3021
+ } else if (n.type === 'assignment' || n.type === 'augmented_assignment' || n.type === 'for_statement' || n.type === 'for_in_clause') {
3022
+ shadow(n.childForFieldName('left'), nested);
3023
+ } else if (n.type === 'parameters' || n.type === 'lambda_parameters') {
3024
+ // Parameter NAMES rebind; annotations (`x: t.Any`) and
3025
+ // default values are expressions that READ the outer
3026
+ // binding. Walking the whole subtree treated every
3027
+ // `t.`-annotated parameter as a rebinding of `t` and
3028
+ // reverted click's TYPE_CHECKING guards to eager.
3029
+ for (const param of n.namedChildren) {
3030
+ if (param.type === 'default_parameter' || param.type === 'typed_default_parameter') {
3031
+ shadow(param.childForFieldName('name'), true);
3032
+ } else if (param.type === 'typed_parameter') {
3033
+ shadow(param.namedChild(0), true);
3034
+ } else if (param.type !== 'keyword_separator' && param.type !== 'positional_separator') {
3035
+ shadow(param, true);
3036
+ }
3037
+ }
3038
+ } else if (n.type === 'named_expression') shadow(n.childForFieldName('name'), nested);
3039
+ else if (n.type === 'function_definition' || n.type === 'class_definition') shadow(n.childForFieldName('name'), nested);
3040
+ else if (n.type === 'as_pattern') shadow(n.childForFieldName('alias'), nested);
3041
+ return true;
3042
+ });
3043
+ }
3044
+ if (typingWildcard) return null;
3045
+ const e = typingBindings.get(name);
3046
+ if (!e || e.imports !== 1 || e.moduleShadow) return null;
3047
+ if (guardNested && e.nestedShadow) return null;
3048
+ return e.kind;
3049
+ };
3050
+ const isTypeCheckingGuard = (condition, guardNested = true) => {
3051
+ if (!condition) return false;
3052
+ if (condition.type === 'parenthesized_expression') return isTypeCheckingGuard(condition.namedChild(0), guardNested);
3053
+ if (condition.type === 'identifier') return typingBindingKind(condition.text, guardNested) === 'flag';
3054
+ if (condition.type === 'attribute') {
3055
+ const object = condition.childForFieldName('object');
3056
+ return condition.childForFieldName('attribute')?.text === 'TYPE_CHECKING' &&
3057
+ object?.type === 'identifier' && typingBindingKind(object.text, guardNested) === 'module';
3058
+ }
3059
+ return false;
3060
+ };
3061
+ const importDeferral = (node) => {
2940
3062
  for (let parent = node.parent; parent; parent = parent.parent) {
2941
3063
  if (parent.type === 'function_definition' || parent.type === 'lambda') {
2942
- return true;
3064
+ return 'function-local';
3065
+ }
3066
+ if (parent.type === 'block' && parent.parent &&
3067
+ (parent.parent.type === 'if_statement' || parent.parent.type === 'elif_clause') &&
3068
+ sameNode(parent.parent.childForFieldName('consequence'), parent) &&
3069
+ isTypeCheckingGuard(parent.parent.childForFieldName('condition'), isNestedScope(parent.parent))) {
3070
+ return 'type-checking';
2943
3071
  }
2944
3072
  }
2945
- return false;
3073
+ return null;
2946
3074
  };
2947
3075
 
2948
3076
  traverseTreeCached(tree.rootNode, (node) => {
2949
3077
  // import statement: import os, import sys as system
2950
3078
  if (node.type === 'import_statement') {
2951
3079
  const line = node.startPosition.row + 1;
2952
- const deferred = isDeferredImport(node);
3080
+ const deferral = importDeferral(node);
2953
3081
 
2954
3082
  for (let i = 0; i < node.namedChildCount; i++) {
2955
3083
  const child = node.namedChild(i);
@@ -2966,14 +3094,14 @@ function findImportsInCode(code, parser) {
2966
3094
  names: [parts[0]],
2967
3095
  type: 'import',
2968
3096
  line,
2969
- ...(deferred && { deferred: true })
3097
+ ...(deferral && { deferred: true, deferredReason: deferral })
2970
3098
  });
2971
3099
  imports.push({
2972
3100
  module: child.text,
2973
3101
  names: [],
2974
3102
  type: 'import-submodule',
2975
3103
  line,
2976
- ...(deferred && { deferred: true })
3104
+ ...(deferral && { deferred: true, deferredReason: deferral })
2977
3105
  });
2978
3106
  } else {
2979
3107
  imports.push({
@@ -2981,7 +3109,7 @@ function findImportsInCode(code, parser) {
2981
3109
  names: [child.text],
2982
3110
  type: 'import',
2983
3111
  line,
2984
- ...(deferred && { deferred: true })
3112
+ ...(deferral && { deferred: true, deferredReason: deferral })
2985
3113
  });
2986
3114
  }
2987
3115
  } else if (child.type === 'aliased_import') {
@@ -2994,7 +3122,7 @@ function findImportsInCode(code, parser) {
2994
3122
  names: [aliasNode ? aliasNode.text : nameNode.text.split('.').pop()],
2995
3123
  type: 'import',
2996
3124
  line,
2997
- ...(deferred && { deferred: true })
3125
+ ...(deferral && { deferred: true, deferredReason: deferral })
2998
3126
  });
2999
3127
  if (aliasNode && aliasNode.text !== nameNode.text) {
3000
3128
  if (!importAliases) importAliases = [];
@@ -3009,7 +3137,7 @@ function findImportsInCode(code, parser) {
3009
3137
  // from ... import statement
3010
3138
  if (node.type === 'import_from_statement') {
3011
3139
  const line = node.startPosition.row + 1;
3012
- const deferred = isDeferredImport(node);
3140
+ const deferral = importDeferral(node);
3013
3141
  let modulePath = '';
3014
3142
  const names = [];
3015
3143
 
@@ -3043,7 +3171,7 @@ function findImportsInCode(code, parser) {
3043
3171
  names,
3044
3172
  type: isRelative ? 'relative' : 'from',
3045
3173
  line,
3046
- ...(deferred && { deferred: true })
3174
+ ...(deferral && { deferred: true, deferredReason: deferral })
3047
3175
  });
3048
3176
  }
3049
3177
  return true;
@@ -3058,7 +3186,7 @@ function findImportsInCode(code, parser) {
3058
3186
  const firstArg = argsNode.namedChild(0);
3059
3187
  if ((funcName === 'importlib.import_module' || funcName === '__import__') && firstArg) {
3060
3188
  const line = node.startPosition.row + 1;
3061
- const deferred = isDeferredImport(node);
3189
+ const deferral = importDeferral(node);
3062
3190
  const isLiteral = firstArg.type === 'string';
3063
3191
  imports.push({
3064
3192
  module: isLiteral ? firstArg.text.replace(/^['"]|['"]$/g, '') : firstArg.text,
@@ -3066,7 +3194,7 @@ function findImportsInCode(code, parser) {
3066
3194
  type: 'dynamic',
3067
3195
  line,
3068
3196
  dynamic: !isLiteral,
3069
- ...(deferred && { deferred: true })
3197
+ ...(deferral && { deferred: true, deferredReason: deferral })
3070
3198
  });
3071
3199
  }
3072
3200
  }
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.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 && 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>