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,419 @@
|
|
|
1
|
+
"""What a Climb is, and whether you could run it. Spec 14.3, decisions 0003.
|
|
2
|
+
|
|
3
|
+
The repository can prove that a file is the object it claims to be. It cannot
|
|
4
|
+
say whether four such objects, each individually valid, tell one consistent
|
|
5
|
+
story. That is this module's job, and it is the reason the graph is assembled
|
|
6
|
+
in one place instead of wherever a command happens to need a piece of it.
|
|
7
|
+
|
|
8
|
+
:class:`~techtree.models.climb.ResolvedClimb` performs the edge checks the
|
|
9
|
+
moment the four objects are put together: the Climb points at the Campaign that
|
|
10
|
+
was loaded, the Campaign at the DataPolicy and the receipt that were loaded, and
|
|
11
|
+
the candidate policy matches the mutation contract. It also runs the rights
|
|
12
|
+
comparison, which raises :class:`~techtree.errors.PolicyError` rather than a
|
|
13
|
+
validation error — a Climb that promises to publish something its DataPolicy
|
|
14
|
+
forbids is not malformed, it is dishonest, and a caller should be able to tell
|
|
15
|
+
those two failures apart.
|
|
16
|
+
|
|
17
|
+
Three checks sit outside that model because they need more than the four
|
|
18
|
+
objects it holds:
|
|
19
|
+
|
|
20
|
+
* the shipped validation evidence must resolve through the catalog and describe
|
|
21
|
+
the same taskset lock the receipt was issued for (decisions 0003 A4: a public
|
|
22
|
+
catalog has no dangling references);
|
|
23
|
+
* the validated tasks must be exactly the tasks the Campaign committed to, in
|
|
24
|
+
order;
|
|
25
|
+
* a development Climb and a development-only proof grade imply each other, so a
|
|
26
|
+
fixture cannot be dressed up as evidence or a real Climb quietly demoted.
|
|
27
|
+
|
|
28
|
+
Compatibility is answered separately and never conflated with validity. A Climb
|
|
29
|
+
can be perfectly well-formed and still not runnable here, and before the managed
|
|
30
|
+
engine exists that is the ordinary case: the absent engine blocks preparing a
|
|
31
|
+
submission while ``list`` and ``show`` continue to display the Climb.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import platform
|
|
37
|
+
import sys
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from typing import Final, Protocol
|
|
40
|
+
|
|
41
|
+
from techtree.catalog.repository import EmbeddedCatalogRepository, climb_reference
|
|
42
|
+
from techtree.engines.registry import EngineRegistry
|
|
43
|
+
from techtree.errors import NotFoundError, PolicyError, PrerequisiteError, UsageError
|
|
44
|
+
from techtree.models.base import Digest
|
|
45
|
+
from techtree.models.catalog import (
|
|
46
|
+
ClimbSummary,
|
|
47
|
+
CompatibilityIssue,
|
|
48
|
+
CompatibilityResult,
|
|
49
|
+
DataPolicySummary,
|
|
50
|
+
EngineCompatibilityStatus,
|
|
51
|
+
)
|
|
52
|
+
from techtree.models.climb import ResolvedClimb, check_climb_policy_consistency
|
|
53
|
+
from techtree.models.data_policy import DataPolicy
|
|
54
|
+
from techtree.models.engine import normalize_host_platform
|
|
55
|
+
from techtree.models.evaluation_backend import SUPPORTED_EVALUATION_BACKEND_KINDS
|
|
56
|
+
from techtree.models.validation import ValidationEvidence
|
|
57
|
+
from techtree.paths import TechtreePaths
|
|
58
|
+
from techtree.settings import resolved_settings
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"CLIMB_LIST_STATUSES",
|
|
62
|
+
"CatalogService",
|
|
63
|
+
"EngineStatusSource",
|
|
64
|
+
"HostInfo",
|
|
65
|
+
"InstalledEngineStatus",
|
|
66
|
+
"current_host_info",
|
|
67
|
+
"data_policy_summary",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
#: What ``list_climbs`` accepts. ``available`` is the default because a reader
|
|
71
|
+
#: browsing Climbs wants the ones they could enter, and in a build whose only
|
|
72
|
+
#: Climbs are development fixtures that has to include them.
|
|
73
|
+
CLIMB_LIST_STATUSES: Final[frozenset[str]] = frozenset(
|
|
74
|
+
{"available", "all", "open", "closed", "development"}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
_AVAILABLE_STATUSES: Final[frozenset[str]] = frozenset({"open", "development"})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class HostInfo:
|
|
82
|
+
"""The three facts about this machine a compatibility answer rests on."""
|
|
83
|
+
|
|
84
|
+
operating_system: str
|
|
85
|
+
architecture: str
|
|
86
|
+
python_version: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def current_host_info() -> HostInfo:
|
|
90
|
+
"""Return the host Techtree is running on, unnormalized.
|
|
91
|
+
|
|
92
|
+
The raw values are carried rather than the normalized platform string so
|
|
93
|
+
that an unsupported machine can still be named in the answer it gets.
|
|
94
|
+
"""
|
|
95
|
+
return HostInfo(
|
|
96
|
+
operating_system=sys.platform,
|
|
97
|
+
architecture=platform.machine(),
|
|
98
|
+
python_version=platform.python_version(),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class EngineStatusSource(Protocol):
|
|
103
|
+
"""Whatever can say what is known about one engine bundle on this host."""
|
|
104
|
+
|
|
105
|
+
def status_for(self, engine_digest: Digest) -> EngineCompatibilityStatus:
|
|
106
|
+
"""Return what is known about that engine here."""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class InstalledEngineStatus:
|
|
111
|
+
"""Answers the engine question from what the filesystem can prove.
|
|
112
|
+
|
|
113
|
+
The managed engine registry owns the installation marker and its verified
|
|
114
|
+
bit, so compatibility reads that same record instead of guessing from a
|
|
115
|
+
directory that may have been left by an interrupted install.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, paths: TechtreePaths) -> None:
|
|
119
|
+
self._paths = paths
|
|
120
|
+
|
|
121
|
+
def status_for(self, engine_digest: Digest) -> EngineCompatibilityStatus:
|
|
122
|
+
"""Return whether that engine bundle is installed on this host."""
|
|
123
|
+
status = EngineRegistry(self._paths, resolved_settings(self._paths)).status(
|
|
124
|
+
engine_digest
|
|
125
|
+
)
|
|
126
|
+
if not status.installed:
|
|
127
|
+
return EngineCompatibilityStatus.NOT_INSTALLED
|
|
128
|
+
if status.verified:
|
|
129
|
+
return EngineCompatibilityStatus.VERIFIED
|
|
130
|
+
return EngineCompatibilityStatus.INSTALLED_UNVERIFIED
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CatalogService:
|
|
134
|
+
"""Resolves, checks, and summarizes the Climbs a build ships."""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
repository: EmbeddedCatalogRepository,
|
|
139
|
+
host_info: HostInfo,
|
|
140
|
+
engine_status: EngineStatusSource,
|
|
141
|
+
) -> None:
|
|
142
|
+
self._repository = repository
|
|
143
|
+
self._host_info = host_info
|
|
144
|
+
self._engine_status = engine_status
|
|
145
|
+
|
|
146
|
+
def list_climbs(self, *, status: str = "available") -> list[ClimbSummary]:
|
|
147
|
+
"""Return one summary per Climb, in catalog order.
|
|
148
|
+
|
|
149
|
+
Every listed Climb is fully resolved first. Listing a Climb that cannot
|
|
150
|
+
be resolved would be advertising something no reader could enter, and
|
|
151
|
+
the failure would then surface at ``prepare`` instead of here.
|
|
152
|
+
"""
|
|
153
|
+
if status not in CLIMB_LIST_STATUSES:
|
|
154
|
+
raise UsageError(
|
|
155
|
+
f"unknown Climb status {status!r}; choose one of "
|
|
156
|
+
+ ", ".join(sorted(CLIMB_LIST_STATUSES)),
|
|
157
|
+
code="unknown_climb_status",
|
|
158
|
+
details={"status": status},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
summaries: list[ClimbSummary] = []
|
|
162
|
+
for reference in self._repository.list_climb_references():
|
|
163
|
+
resolved = self.get_climb(reference)
|
|
164
|
+
if _status_matches(resolved.climb.metadata.status, status):
|
|
165
|
+
summaries.append(self.climb_summary(resolved))
|
|
166
|
+
return summaries
|
|
167
|
+
|
|
168
|
+
def get_climb(self, reference: str) -> ResolvedClimb:
|
|
169
|
+
"""Resolve one Climb into its complete, cross-checked object graph."""
|
|
170
|
+
entry = self._repository.climb_entry(reference)
|
|
171
|
+
climb = self._repository.load_climb(entry.reference)
|
|
172
|
+
campaign = self._repository.load_campaign(climb.campaign_spec_digest)
|
|
173
|
+
data_policy = self._repository.load_data_policy(campaign.data_policy_digest)
|
|
174
|
+
receipt = self._repository.load_validation_receipt(
|
|
175
|
+
campaign.taskset.validation_receipt_digest
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
resolved = ResolvedClimb(
|
|
179
|
+
climb=climb,
|
|
180
|
+
climb_digest=entry.digest,
|
|
181
|
+
campaign=campaign,
|
|
182
|
+
campaign_digest=climb.campaign_spec_digest,
|
|
183
|
+
data_policy=data_policy,
|
|
184
|
+
data_policy_digest=campaign.data_policy_digest,
|
|
185
|
+
publisher_validation=receipt,
|
|
186
|
+
publisher_validation_digest=campaign.taskset.validation_receipt_digest,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self._check_validation_evidence(resolved)
|
|
190
|
+
self.validate_public_policy(resolved)
|
|
191
|
+
return resolved
|
|
192
|
+
|
|
193
|
+
def compatibility(self, resolved: ResolvedClimb) -> CompatibilityResult:
|
|
194
|
+
"""Report whether this host could run this Climb, and what is missing."""
|
|
195
|
+
issues: list[CompatibilityIssue] = []
|
|
196
|
+
|
|
197
|
+
host_platform, host_supported = self._host_platform()
|
|
198
|
+
if not host_supported:
|
|
199
|
+
issues.append(
|
|
200
|
+
CompatibilityIssue(
|
|
201
|
+
code="host_unsupported",
|
|
202
|
+
severity="error",
|
|
203
|
+
message=(
|
|
204
|
+
f"Techtree does not support {host_platform}; it runs on "
|
|
205
|
+
"macOS and Linux, on arm64 and amd64."
|
|
206
|
+
),
|
|
207
|
+
blocking=True,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
engine_digest = resolved.publisher_validation.engine_digest
|
|
212
|
+
engine_status = self._engine_status.status_for(engine_digest)
|
|
213
|
+
engine_issue = _engine_issue(engine_status)
|
|
214
|
+
if engine_issue is not None:
|
|
215
|
+
issues.append(engine_issue)
|
|
216
|
+
|
|
217
|
+
backend_kind = resolved.campaign.evaluation_backend.kind
|
|
218
|
+
backend_supported = backend_kind in SUPPORTED_EVALUATION_BACKEND_KINDS
|
|
219
|
+
if not backend_supported:
|
|
220
|
+
issues.append(
|
|
221
|
+
CompatibilityIssue(
|
|
222
|
+
code="evaluation_backend_unsupported",
|
|
223
|
+
severity="error",
|
|
224
|
+
message=(
|
|
225
|
+
f"This Climb is evaluated by {backend_kind.value}, which "
|
|
226
|
+
"this version of Techtree cannot run."
|
|
227
|
+
),
|
|
228
|
+
blocking=True,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return CompatibilityResult(
|
|
233
|
+
compatible=not any(issue.blocking for issue in issues),
|
|
234
|
+
host_platform=host_platform,
|
|
235
|
+
host_supported=host_supported,
|
|
236
|
+
required_engine_digest=engine_digest,
|
|
237
|
+
engine_status=engine_status,
|
|
238
|
+
evaluation_backend_kind=backend_kind,
|
|
239
|
+
evaluation_backend_supported=backend_supported,
|
|
240
|
+
issues=issues,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def validation_evidence(self, resolved: ResolvedClimb) -> ValidationEvidence:
|
|
244
|
+
"""Return the normalized evidence this Climb's receipt was issued from.
|
|
245
|
+
|
|
246
|
+
Preparing a submission copies the evidence into the draft so that the
|
|
247
|
+
draft stays verifiable when the catalog that shipped it does not
|
|
248
|
+
(decisions document 0003 A4). The graph was already checked when it was
|
|
249
|
+
resolved, so a receipt with no evidence here is a caller asking for
|
|
250
|
+
something this Climb does not have.
|
|
251
|
+
"""
|
|
252
|
+
reference = resolved.publisher_validation.normalized_evidence
|
|
253
|
+
if reference is None:
|
|
254
|
+
raise NotFoundError(
|
|
255
|
+
"this Climb's publisher validation names no normalized evidence",
|
|
256
|
+
code="publisher_validation_evidence_missing",
|
|
257
|
+
details={"climb": climb_reference(resolved.climb)},
|
|
258
|
+
)
|
|
259
|
+
return self._repository.load_validation_evidence(reference.digest)
|
|
260
|
+
|
|
261
|
+
def validate_public_policy(self, resolved: ResolvedClimb) -> None:
|
|
262
|
+
"""Reject contradictions among the Climb, Campaign, and DataPolicy.
|
|
263
|
+
|
|
264
|
+
The rights comparison already ran when the graph was assembled; running
|
|
265
|
+
it again costs nothing and means a caller holding a ``ResolvedClimb``
|
|
266
|
+
from anywhere can ask the question directly.
|
|
267
|
+
"""
|
|
268
|
+
check_climb_policy_consistency(resolved.climb, resolved.data_policy)
|
|
269
|
+
|
|
270
|
+
status = resolved.climb.metadata.status
|
|
271
|
+
proof_grade = resolved.climb.publication.proof_grade
|
|
272
|
+
if (status == "development") != (proof_grade == "development_only"):
|
|
273
|
+
raise PolicyError(
|
|
274
|
+
f"this Climb is {status} and claims a {proof_grade} proof grade; "
|
|
275
|
+
"a development Climb produces development-only results and "
|
|
276
|
+
"nothing else does",
|
|
277
|
+
code="proof_grade_contradiction",
|
|
278
|
+
details={"status": status, "proof_grade": proof_grade},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def climb_summary(self, resolved: ResolvedClimb) -> ClimbSummary:
|
|
282
|
+
"""Project a resolved graph into what ``list`` and ``show`` display."""
|
|
283
|
+
campaign = resolved.campaign
|
|
284
|
+
climb = resolved.climb
|
|
285
|
+
|
|
286
|
+
return ClimbSummary(
|
|
287
|
+
reference=climb_reference(climb),
|
|
288
|
+
climb_digest=resolved.climb_digest,
|
|
289
|
+
campaign_spec_digest=resolved.campaign_digest,
|
|
290
|
+
title=climb.metadata.title,
|
|
291
|
+
summary=climb.metadata.summary,
|
|
292
|
+
status=climb.metadata.status,
|
|
293
|
+
purpose=campaign.metadata.purpose,
|
|
294
|
+
taskset_id=campaign.taskset.ref.id,
|
|
295
|
+
task_count=campaign.taskset.selection.num_tasks,
|
|
296
|
+
subject_harness=campaign.subject.harness.id,
|
|
297
|
+
subject_harness_version=campaign.subject.harness.version,
|
|
298
|
+
# Read off the Climb rather than the Campaign: a public Climb still
|
|
299
|
+
# requires skill_insertion, and ``ResolvedClimb`` refuses to exist
|
|
300
|
+
# unless the Campaign's mutation kind is the one the Climb names.
|
|
301
|
+
mutation_kind=climb.candidate_policy.required_mutation,
|
|
302
|
+
candidate_skill_visibility=climb.candidate_policy.skill_visibility,
|
|
303
|
+
evaluation_backend=campaign.evaluation_backend.kind,
|
|
304
|
+
proof_grade=climb.publication.proof_grade,
|
|
305
|
+
data_policy=data_policy_summary(resolved.data_policy),
|
|
306
|
+
compatibility=self.compatibility(resolved),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# -- Internals ---------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def _host_platform(self) -> tuple[str, bool]:
|
|
312
|
+
"""Return the normalized host platform, or the raw pair and False."""
|
|
313
|
+
try:
|
|
314
|
+
return (
|
|
315
|
+
normalize_host_platform(
|
|
316
|
+
self._host_info.operating_system, self._host_info.architecture
|
|
317
|
+
),
|
|
318
|
+
True,
|
|
319
|
+
)
|
|
320
|
+
except PrerequisiteError:
|
|
321
|
+
return (
|
|
322
|
+
f"{self._host_info.operating_system}/{self._host_info.architecture}",
|
|
323
|
+
False,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _check_validation_evidence(self, resolved: ResolvedClimb) -> None:
|
|
327
|
+
"""Resolve the receipt's evidence and check it describes this taskset.
|
|
328
|
+
|
|
329
|
+
Decisions document 0003 A4: an artifact a public object references is
|
|
330
|
+
resolvable through the catalog, or it is not referenced at all. The
|
|
331
|
+
evidence is also the only shipped record of which tasks were actually
|
|
332
|
+
validated, so it is compared against the Campaign's commitment here.
|
|
333
|
+
"""
|
|
334
|
+
reference = resolved.publisher_validation.normalized_evidence
|
|
335
|
+
if reference is None:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
evidence = self._repository.load_validation_evidence(reference.digest)
|
|
339
|
+
|
|
340
|
+
if evidence.taskset_lock_digest != (
|
|
341
|
+
resolved.publisher_validation.taskset_lock_digest
|
|
342
|
+
):
|
|
343
|
+
raise PolicyError(
|
|
344
|
+
"the shipped validation evidence was produced for a different "
|
|
345
|
+
"taskset than the receipt it belongs to",
|
|
346
|
+
code="validation_evidence_mismatch",
|
|
347
|
+
details={
|
|
348
|
+
"receipt_taskset_lock_digest": (
|
|
349
|
+
resolved.publisher_validation.taskset_lock_digest
|
|
350
|
+
),
|
|
351
|
+
"evidence_taskset_lock_digest": evidence.taskset_lock_digest,
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
validated = [task.task_hash for task in evidence.tasks]
|
|
356
|
+
committed = list(resolved.campaign.taskset.membership.ordered_task_hashes)
|
|
357
|
+
if validated != committed:
|
|
358
|
+
raise PolicyError(
|
|
359
|
+
"the Campaign commits to different tasks than the ones the "
|
|
360
|
+
"publisher validated",
|
|
361
|
+
code="validated_membership_mismatch",
|
|
362
|
+
details={
|
|
363
|
+
"committed_task_count": len(committed),
|
|
364
|
+
"validated_task_count": len(validated),
|
|
365
|
+
},
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def data_policy_summary(data_policy: DataPolicy) -> DataPolicySummary:
|
|
370
|
+
"""Project a DataPolicy into the four rights a reader most needs."""
|
|
371
|
+
return DataPolicySummary(
|
|
372
|
+
raw_episode_server_upload=data_policy.raw_episodes.server_upload,
|
|
373
|
+
raw_episode_training_use=data_policy.raw_episodes.training_use,
|
|
374
|
+
candidate_skill_public_release=data_policy.candidate_skill.public_release,
|
|
375
|
+
uplift_report_visibility=data_policy.derived_artifacts.uplift_report,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _status_matches(climb_status: str, requested: str) -> bool:
|
|
380
|
+
if requested == "all":
|
|
381
|
+
return True
|
|
382
|
+
if requested == "available":
|
|
383
|
+
return climb_status in _AVAILABLE_STATUSES
|
|
384
|
+
return climb_status == requested
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _engine_issue(status: EngineCompatibilityStatus) -> CompatibilityIssue | None:
|
|
388
|
+
"""Turn what is known about the engine into what it means for this Climb."""
|
|
389
|
+
if status is EngineCompatibilityStatus.VERIFIED:
|
|
390
|
+
return None
|
|
391
|
+
if status is EngineCompatibilityStatus.INSTALLED_UNVERIFIED:
|
|
392
|
+
return CompatibilityIssue(
|
|
393
|
+
code="engine_not_verified",
|
|
394
|
+
severity="warning",
|
|
395
|
+
message=(
|
|
396
|
+
"The evaluation engine is installed but has not been checked "
|
|
397
|
+
"since. Run `techtree engine verify` before you rely on a result."
|
|
398
|
+
),
|
|
399
|
+
blocking=False,
|
|
400
|
+
)
|
|
401
|
+
if status is EngineCompatibilityStatus.NOT_INSTALLED:
|
|
402
|
+
return CompatibilityIssue(
|
|
403
|
+
code="engine_not_installed",
|
|
404
|
+
severity="error",
|
|
405
|
+
message=(
|
|
406
|
+
"The evaluation engine this Climb needs is not installed yet. "
|
|
407
|
+
"Run `techtree engine install` before preparing a submission."
|
|
408
|
+
),
|
|
409
|
+
blocking=True,
|
|
410
|
+
)
|
|
411
|
+
return CompatibilityIssue(
|
|
412
|
+
code="engine_status_unknown",
|
|
413
|
+
severity="error",
|
|
414
|
+
message=(
|
|
415
|
+
"Techtree cannot tell whether the evaluation engine this Climb "
|
|
416
|
+
"needs is available on this machine."
|
|
417
|
+
),
|
|
418
|
+
blocking=True,
|
|
419
|
+
)
|
techtree/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Techtree command-line interface."""
|