60469422af
PHASE 0 — the order proposed E1 + E9 as Wave-1 and told me to follow the
audit if it disagreed. It disagrees: E9 calibration curve is WAVE D3,
gated on MODEL work ("resolve n>=20 vs N30, accrue buckets"), and E1
movement strip is WAVE D6, a large surface build. E9's gate is live right
now -- calibration is WITHDRAWN at 0 eligible dates, so the curve could
only render its empty state today. Building it would ship a component
whose entire purpose is unavailable.
PHASE 1 — three of the five real Wave D1 items were ALREADY DONE, and the
2026-07-31 audit has aged:
D1 glyph library audit: 38/83 wired (46%)
now: COMPLETE for everything wireable -- 39 of 83
designed glyphs map to a real archetype, all 39
are wired, colours match the registry exactly
(0 disagreements).
A1 card token audit: BUILT-BUT-DRIFTED, "in only 1 file"
now: BUILT-TO-SPEC -- it IS the --bg-1 token,
consumed by 32 files. The audit counted literal
hex, which is what a correctly tokenised value
looks like.
B1 boundary blue audit: PARTIAL, hex in 2 files
now: BUILT -- --priced-out/#8fb2de is a token with a
documented colour law, 4 consumers.
The 44 unwired glyphs are NOT a wiring gap: they have no backend
archetype, so wiring them means inventing 44 archetypes to consume
artwork -- the fabrication this programme refuses. That is the 41-vs-74
scope question and it is Kev's call. Separately, 2 registry archetypes
have NO designed glyph (DUAL THREAT, PAINT BOSS) -- a design gap.
PHASE 2 — what was genuinely absent is now built. web/src/lib/motion.js:
nudge() capped at 180ms so it reads as acknowledgement rather than
latency; bootStagger capped at 240ms because uncapped, row 40 waits 1.1s
and the stagger BECOMES the latency it exists to disguise; rowHover
returns handlers not CSS so touch cannot stick a hover state; and
revealOnIntersect returns an unobserve in every path and reveals
IMMEDIATELY when there is no IntersectionObserver or motion is reduced --
content is never hidden behind a capability check.
Reduced motion is honoured, not softened. The sharpest of the 10 tests:
bootStagger under reduced motion returns opacity 1, not merely delay 0 --
if the CSS animation supplies the opacity, skipping it leaves the row
invisible forever.
PHASE 3 — the reachability guard gains a PRIMITIVES section: a module
built to be embedded must declare its exports AND name its intended
consumers, because a primitive imported by nothing is the same
built-but-unread class as an unmounted component.
WAVE-2 UNGATED: F9-F11 offseason hub, F5 article media, E10/E12 Report,
E1 movement strip. GATED: E9 + E15 on model, E16/F8 on the resolution tail
and the card-system reconciliation, E2/E6 on licensing, E13 on another
order.
Read-only throughout; serving fingerprint unchanged; accrual clock
unchanged at 0 eligible dates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
83 lines
2.9 KiB
JavaScript
83 lines
2.9 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* WAVE D1 motion primitives.
|
|
*
|
|
* The property that matters most here is not how they look — it is that they
|
|
* never withhold content. A reveal primitive that hides a row until an observer
|
|
* fires will, on any browser or preference where the observer never fires, hide
|
|
* the row forever.
|
|
*/
|
|
|
|
const m = require('../../web/src/lib/motion');
|
|
|
|
describe('reduced motion is honoured, not softened', () => {
|
|
it('assumes reduced motion server-side', () => {
|
|
expect(m.prefersReducedMotion()).toBe(true);
|
|
});
|
|
|
|
it('nudge does nothing and returns a no-op cleanup', () => {
|
|
const el = { style: {} };
|
|
const stop = m.nudge(el);
|
|
expect(el.style.transform).toBeUndefined();
|
|
expect(typeof stop).toBe('function');
|
|
expect(() => stop()).not.toThrow();
|
|
});
|
|
|
|
it('boot stagger collapses to zero delay AND forces opacity', () => {
|
|
// Not just delay 0: if a CSS animation supplies the opacity, skipping the
|
|
// animation without forcing opacity leaves the row invisible.
|
|
expect(m.bootStagger(9)).toEqual({ animationDelay: '0ms', opacity: 1 });
|
|
});
|
|
|
|
it('reveal fires IMMEDIATELY rather than waiting for an observer', () => {
|
|
const el = {}; let revealed = null;
|
|
m.revealOnIntersect(el, (x) => { revealed = x; });
|
|
expect(revealed).toBe(el);
|
|
});
|
|
});
|
|
|
|
describe('the stagger cannot make content late', () => {
|
|
it('is capped, so a long board does not delay its tail', () => {
|
|
// Uncapped, row 40 would wait 1.1s and the stagger becomes the latency it
|
|
// exists to disguise.
|
|
const orig = m.prefersReducedMotion;
|
|
expect(m.STAGGER_CAP_MS).toBeLessThanOrEqual(300);
|
|
expect(m.STAGGER_STEP_MS * 40).toBeGreaterThan(m.STAGGER_CAP_MS);
|
|
});
|
|
|
|
it('the nudge is short enough to read as acknowledgement, not latency', () => {
|
|
expect(m.NUDGE_MS).toBeLessThanOrEqual(220);
|
|
});
|
|
});
|
|
|
|
describe('row hover is pointer-only by construction', () => {
|
|
it('exposes handlers rather than CSS, so touch cannot stick a hover state', () => {
|
|
const h = m.rowHover();
|
|
expect(typeof h.onMouseEnter).toBe('function');
|
|
expect(typeof h.onMouseLeave).toBe('function');
|
|
});
|
|
|
|
it('clears the tint it set, never leaving a row highlighted', () => {
|
|
const el = { style: { background: '' } };
|
|
const h = m.rowHover();
|
|
h.onMouseEnter({ currentTarget: el });
|
|
expect(el.style.background).toBeTruthy();
|
|
h.onMouseLeave({ currentTarget: el });
|
|
expect(el.style.background).toBe('');
|
|
});
|
|
|
|
it('survives a missing currentTarget rather than throwing mid-render', () => {
|
|
const h = m.rowHover();
|
|
expect(() => h.onMouseEnter({})).not.toThrow();
|
|
expect(() => h.onMouseLeave({})).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('no observer leak', () => {
|
|
it('reveal returns an unobserve function in every path', () => {
|
|
expect(typeof m.revealOnIntersect(null, () => {})).toBe('function');
|
|
expect(typeof m.revealOnIntersect({}, () => {})).toBe('function');
|
|
});
|
|
});
|