Wire the composition levers into the benchmark harness, and measure what they move - #215
Conversation
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
There was a problem hiding this comment.
🟡 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-outflags to TypedMemEval and threads them through to the adapter/memory options and arm token stamping. - Introduces
LongMemEvalGoldValueCoverageProbeplus 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.
| 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; | ||
| } |
| /// <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> |
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
W1c added to this branch — expansion inside point-in-time recallTwo composition arms could not run at all: prospective and temporal declare timestamped grounding, so recall anchors at 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 featureExpansion 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:
Written red-first, and verified against a live store
Unit tests assert at the service seam over substitutes, so they prove the clocks are passed, not that the Cypher uses them. The guard was narrowed by making its claim true, never relaxedIt now refuses GraphRAG alone, which genuinely remains unimplemented as-of. Three pre-existing guard tests failed and every one was right to: 5,195 + 767 green. |
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
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
AgentMemoryLongMemEvalAdapterbuilds recall with two levers whose own comments name the failurethey exist for:
ExpandFactsByPredicate— "a relation returned whole, for the aggregation questions top-Kstructurally cannot answer"
ResolveQueryRelations— "the multi-relation case top-K structurally cannot nominate"Both were set only by
LongMemEvalPreparedPairProgram.TypedMemEvalProgramset neither andexposed no flag.
RecallFanOutOptions.Enabledwas set by nothing undertools/at all.Those are precisely the question shapes that scored zero across five verticals —
alias-then-count0/15, arithmetic
delta0/10 andsum0/14, proceduralstep-order0/20 — and 58–99% of everyfailure 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 toRecallOptionsandMemoryOptions.FanOut.Enabled.expand/qrel/fanout). An ON artifact mustnever 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 reportshow 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:
expand-qrelArithmetic's
durationshape — the one that never needed aggregation — did not move (5/12 botharms). That is what separates a mechanism from global inflation.
Reported against it: gross +11/−3 and +18/−6. One arithmetic question hit
MaxExpandedFactsexactly (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 tail3.71and reporting a miss for a value thatwas 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 MEASURABLErather than a confident
0.00— unmeasurable is not zero.