P13 measured this lift and REFUSED it, because okf >=0.8.5 emits the ownership
stamp as the V1 flow mapping `generated: { by: process:okf-ingest, at: ... }`
where 0.3.2 emitted `true`, and `_carries_complete_ingest_stamp` read the new
form as NOT a stamp -- write_concept_file's forgery refusal would have shipped
DISARMED with the whole fail-closed suite green. That blocker is closed first,
red-first, and then the pin moves.
ROW 1, THE SECURITY HALF. `_claims_ingest_ownership` widens the predicate from
"reads as boolean True" to "claims ingest ownership", of which the boolean is
the pre-V1 spelling. The recogniser for the new half is `decode_flow_value` --
the module's ONE flow decoder, the same argument write_concept_file already
makes for `verified`: the writer refuses exactly what the reader can read. A
value the decoder REFUSES is therefore not an ownership claim and writes
through, which is what keeps this from collapsing into "any non-empty
generated". Two arms red before the fix; no YAML library introduced.
THE PIN. okf v0.3.2 -> v0.8.5, guard v0.3.4 -> v1.4.0 spelled `tag =`, not
`rev =`, and not the declared floor 1.2.0 -- both P13 premises hold and the
reason now lives next to the pin in pyproject.toml. The ":40" comment is
corrected: okf has ONE runtime dependency, the guard, and that is what binds
the two lines together. 27/27 imported names resolve across five modules.
THE GOLDENS, REGENERATED AS A DECISION. Seven concept files across four
examples/ingest-golden-* bundles, one line each. Two were regenerated by the
REAL materializer; the other five are derived (http/sql/mcp cannot materialize
outside the tests' stubs) and then MEASURED -- all four golden suites compare
byte for byte against what the stubs produce, and all four are green. The four
`generated == "true"` asserts now read ONE source, conftest.
expected_generated_stamp: four literals for one emitter fact are four places a
later release can leave half-corrected, which is exactly how the pre-V1 form
survived until P13 measured it. tests/test_okf.py keeps its literal on purpose
-- that one round-trips a CURATED half-stamp through our own writer.
THE BLOCK READER. Measured with the full denominator: all four delivered
knowledge bases write `sources` as a BLOCK sequence and none in flow form
(n100 446/446, n200 1133/1133, n500 270/270, r761 2756/2756 = 4605/4605), and
`evidence_for` reported `unreadable` on 4605 of 4605 -- the falsification layer
had no address for any document in any base. `okf.decode_block_mappings` is the
second CARRIER of one grammar, never a second grammar: colon-SPACE separator,
unquote_scalar, duplicate keys refused, SPEC 5.2's actor rule applied. okf's
consume.read_sources was READ for the form and not called; po calls no okf
reader, which is measured and deliberate. After: 4605 present / 4605 entries.
Reading is not a licence to WRITE -- the emitter is untouched and both writers
still refuse what decode_flow_value refuses.
THREE FINDINGS. (1) The first block reader INVENTED data on `- { k: v }` items
-- SPEC-canonical, and the shape tests/golden/block-form-provenance writes for
`verified` -- decoding it as `{'{ id': '...'}`. No arm caught it: the 5.2 actor
rule shielded the fixture by accident. Closed with a flow-decoder branch and
four new arms. (2) One of my own arms was VACUOUS, found by my own mutation M5:
it claimed to prove the colon-SPACE rule and stayed green under first-colon,
because the two rules agree on every delivered value. Renamed, labelled, and
the claim moved to the arm that actually witnesses it. (3) OPEN, and it needs
the operator: the commons-owned worked example declares its second concept
`unreadable`/`block-sequence`, which is now false for po. `shared/` is
pull-only, so closing it needs a commons amendment; the test asserts the
divergence instead of skipping it, keeping the discriminating half (the example
says two entries were seen and the reader returns exactly two).
NINE EXISTING ARMS REWRITTEN, NONE WEAKENED. All nine pinned "the block form is
unreadable" -- the behaviour this order changes. Each keeps its claim on a
specimen that is still unreadable for a reason of its own (5.2: an entry naming
no actor), or pins the REVERSED direction where the old arm stood so the change
cannot be silent. Two got STRONGER: multi-verified.md was authored for "a reader
keeping the last entry reports machine-confirmed for a concept a human signed",
and that could not be tested while the form was unreadable. Three node ids were
renamed; nothing was removed in substance.
Suite 1582 -> 1606 passed / 5 skipped. Both demo goldens byte-unchanged
(ea8c534... / ede3e2f..., shasum -a 1 of the CONTENT, never the git blob id).
ruff check / ruff format / mypy green. shared/ untouched.
Six mutations, all red against the WHOLE suite, each with its own signature:
row 1 detached (2) / block reader detached (17) / flow-item branch detached (7)
/ a stray indented line folds into an INVENTED entry (4) / separator becomes the
first colon (1 -- and that is finding 2) / the stamp expectation reverts to
"true" (4).
Order: 20260912T195112Z-995611104-from-.claude
Record: docs/2026-09-12-p13b-okf-bump.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
257 lines
12 KiB
Python
257 lines
12 KiB
Python
"""Shared e2e fixtures (Step 13): a scripted chat client that emits a SYNTHETIC UsageDetails
|
|
(so token accounting is real-shaped without an LLM), plus store + docs-dir fixtures.
|
|
|
|
The synthetic ``UsageDetails`` is what lets the budget meter / provenance ``token_usage`` be a
|
|
positive, UsageDetails-sourced number in CI — the REAL-provider populated-usage assertion is
|
|
the gated live arm (Step 14).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Callable, Sequence
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from agent_framework import BaseChatClient
|
|
|
|
from portfolio_optimiser.reference_domain import load_reference_projects
|
|
from portfolio_optimiser.run import RunResult
|
|
from portfolio_optimiser.simulation import ScriptedChatClient
|
|
from portfolio_optimiser.verdicts import VerdictStore, seed_store
|
|
|
|
|
|
class SyntheticUsageChatClient(ScriptedChatClient):
|
|
"""The scripted-list-then-default test double — now a THIN subclass of the canonical
|
|
``ScriptedChatClient`` (S2.5 consolidation). It keeps its full PUBLIC surface (the
|
|
``default_reply=`` kwarg constructor, the ``call_count`` attribute, ``model``/OTEL
|
|
``"synthetic"``) but delegates the shared ``_inner_get_response`` body to the canonical — the
|
|
scripted-list-then-default behaviour lives in its selector."""
|
|
|
|
def __init__(
|
|
self,
|
|
scripted: Sequence[str] | None = None,
|
|
*,
|
|
default_reply: str = "ok",
|
|
tokens_per_reply: int = 8,
|
|
) -> None:
|
|
scripted_list = list(scripted or [])
|
|
counter = {"i": 0}
|
|
|
|
def _select(_blob: str, _role: str) -> str:
|
|
i = counter["i"]
|
|
counter["i"] = i + 1
|
|
return scripted_list[i] if i < len(scripted_list) else default_reply
|
|
|
|
super().__init__(
|
|
reply_selector=_select, default_reply=default_reply, tokens_per_reply=tokens_per_reply
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def make_client_factory() -> Callable[..., Callable[[str], BaseChatClient]]:
|
|
"""Return a maker that builds a per-role client factory emitting synthetic usage."""
|
|
|
|
def _make(default_reply: str, *, tokens: int = 8) -> Callable[[str], BaseChatClient]:
|
|
def factory(role: str) -> BaseChatClient:
|
|
return SyntheticUsageChatClient(default_reply=default_reply, tokens_per_reply=tokens)
|
|
|
|
return factory
|
|
|
|
return _make
|
|
|
|
|
|
_DEFAULT_CLAIM = 20_000
|
|
# Last-resort reply for a prompt naming NO known reference project (the anchored per-project
|
|
# fallback below cannot be built then). Kept for that case only.
|
|
_PORTFOLIO_DEFAULT_REPLY = (
|
|
'{"measure":"Reduce scope","affected_items":'
|
|
'[{"code":"01.1","quantity":1,"unit_cost":100000}],"claimed_saving_nok":20000}'
|
|
)
|
|
|
|
|
|
def _anchored_default_replies() -> dict[str, str]:
|
|
"""A VALID default proposal PER reference project, quoting that project's OWN first cost line
|
|
verbatim (S4.0): since the road path anchors the validator to ``project.cost_items``, a generic
|
|
reply carrying an invented magnitude for code ``01.1`` is now — correctly — rejected as a
|
|
fabricated cost line. Anchoring the fixture is the fix; weakening the gate is not.
|
|
|
|
``claimed_saving_nok`` stays ``20_000`` for every project, exactly as the single generic reply
|
|
claimed before, so every ledger/goal/budget assertion built on that figure is unchanged. Each
|
|
project's first line is ``01.1 Rigg og drift`` at >= 480 000 NOK, so P90 (>= 144 000) clears the
|
|
claim on every project."""
|
|
replies: dict[str, str] = {}
|
|
for project in load_reference_projects():
|
|
line = project.cost_items[0]
|
|
replies[project.id] = json.dumps(
|
|
{
|
|
"measure": "Reduce scope",
|
|
"affected_items": [
|
|
{"code": line.code, "quantity": line.quantity, "unit_cost": line.unit_cost}
|
|
],
|
|
"claimed_saving_nok": _DEFAULT_CLAIM,
|
|
}
|
|
)
|
|
return replies
|
|
|
|
|
|
class _ProjectAwareUsageChatClient(ScriptedChatClient):
|
|
"""Selects its reply by scanning the incoming prompt for a known ``project_id`` substring (the
|
|
prompt embeds ``project.id`` at run.py:162 and generate.py:48), falling back to a default valid
|
|
proposal — so ``run_portfolio``'s single ``client_factory`` stays production-shaped while tests
|
|
vary the proposal per project. A THIN subclass: the prompt-scan lives in its selector, the shared
|
|
``_inner_get_response`` body in the canonical.
|
|
|
|
The fallback is itself project-aware (S4.0): a prompt naming a reference project gets that
|
|
project's baseline-anchored default reply, so an un-mapped project still produces a proposal the
|
|
anchored validator admits. Only a prompt naming NO known project falls through to
|
|
``default_reply``."""
|
|
|
|
def __init__(
|
|
self, replies: dict[str, str], *, default_reply: str, tokens_per_reply: int = 8
|
|
) -> None:
|
|
table = dict(replies)
|
|
anchored = _anchored_default_replies()
|
|
|
|
def _select(blob: str, _role: str) -> str:
|
|
explicit = next((r for pid, r in table.items() if pid in blob), None)
|
|
if explicit is not None:
|
|
return explicit
|
|
return next((r for pid, r in anchored.items() if pid in blob), default_reply)
|
|
|
|
super().__init__(
|
|
reply_selector=_select, default_reply=default_reply, tokens_per_reply=tokens_per_reply
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def make_portfolio_client_factory() -> Callable[..., Callable[[str], BaseChatClient]]:
|
|
"""Return a maker that builds a single project-aware client factory: every client it
|
|
produces picks its reply from ``replies`` by scanning the prompt for the project id, so one
|
|
factory serves the whole portfolio (matching ``run_portfolio``'s single-factory seam)."""
|
|
|
|
def _make(
|
|
replies: dict[str, str],
|
|
*,
|
|
default_reply: str = _PORTFOLIO_DEFAULT_REPLY,
|
|
tokens: int = 8,
|
|
) -> Callable[[str], BaseChatClient]:
|
|
def factory(role: str) -> BaseChatClient:
|
|
return _ProjectAwareUsageChatClient(
|
|
replies, default_reply=default_reply, tokens_per_reply=tokens
|
|
)
|
|
|
|
return factory
|
|
|
|
return _make
|
|
|
|
|
|
class _RecordingChatClient(ScriptedChatClient):
|
|
"""Records the incoming prompt blob per call into a SHARED sink, then returns a fixed valid
|
|
reply. Lets a test assert exactly what text reached the prompt — the probe the Step-1 ExpeL
|
|
wiring is made load-bearing against (does a prior verdict reach the hypothesis prompt?). A THIN
|
|
subclass: the canonical records to the ``sink`` (when given one) and returns the constant reply."""
|
|
|
|
def __init__(self, sink: list[str], reply: str, *, tokens_per_reply: int = 8) -> None:
|
|
super().__init__(reply, sink, tokens_per_reply=tokens_per_reply)
|
|
|
|
|
|
@pytest.fixture()
|
|
def make_recording_client_factory() -> Callable[
|
|
[str], tuple[Callable[[str], BaseChatClient], list[str]]
|
|
]:
|
|
"""Return a maker that builds a per-role client factory recording every prompt blob into a
|
|
shared list. Returns ``(factory, recorded_prompts)`` so the test inspects what reached the
|
|
prompt across the whole run (debate rounds + generation)."""
|
|
|
|
def _make(reply: str) -> tuple[Callable[[str], BaseChatClient], list[str]]:
|
|
sink: list[str] = []
|
|
|
|
def factory(role: str) -> BaseChatClient:
|
|
return _RecordingChatClient(sink, reply)
|
|
|
|
return factory, sink
|
|
|
|
return _make
|
|
|
|
|
|
@pytest.fixture()
|
|
def fresh_store() -> VerdictStore:
|
|
return VerdictStore(verdicts=[])
|
|
|
|
|
|
@pytest.fixture()
|
|
def seeded_store() -> VerdictStore:
|
|
return seed_store()
|
|
|
|
|
|
@pytest.fixture()
|
|
def docs_dir(tmp_path) -> str:
|
|
d = tmp_path / "docs"
|
|
d.mkdir()
|
|
(d / "cost.txt").write_text(
|
|
"Asphalt Ab11 unit rate renegotiation reduced the paving cost on the school stretch.",
|
|
encoding="utf-8",
|
|
)
|
|
return str(d)
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# Fase 1b — the FULL-RUN contract, in ONE copy (kø-(p): a second copy of an assertion drifts).
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
#: What the paid run must fell, stated as an assertion rather than as prose. The open honesty limit
|
|
#: after økt 37 is narrow and specific: *"that the emitted structured schema IS accepted by the LIVE
|
|
#: endpoint is NOT verified — the tests prove conformance with the DOCUMENTED subset, not
|
|
#: acceptance."* So the contract asserts schema ACCEPTANCE, never model JUDGEMENT.
|
|
#:
|
|
#: The discriminator is an artefact this repo already owns: ``{run_id}-parse-failures.json`` is
|
|
#: written if and ONLY if some reply failed to parse (økt 35 invariant — "the file's presence is the
|
|
#: signal"). Its ABSENCE beside a RunResult therefore proves that every generation reply came back
|
|
#: in the requested shape, which is exactly what "the live endpoint honoured the schema" means.
|
|
#:
|
|
#: ``validator_decision`` is the second half: it mirrors the VALIDATOR alone (never the checker), so
|
|
#: reading it proves the deterministic gate actually ran on a parsed candidate. Both ``validated``
|
|
#: and ``rejected`` satisfy the contract — a P90 rejection is a run that CONCLUDED, and demanding
|
|
#: ``validated`` would be asserting that the model reasons well, which no schema can promise and
|
|
#: which one paid run could not establish anyway.
|
|
def assert_full_run_contract(result: RunResult, outbox_dir: str | Path, run_id: str) -> None:
|
|
"""Assert the Fase 1b full-run contract on a completed ``run_project`` result.
|
|
|
|
Two things, and deliberately nothing else:
|
|
|
|
1. **No parse-failure artefact** — every reply parsed, i.e. the live endpoint accepted the
|
|
emitted ``response_format`` schema. This is the honesty limit being felled.
|
|
2. **The validator was reached and decided** — ``provenance.validator_decision`` is one of the
|
|
two decisions the deterministic gate emits.
|
|
|
|
Deliberately NOT asserted: ``checker_verdict``, token counts, ``validated_count``, or the
|
|
content of the proposal. Those are model-judgement claims, and one run cannot carry them.
|
|
"""
|
|
artefact = Path(outbox_dir) / f"{run_id}-parse-failures.json"
|
|
if artefact.exists():
|
|
# Quote the evidence in the failure message: the whole point of økt 35 was that the operator
|
|
# should never again have to guess WHY a reply did not parse.
|
|
raise AssertionError(
|
|
f"the endpoint did NOT honour the structured schema — {artefact.name} exists.\n"
|
|
f"{artefact.read_text(encoding='utf-8')[:2000]}"
|
|
)
|
|
assert result.provenance.validator_decision in {"validated", "rejected"}, (
|
|
"the deterministic validator never decided — the run did not reach the gate with a "
|
|
f"parsed candidate (validator_decision={result.provenance.validator_decision!r})"
|
|
)
|
|
|
|
|
|
#: P13b: the §7 ownership stamp as the PINNED WRITER emits it. ONE copy, shared by the four ingest
|
|
#: tests that assert the emitted value, because four literals for one emitter fact are four places a
|
|
#: later okf release can leave half-corrected (the kø-(p) rule, and exactly how the pre-V1 form
|
|
#: survived in four asserts until P13 measured it). The actor token is okf's, not ours: measured
|
|
#: 2026-09-12 against llm-ingestion-okf 0.8.5, which writes `{ by: process:okf-ingest, at: <the
|
|
#: ingested_at the caller passed> }` where 0.3.2 wrote the literal `true`.
|
|
#:
|
|
#: A curated half-stamp written BY THIS REPO (`okf.write_concept_file`) is a different fact and keeps
|
|
#: its own literal — `tests/test_okf.py` round-trips `generated: true` and must not be pointed here.
|
|
def expected_generated_stamp(ingested_at: str) -> str:
|
|
"""The exact ``generated`` value okf's materializer writes for ``ingested_at``."""
|
|
return f"{{ by: process:okf-ingest, at: {ingested_at} }}"
|