Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
34c5d00
fix(visualization): use #graph-svg selector instead of bare svg
Jun 18, 2026
d8e5453
fix(skills): add PATH guard and remove absolute paths from hooks
Jun 18, 2026
cbf9355
fix: resolve PHP `use` imports to file paths
stefan-willems-youngones Jun 23, 2026
ddc8544
fix(graph): correct TESTED_BY edge direction in all consumer sites
tirth8205 Jun 14, 2026
918ef13
fix(refactor): read TESTED_BY by source in dead-code detection
tirth8205 Jun 14, 2026
580205d
test(graph): add end-to-end parser->store->get_transitive_tests guard
tirth8205 Jun 14, 2026
6ece151
fix(action): render repo-relative paths in PR comment
tirth8205 Jun 14, 2026
771307e
fix(csharp): capture receiver call edges (#612)
tirth8205 Jul 17, 2026
eeef686
fix(docs): preserve packaged fallback through MCP wrapper (#613)
tirth8205 Jul 17, 2026
571f665
fix(install): make JSONC sanitizer string-aware (#553)
Jun 24, 2026
9ef6641
Fix Codex hooks on Windows
adamwhite-autoscout Jul 14, 2026
df87b60
Generate skills compatible with Claude Code
michaelbaudino Jun 15, 2026
e5f563b
Make tests use uppercase `SKILL.md`
michaelbaudino Jun 15, 2026
90408c9
fix: harden install config parsing against data loss (#344) + regress…
azizur100389 Jun 10, 2026
0abd789
fix: persist Kotlin/C# annotations + resolve C# namespace importers_o…
azizur100389 Jun 10, 2026
b9ec19d
fix(parser): repair Zig support — grammar nodes were never matched
geeknik Apr 27, 2026
fc549ae
fix(install): preserve malformed nested server collections
tirth8205 Jul 17, 2026
d611a2d
fix(parser): cover in-source tests and nested namespaces
tirth8205 Jul 17, 2026
2416f54
docs: record maintainer reconciliation decisions
tirth8205 Jul 17, 2026
c7d7211
fix(parser): avoid recursion in C# namespace discovery
tirth8205 Jul 17, 2026
017a283
docs: finalize reconciliation validation
tirth8205 Jul 17, 2026
79f5018
docs: record green remote CI matrix
tirth8205 Jul 17, 2026
d897d3b
Revert "Fix Codex hooks on Windows"
tirth8205 Jul 17, 2026
4bd3f2d
docs: record Windows hook split
tirth8205 Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

## [Unreleased]

### Fixed

- Corrected TESTED_BY edge direction across graph, refactor, and transitive-test
consumers, with a parser-to-store-to-query regression (#527/#559/#598 class).
- C# receiver calls now capture bare, chained, member, and null-conditional
invocations with caller attribution (#612); Kotlin/C# annotations and C#
namespace importer resolution—including nested namespaces—are also preserved
(#295/#310, PR #353).
- Restored advertised Zig structure, calls, imports, and test nodes, including
TESTED_BY edges for test blocks embedded in ordinary source files (PR #393).
- Hardened generated skills/configuration: uppercase `SKILL.md` (PR #563),
string-safe JSONC plus top-level and nested-container data-preservation guards
(#553, PR #354), and portable PATH-aware hooks (PR #565).
- Packaged documentation remains reachable through the MCP wrapper (#613),
Action comments render repository-relative paths, and both visualization
templates select the graph SVG specifically (PR #564).
- **PHP `use` imports now resolve to files** (`importers_of`, impact radius,
call disambiguation): PHP `use` statements had no branch in the parser's
import extraction and fell through to the raw-text fallback, storing the whole
`use ...;` statement as the `IMPORTS_FROM` edge target (e.g.
`"use App\Domain\Entity\Job;"`). As a result `importers_of` / `tests_for` /
`inheritors_of` and the upstream side of `get_impact_radius` returned nothing
for PHP classes, and the unresolved targets also degraded cross-file `CALLS`
disambiguation in `resolve_bare_call_targets`. PHP imports are now recorded as
fully-qualified names (handling `as` aliases, grouped `use A\{B, C}`, and
`use function` / `use const`) and resolved to absolute `.php` paths by walking
up from the importing file, mirroring the existing Java resolver. Vendor/global
classes with no local file stay as the bare FQN.

## [2.3.6] - 2026-06-10

**Community-response release.** Built from a full audit of every open PR,
Expand Down
5 changes: 4 additions & 1 deletion code_review_graph/changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,10 @@ def analyze_changes(
for node in changed_funcs:
if node.is_test:
continue
tested = store.get_edges_by_target(node.qualified_name)
# TESTED_BY edges are stored as source=production, target=test by the
# parser, so a changed production function finds its tests by source.
# See: #515
tested = store.get_edges_by_source(node.qualified_name)
if not any(e.kind == "TESTED_BY" for e in tested):
test_gaps.append({
"name": _sanitize_name(node.name),
Expand Down
6 changes: 4 additions & 2 deletions code_review_graph/enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,12 @@ def _format_node_context(
lines.append(f" Flows: {', '.join(flow_names)}")

# Tests
# TESTED_BY edges are stored as source=production, target=test by the
# parser, so look them up by source. See: #515
tests: list[str] = []
for e in store.get_edges_by_target(qn):
for e in store.get_edges_by_source(qn):
if e.kind == "TESTED_BY" and len(tests) < 3:
t = store.get_node(e.source_qualified)
t = store.get_node(e.target_qualified)
if t:
tests.append(t.name)
if tests:
Expand Down
47 changes: 25 additions & 22 deletions code_review_graph/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,10 @@ def get_transitive_tests(
) -> list[dict]:
"""Find tests covering a node, including indirect (transitive) coverage.

1. Direct: TESTED_BY edges targeting this node (+ bare-name fallback).
TESTED_BY edges are stored as source=production, target=test by
the parser, so look them up by source_qualified. See: #515

1. Direct: TESTED_BY edges originating at this node (+ bare-name fallback).
2. Indirect: follow outgoing CALLS edges up to *max_depth* hops,
then collect TESTED_BY edges on each callee.

Expand Down Expand Up @@ -421,31 +424,31 @@ def _node_dict(qn: str, indirect: bool) -> dict | None:
"indirect": indirect,
}

# Direct TESTED_BY
# Direct TESTED_BY (source=production, target=test). See: #515
for qn in input_qns:
for row in conn.execute(
"SELECT source_qualified FROM edges "
"WHERE target_qualified = ? AND kind = 'TESTED_BY'",
"SELECT target_qualified FROM edges "
"WHERE source_qualified = ? AND kind = 'TESTED_BY'",
(qn,),
).fetchall():
src = row["source_qualified"]
if src not in seen:
seen.add(src)
d = _node_dict(src, indirect=False)
tgt = row["target_qualified"]
if tgt not in seen:
seen.add(tgt)
d = _node_dict(tgt, indirect=False)
if d:
results.append(d)

# Bare-name fallback for direct
bare = qualified_name.rsplit("::", 1)[-1] if "::" in qualified_name else qualified_name
for row in conn.execute(
"SELECT source_qualified FROM edges "
"WHERE target_qualified = ? AND kind = 'TESTED_BY'",
"SELECT target_qualified FROM edges "
"WHERE source_qualified = ? AND kind = 'TESTED_BY'",
(bare,),
).fetchall():
src = row["source_qualified"]
if src not in seen:
seen.add(src)
d = _node_dict(src, indirect=False)
tgt = row["target_qualified"]
if tgt not in seen:
seen.add(tgt)
d = _node_dict(tgt, indirect=False)
if d:
results.append(d)

Expand All @@ -464,14 +467,14 @@ def _node_dict(qn: str, indirect: bool) -> dict | None:
next_frontier = set(list(next_frontier)[:max_frontier])
for callee in next_frontier:
for row in conn.execute(
"SELECT source_qualified FROM edges "
"WHERE target_qualified = ? AND kind = 'TESTED_BY'",
"SELECT target_qualified FROM edges "
"WHERE source_qualified = ? AND kind = 'TESTED_BY'",
(callee,),
).fetchall():
src = row["source_qualified"]
if src not in seen:
seen.add(src)
d = _node_dict(src, indirect=True)
tgt = row["target_qualified"]
if tgt not in seen:
seen.add(tgt)
d = _node_dict(tgt, indirect=True)
if d:
results.append(d)
frontier = next_frontier
Expand Down Expand Up @@ -1269,8 +1272,8 @@ def load_flow_adjacency(self) -> "FlowAdjacency":
kind, src, tgt = row["kind"], row["source_qualified"], row["target_qualified"]
if kind == "CALLS":
calls_out.setdefault(src, []).append(tgt)
else: # TESTED_BY
has_tested_by.add(tgt)
else: # TESTED_BY: source is the production node being tested. See: #515
has_tested_by.add(src)

return FlowAdjacency(
calls_out=calls_out,
Expand Down
Loading
Loading