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.
Files changed (174) hide show
  1. techtree/__init__.py +35 -0
  2. techtree/__main__.py +14 -0
  3. techtree/canonical.py +239 -0
  4. techtree/catalog/__init__.py +25 -0
  5. techtree/catalog/repository.py +400 -0
  6. techtree/catalog/service.py +419 -0
  7. techtree/cli/__init__.py +1 -0
  8. techtree/cli/app.py +416 -0
  9. techtree/cli/commands/__init__.py +1 -0
  10. techtree/cli/commands/climb.py +1223 -0
  11. techtree/cli/commands/doctor.py +147 -0
  12. techtree/cli/commands/engine.py +207 -0
  13. techtree/cli/commands/proof.py +556 -0
  14. techtree/cli/commands/publish.py +447 -0
  15. techtree/cli/commands/release.py +303 -0
  16. techtree/cli/commands/run.py +1067 -0
  17. techtree/cli/commands/setup.py +181 -0
  18. techtree/cli/commands/skill.py +221 -0
  19. techtree/cli/commands/uplift.py +698 -0
  20. techtree/cli/commands/withdraw.py +212 -0
  21. techtree/cli/confirm.py +47 -0
  22. techtree/cli/context.py +96 -0
  23. techtree/cli/invoke.py +220 -0
  24. techtree/cli/output.py +280 -0
  25. techtree/constants.py +138 -0
  26. techtree/crypto.py +128 -0
  27. techtree/doctor/__init__.py +1 -0
  28. techtree/doctor/checks.py +675 -0
  29. techtree/doctor/execution_checks.py +435 -0
  30. techtree/doctor/service.py +326 -0
  31. techtree/drafts/__init__.py +32 -0
  32. techtree/drafts/source.py +146 -0
  33. techtree/drafts/store.py +992 -0
  34. techtree/engines/__init__.py +1 -0
  35. techtree/engines/bundle.py +251 -0
  36. techtree/engines/installer.py +679 -0
  37. techtree/engines/registry.py +235 -0
  38. techtree/engines/runner.py +170 -0
  39. techtree/errors.py +262 -0
  40. techtree/fs.py +234 -0
  41. techtree/harness.py +108 -0
  42. techtree/identity/__init__.py +41 -0
  43. techtree/identity/models.py +113 -0
  44. techtree/identity/service.py +199 -0
  45. techtree/identity/store.py +263 -0
  46. techtree/ids.py +85 -0
  47. techtree/manifests/__init__.py +39 -0
  48. techtree/manifests/builder.py +433 -0
  49. techtree/manifests/compare.py +376 -0
  50. techtree/models/__init__.py +282 -0
  51. techtree/models/base.py +201 -0
  52. techtree/models/campaign.py +484 -0
  53. techtree/models/catalog.py +227 -0
  54. techtree/models/cli.py +151 -0
  55. techtree/models/climb.py +254 -0
  56. techtree/models/data_policy.py +130 -0
  57. techtree/models/engine.py +156 -0
  58. techtree/models/episode_receipt.py +130 -0
  59. techtree/models/evaluation_backend.py +113 -0
  60. techtree/models/experiment.py +154 -0
  61. techtree/models/run.py +214 -0
  62. techtree/models/skill.py +156 -0
  63. techtree/models/uplift_report.py +158 -0
  64. techtree/models/validation.py +299 -0
  65. techtree/paths.py +116 -0
  66. techtree/presentation/__init__.py +31 -0
  67. techtree/presentation/build.py +1242 -0
  68. techtree/presentation/compact.py +246 -0
  69. techtree/presentation/evidence.py +169 -0
  70. techtree/presentation/models.py +358 -0
  71. techtree/presentation/rich.py +312 -0
  72. techtree/presentation/sanitize.py +156 -0
  73. techtree/publication/__init__.py +44 -0
  74. techtree/publication/address.py +180 -0
  75. techtree/publication/coordinates.py +26 -0
  76. techtree/publication/journal.py +212 -0
  77. techtree/publication/keccak.py +183 -0
  78. techtree/publication/models.py +209 -0
  79. techtree/publication/offer.py +35 -0
  80. techtree/publication/service.py +618 -0
  81. techtree/publication/transport.py +296 -0
  82. techtree/publication/verify.py +242 -0
  83. techtree/publication/withdraw.py +156 -0
  84. techtree/py.typed +0 -0
  85. techtree/receipts/__init__.py +52 -0
  86. techtree/receipts/bundle.py +578 -0
  87. techtree/receipts/compare.py +1065 -0
  88. techtree/receipts/episode.py +672 -0
  89. techtree/receipts/execution.py +630 -0
  90. techtree/receipts/observed.py +474 -0
  91. techtree/receipts/set.py +336 -0
  92. techtree/receipts/uplift.py +655 -0
  93. techtree/receipts/verify.py +1055 -0
  94. techtree/release/__init__.py +9 -0
  95. techtree/release/bootstrap.py +509 -0
  96. techtree/release/checks.py +376 -0
  97. techtree/release/document.py +125 -0
  98. techtree/release/generate.py +221 -0
  99. techtree/release/models.py +293 -0
  100. techtree/release/provenance.py +109 -0
  101. techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
  102. techtree/resources/catalog/catalog.json +32 -0
  103. techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
  104. techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
  105. techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
  106. techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
  107. techtree/resources/engines/default/engine.json +20 -0
  108. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
  109. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
  110. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
  111. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
  112. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
  113. techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
  114. techtree/resources/engines/default/pyproject.toml +23 -0
  115. techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
  116. techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
  117. techtree/resources/engines/default/tools/normalize_validation.py +222 -0
  118. techtree/resources/engines/default/uv.lock +1758 -0
  119. techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
  120. techtree/resources/release/build-provenance.json +4 -0
  121. techtree/resources/release/release-core.json +24 -0
  122. techtree/runs/__init__.py +31 -0
  123. techtree/runs/artifacts.py +750 -0
  124. techtree/runs/child_registry.py +228 -0
  125. techtree/runs/events.py +478 -0
  126. techtree/runs/executor.py +140 -0
  127. techtree/runs/fake.py +741 -0
  128. techtree/runs/launcher.py +253 -0
  129. techtree/runs/machine.py +489 -0
  130. techtree/runs/real.py +789 -0
  131. techtree/runs/service.py +616 -0
  132. techtree/runs/store.py +555 -0
  133. techtree/runs/validation.py +259 -0
  134. techtree/runs/variants.py +684 -0
  135. techtree/settings.py +143 -0
  136. techtree/skills/__init__.py +14 -0
  137. techtree/skills/archive.py +282 -0
  138. techtree/skills/policy.py +62 -0
  139. techtree/skills/scanner.py +394 -0
  140. techtree/skills/service.py +752 -0
  141. techtree/skills/starter.py +434 -0
  142. techtree/tasksets/__init__.py +1 -0
  143. techtree/tasksets/membership.py +269 -0
  144. techtree/tasksets/provider.py +207 -0
  145. techtree/tasksets/resolver.py +311 -0
  146. techtree/tasksets/service.py +484 -0
  147. techtree/tasksets/verifiers_cli.py +538 -0
  148. techtree/uplift/__init__.py +20 -0
  149. techtree/uplift/context.py +544 -0
  150. techtree/uplift/derive.py +203 -0
  151. techtree/uplift/public_tasks.py +151 -0
  152. techtree/uplift/service.py +719 -0
  153. techtree/uplift/source.py +160 -0
  154. techtree/verifiers/__init__.py +31 -0
  155. techtree/verifiers/budget.py +219 -0
  156. techtree/verifiers/child.py +633 -0
  157. techtree/verifiers/compiler.py +432 -0
  158. techtree/verifiers/config.py +365 -0
  159. techtree/verifiers/credentials.py +321 -0
  160. techtree/verifiers/image.py +126 -0
  161. techtree/verifiers/models.py +527 -0
  162. techtree/verifiers/outputs.py +368 -0
  163. techtree/verifiers/progress.py +192 -0
  164. techtree/verifiers/supervisor.py +341 -0
  165. techtree/verifiers/verify.py +782 -0
  166. techtree/version.py +39 -0
  167. techtree/worker/__init__.py +18 -0
  168. techtree/worker/execute.py +487 -0
  169. techtree/worker/main.py +57 -0
  170. techtree-0.1.0.dist-info/METADATA +344 -0
  171. techtree-0.1.0.dist-info/RECORD +174 -0
  172. techtree-0.1.0.dist-info/WHEEL +4 -0
  173. techtree-0.1.0.dist-info/entry_points.txt +3 -0
  174. techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -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]