Skip to content

fix(answers,pipeline,judge,gate): Stack P fixes, step 2 of #228 - #274

Open
napetrov wants to merge 71 commits into
intel:mainfrom
napetrov:fix/answers-stack-p-step2
Open

napetrov wants to merge 71 commits into
intel:mainfrom
napetrov:fix/answers-stack-p-step2

Conversation

@napetrov

@napetrov napetrov commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Step 2 of #228, "Fix Stack P in place". Stack P (EvaluationPipeline + Answerer) gets five fixes and one stamp, one commit each. Each fix flips the rows it owns in the step-1 characterization table (tests/test_answering_stack_parity.py, PINNED), so the diff of this PR shows the change in numbers. No step 3 work is in this PR. eval/arms_run.py, commands/arms.py and commands/subjects.py are not touched (PR #270 owns them).

Which scores move, and why

These are the parity-table values, measured on the six-question fixture. No live benchmark was run for this PR; the size and direction of the live shift are not measured here.

Difference What changes Row (P, before to after)
D1 top_k P asks the doc source for top_k docs (was the default 5). Retrieved context is smaller, so with-docs answers and their judge inputs change. retrieval.max_results 5 to 3; retrieval.passed ("max_tokens",) to ("max_results", "max_tokens")
D2/D3 placeholder With no docs (D2) or a raising reranker (D3), P no longer judges the placeholder with-docs answer "[FALLBACK: No relevant documentation found]". It is recorded, not scored. judge.calls 12 to 10; judge.contexts.q-empty and q-rerank lose the placeholder with-docs context; delta.per_question q-empty and q-rerank 0.0 to None
D9 statistics Follows from D2/D3: P has 4 paired questions, below MIN_PAIRS_FOR_TEST (6). P gets no significance test. stats.delta_mean 13.33 to 20.0 (4 pairs, was 6); significance.n 6 to None; significance.significant False to None
D8 library id With no context7_id, the pipeline resolves the id through the registry before the client. library_id client/resolved to registry/lib
D6 judge temperature benchmark run gains --judge-temperature. The default is still the provider default, so no default score moves. cli.judge_temperature_flag False to True
stamp method_version: 2 in the answers artifact and in the eval artifact of the single Judge. No score moves. artifact.method_version 2 (P), None (A)

Direction of the live shift, from the D2/D3 change: the placeholder answer is scored near zero by a judge, so dropping those rows raises the with-docs average and the delta on any live run with retrieval misses. Old and new runs of the same product are therefore not comparable on those numbers. compare does not read method_version yet, so it will not refuse the mix (out of scope, below).

Default behaviour that changes live runs: benchmark run, orchestrate evaluate and eval score no longer judge a placeholder with-docs answer. Pass --judge-placeholder to restore the old judging.

An eval artifact made by eval score on answers from before this PR is stamped method_version: 1, the version of the answers it judges. The stamp records the judging method; the answers artifact keeps its own stamp (absent, so version 1). Read the two together; a single eval stamp does not say which answers it judged.

Trust gate: numbers, thresholds not changed

Thresholds are unchanged: min_retrieval_hit_rate 0.60, min_retrieval_coverage 0.80, min_questions 10, max_zero_fraction 0.20, and the rest of DEFAULTS.

What changes is the input of the checks:

  1. min_questions counts the both-arm-scored questions. Placeholder questions (no document, not judged by default) drop out of that count. Fewer counted questions makes the minimum harder to reach, so this is a stricter number change, not a weakening. On the 4-of-14 fixture the count goes from 14 to 10, which still passes at min 10.
  2. zero_score_fraction (fail) counts a question whose retrieval missed as unusable, in the numerator and the denominator. On main a miss was judged at 0 and counted as a zero, so this keeps the fail-severity check failing on the same run. The message says "scored 0 or had no document". Commit c517c727. Regression test: tests/test_trust_gate_unjudged_misses.py fails on the parent commit (4 of 14 passes at 0.0) and passes here.
  3. min_with_docs_avg, min_delta and score_cv (warn) run over the both-arm-scored subset only. The placeholder zeros that main counted are gone from that subset, so these three checks are more lenient than on main. Not changed here; see the decision below.
  4. Retrieval hit rate (warn) runs over every eval record. A question with no document is in the denominator as a miss. Before this PR it was in the rate only when judged. Under default flags the placeholder misses were judged before and counted already, so the net change is small.

Thresholds are not changed, and no retrieval check was added with fail severity.

Probe (probes/gate_probe.py in the review scratch; 14 both-arm questions, mock judge scores a FALLBACK answer 0, otherwise 60; re-run on the head after the fix):

Fixture Check Base (intel/main) Head (this PR)
4 of 14 miss (29%) min_questions (fail, min 10) 14, pass 10, pass
4 of 14 miss zero_score_fraction (fail, max 0.20) FAIL, 0.286 FAIL, 0.286
4 of 14 miss retrieval_hit_rate (warn, min 0.60) 0.714, pass 0.714, pass
14 of 14 miss min_questions (fail, min 10) 14, pass; zero FAIL 1.0; retrieval FAIL 0.0 0, FAIL; early return, no zero check; retrieval_hit_rate 0.0, FAIL
all hit all checks unchanged unchanged

Net effect: the fail-severity zero-score check gives the same answer on the same run as main (FAIL at 0.286). A run where 29% of questions get no document still fails the gate. The retrieval miss shows as a warn-severity rate that passes at 71%, as before. A run where every question misses still fails, through min_questions, and has no retrieval line.

Adding a fail-severity retrieval check, or changing its threshold, is a separate design decision. It is not in this PR.

Changes

  • fix(answers): the doc source is asked for top_k docs (was the default). Dead code deleted in the context arm.
  • fix(benchmark): benchmark run --judge-temperature is parsed and passed to the judge. Batch is not changed.
  • fix(pipeline): the library id goes through resolve_library_id_for_product (explicit id, then registry context7_id, then client).
  • fix(judge): the no-retrieval placeholder is not judged by default; --judge-placeholder on benchmark run, orchestrate evaluate and eval score restores it. Trust gate counts all records for retrieval (see above).
  • fix(gate): zero_score_fraction counts retrieval misses as unusable, so the fail-severity check matches main on the 4-of-14 fixture (commit c517c727, regression test tests/test_trust_gate_unjudged_misses.py). Commit 6bec8910 updates the methodology bullet and parses the evaluations once.
  • refactor(answerer): removed the unused self.llm client, the unreachable if not docs: branch, and their imports.
  • feat(artifacts): method_version (defaults.ANSWERING_METHOD_VERSION = 2) on the answers artifact and the eval artifact of the single Judge; documented in answers.v1.json and eval.v1.json. Artifacts without it read as version 1. Nothing reads it yet.
  • test(parity), test(answerer), test(answerer): lint fix (missing List import); the init tests that only read back attributes replaced by a provider allow-list smoke test (flagged by the test-quality audit); a direct test that top_k reaches the doc source.
  • docs: methodology trust-gate list says which checks run on both-arm-scored questions; the method_version comment says nothing reads it yet.
  • Changelog fragment changelog.d/20261028_120000_napetrov_stack-p-step2.md.

The flag registry (dokimi/cli_flags.py) does not exist on main, so the new flags were added to the parsers directly.

Evidence

  • Parity table: each fix commit flips its rows; the table is the red-then-green record for the numbers above. Red runs for the per-fix commits are in the author's scratch directory, not in this PR.
  • The new top_k test: fails on intel/main (the probe sends no max_results), passes on the head.
  • Full suite on the head (6bec8910; the full run was on 3685c29f, and the last commit only changes one comment line, with the targeted gate and docs tests re-run): 9703 passed, 6 skipped.
  • Regression test tests/test_trust_gate_unjudged_misses.py: fails on the parent commit 0fe7452d (value 0.0, expected 0.286), passes on the head.
  • ruff check .: clean. ruff format --check reports 482 files that would be reformatted; the project does not gate on ruff format, so this is pre-existing and not changed here.
  • Self-review (pr-review, local mode): candidates reviewed in review.md of the review scratch.

Merge Danger

Checked by trial merge against intel/main (no rebase needed for this branch: it contains intel/main).

Both #250 and #255 are ours and the coordinator owns those conflicts; this PR does not resolve them.

Out of scope (listed, not changed here)

  • benchmark batch has no --judge-temperature.
  • compare does not read method_version yet, so it does not refuse mixed versions.
  • eval panel-score (JudgePanel) still judges the placeholder answer and writes its artifact without method_version. The panel's trust numbers therefore differ from the single judge's on the same answers.
  • A registered product without context7_id now raises ValueError for clients that require a declared id, in question generation and answer generation (the registry-first lookup). Intended, but a hard stop where the client used to be asked.
  • Trust-gate decisions for the maintainers: (a) the warn-severity score checks (min_with_docs_avg, min_delta, score_cv) run over the both-arm subset, so they are more lenient than on main; (b) a fail-severity retrieval check is not added (retrieval_hit_rate stays warn). Neither is changed here.

Refs #228

🤖 Generated with Claude Code

…tep 2)

The answerer stored top_k and never sent it, so retrieval asked the doc source for its default max_results. It now sends max_results=top_k, the same request DocTreatment makes on Stack A. With the default top_k of 3 the reranker sees 3 raw docs, not 5, so with-docs answers and their judge scores move.

Flips the PINNED rows D1 retrieval.passed and D1 retrieval.max_results. Test fakes that take the doc-source call now accept max_results.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ntel#228 step 2)

benchmark run had no judge temperature flag, so its judge always ran at the provider default while evaluate and arms run took one. The flag is added (default unset, so runs without it are unchanged) and reaches the EvaluationPipeline judge.

Flips the PINNED row D6 cli.judge_temperature_flag. benchmark batch does not get the flag yet.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…#228)

EvaluationPipeline took the library id from --context7-id or the client's
own resolver, so a product whose products.yaml declares context7_id was
still looked up by its registry key. Resolve it with the shared
resolve_library_id_for_product helper, the same order the arms stack uses:
explicit id, then the registry's context7_id, then the client.

Question generation and answer generation both go through the helper.
The registry is injectable (registry=) and defaults to the bundled
products.yaml. Parity row D8 flips from client/resolved to registry/lib.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…el#228)

When no document reached the prompt (no hit, or the reranker raised), the
answerer records "[FALLBACK: No relevant documentation found]" as the
with-docs answer, and the judge scored that text like a model answer. It
inflated the with-docs side of the paired delta on questions the docs did
not answer.

The judge now leaves a context answer whose doc_source is a no-retrieval
source (NO_RETRIEVAL_SOURCES) unjudged, so its with-docs score and delta
are None and the question drops out of the paired statistics. The record
and its retrieval_hit stay. Opt back in with judge_placeholder=True, or
--judge-placeholder on benchmark run, orchestrate evaluate and eval score.

The trust gate's retrieval_hit_rate read only the both-arms-scored
evaluations. Unjudged misses would then vanish from it and an all-miss run
would read as "not measured". It now counts every evaluation's
retrieval_hit. The thresholds are unchanged.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Answerer built a self.llm client in __init__ that nothing read: the answer
call goes through llm_call_with_usage, which picks the provider's client
itself. Only the unsupported-provider ValueError is kept; the client
construction and its imports are gone.

_generate_with_docs had an "if not docs" branch that could not run, because
_generate_answer_pair calls it only when retrieval returned documents. The
branch is deleted with its unreachable "[No documentation retrieved]" record.

No parity row changes: nothing observable on either stack moved.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…intel#228)

The Stack P answers artifact and the judge's evaluation output now carry
method_version, from defaults.ANSWERING_METHOD_VERSION (2). The steps
before this one are version 1 and have no stamp, so a later compare can
refuse to put runs of different versions side by side. compare does not
read the stamp yet.

answers.v1 and eval.v1 document the field. The arms artifact is not
stamped in this change; the new parity row records that.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
ruff F821 flagged the annotation on _paired_deltas. The module uses
`from __future__ import annotations`, so the missing name did not fail at
import time and the parity tests passed without it.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…set-then-read tests (intel#228)

With the llm construction removed from Answerer.__init__, test_init_openai and
test_init_anthropic only set attributes and read them back, which the test
quality audit flags (set_then_read). The new parametrized test checks what
init does now: each supported provider is accepted. It is marked as a
must-not-raise smoke test.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
)

test_calls_mcp_client builds the Answerer with the default top_k, so a literal
max_results=5 would pass it. The new test builds it with top_k=3 and asserts
the doc source is asked for 3. On intel/main the same probe sends no
max_results at all, so the test fails there.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ons (intel#228)

The methodology list and the method_version comment described the trust gate
and the stamp more broadly than the code does. The score checks run over the
both-arm-scored questions, so a question with no document leaves them. Nothing
reads method_version yet; the comment now says so.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…fraction (intel#228)

A question whose retrieval missed is no longer judged with docs, so it has
no context score. The zero-score check counted only scored questions, so a
run where most questions miss passed it: 4 misses of 14 gave 0.0 PASS, where
intel/main gave 0.286 FAIL (the misses were judged at zero there).

The check now counts a miss (no context score, a baseline score, and
retrieval_hit False) as unusable, in the numerator and the denominator. The
message says such rows had no document. Thresholds, min_questions and the
early return are unchanged. The new fail-severity retrieval check is not
added here.

Regression test: tests/test_trust_gate_unjudged_misses.py. It fails on the
parent commit (4 of 14 passes at 0.0) and passes with this change.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ons once (intel#228)

The methodology bullet still said the zero-score fraction leaves out
questions with no document. It counts them now (c517c72). Update it.

Also parse the evaluations once in evaluate_trust. The input may be a
one-shot iterable, and the old code parsed it again for the retrieval check.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@codecov-commenter

codecov-commenter commented Oct 29, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.09%. Comparing base (b4a0a8c) to head (7e57031).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #274      +/-   ##
==========================================
+ Coverage   94.05%   94.09%   +0.04%     
==========================================
  Files         297      297              
  Lines       38018    38084      +66     
  Branches     6424     6432       +8     
==========================================
+ Hits        35756    35834      +78     
+ Misses       1452     1439      -13     
- Partials      810      811       +1     
Files with missing lines Coverage Δ
dokimi/dokimi/commands/answers.py 95.37% <100.00%> (+0.13%) ⬆️
dokimi/dokimi/commands/benchmark.py 91.18% <100.00%> (+1.39%) ⬆️
dokimi/dokimi/commands/evaluate.py 90.11% <100.00%> (+0.18%) ⬆️
dokimi/dokimi/commands/orchestrate.py 86.25% <100.00%> (+0.17%) ⬆️
dokimi/dokimi/dashboard/aggregator.py 93.79% <100.00%> (+0.17%) ⬆️
dokimi/dokimi/defaults.py 100.00% <100.00%> (ø)
dokimi/dokimi/eval/answerer.py 95.02% <100.00%> (-0.31%) ⬇️
dokimi/dokimi/eval/diagnoser.py 100.00% <100.00%> (ø)
dokimi/dokimi/eval/judge.py 94.33% <100.00%> (+0.11%) ⬆️
dokimi/dokimi/gate/trust_gate.py 100.00% <100.00%> (ø)
... and 2 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot added the cli-interface-change Changes dokimi's user-facing CLI surface label Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

CLI interface change detected

This PR changes dokimi's user-facing CLI surface (commands and/or options):

~ `dokimi answers generate` parameters changed:
    ~ changed option `--top-k`: type: 'int' -> 'dokimi.cli.build_parser.<locals>.positive_int#faf57db5ff9f'
~ `dokimi benchmark run` parameters changed:
    + added option `--judge-placeholder`
    + added option `--judge-temperature`
~ `dokimi eval score` parameters changed:
    + added option `--judge-placeholder`
~ `dokimi evaluate` parameters changed:
    + added option `--judge-placeholder`

Updated on every push; the cli-interface-change label is on while the diff is non-empty.

@vlad-perevezentsev vlad-perevezentsev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at 6bec891. The five Stack P fixes do what step 2 of #228 asks, and the zero-score check keeps failing the same runs as main. Not judging the placeholder has side effects outside the gate's score checks (the retrieval coverage, the diagnosis and the pipeline summary), and the registry lookup misses a custom --registry. These are in the line comments.

What I ran, at 6bec891:

  • make ci-local --offline: 0 failed, 17 passed, 13 skipped (network and Docker jobs).
  • Trust gate with 15 judged rows and 5 judge-error rows, on the base and on the head: the retrieval check goes from 100% (15/15) to "not measured".
  • summarise_diagnoses on 10 hits and 4 misses judged through _evaluate_answer_pair, on the base and on the head: empty_retrieval 4 becomes 0 and insufficient_data 0 becomes 4.
  • resolve_library_id_for_product("my_lib", <Context7 client>) returns my_lib unchanged.

Checked, no issues

  • top_k: Answerer sends max_results=top_k, as DocTreatment does. evaluate and answers generate pass their --top-k, and the new test pins the value at the call site.
  • Zero-score check: unjudged misses count in both the numerator and the denominator, so 4 of 14 misses still fail at 0.286, as on main. The new test fails on the parent commit.
  • Judge temperature: benchmark run --judge-temperature reaches the Judge in single and multi-run mode.
  • Dead code: Answerer.llm was never read, and _generate_with_docs is only called with docs. The provider allow-list keeps the same set.
  • Schemas: method_version is an optional new property in answers.v1 and eval.v1. That widens the schemas, and old files still validate.

Comment thread dokimi/dokimi/gate/trust_gate.py Resolved
Comment thread dokimi/dokimi/eval/judge.py Resolved
Comment thread dokimi/dokimi/orchestrator/pipeline.py Resolved
Comment thread dokimi/dokimi/orchestrator/pipeline.py Resolved
Comment thread dokimi/dokimi/eval/judge.py Resolved
Comment thread dokimi/tests/test_trust_gate.py Resolved

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at 6bec891. The five Stack P fixes do what step 2 of #228 asks, and the zero-score check keeps failing the same runs as the base. Three things are worth fixing before merge, all test gaps on the new code (inline). The open problems from @vlad-perevezentsev's review still hold at this head and block the merge.

I agree with @vlad-perevezentsev on the judge-error rows losing the retrieval rate, the insufficient_data diagnosis of misses, the registry lookup in benchmark run --registry, the summary averages over different questions, the unstamped --judge-placeholder runs, the 0-hit gate test and the changelog flag names. New evidence: 10 hits and 4 misses move min_delta from -17.1 (fail) to +0.0 (pass).

What I ran, on a Linux review host at 6bec891:

  • make ci-local: 27 passed, 1 failed, 7 skipped. The failure is pip-audit, which lists advisories on filelock in the lock files. This PR does not touch them.
  • 9,705 tests passed, 4 skipped; the changed-line coverage step passes.
  • Reproductions for the code findings, with the LLM transport mocked.
  • A mutation run on the new code (33 mutants, 13 of which pass the related tests): _library_id, the unjudged_misses filter and the flag wiring in benchmark run, evaluate and eval score each have mutants that pass every test.

Checked, no issues

  • Schemas: method_version is an optional property on an open object in answers.v1.json and eval.v1.json. schema_compat against the base passes, and no version bump is needed.
  • top_k: every doc-source client's get_library_docs accepts max_results, so the new argument raises nowhere.
  • Dead code: no caller of Answerer.llm or of _generate_with_docs with empty docs remains.
  • Zero-score check: 10 hits and 4 misses give 4/14 = 0.286 and fail, as on the base. Judge-error rows are excluded from the new term.
  • Commits: all 12 carry Signed-off-by; the changelog fragment is present.

Comment thread dokimi/dokimi/gate/trust_gate.py Resolved
Comment thread dokimi/dokimi/orchestrator/pipeline.py Resolved
Comment thread dokimi/dokimi/commands/benchmark.py Resolved
@vchamarthi

vchamarthi commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 4 issue(s) in this PR.

By severity: 4 low

Severity Category Location Finding
low test test_pipeline_summary.py:87 Nit: no test covers the all-paired case.
low test test_pipeline_library_id.py:56 Nit: _RecordingClient.asked exists to record the fallback, but no test reaches it.
low documentation 20261028_120000_napetrov_stack-p-step2.md:7 benchmark run had no --judge-temperature flag before this change (the diff adds it to the parser).
low test test_diagnoser.py:56 Nit: this covers only the fallback_none happy path.
Review details
  • Commit reviewed: 284b240 against merge base f0f95a2
  • Coverage: 25 files in 6 groups
  • Engine: OpenCodeReview v1.13.3, us.anthropic.claude-sonnet-5-5
  • Usage: 1.04M tokens (785k from cache), 51s
  • Tool calls: 53 (33 code_search, 14 file_read, 3 code_comment, 3 file_read_diff)
  • Changelog and methodology docs: 2 files
  • Benchmark command, defaults, and registry wiring: 3 files
  • Evaluate/orchestrate commands and pipeline library_id: 6 files
  • Answerer refactor, answering stack parity, and schemas: 6 files
  • Judge and diagnoser changes with placeholder tests: 5 files
  • Trust gate and unjudged misses tests: 3 files

…rs-stack-p-step2

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

# Conflicts:
#	dokimi/dokimi/orchestrator/pipeline.py
The retrieval coverage floor counted every judge-error row as an unrecorded
outcome, so a run with more than 20% judge errors lost the retrieval check.

Thread: PRRT_kwDOVBg7g86xgwoY
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
With the no-retrieval placeholder left unjudged its delta is None, and
diagnose() returned insufficient_data before it looked at the doc source.

Thread: PRRT_kwDOVBg7g86xgwog
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Same finding as the diagnoser.py thread, fixed in a104154.

@napetrov

Copy link
Copy Markdown
Contributor Author

The unjudged-miss clauses are pinned on head: each one, when broken, fails test_trust_gate.py or test_trust_gate_unjudged_misses.py (d33a895).

@napetrov

Copy link
Copy Markdown
Contributor Author

The three nit commits it lists are on the branch: d33a895.

@napetrov

Copy link
Copy Markdown
Contributor Author

The unjudged_misses test gap at trust_gate.py:202 is closed on head (d33a895).

… count

Eval files written before error rows carried retrieval_hit turned a measured
hit rate into "Not measured" (and a failing rate into a pass) once the
denominator became every evaluation. Such a row now counts as neither
measured nor unrecorded, as before; new error rows still count.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
top_k is sent as max_results, so answers generate --top-k 0 (or a config
retrieval.top_k of 0, "3" or true) made every answer an unjudged placeholder
and still exited 0. The flag takes positive_int, as dokimi evaluate does, and
a bad config value exits 2 before any question is answered.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
eval score and the pipeline copied the answers' method_version into the eval
unchecked: "2", true or 3.5 failed eval validation after judge calls were
paid for and left no file, 0 became 1 and -7 went through. A stamp that is
not a positive int now exits 2 (eval score) before any judge call.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
The library id now comes from the registry for local:, url: and okf:
sources as well, and .cache/topics was keyed on that id alone, so a second
doc set of one registered product silently got the first set's topics.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 31, 2026

Copy link
Copy Markdown
Contributor
  • Ran: full suite, ruff, mypy, bandit and the benchmark gate on this head with the commits below. All green.
  • dashboard generate, 14 questions, 2 retrieval hits, misses unjudged (the default) -> doc_score 80.0, status good; with 0 hits -> doc_score = baseline 60.0, status fair -> expected: misses lower the score (before this PR: 11.4 and 0.0, both poor). Suggested fix: in _load_snapshot, score a no-error retrieval miss that has a baseline score as context 0.0. report eval at 0 hits prints Overall delta: +0.0 over 0 pairs; it should say there are no complete pairs.
  • The PR body says re-judged legacy answers are stamped method_version: 2 -> eval score writes 1. The body's 14-of-14 row says "no retrieval check" -> the gate emits retrieval_hit_rate 0.0 FAIL. Body edit only.
  • Pushed:
    • a6a74c4: stored judge-error rows without retrieval_hit no longer turn a measured (or failing) hit rate into "Not measured".
    • 3d7f8c5: answers generate --top-k 0 or a bad retrieval.top_k in the config exits 2 instead of producing an all-placeholder run.
    • f34aa62: a malformed answers method_version exits 2 before any judge call, instead of crashing after the calls are paid for.
    • b945e44: the topics cache is keyed on the doc source, so a second local: doc set of one product no longer reuses the first set's topics.
  • Left for the author: the dashboard/report treatment above and the body edit.

Conflict in orchestrator/pipeline.py imports only: keep intel#274's resolve_library_id_for_product and intel#273's same_evaluator.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Merged current main (63c9cdb, after #273 and #301) into this branch as 171a022. The only conflict was the import block of orchestrator/pipeline.py: kept both this PR's resolve_library_id_for_product and #273's same_evaluator. Full suite, ruff, mypy, bandit and the strict benchmark run pass.

Comment on lines +34 to +35
of one version that differ in it are told apart. A `method_version` that is not a positive
integer is refused before any judge call (`eval score` exits 2).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

documentation · low
Nit: method_version: null is accepted as legacy version 1, not refused. answers_method_version() explicitly handles None, and test_a_null_stamp_is_legacy confirms that eval score proceeds. Qualify this claim so it matches the accepted input: an absent or null stamp means version 1; any other value must be a positive integer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0f8835a: an absent or null stamp is version 1; any other value must be a positive integer, or eval score exits 2.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in fork commit 0f8835aa (PR head 7e57031d). The changelog now says: "An absent or null stamp means version 1; any other value must be a positive integer, or eval score exits 2 before any judge call."

Checked against the code at 7e57031d: answers_method_version() returns 1 for an absent or None stamp, and refuses any other non-positive-integer value with a ValueError, which eval score turns into exit 2 before the Judge is built. test_a_null_stamp_is_legacy covers the null case.

Comment thread dokimi/docs/methodology.md Outdated
- minimum `with_docs` average,
- average delta,
- score coefficient of variation,
- minimum evaluated question count (both arms scored; a question with no document is not judged by default, so it is not counted here),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

documentation · low
Nit: qualify the no-document exclusion as the single Judge default. For example, eval panel-score on an answer with doc_source="fallback_none" and a nonempty [FALLBACK: ...] text still scores both arms: JudgePanel._score_answer_entry() only skips missing or empty answers. That row therefore counts toward the trust gate's minimum question count. This page also covers panels, so the current unqualified statement incorrectly describes their behavior.

Suggestion:

Suggested change
- minimum evaluated question count (both arms scored; a question with no document is not judged by default, so it is not counted here),
- minimum evaluated question count (both arms scored; the single judge leaves no-document context placeholders unscored by default, so those questions are not counted here; panel scoring still judges them),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 17d5b40: the single Judge leaves out a question with no document; JudgePanel still judges an answer that is only fallback text, and the line says so.

Comment thread dokimi/docs/methodology.md Outdated
- average delta,
- score coefficient of variation,
- minimum evaluated question count (both arms scored; a question with no document is not judged by default, so it is not counted here),
- zero-score fraction (computed when at least one question is scored in both arms; over those paired questions plus retrieval misses with a scored baseline or a judge error, which count as unusable),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

documentation · low
Nit: only retrieval misses with an unscored context arm are added as unusable. With --judge-placeholder, a miss scored 70 in the context arm and 60 in the baseline contributes no zero, whereas this wording says retrieval misses with a scored baseline count as unusable. evaluate_trust explicitly requires r.score(CONTEXT) is None for the extra rows. Qualify the added misses as unscored to describe the denominator and numerator accurately.

Suggestion:

Suggested change
- zero-score fraction (computed when at least one question is scored in both arms; over those paired questions plus retrieval misses with a scored baseline or a judge error, which count as unusable),
- zero-score fraction (computed when at least one question is scored in both arms; over those paired questions plus retrieval misses with an unscored context arm and either a scored baseline or a judge error, which count as unusable),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 17d5b40: the zero-score rows are only retrieval misses whose context arm is unscored (with a scored baseline or a judge error).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in fork commit 17d5b405 (PR head 7e57031d). The zero-score bullet now counts "retrieval misses with an unscored context arm and either a scored baseline or a judge error, which count as unusable", and nothing else is added.

Checked against evaluate_trust at 7e57031d: unjudged_misses takes rows with a None context score, retrieval_hit is False, and either a baseline score or an error. A miss whose context arm is scored is not added.

@napetrov

Copy link
Copy Markdown
Contributor Author

Mapping of the items in your 10-31 06:49Z comment to commits on this head (synced with main b4a0a8c in 6b5ca65):

  • Dashboard: a no-error retrieval miss with a baseline score counts as context 0.0, so 0 hits no longer shows the baseline as the doc score (52a6064).
  • report eval with no complete pair: no delta printed, and it says there are no complete pairs (e6eb131, c6d6c1f).
  • PR body: method_version: 1 for re-judged legacy answers, and the 14-of-14 row shows retrieval_hit_rate 0.0 FAIL (body edit; bf0f032 is an empty marker commit).
  • exai nits: changelog stamp wording (0f8835a), methodology lines 202 and 203 (17d5b40).

Local: report and dashboard tests 1118 passed; ci-local static and typecheck clean.

…rs-stack-p-step2

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A question the context arm never answered, with no retrieval hit and a baseline score,
counted as no score, so the snapshot averaged the baseline alone (0 hits gave doc_score
equal to the baseline). It now counts as 0.0 for the context arm.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.a
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
With no question scored in both arms, report eval printed a 0.0 arm mean, an
Overall delta of +0.0 and a delta over 0 pairs. The Summary cells and the Overall delta
line now show an em dash and a note that there are no complete pairs.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.b
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The changelog said a non-positive-integer stamp is refused; a null or absent one is the
legacy version 1 and is accepted.

Thread: PRRT_kwDOVBg7g86yPRma
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The minimum question count excludes an unjudged question only for the single Judge; a
panel judges a fallback-only answer. The zero-score denominator adds retrieval misses with
an unscored context arm and either a scored baseline or a judge error, and no other miss.

Thread: PRRT_kwDOVBg7g86yPRmb
Thread: PRRT_kwDOVBg7g86yPRmc
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
No code change. The body edit (line 22: method_version 1 for an eval of legacy answers;
the 14-of-14 row: retrieval_hit_rate 0.0 FAIL) is applied to the PR description.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.c
Thread: IC_kwDOVBg7g88AAAABhdSHJQ.d
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
With no complete pair, the note under the Summary still said the means were over 0
complete pairs. It now says there are no complete pairs and that no arm mean or delta is
given.

Thread: IC_kwDOVBg7g88AAAABhdSHJQ.b
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/dashboard/aggregator.py Outdated
without_score = _as_float(ev.score(BASELINE))
if (with_score is None and ev.retrieval_hit is False and without_score is not None
and ev.error is None and (with_s is None or with_s.error is None)):
with_score = 0.0 # a retrieval miss the context arm never answered: the docs did not help

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: this also turns an unjudged miss into a complete pair in _arm_means, changing the reported A/B delta, not just the documentation score/status. With the new test's inputs (one actual pair at 80/60 and one unjudged miss with baseline 60), the dashboard now reports delta -20, while both eval reports report +20 over the sole judged pair; the miss's own QuestionResult.delta remains None. An all-miss run similarly acquires a -60 delta despite having no scored pairs. Keep the zero penalty separate for the documentation health score, and compute the displayed arm means/delta from actual scored pairs so an unjudged placeholder does not become a measured degradation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed, and it also makes the dashboard and report eval disagree on the same eval file, which #286 had just aligned.

Repro at 7e57031, one eval file with one pair (80/60) and one unjudged miss (baseline 60): ResultsAggregator gives context 40.0, baseline 60.0, delta -20.0; report eval on the same file gives | **All** | 2 (1 unpaired) | 80.0 | 60.0 | **+20.0** | and "context arm is helping". The changelog says an unjudged miss "drops out of the paired statistics" and does not mention the dashboard, so a reader of either output gets the wrong idea about the other.

Suggestion: keep the 0.0 only for doc_score/status (the doc health number, as @xaleryb asked), compute avg_with_docs/avg_without_docs/avg_delta from the real pairs as before, and add a changelog line for the dashboard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 60f5b09. The arm means and delta now leave an unjudged miss out, since it is no judged measurement. The dashboard matches report eval on the same file (+20 over the one complete pair). The doc score and status still count the miss as 0.
Tests: the miss case in test_dashboard.py (red on the old code), and an all-miss run now has no delta. The changelog says so too.

if not s["unpaired"]:
return []
if not s["valid"]:
return [f'_No complete pairs: {s["unpaired"]} unpaired questions, so no arm mean or delta is given._', ""]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: the no-pairs handling only updates the Summary and overall recommendation. With the new test's two unpaired questions, the same report still prints context/baseline 0.0 and delta +0.0 in the Dynamic vs Static breakdown (including Total), and claims “+0.0 vs +0.0 — good consistency, benchmark is stable.” Difficulty rows do the same when a difficulty is present. These pre-existing zero fallbacks now contradict this new statement and the Summary's em dashes. Apply the no-pairs rendering to both breakdown tables and guard the agreement recommendation when either set lacks pairs; extend the regression test to cover these outputs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ad1f45e. The Dynamic vs Static and difficulty rows print em dashes for a set with no complete pair, as the Summary does. The agreement bullet says there is no delta, instead of "good consistency".
Regression test: test_breakdown_tables_and_agreement_say_no_pair_instead_of_a_zero_delta (red on the old code).

…atchet

The miss-as-zero rule and the no-pairs lines pushed _load_snapshot over the
cap of 10 and generate_report past its baseline of 18. Move them into
_context_score, _overall_line and _delta_summary; no behavior change.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@vlad-perevezentsev vlad-perevezentsev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at 7e57031. Everything from my first review is fixed and pinned by tests. The new dashboard change scores an unjudged miss as 0, which also moves the dashboard's arm means and delta. So the dashboard and report eval now disagree on the same eval file. I added a repro in @exai-bot's thread.

Agreed with @exai-bot: the dashboard's arm means and delta now count unjudged misses; with no complete pairs, the report's per-set tables and the agreement line still print +0.0.

What I ran, at 7e57031:

  • make ci-local --offline: 0 failed, 19 passed, 13 skipped (network and Docker jobs). The suite had 12,042 passed and 58 skipped. The branch is 0 commits behind main.
  • One eval file with one pair (80/60) and one unjudged miss (baseline 60). The dashboard shows context 40.0, baseline 60.0, delta -20.0. report eval shows 80.0, 60.0, +20.0.
  • Earlier rounds: mutation runs on the gate filter, the top_k override and the diagnoser branch. Each mutant is now killed.

Checked, no issues

  • Trust gate: in zero_score_fraction, misses with an unscored context arm count as unusable, and a judge-error miss counts too. Judge-error rows keep retrieval_hit. An old error row without that field stays out of the coverage count, after a6a74c4. With no pair, the gate still shows the retrieval rate.
  • Diagnosis: an unjudged miss is empty_retrieval. A miss judged with --judge-placeholder that has no baseline is insufficient_data.
  • Stamps: eval score and the pipeline stamp the eval with the answers' method_version: an absent stamp counts as 1, and a malformed one exits 2 before any judge call (21b3054, f34aa62). judge_placeholder is stamped too.
  • Retrieval: top_k reaches the doc source, and a value below 1 is refused (3d7f8c5). The library id comes from --context7-id, then the mcp: ;id=, then the loaded --registry. The topics cache is keyed on the doc source (b945e44).
  • Summaries: the run summary and report eval average the complete pairs and say how many questions were left out. With no pair they print no delta.
  • Changelog and docs: the fragment names only flags and commands that exist, and says old scores are not comparable. methodology.md gives the subset each gate check uses.

A no-error retrieval miss scores 0 as the context arm, so the doc score
counts it, but it is no judged measurement: pairing it with its baseline
made the dashboard report a -20 delta where the eval report gave +20 over
the same file. The arm means and delta are now over the measured pairs;
the doc health (doc_score, status) still counts the miss as 0.

Thread: PRRT_kwDOVBg7g86yUBEO
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A set with no complete pair printed 0.0 for its context, baseline and delta
in the Dynamic vs Static and difficulty tables, and the agreement bullet
claimed consistency. Those rows now print an em dash like the Summary, and
the agreement bullet says there is no delta when either set has no pair.

Thread: PRRT_kwDOVBg7g86yUBES
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Copilot AI balanced review requested due to automatic review settings November 1, 2026 05:11

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

Skipping placeholders exposes a critical all-miss reporting bug that presents zero-valued statistics and concludes minimal impact despite having no paired measurements.

1 open issue
What changed in this PR

1 new must-fix finding.

This PR implements step 2 of #228, correcting Stack P’s answering and judging behavior before consolidating the two evaluation stacks.

Changes:

  • Honor retrieval limits, resolve library IDs through the registry, and wire judge controls through CLI entry points.
  • Leave placeholders unjudged by default, preserve retrieval misses in trust checks, and stamp artifacts with method metadata.
  • Update reports, dashboard calculations, documentation, and regression tests for unpaired results.
File Description
dokimi/​tests/​test_trust_gate.py Covers retrieval rates and revised gate messages.
dokimi/​tests/​test_trust_gate_unjudged_misses.py Tests missed retrievals and judge-error handling.
dokimi/​tests/​test_report_arm_agreement.py Tests report sections without complete pairs.
dokimi/​tests/​test_provider_call_budgets.py Updates the retrieval stub signature.
dokimi/​tests/​test_pipeline_summary.py Checks paired and excluded question counts.
dokimi/​tests/​test_pipeline_library_id.py Tests library resolution and topic-cache isolation.
dokimi/​tests/​test_llm_cli.py Checks early refusal of unsupported judge temperature.
dokimi/​tests/​test_judge_placeholder_entry_points.py Tests placeholder flags through command handlers.
dokimi/​tests/​test_eval_score_method_version.py Tests artifact version propagation and validation.
dokimi/​tests/​test_eval_answerer.py Tests provider acceptance and retrieval limits.
dokimi/​tests/​test_diagnoser.py Tests diagnoses for unjudged placeholders.
dokimi/​tests/​test_dashboard.py Checks paired averages and retrieval-miss health scores.
dokimi/​tests/​test_benchmark_registry_wiring.py Verifies custom registries reach benchmark pipelines.
dokimi/​tests/​test_answers_command.py Tests invalid retrieval-limit rejection.
dokimi/​tests/​test_answering_stack_parity.py Updates pinned Stack P behavior and scores.
dokimi/​tests/​test_answering_stack_p_step2.py Tests judge controls and artifact stamps.
dokimi/​tests/​data/​cli_flags_allowlist.txt Allows the placeholder-judging flag.
dokimi/​tests/​answering_stack_parity.py Passes registry and placeholder options to fixtures.
dokimi/​dokimi/​schemas/​eval.v1.json Declares method and placeholder-judging metadata.
dokimi/​dokimi/​schemas/​answers.v1.json Declares answering-method metadata.
dokimi/​dokimi/​report/​eval_report.py Renders unavailable results when no pairs exist.
dokimi/​dokimi/​orchestrator/​pipeline.py Integrates registry resolution, judge metadata, and pair counts.
dokimi/​dokimi/​gate/​trust_gate.py Includes retrieval misses in relevant trust checks.
dokimi/​dokimi/​eval/​judge.py Skips placeholders by default and preserves retrieval metadata.
dokimi/​dokimi/​eval/​diagnoser.py Classifies unjudged misses as empty retrievals.
dokimi/​dokimi/​eval/​answerer.py Honors retrieval limits, stamps outputs, and removes dead code.
dokimi/​dokimi/​defaults.py Defines method versions and stamp validation.
dokimi/​dokimi/​dashboard/​aggregator.py Separates documentation health from paired measurements.
dokimi/​dokimi/​commands/​orchestrate.py Exposes and forwards placeholder judging.
dokimi/​dokimi/​commands/​evaluate.py Validates answer versions and forwards judge options.
dokimi/​dokimi/​commands/​benchmark.py Wires judge controls, sampling checks, and registries.
dokimi/​dokimi/​commands/​answers.py Validates positive retrieval limits.
dokimi/​docs/​reference/​cli-interface.json Refreshes the CLI interface snapshot.
dokimi/​docs/​methodology.md Clarifies trust-check populations.
dokimi/​changelog.d/​20261028_120000_napetrov_stack-p-step2.md Documents behavior changes and compatibility limits.

Review effort: Balanced

Comment on lines +400 to +401
placeholder = context_answer is not None and context_answer.doc_source in NO_RETRIEVAL_SOURCES
if context_answer is not None and context_answer.text and (self.judge_placeholder or not placeholder):

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli-interface-change Changes dokimi's user-facing CLI surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants