Skip to content

Claims

Every name on this page is importable from sci_etl_core.claims. The whole package is provisional: a name may change in a minor release, with a CHANGELOG entry, until a known consumer depends on it. The Claims and provenance guide shows how the pieces fit together.

sci_etl_core.claims.models

ClaimKind module-attribute

ClaimKind = Literal['measurement', 'assertion']

Modality module-attribute

Modality = Literal['asserted', 'hedged', 'speculative']

Qualifier module-attribute

Qualifier = Literal['=', '<', '<=', '>', '>=', '~']

UncertaintyKind module-attribute

UncertaintyKind = Literal[
    "sd", "se", "ci95", "iqr", "range", "unknown"
]

ExtractionStamp dataclass

ExtractionStamp(
    *,
    model: str,
    prompt_hash: str,
    schema_hash: str,
    extractor_version: str,
)

What produced an extracted value: enough to find every value a fix must redo.

model is the LLM's name, empty when the client does not report one. prompt_hash and schema_hash are :func:content_hash digests of the system prompt and of the JSON Schema the answer had to match, empty when there was no schema. extractor_version names the library release, such as "sci-etl-core/0.6.0".

model instance-attribute

model: str

prompt_hash instance-attribute

prompt_hash: str

schema_hash instance-attribute

schema_hash: str

extractor_version instance-attribute

extractor_version: str

to_dict

to_dict() -> dict[str, str]

Return the stamp as a JSON-friendly mapping.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> ExtractionStamp

Rebuild a stamp from :meth:to_dict output.

EvidenceSpan dataclass

EvidenceSpan(
    *, record_id: str, start: int, end: int, quote: str
)

The sentence or sentences a claim was read from.

start and end are half-open character offsets into the text the model was sent, computed by :func:~sci_etl_core.claims.locate.locate_quote and snapped to sentence boundaries. quote is the model's exact quote, kept for grounding checks.

record_id instance-attribute

record_id: str

start instance-attribute

start: int

end instance-attribute

end: int

quote instance-attribute

quote: str

CanonicalValue dataclass

CanonicalValue(
    *,
    value: float,
    uncertainty: float | None,
    unit: str,
    dimension: tuple[tuple[str, int], ...],
    kind: str,
    conversion_path: tuple[str, ...],
)

A normalized value that a downstream engine computed; sci-etl-core stores it and never converts.

value instance-attribute

value: float

uncertainty instance-attribute

uncertainty: float | None

unit instance-attribute

unit: str

dimension instance-attribute

dimension: tuple[tuple[str, int], ...]

kind instance-attribute

kind: str

conversion_path instance-attribute

conversion_path: tuple[str, ...]

Quantity dataclass

Quantity(
    *,
    verbatim: str,
    value: float | None,
    unit_text: str,
    qualifier: Qualifier = "=",
    uncertainty: float | None = None,
    uncertainty_kind: UncertaintyKind | None = None,
    scale_note: str = "",
    canonical: CanonicalValue | None = None,
)

A number as the paper reports it.

verbatim is the text the value was read from, such as "2.9 ± 0.3 kpc". value is None when the text holds no single number.

verbatim instance-attribute

verbatim: str

value instance-attribute

value: float | None

unit_text instance-attribute

unit_text: str

qualifier class-attribute instance-attribute

qualifier: Qualifier = '='

uncertainty class-attribute instance-attribute

uncertainty: float | None = None

uncertainty_kind class-attribute instance-attribute

uncertainty_kind: UncertaintyKind | None = None

scale_note class-attribute instance-attribute

scale_note: str = ''

canonical class-attribute instance-attribute

canonical: CanonicalValue | None = None

Statistics dataclass

Statistics(
    *,
    effect_metric: str | None = None,
    effect_value: float | None = None,
    ci_low: float | None = None,
    ci_high: float | None = None,
    p_value: float | None = None,
    p_qualifier: Qualifier | None = None,
    test_statistic: float | None = None,
    test_kind: str | None = None,
    degrees_of_freedom: float | None = None,
    group_sizes: tuple[int, ...] = (),
)

The statistics a paper reports with a claim, each optional.

effect_metric class-attribute instance-attribute

effect_metric: str | None = None

effect_value class-attribute instance-attribute

effect_value: float | None = None

ci_low class-attribute instance-attribute

ci_low: float | None = None

ci_high class-attribute instance-attribute

ci_high: float | None = None

p_value class-attribute instance-attribute

p_value: float | None = None

p_qualifier class-attribute instance-attribute

p_qualifier: Qualifier | None = None

test_statistic class-attribute instance-attribute

test_statistic: float | None = None

test_kind class-attribute instance-attribute

test_kind: str | None = None

degrees_of_freedom class-attribute instance-attribute

degrees_of_freedom: float | None = None

group_sizes class-attribute instance-attribute

group_sizes: tuple[int, ...] = ()

ClaimContext dataclass

ClaimContext(
    *, attributes: tuple[tuple[str, str], ...] = ()
)

Domain-specific conditions as sorted (key, value) pairs; sci-etl-core gives keys no meaning.

attributes class-attribute instance-attribute

attributes: tuple[tuple[str, str], ...] = ()

from_mapping classmethod

from_mapping(attributes: Mapping[str, str]) -> ClaimContext

Build a context from a mapping, sorting its pairs.

to_dict

to_dict() -> dict[str, str]

Return the attributes as a mapping.

Claim dataclass

Claim(
    *,
    claim_id: str,
    kind: ClaimKind,
    subject: str,
    predicate: str,
    span: EvidenceSpan,
    stamp: ExtractionStamp,
    object: str = "",
    polarity: Literal[-1, 0, 1] = 0,
    modality: Modality = "asserted",
    quantity: Quantity | None = None,
    statistics: Statistics | None = None,
    context: ClaimContext = ClaimContext(),
    confidence: float | None = None,
)

One value or statement extracted from a paper, with its evidence and provenance.

A "measurement" needs a quantity and an "assertion" an object. polarity is 1 for a positive statement, -1 for a negated one, and 0 when it has no direction. claim_id is :func:make_claim_id of the identifying fields: the same claim extracted by another model has the same id and a different stamp.

claim_id instance-attribute

claim_id: str

kind instance-attribute

kind: ClaimKind

subject instance-attribute

subject: str

predicate instance-attribute

predicate: str

span instance-attribute

stamp instance-attribute

object class-attribute instance-attribute

object: str = ''

polarity class-attribute instance-attribute

polarity: Literal[-1, 0, 1] = 0

modality class-attribute instance-attribute

modality: Modality = 'asserted'

quantity class-attribute instance-attribute

quantity: Quantity | None = None

statistics class-attribute instance-attribute

statistics: Statistics | None = None

context class-attribute instance-attribute

context: ClaimContext = field(default_factory=ClaimContext)

confidence class-attribute instance-attribute

confidence: float | None = None

record_id property

record_id: str

The record the claim was read from.

from_draft classmethod

from_draft(
    draft: ClaimDraft,
    *,
    span: EvidenceSpan,
    stamp: ExtractionStamp,
) -> Claim

Build a trusted claim from a validated draft, the evidence span code located, and a stamp.

Raises:

Type Description
ValueError

The draft is a measurement without a quantity or an assertion without an object.

to_row

to_row() -> dict[str, Any]

Return the claim as one flat, JSON-friendly mapping, the shape every store and table uses.

from_row classmethod

from_row(row: Mapping[str, Any]) -> Claim

Rebuild a claim from :meth:to_row output.

content_hash

content_hash(value: Any) -> str

Return the SHA-256 hex digest of value serialized as compact JSON with sorted keys.

A string is hashed as its JSON string. Equal mappings give equal digests whatever their key order, so one schema always has one hash.

make_claim_id

make_claim_id(
    *,
    record_id: str,
    kind: str,
    subject: str,
    predicate: str,
    object: str,
    polarity: int,
    context: ClaimContext,
    quantity: Quantity | None,
    span: EvidenceSpan,
    schema_hash: str,
) -> str

Return a claim's id: a digest of the fields that make it one claim.

The id covers the record, kind, subject, predicate, object, polarity, context, the quantity's verbatim text with whitespace removed, the span offsets, and the schema hash. It leaves out the model, prompt, and library version, so re-extracting a paper with another model keeps every id whose fields match. Mentions are compared exactly: "DF44" and "DF 44" are different subjects.

sci_etl_core.claims.drafts

QuantityDraft

Bases: BaseModel

A number as the model reads it from the paper.

model_config class-attribute instance-attribute

model_config = ConfigDict(extra='forbid', frozen=True)

verbatim class-attribute instance-attribute

verbatim: str = Field(
    description="The value exactly as the paper writes it, with its unit"
)

value class-attribute instance-attribute

value: float | None = Field(
    default=None,
    description="The number, or null when the text holds no single number",
)

unit_text class-attribute instance-attribute

unit_text: str = Field(
    default="",
    description="The unit exactly as the paper writes it",
)

qualifier class-attribute instance-attribute

qualifier: Literal['=', '<', '<=', '>', '>=', '~'] = '='

uncertainty class-attribute instance-attribute

uncertainty: float | None = None

uncertainty_kind class-attribute instance-attribute

uncertainty_kind: (
    Literal["sd", "se", "ci95", "iqr", "range", "unknown"]
    | None
) = None

scale_note class-attribute instance-attribute

scale_note: str = Field(
    default="",
    description="A factor the paper applies, such as 'x 10^8'",
)

StatisticsDraft

Bases: BaseModel

Statistics the model reads next to a claim.

model_config class-attribute instance-attribute

model_config = ConfigDict(extra='forbid', frozen=True)

effect_metric class-attribute instance-attribute

effect_metric: str | None = None

effect_value class-attribute instance-attribute

effect_value: float | None = None

ci_low class-attribute instance-attribute

ci_low: float | None = None

ci_high class-attribute instance-attribute

ci_high: float | None = None

p_value class-attribute instance-attribute

p_value: float | None = None

p_qualifier class-attribute instance-attribute

p_qualifier: (
    Literal["=", "<", "<=", ">", ">=", "~"] | None
) = None

test_statistic class-attribute instance-attribute

test_statistic: float | None = None

test_kind class-attribute instance-attribute

test_kind: str | None = None

degrees_of_freedom class-attribute instance-attribute

degrees_of_freedom: float | None = None

group_sizes class-attribute instance-attribute

group_sizes: list[int] = Field(default_factory=list)

ClaimDraft

Bases: BaseModel

What the model returns for one claim; validated once, then turned into a claim.

:meth:~sci_etl_core.claims.models.Claim.from_draft builds the claim. quote is the sentence the claim was read from, copied from the paper. The model never supplies ids, offsets, or provenance; code computes them. Subclass it to narrow predicate or the context keys for a field of science, and pass the subclass as the claim extractor's schema.

model_config class-attribute instance-attribute

model_config = ConfigDict(extra='forbid', frozen=True)

kind instance-attribute

kind: Literal['measurement', 'assertion']

subject class-attribute instance-attribute

subject: str = Field(
    description="What the claim is about, as the paper names it"
)

predicate class-attribute instance-attribute

predicate: str = Field(
    description="The property measured or the relation asserted"
)

object class-attribute instance-attribute

object: str = Field(
    default="",
    description="For an assertion, what the subject is related to",
)

polarity class-attribute instance-attribute

polarity: Literal[-1, 0, 1] = 0

modality class-attribute instance-attribute

modality: Literal["asserted", "hedged", "speculative"] = (
    "asserted"
)

quantity class-attribute instance-attribute

quantity: QuantityDraft | None = None

statistics class-attribute instance-attribute

statistics: StatisticsDraft | None = None

context class-attribute instance-attribute

context: dict[str, str] = Field(
    default_factory=dict,
    description="Conditions such as band or sample",
)

quote class-attribute instance-attribute

quote: str = Field(
    description="The sentence the claim was read from, copied exactly"
)

confidence class-attribute instance-attribute

confidence: float | None = Field(
    default=None, ge=0.0, le=1.0
)

sci_etl_core.claims.locate

sentence_bounds

sentence_bounds(text: str) -> list[tuple[int, int]]

Split text into sentences tuned for scientific prose, as half-open (start, end) offsets.

A sentence ends at ., !, or ? followed by whitespace and a character that is not a lowercase letter, and at every blank line. A period does not end a sentence after a common abbreviation such as et al., Fig., Eq., Ref., or cf., after an initialism such as R.A., e.g., or i.e., or inside a number such as 0.5. Offsets exclude the whitespace between sentences.

locate_quote

locate_quote(
    text: str, quote: str, *, min_ratio: float = 0.9
) -> tuple[int, int, float] | None

Return the sentence-snapped offsets of quote in text and how closely it matched.

Matching is exact after collapsing whitespace first, which gives a ratio of 1.0. Otherwise each sentence, and each pair of adjacent sentences, is aligned with the quote through :mod:difflib, counting runs of three or more matching characters. The ratio compares the matched characters with the length of the quote and of the stretch of text they span, so a quote scattered over a long candidate scores low. The best candidate at min_ratio or above wins, and of equal candidates the shortest. The offsets cover whole sentences, so two models that quote different parts of one sentence get the same span. None means the quote is not in the text, so the claim is ungrounded.

Raises:

Type Description
ValueError

min_ratio is not between 0 and 1.

sci_etl_core.claims.extractor_async

AsyncLLMClaimExtractor

AsyncLLMClaimExtractor(
    llm_client: AsyncLLMClient,
    system_prompt: str,
    *,
    schema: type[ClaimDraft] = ClaimDraft,
    result_key: str = "claims",
    validator: RecordValidator | None = None,
    rejections: AsyncRejectionStore | None = None,
    min_ratio: float = 0.9,
    html_parser: Parser | None = None,
    max_chars: int = 120000,
    max_tokens: int | None = None,
    timeout: int = 120,
    now: Callable[[], datetime] | None = None,
)

Bases: AsyncEntityExtractor[Claim]

Extract claims with their evidence and provenance from a record's full text.

One LLM call per record returns :class:~sci_etl_core.claims.drafts.ClaimDraft objects, validated against schema through an inner :class:~sci_etl_core.llm.extraction_async.AsyncLLMEntityExtractor, so structured output, validation, and the schema's place in the LLM cache key all apply. Each draft's quote is then located in the text the model was sent; a draft whose quote cannot be found is ungrounded, logged, and put in rejections with the code "ungrounded". Every other draft becomes a :class:~sci_etl_core.claims.models.Claim stamped with the model, prompt, schema, and library version.

A claim cannot be built without its record, so requires_record is True: call :meth:extract_record. An extractor that wraps this one must delegate to its extract_record.

Configure the extractor.

validator sees each draft as a dict after the schema check. min_ratio is the share of a quote's characters that must be found in one or two adjacent sentences of the text for a fuzzy match. The other arguments are those of :class:~sci_etl_core.llm.extraction_async.AsyncLLMEntityExtractor.

Raises:

Type Description
ValueError

min_ratio is not between 0 and 1.

requires_record class-attribute instance-attribute

requires_record = True

stamp property

What produces this extractor's claims.

answer_schema property

answer_schema: dict[str, object] | None

The JSON Schema requested for the whole answer.

extract async

extract(text: str | bytes) -> Sequence[Claim]

Refuse: a claim needs its record.

Raises:

Type Description
TypeError

Always; call :meth:extract_record instead.

extract_record async

extract_record(
    record: RawRecord, text: str | bytes
) -> Sequence[Claim]

Extract the record's grounded claims.

Raises:

Type Description
LLMError

As for :meth:~sci_etl_core.llm.extraction_async.AsyncLLMEntityExtractor.extract_record.

ClaimStoreError

The rejection store could not be written.

sci_etl_core.claims.store_base

ClaimChange dataclass

ClaimChange(
    *,
    revision: int,
    record_id: str,
    op: Literal["replace", "delete"],
)

One committed change to a record's claims: "replace" or "delete", at revision.

revision instance-attribute

revision: int

record_id instance-attribute

record_id: str

op instance-attribute

op: Literal['replace', 'delete']

ClaimChangePage dataclass

ClaimChangePage(
    *, changes: tuple[ClaimChange, ...], next_revision: int
)

Changes after a revision, oldest first; next_revision is where the next page starts.

changes instance-attribute

changes: tuple[ClaimChange, ...]

next_revision instance-attribute

next_revision: int

AsyncClaimStore

Bases: ABC

An accumulating store of claims, replaced one record at a time.

Every write records a change with a new revision in the same transaction, so a downstream consumer reads what changed by revision, which only grows and never depends on clocks.

replace_record abstractmethod async

replace_record(
    record_id: str, claims: Sequence[Claim]
) -> int

Replace every claim of record_id in one transaction and return the new revision.

An empty claims deletes the record's claims and still records a change. Claims with an id already seen in claims are dropped, and the number dropped is logged.

Raises:

Type Description
ValueError

A claim's span.record_id is not record_id.

ClaimStoreError

The store cannot be written.

delete_record abstractmethod async

delete_record(record_id: str) -> int

Delete every claim of record_id and return the new revision.

claims_for_records abstractmethod async

claims_for_records(
    record_ids: Collection[str],
) -> dict[str, list[Claim]]

Return the stored claims of each record in record_ids that has any, in stored order.

changes_since abstractmethod async

changes_since(
    revision: int, limit: int = 1000
) -> ClaimChangePage

Return up to limit changes with a revision above revision, oldest first.

Raises:

Type Description
ValueError

limit is less than 1.

revision abstractmethod async

revision() -> int

Return the newest revision, or 0 before the first write.

count abstractmethod async

count() -> int

Return how many claims are stored.

aclose async

aclose() -> None

Release the store's resources; the default does nothing.

unique_claims

unique_claims(
    record_id: str, claims: Sequence[Claim]
) -> list[Claim]

Check that every claim belongs to record_id and keep the first claim of each id.

Raises:

Type Description
ValueError

A claim's span.record_id is not record_id.

check_limit

check_limit(limit: int) -> None

Reject a page size below 1.

Raises:

Type Description
ValueError

limit is less than 1.

sci_etl_core.claims.store_memory

InMemoryClaimStore

InMemoryClaimStore()

Bases: AsyncClaimStore

A claim store that lives as long as the process.

replace_record async

replace_record(
    record_id: str, claims: Sequence[Claim]
) -> int

delete_record async

delete_record(record_id: str) -> int

claims_for_records async

claims_for_records(
    record_ids: Collection[str],
) -> dict[str, list[Claim]]

changes_since async

changes_since(
    revision: int, limit: int = 1000
) -> ClaimChangePage

revision async

revision() -> int

count async

count() -> int

sci_etl_core.claims.store_sqlite_async

AsyncSqliteClaimStore

AsyncSqliteClaimStore(path: str | Path)

Bases: AsyncClaimStore

A claim store kept in a SQLite file.

Claims are indexed by record, by (subject, predicate, object), by canonical kind and value, and by context pair. The file and its parent folder are created on first use, foreign keys are enforced on every connection, and the file records its schema version in PRAGMA user_version. Every SQLite failure, including a file written by a newer sci-etl-core, raises :class:~sci_etl_core.exceptions.ClaimStoreError. Close it with :meth:aclose; a later call reopens it. Use each instance from one event loop.

replace_record async

replace_record(
    record_id: str, claims: Sequence[Claim]
) -> int

delete_record async

delete_record(record_id: str) -> int

claims_for_records async

claims_for_records(
    record_ids: Collection[str],
) -> dict[str, list[Claim]]

changes_since async

changes_since(
    revision: int, limit: int = 1000
) -> ClaimChangePage

revision async

revision() -> int

count async

count() -> int

aclose async

aclose() -> None

Close the connection; a later call reopens it.

sci_etl_core.claims.exporter_async

AsyncClaimStoreExporter

AsyncClaimStoreExporter(store: AsyncClaimStore)

Bases: AsyncExporter[Claim]

Store each record's claims in an :class:~sci_etl_core.claims.store_base.AsyncClaimStore.

write replaces the record's claims in one transaction and returns once they are committed, so durable_writes is True. The last write for a record wins, and a write with no claims clears the record's claims.

When write raises :class:~sci_etl_core.exceptions.ClaimStoreError, the pipeline fails the record: it is not marked processed, counts one failed attempt, and is retried on the next run. After max_attempts counted attempts it is quarantined at listing time like any failing record. This is not a memory fault: the pipeline absorbs only :data:~sci_etl_core.ingest_protocol.MEMORY_FAULTS, and this exporter never raises one.

durable_writes class-attribute instance-attribute

durable_writes = True

store property

The store claims are written to.

write async

write(record: RawRecord, entities: Sequence[Claim]) -> None

Replace record's claims with entities.

Raises:

Type Description
ClaimStoreError

The store could not be written.

ValueError

A claim belongs to another record.

aclose async

aclose() -> None

Close the store; it reopens on its next use.

sci_etl_core.claims.rejections

RejectedEntity dataclass

RejectedEntity(
    *,
    entry_id: str,
    record_id: str,
    entity: dict[str, Any],
    violations: tuple[Violation, ...],
    stamp: ExtractionStamp | None,
    quote: str = "",
    created_at: str,
)

An extracted entity that was not kept, with every reason, for review.

entry_id is :func:rejection_id of the other identifying fields, so rejecting the same entity again in a later run gives the same entry. quote is the evidence the model quoted, empty when it quoted none. created_at is an ISO 8601 time with a UTC offset.

entry_id instance-attribute

entry_id: str

record_id instance-attribute

record_id: str

entity instance-attribute

entity: dict[str, Any]

violations instance-attribute

violations: tuple[Violation, ...]

stamp instance-attribute

stamp: ExtractionStamp | None

quote class-attribute instance-attribute

quote: str = ''

created_at instance-attribute

created_at: str

AsyncRejectionStore

Bases: ABC

Storage for rejected entities that a reviewer resolves later.

:class:~sci_etl_core.llm.extraction_async.AsyncLLMEntityExtractor puts every entity its schema or validator rejects here when given rejections=. A rejected entity is kept for review; it is unrelated to a quarantined record, one the pipeline skips after it failed max_attempts times.

put abstractmethod async

put(entries: Sequence[RejectedEntity]) -> None

Store entries; an entry whose entry_id is already stored is left as it is, resolution included.

Raises:

Type Description
ClaimStoreError

The store cannot be written.

unresolved abstractmethod async

unresolved(
    limit: int = 100, after: str | None = None
) -> list[RejectedEntity]

Return up to limit unresolved entries in entry_id order, after the entry_id after.

Raises:

Type Description
ValueError

limit is less than 1.

resolve abstractmethod async

resolve(
    entry_id: str,
    resolution: str,
    corrected: Mapping[str, Any] | None = None,
) -> None

Record a reviewer's resolution of an entry, and the corrected entity if there is one.

Raises:

Type Description
KeyError

No entry has entry_id.

count abstractmethod async

count(*, unresolved_only: bool = True) -> int

Return how many entries are stored, or only how many are unresolved.

aclose async

aclose() -> None

Release the store's resources; the default does nothing.

InMemoryRejectionStore

InMemoryRejectionStore()

Bases: AsyncRejectionStore

A rejection store that lives as long as the process.

put async

put(entries: Sequence[RejectedEntity]) -> None

unresolved async

unresolved(
    limit: int = 100, after: str | None = None
) -> list[RejectedEntity]

resolve async

resolve(
    entry_id: str,
    resolution: str,
    corrected: Mapping[str, Any] | None = None,
) -> None

count async

count(*, unresolved_only: bool = True) -> int

resolution_of async

resolution_of(
    entry_id: str,
) -> tuple[str | None, dict[str, Any] | None]

Return the resolution and corrected entity recorded for entry_id.

Raises:

Type Description
KeyError

No entry has entry_id.

AsyncSqliteRejectionStore

AsyncSqliteRejectionStore(
    path: str | Path,
    now: Callable[[], datetime] | None = None,
)

Bases: AsyncRejectionStore

A rejection store kept in a SQLite file, so a review can span runs.

The file and its parent folder are created on first use, and the file records its schema version in PRAGMA user_version. Every SQLite failure, including a file written by a newer sci-etl-core, raises :class:~sci_etl_core.exceptions.ClaimStoreError. Close it with :meth:aclose. Use each instance from one event loop.

put async

put(entries: Sequence[RejectedEntity]) -> None

unresolved async

unresolved(
    limit: int = 100, after: str | None = None
) -> list[RejectedEntity]

resolve async

resolve(
    entry_id: str,
    resolution: str,
    corrected: Mapping[str, Any] | None = None,
) -> None

count async

count(*, unresolved_only: bool = True) -> int

resolution_of async

resolution_of(
    entry_id: str,
) -> tuple[str | None, dict[str, Any] | None]

Return the resolution and corrected entity recorded for entry_id.

Raises:

Type Description
KeyError

No entry has entry_id.

aclose async

aclose() -> None

Close the connection; a later call reopens it.

rejection_id

rejection_id(
    record_id: str,
    entity: Mapping[str, Any],
    violations: Sequence[Violation],
    stamp: ExtractionStamp | None,
) -> str

Return the id of a rejection: a digest of its record, entity, violation codes and fields, and stamp.