Commit graph

16 commits

Author SHA1 Message Date
a2acfc0f98 feat(portfolio): K10 — notification/notifier seam, opt-in webhook egress (parity row 23) [skip-docs]
S5.2-analog. New notify.py: Notifier protocol + console/file/webhook sinks. The
webhook (the one transport that leaves the machine) fires ONLY behind an explicit
per-run opt-in flag (--allow-webhook-egress), mirroring ingest-spec §8 (the flag
is a run argument, never a config field). Transport is injected — canned in the
suite (NULL socket), real transport behind one seam function default_webhook_transport;
an AST grep-guard proves no network path exists outside that seam. run.py (both
outcomes — a budget stop notifies too) and hitl.py (read-only preserved) share the
same opt-in-gated CLI seam, refusing a webhook-without-opt-in before any spend.
Payload shape is stack-local (no shared notification spec; divergence documented).

Two new load-bearing test files (18 tests): opt-in gate + payload structure + the
grep-guard + run/hitl emit wiring + run-level opt-in threading, each detach-proven
RED. 544 -> 562 green, full gate clean (ruff+format+mypy strict, 26 src files).
README sync (test count x2 + notify.py module note + load-bearing omtale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-24 20:16:56 +02:00
b9dd479865 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
2026-07-24 19:58:29 +02:00
08ffddbbb1 feat(portfolio): K8 — live-run drill, pre-call artifact capture (parity row 21) [skip-docs]
A future operator-gated live run (the M2-analog) is fully rigged and rehearsed
OFFLINE — without one model call, without a key (S4.2-analog, parity row 21;
buildable after K5 + K7). `--live-dry-run` builds everything a real run would
(contracts fail-fast §10 → compose §5 → SDK-client construction → preflight)
and captures the run-config + preflight artifacts, then STOPS before the first
model call. The stop IS the boundary: the loop is never entered, so nothing is
spent (strictly offline, no D6 gate).

- run.py --live-dry-run: requires --outbox + --run-id (the drill's artifacts are
  run_id-named), rejected fail-fast before any build. Writes a run_id-named PAIR
  to the outbox:
  * {run_id}-runconfig.json — comparison-protocol §4 pt 3: model-id per role the
    loop calls (proposer/checker, THROUGH resolve_model — the run's own path),
    profile, and every cap/parameter. Deliberately NO wall-clock date, so the
    bytes stay deterministic (the run's date is stamped at report time, §4 pt 3).
  * {run_id}-preflight.json — the captured preflight verdict (clear + refusals).
    The drill CAPTURES the preflight result rather than gating the build on it:
    exit 0 when clear (rig go-live-ready), non-zero when refused — artifacts
    captured and ZERO model calls in EITHER case.
- The client is constructed (the verified key-free SDK premise) but never called;
  a call-counting stand-in proves 0 calls. Bytes reuse the deterministic house
  JSON writer; run_s10.py/runs/ byte-untouched.

- test_dry_run_loadbearing.py: 7 tests. TWO seams detach-proven RED — the
  0-calls stop seam (neutralise the branch → falls to execute_run → the counting
  client fires → red) and the capture seam (drop the writes → outbox lacks the
  pair → red). Env monkeypatched so the preflight verdict is deterministic
  regardless of the operator's ambient shell.
- 514→521 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
  24 src files). README: test-count sync ×2 + run.py drill note + load-bearing
  mention. IKKE-scope (held): the actual live run (M2-analog, operator) and any
  change to preflight/outbox.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-24 06:54:58 +02:00
c08d92a358 feat(portfolio): K7 — SDK/API preflight, offline pre-spend boundary (parity row 20) [skip-docs]
Everything that CAN be validated WITHOUT a model call is validated BEFORE the
operator pays for one (S4.1-analog, SDK-native — Foundry-auth is MAF-specific,
not mirrored). The preflight IS the boundary: it never calls query(), never
validates a credential VALUE online, never touches the network. It returns a
list of structured, actionable Refusals; the CLI exits non-zero on any, so a
broken config stops cheaply instead of on the first billed call.

- preflight.py: run_preflight + `python -m …preflight --profile anthropic`.
  Four offline checks:
  * credential — ANTHROPIC_API_KEY set + not a placeholder form (the value is
    NEVER checked online, only presence, §1); the bundled CLI's own
    CLAUDE_CODE_OAUTH_TOKEN also satisfies it (run_s10 relies on it — refusing
    would be a false alarm).
  * model_map — the requested profile exists and every id it resolves to,
    THROUGH resolve_model (so the default fall-through is covered), is real,
    not a placeholder left in config.
  * sdk — claude_agent_sdk imports (a missing install is a structured refusal
    naming `uv sync`, never an ImportError out of the preflight), the run-path
    symbols exist, and the bundled Claude Code CLI is present on disk — located
    OFFLINE via the SDK package's own files, mirroring the SDK's _find_cli
    order (bundled first, then a claude on PATH). Verified against installed
    0.2.120 (bundled binary present; the STATE 0.2.110 note was stale).
  * termination — the §8 stop contract constructs with positive caps and the
    per-call USD belt is positive.
- tests/test_preflight.py: credential contract, placeholder detector, model_map
  incl. unknown-profile-without-raising, SDK + bundled-CLI offline probe (with a
  monkeypatched missing-SDK refusal), termination, run_preflight aggregation,
  CLI both paths, and the offline guards (no network import, no query() call —
  AST-based so prose stays green). THREE seams detach-proven RED: credential
  branch, placeholder model-id guard, no-network grep-guard.

478→514 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
24 src files), run_s10.py/runs/ byte-untouched. README test-count sync ×2 +
preflight.py module note + load-bearing mention. IKKE-scope (held): the actual
API call (ALDRI — the preflight IS the boundary) and Foundry/Azure auth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-24 01:34:18 +02:00
1600c188b4 feat(portfolio): K6 — pre-run cost simulation, priced what-if (parity row 18) [skip-docs]
Before ANY spend the operator sees a deterministic UPPER-BOUND USD estimate
for a (portfolio-)run — a what-if over the models in model_map.json (Claude
models) × effort levels (S3.6-analog, D-I pkt. 3 MUST-krav). No network, no
model call, no key: pure config arithmetic (bound by an import-purity test,
mirroring okf.py).

- contracts.py: ModelPriceContract (usd_per_mtok > 0 + REQUIRED source +
  source_date so a stale rate is visible, never silent, §1) + PricingContract
  (non-empty; no hardcoded fallback rate) + load_pricing/_bundled_pricing.
- data/pricing.example.json: per-Mtok rate per model id, each with source+date.
  Example rates are Anthropic's OUTPUT price (the higher rate) so the whole cap
  billed at that single rate can only overstate — the figure is marked ESTIMAT.
  Covers the model model_map configures, so the default path runs green.
- costsim.py: estimate_costs (n_projects × cap × effort_factor tokens at the
  per-Mtok rate; a model with no price fails fast "missing price for <id>",
  never a guess) + render_estimate + `python -m …costsim`. Effort factors are
  a coarse modeling weight (not prices) — max effort = full cap = the true
  upper bound. No price literal anywhere (grep-guard proves it).
- tests/test_costsim.py: schema fail-fast, missing-price fail-fast, scales with
  model × effort + reproducible, grep-guard, import purity, bundled-example +
  CLI offline smoke. Three seams detach-proven RED (effort factor, price guard,
  price literal).

462→478 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
23 src files), run_s10.py/runs/ byte-untouched. README test-count sync ×2 +
costsim.py module note. CLI run-total-cap wiring stays out of scope (planen
lists 4 files); the mechanism is complete and proven load-bearing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-23 23:01:01 +02:00
111b320b75 feat(portfolio): C3.5 — pre-call run-total USD budget belt (parity row 16/31) [skip-docs]
Add a pre-call USD belt on top of the post-charge token/round meter (§8),
so no future live run can loop past its run budget. Belt-and-braces above
the SDK's per-call max_budget_usd cap.

- budget.py: optional run-total `max_cost_usd` on BudgetMeter (fail-fast on
  non-positive, §10) + `guard_before_call(spent_usd)` raising the same
  structured stop event (BudgetKind widened with "cost_usd"; limit/observed
  → float). Reaching the cap exactly does not stop; crossing it does
  (mirrors the token cap).
- loop.py: `_guarded_complete` helper reads the client's accumulated
  total_cost_usd (0.0 for scripted clients) and guards BEFORE every
  client.complete; all three call sites routed through it — one detach point.
- sdk_client.py: total_cost_usd already exposed/accumulated — untouched.
- tests/test_budget.py: meter-level cap tests + load-bearing loop-wiring
  test (counting client; detach the guard → unguarded loop runs to the round
  cap → kind "rounds" not "cost_usd" → red).

457→462 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
22 src files), run_s10.py/runs/ byte-untouched. README test-count sync ×2 +
budget.py belt note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-23 22:45:37 +02:00
a926e4ad46 feat(portfolio): K5 — outbox persistence, run_id-named pairs (parity row 7) [skip-docs]
S2.1-analog: each completed run persists a run_id-named proposal/outcome pair
to the outbox — the system's OWN output layer (the role split §3 Step 7 governs
the inbox and wiki, not this). The outcome carries outcome type + figures, the
two §9 falsifiers mirrored verbatim from the RunResult, the provenance stamp,
and verdict_id — minted the SAME way inbox.py mints an expert verdict's id
(mint_verdict_id over the proposal's candidate features), so a later inbox
verdict about the same candidate joins by id (the K9 key assumption, pinned
here in test and reused there).

New outbox.py reuses artifacts' deterministic house JSON writer (sorted keys /
indent 2 / LF) — same input + same run_id => byte-identical files — and never
touches the S10 artifacts.py fasit formats. run.py grows optional
--outbox/--run-id; run_id is REQUIRED when the outbox is set (no wall-clock
default — a timestamp would break determinism) and is fail-fasted at the CLI
BEFORE any client/spend. A budget stop has no proposal, so it writes no pair.

New test_outbox_loadbearing.py (14): unit (pair, verdict_id join key on both
outcome types, percentiles-vs-reason, verbatim falsifiers, provenance,
round-trip, byte-determinism, run_id fail-fast) + wiring (entrance writes the
pair; no-outbox control; --outbox-without-run-id fails fast before spend).
Detach-proved: drop the persist_outbox call in execute_run -> wiring test RED.

443->457 green, golden byte-exact (13/13), run_s10.py/runs/ untouched, full
gate clean (ruff+format+mypy strict). README synced (count + module + seam).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-23 22:31:18 +02:00
9bae4fb563 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
2026-07-23 22:08:02 +02:00
f2c64da9ee feat(portfolio): K2 — sequential multi-project run (parity row 4)
New portfolio.py: run_portfolio drives N projects sequentially from a
schema-validated reference config, composing each project's §5 context
(merge inbox -> seed -> fold) and running the loop core UNCHANGED per
project, collecting one typed result per project IN CONFIG ORDER. This is
the run path MAF got in its Fase 1 and D7 never had — the prior entrances
(run.py, run_s10.py) drive a single bundle. PortfolioResult holds
per-project results tagged with the config project_id.

Re-entrancy (§3 Step 3): each project composes its OWN context inside the
loop, never a hoisted shared one, so nothing survives one project into the
next except the explicitly shared mutable state — the §8 budget meter, a
portfolio-wide cap. Failure policy is a STACK-LOCAL choice until D-D: the
default RAISES (today everything is thrown); K18 flips it to
collect-and-continue when the D-D wave model lands.

New config contract in contracts.py: ReferenceProjectContract (project_id +
required non-empty bundle_dir + optional inbox_dir) + ReferenceProjectsContract,
loaded fail-fast by load_reference_projects (§10) — a project without a
bundle path is refused before any run. New data/reference_projects.json
example (shape-validated, never executed by the suite). New repo-local
mini-bundle fixture under tests/data/ (a distinct second project, VFD-retrofit
— ALDRI in shared/).

Two detach proofs delivered: drop the bundle_dir Field requirement -> a run
starts on the invalid config and only crashes mid-run -> the fail-fast test
goes red; hoist the per-project composition out of the loop -> project 2 runs
on project 1's context and the VFD marker never reaches its prompt -> the
re-entrancy test goes red. 11 new tests (test_portfolio.py 5 +
test_contracts.py TestReferenceProjects 6). 426 -> 437 tests, golden
byte-exact, full gate clean (ruff + format + mypy strict). README synced
(test count + a Run layer module block).

[skip-docs] — README documents the new module; CLAUDE.md holds invariants
(rules/commands) only, and K2 adds no new invariant, command, or convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-23 21:39:39 +02:00
698e8f21dd feat(ledger): K1 — savings ledger + goal contract (parity rows 2-3)
New ledger.py: typed SavingsLedger; realize is fail-closed on an APPROVED
FeedbackContract + a named expert + an explicit timestamp (the §6 determinism
rule — no wall-clock default). The sum key is DIMENSION-FREE (the dimension
label is annotation only and never participates in the mint), so the same
realized saving surfaced via two dimensions lands in one first-write-wins
slot and is never double-counted. Deterministic JSON persistence
(sort_keys, indent 2, LF, trailing newline), schema-validated on load.

New goals.py: GoalContract (absolute target, hard/soft, fail-fast §10).
A hard goal reached raises GoalReached, a structured stop event carrying
target + observed — never a silent stop; soft flags without stopping.
The percent-goal baseline is D-E-gated: the field is reserved and
construction refuses with an explicit NotImplementedError.

Semantics are marked STACK-LOCAL in the docstrings — mirrored from the MAF
plan's capability description, never from MAF code; format shareability
stays a proposed decision point in the brief.

Two detach proofs delivered (decision gate removed -> red; dimension into
the key mint -> the double-counting test red). 400 -> 426 tests; README
synced (test count + a Value layer module block).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 04:00:14 +02:00
d4efdd9a35 feat(simulation): K4 — closed-loop two-run simulation binds §11 'Closed loop' (closes R-1)
Scripted two-run driver over the run.py composition: run A -> persona verdict
(shared skill artifact) -> §6 promotion gate -> run B on a fresh store. The
marker crosses runs via the promoted wiki layer ONLY - run B reads no inbox,
a rejected verdict is refused fail-closed and its marker never crosses.
Two detach proofs delivered (promotion step removed -> red; verdict exclusion
in bundle_context removed -> red via the '## verdict' section anchor).

Known-limitation note (C-F5, deferred to C3.2): a persona verdict over the
bundle seed's own candidate mints the seed's §4.2 id and is silently shadowed
by first-write-wins; the test has run A propose a distinct candidate.

395 -> 400 tests; README synced (test count + the S10 section now reflects
that D7 has its own scripted closed-loop proof).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:47:05 +02:00
3587854074 feat(run): C2.0 — shippable step-7 run entrance + K2.9 seam bindings (closes C-N2, R-10, K2.9)
- run.py: compose_run_context (§5: merge inbox -> seed -> fold, read-only on
  the inbox) + execute_run (§8 meter, artifacts persisted on BOTH outcomes,
  structured exit 3 on budget stop) + thin CLI (python -m ..run). The model
  client is injected; only default_client_factory constructs the SDK client
  (wired, never executed by the suite). The navigated docs dir comes from the
  validated startup contract (resolves review OBS-2 on the shippable path;
  run_s10.py stays byte-frozen fasit -> won't-fix there).
- test_run_entrance_loadbearing.py: inbox verdict reaches the composed
  context (detach-proven: merge dropped -> red), empty/missing-inbox
  controls, read-only inbox byte-proof, R-10 budget-stop binding via the NEW
  entrance (detach-proven: stop persistence dropped -> red), happy path
  through the CLI with the inbox signal surviving the chain, SDK-wiring test.
- test_ingest_adoption.py (K2.9): the two library guarantees the consumer
  relies on, bound through the seam — empty CSV -> typed SourceError with NO
  partial bundle on disk; non-SELECT SQL -> SourceError 'returned no columns'
  (behavior verified empirically against pin dae0bd1a before binding).
- README: inbox section now points at the shippable entrance; run.py added
  to the run layer; stale test count 265 -> 395.

386 -> 395 tests, full gate green (pytest, ruff check+format, mypy strict);
goldens unchanged; runs/s10 and run_s10.py untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:28:31 +02:00
42f320bed1 chore(release): MIT license + CONTRIBUTING/SECURITY/CODE_OF_CONDUCT + README badges (S12) 2026-07-05 10:32:24 +02:00
0c42cebf90 docs(ingest): I7 — programavslutning, D7-docs avgrenset til CSV + SQL
Docs-only program-avslutning for ingest-laget (D7-stacken). Bygget fra
commons-spec + eget repo alene — MAF-koden kun eksistens-bekreftet (I6-gate),
aldri lest/reverse-engineert.

- README: ingest-seksjon (file/CSV + sql/sqlite RO) som post-S10-tillegg;
  testtall 187 -> 265; HTTP/MCP kun peker (D7 har ingen HTTP-konnektor).
- docs/extending.md (ny): hvordan ingest-laget virker + hvordan legge til en
  kildetype; HTTP/MCP som ærlig extension-point-peker (spec §4, MAF I6-demo,
  create_sdk_mcp_server som ubygd vehikkel — brukt ingen steder i src/).
- docs/2026-07-04-I7-brief.md + -statusrapport.md: I7-brief + D7-lokal
  statusrapport m/ verifiseringslogg. Kryss-stack-kriteriet peker til
  MAF-programrapporten.

Ærlighetsregelen (method-spec §1): ingen artefakt påstår HTTP/MCP-støtte i D7
eller live-kilde-integrasjon. Grep-sjekk mot overpåstand ren.

Verifisering: 265 passed uten nøkkel/nettverk · ruff/mypy rene.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7nuUMUyWB9viiBjz8Bo7R
2026-07-04 20:08:06 +02:00
b0b87570db docs(readme): bring README up to post-S10 state, structured after the MAF sibling's
Status, seam-by-seam architecture, load-bearing test map, the executed S10
live run with committed artifacts, and the offline-suite invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 14:49:14 +02:00
b2f2842c86 feat: initial commit — repo bootstrap (D7 sibling on Claude Agent SDK)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
2026-07-03 06:00:09 +02:00