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,263 @@
1
+ """Where the local signing key lives. Spec section 7.5.
2
+
3
+ ```text
4
+ <techtree home>/identities/
5
+ ├── executor-private-key.bin mode 0600, raw Ed25519 private bytes
6
+ └── executor-public.json mode 0600, canonical ExecutorIdentity
7
+ ```
8
+
9
+ The directory is :attr:`~techtree.paths.TechtreePaths.identities_dir`, which
10
+ WP0 resolved and deliberately never created: decisions document 0001 forbade
11
+ device keys through WP5, so the location was settled and the directory did not
12
+ exist. This module is the first thing entitled to create it.
13
+
14
+ Four properties are the reason this is a module rather than two file writes.
15
+
16
+ *Creation is exclusive.* The private key is created with ``O_EXCL``, so a
17
+ second creation attempt is reported as a conflict instead of quietly replacing
18
+ the key every existing receipt was signed under. A replaced key does not
19
+ invalidate old signatures loudly; it invalidates them silently, which is worse.
20
+
21
+ *The private half is never returned as bytes.* :meth:`IdentityStore.load_private`
22
+ hands back a key object that can sign. Nothing here exposes a serialization of
23
+ it, no error detail carries it, and the only bytes written are the ones written
24
+ once at creation.
25
+
26
+ *The pair is checkable without a document.* :meth:`IdentityStore.verify_pair`
27
+ signs a fixed, domain-separated challenge in memory and verifies it against the
28
+ stored public half. That is how a caller learns the two files still belong to
29
+ each other before it signs anything real with them.
30
+
31
+ *Both halves are digested together.* The key identifier is the digest of the
32
+ public key bytes, so it is derived rather than assigned: two files that
33
+ disagree about which key they describe cannot both be self-consistent.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import os
39
+ from collections.abc import Callable
40
+ from datetime import UTC, datetime
41
+ from pathlib import Path
42
+ from typing import Final
43
+
44
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
45
+ from pydantic import ValidationError as PydanticValidationError
46
+
47
+ from techtree.canonical import canonical_json_bytes, sha256_digest_bytes
48
+ from techtree.crypto import (
49
+ ED25519_PRIVATE_KEY_BYTES,
50
+ generate_private_key,
51
+ load_private_key,
52
+ load_public_key,
53
+ private_key_bytes,
54
+ public_key_bytes,
55
+ public_key_to_base64,
56
+ sign_digest,
57
+ verify_signature,
58
+ )
59
+ from techtree.errors import ConflictError, NotFoundError, ValidationError
60
+ from techtree.fs import (
61
+ atomic_write_bytes,
62
+ ensure_private_directory,
63
+ fsync_directory,
64
+ open_exclusive,
65
+ )
66
+ from techtree.identity.models import LOCAL_IDENTITY_INVALID, ExecutorIdentity
67
+ from techtree.paths import TechtreePaths
68
+
69
+ __all__ = [
70
+ "IDENTITY_SELF_CHECK_CHALLENGE",
71
+ "PRIVATE_KEY_FILENAME",
72
+ "PUBLIC_IDENTITY_FILENAME",
73
+ "IdentityStore",
74
+ ]
75
+
76
+ #: Raw Ed25519 private bytes. The specification permits raw or a clearly
77
+ #: versioned encoding; raw is what the primitives already read and write, and
78
+ #: an encoding this file did not need would be one more thing to get wrong.
79
+ PRIVATE_KEY_FILENAME: Final = "executor-private-key.bin"
80
+
81
+ PUBLIC_IDENTITY_FILENAME: Final = "executor-public.json"
82
+
83
+ #: What :meth:`IdentityStore.verify_pair` signs. Domain-separated so that a
84
+ #: signature made during a self-check can never be replayed as a signature over
85
+ #: a protocol object: no canonical document digests to this value.
86
+ IDENTITY_SELF_CHECK_CHALLENGE: Final = b"techtree/identity/self-check/v1"
87
+
88
+ #: Owner read and write only, for both halves. The public half is not secret,
89
+ #: but it is local state in a single-user directory and there is no reason for
90
+ #: it to be more readable than everything beside it.
91
+ _FILE_MODE: Final = 0o600
92
+
93
+
94
+ class IdentityStore:
95
+ """Creates, loads, and self-checks the local executor identity."""
96
+
97
+ def __init__(
98
+ self,
99
+ paths: TechtreePaths,
100
+ *,
101
+ clock: Callable[[], datetime] | None = None,
102
+ ) -> None:
103
+ self._paths = paths
104
+ self._clock = clock or _utc_now
105
+
106
+ # -- placement ----------------------------------------------------------
107
+
108
+ @property
109
+ def directory(self) -> Path:
110
+ """Return the directory both halves of the identity live in."""
111
+ return self._paths.identities_dir
112
+
113
+ @property
114
+ def private_key_path(self) -> Path:
115
+ """Return where the private half lives. Never copied anywhere else."""
116
+ return self.directory / PRIVATE_KEY_FILENAME
117
+
118
+ @property
119
+ def public_identity_path(self) -> Path:
120
+ """Return where the public half lives."""
121
+ return self.directory / PUBLIC_IDENTITY_FILENAME
122
+
123
+ # -- lifecycle ----------------------------------------------------------
124
+
125
+ def exists(self) -> bool:
126
+ """Return whether both halves of the identity are present."""
127
+ return self.private_key_path.is_file() and self.public_identity_path.is_file()
128
+
129
+ def create(self) -> ExecutorIdentity:
130
+ """Generate a key, write both halves exclusively, and return the public one.
131
+
132
+ The private half is written first and with ``O_EXCL``: it is the file
133
+ whose existence means "this machine has an identity", so it is the file
134
+ that has to win the race. The public half is derived from it, which is
135
+ why rewriting it over a stale one is correct rather than destructive.
136
+ """
137
+ ensure_private_directory(self.directory)
138
+ private_key = generate_private_key()
139
+
140
+ try:
141
+ with open_exclusive(self.private_key_path, _FILE_MODE) as handle:
142
+ handle.write(private_key_bytes(private_key))
143
+ handle.flush()
144
+ os.fsync(handle.fileno())
145
+ except ConflictError as error:
146
+ raise ConflictError(
147
+ "this machine already has a local signing key; replacing it "
148
+ "would silently orphan every receipt signed under the old one",
149
+ code=LOCAL_IDENTITY_INVALID,
150
+ details={"path": str(self.private_key_path)},
151
+ ) from error
152
+
153
+ identity = _identity_for(private_key, created_at=self._clock())
154
+ atomic_write_bytes(
155
+ self.public_identity_path,
156
+ canonical_json_bytes(identity),
157
+ mode=_FILE_MODE,
158
+ )
159
+ fsync_directory(self.directory)
160
+ return identity
161
+
162
+ # -- loading ------------------------------------------------------------
163
+
164
+ def load_public(self) -> ExecutorIdentity:
165
+ """Load and validate the public identity from its stored bytes."""
166
+ path = self.public_identity_path
167
+ try:
168
+ raw = path.read_bytes()
169
+ except FileNotFoundError as error:
170
+ raise NotFoundError(
171
+ "this machine has no local signing identity yet; `techtree "
172
+ "setup` creates one",
173
+ code=LOCAL_IDENTITY_INVALID,
174
+ details={"path": str(path)},
175
+ ) from error
176
+
177
+ try:
178
+ return ExecutorIdentity.model_validate_json(raw)
179
+ except PydanticValidationError as error:
180
+ raise ValidationError(
181
+ "the stored local identity is not a valid identity document: "
182
+ f"{error.errors()[0]['msg']}",
183
+ code=LOCAL_IDENTITY_INVALID,
184
+ details={"path": str(path)},
185
+ ) from error
186
+
187
+ def load_private(self) -> Ed25519PrivateKey:
188
+ """Load the private half into memory.
189
+
190
+ Nothing about the material reaches the caller except a key object that
191
+ can sign: no bytes, no encoding, and no detail in the errors raised
192
+ when the file is unusable.
193
+ """
194
+ path = self.private_key_path
195
+ try:
196
+ raw = path.read_bytes()
197
+ except FileNotFoundError as error:
198
+ raise NotFoundError(
199
+ "this machine has no local signing key yet; `techtree setup` "
200
+ "creates one",
201
+ code=LOCAL_IDENTITY_INVALID,
202
+ details={"path": str(path)},
203
+ ) from error
204
+
205
+ if len(raw) != ED25519_PRIVATE_KEY_BYTES:
206
+ raise ValidationError(
207
+ "the stored local signing key is not an Ed25519 private key",
208
+ code=LOCAL_IDENTITY_INVALID,
209
+ details={"path": str(path), "length": len(raw)},
210
+ )
211
+ return load_private_key(raw)
212
+
213
+ def verify_pair(self) -> bool:
214
+ """Sign a fixed challenge in memory and check it against the public half.
215
+
216
+ Returns ``False`` for two well-formed files that describe different
217
+ keys. A file that cannot be read or parsed at all is a different
218
+ problem and raises, because "your identity is missing" and "your
219
+ identity does not match itself" call for different repairs.
220
+ """
221
+ private_key = self.load_private()
222
+ identity = self.load_public()
223
+ public_key = load_public_key(public_key_bytes(private_key))
224
+ if public_key_to_base64(public_key) != identity.public_key:
225
+ return False
226
+ if _key_id_for(private_key) != identity.key_id:
227
+ return False
228
+
229
+ challenge = sha256_digest_bytes(IDENTITY_SELF_CHECK_CHALLENGE)
230
+ signature = sign_digest(private_key, challenge, key_id=identity.key_id)
231
+ return verify_signature(public_key, challenge, signature)
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # Derivation
236
+ # ---------------------------------------------------------------------------
237
+
238
+
239
+ def _identity_for(
240
+ private_key: Ed25519PrivateKey, *, created_at: datetime
241
+ ) -> ExecutorIdentity:
242
+ """Return the public identity describing one private key."""
243
+ return ExecutorIdentity(
244
+ kind="local_ed25519",
245
+ key_id=_key_id_for(private_key),
246
+ algorithm="ed25519",
247
+ public_key=public_key_to_base64(private_key.public_key()),
248
+ created_at=created_at,
249
+ )
250
+
251
+
252
+ def _key_id_for(private_key: Ed25519PrivateKey) -> str:
253
+ """Return the content-addressed identifier of a key's public half.
254
+
255
+ Derived rather than assigned: a key identifier that was chosen could be
256
+ reused for a different key, and every signature Techtree checks names the
257
+ key by this string.
258
+ """
259
+ return sha256_digest_bytes(public_key_bytes(private_key))
260
+
261
+
262
+ def _utc_now() -> datetime:
263
+ return datetime.now(UTC)
techtree/ids.py ADDED
@@ -0,0 +1,85 @@
1
+ """Prefixed local identifiers. Spec section 10.6.
2
+
3
+ An identifier looks like ``run_5b1c...`` — a namespace prefix, an underscore,
4
+ and 32 lowercase hexadecimal characters from a UUID4.
5
+
6
+ Identifiers are labels, not integrity values. Two objects with the same
7
+ identifier are not thereby the same object; only a digest says that. Nothing in
8
+ the protocol should ever compare identifiers where it means to compare content.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import uuid
15
+ from typing import Final
16
+
17
+ from techtree.errors import ValidationError
18
+
19
+ __all__ = [
20
+ "ID_PREFIXES",
21
+ "id_prefix",
22
+ "new_id",
23
+ "validate_id",
24
+ ]
25
+
26
+ #: Every namespace WP0–WP5 issues identifiers for. The set is closed so that a
27
+ #: typo in a prefix fails at the point it is written rather than becoming a
28
+ #: silent new namespace.
29
+ ID_PREFIXES: Final[frozenset[str]] = frozenset(
30
+ {
31
+ "campaign",
32
+ "climb",
33
+ "draft",
34
+ "run",
35
+ "receipt",
36
+ "uplift",
37
+ "policy",
38
+ }
39
+ )
40
+
41
+ _ID_PATTERN = re.compile(r"^(?P<prefix>[a-z][a-z0-9]*)_(?P<body>[0-9a-f]{32})$")
42
+
43
+
44
+ def _require_known_prefix(prefix: str) -> str:
45
+ if prefix not in ID_PREFIXES:
46
+ raise ValidationError(
47
+ f"unknown identifier prefix {prefix!r}",
48
+ details={"prefix": prefix, "known": ", ".join(sorted(ID_PREFIXES))},
49
+ )
50
+ return prefix
51
+
52
+
53
+ def new_id(prefix: str) -> str:
54
+ """Create a new prefixed UUID4 hexadecimal identifier."""
55
+ _require_known_prefix(prefix)
56
+ return f"{prefix}_{uuid.uuid4().hex}"
57
+
58
+
59
+ def validate_id(value: str, expected_prefix: str | None = None) -> str:
60
+ """Validate syntax and optionally require one prefix."""
61
+ match = _ID_PATTERN.fullmatch(value)
62
+ if match is None:
63
+ raise ValidationError(
64
+ "identifier must be <prefix>_<32 lowercase hexadecimal characters>",
65
+ details={"value": value},
66
+ )
67
+
68
+ prefix = match.group("prefix")
69
+ _require_known_prefix(prefix)
70
+
71
+ if expected_prefix is not None:
72
+ _require_known_prefix(expected_prefix)
73
+ if prefix != expected_prefix:
74
+ raise ValidationError(
75
+ f"expected a {expected_prefix} identifier, got a {prefix} identifier",
76
+ details={"value": value, "expected_prefix": expected_prefix},
77
+ )
78
+
79
+ return value
80
+
81
+
82
+ def id_prefix(value: str) -> str:
83
+ """Return the prefix from a valid ID."""
84
+ validated = validate_id(value)
85
+ return validated.split("_", 1)[0]
@@ -0,0 +1,39 @@
1
+ """Campaign-derived experiment variants and the comparison between them.
2
+
3
+ Spec PR6 §6.3.
4
+
5
+ A Campaign states what is to be measured. It does not state which side of a
6
+ comparison a particular execution is. Two modules turn one into the other:
7
+
8
+ :mod:`techtree.manifests.builder`
9
+ Derivation. Copying the Campaign's scientific configuration unchanged into
10
+ a baseline variant carrying no skill and a candidate variant carrying
11
+ exactly one, and sealing each with the digest of its configuration.
12
+
13
+ :mod:`techtree.manifests.compare`
14
+ Control. Requiring the structural invariants outright, then diffing the two
15
+ configurations down to their leaves as RFC 6901 JSON Pointers, and
16
+ reporting whether the only place they disagree is the one place the
17
+ Campaign's mutation contract permits.
18
+
19
+ Together they are the reason an uplift number means something: the candidate
20
+ run and the baseline run are provably the same experiment apart from the skill
21
+ that was inserted, and the proof is a list of pointers anyone can re-derive
22
+ from the two documents.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from techtree.manifests.builder import (
28
+ build_baseline_manifest,
29
+ build_candidate_manifest,
30
+ build_experiment_configuration,
31
+ )
32
+ from techtree.manifests.compare import compare_manifests
33
+
34
+ __all__ = [
35
+ "build_baseline_manifest",
36
+ "build_candidate_manifest",
37
+ "build_experiment_configuration",
38
+ "compare_manifests",
39
+ ]