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
techtree/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Supported public Python import surface for Techtree.
|
|
2
|
+
|
|
3
|
+
Spec section 10.1. This module performs no filesystem access, no settings
|
|
4
|
+
loading, no CLI registration, and no resource extraction.
|
|
5
|
+
|
|
6
|
+
Only the objects a caller outside Techtree is meant to name appear here. The
|
|
7
|
+
deferred ``ImprovementProgram`` and ``OutcomeContract`` models are not exported,
|
|
8
|
+
because they do not exist: WP0–WP5 reserve only the pointers to them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from techtree.models.campaign import CampaignSpec
|
|
14
|
+
from techtree.models.climb import ClimbManifest
|
|
15
|
+
from techtree.models.data_policy import DataPolicy
|
|
16
|
+
from techtree.models.episode_receipt import EpisodeReceipt
|
|
17
|
+
from techtree.models.evaluation_backend import EvaluationBackendSpec
|
|
18
|
+
from techtree.models.experiment import ExperimentManifest
|
|
19
|
+
from techtree.models.skill import SkillArtifact
|
|
20
|
+
from techtree.models.uplift_report import UpliftReport
|
|
21
|
+
from techtree.models.validation import TasksetValidationReceipt
|
|
22
|
+
from techtree.version import __version__
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CampaignSpec",
|
|
26
|
+
"ClimbManifest",
|
|
27
|
+
"DataPolicy",
|
|
28
|
+
"EpisodeReceipt",
|
|
29
|
+
"EvaluationBackendSpec",
|
|
30
|
+
"ExperimentManifest",
|
|
31
|
+
"SkillArtifact",
|
|
32
|
+
"TasksetValidationReceipt",
|
|
33
|
+
"UpliftReport",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
techtree/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Support ``python -m techtree``. Spec section 10.2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from techtree.cli.app import main as cli_main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
"""Invoke the Techtree CLI entry point."""
|
|
10
|
+
cli_main()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
main()
|
techtree/canonical.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Canonical serialization, digests, and the Verifiers hash boundary.
|
|
2
|
+
|
|
3
|
+
Spec sections 10.7 and 2.8.
|
|
4
|
+
|
|
5
|
+
This is the only place in Techtree that turns an object into bytes for hashing.
|
|
6
|
+
Every digest in the protocol is the SHA-256 of the RFC 8785 canonical JSON form
|
|
7
|
+
produced here, which is what makes two independently built copies of the same
|
|
8
|
+
object agree on one digest.
|
|
9
|
+
|
|
10
|
+
RFC 8785 fixes key ordering, string escaping, and number formatting, so the
|
|
11
|
+
remaining job is converting Python values into the JSON data model without ever
|
|
12
|
+
introducing an ambiguity of our own:
|
|
13
|
+
|
|
14
|
+
* Aware datetimes become UTC instants spelled with ``Z``. Naive datetimes are
|
|
15
|
+
rejected, because "9am" is not an instant and two readers would disagree
|
|
16
|
+
about which bytes it should produce.
|
|
17
|
+
* ``NaN`` and the infinities are rejected. JSON cannot spell them.
|
|
18
|
+
* Types with more than one reasonable JSON spelling — ``bytes``, ``set``,
|
|
19
|
+
arbitrary objects — are rejected rather than guessed at.
|
|
20
|
+
|
|
21
|
+
The Verifiers boundary is here too, and deliberately narrow.
|
|
22
|
+
:func:`normalize_verifiers_task_hash` accepts only what Verifiers actually
|
|
23
|
+
emits: a bare 64-character lowercase hexadecimal string. It never accepts an
|
|
24
|
+
already-prefixed Techtree digest, because silently passing those through would
|
|
25
|
+
turn the boundary into a place where an unvalidated string can enter the
|
|
26
|
+
protocol wearing a valid-looking digest. Prefixed digests go through
|
|
27
|
+
:func:`validate_digest`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import hashlib
|
|
33
|
+
import hmac
|
|
34
|
+
import math
|
|
35
|
+
import re
|
|
36
|
+
from collections.abc import Mapping, Sequence
|
|
37
|
+
from datetime import UTC, datetime
|
|
38
|
+
from decimal import Decimal
|
|
39
|
+
from enum import Enum
|
|
40
|
+
from pathlib import PurePath
|
|
41
|
+
from typing import Final
|
|
42
|
+
|
|
43
|
+
import rfc8785
|
|
44
|
+
from pydantic import BaseModel
|
|
45
|
+
|
|
46
|
+
from techtree.constants import DIGEST_PREFIX
|
|
47
|
+
from techtree.errors import ValidationError
|
|
48
|
+
from techtree.models.base import DIGEST_PATTERN, Digest, JsonValue
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"VERIFIERS_TASK_HASH_LENGTH",
|
|
52
|
+
"canonical_json_bytes",
|
|
53
|
+
"canonical_json_text",
|
|
54
|
+
"digest_object",
|
|
55
|
+
"normalize_verifiers_task_hash",
|
|
56
|
+
"sha256_digest_bytes",
|
|
57
|
+
"to_json_value",
|
|
58
|
+
"validate_digest",
|
|
59
|
+
"verify_bytes_digest",
|
|
60
|
+
"verify_object_digest",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
#: Verifiers emits raw SHA-256 hexadecimal, so a task hash is always this long.
|
|
64
|
+
VERIFIERS_TASK_HASH_LENGTH: Final = 64
|
|
65
|
+
|
|
66
|
+
_DIGEST_RE = re.compile(DIGEST_PATTERN)
|
|
67
|
+
_RAW_TASK_HASH_RE = re.compile(rf"^[0-9a-f]{{{VERIFIERS_TASK_HASH_LENGTH}}}$")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Conversion to the JSON data model
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _unsupported(value: object, reason: str) -> ValidationError:
|
|
76
|
+
return ValidationError(
|
|
77
|
+
f"cannot canonicalize {type(value).__name__}: {reason}",
|
|
78
|
+
details={"python_type": type(value).__name__},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _float_to_json(value: float) -> float:
|
|
83
|
+
if math.isnan(value) or math.isinf(value):
|
|
84
|
+
raise ValidationError(
|
|
85
|
+
"cannot canonicalize a non-finite number",
|
|
86
|
+
details={"value": repr(value)},
|
|
87
|
+
)
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _datetime_to_json(value: datetime) -> str:
|
|
92
|
+
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
|
|
93
|
+
raise ValidationError(
|
|
94
|
+
"cannot canonicalize a naive datetime; it does not name an instant",
|
|
95
|
+
details={"value": value.isoformat()},
|
|
96
|
+
)
|
|
97
|
+
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _model_to_json(value: BaseModel) -> dict[str, JsonValue]:
|
|
101
|
+
# Fields are read straight off the instance rather than through
|
|
102
|
+
# ``model_dump`` so that every leaf passes through the rules in this module
|
|
103
|
+
# instead of Pydantic's own serializers.
|
|
104
|
+
return {
|
|
105
|
+
name: to_json_value(getattr(value, name)) for name in type(value).model_fields
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def to_json_value(value: object) -> JsonValue:
|
|
110
|
+
"""Convert supported values to the JSON data model.
|
|
111
|
+
|
|
112
|
+
Supports Pydantic models, enums, aware datetimes, paths, decimals,
|
|
113
|
+
mappings, sequences, and JSON scalars. Everything else is rejected.
|
|
114
|
+
"""
|
|
115
|
+
if value is None or isinstance(value, str):
|
|
116
|
+
return value
|
|
117
|
+
# ``bool`` is a subclass of ``int``; it has to be settled first.
|
|
118
|
+
if isinstance(value, bool):
|
|
119
|
+
return value
|
|
120
|
+
if isinstance(value, int):
|
|
121
|
+
return value
|
|
122
|
+
if isinstance(value, float):
|
|
123
|
+
return _float_to_json(value)
|
|
124
|
+
if isinstance(value, Decimal):
|
|
125
|
+
if not value.is_finite():
|
|
126
|
+
raise ValidationError(
|
|
127
|
+
"cannot canonicalize a non-finite decimal",
|
|
128
|
+
details={"value": str(value)},
|
|
129
|
+
)
|
|
130
|
+
return float(value)
|
|
131
|
+
if isinstance(value, datetime):
|
|
132
|
+
return _datetime_to_json(value)
|
|
133
|
+
if isinstance(value, Enum):
|
|
134
|
+
return to_json_value(value.value)
|
|
135
|
+
if isinstance(value, PurePath):
|
|
136
|
+
return value.as_posix()
|
|
137
|
+
if isinstance(value, BaseModel):
|
|
138
|
+
return _model_to_json(value)
|
|
139
|
+
if isinstance(value, Mapping):
|
|
140
|
+
converted: dict[str, JsonValue] = {}
|
|
141
|
+
for key, item in value.items():
|
|
142
|
+
if not isinstance(key, str):
|
|
143
|
+
raise _unsupported(key, "mapping keys must be strings")
|
|
144
|
+
converted[key] = to_json_value(item)
|
|
145
|
+
return converted
|
|
146
|
+
if isinstance(value, bytes | bytearray | memoryview):
|
|
147
|
+
raise _unsupported(value, "encode binary data as base64 text first")
|
|
148
|
+
if isinstance(value, set | frozenset):
|
|
149
|
+
raise _unsupported(value, "sets have no defined order; use a list")
|
|
150
|
+
if isinstance(value, Sequence):
|
|
151
|
+
return [to_json_value(item) for item in value]
|
|
152
|
+
raise _unsupported(value, "no canonical JSON representation is defined")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Canonical bytes
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def canonical_json_bytes(value: object) -> bytes:
|
|
161
|
+
"""Serialize a JSON value using RFC 8785 and return UTF-8 bytes."""
|
|
162
|
+
json_value = to_json_value(value)
|
|
163
|
+
try:
|
|
164
|
+
return rfc8785.dumps(json_value)
|
|
165
|
+
except rfc8785.CanonicalizationError as error:
|
|
166
|
+
raise ValidationError(
|
|
167
|
+
f"value cannot be canonicalized: {error}",
|
|
168
|
+
) from error
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def canonical_json_text(value: object) -> str:
|
|
172
|
+
"""Return canonical JSON text."""
|
|
173
|
+
return canonical_json_bytes(value).decode("utf-8")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Digests
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def sha256_digest_bytes(data: bytes) -> Digest:
|
|
182
|
+
"""Return ``sha256:<hex>`` for raw bytes."""
|
|
183
|
+
return f"{DIGEST_PREFIX}{hashlib.sha256(data).hexdigest()}"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def digest_object(value: object) -> Digest:
|
|
187
|
+
"""Canonicalize a protocol object and return its digest."""
|
|
188
|
+
return sha256_digest_bytes(canonical_json_bytes(value))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def validate_digest(value: str) -> Digest:
|
|
192
|
+
"""Validate an already-prefixed Techtree digest and return it unchanged."""
|
|
193
|
+
if _DIGEST_RE.fullmatch(value) is None:
|
|
194
|
+
raise ValidationError(
|
|
195
|
+
"digest must be sha256: followed by 64 lowercase hexadecimal characters",
|
|
196
|
+
details={"value": value},
|
|
197
|
+
)
|
|
198
|
+
return value
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def verify_bytes_digest(data: bytes, expected: Digest) -> bool:
|
|
202
|
+
"""Use constant-time comparison for raw byte digest."""
|
|
203
|
+
return hmac.compare_digest(sha256_digest_bytes(data), validate_digest(expected))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def verify_object_digest(value: object, expected: Digest) -> bool:
|
|
207
|
+
"""Use constant-time comparison for canonical object digest."""
|
|
208
|
+
return hmac.compare_digest(digest_object(value), validate_digest(expected))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Verifiers boundary
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def normalize_verifiers_task_hash(raw: str) -> Digest:
|
|
217
|
+
"""Convert a Verifiers task hash to Techtree digest representation.
|
|
218
|
+
|
|
219
|
+
Accepts exactly one shape: 64 lowercase hexadecimal characters, with no
|
|
220
|
+
prefix, no surrounding whitespace, and no uppercase.
|
|
221
|
+
"""
|
|
222
|
+
if raw.startswith(DIGEST_PREFIX):
|
|
223
|
+
raise ValidationError(
|
|
224
|
+
"Verifiers task hashes are unprefixed; "
|
|
225
|
+
"use validate_digest for Techtree digests",
|
|
226
|
+
details={"value": raw},
|
|
227
|
+
)
|
|
228
|
+
if len(raw) != VERIFIERS_TASK_HASH_LENGTH:
|
|
229
|
+
raise ValidationError(
|
|
230
|
+
"Verifiers task hash must be exactly "
|
|
231
|
+
f"{VERIFIERS_TASK_HASH_LENGTH} characters",
|
|
232
|
+
details={"length": len(raw)},
|
|
233
|
+
)
|
|
234
|
+
if _RAW_TASK_HASH_RE.fullmatch(raw) is None:
|
|
235
|
+
raise ValidationError(
|
|
236
|
+
"Verifiers task hash must be lowercase hexadecimal",
|
|
237
|
+
details={"value": raw},
|
|
238
|
+
)
|
|
239
|
+
return f"{DIGEST_PREFIX}{raw}"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""The embedded catalog: what a build ships and what those objects resolve to.
|
|
2
|
+
|
|
3
|
+
Spec section 14, decisions 0003 A2/A6/A7.
|
|
4
|
+
|
|
5
|
+
Two modules divide the work:
|
|
6
|
+
|
|
7
|
+
:mod:`techtree.catalog.repository`
|
|
8
|
+
Bytes and addresses. Reading the generated index, turning a public
|
|
9
|
+
reference into a file, recomputing the digest of everything it loads, and
|
|
10
|
+
refusing a path that leaves the catalog root.
|
|
11
|
+
|
|
12
|
+
:mod:`techtree.catalog.service`
|
|
13
|
+
Meaning. Assembling the Climb, Campaign, DataPolicy, publisher validation
|
|
14
|
+
receipt, and normalized evidence into one cross-checked graph, summarizing
|
|
15
|
+
it for a reader, and answering whether this host could actually run it.
|
|
16
|
+
|
|
17
|
+
The packaged catalog of this build is valid and empty. Decisions document 0003
|
|
18
|
+
A2 forbids shipping hand-authored placeholder science, so the real
|
|
19
|
+
``hello-world-climb@1`` graph arrives with the generation chain and the graph
|
|
20
|
+
logic is exercised against a complete synthetic fixture instead.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
__all__: list[str] = []
|