Commit Graph
13 Commits
Author SHA1 Message Date
Xuanru LiandCursor 209e6e0148 feat(check): opt-in --layout proseCoverageFloor (#2834)
* feat(check): opt-in --layout proseCoverageFloor for text_occluded

Keep the default prose coverage floor at 0.15 for all callers, and allow
stricter agents (e.g. Zephyr) to lower it via --layout "proseCoverageFloor=0.05"
without changing other layout gates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(check): collapse --layout comments and docs to one line

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check): strict proseCoverageFloor parse + pin 0.07 floor tests

Reject trailing-garbage fractions that Number.parseFloat would accept, and
pin the existing ~0.07 coverage fixture for default vs floor=0.05 (atomic
labels unchanged) plus a collectLayout forwarding assertion.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check): share parseNumberStrict across layout and frame-check

Sweep the sibling --frame-check tol parser (and caption fractions) onto the
same strict Number() helper so trailing garbage cannot prefix-parse.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 16:27:09 -07:00
Xuanru Li 75ed99e1d4 fix(check): elongated pivot drift + counterfactual connector_detached (#2819) 2026-07-26 21:43:42 -07:00
Xuanru LiandClaude Opus 4.8 72e2f08f15 feat(lint): dense motion re-sampling for content_overlap (#2746)
* feat(lint): dense motion re-sampling for content_overlap

Transient text-on-text collisions during continuous motion (e.g. an
orbiting label card crossing the center card) overlap for a fraction of
a second that the sparse 9-point layout grid seeks straight past. The
content_overlap detector is correct; it just never gets a sample at the
crossing moment. Rerun ONLY content_overlap on an 8fps grid (text-only,
cheap) when the composition animates; findings feed the existing
persistence tiering unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(lint): unconditional dense content_overlap pass + honor 500ms floor

Round-1 blocker: the dense motion-overlap re-pass was gated on sparse-grid
geometry fingerprints changing, so an animation aliased to the sparse grid
(identical fingerprints, yet colliding between samples) bypassed the pass —
exactly the transient false-negative it was built to catch. Remove the gate:
the dense pass now runs unconditionally (bounded, text-only), driven by the
composition timeline rather than a fingerprint heuristic.

Round-2 follow-ups:
- Persistence-tier drift: at 8fps, occurrences>=2 spans only ~125ms, not the
  ~500ms the design intends, and it short-circuited before the ms floor.
  content_overlap promotion now requires BOTH occurrences>=2 AND a literal
  firstSeen..lastSeen span >= 500ms, so the wall-clock floor is honored at any
  sampling density. Comment block updated to match.
- Sample cap scales to hold a true 8fps grid up to ~75s (raised 120 -> 600)
  with an explicit note that longer comps degrade below 8fps to stay bounded.

Tests:
- Replaced the trivial "warning at every sample" test with a real between-grid
  regression: a collision living only inside (3.5,4.5) — a gap the sparse grid
  seeks past — is detected and, held ~750ms, promoted to error.
- Replaced the now-invalid "skips when static" test with one asserting the
  dense pass runs even when sparse fingerprints are identical (aliased motion).
- Added a tiering regression: two dense occurrences spanning ~125ms stay a
  warning (not error). Both new guards verified red before the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(lint): settle-free geometry seek for dense content_overlap pass

The dense overlap re-pass did up to OVERLAP_MAX_SAMPLES full-settle seeks
(120ms paint settle each, ~72s of pure sleep at the ceiling) even though
collectOverlap only reads getBoundingClientRect geometry, valid
synchronously after the timeline setTime. Add a settle-free
DENSE_GEOMETRY_SEEK_OPTIONS + driver.seekGeometry used only by the dense
loop; the base grid keeps full-settle driver.seek.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(lint): document + cover content_overlap 500ms boundary for sparse callers

The occurrences>=2 AND heldMs>=500 promotion rule is a semantics change for
sparse callers (--samples 20, --at, short comps) whose two samples can land
<500ms apart. Document the change in the tiering comment and add boundary
tests: 499ms span stays warning, 500ms span promotes to error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(lint): make dense content_overlap seek genuinely geometry-only

Per review: DENSE_GEOMETRY_SEEK_OPTIONS only overrode settleMs, still
inheriting animationFrameSettle:double + waitForFontsMs:500 → ~3 frame
waits + font wait per seek → ~30s at the 600-sample cap. Geometry
(getBoundingClientRect) is valid synchronously post-setTime, so drop all
post-seek waits (animationFrameSettle:none, waitForFontsMs:0, settleMs:0).
Add options-level regression locking the geometry-only contract. Also fix
a stale comment name (detectMotionTextOverlap → collectMotionOverlapSamples).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: collapse multi-line comments to single lines

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:29:24 -07:00
Xuanru Li e710a1686f feat(lint): add off_pivot_rotation hub-referenced layout check (#2744)
## What it catches

A gauge needle / clock hand / dial pointer / radar sweep that rotates about the **wrong pivot** — the recovered center-of-rotation sits far from the dial hub (e.g. `transform-origin` at the needle base or SVG element edge instead of the dial center). Visually the needle "wobbles" or orbits off-axis instead of sweeping cleanly about the hub.

This is a genuine gap in the current checks: `rotation_pivot_drift` (#2741) provably **cannot** catch it — a correct sweeping needle's bbox-center orbits identically to a broken one, so only a **dial-hub reference** distinguishes them. This is the separate hub-referenced check that analysis called for.

## How it works

- Sampler maps 2 material endpoints per frame via `getScreenCTM` (honors the actual rendered transform, independent of `svgOrigin`).
- Resolves the dial hub = shared center of the modal set of static concentric circles, or the arc-center of the largest static near-circular path (Kasa circle fit).
- Fits a circle to the endpoint trajectory to recover the true center-of-rotation; flags drift `> 0.35 * pointer_length`. One warning per hub.
- Never fires without a resolvable hub. Walks the rotation reference to the composition root (not the `<svg>`) so a pointer rotated by a `div` ancestor is measured correctly.
- Multi-body guard: `>= 2` bodies at distinct angular positions on one hub = orbit/atom system, not a dial → suppressed.

## Corpus evidence (autonomous geometry-fuzz run, 81 fuzzed diagrams)

- **7 / 7 true positives, 0 false positives across all 81 samples.**
- Assigned TPs: fuzz005, fuzz017, fuzz032. Bonus TPs: fuzz044, fuzz056, fuzz068, fuzz080.
- **The Gemini-3.6 video-judge itself MISSED all 4 bonus TPs** (`vlm_has_defects: false`) — the deterministic hub-reference check beats the VLM on this defect class.
- FPs driven to 0 by the two principled guards above: fuzz016 (planet arc rotated by a `div` ancestor) cleared by root-walk; fuzz055 (atom) cleared by the multi-body guard.
- fuzz080 reads as a false positive to the connector check but is a true positive here — confirms the architectural boundary between the two checks is drawn correctly.

## Validation

- Autonomous Gemini-3.6 **video**-judge fuzz run to surface candidate defects, then a **deterministic FP sweep** across all 81 rendered compositions (not VLM-gated — code inspection is the arbiter, since the VLM both over- and under-calls this class).
- 9 unit tests (`checkPipeline.offPivotRotation.test.ts`) + full check suite pass; `bun run build` green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 17:38:58 -07:00
Xuanru Li 222aec45ab feat(lint): add rotation_pivot_drift layout check (#2741)
## What
New cross-sample layout check `rotation_pivot_drift` — flags a rotating element that should spin **in place** but pivots about the **wrong point** (e.g. a wheel whose spokes use a hardcoded px `transformOrigin` instead of `svgOrigin`/`%`, so they swing off-center while every existing check still passes).

Motivating prod case: a portrait ad's spoked-wheel whose `#spokes` rotated about `transformOrigin:"250px 250px"` in a resized 460px container — spokes detached from the hub, shipped clean because no rule inspects rotation.

## How
- `layout-audit.browser.js`: `window.__hyperframesRotationSample()` reports each visible transformed element's bbox center + decoded rotation angle per layout sample. Skips `[data-layout-allow-orbit]`.
- `checkPipeline.ts`: accumulates samples across the seek grid; `detectRotationPivotDrift()` (modeled on `detectSweepStatic`) flags an element that (a) actually spins (angle spread > 20° over ≥3 samples), (b) is size-stable (bbox width ratio ≤ 1.6), and (c) whose bbox **center** drifts > `max(10% of its size, 2% of min viewport dim)`. Emits `warning`; not persistence-tiered (not demoted to info).

## FP guards
Real rotation required, ≥3 samples, size stability, `data-layout-allow-orbit` exemption, min area ~2500px². Center-drift (not bbox size) is the discriminator, so a correctly-centered spinner reads drift ≈ 0.

## Validation (`check --json`)
| Fixture | Expected | Result |
|---|---|---|
| off-transformOrigin spoked wheel | fire | **fired — 109px drift on `#spokes`** |
| non-spinning comps (node diagram, device tree) | clean | clean, no FP |
| correctly-centered spinner (`svgOrigin`) | clean | clean (spins 162°, drift 0) |
| `data-layout-allow-orbit` off-origin spinner | clean | clean (exempt) |
| off-`svgOrigin` control, no opt-out | fire | fired — 251px drift |

No false positives. `tsc --noEmit` clean, `oxlint` clean, `check.test.ts` + `layout-audit.browser.test.ts` = 112/112 pass.

## Note
`ROTATION_MAX_SIZE_RATIO` is 1.6 (not 1.3): a rotating anisotropic shape's axis-aligned bbox inherently oscillates (8-spoke star ~1.32×, square 1.41×), so a tighter ratio rejects legitimate targets. Center-drift stays the real discriminator; thin swinging bars are excluded.

Follow-up: a `detectRotationPivotDrift` unit test via the fake driver's `collectRotationSample` (mirroring the sweep_static tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-23 01:44:46 -07:00
Miguel Angel Simon Sierra 67cdf0fdb7 fix(cli): address review — clip-duration audit in check, failure classing, crop observability
Port validate's per-media-element clip audit into check's session
(clip_media_fit findings): an intrinsic duration meaningfully shorter
than the data-duration slot silently shortens the slot at render time,
and neither lint nor the runtime listeners can see it. A linter crash
now reports as check_lint_failure instead of masquerading as a runtime
failure. Finding-crop capture failures stay non-gating but emit a
stderr note and a telemetry error event so rollouts can measure the
second-session failure rate.
2026-07-10 17:53:04 -04:00
Miguel Angel Simon Sierra 4b03866c02 fix(cli): contrast gate judges only readable content; examples pass check
Three filters keep the escalated contrast gate honest, each surfaced by
running check across the registry examples:
- data-layout-ignore (the layout audit's existing decorative opt-out)
  now also excludes set-dressing text from the contrast audit — one
  vocabulary for 'not copy a viewer must read'.
- Text that has (nearly) left the canvas is skipped: sampling a clamped
  off-canvas box reads border pixels and produced the classic false
  white-on-white (a cursor exiting the frame).
- Contrast failures follow the same persistence rule as layout findings:
  observed at a single sample of a multi-sample sweep demotes to
  warning; held failures gate.

Example fixes the sweep exposed: motion-blur's 21 deliberately-dim rail
labels are marked decorative (its vivid labels pass on their own);
nyt-graph's subtitle and source-note adopt the gate's suggested
compliant gray; decision-tree's declared duration drops 15s to 10s —
its content ends at ~9.5s and every render shipped a blank white tail.
2026-07-10 14:08:31 -04:00
Miguel Angel Simon Sierra cea3458016 refactor(cli): single-source-of-truth pass over the check branch
Every duplicated decision gets one owner: rectToBbox lives in checkTypes
(was verbatim in pipeline and browser layers); the audit seek tuning is
one exported AUDIT_SEEK_OPTIONS consumed by check and the deprecated
inspect path; zoom padding/scale defaults export from the capture module
instead of re-literalized in three files; the optional run_id property
is built by one helper across all three telemetry events; check's
--max-transition-samples parsing reuses its own positiveInteger helper;
validate drops a leftover re-export and redundant explicit-default args
go away.

Tests: the contrast candidate round-trip gains a real integration
anchor (the actual browser script eval'd in-page, a wrapper asserting
finish receives the page-script bbox shape) replacing regex-over-source
as the primary guard; the redundant geometry source-golden and a
duplicated deprecation-envelope assertion are dropped.
2026-07-10 13:52:31 -04:00
Miguel Angel Simon Sierra 94f6de8b8d feat(cli): persistence-tiered findings, frozen-sweep guard, occlusion coverage
Layout findings now distinguish held defects from entrance/exit
transients: a dynamic issue seen at a single grid sample demotes to
info, while content_overlap held across two-plus samples (or 500ms+)
promotes to error, resolving the long-standing re-promotion TODO. Static
compositions keep their severity. check gains a sweep_static error when
a 3s+ composition shows zero geometry change across every sample (a
frozen timeline makes every green verdict unreliable); skipped when the
motion sidecar already reported motion_frozen. text_occluded findings
carry a coveredFraction; atomic labels (short, no whitespace) flag on
any cover while prose needs 15%, since partial cover changes what a
short label reads as.

Deprecation-test scaffolding consolidates into deprecationTestHarness;
tier logic and logger tests restructured under the complexity gate
without suppression markers.

Detection mechanics adapted from Adam Rosler's open-sourced
visual-linter design (github.com/Adam-Rosler/hyperframes-visual-linter-design);
the elementFromPoint paint model, opt-out attributes, and single-audit
architecture are unchanged.
2026-07-10 13:27:52 -04:00
Miguel Angel Simon Sierra f4cef54b8b feat(cli): snapshot --zoom and per-finding crops on check --snapshots
snapshot --zoom <selector|x,y,w,h> + --zoom-scale (default 3) crops via
Puppeteer clip at raised deviceScaleFactor — density changes, layout
never does. Selector resolves per frame with 24px padding; no match is
a loud error, and a frame whose clamped region is a sliver (element
collapsed or animated off-canvas) is skipped with a stderr note rather
than written as a useless few-pixel image.

check --snapshots additionally writes finding-NN-<code>.png crops for
error findings with bboxes (cap 12, deterministic re-seek in a second
session) and draws labeled annotation boxes on overview frames via a
transient overlay injected only after audits complete. Skill reference
gains the zoom workflow: check reports a finding, zoom into it, fix,
re-check.
2026-07-10 13:27:52 -04:00
Miguel Angel Simon Sierra 3a02942a03 feat(cli): run-ID telemetry correlation and check breakdown event
HYPERFRAMES_RUN_ID (trimmed, 128-char cap) attaches as run_id to the
generic cli_command / cli_command_result events, absent when unset, so
an orchestrator setting it per design element can group a verify loop's
invocations in analytics. check additionally emits one check_report
event per invocation (including lint-short-circuited and failing runs):
gate booleans, per-class error/warning counts, launch/seek/contrast
phase timings, sample counts, ok and exit code. Timings stay internal;
no command output changes.
2026-07-10 13:27:52 -04:00
Miguel Angel Simon Sierra 7ab6c2b7a2 feat(cli): caption-zone and frame-check gates on check
Ports the EF bridge's captionZone and frameCheck semantics as opt-in
flags so the bespoke bridge can be retired: --caption-zone takes
fractional band geometry (x0;y0;x1;y1) with optional severity routing
and seek points, defaults matching the bridge (caption seek [1], frame
seek [0.5], 2px tolerance, 0.05 opacity floor, 4px minimum size, 0.95
full-frame exclusion, center-in-band comparison, tag|text dedup).
--frame-check adds media bounds detection (img/svg/video/canvas) the
always-on text canvas_overflow never covered, reusing overflowFor.
Breach floor: max(120px, 6% of min canvas dimension). Band math derives
from the composition's own canvas, portrait included. Both gates off by
default; plain check output unchanged.
2026-07-10 13:27:52 -04:00
Miguel Angel Simon Sierra 7d6d41361b feat(cli): add check — single-session verification gate
One command, one Chrome boot: in-process lint gate (browser skipped on
lint errors), passive runtime capture wired before navigation, layout +
motion + contrast audits over one seek grid, optional --snapshots
persisting the contrast-pass screenshots. Aggregated --json envelope
{ok, lint, runtime, layout, motion, contrast, snapshots}; findings carry
selector/data-*/source-file/bbox/time anchors, contrast findings include
fg/bg, measured vs required ratio, and a compliant color suggestion.
Contrast AA failures gate the exit code (they were warning-only in
validate); --strict gates warnings.

Contrast candidates round-trip verbatim between __contrastAuditPrepare
and __contrastAuditFinish: the page script owns their shape (bbox w/h),
and normalizing them Node-side made every sample rect NaN — the audit
reported zero checked elements as green. Regression-pinned in
check.test.ts; E2E on a low-contrast fixture now exits 1 with 8 findings.

Measured on kinetic-type: check 5.6s vs 23.0s for sequential
validate + inspect + snapshot.
2026-07-10 13:27:52 -04:00