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.
- doubleblind_audit-0.1.0/CITATION.cff +19 -0
- doubleblind_audit-0.1.0/LICENSE +21 -0
- doubleblind_audit-0.1.0/MANIFEST.in +9 -0
- doubleblind_audit-0.1.0/PKG-INFO +308 -0
- doubleblind_audit-0.1.0/README.md +275 -0
- doubleblind_audit-0.1.0/docs/social_card.py +96 -0
- doubleblind_audit-0.1.0/doubleblind/__init__.py +3 -0
- doubleblind_audit-0.1.0/doubleblind/__main__.py +3 -0
- doubleblind_audit-0.1.0/doubleblind/cli.py +323 -0
- doubleblind_audit-0.1.0/doubleblind/packet.py +209 -0
- doubleblind_audit-0.1.0/doubleblind/render.py +315 -0
- doubleblind_audit-0.1.0/doubleblind/trace.py +372 -0
- doubleblind_audit-0.1.0/doubleblind-allow.txt +19 -0
- doubleblind_audit-0.1.0/doubleblind_audit.egg-info/PKG-INFO +308 -0
- doubleblind_audit-0.1.0/doubleblind_audit.egg-info/SOURCES.txt +36 -0
- doubleblind_audit-0.1.0/doubleblind_audit.egg-info/dependency_links.txt +1 -0
- doubleblind_audit-0.1.0/doubleblind_audit.egg-info/entry_points.txt +2 -0
- doubleblind_audit-0.1.0/doubleblind_audit.egg-info/requires.txt +3 -0
- doubleblind_audit-0.1.0/doubleblind_audit.egg-info/top_level.txt +1 -0
- doubleblind_audit-0.1.0/examples/brief-clean.md +2 -0
- doubleblind_audit-0.1.0/examples/brief-leaky.md +4 -0
- doubleblind_audit-0.1.0/examples/broken/figure.py +68 -0
- doubleblind_audit-0.1.0/examples/broken/figure_reads_backwards.py +52 -0
- doubleblind_audit-0.1.0/examples/broken/report.md +10 -0
- doubleblind_audit-0.1.0/examples/figure_facts.py +29 -0
- doubleblind_audit-0.1.0/examples/figure_ok.py +39 -0
- doubleblind_audit-0.1.0/examples/gain.py +23 -0
- doubleblind_audit-0.1.0/examples/report.md +11 -0
- doubleblind_audit-0.1.0/examples/results.json +18 -0
- doubleblind_audit-0.1.0/ledger/README.md +268 -0
- doubleblind_audit-0.1.0/ledger/findings.json +282 -0
- doubleblind_audit-0.1.0/ledger/render.py +73 -0
- doubleblind_audit-0.1.0/ledger/summarize.py +55 -0
- doubleblind_audit-0.1.0/protocol/README.md +141 -0
- doubleblind_audit-0.1.0/protocol/blindness.md +127 -0
- doubleblind_audit-0.1.0/pyproject.toml +59 -0
- doubleblind_audit-0.1.0/setup.cfg +4 -0
- 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
|
+
[](https://github.com/GuoCheng24/doubleblind/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/doubleblind-audit/)
|
|
38
|
+
[](https://www.python.org/)
|
|
39
|
+
[](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
|
+
[](https://github.com/GuoCheng24/doubleblind/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/doubleblind-audit/)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](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.
|