techtree 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- techtree/__init__.py +35 -0
- techtree/__main__.py +14 -0
- techtree/canonical.py +239 -0
- techtree/catalog/__init__.py +25 -0
- techtree/catalog/repository.py +400 -0
- techtree/catalog/service.py +419 -0
- techtree/cli/__init__.py +1 -0
- techtree/cli/app.py +416 -0
- techtree/cli/commands/__init__.py +1 -0
- techtree/cli/commands/climb.py +1223 -0
- techtree/cli/commands/doctor.py +147 -0
- techtree/cli/commands/engine.py +207 -0
- techtree/cli/commands/proof.py +556 -0
- techtree/cli/commands/publish.py +447 -0
- techtree/cli/commands/release.py +303 -0
- techtree/cli/commands/run.py +1067 -0
- techtree/cli/commands/setup.py +181 -0
- techtree/cli/commands/skill.py +221 -0
- techtree/cli/commands/uplift.py +698 -0
- techtree/cli/commands/withdraw.py +212 -0
- techtree/cli/confirm.py +47 -0
- techtree/cli/context.py +96 -0
- techtree/cli/invoke.py +220 -0
- techtree/cli/output.py +280 -0
- techtree/constants.py +138 -0
- techtree/crypto.py +128 -0
- techtree/doctor/__init__.py +1 -0
- techtree/doctor/checks.py +675 -0
- techtree/doctor/execution_checks.py +435 -0
- techtree/doctor/service.py +326 -0
- techtree/drafts/__init__.py +32 -0
- techtree/drafts/source.py +146 -0
- techtree/drafts/store.py +992 -0
- techtree/engines/__init__.py +1 -0
- techtree/engines/bundle.py +251 -0
- techtree/engines/installer.py +679 -0
- techtree/engines/registry.py +235 -0
- techtree/engines/runner.py +170 -0
- techtree/errors.py +262 -0
- techtree/fs.py +234 -0
- techtree/harness.py +108 -0
- techtree/identity/__init__.py +41 -0
- techtree/identity/models.py +113 -0
- techtree/identity/service.py +199 -0
- techtree/identity/store.py +263 -0
- techtree/ids.py +85 -0
- techtree/manifests/__init__.py +39 -0
- techtree/manifests/builder.py +433 -0
- techtree/manifests/compare.py +376 -0
- techtree/models/__init__.py +282 -0
- techtree/models/base.py +201 -0
- techtree/models/campaign.py +484 -0
- techtree/models/catalog.py +227 -0
- techtree/models/cli.py +151 -0
- techtree/models/climb.py +254 -0
- techtree/models/data_policy.py +130 -0
- techtree/models/engine.py +156 -0
- techtree/models/episode_receipt.py +130 -0
- techtree/models/evaluation_backend.py +113 -0
- techtree/models/experiment.py +154 -0
- techtree/models/run.py +214 -0
- techtree/models/skill.py +156 -0
- techtree/models/uplift_report.py +158 -0
- techtree/models/validation.py +299 -0
- techtree/paths.py +116 -0
- techtree/presentation/__init__.py +31 -0
- techtree/presentation/build.py +1242 -0
- techtree/presentation/compact.py +246 -0
- techtree/presentation/evidence.py +169 -0
- techtree/presentation/models.py +358 -0
- techtree/presentation/rich.py +312 -0
- techtree/presentation/sanitize.py +156 -0
- techtree/publication/__init__.py +44 -0
- techtree/publication/address.py +180 -0
- techtree/publication/coordinates.py +26 -0
- techtree/publication/journal.py +212 -0
- techtree/publication/keccak.py +183 -0
- techtree/publication/models.py +209 -0
- techtree/publication/offer.py +35 -0
- techtree/publication/service.py +618 -0
- techtree/publication/transport.py +296 -0
- techtree/publication/verify.py +242 -0
- techtree/publication/withdraw.py +156 -0
- techtree/py.typed +0 -0
- techtree/receipts/__init__.py +52 -0
- techtree/receipts/bundle.py +578 -0
- techtree/receipts/compare.py +1065 -0
- techtree/receipts/episode.py +672 -0
- techtree/receipts/execution.py +630 -0
- techtree/receipts/observed.py +474 -0
- techtree/receipts/set.py +336 -0
- techtree/receipts/uplift.py +655 -0
- techtree/receipts/verify.py +1055 -0
- techtree/release/__init__.py +9 -0
- techtree/release/bootstrap.py +509 -0
- techtree/release/checks.py +376 -0
- techtree/release/document.py +125 -0
- techtree/release/generate.py +221 -0
- techtree/release/models.py +293 -0
- techtree/release/provenance.py +109 -0
- techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
- techtree/resources/catalog/catalog.json +32 -0
- techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
- techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
- techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
- techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
- techtree/resources/engines/default/engine.json +20 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
- techtree/resources/engines/default/pyproject.toml +23 -0
- techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
- techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
- techtree/resources/engines/default/tools/normalize_validation.py +222 -0
- techtree/resources/engines/default/uv.lock +1758 -0
- techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
- techtree/resources/release/build-provenance.json +4 -0
- techtree/resources/release/release-core.json +24 -0
- techtree/runs/__init__.py +31 -0
- techtree/runs/artifacts.py +750 -0
- techtree/runs/child_registry.py +228 -0
- techtree/runs/events.py +478 -0
- techtree/runs/executor.py +140 -0
- techtree/runs/fake.py +741 -0
- techtree/runs/launcher.py +253 -0
- techtree/runs/machine.py +489 -0
- techtree/runs/real.py +789 -0
- techtree/runs/service.py +616 -0
- techtree/runs/store.py +555 -0
- techtree/runs/validation.py +259 -0
- techtree/runs/variants.py +684 -0
- techtree/settings.py +143 -0
- techtree/skills/__init__.py +14 -0
- techtree/skills/archive.py +282 -0
- techtree/skills/policy.py +62 -0
- techtree/skills/scanner.py +394 -0
- techtree/skills/service.py +752 -0
- techtree/skills/starter.py +434 -0
- techtree/tasksets/__init__.py +1 -0
- techtree/tasksets/membership.py +269 -0
- techtree/tasksets/provider.py +207 -0
- techtree/tasksets/resolver.py +311 -0
- techtree/tasksets/service.py +484 -0
- techtree/tasksets/verifiers_cli.py +538 -0
- techtree/uplift/__init__.py +20 -0
- techtree/uplift/context.py +544 -0
- techtree/uplift/derive.py +203 -0
- techtree/uplift/public_tasks.py +151 -0
- techtree/uplift/service.py +719 -0
- techtree/uplift/source.py +160 -0
- techtree/verifiers/__init__.py +31 -0
- techtree/verifiers/budget.py +219 -0
- techtree/verifiers/child.py +633 -0
- techtree/verifiers/compiler.py +432 -0
- techtree/verifiers/config.py +365 -0
- techtree/verifiers/credentials.py +321 -0
- techtree/verifiers/image.py +126 -0
- techtree/verifiers/models.py +527 -0
- techtree/verifiers/outputs.py +368 -0
- techtree/verifiers/progress.py +192 -0
- techtree/verifiers/supervisor.py +341 -0
- techtree/verifiers/verify.py +782 -0
- techtree/version.py +39 -0
- techtree/worker/__init__.py +18 -0
- techtree/worker/execute.py +487 -0
- techtree/worker/main.py +57 -0
- techtree-0.1.0.dist-info/METADATA +344 -0
- techtree-0.1.0.dist-info/RECORD +174 -0
- techtree-0.1.0.dist-info/WHEEL +4 -0
- techtree-0.1.0.dist-info/entry_points.txt +3 -0
- techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|
+
]
|