ucn 5.4.2 → 5.5.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 +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
|
@@ -4,62 +4,59 @@ Use this reference when an agent, CI job, or script will make decisions from UCN
|
|
|
4
4
|
|
|
5
5
|
## Evidence classes
|
|
6
6
|
|
|
7
|
-
`confirmed` means UCN found positive identity evidence such as an exact binding, a same-class
|
|
7
|
+
`confirmed` means UCN found positive identity evidence such as an exact binding, a target on the same class definition (a same-name class in another module does not count), a validated receiver type, or an owned import/export chain. It does not mean the edge has a calibrated probability.
|
|
8
8
|
|
|
9
|
-
`unverified` means a syntactic candidate could reach the target but identity could not be established. Preserve and expose these edges. Do not silently treat them as negatives.
|
|
9
|
+
`unverified` means a syntactic candidate could reach the target but identity could not be established. Each carries the engine reason (`possible-dispatch`, `single-owner`, `method-ambiguous`, `overload-ambiguous`, `configuration-alternative`, ...; the per-language meanings are in [resolution-rules.md](resolution-rules.md)). Preserve and expose these edges. Do not silently treat them as negatives.
|
|
10
10
|
|
|
11
|
-
`excluded` means the engine found evidence for a different definition, an incompatible receiver, an external package, an unrelated runtime-language boundary, an arity mismatch, or another stated reason.
|
|
11
|
+
`excluded` means the engine found evidence for a different definition, an incompatible receiver, an external package, an unrelated runtime-language boundary, an arity mismatch, or another stated reason. Excluded sites are counted in `ACCOUNT` as other-target lines and exposed with their reasons under `account.excluded` in JSON. Inspect them when investigating an accuracy issue.
|
|
12
12
|
|
|
13
|
-
`non-call` means the literal name occurred as a definition, import, type reference, property, comment/string, or other text. Use `usages` for the underlying sites; comment/string occurrences appear under `OTHER TEXT` unless `
|
|
13
|
+
`non-call` means the literal name occurred as a definition, import, type reference, property, comment/string, or other text. Use `usages` for the underlying sites; comment/string occurrences appear under `OTHER TEXT` unless `--code-only` (`code_only=true`).
|
|
14
14
|
|
|
15
|
-
`beyond-text` means semantic binding or alias evidence produced an edge that a literal-name ground set could not observe.
|
|
15
|
+
`beyond-text` means semantic binding or alias evidence produced an edge that a literal-name ground set could not observe (Rust `use m::f as g; g()`, a call generated by a macro expansion). A reflective access whose member name is built at runtime from literal fragments (`getattr(self, "_get_%s_perms" % src)`) is also beyond text: it is listed as an unverified caller with reason `reflection-pattern` and the pattern, never confirmed. A Java single-element annotation (`@Marker("x")`) sets the element `value` without naming it: it is listed as an unverified caller of that element with reason `annotation-shorthand`, and `plan` names the element there when the annotation type resolves.
|
|
16
|
+
|
|
17
|
+
## Trust lines
|
|
18
|
+
|
|
19
|
+
Every caller-bearing answer ends with trust lines that truncation preserves:
|
|
20
|
+
|
|
21
|
+
- `ACCOUNT` partitions every line where the name occurs: confirmed, unverified, non-call (import, definition, reference, other text), other-target, unaccounted. In mixed-language repositories it also counts occurrences in unsupported-language source files.
|
|
22
|
+
- `CONTRACT` states the scope of that partition. When unsupported-language files contain the name, it says so and limits the partition-complete claim to supported languages. Semantic completeness is never claimed.
|
|
23
|
+
- `WARNING` identifies unreadable, unparsed, or partially indexed files (including `git-listing-failed`), lines in syntax-error recovery regions, and unsupported-language occurrence sites (file:line plus the line text). `usages` and `tests` carry the same disclosure as a note.
|
|
24
|
+
- `FILTERED` means query options hid evidence.
|
|
25
|
+
- `CALLEE ACCOUNT` partitions a function's call sites (confirmed, unverified, external/builtin, excluded, filtered); `TREE ACCOUNT` does the same for the interior of a caller-direction `trace`.
|
|
16
26
|
|
|
17
27
|
## Conservation scope
|
|
18
28
|
|
|
19
|
-
The caller account partitions a literal-name text-occurrence ground set. A conserved account answers:
|
|
29
|
+
The caller account partitions a literal-name text-occurrence ground set. A conserved account answers: "Did UCN explain every observed literal-name line?" It does not answer: "Did UCN discover every semantically possible runtime caller?"
|
|
20
30
|
|
|
21
31
|
Required automation checks:
|
|
22
32
|
|
|
23
33
|
1. `account.conserved` is true.
|
|
24
34
|
2. `account.contract.textComplete` is true.
|
|
25
|
-
3. No unreadable or unparsed files are reported.
|
|
35
|
+
3. No unreadable or unparsed files are reported, and no ground line sits in a syntax-error recovery region (`account.recovered`, WARNING line).
|
|
26
36
|
4. No contract metadata was lost to truncation.
|
|
27
37
|
5. Unverified and excluded reasons are retained for review.
|
|
28
|
-
6. Compiler/LSP evaluation reports keep configuration-unscored evidence below the release ceiling; a high-precision result from an undersized scored subset is not accepted.
|
|
29
|
-
7. The release review-burden board passes: true edges left unverified, zero-actionable-ambiguity target rate, actionable candidate p95, and effective unverified review items per oracle edge all remain within the published budget. Raw false candidates remain reported separately.
|
|
30
|
-
8. The deterministic cross-command board reports zero disagreements: overlapping `find`, `show`, `source`, `impact`, `tests`, `check`, and caller `trace` claims resolve the same stable handle to the same identity, projections, caller tiers, evidence reasons, totals, and caller account.
|
|
31
38
|
|
|
32
|
-
Even when all
|
|
39
|
+
Even when all five hold, `account.contract.semanticComplete` remains false. Use compiler/type-checker, test, runtime, and framework evidence for semantic decisions.
|
|
33
40
|
|
|
34
|
-
`account.contract.observedTextZero` is safe only for the claim
|
|
41
|
+
`account.contract.observedTextZero` is safe only for the claim "the complete observed literal-name ground set contained no caller candidates or beyond-text edges." It is explicitly not safe-delete proof.
|
|
35
42
|
|
|
36
43
|
## Numeric fields
|
|
37
44
|
|
|
38
|
-
`evidenceScore` (and the
|
|
39
|
-
|
|
40
|
-
## Unverified review burden
|
|
45
|
+
`evidenceScore` (and the `confidence` alias where present) is an ordinal weight used for ordering and thresholding evidence classes. `scoreKind` is `ordinal-evidence-not-probability`. Never display it as measured accuracy or use it as a probability in risk calculations.
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
## Provenance
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
- `zeroActionableUnverifiedTargetRate` is the share of reviewed targets with no actionable ambiguity.
|
|
46
|
-
- `actionableUnverifiedCandidatesP50`, `actionableUnverifiedCandidatesP95`, and `actionableUnverifiedCandidatesMax` measure actionable candidate-set size per target.
|
|
47
|
-
- `unverifiedReviewItemsPerOracleEdge` measures effective review work relative to the oracle workload. Actionable false candidates count individually; each named runtime-dispatch family counts once.
|
|
48
|
-
- `rawFalseUnverifiedPerOracleEdge` preserves the ungrouped false-candidate amplification for auditability.
|
|
49
|
-
- `runtimeDependentOracleEdges`, `compilerDependentOracleEdges`, and `oracleAbstentionEdges` disclose why all-oracle coverage is broader than exact target identity.
|
|
50
|
-
- `unverifiedReasons` groups candidates by the engine reason that kept them out of the confirmed tier.
|
|
49
|
+
Caller and callee JSON carries compact `provenance`: `rule`, contributing `rules` when there is more than one, `validation`, and the receiver's source and origin line when recorded. Incomplete proofs include a `diagnostic`, also shown in text. Callees retain compact `siteProvenance` for each distinct occurrence; their summary uses the weakest confirmed site's rule. Exclusions expose counts by rule and validation under `account.excluded.evidenceSummary`.
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
A lone project owner of a method name does not identify an untyped receiver. Such candidates stay unverified with `single-owner`; importing the class does not change that. `provenance-incomplete` means the available facts cannot establish this declaration. Inspect the site and diagnostic before editing it. Neither reason is permission to discard a possible caller. `validation: unsupported` means the witness collector does not cover that path (for example a wildcard re-export or an external inherited member); the existing classification is kept in report-only mode and is not a validated proof. Missing or inconsistent facts on a supported lookup still route unverified.
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
## Release evaluation
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
Releases are gated on pinned real repositories against independent oracles (ts-morph, Pyright, gopls, rust-analyzer, JDT LS, Roslyn, clangd): 100% in-scope caller and callee recall with zero unaccounted oracle edges, at least 98% confirmed-tier precision, full conservation, zero false-dead claims in the default `deadcode` audit, exactly agreeing cross-command claims for the same handle (`find`, `show`, `source`, `impact`, `tests`, `check`, caller `trace`), bounded unverified review burden (at most 10% of exact true edges left unverified, at least 80% of targets with no actionable ambiguity, at most five actionable candidates at p95), and throughput and memory budgets. Passing these sampled gates is release evidence for the measured static evidence classes; it does not turn dynamic dispatch, generated code, reflection, or external dependencies into complete runtime knowledge.
|
|
57
56
|
|
|
58
57
|
## Truncation
|
|
59
58
|
|
|
60
|
-
CLI and MCP text share the same 10K targeted / 3K broad default budgets and
|
|
61
|
-
100K ceiling. CLI truncation appends preserved contract lines directly. MCP
|
|
62
|
-
results may additionally include:
|
|
59
|
+
CLI and MCP text share the same 10K targeted / 3K broad default budgets and 100K ceiling. Room for every trust line (ACCOUNT, CONTRACT, WARNING, FILTERED, CALLEE ACCOUNT, TREE ACCOUNT) is reserved before any body text, and truncation appends the ones the kept body does not show. A limit that cannot hold them withholds the answer: the text states how many trust lines it shows and the budget that carries all of them, and never shows body text without them. MCP results may additionally include:
|
|
63
60
|
|
|
64
61
|
```json
|
|
65
62
|
{
|
|
@@ -83,7 +80,4 @@ Allow an automated change to proceed to compiler/tests only when:
|
|
|
83
80
|
- the repo health report lists no unsupported source handoff relevant to the change;
|
|
84
81
|
- the change is still validated by the language toolchain and relevant tests.
|
|
85
82
|
|
|
86
|
-
Never auto-delete from a UCN-only signal. Computed dispatch is reported as a
|
|
87
|
-
health blind spot, and modeled registry members are withheld from candidates,
|
|
88
|
-
but neither protection proves runtime reachability. Require usages, entry-point
|
|
89
|
-
review, public API review, and external validation.
|
|
83
|
+
Never auto-delete from a UCN-only signal. Computed dispatch is reported as a health blind spot, and modeled registry members are withheld from candidates, but neither protection proves runtime reachability. C/C++ token-pasting macro invocations and Rust `macro_rules!` invocations that cannot be expanded are reported the same way. Require usages, entry-point review, public API review, and external validation.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UCN - Universal Code Navigator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Auditable answers, with every match counted, unsure ones marked and blind spots named.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/ucn)
|
|
6
6
|
[](https://github.com/mleoca/ucn/actions/workflows/ci.yml)
|
|
@@ -8,7 +8,9 @@ See what code does before you touch it.
|
|
|
8
8
|
|
|
9
9
|
If you work with AI Agents, add UCN as a [Skill or MCP tool](#ai-setup). One tool
|
|
10
10
|
gives the agent compact, source-linked answers to caller, impact, and test
|
|
11
|
-
questions, with uncertainty labeled instead of guessed.
|
|
11
|
+
questions, with uncertainty labeled instead of guessed. The answer depends on
|
|
12
|
+
the indexed source, not on who asks: a person at a terminal and an agent over
|
|
13
|
+
MCP receive the same records for the same question.
|
|
12
14
|
|
|
13
15
|
<img src="assets/readme/ucn-connected-scope.gif" width="736" alt="Conceptual overview of UCN's relationship views, multi-hop exploration, visible uncertainty, index reuse, and refresh after edits. Nodes and timing are illustrative, not a captured query or benchmark.">
|
|
14
16
|
|
|
@@ -103,9 +105,11 @@ Captured findings · [Still image](assets/readme/ucn-evidence-graph.png) · [Dat
|
|
|
103
105
|
|
|
104
106
|
The ACCOUNT line reconciles the observed name occurrences: confirmed calls,
|
|
105
107
|
unverified candidates, non-call occurrences, and matches attributed to another
|
|
106
|
-
target.
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
target. Each occurrence is counted in exactly one category, so the totals can
|
|
109
|
+
be checked line by line rather than taken on trust. CONTRACT describes the
|
|
110
|
+
scope of that accounting. Warnings identify source the index could not cover.
|
|
111
|
+
These details survive text truncation for an agent's output budget; a budget
|
|
112
|
+
too small to hold them withholds the answer and names the budget that would.
|
|
109
113
|
|
|
110
114
|
An empty result therefore means something specific about the inspected code.
|
|
111
115
|
It cannot establish that reflection, generated code, runtime registration, or
|
|
@@ -117,7 +121,7 @@ needs the compiler and tests.
|
|
|
117
121
|
|
|
118
122
|
Release gates compare UCN's answers with independent compilers and language
|
|
119
123
|
servers on a ten-repository board of pinned production codebases. The local
|
|
120
|
-
|
|
124
|
+
October 4, 2026 evaluation recorded these sampled caller results:
|
|
121
125
|
|
|
122
126
|
| Repository | Pinned commit | Oracle | Symbols sampled | Confirmed precision | In-scope recall |
|
|
123
127
|
|---|---|---|---:|---:|---:|
|
|
@@ -134,10 +138,10 @@ September 6, 2026 evaluation recorded these sampled caller results:
|
|
|
134
138
|
|
|
135
139
|
That evaluation reported zero missing in-scope oracle edges in both caller
|
|
136
140
|
and callee answers, and 8,000 cross-command comparisons with zero disagreements.
|
|
137
|
-
The default dead-code audit found zero false-dead results among
|
|
138
|
-
claims;
|
|
141
|
+
The default dead-code audit found zero false-dead results among 9 scored
|
|
142
|
+
claims; 4 additional claims could not be pinned by the oracle and were unscored.
|
|
139
143
|
All ten repositories passed the performance budgets, with steady-state query
|
|
140
|
-
p95 from 4.
|
|
144
|
+
p95 from 4.7 to 101.1 ms. Those timings exclude process startup and indexing;
|
|
141
145
|
cold builds and cache loading are measured separately.
|
|
142
146
|
|
|
143
147
|
The samples are deterministic and stratified by reference activity. Confirmed
|
|
@@ -146,9 +150,9 @@ in either the confirmed or unverified band. Unverified candidates, oracle
|
|
|
146
150
|
abstentions, and unscored findings remain separate. These measurements do not
|
|
147
151
|
establish complete runtime knowledge or identical performance on every machine.
|
|
148
152
|
|
|
149
|
-
The scheduled board covers
|
|
153
|
+
The scheduled board covers 25 pinned repositories: the ten above plus zod,
|
|
150
154
|
express, hono, zustand, fastify, rich, click, attrs, grpc-go, chi, cursive,
|
|
151
|
-
itertools, gson, and jsoup. A rotating fresh-repository arm checks codebases
|
|
155
|
+
itertools, rayon, gson, and jsoup. A rotating fresh-repository arm checks codebases
|
|
152
156
|
outside that pinned board.
|
|
153
157
|
|
|
154
158
|
The [repository manifest](eval/lib/repos.js) records the full commits.
|
|
@@ -188,9 +192,10 @@ npm run trust:gate
|
|
|
188
192
|
|
|
189
193
|
`deps --cycles` groups circular dependencies and distinguishes eager imports
|
|
190
194
|
from deferred or type-only edges. Enumeration limits are disclosed. `repo`
|
|
191
|
-
reports source coverage as well as project structure; its
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
reports source coverage as well as project structure; its HOT ranking counts
|
|
196
|
+
confirmed production callers exactly (a refinement budget only binds on very
|
|
197
|
+
large repositories, and is disclosed), and `repo --sections=stats --hot` ranks
|
|
198
|
+
by all callers.
|
|
194
199
|
|
|
195
200
|
`endpoints --bridge` matches server routes and client requests recognized by
|
|
196
201
|
its framework extractors. `plan` handles code relationships
|
|
@@ -208,6 +213,8 @@ exits 2, and a successful listing exits 0. `--lines` supports `find`, `show`,
|
|
|
208
213
|
`usages`, `search`, and `impact`; `show --lines` lists callers by default.
|
|
209
214
|
Use `--json` when the script needs structured fields or a tree result.
|
|
210
215
|
|
|
216
|
+
Listings are deterministic for a given source tree: a fresh build, a cache
|
|
217
|
+
load, and an incremental rebuild return the same records in the same order.
|
|
211
218
|
Listings have no default row cap. Explicit limits disclose what they omit,
|
|
212
219
|
and a shell-mode character budget fails before writing partial output.
|
|
213
220
|
`source --raw` extracts complete functions and classes unless an explicit
|
|
@@ -215,12 +222,16 @@ line limit is requested; any resulting truncation is reported on stderr.
|
|
|
215
222
|
|
|
216
223
|
Ordinary text-mode command errors also exit 2. JSON keeps exit 0 for successful
|
|
217
224
|
empty results and exit 1 for command errors (`meta.ok: false` plus `error`).
|
|
225
|
+
An exception inside UCN is reported as `Internal error: ...` (JSON adds
|
|
226
|
+
`meta.internalError: true`), never as an answer about the code.
|
|
218
227
|
Target-less `check` exits 1 when `TRUST` is `BLOCKED`, 0 for other completed
|
|
219
228
|
checks, and 2 if it could not run.
|
|
220
229
|
|
|
221
230
|
Outside `--lines`, `find`, text `search`, `deadcode`, `api`, and
|
|
222
|
-
`repo --sections=files` default to a maximum of 500 results.
|
|
223
|
-
|
|
231
|
+
`repo --sections=files` default to a maximum of 500 results. `find` text shows
|
|
232
|
+
the top 5 in detail and counts the rest (JSON returns up to 500); `--limit=N`
|
|
233
|
+
raises both. Use `--limit=N` to request more; `usages` lists every site,
|
|
234
|
+
test files included, unless a limit or `--exclude-tests` is given. Broad `find` queries select candidates by approximate usage
|
|
224
235
|
totals before calculating definition-pinned caller activity, and disclose
|
|
225
236
|
that selection when limited.
|
|
226
237
|
|
package/cli/index.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* Auto-detects mode from target (file path → file mode, directory → project mode).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// Reuse V8 code caches across processes (Node >= 22.1; fix #365): compiling
|
|
11
|
+
// the engine's modules is a fixed cost of every command. Node validates each
|
|
12
|
+
// entry against the source, and NODE_DISABLE_COMPILE_CACHE turns it off.
|
|
13
|
+
try { require('module').enableCompileCache?.(); } catch (_) { /* optional */ }
|
|
14
|
+
|
|
10
15
|
const fs = require('fs');
|
|
11
16
|
const path = require('path');
|
|
12
17
|
|
|
@@ -28,6 +33,7 @@ const {
|
|
|
28
33
|
} = require('../core/registry');
|
|
29
34
|
const { buildPublicParams, isPublicCommand } = require('../core/public-command');
|
|
30
35
|
const { execute } = require('../core/execute');
|
|
36
|
+
const { describeError } = require('../core/errors');
|
|
31
37
|
const { applyOutputBudget, MAX_OUTPUT_CHARS } = require('../core/output-budget');
|
|
32
38
|
const { clearAllCaches } = require('../core/cache');
|
|
33
39
|
const { commentLines } = require('../core/output/lines');
|
|
@@ -188,7 +194,7 @@ function validateNumericFlags(flags) {
|
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
/** Emit the one CLI error contract, including failures before main dispatch. */
|
|
191
|
-
function emitCliError(msg, command = activeCanonicalCommand) {
|
|
197
|
+
function emitCliError(msg, command = activeCanonicalCommand, { internal = false } = {}) {
|
|
192
198
|
const wantsJson = process.argv.includes('--json');
|
|
193
199
|
const error = typeof msg === 'string' ? msg : String(msg);
|
|
194
200
|
if (wantsJson) {
|
|
@@ -201,6 +207,8 @@ function emitCliError(msg, command = activeCanonicalCommand) {
|
|
|
201
207
|
const env = {
|
|
202
208
|
meta: {
|
|
203
209
|
ok: false,
|
|
210
|
+
// An exception inside UCN, not a refusal (fix #394).
|
|
211
|
+
...(internal && { internalError: true }),
|
|
204
212
|
...(surfaceCommand && {
|
|
205
213
|
command: surfaceCommand,
|
|
206
214
|
...(canonical && surfaceCommand !== canonical && {
|
|
@@ -223,11 +231,24 @@ function emitCliError(msg, command = activeCanonicalCommand) {
|
|
|
223
231
|
* Print an error message and abort command execution. Throw instead of calling
|
|
224
232
|
* process.exit so index/cache finally blocks still run.
|
|
225
233
|
*/
|
|
226
|
-
function fail(msg, command = activeCanonicalCommand) {
|
|
227
|
-
emitCliError(msg, command);
|
|
234
|
+
function fail(msg, command = activeCanonicalCommand, options = {}) {
|
|
235
|
+
emitCliError(msg, command, options);
|
|
228
236
|
throw new CommandError();
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/** Fail with an execute() error: refusals are rendered for the CLI, internal errors kept verbatim. */
|
|
240
|
+
function failExecution(execution) {
|
|
241
|
+
if (execution.internalError) fail(execution.error, activeCanonicalCommand, { internal: true });
|
|
242
|
+
fail(formatSurfaceMessage(execution.error, 'cli'));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Report an exception caught outside execute(). */
|
|
246
|
+
function emitCaughtError(e) {
|
|
247
|
+
const described = describeError(e);
|
|
248
|
+
emitCliError(described.internalError ? described.error : `Error: ${described.error}`,
|
|
249
|
+
activeCanonicalCommand, { internal: !!described.internalError });
|
|
250
|
+
}
|
|
251
|
+
|
|
231
252
|
// ============================================================================
|
|
232
253
|
// ARGUMENT PARSING
|
|
233
254
|
// ============================================================================
|
|
@@ -607,7 +628,7 @@ function main() {
|
|
|
607
628
|
}
|
|
608
629
|
} catch (e) {
|
|
609
630
|
if (!(e instanceof CommandError)) {
|
|
610
|
-
|
|
631
|
+
emitCaughtError(e);
|
|
611
632
|
}
|
|
612
633
|
process.exitCode = flags.json ? 1 : 2;
|
|
613
634
|
}
|
|
@@ -717,8 +738,8 @@ function runFileCommand(filePath, command, arg) {
|
|
|
717
738
|
warnInapplicableFlags(canonical, scopedFlags, (message) => console.error(message));
|
|
718
739
|
const params = buildPublicParams(canonical, arg, scopedFlags);
|
|
719
740
|
const execution = execute(index, canonical, params);
|
|
720
|
-
const { ok, result
|
|
721
|
-
if (!ok)
|
|
741
|
+
const { ok, result } = execution;
|
|
742
|
+
if (!ok) failExecution(execution);
|
|
722
743
|
emitCliText(flags.json
|
|
723
744
|
? output.formatPublicJson(canonical, result, params, {
|
|
724
745
|
...execution, surface: 'cli',
|
|
@@ -799,7 +820,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
799
820
|
});
|
|
800
821
|
const publicExecution = execute(index, canonical, publicParams);
|
|
801
822
|
if (!publicExecution.ok) {
|
|
802
|
-
|
|
823
|
+
failExecution(publicExecution);
|
|
803
824
|
}
|
|
804
825
|
emitCliText(flags.json
|
|
805
826
|
? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
|
|
@@ -813,7 +834,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
813
834
|
resultExitCode(canonical, publicExecution.result));
|
|
814
835
|
} catch (e) {
|
|
815
836
|
if (!(e instanceof CommandError)) {
|
|
816
|
-
|
|
837
|
+
emitCaughtError(e);
|
|
817
838
|
}
|
|
818
839
|
process.exitCode = flags.json ? 1 : 2;
|
|
819
840
|
} finally {
|
|
@@ -822,7 +843,12 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
822
843
|
// On cache-hit runs, only re-save if callsCache was mutated OR
|
|
823
844
|
// reachability was computed (MED-1: persists the BFS result so
|
|
824
845
|
// subsequent cold invocations don't repeat the 7-11s tax).
|
|
825
|
-
|
|
846
|
+
// Files whose macro-expansion calls the command derived in part are
|
|
847
|
+
// derived whole first, so the next invocation reads them persisted.
|
|
848
|
+
if (flags.cache) {
|
|
849
|
+
try { index.completeMacroCallDerivations(); } catch (e) { /* best-effort */ }
|
|
850
|
+
}
|
|
851
|
+
if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty || index.macroExpansionDirty)) {
|
|
826
852
|
try { index.saveCache(); } catch (e) { /* best-effort */ }
|
|
827
853
|
}
|
|
828
854
|
if (flags.cache && index.usageCacheDirty) {
|
|
@@ -866,7 +892,7 @@ function runGlobCommand(pattern, command, arg) {
|
|
|
866
892
|
const publicParams = buildPublicParams(canonical, arg, flags);
|
|
867
893
|
const publicExecution = execute(index, canonical, publicParams);
|
|
868
894
|
if (!publicExecution.ok) {
|
|
869
|
-
|
|
895
|
+
failExecution(publicExecution);
|
|
870
896
|
}
|
|
871
897
|
emitCliText(flags.json
|
|
872
898
|
? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
|
|
@@ -906,7 +932,10 @@ Commands:
|
|
|
906
932
|
show <symbol> Symbol summary and relationships
|
|
907
933
|
--sections=summary,callers,callees,source,dependencies,tests,types,example,related
|
|
908
934
|
find <name> Definitions; --type=type, --with-source
|
|
909
|
-
|
|
935
|
+
text: top 5 in detail + a count of the rest; --json: up to 500;
|
|
936
|
+
--limit=N raises both
|
|
937
|
+
usages <name> Calls, imports, definitions, references (test files
|
|
938
|
+
included; --exclude-tests hides them with a count)
|
|
910
939
|
search [term] Text or structural search
|
|
911
940
|
literal text by default; --regex enables regular-expression syntax
|
|
912
941
|
source <symbol|file:range> Exact function, class, or line extraction
|
|
@@ -1045,7 +1074,7 @@ function runInteractive(rootDir) {
|
|
|
1045
1074
|
Commands:
|
|
1046
1075
|
repo Repository overview (--sections=files,stats,health)
|
|
1047
1076
|
show <name> Symbol summary + relationships (--sections=...)
|
|
1048
|
-
find <name> Find definitions (--type=type, --with-source)
|
|
1077
|
+
find <name> Find definitions (--type=type, --with-source; top 5 shown, --limit=N)
|
|
1049
1078
|
usages <name> All usages grouped by type
|
|
1050
1079
|
source <target> Extract a symbol or file:line-range
|
|
1051
1080
|
trace <name> Call tree (--direction=callees|callers, --to=entrypoints)
|
|
@@ -1155,7 +1184,8 @@ Flags can be added per-command: show myFunc --sections=source,callers
|
|
|
1155
1184
|
if (e instanceof FlagValidationError) {
|
|
1156
1185
|
console.log(e.message);
|
|
1157
1186
|
} else {
|
|
1158
|
-
|
|
1187
|
+
const described = describeError(e);
|
|
1188
|
+
console.error(described.internalError ? described.error : `Error: ${described.error}`);
|
|
1159
1189
|
}
|
|
1160
1190
|
}
|
|
1161
1191
|
|
|
@@ -1177,7 +1207,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
|
|
|
1177
1207
|
const publicParams = buildPublicParams(command, arg, iflags);
|
|
1178
1208
|
const publicExecution = execute(index, command, publicParams);
|
|
1179
1209
|
if (!publicExecution.ok) {
|
|
1180
|
-
console.log(
|
|
1210
|
+
console.log(publicExecution.internalError ? publicExecution.error
|
|
1211
|
+
: formatSurfaceMessage(publicExecution.error, 'cli'));
|
|
1181
1212
|
return;
|
|
1182
1213
|
}
|
|
1183
1214
|
console.log(formatCliText(
|
package/core/accessors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { codeUnitCompare } = require('./shared');
|
|
4
|
+
const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
5
5
|
const { getParser, safeParse } = require('../languages');
|
|
6
6
|
|
|
7
7
|
function inheritedAccessor(index, enclosing, name, definition) {
|
|
@@ -45,6 +45,85 @@ function accessKind(tree, usage) {
|
|
|
45
45
|
return 'read';
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// How an occurrence without a simple receiver touches a property (fix #397):
|
|
49
|
+
// 'member' (the property of a longer member chain, `this.ctx.body`),
|
|
50
|
+
// { pattern: true, thisSource } (a destructuring key, `const { body } =
|
|
51
|
+
// this`, which reads the property from the source), or 'none' (a bare
|
|
52
|
+
// variable, an object-literal key: never a property access). null when the
|
|
53
|
+
// token cannot be located.
|
|
54
|
+
const MEMBER_ACCESS_TYPES = new Set(['attribute', 'member_expression', 'member_access_expression',
|
|
55
|
+
'field_expression', 'selector_expression', 'field_access']);
|
|
56
|
+
function receiverlessShape(tree, usage) {
|
|
57
|
+
if (!tree || !Number.isInteger(usage.column)) return null;
|
|
58
|
+
const node = tree.rootNode.descendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
59
|
+
if (!node) return null;
|
|
60
|
+
const parent = node.parent;
|
|
61
|
+
if (MEMBER_ACCESS_TYPES.has(parent?.type) && node.startIndex > parent.startIndex) return 'member';
|
|
62
|
+
let key = null;
|
|
63
|
+
if (node.type === 'shorthand_property_identifier_pattern') key = node;
|
|
64
|
+
else if (parent?.type === 'pair_pattern' && parent.childForFieldName('key')?.startIndex === node.startIndex) key = node;
|
|
65
|
+
if (key) {
|
|
66
|
+
let pattern = key.parent;
|
|
67
|
+
if (pattern?.type === 'pair_pattern' || pattern?.type === 'object_assignment_pattern') pattern = pattern.parent;
|
|
68
|
+
const holder = pattern?.type === 'object_pattern' ? pattern.parent : null;
|
|
69
|
+
const source = holder?.type === 'variable_declarator' ? holder.childForFieldName('value') : null;
|
|
70
|
+
return { pattern: true, thisSource: source?.type === 'this',
|
|
71
|
+
shorthand: key.type === 'shorthand_property_identifier_pattern' };
|
|
72
|
+
}
|
|
73
|
+
return 'none';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Source offset of an occurrence (fix #398F): orders it against same-line
|
|
77
|
+
// assignments when its receiver is typed by return-type flow.
|
|
78
|
+
function flowSiteOf(tree, usage) {
|
|
79
|
+
if (!tree || !Number.isInteger(usage.column)) return null;
|
|
80
|
+
const node = tree.rootNode.descendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
81
|
+
return node ? node.startIndex : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The enclosing callable scopes of a line, outermost first, as the parser's
|
|
85
|
+
// call records spell them (`enclosingFunction.scopeChain`): the innermost
|
|
86
|
+
// scope any record of the file reports around the line (anonymous callbacks
|
|
87
|
+
// included), else the indexed callables containing it.
|
|
88
|
+
function scopeChainAt(index, file, line, calls) {
|
|
89
|
+
let best = null;
|
|
90
|
+
for (const call of calls || []) {
|
|
91
|
+
const scope = call.enclosingFunction;
|
|
92
|
+
if (!scope || scope.startLine > line || scope.endLine < line) continue;
|
|
93
|
+
if (!best || scope.startLine > best.startLine ||
|
|
94
|
+
(scope.startLine === best.startLine && scope.endLine < best.endLine)) best = scope;
|
|
95
|
+
}
|
|
96
|
+
if (best && Array.isArray(best.scopeChain)) return best.scopeChain;
|
|
97
|
+
const scopes = (index.files.get(file)?.symbols || []).filter(symbol =>
|
|
98
|
+
CALLABLE_SYMBOL_KINDS.has(symbol.type) && symbol.startLine <= line && line <= symbol.endLine)
|
|
99
|
+
.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
100
|
+
return scopes.map(symbol => symbol.startLine);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A plain local receiver typed by return-type flow (`result =
|
|
104
|
+
// runner.invoke(..)` with `invoke(..) -> Result`), the evidence the caller
|
|
105
|
+
// engine uses for method calls on the same receiver (fix #398F).
|
|
106
|
+
function flowReceiverType(index, ref, flowMaps) {
|
|
107
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(ref.receiver || '') || ['self', 'cls', 'this'].includes(ref.receiver)) return null;
|
|
108
|
+
const callers = require('./callers');
|
|
109
|
+
let state = flowMaps.get(ref.file);
|
|
110
|
+
if (state === undefined) {
|
|
111
|
+
const calls = callers.getCachedCalls(index, ref.file);
|
|
112
|
+
state = calls ? { calls, map: callers._buildReturnTypeFlowMap(index, ref.file, calls) } : null;
|
|
113
|
+
flowMaps.set(ref.file, state);
|
|
114
|
+
}
|
|
115
|
+
const map = state?.map;
|
|
116
|
+
if (!map) return null;
|
|
117
|
+
const chain = scopeChainAt(index, ref.file, ref.line, state.calls);
|
|
118
|
+
const entry = callers._lookupReturnTypeFlow(map, {
|
|
119
|
+
receiver: ref.receiver, line: ref.line,
|
|
120
|
+
...(Number.isInteger(ref.flowSite) && { callStart: ref.flowSite }),
|
|
121
|
+
enclosingFunction: chain.length ? { startLine: chain[chain.length - 1], scopeChain: chain } : null,
|
|
122
|
+
});
|
|
123
|
+
if (!entry?.type || entry.invalidated || !entry.fromFile) return null;
|
|
124
|
+
return { type: entry.type, fromFile: entry.fromFile };
|
|
125
|
+
}
|
|
126
|
+
|
|
48
127
|
// Descriptors/properties are consumed through reads and writes, not only
|
|
49
128
|
// call syntax. Keep this vocabulary shared by impact and refactoring so the
|
|
50
129
|
// two commands cannot disagree about whether a selected symbol is an accessor.
|
|
@@ -61,6 +140,8 @@ function isAccessorDefinition(definition) {
|
|
|
61
140
|
|
|
62
141
|
function ownerName(definition) {
|
|
63
142
|
if (definition.className) return definition.className;
|
|
143
|
+
// An object-literal accessor's owner is the literal (fix #397).
|
|
144
|
+
if (definition.objectLiteralLine && definition.registryContainer) return definition.registryContainer;
|
|
64
145
|
if (!definition.receiver) return null;
|
|
65
146
|
return String(definition.receiver)
|
|
66
147
|
.replace(/^[*&]\s*/, '')
|
|
@@ -89,11 +170,19 @@ function typeMatchesOwner(index, typeName, contextFile, owner, ownerFile) {
|
|
|
89
170
|
path.resolve(ownerDefs[0].file) === path.resolve(ownerFile);
|
|
90
171
|
}
|
|
91
172
|
|
|
173
|
+
// Members of the same object literal (fix #397): `this` in one names the
|
|
174
|
+
// object the literal builds (or an object inheriting from it).
|
|
175
|
+
function sameObjectLiteral(a, b) {
|
|
176
|
+
return !!a && !!b && !!a.objectLiteralLine && a.objectLiteralLine === b.objectLiteralLine &&
|
|
177
|
+
a.file === b.file;
|
|
178
|
+
}
|
|
179
|
+
|
|
92
180
|
function insideSelectedAccessorBody(index, name, definition, file, line) {
|
|
93
181
|
return (index.symbols.get(name) || []).some(candidate =>
|
|
94
182
|
isAccessorDefinition(candidate) &&
|
|
95
183
|
candidate.file === definition.file &&
|
|
96
184
|
candidate.className === definition.className &&
|
|
185
|
+
(candidate.objectLiteralLine || null) === (definition.objectLiteralLine || null) &&
|
|
97
186
|
file === candidate.file &&
|
|
98
187
|
line >= candidate.startLine &&
|
|
99
188
|
line <= (candidate.endLine || candidate.startLine));
|
|
@@ -114,12 +203,16 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
114
203
|
const confirmed = [];
|
|
115
204
|
const unverified = [];
|
|
116
205
|
const excluded = [];
|
|
206
|
+
const flowMaps = new Map();
|
|
117
207
|
// Use the parser's raw occurrence records rather than usages()' public
|
|
118
208
|
// line-oriented inventory. A line can contain two same-spelled member
|
|
119
209
|
// accesses with different receivers; collapsing them by file+line would
|
|
120
210
|
// make a rename edit one token while silently losing the other.
|
|
121
211
|
const refs = [];
|
|
122
212
|
for (const [file, entry] of index.files) {
|
|
213
|
+
// `files` (fix #398F): a caller that needs one file family (tests)
|
|
214
|
+
// never parses the rest of the project.
|
|
215
|
+
if (options.files && !options.files.has(file)) continue;
|
|
123
216
|
if (!index.matchesFilters(entry.relativePath, options)) continue;
|
|
124
217
|
let content;
|
|
125
218
|
let occurrences;
|
|
@@ -134,9 +227,16 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
134
227
|
const lines = content.split('\n');
|
|
135
228
|
for (const usage of occurrences) {
|
|
136
229
|
if (usage.usageType !== 'reference') continue;
|
|
230
|
+
let shape = null;
|
|
231
|
+
if (!usage.receiver) {
|
|
232
|
+
shape = receiverlessShape(tree, usage);
|
|
233
|
+
if (shape === 'none') continue;
|
|
234
|
+
}
|
|
137
235
|
refs.push({
|
|
138
236
|
...usage,
|
|
139
|
-
|
|
237
|
+
...(shape?.pattern && { patternKey: shape, ...(shape.thisSource && { receiver: 'this' }) }),
|
|
238
|
+
accessKind: shape?.pattern ? 'read' : accessKind(tree, usage),
|
|
239
|
+
...(usage.receiver && { flowSite: flowSiteOf(tree, usage) }),
|
|
140
240
|
file,
|
|
141
241
|
relativePath: entry.relativePath,
|
|
142
242
|
content: lines[usage.line - 1] || '',
|
|
@@ -154,6 +254,7 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
154
254
|
accessKind: ref.accessKind,
|
|
155
255
|
...(Number.isInteger(ref.column) && { column: ref.column }),
|
|
156
256
|
...(ref.receiver && { receiver: ref.receiver }),
|
|
257
|
+
...(ref.patternKey && { patternKey: { shorthand: !!ref.patternKey.shorthand } }),
|
|
157
258
|
};
|
|
158
259
|
|
|
159
260
|
// A backing-store attribute inside the selected getter/setter body
|
|
@@ -168,7 +269,9 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
168
269
|
const enclosing = index.findEnclosingFunction(ref.file, ref.line, true);
|
|
169
270
|
const receiver = ref.receiver || null;
|
|
170
271
|
if (receiver && ['self', 'cls', 'this'].includes(receiver) &&
|
|
171
|
-
((enclosing?.className === owner &&
|
|
272
|
+
((definition.className && enclosing?.className === owner &&
|
|
273
|
+
enclosing.file === definition.file) ||
|
|
274
|
+
(!definition.className && sameObjectLiteral(enclosing, definition)) ||
|
|
172
275
|
inheritedAccessor(index, enclosing, name, definition))) {
|
|
173
276
|
confirmed.push({
|
|
174
277
|
...shaped,
|
|
@@ -195,6 +298,18 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
195
298
|
});
|
|
196
299
|
continue;
|
|
197
300
|
}
|
|
301
|
+
const flowed = !receiverType && receiver ? flowReceiverType(index, ref, flowMaps) : null;
|
|
302
|
+
if (flowed && bareTypeName(flowed.type) === owner &&
|
|
303
|
+
path.resolve(flowed.fromFile) === path.resolve(definition.file)) {
|
|
304
|
+
confirmed.push({
|
|
305
|
+
...shaped,
|
|
306
|
+
callerName: enclosing?.name || null,
|
|
307
|
+
receiverType: flowed.type,
|
|
308
|
+
resolution: 'receiver-return-flow',
|
|
309
|
+
tier: 'confirmed',
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
198
313
|
if (receiver && typeMatchesOwner(index, receiver, ref.file, owner, definition.file)) {
|
|
199
314
|
confirmed.push({
|
|
200
315
|
...shaped,
|