phase: 125_image_anchor_context
All completion criteria verified. The repository is left functional with all changes in the working tree (no commits made, per the harness protocol). ## Final report — phase 125 `image_anchor_context` (verification pass) All 6 tasks were already complete; this pass verified every criterion and ran the full gates: - **Migration 0023** `documents.image_context` (TEXT nullable, reversible) — applied (`alembic current` = 0023 head); round-trip test (4 passed) exercises real upgrade → downgrade 0022 → upgrade. - **Criterion 1** (referenced image enriched + context line in retrieval): integration `test_markdown_referenced_image_gets_context_and_enriched_chunks` + E2E `test_sync_enriches_the_image_doc_with_markdown_context`, `test_chat_answer_quotes_the_context_line` (wire prompt pinned: `<document>` block leads with the one-line `Referenced in …: "alt"` line) — verified present and passing. - **Criterion 2** (standalone image byte-identical): E2E `test_standalone_*` (NULL `image_context`, block body exactly the bare description, no `Referenced in` anywhere in the prompt) + unit `test_describe_image_without_context_is_byte_identical` — passing. - **Criterion 3** (markdown-first: record stored on markdown doc, image picks up context later): integration `test_markdown_indexed_before_image_stores_record_and_image_picks_it_up` + post-index enrichment pass test (markdown file gone from disk) — passing. - **Gates:** `uv run pytest` → 2938 passed, exit 0 · `uv run pytest --cov=app --cov-report=term-missing` → exit 0, TOTAL **99%** (>90%) · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors, 0 warnings. - **Phase E2E in isolation:** `uv run pytest tests/e2e/test_image_anchor_context.py -v --no-cov` → 4 passed. - **Mock-LLM change regression check:** neighboring image E2E suites green in isolation — `test_smoke` (3), `test_image_documents` (16), `test_chat_image_questions` (17), `test_answer_images` (17). - No defects found; no code changes needed. No live-infrastructure changes (local dev DB only). No commit made (harness commits). - Next pending phase: check `.agents/phases/todo/` after the harness moves this phase.
This commit is contained in:
+137
-7
@@ -89,6 +89,7 @@ no longer injected — see :mod:`app.api.chat`).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
@@ -434,6 +435,116 @@ def build_kb_section(overview: str, max_chars: int | None = None) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
#: Phase 125 (task 05, LOCKED A4) — the header line of one block of an
|
||||
#: enriched image doc's stored ``image_context`` (the exact shape the
|
||||
#: importer's reference-block writer emits, tasks 03/04):
|
||||
#: ``Referenced in <source>/<markdown_path>:`` — a block is one
|
||||
#: referring markdown file, and a block's line FOLLOWING its header is
|
||||
#: always its first ``- Alt text: "..."`` bullet. A look-alike line in
|
||||
#: the context body without that bullet is spurious and dropped
|
||||
#: (graceful degradation, A4).
|
||||
_IMAGE_CTX_HEADER_RE = re.compile(r"^Referenced in (?P<ref>.+):$")
|
||||
|
||||
#: The ``- Alt text: "..."`` bullet of the same block. The greedy alt
|
||||
#: field (the block writer does not escape quotes) is captured between
|
||||
#: the opening quote (the pattern's literal) and the CLOSING quote
|
||||
#: (the pattern's trailing ``"$``) — an alt with inner ``"`` survives
|
||||
#: intact (the greedy walk keeps the inner quote, the final one is the
|
||||
#: delimiter; an empty alt matches as the empty group).
|
||||
_IMAGE_CTX_ALT_RE = re.compile(r'^- Alt text: "(?P<alt>.*)"$')
|
||||
|
||||
|
||||
def _image_ctx_clause(ref: str, alt: str | None) -> str:
|
||||
"""One clause of the retrieval context line (phase 125, task 05):
|
||||
the referring markdown's stored identity (``<source>/<path>``
|
||||
exactly as the block header carries it) plus the reference's alt
|
||||
text quoted after a colon — the reference's purpose in one phrase.
|
||||
A reference with no alt (````) renders the bare identity
|
||||
(nothing to quote).
|
||||
"""
|
||||
if alt:
|
||||
return f'{ref}: "{alt}"'
|
||||
return ref
|
||||
|
||||
|
||||
def _extract_context_line(image_context: str) -> str | None:
|
||||
"""The ONE-LINE rendering of an enriched image doc's stored
|
||||
``image_context`` (phase 125, task 05, LOCKED A4) — or ``None``
|
||||
when nothing in the value parses as a reference block (graceful
|
||||
degradation: the ``<document>`` block then shows only the
|
||||
description, exactly pre-phase — the value never raises into the
|
||||
prompt build).
|
||||
|
||||
The stored value (tasks 03/04) is one block per referring markdown
|
||||
file, blank-line joined::
|
||||
|
||||
Referenced in <source>/<markdown_path>:
|
||||
- Alt text: "<alt_text>"
|
||||
Context: <context paragraphs — may span many lines>
|
||||
- Alt text: "<a second reference's alt>"
|
||||
|
||||
The result keeps only what tells the LLM WHY the image exists in
|
||||
the documentation — one clause per referring file (its identity +
|
||||
the FIRST reference's alt), after a single ``Referenced in ``
|
||||
prefix, ``; ``-joined::
|
||||
|
||||
Referenced in docs/a.md: "alt a"; docs/b.md
|
||||
|
||||
The ``Context:`` body is never rendered (the surrounding paragraphs
|
||||
already reached the vision model at enrichment time, LOCKED A3 —
|
||||
the line is the locator + purpose, not a duplicate of the
|
||||
paragraphs), so the line stays short enough not to bloat the
|
||||
prompt. The markdown doc's ``IMAGE_CONTEXT:`` pending record
|
||||
(task 04) carries no ``Referenced in`` header at all and parses to
|
||||
``None`` — and it rides a TEXT doc, which :func:`_document_body`
|
||||
excludes from the line via the ``is_image`` guard anyway.
|
||||
"""
|
||||
clauses: list[str] = []
|
||||
ref: str | None = None # the current block's "source/markdown_path"
|
||||
alt: str | None = None # the current block's first bullet's alt text
|
||||
expecting_alt = False # the line after a header must be its bullet
|
||||
for line in image_context.splitlines():
|
||||
header = _IMAGE_CTX_HEADER_RE.match(line)
|
||||
if header is not None:
|
||||
if ref is not None and not expecting_alt:
|
||||
clauses.append(_image_ctx_clause(ref, alt))
|
||||
ref, alt = header.group("ref"), None
|
||||
expecting_alt = True
|
||||
continue
|
||||
alt_match = _IMAGE_CTX_ALT_RE.match(line) if ref is not None else None
|
||||
if expecting_alt:
|
||||
if alt_match is None:
|
||||
# Spurious header (a look-alike line in context text):
|
||||
# its bullet never came — drop the block.
|
||||
ref, alt, expecting_alt = None, None, False
|
||||
else:
|
||||
alt = alt_match.group("alt")
|
||||
expecting_alt = False
|
||||
continue
|
||||
# Any other line is context body or a second bullet of the
|
||||
# same block — the first reference's alt is already captured,
|
||||
# so the one-line rendering is unaffected (no branch needed:
|
||||
# with a live block and no pending bullet, ``alt`` is never
|
||||
# ``None``).
|
||||
if ref is not None and not expecting_alt:
|
||||
clauses.append(_image_ctx_clause(ref, alt))
|
||||
if not clauses:
|
||||
return None
|
||||
return "Referenced in " + "; ".join(clauses)
|
||||
|
||||
|
||||
def _image_context_line(doc: Document) -> str | None:
|
||||
"""The phase-125 context line for one suggested document (task 05,
|
||||
LOCKED A4) — ``None`` in the common case (EVERY text doc, and every
|
||||
image doc without a parsable ``image_context`` — standalone /
|
||||
pre-phase rows: the byte-identical path) and the one-line
|
||||
:func:`_extract_context_line` rendering otherwise (``is_image``
|
||||
docs with a non-NULL, parsable ``image_context`` only)."""
|
||||
if not doc.is_image or not doc.image_context:
|
||||
return None
|
||||
return _extract_context_line(doc.image_context)
|
||||
|
||||
|
||||
def _document_body(doc: Document) -> str:
|
||||
"""The body of one ``<document>`` suggestion block (phase 118, LOCKED
|
||||
A6): the document's stored summary (stripped) — NEVER the full
|
||||
@@ -447,15 +558,33 @@ def _document_body(doc: Document) -> str:
|
||||
summary-bearing docs makes no settings call for the cap). Content at
|
||||
or under the cap rides whole, unmarked (nothing was cut). No LLM
|
||||
call at chat time — the preview is deterministic.
|
||||
|
||||
Phase 125 (task 05, LOCKED A4): an ENRICHED image doc (``is_image``
|
||||
with a non-NULL ``image_context`` that parses — see
|
||||
:func:`_image_context_line`) leads its body with the ONE short
|
||||
context line — ``Referenced in <source>/<path>: "<alt>"`` — before
|
||||
the description, so the LLM knows WHY the image exists in the
|
||||
documentation, not only what it shows. Every other doc is
|
||||
byte-identical to the pre-phase body: every text doc (its
|
||||
``image_context`` may carry the task-04 pending record, which never
|
||||
renders) and every non-enriched image doc (NULL ``image_context``
|
||||
— the phase's byte-identity criterion).
|
||||
"""
|
||||
summary = (doc.summary or "").strip()
|
||||
if summary:
|
||||
return summary
|
||||
limit = get_settings().suggestion_preview_chars
|
||||
content = doc.content
|
||||
if len(content) > limit:
|
||||
return content[:limit] + "\n" + TRUNCATION_MARKER
|
||||
return content
|
||||
body = summary
|
||||
else:
|
||||
limit = get_settings().suggestion_preview_chars
|
||||
content = doc.content
|
||||
body = (
|
||||
content[:limit] + "\n" + TRUNCATION_MARKER
|
||||
if len(content) > limit
|
||||
else content
|
||||
)
|
||||
context_line = _image_context_line(doc)
|
||||
if context_line is not None:
|
||||
body = context_line + "\n\n" + body
|
||||
return body
|
||||
|
||||
|
||||
def build_high_prompt(
|
||||
@@ -530,7 +659,8 @@ def build_high_prompt(
|
||||
APPENDED after ``title`` — the only position; always present,
|
||||
``created_at`` is NOT NULL) — plus the document's summary
|
||||
(:func:`_document_body`; the NULL/whitespace-summary preview
|
||||
fallback is LOCKED A5).
|
||||
fallback is LOCKED A5; an enriched image doc's summary leads with
|
||||
the phase-125 one-line context line, LOCKED A4).
|
||||
"""
|
||||
# Phase 106 (D5): every document the model sees carries its
|
||||
# creation date — the block's ``date`` attribute (the row's
|
||||
|
||||
Reference in New Issue
Block a user