techtree 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.
- techtree/__init__.py +35 -0
- techtree/__main__.py +14 -0
- techtree/canonical.py +239 -0
- techtree/catalog/__init__.py +25 -0
- techtree/catalog/repository.py +400 -0
- techtree/catalog/service.py +419 -0
- techtree/cli/__init__.py +1 -0
- techtree/cli/app.py +416 -0
- techtree/cli/commands/__init__.py +1 -0
- techtree/cli/commands/climb.py +1223 -0
- techtree/cli/commands/doctor.py +147 -0
- techtree/cli/commands/engine.py +207 -0
- techtree/cli/commands/proof.py +556 -0
- techtree/cli/commands/publish.py +447 -0
- techtree/cli/commands/release.py +303 -0
- techtree/cli/commands/run.py +1067 -0
- techtree/cli/commands/setup.py +181 -0
- techtree/cli/commands/skill.py +221 -0
- techtree/cli/commands/uplift.py +698 -0
- techtree/cli/commands/withdraw.py +212 -0
- techtree/cli/confirm.py +47 -0
- techtree/cli/context.py +96 -0
- techtree/cli/invoke.py +220 -0
- techtree/cli/output.py +280 -0
- techtree/constants.py +138 -0
- techtree/crypto.py +128 -0
- techtree/doctor/__init__.py +1 -0
- techtree/doctor/checks.py +675 -0
- techtree/doctor/execution_checks.py +435 -0
- techtree/doctor/service.py +326 -0
- techtree/drafts/__init__.py +32 -0
- techtree/drafts/source.py +146 -0
- techtree/drafts/store.py +992 -0
- techtree/engines/__init__.py +1 -0
- techtree/engines/bundle.py +251 -0
- techtree/engines/installer.py +679 -0
- techtree/engines/registry.py +235 -0
- techtree/engines/runner.py +170 -0
- techtree/errors.py +262 -0
- techtree/fs.py +234 -0
- techtree/harness.py +108 -0
- techtree/identity/__init__.py +41 -0
- techtree/identity/models.py +113 -0
- techtree/identity/service.py +199 -0
- techtree/identity/store.py +263 -0
- techtree/ids.py +85 -0
- techtree/manifests/__init__.py +39 -0
- techtree/manifests/builder.py +433 -0
- techtree/manifests/compare.py +376 -0
- techtree/models/__init__.py +282 -0
- techtree/models/base.py +201 -0
- techtree/models/campaign.py +484 -0
- techtree/models/catalog.py +227 -0
- techtree/models/cli.py +151 -0
- techtree/models/climb.py +254 -0
- techtree/models/data_policy.py +130 -0
- techtree/models/engine.py +156 -0
- techtree/models/episode_receipt.py +130 -0
- techtree/models/evaluation_backend.py +113 -0
- techtree/models/experiment.py +154 -0
- techtree/models/run.py +214 -0
- techtree/models/skill.py +156 -0
- techtree/models/uplift_report.py +158 -0
- techtree/models/validation.py +299 -0
- techtree/paths.py +116 -0
- techtree/presentation/__init__.py +31 -0
- techtree/presentation/build.py +1242 -0
- techtree/presentation/compact.py +246 -0
- techtree/presentation/evidence.py +169 -0
- techtree/presentation/models.py +358 -0
- techtree/presentation/rich.py +312 -0
- techtree/presentation/sanitize.py +156 -0
- techtree/publication/__init__.py +44 -0
- techtree/publication/address.py +180 -0
- techtree/publication/coordinates.py +26 -0
- techtree/publication/journal.py +212 -0
- techtree/publication/keccak.py +183 -0
- techtree/publication/models.py +209 -0
- techtree/publication/offer.py +35 -0
- techtree/publication/service.py +618 -0
- techtree/publication/transport.py +296 -0
- techtree/publication/verify.py +242 -0
- techtree/publication/withdraw.py +156 -0
- techtree/py.typed +0 -0
- techtree/receipts/__init__.py +52 -0
- techtree/receipts/bundle.py +578 -0
- techtree/receipts/compare.py +1065 -0
- techtree/receipts/episode.py +672 -0
- techtree/receipts/execution.py +630 -0
- techtree/receipts/observed.py +474 -0
- techtree/receipts/set.py +336 -0
- techtree/receipts/uplift.py +655 -0
- techtree/receipts/verify.py +1055 -0
- techtree/release/__init__.py +9 -0
- techtree/release/bootstrap.py +509 -0
- techtree/release/checks.py +376 -0
- techtree/release/document.py +125 -0
- techtree/release/generate.py +221 -0
- techtree/release/models.py +293 -0
- techtree/release/provenance.py +109 -0
- techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
- techtree/resources/catalog/catalog.json +32 -0
- techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
- techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
- techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
- techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
- techtree/resources/engines/default/engine.json +20 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
- techtree/resources/engines/default/pyproject.toml +23 -0
- techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
- techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
- techtree/resources/engines/default/tools/normalize_validation.py +222 -0
- techtree/resources/engines/default/uv.lock +1758 -0
- techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
- techtree/resources/release/build-provenance.json +4 -0
- techtree/resources/release/release-core.json +24 -0
- techtree/runs/__init__.py +31 -0
- techtree/runs/artifacts.py +750 -0
- techtree/runs/child_registry.py +228 -0
- techtree/runs/events.py +478 -0
- techtree/runs/executor.py +140 -0
- techtree/runs/fake.py +741 -0
- techtree/runs/launcher.py +253 -0
- techtree/runs/machine.py +489 -0
- techtree/runs/real.py +789 -0
- techtree/runs/service.py +616 -0
- techtree/runs/store.py +555 -0
- techtree/runs/validation.py +259 -0
- techtree/runs/variants.py +684 -0
- techtree/settings.py +143 -0
- techtree/skills/__init__.py +14 -0
- techtree/skills/archive.py +282 -0
- techtree/skills/policy.py +62 -0
- techtree/skills/scanner.py +394 -0
- techtree/skills/service.py +752 -0
- techtree/skills/starter.py +434 -0
- techtree/tasksets/__init__.py +1 -0
- techtree/tasksets/membership.py +269 -0
- techtree/tasksets/provider.py +207 -0
- techtree/tasksets/resolver.py +311 -0
- techtree/tasksets/service.py +484 -0
- techtree/tasksets/verifiers_cli.py +538 -0
- techtree/uplift/__init__.py +20 -0
- techtree/uplift/context.py +544 -0
- techtree/uplift/derive.py +203 -0
- techtree/uplift/public_tasks.py +151 -0
- techtree/uplift/service.py +719 -0
- techtree/uplift/source.py +160 -0
- techtree/verifiers/__init__.py +31 -0
- techtree/verifiers/budget.py +219 -0
- techtree/verifiers/child.py +633 -0
- techtree/verifiers/compiler.py +432 -0
- techtree/verifiers/config.py +365 -0
- techtree/verifiers/credentials.py +321 -0
- techtree/verifiers/image.py +126 -0
- techtree/verifiers/models.py +527 -0
- techtree/verifiers/outputs.py +368 -0
- techtree/verifiers/progress.py +192 -0
- techtree/verifiers/supervisor.py +341 -0
- techtree/verifiers/verify.py +782 -0
- techtree/version.py +39 -0
- techtree/worker/__init__.py +18 -0
- techtree/worker/execute.py +487 -0
- techtree/worker/main.py +57 -0
- techtree-0.1.0.dist-info/METADATA +344 -0
- techtree-0.1.0.dist-info/RECORD +174 -0
- techtree-0.1.0.dist-info/WHEEL +4 -0
- techtree-0.1.0.dist-info/entry_points.txt +3 -0
- techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""Locking a taskset and proving it valid. Spec section 21.5.
|
|
2
|
+
|
|
3
|
+
One question runs through this module: *is the thing about to be scored the
|
|
4
|
+
thing that was committed to, and is it mechanically sound?* Answering it takes
|
|
5
|
+
two independent halves, and the shape of the code follows that split.
|
|
6
|
+
|
|
7
|
+
The first half is identity. :class:`~techtree.tasksets.resolver.TasksetResolver`
|
|
8
|
+
loads the taskset twice in fresh processes and produces a
|
|
9
|
+
:class:`~techtree.models.validation.TasksetLock`; this module compares that lock
|
|
10
|
+
with the membership a Campaign committed to.
|
|
11
|
+
|
|
12
|
+
The second half is soundness. The pinned Verifiers validator runs every task's
|
|
13
|
+
gold and setup check with no model anywhere, the engine normalizes its output
|
|
14
|
+
into deterministic evidence, and the counts become the mechanical checks a
|
|
15
|
+
:class:`~techtree.models.validation.TasksetValidationReceipt` carries.
|
|
16
|
+
|
|
17
|
+
**Nothing that varies between two correct runs enters the receipt.** Decisions
|
|
18
|
+
document 0003 A1 made the receipt deterministic so that the publisher's receipt
|
|
19
|
+
and a participant's recomputed receipt are *equal*, and that equality is the
|
|
20
|
+
comparison. Everything a real execution also produces — when it started, on
|
|
21
|
+
which host, under which command, and the raw files it left behind — goes into a
|
|
22
|
+
:class:`~techtree.models.validation.ValidationExecutionRecord` instead, which is
|
|
23
|
+
local, never shipped, and never byte-compared.
|
|
24
|
+
|
|
25
|
+
The consequence for callers is worth stating plainly: this service is used
|
|
26
|
+
unchanged by the publisher's catalog generator and by a participant's worker.
|
|
27
|
+
There is no publisher mode. If the two ever produced different receipts for the
|
|
28
|
+
same taskset, that would be the finding, not a detail to reconcile.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import platform
|
|
35
|
+
import sys
|
|
36
|
+
from collections.abc import Sequence
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from datetime import UTC, datetime
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Final, Literal
|
|
41
|
+
|
|
42
|
+
from techtree.canonical import canonical_json_bytes, digest_object, sha256_digest_bytes
|
|
43
|
+
from techtree.constants import (
|
|
44
|
+
TASKSET_VALIDATION_SCHEMA_VERSION,
|
|
45
|
+
VALIDATION_EXECUTION_SCHEMA_VERSION,
|
|
46
|
+
)
|
|
47
|
+
from techtree.engines.registry import EngineRegistry
|
|
48
|
+
from techtree.errors import PrerequisiteError, VerificationError
|
|
49
|
+
from techtree.fs import atomic_write_bytes, ensure_private_directory
|
|
50
|
+
from techtree.models.base import ArtifactRef, Digest
|
|
51
|
+
from techtree.models.campaign import CampaignSpec, TaskSelection, TasksetRef
|
|
52
|
+
from techtree.models.engine import normalize_host_platform
|
|
53
|
+
from techtree.models.validation import (
|
|
54
|
+
REQUIRED_VALIDATION_CHECKS,
|
|
55
|
+
TasksetLock,
|
|
56
|
+
TasksetValidationReceipt,
|
|
57
|
+
UpstreamValidationSummary,
|
|
58
|
+
ValidationCheck,
|
|
59
|
+
ValidationEvidence,
|
|
60
|
+
ValidationExecutionRecord,
|
|
61
|
+
ValidationMethod,
|
|
62
|
+
validation_display_id,
|
|
63
|
+
)
|
|
64
|
+
from techtree.tasksets.membership import (
|
|
65
|
+
MEMBERSHIP_MATCH_CHECK,
|
|
66
|
+
MEMBERSHIP_REPEATABILITY_CHECK,
|
|
67
|
+
compare_membership,
|
|
68
|
+
)
|
|
69
|
+
from techtree.tasksets.resolver import TasksetResolver
|
|
70
|
+
from techtree.tasksets.verifiers_cli import (
|
|
71
|
+
GOLD_CHECK,
|
|
72
|
+
SETUP_CHECK,
|
|
73
|
+
UpstreamCheckCounts,
|
|
74
|
+
VerifiersValidationRunner,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"EVIDENCE_FILENAME",
|
|
79
|
+
"EXECUTION_RECORD_FILENAME",
|
|
80
|
+
"LOCK_FILENAME",
|
|
81
|
+
"RECEIPT_FILENAME",
|
|
82
|
+
"TASKSET_DIRECTORY",
|
|
83
|
+
"VALIDATION_DIRECTORY",
|
|
84
|
+
"TasksetService",
|
|
85
|
+
"TasksetValidationRun",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
#: Where a run keeps everything about its taskset. Spec section 28.
|
|
89
|
+
TASKSET_DIRECTORY: Final = "taskset"
|
|
90
|
+
VALIDATION_DIRECTORY: Final = "validation"
|
|
91
|
+
|
|
92
|
+
LOCK_FILENAME: Final = "lock.json"
|
|
93
|
+
RECEIPT_FILENAME: Final = "receipt.json"
|
|
94
|
+
#: Decisions document 0003 A1 added the normalized evidence and the execution
|
|
95
|
+
#: record after spec section 28 was written. Both live beside the receipt: the
|
|
96
|
+
#: evidence because the receipt points at it, the record because it is the raw
|
|
97
|
+
#: provenance of the same execution.
|
|
98
|
+
EVIDENCE_FILENAME: Final = "evidence.json"
|
|
99
|
+
EXECUTION_RECORD_FILENAME: Final = "execution.json"
|
|
100
|
+
|
|
101
|
+
_JSON_MEDIA_TYPE: Final = "application/json"
|
|
102
|
+
|
|
103
|
+
#: The two mechanical checks that come from the validator itself.
|
|
104
|
+
_UPSTREAM_GOLD_CHECK: Final = "upstream_gold"
|
|
105
|
+
_UPSTREAM_SETUP_CHECK: Final = "upstream_setup"
|
|
106
|
+
_UNIQUENESS_CHECK: Final = "task_hash_uniqueness"
|
|
107
|
+
_TASK_COUNT_CHECK: Final = "expected_task_count"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class TasksetValidationRun:
|
|
112
|
+
"""Everything one validation produced, scientific and operational.
|
|
113
|
+
|
|
114
|
+
Spec section 21.5 has ``resolve_and_validate`` return a lock and a receipt.
|
|
115
|
+
Decisions document 0003 A1 added two more objects with distinct lifetimes —
|
|
116
|
+
the shipped evidence a receipt points at, and the local record of the
|
|
117
|
+
execution — so they are returned together rather than fetched afterwards
|
|
118
|
+
from paths a caller would have to reconstruct.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
lock: TasksetLock
|
|
122
|
+
receipt: TasksetValidationReceipt
|
|
123
|
+
evidence: ValidationEvidence
|
|
124
|
+
execution_record: ValidationExecutionRecord
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def receipt_digest(self) -> Digest:
|
|
128
|
+
"""Return the receipt's content digest, which is its identity."""
|
|
129
|
+
return digest_object(self.receipt)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TasksetService:
|
|
133
|
+
"""Resolves, validates, and issues a receipt for one taskset.
|
|
134
|
+
|
|
135
|
+
The service is bound to one installed engine. Which engine that is comes
|
|
136
|
+
from the caller — a participant uses the one the publisher's receipt names,
|
|
137
|
+
so that a difference in receipts can only ever be a difference in the
|
|
138
|
+
taskset.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
registry: EngineRegistry,
|
|
144
|
+
engine_digest: Digest,
|
|
145
|
+
) -> None:
|
|
146
|
+
self._registry = registry
|
|
147
|
+
self._engine_digest = engine_digest
|
|
148
|
+
self._resolver = TasksetResolver(registry, engine_digest)
|
|
149
|
+
self._validator = VerifiersValidationRunner(registry, engine_digest)
|
|
150
|
+
|
|
151
|
+
# -- the two halves, and the whole -------------------------------------
|
|
152
|
+
|
|
153
|
+
def resolve(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
taskset_ref: TasksetRef,
|
|
157
|
+
selection: TaskSelection,
|
|
158
|
+
) -> TasksetLock:
|
|
159
|
+
"""Return what this reference resolves to against this engine.
|
|
160
|
+
|
|
161
|
+
Exposed separately because the publisher has nothing to compare a lock
|
|
162
|
+
with yet: the Campaign that will commit to a membership is built from
|
|
163
|
+
this lock, not the other way round.
|
|
164
|
+
"""
|
|
165
|
+
return self._resolver.resolve(taskset_ref=taskset_ref, selection=selection)
|
|
166
|
+
|
|
167
|
+
def validate(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
lock: TasksetLock,
|
|
171
|
+
committed_task_hashes: Sequence[Digest],
|
|
172
|
+
expected_task_count: int,
|
|
173
|
+
run_dir: Path,
|
|
174
|
+
) -> TasksetValidationRun:
|
|
175
|
+
"""Run the pinned validation against an already-resolved lock."""
|
|
176
|
+
taskset = run_dir / TASKSET_DIRECTORY
|
|
177
|
+
output_dir = taskset / VALIDATION_DIRECTORY
|
|
178
|
+
ensure_private_directory(taskset)
|
|
179
|
+
ensure_private_directory(output_dir)
|
|
180
|
+
|
|
181
|
+
self._write_lock(run_dir, lock)
|
|
182
|
+
|
|
183
|
+
started_at = datetime.now(UTC)
|
|
184
|
+
process = self._validator.run(
|
|
185
|
+
taskset_id=lock.taskset_ref.id,
|
|
186
|
+
num_tasks=lock.task_count,
|
|
187
|
+
output_dir=output_dir,
|
|
188
|
+
)
|
|
189
|
+
finished_at = datetime.now(UTC)
|
|
190
|
+
|
|
191
|
+
summary = self._validator.parse_summary(output_dir)
|
|
192
|
+
check_counts = self._validator.parse_check_counts(output_dir)
|
|
193
|
+
evidence = self._validator.normalize(
|
|
194
|
+
output_dir, taskset_lock_digest=digest_object(lock)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
checks = self._build_checks(
|
|
198
|
+
lock=lock,
|
|
199
|
+
committed_task_hashes=committed_task_hashes,
|
|
200
|
+
expected_task_count=expected_task_count,
|
|
201
|
+
check_counts=check_counts,
|
|
202
|
+
)
|
|
203
|
+
receipt = TasksetValidationReceipt(
|
|
204
|
+
schema_version=TASKSET_VALIDATION_SCHEMA_VERSION,
|
|
205
|
+
taskset_lock_digest=digest_object(lock),
|
|
206
|
+
engine_digest=lock.engine_digest,
|
|
207
|
+
method=ValidationMethod(
|
|
208
|
+
kind="verifiers_validate",
|
|
209
|
+
mode="all",
|
|
210
|
+
runtime="subprocess",
|
|
211
|
+
validator_revision=evidence.method.validator_revision,
|
|
212
|
+
),
|
|
213
|
+
status=self._receipt_status(checks, summary),
|
|
214
|
+
upstream_summary=summary,
|
|
215
|
+
checks=checks,
|
|
216
|
+
normalized_evidence=_evidence_reference(evidence),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
self._write_evidence(run_dir, evidence)
|
|
220
|
+
self._write_receipt(run_dir, receipt)
|
|
221
|
+
|
|
222
|
+
command = [str(part) for part in process.argv]
|
|
223
|
+
record = ValidationExecutionRecord(
|
|
224
|
+
schema_version=VALIDATION_EXECUTION_SCHEMA_VERSION,
|
|
225
|
+
id=validation_display_id(digest_object(receipt)),
|
|
226
|
+
receipt_digest=digest_object(receipt),
|
|
227
|
+
started_at=started_at,
|
|
228
|
+
finished_at=finished_at,
|
|
229
|
+
command=command,
|
|
230
|
+
command_digest=digest_object({"command": command}),
|
|
231
|
+
host_platform=_host_platform(),
|
|
232
|
+
worker_pid=os.getpid(),
|
|
233
|
+
raw_artifacts=self._validator.validation_artifacts(output_dir),
|
|
234
|
+
)
|
|
235
|
+
self._write_execution_record(run_dir, record)
|
|
236
|
+
|
|
237
|
+
return TasksetValidationRun(
|
|
238
|
+
lock=lock,
|
|
239
|
+
receipt=receipt,
|
|
240
|
+
evidence=evidence,
|
|
241
|
+
execution_record=record,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def resolve_and_validate(
|
|
245
|
+
self,
|
|
246
|
+
*,
|
|
247
|
+
campaign: CampaignSpec,
|
|
248
|
+
run_dir: Path,
|
|
249
|
+
) -> TasksetValidationRun:
|
|
250
|
+
"""Resolve this Campaign's taskset, validate it, and issue a receipt."""
|
|
251
|
+
lock = self.resolve(
|
|
252
|
+
taskset_ref=campaign.taskset.ref,
|
|
253
|
+
selection=campaign.taskset.selection,
|
|
254
|
+
)
|
|
255
|
+
return self.validate(
|
|
256
|
+
lock=lock,
|
|
257
|
+
committed_task_hashes=campaign.taskset.membership.ordered_task_hashes,
|
|
258
|
+
expected_task_count=campaign.taskset.selection.num_tasks,
|
|
259
|
+
run_dir=run_dir,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# -- the mechanical checks ---------------------------------------------
|
|
263
|
+
|
|
264
|
+
def _build_checks(
|
|
265
|
+
self,
|
|
266
|
+
*,
|
|
267
|
+
lock: TasksetLock,
|
|
268
|
+
committed_task_hashes: Sequence[Digest],
|
|
269
|
+
expected_task_count: int,
|
|
270
|
+
check_counts: dict[str, UpstreamCheckCounts],
|
|
271
|
+
) -> list[ValidationCheck]:
|
|
272
|
+
"""Build every check a receipt is required to report.
|
|
273
|
+
|
|
274
|
+
Every detail string is a function of the taskset alone. A path, a
|
|
275
|
+
duration, or a host name in any of them would make two correct receipts
|
|
276
|
+
for the same taskset differ, which is the one thing decisions document
|
|
277
|
+
0003 A1 set out to prevent.
|
|
278
|
+
"""
|
|
279
|
+
checks = [
|
|
280
|
+
_upstream_check(_UPSTREAM_GOLD_CHECK, check_counts.get(GOLD_CHECK)),
|
|
281
|
+
_upstream_check(_UPSTREAM_SETUP_CHECK, check_counts.get(SETUP_CHECK)),
|
|
282
|
+
ValidationCheck(
|
|
283
|
+
id=MEMBERSHIP_REPEATABILITY_CHECK,
|
|
284
|
+
status="passed",
|
|
285
|
+
detail=(
|
|
286
|
+
f"two independent inspections agreed on all {lock.task_count} "
|
|
287
|
+
"task hashes, in order"
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
_uniqueness_check(lock),
|
|
291
|
+
compare_membership(
|
|
292
|
+
list(lock.ordered_task_hashes),
|
|
293
|
+
list(committed_task_hashes),
|
|
294
|
+
check_id=MEMBERSHIP_MATCH_CHECK,
|
|
295
|
+
),
|
|
296
|
+
_task_count_check(lock, expected_task_count),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
reported = {check.id for check in checks}
|
|
300
|
+
missing = [name for name in REQUIRED_VALIDATION_CHECKS if name not in reported]
|
|
301
|
+
if missing:
|
|
302
|
+
raise VerificationError(
|
|
303
|
+
"this validation reported no result for " + ", ".join(missing),
|
|
304
|
+
code="taskset_validation_incomplete",
|
|
305
|
+
details={"missing": list(missing)},
|
|
306
|
+
)
|
|
307
|
+
return checks
|
|
308
|
+
|
|
309
|
+
def _receipt_status(
|
|
310
|
+
self,
|
|
311
|
+
checks: Sequence[ValidationCheck],
|
|
312
|
+
summary: UpstreamValidationSummary,
|
|
313
|
+
) -> Literal["valid", "invalid", "errored"]:
|
|
314
|
+
"""Decide what the receipt claims about this taskset.
|
|
315
|
+
|
|
316
|
+
``errored`` and ``invalid`` are different findings. A task whose check
|
|
317
|
+
raised, timed out, or never recorded a row says the validation did not
|
|
318
|
+
reach a verdict; a task that returned False says it reached one and the
|
|
319
|
+
answer was no. Reporting the first as the second would present an
|
|
320
|
+
unfinished run as a scientific result.
|
|
321
|
+
"""
|
|
322
|
+
if summary.error or summary.timeout or summary.missing:
|
|
323
|
+
return "errored"
|
|
324
|
+
if summary.recorded != summary.total:
|
|
325
|
+
return "errored"
|
|
326
|
+
if summary.invalid or summary.valid != summary.total:
|
|
327
|
+
return "invalid"
|
|
328
|
+
if any(check.status == "failed" for check in checks):
|
|
329
|
+
return "invalid"
|
|
330
|
+
return "valid"
|
|
331
|
+
|
|
332
|
+
# -- persistence -------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
def _write_lock(self, run_dir: Path, lock: TasksetLock) -> ArtifactRef:
|
|
335
|
+
"""Persist the lock this run was issued under."""
|
|
336
|
+
return _write_object(run_dir / TASKSET_DIRECTORY / LOCK_FILENAME, lock, run_dir)
|
|
337
|
+
|
|
338
|
+
def _write_receipt(
|
|
339
|
+
self, run_dir: Path, receipt: TasksetValidationReceipt
|
|
340
|
+
) -> ArtifactRef:
|
|
341
|
+
"""Persist the receipt in exactly the bytes its digest is taken over."""
|
|
342
|
+
return _write_object(
|
|
343
|
+
run_dir / TASKSET_DIRECTORY / VALIDATION_DIRECTORY / RECEIPT_FILENAME,
|
|
344
|
+
receipt,
|
|
345
|
+
run_dir,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _write_evidence(
|
|
349
|
+
self, run_dir: Path, evidence: ValidationEvidence
|
|
350
|
+
) -> ArtifactRef:
|
|
351
|
+
"""Persist the normalized evidence the receipt points at."""
|
|
352
|
+
return _write_object(
|
|
353
|
+
run_dir / TASKSET_DIRECTORY / VALIDATION_DIRECTORY / EVIDENCE_FILENAME,
|
|
354
|
+
evidence,
|
|
355
|
+
run_dir,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _write_execution_record(
|
|
359
|
+
self, run_dir: Path, record: ValidationExecutionRecord
|
|
360
|
+
) -> ArtifactRef:
|
|
361
|
+
"""Persist the local provenance of this execution."""
|
|
362
|
+
return _write_object(
|
|
363
|
+
run_dir
|
|
364
|
+
/ TASKSET_DIRECTORY
|
|
365
|
+
/ VALIDATION_DIRECTORY
|
|
366
|
+
/ EXECUTION_RECORD_FILENAME,
|
|
367
|
+
record,
|
|
368
|
+
run_dir,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
# Check construction
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _upstream_check(
|
|
378
|
+
check_id: str, counts: UpstreamCheckCounts | None
|
|
379
|
+
) -> ValidationCheck:
|
|
380
|
+
"""Turn one of the validator's per-check breakdowns into a receipt check."""
|
|
381
|
+
if counts is None:
|
|
382
|
+
return ValidationCheck(
|
|
383
|
+
id=check_id,
|
|
384
|
+
status="failed",
|
|
385
|
+
detail=(
|
|
386
|
+
"the validator reported no per-check breakdown, so this check "
|
|
387
|
+
"cannot be read as either passed or failed"
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
if counts.passed:
|
|
391
|
+
return ValidationCheck(
|
|
392
|
+
id=check_id,
|
|
393
|
+
status="passed",
|
|
394
|
+
detail=f"all {counts.valid} tasks passed this check",
|
|
395
|
+
)
|
|
396
|
+
return ValidationCheck(id=check_id, status="failed", detail=counts.summary())
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _uniqueness_check(lock: TasksetLock) -> ValidationCheck:
|
|
400
|
+
"""Report whether any task appears twice in the locked membership."""
|
|
401
|
+
hashes = list(lock.ordered_task_hashes)
|
|
402
|
+
repeated = len(hashes) - len(set(hashes))
|
|
403
|
+
if repeated == 0:
|
|
404
|
+
return ValidationCheck(
|
|
405
|
+
id=_UNIQUENESS_CHECK,
|
|
406
|
+
status="passed",
|
|
407
|
+
detail=f"all {len(hashes)} task hashes are distinct",
|
|
408
|
+
)
|
|
409
|
+
return ValidationCheck(
|
|
410
|
+
id=_UNIQUENESS_CHECK,
|
|
411
|
+
status="failed",
|
|
412
|
+
detail=(
|
|
413
|
+
f"{repeated} of {len(hashes)} task hashes repeat, and a repeated "
|
|
414
|
+
"task would be scored twice under one commitment"
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _task_count_check(lock: TasksetLock, expected: int) -> ValidationCheck:
|
|
420
|
+
"""Report whether the lock holds as many tasks as were asked for."""
|
|
421
|
+
if lock.task_count == expected:
|
|
422
|
+
return ValidationCheck(
|
|
423
|
+
id=_TASK_COUNT_CHECK,
|
|
424
|
+
status="passed",
|
|
425
|
+
detail=f"{expected} tasks, as the selection asks for",
|
|
426
|
+
)
|
|
427
|
+
return ValidationCheck(
|
|
428
|
+
id=_TASK_COUNT_CHECK,
|
|
429
|
+
status="failed",
|
|
430
|
+
detail=(
|
|
431
|
+
f"the taskset resolved to {lock.task_count} tasks and the selection "
|
|
432
|
+
f"asks for {expected}"
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
# Bytes
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _evidence_reference(evidence: ValidationEvidence) -> ArtifactRef:
|
|
443
|
+
"""Return the reference a receipt carries to its normalized evidence.
|
|
444
|
+
|
|
445
|
+
No relative path. The receipt is a public object that travels inside a
|
|
446
|
+
content-addressed catalog, where an artifact is found by digest; a path
|
|
447
|
+
would be one machine's answer to a question the catalog answers for
|
|
448
|
+
everyone, and it would make the publisher's receipt and a participant's
|
|
449
|
+
differ over nothing.
|
|
450
|
+
"""
|
|
451
|
+
data = canonical_json_bytes(evidence)
|
|
452
|
+
return ArtifactRef(
|
|
453
|
+
digest=sha256_digest_bytes(data),
|
|
454
|
+
media_type=_JSON_MEDIA_TYPE,
|
|
455
|
+
size=len(data),
|
|
456
|
+
relative_path=None,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _write_object(path: Path, value: object, run_dir: Path) -> ArtifactRef:
|
|
461
|
+
"""Write one object as the canonical bytes its digest is taken over."""
|
|
462
|
+
data = canonical_json_bytes(value)
|
|
463
|
+
ensure_private_directory(path.parent)
|
|
464
|
+
atomic_write_bytes(path, data)
|
|
465
|
+
return ArtifactRef(
|
|
466
|
+
digest=sha256_digest_bytes(data),
|
|
467
|
+
media_type=_JSON_MEDIA_TYPE,
|
|
468
|
+
size=len(data),
|
|
469
|
+
relative_path=path.relative_to(run_dir).as_posix(),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _host_platform() -> str:
|
|
474
|
+
"""Return this host in the protocol's vocabulary, or say it is unsupported.
|
|
475
|
+
|
|
476
|
+
Decisions document 0003 A9. An unsupported host still records *something*:
|
|
477
|
+
the execution record exists to say what happened, and refusing to write one
|
|
478
|
+
because the platform string is unfamiliar would lose the very provenance it
|
|
479
|
+
is for.
|
|
480
|
+
"""
|
|
481
|
+
try:
|
|
482
|
+
return normalize_host_platform(sys.platform, platform.machine())
|
|
483
|
+
except PrerequisiteError:
|
|
484
|
+
return f"{sys.platform}/{platform.machine()}"
|