Commit graph

39 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
613b00f882 fix(okf): tolerate embedded-NUL cross-link target — skip, never raise
navigate_bundle's out-of-bundle filter drops every '/'- and '\'-bearing
target before resolution, but a NUL-byte target carries no path separator
and slipped through to (bundle_dir / target).resolve(), which raises
ValueError: embedded null character — propagating instead of being skipped.
method-spec §72 requires a broken cross-link to be tolerated (skipped,
never raised). Wrap resolve/is_file in `except ValueError: continue`;
parse_concept_file stays outside the guard so malformed concept files
still raise.

Load-bearing test drives the first dangerous target THROUGH the filter
into resolution; detach-proved RED (ValueError propagates) when the guard
is removed. Closes the nullbyte item reported OPEN in OKF trinn E.

442→443 green, golden byte-exact, full gate clean (ruff+format+mypy strict).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
2026-07-23 22:20:50 +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
a7e8ffecb8 chore(deps): re-pin llm-ingestion-okf to v0.3.1 + migrate tests to stable error codes
Pin dae0bd1a -> v0.3.1 (=692f2df) on the public Forgejo mirror; uv.lock pins
the exact commit behind the tag.

- Drop the mypy override: the library ships py.typed from v0.2.0, so strict
  mode now follows its real types instead of follow_untyped_imports.
- Migrate 8 library-error assertions from pytest.raises(match=...) to
  exc.value.code — message text is explicitly unstable from v0.3.0, the
  codes are the stability contract.
- Fix a real breakage the bump surfaced: IngestResult gained a required
  `stamp` field (d3a3bcc), which the delegation fake did not construct.
- The read-only SQL test loses resolution under the code contract
  (`sql_failed` is generic), so it now proves read-onlyness by effect —
  the write never lands — instead of by message wording.
- Correct the guard plan: G1's persist-gate anchor (ingest.py:372-387) died
  with the 2026-07-16 adoption. Door A is ungated by the library's own
  README, so gating stays our responsibility at the call site.

Verified: 426 tests green, golden output byte-exact unchanged, full gate
clean (ruff + format + mypy strict).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmNAgbRXUgvoSKxVK4Bevv
2026-07-20 07:22:09 +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
5732d13369 feat(ingest): adopt llm-ingestion-okf as Door A implementation (first consumer)
Replace the local 391-line ingest implementation with a thin adapter over
the shared llm-ingestion-okf library (git-pinned dae0bd1a via Forgejo,
tool.uv.sources). The materialize() signature is preserved; error types are
now the library's typed hierarchy rooted in IngestError, re-exported from
the consumer seam.

- tests/test_ingest_adoption.py: new load-bearing seam tests (delegation,
  offline invariant — allow_network is never passed, error contract),
  detach-proven red twice.
- Golden suites (file + sql) pass UNCHANGED — byte-exact behaviour proven
  against the repo-local fixtures.
- 6 test files migrated to the library error hierarchy; escaping/typed-cell
  unit tests dropped (byte-bound by the ingest-edge.md golden, unit-owned by
  the library's own 189-test suite). Provenance stamp now asserted
  independently from the §5 rule.
- mypy override follow_untyped_imports for llm_ingestion_okf (no py.typed
  upstream yet — reported as a finding).

Suite: 386 passed; ruff + format + mypy --strict clean; shared/, examples/,
runs/s10/ and run_s10.py byte-untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:46:51 +02:00
80a2fa1a77 feat(inbox): C2.5 — inbox hardening + SDK version guard (closes C-F7, C-N3, R-6)
- File-layer decision vocabulary (§4.2 set) with SKIP semantics — an unknown
  decision never reaches the store (C-F7, the review's run proof is the fixture)
- Fail-fast caps (max_files / max_rationale_chars) via InboxLimitError raised
  OUTSIDE the tolerant try — a cap breach is never swallowed as a skip
- R-6 id grammar (mirrors ingest _ID_RE) as a pydantic pattern on
  VerdictDocument.id AND re-checked in write_verdict, since model_copy(update=)
  bypasses model validation — traversal ids can no longer write outside the inbox
- promotion._filename_token: any sanitised id maps to a content hash — 'e/vil'
  can no longer clobber the distinct id 'evil' (restarbeid-funn 2)
- SDK pinned >=0.2.111,<0.3 + version guard test naming the sdk_client.py
  attribute premises; resolved 0.2.120, all premises re-verified against it
- sdk_client read loop bound offline with REAL SDK message types (R-4/R-5):
  text aggregation, error fail-paths, usage/cost extraction, _total_tokens
  fail-closed, non-positive budget guard
- test_sdk_isolation comment no longer claims the --system-prompt ""
  serialization the test body does not bind (honesty rule §1)

Guard-G2 assessment (guard-plan §4): the allowlist + caps + id grammar landed
here are G2's necessary part; an optional scan_output depth pass over
rationale (still a verbatim prose channel into the fold prompt, R-9) remains
relevant as a later additive session — the trigger picture is unchanged.

4 detach proofs red → restored green. Full gate: 389 passed (365→389),
ruff+format+mypy clean; golden + shared/ + runs/s10/ byte-untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:26:41 +02:00
e7ce6b0a31 fix(validator): C2.6 — finiteness hardening, Infinity can no longer vacuously clear the gate (closes R-2)
IR schema now refuses non-finite numbers (allow_inf_nan=False on quantity/
unit_cost/claimed_saving_nok) and non-finite or negative assumption-band
endpoints; json.loads accepts the bare Infinity literal, so the bundle seam
is tested directly. ModelMapContract rejects empty-string model ids
(min_length=1). check_turn_safety_net documented as a deliberately
unreachable belt under the range-bound debate loop.

18 new tests; detach-proven (re-allow inf/nan -> 5 red, drop min_length ->
2 red). Full gate: 365 passed, ruff/format/mypy clean; golden untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:10:12 +02:00
d8f4e13bfa test(spec): C1.1 — method-spec integrity guard (§11 'Spec integrity', closes C-N1)
Presence-only guard over shared/method-spec.md, mirroring the ingest-spec
guard: file presence, structure markers (##1–##12, Step 1–8, MUST),
framework-neutrality (same forbidden-toolkit list), and §12 coverage of
all 31 consumed contract fields — scoped to the §12 block in backticked
form so a pure table-row removal detaches (precision lesson from the
ingest guard's detach spot-check). Red-proofs (a: missing file, b:
injected toolkit name, c: removed §12 field) run parametrized against a
mutated copy in tmp_path, never against shared/.

Full gate green: 347 passed, ruff + format + mypy clean; no src changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 19:59:01 +02:00
d746891f06 docs(review): full-paritet review + sesjonsplanverk + beslutningsbrief (Fable 5 + 6 Opus-agenter)
Operatør-mandat 2026-07-16: full MAF-paritet (D-G pkt. 2). Review-økt, ingen kildekode.

- review-paritet.md: C-serien re-verifisert (alle åpne), 10 nye R-funn. Kjørt-bevist av
  orkestrator: R-2 (Infinity → validates=True, NaN avvist), R-3 (tom CSV krasjer ingest →
  delvis bundle). R-1 = §11-raden «Closed loop» ubundet. 4/4 detach røde i throwaway-kopi;
  SDK-premisser holder mot 0.2.110→0.2.120; model-id gyldig+billigst; docs §1-ærlige.
- sesjonsplan-paritet.md: ERSTATTER 2026-07-10-planen. 35-raders paritetsmatrise (opphevet
  «Overføres IKKE»-lista, re-klassifisert rad for rad). 16 ugatede sesjoner + gatede med
  eksakte utløsere + avhengighetsgraf + binær leveringsklar-sjekkliste.
- beslutningsbrief.md: operatør-køen i klarspråk (D-A+C-P1, amendment, toolkit-§8, D-B,
  + nytt foreslått delt punkt).
- 2026-07-10-sesjonsplan-d7.md: superseded-merket med peker.

Steg 0 verifisert: commons-amendment IKKE landet, okf-toolkit-repo finnes IKKE.
STATE.md er local-only (gitignored) — ikke committet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:36:07 +02:00
b46f32c682 docs(security): llm-ingestion-guard adopsjonsplan — trust-grense-kartlegging + status planned
Fulgte guard-repoets inclusion/adoption-brief. Kartla repoets ingest-flater mot
briefens §7-checklist: ingest file/sql = førstepart (verbatim render, 0 modellkall,
0 egress-call-sites i src); http = dormant untrusted (avvist fail-fast, extending.md);
inbox = kjørt-bevist injeksjonsflate men førstepart + uwiret (C-F7/C-N2). Status: planned.
Ingen wiring/dep lagt til — kun plan + maskinlesbar markørlinje i STATE.md (local-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 07:40:24 +02:00
3f106fcfd1 docs(review): kryssmodell-review + sesjonsplan-synk mot delt beslutnings-kø D-A–D-E
Fable 5-review av D7-søskenet, speil av MAF-reviewen 2026-07-09: baseline
re-verifisert (265/ruff/mypy), F2/F3/F5/F7-analoger kjørt-bevist, F1/F11
avkreftet i MAF-form, egne funn (method-spec-vakt mangler, Steg-7-inngang,
SDK-versjonsvakt, D-C↔spec-kollisjon), 11/11 detach-eksperimenter røde i
throwaway-kopi. Sesjonsplan: byggbare-nå C1.1/C2.5/C2.0 + D-A-gatede speil
C2.7/C2.8/C3.2/C4.0.
2026-07-10 07:29:35 +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
32640deffc feat(ingest): I5 — SQL D7-speil, bygget fra commons-spec alene
Speiler MAF I4 fra shared/ingest-spec.md alene: manifest → SQL-konnektor →
materialisert OKF-bundle, byte-identisk med den delte golden-fasiten. Gaten
I4→I5 verifisert løst mot ground truth (MAF-commits d7e5f2f/4f45fe6/1b7612b)
før arbeidet startet. Spec byte-identisk delt, ingen spec-endring (I4).

- ingest.py: SqlSource (type: sql, id, connection_ref); ManifestContract.source
  er nå diskriminert union FileSource | SqlSource på type (http/ukjent tag →
  fail-fast). _render_sql_cell (§5 typed: NULL→"", int→decimal, float→korteste
  round-trip, str→verbatim m/ delt _escape_cell, annet→fail — aldri stille
  coercion). _resolve_connection_ref (env-oppslag §4/§8, usatt → fail-fast).
  _read_sql (read-only sqlite file:?mode=ro, ett SELECT, max_rows §8).
  _read_extraction dispatcher på source.type; materialisering/index/replacement
  uendret fra I3.
- examples/ingest-golden-sql/: repo-lokal golden (byte-frossen kopi av I4s fasit).
- Speiltester (I4s load-bearing-sett, gjennom SQL-konnektoren, detach-bevist røde):
  sql-golden byte-fasit + mutasjonskontroller · typed-cell/NULL (NY §I5-søm) ·
  provenance/navigability/verdict-reservasjon/re-ingest-safety · SqlSource-kontrakt/
  typed-rendering/connection_ref/max_rows/read-only · spec-integritet utvidet med
  connection_ref. Stale type:"sql"-avvisningscase erstattet (sql er gyldig post-I5).
- docs/2026-07-04-I5-brief.md: brief + premiss-verifisering.

Suite 265 passed uten nøkkel/nettverk (239 + 26 nye) · ruff + mypy --strict rene.

[skip-docs] README + docs/extending.md er bevisst utsatt til I7 per sesjonsplan
(programmet batcher ingest-doc der, avgrenset til det D7 faktisk har — CSV + SQL nå,
HTTP/MCP kun pekere). Dokumentert i docs/2026-07-04-I5-brief.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MM6BWb1hWmJZuXFZ7rjxT
2026-07-04 07:18:59 +02:00
e03bd79876 feat(ingest): I3 — D7-speil av ingest (filkatalog/CSV), bygget fra commons-spec alene
Speiler MAF I2 fra shared/ingest-spec.md alene: manifest → CSV-konnektor →
materialisert OKF-bundle, byte-identisk med den delte golden-fasiten.

- ingest.py: ManifestContract (pydantic, fail-fast, file-kilde, verdict-reservasjon
  §3, id-grammatikk, max_rows), CSV-konnektor (boundary-checked fail-closed),
  materialisering (§5-frontmatter eksakt rekkefølge, markdown-tabell m/ escaping,
  LF-only, SHA-256 manifest-stamp), index-generering (§6), replacement §3/§5.
- okf.py: _parse_index_entry — tolererer frontmatterløs index (method-spec §3:
  index rendres via body = summary, ikke som typet concept-fil). Golden var
  spec-konform; D7-okf var strengere enn standarden. Scoped: non-index concept-
  filer krever fortsatt type (honesty-test).
- examples/ingest-golden-file/: repo-lokal golden (byte-frossen kopi av I2s fasit).
- Speiltester (I2s load-bearing-sett, alle detach-bevist røde): golden byte-fasit
  + mutasjonskontroller · provenance/navigability/verdict-reservasjon/re-ingest-safety
  · kontrakt fail-fast/max_rows/boundary/kollisjon · spec-integritet §11.
- docs/2026-07-04-I3-brief.md: brief + de to operatør-avgjorte beslutningene.

Suite 239 passed uten nøkkel/nettverk (189 + 50 nye) · ruff + mypy --strict rene.

[skip-docs] README + docs/extending.md er bevisst utsatt til I7 per sesjonsplan
(programmet batcher ingest-doc der, avgrenset til det D7 faktisk har — CSV nå,
SQL/HTTP senere). Endringen er dokumentert i docs/2026-07-04-I3-brief.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MM6BWb1hWmJZuXFZ7rjxT
2026-07-04 06:12:43 +02:00
dc2345e134 Merge commit '6c73d1f7c1' 2026-07-03 22:33:47 +02:00
6c73d1f7c1 Squashed 'shared/' changes from db86e15..7aa53fc
7aa53fc docs(spec): ingest-spec.md — normative ingest layer contract (I1)

git-subtree-dir: shared
git-subtree-split: 7aa53fcf6823bda8c492b53762e3fbfd39ea3058
2026-07-03 22:33:47 +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
0f8af04e88 feat(run): S10 fullført — live-kjøring validert på 1. forsøk, artefakter persistert
Re-kjøring etter post-mortem-fiksene: exit 0, validator=validated,
checker=approve, 2/12 runder, 36 791/150 000 tokens, kost $0.127514
(Haiku 4.5). Artefakter (proposal, provenance med §9-citations,
run_result, usage) committet som bevis på programmets ene live-kjøring
(D6) og som input til S11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 10:57:28 +02:00
7637c6feae fix(run): S10 del 2 — post-mortem: stopp-artefakt, SDK-isolasjon, raw-JSON-direktiv
Transkript-analyse av den stoppede live-kjøringen (10 kall, 162 250 tokens,
$0.331506): konfig-lekkasjen (setting_sources=None laster ALLE filsystem-
settings) injiserte operatørens Claude-konfig i hvert kall — ~10-15k uncachede
tokens, en påtvunget bekreftelses-preamble som gjorde ren-JSON-svar umulige,
og en checker kapret av lekkede instrukser (debatt konvergerte aldri).

- persist_stop_artifacts: stopp-event verbatim + usage/kost persisteres ALLTID
  ved BudgetExceeded (delt usage-shape med fullført-run-stien)
- build_call_options: setting_sources=[] (SDK isolation mode, verifisert mot
  installert 0.2.110-kilde), system_prompt=None → tom system-prompt; detach-
  bevis via monkeypatchet query
- _generation_prompt: krever ONLY the raw JSON object (fence-innpakning ga
  4 fullpris parse-retries)

187/187 uten nøkkel · ruff + mypy --strict rene · tre detach-bevis RØDE → grønn

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 10:49:51 +02:00
0238507df4 feat(run): S10 del 1 — run-lag: §9-citations, artefakt-persistens, SDK-klient
TDD offline (RØD bekreftet før implementasjon): resolve_model (rolle->modell-id,
ukjent profil feiler fail-fast), build_citations (eksakte char-spans, verdict-
ekskludering, uncitable kontekst -> raise FØR spend), persist_run_artifacts
(deterministiske bytes; validator/checker-avgjørelser speilet VERBATIM fra
RunResult — §9 non-konflatering). Run-path-only, aldri importert av tester:
SdkModelClient (claude-agent-sdk 0.2.110 verifisert mot installert pakke;
max_turns=1, tools=[], max_budget_usd per kall; manglende usage -> None så
§8-meteret feiler lukket) + run_s10 (kontrakter FØR klient §10, BudgetExceeded
som strukturert stopp). 178/178 uten nøkkel; ruff+mypy --strict rene; fire
detach-bevis røde -> revertert grønne. Live-kjøringen gjenstår (credential-gated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 07:51:05 +02:00
22bfc80dda feat(learning): S9 — D7 læringssløyfe: verdict-inbox, fail-closed promoteringsgate, artefakt-sourced persona
- inbox.py (§4.2+§5): VerdictDocument med verbatim-id-regel; write_verdict
  authoring-primitiv (deterministisk JSON); load_inbox tolerant (skip, aldri
  raise; sortert på filnavn); merge_inbox_into_store first-write-wins,
  idempotent, skriver aldri (rolle-splitt §3 steg 7)
- promotion.py (§6): promote fail-closed mot {approved,
  approved_with_adjustment}; eksplisitt påkrevd timestamp; minimal frontmatter
  (rationale → description, aldri strukturerte læringsfelt); path-safe token
  med content-hash-fallback; idempotent index-lenking med fast nøytral label
- persona.py (§4.3): load_persona_example fail-fast (run-path-vokabular,
  marker ⊆ rationale); drop_persona_verdict artefakt-sourced ved kalltid mot
  delt shared/-artefakt
- experience.py (kirurgisk): seeding leser verdict_id VERBATIM fra frontmatter
  — re-minting ville kollidert distinkte promoterte kandidater
- 43 nye load-bearing tester (step7/step8/persona), 164/164 uten API-nøkkel;
  to-runs-bevis med fersk store + tom-inbox-kontroll; fire detach-bevis kjørt
  røde og revertert grønne

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 07:36:15 +02:00
9a4caeb419 feat(loop): S8 — D7 agentic loop: budget meter, maker-checker gate, informed refinement
Spec §3 steps 2–5 + §8, TDD-ed offline (scripted, honesty-marked stand-in):
- budget.py: BudgetMeter over TerminationContract — provider-reported usage
  only (missing usage fails closed), structured BudgetExceeded stop event.
- loop.py: ModelClient protocol; blind parse-retry generation (never silent
  repair); round-capped debate with turn safety net and mandated VERDICT
  line; opt-in-reject checker gate (explicit REJECT overrides a validated
  outcome, validator rejection stands); most-recent-reason-verbatim informed
  refinement under max_attempts; validator_decision stamped BEFORE override,
  checker_decision as its own result field (§9, never conflated).
- 45 new tests (121 total, no API key); four detach proofs run RED and
  reverted green: checker override, informed block, surfaced checker output,
  stamp-before-override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 07:21:02 +02:00
46f2f521f7 feat(context): S7 — D7 context seam: OKF navigation + gated ExpeL fold
Step 1 of the loop, built from method-spec §3 alone:
- okf.py (pure stdlib): frontmatter parse, deterministic/tolerant/boundary-checked
  index navigation, bundle_context rendering with type:verdict exclusion
- experience.py: CandidateFeatures from the IR projection, §4.2 id minting,
  structural ranking (0.60·Jaccard + 0.25·type + 0.15·magnitude bucket),
  first-write-wins store, bundle seeding with the realization marker,
  fold-before-generation (empty retrieval → base unchanged)

Load-bearing (§11), each proved RED on detach: verdict-layer exclusion, fold,
seed learning fields, agent-toolkit import guard. 76/76 without an API key;
ruff + mypy --strict clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 06:48:13 +02:00
1e1b7e4506 feat(validator): S6 — deterministic backbone: typed IR, golden-frozen validator, provenance stamp
TDD from method-spec alone (§3 Step 4, §7, §9), golden.json as the only
ground truth: ir.py (construction invariants, fail-fast bundle loader),
validator.py (closed-form feasibility bound 0.30·Σ + Monte Carlo seed
20260624/512 samples/inclusive quantiles — reproduces every frozen golden
field; Rejection as a distinct unconsumable type), provenance.py (stamp
mirroring ONLY the deterministic validator). Mutation controls + seed-detach
proof (§11); 45/45 green without an API key; ruff + mypy --strict clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
2026-07-03 06:27:40 +02:00
4efb72943c feat(scaffold): S5 — D7 sibling scaffold: SDK dep, fail-fast startup contracts, CLAUDE.md
Claude Agent SDK verified against official docs + PyPI 2026-07-03 (0.2.110, CLI
bundled, offline import without API key). Contracts mirror method-spec §10/§4.1/§8:
data-source, model-map (per-profile default required), termination (positive caps),
binary feedback decision. TDD: tests written red-first; suite 14/14 green without
any API key; ruff + mypy --strict clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
2026-07-03 06:06:03 +02:00
97a9172311 Squashed 'shared/' content from commit db86e15
git-subtree-dir: shared
git-subtree-split: db86e159148d7d8553a38516e174370cf913f889
2026-07-03 06:00:11 +02:00
eed2f02bbb Merge commit '97a9172311' as 'shared' 2026-07-03 06:00:11 +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