You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/continue-proof-of-human-202...

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 the onSolve(telemetry) handoff. Network-agnostic (the app verifies with a single-use server nonce and drives status). 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, in soma/components/proof-of-human/types.ts): base motion fields + an optional per-challenge block — pathAdherence? (PathTrace) or rotation? (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 via uix.timers (each move reschedules → a wiggle never locks, stillness locks). Detent tick fires handle-drag every 30°. Telemetry: rotation block (totalRotationDeg / maxAngularVelDegMs / overshootCount / finalErrorDeg / settleMs). Unwrapped visualAngle drives 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 verified from a genuine solve; incomplete/trivial attempts don't; difficulty visibly reshapes the PathTrace curve; the compass renders + the needle settles.
  • Type-check clean across the component; component:audit PASS (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 as ProofOfHuman.Clock (+ ClockProps in types.ts, mirroring RotateAlignProps). clock.svelte imports rotate-align.css too (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 === 0 across 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-solid text) 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 @keyframes annotated /* 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 via animation-timing-function inside keyframes) — body stays occluded by a clipPath whose bottom edge = the sill top ($props.id() for the clip id) → wings appear + flap (2.1s delay = 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 (annotated literal:); 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:cuckoo timer) 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. stepDeg per arrow. toleranceDeg/settleMs as-is.
  • The detents (every 30°) = the hours; the handle-drag tick per detent = the clock ticking. Consider snapping the hand to whole hours in the skin.
  • The target bearing (Dial snippet targetAngle) = 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 transform attribute for positioning — CSS transform OVERRIDES 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 --event recipe @keyframes (R-4.5). The verdict flourish itself is the generic commit motion signature. Timers via uix.timers, never raw setTimeout.

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.measure never fires (PathTrace curve never builds). A computer screenshot foregrounds the tab. RotateAlign has no rAF dependency (target computed in the constructor).
  • Background throttling also stretches setTimeout to ≥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 ONE browser_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> override animation: … paused !important; animation-delay: -1.5s !important (restart it via animation: 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 Svelte bind:value; coordinate-typing is unreliable. Filling via the native value-setter + input event 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 > 150 passes the demo's auto scorer.
  • 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)

  • 1f7018b9 re-architecture (orchestrator + PathTrace, 40 files)
  • f897bdb6 component:audit fixes · e1b5dec8 / 139cc2b6 docs
  • e2a20b95 PathTrace pointer test
  • c4f5b080 RotateAlign (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:vocabulary crash repo-wide on text-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.

Powered by TurnKey Linux.