Recover parenthesized object declarations only when their apparent parameters resolve to earlier visible value bindings. Respect recovered scope, alias hiding and prototype shadowing instead of manufacturing receiver types.
Preserve heuristic confidence and source provenance. Add positive and negative regressions plus a three-repeat labeled initialization fixture to the quality gate.
Validation: cargo fmt --check and ./scripts/quality-gate.sh passed.
Problem: a bare call in a Dart file resolved against every imported
library. An `import 'a.dart' as p;` still bound `foo()` to a.dart's foo,
`hide` and `show` combinators were ignored, and a name re-exported through
a barrel library (`export 'impl.dart';`) never resolved.
Fix: record each Dart import/export directive with its `as` prefix and
show/hide lists. A bare name resolves only in libraries imported without a
prefix whose combinators let it through, plus that library's own exports
that also let it through (one level).
Test: tests/dart_imports.rs covers a prefixed, a hiding, a showing and a
barrelled import; it failed on the prefixed case before this change.
When several files declare the same type leaf name, rust_method keeps
only the impls of the type a call site means, asking each impl's file
which `Foo` it means. An impl in a third file written as
`impl crate:🅰️:Foo` resolved nothing (that file has no `Foo`
declaration or `use` binding), so its methods were dropped and
`x.run()` on an `a::Foo` read "member-not-found". It resolved before #5.
The extractor now records the impl's type path when it is written as
one (`impl_type_path`, generic arguments removed), the method index
keeps it, and the impl filter resolves that path to its declaring file
before falling back to the file's own view of the leaf name. Glob
imports (`use super::*`) are still not expanded; such an impl stays
unresolved rather than guessed.
Test: with `Foo` declared in a.rs and c.rs and `impl crate:🅰️:Foo` in
ext.rs, `x.run()` on `crate:🅰️:Foo` resolves to ext.rs's `run`; it
failed before this change.
imported_paths found the file node with a linear scan of all nodes and
then filtered all edges, for every Python/Dart file it built a set for:
O(files x (nodes + edges)) during resolution, about 10^10 comparisons
for 10k files and a million edges.
Imports edges are now indexed by source file on first use and the file
node is looked up by its stable id, as resolve_import already does. The
index is complete when first built: resolve_relationships resolves
every import before any call. Output is unchanged (all 124 tests pass).
Since #5 Python calls resolve only through import edges, but the
lexical importer could not produce those edges for the common forms:
- `from .util import helper` never matched (the regex needed a leading
letter), so the call went unresolved;
- `from pkg import f` looked up the stem "pkg" instead of
pkg/__init__.py;
- `from x.common import f` failed when y/common.py also existed, and
`from other.common import f` (a module the repository does not have)
resolved to y/common.py by basename.
Imports now resolve Python-specifically: leading dots are relative to
the importing package, and a dotted name maps to `a/b.py` or
`a/b/__init__.py` from the repository root or `src/`. Only a
single-component name (`import foo`) may still fall back to a unique
module basename; a dotted name never does. `from . import x` stays
unresolved (the module is in the import list, not the name).
Tests (both failed before): relative, package and duplicate-stem imports
resolve their calls to pkg/util.py, pkg/__init__.py and x/common.py;
an unknown dotted module does not bind to y/common.py.
Swift ran through the C-style lexical patterns, which never recognize
`func`. On a small file (`func realTarget()`, `func ghost()`, a class
method `caller` that calls `realTarget()` below a `/* ghost() */`
comment) the preview reported:
a.swift -> ghost (a.swift:2 declaration, a.swift:5 in a comment)
a.swift -> caller (its declaration line)
a.swift -> realTarget (its declaration line)
and missed the real call. Now:
caller -> realTarget (a.swift:7)
- Swift gets its own patterns: `func` declarations (with attributes and
modifiers), class/struct/enum/protocol/actor/extension declarations
(`class func` / `class var` are members, not classes), and imports.
- Block comments are masked across lines, nested as Swift allows, and a
`/*` inside a string literal does not open one. The masker is salvaged
from the unmerged language-parity branch (543bafc).
- Calls are attributed to their enclosing function through the existing
brace-scoped scopes, now that functions are recognized.
Test: tests/swift_preview.rs (functions and a class recognized, the call
attributed to `caller`, nothing from comments, nothing from the file
node); it failed before this change.
`check` treated any difference between stored and recomputed metrics as
a gate failure, so a model written by an older brick (without the
`metrics.resolution` field, or with an older formula) always failed,
even with every threshold met. CI checking an archived or cached model
broke on upgrade.
The gate already uses recomputed metrics, so a mismatch is now a
stderr warning. Only fields present in both copies are compared, so a
missing newer field does not count as a mismatch. Tampered metrics
still cannot pass: the existing test's recomputed cycle count fails the
gate, and it still reports "stored metrics".
Test: a freshly built model with `metrics.resolution` removed passes
`check --min-score 0`; it failed before this change.
The empty-model condition was written three times: a scan.rs
diagnostic, a helper in main.rs for `build`'s warning, and the same
helper for `check`'s failure. They could drift. model.rs now owns
`has_source_files` (and `Building::has_source_files`), used by all
three. No behaviour change; the empty-scan CLI test still passes.
Three import fixes from review of #5:
- sarif_uri_path sliced `&rest[i + 1..i + 3]` by byte index, so a `%`
followed by a multibyte character (`file:///src/%aé.c`) panicked the
whole build, and from_str_radix accepted `%+1` as byte 0x01. Escapes
are now decoded on bytes and only from two hex digits.
- Whether findings keep Fold's Symbolic lane was decided for the whole
report with all(): a merged SARIF log with one Fold run and one CodeQL
run demoted every Fold finding to Imported. The lane is now decided
per SARIF run (per report for native findings), and the diagnostic
names only the non-Fold producers.
- The rule that `..` after a symlinked directory follows the filesystem
applied only to relative paths; `/root/linkout/../src/main.rs` and its
file:// form still normalized lexically onto an in-repository file.
Absolute paths that exist now resolve through the filesystem too.
Tests (each failed before this change): percent-escape decoding without
panics, per-run lanes in a merged SARIF, and absolute/file:// paths
through a symlinked directory.
The README had grown to 254 lines covering an overview, every command,
the Fold/SARIF and p4c import contracts, the P4 pilot, per-language
resolution rules, limits, schema history, development and the agent
skill. It is now a 70-line overview (what Brick is, evidence lanes,
languages, quick start, commands, limits, links), and the rest lives in:
- docs/usage.md: every command (build, render, check with diff and
policy gates, mcp, setup), Fold/SARIF and p4c layering, the P4 pilot,
and what the model contains.
- docs/resolution.md: per-language name resolution as the code now
implements it, what is not handled, P4 limits, the regression corpus
and schema version 4.
- docs/development.md: build and test commands, the quality gate, the
toolchain pin, and the brick-sast skill.
Architecture docs are linked, not repeated. "What v0.1 provides" and
the milestone note are folded into usage.md and the roadmap pointer.
Every relative link and anchor resolves.
The audit found README claims the code did not back: resolution was said
to respect lexical namespace everywhere (Python/Swift matched a
repo-wide unique name, Rust types their leaf name), "ambiguous targets
stay external" (not for rival using-directives or #else stubs), Dart
imports were said to resolve (no Dart symbol ever did across files), and
"C/C++" hid that C went through the C++ grammar.
The README now describes the fixed behaviour and its remaining limits:
per-language visibility rules (Python imports, Swift module-wide names,
Dart imported libraries without show/hide), Rust type identity by
declaring file, preprocessor alternatives excluded from resolution,
using-directive ambiguity, the C grammar for .c, Fold report
validation and producer-gated symbolic lane, lossy UTF-8 decoding,
diagnostic counts and empty-scan reporting, check recomputing metrics,
low call-site coverage on large C++ code, no virtual-override fan-out,
and unrecognized Catch2 TEST_CASE bodies. The research note lists the
pinned C grammar.
tree-sitter's `Node::parent` re-descends from the root, so each call is
O(depth), and `enclosing_function`, `lexical_scope` and
`parameter_shadows` walked ancestors through it for every call site and
declaration: extraction time grew quadratically with nesting depth
(sample profile: >90% of time in ts_node_parent under those three).
The main traversal now records each node's parent, and the three
ancestor walks look it up (falling back to `Node::parent` for nodes the
traversal skips, so results are unchanged).
Measured on one C++ file with a call nested N blocks deep (release,
macOS arm64): N=2.5k/5k/10k/20k took 0.26/1.05/4.24/16.8 s before;
after, 20k takes 0.02 s with the same nodes and call edge. The full
suite (111 tests) passes unchanged.
`.c` files were parsed with tree-sitter-cpp, so valid C that uses C++
keywords as identifiers (`int delete = 0; int new = 1;`, a parameter
named `class`) produced grammar-error diagnostics and lost symbols.
`.c` files now parse with tree-sitter-c (pinned =0.24.2, backend
`c:0.24.2`) while staying on the shared C/C++ extraction path, so C
sources and the headers they include still share one include/declaration
index. Headers (`.h`) keep the C++ grammar, which parses the C subset.
Test: cpp_xtu::c_sources_parse_with_the_c_grammar fails before this
change (grammar errors in util.c).
A single invalid byte, such as a Latin-1 `é` in a comment, made
`read_to_string` fail and the whole file and its symbols were skipped.
Files are now read as bytes: a NUL byte still marks a binary file,
which is skipped with a diagnostic; otherwise invalid UTF-8 is decoded
lossily (U+FFFD) and a warning diagnostic records it, so the file keeps
its symbols and calls.
Test: languages::a_non_utf8_byte_does_not_drop_the_whole_file fails
before this change (latin1.rs has no symbols).
Fold report paths were normalized lexically, so `linkout/../src/main.rs`
with `linkout` pointing outside the root attached to the repository's
`src/main.rs` although the OS path names a different file.
A relative path containing `..` that exists in this checkout is now
canonicalized (and must stay under the canonical root); paths absent
from the checkout, such as reports from another machine, keep the
documented lexical matching.
Test: fold::tests::parent_steps_after_a_symlinked_directory_follow_the_filesystem
fails before this change (the finding attaches to src/main.rs).
The Fold report importer was schema-less and trusted self-description:
- `{"hello":1}` imported 0 findings with no diagnostic, like a clean run;
- findings without a file location were dropped silently;
- any JSON declaring backend "source" and confidence "proved"/"latent"
got the Symbolic lane, whoever produced it;
- SARIF `file://` URIs (and %-escapes) never matched a scanned file.
Now a report must be a SARIF log (`runs`) or a Fold findings report
(`findings`), otherwise the import fails with "not a Fold report". The
Symbolic lane requires the producer to be Fold (`tool.name`, or every
SARIF run's `tool.driver.name`, equal to "fold", which is what Fold
writes); other producers' findings are imported in the Imported lane
with a diagnostic. An unsupported `output_version` and findings dropped
for lack of a location are diagnosed. `file://[localhost]` URIs lose
their scheme and are percent-decoded before path matching. Existing
tests that model Fold reports now carry Fold's real envelope.
Tests (fold::tests) failing before this change:
rejects_a_file_that_is_not_a_fold_report,
symbolic_lane_requires_a_fold_producer_not_a_self_declared_label,
sarif_file_uris_match_and_unlocated_findings_are_diagnosed.
A Dart import exposes a library's public top-level names without a
per-name binding, so the binding-based lookup never matched and no
cross-file Dart call resolved, even when the import itself did (pipe's
VerifyCommand::run lost loadProject, analyzeProject, verifyAnalysis and
decodeBaselineEntries).
After the lexical-owner walk and binding lookup fail, a Dart call now
resolves to the one public (not `_`-prefixed) top-level declaration of
that name in the libraries the file imports; several such declarations
make it `ambiguous`, none leaves it external. `package:<name>/x.dart`
imports resolve to `lib/x.dart` when `<name>` is the root pubspec's
package; other packages stay external. `show`/`hide` combinators are not
modelled (noted as a follow-up).
Test: languages::dart_calls_resolve_through_imported_libraries fails
before this change (helper stays not-visible); an unimported file and a
library-private name stay unresolved.
Three lexical-preview bugs, reproduced on one probe:
- A blank or comment-only line in a Python function fell through to the
brace-depth scope rule, which popped every open scope, so every call
after the first blank line in a body was attributed to the file.
Blank/comment lines now leave Python scopes unchanged.
- `class Child(Base):` matched the call pattern and produced a call edge
from the file to Child. The declared class name is no longer a call
on its own header line.
- Calls resolved to the only function with that name anywhere in the
repository, imported or not (a NameError at runtime became an edge).
Resolution now prefers the caller's file; Python then considers only
files the caller imports (resolved import edges, cached per file);
otherwise the call stays external. Swift keeps the module-wide unique
name after the same-file check, because files of one Swift module see
each other without imports. Imported bases resolve the same way, so
`from pkg.b import Base` disambiguates `class Child(Base)`.
Test: languages::python_calls_keep_their_function_and_resolve_only_what_is_visible
fails before this change (go() had no call edges at all).
A directory with no supported source (Go/Java only) built
"1 nodes / 0 edges; score 100.0" with no diagnostic, and
`check --min-score 90 --max-cycles 0` passed it. The one-line summary
also hid diagnostics entirely: the yams run's 7,428 warnings (505 files
with grammar errors) never appeared.
- scan records a warning diagnostic when no source file was scanned;
- `build` prints the diagnostic and warning counts in its summary and
warns on stderr when the model is empty;
- `check` fails a model that contains no scanned source files.
Test: diff_cli::a_scan_with_no_supported_source_is_reported_and_fails_check
fails before this change (no diagnostic; check passes).
`brick check` read `metrics` from the model JSON and gated on them, so
setting `cycle_count` to 0 and `score` to 100 in the file turned a
failing `--max-cycles 0` gate into "passed" without changing the graph.
check now recomputes the metrics from the loaded nodes and edges and
gates on those; if the stored copy differs, that is itself a failure
("stored metrics do not match the graph"). Honest models recompute to
identical values, so existing gates are unchanged.
Test: diff_cli::check_recomputes_metrics_instead_of_trusting_the_file
fails before this change (the tampered model passes).
The base walker descended into type-argument lists, so
`class MyBloc extends Bloc<Ev, St>` emitted inherits edges to Bloc, Ev
and St; on pipe, 39 of 145 Dart inherits edges (27%) pointed at type
arguments (e.g. `Command<int>` -> `int`). C++ `: public Base<Arg>`
shared the walker through `template_argument_list`.
The walker stops at `type_arguments` / `template_argument_list` /
`type_parameters`, and handles tree-sitter-dart's shape, where the
arguments are sibling `type` fields of `superclass` (only the first
names the base) and nested `type` children of a `type`.
Test: languages::generic_type_arguments_are_not_base_classes fails
before this change (["Bloc", "Ev", "St"]).
Every preprocessor branch was indexed as live. In yams,
InternalEventBus.h guards spdlog with
`#if __has_include(<spdlog/spdlog.h>) ... #else namespace spdlog { stubs }`,
so 1,243 spdlog::warn/info/error call sites resolved to the fallback
stubs, which became the 4th and 5th highest fan-in nodes.
Brick does not evaluate conditions, so only the first branch is treated
as live: declarations and prototypes inside `#elif`/`#else` stay in the
graph (marked `source.preprocessor-branch: alternative`) but are not
definitions, declarations or classes for call resolution. Branches are
tracked textually from the directive lines, because conditions like
`__has_include(<x>)` leave tree-sitter-cpp with an error-recovered tree
that has no `preproc_else` node to find. Include-guard (`#ifndef`) code
is a first branch and still resolves.
Test: cpp_xtu::preprocessor_alternative_branches_are_not_call_targets
fails before this change (spdlog::info resolves into the #elif stub).
With `using namespace a; using namespace b;` and both `a::run` and
`b::run` declared, a call to `run` resolved to whichever directive came
first in file order and was labelled `using-directive`. In C++ that
call is ambiguous (or an overload choice Brick does not model).
Directives are now grouped: each `using N::x;` and `using namespace N;`
nominates at most one declaration (its innermost resolving scope), and
the call resolves only when exactly one distinct declaration is
nominated; otherwise it stays external as `ambiguous`.
Test: cpp_xtu::two_using_directives_naming_the_same_function_are_ambiguous
fails before this change; its single-directive case still resolves.
The Rust impl index keyed types by leaf name, so `a::Config` and
`b::Config` shared one `(Config, load)` entry and the inherent method
won: `use crate:🅱️:Config; c.load()` produced a `declared-receiver-type`
edge to `a::Config::load`.
The index now records which files declare each type name and the file
of every impl. When several files declare a name, a call resolves only
among impls of the type the calling file means (its own declaration,
then a `use` binding, then the only declaration); with no such binding
the call is left external as `ambiguous` instead of guessed. Unique
names behave as before.
Test: rust_methods::same_named_types_in_different_modules_resolve_through_use_bindings
fails before this change (edge to src/a.rs).
Adds the audit, literature rationale, experiment contract, labeled-corpus
results, Fold/Paperbridge/Brick/YAMS coverage and dead-code spot checks,
model changes, identity changes, and remaining limits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agents asking "is this function used?" get resolved, unknown, or
unreferenced per function instead of a bare zero in-degree. Unknown lists
why callers may exist: same-name unresolved call sites (excluding foreign
receivers and macros), name references (callbacks, address-taken, macro
bodies), virtual/trait-impl/external-base/outside-class members, framework
attributes, implicit protocol invocations, entry points, test code,
incomplete parses, and, unless assume_closed_world is set, exported or
external-linkage functions. Unreferenced is a review candidate with an
explicit heuristic/compiler evidence basis, never a dead-code proof.
The queries are read-only over validated schema-4 models and return
complete canonical nodes and edges plus metrics.resolution coverage.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
README limits now describe include-closure C++ lookup, declared-receiver
member resolution, Rust macro re-parsing, unresolved reasons, and
metrics.resolution, plus the remaining adapter gaps. The roadmap records
candidate edges, the Fold compiler call lane, and compile_commands/Cargo
module resolution as next stages; literature notes cite the call-graph and
name-resolution papers found through paperbridge.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
C++ calls now resolve through the include closure: header prototypes pair
with definitions in other translation units, lookup walks class (including
in-repository bases), namespace, and `using` scopes outward, and static or
anonymous-namespace definitions stay file-local. Member calls resolve only
through explicitly declared receiver types (parameters, locals, fields,
this, smart pointers, aliases); template parameters stay unknown. Calls
hidden by the most vexing parse are recovered, casts are no longer calls,
reference-returning and conversion-operator declarations are named
correctly, and benign `= {}` default-argument recoveries no longer drop
prototypes or flag files.
Rust method calls resolve through a crate-wide impl index for self, typed
parameters and lets, struct fields, Self-returning constructors, dyn
traits, and Type::/Self:: associated calls, with foreign imports and
generics never guessed. Calls inside expression macros are re-parsed.
Every unresolved call records source.unresolved-reason, weaker resolutions
record source.resolution/source.extraction, and metrics.resolution reports
per-language call-site coverage. Declarations gain facts consumed by
caller-status queries (qualified names, linkage, virtual, trait impls,
attributes, name references, parse-incomplete). C++/Rust External targets
include the reason in their identity; out-of-line C++ methods keep their
IDs.
Linear node/edge scans and per-node ancestor walks are replaced with
indexes, cutting a YAMS scan from 575 s to 23 s.
New labeled cpp_xtu and rust_methods corpora run three times in the
quality gate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Expand module-scoped use trees into exact import paths and bindings. Resolve renamed public symbols and module aliases without global-name fallback; keep unknown and block-local bindings conservative.
Add positive and negative integration tests and an exact three-repeat import benchmark to the quality gate.
Use pinned grammar frontends and scoped heuristic resolution to recover declarations and prevent false dependencies. Constrain filesystem traversal and normalize Fold root aliases.
Add exact multilingual fixtures, seventeen regression tests, and a three-repeat benchmark to the quality gate.