Operator ruling 2026-08-05, which settles decision (g): planning documents are
generally never public, and what OUR OWN sessions generate does not go out on
the forge at all. The example itself stays public so others can run the
process.
`.claude/projects/` is the Voyage session workbench -- 25 briefs/plans/reviews
this project's own sessions produced. Untracked and gitignored, exactly as
STATE.md already is, and for the same stated reason: this repo has a public
mirror, so that class of material is local-only rather than tracked.
The line is drawn at who wrote the document, and it is drawn deliberately:
`docs/plan/`, `docs/research/` and `docs/rapport/` stay tracked. Those are
curated, dated documents written for the repo's readers, three of them linked
from the README as the decision record. Move that line if it was meant wider.
Two files were NOT process artifacts and are not deleted. Both
`build_fixture.py` scripts are cited by tracked tests
(`test_ingest_golden_sql.py`, `test_ingest_golden_http.py`) as the documented
rebuild path for byte-exact goldens -- reproduction code that had landed in the
wrong directory. Moved next to the goldens they build; both docstrings updated,
so no tracked file is left pointing into an untracked tree (verified: the only
remaining `.claude/projects` string in a tracked file is the .gitignore rule
itself). One prose reference in the dated Foundry auth recipe was dropped for
the same reason.
652 tests still pass.
Does NOT address the 27 of these already readable on open/ since the S12
release -- untracking stops future publication only. That retraction is a
separate operator decision and is deliberately not taken here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWsexbQjPo9rsV3aUE54ZS
Measured before writing: the commands existed but were scattered across the
mode partition, and there was no path a newcomer could walk end to end. The
capability gap that made a complete offline walk impossible is closed in
3abc61b; this is the door onto it in the README.
Five steps, each RUN FROM A FRESH CLONE before being written down (git clone +
uv sync + uv run pytest -> 652 passed): read the knowledge base, watch the
learning loop close, run the loop with your own scripted answers, watch the
validator say NO, and price a real run before spending anything. Step 4 is the
one that was missing entirely -- the demo only ever showed a yes, and a
refusal carries far more weight than another approval.
Also documents that `Rejection (..., decision=approved)` is not a
contradiction: the first is the validator's outcome, the second echoes the
human's recorded verdict. Documented rather than changed -- altering a public
output format is the operator's call, not a side effect of writing docs.
`costsim` gets its first mention in the README at all; it was finished, tested
and completely invisible from the surface.
Also points the commons link at open/ (published 2026-08-04). NB: the
repo-standard v0.3.0 register still lists 19 repos and does not know that repo,
so pointing at the correct live URL now trips a false LINK-DEAD. Reported to
org-ops; the URL answers HTTP 200.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWsexbQjPo9rsV3aUE54ZS
An adopter without an API budget had two half-doors and no whole one.
`--live-dry-run` takes their own bundle but stops before the first model call
(`run_project` returns a DryRunReport), while `portfolio_optimiser.simulation`
runs the complete loop but only over ITS bundle with ITS scripted answers.
The seam for the missing third case -- the whole loop over your OWN data,
offline -- already existed as `run_project(client_factory=...)` and had zero
CLI exposure. This is the door onto that one seam, not a second implementation
of it (`scripted_factory` is imported lazily; `simulation` imports `run`, so a
module-level import would be circular).
The honesty banner is part of the feature, not decoration (maalbilde §1): a
scripted run that reads like a model run is worse than having no offline mode,
so every scripted invocation prints what is real (context navigation, debate
plumbing, deterministic validator, verdict) and what is not (the answers).
The two offline modes are mutually exclusive rather than one silently winning,
`--report` mode refuses the new flag by allowlist, and a replies file that
cannot serve the run is refused at the door rather than surfacing as a KeyError
mid-run.
Load-bearing MEASURED against the whole suite (645 -> 652), six mutations all
red: detach the wiring · detach the banner · detach the dry-run exclusivity ·
drop the flag from the --report allowlist · make the loader tolerant · control
(print the banner unconditionally).
The --report blade was measured GREEN first: with a non-existent ledger path
the load failure refused before the gate and masked it entirely. Rewritten
against a valid saved ledger, so rc 1 can only come from mode-exclusivity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWsexbQjPo9rsV3aUE54ZS
CONTRIBUTING.md told strangers to fork and "open a Pull Request". Measured
against the forge itself, that door is shut: the API reports
has_pull_requests=false / has_issues=true for open/portfolio-optimiser, and
the org's published CONVENTIONS.md states the position -- "Issues velkommen
som signaler. PRs ikke akseptert. Fork-and-own er anbefalt adopsjonsmodell."
Replaces the PR workflow with the two routes that are actually open (issues
as signals, fork-and-own), and keeps the engineering standards the section
carried -- load-bearing tests, blocking validator, ruff/mypy, Conventional
Commits -- reframed as what the project holds itself to, which is what a
forker needs and what an issue is weighed against.
Also points CLAUDE.md's commons link at open/portfolio-optimiser-commons
(published 2026-08-04). The `commons` git remote still resolves via ktg/ and
is deliberately left alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWsexbQjPo9rsV3aUE54ZS
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
git-subtree-dir: shared
git-subtree-split: ddaae5d637ba4ee8425291d99cfe5c8f7b632001
Two rules existed. verdicts._unquote took quotes off correctly; the
bundle_context title renderer stripped only `"`. Measured before the fix:
`title: " Spaced "` rendered as `## concept: Spaced ` (the whitespace
half kø-(a) named), and `title: 'Single'` rendered its quotes verbatim —
both ordinary YAML a hand-authoring curator writes, and both reach the
agent's read-context.
The (p) defect class: a duplicated conversion drifts, and the drifted copy
decides something. Same fix shape — ONE source, owned by the module that
owns parse_frontmatter. okf.unquote_scalar is now the rule; verdicts
delegates by identity, so the structural key that _mint_id hashes is
unchanged.
The commons-owned nav-goldens could never have caught this: every golden
title is double-quoted with no inner whitespace, so both rules render them
byte-identically. That is asserted as a control, and it goes RED if a
future golden gains a discriminating title.
Load-bearing MEASURED against the whole suite, three mutations:
- weaken the renderer back to .strip('"') -> ONLY the 2 new rows red,
643 others green (incl. nav-goldens) = it covers ground nothing did
- reintroduce a private _unquote copy in verdicts -> only the identity
test red, 644 green (the copy is behaviourally identical TODAY, which
is exactly why identity is the only thing that catches the class)
- (control, kø-(n)) add `import numpy` to a second src module -> the
existing test_semretrieval_is_the_sole_numpy_importer goes red alone,
confirming that gate is live rather than green-but-dead
638 -> 645 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVMsih6tXx39VMyq6wJ5H7
docs/extending.md claimed SEMANTIC_WEIGHT_DEFAULT = 0.5 long after the code
lowered it to 0.25. It was found by accident while editing the neighbouring
line; nothing in the suite would ever have caught it. This is that gate.
Both bounds were MEASURED, not assumed:
- SCREAMING_CASE discriminates exactly. Across every numeric `name = value`
code span in docs/ it selects the 2 real module constants and rejects all
10 kwargs/locals (max_attempts=3, concurrency=3, realiseringsgrad=0.79).
Ordinary prose stays freely editable, so the gate has no reason to be
switched off.
- Dated documents are observations, not contract. A spike finding or a July
review records what was true when measured; rewriting it to track the code
would falsify the record.
Measuring also rewrote the ambiguity rule: SEMANTIC_WEIGHT_DEFAULT is bound
in both semretrieval and run (a re-export), so a "same name in two modules"
check would have been RED on today's code. Only DIVERGENT values are refused.
Fail-closed throughout, per write_concept_file / read_spend: an unknown
constant name is an error rather than a skip, and a document that is neither
listed live nor recognisably archived goes RED asking to be classified —
otherwise a new guide would be silently unguarded.
Load-bearing MEASURED against the whole 638-test suite:
- drift the DOC (the original defect) -> only this gate goes red; the other
637 stay green, so it covers ground nothing else did
- drift the CODE -> this gate and the semretrieval weight gate both go red
- make an unknown name tolerant -> red
- drop the only citing doc from the live list -> red (twice: coverage and
classification)
- add a new unclassified guide -> red
- (control) remove this gate entirely, code still drifted -> the adjacent
semretrieval gate still goes red, so nothing is masked in either direction
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9AAyWtMqr4HjftKaegTtS
Ran `repo-standard` (v0.1.1, class `standalone`) and fixed everything it
flagged as ERROR, plus the WARN links that were genuinely dead.
README first screen:
- opening line is now byte-identical to the forge description, so
description == catalog == README is machine-checkable (badges moved below).
- `## Install` (required for class `standalone`): clone + `uv sync`, stated as
clone-only because the shared spec, persona skill and example bundles under
`shared/` are read from the working tree at run time. `uv run pytest` named as
the verification, with the fact that no CI runner exists said out loud rather
than implied by a badge.
- `## Non-goals` (required): the five limits already binding in CLAUDE.md —
not a compliance product, not a portfolio-level reallocator, not autonomous
decision-making, not turnkey, not a model benchmark.
Dead relative links (measured, not guessed):
- `docs/plan/2026-07-10-sesjonsplan-fase2-6.md` pointed at
`../2026-07-14-revisjonspakke-DF-DI.md` six times; the file sits in
`docs/plan/`, not `docs/`. (The sibling `../review-2026-07.md` links are
correct and untouched.)
- the Fase-1 spike brief linked repo-root-relative from
`.claude/projects/…/`; re-anchored with `../../../`.
The one remaining README ERROR was a gate false positive: `checkInternalLinks`
resolves targets against `git ls-files`, which lists files only, so a link to a
directory can never resolve. `[shared/](shared/)` now points at
`shared/README.md` — a better target anyway, since that file carries the
pull-only subtree rule. Not fixed here: the classifier lives in another repo.
Remaining WARNs are all inside `shared/`, deliberately untouched: it is a
pull-only commons subtree, and the nav-golden files are byte-level fixtures
that gate `test_nav_golden_*` — four of them are OKF bundle-internal links,
and the `/etc/passwd` ones are the negative escape fixture doing its job.
Suite green: 630 passed, 4 skipped (markdown-only diff; no test touched).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri3aVJPfynCZtHRhesCzUH
`cosine`'s docstring claimed its guard was load-bearing because "a NaN reaching the
ranking sort key would corrupt ordering silently rather than failing loudly" — but the
guard tested `norm == 0.0` only, which a NaN or inf norm passes straight through. The
claim was prose, not behaviour.
Measured, not assumed: `cosine(unit, nan_vector)` AND `cosine(unit, inf_vector)` both
returned `nan`, and a NaN sort key made ranking INPUT-ORDER-DEPENDENT — six permutations
of the same three candidates produced four distinct orderings. That defeats the total
order `HybridRanker` documents ("`id` makes the result independent of input order").
Refuse rather than coerce, and deliberately NOT symmetric with the zero-norm branch: a
zero vector is a legitimate handled state (`FakeEmbedder` returns `np.zeros` by design),
whereas a non-finite component only ever means the INJECTED embedder is broken. Scoring
it `0.0` would launder that into "no semantic similarity" while ranking proceeded on a
forged signal — validation, never repair, mirroring `read_spend`.
Reachable via the documented `Embedder` extension point, not the shipped fake; scoped to
the norms (90% principle — a finite-normed dot-product overflow is not chased).
Also corrects `docs/extending.md`, which stated `SEMANTIC_WEIGHT_DEFAULT = 0.5` while the
code has said `0.25` since the weight was lowered.
625 -> 630 tests. Load-bearing MEASURED against the WHOLE suite, five mutations all red:
detach the guard entirely · coerce to 0.0 instead of raising · check only the first norm ·
drop "non-finite" from the message · (control) detach the zero-norm branch, which fails
ONLY the zero-norm test — the new guard does not mask the existing one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018V9vNBmxAmgJ2JMoHByiHS
Documents the anyio.fail_after-vs-asyncio.wait_for finding and the
cancelled_caught ownership gate, alongside the existing kø-x task-group
invariant it extends.
Advisor review of the prior commit (5269b7d) found the except TimeoutError
branch was wider than the brief asked for: builtin TimeoutError is also
socket.timeout (3.10+) and asyncio.TimeoutError (3.11+), so any TimeoutError
reaching that clause got relabeled mcp_timeout regardless of source. Gate on
anyio.CancelScope.cancelled_caught instead, mirroring _unwrap_ingest_error's
ownership rule (own it, wrap it; otherwise, untouched).
Measured: no live trigger exists today (MCP's own internal read-timeout
converts to McpError before reaching us; a server-side TimeoutError becomes
an ordinary isError result) -- pinned with a synthetic test raising from
StdioServerParameters construction, inside our fail_after scope but before
either nested task group, so it arrives ungrouped. Four mutations red against
the full 625-test suite: drop the translation, revert to asyncio.wait_for,
relabel the code, and drop the cancelled_caught gate.
Also promotes anyio to a declared direct dependency (was transitive via mcp
only) -- ingest_mcp.py now imports it directly.
asyncio.wait_for cancelling stdio_call_tool's run() from outside the anyio
task groups it awaits (stdio_client, ClientSession) never surfaced a
TimeoutError: measured against a real hanging server, the mismatch produced
an anyio.BrokenResourceError wrapped in a BaseExceptionGroup instead. Moving
the deadline to anyio.fail_after, nested inside both task groups, lets
anyio tear down its own structure cleanly and raise a plain TimeoutError,
which is now translated into IngestError(code="mcp_timeout") alongside the
mcp_tool_error/mcp_non_text_content family.
BudgetExceeded carries kind/limit/observed as ONE structured stop event, but only
`observed` was undefended. Measured against the whole suite before writing anything:
four of five raise sites (TokenMeter.charge, tick_round, and BOTH arms of exhausted())
could report any value at all without a single one of 621 tests noticing. Only
PortfolioMeter.check was covered.
What hid it: spikes/_harness.py carries its OWN copy of BudgetExceeded/TokenMeter, so
the spike suite's `observed` assert never touched the shipped module — the production
tick_round had no direct test whatsoever.
exhausted() is the only site that CHOOSES a ledger (the S3.4 pre-call guard), so a
refusal naming portfolio_tokens while reporting the run's own spend would misdirect
every reader of it. Both arms are pinned with observed != limit on purpose: at
exactly-exhausted the two coincide, and a test written there would pass on an
implementation that echoed the cap back as the spend.
No defect in the values themselves (unlike kø-x and kø-p) — the triple was coherent at
all five sites; the gap was purely coverage.
Load-bearing MEASURED: nine mutations, all red — five observed mutations (including the
control) and four echo mutations. 621 -> 623 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LW749xcXQmVEgdipB6KNm4
Two quantization orders existed and met at exactly one comparison.
SavingsLedger quantizes every realized candidate to integer øre and sums the
ints; run.py's goal baselines summed Project.total_cost FLOATS across items and
projects and quantized the total once. _goal_limit_if_reached compared the
former against a threshold derived from the latter — so whether a portfolio pass
stops early was decided by two differently-computed sides.
Measured divergence: three 60000.005 NOK lines are 18000003 øre quantized first
but 18000001 summed first (the float sum drifts to 180000.01499999998).
Decision: quantize per cost line, then sum integers. Each CostItem IS a money
amount — S4.0 made per-line quantity/unit_cost the validator's ground truth — and
integer addition is associative, keeping totals order-independent under the D-D
wave model, which the float fold is not.
ledger.to_ore is now the framework's one NOK->øre conversion; run.py imports it
rather than keeping a private copy (the S4.0 REPLIES precedent).
Measuring the mutations found two further gaps, both now closed: the per-project
baseline is a SECOND call site whose mutation survived the whole suite, and
realize bypassing to_ore with a raw float*100 was caught by nothing.
Load-bearing MEASURED (tests/test_money_quantization_loadbearing.py), five
mutations all red: detach the portfolio baseline · detach the per-project
baseline · reintroduce a private copy in run.py · change the rounding mode · let
realize bypass to_ore. 615 -> 621 tests.
Honesty boundary: sum_claimed_saving_nok (run.py:_aggregate) is deliberately
untouched — a float NOK reporting field that is never quantized and never
compared against the ledger, hence outside the ordering defect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WiY53sm8JFqk7NN75g5wRS
`stdio_call_tool` shipped never having been executed end to end — docs said so
explicitly. Running it found a real defect: `stdio_client` and `ClientSession` are
each an anyio task group, and anyio re-packages anything leaving one in a
`BaseExceptionGroup`. Both errors the transport raises from inside the session
(`mcp_tool_error`, `mcp_non_text_content`) therefore reached callers as exception
groups, never as the `IngestError` the whole Door A path catches and switches on by
`code`. No canned-tool test could see this: they never enter a task group.
`_unwrap_ingest_error` recovers the owned error and re-raises it; anything unowned is
re-raised untouched, so this narrows an exception group rather than blanket-catching.
Duck-typed on `.exceptions` because `except*`/`ExceptionGroup` are 3.11+ and this
project supports >=3.10.
Verified against a REAL server subprocess (a local process costs no model tokens, so
the repo's cost discipline is untouched; the contract tests still spawn nothing):
`examples/ingest-golden-mcp/` + `tests/test_ingest_golden_mcp.py` — byte-identical
golden extraction mirroring the http/sql goldens, plus the tool-error and
missing-`server_ref` branches.
Also recorded: a server on the ingest path must expose a NULL-ARGUMENT tool, so
`datasource.build_mcp_server` cannot serve it (`retrieve_cost_docs(query)` has a
required parameter, verified to return an error result). The two are separate seams
by design.
Load-bearing MEASURED, five mutations all RED: detach the unwrap · detach
`initialize()` · make the error code generic · detach the `isError` branch · change
one byte of the served body.
612 -> 615 tests. ruff + format + mypy clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WiY53sm8JFqk7NN75g5wRS
Every stage of validate_proposal reasoned only about numbers the proposal itself
supplied, so an internally-consistent hallucination cleared the whole gate (F3).
A new stage 0 reconciles each affected_item against the project's CostBaseline
before the CBC solve: an unknown cost code is rejected, and a real code carrying
a quantity/unit_cost outside the configured tolerance (5% default, relative to
the baseline value) is rejected. Validation, never repair.
The baseline argument is OPTIONAL (None = pre-S4.0 behaviour), but both run
paths set it: the road path projects project.cost_items, the bundle path loads
cost-baseline.json when the bundle ships one. Bundles written before the
amendment stay un-anchored, so the commons-owned goldens run byte-identically;
a baseline that exists but is malformed still raises on both loaders.
F8: the method-specific cap now comes from the METHOD_CAPS registry (measure
type -> fraction, injectable) instead of an energy_efficiency string comparison.
The baseline format and tolerance semantics were decided locally — the commons
amendment (D-A pt. 2) never arrived, exactly as in S3.2. D7 mirroring stays open.
Three portfolio fixtures quoted cost codes belonging to OTHER projects; the new
gate caught them. They now quote each project's own lines, and the two copied
REPLIES tables import the single source instead of drifting from it.
Load-bearing measured (tests/test_s40_cost_baseline_loadbearing.py), six
mutations all red: detach the reconciliation stage; detach the magnitude
tolerance; detach the road wiring; detach the bundle wiring; ignore the injected
cap registry; make the optional loader tolerant of malformed content. Control:
with the road wiring detached the repaired portfolio fixtures still pass, so
they are not masking the seam. 597 -> 612 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdwK7bQ4BZkWH4t8MRDKb4
seed_store_from_bundle keyed EVERY `type: verdict` file on bundle_candidate_features — the single
candidate the bundle's validator-input.json describes. A bundle carrying verdicts about several
candidates collapsed them onto one key, so a verdict about candidate B scored a perfect structural
match against candidate A's query and could be folded into A's hypothesis prompt. The ExpeL
substrate was single-candidate by construction.
A verdict file may now carry its own structural key in frontmatter (affected_codes / measure_type /
claimed_saving_nok); absent, keying falls back to the bundle candidate, so every pre-S3.2 seed keeps
working unchanged. promote_verdict writes the three fields, so a promoted verdict — frequently about
a different candidate than the target bundle's projection — does not impersonate that candidate.
Semantics decided HERE, not pulled: commons' seeding rule (method-spec §3 Steg 1 + bundle example)
has not arrived; we said we would build locally first. D7 mirroring stays open.
- ALL THREE fields or none. A partial declaration raises VerdictFrontmatterError rather than merging
with the bundle candidate, which would mint a key belonging to NEITHER candidate. Validation,
never repair (mirrors write_concept_file); the tolerant-skip rule belongs to the RAW inbox layer.
- claimed_saving_nok parses via json.loads — the SAME literal rule the IR projection went through —
and is written back with str() of the raw value. _mint_id hashes that value, so 30000 and 30000.0
are different keys; a normalising writer would split one candidate's signal across two ids.
- The structural key is signal-free, so it does not weaken the Step-8 no-leak property (Test C green).
Load-bearing MEASURED, five mutations all red: detach per-verdict keying · detach the fields
promote_verdict writes · make a partial/unparseable key tolerant · normalise the magnitude on write ·
remove the fallback (control — breaks the step1 suite at collection, proving the fallback bears load).
589 -> 597 tests. Full gate green (pytest, ruff, mypy).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkjvTTxrg9LTrmghebfiij
Two tightenings, each measured by a detached-mutation run:
(1) The validator now blocks a claim above the CBC nominal feasible, in ADDITION
to the P90 stage. Neither dominates the other: an upward-skewed assumption band
lifts P90 ABOVE nominal -- so P90 alone passed review counterexample #1 (claim
100k, nominal 90k, band [0.70, 1.40], measured P90 121057) -- while a
downward-skewed band pushes P90 below it. Independent gate, same Rejection type,
existing rejections keep their existing reason.
(2) An assumption band must enclose its item's unit_cost (low <= unit_cost <=
high, inclusive). A band that misses it states a different price rather than an
uncertainty, and every Monte Carlo draw would then sample away from the item's
stated cost. Checked exactly where the Monte Carlo looks bands up -- per affected
item, by code; a band keyed to no affected item is never sampled and so has no
unit_cost to enclose.
The premise was re-verified against ground truth before building on it, not
taken from STATE: 05.2 unit_cost 215 in (200,230), 03.1 310 in (290,330),
ENERGI-TOTAL-EL 1.0 in [0.70,1.40] and (0.8,1.2). No fixture violates it.
The LLM path already catches ValidationError as a meter-bounded retry
(generate.py:138), so the new invariant cannot crash a run.
Mutations, all RED: detach the nominal block; drop the model_validator
decorator; make the enclosure strict. tests/test_bygg_energi_mikro.py and the
commons golden are UNCHANGED and green -- the regression proof.
586 -> 589 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DDXwUqHVAQQeYE7X1TXy5
Three items on one seam — what a FAILED project does to the wave loop — plus the
snapshot copy they sit next to.
(v) The catch is BaseException, not Exception, and that width was ungated. The
existing collect-and-continue test raises RuntimeError, so it stays green when
the handler is narrowed: measured, the whole of test_portfolio_concurrent_
loadbearing.py (13 tests) passes under the narrowing. asyncio.CancelledError is
the one realistic vector that separates the two — probed first, gather(
return_exceptions=True) COLLECTS it, while KeyboardInterrupt propagates
regardless and could never be helped by a wider catch. Narrowed, a cancelled
member is cast into runs as a fake RunResult and the pass dies in _aggregate,
pointing away from its cause. RED measured.
(t) sum_token_usage excludes a failed project's spend, and that is the honest
answer, not a bug: a run that died before producing a stamp has no provenance,
and inventing one is the fabrication RunFailure exists to avoid. What needed
gating is that those tokens still reach the ledger the global cap is enforced
against — otherwise a repeatedly-failing project burns budget while the meter
reads clean. Pins meter.spent as the pass's real cost, sum_token_usage as the
completed-run subtotal, and their difference as exactly the failed spend. RED
measured against the likely "fix" (sourcing sum_token_usage from the meter),
which is wrong because a seeded meter also carries EARLIER passes' spend; 21
existing budget/portfolio tests stay green under it.
(s) _wave_snapshot uses dataclasses.replace, so a field added later is carried
without touching the function. Not cosmetic: measured, dropping retriever by
hand-enumerating left all 585 tests green — the Step-2 coverage its docstring
credited no longer existed, so the S3.1 retriever seam could be downgraded
mid-pass in silence. Now gated by a property test derived from
dataclasses.fields (not a field count, the shape rejected earlier). The explicit
verdicts copy is retained and separately gated: replace(store) alone shares the
caller's list and takes the byte-identical determinism test RED.
strict=True on the zip is documented as deliberately untested — measured green
when dropped, since gather is built from exactly snapshots, so a test could only
go red by manufacturing a mismatch and would exercise zip rather than this pass.
The new double is registered in the S2.5 consolidation guard's delegating-
overrides list rather than the guard being weakened; it already delegates via
super()._inner_get_response, which test_delegating_overrides_call_super now
enforces on it.
583 -> 586 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbgTCEZma764i1rHTrzceU
The hole: `read_http`'s default transport is the library's `urllib_get`, which invokes the
stdlib opener with no `timeout=`. urllib's documented fallback is then the process-wide default
socket timeout — `None` out of the box — so an http source that accepts a connection and never
answers hangs a run indefinitely. That contradicts the invariant that nothing runs unbounded.
The spec text for S2.4 ("a timeout parameter on `_urllib_get`") could NOT be followed literally:
that function is UPSTREAM library code (`llm_ingestion_okf.connectors`, pinned v0.3.1, pull-only),
signature `(url, credential) -> str` — measured, not assumed. Same failure class as S2.2's
"implement it in `ingest.py`": spec text that says "change X" has to be checked against whether
X is ours at all.
So the fix goes in FRONT of the library: `timeout_get` scopes `socket.setdefaulttimeout` around
a delegate call to the library's own `urllib_get`, and `materialize` now hands the library that
wrapped transport instead of letting it resolve its own untimed default. This meets S2.4's own
verification criterion — a bound WITHOUT a second socket path — and avoids duplicating the
credential-header logic. An explicitly injected `http_get` is passed through UNWRAPPED: a
caller-owned transport (MCP fronts a subprocess with its own `timeout_seconds`) keeps its own
policy, and a process-global side effect is not ours to impose on it.
Honest limit, carried in the code comment, the test docstring and `docs/extending.md`, not just
in the commit: the default socket timeout is PROCESS-global. Under `concurrency=k` the runner is
asyncio on one thread, so the scoping holds; driving `read_http` from a thread-pool executor
would make it unsafe.
Half of S2.4's scope was already delivered upstream — transport failures are categorised as
`SourceError(code="http_transport")`. Coarser than the plan envisaged, but not ours to rewrite.
Two pre-existing guards went red on the first pass, both on PROSE only: `ingest.py` must not
contain "urlopen" (no forked connector) or "ingest_mcp" (AST-guarded mcp-free). No code violated
either — my docstrings merely named them. The guards were left exactly as strict as they were and
the prose was reworded; weakening a real guard to save a comment is the trade this repo refuses.
578 -> 583 tests. Five mutations MEASURED red (restored from scratchpad + `shasum -c` each time,
never `git checkout`):
1. remove the timeout scoping entirely -> RED
2. apply the bound AFTER the delegate call -> RED
3. set the bound but never restore it (no finally)-> RED (the unconditional control)
4. hand the library a bare None again (pre-S2.4) -> RED (the wiring)
5. make the wrapping unconditional -> RED (the conditional control)
Mutations 1 and 2 take ~10s to fail rather than failing instantly: that is the loopback test's
join deadline expiring. It is the measurement that the bound actually BITES — a black-hole
listener on 127.0.0.1 that completes the handshake and never answers, run on a daemon thread so
a detached seam fails an assertion instead of hanging the suite forever. Every other assertion
here only proves we set a global; that one proves the global does something.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdLGwd33vqhkToh98Ym34P
Commons settled on 2026-08-01 that MCP is an extension of the `http` source
family, not a fourth family (`shared/ingest-spec.md` §4). This implements it
with ZERO schema change and zero spec amendment.
The transport discriminator lives in `base_url`, not in new manifest fields:
the shared library rejects unknown manifest keys fail-fast, and we consume it
pull-only at a pinned v0.3.1, so `server_ref`/`tool` as fields would have meant
a spec amendment plus a library release. It buys nothing — the library already
joins `base_url` + `/` + `query`, so `mcp+stdio://<server_ref>` + `<tool>`
reproduces exactly the two-part structure the (now stale) reference plan wanted.
Staying inside the family INHERITS what a fourth family would have had to write
and could have forgotten: the §8 network grant (measured to fire before any tool
call), the `max_rows` cap, §5 verbatim fenced rendering, and the §7 provenance
stamp. The discriminator gates rather than labels — `mcp_get` refuses a URL it
does not own, so an MCP transport can never quietly serve an `https://` manifest
and leave the bundle's provenance claiming a transport that was never used.
Parsing is string-based, not `urlsplit`-based: `urlsplit().hostname` lowercases
the host, which would silently break the case-sensitive env lookup `server_ref`
depends on.
`ingest.py` is untouched — it is AST-guarded mcp-free, so the transport lives in
its own module and is opt-in at the call site. `ingest_mcp.py` imports the open
`mcp` protocol client but never `agent_framework`, keeping the seam D7-portable.
Load-bearing, six mutations all measured RED: detach the scheme guard · make the
refusal unconditional · swap parsing to `urlsplit().hostname` · skip non-text
content instead of raising · force `allow_network=True` · smuggle in a MAF
import. Both source files restored byte-identical (`shasum -c`) after each.
Honesty: `stdio_call_tool` (the real stdio path) is written but never executed
end to end — every test injects a canned tool call, so the suite spawns no
subprocess and opens no socket. No golden fixture, and MCP stays unwired in the
optimiser run path. Stated in docs/extending.md rather than implied away.
555 -> 578 tests; ruff + mypy green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112FPR5TX6pDLiNicBPzE8i
The spec arrives by subtree pull from commons, so drift is something we RECEIVE
rather than author. The existing guard only asserted the section HEADINGS were
present: a pull could empty a section of its normative content and stay green.
Measured, not assumed — with `escape, not depth` deleted from Step 1, the old
skeleton test still passes (M5 below).
Two new guards, one file:
- Rule level (test 6): 25 normative rules, each bound to the section that OWNS
it, so a phrase surviving elsewhere in the document does not count as the spec
still stating the rule. Selection is principled rather than taste — every entry
anchors a §11 conformance seam or a CLAUDE.md invariant. Matching is
whitespace- and emphasis-insensitive: re-wrapping a paragraph upstream is an
editorial change, not a rule change, and must not cost a false red.
- §11 table (test 7): the conformance table still names the seams this suite
implements, and every test it cites exists here. One-directional by design —
the repo carries 26 load-bearing tests, the spec anchors 10, because the spec
governs the METHOD, not this repo.
Five measurements, all as expected: M1 delete a rule → RED · M2 RELOCATE a rule
out of its section while leaving it in the document → RED (this is what the
section binding buys) · M3 drop a seam row from §11 → RED · M4 harmless re-wrap
+ de-bolding → GREEN · M5 the old skeleton test does NOT catch M1 → GREEN.
Spec restored from a byte copy after each mutation, shasum verified.
Known gap, deliberately not patched here: §11 has no row for the portfolio-wide
budget seam (S3.4/F10) — the pulled spec predates it. That is a commons
amendment; a consumer editing shared/ is exactly the drift this guards against.
553 → 555 tests, ruff + mypy green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aw9TECznT5b5H6374CBTvV
PortfolioBudget + PortfolioMeter carry ONE token ledger over a whole portfolio
pass -- and, seeded from a persisted spend file, across passes -- while the
per-run Budget/TokenMeter pair is untouched. Three enforcement points, each
doing a different job:
- startup: a remainder that cannot fund one run raises BudgetRefused before
anything loads (a pass that can afford zero projects is a caller mistake,
not a result);
- wave assembly: an unfundable project is NEVER STARTED and the pass stops
structurally (budget_stop + stopped_early, completed runs preserved).
Because every member of a wave is funded against the SAME pre-wave
remainder, admission RESERVES each member's requirement -- otherwise a wave
of k over-commits the cap by up to k runs;
- pre-call: BudgetMiddleware refuses a call the remainder cannot pay for
instead of making it. The post-charge check stays: real usage is only
knowable after the response, so the guard stops the NEXT call, never the
one in flight.
budget_stop is its own field rather than a widened stop_reason -- a goal-stop
is success, this is resource exhaustion, and fusing them would make "we
stopped" unreadable. PortfolioMeter splits record/check so tokens the provider
already billed reach the ledger even when the same charge breaks the run's own
cap. read_spend raises on corrupt content (our own accounting state, unlike
the tolerant RAW inbox layer); write_spend takes a REQUIRED stamp with no
wall-clock default, mirroring promote_verdict.
Load-bearing MEASURED, not asserted -- 6 mutations, all red: detach the wave
check; detach the pre-call guard; detach the wave reservation; check the run
cap before crediting the global ledger; detach the startup refusal; make
read_spend tolerant. Files restored from shasum-verified copies after each.
Two findings worth keeping: the pre-call guard MASKS a detached wave check if
the test asserts on overspend (spend stays under the cap either way), so the
load-bearing assertion had to become failures == () plus never-started; and
the token arithmetic is probed (32 tokens/run at tokens=8), not guessed.
537 -> 553 tests, ruff + mypy green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EaxFnaDAbMQkmTeX4u7sd
The commons pull (7aa53fc -> a2b57d2) rewrote method-spec §3 Step 1 and added two §11
seams. Measuring okf.py against the new normative text found six contradictions; this
closes all six, gated by the commons-owned nav-goldens that came with the pull.
method-spec §3 Step 1 (navigate_bundle / bundle_context):
- follow cross-links RECURSIVELY, depth-first in first-seen order (there was no
recursion at all — only the root index's links were read, so no hierarchy was
navigable even with the other fixes in place);
- resolve a leading `/` against the BUNDLE ROOT, anything else against the LINKING
file's directory, and drop the retired "a path separator means out-of-bundle"
heuristic, which conflated depth with escape and forbade valid nesting;
- de-duplicate on the RESOLVED path (`./a.md` == `a.md` == `/a.md`), which is also
what terminates cycles;
- exclude index files by BASENAME at every level, so a nested index is navigation and
never renders as content (flat rendering regardless of depth);
- bind index_summary to the ROOT index alone.
safe_resolve stays the sole in-/out-of-bundle test, fail-closed: a target that fails to
resolve for ANY reason is skipped, never raised.
ingest-spec §3 (write_concept_file): it is the repo's one authoring primitive that
materialises a concept file from caller-supplied frontmatter, so it now refuses the
COMPLETE ownership stamp (`generated: true` + `ingest_manifest`) with IngestStampError,
while permitting either field alone. A validation, never a repair — nothing is written.
Gates (tests/test_okf.py, 529 -> 537):
- nav-golden-hierarchy and nav-golden-escape compared against the shipped
expected-read-context.md fasit (trailing-whitespace normalisation only, which the
fixture README explicitly permits; internal blank-line structure stays gated);
- traversal order pinned separately from the rendered output, so a right-looking render
from a wrong walk still fails;
- unit seams for the recursion in isolation, resolved-path dedup, and the leading-`/`
rule's breach case (a real out-of-bundle file addressed by its absolute path).
Load-bearing MEASURED, not asserted: seven mutations each go red — detach the recursion,
restore the separator prefilter, dedup on the raw target, read `/` as filesystem-absolute,
render nested index bodies as content, drop the stamp guard, and the fully naive navigator
with no boundary check (which is what makes the `/`-trap test bite). okf.py restored from a
checksum-verified copy after each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WetWTHpdRbqinN5XHFTaTb
The commons pull (7aa53fc..a2b57d2) retires "a path separator means out-of-bundle"
in method-spec §3 Step 1. test_navigate_skips_null_byte_link stayed GREEN, but its
docstring explained itself via that heuristic ("slips past the `/` pre-filter") —
a live test citing dead doctrine. Behavior and assertions are unchanged; only the
reason is re-anchored on what the new spec makes load-bearing: a target failing to
resolve for ANY reason is skipped, not raised (§11 row "Navigation boundary").
Measured, not assumed: this was the only rotted rationale. The three other
separator mentions (okf.py:26-27, :125, test_okf.py:164) describe what the code
actually does or rest on safe_resolve, which survives the retirement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxGWaoDYb6vmzd7EPfYkqS
Closes OQ1 (SC8). At k>1 every project in a wave reads the WAVE-START store,
so a verdict captured by project A does not reach project B's hypothesis
prompt inside the same wave — sequentially it would. Deliberate: the snapshot
is what removes the append race, and restoring intra-wave visibility would
restore the completion-order dependence the barrier exists to eliminate.
Learning flows ACROSS wave boundaries, not within them; concurrency trades
learning granularity for wall-clock.
The shipped fixture cannot show this (the fold is bundle_dir-gated and no
reference project sets bundle_dir), which is a property of the fixture and not
of the design — so it is pinned on the road-k + bundle-k+1 pair via the
existing load_reference_projects monkeypatch seam, no new production seam.
k=1 (two waves) carries the sentinel; k=2 (one wave) does not; store content
stays identical across k. Each half is the other's control.
Detach measured: remove _wave_snapshot -> RED on three tests including this one.
Also corrects two stale docstrings that outlived Session 1's finding: the
module header and the Step-2 test still named a sorted() in _merge_wave as the
detach point. There is no sorted() there, and a project_id sort would BREAK
the k=1-identity contract rather than protect it. A docstring naming a detach
point that does not exist is the green-but-dead defect this file exists to
prevent, so both now name the snapshot and mark the plan's claim as measured
wrong.
_wave_snapshot gains the honesty line about copying exactly two fields, with
the reason no field-count guard was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vYbqW4MppACRvPhEMDpvF
Documents the wave-model reading of the existing Step-8 goal semantics (a
reading, not a redesign — Session 1 already moved the checks to wave assembly
because the executor required it) and pins it with three tests.
Goal checks run at WAVE ASSEMBLY, per member: a HARD per-project goal removes
the pid before the wave starts, so an excluded project is never STARTED and
leaves no verdict behind; a HARD portfolio goal stops the pass with the
assembled wave still crossing its barrier; SOFT flags and continues.
Detach points measured:
per-project goal keyed per WAVE, not per member -> RED (membership diverges
at k=3, unaffected at k=1)
import threading under src/ -> RED (AST guard)
promote_verdict called on the run path -> RED (tripwire)
Corrects one claim the plan and my first docstring both implied: checking the
goal MID-WAVE does NOT make membership completion-order dependent.
_goal_limit_if_reached reads only the ledger, contract and baseline, all
invariant during a pass, so a later check reaches the same decision.
Determinism of membership is the ONE-WRITER rule's (C3, now a tripwire test),
not the check's placement; placement buys the never-started property. The
docstrings say this rather than claiming a detach point that does not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vYbqW4MppACRvPhEMDpvF
One project's exception no longer cancels its siblings: the wave's gather
runs with return_exceptions=True and each raised project becomes a frozen
RunFailure(project_id, error, error_type) in the defaulted
PortfolioResult.failures, while every completed project keeps its full
RunResult. RunFailure is a distinct type rather than an error field on
RunResult (six required non-defaulted fields -> dummies would be fabricated
provenance); the deviation from the spec's wording is stated in both
docstrings. TaskGroup is rejected: it cancels siblings on first exception.
snapshots reaches _merge_wave unfiltered, and results are paired back to pids
by POSITION (gather resolves in argument order) so a mid-wave failure cannot
disturb store order or misattribute the failure.
Detach points measured, not asserted:
drop return_exceptions=True -> RED (both new tests)
reorder snapshots before merge -> RED (+ Session 1's determinism test)
pair results by sorted(), not position -> RED (failure misattributed)
filter failed members before merge -> GREEN, measured
The last one corrects the plan: its carried-forward requirement implied
filtering before the merge was the hazard. Filtering preserves relative order,
which is all _merge_wave consumes, so the variant is undetectable AND harmless.
The docstring now names the reorder as the detach point and records the
filter asymmetry, rather than claiming a detach point that does not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vYbqW4MppACRvPhEMDpvF
Replaces run_portfolio's sequential loop with a wave loop over _waves(ids, k):
each wave takes a per-project snapshot of the shared store, runs the wave under
one asyncio.gather in a single event loop, then crosses a merge barrier that
folds each project's NEW verdicts back in wave-submission order. Step 2's
contract goes GREEN; runs stays in project_ids order because gather resolves in
argument order, not completion order.
TWO PLAN CORRECTIONS, both found by the RED-first test rather than by reading:
1. The plan specified sorting the merged verdicts on `project_id`. Measured, that
produces a deterministic order which is the WRONG one: lexicographic gives
BRU/FV42/RV13 while the sequential pass gives FV42/RV13/BRU. It satisfies
"deterministic" while breaking "identical to concurrency=1" — and the second is
the actual contract. The merge preserves submission order instead.
2. The plan named the barrier's sort as the load-bearing seam. It is not — with
per-project snapshots the wave list is never reordered by completion, so a
sorted() there would re-sort an already-ordered list and read as a guard while
guarding nothing. The SNAPSHOT is the half that carries the load. Rather than
ship a decorative sort, both halves were measured (scratchpad-restore, never
git checkout):
detach _wave_snapshot -> RED (store lands in completion order)
detach merge ordering -> RED (reversed wave order diverges)
Both restored byte-identical (sha 42b01d46).
The snapshot carries `retriever` across deliberately: dropping it would silently
downgrade a caller-owned store's S3.1 semantic-retrieval opt-in mid-pass.
Also strengthens the scripted-client consolidation guard, which the probe broke by
being a legitimate third _inner_get_response def-site. It pinned a literal count
of 2 — the wrong shape: it failed on any new legitimate subclass while still
passing if someone pasted a duplicated body into an already-listed file. It now
pins the property (registered sites, scripted-lineage overrides must delegate via
super(), foreign-lineage doubles must genuinely be foreign). Verified load-bearing:
removing both delegation sites turns it RED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQapztREtC2mkr5oU811pr
Commits the determinism contract RED, mirroring S3.1 (b05747a RED -> 6618f67
GREEN). The wave executor does not exist yet, so `concurrency=3` still runs the
sequential path — and the test says so precisely rather than passing vacuously:
max in-flight was 1: concurrency=3 never overlapped two projects
call sequence=[FV42 x4, RV13 x4, BRU x4]
That blocked call sequence is the proof the probe works. A test that had gone
GREEN here would have been asserting something the sequential path already
satisfies — the Spike B call-counter tautology repeating — and the plan's
escalation rule would have stopped execution.
The probe returns a coroutine yielding N times via asyncio.sleep(0) (a pure
scheduler yield, no wall clock) before delegating to the canonical body, with N
read from the prompt blob since client_factory's argument is the ROLE, not the
project id. The stream path is delegated untouched, and the subclass chain is
preserved so BudgetMiddleware still engages.
Three self-checks make it non-vacuous by construction: max-in-flight > 1,
completion order != submission order, and >= 3 distinct verdict ids. Self-check
1 asserts non-identity rather than an exact reversal — asyncio.sleep(0) is a
scheduler yield, not an ordering primitive, so demanding a specific permutation
would only add a way to fail for a reason that is not the contract.
The docstring also corrects what the repetitions guard: NOT hash-seed
nondeterminism (PYTHONHASHSEED is fixed per interpreter, so every in-process rep
shares one seed), but scheduler nondeterminism under real concurrency.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQapztREtC2mkr5oU811pr
Add keyword-only `concurrency: int = 1` to `run_portfolio`, a module-level
`_waves` partitioner, and a fail-fast on k < 1 that precedes any project load.
The execution loop is deliberately NOT changed here. At the default k=1 every
wave holds a single project, so the wave partition reproduces the existing
`for pid in ids` order by construction — the parameter is inert until Step 3
wires the executor. The 5 tests pin exactly that: order preservation across
every k, nothing dropped or duplicated, and the k<1 raise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQapztREtC2mkr5oU811pr