docs(proof-of-human): close out the component - READMEs + challenge-aware demo

- soma rotate-align README (was missing): anti-bot core, parts, props incl.
  targetSnapDeg, keyboard, rotation telemetry, sema events, compass+clock skins.
- eidos README links all three challenge morfos/READMEs.
- demo: the Live snippet, API tab, morfo tab and Services tab now follow the
  SELECTED challenge (RotateAlign/Clock knob table added; the two challenge
  morfos share handle-* names so tables swap instead of merging).
- prettier normalization of the compass rotate-align.svelte (pre-existing warn).
- handoff marked COMPONENT COMPLETE; only v2 roadmap + foreign text-blur
  blocker remain.

Gates: component:audit 144/144 PASS, svelte-check clean on touched files,
prettier clean, demo tabs verified live in Chrome.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent caa4397b3d
commit 8b06f6bc70

@ -213,14 +213,19 @@ animation-delay: -1.5s !important` (restart it via `animation: none` + reflow
- (this session) **Clock** — cuckoo skin + `targetSnapDeg` + demo card delay +
eidos README challenge list.
## Still open / deferred (not blocking)
## 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}`).
## 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: more challenges (hold-to-confirm, audio/cognitive for non-motor a11y);
server-driven difficulty by risk score.
- Full READMEs for `rotate-align` (soma) + a deeper clock section (the eidos
README got the challenge list + selectors delta this session; the soma
rotate-align README still doesn't exist).
- The demo's Live code snippet still shows PathTrace regardless of the selected
challenge (pre-existing; also true for RotateAlign).
- Future (v2 roadmap): more challenges (hold-to-confirm, audio/cognitive for
non-motor a11y); server-driven difficulty by risk score.

@ -12,9 +12,12 @@ the letter, then flies off). The consumer skins the moving token/needle and the
per-stage overlays (the carrier-pigeon narrative in the demo is one such skin).
Headless contract: [soma orchestrator](../../../soma/components/proof-of-human/README.md) ·
[soma challenge](../../../soma/components/path-trace/README.md) · Morfos:
soma challenges: [path-trace](../../../soma/components/path-trace/README.md) ·
[rotate-align](../../../soma/components/rotate-align/README.md) (the engine under
both the compass and the clock) · Morfos:
[`proof-of-human.ts`](../../../morfo/components/proof-of-human.ts) ·
[`path-trace.ts`](../../../morfo/components/path-trace.ts). APG: `none` (no official
[`path-trace.ts`](../../../morfo/components/path-trace.ts) ·
[`rotate-align.ts`](../../../morfo/components/rotate-align.ts). APG: `none` (no official
pattern for drag/trace; the arrow-key equivalent + live announcements follow WAI-ARIA
general guidance).

@ -48,7 +48,9 @@
{/each}
{#each CARDS as c (c.a)}
{@const p = pos(c.a, R - 20)}
<text class="ra-card" class:n={c.a === 0} x={p.x} y={p.y + 4} text-anchor="middle">{c.l}</text>
<text class="ra-card" class:n={c.a === 0} x={p.x} y={p.y + 4} text-anchor="middle"
>{c.l}</text
>
{/each}
<g class="ra-dest" class:aligned transform="translate({m.x} {m.y})">
<g class="ra-dest-inner">

@ -0,0 +1,81 @@
# RotateAlign (soma)
A human-verification **challenge** for [`ProofOfHuman`](../proof-of-human/README.md):
rotate a needle onto a randomly placed target bearing and **hold it steady** until
it settles. It requires a `ProofOfHuman.Provider` ancestor — it reports its
behavioural telemetry up through `ProofOfHumanChallengeHost` and the orchestrator
owns the verdict.
## Why this exists (the anti-bot core)
- **Two input modes, one signal.** Pointer drag around the dial OR **arrow keys**
both swing the same needle and feed the same angular telemetry — the keyboard
path is a first-class challenge, not a signal-free bypass.
- **Settle-to-lock.** Completing requires holding the needle on target for
`settleMs` with no further movement: each move reschedules the settle timer
(`uix.timers`), so a wiggle never locks and stillness on target does. The
settle dynamics (overshoot, corrections, angular-velocity profile) are the
discriminating, non-replayable signal.
- **Randomised per mount** (seeded — pass `seed` for reproducible tests/demos).
A replay or a two-key script can't solve it.
## Parts
| Part | Element | Archetype | Description |
| ---------- | ------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `Provider` | `<div>` | `provider` | Generates the target bearing, owns the interaction + telemetry. States `idle\|rotating\|aligned\|complete`. |
| `Dial` | `<div>` | `track` | The face + target marker. Decorative to AT. Exposes `{ targetAngle, angle, aligned }` for eidos to paint. |
| `Needle` | `<div>` | `thumb` | The rotating element (role `button`, `aria-roledescription` "rotating needle"). Rotation is a soma-driven functional transform. |
Soma exposes these three headless parts; eidos composes them into TWO styled
drop-ins over the same engine: `ProofOfHuman.RotateAlign` (the compass — dial +
cardinals + a mailbox target) and `ProofOfHuman.Clock` (the cuckoo clock — set
the hand to the marked hour; the doors swing open and the messenger pigeon flies
off on the verdict). Any rotation metaphor works (a key in a lock, a safe dial…).
## Props (`Provider`)
| Prop | Type | Default | Description |
| --------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toleranceDeg` | `number` | `8` | Angular tolerance (deg) that counts as on-target. |
| `settleMs` | `number` | `450` | Milliseconds the needle must hold steady on target to lock. |
| `stepDeg` | `number` | `6` | Degrees the needle rotates per arrow-key press. |
| `targetSnapDeg` | `number` | — | Quantize the random target to multiples of this many degrees — for skins whose face marks discrete positions (the Clock passes `30` so the target always lands on a whole hour). Omit for a free bearing. |
| `seed` | `number` | random | Deterministic target (tests/demos). Omit = random per mount (the anti-replay property). |
Snippet props: `Needle → { rotating, aligned, angle }` · `Dial → { targetAngle, angle, aligned }`.
Angles are degrees, `0 = up`, clockwise. The provider keeps an unwrapped
`visualAngle` for the needle transform so a CSS transition never spins the long
way round the 360→0 seam; the logic angle stays normalized `[0, 360)`.
## Keyboard
| Focus | Key | Action |
| ------ | ----------------- | ------------------------------------------------- |
| Needle | `←` `↓` / `→` `↑` | Rotate by `stepDeg` (counter-/clockwise; starts). |
| Needle | `Escape` | Cancel the rotation. |
When the challenge is **armed** (the orchestrator goes disabled → enabled while
idle), the provider focuses the needle so the arrow keys work immediately —
gated on the transition, so an already-enabled mount never steals focus on load.
## Telemetry
Base motion fields + the `rotation` block: `totalRotationDeg`,
`maxAngularVelDegMs`, `overshootCount` (sign flips of the target offset outside
tolerance), `finalErrorDeg`, `settleMs` (time from first on-target to lock).
The tip path is sampled on a 100px-radius circle so the base fields
(pathLength/velocity/straightness) stay comparable across challenges.
## Sema events
| Event | Family | Verb | Target | When |
| ------------- | -------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `handle-pick` | `handle` | `pick` | `needle` | Rotation began (pointer down / first arrow). |
| `handle-drag` | `handle` | `drag` | `needle` | Detent tick — fires each time the needle crosses a 30° mark. |
| `handle-drop` | `handle` | `drop` | `dial` | Settled on the target within tolerance; hands telemetry to the orchestrator. |
The evaluable verdict is the orchestrator's `commit`, never the challenge's.
Pack: `sema/components/rotate-align.ts`. Composes the `Gesture.rotate()` layer
(angle + angular velocity + centre-at-pick; zero per-move layout reads).

@ -18,6 +18,7 @@
import { compileMorfo } from '$uix/morfo';
import { proofOfHumanMorfo } from '@/uix/morfo/components/proof-of-human';
import { pathTraceMorfo } from '@/uix/morfo/components/path-trace';
import { rotateAlignMorfo } from '@/uix/morfo/components/rotate-align';
import { getActiveUix } from '$active-uix';
import SystemAxes from '../../lib/SystemAxes.svelte';
@ -161,9 +162,19 @@
}
});
// ── Compiled morfos (orchestrator + challenge) ──────────────────────────────
// ── Compiled morfos (orchestrator + the ACTIVE challenge) ───────────────────
// The two challenge morfos share the handle-* event names, so the tables (and
// the sema panel) follow the selected challenge instead of merging both.
const compiled = compileMorfo(proofOfHumanMorfo);
const compiledChallenge = compileMorfo(pathTraceMorfo);
const compiledPathTrace = compileMorfo(pathTraceMorfo);
const compiledRotateAlign = compileMorfo(rotateAlignMorfo);
const challengeMorfo = $derived(challenge === 'path-trace' ? pathTraceMorfo : rotateAlignMorfo);
const compiledChallenge = $derived(
challenge === 'path-trace' ? compiledPathTrace : compiledRotateAlign
);
const challengeName = $derived(
challenge === 'path-trace' ? 'PathTrace' : challenge === 'clock' ? 'Clock' : 'RotateAlign'
);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const actions = $derived([...compiled.actions.byName.values()]);
const challengeParts = $derived([...compiledChallenge.parts.byKebab.values()]);
@ -187,7 +198,9 @@
' status = res.ok ? "verified" : "failed";',
' }}',
'>',
` <ProofOfHuman.PathTrace${difficulty !== 'md' ? ` difficulty="${difficulty}"` : ''} />`,
challenge === 'path-trace'
? ` <ProofOfHuman.PathTrace${difficulty !== 'md' ? ` difficulty="${difficulty}"` : ''} />`
: ` <ProofOfHuman.${challengeName}${toleranceDeg !== 8 ? ` toleranceDeg={${toleranceDeg}}` : ''}${settleMs !== 450 ? ` settleMs={${settleMs}}` : ''}${rotateStep !== 6 ? ` stepDeg={${rotateStep}}` : ''} />`,
' {#snippet stageVerified()}<Pigeon flying />{/snippet}',
'</ProofOfHuman>'
]
@ -282,13 +295,7 @@
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill><span data-uix-meta-key>stages</span>5</span>
<span data-uix-meta-pill
><span data-uix-meta-key>challenge</span>{challenge === 'path-trace'
? 'PathTrace'
: challenge === 'clock'
? 'Clock'
: 'RotateAlign'}</span
>
<span data-uix-meta-pill><span data-uix-meta-key>challenge</span>{challengeName}</span>
<span data-uix-meta-pill><span data-uix-meta-key>keyboard</span>arrows</span>
<span data-uix-meta-pill><span data-uix-meta-key>palette</span>{PALETTE_SCALES.length}</span>
<span data-uix-meta-pill
@ -677,7 +684,7 @@
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · ProofOfHuman + PathTrace</span>
<span>visual · ProofOfHuman + {challengeName}</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
@ -746,10 +753,10 @@
><td>proof-of-human</td></tr
>
{/each}
{#each Object.entries(pathTraceMorfo.texts ?? {}) as [key, ref] (key)}
{#each Object.entries(challengeMorfo.texts ?? {}) as [key, ref] (key)}
<tr
><td class="name">{key}</td><td class="type">{String(ref).split('|')[1] ?? '—'}</td
><td>path-trace</td></tr
><td>{challengeMorfo.kebab}</td></tr
>
{/each}
</tbody>
@ -835,6 +842,42 @@
</tbody>
</table>
</div>
<div data-uix-subsection-head>RotateAlign / Clock (challenge)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr
><td class="name">toleranceDeg</td><td class="type">number</td><td class="default"
>8</td
><td>Angular tolerance (deg) that counts as on-target.</td></tr
>
<tr
><td class="name">settleMs</td><td class="type">number</td><td class="default">450</td
><td>Milliseconds the needle must hold steady on target to lock.</td></tr
>
<tr
><td class="name">stepDeg</td><td class="type">number</td><td class="default">6</td
><td>Degrees the needle rotates per arrow-key press.</td></tr
>
<tr
><td class="name">seed</td><td class="type">number</td><td class="default empty"
>random</td
><td
>Deterministic bearing (tests/demos). The Clock always snaps the target to a whole
hour.</td
></tr
>
<tr
><td class="name">children</td><td class="type"
>Snippet&lt;[{'{ rotating, aligned, angle }'}]&gt;</td
><td class="default empty">needle</td><td
>Skin the rotating needle (compass) / hand (clock).</td
></tr
>
</tbody>
</table>
</div>
</section>
{/if}
@ -844,8 +887,10 @@
<span data-uix-layer-badge="morfo">morfo</span> · orchestrator + challenge
</h2>
<p data-uix-section-desc>
Two morfos: <code>proof-of-human</code> (orchestrator — stages + verdict) and
<code>path-trace</code> (challenge — the trace). Swapping the challenge never touches the orchestrator.
The <code>proof-of-human</code> morfo (orchestrator — stages + verdict) plus one morfo per
challenge (<code>path-trace</code>, <code>rotate-align</code> — the clock reuses the latter).
The challenge tables below follow the selected challenge. Swapping the challenge never touches
the orchestrator.
</p>
<div data-uix-subsection-head>proof-of-human · parts + events</div>
@ -880,7 +925,7 @@
</table>
</div>
<div data-uix-subsection-head>path-trace · parts + events</div>
<div data-uix-subsection-head>{challengeMorfo.kebab} · parts + events</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Archetype</th><th>Role</th></tr></thead>

Loading…
Cancel
Save

Powered by TurnKey Linux.