Add integration verification and validation phases, role templates, and static graph tests

- Implement Phase 4: Integration Verification to ensure skill package consistency.
- Implement Phase 5: Validation to verify quality and deliver the final skill package.
- Create role-template.md for generating per-role execution detail files.
- Create skill-router-template.md for generating SKILL.md with role-based routing.
- Add tests for static graph relationship writing during index build in test_static_graph_integration.py.
This commit is contained in:
catlog22
2026-02-13 12:35:31 +08:00
parent 6054a01b8f
commit a512564b5a
14 changed files with 2897 additions and 51 deletions

View File

@@ -145,7 +145,7 @@ class Config:
# Staged cascade search configuration (4-stage pipeline)
staged_coarse_k: int = 200 # Number of coarse candidates from Stage 1 binary search
staged_lsp_depth: int = 2 # LSP relationship expansion depth in Stage 2
staged_stage2_mode: str = "precomputed" # "precomputed" (graph_neighbors) | "realtime" (LSP)
staged_stage2_mode: str = "precomputed" # "precomputed" (graph_neighbors) | "realtime" (LSP) | "static_global_graph" (global_relationships)
# Static graph configuration (write relationships to global index during build)
static_graph_enabled: bool = False
@@ -627,7 +627,7 @@ class Config:
staged_stage2_mode = get_env("STAGED_STAGE2_MODE")
if staged_stage2_mode:
mode = staged_stage2_mode.strip().lower()
if mode in {"precomputed", "realtime"}:
if mode in {"precomputed", "realtime", "static_global_graph"}:
self.staged_stage2_mode = mode
log.debug("Overriding staged_stage2_mode from .env: %s", self.staged_stage2_mode)
elif mode in {"live"}:

View File

@@ -1293,6 +1293,9 @@ class ChainSearchEngine:
query=query,
)
if mode == "static_global_graph":
return self._stage2_static_global_graph_expand(coarse_results, index_root=index_root)
return self._stage2_precomputed_graph_expand(coarse_results, index_root=index_root)
except ImportError as exc:
@@ -1343,6 +1346,50 @@ class ChainSearchEngine:
return self._combine_stage2_results(coarse_results, related_results)
def _stage2_static_global_graph_expand(
self,
coarse_results: List[SearchResult],
*,
index_root: Path,
) -> List[SearchResult]:
"""Stage 2 (static_global_graph): expand using GlobalGraphExpander over global_relationships."""
from codexlens.search.global_graph_expander import GlobalGraphExpander
global_db_path = index_root / GlobalSymbolIndex.DEFAULT_DB_NAME
if not global_db_path.exists():
self.logger.debug("Global symbol DB not found at %s, skipping static graph expansion", global_db_path)
return coarse_results
project_id = 1
try:
for p in self.registry.list_projects():
if p.index_root.resolve() == index_root.resolve():
project_id = p.id
break
except Exception:
pass
global_index = GlobalSymbolIndex(global_db_path, project_id=project_id)
global_index.initialize()
try:
expander = GlobalGraphExpander(global_index, config=self._config)
related_results = expander.expand(
coarse_results,
top_n=min(10, len(coarse_results)),
max_related=50,
)
if related_results:
self.logger.debug(
"Stage 2 (static_global_graph) expanded %d base results to %d related symbols",
len(coarse_results), len(related_results),
)
return self._combine_stage2_results(coarse_results, related_results)
finally:
global_index.close()
def _stage2_realtime_lsp_expand(
self,
coarse_results: List[SearchResult],

View File

@@ -0,0 +1,289 @@
"""Tests for static graph relationship writing during index build (T2).
Verifies that IndexTreeBuilder._build_single_dir and _build_dir_worker
correctly write relationships to GlobalSymbolIndex when
config.static_graph_enabled is True.
"""
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from codexlens.config import Config
from codexlens.entities import (
CodeRelationship,
IndexedFile,
RelationshipType,
Symbol,
)
from codexlens.storage.global_index import GlobalSymbolIndex
@pytest.fixture()
def temp_dir():
tmpdir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
yield Path(tmpdir.name)
try:
tmpdir.cleanup()
except (PermissionError, OSError):
pass
def _make_indexed_file(file_path: str) -> IndexedFile:
"""Create a test IndexedFile with symbols and relationships."""
return IndexedFile(
path=file_path,
language="python",
symbols=[
Symbol(name="MyClass", kind="class", range=(1, 20)),
Symbol(name="helper", kind="function", range=(22, 30)),
],
relationships=[
CodeRelationship(
source_symbol="MyClass",
target_symbol="BaseClass",
relationship_type=RelationshipType.INHERITS,
source_file=file_path,
target_file="other/base.py",
source_line=1,
),
CodeRelationship(
source_symbol="MyClass",
target_symbol="os",
relationship_type=RelationshipType.IMPORTS,
source_file=file_path,
source_line=2,
),
CodeRelationship(
source_symbol="helper",
target_symbol="external_func",
relationship_type=RelationshipType.CALL,
source_file=file_path,
source_line=25,
),
],
)
def test_build_single_dir_writes_global_relationships_when_enabled(temp_dir: Path) -> None:
"""When static_graph_enabled=True, relationships should be written to global index."""
from codexlens.storage.index_tree import IndexTreeBuilder
config = Config(
data_dir=temp_dir / "data",
static_graph_enabled=True,
static_graph_relationship_types=["imports", "inherits"],
global_symbol_index_enabled=True,
)
# Set up real GlobalSymbolIndex
global_db_path = temp_dir / "global_symbols.db"
global_index = GlobalSymbolIndex(global_db_path, project_id=1)
global_index.initialize()
# Create a source file
src_dir = temp_dir / "src"
src_dir.mkdir()
test_file = src_dir / "module.py"
test_file.write_text("class MyClass(BaseClass):\n pass\n", encoding="utf-8")
indexed_file = _make_indexed_file(str(test_file))
# Mock parser to return our test IndexedFile
mock_parser = MagicMock()
mock_parser.parse.return_value = indexed_file
mock_mapper = MagicMock()
mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db"
mock_registry = MagicMock()
builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False)
builder.parser_factory = MagicMock()
builder.parser_factory.get_parser.return_value = mock_parser
result = builder._build_single_dir(
src_dir,
languages=None,
project_id=1,
global_index_db_path=global_db_path,
)
assert result.error is None
assert result.files_count == 1
# Verify relationships were written to global index
# Only IMPORTS and INHERITS should be written (not CALL)
rels = global_index.query_by_target("BaseClass", prefix_mode=True)
rels += global_index.query_by_target("os", prefix_mode=True)
assert len(rels) >= 1, "Expected at least 1 relationship written to global index"
# CALL relationship for external_func should NOT be present
call_rels = global_index.query_by_target("external_func", prefix_mode=True)
assert len(call_rels) == 0, "CALL relationships should not be written"
global_index.close()
def test_build_single_dir_skips_relationships_when_disabled(temp_dir: Path) -> None:
"""When static_graph_enabled=False, no relationships should be written."""
from codexlens.storage.index_tree import IndexTreeBuilder
config = Config(
data_dir=temp_dir / "data",
static_graph_enabled=False,
global_symbol_index_enabled=True,
)
global_db_path = temp_dir / "global_symbols.db"
global_index = GlobalSymbolIndex(global_db_path, project_id=1)
global_index.initialize()
src_dir = temp_dir / "src"
src_dir.mkdir()
test_file = src_dir / "module.py"
test_file.write_text("import os\n", encoding="utf-8")
indexed_file = _make_indexed_file(str(test_file))
mock_parser = MagicMock()
mock_parser.parse.return_value = indexed_file
mock_mapper = MagicMock()
mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db"
mock_registry = MagicMock()
builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False)
builder.parser_factory = MagicMock()
builder.parser_factory.get_parser.return_value = mock_parser
result = builder._build_single_dir(
src_dir,
languages=None,
project_id=1,
global_index_db_path=global_db_path,
)
assert result.error is None
# No relationships should be in global index
conn = global_index._get_connection()
count = conn.execute("SELECT COUNT(*) FROM global_relationships").fetchone()[0]
assert count == 0, "No relationships should be written when static_graph_enabled=False"
global_index.close()
def test_relationship_write_failure_does_not_block_indexing(temp_dir: Path) -> None:
"""If global_index.update_file_relationships raises, file indexing continues."""
from codexlens.storage.index_tree import IndexTreeBuilder
config = Config(
data_dir=temp_dir / "data",
static_graph_enabled=True,
static_graph_relationship_types=["imports", "inherits"],
global_symbol_index_enabled=True,
)
src_dir = temp_dir / "src"
src_dir.mkdir()
test_file = src_dir / "module.py"
test_file.write_text("import os\n", encoding="utf-8")
indexed_file = _make_indexed_file(str(test_file))
mock_parser = MagicMock()
mock_parser.parse.return_value = indexed_file
mock_mapper = MagicMock()
mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db"
mock_registry = MagicMock()
# Create a mock GlobalSymbolIndex that fails on update_file_relationships
mock_global_db_path = temp_dir / "global_symbols.db"
builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False)
builder.parser_factory = MagicMock()
builder.parser_factory.get_parser.return_value = mock_parser
# Patch GlobalSymbolIndex so update_file_relationships raises
with patch("codexlens.storage.index_tree.GlobalSymbolIndex") as MockGSI:
mock_gsi_instance = MagicMock()
mock_gsi_instance.update_file_relationships.side_effect = RuntimeError("DB locked")
MockGSI.return_value = mock_gsi_instance
result = builder._build_single_dir(
src_dir,
languages=None,
project_id=1,
global_index_db_path=mock_global_db_path,
)
# File should still be indexed despite relationship write failure
assert result.error is None
assert result.files_count == 1
def test_only_configured_relationship_types_written(temp_dir: Path) -> None:
"""Only relationship types in static_graph_relationship_types should be written."""
from codexlens.storage.index_tree import IndexTreeBuilder
# Only allow 'imports' (not 'inherits')
config = Config(
data_dir=temp_dir / "data",
static_graph_enabled=True,
static_graph_relationship_types=["imports"],
global_symbol_index_enabled=True,
)
global_db_path = temp_dir / "global_symbols.db"
global_index = GlobalSymbolIndex(global_db_path, project_id=1)
global_index.initialize()
src_dir = temp_dir / "src"
src_dir.mkdir()
test_file = src_dir / "module.py"
test_file.write_text("import os\nclass Foo(Bar): pass\n", encoding="utf-8")
indexed_file = _make_indexed_file(str(test_file))
mock_parser = MagicMock()
mock_parser.parse.return_value = indexed_file
mock_mapper = MagicMock()
mock_mapper.source_to_index_db.return_value = temp_dir / "index" / "_index.db"
mock_registry = MagicMock()
builder = IndexTreeBuilder(mock_registry, mock_mapper, config=config, incremental=False)
builder.parser_factory = MagicMock()
builder.parser_factory.get_parser.return_value = mock_parser
result = builder._build_single_dir(
src_dir,
languages=None,
project_id=1,
global_index_db_path=global_db_path,
)
assert result.error is None
# Only IMPORTS should be written
conn = global_index._get_connection()
rows = conn.execute(
"SELECT relationship_type FROM global_relationships"
).fetchall()
rel_types = {row[0] for row in rows}
assert "imports" in rel_types or len(rows) == 0 or rel_types == {"imports"}, \
f"Expected only 'imports', got {rel_types}"
# INHERITS should NOT be present
assert "inherits" not in rel_types, "inherits should not be written when not in config"
# CALL should NOT be present
assert "calls" not in rel_types, "calls should not be written"
global_index.close()