From f1e08bb920e16598902583d385bfbcc086be2db2 Mon Sep 17 00:00:00 2001 From: "songtianyi.theo" Date: Sat, 27 Jun 2026 01:30:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=8D=E7=94=A8=E6=9C=AA=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E7=9A=84=20SVG=20=E7=94=9F=E6=88=90=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当 generate_svg stage 记录被上游剪掉但既有 receipt、输入 hash 和生成输出仍然 current 时,runner 记录 cache_hit 并复用现有产物。 避免下游 gate 抖动导致不必要的 renderer 重跑,审计文档同步更新 M2 状态。 --- .../svglide-node-debug-time-audit.md | 8 +++-- .../scripts/svglide_project_runner.py | 30 +++++++++++++++++++ .../scripts/svglide_project_runner_test.py | 24 +++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/skills/lark-slides/references/svglide-node-debug-time-audit.md b/skills/lark-slides/references/svglide-node-debug-time-audit.md index 794fcc072..00bc2463d 100644 --- a/skills/lark-slides/references/svglide-node-debug-time-audit.md +++ b/skills/lark-slides/references/svglide-node-debug-time-audit.md @@ -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: diff --git a/skills/lark-slides/scripts/svglide_project_runner.py b/skills/lark-slides/scripts/svglide_project_runner.py index 35a7e0434..deb6176ef 100644 --- a/skills/lark-slides/scripts/svglide_project_runner.py +++ b/skills/lark-slides/scripts/svglide_project_runner.py @@ -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] = {} diff --git a/skills/lark-slides/scripts/svglide_project_runner_test.py b/skills/lark-slides/scripts/svglide_project_runner_test.py index 2b61207fa..99aa77576 100644 --- a/skills/lark-slides/scripts/svglide_project_runner_test.py +++ b/skills/lark-slides/scripts/svglide_project_runner_test.py @@ -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("", 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"