feat(portfolio): K9 — HITL verdict routing + pending tracking (parity row 22) [skip-docs]
The operator's view of the long feedback loop (S5.1-analog, parity row 22;
buildable after K5): which proposals still AWAIT an expert verdict, and who
should judge each — a pure file-based id-join across the three layers hitl
READS and NEVER writes (role split §3 Step 7: the expert writes the inbox, the
system reads it; notification is K10's job, never this).
- hitl.py:
* pending_proposals — the id-join. An outbox proposal (K5) is pending unless
its persisted verdict_id (read verbatim from {run_id}-outcome.json, minted
the SAME way the inbox mints a verdict id — the K5 assumption) is in the
settled set. settled = §4.2-valid inbox verdicts (THROUGH load_inbox, so a
skipped/unknown decision never settles anything) ∪ promoted verdicts (§6,
optional bundle_dirs, so the core join is exactly outbox↔inbox).
* RoutingContract — nøkkel→ekspert, schema-validated fail-fast (§10): non-empty
table, non-empty keys/expert ids, optional default_expert. route_pending maps
a proposal's measure (a config-string key NOW; K13 formalizes the dimension
catalog) to an expert; an unmatched measure → default, else UNROUTED.
* CLI python -m …hitl pending|route — pending is a pure report (exit 0); route
loads the routing config fail-fast (a malformed/missing config exits non-zero
WITHOUT touching any layer). Neither subcommand writes anything.
- test_hitl_loadbearing.py: 23 tests. TWO seams detach-proven RED — the id-join
seam (drop the `not in settled` filter → a judged proposal is STILL listed →
red) and the read-only seam (any read path that writes a byte → the before/
after outbox+inbox snapshot diverges → red). Covers: undecided → pending,
inbox/promoted verdict settles, exact-id join (no coincidental match), skipped
decision does not settle, deterministic order, malformed routing fail-fast,
measure→expert / default / UNROUTED, and the CLI subcommands.
- 521→544 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
25 src files). README: test-count sync ×2 + hitl module note + load-bearing
mention. IKKE-scope (held): notification (K10), web-UI, writing the inbox.
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:
parent
08ffddbbb1
commit
b9dd479865
3 changed files with 676 additions and 3 deletions
17
README.md
17
README.md
|
|
@ -13,7 +13,7 @@ human-in-the-loop, and the system learns from the verdicts.
|
|||
> **Status:** the D7 build (S5–S10) is complete, and the deterministic **ingest layer**
|
||||
> (CSV and SQL source types) has since been added in front of the loop. The deterministic
|
||||
> backbone, the agentic loop, the learning loop, and the ingest connectors are wired seam by
|
||||
> seam, each proven by load-bearing tests (521 tests, all running offline without an API
|
||||
> seam, each proven by load-bearing tests (544 tests, all running offline without an API
|
||||
> key). The programme's single budgeted **live model run has been executed and validated** —
|
||||
> its artifacts are committed under [`runs/s10/`](runs/s10/) (see below).
|
||||
|
||||
|
|
@ -133,6 +133,14 @@ description, never from its code)
|
|||
bundled CLI is present on disk, and the §8 stop/budget contract is set. It never calls the
|
||||
API — a green preflight implies no more than that (§1). Each deficiency is a structured,
|
||||
actionable refusal. `uv run python -m portfolio_optimiser_claude.preflight`.
|
||||
- `hitl.py` — the operator's view of the long feedback loop (**offline, read-only**): which
|
||||
proposals still *await* an expert verdict, and who should judge each. A pure file-based
|
||||
id-join across the three layers it READS and never writes — the outbox (K5, the persisted
|
||||
`verdict_id` join key), the inbox (a §4.2-valid verdict settles a proposal), and optionally a
|
||||
bundle's promoted verdicts (§6). Routing maps a proposal's `measure` (a config-string key
|
||||
now; K13 formalizes the dimension catalog) to an expert via a schema-validated table
|
||||
(`nøkkel→ekspert`, fail-fast) with an optional default; an unmatched measure is UNROUTED.
|
||||
`uv run python -m portfolio_optimiser_claude.hitl pending|route`.
|
||||
|
||||
### Load-bearing tests (§11)
|
||||
|
||||
|
|
@ -153,7 +161,10 @@ entrance path, with a no-outbox control, and the outcome carries the inbox join
|
|||
any spend, and the preflight carries no network path of its own),
|
||||
`test_dry_run_loadbearing.py` (the live-run drill captures its `runconfig` + `preflight`
|
||||
artifacts and stops before the first model call — a call-counting client proves zero calls,
|
||||
red the moment the stop seam is detached), and
|
||||
red the moment the stop seam is detached),
|
||||
`test_hitl_loadbearing.py` (a proposal with no verdict is listed pending and disappears once
|
||||
an inbox or promoted verdict shares its id — red the moment the id-join filter is detached —
|
||||
and hitl never writes any layer, proven by a before/after byte snapshot), and
|
||||
`test_sdk_isolation.py` (local config cannot capture the checker).
|
||||
|
||||
## The ingest layer — CSV and SQL, in front of the loop
|
||||
|
|
@ -216,7 +227,7 @@ Python ≥3.10 · [`claude-agent-sdk`](https://pypi.org/project/claude-agent-sdk
|
|||
|
||||
```bash
|
||||
uv sync # install dependencies
|
||||
uv run pytest # 521 tests — run without any API key and without network
|
||||
uv run pytest # 544 tests — run without any API key and without network
|
||||
uv run ruff check . && uv run ruff format --check .
|
||||
uv run mypy src # strict
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue