ruling 0.2.0__tar.gz

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.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
6
+ reports/
7
+ calibration.json
8
+ runs/
9
+ docs/model-design.md
ruling-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bradAGI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ruling-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: ruling
3
+ Version: 0.2.0
4
+ Summary: Open-weight recreation of TypeSafe AI's Jev: typed, probabilistic decisions from a local model, no text generation.
5
+ Project-URL: Homepage, https://github.com/bradAGI/ruling
6
+ Project-URL: Source, https://github.com/bradAGI/ruling
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: fastapi>=0.115
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: huggingface-hub>=0.25
13
+ Requires-Dist: mlx-lm>=0.31
14
+ Requires-Dist: numpy>=1.26
15
+ Requires-Dist: pyarrow>=25.0.1
16
+ Requires-Dist: pydantic>=2.7
17
+ Requires-Dist: transformers>=5.17.0
18
+ Requires-Dist: uvicorn>=0.30
19
+ Provides-Extra: vision
20
+ Requires-Dist: mlx-vlm>=0.7.8; extra == 'vision'
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <img src="assets/banner.png" alt="ruling: typed decisions, no text" width="720" />
25
+ </p>
26
+
27
+ ## ruling: _typed decisions_, not generated text, from open models
28
+
29
+ **Documentation:** [ruling docs](docs/README.md)
30
+
31
+ ----
32
+
33
+ ruling turns any open chat model into a decision engine like TypeSafe's
34
+ [Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev). You send
35
+ a state (text, JSON or images) and typed questions; you get back a probability
36
+ for every option you declared. Nothing is generated, so there is nothing to
37
+ parse and the model cannot invent an answer you did not list. It runs on your
38
+ own machine, serves TypeSafe's API so their client works against it unchanged,
39
+ and learns from the outcomes of the decisions it serves.
40
+
41
+ On the 256 public judgments that carry Jev's own answers, a 19 GB open model
42
+ with no training gets 231 right to Jev's 238, not a significant difference
43
+ (exact McNemar, p = 0.21); [the results](docs/results.md) also say where Jev
44
+ is ahead. Not affiliated with TypeSafe AI.
45
+
46
+ ## Documentation: [docs/](docs/README.md)
47
+
48
+ **Please go to the [ruling docs](docs/README.md).** The usual starting points:
49
+ [Getting started](docs/getting-started.md),
50
+ [Choosing models](docs/models.md), [Fine-tuning](docs/fine-tuning.md) and the
51
+ [Reference](docs/reference.md) for every setting and API route.
52
+
53
+ ## Installation
54
+
55
+ You need an Apple Silicon Mac, Python 3.12+, and [uv](https://docs.astral.sh/uv/).
56
+
57
+ ```bash
58
+ git clone https://github.com/bradAGI/ruling && cd ruling
59
+ uv sync # add --extra vision to ask about images
60
+ uv run ruling serve # downloads a 2.3 GB model on first start, then serves http://127.0.0.1:8010
61
+ ```
62
+
63
+ Open `http://127.0.0.1:8010/` for the playground. On NVIDIA, see
64
+ [vLLM](docs/reference.md#on-nvidia-vllm). Before relying on an answer, read the
65
+ [limits](docs/getting-started.md#limits).
66
+
67
+ ## Reading more
68
+
69
+ - [Results](docs/results.md) and the [full measurements](docs/measurements.md),
70
+ with the command behind every number.
71
+ - [How it works](docs/how-it-works.md): one prefix, many suffixes, and reading
72
+ logits instead of sampling.
73
+ - [Background](docs/background.md): what Jev is, the other open recreations,
74
+ and the papers behind the calibration.
75
+
76
+ ## License
77
+
78
+ MIT. ruling is an independent project with no connection to TypeSafe AI; "Jev"
79
+ and "System One" are their names, used here to say what this reproduces. The
80
+ `authored144` and `perturbations108` datasets and the WANLI row selection are
81
+ converted from TheoLeeCJ/openjev under MIT, with their copyright notice in
82
+ [THIRD_PARTY.md](THIRD_PARTY.md). WANLI is CC-BY-4.0 and SST-5 keeps its own
83
+ terms; neither is committed here. Model weights keep their own licenses.
ruling-0.2.0/README.md ADDED
@@ -0,0 +1,61 @@
1
+ <p align="center">
2
+ <img src="assets/banner.png" alt="ruling: typed decisions, no text" width="720" />
3
+ </p>
4
+
5
+ ## ruling: _typed decisions_, not generated text, from open models
6
+
7
+ **Documentation:** [ruling docs](docs/README.md)
8
+
9
+ ----
10
+
11
+ ruling turns any open chat model into a decision engine like TypeSafe's
12
+ [Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev). You send
13
+ a state (text, JSON or images) and typed questions; you get back a probability
14
+ for every option you declared. Nothing is generated, so there is nothing to
15
+ parse and the model cannot invent an answer you did not list. It runs on your
16
+ own machine, serves TypeSafe's API so their client works against it unchanged,
17
+ and learns from the outcomes of the decisions it serves.
18
+
19
+ On the 256 public judgments that carry Jev's own answers, a 19 GB open model
20
+ with no training gets 231 right to Jev's 238, not a significant difference
21
+ (exact McNemar, p = 0.21); [the results](docs/results.md) also say where Jev
22
+ is ahead. Not affiliated with TypeSafe AI.
23
+
24
+ ## Documentation: [docs/](docs/README.md)
25
+
26
+ **Please go to the [ruling docs](docs/README.md).** The usual starting points:
27
+ [Getting started](docs/getting-started.md),
28
+ [Choosing models](docs/models.md), [Fine-tuning](docs/fine-tuning.md) and the
29
+ [Reference](docs/reference.md) for every setting and API route.
30
+
31
+ ## Installation
32
+
33
+ You need an Apple Silicon Mac, Python 3.12+, and [uv](https://docs.astral.sh/uv/).
34
+
35
+ ```bash
36
+ git clone https://github.com/bradAGI/ruling && cd ruling
37
+ uv sync # add --extra vision to ask about images
38
+ uv run ruling serve # downloads a 2.3 GB model on first start, then serves http://127.0.0.1:8010
39
+ ```
40
+
41
+ Open `http://127.0.0.1:8010/` for the playground. On NVIDIA, see
42
+ [vLLM](docs/reference.md#on-nvidia-vllm). Before relying on an answer, read the
43
+ [limits](docs/getting-started.md#limits).
44
+
45
+ ## Reading more
46
+
47
+ - [Results](docs/results.md) and the [full measurements](docs/measurements.md),
48
+ with the command behind every number.
49
+ - [How it works](docs/how-it-works.md): one prefix, many suffixes, and reading
50
+ logits instead of sampling.
51
+ - [Background](docs/background.md): what Jev is, the other open recreations,
52
+ and the papers behind the calibration.
53
+
54
+ ## License
55
+
56
+ MIT. ruling is an independent project with no connection to TypeSafe AI; "Jev"
57
+ and "System One" are their names, used here to say what this reproduces. The
58
+ `authored144` and `perturbations108` datasets and the WANLI row selection are
59
+ converted from TheoLeeCJ/openjev under MIT, with their copyright notice in
60
+ [THIRD_PARTY.md](THIRD_PARTY.md). WANLI is CC-BY-4.0 and SST-5 keeps its own
61
+ terms; neither is committed here. Model weights keep their own licenses.
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "ruling"
3
+ version = "0.2.0"
4
+ description = "Open-weight recreation of TypeSafe AI's Jev: typed, probabilistic decisions from a local model, no text generation."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ dependencies = [
9
+ "mlx-lm>=0.31",
10
+ "fastapi>=0.115",
11
+ "uvicorn>=0.30",
12
+ "pydantic>=2.7",
13
+ "httpx>=0.27",
14
+ "numpy>=1.26",
15
+ "huggingface-hub>=0.25",
16
+ "pyarrow>=25.0.1",
17
+ "transformers>=5.17.0",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/bradAGI/ruling"
22
+ Source = "https://github.com/bradAGI/ruling"
23
+
24
+ [project.scripts]
25
+ ruling = "ruling.cli:main"
26
+
27
+ [project.optional-dependencies]
28
+ vision = [
29
+ "mlx-vlm>=0.7.8",
30
+ ]
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "mlx-vlm>=0.7.8",
35
+ "peft>=0.21.2",
36
+ "pytest>=8",
37
+ "pytest-playwright>=0.7",
38
+ "pytest-timeout>=2.4.0",
39
+ "torch>=2.14.0",
40
+ "typesafe-sdk>=0.6.0",
41
+ ]
42
+
43
+ [build-system]
44
+ requires = ["hatchling"]
45
+ build-backend = "hatchling.build"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["ruling"]
49
+
50
+ [tool.hatch.build]
51
+ include = ["ruling/**"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ # unit/ and integration/ hold same-named modules; importlib mode keeps them distinct.
56
+ addopts = "--import-mode=importlib"
57
+ markers = [
58
+ "integration: loads a real model; slow on first run",
59
+ "heavy: trains a model or spawns a server; skipped on CI runners without a GPU",
60
+ ]
@@ -0,0 +1,25 @@
1
+ """ruling: typed, probabilistic decisions from a local open-weight model."""
2
+
3
+ from ruling.questions import (
4
+ Choice,
5
+ ChoiceAnswer,
6
+ Noul,
7
+ NoulAnswer,
8
+ Score,
9
+ ScoreAnswer,
10
+ SystemOneRequest,
11
+ SystemOneResponse,
12
+ )
13
+ from ruling.sdk import RulingClient
14
+
15
+ __all__ = [
16
+ "Choice",
17
+ "ChoiceAnswer",
18
+ "Noul",
19
+ "NoulAnswer",
20
+ "RulingClient",
21
+ "Score",
22
+ "ScoreAnswer",
23
+ "SystemOneRequest",
24
+ "SystemOneResponse",
25
+ ]
@@ -0,0 +1,50 @@
1
+ """Send a dataset to the hosted TypeSafe API so Jev's live answers can sit next to ruling's.
2
+
3
+ TypeSafe's HTTP request and response shapes are the same as ruling's, so this
4
+ is a thin client: bearer token, one POST per record, answers converted to
5
+ distributions over the record's canonical keys. Nothing but the record's state
6
+ and questions is sent.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ import httpx
14
+
15
+ from ruling.evals import Distribution, Record, answer_distribution
16
+ from ruling.questions import option_keys
17
+
18
+ API_KEY_ENV = "TYPESAFE_API_KEY"
19
+ BASE_URL_ENV = "TYPESAFE_BASE_URL"
20
+ DEFAULT_BASE_URL = "https://api.typesafe.ai"
21
+
22
+
23
+ def live_answers(
24
+ records: list[Record],
25
+ base_url: str | None = None,
26
+ api_key: str | None = None,
27
+ model: str | None = None,
28
+ timeout: float = 30.0,
29
+ ):
30
+ """Yield (record index, answer distributions by question id) from the hosted API, one request per record."""
31
+ base_url = base_url or os.environ.get(BASE_URL_ENV, DEFAULT_BASE_URL)
32
+ api_key = api_key or os.environ.get(API_KEY_ENV)
33
+ headers = {"authorization": f"Bearer {api_key}"} if api_key else {}
34
+ with httpx.Client(base_url=base_url, headers=headers, timeout=timeout) as client:
35
+ for index, record in enumerate(records):
36
+ body = {"state": record.state,
37
+ "questions": {qid: q.model_dump(exclude_none=True) for qid, q in record.questions.items()}}
38
+ if model is not None:
39
+ body["model"] = model
40
+ response = client.post("/v1/systemone", json=body)
41
+ response.raise_for_status()
42
+ yield index, {qid: answer_distribution(answer, option_keys(record.questions[qid]))
43
+ for qid, answer in response.json()["answers"].items()}
44
+
45
+
46
+ def collect_live(records: list[Record], base_url: str | None = None, api_key: str | None = None,
47
+ model: str | None = None, timeout: float = 30.0) -> dict[tuple[int, str], Distribution]:
48
+ return {(index, qid): distribution
49
+ for index, answers in live_answers(records, base_url, api_key, model, timeout)
50
+ for qid, distribution in answers.items()}
@@ -0,0 +1,257 @@
1
+ """Build labeled datasets from pinned public sources.
2
+
3
+ Nothing here is committed; the builders download a pinned revision and write
4
+ records in the ruling dataset format. WANLI rows follow the 256-row selection
5
+ published by TheoLeeCJ/openjev so results stay comparable. SST-5 gives an
6
+ ordinal five-level Score benchmark. The TypeSafe and Every builders carry
7
+ Jev's own published answers as references, so agreement with Jev can be
8
+ reported on identical inputs.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import io
15
+ import json
16
+ import random
17
+ import re
18
+ import zipfile
19
+ from pathlib import Path
20
+
21
+ import httpx
22
+ from huggingface_hub import hf_hub_download
23
+
24
+ from ruling.evals import Distribution, answer_distribution
25
+
26
+ DATASETS = Path(__file__).resolve().parents[1] / "datasets"
27
+
28
+ WANLI = {"repo": "alisawuffles/WANLI", "revision": "61c95318fd71c55b6ba355d76253254615f387ec", "file": "test.jsonl"}
29
+ SST5 = {"repo": "SetFit/sst5", "revision": "e51bdcd8cd3a30da231967c1a249ba59361279a3", "file": "test.jsonl"}
30
+ WANLI_SELECTION = DATASETS / "wanli256-selection.jsonl"
31
+ TYPESAFE_SELECTION = DATASETS / "typesafe102-selection.jsonl"
32
+ TYPESAFE_CASES = "https://evals.typesafe.ai/{workflow}-cases.js"
33
+ EVERY_EXPERIMENTS = "https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/experiments.json"
34
+ EVERY_ARCHIVE = "https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip"
35
+ EVERY_EXPERIMENTS_GENERATED_AT = "2026-08-28T19:55:23.191Z"
36
+ EVERY_ARCHIVE_SHA256 = "9fbf42e9d9e7cd3b072e0271a0959dbfdca85618e93fe4f0ca519d683c418bf0"
37
+
38
+ NLI_DESCRIPTIONS = {
39
+ "supported": "The evidence establishes the claim",
40
+ "insufficient": "The evidence does not establish either",
41
+ "contradicted": "The evidence establishes the opposite",
42
+ }
43
+ NLI_LABELS = {"entailment": "supported", "neutral": "insufficient", "contradiction": "contradicted"}
44
+ SST5_LEVELS = ["very negative", "negative", "neutral", "positive", "very positive"]
45
+
46
+
47
+ def _rows(source: dict) -> list[dict]:
48
+ path = hf_hub_download(source["repo"], source["file"], repo_type="dataset", revision=source["revision"])
49
+ return [json.loads(line) for line in Path(path).read_text().splitlines() if line.strip()]
50
+
51
+
52
+ def _fetch(url: str) -> bytes:
53
+ response = httpx.get(url, timeout=60.0, follow_redirects=True)
54
+ response.raise_for_status()
55
+ return response.content
56
+
57
+
58
+ def build_wanli() -> list[dict]:
59
+ """256 NLI rows as a 3-way Choice plus a Noul on entailment."""
60
+ by_id = {str(row["id"]): row for row in _rows(WANLI)}
61
+ selection = [json.loads(line) for line in WANLI_SELECTION.read_text().splitlines() if line.strip()]
62
+ records = []
63
+ for item in selection:
64
+ row = by_id[str(item["upstream"]["source_id"])]
65
+ gold = NLI_LABELS[row["gold"]]
66
+ records.append({
67
+ "family": item["family"],
68
+ "source_id": row["id"],
69
+ "state": row["premise"],
70
+ "questions": {
71
+ "verdict": {"type": "choice",
72
+ "instructions": "Assess the claim using only the supplied evidence: " + row["hypothesis"],
73
+ "criteria": {key: NLI_DESCRIPTIONS[key] for key in item["option_ids"]}},
74
+ "supported": {"type": "noul",
75
+ "instructions": "The evidence establishes this claim: " + row["hypothesis"]},
76
+ },
77
+ "labels": {"verdict": gold, "supported": gold == "supported"},
78
+ })
79
+ if len(records) != 256:
80
+ raise ValueError(f"expected 256 WANLI rows, built {len(records)}")
81
+ return records
82
+
83
+
84
+ def build_sst5(limit: int, seed: int = 0) -> list[dict]:
85
+ """A seeded sample of SST-5 test sentences as a five-level Score."""
86
+ rows = _rows(SST5)
87
+ sample = random.Random(seed).sample(rows, limit)
88
+ return [{
89
+ "family": "sentiment",
90
+ "state": row["text"],
91
+ "questions": {"sentiment": {"type": "score", "instructions": "What is the sentiment of the review sentence?",
92
+ "criteria": SST5_LEVELS}},
93
+ "labels": {"sentiment": int(row["label"])},
94
+ } for row in sample]
95
+
96
+
97
+ def build_typesafe() -> list[dict]:
98
+ """The 102 TypeSafe public-eval rows TheoLeeCJ aligned, with Jev's published answers.
99
+
100
+ Labels are the argmax of TypeSafe's reference, the mean of GPT-6 Astra and
101
+ Claude Fable 5.1 at high reasoning, so they measure agreement with big
102
+ models, not ground truth. The selection manifest pins a hash of each
103
+ workflow payload and the build refuses to run if a payload has changed.
104
+ """
105
+ selection = [json.loads(line) for line in TYPESAFE_SELECTION.read_text().splitlines() if line.strip()]
106
+ payloads: dict[str, dict] = {}
107
+ for workflow in sorted({item["upstream"]["workflow"] for item in selection}):
108
+ text = _fetch(TYPESAFE_CASES.format(workflow=workflow)).decode()
109
+ prefix = "__VIEWER_DATA__("
110
+ if not text.startswith(prefix):
111
+ raise ValueError(f"unexpected wrapper in TypeSafe {workflow} payload")
112
+ payload, _ = json.JSONDecoder().raw_decode(text[len(prefix):])
113
+ digest = hashlib.sha256(json.dumps(payload, indent=2).encode()).hexdigest()
114
+ pinned = {item["upstream"]["snapshot_sha256"] for item in selection if item["upstream"]["workflow"] == workflow}
115
+ if digest not in pinned:
116
+ raise ValueError(f"TypeSafe {workflow} payload changed since the selection was frozen")
117
+ payloads[workflow] = payload["eval"]
118
+
119
+ records = []
120
+ for item in selection:
121
+ up = item["upstream"]
122
+ evaluation = payloads[up["workflow"]]
123
+ case = evaluation["cases"][up["case_id"]]
124
+ source_question = evaluation["questions"][up["question_index"]]
125
+ if source_question["type"] == "noul":
126
+ criteria = source_question.get("criteria") or {}
127
+ question = {"type": "noul", "instructions": source_question["instructions"],
128
+ "criteria": {"true": criteria.get("true"), "false": criteria.get("false")}}
129
+ keys, source_keys = ["yes", "no"], ["true", "false"]
130
+ elif source_question["type"] == "choice":
131
+ question = {"type": "choice", "instructions": source_question["instructions"],
132
+ "criteria": source_question["criteria"]}
133
+ keys = source_keys = list(question["criteria"])
134
+ else:
135
+ raise ValueError("the frozen selection excludes score questions")
136
+ if source_keys != item["option_ids"]:
137
+ raise ValueError(f"option order changed for {item['id']}")
138
+
139
+ sets = case["reference_answers"][up["node"]][up["question_id"]]["sets"]
140
+ reference = dict(zip(keys, _mean_reference(sets, source_keys)))
141
+ top = max(reference, key=reference.get)
142
+ if sum(1 for v in reference.values() if abs(v - reference[top]) < 1e-12) != 1:
143
+ raise ValueError(f"reference argmax is not unique for {item['id']}")
144
+ published = next(
145
+ (node["answers"][up["question_id"]] for node in case["models"]["typesafe"]["nodes"]
146
+ if node["node"] == up["node"] and node.get("doc") == up["document_index"]
147
+ and node.get("questions", {}).get(up["question_id"]) == up["question_index"]
148
+ and up["question_id"] in node.get("answers", {})),
149
+ None,
150
+ )
151
+ if published is None:
152
+ raise ValueError(f"no published Jev answer for {item['id']}")
153
+ records.append({
154
+ "family": item["family"],
155
+ "source_id": item["id"],
156
+ "state": evaluation["documents"][up["document_index"]],
157
+ "questions": {up["question_id"]: question},
158
+ "labels": {up["question_id"]: (top == "yes") if question["type"] == "noul" else top},
159
+ "references": {up["question_id"]: {"jev": answer_distribution(published, keys), "reference": reference}},
160
+ })
161
+ if len(records) != 102:
162
+ raise ValueError(f"expected 102 TypeSafe rows, built {len(records)}")
163
+ return records
164
+
165
+
166
+ def _mean_reference(sets: list[dict], keys: list[str]) -> list[float]:
167
+ total = [0.0] * len(keys)
168
+ for answer in sets:
169
+ if answer.get("probabilities"):
170
+ raw = {str(k): float(v) for k, v in answer["probabilities"].items()}
171
+ norm = sum(raw.get(k, 0.0) for k in keys)
172
+ for i, key in enumerate(keys):
173
+ total[i] += raw.get(key, 0.0) / norm
174
+ else:
175
+ value = answer["value"]
176
+ key = str(value).lower() if isinstance(value, bool) else str(value)
177
+ total[keys.index(key)] += 1.0
178
+ return [value / len(sets) for value in total]
179
+
180
+
181
+ def build_every() -> list[dict]:
182
+ """Every's parallel judgment lab: Jev's recorded answers on identical inputs.
183
+
184
+ One record per document with all of that experiment's questions. Gold
185
+ labels exist for the judge grid and the two retrieval grids (author
186
+ labels); the other experiments carry only Jev's answers, so they measure
187
+ agreement. The AI-writing checker is skipped because only excerpts of its
188
+ documents are published.
189
+ """
190
+ data = json.loads(_fetch(EVERY_EXPERIMENTS))
191
+ if data["generated_at"] != EVERY_EXPERIMENTS_GENERATED_AT:
192
+ raise ValueError(f"Every experiments file changed: generated_at is {data['generated_at']}")
193
+ archive_bytes = _fetch(EVERY_ARCHIVE)
194
+ if hashlib.sha256(archive_bytes).hexdigest() != EVERY_ARCHIVE_SHA256:
195
+ raise ValueError("Every source archive changed")
196
+ archive = zipfile.ZipFile(io.BytesIO(archive_bytes))
197
+ script = archive.read("scripts/run-experiments.mjs").decode()
198
+ policy = json.loads(re.search(r'const SUPPORT_POLICY = ("[^\n]+");', script).group(1))
199
+ experiments = {e["id"]: e for e in data["experiments"]}
200
+ records: list[dict] = []
201
+
202
+ grid = experiments["judge-grid"]
203
+ cells = {(c["output"], c["question"]): c for c in grid["typesafe"]["cells"]}
204
+ for document in grid["documents"]:
205
+ questions = {qid: {"type": "noul", "instructions": text} for qid, text in grid["questions"].items()}
206
+ records.append({
207
+ "family": "every_judge-grid",
208
+ "state": {"policy": policy, "candidate_response": document["response"]},
209
+ "questions": questions,
210
+ "labels": {qid: bool(document["expected"][qid]) for qid in questions},
211
+ "references": {qid: {"jev": _noul(cells[document["id"], qid]["value"])} for qid in questions},
212
+ })
213
+
214
+ for name, folder in (("code-rag", "code-repo"), ("company-brain", "company-brain")):
215
+ by_document: dict[str, dict] = {}
216
+ for query in experiments[name]["grid"]:
217
+ for cell in query["cells"]:
218
+ entry = by_document.setdefault(cell["document"], {"questions": {}, "labels": {}, "references": {}})
219
+ entry["questions"][query["id"]] = {"type": "noul", "instructions": query["question"]}
220
+ entry["labels"][query["id"]] = bool(cell["relevant"])
221
+ entry["references"][query["id"]] = {"jev": _noul(cell["value"])}
222
+ for document, entry in by_document.items():
223
+ records.append({"family": f"every_{name}", "state": archive.read(f"fixtures/{folder}/{document}").decode(), **entry})
224
+
225
+ for name in ("action-firewall", "vc-diligence"):
226
+ experiment = experiments[name]
227
+ questions = {qid: {"type": "noul", "instructions": text} for qid, text in experiment["questions"].items()}
228
+ for row in experiment["rows"]:
229
+ records.append({"family": f"every_{name}", "state": row["text"], "questions": questions,
230
+ "references": {qid: {"jev": _noul(row["signals"][qid])} for qid in questions}})
231
+
232
+ for name in ("customer-voice", "ceo-radar", "inbox-urgency", "persona-panel"):
233
+ experiment = experiments[name]
234
+ questions = {qid: ({"type": "noul", "instructions": spec} if isinstance(spec, str) else spec)
235
+ for qid, spec in experiment["questions"].items()}
236
+ for row in experiment["rows"]:
237
+ references = {qid: {"jev": answer_distribution(answer, _keys(questions[qid]))}
238
+ for qid, answer in row["typed_answers"].items()}
239
+ records.append({"family": f"every_{name}", "state": row["document"], "questions": questions,
240
+ "references": references})
241
+ return records
242
+
243
+
244
+ def _noul(probability: float) -> Distribution:
245
+ return {"yes": float(probability), "no": 1.0 - float(probability)}
246
+
247
+
248
+ def _keys(question: dict) -> list[str]:
249
+ if question["type"] == "choice":
250
+ return list(question["criteria"])
251
+ if question["type"] == "score":
252
+ return [str(i) for i in range(len(question["criteria"]))]
253
+ return ["yes", "no"]
254
+
255
+
256
+ def write(records: list[dict], output: Path) -> None:
257
+ output.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in records))