comuvia 0.1.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.
- comuvia/__init__.py +73 -0
- comuvia/__main__.py +7 -0
- comuvia/cli.py +343 -0
- comuvia/evaluation.py +325 -0
- comuvia/history.py +15 -0
- comuvia/identity.py +413 -0
- comuvia/issues.py +31 -0
- comuvia/reasons.py +132 -0
- comuvia/rules/binary-brier-v1/1.json +1 -0
- comuvia/schemas/v0_1/artifact_descriptor.json +194 -0
- comuvia/schemas/v0_1/assessment.json +166 -0
- comuvia/schemas/v0_1/common.json +296 -0
- comuvia/schemas/v0_1/evaluation.json +186 -0
- comuvia/schemas/v0_1/forecast.json +202 -0
- comuvia/schemas/v0_1/interpretation.json +153 -0
- comuvia/schemas/v0_1/outcome.json +207 -0
- comuvia/schemas/v0_1/question.json +191 -0
- comuvia/schemas/v0_1/resource_snapshot.json +227 -0
- comuvia/schemas/v0_1/source_assertion.json +272 -0
- comuvia/selectors.py +70 -0
- comuvia/store.py +499 -0
- comuvia/validation.py +452 -0
- comuvia-0.1.0.dist-info/METADATA +126 -0
- comuvia-0.1.0.dist-info/RECORD +27 -0
- comuvia-0.1.0.dist-info/WHEEL +4 -0
- comuvia-0.1.0.dist-info/entry_points.txt +3 -0
- comuvia-0.1.0.dist-info/licenses/LICENSE +202 -0
comuvia/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Comuvia: offline, provider-neutral evidence and prediction records.
|
|
2
|
+
|
|
3
|
+
Implemented: the nine comuvia 0.1 record types as packaged JSON Schemas, strict parsing,
|
|
4
|
+
single-record validation with named refusal codes, RFC 8785 canonicalization, content and
|
|
5
|
+
record digests, identity and selector verification, a single-writer append-only record
|
|
6
|
+
store, eligibility and the binary-brier-v1 rule with named exclusions, reproduction of
|
|
7
|
+
stored evaluations, and the ``comuvia`` command-line interface. There is no aggregate
|
|
8
|
+
score, no network access and no runtime dependency.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
from .identity import (
|
|
14
|
+
ReferenceCheck,
|
|
15
|
+
Verification,
|
|
16
|
+
canonicalize,
|
|
17
|
+
content_digest,
|
|
18
|
+
is_marker,
|
|
19
|
+
parse_json,
|
|
20
|
+
record_digest,
|
|
21
|
+
reference_to,
|
|
22
|
+
revision_conflicts,
|
|
23
|
+
verify,
|
|
24
|
+
)
|
|
25
|
+
from .evaluation import (
|
|
26
|
+
CandidateResult,
|
|
27
|
+
EvaluationReport,
|
|
28
|
+
Reproduction,
|
|
29
|
+
ScoringRule,
|
|
30
|
+
evaluate,
|
|
31
|
+
evaluation_records,
|
|
32
|
+
load_rule,
|
|
33
|
+
reproduce,
|
|
34
|
+
)
|
|
35
|
+
from .history import latest_revisions
|
|
36
|
+
from .issues import Issue, RefusalError
|
|
37
|
+
from .selectors import SelectorCheck, check_selector
|
|
38
|
+
from .store import RecordStore, StoreEntry, StoreReport
|
|
39
|
+
from .validation import RECORD_TYPES, SCHEMA_VERSION, load_record, schema_files, validate
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"CandidateResult",
|
|
43
|
+
"EvaluationReport",
|
|
44
|
+
"Issue",
|
|
45
|
+
"RECORD_TYPES",
|
|
46
|
+
"RecordStore",
|
|
47
|
+
"ReferenceCheck",
|
|
48
|
+
"RefusalError",
|
|
49
|
+
"Reproduction",
|
|
50
|
+
"SCHEMA_VERSION",
|
|
51
|
+
"ScoringRule",
|
|
52
|
+
"SelectorCheck",
|
|
53
|
+
"StoreEntry",
|
|
54
|
+
"StoreReport",
|
|
55
|
+
"Verification",
|
|
56
|
+
"canonicalize",
|
|
57
|
+
"check_selector",
|
|
58
|
+
"content_digest",
|
|
59
|
+
"evaluate",
|
|
60
|
+
"evaluation_records",
|
|
61
|
+
"is_marker",
|
|
62
|
+
"latest_revisions",
|
|
63
|
+
"load_record",
|
|
64
|
+
"load_rule",
|
|
65
|
+
"parse_json",
|
|
66
|
+
"record_digest",
|
|
67
|
+
"reference_to",
|
|
68
|
+
"reproduce",
|
|
69
|
+
"revision_conflicts",
|
|
70
|
+
"schema_files",
|
|
71
|
+
"validate",
|
|
72
|
+
"verify",
|
|
73
|
+
]
|
comuvia/__main__.py
ADDED
comuvia/cli.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""The ``comuvia`` command-line interface (standard library ``argparse``; no network command).
|
|
2
|
+
|
|
3
|
+
Exit codes: 0 success (exclusions are results, not errors); 1 refusal or mismatch; 2 usage error.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import datetime as _dt
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .evaluation import evaluate, evaluation_records, reproduce
|
|
16
|
+
from .identity import content_digest, record_digest, verify
|
|
17
|
+
from .issues import Issue, RefusalError
|
|
18
|
+
from .selectors import check_selector
|
|
19
|
+
from .store import RecordStore
|
|
20
|
+
from .validation import instant, load_record, validate
|
|
21
|
+
|
|
22
|
+
OK, FAILED, USAGE = 0, 1, 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Output:
|
|
26
|
+
def __init__(self, as_json: bool):
|
|
27
|
+
self.as_json = as_json
|
|
28
|
+
|
|
29
|
+
def emit(self, data: dict, lines: list[str]) -> None:
|
|
30
|
+
if self.as_json:
|
|
31
|
+
print(json.dumps(data, indent=2, ensure_ascii=False, sort_keys=True))
|
|
32
|
+
else:
|
|
33
|
+
for line in lines:
|
|
34
|
+
print(line)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _issues(issues) -> list[dict]:
|
|
38
|
+
return [{"code": i.code, "pointer": i.pointer, "message": i.message} for i in issues]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _issue_lines(issues, indent: str = " ") -> list[str]:
|
|
42
|
+
return [f"{indent}{i.code} at '{i.pointer}': {i.message}" for i in issues]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _read_record(path: str) -> dict:
|
|
46
|
+
return load_record(Path(path).read_bytes())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _now() -> str:
|
|
50
|
+
return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------ commands
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cmd_validate(args, out: _Output) -> int:
|
|
57
|
+
results, lines, failed = [], [], False
|
|
58
|
+
for path in args.files:
|
|
59
|
+
try:
|
|
60
|
+
body = _read_record(path)
|
|
61
|
+
results.append({"file": path, "valid": True, "record_digest": record_digest(body), "issues": []})
|
|
62
|
+
lines.append(f"valid {path} {record_digest(body)}")
|
|
63
|
+
except RefusalError as error:
|
|
64
|
+
failed = True
|
|
65
|
+
results.append({"file": path, "valid": False, "issues": _issues(error.issues)})
|
|
66
|
+
lines += [f"REFUSED {path}"] + _issue_lines(error.issues)
|
|
67
|
+
out.emit({"results": results}, lines)
|
|
68
|
+
return FAILED if failed else OK
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_digest(args, out: _Output) -> int:
|
|
72
|
+
data = Path(args.file).read_bytes()
|
|
73
|
+
if args.content:
|
|
74
|
+
digest, kind = content_digest(data), "content"
|
|
75
|
+
else:
|
|
76
|
+
digest, kind = record_digest(load_record(data)), "record"
|
|
77
|
+
out.emit({"kind": kind, "digest": digest, "byte_length": len(data) if args.content else None}, [digest])
|
|
78
|
+
return OK
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_verify(args, out: _Output) -> int:
|
|
82
|
+
body = _read_record(args.record)
|
|
83
|
+
content = Path(args.content).read_bytes() if args.content else None
|
|
84
|
+
records = RecordStore(args.store).records() if args.store else []
|
|
85
|
+
result = verify(body, expected_digest=args.expected_digest, content=content, records=records)
|
|
86
|
+
selector = None
|
|
87
|
+
if body["record_type"] == "source_assertion" and content is not None:
|
|
88
|
+
snapshot = next((r for r in records if r["record_type"] == body["resource"]["record_type"]
|
|
89
|
+
and r["id"] == body["resource"]["id"] and r["revision"] == body["resource"]["revision"]), None)
|
|
90
|
+
selector = check_selector(body, content, snapshot=snapshot)
|
|
91
|
+
issues = list(result.issues) + (list(selector.issues) if selector else [])
|
|
92
|
+
data = {
|
|
93
|
+
"record_digest": result.record_digest,
|
|
94
|
+
"record_identity": result.record_identity,
|
|
95
|
+
"content_identity": result.content_identity,
|
|
96
|
+
"references": [{"pointer": r.pointer, "result": r.result} for r in result.references],
|
|
97
|
+
"selector": None if selector is None else {"result": selector.result, "checked": list(selector.checked)},
|
|
98
|
+
"timestamp_assurance": result.timestamp_assurance,
|
|
99
|
+
"signature": result.signature,
|
|
100
|
+
"issues": _issues(issues),
|
|
101
|
+
}
|
|
102
|
+
lines = [
|
|
103
|
+
f"record digest {result.record_digest}",
|
|
104
|
+
f"record identity {result.record_identity}",
|
|
105
|
+
f"content identity {result.content_identity}",
|
|
106
|
+
]
|
|
107
|
+
lines += [f"reference {r.pointer:<10} {r.result}" for r in result.references]
|
|
108
|
+
if selector is not None:
|
|
109
|
+
lines.append(f"selector {selector.result} ({', '.join(selector.checked) or 'nothing checkable'})")
|
|
110
|
+
lines += [f"timestamp assurance {result.timestamp_assurance}", f"signature {result.signature}"]
|
|
111
|
+
lines += _issue_lines(issues)
|
|
112
|
+
out.emit(data, lines)
|
|
113
|
+
mismatch = issues or (selector is not None and selector.result == "mismatch")
|
|
114
|
+
return FAILED if mismatch else OK
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_store_init(args, out: _Output) -> int:
|
|
118
|
+
RecordStore.create(args.store)
|
|
119
|
+
out.emit({"store": args.store, "created": True}, [f"created store {args.store}"])
|
|
120
|
+
return OK
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cmd_store_append(args, out: _Output) -> int:
|
|
124
|
+
bodies = [_read_record(path) for path in args.files]
|
|
125
|
+
entries = []
|
|
126
|
+
with RecordStore(args.store, writer=True) as store:
|
|
127
|
+
for path, body in zip(args.files, bodies):
|
|
128
|
+
entry = store.append(body)
|
|
129
|
+
entries.append({"file": path, **entry.as_index(), "appended": entry.appended})
|
|
130
|
+
lines = [
|
|
131
|
+
f"{'appended' if e['appended'] else 'present '} seq={e['sequence']} {e['record_type']} {e['id']} r{e['revision']} {e['digest']}"
|
|
132
|
+
for e in entries
|
|
133
|
+
]
|
|
134
|
+
out.emit({"entries": entries}, lines)
|
|
135
|
+
return OK
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cmd_store_verify(args, out: _Output) -> int:
|
|
139
|
+
report = RecordStore(args.store).verify()
|
|
140
|
+
data = {
|
|
141
|
+
"ok": report.ok,
|
|
142
|
+
"entries": report.entries,
|
|
143
|
+
"bodies": report.bodies,
|
|
144
|
+
"lock_holder": report.lock_holder,
|
|
145
|
+
"lock_note": "holder details last written by a writer; a reader cannot tell whether that holder is still alive",
|
|
146
|
+
"scope": "identity and references only; run 'comuvia reproduce' to check stored evaluation results",
|
|
147
|
+
"issues": _issues(report.issues),
|
|
148
|
+
"detection_limit": report.detection_limit,
|
|
149
|
+
}
|
|
150
|
+
holder = report.lock_holder or "none recorded"
|
|
151
|
+
lines = [f"{'OK' if report.ok else 'PROBLEMS FOUND'}: {report.entries} index entries, {report.bodies} bodies"]
|
|
152
|
+
lines.append(f"writer details last recorded: {holder} (a reader cannot tell whether that writer is alive)")
|
|
153
|
+
lines += _issue_lines(report.issues)
|
|
154
|
+
lines.append(f"limit: {report.detection_limit}")
|
|
155
|
+
lines.append("scope: identity and references only; run 'comuvia reproduce' to check stored evaluation results")
|
|
156
|
+
out.emit(data, lines)
|
|
157
|
+
return OK if report.ok else FAILED
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cmd_store_show(args, out: _Output) -> int:
|
|
161
|
+
store = RecordStore(args.store)
|
|
162
|
+
if args.latest:
|
|
163
|
+
bodies = [b for _, b in sorted(store.latest().items())]
|
|
164
|
+
else:
|
|
165
|
+
bodies = store.records()
|
|
166
|
+
if args.record_type:
|
|
167
|
+
bodies = [b for b in bodies if b["record_type"] == args.record_type]
|
|
168
|
+
if args.id:
|
|
169
|
+
bodies = [b for b in bodies if b["id"] == args.id]
|
|
170
|
+
rows = [{"record_type": b["record_type"], "id": b["id"], "revision": b["revision"], "digest": record_digest(b)} for b in bodies]
|
|
171
|
+
data = {"records": bodies if args.bodies else rows}
|
|
172
|
+
out.emit(data, [f"{r['record_type']:<20} {r['id']} r{r['revision']} {r['digest']}" for r in rows])
|
|
173
|
+
return OK
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def cmd_store_repair(args, out: _Output) -> int:
|
|
177
|
+
with RecordStore(args.store, writer=True) as store:
|
|
178
|
+
removed = store.truncate_torn_tail()
|
|
179
|
+
out.emit({"torn_bytes_removed": removed}, [f"removed {removed} bytes of an incomplete final index line"])
|
|
180
|
+
return OK
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cmd_evaluate(args, out: _Output) -> int:
|
|
184
|
+
if args.append and not args.store:
|
|
185
|
+
raise _Usage("--append requires --store")
|
|
186
|
+
if args.append and (not args.evaluated_at or not args.recorded_by):
|
|
187
|
+
raise _Usage("--append requires --evaluated-at and --recorded-by")
|
|
188
|
+
for name in ("evaluated_at", "recorded_at"):
|
|
189
|
+
value = getattr(args, name)
|
|
190
|
+
if value is not None and instant(value) is None:
|
|
191
|
+
raise _Usage(f"--{name.replace('_', '-')} must be an RFC 3339 timestamp with an offset")
|
|
192
|
+
records = RecordStore(args.store).records() if args.store else [_read_record(p) for p in args.files]
|
|
193
|
+
report = evaluate(records)
|
|
194
|
+
data = report.as_json()
|
|
195
|
+
appended = []
|
|
196
|
+
if args.append:
|
|
197
|
+
bodies = evaluation_records(
|
|
198
|
+
report, records,
|
|
199
|
+
evaluated_at=args.evaluated_at, recorded_by=args.recorded_by, recorded_at=args.recorded_at or _now(),
|
|
200
|
+
)
|
|
201
|
+
with RecordStore(args.store, writer=True) as store:
|
|
202
|
+
for body in bodies:
|
|
203
|
+
entry = store.append(body)
|
|
204
|
+
appended.append({"id": body["id"], "digest": entry.digest, "appended": entry.appended})
|
|
205
|
+
data["evaluation_records"] = appended
|
|
206
|
+
counts = report.counts
|
|
207
|
+
lines = [f"scoring rule {report.scoring_rule['id']} v{report.scoring_rule['version']} {report.scoring_rule['digest']}"]
|
|
208
|
+
lines.append("counts: " + ", ".join(f"{k} {v}" for k, v in counts.items()))
|
|
209
|
+
for r in report.results:
|
|
210
|
+
loss = "" if r.loss is None else f" loss {json.dumps(r.loss)}"
|
|
211
|
+
why = f" ({', '.join(r.reasons)})" if r.reasons else ""
|
|
212
|
+
lines.append(f" {r.status:<10} {r.kind:<9} {r.record['id']} r{r.record['revision']}{loss}{why}")
|
|
213
|
+
lines += [f" {'appended' if a['appended'] else 'present'} evaluation {a['id']}" for a in appended]
|
|
214
|
+
lines.append("no aggregate score is computed")
|
|
215
|
+
out.emit(data, lines)
|
|
216
|
+
return OK
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def cmd_reproduce(args, out: _Output) -> int:
|
|
220
|
+
store = RecordStore(args.store)
|
|
221
|
+
records = store.records()
|
|
222
|
+
evaluations = [b for b in records if b["record_type"] == "evaluation"]
|
|
223
|
+
if args.ids:
|
|
224
|
+
evaluations = [b for b in evaluations if b["id"] in set(args.ids)]
|
|
225
|
+
missing = set(args.ids) - {b["id"] for b in evaluations}
|
|
226
|
+
if missing:
|
|
227
|
+
raise _Usage(f"no stored evaluation with id {sorted(missing)}")
|
|
228
|
+
results, lines, failed = [], [], False
|
|
229
|
+
for body in evaluations:
|
|
230
|
+
reproduction = reproduce(body, records)
|
|
231
|
+
failed |= reproduction.result != "match"
|
|
232
|
+
results.append({
|
|
233
|
+
"id": body["id"], "revision": body["revision"], "result": reproduction.result,
|
|
234
|
+
"reason": reproduction.reason, "stored": reproduction.stored, "recomputed": reproduction.recomputed,
|
|
235
|
+
})
|
|
236
|
+
loss = reproduction.stored.get("loss")
|
|
237
|
+
lines.append(f"{reproduction.result:<11} {body['id']} r{body['revision']} loss {json.dumps(loss)}"
|
|
238
|
+
+ (f" ({reproduction.reason})" if reproduction.reason else ""))
|
|
239
|
+
out.emit({"results": results}, lines or ["no stored evaluations"])
|
|
240
|
+
return FAILED if failed else OK
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------------ parser
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class _Usage(Exception):
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
251
|
+
# --json is accepted before or after the command; SUPPRESS keeps a subcommand's default
|
|
252
|
+
# from overwriting a value given at the top level.
|
|
253
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
254
|
+
common.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help="machine-readable JSON output")
|
|
255
|
+
parser = argparse.ArgumentParser(
|
|
256
|
+
prog="comuvia",
|
|
257
|
+
description="Offline evidence and prediction records: validate, digest, verify, store, evaluate, reproduce.",
|
|
258
|
+
)
|
|
259
|
+
parser.add_argument("--json", action="store_true", help="machine-readable JSON output")
|
|
260
|
+
parser.add_argument("--version", action="version", version=f"comuvia {__version__}")
|
|
261
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
262
|
+
|
|
263
|
+
p = sub.add_parser("validate", parents=[common], help="validate record files")
|
|
264
|
+
p.add_argument("files", nargs="+")
|
|
265
|
+
p.set_defaults(handler=cmd_validate)
|
|
266
|
+
|
|
267
|
+
p = sub.add_parser("digest", parents=[common], help="record digest of a record file, or content digest of raw bytes")
|
|
268
|
+
p.add_argument("file")
|
|
269
|
+
p.add_argument("--content", action="store_true", help="digest the file's exact bytes instead of a record")
|
|
270
|
+
p.set_defaults(handler=cmd_digest)
|
|
271
|
+
|
|
272
|
+
p = sub.add_parser("verify", parents=[common], help="verify a record against content bytes and a store")
|
|
273
|
+
p.add_argument("record")
|
|
274
|
+
p.add_argument("--content", help="retained bytes of the record's resource (snapshot, descriptor or assertion)")
|
|
275
|
+
p.add_argument("--expected-digest", help="the record digest the record should have")
|
|
276
|
+
p.add_argument("--store", help="store whose records resolve the record's references")
|
|
277
|
+
p.set_defaults(handler=cmd_verify)
|
|
278
|
+
|
|
279
|
+
store = sub.add_parser("store", parents=[common], help="single-writer record store").add_subparsers(dest="store_command", required=True)
|
|
280
|
+
p = store.add_parser("init", parents=[common], help="create an empty store")
|
|
281
|
+
p.add_argument("store")
|
|
282
|
+
p.set_defaults(handler=cmd_store_init)
|
|
283
|
+
p = store.add_parser("append", parents=[common], help="append record files (takes the writer lock)")
|
|
284
|
+
p.add_argument("store")
|
|
285
|
+
p.add_argument("files", nargs="+")
|
|
286
|
+
p.set_defaults(handler=cmd_store_append)
|
|
287
|
+
p = store.add_parser(
|
|
288
|
+
"verify", parents=[common],
|
|
289
|
+
help="check index, bodies and references by name (identity only; use 'reproduce' for evaluation results)",
|
|
290
|
+
)
|
|
291
|
+
p.add_argument("store")
|
|
292
|
+
p.set_defaults(handler=cmd_store_verify)
|
|
293
|
+
p = store.add_parser("show", parents=[common], help="list stored records")
|
|
294
|
+
p.add_argument("store")
|
|
295
|
+
p.add_argument("--latest", action="store_true", help="latest revision per record only")
|
|
296
|
+
p.add_argument("--record-type")
|
|
297
|
+
p.add_argument("--id")
|
|
298
|
+
p.add_argument("--bodies", action="store_true", help="include full record bodies (with --json)")
|
|
299
|
+
p.set_defaults(handler=cmd_store_show)
|
|
300
|
+
p = store.add_parser("repair", parents=[common], help="operator action: remove an incomplete final index line")
|
|
301
|
+
p.add_argument("store")
|
|
302
|
+
p.add_argument("--truncate-torn-tail", action="store_true", required=True)
|
|
303
|
+
p.add_argument("--confirm", action="store_true", required=True)
|
|
304
|
+
p.set_defaults(handler=cmd_store_repair)
|
|
305
|
+
|
|
306
|
+
p = sub.add_parser("evaluate", parents=[common], help="evaluate forecasts and narrative candidates")
|
|
307
|
+
source = p.add_mutually_exclusive_group(required=True)
|
|
308
|
+
source.add_argument("--store")
|
|
309
|
+
source.add_argument("--files", nargs="+")
|
|
310
|
+
p.add_argument("--append", action="store_true", help="append evaluation records to the store")
|
|
311
|
+
p.add_argument("--evaluated-at", help="explicit evaluation time for appended records (RFC 3339)")
|
|
312
|
+
p.add_argument("--recorded-by", help="agent recording the evaluations, e.g. evaluator:name")
|
|
313
|
+
p.add_argument("--recorded-at", help="recording time (RFC 3339); defaults to now")
|
|
314
|
+
p.set_defaults(handler=cmd_evaluate)
|
|
315
|
+
|
|
316
|
+
p = sub.add_parser("reproduce", parents=[common], help="re-derive stored evaluations from their pinned inputs")
|
|
317
|
+
p.add_argument("--store", required=True)
|
|
318
|
+
p.add_argument("ids", nargs="*", help="evaluation ids (default: every stored evaluation)")
|
|
319
|
+
p.set_defaults(handler=cmd_reproduce)
|
|
320
|
+
return parser
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def main(argv: list[str] | None = None) -> int:
|
|
324
|
+
parser = build_parser()
|
|
325
|
+
args = parser.parse_args(argv)
|
|
326
|
+
out = _Output(args.json)
|
|
327
|
+
try:
|
|
328
|
+
return args.handler(args, out)
|
|
329
|
+
except _Usage as error:
|
|
330
|
+
parser.print_usage(sys.stderr)
|
|
331
|
+
print(f"comuvia: error: {error}", file=sys.stderr)
|
|
332
|
+
return USAGE
|
|
333
|
+
except RefusalError as error:
|
|
334
|
+
out.emit({"refused": True, "issues": _issues(error.issues)}, ["REFUSED"] + _issue_lines(error.issues))
|
|
335
|
+
return FAILED
|
|
336
|
+
except FileNotFoundError as error:
|
|
337
|
+
out.emit({"refused": True, "issues": [{"code": "file_not_found", "pointer": "", "message": str(error)}]},
|
|
338
|
+
[f"REFUSED: {error}"])
|
|
339
|
+
return FAILED
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
sys.exit(main())
|