phase: 125_image_anchor_context
Build and Push Containers / build-and-push-app (push) Successful in 2m8s
Build and Push Containers / build-and-push-db (push) Failing after 1m38s

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:
2026-09-27 05:53:12 -04:00
parent 62e4f6b572
commit e0307fe3ff
60 changed files with 6583 additions and 82 deletions
+137 -7
View File
@@ -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 (``![](path)``) 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