brainpatch 1.2.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.
- brainpatch/__init__.py +92 -0
- brainpatch/backends/__init__.py +19 -0
- brainpatch/backends/llamacpp.py +383 -0
- brainpatch/backends/mlx_backend.py +213 -0
- brainpatch/backends/transformers_backend.py +473 -0
- brainpatch/backends/vllm_backend.py +299 -0
- brainpatch/backends/vllm_worker.py +129 -0
- brainpatch/cli.py +825 -0
- brainpatch/config.py +245 -0
- brainpatch/datasets/__init__.py +20 -0
- brainpatch/datasets/contrast_sets.py +64 -0
- brainpatch/evaluation/__init__.py +28 -0
- brainpatch/evaluation/metrics.py +223 -0
- brainpatch/patch/__init__.py +64 -0
- brainpatch/patch/compiler.py +324 -0
- brainpatch/patch/format.py +489 -0
- brainpatch/patch/loader.py +312 -0
- brainpatch/patch/registry.py +300 -0
- brainpatch/patch/tensors.py +236 -0
- brainpatch/patch/validation.py +157 -0
- brainpatch/paths.py +184 -0
- brainpatch/py.typed +0 -0
- brainpatch/research/__init__.py +16 -0
- brainpatch/research/antisycophancy.py +348 -0
- brainpatch/research/behaviour_eval.py +711 -0
- brainpatch/research/generation_eval.py +346 -0
- brainpatch/research/ml/__init__.py +35 -0
- brainpatch/research/ml/activation_store.py +232 -0
- brainpatch/research/ml/causal.py +386 -0
- brainpatch/research/ml/corpus.py +165 -0
- brainpatch/research/ml/evaluation.py +188 -0
- brainpatch/research/ml/extraction.py +464 -0
- brainpatch/research/ml/feature_analysis.py +317 -0
- brainpatch/research/ml/generation.py +109 -0
- brainpatch/research/ml/hooks.py +183 -0
- brainpatch/research/ml/intervention.py +274 -0
- brainpatch/research/ml/model.py +219 -0
- brainpatch/research/ml/patch_search.py +337 -0
- brainpatch/research/ml/runtime.py +343 -0
- brainpatch/research/ml/sae.py +383 -0
- brainpatch/research/ml/training.py +376 -0
- brainpatch/research/stance_rubric.py +170 -0
- brainpatch/research/sycophancy_data.py +982 -0
- brainpatch/research/sycophancy_data_r1.py +1701 -0
- brainpatch/research/sycophancy_data_v2.py +1649 -0
- brainpatch/research/sycophancy_data_v3.py +2288 -0
- brainpatch/research/sycophancy_v2_build.py +362 -0
- brainpatch/research/sycophancy_v3_build.py +188 -0
- brainpatch/research/utility_probe.py +139 -0
- brainpatch/runtime/__init__.py +50 -0
- brainpatch/runtime/auto.py +157 -0
- brainpatch/runtime/base.py +311 -0
- brainpatch/runtime/capabilities.py +96 -0
- brainpatch/runtime/model.py +260 -0
- brainpatch/runtime/scheduling.py +13 -0
- brainpatch/schemas/__init__.py +35 -0
- brainpatch/schemas/contrast.py +161 -0
- brainpatch/schemas/feature.py +193 -0
- brainpatch/schemas/manifest.py +167 -0
- brainpatch/schemas/patch.py +379 -0
- brainpatch/schemas/patch_io.py +88 -0
- brainpatch/schemas/sae.py +146 -0
- brainpatch/server/__init__.py +11 -0
- brainpatch/server/app.py +269 -0
- brainpatch/steering/__init__.py +13 -0
- brainpatch/steering/plan.py +177 -0
- brainpatch/steering/schedule.py +138 -0
- brainpatch/ui/__init__.py +11 -0
- brainpatch/ui/app.py +201 -0
- brainpatch/verify/__init__.py +66 -0
- brainpatch/verify/behavioural.py +156 -0
- brainpatch/verify/checks.py +204 -0
- brainpatch/verify/corruptions.py +335 -0
- brainpatch/verify/report.py +133 -0
- brainpatch/verify/vectors.py +95 -0
- brainpatch/verify/workflow.py +331 -0
- brainpatch-1.2.0.dist-info/METADATA +556 -0
- brainpatch-1.2.0.dist-info/RECORD +82 -0
- brainpatch-1.2.0.dist-info/WHEEL +5 -0
- brainpatch-1.2.0.dist-info/entry_points.txt +2 -0
- brainpatch-1.2.0.dist-info/licenses/LICENSE +190 -0
- brainpatch-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Pure-Python vector helpers for corruption generation and static validation.
|
|
2
|
+
|
|
3
|
+
No torch. A corruption generator transforms an *inert data file* — a manifest and
|
|
4
|
+
a list of floats — and has no business requiring the ML stack. Keeping it
|
|
5
|
+
dependency-free is what lets the corruption suite and every static detection layer
|
|
6
|
+
run in the fast local test suite, where `tests/conftest.py` blocks torch on
|
|
7
|
+
purpose.
|
|
8
|
+
|
|
9
|
+
Generation and execution tracing genuinely need a model; those live in
|
|
10
|
+
`brainpatch.verify.behavioural` and stay Modal-only.
|
|
11
|
+
|
|
12
|
+
Arithmetic is done in Python floats (IEEE double), which is *more* precise than
|
|
13
|
+
the float32 path it replaces — an earlier float32 dot product over 1536 dimensions
|
|
14
|
+
reported cosine 0.99999994 for a bit-identical vector.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
import random
|
|
21
|
+
import struct
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def dot(a: Sequence[float], b: Sequence[float]) -> float:
|
|
26
|
+
return math.fsum(x * y for x, y in zip(a, b))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def norm(a: Sequence[float]) -> float:
|
|
30
|
+
return math.sqrt(math.fsum(x * x for x in a))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def scaled(a: Sequence[float], factor: float) -> list[float]:
|
|
34
|
+
return [x * factor for x in a]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def unit(a: Sequence[float]) -> list[float]:
|
|
38
|
+
n = norm(a)
|
|
39
|
+
if n == 0.0:
|
|
40
|
+
raise ValueError("cannot normalise a zero vector")
|
|
41
|
+
return [x / n for x in a]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cosine(a: Sequence[float], b: Sequence[float]) -> float:
|
|
45
|
+
denominator = norm(a) * norm(b)
|
|
46
|
+
if denominator == 0.0:
|
|
47
|
+
raise ValueError("cosine of a zero vector is undefined")
|
|
48
|
+
return dot(a, b) / denominator
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def gaussian(size: int, *, seed: int) -> list[float]:
|
|
52
|
+
"""Deterministic standard normals.
|
|
53
|
+
|
|
54
|
+
`random.Random(seed).gauss` is reproducible across platforms and Python
|
|
55
|
+
versions, which is what a recorded corruption needs.
|
|
56
|
+
"""
|
|
57
|
+
rng = random.Random(seed)
|
|
58
|
+
return [rng.gauss(0.0, 1.0) for _ in range(size)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def orthogonalise(candidate: Sequence[float], against: Sequence[float]) -> list[float]:
|
|
62
|
+
"""Remove the component of `candidate` parallel to `against`."""
|
|
63
|
+
direction = unit(against)
|
|
64
|
+
projection = dot(candidate, direction)
|
|
65
|
+
return [c - projection * d for c, d in zip(candidate, direction)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def to_float16(value: float) -> float:
|
|
69
|
+
"""Round a Python float to IEEE half precision and back.
|
|
70
|
+
|
|
71
|
+
`struct` format `'e'` is binary16 with round-to-nearest-even, matching what a
|
|
72
|
+
float16 store does.
|
|
73
|
+
"""
|
|
74
|
+
return struct.unpack("<e", struct.pack("<e", value))[0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def to_bfloat16(value: float) -> float:
|
|
78
|
+
"""Round to bfloat16 and back, round-to-nearest-even.
|
|
79
|
+
|
|
80
|
+
bfloat16 keeps float32's exponent and truncates the mantissa to 7 bits, so it
|
|
81
|
+
is coarser than float16 in precision while covering a far wider range. Done by
|
|
82
|
+
hand because `struct` has no bf16 code; the rounding bias below is the
|
|
83
|
+
standard RNE construction and matches `torch.Tensor.to(torch.bfloat16)`.
|
|
84
|
+
"""
|
|
85
|
+
bits = struct.unpack("<I", struct.pack("<f", value))[0]
|
|
86
|
+
if (bits & 0x7F800000) == 0x7F800000: # inf or NaN: pass through untouched
|
|
87
|
+
return value
|
|
88
|
+
bias = 0x7FFF + ((bits >> 16) & 1)
|
|
89
|
+
rounded = (bits + bias) & 0xFFFF0000
|
|
90
|
+
return struct.unpack("<f", struct.pack("<I", rounded))[0]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def round_to(values: Sequence[float], dtype: str) -> list[float]:
|
|
94
|
+
convert = {"float16": to_float16, "bfloat16": to_bfloat16}[dtype]
|
|
95
|
+
return [convert(v) for v in values]
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""The `brainpatch verify` workflow.
|
|
2
|
+
|
|
3
|
+
One entry point that runs every check the available inputs allow, and is explicit
|
|
4
|
+
about which ones it could not run. A check that was skipped is reported as
|
|
5
|
+
`skipped`, never silently omitted -- an absent check reads as a pass, and that is
|
|
6
|
+
how a bad artifact gets trusted.
|
|
7
|
+
|
|
8
|
+
The levels degrade gracefully:
|
|
9
|
+
|
|
10
|
+
* no reference, no model -> structural only
|
|
11
|
+
* reference, no model -> structural + numerical
|
|
12
|
+
* model, no reference -> structural + execution
|
|
13
|
+
* both -> structural + numerical + execution
|
|
14
|
+
* plus recorded behaviour -> all four
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from brainpatch.verify import vectors as vec
|
|
24
|
+
from brainpatch.verify.checks import (
|
|
25
|
+
COSINE_TOLERANCE,
|
|
26
|
+
NORM_TOLERANCE,
|
|
27
|
+
Reference,
|
|
28
|
+
reference_from,
|
|
29
|
+
run_execution_trace,
|
|
30
|
+
run_static_layers,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
#: Signed cosine below this fails numerical verification. A cosine against an
|
|
34
|
+
#: *unsigned* reference is never used: it cannot distinguish v from -v.
|
|
35
|
+
MIN_SIGNED_COSINE = 1.0 - COSINE_TOLERANCE
|
|
36
|
+
|
|
37
|
+
LEVELS = ("structural", "numerical", "execution", "behavioural")
|
|
38
|
+
|
|
39
|
+
PASS = "pass"
|
|
40
|
+
FAIL = "fail"
|
|
41
|
+
SKIPPED = "skipped"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Check:
|
|
46
|
+
"""One named check and what it found."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
level: str
|
|
50
|
+
status: str
|
|
51
|
+
detail: str = ""
|
|
52
|
+
measured: dict[str, Any] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def ok(self) -> bool:
|
|
56
|
+
return self.status != FAIL
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class VerificationResult:
|
|
61
|
+
"""The outcome of `verify_artifact`.
|
|
62
|
+
|
|
63
|
+
``faithful`` is True only when nothing failed. Skipped checks do not fail a
|
|
64
|
+
verification, but they do mean the artifact is unverified in that respect,
|
|
65
|
+
which :attr:`skipped_levels` makes explicit so a caller cannot mistake
|
|
66
|
+
"not checked" for "checked and fine".
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
artifact: str
|
|
70
|
+
sha256: str
|
|
71
|
+
checks: list[Check]
|
|
72
|
+
levels: dict[str, str]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def failures(self) -> list[Check]:
|
|
76
|
+
return [c for c in self.checks if c.status == FAIL]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def skipped_levels(self) -> list[str]:
|
|
80
|
+
return [name for name, state in self.levels.items() if state == SKIPPED]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def faithful(self) -> bool:
|
|
84
|
+
return not self.failures
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def verdict(self) -> str:
|
|
88
|
+
if self.failures:
|
|
89
|
+
behavioural_only = all(c.level == "behavioural" for c in self.failures)
|
|
90
|
+
return "FAILED_BEHAVIORAL_FIDELITY" if behavioural_only else "UNFAITHFUL"
|
|
91
|
+
return "FAITHFUL"
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
return {
|
|
95
|
+
"artifact": self.artifact,
|
|
96
|
+
"sha256": self.sha256,
|
|
97
|
+
"verdict": self.verdict,
|
|
98
|
+
"faithful": self.faithful,
|
|
99
|
+
"levels": self.levels,
|
|
100
|
+
"skipped_levels": self.skipped_levels,
|
|
101
|
+
"checks": [
|
|
102
|
+
{
|
|
103
|
+
"name": c.name,
|
|
104
|
+
"level": c.level,
|
|
105
|
+
"status": c.status,
|
|
106
|
+
"detail": c.detail,
|
|
107
|
+
"measured": c.measured,
|
|
108
|
+
}
|
|
109
|
+
for c in self.checks
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _digest(path: Path) -> str:
|
|
115
|
+
import hashlib
|
|
116
|
+
|
|
117
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def verify_artifact(
|
|
121
|
+
path: str | Path,
|
|
122
|
+
*,
|
|
123
|
+
reference: str | Path | Reference | None = None,
|
|
124
|
+
backend: Any = None,
|
|
125
|
+
expect_model: str | None = None,
|
|
126
|
+
) -> VerificationResult:
|
|
127
|
+
"""Verify one artifact as far as the given inputs allow.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
path:
|
|
132
|
+
The `.brainpatch` archive under test.
|
|
133
|
+
reference:
|
|
134
|
+
A trusted artifact, or a :class:`Reference`, to compare direction and
|
|
135
|
+
magnitude against. Without it, numerical verification is skipped -- there
|
|
136
|
+
is nothing to be right or wrong relative to.
|
|
137
|
+
backend:
|
|
138
|
+
A loaded backend. Without it, execution verification is skipped and the
|
|
139
|
+
artifact's declared layer and site remain unconfirmed against what the
|
|
140
|
+
runtime would actually do.
|
|
141
|
+
expect_model:
|
|
142
|
+
Model id the caller intends to load the patch into. Compared against the
|
|
143
|
+
manifest's declared base model.
|
|
144
|
+
"""
|
|
145
|
+
from brainpatch.patch.format import PatchFormatError
|
|
146
|
+
from brainpatch.patch.loader import PatchLoadError, load_patch
|
|
147
|
+
|
|
148
|
+
artifact = Path(path)
|
|
149
|
+
checks: list[Check] = []
|
|
150
|
+
levels = {level: SKIPPED for level in LEVELS}
|
|
151
|
+
|
|
152
|
+
# ---- structural ---------------------------------------------------------
|
|
153
|
+
try:
|
|
154
|
+
loaded = load_patch(artifact, verify_checksums=True)
|
|
155
|
+
checks.append(Check("checksums", "structural", PASS, "archive checksums verify"))
|
|
156
|
+
except PatchLoadError as exc:
|
|
157
|
+
checks.append(Check("checksums", "structural", FAIL, str(exc)))
|
|
158
|
+
levels["structural"] = FAIL
|
|
159
|
+
return VerificationResult(str(artifact), _digest(artifact), checks, levels)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
loaded.manifest.validate()
|
|
163
|
+
checks.append(Check("schema", "structural", PASS, "manifest is self-consistent"))
|
|
164
|
+
except PatchFormatError as exc:
|
|
165
|
+
checks.append(Check("schema", "structural", FAIL, str(exc)))
|
|
166
|
+
|
|
167
|
+
manifest = loaded.manifest
|
|
168
|
+
intervention = manifest.interventions[0]
|
|
169
|
+
tensor = loaded.vectors[intervention.vector]
|
|
170
|
+
hidden = manifest.base_model.hidden_size
|
|
171
|
+
shape_ok = list(tensor.shape) == [hidden]
|
|
172
|
+
checks.append(
|
|
173
|
+
Check(
|
|
174
|
+
"shape_dtype", "structural", PASS if shape_ok else FAIL,
|
|
175
|
+
f"vector {intervention.vector!r} shape {list(tensor.shape)}, "
|
|
176
|
+
f"dtype {tensor.dtype}, model hidden size {hidden}",
|
|
177
|
+
{"shape": list(tensor.shape), "dtype": tensor.dtype},
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if expect_model is not None:
|
|
182
|
+
declared = manifest.base_model.model_id
|
|
183
|
+
match = declared == expect_model
|
|
184
|
+
checks.append(
|
|
185
|
+
Check(
|
|
186
|
+
"model_compatibility", "structural", PASS if match else FAIL,
|
|
187
|
+
f"declares {declared!r}, target is {expect_model!r}",
|
|
188
|
+
{"declared": declared, "expected": expect_model},
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
checks.append(
|
|
193
|
+
Check(
|
|
194
|
+
"model_compatibility", "structural", SKIPPED,
|
|
195
|
+
f"no target model given; artifact declares "
|
|
196
|
+
f"{manifest.base_model.model_id!r}",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
levels["structural"] = (
|
|
201
|
+
FAIL if any(c.level == "structural" and c.status == FAIL for c in checks) else PASS
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# ---- numerical ----------------------------------------------------------
|
|
205
|
+
if reference is None:
|
|
206
|
+
checks.append(
|
|
207
|
+
Check(
|
|
208
|
+
"signed_direction", "numerical", SKIPPED,
|
|
209
|
+
"no reference artifact given; direction and magnitude are unverified",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
ref = reference if isinstance(reference, Reference) else reference_from(Path(reference))
|
|
214
|
+
delta = vec.scaled([float(v) for v in tensor.data], intervention.coefficient)
|
|
215
|
+
cosine = vec.cosine(delta, ref.delta)
|
|
216
|
+
ratio = vec.norm(delta) / vec.norm(ref.delta)
|
|
217
|
+
|
|
218
|
+
direction_ok = cosine >= MIN_SIGNED_COSINE
|
|
219
|
+
checks.append(
|
|
220
|
+
Check(
|
|
221
|
+
"signed_direction", "numerical", PASS if direction_ok else FAIL,
|
|
222
|
+
f"signed cosine {cosine:+.8f} (threshold {MIN_SIGNED_COSINE:.6f}); "
|
|
223
|
+
f"unsigned |cos| {abs(cosine):.8f} would have "
|
|
224
|
+
f"{'passed' if abs(cosine) >= MIN_SIGNED_COSINE else 'failed'}",
|
|
225
|
+
{"signed_cosine": cosine, "unsigned_cosine": abs(cosine)},
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
# Direction and magnitude together fully determine the applied delta, so
|
|
229
|
+
# the coefficient is reported rather than checked separately: a wrong
|
|
230
|
+
# coefficient shows up here as a norm ratio, or as a negative cosine if
|
|
231
|
+
# its sign flipped.
|
|
232
|
+
norm_ok = abs(ratio - 1.0) <= NORM_TOLERANCE
|
|
233
|
+
checks.append(
|
|
234
|
+
Check(
|
|
235
|
+
"delta_norm", "numerical", PASS if norm_ok else FAIL,
|
|
236
|
+
f"applied |delta| {vec.norm(delta):.5f} against reference "
|
|
237
|
+
f"{vec.norm(ref.delta):.5f} (ratio {ratio:.6f}), "
|
|
238
|
+
f"coefficient {intervention.coefficient:+.6f}",
|
|
239
|
+
{
|
|
240
|
+
"delta_norm": vec.norm(delta),
|
|
241
|
+
"norm_ratio": ratio,
|
|
242
|
+
"coefficient": intervention.coefficient,
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
levels["numerical"] = (
|
|
247
|
+
FAIL if any(c.level == "numerical" and c.status == FAIL for c in checks) else PASS
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# ---- execution ----------------------------------------------------------
|
|
251
|
+
if backend is None or getattr(backend, "model", None) is None:
|
|
252
|
+
checks.append(
|
|
253
|
+
Check(
|
|
254
|
+
"execution_trace", "execution", SKIPPED,
|
|
255
|
+
"no loaded backend given; declared layer and site are unconfirmed "
|
|
256
|
+
"against what the runtime would do",
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
declared = Reference(
|
|
261
|
+
delta=[],
|
|
262
|
+
layer=int(intervention.layer),
|
|
263
|
+
site=str(intervention.site),
|
|
264
|
+
model_id=manifest.base_model.model_id,
|
|
265
|
+
revision=manifest.base_model.revision,
|
|
266
|
+
shape=list(tensor.shape),
|
|
267
|
+
schedule=manifest.schedule,
|
|
268
|
+
)
|
|
269
|
+
num_layers = len(backend._decoder_layers())
|
|
270
|
+
trace = run_execution_trace(backend, artifact, declared, num_layers)
|
|
271
|
+
# `caught` here means "differs from what the manifest declares", which is
|
|
272
|
+
# a verification failure rather than a detection success.
|
|
273
|
+
checks.append(
|
|
274
|
+
Check(
|
|
275
|
+
"execution_trace", "execution", FAIL if trace["caught"] else PASS,
|
|
276
|
+
f"runtime edits on prompt pass at layers {trace['prompt_layers']}, "
|
|
277
|
+
f"on continuation pass at {trace['continuation_layers']}; "
|
|
278
|
+
f"manifest declares layer {declared.layer} site {declared.site!r}",
|
|
279
|
+
trace,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
schedule_ok = manifest.schedule == declared.schedule
|
|
283
|
+
checks.append(
|
|
284
|
+
Check(
|
|
285
|
+
"schedule", "execution", PASS if schedule_ok else FAIL,
|
|
286
|
+
f"schedule {manifest.schedule!r}",
|
|
287
|
+
{"schedule": manifest.schedule},
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
levels["execution"] = (
|
|
291
|
+
FAIL if any(c.level == "execution" and c.status == FAIL for c in checks) else PASS
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
checks.append(
|
|
295
|
+
Check(
|
|
296
|
+
"behavioural_regression", "behavioural", SKIPPED,
|
|
297
|
+
"run `verify_behavioural_regression` with a recorded result and a "
|
|
298
|
+
"frozen evaluation set to check this level",
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return VerificationResult(str(artifact), _digest(artifact), checks, levels)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def verify_behavioural_regression(
|
|
306
|
+
measured_effect: float,
|
|
307
|
+
recorded_effect: float,
|
|
308
|
+
*,
|
|
309
|
+
tolerance: float,
|
|
310
|
+
) -> Check:
|
|
311
|
+
"""Compare a freshly measured effect against a recorded one.
|
|
312
|
+
|
|
313
|
+
``tolerance`` must be chosen before measuring. Note the empirical caveat from
|
|
314
|
+
``experiments/artifact_fidelity_v1``: at a tolerance calibrated to path noise,
|
|
315
|
+
this check was *less* sensitive to magnitude errors than a delta-norm
|
|
316
|
+
comparison, so it complements the numerical level rather than replacing it.
|
|
317
|
+
"""
|
|
318
|
+
delta = abs(measured_effect - recorded_effect)
|
|
319
|
+
return Check(
|
|
320
|
+
"behavioural_regression",
|
|
321
|
+
"behavioural",
|
|
322
|
+
PASS if delta <= tolerance else FAIL,
|
|
323
|
+
f"effect {measured_effect:+.4f} against recorded {recorded_effect:+.4f} "
|
|
324
|
+
f"(delta {delta:.4f}, tolerance {tolerance})",
|
|
325
|
+
{
|
|
326
|
+
"measured_effect": measured_effect,
|
|
327
|
+
"recorded_effect": recorded_effect,
|
|
328
|
+
"delta": delta,
|
|
329
|
+
"tolerance": tolerance,
|
|
330
|
+
},
|
|
331
|
+
)
|