phase: 165_prose_word_wrap
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 11s

All verification complete. Final report:

**Phase 165 — Prose word wrap (final verification pass)**
- Verified: `PROSE_FORMATS={txt,text}` + `isProseDoc` (single-site, image arm wins); `renderDocument` three-way branch (md → `.doc-md`, prose → `.doc-prose` text node, else `pre.doc-raw` byte-identical); `.doc-prose` CSS = 72rem column, `pre-wrap` + `overflow-wrap: anywhere`, tokens only, zero literals; `[data-view="pages"]` hide list includes `.doc-prose` as one grouped declaration; phase-116 scrollbar rules intact and modal-scoped; no `innerHTML` in the prose path.
- Tests/lint (all 100% green):
  - `uv run pytest` → **3854 passed** (unit + integration)
  - `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90%)
  - `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**
  - `uv run pytest tests/e2e/test_prose_word_wrap.py -v --no-cov` → **5 passed** (isolation)
  - Isolation regressions: document_viewer 8, image_documents 4, pdf_pages_in_sources 4, pdf_strip_additive_selfheal 5, edit_summaries 4, summary_in_viewer 4, chat_answer_clusters 4, garbage_pdf 3, pdf_support 6, sticky_navbar 3, smoke 3 — all passed
- Completion criteria: image description wraps, full text, both surfaces ✔; `.txt` wraps same way ✔; `py`/`json`/code keep `pre.doc-raw` + horizontal overflow + themed modal scrollbar (E2E negative control + pins) ✔; no innerHTML, AA `--ink`/`--surface` pair ✔; full gates green ✔; commit + phase-dir move are harness-owned (not executed per pipeline rules).
- Documentation: README already updated by task 03 (viewer section + image-docs section); full README/AGENTS.md review found no further inconsistencies — **verified consistent, no changes needed**.
- Notable deviations: `.doc-prose` omits `font-family: inherit` from 00_phase D2's snippet — task 01's authoritative "exact property set" excludes it and `div` inherits fonts anyway (behavior identical). PDF deferral (A4) intact: `format == "pdf"` stays in `.doc-raw`, owner call recorded.
- Next pending phase: `166_hide_empty_answer_bubble`.
This commit is contained in:
2026-10-07 22:57:40 -04:00
parent 8d70aa946c
commit b0c7aed613
36 changed files with 2198 additions and 64 deletions
+292
View File
@@ -83,6 +83,17 @@ loosened):
bar, the phase-12/34 breach) + the ``#doc-copy-announcer``
(role=status) + the ``#doc-meta`` row the shared core builds the
control into.
Phase 165 (task 01) — the prose content block (TODO.md L3): the
prose-set constant (``PROSE_FORMATS`` + ``isProseDoc`` — the ONE
single-site rule), ``renderDocument``'s three-way content branch
(markdown → ``.doc-md``, prose (image descriptions, txt/text) →
``.doc-prose`` via textContent (never innerHTML), everything else →
``.doc-raw`` untouched), the ``isProseDoc`` truth table and the
format table as node behavioral runs over the real module, and the
``.doc-prose`` CSS (the 72rem reading column + ``pre-wrap`` +
``overflow-wrap: anywhere``, tokens only) beside the ``.doc-raw``
contract that stays locked (phase 116 A1 / phase 165 A3).
"""
from __future__ import annotations
@@ -1810,3 +1821,284 @@ def test_page_render_wires_the_copy_path_with_the_composed_string() -> None:
assert out["failOut"]["selectCount"] == 1
assert out["failOut"]["status"] == "Path ready — copy it from the field."
assert out["failOut"]["className"] == "doc-copy-pop" # the floating block
# ---------------------------------------------------------------------------
# Phase 165 (task 01) — the prose content block: PROSE_FORMATS /
# isProseDoc / renderDocument's third branch / the .doc-prose CSS
# (TODO.md L3 — image descriptions and txt/text docs WRAP)
# ---------------------------------------------------------------------------
STYLES_CSS = FRONTEND / "assets" / "styles.css"
def _content_branch(js: str) -> str:
"""The THREE-WAY content branch of renderDocument: from the
markdown ``if`` to the close of the raw ``else`` (the exact end
anchor: the raw arm's ``slot.appendChild(pre);`` + its closing
brace — the _stmt_block brace-walk would swallow the trailing
strip-mount statement, so this slice is literal)."""
start = js.index("if (doc.format === \"md\" || doc.format === \"markdown\") {")
end = js.index("slot.appendChild(pre);\n }") + len("slot.appendChild(pre);\n }")
return js[start:end]
def _prose_branch(js: str) -> str:
"""The prose arm alone: ``} else if (isProseDoc(doc)) {`` to its
closing brace after ``slot.appendChild(prose);``."""
start = js.index("} else if (isProseDoc(doc)) {")
end = js.index("slot.appendChild(prose);\n }") + len("slot.appendChild(prose);\n }")
return js[start:end]
def _prose_rule(js: str) -> str:
"""The phase-165 prose rule at module scope: the PROSE_FORMATS
const through the close of isProseDoc (extracted together — the
function references the const; the _fn brace-walk house pattern)."""
start = js.index("const PROSE_FORMATS")
end = js.index("}", js.index("function isProseDoc")) + 1
return js[start:end]
def test_prose_set_is_one_named_constant() -> None:
"""LOCKED A1: the prose rule lives in ONE named constant pair —
``const PROSE_FORMATS = new Set(["txt", "text"])`` +
``isProseDoc(doc)`` with the image arm UNCONDITIONAL (phase 122
A3 — an image doc's content IS the vision description, prose by
construction). Grep-verified single-site rule: the content-branch
block itself names no format literal — it calls isProseDoc."""
js = _read(DOCUMENT_JS)
rule = _prose_rule(js)
assert 'const PROSE_FORMATS = new Set(["txt", "text"]);' in rule
assert "doc.is_image === true || PROSE_FORMATS.has(doc.format)" in rule
branch = _content_branch(js)
assert "isProseDoc(doc)" in branch, "the prose arm gates on the ONE helper"
assert '"txt"' not in branch and '"text"' not in branch, (
"no second format literal in the content branch (single-site rule)"
)
def test_content_branch_is_three_way_md_prose_raw() -> None:
"""renderDocument's content branch is markdown → prose → raw, in
that order, all three appending into the SAME ``slot`` (phase 122
image block FIRST, phase 36 summary between, phase 163 additive
strip last — all unchanged). The markdown and raw arms stay
byte-identical to pre-phase (locked A3); the prose arm is
textContent-only — the XSS contract (never innerHTML for document
content) is PINNED on the branch source."""
js = _read(DOCUMENT_JS)
render = _fn(js, "renderDocument")
md_i = render.find('if (doc.format === "md" || doc.format === "markdown") {')
prose_i = render.find("} else if (isProseDoc(doc)) {", md_i)
raw_i = render.find("} else {", prose_i)
assert -1 < md_i < prose_i < raw_i, "the prose arm sits BETWEEN markdown and raw"
# The untouched markdown / raw arms (byte-identical contracts).
for stmt in (
'wrap.className = "doc-md";',
"wrap.innerHTML = renderMarkdown(doc.content);",
"slot.appendChild(wrap);",
'pre.className = "doc-raw";',
"pre.textContent = doc.content;",
"slot.appendChild(pre);",
):
assert stmt in render, f"the pre-phase arms must keep {stmt!r}"
# The prose arm: div + class + text node + the same slot.
prose_branch = _prose_branch(js)
assert 'document.createElement("div")' in prose_branch
assert 'prose.className = "doc-prose";' in prose_branch
assert "prose.textContent = doc.content;" in prose_branch
assert "slot.appendChild(prose);" in prose_branch
assert "innerHTML" not in prose_branch, (
"the prose arm never touches innerHTML (the XSS contract)"
)
#: (doc, expected isProseDoc) — LOCKED A1's exact set:
#: {is_image: true} ∪ {format ∈ txt, text}; code, code-ish, pdf,
#: unknown, extension-less (no format) and the string-``"true"``
#: lookalike all stay on the scrolling side (locked A3).
_PROSE_CASES: list[tuple[dict[str, object], bool]] = [
({"is_image": True, "format": "png"}, True),
({"is_image": True, "format": "webp"}, True),
({"is_image": True, "format": "md"}, True), # the image arm wins
({"is_image": True, "format": "pdf"}, True), # …regardless of format
({"is_image": True}, True), # …even without one
({"format": "txt"}, True),
({"format": "text"}, True),
({"is_image": False, "format": "txt"}, True),
({"is_image": False, "format": "text"}, True),
({"is_image": "true", "format": "py"}, False), # strict === true
({"is_image": False, "format": "py"}, False),
({"is_image": False, "format": "yaml"}, False),
({"is_image": False, "format": "yml"}, False),
({"is_image": False, "format": "json"}, False),
({"is_image": False, "format": "j2"}, False),
({"is_image": False, "format": "container"}, False), # quadlet family
({"is_image": False, "format": "network"}, False),
({"is_image": False, "format": "volume"}, False),
({"is_image": False, "format": "kube"}, False),
({"is_image": False, "format": "pdf"}, False), # phase 165 A4 (deferred)
({"is_image": False, "format": "md"}, False),
({"is_image": False, "format": "markdown"}, False),
({"is_image": False, "format": "dockerfile"}, False),
({"is_image": False, "format": "rst"}, False), # unknown suffix
({"is_image": False, "format": ""}, False),
({"is_image": False}, False), # extension-less (no format at all)
]
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_is_prose_doc_truth_table() -> None:
"""Behavioral (node): the REAL isProseDoc over the full truth
table — a future format cannot drift into prose by accident
(phase 165 D3's pin)."""
js = _read(DOCUMENT_JS)
script = (
_prose_rule(js)
+ "\nconst cases = "
+ json.dumps([c for c, _ in _PROSE_CASES])
+ ";\nconsole.log(JSON.stringify(cases.map((d) => isProseDoc(d))));\n"
)
assert json.loads(_run_node(script)) == [e for _, e in _PROSE_CASES]
# The renderDocument content-branch harness: the minimal fake DOM of
# test_viewer_additive_toggle.py (the house pattern), the real
# docImageBlock standing in for the image arm's first block.
_CONTENT_BRANCH_FAKE_DOM = """
function makeEl(tag) {
return {
tag, className: "", textContent: "", innerHTML: "", attrs: {}, children: [],
setAttribute(k, v) { this.attrs[k] = String(v); },
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; },
appendChild(c) { this.children.push(c); return c; },
append(...cs) { this.children.push(...cs); },
replaceChildren(...cs) { this.children = cs; },
addEventListener() {},
};
}
globalThis.document = { createElement: (t) => makeEl(t) };
function fmtDate(x) { return "DATE:" + x; }
function metaBadge(cls, text, title) {
const s = makeEl("span");
s.className = cls;
s.textContent = text;
if (title !== undefined) s.setAttribute("title", title);
return s;
}
function copyPathButton() { return makeEl("button"); }
function originAnchor() { return makeEl("a"); }
async function docAdminReady() { return false; }
function wireDateEdit() {}
function wireSummaryEdit() {}
function renderMarkdown(s) { return s; }
function docPagesStrip() { return makeEl("div"); }
"""
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
def test_render_document_content_branch_format_table() -> None:
"""Behavioral (node, fake DOM): the REAL renderDocument per
format — txt/text → .doc-prose (the TODO's wrapping ask), md /
markdown → .doc-md (the escape-first renderer, unchanged), and
py/yaml/json/j2/quadlet/pdf/""/unknown → .doc-raw (locked A1/A3:
the code scroller unchanged). An is_image doc lands in
.doc-prose whatever its format — never a doc-raw sibling."""
js = _read(DOCUMENT_JS)
script = (
_CONTENT_BRANCH_FAKE_DOM
+ _prose_rule(js)
+ _fn(js, "renderDocument")
+ _fn(js, "docImageBlock")
+ """
const base = {
title: "T", path: "p", source: "S", content: "BODY", summary: null,
created_at: "c", indexed_at: "i", chunks: 1,
};
function classesFor(doc) {
const contentEl = makeEl("div");
renderDocument(doc, { titleEl: makeEl("h1"), metaEl: makeEl("div"), contentEl });
return contentEl.children.map((c) => c.className);
}
const out = {};
for (const format of ["md", "markdown", "txt", "text", "py", "yaml", "yml",
"json", "j2", "conf", "pdf", "container", "network", "volume", "kube",
"dockerfile", "rst", ""]) {
out[format] = classesFor({ ...base, format });
}
out["image:png"] = classesFor({ ...base, format: "png", is_image: true,
image_url: "/api/documents/x/image" });
console.log(JSON.stringify(out));
"""
)
got = json.loads(_run_node(script))
expected = {
"md": ["doc-md"],
"markdown": ["doc-md"],
"txt": ["doc-prose"],
"text": ["doc-prose"],
"py": ["doc-raw"],
"yaml": ["doc-raw"],
"yml": ["doc-raw"],
"json": ["doc-raw"],
"j2": ["doc-raw"],
"conf": ["doc-raw"],
"pdf": ["doc-raw"], # phase 165 A4: PDF text stays raw (owner call)
"container": ["doc-raw"],
"network": ["doc-raw"],
"volume": ["doc-raw"],
"kube": ["doc-raw"],
"dockerfile": ["doc-raw"],
"rst": ["doc-raw"],
"": ["doc-raw"],
"image:png": ["doc-image", "doc-prose"], # image block first, prose below
}
assert got == expected
def test_doc_prose_css_wraps_in_the_reading_column() -> None:
"""styles.css: the ``.doc-prose`` rule (immediately after
``.doc-raw``) is the .doc-md column with prose rhythm —
``white-space: pre-wrap`` + ``overflow-wrap: anywhere`` (the
TODO's literal no-horizontal-scroll ask) inside the 72rem
``var(--chat-column)`` reading column, phase-08 TOKENS only (zero
color literals — the :root-drift contract; --ink on --surface is
the documented AA pair), no animation. The ``.doc-raw`` rule
keeps the locked code contract (A3): ``white-space: pre`` +
``overflow-x: auto``."""
css = _read(STYLES_CSS)
raw_i = css.find(".doc-raw {")
prose_i = css.find(".doc-prose {")
assert -1 < raw_i < prose_i, ".doc-prose must sit immediately after .doc-raw"
# Provenance comment directly above the rule (house style) — it
# cites the TODO, the defect and the locked decisions.
comment_start = css.rfind("/*", 0, prose_i)
provenance = css[comment_start:prose_i]
assert -1 < comment_start < prose_i
for citation in ("phase 165", "TODO.md L3", ".doc-md", ".doc-raw", "A1", "A2"):
assert citation in provenance, f"the comment must cite {citation!r}"
assert "no animation" in provenance
rule = css[prose_i : css.find("}", prose_i)]
for decl in (
"width: 100%",
"max-width: var(--chat-column)",
"margin-inline: auto",
"background: var(--surface)",
"border: 1px solid var(--line)",
"border-radius: var(--radius)",
"box-shadow: var(--shadow)",
"padding: 1.5rem 1.75rem",
"color: var(--ink)",
"white-space: pre-wrap",
"overflow-wrap: anywhere",
"line-height: 1.55",
):
assert decl in rule, f".doc-prose must carry {decl!r}"
# Tokens only — zero color literals (the :root-drift test parses
# built-ins from this file; no new hue — the monochrome invariant).
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", rule)
assert "rgb(" not in rule and "hsl(" not in rule and "color-mix(" not in rule
# The untouched code scroller (phase 116 locked A1, phase 165 A3).
raw_rule = css[raw_i : css.find("}", raw_i)]
assert "white-space: pre" in raw_rule
assert "overflow-x: auto" in raw_rule