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,1242 @@
|
|
|
1
|
+
"""Turning one signed report into one neutral payload. Spec section 7.14.
|
|
2
|
+
|
|
3
|
+
Everything a reader is shown comes from here, and everything here comes from
|
|
4
|
+
the signed report and the signed receipts it commits to. That is the whole
|
|
5
|
+
design: a renderer receives numbers it cannot recompute, statuses it cannot
|
|
6
|
+
soften, and caveats it cannot drop, so two channels showing the same run cannot
|
|
7
|
+
say different things about it.
|
|
8
|
+
|
|
9
|
+
Four rules shape what is built.
|
|
10
|
+
|
|
11
|
+
*The report decides, the builder describes.* No score is recomputed, no verdict
|
|
12
|
+
is re-derived, and no status is interpreted. ``decision``, ``proof_grade`` and
|
|
13
|
+
the comparison status are copied; the caveats are the plain-English reading of
|
|
14
|
+
those same fields rather than a second opinion about them.
|
|
15
|
+
|
|
16
|
+
*Warnings are carried, never smoothed.* A real comparison in this build is
|
|
17
|
+
usually ``controlled_with_warnings`` — a provider that publishes no model
|
|
18
|
+
revision, a daemon that reports no resolved image digest — and that is a fact
|
|
19
|
+
about how strongly the run is attested. It arrives as a caveat with warning
|
|
20
|
+
severity, in front of the reader, in every channel.
|
|
21
|
+
|
|
22
|
+
*A rejected candidate is a measurement.* The decision is stated as what it is.
|
|
23
|
+
A Skill that did not clear the Campaign's declared threshold produced evidence
|
|
24
|
+
about that Skill, and framing it as a failure of the run would be both
|
|
25
|
+
discouraging and wrong.
|
|
26
|
+
|
|
27
|
+
*Nothing is offered that does not exist.* ``next_actions`` names commands this
|
|
28
|
+
build actually has. Section 7.14's four steps are all present for a graded
|
|
29
|
+
result: look at the tasks, export the context a host agent proposes a revision
|
|
30
|
+
from, prepare the v1-against-v2 comparison, and verify the local proof. The
|
|
31
|
+
reasoning turn itself is WP9+'s, so what is offered is the deterministic
|
|
32
|
+
command that produces its input rather than a promise to think.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from collections.abc import Sequence
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from typing import Final, Literal
|
|
40
|
+
|
|
41
|
+
from techtree.errors import PrerequisiteError
|
|
42
|
+
from techtree.identity.models import VerificationResult
|
|
43
|
+
from techtree.models.campaign import CampaignSpec
|
|
44
|
+
from techtree.models.cli import NextAction
|
|
45
|
+
from techtree.models.episode_receipt import EpisodeReceipt
|
|
46
|
+
from techtree.models.skill import SkillArtifact
|
|
47
|
+
from techtree.models.uplift_report import (
|
|
48
|
+
ComparisonStatus,
|
|
49
|
+
TaskDelta,
|
|
50
|
+
UpliftDecision,
|
|
51
|
+
UpliftReport,
|
|
52
|
+
)
|
|
53
|
+
from techtree.presentation.evidence import RecordedEvidence
|
|
54
|
+
from techtree.presentation.models import (
|
|
55
|
+
PRESENTATION_SCHEMA_VERSION,
|
|
56
|
+
DerivedCost,
|
|
57
|
+
EconomicsSource,
|
|
58
|
+
PresentationCaveat,
|
|
59
|
+
ScoreBar,
|
|
60
|
+
SkillSummary,
|
|
61
|
+
TaskOutcome,
|
|
62
|
+
TaskResultRow,
|
|
63
|
+
UpliftPresentationPayload,
|
|
64
|
+
)
|
|
65
|
+
from techtree.presentation.sanitize import (
|
|
66
|
+
ensure_no_hidden_task_material,
|
|
67
|
+
sanitize_label,
|
|
68
|
+
)
|
|
69
|
+
from techtree.publication.offer import publish_action
|
|
70
|
+
from techtree.receipts.compare import (
|
|
71
|
+
MODEL_REVISION_UNDISCOVERABLE,
|
|
72
|
+
weaker_claim_warnings,
|
|
73
|
+
)
|
|
74
|
+
from techtree.receipts.execution import (
|
|
75
|
+
ComparisonExecutionRecord,
|
|
76
|
+
CostProvenance,
|
|
77
|
+
TotalCost,
|
|
78
|
+
VariantUsage,
|
|
79
|
+
)
|
|
80
|
+
from techtree.verifiers.budget import price_profile_for
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"BASELINE_SKILL_LABEL",
|
|
84
|
+
"FIRST_CHANGE_LABEL",
|
|
85
|
+
"FIRST_RESULT_LABEL",
|
|
86
|
+
"HELD_FIXED_LINE",
|
|
87
|
+
"LATER_RESULT_LABEL",
|
|
88
|
+
"NOT_BROAD_CAPABILITY_LINE",
|
|
89
|
+
"P1_MEANING",
|
|
90
|
+
"SCORE_BAR_WIDTH",
|
|
91
|
+
"SECOND_CHANGE_LABEL",
|
|
92
|
+
"SECOND_RESULT_LABEL",
|
|
93
|
+
"VERIFICATION_FAILED",
|
|
94
|
+
"VERIFICATION_NOT_VERIFIED",
|
|
95
|
+
"VERIFICATION_VERIFIED",
|
|
96
|
+
"build_uplift_presentation",
|
|
97
|
+
"cost_explanation",
|
|
98
|
+
"cost_summary",
|
|
99
|
+
"decision_headline",
|
|
100
|
+
"efficiency_sentence",
|
|
101
|
+
"score_bars",
|
|
102
|
+
"solved_line",
|
|
103
|
+
"task_count_line",
|
|
104
|
+
"task_counts",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
#: What a Skill-insertion comparison measures against. Not an absent value,
|
|
108
|
+
#: and not what a replacement measures against: a replacement's baseline is a
|
|
109
|
+
#: Skill, and it is called by its own name.
|
|
110
|
+
BASELINE_SKILL_LABEL: Final = "No tested Skill"
|
|
111
|
+
|
|
112
|
+
#: What each result is called. Decisions document 0009 fixes the spellings: the
|
|
113
|
+
#: first result is the receipt for adding a Skill, and a later one is the same
|
|
114
|
+
#: comparison run again with a revised Skill. Which of them a reader is holding
|
|
115
|
+
#: is worked out from the Skills the run itself carries, never assumed — a
|
|
116
|
+
#: third comparison labelled as the second would be a receipt for work nobody
|
|
117
|
+
#: did.
|
|
118
|
+
FIRST_RESULT_LABEL: Final = "Hello World Uplift Receipt"
|
|
119
|
+
SECOND_RESULT_LABEL: Final = "Hello World — Iteration 2"
|
|
120
|
+
LATER_RESULT_LABEL: Final = "Hello World — A Later Iteration"
|
|
121
|
+
|
|
122
|
+
#: The other half of decisions document 0019 section 3's second statement, and
|
|
123
|
+
#: the whole of its first: one changed Skill, and everything else the same. It
|
|
124
|
+
#: is a constant rather than a sentence each renderer writes, because a channel
|
|
125
|
+
#: that made a weaker version of this claim than another channel would be the
|
|
126
|
+
#: one somebody quoted.
|
|
127
|
+
HELD_FIXED_LINE: Final = (
|
|
128
|
+
"Everything else was the same on both sides: the same model sampled the "
|
|
129
|
+
"same way, the same harness and tools, the same runtime image, the same "
|
|
130
|
+
"tasks in the same order, the same reward, and the same declared limits."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
#: What changed, in the words decisions document 0019 section 1 fixes. The two
|
|
134
|
+
#: sides are named as roles rather than by digest, because the digests are
|
|
135
|
+
#: printed underneath and a reader needs the shape of the comparison first.
|
|
136
|
+
FIRST_CHANGE_LABEL: Final = "No tested Skill → Skill v1"
|
|
137
|
+
SECOND_CHANGE_LABEL: Final = "Skill v1 → Skill v2"
|
|
138
|
+
|
|
139
|
+
#: Decisions document 0005 section 3.4, verbatim. The only words this build is
|
|
140
|
+
#: permitted to explain ``P1`` with.
|
|
141
|
+
P1_MEANING: Final = "integrity-bound, participant-attested local execution"
|
|
142
|
+
|
|
143
|
+
#: What each decision actually established, said as what was measured rather
|
|
144
|
+
#: than as a verdict that was won.
|
|
145
|
+
#:
|
|
146
|
+
#: The bar on this Climb is to beat a baseline of zero by any margin at all, on
|
|
147
|
+
#: a synthetic task family, at proof grade P1, with no publication eligibility.
|
|
148
|
+
#: "Accepted" and "met the threshold" are both literally true of that and both
|
|
149
|
+
#: read as a benchmark somebody passed, which is the one thing this result is
|
|
150
|
+
#: not. So the accepted case says what it can defend — that the Skill improved
|
|
151
|
+
#: on this particular task family — and the standing line underneath says what
|
|
152
|
+
#: it cannot.
|
|
153
|
+
#:
|
|
154
|
+
#: An accepted decision always means the candidate out-scored the baseline:
|
|
155
|
+
#: :func:`~techtree.receipts.uplift.decide_uplift` reaches it only through a
|
|
156
|
+
#: positive delta. A rejected one may still have moved the score a little, so
|
|
157
|
+
#: it is described by the bar it fell under rather than as no improvement.
|
|
158
|
+
DECISION_HEADLINE: Final[dict[str, str]] = {
|
|
159
|
+
"accepted": "Improved on this development task family",
|
|
160
|
+
"rejected": "Did not clear the bar this Climb declared",
|
|
161
|
+
"inconclusive": "Not decided: this Climb declared no rule that could decide it",
|
|
162
|
+
"invalid": "Not valid: this comparison cannot carry a result",
|
|
163
|
+
"development_only": "Development-only: this report states no verdict",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#: The sentence that stops any of the headlines above being read as a claim
|
|
167
|
+
#: about what the Skill can do in general. It is not a footer: decisions
|
|
168
|
+
#: document 0013's toy/synthetic boundary belongs in the first lines, beside
|
|
169
|
+
#: the number a reader would otherwise quote on its own.
|
|
170
|
+
NOT_BROAD_CAPABILITY_LINE: Final = "Not broad-capability evidence"
|
|
171
|
+
|
|
172
|
+
#: How wide a score bar is drawn, in cells. Fixed so two bars are comparable
|
|
173
|
+
#: and so a rendering is the same width on every terminal.
|
|
174
|
+
SCORE_BAR_WIDTH: Final = 24
|
|
175
|
+
|
|
176
|
+
VERIFICATION_VERIFIED: Final = "verified_offline"
|
|
177
|
+
VERIFICATION_FAILED: Final = "verification_failed"
|
|
178
|
+
VERIFICATION_NOT_VERIFIED: Final = "not_verified"
|
|
179
|
+
|
|
180
|
+
_FILLED: Final = "█"
|
|
181
|
+
_EMPTY: Final = "·"
|
|
182
|
+
|
|
183
|
+
#: What one task is worth when it is got right. The rewards this build measures
|
|
184
|
+
#: are all-or-nothing, and a headline count is only offered for a reward that
|
|
185
|
+
#: is: see :func:`_tasks_scored_full`.
|
|
186
|
+
_FULL_SCORE: Final = 1.0
|
|
187
|
+
|
|
188
|
+
#: The unit the recorded prices are quoted in.
|
|
189
|
+
_TOKENS_PER_MILLION: Final = 1_000_000.0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build_uplift_presentation(
|
|
193
|
+
*,
|
|
194
|
+
report: UpliftReport,
|
|
195
|
+
campaign: CampaignSpec,
|
|
196
|
+
baseline_receipts: Sequence[EpisodeReceipt],
|
|
197
|
+
candidate_receipts: Sequence[EpisodeReceipt],
|
|
198
|
+
campaign_title: str,
|
|
199
|
+
baseline_skill: SkillArtifact | None,
|
|
200
|
+
candidate_skill: SkillArtifact,
|
|
201
|
+
verification: VerificationResult | None,
|
|
202
|
+
execution_record: ComparisonExecutionRecord | None = None,
|
|
203
|
+
recorded_evidence: RecordedEvidence | None = None,
|
|
204
|
+
) -> UpliftPresentationPayload:
|
|
205
|
+
"""Build a channel-neutral presentation payload from a signed report.
|
|
206
|
+
|
|
207
|
+
``verification`` is the result of checking the run's proof bundle offline,
|
|
208
|
+
or ``None`` when the reader asked not to check it. The three states are
|
|
209
|
+
kept apart in ``verification_status`` because "not checked" and "checked
|
|
210
|
+
and failed" are different things to tell somebody, and a development-only
|
|
211
|
+
report has no proof to check at all.
|
|
212
|
+
|
|
213
|
+
``execution_record`` is decisions document 0007 R6's signed operational
|
|
214
|
+
record, when the run carries one. It is the only source of a *reported*
|
|
215
|
+
cost figure and the preferred source of tokens and timing; a payload built
|
|
216
|
+
without one says so in ``economics_source`` and carries the
|
|
217
|
+
operational-evidence caveat rather than a number nobody signed.
|
|
218
|
+
|
|
219
|
+
``campaign`` is the run's own signed Campaign. It says which model was
|
|
220
|
+
measured, which is what lets a cost be worked out from the prices this
|
|
221
|
+
release recorded and what lets the weaker-claim warning name its
|
|
222
|
+
coordinate instead of gesturing at one.
|
|
223
|
+
|
|
224
|
+
``recorded_evidence`` is what
|
|
225
|
+
:func:`~techtree.presentation.evidence.read_recorded_evidence` read back
|
|
226
|
+
out of the run's own digest-checked files, when they are still there. It
|
|
227
|
+
adds nothing to any artifact; it is counted while the result is drawn.
|
|
228
|
+
"""
|
|
229
|
+
economics = _economics(execution_record, baseline_receipts, candidate_receipts)
|
|
230
|
+
task_rows = _task_rows(report.task_deltas)
|
|
231
|
+
scored_full = _tasks_scored_full(task_rows)
|
|
232
|
+
seen = recorded_evidence
|
|
233
|
+
baseline_seen = None if seen is None else seen.baseline
|
|
234
|
+
candidate_seen = None if seen is None else seen.candidate
|
|
235
|
+
derived = _derived_cost(campaign, execution_record, economics)
|
|
236
|
+
unavailable = (
|
|
237
|
+
None
|
|
238
|
+
if derived is not None or economics.cost.cost_usd is not None
|
|
239
|
+
else _cost_unavailable_reason(campaign, execution_record, economics)
|
|
240
|
+
)
|
|
241
|
+
generation = _generation(baseline_skill)
|
|
242
|
+
primary = report.primary_result
|
|
243
|
+
payload = UpliftPresentationPayload(
|
|
244
|
+
schema_version=PRESENTATION_SCHEMA_VERSION,
|
|
245
|
+
run_id=report.run_id,
|
|
246
|
+
campaign_title=sanitize_label(campaign_title),
|
|
247
|
+
comparison_label=_comparison_label(generation),
|
|
248
|
+
change_label=_change_label(generation, baseline_skill, candidate_skill),
|
|
249
|
+
baseline_skill=_skill_summary(baseline_skill, _baseline_label(baseline_skill)),
|
|
250
|
+
candidate_skill=_skill_summary(candidate_skill, candidate_skill.name),
|
|
251
|
+
baseline_score=primary.baseline_mean,
|
|
252
|
+
candidate_score=primary.candidate_mean,
|
|
253
|
+
absolute_delta=primary.absolute_delta,
|
|
254
|
+
relative_delta=primary.relative_delta,
|
|
255
|
+
wins=primary.wins,
|
|
256
|
+
losses=primary.losses,
|
|
257
|
+
ties=primary.ties,
|
|
258
|
+
task_rows=task_rows,
|
|
259
|
+
baseline_tasks_scored_full=scored_full[0],
|
|
260
|
+
candidate_tasks_scored_full=scored_full[1],
|
|
261
|
+
baseline_tokens=economics.baseline_tokens,
|
|
262
|
+
candidate_tokens=economics.candidate_tokens,
|
|
263
|
+
baseline_seconds=economics.baseline_seconds,
|
|
264
|
+
candidate_seconds=economics.candidate_seconds,
|
|
265
|
+
baseline_model_turns=(
|
|
266
|
+
None if baseline_seen is None else baseline_seen.model_turns
|
|
267
|
+
),
|
|
268
|
+
candidate_model_turns=(
|
|
269
|
+
None if candidate_seen is None else candidate_seen.model_turns
|
|
270
|
+
),
|
|
271
|
+
baseline_rate_limited_calls=(
|
|
272
|
+
None if baseline_seen is None else baseline_seen.rate_limited_calls
|
|
273
|
+
),
|
|
274
|
+
candidate_rate_limited_calls=(
|
|
275
|
+
None if candidate_seen is None else candidate_seen.rate_limited_calls
|
|
276
|
+
),
|
|
277
|
+
every_rollout_completed=(
|
|
278
|
+
None if seen is None else seen.every_rollout_completed
|
|
279
|
+
),
|
|
280
|
+
economics_source=economics.source,
|
|
281
|
+
cost_usd=economics.cost.cost_usd,
|
|
282
|
+
cost_provenance=economics.cost.provenance,
|
|
283
|
+
derived_cost=derived,
|
|
284
|
+
cost_unavailable_reason=unavailable,
|
|
285
|
+
decision=report.decision.value,
|
|
286
|
+
proof_grade=report.proof_grade,
|
|
287
|
+
verification_status=_verification_status(verification),
|
|
288
|
+
caveats=_caveats(
|
|
289
|
+
report=report,
|
|
290
|
+
campaign=campaign,
|
|
291
|
+
verification=verification,
|
|
292
|
+
economics=economics,
|
|
293
|
+
recorded_evidence=recorded_evidence,
|
|
294
|
+
derived=derived,
|
|
295
|
+
),
|
|
296
|
+
next_actions=_next_actions(
|
|
297
|
+
report.run_id,
|
|
298
|
+
report.proof_grade,
|
|
299
|
+
# Publishing is offered for a run whose proof was checked here and
|
|
300
|
+
# held together, and for no other run. A result nobody verified is
|
|
301
|
+
# not a result anybody should be invited to send anywhere.
|
|
302
|
+
verified=verification is not None and verification.verified,
|
|
303
|
+
publication_eligible=report.publication_eligible,
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
ensure_no_hidden_task_material(payload)
|
|
307
|
+
return payload
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def task_counts(payload: UpliftPresentationPayload) -> tuple[int, int, int] | None:
|
|
311
|
+
"""Return each side's count and the total, in the unit a person counts in.
|
|
312
|
+
|
|
313
|
+
A mean is what the report holds and a count is what a reader thinks in, and
|
|
314
|
+
the two are the same fact for an all-or-nothing reward. A reward that is not
|
|
315
|
+
all-or-nothing has no such count, and this returns nothing rather than
|
|
316
|
+
rounding one into existence.
|
|
317
|
+
|
|
318
|
+
The three numbers are returned rather than one sentence because the two
|
|
319
|
+
channels have room for different words around them and must never be free
|
|
320
|
+
to disagree about the numbers themselves.
|
|
321
|
+
"""
|
|
322
|
+
baseline = payload.baseline_tasks_scored_full
|
|
323
|
+
candidate = payload.candidate_tasks_scored_full
|
|
324
|
+
if baseline is None or candidate is None:
|
|
325
|
+
return None
|
|
326
|
+
return baseline, candidate, len(payload.task_rows)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def task_count_line(payload: UpliftPresentationPayload) -> str | None:
|
|
330
|
+
"""Return the count, both sides and the movement, as one line."""
|
|
331
|
+
counted = task_counts(payload)
|
|
332
|
+
if counted is None:
|
|
333
|
+
return None
|
|
334
|
+
baseline, candidate, total = counted
|
|
335
|
+
return f"{baseline} of {total} → {candidate} of {total} ({candidate - baseline:+d})"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def decision_headline(payload: UpliftPresentationPayload) -> str:
|
|
339
|
+
"""Return what this comparison established, in one line."""
|
|
340
|
+
return DECISION_HEADLINE[payload.decision]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def solved_line(payload: UpliftPresentationPayload) -> str:
|
|
344
|
+
"""Return the count a reader can act on: solved, still failing, regressed.
|
|
345
|
+
|
|
346
|
+
In a Skill-insertion comparison the baseline scores nothing on every task,
|
|
347
|
+
so a tie means both sides failed the task rather than that nothing moved.
|
|
348
|
+
Reporting wins and ties would then read as a clean sweep while hiding the
|
|
349
|
+
most actionable fact in the run, which is how many tasks are still failing.
|
|
350
|
+
Wins, losses and ties stay in the per-task table and in the machine
|
|
351
|
+
envelope, where they are read by something that knows what they mean.
|
|
352
|
+
|
|
353
|
+
A reward that is not all-or-nothing carries no count of solved tasks (see
|
|
354
|
+
:func:`_tasks_scored_full`), and nothing is inferred in its absence: the
|
|
355
|
+
regressions are reported on their own rather than beside a number this
|
|
356
|
+
build would have had to invent.
|
|
357
|
+
"""
|
|
358
|
+
regressions = f"{payload.losses} regression{'' if payload.losses == 1 else 's'}"
|
|
359
|
+
counted = task_counts(payload)
|
|
360
|
+
if counted is None:
|
|
361
|
+
return regressions
|
|
362
|
+
_, candidate, total = counted
|
|
363
|
+
return (
|
|
364
|
+
f"Solved {candidate} of {total} · {total - candidate} still failing · "
|
|
365
|
+
f"{regressions}"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def efficiency_sentence(payload: UpliftPresentationPayload) -> str | None:
|
|
370
|
+
"""Return what the candidate saved against the baseline, on this run.
|
|
371
|
+
|
|
372
|
+
The two sides' turn counts, token totals and clocks are the most striking
|
|
373
|
+
measurement most runs produce, and a pair of numbers on adjacent lines is
|
|
374
|
+
not a finding. The difference is, so it is stated — and stated as a fact
|
|
375
|
+
about this one controlled comparison, because without that anchor the
|
|
376
|
+
percentages read as a general claim about what Skills do, which nothing
|
|
377
|
+
here measures.
|
|
378
|
+
|
|
379
|
+
Every figure needs both of its sides. A measurement one side did not record
|
|
380
|
+
is left out entirely rather than compared against a zero, and a side that
|
|
381
|
+
spent exactly what the other did contributes no difference to report. A
|
|
382
|
+
comparison with no differences left to report gets no sentence at all.
|
|
383
|
+
"""
|
|
384
|
+
turns = _saving(
|
|
385
|
+
payload.baseline_model_turns,
|
|
386
|
+
payload.candidate_model_turns,
|
|
387
|
+
fewer="took {count} fewer model turn{s}",
|
|
388
|
+
more="took {count} more model turn{s}",
|
|
389
|
+
)
|
|
390
|
+
tokens = _saving(
|
|
391
|
+
payload.baseline_tokens,
|
|
392
|
+
payload.candidate_tokens,
|
|
393
|
+
fewer="used {count} fewer token{s}",
|
|
394
|
+
more="used {count} more token{s}",
|
|
395
|
+
)
|
|
396
|
+
clock = _saving(
|
|
397
|
+
payload.baseline_seconds,
|
|
398
|
+
payload.candidate_seconds,
|
|
399
|
+
fewer="finished {count} seconds sooner",
|
|
400
|
+
more="finished {count} seconds later",
|
|
401
|
+
)
|
|
402
|
+
savings = [phrase for phrase in (turns, tokens, clock) if phrase is not None]
|
|
403
|
+
if not savings:
|
|
404
|
+
return None
|
|
405
|
+
counted = turns is not None or tokens is not None
|
|
406
|
+
return (
|
|
407
|
+
f"On this controlled run the Skill {_listed(savings)}. "
|
|
408
|
+
f"{_WORK_OR_WEATHER[counted, clock is not None]}"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
#: What the numbers above are properties of. Turn and token counts are
|
|
413
|
+
#: properties of the work and reproduce; a clock also reads this machine and
|
|
414
|
+
#: how busy the provider was that afternoon, and a reader deciding what to
|
|
415
|
+
#: repeat needs to know which of the two they are holding.
|
|
416
|
+
_WORK_OR_WEATHER: Final[dict[tuple[bool, bool], str]] = {
|
|
417
|
+
(True, True): (
|
|
418
|
+
"Those counts are properties of the work; how long each side took also "
|
|
419
|
+
"depends on this machine and on how busy the provider was."
|
|
420
|
+
),
|
|
421
|
+
(True, False): "Those counts are properties of the work.",
|
|
422
|
+
(False, True): (
|
|
423
|
+
"How long each side took depends on this machine and on how busy the "
|
|
424
|
+
"provider was, as well as on the work."
|
|
425
|
+
),
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _saving(
|
|
430
|
+
baseline: float | int | None,
|
|
431
|
+
candidate: float | int | None,
|
|
432
|
+
*,
|
|
433
|
+
fewer: str,
|
|
434
|
+
more: str,
|
|
435
|
+
) -> str | None:
|
|
436
|
+
"""Return one measured difference, or nothing where there is not one."""
|
|
437
|
+
if baseline is None or candidate is None or baseline == candidate:
|
|
438
|
+
return None
|
|
439
|
+
difference = abs(baseline - candidate)
|
|
440
|
+
count = f"{difference:,.1f}" if isinstance(difference, float) else f"{difference:,}"
|
|
441
|
+
phrase = (fewer if candidate < baseline else more).format(
|
|
442
|
+
count=count, s="" if difference == 1 else "s"
|
|
443
|
+
)
|
|
444
|
+
if not baseline:
|
|
445
|
+
return phrase
|
|
446
|
+
return f"{phrase} ({difference / baseline:.0%})"
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _listed(phrases: list[str]) -> str:
|
|
450
|
+
"""Join what was measured into one readable list."""
|
|
451
|
+
if len(phrases) == 1:
|
|
452
|
+
return phrases[0]
|
|
453
|
+
if len(phrases) == 2:
|
|
454
|
+
return f"{phrases[0]} and {phrases[1]}"
|
|
455
|
+
return f"{', '.join(phrases[:-1])}, and {phrases[-1]}"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _calls(count: int) -> str:
|
|
459
|
+
"""Return a count of model calls, in the number the count actually is."""
|
|
460
|
+
return f"{count:,} model call" if count == 1 else f"{count:,} model calls"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def cost_summary(payload: UpliftPresentationPayload) -> str:
|
|
464
|
+
"""Return the figure and, in the same breath, what kind of figure it is.
|
|
465
|
+
|
|
466
|
+
Decisions document 0007 R6: a number that was worked out and a number that
|
|
467
|
+
was billed are different claims, and the one word that tells them apart
|
|
468
|
+
travels with the number rather than somewhere below it.
|
|
469
|
+
"""
|
|
470
|
+
if payload.cost_usd is not None:
|
|
471
|
+
return f"${payload.cost_usd:.2f}, {_COST_PROVENANCE[payload.cost_provenance]}"
|
|
472
|
+
if payload.derived_cost is not None:
|
|
473
|
+
return f"about ${payload.derived_cost.usd:.2f}, worked out here, not billed"
|
|
474
|
+
return "unavailable"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def cost_explanation(payload: UpliftPresentationPayload) -> list[str]:
|
|
478
|
+
"""Return what a reader needs in order to judge the figure above it."""
|
|
479
|
+
if payload.cost_usd is not None:
|
|
480
|
+
return []
|
|
481
|
+
derived = payload.derived_cost
|
|
482
|
+
if derived is None:
|
|
483
|
+
assert payload.cost_unavailable_reason is not None
|
|
484
|
+
return [payload.cost_unavailable_reason]
|
|
485
|
+
lines = [
|
|
486
|
+
f"Computed from {derived.input_tokens:,} input and "
|
|
487
|
+
f"{derived.output_tokens:,} output tokens at the prices this release "
|
|
488
|
+
"recorded. Your provider's bill is what you actually pay."
|
|
489
|
+
]
|
|
490
|
+
cached = derived.cached_input_tokens
|
|
491
|
+
if cached and not derived.prices_name_a_cached_rate:
|
|
492
|
+
lines.append(
|
|
493
|
+
f"{cached:,} of those input tokens came back from the provider's "
|
|
494
|
+
"cache. The recorded prices name no separate rate for those, so "
|
|
495
|
+
"every token is priced at the full rate and the figure above is "
|
|
496
|
+
"on the high side."
|
|
497
|
+
)
|
|
498
|
+
return lines
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def score_bars(payload: UpliftPresentationPayload) -> list[ScoreBar]:
|
|
502
|
+
"""Return the two bars a renderer draws the headline with.
|
|
503
|
+
|
|
504
|
+
The scale is the larger of the two scores or one, so a reward that is
|
|
505
|
+
bounded at 1.0 draws against a full bar and one that is not stays
|
|
506
|
+
comparable between the two sides.
|
|
507
|
+
"""
|
|
508
|
+
maximum = max(1.0, payload.baseline_score, payload.candidate_score)
|
|
509
|
+
return [
|
|
510
|
+
ScoreBar(
|
|
511
|
+
label=label,
|
|
512
|
+
value=value,
|
|
513
|
+
maximum=maximum,
|
|
514
|
+
display=_bar(value, maximum),
|
|
515
|
+
)
|
|
516
|
+
for label, value in (
|
|
517
|
+
("Baseline", payload.baseline_score),
|
|
518
|
+
("Candidate", payload.candidate_score),
|
|
519
|
+
)
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ---------------------------------------------------------------------------
|
|
524
|
+
# The pieces
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _bar(value: float, maximum: float) -> str:
|
|
529
|
+
"""Return one fixed-width bar, plus the number it draws."""
|
|
530
|
+
filled = (
|
|
531
|
+
0
|
|
532
|
+
if value <= 0
|
|
533
|
+
else min(SCORE_BAR_WIDTH, round(SCORE_BAR_WIDTH * value / maximum))
|
|
534
|
+
)
|
|
535
|
+
return f"{_FILLED * filled}{_EMPTY * (SCORE_BAR_WIDTH - filled)} {value:.3f}"
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _generation(baseline_skill: SkillArtifact | None) -> int | None:
|
|
539
|
+
"""Return which comparison in the chain this is, or ``None`` when unknown.
|
|
540
|
+
|
|
541
|
+
A :class:`~techtree.models.skill.SkillArtifact` names the Skill it revises
|
|
542
|
+
and nothing further back, so the chain a run can see is the chain its own
|
|
543
|
+
two Skills record. That is enough to tell the first two apart, which is the
|
|
544
|
+
distinction that matters: a baseline carrying no Skill is the first
|
|
545
|
+
comparison, and a baseline carrying a Skill that revised nothing is the
|
|
546
|
+
second. A baseline that is itself a revision means the chain runs deeper
|
|
547
|
+
than this run can count, and the honest answer there is that the number is
|
|
548
|
+
not known rather than a number that happens to be two.
|
|
549
|
+
"""
|
|
550
|
+
if baseline_skill is None:
|
|
551
|
+
return 1
|
|
552
|
+
if baseline_skill.parent_skill_digest is None:
|
|
553
|
+
return 2
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _comparison_label(generation: int | None) -> str:
|
|
558
|
+
"""Return which result in the chain a reader is looking at.
|
|
559
|
+
|
|
560
|
+
Decisions document 0009 names the first two. Anything further along is
|
|
561
|
+
named as what it is — a later iteration — because a receipt that claimed
|
|
562
|
+
an ordinal the run cannot prove would be worse than one that does not
|
|
563
|
+
claim an ordinal at all. Which Skill sat on each side is spelled out under
|
|
564
|
+
"what changed" rather than compressed into this line.
|
|
565
|
+
"""
|
|
566
|
+
if generation == 1:
|
|
567
|
+
return FIRST_RESULT_LABEL
|
|
568
|
+
if generation == 2:
|
|
569
|
+
return SECOND_RESULT_LABEL
|
|
570
|
+
return LATER_RESULT_LABEL
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _change_label(
|
|
574
|
+
generation: int | None,
|
|
575
|
+
baseline_skill: SkillArtifact | None,
|
|
576
|
+
candidate_skill: SkillArtifact,
|
|
577
|
+
) -> str:
|
|
578
|
+
"""Return the one change this comparison measured, as an arrow.
|
|
579
|
+
|
|
580
|
+
Decisions document 0019 section 1 fixes the wording of the first two. Once
|
|
581
|
+
the chain runs deeper than the run can number, the two Skills are named by
|
|
582
|
+
their own names instead, which is always true of them.
|
|
583
|
+
"""
|
|
584
|
+
if generation == 1:
|
|
585
|
+
return FIRST_CHANGE_LABEL
|
|
586
|
+
if generation == 2:
|
|
587
|
+
return SECOND_CHANGE_LABEL
|
|
588
|
+
assert baseline_skill is not None
|
|
589
|
+
return (
|
|
590
|
+
f"{sanitize_label(baseline_skill.name)} → "
|
|
591
|
+
f"{sanitize_label(candidate_skill.name)}"
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _baseline_label(baseline_skill: SkillArtifact | None) -> str:
|
|
596
|
+
"""Return what the baseline side is called.
|
|
597
|
+
|
|
598
|
+
Decisions document 0019 section 1: a baseline is a role, not the absence of
|
|
599
|
+
a Skill. A replacement's baseline carries the Skill being revised, and
|
|
600
|
+
naming it "No tested Skill" beside that Skill's own digest would print a
|
|
601
|
+
contradiction.
|
|
602
|
+
"""
|
|
603
|
+
if baseline_skill is None:
|
|
604
|
+
return BASELINE_SKILL_LABEL
|
|
605
|
+
return baseline_skill.name
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _skill_summary(skill: SkillArtifact | None, label: str) -> SkillSummary:
|
|
609
|
+
"""Summarize one side's Skill by size and content address."""
|
|
610
|
+
if skill is None:
|
|
611
|
+
return SkillSummary(
|
|
612
|
+
label=sanitize_label(label), root_digest=None, file_count=0, total_bytes=0
|
|
613
|
+
)
|
|
614
|
+
return SkillSummary(
|
|
615
|
+
label=sanitize_label(label),
|
|
616
|
+
root_digest=skill.root_digest,
|
|
617
|
+
file_count=len(skill.files),
|
|
618
|
+
total_bytes=sum(file.size for file in skill.files),
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _task_rows(deltas: Sequence[TaskDelta]) -> list[TaskResultRow]:
|
|
623
|
+
"""Return one row per committed task, in the report's own order."""
|
|
624
|
+
return [
|
|
625
|
+
TaskResultRow(
|
|
626
|
+
position=position,
|
|
627
|
+
task_label=_task_label(position, delta.task_hash),
|
|
628
|
+
baseline_score=delta.baseline_reward,
|
|
629
|
+
candidate_score=delta.candidate_reward,
|
|
630
|
+
delta=delta.delta,
|
|
631
|
+
outcome=_outcome(delta),
|
|
632
|
+
)
|
|
633
|
+
for position, delta in enumerate(deltas)
|
|
634
|
+
]
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _task_label(position: int, task_hash: str) -> str:
|
|
638
|
+
"""Name one task by its place and the first cell of its hash.
|
|
639
|
+
|
|
640
|
+
The taskset's public names are not carried in a receipt, and a hash is not
|
|
641
|
+
hidden material: it is the same identifier the TasksetLock commits to. The
|
|
642
|
+
prefix is enough to tell two rows apart and to find the task in the lock.
|
|
643
|
+
"""
|
|
644
|
+
_, _, hexadecimal = task_hash.partition(":")
|
|
645
|
+
return f"task {position + 1:02d} · {hexadecimal[:8]}"
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _outcome(delta: TaskDelta) -> TaskOutcome:
|
|
649
|
+
"""Return which way one task moved."""
|
|
650
|
+
if delta.candidate_reward > delta.baseline_reward:
|
|
651
|
+
return "win"
|
|
652
|
+
if delta.candidate_reward < delta.baseline_reward:
|
|
653
|
+
return "loss"
|
|
654
|
+
return "tie"
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _tasks_scored_full(rows: Sequence[TaskResultRow]) -> tuple[int | None, int | None]:
|
|
658
|
+
"""Return how many tasks each side got right, when that is a countable thing.
|
|
659
|
+
|
|
660
|
+
A person reads "24 of 36", not "0.667". The translation is only honest for
|
|
661
|
+
a reward that is all-or-nothing: a task scored 0.4 was not got right and
|
|
662
|
+
was not got wrong, and counting it either way would invent a number the
|
|
663
|
+
report does not contain. So the count is offered only when every score on
|
|
664
|
+
both sides is exactly zero or exactly the full reward, and the mean stands
|
|
665
|
+
alone otherwise.
|
|
666
|
+
"""
|
|
667
|
+
if not rows:
|
|
668
|
+
return None, None
|
|
669
|
+
scores = [(row.baseline_score, row.candidate_score) for row in rows]
|
|
670
|
+
if any(value not in (0.0, _FULL_SCORE) for pair in scores for value in pair):
|
|
671
|
+
return None, None
|
|
672
|
+
return (
|
|
673
|
+
sum(1 for baseline, _ in scores if baseline == _FULL_SCORE),
|
|
674
|
+
sum(1 for _, candidate in scores if candidate == _FULL_SCORE),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@dataclass(frozen=True)
|
|
679
|
+
class _Economics:
|
|
680
|
+
"""What a payload can honestly say about cost and time, and from where."""
|
|
681
|
+
|
|
682
|
+
source: EconomicsSource
|
|
683
|
+
baseline_tokens: int | None
|
|
684
|
+
candidate_tokens: int | None
|
|
685
|
+
baseline_seconds: float | None
|
|
686
|
+
candidate_seconds: float | None
|
|
687
|
+
cost: TotalCost
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _economics(
|
|
691
|
+
record: ComparisonExecutionRecord | None,
|
|
692
|
+
baseline_receipts: Sequence[EpisodeReceipt],
|
|
693
|
+
candidate_receipts: Sequence[EpisodeReceipt],
|
|
694
|
+
) -> _Economics:
|
|
695
|
+
"""Decide what this payload may say about what the comparison consumed.
|
|
696
|
+
|
|
697
|
+
Decisions document 0007 R6 makes the signed execution record the source of
|
|
698
|
+
this, so it is preferred whenever there is one. Without it, the receipts
|
|
699
|
+
are asked for a token total — they carry one only if the evaluation
|
|
700
|
+
recorded tokens as a metric — and time and cost are simply not known. In
|
|
701
|
+
no branch is a number produced that this payload could not name a source
|
|
702
|
+
for.
|
|
703
|
+
"""
|
|
704
|
+
if record is not None:
|
|
705
|
+
return _Economics(
|
|
706
|
+
source="comparison_execution_record",
|
|
707
|
+
baseline_tokens=record.baseline.usage.total_tokens,
|
|
708
|
+
candidate_tokens=record.candidate.usage.total_tokens,
|
|
709
|
+
baseline_seconds=record.baseline.elapsed_seconds,
|
|
710
|
+
candidate_seconds=record.candidate.elapsed_seconds,
|
|
711
|
+
cost=record.total_cost,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
baseline_tokens = _tokens(baseline_receipts)
|
|
715
|
+
candidate_tokens = _tokens(candidate_receipts)
|
|
716
|
+
recorded = baseline_tokens is not None or candidate_tokens is not None
|
|
717
|
+
return _Economics(
|
|
718
|
+
source="episode_receipts" if recorded else "unavailable",
|
|
719
|
+
baseline_tokens=baseline_tokens,
|
|
720
|
+
candidate_tokens=candidate_tokens,
|
|
721
|
+
baseline_seconds=None,
|
|
722
|
+
candidate_seconds=None,
|
|
723
|
+
cost=TotalCost(cost_usd=None, provenance=CostProvenance.UNAVAILABLE),
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _derived_cost(
|
|
728
|
+
campaign: CampaignSpec,
|
|
729
|
+
record: ComparisonExecutionRecord | None,
|
|
730
|
+
economics: _Economics,
|
|
731
|
+
) -> DerivedCost | None:
|
|
732
|
+
"""Work one comparison's cost out from the tokens it recorded, or return none.
|
|
733
|
+
|
|
734
|
+
Only ever reached when the provider reported no cost of its own: a figure
|
|
735
|
+
it billed is the better answer wherever there is one. Nothing computed here
|
|
736
|
+
is written anywhere. It is multiplication, done while the result is drawn,
|
|
737
|
+
over token counts the signed execution record already holds and prices this
|
|
738
|
+
release recorded on a named day.
|
|
739
|
+
|
|
740
|
+
A model this release recorded no prices for produces nothing rather than a
|
|
741
|
+
guess, and so does a run whose sides did not both report their usage. Both
|
|
742
|
+
are said out loud by the caveat rather than shown as a blank.
|
|
743
|
+
"""
|
|
744
|
+
if record is None or economics.cost.provenance is not CostProvenance.UNAVAILABLE:
|
|
745
|
+
return None
|
|
746
|
+
usage = (record.baseline.usage, record.candidate.usage)
|
|
747
|
+
input_tokens = _summed(usage, "input_tokens")
|
|
748
|
+
output_tokens = _summed(usage, "output_tokens")
|
|
749
|
+
if input_tokens is None or output_tokens is None:
|
|
750
|
+
return None
|
|
751
|
+
try:
|
|
752
|
+
prices = price_profile_for(campaign.subject.model.model_id)
|
|
753
|
+
except PrerequisiteError:
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
cached = _summed(usage, "cached_input_tokens")
|
|
757
|
+
return DerivedCost(
|
|
758
|
+
usd=(
|
|
759
|
+
input_tokens * prices.input_usd_per_mtok
|
|
760
|
+
+ output_tokens * prices.output_usd_per_mtok
|
|
761
|
+
)
|
|
762
|
+
/ _TOKENS_PER_MILLION,
|
|
763
|
+
input_tokens=input_tokens,
|
|
764
|
+
output_tokens=output_tokens,
|
|
765
|
+
# A split that claims more cached input than there was input describes
|
|
766
|
+
# no cache this can price around, so it is carried as unknown rather
|
|
767
|
+
# than repaired into something plausible.
|
|
768
|
+
cached_input_tokens=(
|
|
769
|
+
None if cached is None or cached > input_tokens else cached
|
|
770
|
+
),
|
|
771
|
+
# The recorded profile quotes one rate per direction and says outright
|
|
772
|
+
# that they are the highest uncached ones. Until a profile distinguishes
|
|
773
|
+
# a cached rate, every input token is priced as though the provider
|
|
774
|
+
# cached none of it, which can only make the figure too high.
|
|
775
|
+
prices_name_a_cached_rate=False,
|
|
776
|
+
model_id=sanitize_label(prices.model_id),
|
|
777
|
+
input_usd_per_mtok=prices.input_usd_per_mtok,
|
|
778
|
+
output_usd_per_mtok=prices.output_usd_per_mtok,
|
|
779
|
+
prices_recorded_on=sanitize_label(prices.recorded_on),
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _summed(usage: tuple[VariantUsage, VariantUsage], field: str) -> int | None:
|
|
784
|
+
"""Return both sides' count of one thing, or ``None`` when a side has none."""
|
|
785
|
+
total = 0
|
|
786
|
+
for side in usage:
|
|
787
|
+
recorded = getattr(side, field)
|
|
788
|
+
if recorded is None:
|
|
789
|
+
return None
|
|
790
|
+
total += int(recorded)
|
|
791
|
+
return total
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _tokens(receipts: Sequence[EpisodeReceipt]) -> int | None:
|
|
795
|
+
"""Return one side's token total when the receipts carry one.
|
|
796
|
+
|
|
797
|
+
A receipt records whatever metrics the evaluation recorded. The pinned
|
|
798
|
+
build records token usage on the trace rather than in that table, so this
|
|
799
|
+
is ``None`` for every run this release produces, and the caveat that says
|
|
800
|
+
so is emitted rather than a zero that would read as "used no tokens".
|
|
801
|
+
"""
|
|
802
|
+
totals: list[float] = []
|
|
803
|
+
for receipt in receipts:
|
|
804
|
+
for traces in receipt.named_traces.values():
|
|
805
|
+
for trace in traces:
|
|
806
|
+
recorded = trace.metrics.get("total_tokens")
|
|
807
|
+
if recorded is None:
|
|
808
|
+
return None
|
|
809
|
+
totals.append(recorded)
|
|
810
|
+
return int(sum(totals)) if totals else None
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _verification_status(verification: VerificationResult | None) -> str:
|
|
814
|
+
"""Return the three-state answer to "was this proof checked?"."""
|
|
815
|
+
if verification is None:
|
|
816
|
+
return VERIFICATION_NOT_VERIFIED
|
|
817
|
+
return VERIFICATION_VERIFIED if verification.verified else VERIFICATION_FAILED
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
#: How each cost provenance is spelled for a person, in one phrase that fits
|
|
821
|
+
#: beside the number. Owned here rather than by either renderer, so that the
|
|
822
|
+
#: terminal and a phone cannot describe one figure two ways.
|
|
823
|
+
_COST_PROVENANCE: Final[dict[CostProvenance, str]] = {
|
|
824
|
+
CostProvenance.PROVIDER_REPORTED: "reported by the provider",
|
|
825
|
+
CostProvenance.COMPUTED_FROM_PINNED_PRICE: "computed from the pinned price",
|
|
826
|
+
CostProvenance.ESTIMATED: "estimated, not billed",
|
|
827
|
+
CostProvenance.UNAVAILABLE: "unavailable",
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _caveats(
|
|
832
|
+
*,
|
|
833
|
+
report: UpliftReport,
|
|
834
|
+
campaign: CampaignSpec,
|
|
835
|
+
verification: VerificationResult | None,
|
|
836
|
+
economics: _Economics,
|
|
837
|
+
recorded_evidence: RecordedEvidence | None,
|
|
838
|
+
derived: DerivedCost | None,
|
|
839
|
+
) -> list[PresentationCaveat]:
|
|
840
|
+
"""Return everything a reader has to know to read the numbers correctly.
|
|
841
|
+
|
|
842
|
+
The order is the order they matter in: what would make the result invalid
|
|
843
|
+
first, then what bounds how much it proves, then the standing facts about
|
|
844
|
+
this release.
|
|
845
|
+
"""
|
|
846
|
+
caveats: list[PresentationCaveat] = []
|
|
847
|
+
|
|
848
|
+
if report.proof_grade == "development_only":
|
|
849
|
+
caveats.append(
|
|
850
|
+
PresentationCaveat(
|
|
851
|
+
code="development_only_result",
|
|
852
|
+
severity="error",
|
|
853
|
+
text=(
|
|
854
|
+
"This report is development-only. Its numbers are not "
|
|
855
|
+
"evidence and it withholds a verdict."
|
|
856
|
+
),
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
if verification is not None and not verification.verified:
|
|
860
|
+
failures = verification.failures
|
|
861
|
+
remainder = (
|
|
862
|
+
"" if len(failures) == 1 else f" ({len(failures) - 1} more checks failed)"
|
|
863
|
+
)
|
|
864
|
+
caveats.append(
|
|
865
|
+
PresentationCaveat(
|
|
866
|
+
code="proof_verification_failed",
|
|
867
|
+
severity="error",
|
|
868
|
+
text=(
|
|
869
|
+
"This run's local proof did not verify, so nothing below "
|
|
870
|
+
f"can be relied on: {failures[0].detail}{remainder}."
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
if verification is None and report.proof_grade == "P1":
|
|
875
|
+
caveats.append(
|
|
876
|
+
PresentationCaveat(
|
|
877
|
+
code="proof_not_verified",
|
|
878
|
+
severity="warning",
|
|
879
|
+
text=(
|
|
880
|
+
"The local proof was not checked for this rendering. Run "
|
|
881
|
+
"`techtree proof verify` to check it."
|
|
882
|
+
),
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
if report.statuses.comparison is ComparisonStatus.CONTROLLED_WITH_WARNINGS:
|
|
886
|
+
caveats.append(
|
|
887
|
+
PresentationCaveat(
|
|
888
|
+
code="comparison_controlled_with_warnings",
|
|
889
|
+
severity="warning",
|
|
890
|
+
text=_weak_attestation_text(campaign),
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
if report.proof_grade == "P1":
|
|
894
|
+
caveats.append(
|
|
895
|
+
PresentationCaveat(
|
|
896
|
+
code="local_participant_attestation",
|
|
897
|
+
severity="warning",
|
|
898
|
+
text=(
|
|
899
|
+
f"Proof grade P1 means {P1_MEANING}. Your own local key "
|
|
900
|
+
"vouches for bytes that verify against each other — not "
|
|
901
|
+
"for who ran them."
|
|
902
|
+
),
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
caveats.append(
|
|
906
|
+
PresentationCaveat(
|
|
907
|
+
code="no_independent_reproduction",
|
|
908
|
+
severity="warning",
|
|
909
|
+
text=(
|
|
910
|
+
"Nobody has independently reproduced this comparison, and no "
|
|
911
|
+
"platform witnessed it."
|
|
912
|
+
),
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
caveats.append(
|
|
916
|
+
PresentationCaveat(
|
|
917
|
+
code="introductory_task_family",
|
|
918
|
+
severity="warning",
|
|
919
|
+
text=(
|
|
920
|
+
"This is a toy introductory Climb. Its task family is "
|
|
921
|
+
"synthetic and demonstrates the mechanism; it measures no "
|
|
922
|
+
"broad capability."
|
|
923
|
+
),
|
|
924
|
+
)
|
|
925
|
+
)
|
|
926
|
+
caveats.append(
|
|
927
|
+
PresentationCaveat(
|
|
928
|
+
code="no_server_upload",
|
|
929
|
+
severity="info",
|
|
930
|
+
text=(
|
|
931
|
+
"The raw episodes stay on this machine. They are not in the "
|
|
932
|
+
"proof directory and nothing sends them: publishing a run "
|
|
933
|
+
"sends its proof and never its episodes, and nothing is "
|
|
934
|
+
"published unless you publish this run yourself. Model "
|
|
935
|
+
"inference was still sent to the model provider this run "
|
|
936
|
+
"used, under that provider's policies."
|
|
937
|
+
),
|
|
938
|
+
)
|
|
939
|
+
)
|
|
940
|
+
caveats.append(
|
|
941
|
+
PresentationCaveat(
|
|
942
|
+
# Spec section 7.15 calls this "no Relay requirement". The reader
|
|
943
|
+
# is a participant rather than an architect, so the caveat says
|
|
944
|
+
# what it means for them and names no internal component.
|
|
945
|
+
code="no_external_evidence_service",
|
|
946
|
+
severity="info",
|
|
947
|
+
text="No external evidence service is required, used, or contacted.",
|
|
948
|
+
)
|
|
949
|
+
)
|
|
950
|
+
throttling = _throttling_caveat(recorded_evidence)
|
|
951
|
+
if throttling is not None:
|
|
952
|
+
caveats.append(throttling)
|
|
953
|
+
caveats.append(_economics_caveat(economics, derived))
|
|
954
|
+
if report.decision is UpliftDecision.REJECTED:
|
|
955
|
+
caveats.append(
|
|
956
|
+
PresentationCaveat(
|
|
957
|
+
code="rejected_is_evidence",
|
|
958
|
+
severity="info",
|
|
959
|
+
text=(
|
|
960
|
+
"A rejected candidate is a measurement, not a failed run: "
|
|
961
|
+
"this Skill did not meet the threshold the Campaign "
|
|
962
|
+
"declared in advance."
|
|
963
|
+
),
|
|
964
|
+
)
|
|
965
|
+
)
|
|
966
|
+
return caveats
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _weak_attestation_text(campaign: CampaignSpec) -> str:
|
|
970
|
+
"""Say which declared coordinate the run could not confirm, when it can.
|
|
971
|
+
|
|
972
|
+
"At least one declared coordinate could not be confirmed" is true and
|
|
973
|
+
useless: a reader cannot weigh a warning whose subject is withheld. The
|
|
974
|
+
subject is not guessed here — the same function the comparison itself used
|
|
975
|
+
to decide whether to raise this warning is asked again, over the run's own
|
|
976
|
+
signed Campaign, and the coordinate is named only when that answer names
|
|
977
|
+
it. A cause this build does not have a plain sentence for keeps the honest
|
|
978
|
+
general wording rather than borrowing the model-revision one.
|
|
979
|
+
"""
|
|
980
|
+
recorded = weaker_claim_warnings(campaign)
|
|
981
|
+
coordinates = {check.id for check in recorded}
|
|
982
|
+
if coordinates == {MODEL_REVISION_UNDISCOVERABLE}:
|
|
983
|
+
named = (
|
|
984
|
+
"Your provider publishes no immutable build identifier for "
|
|
985
|
+
f"{sanitize_label(campaign.subject.model.model_id)}, so both sides "
|
|
986
|
+
"provably used the same model name but not provably the same model "
|
|
987
|
+
"build."
|
|
988
|
+
)
|
|
989
|
+
else:
|
|
990
|
+
named = (
|
|
991
|
+
"At least one declared coordinate could not be confirmed from what "
|
|
992
|
+
"the run observed, and this build has no plainer name for it."
|
|
993
|
+
)
|
|
994
|
+
return (
|
|
995
|
+
"The comparison is controlled with warnings, which means one "
|
|
996
|
+
f"coordinate is attested more weakly than the rest. {named} No "
|
|
997
|
+
"mismatch was found; a mismatch would have made the comparison invalid."
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _throttling_caveat(
|
|
1002
|
+
recorded_evidence: RecordedEvidence | None,
|
|
1003
|
+
) -> PresentationCaveat | None:
|
|
1004
|
+
"""Say how often the provider refused each side, when the run still says.
|
|
1005
|
+
|
|
1006
|
+
Two sides that met different amounts of throttling did not quite meet the
|
|
1007
|
+
same conditions, so an asymmetry is part of how much the comparison proves
|
|
1008
|
+
and is raised as a warning. An even count is worth stating and is not a
|
|
1009
|
+
qualification of anything, so it is a note. A run whose recorded traces
|
|
1010
|
+
can no longer be read says nothing here at all.
|
|
1011
|
+
"""
|
|
1012
|
+
if recorded_evidence is None:
|
|
1013
|
+
return None
|
|
1014
|
+
baseline = recorded_evidence.baseline.rate_limited_calls
|
|
1015
|
+
candidate = recorded_evidence.candidate.rate_limited_calls
|
|
1016
|
+
if baseline == candidate == 0:
|
|
1017
|
+
text = "The provider refused no model call on either side."
|
|
1018
|
+
else:
|
|
1019
|
+
text = (
|
|
1020
|
+
f"The provider refused {_calls(baseline)} with a rate limit on the "
|
|
1021
|
+
f"baseline side and {candidate:,} on the candidate side."
|
|
1022
|
+
)
|
|
1023
|
+
if recorded_evidence.every_rollout_completed:
|
|
1024
|
+
text = f"{text} Every rollout still ran to completion."
|
|
1025
|
+
return PresentationCaveat(
|
|
1026
|
+
code="provider_rate_limiting",
|
|
1027
|
+
severity="warning" if baseline != candidate else "info",
|
|
1028
|
+
text=text,
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
#: What each cost provenance means to a reader, in the words decisions
|
|
1033
|
+
#: document 0007 R6 requires: a computed or estimated figure is never allowed
|
|
1034
|
+
#: to read as one the provider billed.
|
|
1035
|
+
_COST_CAVEATS: Final[
|
|
1036
|
+
dict[CostProvenance, tuple[str, Literal["info", "warning", "error"], str]]
|
|
1037
|
+
] = {
|
|
1038
|
+
CostProvenance.PROVIDER_REPORTED: (
|
|
1039
|
+
"cost_provider_reported",
|
|
1040
|
+
"info",
|
|
1041
|
+
"The cost shown is the figure the provider reported for this "
|
|
1042
|
+
"comparison, taken from its signed execution record.",
|
|
1043
|
+
),
|
|
1044
|
+
CostProvenance.COMPUTED_FROM_PINNED_PRICE: (
|
|
1045
|
+
"cost_computed_from_pinned_price",
|
|
1046
|
+
"info",
|
|
1047
|
+
"The cost shown was computed from the recorded token usage and the "
|
|
1048
|
+
"price this release pins. The provider did not report it.",
|
|
1049
|
+
),
|
|
1050
|
+
CostProvenance.ESTIMATED: (
|
|
1051
|
+
"cost_estimated",
|
|
1052
|
+
"warning",
|
|
1053
|
+
"The cost shown is an estimate. It is not a figure the provider "
|
|
1054
|
+
"reported and it is not what you were charged.",
|
|
1055
|
+
),
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _cost_unavailable_reason(
|
|
1060
|
+
campaign: CampaignSpec,
|
|
1061
|
+
record: ComparisonExecutionRecord | None,
|
|
1062
|
+
economics: _Economics,
|
|
1063
|
+
) -> str:
|
|
1064
|
+
"""Say which of the two things a cost needs this run is missing.
|
|
1065
|
+
|
|
1066
|
+
"Unavailable" on its own leaves a reader who has just spent money unable
|
|
1067
|
+
to tell whether the run lost the tokens or the release lost the prices, so
|
|
1068
|
+
the sentence names whichever one is actually absent.
|
|
1069
|
+
"""
|
|
1070
|
+
if record is None:
|
|
1071
|
+
return (
|
|
1072
|
+
"This run wrote no signed execution record, so there is no signed "
|
|
1073
|
+
"token total to work a cost out from."
|
|
1074
|
+
)
|
|
1075
|
+
model_id = campaign.subject.model.model_id
|
|
1076
|
+
try:
|
|
1077
|
+
price_profile_for(model_id)
|
|
1078
|
+
except PrerequisiteError:
|
|
1079
|
+
return (
|
|
1080
|
+
"This release recorded no provider prices for "
|
|
1081
|
+
f"{sanitize_label(model_id)}, so the tokens this run recorded "
|
|
1082
|
+
"cannot be turned into a cost."
|
|
1083
|
+
)
|
|
1084
|
+
if economics.cost.provenance is not CostProvenance.UNAVAILABLE:
|
|
1085
|
+
return _COST_PROVENANCE[economics.cost.provenance]
|
|
1086
|
+
return (
|
|
1087
|
+
"Neither side of this comparison reported how many tokens it used, "
|
|
1088
|
+
"and the provider reported no cost of its own."
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _economics_caveat(
|
|
1093
|
+
economics: _Economics, derived: DerivedCost | None
|
|
1094
|
+
) -> PresentationCaveat:
|
|
1095
|
+
"""Say where the cost and timing came from, or that they are unknown.
|
|
1096
|
+
|
|
1097
|
+
Decisions document 0007 R6: missing economics is an operational-evidence
|
|
1098
|
+
warning about what is unknown, and never a finding about the measurement.
|
|
1099
|
+
Both sentences below end by saying so, because the reader most likely to
|
|
1100
|
+
meet this caveat is the one who needs to be told that their result still
|
|
1101
|
+
stands.
|
|
1102
|
+
"""
|
|
1103
|
+
if economics.source == "comparison_execution_record":
|
|
1104
|
+
named = _COST_CAVEATS.get(economics.cost.provenance)
|
|
1105
|
+
if named is not None:
|
|
1106
|
+
code, severity, text = named
|
|
1107
|
+
return PresentationCaveat(code=code, severity=severity, text=text)
|
|
1108
|
+
if derived is not None:
|
|
1109
|
+
return PresentationCaveat(
|
|
1110
|
+
code="cost_derived_while_rendering",
|
|
1111
|
+
severity="info",
|
|
1112
|
+
text=(
|
|
1113
|
+
"Timing and token counts come from this run's signed "
|
|
1114
|
+
"execution record. The provider reported no figure for what "
|
|
1115
|
+
"it charged, so the one shown was worked out from those "
|
|
1116
|
+
"token counts and the prices this release recorded. It was "
|
|
1117
|
+
"not written into anything this run signed, and what the "
|
|
1118
|
+
"comparison measured is unaffected."
|
|
1119
|
+
),
|
|
1120
|
+
)
|
|
1121
|
+
return PresentationCaveat(
|
|
1122
|
+
code="cost_unavailable",
|
|
1123
|
+
severity="warning",
|
|
1124
|
+
text=(
|
|
1125
|
+
"Timing and token counts come from this run's signed execution "
|
|
1126
|
+
"record. No cost was reported for it and none could be worked "
|
|
1127
|
+
"out from what it recorded. What the comparison measured is "
|
|
1128
|
+
"unaffected."
|
|
1129
|
+
),
|
|
1130
|
+
)
|
|
1131
|
+
if economics.source == "episode_receipts":
|
|
1132
|
+
return PresentationCaveat(
|
|
1133
|
+
code="operational_evidence_unavailable",
|
|
1134
|
+
severity="warning",
|
|
1135
|
+
text=(
|
|
1136
|
+
"This run has no signed execution record, so its timing and "
|
|
1137
|
+
"cost are unavailable; the token count shown comes from the "
|
|
1138
|
+
"receipts. What the comparison measured is unaffected."
|
|
1139
|
+
),
|
|
1140
|
+
)
|
|
1141
|
+
return PresentationCaveat(
|
|
1142
|
+
code="operational_evidence_unavailable",
|
|
1143
|
+
severity="warning",
|
|
1144
|
+
text=(
|
|
1145
|
+
"This run has no signed execution record, so how long it took, "
|
|
1146
|
+
"how many tokens it used and what it cost are all unavailable. "
|
|
1147
|
+
"What the comparison measured is unaffected."
|
|
1148
|
+
),
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _next_actions(
|
|
1153
|
+
run_id: str,
|
|
1154
|
+
proof_grade: str,
|
|
1155
|
+
*,
|
|
1156
|
+
verified: bool,
|
|
1157
|
+
publication_eligible: bool,
|
|
1158
|
+
) -> list[NextAction]:
|
|
1159
|
+
"""Return the steps this build can actually carry out for this report.
|
|
1160
|
+
|
|
1161
|
+
A development-only report is offered only the first of them. It has no
|
|
1162
|
+
proof bundle and never claimed one, and nothing may be derived from
|
|
1163
|
+
invented numbers, so the three commands that would refuse it are not
|
|
1164
|
+
suggested: an action a caller cannot carry out is worse than one fewer
|
|
1165
|
+
suggestion.
|
|
1166
|
+
|
|
1167
|
+
Publishing is offered second, and only to a run whose proof was checked in
|
|
1168
|
+
this very reading and held together. It goes above ``verify_proof`` for the
|
|
1169
|
+
same reason it is only offered at all: the check has just been run and
|
|
1170
|
+
passed, so re-running it is a step already taken, while publishing is the
|
|
1171
|
+
step that has not been. The envelope carries three actions, so a position
|
|
1172
|
+
here is a decision about what a reader is actually shown.
|
|
1173
|
+
"""
|
|
1174
|
+
actions = [
|
|
1175
|
+
NextAction(
|
|
1176
|
+
id="inspect_tasks",
|
|
1177
|
+
label="Look at every task, including the ones that regressed",
|
|
1178
|
+
reason="The per-task table is where a Skill's effect is legible.",
|
|
1179
|
+
cli=["techtree", "run", "result", run_id, "--show-tasks", "all"],
|
|
1180
|
+
hermes_tool=None,
|
|
1181
|
+
hermes_args=None,
|
|
1182
|
+
requires_user_confirmation=False,
|
|
1183
|
+
)
|
|
1184
|
+
]
|
|
1185
|
+
if proof_grade == "development_only":
|
|
1186
|
+
return actions
|
|
1187
|
+
|
|
1188
|
+
if verified and publication_eligible:
|
|
1189
|
+
actions.append(publish_action(run_id))
|
|
1190
|
+
|
|
1191
|
+
# Ordered by usefulness, because the CLI envelope carries at most three and
|
|
1192
|
+
# truncates from the end. Checking that the result is real comes before
|
|
1193
|
+
# acting on it, and the context has to exist before a revision can be
|
|
1194
|
+
# compared against anything.
|
|
1195
|
+
actions.append(
|
|
1196
|
+
NextAction(
|
|
1197
|
+
id="verify_proof",
|
|
1198
|
+
label="Verify this run's local proof",
|
|
1199
|
+
reason="It checks offline, from the bytes the run stored.",
|
|
1200
|
+
cli=["techtree", "proof", "verify", run_id],
|
|
1201
|
+
hermes_tool=None,
|
|
1202
|
+
hermes_args=None,
|
|
1203
|
+
requires_user_confirmation=False,
|
|
1204
|
+
)
|
|
1205
|
+
)
|
|
1206
|
+
actions.append(
|
|
1207
|
+
NextAction(
|
|
1208
|
+
id="improvement_context",
|
|
1209
|
+
label="Export what a host agent needs to propose one Skill revision",
|
|
1210
|
+
reason=(
|
|
1211
|
+
"It carries the regressions, the failures and the objective, "
|
|
1212
|
+
"and no hidden task material."
|
|
1213
|
+
),
|
|
1214
|
+
cli=["techtree", "uplift", "context", run_id],
|
|
1215
|
+
hermes_tool=None,
|
|
1216
|
+
hermes_args=None,
|
|
1217
|
+
requires_user_confirmation=False,
|
|
1218
|
+
)
|
|
1219
|
+
)
|
|
1220
|
+
actions.append(
|
|
1221
|
+
NextAction(
|
|
1222
|
+
id="prepare_replacement",
|
|
1223
|
+
label="Prepare a comparison of this Skill against a revision of it",
|
|
1224
|
+
reason=(
|
|
1225
|
+
"The baseline is pinned to the Skill this run measured, so the "
|
|
1226
|
+
"second comparison starts where this one ended."
|
|
1227
|
+
),
|
|
1228
|
+
cli=[
|
|
1229
|
+
"techtree",
|
|
1230
|
+
"uplift",
|
|
1231
|
+
"prepare",
|
|
1232
|
+
"--from-run",
|
|
1233
|
+
run_id,
|
|
1234
|
+
"--candidate-skill",
|
|
1235
|
+
"PATH",
|
|
1236
|
+
],
|
|
1237
|
+
hermes_tool=None,
|
|
1238
|
+
hermes_args=None,
|
|
1239
|
+
requires_user_confirmation=True,
|
|
1240
|
+
)
|
|
1241
|
+
)
|
|
1242
|
+
return actions
|