ucn 3.8.25 → 4.0.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.
@@ -57,8 +57,11 @@ Walks UP the caller chain recursively. Shows the full tree of functions affected
57
57
  ucn blast helper # callers of callers (depth 3)
58
58
  ucn blast helper --depth=5 # deeper chain
59
59
  ucn blast helper --exclude=test # skip test callers
60
+ ucn blast helper --expand-unverified # follow unverified edges too (marked ⚠, possible impact)
60
61
  ```
61
62
 
63
+ The tree trunk is confirmed-evidence-only; dispatch-possible/ambiguous caller candidates appear in an `UNVERIFIED EDGES` section (see "Reading Tiered Output" below).
64
+
62
65
  ### 4. `trace` — Understand execution flow (downward)
63
66
 
64
67
  Draws the call tree downward from any function. Compact by default; setting `--depth=N` shows the full tree to that depth with all children expanded.
@@ -71,6 +74,8 @@ ucn trace generate_report --all # all children at default depth
71
74
 
72
75
  Shows the entire pipeline — what `generate_report` calls, what those functions call, etc. — as an indented tree. No file reading needed. Invaluable for understanding orchestrator functions or entry points.
73
76
 
77
+ **Prefer `trace` over chained `about` calls.** If you find yourself running `ucn about` 4–5 times in a row to follow a call chain (entry → leaves), one `ucn trace <fn> --depth=N` returns the same information in a single call. Use `--depth=N` to limit how deep the tree goes.
78
+
74
79
  ### 5. `fn` / `class` — Extract without reading the whole file
75
80
 
76
81
  Pull one or more functions out of a large file. Supports comma-separated names for bulk extraction.
@@ -83,12 +88,13 @@ ucn class MarketDataFetcher
83
88
 
84
89
  ### 6. `deadcode` — Find unused code
85
90
 
86
- Lists all functions and classes with zero callers across the project. Framework entry points (Express routes, Spring controllers, Celery tasks, etc.) are automatically excluded.
91
+ Lists all functions and classes with zero callers across the project. Framework entry points (Express routes, Spring controllers, Celery tasks, etc.) and exported/public API symbols — including methods of exported classes in JS/TS/Python — are automatically excluded (`--include-exported` audits them). Interface/trait method declarations are labeled `[declared on interface X — contract surface, not executable code]`: unreferenced is true, but deleting one changes the API contract, not dead logic.
87
92
 
88
93
  ```bash
89
94
  ucn deadcode # Everything
90
95
  ucn deadcode --exclude=test # Skip test files (most useful)
91
96
  ucn deadcode --include-decorated # Include framework-registered functions
97
+ ucn deadcode --include-exported # Audit exported/public API symbols too
92
98
  ```
93
99
 
94
100
  ### 7. `brief` — One-screen "before-I-touch-this" summary
@@ -161,7 +167,7 @@ ucn entrypoints --exclude-tests # Hide test fixtures (JUnit @Test, pyte
161
167
  | Project complexity stats | `ucn stats` | File counts, symbol counts, lines by language. `--functions` for per-function line counts. `--hot --top=N` for the most-called functions (orientation primitive on a new repo) |
162
168
  | Find by glob pattern | `ucn find "handle*"` | Locate definitions matching a glob (supports * and ?) |
163
169
  | Text search with context | `ucn search term --context=3` | Like grep -C 3, shows surrounding lines |
164
- | Regex search (default) | `ucn search '\d+'` | Search supports regex by default (alternation, character classes, etc.) |
170
+ | Regex search (default) | `ucn search '\d+'` | JavaScript regex (V8 engine). See "Regex notes" below for syntax — alternation is `a|b`, not grep-style |
165
171
  | Text search filtered | `ucn search term --exclude=test` | Search only in matching files |
166
172
  | Structural search (index) | `ucn search --type=function --param=Request` | Query the symbol table, not text. Finds functions by param, return type, decorator, etc. |
167
173
  | Find all db.* calls | `ucn search --type=call --receiver=db` | Search call sites by receiver — something grep can't do |
@@ -187,6 +193,16 @@ ucn entrypoints --exclude-tests # Hide test fixtures (JUnit @Test, pyte
187
193
  | Pre-commit summary | `ucn check [--base=main]` | Changed funcs + signature drift + affected tests in one shot |
188
194
  | Find missing-await bugs | `ucn audit-async` | Lists async calls inside async functions that lack `await`. JS/TS/Python only. Filter with `--file`, `--exclude`, `--limit` |
189
195
 
196
+ ## Regex Notes (`search` command)
197
+
198
+ `search` uses **JavaScript regex** (the V8 engine), not grep BRE/ERE. Common gotchas:
199
+
200
+ - Alternation is `a|b`, **not** `a\|b`. `ucn search "flask|fastapi|django"` works; `ucn search "flask\|fastapi\|django"` matches the literal string.
201
+ - `(`, `[`, `{` outside character classes do **not** need escaping for literal match in most cases — but normal JS regex semantics apply (you can still escape them for clarity).
202
+ - Wrap the pattern in single quotes so the shell does not interpret special characters (`*`, `?`, `\`, `$`, etc.).
203
+ - Default is case-insensitive; pass `--case-sensitive` to flip.
204
+ - `--no-regex` forces literal-string search if you need to match regex metacharacters as text without escaping.
205
+
190
206
  ## Reading Call-Site Patterns
191
207
 
192
208
  `verify`, `impact`, and `about` annotate call sites with structural classification flags. Watch for these in summary lines (`Patterns: 4 in try, 4 in callback`) and per-call-site entries:
@@ -201,23 +217,32 @@ ucn entrypoints --exclude-tests # Hide test fixtures (JUnit @Test, pyte
201
217
 
202
218
  `audit-async` is the focused tool for the `awaited=false` case across an entire async function body.
203
219
 
204
- ## Confidence Scores
220
+ ## Reading Tiered Output (about / context / impact / trace / blast / reverse-trace / affected-tests)
221
+
222
+ Caller answers are a **partition of every text occurrence** of the symbol — nothing is silently hidden. Sections:
223
+
224
+ - `CALLERS — CONFIRMED (N, X prod + Y test):` — edges with binding/receiver/import evidence. Prod callers listed first, then a `test callers:` subheader. An `evidence:` line aggregates resolution labels for the section.
225
+ - `CALLERS — UNVERIFIED (N) — call syntax, no binding/receiver evidence:` — name-matched call sites the engine could not verify, one line each with the reason (`method-no-evidence`, `ambiguous-binding`, `call-not-resolved`). Capped at 10; `--all` lifts the cap. **Treat these as possible callers when refactoring.**
226
+ - `NON-CALL OCCURRENCES: N (...)` — imports/definitions/references/other-text, counts only (drill in with `ucn usages <name>`).
227
+ - `ACCOUNT:` — the reconciliation line. Every ground line is in exactly one bucket; `0 unaccounted` means the partition is complete. `+N beyond-text callers` are alias-resolved call sites plain text search would miss.
228
+ - `WARNING: N unparsed file(s) ...` — files containing the symbol that failed to parse. Their lines were NOT analyzed — fall back to text search for those files.
229
+ - `FILTERED: N hidden by flags` — entries your display flags hid (they still count in ACCOUNT).
230
+
231
+ **Trust rules:** a CONFIRMED(0) + UNVERIFIED(0) answer with `0 unaccounted` and no WARNING means the symbol genuinely has no callers — safe to act on, same as a clean grep. Any UNVERIFIED entries or WARNINGs mean: verify those sites before a breaking change.
232
+
233
+ Resolution labels in `evidence:` lines (high to low): `exact-binding` (0.98, import/binding evidence) · `same-class` (0.92) · `receiver-hint` (0.80, inferred receiver type) · `scope-match` (0.65, import/receiver-binding scope evidence) · `name-only` (0.40) · `uncertain` (0.25). Confirmed tier = scope-match and above. JSON output keeps per-edge decimals plus `tier`.
234
+
235
+ Flags: `--min-confidence=0.7` filters confirmed edges (hidden count appears in FILTERED). `--include-uncertain` is an **implied no-op** for about/context/impact/trace/blast/reverse-trace/affected-tests (everything is shown, tiered); it keeps its meaning for `smart`/`verify`. `--include-methods` is an implied no-op for about/context/impact.
236
+
237
+ ### Tree commands (trace / blast / reverse-trace / affected-tests)
205
238
 
206
- `about` and `context` show confidence per caller/callee edge by default (e.g. `confidence: 0.65 (scope-match)`). Resolution labels (high to low):
239
+ The tree trunk holds **confirmed edges only**. Unverified caller candidates render in an `UNVERIFIED EDGES` section with parent attribution (`at <node> (hop N): file:line [enclosing fn] (reason)`) and are **not expanded by default** pass `--expand-unverified` to follow them; every downstream node is then marked `[⚠ via <reason>]` / `[⚠ unverified chain]` and counted as *possibly affected*, never confirmed. Unresolved callee calls (`trace` down) render as `[unverified] name — reason` leaves under their node. Reconciliation lines:
207
240
 
208
- | Label | Score | Meaning |
209
- |-------|-------|---------|
210
- | `exact-binding` | 0.98 | Import or binding evidence direct call |
211
- | `same-class` | 0.92 | Method call resolved within the enclosing class |
212
- | `receiver-hint` | 0.80 | Receiver type inferred (Go/Java/Rust) — e.g. `f.run()` where `f: Filter` |
213
- | `scope-match` | 0.65 | Symbol resolved by name within a file/package scope |
214
- | `name-only` | 0.40 | Name match only, no binding/scope evidence — review before trusting |
215
- | `uncertain` | 0.25 | Ambiguous — multiple candidates, hidden unless `--include-uncertain` |
241
+ - `ACCOUNT:` the root hop's text-ground partition (same as context/impact).
242
+ - `TREE ACCOUNT:` — interior conservation: nodes expanded, confirmed/unverified/excluded edge counts by reason, depth-limit cuts.
243
+ - `CALLEE ACCOUNT:` (trace down) every call site in every expanded node lands in confirmed/unverified/external/excluded/filtered.
216
244
 
217
- Filter or hide:
218
- - `--min-confidence=0.7` — keep only high-confidence edges (≥ same-class)
219
- - `--hide-confidence` — silence the scores (legacy `--no-confidence` is a silent alias)
220
- - `--include-uncertain` — include otherwise-filtered low-confidence matches
245
+ `reverse-trace` marks `★ entry point` only when a node has **zero candidates in both tiers**; zero confirmed with unverified candidates shows `⚠ no confirmed callers — N unverified` instead. `affected-tests` splits its answer into the confirmed band (`Test files to run`, coverage, `Uncovered`) and a `POSSIBLY AFFECTED` band (functions + test files reachable only through unverified chains).
221
246
 
222
247
  ## Symbol Handles (stable IDs)
223
248
 
@@ -253,9 +278,10 @@ ucn [target] <command> [name] [--flags]
253
278
  | `--in=src/core` | Limit search to a subdirectory |
254
279
  | `--depth=N` | Control tree depth for `trace`, `graph`, and detail level for `find` (default 3). Also expands all children — no breadth limit |
255
280
  | `--all` | Expand truncated sections. Applies to `about`, `blast`, `trace`, `reverse-trace`, `related`, `find`, `toc`, `fn`, `class`, `graph`, `diff-impact` |
281
+ | `--expand-unverified` | `blast`/`reverse-trace`: follow unverified caller edges in the tree. Downstream nodes are marked as unverified chains — possible, not confirmed, impact |
256
282
  | `--include-tests` | Include test files in usage counts (`about`) and results (`find`, `usages`, `deadcode`). Callers always include tests. |
257
283
  | `--exclude-tests` | Exclude test entries from `entrypoints` (tests are included by default since they ARE entry points). |
258
- | `--include-methods` | Include `obj.method()` calls in caller/callee analysis. `impact` defaults to true (show all callable sites); other commands default to false (use `--no-include-methods` to opt out). Applies to `about`, `context`, `impact`, `verify`, `blast`, `smart`, `trace`, `reverse-trace`, `affected-tests` |
284
+ | `--include-methods` | Include `obj.method()` calls in caller/callee analysis. Implied (no-op) for `about`/`context`/`impact` method calls are always analyzed and tiered by receiver evidence. Applies to `verify`, `blast`, `smart`, `trace`, `reverse-trace`, `affected-tests` |
259
285
  | `--base=<ref>` | Git ref for diff-impact (default: HEAD) |
260
286
  | `--staged` | Analyze staged changes (diff-impact) |
261
287
  | `--no-cache` | Force re-index after editing files |
@@ -277,7 +303,7 @@ ucn [target] <command> [name] [--flags]
277
303
  | `--max-lines=N` | Max source lines for `class` (large classes show summary by default) |
278
304
  | `--case-sensitive` | Case-sensitive text search (default: case-insensitive) |
279
305
  | `--exact` | Exact name match only in `find`/`typedef` (no substring) |
280
- | `--include-uncertain` | Include ambiguous/uncertain matches. Applies to `about`, `context`, `blast`, `smart`, `trace`, `reverse-trace`, `affected-tests` |
306
+ | `--include-uncertain` | Include ambiguous/uncertain matches. Implied (no-op) for `about`/`context`/`impact` — unverified callers are always shown in their own tier. Applies to `blast`, `smart`, `trace`, `reverse-trace`, `affected-tests` |
281
307
  | `--hide-confidence` | Hide confidence scores (shown by default) in `context`/`about` |
282
308
  | `--min-confidence=N` | Filter edges below confidence threshold (e.g., `--min-confidence=0.7` keeps only high-confidence edges) |
283
309
  | `--calls-only` | Only show call/test-case matches in `tests` (skip file-level results) |
package/README.md CHANGED
@@ -34,6 +34,8 @@ UCN is deliberately lightweight:
34
34
  - **No language servers** - tree-sitter does the parsing, no compilation needed
35
35
  - **MCP is optional** - only needed if you connect UCN to an AI agent, the CLI and Skill work on their own
36
36
 
37
+ And it's built to be **trusted**: every "who calls this?" splits into what UCN can prove and what it can't — each flagged with a reason, nothing silently dropped, [measured in CI against real compilers and language servers](#answers-you-can-trust).
38
+
37
39
  ---
38
40
 
39
41
  ```bash
@@ -59,16 +61,20 @@ build
59
61
  │ └── compareNames (core/discovery.js:170) 1x
60
62
  ├── parallelBuild (core/parallel-build.js:25) 1x
61
63
  ├── indexFile (core/project.js:310) 1x
62
- │ ├── addSymbol (core/project.js:398) 4x
63
- │ ├── detectLanguage (languages/index.js:209) 1x
64
+ │ ├── addSymbol (core/project.js:410) 4x
65
+ │ ├── detectLanguage (languages/index.js:288) 1x
64
66
  │ ├── parse (core/parser.js:69) 1x
65
67
  │ ├── extractImports (core/imports.js:19) 1x
66
68
  │ └── extractExports (core/imports.js:44) 1x
67
- ├── buildImportGraph (core/project.js:631) 1x
68
- └── buildInheritanceGraph (core/project.js:636) 1x
69
+ ├── buildImportGraph (core/project.js:648) 1x
70
+ └── buildInheritanceGraph (core/project.js:653) 1x
71
+ … calls UCN can't prove a receiver for (arr.push(), obj.get()) show as
72
+ [unverified] leaves — abridged here
73
+
74
+ CALLEE ACCOUNT: 23 nodes · 329 call sites = 43 confirmed + 151 unverified + 34 builtin + 101 excluded
69
75
  ```
70
76
 
71
- One command. No files opened. Every function located by file and line.
77
+ One command, no files opened and the `CALLEE ACCOUNT:` line proves all 329 calls were sorted, nothing dropped.
72
78
 
73
79
  ---
74
80
 
@@ -84,25 +90,81 @@ expandGlob (function)
84
90
  core/discovery.js:191-222 → core/discovery.js:191:expandGlob
85
91
  expandGlob (pattern: string, options: number = {}) : string[]
86
92
 
87
- CALLERS (7):
88
- confidence: 0 high (>0.8), 7 medium (0.5-0.8), 0 low (<0.5)
89
- cli/index.js:1190 [runGlobCommand]
93
+ USAGES: 6 total
94
+ 3 calls, 3 imports, 0 references
95
+
96
+ CALLERS — CONFIRMED (7, 3 prod + 4 test):
97
+ evidence: scope-match (all)
98
+ cli/index.js:1201 [runGlobCommand]
90
99
  const files = expandGlob(pattern);
91
- confidence: 0.65 (scope-match)
92
- core/cache.js:417 [isCacheStale]
100
+ core/cache.js:466 [isCacheStale]
93
101
  const currentFiles = expandGlob(pattern, globOpts);
94
- confidence: 0.65 (scope-match)
95
- ... (5 more)
102
+ core/project.js:192 [build]
103
+ files = expandGlob(pattern, globOpts);
104
+ test callers:
105
+ test/integration.test.js:167
106
+ const files = expandGlob('**/*.go', { root: tmpDir });
107
+ ... (3 more test callers)
96
108
 
97
109
  CALLEES (3):
98
- parseGlobPattern [utility] - core/discovery.js:227
99
- walkDir [utility] {fs} - core/discovery.js:284
100
- compareNames [utility] - core/discovery.js:170
110
+ evidence: exact-binding (all)
111
+ parseGlobPattern [utility] - core/discovery.js:227 (1x)
112
+ walkDir [utility] {fs} - core/discovery.js:284 (1x)
113
+ compareNames [utility] - core/discovery.js:170 (1x)
114
+
115
+ ACCOUNT: "expandGlob" occurs on 14 lines in 6 files: 7 confirmed, 0 unverified,
116
+ 7 non-call (4 import, 1 definition, 1 reference, 1 other-text), 0 other-target, 0 unaccounted
101
117
 
102
118
  TESTS: 5 matches in 1 file(s)
103
119
  ```
104
120
 
105
- Each caller comes with a confidence score (`exact-binding`, `same-class`, `receiver-hint`, `scope-match`, `name-only`) so you can tell direct calls from name-only matches. Add `--hide-confidence` to silence the scores, or `--min-confidence=0.7` to drop everything below `same-class`. Add `--git` to see who last touched the function. Need to trace execution upward instead? `ucn reverse-trace fn` walks the caller chain back to entry points.
121
+ Callers split into **CONFIRMED** (binding/receiver/import evidence prod first, then tests) and **UNVERIFIED** (found but unproven, each with a reason). The `ACCOUNT:` line reconciles every occurrence; `0 unaccounted` means nothing was hidden. Tune with `--min-confidence` / `--hide-confidence` / `--git`; walk callers *upward* with `ucn reverse-trace fn`.
122
+
123
+ ## Answers you can trust
124
+
125
+ UCN doesn't just find a name — it tells you how sure it is. Every answer from `about`, `context`, and `impact` partitions *every* place the name appears into buckets you can act on:
126
+
127
+ ```
128
+ $ ucn impact saveCache
129
+
130
+ CALL SITES: 2 confirmed + 9 unverified
131
+
132
+ test/regression-go.test.js (2 calls)
133
+ :2004
134
+ saveCache(index, cachePath);
135
+
136
+ UNVERIFIED CALL SITES (9) — call syntax, no binding/receiver evidence:
137
+ mcp/server.js:779: index.saveCache(); (method-ambiguous)
138
+ test/cache.test.js:1779: index.saveCache(); (method-ambiguous)
139
+ ... (7 more)
140
+
141
+ ACCOUNT: "saveCache" occurs on 47 lines in 8 files: 2 confirmed, 9 unverified,
142
+ 9 non-call (2 import, 1 definition, 0 reference, 6 other-text), 27 other-target, 0 unaccounted
143
+ ```
144
+
145
+ UCN sorts every one of the 47 places the name appears:
146
+
147
+ - **2 confirmed** — call sites it can prove resolve to *this* `saveCache`.
148
+ - **9 unverified** — real call sites it found but won't claim. `index.saveCache()` has an untyped receiver, so UCN can't prove which `saveCache` runs; it shows the site and the reason (`method-ambiguous`) instead of guessing.
149
+ - **27 other-target** — occurrences that belong to a *different* `saveCache`, kept separate so they never pollute the answer.
150
+ - **9 non-call** — imports, the definition, plain text.
151
+ - **`0 unaccounted`** — the partition is complete. Nothing was dropped on the floor.
152
+
153
+ The payoff: a **confirmed** answer is safe to refactor against, and an empty result with `0 unaccounted` means the symbol truly has no callers. UCN never hides a caller — and never invents one.
154
+
155
+ ### Measured against ground truth
156
+
157
+ This isn't a promise — it's a gate. CI re-derives UCN's caller answers from real compilers and language servers and fails the build if a single true call edge is neither shown nor accounted for:
158
+
159
+ | Language | Oracle | Confirmed-tier precision |
160
+ |---|---|---|
161
+ | TypeScript / JavaScript | ts-morph | 99.4–100% |
162
+ | Python | pyright (LSP) | 97.7–99.9% |
163
+ | Go | gopls | 99.9–100% |
164
+ | Rust | rust-analyzer | 98.9–100% |
165
+ | Java | jdtls | 96.2% |
166
+
167
+ Ten pinned real-world repos (zod, express, httpx, rich, cobra, grpc-go, ripgrep, cursive, gson, preact-signals), three sampling seeds, every run gated at `missing-unexplained = 0`. The tree commands — `trace`, `blast`, `reverse-trace`, `affected-tests` — follow the same rule: confirmed trunk, uncertain branches flagged (`--expand-unverified` to follow). Run `ucn doctor` for the trust report on *your* repo.
106
168
 
107
169
  ## Change code without breaking things
108
170
 
@@ -217,33 +279,36 @@ affected-tests: expandGlob
217
279
  core/discovery.js:191
218
280
  1 function changed → 15 functions affected (depth 3)
219
281
 
220
- Test files to run (19):
282
+ Test files to run (20):
221
283
 
222
284
  test/integration.test.js (covers: expandGlob, build, idx, setupProject)
223
285
  L47: index.build(null, { quiet: true }); [call]
224
286
  L167: const files = expandGlob('**/*.go', { root: tmpDir }); [call]
225
287
  ...
226
288
 
227
- Uncovered (10): runGlobCommand, main, runProjectCommand, ...
289
+ POSSIBLY AFFECTED (1) reachable only through unverified call edges:
290
+ doctor
291
+
292
+ Uncovered (10): runGlobCommand, main, runProjectCommand, runFileCommand, evaluateRepo, ...
228
293
  ⚠ These affected functions have no test references
229
294
 
230
- Summary: 15 affected → 19 test files, 5/15 functions covered (33%)
295
+ Summary: 15 affected → 20 test files, 5/15 functions covered (33%) · 1 possibly affected (unverified chains)
231
296
  ```
232
297
 
298
+ The confirmed closure is what you run; `POSSIBLY AFFECTED` lists functions reached only through unverified edges — extra tests worth a look, kept separate.
299
+
233
300
  ## Find unused code
234
301
 
235
302
  ```
236
303
  $ ucn deadcode --exclude=test
237
304
 
238
- Dead code: 3 unused symbol(s)
305
+ Dead code: 2 unused symbol(s)
239
306
 
240
- core/bridge.js
241
- [ 90- 92] endsWithWildcard (function)
242
- [ 258- 265] parsePythonDecorator (function)
243
- core/search.js
244
- [1409-1445] _testBodyReferencesClass (function)
307
+ core/output/analysis.js
308
+ [ 29- 32] formatHistogramLine (function)
309
+ [ 41- 44] shouldShowReachability (function)
245
310
 
246
- 325 exported symbol(s) excluded (all have callers). Use --include-exported to audit them.
311
+ 353 exported symbol(s) excluded (all have callers). Use --include-exported to audit them.
247
312
  ```
248
313
 
249
314
  Find missing-await bugs:
@@ -297,6 +362,7 @@ function compareNames(a, b) {
297
362
  - **Coverage** - every command, every supported language, every surface (CLI, MCP, interactive)
298
363
  - **Systematic** - a harness exercises all command and flag combinations against real multi-language fixtures
299
364
  - **Test types** - unit, integration, per-language regression, formatter, cache, MCP edge cases, architecture parity guards
365
+ - **Ground truth** - caller accuracy is measured against ts-morph, pyright, gopls, rust-analyzer, and jdtls on 10 pinned real repos, gated on zero unexplained edges (see [Answers you can trust](#answers-you-can-trust))
300
366
 
301
367
  ---
302
368
 
@@ -369,8 +435,9 @@ Run `ucn --help` for the full command list and flags.
369
435
 
370
436
  - Single-project scope - follows imports within the project, not into `node_modules` or `site-packages`
371
437
  - No runtime execution - static analysis only
372
- - Dynamic dispatch and reflection are only partially visible or invisible
373
- - JS, TS, and Python method calls can be uncertain when receiver type is unknown
438
+ - Reflection (Python `getattr`, Java reflection) is invisible - the target is built at runtime
439
+ - Interface/trait dispatch can't be resolved to a single impl, but candidates are surfaced as `possible-dispatch` in the unverified tier, never silently dropped
440
+ - JS, TS, and Python method calls on an untyped receiver can't always be proven - UCN surfaces these in the UNVERIFIED tier with a reason rather than dropping or guessing them ([Answers you can trust](#answers-you-can-trust))
374
441
  - Large repos take a few seconds on the first query, then use cache
375
442
 
376
443
  If you need compiler diagnostics, taint analysis, or runtime semantics, those are different tools for different jobs. UCN trades that depth for speed, portability, and zero setup.
package/cli/index.js CHANGED
@@ -197,6 +197,7 @@ function parseFlags(tokens) {
197
197
  includeExported: tokens.includes('--include-exported') || undefined,
198
198
  includeDecorated: tokens.includes('--include-decorated') || undefined,
199
199
  includeUncertain: tokens.includes('--include-uncertain') || undefined,
200
+ expandUnverified: tokens.includes('--expand-unverified') || undefined,
200
201
  includeMethods: tokens.some(a => a === '--include-methods=false' || a === '--no-include-methods') ? false : tokens.some(a => a === '--include-methods' || (a.startsWith('--include-methods=') && a !== '--include-methods=false')) ? true : undefined,
201
202
  detailed: tokens.includes('--detailed') || undefined,
202
203
  topLevel: tokens.includes('--top-level') || undefined,
@@ -279,11 +280,11 @@ flags.followSymlinks = !args.includes('--no-follow-symlinks');
279
280
 
280
281
  // Known flags for validation
281
282
  const knownFlags = new Set([
282
- '--help', '-h', '--mcp',
283
+ '--help', '-h', '--version', '-v', '--mcp',
283
284
  '--json', '--verbose', '--no-quiet', '--quiet',
284
285
  '--code-only', '--with-types', '--top-level', '--exact', '--case-sensitive',
285
286
  '--no-cache', '--clear-cache', '--include-tests', '--exclude-tests',
286
- '--include-exported', '--include-decorated', '--expand', '--interactive', '-i', '--all', '--include-methods', '--no-include-methods', '--include-uncertain', '--detailed', '--calls-only',
287
+ '--include-exported', '--include-decorated', '--expand', '--interactive', '-i', '--all', '--include-methods', '--no-include-methods', '--include-uncertain', '--expand-unverified', '--detailed', '--calls-only',
287
288
  '--file', '--context', '--exclude', '--not', '--in',
288
289
  '--depth', '--direction', '--add-param', '--remove-param', '--rename-to',
289
290
  '--default', '--top', '--no-follow-symlinks',
@@ -303,6 +304,12 @@ if (args.includes('--help') || args.includes('-h')) {
303
304
  process.exit(0);
304
305
  }
305
306
 
307
+ // Handle version flag — read from package.json (single source of truth, shared with MCP serverInfo)
308
+ if (args.includes('--version') || args.includes('-v')) {
309
+ console.log(require('../package.json').version);
310
+ process.exit(0);
311
+ }
312
+
306
313
  // Validate flags
307
314
  const unknownFlags = args.filter(a => {
308
315
  if (!a.startsWith('-')) return false;
@@ -653,6 +660,16 @@ function runProjectCommand(rootDir, command, arg) {
653
660
  console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
654
661
  }
655
662
  }
663
+ // Tiered-output contract: unverified callers are always shown for
664
+ // these commands, so the legacy reveal flags are implied no-ops.
665
+ if (['about', 'context', 'impact', 'trace', 'blast', 'reverseTrace', 'affectedTests'].includes(canonical)) {
666
+ if (flags.includeUncertain) {
667
+ console.error(`Note: --include-uncertain is implied for '${toCliName(canonical)}' — unverified candidates are always shown (tiered).`);
668
+ }
669
+ if (['about', 'context', 'impact'].includes(canonical) && flags.includeMethods) {
670
+ console.error(`Note: --include-methods is implied for '${toCliName(canonical)}' — method calls are tiered by receiver evidence.`);
671
+ }
672
+ }
656
673
  }
657
674
 
658
675
  switch (canonical) {
@@ -1514,7 +1531,7 @@ Common Flags:
1514
1531
  --in=<path> Only in path (e.g., --in=src/core)
1515
1532
  --depth=N Max depth: blast=3, trace=3, reverse-trace=5, graph=2, affected-tests=3
1516
1533
  --direction=X Graph direction: imports, importers, or both (default: both)
1517
- --all Show full results: all callers/callees (about), full tree (trace/blast),
1534
+ --all Show full results: all callers/callees + unverified (about/context), full tree (trace/blast),
1518
1535
  all names (related/find/fn/class/toc), all changed (diff-impact)
1519
1536
  --top=N Limit callers/callees (about), similar functions (related), search results
1520
1537
  --limit=N Limit result count (find, usages, search, deadcode, api, toc, entrypoints, diff-impact)
@@ -1527,8 +1544,13 @@ Common Flags:
1527
1544
  --include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
1528
1545
  --exclude-tests Exclude test files (entrypoints — tests are included by default)
1529
1546
  --class-name=X Scope to specific class (e.g., --class-name=Repository)
1530
- --include-methods Include method calls (obj.fn) in caller/callee analysis
1531
- --include-uncertain Include ambiguous/uncertain matches
1547
+ --include-methods Include method calls (obj.fn) in trace/blast/smart/verify analysis
1548
+ (implied for about/context/impact — method calls are tiered by evidence)
1549
+ --include-uncertain Include ambiguous/uncertain matches in smart/verify
1550
+ (implied for about/context/impact/trace/blast/reverse-trace/affected-tests —
1551
+ unverified candidates always shown, tiered)
1552
+ --expand-unverified Follow unverified caller edges in blast/reverse-trace trees
1553
+ (downstream nodes marked as unverified chains — possible, not confirmed, impact)
1532
1554
  --hide-confidence Hide confidence scores (shown by default in about, context)
1533
1555
  --min-confidence=N Filter low-confidence edges (about, context, blast, trace,
1534
1556
  reverse-trace, smart, affected-tests)
@@ -1561,6 +1583,7 @@ Common Flags:
1561
1583
  --staged Analyze staged changes (diff-impact)
1562
1584
  --no-follow-symlinks Don't follow symbolic links
1563
1585
  -i, --interactive Keep index in memory for multiple queries
1586
+ -v, --version Print the UCN version and exit
1564
1587
 
1565
1588
  Quick Start:
1566
1589
  ucn toc # See project structure