Skip to content

feat: agent-safe CLI contract, resolver correctness, map/digest merge (#31-#37) - #38

Open
aeroxy wants to merge 16 commits into
mainfrom
dev
Open

feat: agent-safe CLI contract, resolver correctness, map/digest merge (#31-#37)#38
aeroxy wants to merge 16 commits into
mainfrom
dev

Conversation

@aeroxy

@aeroxy aeroxy commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Resolves the seven issues filed against 3.0.0, plus review follow-ups:

MCP mirrors the CLI: trace rejections render a valid trace.v1 doc with found:false, callers folds hidden-unattributed counts into the response body, and the unattributed section shares the --limit budget everywhere.

Summary by CodeRabbit

  • New Features
    • Standardized CLI error contract: stderr for rejections/notes; --json includes ast-bro.error.v1; consistent exit codes (including 3 for cycles).
    • Call graph/trace/impact and dependency commands now show true totals with truncation metadata; callers adds unattributed call-site reporting.
    • map/digest share unified behavior (member caps, projections); chunking now supports TOML and PowerShell; MCP tools/schema updated.
  • Bug Fixes
    • Improved receiver/scope resolution consistency (including case-insensitive PHP late-binding keywords).
  • Documentation
    • Updated README, wiki, and command contract docs.
  • Tests
    • Expanded CLI/MCP and calls e2e coverage for error, truncation, and attribution rules.

…#31-#37)

Resolves the seven issues filed against 3.0.0, plus review follow-ups:

- #31 callers under-reports on receiver shadowing: pass A now respects
  explicit receivers (self-like keywords only; type-qualified calls bind
  to the scoped qn), self-like binding prefers the caller's own scope,
  and edges the resolver could not attribute are surfaced in callers
  output (text section + JSON unattributed/_total/_truncated/_hidden)
  instead of silently dropped. PHP scope keywords are case-folded.
- #33/#36 one error contract: stdout carries results only; exit 0 = query
  ran (empty answers included), 2 = query could not run as asked, 1 =
  internal failure; rejections print to stderr and emit an
  ast-bro.error.v1 envelope under --json (new src/cli_error.rs). Unknown
  flags exit 2 with a cross-subcommand hint instead of help-on-stdout.
  Empty/unresolved path lists are rejected before walking.
- #32 truncation is never silent: callers/callees/reverse-deps/impact
  report true totals when --limit trims display, JSON carries
  total/truncated (+ frontier_truncated for depth-stopped walks, noted
  on stderr only when --depth was raised), map --max-members prints
  +N more and JSON dropped_members agrees with the text renderer.
- #37 map and digest are one command: --detail names|signatures|full,
  one-polarity visibility flags, scope flags on both; digest is an alias
  for map --preset digest with explicit flags overriding the preset.
- #35 map on a large directory hints at the digest preset (stderr,
  never a silent redirect).
- #34 the Read-hook substitution leads with "nothing failed" so the
  map is read as the answer, not an error.

MCP mirrors the CLI: trace rejections render a valid trace.v1 doc with
found:false, callers folds hidden-unattributed counts into the response
body, and the unattributed section shares the --limit budget everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR standardizes CLI errors and stdout/stderr behavior, adds structured JSON rejection envelopes, improves receiver-aware call-graph resolution and traversal reporting, adds plain-text chunking, and reports totals and truncation across analysis outputs.

Changes

CLI and analysis behavior

Layer / File(s) Summary
Centralized CLI error contract
src/cli_error.rs, src/graph_cache/mod.rs, src/lib.rs, README.md, wiki/architecture.md
Structured errors now provide stable kinds, exit codes, stderr routing, JSON envelopes, path validation, and shared graph loading.
Call analysis and rendering
src/calls/*, src/adapters/php.rs, src/graph_cache/delta.rs
Receiver-aware resolution, frontier reporting, unattributed callers, trace outcomes, and truncation-aware text/JSON rendering are added.
Bounded analysis outputs
src/core.rs, src/impact.rs, src/deps/*, src/mcp/tools.rs
Full totals are preserved while display results are capped; map/digest presets, member caps, projections, and MCP path handling are unified.
Rust visibility extraction
src/adapters/rust.rs
Inherited visibility and canonical private defaults are applied across traits, impls, modules, macros, and fields.
Plain-text indexing
src/search/chunker.rs
TOML and PowerShell files are indexed using blank-line chunking with LF and CRLF support.
Documentation and validation
skills/*, wiki/*, tests/*, src/hook/decide.rs
CLI, call-graph, filtering, MCP, hook, and chunking contracts are documented and covered by integration and unit tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit watched the call graph grow,
With hidden paths now set aglow.
Errors hop to stderr clear,
Totals tell what's missing here.
“Map and trace!” the bunny sings,
With capped results and truthful things. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main themes: CLI contract hardening, resolver fixes, and the map/digest merge.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@aeroxy

aeroxy commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Unify CLI contracts and fix resolver and truncation reporting

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Standardizes CLI/MCP errors, channels, exit codes, and machine-readable rejection envelopes.
• Corrects receiver-aware call resolution and surfaces unattributed caller edges.
• Unifies map/digest options and reports every output truncation explicitly.
Diagram

graph TD
  A["CLI and MCP"] --> B{"Valid request?"} -->|Yes| C["Map or graph"] --> D["Resolver and cache"] --> E["Traversal totals"] --> F["Text or JSON"] --> G["Result channel"]
  B -->|No| H["Error contract"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Count-and-cap traversal accumulator
  • ➕ Preserves true totals without storing every graph hit
  • ➕ Keeps memory consumption proportional to the requested display limit
  • ➕ Centralizes total, truncation, and frontier metadata in traversal APIs
  • ➖ Requires broader traversal API redesign
  • ➖ Must preserve deterministic ordering and filtering while counting discarded hits
  • ➖ Adds complexity to impact queries that merge several result sources
2. Keep digest as a separate command
  • ➕ Maintains a simpler command-specific argument surface
  • ➕ Avoids preset precedence rules
  • ➖ Duplicates walking, filtering, and rendering behavior
  • ➖ Allows map and digest JSON semantics to diverge again
  • ➖ Prevents shared detail, visibility, and scope controls

Recommendation: The unified error contract and map/digest implementation are the right architectural direction because they eliminate command-specific behavior and schema drift. Consider evolving the current unbounded-collection-then-truncate implementation into a count-and-cap accumulator for very large repositories; it retains truthful totals while making --limit a memory bound as well as a display bound.

Files changed (27) +2138 / -594

Enhancement (7) +1007 / -276
render.rsRender caller uncertainty and truncation explicitly +164/-7

Render caller uncertainty and truncation explicitly

• Adds true totals, truncation and frontier fields, plus text and JSON sections for unattributed call sites and hidden counts.

src/calls/render.rs

traverse.rsExpose traversal frontier and unattributed edges +90/-5

Expose traversal frontier and unattributed edges

• Introduces traversal results with depth-frontier metadata and finds unresolved bare edges naming queried targets for caller reporting.

src/calls/traverse.rs

cli_error.rsIntroduce the shared CLI error contract +128/-0

Introduce the shared CLI error contract

• Defines categorized errors, exit-code mapping, human stderr diagnostics, and the 'ast-bro.error.v1' machine-readable envelope.

src/cli_error.rs

cli.rsStandardize dependency errors and reverse-dependency limits +65/-56

Standardize dependency errors and reverse-dependency limits

• Routes dependency command failures through the shared error envelope and reports true reverse-dependency totals before display truncation.

src/deps/cli.rs

render.rsRender truthful reverse-dependency totals +24/-2

Render truthful reverse-dependency totals

• Adds total and truncated metadata to reverse-dependency JSON and displays true importer counts in text headers.

src/deps/render.rs

lib.rsUnify map/digest and enforce the CLI contract +487/-197

Unify map/digest and enforce the CLI contract

• Introduces shared map arguments, detail levels and digest presets with explicit overrides. It also validates paths and symbols, standardizes parse failures, preserves result-only stdout, and adds large-map hints.

src/lib.rs

tools.rsExpose unified map and truthful limits through MCP +49/-9

Expose unified map and truthful limits through MCP

• Adds map detail and member-cap controls, digest glob support, reduced digest JSON payloads, and true reverse-dependency totals.

src/mcp/tools.rs

Bug fix (8) +474 / -193
php.rsCase-fold PHP scoped-call keywords +6/-1

Case-fold PHP scoped-call keywords

• Recognizes case-insensitive 'self', 'static', and 'parent' spellings when normalizing PHP scoped calls.

src/adapters/php.rs

cli.rsReport complete call-query metadata and standardized errors +125/-83

Report complete call-query metadata and standardized errors

• Uses shared graph loading and symbol errors, adds callee limits and frontier metadata, and exposes unattributed caller sites within the shared limit budget.

src/calls/cli.rs

mcp.rsMirror caller and trace correctness in MCP +75/-8

Mirror caller and trace correctness in MCP

• Adds true totals, frontier and unattributed caller metadata to MCP responses. Unresolved traces now return valid trace documents with 'found: false'.

src/calls/mcp.rs

resolve.rsMake call resolution receiver-aware +80/-31

Make call resolution receiver-aware

• Prevents explicit receivers from binding to unrelated same-file or imported homonyms. Self-like calls prefer the caller's scope, while type-qualified calls require a matching local type scope.

src/calls/resolve.rs

core.rsAlign map truncation across text and JSON +86/-16

Align map truncation across text and JSON

• Reports omitted type members in text and JSON, applies member caps consistently, and removes docs from reduced-detail JSON payloads.

src/core.rs

delta.rsReuse receiver classification during incremental promotion +2/-4

Reuse receiver classification during incremental promotion

• Applies the resolver's self-like receiver rules when promoting cached bare calls, preventing incremental and full builds from diverging.

src/graph_cache/delta.rs

decide.rsClarify successful Read-hook substitution +10/-2

Clarify successful Read-hook substitution

• Moves a single “nothing failed” notice before the substituted structure map so agents interpret the map as the requested answer.

src/hook/decide.rs

impact.rsPreserve true impact totals before limiting output +90/-48

Preserve true impact totals before limiting output

• Computes complete caller, importer, and transitive impact counts, then applies display limits with per-section truncation metadata.

src/impact.rs

Refactor (3) +48 / -42
trace.rsSeparate trace resolution failures from rendering +8/-16

Separate trace resolution failures from rendering

• Carries the unresolved endpoint in 'TraceOutcome', allowing CLI and MCP callers to enforce their respective error channels and schemas.

src/calls/trace.rs

context.rsAdopt shared symbol-query error handling +7/-26

Adopt shared symbol-query error handling

• Uses the common graph-loading and symbol-not-found paths so context queries follow the uniform CLI contract.

src/context.rs

mod.rsCentralize symbol-query graph loading +33/-0

Centralize symbol-query graph loading

• Adds a shared loader that resolves project roots, ensures call graphs exist, and emits contract-compliant path or index failures.

src/graph_cache/mod.rs

Tests (5) +560 / -73
calls_e2e.rsCover receiver resolution and call truncation regressions +275/-0

Cover receiver resolution and call truncation regressions

• Adds end-to-end cases for explicit receiver shadowing, unattributed callers, shared limit budgets, true totals, frontier cutoffs, and receiver keyword classification.

tests/calls_e2e.rs

cli_ergonomics.rsVerify the uniform CLI and map/digest contracts +222/-63

Verify the uniform CLI and map/digest contracts

• Tests stderr-only rejections, exit codes, JSON error envelopes, path validation, sibling-flag hints, preset equivalence, and large-map guidance.

tests/cli_ergonomics.rs

digest_format.rsVerify text and JSON member-cap parity +45/-0

Verify text and JSON member-cap parity

• Confirms caps apply only to type members and that text omission counts match JSON 'dropped_members'.

tests/digest_format.rs

show_markdown.rsUpdate symbol-miss expectations for show +11/-4

Update symbol-miss expectations for show

• Verifies non-matching code symbols now reject with exit 2 and a stderr diagnostic while exact matches remain supported.

tests/show_markdown.rs

surface_e2e.rsUpdate surface error-channel coverage +7/-6

Update surface error-channel coverage

• Confirms unsupported language overrides produce exit 2, empty stdout, and an actionable stderr hint.

tests/surface_e2e.rs

Documentation (4) +49 / -10
README.mdDocument the agent-safe CLI and map contract +15/-0

Document the agent-safe CLI and map contract

• Documents the error schema, stdout/stderr rules, exit codes, explicit truncation metadata, and unified map/digest behavior.

README.md

SKILL.mdUpdate agent guidance for errors, limits, and digest presets +10/-4

Update agent guidance for errors, limits, and digest presets

• Explains the uniform recovery contract, default result caps, map detail controls, and unattributed caller reporting for agent users.

skills/ast-bro/SKILL.md

architecture.mdDescribe the unified CLI architecture +16/-2

Describe the unified CLI architecture

• Documents result and diagnostic channels, exit semantics, JSON rejection envelopes, truthful truncation, and shared map/digest internals.

wiki/architecture.md

calls.mdDocument receiver-aware call resolution +8/-4

Document receiver-aware call resolution

• Explains explicit and self-like receiver handling, unattributed caller discovery, default ambiguity visibility, and the revised resolver passes.

wiki/calls.md

@qodo-code-review

qodo-code-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Hidden unattributed total wrong 🐞 Bug ≡ Correctness ⭐ New
Description
run_callers computes unattributed_total after clearing unresolved call sites when ambiguous
edges are hidden, so JSON/text can report unattributed_total: 0 even when `unattributed_hidden >
0. This breaks the documented Unattributed` contract (“true total behind them”) and misleads
consumers about how many unresolved call sites existed.
Code

src/calls/cli.rs[R186-190]

+    let unattributed = render::Unattributed {
+        edges: &unattributed,
+        total: unattributed_total,
+        hidden: unattributed_hidden,
+        declarers,
Relevance

●●● Strong

Repo tends to accept correctness fixes ensuring reported totals/truncation metadata match documented
contracts.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both CLI and MCP clear unattributed when ambiguous/unresolved sites are hidden and then compute
unattributed_total from the cleared vector; meanwhile the renderer documents Unattributed.total
as the true total behind the displayed sample, meaning the current value is incorrect whenever
anything is hidden.

src/calls/cli.rs[162-190]
src/calls/mcp.rs[50-80]
src/calls/render.rs[45-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When `--hide-ambiguous` (CLI) / `include_ambiguous:false` (MCP) hides unresolved call sites, the code clears the `unattributed` vector and *then* sets `unattributed_total = unattributed.len()`. This makes `Unattributed.total` exclude hidden sites, contradicting the renderer contract that `total` is the true pre-filter count.

### Issue Context
`src/calls/render.rs` documents `Unattributed.total` as the “true total behind them” and `hidden` as the count dropped by `--hide-ambiguous`. Both the CLI and MCP implementations currently lose the pre-hide total.

### Fix Focus Areas
- src/calls/cli.rs[162-191]
- src/calls/mcp.rs[50-80]

### What to change
- Capture `unattributed_total` **before** any hiding/truncation, e.g. immediately after computing/filtering `unattributed`.
- If you want `total` to always include hidden rows, set `unattributed_total` to the pre-hide length (or compute it as `unattributed.len() + unattributed_hidden` after clearing, ensuring it matches the intended semantics).
- Keep `edges` as the displayed sample (possibly empty when hidden), but keep `total` as the true count so JSON fields like `unattributed_total` remain accurate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Lexical type receiver misbinds ✓ Resolved 🐞 Bug ≡ Correctness
Description
For an unqualified type path such as Foo::method, the resolver selects the first same-file
qualified name ending in ::Foo::method, even when multiple lexical scopes define that path. A call
under OuterB can therefore bind Exact to OuterA::Foo::method, corrupting callers, callees,
trace, and impact results.
Code

src/calls/resolve.rs[129]

+                        fp.defined.iter().find(|q| q.0.ends_with(&full)).cloned()
Relevance

●●● Strong

Directly violates the PR’s scoped receiver goal; similar call-graph attribution correctness fixes
were accepted.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The resolver uses find over suffix matches without considering the caller's lexical scope or
checking uniqueness. The declaration walker includes every parent in callable QNs, so
file::OuterA::Foo::method and file::OuterB::Foo::method can coexist and both satisfy the suffix
before the selected result is labeled Exact.

src/calls/resolve.rs[127-140]
src/calls/build.rs[161-203]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Same-file resolution of an unqualified type path uses the first suffix match. When multiple lexical scopes contain `Foo::method`, this can assign an `Exact` edge to the wrong scope.

## Issue Context
Callable qualified names include all declaration parents. Resolve the receiver relative to the caller's lexical scope; if that does not produce a unique target, preserve the candidates as ambiguous rather than selecting the first match.

## Fix Focus Areas
- src/calls/resolve.rs[118-129]
- src/calls/build.rs[161-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Default callee frontier misreported ✓ Resolved 🐞 Bug ≡ Correctness
Description
At depth 1, run_callees bypasses callees_info, leaving frontier_truncated false even when
direct callees have outgoing calls. JSON consumers are therefore told the graph ended when the
default depth actually cut off reachable edges.
Code

src/calls/cli.rs[R224-225]

+                    let info = traverse::callees_info(calls, &c.qn, depth.max(1));
+                    frontier_truncated |= info.frontier_truncated;
Relevance

●●● Strong

Incorrect frontier metadata contradicts stated PR intent and accepted accurate-truncation precedent.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI only obtains TraversalInfo in the depth-greater-than-one branch, while the traversal marks
a frontier when a node at the cutoff still has edges. The renderer serializes the unchanged false
value directly into the JSON response.

src/calls/cli.rs[215-245]
src/calls/traverse.rs[165-214]
src/calls/render.rs[623-632]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`callees --depth 1` uses the one-hop path without computing traversal metadata, so `frontier_truncated` remains false when additional reachable calls exist.

## Issue Context
Preserve one-hop output semantics while inspecting resolved frontier nodes for unexplored outgoing edges, and add coverage for the default depth.

## Fix Focus Areas
- src/calls/cli.rs[215-245]
- src/calls/traverse.rs[165-214]
- tests/calls_e2e.rs[1887-1904]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Type caller limits ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new limit accounting truncates only callable hits, leaving implementations and constructions
in type_groups unbounded for callers <Type> --limit. Such JSON can report total: 0 and
truncated: false while returning arbitrarily many type results.
Code

src/calls/cli.rs[R103-105]

+    let total = hits.len();
    if hits.len() > limit {
        hits.truncate(limit);
Relevance

●●● Strong

Directly violates PR’s true-total and non-silent truncation contract; closely matches accepted
limit-accounting fixes.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Type results are accumulated separately and never truncated, while total is computed exclusively
from hits. The renderer emits every implementation and construction but derives total and
truncated solely from callable-hit metadata.

src/calls/cli.rs[44-105]
src/calls/render.rs[433-511]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Type-specific caller entries are excluded from `--limit`, total, and truncation calculations.

## Issue Context
Define and enforce a combined display budget for callable hits, implementations, constructions, and unattributed sites. Ensure JSON reports true totals for every limited section.

## Fix Focus Areas
- src/calls/cli.rs[44-158]
- src/calls/render.rs[433-511]
- tests/calls_e2e.rs[1831-1941]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Private trait impls leak 🐞 Bug ≡ Correctness
Description
Every trait-implementation method receives empty inherited visibility without considering whether
the implemented trait is private. Because the impl container and method are not marked private,
digest and --no-private can expose a public type's implementation of a private trait.
Code

src/adapters/rust.rs[R358-363]

+                children.push(_function_to_decl(
+                    &item,
+                    src,
+                    true,
+                    if trait_node.is_some() { Vis::Inherited } else { Vis::Owned },
+                ));
Relevance

●●● Strong

Deterministic privacy leak; team accepts Rust public-surface correctness fixes.

PR-#22

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Trait impl methods are always assigned Vis::Inherited, which leaves absent modifiers empty, and
the impl declaration itself also has empty visibility. Both digest traversal and member filtering
suppress only the exact private marker; the added visibility test covers only a public trait
implemented by a public type.

src/adapters/rust.rs[342-394]
src/core.rs[1059-1138]
tests/digest_format.rs[488-550]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Rust methods implementing a private trait remain eligible for public-only map and digest output because `Vis::Inherited` produces empty visibility and the impl container also has no effective visibility.

## Issue Context
Determine an impl's effective API visibility using the implemented trait and implementing type, or introduce a dedicated filtering rule for trait impl declarations. A public trait's methods must remain visible, while an implementation of a private trait must be omitted from public-only output.

## Fix Focus Areas
- src/adapters/rust.rs[342-394]
- src/core.rs[1059-1138]
- tests/digest_format.rs[488-550]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. JSON hides partial path misses ✓ Resolved 🐞 Bug ≡ Correctness
Description
MCP digest emits the new partial-miss note only in its text branch, while json: true silently maps
the surviving paths as though every requested input resolved. Since MCP has no client-visible stderr
channel, JSON consumers cannot detect that their result is incomplete.
Code

src/mcp/tools.rs[R604-611]

+        match &path_note {
+            Some(n) => CallResult::Text(format!(
+                "{}\n{}",
+                n,
+                core::render_digest(&results, &opts, root)
+            )),
+            None => CallResult::Text(core::render_digest(&results, &opts, root)),
+        }
Relevance

●●● Strong

Closely matches accepted MCP fixes preventing JSON clients from silently losing result metadata.

PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
resolve_paths_for_mcp returns path_note, but the JSON branch returns render_json_map without
consuming it; only the newly added text match includes the note. The MCP documentation says
partial misses are carried because MCP has no stderr, while current tests verify partial misses only
for text responses.

src/mcp/tools.rs[568-611]
src/lib.rs[930-959]
tests/mcp_e2e.rs[114-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MCP digest receives partial-path diagnostics but includes them only in text output. JSON responses silently omit missing requested paths and appear complete.

## Issue Context
Keep the response valid JSON while exposing partial input resolution through machine-readable metadata or a structured MCP error. Add a test containing one existing and one missing path with `json: true`.

## Fix Focus Areas
- src/mcp/tools.rs[568-611]
- src/lib.rs[930-959]
- tests/mcp_e2e.rs[114-142]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. No-docs deletes Markdown structure ✓ Resolved 🐞 Bug ≡ Correctness
Description
_map_eligible removes every Markdown Heading and CodeBlock declaration when include_docs is
false, even though these nodes represent document structure rather than attached documentation. JSON
signatures/no-docs output consequently loses headings and potentially their entire subtrees while
equivalent text output retains them.
Code

src/core.rs[R1477-1478]

+    if matches!(d.kind, Heading | CodeBlock) && !opts.include_docs {
+        return false;
Relevance

●●● Strong

Deterministic output-consistency bug; team accepts preserving structured machine output and
correcting Markdown map rendering.

PR-#16
PR-#17

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Markdown adapter models sections and fenced blocks as Heading and CodeBlock declarations, with
headings carrying empty docs. The text renderer only uses include_docs to suppress attached
documentation and still renders declaration signatures, whereas JSON applies _filter_decls and
removes these structural nodes entirely.

src/adapters/markdown.rs[33-105]
src/core.rs[686-765]
src/core.rs[1535-1541]
src/mcp/tools.rs[514-525]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Disabling documentation currently removes Markdown Heading and CodeBlock declarations from JSON rather than only removing documentation fields. This makes JSON materially incomplete and inconsistent with text rendering.

## Issue Context
The Markdown adapter stores headings and fenced blocks as structural declarations with empty `docs`. Keep these declarations eligible and let the existing JSON projection remove only documentation-related fields.

## Fix Focus Areas
- src/core.rs[1411-1481]
- src/core.rs[686-765]
- src/core.rs[1535-1578]
- src/adapters/markdown.rs[33-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (10)
8. Qualified self paths stay ambiguous ✓ Resolved 🐞 Bug ≡ Correctness
Description
Qualified Rust receivers such as crate::a::Foo are matched literally against repository QNs, which
never contain the leading crate, self, or super segment. These explicit calls therefore fall
through to dependency-based resolution and can remain Ambiguous when same-named methods coexist,
corrupting callers and callees results.
Code

src/calls/resolve.rs[R129-130]

+                        let matches: Vec<&Qn> =
+                            fp.defined.iter().filter(|q| q.0.ends_with(&full)).collect();
Relevance

●●● Strong

Matches PR resolver intent; team previously accepted fixes preventing ambiguous call edges from
hiding correct results.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Rust adapter preserves everything before the final call segment as the receiver, including
qualified crate::/self:: paths. QNs instead use <repo-relative-file>::<scope>::<name>, and
later resolution retains multiple same-file candidates as Ambiguous, so the literal suffix
comparison cannot reliably resolve these calls.

src/adapters/rust.rs[548-561]
src/calls/graph.rs[13-40]
src/calls/resolve.rs[248-270]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Qualified Rust receivers such as `crate::a::Foo`, `self::a::Foo`, and `super::a::Foo` cannot match repository-qualified names directly. This causes explicitly qualified calls to fall through and potentially remain ambiguous.

## Issue Context
The Rust adapter preserves the complete receiver path, while call-graph QNs begin with the repository-relative filename. Resolve self-relative prefixes against the caller's module scope before matching, without restoring the unsafe terminal-segment fallback.

## Fix Focus Areas
- src/calls/resolve.rs[115-154]
- src/adapters/rust.rs[548-561]
- src/calls/graph.rs[13-40]
- tests/calls_e2e.rs[2074-2118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Nested drops are overcounted ✓ Resolved 🐞 Bug ≡ Correctness
Description
The JSON filter recursively counts truncation inside child types before applying the current type's
cap, so descendants of a child later removed wholesale remain in dropped_members. Text rendering
skips that removed child before recursion and reports only one omitted member, violating the
text/JSON truncation consistency contract.
Code

src/core.rs[1443]

+            if _is_capped_type(&d.kind) {
Relevance

●●● Strong

Directly violates the PR’s text/JSON truncation consistency contract; accurate truncation fixes are
routinely accepted.

PR-#25
PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
_filter_decls passes the shared counter into child recursion at line 1439, then truncates the
current type at lines 1443-1447. _render_decl instead checks the parent cap and continues before
recursively rendering a removed child, so nested omissions produce different counts.

src/core.rs[1439-1447]
src/core.rs[757-765]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Nested type truncation can make JSON `dropped_members` exceed the corresponding text `+N` count because discarded subtrees contribute recursive drop counts before their parent removes them.

## Issue Context
Apply the parent member cap before recursively accumulating retained children's truncation, or track per-subtree counts and discard them when that subtree is removed. Add a regression containing a nested capped type that is itself removed by an ancestor cap.

## Fix Focus Areas
- src/core.rs[1439-1447]
- src/core.rs[757-765]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Missing paths become empty success ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new empty-results branch assumes the supplied paths existed, but walk_and_parse also returns
an empty vector when every path is unresolved. Text MCP map requests therefore return `isError:
false` with an authoritative “0 parseable files” answer even though nothing was inspected.
Code

src/mcp/tools.rs[R533-537]

+    } else if results.is_empty() {
+        // Same empty-answer message as the CLI (issue #33): the paths
+        // exist but contain nothing parseable — say so rather than return
+        // an empty string.
+        CallResult::Text("# 0 parseable file(s) in the given path(s)".to_string())
Relevance

●●● Strong

Exact precedent favors rejecting invalid MCP input rather than masking it as an empty successful
result.

PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
run_map calls walk_and_parse without resolving its non-empty path list first, then converts
every empty result into CallResult::Text. The walker returns an empty vector when all path
expansions are missing, while CallResult::Text is explicitly the successful MCP result variant
rather than CallResult::Error.

src/mcp/tools.rs[332-337]
src/mcp/tools.rs[490-537]
src/lib.rs[675-699]
src/lib.rs[749-753]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MCP `map` treats an all-unresolved path list as a successful empty result because `walk_and_parse` does not distinguish missing inputs from existing inputs with no parseable files.

## Issue Context
Validate path resolution before parsing. Return `CallResult::Error` when none of the requested paths resolve, while preserving successful empty responses for existing paths containing no parseable files. Add text and JSON MCP regression coverage.

## Fix Focus Areas
- src/mcp/tools.rs[490-537]
- src/lib.rs[675-753]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Restricted Rust items leak 🐞 Bug ≡ Correctness
Description
_visibility_or_private leaves pub(crate), pub(super), and pub(in ...) as non-private values.
Because renderers hide only the exact string private, digest's public-only preset and
--no-private expose restricted declarations.
Code

src/adapters/rust.rs[R1071-1074]

+    if v.is_empty() {
+        "private".to_string()
+    } else {
+        v
Relevance

●●● Strong

Deterministic visibility leak directly affects the PR’s public-only map/digest merge.

PR-#24

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The adapter canonicalizes only a missing modifier to private, while the map and digest renderers
exclude declarations solely when visibility equals private. Restricted pub(...) declarations
therefore pass every public-only check.

src/adapters/rust.rs[1051-1075]
src/core.rs[685-690]
src/core.rs[1054-1110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Restricted Rust visibility modifiers are treated as public by filters that hide private declarations.

## Issue Context
All map and digest filtering paths recognize only the canonical `private` value. Rust visibility should distinguish unrestricted `pub` from crate-, parent-, and path-restricted visibility for the public-only view.

## Fix Focus Areas
- src/adapters/rust.rs[1051-1075]
- src/core.rs[685-690]
- src/core.rs[1054-1110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Whitespace blank lines ignored ✓ Resolved 🐞 Bug ➹ Performance
Description
The plain-text splitter recognizes only adjacent line terminators, so whitespace-only blank lines
such as \n    \n are not boundaries. TOML and PowerShell files using such separators can
consequently produce oversized, poorly localized search chunks.
Code

src/search/chunker.rs[R201-207]

+        loop {
+            if i < len && bytes[i] == b'\n' {
+                i += 1;
+            } else if i + 1 < len && bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
+                i += 2;
+            } else {
+                break;
Relevance

●●● Strong

Localized splitter bug with straightforward tests; search correctness and performance fixes are
historically accepted.

PR-#20

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The terminator-consumption loop stops as soon as it encounters a space or tab, even if another line
terminator follows. Existing tests cover only completely empty LF and CRLF lines, leaving
whitespace-only separators untested.

src/search/chunker.rs[175-221]
src/search/chunker.rs[473-522]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Plain-text paragraph splitting fails when a blank line contains spaces or tabs.

## Issue Context
The new TOML and PowerShell chunker consumes only directly adjacent LF or CRLF terminators. Consume horizontal whitespace between line terminators and add LF/CRLF regression coverage without changing source offsets or coverage.

## Fix Focus Areas
- src/search/chunker.rs[175-221]
- src/search/chunker.rs[473-522]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Qualified receiver misbinding ✓ Resolved 🐞 Bug ≡ Correctness
Description
The terminal-segment fallback can resolve an explicitly qualified call such as
external::Thing::run() to an unrelated same-file local::Thing::run(). The resulting edge is
marked exact, corrupting callers, callees, trace, and impact results.
Code

src/calls/resolve.rs[R126-130]

+                            .or_else(|| {
+                                let last =
+                                    normalized.rsplit("::").next().unwrap_or(normalized.as_str());
+                                let scoped = format!("::{}::{}", last, raw.bare_name);
+                                fp.defined.iter().find(|q| q.0.ends_with(&scoped))
Relevance

●●● Strong

Exact-edge misbinding directly contradicts this PR’s stated resolver-correctness goal.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The resolver discards all but the receiver's final segment when the full match fails. Rust
extraction retains the full scoped receiver, so external::Thing::run() reaches this fallback as
external::Thing and can suffix-match any local Thing::run.

src/calls/resolve.rs[116-132]
src/adapters/rust.rs[548-561]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Explicitly qualified receivers fall back to matching only their terminal type segment, allowing unrelated same-file types to receive exact call edges.

## Issue Context
Rust call extraction preserves the complete path preceding a scoped call. If the complete receiver does not match, discarding its qualifiers changes the target rather than leaving the call unresolved or ambiguous.

## Fix Focus Areas
- src/calls/resolve.rs[116-132]
- src/adapters/rust.rs[548-561]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Names mode silently drops functions ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new map --detail names --max-members N path forwards its per-type cap to a digest renderer
that also truncates module-level free functions without a remainder marker. Large modules therefore
silently lose declarations despite the documented per-type scope and non-silent truncation contract.
Code

src/lib.rs[R981-984]

+            let opts = DigestOptions {
+                include_private,
+                include_fields,
+                max_members_per_type: max_members.unwrap_or(usize::MAX),
Relevance

●●● Strong

Silent cap-based omission directly conflicts with this PR’s non-silent, per-type truncation
contract.

PR-#25
PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new names branch passes max_members into DigestOptions::max_members_per_type. Despite its
name, the digest renderer slices the free-function list to that value and, unlike the type-member
branch, adds no +N more line.

src/lib.rs[568-570]
src/lib.rs[979-992]
src/core.rs[909-930]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Names rendering applies the type-member cap to free functions and omits any truncation notice.

## Issue Context
Either leave module-level free functions uncapped, matching the documented per-type behavior, or add explicit totals and remainder reporting consistently across text and JSON.

## Fix Focus Areas
- src/lib.rs[568-570]
- src/lib.rs[979-992]
- src/core.rs[909-930]
- tests/digest_format.rs[326-370]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Overloads break dropped count ✓ Resolved 🐞 Bug ≡ Correctness
Description
JSON computes dropped_members by truncating raw type children, while names-detail text collapses
overloads before applying the same cap. For overloaded APIs, JSON and text retain different logical
members and report different drop counts for the same command options.
Code

src/core.rs[R1403-1408]

+            let is_type = matches!(d.kind, Class | Struct | Interface | Record | Enum);
+            if is_type {
+                if let Some(cap) = opts.max_members {
+                    if children.len() > cap {
+                        *dropped += children.len() - cap;
+                        children.truncate(cap);
Relevance

●●● Strong

JSON/text disagreement violates the PR’s explicit dropped_members consistency requirement.

PR-#16
PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The JSON filter counts and truncates the raw filtered child vector, whereas digest text first
invokes _collapse_overloads and then computes the shown and omitted counts. The CLI routes names
text through the latter but all JSON detail levels through the former.

src/core.rs[909-920]
src/core.rs[1376-1418]
src/lib.rs[972-992]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Text and JSON apply `--max-members` to different member representations when overloads exist.

## Issue Context
Normalize or collapse overloads consistently before truncation and dropped-member accounting, or clearly represent equivalent logical groups in both formats. Add an overloaded-method fixture that compares text `+N more` with JSON `dropped_members`.

## Fix Focus Areas
- src/core.rs[909-920]
- src/core.rs[1376-1418]
- src/lib.rs[972-992]
- tests/digest_format.rs[326-370]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. Names mode ignores output flags ✓ Resolved 🐞 Bug ≡ Correctness
Description
The names-detail branch constructs DigestOptions, which has no attribute or line-number controls,
so --no-attrs and --no-lines are silently ignored in names text output. This breaks the
advertised unified map/digest flag contract and emits information explicitly disabled by the caller.
Code

src/lib.rs[R981-986]

+            let opts = DigestOptions {
+                include_private,
+                include_fields,
+                max_members_per_type: max_members.unwrap_or(usize::MAX),
+                max_heading_depth: 3,
+            };
Relevance

●●● Strong

Team previously accepted fixes ensuring requested output modes and flags are honored consistently.

PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
run_map_digest computes MapOptions containing both controls but replaces them with the narrower
DigestOptions for names output. render_digest unconditionally renders inline attributes and line
suffixes, and DigestOptions exposes no way to disable either.

src/lib.rs[954-992]
src/core.rs[268-283]
src/core.rs[872-906]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Names-detail text rendering discards the requested attribute and line-number settings.

## Issue Context
Propagate these controls into the digest renderer or use a shared option-aware renderer. Cover both `map --detail names` and the `digest` alias.

## Fix Focus Areas
- src/lib.rs[954-992]
- src/core.rs[268-283]
- src/core.rs[872-906]
- tests/digest_format.rs[326-370]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Non-types pass implements validation ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new target_exists check accepts any declaration returned by find_symbols, including
functions and fields. Consequently, implements helper can exit 0 with an authoritative zero-match
answer even though helper is not a valid type target.
Code

src/lib.rs[R1217-1219]

+                let target_exists = results
+                    .iter()
+                    .any(|r| !crate::core::find_symbols(r, target).is_empty());
Relevance

●●● Strong

Team accepts fail-fast validation fixes preventing invalid inputs from masquerading as authoritative
empty results.

PR-#16
PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
find_symbols traverses and returns all declaration kinds, whereas implementation discovery
explicitly collects only class, struct, interface, and record declarations. Using the former as the
existence check therefore validates targets that the latter cannot query.

src/lib.rs[1209-1229]
src/core.rs[1146-1203]
src/core.rs[1235-1307]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`implements` treats any matching declaration as proof that its target type exists.

## Issue Context
Restrict target validation to the declaration kinds supported by `find_implementations`, while retaining exit 0 for a real type with no implementations.

## Fix Focus Areas
- src/lib.rs[1209-1229]
- src/core.rs[1235-1307]
- tests/cli_ergonomics.rs[129-146]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/calls/cli.rs
Comment thread src/calls/cli.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/lib.rs
Comment thread src/lib.rs
Comment thread src/core.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/impact.rs (1)

362-379: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Unbounded reverse BFS per construction site is now quadratic-ish on wide types.

These walks previously stopped at opts.limit; with usize::MAX each construction site expands its full reverse-reachable set, and the implementor branch nests that inside a second loop (implementors × constructions × reachable set). For a type constructed in hundreds of places this is a large amount of repeated traversal whose results are then collapsed by consider into one minimum-depth entry per qn.

Reinstating a limit would bring back the wrong totals #32 fixes, so the fix is sharing work instead: track qns already resolved at a depth <= candidate depth and skip re-expanding them, or run one multi-source BFS seeded with all construction sources instead of one walk per site.

Also applies to: 420-437

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/impact.rs` around lines 362 - 379, Optimize the reverse traversal around
traverse::callers and its construction-site loop without restoring the
opts.limit bound. Share traversal work across construction and implementor
sources by tracking qns already resolved at an equal or shallower candidate
depth, or by using a single multi-source BFS, while preserving consider’s
minimum-depth results and correct totals.
🧹 Nitpick comments (6)
src/lib.rs (1)

845-861: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated envelope construction — add a JSON-only emit to CliError.

The ast-bro.error.v1 shape now lives in two places (CliError::emit and here), so a future field addition can silently drift on the parse-error path. Consider exposing a emit_json_only(&self) (or json_envelope(&self) -> Value) on CliError and calling it here.

♻️ Sketch
// src/cli_error.rs
impl CliError {
    /// Envelope without the human text (for call sites that already
    /// printed a rendered message, e.g. clap parse errors).
    pub fn emit_json_only(&self) {
        eprintln!("{}", self.envelope());
    }
}
     if json_mode {
         let mut err = crate::cli_error::CliError::new(subcommand, kind, detail);
         if let Some(h) = hint {
             err = err.hint(h);
         }
-        // Human text already printed via clap above; emit only the envelope.
-        let mut doc = serde_json::json!({ ... });
-        ...
-        eprintln!("{}", doc);
+        // Human text already printed via clap above; emit only the envelope.
+        err.emit_json_only();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 845 - 861, Move the JSON envelope construction into
a reusable `CliError` method, such as `emit_json_only` backed by the existing
envelope builder, and update the `json_mode` parse-error path in `src/lib.rs` to
construct the error and call that method. Remove the duplicated
`serde_json::json!` field assembly there while preserving the existing hint and
JSON-only output behavior.
tests/calls_e2e.rs (2)

1906-1941: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Conditional assertion can no-op.

If the fixture ever yields unattributed_total == shown, the unattributed_truncated check is skipped and the test still passes. Assert the expected total (3) explicitly so the truncation flag is always exercised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/calls_e2e.rs` around lines 1906 - 1941, Strengthen
callers_limit_bounds_the_unattributed_section_too by asserting that
unattributed_total equals the fixture’s expected three entries before checking
truncation. Then assert unattributed_truncated is true unconditionally, ensuring
the test always exercises the capped unattributed output.

1759-1763: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Negative assertion is spacing-dependent and can silently pass.

"Caller::getCtx (Exact)" hard-codes two spaces from the renderer's column padding. Any padding change makes the regression pass vacuously. Assert on JSON instead (target + confidence), as the other tests in this file do for exactly this reason.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/calls_e2e.rs` around lines 1759 - 1763, Update the assertion in the
test around the existing output check to parse and inspect the emitted JSON,
asserting the target and confidence for the explicit-receiver call rather than
matching the renderer’s spacing-dependent text. Follow the JSON assertion
pattern used by the other tests in the file, preserving the requirement that the
local homonym is not classified as Exact.
src/calls/traverse.rs (1)

166-174: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Depth-cap probe re-clones the edge list.

edges_at clones the whole adjacency vector (graph.forward/reverse.get(qn).cloned()), and the cap branch calls it a second time purely to test emptiness. On wide frontiers with usize::MAX limits (the new unbounded walks in cli.rs/impact.rs) this doubles the clone cost for every capped node. Cheaper: check non-emptiness without materializing, e.g. pass an has_edges closure alongside edges_at, or hoist the single edges_at(&cur) result before the depth check and reuse it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/traverse.rs` around lines 166 - 174, The depth-cap branch in the
queue traversal loop redundantly clones adjacency data through edges_at(&cur)
solely to check whether edges remain. Restructure the traversal around edges_at
so each node’s edge list is materialized at most once, reusing that result for
the depth-cap frontier_truncated check and subsequent expansion while preserving
existing walk behavior.
src/core.rs (1)

725-759: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Visibility rule is now expressed twice.

The visible closure re-implements the field/private gates already enforced by the early returns at Lines 681-687 (and again in _filter_decls). If one copy drifts, --max-members will count members the renderer never emits (or vice versa). Extracting a single fn is_visible(d: &Declaration, opts: &MapOptions) -> bool and using it in all three places keeps text and JSON caps in agreement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core.rs` around lines 725 - 759, Extract the field/private visibility
predicate from the local visible closure into a shared is_visible(&Declaration,
&MapOptions) helper. Replace the early-return checks near the declaration
renderer, the _filter_decls logic, and the max-members counting closure with
this helper so rendered output and JSON member caps use identical visibility
rules.
src/calls/cli.rs (1)

103-116: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

callers has no display-truncation stderr note; callees does.

run_callees (Lines 248-254) prints # note: N callee(s) total; showing M … when --limit cuts the list, but run_callers only encodes that in the text header suffix / JSON truncated. For an agent piping stdout, the two commands now behave differently for the same condition. Consider mirroring the note here.

♻️ Suggested addition
     let total = hits.len();
     if hits.len() > limit {
         hits.truncate(limit);
+        eprintln!(
+            "# note: {} caller(s) total; showing {} (raise --limit to see the rest)",
+            total,
+            hits.len()
+        );
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/cli.rs` around lines 103 - 116, Update run_callers to emit the same
stderr display-truncation note as run_callees when --limit reduces hits, using
total and the post-truncation hits.len() to report the full and shown counts.
Keep the existing frontier_truncated/depth note and header/JSON truncation
behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/ast-bro/SKILL.md`:
- Line 15: Update the Result caps documentation to state that the impact command
defaults to depth 2, while keeping callers and callees at depth 1. Ensure the
documented defaults match the depth declaration for Commands::Impact.

In `@src/calls/render.rs`:
- Around line 108-123: Update the caller header truncation check in the
rendering function containing total, shown, and header_line_suffixed so the
suffix compares the combined total against the combined shown count, rather than
calling trunc.header_suffix(shown) with mismatched callable-only truncation
data. Also inspect render_callers_json_extended and align its truncated and
total fields with the same type-group-inclusive counting semantics used by the
text output.

In `@src/calls/resolve.rs`:
- Around line 114-119: Update the scoped-receiver lookup in the surrounding
call-resolution logic to fall back to the receiver’s terminal segment when the
full namespace/path-qualified receiver does not match. Preserve the existing
full-receiver match first, then derive the last component from `recv`, build the
corresponding scoped name with `raw.bare_name`, and search `fp.defined` so
qualified receivers retain same-file Exact bindings.

In `@src/lib.rs`:
- Around line 1427-1435: Update the error-kind mapping in the surface error
handling match to map only SurfaceError::NoEntryPoint to PathNotFound; map
SurfaceError::Io alongside SurfaceError::Parse to IndexError, preserving the
existing BadOverride mapping to BadArgument and CliError construction.

In `@src/mcp/tools.rs`:
- Around line 827-834: Update the reverse-dependency flow around
crate::deps::traverse::reverse so traversal remains bounded by a.limit while
still producing the true total count. Change the traversal/API usage to return
the limited page and total count together, then remove the full-closure
materialization and post hoc hits.truncate call while preserving the existing
output behavior.
- Around line 27-33: Update the max_members property in both MCP schemas,
including the digest schema near its corresponding definition, with a minimum
constraint matching its usize deserialization: use 0 unless the implementation
explicitly disallows zero. Keep the existing integer type and descriptions
unchanged.

In `@wiki/calls.md`:
- Around line 164-168: The documentation in wiki/calls.md is out of sync with
the receiver_is_self_like behavior. Update the import-resolution description to
say it applies when the receiver is self-like, including self(), Self, crate,
super, this, and $this, rather than only when there is no explicit receiver;
also update the Pass B suppression list to include this and $this.

---

Outside diff comments:
In `@src/impact.rs`:
- Around line 362-379: Optimize the reverse traversal around traverse::callers
and its construction-site loop without restoring the opts.limit bound. Share
traversal work across construction and implementor sources by tracking qns
already resolved at an equal or shallower candidate depth, or by using a single
multi-source BFS, while preserving consider’s minimum-depth results and correct
totals.

---

Nitpick comments:
In `@src/calls/cli.rs`:
- Around line 103-116: Update run_callers to emit the same stderr
display-truncation note as run_callees when --limit reduces hits, using total
and the post-truncation hits.len() to report the full and shown counts. Keep the
existing frontier_truncated/depth note and header/JSON truncation behavior
unchanged.

In `@src/calls/traverse.rs`:
- Around line 166-174: The depth-cap branch in the queue traversal loop
redundantly clones adjacency data through edges_at(&cur) solely to check whether
edges remain. Restructure the traversal around edges_at so each node’s edge list
is materialized at most once, reusing that result for the depth-cap
frontier_truncated check and subsequent expansion while preserving existing walk
behavior.

In `@src/core.rs`:
- Around line 725-759: Extract the field/private visibility predicate from the
local visible closure into a shared is_visible(&Declaration, &MapOptions)
helper. Replace the early-return checks near the declaration renderer, the
_filter_decls logic, and the max-members counting closure with this helper so
rendered output and JSON member caps use identical visibility rules.

In `@src/lib.rs`:
- Around line 845-861: Move the JSON envelope construction into a reusable
`CliError` method, such as `emit_json_only` backed by the existing envelope
builder, and update the `json_mode` parse-error path in `src/lib.rs` to
construct the error and call that method. Remove the duplicated
`serde_json::json!` field assembly there while preserving the existing hint and
JSON-only output behavior.

In `@tests/calls_e2e.rs`:
- Around line 1906-1941: Strengthen
callers_limit_bounds_the_unattributed_section_too by asserting that
unattributed_total equals the fixture’s expected three entries before checking
truncation. Then assert unattributed_truncated is true unconditionally, ensuring
the test always exercises the capped unattributed output.
- Around line 1759-1763: Update the assertion in the test around the existing
output check to parse and inspect the emitted JSON, asserting the target and
confidence for the explicit-receiver call rather than matching the renderer’s
spacing-dependent text. Follow the JSON assertion pattern used by the other
tests in the file, preserving the requirement that the local homonym is not
classified as Exact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 78ec2ac9-7e1c-4387-a4b8-8e138ed259fc

📥 Commits

Reviewing files that changed from the base of the PR and between d9bccef and c0ce6f8.

📒 Files selected for processing (27)
  • README.md
  • skills/ast-bro/SKILL.md
  • src/adapters/php.rs
  • src/calls/cli.rs
  • src/calls/mcp.rs
  • src/calls/render.rs
  • src/calls/resolve.rs
  • src/calls/trace.rs
  • src/calls/traverse.rs
  • src/cli_error.rs
  • src/context.rs
  • src/core.rs
  • src/deps/cli.rs
  • src/deps/render.rs
  • src/graph_cache/delta.rs
  • src/graph_cache/mod.rs
  • src/hook/decide.rs
  • src/impact.rs
  • src/lib.rs
  • src/mcp/tools.rs
  • tests/calls_e2e.rs
  • tests/cli_ergonomics.rs
  • tests/digest_format.rs
  • tests/show_markdown.rs
  • tests/surface_e2e.rs
  • wiki/architecture.md
  • wiki/calls.md

Comment thread skills/ast-bro/SKILL.md Outdated
Comment thread src/calls/render.rs Outdated
Comment thread src/calls/resolve.rs
Comment thread src/lib.rs
Comment thread src/mcp/tools.rs Outdated
Comment thread src/mcp/tools.rs
Comment thread wiki/calls.md Outdated
aeroxy and others added 4 commits July 29, 2026 02:01
Correctness:
- callees --depth 1 (one-hop path) now reports frontier_truncated when a
  resolved direct callee has outgoing edges, matching deeper walks.
- callers <Type> --limit bounds type groups too: implementations and
  constructions share the display budget after callable hits; groups
  carry implementations_total / constructions_total, text section
  headers say "(showing N; ...)", and top-level total/truncated compare
  combined-shown vs combined-total in both text and JSON.
- implements only accepts *type* declarations as proof the target
  exists; a function homonym no longer validates a 0-match answer.
- map --detail names honors --no-attrs / --no-lines (DigestOptions
  grew both switches; wired through CLI and MCP).
- --max-members counts raw declarations before overload collapse, so
  text "+N more" equals JSON dropped_members on overloaded APIs; free
  functions are never capped (matching JSON and the per-type contract).
- Pass A scoped-receiver match handles namespace-qualified receivers
  (Foo\Greeter::m(), a::b::Type::m()) via separator normalization with
  a terminal-segment fallback.
- surface: an Io error on an existing path exits 1 (index_error), not 2.

Performance:
- impact: one depth-monotonic multi-source reverse BFS
  (traverse::callers_multi) replaces a walk per construction site,
  which was quadratic-ish on widely-constructed types. Same results
  (consider still collapses to per-qn minimum depth).

Polish:
- CliError::envelope()/emit_json_only() — the ast-bro.error.v1 shape
  lives in one place; the clap parse-error path reuses it.
- MCP schemas constrain max_members with minimum: 0.
- SKILL.md: impact --depth default is 2; wiki/calls.md matches
  receiver_is_self_like and the self-like import gate.

Rejected (with reasoning, not changed): bounding the reverse-deps walk
by --limit again — the true total #32 requires needs the full BFS, which
is O(V+E) over the in-memory file graph, the same cost class as `graph`
and `cycles`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--no-docs now drops docs, docs_inside, and doc_start_byte from
ast-bro.map.v1; --no-lines drops start_line/end_line/start_byte/
end_byte; --no-attrs drops attrs. doc_start_byte rides with either
group (a doc artifact and a byte offset). Omission is opt-in per call:
with no projection flags the payload is serialized through the original
path and stays byte-identical, so no schema bump. The MCP map tool
inherits the behavior via the shared renderer.

(--no-private/--no-fields filtering and truncated/dropped_members
already reached JSON via the #37/#32 work; this closes the rest of the
flag set vlsi measured against 3.0.0.)

Measured on src/core.rs: 72.8KB -> 41.4KB with the full projection
stack; doc-heavy packages shed considerably more.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--no-private and the digest public-only view were no-ops for Rust:
every renderer filters on the literal "private", while the Rust adapter
emitted "" for un-pub items (its own comment said "Rust default is
private"). The adapter now emits the canonical string, with inherited
contexts exempted:

- private when the modifier is missing: module-scope fns/structs/enums/
  traits/mods, inherent-impl methods, named + tuple struct fields,
  extern-block items, non-#[macro_export] macros
- inherited ("" as before): trait method declarations, trait consts,
  trait-impl methods (they cannot carry a modifier and take the trait's
  reach), enum variants, impl-block containers

Also fixes two latent digest-renderer gaps this exposed: _flatten_types
and _flatten_free_functions never applied the visibility gate to types
or namespace recursion, so private types (and pub fns inside private
mods, e.g. #[cfg(test)] mod tests) leaked into the public-only text
view while the JSON filter dropped them. Text and JSON now agree.

Effect: `map --no-private` actually strips Rust private items;
`sb digest src/` on this repo shrinks 53.7KB -> 21.8KB and delivers the
documented "types and public methods"; map's default (and the Read-hook
substitution) is unchanged. Note for release notes: ast-bro.map.v1
payloads for Rust now carry "visibility": "private" where they carried
"" — a value change, not a shape change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aeroxy

aeroxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

@aeroxy

aeroxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Comment thread src/calls/resolve.rs Outdated
Comment thread src/adapters/rust.rs Outdated
Comment thread src/search/chunker.rs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 082e3cc

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/mcp/tools.rs (1)

490-541: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

run_map's signatures/full branch silently returns empty text — misaligned with the CLI's issue #33 fix.

When results is empty (e.g. paths contain nothing parseable), the else branch at Lines 533-540 builds an empty out and returns CallResult::Text("") silently. The CLI's equivalent path in run_map_digest explicitly guards this case and emits "# 0 parseable file(s) in the given path(s)", precisely because a silent empty answer is indistinguishable from a broken call. The names branch here is safe only because render_digest itself has that fallback; the signatures/full branch has no such guard.

🩹 Proposed fix
     } else {
+        if results.is_empty() {
+            return CallResult::Text("# 0 parseable file(s) in the given path(s)\n".to_string());
+        }
         let mut out = String::new();
         for res in &results {
             out.push_str(&core::render_map(res, &opts));
             out.push('\n');
         }
         CallResult::Text(out)
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/tools.rs` around lines 490 - 541, Update the signatures/full branch
of run_map to detect an empty results collection before rendering and return the
same "# 0 parseable file(s) in the given path(s)" message used by
run_map_digest. Preserve the existing rendering loop for non-empty results and
leave the JSON and names branches unchanged.
src/impact.rs (1)

351-371: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Ambiguous, hidden constructors still seed depth-2+ dependents.

ambiguous_hidden (line 358) gates the test_calls push at line 360 but not the ctor_walk_seeds push at lines 368-369. So when a construction edge is ambiguous and --hide-ambiguous/!opts.include_ambiguous is set, its own entry is hidden, but callers of that constructor still get traversed via callers_multi and reported as depth-2+ transitive dependents. The parallel implementor-construction path (lines 405-414) correctly continues on the same condition before seeding — this path doesn't.

🐛 Proposed fix
             // Callers of the constructors are depth-2+ dependents — seeded
             // into the shared walk below.
-            if opts.depth > 1 {
+            // Skip ambiguous, hidden constructors so their callers don't
+            // leak into depth-2+ output either (matches the
+            // implementor-construction path below).
+            if opts.depth > 1 && !ambiguous_hidden {
                 ctor_walk_seeds.push((e.source.clone(), 1));
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/impact.rs` around lines 351 - 371, Update the constructor loop around
collect_type_callers so ambiguous edges hidden by !opts.include_ambiguous are
skipped before adding ctor_walk_seeds. Match the parallel
implementor-construction path’s continue behavior, preventing hidden
constructors and their depth-2+ dependents from entering the traversal while
preserving existing handling for visible edges.
🧹 Nitpick comments (2)
skills/ast-bro/SKILL.md (1)

34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to the new fenced blocks.

markdownlint flags MD040 on both new fences; the surrounding examples in this file use the same bare form, so bash keeps them consistent and highlighted.

♻️ Proposed fix
-   ```
+   ```bash
    sb digest src/
    sb digest src/ --glob '*.java' --max-members 8
    ```

As per static analysis hints (MD040, fenced-code-language).

Also applies to: 40-42

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/ast-bro/SKILL.md` at line 34, Add the bash language identifier to both
newly introduced fenced code blocks in SKILL.md, including the block containing
the sb digest examples and the additional block referenced by the comment, while
preserving their existing commands and formatting.

Source: Linters/SAST tools

src/core.rs (1)

729-763: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated is_type predicate between text and JSON capping paths.

matches!(decl.kind, Class | Struct | Interface | Record | Enum) here is repeated verbatim in _filter_decls (Line 1430). Both drive the same --max-members cap and are covered by a test asserting parity between the text +N more and JSON dropped_members. Extracting a single fn _is_capped_type(kind: DeclarationKind) -> bool helper removes the risk of the two capping paths silently diverging if the type-kind list changes.

♻️ Proposed refactor
+fn _is_capped_type(kind: DeclarationKind) -> bool {
+    use DeclarationKind::*;
+    matches!(kind, Class | Struct | Interface | Record | Enum)
+}
+
 fn _render_decl(decl: &Declaration, opts: &MapOptions, indent: usize, out: &mut Vec<String>) {
     ...
-    let is_type = matches!(decl.kind, Class | Struct | Interface | Record | Enum);
+    let is_type = _is_capped_type(decl.kind);

And in _filter_decls:

-            let is_type = matches!(d.kind, Class | Struct | Interface | Record | Enum);
+            let is_type = _is_capped_type(d.kind);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core.rs` around lines 729 - 763, Extract the repeated type-kind predicate
into a shared _is_capped_type helper accepting DeclarationKind and returning
whether the kind is Class, Struct, Interface, Record, or Enum. Replace the local
is_type check in the text rendering path and the equivalent predicate in
_filter_decls, preserving identical --max-members behavior and parity between
text and JSON outputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/ast-bro/SKILL.md`:
- Line 15: Remove `context` from the `--depth` defaults list in the result-caps
documentation. Keep `context` documented with its `--budget` option and internal
depth-2 behavior, while retaining the `--depth` defaults for commands that
expose that flag.
- Line 11: Update the `--json` schema example in the command-output
documentation to use the emitted schema identifier `ast-bro.outline.v1` for
`map`/`digest`, replacing the nonexistent `ast-bro.map.v1`; keep the surrounding
stability and `--compact` behavior unchanged.

In `@src/lib.rs`:
- Around line 1159-1171: Update the read_to_string error handling in the squeeze
path: keep directory inputs as CliErrorKind::BadArgument, but classify other
read failures as the existing index_error/exit-1 error kind used for IO
failures, matching the surface handler’s behavior. Preserve the current
path-specific diagnostic details and JSON exit handling.

In `@wiki/calls.md`:
- Line 182: Update the traversal reference in the ambiguous-edge guidance to use
the exported entrypoint `src/deps/traverse.rs::forward` instead of the internal
`forward_bfs` primitive, leaving the surrounding dependency-closure instructions
unchanged.

---

Outside diff comments:
In `@src/impact.rs`:
- Around line 351-371: Update the constructor loop around collect_type_callers
so ambiguous edges hidden by !opts.include_ambiguous are skipped before adding
ctor_walk_seeds. Match the parallel implementor-construction path’s continue
behavior, preventing hidden constructors and their depth-2+ dependents from
entering the traversal while preserving existing handling for visible edges.

In `@src/mcp/tools.rs`:
- Around line 490-541: Update the signatures/full branch of run_map to detect an
empty results collection before rendering and return the same "# 0 parseable
file(s) in the given path(s)" message used by run_map_digest. Preserve the
existing rendering loop for non-empty results and leave the JSON and names
branches unchanged.

---

Nitpick comments:
In `@skills/ast-bro/SKILL.md`:
- Line 34: Add the bash language identifier to both newly introduced fenced code
blocks in SKILL.md, including the block containing the sb digest examples and
the additional block referenced by the comment, while preserving their existing
commands and formatting.

In `@src/core.rs`:
- Around line 729-763: Extract the repeated type-kind predicate into a shared
_is_capped_type helper accepting DeclarationKind and returning whether the kind
is Class, Struct, Interface, Record, or Enum. Replace the local is_type check in
the text rendering path and the equivalent predicate in _filter_decls,
preserving identical --max-members behavior and parity between text and JSON
outputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b55affa0-bafc-4487-b5e0-423621a22837

📥 Commits

Reviewing files that changed from the base of the PR and between d9bccef and 082e3cc.

📒 Files selected for processing (31)
  • README.md
  • skills/ast-bro/SKILL.md
  • src/adapters/php.rs
  • src/adapters/rust.rs
  • src/calls/cli.rs
  • src/calls/mcp.rs
  • src/calls/render.rs
  • src/calls/resolve.rs
  • src/calls/trace.rs
  • src/calls/traverse.rs
  • src/cli_error.rs
  • src/context.rs
  • src/core.rs
  • src/deps/cli.rs
  • src/deps/render.rs
  • src/graph_cache/delta.rs
  • src/graph_cache/mod.rs
  • src/hook/decide.rs
  • src/impact.rs
  • src/lib.rs
  • src/mcp/tools.rs
  • src/search/chunker.rs
  • tests/calls_e2e.rs
  • tests/cli_ergonomics.rs
  • tests/digest_format.rs
  • tests/show_markdown.rs
  • tests/surface_e2e.rs
  • wiki/architecture.md
  • wiki/calls.md
  • wiki/file-filtering.md
  • wiki/search.md

Comment thread skills/ast-bro/SKILL.md
Comment thread skills/ast-bro/SKILL.md Outdated
Comment thread src/lib.rs
Comment thread wiki/calls.md Outdated
aeroxy and others added 2 commits July 29, 2026 11:02
- squeeze: a read failure on an existing file exits 1 (index_error),
  matching the surface handler's Io mapping; a directory argument stays
  exit 2 (bad_argument).
- impact: constructions hidden by --hide-ambiguous no longer seed the
  transitive walk — same rule the implementor path already applied, so
  depth-2+ dependents of a hidden edge can't leak into the report.
- MCP map: zero parseable files returns the CLI's "# 0 parseable
  file(s) in the given path(s)" message instead of an empty string.
- core: extract _is_capped_type — one shared definition of which kinds
  --max-members caps, used by both the text renderer and the JSON
  filter so the two can't drift.
- SKILL.md: drop `context` from the --depth defaults list (it exposes
  no --depth; it walks to depth 2 internally under --budget).
- wiki/calls.md: pass C references src/deps/traverse.rs::forward (the
  actual exported entrypoint), not forward_bfs.

Rejected with reasoning: renaming ast-bro.map.v1 to "ast-bro.outline.v1"
(the latter does not exist — the binary emits ast-bro.map.v1, verified
live) and bash-tagging two fenced blocks (all 18 indented example
blocks in SKILL.md share the untagged style; partial tagging would be
inconsistent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(self)

- search/chunker: a line holding only spaces/tabs is still a blank line.
  The paragraph splitter consumes horizontal whitespace between line
  terminators (committing only when another terminator follows, so
  indentation opening a non-blank line stays put). Split points still
  land right after an ASCII \n — UTF-8 boundary and byte-exact coverage
  unchanged. LF + CRLF regression tests added.
- calls/resolve: drop the terminal-segment fallback for qualified
  receivers. `other::Type::method()` names another scope's Type; when
  the complete normalized receiver path doesn't match a local qn the
  edge defers to pass B/C (Inferred via dep closure, or honestly
  Ambiguous) instead of binding Exact to a same-file homonym. A PHP
  self-referencing FQN now resolves Inferred rather than Exact.
- adapters/rust: `pub(self)` / `pub(in self)` normalize to "private" —
  they grant no reach beyond the module. Wider restricted forms
  (pub(crate), pub(super), pub(in path)) deliberately stay visible in
  the public-only view, matching how C# internal and Java
  package-private have always been treated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aeroxy

aeroxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

Comment thread src/mcp/tools.rs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ae310db

@aeroxy

aeroxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

MCP map with an all-missing path list returned "# 0 parseable file(s)"
(or a valid-looking {"files": []} under json) with isError:false — the
exact confusion #33 fixed on the CLI, where walk_and_parse silently
drops missing inputs. The server has no stderr and no exit codes, so
the distinction must ride the error channel.

New resolve_paths_for_mcp (the MCP-side counterpart of require_paths)
returns Err for an empty list or a list where nothing resolves, and a
partial-miss note otherwise. Wired into map, digest, implements, and
run (which keeps its bare default "." and validates only explicit
paths). Text responses prepend the partial-miss note; JSON responses
stay pure. Existing paths with no parseable files remain a successful
empty answer.

Adds tests/mcp_e2e.rs — the first e2e coverage of the MCP server over
stdio: missing-path text + json reject with isError, empty-but-existing
dirs stay success, partial misses render with the note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/calls/resolve.rs Outdated
Comment thread src/core.rs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ae310db

- calls/resolve: when several lexical scopes in one file declare the
  same `Type::method` suffix, pass A no longer takes the first suffix
  match. A unique match still binds Exact; multiple matches bind only a
  candidate whose scope encloses the caller (innermost wins — what the
  unqualified path means in source). A caller outside every declaring
  scope defers to pass B/C, preserving candidates as Ambiguous.
- core/_filter_decls: apply the parent's --max-members cap to eligible
  direct children BEFORE recursing, so a capped subtree that the
  ancestor cap removes contributes nothing to dropped_members. JSON now
  equals the sum of the text renderer's "+N more" lines even with
  nested capped types (previously the removed subtree's internal drops
  were counted first and never rolled back). Eligibility lives in one
  _map_eligible predicate shared by the parent filter and the pre-cap
  child count.

Regression tests: mod-a/mod-b Foo::method scope binding (Exact to the
enclosing scope; no arbitrary Exact from outside), and a nested capped
class removed by an ancestor cap (dropped_members == Σ text "+N").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aeroxy

aeroxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

@aeroxy

aeroxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Comment thread src/calls/resolve.rs
Comment thread src/core.rs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f11285a

- adapters/rust: replace the `inherited: bool` parameter with a
  two-variant Vis enum (Owned | Inherited) so call sites name the
  semantics instead of passing a bare bool; same for
  _const_or_static_to_field.
- tests: pin the restricted-visibility design down — pub(self) /
  pub(in self) are hidden by --no-private, pub(crate) / pub(super) /
  pub(in crate) stay visible (the C# internal / Java package-private
  analogy). Extend the self-like receiver regression with a variable
  named `static` alongside the existing `parent` case.
- tests/mcp_e2e: bound the server wait at 30s (thread + recv_timeout,
  no new deps) and capture stderr into panic messages instead of
  nulling it.

Rejected from the review round: the "+N parsing is vacuous" claim (the
test asserts equality with a fixture-guaranteed 2, so a parse failure
reads 0 != 2 and fails loudly) and "update downstream prompt consumers"
(sb prompt is SKILL.md with frontmatter stripped — the contract ships
in the prompt by construction).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
tests/calls_e2e.rs (1)

1928-1941: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test passes vacuously if the fixture stops yielding unattributed sites.

With shown == 0 and unattributed_total == 0 every assertion holds, so a regression that drops unattributed reporting entirely would go unnoticed. Anchor the fixture expectation first.

💚 Proposed tightening
     let shown = doc["unattributed"].as_array().unwrap().len();
     let total = doc["unattributed_total"].as_u64().unwrap() as usize;
+    assert!(
+        total >= 2,
+        "fixture must produce unattributed sites for this test to mean anything: {out}"
+    );
     assert!(
         shown <= 1,
         "unattributed section must respect the remaining --limit budget: {out}"
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/calls_e2e.rs` around lines 1928 - 1941, Strengthen the test around the
callers JSON response by asserting the fixture produces at least one
unattributed site before validating limit and truncation behavior. Update the
assertions in the test using doc["unattributed_total"] so a zero total fails
explicitly, while preserving the existing shown-count and truncation checks.
src/core.rs (1)

750-754: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

visible duplicates _map_eligible minus the Heading/CodeBlock rule.

_is_capped_type was extracted precisely so the two renderers can't drift; the eligibility predicate is the other half of that contract and is still forked. Consider a shared predicate (taking the field/private/docs flags) used by both _render_decl and _map_eligible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core.rs` around lines 750 - 754, The visible predicate in the declaration
rendering flow duplicates eligibility logic from _map_eligible and can drift.
Extract or reuse a shared predicate, including field, private, and
documentation-related flags as needed, then update both visible and
_map_eligible to call it while preserving _map_eligible’s Heading/CodeBlock
exclusion.
tests/digest_format.rs (1)

488-547: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Substring assertions on the whole digest can false-negative via the temp path.

!digest.contains("Hidden") / "gain" also match the rendered directory and file names. Vanishingly unlikely with tempdir(), but scoping the negative checks to declaration lines (or p-stripped output) makes the test independent of the environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/digest_format.rs` around lines 488 - 547, Update
rust_missing_pub_means_private_except_inherited_contexts so negative assertions
inspect declaration content rather than the entire digest, avoiding matches from
the temporary directory or file path. Strip the input path from rendered output
or scope checks to relevant declaration lines, while preserving the existing
visibility assertions.
src/calls/render.rs (1)

76-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse section_suffix here.

This inlines the same string the new helper produces.

♻️ Proposed tidy-up
-    let suffix = if total > edges.len() {
-        format!(" (showing {}; raise --limit to see the rest)", edges.len())
-    } else {
-        String::new()
-    };
+    let suffix = section_suffix(total, edges.len());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/render.rs` around lines 76 - 80, Update the suffix construction in
the rendering flow to reuse the existing section_suffix helper instead of
duplicating its formatted string and conditional logic. Preserve the current
total-versus-edges behavior and pass the appropriate values to section_suffix.
src/calls/mcp.rs (1)

68-89: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Text-mode MCP responses drop the frontier signal.

frontier_truncated reaches JSON consumers but the text branch has no equivalent — the CLI compensates with a stderr note, which MCP lacks. Consider prepending it alongside hidden_note so a depth-capped walk isn't read as exhaustive.

♻️ Suggested addition
     } else {
         format!(
-            "{}{}",
+            "{}{}{}",
             hidden_note,
+            if frontier_truncated && depth > 1 {
+                format!(
+                    "# note: depth {} reached with unexplored edges beyond it; raise depth to walk further\n",
+                    depth
+                )
+            } else {
+                String::new()
+            },
             render::render_callers_text(target, &hits, &unattributed, unattributed_total, &trunc)
         )
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/mcp.rs` around lines 68 - 89, Update the non-JSON branch around
render_callers_text to include a text representation of frontier_truncated
alongside hidden_note before the rendered callers output, ensuring MCP text
responses indicate when the depth-capped walk is incomplete while preserving the
existing JSON behavior.
src/calls/traverse.rs (1)

219-227: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

frontier_truncated can fire when nothing new is reachable.

edges_at(&cur) being non-empty doesn't mean unexplored nodes remain — all targets may already be in seen/reported. The stderr hint ("raise --depth to walk further") then appears on walks that are actually complete. Checking for at least one unseen target would tighten it, at the cost of threading seen into the check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/traverse.rs` around lines 219 - 227, Update the depth-cap handling
in the traversal loop to set frontier_truncated only when edges_at(&cur)
contains at least one target not already present in seen or reported. Thread the
existing reachability state into this check, preserving the current depth-limit
behavior and avoiding the truncation hint when all targets were already
discovered.
src/lib.rs (1)

882-955: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Path-resolution loop is duplicated between require_paths and resolve_paths_for_mcp.

Both walk paths, split into existing/missing with identical expand_existing semantics; only the reporting differs. Extracting the split keeps CLI and MCP from drifting on what "resolved" means.

♻️ Suggested extraction
fn split_existing(paths: &[PathBuf]) -> (Vec<PathBuf>, Vec<String>) {
    let mut existing = Vec::new();
    let mut missing = Vec::new();
    for p in paths {
        if path_glob::expand_existing(p).is_empty() {
            missing.push(p.display().to_string());
        } else {
            existing.push(p.clone());
        }
    }
    (existing, missing)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 882 - 955, Extract the shared path partitioning
logic from require_paths and resolve_paths_for_mcp into a helper such as
split_existing(paths), returning existing paths and missing display strings.
Update both callers to use this helper while preserving their distinct CLI and
MCP error/reporting behavior.
tests/cli_ergonomics.rs (1)

139-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These two assertions are coupled to this repo's own source shape.

implements CliError flips to a non-empty match the moment anything impls CliError (e.g. a Display/From impl), and the stdout.len() > 25_000 threshold depends on src/ staying above the hint cutoff. A small fixture directory / a type that is intentionally never implemented would keep both tests asserting the contract rather than the repo's current contents.

Also applies to: 272-283

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_ergonomics.rs` around lines 139 - 146, Update the affected tests in
tests/cli_ergonomics.rs to use dedicated fixture sources instead of the
repository’s src/ tree and CliError. Define a fixture type that has no
implementations and keep the fixture contents intentionally sized to exercise
the intended hint-cutoff behavior, so the assertions remain stable as production
code changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/ast-bro/SKILL.md`:
- Line 34: Update both shell command fences in the documentation to use the bash
language tag, changing each opening fence to ```bash while preserving the
commands and closing fences.

In `@src/lib.rs`:
- Around line 789-795: Update the rejection-path argument collection in
src/lib.rs to use std::env::args_os() and convert arguments with
to_string_lossy() before collecting them as Vec<String>. Preserve the existing
json_mode and subcommand detection so non-UTF-8 arguments still follow the
exit-2 handling contract without panicking.

In `@src/mcp/tools.rs`:
- Around line 573-577: Preserve and surface the partial-miss note in the digest
response: update the result-building logic around resolve_paths_for_mcp and
walk_and_parse to use path_note instead of discarding it, prefixing non-JSON
output with the note consistently with run_map’s note handling.

In `@wiki/architecture.md`:
- Line 39: Update the exit-code documentation to clarify that exit code 0
indicates successful execution but may contain partial results, such as path
misses; require consumers to inspect stderr notes before treating output as
exhaustive. Apply this contract correction at wiki/architecture.md lines 39-39
and README.md lines 453-453, replacing wording that claims every exit-0 result
is complete.

---

Nitpick comments:
In `@src/calls/mcp.rs`:
- Around line 68-89: Update the non-JSON branch around render_callers_text to
include a text representation of frontier_truncated alongside hidden_note before
the rendered callers output, ensuring MCP text responses indicate when the
depth-capped walk is incomplete while preserving the existing JSON behavior.

In `@src/calls/render.rs`:
- Around line 76-80: Update the suffix construction in the rendering flow to
reuse the existing section_suffix helper instead of duplicating its formatted
string and conditional logic. Preserve the current total-versus-edges behavior
and pass the appropriate values to section_suffix.

In `@src/calls/traverse.rs`:
- Around line 219-227: Update the depth-cap handling in the traversal loop to
set frontier_truncated only when edges_at(&cur) contains at least one target not
already present in seen or reported. Thread the existing reachability state into
this check, preserving the current depth-limit behavior and avoiding the
truncation hint when all targets were already discovered.

In `@src/core.rs`:
- Around line 750-754: The visible predicate in the declaration rendering flow
duplicates eligibility logic from _map_eligible and can drift. Extract or reuse
a shared predicate, including field, private, and documentation-related flags as
needed, then update both visible and _map_eligible to call it while preserving
_map_eligible’s Heading/CodeBlock exclusion.

In `@src/lib.rs`:
- Around line 882-955: Extract the shared path partitioning logic from
require_paths and resolve_paths_for_mcp into a helper such as
split_existing(paths), returning existing paths and missing display strings.
Update both callers to use this helper while preserving their distinct CLI and
MCP error/reporting behavior.

In `@tests/calls_e2e.rs`:
- Around line 1928-1941: Strengthen the test around the callers JSON response by
asserting the fixture produces at least one unattributed site before validating
limit and truncation behavior. Update the assertions in the test using
doc["unattributed_total"] so a zero total fails explicitly, while preserving the
existing shown-count and truncation checks.

In `@tests/cli_ergonomics.rs`:
- Around line 139-146: Update the affected tests in tests/cli_ergonomics.rs to
use dedicated fixture sources instead of the repository’s src/ tree and
CliError. Define a fixture type that has no implementations and keep the fixture
contents intentionally sized to exercise the intended hint-cutoff behavior, so
the assertions remain stable as production code changes.

In `@tests/digest_format.rs`:
- Around line 488-547: Update
rust_missing_pub_means_private_except_inherited_contexts so negative assertions
inspect declaration content rather than the entire digest, avoiding matches from
the temporary directory or file path. Strip the input path from rendered output
or scope checks to relevant declaration lines, while preserving the existing
visibility assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5848e320-043a-4d49-b9a7-a2e456ca991a

📥 Commits

Reviewing files that changed from the base of the PR and between d9bccef and f11285a.

📒 Files selected for processing (32)
  • README.md
  • skills/ast-bro/SKILL.md
  • src/adapters/php.rs
  • src/adapters/rust.rs
  • src/calls/cli.rs
  • src/calls/mcp.rs
  • src/calls/render.rs
  • src/calls/resolve.rs
  • src/calls/trace.rs
  • src/calls/traverse.rs
  • src/cli_error.rs
  • src/context.rs
  • src/core.rs
  • src/deps/cli.rs
  • src/deps/render.rs
  • src/graph_cache/delta.rs
  • src/graph_cache/mod.rs
  • src/hook/decide.rs
  • src/impact.rs
  • src/lib.rs
  • src/mcp/tools.rs
  • src/search/chunker.rs
  • tests/calls_e2e.rs
  • tests/cli_ergonomics.rs
  • tests/digest_format.rs
  • tests/mcp_e2e.rs
  • tests/show_markdown.rs
  • tests/surface_e2e.rs
  • wiki/architecture.md
  • wiki/calls.md
  • wiki/file-filtering.md
  • wiki/search.md

Comment thread skills/ast-bro/SKILL.md Outdated
Comment thread src/lib.rs Outdated
Comment thread src/mcp/tools.rs Outdated
Comment thread wiki/architecture.md Outdated
aeroxy and others added 2 commits July 29, 2026 12:07
… JSON

- calls/resolve: crate::/self::/super:: receiver prefixes never match
  qns textually (qns start with the repo-relative file path). Strip the
  prefixes and resolve the remainder against the caller's scope chain
  by anchored equality — self:: at the caller's enclosing scope
  (innermost first), super:: one level up the same chain, crate:: at
  the file segment. No terminal-segment fallback; a miss still defers
  to pass B/C. Previously all three forms fell through and could stay
  Ambiguous despite naming their target explicitly.
- core/_map_eligible: markdown Heading/CodeBlock declarations are the
  structure of the file (their docs are empty), not doc comments — stop
  deleting them from JSON when include_docs is off. The detail-level
  work had made this catastrophic: digest README.md --json returned 0
  declarations against 29 headings + 23 code blocks in text. The JSON
  projection still strips documentation fields; the declarations stay.

Tests: crate/self/super forms each bind Exact to the enclosing scope's
Foo past a same-file decoy; markdown JSON now carries all 52
declarations with docs keys stripped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st rigor

Inline findings:
- lib.rs: the clap rejection path collects args via args_os() +
  to_string_lossy(), so a non-UTF-8 argument exits 2 per the contract
  instead of panicking std::env::args().
- MCP digest/implements: partial-miss path notes are prepended to
  non-JSON responses instead of discarded (matching map).
- docs: exit-0 wording in wiki/architecture.md and README no longer
  claims completeness — qualifications arrive as stderr notes; read
  them before treating output as exhaustive.
- SKILL.md: all 18 bare opening fences tagged ```bash (uniformly,
  resolving the earlier partial-tagging objection).

Nitpicks:
- MCP callers text carries the frontier note when an explicitly
  deepened walk was cut (same depth>1 gating as the CLI).
- render: unattributed_section reuses section_suffix.
- traverse: frontier_truncated fires only when something genuinely
  unexplored lies beyond the cap — back-edges to visited nodes no
  longer produce a raise---depth hint that would deliver nothing.
- core: _render_decl's early-return and member-visibility closure both
  call _map_eligible — one projection predicate for text and JSON.
- lib.rs: split_existing extracts the path partition shared by
  require_paths and resolve_paths_for_mcp.
- tests: unattributed-budget test asserts total >= 2 — which exposed
  the fixture only ever produced one unattributed site (the rest
  resolved Inferred via the dep closure), so the truncation branch had
  never run; rebuilt with three genuinely ambiguous sites. implements
  zero-impls test is fixture-local. Rust visibility test strips the
  temp path before negative assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aeroxy

aeroxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

Comment thread src/adapters/rust.rs
Comment thread src/mcp/tools.rs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b02278b

aeroxy and others added 2 commits July 29, 2026 12:28
The stripping loop collapsed crate::/self::/super:: into one boolean and
walked the caller's scope chain upward taking the first anchored match —
"search upward until found", not the per-prefix semantics claimed. An
ancestor decoy proved it: super::Foo::method() from inside `deep` bound
deep's own Foo instead of the parent's.

Each prefix now picks its own anchor:
- crate::P — one anchored trial at the file segment, no walk; an
  intermediate scope declaring the same path cannot shadow the root.
- super::^n P — the walk skips n levels past the caller's own scope
  before the first trial, so a caller-level decoy cannot shadow the
  parent.
- self::P — walk from the enclosing scope, as before.

Documented limitation (previously overstated): a method's qn carries a
type segment indistinguishable from a module, so super:: may start one
level early for methods and self:: keeps walking upward — anchored
equality keeps any hit real, and every case is no worse than before;
free functions are exact.

Regression tests pin both reviewer scenarios: the super:: ancestor
decoy and the crate:: intermediate-scope decoy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- adapters/rust: a trait impl inherits its trait's reach. During impl
  distribution (the one place the impl and the same-scope trait decl
  are both in hand), methods implementing a locally-declared private
  trait are marked private, so `impl Hidden for Loud` no longer leaks
  its methods into public-only map/digest output just because the type
  is pub. Traits declared elsewhere (other files, std) keep inherited
  visibility — their modifier is unknowable from this file.
- MCP json partial misses: resolve_paths_for_mcp returns the raw
  missing list; text responses keep the "# note: path not found" line
  (partial_note), and JSON responses inject the unresolved originals as
  a missing_paths field (with_missing_paths) so a partial payload can
  never read as covering inputs it never saw. Applied to map, digest,
  and implements; no-op (byte-identical payload) when everything
  resolved.

Tests: private-trait impl hidden in digest / shown in map default;
mcp_e2e drives map + digest with one existing + one missing path under
json:true and asserts missing_paths, the rendered file, and no phantom
field on clean requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@vlsi vlsi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 72a53b5. I built the branch and ran the binary against two real repositories: this one, and pgjdbc (~300k lines of Java) where I had an existing API-naming task to drive it end to end. cargo test --release --no-fail-fast passes except deps_e2e::go_module_prefix_strips_correctly, which is red on main too.

The error contract holds up on real code. I re-measured every case in the diagnostics table I keep for this tool — map /nope.java, surface /nope, show <file> NoSuchSymbol, implements NoSuchSymbol <path>, callers NoSuchSymbol, deps /nope.java, map with no paths, and an unknown flag — and all eight now behave identically: exit 2, empty stdout, message on stderr. The table collapsed into one sentence, which is the point of #33/#36. The map/digest merge also came through clean: map --preset digest --json is byte-identical to digest --json, and --max-members text/JSON parity matched exactly on four files I checked.

Two things I would fix before merge, plus three smaller ones.

Blocking — the unattributed section is unusable on common method names. Details inline on src/calls/traverse.rs. On pgjdbc, callers PgConnection.close returns 0 resolved callers and 1124 unattributed; the first three are connection.close(), os.close() on an OutputStream, and lo.close() on a LargeObject. In this repository, callers CliError.new returns 1 resolved and 1085 unattributed — every Vec::new() and String::new() in the tree.

implements <TARGET> now rejects a call that --help says is valid. Details inline on src/lib.rs.

For the record, on precise targets the section does exactly what #31 promised, and that is worth keeping. On pgjdbc, callers CodecContextBuilder.registry reports 2 resolved (Inferred) plus 5 unattributed against 8 hits for git grep '\.registry(', the eighth being the declaration. CodecContextBuilder.prefersJavaTime reports 0 plus 1, and grep finds exactly one real call site. Fluent chains through an interface-typed receiver still do not resolve, but they no longer vanish silently. The fix is to gate the section on whether the name discriminates, not to remove it.

Three smaller items I did not anchor inline:

  • digest --json drops docs from ast-bro.map.v1 without a schema bump. Measured on src/core.rs: 18 docs keys and 55,586 bytes on main, 0 keys and 16,675 bytes here. The README documents the behavior, but the caller never opted in — the preset did — and the schema constant is unchanged, so a consumer indexing docs breaks with no signal to guard on. A release-note line would cover it; a v2 would be safer.
  • callees <Type> did not get the --limit fix that callers <Type> got in 45312b0. Ancestor groups are neither bounded by --limit nor counted in total, so truncated in ast-bro.callees.v1 describes only matches. Inheritance chains are short, so this is a consistency gap rather than a volume problem.
  • callers, impact, and reverse-deps now walk with usize::MAX to compute a true total. At the default --depth 1 that costs nothing; at --depth 5 on a large repository it is the entire reverse cone, where the old code stopped at 200 hits. The trade-off looks right, but --limit no longer bounds work, and the docs still read as though it does.

Comment thread src/calls/traverse.rs
Comment thread src/calls/cli.rs
Comment thread src/lib.rs
Comment thread src/calls/resolve.rs
Comment thread README.md

@vlsi vlsi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more, found while re-measuring the cap reporting on real repositories.

Comment thread src/calls/cli.rs Outdated
aeroxy and others added 2 commits July 30, 2026 22:23
The unresolved-call-site section (#31) keyed only on the callee name, and
candidate sets are keyed on the bare name too, so every `Vec::new()` in the
tree carried `CliError::new` among its candidates: `callers CliError.new`
reported 1 resolved caller and 1020 "possible" ones, `callers
PgConnection.close` on pgjdbc 0 and 1124. A number that size reads as a
rename cost while describing other symbols entirely — the opposite of what
the section is for. Three narrowings, none of them type inference:

- receiver check: a receiver naming a project type other than the target's
  enclosing type meant that other type (pass A's rule, one layer later).
  Locals, parameters and external types stay in, or the real
  `connection.close()` sites would go with the noise.
- discrimination gate: `name_declarers` counts the project symbols sharing
  the target's terminal name; past 3 the rows are withheld for a one-line
  count that states the reason. JSON carries `unattributed_declarers` and
  `unattributed_suppressed`, so MCP stops shipping 199 unattributable rows
  into an agent's context.
- own cap of min(--limit, 25), ordered by ambiguity breadth ascending, with
  the receiver on every row. Sharing --limit gave the worst-resolved targets
  the most screen.

Also from the review round:

- `implements <TARGET>` declared PATHS optional in clap while rejecting the
  call at runtime, and blamed a shell substitution for a missing argument.
  Required in clap now, and the error envelope names the argument instead of
  quoting clap's first line.
- the `crate::` anchor fired only when the caller's own file declared a
  matching path — precisely the case where anchoring there is wrong. Gated
  on the caller being the crate root. (Pass C can still prefer a same-file
  decoy; it no longer claims Exact.)
- `callees` printed the post-cap count in its stdout header while stderr and
  JSON reported the true total, and `--hide-external` left header, note and
  `total` counting three different populations. One filter, applied before
  counting, in both CLI and MCP.
- `callees <Type>` ancestor groups now count toward `total` and share the
  display budget, as `callers <Type>` already did.
- `run`, `search` and `find-related` didn't follow the error contract the
  README claims for every subcommand: unresolved paths now exit 2 with empty
  stdout and an `ast-bro.error.v1` envelope, and `find-related --json` stops
  answering "nothing is similar" for a location the index never saw. `run`'s
  exit 1 on no matches is grep convention and is now documented as such.
- `map --json` payloads with keys removed carry `projected: {docs,
  line_numbers, attributes}`, so a consumer of `digest --json` has a signal
  to guard on rather than an absence to infer.
- `--limit` help and docs now say it caps display, not the walk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`apply_delta_to_calls` step 9 re-resolved bare edges by assigning
`symbol_table[name]` — the *unfiltered* global match list — as the edge's
candidate set, and it did so for every bare edge in the graph, not just the
ones the delta touched. A cold build gets there through pass C, which filters
candidates to what the caller's file can reach through the dep graph, so the
two paths disagreed and one edit to an unrelated file changed answers
everywhere: on ast-bro itself, `callers CliError.new` reported 1023
unresolved sites cold and 1073 after appending a comment to
src/adapters/sql.rs, because every `Vec::new()` in the tree had just been
handed `CliError::new` as a candidate the dep filter had ruled out.

Pass C's decision is now one function, `resolve::disambiguate`, used by both
the cold build and the updater, with the per-file dep closure memoized
(`ClosureCache`) since a sweep re-asks for it repeatedly. Pass B's rule —
single global match, no receiver — still runs first, unchanged.

Parity across 15 symbols × callers/callees on this repo is now byte-identical
between a partial update and a cold build of the same content. The regression
test asserts it on a fixture where the dep filter leaves two of three
homonyms, which is the shape that diverged; it fails against the old code.

Closes the "pass C is not re-run in the partial-update path" known gap in
wiki/calls.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aeroxy

aeroxy commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@aeroxy

aeroxy commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

/agentic_review

Comment thread src/calls/cli.rs
Comment on lines +186 to +190
let unattributed = render::Unattributed {
edges: &unattributed,
total: unattributed_total,
hidden: unattributed_hidden,
declarers,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Hidden unattributed total wrong 🐞 Bug ≡ Correctness

run_callers computes unattributed_total after clearing unresolved call sites when ambiguous
edges are hidden, so JSON/text can report unattributed_total: 0 even when `unattributed_hidden >
0. This breaks the documented Unattributed` contract (“true total behind them”) and misleads
consumers about how many unresolved call sites existed.
Agent Prompt
### Issue description
When `--hide-ambiguous` (CLI) / `include_ambiguous:false` (MCP) hides unresolved call sites, the code clears the `unattributed` vector and *then* sets `unattributed_total = unattributed.len()`. This makes `Unattributed.total` exclude hidden sites, contradicting the renderer contract that `total` is the true pre-filter count.

### Issue Context
`src/calls/render.rs` documents `Unattributed.total` as the “true total behind them” and `hidden` as the count dropped by `--hide-ambiguous`. Both the CLI and MCP implementations currently lose the pre-hide total.

### Fix Focus Areas
- src/calls/cli.rs[162-191]
- src/calls/mcp.rs[50-80]

### What to change
- Capture `unattributed_total` **before** any hiding/truncation, e.g. immediately after computing/filtering `unattributed`.
- If you want `total` to always include hidden rows, set `unattributed_total` to the pre-hide length (or compute it as `unattributed.len() + unattributed_hidden` after clearing, ensuring it matches the intended semantics).
- Keep `edges` as the displayed sample (possibly empty when hidden), but keep `total` as the true count so JSON fields like `unattributed_total` remain accurate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ee39be9

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/mcp/tools.rs (1)

896-903: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Reverse traversal is still unbounded while limit only trims the display.

reverse(..., usize::MAX, ...) materializes the full reachable importer set before hits.truncate(a.limit), so limit no longer bounds traversal or memory on large repos. Same concern as previously raised; if the true-total requirement makes a bounded page impractical, a counting-only walk that doesn't retain every DepHit would preserve both properties.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/tools.rs` around lines 896 - 903, Update the reverse traversal around
crate::deps::traverse::reverse so limit bounds retained results and memory
instead of traversing with usize::MAX and truncating afterward. Preserve the
true total by using a counting-only traversal or equivalent approach that counts
all reachable importers without materializing every DepHit, while returning only
the first a.limit hits.
🧹 Nitpick comments (6)
tests/show_markdown.rs (1)

49-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that rejected show calls keep stdout empty.

This test pins the exit code and stderr, but not the no-stdout portion of the CLI error contract.

Proposed test addition
     assert_eq!(out.status.code(), Some(2), "substring must not match a code symbol");
+    assert!(
+        out.stdout.is_empty(),
+        "rejected show call wrote to stdout:\n{}",
+        String::from_utf8_lossy(&out.stdout)
+    );
     let stderr = String::from_utf8(out.stderr).expect("utf8");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/show_markdown.rs` around lines 49 - 58, Update the rejected
show-command test around Command::new(bin()) to assert that out.stdout is empty,
while preserving the existing exit-code and stderr assertions. This should
verify the CLI error contract for substring-matching failures without changing
production behavior.
tests/digest_format.rs (1)

614-620: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Substring overlap makes the failure message misleading.

"in_self_only" contains "self_only", so a regression that leaks only in_self_only fails the self_only assertion first and points at the wrong item. Matching on the rendered token (e.g. self_only()) disambiguates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/digest_format.rs` around lines 614 - 620, Update the hidden-item
assertions in the no-private output test to match each rendered token
distinctly, such as using the complete `self_only()` and `in_self_only()` forms,
so substring overlap cannot report the wrong item; keep the shown-item
visibility assertions unchanged.
tests/calls_e2e.rs (1)

2511-2593: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider pinning the cache-invalidation trigger rather than relying on mtime resolution.

The delta path is entered because src/unrelated.rs is rewritten immediately after the cold build; on filesystems with coarse mtime granularity the edit can land in the same tick and the re-query may silently take the "nothing changed" path, making the parity assertions pass vacuously. Asserting that the incremental run actually saw a delta (or nudging mtime explicitly) would keep the test honest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/calls_e2e.rs` around lines 2511 - 2593, Make
incremental_update_matches_a_cold_build_of_the_same_content explicitly trigger
cache invalidation after rewriting unrelated.rs, rather than relying on
filesystem mtime resolution. Nudge the file’s modification timestamp to a later
value using the test’s available filesystem/time utilities, or assert the
existing delta-path indicator if the query exposes one, before issuing the
incremental query. Keep the parity assertions unchanged.
tests/cli_ergonomics.rs (1)

276-296: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These two tests encode the repo's own directory sizes.

src/ staying above 25 KB is safe, but src/hook/ staying below the threshold is a property of current sources, so ordinary growth will flip map_on_small_input_stays_quiet into a failure unrelated to the hint logic. A tempfile fixture (one tiny file vs. many generated ones) pins both sides of the threshold to the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_ergonomics.rs` around lines 276 - 296, Replace the
source-directory-dependent coverage in
map_on_large_directory_hints_at_digest_preset and map_on_small_input_stays_quiet
with tempfile-based fixtures: create a deliberately small input below the
threshold and a generated directory above it, then invoke map using those paths.
Preserve the assertions for exit status, stdout size where applicable, and
digest-preset hints only on stderr for the large fixture.
src/lib.rs (1)

806-810: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Subcommand detection can name a flag value instead of the subcommand.

find(|a| !a.starts_with('-')) picks the first non-dash token, which for a global-flag-with-value form (e.g. --some-value foo bogus-sub) is the flag's value, not the subcommand. Envelope command and the `{}` has no `{}` flag detail then name the wrong thing. Using e.get(ContextKind::InvalidSubcommand) / clap's usage context, or matching the token against Cli::command().get_subcommands(), keeps the field honest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 806 - 810, Update subcommand detection in the raw
argument parsing around the subcommand variable so flag values are not treated
as subcommands. Match tokens against the actual subcommands exposed by
Cli::command() (or use clap’s InvalidSubcommand/usage context), and retain the
existing ast-bro fallback when no valid subcommand is found.
README.md (1)

444-445: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stale JSON schema table now that rejections are covered.

map and digest both document ast-bro.map.v1 elsewhere, and rejections are described in the paragraph below, so the standalone table with outdated ast-bro.outline.v1 entries is misleading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 444 - 445, Remove the stale JSON schema table entry
from README.md, including the outdated ast-bro.outline.v1/ast-bro.error.v1
documentation. Keep the existing ast-bro.map.v1 references for map and digest
and the paragraph describing rejected calls unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/graph_cache/delta.rs`:
- Around line 258-286: The single-candidate promotion in the delta update loop
should match cold-build confidence semantics. In the `calls.forward` iteration,
change the `Confidence` assigned alongside `CallTarget::Resolved` for a unique
global symbol-table match to the same exact-confidence value used by Pass B;
leave the receiver check and delegated `resolve::disambiguate` path unchanged.

In `@src/mcp/tools.rs`:
- Around line 1024-1027: Update the run tool’s resolve_paths_for_mcp handling to
retain both resolved and missing paths instead of discarding the missing list.
Prefix text responses with partial_note(&missing), and wrap JSON responses with
with_missing_paths(..., &missing), while preserving the existing error handling
for resolution failures.

In `@wiki/calls.md`:
- Around line 313-315: Update the “Bare-name re-resolution” documentation to
describe single-match, no-receiver hits as promoting to Resolved/Exact, matching
Pass B’s documented result and confidence semantics; leave the disambiguation
behavior and parity explanation unchanged.

---

Duplicate comments:
In `@src/mcp/tools.rs`:
- Around line 896-903: Update the reverse traversal around
crate::deps::traverse::reverse so limit bounds retained results and memory
instead of traversing with usize::MAX and truncating afterward. Preserve the
true total by using a counting-only traversal or equivalent approach that counts
all reachable importers without materializing every DepHit, while returning only
the first a.limit hits.

---

Nitpick comments:
In `@README.md`:
- Around line 444-445: Remove the stale JSON schema table entry from README.md,
including the outdated ast-bro.outline.v1/ast-bro.error.v1 documentation. Keep
the existing ast-bro.map.v1 references for map and digest and the paragraph
describing rejected calls unchanged.

In `@src/lib.rs`:
- Around line 806-810: Update subcommand detection in the raw argument parsing
around the subcommand variable so flag values are not treated as subcommands.
Match tokens against the actual subcommands exposed by Cli::command() (or use
clap’s InvalidSubcommand/usage context), and retain the existing ast-bro
fallback when no valid subcommand is found.

In `@tests/calls_e2e.rs`:
- Around line 2511-2593: Make
incremental_update_matches_a_cold_build_of_the_same_content explicitly trigger
cache invalidation after rewriting unrelated.rs, rather than relying on
filesystem mtime resolution. Nudge the file’s modification timestamp to a later
value using the test’s available filesystem/time utilities, or assert the
existing delta-path indicator if the query exposes one, before issuing the
incremental query. Keep the parity assertions unchanged.

In `@tests/cli_ergonomics.rs`:
- Around line 276-296: Replace the source-directory-dependent coverage in
map_on_large_directory_hints_at_digest_preset and map_on_small_input_stays_quiet
with tempfile-based fixtures: create a deliberately small input below the
threshold and a generated directory above it, then invoke map using those paths.
Preserve the assertions for exit status, stdout size where applicable, and
digest-preset hints only on stderr for the large fixture.

In `@tests/digest_format.rs`:
- Around line 614-620: Update the hidden-item assertions in the no-private
output test to match each rendered token distinctly, such as using the complete
`self_only()` and `in_self_only()` forms, so substring overlap cannot report the
wrong item; keep the shown-item visibility assertions unchanged.

In `@tests/show_markdown.rs`:
- Around line 49-58: Update the rejected show-command test around
Command::new(bin()) to assert that out.stdout is empty, while preserving the
existing exit-code and stderr assertions. This should verify the CLI error
contract for substring-matching failures without changing production behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fdfd21be-6f6c-45d3-82cb-266e683c9cf4

📥 Commits

Reviewing files that changed from the base of the PR and between d9bccef and ee39be9.

📒 Files selected for processing (33)
  • README.md
  • skills/ast-bro/SKILL.md
  • src/adapters/php.rs
  • src/adapters/rust.rs
  • src/calls/cli.rs
  • src/calls/mcp.rs
  • src/calls/render.rs
  • src/calls/resolve.rs
  • src/calls/trace.rs
  • src/calls/traverse.rs
  • src/cli_error.rs
  • src/context.rs
  • src/core.rs
  • src/deps/cli.rs
  • src/deps/render.rs
  • src/graph_cache/delta.rs
  • src/graph_cache/mod.rs
  • src/hook/decide.rs
  • src/impact.rs
  • src/lib.rs
  • src/mcp/tools.rs
  • src/search/chunker.rs
  • src/search/cli.rs
  • tests/calls_e2e.rs
  • tests/cli_ergonomics.rs
  • tests/digest_format.rs
  • tests/mcp_e2e.rs
  • tests/show_markdown.rs
  • tests/surface_e2e.rs
  • wiki/architecture.md
  • wiki/calls.md
  • wiki/file-filtering.md
  • wiki/search.md

Comment thread src/graph_cache/delta.rs
Comment thread src/mcp/tools.rs
Comment on lines +1024 to +1027
match crate::resolve_paths_for_mcp("run", &a.paths) {
Ok((paths, _)) => paths,
Err(e) => return CallResult::Error(e),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

run discards its unresolved paths silently.

Every other tool in this file now reports partial misses via partial_note / with_missing_paths; here the missing half is dropped, so a caller passing one good and one bad path gets a clean-looking match list that never mentions the path it didn't search. MCP has no stderr, so the signal has to ride the payload.

🔧 Keep the missing list and surface it
-        match crate::resolve_paths_for_mcp("run", &a.paths) {
-            Ok((paths, _)) => paths,
-            Err(e) => return CallResult::Error(e),
-        }
+        match crate::resolve_paths_for_mcp("run", &a.paths) {
+            Ok((paths, miss)) => {
+                missing = miss;
+                paths
+            }
+            Err(e) => return CallResult::Error(e),
+        }

Then prefix the text output with partial_note(&missing) and wrap the JSON returns in with_missing_paths(..., &missing).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/tools.rs` around lines 1024 - 1027, Update the run tool’s
resolve_paths_for_mcp handling to retain both resolved and missing paths instead
of discarding the missing list. Prefix text responses with
partial_note(&missing), and wrap JSON responses with with_missing_paths(...,
&missing), while preserving the existing error handling for resolution failures.

Comment thread wiki/calls.md
Comment on lines +313 to +315
7. Bare-name re-resolution: for every `Bare` edge, look up its name in the (now updated) symbol table. Single-match, no-receiver hits promote to `Resolved/Inferred` (pass B's rule, receiver suppression included); everything else goes through `resolve::disambiguate` — the *same* function pass C uses — so candidates are filtered to what the caller's file can reach through the dep graph, with a memoized per-file closure. Picks up two cases the partial path would otherwise miss: edges demoted in step 6 whose target moved to a different file, and pre-existing `Bare` edges in unchanged files that finally have a target because a *new* file in this delta defines it.

The invariant is parity — a partial update must produce the graph a cold build of the same content produces. This step used to assign the *unfiltered* symbol-table entry as an edge's candidate set, which broke that: it ran over the whole graph, so one edit to an unrelated file gave every bare edge in the project candidates the dep filter had ruled out. Measured on ast-bro itself, `callers CliError.new` reported 1023 unresolved sites cold and 1073 after appending a comment to `src/adapters/sql.rs` — every `Vec::new()` in the tree newly counted as a possible caller. Reusing pass C's own decision function is what keeps the two paths honest; `tests/calls_e2e.rs::incremental_update_matches_a_cold_build_of_the_same_content` pins it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use Exact for the reused single-candidate Pass B result.

Line 313 calls this “pass B's rule,” but Pass B documents a single candidate as Resolved/Exact. Calling it Inferred contradicts the stated cold-build parity and misstates confidence semantics.

Proposed documentation fix
-7. Bare-name re-resolution: for every `Bare` edge, look up its name in the (now updated) symbol table. Single-match, no-receiver hits promote to `Resolved/Inferred` (pass B's rule, receiver suppression included);
+7. Bare-name re-resolution: for every `Bare` edge, look up its name in the (now updated) symbol table. Single-match, no-receiver hits promote to `Resolved/Exact` (pass B's rule, receiver suppression included);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
7. Bare-name re-resolution: for every `Bare` edge, look up its name in the (now updated) symbol table. Single-match, no-receiver hits promote to `Resolved/Inferred` (pass B's rule, receiver suppression included); everything else goes through `resolve::disambiguate` — the *same* function pass C uses — so candidates are filtered to what the caller's file can reach through the dep graph, with a memoized per-file closure. Picks up two cases the partial path would otherwise miss: edges demoted in step 6 whose target moved to a different file, and pre-existing `Bare` edges in unchanged files that finally have a target because a *new* file in this delta defines it.
The invariant is parity — a partial update must produce the graph a cold build of the same content produces. This step used to assign the *unfiltered* symbol-table entry as an edge's candidate set, which broke that: it ran over the whole graph, so one edit to an unrelated file gave every bare edge in the project candidates the dep filter had ruled out. Measured on ast-bro itself, `callers CliError.new` reported 1023 unresolved sites cold and 1073 after appending a comment to `src/adapters/sql.rs` — every `Vec::new()` in the tree newly counted as a possible caller. Reusing pass C's own decision function is what keeps the two paths honest; `tests/calls_e2e.rs::incremental_update_matches_a_cold_build_of_the_same_content` pins it.
7. Bare-name re-resolution: for every `Bare` edge, look up its name in the (now updated) symbol table. Single-match, no-receiver hits promote to `Resolved/Exact` (pass B's rule, receiver suppression included); everything else goes through `resolve::disambiguate` — the *same* function pass C uses — so candidates are filtered to what the caller's file can reach through the dep graph, with a memoized per-file closure. Picks up two cases the partial path would otherwise miss: edges demoted in step 6 whose target moved to a different file, and pre-existing `Bare` edges in unchanged files that finally have a target because a *new* file in this delta defines it.
The invariant is parity — a partial update must produce the graph a cold build of the same content produces. This step used to assign the *unfiltered* symbol-table entry as an edge's candidate set, which broke that: it ran over the whole graph, so one edit to an unrelated file gave every bare edge in the project candidates the dep filter had ruled out. Measured on ast-bro itself, `callers CliError.new` reported 1023 unresolved sites cold and 1073 after appending a comment to `src/adapters/sql.rs` — every `Vec::new()` in the tree newly counted as a possible caller. Reusing pass C's own decision function is what keeps the two paths honest; `tests/calls_e2e.rs::incremental_update_matches_a_cold_build_of_the_same_content` pins it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wiki/calls.md` around lines 313 - 315, Update the “Bare-name re-resolution”
documentation to describe single-match, no-receiver hits as promoting to
Resolved/Exact, matching Pass B’s documented result and confidence semantics;
leave the disambiguation behavior and parity explanation unchanged.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 5 seconds.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/calls/cli.rs (1)

261-297: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep total consistent across --depth.

depth <= 1 extends callees_one_hop, which returns every direct call-site edge, while depth >= 2 uses BFS deduping by resolved target qn. That makes # N callee(s) change meaning across depth 12/3 and can make JSON total and shown diverge unexpectedly for duplicate call sites to the same callee. Either extend the traversal contract for depth 1 to report total resolved targets or count it in the same reported-target terms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/cli.rs` around lines 261 - 297, Make the depth <= 1 path use the
same deduplicated resolved-target counting semantics as the depth >= 2 path,
while preserving all direct call-site edges for display. Update the total
calculation around callees_one_hop and all_edges so duplicate call sites to one
resolved callee contribute once, keeping JSON total and shown consistent across
depths.
🧹 Nitpick comments (14)
src/impact.rs (1)

476-483: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the truncation-suffix formatting.

The "showing {} — raise --limit to see the rest" phrasing is now hand-built in three places in this file (here, Line 686-695, Line 777-785) plus src/deps/render.rs. A single helper (e.g. fn count_suffix(total: usize, shown: usize) -> String) keeps the agent-facing wording from drifting between sections.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/impact.rs` around lines 476 - 483, Extract the repeated truncation suffix
formatting into a shared helper such as count_suffix(total: usize, shown:
usize), then replace the hand-built “showing … raise --limit …” wording in this
formatting block and the corresponding sections around the other affected-count
messages, including the dependency renderer. Preserve the existing wording and
values while centralizing its construction.
tests/digest_format.rs (4)

437-445: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Negative assertions here don't strip the temp path.

rust_missing_pub_means_private_except_inherited_contexts (Line 526) deliberately replaces the fixture path before asserting absence, because the rendered header carries the random temp path. Applying the same strip here keeps these two assertions from depending on temp-dir naming.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/digest_format.rs` around lines 437 - 445, Update the test around the
`without` result and its `--no-attrs`/`--no-lines` assertions to remove the
temporary fixture path before checking for absent content, following the
path-normalization approach used by
`rust_missing_pub_means_private_except_inherited_contexts`. Keep the existing
assertions and messages unchanged after normalization.

448-486: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider also pinning the projected metadata block.

render_json_map (src/core.rs:1573-1583) adds a projected object whenever anything is stripped, and omits it otherwise. That's the field consumers guard on, but no assertion covers it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/digest_format.rs` around lines 448 - 486, Extend the
json_honors_projection_flags test to assert the projected metadata block: the
default payload must omit projected, while calls using --no-docs, --no-lines, or
--no-attrs must include it.

533-535: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

gain doesn't test visibility here.

digest defaults to include_fields: false (DigestOptions::default(), src/core.rs:293-302), so the private field is hidden because fields are suppressed, not because it's non-pub. The map --no-private check at Line 539-546 is the one that actually exercises field visibility; consider dropping gain from this list or adding --include-fields to make the claim real.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/digest_format.rs` around lines 533 - 535, The visibility assertion in
the digest test incorrectly includes gain while fields are disabled by default.
Remove gain from the hidden-name list, or update the digest invocation to enable
fields so the assertion actually verifies its non-public visibility; keep the
existing map --no-private coverage unchanged.

412-418: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider asserting the absence of a cap note too.

contains("a()") proves the names survive but not that no cap fired. Adding assert!(!text.contains("+"))-style negative check (or matching "more member(s)") pins the actual claim of the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/digest_format.rs` around lines 412 - 418, Strengthen the assertions in
the map detail test around run by verifying the output does not contain the
cap-indicator text (such as the “more member(s)” note) after confirming a(),
b(), and c() are present. Use the formatter’s actual cap-note wording rather
than a broad symbol check.
src/mcp/tools.rs (1)

640-647: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated partial-miss note prefixing across MCP tools. partial_note returns the note but each tool hand-rolls the same match &path_note { Some(n) => format!("{}\n{}", n, out), None => out } prefixing, so text-mode note formatting can drift per tool. Add one helper (e.g. fn with_note(note: &Option<String>, out: String) -> String) next to partial_note and use it everywhere.

  • src/mcp/tools.rs#L640-L647: replace the match &path_note block in run_digest with the shared helper.
  • src/mcp/tools.rs#L782-L785: replace the match &path_note block in run_implements with the shared helper (and reuse it for run_map's note closure at Line 539-542).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/tools.rs` around lines 640 - 647, The partial-miss note prefixing is
duplicated across MCP tools and should use one shared formatter. In
src/mcp/tools.rs lines 640-647, replace run_digest’s path_note match with a
helper such as with_note; in lines 782-785, make the same change in
run_implements; also reuse the helper in run_map’s note closure at lines
539-542. Define the helper next to partial_note and preserve the existing
note-before-output formatting.
src/lib.rs (1)

538-546: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

map/digest help still advertises PATHS as optional while the runtime rejects an empty list.

implements was made required = true so --help and the runtime agree; MapArgs.paths deliberately stays optional to surface the no_input envelope. Both behaviours are defensible, but map --help printing [PATHS]... while ast-bro map exits 2 is the same mismatch that was fixed for implements. Consider documenting the intent in the doc comment (or value_name) so the divergence is explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 538 - 546, Update the MapArgs paths argument
documentation or value_name configuration to explicitly state that paths may be
omitted and empty input produces the no_input envelope, while preserving the
existing runtime validation and optional argument behavior for both map and
digest.
src/calls/resolve.rs (2)

349-356: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

file.contains("/bin/") matches more than crate roots.

Any path with a bin/ component (tools/bin/helper.rs, vendor/bin/x.rs) is treated as a crate root, so the crate:: arm can fire in a file that isn't one. The blast radius is small — anchored equality still requires the caller's own file to declare the full crate::P::name path — but the check is cheap to tighten to Cargo's actual layout.

♻️ Suggested tightening
 fn is_crate_root(file: &str) -> bool {
     let name = file.rsplit('/').next().unwrap_or(file);
-    matches!(name, "lib.rs" | "main.rs") || file.contains("/bin/")
+    matches!(name, "lib.rs" | "main.rs")
+        || file.contains("src/bin/")
+        || file.starts_with("bin/")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/resolve.rs` around lines 349 - 356, Update is_crate_root so
directory-based detection only recognizes Cargo binary crate roots directly
under the repository’s src/bin directory, rather than any path containing a bin
component. Preserve the existing lib.rs and main.rs checks, and keep paths such
as tools/bin/helper.rs and vendor/bin/x.rs from being classified as crate roots.

358-377: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Python's cls isn't in the self-like set.

cls.helper() inside a @classmethod names the enclosing scope exactly like self.helper(), so it currently misses same-file/import binding and falls through to pass B/C. Conservative (no wrong Exact), but a cheap addition if Python classmethods matter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/resolve.rs` around lines 358 - 377, Add Python’s "cls" receiver to
receiver_is_self_like so cls.helper() in classmethods follows the same
enclosing-scope promotion path as self-like receivers. Update the function’s
explanatory comment to document Python classmethod usage while preserving all
existing receiver behavior.
tests/cli_ergonomics.rs (2)

60-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the envelope parser.

This seven-line find-and-parse block is repeated verbatim seven times (Lines 60-64, 112-116, 210-214, 342-346, 366-370, 394-398, 426-430).

♻️ Suggested helper
+fn envelope(stderr: &str) -> serde_json::Value {
+    let line = stderr
+        .lines()
+        .find(|l| l.starts_with('{'))
+        .unwrap_or_else(|| panic!("json envelope on stderr:\n{stderr}"));
+    serde_json::from_str(line).expect("valid json")
+}

Each site then becomes let doc = envelope(&stderr);.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_ergonomics.rs` around lines 60 - 64, Extract the repeated stderr
JSON lookup and deserialization into an `envelope` helper in the test module.
Update the seven call sites in `tests/cli_ergonomics.rs` to use `let doc =
envelope(&stderr);`, preserving the existing behavior of selecting the first
line beginning with `{` and failing on missing or invalid JSON.

276-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These two tests assert on the repo's own source size.

stdout.len() > 25_000 for src/ and "no hint" for src/hook/ both break as the tree grows or shrinks, with a failure that says nothing about the hint logic. implements_existing_type_with_zero_impls_exits_0 (Line 140-150) already went tempdir-local for this reason; a generated fixture (one large file above the threshold, one small dir below) would make both assertions self-contained.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_ergonomics.rs` around lines 276 - 296, Update
map_on_large_directory_hints_at_digest_preset and map_on_small_input_stays_quiet
to use a temporary, generated fixture rather than repo source paths and size
assumptions. Create one directory containing a file that reliably exceeds the
threshold and another small directory below it, then invoke run with those
fixture paths while preserving the existing exit-code, stdout, and stderr hint
assertions.
src/adapters/rust.rs (1)

419-424: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

One boolean trap survives the fix.

The Vis doc at Lines 1106-1108 introduces the enum to avoid unlabeled booleans, but is_method: bool sits right beside it — _function_to_decl(&item, src, true, Vis::Inherited) still reads as a puzzle at the call site. A second small enum (or deriving is_method from Vis/caller context) would finish the job.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/rust.rs` around lines 419 - 424, The _function_to_decl signature
still uses the unlabeled is_method boolean, leaving call sites ambiguous.
Replace it with a small descriptive enum or derive the method status from
existing Vis/caller context, then update all _function_to_decl callers to use
the clearer representation while preserving declaration behavior.
src/calls/mcp.rs (1)

46-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This unattributed policy is now duplicated in the CLI path.

The hide-ambiguous clear, the unattributed_total ordering, the declarer gate against MAX_DECLARERS_TO_LIST, and the UNATTRIBUTED_SAMPLE.min(limit) cap all appear verbatim in src/calls/cli.rs run_callers. Since the whole point of these rules is that both surfaces answer identically, a shared helper returning the built render::Unattributed (plus the hidden count for the caller to channel to stderr or response text) would keep them from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/mcp.rs` around lines 46 - 84, Extract the duplicated
unattributed-callers policy from the MCP flow and CLI run_callers into a shared
helper that constructs render::Unattributed and returns the hidden count
separately. Reuse this helper in both paths, preserving the include_ambiguous
clearing, declarer gate using MAX_DECLARERS_TO_LIST, and
UNATTRIBUTED_SAMPLE.min(limit) cap while leaving each surface’s stderr or
response-note handling intact.
tests/calls_e2e.rs (1)

1759-1763: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer the JSON assertion for this negative.

"Caller::getCtx (Exact)" hard-codes the two-space gap in render_callees_text's format string, so any rendering tweak turns this into a vacuous pass on the exact regression it guards. Later tests in this file (e.g. Lines 2070-2081) already assert on --json confidence + target instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/calls_e2e.rs` around lines 1759 - 1763, The negative assertion in the
relevant end-to-end test is coupled to render_callees_text’s spacing. Replace
the raw-output contains check with a --json assertion that verifies the local
homonym does not produce an entry combining target Caller::getCtx with Exact
confidence, following the existing JSON assertion pattern later in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 454: Update the README’s “Machine-readable rejections” statement to
account for the `impact --mode <bogus>` exception, or modify the
`Commands::Impact` handler to route invalid modes through `CliError` with
`ErrorKind::BadArgument` so `--json` emits the `ast-bro.error.v1` envelope.
Ensure the documented claim matches the implemented behavior.

In `@src/calls/mcp.rs`:
- Around line 196-198: The MCP JSON callees path in the json branch of the calls
handler omits the promised truncation metadata. Compute the frontier-truncated
state in the existing depth <= 1 and depth > 1 traversal branches using the same
logic as run_callees, then pass it through render_callees_json (and its callers)
so the payload includes total and frontier_truncated while preserving the
existing schema, target, depth, and matches fields.

In `@src/calls/traverse.rs`:
- Around line 104-142: Update callers_multi’s traversal around the seed queue
and while-loop to process entries by total-depth layers rather than relying on
FIFO ordering across mixed base depths. Ensure all nodes at depth d are expanded
before any depth d+1 entries, so first visits and predicate reports use minimum
total depth and entries beyond max_total_depth are excluded consistently.

In `@src/lib.rs`:
- Around line 1315-1331: Extend the target type check in the implements handling
block to recognize delegate declarations by including "delegate" alongside the
existing class, struct, interface, record, and enum kinds. Keep the
SymbolNotFound behavior unchanged for targets that still do not exist.

In `@src/mcp/tools.rs`:
- Around line 786-789: The output header in the find-implementations flow should
reflect the computed transitive setting instead of always claiming transitive
results. Update the format construction near find_implementations to include the
“incl. transitive” wording only when transitive is enabled, while preserving the
existing match count and target text.

In `@tests/calls_e2e.rs`:
- Around line 2576-2592: Extend the parity assertions in the test around the
existing total-count key loop to explicitly compare per-edge confidence for
matched caller edges between incremental and cold results. Include
unchanged-edge and newly resolved-edge cases so the comparison detects
differences between preserved original confidence and Inferred confidence, while
retaining the existing count-field checks.

In `@tests/cli_ergonomics.rs`:
- Around line 197-203: Update map_preset_digest_json_matches_digest_json to
retain and assert successful exit statuses for both run invocations, and verify
each stdout is non-empty before comparing them. Keep the existing byte-identical
output assertion after these checks.

In `@tests/digest_format.rs`:
- Around line 615-620: Rename the overlapping fixture function identifiers in
the digest-format test to non-overlapping names, including the
`self_only`/`in_self_only` pair and `crate_wide`/`in_crate_wide` pair. Update
the corresponding positive and negative assertion lists so each visibility case
is checked independently.

---

Outside diff comments:
In `@src/calls/cli.rs`:
- Around line 261-297: Make the depth <= 1 path use the same deduplicated
resolved-target counting semantics as the depth >= 2 path, while preserving all
direct call-site edges for display. Update the total calculation around
callees_one_hop and all_edges so duplicate call sites to one resolved callee
contribute once, keeping JSON total and shown consistent across depths.

---

Nitpick comments:
In `@src/adapters/rust.rs`:
- Around line 419-424: The _function_to_decl signature still uses the unlabeled
is_method boolean, leaving call sites ambiguous. Replace it with a small
descriptive enum or derive the method status from existing Vis/caller context,
then update all _function_to_decl callers to use the clearer representation
while preserving declaration behavior.

In `@src/calls/mcp.rs`:
- Around line 46-84: Extract the duplicated unattributed-callers policy from the
MCP flow and CLI run_callers into a shared helper that constructs
render::Unattributed and returns the hidden count separately. Reuse this helper
in both paths, preserving the include_ambiguous clearing, declarer gate using
MAX_DECLARERS_TO_LIST, and UNATTRIBUTED_SAMPLE.min(limit) cap while leaving each
surface’s stderr or response-note handling intact.

In `@src/calls/resolve.rs`:
- Around line 349-356: Update is_crate_root so directory-based detection only
recognizes Cargo binary crate roots directly under the repository’s src/bin
directory, rather than any path containing a bin component. Preserve the
existing lib.rs and main.rs checks, and keep paths such as tools/bin/helper.rs
and vendor/bin/x.rs from being classified as crate roots.
- Around line 358-377: Add Python’s "cls" receiver to receiver_is_self_like so
cls.helper() in classmethods follows the same enclosing-scope promotion path as
self-like receivers. Update the function’s explanatory comment to document
Python classmethod usage while preserving all existing receiver behavior.

In `@src/impact.rs`:
- Around line 476-483: Extract the repeated truncation suffix formatting into a
shared helper such as count_suffix(total: usize, shown: usize), then replace the
hand-built “showing … raise --limit …” wording in this formatting block and the
corresponding sections around the other affected-count messages, including the
dependency renderer. Preserve the existing wording and values while centralizing
its construction.

In `@src/lib.rs`:
- Around line 538-546: Update the MapArgs paths argument documentation or
value_name configuration to explicitly state that paths may be omitted and empty
input produces the no_input envelope, while preserving the existing runtime
validation and optional argument behavior for both map and digest.

In `@src/mcp/tools.rs`:
- Around line 640-647: The partial-miss note prefixing is duplicated across MCP
tools and should use one shared formatter. In src/mcp/tools.rs lines 640-647,
replace run_digest’s path_note match with a helper such as with_note; in lines
782-785, make the same change in run_implements; also reuse the helper in
run_map’s note closure at lines 539-542. Define the helper next to partial_note
and preserve the existing note-before-output formatting.

In `@tests/calls_e2e.rs`:
- Around line 1759-1763: The negative assertion in the relevant end-to-end test
is coupled to render_callees_text’s spacing. Replace the raw-output contains
check with a --json assertion that verifies the local homonym does not produce
an entry combining target Caller::getCtx with Exact confidence, following the
existing JSON assertion pattern later in the file.

In `@tests/cli_ergonomics.rs`:
- Around line 60-64: Extract the repeated stderr JSON lookup and deserialization
into an `envelope` helper in the test module. Update the seven call sites in
`tests/cli_ergonomics.rs` to use `let doc = envelope(&stderr);`, preserving the
existing behavior of selecting the first line beginning with `{` and failing on
missing or invalid JSON.
- Around line 276-296: Update map_on_large_directory_hints_at_digest_preset and
map_on_small_input_stays_quiet to use a temporary, generated fixture rather than
repo source paths and size assumptions. Create one directory containing a file
that reliably exceeds the threshold and another small directory below it, then
invoke run with those fixture paths while preserving the existing exit-code,
stdout, and stderr hint assertions.

In `@tests/digest_format.rs`:
- Around line 437-445: Update the test around the `without` result and its
`--no-attrs`/`--no-lines` assertions to remove the temporary fixture path before
checking for absent content, following the path-normalization approach used by
`rust_missing_pub_means_private_except_inherited_contexts`. Keep the existing
assertions and messages unchanged after normalization.
- Around line 448-486: Extend the json_honors_projection_flags test to assert
the projected metadata block: the default payload must omit projected, while
calls using --no-docs, --no-lines, or --no-attrs must include it.
- Around line 533-535: The visibility assertion in the digest test incorrectly
includes gain while fields are disabled by default. Remove gain from the
hidden-name list, or update the digest invocation to enable fields so the
assertion actually verifies its non-public visibility; keep the existing map
--no-private coverage unchanged.
- Around line 412-418: Strengthen the assertions in the map detail test around
run by verifying the output does not contain the cap-indicator text (such as the
“more member(s)” note) after confirming a(), b(), and c() are present. Use the
formatter’s actual cap-note wording rather than a broad symbol check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fdfd21be-6f6c-45d3-82cb-266e683c9cf4

📥 Commits

Reviewing files that changed from the base of the PR and between d9bccef and ee39be9.

📒 Files selected for processing (33)
  • README.md
  • skills/ast-bro/SKILL.md
  • src/adapters/php.rs
  • src/adapters/rust.rs
  • src/calls/cli.rs
  • src/calls/mcp.rs
  • src/calls/render.rs
  • src/calls/resolve.rs
  • src/calls/trace.rs
  • src/calls/traverse.rs
  • src/cli_error.rs
  • src/context.rs
  • src/core.rs
  • src/deps/cli.rs
  • src/deps/render.rs
  • src/graph_cache/delta.rs
  • src/graph_cache/mod.rs
  • src/hook/decide.rs
  • src/impact.rs
  • src/lib.rs
  • src/mcp/tools.rs
  • src/search/chunker.rs
  • src/search/cli.rs
  • tests/calls_e2e.rs
  • tests/cli_ergonomics.rs
  • tests/digest_format.rs
  • tests/mcp_e2e.rs
  • tests/show_markdown.rs
  • tests/surface_e2e.rs
  • wiki/architecture.md
  • wiki/calls.md
  • wiki/file-filtering.md
  • wiki/search.md

Comment thread README.md
Comment thread src/calls/mcp.rs
Comment thread src/calls/traverse.rs
Comment thread src/lib.rs
Comment on lines +1315 to +1331
let target_exists = results.iter().any(|r| {
crate::core::find_symbols(r, target).iter().any(|m| {
matches!(
m.kind.as_str(),
"class" | "struct" | "interface" | "record" | "enum"
)
})
});
if !target_exists {
crate::cli_error::CliError::new(
"implements",
crate::cli_error::ErrorKind::SymbolNotFound,
format!("no type named '{}' in the given path(s)", target),
)
.hint("A 0-match answer is only reported for types that exist. Check the spelling or widen the search path.")
.exit(*json);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Which DeclarationKind values do adapters assign to type declarations?
rg -nP --type=rust -C2 'kind:\s*DeclarationKind::(Class|Struct|Interface|Record|Enum|Delegate|Namespace)' src/adapters | head -60

Repository: aeroxy/ast-bro

Length of output: 2766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DeclarationKind definitions =="
rg -n "pub enum DeclarationKind|enum DeclarationKind|enum class DeclarationKind|DeclarationKind::[A-Z_" src -g '*.rs' -g '*.ts' -g '*.js' | head -80

echo
echo "== adapter declaration construction sites =="
for f in $(git ls-files src/adapters | sort); do
  echo "--- $f"
  rg -n "Declaration\\s*{|kind:\\s*DeclarationKind::" "$f" -C 1 | head -120
done

echo
echo "== implements type check implementation =="
sed -n '1280,1340p' src/lib.rs | cat -n

Repository: aeroxy/ast-bro

Length of output: 444


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== all DeclarationKind enum members =="
rg -n "enum DeclarationKind" src || true
rg -n "declare_enum!\\(DeclarationKind|EnumDecl!" src || true

echo
echo "== declaration-visitor helper symbols =="
rg -n "parse_?declaration|visit_?declaration|DeclarationKind|Declaration\\s*=" src/adapters src/core -g '*.rs' | head -200

echo
echo "== adapter function signatures and relevant node types =="
rg -n "fn \\w+\\(|const .*DEC|kind: DeclarationKind::" src/adapters -C 2 | head -220

Repository: aeroxy/ast-bro

Length of output: 13289


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DeclarationKind definitions =="
rg -n "pub enum DeclarationKind|enum DeclarationKind|EnumDecl|declare_enum" src -g '*.rs' | head -120

echo
echo "== declaration creation kind assignments =="
python3 - <<'PY'
import re, subprocess, pathlib
# simple scanner because exact syntax may vary
for path in pathlib.Path('src').rglob('*'):
    if path.suffix not in {'.rs'}:
        continue
    try:
        text = path.read_text()
    except UnicodeDecodeError:
        continue
    for m in re.finditer(r'kind:\s*DeclarationKind::([A-Za-z0-9_]+)', text):
        start = max(0, text.rfind('\n', 0, m.start())-200)
        snippet = text[m.start():text.find('\n}', m.end())+2]
        print(f"{path}:{m.start()+1}: {m.group(1)} :: {snippet.splitlines()[0].strip()}")
PY

echo
echo "== implementation allowlist =="
sed -n '1300,1335p' src/lib.rs | cat -n

Repository: aeroxy/ast-bro

Length of output: 7474


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DeclarationKind definitions/imports =="
rg -n "pub enum DeclarationKind|enum DeclarationKind|EnumDecl|declare_enum|DeclarationKind" src/core src/adapters src/lib.rs -g '*.rs' | head -200

echo
echo "== declaration construction snippets by file =="
for f in $(git ls-files 'src/adapters/*.rs' | sort); do
  echo "==$f"
  rg -n "kind:\s*DeclarationKind::" "$f" -C 2
done

echo
echo "== type-filter logic =="
sed -n '1312,1332p' src/lib.rs | cat -n

Repository: aeroxy/ast-bro

Length of output: 13208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== core.rs relevant definitions =="
sed -n '1,80p' src/core.rs | cat -n

echo
echo "== find_symbols implementation =="
rg -n "fn find_symbols|pub fn find_symbols|find_symbols" src -g '*.rs' -C 8 | head -200

echo
echo "== find_implementations implementation =="
rg -n "fn find_implementations|pub fn find_implementations|find_implementations" src -g '*.rs' -C 12 | head -240

echo
echo "== TypeScript delegate/namespace target declaration construction snippets =="
rg -n "delegate\\b|namespace\\b|interface\\b|type\\b" src/adapters/typescript.rs -C 2 | sed -n '1,220p'

Repository: aeroxy/ast-bro

Length of output: 17989


Include declared type aliases in the implements target check.

DeclarationKind::Delegate emits kind: "delegate" in several adapters, and existing adapter-level code treats delegates as targetable type declarations. Since the current check only recognizes class|struct|interface|record|enum, implements X can exit 2 instead of returning 0 matches when X is a declared alias/delegate that exists but has no implementations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 1315 - 1331, Extend the target type check in the
implements handling block to recognize delegate declarations by including
"delegate" alongside the existing class, struct, interface, record, and enum
kinds. Keep the SymbolNotFound behavior unchanged for targets that still do not
exist.

Comment thread src/mcp/tools.rs
Comment on lines +786 to +789
out.push_str(&format!(
"# {} match(es) for '{}' (incl. transitive):\n",
matches.len(), a.target
);
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Header claims transitive results even when direct: true.

transitive is computed at Line 773 and honored by find_implementations, but the text header hard-codes (incl. transitive). An agent asking for direct subtypes gets output labelled as including transitive ones.

🔧 Proposed fix
         out.push_str(&format!(
-            "# {} match(es) for '{}' (incl. transitive):\n",
-            matches.len(), a.target
+            "# {} match(es) for '{}'{}:\n",
+            matches.len(),
+            a.target,
+            if transitive { " (incl. transitive)" } else { " (direct only)" }
         ));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
out.push_str(&format!(
"# {} match(es) for '{}' (incl. transitive):\n",
matches.len(), a.target
);
));
out.push_str(&format!(
"# {} match(es) for '{}'{}:\n",
matches.len(),
a.target,
if transitive { " (incl. transitive)" } else { " (direct only)" }
));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/tools.rs` around lines 786 - 789, The output header in the
find-implementations flow should reflect the computed transitive setting instead
of always claiming transitive results. Update the format construction near
find_implementations to include the “incl. transitive” wording only when
transitive is enabled, while preserving the existing match count and target
text.

Comment thread tests/calls_e2e.rs
Comment on lines +2576 to +2592
for key in [
"total",
"unattributed_total",
"unattributed_declarers",
"unattributed_suppressed",
] {
assert_eq!(
incremental[key], cold_again[key],
"{key} diverged between a partial update and a cold build of the same content:\n\
incremental={incremental}\ncold={cold_again}"
);
assert_eq!(
cold[key], cold_again[key],
"{key} changed across an edit that touched no relevant file:\n\
before={cold}\nafter={cold_again}"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# What does the delta detector compare — mtime, size, or hash?
rg -nP --type=rust -C4 '\b(mtime|modified|len\(\)|hash)\b' src/search/cache.rs src/graph_cache/ | head -80

Repository: aeroxy/ast-bro

Length of output: 4940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== Locate relevant files ==\n'
git ls-files | rg 'tests/calls_e2e\.rs|src/graph_cache/delta\.rs|src/search/cache\.rs' || true

printf '\n== calls_e2e parity test section ==\n'
sed -n '2510,2595p' tests/calls_e2e.rs | cat -n

printf '\n== delta.rs confidence mentions and relevant functions ==\n'
rg -n -C 5 'Inferred|Exact|confidence|Partial|total|unattributed' src/graph_cache/delta.rs | head -240

printf '\n== search cache rebuild path flags ==\n'
rg -n -C 4 'PartialUpdate|Rebuild|requires_rebuild|rebuild|compute_delta|mtime_only|modified' src tests -g '*.rs' | head -320

Repository: aeroxy/ast-bro

Length of output: 22928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== locate callers graph json definitions ==\n'
rg -n -C 4 'struct .*Graph|confidenc|confidence|unattributed|total.*matches|Output|QueryResult|match.*json|serde_json::to|rebuild' src/calls src/cmd src -g '*.rs' | head -400

printf '\n== callers output builders ==\n'
rg -n -C 8 'fn calls|command calls|callers|build.*calls|calls\(' src -g '*.rs'

Repository: aeroxy/ast-bro

Length of output: 22018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== callers json payload field names ==\n'
sed -n '620,710p' src/calls/render.rs | cat -n

printf '\n== exact callers output builder invocations ==\n'
rg -n -C 4 'render_callers_json|render_callers_json_extended|Unattributed|json_unattributed|json_unattributed_fields|unattributed' src/calls src/cli src -g '*.rs' | head -320

Repository: aeroxy/ast-bro

Length of output: 21573


Check the relevant delta-confidence comparison explicitly.

The current parity keys omit per-edge confidence, and delta.rs preserves unchanged-file edges with their original confidence while newly resolved ones may get Inferred; this test can pass even when incremental/cold disagree. Compare matched caller edges’ confidence alongside the count fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/calls_e2e.rs` around lines 2576 - 2592, Extend the parity assertions in
the test around the existing total-count key loop to explicitly compare per-edge
confidence for matched caller edges between incremental and cold results.
Include unchanged-edge and newly resolved-edge cases so the comparison detects
differences between preserved original confidence and Inferred confidence, while
retaining the existing count-field checks.

Comment thread tests/cli_ergonomics.rs
Comment on lines +197 to +203
#[test]
fn map_preset_digest_json_matches_digest_json() {
// #37: `digest` is exactly `map --preset digest`.
let (_, a, _) = run(&["map", "src/core.rs", "--preset", "digest", "--json", "--compact"]);
let (_, b, _) = run(&["digest", "src/core.rs", "--json", "--compact"]);
assert_eq!(a, b, "digest alias must be byte-identical to the preset");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This equality passes vacuously if both commands start failing.

Both exit codes are discarded, so two empty stdouts compare equal. Assert success and non-empty output before comparing.

💚 Proposed fix
-    let (_, a, _) = run(&["map", "src/core.rs", "--preset", "digest", "--json", "--compact"]);
-    let (_, b, _) = run(&["digest", "src/core.rs", "--json", "--compact"]);
+    let (ca, a, ea) = run(&["map", "src/core.rs", "--preset", "digest", "--json", "--compact"]);
+    let (cb, b, _) = run(&["digest", "src/core.rs", "--json", "--compact"]);
+    assert_eq!(ca, Some(0), "stderr:\n{ea}");
+    assert_eq!(cb, Some(0));
+    assert!(!a.is_empty(), "empty payloads compare equal for the wrong reason");
     assert_eq!(a, b, "digest alias must be byte-identical to the preset");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn map_preset_digest_json_matches_digest_json() {
// #37: `digest` is exactly `map --preset digest`.
let (_, a, _) = run(&["map", "src/core.rs", "--preset", "digest", "--json", "--compact"]);
let (_, b, _) = run(&["digest", "src/core.rs", "--json", "--compact"]);
assert_eq!(a, b, "digest alias must be byte-identical to the preset");
}
#[test]
fn map_preset_digest_json_matches_digest_json() {
// `#37`: `digest` is exactly `map --preset digest`.
let (ca, a, ea) = run(&["map", "src/core.rs", "--preset", "digest", "--json", "--compact"]);
let (cb, b, _) = run(&["digest", "src/core.rs", "--json", "--compact"]);
assert_eq!(ca, Some(0), "stderr:\n{ea}");
assert_eq!(cb, Some(0));
assert!(!a.is_empty(), "empty payloads compare equal for the wrong reason");
assert_eq!(a, b, "digest alias must be byte-identical to the preset");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_ergonomics.rs` around lines 197 - 203, Update
map_preset_digest_json_matches_digest_json to retain and assert successful exit
statuses for both run invocations, and verify each stdout is non-empty before
comparing them. Keep the existing byte-identical output assertion after these
checks.

Comment thread tests/digest_format.rs
Comment on lines +615 to +620
for hidden in ["self_only", "in_self_only"] {
assert!(!out.contains(hidden), "`{hidden}` is private-equivalent:\n{out}");
}
for shown in ["crate_wide", "parent_wide", "in_crate_wide", "open"] {
assert!(out.contains(shown), "`{shown}` must stay visible:\n{out}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Overlapping fixture names make these assertions unable to fail independently.

"crate_wide" is a substring of "in_crate_wide", so the positive assertion for pub(crate) fn crate_wide passes as long as in_crate_wide renders — a regression that hides only pub(crate) would go undetected. Same overlap between self_only and in_self_only on the negative side. Rename the fixture functions to non-overlapping identifiers (e.g. crate_wide / in_crate_form).

🔧 Proposed fixture rename
-         pub(self) fn self_only() {}\n\
-         pub(in self) fn in_self_only() {}\n\
+         pub(self) fn self_only() {}\n\
+         pub(in self) fn scoped_self_form() {}\n\
          pub(crate) fn crate_wide() {}\n\
          pub(super) fn parent_wide() {}\n\
-         pub(in crate) fn in_crate_wide() {}\n\
+         pub(in crate) fn scoped_crate_form() {}\n\

and update the two assertion lists accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/digest_format.rs` around lines 615 - 620, Rename the overlapping
fixture function identifiers in the digest-format test to non-overlapping names,
including the `self_only`/`in_self_only` pair and `crate_wide`/`in_crate_wide`
pair. Update the corresponding positive and negative assertion lists so each
visibility case is checked independently.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/calls/render.rs (1)

747-756: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make MCP callees emit the same callees.v1 truncation fields as the CLI.

src/calls/mcp.rs::run_callees_text uses render_callees_json(...), while the CLI uses render_callees_json_extended(...); only the extended payload includes total, truncated, and frontier_truncated for ast-bro.callees.v1. A consumer guarding on truncated gets a different surface for the same schema version. Thread equivalent truncation state through both callee JSON paths like the caller JSON paths do.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/render.rs` around lines 747 - 756, Update the MCP callee flow in
run_callees_text and its render_callees_json path to carry the same truncation
state used by render_callees_json_extended and the caller JSON paths. Ensure
ast-bro.callees.v1 always emits total, truncated, and frontier_truncated
consistently with the CLI, preserving the existing calculations for shown
results and frontier truncation.
♻️ Duplicate comments (3)
src/lib.rs (1)

1315-1331: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Type-declaration allowlist still omits delegate (and other adapter type kinds).

DeclarationKind::Delegate renders as "delegate" (src/core.rs:54-76), so implements SomeDelegate on an existing declared delegate exits 2 (symbol_not_found) instead of returning a legitimate 0-match answer.

🔧 Proposed fix
                     matches!(
                         m.kind.as_str(),
-                        "class" | "struct" | "interface" | "record" | "enum"
+                        "class" | "struct" | "interface" | "record" | "enum" | "delegate"
                     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 1315 - 1331, Update the target type-kind allowlist
in the `target_exists` check to include `"delegate"` and any other declaration
kinds rendered by `DeclarationKind` that represent valid type declarations, so
existing delegates proceed to the legitimate 0-match result instead of
`SymbolNotFound`.
src/calls/traverse.rs (1)

104-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The "sorted seeds + FIFO ⇒ depth-monotonic" invariant only holds while seed base depths differ by at most 1.

With seeds (A,1) and (B,5), popping A pushes its callers at depth 2 behind B(5), so a node reachable at total depth 2 from A can be first visited (and reported, or dropped above max_total_depth) from B's deeper path. impact currently only seeds depths 1 and 2, so this is latent rather than live — but the comment states the invariant unconditionally, and any future seed at depth 3+ silently breaks min-depth reporting. Either expand one total-depth layer at a time (BTreeMap<usize, Vec<Qn>>), or state the ≤1-spread precondition and debug-assert it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/traverse.rs` around lines 104 - 142, Fix the traversal ordering in
the function containing the sorted seeds and FIFO queue: ensure nodes are
expanded in globally nondecreasing total depth even when seed depths differ by
more than one, such as by processing depth buckets with a BTreeMap. Preserve
minimum-depth first visitation, predicate-based reporting, and max_total_depth
handling; do not rely on the current unconditional sorted-seeds/FIFO invariant.
README.md (1)

454-454: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

impact --mode <bogus> is still an exception to this claim.

The Commands::Impact handler rejects an unknown --mode with a bare # note: on stderr plus std::process::exit(2) — no ast-bro.error.v1 envelope — so a consumer keying on schema == "ast-bro.error.v1" sees nothing. Route it through CliError/ErrorKind::BadArgument or narrow the wording here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 454, Update the Commands::Impact handling for an invalid
--mode value so it reports through CliError using ErrorKind::BadArgument and
emits the documented ast-bro.error.v1 envelope under --json, instead of printing
a bare note and exiting directly; alternatively narrow the README claim to
exclude this case.
🧹 Nitpick comments (4)
src/lib.rs (1)

790-883: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Subcommand attribution in the envelope is a positional guess.

raw.iter().find(|a| !a.starts_with('-')) picks the first non-flag token, which for a bad value (ast-bro callers foo --depth abc) yields the subcommand correctly, but for a global-flag-first invocation of an unknown subcommand it reports whatever token appears first — including a bare value. Clap already knows the matched subcommand path for most error kinds via ContextKind::InvalidSubcommand / Error::cmd; using the raw args only as a fallback would keep command in ast-bro.error.v1 trustworthy for consumers keying on it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 790 - 883, Update subcommand attribution in
exit_with_parse_error to use clap’s parsed command context, preferring
ContextKind::InvalidSubcommand or the command associated with Error::cmd when
available. Retain raw-argument scanning only as a fallback, and avoid treating
bare values as the command for global-flag-first unknown-subcommand errors;
ensure the ast-bro.error.v1 envelope’s command remains the actual subcommand.
src/calls/resolve.rs (2)

115-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Consider extracting this receiver-resolution closure into a named function.

The and_then closure now spans ~125 lines, declares a local enum SelfRel, and holds three distinct resolution strategies (crate-anchored, self/super walk, suffix match with caller-scope tie-break). Lifting it to fn resolve_scoped_receiver(recv, bare_name, caller, defined) -> Option<Qn> would make each strategy independently testable and keep the for raw in fp.raw_edges loop readable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/resolve.rs` around lines 115 - 241, Extract the receiver-resolution
`and_then` closure into a named `resolve_scoped_receiver` function accepting the
receiver, bare name, caller, and defined QNs. Move the `SelfRel` enum and
crate-anchored, self/super-walk, and suffix tie-break strategies into that
function without changing their behavior, then call it from the raw-edge loop so
the loop remains concise and each strategy is independently testable.

353-356: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

is_crate_root's /bin/ check is broader than "crate root".

file.contains("/bin/") matches any path with a bin component (e.g. tools/bin/helper.rs, crates/x/src/bin/y.rs is intended but scripts/bin/gen.rs is not). Restricting it to src/bin/ would match the Cargo layout the doc comment describes. The downstream anchored-equality check limits the blast radius, so this is a tightening rather than a live bug.

♻️ Proposed tightening
-    matches!(name, "lib.rs" | "main.rs") || file.contains("/bin/")
+    matches!(name, "lib.rs" | "main.rs") || file.contains("src/bin/")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/calls/resolve.rs` around lines 353 - 356, Update is_crate_root so the
binary crate-root condition matches only paths containing the Cargo-style
/src/bin/ component, while preserving the existing lib.rs and main.rs checks.
src/core.rs (1)

1440-1447: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nit: from_ref(..).pop() recursion allocates a Vec per child.

Extracting the single-declaration case into a _filter_one(d, opts, dropped) -> Option<Declaration> helper (with _filter_decls mapping over it) removes the per-child allocation and reads more directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core.rs` around lines 1440 - 1447, Refactor the recursion around
_filter_decls to add a _filter_one helper returning Option<Declaration> for a
single declaration, then have _filter_decls map over _filter_one. Replace the
per-child std::slice::from_ref(c) and pop() call in the children construction
with direct _filter_one usage, preserving the existing filtering and
depth-capping behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/impact.rs`:
- Around line 606-612: Apply the existing --limit display-capping behavior to
the forward “→ calls” section constructed here and the “→ imports” section near
the referenced code: preserve the uncapped count in total, limit the entries
shown, and set truncated based on whether entries were omitted, matching the
implementation used by the capped dependent/transitive/reverse-import sections.

In `@tests/mcp_e2e.rs`:
- Line 73: Replace the Unix-specific `/nope/...` missing-path fixtures in the
MCP requests and related assertions with a platform-independent nonexistent name
such as `gone.rs`; update all corresponding `missing` and `missing_paths`
expectations in the affected test cases to match.

---

Outside diff comments:
In `@src/calls/render.rs`:
- Around line 747-756: Update the MCP callee flow in run_callees_text and its
render_callees_json path to carry the same truncation state used by
render_callees_json_extended and the caller JSON paths. Ensure
ast-bro.callees.v1 always emits total, truncated, and frontier_truncated
consistently with the CLI, preserving the existing calculations for shown
results and frontier truncation.

---

Duplicate comments:
In `@README.md`:
- Line 454: Update the Commands::Impact handling for an invalid --mode value so
it reports through CliError using ErrorKind::BadArgument and emits the
documented ast-bro.error.v1 envelope under --json, instead of printing a bare
note and exiting directly; alternatively narrow the README claim to exclude this
case.

In `@src/calls/traverse.rs`:
- Around line 104-142: Fix the traversal ordering in the function containing the
sorted seeds and FIFO queue: ensure nodes are expanded in globally nondecreasing
total depth even when seed depths differ by more than one, such as by processing
depth buckets with a BTreeMap. Preserve minimum-depth first visitation,
predicate-based reporting, and max_total_depth handling; do not rely on the
current unconditional sorted-seeds/FIFO invariant.

In `@src/lib.rs`:
- Around line 1315-1331: Update the target type-kind allowlist in the
`target_exists` check to include `"delegate"` and any other declaration kinds
rendered by `DeclarationKind` that represent valid type declarations, so
existing delegates proceed to the legitimate 0-match result instead of
`SymbolNotFound`.

---

Nitpick comments:
In `@src/calls/resolve.rs`:
- Around line 115-241: Extract the receiver-resolution `and_then` closure into a
named `resolve_scoped_receiver` function accepting the receiver, bare name,
caller, and defined QNs. Move the `SelfRel` enum and crate-anchored,
self/super-walk, and suffix tie-break strategies into that function without
changing their behavior, then call it from the raw-edge loop so the loop remains
concise and each strategy is independently testable.
- Around line 353-356: Update is_crate_root so the binary crate-root condition
matches only paths containing the Cargo-style /src/bin/ component, while
preserving the existing lib.rs and main.rs checks.

In `@src/core.rs`:
- Around line 1440-1447: Refactor the recursion around _filter_decls to add a
_filter_one helper returning Option<Declaration> for a single declaration, then
have _filter_decls map over _filter_one. Replace the per-child
std::slice::from_ref(c) and pop() call in the children construction with direct
_filter_one usage, preserving the existing filtering and depth-capping behavior.

In `@src/lib.rs`:
- Around line 790-883: Update subcommand attribution in exit_with_parse_error to
use clap’s parsed command context, preferring ContextKind::InvalidSubcommand or
the command associated with Error::cmd when available. Retain raw-argument
scanning only as a fallback, and avoid treating bare values as the command for
global-flag-first unknown-subcommand errors; ensure the ast-bro.error.v1
envelope’s command remains the actual subcommand.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fdfd21be-6f6c-45d3-82cb-266e683c9cf4

📥 Commits

Reviewing files that changed from the base of the PR and between d9bccef and ee39be9.

📒 Files selected for processing (33)
  • README.md
  • skills/ast-bro/SKILL.md
  • src/adapters/php.rs
  • src/adapters/rust.rs
  • src/calls/cli.rs
  • src/calls/mcp.rs
  • src/calls/render.rs
  • src/calls/resolve.rs
  • src/calls/trace.rs
  • src/calls/traverse.rs
  • src/cli_error.rs
  • src/context.rs
  • src/core.rs
  • src/deps/cli.rs
  • src/deps/render.rs
  • src/graph_cache/delta.rs
  • src/graph_cache/mod.rs
  • src/hook/decide.rs
  • src/impact.rs
  • src/lib.rs
  • src/mcp/tools.rs
  • src/search/chunker.rs
  • src/search/cli.rs
  • tests/calls_e2e.rs
  • tests/cli_ergonomics.rs
  • tests/digest_format.rs
  • tests/mcp_e2e.rs
  • tests/show_markdown.rs
  • tests/surface_e2e.rs
  • wiki/architecture.md
  • wiki/calls.md
  • wiki/file-filtering.md
  • wiki/search.md

Comment thread src/impact.rs
Comment on lines 606 to 612
ImpactSection {
title: format!("→ calls ({})", entries.len()),
total: entries.len(),
truncated: false,
entries,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Forward sections hard-code truncated: false and never apply --limit.

--limit's help text says "Cap how many results are displayed per section", and the dependents/transitive/reverse-import sections now honour it, but → calls here and → imports (Lines 736-739) emit every entry with total: entries.len(), truncated: false. On a fan-out-heavy symbol that section alone can dwarf the capped ones. Either cap these two the same way, or narrow the flag's help text to the sections it actually bounds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/impact.rs` around lines 606 - 612, Apply the existing --limit
display-capping behavior to the forward “→ calls” section constructed here and
the “→ imports” section near the referenced code: preserve the uncapped count in
total, limit the entries shown, and set truncated based on whether entries were
omitted, matching the implementation used by the capped
dependent/transitive/reverse-import sections.

Comment thread tests/mcp_e2e.rs
let resp = call_tool(
tmp.path(),
"map",
serde_json::json!({"paths": ["/nope/missing-xyz.rs"]}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'Files:'
git ls-files | rg '(^|/)tests/mcp_e2e\.rs$|mcp' || true

echo
echo 'Target excerpt:'
if [ -f tests/mcp_e2e.rs ]; then
  nl -ba tests/mcp_e2e.rs | sed -n '55,170p'
else
  echo "tests/mcp_e2e.rs not found"
fi

echo
echo 'Search for missing_paths and /nope:'
rg -n "missing_paths|/nope|gone\.rs|slash|paths" tests/mcp_e2e.rs tests src .github 2>/dev/null || true

Repository: aeroxy/ast-bro

Length of output: 321


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'Target excerpt with cat:'
cat -n tests/mcp_e2e.rs | sed -n '60,175p'

echo
echo 'Search for missing_paths and /nope:'
rg -n "missing_paths|/nope|gone\.rs|slash|paths" tests/mcp_e2e.rs src || true

echo
echo 'Read relevant MCP implementation snippets:'
for f in src/search/mcp.rs src/calls/mcp.rs src/mcp/tools.rs src/mcp/protocol.rs src/mcp/mod.rs; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat -n "$f" | sed -n '1,260p'
  fi
done

Repository: aeroxy/ast-bro

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'Relevant MCP implementation snippets:'
sed -n '460,555p' src/mcp/tools.rs
sed -n '585,635p' src/mcp/tools.rs
sed -n '968,1008p' src/lib.rs
sed -n '70,90p' src/project_root.rs
sed -n '160,190p' src/project_root.rs
sed -n '150,180p' src/project_root.rs

echo
echo 'Read-only behavioral probe for split_existing / missing_PATHs logic:'
python3 - <<'PY'
from pathlib import Path
import tempfile

for root in [Path(tempfile.gettempdir()), Path("C:\\temp")]:
    root.mkdir(exist_ok=True)
    for name in ["lib.rs", "gone.rs", "nope/gone.rs", "/nope/gone.rs"]:
        p = root / name
        missing = not p.exists()
        print(f"{root!s:20} {name!s:18} exists={not missing}")
PY

Repository: aeroxy/ast-bro

Length of output: 11085


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'path_glob expand_existing implementation:'
rg -n "pub fn expand_existing|fn expand_existing|expand_existing" src tests -A 120 -B 20

echo
echo 'Relevant PathBuf display cases in tests:'
python3 - <<'PY'
import subprocess, json

code = r'''
use std::path::PathBuf;

fn main() {
    for s in ["gone.rs", "nope/gone.rs", "/nope/gone.rs"] {
        let p = PathBuf::from(&s);
        println!("{:?}: {}", s, p.display());
    }
}
'''
with open('/tmp/path_display_probe.rs', 'w') as f:
    f.write(code)
subprocess.run(['rustc', '/tmp/path_display_probe.rs', '-o', '/tmp/path_display_probe'])
subprocess.check_call(['/tmp/path_display_probe'])
PY

Repository: aeroxy/ast-bro

Length of output: 16044


Avoid Unix-specific missing-path fixtures.

The /nope/... inputs are always treated as missing, and the text assertions/assertions rely on the input being displayed with forward slashes. Use a platform-independent nonexistent path name such as gone.rs in the MCP requests and corresponding missing/missing_paths assertions.

Also applies to: 89, 121-127, 138, 155-163.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/mcp_e2e.rs` at line 73, Replace the Unix-specific `/nope/...`
missing-path fixtures in the MCP requests and related assertions with a
platform-independent nonexistent name such as `gone.rs`; update all
corresponding `missing` and `missing_paths` expectations in the affected test
cases to match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants