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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-15 02:11:29 +02:00
commit d74f32dc1c
15 changed files with 446 additions and 36 deletions

View file

@ -22,7 +22,6 @@ Two entry points, because the LLM call is async while ``validator.self_repair``
from __future__ import annotations
import json
import re
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field, replace
from typing import Any
@ -477,28 +476,6 @@ def _grounding_text(
)
#: The identifier forms the DELIVERED corpora actually carry, TRANSCRIBED from the measurement
#: (``docs/2026-09-09-p8-forankringstilbudet.md`` § 2) rather than chosen: over K2's two delivered
#: bundles the first form finds 50 distinct tokens and the second none, while over the three
#: N payloads the second finds 269-981 distinct and the first at most 3.
#:
#: **Why a pattern is admissible HERE and not in ``_ground_against_input``.** That is a GATE, and a
#: pattern there would be a rule about shapes that can wrongly REFUSE a real code. This is a
#: REPORT: a form it does not know is a token it fails to count, so it errs toward saying the input
#: offers LESS than it does — an under-count is a quiet report, never a false rejection.
#:
#: Bare numbers are deliberately EXCLUDED, with the number: K2 carries 46 394 occurrences over
#: 2 117 distinct values (P7 § 2), so counting them would make every report positive and the
#: measurement inert — the repo's cardinal class, a gate that can only come out green.
_IDENTIFIER_FORMS = (
# ``SHA-01``, ``RIM-02``, ``B-20-00-00``, ``FOR-2011-12-06-1357`` (K2's 50).
re.compile(r"\b[A-ZÆØÅ]{1,8}[-_]\d{1,4}(?:[-_]\d{1,4})*\b"),
# ``Krav 3.3.1—13`` (the N corpora's dominant form). EM-DASH U+2014 AND the hyphen, because the
# binding known positive is the em-dash spelling and only the em-dash spelling scores 6 of 6.
re.compile(r"Krav\s+\d+(?:\.\d+)*\s*[\u2014-]\s*\d+"),
)
@dataclass(frozen=True)
class GroundingOffer:
"""P8: what the DELIVERED input of one run can lawfully ground an ``affected_item`` code in.
@ -539,13 +516,12 @@ def grounding_offer(
Deterministic and free: no model call, no network, and no second walk of the bundle.
"""
text = _grounding_text(project, baseline, delivered).text
found: set[str] = set()
for form in _IDENTIFIER_FORMS:
found |= set(form.findall(text))
grounding = _grounding_text(project, baseline, delivered)
return GroundingOffer(
chars=len(text),
identifiers=len(found),
chars=len(grounding.text),
# ONE reader, shared with P19/B3's gate (kø-(p)): a report and a gate that counted
# identifiers differently would disagree about one run's own input.
identifiers=len(grounding.identifiers),
cost_lines=0 if baseline is None else len(baseline.items),
)