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,299 @@
|
|
|
1
|
+
"""Taskset locking and mechanical validation. Spec 11.9, decisions 0003 A1.
|
|
2
|
+
|
|
3
|
+
Three objects, with a deliberate split between them.
|
|
4
|
+
|
|
5
|
+
``TasksetLock``
|
|
6
|
+
What the taskset resolved to: engine, package, ordered task hashes, and the
|
|
7
|
+
membership digest computed from them.
|
|
8
|
+
|
|
9
|
+
``TasksetValidationReceipt``
|
|
10
|
+
The scientific answer to one narrow question — is this taskset mechanically
|
|
11
|
+
valid and internally consistent? It carries no identifier, no timestamp, no
|
|
12
|
+
duration, no path, and no raw log. Decisions document 0003 makes the
|
|
13
|
+
receipt's own content digest its identity, which is what allows the
|
|
14
|
+
publisher's receipt and a participant's locally recomputed receipt to be
|
|
15
|
+
compared by equality instead of by narrative. A display identifier can be
|
|
16
|
+
derived with :func:`validation_display_id`; it is never stored.
|
|
17
|
+
|
|
18
|
+
``ValidationExecutionRecord``
|
|
19
|
+
Everything the receipt refuses to carry: when it ran, on which host, with
|
|
20
|
+
which command, and which raw artifacts it left behind. It is local
|
|
21
|
+
operational provenance, never part of the Campaign graph, never referenced
|
|
22
|
+
by a public Climb, and never byte-compared by ``make generated-check``.
|
|
23
|
+
|
|
24
|
+
Between the raw upstream output and the receipt sits ``ValidationEvidence``:
|
|
25
|
+
the normalized, deterministic form of what the validator reported, with
|
|
26
|
+
elapsed times, temporary paths, run identifiers, and host detail removed. That
|
|
27
|
+
is the artifact a receipt points at and the one that ships publicly.
|
|
28
|
+
|
|
29
|
+
The receipt does not answer whether the environment is discriminative, robust
|
|
30
|
+
to reward hacking, suitable for RL, or economically useful. Those belong to a
|
|
31
|
+
future ``EnvironmentQualificationReport``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import Final, Literal, Self
|
|
37
|
+
|
|
38
|
+
from pydantic import Field, model_validator
|
|
39
|
+
|
|
40
|
+
from techtree.models.base import (
|
|
41
|
+
ArtifactRef,
|
|
42
|
+
Digest,
|
|
43
|
+
NonEmptyString,
|
|
44
|
+
ProtocolModel,
|
|
45
|
+
StateModel,
|
|
46
|
+
UtcDateTime,
|
|
47
|
+
)
|
|
48
|
+
from techtree.models.campaign import TasksetRef
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"REQUIRED_VALIDATION_CHECKS",
|
|
52
|
+
"TasksetLock",
|
|
53
|
+
"TasksetValidationReceipt",
|
|
54
|
+
"UpstreamValidationSummary",
|
|
55
|
+
"ValidationCheck",
|
|
56
|
+
"ValidationEvidence",
|
|
57
|
+
"ValidationEvidenceSummary",
|
|
58
|
+
"ValidationEvidenceTask",
|
|
59
|
+
"ValidationExecutionRecord",
|
|
60
|
+
"ValidationMethod",
|
|
61
|
+
"ValidationTaskOutcome",
|
|
62
|
+
"validation_display_id",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
#: Every check a receipt must report on. A receipt that silently omits one is
|
|
66
|
+
#: not a weaker receipt, it is an unreadable one: a reader cannot tell whether
|
|
67
|
+
#: the check passed or was never attempted.
|
|
68
|
+
REQUIRED_VALIDATION_CHECKS: Final[tuple[str, ...]] = (
|
|
69
|
+
"upstream_gold",
|
|
70
|
+
"upstream_setup",
|
|
71
|
+
"membership_repeatability",
|
|
72
|
+
"task_hash_uniqueness",
|
|
73
|
+
"committed_membership_match",
|
|
74
|
+
"expected_task_count",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
#: How many hexadecimal characters of the receipt digest a display identifier
|
|
78
|
+
#: shows. Enough to be unambiguous in a terminal, short enough to read aloud.
|
|
79
|
+
_DISPLAY_ID_HEX_LENGTH: Final = 24
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TasksetLock(ProtocolModel):
|
|
83
|
+
"""What a taskset reference resolved to, pinned."""
|
|
84
|
+
|
|
85
|
+
schema_version: Literal["techtree.taskset-lock.v1alpha1"]
|
|
86
|
+
taskset_ref: TasksetRef
|
|
87
|
+
engine_digest: Digest
|
|
88
|
+
resolved_package_digest: Digest
|
|
89
|
+
ordered_task_hashes: list[Digest]
|
|
90
|
+
membership_digest: Digest
|
|
91
|
+
task_count: int = Field(ge=1)
|
|
92
|
+
|
|
93
|
+
@model_validator(mode="after")
|
|
94
|
+
def _check_task_list(self) -> Self:
|
|
95
|
+
"""Reject a hash list that disagrees with the count or repeats a task."""
|
|
96
|
+
if len(self.ordered_task_hashes) != self.task_count:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"lock records {len(self.ordered_task_hashes)} task hashes but "
|
|
99
|
+
f"a task_count of {self.task_count}"
|
|
100
|
+
)
|
|
101
|
+
if len(set(self.ordered_task_hashes)) != len(self.ordered_task_hashes):
|
|
102
|
+
raise ValueError("locked task hashes must be unique")
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ValidationCheck(ProtocolModel):
|
|
107
|
+
"""One mechanical check and what it found."""
|
|
108
|
+
|
|
109
|
+
id: NonEmptyString
|
|
110
|
+
status: Literal["passed", "failed", "warning", "not_run"]
|
|
111
|
+
detail: NonEmptyString
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class UpstreamValidationSummary(ProtocolModel):
|
|
115
|
+
"""What the upstream validator reported, in aggregate."""
|
|
116
|
+
|
|
117
|
+
mode: Literal["all", "gold", "setup"]
|
|
118
|
+
total: int = Field(ge=0)
|
|
119
|
+
recorded: int = Field(ge=0)
|
|
120
|
+
valid: int = Field(ge=0)
|
|
121
|
+
invalid: int = Field(ge=0)
|
|
122
|
+
error: int = Field(ge=0)
|
|
123
|
+
timeout: int = Field(ge=0)
|
|
124
|
+
missing: int = Field(ge=0)
|
|
125
|
+
valid_rate: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
126
|
+
|
|
127
|
+
@model_validator(mode="after")
|
|
128
|
+
def _check_counts_add_up(self) -> Self:
|
|
129
|
+
"""Reject a summary whose parts do not account for its total."""
|
|
130
|
+
counted = self.valid + self.invalid + self.error + self.timeout + self.missing
|
|
131
|
+
if counted != self.total:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"summary accounts for {counted} tasks but reports a total of "
|
|
134
|
+
f"{self.total}"
|
|
135
|
+
)
|
|
136
|
+
if self.recorded > self.total:
|
|
137
|
+
raise ValueError("recorded cannot exceed total")
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ValidationMethod(ProtocolModel):
|
|
142
|
+
"""How the validation was performed, in reproducible terms."""
|
|
143
|
+
|
|
144
|
+
kind: Literal["verifiers_validate"]
|
|
145
|
+
mode: Literal["all"]
|
|
146
|
+
runtime: Literal["subprocess"]
|
|
147
|
+
validator_revision: NonEmptyString
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ValidationTaskOutcome(ProtocolModel):
|
|
151
|
+
"""One validator verdict for one task."""
|
|
152
|
+
|
|
153
|
+
valid: bool
|
|
154
|
+
reason: NonEmptyString
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ValidationEvidenceTask(ProtocolModel):
|
|
158
|
+
"""The normalized per-task record the validator produced."""
|
|
159
|
+
|
|
160
|
+
position: int = Field(ge=0)
|
|
161
|
+
task_hash: Digest
|
|
162
|
+
gold: ValidationTaskOutcome
|
|
163
|
+
setup: ValidationTaskOutcome
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ValidationEvidenceSummary(ProtocolModel):
|
|
167
|
+
"""Aggregate counts over the normalized task records."""
|
|
168
|
+
|
|
169
|
+
total: int = Field(ge=0)
|
|
170
|
+
valid: int = Field(ge=0)
|
|
171
|
+
invalid: int = Field(ge=0)
|
|
172
|
+
error: int = Field(ge=0)
|
|
173
|
+
timeout: int = Field(ge=0)
|
|
174
|
+
missing: int = Field(ge=0)
|
|
175
|
+
|
|
176
|
+
@model_validator(mode="after")
|
|
177
|
+
def _check_counts_add_up(self) -> Self:
|
|
178
|
+
"""Reject a summary whose parts do not account for its total."""
|
|
179
|
+
counted = self.valid + self.invalid + self.error + self.timeout + self.missing
|
|
180
|
+
if counted != self.total:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"evidence accounts for {counted} tasks but reports a total of "
|
|
183
|
+
f"{self.total}"
|
|
184
|
+
)
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ValidationEvidence(ProtocolModel):
|
|
189
|
+
"""Deterministic normalized validator output. Decisions 0003 A1.
|
|
190
|
+
|
|
191
|
+
Normalization has already removed elapsed times, wall-clock timestamps,
|
|
192
|
+
temporary paths, run identifiers, process identifiers, hostnames, absolute
|
|
193
|
+
output directories, log prefixes, and tracebacks from successful runs.
|
|
194
|
+
What remains is the part two honest parties must agree on.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
schema_version: Literal["techtree.validation-evidence.v1alpha1"]
|
|
198
|
+
taskset_lock_digest: Digest
|
|
199
|
+
method: ValidationMethod
|
|
200
|
+
tasks: list[ValidationEvidenceTask]
|
|
201
|
+
summary: ValidationEvidenceSummary
|
|
202
|
+
|
|
203
|
+
@model_validator(mode="after")
|
|
204
|
+
def _check_tasks_are_a_dense_ordered_run(self) -> Self:
|
|
205
|
+
"""Reject gaps, repeats, or an order that depends on who wrote the file."""
|
|
206
|
+
positions = [task.position for task in self.tasks]
|
|
207
|
+
if positions != list(range(len(self.tasks))):
|
|
208
|
+
raise ValueError(
|
|
209
|
+
"evidence tasks must be sorted by position and cover 0..n-1 "
|
|
210
|
+
"without gaps"
|
|
211
|
+
)
|
|
212
|
+
hashes = [task.task_hash for task in self.tasks]
|
|
213
|
+
if len(set(hashes)) != len(hashes):
|
|
214
|
+
raise ValueError("evidence task hashes must be unique")
|
|
215
|
+
if self.summary.total != len(self.tasks):
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"evidence lists {len(self.tasks)} tasks but its summary reports "
|
|
218
|
+
f"{self.summary.total}"
|
|
219
|
+
)
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TasksetValidationReceipt(ProtocolModel):
|
|
224
|
+
"""Is this taskset mechanically valid and internally consistent?
|
|
225
|
+
|
|
226
|
+
Decisions document 0003 A1 removed the identifier, creation time, raw
|
|
227
|
+
artifact list, durations, absolute paths, and tracebacks that the original
|
|
228
|
+
shape carried. Every one of those varies between two correct runs of the
|
|
229
|
+
same validation, which meant the publisher's receipt and a participant's
|
|
230
|
+
receipt could never be equal even when they agreed completely. They now
|
|
231
|
+
are, and that equality is the check.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
schema_version: Literal["techtree.taskset-validation.v1alpha1"]
|
|
235
|
+
taskset_lock_digest: Digest
|
|
236
|
+
engine_digest: Digest
|
|
237
|
+
method: ValidationMethod
|
|
238
|
+
status: Literal["valid", "invalid", "errored"]
|
|
239
|
+
upstream_summary: UpstreamValidationSummary
|
|
240
|
+
checks: list[ValidationCheck]
|
|
241
|
+
normalized_evidence: ArtifactRef | None
|
|
242
|
+
|
|
243
|
+
@model_validator(mode="after")
|
|
244
|
+
def _check_required_checks_are_reported(self) -> Self:
|
|
245
|
+
"""Reject a receipt that omits or repeats a required check."""
|
|
246
|
+
reported = [check.id for check in self.checks]
|
|
247
|
+
if len(set(reported)) != len(reported):
|
|
248
|
+
raise ValueError("a receipt reports each check exactly once")
|
|
249
|
+
missing = [name for name in REQUIRED_VALIDATION_CHECKS if name not in reported]
|
|
250
|
+
if missing:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
"receipt is missing required checks: " + ", ".join(missing)
|
|
253
|
+
)
|
|
254
|
+
if self.status == "valid" and any(
|
|
255
|
+
check.status == "failed" for check in self.checks
|
|
256
|
+
):
|
|
257
|
+
raise ValueError("a receipt with a failed check is not valid")
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ValidationExecutionRecord(StateModel):
|
|
262
|
+
"""Local operational provenance for one validation execution.
|
|
263
|
+
|
|
264
|
+
Decisions document 0003 A1. This is the mutable, host-specific companion to
|
|
265
|
+
the receipt. It is never part of the Campaign graph, never referenced by a
|
|
266
|
+
public Climb, never the publisher commitment, and never byte-compared by
|
|
267
|
+
generated-fixture checks.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
schema_version: Literal["techtree.validation-execution.v1alpha1"]
|
|
271
|
+
id: NonEmptyString
|
|
272
|
+
receipt_digest: Digest
|
|
273
|
+
started_at: UtcDateTime
|
|
274
|
+
finished_at: UtcDateTime
|
|
275
|
+
command: list[NonEmptyString]
|
|
276
|
+
command_digest: Digest
|
|
277
|
+
host_platform: NonEmptyString
|
|
278
|
+
worker_pid: int | None
|
|
279
|
+
raw_artifacts: list[ArtifactRef]
|
|
280
|
+
|
|
281
|
+
@model_validator(mode="after")
|
|
282
|
+
def _check_execution_window(self) -> Self:
|
|
283
|
+
"""Reject a record that finished before it started."""
|
|
284
|
+
if self.finished_at < self.started_at:
|
|
285
|
+
raise ValueError("finished_at cannot precede started_at")
|
|
286
|
+
if not self.command:
|
|
287
|
+
raise ValueError("a validation execution records the command it ran")
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def validation_display_id(receipt_digest: Digest) -> str:
|
|
292
|
+
"""Return the human-facing identifier derived from a receipt digest.
|
|
293
|
+
|
|
294
|
+
The value is derived for display only. Storing it inside the receipt would
|
|
295
|
+
put a second, redundant identity in an object whose whole point is that its
|
|
296
|
+
content is its identity.
|
|
297
|
+
"""
|
|
298
|
+
_, _, hexadecimal = receipt_digest.partition(":")
|
|
299
|
+
return f"validation_{hexadecimal[:_DISPLAY_ID_HEX_LENGTH]}"
|
techtree/paths.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Where Techtree keeps its local state. Spec sections 10.10 and 28.
|
|
2
|
+
|
|
3
|
+
One root directory holds everything: the settings file, the download cache,
|
|
4
|
+
submission drafts, runs, and installed managed engines. Resolving that root
|
|
5
|
+
through ``platformdirs`` means the location follows each platform's own
|
|
6
|
+
convention instead of hard-coding a Unix dotfile path.
|
|
7
|
+
|
|
8
|
+
Nothing here touches the filesystem at import time. Directories are created
|
|
9
|
+
only when :func:`ensure_path_layout` is called, which keeps ``import techtree``
|
|
10
|
+
free of side effects.
|
|
11
|
+
|
|
12
|
+
``identities_dir`` is resolved here and created elsewhere. The local executor
|
|
13
|
+
identity (spec section 7.5) is made by ``techtree setup``, or by a run that
|
|
14
|
+
reaches the point of signing its receipts, and
|
|
15
|
+
:class:`~techtree.identity.store.IdentityStore` creates the directory then.
|
|
16
|
+
Creating it as part of the layout would leave every machine looking as though
|
|
17
|
+
it held a key.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from platformdirs import user_data_path
|
|
26
|
+
|
|
27
|
+
from techtree.canonical import validate_digest
|
|
28
|
+
from techtree.fs import ensure_private_directory
|
|
29
|
+
from techtree.ids import validate_id
|
|
30
|
+
from techtree.models.base import Digest
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"APPLICATION_NAME",
|
|
34
|
+
"TechtreePaths",
|
|
35
|
+
"default_paths",
|
|
36
|
+
"ensure_path_layout",
|
|
37
|
+
"paths_from_root",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
APPLICATION_NAME = "techtree"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class TechtreePaths:
|
|
45
|
+
"""Every directory and file Techtree owns, derived from one root."""
|
|
46
|
+
|
|
47
|
+
root: Path
|
|
48
|
+
config_file: Path
|
|
49
|
+
cache_dir: Path
|
|
50
|
+
drafts_dir: Path
|
|
51
|
+
runs_dir: Path
|
|
52
|
+
engines_dir: Path
|
|
53
|
+
identities_dir: Path
|
|
54
|
+
|
|
55
|
+
def draft_dir(self, draft_id: str) -> Path:
|
|
56
|
+
"""Return the directory holding one submission draft."""
|
|
57
|
+
return self.drafts_dir / validate_id(draft_id, "draft")
|
|
58
|
+
|
|
59
|
+
def run_dir(self, run_id: str) -> Path:
|
|
60
|
+
"""Return the directory holding one run."""
|
|
61
|
+
return self.runs_dir / validate_id(run_id, "run")
|
|
62
|
+
|
|
63
|
+
def skills_cache_dir(self) -> Path:
|
|
64
|
+
"""Return the directory holding Skills Techtree materialized itself."""
|
|
65
|
+
return self.cache_dir / "skills"
|
|
66
|
+
|
|
67
|
+
def skill_cache_dir(self, digest: Digest) -> Path:
|
|
68
|
+
"""Return the cache directory holding one Skill, named by its digest.
|
|
69
|
+
|
|
70
|
+
Content-addressed by the Skill's own root digest and spelled the way
|
|
71
|
+
an engine install is, so the same Skill is materialized once per home
|
|
72
|
+
and a directory's name is a claim its contents can be checked against.
|
|
73
|
+
"""
|
|
74
|
+
return self.skills_cache_dir() / validate_digest(digest).replace(":", "-", 1)
|
|
75
|
+
|
|
76
|
+
def engine_dir(self, digest: Digest) -> Path:
|
|
77
|
+
"""Return the install directory for one managed engine bundle.
|
|
78
|
+
|
|
79
|
+
The digest's colon becomes a dash: ``sha256-<hex>``. A colon is legal
|
|
80
|
+
on POSIX but not on Windows, and an engine install is content-addressed
|
|
81
|
+
on every platform.
|
|
82
|
+
"""
|
|
83
|
+
return self.engines_dir / validate_digest(digest).replace(":", "-", 1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def paths_from_root(root: Path) -> TechtreePaths:
|
|
87
|
+
"""Construct deterministic test paths."""
|
|
88
|
+
resolved = Path(root)
|
|
89
|
+
return TechtreePaths(
|
|
90
|
+
root=resolved,
|
|
91
|
+
config_file=resolved / "config.toml",
|
|
92
|
+
cache_dir=resolved / "cache",
|
|
93
|
+
drafts_dir=resolved / "drafts",
|
|
94
|
+
runs_dir=resolved / "runs",
|
|
95
|
+
engines_dir=resolved / "engines",
|
|
96
|
+
identities_dir=resolved / "identities",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def default_paths() -> TechtreePaths:
|
|
101
|
+
"""Resolve platform paths through platformdirs."""
|
|
102
|
+
return paths_from_root(
|
|
103
|
+
user_data_path(appname=APPLICATION_NAME, appauthor=False, roaming=False)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def ensure_path_layout(paths: TechtreePaths) -> None:
|
|
108
|
+
"""Create top-level private directories."""
|
|
109
|
+
for directory in (
|
|
110
|
+
paths.root,
|
|
111
|
+
paths.cache_dir,
|
|
112
|
+
paths.drafts_dir,
|
|
113
|
+
paths.runs_dir,
|
|
114
|
+
paths.engines_dir,
|
|
115
|
+
):
|
|
116
|
+
ensure_private_directory(directory)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Showing a result without becoming part of it. Spec sections 7.13-7.17.
|
|
2
|
+
|
|
3
|
+
Spec section 3.5 draws the line this package lives on: rich terminal output is
|
|
4
|
+
a view, not scientific evidence. No colour, markup, prose, or channel-specific
|
|
5
|
+
formatting enters an ``EpisodeReceipt`` or an ``UpliftReport``, and nothing in
|
|
6
|
+
this package can change a number, a status, or a digest — every one of them is
|
|
7
|
+
copied out of a signed report and formatted.
|
|
8
|
+
|
|
9
|
+
That is why the payload is channel-neutral. One builder produces one
|
|
10
|
+
:class:`~techtree.presentation.models.UpliftPresentationPayload` from the
|
|
11
|
+
signed report; a terminal renderer, a phone-sized Markdown renderer, and the
|
|
12
|
+
founder's own rich-output Skill all consume the same object. A second builder
|
|
13
|
+
per channel would be a second place a result could be described differently.
|
|
14
|
+
|
|
15
|
+
The division of labour:
|
|
16
|
+
|
|
17
|
+
``models``
|
|
18
|
+
The neutral payload, and the pieces a renderer draws.
|
|
19
|
+
``build``
|
|
20
|
+
Turning one signed report into one payload.
|
|
21
|
+
``rich``
|
|
22
|
+
The accessible terminal rendering.
|
|
23
|
+
``compact``
|
|
24
|
+
The bounded, ANSI-free rendering a gateway or a phone can carry.
|
|
25
|
+
``sanitize``
|
|
26
|
+
What may appear at all, enforced rather than intended.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
__all__: list[str] = []
|