feat(s42): byte-deterministic write_run_config outbox writer

This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 12:39:42 +02:00
commit ebae4639dd
2 changed files with 75 additions and 2 deletions

View file

@ -1,4 +1,9 @@
"""RAW output layer (Fase 2a, S2.1 · målbilde §3, R2): byte-deterministic outbox writer.
"""RAW output layer (Fase 2a, S2.1 · målbilde §3, R2): byte-deterministic outbox writers.
Two writers live here. ``write_run_config`` (S4.2, comparison protocol §4 pkt 3) persists the
run-config artefact ``{run_id}-runconfig.json`` the resolved model-id per built role, profile,
params and token cap describing a run WITHOUT a model call (the S4.2 dry-run drill + the M2 live
run). ``write_outbox`` persists the post-run proposal/outcome artefacts.
After a run, ``write_outbox`` persists two JSON artefacts ``{run_id}-proposal.json`` (the candidate
IR + its provenance stamp) and ``{run_id}-outcome.json`` (the validated Monte-Carlo percentiles OR
@ -91,3 +96,38 @@ def write_outbox(
outcome_path.write_text(_dump(outcome_payload), encoding="utf-8")
return proposal_path, outcome_path
def write_run_config(
config_dir: str,
run_id: str,
*,
profile: str,
resolved_models: dict[str, str],
max_rounds: int,
max_tokens: int,
top_k: int,
) -> Path:
"""Write the byte-deterministic run-config artefact ``{run_id}-runconfig.json`` (S4.2, comparison
protocol §4 pkt 3): the resolved model-id per *built* role, the profile, the round/token
parameters and the token cap everything that describes a run WITHOUT a model call. Takes plain
data only (the caller resolves the models via ``resolve_model``), so this module stays MAF-free.
NO wall-clock / date (that lives in the S11 report envelope, not the deterministic artefact), so
two runs with identical config produce byte-identical files (mirrors ``write_outbox``)."""
directory = Path(config_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-runconfig.json"
path.write_text(
_dump(
{
"run_id": run_id,
"profile": profile,
"resolved_models": resolved_models,
"max_rounds": max_rounds,
"max_tokens": max_tokens,
"top_k": top_k,
}
),
encoding="utf-8",
)
return path

View file

@ -18,7 +18,7 @@ from agent_framework import BaseChatClient
from conftest import SyntheticUsageChatClient
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
from portfolio_optimiser.outbox import write_outbox
from portfolio_optimiser.outbox import write_outbox, write_run_config
from portfolio_optimiser.provenance import Citation, ProvenanceStamp
from portfolio_optimiser.retrieval import TextSpan
from portfolio_optimiser.run import run_project
@ -133,6 +133,39 @@ def test_outbox_is_byte_deterministic(tmp_path) -> None:
assert oa.read_bytes() == ob.read_bytes()
def test_write_run_config_byte_deterministic_and_fields(tmp_path) -> None:
"""S4.2 T-4.2a: ``write_run_config`` writes ``{run_id}-runconfig.json`` carrying profile +
resolved model per BUILT role + params + token cap; two identical writes are byte-identical
(sort_keys/indent/LF, no wall-clock), and the payload leaks NO date/timestamp key (§4 pkt 3
excludes wall-clock for byte-determinism). Drop a field, break ``_dump`` determinism, or leak a
timestamp RED."""
import json
a = tmp_path / "a"
b = tmp_path / "b"
kwargs = dict(
profile="local",
resolved_models={"proposer": "qwen3:4b", "checker": "qwen3:4b"},
max_rounds=3,
max_tokens=100_000,
top_k=3,
)
pa = write_run_config(str(a), "run-1", **kwargs)
pb = write_run_config(str(b), "run-1", **kwargs)
assert pa.name == "run-1-runconfig.json"
assert pa.read_bytes() == pb.read_bytes() # byte-deterministic
text = pa.read_text(encoding="utf-8")
assert text.endswith("\n")
payload = json.loads(text)
assert payload["run_id"] == "run-1"
assert payload["profile"] == "local"
assert payload["resolved_models"] == {"proposer": "qwen3:4b", "checker": "qwen3:4b"}
assert payload["max_rounds"] == 3
assert payload["max_tokens"] == 100_000
assert payload["top_k"] == 3
assert not any(k in payload for k in ("date", "timestamp", "created", "ts"))
def test_outbox_registered_maf_free() -> None:
"""T-2.1d meta: outbox.py is registered in the MAF-free guard list, so test_okf_is_maf_free
actually scans it otherwise the MAF-free claim would be green-but-dead (never checked)."""