feat(pipeline): support incremental runs and analysis caching

Reuse existing output directory by default instead of failing
Cache successful Gemini analysis results using content hashing
Skip unchanged files and retry failed analyses on subsequent runs
Update --force flag to explicitly delete and rebuild the output
This commit is contained in:
2026-04-02 11:03:04 +08:00
parent c73767073e
commit 0cf62d1ac5
4 changed files with 165 additions and 22 deletions

View File

@@ -64,6 +64,15 @@ class FlakyAnalyzer:
return f"# {relative_path.as_posix()}\n\n{len(content)}"
class RecordingAnalyzer:
def __init__(self) -> None:
self.paths: list[str] = []
def analyze(self, relative_path: Path, content: str) -> str:
self.paths.append(relative_path.as_posix())
return f"# {relative_path.as_posix()}\n\n{len(content)}"
class PipelineTests(unittest.TestCase):
def test_strip_comment_out_blocks_removes_nested_blocks(self) -> None:
source = "<root><ui:CommentOut><x/><ui:CommentOut><y/></ui:CommentOut></ui:CommentOut><z/></root>"
@@ -173,6 +182,47 @@ class PipelineTests(unittest.TestCase):
self.assertTrue((docs_root / "Scripts" / "Keep.bas.analysis.md").exists())
self.assertTrue(any("Analysis failed for Flows/Active.xaml" in item for item in report.warnings))
def test_pipeline_resume_skips_successfully_cached_analyses(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
first = RecordingAnalyzer()
ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(first)
self.assertEqual(
first.paths,
["Flows/Active.xaml", "main.xaml", "Scripts/Keep.bas"],
)
second = RecordingAnalyzer()
ProjectPipeline(project_root, output_root, "main.xaml", force=False).run(second)
self.assertEqual(second.paths, [])
def test_pipeline_resume_retries_failed_analysis_and_reanalyzes_changed_files(self) -> None:
with TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
project_root = tmp_path / "project"
output_root = tmp_path / "workspace"
(project_root / "Flows").mkdir(parents=True)
(project_root / "Flows" / "Active.xaml").write_text(ACTIVE_XAML, encoding="utf-8")
(project_root / "Scripts").mkdir()
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nEnd Sub", encoding="utf-8")
(project_root / "main.xaml").write_text(MAIN_XAML, encoding="utf-8")
ProjectPipeline(project_root, output_root, "main.xaml", force=True).run(FlakyAnalyzer())
(project_root / "Scripts" / "Keep.bas").write_text("Sub Keep()\nMsgBox \"updated\"\nEnd Sub", encoding="utf-8")
retry = RecordingAnalyzer()
ProjectPipeline(project_root, output_root, "main.xaml", force=False).run(retry)
self.assertEqual(retry.paths, ["Flows/Active.xaml", "Scripts/Keep.bas"])
if __name__ == "__main__":
unittest.main()