open-interpretability 0.0.20__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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Errata: harness bugs that affected signed records (``docs/PROTOCOL.md`` §5).
|
|
2
|
+
|
|
3
|
+
Records cannot be rewritten. A maintainer who finds a bug that affected some of them
|
|
4
|
+
appends an erratum to ``wiki/errata.jsonl``. Then they sign the file with
|
|
5
|
+
``open-interpretability sign wiki/errata.jsonl``. Each line is one :class:`Erratum`. It names the
|
|
6
|
+
records it covers (a list of record paths, a :class:`ErratumRule`, or both), the bug, and
|
|
7
|
+
the commit that fixed it. It either lists :class:`Edit` objects for the records, or sets
|
|
8
|
+
the records aside.
|
|
9
|
+
|
|
10
|
+
The record files never change. The site build applies the edits to a copy of each covered
|
|
11
|
+
record (:func:`apply_errata`). So pages, standings and ``registry.parquet`` read the
|
|
12
|
+
corrected values. ``open-interpretability reproduce`` checks the corrected record, and skips a
|
|
13
|
+
record that is set aside.
|
|
14
|
+
|
|
15
|
+
``open-interpretability check`` verifies the file (:mod:`openinterp.errata_checks`). Errata have
|
|
16
|
+
no page of their own. The site shows each erratum on the experiments it covers, with the old
|
|
17
|
+
and new value of each edit. The standing rule (:mod:`openinterp.builder.standing`) leaves
|
|
18
|
+
out the records that are set aside.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from datetime import date as Date
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
29
|
+
|
|
30
|
+
from pydantic import AwareDatetime, Field, JsonValue, model_validator
|
|
31
|
+
|
|
32
|
+
from openinterp.core.paths import DEFAULT_WIKI_DIR
|
|
33
|
+
from openinterp.core.strict import StrictModel
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from openinterp.core.schemas import AuditRecord
|
|
37
|
+
|
|
38
|
+
ERRATA_FILE = "errata.jsonl"
|
|
39
|
+
"""The errata file's name in the wiki directory."""
|
|
40
|
+
|
|
41
|
+
FIXED_PATHS = ("submission.identifier", "verdict.timestamp")
|
|
42
|
+
"""Fields an edit can't change: they give the record's unit and id, so its place."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Edit(StrictModel):
|
|
46
|
+
"""One corrected field of a record: its dotted path and its new value.
|
|
47
|
+
|
|
48
|
+
The path names keys and list indices of the record's JSON, for example
|
|
49
|
+
``verdict.status`` or ``submission.top_examples.0.max_activation``. The field must exist.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
path: str = Field(pattern=r"^[a-z_]+(\.([a-z_]+|[0-9]+))*$")
|
|
53
|
+
value: JsonValue
|
|
54
|
+
|
|
55
|
+
@model_validator(mode="after")
|
|
56
|
+
def _not_fixed(self) -> Edit:
|
|
57
|
+
if any(self.path == p or self.path.startswith(p + ".") for p in FIXED_PATHS):
|
|
58
|
+
raise ValueError(f"an edit can't change {self.path}: it places the record")
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class AppliedEdit:
|
|
64
|
+
"""An edit as applied to one record: which erratum made it, and the value before."""
|
|
65
|
+
|
|
66
|
+
erratum: str # the erratum's id
|
|
67
|
+
path: str
|
|
68
|
+
before: Any
|
|
69
|
+
after: Any
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ErratumRule(StrictModel):
|
|
73
|
+
"""A rule that selects records. A record matches when it matches every field given.
|
|
74
|
+
|
|
75
|
+
A field that is ``None`` matches every record. A record without a review policy has no
|
|
76
|
+
protocol version, so a rule with ``protocol_versions`` does not match it.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
models: list[str] | None = Field(default=None, min_length=1, description="Model names.")
|
|
80
|
+
kinds: list[Literal["mlp_neuron_pole", "attn_head", "dictionary_feature"]] | None = Field(
|
|
81
|
+
default=None, min_length=1
|
|
82
|
+
)
|
|
83
|
+
protocol_versions: list[str] | None = Field(default=None, min_length=1)
|
|
84
|
+
harness_versions: list[str] | None = Field(default=None, min_length=1)
|
|
85
|
+
reviewed_from: AwareDatetime | None = Field(
|
|
86
|
+
default=None, description="First verdict time that matches (inclusive)."
|
|
87
|
+
)
|
|
88
|
+
reviewed_before: AwareDatetime | None = Field(
|
|
89
|
+
default=None, description="Verdict time from which records no longer match."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="after")
|
|
93
|
+
def _selects_something(self) -> ErratumRule:
|
|
94
|
+
if all(value is None for value in self.model_dump().values()):
|
|
95
|
+
raise ValueError("a rule needs at least one field; list the records instead")
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def matches(self, record: AuditRecord) -> bool:
|
|
99
|
+
"""Return whether ``record`` matches every field of the rule."""
|
|
100
|
+
ident, verdict = record.submission.identifier, record.verdict
|
|
101
|
+
provenance = record.submission.provenance
|
|
102
|
+
policy = verdict.policy if verdict is not None else None
|
|
103
|
+
checks = (
|
|
104
|
+
(self.models, ident.model_name),
|
|
105
|
+
(self.kinds, ident.kind),
|
|
106
|
+
(self.protocol_versions, policy.protocol_version if policy else None),
|
|
107
|
+
(self.harness_versions, provenance.harness_version if provenance else None),
|
|
108
|
+
)
|
|
109
|
+
if any(allowed is not None and value not in allowed for allowed, value in checks):
|
|
110
|
+
return False
|
|
111
|
+
when = verdict.timestamp if verdict is not None else None
|
|
112
|
+
if self.reviewed_from is not None and (when is None or when < self.reviewed_from):
|
|
113
|
+
return False
|
|
114
|
+
return self.reviewed_before is None or (when is not None and when < self.reviewed_before)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Erratum(StrictModel):
|
|
118
|
+
"""One harness bug and the records it affected. One line of ``wiki/errata.jsonl``."""
|
|
119
|
+
|
|
120
|
+
id: str = Field(pattern=r"^[a-z0-9][a-z0-9-]*$", description="Unique; the site's anchor.")
|
|
121
|
+
date: Date
|
|
122
|
+
title: str = Field(min_length=1, description="One line for the banner.")
|
|
123
|
+
bug: str = Field(min_length=1, description="What was wrong, and what it did to records.")
|
|
124
|
+
fixed_in: str = Field(pattern=r"^[0-9a-f]{7,40}$", description="The commit that fixed it.")
|
|
125
|
+
edits: list[Edit] = Field(default_factory=list, description="Corrections to each record.")
|
|
126
|
+
set_aside: bool = Field(default=False, description="The records count toward no status.")
|
|
127
|
+
records: list[str] = Field(
|
|
128
|
+
default_factory=list,
|
|
129
|
+
description="Covered records, as paths relative to wiki/experiments/.",
|
|
130
|
+
)
|
|
131
|
+
rule: ErratumRule | None = None
|
|
132
|
+
|
|
133
|
+
@model_validator(mode="after")
|
|
134
|
+
def _covers_something(self) -> Erratum:
|
|
135
|
+
if not self.records and self.rule is None:
|
|
136
|
+
raise ValueError("an erratum needs records, a rule, or both")
|
|
137
|
+
if bool(self.edits) == self.set_aside:
|
|
138
|
+
raise ValueError("an erratum either lists edits or sets its records aside")
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def covers(self, record: AuditRecord) -> bool:
|
|
142
|
+
"""Return whether the erratum covers ``record``: it is listed or matches the rule."""
|
|
143
|
+
return record_ref(record) in self.records or (
|
|
144
|
+
self.rule is not None and self.rule.matches(record)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def errata_path(wiki_dir: Path | str = DEFAULT_WIKI_DIR) -> Path:
|
|
149
|
+
"""Where the errata file is: ``<wiki_dir>/errata.jsonl``."""
|
|
150
|
+
return Path(wiki_dir) / ERRATA_FILE
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def record_ref(record: AuditRecord) -> str:
|
|
154
|
+
"""How an erratum names ``record``: its path relative to ``wiki/experiments/``."""
|
|
155
|
+
from openinterp.builder.wiki import archive_path
|
|
156
|
+
from openinterp.core.paths import experiments_dir
|
|
157
|
+
|
|
158
|
+
return archive_path(Path(), record).relative_to(experiments_dir(Path())).as_posix()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def parse_errata(text: str) -> list[Erratum]:
|
|
162
|
+
"""Parse the content of an errata file. Raises ``ValueError`` naming the bad line."""
|
|
163
|
+
errata = []
|
|
164
|
+
for number, line in enumerate(text.splitlines(), start=1):
|
|
165
|
+
try:
|
|
166
|
+
errata.append(Erratum.model_validate_json(line))
|
|
167
|
+
except ValueError as exc:
|
|
168
|
+
raise ValueError(f"line {number}: {exc}") from exc
|
|
169
|
+
return errata
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def load_errata(wiki_dir: Path | str = DEFAULT_WIKI_DIR) -> list[Erratum]:
|
|
173
|
+
"""Every erratum in ``wiki_dir``, in file order; none if there is no errata file."""
|
|
174
|
+
path = errata_path(wiki_dir)
|
|
175
|
+
return parse_errata(path.read_text(encoding="utf-8")) if path.exists() else []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def covering(errata: Sequence[Erratum], record: AuditRecord) -> list[Erratum]:
|
|
179
|
+
"""The errata in ``errata`` that cover ``record``, in file order."""
|
|
180
|
+
return [erratum for erratum in errata if erratum.covers(record)]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def apply_errata(
|
|
184
|
+
record: AuditRecord, errata: Sequence[Erratum]
|
|
185
|
+
) -> tuple[AuditRecord, list[AppliedEdit]]:
|
|
186
|
+
"""``record`` with the edits of ``errata`` applied in order, and what each edit changed.
|
|
187
|
+
|
|
188
|
+
Returns ``record`` itself when no erratum edits it. Raises ``ValueError`` when an edit's
|
|
189
|
+
path is not in the record, or the edited record is not valid.
|
|
190
|
+
"""
|
|
191
|
+
from openinterp.core.schemas import AuditRecord
|
|
192
|
+
|
|
193
|
+
edits = [(erratum.id, edit) for erratum in errata for edit in erratum.edits]
|
|
194
|
+
if not edits:
|
|
195
|
+
return record, []
|
|
196
|
+
data = record.model_dump(mode="json")
|
|
197
|
+
applied = []
|
|
198
|
+
for erratum_id, edit in edits:
|
|
199
|
+
before = _replace(data, edit.path, edit.value)
|
|
200
|
+
applied.append(AppliedEdit(erratum_id, edit.path, before, edit.value))
|
|
201
|
+
try:
|
|
202
|
+
return AuditRecord.model_validate_json(json.dumps(data)), applied
|
|
203
|
+
except ValueError as exc:
|
|
204
|
+
raise ValueError(f"the edited record is not valid: {exc}") from exc
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _replace(data: Any, path: str, value: Any) -> Any:
|
|
208
|
+
"""Set the field at dotted ``path`` in ``data`` to ``value``; return its old value."""
|
|
209
|
+
*parents, last = path.split(".")
|
|
210
|
+
node = data
|
|
211
|
+
try:
|
|
212
|
+
for part in parents:
|
|
213
|
+
node = node[int(part)] if isinstance(node, list) else node[part]
|
|
214
|
+
key = int(last) if isinstance(node, list) else last
|
|
215
|
+
before = node[key]
|
|
216
|
+
except (KeyError, IndexError, TypeError, ValueError):
|
|
217
|
+
raise ValueError(f"the record has no field {path}") from None
|
|
218
|
+
node[key] = value
|
|
219
|
+
return before
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def corrected(record: AuditRecord, errata: Sequence[Erratum]) -> tuple[AuditRecord, bool]:
|
|
223
|
+
"""``record`` with the edits of the errata that cover it, and whether one sets it aside."""
|
|
224
|
+
found = covering(errata, record)
|
|
225
|
+
return apply_errata(record, found)[0], any(erratum.set_aside for erratum in found)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def standing_records(
|
|
229
|
+
records: Sequence[AuditRecord], errata: Sequence[Erratum]
|
|
230
|
+
) -> list[AuditRecord]:
|
|
231
|
+
"""The records that count toward a status, with their errata's edits applied.
|
|
232
|
+
|
|
233
|
+
This is the standing rule's first step (:mod:`openinterp.builder.standing`), for readers
|
|
234
|
+
that don't build the site's tree. A record that an erratum sets aside is left out. The
|
|
235
|
+
exception is a unit whose records are all set aside: then its status rests on them, so
|
|
236
|
+
they stay. The order of ``records`` is kept.
|
|
237
|
+
"""
|
|
238
|
+
fixed = [(record, *corrected(record, errata)) for record in records]
|
|
239
|
+
counted = {r.submission.identifier.slug for r, _, aside in fixed if not aside}
|
|
240
|
+
return [
|
|
241
|
+
edited
|
|
242
|
+
for record, edited, aside in fixed
|
|
243
|
+
if not aside or record.submission.identifier.slug not in counted
|
|
244
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Bulky record evidence in a content-addressed store, outside the repository (issue #3254).
|
|
2
|
+
|
|
3
|
+
Two fields make up about 59% of a record: ``submission.top_examples`` and
|
|
4
|
+
``verdict.explanation_scores``. :func:`split` takes them out of a record's JSON. Each one
|
|
5
|
+
becomes a reference, ``{"evidence_sha256": "<hash>"}``, to a blob that holds the field's
|
|
6
|
+
exact JSON. :func:`resolve` puts each blob back, and refuses a blob whose hash is wrong.
|
|
7
|
+
|
|
8
|
+
A record's signature covers its file's bytes, so it covers the hashes. A hash covers its
|
|
9
|
+
blob. So a signed slim record still vouches for all of its evidence. Records are
|
|
10
|
+
write-once, so the records that are already whole stay whole.
|
|
11
|
+
|
|
12
|
+
Blobs live in a local store, ``~/.cache/openinterp/evidence/`` by default
|
|
13
|
+
(``$OPENINTERP_EVIDENCE_DIR``). A blob that isn't there is fetched, read-only, from the
|
|
14
|
+
public dataset :data:`EVIDENCE_REPO`. :func:`openinterp.builder.wiki.archive_record` writes
|
|
15
|
+
slim records unless ``OPENINTERP_OFFLOAD_EVIDENCE=0``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
FIELDS = (("submission", "top_examples"), ("verdict", "explanation_scores"))
|
|
27
|
+
"""The record fields that :func:`split` moves to the store: (parent key, field)."""
|
|
28
|
+
REF_KEY = "evidence_sha256"
|
|
29
|
+
EVIDENCE_REPO = "sackfield/open-interpretability-evidence"
|
|
30
|
+
"""The public Hugging Face dataset that holds the blobs, by path ``<sha[:2]>/<sha>.json``."""
|
|
31
|
+
DIR_ENV = "OPENINTERP_EVIDENCE_DIR"
|
|
32
|
+
OFFLOAD_ENV = "OPENINTERP_OFFLOAD_EVIDENCE" # "0" makes archive_record write whole records
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EvidenceMissing(ValueError):
|
|
36
|
+
"""A record refers to a blob that neither the local store nor the dataset holds."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def offload_enabled() -> bool:
|
|
40
|
+
"""Whether new archived records are written slim: yes, unless the variable is ``0``.
|
|
41
|
+
|
|
42
|
+
Phase 3 of issue #3254 turned this on. CI uploads the blobs from ``evidence/<branch>``.
|
|
43
|
+
"""
|
|
44
|
+
return os.environ.get(OFFLOAD_ENV, "1") != "0"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def store_dir() -> Path:
|
|
48
|
+
"""The local blob store: ``$OPENINTERP_EVIDENCE_DIR``, else ``~/.cache/openinterp/evidence``."""
|
|
49
|
+
return Path(os.environ.get(DIR_ENV) or Path.home() / ".cache" / "openinterp" / "evidence")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def blob_name(sha: str) -> str:
|
|
53
|
+
"""A blob's path in the store and in the dataset."""
|
|
54
|
+
return f"{sha[:2]}/{sha}.json"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def canonical(value: Any) -> bytes:
|
|
58
|
+
"""The bytes that a blob holds for ``value``: sorted keys, no spaces, UTF-8."""
|
|
59
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def sha256(data: bytes) -> str:
|
|
63
|
+
"""The hex SHA-256 of ``data``."""
|
|
64
|
+
return hashlib.sha256(data).hexdigest()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_ref(value: Any) -> bool:
|
|
68
|
+
"""Whether ``value`` is a reference that :func:`split` wrote."""
|
|
69
|
+
return isinstance(value, dict) and set(value) == {REF_KEY}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def put(data: bytes, root: Path | None = None) -> str:
|
|
73
|
+
"""Store ``data`` in the local store, and return its hash."""
|
|
74
|
+
sha = sha256(data)
|
|
75
|
+
path = (root or store_dir()) / blob_name(sha)
|
|
76
|
+
if not path.exists():
|
|
77
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
tmp = path.with_suffix(f".{os.getpid()}.tmp")
|
|
79
|
+
tmp.write_bytes(data)
|
|
80
|
+
tmp.replace(path) # atomic: a reader sees no blob or a whole one
|
|
81
|
+
return sha
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _fetch(sha: str) -> bytes | None:
|
|
85
|
+
"""The blob from the public dataset, or ``None`` if it can't be had."""
|
|
86
|
+
try:
|
|
87
|
+
from huggingface_hub import hf_hub_download
|
|
88
|
+
|
|
89
|
+
local = hf_hub_download(EVIDENCE_REPO, blob_name(sha), repo_type="dataset")
|
|
90
|
+
except Exception: # noqa: BLE001 - offline, no dataset yet, or no such blob
|
|
91
|
+
return None
|
|
92
|
+
return Path(local).read_bytes()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get(sha: str, root: Path | None = None, fetch=None) -> bytes:
|
|
96
|
+
"""The blob with hash ``sha``: from the local store, else fetched and stored.
|
|
97
|
+
|
|
98
|
+
``fetch`` defaults to the read-only fetch from :data:`EVIDENCE_REPO`. Raises
|
|
99
|
+
:class:`EvidenceMissing` when the blob can't be had or its hash is wrong.
|
|
100
|
+
"""
|
|
101
|
+
path = (root or store_dir()) / blob_name(sha)
|
|
102
|
+
data = path.read_bytes() if path.exists() else (fetch or _fetch)(sha)
|
|
103
|
+
if data is None:
|
|
104
|
+
raise EvidenceMissing(
|
|
105
|
+
f"evidence blob {sha} is not in {root or store_dir()} or {EVIDENCE_REPO}"
|
|
106
|
+
)
|
|
107
|
+
if sha256(data) != sha:
|
|
108
|
+
raise EvidenceMissing(f"evidence blob {sha} has the wrong hash; the store holds a bad copy")
|
|
109
|
+
if not path.exists():
|
|
110
|
+
put(data, root)
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def split(data: dict, root: Path | None = None) -> dict:
|
|
115
|
+
"""A copy of a record's JSON with each field in :data:`FIELDS` moved to the store.
|
|
116
|
+
|
|
117
|
+
A field that is missing, ``None`` or already a reference stays as it is.
|
|
118
|
+
"""
|
|
119
|
+
out = json.loads(json.dumps(data))
|
|
120
|
+
for parent, field in FIELDS:
|
|
121
|
+
node = out.get(parent)
|
|
122
|
+
if not isinstance(node, dict) or node.get(field) is None or is_ref(node[field]):
|
|
123
|
+
continue
|
|
124
|
+
node[field] = {REF_KEY: put(canonical(node[field]), root)}
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def resolve(data: Any, root: Path | None = None, fetch=None) -> Any:
|
|
129
|
+
"""``data`` with each reference in :data:`FIELDS` replaced by its blob's value.
|
|
130
|
+
|
|
131
|
+
Data with no reference comes back as it is, so whole records cost nothing.
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(data, dict):
|
|
134
|
+
return data
|
|
135
|
+
refs = [
|
|
136
|
+
(parent, field)
|
|
137
|
+
for parent, field in FIELDS
|
|
138
|
+
if isinstance(data.get(parent), dict) and is_ref(data[parent].get(field))
|
|
139
|
+
]
|
|
140
|
+
if not refs:
|
|
141
|
+
return data
|
|
142
|
+
out = dict(data)
|
|
143
|
+
for parent, field in refs:
|
|
144
|
+
sha = data[parent][field][REF_KEY]
|
|
145
|
+
out[parent] = {**out[parent], field: json.loads(get(sha, root, fetch))}
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def resolved_text(json_data: str | bytes | bytearray) -> str | bytes | bytearray:
|
|
150
|
+
"""A record's JSON text with its references resolved; whole records come back as given."""
|
|
151
|
+
text = json_data.decode() if isinstance(json_data, bytes | bytearray) else json_data
|
|
152
|
+
if REF_KEY not in text:
|
|
153
|
+
return json_data
|
|
154
|
+
return json.dumps(resolve(json.loads(text)))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def resolved_dict_text(obj: Any) -> str | None:
|
|
158
|
+
"""A record dict's JSON text with its references resolved, or ``None`` if it has none."""
|
|
159
|
+
resolved = resolve(obj)
|
|
160
|
+
return None if resolved is obj else json.dumps(resolved)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""What a scan shows of a unit.
|
|
2
|
+
|
|
3
|
+
A scan shows a unit's top-activating examples and the histogram of its strengths. For a
|
|
4
|
+
neuron, it shows the same for the other pole.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, model_validator
|
|
10
|
+
|
|
11
|
+
from openinterp.core.identifiers import Pole
|
|
12
|
+
from openinterp.core.strict import StrictModel, UnitFloat
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Histogram(StrictModel):
|
|
16
|
+
"""Counts of strengths in bins; ``bin_edges`` has one more entry than ``counts``."""
|
|
17
|
+
|
|
18
|
+
bin_edges: list[float] = Field(min_length=2)
|
|
19
|
+
counts: list[int]
|
|
20
|
+
|
|
21
|
+
@model_validator(mode="after")
|
|
22
|
+
def _shape(self) -> Histogram:
|
|
23
|
+
if len(self.counts) != len(self.bin_edges) - 1:
|
|
24
|
+
raise ValueError("counts must have exactly len(bin_edges) - 1 entries")
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def share_at_least(self, line: float) -> float:
|
|
28
|
+
"""Return the share of values at or above ``line``.
|
|
29
|
+
|
|
30
|
+
Only bins wholly above ``line`` count, so the result is a lower bound, within one
|
|
31
|
+
bin of the exact share.
|
|
32
|
+
"""
|
|
33
|
+
total = sum(self.counts)
|
|
34
|
+
above = sum(n for lo, n in zip(self.bin_edges, self.counts, strict=False) if lo >= line)
|
|
35
|
+
return above / total if total else 0.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ActivatingExample(StrictModel):
|
|
39
|
+
"""A text snippet with per-token strengths (non-negative magnitudes).
|
|
40
|
+
|
|
41
|
+
For an attention head, ``attends_to`` lists the tokens the peak token attends to
|
|
42
|
+
most, strongest first, with their attention weights in ``attention_weights``.
|
|
43
|
+
``peak_labels`` gives the peak token's label in each of the scan's label sets. A
|
|
44
|
+
document label, such as language, is the label of the token's document. It is
|
|
45
|
+
``None`` when the scan had no label sets, or the peak token has no label in any.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
text: str
|
|
49
|
+
tokens: list[str]
|
|
50
|
+
activations: list[float]
|
|
51
|
+
max_activation: float = Field(ge=0.0)
|
|
52
|
+
max_token_index: int = Field(ge=0)
|
|
53
|
+
attends_to: list[int] = Field(default_factory=list)
|
|
54
|
+
attention_weights: list[UnitFloat] = Field(default_factory=list)
|
|
55
|
+
peak_labels: dict[str, str] | None = None
|
|
56
|
+
|
|
57
|
+
@model_validator(mode="after")
|
|
58
|
+
def _aligned(self) -> ActivatingExample:
|
|
59
|
+
if len(self.tokens) != len(self.activations):
|
|
60
|
+
raise ValueError("tokens and activations must be the same length")
|
|
61
|
+
if self.tokens and self.max_token_index >= len(self.tokens):
|
|
62
|
+
raise ValueError("max_token_index out of range")
|
|
63
|
+
if len(self.attends_to) != len(self.attention_weights):
|
|
64
|
+
raise ValueError("attends_to and attention_weights must be the same length")
|
|
65
|
+
if any(not 0 <= i <= self.max_token_index for i in self.attends_to):
|
|
66
|
+
raise ValueError("attends_to must index tokens up to the peak (attention is causal)")
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OppositePole(StrictModel):
|
|
71
|
+
"""A neuron's other pole, read from the same scan as the audited pole.
|
|
72
|
+
|
|
73
|
+
The neuron's page shows it beside the audited pole. It is reported, not gated, and
|
|
74
|
+
neither agent sees it.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
pole: Pole
|
|
78
|
+
top_examples: list[ActivatingExample]
|
|
79
|
+
activation_histogram: Histogram | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_opposite_pole(model) -> None:
|
|
83
|
+
"""Reject an ``opposite_pole`` that the unit cannot have.
|
|
84
|
+
|
|
85
|
+
Only a neuron pole has an opposite pole, and it must not be the audited pole.
|
|
86
|
+
"""
|
|
87
|
+
if model.opposite_pole is None:
|
|
88
|
+
return
|
|
89
|
+
pole = model.identifier.pole
|
|
90
|
+
if pole is None:
|
|
91
|
+
raise ValueError("opposite_pole is only for neuron poles")
|
|
92
|
+
if model.opposite_pole.pole == pole:
|
|
93
|
+
raise ValueError("opposite_pole must be the pole the identifier does not name")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Exception hierarchy for openinterp."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OpenInterpError(Exception):
|
|
5
|
+
"""Base class for all openinterp errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArchitectureNotSupportedError(OpenInterpError):
|
|
9
|
+
"""The hook runner could not locate MLP layers on the given model."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PatchingError(OpenInterpError):
|
|
13
|
+
"""An activation patching run could not be performed as requested."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShapeMismatchError(PatchingError):
|
|
17
|
+
"""Source and target activations have incompatible shapes."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentError(OpenInterpError):
|
|
21
|
+
"""An LLM agent failed to produce a valid structured response."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegistryError(OpenInterpError):
|
|
25
|
+
"""An audit record on disk is missing or malformed."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SyncError(OpenInterpError):
|
|
29
|
+
"""Deploying the static site to object storage failed."""
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Schemas for scoring an explanation on held-out text (detection, fuzzing, embedding).
|
|
2
|
+
|
|
3
|
+
These follow Delphi (Paulo et al., 2025). A judge sees only the explanation.
|
|
4
|
+
|
|
5
|
+
- *detection*: the judge picks out the held-out windows that activate the pole.
|
|
6
|
+
- *fuzzing*: the judge says whether the marked tokens in a window activate the pole.
|
|
7
|
+
- *embedding*: an embedding model ranks windows by similarity to the explanation.
|
|
8
|
+
|
|
9
|
+
The scores are reported, not gated (docs/PROTOCOL.md §4).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
|
|
16
|
+
from openinterp.core.costs import ExperimentCosts
|
|
17
|
+
from openinterp.core.strict import StrictModel, UnitFloat
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ScoredWindow(StrictModel):
|
|
21
|
+
"""A held-out window as the judge saw it, measured on its own (like a probe)."""
|
|
22
|
+
|
|
23
|
+
text: str
|
|
24
|
+
tokens: list[str]
|
|
25
|
+
activations: list[float]
|
|
26
|
+
quantile: int | None = Field(
|
|
27
|
+
default=None,
|
|
28
|
+
ge=0,
|
|
29
|
+
description="Activation bin of the window's peak among active held-out windows "
|
|
30
|
+
"(0 = weakest); None when the window is inactive.",
|
|
31
|
+
)
|
|
32
|
+
highlighted: list[int] = Field(
|
|
33
|
+
default_factory=list, description="Fuzzing: indices of the tokens marked for the judge."
|
|
34
|
+
)
|
|
35
|
+
label: bool = Field(
|
|
36
|
+
description="What a perfect judge answers: the window is active (detection), or its "
|
|
37
|
+
"marked tokens are exactly its active tokens (fuzzing)."
|
|
38
|
+
)
|
|
39
|
+
judged: bool | None = Field(default=None, description="The judge's answer.")
|
|
40
|
+
similarity: float | None = Field(
|
|
41
|
+
default=None, description="Embedding: cosine between the explanation and the window."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class JudgeScore(StrictModel):
|
|
46
|
+
"""How well the judge's answers match the labels.
|
|
47
|
+
|
|
48
|
+
``recall_by_quantile[q]`` is the share of positive windows in activation bin ``q``
|
|
49
|
+
that the judge got right. It is ``None`` when the bin had no positive windows. It
|
|
50
|
+
shows the weak activations, which the top activations can otherwise hide.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
balanced_accuracy: UnitFloat
|
|
54
|
+
true_positive_rate: UnitFloat | None
|
|
55
|
+
false_positive_rate: UnitFloat | None
|
|
56
|
+
recall_by_quantile: list[UnitFloat | None]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ControlScore(StrictModel):
|
|
60
|
+
"""The judge's scores for a control explanation, on the same windows as the real one.
|
|
61
|
+
|
|
62
|
+
A control shows how much of a score comes from the windows and not from the
|
|
63
|
+
explanation. ``empty`` gives the judge no explanation. ``other_unit`` gives it the
|
|
64
|
+
explanation of another unit of the same model. A real explanation should score well
|
|
65
|
+
above both.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
kind: str = Field(description="'empty' or 'other_unit'.")
|
|
69
|
+
explanation: str = Field(description="What the judge saw; '' for the empty control.")
|
|
70
|
+
source_slug: str | None = Field(
|
|
71
|
+
default=None, description="other_unit: the unit whose explanation this is."
|
|
72
|
+
)
|
|
73
|
+
detection_judged: list[bool] = Field(default_factory=list)
|
|
74
|
+
fuzzing_judged: list[bool] = Field(default_factory=list)
|
|
75
|
+
detection: JudgeScore | None = None
|
|
76
|
+
fuzzing: JudgeScore | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ExplanationScores(StrictModel):
|
|
80
|
+
"""Delphi-style scores of ``explanation`` on windows the proposer never saw.
|
|
81
|
+
|
|
82
|
+
Windows are cut from corpus snippets outside the scan's top examples and measured on
|
|
83
|
+
their own, exactly as the judge reads them. Active windows (peak ≥ ``threshold``) are
|
|
84
|
+
drawn evenly from ``quantiles`` bins of their peak strength, split at
|
|
85
|
+
``quantile_edges``, and matched with as many inactive ones.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
slug: str = Field(description="The unit scored: its identifier's slug.")
|
|
89
|
+
explanation: str = Field(min_length=1)
|
|
90
|
+
threshold: float = Field(gt=0.0, description="Pole strength counted as active.")
|
|
91
|
+
window_tokens: int = Field(gt=0)
|
|
92
|
+
quantiles: int = Field(gt=0)
|
|
93
|
+
quantile_edges: list[float]
|
|
94
|
+
seed: int
|
|
95
|
+
corpus: str
|
|
96
|
+
corpus_sha256: str | None = None
|
|
97
|
+
candidate_windows: int = Field(ge=0, description="Held-out windows measured.")
|
|
98
|
+
active_windows: int = Field(ge=0, description="Of those, windows reaching the threshold.")
|
|
99
|
+
judge_model: str | None = None
|
|
100
|
+
embedding_model: str | None = None
|
|
101
|
+
detection: JudgeScore | None = None
|
|
102
|
+
fuzzing: JudgeScore | None = None
|
|
103
|
+
embedding_auroc: UnitFloat | None = Field(
|
|
104
|
+
default=None,
|
|
105
|
+
description="AUROC of the embedding similarity for active vs inactive detection windows.",
|
|
106
|
+
)
|
|
107
|
+
detection_windows: list[ScoredWindow] = Field(default_factory=list)
|
|
108
|
+
fuzzing_windows: list[ScoredWindow] = Field(default_factory=list)
|
|
109
|
+
fuzzing_decoys: str | None = Field(
|
|
110
|
+
default=None,
|
|
111
|
+
description="How fuzzing decoys were marked: 'matched-shape' (the real marks' count "
|
|
112
|
+
"and spacing, moved) since issue #3505. None for older scores, whose decoys were "
|
|
113
|
+
"random tokens that explanation-free rules could tell apart.",
|
|
114
|
+
)
|
|
115
|
+
rule_baseline: dict[str, float] = Field(
|
|
116
|
+
default_factory=dict,
|
|
117
|
+
description="Balanced accuracy of explanation-free rules on these windows, best cut "
|
|
118
|
+
"chosen on them, so an upper bound: detection_length, fuzzing_contiguous, "
|
|
119
|
+
"fuzzing_count. A score means something only above these.",
|
|
120
|
+
)
|
|
121
|
+
controls: list[ControlScore] = Field(
|
|
122
|
+
default_factory=list,
|
|
123
|
+
description="The same windows judged with control explanations "
|
|
124
|
+
"(open-interpretability score --controls). Each control's answers are scored against "
|
|
125
|
+
"these windows' labels.",
|
|
126
|
+
)
|
|
127
|
+
costs: ExperimentCosts | None = Field(
|
|
128
|
+
default=None,
|
|
129
|
+
description="What scoring cost; a review moves it into the record's provenance.",
|
|
130
|
+
)
|