feat(prepass,explore): padding dies at the prompt; a refusal the model can act on is a return value [skip-docs]

Order 20260908T195801Z. Findings 4 and 5 from the S7 acid test, then the two things
finding 99 measured and deliberately did not fix (D3, D2).

No user-facing surface changes: no new flag, no new command, no changed output
contract. Both seams are internal (the pre-pass rendering, and the shape a tool
answers a model with), so [skip-docs] rather than a README edit that would describe
nothing an operator can do differently.

FINDING 4 -- MEASURED, NOTHING BUILT. K2's price schedule IS readable without
guessing (8 column spans, 71 of 91 non-blank rows give >= 2 cells, the split stable
for K = 2..64). But 0 of 92 rows name all three of code/quantity/unit_cost -- also
under a looser substring match -- and 0 of 91 data rows carry code + quantity +
amount. The triple is not formatted away; it is not in the document. It is a price
SUMMARY plus nine rate cards whose unit-price columns are empty (pre-award). The
order's binding decision rule therefore falls against building:
--derive-cost-baseline keeps refusing, and MAJOR-4's own honesty limit holds.

FINDING 5 -- BUILT. Measured on the actual rendering path (concept_text, not the
raw file): the delivered excerpt is 104 lines / 67 245 chars, carrying 208 interior
whitespace runs, 117 of them >= 100 and the longest 887 -- 56 806 of 67 245
characters = 84.5 %, over 72 of 104 lines. collapse_padding, called from
_data_blocks (the one renderer both arms share, and therefore AFTER
verify_against_bundle -- collapsing in concept_text would break every payload's own
digest), gives -72.4 %: line count invariant, non-whitespace byte-identical, leading
indentation untouched, no number changed.

F99-D3 -- read_file / read_dir / read_bundle now RETURN their refusal. MAF turns a
tool raise into "Error: Function failed." (_tools.py:1410-1432, :1427) and counts it
against DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST = 3, so everything the refusing
arm knows is destroyed on the way out. The gates are unchanged; the property they
exist for -- the reason travels, the bytes never do -- is now asserted explicitly on
the returned value. The arm is keyed on named classes, never bare Exception, because
ExplorationError is itself a RuntimeError subclass.

F99-D2 -- the invariant row, plus one for finding 5 (a stated deviation from "one
row only": finding 5 is a separately built seam and the ledger's standing rule
requires its own row).

19 existing arms rewritten, never deleted and never weakened: where the class
carried a distinction, the refusal KIND carries it now.

13 mutations, all red against the whole suite (W1-W5, M1-M8), each restored from
scratchpad with shasum -c. Control 1543 passed / 5 skipped (from 1529/5, a strict
superset, 0 removed). Golden demo-transcript.stdout unchanged
(shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

Measurement: docs/2026-09-08-funn-4-5-og-read-nekt.md

Co-Authored-By: Claude <Opus 5>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-08 23:36:54 +02:00
commit eb4137415c
13 changed files with 975 additions and 69 deletions

View file

@ -1839,6 +1839,71 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
nekten er nøklet på den ENE erklærte typen ExpeL-folden eier, aldri på et bredere
«ikke i `context_files`»-komplement, som M4 viser ville rammet `index.md` — navigasjon, ikke en
dom. Måling: `docs/2026-09-04-s2c-debatt-k2.md` § 8.
- **En nekt modellen skal kunne RETTE SEG ETTER er en RETURVERDI, aldri et raise — og dét gjelder
HVERT verktøy, ikke bare det ene funn 99 rakk (funn 99 D2+D3, 08.09):** MAF gjør en tools
exception om til den ugjennomsiktige strengen `"Error: Function failed."`
(`agent_framework/_tools.py:1410-1432`), undertrykker detaljen med mindre `include_detailed_errors`
er satt (`:1427`), og teller den mot `DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST = 3` (`:96`,
`:2718-2731`). Alt den nektende armen VET — hvilke base-id-er som finnes, hvilket dokument stien
egentlig navngir, hvilken rung som leser det — tilintetgjøres altså på vei ut, og tre av dem
avslutter forespørselen. MÅLT live: modellen gjettet på JSON-formatet, som var riktig hele veien,
og trippelen rett foran en Azure-400 var tre `read_file`-nekter på samme dokument. `quick_validate`
returnerer `{"decision":"refused",…}` og hver gren — refusal inkludert — står i `quick_validations`;
`read_bundle`/`read_dir` returnerer `{"refused", "refusal"}` (ingen nøkkel en vellykket listing
har), `read_file` en streng med sentinel-prefikset `REFUSED (<kind>): ` fordi en mapping DER ville
endret formen på hver VELLYKKET lesning og dermed hver prompt-byte S2c målte. **`refusal`-KINDEN er
ikke pynt:** klasse-distinksjonen `pytest.raises` bar før (F3s «dette er et dokument» vs «dette
finnes ikke») ville forsvunnet med raisen, så den bæres som felt, avledet av `type(exc).__name__`
ÉN kilde, aldri en andre tabell. **GATENE ER URØRT** — verdict-laget og §4.1a-dimensjonen nekter
nøyaktig det de nektet før — og egenskapen de finnes for asserteres nå EKSPLISITT på returverdien:
**grunnen reiser, bytene aldri.** **Armen er nøklet på KLASSEN, aldri bar `Exception`:**
`_RETURNABLE_REFUSALS` navngir hver klasse én for én, fordi `ExplorationError` selv er en
`RuntimeError`-subklasse, så `except RuntimeError` ville slukt en feil ingen har navngitt og gjort
en ukjent svikt om til et selvsikkert svar — verre enn den ugjennomsiktige strengen. Load-bearing
MÅLT (`tests/test_explore_read_tools_refusal.py`, 7 armer), åtte mutasjoner alle røde mot HELE
suiten + grønn kontroll 1543/5 og golden BYTE-UENDRET: M1 `read_file` raiser igjen (12) · M2
`read_dir` (10) · M3 `read_bundle` (3) · M4 armen fanger bar `Exception` (2 — kjent-negativen PLUSS
et eldre, uavhengig vitne, `test_a_nonexistent_sibling_is_still_an_os_error`) · M5 nekten dropper
grunnen (10) · M6 dropp `refusal`-kinden (9) · M7 `read_file` mister sentinel-prefikset (12) ·
M8 nekten bærer det nektede dokumentets bytes (8, fem i eldre tester). **Ærlighets-grenser,
uttalt:** et dokument hvis første tegn ER sentinelen ville vært uskillbar fra en nekt — en ekte
luke, skrevet ned i stedet for bortforklart; en `OSError` (f.eks. `FileNotFoundError` på en sti
som ikke finnes) står UTENFOR settet og propagerer fortsatt, gatet av en eksisterende arm; og at en
LEVENDE modell faktisk RETTER seg etter nekten er ikke bevist (structured-output-grensens klasse).
Måling: `docs/2026-09-08-funn-4-5-og-read-nekt.md` § 3.
- **Et regneark-renders PADDING dør på vei inn i prompten; ingenting annet gjør det (funn 5, 08.09):**
S7c hadde alt FELT den åpenbare kuren — begge låser åpnet LEVERTE K2s prisskjema (rang 10, bytene i
2 av 11 prompter) og modellen siterte `5647500`/`prissammenstilling`/`prisskjema`/`pris` i **0 av 4**
og **0 av 11** svar. Blindsonen hadde flyttet seg fra RANGERINGEN til LESNINGEN, og dét po eier der
er rendringen. **MÅLT PÅ DEN FAKTISKE RENDRINGS-STIEN** (`prepass.concept_text`, ikke råfila): det
leverte utdraget er **104 linjer / 67 245 tegn** — hvilket AVSTEMMER de to nevnerne (119 linjer /
101 188 B på disk, samme dokument før frontmatter-splitten og produsentens per-linje `rstrip`) — og
bærer **208** indre mellomrom-løp ≥ 2, **117** av dem ≥ 100 og det lengste **887**, til sammen
**56 806 av 67 245 tegn = 84,5 %** over **72 av 104 linjer**. Etikett og beløp nådde altså modellen
hundrevis av tegn fra hverandre (`82` · 593 mellomrom · beskrivelse · 250 mellomrom · `5647500.0`).
Etter: **67 245 → 18 531 tegn (72,4 %)**, linjeantall uendret, ikke-whitespace byte-identisk, og
linja leser `82 Prosjektering (tiltransport av prosjekterende) 5647500.0`. **INGEN SKJØNN, og det er
fire løfter med hver sin gate:** indre løp av mellomrom/tab kollapser til ÉN (K = 2, altså finnes
det ingen terskel å forsvare); **LEDENDE whitespace er URØRT** (indentering er markdown-STRUKTUR —
nestede lister, indentert kode — og en regel som flatet den ville omskrevet dokumenter i stedet for
å av-padde dem; dét er hva `(?<=\S)` kjøper); **ingen newline røres**, så linjeantallet er invariant
og ingen rad slås sammen med naboen (en naiv `re.sub(r"\s+", " ", text)` gjør en tabell til et
avsnitt); **ingen ikke-whitespace-tegn røres**, så intet tall kan endres — asserten er den
PER-LINJE ikke-whitespace-SEKVENSEN, ikke «sifrene finnes fortsatt et sted». **HVOR den bor er det
bærende valget:** i `_data_blocks`, den ENE rendreren begge armer deler (kø-(p) — en fiks i
`render_context` alene ville latt utforskningen lese padding), og derfor ETTER
`verify_against_bundle`. `concept_text` er re-utledningen en payloads bytes bindes til, så en
kollaps DER ville fått hver payload noensinne skrevet til å feile sin egen digest og opphevet gaten
som stopper en payload fra å levere bytes basen ikke holder. Load-bearing MÅLT
(`tests/test_prepass_padding_collapse_loadbearing.py`, 7 armer), fem mutasjoner alle røde mot HELE
suiten + grønn kontroll 1543/5 og golden BYTE-UENDRET: W1 detach kollapsen fra `_data_blocks` (2) ·
W2 kollaps ledende whitespace også (1, indenterings-armen ALENE) · W3 kollaps newlines også (4,
inkl. kontrollen) · W4 skriv om tall (2) · W5 kollaps inne i `concept_text` (1, separasjons-armen
alene). **Ærlighets-grenser, uttalt:** kollaps til ett mellomrom mister CELLE-GRENSEN — `Post SUM`
blir uskillbar fra prosa med de to ordene — og å finne opp en skilletegn ville vært nøyaktig det
skjønnet regelen unngår, uten en måling som sier hvilket en modell leser bedre; at en modell så
BRUKER tallene er IKKE vist (det krever en betalt kjøring, som ikke er bestilt).
Måling: `docs/2026-09-08-funn-4-5-og-read-nekt.md` § 2.
- **Eksperten kan svare på forslaget som ligger på BORDET — og svaret BRUKES, det registreres ikke
bare (MAJOR-2, økt 8889):** før dette fantes ÉN menneske-søm inne i løkka, `plan_reviewer`, og
den fyrer FØR noen hypotese finnes. **Nevneren er målt** (`docs/2026-09-02-misjonsreview-v2.md`