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:
parent
aa2abe8293
commit
1c958ab8d6
5 changed files with 226 additions and 59 deletions
|
|
@ -732,6 +732,14 @@ class DocumentAccount:
|
|||
counts: dict[str, int]
|
||||
fates: dict[str, Fate]
|
||||
error: str | None = None
|
||||
#: The `(source digest, asset digest)` pairs for the images this run
|
||||
#: REWROTE -- a BMP that reaches the bundle as a PNG, today's only case.
|
||||
#: Booked because the bundle states the same pairs only as prose on a
|
||||
#: pointer's second line, and measured by PM 2026-09-19 an ordinary HTML
|
||||
#: document with two `<p>` elements writes exactly that prose. A reader
|
||||
#: proving a conversion off the bundle text is reading the document; this
|
||||
#: is the same fact written by the run.
|
||||
conversions: tuple[tuple[str, str], ...] = ()
|
||||
#: How many U+00AD the normalisation door removed from this document's
|
||||
#: text before the persist gate saw it (operator decision 2026-09-18).
|
||||
#: Booked rather than silently applied: a door that changes a source's
|
||||
|
|
@ -765,6 +773,7 @@ class DocumentAccount:
|
|||
"status": self.status,
|
||||
"code": self.code,
|
||||
"normalised_soft_hyphen": self.normalised_soft_hyphen,
|
||||
"conversions": [{"from": before, "to": after} for before, after in self.conversions],
|
||||
"inventory": dict(self.counts),
|
||||
"fates": {kind: self.fates[kind].to_json() for kind in self.counts},
|
||||
"unaccounted": self.unaccounted,
|
||||
|
|
@ -943,7 +952,14 @@ def _persisted_account(
|
|||
fates[kind].carried += 1
|
||||
if "image" in fates:
|
||||
_book_images(inv, fates["image"], assets, finder)
|
||||
return DocumentAccount(inv.source_file, PERSISTED, None, counts, fates)
|
||||
return DocumentAccount(
|
||||
inv.source_file,
|
||||
PERSISTED,
|
||||
None,
|
||||
counts,
|
||||
fates,
|
||||
conversions=() if assets is None else assets.conversions,
|
||||
)
|
||||
|
||||
|
||||
def _refused_account(inv: Inventory, code: str) -> DocumentAccount:
|
||||
|
|
|
|||
|
|
@ -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("-")
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from .assets import (
|
|||
AssetRejection,
|
||||
ExtractedImage,
|
||||
asset_name,
|
||||
conversion,
|
||||
)
|
||||
from .connectors import safe_resolve
|
||||
from .errors import IngestError, MaterializationError, SegmentationError, SourceError
|
||||
|
|
@ -652,11 +653,19 @@ class InboxResult:
|
|||
|
||||
@dataclass(frozen=True)
|
||||
class DocumentAssets:
|
||||
"""One persisted document's image outcome."""
|
||||
"""One persisted document's image outcome.
|
||||
|
||||
`conversions` is the run's own list of `(source digest, asset digest)`
|
||||
pairs for the images it REWROTE, in the order they were carried. The
|
||||
bundle states the same pairs in prose on each pointer's second line; this
|
||||
is the machine-readable side of the same fact, and the difference is who
|
||||
wrote it -- a document can produce that prose and cannot produce this.
|
||||
"""
|
||||
|
||||
source_file: str
|
||||
carried: int
|
||||
rejected: tuple[AssetRejection, ...]
|
||||
conversions: tuple[tuple[str, str], ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -1448,6 +1457,11 @@ def process_inbox(
|
|||
source_file=source_name(path),
|
||||
carried=len(document.images),
|
||||
rejected=document.rejected,
|
||||
conversions=tuple(
|
||||
pair
|
||||
for pair in (conversion(image) for image in document.images)
|
||||
if pair is not None
|
||||
),
|
||||
)
|
||||
)
|
||||
for target_name, content, reasons in outputs:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue