**Phase 118 final verification pass — complete.** All criteria verified; 4 pre-existing defects found and fixed.
- **Verified:** summary-seed wiring (`select_suggested` top-5 no-floor → summary blocks, no full text in HIGH prompt), all-doc markdown summaries + NULL backfill (`summary_backfilled`, no `sources_meta` bump), `read` adds full text with `read_docs`-only dedupe, `done.sources` = suggested+read / durable record = suggested+related+read + `suggested=N` log line (seen live in E2E), byte-locked PERSONA/LOW/TOOLS_SECTION, battery gate PASS recorded in `TOOL_CALLING_TESTING.md` §10 (turbo 2026-09-16: 1/2/4 GREEN, cond-3 reported 9/10 per A7, contract 21/21, caps 0).
- **Defects fixed (all pre-existing, none phase-118):** ① `ChatMessage` schema missing the phase-113 `related` key → `extra="forbid"` 422'd every done-time auto-save of grounded turns with a related tier, leaving `message_count=1` (root cause of `test_share_chat` 3F; browser-level instrumentation proved the PUT 422) — added the field + unit/integration pins; ② `test_theme_semantic_completion` pins stale vs phase-117 debox (border/chip removed) — re-targeted to assert border/chip *absence*; ③ `test_header_consistency` `<26`px pin red on 26.125px native date-input line — bound relaxed to `<34` (wrap-detection intent kept); ④ `test_navbar_refresh` bor.chat.v1 key set updated for `related`.
- **Test/lint/coverage:** `uv run pytest --cov=app --cov-report=term-missing` → **2506 passed, app/ 99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors.
- **E2E:** new story suite in isolation → **2 passed**; full 103-suite matrix sweep (each isolated) → **all 103 green** after the fixes; `test_share_chat` 4 passed, `test_theme_semantic_completion` 8 passed, `test_header_consistency` 3 passed, `test_navbar_refresh` 7 passed.
- **Deviations:** none from LOCKED decisions. Note: orphaned diagnostic uvicorn processes briefly made E2E sessions exercise stale code — killed and re-verified; a sweep-regenerated tracked screenshot was restored. No commits made (harness commits).
- **Completion criteria:** all 7 ✅ (commit/phase-move is the harness's step).
- **Next pending phase:** none — `todo/` holds only this phase's overview pending the harness move.
179 lines
7.7 KiB
Python
179 lines
7.7 KiB
Python
"""Prompt-lock pin (phase 112, task 03 — owner decision iii, 2026-09-14).
|
|
|
|
The persona + HONESTY GATE text is **locked verbatim** (PLAN §6): it
|
|
changes through the plan, never in code. This module byte-pins the
|
|
locked prompt constants against their pre-phase-112 anchor values —
|
|
sha256 + exact prefix/suffix + total length, so *any* byte change
|
|
(option (i)'s copy tightening, option (ii)'s plan amendment, or an
|
|
accidental edit) fails loudly until the anchors are re-cut as part of
|
|
the same plan revision. ``tests.unit.test_prompts`` pins the
|
|
assembled-prompt structure and the behavioral contracts on top of
|
|
these constants; this file pins the constants themselves.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
|
|
from app.rag.prompts import (
|
|
PERSONA,
|
|
SUGGEST_INTRO,
|
|
TOOLS_SECTION,
|
|
_base,
|
|
build_deflect_prompt,
|
|
)
|
|
|
|
|
|
def _sha256(text: str) -> str:
|
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
# ---------- PERSONA (the locked base of BOTH the HIGH and LOW prompts) ----------
|
|
|
|
#: Pre-phase-112 anchors for ``PERSONA`` (the ``{relevance}`` placeholder
|
|
#: and the line wrapping included).
|
|
PERSONA_SHA256 = "e31792a73e64c53853097e0f7b6df8b96c5f2d05c286944c3edba16dde7777fe"
|
|
PERSONA_LEN = 706
|
|
PERSONA_PREFIX = (
|
|
'You are "Brain of Reese" — the digital brain of Reese, a self-hoster and\n'
|
|
"homelab tinkerer. Personality: chippy, upbeat, warm, and genuinely\n"
|
|
"optimistic about the user's ability to do things.\n"
|
|
"\n"
|
|
"Rules:\n"
|
|
)
|
|
PERSONA_SUFFIX = (
|
|
"4. Never invent facts, hosts, or steps that are not in the context.\n"
|
|
"5. Keep answers tight: short paragraphs, bullets where helpful.\n"
|
|
"\n"
|
|
"<relevance>{relevance}</relevance>"
|
|
)
|
|
|
|
|
|
def test_persona_byte_locked() -> None:
|
|
"""Any byte change to the locked persona (opening, any rule line, the
|
|
``<relevance>`` placeholder) fails on the sha256; the prefix/suffix
|
|
anchors name the damaged region for the diff."""
|
|
assert len(PERSONA) == PERSONA_LEN
|
|
assert _sha256(PERSONA) == PERSONA_SHA256
|
|
assert PERSONA.startswith(PERSONA_PREFIX)
|
|
assert PERSONA.endswith(PERSONA_SUFFIX)
|
|
|
|
|
|
def test_high_and_low_bases_byte_locked() -> None:
|
|
"""``_base`` only substitutes ``{relevance}`` — the per-mode base
|
|
lengths pin the substitution against a moved or re-spelled
|
|
placeholder in the locked text."""
|
|
assert len(_base("HIGH")) == 699 # PERSONA_LEN - 11 + 4
|
|
assert len(_base("LOW")) == 698 # PERSONA_LEN - 11 + 3
|
|
|
|
|
|
# ---------- TOOLS_SECTION (the HIGH prompt's locked ``<tools>`` copy) ----------
|
|
|
|
#: Anchors for ``TOOLS_SECTION`` — pre-phase-112 values, RE-CUT for
|
|
#: phase 118 (task 04, A6, owner directive 2026-09-15): the ``read``
|
|
#: clause was rewritten for the summary-seed mode (the ``<documents>``
|
|
#: section holds SUMMARIES — ``read`` adds the full text; only an
|
|
#: already-read document is refused). Only that clause moved — the
|
|
#: prefix (the ``ls``-clause opening) and the suffix (the
|
|
#: discipline-rules ending) survived byte-identical, so they are the
|
|
#: same anchors as pre-phase-118.
|
|
TOOLS_SECTION_SHA256 = "87ee80faf0170da6ab1de518177313def6460785613fa074312a2a5f9f071750"
|
|
TOOLS_SECTION_LEN = 2465
|
|
TOOLS_SECTION_PREFIX = (
|
|
"<tools>\n"
|
|
"You may extend your context with three tools. `ls` lists the "
|
|
"knowledge base as a tree, one level at a time: "
|
|
)
|
|
TOOLS_SECTION_SUFFIX = (
|
|
"Never repeat a call that was refused or already succeeded — the refusal "
|
|
"already told you the correct form. Answer as soon as you have what "
|
|
"you need.\n</tools>"
|
|
)
|
|
|
|
|
|
def test_tools_section_byte_locked() -> None:
|
|
"""The ``<tools>`` teaching is LOCKED verbatim too (the E2E mock keys
|
|
on the ``<tools>`` marker's presence; the wording is the owner's):
|
|
sha256 + exact prefix/suffix + total length."""
|
|
assert len(TOOLS_SECTION) == TOOLS_SECTION_LEN
|
|
assert _sha256(TOOLS_SECTION) == TOOLS_SECTION_SHA256
|
|
assert TOOLS_SECTION.startswith(TOOLS_SECTION_PREFIX)
|
|
assert TOOLS_SECTION.endswith(TOOLS_SECTION_SUFFIX)
|
|
|
|
|
|
# ---------- the LOW prompt's locked DEFLECT_MODE body ----------
|
|
|
|
#: The ``DEFLECT_MODE`` body exactly as it was pre-phase-112 (the
|
|
#: phase-71 plain-text line included) — inline in
|
|
#: :func:`app.rag.prompts.build_deflect_prompt`, so it is pinned through
|
|
#: the built prompt rather than a module constant.
|
|
LOW_BODY_SHA256 = "c9868cfccdd0ff79d7c1de5df5f6dea0182d3726ca912c9ef609ab54b563a0a4"
|
|
LOW_BODY_LEN = 259
|
|
LOW_BODY = (
|
|
"DEFLECT_MODE: retrieval was weak — the titles below are the closest "
|
|
"your notes come to the question. They are titles only; do not pretend "
|
|
"they answer it. Use them to propose 2-3 alternative questions.\n"
|
|
"Reply in plain text only — you have no tools in this mode."
|
|
)
|
|
|
|
|
|
def test_deflect_body_byte_locked() -> None:
|
|
"""The LOW build = locked base + exactly the locked DEFLECT_MODE body
|
|
+ the weak-hit title list — byte for byte (the mock keys on the
|
|
``DEFLECT_MODE`` marker's presence; the body wording is locked)."""
|
|
assert len(LOW_BODY) == LOW_BODY_LEN
|
|
assert _sha256(LOW_BODY) == LOW_BODY_SHA256
|
|
assert build_deflect_prompt(["T1", "T2"]) == _base("LOW") + "\n" + LOW_BODY + "\n- T1\n- T2"
|
|
prompt = build_deflect_prompt(["T1", "T2"])
|
|
assert prompt.count(LOW_BODY) == 1
|
|
assert prompt.index("DEFLECT_MODE") < prompt.index(
|
|
"Reply in plain text only"
|
|
) # the marker precedes the plain-text line
|
|
|
|
|
|
#: The full LOW prompt build on the canonical fixture titles — sha-pinned
|
|
#: (phase 118, task 03): the deflection path is UNTOUCHED by the
|
|
#: summary-seed re-revision (LOCKED A8) — the same inputs must produce
|
|
#: the pre-phase bytes, so this anchor is a pre-phase-118 value.
|
|
LOW_PROMPT_SHA256 = "726eddb4eb3bcc26c840011f6f8635d6af55aa09d4d064bbb9e256caa9665837"
|
|
LOW_PROMPT_LEN = 968
|
|
|
|
|
|
def test_low_prompt_build_byte_identical_to_pre_phase() -> None:
|
|
"""Phase 118 contract: the LOW prompt output is byte-identical to
|
|
pre-phase for identical inputs — the summary seeding (and the new
|
|
``SUGGEST_INTRO`` line) never leaks into the deflection path."""
|
|
prompt = build_deflect_prompt(["T1", "T2"])
|
|
assert len(prompt) == LOW_PROMPT_LEN
|
|
assert _sha256(prompt) == LOW_PROMPT_SHA256
|
|
assert "SUGGEST_INTRO" not in prompt and "<documents>" not in prompt
|
|
|
|
|
|
# ---------- SUGGEST_INTRO (phase 118, task 03 — the start-here framing) ----------
|
|
|
|
#: Phase-118 anchors for ``SUGGEST_INTRO`` — the ``<documents>``
|
|
#: section's intro line (the owner's "start here if these summaries seem
|
|
#: right to you" framing, TODO L3). The E2E mock's ``_document_block``
|
|
#: parser is regex-based over the block markup (which stays byte-stable
|
|
#: around the intro), so this constant is a prompt-copy lock, pinned the
|
|
#: way ``TOOLS_SECTION`` is: sha256 + prefix + total length.
|
|
SUGGEST_INTRO_SHA256 = "7b14d2dedc6ebc4e440d32dd1edb979a7461c94034b9541c9f37b9042f394d3a"
|
|
SUGGEST_INTRO_LEN = 323
|
|
SUGGEST_INTRO_PREFIX = (
|
|
"The blocks below are the summaries of the top-ranked documents for "
|
|
"your question — start here if one seems right to you: "
|
|
)
|
|
|
|
|
|
def test_suggest_intro_byte_locked() -> None:
|
|
"""The start-here framing is LOCKED copy (phase 118): sha256 + exact
|
|
prefix + total length; the three contracts it must carry (summaries
|
|
are the starting points; ``read`` adds the full text, which is NOT
|
|
in the prompt until read; cite by path) are pinned as substrings."""
|
|
assert len(SUGGEST_INTRO) == SUGGEST_INTRO_LEN
|
|
assert _sha256(SUGGEST_INTRO) == SUGGEST_INTRO_SHA256
|
|
assert SUGGEST_INTRO.startswith(SUGGEST_INTRO_PREFIX)
|
|
assert "call `read`" in SUGGEST_INTRO
|
|
assert "combined `source/path`" in SUGGEST_INTRO
|
|
assert "its full text is not in the prompt until you read it" in SUGGEST_INTRO
|
|
assert "Cite the document(s) you used, by path." in SUGGEST_INTRO
|