Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.9.7",
"version": "2.9.8",
"author": {
"name": "Egonex"
},
Expand Down
2 changes: 1 addition & 1 deletion .copilot-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.9.7",
"version": "2.9.8",
"author": {
"name": "Egonex"
},
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "understand-anything",
"displayName": "Understand Anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.9.7",
"version": "2.9.8",
"author": {
"name": "Egonex"
},
Expand Down
79 changes: 79 additions & 0 deletions tests/skill/understand/test_merge_batch_graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,46 @@ def test_scala_multimodule_sbt_layout(self) -> None:
cands = mbg.production_candidates("modules/core/src/test/scala/com/foo/BarSpec.scala")
self.assertIn("modules/core/src/main/scala/com/foo/Bar.scala", cands)

def test_ruby_rails_minitest_layout(self) -> None:
cands = mbg.production_candidates("test/models/user_test.rb")
self.assertEqual(cands[0], "app/models/user.rb")
self.assertIn("lib/models/user.rb", cands)
self.assertIn("models/user.rb", cands)

def test_ruby_rails_rspec_layout(self) -> None:
cands = mbg.production_candidates("spec/services/billing/charge_spec.rb")
self.assertEqual(cands[0], "app/services/billing/charge.rb")

def test_ruby_lib_under_test_root(self) -> None:
self.assertIn("lib/tasks/cleanup.rb", mbg.production_candidates("test/lib/tasks/cleanup_test.rb"))
self.assertIn("lib/tasks/cleanup.rb", mbg.production_candidates("spec/lib/tasks/cleanup_spec.rb"))

def test_ruby_gem_layout(self) -> None:
self.assertIn("lib/my_gem/parser.rb", mbg.production_candidates("spec/my_gem/parser_spec.rb"))
self.assertIn("lib/my_gem/parser.rb", mbg.production_candidates("test/my_gem/parser_test.rb"))
self.assertIn("lib/parser.rb", mbg.production_candidates("test/test_parser.rb"))

def test_ruby_engine_prefix_is_preserved(self) -> None:
cands = mbg.production_candidates("engines/billing/spec/models/invoice_spec.rb")
self.assertEqual(cands[0], "engines/billing/app/models/invoice.rb")
self.assertIn("engines/billing/lib/models/invoice.rb", cands)

def test_ruby_sibling_outside_test_root(self) -> None:
self.assertEqual(mbg.production_candidates("lib/parser_test.rb"), ["lib/parser.rb"])

def test_ruby_test_root_skips_sibling(self) -> None:
self.assertNotIn("test/models/user.rb", mbg.production_candidates("test/models/user_test.rb"))

def test_ruby_non_test_files_have_no_candidates(self) -> None:
for path in [
"spec/spec_helper.rb",
"spec/rails_helper.rb",
"test/test_helper.rb",
"app/models/user.rb",
]:
with self.subTest(path=path):
self.assertEqual(mbg.production_candidates(path), [])

def test_js_ts_test_subdir_walkout(self) -> None:
# Some JS/TS projects use `<dir>/test/` or `<dir>/spec/` instead of
# the more idiomatic `__tests__/`. Walk out of either.
Expand Down Expand Up @@ -427,6 +467,45 @@ def test_scala_multimodule_sbt_pairing_emits_forward_edge(self) -> None:
"file:modules/core/src/test/scala/com/foo/BarSpec.scala",
)

def test_ruby_rails_pairing_emits_forward_edges(self) -> None:
nodes_by_id = {
f"file:{path}": _file_node(path)
for path in [
"app/models/user.rb",
"test/models/user_test.rb",
"app/controllers/api/users_controller.rb",
"spec/controllers/api/users_controller_spec.rb",
]
}
edges: list[dict[str, Any]] = []

added, dropped, tagged, swapped = mbg.link_tests(nodes_by_id, edges)

self.assertEqual((added, dropped, tagged, swapped), (2, 0, 2, 0))
self.assertEqual(
{(e["source"], e["target"]) for e in edges},
{
("file:app/models/user.rb", "file:test/models/user_test.rb"),
(
"file:app/controllers/api/users_controller.rb",
"file:spec/controllers/api/users_controller_spec.rb",
),
},
)
self.assertTrue(all(e["direction"] == "forward" for e in edges))

def test_ruby_support_file_in_test_tree_is_not_paired(self) -> None:
nodes_by_id = {
f"file:{path}": _file_node(path)
for path in ["test/support/user.rb", "test/support/user_test.rb"]
}
edges: list[dict[str, Any]] = []

added, _dropped, _tagged, _swapped = mbg.link_tests(nodes_by_id, edges)

self.assertEqual(added, 0)
self.assertEqual(edges, [])

def test_swift_canonical_llm_edge_is_preserved(self) -> None:
# Regression for #646: a production → test edge must not be treated
# as production → production just because the test file is Swift.
Expand Down
2 changes: 1 addition & 1 deletion understand-anything-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.9.7",
"version": "2.9.8",
"author": {
"name": "Egonex"
},
Expand Down
2 changes: 1 addition & 1 deletion understand-anything-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@understand-anything/skill",
"version": "2.9.7",
"version": "2.9.8",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion understand-anything-plugin/packages/viewer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "understand-anything-viewer",
"version": "2.9.7",
"version": "2.9.8",
"description": "Standalone read-only viewer for Understand-Anything knowledge graphs — no Claude Code or LLM required.",
"type": "module",
"license": "MIT",
Expand Down
33 changes: 33 additions & 0 deletions understand-anything-plugin/skills/understand/merge-batch-graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ def resolve_ua_dir(root: Path) -> Path:
".rb": frozenset({"spec_helper"}),
}

_RUBY_TEST_ROOTS: frozenset[str] = frozenset({"test", "spec"})
_RUBY_PRODUCTION_ROOTS: tuple[str, ...] = ("app", "lib", "")
_RUBY_SUPPORT_STEMS: frozenset[str] = frozenset({"test_helper", "spec_helper", "rails_helper"})


# Mirrors packages/core/src/schema.ts so the dashboard validator has nothing
# left to auto-correct for the `direction` field on merged graphs.
Expand Down Expand Up @@ -381,6 +385,17 @@ def _strip_test_infix(stem: str) -> str | None:
return None


def _strip_ruby_test_marker(stem: str) -> str | None:
if stem in _RUBY_SUPPORT_STEMS:
return None
for suffix in ("_test", "_spec"):
if stem.endswith(suffix):
return stem[: -len(suffix)]
if stem.startswith("test_"):
return stem[len("test_"):]
return None


def _join(dir_path: str, name: str) -> str:
"""Join a (possibly empty) directory path to a basename with a single
slash, dropping the slash entirely when there is no directory."""
Expand Down Expand Up @@ -576,6 +591,24 @@ def production_candidates(test_path: str) -> list[str]:
)
break

elif ext == ".rb":
base_stem = _strip_ruby_test_marker(stem)
if base_stem:
test_root_idx = next(
(i for i, seg in enumerate(dir_segs) if seg in _RUBY_TEST_ROOTS),
None,
)
if test_root_idx is None:
_add_unique(candidates, _join(dir_path, f"{base_stem}.rb"))
else:
prefix_segs = dir_segs[:test_root_idx]
tail_segs = dir_segs[test_root_idx + 1 :]
for root in _RUBY_PRODUCTION_ROOTS:
new_dir = "/".join(
seg for seg in (*prefix_segs, root, *tail_segs) if seg
)
_add_unique(candidates, _join(new_dir, f"{base_stem}.rb"))

# ── C/C++ ─────────────────────────────────────────────────────────
elif ext in {".c", ".cpp", ".cc"}:
if stem.startswith("test_"):
Expand Down