A full 101-scenario baseline run came back failing every quality gate. Instead of guessing at prompt tuning, the low scores were reproduced live, one real answer at a time. That surfaced a genuine, previously-undiscovered bug — the eval harness's own scenario files, plus a 2.3GB leftover test directory, were being ingested into the very ledger it grades answers against. Fixed both, rebuilt the whole pipeline from scratch, and re-ran the identical suite for a controlled before/after. The honest result: real improvement in some metrics, none in others — reported here without smoothing it over.
ollama llama3:latest · devlog_170 · 2026-09-06A previous session's most complete eval run (the pre-Phase-2, 32-scenario suite)
came back Status: FAIL against every gate — 45.8% answer correctness
against an 85% bar, 12.5% hallucination against a 5% ceiling. That was logged as an open finding,
not silenced: the harness doing its job is not a bug. Two things were still undone: (1) the
current 101-scenario, 7-category dataset had never been run end-to-end, only the older
32-scenario version had; (2) nobody had yet looked at why specific answers were scoring
low — matcher strictness, a genuinely wrong answer, and a contaminated evidence set all produce
the same low number, and only reading the real answers tells them apart.
This report's purpose is narrow and honest: run the full baseline, diagnose real failures instead of guessing, fix what diagnosis actually finds, and report the before/after numbers plainly — including the parts that didn't improve.
TODO.md — "Real finding from `ekos eval run`, not a harness bug"| Evaluator | What it measures |
|---|---|
| Answer correctness | Fraction of a scenario's expected_facts present as a case-insensitive substring of the answer text — strict keyword matching, deliberately no LLM judge (RFC 0138 Non-goals). |
| Evidence groundedness | Whether cited evidence actually backs the claim made. |
| Completeness | Combines answer + evidence match coverage for the scenario. |
| Recall@10 | Whether a scenario's expected_objects rank in the top 10 lexical retrieval hits. |
| Hallucination rate | For adversarial scenarios (should_refuse: true) — did the agent fabricate an answer instead of refusing? |
Dataset: ekos-full, 101 scenarios across 7 categories —
architecture (20), code (15), dependencies (12), lineage (12), history (12), security (12),
adversarial (18). Agent: ollama llama3:latest, the model this
workspace's own ekos.toml is configured with (no
ANTHROPIC_API_KEY/OPENAI_API_KEY was set in
this environment, so an A/B against a stronger provider stays an open follow-up, not part of
this comparison). Gates: correctness ≥85%, groundedness ≥90%, completeness ≥80%, recall@10
≥80%, hallucination ≤5%.
Procedure: (1) run the full suite cold against the existing ledger — the
contaminated baseline; (2) reproduce individual low scorers live via ekos
ask to find a real failure pattern; (3) fix what that diagnosis actually found; (4) wipe
.ekos/ and rebuild the whole pipeline from scratch
(init → build → recover → resolve --force → compile → commit); (5)
re-run the identical suite, same agent, same gates — the clean baseline. Only the ledger's
underlying data changed between the two runs.
$ ekos ask "What module redacts secrets and PII before content reaches
the artifact store or ledger?"
"...the module responsible for redacting secrets and PII is the one
mentioned in `evals/datasets/security.yaml`."
Sources: evals/datasets/security.yaml (should never be a source)
ekos/crates/common/src/redaction.rs (the real answer, present but not named)
Scenario sec-001 expects the word "redaction" in the
answer. The real module (redaction.rs) was in the model's evidence set
— but the final answer names the eval's own question file back at itself instead. Confirmed
directly against the ledger, not inferred from the answer alone:
$ ekos query find "FooBarNonexistentAnalyzer" --mode lexical 1 result(s): evals/datasets/adversarial.yaml ← an intentionally-fake name that exists nowhere in EKOS's real source
The adversarial scenario built around that fake name (adv-001
) did correctly refuse to fabricate a description — no hallucination there — but
its stated reason was circular: "the only available information... comes from a search match in
the evals/datasets/adversarial.yaml file." A separate, architecturally
unrelated question (arch-001, "what does EKOS compile enterprise
systems into?") got a plausible-sounding but flatly wrong answer — "a runtime environment"
instead of "the Canonical Knowledge Model" — with no evals/ path
anywhere in sight, showing the two problems are genuinely separate.
ekos.toml's [observe]
ignore-patterns was missing two directories — the same contamination class already fixed
once for doc/doc-sa/doc-objects
(devlog_90):
+ "evals", # RFC 0138's own scenario YAML + saved JSON reports + "test-runs", # 2.3GB local, git-ignored leftover from the RFC 0111/0113 + # E2E soak test (devlog_144) -- git-ignored is not the same + # as ekos.toml-ignored
test-runs/ was the larger of the two: a duplicated
95-partition Elixir workspace plus MySQL/MSSQL DB-script fixtures, fully re-observed on every
build despite being correctly excluded from git. It alone caused a 376-conflict
ekos resolve hard-stop (generic names like generate
/validate/json colliding across
Rust/Python/Elixir/JS symbols from that test corpus) and roughly a third of the ledger's total
object count.
ekos resolve identity conflictsFooBarNonexistentAnalyzer now returns zero ledger hits.ekos-full, 101 scenarios, same agent, same gates| Metric | Contaminated | Clean | Δ |
|---|---|---|---|
| Passed | 47/101 | 48/101 | +1 |
| Answer correctness | 39.3% | 37.6% | −1.7pp |
| Evidence groundedness | 75.5% | 78.3% | +2.8pp |
| Completeness | 37.6% | 36.8% | −0.8pp |
| Recall@10 (see correction below) | 75.0% | 65.0% | −10pp* |
| Hallucination rate | 12.9% | 9.9% | −3.0pp (better) |
Category pass counts, contaminated → clean: adversarial
7→10/18, architecture 9→8/20, code 8→7/15, dependencies 11→11/12, history 2→2/12, lineage 6→7/12,
security 4→3/12. No gate passes in either run — both are still
Status: FAIL.
* Correction (2026-09-07). The −10pp
recall@10 line above was reported here as a regression. It is not one — it is
noise, and the follow-up work says so plainly. Only 10 of the 101
scenarios carry expected_objects, so recall@10 is a mean over
ten values and the "10-point drop" is exactly one scenario flipping from 1.0 to
0.0. The right response is to raise the metric's statistical power, not to tune ranking against a
single data point — which is what RFC 0139
does. Left visible rather than quietly edited out, because a published number that turned out to
be over-read is worth more as a correction than as a deletion.
Second correction (2026-09-08). The column headed Clean above is not clean. Both runs in this table were compiled from a corpus that was 94% not our code — a Python virtualenv and two scanner-output directories that neither exclusion covered. §07 has the measurements, and §09 has the run against a genuinely clean ledger. Kept in place for the same reason as the first correction: a published number that turned out to be over-read is worth more as a correction than as a deletion.
| This report shows | Does not claim |
|---|---|
| A real, confirmed ledger-contamination bug, with a live repro and a zero-hits verification after the fix | That fixing it explains the original 45%-correctness finding — it doesn't |
| A real, modest improvement in hallucination rate (−3.0pp) and evidence groundedness (+2.8pp) | A win across every metric — answer correctness and completeness are flat, and the recall@10 line is one scenario of ten flipping, not a regression (see the correction in §05) |
| The RFC 0016 fact-ledger's WAL surviving an OOM-kill mid-commit with zero corruption, confirmed under a real failure | A performance benchmark — the from-scratch debug-build rebuild took roughly 1.5-2 hours at this corpus size |
The honest conclusion: ledger self-contamination was real and worth fixing — a
system should never cite its own exam paper as evidence — but it was not the dominant cause of
weak answer quality. llama3:latest's own answer-generation quality
(see arch-001, §03) remains the primary open gap. The original TODO's
next steps are now the clearer priority: A/B a stronger provider once an API key is available,
raise the recall@10 metric's statistical power, and only after that, revisit prompt/retrieval
tuning.
What happened next (2026-09-07). Following that conclusion led to RFC 0139, and the conclusion above turned out to be half right. "The model is the primary gap" was the wrong headline: capturing what the model was actually shown revealed that lexical search returned nothing at all for 80 of 89 scenarios, and that 26 completely different questions were being answered from byte-identical evidence. The answer was usually not in the context to find. Relaxing the query lifted answer correctness 37.6% → 48.1% — and, exactly as that RFC predicted it might, briefly made fabrication worse (10 → 15) until a weak-evidence refusal guard was added. The lesson worth keeping: this page's own numbers were honest but under-diagnosed, and only capturing the transcripts settled it.
§04 and §05 above are built on a run labelled clean because
evals/ and a 2.3 GB test-runs/ directory
had been excluded. Both exclusions were real. Neither was close to sufficient — and nothing in
the metrics said so. What eventually said so was a sample of cited claims: every one
pointed into web/api/.venv/lib/python3.13/site-packages/.
| Contaminant | Scale | Why it hid |
|---|---|---|
| Python virtualenv | 3,758 of 3,797 Python files analysed (98.7%) were third-party; 27,408 of 53,830 CKM objects (50.9%) were site-packages, vs 3,838 (7.1%) from EKOS itself | Git-ignored — but .gitignore excludes nothing from the observation walk. Same way test-runs/ was missed. |
| SonarCloud scanner output | 9,107 files (91% of what remained) in two .scannerwork/ directories | Invisible at the CKM layer — just 4 objects, being .ucfg intermediates. But ekos build writes a File object per observed file straight to the ledger, bypassing the CKM: 8,685 of 9,964 ledger File objects, all indexed, all competing with real code in every retrieval. |
Both were self-inflicted, like evals/:
they exist because this project runs its own tooling on itself. A compiler that reads its own
workspace keeps eating its own exhaust.
| After both exclusions | Before | After |
|---|---|---|
| Files observed | ~13,700 | 834 |
| CKM objects | 53,830 | 12,283 |
| EKOS's own source, as a share of the model | 7.1% | 31.6% |
| Identity conflicts | 223 | 25 |
Pipeline through compile | ~1 hour | 53 seconds |
The exclusions removed no knowledge.
After dropping 9,130 files, recover produced identical counts:
2,623 Rust symbols, 1,734 Calls edges, 128 JS modules, 2,518 JS symbols.
That equality is the check that separates a correct exclusion from an over-aggressive one — "fewer
objects" on its own is indistinguishable from "lost knowledge."
Every claim the reasoner saw was a name and, at best, a file path. Measured across
a full run: 0 of 1,289 rendered claims carried a line number; 26.4% carried any
location at all. Analyzers had recorded a source_span per symbol since
RFC 0088 — but never attached a file to it. A symbol knew it lived at lines 200-322 of an
unnamed file.
| Claim locations | Before | After |
|---|---|---|
| Carrying any source location | 26.4% | 67.3% |
Carrying a precise path:line | 0 of 1,289 | 29.8% |
| Evidence records in the ledger | 9,928 | 36,109 → 8,774 (clean corpus) |
The feature shipped, was verified, and did nothing — twice. Both failures presented as success, which is the part worth keeping:
| Silent failure | What it looked like |
|---|---|
The analyzer pass cache could never be invalidated by a code change. CompilerPass::version defaults to "v1" and almost no analyzer overrode it; cache_inputs fingerprints the artifacts read, never the logic reading them. | A 42-minute rebuild, every stage exit 0, a healthy-looking summary — rebuilding the ledger from pre-change data. The only tell: Passes run: 0 buried in a log. |
Evidence line numbers were destroyed in two hops — compile flattened them away, commit could not restore them. | Masked for span-carrying symbols, which re-derive a range. Permanent data loss for the two analyzers that record a real line with no span behind it. Fixing either hop alone would look correct and stay just as broken. |
Both now carry guard tests — each verified by breaking what it guards and watching it fail. For a bug class defined by looking correct, a passing test is not evidence.
ekos-full, 101 scenarios, ollama llama3:latest, ruler v5| Metric | Contaminated (post-RFC 0139) | Clean corpus | Δ |
|---|---|---|---|
| Passed | 39/101 | 43/101 | +4 |
| Answer correctness | 42.5% | 49.7% | +7.2pp |
| Evidence groundedness | 44.0% | 51.6% | +7.6pp |
| Completeness | — | 45.6% | — |
| Invalid citations | — | 0.0% | clean |
Fabrications (of should_refuse) | 3 | 6 — 30% rate | worse |
The regression is stated, not buried. Six scenarios fabricated where three did before. A plausible mechanism: with third-party noise gone, an adversarial question now retrieves real EKOS objects, which read as more convincing evidence and make the model likelier to answer than refuse. That is a hypothesis awaiting the transcripts, not a finding.
The flattering baseline we did not use. Re-grading the original saved answers under the current ruler reports 20/101, which would let this deck claim 20 → 43. It is invalid: that report recorded a ranked result list for only 10 of 101 scenarios (today's records 101/101), so re-grading it penalises 91 scenarios that never captured the data the newer ruler asks for. The comparison above uses the post-RFC 0139 run instead — same ruler, same provider, so only the corpus differs.
Two measurement defects found in the harness itself while
reading this run: the report line "Answered but uncited: 57 (excluded from groundedness)"
is mislabelled — only 10 are excluded, 44 score 0.0, so the metric is more honest than its own
caption claims. And recall@10 is still a mean over just 17 scenarios carrying
expected_objects, so it remains too low-powered to compare across runs —
the same statistical-power problem §05's correction named, only partly addressed.
Five days later (2026-09-14), a further round of fixes shipped — RFC 0139 Phase
2 (a bareword "or" in a fallback search treated as a required literal word; recall@10 graded
against the raw question instead of the query the REASON pipeline actually issues), RFC 0140
§3/§4 (on-demand source text; an opt-in LLM rerank, off by default), and RFC 0141 (a real
signature property on Rust/Python/Elixir functions; attributes on
Calls edges). Same procedure as every prior section: real
recover/resolve --force/compile/commit
rebuild, then the identical 101-scenario suite, same agent, same gates.
| Metric | §09 clean corpus | This rebuild | Δ |
|---|---|---|---|
| Passed | 43/101 | 42/101 | −1 |
| Answer correctness | 49.7% | 49.7% | bit-identical |
| Evidence groundedness | 51.6% | 51.6% | bit-identical |
| Completeness | 45.6% | 46.4% | +0.8pp |
| Recall@10 | 52.9% | 47.1% | −5.8pp* |
| Hallucination rate | 5.9% | 7.9% | +2.0pp |
The bit-identical scores are a real finding, not
a stalled measurement. Every LlmProvider implementation is
contractually required to run at temperature: 0 (RFC 0008) — an
unchanged prompt has no source of entropy left to produce a different completion from a local
model. Landing on the exact same 8-significant-figure score twice confirms this comparison is
measuring like-for-like, not noise.
* The recall@10 line is a correction, not a
regression — the same class of finding as §05's. Diffing every scenario's own
retrieval_recall between the two reports: exactly one
scenario changed. code-002 flipped from 1.0 → 0.0.
Before today, the harness graded recall against the raw question sentence — a different,
unrelated query from what the REASON pipeline actually searches with — and that raw-question
probe happened to retrieve the right object for this one scenario, reporting a perfect score for
a search the pipeline never ran. RFC 0139 Phase 2 fixed the harness to grade the pipeline's real
query instead, and it now honestly reports zero.
$ ekos query find "LlmProvider" --mode lexical a83c0280... build_llm_provider ← now ranks #3, was outside the top ten $ ekos ask --explain "What function builds the LlmProvider used by ekos ask, ..." query type: Structural, routing confidence: 1.00 plan: Compose[ Graph Dependents from LlmProvider (2 hops), Fact LlmProvider.* ] Insufficient evidence. ... does not mention the specific function ...
code-002 still fails —
and it is a new, separate, real finding, not a failed fix. RFC 0141's signature property
demonstrably works: build_llm_provider moved from outside the top ten to
#3 for a bare lexical search, verified directly above, independent of the eval
score. But code-002's own question resolves "LlmProvider" as an
exact-name entity match with confidence 1.00, so the planner routes to a direct dependents-graph
and fact dump on the trait itself and never calls Search at
all — the function the fix made rankable never gets a chance to surface, regardless of ranking
quality. A retrieval-routing gap, not the retrieval-ranking gap RFC 0141 closed.
The refusal shown above is honest and correct given what the model was actually shown.
13 scenarios flipped pass/fail (6 to pass, 7 to fail, net −1). Spot-checked the two adversarial flips: one answer opens "REFUSING." instead of the prompt's required exact opening ("Insufficient evidence.") — a real refusal in intent, graded as a fabrication on wording; the other hedges rather than committing. Neither traces cleanly to a specific fix shipped this round — read as local-model wording variance on borderline prompts, not concluded further here.
The code-002 routing gap from §10 looked like the next
obvious fix. A multi-round classification of all 59 then-failing scenarios said otherwise: a
bigger, cheaper, previously-undiscovered bug was hiding in plain sight —
Search/Graph evidence claims rendered only an
object's name, discarding the exact text BM25 had already matched against. Alongside it,
a citation-block parser that rejected a bare claim position or an "evidence <id>"-wrapped
uuid, and a refusal-wording mismatch where the model's own echoed prompt header ("REFUSING.")
wasn't recognised as a refusal at all. Fixed all three, plus three grading-ruler defects that
would otherwise have inflated the measured gain. The routing fix from §10 is re-sequenced to run
after this, not before — a re-routed plan only helps if the evidence it finds can render its
content, not just a name.
| Metric | §10 rebuild | Phase 1 | Δ |
|---|---|---|---|
| Passed | 42/101 | 53/101 | +11 |
| Answer correctness | 49.7% | 50.1% | +0.4pp |
| Evidence groundedness | 51.6% | 65.9% | +14.3pp |
| Completeness | 46.4% | 50.1% | +3.7pp |
| Recall@10 | 47.1% | 64.7% | +17.6pp |
| Hallucinated (count) | 8/101 | 7/101 | −1 |
Measured category-by-category, not in one run.
A single full-suite background run was OOM-killed twice by the host (real memory pressure, not a
fluke — confirmed via free -h). Each of the 7 categories runs
comfortably inside a single foreground call, and a fresh process per category releases memory
before the next one starts — that's what actually produced this number, at the cost of manually
diffing 7 report files afterward instead of one.
22 scenarios flipped to pass, 11 flipped to
fail — both reported, not just the net. The code category
alone went 3/15 → 8/15, the single largest jump, almost entirely the citation-parsing and
evidence-content fixes landing on scenarios whose answers were already correct and only the
citation mechanics were broken.
$ ekos eval run --dataset ekos-full --category code --agent ollama --save-answers Scenarios: 15 Passed: 8 (was 3/15 before Phase 1) Evidence groundedness: 75.0% Recall@10: 57.1%
Two real side effects, measured and reported, not
hidden. Richer excerpt text on weak/partial-overlap claims made the model fabricate on 4
adversarial false-premise questions it previously refused correctly — exactly offset by 4
different adversarial scenarios the wording fix corrected, net zero for that category but a real
trade-off between generosity of evidence and adversarial safety. Separately, restyling the
prompt's headers to a bracketed form coincided with 3 scenarios where the model wrote
Cited evidence: [...] in prose instead of the required JSON block —
history's only net regression (4/12 → 3/12). Neither pursued further
this pass; both are open follow-ups, not swept under the rug.
$ ekos eval run --dataset ekos-full --agent ollama --json \
--output evals/reports/run.json
$ ekos eval history # trend table across every saved run
| Everything behind this deck | Where |
|---|---|
Real ekos ask/ekos query find transcripts | docs/presentations/examples/eval-comparison/ |
| Full contaminated + clean report JSON | same folder, both saved verbatim |
| Write-up with full "Knowledge Captured" section | devlogs/devlog_170.md, then 172 (honest ruler), 173 (RFC 0140 + the silent caches), 174 (the 94% corpus finding), 182 (RFC 0139/0140/0141 rebuild), 183 (Phase 1: wording/citations/evidence content) |
| Claim-location measurement — offline, deterministic, no LLM calls | cargo run -p ekos --example evidence_locations |
If one thing from this page is worth carrying elsewhere: verify a build by measuring its output, not its exit status. Three separate times here, a run exited 0, printed a plausible summary, and was wrong — a cache that served stale data, a corpus that was 94% foreign, and a measurement read off a search index that had not become visible yet. Each was caught by a quantity check on the artifact, never by a green status.