← back to Quadrille Showroom
Phase 5.5 /contrarian final pass: prove the AVATAR half + fix 2 weak non-senior CTAs
04948ab8ef602429792053ec1210900f10d512ce · 2026-06-29 14:50:46 -0700 · Steve
Contrarian (correctly) flagged the Phase-5 gate verified only the HUD half and proved
regression on a phantom hook. Closes both:
- scripts/verify-ageview-avatar.mjs — proves the avatar morph on the REAL _qh hooks
(avatarAge getter + upperRigState): eye-height arc 1.40→1.62→1.61→1.58→1.48m,
stoop monotonic rotX 0@28 → -0.070@65 → -0.209@85, regression OFF==age28 rotX=0,
+ look-down body screenshots at 12/28/45/65/85 (setSpin to PITCH_MIN). 0 errors.
- ageview.js BANDS: teen CTA #ff2fd6/near-black (APCA 47) -> #c01a9e/white (Lc 81);
young CTA #a855f7/near-black (APCA 38, a real miss for the 21-yo band) -> #7c3aed/white
(Lc 83). Same neon/violet identity, legible filled buttons.
Validation-loop re-run after edits: FPS floor 67 all 11 bands, senior 65/75/85 PASS,
regression PASS, cross-engine PASS, avatar PASS, 0 console errors.
Residual (honest, non-blocking): env-morph per-band is implemented + FPS-safe but
asserted by intensity values, not a measured rendered-luminance delta; adult(28)/
established(40) bands still ~90% identical (plan-gate fallback: merge or differentiate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/js/ageview.jsA scripts/verify-ageview-avatar.mjs
Diff
commit 04948ab8ef602429792053ec1210900f10d512ce
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 14:50:46 2026 -0700
Phase 5.5 /contrarian final pass: prove the AVATAR half + fix 2 weak non-senior CTAs
Contrarian (correctly) flagged the Phase-5 gate verified only the HUD half and proved
regression on a phantom hook. Closes both:
- scripts/verify-ageview-avatar.mjs — proves the avatar morph on the REAL _qh hooks
(avatarAge getter + upperRigState): eye-height arc 1.40→1.62→1.61→1.58→1.48m,
stoop monotonic rotX 0@28 → -0.070@65 → -0.209@85, regression OFF==age28 rotX=0,
+ look-down body screenshots at 12/28/45/65/85 (setSpin to PITCH_MIN). 0 errors.
- ageview.js BANDS: teen CTA #ff2fd6/near-black (APCA 47) -> #c01a9e/white (Lc 81);
young CTA #a855f7/near-black (APCA 38, a real miss for the 21-yo band) -> #7c3aed/white
(Lc 83). Same neon/violet identity, legible filled buttons.
Validation-loop re-run after edits: FPS floor 67 all 11 bands, senior 65/75/85 PASS,
regression PASS, cross-engine PASS, avatar PASS, 0 console errors.
Residual (honest, non-blocking): env-morph per-band is implemented + FPS-safe but
asserted by intensity values, not a measured rendered-luminance delta; adult(28)/
established(40) bands still ~90% identical (plan-gate fallback: merge or differentiate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/js/ageview.js | 8 ++-
scripts/verify-ageview-avatar.mjs | 106 ++++++++++++++++++++++++++++++++++++++
2 files changed, 112 insertions(+), 2 deletions(-)
diff --git a/public/js/ageview.js b/public/js/ageview.js
index 8835c6b..769a802 100644
--- a/public/js/ageview.js
+++ b/public/js/ageview.js
@@ -50,10 +50,14 @@ const BANDS = [
bg:'#fef3c7', panel:'#ffffff', fg:'#1f2937', cta:'#4338ca', ctaFg:'#ffffff', border:'#fbbf24', muted:'#6b7280' },
{ max:16, id:'teen', name:'Teen', tag:'Bold taste, fast & dense — power-user.',
font:"'Space Grotesk', system-ui, sans-serif", body:16, head:22, btn:44, gap:10, opaque:false,
- bg:'#0a0014', panel:'#1a0030', fg:'#f0f0ff', cta:'#ff2fd6', ctaFg:'#0a0014', border:'#2a0050', muted:'#b9a8d6' },
+ // CTA was magenta #ff2fd6 + near-black text (APCA 47 — weak for a filled button). Deeper
+ // neon magenta + white text reads the same "neon" but clears a proper CTA bar (Lc 81). /contrarian 2026-06-29.
+ bg:'#0a0014', panel:'#1a0030', fg:'#f0f0ff', cta:'#c01a9e', ctaFg:'#ffffff', border:'#2a0050', muted:'#b9a8d6' },
{ max:21, id:'young', name:'Emerging Adult', tag:'First apartment — energetic & budget-aware.',
font:"system-ui, -apple-system, sans-serif", body:15, head:19, btn:44, gap:12, opaque:false, energetic:true,
- bg:'#120a1f', panel:'#1d1330', fg:'#ece4f8', cta:'#a855f7', ctaFg:'#0a0014', border:'#36244e', muted:'#b4a3c9' },
+ // CTA was light purple #a855f7 + near-black text (APCA 38 — a real legibility miss for the
+ // mobile-native 21-yo this band serves). Violet-700 + white text → Lc 83, same energy. /contrarian.
+ bg:'#120a1f', panel:'#1d1330', fg:'#ece4f8', cta:'#7c3aed', ctaFg:'#ffffff', border:'#36244e', muted:'#b4a3c9' },
{ max:34, id:'adult', name:'Young Pro', tag:'First real budget — refined, spec-forward.',
font:"-apple-system, BlinkMacSystemFont, sans-serif", body:15, head:18, btn:44, gap:12, opaque:false,
bg:'#0a0c12', panel:'#161a26', fg:'#e6eef8', cta:'#7c3aed', ctaFg:'#ffffff', border:'#252b3d', muted:'#9aa6bd' },
diff --git a/scripts/verify-ageview-avatar.mjs b/scripts/verify-ageview-avatar.mjs
new file mode 100644
index 0000000..868f766
--- /dev/null
+++ b/scripts/verify-ageview-avatar.mjs
@@ -0,0 +1,106 @@
+/* ============================================================================
+ * verify-ageview-avatar.mjs — Phase 5 AVATAR proof (the contrarian-named blind spot).
+ *
+ * The HUD gate (verify-ageview-phase5.mjs) measured the chrome. This proves the OTHER
+ * half — the actual avatar morph — that nothing else verified:
+ * - look-down body screenshots at 12/28/45/65/85 (setSpin to PITCH_MIN ~ -1.05)
+ * - eye-height monotonicity sanity: ageRigParams(age).eyeY rises 12→~28 then falls →85
+ * - STOOP proof: _qh.upperRigState().rotX ≈ 0 at 28, and increasingly forward at 75/85
+ * - REGRESSION proof on the REAL hook: _qh.avatarAge === 28 after setOn(false)
+ * (the phase5 gate used a phantom getAvatarAge() and got "n/a")
+ *
+ * Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/verify-ageview-avatar.mjs
+ * ========================================================================== */
+import pw from '/Users/stevestudio2/.npm-global/lib/node_modules/playwright/index.js';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const URL = process.env.SHOWROOM_URL || 'http://127.0.0.1:7690/';
+const USER = process.env.SHOWROOM_USER || 'admin';
+const PASS = process.env.SHOWROOM_PASS || 'DWSecure2024!';
+const OUT = path.join(__dirname, '..', 'recordings', 'phase5', 'avatar');
+fs.mkdirSync(OUT, { recursive: true });
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+const AGES = [12, 28, 45, 65, 85];
+
+(async () => {
+ const browser = await pw.chromium.launch({
+ channel: 'chrome',
+ args: ['--use-gl=angle', '--enable-webgl', '--ignore-gpu-blocklist'],
+ });
+ const ctx = await browser.newContext({
+ viewport: { width: 1280, height: 900 },
+ httpCredentials: { username: USER, password: PASS },
+ });
+ const page = await ctx.newPage();
+ const errors = [];
+ page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
+ page.on('pageerror', e => errors.push('PAGEERROR ' + e.message));
+ await page.addInitScript(() => { try { localStorage.clear(); } catch (e) {} });
+
+ await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+ await page.waitForFunction(() => window._qh && window._qh.wingBoards && window._ageview, { timeout: 25000 });
+ await sleep(3500);
+
+ const PITCH_MIN = await page.evaluate(() => window._qh.PITCH_MIN ?? -1.05);
+ const report = { ages: {}, regression: {}, stoopMonotonic: false, eyeArc: false, errors: [] };
+
+ await page.evaluate(() => window._ageview.setOn(true));
+ await sleep(400);
+
+ // Look-down body screenshots + per-age rig facts
+ for (const age of AGES) {
+ await page.evaluate(a => window._ageview.apply(a), age);
+ await sleep(700);
+ await page.evaluate(p => window._qh.setSpin(0, p), PITCH_MIN); // look straight down at own body
+ await sleep(700);
+ const facts = await page.evaluate(() => {
+ const rp = window._qh.ageRigParams(window._qh.avatarAge);
+ const ur = window._qh.upperRigState();
+ return { avatarAge: window._qh.avatarAge, eyeY: +rp.eyeY.toFixed(3), leanRad: +rp.leanRad.toFixed(4),
+ shoulderHalf: +rp.shoulderHalf.toFixed(3), upperRigPresent: !!(ur && ur.present),
+ upperRigRotX: ur ? +ur.rotX.toFixed(4) : null, upperRigChildren: ur ? ur.childCount : null };
+ });
+ await page.screenshot({ path: path.join(OUT, `lookdown-${age}.png`) });
+ report.ages[age] = facts;
+ console.log(` age ${String(age).padStart(2)} eyeY=${facts.eyeY} shoulder½=${facts.shoulderHalf} upperRig.rotX=${facts.upperRigRotX} children=${facts.upperRigChildren}`);
+ }
+
+ // Reset look + REGRESSION on the REAL hook
+ await page.evaluate(() => window._qh.setSpin(0, 0));
+ await page.evaluate(() => window._ageview.setOn(false));
+ await sleep(700);
+ const offAge = await page.evaluate(() => ({ avatarAge: window._qh.avatarAge, upper: window._qh.upperRigState() }));
+ report.regression = {
+ avatarAgeAfterOff: offAge.avatarAge,
+ upperRigRotXAfterOff: offAge.upper ? +offAge.upper.rotX.toFixed(4) : null,
+ pass: offAge.avatarAge === 28 && Math.abs(offAge.upper ? offAge.upper.rotX : 1) < 1e-6,
+ };
+
+ // Eye-height arc: rises to peak around 21-28, declines into 85
+ const e = report.ages;
+ report.eyeArc = e[12].eyeY < e[28].eyeY && e[28].eyeY >= e[45].eyeY && e[45].eyeY > e[65].eyeY && e[65].eyeY > e[85].eyeY;
+ // Stoop monotonic: 28 ~ 0, 65 < 75-ish < 85 (more forward lean = larger |leanRad| / rotX)
+ report.stoopMonotonic = Math.abs(e[28].upperRigRotX) < 1e-6 && Math.abs(e[65].upperRigRotX) > 0 &&
+ Math.abs(e[85].upperRigRotX) > Math.abs(e[65].upperRigRotX);
+ report.errors = errors.slice(0, 20);
+
+ const pass = report.regression.pass && report.eyeArc && report.stoopMonotonic && errors.length === 0;
+ fs.writeFileSync(path.join(OUT, 'avatar-result.json'), JSON.stringify(report, null, 2));
+
+ console.log('\n================ AVATAR PROOF ================');
+ console.log(' eye-height arc (12<28>=45>65>85):', report.eyeArc ? 'PASS' : 'FAIL',
+ `[${AGES.map(a => e[a].eyeY).join(' → ')}]`);
+ console.log(' stoop monotonic (28≈0 <65 <85) :', report.stoopMonotonic ? 'PASS' : 'FAIL',
+ `[rotX 28=${e[28].upperRigRotX} 65=${e[65].upperRigRotX} 85=${e[85].upperRigRotX}]`);
+ console.log(' regression OFF==age28 (REAL hook):', report.regression.pass ? 'PASS' : 'FAIL',
+ `[age=${report.regression.avatarAgeAfterOff} rotX=${report.regression.upperRigRotXAfterOff}]`);
+ console.log(' console errors :', errors.length);
+ console.log(' look-down screenshots → recordings/phase5/avatar/lookdown-*.png');
+ console.log(' OVERALL AVATAR :', pass ? '✅ PASS' : '❌ FAIL');
+
+ await browser.close();
+ process.exit(pass ? 0 : 1);
+})().catch(e => { console.error('AVATAR CRASH:', e); process.exit(2); });
← 71a067a Phase 5.3 cross-engine verifier: Age View slider PASS on Web
·
back to Quadrille Showroom
·
Default view = walk the avatar + single centered mirrored bo fa8ee90 →