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.
Files changed (51) hide show
  1. claims/__init__.py +132 -0
  2. claims/__main__.py +5 -0
  3. claims/adapters/__init__.py +6 -0
  4. claims/adapters/array.py +212 -0
  5. claims/adapters/base.py +75 -0
  6. claims/adapters/cells.py +94 -0
  7. claims/adapters/columnar.py +188 -0
  8. claims/adapters/prose.py +261 -0
  9. claims/adapters/reads.py +47 -0
  10. claims/adapters/rows.py +116 -0
  11. claims/adapters/sheet.py +149 -0
  12. claims/adapters/sqlite.py +73 -0
  13. claims/adapters/table.py +135 -0
  14. claims/adapters/tree.py +163 -0
  15. claims/checks.py +511 -0
  16. claims/cli.py +1103 -0
  17. claims/core/__init__.py +9 -0
  18. claims/core/artifacts.py +121 -0
  19. claims/core/checks.py +132 -0
  20. claims/core/claims.py +100 -0
  21. claims/core/evidence.py +708 -0
  22. claims/core/manifest.py +349 -0
  23. claims/core/numbers.py +146 -0
  24. claims/core/outcomes.py +410 -0
  25. claims/core/report.py +503 -0
  26. claims/corpus.py +344 -0
  27. claims/exceptions.py +106 -0
  28. claims/export/__init__.py +23 -0
  29. claims/export/rocrate.py +787 -0
  30. claims/export/sarif.py +401 -0
  31. claims/manifest.py +59 -0
  32. claims/models.py +222 -0
  33. claims/numbers.py +453 -0
  34. claims/origins.py +303 -0
  35. claims/policy.py +369 -0
  36. claims/provenance.py +42 -0
  37. claims/record.py +1099 -0
  38. claims/regression.py +171 -0
  39. claims/renderers/__init__.py +10 -0
  40. claims/renderers/sarif.py +294 -0
  41. claims/reruns.py +111 -0
  42. claims/resolve.py +116 -0
  43. claims/stale.py +333 -0
  44. claims/starter.py +432 -0
  45. claims/toolchain.py +120 -0
  46. claims/verify.py +1371 -0
  47. claims/why.py +398 -0
  48. claims_cli-0.6.0.dist-info/METADATA +139 -0
  49. claims_cli-0.6.0.dist-info/RECORD +51 -0
  50. claims_cli-0.6.0.dist-info/WHEEL +4 -0
  51. claims_cli-0.6.0.dist-info/entry_points.txt +2 -0
@@ -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