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.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
@@ -0,0 +1,446 @@
1
+ """Commands that vouch for records: ``reproduce``, ``sign`` and ``signatures``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from openinterp.cli import cli, wiki_dir_option
11
+ from openinterp.cli_memory import memory_option
12
+ from openinterp.core.paths import experiments_dir
13
+
14
+
15
+ @cli.command()
16
+ @click.argument("records", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path))
17
+ @click.option(
18
+ "--base-ref",
19
+ help="Reproduce every record added or changed since this git ref (e.g. origin/main).",
20
+ )
21
+ @click.option(
22
+ "--sample",
23
+ type=click.IntRange(min=1),
24
+ help="Reproduce this many records of the registry, chosen in rotation by --date. Implies "
25
+ "--include-signed, and compares only the measurements of older protocols' records.",
26
+ )
27
+ @click.option(
28
+ "--date",
29
+ "sample_date",
30
+ type=click.DateTime(formats=["%Y-%m-%d"]),
31
+ help="The day that selects the --sample records (default: today, UTC).",
32
+ )
33
+ @click.option(
34
+ "--time-budget",
35
+ type=click.FloatRange(min=0),
36
+ help="Minutes after which no new record is started. The records not reached are listed "
37
+ "and don't fail the run. Leave room for the slowest record within any outer time limit.",
38
+ )
39
+ @click.option(
40
+ "--include-signed",
41
+ is_flag=True,
42
+ help="Reproduce signed records too. A signature that doesn't verify still fails. A "
43
+ "signature that landed outside its key's validity window is reproduced anyway.",
44
+ )
45
+ @wiki_dir_option()
46
+ @click.option(
47
+ "--atol",
48
+ default=1e-3,
49
+ show_default=True,
50
+ type=click.FloatRange(min=0),
51
+ help="Absolute tolerance, as a fraction of the neuron's max activation.",
52
+ )
53
+ @click.option("--rtol", default=1e-2, show_default=True, type=click.FloatRange(min=0))
54
+ @memory_option
55
+ @click.option(
56
+ "--device",
57
+ default="auto",
58
+ show_default=True,
59
+ help="cpu, cuda, mps or auto (CUDA, else MPS, else CPU).",
60
+ )
61
+ @click.option(
62
+ "--corpus",
63
+ "corpora",
64
+ multiple=True,
65
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
66
+ help="A scan corpus (repeatable): re-draw the corpus sample of each record that "
67
+ "scanned it, matched by SHA-256, and check it holds the top examples. The built-in "
68
+ "corpus is always known. Other records get a warning.",
69
+ )
70
+ @click.option(
71
+ "--allow-model",
72
+ "allowed_models",
73
+ multiple=True,
74
+ help="Only load these Hugging Face models (repeatable); records naming any other model "
75
+ "fail without a download. Default: any model.",
76
+ )
77
+ @click.option(
78
+ "--allowed-signers",
79
+ default=Path(".github/allowed_signers"),
80
+ show_default=True,
81
+ type=click.Path(dir_okay=False, path_type=Path),
82
+ help="Records signed by a key listed here skip reproduction. With --base-ref the file "
83
+ "is read from that ref, so a branch can't add its own key.",
84
+ )
85
+ def reproduce(
86
+ records,
87
+ base_ref,
88
+ sample,
89
+ sample_date,
90
+ time_budget,
91
+ include_signed,
92
+ wiki_dir,
93
+ atol,
94
+ rtol,
95
+ device,
96
+ corpora,
97
+ allowed_models,
98
+ allowed_signers,
99
+ ):
100
+ """Re-run experiment records against their pinned model and compare (no LLM calls).
101
+
102
+ Each record's scan evidence, counterfactual deltas and review are measured again
103
+ with the model revision and dtype it pins, under its own review policy. Exits 1 if
104
+ any number differs beyond tolerance or any pass/fail, gate or status differs.
105
+ """
106
+ import os
107
+
108
+ from openinterp.core.errata import load_errata
109
+ from openinterp.reproduce import Tolerance
110
+ from openinterp.signing import allowed_signers_text, landing_times
111
+
112
+ paths = _chosen_records(records, base_ref, sample, sample_date, wiki_dir)
113
+ if not paths:
114
+ click.echo("No records to reproduce.")
115
+ return
116
+ in_actions = os.environ.get("GITHUB_ACTIONS") == "true"
117
+ reproducer = _Reproducer(
118
+ signers=allowed_signers_text(allowed_signers, base_ref),
119
+ landed=landing_times(base_ref or "HEAD"),
120
+ tolerance=Tolerance(atol=atol, rtol=rtol),
121
+ scan_corpora=scan_corpora(corpora),
122
+ corpus_given=bool(corpora),
123
+ allowed_models=allowed_models,
124
+ device=device,
125
+ include_signed=include_signed or sample is not None,
126
+ measurements_only=sample is not None,
127
+ errata=load_errata(wiki_dir),
128
+ )
129
+ results, unreached = _check_all(reproducer, paths, in_actions, time_budget)
130
+ failed = sum(bool(problems) for _, problems, _ in results)
131
+ summary = f"{len(results) - failed}/{len(results)} record(s) reproduced."
132
+ if unreached:
133
+ summary += f" {len(unreached)} not reached in the {time_budget:g}-minute time budget."
134
+ click.echo(summary)
135
+ if in_actions and os.environ.get("GITHUB_STEP_SUMMARY"):
136
+ _step_summary(os.environ["GITHUB_STEP_SUMMARY"], summary, results, unreached)
137
+ if failed:
138
+ raise SystemExit(1)
139
+
140
+
141
+ def _check_all(reproducer, paths, in_actions: bool, time_budget: float | None):
142
+ """Check each record in turn. Return the results, and the records not started.
143
+
144
+ No record starts once ``time_budget`` minutes have passed.
145
+ """
146
+ import time
147
+
148
+ start = time.monotonic()
149
+ results: list[tuple[Path, list[str], list[str]]] = []
150
+ for n, path in enumerate(paths):
151
+ if time_budget is not None and time.monotonic() - start >= time_budget * 60:
152
+ for later in paths[n:]:
153
+ click.echo(f"NOT REACHED: {later}")
154
+ return results, paths[n:]
155
+ status, problems, warnings = reproducer.check(path)
156
+ results.append((path, problems, warnings))
157
+ _report(path, status, problems, warnings, in_actions)
158
+ return results, []
159
+
160
+
161
+ def _chosen_records(records, base_ref, sample, sample_date, wiki_dir) -> list[Path]:
162
+ """Return the named records, then the branch's changes, then the day's sample."""
163
+ from datetime import UTC, datetime
164
+
165
+ from openinterp.checks import changed_records
166
+ from openinterp.reproduce import rotating_sample
167
+
168
+ paths = list(records)
169
+ if base_ref:
170
+ paths += [p for p in changed_records(wiki_dir, base_ref) if p not in paths]
171
+ if sample:
172
+ day = (sample_date or datetime.now(UTC)).date()
173
+ registry = sorted(experiments_dir(wiki_dir).rglob("*.json"))
174
+ paths += [p for p in rotating_sample(registry, sample, day) if p not in paths]
175
+ return paths
176
+
177
+
178
+ @dataclass
179
+ class _Reproducer:
180
+ """Checks one record at a time, loading each pinned model once."""
181
+
182
+ signers: object
183
+ landed: dict # record path -> when its signature landed (signing.landing_times)
184
+ tolerance: object
185
+ scan_corpora: dict[str, list[str]] # documents by the SHA-256 of their corpus file
186
+ allowed_models: tuple[str, ...]
187
+ device: str
188
+ corpus_given: bool = False # --corpus given: warn for records it doesn't cover
189
+ include_signed: bool = False
190
+ measurements_only: bool = False # for records from another protocol version
191
+ errata: list = field(default_factory=list) # wiki/errata.jsonl (core/errata.py)
192
+ runners: dict[tuple, object] = field(default_factory=dict)
193
+
194
+ def check(self, path: Path) -> tuple[str, list[str], list[str]]:
195
+ """(status, mismatches, warnings) for the record at ``path``."""
196
+ from pydantic import ValidationError
197
+
198
+ from openinterp.core.schemas import AuditRecord
199
+ from openinterp.reproduce import reproduce_record
200
+ from openinterp.signing import SignatureError, SignatureOutsideWindow, verify_record
201
+
202
+ notes = []
203
+ try:
204
+ signer = verify_record(path, self.signers, self.landed.get(path.resolve()))
205
+ except SignatureOutsideWindow as exc:
206
+ signer, notes = None, [f"{exc}, so it is reproduced"]
207
+ except SignatureError as exc:
208
+ return "FAILED", [str(exc)], []
209
+ if signer and not self.include_signed:
210
+ return f"SIGNED by {signer} (reproduction skipped)", [], []
211
+ try:
212
+ record = AuditRecord.model_validate_json(path.read_text(encoding="utf-8"))
213
+ except ValidationError:
214
+ return "FAILED", ["invalid experiment record"], []
215
+ record, aside = self.corrected(record, notes)
216
+ if aside:
217
+ return f"SET ASIDE by erratum {aside} (reproduction skipped)", [], notes
218
+ reason = self.refusal(record)
219
+ if reason:
220
+ return "FAILED", [reason], notes
221
+ runner = self.runner(record)
222
+ report = reproduce_record(
223
+ record,
224
+ runner,
225
+ self.tolerance,
226
+ path,
227
+ self.scan_corpus(record, path, notes),
228
+ measurements_only=self.measurements_only,
229
+ )
230
+ status = "FAILED" if report.mismatches else "REPRODUCED"
231
+ return status, report.mismatches, notes + report.warnings
232
+
233
+ def scan_corpus(self, record, path: Path, notes: list[str]):
234
+ """(documents, SHA-256) of the corpus that ``record`` scanned, if it is known here."""
235
+ prov = record.submission.provenance
236
+ if prov is None or prov.corpus_sample is None:
237
+ return None
238
+ if prov.corpus_sha256 in self.scan_corpora:
239
+ return self.scan_corpora[prov.corpus_sha256], prov.corpus_sha256
240
+ if self.corpus_given:
241
+ notes.append(
242
+ "no corpus given is the scan's, so its sample was not checked. "
243
+ + _rebuild_hint(record, path)
244
+ )
245
+ return None
246
+
247
+ def corrected(self, record, notes: list[str]):
248
+ """``record`` with its errata's edits, and the id of an erratum that sets it aside.
249
+
250
+ A harness fix can change what a record reproduces to. Its erratum's edits give the
251
+ corrected values, so those are the values to check.
252
+ """
253
+ from openinterp.core.errata import apply_errata, covering
254
+
255
+ found = covering(self.errata, record)
256
+ aside = next((x.id for x in found if x.set_aside), None)
257
+ if aside or not found:
258
+ return record, aside
259
+ notes.append(f"checked with the edits of erratum {', '.join(x.id for x in found)}")
260
+ return apply_errata(record, found)[0], None
261
+
262
+ def refusal(self, record) -> str | None:
263
+ """Why ``record`` can't be reproduced here, or ``None``."""
264
+ from openinterp.reproduce import check_protocol
265
+
266
+ model, allowed = record.submission.identifier.model_name, self.allowed_models
267
+ reason = None if self.measurements_only else check_protocol(record)
268
+ if allowed and model not in allowed:
269
+ reason = f"model {model} is not on the allow-list ({', '.join(allowed)})"
270
+ elif allowed and not _preset_dictionary(record):
271
+ reason = "its dictionary is not a preset's (openinterp/harness/dictionaries.py)"
272
+ return reason
273
+
274
+ def runner(self, record):
275
+ from openinterp.cli import _load_runner
276
+ from openinterp.half_precision import reproduction_dtype
277
+ from openinterp.harness.units import attn_implementation
278
+ from openinterp.reproduce import pinned_environment
279
+
280
+ model = record.submission.identifier.model_name
281
+ revision, pinned = pinned_environment(record)
282
+ dtype = reproduction_dtype(pinned)
283
+ attn = attn_implementation(record.submission.identifier)
284
+ key = (model, revision, dtype, attn)
285
+ if key not in self.runners:
286
+ self.runners[key] = _load_runner(model, self.device, dtype, revision, attn)
287
+ return self.runners[key]
288
+
289
+
290
+ def _report(path: Path, status: str, problems, warnings, in_actions: bool) -> None:
291
+ """One record's result, as GitHub Actions annotations when running there."""
292
+ click.echo(f"{status}: {path}")
293
+ for message in problems:
294
+ click.echo(f"::error file={path}::{message}" if in_actions else f" mismatch: {message}")
295
+ for message in warnings:
296
+ click.echo(f"::warning file={path}::{message}" if in_actions else f" warning: {message}")
297
+
298
+
299
+ def _step_summary(path: str, summary: str, results, unreached=()) -> None:
300
+ with open(path, "a", encoding="utf-8") as fh:
301
+ fh.write(f"### Reproduction\n\n{summary}\n\n")
302
+ for record, problems, _ in results:
303
+ fh.write(f"- {'❌' if problems else '✅'} `{record}`\n")
304
+ fh.writelines(f" - {message}\n" for message in problems)
305
+ fh.writelines(f"- ⏭️ `{record}` (not reached)\n" for record in unreached)
306
+
307
+
308
+ def _preset_dictionary(record) -> bool:
309
+ """Check that a feature record's dictionary is on the allow-list.
310
+
311
+ A feature record pins the weights of its dictionary's preset. So the allow-list also
312
+ limits which dictionaries are downloaded. Other records have none to check.
313
+ """
314
+ from openinterp.harness.dictionaries import DICTIONARIES
315
+
316
+ ident, profile = record.submission.identifier, record.submission.dictionary_profile
317
+ if ident.kind != "dictionary_feature" or profile is None:
318
+ return True
319
+ spec = DICTIONARIES.get(ident.dictionary)
320
+ return spec is not None and (spec.repo, spec.revision) == (profile.repo, profile.revision)
321
+
322
+
323
+ @cli.command()
324
+ @click.argument(
325
+ "records", nargs=-1, required=True, type=click.Path(exists=True, dir_okay=False, path_type=Path)
326
+ )
327
+ @click.option(
328
+ "--key",
329
+ default=Path("~/.ssh/openinterp_signing"),
330
+ show_default=True,
331
+ envvar="OPENINTERP_SIGNING_KEY",
332
+ type=click.Path(dir_okay=False, path_type=Path),
333
+ help="Private SSH key to sign with (or $OPENINTERP_SIGNING_KEY); its public key must be "
334
+ "in --allowed-signers. Use a key kept for signing, not your login key.",
335
+ )
336
+ @click.option(
337
+ "--allowed-signers",
338
+ default=Path(".github/allowed_signers"),
339
+ show_default=True,
340
+ type=click.Path(dir_okay=False, path_type=Path),
341
+ )
342
+ def sign(records, key, allowed_signers):
343
+ """Vouch for experiment records with your SSH key, so CI skips reproducing them.
344
+
345
+ Writes <id>.json.sig next to each record; the record itself is not changed. The
346
+ verdict still comes from the review protocol: a signature only says you vouch that
347
+ the record is what it claims to be. Commit the .sig files with the records.
348
+ """
349
+ from openinterp.signing import (
350
+ SignatureError,
351
+ SignatureOutsideWindow,
352
+ allowed_signers_text,
353
+ sign_record,
354
+ verify_record,
355
+ )
356
+
357
+ signers = allowed_signers_text(allowed_signers)
358
+ for path in records:
359
+ sig = sign_record(path, key.expanduser())
360
+ try:
361
+ signer = verify_record(path, signers)
362
+ except SignatureOutsideWindow as exc:
363
+ sig.unlink()
364
+ click.echo(f"Did not sign {path}: {exc}.", err=True)
365
+ raise SystemExit(1) from None
366
+ except SignatureError:
367
+ click.echo(
368
+ f"Wrote {sig}, but {key}.pub is not in {allowed_signers}, so CI will reject "
369
+ "it. Add it with:\n"
370
+ f' echo "<your-name> namespaces=\\"openinterp-record\\" $(cat {key}.pub)" '
371
+ f">> {allowed_signers}",
372
+ err=True,
373
+ )
374
+ raise SystemExit(1) from None
375
+ click.echo(f"Signed {path} as {signer}: {sig}")
376
+
377
+
378
+ @cli.command()
379
+ @click.argument("records", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path))
380
+ @click.option(
381
+ "--ref",
382
+ default="HEAD",
383
+ show_default=True,
384
+ help="Check each signature at the time it landed on this ref's first-parent history.",
385
+ )
386
+ @click.option(
387
+ "--allowed-signers",
388
+ default=Path(".github/allowed_signers"),
389
+ show_default=True,
390
+ type=click.Path(dir_okay=False, path_type=Path),
391
+ )
392
+ @click.option("--paths-only", is_flag=True, help="Print only the paths, one per line.")
393
+ @wiki_dir_option()
394
+ def signatures(records, ref, allowed_signers, paths_only, wiki_dir):
395
+ """List signed records whose signature CI must not trust (default: every record).
396
+
397
+ Each signature is checked at the time it landed on --ref, against the validity
398
+ window of its key in --allowed-signers. After you revoke a key with valid-before
399
+ (SECURITY.md), use it to find the records signed in its exposure window. Then
400
+ reproduce them: open-interpretability reproduce --include-signed \\
401
+ $(open-interpretability signatures --paths-only).
402
+ """
403
+ from openinterp.signing import (
404
+ SignatureError,
405
+ allowed_signers_text,
406
+ landing_times,
407
+ signature_path,
408
+ verify_record,
409
+ )
410
+
411
+ paths = list(records) or sorted(experiments_dir(wiki_dir).rglob("*.json"))
412
+ paths = [p for p in paths if signature_path(p).exists()]
413
+ signers, landed = allowed_signers_text(allowed_signers), landing_times(ref)
414
+ untrusted = 0
415
+ for path in paths:
416
+ at = landed.get(path.resolve())
417
+ try:
418
+ verify_record(path, signers, at)
419
+ except SignatureError as exc:
420
+ untrusted += 1
421
+ when = "not landed" if at is None else f"landed {at:%Y-%m-%d %H:%M}Z"
422
+ click.echo(path if paths_only else f"{path} ({when}): {exc}")
423
+ if not paths_only:
424
+ click.echo(f"{untrusted} of {len(paths)} signed record(s) not trusted.")
425
+
426
+
427
+ def scan_corpora(paths) -> dict[str, list[str]]:
428
+ """Documents by the SHA-256 of their corpus file: the built-in corpus and ``paths``."""
429
+ from openinterp.cli_evidence import read_corpus
430
+
431
+ corpora = {}
432
+ for path in (None, *paths):
433
+ texts, _, sha, _ = read_corpus(path)
434
+ corpora[sha] = texts
435
+ return corpora
436
+
437
+
438
+ def _rebuild_hint(record, path: Path) -> str:
439
+ """How to get the corpus that ``record`` scanned, from its source or the committed table."""
440
+ from openinterp.corpus_rebuild import scanned_source
441
+
442
+ try:
443
+ scanned_source(record.submission)
444
+ except ValueError as error:
445
+ return f"Its corpus can't be rebuilt: {error}."
446
+ return f"Rebuild it with `open-interpretability corpus --same-as {path} --out <file>`."