feat(p19): a direction must NAME the requirement that binds it, and have READ it

Two paid rounds scored 0 of 26 fasit concepts opened -- the same number twice.
P18 closed the navigation side (a listing is a window, an invented path is
refused by name) and it did not move, which makes it a ROLE question: nothing
in the loop ever asked the model to say what requirement binds the direction it
committed to, so opening one was never on the critical path to an answer.

A PREMISE OF THE ORDER WAS FELLED BEFORE ANYTHING WAS BUILT ON IT. A1 places
the demand in _INSTRUCTIONS[HYPOTHESISER_ROLE] alone. Measured: the stress
command sends --mandate and NOT --explore, the two are refused together by
name, and none of the nine round-1/2 outboxes holds a {run_id}-exploration.json
-- the hypothesiser never runs in a stress round, so A3 would have been
unreachable in exactly the paid runs this order commissions.

A2's own sentence resolves it: the refusal goes to the model "som en tur den
kan rette (samme mekanisme som quick_validate's nekt), ikke som en raise" --
and quick_validate IS a tool. declare_requirement therefore lives in
navigator_tools, held by BOTH roles that navigate (the exploration, and since
S2c the debate). It EXISTS only when the caller offers both sinks, which keeps
every pre-P19 call site byte-identical; one sink without the other is refused
at construction. 'opened' is the SAME list ExplorationToolRecorder fills, so
the refusal reads the run's own read trace.

The marked hypothesis carries 'requirement' as a REQUIRED key: omitted is a
hard error, explicit null is legal and needs 'why_none', a half-named one is
refused. A minted approach carries it; a seed never acquires one. The proposer
prompt names it only when the field exists, and the judge counts a hit against
THIS approach's fasit concepts, never against the base.

Load-bearing measured (12 arms), four mutations all red against the whole
suite, green control 1711/5 and demo-transcript.stdout byte-unchanged.
A-iii's predicted signature was FALSIFIED: the golden stays green because the
demo runs without a mandate, so _build_messages' approach branch is never
taken there. A-iv was GREEN first -- the repo's vacuous-gate class, 24th time:
the arm drove _attributable while the hit is computed at the call site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-15 01:21:47 +02:00
commit c84e8bf6f1
22 changed files with 903 additions and 35 deletions

View file

@ -43,6 +43,31 @@ from portfolio_optimiser.ir import AffectedItem, CostBaseline, SavingsProposal
OWN_PROPOSAL_ID = "own-proposal"
class BindingRequirement(BaseModel):
"""The ONE document in the knowledge base that BINDS a direction (P19 DEL A).
Measured over two paid stress rounds (P16 § 3, P18 § 1): **0 of 26** fasit concepts were ever
opened, in both rounds, while every run still produced proposals so a direction could be
committed to, quantified and validated without a single requirement of the corpus having been
read. P18 closed the navigation side of that (a window, a named refusal for an invented path)
and the number did not move, which is what made it a ROLE question rather than a ladder one:
nothing in the loop ever asked the model to name what binds it.
``path`` is bundle-relative exactly as a listing gave it the same string ``read_file`` takes,
so it can be checked against what the run actually opened without a second normalisation.
``ref`` is the requirement's OWN number as the document's frontmatter states it, never a
paraphrase: it is what a reader searches the corpus with, and what the judge matches the fasit
on.
Both are required with ``min_length=1``. A half-declared requirement would be worse than none:
it reads as a citation and points at nothing, which is the fabricated-provenance class
``write_concept_file`` refuses and ``RunFailure`` names in its own docstring.
"""
path: str = Field(min_length=1)
ref: str = Field(min_length=1)
class Approach(BaseModel):
"""One approach a domain expert wants evaluated for a project.
@ -84,6 +109,15 @@ class Approach(BaseModel):
#: The field is a ROUTING key, never a claim about content. It says which pipeline the approach
#: must be evaluated in, and ``route_by_bundle`` is the one place that reads it.
bundle_id: str = ""
#: The ONE requirement of the knowledge base that binds this direction (P19 DEL A).
#:
#: DEFAULTS to ``None``, which keeps every mandate written before today valid and dispatchable
#: unchanged — the ``bundle_id`` precedent, and the honest reading of a commission whose author
#: named no requirement. ``None`` is therefore "none stated", never "none exists": the loop
#: that MINTS an approach must say which of the two it means (``why_none`` on the hypothesis
#: line), because a direction the base has no requirement for is a finding, while one nobody
#: looked for is a silence.
requirement: BindingRequirement | None = None
class Mandate(BaseModel):