phase: 139_read_range_tool_lines
Build and Push Containers / build-and-push-app (push) Successful in 2m54s
Build and Push Containers / build-and-push-db (push) Successful in 17s

All verification is complete and green. Phase 139's three tasks (01–03) were already implemented in prior passes; this was the final verification pass, and every gate holds.

## Phase 139 — final verification report

**Verified in working tree (no code changes needed):**
- `readRangeSuffix` single-source-of-truth helper in `app.js` + byte-identical parity copy in `shared.js` (EN-dash ` (lines N–M)`, `(from line N)`, `(first N lines)`, `Number.isInteger(x) && x >= 1` guard mirroring the server's `_whole_line_number`)
- `appendToolLine(wrap, name, argument, offset = null, limit = null)` — `<span class="read-range">` sibling after `<code>`, only when suffix non-empty; `shared.js` `addToolLines` parity
- Live `tool` branch: guarded `ev.offset`/`ev.limit`, additive `toolAcc` records (non-range keeps pre-phase shape), status label carries the suffix (A2); restore path passes stored `t.offset`/`t.limit` through, phase-95 truncation note untouched
- E2E: `SAME_FILE_TRIGGER` mock flow (same file read twice, offsets 10/20 then 40/15) + 3-test Playwright suite

**Test / lint / coverage results:**
- `uv run pytest` (unit + integration): **3283 passed, 9 skipped, 0 failed**
- `uv run pytest --cov=app --cov-report=term-missing`: **TOTAL 99%** (>90% gate)
- `uv run pytest tests/e2e/test_read_range_tool_lines.py -v --no-cov`: **3 passed** in isolation
- `uv run ruff check .`: clean; `uv run pyright`: **0 errors, 0 warnings**

**Completion criteria:**
1. Range read renders `(lines N–M)` live; two same-file reads show DIFFERENT ranges — ✅ (E2E test 1)
2. Plain read byte-identical; phase-87/117 pins + emoji guard pass unedited — ✅ (E2E test 2 + full suite)
3. Suffix survives save → restore on chat page + shared page — ✅ (E2E test 3)
4. Full suite green, coverage >90%, E2E isolated green, ruff + pyright clean — ✅
5. Commit + phase move — left to the harness per executor rules (no `git add`/`commit` run)

**Notable:** the missing pytest summary line on one run was a transient output-buffering artifact, confirmed by re-run; DB was already up and healthy.

**Next pending phase:** `140_pictorial_pdf_pages`
This commit is contained in:
2026-09-30 12:55:23 -04:00
parent 1a657ce4ca
commit 9e7262a5c8
31 changed files with 2197 additions and 25 deletions
+30
View File
@@ -203,6 +203,17 @@ function addToolLines(wrap, tools) {
const code = document.createElement("code");
code.textContent = argument; // the path is data, never markup
line.appendChild(code);
// Phase 139 (task 01): the SAME requested-range suffix as the chat
// page's appendToolLine (the parity contract) — a span SIBLING
// after the <code> child, appended ONLY when the suffix is
// non-empty: a record without the fields renders exactly as today.
const rangeSuffix = readRangeSuffix(t.offset, t.limit);
if (rangeSuffix) {
const range = document.createElement("span");
range.className = "read-range";
range.textContent = rangeSuffix; // the range is data, never markup
line.appendChild(range);
}
} else if (
(t.name === "grep" || t.name === "search_documents") && argument
) {
@@ -236,6 +247,25 @@ function addToolLines(wrap, tools) {
body.insertBefore(disclosure, body.querySelector(".bubble"));
}
/* Phase 139 (task 01): the shared page's parity copy of app.js's
* readRangeSuffix — the requested line range on the Reading line. The
* saved record now carries the chat page's offset/limit (the toolAcc
* stamp, phase 139); a record saved before the phase (no fields) renders
* exactly as today (no migration). IDENTICAL literals and IDENTICAL
* Number.isInteger guard (mirrors the server's _whole_line_number,
* app/api/chat.py) — the shared page re-renders saved records
* pixel-identically to the chat page's restore path (the phase-50
* contract). The range shown is the model's REQUESTED extent (the
* ASSUMPTION A1 pin in app.js's readRangeSuffix). */
function readRangeSuffix(offset, limit) {
const offOk = Number.isInteger(offset) && offset >= 1;
const limOk = Number.isInteger(limit) && limit >= 1;
if (offOk && limOk) return ` (lines ${offset}–${offset + limit - 1})`;
if (offOk) return ` (from line ${offset})`;
if (limOk) return ` (first ${limit} lines)`;
return "";
}
/* "Maybe try:" chips under a deflected bubble (honesty gate, phase
* 04) — PLAIN SPAN text, not buttons (owner-locked 2026-08-29: a
* guest tapping a chip has nowhere to go — zero interactive