#!/usr/bin/env node // The authoritative acceptance rule: E subset-of D, singles and chords alike. // The rule itself is pure, so it is executed here (not pattern-matched); the // wiring around it is asserted against the served source. import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; const app = fs.readFileSync(new URL('../static/app.js', import.meta.url), 'utf8'); // ---- run the real helper ------------------------------------------------- const src = app.slice(app.indexOf('function expectedSubsetOfDetected'), app.indexOf('function detectedPitchesFromSalience')); const ctx = vm.createContext({}); vm.runInContext(src + '\nthis.f = expectedSubsetOfDetected;', ctx); const sub = ctx.f; const ACCEPT = true, REJECT = false; const cases = [ // singles ['single exact', [67], [67], ACCEPT], ['single + one extra', [67], [48, 67], ACCEPT], ['single + many extras', [67], [48, 60, 64, 67, 72], ACCEPT], ['single wrong neighbour', [67], [66], REJECT], ['single absent', [67], [48, 60, 64], REJECT], // chords ['chord exact', [67, 71], [67, 71], ACCEPT], ['chord + extras', [67, 71], [48, 52, 55, 67, 71], ACCEPT], ['chord missing one', [67, 71], [48, 52, 55, 67], REJECT], ['chord one wrong', [67, 71], [67, 72], REJECT], // larger chord ['triad + extras', [48, 52, 55], [48, 52, 55, 60, 67], ACCEPT], ['triad missing one', [48, 52, 55], [48, 52], REJECT], // the spec's numeric examples ['spec {4,6} in {1..5}', [4, 6], [1, 2, 3, 4, 5], REJECT], ['spec {4,5} in {1..5}', [4, 5], [1, 2, 3, 4, 5], ACCEPT], // extra-hand notes ['extra hand, single', [67], [48, 52, 55, 67], ACCEPT], ['extra hand, partial chord',[67, 71], [48, 52, 55, 67], REJECT], ]; let pass = 0; for (const [name, E, D, want] of cases) { const got = sub(E, D); assert.equal(got, want, `${name}: expected ${want}, got ${got} E=${JSON.stringify(E)} D=${JSON.stringify(D)}`); pass++; } // degenerate input must never accept assert.equal(sub([], [60]), false, 'empty expected must not accept'); assert.equal(sub([60], []), false, 'empty detected must not accept'); // ---- wiring assertions --------------------------------------------------- const frame = app.slice(app.indexOf('function handleFollowerFrame'), app.indexOf('function traceFollowerFrame')); // compare against the real CALL, not the comment that mentions it assert.ok(frame.indexOf('tryGestureSubsetAdvance()') < frame.indexOf('if (out.advance) deliverFollowerAdvance('), 'the subset rule must run BEFORE the follower proposal (no follower veto)'); const accept = app.slice(app.indexOf('function tryGestureSubsetAdvance'), app.indexOf('function logGesture')); assert.match(accept, /maxJump: 0/, 'exactly one event, never a jump'); assert.match(accept, /attack\.serial !== g\.attackSerial/, 'gesture must be bound to its own token'); assert.match(accept, /attack\.serial > _hostConsumedAttackSerial/, 'token must be unconsumed'); assert.match(accept, /g\.consumed = true/, 'gesture consumed exactly once'); assert.match(accept, /_hostConsumedAttackSerial = attack\.serial/); assert.match(accept, /seek\(state\.tracker\.position/, 'follower re-anchored after direct acceptance'); // singles and chords must NOT diverge in this path assert.doesNotMatch(accept, /length === 1|length > 1|isChord/, 'singles and chords share one rule'); // D must not be built from the score const build = app.slice(app.indexOf('function detectedPitchesFromSalience'), app.indexOf('function beginGesture')); for (const forbidden of ['expectedPitches', 'tracker.score', '_minFreq', 'leadPitchFromEvent']) { assert.ok(!build.includes(forbidden), `D construction must not consult ${forbidden}`); } // observability const log = app.slice(app.indexOf('function logGesture'), app.indexOf('function logGesture') + 1600); for (const field of ['gestureId','detectedPitches','expectedPitches','subsetSatisfied', 'missingExpectedPitches','extraDetectedPitches','accepted', 'trackerBefore','trackerAfter','followerPositionBefore','followerPositionAfter']) { assert.ok(log.includes(field), `gesture record must carry ${field}`); } console.log(`PASS: subset acceptance — ${pass} matrix cases, singles+chords unified, follower cannot veto, D is score-independent`);