A parsed maintenance record is only as defensible as its least-documented field. This page shows how to attach a compact provenance envelope — source hash, page, bounding box, OCR confidence, and extractor version — to every individual field of a record rather than stamping the whole document once. It is the field-granularity companion to Data Lineage & Provenance Tracking, and it exists because EASA Part-145 point 145.A.55 obliges an organization to keep records that faithfully reflect the maintenance performed, which in practice means being able to defend each recorded value, not just the page it sat on.
Prerequisites & inputs
Before capturing field-level provenance you need three upstream products. First, a source-document fingerprint: a single SHA-256 computed over the raw scan bytes at ingestion. Second, per-character geometry from the OCR layer — page index and pixel bounding boxes — together with a per-token confidence, typically produced alongside OCR Confidence Scoring & Fallbacks. Third, an extractor identity: a version string for whatever regex or model resolved the raw token into a typed field. With those three, every field can carry a self-describing origin. For US-certificated work the same discipline answers to the FAA Part-145 Recordkeeping Standards, whose § 145.219 retention duty presumes each recorded value can be substantiated on request.
The target record for this walkthrough is a component removal on G-EZBK, ATA 27-51 (trailing-edge flaps), part AS3400-11, recorded at 9,842 cycles.
Field-level provenance record
Step-by-step implementation
Step 1 — model the envelope as a reusable type. Define a single FieldProvenance model and a generic TrackedField that pairs a value with its envelope. Because provenance shape is identical across every field, this keeps the record schema readable rather than doubling its width.
Step 2 — validate at the point of capture. Confidence must lie in [0.0, 1.0], the bounding box must have positive extent, and the source hash must be a 64-character hex digest. Reject at construction; a malformed envelope is worse than none because it looks authoritative.
Step 3 — assemble the record from tracked fields. Each maintenance field becomes a TrackedField, and the record model composes them. Serialization then emits both value and provenance together, so a downstream consumer never sees a bare value.
from __future__ import annotations
import logging
from datetime import datetime, timezone
from typing import Generic, Tuple, TypeVar
from pydantic import BaseModel, ConfigDict, Field, field_validator
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger("mro.lineage.field")
T = TypeVar("T")
class FieldProvenance(BaseModel):
"""Origin envelope attached to one field value.
Kept per EASA Part-145 point 145.A.55, which requires the organisation
to keep records of maintenance carried out that reflect the work done.
"""
model_config = ConfigDict(frozen=True)
source_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
page_index: int = Field(ge=0)
bbox: Tuple[int, int, int, int]
ocr_confidence: float = Field(ge=0.0, le=1.0)
extractor_version: str = Field(min_length=1)
captured_at: datetime
@field_validator("bbox")
@classmethod
def positive_extent(cls, v: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
_, _, w, h = v
if w <= 0 or h <= 0:
raise ValueError("bbox width and height must be positive")
return v
@field_validator("captured_at", mode="before")
@classmethod
def enforce_utc(cls, v: object) -> datetime:
if isinstance(v, str):
return datetime.fromisoformat(v.replace("Z", "+00:00")).astimezone(timezone.utc)
if isinstance(v, datetime):
return v.astimezone(timezone.utc)
raise TypeError("captured_at must be datetime or ISO 8601 string")
class TrackedField(BaseModel, Generic[T]):
value: T
provenance: FieldProvenance
class ComponentRemoval(BaseModel):
aircraft_registration: TrackedField[str]
ata_chapter: TrackedField[str]
part_number: TrackedField[str]
cycles_at_removal: TrackedField[int]
def track(value: T, source_sha256: str, page_index: int,
bbox: Tuple[int, int, int, int], ocr_confidence: float,
extractor_version: str) -> TrackedField[T]:
field = TrackedField[type(value)](
value=value,
provenance=FieldProvenance(
source_sha256=source_sha256,
page_index=page_index,
bbox=bbox,
ocr_confidence=ocr_confidence,
extractor_version=extractor_version,
captured_at=datetime.now(timezone.utc),
),
)
if ocr_confidence < 0.90:
logger.warning("low-confidence field conf=%.2f bbox=%s", ocr_confidence, bbox)
return field
Worked example
Feeding the G-EZBK removal through track produces one envelope per field. The part number, read cleanly from a printed OEM label, carries ocr_confidence=0.98; the cycles figure, hand-stamped and partly smudged, carries 0.86 and trips the low-confidence log line:
2026-05-12 09:14:22 | WARNING | low-confidence field conf=0.86 bbox=(410, 622, 96, 20)
Serializing the assembled ComponentRemoval yields, for the part-number field:
{
"value": "AS3400-11",
"provenance": {
"source_sha256": "3f9c...b1e0",
"page_index": 1,
"bbox": [268, 540, 132, 22],
"ocr_confidence": 0.98,
"extractor_version": "pn-regex-v4.1.0",
"captured_at": "2026-05-12T09:14:22+00:00"
}
}
An auditor reviewing this record can open page 2 of the source scan (page_index is zero-based), crop the pixel box, and confirm by eye that the label reads AS3400-11. The smudged cycles field, flagged at capture, is exactly the one they should scrutinize.
Edge cases & compliance gotchas
Mixed OEM label formats. One vendor prints part numbers with an embedded space (AS3400 11); another hyphenates. The extractor version in the envelope lets you later distinguish values normalized by pn-regex-v4.1.0 from an earlier buggy v4.0.x, so a recall of one extractor version does not force re-review of every field.
Unicode diacritics in technician names. A certifying technician recorded as Muñoz may arrive from OCR as Munoz or as a decomposed Muñoz. Store the value NFC-normalized but keep the raw bounding box, so the envelope still points at the ink that was actually read — normalization is a transform, not a source of truth.
Zero-area bounding boxes from vector text. Digitally-born PDFs sometimes expose glyphs with degenerate boxes; the positive_extent validator rejects these at capture rather than letting a (x, y, 0, 0) box imply a location that cannot be visually confirmed.
Timezone drift on the capture stamp. A scanner in a different timezone that writes local time will silently misorder fields against a UTC ledger. Forcing captured_at to UTC at construction removes the ambiguity before it propagates. With these guards in place, every field in the record carries its own defensible origin, and the record as a whole becomes something a reviewer can verify field by field rather than trust wholesale.