claims-cli 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claims/__init__.py +132 -0
- claims/__main__.py +5 -0
- claims/adapters/__init__.py +6 -0
- claims/adapters/array.py +212 -0
- claims/adapters/base.py +75 -0
- claims/adapters/cells.py +94 -0
- claims/adapters/columnar.py +188 -0
- claims/adapters/prose.py +261 -0
- claims/adapters/reads.py +47 -0
- claims/adapters/rows.py +116 -0
- claims/adapters/sheet.py +149 -0
- claims/adapters/sqlite.py +73 -0
- claims/adapters/table.py +135 -0
- claims/adapters/tree.py +163 -0
- claims/checks.py +511 -0
- claims/cli.py +1103 -0
- claims/core/__init__.py +9 -0
- claims/core/artifacts.py +121 -0
- claims/core/checks.py +132 -0
- claims/core/claims.py +100 -0
- claims/core/evidence.py +708 -0
- claims/core/manifest.py +349 -0
- claims/core/numbers.py +146 -0
- claims/core/outcomes.py +410 -0
- claims/core/report.py +503 -0
- claims/corpus.py +344 -0
- claims/exceptions.py +106 -0
- claims/export/__init__.py +23 -0
- claims/export/rocrate.py +787 -0
- claims/export/sarif.py +401 -0
- claims/manifest.py +59 -0
- claims/models.py +222 -0
- claims/numbers.py +453 -0
- claims/origins.py +303 -0
- claims/policy.py +369 -0
- claims/provenance.py +42 -0
- claims/record.py +1099 -0
- claims/regression.py +171 -0
- claims/renderers/__init__.py +10 -0
- claims/renderers/sarif.py +294 -0
- claims/reruns.py +111 -0
- claims/resolve.py +116 -0
- claims/stale.py +333 -0
- claims/starter.py +432 -0
- claims/toolchain.py +120 -0
- claims/verify.py +1371 -0
- claims/why.py +398 -0
- claims_cli-0.6.0.dist-info/METADATA +139 -0
- claims_cli-0.6.0.dist-info/RECORD +51 -0
- claims_cli-0.6.0.dist-info/WHEEL +4 -0
- claims_cli-0.6.0.dist-info/entry_points.txt +2 -0
claims/core/outcomes.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""The verdict vocabulary: three stages, one derived outcome, and why it obtained.
|
|
2
|
+
|
|
3
|
+
Execution, extraction and comparison are reported separately. Collapsing them is how a
|
|
4
|
+
missing extractor becomes a quotation that failed to check out."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
from claims.core.evidence import Conversion, Statistic
|
|
13
|
+
|
|
14
|
+
SCHEMA_VERSION = "repro/1"
|
|
15
|
+
|
|
16
|
+
_SHA256_HEX = r"^[a-f0-9]{64}$"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------------- outcomes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExecutionStatus(enum.StrEnum):
|
|
23
|
+
COMPLETED = "completed"
|
|
24
|
+
UNAVAILABLE = "unavailable"
|
|
25
|
+
"""The backend could not run: a missing binary, an unreadable file, an absent registry."""
|
|
26
|
+
FAILED = "failed"
|
|
27
|
+
"""The backend raised something unexpected. A defect, never a scientific outcome."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExtractionStatus(enum.StrEnum):
|
|
31
|
+
EXTRACTED = "extracted"
|
|
32
|
+
ABSENT = "absent"
|
|
33
|
+
"""The artifact was read and holds no such value. Silence, which is not contradiction."""
|
|
34
|
+
INVALID = "invalid"
|
|
35
|
+
"""A value was located and is not of the type the assertion requires."""
|
|
36
|
+
NOT_ATTEMPTED = "not_attempted"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ComparisonStatus(enum.StrEnum):
|
|
40
|
+
MATCH = "match"
|
|
41
|
+
MISMATCH = "mismatch"
|
|
42
|
+
NOT_APPLICABLE = "not_applicable"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Outcome(enum.StrEnum):
|
|
46
|
+
"""The flattened view, for display and for tools that want one field.
|
|
47
|
+
|
|
48
|
+
Derived from the three stages, never set directly, so the flattening cannot drift from
|
|
49
|
+
what the stages say.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
VERIFIED = "verified"
|
|
53
|
+
MISMATCH = "mismatch"
|
|
54
|
+
NOT_FOUND = "not_found"
|
|
55
|
+
UNCHECKED = "unchecked"
|
|
56
|
+
ERROR = "error"
|
|
57
|
+
NOT_OFFERED = "not_offered"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Ordering(enum.StrEnum):
|
|
61
|
+
"""Whether a confirmatory claim's evidence was produced after the plan it names.
|
|
62
|
+
|
|
63
|
+
The outcome is separate from the reason, on the same principle the evidence stages
|
|
64
|
+
follow: a check that could not run is not a check that failed. `unchecked` carries an
|
|
65
|
+
`OrderingReason` saying which of nine conditions stopped it, because collapsing a
|
|
66
|
+
missing run record, an unpinned plan, and an unlinked output into one word loses the
|
|
67
|
+
only information that says what to fix.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
ORDERED = "ordered"
|
|
71
|
+
"""Every run producing this claim's evidence started after its plan was registered."""
|
|
72
|
+
VIOLATED = "violated"
|
|
73
|
+
"""A run started before the plan it names was registered."""
|
|
74
|
+
UNCHECKED = "unchecked"
|
|
75
|
+
"""The record does not settle it. Not a finding that the ordering is wrong."""
|
|
76
|
+
NOT_APPLICABLE = "not_applicable"
|
|
77
|
+
"""The claim is not confirmatory, so there is no registration for it to postdate."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class OrderingReason(enum.StrEnum):
|
|
81
|
+
"""Why an ordering check reached the outcome it did."""
|
|
82
|
+
|
|
83
|
+
RUN_FOLLOWS_REGISTRATION = "run_follows_registration"
|
|
84
|
+
RUN_PRECEDES_REGISTRATION = "run_precedes_registration"
|
|
85
|
+
NO_RUN_RECORD = "no_run_record"
|
|
86
|
+
NO_EVIDENCE_OFFERED = "no_evidence_offered"
|
|
87
|
+
"""The claim names no evidence, so it names no artifact and no run can produce one. Separate
|
|
88
|
+
from `no_run_record`, which sends a reader looking for a run that should exist: here nothing
|
|
89
|
+
was offered to order against, and the fix is to declare evidence."""
|
|
90
|
+
NO_REGISTERED_PLAN = "no_registered_plan"
|
|
91
|
+
REGISTERED_PLAN_UNPINNED = "registered_plan_unpinned"
|
|
92
|
+
REGISTERED_PLAN_CHANGED = "registered_plan_changed"
|
|
93
|
+
RUN_OUTPUT_UNLINKED = "run_output_unlinked"
|
|
94
|
+
"""The run names an artifact but records no digest for it, so a later file with the same
|
|
95
|
+
name can be presented as the output of an earlier run."""
|
|
96
|
+
RUN_OUTPUT_CHANGED = "run_output_changed"
|
|
97
|
+
TIMESTAMP_MISSING = "timestamp_missing"
|
|
98
|
+
AMBIGUOUS_PRODUCING_RUN = "ambiguous_producing_run"
|
|
99
|
+
NOT_CONFIRMATORY = "not_confirmatory"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RegistrationAuthority(enum.StrEnum):
|
|
103
|
+
"""Who attests to the registration timestamp.
|
|
104
|
+
|
|
105
|
+
A hash and two self-recorded timestamps establish internal consistency, not chronology:
|
|
106
|
+
an actor who can write `registered_at`, `started_at` and the plan digest after seeing
|
|
107
|
+
results can manufacture an ordered history. Recording the authority makes that limit a
|
|
108
|
+
field rather than a caveat, and lets a policy require better than the weakest one.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
SELF_RECORDED = "self_recorded"
|
|
112
|
+
GIT_REMOTE = "git_remote"
|
|
113
|
+
OSF = "osf"
|
|
114
|
+
ZENODO = "zenodo"
|
|
115
|
+
TRUSTED_TIMESTAMP = "trusted_timestamp"
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def rank(self) -> int:
|
|
119
|
+
return _AUTHORITY_RANK[self]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
_AUTHORITY_RANK = {
|
|
123
|
+
RegistrationAuthority.SELF_RECORDED: 0,
|
|
124
|
+
RegistrationAuthority.GIT_REMOTE: 1,
|
|
125
|
+
RegistrationAuthority.OSF: 2,
|
|
126
|
+
RegistrationAuthority.ZENODO: 2,
|
|
127
|
+
RegistrationAuthority.TRUSTED_TIMESTAMP: 3,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Validity(enum.StrEnum):
|
|
132
|
+
"""Whether a decision describes the artifact the manifest declared."""
|
|
133
|
+
|
|
134
|
+
AUTHORITATIVE = "authoritative"
|
|
135
|
+
UNPINNED_ARTIFACT = "unpinned_artifact"
|
|
136
|
+
"""No digest was recorded, so the file read may not be the file meant."""
|
|
137
|
+
BROKEN_PIN = "broken_pin"
|
|
138
|
+
"""The file read is provably not the file that was pinned. The comparison still ran and
|
|
139
|
+
its result is reported, marked non-authoritative, because a diagnostic is more useful than
|
|
140
|
+
a blank."""
|
|
141
|
+
ARTIFACT_ABSENT = "artifact_absent"
|
|
142
|
+
"""Nothing exists at the declared path. No comparison ran, so no decision against this
|
|
143
|
+
artifact describes anything that was read."""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Reason(enum.StrEnum):
|
|
147
|
+
"""Why an outcome obtained. Machine-readable, so a tool can route by cause."""
|
|
148
|
+
|
|
149
|
+
PASSAGE_PRESENT = "passage_present"
|
|
150
|
+
VALUE_MATCH = "value_match"
|
|
151
|
+
PASSAGE_ABSENT = "passage_absent"
|
|
152
|
+
VALUE_MISMATCH = "value_mismatch"
|
|
153
|
+
POINTER_ABSENT = "pointer_absent"
|
|
154
|
+
COLUMN_ABSENT = "column_absent"
|
|
155
|
+
ROW_ABSENT = "row_absent"
|
|
156
|
+
ROW_AMBIGUOUS = "row_ambiguous"
|
|
157
|
+
ROW_SELECTOR_INVALID = "row_selector_invalid"
|
|
158
|
+
SELECTOR_NOT_SCALAR = "selector_not_scalar"
|
|
159
|
+
"""The locator resolved to a container. A list or mapping is not a value."""
|
|
160
|
+
FORMAT_UNSUPPORTED = "format_unsupported"
|
|
161
|
+
"""No adapter addresses this format. Reported rather than approximated: falling back to
|
|
162
|
+
searching the file for the printed number would find it wherever it appears and call
|
|
163
|
+
that verification."""
|
|
164
|
+
WRONG_PAGE = "wrong_page"
|
|
165
|
+
"""The passage occurs in the artifact, on a different page than the one asserted."""
|
|
166
|
+
VALUE_NOT_NUMERIC = "value_not_numeric"
|
|
167
|
+
PLACES_MISMATCH = "places_mismatch"
|
|
168
|
+
"""The assertion declares how many decimal places the manuscript reports the value to,
|
|
169
|
+
and the value as printed is written to another number. Distinct from `value_mismatch`
|
|
170
|
+
because the file is not what disagrees: the two statements about the manuscript do, and
|
|
171
|
+
the fix is in the manuscript or in the declaration."""
|
|
172
|
+
PASSAGE_AMBIGUOUS = "passage_ambiguous"
|
|
173
|
+
"""One pair of prose anchors selected two different values, so the document states two
|
|
174
|
+
numbers where the assertion addresses one. Reported rather than resolved to the first: a
|
|
175
|
+
document contradicting itself is a finding."""
|
|
176
|
+
NUMBER_AS_WORD = "number_as_word"
|
|
177
|
+
"""The locator selected an English cardinal written out, under a locator that did not ask
|
|
178
|
+
for one. Distinct from `value_not_numeric` because the fix is different: the value is
|
|
179
|
+
there and a reader would call it a number, so an author told only that no number was found
|
|
180
|
+
goes looking for a broken anchor."""
|
|
181
|
+
QUOTATION_AMBIGUOUS = "quotation_ambiguous"
|
|
182
|
+
"""The quoted passage occurs more than once in the artifact, and the record does not say
|
|
183
|
+
which occurrence it means. Distinct from `passage_ambiguous`, which says the document states
|
|
184
|
+
two different numbers: a passage written twice contradicts nothing, and the fix is a
|
|
185
|
+
`prefix` or `suffix` on the quote assertion, naming the text on either side of the
|
|
186
|
+
occurrence meant, not an edit to the document. A `prefix` or `suffix` that singles out no
|
|
187
|
+
occurrence leaves this reason standing. The two shared a reason
|
|
188
|
+
after `citations`' `ambiguous` state, which `repro verify` reported as `KeyError: 'ambiguous'`
|
|
189
|
+
under 0.4.2, was first mapped."""
|
|
190
|
+
EXTRACTORS_DISAGREE = "extractors_disagree"
|
|
191
|
+
"""Two extractors that both read the artifact disagree about whether the passage is in it.
|
|
192
|
+
Distinct from `passage_absent`, which says the artifact was read and does not contain it --
|
|
193
|
+
an accusation against the manuscript. This one accuses nothing and asks for a better reader,
|
|
194
|
+
and it shared `passage_ambiguous` until an author was told a document contradicted itself
|
|
195
|
+
when the real answer was that two readers of it did."""
|
|
196
|
+
EXTRACTOR_MISSING = "extractor_missing"
|
|
197
|
+
ARTIFACT_MISSING = "artifact_missing"
|
|
198
|
+
ARTIFACT_UNREADABLE = "artifact_unreadable"
|
|
199
|
+
ARTIFACT_UNDECLARED = "artifact_undeclared"
|
|
200
|
+
BACKEND_DEFECT = "backend_defect"
|
|
201
|
+
NOT_OFFERED = "not_offered"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class Warning_(enum.StrEnum):
|
|
205
|
+
"""Notes about the evidence itself, orthogonal to the outcome."""
|
|
206
|
+
|
|
207
|
+
SHORT = "short"
|
|
208
|
+
TRUNCATED = "truncated"
|
|
209
|
+
NORMALIZED = "normalized"
|
|
210
|
+
WRONG_PAGE = "wrong_page"
|
|
211
|
+
PAGE_UNCHECKED = "page_unchecked"
|
|
212
|
+
"""The quote assertion declares a `page`, the passage was found, and nothing checked the
|
|
213
|
+
page: the artifact is not read page by page, or the passage matched only with spacing
|
|
214
|
+
ignored. The passage is in the artifact, so the assertion is `verified`; the page is
|
|
215
|
+
neither confirmed nor refuted, and a policy grades it as it grades a check that did not
|
|
216
|
+
run."""
|
|
217
|
+
POSITIONAL_ADDRESS = "positional_address"
|
|
218
|
+
"""Addressed by row index, which names a different cell if the table is reordered."""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class Tie(BaseModel):
|
|
222
|
+
"""A compared value exactly halfway between two values at the comparison precision.
|
|
223
|
+
|
|
224
|
+
Rounding half up prints one neighbor and rounding half to even may print the other, and
|
|
225
|
+
software is divided between the two, so a manuscript printing either has rounded
|
|
226
|
+
correctly. Both verify. The record says that it was a tie, so the acceptance is visible
|
|
227
|
+
and is not mistaken for an ordinary agreement.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
231
|
+
|
|
232
|
+
lower: str
|
|
233
|
+
upper: str
|
|
234
|
+
"""The two values the compared one lies halfway between, at the printed precision."""
|
|
235
|
+
|
|
236
|
+
printed: str
|
|
237
|
+
"""The value the manuscript prints. `lower` or `upper` where the assertion verified, and
|
|
238
|
+
neither where it is a mismatch."""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class DecisionSide(BaseModel):
|
|
242
|
+
"""What one side of a two-sided assertion read, and where it read it.
|
|
243
|
+
|
|
244
|
+
`Decision` names one artifact and one locator, which is all a `metric` or a `quote` has.
|
|
245
|
+
A correspondence reads two, and recording only one would leave half the decision unbound:
|
|
246
|
+
the locator digest exists so a selector edited after the fact changes the record, and a
|
|
247
|
+
selector nothing records is a selector nothing binds.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
251
|
+
|
|
252
|
+
name: str
|
|
253
|
+
artifact_id: str
|
|
254
|
+
artifact_digest: str | None = None
|
|
255
|
+
locator_digest: str | None = None
|
|
256
|
+
validity: Validity = Validity.AUTHORITATIVE
|
|
257
|
+
extracted: str | None = None
|
|
258
|
+
"""The value read, in the representation the artifact used. `None` where this side did
|
|
259
|
+
not extract, which is why the decision reports no comparison."""
|
|
260
|
+
|
|
261
|
+
conversion: Conversion | None = None
|
|
262
|
+
"""The conversion this side declares, or None."""
|
|
263
|
+
|
|
264
|
+
converted: str | None = None
|
|
265
|
+
"""`extracted` under `conversion`, which is the value compared. None where no conversion
|
|
266
|
+
is declared, or where the side read no number to convert."""
|
|
267
|
+
|
|
268
|
+
extraction_digest: str = ""
|
|
269
|
+
"""sha256 of what this side's extractor produced. `unknown` where the side produced
|
|
270
|
+
nothing to hash.
|
|
271
|
+
|
|
272
|
+
Per side rather than per decision because the decision-level digest covers both
|
|
273
|
+
extractions at once and so cannot say which of the two moved, and a correspondence exists
|
|
274
|
+
to be told which of two artifacts changed."""
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class DecisionMember(BaseModel):
|
|
278
|
+
"""What one member of an aggregate read, and where it read it.
|
|
279
|
+
|
|
280
|
+
One per member, in the order the manifest lists them, so a report can show every value
|
|
281
|
+
the statistic was computed over and a deleted or edited member changes the record.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
285
|
+
|
|
286
|
+
artifact_id: str
|
|
287
|
+
artifact_digest: str | None = None
|
|
288
|
+
locator_digest: str | None = None
|
|
289
|
+
validity: Validity = Validity.AUTHORITATIVE
|
|
290
|
+
extracted: str | None = None
|
|
291
|
+
"""The value read, in the representation the artifact used. `None` where this member did
|
|
292
|
+
not extract, which is why the decision reports no comparison."""
|
|
293
|
+
|
|
294
|
+
converted: str | None = None
|
|
295
|
+
"""`extracted` under the assertion's `conversion`, which is the value the statistic is
|
|
296
|
+
over. None where no conversion is declared, or where a member read no number."""
|
|
297
|
+
|
|
298
|
+
extraction_digest: str = ""
|
|
299
|
+
"""sha256 of what this member's extractor produced. `unknown` where it produced nothing
|
|
300
|
+
to hash."""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Decision(BaseModel):
|
|
304
|
+
"""The outcome of evaluating one evidence assertion. Frozen: a decision that can be
|
|
305
|
+
edited after the fact records nothing."""
|
|
306
|
+
|
|
307
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
308
|
+
|
|
309
|
+
claim_id: str
|
|
310
|
+
claim_digest: str
|
|
311
|
+
kind: str
|
|
312
|
+
|
|
313
|
+
execution: ExecutionStatus
|
|
314
|
+
extraction: ExtractionStatus
|
|
315
|
+
comparison: ComparisonStatus
|
|
316
|
+
reason: Reason
|
|
317
|
+
detail: str = ""
|
|
318
|
+
|
|
319
|
+
validity: Validity = Validity.AUTHORITATIVE
|
|
320
|
+
artifact_id: str = ""
|
|
321
|
+
artifact_digest: str | None = None
|
|
322
|
+
"""The digest of the bytes actually read, so the decision can be re-checked against the
|
|
323
|
+
same content rather than the same path."""
|
|
324
|
+
|
|
325
|
+
locator_digest: str | None = None
|
|
326
|
+
"""The digest of the canonical locator, so a decision binds how the value was addressed
|
|
327
|
+
as well as what was read. A selector edited after the fact changes this even where the
|
|
328
|
+
artifact is untouched."""
|
|
329
|
+
|
|
330
|
+
extracted: str | None = None
|
|
331
|
+
"""The value read from the one artifact, in the representation the artifact used. None
|
|
332
|
+
where no value was read, and for a kind that reads no value or reads two: a
|
|
333
|
+
`correspondence` records what each side read in `sides`."""
|
|
334
|
+
|
|
335
|
+
conversion: Conversion | None = None
|
|
336
|
+
"""The conversion the assertion declares, or None."""
|
|
337
|
+
|
|
338
|
+
converted: str | None = None
|
|
339
|
+
"""`extracted` under `conversion`, which is the value compared with the printed one. None
|
|
340
|
+
where no conversion is declared, or where no number was read to convert."""
|
|
341
|
+
|
|
342
|
+
tie: Tie | None = None
|
|
343
|
+
"""Set where the compared value lay exactly halfway between two values at the printed
|
|
344
|
+
precision. None otherwise, and under `absolute` and `relative`, which round nothing."""
|
|
345
|
+
|
|
346
|
+
sides: tuple[DecisionSide, ...] = ()
|
|
347
|
+
"""The two values a `correspondence` compared, each with the artifact and locator it came
|
|
348
|
+
from. Empty for every kind that reads one artifact, which records its single artifact and
|
|
349
|
+
locator in the fields above."""
|
|
350
|
+
|
|
351
|
+
members: tuple[DecisionMember, ...] = ()
|
|
352
|
+
"""The values an `aggregate` was computed over, each with the artifact and locator it
|
|
353
|
+
came from. Empty for every other kind."""
|
|
354
|
+
|
|
355
|
+
statistic: Statistic | None = None
|
|
356
|
+
"""What an `aggregate` computed over `members`. None for every other kind."""
|
|
357
|
+
|
|
358
|
+
computed: str | None = None
|
|
359
|
+
"""The statistic of the members' values, which is what was compared with the printed
|
|
360
|
+
value. Written exactly where it is a terminating decimal, and otherwise rounded half-even
|
|
361
|
+
to three places past the precision the manuscript prints; the comparison is made on the
|
|
362
|
+
exact value either way. None where a member held no number."""
|
|
363
|
+
|
|
364
|
+
backend: str = ""
|
|
365
|
+
backend_version: str = ""
|
|
366
|
+
"""The protocol version of the backend's interface. Named because the same inputs can
|
|
367
|
+
receive different decisions after a backend upgrade, and a stored decision that does not
|
|
368
|
+
say which backend produced it cannot be compared with a later one. It says nothing about
|
|
369
|
+
the program that read the artifact; `tool_version` does."""
|
|
370
|
+
|
|
371
|
+
tool: str = ""
|
|
372
|
+
"""What performs the extraction: `pdftotext` for a quotation, the distribution supplying
|
|
373
|
+
the format adapters for a value. A separate field from `backend_version` because the two
|
|
374
|
+
move independently -- an extractor upgraded under a backend that did not change reads a
|
|
375
|
+
ligature or a column differently while the protocol version stays where it was."""
|
|
376
|
+
|
|
377
|
+
tool_version: str = ""
|
|
378
|
+
"""The version `tool` reports, as it reports it, or `unknown` where it could not be
|
|
379
|
+
interrogated. Empty only on a decision no backend produced: a version that was sought and
|
|
380
|
+
not obtained has to be distinguishable from one that was never sought."""
|
|
381
|
+
|
|
382
|
+
extraction_digest: str = ""
|
|
383
|
+
"""sha256 of what the extractor produced -- the whole extracted text for a quotation, the
|
|
384
|
+
extracted value for a number. `tool_version` catches drift that announces itself; this
|
|
385
|
+
catches drift from any cause, including a rebuilt binary reporting the same version and an
|
|
386
|
+
environment that changes an encoding path. `unknown` where the extraction produced nothing
|
|
387
|
+
to hash, empty only on a decision no backend produced.
|
|
388
|
+
|
|
389
|
+
Recorded and not enforced. Whether a changed extraction invalidates a stored decision the
|
|
390
|
+
way a changed artifact does is a policy question, and `claims.policy` decides policy."""
|
|
391
|
+
|
|
392
|
+
warnings: tuple[Warning_, ...] = ()
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def outcome(self) -> Outcome:
|
|
396
|
+
if self.execution is ExecutionStatus.FAILED:
|
|
397
|
+
return Outcome.ERROR
|
|
398
|
+
if self.execution is ExecutionStatus.UNAVAILABLE:
|
|
399
|
+
return Outcome.UNCHECKED
|
|
400
|
+
if self.extraction in (ExtractionStatus.ABSENT, ExtractionStatus.INVALID):
|
|
401
|
+
return Outcome.NOT_FOUND
|
|
402
|
+
if self.comparison is ComparisonStatus.MATCH:
|
|
403
|
+
return Outcome.VERIFIED
|
|
404
|
+
if self.comparison is ComparisonStatus.MISMATCH:
|
|
405
|
+
return Outcome.MISMATCH
|
|
406
|
+
return Outcome.UNCHECKED
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def is_authoritative(self) -> bool:
|
|
410
|
+
return self.validity is Validity.AUTHORITATIVE
|