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
techtree/receipts/set.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""The ordered commitment over one variant's receipts. Spec section 7.7.
|
|
2
|
+
|
|
3
|
+
An uplift report cannot carry dozens of receipt digests as a loose list and
|
|
4
|
+
still be checkable. What it can carry is one digest, of one manifest, that
|
|
5
|
+
commits to every receipt in the Campaign's own committed order together with
|
|
6
|
+
the membership that fixes that order. This module is that manifest.
|
|
7
|
+
|
|
8
|
+
Three properties are the whole point.
|
|
9
|
+
|
|
10
|
+
*Ordered by membership, never by arrival.* Two concurrent variants finish their
|
|
11
|
+
episodes in whatever order the provider and the containers produced, and a
|
|
12
|
+
commitment built in that order would differ between two correct runs of the
|
|
13
|
+
same experiment. Receipts are indexed by task hash and emitted in the order the
|
|
14
|
+
TasksetLock fixes, so two variants of one comparison commit to the same
|
|
15
|
+
positions.
|
|
16
|
+
|
|
17
|
+
*Tamper-evident in both directions.* Each receipt travels inside an
|
|
18
|
+
:class:`~techtree.models.base.ObjectEnvelope` carrying the digest of its own
|
|
19
|
+
canonical bytes, and the manifest commits to those digests. Editing a receipt
|
|
20
|
+
breaks its envelope; editing an envelope breaks the manifest; reordering the
|
|
21
|
+
manifest breaks the membership commitment. :func:`verify_receipt_set`
|
|
22
|
+
recomputes all three from the receipts themselves rather than trusting any
|
|
23
|
+
recorded value.
|
|
24
|
+
|
|
25
|
+
*Local, and honestly named.* The frozen v0.1 protocol has no receipt-set
|
|
26
|
+
object, so this is a versioned local manifest and says so in its schema
|
|
27
|
+
version. It is written into the run directory and referenced by digest; it is
|
|
28
|
+
not published and not part of the Campaign graph.
|
|
29
|
+
|
|
30
|
+
Signatures are deliberately not verified here. The envelope carries an optional
|
|
31
|
+
detached signature, and the key that would check it belongs to the local
|
|
32
|
+
executor identity (spec section 7.5), which is a later slice. This module
|
|
33
|
+
verifies structure, order and content; a caller that holds the identity adds
|
|
34
|
+
the signature check on top and nothing here has to change for it to.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from collections.abc import Sequence
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Final, Literal, Self
|
|
42
|
+
|
|
43
|
+
from pydantic import Field, model_validator
|
|
44
|
+
|
|
45
|
+
from techtree.canonical import (
|
|
46
|
+
canonical_json_bytes,
|
|
47
|
+
digest_object,
|
|
48
|
+
sha256_digest_bytes,
|
|
49
|
+
validate_digest,
|
|
50
|
+
)
|
|
51
|
+
from techtree.errors import VerificationError
|
|
52
|
+
from techtree.fs import atomic_write_bytes, ensure_private_directory
|
|
53
|
+
from techtree.models.base import (
|
|
54
|
+
ArtifactRef,
|
|
55
|
+
Digest,
|
|
56
|
+
JsonValue,
|
|
57
|
+
NonEmptyString,
|
|
58
|
+
ObjectEnvelope,
|
|
59
|
+
ProtocolModel,
|
|
60
|
+
)
|
|
61
|
+
from techtree.models.episode_receipt import EpisodeReceipt
|
|
62
|
+
from techtree.models.experiment import ExperimentVariant
|
|
63
|
+
from techtree.receipts.episode import (
|
|
64
|
+
EPISODE_COUNT_MISMATCH,
|
|
65
|
+
TASK_MEMBERSHIP_MISMATCH,
|
|
66
|
+
)
|
|
67
|
+
from techtree.tasksets.membership import membership_digest
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"RECEIPT_SET_INVALID",
|
|
71
|
+
"RECEIPT_SET_MEDIA_TYPE",
|
|
72
|
+
"RECEIPT_SET_SCHEMA_VERSION",
|
|
73
|
+
"ReceiptSetManifest",
|
|
74
|
+
"build_receipt_set",
|
|
75
|
+
"receipt_set_path",
|
|
76
|
+
"seal_receipt",
|
|
77
|
+
"verify_receipt_set",
|
|
78
|
+
"write_receipt_set",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
#: A local bundle manifest, versioned as one. Spec section 7.7 permits exactly
|
|
82
|
+
#: this when the frozen protocol has no receipt-set object to extend.
|
|
83
|
+
RECEIPT_SET_SCHEMA_VERSION: Final = "techtree.receipt-set.v1alpha1"
|
|
84
|
+
|
|
85
|
+
#: Stable error code. Spec section 15.
|
|
86
|
+
RECEIPT_SET_INVALID: Final = "receipt_set_invalid"
|
|
87
|
+
|
|
88
|
+
RECEIPT_SET_MEDIA_TYPE: Final = "application/json"
|
|
89
|
+
|
|
90
|
+
#: The manifest sits *beside* one variant's receipt directory rather than
|
|
91
|
+
#: inside it. A run's receipt directory is read back whole and every file in it
|
|
92
|
+
#: is parsed as a receipt (``techtree.runs.artifacts``), so a manifest filed
|
|
93
|
+
#: among them would be read as a malformed receipt the first time a run's
|
|
94
|
+
#: results were loaded.
|
|
95
|
+
_RECEIPTS_DIRECTORY: Final = "receipts"
|
|
96
|
+
_RECEIPT_SET_SUFFIX: Final = "-set.json"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ReceiptSetManifest(ProtocolModel):
|
|
100
|
+
"""One variant's receipts, committed to in committed task order."""
|
|
101
|
+
|
|
102
|
+
schema_version: Literal["techtree.receipt-set.v1alpha1"]
|
|
103
|
+
run_id: NonEmptyString
|
|
104
|
+
variant: ExperimentVariant
|
|
105
|
+
experiment_manifest_digest: Digest
|
|
106
|
+
ordered_receipt_digests: list[Digest]
|
|
107
|
+
task_membership_digest: Digest
|
|
108
|
+
receipt_count: int = Field(ge=1)
|
|
109
|
+
|
|
110
|
+
@model_validator(mode="after")
|
|
111
|
+
def _check_the_commitment_is_internally_consistent(self) -> Self:
|
|
112
|
+
"""Reject a manifest whose count or digests contradict themselves."""
|
|
113
|
+
if len(self.ordered_receipt_digests) != self.receipt_count:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"the manifest lists {len(self.ordered_receipt_digests)} receipt "
|
|
116
|
+
f"digests and claims a receipt_count of {self.receipt_count}"
|
|
117
|
+
)
|
|
118
|
+
if len(set(self.ordered_receipt_digests)) != len(self.ordered_receipt_digests):
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"a receipt set commits to each receipt once; a repeated digest "
|
|
121
|
+
"would let one episode occupy two positions"
|
|
122
|
+
)
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def seal_receipt(receipt: EpisodeReceipt) -> ObjectEnvelope[EpisodeReceipt]:
|
|
127
|
+
"""Wrap one receipt with the digest of its own canonical bytes.
|
|
128
|
+
|
|
129
|
+
Unsigned. The envelope's signature field is filled in by whoever holds the
|
|
130
|
+
local executor identity; the digest is what makes the payload immutable and
|
|
131
|
+
is computed here, where the payload is built.
|
|
132
|
+
"""
|
|
133
|
+
return ObjectEnvelope[EpisodeReceipt](
|
|
134
|
+
payload=receipt,
|
|
135
|
+
payload_digest=digest_object(receipt),
|
|
136
|
+
signature=None,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_receipt_set(
|
|
141
|
+
*,
|
|
142
|
+
run_id: str,
|
|
143
|
+
variant: ExperimentVariant,
|
|
144
|
+
experiment_manifest_digest: Digest,
|
|
145
|
+
signed_receipts: Sequence[ObjectEnvelope[EpisodeReceipt]],
|
|
146
|
+
ordered_task_hashes: Sequence[Digest],
|
|
147
|
+
) -> ReceiptSetManifest:
|
|
148
|
+
"""Order receipts by TasksetLock membership and build the commitment."""
|
|
149
|
+
committed = [validate_digest(value) for value in ordered_task_hashes]
|
|
150
|
+
by_task = _envelopes_by_task(
|
|
151
|
+
signed_receipts,
|
|
152
|
+
committed=committed,
|
|
153
|
+
run_id=run_id,
|
|
154
|
+
variant=variant,
|
|
155
|
+
experiment_manifest_digest=experiment_manifest_digest,
|
|
156
|
+
)
|
|
157
|
+
return ReceiptSetManifest(
|
|
158
|
+
schema_version=RECEIPT_SET_SCHEMA_VERSION,
|
|
159
|
+
run_id=run_id,
|
|
160
|
+
variant=variant,
|
|
161
|
+
experiment_manifest_digest=validate_digest(experiment_manifest_digest),
|
|
162
|
+
ordered_receipt_digests=[
|
|
163
|
+
by_task[task_hash].payload_digest for task_hash in committed
|
|
164
|
+
],
|
|
165
|
+
task_membership_digest=membership_digest(committed),
|
|
166
|
+
receipt_count=len(committed),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def verify_receipt_set(
|
|
171
|
+
*,
|
|
172
|
+
manifest: ReceiptSetManifest,
|
|
173
|
+
signed_receipts: Sequence[ObjectEnvelope[EpisodeReceipt]],
|
|
174
|
+
ordered_task_hashes: Sequence[Digest],
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Verify order, count, payload digests and task membership, or refuse.
|
|
177
|
+
|
|
178
|
+
Everything is recomputed from the receipts. A recorded value is never
|
|
179
|
+
compared against itself, which is the only way this can detect a receipt
|
|
180
|
+
that was edited after the manifest was written.
|
|
181
|
+
"""
|
|
182
|
+
rebuilt = build_receipt_set(
|
|
183
|
+
run_id=manifest.run_id,
|
|
184
|
+
variant=manifest.variant,
|
|
185
|
+
experiment_manifest_digest=manifest.experiment_manifest_digest,
|
|
186
|
+
signed_receipts=signed_receipts,
|
|
187
|
+
ordered_task_hashes=ordered_task_hashes,
|
|
188
|
+
)
|
|
189
|
+
if rebuilt == manifest:
|
|
190
|
+
return
|
|
191
|
+
raise VerificationError(
|
|
192
|
+
"this receipt set does not commit to the receipts it was given; one of "
|
|
193
|
+
"them, or the manifest itself, changed after it was written",
|
|
194
|
+
code=RECEIPT_SET_INVALID,
|
|
195
|
+
details={
|
|
196
|
+
"run_id": manifest.run_id,
|
|
197
|
+
"variant": manifest.variant.value,
|
|
198
|
+
"recorded": _digest_detail(manifest.ordered_receipt_digests),
|
|
199
|
+
"recomputed": _digest_detail(rebuilt.ordered_receipt_digests),
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def receipt_set_path(run_root: Path, variant: ExperimentVariant) -> Path:
|
|
205
|
+
"""Return where one variant's receipt-set manifest lives inside a run."""
|
|
206
|
+
return run_root / _RECEIPTS_DIRECTORY / f"{variant.value}{_RECEIPT_SET_SUFFIX}"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def write_receipt_set(manifest: ReceiptSetManifest, path: Path) -> ArtifactRef:
|
|
210
|
+
"""Write canonical JSON and return the digest reference to it.
|
|
211
|
+
|
|
212
|
+
Canonical bytes rather than the readable spelling, so that the digest of
|
|
213
|
+
the file and the digest of the object are the same number and a reader can
|
|
214
|
+
check the commitment by hashing what is on disk.
|
|
215
|
+
"""
|
|
216
|
+
data = canonical_json_bytes(manifest)
|
|
217
|
+
ensure_private_directory(path.parent)
|
|
218
|
+
atomic_write_bytes(path, data)
|
|
219
|
+
return ArtifactRef(
|
|
220
|
+
digest=sha256_digest_bytes(data),
|
|
221
|
+
media_type=RECEIPT_SET_MEDIA_TYPE,
|
|
222
|
+
size=len(data),
|
|
223
|
+
relative_path=None,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Joining
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _envelopes_by_task(
|
|
233
|
+
signed_receipts: Sequence[ObjectEnvelope[EpisodeReceipt]],
|
|
234
|
+
*,
|
|
235
|
+
committed: Sequence[Digest],
|
|
236
|
+
run_id: str,
|
|
237
|
+
variant: ExperimentVariant,
|
|
238
|
+
experiment_manifest_digest: Digest,
|
|
239
|
+
) -> dict[Digest, ObjectEnvelope[EpisodeReceipt]]:
|
|
240
|
+
"""Index one variant's sealed receipts by the task each one scored."""
|
|
241
|
+
if len(signed_receipts) != len(committed):
|
|
242
|
+
raise VerificationError(
|
|
243
|
+
f"the {variant.value} receipt set was given {len(signed_receipts)} "
|
|
244
|
+
f"receipts for {len(committed)} committed tasks",
|
|
245
|
+
code=EPISODE_COUNT_MISMATCH,
|
|
246
|
+
details={
|
|
247
|
+
"variant": variant.value,
|
|
248
|
+
"receipts": len(signed_receipts),
|
|
249
|
+
"committed": len(committed),
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
by_task: dict[Digest, ObjectEnvelope[EpisodeReceipt]] = {}
|
|
254
|
+
for envelope in signed_receipts:
|
|
255
|
+
receipt = envelope.payload
|
|
256
|
+
_require_sealed(envelope)
|
|
257
|
+
_require_belongs(
|
|
258
|
+
receipt,
|
|
259
|
+
run_id=run_id,
|
|
260
|
+
variant=variant,
|
|
261
|
+
experiment_manifest_digest=experiment_manifest_digest,
|
|
262
|
+
)
|
|
263
|
+
task_hash = validate_digest(receipt.task_hash)
|
|
264
|
+
if task_hash in by_task:
|
|
265
|
+
raise VerificationError(
|
|
266
|
+
f"two {variant.value} receipts claim task {task_hash}",
|
|
267
|
+
code=TASK_MEMBERSHIP_MISMATCH,
|
|
268
|
+
details={"variant": variant.value, "task_hash": task_hash},
|
|
269
|
+
)
|
|
270
|
+
by_task[task_hash] = envelope
|
|
271
|
+
|
|
272
|
+
missing: list[JsonValue] = [value for value in committed if value not in by_task]
|
|
273
|
+
unexpected: list[JsonValue] = [
|
|
274
|
+
value for value in sorted(set(by_task) - set(committed))
|
|
275
|
+
]
|
|
276
|
+
if missing or unexpected:
|
|
277
|
+
raise VerificationError(
|
|
278
|
+
f"the {variant.value} receipts do not cover the tasks the Campaign "
|
|
279
|
+
"commits to",
|
|
280
|
+
code=TASK_MEMBERSHIP_MISMATCH,
|
|
281
|
+
details={
|
|
282
|
+
"variant": variant.value,
|
|
283
|
+
"missing": missing,
|
|
284
|
+
"unexpected": unexpected,
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
return by_task
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _require_sealed(envelope: ObjectEnvelope[EpisodeReceipt]) -> None:
|
|
291
|
+
"""Require an envelope's digest to describe the payload it carries."""
|
|
292
|
+
computed = digest_object(envelope.payload)
|
|
293
|
+
if computed == envelope.payload_digest:
|
|
294
|
+
return
|
|
295
|
+
raise VerificationError(
|
|
296
|
+
"a receipt no longer matches the digest it was sealed under, so it was "
|
|
297
|
+
"changed after it was written",
|
|
298
|
+
code=RECEIPT_SET_INVALID,
|
|
299
|
+
details={
|
|
300
|
+
"receipt_id": envelope.payload.id,
|
|
301
|
+
"sealed": envelope.payload_digest,
|
|
302
|
+
"computed": computed,
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _require_belongs(
|
|
308
|
+
receipt: EpisodeReceipt,
|
|
309
|
+
*,
|
|
310
|
+
run_id: str,
|
|
311
|
+
variant: ExperimentVariant,
|
|
312
|
+
experiment_manifest_digest: Digest,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Require a receipt to belong to the run, variant and manifest committed to."""
|
|
315
|
+
if (
|
|
316
|
+
receipt.run_id == run_id
|
|
317
|
+
and receipt.variant is variant
|
|
318
|
+
and receipt.experiment_manifest_digest == experiment_manifest_digest
|
|
319
|
+
):
|
|
320
|
+
return
|
|
321
|
+
raise VerificationError(
|
|
322
|
+
"a receipt from a different run, variant or experiment manifest cannot "
|
|
323
|
+
"join this receipt set",
|
|
324
|
+
code=RECEIPT_SET_INVALID,
|
|
325
|
+
details={
|
|
326
|
+
"receipt_id": receipt.id,
|
|
327
|
+
"run_id": receipt.run_id,
|
|
328
|
+
"variant": receipt.variant.value,
|
|
329
|
+
"experiment_manifest_digest": receipt.experiment_manifest_digest,
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _digest_detail(digests: Sequence[Digest]) -> list[JsonValue]:
|
|
335
|
+
"""Return a digest list in the shape typed error details carry."""
|
|
336
|
+
return [value for value in digests]
|