doubleblind-audit 0.1.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.
Files changed (38) hide show
  1. doubleblind_audit-0.1.0/CITATION.cff +19 -0
  2. doubleblind_audit-0.1.0/LICENSE +21 -0
  3. doubleblind_audit-0.1.0/MANIFEST.in +9 -0
  4. doubleblind_audit-0.1.0/PKG-INFO +308 -0
  5. doubleblind_audit-0.1.0/README.md +275 -0
  6. doubleblind_audit-0.1.0/docs/social_card.py +96 -0
  7. doubleblind_audit-0.1.0/doubleblind/__init__.py +3 -0
  8. doubleblind_audit-0.1.0/doubleblind/__main__.py +3 -0
  9. doubleblind_audit-0.1.0/doubleblind/cli.py +323 -0
  10. doubleblind_audit-0.1.0/doubleblind/packet.py +209 -0
  11. doubleblind_audit-0.1.0/doubleblind/render.py +315 -0
  12. doubleblind_audit-0.1.0/doubleblind/trace.py +372 -0
  13. doubleblind_audit-0.1.0/doubleblind-allow.txt +19 -0
  14. doubleblind_audit-0.1.0/doubleblind_audit.egg-info/PKG-INFO +308 -0
  15. doubleblind_audit-0.1.0/doubleblind_audit.egg-info/SOURCES.txt +36 -0
  16. doubleblind_audit-0.1.0/doubleblind_audit.egg-info/dependency_links.txt +1 -0
  17. doubleblind_audit-0.1.0/doubleblind_audit.egg-info/entry_points.txt +2 -0
  18. doubleblind_audit-0.1.0/doubleblind_audit.egg-info/requires.txt +3 -0
  19. doubleblind_audit-0.1.0/doubleblind_audit.egg-info/top_level.txt +1 -0
  20. doubleblind_audit-0.1.0/examples/brief-clean.md +2 -0
  21. doubleblind_audit-0.1.0/examples/brief-leaky.md +4 -0
  22. doubleblind_audit-0.1.0/examples/broken/figure.py +68 -0
  23. doubleblind_audit-0.1.0/examples/broken/figure_reads_backwards.py +52 -0
  24. doubleblind_audit-0.1.0/examples/broken/report.md +10 -0
  25. doubleblind_audit-0.1.0/examples/figure_facts.py +29 -0
  26. doubleblind_audit-0.1.0/examples/figure_ok.py +39 -0
  27. doubleblind_audit-0.1.0/examples/gain.py +23 -0
  28. doubleblind_audit-0.1.0/examples/report.md +11 -0
  29. doubleblind_audit-0.1.0/examples/results.json +18 -0
  30. doubleblind_audit-0.1.0/ledger/README.md +268 -0
  31. doubleblind_audit-0.1.0/ledger/findings.json +282 -0
  32. doubleblind_audit-0.1.0/ledger/render.py +73 -0
  33. doubleblind_audit-0.1.0/ledger/summarize.py +55 -0
  34. doubleblind_audit-0.1.0/protocol/README.md +141 -0
  35. doubleblind_audit-0.1.0/protocol/blindness.md +127 -0
  36. doubleblind_audit-0.1.0/pyproject.toml +59 -0
  37. doubleblind_audit-0.1.0/setup.cfg +4 -0
  38. doubleblind_audit-0.1.0/tests/test_doubleblind.py +483 -0
@@ -0,0 +1,19 @@
1
+ cff-version: 1.2.0
2
+ message: "If this was useful in your work, please cite it."
3
+ title: "doubleblind: two verification layers that are blind to different defects"
4
+ abstract: "An agent cannot check its own output: the reasoning that produced a claim is the reasoning being asked to verify it. Two independent layers catch different things and neither catches the other's. A machine that recomputes finds numbers that exist in no file, stale artifacts and bounds stated tighter than the data, but is structurally unable to see a correct number inside a sentence that does not follow from it. A reader given the artifact and no context finds exactly those, but cannot see a fabricated number that looks plausible. This repository ships both, a linter for the intent leakage that destroys the reviewer's independence, and a ledger of real defects recording which layer missed each one and why."
5
+ type: software
6
+ authors:
7
+ - family-names: Cheng
8
+ given-names: Guo
9
+ orcid: "https://orcid.org/0009-0006-7813-0114"
10
+ repository-code: "https://github.com/GuoCheng24/doubleblind"
11
+ license: MIT
12
+ version: 0.1.0
13
+ date-released: 2026-09-23
14
+ keywords:
15
+ - verification
16
+ - reproducibility
17
+ - AI agents
18
+ - peer review
19
+ - research software
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guo Cheng
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.
@@ -0,0 +1,9 @@
1
+ # The sdist ships the tests, so it has to ship what the tests read: the
2
+ # ledger they count, the examples they audit, and the protocol the README
3
+ # links to. Without this an unpacked sdist carries a test suite it cannot run.
4
+ include README.md LICENSE CITATION.cff doubleblind-allow.txt
5
+ recursive-include ledger *.json *.md *.py
6
+ recursive-include examples *.py *.md *.json
7
+ recursive-include protocol *.md
8
+ recursive-include tests *.py
9
+ recursive-include docs *.py
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: doubleblind-audit
3
+ Version: 0.1.0
4
+ Summary: Three layers that cannot see each other's mistakes: re-derive every number in prose from a committed file, brief a reviewer that was told nothing, and audit the figure a reader will actually see.
5
+ Author: Guo Cheng
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/GuoCheng24/doubleblind
8
+ Project-URL: Ledger, https://github.com/GuoCheng24/doubleblind/tree/main/ledger
9
+ Project-URL: Protocol, https://github.com/GuoCheng24/doubleblind/tree/main/protocol
10
+ Project-URL: Source, https://github.com/GuoCheng24/doubleblind
11
+ Project-URL: Issues, https://github.com/GuoCheng24/doubleblind/issues
12
+ Keywords: agents,verification,reproducibility,review,llm,research,ci
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Environment :: Console
24
+ Classifier: Operating System :: OS Independent
25
+ Classifier: Topic :: Scientific/Engineering
26
+ Classifier: Topic :: Software Development :: Quality Assurance
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: render
31
+ Requires-Dist: matplotlib>=3.9; extra == "render"
32
+ Dynamic: license-file
33
+
34
+ # doubleblind
35
+
36
+ [![tests](https://github.com/GuoCheng24/doubleblind/actions/workflows/ci.yml/badge.svg)](https://github.com/GuoCheng24/doubleblind/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/doubleblind-audit?label=PyPI&color=0b6e4f)](https://pypi.org/project/doubleblind-audit/)
38
+ [![python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
39
+ [![licence](https://img.shields.io/badge/licence-MIT-green)](https://github.com/GuoCheng24/doubleblind/blob/main/LICENSE)
40
+
41
+ **Your agent wrote the report. Ask it whether the report is true and it will say yes.**
42
+
43
+ Not because it is lying. The reasoning that produced the claim is the reasoning
44
+ being asked to check it, and it cannot notice what it did not think of the first
45
+ time. The same goes for the tests it writes for itself and the review it gives
46
+ itself when you ask it to look again.
47
+
48
+ Three things catch that, and **no two of them catch the same defects**:
49
+
50
+ | | finds | structurally cannot see |
51
+ |---|---|---|
52
+ | **a machine that recomputes**<br>`doubleblind trace` | a number that exists in no file; a bound stated tighter than the interval; a quantity written in words | anything nobody thought to check — above all a **correct number inside a sentence that does not follow from it** |
53
+ | **a reader with no context**<br>`doubleblind review` | claims that do not follow; a comparison pointing the wrong way; a framing the data will not carry | anything needing exact recomputation — a fabricated number that looks plausible reads as fine |
54
+ | **a machine that looks at the picture**<br>`doubleblind render` | two labels that read as one word; a caption unreadable at the size it will be seen; a headline over artwork; a character the font could not draw | **whether the shape a reader takes from the figure is the shape the data supports** |
55
+
56
+ Each of the three is automated here, each one is told where it stops, and a
57
+ ledger records what got through anyway — including the six defects that got
58
+ through in this repository.
59
+
60
+ ```bash
61
+ pip install doubleblind-audit # the command is `doubleblind`
62
+ ```
63
+
64
+ The distribution name carries a suffix because the bare one on PyPI is an
65
+ unrelated project; what you import and type is `doubleblind`. From a clone,
66
+ `pip install -e .`, or run it in place with `python -m doubleblind`.
67
+
68
+ Standard library only, no dependencies, Python 3.9+. The reviewer layer works
69
+ with Claude Code, Codex CLI, DeepSeek, Kimi, or any OpenAI-compatible endpoint.
70
+
71
+ ---
72
+
73
+ ## Both blind spots, in one file, in sixty seconds
74
+
75
+ `examples/broken/report.md` has three defects planted in it.
76
+
77
+ ```console
78
+ $ doubleblind trace examples/broken/report.md \
79
+ --data examples/results.json --derive 'python3 examples/gain.py'
80
+
81
+ UNSUPPORTED 97.50% examples/broken/report.md:3
82
+ On all 400 items of widgets-v2, the treatment reaches 97.50% accuracy against a
83
+ nearest committed values: 0.91 (results.json per_category_accuracy.colour),
84
+ 0.89 (results.json per_category_accuracy.count),
85
+ 0.875 (results.json treatment.accuracy)
86
+
87
+ UNCHECKABLE "two thirds" examples/broken/report.md:10
88
+ UNCHECKABLE "four fifths" examples/broken/report.md:10
89
+ Head removal accounts for between two thirds and four fifths of the effect.
90
+ A quantity written as words cannot be traced to a file. Write the number.
91
+ ```
92
+
93
+ Two of three, and it pointed at the `0.875` that `97.50%` should have been. The
94
+ third defect is this sentence:
95
+
96
+ > Shape is where the treatment pays: it rises to **74.00%**, the largest
97
+ > movement of any category.
98
+
99
+ `74.00%` is right — `shape` really is 0.74. It is also 0.74 *before* the
100
+ treatment. Shape is the one category that does not move, and the two that do are
101
+ not in the sentence. Every quantity checks out and the sentence is false, so
102
+ `trace` passes it, and **a test in this repository asserts that `trace` keeps
103
+ passing it**. A claim about a blind spot is worth nothing unless it is pinned
104
+ down.
105
+
106
+ That sentence is what the second layer is for.
107
+
108
+ ---
109
+
110
+ ## The second layer: a reader who was told nothing
111
+
112
+ ```console
113
+ $ doubleblind review examples/broken/report.md --data examples/results.json --agent claude
114
+ packet: packet.md (1847 bytes, sha256 4f2a9c...)
115
+
116
+ Send it with Claude Code:
117
+
118
+ Agent(
119
+ subagent_type='general-purpose',
120
+ model='<a model that is NOT the one that wrote the artifact>',
121
+ run_in_background=False,
122
+ prompt=open('packet.md').read(),
123
+ )
124
+
125
+ Then, before you believe the verdict, record two things next to it:
126
+ 1. which model answered - it must not be the one that wrote the artifact;
127
+ 2. this packet's sha256 - 4f2a9c...
128
+ ```
129
+
130
+ Two properties make that reader independent and **both are required**: a
131
+ **different model**, because the same model with a fresh context still carries
132
+ the priors that wrote the artifact; and **zero context**, because a reviewer who
133
+ knows the wanted answer is not a second opinion.
134
+
135
+ The second is the one that gets lost, and never on purpose. It is lost in the
136
+ request:
137
+
138
+ ```console
139
+ $ doubleblind lint examples/brief-leaky.md
140
+
141
+ answer-in-the-request
142
+ examples/brief-leaky.md:2 "Please confirm that"
143
+ -> Ask what the evidence supports, not whether a stated thing is true.
144
+
145
+ social-pressure
146
+ examples/brief-leaky.md:1 "I already verified"
147
+ examples/brief-leaky.md:3 "quick sanity check"
148
+ -> Saying you checked makes a finding an accusation.
149
+
150
+ ... 6 mechanisms, 9 phrases
151
+
152
+ 9 phrase(s) in this brief tell the reviewer what to conclude.
153
+ A reviewer that knows the wanted answer is not a second opinion.
154
+ ```
155
+
156
+ `lint` reads the request you were about to send and finds the phrases that carry
157
+ the answer, grouped by mechanism rather than wording: **answer-in-the-request**,
158
+ **conclusion-stated**, **polarity-steering**, **social-pressure**,
159
+ **scope-narrowing**, **authorship-leak**. The brief this repository ships is
160
+ linted by its own test suite, so it cannot rot.
161
+
162
+ ---
163
+
164
+ ## The third layer: what a reader sees
165
+
166
+ The ledger is blunt about where the gap was. Of the defects recorded in it, **9
167
+ were caught by a person looking at the rendered artifact**, and 6 of those 9
168
+ were rendering defects — a headline lying across a tile grid, two labels three
169
+ pixels apart that read as one word, a subscript the font could not draw, a
170
+ caption nobody could read in a thumbnail. Every number behind all of them was
171
+ correct, so neither of the other two layers could ever have reached them.
172
+
173
+ ```console
174
+ $ doubleblind render examples/broken/figure.py
175
+
176
+ [render] 13 text objects; legibility floor 33 pt on the canvas is 10 px at 30%
177
+ ! 'One category does not move' sits on undeclared artwork (28x6 px of its box)
178
+ ! 'per category' sits across a rule
179
+ ! 11 pt is 3.3 px at 30% - unreadable: 'measured on the 150-item subset, paired'
180
+ ! contrast 1.3:1 (needs 4.5:1) for 'provisional'
181
+ ! 'strict' and '87.5%' are 5 px apart and read as one word
182
+ ! the font cannot draw '129514 (\N{TEST TUBE})' - it renders as an empty box
183
+ ```
184
+
185
+ Six rules, one per defect class, each of them bought:
186
+
187
+ - **Legibility at the size it will be seen.** A figure drawn at 1200 px is
188
+ unfurled at about 360 px, so text under ~10 px there is texture whatever it
189
+ says — a floor of 33 pt on the canvas. Declare a footer or a watermark as
190
+ chrome with `gid="doubleblind:chrome"` and it is exempt; an earlier version
191
+ measured legible *area* instead and made a figure fail harder the more
192
+ carefully it was labelled.
193
+ - **Contrast**, WCAG 2.1, against whatever the text actually sits on.
194
+ - **Text on artwork** at *any* real overlap rather than a share of its own box —
195
+ a title over a field of tiles covers a few percent of that box and is still a
196
+ title with tiles through it. A 12% threshold passed that twice.
197
+ - **Text across a rule**, because a rule is a `Line2D` and every check that
198
+ walked `ax.patches` reported clean while column headers sat on a separator.
199
+ - **Text that merely touches** — a gap under a third of a character on a shared
200
+ line, negative gaps included, because three pixels is not an overlap and reads
201
+ as one run-on word.
202
+ - **Characters the font cannot draw**, from the renderer's own warnings, because
203
+ a missing glyph has a bounding box like any other and is invisible to every
204
+ geometric test.
205
+
206
+ `matplotlib` is the one optional dependency: `pip install 'doubleblind-audit[render]'`.
207
+
208
+ The distribution is **`doubleblind-audit`**; what you import and type is `doubleblind`. The bare name on PyPI belongs to an unrelated project ([a filename randomiser](https://pypi.org/project/doubleblind/)), so `pip install doubleblind` fetches someone else's package.
209
+
210
+ **And the blind spot, pinned down like the others.**
211
+ `examples/broken/figure_reads_backwards.py` passes this layer completely. Every
212
+ number in it is right and nothing in it is geometrically wrong. What a reader
213
+ takes from it is a steeply rising relationship, because each family's points are
214
+ joined and each of those segments is steep. Over the range actually measured the
215
+ pooled slope is **0.30**; the within-family segments the eye follows have slope
216
+ **30**. A test asserts that `render` keeps passing it, because no rule about
217
+ geometry knows which slope a reader will perceive — and that is the part still
218
+ left to a person.
219
+
220
+ ## On your own work
221
+
222
+ ```bash
223
+ # every number on the page must exist in a file you committed
224
+ doubleblind trace README.md --data results/
225
+
226
+ # numbers no file stores - per-category gains, medians, paired deltas - come
227
+ # from a script that is also committed, so a reader can run the same line
228
+ doubleblind trace README.md --data results/ --derive 'python scripts/metrics.py'
229
+
230
+ # exemptions need a reason, or the allow list becomes somewhere to hide things
231
+ echo "400 the dataset size, fixed by the benchmark" >> doubleblind-allow.txt
232
+
233
+ # then the reader who was told nothing
234
+ doubleblind review README.md --data results/ --agent codex
235
+ ```
236
+
237
+ `trace` exits 1 on an unsupported number, `lint` exits 1 on a leaky brief, so
238
+ both drop into CI unchanged. See [`protocol/`](https://github.com/GuoCheng24/doubleblind/tree/main/protocol) for the reviewer brief
239
+ and the per-agent adapters, and [`protocol/blindness.md`](https://github.com/GuoCheng24/doubleblind/blob/main/protocol/blindness.md)
240
+ for how to record that the reviewer really was blind.
241
+
242
+ ### What `trace` gets right, and where it gets weaker
243
+
244
+ Every rule in it was paid for. `79.75` must not match inside `179.751`, so
245
+ patterns are anchored rather than `\b`-bounded. Prose that says `88.4` claims one
246
+ decimal, so the tolerance is half an ulp *at the precision the prose chose*. A
247
+ whole number is a count, so `11 items` is not supported by a stored `10.6`.
248
+ Dates, clock times, semantic versions, hex digests, URLs and fenced code are not
249
+ results. A quantity written in words is reported rather than skipped, because
250
+ *"two thirds to four fifths"* once shipped in place of 64.43%–81.17% and every
251
+ digit-based check passed it.
252
+
253
+ It gets **weaker the more data you point it at**. A raw per-item dump holds every
254
+ id, index and token count, so a round number in prose will coincide with one of
255
+ them and be called traced. `trace` says so when the pool gets large. Point
256
+ `--data` at summary files and use `--derive` for the rest.
257
+
258
+ ---
259
+
260
+ ## The ledger
261
+
262
+ [`ledger/`](https://github.com/GuoCheng24/doubleblind/tree/main/ledger) records real defects that shipped, each with the layer that
263
+ missed it and why that layer could not have seen it. It is the part of this
264
+ repository that cannot be regenerated, and it includes 7 defects in
265
+ `doubleblind` itself - one of which disarmed a CI step for every document in
266
+ the repository at once, one that only appeared when the package was installed
267
+ into a clean virtualenv and the README's own quickstart was followed from
268
+ somewhere else, and one where three different flags could have said an axis was
269
+ hidden and only the third one moved.
270
+
271
+ The pattern is consistent enough to plan around. On a document that had already
272
+ passed 37 mechanical checks and two rounds of its author's own review, a
273
+ zero-context reviewer on a different model returned nine findings in ten
274
+ minutes; 5 were verified command by command and all five stood. **4 of those 5
275
+ were correct numbers in sentences that did not follow from them** — a causal
276
+ claim, a framing, a ranking, and a promise the guard made about itself. None is
277
+ reachable by comparing quantities.
278
+
279
+ The fifth was the other kind: a column in the data nobody had looked at. 11 of
280
+ 100 items had never finished writing their reasoning, and the answer extractor
281
+ had credited 5 of them from a half-written trace. Counted as no-answer instead,
282
+ three of four confidence intervals stopped containing the number being
283
+ reproduced. The page's verdict turned on it and the page did not mention it.
284
+
285
+ And the honest part: of 22 recorded defects, **10 were caught by a person
286
+ looking at the rendered artifact** — 7 of those 10 rendering defects that no
287
+ amount of number-checking would ever have reached. That is the bar
288
+ `doubleblind render` exists to shrink, and the ledger is how you find out
289
+ whether it does. That is the number this tooling
290
+ exists to shrink, and the ledger is how you find out whether it does.
291
+
292
+ ## What this is not
293
+
294
+ It is not an autonomous research agent and it writes nothing. It is what you run
295
+ *after* an agent has produced something you are about to publish, on the premise
296
+ that the agent that produced it is the wrong thing to ask.
297
+
298
+ For the generative side — reading literature, proposing experiments, drafting
299
+ papers — [ARIS](https://github.com/wanshuiyin/Auto-claude-code-research-in-sleep)
300
+ does that, and its cross-model review gate is careful work: it builds an
301
+ un-forgeable evidence chain from the host's session events to prove the reviewer
302
+ really was a different model. `doubleblind` needs no such machinery because it
303
+ never asserts the reviewer was independent. It tells you to write down the model
304
+ and the packet hash, and gives you nothing if you don't.
305
+
306
+ ## Licence
307
+
308
+ MIT.
@@ -0,0 +1,275 @@
1
+ # doubleblind
2
+
3
+ [![tests](https://github.com/GuoCheng24/doubleblind/actions/workflows/ci.yml/badge.svg)](https://github.com/GuoCheng24/doubleblind/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/doubleblind-audit?label=PyPI&color=0b6e4f)](https://pypi.org/project/doubleblind-audit/)
5
+ [![python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
6
+ [![licence](https://img.shields.io/badge/licence-MIT-green)](https://github.com/GuoCheng24/doubleblind/blob/main/LICENSE)
7
+
8
+ **Your agent wrote the report. Ask it whether the report is true and it will say yes.**
9
+
10
+ Not because it is lying. The reasoning that produced the claim is the reasoning
11
+ being asked to check it, and it cannot notice what it did not think of the first
12
+ time. The same goes for the tests it writes for itself and the review it gives
13
+ itself when you ask it to look again.
14
+
15
+ Three things catch that, and **no two of them catch the same defects**:
16
+
17
+ | | finds | structurally cannot see |
18
+ |---|---|---|
19
+ | **a machine that recomputes**<br>`doubleblind trace` | a number that exists in no file; a bound stated tighter than the interval; a quantity written in words | anything nobody thought to check — above all a **correct number inside a sentence that does not follow from it** |
20
+ | **a reader with no context**<br>`doubleblind review` | claims that do not follow; a comparison pointing the wrong way; a framing the data will not carry | anything needing exact recomputation — a fabricated number that looks plausible reads as fine |
21
+ | **a machine that looks at the picture**<br>`doubleblind render` | two labels that read as one word; a caption unreadable at the size it will be seen; a headline over artwork; a character the font could not draw | **whether the shape a reader takes from the figure is the shape the data supports** |
22
+
23
+ Each of the three is automated here, each one is told where it stops, and a
24
+ ledger records what got through anyway — including the six defects that got
25
+ through in this repository.
26
+
27
+ ```bash
28
+ pip install doubleblind-audit # the command is `doubleblind`
29
+ ```
30
+
31
+ The distribution name carries a suffix because the bare one on PyPI is an
32
+ unrelated project; what you import and type is `doubleblind`. From a clone,
33
+ `pip install -e .`, or run it in place with `python -m doubleblind`.
34
+
35
+ Standard library only, no dependencies, Python 3.9+. The reviewer layer works
36
+ with Claude Code, Codex CLI, DeepSeek, Kimi, or any OpenAI-compatible endpoint.
37
+
38
+ ---
39
+
40
+ ## Both blind spots, in one file, in sixty seconds
41
+
42
+ `examples/broken/report.md` has three defects planted in it.
43
+
44
+ ```console
45
+ $ doubleblind trace examples/broken/report.md \
46
+ --data examples/results.json --derive 'python3 examples/gain.py'
47
+
48
+ UNSUPPORTED 97.50% examples/broken/report.md:3
49
+ On all 400 items of widgets-v2, the treatment reaches 97.50% accuracy against a
50
+ nearest committed values: 0.91 (results.json per_category_accuracy.colour),
51
+ 0.89 (results.json per_category_accuracy.count),
52
+ 0.875 (results.json treatment.accuracy)
53
+
54
+ UNCHECKABLE "two thirds" examples/broken/report.md:10
55
+ UNCHECKABLE "four fifths" examples/broken/report.md:10
56
+ Head removal accounts for between two thirds and four fifths of the effect.
57
+ A quantity written as words cannot be traced to a file. Write the number.
58
+ ```
59
+
60
+ Two of three, and it pointed at the `0.875` that `97.50%` should have been. The
61
+ third defect is this sentence:
62
+
63
+ > Shape is where the treatment pays: it rises to **74.00%**, the largest
64
+ > movement of any category.
65
+
66
+ `74.00%` is right — `shape` really is 0.74. It is also 0.74 *before* the
67
+ treatment. Shape is the one category that does not move, and the two that do are
68
+ not in the sentence. Every quantity checks out and the sentence is false, so
69
+ `trace` passes it, and **a test in this repository asserts that `trace` keeps
70
+ passing it**. A claim about a blind spot is worth nothing unless it is pinned
71
+ down.
72
+
73
+ That sentence is what the second layer is for.
74
+
75
+ ---
76
+
77
+ ## The second layer: a reader who was told nothing
78
+
79
+ ```console
80
+ $ doubleblind review examples/broken/report.md --data examples/results.json --agent claude
81
+ packet: packet.md (1847 bytes, sha256 4f2a9c...)
82
+
83
+ Send it with Claude Code:
84
+
85
+ Agent(
86
+ subagent_type='general-purpose',
87
+ model='<a model that is NOT the one that wrote the artifact>',
88
+ run_in_background=False,
89
+ prompt=open('packet.md').read(),
90
+ )
91
+
92
+ Then, before you believe the verdict, record two things next to it:
93
+ 1. which model answered - it must not be the one that wrote the artifact;
94
+ 2. this packet's sha256 - 4f2a9c...
95
+ ```
96
+
97
+ Two properties make that reader independent and **both are required**: a
98
+ **different model**, because the same model with a fresh context still carries
99
+ the priors that wrote the artifact; and **zero context**, because a reviewer who
100
+ knows the wanted answer is not a second opinion.
101
+
102
+ The second is the one that gets lost, and never on purpose. It is lost in the
103
+ request:
104
+
105
+ ```console
106
+ $ doubleblind lint examples/brief-leaky.md
107
+
108
+ answer-in-the-request
109
+ examples/brief-leaky.md:2 "Please confirm that"
110
+ -> Ask what the evidence supports, not whether a stated thing is true.
111
+
112
+ social-pressure
113
+ examples/brief-leaky.md:1 "I already verified"
114
+ examples/brief-leaky.md:3 "quick sanity check"
115
+ -> Saying you checked makes a finding an accusation.
116
+
117
+ ... 6 mechanisms, 9 phrases
118
+
119
+ 9 phrase(s) in this brief tell the reviewer what to conclude.
120
+ A reviewer that knows the wanted answer is not a second opinion.
121
+ ```
122
+
123
+ `lint` reads the request you were about to send and finds the phrases that carry
124
+ the answer, grouped by mechanism rather than wording: **answer-in-the-request**,
125
+ **conclusion-stated**, **polarity-steering**, **social-pressure**,
126
+ **scope-narrowing**, **authorship-leak**. The brief this repository ships is
127
+ linted by its own test suite, so it cannot rot.
128
+
129
+ ---
130
+
131
+ ## The third layer: what a reader sees
132
+
133
+ The ledger is blunt about where the gap was. Of the defects recorded in it, **9
134
+ were caught by a person looking at the rendered artifact**, and 6 of those 9
135
+ were rendering defects — a headline lying across a tile grid, two labels three
136
+ pixels apart that read as one word, a subscript the font could not draw, a
137
+ caption nobody could read in a thumbnail. Every number behind all of them was
138
+ correct, so neither of the other two layers could ever have reached them.
139
+
140
+ ```console
141
+ $ doubleblind render examples/broken/figure.py
142
+
143
+ [render] 13 text objects; legibility floor 33 pt on the canvas is 10 px at 30%
144
+ ! 'One category does not move' sits on undeclared artwork (28x6 px of its box)
145
+ ! 'per category' sits across a rule
146
+ ! 11 pt is 3.3 px at 30% - unreadable: 'measured on the 150-item subset, paired'
147
+ ! contrast 1.3:1 (needs 4.5:1) for 'provisional'
148
+ ! 'strict' and '87.5%' are 5 px apart and read as one word
149
+ ! the font cannot draw '129514 (\N{TEST TUBE})' - it renders as an empty box
150
+ ```
151
+
152
+ Six rules, one per defect class, each of them bought:
153
+
154
+ - **Legibility at the size it will be seen.** A figure drawn at 1200 px is
155
+ unfurled at about 360 px, so text under ~10 px there is texture whatever it
156
+ says — a floor of 33 pt on the canvas. Declare a footer or a watermark as
157
+ chrome with `gid="doubleblind:chrome"` and it is exempt; an earlier version
158
+ measured legible *area* instead and made a figure fail harder the more
159
+ carefully it was labelled.
160
+ - **Contrast**, WCAG 2.1, against whatever the text actually sits on.
161
+ - **Text on artwork** at *any* real overlap rather than a share of its own box —
162
+ a title over a field of tiles covers a few percent of that box and is still a
163
+ title with tiles through it. A 12% threshold passed that twice.
164
+ - **Text across a rule**, because a rule is a `Line2D` and every check that
165
+ walked `ax.patches` reported clean while column headers sat on a separator.
166
+ - **Text that merely touches** — a gap under a third of a character on a shared
167
+ line, negative gaps included, because three pixels is not an overlap and reads
168
+ as one run-on word.
169
+ - **Characters the font cannot draw**, from the renderer's own warnings, because
170
+ a missing glyph has a bounding box like any other and is invisible to every
171
+ geometric test.
172
+
173
+ `matplotlib` is the one optional dependency: `pip install 'doubleblind-audit[render]'`.
174
+
175
+ The distribution is **`doubleblind-audit`**; what you import and type is `doubleblind`. The bare name on PyPI belongs to an unrelated project ([a filename randomiser](https://pypi.org/project/doubleblind/)), so `pip install doubleblind` fetches someone else's package.
176
+
177
+ **And the blind spot, pinned down like the others.**
178
+ `examples/broken/figure_reads_backwards.py` passes this layer completely. Every
179
+ number in it is right and nothing in it is geometrically wrong. What a reader
180
+ takes from it is a steeply rising relationship, because each family's points are
181
+ joined and each of those segments is steep. Over the range actually measured the
182
+ pooled slope is **0.30**; the within-family segments the eye follows have slope
183
+ **30**. A test asserts that `render` keeps passing it, because no rule about
184
+ geometry knows which slope a reader will perceive — and that is the part still
185
+ left to a person.
186
+
187
+ ## On your own work
188
+
189
+ ```bash
190
+ # every number on the page must exist in a file you committed
191
+ doubleblind trace README.md --data results/
192
+
193
+ # numbers no file stores - per-category gains, medians, paired deltas - come
194
+ # from a script that is also committed, so a reader can run the same line
195
+ doubleblind trace README.md --data results/ --derive 'python scripts/metrics.py'
196
+
197
+ # exemptions need a reason, or the allow list becomes somewhere to hide things
198
+ echo "400 the dataset size, fixed by the benchmark" >> doubleblind-allow.txt
199
+
200
+ # then the reader who was told nothing
201
+ doubleblind review README.md --data results/ --agent codex
202
+ ```
203
+
204
+ `trace` exits 1 on an unsupported number, `lint` exits 1 on a leaky brief, so
205
+ both drop into CI unchanged. See [`protocol/`](https://github.com/GuoCheng24/doubleblind/tree/main/protocol) for the reviewer brief
206
+ and the per-agent adapters, and [`protocol/blindness.md`](https://github.com/GuoCheng24/doubleblind/blob/main/protocol/blindness.md)
207
+ for how to record that the reviewer really was blind.
208
+
209
+ ### What `trace` gets right, and where it gets weaker
210
+
211
+ Every rule in it was paid for. `79.75` must not match inside `179.751`, so
212
+ patterns are anchored rather than `\b`-bounded. Prose that says `88.4` claims one
213
+ decimal, so the tolerance is half an ulp *at the precision the prose chose*. A
214
+ whole number is a count, so `11 items` is not supported by a stored `10.6`.
215
+ Dates, clock times, semantic versions, hex digests, URLs and fenced code are not
216
+ results. A quantity written in words is reported rather than skipped, because
217
+ *"two thirds to four fifths"* once shipped in place of 64.43%–81.17% and every
218
+ digit-based check passed it.
219
+
220
+ It gets **weaker the more data you point it at**. A raw per-item dump holds every
221
+ id, index and token count, so a round number in prose will coincide with one of
222
+ them and be called traced. `trace` says so when the pool gets large. Point
223
+ `--data` at summary files and use `--derive` for the rest.
224
+
225
+ ---
226
+
227
+ ## The ledger
228
+
229
+ [`ledger/`](https://github.com/GuoCheng24/doubleblind/tree/main/ledger) records real defects that shipped, each with the layer that
230
+ missed it and why that layer could not have seen it. It is the part of this
231
+ repository that cannot be regenerated, and it includes 7 defects in
232
+ `doubleblind` itself - one of which disarmed a CI step for every document in
233
+ the repository at once, one that only appeared when the package was installed
234
+ into a clean virtualenv and the README's own quickstart was followed from
235
+ somewhere else, and one where three different flags could have said an axis was
236
+ hidden and only the third one moved.
237
+
238
+ The pattern is consistent enough to plan around. On a document that had already
239
+ passed 37 mechanical checks and two rounds of its author's own review, a
240
+ zero-context reviewer on a different model returned nine findings in ten
241
+ minutes; 5 were verified command by command and all five stood. **4 of those 5
242
+ were correct numbers in sentences that did not follow from them** — a causal
243
+ claim, a framing, a ranking, and a promise the guard made about itself. None is
244
+ reachable by comparing quantities.
245
+
246
+ The fifth was the other kind: a column in the data nobody had looked at. 11 of
247
+ 100 items had never finished writing their reasoning, and the answer extractor
248
+ had credited 5 of them from a half-written trace. Counted as no-answer instead,
249
+ three of four confidence intervals stopped containing the number being
250
+ reproduced. The page's verdict turned on it and the page did not mention it.
251
+
252
+ And the honest part: of 22 recorded defects, **10 were caught by a person
253
+ looking at the rendered artifact** — 7 of those 10 rendering defects that no
254
+ amount of number-checking would ever have reached. That is the bar
255
+ `doubleblind render` exists to shrink, and the ledger is how you find out
256
+ whether it does. That is the number this tooling
257
+ exists to shrink, and the ledger is how you find out whether it does.
258
+
259
+ ## What this is not
260
+
261
+ It is not an autonomous research agent and it writes nothing. It is what you run
262
+ *after* an agent has produced something you are about to publish, on the premise
263
+ that the agent that produced it is the wrong thing to ask.
264
+
265
+ For the generative side — reading literature, proposing experiments, drafting
266
+ papers — [ARIS](https://github.com/wanshuiyin/Auto-claude-code-research-in-sleep)
267
+ does that, and its cross-model review gate is careful work: it builds an
268
+ un-forgeable evidence chain from the host's session events to prove the reviewer
269
+ really was a different model. `doubleblind` needs no such machinery because it
270
+ never asserts the reviewer was independent. It tells you to write down the model
271
+ and the packet hash, and gives you nothing if you don't.
272
+
273
+ ## Licence
274
+
275
+ MIT.