复用未失效的 SVG 生成产物

当 generate_svg stage 记录被上游剪掉但既有 receipt、输入 hash 和生成输出仍然 current 时,runner 记录 cache_hit 并复用现有产物。

避免下游 gate 抖动导致不必要的 renderer 重跑,审计文档同步更新 M2 状态。
This commit is contained in:
songtianyi.theo
2026-06-27 01:30:04 +08:00
parent 8cfbced316
commit f1e08bb920
3 changed files with 59 additions and 3 deletions

View File

@@ -107,7 +107,7 @@ Status conventions:
|---|---|---|---|
| Composite `plan_gate` output path mismatch | Fixed in code; test gap | Align composite `plan_gate` output list with the real `02-plan/strategy-review.json` output instead of nonexistent `06-check/strategy-review.json`. | `svglide_project_runner.py`; needs composite output existence test. |
| Composite gate stale ownership | Fixed in code for stale child rerun; broader ownership open | Composite gates now rerun stale child substages instead of failing the parent gate. Broader long-term work remains for richer stale diagnostics and all child-output ownership edge cases. | `svglide_project_runner.py`; `svglide_project_runner_test.py` covers stale legacy child rerun. |
| `generate_svg` repeated reruns | Partially fixed in code | `generate_svg` now records the real generation input boundary in `input_hashes` and explicitly excludes downstream quality receipts. Full render-cache reuse by renderer/template source hash is still open. | `svglide_project_runner.py`; `svglide_project_runner_test.py` covers downstream quality-gate isolation and changed-plan invalidation. |
| `generate_svg` repeated reruns | Fixed for current receipt reuse; broader renderer dependency hashing open | `generate_svg` now records the real generation input boundary in `input_hashes`, explicitly excludes downstream quality receipts, and reuses a current passed `generate_svg` receipt when a stage record was pruned but generation inputs/outputs are unchanged. | `svglide_project_runner.py`; `svglide_project_runner_test.py` covers downstream quality-gate isolation, changed-plan invalidation, and cache reuse after stage-record pruning. |
| Page-family role collapse to `content` | Mitigated for this run; generic guard open | Prefer explicit `canvas_spec.page_role` over weak top-level `page_type`. Ensure selected page-family decks preserve `page_role` / `page_variant_id`. | `svglide_project_runner.py`; current deck roles passed page-family smoke. |
| `template_fidelity` used as strict publishing blocker | Fixed in code for current-deck publish; promotion split still needs UI/docs cleanup | If page-family smoke passes and only soft screenshot issues remain above `warn_min`, mark template fidelity as `passed_with_warnings` and write separate `current-deck-visual-integrity` evidence. Quality gate now requires that current-deck evidence for warning-based publish, while template promotion still requires true fidelity pass. | `svglide_project_runner.py`, `svglide_quality_gate.py`; tests in `svglide_project_runner_test.py`, `svglide_quality_gate_test.py`. |
| `text_decoration_policy` missing in role consumption | Fixed in code | If `text_style_roles` exist but no explicit decoration policy exists, record renderer default absent policy instead of leaving receipt incomplete. | `beautiful_template_fidelity_check.py`; test in `beautiful_template_fidelity_check_test.py`. |
@@ -129,7 +129,7 @@ Status conventions:
### Still Open For Long-Term Speed
- Composite gate child stale ownership is less risky after stale-child rerun support, but richer diagnostics and child-output ownership tests are still needed.
- `generate_svg` has a clearer input boundary now; full cache reuse and renderer/template dependency hashing remain open.
- `generate_svg` can reuse a current receipt after stage-record pruning; broader renderer/template source dependency hashing remains open.
- Preflight needs a broader false-positive fixture suite.
- Debug-time audit now separates runner time from between-event gaps; automatic agent bucket capture is still open.
- Template promotion fidelity and current deck publish fidelity now have separate receipts for the warning path; remaining work is naming/docs cleanup and any UI/report surfacing.
@@ -166,7 +166,9 @@ Green:
- Store generation input hash set separately from upstream gate receipts.
- Let `generate_svg` reuse existing raw/contract/prepared outputs when generation inputs are unchanged.
- Completed first slice: `generate_svg` receipt `input_hashes` now records plan, lock, source evidence, source receipt, assets, and asset manifest, while excluding downstream quality receipts.
- Completed slices:
- `generate_svg` receipt `input_hashes` now records plan, lock, source evidence, source receipt, assets, and asset manifest, while excluding downstream quality receipts.
- If the `generate_svg` stage record was pruned but the existing passed receipt and generated outputs are still current, runner records a `cache_hit` and reuses the existing output instead of rerunning generation.
Validation:

View File

@@ -2617,6 +2617,33 @@ def validate_generator_receipt(project_root: Path, receipt: dict[str, Any]) -> l
return issues
def try_reuse_generate_svg_receipt(project_root: Path, state: dict[str, Any], *, started_at: str) -> dict[str, Any] | None:
path = receipt_path(project_root, "generate_svg")
receipt = read_json_optional(path)
if receipt.get("status") != "passed":
return None
try:
require_generated_svg_current(project_root)
except RunnerError:
return None
cached = dict(receipt)
cached["cache_hit"] = True
cached["cache_reason"] = "generate inputs and generated visual outputs are current"
record_stage(state, "generate_svg", "passed", path)
record_timing_event(
state,
stage="generate_svg",
status="passed",
started_at=started_at,
ended_at=now_iso(),
wall_time_seconds=0.0,
cache_hit=True,
)
write_state(project_root, state)
write_timing_report(project_root, state)
return cached
def run_generate_svg_stage(
project_root: Path,
state: dict[str, Any],
@@ -2627,6 +2654,9 @@ def run_generate_svg_stage(
require_source_current(project_root)
require_assets_current(project_root)
started_at = now_iso()
cached = try_reuse_generate_svg_receipt(project_root, state, started_at=started_at)
if cached:
return cached
generation_mode = plan_generation_mode(project_root)
command = svg_generator_command(project_root)
artboard_result: dict[str, Any] = {}

View File

@@ -2175,6 +2175,30 @@ class SVGlideProjectRunnerTest(unittest.TestCase):
self.assertNotIn("06-check/quality-gate.json", receipt["input_hashes"])
self.assertTrue((project_root / "04-svg/page-001.receipt.json").exists())
def test_generate_svg_reuses_current_receipt_after_stage_record_pruned(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
plan_root = Path(tmpdir) / ".lark-slides/plan"
result = runner.init_project("smoke", "Smoke", plan_root=plan_root)
project_root = Path(result["project_root"])
self.write_plan(project_root)
self.run_source(project_root)
runner.run_stage(project_root, "package-check")
runner.run_stage(project_root, "assets")
for page in range(1, 4):
(project_root / f"04-svg/page-{page:03d}.svg").write_text("<svg></svg>", encoding="utf-8")
runner.run_stage(project_root, "generate-svg")
state = runner.load_state(project_root)
state["stages"].pop("generate_svg")
runner.write_state(project_root, state)
receipt = runner.run_generate_svg_stage(project_root, runner.load_state(project_root))
self.assertEqual(receipt["status"], "passed")
self.assertTrue(receipt["cache_hit"])
updated = runner.load_state(project_root)
self.assertEqual(updated["stages"]["generate_svg"]["status"], "passed")
self.assertTrue(updated["timing_events"][-1]["cache_hit"])
def test_generate_svg_injects_file_backed_cover_asset(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
plan_root = Path(tmpdir) / ".lark-slides/plan"