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,376 @@
1
+ """Proving that two variants differ only where permitted. Spec PR6 §6.5.
2
+
3
+ An uplift number is worth exactly as much as the claim that the two runs behind
4
+ it were the same experiment apart from the candidate skill. This module is that
5
+ claim, computed rather than asserted.
6
+
7
+ The check runs in two stages, and the order matters.
8
+
9
+ First the structural invariants are required outright: same Campaign, same
10
+ improvement program, same public context, same DataPolicy, same OutcomeContract,
11
+ same evaluation backend, the right variant on each side, and the skill counts
12
+ the Campaign's mutation kind requires — no skill on the baseline and exactly one
13
+ on the candidate for an insertion, one differing skill on each side for a
14
+ replacement. These are stated as invariants rather
15
+ than discovered as differences because a diff can only say *that* two documents
16
+ disagree — it cannot say which disagreements were structurally impossible.
17
+
18
+ Then the two configurations are canonicalized to JSON and walked to their
19
+ leaves. A leaf that disagrees is reported as an RFC 6901 JSON Pointer, and the
20
+ pointers are checked against the Campaign's mutation contract, which in v0.1
21
+ permits exactly one root: ``/agents/subject/harness/skills``. A difference
22
+ anywhere else — a different model, a different image, a different task list, a
23
+ different ``use_bundled_skill`` — is a violation, and so is *no* difference at
24
+ all, because two identical configurations measure nothing.
25
+
26
+ Containment is compared segment by segment, so ``skills_extra`` is not inside
27
+ ``skills``. A prefix test written with ``startswith`` alone would admit exactly
28
+ the field an attacker would add.
29
+
30
+ Two further choices are worth stating.
31
+
32
+ *The diff is over canonical JSON, not over Python objects.* Comparing models
33
+ would compare whatever Python decided equality means for each field type. The
34
+ protocol's own byte-level form is what two independent implementations agree
35
+ on, so it is what is compared.
36
+
37
+ *The invariants are checked here even where a model already enforces them.*
38
+ :class:`~techtree.models.experiment.ExperimentManifest` refuses a baseline with
39
+ a skill. This module refuses one again. The model protects documents that are
40
+ built; this function protects documents that are loaded, and the second is the
41
+ case that matters when a comparison is being audited rather than created.
42
+
43
+ :func:`compare_manifests` never raises: it reports what it found.
44
+ :func:`assert_controlled_comparison` is the separate step that turns an
45
+ uncontrolled finding into a refusal, so a caller can inspect a bad comparison
46
+ before deciding what to do about it.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import json
52
+ from typing import Final
53
+
54
+ from techtree.canonical import canonical_json_bytes
55
+ from techtree.errors import VerificationError
56
+ from techtree.models.base import JsonValue
57
+ from techtree.models.campaign import MutationContract, MutationKind
58
+ from techtree.models.experiment import (
59
+ ExperimentManifest,
60
+ ExperimentVariant,
61
+ JsonDifference,
62
+ ManifestComparison,
63
+ )
64
+
65
+ __all__ = [
66
+ "MANIFEST_COMPARISON_INVALID",
67
+ "POINTER_SEPARATOR",
68
+ "assert_controlled_comparison",
69
+ "compare_manifests",
70
+ "diff_values",
71
+ "json_pointer_escape",
72
+ "pointer_is_within",
73
+ ]
74
+
75
+ #: RFC 6901 separates reference tokens with a solidus and gives it no other
76
+ #: meaning, which is why a token containing one has to be escaped.
77
+ POINTER_SEPARATOR: Final = "/"
78
+
79
+ #: The one error code an uncontrolled comparison reports. Spec PR6 §6.10.
80
+ MANIFEST_COMPARISON_INVALID: Final = "manifest_comparison_invalid"
81
+
82
+
83
+ def json_pointer_escape(segment: str) -> str:
84
+ """Escape ``~`` as ``~0`` and ``/`` as ``~1``.
85
+
86
+ In that order: escaping the solidus first would turn its replacement's
87
+ tilde into a second escape.
88
+ """
89
+ return segment.replace("~", "~0").replace(POINTER_SEPARATOR, "~1")
90
+
91
+
92
+ def diff_values(
93
+ baseline: JsonValue,
94
+ candidate: JsonValue,
95
+ *,
96
+ pointer: str = "",
97
+ ) -> list[JsonDifference]:
98
+ """Recursively compare JSON values and return the leaves that disagree.
99
+
100
+ The walk descends only where both sides are the same kind of container.
101
+ Where they are not — an object against an array, a string against a number,
102
+ a present element against a missing one — the disagreement is reported at
103
+ that pointer and not decomposed further, because there is no shared
104
+ structure left to attribute it to.
105
+
106
+ Object keys are visited in sorted order and array elements in index order,
107
+ so the same pair of documents always produces the same list.
108
+ """
109
+ if isinstance(baseline, dict) and isinstance(candidate, dict):
110
+ return _diff_objects(baseline, candidate, pointer)
111
+ if isinstance(baseline, list) and isinstance(candidate, list):
112
+ return _diff_arrays(baseline, candidate, pointer)
113
+ if _same_scalar(baseline, candidate):
114
+ return []
115
+ return [_difference(pointer, baseline, candidate)]
116
+
117
+
118
+ def pointer_is_within(pointer: str, allowed_root: str) -> bool:
119
+ """Return whether a pointer is the allowed root or descends from it.
120
+
121
+ ``/agents/subject/harness/skills_extra`` is not within
122
+ ``/agents/subject/harness/skills``: containment is a boundary between
123
+ reference tokens, not a string prefix.
124
+ """
125
+ return pointer == allowed_root or pointer.startswith(
126
+ f"{allowed_root}{POINTER_SEPARATOR}"
127
+ )
128
+
129
+
130
+ def compare_manifests(
131
+ baseline: ExperimentManifest,
132
+ candidate: ExperimentManifest,
133
+ mutation: MutationContract,
134
+ ) -> ManifestComparison:
135
+ """Compare two variants' configurations and report whether it is controlled."""
136
+ allowed = list(mutation.allowed_differences)
137
+
138
+ violations = [
139
+ *_variant_violations(baseline, candidate),
140
+ *_shared_field_violations(baseline, candidate),
141
+ *_skill_count_violations(baseline, candidate, mutation),
142
+ ]
143
+
144
+ differences = sorted(
145
+ diff_values(
146
+ _configuration_json(baseline),
147
+ _configuration_json(candidate),
148
+ ),
149
+ key=lambda difference: difference.pointer,
150
+ )
151
+
152
+ if not differences:
153
+ violations.append(
154
+ "the candidate configuration is identical to the baseline, so the "
155
+ "pair measures nothing"
156
+ )
157
+ violations.extend(_pointer_violations(differences, allowed))
158
+
159
+ return ManifestComparison(
160
+ baseline_configuration_digest=baseline.configuration_digest,
161
+ candidate_configuration_digest=candidate.configuration_digest,
162
+ differences=differences,
163
+ allowed_differences=allowed,
164
+ controlled=not violations,
165
+ violations=violations,
166
+ )
167
+
168
+
169
+ def assert_controlled_comparison(comparison: ManifestComparison) -> None:
170
+ """Raise :class:`VerificationError` when the comparison is not controlled."""
171
+ if comparison.controlled:
172
+ return
173
+ raise VerificationError(
174
+ "the candidate differs from the baseline somewhere the Campaign does "
175
+ "not permit, so the comparison would not measure the skill: "
176
+ + "; ".join(comparison.violations),
177
+ code=MANIFEST_COMPARISON_INVALID,
178
+ details={
179
+ "violations": list(comparison.violations),
180
+ "differences": [
181
+ difference.pointer for difference in comparison.differences
182
+ ],
183
+ "allowed_differences": list(comparison.allowed_differences),
184
+ },
185
+ )
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # The diff
190
+ # ---------------------------------------------------------------------------
191
+
192
+
193
+ def _diff_objects(
194
+ baseline: dict[str, JsonValue],
195
+ candidate: dict[str, JsonValue],
196
+ pointer: str,
197
+ ) -> list[JsonDifference]:
198
+ differences: list[JsonDifference] = []
199
+ for key in sorted(baseline.keys() | candidate.keys()):
200
+ child = f"{pointer}{POINTER_SEPARATOR}{json_pointer_escape(key)}"
201
+ if key not in baseline:
202
+ differences.append(_difference(child, None, candidate[key]))
203
+ elif key not in candidate:
204
+ differences.append(_difference(child, baseline[key], None))
205
+ else:
206
+ differences.extend(
207
+ diff_values(baseline[key], candidate[key], pointer=child)
208
+ )
209
+ return differences
210
+
211
+
212
+ def _diff_arrays(
213
+ baseline: list[JsonValue],
214
+ candidate: list[JsonValue],
215
+ pointer: str,
216
+ ) -> list[JsonDifference]:
217
+ differences: list[JsonDifference] = []
218
+ for index in range(max(len(baseline), len(candidate))):
219
+ child = f"{pointer}{POINTER_SEPARATOR}{index}"
220
+ if index >= len(baseline):
221
+ differences.append(_difference(child, None, candidate[index]))
222
+ elif index >= len(candidate):
223
+ differences.append(_difference(child, baseline[index], None))
224
+ else:
225
+ differences.extend(
226
+ diff_values(baseline[index], candidate[index], pointer=child)
227
+ )
228
+ return differences
229
+
230
+
231
+ def _same_scalar(baseline: JsonValue, candidate: JsonValue) -> bool:
232
+ """Compare two JSON scalars without letting Python widen the question.
233
+
234
+ ``True == 1`` and ``1 == 1.0`` in Python, and neither is true of the JSON
235
+ documents these came from, so the type has to agree before the value does.
236
+ """
237
+ if isinstance(baseline, bool) != isinstance(candidate, bool):
238
+ return False
239
+ if isinstance(baseline, int | float) != isinstance(candidate, int | float):
240
+ return False
241
+ return baseline == candidate
242
+
243
+
244
+ def _difference(
245
+ pointer: str, baseline: JsonValue, candidate: JsonValue
246
+ ) -> JsonDifference:
247
+ # The document root is the empty pointer, which the model cannot hold; a
248
+ # whole-configuration disagreement is reported at ``/`` instead.
249
+ return JsonDifference(
250
+ pointer=pointer or POINTER_SEPARATOR,
251
+ baseline=baseline,
252
+ candidate=candidate,
253
+ )
254
+
255
+
256
+ def _configuration_json(manifest: ExperimentManifest) -> JsonValue:
257
+ """Return the canonical JSON form of one manifest's configuration."""
258
+ decoded: JsonValue = json.loads(
259
+ canonical_json_bytes(manifest.configuration).decode("utf-8")
260
+ )
261
+ return decoded
262
+
263
+
264
+ # ---------------------------------------------------------------------------
265
+ # The invariants
266
+ # ---------------------------------------------------------------------------
267
+
268
+
269
+ def _variant_violations(
270
+ baseline: ExperimentManifest, candidate: ExperimentManifest
271
+ ) -> list[str]:
272
+ violations: list[str] = []
273
+ if baseline.variant is not ExperimentVariant.BASELINE:
274
+ violations.append(
275
+ f"the baseline side of the comparison is a {baseline.variant.value} "
276
+ "manifest"
277
+ )
278
+ if candidate.variant is not ExperimentVariant.CANDIDATE:
279
+ violations.append(
280
+ f"the candidate side of the comparison is a {candidate.variant.value} "
281
+ "manifest"
282
+ )
283
+ return violations
284
+
285
+
286
+ def _shared_field_violations(
287
+ baseline: ExperimentManifest, candidate: ExperimentManifest
288
+ ) -> list[str]:
289
+ """Report every field the two variants are required to share."""
290
+ return [
291
+ f"the baseline and the candidate name a different {label}"
292
+ for label, left, right in (
293
+ ("Campaign", baseline.campaign_spec_digest, candidate.campaign_spec_digest),
294
+ ("improvement program", baseline.program_ref, candidate.program_ref),
295
+ ("public context", baseline.public_context, candidate.public_context),
296
+ (
297
+ "DataPolicy",
298
+ baseline.configuration.data_policy_digest,
299
+ candidate.configuration.data_policy_digest,
300
+ ),
301
+ (
302
+ "OutcomeContract",
303
+ baseline.configuration.outcome_contract_digest,
304
+ candidate.configuration.outcome_contract_digest,
305
+ ),
306
+ (
307
+ "evaluation backend",
308
+ baseline.configuration.evaluation_backend,
309
+ candidate.configuration.evaluation_backend,
310
+ ),
311
+ )
312
+ if left != right
313
+ ]
314
+
315
+
316
+ def _skill_count_violations(
317
+ baseline: ExperimentManifest,
318
+ candidate: ExperimentManifest,
319
+ mutation: MutationContract,
320
+ ) -> list[str]:
321
+ target = mutation.target_agent
322
+ baseline_agent = baseline.configuration.agents.get(target)
323
+ candidate_agent = candidate.configuration.agents.get(target)
324
+
325
+ if baseline_agent is None or candidate_agent is None:
326
+ return [f"a variant defines no {target} agent to compare"]
327
+
328
+ baseline_skills = baseline_agent.harness.skills
329
+ candidate_skills = candidate_agent.harness.skills
330
+
331
+ violations: list[str] = []
332
+ # The candidate always carries exactly one skill. What the baseline carries
333
+ # is what the mutation kind decides: an insertion measures one skill against
334
+ # none, a replacement measures one revision against the previous one.
335
+ if mutation.kind is MutationKind.SKILL_INSERTION:
336
+ if baseline_skills:
337
+ violations.append(
338
+ f"the baseline carries {len(baseline_skills)} candidate skills; "
339
+ "a baseline carries none"
340
+ )
341
+ elif len(baseline_skills) != 1:
342
+ violations.append(
343
+ f"the baseline carries {len(baseline_skills)} skills; a replacement "
344
+ "replaces exactly one"
345
+ )
346
+ if len(candidate_skills) != 1:
347
+ violations.append(
348
+ f"the candidate carries {len(candidate_skills)} skills; "
349
+ "v0.1 measures exactly one"
350
+ )
351
+
352
+ # A replacement whose two sides name the same content is not a replacement.
353
+ # The root digest is what a skill reference is, so comparing it is the whole
354
+ # of the check.
355
+ if (
356
+ mutation.kind is MutationKind.SKILL_REPLACEMENT
357
+ and len(baseline_skills) == 1
358
+ and len(candidate_skills) == 1
359
+ and baseline_skills[0].digest == candidate_skills[0].digest
360
+ ):
361
+ violations.append(
362
+ "the candidate skill has the baseline skill's root digest, so the "
363
+ "replacement measures nothing"
364
+ )
365
+ return violations
366
+
367
+
368
+ def _pointer_violations(
369
+ differences: list[JsonDifference], allowed: list[str]
370
+ ) -> list[str]:
371
+ return [
372
+ f"the candidate differs at {difference.pointer}, which the mutation "
373
+ "contract does not permit"
374
+ for difference in differences
375
+ if not any(pointer_is_within(difference.pointer, root) for root in allowed)
376
+ ]
@@ -0,0 +1,282 @@
1
+ """Techtree protocol models. Spec section 11.1.
2
+
3
+ This package exports only stable model classes. It never imports services, CLI
4
+ code, resources, or settings, and no model file reaches back into
5
+ :mod:`techtree.canonical` — models describe documents, they do not hash them.
6
+
7
+ Exports are grouped the way the protocol is layered:
8
+
9
+ Campaign and public wrapper
10
+ The scientific contract and the public invitation that wraps it.
11
+
12
+ Execution artifacts
13
+ Everything one run produces, all of it anchored to a Campaign digest.
14
+
15
+ Taskset validation
16
+ The lock, the deterministic receipt, and the local execution record.
17
+
18
+ Catalog, CLI, and local state
19
+ What the packaged catalog ships and what the CLI hands back.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from techtree.models.base import (
25
+ ArtifactRef,
26
+ Digest,
27
+ JsonScalar,
28
+ JsonValue,
29
+ NonEmptyString,
30
+ ObjectEnvelope,
31
+ ProtocolModel,
32
+ PublicKeyRef,
33
+ SignatureEnvelope,
34
+ StateModel,
35
+ UtcDateTime,
36
+ )
37
+
38
+ # --- Campaign and public wrapper -------------------------------------------
39
+ from techtree.models.campaign import (
40
+ AgentSpec,
41
+ BudgetSpec,
42
+ CampaignContext,
43
+ CampaignMetadata,
44
+ CampaignSpec,
45
+ CampaignTaskset,
46
+ EnvironmentSpec,
47
+ EvidenceRequirements,
48
+ ExecutionSpec,
49
+ HarnessSpec,
50
+ ModelSpec,
51
+ MutationContract,
52
+ PackageRef,
53
+ ProgramRef,
54
+ PublicContext,
55
+ RuntimeSpec,
56
+ SamplingSpec,
57
+ ScoringSpec,
58
+ TaskMembershipCommitment,
59
+ TaskSelection,
60
+ TasksetRef,
61
+ )
62
+ from techtree.models.catalog import (
63
+ CatalogClimbEntry,
64
+ CatalogIndex,
65
+ CatalogObjectLocation,
66
+ ClimbSummary,
67
+ CompatibilityIssue,
68
+ CompatibilityResult,
69
+ DataPolicySummary,
70
+ EngineCompatibilityStatus,
71
+ )
72
+
73
+ # --- Catalog, CLI, and local state -----------------------------------------
74
+ from techtree.models.cli import (
75
+ CheckStatus,
76
+ CliEnvelope,
77
+ CliError,
78
+ CliMessage,
79
+ DoctorCheck,
80
+ MessageLevel,
81
+ NextAction,
82
+ )
83
+ from techtree.models.climb import (
84
+ CandidateConstraints,
85
+ CandidatePolicy,
86
+ ClimbManifest,
87
+ ClimbMetadata,
88
+ LeaderboardPolicy,
89
+ PublicationPolicy,
90
+ ResolvedClimb,
91
+ check_climb_policy_consistency,
92
+ )
93
+ from techtree.models.data_policy import (
94
+ CandidateSkillPolicy,
95
+ DataOwner,
96
+ DataPolicy,
97
+ DerivedArtifactPolicy,
98
+ RawEpisodePolicy,
99
+ RevocationPolicy,
100
+ )
101
+ from techtree.models.engine import (
102
+ EngineDescriptor,
103
+ EngineInstallation,
104
+ EnginePackage,
105
+ EngineStatus,
106
+ HostPlatform,
107
+ normalize_host_platform,
108
+ )
109
+
110
+ # --- Execution artifacts ---------------------------------------------------
111
+ from techtree.models.episode_receipt import (
112
+ EpisodeReceipt,
113
+ EvidenceStatus,
114
+ NamedTraceReceipt,
115
+ ScoreStatus,
116
+ SubjectRuntimeReceipt,
117
+ )
118
+ from techtree.models.evaluation_backend import (
119
+ AttestationKind,
120
+ EvaluationBackendKind,
121
+ EvaluationBackendSpec,
122
+ )
123
+ from techtree.models.experiment import (
124
+ ExperimentConfiguration,
125
+ ExperimentManifest,
126
+ ExperimentVariant,
127
+ JsonDifference,
128
+ ManifestComparison,
129
+ )
130
+ from techtree.models.run import (
131
+ PolicyAcknowledgement,
132
+ RunEvent,
133
+ RunPhase,
134
+ RunProgress,
135
+ RunRequest,
136
+ RunState,
137
+ RunStatus,
138
+ )
139
+ from techtree.models.skill import (
140
+ PolicyAcceptanceRequirement,
141
+ SkillArtifact,
142
+ SkillFile,
143
+ SubmissionDraft,
144
+ )
145
+ from techtree.models.uplift_report import (
146
+ ComparisonStatus,
147
+ ExecutionStatus,
148
+ PrimaryUpliftResult,
149
+ PublicationStatus,
150
+ TaskDelta,
151
+ UpliftDecision,
152
+ UpliftReport,
153
+ UpliftStatuses,
154
+ )
155
+
156
+ # --- Taskset validation ----------------------------------------------------
157
+ from techtree.models.validation import (
158
+ TasksetLock,
159
+ TasksetValidationReceipt,
160
+ UpstreamValidationSummary,
161
+ ValidationCheck,
162
+ ValidationEvidence,
163
+ ValidationEvidenceSummary,
164
+ ValidationEvidenceTask,
165
+ ValidationExecutionRecord,
166
+ ValidationMethod,
167
+ ValidationTaskOutcome,
168
+ validation_display_id,
169
+ )
170
+
171
+ __all__ = [
172
+ "AgentSpec",
173
+ "ArtifactRef",
174
+ "AttestationKind",
175
+ "BudgetSpec",
176
+ "CampaignContext",
177
+ "CampaignMetadata",
178
+ "CampaignSpec",
179
+ "CampaignTaskset",
180
+ "CandidateConstraints",
181
+ "CandidatePolicy",
182
+ "CandidateSkillPolicy",
183
+ "CatalogClimbEntry",
184
+ "CatalogIndex",
185
+ "CatalogObjectLocation",
186
+ "CheckStatus",
187
+ "CliEnvelope",
188
+ "CliError",
189
+ "CliMessage",
190
+ "ClimbManifest",
191
+ "ClimbMetadata",
192
+ "ClimbSummary",
193
+ "ComparisonStatus",
194
+ "CompatibilityIssue",
195
+ "CompatibilityResult",
196
+ "DataOwner",
197
+ "DataPolicy",
198
+ "DataPolicySummary",
199
+ "DerivedArtifactPolicy",
200
+ "Digest",
201
+ "DoctorCheck",
202
+ "EngineCompatibilityStatus",
203
+ "EngineDescriptor",
204
+ "EngineInstallation",
205
+ "EnginePackage",
206
+ "EngineStatus",
207
+ "EnvironmentSpec",
208
+ "EpisodeReceipt",
209
+ "EvaluationBackendKind",
210
+ "EvaluationBackendSpec",
211
+ "EvidenceRequirements",
212
+ "EvidenceStatus",
213
+ "ExecutionSpec",
214
+ "ExecutionStatus",
215
+ "ExperimentConfiguration",
216
+ "ExperimentManifest",
217
+ "ExperimentVariant",
218
+ "HarnessSpec",
219
+ "HostPlatform",
220
+ "JsonDifference",
221
+ "JsonScalar",
222
+ "JsonValue",
223
+ "LeaderboardPolicy",
224
+ "ManifestComparison",
225
+ "MessageLevel",
226
+ "ModelSpec",
227
+ "MutationContract",
228
+ "NamedTraceReceipt",
229
+ "NextAction",
230
+ "NonEmptyString",
231
+ "ObjectEnvelope",
232
+ "PackageRef",
233
+ "PolicyAcceptanceRequirement",
234
+ "PolicyAcknowledgement",
235
+ "PrimaryUpliftResult",
236
+ "ProgramRef",
237
+ "ProtocolModel",
238
+ "PublicContext",
239
+ "PublicKeyRef",
240
+ "PublicationPolicy",
241
+ "PublicationStatus",
242
+ "RawEpisodePolicy",
243
+ "ResolvedClimb",
244
+ "RevocationPolicy",
245
+ "RunEvent",
246
+ "RunPhase",
247
+ "RunProgress",
248
+ "RunRequest",
249
+ "RunState",
250
+ "RunStatus",
251
+ "RuntimeSpec",
252
+ "SamplingSpec",
253
+ "ScoreStatus",
254
+ "ScoringSpec",
255
+ "SignatureEnvelope",
256
+ "SkillArtifact",
257
+ "SkillFile",
258
+ "StateModel",
259
+ "SubjectRuntimeReceipt",
260
+ "SubmissionDraft",
261
+ "TaskDelta",
262
+ "TaskMembershipCommitment",
263
+ "TaskSelection",
264
+ "TasksetLock",
265
+ "TasksetRef",
266
+ "TasksetValidationReceipt",
267
+ "UpliftDecision",
268
+ "UpliftReport",
269
+ "UpliftStatuses",
270
+ "UpstreamValidationSummary",
271
+ "UtcDateTime",
272
+ "ValidationCheck",
273
+ "ValidationEvidence",
274
+ "ValidationEvidenceSummary",
275
+ "ValidationEvidenceTask",
276
+ "ValidationExecutionRecord",
277
+ "ValidationMethod",
278
+ "ValidationTaskOutcome",
279
+ "check_climb_policy_consistency",
280
+ "normalize_host_platform",
281
+ "validation_display_id",
282
+ ]