Evidence

Provenance for the numbers a model states: SourceEvidence citations written through the edit seam (attach), re-checked against their documents (verify – intact / drifted / lost / unreachable), and counted honestly (coverage). The evidence guide covers the workflow and the two storage patterns; the design is docs/design/provenance.md.

Evidence-linked models: attach, verify, and count SourceEvidence citations.

The model-side vocabulary is the Evidence::SourceEvidence metadata definition (examples/evidence.sysml, a longeron convention package – design: docs/design/provenance.md): a citation from a model element to a region of a source document, carried in the model as an ordinary metadata annotation, so it parses, exports, survives JSON, and projects to RDF like any other content:

attribute mass : Real = 0.055 [SI::kg] {
    @Evidence::SourceEvidence {
        document = "https://emaxmodel.com/products/mt2213";
        sha256 = "9f2c...";
        quote = "Weight: 55g";
        retrieved = "2026-08-28";
    }
}

This module is the Python-side toolchain:

  • attach() writes a citation through longeron.edit (the tracker records it; a refusal mutates nothing). It computes the document’s sha256, extracts its text, and refuses a quote the document does not contain – quote-primary anchoring, because the quote survives re-rendering while page geometry does not.

  • verify() re-checks every citation and returns one Verdict each: intact, drifted (sha256 mismatch), lost (hash intact, text no longer contains the quote), or unreachable (the file or URL cannot be read at all).

  • coverage() is the honest metric: which stated attribute values carry a citation, counted and never inflated.

Documents. A document is a repo-relative path (resolved against the model’s source directory) or an http(s) URL. URL documents are fetched once into a local cache – ~/.cache/longeron/evidence, honoring $LONGERON_CACHE_DIR and $XDG_CACHE_HOME, the same convention as the corpus cache – which keeps verification fast and is never authoritative: the hash on the citation is. Two storage patterns (the guide, docs/guides/evidence.md): owned documents commit under evidence/ via git LFS (longeron evidence init writes the .gitattributes stanza); third-party datasheets are cited by URL + sha256 + quote and never committed.

Text extraction. PDFs extract through pypdf, falling back to pdfminer.six when pypdf yields nothing – both license-clean, behind the [evidence] extra (MissingExtraError without it). HTML (a fetched product page) is reduced to its visible text with the standard library’s parser, so quotes anchor to what a reader sees, not to markup. Everything else reads as UTF-8 text. Quote matching normalizes whitespace on both sides (PDF extraction rewraps lines) and is otherwise exact.

attach(..., verify=False) is the documented escape hatch for offline authoring: it writes the citation without reading the document, using whatever sha256 you supply (or none). verify later reports honestly what such a citation is worth.

class longeron.evidence.Citation(element, qname, document, sha256, quote, page=None, bbox=None, retrieved=None, usage=None)[source]

Bases: object

One SourceEvidence annotation, read back from the model.

element: Element

the cited element (the annotation’s owner)

qname: str

its qualified name (or best-effort label)

document: str

repo-relative path or URL

usage: MetadataUsage | None = None

the annotation element itself

class longeron.evidence.CoverageReport(facts)[source]

Bases: object

Which stated values carry evidence – counted, never inflated.

class longeron.evidence.Fact(element, qname, value, documents)[source]

Bases: object

One stated value: a leaf attribute with a literal (non-derived) value.

documents: list[str]

cited documents (empty = uncited)

class longeron.evidence.Verdict(citation, status, detail='')[source]

Bases: object

One citation’s verification result.

longeron.evidence.attach(model, element_or_qname, document, quote, *, page=None, bbox=None, retrieved=None, sha256=None, verify=True)[source]

Cite document as the evidence for an element; returns the annotation.

Reads the document (fetching a URL document once, into the cache), computes its sha256, extracts its text, and refuses (EditError) when the text does not contain quote – naming the document and, when cheaply computable, the closest text it does contain. An unreadable document refuses too. A supplied sha256 that contradicts the computed one refuses – the pin must be true. Nothing mutates on any refusal, and the tracker records nothing.

On success the citation is written through longeron.edit.add_metadata() (so a registered Tracker records it) as an @Evidence::SourceEvidence annotation; retrieved defaults to today’s ISO date.

verify=False is the escape hatch for offline authoring: the document is not read, and the citation stores exactly what you pass (including sha256=None). verify() later reports what such a citation is worth.

Return type:

MetadataUsage

longeron.evidence.cache_dir()[source]

The gitignored document cache: ~/.cache/longeron/evidence.

$LONGERON_CACHE_DIR and $XDG_CACHE_HOME are honored, the same convention as the corpus cache (scripts/check_corpus.py). The cache only speeds verification up; the sha256 on each citation stays the authority (the ratified design decision 1).

Return type:

Path

longeron.evidence.citations(model, root=None)[source]

Every SourceEvidence citation under root (default: the model).

Elements inside library packages are skipped, matching validate’s posture: a merged-in library is context, not the model under inspection.

Return type:

list[Citation]

longeron.evidence.coverage(model, root=None)[source]

The honest coverage metric over the stated values under root.

A stated value is an attribute usage whose value expression is a literal fact – a number, string, or boolean, optionally with a unit annotation or a sign. Derived values (expressions over other features) state no independent fact and are not counted; constraint constants are not counted either (they live inside expressions, not on citable elements). Library-package content is skipped.

Return type:

CoverageReport

longeron.evidence.document_text(path)[source]

Extract the text of a local document.

.pdf extracts through pypdf, falling back to pdfminer.six when pypdf yields no text (both ship with the [evidence] extra; MissingExtraError names the install command without it). HTML content – by suffix or by sniffing the cached bytes of a URL document – is reduced to its visible text (script and style content dropped, entities unescaped) with html.parser, so quotes anchor to the text a reader sees. Every other file reads as UTF-8 text, with undecodable bytes replaced.

Return type:

str

longeron.evidence.init_lfs(directory='.')[source]

Write the evidence/ git-LFS stanza into <directory>/.gitattributes.

Storage pattern 1 of the provenance design: owned, redistributable documents commit under evidence/ as LFS objects, so the binary pollution never starts. Idempotent – an existing stanza is left alone; anything else in .gitattributes is preserved. Returns the .gitattributes path.

Return type:

Path

longeron.evidence.verify(model, root=None, *, fetch=True)[source]

Re-check every citation under root; one Verdict each.

  • intact: the document hashes to the citation’s sha256 and its text still contains the quote.

  • drifted: the document reads, but its sha256 changed.

  • lost: the sha256 matches (or the citation never pinned one) but the text no longer contains the quote.

  • unreachable: the file or URL cannot be read at all.

fetch=False stays offline: URL documents verify against the local cache only, and an uncached one is unreachable (the lint seam uses this, so longeron lint never touches the network).

Return type:

list[Verdict]