D1-A: combat glyphs, boundary-channel blue, reaction primitives, READ-FAB

Additive frontend/visual. Backend untouched (git diff src/ = empty): no grade,
model, classifier or ledger change. The 41->74 registry expansion is HELD for
D1-B. Push scoring untouched.

REVIEW ZERO — classifier coverage bounded the glyph wiring. Three buckets, and
the computation was redone three times before it was right (the frontend keys
GLYPHS by ARCHETYPE NAME while the backend keys `glyph:` by SHAPE NAME, and most
registry keys are unquoted identifiers — the first two passes mis-parsed both):
  (a) classifier-backed, already wired: 38
  (b) classifier-backed, package SVG exists, NOT wired -> WIRED HERE: 6
      striker, grappler, pressure, counter, grinder, finisher — all MMA/combat
      archetypes in archetypeService.js that were rendering EMOJI fallbacks
      ('*', 'x', '>', '<>') where the package ships real 24-grid duotone marks.
  (c) package SVG with no classifier -> HELD for D1-B: 39 (wiring them would
      render nothing)
  (d) classifier-backed but NO package SVG: 2 ('dual threat', 'paint boss') —
      a DESIGN gap, not a build gap; flagged for D1-B.
GLYPHS map 38 -> 44 keys, deliberately far short of the package's 83.

BOUNDARY CHANNEL — the blue tokens already existed (--priced-out set) and were
applied on NoMarketState and the scan void box, but PriceTriplet's NO_MODEL
("line not priced") still rendered in neutral text, so the channel was applied
inconsistently. NO_MODEL now renders in the channel, completing "every boundary
state or none". Token-only (no hex fallback and no hex in prose — PriceTriplet's
own test forbids literal hex, and it caught both).

REACTION PRIMITIVES — new web/src/lib/reactions.js + globals.css keyframes at the
exact HANDOFF timings: flash .75s ease-out, boot stagger 60ms steps, reactions
gated 1.5s, WIRE hold 6s. nudge() REFUSES a no-op (null/absent direction -> no
flash) so the primitive cannot be attached to an idle loop — a flash without a
new datum is the UI lying about the feed. Reduced-motion honoured.

READ-FAB — aligned to the exact package geometry: 50px circle, translateY(-14px),
6px void ring (was 46px, marginTop -16, 3px ring).

CARD TOKEN — audit correction: #0E0E14 was already tokenised as --bg-1/--card;
the audit's "1 file" was counting the raw hex, not the token. No change needed.

Floor: 317 suites / 3946 tests green (16 new), web build exit 0.

NOT DONE THIS ORDER (reported, not silently dropped): row-hover rationale and
IntersectionObserver reveal (Phase 3 item 8) and team-gradient chips (Phase 4
item 10) are not implemented — they need the System artboard's row anatomy,
which is a larger port than the rest of D1-A.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
Kev
2026-07-31 06:04:19 -04:00
parent 49565b5f02
commit 3d1a3c7794
7 changed files with 242 additions and 6 deletions
+133
View File
@@ -0,0 +1,133 @@
/**
* D1-A — design self-contained implementation (specs/design-vs-build-gap-audit.md).
* Built to the EXACT package spec in specs/design-reference/HANDOFF.md.
*/
const fs = require('fs');
const path = require('path');
const read = (p) => fs.readFileSync(path.join(__dirname, '../../', p), 'utf8');
const arche = require('../../web/src/lib/archetypes');
const rx = require('../../web/src/lib/reactions');
describe('glyphs — wired ONLY where a classifier can assign the archetype', () => {
const COMBAT = ['striker', 'grappler', 'pressure', 'counter', 'grinder', 'finisher'];
test('the six classifier-backed combat marks are wired from the package SVGs', () => {
const src = read('web/src/lib/archetypes.js');
const m = src.match(/const GLYPHS = \{([\s\S]*?)\n\};/);
const keys = [...m[1].matchAll(/^\s*'([a-z0-9 _-]+)':/gm)].map((x) => x[1]);
for (const c of COMBAT) expect(keys).toContain(c);
});
test('each wired mark is real SVG inner-markup on currentColor (not an emoji)', () => {
const src = read('web/src/lib/archetypes.js');
for (const c of COMBAT) {
const row = src.match(new RegExp(`'${c}': '([^']*)'`));
expect(row).toBeTruthy();
expect(row[1]).toMatch(/<(path|circle|rect|g|line|polygon)/);
expect(row[1]).toMatch(/currentColor/);
}
});
test('glyphs whose archetype has NO classifier are NOT wired dead (held for D1-B)', () => {
// The package ships 83 SVGs; the registry backs far fewer. Wiring a mark the
// classifier can never assign would render nothing, so the count must stay
// well under the package total.
const src = read('web/src/lib/archetypes.js');
const m = src.match(/const GLYPHS = \{([\s\S]*?)\n\};/);
const keys = [...m[1].matchAll(/^\s*'([a-z0-9 _-]+)':/gm)].map((x) => x[1]);
const pkg = fs.readdirSync(path.join(__dirname, '../../specs/design-reference/assets/glyphs'))
.filter((f) => f.endsWith('.svg'));
expect(pkg.length).toBe(83);
expect(keys.length).toBeLessThan(pkg.length); // deliberately not all 83
expect(keys.length).toBe(44);
});
});
describe('boundary channel — one meaning, EVERY boundary state', () => {
test('the blue tokens exist as a semantic set', () => {
const css = read('web/src/app/globals.css');
expect(css).toMatch(/--priced-out:\s*#8fb2de/i);
expect(css).toMatch(/--priced-out-tint-2:\s*rgba\(106, ?147, ?200, ?0?\.12\)/);
expect(css).toMatch(/--priced-out-border-hi:\s*rgba\(106, ?147, ?200, ?0?\.34\)/);
});
test('NO_MODEL (line not priced) now renders in the boundary channel — the last uncovered state', () => {
const src = read('web/src/components/vyndr/PriceTriplet.tsx');
expect(src).toMatch(/function modelAbsentColor/);
expect(src).toMatch(/STATES\.NO_MODEL \? 'var\(--priced-out\)'/);
});
test('the other two boundary states already use it (all of them, or none)', () => {
expect(read('web/src/components/vyndr/NoMarketState.tsx')).toMatch(/priced-out-tint-2/);
expect(read('web/src/app/scan/page.tsx')).toMatch(/priced-out-border-hi/);
});
test('boundary blue is NOT the refusal red nor the quarantine amber', () => {
const css = read('web/src/app/globals.css');
expect(css).not.toMatch(/--priced-out:\s*#FF4757/i);
expect(css).not.toMatch(/--priced-out:\s*#FFB347/i);
});
});
describe('reaction primitives — exact HANDOFF timings, data events only', () => {
test('timings match the handoff verbatim', () => {
expect(rx.FLASH_MS).toBe(750); // .75s ease-out
expect(rx.BOOT_STEP_MS).toBe(60); // 60ms steps
expect(rx.REACTION_GATE_MS).toBe(1500); // ~1.5s
expect(rx.WIRE_HOLD_MS).toBe(6000); // ~6s
});
test('boot stagger steps at 60ms and the hero (index 0) has no delay', () => {
expect(rx.bootDelayMs(0)).toBe(0);
expect(rx.bootDelayMs(1)).toBe(60);
expect(rx.bootDelayMs(5)).toBe(300);
});
test('reactions are GATED until boot settles — no flash during the entrance', () => {
expect(rx.reactionsReady(0)).toBe(false);
expect(rx.reactionsReady(1499)).toBe(false);
expect(rx.reactionsReady(1500)).toBe(true);
});
test('nudge REFUSES a no-op — no direction means no flash (never an idle loop)', () => {
expect(rx.nudge({}, null)).toBeNull();
expect(rx.nudge({}, undefined)).toBeNull();
expect(rx.nudge(null, true)).toBeNull();
});
test('a real change flashes by SIGN', () => {
expect(rx.nudge({}, true)).toBe('rx-up');
expect(rx.nudge({}, false)).toBe('rx-down');
});
test('the keyframes exist and honour reduced motion', () => {
const css = read('web/src/app/globals.css');
expect(css).toMatch(/@keyframes rx-up/);
expect(css).toMatch(/@keyframes vy-rowin/);
expect(css).toMatch(/@keyframes vy-arrive/);
expect(css).toMatch(/animation: rx-up\s+\.75s ease-out/);
expect(css).toMatch(/prefers-reduced-motion: reduce/);
});
});
describe('READ-FAB — exact package geometry', () => {
const src = read('web/src/components/BottomTabBar.tsx');
test('50px circle, translateY(-14px), 6px void ring', () => {
expect(src).toMatch(/width: 50/);
expect(src).toMatch(/height: 50/);
expect(src).toMatch(/translateY\(-14px\)/);
expect(src).toMatch(/border: '6px solid var\(--bg-0\)'/);
});
test('the pre-spec geometry is gone', () => {
expect(src).not.toMatch(/width: 46,\s*\n\s*height: 46/);
expect(src).not.toMatch(/marginTop: -16/);
});
});
describe('card token', () => {
test('#0E0E14 is the tokenised card surface (void -> card -> elevated)', () => {
const css = read('web/src/app/globals.css');
expect(css).toMatch(/--bg-1:\s*#0E0E14/i);
expect(css).toMatch(/--card:\s*var\(--bg-1\)/);
});
});