mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
d699782efa
## Summary Partitions the judge-verdict cache key by judge-prompt version so a prompt revision invalidates stale entries automatically, instead of serving a verdict cached under the old prompt until the TTL expires. The cache key previously included `judge_model` but no prompt-version component. A judge-prompt revision (e.g. #213's Step-0 sub-claim decomposition) therefore did not invalidate stale entries — a verdict cached under the old prompt was still served, silently bypassing the new prompt logic. Pre-existing cache-key design (P2#1 in the #355 post-squash review); affects every prompt revision, not just the decomposition path. ## What changed - **`_cache_key` gains a `prompt_version` component**, kept separate from `judge_model` (independent axes of judge behavior). A prompt revision partitions the keyspace; same-version entries still dedup (no regression). - **Cache invalidation keys on `JUDGE_PROMPT_SHA256`** — the SHA-256 of the canonical judge-prompt section, the single source of truth. `scripts/check_judge_prompt_version.py` keeps that hash in lockstep with the prompt text, so any prompt edit changes the key and invalidates stale entries — no reliance on a human remembering to bump a label. `JUDGE_PROMPT_VERSION` is a decoupled human-readable label for logs/diffs only. - **Fail-CLOSED on unknown version**: when the caller declares the prompt version unknown (`None`), the pipeline binds a run-local component (`__unknown__:<audit_run_id>`), so a stale entry is never served across an unknown-version boundary. Cross-run hits are disabled; within-run dedup for repeated citations still holds. - **CI backstop** `scripts/check_judge_prompt_version.py`: hashes the canonical judge-prompt section (between the `JUDGE-PROMPT-CANONICAL` markers in the agent `.md`) and fails if it drifts from the pinned `JUDGE_PROMPT_SHA256`, forcing a hash re-pin in the same change. Wired into `spec-consistency.yml` + the pytest manifest. - **Contract + docstring alignment** (second commit): the agent-prompt contract and the lint docstring described invalidation as keyed on the `JUDGE_PROMPT_VERSION` label, but the pipeline already falls back to `JUDGE_PROMPT_SHA256`. A downstream implementer following the stale contract would key the cache on a value whose bump the drift guard does not enforce, re-opening the bug. Re-attributed to the SHA256 fingerprint. ## Verification - RED→GREEN + mutation-verified: reverting the fallback to the label re-fails the hash-tracking guard test (`1 != 2`, stale entry not invalidated); restored → 6 passed. - Within-run dedup preserved under fail-closed; same-version + default-constant dedup preserved (no regression). - Lint companion: clean passes, drift fails, missing markers error out. - Full suite **2275 passed / 3 skipped** (= 2278 collected − 3 skipped). - Independent diff review: 0 P1 / 0 P2. Security review: 0 findings. Closes #361
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for check_judge_prompt_version (#361 drift guard).
|
|
|
|
The clean fixture must PASS; a prompt edit that does not re-pin the hash must
|
|
FAIL (the core #361 backstop); a correct re-pin must PASS again; a removed
|
|
marker must error out (so the guard cannot be silently disabled).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import scripts.check_judge_prompt_version as lint
|
|
|
|
_PROMPT_BODY = "> CLAIM: {claim_text}\n> Output ONE verdict from {SUPPORTED, UNSUPPORTED}."
|
|
|
|
|
|
def _agent_md(prompt_body: str, *, with_markers: bool = True) -> str:
|
|
if with_markers:
|
|
section = (
|
|
"<!-- JUDGE-PROMPT-CANONICAL-START (#361): note -->\n\n"
|
|
f"{prompt_body}\n\n"
|
|
"<!-- JUDGE-PROMPT-CANONICAL-END (#361) -->"
|
|
)
|
|
else:
|
|
section = prompt_body
|
|
return f"### Step 5 — Judge invocation\n\n{section}\n\n### Step 6 — next\n"
|
|
|
|
|
|
def _constants_src(pinned_hash: str) -> str:
|
|
return (
|
|
'JUDGE_PROMPT_VERSION = "step0-decomp-v1"\n'
|
|
f'JUDGE_PROMPT_SHA256 = "{pinned_hash}"\n'
|
|
)
|
|
|
|
|
|
def _hash_of(prompt_body: str) -> str:
|
|
# Compute the expected hash via the PRODUCTION extraction path, not a shadow
|
|
# copy of the regex — so an extraction bug fails test_clean_fixture_passes
|
|
# instead of hiding behind a matching wrong answer on both sides.
|
|
section = lint._extract_prompt_section(_agent_md(prompt_body))
|
|
assert section is not None
|
|
return hashlib.sha256(section.encode("utf-8")).hexdigest()
|
|
|
|
|
|
class CheckJudgePromptVersionTest(unittest.TestCase):
|
|
def _write_root(self, agent_md: str, constants_src: str) -> Path:
|
|
root = Path(tempfile.mkdtemp())
|
|
agent = root / lint._AGENT_REL
|
|
agent.parent.mkdir(parents=True, exist_ok=True)
|
|
agent.write_text(agent_md, encoding="utf-8")
|
|
constants = root / lint._CONSTANTS_REL
|
|
constants.parent.mkdir(parents=True, exist_ok=True)
|
|
constants.write_text(constants_src, encoding="utf-8")
|
|
return root
|
|
|
|
def test_clean_fixture_passes(self) -> None:
|
|
root = self._write_root(_agent_md(_PROMPT_BODY), _constants_src(_hash_of(_PROMPT_BODY)))
|
|
self.assertEqual(lint.check(root), 0)
|
|
|
|
def test_prompt_edit_without_repin_fails(self) -> None:
|
|
# Prompt changed; pinned hash still points at the OLD prompt → drift → fail.
|
|
root = self._write_root(
|
|
_agent_md(_PROMPT_BODY + "\n> EXTRA LINE that changes behavior"),
|
|
_constants_src(_hash_of(_PROMPT_BODY)),
|
|
)
|
|
self.assertEqual(lint.check(root), 1)
|
|
|
|
def test_prompt_edit_with_correct_repin_passes(self) -> None:
|
|
# The correct bump flow: prompt changed AND hash re-pinned → pass.
|
|
new_body = _PROMPT_BODY + "\n> EXTRA LINE that changes behavior"
|
|
root = self._write_root(_agent_md(new_body), _constants_src(_hash_of(new_body)))
|
|
self.assertEqual(lint.check(root), 0)
|
|
|
|
def test_missing_markers_errors(self) -> None:
|
|
root = self._write_root(
|
|
_agent_md(_PROMPT_BODY, with_markers=False),
|
|
_constants_src(_hash_of(_PROMPT_BODY)),
|
|
)
|
|
self.assertEqual(lint.check(root), 2)
|
|
|
|
def test_missing_pinned_constant_errors(self) -> None:
|
|
root = self._write_root(_agent_md(_PROMPT_BODY), 'JUDGE_PROMPT_VERSION = "x"\n')
|
|
self.assertEqual(lint.check(root), 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|