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,311 @@
|
|
|
1
|
+
"""Turning a taskset reference into a lock. Spec section 21.3.
|
|
2
|
+
|
|
3
|
+
A ``TasksetLock`` is the answer to one question: given this engine and this
|
|
4
|
+
reference, which tasks *are* the Climb? The answer has to be the same for the
|
|
5
|
+
publisher and for every participant, so this module is built around three
|
|
6
|
+
refusals.
|
|
7
|
+
|
|
8
|
+
*Ask twice, in two fresh processes.* One inspection tells you what a taskset
|
|
9
|
+
loaded as; two tell you whether loading it is deterministic. A taskset that
|
|
10
|
+
draws from a random source, iterates a set, or depends on dictionary order will
|
|
11
|
+
usually agree with itself inside one process and disagree across two, which is
|
|
12
|
+
exactly why the second run is a new interpreter rather than a second call.
|
|
13
|
+
|
|
14
|
+
*Count and uniqueness are required, not observed.* A selection asking for 36
|
|
15
|
+
tasks and receiving 35 is not a smaller Climb, it is a different one; a
|
|
16
|
+
membership naming the same task twice would score it twice under one
|
|
17
|
+
commitment.
|
|
18
|
+
|
|
19
|
+
*The package the engine actually holds is what gets locked.* Decisions document
|
|
20
|
+
0003 A8 requires three values to be equal — the Campaign's
|
|
21
|
+
``taskset.ref.package.digest``, the engine descriptor's ``source_digest``, and
|
|
22
|
+
the lock's ``resolved_package_digest``. This module recomputes the digest from
|
|
23
|
+
the installed source tree rather than copying either declaration, so the
|
|
24
|
+
equality is a fact about the files on disk instead of a fact about two
|
|
25
|
+
documents agreeing with each other.
|
|
26
|
+
|
|
27
|
+
Nothing in the lock is a timestamp, a path, or a process detail. Resolving the
|
|
28
|
+
same reference against the same engine twice produces byte-identical bytes, and
|
|
29
|
+
that is what lets a participant's lock and the publisher's lock be compared by
|
|
30
|
+
digest instead of by narrative.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import tempfile
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Final
|
|
38
|
+
|
|
39
|
+
from techtree.canonical import validate_digest
|
|
40
|
+
from techtree.constants import TASKSET_LOCK_SCHEMA_VERSION
|
|
41
|
+
from techtree.engines.bundle import (
|
|
42
|
+
PACKAGES_DIRECTORY,
|
|
43
|
+
package_source_digest,
|
|
44
|
+
read_engine_descriptor,
|
|
45
|
+
)
|
|
46
|
+
from techtree.engines.registry import EngineRegistry
|
|
47
|
+
from techtree.engines.runner import EngineRunner
|
|
48
|
+
from techtree.errors import EngineError, ValidationError, VerificationError
|
|
49
|
+
from techtree.models.base import Digest
|
|
50
|
+
from techtree.models.campaign import TaskSelection, TasksetRef
|
|
51
|
+
from techtree.models.validation import TasksetLock
|
|
52
|
+
from techtree.tasksets.membership import (
|
|
53
|
+
MEMBERSHIP_REPEATABILITY_CHECK,
|
|
54
|
+
TasksetInspection,
|
|
55
|
+
assert_unique_task_hashes,
|
|
56
|
+
compare_membership,
|
|
57
|
+
load_inspection_output,
|
|
58
|
+
membership_digest,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"INSPECTION_TIMEOUT_SECONDS",
|
|
63
|
+
"INSPECT_TASKSET_TOOL",
|
|
64
|
+
"SUPPORTED_CONFIG_KEYS",
|
|
65
|
+
"TasksetResolver",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
#: The engine helper that reports task identity. It lives inside the digested
|
|
69
|
+
#: bundle (decisions document 0003 A3) and is addressed through the registry,
|
|
70
|
+
#: never as a path a caller supplies.
|
|
71
|
+
INSPECT_TASKSET_TOOL: Final = "inspect_taskset.py"
|
|
72
|
+
|
|
73
|
+
#: Loading a taskset imports Verifiers and the reference package, which is slow
|
|
74
|
+
#: on a cold filesystem cache and instant afterwards. The limit exists to end a
|
|
75
|
+
#: hung process, not to police performance.
|
|
76
|
+
INSPECTION_TIMEOUT_SECONDS: Final = 300.0
|
|
77
|
+
|
|
78
|
+
#: The only taskset configuration WP5 can honor. The helper takes the task count
|
|
79
|
+
#: as an argument, so a configuration may restate it and may say nothing else:
|
|
80
|
+
#: a key nothing applies would be a setting that silently does not happen.
|
|
81
|
+
SUPPORTED_CONFIG_KEYS: Final[tuple[str, ...]] = ("num_tasks",)
|
|
82
|
+
|
|
83
|
+
_OUTPUT_FILENAME: Final = "inspection.json"
|
|
84
|
+
_TEMPORARY_PREFIX: Final = "techtree-inspect-"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TasksetResolver:
|
|
88
|
+
"""Resolves one taskset reference against one installed engine."""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
registry: EngineRegistry,
|
|
93
|
+
engine_digest: Digest,
|
|
94
|
+
*,
|
|
95
|
+
timeout_seconds: float = INSPECTION_TIMEOUT_SECONDS,
|
|
96
|
+
) -> None:
|
|
97
|
+
self._registry = registry
|
|
98
|
+
self._engine_digest = validate_digest(engine_digest)
|
|
99
|
+
self._runner = EngineRunner(registry, self._engine_digest)
|
|
100
|
+
self._timeout_seconds = timeout_seconds
|
|
101
|
+
|
|
102
|
+
def resolve(
|
|
103
|
+
self,
|
|
104
|
+
*,
|
|
105
|
+
taskset_ref: TasksetRef,
|
|
106
|
+
selection: TaskSelection,
|
|
107
|
+
) -> TasksetLock:
|
|
108
|
+
"""Load the taskset twice, enforce determinism, and build the lock."""
|
|
109
|
+
_require_supported_reference(taskset_ref, selection)
|
|
110
|
+
resolved_package_digest = self._package_digest(taskset_ref)
|
|
111
|
+
|
|
112
|
+
first = self._inspect(taskset_ref, selection)
|
|
113
|
+
second = self._inspect(taskset_ref, selection)
|
|
114
|
+
|
|
115
|
+
repeatability = compare_membership(
|
|
116
|
+
second.ordered_task_hashes,
|
|
117
|
+
first.ordered_task_hashes,
|
|
118
|
+
check_id=MEMBERSHIP_REPEATABILITY_CHECK,
|
|
119
|
+
)
|
|
120
|
+
if repeatability.status != "passed":
|
|
121
|
+
raise VerificationError(
|
|
122
|
+
f"taskset {taskset_ref.id} produced a different membership on a "
|
|
123
|
+
f"second load, so it cannot be locked: {repeatability.detail}",
|
|
124
|
+
code="taskset_membership_unstable",
|
|
125
|
+
details={
|
|
126
|
+
"taskset_id": taskset_ref.id,
|
|
127
|
+
"check": repeatability.id,
|
|
128
|
+
"detail": repeatability.detail,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
ordered_task_hashes = first.ordered_task_hashes
|
|
133
|
+
if len(ordered_task_hashes) != selection.num_tasks:
|
|
134
|
+
raise VerificationError(
|
|
135
|
+
f"taskset {taskset_ref.id} resolved to "
|
|
136
|
+
f"{len(ordered_task_hashes)} tasks but the selection asks for "
|
|
137
|
+
f"{selection.num_tasks}",
|
|
138
|
+
code="taskset_task_count_unexpected",
|
|
139
|
+
details={
|
|
140
|
+
"taskset_id": taskset_ref.id,
|
|
141
|
+
"resolved": len(ordered_task_hashes),
|
|
142
|
+
"expected": selection.num_tasks,
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
assert_unique_task_hashes(ordered_task_hashes)
|
|
146
|
+
|
|
147
|
+
return TasksetLock(
|
|
148
|
+
schema_version=TASKSET_LOCK_SCHEMA_VERSION,
|
|
149
|
+
taskset_ref=taskset_ref,
|
|
150
|
+
engine_digest=self._engine_digest,
|
|
151
|
+
resolved_package_digest=resolved_package_digest,
|
|
152
|
+
ordered_task_hashes=ordered_task_hashes,
|
|
153
|
+
membership_digest=membership_digest(ordered_task_hashes),
|
|
154
|
+
task_count=len(ordered_task_hashes),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# -- one inspection ----------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def _inspect(
|
|
160
|
+
self, taskset_ref: TasksetRef, selection: TaskSelection
|
|
161
|
+
) -> TasksetInspection:
|
|
162
|
+
"""Run the engine helper once, in its own process, and read its report."""
|
|
163
|
+
script = self._registry.tool_path(self._engine_digest, INSPECT_TASKSET_TOOL)
|
|
164
|
+
|
|
165
|
+
with tempfile.TemporaryDirectory(prefix=_TEMPORARY_PREFIX) as directory:
|
|
166
|
+
output = Path(directory) / _OUTPUT_FILENAME
|
|
167
|
+
result = self._runner.run_python_script(
|
|
168
|
+
script,
|
|
169
|
+
[
|
|
170
|
+
"--taskset-id",
|
|
171
|
+
taskset_ref.id,
|
|
172
|
+
"--num-tasks",
|
|
173
|
+
str(selection.num_tasks),
|
|
174
|
+
"--output",
|
|
175
|
+
str(output),
|
|
176
|
+
],
|
|
177
|
+
timeout=self._timeout_seconds,
|
|
178
|
+
)
|
|
179
|
+
if result.exit_code != 0:
|
|
180
|
+
raise EngineError(
|
|
181
|
+
f"the engine could not inspect taskset {taskset_ref.id}: "
|
|
182
|
+
f"{_last_line(result.stderr)}",
|
|
183
|
+
code="taskset_inspection_failed",
|
|
184
|
+
details={
|
|
185
|
+
"taskset_id": taskset_ref.id,
|
|
186
|
+
"engine_digest": self._engine_digest,
|
|
187
|
+
"exit_code": result.exit_code,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
inspection = load_inspection_output(output)
|
|
191
|
+
|
|
192
|
+
if inspection.taskset_id != taskset_ref.id:
|
|
193
|
+
raise VerificationError(
|
|
194
|
+
f"the engine inspected {inspection.taskset_id} but "
|
|
195
|
+
f"{taskset_ref.id} was asked for",
|
|
196
|
+
code="taskset_identity_mismatch",
|
|
197
|
+
details={
|
|
198
|
+
"requested": taskset_ref.id,
|
|
199
|
+
"inspected": inspection.taskset_id,
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
return inspection
|
|
203
|
+
|
|
204
|
+
# -- the package the engine holds --------------------------------------
|
|
205
|
+
|
|
206
|
+
def _package_digest(self, taskset_ref: TasksetRef) -> Digest:
|
|
207
|
+
"""Return the source digest of the installed package, having checked it.
|
|
208
|
+
|
|
209
|
+
Decisions document 0003 A8. Two of the three required equalities are
|
|
210
|
+
settled here, against the recomputed tree: the engine descriptor's
|
|
211
|
+
declaration and the Campaign's commitment. The third is the lock's own
|
|
212
|
+
field, which is this return value.
|
|
213
|
+
"""
|
|
214
|
+
engine_root = self._registry.path(self._engine_digest)
|
|
215
|
+
package_name = taskset_ref.package.name
|
|
216
|
+
package_root = engine_root / PACKAGES_DIRECTORY / package_name
|
|
217
|
+
|
|
218
|
+
if not package_root.is_dir():
|
|
219
|
+
raise EngineError(
|
|
220
|
+
f"engine {self._engine_digest} ships no package named {package_name}",
|
|
221
|
+
code="engine_package_missing",
|
|
222
|
+
details={
|
|
223
|
+
"engine_digest": self._engine_digest,
|
|
224
|
+
"package": package_name,
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
resolved = package_source_digest(package_root)
|
|
229
|
+
declared = _declared_source_digest(engine_root, package_name)
|
|
230
|
+
|
|
231
|
+
if declared != resolved:
|
|
232
|
+
raise VerificationError(
|
|
233
|
+
f"the {package_name} source tree inside engine "
|
|
234
|
+
f"{self._engine_digest} hashes to {resolved}, but the engine "
|
|
235
|
+
f"descriptor declares {declared}",
|
|
236
|
+
code="engine_package_digest_mismatch",
|
|
237
|
+
details={
|
|
238
|
+
"package": package_name,
|
|
239
|
+
"resolved": resolved,
|
|
240
|
+
"declared": declared,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
if taskset_ref.package.digest != resolved:
|
|
244
|
+
raise VerificationError(
|
|
245
|
+
f"the Campaign commits to {package_name} at "
|
|
246
|
+
f"{taskset_ref.package.digest}, but this engine holds {resolved}",
|
|
247
|
+
code="taskset_package_digest_mismatch",
|
|
248
|
+
details={
|
|
249
|
+
"package": package_name,
|
|
250
|
+
"committed": taskset_ref.package.digest,
|
|
251
|
+
"resolved": resolved,
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
return resolved
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _declared_source_digest(engine_root: Path, package_name: str) -> Digest:
|
|
258
|
+
"""Return what the installed engine's descriptor says one package hashes to."""
|
|
259
|
+
descriptor = read_engine_descriptor(engine_root)
|
|
260
|
+
for package in descriptor.packages:
|
|
261
|
+
if package.name == package_name:
|
|
262
|
+
return package.source_digest
|
|
263
|
+
raise EngineError(
|
|
264
|
+
f"the engine descriptor lists no package named {package_name}",
|
|
265
|
+
code="engine_package_missing",
|
|
266
|
+
details={
|
|
267
|
+
"package": package_name,
|
|
268
|
+
"available": [entry.name for entry in descriptor.packages],
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _require_supported_reference(
|
|
274
|
+
taskset_ref: TasksetRef, selection: TaskSelection
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Refuse a reference this work package cannot resolve honestly."""
|
|
277
|
+
if taskset_ref.package.kind != "embedded":
|
|
278
|
+
raise ValidationError(
|
|
279
|
+
f"WP5 resolves the embedded reference package only, not a "
|
|
280
|
+
f"{taskset_ref.package.kind} package",
|
|
281
|
+
code="taskset_package_kind_unsupported",
|
|
282
|
+
details={"kind": taskset_ref.package.kind, "package": taskset_ref.id},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
unsupported = sorted(set(taskset_ref.config) - set(SUPPORTED_CONFIG_KEYS))
|
|
286
|
+
if unsupported:
|
|
287
|
+
raise ValidationError(
|
|
288
|
+
"the taskset configuration sets "
|
|
289
|
+
+ ", ".join(unsupported)
|
|
290
|
+
+ ", and nothing in WP5 would apply it",
|
|
291
|
+
code="taskset_config_unsupported",
|
|
292
|
+
details={
|
|
293
|
+
"unsupported_keys": list(unsupported),
|
|
294
|
+
"supported_keys": list(SUPPORTED_CONFIG_KEYS),
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
configured = taskset_ref.config.get("num_tasks")
|
|
299
|
+
if configured is not None and configured != selection.num_tasks:
|
|
300
|
+
raise ValidationError(
|
|
301
|
+
f"the taskset configuration asks for {configured} tasks but the "
|
|
302
|
+
f"selection asks for {selection.num_tasks}",
|
|
303
|
+
code="taskset_config_conflict",
|
|
304
|
+
details={"config": configured, "selection": selection.num_tasks},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _last_line(text: str) -> str:
|
|
309
|
+
"""Return the last meaningful line of a child process's diagnostics."""
|
|
310
|
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
311
|
+
return lines[-1] if lines else "the engine reported no diagnostics"
|