refactor: use project-named state file and store prompt/params directly
- State filename now derives from config: cards.bulkgen.yaml produces .cards.bulkgen-state.yaml instead of .bulkgen.state.yaml - Store resolved prompt text and extra params directly in state file instead of hashing them, making state files human-readable - Only file input contents remain hashed (SHA-256) - Thread project_name through builder and CLI - Remove hash_string() and _extra_hash() helpers - Update .gitignore pattern to .*.bulkgen-state.yaml
This commit is contained in:
parent
870023865d
commit
0ecf1f0f9e
7 changed files with 98 additions and 82 deletions
|
|
@ -10,13 +10,15 @@ from bulkgen.state import (
|
|||
BuildState,
|
||||
TargetState,
|
||||
hash_file,
|
||||
hash_string,
|
||||
is_target_dirty,
|
||||
load_state,
|
||||
record_target_state,
|
||||
save_state,
|
||||
state_filename,
|
||||
)
|
||||
|
||||
_PROJECT = "test"
|
||||
|
||||
|
||||
class TestHashFunctions:
|
||||
"""Test hashing helpers."""
|
||||
|
|
@ -34,18 +36,22 @@ class TestHashFunctions:
|
|||
h2 = hash_file(f)
|
||||
assert h1 != h2
|
||||
|
||||
def test_hash_string_deterministic(self) -> None:
|
||||
assert hash_string("abc") == hash_string("abc")
|
||||
|
||||
def test_hash_string_differs(self) -> None:
|
||||
assert hash_string("abc") != hash_string("xyz")
|
||||
class TestStateFilename:
|
||||
"""Test state filename derivation."""
|
||||
|
||||
def test_state_filename(self) -> None:
|
||||
assert state_filename("cards") == ".cards.bulkgen-state.yaml"
|
||||
|
||||
def test_state_filename_simple(self) -> None:
|
||||
assert state_filename("project") == ".project.bulkgen-state.yaml"
|
||||
|
||||
|
||||
class TestStatePersistence:
|
||||
"""Test save/load round-trip of build state."""
|
||||
|
||||
def test_load_missing_file_returns_empty(self, project_dir: Path) -> None:
|
||||
state = load_state(project_dir)
|
||||
state = load_state(project_dir, _PROJECT)
|
||||
assert state.targets == {}
|
||||
|
||||
def test_save_and_load_round_trip(self, project_dir: Path) -> None:
|
||||
|
|
@ -53,40 +59,36 @@ class TestStatePersistence:
|
|||
targets={
|
||||
"out.txt": TargetState(
|
||||
input_hashes={"dep.txt": "abc123"},
|
||||
prompt_hash="prompt_hash_val",
|
||||
prompt="Generate something",
|
||||
model="mistral-large-latest",
|
||||
extra_hash="",
|
||||
extra_params={},
|
||||
)
|
||||
}
|
||||
)
|
||||
save_state(state, project_dir)
|
||||
loaded = load_state(project_dir)
|
||||
save_state(state, project_dir, _PROJECT)
|
||||
loaded = load_state(project_dir, _PROJECT)
|
||||
|
||||
assert loaded.targets["out.txt"].model == "mistral-large-latest"
|
||||
assert loaded.targets["out.txt"].input_hashes == {"dep.txt": "abc123"}
|
||||
assert loaded.targets["out.txt"].prompt_hash == "prompt_hash_val"
|
||||
assert loaded.targets["out.txt"].prompt == "Generate something"
|
||||
|
||||
def test_load_empty_yaml(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / ".bulkgen.state.yaml").write_text("")
|
||||
state = load_state(project_dir)
|
||||
_ = (project_dir / state_filename(_PROJECT)).write_text("")
|
||||
state = load_state(project_dir, _PROJECT)
|
||||
assert state.targets == {}
|
||||
|
||||
def test_save_overwrites_existing(self, project_dir: Path) -> None:
|
||||
state1 = BuildState(
|
||||
targets={
|
||||
"a.txt": TargetState(input_hashes={}, prompt_hash="h1", model="m1")
|
||||
}
|
||||
targets={"a.txt": TargetState(input_hashes={}, prompt="p1", model="m1")}
|
||||
)
|
||||
save_state(state1, project_dir)
|
||||
save_state(state1, project_dir, _PROJECT)
|
||||
|
||||
state2 = BuildState(
|
||||
targets={
|
||||
"b.txt": TargetState(input_hashes={}, prompt_hash="h2", model="m2")
|
||||
}
|
||||
targets={"b.txt": TargetState(input_hashes={}, prompt="p2", model="m2")}
|
||||
)
|
||||
save_state(state2, project_dir)
|
||||
save_state(state2, project_dir, _PROJECT)
|
||||
|
||||
loaded = load_state(project_dir)
|
||||
loaded = load_state(project_dir, _PROJECT)
|
||||
assert "b.txt" in loaded.targets
|
||||
assert "a.txt" not in loaded.targets
|
||||
|
||||
|
|
@ -95,16 +97,16 @@ class TestStatePersistence:
|
|||
targets={
|
||||
"out.txt": TargetState(
|
||||
input_hashes={"f.txt": "hash"},
|
||||
prompt_hash="ph",
|
||||
prompt="do something",
|
||||
model="m",
|
||||
extra_hash="eh",
|
||||
extra_params={"width": 512},
|
||||
)
|
||||
}
|
||||
)
|
||||
save_state(state, project_dir)
|
||||
save_state(state, project_dir, _PROJECT)
|
||||
|
||||
raw: object = yaml.safe_load( # pyright: ignore[reportAny]
|
||||
(project_dir / ".bulkgen.state.yaml").read_text()
|
||||
(project_dir / state_filename(_PROJECT)).read_text()
|
||||
)
|
||||
assert isinstance(raw, dict)
|
||||
assert "targets" in raw
|
||||
|
|
@ -296,9 +298,9 @@ class TestRecordAndDirtyRoundTrip:
|
|||
state=state,
|
||||
project_dir=project_dir,
|
||||
)
|
||||
save_state(state, project_dir)
|
||||
save_state(state, project_dir, _PROJECT)
|
||||
|
||||
loaded_state = load_state(project_dir)
|
||||
loaded_state = load_state(project_dir, _PROJECT)
|
||||
assert not is_target_dirty(
|
||||
"result.md",
|
||||
resolved_prompt="do the thing",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue