Commit graph

12 commits

Author SHA1 Message Date
938a1ca30e feat(row6): a proposal whose approach declared no requirement is unsupported
Stress round 6 validated three falsification arms, and every validated
approach rested only on run-level declarations nobody can attribute to one
approach. declare_requirement now takes a required approach_id (a mandate
id or own-proposal; an unknown id is refused naming the valid ones), and a
ValidatedProposal whose approach has neither a mandate requirement nor a
declaration under its own id becomes validator.Unsupported - a Rejection
subclass carrying the validator's own ruling, reported as `unsupported` in
coverage, the outcome artefact, the settlement and the judge, and never
counted or summed. The rule is active whenever the debate held the
declaration tool, the micro base included; the road and pre-pass paths are
untouched. Declaration quality is not judged, so the rule can be satisfied
by declaring any document the run read.

The v1 gate's row 6 probes pass; its artefact half reads IKKE MÅLT because
stress round 6 predates approach-addressed declarations, and IKKE MÅLT is
never green - it fails the exit code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 16:40:54 +02:00
0a81de2d76 feat(p22): stage 0's unknown-code refusal NAMES the codes the project buys
P21 was the first ANCHORED stress round and it bought something real -- must_refuse 5/5, all
five caught on stage0-baseline. It cost something measured just as clearly: 0 of 20 approaches
validated, and 26 of 26 rejections (20 approach rows plus 6 own-proposals -- a LARGER population
than the 20) read `unknown cost code '<invention>': not in project P's cost baseline (5 known
codes)`. The model invented signalregulering_konstruksjon, VENTIL_IMP, RIGG01, baerelag_asfalt
and 22 more, and it could not have done otherwise: the price schedule reaches the VALIDATOR and
never the proposer, and the refusal stated the COUNT of known codes, not one name. Step 5 feeds
that sentence verbatim into the next attempt -- and "you guessed wrong, there are five right
answers" carries nothing to correct towards.

The contrast already lived in the same stage: the MAGNITUDE half NAMES the baseline value, and
that is the half that let the loop converge in session 94. This gives that property to the other
half, in one place, and Step 5 carries it forward for free.

The window is a FIXED COUNT of whole codes (20), never a share, and it counts codes rather than
characters because a character cut can sever a code mid-name and hand the proposer an identifier
that exists nowhere. The cut is announced; a schedule that fits is not marked truncated; the
order is the schedule's own.

Measured with denominators: every cost baseline in this repo or its measured corpora is at most
six codes, and the largest real delivered price schedule measured is K2's prissammenstilling at
14 priced rows. Nothing measured reaches the window; it exists for the R761-style mengdebeskrivelse.

Load-bearing MEASURED (tests/test_named_known_codes_loadbearing.py, 10 arms), eight mutations all
red against the WHOLE suite + green control 1873/5 (from 1863/5, superset, 0 removed) and golden
demo-transcript.stdout BYTE-UNCHANGED (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f):
A1 revert to the bare count (7 red) - A2 a share instead of a fixed window (6) - A3 sorted (1) -
A4 silent cut (2) - A5 character slice (3) - A6 no bound at all (3) - A7 break the rejection_stage
marker (2, one an OLDER independent witness) - A8 grow the magnitude half with a code list
(2, one an OLDER independent witness).

Honesty limits, stated: no paid run yet confirms this changes the outcome live (that is DEL D);
the truncation branch is exercised only synthetically because nothing measured reaches the window;
and the schedule still does not reach the prompt, so the first attempt guesses as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 22:52:37 +02:00
7b4f85d77c feat(p21): the PROJECT carries the price, so a run against a road normal can be anchored
Four paid stress rounds ran entirely UN-ANCHORED, all of them, because the one file
loader reads cost-baseline.json out of the BUNDLE and no vegnormal ships one: N100,
N200, N500 and R761 are knowledge, and knowledge carries requirements, never amounts.
The validator's stage 0 -- the one stage that tells an invented cost line from a line
this project actually buys -- was skipped in every single run, so "validated" could not
mean what it says. P20 G1/G2 measured real R761 process numbers (12.11 three times on
Soraasen, 1.1.1 on Lindaas) validating with amounts nobody had anywhere.

--cost-baseline FILE is PM decision (e), taken over the three alternatives P20 wrote
down. A LOADED object, never a path (prepass_payload's rule): the CLI owns the file and
loads it ONCE, so the notice, the stamp and every base of an --across-bundle pass all
descend from one read. ONE parse, two doors -- load_cost_baseline delegates to
load_cost_baseline_file -- while safe_resolve stays on the bundle door alone, because a
project's own schedule is legitimately outside every base. No tolerant twin: this path
exists only because an operator NAMED a file.

DEL B: five anchored context sets, a1-a3 with their line and a4 with none, so stage 0 is
what catches the falsification arm. THE ORDER'S OWN ARM (h) WAS FELLED BY MEASUREMENT:
"no baseline code is a requirement number the base declares" is measured 0 of 4 on the
project-coded sets and 5 of 5 on kontrakt-sorasen -- which is what R761 Prosesskoden IS,
a bill of quantities priced BY process code. The complement keeps both, and the order's
own mutation still bites.

DEL B3: the judge reports anchored (off the run's own stamp), priced per row, and WHICH
falsifier caught the falsification arm.

Load-bearing MEASURED, five mutations all red against the WHOLE suite, green control
1850/5 (from 1809/5, superset, 0 removed), golden byte-unchanged:
A3(i) the flag is read but the baseline is unused (3 red) . A3(ii) only the first base
gets it (1) . A3(iii) report_forbidden drops it (1) . B2(i) a4 gets a line (1, arm (g)
alone) . B2(ii) a code swapped to 12.11 (2, arms (f) and (h)).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:49:10 +02:00
c8f0c8f7c4 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>
2026-09-15 06:02:46 +02:00
d74f32dc1c feat(p19): a cost code must have a FORM where the input offers forms
P18's round 2 ended with two VALIDATED proposals whose affected_item codes
were ordinary words from a road standard's prose -- impulsventilator (4 of
270 N500 documents) and bituminoest baerelag (4 of 1133 N200). Both are
grounded in P7's sense and neither is inert in P18/B1's sense; they are simply
not identifiers of a cost line, and the gate had no stage that could say so.
Known positive MEASURED, not asserted: replayed offline against the bases
those runs were given, both come back Rejection naming the denominator.

IDENTIFIER_FORMS moved from generate.py to validator.py: they now drive both
P8's report and this gate, and two copies of "what an identifier looks like"
would let the two disagree about one run's own input.

B1 -- two new forms, transcribed from measurement. R761's requirement numbers
are bare dotted numbers and all six refs in kontrakt-sorasen's fasit are of
that shape, which neither pre-P19 form matched: r761's whole offer was 3
identifiers over 6.5 MB, and is now 2332. The FIRST form was widened in the
same pass because B2 made these forms decide prose vs identifier, and this
repo's own ENERGI-TOTAL-EL matched none of them -- a gate may only be wrong in
the direction that admits too much.

Three things keep the gate from being a rule about shapes: the generality
guard (it fires only where the input offers forms), the baseline exemption
(stage 0 has already ruled that code real), and full-matching.

Honesty limit, measured and given its OWN arm: a decimal and an R761 process
number are typographically identical, so the form counts both. P8's existing
"bare numbers" arm is narrowed to bare INTEGERS accordingly.

Measured over all nine round-1+2 outboxes: 26 of 36 codes are prose.

Load-bearing measured (22 arms), four mutations all red against the whole
suite (16 / 10 / 1 / 2), green control 1734/5 and the golden byte-unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 02:11:29 +02:00
7a7c988253 feat(p18): an identifier that stands everywhere identifies nothing
P18 parts B and C (order 20260914T105139Z).

B1 -- stage 0b. P7 made it `item.code in grounding`: plain containment over
ONE concatenated string. P16 ran it against a delivered corpus and measured
what containment cannot tell apart: the falsification arm a4-indeksregulering
put 250 000 NOK on a single cost line coded R761 -- the knowledge base's OWN
NAME, carried by all 2 756 of its concept documents -- and the whole gate
said validated (stage 0 skipped, un-anchored run; checker approve).

The grounding is now carried as the DOCUMENTS it is made of (validator.
Grounding), not as a blob. A structure and not a second argument beside the
text: the boundaries and the text are one fact, and .text is derived, so the
gate and P8's report measure the same characters. run.py composes one
document per concept file where the base is already walked; generate.
_grounding_text folds each cost line in as a one-line document.

N and A are MEASURED, not chosen (14.09, four mounted vegnormal bases):
- every must_cite ref and mandate affected_code in the four context sets --
  shortest real identifier is FOUR characters (12.1, 52.1), so N = 3 sits one
  below the measurement and cannot refuse anything measured;
- document frequency of every code-shaped token per base -- 1 692 distinct
  and NOT ONE reaches 5 %. Highest anywhere 6/446 (1.35 %), highest a fasit
  names 3/446 (0.67 %), R761 2 756/2 756 (100 %). A = 0.05 therefore sits
  3.7x above the highest real token and 20x below the defect.
Length is NOT what makes the defect inert (R761 is four characters); the
share is. And a share is not a measurement without a denominator big enough
to take one (ansikt 4): one of three is 33 %, so an ABSOLUTE floor of 10
documents gates it. Highest absolute count any real identifier reaches is 6,
and every fixture in the repo is far below 10 -- which is why every pre-P18
gate is UNTOUCHED by this rule rather than exempted from it. Grounding.of
(one document) can never reach the floor by construction.

The refusal NAMES the denominator ("appears in 2756 of the 2756 documents
this run was given"), because Step 5 feeds that reason verbatim into the next
attempt's prompt: a proposer told only "ungrounded" answers with another
token of the same kind.

B2 SPIKE (measured, NOT built) FELLED the order's own alternative: option (b)
"ground in what the run OPENED" was run over P16's 16 code rows -- R761
stands in every OPENED document too, so (b) would NOT have caught the defect,
while B1 makes it inert and still grounds the real process line 65
ASFALTDEKKER (29/2756 = 1.05 %). (b) is not a substitute for B1.

C1 -- --docs-dir is optional once --bundle-dir is given (P16 FUNN 2). On the
bundle path docs_dir is never read: retrieval, the chunk tool and the "no
citable content" check all live in the road branch. Bound ONCE from
--bundle-dir, which is byte-identically what the README already tells an
operator to type by hand. NOT the "--docs-dir omvei": no such path is opened
and the road branch still refuses without a real --docs-dir (own arm).

C2 -- the judge's snippet arm counts only under citation_scope == "narrowed",
as (a) already does (PM decision, P16 s 6.2). P16's reason for (b') being
clean -- snippets are bodies while ref/title live in frontmatter, 0 of 446
n100 bodies -- holds for "Krav 4.1.2-1" but NOT for R761, where a process
number like 12.1 stands in the bodies. Under a whole-base citation list that
mark was "cited" before any model call.

tests: test_inert_identifier_loadbearing.py (7 arms; known positive is P16's
OWN artefact replayed against the base that run was given, known negative is
26 of 26 fasit references still grounding), test_docs_dir_optional_
loadbearing.py (5 arms). test_stress_judge_loadbearing.py's snippet arm split
into narrowed/whole-base -- the pair is the discriminator, same snippet, same
mark, only the scope differs. The grounding tests migrate from str to
Grounding.of (the honest reading of a caller that declared no boundaries).

Verification: uv run pytest -q 1698 passed / 5 skipped (1685 after part A,
strict superset, 0 removed). ruff check + format clean, mypy clean (38
files). Golden demo-transcript.stdout BYTE-UNCHANGED, shasum -a 1 of the
CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f. No version bump, no push.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:28:40 +02:00
277bb95777 feat(validator,generate,run): an identifier a proposal builds on must be in the input, or the verdict falls [skip-docs]
P6 (økt 108) ended in ValidatedProposal (verdict 5fd6272e3725fe68) on two cost
codes -- M-04-01 / M-04-03 -- that appear in NO prompt of that run. Measured
here first, verbatim: validate_proposal(p, baseline=None) validates it; the same
proposal against any non-empty CostBaseline is rejected naming both codes.

So the hole was never "fabrication goes uncaught" -- _reconcile_against_baseline
exists and is right -- but that the falsifier is reached only through
`if baseline is not None`. The input always exists; the baseline does not.

New stage 0b (_ground_against_input), OUTSIDE the baseline branch, after stage 0
so an anchored run's message is byte-identical to before. ONE Rejection, the
validator's own type, naming EVERY ungrounded identifier "; "-joined in the
proposal's own order (økt 94's completeness reason).

The rule has NO pattern -- `code in grounding`, exact substring -- and that is a
measurement: over the delivered corpora (K2 1108 files / 2 005 561 chars, the
three N payloads 8 excerpts each) the identifier forms are heterogeneous, and a
pattern chosen to cover them would be a rule about shapes. Bare numerals are the
one inert class (46 394 occurrences / 2 117 distinct in K2); the rule fails OPEN
there, never closed.

Evidence is three non-model-authored sources: what run_project DELIVERED (the
rendered cut/pointer/chunks plus the base's context_files -- never files, which
would make the type: verdict layer evidence), the project's own cost lines, and
the baseline's codes when anchored. The rendered PROMPT is deliberately NOT
evidence, on two measurements: gen_context IS the debate output on the S2c path,
and from attempt 2 the prompt carries the previous Rejection.reason verbatim --
which for this stage QUOTES the identifier it just refused. Grounding in the
prompt would let the gate's own refusal disarm it on its second round.

Prose scanning was chosen against WITH THE NUMBERS: a typed gate catches 2/2
(P6) and 2/2 (S7c) -- 100% of what reached a verdict. What stays uncaught, said
plainly: an ungrounded identifier that lives only in agent/debate prose and never
becomes an affected_item code (2 of 4 P6, 2 of 4 S7c, 1 of 2 P4).

Iron Law: 9 red / 2 green before the rule existed. Ten mutations all red against
the whole suite, green control 1558 passed / 5 skipped (from 1543/5, superset,
0 removed), golden demo-transcript.stdout BYTE-UNCHANGED (shasum -a 1 of the
CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

Three existing fixtures changed, no gate weakened -- most of all
test_pre_amendment_bundle_runs_unchanged, which sent the SAME FABRICATED code and
asserted it validated: the økt-108 hole written down as an expectation.

No paid run. Order 20260909T113641Z-38938691-from-.claude.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:04:01 +02:00
78e8e39147 fix(validator): stage 0 navngir ALLE baseline-overtredelser, ikke bare den foerste
K2-funn (b), maalt live i oekt 94 (docs/2026-09-06-major2-levende-k2.md § 4,
kjoering 3, max_attempts=3):

  1000/500 -> "quantity 1000 ... baseline 1250" -> 1188/350
           -> "unit_cost 350 ... baseline 850"  -> 1000/850 -> forsoekene brukt opp

Steg 5 mater avvisningsgrunnen ORDRETT inn i neste forsoeks prompt, saa en
melding som navngir ETT felt leses som en instruks om aa rette det feltet.
Modellen fant hver riktig verdi og aldri begge samtidig: den rettet feltet
avvisningen navnga og brakk det andre. Loekka oscillerte i stedet for aa
konvergere.

Endringen gjelder KUN meldingens fullstendighet. D6 er uendret: en hvilken som
helst overtredelse avviser fortsatt, i samme stage, foer loeseren.
max_attempts heves IKKE og eksponeres IKKE.

Hver overtredelse beholder dagens setning ORDRETT, sammenfoeyd med "; ", saa
NOEYAKTIG EN overtredelse rendres byte-identisk med foer - det er dette som
holder de eksisterende delstreng-assertene i S4.0-, reserve- og
levert-bundle-gatene staaende. Skilletegnet er valgt framfor linjeskift fordi
Rejection.reason ogsaa lander i outbox-JSON, de hostede payloadene og
terminal-notisene.

Rekkefoelgen er FORSLAGETS egen (linjer i oppgitt rekkefoelge, quantity foer
unit_cost i en linje), saa to identiske forsoek gir to identiske prompter. En
ukjent kostkode bidrar med sin ENE setning og ingen magnitude-setninger: det
finnes ingen baseline-linje aa avvike fra, og en sammenligning mot ingenting er
nettopp den fabrikasjonen dette steget finnes for.

Load-bearing MAALT (tests/test_stage0_all_violations_loadbearing.py, 6 armer),
seks mutasjoner ALLE ROEDE mot HELE suiten + groenn kontroll 1387/5 (fra
1381/5; +6, 0 fjernet - strengt supersett) og golden demo-transcript.stdout
BYTE-UENDRET (shasum -a 1 av INNHOLDET = ea8c534773acdbe41ae68f2c55724d69aaf8be4f):
returner ved foerste overtredelse (4 roede) - rapporter kun den siste (4) -
ustabil rekkefoelge i linja (1) - over-rapporter et felt som er INNENFOR
toleransen (28) - drift enkelt-overtredelsens form (1) - la en ukjent kode
ogsaa emittere magnitude-setninger (19).

Co-Authored-By: Claude <claude-opus-5>
2026-09-07 00:38:50 +02:00
126807aee7 feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0)
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
2026-08-03 17:19:31 +02:00
e8cec2e2c0 feat(validator): enforce the deterministic stage-2 bound and band enclosure (S2.7)
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
2026-08-03 16:22:54 +02:00
69ca508677 feat(fase1): assessment-method encoding — persisted dimension-scoped example + validator rule (F1) 2026-07-07 08:15:44 +02:00
bbba6e7337 feat(fase2): promote blocking validator + IR to src 2026-06-24 13:24:03 +02:00