phase: 100_page_width_consistency
Build and Push Containers / build-and-push-app (push) Successful in 4m3s
Build and Push Containers / build-and-push-db (push) Successful in 18s

**Phase 100 — final verification pass: all green.**

- Verified the shipped CSS contract directly: `--chat-column: 72rem` in `:root`; 0 literal `max-width: 46rem`; no `@media (min-width: 1500px)` block; exactly 4 token-capped reading columns; tuning/theme/doc-edit shells cap-free, structurally `.sources-shell`; `mock_llm.py` diff is comment-only.
- Defect found & fixed (phase-93 suite): `test_theme_semantic_completion.py::test_reset_removes_tag_byte_identical` raced theme.js's post-PUT refetch — it asserted the `#bor-theme` tag was gone right after the result line, but tag removal lands in the reconcile after the re-fetch (failed ~1 in 5 runs after `test_chat_rag`, reproduced). Fixed with an auto-waiting `expect(...).to_have_count(0)` (settled-state wait, the file's existing house pattern). 6/6 clean on the repro loop after.
- Tests/lint/coverage: `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/`; `uv run ruff check .` + `uv run pyright` → 0 errors.
- E2E in isolation (all passed): `test_wide_desktop_column.py` 3 (the phase suite — chat==tuning==theme==RAG ±4px at 1280 & 1920, ≈1152px; shared ≈1152px; standalone doc ≈1112px; modal unchanged ≈1100px; 360px overflow-free), `test_ui_customization` 4, `test_admin_theme_tab` 5, `test_document_viewer` 7, `test_save_share_ux` 5, `test_sticky_navbar` 3, `test_markdown_tables` 6, `test_responsive_polish` 7, `test_chat_rag` 3, `test_theme_semantic_completion` 8.
- Completion criteria: ① measured 72rem everywhere (≥~1200px, ±4px) + full-width below + 360px clean — **PASS** (E2E); ② zero 46rem rules / no 1500px block / four token selectors — **PASS** (grep + unit pins); ③ B4 byte-identical no-op + mobile squeeze — **PASS** (theme + responsive suites green); ④ full suite / coverage / lint — **PASS**; ⑤ atomic commit — left to the harness per executor protocol (all changes in the working tree, uncommitted).
- Deviations: none from phase decisions; only change in this pass is the race fix above (test-only, behavior unchanged).
- Next pending phase: `98_sync_summary_visibility` (numeric order in `todo/`; also pending: 99, 101–105).
This commit is contained in:
2026-09-12 13:37:19 -04:00
parent a2ca2f905f
commit 58e9d94cff
35 changed files with 1295 additions and 428 deletions
@@ -0,0 +1,9 @@
**Phase 100 — final verification pass: all green.**
- Verified the shipped CSS contract directly: `--chat-column: 72rem` in `:root`; 0 literal `max-width: 46rem`; no `@media (min-width: 1500px)` block; exactly 4 token-capped reading columns; tuning/theme/doc-edit shells cap-free, structurally `.sources-shell`; `mock_llm.py` diff is comment-only.
- Defect found & fixed (phase-93 suite): `test_theme_semantic_completion.py::test_reset_removes_tag_byte_identical` raced theme.js's post-PUT refetch — it asserted the `#bor-theme` tag was gone right after the result line, but tag removal lands in the reconcile after the re-fetch (failed ~1 in 5 runs after `test_chat_rag`, reproduced). Fixed with an auto-waiting `expect(...).to_have_count(0)` (settled-state wait, the file's existing house pattern). 6/6 clean on the repro loop after.
- Tests/lint/coverage: `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/`; `uv run ruff check .` + `uv run pyright` → 0 errors.
- E2E in isolation (all passed): `test_wide_desktop_column.py` 3 (the phase suite — chat==tuning==theme==RAG ±4px at 1280 & 1920, ≈1152px; shared ≈1152px; standalone doc ≈1112px; modal unchanged ≈1100px; 360px overflow-free), `test_ui_customization` 4, `test_admin_theme_tab` 5, `test_document_viewer` 7, `test_save_share_ux` 5, `test_sticky_navbar` 3, `test_markdown_tables` 6, `test_responsive_polish` 7, `test_chat_rag` 3, `test_theme_semantic_completion` 8.
- Completion criteria: ① measured 72rem everywhere (≥~1200px, ±4px) + full-width below + 360px clean — **PASS** (E2E); ② zero 46rem rules / no 1500px block / four token selectors — **PASS** (grep + unit pins); ③ B4 byte-identical no-op + mobile squeeze — **PASS** (theme + responsive suites green); ④ full suite / coverage / lint — **PASS**; ⑤ atomic commit — left to the harness per executor protocol (all changes in the working tree, uncommitted).
- Deviations: none from phase decisions; only change in this pass is the race fix above (test-only, behavior unchanged).
- Next pending phase: `98_sync_summary_visibility` (numeric order in `todo/`; also pending: 99, 101–105).
@@ -0,0 +1,96 @@
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 10%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 24%]
........................................................................ [ 28%]
........................................................................ [ 31%]
........................................................................ [ 35%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 66%]
........................................................................ [ 70%]
........................................................................ [ 73%]
........................................................................ [ 77%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 94%]
........................................................................ [ 98%]
.................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 181 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 127 1 99%
app/api/git_sources.py 229 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 115 0 100%
app/api/tokens.py 28 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 140 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 66 0 100%
app/models.py 125 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 316 1 99%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/folder_summaries.py 121 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 190 3 98%
app/rag/llm.py 243 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 306 0 100%
--------------------------------------------------
TOTAL 3824 15 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,18 @@
Task 01 complete. All gates verified.
**Implemented** (`frontend/assets/styles.css` only — D3, no HTML/JS/app change):
- `:root { --chat-column: 72rem }` — now equals the `.container` cap; provenance comment cites the owner instruction 2026-09-12 and supersedes the 2026-08-31 instruction
- Entire `@media (min-width: 1500px)` block deleted (92rem doubling + `.doc-shell` cap lift); `prefers-reduced-motion` block kept
- `.tuning-shell` / `.theme-shell` / `.doc-edit-shell`: `max-width: 46rem` + `margin-inline: auto` removed — now structurally match `.sources-shell` (flex column, no cap)
- `.remove-confirm-panel` / `.ignore-editor-panel`: `min(46rem, …)` → `min(var(--chat-column), …)` (the task's hard grep gate required removing the literals; the phase context lists both pin files for task 02's sweep)
- Comment sweep: all stale 46rem/92rem/1500px width claims rewritten to the 72rem contract (0 added lines carry old literals; 36 removed)
**Verification:**
- Width grep clean: `grep -nE "(^|[^.0-9])46rem|(^|[^.0-9])92rem|1500px|…"` → nothing. The task's exact grep only matches 7 `font-size: 0.92rem;` lines — font sizes are non-width CSS, explicitly "do NOT touch"
- Playwright sanity (mock LLM + real app): chat/RAG/tuning/theme all measure **1152px (72rem), centered, at both 1920px and 1280px** — identical to the RAG reference; doc modal panel unchanged at **1100px** (`.doc-md` 1058px, panel binds per D2); standalone doc page 1152px shell / 1112px column; shared page 1152px; **360px: zero horizontal overflow** (chat/tuning/shared scrollWidth = 360). DB restored byte-exact after the check
- `uv run pytest` → 2043 passed, **10 failed — exactly the width pins task 02 rewrites** (`test_wide_column_css.py` ×7, `test_doc_edit_screen.py`, `test_remove_confirm_modal.py`, `test_source_ignore_paths.py`; the other named files carry fixture/docstring text only and stay green)
- `uv run pytest --cov=app` → **99% on `app/`** (>90% gate); `uv run ruff check .` clean; `uv run pyright` → 0 errors, 0 warnings
**Notable decisions:** the two 46rem dialog panels ride the token (only literal-free reading that satisfies the grep gate); `0.92rem` font sizes left untouched per the do-not-touch rule.
**Next pending task:** `02_width_unit_pins.md`
@@ -0,0 +1,295 @@
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 10%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 21%]
.....F.................................................................. [ 24%]
........................................................................ [ 28%]
........................................................................ [ 31%]
........................................................................ [ 35%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 49%]
........................................................F............... [ 52%]
........................................................................ [ 56%]
........................................................................ [ 59%]
........................................................................ [ 63%]
F....................................................................... [ 66%]
........................................................................ [ 70%]
........................................................................ [ 73%]
..................FFF.F.FFF............................................. [ 77%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 94%]
........................................................................ [ 98%]
..................................... [100%]
=================================== FAILURES ===================================
______________ test_doc_edit_shell_is_the_hardcoded_46rem_column _______________
def test_doc_edit_shell_is_the_hardcoded_46rem_column() -> None:
""".doc-edit-shell: the 46rem base column — HARD-CODED 46rem (a
form column, not a reading column — it must NOT ride
--chat-column, so phase 58's wide-desktop doubling never stretches
the form), centered, a flex column on the container frame."""
css = _css()
block = re.search(r"\.doc-edit-shell \{([\s\S]*?)\n\}", css)
assert block, "styles.css must style .doc-edit-shell"
body = block.group(1)
> assert "max-width: 46rem" in body, "the 46rem base column (hard-coded)"
E AssertionError: the 46rem base column (hard-coded)
E assert 'max-width: 46rem' in '\n width: 100%;\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n flex: 1;'
tests/unit/test_doc_edit_screen.py:467: AssertionError
__________________ test_modal_css_targets_and_contrast_pairs ___________________
def test_modal_css_targets_and_contrast_pairs() -> None:
"""The WCAG 2.1 AA basics in CSS: both buttons >=44px; the
destructive button rides the err token family (err-ink on err-bg
9.3:1, the err-line border — the .tuning-delete / .steering-delete
convention; the hover inverts to --bg on --err-line, 5.2:1);
Cancel is the ghost ink-soft family (5.1:1 on --surface); the
error line is the err pair; the panel caps at the 46rem
chat-column width or the viewport."""
css = _css()
btn = css[css.find(".remove-confirm-btn {"):]
btn = btn[: btn.find("\n}")]
assert "min-height: 44px" in btn and "min-width: 44px" in btn
remove = css[css.find(".remove-confirm-remove {"):]
remove = remove[: remove.find("\n}")]
for prop in ("var(--err-bg)", "var(--err-ink)", "var(--err-line)"):
assert prop in remove, f"the destructive pair must keep {prop}"
hover = css[css.find(".remove-confirm-remove:hover:not(:disabled) {"):]
hover = hover[: hover.find("\n}")]
assert "var(--err-line)" in hover and "var(--bg)" in hover, (
"the hover inversion: dark --bg on --err-line (5.2:1)"
)
cancel = css[css.find(".remove-confirm-cancel {"):]
cancel = cancel[: cancel.find("\n}")]
assert "var(--ink-soft)" in cancel and "transparent" in cancel
err = css[css.find(".remove-confirm-error {"):]
err = err[: err.find("\n}")]
assert "var(--err-ink)" in err and "var(--err-bg)" in err
panel = css[css.find(".remove-confirm-panel {"):]
panel = panel[: panel.find("\n}")]
> assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)"
E AssertionError: the 46rem chat-column cap (or the viewport)
E assert 'min(46rem' in '.remove-confirm-panel {\n /* position:relative lifts the panel above the fixed backdrop\n (positioned elements p...d: var(--surface);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n box-shadow: var(--shadow-lg);'
tests/unit/test_remove_confirm_modal.py:476: AssertionError
_________________ test_dialog_button_and_target_contrast_pairs _________________
def test_dialog_button_and_target_contrast_pairs() -> None:
"""The WCAG 2.1 AA basics in CSS: both dialog buttons >=44px;
Cancel is the ghost ink-soft family (5.1:1 on --surface) with the
brand-soft hover (12.4:1); Save is the solid brand family (--bg
text on --brand 5.2:1, the .new-chat-btn convention) with the
lightened hover; the error line is the err pair; the panel caps
at the 46rem chat-column width or the viewport; the visible
label is ink-soft (5.1:1) — never a label-less textarea."""
css = _css()
btn = _css_rule(css, ".ignore-editor-btn")
assert "min-height: 44px" in btn and "min-width: 44px" in btn
cancel = _css_rule(css, ".ignore-editor-cancel")
assert "var(--ink-soft)" in cancel and "transparent" in cancel
cancel_hover = _css_rule(css, ".ignore-editor-cancel:hover:not(:disabled)")
assert "var(--brand-soft)" in cancel_hover and "var(--brand-ink)" in cancel_hover
save = _css_rule(css, ".ignore-editor-save")
assert "var(--brand)" in save and "var(--bg)" in save, (
"Save: the solid brand family (--bg text on --brand, 5.2:1)"
)
save_hover = _css_rule(css, ".ignore-editor-save:hover:not(:disabled)")
assert "background" in save_hover, "the hover lightens the fill"
err = _css_rule(css, ".ignore-editor-error")
assert "var(--err-ink)" in err and "var(--err-bg)" in err
panel = _css_rule(css, ".ignore-editor-panel")
> assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)"
E AssertionError: the 46rem chat-column cap (or the viewport)
E assert 'min(46rem' in '.ignore-editor-panel {\n \n position: relative;\n z-index: 1;\n margin: auto;\n width: min(var(--chat-column), c...var(--surface);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n box-shadow: var(--shadow-lg);\n}'
tests/unit/test_source_ignore_paths.py:520: AssertionError
__________________ test_root_declares_chat_column_46rem_base ___________________
def test_root_declares_chat_column_46rem_base() -> None:
""":root declares --chat-column: 46rem (the PLAN §7 base) with the
owner-provenance comment (instruction 2026-08-31, TODO L5)."""
css = _css()
root = _rule_block(css, ":root")
> assert "--chat-column: 46rem" in root, (
":root must declare the --chat-column base (46rem)"
)
E AssertionError: :root must declare the --chat-column base (46rem)
E assert '--chat-column: 46rem' in '{\n /* Palette — all text/background pairs meet WCAG 2.1 AA (>= 4.5:1) */\n --bg: #0f0a0a; /* page: ink ... — supersedes the 2026-08-31 instruction, and\n the wide-desktop doubling with it). */\n --chat-column: 72rem;\n}'
tests/unit/test_wide_column_css.py:66: AssertionError
___________________ test_wide_media_block_doubles_the_token ____________________
def test_wide_media_block_doubles_the_token() -> None:
"""A @media (min-width: 1500px) block sets --chat-column: 92rem on
:root — the single wide override (2x the base)."""
css = _css()
m = re.search(r"@media \(min-width: 1500px\) \{", css)
> assert m, "styles.css must carry the @media (min-width: 1500px) block"
E AssertionError: styles.css must carry the @media (min-width: 1500px) block
E assert None
tests/unit/test_wide_column_css.py:82: AssertionError
____________ test_wide_block_lives_in_the_bottom_responsive_region _____________
def test_wide_block_lives_in_the_bottom_responsive_region() -> None:
"""The min-width sibling sits alongside the max-width responsive
blocks at the bottom of the file (after the <=640px block)."""
css = _css()
> wide = css.index("@media (min-width: 1500px)")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: substring not found
tests/unit/test_wide_column_css.py:108: ValueError
_____________ test_shared_shell_keeps_the_centered_column_comment ______________
def test_shared_shell_keeps_the_centered_column_comment() -> None:
""".shared-shell's inline comment keeps the "centered chat column"
wording and notes the wide override (task 01 work item)."""
css = _css()
rule = css[css.index(".shared-shell {") : css.index(".shared-shell {") + 400]
> assert "the PLAN §7 centered chat column" in rule
E assert 'the PLAN §7 centered chat column' in '.shared-shell {\n width: 100%;\n max-width: var(--chat-column); /* the centered chat column — 72rem,\n the .con...\'s title; the static\n fallback is "Shared conversation") — the page-head h1 size. */\n#shared-title { margin: 0 0 '
tests/unit/test_wide_column_css.py:143: AssertionError
___________________ test_tuning_shell_stays_hardcoded_46rem ____________________
def test_tuning_shell_stays_hardcoded_46rem() -> None:
""".tuning-shell (the form column, out of scope) keeps its
hard-coded max-width: 46rem at every width — it never widens."""
css = _css()
tuning = _rule_block(css, ".tuning-shell")
> assert "max-width: 46rem" in tuning, (
".tuning-shell must stay hard-coded 46rem (negative pin)"
)
E AssertionError: .tuning-shell must stay hard-coded 46rem (negative pin)
E assert 'max-width: 46rem' in '{\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n flex: 1;\n}'
tests/unit/test_wide_column_css.py:161: AssertionError
__________________ test_no_other_hardcoded_46rem_rule_remains __________________
def test_no_other_hardcoded_46rem_rule_remains() -> None:
"""After the switch, the form columns are the ONLY rules with a
literal max-width: 46rem: .tuning-shell (phase 27),
.doc-edit-shell (phase 59, task 06 — the doc edit screen is a
FORM column, not a reading column, so it must not ride
--chat-column and phase 58's wide-desktop doubling must never
stretch the form), and .theme-shell (phase 91 task 04 — the
admin Theme editor is a form column too: the palette grid +
fieldsets must never ride the wide-desktop doubling). Every
reading column rides the token (the --chat-column base
declaration is the other non-rule occurrence of 46rem)."""
css = _css()
> assert css.count("max-width: 46rem") == 3, (
"only the form columns (.tuning-shell, .doc-edit-shell, "
".theme-shell) may keep a literal max-width: 46rem"
)
E AssertionError: only the form columns (.tuning-shell, .doc-edit-shell, .theme-shell) may keep a literal max-width: 46rem
E assert 0 == 3
E + where 0 = <built-in method count of str object at 0x2673a210>('max-width: 46rem')
E + where <built-in method count of str object at 0x2673a210> = '/* ==========================================================================\n Brain of Reese — design system (no ...ill animate. */\n@media (prefers-reduced-motion: reduce) {\n .app-nav,\n .app-nav.is-open { transition: none; }\n}\n'.count
tests/unit/test_wide_column_css.py:181: AssertionError
_____________ test_comments_cite_the_wide_override_with_provenance _____________
def test_comments_cite_the_wide_override_with_provenance() -> None:
"""The block comments that claimed the "46rem column contract" now
name base 46rem + the 2x wide override, with the owner
instruction (2026-08-31, TODO L5) as the provenance at the token
and the media block."""
css = _css()
# The stale "≤46rem" contract claims are gone from the file.
assert "≤46rem" not in css, (
"the stale '≤46rem' contract wording must be updated"
)
# Provenance at the two authoritative spots (token + wide block).
> token_idx = css.index("--chat-column: 46rem")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E ValueError: substring not found
tests/unit/test_wide_column_css.py:201: ValueError
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 181 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 127 1 99%
app/api/git_sources.py 229 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 115 0 100%
app/api/tokens.py 28 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 140 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 66 0 100%
app/models.py 125 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 316 1 99%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/folder_summaries.py 121 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 190 3 98%
app/rag/llm.py 243 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 306 0 100%
--------------------------------------------------
TOTAL 3824 15 99%
=========================== short test summary info ============================
FAILED tests/unit/test_doc_edit_screen.py::test_doc_edit_shell_is_the_hardcoded_46rem_column
FAILED tests/unit/test_remove_confirm_modal.py::test_modal_css_targets_and_contrast_pairs
FAILED tests/unit/test_source_ignore_paths.py::test_dialog_button_and_target_contrast_pairs
FAILED tests/unit/test_wide_column_css.py::test_root_declares_chat_column_46rem_base
FAILED tests/unit/test_wide_column_css.py::test_wide_media_block_doubles_the_token
FAILED tests/unit/test_wide_column_css.py::test_wide_block_lives_in_the_bottom_responsive_region
FAILED tests/unit/test_wide_column_css.py::test_shared_shell_keeps_the_centered_column_comment
FAILED tests/unit/test_wide_column_css.py::test_tuning_shell_stays_hardcoded_46rem
FAILED tests/unit/test_wide_column_css.py::test_no_other_hardcoded_46rem_rule_remains
FAILED tests/unit/test_wide_column_css.py::test_comments_cite_the_wide_override_with_provenance
tests FAILED: unit + integration suite did not pass
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation FAILED (see output above)
@@ -0,0 +1,13 @@
Everything is green. Final report:
**Fixed:** the harness gate requires a fully green suite, so the old-contract width pins (which asserted the retired 46rem/92rem contract) were updated to the new 72rem contract — `test_wide_column_css.py` rewritten (token = 72rem = `.container` cap with 2026-09-12 provenance; no `min-width:1500px` block; flipped negative pin: zero literal `max-width: 46rem`; the three form shells structurally match `.sources-shell`; both dialogs pinned at `min(var(--chat-column), …)`; no stale width comments), plus the single pin + docstrings in `test_doc_edit_shell` (no cap, rides the container), `test_remove_confirm_modal` and `test_source_ignore_paths` (`min(var(--chat-column)`). `styles.css` from the prior attempt is unchanged (already verified: 1152px columns on every page at 1280/1920, modal panel unchanged 1100px, no 360px overflow).
**Results (exact commands):**
- `uv run pytest` → **2052 passed, 0 failed**
- `uv run pytest --cov=app --cov-report=term` → **app/ 99%** (>90% gate)
- `uv run ruff check .` → **All checks passed!**
- `uv run pyright` → **0 errors, 0 warnings**
**Notable decisions:** the harness rejection overrode the task file's "expected breakage" clause — the pin updates (task 02's unit-pin scope) landed here to satisfy the green gate; task 02's remaining work is only the docstring sweep of `test_markdown_tables` / `test_shared_page` / `test_pinned_composer` (fixture text, per pin judgment). No HTML/JS/app change (D3); DB left as found.
**Next pending task:** `02_width_unit_pins.md` (unit-pin scope now largely satisfied — residual docstring sweep remains)
@@ -0,0 +1,96 @@
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 10%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 24%]
........................................................................ [ 28%]
........................................................................ [ 31%]
........................................................................ [ 35%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 66%]
........................................................................ [ 70%]
........................................................................ [ 73%]
........................................................................ [ 77%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 94%]
........................................................................ [ 98%]
.................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 181 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 127 1 99%
app/api/git_sources.py 229 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 115 0 100%
app/api/tokens.py 28 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 140 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 66 0 100%
app/models.py 125 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 316 1 99%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/folder_summaries.py 121 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 190 3 98%
app/rag/llm.py 243 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 306 0 100%
--------------------------------------------------
TOTAL 3824 15 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,14 @@
**Task 02 — unit pins for the 72rem width contract — complete.**
**Implemented**
- `test_wide_column_css.py` rewritten to the phase-100 contract (kept the balanced-brace `_rule_block` helpers): 72rem token + 2026-09-12 provenance + token-equals-`.container` pin; no `@media (min-width: 1500px)` block and no `92rem`; the four reading-column selectors use the token (`count == 4`); flipped negative pin — zero literal `max-width: 46rem`; `.tuning-shell`/`.theme-shell`/`.doc-edit-shell` cap-free, token-free, `margin-inline`-free with the `.sources-shell` shape; dialog panels ride `min(var(--chat-column), calc(100vw - 2rem))`; stale comment claims gone, 72rem named at chat/shared shells.
- Grep sweep (`46rem|92rem|chat-column` under `tests/unit/`) judged per pin: `test_doc_edit_screen.py` (shell cap-free pin), `test_remove_confirm_modal.py` + `test_source_ignore_paths.py` (dialog `min(var(--chat-column)` pins), `test_markdown_tables.py`, `test_shared_page.py` (docstrings + token pin kept; its "no 46rem" negative pin still valid), `test_pinned_composer.py` (docstring) all updated to the new contract; no fixture text touched. Every remaining reference is a new-contract pin or description.
**Results**
- `uv run pytest tests/unit/ -q` → green (exit 0; 8/8 in the rewritten file)
- `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, 1 warning; `app/` coverage **99%** (>90% gate)
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
**Decisions** — dialog panels (remove-confirm, ignore-editor) were moved onto the token by task 01, so their unit pins now assert the token form (behavior change is task 01's, already validated); no `app/`/HTML/JS touched. No defects found in prior work.
**Next pending task:** `.agents/phases/todo/100_page_width_consistency/03_e2e_width_sweep.md`
@@ -0,0 +1,96 @@
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 10%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 24%]
........................................................................ [ 28%]
........................................................................ [ 31%]
........................................................................ [ 35%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 66%]
........................................................................ [ 70%]
........................................................................ [ 73%]
........................................................................ [ 77%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 94%]
........................................................................ [ 98%]
.................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 181 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 127 1 99%
app/api/git_sources.py 229 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 115 0 100%
app/api/tokens.py 28 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 140 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 66 0 100%
app/models.py 125 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 316 1 99%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/folder_summaries.py 121 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 190 3 98%
app/rag/llm.py 243 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 306 0 100%
--------------------------------------------------
TOTAL 3824 15 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
@@ -0,0 +1,18 @@
**Phase 100, task 03 complete — all green.**
**Implemented**
- Rewrote `tests/e2e/test_wide_desktop_column.py` to the measured phase-100 contract: chat == tuning == theme == RAG shells (±4px) at 1280×800 AND 1920×1080 (each ≈1152px, the 72rem container); modal unchanged (panel ~1100px, inner `.doc-md` ~1058px); shared `.shared-shell` ≈1152px; standalone `document.html` `.doc-md` ≈1112px; 360px overflow-free, 900px full-width — seeding helpers kept. Verified pins are discriminating (temp 46rem token → all 3 fail; CSS restored).
- Updated in place: `test_document_viewer.py` (standalone `.doc-md` ≤736px pin → ≈1112px), `test_responsive_polish.py` (92rem/46rem pins + constants → 72rem container); comment-only fixes in `test_save_share_ux.py`, `test_sticky_navbar.py`, `test_markdown_tables.py`, `mock_llm.py`.
**Test / lint / coverage (exact commands)**
- `uv run pytest tests/e2e/test_wide_desktop_column.py -v --no-cov` → 3 passed (in isolation)
- Updated suites in isolation: document_viewer 7, save_share_ux 5, sticky_navbar 3, markdown_tables 6, responsive_polish 7 — all passed
- Theme no-op UNCHANGED: `test_ui_customization.py` 4, `test_admin_theme_tab.py` 5 — all passed
- Regression: `test_chat_rag.py` 3, `test_theme_semantic_completion.py` 8 — all passed
- `uv run pytest --cov=app --cov-report=term-missing` → 2052 passed, **99%** on `app/` (>90%)
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings
**Decisions**
- Per harness protocol I did NOT move the phase dir or commit — all changes left in the working tree for the harness's single atomic phase commit (task-file step 5's commit/move handled by harness).
**Next pending task:** phase 100 is fully done; next in numeric order is `101_tokens_page_overhaul` (task 01).
@@ -0,0 +1,96 @@
........................................................................ [ 3%]
........................................................................ [ 7%]
........................................................................ [ 10%]
........................................................................ [ 14%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 24%]
........................................................................ [ 28%]
........................................................................ [ 31%]
........................................................................ [ 35%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 45%]
........................................................................ [ 49%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 66%]
........................................................................ [ 70%]
........................................................................ [ 73%]
........................................................................ [ 77%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 94%]
........................................................................ [ 98%]
.................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 181 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 13 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 127 1 99%
app/api/git_sources.py 229 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 115 0 100%
app/api/tokens.py 28 0 100%
app/api/ui_settings.py 55 0 100%
app/config.py 140 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 124 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 20 0 100%
app/core/theming.py 38 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 66 0 100%
app/models.py 125 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 316 1 99%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/folder_summaries.py 121 0 100%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 190 3 98%
app/rag/llm.py 243 1 99%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 306 0 100%
--------------------------------------------------
TOTAL 3824 15 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
validation OK
+71 -78
View File
@@ -45,10 +45,13 @@
--header-h: 64px;
/* Reading column: 46rem base (PLAN §7 lineage); 2x on wide desktops
(owner instruction 2026-08-31, TODO L5 / D2 — chat + shared +
document view). */
--chat-column: 46rem;
/* Reading column: 72rem — EQUAL to the .container's 72rem cap, so
the reading columns (chat, shared, the document viewer's .doc-md
+ its summary panel) ride the same width as every other page
(owner instruction 2026-09-12: "match the width of the RAG page
for all other pages" — supersedes the 2026-08-31 instruction, and
the wide-desktop doubling with it). */
--chat-column: 72rem;
}
* { box-sizing: border-box; }
@@ -389,8 +392,11 @@ body::before {
/* Chat is a vertical conversation: a centered, capped column is the
correct layout here (PLAN §UI/UX). The surrounding frame keeps it
from collapsing into a hairline on wide screens. The cap is the
--chat-column token: 46rem base, 2x (92rem) at >=1500px wide
desktops (owner instruction 2026-08-31, TODO.md L5 / D2). */
--chat-column token: 72rem — equal to the .container's cap, so the
chat column reads at the RAG page's full container width at every
viewport (owner instruction 2026-09-12: "match the width of the
RAG page for all other pages" — the wide-desktop doubling was
retired with it). */
.chat-shell {
max-width: var(--chat-column);
margin-inline: auto;
@@ -409,8 +415,8 @@ body::before {
flips this to a vertical stack (flex-direction: column +
align-items: stretch — full-width pills, New chat above Share; the
existing ≤640px pill rules apply to the stacked pills unchanged).
The reading-column contract is untouched (--chat-column: 46rem base,
92rem at >=1500px — PLAN §7 lineage).
The reading-column contract is untouched (--chat-column: 72rem —
the .container width at every viewport).
Phase 65 (task 03, 2026-09-01, `TODO.md` L3, owner-locked A2): the
row is the TOP member of the pinned .chat-bottom cluster (task
@@ -504,8 +510,9 @@ body::before {
/* GFM pipe tables (phase 44, 2026-08-27, TODO.md L6): the shared
renderer wraps every table in .md-table-wrap — the horizontal
scroller, so a wide table scrolls inside the bubble instead of
breaking the reading column (--chat-column: 46rem base, 92rem at
>=1500px) — around a semantic <table class="md-table">
breaking the reading column (--chat-column: 72rem — the
.container width at every viewport) — around a semantic
<table class="md-table">
(escape-first cells; alignment colons render left, owner decision).
Phase-08 tokens only: --line hairline borders and the thead tinted
from the plain surface family — --ink on --surface is 14.5:1 (PLAN
@@ -603,8 +610,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
--surface ≈10.4:1 (11.6:1 on the page bg), and --ink on --brand-soft
in the path `code` ≈11.5:1 — all comfortably AA in the (single dark)
theme. Inline rows only: appending lines never shifts the chat
column (46rem base; 92rem at >=1500px — no new container), and the
rows are not interactive — no focus targets. */
column (72rem — the .container width at every viewport; no new
container), and the rows are not interactive — no focus targets. */
.tool-calls {
display: flex;
flex-direction: column;
@@ -960,15 +967,16 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Global tuning page (phase 27) ---------- */
/* /tuning.html: create / edit / delete steering notes without a chat
conversation. Same width discipline as the chat column — a centered,
capped column on the 72rem frame; the form and the note list span its
FULL width (no skinny lists). Every interactive target is >=44px;
text pairs reuse the Phase-08 AA palette (dark ink on brand 5.2:1,
brand-ink/brand-soft 6.9:1, ok 10.6:1, err 9.1:1, ink-soft >=6.9:1).
No filter: blur, no CDN, system font stack. */
conversation. Full-container width (owner instruction 2026-09-12:
"match the width of the RAG page for all other pages" — the
phase-27 capped-column discipline is superseded): the shell rides
the .container's 72rem frame like the RAG page, and the form and
the note list span its FULL width (no skinny lists). Every
interactive target is >=44px; text pairs reuse the Phase-08 AA
palette (dark ink on brand 5.2:1, brand-ink/brand-soft 6.9:1,
ok 10.6:1, err 9.1:1, ink-soft >=6.9:1). No filter: blur, no CDN,
system font stack. */
.tuning-shell {
max-width: 46rem;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1.25rem;
@@ -1241,8 +1249,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Composer ---------- */
/* Phase 52 (2026-08-30, TODO.md L3): the composer is PINNED to the
viewport bottom. The page scrolls at the document level and
`.chat-shell` (the centered reading column — 46rem base, 92rem at
>=1500px) is the composer's sticky
`.chat-shell` (the centered reading column — 72rem, the
.container width at every viewport) is the composer's sticky
containing block, so the box sticks to the bottom edge of the
viewport at every scroll position and settles back into its normal
flow position (above the footer) once the document bottom is
@@ -2265,8 +2273,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
a fixed full-viewport dim backdrop + a centered panel (z-index
1000, above the sticky header (20) + skip-link (100); NO blur —
the phase-08 no-blur perf anchor), scaled to a compact dialog:
the 46rem chat-column width or the viewport, whichever is
narrower. Phase-08 tokens only; system fonts; no CDN.
the chat-column width (the --chat-column token — 72rem, the
.container width) or the viewport, whichever is narrower.
Phase-08 tokens only; system fonts; no CDN.
AA pairs: title/copy are --ink on --surface (13.8:1); the source
value is --ink on --bg (16.7:1); the error line is the err pair
@@ -2302,7 +2311,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
position: relative;
z-index: 1;
margin: auto;
width: min(46rem, calc(100vw - 2rem));
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
@@ -2405,7 +2414,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
#remove-confirm-dialog overlay contract (phase 69): a fixed
full-viewport dim backdrop + a centered panel (z-index 1000, above
the sticky header (20) + skip-link (100); NO blur — the phase-08
no-blur perf anchor), scaled to the 46rem chat-column width or the
no-blur perf anchor), scaled to the chat-column width (the
--chat-column token — 72rem, the .container width) or the
viewport, whichever is narrower. The box: a VISIBLE block label
(WCAG — never aria-label-only) over a mono textarea (the box is a
data entry, not prose — the var(--mono) stack); the error line is
@@ -2442,7 +2452,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
position: relative;
z-index: 1;
margin: auto;
width: min(46rem, calc(100vw - 2rem));
width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
@@ -3168,8 +3178,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Theme view (phase 91, tasks 04 + 05) ----------
The shell's seventh view (#view-theme): the admin palette + branding
editor (task 05). The centered 46rem column (the .tuning-shell
language — this is a form view, the tuning-view width pattern), the
editor (task 05). The full 72rem container width (owner
instruction 2026-09-12 — the phase-91 form-column cap is
superseded), the
form card (the #tune-form language: surface fill, --line hairline,
radius, shadow), the fieldset groups (Branding / Palette) with the
.theme-note sub-copy, the 3-column (desktop) / 1-column (mobile)
@@ -3184,8 +3195,6 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible
via the global 3px outline rule. No CDN, system fonts. */
.theme-shell {
max-width: 46rem;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1.25rem;
@@ -3347,12 +3356,13 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Shared page (phase 51, task 03) ----------
/shared/<token>: the anonymous read-only conversation (owner-locked
2026-08-29, TODO.md L6). The shell maps to the PLAN §7 centered
chat column (--chat-column: 46rem base, 92rem at >=1500px) — the
conversation reads exactly like the chat
page (the .msg/.bubble/.thinking/.tool-calls/.msg-meta rules apply
unchanged) with NO composer, so the column contract holds for a
guest. Zero interactive controls (owner-locked): the chips are
2026-08-29, TODO.md L6). The shared page reads exactly like the
chat page — both at the 72rem container width (--chat-column, now
equal to the .container cap — owner instruction 2026-09-12; the
wide-desktop doubling was retired) — and the .msg/.bubble/.thinking/
.tool-calls/.msg-meta rules apply unchanged, with NO composer, so
the column contract holds for a guest. Zero interactive controls
(owner-locked): the chips are
plain text, so the pill families' pointer treatments are switched
off IN THIS SCOPE ONLY — the chat page's interactive chips keep
their styles untouched. Every pair reuses the Phase-08 AA palette
@@ -3361,8 +3371,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
fonts. */
.shared-shell {
width: 100%;
max-width: var(--chat-column); /* the PLAN §7 centered chat column
(46rem base; 92rem at >=1500px — phase 58) */
max-width: var(--chat-column); /* the centered chat column — 72rem,
the .container width at every viewport */
margin-inline: auto;
display: flex;
flex-direction: column;
@@ -3525,9 +3535,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
#doc-content { display: flex; flex-direction: column; }
.doc-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); }
/* Markdown: the centered reading column — 46rem base, 2x (92rem) at
>=1500px (PLAN §7.1 lineage; owner instruction 2026-08-31, TODO.md
L5 / D2). */
/* Markdown: the centered reading column — 72rem, the .container width
at every viewport (owner instruction 2026-09-12: "match the width
of the RAG page for all other pages" — the wide-desktop doubling
was retired). */
.doc-md {
width: 100%;
max-width: var(--chat-column);
@@ -3576,8 +3587,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
overflow-wrap: anywhere;
}
/* md/markdown: the panel matches the .doc-md centered reading column
(46rem base; 92rem at >=1500px — the same --chat-column token) —
it is the column's label. Raw formats stay full width (the
(72rem — the same --chat-column token, the .container width at
every viewport) — it is the column's label. Raw formats stay full width (the
.doc-raw default above), matching the full-width pre; in engines
without :has() the panel degrades to that full-width default. */
.doc-summary:has(+ .doc-md) {
@@ -3743,8 +3754,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Document modal (phase 26) ----------
"New documents should open in an almost-fullscreen modal, not in a new
page" (TODO.md L4). The overlay reuses the viewer page's .doc-meta
badge classes, the .doc-md reading column (46rem base; 92rem at
>=1500px), and the .doc-raw
badge classes, the .doc-md reading column (72rem — the .container
width; inside the modal the 1100px panel stays its effective
ceiling), and the .doc-raw
pre — this block only adds the chrome (backdrop, panel, header,
actions, scroll container). Phase-08 tokens only; NO blur (the
phase-08 no-blur perf anchor); no new assets; system fonts.
@@ -3886,9 +3898,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
}
/* The scroll container: vertical scroll lives HERE, never the viewport.
.doc-md keeps its centered reading column inside (46rem base;
92rem at >=1500px, capped there by the 1100px panel); .doc-raw keeps
its own overflow-x. tabindex="-1" in the markup is the JS focus target. */
.doc-md keeps its centered reading column inside (72rem — wider
than the 1100px panel's inner width, which stays its effective
ceiling); .doc-raw keeps its own overflow-x. tabindex="-1" in the
markup is the JS focus target. */
.doc-modal-content {
flex: 1;
min-height: 0;
@@ -3906,16 +3919,14 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/doc-edit.html: the admin-gated edit screen for a doc draft (title,
in-repo path, markdown body) — a FLOW page, not one of the app's
pages, so the header is SLIM (brand + "← Back to chat" only). The
46rem base column is HARD-CODED: a form column, not a reading
column — it does not ride --chat-column, so phase 58's wide-desktop
doubling never stretches the form. The .sources-gate gate is reused
verbatim (phases 16/35/50). Every pair reuses the Phase-08 AA
palette; touch targets >=44px; :focus-visible via the global 3px
outline rule. No CDN, system fonts. */
shell rides the full 72rem container like every other page (owner
instruction 2026-09-12 — the phase-59 form-column cap is
superseded). The .sources-gate gate is reused verbatim
(phases 16/35/50). Every pair reuses the Phase-08 AA palette;
touch targets >=44px; :focus-visible via the global 3px outline
rule. No CDN, system fonts. */
.doc-edit-shell {
width: 100%;
max-width: 46rem; /* the 46rem base column (hard-coded — see above) */
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 1.25rem;
@@ -4385,9 +4396,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
.theme-reset { width: 100%; }
/* Phase 51: the shared page squeezes like the chat column — the
title and the note step down (the empty-state-title family); the
shell keeps its base 46rem column (the >=1500px 92rem override
never applies here — it is already the narrowest box on the
page) and .msg-body's 92% override above applies. */
shell is full-width here (the 72rem cap never binds on a phone —
the container is already 100%) and .msg-body's 92% override
above applies. */
#shared-title { font-size: 1.35rem; }
.shared-note { font-size: 0.88rem; }
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
@@ -4418,24 +4429,6 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
}
}
/* Phase 58: wide desktops (viewport >=1500px) read the 2x column —
--chat-column doubles to 92rem for the four reading shells (chat,
shared, the document viewer's .doc-md and its summary panel). The
chat and shared shells ARE their .container (the token max-width
overrides the .container's 72rem cap — same specificity, later in
the file), but the document page's .container.doc-shell WRAPS the
column, so the 72rem cap would bind first and pin .doc-md at
~1112px: the wide block lifts the shell's cap to the column plus
the container's two 1.25rem gutters, letting .doc-md's own 92rem
cap bind (1472px at the 16px root). Everything below 1500px
renders exactly as before, and .tuning-shell (a form, not a
reading surface) keeps its hard-coded 46rem at every width
(owner instruction 2026-08-31, TODO.md L5 / D2). */
@media (min-width: 1500px) {
:root { --chat-column: 92rem; }
.doc-shell { max-width: calc(var(--chat-column) + 2 * 1.25rem); }
}
/* Phase 46: prefers-reduced-motion stills the mobile menu — no
180ms slide+fade; open/close snaps (the visibility/opacity flip
applies instantly) and stays correct. BOTH states are named: the
+6 -3
View File
@@ -327,7 +327,8 @@ Implements just enough of the aipi surface:
a deliberately wide 5-column table — byte-stable, so the story E2E
can assert the rendered ``<table class="md-table">`` shape, the
escaped XSS line, and the wrapper's horizontal scroll inside the
46rem column. Checked BEFORE the ``DEFLECT_MODE`` branch (a
72rem container column (phase 100). Checked BEFORE the
``DEFLECT_MODE`` branch (a
deflection prompt never carries the marker, same reasoning as
``SUMMARY_MODE``), so a marker question always gets the table
answer; the E2E asks it against an on-topic fixture (HIGH gate) and
@@ -584,7 +585,8 @@ TABLE_TRIGGER = "show me a table"
#: The fixed table answer (phase 44) — byte-stable on purpose: the story
#: E2E asserts the rendered table shape, the escaped ``<img onerror>``
#: line (the XSS payload must survive the mock byte-for-byte), and the
#: wide table's ``scrollWidth > clientWidth`` inside the 46rem column.
#: wide table's ``scrollWidth > clientWidth`` inside the 72rem container
#: column (phase 100).
#: Phase 74 (chat history, TODO L4): a user message containing this
#: substring (case-insensitive) gets the deterministic HISTORY ECHO
#: (``_history_echo`` below — see the module docstring): the prior-turn
@@ -1653,7 +1655,8 @@ def compose_answer(body: dict[str, Any]) -> str:
# <img onerror> XSS probe line (it must survive the mock
# byte-for-byte so the E2E can prove the renderer neutralizes
# it), and a wide 5-column table (guarantees scrollWidth >
# clientWidth inside the 46rem column). Byte-stable. Checked
# clientWidth inside the 72rem container column, phase 100).
# Byte-stable. Checked
# BEFORE the DEFLECT_MODE branch: a deflection prompt never
# carries the marker (it lives in the user message, same
# reasoning as SUMMARY_MODE), so a marker question always gets
+12 -3
View File
@@ -24,7 +24,9 @@ Test → story mapping (Playwright Mapping Rule):
renders as escaped text; no dialog fires.
6. ``test_standalone_page_still_works`` — the dedicated ``/document.html``
page keeps its phase-10 contract (title/content/badges, not-found,
dark theme, no-CDN, a11y frame, ≤736px md column).
dark theme, no-CDN, a11y frame, the 72rem-container md column —
phase 100: ≈1112px at the 1280px fixture viewport, the container's
inner content).
7. ``test_modal_theme_and_no_cdn`` — dark page background, the panel on
the Phase-08 surface colour, every asset same-origin or ``data:``.
"""
@@ -406,9 +408,16 @@ def test_standalone_page_still_works(
expect(page.locator(".doc-shell")).to_have_attribute("aria-live", "polite")
assert page.evaluate("() => document.activeElement && document.activeElement.id") == "main"
# Markdown column centered and capped at 46rem (736px at 16px root).
# Markdown column: the 72rem container's inner content (phase 100 —
# the 46rem cap and its wide-desktop doubling are retired): at the
# 1280px fixture viewport the container is 1152px border-box, so
# .doc-md (width:100% inside it) measures 1152 − 2×1.25rem = 1112px.
box = page.locator("#doc-content .doc-md").bounding_box()
assert box is not None and box["width"] <= 736 + 1
assert box is not None, "the standalone .doc-md column is not rendered"
assert abs(box["width"] - 1112) <= 4, (
f"the standalone .doc-md column is {box['width']:.0f}px, "
f"want 1112px (the 72rem container's inner content) ±4px"
)
assert errors == [], f"console crashes: {errors}"
+4 -3
View File
@@ -22,7 +22,8 @@ Test → story mapping (Playwright Mapping Rule):
NOT deflected (the honesty-gate interplay is part of the contract).
2. ``test_wide_table_scrolls`` — the wide table's wrapper has
``scrollWidth > clientWidth`` and horizontal scroll moves it; the
page itself has no horizontal overflow (the 46rem column holds).
page itself has no horizontal overflow (the 72rem container column
holds — phase 100).
3. ``test_table_xss_safe`` — the ``<img onerror>`` line renders as
visible, escaped text: zero injected ``<img>`` nodes, no dialog.
4. ``test_viewer_table_renders`` — the fixture's pipe table opens from
@@ -219,8 +220,8 @@ def test_wide_table_scrolls(
after = wrap.evaluate("el => el.scrollLeft")
assert after > before, "the wrapper must scroll horizontally"
# The 46rem chat column must not break the page: no horizontal
# document overflow (PLAN §7.1).
# The 72rem container column must not break the page: no horizontal
# document overflow (PLAN §7.1, as revised by phase 100).
page_scroll, page_client = page.evaluate(
"() => [document.documentElement.scrollWidth, document.documentElement.clientWidth]"
)
+25 -18
View File
@@ -12,12 +12,13 @@ Test → story mapping:
1. ``test_no_horizontal_overflow_at_viewports`` — 360/375/768/1280/1600 on
both pages: ``documentElement.scrollWidth <= clientWidth``.
2. ``test_chat_column_capped_and_centered`` — the reading column rides
--chat-column (46rem base; 92rem at >=1500px, phase 58 / owner
instruction 2026-08-31 TODO L5): at 1600px (a wide desktop) the
``.chat-shell`` is 92rem (1472px, ±2%) and horizontally centered
(±2%); at 1280px (below the wide breakpoint) it stays ≤ 46rem
(736px, +2%); at 768px the column uses most of the width (no
mid-column dead zones).
--chat-column (72rem — EQUAL to the .container's cap at every
viewport, phase 100 / owner instruction 2026-09-12: the 46rem base
+ the >=1500px 92rem doubling are retired): at 1600px (a wide
desktop) the ``.chat-shell`` is the 72rem container (1152px, ±2%)
and horizontally centered (±2%); at 1280px the same 1152px (the
container cap binds, not the viewport); at 768px the column is
full-width (no mid-column dead zones).
3. ``test_sources_table_full_width`` — at 1280px ``.table-wrap`` ≥ 80% of
the container; below 640px the table keeps its 640px min-width and the
wrapper scrolls horizontally instead of squeezing.
@@ -55,8 +56,11 @@ REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "docs"
VIEWPORTS = ((360, 740), (375, 812), (768, 1024), (1280, 800), (1600, 900))
CHAT_SHELL_CAP_PX = 46 * 16 # 736px — the --chat-column base (PLAN §7.1 lineage)
CHAT_SHELL_WIDE_PX = 92 * 16 # 1472px — the 2x wide override (phase 58, >=1500px)
# Phase 100: the ONE width — the 72rem container, border-box (the
# 2×1.25rem gutters are inside the measured box). The 46rem base and
# the 92rem wide-desktop doubling are retired (owner instruction
# 2026-09-12: "match the width of the RAG page for all other pages").
CHAT_SHELL_CONTAINER_PX = 72 * 16 # 1152px — the --chat-column = container cap
# Mock-LLM marker for a 3s pre-token window (see tests/e2e/mock_llm.py).
SLOW_QUESTION = "pretend to think slowly, please"
@@ -225,18 +229,19 @@ def test_no_horizontal_overflow_at_viewports(
def test_chat_column_capped_and_centered(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""AC1 (phase 58 contract): the reading column doubles to 92rem on
wide desktops (>=1500px — 1600px here), stays at the 46rem base
below the breakpoint (1280px), and still uses most of the width on
tablets (no mid-column dead zones)."""
"""AC1 (phase 100 contract): the reading column is the 72rem
container at every viewport — 1600px here (the cap binds, not the
viewport) and 1280px alike (1152px, ±2%, centered), and still
full-width on tablets (no mid-column dead zones). The phase-58
46rem/92rem contract it used to pin is retired."""
page = browser.new_page(viewport={"width": 1600, "height": 900})
try:
page.goto(f"{app_url}/")
box = page.locator(".chat-shell").bounding_box()
assert box is not None
assert CHAT_SHELL_WIDE_PX * 0.98 <= box["width"] <= CHAT_SHELL_WIDE_PX * 1.02, (
f"at 1600px (>=1500px) the chat column is {box['width']:.0f}px, "
f"not the 92rem wide override (±2%)"
assert CHAT_SHELL_CONTAINER_PX * 0.98 <= box["width"] <= CHAT_SHELL_CONTAINER_PX * 1.02, (
f"at 1600px the chat column is {box['width']:.0f}px, "
f"not the 72rem container (1152px, ±2%)"
)
center = box["x"] + box["width"] / 2
assert abs(center - 1600 / 2) <= 0.02 * 1600, (
@@ -250,9 +255,11 @@ def test_chat_column_capped_and_centered(
narrow.goto(f"{app_url}/")
box = narrow.locator(".chat-shell").bounding_box()
assert box is not None
assert box["width"] <= CHAT_SHELL_CAP_PX * 1.02, (
f"at 1280px (<1500px) the chat column {box['width']:.0f}px exceeds "
f"the 46rem base cap (+2%)"
# 1280 > 1152: the container cap binds — the SAME 72rem width as
# at 1600px (the cap no longer depends on the viewport at all).
assert box["width"] <= CHAT_SHELL_CONTAINER_PX * 1.02, (
f"at 1280px the chat column {box['width']:.0f}px exceeds "
f"the 72rem container cap (+2%)"
)
finally:
narrow.close()
+3 -2
View File
@@ -41,7 +41,8 @@ unshare, layout) are unchanged.
* **Layout (L6 / A5)** — desktop 1280×800: ``#new-chat-btn`` and
``#share-chat-btn`` share one horizontal row inside ``.chat-actions``
(overlapping y bands, Share's x beyond New chat's x + width, each
pill at intrinsic width — never the full 46rem column); mobile
pill at intrinsic width — never the full 72rem container column);
mobile
390×844: stacked vertically (Share below New chat, full-width);
360px wide: no horizontal page overflow;
* **Admin still works (A1 sanity)** — the same auto-save machinery
@@ -502,7 +503,7 @@ def test_action_row_layout(page: Page, app_url: str, mock_llm: int, db_ready: No
# Desktop (1280×800): one horizontal row — overlapping y bands,
# Share to the right of New chat, each pill at its INTRINSIC width
# (never the full 46rem chat column).
# (never the full 72rem container column — phase 100).
nb = new_btn.bounding_box()
sb = share_btn.bounding_box()
assert nb is not None and sb is not None
+1 -1
View File
@@ -265,7 +265,7 @@ def test_doc_header_stuck_at_top_on_long_document(
expect(page.locator("#doc-content .doc-md")).not_to_be_empty()
# The doc must actually scroll (the ~40,000-char body fills many
# viewports in the 46rem column).
# viewports in the 72rem container column — phase 100).
sh = page.evaluate("() => document.documentElement.scrollHeight")
assert sh > VIEWPORT_H, (
f"the long document must make the page scroll "
+5 -4
View File
@@ -979,10 +979,11 @@ def test_reset_removes_tag_byte_identical(page: Page, app_url: str, db_ready: No
# The tag is GONE from the live document (theme.js reconciles the
# #bor-theme tag's DOM text to the settled, now-default values —
# the no-op case removes the tag and clears the <html>
# overrides)…
assert 'id="bor-theme"' not in page.content(), (
"the reset document must be tag-free"
)
# overrides). The removal lands in the post-PUT refetch's
# reconcile (the result line above only signals the PUT landed),
# so WAIT for the settled tag-free state rather than racing the
# refetch round-trip…
expect(page.locator("#bor-theme")).to_have_count(0, timeout=30_000)
# …and a FRESH load serves NO tag at all (the all-NULL row is the
# no-op injection).
r = httpx.get(app_url + "/", timeout=10)
+227 -154
View File
@@ -1,16 +1,33 @@
"""Phase 58 E2E (Playwright): the 2x reading column on wide desktops —
measured bounding-box widths, not CSS pins.
"""Phase 100 E2E (Playwright): every page matches the RAG page's width —
the full 72rem container at every viewport, measured bounding boxes.
TODO.md L5 (owner instruction 2026-08-31, roadmap confirmation D2 +
expansion): "The chat response needs to be 2x wider on wide desktops.
there's a lot of unused space." — extended to the document view. The
owner-locked contract: viewport >=1500px doubles ``--chat-column`` to
92rem (1472px at the 16px root) for the four reading shells —
``.chat-shell``, ``.shared-shell``, ``.doc-md`` and
``.doc-summary:has(+ .doc-md)`` — while everything below the breakpoint
renders exactly as before (46rem / 736px) and ``.tuning-shell`` (a
form, not a reading surface) never widens (CSS-pinned by the unit
suite, task 01 — the browser proof of the MEASURED width is this file).
Owner request (chat, 2026-09-12): "The theme, tuning, and chat pages
are still pretty narrow, I want you to match the width of the RAG page
for all other pages to keep things consistent." — SUPERSEDES the
phase-58 2026-08-31 46rem/92rem reading-column contract (recorded per
the phase-94/96/97 convention in `.agents/phases/todo/
100_page_width_consistency/00_phase.md`, decisions D1–D4). The phase-58
suite this file used to be updates IN PLACE to the new contract (the
phase-97 task-07/08 precedent: a completed phase's suite follows its
changed contract; its seeding helpers are kept).
The measured contract (the 16px root, the ±4px tolerance is the task
spec):
* the container is ``min(100%, 72rem)`` border-box with the 2×1.25rem
gutters INSIDE the box — at any viewport ≥ ~1200px every shell
(``.container.*-shell``) measures **1152px** (72rem), identical on
the chat page, Tuning, Theme, and the RAG page;
* the standalone ``document.html`` page's ``.doc-md`` is the container's
inner content — **1112px** (1152 − 2×1.25rem);
* the same-page document MODAL is untouched (D2): the panel stays
**~1100px** and the panel binds the ``.doc-md`` inside it —
**1058px** (1100 − 2×1px borders − 2×1.25rem of
``.doc-modal-content`` padding), NOT 1152;
* below the container cap everything is full-width as before: at 360px
no horizontal overflow and the shell is the viewport width; at 900px
chat/tuning/theme all measure 900px (equal — the cap never bound
below 72rem anyway).
Run in isolation (DB must be up: ``podman compose up -d db``):
@@ -18,33 +35,30 @@ Run in isolation (DB must be up: ``podman compose up -d db``):
Test → contract mapping:
1. ``test_chat_column_wide_vs_base`` — ``/`` at 1920×1080: the
``.chat-shell`` bounding box is 1472px (92rem, ±4px) and centered;
at 1280×800 (below the wide breakpoint) it is back to 736px
(46rem, ±4px).
2. ``test_document_column_wide_vs_base`` — a seeded markdown document
that carries a summary: at 1920 both ``.doc-md`` and the adjacent
``.doc-summary:has(+ .doc-md)`` panel are 1472px; at 1280
``.doc-md`` is 736px.
3. ``test_shared_column_wide`` — a real auto-saved conversation shared
by token: ``/shared/<token>`` at 1920 renders ``.shared-shell`` at
1472px in a fresh anonymous context.
4. ``test_narrow_unchanged`` — 360px: no horizontal overflow and the
``.chat-shell`` is the existing mobile rule — full-bleed at the
viewport width (the shell IS its .container; the 0.9rem mobile
gutters live in its own padding, inside the measured box), 900px:
the shell holds the 46rem base (736px, not the 1472px wide rule —
at 900px a leaked wide override would pin the shell to the 860px
content box instead, so 736px is the discriminator) — the wide
rule does not leak below 1500px.
1. ``test_all_columns_match_the_rag_page`` — the core pin: at
1280×800 the ``.chat-shell`` (``/``), ``.tuning-shell``
(``/tuning.html``), ``.theme-shell`` (``/theme.html``) and the RAG
page's ``.sources-shell`` (``/sources.html``) bounding boxes are ALL
EQUAL (±4px) — the owner's "match the width of the RAG page" as one
assertion; at 1920×1080 the same four are still equal to each other
and each measures ≈1152px (72rem, ±4px).
2. ``test_reader_columns_wide`` — the token's consumers at 1920×1080:
the modal panel is still ~1100px (D2: the modal is UNCHANGED) with
its ``.doc-md`` at ~1058px; ``/shared/<token>``'s ``.shared-shell``
is ≈1152px; the standalone ``document.html`` page's ``.doc-md`` is
≈1112px (the 72rem container's inner content).
3. ``test_narrow_unchanged`` — the no-regression leg: at 360×800 no
horizontal overflow and ``.chat-shell`` is the viewport width
(full-bleed — the mobile gutters sit in the container's padding,
inside the measured box); at 900×600 chat/tuning/theme are all
900px wide, equal to each other.
DB isolation: the shared-chat row is deleted in a ``finally`` (admin
cookie — the house pattern of test_share_chat.py, whose distinctive
question text keeps the auto-title unique); the fixture document rows
follow the story-fixture convention of the sibling suites (truncate +
re-import; the summary document is a direct row insert, the
test_document_viewer.py XSS-fixture pattern — the viewer is
database-only).
re-import; the viewer fixture is a direct row insert — the viewer is
database-only, the test_document_viewer.py XSS-fixture pattern).
"""
from __future__ import annotations
@@ -70,14 +84,22 @@ from e2e.auth_helpers import login
REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "docs"
# The two proof viewports (task spec): wide desktop vs just below the
# 1500px breakpoint, at the 16px root the phase-58 rem contract:
# 92rem = 1472px, 46rem = 736px. ±4px tolerance (task spec).
WIDE_PX = 92 * 16 # 1472px — the 2x wide override (>=1500px)
BASE_PX = 46 * 16 # 736px — the --chat-column base
TOL_PX = 4
WIDE_VIEWPORT: ViewportSize = {"width": 1920, "height": 1080}
BASE_VIEWPORT: ViewportSize = {"width": 1280, "height": 800}
# The phase-100 measured contract (16px root, the task spec):
# * CONTAINER_PX — 72rem border-box, the 2×1.25rem gutters INSIDE the
# box; every .container.*-shell measures this at any viewport
# >= ~1200px (the RAG page's width — the owner's ask).
# * DOC_MD_STANDALONE_PX — the standalone document page's .doc-md: the
# container's inner content (CONTAINER_PX minus the gutters).
# * MODAL_PANEL_PX / DOC_MD_MODAL_PX — the SAME-PAGE modal is untouched
# (D2): the 1100px border-box panel (minus its 1px borders) and
# .doc-modal-content's 2×1.25rem padding bind the .doc-md inside it.
CONTAINER_PX = 72 * 16 # 1152px — the 72rem container (border-box)
DOC_MD_STANDALONE_PX = CONTAINER_PX - 2 * 20 # 1112px — inner content
MODAL_PANEL_PX = 1100 # the phase-26 panel ceiling (D2: unchanged)
DOC_MD_MODAL_PX = MODAL_PANEL_PX - 2 - 2 * 20 # 1058px — panel-inner
TOL_PX = 4 # the task spec's ±4px tolerance
VIEWPORT_1280: ViewportSize = {"width": 1280, "height": 800}
VIEWPORT_1920: ViewportSize = {"width": 1920, "height": 1080}
MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"
SHARE_URL_RE = re.compile(r"^/shared/[0-9a-f-]{36}$")
@@ -139,11 +161,11 @@ def _reset_db(mock_port: int, seed: bool) -> ImportSummary | None:
def _seed_summary_doc() -> None:
"""One markdown document carrying a stored summary — the ONLY shape
that renders both reading-column pins on one page: ``.doc-summary``
(the phase-36 panel) directly above ``.doc-md`` (the ``:has(
+ .doc-md)`` sibling match). Direct row insert — the viewer is
database-only (the test_document_viewer.py XSS-fixture pattern)."""
"""One markdown document carrying a stored summary — the viewer
fixture the phase-100 suite opens in BOTH the same-page modal and
the standalone document page (the test_document_viewer.py
XSS-fixture pattern: a direct row insert, the viewer is
database-only)."""
with SessionLocal() as db:
db.add(
Document(
@@ -153,11 +175,11 @@ def _seed_summary_doc() -> None:
title=FIXTURE_TITLE,
content=(
"# Wide Column Fixture\n\n"
"Phase-58 width pin: a markdown document with a "
"summary, so the viewer renders the .doc-summary "
"panel directly above the .doc-md column."
"Phase-100 width pin: a markdown document with a "
"summary, opened in the same-page modal and the "
"standalone document page."
),
summary="A phase-58 fixture summary for the wide-column pin.",
summary="A phase-100 fixture summary for the width pin.",
content_hash="w" * 64,
indexed_at=datetime.now(UTC),
)
@@ -170,14 +192,29 @@ def _seed_summary_doc() -> None:
# ---------------------------------------------------------------------------
def _drill(page: Page, *names: str) -> None:
"""Phase 97: the catalog is the drill-down tree — click through the
source/folder rows (exact name match, one per name) to the level
that holds the asserted file."""
for name in names:
page.click(f'#folders-tbody a.folder-link:text-is("{name}")')
def _box_width(page: Page, selector: str, label: str) -> float:
"""The element's bounding-box width (the measured rendered width,
not the computed style) — None means the element is not rendered,
which is a test error, not a measurement."""
box = page.locator(selector).first.bounding_box()
assert box is not None, f"{selector} not rendered ({label})"
return box["width"]
def _assert_width(page: Page, selector: str, expected_px: int, label: str) -> None:
"""The element's bounding-box width is ``expected_px`` ±4px (task
spec) — the measured rendered width, not the computed style."""
box = page.locator(selector).first.bounding_box()
assert box is not None, f"{selector} not rendered ({label})"
assert abs(box["width"] - expected_px) <= TOL_PX, (
f"{label}: {selector} is {box['width']:.1f}px, "
f"want {expected_px}px ±{TOL_PX}px"
width = _box_width(page, selector, label)
assert abs(width - expected_px) <= TOL_PX, (
f"{label}: {selector} is {width:.1f}px, want {expected_px}px ±{TOL_PX}px"
)
@@ -265,109 +302,132 @@ def _delete_chat(app_url: str, cookies: dict[str, str], chat_id: str) -> None:
httpx.delete(f"{app_url}/api/chats/{chat_id}", timeout=10, cookies=cookies)
def _measure_shells(
browser: Browser, app_url: str, viewport: ViewportSize
) -> dict[str, float]:
"""One signed-in page at ``viewport``; measure the bounding-box
width of every shell under test after its view is shown:
* ``.chat-shell`` on ``/`` (the chat view — the div IS
``.container.chat-shell``);
* ``.tuning-shell`` on ``/tuning.html``, ``.theme-shell`` on
``/theme.html``, and the RAG page's ``.sources-shell`` on
``/sources.html`` (admin deep-links — A11's one shell document).
"""
page = browser.new_page(viewport=viewport)
widths: dict[str, float] = {}
try:
login(page, app_url, next="/")
page.locator(".chat-shell").wait_for(state="visible", timeout=10_000)
widths[".chat-shell"] = _box_width(page, ".chat-shell", "chat")
for path, selector in (
("/tuning.html", ".tuning-shell"),
("/theme.html", ".theme-shell"),
("/sources.html", ".sources-shell"),
):
page.goto(app_url + path)
page.locator(selector).wait_for(state="visible", timeout=10_000)
widths[selector] = _box_width(page, selector, path)
finally:
page.close()
return widths
# ---------------------------------------------------------------------------
# 1. Chat: 1472px at 1920, back to 736px at 1280 (centered both ways)
# 1. The core pin: chat == tuning == theme == RAG page (±4px), at both
# proof viewports; each ≈1152px (the 72rem container) at 1920
# ---------------------------------------------------------------------------
def test_chat_column_wide_vs_base(
def test_all_columns_match_the_rag_page(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""The chat page's .chat-shell doubles at the 1500px breakpoint:
92rem (1472px, ±4px) at 1920×1080, centered; 46rem (736px, ±4px)
at 1280×800 — the base below the breakpoint."""
wide = browser.new_page(viewport=WIDE_VIEWPORT)
try:
login(wide, app_url, next="/") # phase 79: the chips need a session
wide.locator("#suggestions .suggestion-chip").first.wait_for(
state="visible", timeout=10_000
"""The owner's "match the width of the RAG page" as ONE assertion:
at 1280×800 the four shells are all EQUAL (±4px); at 1920×1080
they are still equal to each other and each measures ≈1152px
(72rem at the 16px root, ±4px — the box includes the container's
2×1.25rem padding, border-box)."""
# --- 1280×800 (just above the container cap — the owner's report) ---
widths = _measure_shells(browser, app_url, VIEWPORT_1280)
ref_selector, ref = next(iter(widths.items()))
for selector, width in widths.items():
assert abs(width - ref) <= TOL_PX, (
f"@1280px {selector} is {width:.1f}px but {ref_selector} is "
f"{ref:.1f}px — the columns must match the RAG page (±{TOL_PX}px)"
)
_assert_width(wide, ".chat-shell", WIDE_PX, "chat @ 1920px")
_assert_centered(wide, ".chat-shell", 1920, "chat @ 1920px")
finally:
wide.close()
base = browser.new_page(viewport=BASE_VIEWPORT)
try:
login(base, app_url, next="/") # phase 79: the chips need a session
base.locator("#suggestions .suggestion-chip").first.wait_for(
state="visible", timeout=10_000
# --- 1920×1080: equal AND each the 72rem container (≈1152px) ------
widths = _measure_shells(browser, app_url, VIEWPORT_1920)
ref_selector, ref = next(iter(widths.items()))
for selector, width in widths.items():
assert abs(width - ref) <= TOL_PX, (
f"@1920px {selector} is {width:.1f}px but {ref_selector} is "
f"{ref:.1f}px — the columns must match the RAG page (±{TOL_PX}px)"
)
assert abs(width - CONTAINER_PX) <= TOL_PX, (
f"@1920px {selector} is {width:.1f}px, want "
f"{CONTAINER_PX}px (the 72rem container) ±{TOL_PX}px"
)
_assert_width(base, ".chat-shell", BASE_PX, "chat @ 1280px")
_assert_centered(base, ".chat-shell", 1280, "chat @ 1280px")
finally:
base.close()
# ---------------------------------------------------------------------------
# 2. Document viewer: .doc-md (and its .doc-summary panel) at both
# widths — the owner-expanded surface
# 2. The token's consumers at 1920×1080: the modal (unchanged, D2), the
# shared page, and the standalone document page
# ---------------------------------------------------------------------------
def test_document_column_wide_vs_base(
def test_reader_columns_wide(
browser: Browser, app_url: str, mock_llm: int, db_ready: None
) -> None:
"""A seeded md document (with a summary) renders .doc-md at 1472px
at 1920 and 736px at 1280 — and the adjacent .doc-summary panel
matches the column at 1920 (.doc-summary:has(+ .doc-md))."""
"""At 1920×1080: the same-page document MODAL is untouched (D2 —
the panel is still ~1100px and IT binds the ``.doc-md`` inside,
~1058px, not 1152); ``/shared/<token>``'s ``.shared-shell`` is
≈1152px; the standalone ``document.html`` page's ``.doc-md`` is
≈1112px (the 72rem container's inner content)."""
_reset_db(mock_llm, seed=True)
_seed_summary_doc()
wide = browser.new_page(viewport=WIDE_VIEWPORT)
page = browser.new_page(viewport=VIEWPORT_1920)
try:
login(wide, app_url, next="/") # phase 79: the viewer content is gated
wide.goto(app_url + FIXTURE_DOC_URL)
expect(wide.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000)
expect(wide.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000)
expect(
wide.locator(".doc-summary:has(+ .doc-md)")
).to_have_count(1, timeout=15_000)
_assert_width(wide, "#doc-content .doc-md", WIDE_PX, "document @ 1920px")
# (a) the same-page modal — the 1100px panel stays its effective
# ceiling (D2: pin the PANEL, not the 1152 the column would be).
login(page, app_url, next="/sources.html")
page.locator("#folders-tbody .folder-link").first.wait_for(
state="visible", timeout=15_000
)
_drill(page, FIXTURE_SOURCE, "notes")
row = page.locator("#docs-tbody tr", has_text="wide-column-fixture.md")
expect(row).to_have_count(1)
row.locator("td:nth-child(2) a.doc-link").click()
expect(page.locator(".doc-modal")).to_be_visible()
expect(page.locator("#doc-modal-title")).to_have_text(FIXTURE_TITLE)
_assert_width(page, "#doc-modal-panel", MODAL_PANEL_PX, "modal panel @ 1920px")
_assert_width(
wide, ".doc-summary:has(+ .doc-md)", WIDE_PX, "summary panel @ 1920px"
page,
"#doc-modal-content .doc-md",
DOC_MD_MODAL_PX,
"modal .doc-md @ 1920px",
)
finally:
wide.close()
base = browser.new_page(viewport=BASE_VIEWPORT)
try:
login(base, app_url, next="/") # phase 79: the viewer content is gated
base.goto(app_url + FIXTURE_DOC_URL)
expect(base.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000)
expect(base.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000)
_assert_width(base, "#doc-content .doc-md", BASE_PX, "document @ 1280px")
finally:
base.close()
# ---------------------------------------------------------------------------
# 3. Shared page: .shared-shell at 1472px for a guest at 1920
# ---------------------------------------------------------------------------
def test_shared_column_wide(
page: Page,
browser: Browser,
app_url: str,
mock_llm: int,
db_ready: None,
) -> None:
"""A real auto-saved conversation, shared by token, renders its
.shared-shell at 1472px (±4px) at 1920 in a FRESH anonymous
context (no cookies — the guest's only credential is the token)."""
_reset_db(mock_llm, seed=True)
page.set_default_timeout(30_000)
login(page, app_url, next="/")
expect(page).to_have_url(app_url + "/", timeout=30_000)
# (b) the standalone document page: the 72rem container's inner
# content (1152 − 2×1.25rem gutters).
page.goto(app_url + FIXTURE_DOC_URL)
expect(page.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000)
expect(page.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000)
_assert_width(
page,
"#doc-content .doc-md",
DOC_MD_STANDALONE_PX,
"standalone .doc-md @ 1920px",
)
# (c) the shared page: .shared-shell at the full 72rem container.
page.goto(app_url + "/")
q = "How is my Kubernetes cluster set up? (wide-column)"
_ask(page, q)
cookies = _admin_cookies(page)
row = _wait_saved_row(app_url, cookies, _auto_title(q))
chat_id: str = row["id"]
saved_row = _wait_saved_row(app_url, cookies, _auto_title(q))
chat_id: str = saved_row["id"]
anon_ctx = None
try:
# Public since phase 55 — the share endpoint takes no session.
@@ -376,7 +436,7 @@ def test_shared_column_wide(
share_url = r.json()["share_url"]
assert SHARE_URL_RE.fullmatch(share_url), f"bad share_url shape: {share_url}"
anon_ctx = browser.new_context(viewport=WIDE_VIEWPORT)
anon_ctx = browser.new_context(viewport=VIEWPORT_1920)
anon = anon_ctx.new_page()
anon.set_default_timeout(30_000)
anon.goto(app_url + share_url)
@@ -386,49 +446,62 @@ def test_shared_column_wide(
expect(anon.locator(".msg.brain .bubble")).to_contain_text(
MOCK_ANSWER_MARKER, timeout=30_000
)
_assert_width(anon, ".shared-shell", WIDE_PX, "shared @ 1920px")
_assert_width(anon, ".shared-shell", CONTAINER_PX, "shared @ 1920px")
_assert_centered(anon, ".shared-shell", 1920, "shared @ 1920px")
finally:
if anon_ctx is not None:
anon_ctx.close()
_delete_chat(app_url, cookies, chat_id)
finally:
page.close()
# ---------------------------------------------------------------------------
# 4. Below the breakpoint: 360px and 900px are byte-for-byte the old
# rules — no overflow, no leaked wide column
# 3. Below the container cap: full-width as today — 360px overflow-free,
# 900px chat == tuning == theme (the cap never bound below 72rem)
# ---------------------------------------------------------------------------
def test_narrow_unchanged(
browser: Browser, app_url: str, db_ready: None
) -> None:
"""The min-width:1500px override must not leak below the
breakpoint: at 360px no horizontal overflow and the shell is the
existing mobile rule — full-bleed at the viewport width (the shell
IS its .container, so the 0.9rem mobile gutters sit in its own
padding, inside the measured box); at 900px the shell holds the
46rem base (736px — a leaked 92rem rule would pin it to the 860px
content box instead, so 736px is the discriminator)."""
phone = browser.new_page(viewport={"width": 360, "height": 740})
"""The mobile/tablet layouts are UNCHANGED (D3): at 360×800 no
horizontal overflow and ``.chat-shell`` is the viewport width
(full-bleed — the shell IS its .container, so the mobile gutters sit
in its padding, inside the measured box); at 900×600 the
chat/tuning/theme shells are ALL 900px wide, equal to each other
(the cap never bound below 72rem anyway)."""
phone = browser.new_page(viewport={"width": 360, "height": 800})
try:
login(phone, app_url, next="/") # phase 79: the chips need a session
phone.locator("#suggestions .suggestion-chip").first.wait_for(
state="visible", timeout=10_000
)
login(phone, app_url, next="/")
phone.locator(".chat-shell").wait_for(state="visible", timeout=10_000)
_assert_no_doc_overflow(phone, "chat @ 360px")
_assert_width(phone, ".chat-shell", 360, "chat @ 360px")
finally:
phone.close()
tablet = browser.new_page(viewport={"width": 900, "height": 800})
tablet = browser.new_page(viewport={"width": 900, "height": 600})
try:
login(tablet, app_url, next="/") # phase 79: the chips need a session
tablet.locator("#suggestions .suggestion-chip").first.wait_for(
state="visible", timeout=10_000
)
login(tablet, app_url, next="/")
tablet.locator(".chat-shell").wait_for(state="visible", timeout=10_000)
_assert_no_doc_overflow(tablet, "chat @ 900px")
_assert_width(tablet, ".chat-shell", BASE_PX, "chat @ 900px")
_assert_centered(tablet, ".chat-shell", 900, "chat @ 900px")
widths = {".chat-shell": _box_width(tablet, ".chat-shell", "chat @ 900px")}
for path, selector in (
("/tuning.html", ".tuning-shell"),
("/theme.html", ".theme-shell"),
):
tablet.goto(app_url + path)
tablet.locator(selector).wait_for(state="visible", timeout=10_000)
widths[selector] = _box_width(tablet, selector, f"{path} @ 900px")
ref_selector, ref = next(iter(widths.items()))
for selector, width in widths.items():
assert abs(width - ref) <= TOL_PX, (
f"@900px {selector} is {width:.1f}px but {ref_selector} is "
f"{ref:.1f}px — the cap must not bind below the container cap"
)
assert abs(width - 900) <= TOL_PX, (
f"@900px {selector} is {width:.1f}px, want the full 900px "
f"viewport width ±{TOL_PX}px"
)
finally:
tablet.close()
+16 -11
View File
@@ -10,8 +10,9 @@ without a browser:
* the house shell (AGENTS.md rule 5 + the login.html/shared.html
minimal-flow-page lineage): skip-link, the SLIM header (brand +
"Back to chat" — no nav), the 46rem base column (hard-coded — a form
column, NOT ``--chat-column``), the ``container`` frame;
"Back to chat" — no nav), the full 72rem container (the phase-59
form-column cap is retired — owner instruction 2026-09-12), the
``container`` frame;
* the form contract: ``#draft-title`` / ``#draft-path`` /
``#draft-body`` with visible labels, ``#push-doc-btn`` (the exact
"Push to docs branch" copy) + the back link, ``#push-status``
@@ -455,21 +456,25 @@ def test_trim_git_detail_behavior_is_pinned_by_the_markers() -> None:
# ---------- styles.css: the new classes ----------
def test_doc_edit_shell_is_the_hardcoded_46rem_column() -> None:
""".doc-edit-shell: the 46rem base column — HARD-CODED 46rem (a
form column, not a reading column — it must NOT ride
--chat-column, so phase 58's wide-desktop doubling never stretches
the form), centered, a flex column on the container frame."""
def test_doc_edit_shell_rides_the_full_container() -> None:
""".doc-edit-shell: the full 72rem container (owner instruction
2026-09-12 — the phase-59 form-column cap is superseded): no
max-width, no margin-inline (the .container ancestor centers), a
flex column on the container frame."""
css = _css()
block = re.search(r"\.doc-edit-shell \{([\s\S]*?)\n\}", css)
assert block, "styles.css must style .doc-edit-shell"
body = block.group(1)
assert "max-width: 46rem" in body, "the 46rem base column (hard-coded)"
assert "max-width" not in body, (
"no cap — the shell rides the .container's 72rem frame"
)
assert "--chat-column" not in body, (
"the form column does not ride --chat-column (phase 58 must "
"not stretch it)"
"the shell rides the .container frame, not the reading-column "
"token"
)
assert "margin-inline" not in body, (
"the .container ancestor centers (no shell-level centering)"
)
assert "margin-inline: auto" in body
assert "flex-direction: column" in body
+2 -1
View File
@@ -206,7 +206,8 @@ def test_header_comment_notes_the_table_pass() -> None:
def test_table_wrap_is_the_horizontal_scroller() -> None:
""".md-table-wrap { overflow-x: auto } — the wrapper (not the
table) is the scroller, so a wide table scrolls inside the bubble
instead of breaking the 46rem column (story AC5)."""
instead of breaking the 72rem column (story AC5 — the width
contract is now the 72rem container, phase 100)."""
css = _text(STYLES_CSS)
block = re.search(r"\.md-table-wrap\s*\{([^}]*)\}", css)
assert block, "styles.css must define .md-table-wrap"
+2 -2
View File
@@ -6,8 +6,8 @@ be at the bottom of the screen on an EMPTY chat too, which sticky alone
cannot do).
The chat page scrolls at the DOCUMENT level and `.chat-shell` (the
centered 46rem column, PLAN §7.1) is the composer's sticky containing
block. The pin is therefore TWO rules, and both are pinned here:
centered 72rem column — the .container width, phase 100) is the
composer's sticky containing block. The pin is therefore TWO rules, and both are pinned here:
* `.messages { flex: 1 1 auto }` absorbs the free space of a short page,
so the composer's RESTING (in-flow) position is already the bottom of
+6 -3
View File
@@ -450,8 +450,9 @@ def test_modal_css_targets_and_contrast_pairs() -> None:
9.3:1, the err-line border — the .tuning-delete / .steering-delete
convention; the hover inverts to --bg on --err-line, 5.2:1);
Cancel is the ghost ink-soft family (5.1:1 on --surface); the
error line is the err pair; the panel caps at the 46rem
chat-column width or the viewport."""
error line is the err pair; the panel caps at the chat-column
width (the --chat-column token — 72rem, owner instruction
2026-09-12) or the viewport."""
css = _css()
btn = css[css.find(".remove-confirm-btn {"):]
btn = btn[: btn.find("\n}")]
@@ -473,4 +474,6 @@ def test_modal_css_targets_and_contrast_pairs() -> None:
assert "var(--err-ink)" in err and "var(--err-bg)" in err
panel = css[css.find(".remove-confirm-panel {"):]
panel = panel[: panel.find("\n}")]
assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)"
assert "min(var(--chat-column)" in panel, (
"the chat-column cap via the token (or the viewport)"
)
+9 -8
View File
@@ -19,8 +19,8 @@ silent regression is caught without a browser:
chat page's interactive builders, and the rendered messages contain
no button/form/link — the chips are plain spans, the source chips
carry no ``href``;
* the shared shell's reading-column mapping (--chat-column token,
phase 58) + the static-chip and
* the shared shell's reading-column mapping (--chat-column token —
the 72rem container width, phase 100) + the static-chip and
invalid-state CSS (the ≤640px squeeze included).
"""
from __future__ import annotations
@@ -384,17 +384,18 @@ def test_brand_note_resolves_at_call_time() -> None:
def test_shared_shell_maps_to_the_reading_column() -> None:
"""The PLAN §7 column contract (phase 58): .shared-shell is the
centered chat column riding the --chat-column token (46rem base,
92rem at >=1500px wide desktops — owner instruction 2026-08-31,
TODO L5 / D2). The conversation reads exactly like the chat
page's, so the existing .msg/.bubble CSS applies unchanged."""
"""The 72rem-everywhere contract (phase 100, owner instruction
2026-09-12): .shared-shell is the centered chat column riding the
--chat-column token — equal to the .container's cap, so the shared
page reads at the RAG page's width. The conversation reads exactly
like the chat page's, so the existing .msg/.bubble CSS applies
unchanged."""
css = _css()
block = re.search(r"\.shared-shell \{([\s\S]*?)\n\}", css)
assert block, "styles.css must style .shared-shell"
body = block.group(1)
assert "max-width: var(--chat-column)" in body, (
"the PLAN §7 centered chat column (phase 58 token)"
"the centered chat column riding the token (the 72rem width)"
)
assert "46rem" not in body.split("/*")[0], (
"no hard-coded cap — the width rides the token"
+5 -2
View File
@@ -499,7 +499,8 @@ def test_dialog_button_and_target_contrast_pairs() -> None:
brand-soft hover (12.4:1); Save is the solid brand family (--bg
text on --brand 5.2:1, the .new-chat-btn convention) with the
lightened hover; the error line is the err pair; the panel caps
at the 46rem chat-column width or the viewport; the visible
at the chat-column width (the --chat-column token — 72rem, owner
instruction 2026-09-12) or the viewport; the visible
label is ink-soft (5.1:1) — never a label-less textarea."""
css = _css()
btn = _css_rule(css, ".ignore-editor-btn")
@@ -517,7 +518,9 @@ def test_dialog_button_and_target_contrast_pairs() -> None:
err = _css_rule(css, ".ignore-editor-error")
assert "var(--err-ink)" in err and "var(--err-bg)" in err
panel = _css_rule(css, ".ignore-editor-panel")
assert "min(46rem" in panel, "the 46rem chat-column cap (or the viewport)"
assert "min(var(--chat-column)" in panel, (
"the chat-column cap via the token (or the viewport)"
)
label = _css_rule(css, ".ignore-editor-label")
assert "display: block" in label and "var(--ink-soft)" in label, (
"a visible block label (WCAG — never aria-label-only)"
+122 -107
View File
@@ -1,29 +1,33 @@
"""Unit: the 2x reading column on wide desktops (phase 58, task 01).
"""Unit: every page matches the RAG page's 72rem width (phase 100).
The measured-width browser proof is E2E-gated by the phase-58 story
suite (task 02); like the other frontend-adjacent unit files (the
The measured-width browser proof is E2E-gated by the phase's width
suite (task 03); like the other frontend-adjacent unit files (the
test_save_chat_ui.py pattern), this module pins the styles.css markers
the wide-column contract depends on, so a silent regression is caught
the width contract depends on, so a silent regression is caught
without a browser:
* the ``--chat-column`` custom property in ``:root`` — 46rem base
(the PLAN §7 column lineage) with the provenance comment (owner
instruction 2026-08-31, TODO L5 / D2);
* the ``@media (min-width: 1500px)`` block at the bottom of the
responsive region — the SINGLE place that doubles the token to
92rem (2x);
* the ``--chat-column`` custom property in ``:root`` — 72rem, EQUAL to
the ``.container``'s 72rem cap (owner instruction 2026-09-12:
"match the width of the RAG page for all other pages" — supersedes
the 2026-08-31 instruction), with the provenance comment;
* NO ``@media (min-width: 1500px)`` block anywhere in the file — the
phase-58 wide-desktop doubling is deleted in full (the token is
72rem at every viewport);
* the four reading-column selectors — ``.chat-shell``,
``.shared-shell``, ``.doc-md``, ``.doc-summary:has(+ .doc-md)`` —
each capped with ``max-width: var(--chat-column)`` and NOTHING else
in the file uses the token (exactly four rules);
* the negative pin — the form columns (``.tuning-shell``; and from
phase 59, task 06, ``.doc-edit-shell`` — forms, not reading
surfaces) are the only literal ``max-width: 46rem`` rules left in
the file, kept hard-coded so the wide-desktop doubling never
stretches a form;
* the "46rem column contract" block comments were updated to name the
base value + the wide override (the stale "≤46rem" contract claims
are gone from the reading-column comments).
in the file uses the token for a max-width (exactly four rules);
* the flipped negative pin — ZERO literal ``max-width: 46rem`` rules
remain (the phase-27/59/91 form-column caps are retired): the form
shells (``.tuning-shell``, ``.theme-shell``, ``.doc-edit-shell``)
carry no cap, no token, and no ``margin-inline`` — structurally
``.sources-shell``;
* the two source-page dialogs (phase 69 remove-confirm, phase 89
ignore-editor) cap at the chat-column width THROUGH THE TOKEN —
``min(var(--chat-column), calc(100vw - 2rem))`` — or the viewport;
* the stale width comments are gone — no "46rem base" / "92rem at
>=1500px" / "1500px" width claims remain (the "46rem column
contract" wording was rewritten to the 72rem contract, 2026-09-12).
"""
from __future__ import annotations
@@ -58,57 +62,52 @@ def _rule_block(css: str, selector: str) -> str:
# ---------- the --chat-column token ----------
def test_root_declares_chat_column_46rem_base() -> None:
""":root declares --chat-column: 46rem (the PLAN §7 base) with the
owner-provenance comment (instruction 2026-08-31, TODO L5)."""
def test_root_declares_chat_column_72rem_equal_to_the_container() -> None:
""":root declares --chat-column: 72rem — EQUAL to the .container's
72rem cap (one width for everything) — with the owner-provenance
comment (instruction 2026-09-12)."""
css = _css()
root = _rule_block(css, ":root")
assert "--chat-column: 46rem" in root, (
":root must declare the --chat-column base (46rem)"
assert "--chat-column: 72rem" in root, (
":root must declare the --chat-column width (72rem)"
)
pre = css[: css.index("--chat-column: 46rem")]
# The token equals the container's cap (the RAG page's width).
container = _rule_block(css, ".container")
assert "max-width: 72rem" in container, (
".container's 72rem cap is the width the token now equals"
)
pre = css[: css.index("--chat-column: 72rem")]
comment = pre[pre.rindex("/*") : pre.rindex("*/")]
assert "owner instruction 2026-08-31" in comment, (
assert "owner instruction 2026-09-12" in comment, (
"the token's comment must cite the owner instruction "
"(2026-08-31, TODO L5)"
"(2026-09-12 — match the width of the RAG page)"
)
def test_wide_media_block_doubles_the_token() -> None:
"""A @media (min-width: 1500px) block sets --chat-column: 92rem on
:root — the single wide override (2x the base)."""
def test_the_wide_desktop_doubling_is_retired() -> None:
"""No @media (min-width: 1500px) block remains (the phase-58 2x
override is deleted in full, 2026-09-12) and the token is never
assigned the old wide value — 72rem at every viewport."""
css = _css()
m = re.search(r"@media \(min-width: 1500px\) \{", css)
assert m, "styles.css must carry the @media (min-width: 1500px) block"
start = css.index("{", m.start())
depth = 0
for i in range(start, len(css)):
if css[i] == "{":
depth += 1
elif css[i] == "}":
depth -= 1
if depth == 0:
block = css[m.start() : i + 1]
break
else:
raise AssertionError("unbalanced braces in the wide media block")
assert ":root { --chat-column: 92rem; }" in block, (
"the wide block must set :root { --chat-column: 92rem; }"
assert "@media (min-width: 1500px)" not in css, (
"the wide-desktop media block must be deleted (retired 2026-09-12)"
)
assert "--chat-column: 92rem" not in css, (
"the token is never doubled to the old wide value"
)
# The wide block is the ONLY min-width:1500 media in the file and
# the only place 92rem is assigned to the token.
assert css.count("@media (min-width: 1500px)") == 1
assert css.count("--chat-column: 92rem") == 1
def test_wide_block_lives_in_the_bottom_responsive_region() -> None:
"""The min-width sibling sits alongside the max-width responsive
blocks at the bottom of the file (after the <=640px block)."""
def test_no_literal_46rem_width_remains() -> None:
"""Flipped negative pin (phase 100 D1): ZERO literal
max-width: 46rem rules remain in the file — the phase-27/59/91
form-column caps are retired, and the dialog panels ride the
token, not a 46rem literal."""
css = _css()
wide = css.index("@media (min-width: 1500px)")
mobile = css.rindex("@media (max-width: 640px)")
assert wide > mobile, (
"the wide override belongs in the bottom media-query region"
assert css.count("max-width: 46rem") == 0, (
"no literal max-width: 46rem rule may remain (retired 2026-09-12)"
)
assert "min(46rem" not in css, (
"the dialog panels must ride the token, not a 46rem literal"
)
@@ -119,7 +118,7 @@ def test_the_four_reading_columns_use_the_token() -> None:
""".chat-shell, .shared-shell, .doc-md and
.doc-summary:has(+ .doc-md) each cap with
max-width: var(--chat-column) — and exactly those four rules use
the token (no other selector)."""
the token for a max-width (no other selector)."""
css = _css()
for selector in (
".chat-shell",
@@ -135,73 +134,89 @@ def test_the_four_reading_columns_use_the_token() -> None:
)
def test_shared_shell_keeps_the_centered_column_comment() -> None:
""".shared-shell's inline comment keeps the "centered chat column"
wording and notes the wide override (task 01 work item)."""
css = _css()
rule = css[css.index(".shared-shell {") : css.index(".shared-shell {") + 400]
assert "the PLAN §7 centered chat column" in rule
assert "92rem at >=1500px" in rule, "the comment must note the wide override"
def test_doc_md_keeps_width_100_under_the_cap() -> None:
""".doc-md stays width:100% under the token cap (the modal's
1100px panel remains its effective ceiling there)."""
1100px panel remains its effective ceiling there — phase 100 D2)."""
assert "width: 100%" in _rule_block(_css(), ".doc-md")
# ---------- the negative pins ----------
# ---------- the form shells + the dialog panels ----------
def test_tuning_shell_stays_hardcoded_46rem() -> None:
""".tuning-shell (the form column, out of scope) keeps its
hard-coded max-width: 46rem at every width — it never widens."""
def test_the_form_shells_ride_the_full_container() -> None:
"""The form shells (.tuning-shell — phase 27, .theme-shell —
phase 91, .doc-edit-shell — phase 59) carry NO cap, NO token, and
NO margin-inline: the .container ancestor centers them, so they
ride the 72rem frame like .sources-shell (flex column,
gap 1.25rem, flex: 1 — the owner instruction 2026-09-12 retires
the form-column caps)."""
css = _css()
tuning = _rule_block(css, ".tuning-shell")
assert "max-width: 46rem" in tuning, (
".tuning-shell must stay hard-coded 46rem (negative pin)"
for selector in (".tuning-shell", ".theme-shell", ".doc-edit-shell"):
block = _rule_block(css, selector)
assert "max-width" not in block, (
f"{selector} must carry no cap (retired 2026-09-12)"
)
assert "var(--chat-column)" not in tuning, (
".tuning-shell must NOT reference the reading-column token"
assert "var(--chat-column)" not in block, (
f"{selector} rides the .container frame, not the token"
)
assert "margin-inline" not in block, (
f"{selector} is centered by the .container ancestor"
)
for decl in (
"display: flex",
"flex-direction: column",
"gap: 1.25rem",
"flex: 1",
):
assert decl in block, (
f"{selector} keeps the .sources-shell shape ({decl})"
)
def test_no_other_hardcoded_46rem_rule_remains() -> None:
"""After the switch, the form columns are the ONLY rules with a
literal max-width: 46rem: .tuning-shell (phase 27),
.doc-edit-shell (phase 59, task 06 — the doc edit screen is a
FORM column, not a reading column, so it must not ride
--chat-column and phase 58's wide-desktop doubling must never
stretch the form), and .theme-shell (phase 91 task 04 — the
admin Theme editor is a form column too: the palette grid +
fieldsets must never ride the wide-desktop doubling). Every
reading column rides the token (the --chat-column base
declaration is the other non-rule occurrence of 46rem)."""
def test_the_dialog_panels_ride_the_token() -> None:
"""The two source-page dialogs (phase 69 remove-confirm, phase 89
ignore-editor) cap at the chat-column width THROUGH THE TOKEN —
min(var(--chat-column), calc(100vw - 2rem)) — or the viewport,
whichever is narrower (the 46rem literals ride no more)."""
css = _css()
assert css.count("max-width: 46rem") == 3, (
"only the form columns (.tuning-shell, .doc-edit-shell, "
".theme-shell) may keep a literal max-width: 46rem"
for selector in (".remove-confirm-panel", ".ignore-editor-panel"):
block = _rule_block(css, selector)
assert "min(var(--chat-column), calc(100vw - 2rem))" in block, (
f"{selector} must ride the token (the chat-column width "
"or the viewport)"
)
assert "max-width: 46rem" in _rule_block(css, ".tuning-shell")
assert "max-width: 46rem" in _rule_block(css, ".doc-edit-shell")
assert "max-width: 46rem" in _rule_block(css, ".theme-shell")
def test_comments_cite_the_wide_override_with_provenance() -> None:
"""The block comments that claimed the "46rem column contract" now
name base 46rem + the 2x wide override, with the owner
instruction (2026-08-31, TODO L5) as the provenance at the token
and the media block."""
# ---------- the comment contract ----------
def test_comments_carry_the_72rem_contract() -> None:
"""The stale width claims are gone from the file: no "≤46rem",
"46rem base", "92rem at >=1500px" or "1500px" wording (the
2026-08-31 contract was rewritten to the 72rem contract), with the
owner instruction (2026-09-12) as the provenance at the token and
the reading-column comments naming 72rem."""
css = _css()
# The stale "≤46rem" contract claims are gone from the file.
assert "≤46rem" not in css, (
"the stale '≤46rem' contract wording must be updated"
)
# Provenance at the two authoritative spots (token + wide block).
token_idx = css.index("--chat-column: 46rem")
wide_idx = css.index("@media (min-width: 1500px)")
assert "owner instruction 2026-08-31" in css[max(0, token_idx - 400) : token_idx]
assert "owner instruction 2026-08-31" in css[max(0, wide_idx - 500) : wide_idx]
# The chat-shell comment names base + override.
assert "46rem base" not in css, "no stale '46rem base' claim may remain"
assert "92rem at >=1500px" not in css, (
"no stale wide-override claim may remain"
)
assert "1500px" not in css, (
"the wide-desktop breakpoint wording is retired (2026-09-12)"
)
# Provenance at the authoritative spot (the token).
token_idx = css.index("--chat-column: 72rem")
assert "owner instruction 2026-09-12" in css[max(0, token_idx - 400) : token_idx]
# The chat-shell + shared-shell comments name the 72rem contract.
chat_comment = css[: css.index(".chat-shell {")]
assert "46rem base" in chat_comment and "92rem" in chat_comment
assert "72rem" in chat_comment, (
"the chat-shell comment must name the 72rem contract"
)
shared_idx = css.index(".shared-shell {")
shared_rule = css[shared_idx : shared_idx + 400]
assert "72rem" in shared_rule, (
"the shared-shell comment must name the 72rem contract"
)