ucn 5.4.2 → 5.5.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.
Files changed (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +40 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +457 -60
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +2113 -0
  62. package/core/rust-modules.js +185 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1161 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2237 -115
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. 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 target, a validated receiver type, or an owned import/export chain. It does not mean the edge has a calibrated probability.
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. Inspect excluded reasons when investigating an accuracy issue.
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 `codeOnly=true`.
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: “Did UCN explain every observed literal-name line?” It does not answer: “Did UCN discover every semantically possible runtime caller?”
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 eight hold, `account.contract.semanticComplete` remains false. Use compiler/type-checker, test, runtime, and framework evidence for semantic decisions.
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 “the complete observed literal-name ground set contained no caller candidates or beyond-text edges.” It is explicitly not safe-delete proof.
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 legacy `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.
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
- Semantic recall can be complete while the result is still impractical: a true edge may be present only in a large unverified set. Release evaluation therefore measures both placement and the amount of review an agent must perform.
47
+ ## Provenance
43
48
 
44
- - `trueEdgeUnverifiedRate` is the share of statically exact oracle edges found only in the unverified tier. Runtime-polymorphic, compiler-dependent template, and oracle-unresolved references remain in the all-oracle view but never inflate the exact denominator.
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
- Configuration-gated candidates remain visible but are not labeled false when the compiler/LSP oracle did not score them. The raw JSON rollup is the source of truth for these fields; the Markdown report is generated from the same data.
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
- The publish-blocking portable-AST ceilings are: at most 10% of exact true edges left unverified, at most 20% within any sufficiently sampled symbol kind, at least 80% of targets with zero actionable ambiguity, actionable-candidate p95 at most five, and at most 0.10 effective review items per oracle edge. The semantic gate separately requires 100% in-scope recall, at least 98% confirmed-tier precision, full conservation, and 100% public-command proof recall. Three class-aware floors gate at zero: unaccounted oracle edges of the exact, runtime-dispatch, and oracle-unresolved classes (caller and callee arms; compile-time-dispatch attributions stay report-only because their non-exact band varies with the compiler oracle's environment), runtime-dispatch oracle edges excluded with reason (possible-dispatch routing is demote-only), and deferred oracle-unresolved edges that exact definition lookup pins to the target re-enter the gate-bearing universe. Passing these sampled floors is release evidence, not runtime-completeness proof.
53
+ ## Release evaluation
55
54
 
56
- The performance gate runs each pinned repository in three independent processes with a fixed worker shape. Publish and PR gates require both CPU and wall-throughput floors on the median run, pin the expected file/LOC workload, and use the worst observed build/full-board peak RSS so memory failures cannot be averaged away. Exploratory one-process runs keep wall throughput diagnostic. Scoped reports are separate from the full release artifact; the dated rollup is release-qualified only when every required row came from a full release invocation. The composite release gate runs this stage first so verbose compiler-oracle output and report consumers cannot perturb its samples.
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
@@ -105,7 +105,8 @@ The ACCOUNT line reconciles the observed name occurrences: confirmed calls,
105
105
  unverified candidates, non-call occurrences, and matches attributed to another
106
106
  target. CONTRACT describes the scope of that accounting. Warnings identify
107
107
  source the index could not cover. These details survive text truncation for
108
- an agent's output budget.
108
+ an agent's output budget; a budget too small to hold them withholds the answer
109
+ and names the budget that would.
109
110
 
110
111
  An empty result therefore means something specific about the inspected code.
111
112
  It cannot establish that reflection, generated code, runtime registration, or
@@ -117,7 +118,7 @@ needs the compiler and tests.
117
118
 
118
119
  Release gates compare UCN's answers with independent compilers and language
119
120
  servers on a ten-repository board of pinned production codebases. The local
120
- September 6, 2026 evaluation recorded these sampled caller results:
121
+ October 4, 2026 evaluation recorded these sampled caller results:
121
122
 
122
123
  | Repository | Pinned commit | Oracle | Symbols sampled | Confirmed precision | In-scope recall |
123
124
  |---|---|---|---:|---:|---:|
@@ -134,10 +135,10 @@ September 6, 2026 evaluation recorded these sampled caller results:
134
135
 
135
136
  That evaluation reported zero missing in-scope oracle edges in both caller
136
137
  and callee answers, and 8,000 cross-command comparisons with zero disagreements.
137
- The default dead-code audit found zero false-dead results among 13 scored
138
- claims; 13 additional claims could not be pinned by the oracle and were unscored.
138
+ The default dead-code audit found zero false-dead results among 9 scored
139
+ claims; 4 additional claims could not be pinned by the oracle and were unscored.
139
140
  All ten repositories passed the performance budgets, with steady-state query
140
- p95 from 4.5 to 76.2 ms. Those timings exclude process startup and indexing;
141
+ p95 from 4.7 to 101.1 ms. Those timings exclude process startup and indexing;
141
142
  cold builds and cache loading are measured separately.
142
143
 
143
144
  The samples are deterministic and stratified by reference activity. Confirmed
@@ -146,9 +147,9 @@ in either the confirmed or unverified band. Unverified candidates, oracle
146
147
  abstentions, and unscored findings remain separate. These measurements do not
147
148
  establish complete runtime knowledge or identical performance on every machine.
148
149
 
149
- The scheduled board covers 24 pinned repositories: the ten above plus zod,
150
+ The scheduled board covers 25 pinned repositories: the ten above plus zod,
150
151
  express, hono, zustand, fastify, rich, click, attrs, grpc-go, chi, cursive,
151
- itertools, gson, and jsoup. A rotating fresh-repository arm checks codebases
152
+ itertools, rayon, gson, and jsoup. A rotating fresh-repository arm checks codebases
152
153
  outside that pinned board.
153
154
 
154
155
  The [repository manifest](eval/lib/repos.js) records the full commits.
@@ -188,9 +189,10 @@ npm run trust:gate
188
189
 
189
190
  `deps --cycles` groups circular dependencies and distinguishes eager imports
190
191
  from deferred or type-only edges. Enumeration limits are disclosed. `repo`
191
- reports source coverage as well as project structure; its quick HOT ranking
192
- has a disclosed refinement budget, and `repo --sections=stats --hot` requests
193
- the exact ranking.
192
+ reports source coverage as well as project structure; its HOT ranking counts
193
+ confirmed production callers exactly (a refinement budget only binds on very
194
+ large repositories, and is disclosed), and `repo --sections=stats --hot` ranks
195
+ by all callers.
194
196
 
195
197
  `endpoints --bridge` matches server routes and client requests recognized by
196
198
  its framework extractors. `plan` handles code relationships
@@ -215,12 +217,16 @@ line limit is requested; any resulting truncation is reported on stderr.
215
217
 
216
218
  Ordinary text-mode command errors also exit 2. JSON keeps exit 0 for successful
217
219
  empty results and exit 1 for command errors (`meta.ok: false` plus `error`).
220
+ An exception inside UCN is reported as `Internal error: ...` (JSON adds
221
+ `meta.internalError: true`), never as an answer about the code.
218
222
  Target-less `check` exits 1 when `TRUST` is `BLOCKED`, 0 for other completed
219
223
  checks, and 2 if it could not run.
220
224
 
221
225
  Outside `--lines`, `find`, text `search`, `deadcode`, `api`, and
222
- `repo --sections=files` default to a maximum of 500 results. Use `--limit=N`
223
- to request more; `usages` lists every site unless a limit is given. Broad `find` queries select candidates by approximate usage
226
+ `repo --sections=files` default to a maximum of 500 results. `find` text shows
227
+ the top 5 in detail and counts the rest (JSON returns up to 500); `--limit=N`
228
+ raises both. Use `--limit=N` to request more; `usages` lists every site,
229
+ test files included, unless a limit or `--exclude-tests` is given. Broad `find` queries select candidates by approximate usage
224
230
  totals before calculating definition-pinned caller activity, and disclose
225
231
  that selection when limited.
226
232
 
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
- emitCliError(`Error: ${e.message}`);
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, error } = execution;
721
- if (!ok) fail(formatSurfaceMessage(error, 'cli'));
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
- fail(formatSurfaceMessage(publicExecution.error, 'cli'));
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
- emitCliError(`Error: ${e.message}`);
837
+ emitCaughtError(e);
817
838
  }
818
839
  process.exitCode = flags.json ? 1 : 2;
819
840
  } finally {
@@ -822,7 +843,7 @@ 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
- if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty)) {
846
+ if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty || index.macroExpansionDirty)) {
826
847
  try { index.saveCache(); } catch (e) { /* best-effort */ }
827
848
  }
828
849
  if (flags.cache && index.usageCacheDirty) {
@@ -866,7 +887,7 @@ function runGlobCommand(pattern, command, arg) {
866
887
  const publicParams = buildPublicParams(canonical, arg, flags);
867
888
  const publicExecution = execute(index, canonical, publicParams);
868
889
  if (!publicExecution.ok) {
869
- fail(formatSurfaceMessage(publicExecution.error, 'cli'));
890
+ failExecution(publicExecution);
870
891
  }
871
892
  emitCliText(flags.json
872
893
  ? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
@@ -906,7 +927,10 @@ Commands:
906
927
  show <symbol> Symbol summary and relationships
907
928
  --sections=summary,callers,callees,source,dependencies,tests,types,example,related
908
929
  find <name> Definitions; --type=type, --with-source
909
- usages <name> Calls, imports, definitions, references
930
+ text: top 5 in detail + a count of the rest; --json: up to 500;
931
+ --limit=N raises both
932
+ usages <name> Calls, imports, definitions, references (test files
933
+ included; --exclude-tests hides them with a count)
910
934
  search [term] Text or structural search
911
935
  literal text by default; --regex enables regular-expression syntax
912
936
  source <symbol|file:range> Exact function, class, or line extraction
@@ -1045,7 +1069,7 @@ function runInteractive(rootDir) {
1045
1069
  Commands:
1046
1070
  repo Repository overview (--sections=files,stats,health)
1047
1071
  show <name> Symbol summary + relationships (--sections=...)
1048
- find <name> Find definitions (--type=type, --with-source)
1072
+ find <name> Find definitions (--type=type, --with-source; top 5 shown, --limit=N)
1049
1073
  usages <name> All usages grouped by type
1050
1074
  source <target> Extract a symbol or file:line-range
1051
1075
  trace <name> Call tree (--direction=callees|callers, --to=entrypoints)
@@ -1155,7 +1179,8 @@ Flags can be added per-command: show myFunc --sections=source,callers
1155
1179
  if (e instanceof FlagValidationError) {
1156
1180
  console.log(e.message);
1157
1181
  } else {
1158
- console.error(`Error: ${e.message}`);
1182
+ const described = describeError(e);
1183
+ console.error(described.internalError ? described.error : `Error: ${described.error}`);
1159
1184
  }
1160
1185
  }
1161
1186
 
@@ -1177,7 +1202,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}) {
1177
1202
  const publicParams = buildPublicParams(command, arg, iflags);
1178
1203
  const publicExecution = execute(index, command, publicParams);
1179
1204
  if (!publicExecution.ok) {
1180
- console.log(formatSurfaceMessage(publicExecution.error, 'cli'));
1205
+ console.log(publicExecution.internalError ? publicExecution.error
1206
+ : formatSurfaceMessage(publicExecution.error, 'cli'));
1181
1207
  return;
1182
1208
  }
1183
1209
  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
- accessKind: accessKind(tree, usage),
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 && enclosing.file === definition.file) ||
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,
package/core/account.js CHANGED
@@ -69,6 +69,7 @@ const UNSUPPORTED_SITE_TEXT_MAX = 160;
69
69
  * fileCount: number, // files (indexed + unparsed + unsupported) with >= 1 matching line
70
70
  * perFile: Map<string, number[]>, // absPath -> sorted 1-indexed line numbers (indexed files only)
71
71
  * unparsed: { fileCount: number, lines: number, files: string[] }, // relative paths
72
+ * recovered: { fileCount, lines, files, sites }, // matched lines inside parse-error recovery regions (subset of perFile)
72
73
  * unsupported: { fileCount: number, lines: number, files: string[],
73
74
  * languages: Object<string, number>,
74
75
  * sites: Array<{file: string, line: number, text: string}>,
@@ -125,11 +126,39 @@ function computeGroundSet(index, name) {
125
126
  ? index.discoveryIssues.map(issue => ({ ...issue })) : [];
126
127
  unreadableFiles.sort();
127
128
 
129
+ // fix #367e: ground lines inside a parser-recovery region of an indexed
130
+ // file. They are classified like any other line, but from a partially
131
+ // parsed tree - disclosed, never silently trusted.
132
+ const recovered = { fileCount: 0, lines: 0, files: [], sites: [] };
133
+ for (const [filePath, matchedLines] of perFile) {
134
+ const regions = index.files.get(filePath)?.parseErrorRegions;
135
+ if (!regions || regions.length === 0) continue;
136
+ let inFile = 0;
137
+ let r = 0;
138
+ for (const line of matchedLines) {
139
+ while (r < regions.length && regions[r][1] < line) r++;
140
+ if (r < regions.length && regions[r][0] <= line) {
141
+ inFile++;
142
+ if (recovered.sites.length < 10) {
143
+ recovered.sites.push({ file: path.relative(index.root, filePath), line });
144
+ }
145
+ }
146
+ }
147
+ if (inFile > 0) {
148
+ recovered.fileCount++;
149
+ recovered.lines += inFile;
150
+ recovered.files.push(path.relative(index.root, filePath));
151
+ }
152
+ }
153
+ recovered.files.sort(codeUnitCompare);
154
+ recovered.sites.sort((a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line);
155
+
128
156
  const result = {
129
157
  total: total + unparsed.lines + unsupported.lines,
130
158
  fileCount: fileCount + unparsed.fileCount + unsupported.fileCount,
131
159
  perFile,
132
160
  unparsed,
161
+ recovered,
133
162
  unsupported,
134
163
  unreadableFiles,
135
164
  skippedSources,
@@ -473,6 +502,7 @@ function buildAccount(index, name, parts) {
473
502
  line: e.line, reason: e.reason, provenance: e.provenance })) }),
474
503
  },
475
504
  unparsed: groundSet.unparsed,
505
+ ...(groundSet.recovered?.lines > 0 && { recovered: groundSet.recovered }),
476
506
  unsupported,
477
507
  unreadableFiles: groundSet.unreadableFiles,
478
508
  skippedSources: groundSet.skippedSources || [],