Artifact 87354d9e75f50f1d3e6e175e47fff020401472dc8343d2107116918c7d1615dc:
- File
tools/timeline-verify.js
— part of check-in
[54f1834d26]
at
2026-08-15 18:04:23
on branch trunk
— timeline: branch lines one color fork-to-merge, uniform stroke
Look-ahead branch map colors merge rails by the commit they descend to; join curves keep their branch color to the fork; off-page parents render neutral, never a hashed hue for the empty name; drop vector-effect (zoom-variant thickness) for plain 2.5px strokes. Lint now checks color continuity at every cell boundary and stroke uniformity; unit tests pin the dev-line coloring end to end. (user: dmestas, size: 6116) [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 AND color continuity across rows and date // dividers, layer tiling, node/title/time alignment, uniform stroke // thickness, 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 Map(); 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.set(Math.round((sr.left + +ln.getAttribute("x1") * sx) * 2) / 2, ln.getAttribute("stroke")); } 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.set(Math.round((sr.left + px * sx) * 2) / 2, p.getAttribute("stroke")); } } } 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, color] of below) { const match = [...above.keys()].find(a => Math.abs(a - x) <= TOL); if (match === undefined) { out.violations.push(`row ${i}->${i + 1}: rail leaving at x=${x} has no continuation (next has ${[...above.keys()].join(",") || "none"})`); } else if (above.get(match) !== color) { out.violations.push(`row ${i}->${i + 1}: rail at x=${x} changes color mid-line ${color} -> ${above.get(match)}`); } } } } // 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}`); } } // 3b: uniform stroke thickness — every line's effective horizontal // width (strokeWidth x scaleX) must match. const widths = new Set(); for (const {td} of cells) { for (const svg of td.querySelectorAll("svg")) { const sr = svg.getBoundingClientRect(); if (sr.height < 0.5) continue; const sx = sr.width / svg.viewBox.baseVal.width; for (const ln of svg.querySelectorAll("line, path")) widths.add(Math.round(parseFloat(ln.getAttribute("stroke-width")) * sx * 4) / 4); } } if (widths.size > 1) out.violations.push(`non-uniform stroke widths: ${[...widths].join(", ")}`); // 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)}); })()