Commit Graph
61 Commits
Author SHA1 Message Date
Samraaj BathandClaude Fable 5 4c1dea77e1 Parallelize the clone queue: in-flight dedup + WORKER_CONCURRENCY
Two changes that multiply effective queue throughput:

- In-flight dedup: POST /v1/clones now attaches identical submits (same
  cacheKey) to the already queued/running job instead of enqueueing a
  duplicate capture. Retry-spam previously multiplied queue load (observed
  4x same-URL submissions in prod); now it's free. Best-effort — a
  same-instant race can still double-submit. Gated on !noCache.

- WORKER_CONCURRENCY (default 1): the worker registers N independent
  pg-boss pollers, running up to N captures concurrently per process with
  no head-of-line blocking. Each slot gets its own verify harness dir
  (harnessDir/slot-i) so concurrent framework builds never collide;
  concurrency 1 keeps the flat harnessDir layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:44:42 -07:00
Samraaj BathandGitHub 99aca7d43d Merge pull request #18 from ion-design/fix-chip-width-lock
Don't lock content-derived button widths; nowrap icon+text chips
2026-07-06 13:39:09 -07:00
Samraaj BathandClaude Opus 4.8 27cccd8964 Don't lock content-derived button widths; nowrap icon+text chips
Two general fixes for single-line chips/pills whose labels wrapped onto
a second line in generated output:

1. fixedWidthButtonLike treated any constant-width button as authored
   fixed-width, but a content-sized chip whose label never changes paints
   the same width at every viewport. When the sizing probe proved
   width:auto reproduces the box at its own max-content at every painted
   sample (with genuinely wrappable text inside), the width is
   content-derived - don't lock it. Baking that px invited the emitters'
   quantization (snapLen 0.1px rounding, snapBase spacing-scale snap) to
   land fractionally below the intrinsic single-line width, wrapping the
   last word. Authored intent (sourceFixedWidth/sourceFixedSize) still
   locks.

2. nowrapWrapVulnerable bailed on any element child, so an icon svg
   beside the label disqualified the classic icon+text chip shape.
   Replaced children (svg/img/...) can't wrap and their constant width is
   already inside the probe's wMin/wMax, so they're now permitted; any
   non-replaced element child still bails.

525 tests (5 new), typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:36:09 -07:00
Samraaj BathandGitHub 03226f8a53 Merge pull request #17 from ion-design/fork-salvage
Salvage from #15: capture hardening, service events/preview, pattern catalog
2026-07-06 12:34:35 -07:00
Samraaj BathandClaude Fable 5 b66a993b35 docs(SERVICE): correct event-stream contract and document preview.html
Audited docs against the fork-salvage API surface (PR #17). SERVICE.md fixes:

- Event stream: events are the compiler's granular {t, ...} log stream plus
  service phases, not a fixed capture/generate/verify enum. Corrected the
  "{seq, t, ...payload}" claim — returned bodies carry `t` but NOT `seq`.
  Documented that `after` is a seq cursor (DB filters seq > N) and that the
  in-memory backend ignores `after` and returns the full list each poll.
- preview.html: new Preview section — the flat, self-contained generate-time
  artifact (file-map path `preview.html`, manifest `preview_html`, always
  present), fetched via /v1/clones/:id/files/preview.html. Documented its
  arrival on the `generated` event, relative `public/assets/...` resolution
  through the files route, the authenticated-iframe caveat + proxy
  recommendation, and the original→preview.html→deployed staging. Stated
  plainly that it is static/runtime-free: frozen animations, no interactions.
- Added the `preview` option and clarified app-preview (built export) vs the
  flat preview.html.

DEPLOY.md migration list (0000/0001/0002) verified accurate — no change.
README.md / docs/README.md make no stale API claims — no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 10:27:53 -07:00
Samraaj BathandClaude Fable 5 01e59d96bd Emit a flat preview.html at generate time for instant first-page display
A self-contained, runtime-free render of the entry page, built directly
from the IR using the SAME decision code as the app: collectNodeRules/
assembleCss for per-node banded CSS, propsList/resolveTag for tags,
attributes, and asset mapping - so the preview cannot drift from what the
built app resolves to. No build toolchain involved (~2s, pure string
building, byte-stable). Lottie/video render as captured poster stills;
zero script tags; asset paths relative to the app dir so the file serves
from the artifact file map as-is. Multi-page runs emit the entry page
only. The manifest records preview_html so consumers can key on the
generated event, show the preview immediately, and swap to the deployed
app when the build finishes.

Consistency vs the built app's own 1280px renders: 0.8-1.8% pixel diff on
reference runs (5.3% on an animation-heavy page - the frozen-animation
class, as expected).

520 tests pass (5 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:59:57 -07:00
Samraaj BathandClaude Fable 5 c35680eb13 Docs: job event stream, app-preview routes, migration notes
SERVICE.md documents the /events polling endpoint, the event persistence
model, the in-memory backend's enqueue-202 contract, and the app-preview
route; DEPLOY.md notes the job_events migration and that db:migrate is
journal-tracked/idempotent (with the manual-psql alternative).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:16:08 -07:00
Samraaj BathandClaude Fable 5 bbe4a5643b Service: job event stream, async in-memory backend, preview routes
Ported from PR #15 (packages-side surface only; the fork's dev UI page is
intentionally dropped - the API surface is the product):
- job_events table + repository, worker emits stage transitions, additive
  GET /v1/clones/:id/events polling route. The fork's hand-written
  migration was never registered in Drizzle's journal (it silently never
  applied); regenerated properly via drizzle-kit from schema.ts with the
  (job_id, seq) index declared, and verified against a live Postgres
- In-memory backend moves to the documented enqueue-202 + poll contract
  with single-flight BUSY guard; POST honors backend httpStatus
- app-preview static serving routes; the preview BUILD half is deferred
  until the compiler exports buildApp/DEFAULT_HARNESS_DIR - routes 404
  cleanly until then
- preview option threaded through core types; cache key intentionally
  unchanged while the flag is inert

All workspaces typecheck; api 18/18, core 13/13, worker 1/1 pass;
migration verified applying (table + index + FK) on postgres:16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:00:55 -07:00
Samraaj BathandClaude Fable 5 f991f469cf Pattern catalog: pinned library fingerprints as hint-only recipe evidence
Ported from PR #15: a SHA256-pinned catalog of 49 generic library and
platform fingerprints (carousel/consent/chat/commerce vendors) with an
O(1) signature index. Catalog hits strengthen recipe recognition (small
confidence deltas, marquee->logo-cloud, carousel false-grid skip,
lightbox->gallery, commerce prior) and are recorded in the manifest as
pattern evidence with node references - they never override captured
geometry, which remains gated by the existing plan-agreement checks.
The integrity pin is unconditional: a catalog/lock mismatch always
throws (the fork's env-var soft-warning mode is removed).

515 tests pass (10 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:25:58 -07:00
Samraaj BathandClaude Fable 5 7e8863fc7e Capture hardening: deterministic env shim, extended lazy sweep, nav budget with bot-wall fast-fail
Salvaged and re-implemented from PR #15:
- Deterministic environment shim (seeded Math.random, pinned-but-advancing
  Date consistent across constructor/now/getTime/valueOf/toISOString) so
  time/random-dependent pages capture reproducibly; epoch and seed are
  recorded run metadata, performance.now stays real for motion sampling
- Post-snapshot lazy-asset discovery sweep for channels the walker misses:
  noscript contents (DOMParser), inline style url(), data-background(-image),
  img[loading=lazy] currentSrc
- Navigation gets a total 90s budget, an early bot-wall probe that aborts
  with a pollution-style error instead of capturing garbage (WALL_RE now
  shared with the validator gate), and one fresh-context retry scoped to
  the initial nav on retryable failure classes only

505 tests pass (29 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:49:14 -07:00
Samraaj BathandGitHub 3faf1ef043 Merge pull request #16 from ion-design/more-fidelity-upgrades
Fidelity campaign: capture integrity, emission geometry, honest validation
2026-07-04 13:05:53 -07:00
Samraaj BathandClaude Fable 5 c49d5b6ab7 Recover viewport-relative min-heights and capped-container centering at emission
- viewportMinHeightLaw: a box authored min-h-screen / min-h-[calc(100vh -
  Cpx)] had its per-viewport px baked at capture, leaving whitespace (or
  overflow) at window heights between captured viewports; when the source
  class list corroborates a vh token and per-regime offsets agree, emit
  the authored law (100vh / calc(100vh - Cpx)) instead of frozen px
- isCappedCenteredContainer: max-width-capped centered content columns
  emit mx-auto (including as flex/grid items, where auto margins center
  along the main axis) so pages stay centered at non-captured window
  widths instead of pinning left at frozen px
- Circular-slide guard skips fluid full-bleed sections so
  viewport-spanning shrink-0 items keep fluid width

476 tests pass (4 new, with corroboration-guard negatives), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 12:42:11 -07:00
Samraaj BathandClaude Fable 5 22d0bf19e9 Rebase viewport-relative positioning when grafting iframe subtrees
A grafted frame's position:fixed children re-resolved against the MAIN
page viewport and escaped the frame box's overflow clip (fixed ignores
overflow ancestors) - a full-viewport backdrop inside an embedded iframe
could paint across the entire page. Grafted subtrees now demote
fixed->absolute and sticky->relative, and a static graft host is promoted
to relative so demoted absolutes anchor and clip inside the frame box.

472 tests pass (4 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:23:47 -07:00
Samraaj BathandClaude Fable 5 d56fbbe45e Per-viewport DOM divergence: graft non-canonical-only children, drift stand-in policy
The IR's canonical tree came from one viewport, so DOM that exists only
at other widths (mobile-only pagination bullets, per-viewport product
sets) either vanished or left empty display:none shells.

- Unmatched children at non-canonical viewports graft into the IR at
  deterministic anchored positions with per-viewport data scoped to their
  source widths; emission gives them a base hide plus a reveal band
  (hidden max-[...]:block), capped per parent, invisible subtrees skipped
- Whole-set identity drift (a container serving disjoint same-family
  child sets per viewport) is detected by mutual mismatch; canonical
  content stands in at the drift viewport instead of an empty shell, and
  manifest.json records the divergence
- Per-item divergence resolves naturally via grafting: each viewport
  shows exactly its captured content, mutually exclusive by banding

468 tests pass (8 new), typecheck clean, double-build byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 07:26:55 -07:00
Samraaj BathandClaude Fable 5 3f0757dc23 Snap percent widths up for content-fit-sensitive boxes
Round-to-nearest 0.5% could land a nowrap flex row's width fractionally
below its intrinsic content width, wrapping it to two lines. When the
sizing probe shows a box painting at its max-content width with no slack,
the snap now ceils to the next step; all other boxes keep nearest
rounding, and the existing reproduce-verification still bails the plan on
any overshoot.

460 tests pass (2 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 07:15:27 -07:00
Samraaj BathandClaude Fable 5 03743333c1 Font-wait counts only actively-loading faces; excuse aborted fetches whose file exists
- Lazy-loading semantics: an @font-face never matched by rendered text
  stays "unloaded" forever - the readiness poll now waits only on faces
  in "loading" state, ending the instant fetches settle (removes ~3s of
  dead deadline per viewport on affected pages); unreferenced faces log
  per-face informationally instead of warning
- Asset gate counts a failure only for real HTTP >= 400 or a failed
  fetch whose target file is genuinely absent from the export - aborted
  srcset-candidate image fetches with valid files on disk are excused,
  matching the existing streaming-media treatment

458 tests pass (11 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 07:03:21 -07:00
Samraaj BathandClaude Fable 5 fae44aab93 Validation waits for webfonts before measuring; sticky wrappers keep fluid width
- The render walk and screenshots now await document.fonts.ready plus a
  bounded state-based poll until every declared face is terminal
  (loaded/error); pending families surface as a font_wait_warning event
  instead of skewing text metrics with fallback-font boxes
- position:sticky full-width wrappers share the static/relative
  fluid-fill branch (a sticky box fills its containing block like any
  in-flow box) instead of freezing at canonical px; fixed-width sticky
  rails still freeze

447 tests pass (12 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 06:21:07 -07:00
Samraaj BathandClaude Fable 5 5b60ded62d Grid plan equality guard, named-token px validation, breakpoint-ordered source variants, hidden-text diagnostic
- A grid-cols-N recipe plan may only override the authored template when
  computed tracks are near-equal - asymmetric sidebar templates
  (260px 1fr) keep their authored geometry
- Source-intent named tokens (max-w-md etc.) re-emit only when their
  Tailwind-4 value matches the captured computed px; mismatches (older
  token scales) emit the captured value instead
- Per-axis source-variant selection keeps the highest-min-width active
  variant instead of last-in-class-order, matching real Tailwind cascade
  (lg:grid-cols-3 no longer loses to a later md:grid-cols-2 at desktop) -
  latent since the initial commit
- New non-blocking textHiddenInClone metric in the dom gate: counts
  capture-visible text that renders hidden in the clone, so silent
  disappearances surface in reports

435 tests pass (10 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 06:11:14 -07:00
Samraaj BathandClaude Fable 5 f16c3aa4e8 Rotator classification: reject element-bearing containers, non-destructive runtime
A progressively-growing feed panel (rows injected after cap tagging)
passed the rotator leaf guard - which only rejected containers with
CAPPED children - and the rotator runtime's textContent writes then
flattened 51 statically-emitted descendants to one text node. Three
layers: emission drops rotators whose IR target has element children;
the capture leaf guard rejects any element child (capped or not); the
DittoMotion runtime saves/restores cloned child nodes via
replaceChildren so even a misclassified rotator can't destroy structure.

425 tests pass (6 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 05:33:06 -07:00
Samraaj BathandClaude Fable 5 ec4e4397cc Trust geometry-corroborated authored fixed sizes; guard aspect-law width transfer
- Authored fixed h-/w- utilities from the source cascade, when the
  captured geometry corroborates them, suppress fill/flow re-derivation
  and emit as captured computed px per band (root-font-size independent) -
  fixes logo tiles collapsing to h-full and replaced-element sizes
  dropping to intrinsic
- aspectHeightLaw rejects ratios where the CSS aspect-ratio/max-height
  width transfer would clamp width below the captured box, falling back to
  per-band explicit dimensions

419 tests pass (4 new, each verified discriminating), typecheck clean,
double-regen byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 04:55:07 -07:00
Samraaj BathandClaude Fable 5 994d2bc21e Band coverage for never-visible occupying items, page-height trust guard, font materialization integrity
- Items never visible at the canonical viewport (carousel slides off-screen
  at every captured width) now fall through to per-viewport geometry bands
  when they occupy layout, instead of freezing canonical geometry
- The explicit-height probe trust excludes body/html/main and any wrapper
  whose height matches the page scrollHeight - page-height nodes keep
  flowing, and the layout height gate stays meaningful
- @font-face and style-rule url() harvesting resolves against the OWNING
  stylesheet's href (walking @import nesting), not document.baseURI;
  font bytes are magic-byte validated at the storage choke point (HTML
  bodies from catch-all 200 routes are rejected); font-face dedup prefers
  the variant whose source actually resolved, deterministically

415 tests pass (16 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 04:29:50 -07:00
Samraaj BathandClaude Fable 5 1d68f16f96 Emission geometry wave: occupying off-screen items, height/width chain trust, subgrid, track-relative percentages, banded override precedence
- Off-viewport in-flow items (carousel slides beyond the right edge) keep
  their per-viewport geometry deltas - they occupy layout and set track
  cross-sizes even when not visible
- Height chains: single-viewport probe verdicts accepted for nodes that
  only paint at one band; un-probed picture/object-fit:cover children
  filling the parent count as height-deriving; a node whose own probe says
  authored-explicit keeps its height at emission (twin of the flow guard)
- Symmetric authored-width circular guard in the walker (height-only
  before), harvesting shadow-root adopted/style sheets for custom elements
- Auto margins only when width is genuinely content-constrained; gutter-
  inset full-width blocks keep literal margins
- grid-template-rows: subgrid never dropped; variant-only subgrid intent
  re-enters via partial-coverage escape; grid-item percentage widths bail
  to auto (they resolve against the track, not the container)
- Banded raw-prop detection counts non-raw band overrides so a banded
  bg-none reset isn't beaten by an inlined gradient

399 tests pass (13 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 03:46:31 -07:00
Samraaj BathandClaude Fable 5 144e7ab16b Clear the show-poster flag after per-viewport video source reload
v.load() resets the media element's show-poster flag; pause cancels
autoplay and the at-zero seek skip left nothing to clear it, so
screenshots after a source re-selection rendered the poster instead of
the new source's frame 0. Reloaded videos now force an epsilon seek
(awaiting seeked, which also serves as the readiness signal on slow
CDNs), and re-selection emits a video_source_reselected log event.

386 tests pass (4 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 03:13:14 -07:00
Samraaj BathandClaude Fable 5 762855664c Composed-tree shadow DOM capture, sized invisible spacers, track re-anchoring, block fill-width slides, javascript: href sanitization
- Walker serializes the composed (flattened) tree: open shadow roots walk
  as host children with <slot> replaced by its assigned light nodes (or
  fallback), so custom-element content (product cards, web components) is
  captured instead of arriving childless
- In-flow visibility:hidden nodes with a nonzero border box survive the
  prune as sized invisible placeholders - ghost spacers carry load-bearing
  geometry
- When pruning drops leading in-flow children of a horizontally-translated
  track, the baked translateX is re-anchored by their aggregate margin-box
  width (settled carousel tracks return to origin instead of pushing real
  content off-screen); animation-owned transforms unaffected
- Circular-shrink slide guard counts block-level fill children as
  width-deriving regardless of wAuto (block auto-width IS fill), pinning
  library-sized slide widths uniformly
- javascript: hrefs emit as "#" and the link gate normalizes both sides

382 tests pass (18 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 02:41:00 -07:00
Samraaj BathandClaude Fable 5 6e4921884c Hover capture via forced pseudo-state, text-wrap support, per-viewport video source re-selection, svg paint recovery, semantic tokens & section naming
Capture:
- Hover/focus states are driven with CDP CSS.forcePseudoState instead of
  moving the real cursor - transparent full-viewport overlay layers were
  swallowing every pointer hover, silently capturing zero hover states on
  hover-rich sites
- text-wrap (balance/pretty) captured and emitted (Tailwind text-balance/
  text-pretty, arbitrary fallback)
- Videos re-run <source> selection per viewport before frame-0
  normalization: resize-without-reload kept aspect-gated variants stuck on
  the load-time choice, poisoning mobile ground-truth screenshots
- svg roots capture their computed paint; fill="none" with a computed
  paint (the fill-current pattern) recovers the real color instead of
  rendering blank

Tokens & naming:
- Full CSS-Color-4 parsing (oklab/oklch/lab/lch/hsl) so modern colors
  cluster and earn semantic roles; visually-equal literals share one token;
  decoration/gradient/shadow colors consult the palette before minting
  opaque tokens (ridge: 45 opaque tokens -> 15, anthropic: 12 -> 7)
- Expanded role vocabulary (background/foreground/primary/accent/border/
  surface/muted) with deterministic tiebreaks and a chroma guard
- Section names mine CMS section ids and js-* hooks with hashy-suffix
  stripping (split_callout_JtTWTt -> split-callout-section)

364 tests pass (42 new), typecheck clean; determinism verified by
double-regen byte-comparison on two reference runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 02:22:15 -07:00
Samraaj BathandClaude Fable 5 db0054e43b Emit html background only when the source html paints one
Painting the body's color onto a transparent source html flips CSS 2.1
background propagation: body's background stops propagating to the canvas
and paints above negative-z descendants, burying full-bleed z<0 hero
backdrops under an opaque page background. Emit the html rule only when
the source html actually painted one; fall back to #ffffff only when both
html and body are transparent. Applies to the Next globals, Tailwind, and
multi-route Vite paths via shared helpers.

6 new tests; suite at 322 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 01:44:12 -07:00
Samraaj BathandClaude Fable 5 0dd71a68d9 Popup dismissal hardening: shadow roots, overlay units, scroll-lock recovery
- Overlay detection resolves effective z-index through stacking ancestors,
  treats a zero-size max-z wrapper plus its full-viewport descendant as one
  removable unit, pierces open shadow roots, and relaxes the z floor when
  the page is scroll-locked; a lock persisting after dismissal reports
  blocking even with no detected overlay
- clickDismiss runs in every frame (close buttons live in cross-origin
  popup iframes), matches decline/no-thanks/aria-label close affordances,
  and re-runs before every snapshot AND screenshot to catch late-mounting
  dialogs
- rootUnclamp triggers from IR content extent (a scroll-lock collapses
  captured scrollHeight) and strips vendor-injected body position/overflow
  locks when firing
- Pollution gate fails on the scroll-lock contradiction: scrollHeight
  pinned to one viewport while IR content spans multiple
- Frame graft skips popup-creative vendor hosts; inline embedded signup
  forms remain graftable (regression-tested)

322 tests pass (24 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 01:44:12 -07:00
Samraaj BathandClaude Fable 5 23505ee229 Validation server: HTTP Range support, bounded settle, crash-proof reporting
- serveStatic answers single-range requests with proper 206/Content-Range
  (416 for unsatisfiable), so Chromium streams large media in bounded
  chunks instead of holding one throttled progressive request open past
  the navigation budget
- goto policy: waitUntil "load" + up to 10s of sustained-quiet polling
  replaces the hard networkidle timeout - long autoplaying media can no
  longer make validation unreachable (applied to renderApp, probe widths,
  and the interaction gate)
- validateRun catches render crashes and records them as runtime errors,
  so a failed render writes a report with a failed gate 0 instead of
  crashing reportless

292 tests pass (14 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 01:26:22 -07:00
Samraaj BathandClaude Fable 5 b445b62a69 Fix flex-basis 0% collapse, fluid max-width centering, letter-spacing snap, video frame normalization
- Keep basis/h/min-h [0%] literal (0% content-sizes on an indefinite axis;
  0px is definite zero) and fold grow + zero-basis into flex-1 - a
  flex:1 1 0% child no longer collapses to height 0 in auto-height columns
- Width centering accepts per-viewport caps (max-width: min(Npx, fluid))
  with symmetric gaps -> mx-auto + banded max-w instead of literal banded
  margins that drift off-centre between captured widths; non-painted
  viewports no longer poison the inference
- letter-spacing keeps sub-0.1px precision (no zero snap); style gate
  normalizes Chromium's letter-spacing "normal" <-> 0px serialization
- Videos pause + seek to frame 0 before every source AND clone screenshot,
  removing playback-time nondeterminism from perceptual evidence

278 tests pass (19 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:59:48 -07:00
Samraaj BathandClaude Fable 5 15d1e7a9e3 Canvas raster fallback, CDP rest-state screenshots, lottie overlay containment
- Visible canvases (>=48px) are captured as PNG stills at capture time
  (toDataURL, element-screenshot fallback for tainted/webgl) and emitted as
  cid-carrying <img> elements filling the captured box, so canvas-driven
  bands no longer render blank
- Source AND clone full-page screenshots now capture via CDP
  Page.captureScreenshot with captureBeyondViewport: no scroll-stitching,
  no scroll events, so scroll-linked animations can't smear mid-scrub
  states into perceptual evidence (pixel-identical to the old path on
  static pages; Playwright fallback retained)
- DittoLottie's runtime overlay stays position:absolute/inset:0 for its
  whole life and the host carries data-ditto-lottie with a scoped
  svg/canvas fit rule, so the runtime animation always fills the pinned
  per-viewport box instead of escaping to aspect height

259 tests pass (12 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:36:08 -07:00
Samraaj BathandClaude Fable 5 e33354cfb5 Neutralize animation-owned transforms across viewports; probe-veto flex auto-width conversion
- Nodes carrying an infinite CSS animation at ANY viewport get their frozen
  mid-animation transform zeroed at EVERY viewport (the @keyframes owns the
  property at runtime); animOwnedProps is now viewport-aware so breakpoint-
  gated marquees can't bake a mid-scroll translateX into other bands
- contentSizedFlexRow/contentSizedFlexItemAuto honor the sizing probe: a
  flex item whose captured width the probe proved load-bearing
  (wAuto=false) is never converted to width:auto, preserving narrow
  centered text columns

247 tests pass (7 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:18:56 -07:00
Samraaj BathandClaude Fable 5 d5a713d31f Honor sizing-probe verdicts in height emission; pin lottie mount heights
- heightFlows() bails when the capture probe proved a height authored-
  explicit (hAuto=false, hFill=false), so circular parent/child flow
  reasoning can no longer drop 100vh heroes to 0px; the probe verdict now
  vetoes the drop through both OR'd paths at the call site
- Space-distributing flex columns (space-between/around/evenly, center,
  flex-end) disqualify the content-driven read - the box height is load-
  bearing for the distribution
- Lottie mounts pin their captured per-viewport height like other replaced
  content, so aspect-fit runtime svgs can't inflate past the captured box
- DittoLottie forwards the placeholder's data-cid onto the runtime svg/
  canvas so the media stays addressable after the swap

240 tests pass (9 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:43:36 -07:00
Samraaj BathandClaude Fable 5 6a930d57b1 Section decomposition: recursive descent with semantic band detection
detectSections gave up on real pages: <body> swallowed everything, a single
oversized-wrapper expansion pass required >=3 candidates, and a 62px navbar
failed the 64px height bar - so nearly every clone shipped as one section
and one monolithic component.

Rewritten around a shared recursive core (detectSectionNodes): containers
covering >50% of the page are wrappers to descend into; a split is accepted
only when >=2 stacked bands tile >=80% of the container (side-by-side
columns and overlays never split; truly single-band pages legally stay 1).
Semantic tags get a 24px bar with nav-evidence detection for thin bars.
The validator's section list and the emitted section components now consume
the same root set, so gate-5 bands and sections/*.tsx files always agree.
Naming is evidence-based and deterministic: navbar/header/hero (only when
actually near the top), recipe names (logo-cloud, product-grid...), heading
slugs, contact/media, positional fallback.

Frozen-capture probes: 1 -> 6..17 sections across the reference runs, with
the old whole-page "hero-section" monoliths splitting into honest bands.

231 tests pass (13 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:31:07 -07:00
Samraaj BathandClaude Fable 5 d3ec154bae Capture & emission fidelity wave: dotLottie support, scroll-state isolation, authored-geometry trust, honest quality scoring
Capture:
- Preserve real asset extensions (.lottie no longer truncated); extract the
  default animation JSON from dotLottie ZIP containers at materialization
  (minimal built-in ZIP reader, no new deps)
- Reset scroll + settle before every viewport snapshot so scroll-linked
  styles can't bake into captured computed values
- Marquee detection now requires content overflow (>1.35x), sustained
  velocity across separated windows, scroll independence (jiggle test), and
  genuine content repetition — kills scroll-settle-lerp false positives
- Sizing probe trusts authored explicit heights (px/vh/calc) over circular
  parent/child reflow verdicts, fixing dropped 100vh heroes

Normalize/generate:
- Canonicalize identity transforms to "none" per viewport; emit explicit
  transform resets across bands when any band has a real transform
- DittoLottie keeps the captured placeholder until DOMLoaded; failed loads
  no longer blank the container
- mx-auto only for true auto-margin centering; literal per-band margins are
  preserved (fixes full-bleed regressions)
- Spacing-scale snap tightened to 0.25px (3.5px stays p-[3.5px]); emit
  whitespace-nowrap for wrap-vulnerable single-line text leaves
- Grid recipes no longer override captured track counts or column spans;
  responsive re-flow plans apply only when geometry agrees

Quality scoring:
- Rewritten 6-dimension rubric (payload, decomposition, duplication,
  semantics, hygiene, runtime) with catastrophe caps and per-dimension
  reporting; thresholds are documented calibration guides

218 tests pass (74 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:09:16 -07:00
Samraaj BathandClaude Fable 5 77be868ee3 Generated-code hygiene: listener cleanup, shared cn(), whitespace collapse, probe stripping, sub-pixel snapping, clone-origin metadata, dependency pinning
- DittoWire/Accordion/DropdownMenu runtime templates: AbortController-based
  listener cleanup, idempotent effects, orphan panel removal on unmount
- Emit a single src/lib/utils.ts cn() instead of a copy per component file
- Collapse captured whitespace runs under white-space:normal (pre* preserved)
- Tag source-injected font-metric probe nodes at capture; drop them from IR
- Snap sub-pixel arbitrary lengths (integer within 0.1px, else 1 decimal)
- Emit SITE_ORIGIN (src/lib/site.ts, env-overridable): sitemap/robots/
  canonical/og:url/JSON-LD resolve against the clone's own origin instead of
  the source domain
- Pin lottie-web in generated package.json whenever DittoLottie is emitted

144 tests pass (21 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:51:50 -07:00
Samraaj BathandGitHub dfcd4a6563 Merge pull request #14 from ion-design/clone-fidelity-fixes
Clone-fidelity fixes from the new audit reviews
2026-07-03 21:54:48 -07:00
Samraaj BathandClaude Fable 5 afab12ff14 Fix mobile chip-strip overlap, reveal-replay stagger, scroll-linked text-fill
Three clone-fidelity fixes from manual review of the acceptance screenshots.

Fix 1 — mobile nav chip-strip overlap (generate/css.ts). A horizontally
scrollable flex strip (overflow-x:auto flex <ul>) of nowrap chips collapsed
because the base viewport reported min-width:0px on the chips (a mobile-only
strip that is 0-wide at desktop), so the generator emitted `min-w-0`, letting
the chips shrink below their content instead of overflowing — the nowrap text
then collided. Suppress `min-w-0` for a nowrap flex item whose flex parent
scrolls horizontally; scoped away from legitimate min-w-0 truncation (whose
parent does not scroll-x). Verified on a fresh ooni.com clone: 0 chip overlaps
at 375, scrollWidth 776 > clientWidth 375.

Fix 2 — reveal-replay stagger leaves tiles unpainted (generate/motion.ts).
DittoMotion re-hid each revealed element and replayed its entrance on
scroll-into-view preserving the captured delay/duration, so a fast scroll /
full-page screenshot caught most grid tiles mid-entrance. Cap the replayed
delay (<=300ms) and duration (<=600ms), settle each tile per-element a bounded
time after it enters view, and make the global failsafe settle (not re-animate)
so nothing stays hidden. Validator settle path (__dittoMotionStop) unchanged.
Verified: cropin cotton renders 12/12 crop tiles in a normal full-page pass;
motion gate reveals 24/24.

Fix 3 — scroll-linked text-fill frozen at end state (capture/stabilize.ts,
capture/capture.ts, generate/css.ts). A view-timeline text-fill
(animation-duration resolves to `auto`) was baked filled: the clone has no
scroll timeline, so emitting the animation made it jump to its end keyframe via
fill-mode:both. Capture-side, cancel scroll/view-timeline animations before each
snapshot so the DOM records the at-rest state; generation-side, suppress the
animation-* props when animation-duration is `auto` (we render the at-rest
state, never replay a scroll-linked animation). Teach the motion gate to exclude
these from the static-CSS expectation so the gate stays honest. Verified on a
fresh ooni clone: the "world's no.1 pizza ovens" em is white/gray (rgb 240,240,
240), not filled yellow, at rest; motion gate still passes.

Tests: +7 fixtures (css scroll-strip + scroll-timeline, reveal-replay caps,
motion-gate scroll-timeline exclusion); 123/123 compiler tests green, full test
suite green, typecheck clean. No gate regression on the fresh ooni run vs the
20260704-024247 baseline (all gate pass/fail unchanged; perceptual within
0.0004; motion restored to pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 21:46:28 -07:00
Samraaj BathandClaude Fable 5 f3d433c024 Teach motion gate about reveal-replay; auto-provision missing harness deps
- motionGate: a captured CSS entrance animation the clone replays through
  a DittoMotion reveal (re-hide on mount, restart @keyframes on scroll)
  is graded by the scroll-reveal check, not the static-CSS expectation —
  previously every such cid failed emitted=false. Also drive every
  captured reveal instead of the first 16 (pages with more reveals could
  never pass; this was the spurious "reveals 16/24").
- render/harness: the validator copies app sources over the preinstalled
  harness without installing deps, so a Lottie clone's injected
  lottie-web crashed webpack ("build failed" with no real signal).
  Missing app deps are now installed into the harness pinned to the
  app's exact version (--no-save, idempotent), with a clear gate issue
  on install failure.

Cotton motion gate: 33 static + 10 replayed + 24/24 reveals -> pass.
Cropin home build gate: pass (was webpack Module not found).
116/116 compiler tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:22:42 -07:00
Samraaj BathandClaude Fable 5 7f99b76f66 Fix build-breaking ident rewrite, blank-iframe grafting, and layout-gate regressions
Round-3 fixes from gate validation + human visual review of fresh
cropin.com and ooni.com clones:

- app.ts: section data-var rename used an unanchored substring replace,
  so Tile2_data matched inside MediaTile2_data and emitted a corrupted
  Mediatile2Data usage (ReferenceError at prerender). Word-boundary
  regex; every map site now shares one derivation.
- graft.ts: about:blank iframes were blanket-skipped, missing the
  Klaviyo newsletter form (mounted into a blank same-origin frame via
  JS) — the audit's #1 complaint. Blank frames now graft; the
  visibility gate still excludes 0-size tracking pixels.
- walker.ts: isVisible() now rejects boxes wholly outside the viewport
  (off-left always; off-right only when the page isn't horizontally
  scrollable; fixed boxes fully above/below), so a closed slide-in
  drawer's contents no longer count as expected text (91.8% -> 93.4%).
- css.ts: four sizing-regime corrections — pin captured px for
  circular shrink-0 slides (Splide slide chain collapsed 0x0);
  flex-basis:100% for full-width shrink-0 slides; keep fixed-px grid
  templates for scrolling track lists (repeat(N,1fr) squished a
  50-track carousel); single full-bleed fixed track -> minmax(0,1fr);
  keep authored heights whose in-flow children are fill children
  (aspect-video heroes inflated 240 -> 720px); width:100% for
  inset-spanned aspect boxes.

Gate scores: ooni home 88.7 -> 93.3 (responsive 32 fails -> pass),
pizza-ovens 90.8 -> 97.6 (perceptual 46.7% -> 17.2%).
107/107 compiler tests, all workspaces green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:42:34 -07:00
Samraaj BathandClaude Fable 5 199359d0a6 Wave 4: reveal settling + replay, 206/magic-byte media fix, hidden-geometry banding, bare-zero utility gating
Capture (4a):
- stabilize: dwell-scroll settling pass + known-library pre-reveal
  neutralization after lazy promotion, so every viewport snapshot records
  the post-reveal steady state (about-page sections no longer captured
  blanket-hidden: 24 base `invisible` wrappers -> 0)
- probeReveals: extended to the visibility + entrance-animation family;
  DittoMotion replays them (JS re-hide on mount, IntersectionObserver
  reveal, 4s force-reveal failsafe) so SSR/non-JS still shows content
- assets: 206 range-response bodies are no longer stored as full assets
  (full-fetch fallback) + video magic-byte validation - the about-page
  hero video is now a valid 6.0MB ftyp mp4 instead of 18.9KB of garbage

Generate (4b):
- css: hidden-node geometry policy - a visibility:hidden box with a 0x0
  captured bbox at a band becomes display:none there; an OCCUPYING hidden
  box gets the captured per-viewport geometry instead of the baked
  canonical one. Also covers the ancestor-hidden-at-base case (the
  cropin.com/cotton slider arrow: hide inherited from an elementor-widget
  ancestor at every width parked a desktop left:548px box inside a 375px
  viewport -> body.scrollWidth 585; now 375 on both verified pages)
- tailwind: bare-zero utility gating - font-size:0 -> text-[0px] and
  letter-spacing:0 -> tracking-[0px] (text-0/tracking-0 are silently
  invalid in Tailwind v4; fixes permanently-visible map labels on the
  cotton Global-presence section)

Verified: 87/87 compiler tests green, full-workspace suite green,
regenerated cropin.com + /cotton/ builds measure 375px scrollWidth at a
375px viewport, and a fresh /about/ clone renders all reveal sections
with scroll-replay against the live site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:33:10 -07:00
Samraaj BathandClaude Fable 5 9aed2540aa Clone-fidelity fix waves 1-3 (+ wave 4 in flight): capture settling, media, iframes
Fixes from the cropin.com / ooni.com audit reviews:
- walker: preserve whitespace-only text in inline elements ("ofthe" bug);
  capture ::placeholder styles for inputs
- css: emit visibility (hidden-at-rest wordmark artifact); exempt list
  markers from inherited elision (lost bullets); ::placeholder rules
- seo: sanitize og:type to Next's enum (render crash -> dev badge leak);
  generated next.config disables devIndicators
- capture/stabilize: promote lazy-loader data attrs before snapshots
  (collapsed sections, viewport-inconsistent captures); settle autoplay
  carousels to home slide before every snapshot; force-reveal hidden
  videos for poster shots + log failures
- generate: ship downloaded video files as local <video src>; keep
  picture>source through IR prune with srcset rewrite (mobile-crop-on-
  desktop heroes); pin Tailwind named screens to computeBands boundaries
- capture/graft: capture cross-origin iframe subtrees (Klaviyo signup
  forms) with element-screenshot fallback; asset download retry +
  visual-assets-missing reporting

Checkpoint commit: wave 4 (reveal settling, 206 range-response fix,
hidden-geometry banding, bare-zero utility gating) is mid-implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:45:42 -07:00
Samraaj BathandGitHub da537e68b8 Merge pull request #9 from ion-design/lottie-animation-capture
feat(compiler): capture and replay Lottie animations
2026-07-03 14:05:40 -07:00
Samraaj Bath 13ce4b89a1 Fix Lottie replay for Next builds and fallback sources 2026-07-03 14:03:42 -07:00
Samraaj Bath 7e192863b4 Merge remote-tracking branch 'origin/main' into lottie-animation-capture 2026-07-03 13:33:03 -07:00
Samraaj BathandGitHub 905fb14e2d Merge pull request #12 from michaelgbcole/updates
Add ditto unpack CLI to turn clone result JSON into a project tree
2026-07-03 13:18:02 -07:00
Samraaj Bath e28cc57c85 Fix unpack quickstart docs and hash verification 2026-07-03 13:16:05 -07:00
Samraaj BathandGitHub 4de6e244a0 Merge pull request #11 from ion-design/codex/readme-methodology-link
[codex] Add methodology doc and homepage link
2026-06-30 12:58:51 -07:00
Samraaj Bath 74219bb921 Add methodology doc and site link 2026-06-30 12:24:59 -07:00
Samraaj BathandGitHub 46d8c150d3 Merge pull request #2 from ion-design/dependabot/github_actions/actions/setup-node-6
Bump actions/setup-node from 4 to 6
2026-06-30 11:03:03 -07:00
Samraaj BathandGitHub b4d7ee6ff6 Merge pull request #10 from ion-design/codex/remove-opencode-sidecar
[codex] Remove opencode sidecar tooling
2026-06-30 10:53:29 -07:00
Samraaj Bath 893da2e332 Remove opencode sidecar tooling 2026-06-30 10:48:40 -07:00
Samraaj BathandGitHub bae9688e2b Merge pull request #1 from ion-design/dependabot/github_actions/actions/checkout-7
Bump actions/checkout from 4 to 7
2026-06-30 10:41:16 -07:00
Samraaj Bath d27f34bea8 Update hosted signup docs 2026-06-29 22:21:24 -07:00
Samraaj Bath 7d0e85ddb4 Allow www signup origin 2026-06-29 20:39:57 -07:00
Samraaj Bath cf8af6d936 Add verified email API key signup 2026-06-29 20:35:16 -07:00
Samraaj Bath 49a6541b24 Add public API key signup 2026-06-29 19:53:39 -07:00
Samraaj BathandGitHub 2b9ebd9a9b Merge pull request #8 from ion-design/codex/readme-usage-guide
[codex] Simplify README usage guide
2026-06-29 16:31:56 -07:00
Samraaj Bath 12dc7d9220 Simplify README usage guide 2026-06-29 16:29:39 -07:00
Samraaj BathandGitHub ca476b1a0f Merge pull request #5 from ion-design/codex/open-source-readiness
[codex] Prepare repo for open source
2026-06-29 16:22:59 -07:00
Samraaj Bath 3f7c9ed227 Prepare repo for open source 2026-06-29 16:14:44 -07:00
Samraaj Bath 66dfdcc58d Initial commit 2026-06-29 15:11:48 -07:00