Commit graph

79 commits

Author SHA1 Message Date
40b46317f3 test(ingest-spec): the guard read one table and called it the whole spec
`test_ingest_spec_loadbearing.py` anchored §12, the toolkit rule and presence.
Measured before touching it: gutting any of §1-§11 to its heading alone left
this file green for 11/11 sections, and for 10 of those 11 nothing in the whole
806-test suite went red either. §7 was the single overlap — the O2 ratchet reads
its `generated` row. A commons pull could have emptied a normative section and
no test would have noticed.

`_STRUCTURE_MARKERS` binds ONE verbatim normative phrase to the section that owns
it, the form already proven in `test_method_spec_rule_phrases_loadbearing.py`.
The selection is ours: each phrase names a seam this repo carries and cites the
owning module (okf.py, ingest.py, promotion.py, the goldens, the offline
invariant). §7 is bound on a second, independent sentence so the two guards do
not key on the same row.

Value-proved, not just detached: 11 phrases, 11/11 GREEN BEFORE / RED AFTER on
the same mutation, run against the working-tree spec and restored byte-identical
(sha256 verified, `shared/` clean before and after). The denominator is a test of
its own — `test_the_older_half_is_blind_to_the_emptied_section` keeps the §12
half's blindness machine-checked, so the value-proof cannot decay into "the new
marker can go red".

806 -> 884 tests. ruff, mypy strict, full suite green; offline, no `shared/` edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQBdZYCWHLKNH8tLiZsXFq
2026-08-18 17:52:38 +02:00
90a41774fc test(sdk): the pin was a permission, so give the premises a proof
The guard checked whether the installed SDK satisfied the pin. Nobody had
ever checked whether anyone had READ it. Those are different questions, and
the gap between them was a whole version range: pinned >=0.2.111,<0.3,
premises source-verified through 0.2.110, installed 0.2.120. Every build in
between was admissible and unexamined — `uv sync --upgrade` would have kept
806 tests green on an SDK no one had opened. Written red first: a guard
handed 0.2.140 returned it without complaint.

_VERIFIED_THROUGH is the ratchet. It records the newest build actually read
at source, and a newer one fails naming the five premises to re-check. The
pin is untouched and was never the defect — measurement dissolved the
premise that it needed lifting. It was not too narrow but too wide, and a
wider permission is not repaired by widening it further.

The premises themselves were prose the failure message recited. Nothing
tested them, so one that stopped being true would have surfaced on the one
live paid run (S10, D6). They are now a table introspected against the
installed package, with the printed prose derived from that same table so a
checked attribute cannot go unreported or a reported one unchecked. The
premise introspection structurally cannot see — that query() yields an
AssistantMessage then a closing ResultMessage — is named apart, and is the
honest reason the human reading still has to happen.

Value-proved, not merely named: disabling the ratchet reds 1 test, stubbing
the inventory to "no gaps" reds 3, re-hardcoding the prose reds 1, and
lowering _VERIFIED_THROUGH below the installed build reds the real
installed-version test rather than only a monkeypatched one.

0.2.139 read at source (0.2.120 -> 0.2.139, latest on PyPI today; STATE said
0.2.134, measured 08-09 and stale). The public query.py is byte-identical,
every premise field keeps its type and default, and the parser changes are
additive. One needed a look: 0.2.139 added a skills path defaulting
setting_sources to ["user", "project"], which would have undone the S10
isolation fix — it fires only on None, so the explicit [] is out of reach.
Prose carrying stale version claims moved with the reading, never ahead of
it: each was re-verified at 0.2.139 before being restated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dKDjVG7qrBh9NkAAxutqN
2026-08-18 16:57:57 +02:00
0b69354c74 docs(changelog): cut 0.1.0 — the version the two open gaps allow v0.1.0
The repo has never had a tag, so this is version one. The number is chosen
from maturity, not from habit: the shared spec's V1 `generated` shape is
landed upstream but not adopted here (the golden is the library's emission,
so adoption is gated on the ingest pin swap), and the SDK pin reaches
further than the range whose premises are source-verified. Both gaps are
held open by tests on purpose. 1.0.0 would claim a settled surface this
implementation does not have.

pyproject.toml already declares 0.1.0 and needs no change; README carries
three badges, none of them versioned, and src/ declares no __version__ —
enumerated, not assumed, so nothing else can drift from the tag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eYPddPVYPMxc5L7a4nxvA
2026-08-17 13:33:19 +02:00
3793466521 fix(readme): the four dead TOC anchors, and a guard that reads the renderer instead of modelling it
Ordre 39's TOC was built from a model of Forgejo's slugger ("each space becomes
one hyphen"). Measured against the published page, the renderer COLLAPSES each
run of [space|hyphen] into one — so the four headings carrying an em-dash between
spaces got two hyphens in the anchor and none in the id. Four of eleven links
were dead on the open mirror while every local check said green.

The anchors lose the extra hyphen; no heading text changes.

test_readme_anchors_loadbearing.py makes the check load-bearing (spec §11). It
pins the 14 heading ids Forgejo actually emitted on 2026-08-17, so the rule is
checked against the EMISSION rather than against itself — the failure mode that
let three known-negative controls agree with the wrong model. Known-negative
fixtures cover a broken anchor and a "## Phantom Heading" inside a fence (with a
positive control, so the fence test cannot pass by finding nothing), and the
naive space-rule is asserted to miss exactly the four ids that shipped dead.

Value-proved green-before/red-after on four mutations, including one that stays
green for the right reason and is recorded as a limit. Offline; no network.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwphwRfCBaDDCzeYghKuoM
2026-08-17 11:28:46 +02:00
a501d8d157 docs(readme): add table of contents
Round 2 of the AAA+ program, B-axis: a 415-line README with 11 H2
sections had no navigation aid, forcing scroll-to-scan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmKk4ryAX8Fhu91hCi4azN
2026-08-16 16:18:55 +02:00
3bdf7f0669 docs(scope): the independence claim gets a date instead of a future it cannot keep
Operator decision 2026-08-09: the two implementations are developed in the open
against each other. Reading the sibling is permitted and exchange may go both ways;
code is copied only where it genuinely serves the solution, never as a shortcut. The
constraint that remains is what the Microsoft Agent Framework and the Claude Agent
SDK each actually offer — which is the difference the comparison exists to measure.

The old rule lived in three places and two of them would have started lying:

- CLAUDE.md carried the prohibition, and it is read at every session start — left
  alone it would have made the next session refuse exactly what was authorised.
- README stated in the present tense that this repo is "built from the shared spec
  alone" and "deliberately does not reverse-engineer the MAF sibling". True for
  every line written so far, and untrue for anything written from today.

So the README claim is DATED rather than deleted. Through 2026-08-09 the
independence is real and stays claimed; from 2026-08-09 the open-competition rule
applies and is stated plainly, with the consequence spelled out: anything comparing
the two implementations as INDEPENDENT evidence must be scoped to work on or before
that date. The honesty rule (method spec §1) does not let a claim outrun what was
actually done, and that applies to the README's claim about itself.

CHANGELOG is deliberately UNTOUCHED. It describes what was true at that release, and
it was true — 73 commits through 3e91072 were built from the spec alone. A changelog
records history; it is not rewritten because a forward rule changed.

What did NOT change, and is restated in the README so the dating cannot be misread as
a loosening of the method: the spec is still normative and the golden suite is still
the validator's only oracle.

787 tests green, ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWMfPKmJtv7JvYnpomkdV
2026-08-09 12:56:30 +02:00
3e91072a7c test(loadbearing): bind a rule phrase to every section the shape guard cannot see
The spec guard anchored SHAPE only: the `## 1.`-`## 12.` headings, `### Step 1`-`8`,
and the bare literal `MUST` — the last checked against the WHOLE document. So a
commons pull could empty a section of its normative content and stay green as long
as the heading survived and `MUST` appeared anywhere else. §5 was the sharpest case:
it carries no `MUST` at all, so nothing in the old guard ever touched it.

One verbatim normative phrase is now bound to the section that OWNS it, matched
whitespace- and emphasis-normalized so a reflow or a bolding change does not
false-red — with a control that a changed WORD still does. Two properties make the
binding real: the phrase must sit in its own section body (moving it elsewhere
leaves the owner unanchored, proven), and it must be unique in the document (a
phrase occurring twice could be satisfied from a section other than its owner).

The selection is ours, not mirrored. Each phrase names a seam this implementation
actually carries, owning module cited per row — a phrase anchoring a seam we do NOT
implement would be a green-but-dead guard of the opposite kind.

Value-proven against the REAL spec, not a copy: gutting each section (heading kept)
reds this guard 11/11, and the OLD guard stayed GREEN for 10 of them. §3 is the
single overlap — it owns the `### Step N` headings — so there this is defence in
depth, not new coverage. That distinction is recorded in the file rather than left
to sound like eleven. Restore sha256-verified; `git status` clean before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWMfPKmJtv7JvYnpomkdV
2026-08-09 10:25:55 +02:00
5c25ae4889 test(loadbearing): the spec moved to O2 and the emitter did not — ratchet the gap
commons executed V1 (54e0ec7): §7's `generated` row is now the O2 inline mapping
`{ by: process:okf-ingest, at: <ingested_at> }`, and `generated: true` is gone from
the spec. The subtree pull brought that text in — and all 711 tests stayed green
while the shipped spec and the shipped emission disagreed. Green-but-dead, the
exact failure mode §11 exists for.

The byte form is not ours to edit: the golden bundles are compared byte-for-byte
against `materialize()` output, which delegates to llm-ingestion-okf pinned at
v0.3.2 — measured emitting `"generated": "true"` (materialize.py:103), with an
ownership predicate on the literal (`:89`) that the new §3 forbids. Editing the
golden to O2 would turn the suite RED against the pin, not conformant. Adoption is
gated on an okf release that emits O2, and the pin move is operator-owned.

So the divergence is RECORDED instead of hidden, and ratcheted in both directions:
RED if commons reverts §7, RED the moment a materialize() run reaches O2 — which is
when the four golden blobs and the two verbatim asserts must be adopted in the same
commit and this file deleted.

Value-proven with the mutation harness, all four green-before/red-after, restores
sha256-verified: spec row removed · actor renamed · pre-V1 literal returned · and
the one that matters, the installed emitter mutated to O2 (M4 fails on MY assert at
:115, after the population control passed — not on a NameError, not on the control).

The two existing `== "true"` asserts no longer read as conformance claims, and the
§12 anchor's justification is re-measured (`generated` appears 22 times outside §12,
once inside) since the old one cited a literal the spec no longer carries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWMfPKmJtv7JvYnpomkdV
2026-08-09 10:19:40 +02:00
c4cf448e95 Merge commit '89e3ad8efe' 2026-08-09 10:15:28 +02:00
89e3ad8efe Squashed 'shared/' changes from 35220f7..aa9eaa9
aa9eaa9 docs(plan): V1-etterspill — §12 mangler rader for `by`/`at`, presedens målt begge veier
54e0ec7 spec(ingest): V1 — `generated` til O2-formen, ratifisert 2026-08-02
ddaae5d chore(release): publiseringsklar for open/ — README for standalone rot + MIT + policy-filer
f98b287 docs(plan): V1 RATIFISERT — og :275 er en andre tabellrad, ikke prosa
d6bced7 docs(plan): SS11 ankrer ikke SS8 — funnet var reelt, men ikke raden som ble bestilt
3174475 docs(plan): §7.2 — feilanker-failuremoden var ikke hypotetisk, den inntraff
a2b57d2 docs(plan): V1 — «de 5 linjene» var ikke homogene; :214 er ikke en literal
d63e45d docs(plan): okf-versjonssjekken utført — hypotesen falsifisert på to stale premisser
ef31dda docs(plan): V1 §4.2 — pin + id + sitering avgjort, og ratifiseringsgaten funnet

git-subtree-dir: shared
git-subtree-split: aa9eaa9df06463a8cf00bd28fd133b1f20d7b888
2026-08-09 10:15:28 +02:00
82d46148e2 test(loadbearing): measure the last 8 all-negative tests instead of assuming them
Point 2 of the vacuity sweep is now MEASURED, not paired-by-reading. Every
one of the 8 got a mutation that detaches the seam it claims to guard, run
through a harness that asserts the anchor is unique before mutating, restores
in `finally`, and sha256-verifies the restore.

Six were value-proven — the negative itself went RED under its detach:
  hitl :260  load_routing invents a default        -> RED
  hitl :298  route_pending hardcodes a fallback    -> RED
  step7 :145 the is_dir() guard deleted            -> RED
  step7 :171 the §4.2 vocabulary filter deleted    -> RED
  step7 :253 the id grammar off the model          -> RED
  prov  :111 sdk_version becomes required          -> RED

Two did not, and both are fixed here.

hitl :197 — the guard it appeared to prove is DEAD. Deleting the `is_dir()`
early-out from load_outbox_proposals leaves all 711 tests green: the tolerance
comes from `Path.glob`, which yields nothing on a missing directory and never
raises. The contrast is the finding: load_inbox carries an identically-shaped
guard that IS load-bearing, because it walks with `Path.iterdir`, which DOES
raise (measured both ways). Same guard, opposite verdict, and the difference
is the stdlib call behind it — the point-3 lesson one level out, where the
default being pinned belongs to the standard library rather than the SDK.
The stdlib baseline is now anchored explicitly, so a Python that makes glob
raise turns this red instead of quietly promoting a dead line to a seam.
What the test always did prove is kept and stated: replacing the early-out
with a raise turns it red, so it does hold tolerance.

portfolio :175 — the negative asserted over an unheld population. Measured, it
is real today (6 prompts), so the test is not vacuous now; nothing in it says
so, and a run_portfolio that stopped prompting would leave it green while
proving nothing. A positive control now runs first. Value-proven: green before,
red after the same mutation (iterate no projects), and it is that assertion
which fails, not an import.

The sibling repo sent the same rule from the other stack this week, arrived at
independently via its B4 empty-negative: on a negative assert, prove FIRST that
the event happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr6TwWrHDHeukHy3bL4hgb
2026-08-09 10:08:12 +02:00
e6bc1832a2 docs(links): the commons pointer must resolve for a stranger, not just for me
Both links named the private ktg/ namespace. commons published the repo at
open/ (identical history, same HEAD) and reported the four hits it measured
in our working copy. These are the two we own; the two in shared/README.md
are commons-owned and arrive via subtree pull, never edited in place.

README.md:64 is the one that mattered: it sits on the published surface and
promised a clonable source a stranger gets a 404 on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr6TwWrHDHeukHy3bL4hgb
2026-08-09 09:58:22 +02:00
880f00f305 docs(brief): the plan reference must not be a link only this machine can follow
The repo-standard gate SKIPs a link that leaves the repository — and a SKIP is
"not measured", never a pass. This one left it into `/Users/ktg/repos/...`, so
for every reader but the operator it was a dead click dressed as a citation.

The reference itself is correct and stays: the programme plans deliberately
live in the sibling repo (CLAUDE.md). Only the link form goes, replaced by the
repo name plus the path inside it — which a stranger can act on and this
machine's layout can no longer invalidate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gu3n2PVVnb9fZDCe2wnU1h
2026-08-04 11:55:17 +02:00
90ed9ee469 docs(readme): satisfy the repo-standard first screen
The gate flagged three real ERRORs for a standalone-class repo:

- the opening line was a licence badge, so `description == catalog ==
  README` — the one thread a machine can actually check — was broken.
  The forge description now leads, badges follow, and the paragraph
  under them stops repeating it.
- no `## Install`. Added as its own top-level heading, because that
  fixed position is what an agent handed "install this" pattern-matches
  on. It states the uv-from-source path honestly: nothing is on a
  package index, the one non-PyPI dependency resolves from its pinned
  tag, and since this forge has no CI runner the clean-clone `uv run
  pytest` IS the verification — said in as many words so the absent CI
  badge reads as a choice rather than an omission.
- no `## Non-goals`. Six, each traceable to something the repo already
  enforces: no reallocation between projects, no compliance claims, no
  network connector (`http` is rejected fail-fast), no query-time
  retrieval, not a port of the MAF sibling, not a running service.

Also links CHANGELOG.md, which the README had never pointed at.

The gate still reports 7 ERROR here. All seven are one defect in the
check, not in the repo: `checkInternalLinks` resolves targets against
the set of tracked FILES, so every `[x](dir/)` is "missing" even when
the directory is tracked and full. All seven targets verified present.
Rewriting them to point at some file inside each directory would make
the runs/s10 sentence untrue — it claims all four artifacts live there
— so the links stand and the defect went to repo-standard by coord
(20260803T194933Z, with a minimal repro).

Suite green at 711 passed; the README<->--help parity test reads this
file and still holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gu3n2PVVnb9fZDCe2wnU1h
2026-08-03 21:50:08 +02:00
a43b5c7336 test(loadbearing): positive controls for three measured-vacuous negatives
Continues the sibling-vacuity sweep (pkt. 2). Each fix is value-proved:
GREEN BEFORE / RED AFTER under the same mutation, never a detach-proof alone.

- test_goal_without_ledger_reads_an_empty_book asserted only `code == 0`.
  Measured: stubbing check_goal_before_spend to return False before ever
  reading the ledger left it GREEN — it could not tell "empty book, goal
  evaluated" from "check skipped", which is the one thing its name claims.
  Now asserts the evaluation line (realized 0 of 1.0 NOK, not reached).

- test_the_allowlist_names_only_flags_the_cli_actually_has computed
  `missing == []` over _PORTFOLIO_SUPPORTED_DESTS. Measured: mutating the
  allowlist to frozenset() left it GREEN — an empty iteration yields an
  empty list, so a blind scanner reports no findings exactly as a clean one
  does. Now proves the detector fires on a flag the CLI lacks first.

- test_the_system_prompt_is_empty asserted `system_prompt is None`, which is
  the SDK's OWN default (measured, 0.2.120). Deleting `system_prompt=None`
  from build_call_options left it GREEN: it pinned the SDK, not our code.
  The distinguishable seam is the Claude Code preset the S10 post-mortem
  retired, so the test now guards that and is renamed for what it proves.
  The None-vs-untouched limit is UNCONTROLLABLE and stated in the test.

Also pins the SDK defaults both assertions choose against, so a future SDK
shipping [] or a preset default degrades the anchor loudly instead of
silently (an ANCHOR CAN DEGENERATE).

Negative findings, so no session re-measures them: test_zero_model_calls is
NOT vacuous (detaching the dry-run gate goes RED — though it dies inside the
client, before reaching its own `calls == []`); notify/ir/validator/step1
and the two cli_paritet flag tests are covered by same-class sibling pairing
on the same function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQDNnD2szj3dthvqzd9Y8E
2026-08-01 20:24:58 +02:00
fae5b22578 test(loadbearing): positive controls for the static-guard half of the sibling-vacuity class
Point 2 of the sweep, enumerated rather than assumed. STATE's total was right and
its distribution was not: 86 hits confirmed (`assert not X` 41 / `== []` 42 /
`== {}` 2 / `== set()` 1), but per file measured `test_cli_paritet` 13 (STATE said
19), `test_preflight` 10 (11), `test_step7` 4 (6).

AST triage split the 86: 55 hits sit in 50 tests whose assertions are ALL
negative; the other 31 already have a positive sibling assert in the same test.

Two negative results worth recording, because they bound the remaining work:

- The `test_preflight` "clears" family (`_check_credentials(...) == []` and
  friends) is NOT vacuous. Each sits beside a sibling in the same class that
  asserts refusals are non-empty, so a no-op checker turns the sibling red.
  Class-level pairing is a real control; these need no change.
- `test_method_spec_loadbearing.py` already models the right pattern for
  detectors — explicit `test_guard_red_when_*` red-proofs against a mutated COPY.

This commit fixes the class that had no control at all: static/AST guards that
assert an absence without ever showing the scanner can detect a presence.

1. TAUTOLOGICAL RED-PROOFS (both spec guards). `test_guard_red_when_spec_missing`
   asserted a file is absent from a fresh `tmp_path` — true by construction of the
   fixture, and it never called the guard it is named for. It would have stayed
   green with `test_spec_is_present` deleted outright. Both now exercise the same
   `_spec_is_present` predicate the guard calls, in both directions.

2. MISSING RED-PROOF. `test_spec_keeps_structure_markers` had none, unlike its
   toolkit and contract-field siblings: with `_STRUCTURE_MARKERS` emptied or
   `_missing_markers` stubbed to `[]` it reported green forever. Added
   `test_guard_red_when_marker_removed`, parametrized over all 21 markers.

3. BLIND IMPORT SCANNERS (costsim x2, okf, preflight, notify). Every one asserted
   `not names & {forbidden}` or `outside == set()` with nothing showing `names`
   was non-empty — an empty scan satisfies them exactly as well as real purity.
   `test_okf_is_pure_stdlib`'s subset check is likewise trivially true of the
   empty set, so it did not guard its neighbour either. Each now asserts a
   known-present module first. The notify guard gets the strongest form
   available: it proves the detector DOES match a network import inside the seam,
   so the matcher itself is shown to work rather than only its silence.

Value-proved, not merely detach-proved. Seven vacuity mutations run against the
NEW tests: all seven RED, each dying on the intended control line. The same
mutations run against the PRE-CHANGE tests (session edits stashed): all five
applicable ones GREEN — blind to the vacuity they were meant to catch. Green
before, red after, same mutation, is the value-proof.

Harness held original bytes in memory, restored in `finally`, sha256-verified
every restore, and checked each run ACTUALLY RAN (a wrong test id yields rc!=0
and mimics red). `git status` clean before and after.

Remaining in the class and NOT closed here: ~45 all-negative tests, mostly CLI
refusal (`calls == []` after a refused invocation) and empty-default
(`missing dir -> []`). Listed in STATE, not silently dropped.

Suite 690 -> 711.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJmse16bEkaSBtvXhncEUc
2026-08-01 20:01:21 +02:00
d691510163 fix(okf): nested frontmatter keys must not forge the type that gates verdict exclusion
The frontmatter parser is a line-oriented approximation of YAML with no nesting
model, and it flattened INDENTED keys into the same mapping. Two measured
defects followed, the second load-bearing:

1. Sibling blocks sharing an inner key COLLIDED. Given OKF §10's canonical
   Attested Computation shape, `executor.resource` vanished silently and
   `attester.resource` was promoted to a top-level `resource`. No error raised.

2. An indented `type:` OVERWROTE the column-0 one, making the verdict-exclusion
   gate in `bundle_context` forgeable. A file declaring `type: verdict` at
   column 0, carrying any nested block with a `type:` in it, rendered its body
   straight into the read-context — defeating the §11 seam whose own docstring
   claimed "a mislabelled or injected edge cannot smuggle a verdict into the
   context". The type CHECK was there; the VALUE it checked was writable.

This is spec-legal input, not malformed input: method-spec §2 calls it YAML
frontmatter, and ingest-spec §7 (`:153`, `:216`) says unknown keys MAY follow
the stamp and ride through navigation.

Fix: only column-0 keys participate; indented lines are skipped, never
flattened. Nested blocks become OPAQUE — stated as a limitation in the
docstring, not dressed up as a nesting model we do not have (§1 honesty rule).

Single parse site, so the fix covers the class: `hitl.py:157` (verdict_id,
gated on .type) and `experience.py:137` (realization_rate, expected_actual)
all read through `ConceptFile.frontmatter`; `promotion.py` only emits, from a
fixed template.

Golden-neutral by measurement: 0 indented frontmatter lines across all 17
frontmatter-bearing files in `shared/examples/`.

Both tests written RED first and confirmed to die on the SEAM assertion, not on
a collateral one — the forgery test rendered `## project: Seed` before the fix.
Each negative carries a standalone positive control (sessions 17-18: a control
can itself hide behind a preceding assert).

Provenance: hypothesis received from llm-ingestion-okf, who measured the
flattening in their own parser and flagged that ours shares the form. They had
NOT measured our side. The type-clobber variant is ours, found by measuring the
neighbourhood rather than only the reported case.

Suite 688 -> 690.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJmse16bEkaSBtvXhncEUc
2026-08-01 19:53:03 +02:00
30ba68a703 test(loadbearing): close the vacuous-negative class across the whole suite
Oekt 17 found the class on four named files. This sweep ENUMERATES it: 42 negative
substring assertions across 21 test files (STATE's "~34 across 23" was a premise --
measured, it is 42/21). Sixteen of them measured an absence without ever having
shown presence; all sixteen now carry a positive control asserting the searched-for
string PRESENT in the source artifact, in EXACTLY the form the negative looks for.

Files touched: test_costsim, test_loop, test_okf (3 sites), test_preflight,
test_run_entrance, test_s10_run_layer, test_sdk_version_guard, test_simulation
(2 sites), test_step1_expel, test_step5_refine, test_step7_async_loop,
test_step8_promotion, test_valuereport.

VALUE-PROOF (green-without / red-with, per the oekt-17 rule that a detach proof is
not a value proof). Seven source/fixture mutations, each making the negative vacuous:

  M1 verdict fixture loses the realization signal        VALUE-PROVEN
  M2 decoy fixture loses its text                        VALUE-PROVEN
  M3 renderer stops emitting typed section headings      VALUE-PROVEN
  M4 promotion stops writing the marker                  VALUE-PROVEN (pass 2)
  M5 fold stops rendering the realization surface        VALUE-PROVEN
  M6 report stops labelling the cost section             VALUE-PROVEN
  M7 preflight stops importing the SDK                   VALUE-PROVEN

M4 needed pass 2: a PRECEDING assertion caught the same mutation, hiding the new
control behind it -- the oekt-17 lesson reproduced. The remaining nine controls are
vacuity guards (non-emptiness / form-presence) whose mutation would have to break
the source artificially; they are stated as guards, not claimed as value-proven.

MEASURED FINDING (test_loop): the FIRST-RUN-MARKER negative cannot be given a
positive control at all. Within a run only the CHECKER's critique is fed back --
the proposer's own prior reasoning crosses no prompt boundary, not even within a
run. So that negative holds trivially. Left in place with the limitation stated in
the test rather than dressed up as a controlled seam; the CRITIQUE negative beside
it IS controlled and is the real seam.

Mutations were in-place on src/ and shared/ with original bytes restored and
sha-verified; git status clean before and after. Suite 688 -> 688 (assertions added
inside existing tests, no new test cases). ruff + mypy --strict green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Vc5PmZGjwuJypdhzKnJa5
2026-07-31 21:39:28 +02:00
123ecc3113 test(loadbearing): positive controls for the vacuous-negative class
STATE pkt. 2 scoped a measurement of the substring guards against tmp_path-
GENERATED artefacts. Measured, not reasoned: every one of the 18 assertions
behind those 11 line refs was detached for real and each is individually
load-bearing. Mutation matrix (src/lib mutated in place, restored + sha-verified,
`git status` clean before and after):

  M1  render_table drops rows            -> ingest_lb:91, sql_lb:104,105   RED
  M2  SQL NULL -> naive str() "None"     -> sql_lb:61,62                   RED
  M3  whole REAL loses its .0            -> sql_lb:69                      RED
  M4  _update_index_lines over-reaches   -> ingest_lb:165,166,189 sql:162  RED
  M5  _update_index_lines under-reaches  -> ingest_lb:188 (negative)       RED
  M6  _link_in_index no-op               -> ingest_lb:169, sql_lb:164      RED
  M7  collision gate clobbers first      -> test_ingest:141                RED
  M8  index label leaks the rationale    -> step8:179,180,194 (negative)   RED
  M9  index label varies per verdict     -> step8:186,187,188              RED
  M10 re-promotion double-links          -> step8:170                      RED
  M11 fold drops the rationale prose     -> step8:151                      RED
  M12 seeding re-mints the verdict id    -> step8:163,164                  RED

A second pass was required because pytest stops at the FIRST failing assert:
six assertions sat behind a failing one and were therefore unmeasured at test
level. Re-run with the preceding assertion neutralised, each of those six is
load-bearing too (ingest_lb:91-B, :166; sql_lb:62, :105; step8:180, :164).

The finding is structural, and it is the reason this commit is not empty. Five
NEGATIVE assertions carried no positive control, so they measure an absence
without ever establishing the presence. Proven by value-proof (not merely a red
proof): under a plausible drift — `_link_in_index` detached, or `description`
stopped carrying the rationale — all three tests stayed GREEN with the control
removed and go RED with it present. green-without / red-with is what makes these
controls value-adding rather than decorative.

  test_ingest_loadbearing.py       the ingest-edge link is asserted PRESENT, in
                                   exactly the form the removal assertion seeks
  test_step8_promotion_loadbearing the marker/rationale are asserted live in the
                                   promoted file before the index/context
                                   exclusions are allowed to mean anything

Next lens, enumerated rather than assumed: the class reaches 23 test files, not
the 4 STATE named — ~34 negative substring assertions in total. "Negative without
a positive control" is the sharp, cheap successor to "substring assertion".

Suite 688 passed; ruff + ruff format + mypy --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzEtJzL6SKYbYtSQRY5o57
2026-07-31 21:15:57 +02:00
769687159f test(sdk-guard): anchor the pyproject pin, make the detach-proof a test
The sweep the §12 work called for, run over every test reading a static repo
document. Enumerated population: four such guards (method-spec, ingest-spec,
README, pyproject). Three were already sound — the two spec guards were
anchored in sessions 14/15, and the README guard extracts flags by regex and
cross-checks them against real --help output with explicit vacuity guards.

The fourth was green-but-dead, and it was MEASURED, not inferred:
`assert _PIN in _PYPROJECT.read_text()` stayed GREEN (4 passed) while the real
dependency drifted to >=0.2.110 below the guard's own verified floor, because
the literal survived in a trailing comment. The comment above it claimed
"Detach-proof: the pin and this guard cannot drift apart silently" — the exact
drift it named is what it let through.

Three narrowings, each one a measured degeneration rather than a precaution:
- ANCHOR: match inside the `dependencies = [...]` array, fail-closed with
  ValueError when the array is renamed (a silently empty slice would make
  every assertion vacuous).
- QUOTED FORM: the slice alone still did not detach — a comment sits inside
  the array too. Requiring `"<pin>"` with comments stripped does.
- VALUE BINDING: _PIN is now DERIVED from _VERIFIED_FLOOR/_CEILING via
  _pin_for(), so the range this guard enforces and the pin it demands cannot
  part company. The error message derives from it too, instead of carrying a
  third hand-maintained copy that could lie.

Five permanent red-proofs replace the manual spot-check, all run against a
mutated COPY of the text, never pyproject.toml itself. Measured degeneracy:
substring-anywhere restored -> 1 red; anchor widened to the whole file -> 2
red; the derived-pin binding severed -> 2 red.

Suite 683 -> 688; ruff, format and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FcKMxznPVR9zfdsdu5Ztdn
2026-07-31 20:56:21 +02:00
e70de95afb test(ingest-spec): make the §12 detach-proofs permanent tests
Mirrors the form already solved in test_method_spec_loadbearing.py: every
predicate now takes the spec TEXT as an argument, so last session's four
manual detach measurements become tests in the suite instead of a one-off
spot-check that died with the session.

Red-proofs (mutated COPY in tmp_path, never shared/):
- M1 row removed from §12   -> the field reads as undocumented
- M2 row renamed            -> detach-proof is not a value-proof
- M3 §12 heading renamed    -> ValueError, fail-closed (no wider fallback)
- M4 anchor degenerates     -> the slice guard is red on the whole spec

Measured, not asserted: mutating _cross_check_table to return the whole
text turns 2 tests red; reverting the row assertion to substring-anywhere
turns 14 red. 16 -> 61 tests in this file; suite 638 -> 683.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011t6M7qfsYgpZyNvRySixNj
2026-07-31 18:35:41 +02:00
e2eb31675d test(ingest-spec): anchor the contract-field rows to §12, not to prose
`assert field in text` over the whole spec was green-but-dead: prose saturates
every field name (§1's honesty rule alone carries `generated: true` twice,
independent of the §12 table), so no amendment dropping a row could turn it red.
Measured: deleting the `generated` row from §12 left the file 15/15 green.

The spec appoints its own anchor — §12 says "completeness is enforced by the
spec-integrity test", and this is that test. The comment above _CONTRACT_FIELDS
already claimed §12; only the assertion did not. Each field must now appear as a
row's FIRST column, so a field named only inside another row's prose fails.

A slice guard comes with it: a `_cross_check_table()` that degenerated into the
full text would restore the dead state silently, so it is asserted to be a slice.

Detach proofs (mutate, run, restore from copy):
  M1 §12 `generated` row removed  -> RED [generated] only
  M2 row renamed to `generated_x` -> RED [generated] only (value proof)
  M3 §12 heading renamed          -> RED 14/16 (anchor gone, fail-closed)
  M4 slice returns whole spec     -> RED slice guard; [generated] goes GREEN
                                     again under M1 — the guard is what binds it
Control green, 637 -> 638.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWrLqjkDvUtrGL5VXXe2ip
2026-07-31 18:26:35 +02:00
7d8de32543 feat(okf): navigate hierarchy — escape, not depth, is forbidden
The pulled method-spec (commons 9801d35) retires the "a target containing a
path separator is out-of-bundle" heuristic, which conflated depth with escape
and forbade valid hierarchy. Triage of the pull found FIVE contradictions in
okf.py, not the two STATE had measured on line 127 alone:

1. the separator ban skipped every legal nested target;
2. de-duplication keyed on the RAW target (`resolved` was computed a line
   later), not on the resolved path;
3. navigation never recursed — only the root index's links were read;
4. a leading `/` became filesystem-absolute via pathlib rather than denoting
   the bundle root (safe, because the boundary check caught it, but the right
   outcome for the wrong reason — and wrong the moment `/a/index.md` must be
   FOLLOWED);
5. rendering excluded only `verdict`, so a nested index body would render as
   content.

navigate_bundle is now depth-first in first-seen link order, de-duplicating on
the resolved path (so `./a.md` and `a.md` are one entry and cycles terminate);
resolution and the fail-closed boundary check move to _resolve_target, the sole
in-/out-of-bundle test. The missing-index rule binds the bundle root alone.
bundle_context renders flat regardless of depth and drops nested index bodies:
only the root index is the summary.

The gate is the commons-owned nav-golden pair that arrived with the same pull —
bundle in, expected-read-context out. Its negative case exists so the gate can
go red at all, and carries a real decoy one level up plus a `/etc/passwd` trap.

Detach-proved (mutate, run, restore from copy) — each new seam goes RED:
  D1 reinstate the separator heuristic -> RED
  D2 re-key dedup on the raw target   -> RED
  D3 read a leading `/` as absolute   -> RED
  D4 render nested index bodies       -> RED
Control after restore: 24 passed. Suite 631 -> 637, ruff + mypy --strict clean.

Comments asserting the retired doctrine were corrected rather than left to
document a rule the code no longer follows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1zp3BxCuzRnUtJPzvEFTQ
2026-07-31 18:13:20 +02:00
ae5249488f Squashed 'shared/' changes from 7aa53fc..35220f7
35220f7 docs(plan): V1 §4.1 — serialiseringsformen er bundet av :158, så 6 sider er invariant
8a7d430 docs(plan): operatøren avgjorde V1 (O2) og B1 (O1) — B1 utført, V1 gated
f306c7b docs(plan): V1 §5.1 — konsument-kostnaden målt @ 8a14137, O3-raden priset
ab0ea8f docs(plan): innboksrunden — V1 utvidet med tre målte funn, §7.2-siteringen ref-bundet
84a3010 docs(plan): §7.2 — MCP-spørsmålet er avgjort av frossen tekst, S2.2 og S2.4 er ugated
e984d51 docs(plan): D-B ankret, D-A#3s årsak er repo-avhengig, V1 utvidet med oppstrøms-evidens
3de702b docs(plan): V1 §6 — rekkevidde-forbeholdet korrigert, og funnet under funnet
249425b docs(plan): amendment-underlag — D-A#3s ÅRSAK rettet, drift ikke avvik
4876970 docs(plan): V1-underlag — generated-feltets form etter OKF v0.2
3f18ca7 docs(plan): B1/D4 operator question — nav-golden's normative status, four costed options
29ad8ca docs(plan): amendment-underlag — frossen tekst per køpunkt, med målt fasit-effekt
a67a243 docs(plan): ordering-axes ruling — three distinct axes, as an interpretation record
381d9e5 docs(plan): guard adoption plan pins no version — v0.2 header was stale
54ca0ff docs(plan): D3 ratified — status vocabulary landed in coord register
fe6b998 docs(plan): D3 status vocabulary — canonical 7-token set for roll-up register
c66ccc3 docs(plan): D2 register-form fix — two-output model resolves ÅS#5
b641741 docs(examples): nav-golden fixture class — bundle → read-context goldens
9801d35 docs(spec): method-spec §3 Step 1 — Q3 navigation contract (hierarchy)
bfa5a9b docs(spec): ingest-spec — land ratified D1 stamp model
5f046ab docs(plan): Q3 finding — method-spec §3 silently forbids hierarchical bundles
a013e8b docs(plan): F1 direction — free-text sources belong in ingest-spec door A
84191c6 docs(plan): record commons↔implementation-repo coordination boundary
cae9972 docs(plan): llm-ingestion-guard adoption — planned at future untrusted ingest boundaries

git-subtree-dir: shared
git-subtree-split: 35220f7a1e88bc167f725cb19ec04c1da97c387b
2026-07-31 17:39:45 +02:00
d024a236da Merge commit 'ae5249488f' 2026-07-31 17:39:45 +02:00
8a141370f3 test(valuereport): bind _SHARE_DIGITS to its MEASURED band, not to itself
The constant was detach-proof but value-unproven: mutating 6 -> 4 left all 628
tests green, so nothing held the figure to the claim it carries. Measuring what
actually constrains it gave a narrower answer than the premise assumed — 6 -> 4
CANNOT be made red without inventing a resolution requirement no layer states,
and §1 forbids asserting more than the implementation carries.

Measured band, both ends now load-bearing:
  * d >= 17 -> the 1-ULP float tail of a cohort subtraction reaches the JSON
    bytes (0.1 - 0.3 publishes as -0.19999999999999998, not -0.2).
  * d <= 2  -> the rendered percent moves (2/7 renders 29.0%, not 28.6%).
  * d in [3, 16] -> identical to every consumer this system has.

Both proofs are stated WITHOUT reference to the constant's own value — the
exact decimal difference of the two PUBLISHED shares, and a percent computed
from the RAW NOK figures — so they bind the claim rather than the number. A
literal like 0.142857 would only have bound 6 to itself.

The :61 comment justified only the upper end; it now records the measurement
and says plainly that 6 is convention inside the band, not a derived figure.

Mutation-verified: d=2 RED, d=3/4/5/16 GREEN, d=17 RED. Suite 628 -> 631.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-26 15:28:20 +02:00
f41264fbd3 fix(okf): the index entry point never requires 'type', frontmatter or not
navigate_bundle died on an index.md that carried a frontmatter block without
a 'type' field: _parse_index_entry keyed its tolerance on the ABSENCE of the
block, so a frontmatter-ful index fell through to parse_concept_file and
raised. Not merely the index read failed — the whole navigation did.

OKF v0.2 (A-E6) triggers this: it stamps okf_version into index.md. Our own
assumption ("a generated index has no frontmatter") is frozen in the golden,
so nothing caught it. The defect is ours, not theirs.

method-spec §3 Step 1 renders the index as "the index body (the summary)"
and every OTHER file as a "non-index concept file" — the index is not a
concept file, so the 'type' requirement never reached it in the first place.
The tolerance is now keyed on being the index, which is what it always meant.

Load-bearing, both directions detach-proved:
- test_index_with_frontmatter_lacking_type_navigates goes RED when the
  tolerance is re-keyed on absence-of-frontmatter.
- test_missing_type_is_an_error goes RED when the index default leaks onto
  concept files. Its vehicle moved from index.md to a non-index file: it
  proves the concept-file rule, and an index.md vehicle now proves the
  opposite of what the test is named for. This also closes a real gap —
  nothing tested a non-index file WITH frontmatter but WITHOUT 'type'.

Goldens untouched (shared bundle has type: index; ingest golden index has no
frontmatter). Suite 627 -> 628 passed.
2026-07-26 14:48:37 +02:00
ffb0503da3 docs(portfolio): A5 premise corrected — the quantisation POINT is the live divergence
MAF measured their own tree and corrected our claim that they "round one
monetary figure": their projection rounds nothing (value_report.py:48,
0 hits for round()); the one operation is a NOK->ore quantisation at the
ledger edge (ledger.py:204-214, Decimal + ROUND_HALF_UP). Rule 1 as we
wrote it therefore solved a divergence that did not exist and opened one
that did not — it condemned their deliberate, documented quantisation as
"rendering". Adopted their proposed split as rules 1a/1b.

Then measured here, which REFUTED our own first hypothesis: we assumed
float accumulation was the divergence. It is not — 0 of 20 000 random
portfolios diverge on 2-decimal NOK input. The real mechanism is where
quantisation sits relative to summation, and it only wakes on sub-ore
input: 66.4% of totals diverge, worst 6 ore over 28 rows, and a designed
100 x 0.005 case diverges by a factor of 2.

Sub-ore input is reachable in BOTH stacks — neither ir.py:42
(claimed_saving_nok) nor ledger.py:42 (amount_nok) constrains decimals;
both are float = Field(gt=0, allow_inf_nan=False). So two stacks that
both obey 1a+1b can still return different totals on identical input,
which is exactly what A5 exists to close. Hence new rule 1c: the
amendment MUST fix one quantisation point — reject finer-than-ore claims
at the contract edge, or quantise once on the total — and both stacks
must land on the same choice.

Doc-only; no source touched. Suite 627 passed. Nothing patched
unilaterally: shared/ is pull-only and the amendment is not in commons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 19:58:04 +02:00
457a0f4e91 docs(portfolio): D-A approved in full — D7's proposed amendment text with measured anchors
The operator approved the whole D-A bundle on 2026-07-25: the four spec
corrections (C-F2/C-F3/C-F4/C-F5), brief §7 (the projection contract over the
ledger), and the C-P1 collision resolved by RE-SCOPING semantic retrieval
rather than amending the frozen "structural, never textual" rule.

This is D7's contribution to the text, not the decision itself (that is
protocolled in the main repo's shared queue) and not the amendment (shared/ is
a PULL-ONLY subtree — the text must land in commons and be pulled by both
siblings). Each point carries the verbatim spec anchor it changes, proposed
RFC 2119 wording, and what we MEASURED about compatibility rather than what we
assume.

Two things are stated as open rather than settled, because they are:
- A2 (cost baseline): the golden bundle has no cost-baseline.json, so an
  unconditional requirement would change the fasit bytes for both stacks. Our
  wording requires a baseline only for runs that can spend; if the amendment
  chooses otherwise, the fasit change must be planned in both repos.
- A5 (rounding): the divergence is live — our monetary figures are unrounded,
  MAF rounds one. The proposed rule lands on our current behaviour, which is
  exactly why it cannot be decided here. Neither side patches unilaterally.

Code anchors re-verified against the working tree this session (the ledger fix
earlier today shifted valuereport line numbers), not copied from STATE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 15:38:51 +02:00
f300c64b0e feat(run): stamp the drill's SDK build in the dry-run run-config [skip-docs]
The K8 drill captured a run-config that described the rig it rehearsed —
model ids, parameters, caps — without saying which SDK build would drive it.
The SDK's reported USD figure is computed against a price table frozen at build
time, so a rig record without the build is not traceable, and the drill exists
precisely to rig a future live run.

build_dry_run_config now takes the client the drill constructed and reads the
build from it, the same seam rule the provenance stamp follows: a drill driven
by the scripted stand-in stamps null rather than the installed version, because
reading the environment would describe a rig that never existed (§1).

Load-bearing (§11): the two new tests went RED before the change (no such key),
and the detach point is named in the class docstring — read importlib.metadata
instead of the client and the scripted drill claims a build it never used. The
existing dry-run tests assert individual keys rather than a key set, so the
additive field leaves them untouched, and byte-determinism still holds.

624 -> 627 passed, ruff + mypy --strict clean. README states the new field.
STATE post 2a, approved by the operator this session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 15:35:16 +02:00
fc4a536e09 fix(run): classify portfolio-mode flags by allowlist so a new flag fails closed
The portfolio entrance refused unsupported flags from a hard-coded BLOCKLIST:
--inbox, --out, --outbox, --run-id, --value-report, --live-dry-run. That
construction fails OPEN. A flag added to the parser later and forgotten in the
list is accepted, does nothing, and says nothing — the operator's flag is a
claim the run does not back (§1). MAF's report mode already used an allowlist;
the divergence was raised as an open question and the operator decided it this
session in favour of fail-closed.

unsupported_flags_given() now reports every flag GIVEN that the allowlist does
not name. "Given" is measured against the parser's own default, so it needs no
knowledge of which flags exist — that is what keeps it correct for flags added
after it was written, including store_true switches.

Load-bearing (§11), detach-proven twice (before and after ruff format, restored
from a copy): swapping the membership test back to a hard-coded refusal list
turns test_a_flag_nobody_classified_is_refused RED, while every CLI-level
refusal test stays green — they only exercise flags a blocklist already names,
so they do not cover this seam. The other direction is covered too: a run
passing all fourteen honoured flags still exits 0, and the allowlist entries
are checked against the CLI's own --help so a rename cannot leave a dead entry.

612 -> 624 passed, ruff + mypy --strict clean. README states the allowlist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 15:33:30 +02:00
2c1317bdb5 fix(ledger): normalize every load rejection to ValueError at the ledger's own entrance
SavingsLedger.load unpacked the payload with `**`, so a valid-JSON but
non-object book ([], "x", 3, null) escaped as a raw TypeError — a failure mode
no caller catching ValueError would see. The run path was already covered:
valuereport.load_ledger caught the TypeError and re-raised it as ValueError,
and `run.py --goals` goes through that function. The leak reached only callers
outside that one path, which is why the suite stayed green.

The fix moves the normalization DOWN into ledger.py, where the public boundary
is, and deletes the now-dead patch in valuereport.load_ledger. One except
clause now covers the whole boundary: unparsable bytes (JSONDecodeError),
non-object top level (explicit check), wrong-shaped object (ValidationError).

Load-bearing (§11): the new TestLoadHasOneFailureType went RED before the fix
with exactly the TypeError it exists to forbid — pytest.raises(ValueError) does
not swallow it. Detach point named in the class docstring: drop the isinstance
check and the array/string cases raise TypeError again.

Found by cross-checking MAF's 7dab2df; queued in STATE as post 2b, approved by
the operator this session. 604 -> 612 passed, ruff + mypy --strict clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 15:29:27 +02:00
3529299335 docs(portfolio): mark the parity table's D7 column as a frozen 2026-07-16 measurement
The "D7-status i dag" column was measured in Steg 0 on 2026-07-16 and never
updated. Twelve build sessions have landed since, so twelve rows still read
"MANGLER" for capabilities that now exist — a later session reading the column
as current would rebuild something already built.

The note names each landed row with its commit (verified against git, not
against STATE's claim), and points at git history + STATE.md as the ground
truth for what exists today. The MAF column and the gate statuses are NOT
stale and remain valid planning input; only the D7 column is pinned to its
measurement date.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 15:27:52 +02:00
bf50d2c977 docs(portfolio): pin parity row 25 to MAF's S5.4 span 878c989..7dab2df
MAF supplied the commit hash we had queued as an open coord question. The row
recorded "hash IKKE oppgitt — spurt"; it now carries the span rather than the
single point so the row stays diffable, per their own suggestion.

Recorded honestly: the hash is verified by MAF against their git and is NOT
independently checkable from here — we do not read MAF src, and a convenient
hash does not lapse that constraint.

The rounding divergence (they round one krone amount, we round none) is now
stated as live-not-disagreement: neither side patches unilaterally before the
commons amendment lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 15:09:37 +02:00
25d9bc6fe8 test(portfolio): cover the CLI→run_portfolio wire for --verdict-dir (§11 gap)
The gap, found by the mutation sweep of 2026-07-25: verdict_dir=args.verdict_dir
→ None in run.py's execute_portfolio call left the suite 603/603 GREEN. The
flag was wired but not guarded — the inner merge (test_portfolio_learning_
loadbearing.py), the argparse refusal (--verdict-dir without --portfolio) and
the README↔--help sync all stay green under that mutation, so none of them
covered the forwarding itself.

One load-bearing test, no production code. It authors an expert verdict into a
tmp portfolio inbox — keyed on the bundle's own codes + measure type so it ranks
into the fold, with a distinct saving so its id cannot collide with the bundle's
seed — drives main(["--portfolio", …, "--verdict-dir", X]) with the scripted
client, and asserts the verdict's id AND a marker token (present nowhere in the
bundle) reach the proposer prompt.

Detach proof (mutation restored from a COPY, never git checkout): the wire
mutated to None → 1 failed, 603 passed, and the failure is this test alone.
Restored → 604 passed, ruff format left 71 files unchanged, ruff check + mypy
--strict clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 12:23:45 +02:00
8d5554ba3f docs(portfolio): spec-gap §7 i operatør-køen + paritetsrad 24/25 rettet
Coord-runde 2026-07-25, ingen kodeendring (gate urørt: 603 passed, ruff+mypy rene).

- brief §7: projeksjons-kontrakten over hovedboken som spec-gap, meldt uavhengig
  fra BEGGE stacker. MAF-søskenets ordlyd bevart verbatim; våre tre skjønnsvalg
  målt mot kode (runding av kronebeløp: ingen hos oss, én hos dem — reell
  divergens på identiske inndata). Anbefalt inn i D-A-bolken.
- paritetsplan rad 24/25: STALE «Gjenstår i MAF» rettet etter at MAF eksplisitt
  frigav vårt planverk. Rad 25 merket «hash ikke oppgitt — spurt» framfor antatt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 07:41:52 +02:00
bf87776bb3 feat(portfolio): stamp the producing SDK build in provenance (wiki-advisory F1) [skip-docs]
The advisory finding: provenance.py/artifacts.py stamped no SDK version, while
the SDK's total_cost_usd is a client-side ESTIMATE computed against a price
table frozen when the SDK was built. An untraceable estimate is a figure nobody
can check later, so the run now records which build produced it.

Provenance gains sdk_version: str | None. The value comes from the PRODUCING
CLIENT — getattr(client, "sdk_version", None) — exactly as model and cost_usd
already do, never from importlib.metadata at stamp time. That distinction is
the seam: a run driven by the scripted stand-in used no SDK at all, and
stamping the installed version there would attribute a build to a run that
never touched it (§1). SdkModelClient reads the installed build once from
package metadata (offline: no key, no network); every other client reports
null. A blank string is refused by the schema — null is the one way to say
"not produced by the SDK".

Scope note: this traceability covers OUR run cost only. The savings the
framework recommends are settled by the deterministic validator against the
golden suite, and no SDK estimate touches them.

Two seams, both detach-proven RED:
- make the stamp read importlib.metadata instead of the client → a scripted run
  claims a build it never used → red
- back-fill runs/s10/provenance.json → red

That second guard is the point of the change as much as the first. runs/s10/ is
the byte-frozen record of the ONE live run (2026-07-03), executed before this
field existed; the suite reads it nowhere else, so nothing would have caught a
retro-stamp. Adding a build id to it now would be a guess presented as
provenance. It stays without one, and the README says why.

run_s10.py is deliberately untouched (byte-frozen fasit script), and the field
defaults to None, so every existing caller and artifact shape is unchanged.

603 passed · ruff clean · mypy strict clean · runs/s10/ byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 06:57:30 +02:00
da93a68ce7 feat(portfolio): K12 — CLI parity, doc sync, knowledge-base recipe (parity row 24) [skip-docs]
The last ungated build session: the operator now drives the whole build from the
command line, and the documents claim exactly what the code does (§1).

run.py becomes the collecting entrance. Exactly one of --bundle (one project) or
--portfolio (N projects from a schema-validated reference config, with
--verdict-dir as the portfolio-level expert inbox) is required; both and neither
are refused. --goals loads a goal contract and checks it against --ledger's
realized sum BEFORE the first model call: the §8 caps bound spend, the goal bounds
achievement, so a hard target the book already meets stops the run at exit 4
without constructing a client. A soft target reached is a flag and the run
continues; an absent ledger is an empty book, so the goal is still evaluated,
never skipped. The one declared goal also drives --value-report's goal progress —
one contract, never two figures that can disagree.

The portfolio path persists nothing (K3 returns typed results; the outbox names
pairs by run_id, which a portfolio pass has none of). Rather than accept
--out/--outbox/--run-id/--value-report/--inbox/--live-dry-run there and silently
ignore them, the entrance refuses them and says why. run_portfolio is imported
lazily — portfolio.py imports this module, so a module-level import is circular.

Three seams, each detach-proven RED:
- unwire the goal check → the run proceeds and spends → red
- unwire the portfolio branch → the configured projects never run → red
- document a flag no CLI offers → the README honesty grep goes red

That last one is the doc-sync made load-bearing: the test reads README.md,
collects every --flag it documents (excluding third-party dev-tooling lines) and
asserts each exists in the --help of a CLI the README names. The drift it exists
to close was real — README claimed 562 tests, CHANGELOG claimed 265, actual 597.

Docs synced to the code: README gains an operator-CLI section and honest goal/
portfolio descriptions, CHANGELOG is rewritten to what actually shipped, and
docs/oppskrift-kunnskapsbase.md delivers D-H point 1 — the documented team
process for building a knowledge base, with the honest 1–2 week expectation and
every factory-dependent step (verdict translation, demo path) marked NOT BUILT.

597 passed · ruff clean · mypy strict clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 06:42:52 +02:00
4dcdd8017a feat(portfolio): K11 — per-run value report, pure projection over the three layers (parity row 25) [skip-docs]
The S5.4 analog: every run/portfolio pass can produce a deterministic value
report — modelled → expert-corrected → realized, goal progress, a quantified
learning effect, cost against value — with no model call, no clock and no new
state. It is a PURE PROJECTION over what is already persisted (K5 outbox pairs,
§4.2 inbox verdicts, K1 ledger), joined on the verdict_id K5 mints.

The honesty rule (§1) sets the shape, not the layout:
- approved      -> the claim stands        (quantified)
- rejected      -> the claim is void, 0    (quantified — an earned zero)
- approved_with_adjustment -> §4.2 carries NO adjusted amount, so the corrected
  value is UNQUANTIFIED, never back-filled with the claim
- no verdict    -> realized is UNMARKED, never zero-that-reads-as-judged and
  never the modelled figure
Partial quantification is counted in the output (2 of 4 …, 2 UNMARKED) rather
than summed into a full-looking total. Learning is measured, not asserted: a
rising approval share is reported only alongside the modelled→corrected gap that
shrank behind it, over cohorts split by run_id order. Cost (USD, a K6 upper
bound) and value (NOK) sit side by side and are never divided — no sourced FX
rate exists here, and a ratio would invent one.

Surfaces: standalone CLI (valuereport) and an opt-in --value-report on run.py,
which requires --outbox and is refused BEFORE any spend without one; the report
is written on both run outcomes and never rewrites the run's exit code (a budget
stop stays a budget stop).

Six seams detach-proven RED: honesty boundary (corrected + realized), gap
arithmetic, projection purity, run-seam wiring, pre-spend fail-fast, both-outcome
reporting. Fixtures are COMMITTED and generated with the real primitives, so the
id-join under test is genuine.

Note on the purity test: it was green-but-dead in its first form. Snapshotting
the committed fixture tree in place let an earlier test's stray write pre-seed
the 'before' snapshot, so the detached write reproduced it byte-for-byte. Every
test now projects from a per-test copy, and a pinned file-set test guards the
committed tree. Found by running the detach proof — which is what §11 is for.

portfolio.py is deliberately NOT wired: run_portfolio persists nothing, so there
is nothing for a projection to read. Its docstring now says that instead of
promising the wiring it did not get.

562 -> 584 tests green; ruff + mypy --strict clean over 27 src files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 06:25:02 +02:00
d0107e7f8c chore(deps): re-pin llm-ingestion-okf v0.3.1 -> v0.3.2
Door A pin bump to the tag released 2026-07-23 (f14c075), verified present on
the public open/ remote before bumping. v0.3.2 fixes frontmatter/index values
to be emitted verbatim (only source_query is whitespace-collapsed, per
ingest-spec §5) and types the NUL-byte path failure as SourceError.

Verified after the bump: 562 tests green, golden fixture byte-for-byte
identical (test_materializes_golden_byte_for_byte), mutation controls still
diverge so the golden test stays load-bearing, ruff + mypy --strict clean.
No error code changed, so the exc.value.code asserts stand unmigrated.

This confirms the library CHANGELOG claim that shipped golden fixtures and
both consumers are unaffected — for this consumer, as measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 06:07:35 +02:00
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