feat(p20): the requirement that is RIGHT, and a clause number that is not a price

Three seams, one commit: A, B and C touch the same four modules (run.py carries
the debate task, the grounding composition and the announcement; okf.py carries
one reference-number vocabulary read by both A and B), so splitting them into
three commits would have meant hunk-level staging of entangled files. Stated
rather than silently restructured.

A — the declaration answers with the DOCUMENT's own words. Measured: 13
declarations over round 3 and P17b, not one naming a fasit concept, while the
tool answered {"declared": true, ...} by echoing the caller's own arguments. It
now returns the document's title and req_number, read off Bundle.context_files
(so the type: verdict layer can never be named back), plus the sentence saying
what the declaration binds. A path the base carries as no concept answers with
empty strings rather than refusing. The commission's success_criteria now reach
the DEBATE task through mandate.criteria_block, the one renderer, empty when
there are none — which is what keeps every un-commissioned prompt, and the
golden, byte-identical.

B — a clause number is not a price. THE ORDER'S OWN RULE WAS FELLED BY
MEASUREMENT: it asks to refuse a code that IS declared req_number/prosessnr,
and neither of its two known positives is. n500 declares seksjon 10.4.1..10.4.4
but never the bare 10.4; r761 declares 2727 prosessnr and 2753 seksjon, none of
them 1.10.4, which occurs once, as prose ("iht. vegnormal N200 kap. 1.10.4").
The COMPLEMENT fires on both and closes the hole _ground_against_input already
admits in writing -- "it fails OPEN on a coincidental match". Unanchored run +
requirement-shaped code + the base declares a vocabulary + the code is not in
it -> refused, naming the denominator. All five of kontrakt-sorasen's real
process codes ARE declared and pass, which is what keeps the one context set
built on real codes measurable. Replayed over all 24 codes of round 3 + P17b:
exactly the two known positives flip validated -> rejected, 22 unchanged.

C — a parse failure no longer burns the round ledger blind. _fetch_parsed takes
a BUILDER instead of a finished message list, so the retry carries the parse
reason; measured, kontrakt-sorasen-04 spent 11 of 12 rounds re-asking the same
question. And announced_subject names the routed bases instead of saying "the
portfolio" for a two-base commission.

Suite 1807/5 (from 1781, +26, 0 removed), golden demo-transcript.stdout
BYTE-UNCHANGED (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f),
ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-15 06:02:46 +02:00
commit c8f0c8f7c4
12 changed files with 1098 additions and 37 deletions

View file

@ -481,6 +481,17 @@ when the seam is detached, so the loop cannot silently degrade into theater.
that does not use identifiers cannot be answered in them. A code the project's cost baseline
carries is never refused for its shape, because stage 0 has already ruled it a real line.
**A clause number is not a price.** A standard numbers its own requirements and a process code
numbers its own settlement posts, and both look exactly like a bare decimal: `10.4`, `1.10.4`,
`12.1`. When a run is **unanchored** — the knowledge base ships no cost baseline, so the
validator's stage 0 never runs — a cost code shaped like a clause number is refused unless it is
one of the reference numbers the base itself declares (`req_number`, `prosessnr`, `seksjon` in a
document's own frontmatter). The refusal names the denominator: *"not one of the 2765 this
knowledge base declares"*. A code the base does declare still validates, so a project priced in
real process codes is untouched; an input that declares no reference numbers at all cannot trip
the rule at all. `code_forms` reports the third value, `requirement`, for a code the base does
declare. Anchored runs are unaffected — stage 0 is the stronger falsifier and keeps the ruling.
**Naming the requirement that binds a direction.** Whoever navigates a knowledge base — the
exploration's hypothesiser and, since the debate started navigating, the proposer — is asked to
name the ONE requirement that binds the direction it commits to, and to declare it with
@ -491,7 +502,23 @@ when the seam is detached, so the loop cannot silently degrade into theater.
`"why_none"` — a base that holds no requirement for a direction is a finding worth stating, and
the field is never simply omitted. Where an approach carries one, the proposer's prompt names it
and asks for it back verbatim in `measure`, and the declaration is written to
`{run_id}-debate.json` / `{run_id}-exploration.json` under `requirements`.
`{run_id}-debate.json` / `{run_id}-exploration.json` under `requirements`. The reply gives back
**the document's own** `title` and `req_number`, read off the base rather than echoed from the
arguments, plus the sentence that says what the declaration binds — so a declaration of the wrong
requirement can be seen to be wrong. The route is named in the instruction as well as the
answer: pass `read_dir` a `filter` word from the approach's own label.
**What the commissioner counts as success reaches the readers.** A mandate's `success_criteria`
used to reach the announcement and nothing else. It is now restated verbatim in the debate's task
message — the prompt where `declare_requirement` is available — through one renderer, and is
omitted entirely when the commission states none.
**A reply that could not be parsed says why, once.** A malformed reply used to be retried with
the byte-identical prompt; measured, one paid run spent eleven of its twelve rounds re-asking a
question it had already answered the same wrong way. The next attempt's prompt now carries the
parse reason — only the reason, never the discarded JSON — as a block of its own, distinct from
a validator rejection (the numbers were refuted) and from expert feedback (a person objected).
The verbatim reply is still captured to `{run_id}-parse-failures.json` as before.
**Answering the plan review (`--plan-review`).** With `enable_plan_review` set, the exploration
stops before the loop is allowed to run and asks you to sign the plan off. `--plan-review`