feat(comparison): comparator-as-record registry + trustworthy-comparison method - #18
Merged
Merged
Conversation
…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
force-pushed
the
dev/juniperbevensee/alembic-methodology
branch
from
July 28, 2026 03:10
ca3d4cc to
4f5569f
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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.best_method: "simple"; the imported variant scores 0.442 where the real one scores 0.731.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
docs/trustworthy-comparison.mddocs/baseline-registry.mddocs/results-provenance-checklist.mdmatilde_plugin/engine/comparison.pycompare()raises rather than returning a delta when split or criterion differ.tests/test_comparison.pyREADME.mdhermes-skill/SKILL.mdAlso distilled (each with a concrete mechanism)
SR = 22050was declared and neverreferenced 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.
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.
lowest value swept, then read as "the model is confident about a lot of
boundaries" — which inverts the meaning.
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 = 499in both runs proves the identical test set).0regardless of status, so a405was indistinguishable from success, and aconfident, detailed, wrong root-cause analysis was written from the symptom.
Generalized: anything that cannot fail loudly will eventually be believed.
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:
maximising F1 on the test set. Upper bounds, not held-out estimates.
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, andthe subtraction refuses unless split and criterion match. The
docs/versioncarries 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 -q— 144 passed, 7 skipped before → 183 passed, 7skipped after. Nothing broken; +39 new.
python3 scripts/check_sanitization.pyon every added/changed file — cleanon the deterministic layer. Semantic layer skipped locally (no
ANTHROPIC_API_KEY); CI will run it.filenames,
/opt/data, tool names) — no hits.Deliberately left out
configure_threads()(sizes the torch thread pool from the cgroup CPUquota). 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.
method-name→callable map, the pre-registered arms, the hardcoded results paths.
Those are the particulars;
register_baseline_from_results()is what remainsafter stripping them.
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.
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