@applesnort/crosscheck 0.2.0
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.
- package/LICENSE +21 -0
- package/PROVENANCE.md +57 -0
- package/README.md +244 -0
- package/bin/crosscheck.mjs +426 -0
- package/fixtures/calibration/PREREGISTERED.md +522 -0
- package/fixtures/calibration/expected.json +144 -0
- package/fixtures/calibration/src/session.js +123 -0
- package/foreman.md +140 -0
- package/lenses/architect.md +92 -0
- package/lenses/check.md +87 -0
- package/lenses/security-check.md +105 -0
- package/lenses/taint.md +102 -0
- package/lenses/ux.md +105 -0
- package/lib/baseline.mjs +78 -0
- package/lib/calibrate.mjs +169 -0
- package/lib/corpus.mjs +340 -0
- package/lib/lenses.mjs +200 -0
- package/lib/merge.mjs +310 -0
- package/lib/parse.mjs +96 -0
- package/lib/prompt.mjs +85 -0
- package/lib/run.mjs +129 -0
- package/lib/sarif.mjs +176 -0
- package/package.json +53 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# Pre-registered evaluation criteria
|
|
2
|
+
|
|
3
|
+
Written **before** the runs it governs, and committed separately, so the standard
|
|
4
|
+
cannot be adjusted after seeing the numbers. The claim being tested is the one
|
|
5
|
+
this project leads with, so it is the one most worth protecting from its author.
|
|
6
|
+
|
|
7
|
+
## The claim under test
|
|
8
|
+
|
|
9
|
+
> A finding that several independent lenses reported is more likely to be real
|
|
10
|
+
> than one a single lens reported.
|
|
11
|
+
|
|
12
|
+
Everything in this repository's ranking depends on that. If it is false, the
|
|
13
|
+
consensus score is decoration and the honest move is to remove it from the
|
|
14
|
+
headline and keep only the deduplication.
|
|
15
|
+
|
|
16
|
+
## Why earlier runs could not test it
|
|
17
|
+
|
|
18
|
+
Two runs against this fixture produced **zero false positives** across 16
|
|
19
|
+
findings. With single-lens precision already at 100%, consensus had nothing to
|
|
20
|
+
discriminate — the comparison was undefined, not favourable. Reporting that as
|
|
21
|
+
support would have been dishonest, so the README recorded it as unproven.
|
|
22
|
+
|
|
23
|
+
A test of "consensus filters mistakes" requires a run that contains mistakes.
|
|
24
|
+
|
|
25
|
+
## Method
|
|
26
|
+
|
|
27
|
+
Two configurations against the same fixture and the same ground truth, differing
|
|
28
|
+
only in the model tier driving the lenses:
|
|
29
|
+
|
|
30
|
+
1. **Capable tier** — the baseline already measured.
|
|
31
|
+
2. **Weaker tier** — cheaper lenses make more errors, which is the point. This
|
|
32
|
+
generates the false positives the claim needs in order to be falsifiable.
|
|
33
|
+
|
|
34
|
+
The prompts, lens definitions, fixture, and ground truth are identical across
|
|
35
|
+
configurations. Only the model changes.
|
|
36
|
+
|
|
37
|
+
## Falsification criteria — fixed in advance
|
|
38
|
+
|
|
39
|
+
Given a run with **at least 3 false positives** (below that, the comparison is
|
|
40
|
+
too thin to mean anything and the result is reported as inconclusive):
|
|
41
|
+
|
|
42
|
+
- **Claim supported** if consensus precision exceeds solo precision by at least
|
|
43
|
+
10 percentage points.
|
|
44
|
+
- **Claim refuted** if consensus precision is at or below solo precision. The
|
|
45
|
+
consensus score is then removed from the README's headline claims and demoted
|
|
46
|
+
to an implementation detail, and this file records the refutation.
|
|
47
|
+
- **Inconclusive** for anything between. Reported as inconclusive, not as
|
|
48
|
+
partial support.
|
|
49
|
+
|
|
50
|
+
## Rules that make the above mean something
|
|
51
|
+
|
|
52
|
+
- **Every run is reported**, including runs that go against the claim. No run is
|
|
53
|
+
discarded for being unflattering, and the count of runs performed is stated.
|
|
54
|
+
- **The fixture is frozen** for the duration of these runs. No decoy is made
|
|
55
|
+
subtler and no defect is reworded after seeing which ones the lenses bit.
|
|
56
|
+
- **One fixture change is permitted before the runs begin**, and it is declared
|
|
57
|
+
here rather than discovered later: `getSessionForUser` accepted no caller
|
|
58
|
+
identity, so its planted authorization gap had nothing in scope to check
|
|
59
|
+
against, and both prior runs were scored down for declining to report it. The
|
|
60
|
+
function now accepts a caller identity and ignores it, which makes the defect
|
|
61
|
+
unambiguous. This corrects a flaw in the experiment, and it is expected to
|
|
62
|
+
*raise* recall — which is exactly why it is declared before the run rather
|
|
63
|
+
than after.
|
|
64
|
+
- **No threshold tuning against these runs.** The similarity threshold and line
|
|
65
|
+
tolerance were fixed by earlier measurement and stay fixed here. If they later
|
|
66
|
+
need re-measuring, that is a separate exercise with its own record.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# Outcome — recorded 2026-08-05
|
|
71
|
+
|
|
72
|
+
Two configurations run, three lenses each, identical prompts and fixture. Both
|
|
73
|
+
runs are reported, as required.
|
|
74
|
+
|
|
75
|
+
| | capable tier | weaker tier |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| recall | 100% (7/7) | 100% (7/7) |
|
|
78
|
+
| findings | 8 | 9 |
|
|
79
|
+
| false positives | **0** | **0** |
|
|
80
|
+
| consensus precision | 100% | 100% |
|
|
81
|
+
| solo precision | 100% | 100% |
|
|
82
|
+
| consensus findings | 4 | 2 |
|
|
83
|
+
|
|
84
|
+
## Verdict: INCONCLUSIVE
|
|
85
|
+
|
|
86
|
+
The criteria required at least 3 false positives for the comparison to mean
|
|
87
|
+
anything. Both runs produced **zero**, so consensus precision and solo precision
|
|
88
|
+
are both 100% and the comparison is undefined for a third time. The claim is
|
|
89
|
+
neither supported nor refuted. Per the rules above this is recorded as
|
|
90
|
+
inconclusive, not as partial support, and the consensus score stays in the code
|
|
91
|
+
without a validation claim attached to it in the README.
|
|
92
|
+
|
|
93
|
+
## Why the method failed to test the claim
|
|
94
|
+
|
|
95
|
+
The premise was that a weaker model tier would make more mistakes, supplying the
|
|
96
|
+
false positives the comparison needs. It did not. The weaker tier was **terser,
|
|
97
|
+
not wronger** — it reported fewer findings per lens, in shorter sentences, and
|
|
98
|
+
every one of them still landed on a real planted defect. Lower capability showed
|
|
99
|
+
up as reduced coverage and cruder severity judgement, not as invention. That is
|
|
100
|
+
worth knowing on its own, and it means model tier is the wrong knob for
|
|
101
|
+
generating false positives.
|
|
102
|
+
|
|
103
|
+
## What the declared fixture change did
|
|
104
|
+
|
|
105
|
+
`getSessionForUser` now accepts a caller identity and ignores it. Recall went
|
|
106
|
+
from 71-86% to **100% at both tiers**, and all six lens runs found the
|
|
107
|
+
authorization gap. This confirms the earlier miss was a flaw in the fixture, not
|
|
108
|
+
a gap in the lenses: with no caller identity in scope there was genuinely nothing
|
|
109
|
+
to check against. Declared in advance precisely because it was expected to
|
|
110
|
+
flatter the numbers.
|
|
111
|
+
|
|
112
|
+
## What would actually test the claim
|
|
113
|
+
|
|
114
|
+
Not another tier, and not subtler decoys authored by the same person who wrote the
|
|
115
|
+
ground truth — that is the p-hacking route this file exists to block. It needs a
|
|
116
|
+
target where the lenses genuinely err: real production code with independent
|
|
117
|
+
ground truth, or a defect corpus someone else built. Until such a run exists, the
|
|
118
|
+
consensus weighting remains an unvalidated hypothesis, and the README says so.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
# Round 2 — external corpus. Criteria fixed 2026-08-05, before any run.
|
|
123
|
+
|
|
124
|
+
The first three rounds could not test the claim because the lenses never erred,
|
|
125
|
+
and every decoy was written by the same person who wrote the ground truth, the
|
|
126
|
+
lens prompts, and the scorer. That circularity is the flaw. This round removes it
|
|
127
|
+
by using a corpus authored by someone else.
|
|
128
|
+
|
|
129
|
+
## Corpus
|
|
130
|
+
|
|
131
|
+
[OWASP Benchmark](https://owasp.org/www-project-benchmark/) v1.2 — 2,740
|
|
132
|
+
self-contained Java servlets, each carrying one intentional CWE and a label in
|
|
133
|
+
`expectedresults-1.2.csv`:
|
|
134
|
+
|
|
135
|
+
- **1,415 labeled `true`** — the vulnerability is real.
|
|
136
|
+
- **1,325 labeled `false`** — the code follows the same taint path into a *safe*
|
|
137
|
+
sink. It looks vulnerable and is not.
|
|
138
|
+
|
|
139
|
+
Those 1,325 are the point. They are externally authored false-positive
|
|
140
|
+
opportunities, which is exactly what three rounds of hand-made decoys failed to
|
|
141
|
+
produce.
|
|
142
|
+
|
|
143
|
+
The corpus is not vendored into this repository. `licenseInfo` reports null on the
|
|
144
|
+
upstream repo, so no assumption is made about redistribution terms: a fetch
|
|
145
|
+
script clones it locally, it is gitignored, and only the adapter, the sample
|
|
146
|
+
manifest, and the resulting scores are committed.
|
|
147
|
+
|
|
148
|
+
## Sampling rule — fixed here, applied mechanically
|
|
149
|
+
|
|
150
|
+
For each of the 11 categories, take the **first 3 `true` and first 3 `false`
|
|
151
|
+
cases by ascending test number**. Up to 66 cases, balanced by construction and
|
|
152
|
+
reproducible by anyone. Where a category holds fewer than 3 of a label, take what
|
|
153
|
+
exists and state the shortfall.
|
|
154
|
+
|
|
155
|
+
No hand-picking, and no re-drawing the sample after seeing results. The manifest
|
|
156
|
+
is committed before the run.
|
|
157
|
+
|
|
158
|
+
## Scoring
|
|
159
|
+
|
|
160
|
+
Benchmark labels a whole test case, not a line, so line-span matching does not
|
|
161
|
+
apply. A finding **matches** a case when it cites the case's CWE number or its
|
|
162
|
+
category vocabulary. Then:
|
|
163
|
+
|
|
164
|
+
- label `true` + match → true positive
|
|
165
|
+
- label `true` + no match → missed
|
|
166
|
+
- label `false` + match → **false positive**
|
|
167
|
+
- label `false` + no match → correctly declined
|
|
168
|
+
|
|
169
|
+
Only findings matching the expected CWE count either way. A lens reporting some
|
|
170
|
+
unrelated real issue in a `false` case is neither credited nor penalised, and the
|
|
171
|
+
count of those is reported separately.
|
|
172
|
+
|
|
173
|
+
## Falsification criteria
|
|
174
|
+
|
|
175
|
+
Given at least 3 false positives — which this corpus should finally supply:
|
|
176
|
+
|
|
177
|
+
- **Claim supported** if consensus precision exceeds solo precision by ≥10
|
|
178
|
+
percentage points.
|
|
179
|
+
- **Claim refuted** if consensus precision is at or below solo precision. The
|
|
180
|
+
consensus score is then removed from the README's headline and demoted to an
|
|
181
|
+
implementation detail.
|
|
182
|
+
- **Inconclusive** otherwise, and reported as such.
|
|
183
|
+
|
|
184
|
+
Unchanged from round 1: every run reported, no threshold tuning against these
|
|
185
|
+
runs, no post-hoc sample changes.
|
|
186
|
+
|
|
187
|
+
## Declared threat to validity
|
|
188
|
+
|
|
189
|
+
OWASP Benchmark is synthetic, widely published, and almost certainly present in
|
|
190
|
+
model training data. Absolute recall may therefore be inflated by memorisation
|
|
191
|
+
and should not be quoted as evidence the lenses are good.
|
|
192
|
+
|
|
193
|
+
This does **not** undermine the measurement being made here. The question is
|
|
194
|
+
whether agreement between lenses predicts correctness — a comparison *within* the
|
|
195
|
+
run, between consensus and solo findings drawn from the same model on the same
|
|
196
|
+
corpus. Memorisation would have to affect consensus and solo findings
|
|
197
|
+
differentially to bias that, which there is no reason to expect. Stated here so
|
|
198
|
+
it cannot be raised later as though it were concealed.
|
|
199
|
+
|
|
200
|
+
## Round 2 addendum — a second security-capable lens, declared before running
|
|
201
|
+
|
|
202
|
+
Building the adapter surfaced a flaw in the experiment. OWASP Benchmark is
|
|
203
|
+
entirely security cases, and in this roster only `security-check` owns security —
|
|
204
|
+
`check` and `architect` both name "security categories" in their `not-owns`. So no
|
|
205
|
+
finding on this corpus could ever be reported by two lenses, and consensus could
|
|
206
|
+
never form. The corpus would measure false-positive rate well and leave the
|
|
207
|
+
consensus claim exactly as untestable as before.
|
|
208
|
+
|
|
209
|
+
Testing agreement requires at least two competent, independent observers in the
|
|
210
|
+
domain under test. A single-domain corpus therefore needs a second
|
|
211
|
+
security-capable lens. Adding one is a requirement of the measurement, not a
|
|
212
|
+
thumb on the scale — but it is declared here, before the run, with the
|
|
213
|
+
constraints that keep it honest:
|
|
214
|
+
|
|
215
|
+
- **Written without looking at any corpus case.** The lens is authored from the
|
|
216
|
+
method alone. No Benchmark file is read while writing it.
|
|
217
|
+
- **A genuinely different method, not a paraphrase.** `security-check` works from
|
|
218
|
+
OWASP categories and CWE checklists — a taxonomy walk. The new `taint` lens
|
|
219
|
+
works sink-first: enumerate the dangerous operations, trace each argument back
|
|
220
|
+
toward its origin, and ask whether anything sanitises it in between. Same
|
|
221
|
+
domain, different reasoning path. That is precisely the "two lenses that
|
|
222
|
+
overlap in domain but not in method" case the independence weighting exists to
|
|
223
|
+
score.
|
|
224
|
+
- **No tuning toward agreement.** The lens is not adjusted after any run, and its
|
|
225
|
+
overlap with `security-check` is measured, not assumed.
|
|
226
|
+
|
|
227
|
+
If the two lenses turn out to be highly redundant, the independence weighting
|
|
228
|
+
should *discount* their agreement — and that outcome is as interesting as the
|
|
229
|
+
alternative. It is reported either way.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
# Round 2 outcome — recorded 2026-08-05
|
|
234
|
+
|
|
235
|
+
66 OWASP Benchmark cases, 33 vulnerable and 33 safe, two lenses
|
|
236
|
+
(`security-check`, `taint`), three batches each. All six agents completed. Zero
|
|
237
|
+
unparsed lines, zero unmapped findings.
|
|
238
|
+
|
|
239
|
+
| | |
|
|
240
|
+
|---|---|
|
|
241
|
+
| recall | **97.0%** (32/33) |
|
|
242
|
+
| specificity | **97.0%** (32/33) |
|
|
243
|
+
| false positives | **1** |
|
|
244
|
+
| unrelated findings | 3 (neither credited nor penalised) |
|
|
245
|
+
| consensus detections | 15, precision **93.3%** |
|
|
246
|
+
| solo detections | 18, precision **100.0%** |
|
|
247
|
+
| measured lens overlap | 0.4545 |
|
|
248
|
+
|
|
249
|
+
## Verdict: INCONCLUSIVE
|
|
250
|
+
|
|
251
|
+
The criteria require at least 3 false positives. This run produced **1**, so the
|
|
252
|
+
comparison is inconclusive and the claim is neither supported nor refuted.
|
|
253
|
+
|
|
254
|
+
Consensus precision came out *below* solo precision, 93.3% against 100%. That is
|
|
255
|
+
directionally against the claim, and it is tempting to call it a refutation — the
|
|
256
|
+
criteria deliberately forbid that. One false positive cannot separate a real
|
|
257
|
+
effect from noise, and a rule that only binds when the result is unwelcome is not
|
|
258
|
+
a rule. Recorded as inconclusive.
|
|
259
|
+
|
|
260
|
+
## The single false positive is the most informative result here
|
|
261
|
+
|
|
262
|
+
`BenchmarkTest00052`, a safe `sqli` case, was flagged by **both** lenses.
|
|
263
|
+
|
|
264
|
+
The case builds `"{call " + param + "}"` and executes it, which is the vulnerable
|
|
265
|
+
idiom exactly. But `param` comes from
|
|
266
|
+
`SeparateClassRequest.getTheValue("BenchmarkTest00052")`, and that helper
|
|
267
|
+
**returns the constant `"bar"`**, ignoring its argument. The value is never
|
|
268
|
+
attacker-controlled and the SQL is not injectable.
|
|
269
|
+
|
|
270
|
+
Both lenses inferred taint from the helper's *name*. The class is called
|
|
271
|
+
`SeparateClassRequest`, it is constructed from the request, and its sibling
|
|
272
|
+
methods (`getTheParameter`, `getTheCookie`) really are request accessors. Neither
|
|
273
|
+
lens opened the helper.
|
|
274
|
+
|
|
275
|
+
Two consequences worth more than the headline numbers:
|
|
276
|
+
|
|
277
|
+
**Independence of method did not produce independence of failure.** The lenses
|
|
278
|
+
reason differently — one walks CWE categories, the other traces sinks backward —
|
|
279
|
+
and the overlap measurement confirms they are only ~45% redundant. They still
|
|
280
|
+
failed identically, because both had to resolve the same opaque helper and both
|
|
281
|
+
guessed from its name. Consensus cannot filter an error whose *cause* is shared.
|
|
282
|
+
That is a real limit on what agreement-weighting can do, and it is invisible in
|
|
283
|
+
any evaluation where the lenses do not err.
|
|
284
|
+
|
|
285
|
+
**A caveat in the prompt does not prevent the behaviour it warns about.**
|
|
286
|
+
`lenses/taint.md` names this exact mistake twice — "a sink whose argument is
|
|
287
|
+
trusted" and "a sanitiser you did not recognise… the origin is `unknown`, and the
|
|
288
|
+
correct output is either silence or a `CONSIDER`". The lens described its own
|
|
289
|
+
failure mode and then committed it. Prompt-stated discipline is not enforcement.
|
|
290
|
+
|
|
291
|
+
## The one miss
|
|
292
|
+
|
|
293
|
+
`BenchmarkTest00098`, `trustbound`. Trust-boundary violations — untrusted data
|
|
294
|
+
written into a session attribute — have no dangerous sink to trace and no
|
|
295
|
+
recognisable category idiom, so neither lens's method reaches them.
|
|
296
|
+
|
|
297
|
+
## Threat to validity, restated
|
|
298
|
+
|
|
299
|
+
97% recall *and* 97% specificity on a public benchmark almost certainly reflects
|
|
300
|
+
some memorisation; the corpus is widely published and predates the models. Those
|
|
301
|
+
two figures should not be quoted as evidence the lenses are good.
|
|
302
|
+
|
|
303
|
+
The within-run consensus-versus-solo comparison is less exposed to that, but the
|
|
304
|
+
run failed to test it for a different reason: the lenses were nearly always
|
|
305
|
+
right, so there was almost nothing for consensus to discriminate. Three rounds of
|
|
306
|
+
self-authored fixtures and one round of external corpus have all failed the same
|
|
307
|
+
way. The obstacle is not fixture quality — it is that these lenses, on
|
|
308
|
+
benchmark-shaped code, do not make enough mistakes to measure a mistake filter.
|
|
309
|
+
|
|
310
|
+
Testing the claim needs targets where competent review genuinely errs: real
|
|
311
|
+
production code with independent ground truth, ambiguous cases with expert
|
|
312
|
+
disagreement, or a corpus built to defeat exactly the shortcut seen here — an
|
|
313
|
+
opaque helper whose behaviour contradicts its name. That last idea comes from
|
|
314
|
+
this run's single false positive, and building a fixture around it would be
|
|
315
|
+
legitimate only with new criteria fixed in advance.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
# Round 3 — deception corpus. Criteria fixed 2026-08-05, before the corpus exists.
|
|
320
|
+
|
|
321
|
+
## The question changed, and this states why before any data is collected
|
|
322
|
+
|
|
323
|
+
Five rounds have failed to test "consensus predicts correctness" because the
|
|
324
|
+
lenses almost never erred. Round 2's single false positive showed why that
|
|
325
|
+
framing is the wrong one: two lenses with genuinely different methods
|
|
326
|
+
(~45% measured overlap) failed **identically** on `BenchmarkTest00052`, because
|
|
327
|
+
both had to resolve one opaque helper and both guessed from its name.
|
|
328
|
+
|
|
329
|
+
Consensus can only filter errors that are *uncorrelated between lenses*. So
|
|
330
|
+
whether it helps is not a property of consensus — it is a property of the error
|
|
331
|
+
structure of a given roster on a given codebase. A corpus of traps therefore
|
|
332
|
+
cannot answer "does consensus work", because whoever picks the ratio of shared to
|
|
333
|
+
method-specific traps picks the answer.
|
|
334
|
+
|
|
335
|
+
The testable question is: **does consensus filter method-specific errors while
|
|
336
|
+
failing on shared ones, in the direction and rough magnitude predicted?** If yes,
|
|
337
|
+
the tool's job is to measure that ratio for your roster, and consensus weighting
|
|
338
|
+
is worth having exactly when your failures are uncorrelated. If no — if consensus
|
|
339
|
+
fails to filter even method-specific errors — the weighting is worthless and
|
|
340
|
+
comes out.
|
|
341
|
+
|
|
342
|
+
## Corpus design — the mix is declared here and not changed afterward
|
|
343
|
+
|
|
344
|
+
20 cases, one defect-or-not per file, written for this experiment. Lenses
|
|
345
|
+
(`security-check`, `taint`) are **frozen**: not edited before, during, or after.
|
|
346
|
+
|
|
347
|
+
| class | n | what it is | correct answer |
|
|
348
|
+
|---|---|---|---|
|
|
349
|
+
| `vulnerable` | 5 | plainly exploitable, no tricks | flag it |
|
|
350
|
+
| `clean` | 5 | plainly safe, no tricks | say nothing |
|
|
351
|
+
| `trap-category` | 3 | matches a known-bad *idiom*, but the value is provably constant or the use is non-security | say nothing |
|
|
352
|
+
| `trap-flow` | 3 | value genuinely untrusted, but the sink is inert or a real sanitiser intervenes | say nothing |
|
|
353
|
+
| `trap-shared` | 4 | a helper whose behaviour contradicts its name — two safe, two genuinely exploitable | 2 say nothing, 2 flag it |
|
|
354
|
+
|
|
355
|
+
`trap-category` should fool a taxonomy walk and not a flow trace.
|
|
356
|
+
`trap-flow` should fool a flow trace and not a taxonomy walk.
|
|
357
|
+
`trap-shared` should fool both, in both directions: two produce shared false
|
|
358
|
+
positives, two produce shared *misses*. Including shared misses matters — a
|
|
359
|
+
corpus containing only false-positive bait would engineer consensus to look bad.
|
|
360
|
+
|
|
361
|
+
## Predictions, recorded before running
|
|
362
|
+
|
|
363
|
+
1. Consensus precision **exceeds** solo precision, because `trap-category` and
|
|
364
|
+
`trap-flow` errors should be solo by construction while only `trap-shared`
|
|
365
|
+
produces agreement on a wrong answer.
|
|
366
|
+
2. At least 3 false positives total, satisfying the threshold that rounds 1-2 never
|
|
367
|
+
reached.
|
|
368
|
+
3. Both `trap-shared` false-positive cases are flagged by **both** lenses.
|
|
369
|
+
4. Measured lens overlap stays well below 1.0, confirming the two remain
|
|
370
|
+
distinguishable.
|
|
371
|
+
|
|
372
|
+
If prediction 1 fails while 2 holds, the claim is **refuted** and consensus
|
|
373
|
+
weighting is removed from the README headline. If 2 fails again, the corpus was
|
|
374
|
+
too easy and that is reported as another inconclusive round — not retried with
|
|
375
|
+
harder traps under the same criteria.
|
|
376
|
+
|
|
377
|
+
## What this round cannot show
|
|
378
|
+
|
|
379
|
+
It cannot establish that consensus helps on *real* code, because the trap mix is
|
|
380
|
+
chosen. It can establish the mechanism by which consensus helps or fails, and
|
|
381
|
+
whether the tool's own overlap measurement predicts which case you are in. That
|
|
382
|
+
is the claim the README will be allowed to make if the predictions hold — no
|
|
383
|
+
broader one.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
# Round 3 outcome — recorded 2026-08-05
|
|
388
|
+
|
|
389
|
+
20 cases, two lenses, one batch each. Zero unparsed, zero unmapped.
|
|
390
|
+
|
|
391
|
+
| | |
|
|
392
|
+
|---|---|
|
|
393
|
+
| recall | **100%** (7/7) |
|
|
394
|
+
| specificity | **100%** (13/13) |
|
|
395
|
+
| false positives | **0** |
|
|
396
|
+
| consensus detections | 7, precision 100% |
|
|
397
|
+
| solo detections | **0** |
|
|
398
|
+
| measured lens overlap | **1.0** |
|
|
399
|
+
|
|
400
|
+
## All four predictions failed
|
|
401
|
+
|
|
402
|
+
1. Consensus exceeds solo precision — **untestable**: there were no solo
|
|
403
|
+
detections at all.
|
|
404
|
+
2. At least 3 false positives — **failed**: zero.
|
|
405
|
+
3. Both `trap-shared` false-positive cases flagged by both lenses — **failed**:
|
|
406
|
+
both were correctly declined.
|
|
407
|
+
4. Lens overlap stays below 1.0 — **failed**: it is exactly 1.0. The two lenses
|
|
408
|
+
returned *identical* detection sets across all 20 cases.
|
|
409
|
+
|
|
410
|
+
Verdict: **INCONCLUSIVE**, round six. Per the criteria, a second failure of
|
|
411
|
+
prediction 2 means the corpus was too easy and is reported as such, not retried
|
|
412
|
+
with harder traps under the same rules.
|
|
413
|
+
|
|
414
|
+
## I compromised the shared traps myself
|
|
415
|
+
|
|
416
|
+
The dispatch prompt included this line:
|
|
417
|
+
|
|
418
|
+
> The modules import from `helpers/` and `db.js`. You may and should read those
|
|
419
|
+
> too — what a helper is named and what it does are separate questions.
|
|
420
|
+
|
|
421
|
+
That is a direct hint defeating the entire `trap-shared` class. Those four cases
|
|
422
|
+
exist to test whether a lens infers behaviour from a helper's name, and the prompt
|
|
423
|
+
told the lenses not to. The `trap-shared` results are therefore void: they measure
|
|
424
|
+
whether a coached reviewer opens a helper, which was never in doubt.
|
|
425
|
+
|
|
426
|
+
`trap-category` and `trap-flow` received no such hint and were still declined
|
|
427
|
+
perfectly — six for six. Those results stand, and they say the lenses do not bite
|
|
428
|
+
on idiom-matching or on inert sinks.
|
|
429
|
+
|
|
430
|
+
## What the overlap of 1.0 actually shows
|
|
431
|
+
|
|
432
|
+
This is the round's real result, and it is not about the consensus claim.
|
|
433
|
+
|
|
434
|
+
`security-check` and `taint` were written with deliberately different methods — a
|
|
435
|
+
CWE taxonomy walk versus sink-first flow tracing — and on OWASP Benchmark they
|
|
436
|
+
measured 0.4545 overlap, meaningfully independent. On this corpus they produced
|
|
437
|
+
*byte-for-byte equivalent* detection sets: same 7 files, same verdicts.
|
|
438
|
+
|
|
439
|
+
The independence weighting handled that correctly. At overlap 1.0 it scores their
|
|
440
|
+
agreement as **1.0 effective confirmations** — worth exactly one lens, because
|
|
441
|
+
that is what it is worth. The mechanism detected a redundant roster and refused to
|
|
442
|
+
inflate confidence for it.
|
|
443
|
+
|
|
444
|
+
So the weighting has now done useful work twice, on real data, in both directions:
|
|
445
|
+
0.45 on a corpus where the lenses differed, 1.0 on one where they did not. That
|
|
446
|
+
capability — *telling you whether your roster is redundant on your code* — is
|
|
447
|
+
measured and defensible. It is not the same as the claim that consensus predicts
|
|
448
|
+
correctness, which remains unproven.
|
|
449
|
+
|
|
450
|
+
## Round 4: repeat without the hint
|
|
451
|
+
|
|
452
|
+
The one experiment this round should have run. Same 20 cases, same frozen lenses,
|
|
453
|
+
same criteria — with the helper-reading sentence removed from the dispatch prompt.
|
|
454
|
+
This corrects my methodological error rather than making the corpus harder, and
|
|
455
|
+
that distinction is why it is permitted under the rules above.
|
|
456
|
+
|
|
457
|
+
Prediction, recorded now: **both `ts01` and `ts02` are flagged by both lenses**,
|
|
458
|
+
reproducing round 2's `BenchmarkTest00052` failure and finally supplying shared
|
|
459
|
+
false positives. If they are declined again, these lenses do not commit the
|
|
460
|
+
name-inference error unprompted, round 2's single false positive was a fluke, and
|
|
461
|
+
the claim is abandoned as untestable by any means available here.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
# Round 4 outcome — recorded 2026-08-05. Last round on this question.
|
|
466
|
+
|
|
467
|
+
Identical corpus, identical frozen lenses, one sentence removed from the dispatch
|
|
468
|
+
prompt. Result identical to round 3: **20/20**, recall 100%, specificity 100%,
|
|
469
|
+
zero false positives, zero solo detections, overlap 1.0. Both lenses again
|
|
470
|
+
returned the same seven files.
|
|
471
|
+
|
|
472
|
+
**The prediction failed.** `ts01` and `ts02` were declined again. Without any
|
|
473
|
+
prompting, both lenses opened `helpers/requestValues.js`, saw that `getUserInput`
|
|
474
|
+
and `readParam` return constants, and said nothing. The hint I removed was not
|
|
475
|
+
what saved them in round 3 — they simply do not commit the name-inference error
|
|
476
|
+
here.
|
|
477
|
+
|
|
478
|
+
## Conclusion: the claim is abandoned as untestable by available means
|
|
479
|
+
|
|
480
|
+
Six rounds. Two self-authored fixtures, one external corpus of 2,740 cases, two
|
|
481
|
+
model tiers, a purpose-built deception corpus, and a controlled prompt ablation.
|
|
482
|
+
Total false positives observed across all of it: **one**.
|
|
483
|
+
|
|
484
|
+
The obstacle was never fixture quality. It is that these lenses, on small
|
|
485
|
+
self-contained modules with visible imports, are close to perfect — and an error
|
|
486
|
+
filter cannot be measured on a process that does not err. Benchmark-shaped code is
|
|
487
|
+
where LLM review is *strongest*, which makes it precisely the wrong place to
|
|
488
|
+
measure this.
|
|
489
|
+
|
|
490
|
+
Round 2's single false positive is worth one more note. It occurred at 22 unfamiliar
|
|
491
|
+
Java files per agent, with the deceptive helper several imports deep in a large
|
|
492
|
+
repository. Rounds 3 and 4 gave 20 small modules with direct imports and produced
|
|
493
|
+
nothing. That suggests the error rate is driven by context load and unfamiliarity
|
|
494
|
+
rather than by the trap itself — which is consistent with needing real production
|
|
495
|
+
code to observe, and is exactly what cannot be assembled with reliable ground
|
|
496
|
+
truth here.
|
|
497
|
+
|
|
498
|
+
The claim is therefore **not refuted** — the pre-registered refutation condition
|
|
499
|
+
(consensus precision at or below solo, given at least three false positives) never
|
|
500
|
+
triggered. It is unfalsifiable with the means available, and an unfalsifiable
|
|
501
|
+
claim does not get to be a headline. The consensus score stays in the code and in
|
|
502
|
+
the report, documented as an unvalidated hypothesis.
|
|
503
|
+
|
|
504
|
+
## What *is* established, and becomes the headline instead
|
|
505
|
+
|
|
506
|
+
The independence measurement works and has produced meaningful, discriminating
|
|
507
|
+
results on real data in both directions:
|
|
508
|
+
|
|
509
|
+
- **0.4545** on OWASP Benchmark — two lenses with genuinely different methods,
|
|
510
|
+
meaningfully independent.
|
|
511
|
+
- **1.0** on the deception corpus — the same two lenses, byte-for-byte identical
|
|
512
|
+
detection sets, and the weighting correctly scored their agreement as worth
|
|
513
|
+
exactly one lens rather than two.
|
|
514
|
+
|
|
515
|
+
A tool that tells you *your roster is redundant on your code* is answering a
|
|
516
|
+
question you can act on: drop a lens, or replace it with one that fails
|
|
517
|
+
differently. That is measured, reproducible, and does not depend on the
|
|
518
|
+
unvalidated claim.
|
|
519
|
+
|
|
520
|
+
No further rounds on this question. Reopening it requires a corpus of real
|
|
521
|
+
production code with independently authored ground truth, and new criteria fixed
|
|
522
|
+
in advance.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Ground truth for the calibration fixture. Each entry is a defect planted on purpose, with the lens expected to catch it and the severity a correct panel should assign. `alsoAcceptedBy` lists lenses that may legitimately report the same defect without it counting as a false positive. `line` is the most specific offending statement; `span` is the acceptable anchor range, since a lens may reasonably cite the function signature or the surrounding block instead.",
|
|
3
|
+
"target": [
|
|
4
|
+
"fixtures/calibration/src/session.js"
|
|
5
|
+
],
|
|
6
|
+
"defects": [
|
|
7
|
+
{
|
|
8
|
+
"id": "predictable-token",
|
|
9
|
+
"file": "fixtures/calibration/src/session.js",
|
|
10
|
+
"line": 19,
|
|
11
|
+
"severity": "BLOCK",
|
|
12
|
+
"expectedBy": "security-check",
|
|
13
|
+
"alsoAcceptedBy": [
|
|
14
|
+
"check"
|
|
15
|
+
],
|
|
16
|
+
"summary": "session token derived from predictable inputs (user id + second-resolution clock)",
|
|
17
|
+
"span": [
|
|
18
|
+
17,
|
|
19
|
+
20
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "timing-unsafe-compare",
|
|
24
|
+
"file": "fixtures/calibration/src/session.js",
|
|
25
|
+
"line": 24,
|
|
26
|
+
"severity": "BLOCK",
|
|
27
|
+
"expectedBy": "security-check",
|
|
28
|
+
"alsoAcceptedBy": [],
|
|
29
|
+
"summary": "secret compared with non-constant-time equality",
|
|
30
|
+
"span": [
|
|
31
|
+
23,
|
|
32
|
+
25
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "expires-as-number",
|
|
37
|
+
"file": "fixtures/calibration/src/session.js",
|
|
38
|
+
"line": 32,
|
|
39
|
+
"severity": "BLOCK",
|
|
40
|
+
"expectedBy": "architect",
|
|
41
|
+
"alsoAcceptedBy": [
|
|
42
|
+
"check"
|
|
43
|
+
],
|
|
44
|
+
"summary": "expiry stored as a numeric timestamp, defeating any date-based TTL, with the grace period baked into the stored value",
|
|
45
|
+
"span": [
|
|
46
|
+
27,
|
|
47
|
+
36
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "unguarded-expiry-read",
|
|
52
|
+
"file": "fixtures/calibration/src/session.js",
|
|
53
|
+
"line": 39,
|
|
54
|
+
"severity": "BLOCK",
|
|
55
|
+
"expectedBy": "check",
|
|
56
|
+
"alsoAcceptedBy": [
|
|
57
|
+
"architect",
|
|
58
|
+
"security-check"
|
|
59
|
+
],
|
|
60
|
+
"summary": "read returns a record without checking expiry, so an expired session is treated as live",
|
|
61
|
+
"span": [
|
|
62
|
+
38,
|
|
63
|
+
40
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "falsy-quota-reset",
|
|
68
|
+
"file": "fixtures/calibration/src/session.js",
|
|
69
|
+
"line": 43,
|
|
70
|
+
"severity": "FIX",
|
|
71
|
+
"expectedBy": "check",
|
|
72
|
+
"alsoAcceptedBy": [],
|
|
73
|
+
"summary": "a quota of 0 is treated as absent and silently resets to the default",
|
|
74
|
+
"span": [
|
|
75
|
+
42,
|
|
76
|
+
44
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "missing-ownership-check",
|
|
81
|
+
"file": "fixtures/calibration/src/session.js",
|
|
82
|
+
"line": 51,
|
|
83
|
+
"severity": "BLOCK",
|
|
84
|
+
"expectedBy": "security-check",
|
|
85
|
+
"alsoAcceptedBy": [
|
|
86
|
+
"check"
|
|
87
|
+
],
|
|
88
|
+
"summary": "getSessionForUser accepts callerUserId and never compares it to record.session.userId, so any caller holding a token reads that session regardless of whose it is",
|
|
89
|
+
"span": [
|
|
90
|
+
46,
|
|
91
|
+
52
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "swallowed-error",
|
|
96
|
+
"file": "fixtures/calibration/src/session.js",
|
|
97
|
+
"line": 58,
|
|
98
|
+
"severity": "BLOCK",
|
|
99
|
+
"expectedBy": "check",
|
|
100
|
+
"alsoAcceptedBy": [
|
|
101
|
+
"architect"
|
|
102
|
+
],
|
|
103
|
+
"summary": "catch discards the failure and returns an empty list, making an outage look like no data",
|
|
104
|
+
"span": [
|
|
105
|
+
54,
|
|
106
|
+
59
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"decoys": {
|
|
111
|
+
"note": "Correct code included on purpose, to make precision measurable. A finding anchored on any of these is a false positive. Everything from line 61 onward is deliberately correct.",
|
|
112
|
+
"cleanFrom": 61,
|
|
113
|
+
"items": [
|
|
114
|
+
{
|
|
115
|
+
"line": 68,
|
|
116
|
+
"why": "Math.random() for retry jitter is not a security decision"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"line": 72,
|
|
120
|
+
"why": "randomBytes is the correct source for an opaque id"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"line": 78,
|
|
124
|
+
"why": "timingSafeEqual with the required length pre-check"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"line": 91,
|
|
128
|
+
"why": "`== null` is the one correct loose comparison here"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"line": 98,
|
|
132
|
+
"why": "the error is logged AND rethrown, not swallowed"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"line": 109,
|
|
136
|
+
"why": "interpolation into a log line, not into a query"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"line": 118,
|
|
140
|
+
"why": "`|| default` where 0 cannot legitimately arrive"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|