Compound Labs
Get the newsletter
THE STANDUPAnthropic fixes Claude 5.1 error spikesERROR SPIKES, FIX DEPLOYEDCOMPOUND MASTODONSkillWorks records inactive files beside their roster status22 INACTIVE FILES STILL LOADCTXWINDOWContext Window tracks AI lab changes and filters their evidenceFILTERS KEEP EVIDENCE TOGETHERAGENTWIREmicrosoft/agent-governance-toolkit - AI Agent Governance ToolkitPOLICY ENFORCEMENT FOR AI AGENTSCOMPOUND FACEBOOKCompound Portfolio restores its sweep after a missing module stops desk dataSWEEP RESTORED, DESK DATA WRITTENBLOCKDEXBlockDex shows no destination for captured rows with rejected slash names31 ROWS, NO DESTINATIONCONTEXT WINDOWOpenAI adds 1024p image pricing: $0.50 and $0.251024P IMAGE, $0.50 AND $0.25COMPOUND BLUESKYCompound Labs renamed job labels so job-run finds breaker stateBREAKER STATE, NOW FINDABLECTXWINDOWContext Window tracks document changes but failed mobileMOBILE WORKFLOW, ONE COLUMNAGENTWIREMineDojo Voyager: Open-Ended Embodied AgentOPEN-ENDED EMBODIED AGENTFOUNDER BLUESKYdeploy.sh now fails false Vercel production landingsFALSE LANDINGS NOW FAILCOMPOUNDlast known fixes redirect tracking after migrationSTUDIO SOURCES NOW SAY COMPOUNDCONTEXT WINDOWOpenAI adds sora-2-pro pricing720P VIDEO, $0.15, $0.30FOUNDER LINKEDINdeploy.sh now stops failed promotes from marking production landedFAILED PROMOTES NO LONGER LANDUSINGITUPUsing It Up records a receipt fixing a wobbling wooden tableONE RECEIPT, TABLE STAYED LEVELCOMPOUNDpnpm v12.4.2POSIX BIN SHIMS REPLACEDFOUNDER PEERLISTCompound Labs' deploy runner rejects failed promotesFAILED PROMOTES NO LONGER LANDTRUSTDESKTrustDesk fixes host migration redirects while keeping /api/ outOLD HOST, 308; API STAYS LIVE
Independent product R&D labFounded and run by Isaiah Kim, @kyisaiah47Newest commit Sep 16, 2026, AgentwireNewest writing Sep 16, 2026Site changelog Sep 16, 2026
Aug 16, 20264 min read

Why my annotated screenshots started at flag #2: numbering markers on a page you do not control

The deliverable is a PDF, and the part of it people actually look at is a screenshot of their own homepage with the failing elements outlined and numbered. Everything else in the document is a table.

A headless reviewer reads the finished PDF, including looking at those screenshots, before the send. On 2026-07-20 it rejected three drafts in a row. Two of its complaints were about the same code: the visible numbering started at #2, and some flags were outside the frame.

What the annotation pass is working with

The crawl stores axe-core violations per page, keeping up to three sample nodes per rule, each with the CSS selector axe recorded. The evidence pass reopens the worst pages at 1440x1500, resolves those selectors, outlines every match (red for critical, amber for serious), scrolls to wherever the failures cluster, and takes one viewport shot. Not fullPage: a tall municipal page rendered into a PDF page is unreadably small.

CivicBinder accessibility audit report with metrics (78 issues, 5 defects) and HTML code examples, Tool shows both quantified findings and actual HTML snippets from the analyzed website side-by-side.
CivicBinder accessibility audit report with metrics (78 issues, 5 defects) and HTML code examples, Tool shows both quantified findings and actual HTML snippets from the analyzed website side-by-side.

Marking during the walk is what broke it

The original code did the outline, the badge, and the number in one page.evaluate over every failing element in the document, then scrolled and shot. Two things fall out of that ordering.

The counter is document-wide. Element 1 is in the masthead, the densest cluster is 900px down, so the first badge a reader sees is #4 and the sequence has holes in it.

And the badges were position: absolute at scrollY + r.top, a document coordinate. For a fixed or sticky element that number describes where the element happened to be sitting at scroll 0, and the element does not stay there. The sticky header travels with the viewport, the badge does not, and municipal sites ship sticky headers and floating chat bubbles as a matter of course.

The caption number was wrong separately. It printed the count of every outlined element in the document, while shown came from the cluster search: how many recorded Y positions fell inside a 1500px window. That is not a count of things a reader can see. Zero-width elements, horizontally off-screen elements and anything inside a collapsed accordion all counted.

Two passes, with the scroll between them

Pass 1 now only outlines and tags, returning sorted Y positions for the cluster search. After the scroll settles, pass 2 does the numbering:

const shown = await page.evaluate(() => {
  let n = 0;
  document.querySelectorAll('[data-cb-mark]').forEach((el) => {
    const r = el.getBoundingClientRect();
    if (r.bottom < 24 || r.top > innerHeight - 24 || r.width === 0) return; // off-screen
    n++;
    const badge = document.createElement('div');
    badge.textContent = n;
    badge.style.cssText = `position:fixed; z-index:2147483647;` +
      `left:${Math.max(4, r.left - 12)}px; top:${Math.max(4, r.top - 12)}px; ...`;
    document.body.appendChild(badge);
  });
  return n;
});
if (!shown) { console.error(`skip (no failing element visible after scroll): ${p.url}`); continue; }

position: fixed is correct here precisely because nothing scrolls again before the shutter. The clamps stop a badge on a flush-left element from landing at x = -12.

before (rejected 2026-07-20)after
badge coordinatesabsolute, scrollX/scrollY + rectfixed, viewport rect, clamped to 4px
numberingduring the document-wide walkafter the final scroll, over in-frame elements only
first visible badgewhatever survived the cropalways 1
fixed/sticky elementsbadge pinned to a coordinate the element leavesplaced after the last scroll, so it cannot diverge
caption countevery outlined element on the pagethe number of badges actually drawn
nothing in frameshot taken anywaypage skipped with continue

Three numbers, none of them interchangeable

One page in a run has 42 failing nodes. The outline set is smaller, because the scan keeps three sample nodes per rule. Eleven badges land in frame. The caption prints the first and the last of those, and the manifest now derives both marked and shown from the same return value, so they cannot drift apart again.

If zero badges land, the page is dropped from the binder rather than shipped with an empty red rectangle. The reviewer would have failed it anyway: its fourth check is that every screenshot shows numbered markers and that the caption agrees with them. Any error inside the reviewer itself returns pass: false, so a broken gate blocks the send instead of waving it through.

How we built this, and what the finished binder looks like: CivicBinder.

---

One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did, read off the repository and the live site, not written from memory. Join the list.

All writing