Artifact 862be524a7cb7b6bb567c64d8859e82cf3db0a586708da605e9d50223bcaa582:
- File
tools/timeline-verify.js
— part of check-in
[994815316c]
at
2026-08-15 17:45:11
on branch trunk
— timeline: seamless graph tiling, date-row rail carry, mobile pass
Tail layer at fixed px width (was stretching to 100% — x misalign); head at top:0 with cy=24; date rows render active rails so pills sit beside an unbroken graph; 5px card gaps; mobile keeps vertical card padding so alignment holds. tools/timeline-verify.js is the browser geometric lint: 0 violations on all three demo repos at 1280 and 375. (user: dmestas, size: 5133) [annotate] [blame] [check-ins using]
// Timeline graph geometric lint — run against a live quarry with: // agent-browser open http://HOST/r/REPO/timeline // agent-browser eval -b "$(base64 < tools/timeline-verify.js)" // Asserts rail continuity across rows and date dividers, layer // tiling, node/title/time alignment, and no page overflow — at any // viewport. Expect violationCount 0. // Timeline graph geometric lint. Walks every graph cell (rows + date // rows) top-to-bottom and verifies: // 1. vertical tiling: each cell's layers cover it fully (head 0..36, // tail 36..bottom) and consecutive cells touch with no gap // 2. x continuity: the set of rail x positions leaving a cell's bottom // edge equals the set entering the next cell's top edge // 3. node alignment: each node circle's center aligns with its card's // title first-line center (±3px) and its time text center (±5px) // 4. no page-level horizontal overflow (() => { const TOL = 1.5, out = {violations: [], cells: 0, nodes: 0}; const rows = [...document.querySelectorAll("table.tl tr")]; const cells = rows.map(tr => ({tr, td: tr.querySelector("td.tl-graph")})).filter(c => c.td); // Collect, per cell: rect, and rail x sets at its top and bottom edges. const edgeXs = (td, edge) => { const xs = new Set(); for (const svg of td.querySelectorAll("svg")) { const sr = svg.getBoundingClientRect(); if (sr.height < 0.5) continue; const vb = svg.viewBox.baseVal, sx = sr.width / vb.width, sy = sr.height / vb.height; const atEdge = edge === "top" ? sr.top : sr.bottom; const cellR = td.getBoundingClientRect(); const cellEdge = edge === "top" ? cellR.top : cellR.bottom; if (Math.abs(atEdge - cellEdge) > TOL) continue; // this layer doesn't reach the edge for (const ln of svg.querySelectorAll("line")) { const y1 = +ln.getAttribute("y1"), y2 = +ln.getAttribute("y2"); const lo = Math.min(y1, y2), hi = Math.max(y1, y2); const touches = edge === "top" ? lo <= 0.01 : hi >= vb.height - 0.01; if (touches) xs.add(Math.round((sr.left + +ln.getAttribute("x1") * sx) * 2) / 2); } for (const p of svg.querySelectorAll("path")) { const d = p.getAttribute("d"); // M x0 y0 Q qx qy x1 y1 const m = d.match(/M ([\d.]+) ([\d.]+) Q [\d.]+ [\d.]+ ([\d.]+) ([\d.]+)/); if (!m) continue; const pts = [[+m[1], +m[2]], [+m[3], +m[4]]]; for (const [px, py] of pts) { const touches = edge === "top" ? py <= 0.01 : py >= vb.height - 0.01; if (touches) xs.add(Math.round((sr.left + px * sx) * 2) / 2); } } } return xs; }; // 1+2: tiling and x continuity between consecutive graph cells. for (let i = 0; i < cells.length; i++) { out.cells++; const {td, tr} = cells[i]; const r = td.getBoundingClientRect(); // layers cover the cell vertically let covered = 0; for (const svg of td.querySelectorAll("svg")) covered += svg.getBoundingClientRect().height; if (td.querySelector("svg") && covered < r.height - TOL * 2) out.violations.push(`row ${i}: layers cover ${covered.toFixed(1)}px of ${r.height.toFixed(1)}px cell`); if (i + 1 < cells.length) { const next = cells[i + 1].td.getBoundingClientRect(); if (next.top - r.bottom > TOL) out.violations.push(`row ${i}->${i + 1}: ${(next.top - r.bottom).toFixed(1)}px vertical gap between cells`); const below = edgeXs(td, "bottom"), above = edgeXs(cells[i + 1].td, "top"); for (const x of below) { if (![...above].some(a => Math.abs(a - x) <= TOL)) out.violations.push(`row ${i}->${i + 1}: rail leaving at x=${x} has no continuation (next has ${[...above].join(",") || "none"})`); } } } // 3: node alignment with title first line and time. for (const {tr, td} of cells) { const c = td.querySelector("circle"); if (!c) continue; out.nodes++; const cr = c.getBoundingClientRect(), cyy = cr.top + cr.height / 2; const title = tr.querySelector(".tl-title"); if (title) { const range = document.createRange(); range.selectNodeContents(title); const first = range.getClientRects()[0]; if (first && Math.abs((first.top + first.height / 2) - cyy) > 3) out.violations.push(`node/title misaligned by ${((first.top + first.height / 2) - cyy).toFixed(1)}px at "${title.textContent.slice(0, 30)}"`); } const time = tr.querySelector("td.tl-time a"); if (time) { const trr = time.getBoundingClientRect(); if (Math.abs((trr.top + trr.height / 2) - cyy) > 5) out.violations.push(`node/time misaligned by ${((trr.top + trr.height / 2) - cyy).toFixed(1)}px at ${time.textContent}`); } } // 4: no page horizontal overflow. if (document.documentElement.scrollWidth > window.innerWidth + 1) out.violations.push(`page overflows: scrollWidth ${document.documentElement.scrollWidth} > viewport ${window.innerWidth}`); return JSON.stringify({cells: out.cells, nodes: out.nodes, violationCount: out.violations.length, violations: out.violations.slice(0, 12)}); })()