fix(accounting,gate): the conversion claim comes from the run's ledger

Chose the side channel over neutralising pointer-shaped document text,
because the second fix changes what every document SAYS in order to
defend a tool outside the build: a source quoting a bundle listing would
come out altered and existing bundles would move bytes. This reads a
file the run already writes.

`assets.conversion` names the pair, `DocumentAssets.conversions` carries
it out of the run, `DocumentAccount.conversions` books it, and the
accounting JSON states it per document. `_declared_conversions` reads it;
`_conversions` now believes a pair only when the RUN booked it AND a
pointer block confirms it for the asset it names. The confirmation can be
forged and the ledger cannot, which is why the ledger decides.

Measured through the real `okf build`: the three arms PM reproduced
(two `<p>`, one `<p>` with `<br>`, a markdown note beside the carrier)
go forged -> refused, 3 of 3, with the known-positive True in all three.
The text-level regression guard goes 3 arms to 13, the two new ones being
a perfectly written pointer block the run never booked.

R761, rebuilt: 25 BMP sources, 19 held, 19 of 19 conversions confirmed
against 19 declared, 50 assets (29 JPEG + 21 PNG, 0 BMP), SHY 71,
u = 0, d = 0, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-19 19:12:20 +02:00
commit 1c958ab8d6
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 226 additions and 59 deletions

View file

@ -887,6 +887,23 @@ def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def conversion(image: ExtractedImage) -> tuple[str, str] | None:
"""`(the source's digest, the carried asset's digest)`, or `None`.
THE RUN'S OWN RECORD OF WHAT IT REWROTE, for a reader that must not have
to take the bundle's word for it. `render_block` states the same pair on
the pointer's second line, which is where a person reads it -- but that
line is markdown in a concept body, and measured by PM 2026-09-19 an
ordinary HTML document with two `<p>` elements produces the same two
lines. A judge reading the claim off the bundle text is therefore reading
an untrusted document; a judge reading it off the accounting is reading
this function's output, which no document can reach.
"""
if image.converted_from is None or image.source_sha256 is None:
return None
return (image.source_sha256, digest(image.data))
def _reduce(text: str) -> str:
return _SEPARATOR_RUN.sub("-", unicodedata.normalize("NFC", text).lower()).strip("-")