IB-WP-0019-T01: plan snapshot persistence

Every generate plan invocation now appends its compact summary to
output/budget/plans.yaml with a deterministic 12-char snapshot_id
hashed over the selection filters and the estimated call/token/cost
totals. Identical-fingerprint plans refresh the most recent entry's
recorded_at instead of stacking duplicates. Retention defaults to the
last 50 snapshots; older entries are pruned and counted on a top-level
pruned_count field.

The summary now echoes its input filters (chapter_filter, chunk_filter,
from_chapter, to_chapter) so reviewers can read the snapshot without
cross-referencing the CLI invocation.

New module src/infospace_bench/budget.py owns layer 1 (per-infospace
recording) of the IB-WP-0019 three-layer design; layer 2 still belongs
in llm-connect LLM-WP-0004 and layer 3 in state-hub.

99 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 19:19:35 +02:00
parent df87e212a2
commit 182f7011bb
4 changed files with 337 additions and 1 deletions

View File

@@ -15,6 +15,7 @@ from .evaluation_io import read_entity_evaluations
from .history import get_history, read_metrics_file, record_check_results
from .lifecycle import create_infospace, load_infospace, register_artifact
from .openrouter import OpenRouterAssistedGenerationAdapter
from .budget import record_plan_snapshot
from .source_intake import SourceChunk, normalize_source
from .workflow import (
AssistedGenerationAdapter,
@@ -113,6 +114,7 @@ def plan_generation(
words_per_token: float = WORDS_PER_TOKEN_DEFAULT,
entities_per_chunk: int = ENTITIES_PER_CHUNK_ESTIMATE,
full: bool = False,
persist: bool = True,
) -> dict[str, Any]:
root_path = Path(root)
status = status_generation(root_path)
@@ -129,9 +131,15 @@ def plan_generation(
words_per_token=words_per_token,
entities_per_chunk=entities_per_chunk,
)
summary["chapter_filter"] = list(chapter_filter) if chapter_filter else None
summary["chunk_filter"] = list(chunk_filter) if chunk_filter else None
summary["from_chapter"] = from_chapter
summary["to_chapter"] = to_chapter
summary["root"] = str(root_path)
summary["stale"] = status["stale"]
summary["status"] = "planned"
if persist:
summary["snapshot_id"] = record_plan_snapshot(root_path, summary)
if not full:
return summary
workflow_ids = _workflow_ids_for_stage(stage)