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; --header-h: 64px;
/* Reading column: 46rem base (PLAN §7 lineage); 2x on wide desktops /* Reading column: 72rem — EQUAL to the .container's 72rem cap, so
(owner instruction 2026-08-31, TODO L5 / D2 — chat + shared + the reading columns (chat, shared, the document viewer's .doc-md
document view). */ + its summary panel) ride the same width as every other page
--chat-column: 46rem; (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; } * { box-sizing: border-box; }
@@ -389,8 +392,11 @@ body::before {
/* Chat is a vertical conversation: a centered, capped column is the /* Chat is a vertical conversation: a centered, capped column is the
correct layout here (PLAN §UI/UX). The surrounding frame keeps it correct layout here (PLAN §UI/UX). The surrounding frame keeps it
from collapsing into a hairline on wide screens. The cap is the from collapsing into a hairline on wide screens. The cap is the
--chat-column token: 46rem base, 2x (92rem) at >=1500px wide --chat-column token: 72rem — equal to the .container's cap, so the
desktops (owner instruction 2026-08-31, TODO.md L5 / D2). */ 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 { .chat-shell {
max-width: var(--chat-column); max-width: var(--chat-column);
margin-inline: auto; margin-inline: auto;
@@ -409,8 +415,8 @@ body::before {
flips this to a vertical stack (flex-direction: column + flips this to a vertical stack (flex-direction: column +
align-items: stretch — full-width pills, New chat above Share; the align-items: stretch — full-width pills, New chat above Share; the
existing ≤640px pill rules apply to the stacked pills unchanged). existing ≤640px pill rules apply to the stacked pills unchanged).
The reading-column contract is untouched (--chat-column: 46rem base, The reading-column contract is untouched (--chat-column: 72rem —
92rem at >=1500px — PLAN §7 lineage). the .container width at every viewport).
Phase 65 (task 03, 2026-09-01, `TODO.md` L3, owner-locked A2): the 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 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 /* GFM pipe tables (phase 44, 2026-08-27, TODO.md L6): the shared
renderer wraps every table in .md-table-wrap — the horizontal renderer wraps every table in .md-table-wrap — the horizontal
scroller, so a wide table scrolls inside the bubble instead of scroller, so a wide table scrolls inside the bubble instead of
breaking the reading column (--chat-column: 46rem base, 92rem at breaking the reading column (--chat-column: 72rem — the
>=1500px) — around a semantic <table class="md-table"> .container width at every viewport) — around a semantic
<table class="md-table">
(escape-first cells; alignment colons render left, owner decision). (escape-first cells; alignment colons render left, owner decision).
Phase-08 tokens only: --line hairline borders and the thead tinted Phase-08 tokens only: --line hairline borders and the thead tinted
from the plain surface family — --ink on --surface is 14.5:1 (PLAN 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 --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) in the path `code` ≈11.5:1 — all comfortably AA in the (single dark)
theme. Inline rows only: appending lines never shifts the chat theme. Inline rows only: appending lines never shifts the chat
column (46rem base; 92rem at >=1500px — no new container), and the column (72rem — the .container width at every viewport; no new
rows are not interactive — no focus targets. */ container), and the rows are not interactive — no focus targets. */
.tool-calls { .tool-calls {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -960,15 +967,16 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Global tuning page (phase 27) ---------- */ /* ---------- Global tuning page (phase 27) ---------- */
/* /tuning.html: create / edit / delete steering notes without a chat /* /tuning.html: create / edit / delete steering notes without a chat
conversation. Same width discipline as the chat column — a centered, conversation. Full-container width (owner instruction 2026-09-12:
capped column on the 72rem frame; the form and the note list span its "match the width of the RAG page for all other pages" — the
FULL width (no skinny lists). Every interactive target is >=44px; phase-27 capped-column discipline is superseded): the shell rides
text pairs reuse the Phase-08 AA palette (dark ink on brand 5.2:1, the .container's 72rem frame like the RAG page, and the form and
brand-ink/brand-soft 6.9:1, ok 10.6:1, err 9.1:1, ink-soft >=6.9:1). the note list span its FULL width (no skinny lists). Every
No filter: blur, no CDN, system font stack. */ 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 { .tuning-shell {
max-width: 46rem;
margin-inline: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.25rem; gap: 1.25rem;
@@ -1241,8 +1249,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Composer ---------- */ /* ---------- Composer ---------- */
/* Phase 52 (2026-08-30, TODO.md L3): the composer is PINNED to the /* Phase 52 (2026-08-30, TODO.md L3): the composer is PINNED to the
viewport bottom. The page scrolls at the document level and viewport bottom. The page scrolls at the document level and
`.chat-shell` (the centered reading column — 46rem base, 92rem at `.chat-shell` (the centered reading column — 72rem, the
>=1500px) is the composer's sticky .container width at every viewport) is the composer's sticky
containing block, so the box sticks to the bottom edge of the containing block, so the box sticks to the bottom edge of the
viewport at every scroll position and settles back into its normal viewport at every scroll position and settles back into its normal
flow position (above the footer) once the document bottom is 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 a fixed full-viewport dim backdrop + a centered panel (z-index
1000, above the sticky header (20) + skip-link (100); NO blur — 1000, above the sticky header (20) + skip-link (100); NO blur —
the phase-08 no-blur perf anchor), scaled to a compact dialog: the phase-08 no-blur perf anchor), scaled to a compact dialog:
the 46rem chat-column width or the viewport, whichever is the chat-column width (the --chat-column token — 72rem, the
narrower. Phase-08 tokens only; system fonts; no CDN. .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 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 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; position: relative;
z-index: 1; z-index: 1;
margin: auto; margin: auto;
width: min(46rem, calc(100vw - 2rem)); width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem; padding: 1.5rem;
background: var(--surface); background: var(--surface);
border: 1px solid var(--line); 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 #remove-confirm-dialog overlay contract (phase 69): a fixed
full-viewport dim backdrop + a centered panel (z-index 1000, above full-viewport dim backdrop + a centered panel (z-index 1000, above
the sticky header (20) + skip-link (100); NO blur — the phase-08 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 viewport, whichever is narrower. The box: a VISIBLE block label
(WCAG — never aria-label-only) over a mono textarea (the box is a (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 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; position: relative;
z-index: 1; z-index: 1;
margin: auto; margin: auto;
width: min(46rem, calc(100vw - 2rem)); width: min(var(--chat-column), calc(100vw - 2rem));
padding: 1.5rem; padding: 1.5rem;
background: var(--surface); background: var(--surface);
border: 1px solid var(--line); 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) ---------- /* ---------- Theme view (phase 91, tasks 04 + 05) ----------
The shell's seventh view (#view-theme): the admin palette + branding The shell's seventh view (#view-theme): the admin palette + branding
editor (task 05). The centered 46rem column (the .tuning-shell editor (task 05). The full 72rem container width (owner
language — this is a form view, the tuning-view width pattern), the instruction 2026-09-12 — the phase-91 form-column cap is
superseded), the
form card (the #tune-form language: surface fill, --line hairline, form card (the #tune-form language: surface fill, --line hairline,
radius, shadow), the fieldset groups (Branding / Palette) with the radius, shadow), the fieldset groups (Branding / Palette) with the
.theme-note sub-copy, the 3-column (desktop) / 1-column (mobile) .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 tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible
via the global 3px outline rule. No CDN, system fonts. */ via the global 3px outline rule. No CDN, system fonts. */
.theme-shell { .theme-shell {
max-width: 46rem;
margin-inline: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.25rem; gap: 1.25rem;
@@ -3347,12 +3356,13 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Shared page (phase 51, task 03) ---------- /* ---------- Shared page (phase 51, task 03) ----------
/shared/<token>: the anonymous read-only conversation (owner-locked /shared/<token>: the anonymous read-only conversation (owner-locked
2026-08-29, TODO.md L6). The shell maps to the PLAN §7 centered 2026-08-29, TODO.md L6). The shared page reads exactly like the
chat column (--chat-column: 46rem base, 92rem at >=1500px) — the chat page — both at the 72rem container width (--chat-column, now
conversation reads exactly like the chat equal to the .container cap — owner instruction 2026-09-12; the
page (the .msg/.bubble/.thinking/.tool-calls/.msg-meta rules apply wide-desktop doubling was retired) — and the .msg/.bubble/.thinking/
unchanged) with NO composer, so the column contract holds for a .tool-calls/.msg-meta rules apply unchanged, with NO composer, so
guest. Zero interactive controls (owner-locked): the chips are 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 plain text, so the pill families' pointer treatments are switched
off IN THIS SCOPE ONLY — the chat page's interactive chips keep off IN THIS SCOPE ONLY — the chat page's interactive chips keep
their styles untouched. Every pair reuses the Phase-08 AA palette 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. */ fonts. */
.shared-shell { .shared-shell {
width: 100%; width: 100%;
max-width: var(--chat-column); /* the PLAN §7 centered chat column max-width: var(--chat-column); /* the centered chat column — 72rem,
(46rem base; 92rem at >=1500px — phase 58) */ the .container width at every viewport */
margin-inline: auto; margin-inline: auto;
display: flex; display: flex;
flex-direction: column; 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-content { display: flex; flex-direction: column; }
.doc-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); } .doc-loading { margin: 1.5rem auto; text-align: center; color: var(--ink-soft); }
/* Markdown: the centered reading column — 46rem base, 2x (92rem) at /* Markdown: the centered reading column — 72rem, the .container width
>=1500px (PLAN §7.1 lineage; owner instruction 2026-08-31, TODO.md at every viewport (owner instruction 2026-09-12: "match the width
L5 / D2). */ of the RAG page for all other pages" — the wide-desktop doubling
was retired). */
.doc-md { .doc-md {
width: 100%; width: 100%;
max-width: var(--chat-column); max-width: var(--chat-column);
@@ -3576,8 +3587,8 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* md/markdown: the panel matches the .doc-md centered reading column /* md/markdown: the panel matches the .doc-md centered reading column
(46rem base; 92rem at >=1500px — the same --chat-column token) — (72rem — the same --chat-column token, the .container width at
it is the column's label. Raw formats stay full width (the 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 .doc-raw default above), matching the full-width pre; in engines
without :has() the panel degrades to that full-width default. */ without :has() the panel degrades to that full-width default. */
.doc-summary:has(+ .doc-md) { .doc-summary:has(+ .doc-md) {
@@ -3743,8 +3754,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
/* ---------- Document modal (phase 26) ---------- /* ---------- Document modal (phase 26) ----------
"New documents should open in an almost-fullscreen modal, not in a new "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 page" (TODO.md L4). The overlay reuses the viewer page's .doc-meta
badge classes, the .doc-md reading column (46rem base; 92rem at badge classes, the .doc-md reading column (72rem — the .container
>=1500px), and the .doc-raw 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, pre — this block only adds the chrome (backdrop, panel, header,
actions, scroll container). Phase-08 tokens only; NO blur (the actions, scroll container). Phase-08 tokens only; NO blur (the
phase-08 no-blur perf anchor); no new assets; system fonts. 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. /* The scroll container: vertical scroll lives HERE, never the viewport.
.doc-md keeps its centered reading column inside (46rem base; .doc-md keeps its centered reading column inside (72rem — wider
92rem at >=1500px, capped there by the 1100px panel); .doc-raw keeps than the 1100px panel's inner width, which stays its effective
its own overflow-x. tabindex="-1" in the markup is the JS focus target. */ ceiling); .doc-raw keeps its own overflow-x. tabindex="-1" in the
markup is the JS focus target. */
.doc-modal-content { .doc-modal-content {
flex: 1; flex: 1;
min-height: 0; 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, /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 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 pages, so the header is SLIM (brand + "← Back to chat" only). The
46rem base column is HARD-CODED: a form column, not a reading shell rides the full 72rem container like every other page (owner
column — it does not ride --chat-column, so phase 58's wide-desktop instruction 2026-09-12 — the phase-59 form-column cap is
doubling never stretches the form. The .sources-gate gate is reused superseded). The .sources-gate gate is reused verbatim
verbatim (phases 16/35/50). Every pair reuses the Phase-08 AA (phases 16/35/50). Every pair reuses the Phase-08 AA palette;
palette; touch targets >=44px; :focus-visible via the global 3px touch targets >=44px; :focus-visible via the global 3px outline
outline rule. No CDN, system fonts. */ rule. No CDN, system fonts. */
.doc-edit-shell { .doc-edit-shell {
width: 100%; width: 100%;
max-width: 46rem; /* the 46rem base column (hard-coded — see above) */
margin-inline: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.25rem; gap: 1.25rem;
@@ -4385,9 +4396,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
.theme-reset { width: 100%; } .theme-reset { width: 100%; }
/* Phase 51: the shared page squeezes like the chat column — the /* Phase 51: the shared page squeezes like the chat column — the
title and the note step down (the empty-state-title family); the title and the note step down (the empty-state-title family); the
shell keeps its base 46rem column (the >=1500px 92rem override shell is full-width here (the 72rem cap never binds on a phone —
never applies here — it is already the narrowest box on the the container is already 100%) and .msg-body's 92% override
page) and .msg-body's 92% override above applies. */ above applies. */
#shared-title { font-size: 1.35rem; } #shared-title { font-size: 1.35rem; }
.shared-note { font-size: 0.88rem; } .shared-note { font-size: 0.88rem; }
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; } .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 /* Phase 46: prefers-reduced-motion stills the mobile menu — no
180ms slide+fade; open/close snaps (the visibility/opacity flip 180ms slide+fade; open/close snaps (the visibility/opacity flip
applies instantly) and stays correct. BOTH states are named: the 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 a deliberately wide 5-column table — byte-stable, so the story E2E
can assert the rendered ``<table class="md-table">`` shape, the can assert the rendered ``<table class="md-table">`` shape, the
escaped XSS line, and the wrapper's horizontal scroll inside 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 deflection prompt never carries the marker, same reasoning as
``SUMMARY_MODE``), so a marker question always gets the table ``SUMMARY_MODE``), so a marker question always gets the table
answer; the E2E asks it against an on-topic fixture (HIGH gate) and 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 #: The fixed table answer (phase 44) — byte-stable on purpose: the story
#: E2E asserts the rendered table shape, the escaped ``<img onerror>`` #: E2E asserts the rendered table shape, the escaped ``<img onerror>``
#: line (the XSS payload must survive the mock byte-for-byte), and the #: 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 #: Phase 74 (chat history, TODO L4): a user message containing this
#: substring (case-insensitive) gets the deterministic HISTORY ECHO #: substring (case-insensitive) gets the deterministic HISTORY ECHO
#: (``_history_echo`` below — see the module docstring): the prior-turn #: (``_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 # <img onerror> XSS probe line (it must survive the mock
# byte-for-byte so the E2E can prove the renderer neutralizes # byte-for-byte so the E2E can prove the renderer neutralizes
# it), and a wide 5-column table (guarantees scrollWidth > # 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 # BEFORE the DEFLECT_MODE branch: a deflection prompt never
# carries the marker (it lives in the user message, same # carries the marker (it lives in the user message, same
# reasoning as SUMMARY_MODE), so a marker question always gets # 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. renders as escaped text; no dialog fires.
6. ``test_standalone_page_still_works`` — the dedicated ``/document.html`` 6. ``test_standalone_page_still_works`` — the dedicated ``/document.html``
page keeps its phase-10 contract (title/content/badges, not-found, 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 7. ``test_modal_theme_and_no_cdn`` — dark page background, the panel on
the Phase-08 surface colour, every asset same-origin or ``data:``. 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") expect(page.locator(".doc-shell")).to_have_attribute("aria-live", "polite")
assert page.evaluate("() => document.activeElement && document.activeElement.id") == "main" 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() 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}" 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). NOT deflected (the honesty-gate interplay is part of the contract).
2. ``test_wide_table_scrolls`` — the wide table's wrapper has 2. ``test_wide_table_scrolls`` — the wide table's wrapper has
``scrollWidth > clientWidth`` and horizontal scroll moves it; the ``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 3. ``test_table_xss_safe`` — the ``<img onerror>`` line renders as
visible, escaped text: zero injected ``<img>`` nodes, no dialog. visible, escaped text: zero injected ``<img>`` nodes, no dialog.
4. ``test_viewer_table_renders`` — the fixture's pipe table opens from 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") after = wrap.evaluate("el => el.scrollLeft")
assert after > before, "the wrapper must scroll horizontally" assert after > before, "the wrapper must scroll horizontally"
# The 46rem chat column must not break the page: no horizontal # The 72rem container column must not break the page: no horizontal
# document overflow (PLAN §7.1). # document overflow (PLAN §7.1, as revised by phase 100).
page_scroll, page_client = page.evaluate( page_scroll, page_client = page.evaluate(
"() => [document.documentElement.scrollWidth, document.documentElement.clientWidth]" "() => [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 1. ``test_no_horizontal_overflow_at_viewports`` — 360/375/768/1280/1600 on
both pages: ``documentElement.scrollWidth <= clientWidth``. both pages: ``documentElement.scrollWidth <= clientWidth``.
2. ``test_chat_column_capped_and_centered`` — the reading column rides 2. ``test_chat_column_capped_and_centered`` — the reading column rides
--chat-column (46rem base; 92rem at >=1500px, phase 58 / owner --chat-column (72rem — EQUAL to the .container's cap at every
instruction 2026-08-31 TODO L5): at 1600px (a wide desktop) the viewport, phase 100 / owner instruction 2026-09-12: the 46rem base
``.chat-shell`` is 92rem (1472px, ±2%) and horizontally centered + the >=1500px 92rem doubling are retired): at 1600px (a wide
(±2%); at 1280px (below the wide breakpoint) it stays ≤ 46rem desktop) the ``.chat-shell`` is the 72rem container (1152px, ±2%)
(736px, +2%); at 768px the column uses most of the width (no and horizontally centered (±2%); at 1280px the same 1152px (the
mid-column dead zones). 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 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 the container; below 640px the table keeps its 640px min-width and the
wrapper scrolls horizontally instead of squeezing. wrapper scrolls horizontally instead of squeezing.
@@ -55,8 +56,11 @@ REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "docs" FIXTURES = REPO / "tests" / "fixtures" / "docs"
VIEWPORTS = ((360, 740), (375, 812), (768, 1024), (1280, 800), (1600, 900)) 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) # Phase 100: the ONE width — the 72rem container, border-box (the
CHAT_SHELL_WIDE_PX = 92 * 16 # 1472px — the 2x wide override (phase 58, >=1500px) # 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). # Mock-LLM marker for a 3s pre-token window (see tests/e2e/mock_llm.py).
SLOW_QUESTION = "pretend to think slowly, please" 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( def test_chat_column_capped_and_centered(
browser: Browser, app_url: str, db_ready: None browser: Browser, app_url: str, db_ready: None
) -> None: ) -> None:
"""AC1 (phase 58 contract): the reading column doubles to 92rem on """AC1 (phase 100 contract): the reading column is the 72rem
wide desktops (>=1500px — 1600px here), stays at the 46rem base container at every viewport — 1600px here (the cap binds, not the
below the breakpoint (1280px), and still uses most of the width on viewport) and 1280px alike (1152px, ±2%, centered), and still
tablets (no mid-column dead zones).""" 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}) page = browser.new_page(viewport={"width": 1600, "height": 900})
try: try:
page.goto(f"{app_url}/") page.goto(f"{app_url}/")
box = page.locator(".chat-shell").bounding_box() box = page.locator(".chat-shell").bounding_box()
assert box is not None assert box is not None
assert CHAT_SHELL_WIDE_PX * 0.98 <= box["width"] <= CHAT_SHELL_WIDE_PX * 1.02, ( assert CHAT_SHELL_CONTAINER_PX * 0.98 <= box["width"] <= CHAT_SHELL_CONTAINER_PX * 1.02, (
f"at 1600px (>=1500px) the chat column is {box['width']:.0f}px, " f"at 1600px the chat column is {box['width']:.0f}px, "
f"not the 92rem wide override (±2%)" f"not the 72rem container (1152px, ±2%)"
) )
center = box["x"] + box["width"] / 2 center = box["x"] + box["width"] / 2
assert abs(center - 1600 / 2) <= 0.02 * 1600, ( assert abs(center - 1600 / 2) <= 0.02 * 1600, (
@@ -250,9 +255,11 @@ def test_chat_column_capped_and_centered(
narrow.goto(f"{app_url}/") narrow.goto(f"{app_url}/")
box = narrow.locator(".chat-shell").bounding_box() box = narrow.locator(".chat-shell").bounding_box()
assert box is not None assert box is not None
assert box["width"] <= CHAT_SHELL_CAP_PX * 1.02, ( # 1280 > 1152: the container cap binds — the SAME 72rem width as
f"at 1280px (<1500px) the chat column {box['width']:.0f}px exceeds " # at 1600px (the cap no longer depends on the viewport at all).
f"the 46rem base cap (+2%)" 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: finally:
narrow.close() narrow.close()
+3 -2
View File
@@ -41,7 +41,8 @@ unshare, layout) are unchanged.
* **Layout (L6 / A5)** — desktop 1280×800: ``#new-chat-btn`` and * **Layout (L6 / A5)** — desktop 1280×800: ``#new-chat-btn`` and
``#share-chat-btn`` share one horizontal row inside ``.chat-actions`` ``#share-chat-btn`` share one horizontal row inside ``.chat-actions``
(overlapping y bands, Share's x beyond New chat's x + width, each (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); 390×844: stacked vertically (Share below New chat, full-width);
360px wide: no horizontal page overflow; 360px wide: no horizontal page overflow;
* **Admin still works (A1 sanity)** — the same auto-save machinery * **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, # Desktop (1280×800): one horizontal row — overlapping y bands,
# Share to the right of New chat, each pill at its INTRINSIC width # 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() nb = new_btn.bounding_box()
sb = share_btn.bounding_box() sb = share_btn.bounding_box()
assert nb is not None and sb is not None 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() expect(page.locator("#doc-content .doc-md")).not_to_be_empty()
# The doc must actually scroll (the ~40,000-char body fills many # 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") sh = page.evaluate("() => document.documentElement.scrollHeight")
assert sh > VIEWPORT_H, ( assert sh > VIEWPORT_H, (
f"the long document must make the page scroll " 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 # The tag is GONE from the live document (theme.js reconciles the
# #bor-theme tag's DOM text to the settled, now-default values — # #bor-theme tag's DOM text to the settled, now-default values —
# the no-op case removes the tag and clears the <html> # the no-op case removes the tag and clears the <html>
# overrides)… # overrides). The removal lands in the post-PUT refetch's
assert 'id="bor-theme"' not in page.content(), ( # reconcile (the result line above only signals the PUT landed),
"the reset document must be tag-free" # 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 # …and a FRESH load serves NO tag at all (the all-NULL row is the
# no-op injection). # no-op injection).
r = httpx.get(app_url + "/", timeout=10) r = httpx.get(app_url + "/", timeout=10)
+252 -179
View File
@@ -1,16 +1,33 @@
"""Phase 58 E2E (Playwright): the 2x reading column on wide desktops — """Phase 100 E2E (Playwright): every page matches the RAG page's width —
measured bounding-box widths, not CSS pins. the full 72rem container at every viewport, measured bounding boxes.
TODO.md L5 (owner instruction 2026-08-31, roadmap confirmation D2 + Owner request (chat, 2026-09-12): "The theme, tuning, and chat pages
expansion): "The chat response needs to be 2x wider on wide desktops. are still pretty narrow, I want you to match the width of the RAG page
there's a lot of unused space." — extended to the document view. The for all other pages to keep things consistent." — SUPERSEDES the
owner-locked contract: viewport >=1500px doubles ``--chat-column`` to phase-58 2026-08-31 46rem/92rem reading-column contract (recorded per
92rem (1472px at the 16px root) for the four reading shells — the phase-94/96/97 convention in `.agents/phases/todo/
``.chat-shell``, ``.shared-shell``, ``.doc-md`` and 100_page_width_consistency/00_phase.md`, decisions D1–D4). The phase-58
``.doc-summary:has(+ .doc-md)`` — while everything below the breakpoint suite this file used to be updates IN PLACE to the new contract (the
renders exactly as before (46rem / 736px) and ``.tuning-shell`` (a phase-97 task-07/08 precedent: a completed phase's suite follows its
form, not a reading surface) never widens (CSS-pinned by the unit changed contract; its seeding helpers are kept).
suite, task 01 — the browser proof of the MEASURED width is this file).
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``): 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: Test → contract mapping:
1. ``test_chat_column_wide_vs_base`` — ``/`` at 1920×1080: the 1. ``test_all_columns_match_the_rag_page`` — the core pin: at
``.chat-shell`` bounding box is 1472px (92rem, ±4px) and centered; 1280×800 the ``.chat-shell`` (``/``), ``.tuning-shell``
at 1280×800 (below the wide breakpoint) it is back to 736px (``/tuning.html``), ``.theme-shell`` (``/theme.html``) and the RAG
(46rem, ±4px). page's ``.sources-shell`` (``/sources.html``) bounding boxes are ALL
2. ``test_document_column_wide_vs_base`` — a seeded markdown document EQUAL (±4px) — the owner's "match the width of the RAG page" as one
that carries a summary: at 1920 both ``.doc-md`` and the adjacent assertion; at 1920×1080 the same four are still equal to each other
``.doc-summary:has(+ .doc-md)`` panel are 1472px; at 1280 and each measures ≈1152px (72rem, ±4px).
``.doc-md`` is 736px. 2. ``test_reader_columns_wide`` — the token's consumers at 1920×1080:
3. ``test_shared_column_wide`` — a real auto-saved conversation shared the modal panel is still ~1100px (D2: the modal is UNCHANGED) with
by token: ``/shared/<token>`` at 1920 renders ``.shared-shell`` at its ``.doc-md`` at ~1058px; ``/shared/<token>``'s ``.shared-shell``
1472px in a fresh anonymous context. is ≈1152px; the standalone ``document.html`` page's ``.doc-md`` is
4. ``test_narrow_unchanged`` — 360px: no horizontal overflow and the ≈1112px (the 72rem container's inner content).
``.chat-shell`` is the existing mobile rule — full-bleed at the 3. ``test_narrow_unchanged`` — the no-regression leg: at 360×800 no
viewport width (the shell IS its .container; the 0.9rem mobile horizontal overflow and ``.chat-shell`` is the viewport width
gutters live in its own padding, inside the measured box), 900px: (full-bleed — the mobile gutters sit in the container's padding,
the shell holds the 46rem base (736px, not the 1472px wide rule — inside the measured box); at 900×600 chat/tuning/theme are all
at 900px a leaked wide override would pin the shell to the 860px 900px wide, equal to each other.
content box instead, so 736px is the discriminator) — the wide
rule does not leak below 1500px.
DB isolation: the shared-chat row is deleted in a ``finally`` (admin DB isolation: the shared-chat row is deleted in a ``finally`` (admin
cookie — the house pattern of test_share_chat.py, whose distinctive cookie — the house pattern of test_share_chat.py, whose distinctive
question text keeps the auto-title unique); the fixture document rows question text keeps the auto-title unique); the fixture document rows
follow the story-fixture convention of the sibling suites (truncate + follow the story-fixture convention of the sibling suites (truncate +
re-import; the summary document is a direct row insert, the re-import; the viewer fixture is a direct row insert — the viewer is
test_document_viewer.py XSS-fixture pattern — the viewer is database-only, the test_document_viewer.py XSS-fixture pattern).
database-only).
""" """
from __future__ import annotations from __future__ import annotations
@@ -70,14 +84,22 @@ from e2e.auth_helpers import login
REPO = Path(__file__).resolve().parents[2] REPO = Path(__file__).resolve().parents[2]
FIXTURES = REPO / "tests" / "fixtures" / "docs" FIXTURES = REPO / "tests" / "fixtures" / "docs"
# The two proof viewports (task spec): wide desktop vs just below the # The phase-100 measured contract (16px root, the task spec):
# 1500px breakpoint, at the 16px root the phase-58 rem contract: # * CONTAINER_PX — 72rem border-box, the 2×1.25rem gutters INSIDE the
# 92rem = 1472px, 46rem = 736px. ±4px tolerance (task spec). # box; every .container.*-shell measures this at any viewport
WIDE_PX = 92 * 16 # 1472px — the 2x wide override (>=1500px) # >= ~1200px (the RAG page's width — the owner's ask).
BASE_PX = 46 * 16 # 736px — the --chat-column base # * DOC_MD_STANDALONE_PX — the standalone document page's .doc-md: the
TOL_PX = 4 # container's inner content (CONTAINER_PX minus the gutters).
WIDE_VIEWPORT: ViewportSize = {"width": 1920, "height": 1080} # * MODAL_PANEL_PX / DOC_MD_MODAL_PX — the SAME-PAGE modal is untouched
BASE_VIEWPORT: ViewportSize = {"width": 1280, "height": 800} # (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" MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"
SHARE_URL_RE = re.compile(r"^/shared/[0-9a-f-]{36}$") 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: def _seed_summary_doc() -> None:
"""One markdown document carrying a stored summary — the ONLY shape """One markdown document carrying a stored summary — the viewer
that renders both reading-column pins on one page: ``.doc-summary`` fixture the phase-100 suite opens in BOTH the same-page modal and
(the phase-36 panel) directly above ``.doc-md`` (the ``:has( the standalone document page (the test_document_viewer.py
+ .doc-md)`` sibling match). Direct row insert — the viewer is XSS-fixture pattern: a direct row insert, the viewer is
database-only (the test_document_viewer.py XSS-fixture pattern).""" database-only)."""
with SessionLocal() as db: with SessionLocal() as db:
db.add( db.add(
Document( Document(
@@ -153,11 +175,11 @@ def _seed_summary_doc() -> None:
title=FIXTURE_TITLE, title=FIXTURE_TITLE,
content=( content=(
"# Wide Column Fixture\n\n" "# Wide Column Fixture\n\n"
"Phase-58 width pin: a markdown document with a " "Phase-100 width pin: a markdown document with a "
"summary, so the viewer renders the .doc-summary " "summary, opened in the same-page modal and the "
"panel directly above the .doc-md column." "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, content_hash="w" * 64,
indexed_at=datetime.now(UTC), 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: def _assert_width(page: Page, selector: str, expected_px: int, label: str) -> None:
"""The element's bounding-box width is ``expected_px`` ±4px (task """The element's bounding-box width is ``expected_px`` ±4px (task
spec) — the measured rendered width, not the computed style.""" spec) — the measured rendered width, not the computed style."""
box = page.locator(selector).first.bounding_box() width = _box_width(page, selector, label)
assert box is not None, f"{selector} not rendered ({label})" assert abs(width - expected_px) <= TOL_PX, (
assert abs(box["width"] - expected_px) <= TOL_PX, ( f"{label}: {selector} is {width:.1f}px, want {expected_px}px ±{TOL_PX}px"
f"{label}: {selector} is {box['width']:.1f}px, "
f"want {expected_px}px ±{TOL_PX}px"
) )
@@ -265,170 +302,206 @@ 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) 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 browser: Browser, app_url: str, db_ready: None
) -> None: ) -> None:
"""The chat page's .chat-shell doubles at the 1500px breakpoint: """The owner's "match the width of the RAG page" as ONE assertion:
92rem (1472px, ±4px) at 1920×1080, centered; 46rem (736px, ±4px) at 1280×800 the four shells are all EQUAL (±4px); at 1920×1080
at 1280×800 — the base below the breakpoint.""" they are still equal to each other and each measures ≈1152px
wide = browser.new_page(viewport=WIDE_VIEWPORT) (72rem at the 16px root, ±4px — the box includes the container's
try: 2×1.25rem padding, border-box)."""
login(wide, app_url, next="/") # phase 79: the chips need a session # --- 1280×800 (just above the container cap — the owner's report) ---
wide.locator("#suggestions .suggestion-chip").first.wait_for( widths = _measure_shells(browser, app_url, VIEWPORT_1280)
state="visible", timeout=10_000 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) # --- 1920×1080: equal AND each the 72rem container (≈1152px) ------
try: widths = _measure_shells(browser, app_url, VIEWPORT_1920)
login(base, app_url, next="/") # phase 79: the chips need a session ref_selector, ref = next(iter(widths.items()))
base.locator("#suggestions .suggestion-chip").first.wait_for( for selector, width in widths.items():
state="visible", timeout=10_000 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 # 2. The token's consumers at 1920×1080: the modal (unchanged, D2), the
# widths — the owner-expanded surface # 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 browser: Browser, app_url: str, mock_llm: int, db_ready: None
) -> None: ) -> None:
"""A seeded md document (with a summary) renders .doc-md at 1472px """At 1920×1080: the same-page document MODAL is untouched (D2 —
at 1920 and 736px at 1280 — and the adjacent .doc-summary panel the panel is still ~1100px and IT binds the ``.doc-md`` inside,
matches the column at 1920 (.doc-summary:has(+ .doc-md)).""" ~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) _reset_db(mock_llm, seed=True)
_seed_summary_doc() _seed_summary_doc()
wide = browser.new_page(viewport=WIDE_VIEWPORT) page = browser.new_page(viewport=VIEWPORT_1920)
try: try:
login(wide, app_url, next="/") # phase 79: the viewer content is gated # (a) the same-page modal — the 1100px panel stays its effective
wide.goto(app_url + FIXTURE_DOC_URL) # ceiling (D2: pin the PANEL, not the 1152 the column would be).
expect(wide.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000) login(page, app_url, next="/sources.html")
expect(wide.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000) page.locator("#folders-tbody .folder-link").first.wait_for(
expect( state="visible", timeout=15_000
wide.locator(".doc-summary:has(+ .doc-md)") )
).to_have_count(1, timeout=15_000) _drill(page, FIXTURE_SOURCE, "notes")
_assert_width(wide, "#doc-content .doc-md", WIDE_PX, "document @ 1920px") 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( _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) # (b) the standalone document page: the 72rem container's inner
try: # content (1152 − 2×1.25rem gutters).
login(base, app_url, next="/") # phase 79: the viewer content is gated page.goto(app_url + FIXTURE_DOC_URL)
base.goto(app_url + FIXTURE_DOC_URL) expect(page.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000)
expect(base.locator("#doc-title")).to_have_text(FIXTURE_TITLE, timeout=15_000) expect(page.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000)
expect(base.locator("#doc-content .doc-md")).to_be_visible(timeout=15_000) _assert_width(
_assert_width(base, "#doc-content .doc-md", BASE_PX, "document @ 1280px") 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)
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.
r = httpx.post(f"{app_url}/api/chats/{chat_id}/share", timeout=10)
assert r.status_code == 200
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=VIEWPORT_1920)
anon = anon_ctx.new_page()
anon.set_default_timeout(30_000)
anon.goto(app_url + share_url)
expect(anon.locator("#shared-title")).to_have_text(
_auto_title(q), timeout=15_000
)
expect(anon.locator(".msg.brain .bubble")).to_contain_text(
MOCK_ANSWER_MARKER, timeout=30_000
)
_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: finally:
base.close() page.close()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 3. Shared page: .shared-shell at 1472px for a guest at 1920 # 3. Below the container cap: full-width as today — 360px overflow-free,
# --------------------------------------------------------------------------- # 900px chat == tuning == theme (the cap never bound below 72rem)
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)
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"]
anon_ctx = None
try:
# Public since phase 55 — the share endpoint takes no session.
r = httpx.post(f"{app_url}/api/chats/{chat_id}/share", timeout=10)
assert r.status_code == 200
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 = anon_ctx.new_page()
anon.set_default_timeout(30_000)
anon.goto(app_url + share_url)
expect(anon.locator("#shared-title")).to_have_text(
_auto_title(q), timeout=15_000
)
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_centered(anon, ".shared-shell", 1920, "shared @ 1920px")
finally:
if anon_ctx is not None:
anon_ctx.close()
_delete_chat(app_url, cookies, chat_id)
# ---------------------------------------------------------------------------
# 4. Below the breakpoint: 360px and 900px are byte-for-byte the old
# rules — no overflow, no leaked wide column
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_narrow_unchanged( def test_narrow_unchanged(
browser: Browser, app_url: str, db_ready: None browser: Browser, app_url: str, db_ready: None
) -> None: ) -> None:
"""The min-width:1500px override must not leak below the """The mobile/tablet layouts are UNCHANGED (D3): at 360×800 no
breakpoint: at 360px no horizontal overflow and the shell is the horizontal overflow and ``.chat-shell`` is the viewport width
existing mobile rule — full-bleed at the viewport width (the shell (full-bleed — the shell IS its .container, so the mobile gutters sit
IS its .container, so the 0.9rem mobile gutters sit in its own in its padding, inside the measured box); at 900×600 the
padding, inside the measured box); at 900px the shell holds the chat/tuning/theme shells are ALL 900px wide, equal to each other
46rem base (736px — a leaked 92rem rule would pin it to the 860px (the cap never bound below 72rem anyway)."""
content box instead, so 736px is the discriminator).""" phone = browser.new_page(viewport={"width": 360, "height": 800})
phone = browser.new_page(viewport={"width": 360, "height": 740})
try: try:
login(phone, app_url, next="/") # phase 79: the chips need a session login(phone, app_url, next="/")
phone.locator("#suggestions .suggestion-chip").first.wait_for( phone.locator(".chat-shell").wait_for(state="visible", timeout=10_000)
state="visible", timeout=10_000
)
_assert_no_doc_overflow(phone, "chat @ 360px") _assert_no_doc_overflow(phone, "chat @ 360px")
_assert_width(phone, ".chat-shell", 360, "chat @ 360px") _assert_width(phone, ".chat-shell", 360, "chat @ 360px")
finally: finally:
phone.close() phone.close()
tablet = browser.new_page(viewport={"width": 900, "height": 800}) tablet = browser.new_page(viewport={"width": 900, "height": 600})
try: try:
login(tablet, app_url, next="/") # phase 79: the chips need a session login(tablet, app_url, next="/")
tablet.locator("#suggestions .suggestion-chip").first.wait_for( tablet.locator(".chat-shell").wait_for(state="visible", timeout=10_000)
state="visible", timeout=10_000
)
_assert_no_doc_overflow(tablet, "chat @ 900px") _assert_no_doc_overflow(tablet, "chat @ 900px")
_assert_width(tablet, ".chat-shell", BASE_PX, "chat @ 900px") widths = {".chat-shell": _box_width(tablet, ".chat-shell", "chat @ 900px")}
_assert_centered(tablet, ".chat-shell", 900, "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: finally:
tablet.close() 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 * the house shell (AGENTS.md rule 5 + the login.html/shared.html
minimal-flow-page lineage): skip-link, the SLIM header (brand + minimal-flow-page lineage): skip-link, the SLIM header (brand +
"Back to chat" — no nav), the 46rem base column (hard-coded — a form "Back to chat" — no nav), the full 72rem container (the phase-59
column, NOT ``--chat-column``), the ``container`` frame; form-column cap is retired — owner instruction 2026-09-12), the
``container`` frame;
* the form contract: ``#draft-title`` / ``#draft-path`` / * the form contract: ``#draft-title`` / ``#draft-path`` /
``#draft-body`` with visible labels, ``#push-doc-btn`` (the exact ``#draft-body`` with visible labels, ``#push-doc-btn`` (the exact
"Push to docs branch" copy) + the back link, ``#push-status`` "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 ---------- # ---------- styles.css: the new classes ----------
def test_doc_edit_shell_is_the_hardcoded_46rem_column() -> None: def test_doc_edit_shell_rides_the_full_container() -> None:
""".doc-edit-shell: the 46rem base column — HARD-CODED 46rem (a """.doc-edit-shell: the full 72rem container (owner instruction
form column, not a reading column — it must NOT ride 2026-09-12 — the phase-59 form-column cap is superseded): no
--chat-column, so phase 58's wide-desktop doubling never stretches max-width, no margin-inline (the .container ancestor centers), a
the form), centered, a flex column on the container frame.""" flex column on the container frame."""
css = _css() css = _css()
block = re.search(r"\.doc-edit-shell \{([\s\S]*?)\n\}", css) block = re.search(r"\.doc-edit-shell \{([\s\S]*?)\n\}", css)
assert block, "styles.css must style .doc-edit-shell" assert block, "styles.css must style .doc-edit-shell"
body = block.group(1) 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, ( assert "--chat-column" not in body, (
"the form column does not ride --chat-column (phase 58 must " "the shell rides the .container frame, not the reading-column "
"not stretch it)" "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 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: def test_table_wrap_is_the_horizontal_scroller() -> None:
""".md-table-wrap { overflow-x: auto } — the wrapper (not the """.md-table-wrap { overflow-x: auto } — the wrapper (not the
table) is the scroller, so a wide table scrolls inside the bubble 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) css = _text(STYLES_CSS)
block = re.search(r"\.md-table-wrap\s*\{([^}]*)\}", css) block = re.search(r"\.md-table-wrap\s*\{([^}]*)\}", css)
assert block, "styles.css must define .md-table-wrap" 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). cannot do).
The chat page scrolls at the DOCUMENT level and `.chat-shell` (the The chat page scrolls at the DOCUMENT level and `.chat-shell` (the
centered 46rem column, PLAN §7.1) is the composer's sticky containing centered 72rem column — the .container width, phase 100) is the
block. The pin is therefore TWO rules, and both are pinned here: 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, * `.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 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 9.3:1, the err-line border — the .tuning-delete / .steering-delete
convention; the hover inverts to --bg on --err-line, 5.2:1); 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 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 error line is the err pair; the panel caps at the chat-column
chat-column width or the viewport.""" width (the --chat-column token — 72rem, owner instruction
2026-09-12) or the viewport."""
css = _css() css = _css()
btn = css[css.find(".remove-confirm-btn {"):] btn = css[css.find(".remove-confirm-btn {"):]
btn = btn[: btn.find("\n}")] 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 assert "var(--err-ink)" in err and "var(--err-bg)" in err
panel = css[css.find(".remove-confirm-panel {"):] panel = css[css.find(".remove-confirm-panel {"):]
panel = panel[: panel.find("\n}")] 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 chat page's interactive builders, and the rendered messages contain
no button/form/link — the chips are plain spans, the source chips no button/form/link — the chips are plain spans, the source chips
carry no ``href``; carry no ``href``;
* the shared shell's reading-column mapping (--chat-column token, * the shared shell's reading-column mapping (--chat-column token —
phase 58) + the static-chip and the 72rem container width, phase 100) + the static-chip and
invalid-state CSS (the ≤640px squeeze included). invalid-state CSS (the ≤640px squeeze included).
""" """
from __future__ import annotations 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: def test_shared_shell_maps_to_the_reading_column() -> None:
"""The PLAN §7 column contract (phase 58): .shared-shell is the """The 72rem-everywhere contract (phase 100, owner instruction
centered chat column riding the --chat-column token (46rem base, 2026-09-12): .shared-shell is the centered chat column riding the
92rem at >=1500px wide desktops — owner instruction 2026-08-31, --chat-column token — equal to the .container's cap, so the shared
TODO L5 / D2). The conversation reads exactly like the chat page reads at the RAG page's width. The conversation reads exactly
page's, so the existing .msg/.bubble CSS applies unchanged.""" like the chat page's, so the existing .msg/.bubble CSS applies
unchanged."""
css = _css() css = _css()
block = re.search(r"\.shared-shell \{([\s\S]*?)\n\}", css) block = re.search(r"\.shared-shell \{([\s\S]*?)\n\}", css)
assert block, "styles.css must style .shared-shell" assert block, "styles.css must style .shared-shell"
body = block.group(1) body = block.group(1)
assert "max-width: var(--chat-column)" in body, ( 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], ( assert "46rem" not in body.split("/*")[0], (
"no hard-coded cap — the width rides the token" "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 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 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 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.""" label is ink-soft (5.1:1) — never a label-less textarea."""
css = _css() css = _css()
btn = _css_rule(css, ".ignore-editor-btn") 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") err = _css_rule(css, ".ignore-editor-error")
assert "var(--err-ink)" in err and "var(--err-bg)" in err assert "var(--err-ink)" in err and "var(--err-bg)" in err
panel = _css_rule(css, ".ignore-editor-panel") 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") label = _css_rule(css, ".ignore-editor-label")
assert "display: block" in label and "var(--ink-soft)" in label, ( assert "display: block" in label and "var(--ink-soft)" in label, (
"a visible block label (WCAG — never aria-label-only)" "a visible block label (WCAG — never aria-label-only)"
+125 -110
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 The measured-width browser proof is E2E-gated by the phase's width
suite (task 02); like the other frontend-adjacent unit files (the suite (task 03); like the other frontend-adjacent unit files (the
test_save_chat_ui.py pattern), this module pins the styles.css markers 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: without a browser:
* the ``--chat-column`` custom property in ``:root`` — 46rem base * the ``--chat-column`` custom property in ``:root`` — 72rem, EQUAL to
(the PLAN §7 column lineage) with the provenance comment (owner the ``.container``'s 72rem cap (owner instruction 2026-09-12:
instruction 2026-08-31, TODO L5 / D2); "match the width of the RAG page for all other pages" — supersedes
* the ``@media (min-width: 1500px)`` block at the bottom of the the 2026-08-31 instruction), with the provenance comment;
responsive region — the SINGLE place that doubles the token to * NO ``@media (min-width: 1500px)`` block anywhere in the file — the
92rem (2x); phase-58 wide-desktop doubling is deleted in full (the token is
72rem at every viewport);
* the four reading-column selectors — ``.chat-shell``, * the four reading-column selectors — ``.chat-shell``,
``.shared-shell``, ``.doc-md``, ``.doc-summary:has(+ .doc-md)`` — ``.shared-shell``, ``.doc-md``, ``.doc-summary:has(+ .doc-md)`` —
each capped with ``max-width: var(--chat-column)`` and NOTHING else each capped with ``max-width: var(--chat-column)`` and NOTHING else
in the file uses the token (exactly four rules); in the file uses the token for a max-width (exactly four rules);
* the negative pin — the form columns (``.tuning-shell``; and from * the flipped negative pin — ZERO literal ``max-width: 46rem`` rules
phase 59, task 06, ``.doc-edit-shell`` — forms, not reading remain (the phase-27/59/91 form-column caps are retired): the form
surfaces) are the only literal ``max-width: 46rem`` rules left in shells (``.tuning-shell``, ``.theme-shell``, ``.doc-edit-shell``)
the file, kept hard-coded so the wide-desktop doubling never carry no cap, no token, and no ``margin-inline`` — structurally
stretches a form; ``.sources-shell``;
* the "46rem column contract" block comments were updated to name the * the two source-page dialogs (phase 69 remove-confirm, phase 89
base value + the wide override (the stale "≤46rem" contract claims ignore-editor) cap at the chat-column width THROUGH THE TOKEN —
are gone from the reading-column comments). ``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 from __future__ import annotations
@@ -58,57 +62,52 @@ def _rule_block(css: str, selector: str) -> str:
# ---------- the --chat-column token ---------- # ---------- the --chat-column token ----------
def test_root_declares_chat_column_46rem_base() -> None: def test_root_declares_chat_column_72rem_equal_to_the_container() -> None:
""":root declares --chat-column: 46rem (the PLAN §7 base) with the """:root declares --chat-column: 72rem — EQUAL to the .container's
owner-provenance comment (instruction 2026-08-31, TODO L5).""" 72rem cap (one width for everything) — with the owner-provenance
comment (instruction 2026-09-12)."""
css = _css() css = _css()
root = _rule_block(css, ":root") root = _rule_block(css, ":root")
assert "--chat-column: 46rem" in root, ( assert "--chat-column: 72rem" in root, (
":root must declare the --chat-column base (46rem)" ":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("*/")] 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 " "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: def test_the_wide_desktop_doubling_is_retired() -> None:
"""A @media (min-width: 1500px) block sets --chat-column: 92rem on """No @media (min-width: 1500px) block remains (the phase-58 2x
:root — the single wide override (2x the base).""" override is deleted in full, 2026-09-12) and the token is never
assigned the old wide value — 72rem at every viewport."""
css = _css() css = _css()
m = re.search(r"@media \(min-width: 1500px\) \{", css) assert "@media (min-width: 1500px)" not in css, (
assert m, "styles.css must carry the @media (min-width: 1500px) block" "the wide-desktop media block must be deleted (retired 2026-09-12)"
start = css.index("{", m.start()) )
depth = 0 assert "--chat-column: 92rem" not in css, (
for i in range(start, len(css)): "the token is never doubled to the old wide value"
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; }"
) )
# 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: def test_no_literal_46rem_width_remains() -> None:
"""The min-width sibling sits alongside the max-width responsive """Flipped negative pin (phase 100 D1): ZERO literal
blocks at the bottom of the file (after the <=640px block).""" 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() css = _css()
wide = css.index("@media (min-width: 1500px)") assert css.count("max-width: 46rem") == 0, (
mobile = css.rindex("@media (max-width: 640px)") "no literal max-width: 46rem rule may remain (retired 2026-09-12)"
assert wide > mobile, ( )
"the wide override belongs in the bottom media-query region" 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 """.chat-shell, .shared-shell, .doc-md and
.doc-summary:has(+ .doc-md) each cap with .doc-summary:has(+ .doc-md) each cap with
max-width: var(--chat-column) — and exactly those four rules use 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() css = _css()
for selector in ( for selector in (
".chat-shell", ".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: def test_doc_md_keeps_width_100_under_the_cap() -> None:
""".doc-md stays width:100% under the token cap (the modal's """.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") 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: def test_the_form_shells_ride_the_full_container() -> None:
""".tuning-shell (the form column, out of scope) keeps its """The form shells (.tuning-shell — phase 27, .theme-shell —
hard-coded max-width: 46rem at every width — it never widens.""" 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() css = _css()
tuning = _rule_block(css, ".tuning-shell") for selector in (".tuning-shell", ".theme-shell", ".doc-edit-shell"):
assert "max-width: 46rem" in tuning, ( block = _rule_block(css, selector)
".tuning-shell must stay hard-coded 46rem (negative pin)" 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: def test_the_dialog_panels_ride_the_token() -> None:
"""After the switch, the form columns are the ONLY rules with a """The two source-page dialogs (phase 69 remove-confirm, phase 89
literal max-width: 46rem: .tuning-shell (phase 27), ignore-editor) cap at the chat-column width THROUGH THE TOKEN —
.doc-edit-shell (phase 59, task 06 — the doc edit screen is a min(var(--chat-column), calc(100vw - 2rem)) — or the viewport,
FORM column, not a reading column, so it must not ride whichever is narrower (the 46rem literals ride no more)."""
--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() css = _css()
assert css.count("max-width: 46rem") == 3, ( for selector in (".remove-confirm-panel", ".ignore-editor-panel"):
"only the form columns (.tuning-shell, .doc-edit-shell, " block = _rule_block(css, selector)
".theme-shell) may keep a literal max-width: 46rem" assert "min(var(--chat-column), calc(100vw - 2rem))" in block, (
) f"{selector} must ride the token (the chat-column width "
assert "max-width: 46rem" in _rule_block(css, ".tuning-shell") "or the viewport)"
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 comment contract ----------
"""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 def test_comments_carry_the_72rem_contract() -> None:
and the media block.""" """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() css = _css()
# The stale "≤46rem" contract claims are gone from the file.
assert "≤46rem" not in css, ( assert "≤46rem" not in css, (
"the stale '≤46rem' contract wording must be updated" "the stale '≤46rem' contract wording must be updated"
) )
# Provenance at the two authoritative spots (token + wide block). assert "46rem base" not in css, "no stale '46rem base' claim may remain"
token_idx = css.index("--chat-column: 46rem") assert "92rem at >=1500px" not in css, (
wide_idx = css.index("@media (min-width: 1500px)") "no stale wide-override claim may remain"
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] assert "1500px" not in css, (
# The chat-shell comment names base + override. "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 {")] 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"
)