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,259 @@
1
+ """Where a run's taskset validation comes from. Spec PR8 §8.4.
2
+
3
+ An executor has to know that the taskset it is about to score is the taskset
4
+ the Campaign committed to, and that somebody mechanically validated it. Who
5
+ answers that question changes over the life of the product: today it is the
6
+ publisher's own receipt, snapshotted into the draft and re-checked offline;
7
+ later it is a local Verifiers run that recomputes the same receipt on the
8
+ participant's machine.
9
+
10
+ This module is the seam between the two. The executor depends on
11
+ :class:`TasksetValidationProvider` and nothing else, so the later change
12
+ replaces one object at wiring time and touches no run lifecycle code.
13
+
14
+ :class:`PublisherFixtureValidationProvider` is the development answer. It is
15
+ careful about what it claims: it verifies the snapshotted publisher commitment
16
+ and says so — ``source = publisher_fixture``, ``execution_record = None`` —
17
+ rather than presenting a re-read commitment as a validation this machine
18
+ performed. Nothing downstream is entitled to treat it as one, which is
19
+ consistent with the fake executor's reports being development-only throughout.
20
+
21
+ The lock the receipt was issued under is not shipped in the public catalog;
22
+ only its digest is. The provider therefore *derives* the lock from what the
23
+ draft does carry — the Campaign's taskset reference and committed membership,
24
+ and the receipt's engine digest — and requires the derived lock to digest to
25
+ exactly the value the receipt names. A derivation that does not reproduce the
26
+ committed digest is a failure, never a warning.
27
+
28
+ :class:`UnconfiguredTasksetValidationProvider` is the fail-closed default for
29
+ any wiring that has no provider yet. It answers every request with
30
+ ``taskset_validation_provider_unavailable``.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from dataclasses import dataclass
36
+ from enum import StrEnum
37
+ from typing import Final, NoReturn, Protocol
38
+
39
+ from pydantic import ValidationError as PydanticValidationError
40
+
41
+ from techtree.canonical import digest_object, to_json_value
42
+ from techtree.constants import TASKSET_LOCK_SCHEMA_VERSION
43
+ from techtree.errors import PrerequisiteError, VerificationError
44
+ from techtree.models.base import JsonValue
45
+ from techtree.models.validation import (
46
+ TasksetLock,
47
+ TasksetValidationReceipt,
48
+ ValidationExecutionRecord,
49
+ )
50
+ from techtree.runs.artifacts import (
51
+ VALIDATION_MARKER_SCHEMA_VERSION,
52
+ RunInputBundle,
53
+ )
54
+
55
+ __all__ = [
56
+ "TASKSET_VALIDATION_INVALID",
57
+ "TASKSET_VALIDATION_PROVIDER_UNAVAILABLE",
58
+ "PublisherFixtureValidationProvider",
59
+ "TasksetValidationOutcome",
60
+ "TasksetValidationProvider",
61
+ "TasksetValidationSource",
62
+ "UnconfiguredTasksetValidationProvider",
63
+ "derive_taskset_lock",
64
+ ]
65
+
66
+ #: Stable error codes. Spec PR8 §8.16.
67
+ TASKSET_VALIDATION_INVALID: Final = "taskset_validation_invalid"
68
+ TASKSET_VALIDATION_PROVIDER_UNAVAILABLE: Final = (
69
+ "taskset_validation_provider_unavailable"
70
+ )
71
+
72
+
73
+ class TasksetValidationSource(StrEnum):
74
+ """Where a validation outcome came from."""
75
+
76
+ PUBLISHER_FIXTURE = "publisher_fixture"
77
+ LOCAL_VERIFIERS = "local_verifiers"
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class TasksetValidationOutcome:
82
+ """The validation artifacts an executor needs, and their provenance."""
83
+
84
+ lock: TasksetLock
85
+ receipt: TasksetValidationReceipt
86
+ execution_record: ValidationExecutionRecord | None
87
+ source: TasksetValidationSource
88
+
89
+ def marker_document(self) -> dict[str, JsonValue]:
90
+ """Return the JSON projection persisted beside the run.
91
+
92
+ The source is carried explicitly so that a reader of a run directory
93
+ can tell a re-checked publisher commitment from a locally executed
94
+ validation without inferring it from which fields are absent.
95
+ """
96
+ return {
97
+ "schema_version": VALIDATION_MARKER_SCHEMA_VERSION,
98
+ "source": self.source.value,
99
+ "lock": to_json_value(self.lock),
100
+ "lock_digest": digest_object(self.lock),
101
+ "receipt": to_json_value(self.receipt),
102
+ "receipt_digest": digest_object(self.receipt),
103
+ "execution_record": (
104
+ None
105
+ if self.execution_record is None
106
+ else to_json_value(self.execution_record)
107
+ ),
108
+ }
109
+
110
+
111
+ class TasksetValidationProvider(Protocol):
112
+ """Answers what a run may assume about its taskset's validation."""
113
+
114
+ def validate(
115
+ self,
116
+ *,
117
+ run_id: str,
118
+ inputs: RunInputBundle,
119
+ ) -> TasksetValidationOutcome:
120
+ """Return the validation artifacts required by the executor."""
121
+ ...
122
+
123
+
124
+ class PublisherFixtureValidationProvider:
125
+ """Re-checks the snapshotted publisher commitment. Development only.
126
+
127
+ It does not claim to have rerun validation locally, and it never
128
+ fabricates a :class:`~techtree.models.validation.ValidationExecutionRecord`
129
+ for work that did not happen.
130
+ """
131
+
132
+ def validate(
133
+ self,
134
+ *,
135
+ run_id: str,
136
+ inputs: RunInputBundle,
137
+ ) -> TasksetValidationOutcome:
138
+ """Verify the publisher's receipt and return it as this run's outcome."""
139
+ campaign = inputs.campaign
140
+ source = inputs.source
141
+ receipt = source.publisher_validation
142
+
143
+ _require(
144
+ source.publisher_validation_digest
145
+ == campaign.taskset.validation_receipt_digest,
146
+ "the publisher validation receipt this run owns is not the one the "
147
+ "Campaign commits to",
148
+ run_id,
149
+ )
150
+
151
+ reference = receipt.normalized_evidence
152
+ _require(
153
+ reference is not None
154
+ and reference.digest == digest_object(inputs.validation_evidence),
155
+ "the normalized evidence this run owns is not what the publisher's "
156
+ "receipt was issued from",
157
+ run_id,
158
+ )
159
+
160
+ _require(
161
+ receipt.status == "valid",
162
+ f"the publisher's validation receipt reports {receipt.status}, so "
163
+ "this taskset is not fit to be scored",
164
+ run_id,
165
+ status=receipt.status,
166
+ )
167
+
168
+ lock = derive_taskset_lock(run_id, inputs)
169
+ _require(
170
+ digest_object(lock) == receipt.taskset_lock_digest,
171
+ "the taskset lock derived from this Campaign is not the lock the "
172
+ "publisher's receipt was issued under",
173
+ run_id,
174
+ )
175
+ _require(
176
+ lock.ordered_task_hashes
177
+ == list(campaign.taskset.membership.ordered_task_hashes)
178
+ and lock.membership_digest == campaign.taskset.membership.membership_digest
179
+ and lock.task_count == campaign.taskset.selection.num_tasks,
180
+ "the locked task membership is not the membership the Campaign commits to",
181
+ run_id,
182
+ )
183
+ _require(
184
+ inputs.validation_evidence.taskset_lock_digest
185
+ == receipt.taskset_lock_digest,
186
+ "the normalized evidence was produced under a different taskset "
187
+ "lock than the receipt",
188
+ run_id,
189
+ )
190
+
191
+ return TasksetValidationOutcome(
192
+ lock=lock,
193
+ receipt=receipt,
194
+ execution_record=None,
195
+ source=TasksetValidationSource.PUBLISHER_FIXTURE,
196
+ )
197
+
198
+
199
+ class UnconfiguredTasksetValidationProvider:
200
+ """The fail-closed provider for a build with no validation source wired."""
201
+
202
+ def validate(
203
+ self,
204
+ *,
205
+ run_id: str,
206
+ inputs: RunInputBundle,
207
+ ) -> NoReturn:
208
+ """Refuse to let a run proceed without a validation source."""
209
+ raise PrerequisiteError(
210
+ "this build has no taskset validation source configured, so no run "
211
+ "may claim its taskset was validated",
212
+ code=TASKSET_VALIDATION_PROVIDER_UNAVAILABLE,
213
+ details={"run_id": run_id},
214
+ )
215
+
216
+
217
+ def derive_taskset_lock(run_id: str, inputs: RunInputBundle) -> TasksetLock:
218
+ """Rebuild the lock the publisher's receipt was issued under.
219
+
220
+ Every field comes from something the run already owns and has verified:
221
+ the taskset reference and committed membership from the Campaign, and the
222
+ engine digest from the receipt. The caller checks the result against the
223
+ digest the receipt names, which is what makes the derivation a proof
224
+ rather than a guess.
225
+ """
226
+ campaign = inputs.campaign
227
+ receipt = inputs.source.publisher_validation
228
+ membership = campaign.taskset.membership
229
+
230
+ try:
231
+ return TasksetLock(
232
+ schema_version=TASKSET_LOCK_SCHEMA_VERSION,
233
+ taskset_ref=campaign.taskset.ref,
234
+ engine_digest=receipt.engine_digest,
235
+ resolved_package_digest=campaign.taskset.ref.package.digest,
236
+ ordered_task_hashes=list(membership.ordered_task_hashes),
237
+ membership_digest=membership.membership_digest,
238
+ task_count=len(membership.ordered_task_hashes),
239
+ )
240
+ except PydanticValidationError as error:
241
+ raise VerificationError(
242
+ "no taskset lock can be derived from this Campaign: "
243
+ f"{error.errors()[0]['msg']}",
244
+ code=TASKSET_VALIDATION_INVALID,
245
+ details={"run_id": run_id},
246
+ ) from error
247
+
248
+
249
+ def _require(condition: bool, message: str, run_id: str, **details: str) -> None:
250
+ """Raise a typed validation failure unless the condition holds."""
251
+ if condition:
252
+ return
253
+ reported: dict[str, JsonValue] = {"run_id": run_id}
254
+ reported.update(details)
255
+ raise VerificationError(
256
+ message,
257
+ code=TASKSET_VALIDATION_INVALID,
258
+ details=reported,
259
+ )