diff --git a/packages/pi-rbtr/README.md b/packages/pi-rbtr/README.md index 088701a0..153bd3c7 100644 --- a/packages/pi-rbtr/README.md +++ b/packages/pi-rbtr/README.md @@ -41,7 +41,7 @@ Seven tools, registered automatically on session start: | `rbtr_search` | Search by name, keyword, or concept (BM25 + semantic + name fusion). Optional `keywords`/`variants` for query expansion. | | `rbtr_read_symbol` | Read a symbol's full source by name | | `rbtr_list_symbols` | Structural table of contents for a file | -| `rbtr_find_refs` | Find references via the dependency graph (imports, tests, docs) | +| `rbtr_find_refs` | Find references via the dependency graph (imports, docs) | | `rbtr_changed_symbols` | Symbols that changed between two git refs | | `rbtr_index` | Index the repository (background, incremental) | | `rbtr_status` | Check whether the index exists and how many symbols it contains | diff --git a/packages/pi-rbtr/extensions/rbtr/generated/protocol.ts b/packages/pi-rbtr/extensions/rbtr/generated/protocol.ts index e37d601a..fe41352e 100644 --- a/packages/pi-rbtr/extensions/rbtr/generated/protocol.ts +++ b/packages/pi-rbtr/extensions/rbtr/generated/protocol.ts @@ -71,7 +71,7 @@ export type QueryKind = "concept" | "identifier" | "code"; /** * Kind of relationship between chunks. */ -export type EdgeKind = "calls" | "imports" | "inherits" | "tests" | "documents" | "configures"; +export type EdgeKind = "calls" | "imports" | "inherits" | "documents" | "configures"; /** * How a symbol changed between two indexed commits. */ diff --git a/packages/pi-rbtr/extensions/rbtr/index.ts b/packages/pi-rbtr/extensions/rbtr/index.ts index b5ab40bc..89d27417 100644 --- a/packages/pi-rbtr/extensions/rbtr/index.ts +++ b/packages/pi-rbtr/extensions/rbtr/index.ts @@ -458,7 +458,7 @@ export default function rbtrIndexExtension(pi: ExtensionAPI) { "It understands meaning, not just substrings.\n" + "- Known symbol by name → rbtr_read_symbol. No file path needed.\n" + "- File outline before loading → rbtr_list_symbols.\n" + - "- Callers / tests / doc mentions of a symbol → rbtr_find_refs.\n" + + "- Callers / doc mentions of a symbol → rbtr_find_refs.\n" + "- Structural diff between refs (for PR review) → rbtr_changed_symbols.\n" + "Keep grep for literal strings (error messages, config keys, regexes) and use `read` for full-file reads; " + "rbtr_* tools are for structure. When a concept-question arises, reach for rbtr_search first.", @@ -848,7 +848,7 @@ export default function rbtrIndexExtension(pi: ExtensionAPI) { "Prefer rbtr_search over grep for concept-shaped questions: 'how does X work', 'where is Y handled', 'find the code that does Z'. It understands meaning, not just substrings — 'retry logic' finds 'backoff' and 'reconnect attempts', grep would miss them.", "Use grep when the user gives you a literal string or identifier that must match exactly (an error message, a regex, a configuration key, an import path).", "Use rbtr_read_symbol (not rbtr_search) when you already know a symbol name and want its full source.", - "Chain: rbtr_search finds candidates → pass the `name` field from a hit as the `symbol` parameter to rbtr_read_symbol for the full source → rbtr_find_refs for callers / tests.", + "Chain: rbtr_search finds candidates → pass the `name` field from a hit as the `symbol` parameter to rbtr_read_symbol for the full source → rbtr_find_refs for callers and docs.", "Scores are meaningful relative to the top result. A big drop-off after the first few hits means the tail is probably noise.", "For concept queries (natural language like 'how does idle unload work'): provide `keywords` (3-5 synonym identifiers or alternative terms, e.g. ['timeout', 'evict', 'unload_model']) and `variants` (1-2 rephrases using different terminology, e.g. ['when does the model get released from memory']).", "For identifier queries (symbol names like `fuse_scores`): optionally provide `keywords` only (alternative names the symbol might have, e.g. ['merge_scores', 'combine_results']). Omit `variants`.", @@ -1010,11 +1010,11 @@ export default function rbtrIndexExtension(pi: ExtensionAPI) { name: "rbtr_find_refs", label: "rbtr find-refs", description: - "Walk the dependency graph to find every place a symbol is imported, called from tests, or mentioned in docs. Takes a `symbol` string (same format as rbtr_read_symbol). Returns structural edges (source → target, kind=imports|tests|docs), not text matches.", - promptSnippet: "Find who imports / tests / documents a symbol via the index's dependency graph", + "Walk the dependency graph to find every place a symbol is imported or mentioned in docs. Takes a `symbol` string (same format as rbtr_read_symbol). Returns structural edges (source → target, kind=imports|docs), not text matches.", + promptSnippet: "Find who imports / documents a symbol via the index's dependency graph", promptGuidelines: [ - "Use rbtr_find_refs for impact analysis: 'what would break if I change X', 'which tests cover X', 'is X documented anywhere'.", - "This is structural, not textual: edge kinds ('imports', 'tests', 'docs') give you intent, not raw string occurrences. Prefer this to grep when asking a graph-shaped question.", + "Use rbtr_find_refs for impact analysis: 'what would break if I change X', 'is X documented anywhere'.", + "This is structural, not textual: edge kinds ('imports', 'docs') give you intent, not raw string occurrences. Prefer this to grep when asking a graph-shaped question.", "Use grep when you need every raw occurrence of an identifier including inside strings / comments / unsupported file types.", "Chain after rbtr_search or rbtr_read_symbol: you've identified the symbol, now find who depends on it.", "Pass file_paths to disambiguate a name that exists in several files — references are resolved only against symbols defined in the listed files.", diff --git a/packages/rbtr/ARCHITECTURE.md b/packages/rbtr/ARCHITECTURE.md index 0ea28f69..76906ddb 100644 --- a/packages/rbtr/ARCHITECTURE.md +++ b/packages/rbtr/ARCHITECTURE.md @@ -799,8 +799,6 @@ for the dispatch chain. - **Import edges** - structural (tree-sitter import extractor) or text-search fallback. -- **Test edges** - `test_foo.py` → `foo.py` by naming - convention and import analysis. - **Doc edges** - markdown sections mentioning symbol names. Import resolution maps a module string to a repo file: diff --git a/packages/rbtr/README.md b/packages/rbtr/README.md index 544eec2a..f1e34a24 100644 --- a/packages/rbtr/README.md +++ b/packages/rbtr/README.md @@ -171,7 +171,7 @@ rbtr list-symbols src/rbtr/index/search.py ### `rbtr find-refs ` Symbols that reference a given symbol via the dependency -graph (imports, tests, docs). +graph (imports, docs). ```bash rbtr find-refs IndexStore diff --git a/packages/rbtr/src/rbtr/index/edges.py b/packages/rbtr/src/rbtr/index/edges.py index b7dfea2c..b0c88da4 100644 --- a/packages/rbtr/src/rbtr/index/edges.py +++ b/packages/rbtr/src/rbtr/index/edges.py @@ -1,4 +1,4 @@ -"""Edge inference — imports, tests, docs relationships. +"""Edge inference — imports and docs relationships. All public functions are pure: chunks in, edges out. No I/O, no store access. The orchestrator feeds them chunks and writes @@ -47,10 +47,6 @@ class ImportResolution: """Directories to prepend when resolving absolute imports.""" path_substitutions: tuple[tuple[str, str], ...] """Module path prefix replacements (e.g. `("crate/", "src/")`).""" - test_prefix: str = "" - """Test file name prefix (e.g. `test_`).""" - test_suffix: str = "" - """Test file name suffix (e.g. `.test`, `_test`).""" module_style: ModuleStyle = ModuleStyle.PATH """How module strings map to file paths. See `ModuleStyle`.""" own_extensions: frozenset[str] = frozenset() @@ -78,8 +74,6 @@ def build_resolution_map(mgr: LanguageManager) -> dict[str, ImportResolution]: index_files=tuple(idxs), source_roots=reg.source_roots, path_substitutions=reg.path_substitutions, - test_prefix=reg.test_prefix, - test_suffix=reg.test_suffix, module_style=reg.module_style, own_extensions=reg.extensions, ) @@ -158,8 +152,8 @@ def _suffix_matches( """Collect repo files matching *name* as a full-path suffix. A file matches when it equals `name{ext}` or ends with `/name{ext}` for - some extension. Shared by the import resolver's Tier 3 and the test-edge - `_find_source_file` last resort so both apply one suffix-matching policy. + some extension. Used by the import resolver's Tier 3 (drop rather than + guess on ambiguity). """ matches: list[str] = [] for ext in extensions: @@ -430,183 +424,3 @@ def infer_import_edges( edges.extend(_text_search_import_edges(imp, file_symbols, stem_index)) return edges - - -# ── Test↔code edges ───────────────────────────────────────────────── -# -# Strategy: file-naming heuristic + import analysis (structural or -# text-search depending on what's available in the import chunks). - - -def _strip_test_affix( - file_path: str, - *, - test_prefix: str = "test_", - test_suffix: str = "", -) -> str | None: - """Extract the base name from a test file path. - - Tries the language's prefix first, then suffix:: - - test_prefix="test_": tests/test_foo.py → foo - test_suffix=".test": src/models.test.ts → models - test_suffix="_test": pkg/foo_test.go → foo - test_suffix="Test": FooTest.java → Foo - """ - name = PurePosixPath(file_path).stem - if test_prefix and name.startswith(test_prefix): - return name[len(test_prefix) :] - if test_suffix and name.endswith(test_suffix): - return name[: -len(test_suffix)] - return None - - -def _find_source_file( - base_name: str, - test_path: str, - repo_files: set[str], - resolution: ImportResolution | None = None, -) -> str | None: - """Find a source file matching *base_name*. - - Uses language-aware resolution when available, falling back - to `.py` heuristics. - """ - extensions = resolution.extensions if resolution else (".py",) - source_roots = resolution.source_roots if resolution else ("", "src") - - # Direct matches: source_root/base_name + ext. - for root in source_roots: - for ext in extensions: - candidate = f"{root}/{base_name}{ext}" if root else f"{base_name}{ext}" - if candidate in repo_files: - return candidate - - # Sibling of test directory: tests/test_foo → ../foo + ext. - test_dir = PurePosixPath(test_path).parent - if test_dir.name in ("tests", "test"): - parent = test_dir.parent - for root in ("", "src"): - for ext in extensions: - candidate = ( - str(parent / root / f"{base_name}{ext}") - if root - else str(parent / f"{base_name}{ext}") - ) - if candidate in repo_files: - return candidate - - # Underscore-to-slash expansion (Python convention: test_foo_bar → foo/bar). - if "_" in base_name: - nested = base_name.replace("_", "/") - for root in source_roots: - for ext in extensions: - candidate = f"{root}/{nested}{ext}" if root else f"{nested}{ext}" - if candidate in repo_files: - return candidate - - # Last resort: full-path suffix search, unique-match-only (same policy as - # the import resolver's Tier 3 — drop rather than guess on ambiguity). - candidates = _suffix_matches(base_name, repo_files, extensions) - if len(candidates) == 1: - return candidates[0] - - return None - - -def _imported_names_from_file( - test_chunks: list[Chunk], - source_file: str, - repo_files: set[str], - resolution: ImportResolution | None = None, -) -> list[str]: - """Extract symbol names imported from *source_file* by the test. - - Reads `chunk.metadata` — no language-specific parsing. - """ - names: list[str] = [] - for c in test_chunks: - if c.kind != ChunkKind.IMPORT or not c.metadata: - continue - - resolved = _resolve_import_to_file(c.metadata, c.file_path, repo_files, resolution) - if resolved != source_file: - continue - - names_str = c.metadata.names - if names_str: - names.extend(names_str.split(",")) - - return names - - -def infer_test_edges( - chunks: list[Chunk], - repo_files: set[str], - resolution_map: dict[str, ImportResolution] | None = None, -) -> list[Edge]: - """Infer `TESTS` edges from test files. - - Links test functions to the source symbols they test, using - file naming conventions and import analysis. When - *resolution_map* is provided, uses language-aware test-file - detection and file resolution. - """ - by_file: dict[str, list[Chunk]] = {} - # Track per-file language for resolution lookup. - file_language: dict[str, str] = {} - for c in chunks: - by_file.setdefault(c.file_path, []).append(c) - if c.language and c.file_path not in file_language: - file_language[c.file_path] = c.language - - symbol_index = _build_symbol_index(chunks) - edges: list[Edge] = [] - - for file_path, file_chunks in by_file.items(): - lang = file_language.get(file_path, "") - resolution = resolution_map.get(lang) if resolution_map else None - - base_name = _strip_test_affix( - file_path, - test_prefix=resolution.test_prefix if resolution else "test_", - test_suffix=resolution.test_suffix if resolution else "", - ) - if base_name is None: - continue - - source_file = _find_source_file(base_name, file_path, repo_files, resolution) - if source_file is None: - continue - - test_fns = [c for c in file_chunks if c.kind in (ChunkKind.FUNCTION, ChunkKind.METHOD)] - if not test_fns: - continue - - imported_names = _imported_names_from_file(file_chunks, source_file, repo_files, resolution) - - if imported_names: - for test_fn in test_fns: - for name in imported_names: - target = symbol_index.get((source_file, name)) - if target is not None: - edges.append( - Edge(source_id=test_fn.id, target_id=target.id, kind=EdgeKind.TESTS) - ) - else: - source_symbols = [ - c - for c in by_file.get(source_file, []) - if c.kind in (ChunkKind.FUNCTION, ChunkKind.CLASS, ChunkKind.METHOD) - ] - if source_symbols: - for test_fn in test_fns: - edges.append( - Edge( - source_id=test_fn.id, - target_id=source_symbols[0].id, - kind=EdgeKind.TESTS, - ) - ) - - return edges diff --git a/packages/rbtr/src/rbtr/index/models.py b/packages/rbtr/src/rbtr/index/models.py index 973dd1eb..bae108f1 100644 --- a/packages/rbtr/src/rbtr/index/models.py +++ b/packages/rbtr/src/rbtr/index/models.py @@ -62,7 +62,6 @@ class EdgeKind(StrEnum): CALLS = "calls" IMPORTS = "imports" INHERITS = "inherits" - TESTS = "tests" DOCUMENTS = "documents" CONFIGURES = "configures" diff --git a/packages/rbtr/src/rbtr/index/orchestrator.py b/packages/rbtr/src/rbtr/index/orchestrator.py index 8669664f..ccd042e7 100644 --- a/packages/rbtr/src/rbtr/index/orchestrator.py +++ b/packages/rbtr/src/rbtr/index/orchestrator.py @@ -32,7 +32,7 @@ from rbtr.config import config from rbtr.git import FileEntry, changed_files, list_files from rbtr.index.chunks import chunk_plaintext, detect_prose_format, host_presence_chunk -from rbtr.index.edges import build_resolution_map, infer_import_edges, infer_test_edges +from rbtr.index.edges import build_resolution_map, infer_import_edges from rbtr.index.embeddings import Embedder, embedding_text from rbtr.index.models import ( Chunk, @@ -406,7 +406,6 @@ def _infer_and_store_edges( resolution_map = build_resolution_map(mgr) edges: list[Edge] = [] edges.extend(infer_import_edges(chunks, repo_files, resolution_map)) - edges.extend(infer_test_edges(chunks, repo_files, resolution_map)) with store.session() as session: session.replace_edges(commit_sha, edges, repo_id=repo_id) diff --git a/packages/rbtr/src/rbtr/languages/c/plugin.py b/packages/rbtr/src/rbtr/languages/c/plugin.py index c6dbcad4..70e35416 100644 --- a/packages/rbtr/src/rbtr/languages/c/plugin.py +++ b/packages/rbtr/src/rbtr/languages/c/plugin.py @@ -40,6 +40,5 @@ # any leading run. doc_comment_node_types=frozenset({"comment"}), source_roots=("", "include", "src"), - test_prefix="test_", language_plugin_version=3, ) diff --git a/packages/rbtr/src/rbtr/languages/cpp/plugin.py b/packages/rbtr/src/rbtr/languages/cpp/plugin.py index 253e43b1..9e20db69 100644 --- a/packages/rbtr/src/rbtr/languages/cpp/plugin.py +++ b/packages/rbtr/src/rbtr/languages/cpp/plugin.py @@ -40,6 +40,5 @@ class Foo { # Same grammar as C — single `comment` node. doc_comment_node_types=frozenset({"comment"}), source_roots=("", "include", "src"), - test_prefix="test_", language_plugin_version=5, ) diff --git a/packages/rbtr/src/rbtr/languages/go/plugin.py b/packages/rbtr/src/rbtr/languages/go/plugin.py index cc4e2dff..7c9117ef 100644 --- a/packages/rbtr/src/rbtr/languages/go/plugin.py +++ b/packages/rbtr/src/rbtr/languages/go/plugin.py @@ -38,6 +38,5 @@ # link). The grammar uses a single `comment` # type for both line and block forms. doc_comment_node_types=frozenset({"comment"}), - test_suffix="_test", language_plugin_version=3, ) diff --git a/packages/rbtr/src/rbtr/languages/java/plugin.py b/packages/rbtr/src/rbtr/languages/java/plugin.py index 35f272ad..74c7ccc2 100644 --- a/packages/rbtr/src/rbtr/languages/java/plugin.py +++ b/packages/rbtr/src/rbtr/languages/java/plugin.py @@ -45,7 +45,6 @@ class Service { # directly above a method or class. doc_comment_node_types=frozenset({"block_comment", "line_comment"}), source_roots=("", "src/main/java"), - test_suffix="Test", module_style=ModuleStyle.DOTTED, language_plugin_version=3, ) diff --git a/packages/rbtr/src/rbtr/languages/javascript/plugin.py b/packages/rbtr/src/rbtr/languages/javascript/plugin.py index 8a5fbc9d..78d17ca1 100644 --- a/packages/rbtr/src/rbtr/languages/javascript/plugin.py +++ b/packages/rbtr/src/rbtr/languages/javascript/plugin.py @@ -126,7 +126,6 @@ def extract_import_meta( index_files=frozenset({"index.js"}), import_targets=frozenset({"javascript", "css"}), source_roots=("", "src"), - test_suffix=".test", language_plugin_version=4, ) typescript = LanguageRegistration( @@ -153,7 +152,6 @@ def extract_import_meta( index_files=frozenset({"index.ts", "index.js"}), import_targets=frozenset({"typescript", "tsx", "javascript", "css"}), source_roots=("", "src"), - test_suffix=".test", language_plugin_version=4, ) tsx = LanguageRegistration( @@ -180,7 +178,6 @@ def extract_import_meta( index_files=frozenset({"index.tsx", "index.ts", "index.js"}), import_targets=frozenset({"tsx", "typescript", "javascript", "css"}), source_roots=("", "src"), - test_suffix=".test", language_plugin_version=1, ) diff --git a/packages/rbtr/src/rbtr/languages/python/plugin.py b/packages/rbtr/src/rbtr/languages/python/plugin.py index c97f4b89..8e5a6bda 100644 --- a/packages/rbtr/src/rbtr/languages/python/plugin.py +++ b/packages/rbtr/src/rbtr/languages/python/plugin.py @@ -110,7 +110,6 @@ def extract_import_meta( class_scope_types=frozenset({"class_definition"}), index_files=frozenset({"__init__.py"}), source_roots=("", "src"), - test_prefix="test_", module_style=ModuleStyle.DOTTED, language_plugin_version=4, ) diff --git a/packages/rbtr/src/rbtr/languages/registration.py b/packages/rbtr/src/rbtr/languages/registration.py index e6948072..ebf18b3c 100644 --- a/packages/rbtr/src/rbtr/languages/registration.py +++ b/packages/rbtr/src/rbtr/languages/registration.py @@ -44,6 +44,9 @@ from rbtr.index.models import Chunk, ImportMeta +if TYPE_CHECKING: + from tree_sitter import Language, Node, Range + class ModuleStyle(StrEnum): """How import module strings map to file paths. @@ -68,9 +71,6 @@ class ModuleStyle(StrEnum): DOTTED = "dotted" -if TYPE_CHECKING: - from tree_sitter import Language, Node, Range - type ImportResolver = Callable[[Node, dict[str, list[Node]]], ImportMeta] type ImportExtractor = Callable[[ImportResolver, Node, dict[str, list[Node]]], ImportMeta] """Type alias for the import-metadata extractor override. @@ -146,8 +146,7 @@ class LanguageRegistration: - **Import resolution** (feeds the edge graph) — `index_files`, `import_targets`, `source_roots`, `path_substitutions`, `module_style`. - - **Housekeeping** — `test_prefix`, `test_suffix`, - `language_plugin_version`. + - **Housekeeping** — `language_plugin_version`. Extraction overrides — a custom name/scope/import resolver or a chunker — are *not* constructor arguments; attach them with the @@ -302,12 +301,6 @@ def _py_import(resolver, node, captures): before resolution (first matching prefix wins), for alias prefixes that don't map 1:1 to directories — e.g. `(("crate/", "src/"),)` so Rust's `crate::` resolves under `src/`. Empty → no rewriting.""" - test_prefix: str = "" - """Filename prefix marking a test file, e.g. `"test_"` (Python), used to - link a test back to the module it exercises. Empty → none.""" - test_suffix: str = "" - """Filename suffix (before the extension) marking a test file, e.g. - `".test"` (JS/TS), `"_test"` (Go). Empty → none.""" language_plugin_version: int = 1 """Extractor version. Bump on any extraction change (query, chunker, override, or scope config) — it triggers re-extraction of every blob diff --git a/packages/rbtr/src/rbtr/languages/ruby/plugin.py b/packages/rbtr/src/rbtr/languages/ruby/plugin.py index 26869fff..6480d531 100644 --- a/packages/rbtr/src/rbtr/languages/ruby/plugin.py +++ b/packages/rbtr/src/rbtr/languages/ruby/plugin.py @@ -92,7 +92,6 @@ def extract_import_meta( # type. doc_comment_node_types=frozenset({"comment"}), source_roots=("", "lib"), - test_prefix="test_", language_plugin_version=4, ) diff --git a/packages/rbtr/src/rbtr/languages/rust/plugin.py b/packages/rbtr/src/rbtr/languages/rust/plugin.py index fd2501cd..11ec2ebc 100644 --- a/packages/rbtr/src/rbtr/languages/rust/plugin.py +++ b/packages/rbtr/src/rbtr/languages/rust/plugin.py @@ -145,7 +145,6 @@ def extract_import_meta( doc_comment_node_types=frozenset({"line_comment", "block_comment"}), index_files=frozenset({"mod.rs"}), path_substitutions=(("crate/", "src/"),), - test_prefix="test_", language_plugin_version=4, ) diff --git a/packages/rbtr/src/rbtr/tests/index/cases_edges.py b/packages/rbtr/src/rbtr/tests/index/cases_edges.py index 6ae222c0..29a42110 100644 --- a/packages/rbtr/src/rbtr/tests/index/cases_edges.py +++ b/packages/rbtr/src/rbtr/tests/index/cases_edges.py @@ -1,10 +1,9 @@ """Scenarios for the edge-inference functions. Each case returns an `EdgeScenario` describing the chunks present, -the repo's file set, the inference function to call, and the -expected edge IDs. Tests in `test_edges_inference.py` dispatch -on `EdgeScenario.fn` to the matching `infer_*` function and assert -the result. +the repo's file set, and the expected edge IDs. Tests in +`test_edge_inference.py` run `infer_import_edges` and assert the +result. Chunks are declared as `ChunkSpec` tuples of the fields that matter for edge inference. The fixture fills in pydantic-required @@ -15,7 +14,6 @@ from __future__ import annotations from dataclasses import dataclass -from enum import StrEnum from rbtr.index.edges import ImportResolution from rbtr.index.models import ChunkKind, EdgeKind, ImportMeta @@ -24,14 +22,8 @@ from .cases_common import ChunkSpec -class InferFn(StrEnum): - IMPORT = "import" - TEST = "test" - - @dataclass(frozen=True) class EdgeScenario: - fn: InferFn chunks: list[ChunkSpec] repo_files: frozenset[str] = frozenset() # Expected set of (source_id, target_id) pairs in the result, @@ -59,7 +51,6 @@ class EdgeScenario: def case_import_from_import() -> EdgeScenario: """from src.models import User — module + names → exact target.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -85,7 +76,6 @@ def case_import_from_import() -> EdgeScenario: def case_import_variable_target() -> EdgeScenario: """from src.config import config — named import of a module-level VARIABLE.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp_cfg", @@ -111,7 +101,6 @@ def case_import_variable_target() -> EdgeScenario: def case_import_destructured_variable_target() -> EdgeScenario: """from src.config import a — `a` came from `a, b = load()`, still links.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp_a", @@ -137,7 +126,6 @@ def case_import_destructured_variable_target() -> EdgeScenario: def case_import_bare_module() -> EdgeScenario: """import src.models — bare import → edges to all non-import chunks.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -169,7 +157,6 @@ def case_import_bare_module() -> EdgeScenario: def case_import_relative() -> EdgeScenario: """from .models import Chunk — dots=1 + module + names.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -195,7 +182,6 @@ def case_import_relative() -> EdgeScenario: def case_import_relative_dot_only_unresolved() -> EdgeScenario: """from . import utils — dots=1 only; package __init__ missing.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -221,7 +207,6 @@ def case_import_relative_dot_only_unresolved() -> EdgeScenario: def case_import_stdlib_module_skipped() -> EdgeScenario: """import os — module doesn't resolve to a repo file.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -241,7 +226,6 @@ def case_import_stdlib_module_skipped() -> EdgeScenario: def case_import_target_symbol_missing() -> EdgeScenario: """Named import but symbol doesn't exist in target file.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -267,7 +251,6 @@ def case_import_target_symbol_missing() -> EdgeScenario: def case_import_multiple_names() -> EdgeScenario: """from src.models import Chunk, Edge — names list parsed.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -299,7 +282,6 @@ def case_import_multiple_names() -> EdgeScenario: def case_import_monorepo_absolute_suffix() -> EdgeScenario: """Absolute import resolves across a packages/*/src layout via suffix.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -330,7 +312,6 @@ def case_import_monorepo_absolute_suffix() -> EdgeScenario: def case_import_suffix_helpers_non_collision() -> EdgeScenario: """Full-path suffix resolves to the right file, not a same-named stem.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -368,7 +349,6 @@ def case_import_suffix_helpers_non_collision() -> EdgeScenario: def case_import_suffix_collision_dropped() -> EdgeScenario: """Same full-path suffix in two packages is ambiguous → no edge.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -406,7 +386,6 @@ def case_import_suffix_collision_dropped() -> EdgeScenario: def case_import_suffix_single_segment_guard() -> EdgeScenario: """A single-segment module never matches a nested file via suffix.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -435,7 +414,6 @@ def case_import_suffix_single_segment_guard() -> EdgeScenario: def case_import_text_fallback_file_stem_match() -> EdgeScenario: """No metadata → text search matches repo file stem.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -458,7 +436,6 @@ def case_import_text_fallback_file_stem_match() -> EdgeScenario: def case_import_text_fallback_no_match() -> EdgeScenario: """No metadata + no file stem match.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -475,7 +452,6 @@ def case_import_text_fallback_no_match() -> EdgeScenario: def case_import_text_fallback_short_stem_skipped() -> EdgeScenario: """File stems shorter than 3 chars are ignored by text search.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -501,7 +477,6 @@ def case_import_text_fallback_short_stem_skipped() -> EdgeScenario: def case_import_relative_overflow() -> EdgeScenario: """Relative import with more dots than path depth.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -527,7 +502,6 @@ def case_import_relative_overflow() -> EdgeScenario: def case_import_metadata_without_module_or_dots() -> EdgeScenario: """Metadata only has `names` — no way to resolve.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -550,183 +524,6 @@ def case_import_metadata_without_module_or_dots() -> EdgeScenario: ) -# ── test edges ────────────────────────────────────────────────────── - - -def case_test_edges_with_import() -> EdgeScenario: - """Test file imports source — edge links test_fn to imported symbol.""" - return EdgeScenario( - fn=InferFn.TEST, - chunks=[ - ChunkSpec( - id="fn1", - kind=ChunkKind.FUNCTION, - name="do_stuff", - file_path="src/foo.py", - ), - ChunkSpec( - id="imp1", - kind=ChunkKind.IMPORT, - name="from src.foo import do_stuff", - file_path="tests/test_foo.py", - metadata=ImportMeta(module="src.foo", names="do_stuff"), - ), - ChunkSpec( - id="tf1", - kind=ChunkKind.FUNCTION, - name="test_do_stuff", - file_path="tests/test_foo.py", - ), - ], - repo_files=frozenset({"src/foo.py", "tests/test_foo.py"}), - expected=frozenset({("tf1", "fn1")}), - ) - - -def case_test_edges_fallback_no_import() -> EdgeScenario: - """No import in test file — fall back to file-name convention.""" - return EdgeScenario( - fn=InferFn.TEST, - chunks=[ - ChunkSpec( - id="fn1", - kind=ChunkKind.FUNCTION, - name="do_stuff", - file_path="foo.py", - ), - ChunkSpec( - id="tf1", - kind=ChunkKind.FUNCTION, - name="test_do_stuff", - file_path="test_foo.py", - ), - ], - repo_files=frozenset({"foo.py", "test_foo.py"}), - expected=frozenset({("tf1", "fn1")}), - ) - - -def case_test_edges_no_source_file() -> EdgeScenario: - """Test file exists but the source it would test doesn't.""" - return EdgeScenario( - fn=InferFn.TEST, - chunks=[ - ChunkSpec( - id="tf1", - kind=ChunkKind.FUNCTION, - name="test_stuff", - file_path="tests/test_foo.py", - ), - ], - repo_files=frozenset({"tests/test_foo.py"}), - expected=frozenset(), - ) - - -def case_test_edges_non_test_file_skipped() -> EdgeScenario: - """A plain source file never yields test edges.""" - return EdgeScenario( - fn=InferFn.TEST, - chunks=[ - ChunkSpec( - id="fn1", - kind=ChunkKind.FUNCTION, - name="foo", - file_path="src/foo.py", - ), - ], - repo_files=frozenset({"src/foo.py"}), - expected=frozenset(), - ) - - -def case_test_edges_multiple_test_fns() -> EdgeScenario: - """Several tests in the same file all link to the same source fn.""" - return EdgeScenario( - fn=InferFn.TEST, - chunks=[ - ChunkSpec( - id="fn1", - kind=ChunkKind.FUNCTION, - name="do_stuff", - file_path="foo.py", - ), - ChunkSpec( - id="tf1", - kind=ChunkKind.FUNCTION, - name="test_a", - file_path="test_foo.py", - ), - ChunkSpec( - id="tf2", - kind=ChunkKind.FUNCTION, - name="test_b", - file_path="test_foo.py", - ), - ], - repo_files=frozenset({"foo.py", "test_foo.py"}), - expected=frozenset({("tf1", "fn1"), ("tf2", "fn1")}), - ) - - -def case_test_edges_imports_without_functions() -> EdgeScenario: - """Test file has only imports, no test functions — no edges.""" - return EdgeScenario( - fn=InferFn.TEST, - chunks=[ - ChunkSpec( - id="imp1", - kind=ChunkKind.IMPORT, - name="from src.foo import do_stuff", - file_path="tests/test_foo.py", - metadata=ImportMeta(module="src.foo", names="do_stuff"), - ), - ChunkSpec( - id="fn1", - kind=ChunkKind.FUNCTION, - name="do_stuff", - file_path="src/foo.py", - ), - ], - repo_files=frozenset({"src/foo.py", "tests/test_foo.py"}), - expected=frozenset(), - ) - - -def case_test_edges_monorepo_suffix() -> EdgeScenario: - """Test resolves its source by full-path suffix across a monorepo. - - The source is neither at a root nor a sibling of the test directory, so - only `_find_source_file`'s last-resort suffix match locates it. - """ - return EdgeScenario( - fn=InferFn.TEST, - chunks=[ - ChunkSpec( - id="fn1", - kind=ChunkKind.FUNCTION, - name="build_widget", - file_path="packages/core/src/core/factory.py", - language="python", - ), - ChunkSpec( - id="tf1", - kind=ChunkKind.FUNCTION, - name="test_build_widget", - file_path="packages/app/tests/test_factory.py", - language="python", - ), - ], - repo_files=frozenset( - { - "packages/core/src/core/factory.py", - "packages/app/tests/test_factory.py", - } - ), - expected=frozenset({("tf1", "fn1")}), - ) - - # ── doc edges ─────────────────────────────────────────────────────── @@ -749,7 +546,6 @@ def case_test_edges_monorepo_suffix() -> EdgeScenario: def case_doc_md_link_to_code() -> EdgeScenario: """MD [link](../src/foo.py) → DOCUMENTS edges to all symbols.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -782,7 +578,6 @@ def case_doc_md_link_to_code() -> EdgeScenario: def case_doc_md_link_to_doc() -> EdgeScenario: """MD [link](other.md) → DOCUMENTS edges to all DOC_SECTIONs.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -817,7 +612,6 @@ def case_doc_md_link_to_doc() -> EdgeScenario: def case_doc_md_link_with_fragment() -> EdgeScenario: """MD [link](other.md#details) → targeted DOCUMENTS edge.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -852,7 +646,6 @@ def case_doc_md_link_with_fragment() -> EdgeScenario: def case_doc_rst_func_role() -> EdgeScenario: """RST :func:`do_stuff` → targeted DOCUMENTS edge to function.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -879,7 +672,6 @@ def case_doc_rst_func_role() -> EdgeScenario: def case_doc_rst_doc_role() -> EdgeScenario: """RST :doc:`api/module` → DOCUMENTS edges to all chunks.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="imp1", @@ -907,7 +699,6 @@ def case_doc_rst_doc_role() -> EdgeScenario: def case_doc_md_bare_mention_no_edge() -> EdgeScenario: """Prose mentions do_stuff without a link → no edge.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="doc1", @@ -931,7 +722,6 @@ def case_doc_md_bare_mention_no_edge() -> EdgeScenario: def case_doc_no_references_no_edges() -> EdgeScenario: """DOC_SECTION only, no IMPORT chunks → no edges.""" return EdgeScenario( - fn=InferFn.IMPORT, chunks=[ ChunkSpec( id="doc1", @@ -952,7 +742,6 @@ def case_doc_no_references_no_edges() -> EdgeScenario: def case_import_html_script_src_relative() -> EdgeScenario: """HTML