Skip to content

feat(comparison): comparator-as-record registry + trustworthy-comparison method - #18

Merged
juniperbevensee merged 3 commits into
mainfrom
dev/juniperbevensee/alembic-methodology
Jul 28, 2026
Merged

feat(comparison): comparator-as-record registry + trustworthy-comparison method#18
juniperbevensee merged 3 commits into
mainfrom
dev/juniperbevensee/alembic-methodology

Conversation

@juniperbevensee

Copy link
Copy Markdown
Collaborator

Promotion of the generalizable methodology from a 2026-07-27/28 research-integrity
audit, under docs/promotion-and-upstream.md
Path A: strip the particulars, keep the method, PR from a branch into main,
let the gate run.

The audit's raw material stays where it is as the historical record. What is
promoted here is only what is useful to someone who has never seen the project.
No study, dataset, species, model, function name, filename or path appears in the
diff.

The spine

The same experimental comparison — "the new model beats the classical
baseline"
— was invalid four different ways across three attempts by two
different agents
, and each fix moved the bug rather than removing it:

Attempt What was fixed What was still true
1 comparator was print(f"Baseline: P=0.860, R=0.636, F1=0.731") — an f-string with no interpolation, in eight scripts. It printed into a job-completion notification and was read back as a measurement.
2 the constant was removed the baseline was reimplemented inside the comparison script. Recall 0.636 → 0.098 (F1 0.731 → 0.170); the new arm "won" by +0.285.
3 the callable was imported it was the wrong one of two variants, chosen by matching its name. The results file said best_method: "simple"; the imported variant scores 0.442 where the real one scores 0.731.
4 the right callable run at library signature defaults (0.7/30/20) instead of the parameters tuned on train and saved (0.5/10/50).

Every arithmetic step was correct every time. Nothing was fabricated. The
structural cause is one sentence: the comparator was never represented as
data
— it existed as a number, then a function name, then a call, never as a
record carrying its own provenance. So each fix had to re-derive from memory or
from a name whatever the last fix had lost.

Files

File Why
docs/trustworthy-comparison.md The centrepiece. The four-ways-wrong case study, why every fix moved the bug, five more generic failure modes from the same audit, and 14 checkable rules — including a comparability-check table to write out loud in any report.
docs/baseline-registry.md The comparator-as-record pattern: four welded fields, the API, the contract a producing script must satisfy, and a "what this still does NOT catch" section.
docs/results-provenance-checklist.md What a results JSON must carry to be regenerable from itself: seed + which RNGs, library versions, git SHA, the split member lists, every data-reduction decision, the matching criterion, how the operating point was chosen, known leakage.
matilde_plugin/engine/comparison.py Vendored, generalized, stdlib-only, domain-neutral. compare() raises rather than returning a delta when split or criterion differ.
tests/test_comparison.py 39 tests, one per failure, each named for the mistake it reproduces.
README.md New Method docs section — a docs index, since the repo had none. Plus a row for the new engine module.
hermes-skill/SKILL.md A "Before you report that A beats B" subsection under the existing skepticism section, with pointers. This is the difference between a doc that exists and a doc that gets used.

Also distilled (each with a concrete mechanism)

  • A declared-but-unused constant. SR = 22050 was declared and never
    referenced again (the name appears exactly once in the file). The model was
    trained at native 48 kHz. Scripts read the constant as intent and helpfully
    resampled — holding split, matcher and threshold fixed, true positives went
    449 → 215, a 2.1× difference, F1 0.766 → 0.539. Across the reporting
    scripts the same model appeared as low as 0.457.
  • A label column that was not a label. Column 3 of the annotation rows was a
    frequency in Hz. Four scripts used it as the class, producing 829 singleton
    classes over 829 examples
    — an 830-way softmax with one example each. The
    real 13-family / 74-label taxonomy was in an adjacent file in the same
    directory.
  • An optimum on the sweep boundary. The reported best threshold was the
    lowest value swept, then read as "the model is confident about a lot of
    boundaries" — which inverts the meaning.
  • Unseeded stochasticity. Same script, same data: F1 0.7839 then 0.7675. The
    rerun overwrote the artifact the published figure was read from. The
    discrepancy was explained as "a different random split" — the one thing that
    was controlled (tp + fn = 499 in both runs proves the identical test set).
  • Silent failure believed. A helper printed HTTP response bodies and exited
    0 regardless of status, so a 405 was indistinguishable from success, and a
    confident, detailed, wrong root-cause analysis was written from the symptom.
    Generalized: anything that cannot fail loudly will eventually be believed.
  • Calibration is not validation. A report can be excellent in form —
    recommending a range over a point estimate, flagging that 1.7 points at n=30 is
    within normal variation — while the comparison underneath is invalid. Hedging
    raises the credibility of whatever it is attached to, which makes attaching it
    to an unchecked claim worse than stating the claim bluntly.

The resolution, with its caveats

Re-scored under one split with one matcher, the classical detector beat the CNN:
0.801 vs 0.775 (validated matcher, IoU ≥ 0.3 or overlap ≥ 0.5) and
0.762 vs 0.747 (IoU ≥ 0.3 only).

Both caveats are stated in the docs everywhere those numbers appear:

  • Both arms are oracle-best — each arm's operating point was chosen by
    maximising F1 on the test set. Upper bounds, not held-out estimates.
  • 22 of 30 test files had leaked into the CNN's training/validation set. Only
    8 were genuinely held out for that arm. The classical detector won despite
    that, which makes the finding stronger, not weaker — but it is not a clean
    measurement of either arm.

The publishable result is a negative one, and it took four invalid comparisons
to reach it.

Design decision: why four fields, not one

A registry returning only a number would have prevented failure 1 and none of
the others — and would make 2–4 harder to catch, because the comparator now
looks authoritative. So a record is fn + params + split + criterion, and
the subtraction refuses unless split and criterion match. The docs/ version
carries this as a table.

The honesty carried across from the source draft is the "what this still does
NOT catch"
list — a wrong-but-well-formed criterion passes; declared matching
semantics beyond the thresholds are unverified; a leaky split is legal (only a
missing one is refused). Structure makes disagreement visible, not truthful.
The registry is a floor: it removes failures that come from losing
information
, and cannot remove failures where the information is wrong at the
source.

Checks

  • python3 -m pytest -q144 passed, 7 skipped before → 183 passed, 7
    skipped
    after. Nothing broken; +39 new.
  • python3 scripts/check_sanitization.py on every added/changed file — clean
    on the deterministic layer. Semantic layer skipped locally (no
    ANTHROPIC_API_KEY); CI will run it.
  • Manual grep of the staged diff for study particulars (dataset names, species,
    filenames, /opt/data, tool names) — no hits.

Deliberately left out

  • configure_threads() (sizes the torch thread pool from the cgroup CPU
    quota). Real and useful, but the generalizable content is a rule — "when a
    job feels slow, suspect CPU quota before memory" — not a function, and
    vendoring it would put cgroup and torch specifics into a stdlib-only engine.
  • The domain-specific pre-registration helpers from the source module — the
    method-name→callable map, the pre-registered arms, the hardcoded results paths.
    Those are the particulars; register_baseline_from_results() is what remains
    after stripping them.
  • Infrastructure and tooling findings from the audit (container CPU
    throttling, a specific volume being an active Time Machine destination, a
    particular tool's read-before-write guard, one project's stale runtime copy).
    Operationally real, not generalizable method.
  • The audit's findings about the citation engine's own axis reporting. Those
    are bug reports against this package, not methodology, and belong in issues or
    a separate PR — not folded into a method doc.

🤖 Generated with Claude Code

juniperbevensee and others added 2 commits July 28, 2026 15:10
…son method

Promotes, under docs/promotion-and-upstream.md Path A, the generalizable
methodology from an audit of one experimental comparison that was invalid four
different ways across three attempts, each fix moving the bug rather than
removing it: a hardcoded comparator printed by eight scripts and read back out
of a job notification; a reimplementation of the baseline (recall 0.636 ->
0.098); the wrong one of two variants, chosen by name (0.731 vs 0.442); and the
right one run at library signature defaults instead of its tuned parameters.

Every particular is stripped — no study, dataset, species, model, filename or
path. What is promoted is the method and the enforcement.

docs/trustworthy-comparison.md — the case study and 14 checkable rules. Also
covers the other generic failure modes from the same audit: a declared-but-unused
`SR = 22050` constant that scripts believed and resampled to, changing true
positives 2.1x; a label column that was a frequency in Hz, producing 829
singleton classes over 829 examples while the real 13-family/74-label taxonomy
sat in an adjacent file; an optimum on the boundary of the swept range read as
evidence of confidence; unseeded torch making the same script produce F1 0.7839
then 0.7675 and overwriting the artifact a published figure cited; and a helper
that printed HTTP bodies and exited 0, so a 405 was indistinguishable from
success. Generalized: anything that cannot fail loudly will eventually be
believed.

docs/baseline-registry.md — the four welded fields, the API, the contract a
producing script must satisfy, and a "what this still does NOT catch" section.
That last part is the most valuable: structure makes disagreement visible, not
truthful.

docs/results-provenance-checklist.md — what a results JSON must carry to be
regenerable from itself.

matilde_plugin/engine/comparison.py — stdlib-only, domain-neutral vendoring.
compare() raises IncomparableError rather than returning a delta when the split
or criterion differ; split identity is a hash of the sorted member list, never a
label; Criterion is structured data with prose excluded from equality; run() is
bound to the tuned parameters and refuses overrides.

tests/test_comparison.py — 39 tests, one per failure, each named for the mistake
it reproduces. Suite: 144 -> 183 passed, 7 skipped.

The re-scored resolution is stated with its caveats wherever it appears: under
one split and one matcher the classical detector beat the CNN (0.801 vs 0.775
validated matcher; 0.762 vs 0.747 IoU-only), both arms oracle-best on test so
upper bounds not held-out estimates, and it won despite 22 of 30 test files
having leaked into the CNN's training/validation set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…se inference

An independent audit blocked this PR: the docs asserted a settled empirical
conclusion that a better-controlled run contradicts.

A sixth run on the model's TRUE held-out 15 files, zero leakage, thresholds
chosen on validation, reverses the direction on both criteria: CNN 0.859 vs
0.721 (validated matcher) and 0.847 vs 0.630 (IoU-only). The clean margin is
five times the leaky one, pointing the other way.

Worse, the caveat contained a falsified inference. It argued that leakage
inflated the CNN, therefore removing it made the classical detector's win
stronger. Empirically the CNN scored HIGHER on the clean split (0.859) than the
leaked one (0.775) — leakage was not inflating it. That reasoning was asserted
because it sounded conservative and was never checked against a measurement,
which is exactly what this document's own rule 14 warns against.

Rewritten to state what the evidence carries: the comparison was invalid four
ways, a fifth attempt favoured the classical detector, a sixth reversed it, and
the margin is not stable across splits. A result that flips when you fix the
protocol was never a result.

Also from the audit:

- comparison.py: differences() used the literal string as its missing-field
  marker, so a field whose VALUE was that same string was indistinguishable from
  an absent one. differences() returned [] for unequal criteria, and compare(),
  which gated on differences(), computed a delta between incomparable arms. The
  marker is now a unique object, compare() gates on inequality itself, and an
  unlocalised inequality still refuses. Regression test included; it fails
  against the old marker.
- test_declared_none_differs_from_absent asserted the marker's literal value,
  coupling the test to the very detail that was broken. Now asserts semantics.
- rule 12's exemplar no longer cites the reversed finding.
- "eight scripts" clarified: seven printed the same non-interpolated f-string, an
  eighth fed a different constant into a delta.
- the 0.457-0.766 spread now notes it mixes two variables.

184 passed, 7 skipped (was 183/7).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@juniperbevensee
juniperbevensee force-pushed the dev/juniperbevensee/alembic-methodology branch from ca3d4cc to 4f5569f Compare July 28, 2026 03:10
The sanitization gate's semantic layer flagged two docs-index rows for
referencing a specific engagement's particulars rather than generic methodology:
the attempt count of one case study, and 'the bounded-sample validation study'
as a definite thing. The docs themselves passed; only the index prose was flagged.

Reworded to name the failure modes and the mechanism instead. Better writing
regardless -- an index should say what a reader will learn, not how many times
one team got it wrong.

Note the gate is LLM-backed and non-deterministic: this same content passed on an
earlier run of the branch. Worth knowing before treating one pass as clearance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@juniperbevensee
juniperbevensee merged commit 1d60070 into main Jul 28, 2026
3 checks passed
@juniperbevensee
juniperbevensee deleted the dev/juniperbevensee/alembic-methodology branch July 28, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant