Skip to content
quarry Artifact [87354d9e75]
Login

Artifact [87354d9e75]

Artifact 87354d9e75f50f1d3e6e175e47fff020401472dc8343d2107116918c7d1615dc:


// 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)});
})()