- Makefile: add -Itests to the test-repro-runner compile so repro files in the
tests/repro/ subdir resolve "test_framework.h" (they sit one dir deeper than
the existing tests/*.c, which find it relative to their own directory).
- #495: cfg-gated twin functions collapse — two mutually-exclusive #[cfg] Rust
fns get identical qualified_name and the UNIQUE(project,qualified_name) upsert
overwrites one; asserts the twins get distinct QNs.
- #521: Route nodes minted from URL string literals in infra/config files
(try_upsert_infra_route has no source-vs-config guard); indexes a YAML-only
fixture and asserts zero Route nodes.
- #382: Java class-level + marker_annotation decorators dropped; asserts @Entity/
@RestController on the class and @Override on the method (strengthens the weak
existing #382 method-only test).
All three assert correct behaviour and are RED on current code.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Stand up a separate, non-gating reproduce-first suite that holds one RED case
per open bug issue (the redness is the deliverable + the regression guard):
- tests/repro/ + repro_main.c -> `make test-repro` (its own runner with its own
main + counters; deliberately EXCLUDED from ALL_TEST_SRCS so the gating
`make test` / ci-ok required check stays green and PRs are not wedged)
- repro_extraction.c: first reproduction, #554 -- a C++ out-of-line method's
inner CALLS edge must attribute to the class-qualified Method QN, not the
Module; ties the call's enclosing_func_qn to the method definition's own
qualified_name so a class-qualifier drop (the live root cause) fails it
- scripts/repro.sh: build+run the board; a build/link failure fails the job,
while expected test redness is reported as the board state (job stays green)
- .github/workflows/bug-repro.yml: workflow_dispatch (platform filter) + qa/**
push; runs the board on linux x2 / macos x2 / windows so many bug vectors can
be reproduced on many platforms at once
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
After the C declarator-name walker was de-duplicated into one shared helper
(cbm_resolve_c_declarator_name_node), the C/C++ enclosing-function resolver now
resolves qualified names (Foo::bar) via resolve_qualified_name() and no longer
treats type_identifier as a terminal name. Add reproduce-first coverage so the
#438 fix cannot silently regress on the qualified-declarator path:
- cpp_out_of_line_method_caller_attribution: a call inside `void Foo::bar()` must
attribute to the method, not the module.
- cpp_out_of_line_ctor_dtor_caller_attribution: calls inside `Foo::Foo()` and
`Foo::~Foo()` must attribute to the special member, not the module.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A CALLS edge whose caller is a C/C++/CUDA/GLSL function was sourced to the
file's Module node instead of the calling Function. "Find callers of X"
returned a file path, outbound trace_path returned empty, and
(:Function)-[:CALLS]->(:Function) queries missed for these languages.
Root cause: the enclosing-function resolvers read only tree-sitter's `name`
field, but a `function_definition` node has none — the name lives in the
declarator chain (pointer/function/parenthesized/array declarators). So
func_node_name() (internal/cbm/helpers.c) and resolve_func_name_node()
(internal/cbm/extract_unified.c) returned NULL, the enclosing scope fell
back to the module QN, and the edge was attributed to the Module node. This
is the C counterpart to #220, which fixed the definition-naming path but not
the enclosing-call path.
Fix: descend the declarator chain to the innermost name node (mirroring
resolve_c_declarator_name in extract_defs.c, including qualified and operator
names) when a function_definition lacks a `name` field. Adds the regression
test c_caller_attribution asserting a C call's enclosing_func_qn is the
function, not the module.
Fixes#438
Signed-off-by: Kris Kersey <kris@kerseyfabrications.com>
Addresses review on #539.
- Free p->saved_adr in cbm_pipeline_free so it is not leaked on error
paths that exit before the restore in dump_and_persist_hashes (e.g. a
cbm_gbuf_dump_to_sqlite failure).
- Check cbm_store_adr_store's return value on restore and log an error
instead of silently dropping the ADR (the original #516 symptom).
- Add reproduce-first test pipeline_adr_survives_full_reindex: index,
store an ADR, force a full re-index by adding files, assert the ADR
survives unchanged. Passes against the fix.
Signed-off-by: RithvikReddy0-0 <rithvikreddymukkara@gmail.com>
The MSYS2 CLANG64 toolchain bakes its package-tracker URL
(https://github.com/msys2/MINGW-packages) into the static Windows .exe via
libc++/compiler-rt. The Layer 2 binary string audit flagged it as an
unauthorized URL, failing smoke-windows (both the standard and ui variants)
while Linux smoke stayed green. Add it to ALLOWED_URLS alongside the other
compiler/linker toolchain URLs (gcc.gnu.org, sourceware.org, launchpad).
Add tests/test_security_strings_allowlist.sh as a reproduce-first regression
guard: it reproduces the BLOCKED failure with a binary fixture and includes a
negative control proving a genuinely unauthorized URL is still rejected. Wired
into scripts/test.sh as Step 6.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
extract_class_def() sets start_line and end_line from the tree-sitter
node span, but left def.lines = 0 (zeroed by memset at function entry).
The equivalent function push_method_def() correctly computes
def.lines = end_line - start_line + 1
The same one-liner was simply missing from the class extraction path.
This affects all languages — Python, TypeScript, Go, and any other
language where Class nodes are extracted via extract_class_def().
Regression test: after indexing a project, at least one Class node must
have c.lines > 0.
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
Route QNs (__route__METHOD__/path) are the rendezvous key for joining a client
HTTP_CALLS to the server handler that serves it, but they were built from the raw
path. Frameworks write path parameters with different placeholder syntaxes
(":id" Express/clients, "{id}" Axum/Spring/OpenAPI, "<id>" Flask/Rocket), so a
client call and its handler produced different QNs and never joined. Static
routes joined; parameterized routes (most of a REST surface) did not.
Add cbm_route_canon_path() which collapses each parameter token (":name",
"{name}", "<name>", "${...}") to a single "{}" token, and apply it to the path
at every HTTP-route QN construction site (pass_route_nodes, pass_parallel,
pass_calls, pass_cross_repo). Broker/async/infra QNs are left untouched. Only the
QN (identity/dedup key) is canonicalized; the Route node display name keeps the
original path. Parameter names are intentionally discarded so the same logical
endpoint matches across services that name the variable differently.
Adds tests/test_route_canon.c (11 cases). Fixes#505.
Signed-off-by: imateusdev <imateusdev@gmail.com>
cbm_store_node_degree backs the MCP get_code_snippet callers/callees
counts and the Cypher in_degree/out_degree virtual property, which are
deliberately CALLS-scoped. Restore it to CALLS-only and scope the
INHERITS/IMPLEMENTS broadening to cbm_store_search alone, which is the
search_graph surface issue #558 is about. Adjust the new tests so
node_degree assertions expect CALLS-only counts while the search_graph
result assertions expect the broadened structural degree.
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
search_graph reported in_degree/out_degree of 0 for Class nodes that
have real INHERITS edges, because the degree computation in both
cbm_store_node_degree (Cypher virtual properties) and cbm_store_search
(result population and min_degree/max_degree filter) only counted CALLS
(and USAGE) edges. A min_degree filter therefore silently excluded
well-connected classes.
Broaden the edge-type predicate in both surfaces to
IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS') so search_graph and
Cypher report the same edge-type-agnostic degree and stay consistent.
Add regression tests covering: a parent class with N INHERITS children,
combined CALLS+INHERITS degree without double counting, min_degree now
including inheritance-only classes, and isolated nodes still reporting
zero.
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
try_route_from_annotation read the annotation name via the "name" field
(ts_node_child_by_field_name). tree-sitter-kotlin annotation nodes have no
"name" field: the name lives in a nested type_identifier under (user_type)
or (constructor_invocation (user_type ...)). The lookup returned null, so
every Kotlin @RestController route was dropped (0 decorator-source routes)
while Java worked. Args are likewise under constructor_invocation as
value_arguments (each wrapped in value_argument), not Java argument_list.
Add annotation_name_node()/annotation_args_node() that fall back to the
Kotlin AST shape (Java path unchanged) and unwrap value_argument when
scanning for the route path. Kotlin @GetMapping/@PostMapping/etc. now yield
Route nodes and HANDLES edges. Adds handles_spring_kotlin regression test.
Signed-off-by: jsw6701 <86887824+jsw6701@users.noreply.github.com>
Python calls of the form iris_obj.classMethodValue('Pkg.Class', 'Method')
emitted no CALLS edge. The detection existed in walk_calls, which is dead
code — the pipeline uses cbm_extract_unified → handle_calls, not walk_calls.
Move the IRIS Native API string-dispatch detection to handle_calls. When a
Python call's callee ends in .classMethodValue, .classMethodVoid,
.classMethodBoolean, or .classMethodObject, and both string-literal arguments
can be extracted, emit a synthetic CALLS edge to Arg1.Arg2.
The pattern generalizes beyond this specific API: any library that takes
class/method names as string literals follows the same shape.
Regression test: python_iris_classMethodValue in test_extraction.c.
Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
cbm_gitignore_merge copied src patterns into dst one strdup at a time and
returned void on failure, leaving dst with a partial set of patterns and no
signal to the caller. If the dropped pattern was the critical exclude (the
.git/info/exclude entry that prevents an OOM walk), the bug could silently
recur with some patterns applied and others dropped.
Make the merge atomic: on any allocation failure roll back the partial copies
so dst is left exactly as it was, and return bool so callers can tell. The
.gitignore caller now degrades gracefully (exclude patterns skipped, as if the
file were absent) instead of ending up with a half-merged matcher.
Adds a reproduce-first test that injects a strdup failure mid-merge via a test
seam and asserts dst is unchanged; it is red without the rollback.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
- Declare cbm_artifact_export_last_error() in artifact.h and drop the ad-hoc
forward declarations in pipeline.c and test_artifact.c so there is a single
shared declaration.
- Document that dump_and_persist_hashes now intentionally propagates a failed
artifact export as a pipeline error (previously silently ignored), so the
behavior change is recorded in-tree.
- Use MoveFileExA(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) for the
Windows replace in write_file_atomic instead of unlink+rename; it replaces
the destination atomically (credit @Ayush7Ranjan, from #492).
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
The #334 plausibility gate compares committed (extracted) node count
against persisted rows, but committed_nodes was read AFTER
cbm_gbuf_dump_to_sqlite(), which calls release_gbuf_indexes() and frees
node_by_qn. cbm_gbuf_node_count() therefore returned 0, committed_nodes
fell at/below CBM_DUMP_VERIFY_MIN_FLOOR, and the gate never fired (a
healthy index reported expected_nodes:0 while expected_edges was correct,
since edges.count survives the dump).
- pipeline.c: capture committed node/edge counts before the dump.
- pipeline_incremental.c: set committed counts on the incremental path too
(via new cbm_pipeline_set_committed_counts), so the gate covers
incremental/reparse reindexes — the scenario #334 actually reproduces.
- test_pipeline.c: drive the real pipeline and assert committed_nodes > 0
and == persisted (the existing dump_verify tests fed synthetic counts and
could not catch the capture-ordering bug).
Signed-off-by: soren <nguiasoren@gmail.com>
Drop the repo-local .git/config pass in resolve_global_excludes_path() so
an untrusted repo cannot point core.excludesFile at an arbitrary file (e.g.
~/.ssh/id_rsa) and have cbm open and read it merely by being indexed. git is
safe here because it only trusts a repo's .git/config after a deliberate
clone/init; the indexer has no such trust step. Global excludes now resolve
only from the user-owned ~/.gitconfig and $XDG_CONFIG_HOME/git/config. A
repo's own ignores remain handled by .gitignore / .cbmignore.
Add a guard test asserting a repo-local .git/config whose excludesFile points
outside the repo is ignored, not read.
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Compare persisted SQLite node counts to in-memory dump counts after
index_repository completes so partial WAL/durability loss surfaces as
status:"degraded" instead of silent indexed.
Signed-off-by: Sam Li <yangsec888@gmail.com>
trace_path mode=cross_service resolved its default edge-type list to
{HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, CALLS} — none of the CROSS_* edge
types the cross-repo pass actually writes. Since cbm_store_bfs filters
edges with an exact `WHERE e.type IN (...)` clause, cross-service traces
silently skipped every cross hop even when the edge was present and
visible via query_graph.
Add the six CROSS_* types (CROSS_HTTP_CALLS, CROSS_ASYNC_CALLS,
CROSS_CHANNEL, CROSS_GRPC_CALLS, CROSS_GRAPHQL_CALLS, CROSS_TRPC_CALLS)
to the cross_service default list — exactly the set pass_cross_repo.c
emits and cross_repo_delete_edges() cleans up. The count is now computed
from the array size (sizeof) so it can't drift; the dead MCP_N_DEFAULTS_4
constant is removed. Tool description updated to match.
Adds integ_mcp_trace_path_cross_service: seeds a CROSS_HTTP_CALLS edge
between two indexed functions with no CALLS relationship and asserts
cross_service surfaces the hop while calls mode does not.
Signed-off-by: kirilklein <kikl@di.ku.dk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline): persist call-site line on CALLS edges
Persist CBMCall.start_line as a "line" property on CALLS edges in both resolution paths (finalize_and_emit + calls_emit_edge), so call-site lines are queryable instead of needing grep. Scoped to CALLS only: route/config edge props feed full-only predump passes the incremental path does not re-run, so changing them desyncs full vs incremental indexing. Verified: full suite matches unpatched main; line present on CALLS both paths.
Fixes#503
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
* test(pipeline): assert CALLS edge carries call-site line
Indexes a Go fixture where Helper() is called on line 8 and asserts the Main->Helper CALLS edge persists a line property (line:8) in its JSON props.
Refs #503.
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
---------
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
* fix(discover): honor .git/info/exclude during file discovery
index_repository only loaded .gitignore when building the exclusion
matcher. .git/info/exclude — the per-clone exclude file that Git treats
as authoritative — was never read, so paths excluded only there were
walked in full. On repos with Sandcastle worktrees this caused the
indexer to traverse 661K files / 170 GB and OOM (issue #489).
Add cbm_gitignore_merge() to append patterns from one matcher into
another. In cbm_discover_ex, after loading .gitignore, also load
.git/info/exclude and merge its patterns in. If only the exclude file
exists, use it directly. No downstream call paths change.
Add three unit tests for cbm_gitignore_merge and two integration tests
that reproduce the issue scenario (exclude-only and exclude-stacked-
with-gitignore).
Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
* style(discover): fix clang-format alignment in cbm_gitignore_merge
Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
---------
Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
* fix(search): ignore empty-string label in search_graph name_pattern path
search_where_basic guarded the label filter on NULL only (`if (params->label)`),
so a search passing label="" appended a literal `n.label = ''` clause that
matches no node — silently returning zero results. The BM25 query path already
treats an empty label as absent, so name_pattern/qn_pattern searches and BM25
searches behaved inconsistently for the same `"label":""` payload (issue #481):
agents fell back to grep because structural class/service discovery returned
nothing.
Guard the clause on `params->label && params->label[0]` so an empty-string
label is a no-op, identical to an omitted label and to the BM25 path.
Adds store_search_empty_label_ignored: name_pattern + label="" returns the same
match as name_pattern alone, while a non-empty label still filters.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(search): address QA round 1 — cover qn_pattern + empty label
QA round 1 noted store_search_empty_label_ignored asserted only the
name_pattern path though the comment referenced qn_pattern too. Add a
qn_pattern + label="" assertion so a future change that special-cased the
qn_pattern branch can't silently reintroduce the empty-label bug. Both paths
share search_where_basic, so this locks in the shared guard.
Test-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(resolver): disambiguate qualified cross-file calls by full namespace tail
cbm_registry_resolve reduced a package/namespace-qualified callee
(Foo::Bar::sub) to its bare simple name before the multi-candidate name
lookup. When that bare name was defined in several packages, the bare-name
scorer (best_by_import_distance) picked one winner deterministically and
routed every caller to it — collapsing distinct packages' same-named subs
onto a single definition and orphaning the rest. Impact is heaviest for Perl,
which has no cross-file LSP and relies entirely on this generic resolver, but
the defect applies to any "::"- or "."-qualified callee.
Add qualified_suffix_match: when a callee is qualified and its bare name has
multiple candidates, pick the sole candidate whose qualified tail matches at a
segment boundary (normalizing "::" to "."). Returns the unique match with a new
high-confidence "qualified_suffix" strategy; falls through to the existing
bare-name scoring when zero or several candidates match. Language agnostic —
bare callees contain no separator and are unaffected.
Adds resolve_qualified_disambiguates_same_name covering per-package routing of
a name defined in three packages, plus the bare-call (no-disambiguation) case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(resolver): address QA round 1 (qualified-suffix edge coverage)
Harden resolve_qualified_disambiguates_same_name and add a new test per the
QA round 1 advisory notes, guarding the fall-through paths against silent
regression:
- dotted callee form (App.Beta.save) resolves via qualified_suffix
- qualified callee matching no candidate tail falls through (not qualified_suffix)
- resolve_qualified_ambiguous_tail_falls_through: two candidates sharing the
same qualified tail are ambiguous → fall through to bare-name scoring
Test-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(perl): stop extracting config strings as call targets
The Perl branch of extract_scripting_callee blindly returned the text of
child(0) of every call node. In config-heavy Perl (.cgi/.pl with embedded
log4perl-style config), tree-sitter-perl misparses dotted config tokens
(e.g. "log4perl.appender.File.utf8") into call-shaped nodes, and that
dotted string was emitted as a callee_name, later matched by the generic
short-name resolver to unrelated project subs.
Now the Perl branch pulls the real name token (method/function field, else
child(0)) and validates it as a bare Perl sub/method identifier via
perl_is_identifier_callee: must start with a letter or '_' and contain only
[A-Za-z0-9_:] (allowing the '::' package separator). Any '.', whitespace,
quote, or '/' disqualifies it and NULL is returned so no CALLS edge forms.
Gated to CBM_LANG_PERL; other languages are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(resolver): don't match Perl builtins to project subs
Perl builtins (push/shift/keys/sprintf/...) carry no language or call-kind
awareness through the generic name-matcher in cbm_registry_resolve. When a
project defines a sub whose name collides with a builtin, an invocation of
the builtin was wired to that sub by same-module / suffix matching - a
false-positive CALLS edge.
Adds cbm_perl_is_builtin (curated, sorted bsearch set of 94 perlfunc core
builtins) and applies it in both call-resolution passes (sequential
resolve_single_call and parallel resolve_file_calls), gated on the file
language == CBM_LANG_PERL and only AFTER LSP resolution has declined, so a
genuine LSP-resolved call is never suppressed. The file language is threaded
into both resolvers via a new trailing CBMLanguage parameter; every other
language reaches cbm_registry_resolve unchanged (byte-identical behavior).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(resolver): suppress generic short-name matching for Perl method calls
A Perl method call ($obj->m / Class->m) carries no receiver type at the
structural tier, so the generic short-name matcher in cbm_registry_resolve
would wire $dbh->commit / $cgi->param / $logger->log to any project sub
sharing the bare method name - the dominant source of false-positive CALLS
edges in CPAN/framework-heavy Perl. Resolving such a call correctly is the
LSP's job, not the bare-name matcher's.
Adds a CBMCall.is_method flag (zero-init false, so all other languages and
existing call sites are unaffected). method_call_expression is added to the
Perl call node set and handle_calls sets is_method=true only for that node
type when the file language is Perl. Both call-resolution passes then skip
generic resolution for Perl method calls (combined with the builtin guard
from the prior commit). Genuine intra-project function calls (non-method,
non-builtin) still resolve as before. LSP-resolved method calls are
unaffected because the guard runs only after LSP resolution declines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(perl): cover call-graph noise fixes (builtins, methods, config strings)
Hermetic tests for the three Perl call-graph noise fixes:
test_extraction.c (extraction tier):
- config string is never emitted as a callee; genuine call still extracted
- builtin calls (push/keys) extracted but never flagged is_method
- arrow/method calls ($self->commit / $dbh->commit) set is_method=true,
while the genuine function call (helper) does not
- a JS method call never sets is_method (flag is Perl-only — other
languages unaffected)
test_registry.c (resolver tier):
- cbm_perl_is_builtin recognizes core builtins (incl. first/last of the
sorted set) and rejects project subs, case variants, empty, and NULL
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(perl): address QA round 1
Round 1 (Claude panel + DO DeepSeek) findings:
- Rework the Perl noise guard so it suppresses only WEAK generic matches
(suffix_match/unique_name) and KEEPS high-confidence same_module/import_map.
The prior guard ran before cbm_registry_resolve and dropped genuine same-file
calls to builtin-named subs (e.g. a project sub log/index/open called as a
bare function) that pre-PR resolved via same_module. Extracted the decision
into pure, unit-tested cbm_perl_suppress_generic_match() shared by the
sequential (pass_calls.c) and parallel (pass_parallel.c) resolvers; corrected
the inaccurate comments (Perl has no LSP/textual stage before the guard).
- Tighten perl_is_identifier_callee to require '::' pairs (reject a lone ':',
':::', or trailing '::').
- Add resolver-contract tests covering weak-match suppression, same_module/
import_map retention, genuine-call survival, non-Perl no-op, and NULL strategy.
Verified on a real Perl monorepo: .cgi builtin/CPAN/config-string noise stays
eliminated while same_module edges to builtin-named subs are recovered.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(perl): address QA round 2
Round 2 (Claude panel) caught a regression introduced by the round-1 refactor:
cbm_perl_suppress_generic_match whitelisted only the exact strategies
"same_module" and "import_map", but resolve_import_map can also return
"import_map_suffix" (confidence 0.85 — a genuine import-based resolution, not a
weak short-name guess). A '::'-qualified Perl builtin/method call resolved via
the import-suffix fallback was therefore dropped, contradicting the helper's
documented contract and partially missing acceptance criterion (d).
Add import_map_suffix to the kept (high-confidence) set so only the weak
short-name strategies (suffix_match / unique_name) are suppressed; update the
doc comment and add a unit-test case asserting import_map_suffix is retained.
Deferred as advisory (non-blocking, noted on the PR): a hypothetical leading-'::'
(main:: shorthand) under-extraction in perl_is_identifier_callee, and a
colon-edge-case coverage gap (logic correct by inspection).
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>