@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.
@@ -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
+ }