feat(portfolio): K3 — portfolio learning loop (shared verdict store, parity row 5) [skip-docs]

A single VerdictStore threaded through run_portfolio: a verdict available when
project k composes survives into project k+1's fold (method-spec §5 cross-project
threading). The optional verdict_dir is the portfolio-level expert inbox, read
before each fold (role split §3 Step 7 — the portfolio never writes a run's own
verdict back; §1/§6 — no self-contamination, only expert/seed verdicts cross).
compose_run_context gains an optional passed-in store (None = fresh; every
existing caller composes exactly as before).

Load-bearing (tests/test_portfolio_learning_loadbearing.py), 2 detach proofs +
control + §4.2 idempotency:
- cross-project threading: project 1's bundle seed survives into project 2's
  prompt via the shared store; detach (compose ignores the passed-in store,
  always fresh) -> red.
- portfolio inbox fold: a verdict_dir marker reaches the project's fold; detach
  (drop the run_portfolio merge) -> red; control (no verdict_dir) -> marker absent.
- double-merge idempotency: a verdict merged before every project folds exactly
  once (first-write-wins on id).

437->442 green, golden byte-exact, full gate clean (ruff + format + mypy strict).
run_s10.py and runs/ byte-untouched. README synced (test count, portfolio block,
load-bearing list). K2 re-entrancy test stays green — the shared store threads
verdict fold lines only, never bundle context markers.

[skip-docs]: no invariant changed (CLAUDE.md untouched); the run_portfolio and
compose_run_context docstrings + README carry the doc need.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
This commit is contained in:
Kjell Tore Guttormsen 2026-07-23 22:08:02 +02:00
commit 9bae4fb563
4 changed files with 275 additions and 17 deletions

View file

@ -61,7 +61,7 @@ class ComposedRunContext:
def compose_run_context(
bundle_dir: Path, inbox_dir: Path | None = None, *, k: int
bundle_dir: Path, inbox_dir: Path | None = None, *, k: int, store: VerdictStore | None = None
) -> ComposedRunContext:
"""Compose the run context per §5: merge inbox → seed → fold — read-only.
@ -69,10 +69,17 @@ def compose_run_context(
fast ahead of any spend (§9). A missing/empty ``inbox_dir`` (or ``None``)
leaves the composition identical to the no-inbox base. Nothing is ever
written the system reads the inbox, the expert writes it (§3 Step 7).
A passed-in ``store`` is used AS-IS (its existing verdicts survive the
merge, first-write-wins) the §5 cross-project threading a portfolio pass
(K3) relies on: a verdict available at project k reaches project k+1's
fold. ``None`` (the single-run default) builds a fresh store, so every
existing caller composes exactly as before.
"""
citations = build_citations(navigate_bundle(bundle_dir))
ir_projection = load_validator_input(bundle_dir)
store = VerdictStore()
if store is None:
store = VerdictStore()
inbox_merged = merge_inbox_into_store(store, inbox_dir) if inbox_dir is not None else 0
seeded = seed_store_from_bundle(store, bundle_dir)
context = fold_experience(