feat(major2): run_project owns the review sink, keys it per approach and writes it from a finally [skip-docs]

Ordre 20260904T173146Z-8102814273-from-portfolio-optimiser, steg 4 av 10.

run_project faar proposal_reviewer (keyword-only, None => byte-identisk kjoering), eier
sinken expert_reviews ved siden av parse_failures, og skriver
{run_id}-proposal-reviews.json fra den EKSISTERENDE genererings-finally-en.

Skriveregelen er IFF en reviewer ble gitt, OGSAA naar lista er tom (D4). Begge halvdeler
er baerende og trekker hver sin vei: write_debate_tools skriver ubetinget fordi DER er det
tomme tilfellet regresjonen; her maa en reviewer-LOES kjoering la utboksen staa byte-identisk
(to eksisterende tester pinner et EKSAKT fire-navns-listing), mens en reviewer som ble tilbudt
og aldri konsultert er et faktum artefaktet maa kunne SI.

Noeklingen: med mandat er hver post noeklet - kjoeringens eget forslag paa OWN_PROPOSAL_ID -
og None betyr kun EN ting: det fantes intet mandat. RunResult.expert_revisions bygges FRA
sinken, aldri ved siden av (kø-(p)).

RODT foer impl: 8 armer.

REGRESJON FANGET AV FULL SUITE OG RETTET HER: steg 3s _FeedbackAwareChatClient kopierte
_inner_get_response-kroppen og gjorde test_scripted_client_consolidation
::test_inner_get_response_collapsed_to_two_sites roed. Doblen overstyrer naa _next_reply i
stedet - basen har alt lagt DENNE kallets prompt i received_texts naar den ber om et svar,
saa sommen holder uten en tredje kopi av kroppen, og registeret i vakten trenger ingen ny
oppfoering. Aa registrere fila som foreign lineage var ikke mulig og heller ikke riktig:
Group B bruker ScriptedChatClient, som den vakten nekter for nettopp den lista.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-05 06:58:18 +02:00
commit 59f3fce20a
3 changed files with 410 additions and 27 deletions

View file

@ -248,6 +248,34 @@ def write_plan_review(
return path
def write_proposal_reviews(
outbox_dir: str,
run_id: str,
*,
payload: Mapping[str, Any],
) -> Path:
"""Write ``{run_id}-proposal-reviews.json`` — what a human answered about the proposals this
run put on the table (MAJOR-2) and return its path.
Takes an already-rendered plain mapping (``proposal_review.proposal_reviews_payload``) for the
reason ``write_plan_review`` does: the renderer lives in the module that owns the type, and
this layer stays MAF-free. The writer prepends ``run_id`` exactly as its sibling does, so the
on-disk object is ``{"reviews": [...], "run_id": ...}`` under ``_dump``'s sorted keys.
**The write rule is: iff a reviewer was given, INCLUDING when the list is empty** (D4).
Both halves are load-bearing and they pull in opposite directions. ``write_debate_tools``
writes unconditionally because THERE the empty case is the regression; here a reviewer-LESS
run must leave the outbox byte-identical, and two existing tests pin an exact four-name
listing on such a run. But a reviewer that was offered and never consulted no candidate ever
validated is a fact this artefact must be able to STATE, not something an operator has to
infer from a missing file. "Iff a reviewer was given" is the only rule that keeps both."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-proposal-reviews.json"
path.write_text(_dump({"run_id": run_id, **dict(payload)}), encoding="utf-8")
return path
def write_run_config(
config_dir: str,
run_id: str,