All work AI SYSTEMS / DEVELOPER TOOLS prototype

12  /  AI SYSTEMS / DEVELOPER TOOLS

DesignLens

Generating a site with AI is trivial; writing the prompt is not, so my scanner reads a real site and writes it.

30 May – 2 June 2026TypeScriptPlaywrightOpenAI APIpnpm workspacesNext.js 15Tailwind CSS v4HonoVitest

The challenge

AI build tools made generating a website trivial. Writing the prompt became the hard part, and a vague prompt returns a generic site. My first runs proved the point: the scanner missed most of a homepage's sections and produced a brief thin enough that any builder would have to guess. What I was after was a brief carrying enough of the real site — its business, its exact tokens, every section of every page — that guessing was unnecessary.

What I built

I built a pnpm monorepo of eleven TypeScript workspaces around a five-stage pipeline: capture, extract, vision, memory, prompt. I keep extraction deterministic rather than vision-guessed — computed styles are clustered perceptually with CIEDE2000 ΔE, roles come from declared CSS variables first, and the stack is fingerprinted from class-name signatures across Tailwind, Bootstrap, MUI, Webflow and Elementor. I use the model only where judgement is needed: one cheap read per section and one business brief. Everything downstream assembles deterministically into master.md, a section-by-section prompt per page, tokens exported as tailwind.config.js, tokens.css and tokens.json, and screenshots — full-page and above-the-fold at three breakpoints, per-section at desktop — all zipped. Two surfaces run it: a local web app that takes a URL and shows six animated stage illustrations while it works, and a Next.js dashboard.

How it works

UNTRUSTED REFUSAL JUDGEMENT A stranger's URLbound for a headless browser The SSRF guardresolves DNS, returns the IP Deterministic extractstyles clustered by CIEDE2000 Harvested copy, fencedinert data, not instructions The model's two jobsa read per section, one brief Grounding guardrailevery hex and px must exist The bundle assemblesmaster.md, prompts, tokens Never deployed it ran locally on 1 June
The mechanism, drawn from the build.

What it looks like

DesignLens — screenshot
Running locallyAI SYSTEMS / DEVELOPER TOOLS

The detail

01

The SSRF guard

My scanner renders attacker-supplied URLs in a headless browser, which turns a weak guard into an open proxy onto your own network. My check resolves DNS, rejects private and reserved IPv4 and IPv6 ranges, non-http schemes and any .internal, .local or localhost host, then hands back the exact resolved IP the caller must connect to — closing the rebinding window between check and fetch. I re-validate every redirect hop and cap them at five; 67 tests cover it.

02

Fenced as data

The per-section read embeds headings, paragraphs and calls to action harvested from an arbitrary third-party page straight into the model's user message, so a hostile page could plant instructions in its own copy. I wrap all harvested content in an explicit delimited fence that declares it inert and names the system message as the only authority. I give the business-brief digest the same treatment.

03

Grounding guardrail

Every hex and px value in the generated markdown has to exist in the extracted design system, or the brief is quietly misdescribing the brand. The flat prompt path fails on a violation; deep scan has nobody in the loop, so it neutralises the offending literal, keeps the deliverable buildable, and reports the count. I exempt the execution values the elevation block emits itself through a narrow allowlist — hairline borders, focus rings, glass-card radii, mesh-gradient blur — so craft detail survives while an invented off-scale 13px still gets caught.

04

Scheduling, not shortcuts

The brief for a large speed-up came with a condition attached: not if it costs quality. So I changed no prompt and no model. I moved every section across every page into one bounded pool of twelve in flight instead of roughly 128 sequential awaits, the business brief now runs alongside section analysis because it only needs harvested copy, and I harvest per-section content inside the existing render rather than paying a second navigation per page.

05

A dead button

My local web app's client script lives inside a backtick template literal, where \d and \/ were silently eaten as escape sequences. The served JavaScript carried a malformed regex and threw at parse time, so the whole script failed to load, the Scan button was never bound, and clicking it did nothing — while every API-level test I had stayed green. I fixed it by doubling the escapes and adding a guard: at server boot the inline script is extracted and compiled with new Function, so a broken UI fails loudly at startup instead of shipping quietly.

The outcome

I ran it end to end locally on 1 June and it left a real 30 MB bundle on disk: master.md, eight per-page section-by-section prompts, tokens in three export formats, and the screenshots. 660 tests across 48 files pass, the SSRF suite 67 of 67, and my extraction golden set — 18 hand-authored fixture harvests rather than live captures — clears all three gates at 97.6% palette, 100% typography and 100% stack against thresholds of 95, 95 and 90. I never deployed it; the Vercel, Fly and Railway files are stubs marked TODO(Wave 5). I stopped work on 2 June with the generated output still not good enough by my own judgement and the remediation pass half-done — an uncommitted change is still sitting in deepscan/assemble.ts.