/** * Geometric dislike (feedback Mode 1) — the SHARED C++/WASM core primitive * (one-core-engine P3; rl-feedback-design §2.1). Drives the probe's read-only * count accessors + the dislikeGeometric driver. * * The core reads the MLP's CURRENT input and the passed HEARD (post-pipeline) * output vector. With NO positives it runs the cold-start fallback and returns * FeedbackAction 15 (GeometricColdStart); with positives it computes the k-NN * centroid push-away target and returns 14 (GeometricPush). */ import { test, expect } from '@playwright/test'; import { loadProbe, settleInputs, countChanged, allWithin } from './helpers'; // Fixed by the WASM build (`nisps/wasm/bindings.cpp`: MLP<32,10,14,18,126>). const N_OUTPUTS = 126; // A heard vector deliberately distinct from any plausible net output. const HEARD = new Array(N_OUTPUTS).fill(0.9); test.describe('geometric dislike (Mode 1) — core-backed', () => { test.beforeEach(async ({ page }) => { await loadProbe(page); }); test('dislike before any likes returns cold-start (15) and stores a negative', async ({ page }) => { const r = await page.evaluate((heard) => { const p = window.__nisps!; p.setFeedbackMode('avoid'); // geometric dislike proto mode maps to core Avoid p.setAvoidStyle(0); // Geometric (default) p.setInputs(0.5, 0.5); const action = p.dislikeGeometric(heard); return { action, counts: p.feedbackCounts() }; }, HEARD); expect(r.action).toBe(15); // GeometricColdStart expect(r.counts.positive).toBe(0); expect(r.counts.negative).toBe(1); }); test('two likes then a dislike pushes (14), changes the disliked output, stays bounded', async ({ page }) => { await page.evaluate(() => { window.__nisps!.setFeedbackMode('avoid'); window.__nisps!.setAvoidStyle(0); }); // Like at two distinct inputs — the core's thumbsUp auto-stores a positive // into the k-NN centroid while in Avoid+Geometric mode (ADR §2.1). await settleInputs(page, 0.2, 0.3); await page.evaluate(() => window.__nisps!.thumbsUp()); await settleInputs(page, 0.8, 0.7); await page.evaluate(() => window.__nisps!.thumbsUp()); // Settle at a third input and capture the heard-≠-output baseline there. const before = await settleInputs(page, 0.5, 0.5); // Sanity: the heard vector we will pass genuinely differs from the outputs. expect(countChanged(before, HEARD, 1e-3)).toBeGreaterThan(0); // Dislike at the settled input with an explicit lr for a visibly-audible push. const res = await page.evaluate((heard) => { const p = window.__nisps!; const action = p.dislikeGeometric(heard, 1.0); return { action, counts: p.feedbackCounts() }; }, HEARD); // Re-settle at the same input; the weights moved, so the output must too. const after = await settleInputs(page, 0.5, 0.5); expect(res.action).toBe(14); // GeometricPush (positives exist → not cold-start) expect(res.counts.positive).toBeGreaterThanOrEqual(2); expect(res.counts.negative).toBe(1); expect(countChanged(before, after, 1e-4)).toBeGreaterThan(0); expect(allWithin(after, 0, 1)).toBe(true); }); });