14 KiB
ProofOfHuman — handoff (2026-07-14, rev 2)
Anti-bot human-verification component, re-architected from a single
drag-to-target widget into an orchestrator + pluggable challenges. Three
challenge surfaces now ship: PathTrace, RotateAlign (compass skin) and
Clock (the cuckoo-clock skin over the same RotateAlign engine) — landed
this session. Read this top-to-bottom to resume cold.
⚠️ Concurrency warning — READ FIRST
Another Claude session is working on palabras / words in this SAME repo and
committing to the SAME branch (alpha-0.1-sec-dom) concurrently. The git index
(.git/index) is shared, so a plain git add … then git commit can be
clobbered mid-way (it happened: their 5b1e228a words(align) reset my staged
index). Mitigations: stage + commit ATOMICALLY in one command
(git reset && git add … && git commit … && git push); never leave files staged
across tool calls; consider working on a separate branch. Never touch
words/**, palabras/**, chronos/**, web/routes/alpha/**, rt.css,
src/uix/eidos/lib/recipes/base.ts (chronos hunk) — stage explicit paths only.
The architecture (shipped)
ProofOfHuman= orchestrator. Interaction- and purpose-agnostic. Owns the abstract stages (idle → active → verifying → verified / failed), the evaluable verdict (commit-confirm / fail / cancel), live-region announcements and theonSolve(telemetry)handoff. Network-agnostic (the app verifies with a single-use server nonce and drivesstatus). Per-stage animation lives in consumer slots (stageIdle … stageFailed).ProofOfHumanChallengeHost— the context a challenge uses to report up:stage·locked·begin()·solve(telemetry)·cancel().- A challenge = its own component + morfo, consumes
ProofOfHumanProvider.require(), produces telemetry, drives the lifecycle. Swapping the challenge never touches the orchestrator. Each challenge is a headless soma triplet (Provider + two parts) that eidos SKINS. - Telemetry (
ProofOfHumanTelemetry, insoma/components/proof-of-human/types.ts): base motion fields + an optional per-challenge block —pathAdherence?(PathTrace) orrotation?(RotateAlign). Add a new block for a new challenge kind.
Shipped challenges
PathTrace — follow a random curved path
- Follow a per-instance random Catmull-Rom curve (pointer OR arrow keys), staying inside a tolerance corridor; progress advances ONLY in-corridor and monotonically. Keyboard yields real signal → no free bypass; a two-key script can't solve it. Skin: envelope token traced to a carrier pigeon.
- Files:
morfo/components/path-trace.ts,soma/components/path-trace/(curve.ts is pure geometry),sema/components/path-trace.ts,eidos/components/proof-of-human/path-trace.svelte(+.css).
RotateAlign — rotate to a random bearing ← the clock's engine
- Rotate a needle onto a per-instance random bearing and HOLD it steady until it
settles (pointer OR arrow keys). Composes
Gesture.rotate(). Settle-to-lock viauix.timers(each move reschedules → a wiggle never locks, stillness locks). Detent tick fireshandle-dragevery 30°. Telemetry:rotationblock (totalRotationDeg / maxAngularVelDegMs / overshootCount / finalErrorDeg / settleMs). UnwrappedvisualAngledrives the needle transform so a CSS transition never spins the long way round the 360→0 seam. - Knobs:
toleranceDeg(8) ·settleMs(450) ·stepDeg(6) ·seed. - Files:
morfo/components/rotate-align.ts,soma/components/rotate-align/,sema/components/rotate-align.ts,eidos/components/proof-of-human/rotate-align.svelte(+.css). - Current skin = compass: dial + ticks + N/E/S/O + a MAILBOX target (the
pigeon's destination) + a magnetically-settling needle. Exposed as
ProofOfHuman.RotateAlign.
Demo
web/routes/uix/components/proof-of-human/+page.svelte — a challenge switcher
(PathTrace | RotateAlign), per-challenge controls, the carrier-pigeon narrative
over the stage slots, telemetry table (pathAdherence + rotation rows).
Verified
- Tests (server + real chromium): 25/25 — both morfos, both providers (each: pointer AND arrow trace completes with real signal; a two-key script can't solve; Escape cancels), curve geometry.
- End-to-end in real Chrome — both challenges reach
verifiedfrom a genuine solve; incomplete/trivial attempts don't;difficultyvisibly reshapes the PathTrace curve; the compass renders + the needle settles. - Type-check clean across the component;
component:auditPASS (the two harness false positives D-1.5/D-4.3 are documented in the eidos README).
SHIPPED this session: the CLOCK skin (cuckoo)
Built exactly to the locked mockup spec below. What landed:
eidos/components/proof-of-human/clock.svelte+clock.css— composes the SAME soma parts (RotateAlign.Provider/Dial/Needle); exported asProofOfHuman.Clock(+ClockPropsintypes.ts, mirroringRotateAlignProps).clock.svelteimportsrotate-align.csstoo (shared skin box + needle behavior).- One engine knob added (declared deviation from "engine unchanged" — the
design needs hour-aligned targets): soma
RotateAlignProviderProps.targetSnapDeg?quantizes the random bearing (buildTarget). The Clock passes 30 → 12 possible hours; a free bearing would miss the marked hour by up to 15° > the 8° tolerance ~47% of the time. Unit-tested (% 30 === 0across seeds + random). - Face: ring + 11 hour ticks (12 = the house) + numbers at 3/6/9 + a
target-hour badge (accent circle + number,
--color-content-on-solidtext) at r=68; the badge REPLACES the 3/6/9 numeral when it lands there; hour 12 special-cases to under the sill (y=58). Hand = tapered pointer with counterweight and hub (content-primary), tip r=58 meeting the badge edge. - Cuckoo sequence (all CSS in
clock.css, keyed on[data-proof-of-human][data-state='verified'], every@keyframesannotated/* functional: */per R-4.5): doors swing ±118° (0.5s) → pigeon (FRONT-ON: two eyes, beak down holding the letter) rises to peek — one 3.4s keyframe timeline (wait 13% → peek 26% → hold 62% → fly 100%, per-segment easing viaanimation-timing-functioninside keyframes) — body stays occluded by aclipPathwhose bottom edge = the sill top ($props.id()for the clip id) → wings appear + flap (2.1sdelay = the 62% mark) and the bird rises, shrinks, fades IN FRONT of the roof (pigeon drawn last). Reduced-motion: doors + peek static, no flight. Pigeon palette = the demo bird's literal hexes (annotatedliteral:); house/face = theme tokens; roof + badge flip to fulfill on verified. - Demo: third chip (PathTrace | RotateAlign | Clock) reusing the rotate knobs;
clock-specific idle instruction; for the Clock the demo's success card is
DELAYED 3.3s (
proof-of-human:cuckootimer) so the cuckoo dispatch plays uncovered, and the demo's corner stage-pigeon is suppressed in BOTH the verifying and verified slots (the cuckoo IS the pigeon — user caught the verifying one still showing).
The original CLOCK spec (kept for reference)
The user's favourite skin. It reuses the RotateAlign engine unchanged — only the visual is new. The design is locked via a mockup that went through several iterations; reproduce it faithfully (the mockup is not visible in a new session, so the spec is here).
Concept
A cuckoo clock. "Set the clock to N" → the hand ticks to the hours (the RotateAlign detents), and on settle the clock's little doors swing open and a pigeon leans out FRONT-ON with the letter in its beak, then flies off (the cuckoo → a messenger pigeon). This unifies the whole component's narrative: the compass points to the mailbox, the clock dispatches the pigeon — one messenger.
Mapping to the engine
- The needle = the clock HAND.
stepDegper arrow.toleranceDeg/settleMsas-is. - The detents (every 30°) = the hours; the
handle-dragtick per detent = the clock ticking. Consider snapping the hand to whole hours in the skin. - The target bearing (
DialsnippettargetAngle) = the target HOUR (targetHour = round(targetAngle/30), 0→12). Mark it on the face. - On the orchestrator
data-state='verified'→ run the cuckoo reaction.
Visual + animation spec (from the mockup)
- Face: ring + 12 hour ticks + numbers at 3/6/9 (12 is replaced by the cuckoo house). A target marker at the target hour.
- Cuckoo house at the TOP (12 o'clock): a little roof (triangle) over a dark
doorway interior, two doors (left/right) that SWING open, and a sill bar
in front. Doors swing via
transform-box: fill-box; transform-origin: left/right center; transform: rotate(±118deg). - The pigeon must be FRONT-FACING (this was the sticking point — a side-profile bird was rejected twice). Head-on: a round head with TWO eyes, a small beak pointing DOWN holding a little envelope, an oval body, and two wings that are hidden until flight.
- Sequence on verified: doors swing open (~0.5s) → pigeon PEEKS — head + letter emerge ABOVE the sill while the body stays hidden BEHIND the sill (occlusion: draw the pigeon before the sill; the sill bar sits in front of the lower body) → hold ~0.8s → FLY — the wings appear + flap (rotate, alternate) and the bird rises, shrinks and fades up-and-away (front-on flying-toward-you).
- CRITICAL SVG gotcha (bit me 3×): an SVG element that carries a
transformattribute for positioning — CSStransformOVERRIDES it entirely (jumps to the origin). Wrap it: OUTER group keeps the position (transform="translate(x y)"attr), INNER group does the CSS animation (transform-box: fill-box). Same fix the compass mailbox uses. - Doctrine: the pigeon-fly is bespoke CONTENT animation (its own CSS/rAF via
eidos.dom/eidos.timers, like the demo pigeon), NOT an--eventrecipe@keyframes(R-4.5). The verdict flourish itself is the genericcommitmotion signature. Timers viauix.timers, never rawsetTimeout.
How it was built
All four steps above were executed as specced (the hand stays continuous — no
visual snap-to-hour: with stepDeg 6 a 30° visual quantum would kill the
feedback per press; the TARGET is what snaps). Verified end-to-end in real
Chrome: idle face, live door-swing/peek/fly frames (light + dark), telemetry →
verified from genuine arrow signal, component:audit PASS (144/144), tests
81/81 green across the poh scopes, type-check clean on touched files.
Verification-env gotchas
- The in-app Browser pane / a backgrounded Chrome tab report
visibilityState: hidden, which freezes rAF →dom.measurenever fires (PathTrace curve never builds). Acomputerscreenshot foregrounds the tab. RotateAlign has no rAF dependency (target computed in the constructor). - Background throttling also stretches
setTimeoutto ≥1s — a JS solve loop with 30ms waits blows the 45s CDP budget in a backgrounded tab. Screenshot first (foregrounds), then run the script, ideally in ONEbrowser_batch. - To FILM a one-shot CSS choreography: solve + zooms in the SAME
browser_batch(inter-call latency between separate tool calls eats a 3.4s window), or freeze poses with a debug<style>overrideanimation: … paused !important; animation-delay: -1.5s !important(restart it viaanimation: none+ reflow first — redeclaring the same animation-name does NOT restart it). - Screenshots on the in-app Browser pane hang; use the claude-in-chrome MCP (real Chrome). Screenshot perms are per tab-group — a fresh tab may need re-grant.
form_input(with a ref) triggers Sveltebind:value; coordinate-typing is unreliable. Filling via the native value-setter +inputevent also works.- Driving a trace from JS: precompute the input sequence (arrows / pointer deltas /
rotation) — the reactive DOM doesn't update synchronously inside a JS loop, so
read state BEFORE, compute the whole sequence, dispatch. Add real waits so
durationMs > 150passes the demo'sautoscorer. - Dev server:
npm run dev(picks a free port; was on 5174). The Edit-hook may warn that "another chat's dev server is running" — start your own if the Browser tools can't reach it.
Commits (alpha-0.1-sec-dom)
1f7018b9re-architecture (orchestrator + PathTrace, 40 files)f897bdb6component:audit fixes ·e1b5dec8/139cc2b6docse2a20b95PathTrace pointer testc4f5b080RotateAlign (engine + compass skin + demo switcher + difficulty fix)- (this session) Clock — cuckoo skin +
targetSnapDeg+ demo card delay + eidos README challenge list.
COMPONENT COMPLETE (2026-07-14, closing pass)
The documentation debt was cleared in the closing pass: soma rotate-align
README written (anti-bot core, parts, props incl. targetSnapDeg, keyboard,
telemetry rotation block, sema events, compass+clock skins); eidos README
links all three morfos/challenges; the demo's Live snippet, API tab, morfo tab
and Services tab now follow the SELECTED challenge (the two challenge morfos
share handle-* event names, so the sema panel/tables swap instead of merging
— merged lists would collide on keyed {#each}). The soma orchestrator README
also gained "Adding a new challenge" — the 8-step recipe (morfo → langs →
soma provider → telemetry block → sema pack → eidos skin → tests → demo+gates)
with the non-negotiable invariants, distilled from how RotateAlign/Clock were
actually built.
Still open / deferred (not blocking, out of component scope)
- Foreign blocker:
morfo:check/perm:check/morfo:vocabularycrash repo-wide ontext-blur.ts(task_05ed143d) — not ours; tests + browser cover the smoke. - Future (v2 roadmap): more challenges (hold-to-confirm, audio/cognitive for non-motor a11y); server-driven difficulty by risk score.