Skip to content

Wire the composition levers into the benchmark harness, and measure what they move - #215

Merged
joslat merged 14 commits into
mainfrom
feat/agenteval-0.33-cd
Sep 8, 2026
Merged

Wire the composition levers into the benchmark harness, and measure what they move#215
joslat merged 14 commits into
mainfrom
feat/agenteval-0.33-cd

Conversation

@joslat

@joslat joslat commented Sep 7, 2026

Copy link
Copy Markdown
Owner

What this is

The benchmark harness could not switch on the engine's own composition machinery, so every
TypedMemEval number this repository holds was measured with it off. This wires it, guards that it
arrives, and adds a value-grained instrument for reading the results.

The defect

AgentMemoryLongMemEvalAdapter builds recall with two levers whose own comments name the failure
they exist for:

  • ExpandFactsByPredicate"a relation returned whole, for the aggregation questions top-K
    structurally cannot answer"
  • ResolveQueryRelations"the multi-relation case top-K structurally cannot nominate"

Both were set only by LongMemEvalPreparedPairProgram. TypedMemEvalProgram set neither and
exposed no flag. RecallFanOutOptions.Enabled was set by nothing under tools/ at all.

Those are precisely the question shapes that scored zero across five verticals — alias-then-count
0/15, arithmetic delta 0/10 and sum 0/14, procedural step-order 0/20 — and 58–99% of every
failure in that family was a failure where the required evidence was incomplete in the answer
context.

What changed

  • --expand-facts, --resolve-query-relations, --recall-fan-out, threaded through to
    RecallOptions and MemoryOptions.FanOut.Enabled.
  • Each lever stamps itself into the arm token (expand / qrel / fanout). An ON artifact must
    never be mistakable for a default one on disk — a run that silently took the default path would
    reproduce the baseline and read as "the levers do nothing", which is the most dangerous outcome
    available here.
  • LongMemEvalGoldValueCoverageProbe — record-only, reads what recall already returned. It reports
    how many of the gold-bearing turns' values reached the recalled facts, separating "retrieval never
    had it"
    from "retrieval had it and the answer still missed".

Off stays off. An unflagged run is byte-identical to every sealed measurement before it, and a
test asserts it.

Measured effect

Same corpus, same seed, same judge, gates passed on both:

vertical default expand-qrel
arithmetic 7/50 = 14% 15/50 = 30%
procedural 9/80 = 11.2% 21/80 = 26.2%

Arithmetic's duration shape — the one that never needed aggregation — did not move (5/12 both
arms). That is what separates a mechanism from global inflation.

Reported against it: gross +11/−3 and +18/−6. One arithmetic question hit MaxExpandedFacts
exactly (10 → 60 facts) and regressed; expansion injects distractors as well as evidence.

Tests

767 in the harness suite, 5,192 in the main unit suite. The new ones follow each lever from the
command line to the object the engine actually reads — a flag that parses is not a flag that
arrives
, which is what all five reachable-but-not-fed instances in this repo had in common.

They also caught two real defects in the instrument before it touched any data: an integer rule
matching the digit groups inside decimals (nine values reported for three), and an amount pattern
that could not match a bare 1113.71, matching the tail 3.71 and reporting a miss for a value that
was present.

One thing shipped deliberately imperfect

The instrument's first version sourced required values from the gold answer, which for an
aggregation question states a total that is never stored. Validation against real corpus text showed
it measured the wrong thing (arithmetic: mean 1.00 value per gold — the result, not the inputs) and
was entirely silent on procedural (0 of 80 golds carry a value). The commit history keeps that
finding rather than hiding it, and the follow-up commit re-sources from gold-bearing turns.

It remains numeric-only by design: procedural's values are step names, and a name matcher tuned
on invented multi-word corpus names would be tuned on padding. The line prints NOT MEASURABLE
rather than a confident 0.00 — unmeasurable is not zero.

joslat and others added 5 commits September 3, 2026 21:07
Probed rather than trusted: both fixes the gate required are present in the published package, hashed
from the embedded resources.

  forgetting   BE14B81AE4E2F3D8   (was 7FE6E166 -- the 72-distractor lineage)
  prospective  39F205B722948949   (was A570B890 -- due-window answer key fixed, V8 4/18 -> 16/18)

EIGHT OF NINE CORPORA MOVED. Only bitemporal is unchanged, and its numbers are off-state anyway
because supersession never fired on it. So every prior TypedMemEval number this project holds is now
historical -- including row 53's arithmetic control, whose corpus moved at 0.33 (2FEDA94B ->
535F4ED0). C-D genuinely re-establishes that control rather than reusing it; my earlier note claiming
otherwise was true only up to 0.32.

Two corpora that our verb could not previously name -- conjunction and semantic -- are reachable
without code changes, because the vertical descriptors come from the package. Verified by dry run:
conjunction selects 65 questions across all four shapes including conditional-branch (15), which is
what the recall-fan-out router's ship/no-ship decision reads.

STAGE 1 CLEAN ON ALL NINE, spending nothing: every corpus loads, every question count and shape
distribution is as expected under seed 20260821. Release 0-warn; 5192 + 744 + 54 unit tests green
against the new package with no source changes.

Also corrected in the probe record: I previously wrote that prospective was "unmoved since 0.29"
having hashed only 0.31 and 0.32 and inferred the rest. Measured: 0.29 carried 6DDD3E9BB594816E. The
conclusion held; the supporting fact did not, and inferring a hash inside an argument for hashing is
the worst place to do it.

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

The C-D family run (five verticals, 295 questions, ~35h) measured this engine
with its composition machinery hard off, because no TypedMemEval run could
switch it on:

  ExpandFactsByPredicate / ResolveQueryRelations -- set ONLY by
  LongMemEvalPreparedPairProgram. The adapter's own comments say they exist
  'for the aggregation questions top-K structurally cannot answer' and 'the
  multi-relation case top-K structurally cannot nominate'.

  RecallFanOutOptions.Enabled -- set by NOTHING under tools/ at all.

Those are precisely the shapes that scored 0/15 (alias-then-count), 0/10
(arithmetic delta), 0/14 (sum) and 0/20 (procedural step-order), and 58-99% of
every failure across the five corpora was a failure where the required evidence
was incomplete in the answer context.

This adds --expand-facts, --resolve-query-relations and --recall-fan-out,
threads recallFanOut to MemoryOptions.FanOut.Enabled, and stamps each lever
into the arm token (expand / qrel / fanout) so an ON artifact can never be
mistaken for the default arm on disk.

Ten guard tests follow each lever from the command line to the object the
engine reads -- a flag that parses is not a flag that arrives, which is what
all five reachable-but-not-fed instances had in common.

Off stays off: an unflagged run is byte-identical to every sealed measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
The ON arm's whole value is its pairing with row 54's arithmetic 7/50, which
was graded on 0.33. Arithmetic's corpus is byte-identical in 0.34
(535f4ed01b92 both), but the RUNNER is not known to be, and no corpus hash
detects a judge change -- so the pin follows the baseline, not the newest
release.

Verified from the artifacts before the run started, not assumed:
  corpus 535f4ed01b92d4bb == 535f4ed01b92d4bb
  judge  b1d3f7216032ba27 == b1d3f7216032ba27

0.34 returns when the next vertical's baseline lives there.

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

Row 56 pre-registered a session-grained metric as the confirmation test for a
fact-grained mechanism, and it nearly missed a real effect: six of eleven gained
questions had coverage 4/4 -> 4/4, and one LOST question's coverage improved.
Row 58 then produced losses that volume cannot explain -- tme-prc-045 lost a
required session while gaining ONE fact.

LongMemEvalGoldValueCoverage measures, per question, how many of the distinctive
values the gold carries actually reached the recalled facts. Deliberately narrow:
amounts and multi-digit quantities only, names excluded, and null (not zero) when
the gold carries no recognised value -- folding those together is the
constant-column failure this repo has hit three times.

The tests earned their keep before any data: they caught an integer rule matching
the digit groups INSIDE decimals (nine values for three) and an amount pattern
that could not match a bare 1113.71, matching the tail 3.71 and reporting a miss
for a value that was present.

Then validation against real gold text killed the premise, and the type is
committed UNWIRED because of it:

  arithmetic  36/50 golds measurable, mean 1.00 values
  procedural   0/80 golds measurable

It is silent on exactly the case it was built for, and even for arithmetic it
reads the gold ANSWER (the total, which is not stored) rather than the components
(which are). The correct source is the gold-bearing turns via
LongMemEvalMessageOrigin.HasAnswer -- cheap, but not applied, because a fix aimed
at the wrong question is not worth wiring, and procedural's values are step names
that this rule excludes on purpose.

Kept, tested, honestly labelled arithmetic-shaped. Third hypothesis falsified this
wave after ComponentCoverage and crowding-out.

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

Validation of the first version killed its premise: it read required values from
the gold ANSWER, which for an aggregation question states a total that is never
stored -- the components are. Measured that way it reported a miss on questions
the engine answered correctly (arithmetic: 36/50 golds measurable, mean 1.00
value each -- the result, not the inputs).

LongMemEvalGoldValueCoverageProbe now takes required values from the turns
LongMemEvalMessageOrigin.HasAnswer marks, which carry the components, and reports
per run how many reached the recalled facts. That separates 'retrieval never had
the value' from 'retrieval had it and the answer still missed' -- the distinction
the session-grained metric registered in row 56 could not resolve, and the one
that decides whether arithmetic's delta 1/10 is a retrieval or a reasoning gap.

Record-only: it reads what recall already returned and influences nothing. A
measurement that changed the thing it measures would void every pairing in this
wave.

Unmeasurable stays distinct from zero, in the code and in the printed line.
Procedural carries no amounts in any of its 80 golds, so the run prints NOT
MEASURABLE rather than a confident 0.00 -- the constant-column failure this
repository has hit three times.

767 tests green.

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 7, 2026 07:26

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 gold-value coverage reporting contains a misleading “NOT MEASURABLE” message and an XML doc comment issue that should be corrected to avoid incorrect diagnostics and documentation warnings.

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

Pull request overview

This PR updates the AgentMemory.LongMemEval benchmark harness so TypedMemEval can explicitly toggle the engine’s composition-related retrieval levers (expand facts, resolve query relations, recall fan-out), and adds a record-only probe to measure whether numeric “gold-bearing” values actually reached retrieved facts (separating retrieval misses from answering misses).

Changes:

  • Adds --expand-facts, --resolve-query-relations, and --recall-fan-out flags to TypedMemEval and threads them through to the adapter/memory options and arm token stamping.
  • Introduces LongMemEvalGoldValueCoverageProbe plus reporting to measure value-grained retrieval coverage without affecting retrieval/prompting.
  • Adds focused unit tests to ensure flags are recognized, carried end-to-end, and that the new coverage metric behaves correctly (normalization, null vs zero, partial coverage).
File summaries
File Description
tools/AgentMemory.LongMemEval/TypedMemEvalProgram.cs Wires new CLI levers into the TypedMemEval run path and prints gold-value coverage summary.
tools/AgentMemory.LongMemEval/TypedMemEvalArm.cs Extends arm identity to include the new levers and stamps them into file tokens/descriptions.
tools/AgentMemory.LongMemEval/LongMemEvalMemoryProfile.cs Threads recall fan-out into MemoryOptions so the engine can actually see it.
tools/AgentMemory.LongMemEval/LongMemEvalGoldValueCoverage.cs Adds the numeric-only extraction/normalization + probe for value-grained retrieval coverage.
tools/AgentMemory.LongMemEval/AgentMemoryLongMemEvalAdapter.cs Records gold-turn values vs recalled fact values via the optional probe.
tools/AgentMemory.LongMemEval/AgentMemory.LongMemEval.csproj Bumps AgentEval dependency version.
tests/AgentMemory.Tests.Unit.LongMemEval/TypedMemEvalCommandLineTests.cs Verifies new flags are advertised and carried onto the options record.
tests/AgentMemory.Tests.Unit.LongMemEval/TypedMemEvalArmProvenanceTests.cs Updates arm provenance assertions for the new levers.
tests/AgentMemory.Tests.Unit.LongMemEval/GraphRagWiringTests.cs Threads recall fan-out through the service wiring tests.
tests/AgentMemory.Tests.Unit.LongMemEval/GoldValueCoverageTests.cs Adds unit coverage for extraction/measurement/probe correctness and failure boundaries.
tests/AgentMemory.Tests.Unit.LongMemEval/FacadeCallerFeedsTests.cs Updates options construction to include new lever fields.
tests/AgentMemory.Tests.Unit.LongMemEval/CompositionLeverWiringTests.cs New end-to-end tests proving each lever reaches the actual consumer object(s).
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • 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 on lines +582 to +588
if (samples.Count == 0)
{
Console.WriteLine(
"typedmemeval: gold-value coverage — NOT MEASURABLE on this vertical "
+ "(no gold-bearing turn carries an amount or quantity).");
return;
}
Comment on lines +569 to +572
/// <summary>Announces what supersession actually wrote, loudly when it wrote nothing.</summary>
/// <summary>
/// Reports value-grained retrieval coverage, and reports the UNMEASURED count beside it.
/// </summary>
joslat and others added 3 commits September 7, 2026 10:22
Self-review of the diff caught five files whose encoding moved for no reason:
four .cs files gained a UTF-8 BOM from Python writes using utf-8-sig, and the
.csproj LOST its BOM to PowerShell Set-Content.

None of it was intended and none of it belongs in a diff about retrieval levers.
An encoding flip is invisible in review, shows every line of a file as changed
in some tools, and this repository has been bitten by a regenerated snapshot
silently acquiring a BOM before.

Content is unchanged; only the byte-order marks are restored to what main has.

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

RecallAsOfAsync reached SearchFactsAsOfAsync, which had no expansion parameters
at all -- so a caller asking for aggregation on a point-in-time question got
top-K similarity and nothing else, silently. The benchmark harness refused to
run rather than let those flags do nothing, which is why prospective and
temporal could not take a composition arm.

This is a user-facing gap, not only a harness one: any application asking 'what
did I know as of March' gets no aggregation support on the one path whose whole
purpose is point-in-time questions.

THE INVARIANT IS THE FEATURE. Expansion widens a query from 'the K most similar
facts' to 'this relation, whole', and a relation returned whole from an as-of
query must be the relation AS IT STOOD THEN. A dropped clock here does not lose
a row, it ADDS one -- a present-day fact inside an answer about March -- and a
wrong point-in-time answer is indistinguishable from a right one without
checking the clock.

So the invariant is structural, not merely tested:
  - SearchByCanonicalPredicatesAsOfAsync is a SEPARATE method from the unbounded
    one, with NON-OPTIONAL clocks. Passing the wrong one is not a mistake
    reachable by argument.
  - Its default returns EMPTY and never delegates to the unbounded overload. An
    implementor without the clock-aware query degrades to unexpanded recall --
    incomplete, which is recoverable. Delegating would make every non-overriding
    implementor leak facts from outside the window, which is not.
  - The Cypher carries the same four clock predicates as SearchFactsAsOf, in the
    same order, so the similarity half and the expansion half of one recall
    cannot disagree about what 'as of' means.

Written RED-FIRST per the ruling: AsOfExpansionTests did not COMPILE before the
overload existed, then failed 2 of 3 once it did, with the off-state test passing
-- so the harness was sound and the feature was the gap.

The harness guard is NARROWED BY MAKING ITS CLAIM TRUE, never relaxed: it now
refuses GraphRAG alone, which genuinely remains unimplemented as-of.

Three pre-existing guard tests failed and every one was right to.
AsOfRecallDivergenceTests required the three options be struck from the
documented divergence list deliberately; the injection guard test required its
expectation rewritten. Added the converse case too -- expansion options are now
ACCEPTED -- because either test alone permits a half-wired capability: reachable
but refused, or accepted but not implemented.

EnableGraphRag deliberately stays on the divergence list. Bundling GraphRAG-as-of
would ship two capabilities under one invariant test.

5,195 + 767 tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
The unit tests assert at the service seam over substitutes, so they prove the
clocks are PASSED. They cannot prove the Cypher USES them, or that it parses at
all -- without this the query's first execution would have been inside a
multi-hour paid run.

Three facts under one predicate, so expansion matches all three by predicate_key
and only the clocks can separate them:
  in-window            returned
  not-yet-true         excluded by the VALID clock (valid_from after as-of)
  no-longer-believed   excluded by the TRANSACTION clock (invalidated before as-of)

The second test is the control and it earned itself on the first run: both
queries came back empty, which immediately identified the fault as my seeding
(raw predicate rather than canonical predicate_key) rather than the new query.
Without it an empty as-of result would have looked like a working filter, and a
query matching nothing would have passed for the right reasons by accident.

That control also guards the asymmetry that makes this feature dangerous: a
dropped clock in expansion does not lose a row, it ADDS one, and the extra fact
looks exactly like legitimate relation completeness.

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

joslat commented Sep 7, 2026

Copy link
Copy Markdown
Owner Author

W1c added to this branch — expansion inside point-in-time recall

Two composition arms could not run at all: prospective and temporal declare timestamped grounding, so recall anchors at RecallAsOfAsync, which had no expansion parameters at all. An existing guard refused rather than running flags that would silently do nothing — it saved two 5-hour runs for about a minute of cost.

That is a user-facing gap, not only a harness one: any application asking "what did I know as of March" got top-K similarity and no aggregation support, on the one path whose whole purpose is point-in-time questions.

The invariant is the feature

Expansion widens a query from "the K most similar facts" to "this relation, whole", and a relation returned whole from an as-of query must be the relation as it stood then. A dropped clock here does not lose a row — it adds one, and the extra fact looks exactly like legitimate completeness.

So the invariant is structural rather than only tested:

  • SearchByCanonicalPredicatesAsOfAsync is a separate method with non-optional clocks — passing the wrong one is not reachable by argument.
  • Its default returns empty and never delegates to the unbounded overload. A non-overriding implementor degrades to unexpanded recall, which is merely incomplete. Delegating would make every one of them silently leak facts from outside the window. Incomplete is recoverable; wrong-and-confident is not.
  • The Cypher carries the same four clock predicates as SearchFactsAsOf, in the same order, so the similarity half and the expansion half of one recall cannot disagree about what "as of" means.

Written red-first, and verified against a live store

AsOfExpansionTests did not compile before the overload existed, then failed 2 of 3 once it did — with the off-state test passing, proving the harness was sound and the feature was the gap.

Unit tests assert at the service seam over substitutes, so they prove the clocks are passed, not that the Cypher uses them. AsOfExpansionIntegrationTests runs it against live Neo4j: three facts under one predicate, not-yet-true excluded by the valid clock, no-longer-believed by the transaction clock. Its control test earned itself immediately — the first run came back empty from both queries, identifying the fault as my seeding rather than the new query.

The guard was narrowed by making its claim true, never relaxed

It now refuses GraphRAG alone, which genuinely remains unimplemented as-of.

Three pre-existing guard tests failed and every one was right to: AsOfRecallDivergenceTests required the three options be struck from the documented divergence list deliberately. I added the converse case too — expansion options are now accepted — because either test alone permits a half-wired capability: reachable but refused, or accepted but not implemented.

5,195 + 767 green.

joslat and others added 6 commits September 8, 2026 02:18
Prospective's composition arm came back flat with facts-per-question 6.20 -> 5.98,
where arithmetic saw 3.8x, procedural 5.6x and conjunction 4.7x. Two explanations
with opposite consequences: expansion ran and had nothing to expand (the result
stands), or expansion never ran (the result is void and W1c is defective).

Settled against live Neo4j rather than by argument:
  dense relation, 12 facts under one predicate -> expansion returns all 12
  singleton relations, 12 predicates x 1 fact  -> returns 12, adding nothing

So the code fires, and prospective's corpus is the singleton shape -- unique
reminders, one fact per relation. The flatness is a property of the data.

That turns the wave's pattern into a mechanism with two necessary conditions:
relations must be DENSE, or expansion returns what similarity already had; and
the question must need RELATION-COMPLETENESS, or the extra facts do not help
(conjunction had 4.7x the facts and scored 12->11). Both met gives +15/+16.

The second test is the control and it is the point: a relation of size one is
already complete in the top-K, so no gain is available and none is a bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
Wave 1 measured five verticals and the fact ratio against the default arm
separated the outcomes perfectly:

  arithmetic  3.8x -> +16 points
  procedural  5.6x -> +15
  conjunction 4.7x ->  -1   (dense, but its questions need hops)
  prospective 0.96x -> -2   (sparse: singleton relations)
  temporal    1.04x ->  0   (sparse)

Every vertical below 1.1x was flat; every gain came from one above 3.8x.
Expansion returns a relation WHOLE, so it can only add rows where relations are
dense. On singleton relations it returns exactly what similarity already had.

So this prints facts/question on any expansion arm, to make the five-hour
question answerable cheaply.

AND IT IS DELIBERATELY MODEST, because measuring it immediately falsified the
claim I first wrote: temporal reads 10.00 facts at n=1 against a 5.54 full-run
mean -- roughly 2x off, in the direction that makes a sparse vertical look dense.
The docstring and the printed line both now say one question is a noisy estimate
and to sample 5+ before treating it as a go/no-go. The instrument is worth having
because it is cheap, not because one sample is reliable.

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

1. --arithmetic-memory costs 16 points (arithmetic 30% -> 14%). Not because the
   accountant is bad, but because it was never finished: it writes counts and
   sums, marks them with derivation_key, and NOTHING at recall reads that mark.
   ~2,560 derived facts entered the same similarity pool and displaced the source
   values they were computed from -- facts/question fell 35.4 -> 27.6. A feature
   that writes answers into memory made the inputs harder to retrieve.

   RecallOptions.MaxDerivedFacts now has three deliberate states:
     null  compete in the ordinary pool -- pre-existing, and the measured-harmful
           one; kept as the default only so sealed measurements stay comparable
     0     excluded; strictly better whenever nothing reads them
     >0    their OWN budget, appended, so a count reaches the prompt WITHOUT
           costing a source fact its slot

   That third state is what makes the accountant measurable at all: whether it
   HELPS is still unknown, and today's evidence does not establish it either way.

   A test caught a real bug in the wiring: the early return when expansion is off
   skipped the derived budget entirely, so the feature would have done nothing on
   every non-expansion recall -- silently, with a knob that looked like a fix.

2. Wave 1 spent ten hours learning that prospective and temporal have SINGLETON
   relations, which predicate expansion cannot widen by construction. Density is
   a property of the STORE and identical in both arms, so it needs no paired
   ratio: ReadPredicateDensityAsync reports facts, predicates, mean, largest
   relation, and the share holding >1 fact. Validated against the known-dense
   vertical first: arithmetic reads mean 3.61, largest 60, 39% multi-fact.

Derived facts are identified by derivation_key, which no ordinary fact carries,
so the filter is a property test rather than a heuristic. Both query flags default
false, leaving the Cypher byte-for-byte what it was.

AS-OF DIVERGENCE, DELIBERATE AND RECORDED: derived facts stay out of point-in-time
recall. A derived total is computed "now" from inputs with their own validity
windows, and nothing records the window the AGGREGATE held over -- so placing one
in an as-of answer would assert a total that may never have been true at that
instant, indistinguishable from a correct one without recomputing it. A substrate
gap, not an oversight.

Two guard tests failed and both were right to: the as-of divergence list demanded
this decision be documented, and the enum-count guard demanded architecture.md be
updated. 5,201 + 767 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
Declared in the pre-registration BEFORE the run rather than discovered after it:
without this, the derived-budget arm and row 61's arm (same accountant, same
expansion, no read side) produce an identical token, so their artifacts are
indistinguishable on disk. That is precisely the defect TypedMemEvalArm exists to
close -- a number with its story attached separately.

The VALUE is in the token, not merely the fact that it was set: 0 means exclude
and 10 means own-budget, and those are different arms making opposite predictions.

Zero is explicitly not 'unset'. Negative is rejected at parse time rather than
running as a typo.

774 harness tests green.
The derived-budget arm scored 10/50 against 15/50 for expansion alone, so the
accountant still cost 10 points even with a read side. The reason is a defect in
that read side, found by reading the code rather than theorising about the score.

facts/question fell 35.44 -> 27.94. If the vector search excludes derived facts
and APPENDS up to 10, source facts should have stayed near 35.

FactQueries.SearchByCanonicalPredicates -- the EXPANSION query -- had no
derivation filter at all: `predicate_key IN $keys AND invalidated_at IS NULL`.
Expansion returns a relation WHOLE, so it handed back the accountant's derived
facts alongside the sources and filled its 60 slots with the very output the
derived budget exists to segregate.

I filtered the path that mattered least and left open the one that mattered most
-- the same shape as the early-return bug the unit tests caught, one layer down.
Half a read side reads as a failed feature.

The budget now covers both retrieval paths or neither, pinned by two tests: one
asserting expansion is filtered when a budget is set, one asserting null leaves
expansion exactly as it was.

Consequence for the ledger: row 66 measured a HALF-WIRED read side, so it does
not settle whether the accountant helps -- only that the read side must cover
expansion. The ship/no-ship call waits for the re-measure.

5,203 + 774 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
1. "NOT MEASURABLE" conflated two different facts. An empty sample set means
   "the probe ran and no gold-bearing turn carries an amount" ONLY if the probe
   ran -- on the oracle arm there is no adapter, nothing calls Record, and the
   same empty list means something else entirely. One message for both would
   report an UNMEASURED arm as a measured-empty one.

   That is the constant-column failure made inside the instrument built to avoid
   it. The null-vs-zero discipline has to apply to the reporting line, not only
   to the metric. Now three states: NOT MEASURED (no probe on this arm), NOT
   MEASURABLE (probe ran, nothing to measure), and a count.

2. Doc-comment orphaning, for the fourth time in this repository. Each inserted
   print method prepended its doc block but appended its method, so four
   summaries stacked in reverse order and PrintGoldValueCoverage carried a
   summary about supersession. Re-paired every block with its own method.

775 harness tests green. Release verification deferred: the running measurement
holds those DLLs, and the build correctly refuses rather than corrupting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RM9gMsvo3SpYqEBENbaefo
@joslat
joslat merged commit a5328cc into main Sep 8, 2026
7 checks passed
@joslat
joslat deleted the feat/agenteval-0.33-cd branch September 8, 2026 21:37
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