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/why.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""For one claim: what the record establishes, and what it does not.
|
|
2
|
+
|
|
3
|
+
`repro verify` answers for every assertion and `repro stale` for every file. This puts the two
|
|
4
|
+
answers for one claim side by side, with the commands and files upstream of what it reads, and
|
|
5
|
+
then lists what nothing in the record covers: a file no command is recorded for, a command with
|
|
6
|
+
no re-run that reproduced, an assertion that was not checked.
|
|
7
|
+
|
|
8
|
+
Under each assertion it also puts what `claims.checks` recorded of it: what it declared and how
|
|
9
|
+
it came out at each `repro verify --record` that found it changed.
|
|
10
|
+
|
|
11
|
+
Nothing is evaluated here that those two do not evaluate. Each assertion goes through
|
|
12
|
+
`claims.verify.check_evidence`, each state comes from `claims.stale.stale`, and the chain is the
|
|
13
|
+
part of that report's graph `repro.regenerate.upstream` reaches from the files the claim reads.
|
|
14
|
+
Executes nothing and writes nothing.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import collections
|
|
20
|
+
import difflib
|
|
21
|
+
import pathlib
|
|
22
|
+
|
|
23
|
+
from claims.adapters.reads import reading_once
|
|
24
|
+
from claims.checks import Records, address, checked
|
|
25
|
+
from claims.exceptions import ReproError
|
|
26
|
+
from claims.models import (
|
|
27
|
+
AggregateEvidence,
|
|
28
|
+
AssertionAccount,
|
|
29
|
+
ChainNode,
|
|
30
|
+
Claim,
|
|
31
|
+
ClaimAccount,
|
|
32
|
+
Conversion,
|
|
33
|
+
CorrespondenceEvidence,
|
|
34
|
+
Decision,
|
|
35
|
+
Evidence,
|
|
36
|
+
FileRead,
|
|
37
|
+
Freshness,
|
|
38
|
+
Manifest,
|
|
39
|
+
NotEstablished,
|
|
40
|
+
NotEstablishedKind,
|
|
41
|
+
Outcome,
|
|
42
|
+
QuoteEvidence,
|
|
43
|
+
StaleCause,
|
|
44
|
+
TableCellEvidence,
|
|
45
|
+
Validity,
|
|
46
|
+
)
|
|
47
|
+
from claims.reruns import upstream
|
|
48
|
+
from claims.resolve import stored
|
|
49
|
+
from claims.stale import stale
|
|
50
|
+
from claims.verify import DEFAULT_BACKENDS, Backend, artifact_states, check_evidence
|
|
51
|
+
|
|
52
|
+
#: Ids named in an error. A manifest can declare thousands.
|
|
53
|
+
MAX_LISTED = 10
|
|
54
|
+
|
|
55
|
+
#: What the design does not check. An account ends on each that applies to it: the first where
|
|
56
|
+
#: a command is recorded, the second where an assertion has an address, the third where there
|
|
57
|
+
#: is an assertion.
|
|
58
|
+
UNDECLARED_INPUTS = (
|
|
59
|
+
"the inputs each command declares are the author's list; a file a command reads and does "
|
|
60
|
+
"not declare is invisible"
|
|
61
|
+
)
|
|
62
|
+
ADDRESS_IS_CHOSEN = (
|
|
63
|
+
"the address is the author's choice; nothing checks that it holds the quantity the "
|
|
64
|
+
"sentence means"
|
|
65
|
+
)
|
|
66
|
+
COMPARISON_ONLY = (
|
|
67
|
+
"a verified assertion says the declared value or passage is in the file, not that the "
|
|
68
|
+
"analysis is right"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
#: What is said of a file no command is recorded for, by what its pin found.
|
|
72
|
+
_TAKEN_AS_GIVEN = {
|
|
73
|
+
Validity.AUTHORITATIVE: "its bytes are pinned and taken as given",
|
|
74
|
+
Validity.UNPINNED_ARTIFACT: "its bytes are not pinned and are taken as given",
|
|
75
|
+
Validity.BROKEN_PIN: "its bytes are not the pinned ones and are taken as given",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_NOT_CHECKED = {
|
|
79
|
+
Outcome.UNCHECKED: NotEstablishedKind.UNCHECKED,
|
|
80
|
+
Outcome.NOT_FOUND: NotEstablishedKind.NOT_FOUND,
|
|
81
|
+
Outcome.ERROR: NotEstablishedKind.ERROR,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class UnknownClaimError(ReproError):
|
|
86
|
+
"""A claim was named that the manifest does not declare."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, unknown: str, declared: tuple[str, ...]) -> None:
|
|
89
|
+
self.unknown = unknown
|
|
90
|
+
self.declared = declared
|
|
91
|
+
near = tuple(difflib.get_close_matches(unknown, declared, n=MAX_LISTED))
|
|
92
|
+
listed = near or declared[:MAX_LISTED]
|
|
93
|
+
more = len(declared) - len(listed)
|
|
94
|
+
super().__init__(
|
|
95
|
+
f"the manifest declares no claim {unknown!r}; "
|
|
96
|
+
+ ("nearest: " if near else "declared: ")
|
|
97
|
+
+ (", ".join(listed) or "none")
|
|
98
|
+
+ (f", and {more} more" if more > 0 and not near else "")
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
#: One file an assertion reads: the artifact, the side it is, the address, the value stored
|
|
103
|
+
#: there, and the conversion declared for it with the value under it.
|
|
104
|
+
Held = tuple[str, str, str, str | None, Conversion | None, str | None]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _held(evidence: Evidence, decision: Decision, paths: dict[str, pathlib.Path]) -> list[Held]:
|
|
108
|
+
"""Each file an assertion reads, and what was found at the address in it."""
|
|
109
|
+
if isinstance(evidence, QuoteEvidence):
|
|
110
|
+
return [(evidence.artifact, "", "", None, None, None)]
|
|
111
|
+
if isinstance(evidence, CorrespondenceEvidence):
|
|
112
|
+
read = {side.name: side for side in decision.sides}
|
|
113
|
+
return [
|
|
114
|
+
(
|
|
115
|
+
side.artifact,
|
|
116
|
+
side.name,
|
|
117
|
+
address(side.locator),
|
|
118
|
+
read[side.name].extracted if side.name in read else None,
|
|
119
|
+
side.conversion,
|
|
120
|
+
read[side.name].converted if side.name in read else None,
|
|
121
|
+
)
|
|
122
|
+
for side in evidence.sides
|
|
123
|
+
]
|
|
124
|
+
if isinstance(evidence, AggregateEvidence):
|
|
125
|
+
# No member was read where a file is absent or undeclared: the engine stops before
|
|
126
|
+
# the backend, and the decision lists none.
|
|
127
|
+
read = [(m.extracted, m.converted) for m in decision.members]
|
|
128
|
+
return [
|
|
129
|
+
(member.artifact, "", address(member.locator), stored, evidence.conversion, converted)
|
|
130
|
+
for member, (stored, converted) in zip(
|
|
131
|
+
evidence.members, read or [(None, None)] * evidence.count, strict=True
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
if isinstance(evidence, TableCellEvidence) and not evidence.addresses_one_row:
|
|
135
|
+
# Neither `row` nor `where`, or both: there is no one address to show.
|
|
136
|
+
return [(evidence.artifact, "", "", None, None, None)]
|
|
137
|
+
path = paths.get(evidence.artifact)
|
|
138
|
+
return [
|
|
139
|
+
(
|
|
140
|
+
evidence.artifact,
|
|
141
|
+
"",
|
|
142
|
+
address(evidence.locator),
|
|
143
|
+
stored(evidence, path) if path is not None else None,
|
|
144
|
+
evidence.conversion,
|
|
145
|
+
decision.converted,
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _not_established(
|
|
151
|
+
claim: Claim, assertions: tuple[AssertionAccount, ...], chain: tuple[ChainNode, ...]
|
|
152
|
+
) -> tuple[NotEstablished, ...]:
|
|
153
|
+
"""What the record does not cover for this claim, then what the design does not check."""
|
|
154
|
+
lines = []
|
|
155
|
+
for node in chain:
|
|
156
|
+
if node.regeneration is None and node.validity in _TAKEN_AS_GIVEN:
|
|
157
|
+
lines.append(
|
|
158
|
+
NotEstablished(
|
|
159
|
+
kind=NotEstablishedKind.NO_PRODUCER,
|
|
160
|
+
subject=node.path or node.artifact,
|
|
161
|
+
said=f"no command is recorded for it; {_TAKEN_AS_GIVEN[node.validity]}",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
for node in chain:
|
|
165
|
+
if node.validity is Validity.ARTIFACT_ABSENT:
|
|
166
|
+
lines.append(
|
|
167
|
+
NotEstablished(
|
|
168
|
+
kind=NotEstablishedKind.ABSENT,
|
|
169
|
+
subject=node.path or node.artifact,
|
|
170
|
+
said="nothing at the declared path",
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
elif node.regeneration is not None and node.validity is Validity.BROKEN_PIN:
|
|
174
|
+
lines.append(
|
|
175
|
+
NotEstablished(
|
|
176
|
+
kind=NotEstablishedKind.BROKEN_PIN,
|
|
177
|
+
subject=node.path or node.artifact,
|
|
178
|
+
said="its bytes are not the pinned ones",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
for node in chain:
|
|
182
|
+
record = node.regeneration
|
|
183
|
+
if record is None:
|
|
184
|
+
continue
|
|
185
|
+
# A record on a cycle is compared with no re-run, whether or not one is recorded.
|
|
186
|
+
if StaleCause.CYCLE in record.causes:
|
|
187
|
+
lines.append(
|
|
188
|
+
NotEstablished(
|
|
189
|
+
kind=NotEstablishedKind.CYCLE,
|
|
190
|
+
subject=record.id,
|
|
191
|
+
said="on a cycle of commands, so it is compared with no re-run",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
elif record.rerun_at is None:
|
|
195
|
+
lines.append(
|
|
196
|
+
NotEstablished(
|
|
197
|
+
kind=NotEstablishedKind.NEVER_RERUN,
|
|
198
|
+
subject=record.id,
|
|
199
|
+
said="no re-run that reproduced is recorded; nothing in the record shows "
|
|
200
|
+
f"the command produces {node.path or node.artifact}",
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
if not claim.evidence:
|
|
204
|
+
lines.append(
|
|
205
|
+
NotEstablished(
|
|
206
|
+
kind=NotEstablishedKind.NO_EVIDENCE,
|
|
207
|
+
subject=claim.id,
|
|
208
|
+
said="the claim offers no evidence, so nothing was checked",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
for assertion in assertions:
|
|
212
|
+
if assertion.outcome in _NOT_CHECKED:
|
|
213
|
+
reason = assertion.reason.value.replace("_", " ")
|
|
214
|
+
lines.append(
|
|
215
|
+
NotEstablished(
|
|
216
|
+
kind=_NOT_CHECKED[assertion.outcome],
|
|
217
|
+
subject=f"{assertion.kind} {assertion.name}".strip(),
|
|
218
|
+
said=f"{assertion.outcome.value.replace('_', ' ')}: {reason}"
|
|
219
|
+
+ (f": {assertion.detail}" if assertion.detail else ""),
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
if any(node.regeneration is not None for node in chain):
|
|
223
|
+
lines.append(
|
|
224
|
+
NotEstablished(kind=NotEstablishedKind.UNDECLARED_INPUTS, said=UNDECLARED_INPUTS)
|
|
225
|
+
)
|
|
226
|
+
if any(read.address for assertion in assertions for read in assertion.reads):
|
|
227
|
+
lines.append(NotEstablished(kind=NotEstablishedKind.ADDRESS, said=ADDRESS_IS_CHOSEN))
|
|
228
|
+
if assertions:
|
|
229
|
+
lines.append(NotEstablished(kind=NotEstablishedKind.COMPARISON, said=COMPARISON_ONLY))
|
|
230
|
+
return tuple(lines)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def why(
|
|
234
|
+
manifest: Manifest, claim_id: str, backends: tuple[Backend, ...] = DEFAULT_BACKENDS
|
|
235
|
+
) -> ClaimAccount:
|
|
236
|
+
"""Evaluate one claim's assertions and say what each file it rests on is, against the
|
|
237
|
+
record of re-runs.
|
|
238
|
+
|
|
239
|
+
Executes nothing and writes nothing.
|
|
240
|
+
"""
|
|
241
|
+
claim = next((c for c in manifest.claims if c.id == claim_id), None)
|
|
242
|
+
if claim is None:
|
|
243
|
+
raise UnknownClaimError(claim_id, tuple(c.id for c in manifest.claims))
|
|
244
|
+
|
|
245
|
+
report = stale(manifest)
|
|
246
|
+
states, paths = artifact_states(manifest)
|
|
247
|
+
registry: dict[str, Backend] = {b.kind: b for b in backends}
|
|
248
|
+
with reading_once():
|
|
249
|
+
decisions = [
|
|
250
|
+
check_evidence(claim, evidence, manifest, paths, states, registry)
|
|
251
|
+
for evidence in claim.evidence
|
|
252
|
+
]
|
|
253
|
+
held = [
|
|
254
|
+
_held(evidence, decision, paths)
|
|
255
|
+
for evidence, decision in zip(claim.evidence, decisions, strict=True)
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
def read(
|
|
259
|
+
artifact_id: str,
|
|
260
|
+
side: str,
|
|
261
|
+
where: str,
|
|
262
|
+
value: str | None,
|
|
263
|
+
conversion: Conversion | None,
|
|
264
|
+
converted: str | None,
|
|
265
|
+
) -> FileRead:
|
|
266
|
+
declared, state = manifest.artifact(artifact_id), states.get(artifact_id)
|
|
267
|
+
return FileRead(
|
|
268
|
+
artifact=artifact_id,
|
|
269
|
+
path=declared.path.as_posix() if declared else "",
|
|
270
|
+
side=side,
|
|
271
|
+
validity=state.validity if state else None,
|
|
272
|
+
pinned=state.expected if state else None,
|
|
273
|
+
on_disk=state.actual if state else None,
|
|
274
|
+
address=where,
|
|
275
|
+
stored=value,
|
|
276
|
+
conversion=conversion,
|
|
277
|
+
converted=converted,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# What the record of checks holds for each assertion, and the assertion as it stands where
|
|
281
|
+
# the record's last entry is something else.
|
|
282
|
+
root = manifest.path.parent if manifest.path else pathlib.Path.cwd()
|
|
283
|
+
kept = Records(root)
|
|
284
|
+
past = kept.history
|
|
285
|
+
present = checked(claim, tuple(decisions))
|
|
286
|
+
# The last state of each assertion of a claim that is no longer declared, where it was
|
|
287
|
+
# not verified and had an address.
|
|
288
|
+
declared = {each.id for each in manifest.claims}
|
|
289
|
+
gone = [
|
|
290
|
+
states[-1]
|
|
291
|
+
for identity, states in past.items()
|
|
292
|
+
if identity[0] not in declared
|
|
293
|
+
and states[-1].outcome is not Outcome.VERIFIED
|
|
294
|
+
and states[-1].address
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
freshness = next(node for node in report.claims if node.id == claim.id).assertions
|
|
298
|
+
assertions = tuple(
|
|
299
|
+
AssertionAccount(
|
|
300
|
+
kind=evidence.kind,
|
|
301
|
+
name=getattr(evidence, "name", ""),
|
|
302
|
+
printed=evidence.text
|
|
303
|
+
if isinstance(evidence, QuoteEvidence)
|
|
304
|
+
else getattr(evidence, "reported", None),
|
|
305
|
+
reads=tuple(read(*found) for found in files),
|
|
306
|
+
places=getattr(evidence, "places", None),
|
|
307
|
+
tie=decision.tie,
|
|
308
|
+
statistic=evidence.statistic if isinstance(evidence, AggregateEvidence) else None,
|
|
309
|
+
computed=decision.computed,
|
|
310
|
+
outcome=decision.outcome,
|
|
311
|
+
reason=decision.reason,
|
|
312
|
+
detail=decision.detail,
|
|
313
|
+
state=node.state,
|
|
314
|
+
warnings=decision.warnings,
|
|
315
|
+
history=recorded,
|
|
316
|
+
now=None if not recorded or now.same_as(recorded[-1]) else now,
|
|
317
|
+
formerly=tuple(
|
|
318
|
+
last
|
|
319
|
+
for last in gone
|
|
320
|
+
if last.address == now.address and set(last.digests) == set(now.digests)
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
for evidence, decision, files, node, now in zip(
|
|
324
|
+
claim.evidence, decisions, held, freshness, present, strict=True
|
|
325
|
+
)
|
|
326
|
+
for recorded in (past.get(now.identity, ()),)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# The records upstream of each file the claim reads, by `upstream`, and so the files: each
|
|
330
|
+
# record's output at the record's distance and its inputs one step further.
|
|
331
|
+
records = {record.id: record for record in manifest.regenerations}
|
|
332
|
+
producers = {record.output.artifact: record for record in manifest.regenerations}
|
|
333
|
+
steps: dict[str, int] = {}
|
|
334
|
+
for name in dict.fromkeys(name for evidence in claim.evidence for name in evidence.artifacts):
|
|
335
|
+
steps[name] = 0
|
|
336
|
+
producer = producers.get(name)
|
|
337
|
+
if producer is None:
|
|
338
|
+
continue
|
|
339
|
+
for record_id, distance in {**upstream(producer, producers), producer.id: 0}.items():
|
|
340
|
+
record = records[record_id]
|
|
341
|
+
for artifact_id, at in (
|
|
342
|
+
(record.output.artifact, distance),
|
|
343
|
+
*((wanted.artifact, distance + 1) for wanted in record.inputs),
|
|
344
|
+
):
|
|
345
|
+
steps[artifact_id] = min(at, steps.get(artifact_id, at))
|
|
346
|
+
|
|
347
|
+
nodes = {node.artifact: node for node in report.regenerations}
|
|
348
|
+
chain = tuple(
|
|
349
|
+
ChainNode(
|
|
350
|
+
artifact=name,
|
|
351
|
+
path=declared.path.as_posix() if declared else "",
|
|
352
|
+
steps=steps[name],
|
|
353
|
+
validity=states[name].validity if name in states else None,
|
|
354
|
+
state=nodes[name].state if name in nodes else Freshness.NO_PRODUCER,
|
|
355
|
+
command=producers[name].command if name in producers else (),
|
|
356
|
+
volatile=producers[name].volatile if name in producers else (),
|
|
357
|
+
regeneration=nodes.get(name),
|
|
358
|
+
)
|
|
359
|
+
for name in sorted(steps, key=steps.__getitem__)
|
|
360
|
+
for declared in (manifest.artifact(name),)
|
|
361
|
+
)
|
|
362
|
+
on_chain = {node.regeneration.id for node in chain if node.regeneration}
|
|
363
|
+
by_outcome = collections.Counter(assertion.outcome for assertion in assertions)
|
|
364
|
+
by_state = collections.Counter(assertion.state for assertion in assertions)
|
|
365
|
+
|
|
366
|
+
return ClaimAccount(
|
|
367
|
+
project=report.project,
|
|
368
|
+
manifest_digest=report.manifest_digest,
|
|
369
|
+
claim_id=claim.id,
|
|
370
|
+
text=claim.text,
|
|
371
|
+
where=claim.where,
|
|
372
|
+
assertions=assertions,
|
|
373
|
+
chain=chain,
|
|
374
|
+
edges=tuple(
|
|
375
|
+
edge
|
|
376
|
+
for edge in report.edges
|
|
377
|
+
if (edge.kind == "reads" and edge.source == claim.id)
|
|
378
|
+
or (edge.kind == "produced_by" and edge.target in on_chain)
|
|
379
|
+
or (edge.kind == "consumes" and edge.source in on_chain)
|
|
380
|
+
),
|
|
381
|
+
findings=tuple(found for found in report.findings if found.regeneration in on_chain),
|
|
382
|
+
not_established=_not_established(claim, assertions, chain),
|
|
383
|
+
records=kept.states,
|
|
384
|
+
set_aside=kept.set_aside,
|
|
385
|
+
no_local_record=kept.no_local_record,
|
|
386
|
+
outcomes={outcome: by_outcome[outcome] for outcome in Outcome if by_outcome[outcome]},
|
|
387
|
+
states={state: by_state[state] for state in Freshness if by_state[state]},
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
__all__ = [
|
|
392
|
+
"ADDRESS_IS_CHOSEN",
|
|
393
|
+
"COMPARISON_ONLY",
|
|
394
|
+
"UNDECLARED_INPUTS",
|
|
395
|
+
"UnknownClaimError",
|
|
396
|
+
"address",
|
|
397
|
+
"why",
|
|
398
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: claims-cli
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Check what a manuscript states against the files it names: numbers, table cells and quotations
|
|
5
|
+
Project-URL: Changelog, https://github.com/elliottower/reproducible-science/blob/main/CHANGELOG.md
|
|
6
|
+
Project-URL: Homepage, https://github.com/elliottower/reproducible-science
|
|
7
|
+
Project-URL: Issues, https://github.com/elliottower/reproducible-science/issues
|
|
8
|
+
Project-URL: Source, https://github.com/elliottower/reproducible-science/tree/main/packages/claims
|
|
9
|
+
Author-email: Elliot Tower <elliot@elliottower.ai>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: citations,manuscript,open science,provenance,reproducibility
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: citations<0.7,>=0.6
|
|
23
|
+
Requires-Dist: provenance-core<0.7,>=0.6
|
|
24
|
+
Requires-Dist: pydantic>=2.9
|
|
25
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
26
|
+
Provides-Extra: arrays
|
|
27
|
+
Requires-Dist: numpy>=2.2.6; extra == 'arrays'
|
|
28
|
+
Provides-Extra: hdf5
|
|
29
|
+
Requires-Dist: h5py>=3.12; extra == 'hdf5'
|
|
30
|
+
Provides-Extra: netcdf
|
|
31
|
+
Requires-Dist: netcdf4>=1.7.2; extra == 'netcdf'
|
|
32
|
+
Provides-Extra: parquet
|
|
33
|
+
Requires-Dist: pyarrow>=18; extra == 'parquet'
|
|
34
|
+
Provides-Extra: rds
|
|
35
|
+
Requires-Dist: rdata>=1; extra == 'rds'
|
|
36
|
+
Provides-Extra: sheets
|
|
37
|
+
Requires-Dist: openpyxl>=3.1; extra == 'sheets'
|
|
38
|
+
Requires-Dist: xlrd>=2.0.1; extra == 'sheets'
|
|
39
|
+
Provides-Extra: stata-spss
|
|
40
|
+
Requires-Dist: pyreadstat>=1.3.6; extra == 'stata-spss'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# claims-cli
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/claims-cli/)
|
|
46
|
+
[](https://pypi.org/project/claims-cli/)
|
|
47
|
+
[](https://github.com/elliottower/reproducible-science/blob/main/LICENSE)
|
|
48
|
+
[](https://reproducible.science/)
|
|
49
|
+
|
|
50
|
+
Check what a manuscript states against the files it names.
|
|
51
|
+
|
|
52
|
+
The part of [reproducible-science](https://github.com/elliottower/reproducible-science) that reads a manifest of claims and checks each one: a number against a results file, a table cell against a table, a quotation against its source. It installs alone, and it runs no command of yours.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install claims-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This installs `claims` and one dependency of ours, [`citations`](https://pypi.org/project/citations/), which reads the documents a quotation is checked in. The optional readers are extras, each named for what it reads:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install "claims-cli[parquet,sheets]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`arrays`, `hdf5`, `netcdf`, `parquet`, `rds`, `sheets` and `stata-spss`. Without the extra a format needs, a check of that format is `unchecked` and names the extra to install.
|
|
67
|
+
|
|
68
|
+
## What it reads
|
|
69
|
+
|
|
70
|
+
`repro.yaml`, a manifest that pins a project's files by sha256 and declares what the manuscript claims about them:
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
artifacts:
|
|
74
|
+
- id: results
|
|
75
|
+
path: analysis/results.json
|
|
76
|
+
digest: {algorithm: sha256, value: 9f635f9a...}
|
|
77
|
+
|
|
78
|
+
claims:
|
|
79
|
+
- id: primary
|
|
80
|
+
text: The treatment reduced the primary outcome
|
|
81
|
+
evidence:
|
|
82
|
+
- kind: metric
|
|
83
|
+
artifact: results
|
|
84
|
+
name: p_value
|
|
85
|
+
pointer: /primary/p
|
|
86
|
+
reported: "0.031"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The file has the same name and the same format whichever command reads it. The [specification](https://reproducible.science/reference/spec/) defines every kind of evidence and every outcome.
|
|
90
|
+
|
|
91
|
+
## The commands
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claims manifest init
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Writes a starter `repro.yaml` at the top of the project: each file it finds or is given, pinned, with example claims that verify as written. It never overwrites one.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
claims verify
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Checks every declared assertion against the file it names and prints one line per assertion: `ok`, `MISS` where the file holds another value, `GONE` where the address does not resolve, `--` where the check could not run. It exits 1 where the policy fails, and `--format json` and `--format sarif` print the report as a document.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
claims stale
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Says which claims read a file that is no longer what a recorded re-run produced. It reads `.repro/reproductions.jsonl` and runs nothing.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
claims why primary
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For one claim: each assertion as `claims verify` evaluates it now, each file the claim rests on and the command recorded for it, and then what the record does not establish.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
claims numbers
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Counts every number the manuscript's text prints and says which a claim accounts for.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
claims export record
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Writes one record per checked assertion, for another program to read. `sarif` and `ro-crate` write the same records in those formats.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
claims origins --repo https://github.com/owner/study=../study
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For each artifact copied from another repository, which the manifest says with `origin: {repository, commit, path}`, compares the pinned digest with the bytes of that path at that commit: `same`, `differs`, `absent`, `unknown commit` or `not checked`, each with the reason. A clone given with `--repo` is read with `git cat-file`, so committed bytes are compared and never a working tree. With `--fetch`, a GitHub repository no clone is given for is read from `raw.githubusercontent.com`; without it nothing touches the network. `claims verify` never reads an origin.
|
|
134
|
+
|
|
135
|
+
## With `repro`
|
|
136
|
+
|
|
137
|
+
[`reproducible-science`](https://pypi.org/project/reproducible-science/) depends on this package and adds the commands that run things: `repro reproduce`, which runs each declared command again and writes the record `claims stale` reads, `repro check` and `repro audit`, which drive `prereg`, `results` and `citations`, and `repro ci`. Under `repro`, every command above has the same name, does the same thing and prints the same output: `repro verify` and `claims verify` are one function. A project that uses `repro` has no reason to install this by itself. It is separate so that checking a manuscript's claims does not require installing something that executes commands a manifest names.
|
|
138
|
+
|
|
139
|
+
MIT licensed.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
claims/__init__.py,sha256=qiTFpxrEffnmtOD3_tEqjih6HFfUW-o640bhaPIT7Bg,3410
|
|
2
|
+
claims/__main__.py,sha256=2G4pjnNFGBK8XPrjiUkGpJHULOk-NzoQrDTdT8xnRcA,115
|
|
3
|
+
claims/checks.py,sha256=MuaPkL3BQvuVb05nQ5CAfGBWMoLbJVfnpWtVzIV50FM,21458
|
|
4
|
+
claims/cli.py,sha256=jnG7jGyfKAXTM84WfPnB9msRtGznCl5VUnhJnulZKn4,49683
|
|
5
|
+
claims/corpus.py,sha256=fjdB-Csd92R-UgMa4RRAHM72JeO3SAXxLA0GaDo_Xls,12538
|
|
6
|
+
claims/exceptions.py,sha256=Hx05bLyk9Xtx9jClieFtrMuG9OSDm5FyFIW0Y_RWz_A,3835
|
|
7
|
+
claims/manifest.py,sha256=QcHVysmvs3jdFfMgDPpPq96jf00dxyW5ym5-a57Fz6w,2310
|
|
8
|
+
claims/models.py,sha256=PSIIIhhiWXDRai5gm9-nmZPx3yXRlZu8j20nDN9W8qs,5840
|
|
9
|
+
claims/numbers.py,sha256=w0yxK_UJoTg8prqv-dNk_wfLZjOUPekDc1o4v350gVE,19295
|
|
10
|
+
claims/origins.py,sha256=eeJBwUz7y27yxPJ13CviQgGL2FOQWM6gGbFFl3Cxfrw,11220
|
|
11
|
+
claims/policy.py,sha256=sWGlpveOA4eCPAGuotaCbw8PadZQ1dvAU4KM1bt6oMA,14540
|
|
12
|
+
claims/provenance.py,sha256=7AXuxhtQaxfAM_Gok-OPruTu0O9jWLOBsn_xf7MIOmk,1590
|
|
13
|
+
claims/record.py,sha256=Z337-37F--HhMVo7YOZ6rJqYsp3QA_Ggv1sZS7EBvwU,43811
|
|
14
|
+
claims/regression.py,sha256=2E0_GnIs_PeacN0UU6zuPtz71i08LDItz5UYKjxGoYg,5945
|
|
15
|
+
claims/reruns.py,sha256=a_1mEux5TLg6T9Gelseb65aoV5gi9CKS9nHXUPsttCc,4563
|
|
16
|
+
claims/resolve.py,sha256=wuGeocIE2NB6Z9FhXnF3DtAGNkjKR-O-9VI-wkGTdBk,4628
|
|
17
|
+
claims/stale.py,sha256=AppPzUyqD6nrx_58DDoWYSFORL50MK0dgViiv5nSD9Y,13142
|
|
18
|
+
claims/starter.py,sha256=8lV9zSG3kfSpa0AGtUtPGPYCcjOYYVnLMdYUYWMLauI,18380
|
|
19
|
+
claims/toolchain.py,sha256=QmzalHL4jHqwXkgQQRTZapfrxuXrkRsl9FWuMlg5V08,4747
|
|
20
|
+
claims/verify.py,sha256=tqvMEg3Fawigp4IN92BJUAwIGDkKjnLIObE_Op6qSRE,59721
|
|
21
|
+
claims/why.py,sha256=YxC8s9yw0eL5FCpTPQ0_CEYIIc4vyBqJxvP9MSyw9ak,15860
|
|
22
|
+
claims/adapters/__init__.py,sha256=Y-oo_AD11xdK4CMEbZbANMWiO_VgHvcnSiHMac5Ywq0,251
|
|
23
|
+
claims/adapters/array.py,sha256=ggeJd3NuNCpWPISiz13QwkMAhN76j5vJ8Fl695zEE9k,9280
|
|
24
|
+
claims/adapters/base.py,sha256=DvoarzVVQwAKFXylczPJqdiqX6HnQj-0Lm9lh_tBgyk,2978
|
|
25
|
+
claims/adapters/cells.py,sha256=XcmOhVsRoRAdMNqKmIvS75Imf5cDKEUNnGvm5ePsE5c,3858
|
|
26
|
+
claims/adapters/columnar.py,sha256=_4PYDb2SQiXP4Lo1I4brvoINorJn5y-VWxs02XkaRsY,8167
|
|
27
|
+
claims/adapters/prose.py,sha256=-XatJ-oz6QatX0_-j_3DqTbzZ4ZWvFqQUR0nk4OUQno,12436
|
|
28
|
+
claims/adapters/reads.py,sha256=b1V9nhRqT7dtmtayw95I_qAgbQZWJS5OUZAwgUUpDXw,1696
|
|
29
|
+
claims/adapters/rows.py,sha256=HhQg2yQHwFYnj8_7u1jRMBYgTvocMmVCIL5IcMz4ZQU,5116
|
|
30
|
+
claims/adapters/sheet.py,sha256=VUEibAXEDvm-t_EZIngaUuQ6UuS47e950ZasKAQOM74,6581
|
|
31
|
+
claims/adapters/sqlite.py,sha256=b7xEYLMxSAIeIOMPEIm7wIaX6gRJfAH0SUjltg6nwTM,2997
|
|
32
|
+
claims/adapters/table.py,sha256=B603jtqpJO_HpPFeKcohbX0wbswDZb4tW9PvOW5W5hQ,5859
|
|
33
|
+
claims/adapters/tree.py,sha256=zRaPi3vY35jYz4WqDfoI8VlIgVnR6KXKwBvi4euu5CM,6247
|
|
34
|
+
claims/core/__init__.py,sha256=C3QIc8B_TH4eDcU8g7VaOJdpGlBmu0wnAh6qJFKiLFA,477
|
|
35
|
+
claims/core/artifacts.py,sha256=SNtxep20cR5vkEUoCF8KAW15akA-LcwbmbTqF-kryNs,4134
|
|
36
|
+
claims/core/checks.py,sha256=cnoRJmv-WB-RbvcbzUM1AX0RgY6UqAdJPgXMyZDzxYs,5418
|
|
37
|
+
claims/core/claims.py,sha256=Ag6aXut6bq4piMcOjVXowlt9Ibs4qPvwJOmrohBn6Rc,3754
|
|
38
|
+
claims/core/evidence.py,sha256=Tp16mtUmO2SrE-D8YKn374RMuDDrCL4MJa-ZhJ9gpqw,27675
|
|
39
|
+
claims/core/manifest.py,sha256=4h5TqeuguFK08s5HCW2dYauSDcxbmJpUpFVjj9dvWzA,14267
|
|
40
|
+
claims/core/numbers.py,sha256=EyMn4Sz8_LUnex_oszxCsy7m6t2WX6EODQT2My1vUPY,6115
|
|
41
|
+
claims/core/outcomes.py,sha256=GwgCCmL31FZbz_W9Ka8mFHUUHpWUZatJPuCTI31GHd8,18024
|
|
42
|
+
claims/core/report.py,sha256=8LFKthC8yfemxLyEd3WTHoCoY7CpSul3zf8rIbUIXHM,18258
|
|
43
|
+
claims/export/__init__.py,sha256=FUJ_4x22azFR_CTLZ4aXT1ezo4znFwUY_Eo7JgqME7A,670
|
|
44
|
+
claims/export/rocrate.py,sha256=RrPePcaf1UyGwwemT_1D97GR2_REygMI2CyJrUnntWE,32953
|
|
45
|
+
claims/export/sarif.py,sha256=EuP8l3YZZm0in2OVvydYLXX6vHO1Lc66ypNnO54kj5s,15628
|
|
46
|
+
claims/renderers/__init__.py,sha256=KMqGyJNw9myWpkEREoKRFuy-bUJvNRq7AhI9lyKRji0,345
|
|
47
|
+
claims/renderers/sarif.py,sha256=yz30yg61O5zt6_xOogXcaXjXmCnQOlAk-Wuy3ijH8xU,12928
|
|
48
|
+
claims_cli-0.6.0.dist-info/METADATA,sha256=fTN3iaV9RQspSTP3ASX-gFVg7sk4yf9n47NpKEwNckA,6372
|
|
49
|
+
claims_cli-0.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
50
|
+
claims_cli-0.6.0.dist-info/entry_points.txt,sha256=qPRfNHlVZYCAD9YWmd03hgV6iYh8YfhM2xLJx7qOM5M,43
|
|
51
|
+
claims_cli-0.6.0.dist-info/RECORD,,
|