techtree 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. techtree/__init__.py +35 -0
  2. techtree/__main__.py +14 -0
  3. techtree/canonical.py +239 -0
  4. techtree/catalog/__init__.py +25 -0
  5. techtree/catalog/repository.py +400 -0
  6. techtree/catalog/service.py +419 -0
  7. techtree/cli/__init__.py +1 -0
  8. techtree/cli/app.py +416 -0
  9. techtree/cli/commands/__init__.py +1 -0
  10. techtree/cli/commands/climb.py +1223 -0
  11. techtree/cli/commands/doctor.py +147 -0
  12. techtree/cli/commands/engine.py +207 -0
  13. techtree/cli/commands/proof.py +556 -0
  14. techtree/cli/commands/publish.py +447 -0
  15. techtree/cli/commands/release.py +303 -0
  16. techtree/cli/commands/run.py +1067 -0
  17. techtree/cli/commands/setup.py +181 -0
  18. techtree/cli/commands/skill.py +221 -0
  19. techtree/cli/commands/uplift.py +698 -0
  20. techtree/cli/commands/withdraw.py +212 -0
  21. techtree/cli/confirm.py +47 -0
  22. techtree/cli/context.py +96 -0
  23. techtree/cli/invoke.py +220 -0
  24. techtree/cli/output.py +280 -0
  25. techtree/constants.py +138 -0
  26. techtree/crypto.py +128 -0
  27. techtree/doctor/__init__.py +1 -0
  28. techtree/doctor/checks.py +675 -0
  29. techtree/doctor/execution_checks.py +435 -0
  30. techtree/doctor/service.py +326 -0
  31. techtree/drafts/__init__.py +32 -0
  32. techtree/drafts/source.py +146 -0
  33. techtree/drafts/store.py +992 -0
  34. techtree/engines/__init__.py +1 -0
  35. techtree/engines/bundle.py +251 -0
  36. techtree/engines/installer.py +679 -0
  37. techtree/engines/registry.py +235 -0
  38. techtree/engines/runner.py +170 -0
  39. techtree/errors.py +262 -0
  40. techtree/fs.py +234 -0
  41. techtree/harness.py +108 -0
  42. techtree/identity/__init__.py +41 -0
  43. techtree/identity/models.py +113 -0
  44. techtree/identity/service.py +199 -0
  45. techtree/identity/store.py +263 -0
  46. techtree/ids.py +85 -0
  47. techtree/manifests/__init__.py +39 -0
  48. techtree/manifests/builder.py +433 -0
  49. techtree/manifests/compare.py +376 -0
  50. techtree/models/__init__.py +282 -0
  51. techtree/models/base.py +201 -0
  52. techtree/models/campaign.py +484 -0
  53. techtree/models/catalog.py +227 -0
  54. techtree/models/cli.py +151 -0
  55. techtree/models/climb.py +254 -0
  56. techtree/models/data_policy.py +130 -0
  57. techtree/models/engine.py +156 -0
  58. techtree/models/episode_receipt.py +130 -0
  59. techtree/models/evaluation_backend.py +113 -0
  60. techtree/models/experiment.py +154 -0
  61. techtree/models/run.py +214 -0
  62. techtree/models/skill.py +156 -0
  63. techtree/models/uplift_report.py +158 -0
  64. techtree/models/validation.py +299 -0
  65. techtree/paths.py +116 -0
  66. techtree/presentation/__init__.py +31 -0
  67. techtree/presentation/build.py +1242 -0
  68. techtree/presentation/compact.py +246 -0
  69. techtree/presentation/evidence.py +169 -0
  70. techtree/presentation/models.py +358 -0
  71. techtree/presentation/rich.py +312 -0
  72. techtree/presentation/sanitize.py +156 -0
  73. techtree/publication/__init__.py +44 -0
  74. techtree/publication/address.py +180 -0
  75. techtree/publication/coordinates.py +26 -0
  76. techtree/publication/journal.py +212 -0
  77. techtree/publication/keccak.py +183 -0
  78. techtree/publication/models.py +209 -0
  79. techtree/publication/offer.py +35 -0
  80. techtree/publication/service.py +618 -0
  81. techtree/publication/transport.py +296 -0
  82. techtree/publication/verify.py +242 -0
  83. techtree/publication/withdraw.py +156 -0
  84. techtree/py.typed +0 -0
  85. techtree/receipts/__init__.py +52 -0
  86. techtree/receipts/bundle.py +578 -0
  87. techtree/receipts/compare.py +1065 -0
  88. techtree/receipts/episode.py +672 -0
  89. techtree/receipts/execution.py +630 -0
  90. techtree/receipts/observed.py +474 -0
  91. techtree/receipts/set.py +336 -0
  92. techtree/receipts/uplift.py +655 -0
  93. techtree/receipts/verify.py +1055 -0
  94. techtree/release/__init__.py +9 -0
  95. techtree/release/bootstrap.py +509 -0
  96. techtree/release/checks.py +376 -0
  97. techtree/release/document.py +125 -0
  98. techtree/release/generate.py +221 -0
  99. techtree/release/models.py +293 -0
  100. techtree/release/provenance.py +109 -0
  101. techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
  102. techtree/resources/catalog/catalog.json +32 -0
  103. techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
  104. techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
  105. techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
  106. techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
  107. techtree/resources/engines/default/engine.json +20 -0
  108. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
  109. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
  110. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
  111. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
  112. techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
  113. techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
  114. techtree/resources/engines/default/pyproject.toml +23 -0
  115. techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
  116. techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
  117. techtree/resources/engines/default/tools/normalize_validation.py +222 -0
  118. techtree/resources/engines/default/uv.lock +1758 -0
  119. techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
  120. techtree/resources/release/build-provenance.json +4 -0
  121. techtree/resources/release/release-core.json +24 -0
  122. techtree/runs/__init__.py +31 -0
  123. techtree/runs/artifacts.py +750 -0
  124. techtree/runs/child_registry.py +228 -0
  125. techtree/runs/events.py +478 -0
  126. techtree/runs/executor.py +140 -0
  127. techtree/runs/fake.py +741 -0
  128. techtree/runs/launcher.py +253 -0
  129. techtree/runs/machine.py +489 -0
  130. techtree/runs/real.py +789 -0
  131. techtree/runs/service.py +616 -0
  132. techtree/runs/store.py +555 -0
  133. techtree/runs/validation.py +259 -0
  134. techtree/runs/variants.py +684 -0
  135. techtree/settings.py +143 -0
  136. techtree/skills/__init__.py +14 -0
  137. techtree/skills/archive.py +282 -0
  138. techtree/skills/policy.py +62 -0
  139. techtree/skills/scanner.py +394 -0
  140. techtree/skills/service.py +752 -0
  141. techtree/skills/starter.py +434 -0
  142. techtree/tasksets/__init__.py +1 -0
  143. techtree/tasksets/membership.py +269 -0
  144. techtree/tasksets/provider.py +207 -0
  145. techtree/tasksets/resolver.py +311 -0
  146. techtree/tasksets/service.py +484 -0
  147. techtree/tasksets/verifiers_cli.py +538 -0
  148. techtree/uplift/__init__.py +20 -0
  149. techtree/uplift/context.py +544 -0
  150. techtree/uplift/derive.py +203 -0
  151. techtree/uplift/public_tasks.py +151 -0
  152. techtree/uplift/service.py +719 -0
  153. techtree/uplift/source.py +160 -0
  154. techtree/verifiers/__init__.py +31 -0
  155. techtree/verifiers/budget.py +219 -0
  156. techtree/verifiers/child.py +633 -0
  157. techtree/verifiers/compiler.py +432 -0
  158. techtree/verifiers/config.py +365 -0
  159. techtree/verifiers/credentials.py +321 -0
  160. techtree/verifiers/image.py +126 -0
  161. techtree/verifiers/models.py +527 -0
  162. techtree/verifiers/outputs.py +368 -0
  163. techtree/verifiers/progress.py +192 -0
  164. techtree/verifiers/supervisor.py +341 -0
  165. techtree/verifiers/verify.py +782 -0
  166. techtree/version.py +39 -0
  167. techtree/worker/__init__.py +18 -0
  168. techtree/worker/execute.py +487 -0
  169. techtree/worker/main.py +57 -0
  170. techtree-0.1.0.dist-info/METADATA +344 -0
  171. techtree-0.1.0.dist-info/RECORD +174 -0
  172. techtree-0.1.0.dist-info/WHEEL +4 -0
  173. techtree-0.1.0.dist-info/entry_points.txt +3 -0
  174. techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ """The managed Verifiers engine: bundle, installer, registry, runner."""
@@ -0,0 +1,251 @@
1
+ """What the engine bundle is, and what it hashes to. Spec section 20.2.
2
+
3
+ The bundle is the static half of the managed engine: a descriptor, a project
4
+ file pinning Verifiers to one commit, the lock that pins everything else, the
5
+ two helper tools that run inside the engine, and the reference package source.
6
+ It contains no Campaign, Climb, DataPolicy, catalog, golden, or schema, which
7
+ is what keeps Campaign generation free of a digest cycle (spec section 20.1).
8
+
9
+ Everything here is content-addressed by the same construction: enumerate the
10
+ files deterministically, describe each one by relative path, size, and content
11
+ digest, sort by path, and digest the canonical JSON manifest. The manifest
12
+ schema is named so that a digest can never be confused with the digest of some
13
+ other kind of tree — an engine bundle and a package tree hash differently even
14
+ if they somehow held identical files.
15
+
16
+ What is excluded from a digest is as much a part of the contract as what is
17
+ included. A virtual environment, a bytecode cache, and the local installation
18
+ marker are all things that appear *because* an engine was installed. Hashing
19
+ them would make an engine's digest change the moment it was used, which is the
20
+ opposite of what a content address is for.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ from collections.abc import Iterator, Sequence
27
+ from dataclasses import dataclass
28
+ from importlib.resources import files as resource_files
29
+ from importlib.resources.abc import Traversable
30
+ from pathlib import Path
31
+ from typing import Final
32
+
33
+ from techtree.canonical import digest_object, sha256_digest_bytes
34
+ from techtree.errors import EngineError, ValidationError
35
+ from techtree.fs import ensure_private_directory
36
+ from techtree.models.base import Digest, JsonValue
37
+ from techtree.models.engine import EngineDescriptor
38
+
39
+ __all__ = [
40
+ "BUNDLE_MANIFEST_SCHEMA",
41
+ "DEFAULT_ENGINE_NAME",
42
+ "DESCRIPTOR_FILENAME",
43
+ "ENGINE_TOOLS",
44
+ "INSTALLATION_FILENAME",
45
+ "PACKAGES_DIRECTORY",
46
+ "PACKAGE_MANIFEST_SCHEMA",
47
+ "TOOLS_DIRECTORY",
48
+ "BundleFile",
49
+ "content_manifest",
50
+ "copy_engine_bundle",
51
+ "default_engine_descriptor",
52
+ "default_engine_digest",
53
+ "embedded_engine_root",
54
+ "engine_bundle_digest",
55
+ "enumerate_bundle_files",
56
+ "package_source_digest",
57
+ "read_engine_descriptor",
58
+ ]
59
+
60
+ DEFAULT_ENGINE_NAME: Final = "default"
61
+ DESCRIPTOR_FILENAME: Final = "engine.json"
62
+ #: Written by the installer when an installation is complete, and therefore
63
+ #: never part of what the bundle hashes to.
64
+ INSTALLATION_FILENAME: Final = "installed.json"
65
+ PACKAGES_DIRECTORY: Final = "packages"
66
+ TOOLS_DIRECTORY: Final = "tools"
67
+
68
+ #: The helpers that run inside the managed engine. Decisions document 0003 A3
69
+ #: puts them inside the digested bundle: they are shipped, hashed, and copied
70
+ #: unchanged, so the code that inspects a taskset is pinned exactly as tightly
71
+ #: as the library it inspects it with.
72
+ ENGINE_TOOLS: Final[tuple[str, ...]] = (
73
+ "inspect_taskset.py",
74
+ "normalize_eval_output.py",
75
+ "normalize_validation.py",
76
+ )
77
+
78
+ #: Manifest schema names. They are part of the hashed bytes, so a bundle digest
79
+ #: and a package-tree digest are different values by construction.
80
+ BUNDLE_MANIFEST_SCHEMA: Final = "techtree.engine-bundle.v1"
81
+ #: Decisions document 0003 A8.
82
+ PACKAGE_MANIFEST_SCHEMA: Final = "techtree.package-content.v1"
83
+
84
+ #: Directories that exist because an engine was built or used, never because it
85
+ #: was authored.
86
+ _EXCLUDED_DIRECTORIES: Final[frozenset[str]] = frozenset(
87
+ {".git", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".venv", "__pycache__"}
88
+ )
89
+ #: Files that are local state or operating-system litter rather than content.
90
+ _EXCLUDED_FILENAMES: Final[frozenset[str]] = frozenset(
91
+ {".DS_Store", INSTALLATION_FILENAME}
92
+ )
93
+ _EXCLUDED_SUFFIXES: Final[tuple[str, ...]] = (".pyc", ".pyo", ".tmp")
94
+
95
+ #: An engine name becomes a resource path segment, so it is restricted to a
96
+ #: shape that cannot escape the resources directory.
97
+ _ENGINE_NAME_RE: Final = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class BundleFile:
102
+ """One file in a hashed tree, described by content rather than by mtime."""
103
+
104
+ relative_path: str
105
+ size: int
106
+ digest: Digest
107
+
108
+
109
+ def embedded_engine_root(name: str = DEFAULT_ENGINE_NAME) -> Traversable:
110
+ """Locate one packaged engine bundle inside the installed distribution."""
111
+ if _ENGINE_NAME_RE.fullmatch(name) is None:
112
+ raise ValidationError(
113
+ f"{name!r} is not a valid engine name",
114
+ details={"name": name},
115
+ )
116
+
117
+ root = resource_files("techtree") / "resources" / "engines" / name
118
+ if not root.is_dir():
119
+ raise EngineError(
120
+ f"this build does not ship an engine named {name!r}",
121
+ code="engine_bundle_missing",
122
+ details={"name": name},
123
+ )
124
+ return root
125
+
126
+
127
+ def enumerate_bundle_files(root: Traversable) -> list[BundleFile]:
128
+ """Enumerate a tree deterministically, by relative path.
129
+
130
+ Sorting is by the POSIX relative path, so the order does not depend on the
131
+ filesystem, the platform, or the order the files were written in.
132
+ """
133
+ return sorted(_walk(root, prefix=""), key=lambda entry: entry.relative_path)
134
+
135
+
136
+ def content_manifest(
137
+ schema_version: str, files: Sequence[BundleFile]
138
+ ) -> dict[str, JsonValue]:
139
+ """Return the canonical manifest that a tree digest is taken over."""
140
+ return {
141
+ "schema_version": schema_version,
142
+ "files": [
143
+ {
144
+ "path": entry.relative_path,
145
+ "size": entry.size,
146
+ "digest": entry.digest,
147
+ }
148
+ for entry in files
149
+ ],
150
+ }
151
+
152
+
153
+ def engine_bundle_digest(root: Traversable) -> Digest:
154
+ """Return the digest of one engine bundle's static contents."""
155
+ return digest_object(
156
+ content_manifest(BUNDLE_MANIFEST_SCHEMA, enumerate_bundle_files(root))
157
+ )
158
+
159
+
160
+ def package_source_digest(package_root: Traversable) -> Digest:
161
+ """Return the source-tree digest of one shipped package.
162
+
163
+ Decisions document 0003 A8: this is a digest of the *source* the engine
164
+ installs, not of a wheel and not of what lands in site-packages. It is one
165
+ of the three values required to be equal — the Campaign's package digest,
166
+ the engine descriptor's ``source_digest``, and the TasksetLock's
167
+ ``resolved_package_digest``.
168
+ """
169
+ return digest_object(
170
+ content_manifest(PACKAGE_MANIFEST_SCHEMA, enumerate_bundle_files(package_root))
171
+ )
172
+
173
+
174
+ def read_engine_descriptor(root: Traversable) -> EngineDescriptor:
175
+ """Load and validate ``engine.json`` from a bundle."""
176
+ descriptor = root / DESCRIPTOR_FILENAME
177
+ if not descriptor.is_file():
178
+ raise EngineError(
179
+ f"engine bundle has no {DESCRIPTOR_FILENAME}",
180
+ code="engine_descriptor_missing",
181
+ details={"file": DESCRIPTOR_FILENAME},
182
+ )
183
+
184
+ raw = descriptor.read_bytes()
185
+ try:
186
+ return EngineDescriptor.model_validate_json(raw)
187
+ except ValueError as error:
188
+ raise ValidationError(
189
+ f"engine descriptor is not a valid EngineDescriptor: {error}",
190
+ details={"file": DESCRIPTOR_FILENAME},
191
+ ) from error
192
+
193
+
194
+ def copy_engine_bundle(root: Traversable, destination: Path) -> None:
195
+ """Copy exactly the hashed files of a bundle into an empty directory.
196
+
197
+ Only enumerated files are written, so an installed bundle contains the same
198
+ bytes the digest was taken over and nothing else — no bytecode cache from
199
+ the developer's machine, no stray editor file. Content is written rather
200
+ than linked: an installed engine must not change because a source tree
201
+ somewhere else did.
202
+ """
203
+ if destination.is_dir() and any(destination.iterdir()):
204
+ raise EngineError(
205
+ f"refusing to copy an engine bundle into a non-empty directory: "
206
+ f"{destination}",
207
+ code="engine_destination_not_empty",
208
+ details={"path": str(destination)},
209
+ )
210
+
211
+ ensure_private_directory(destination)
212
+ for entry in enumerate_bundle_files(root):
213
+ target = destination / entry.relative_path
214
+ ensure_private_directory(target.parent)
215
+ source: Traversable = root
216
+ for segment in entry.relative_path.split("/"):
217
+ source = source / segment
218
+ target.write_bytes(source.read_bytes())
219
+
220
+
221
+ def default_engine_descriptor() -> EngineDescriptor:
222
+ """Return the descriptor of the engine this build ships."""
223
+ return read_engine_descriptor(embedded_engine_root())
224
+
225
+
226
+ def default_engine_digest() -> Digest:
227
+ """Return the digest of the engine bundle this build ships."""
228
+ return engine_bundle_digest(embedded_engine_root())
229
+
230
+
231
+ def _walk(root: Traversable, *, prefix: str) -> Iterator[BundleFile]:
232
+ """Yield every included file under ``root``, depth first."""
233
+ for entry in root.iterdir():
234
+ name = entry.name
235
+ relative = f"{prefix}{name}"
236
+
237
+ if entry.is_dir():
238
+ if name in _EXCLUDED_DIRECTORIES:
239
+ continue
240
+ yield from _walk(entry, prefix=f"{relative}/")
241
+ continue
242
+
243
+ if name in _EXCLUDED_FILENAMES or name.endswith(_EXCLUDED_SUFFIXES):
244
+ continue
245
+
246
+ content = entry.read_bytes()
247
+ yield BundleFile(
248
+ relative_path=relative,
249
+ size=len(content),
250
+ digest=sha256_digest_bytes(content),
251
+ )