Skip to content

Gate the re-grade on corpus identity: ids and text cannot detect a redraw - #213

Merged
joslat merged 2 commits into
mainfrom
feat/corpus-sha-keying-guard
Sep 1, 2026
Merged

Gate the re-grade on corpus identity: ids and text cannot detect a redraw#213
joslat merged 2 commits into
mainfrom
feat/corpus-sha-keying-guard

Conversation

@joslat

@joslat joslat commented Sep 1, 2026

Copy link
Copy Markdown
Owner

From the SHA-only keying audit.

AgentEval redraws corpora keeping the question_id set 100% identical with zero byte-identical
items
, and neither corpus_id nor revision moves — corpus_sha256 is the only distinguishing
field
. For bitemporal, 27 of 60 items keep the same question text with a different gold.

That defeats the protection the re-grade already had. The replay is position-keyed and verifies
question text per position: it catches a redraw that reworded anything, and catches nothing
when the text is stable and only the gold moved. Re-grading across such a redraw replays the right
answers against the wrong keys and reports an agreement rate that means nothing — after paying for
the judge pass.

The gate

Corpus sha compared before any judge call. Stored value from the artifact's own
TypedOutcomes.CorpusSha256; current value hashed from the embedded resource this build carries,
rather than read from a manifest — the bytes the run would use, not a claim about them.

Three outcomes, deliberately distinct:

  • match → proceed
  • mismatchABORT, naming both shas
  • no sha recordedwarn, don't fail — artifacts predating provenance capture are still
    re-gradable, but it is the one case where nothing can be verified, so it says so rather than
    passing quietly

Verified both ways: a matching artifact proceeds; an artifact whose sha is moved to the current
bitemporal lineage (abf2f3f4) aborts with both shas named.

Also documents the adapter's blindness as a test rather than pretending it away — identical text
passes the positional check whatever the gold now says, which is exactly why identity belongs one
layer up.

Release 0-warn; 734/734.

…draw

AgentEval redraws corpora keeping the question_id set 100% IDENTICAL with ZERO byte-identical items,
and neither corpus_id nor revision moves either -- corpus_sha256 is the only distinguishing field.
For bitemporal, 27 of 60 items keep the SAME question text and carry a DIFFERENT gold.

That defeats the protection the re-grade already had. The replay is position-keyed and verifies the
question TEXT at each position, which catches a redraw that reworded anything and catches NOTHING
when the text is stable and only the gold moved. Re-grading across such a redraw would replay the
right answers against the wrong keys and report an agreement rate that means nothing -- after paying
for the judge pass.

So the corpus sha is now compared BEFORE any judge call. The stored value comes from the artifact's
own TypedOutcomes.CorpusSha256; the current one is hashed from the embedded resource this build
actually carries, rather than read from a manifest, so it is the bytes the run would use and not a
claim about them.

Three outcomes, deliberately distinct: a match proceeds; a MISMATCH ABORTS and names both shas; and
an artifact carrying no sha at all WARNS rather than failing -- artifacts predating provenance
capture are still re-gradable, but it is the one case where nothing can be verified, so it says so
instead of passing quietly.

Verified both directions: a matching artifact proceeds, and an artifact whose sha was moved to the
current bitemporal lineage (abf2f3f4) aborts with both shas named.

Also documents the adapter's blindness as a test rather than pretending it away: identical text
passes the positional check no matter what the gold now says, which is precisely why corpus identity
must be established one layer up.

Release 0-warn; LongMemEval 734/734.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
Copilot AI lite review requested due to automatic review settings September 1, 2026 13:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new gate can proceed silently when the current corpus SHA can’t be resolved and can throw on malformed/short SHA strings, undermining the intended “verify-before-spend” behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds a corpus-identity gate to the TypedMemEval re-grade tool to prevent re-grading artifacts against a different (redrawn) corpus with identical question ids/text, and adds a unit test documenting the replay adapter’s positional-blindness to “gold moved behind unchanged text”.

Changes:

  • Abort re-grade early when the artifact’s stored TypedOutcomes.CorpusSha256 doesn’t match the corpus bytes embedded in the current build.
  • Warn (non-fatal) when an artifact has no recorded corpus SHA (pre-provenance artifacts).
  • Add a unit test that documents the replay adapter’s inability to detect moved gold when question text is unchanged.
File summaries
File Description
tools/AgentMemory.LongMemEval/TypedMemEvalRegradeProgram.cs Adds the corpus SHA gate and computes current corpus SHA from embedded resources.
tests/AgentMemory.Tests.Unit.LongMemEval/TypedMemEvalReplayAdapterTests.cs Adds a unit test documenting a known limitation of position-keyed replay.
Review details

Suppressed comments (1)

tools/AgentMemory.LongMemEval/TypedMemEvalRegradeProgram.cs:140

  • If CurrentCorpusSha returns null (resource not found or ambiguous), the code currently proceeds silently even though the doc comment says this should be treated as "cannot verify". Also, slicing storedCorpusSha[..16] can throw if the stored value is shorter than 16 chars. Add an explicit warning branch for the null case and make the abort message length-safe.
            else if (currentCorpusSha is not null &&
                     !string.Equals(storedCorpusSha, currentCorpusSha, StringComparison.OrdinalIgnoreCase))
            {
                Console.Error.WriteLine(
                    $"regrade: ABORT — corpus mismatch. The artifact was produced against " +
                    $"{storedCorpusSha[..16]}… and this build carries {currentCorpusSha[..16]}…. " +
                    "Question ids and text are NOT sufficient to detect this: corpora are redrawn " +
                    "keeping ids identical, and gold can move behind unchanged text. Re-grade with " +
                    "the package the artifact was produced against.");
                return 5;
  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/AgentMemory.LongMemEval/TypedMemEvalRegradeProgram.cs
Comment thread tools/AgentMemory.LongMemEval/TypedMemEvalRegradeProgram.cs Outdated
Comment thread tools/AgentMemory.LongMemEval/TypedMemEvalRegradeProgram.cs Outdated
…ash once

All three from Copilot's review of #213, and all three real.

A non-string CorpusSha256 -- null, a number, an object on a malformed or older artifact -- would have
thrown out of GetString() and killed the tool. This gate's entire job is to fail SAFELY before
anything is spent, so it now checks ValueKind and treats anything that is not a non-empty string as
"cannot verify", which takes the warn path rather than the crash path. Verified with a null.

FirstOrDefault over manifest resource names resolved an ambiguity by manifest ordering, which is not
guaranteed. A gate that picks a different corpus on a different run is WORSE than no gate, because it
looks like it verified something. It now requires exactly one match and returns null otherwise.

And the hash is taken straight off the resource stream instead of buffering into a MemoryStream and
copying again via ToArray -- the corpora run to ~1.5 MB and that was two full copies to compute a
digest.

Verified after the change: a moved sha still ABORTS with both shas named; a null sha WARNS and
proceeds. LongMemEval 734/734.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
@joslat
joslat merged commit e2969f1 into main Sep 1, 2026
7 checks passed
@joslat
joslat deleted the feat/corpus-sha-keying-guard branch September 1, 2026 15:47
joslat added a commit that referenced this pull request Sep 2, 2026
Copilot's review of #214: the mismatch message sliced both shas at a fixed [..16], which throws
ArgumentOutOfRangeException on a truncated or corrupt value -- in the one branch whose entire purpose
is to fail SAFELY with an exit code rather than an exception.

Second time on this same gate. #213 hardened the sha's TYPE (a non-string CorpusSha256 would have
thrown out of GetString()) and left its LENGTH assumed. Same defensive question, same guard, asked
about one property and not the next. A guard that can throw is not a guard.

Verified end to end with a truncated sha: it now aborts cleanly and prints the short value as-is
rather than crashing.

Release 0-warn; LongMemEval 744/744.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
joslat added a commit that referenced this pull request Sep 2, 2026
#214)

* Make the corpus-identity gate a tested unit, and close the id-keying audit

Discharges the family-wide audit: every cache, baseline, join and tool of ours that could key on
question_id/corpus_id/revision across corpus versions.

RESULT: one gap, in one surface, already closed in #213 -- the --regrade cross-artifact join, whose
position-and-text keying catches a reworded redraw and catches nothing when the text is stable and
only the gold moves. Everything else is clean, and two of those are worth recording rather than
merely ticking:

  - PREPARED-CORPUS REUSE already compares datasetSha256 alongside eleven identity fields, and its
    own header states the principle this audit is about -- "treating unknown as equal is how a check
    stops being able to fail". It reports unrecorded fields on older manifests as DRIFT rather than
    agreement. That is the standard the re-grade has now been brought up to.
  - No other verb reads two artifacts at all, so --regrade was the only place the hazard could live.

WHAT THIS COMMIT ADDS: the gate's decision was a code path only exercisable by running the tool
against a hand-mutated artifact. It is now CorpusIdentity.Verify with three verdicts and unit tests
for each, including hex-casing (artifacts and manifests disagree about it, and that is not a redraw).

Unverifiable stays its own verdict deliberately. Folded into Match it passes silently -- the
constant-column failure this project has been bitten by three times. Folded into Mismatch it blocks
every artifact older than provenance capture, making the gate obstructive enough to get bypassed. So
it warns and refuses to be read as agreement.

Release 0-warn; LongMemEval 742/742.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo

* Stop the abort path assuming its sha is well-formed

Copilot's review of #214: the mismatch message sliced both shas at a fixed [..16], which throws
ArgumentOutOfRangeException on a truncated or corrupt value -- in the one branch whose entire purpose
is to fail SAFELY with an exit code rather than an exception.

Second time on this same gate. #213 hardened the sha's TYPE (a non-string CorpusSha256 would have
thrown out of GetString()) and left its LENGTH assumed. Same defensive question, same guard, asked
about one property and not the next. A guard that can throw is not a guard.

Verified end to end with a truncated sha: it now aborts cleanly and prints the short value as-is
rather than crashing.

Release 0-warn; LongMemEval 744/744.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants