Skip to content

feat(doctor): the reranker weights are verified against the published digest (#734, slice 3) - #972

Merged
github-actions[bot] merged 3 commits into
mainfrom
fix/734-reranker-identity
Nov 10, 2026
Merged

github-actions[bot] merged 3 commits into
mainfrom
fix/734-reranker-identity

Conversation

@cdeust

@cdeust cdeust commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

reranker_model_sha256 was recorded in both benchmark manifests and compared with nothing (docs/audits/model-identity-inventory-2026-11-06.md, "The reranker digest is recorded, never verified"). FlashRank downloads its ONNX file by name. A changed or corrupt file that still loads gave reranker_state: loaded and a different digest, and neither cortex doctor nor a manifest said that the bytes differed from the ones the published numbers were measured with. This PR compares the on-disk digest with the digest the committed results carry, and reports the result in cortex doctor and in both manifests.

  • mcp_server/core/reranker_identity.py (pure): classify_model_digest(sha256) returns absent, verified or mismatch against EXPECTED_MODEL_SHA256 = d3dd7b09…41d1. That value is the only digest among the 131 reranker_model_sha256 entries committed under benchmarks/results/. Its source is benchmarks/results/repro/20261014T232318Z/MANIFEST.json (reranker_state: loaded, git b9d4dd5).
  • core/reranker_model.py gains model_path_under(cache_dir), the existing path join made public, and model_identity(). core.reranker re-exports model_identity.
  • mcp_server/doctor_reranker.py adds a new optional check, Reranker weights identity, to the PostgreSQL and SQLite check lists. It only reads the cached file: it never downloads, deletes or loads a model. An absent or different file gives a warning, because recall still runs without the reranker. An unreadable file is reported, not raised.
  • benchmarks/lib/write_manifest.py and benchmarks/_repro.py record reranker_model_identity next to the digest (unresolved in the degraded arm).

Refs #734 (slice 3; the issue stays open). Remaining slices: one effective manifest returned by cortex doctor and the benchmarks; a digest check of the embedding snapshot files; a receipt of memory operations run with networking disabled.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing behavior)
  • Refactor (no functional change; rules/coding-standards.md compliance)
  • Documentation only
  • Audit-finding closure (cite the finding ID): model-identity inventory 2026-11-06, "The reranker digest is recorded, never verified"

Test plan

  • Related tests pass: pytest tests_py/core/test_reranker_identity.py tests_py/test_doctor_reranker.py tests_py/test_doctor.py tests_py/core/test_reranker.py tests_py/benchmarks/ gives 456 passed and 15 skipped. I did not run the full suite locally; CI runs it.
  • New tests: tests_py/core/test_reranker_identity.py (4 tests, one checks that the pin equals the digest of the cited manifest) and tests_py/test_doctor_reranker.py (5 tests: absent, verified, mismatch, unreadable, registered on both backends).
  • Mutation survival: inverting the comparison, dropping the case fold, or leaving the check out of one backend's list fails a test. A wrong pin fails the provenance test.
  • Manual check: CORTEX_RERANKER_OFFLINE=1 python -m mcp_server.doctor in this container prints [WARN] Reranker weights identity absent at …/flashrank/ms-marco-MiniLM-L-12-v2/flashrank-MiniLM-L-12-v2_Q.onnx.

Audit notes

  • Engineering review: ruff check and ruff format --check (0.17.3, the version in uv.lock) pass on the changed files. scripts/check_craftsmanship.py: OK. scripts/check_doc_claims.py: OK.
  • Genius review: not run.
  • Outstanding: the verified path is not tested on a real FlashRank download, because this container's proxy does not allow the model download and the tests use synthetic bytes. PostgreSQL runs are not verified. The CI of this PR had not started when the PR was opened.

Coding-standards compliance

  • §2.2 Layer dependency direction preserved: core/reranker_identity.py imports nothing outside the standard library, and the doctor composes core with infrastructure.
  • §3.2 No untyped dicts at new boundaries.
  • §4.1 mcp_server/doctor.py was already over 500 lines (691 before this PR, baseline debt). This PR adds 6 lines to it and puts the check in a new 61-line module.
  • §4.2 No function > 50 lines.
  • §4.4 No function with > 4 parameters.
  • §7 Local reasoning preserved.
  • §8 The pinned digest carries a # source: comment.
  • §9 No dead code, no TODOs.

Breaking changes

None. The manifests gain one key, and doctor gains one optional check that does not change the exit code.

Screenshots / logs

[WARN] Reranker weights identity   absent at /root/.cache/flashrank/ms-marco-MiniLM-L-12-v2/flashrank-MiniLM-L-12-v2_Q.onnx; recall runs without reranking

Reviewer checklist

https://claude.ai/code/session_018kyZYtnEGra9C5XdQUC4Xj


Generated by Claude Code

… digest (#734, slice 3)

The FlashRank ONNX sha256 was recorded in every benchmark manifest and
compared with nothing (docs/audits/model-identity-inventory-2026-11-06.md):
a changed or corrupt file that still loads gave reranker_state "loaded" and
no surface said the bytes differed.

- core/reranker_identity.py classifies a digest as absent, verified or
  mismatch against the sha256 every committed result carries (131 entries,
  one value, cited to benchmarks/results/repro/20261014T232318Z/MANIFEST.json).
- cortex doctor gains an optional "Reranker weights identity" check on both
  backends: it reads the cached file, never downloads, deletes or loads.
- Both benchmark manifests record reranker_model_identity next to the digest.

Refs #734

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYtnEGra9C5XdQUC4Xj
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T11:52:52.188099Z 3aa5320 New commits
🔒 Security Review ✅ Completed 2026-11-10T11:54:32.932972Z 3aa5320 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 2m 36s —— View job


Review 1 of 3 (ADR-1119) is done

None of the five reviews found a Blocker. The posted comment has three Notes, no action before merge.

Verdict: approve

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d25ef364a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread mcp_server/doctor_reranker.py Outdated
"CORTEX_RERANKER_OFFLINE"
)
_MISMATCH_FIX = (
"Delete {path} and let FlashRank download it again; results measured with "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove the model directory rather than only the ONNX file

When a cached model has a mismatching digest, following this fix deletes only the ONNX file but leaves its model directory intact. The installed FlashRank 0.2.10 implementation of Ranker._prepare_model_dir downloads only under if not self.model_dir.exists() (also documented in docs/adr/ADR-0244-mcp-server-core-reranker-model.md), so the next load skips downloading and fails on the missing weights. Cortex then disables reranking for the rest of that process. Recommend removing the model's cache directory, or explicitly reprovisioning its complete contents, so the suggested repair actually restores reranking.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 2df9d02. FlashRank 0.2.10 Ranker._prepare_model_dir downloads only under if not self.model_dir.exists(), so deleting the ONNX file alone would leave reranking disabled. Both hints now name the model directory: remove it on a mismatch, and remove it if present when the file is absent. The doctor tests assert the directory path.


Generated by Claude Code

@claude

claude Bot commented Nov 10, 2026

Copy link
Copy Markdown

Review 1 of 3 (ADR-1119) — PR #972, commit 9d25ef3

I ran five reviews: code quality, performance, test coverage, documentation accuracy and security. None of them found a Blocker.

What I checked:

  • Pinned digest. EXPECTED_MODEL_SHA256 equals the reranker_model_sha256 in benchmarks/results/repro/20261014T232318Z/MANIFEST.json, and that manifest says reranker_state: loaded. All 131 committed reranker_model_sha256 entries carry this same value.
  • Same cache directory as recall. The doctor reads the directory that hooks/wiring.py registers behind core.reranker_model.reranker_cache_dir, and builds the path with the same model_path_under. So it hashes the file FlashRank loads.
  • Layers and craftsmanship. doctor_reranker.py sits where the other doctor_*.py modules sit. core/reranker_identity.py imports only the standard library. doctor.py was already over the size limit and is in the baseline. No new numeric constant needs a # source:.
  • Existing tests. No existing test checks the exact manifest key set. The test that requires _recorded_failures to come last in both check lists still passes.
  • Docstrings. The claim that the check never downloads, deletes or loads a model holds: it only reads the file in chunks, and catches OSError only. No text claims the weights are checked before FlashRank loads them.
  • Hot paths. Recall and server startup do not hash the file. Each manifest still hashes it once.

Notes, no action before merge

  • mcp_server/doctor_reranker.py:33 — the mismatch fix says to delete the .onnx file. If FlashRank only downloads when the whole model directory is missing, it will not download again. I could not check FlashRank's source here. Pointing the fix at the parent directory avoids the question.
  • benchmarks/_repro.py:83-84 and benchmarks/lib/write_manifest.py:91-92 — model_identity() runs inside the same broad except as ensure_reranker_loaded(). A hashing error after a successful load records everything as unresolved and loses the loaded state, and neither manifest builder has a test.
  • reranker_model.model_identity() always compares against the L-12 pin. The reranker-matrix cells that swap _MODEL_NAME will record mismatch. That is true, but nothing documents it.

Verdict: approve

@claude

claude Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 2m 10s —— View job


Review 2 of 3 (ADR-1119) — PR #972, commits after 9d25ef3 (2df9d02)

Review 1 found no Blockers, so nothing was carried over. I reviewed only the fixup 2df9d02 (mcp_server/doctor_reranker.py, tests_py/test_doctor_reranker.py). I ran five reviews on it: code quality, performance, test coverage, documentation accuracy and security. None of them found a Blocker.

What I checked:

  • The fix now names the directory, which settles review 1's first note. model_path_under builds {cache}/{_MODEL_NAME}/{_MODEL_FILE} with /, and neither name contains a /. So rsplit("/", 1)[0] is always {cache}/ms-marco-MiniLM-L-12-v2, on Windows too, and never the cache root, ~ or /, even when XDG_CACHE_HOME is empty. It is the directory Ranker._prepare_model_dir checks: ADR-0244 quotes FlashRank 0.2.10 downloading only when that directory is absent. The FlashRank source is not installed here, so this rests on the ADR.
  • The docstring still holds. The check only reads the file. "Remove" appears only in the fix text, which goes through str.format as an argument, so there is no injection.
  • The tests catch a revert. The trailing space in f"Remove {dir} " means a fix that names the .onnx file again fails both assertions. _joined returns exactly what production builds, because the fixture's directory name equals _MODEL_NAME.
  • Limits. The file has 70 lines and the function about 27. Lint, tests and the craftsmanship gate were not run locally because this job was not allowed to run commands. I posted this here instead of with gh pr comment because this job could not write the comment body to a file. CI Pipeline was still running when I posted.

Notes, no action before merge

  • tests_py/test_doctor_reranker.py:23-28: _joined never ends if no parent has the expected name (Path("/").parent == Path("/")). pytest-timeout (300 s) would end it, but f"{tmp_path}/{_MODEL_NAME}" would be simpler.
  • mcp_server/doctor_reranker.py:51: the doctor rebuilds the directory by taking apart core's path format. A model_dir_under(cache_dir) in core/reranker_model.py would keep that format in one place.
  • _ABSENT_FIX: "run one recall" does not download in a server that has already failed to load, because _flashrank_failed (core/reranker.py:70) stays set for the life of the process. "Restart Cortex, then run one recall" would be accurate.
    · Branch fix/734-reranker-identity

Verdict: approve

cdeust commented Nov 10, 2026

Copy link
Copy Markdown
Owner Author

CI autopilot : unit en échec sur 2df9d02 (run 42808456857), à cause du gate de mutation (ADR-1122) ; merge n'échoue que parce que unit a échoué.

  • Diagnostic : deux fichiers modifiés gardaient des mutants vivants.
    • core/reranker_model.py : aucun test sélectionné n'appelait model_identity, d'où le mutant x_model_identity__mutmut_1, non couvert.
    • doctor_reranker.py : 13 mutants survivaient, parce que les tests ne vérifiaient qu'un fragment du détail et de l'aide, et jamais optional dans la branche OK.
  • Correctif (fixup 3aa5320, tests seulement, le code de production n'a pas changé) :
    • les tests du doctor vérifient les arguments exacts du Check (nom, ok, détail, aide, optional) dans les quatre branches ;
    • un test appelle reranker_model.model_identity avec les providers configurés et vérifie le chemin lu.
  • Reçu local : GITHUB_BASE_REF=main python scripts/ci_mutation_diff.py donne 4 file(s), 0 failed, 0 untested, avec 0 survivant dans chacun des quatre fichiers. Les suites liées donnent 457 passed et 15 skipped ; le gate de craftsmanship passe.
  • Non vérifié : la CI de 3aa5320 (en cours) et la matrice des trois OS.

Generated by Claude Code

@claude

claude Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 1m 42s —— View job


Review 3 of 3 (ADR-1119) is done

None of the five reviews found a Blocker, and the comment has three Notes. The whole PR is accepted: no file or commit has an open Blocker.

Verdict: approve
· Branch fix/734-reranker-identity

@claude

claude Bot commented Nov 10, 2026

Copy link
Copy Markdown

Review 3 of 3 (ADR-1119) — PR #972, commits after 2df9d02 (3aa5320)

Reviews 1 and 2 found no Blockers, so nothing was carried over. I reviewed only 3aa5320, which changes only the tests (tests_py/core/test_reranker_identity.py, tests_py/test_doctor_reranker.py). I ran five reviews: code quality, performance, test coverage, documentation accuracy and security. None of them found a Blocker.

What I checked:

  • Exact assertions match production. The detail and fix strings expected by the four identity tests match the ones mcp_server/doctor_reranker.py builds, character for character, optional=True included. The unreadable-file test builds the same PermissionError(13, …, path), so it also pins the printed path. This settles review 2's first note: _joined is gone.
  • New model_identity test. It replaces _cache_dir_provider and _model_sha256_provider through monkeypatch, which restores them. The path it asserts is the one model_path_under builds.
  • Isolation. The only write goes to tmp_path. The real ~/.cache/flashrank is never read, and nothing goes to the network.
  • Limits. The files have 58 and 107 lines and every function is short. This job was not allowed to run pytest, ruff or the craftsmanship gate, so these results come from reading the code. Check CI Pipeline on 3aa5320 before merging.

Notes, no action before merge

  • tests_py/test_doctor_reranker.py:3-5: the docstring says every test records the Check arguments, but test_the_check_runs_on_both_backends only checks list membership.
  • tests_py/core/test_reranker_identity.py: the model_identity stub returns the pinned digest. A body that returns VERIFIED without looking at the digest would survive; a stub that returns a different digest and expects MISMATCH would catch it.
  • mcp_server/doctor_reranker.py:54: only PermissionError is tested, so narrowing except OSError (for example to PermissionError) would survive.

Accepted: the whole PR (9d25ef3, 2df9d02, 3aa5320). No file or commit has an open Blocker.

Verdict: approve

@github-actions
github-actions Bot merged commit 4b45b33 into main Nov 10, 2026
18 checks passed
github-actions Bot pushed a commit that referenced this pull request Nov 10, 2026
…973)

* docs(agents): state the CI contract every agent and routine follows

The scheduled routines were written before the review contract: on
2026-11-10 they measured branches against the main branch and rebased
them with a forced push, commented on approved pull requests (#968) and
opened pull requests while another was in flight (#971, #972). Their
prompt tells them to read CLAUDE.md and docs/ and to apply the
repository's standards, so the rules "Review rounds" does not hold yet
are written next to it, and both are said to win over a less
restrictive routine prompt.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYsWnKSjpX4oNV19PCF
Signed-off-by: Claude <[email protected]>

* docs(agents): rule 1 names the two merges the verdict does not decide

Notes of review 1 of #973, taken in one push because that review's
verdict cannot be read by the resolver (its comment does not link the
run, fixed for later reviews by ADR-1130, #975): the low-risk Dependabot
merge and the "do not merge" title, and what a pull request cut down
after its third review waits for.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYsWnKSjpX4oNV19PCF
Signed-off-by: Claude <[email protected]>

---------

Signed-off-by: Claude <[email protected]>
Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants