all2md.roundtrip
Round-trip fidelity scoring — how much structure survives a conversion.
Where all2md.confidence asks “how much should I trust this conversion?”
without a reference, this module asks a question that has one: convert a
document to another format, parse it straight back, and measure what changed.
The source AST is the ground truth; the re-parsed AST is the candidate:
original --render--> via-format --parse--> round-tripped
| |
+--------------- compared -------------------+
That makes the score a genuine regression guard: a clean Markdown document
round-trips through Markdown at exactly 100, so any drift is a real defect
rather than measurement noise. It is also the second half of the substrate the
optimize capstone consumes – all2md.confidence scores conversions
where no reference exists (gnarly PDFs), this module scores the ones where a
reference can be manufactured.
The comparison is deliberately structural rather than textual. Two documents
that serialize to different bytes may carry identical structure (Markdown is
happy to write a bullet as * or -), while two documents with identical
text may have lost every heading. Five dimensions are scored independently and
combined:
structure(weight 0.40) – the block skeleton: heading levels, list nesting and ordering, table placement, code blocks, quotes.text(0.30) – the document-wide word stream, in order.inline(0.15) – inline formatting: bold, italic, code, links, images.tables(0.10) – table dimensions and cell text.references(0.05) – hyperlink and image targets (URLs).
Dimensions absent from the source are dropped and the remaining weights are renormalized, so a document with no tables is neither rewarded nor punished for the tables it does not have.
structure and text are scored against independent alignments, and that
separation is load-bearing. An earlier design aligned blocks by shape and then
compared the text of each aligned pair, which meant two paragraphs could pair up
merely because both were paragraphs – a demoted heading would shift every
subsequent pairing and crater the text score for a document that had not lost a
single word. Scoring the word stream document-wide keeps a structural change
from being punished twice.
Alongside the score, the report lists concrete StructuralDelta incidents
(“2 headings became paragraphs”, “table 1: 4x3 -> 4x2”) so a low score is
actionable rather than merely alarming.
- all2md.roundtrip.DIMENSION_WEIGHTS: dict[str, float] = {'inline': 0.15, 'references': 0.05, 'structure': 0.4, 'tables': 0.1, 'text': 0.3}
Relative weight of each fidelity dimension in the composite score.
- all2md.roundtrip.TEXT_INTACT_THRESHOLD = 0.995
Text similarity at or above which the word stream is considered intact. Whitespace normalization can nudge a faithful document a hair under 1.0, so this is not exactly 1.
- all2md.roundtrip.TEXT_MANGLED_THRESHOLD = 0.75
Text similarity below which lost words are a
warnrather than aninfo.
- all2md.roundtrip.TEXT_SIMILARITY_TOKEN_CAP = 20000
Combined word count (original + round-tripped) above which the order-sensitive text comparison is abandoned for an order-insensitive multiset overlap.
SequenceMatcheris quadratic in the worst case, and its worst case is not hypothetical: 10k+10k words drawn from a small vocabulary (an OCR-garbled page, a table of repeated cells) takes seconds, and 20k+20k takes half a minute. Word order is already thestructuredimension’s concern, so degrading to a bag-of-words overlap above the cap costs little and bounds the runtime ofall2md roundtripon a large corpus.
- class all2md.roundtrip.RoundTripReport
Bases:
objectStructural fidelity of a
parse -> render(via) -> parseround trip.- Parameters:
score (int) – Overall fidelity,
0(nothing survived) to100(structurally identical).band ({"high", "medium", "low"}) – Coarse bucket derived from
score, using the same thresholds asConfidenceReport.source_format (str) – Format the original was parsed from (e.g.
"docx").via (str) – Format the document was round-tripped through (e.g.
"markdown").metrics (dict) – Per-dimension scores in
0-100, keyed byDIMENSION_WEIGHTS. Dimensions the source does not exercise are omitted entirely.deltas (list of StructuralDelta) – Concrete differences found, most severe and most structural first.
- score: int
- band: Literal['high', 'medium', 'low', 'not_assessed']
- source_format: str
- via: str
- metrics: dict[str, int]
- deltas: list[StructuralDelta]
- to_dict() dict[str, Any]
Return a JSON-safe dict.
- classmethod from_dict(data: dict[str, Any]) RoundTripReport
Reconstruct a
RoundTripReportfrom itsto_dict()form.
- __init__(score: int, band: ~typing.Literal['high', 'medium', 'low', 'not_assessed'], source_format: str, via: str, metrics: dict[str, int] = <factory>, deltas: list[~all2md.roundtrip.StructuralDelta] = <factory>) None
- class all2md.roundtrip.StructuralDelta
Bases:
objectA single concrete difference between the original and the round trip.
- Parameters:
kind (str) – Machine-readable category (e.g.
"block_lost","block_changed","inline_lost","table_changed","reference_lost").detail (str or None, default = None) – Human-readable qualifier, e.g.
"heading(h2) -> paragraph".count (int, default = 1) – How many times this delta occurred. Deltas sharing
(kind, detail, severity)are coalesced with their counts summed.severity ({"info", "warn", "error"}, default = "warn") – How serious the difference is. Purely descriptive: the score comes from the dimension metrics, not from summing delta penalties.
- kind: str
- detail: str | None = None
- count: int = 1
- severity: Literal['info', 'warn', 'error'] = 'warn'
- to_dict() dict[str, Any]
Return a JSON-safe dict, omitting
detailwhen unset.
- classmethod from_dict(data: dict[str, Any]) StructuralDelta
Reconstruct a
StructuralDeltafrom itsto_dict()form.
- __init__(kind: str, detail: str | None = None, count: int = 1, severity: Literal['info', 'warn', 'error'] = 'warn') None
- all2md.roundtrip.build_report(original: Document, roundtripped: Document, *, source_format: str, via: str) RoundTripReport
Assemble a scored
RoundTripReportfrom two ASTs.
- all2md.roundtrip.coalesce_deltas(deltas: list[StructuralDelta]) list[StructuralDelta]
Merge deltas sharing
(kind, detail, severity), summing counts.Keeps first-seen order, which puts document-order structural findings first.
- all2md.roundtrip.net_block_deltas(deltas: list[StructuralDelta]) list[StructuralDelta]
Cancel
block_lostagainstblock_addedfor the same block description.Sequence alignment reports a moved block as one deleted and one inserted, so a document that merely gained a paragraph can be described as having lost one too. Netting the pair leaves the honest multiset statement – “one heading became a paragraph” – and lets block ordering be judged by the
structurescore, which is what actually measures it.Expects already-coalesced deltas, and drops any whose count nets to zero.
- all2md.roundtrip.score_roundtrip(original: Document, roundtripped: Document) tuple[int, dict[str, int], list[StructuralDelta]]
Compare two ASTs and return
(score, per-dimension metrics, deltas).Only the dimensions the original actually exercises are scored; their weights are renormalized to sum to 1. A document with no tables therefore neither gains nor loses points for its (absent) tables, and an empty document scores a vacuous
100.- Parameters:
- Returns:
The
0-100score, the per-dimension0-100metrics, and the coalesced, severity-ranked structural deltas.- Return type:
tuple of (int, dict, list)