From ec591c1328f893da7cdcfccc69ed9233bcb69d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:16:35 -0700 Subject: [PATCH 001/101] Harden empirical session ownership Make microphone startup, active callbacks, cancellation, and page departure use explicit lifecycle ownership without changing acquisition, fingerprint, evidence, campaign, or gate semantics. --- apps/web/src/sessionLifecycle.test.ts | 159 +++++++++++++++++ apps/web/src/sessionLifecycle.ts | 146 +++++++++++++++ apps/web/src/useStrikeSession.ts | 248 +++++++++++++++++--------- docs/SESSION_LIFECYCLE.md | 37 ++++ scripts/browser-smoke.sh | 5 + scripts/session-lifecycle-e2e.mjs | 204 +++++++++++++++++++++ 6 files changed, 719 insertions(+), 80 deletions(-) create mode 100644 apps/web/src/sessionLifecycle.test.ts create mode 100644 apps/web/src/sessionLifecycle.ts create mode 100644 docs/SESSION_LIFECYCLE.md mode change 100644 => 100755 scripts/browser-smoke.sh create mode 100644 scripts/session-lifecycle-e2e.mjs diff --git a/apps/web/src/sessionLifecycle.test.ts b/apps/web/src/sessionLifecycle.test.ts new file mode 100644 index 00000000..832f0b11 --- /dev/null +++ b/apps/web/src/sessionLifecycle.test.ts @@ -0,0 +1,159 @@ +import type { CaptureGraph, OpenedMicrophone } from "@everything-rings/acquisition"; +import { describe, expect, it, vi } from "vitest"; +import { + OpeningSessionResources, + SessionLifecycleGeneration, + ownsSessionResources, +} from "./sessionLifecycle"; + +function fakeMicrophone() { + const track = { stop: vi.fn() }; + const stream = { getTracks: vi.fn(() => [track]) }; + const microphone = { + stream, + track: {}, + settings: {}, + } as unknown as OpenedMicrophone; + return { microphone, track, stream }; +} + +function fakeContext() { + return { + close: vi.fn(async (): Promise => undefined), + } as unknown as AudioContext; +} + +function fakeGraph() { + const graph = { + node: { port: { onmessage: vi.fn() } }, + disconnect: vi.fn(), + } as unknown as CaptureGraph; + return graph; +} + +function fakeWorker() { + return { + onmessage: vi.fn(), + onerror: vi.fn(), + terminate: vi.fn(), + } as unknown as Worker; +} + +describe("SessionLifecycleGeneration", () => { + it("invalidates a cancelled generation and gives the next start a distinct owner", () => { + const lifecycle = new SessionLifecycleGeneration(); + const first = lifecycle.begin(); + + expect(lifecycle.owns(first)).toBe(true); + lifecycle.invalidate(); + expect(lifecycle.owns(first)).toBe(false); + + const second = lifecycle.begin(); + expect(second).not.toBe(first); + expect(lifecycle.owns(second)).toBe(true); + expect(lifecycle.owns(first)).toBe(false); + }); + + it("requires both generation and exact resource identity for callback ownership", () => { + const lifecycle = new SessionLifecycleGeneration(); + const first = lifecycle.begin(); + const firstResources = {}; + const secondResources = {}; + + expect(ownsSessionResources(lifecycle, first, firstResources, firstResources)).toBe(true); + expect(ownsSessionResources(lifecycle, first, secondResources, firstResources)).toBe(false); + + lifecycle.invalidate(); + expect(ownsSessionResources(lifecycle, first, firstResources, firstResources)).toBe(false); + + const second = lifecycle.begin(); + expect(ownsSessionResources(lifecycle, second, secondResources, secondResources)).toBe(true); + expect(ownsSessionResources(lifecycle, second, firstResources, secondResources)).toBe(false); + }); + + it("makes queued callbacks from a disposed generation inert", () => { + const lifecycle = new SessionLifecycleGeneration(); + const generation = lifecycle.begin(); + const resources = {}; + let current: typeof resources | undefined = resources; + const effects: string[] = []; + const callback = (): void => { + if (!ownsSessionResources(lifecycle, generation, current, resources)) return; + effects.push("state-change"); + }; + + lifecycle.invalidate(); + current = undefined; + callback(); + + expect(effects).toEqual([]); + }); +}); + +describe("OpeningSessionResources", () => { + it("stops a microphone that resolves after startup cancellation", () => { + const opening = new OpeningSessionResources(); + const fake = fakeMicrophone(); + + opening.dispose(); + + expect(opening.attachMicrophone(fake.microphone)).toBe(false); + expect(fake.track.stop).toHaveBeenCalledOnce(); + }); + + it("immediately cleans partial microphone/context ownership on cancellation", () => { + const opening = new OpeningSessionResources(); + const fake = fakeMicrophone(); + const context = fakeContext(); + + expect(opening.attachMicrophone(fake.microphone)).toBe(true); + expect(opening.attachContext(context)).toBe(true); + + opening.dispose(); + opening.dispose(); + + expect(fake.track.stop).toHaveBeenCalledOnce(); + expect(context.close).toHaveBeenCalledOnce(); + }); + + it("cleans graph and worker resources that arrive after cancellation", () => { + const opening = new OpeningSessionResources(); + const graph = fakeGraph(); + const worker = fakeWorker(); + + opening.dispose(); + + expect(opening.attachGraph(graph)).toBe(false); + expect(opening.attachWorker(worker)).toBe(false); + expect(graph.disconnect).toHaveBeenCalledOnce(); + expect(worker.terminate).toHaveBeenCalledOnce(); + }); + + it("transfers a complete startup exactly once without premature cleanup", () => { + const opening = new OpeningSessionResources(); + const fake = fakeMicrophone(); + const context = fakeContext(); + const graph = fakeGraph(); + const worker = fakeWorker(); + + expect(opening.attachMicrophone(fake.microphone)).toBe(true); + expect(opening.attachContext(context)).toBe(true); + expect(opening.attachGraph(graph)).toBe(true); + expect(opening.attachWorker(worker)).toBe(true); + + const claimed = opening.claim(); + expect(claimed).toEqual({ + microphone: fake.microphone, + context, + graph, + worker, + }); + expect(opening.claim()).toBeUndefined(); + + opening.dispose(); + expect(fake.track.stop).not.toHaveBeenCalled(); + expect(context.close).not.toHaveBeenCalled(); + expect(graph.disconnect).not.toHaveBeenCalled(); + expect(worker.terminate).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/sessionLifecycle.ts b/apps/web/src/sessionLifecycle.ts new file mode 100644 index 00000000..b6162d0f --- /dev/null +++ b/apps/web/src/sessionLifecycle.ts @@ -0,0 +1,146 @@ +import type { CaptureGraph, OpenedMicrophone } from "@everything-rings/acquisition"; + +function stopMicrophone(microphone: OpenedMicrophone): void { + microphone.stream.getTracks().forEach((track) => { + try { track.stop(); } catch { /* already stopped */ } + }); +} + +function closeContext(context: AudioContext): void { + try { void context.close().catch(() => undefined); } catch { /* already closed */ } +} + +function disconnectGraph(graph: CaptureGraph): void { + try { graph.node.port.onmessage = null; } catch { /* port already unavailable */ } + try { graph.disconnect(); } catch { /* already disconnected */ } +} + +function terminateWorker(worker: Worker): void { + try { worker.onmessage = null; } catch { /* worker already unavailable */ } + try { worker.onerror = null; } catch { /* worker already unavailable */ } + try { worker.terminate(); } catch { /* already terminated */ } +} + +export class SessionLifecycleGeneration { + private generation = 0; + + begin(): number { + this.generation += 1; + return this.generation; + } + + invalidate(): void { + this.generation += 1; + } + + owns(generation: number): boolean { + return generation === this.generation; + } +} + +export function ownsSessionResources( + lifecycle: SessionLifecycleGeneration, + generation: number, + current: T | undefined, + expected: T, +): boolean { + return lifecycle.owns(generation) && current === expected; +} + +export interface ClaimedOpeningSessionResources { + readonly microphone: OpenedMicrophone; + readonly context: AudioContext; + readonly graph: CaptureGraph; + readonly worker: Worker; +} + +export class OpeningSessionResources { + private microphone: OpenedMicrophone | undefined; + private context: AudioContext | undefined; + private graph: CaptureGraph | undefined; + private worker: Worker | undefined; + private disposed = false; + private claimed = false; + + attachMicrophone(microphone: OpenedMicrophone): boolean { + if (this.disposed || this.claimed) { + stopMicrophone(microphone); + return false; + } + if (this.microphone !== undefined) throw new Error("Opening session already owns a microphone"); + this.microphone = microphone; + return true; + } + + attachContext(context: AudioContext): boolean { + if (this.disposed || this.claimed) { + closeContext(context); + return false; + } + if (this.context !== undefined) throw new Error("Opening session already owns an audio context"); + this.context = context; + return true; + } + + attachGraph(graph: CaptureGraph): boolean { + if (this.disposed || this.claimed) { + disconnectGraph(graph); + return false; + } + if (this.graph !== undefined) throw new Error("Opening session already owns a capture graph"); + this.graph = graph; + return true; + } + + attachWorker(worker: Worker): boolean { + if (this.disposed || this.claimed) { + terminateWorker(worker); + return false; + } + if (this.worker !== undefined) throw new Error("Opening session already owns an analysis worker"); + this.worker = worker; + return true; + } + + claim(): ClaimedOpeningSessionResources | undefined { + if (this.disposed || this.claimed) return undefined; + if ( + this.microphone === undefined || + this.context === undefined || + this.graph === undefined || + this.worker === undefined + ) return undefined; + + const claimed = { + microphone: this.microphone, + context: this.context, + graph: this.graph, + worker: this.worker, + }; + this.claimed = true; + this.microphone = undefined; + this.context = undefined; + this.graph = undefined; + this.worker = undefined; + return claimed; + } + + dispose(): void { + if (this.disposed || this.claimed) return; + this.disposed = true; + + const worker = this.worker; + const graph = this.graph; + const microphone = this.microphone; + const context = this.context; + this.worker = undefined; + this.graph = undefined; + this.microphone = undefined; + this.context = undefined; + + if (worker !== undefined) terminateWorker(worker); + if (graph !== undefined) disconnectGraph(graph); + if (microphone !== undefined) stopMicrophone(microphone); + if (context !== undefined) closeContext(context); + } +} diff --git a/apps/web/src/useStrikeSession.ts b/apps/web/src/useStrikeSession.ts index 130be75d..e4eabdcb 100644 --- a/apps/web/src/useStrikeSession.ts +++ b/apps/web/src/useStrikeSession.ts @@ -28,6 +28,11 @@ import { type QualifiedAttempt, type QualifiedAttemptLedger, } from "./qualifiedAttemptLedger"; +import { + OpeningSessionResources, + SessionLifecycleGeneration, + ownsSessionResources, +} from "./sessionLifecycle"; import { microphoneStartFailureCopy, playbackFailureCopy } from "./sessionErrors"; export type StrikeSessionState = @@ -91,6 +96,9 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { const [playbackFailure, setPlaybackFailure] = useState(); const [audioTiming, setAudioTiming] = useState(); const resources = useRef(undefined); + const pendingStartup = useRef(undefined); + const lifecycle = useRef(new SessionLifecycleGeneration()); + const stateRef = useRef("idle"); const requestId = useRef(0); const attemptLedger = useRef(createQualifiedAttemptLedger()); const instrumentPreparation = useRef | undefined>(undefined); @@ -99,6 +107,11 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { const pendingInstrumentEvents = useRef(new Map()); const maximumQualifiedAttempts = options.maximumQualifiedAttempts; + function transitionState(next: StrikeSessionState): void { + stateRef.current = next; + setState(next); + } + function syncLedger(next: QualifiedAttemptLedger): void { attemptLedger.current = next; setAttempts([...next.attempts]); @@ -151,6 +164,7 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { outputChannelCount: [1], }); node.port.onmessage = (event: MessageEvent) => { + if (resources.current !== current) return; if (event.data.type !== "NOTE_STARTED") return; const sentContextTime = pendingInstrumentEvents.current.get(event.data.eventId); if (sentContextTime === undefined) return; @@ -195,69 +209,139 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { } } - function disposeResources(): void { + function disposeSessionResources(current: SessionResources | undefined, updateUi = true): void { instrumentGeneration.current += 1; - const current = resources.current; if (current === undefined) return; - current.playback.stop(); - current.instrument?.port.postMessage({ type: "ALL_NOTES_OFF" }); - current.instrument?.disconnect(); - current.graph.disconnect(); - current.microphone.stream.getTracks().forEach((track) => track.stop()); - current.worker.terminate(); - void current.context.close(); - resources.current = undefined; + try { current.playback.stop(); } catch { /* playback already unavailable */ } + try { current.instrument?.port.postMessage({ type: "ALL_NOTES_OFF" }); } catch { /* port already unavailable */ } + try { + if (current.instrument !== undefined) current.instrument.port.onmessage = null; + } catch { /* port already unavailable */ } + try { current.instrument?.disconnect(); } catch { /* already disconnected */ } + try { current.graph.node.port.onmessage = null; } catch { /* port already unavailable */ } + try { current.worker.onmessage = null; } catch { /* worker already unavailable */ } + try { current.worker.onerror = null; } catch { /* worker already unavailable */ } + try { current.graph.disconnect(); } catch { /* already disconnected */ } + current.microphone.stream.getTracks().forEach((track) => { + try { track.stop(); } catch { /* already stopped */ } + }); + try { current.worker.terminate(); } catch { /* already terminated */ } + try { void current.context.close().catch(() => undefined); } catch { /* already closed */ } + if (resources.current === current) resources.current = undefined; instrumentPreparation.current = undefined; pendingInstrumentEvents.current.clear(); - setInstrumentReady(false); + if (updateUi) setInstrumentReady(false); + } + + function supersedeLifecycle(): number { + const generation = lifecycle.current.begin(); + const opening = pendingStartup.current; + pendingStartup.current = undefined; + opening?.dispose(); + const current = resources.current; + resources.current = undefined; + disposeSessionResources(current); + return generation; + } + + function invalidateLifecycle(updateUi = true): void { + lifecycle.current.invalidate(); + const opening = pendingStartup.current; + pendingStartup.current = undefined; + opening?.dispose(); + const current = resources.current; + resources.current = undefined; + disposeSessionResources(current, updateUi); + } + + function clearPendingStartup(opening: OpeningSessionResources): void { + if (pendingStartup.current === opening) pendingStartup.current = undefined; } async function start(): Promise { - let openingMicrophone: OpenedMicrophone | undefined; - let openingContext: AudioContext | undefined; - let openingGraph: CaptureGraph | undefined; - let openingWorker: Worker | undefined; + const generation = supersedeLifecycle(); + const opening = new OpeningSessionResources(); + pendingStartup.current = opening; + + requestId.current += 1; + const clearedLedger = clearQualifiedAttemptLedger(); + attemptLedger.current = clearedLedger; + setAttempts([]); + setFailureReason(undefined); + setFingerprint(undefined); + setCapture(undefined); + setQuality(undefined); + setInstrumentFailure(undefined); + setPlaybackFailure(undefined); + setAudioTiming(undefined); + transitionState("warming"); try { - disposeResources(); - requestId.current += 1; - const clearedLedger = clearQualifiedAttemptLedger(); - attemptLedger.current = clearedLedger; - setAttempts([]); - setFailureReason(undefined); - setFingerprint(undefined); - setCapture(undefined); - setQuality(undefined); - setInstrumentFailure(undefined); - setPlaybackFailure(undefined); - setAudioTiming(undefined); - setState("warming"); - - openingMicrophone = await openMicrophone(); - openingContext = new AudioContext(); - await openingContext.resume(); - openingGraph = await createCaptureGraph(openingContext, openingMicrophone.stream, captureWorkletUrl); - openingWorker = new Worker(new URL("./analysis.worker.ts", import.meta.url), { type: "module" }); - - const microphone = openingMicrophone; - const context = openingContext; - const graph = openingGraph; - const worker = openingWorker; - resources.current = { - context, - microphone, - graph, - worker, - playback: new SamplePlaybackController(context), + const microphone = await openMicrophone(); + if (!opening.attachMicrophone(microphone) || !lifecycle.current.owns(generation)) { + opening.dispose(); + clearPendingStartup(opening); + return; + } + + const context = new AudioContext(); + if (!opening.attachContext(context) || !lifecycle.current.owns(generation)) { + opening.dispose(); + clearPendingStartup(opening); + return; + } + + await context.resume(); + if (!lifecycle.current.owns(generation)) { + opening.dispose(); + clearPendingStartup(opening); + return; + } + + const graph = await createCaptureGraph(context, microphone.stream, captureWorkletUrl); + if (!opening.attachGraph(graph) || !lifecycle.current.owns(generation)) { + opening.dispose(); + clearPendingStartup(opening); + return; + } + + const worker = new Worker(new URL("./analysis.worker.ts", import.meta.url), { type: "module" }); + if (!opening.attachWorker(worker) || !lifecycle.current.owns(generation)) { + opening.dispose(); + clearPendingStartup(opening); + return; + } + + if (!lifecycle.current.owns(generation)) { + opening.dispose(); + clearPendingStartup(opening); + return; + } + const playback = new SamplePlaybackController(context); + const claimed = opening.claim(); + clearPendingStartup(opening); + if (claimed === undefined) { + opening.dispose(); + throw new Error("Session startup could not claim complete resources"); + } + + const current: SessionResources = { + context: claimed.context, + microphone: claimed.microphone, + graph: claimed.graph, + worker: claimed.worker, + playback, }; - setSettings(microphone.settings); - setAudioTiming(initialAudioTiming(context)); + resources.current = current; + setSettings(current.microphone.settings); + setAudioTiming(initialAudioTiming(current.context)); - graph.node.port.onmessage = (event: MessageEvent) => { + current.graph.node.port.onmessage = (event: MessageEvent) => { + if (!ownsSessionResources(lifecycle.current, generation, resources.current, current)) return; if (event.data.type === "STATE") { - if (event.data.state === "warming") setState("warming"); - if (event.data.state === "armed") setState("armed"); - if (event.data.state === "capturing") setState("capturing"); + if (event.data.state === "warming") transitionState("warming"); + if (event.data.state === "armed") transitionState("armed"); + if (event.data.state === "capturing") transitionState("capturing"); return; } @@ -267,7 +351,7 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { setQuality(assessment.quality); if (!assessment.ok) { setFailureReason(assessment.reason); - setState("failure"); + transitionState("failure"); return; } @@ -282,39 +366,41 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { ); } catch (error) { setFailureReason(error instanceof Error ? error.message : String(error)); - setState("error"); + transitionState("error"); return; } - setState("analyzing"); + transitionState("analyzing"); const samples = nextCapture.samples.slice(); - worker.postMessage( + current.worker.postMessage( { type: "ANALYZE", requestId: id, samples, sampleRate: nextCapture.sampleRate }, [samples.buffer], ); }; - worker.onmessage = (event: MessageEvent) => { + current.worker.onmessage = (event: MessageEvent) => { + if (!ownsSessionResources(lifecycle.current, generation, resources.current, current)) return; if (event.data.type === "FAILURE") { if (!settleCurrentAttempt(event.data.requestId, { status: "failure", reason: event.data.reason })) return; setFingerprint(undefined); setFailureReason(event.data.reason); - setState("failure"); + transitionState("failure"); return; } const nextFingerprint = event.data.fingerprint; if (!settleCurrentAttempt(event.data.requestId, { status: "success", fingerprint: nextFingerprint })) return; setFingerprint(nextFingerprint); - setState("success"); + transitionState("success"); void prepareInstrument(nextFingerprint); }; - worker.onerror = () => { + current.worker.onerror = () => { + if (!ownsSessionResources(lifecycle.current, generation, resources.current, current)) return; const pendingRequest = attemptLedger.current.pending?.requestId; if (pendingRequest === undefined) { setFailureReason("Analysis stopped unexpectedly. Start the session again."); - setState("error"); + transitionState("error"); return; } settleCurrentAttempt( @@ -324,19 +410,17 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { requestId.current += 1; setFingerprint(undefined); setFailureReason("Analysis stopped unexpectedly. Start the session again."); - setState("error"); + transitionState("error"); }; } catch (error) { + const stillCurrent = lifecycle.current.owns(generation); + opening.dispose(); + clearPendingStartup(opening); + if (!stillCurrent) return; retainInterruptedQualifiedAttempt(); requestId.current += 1; - if (resources.current === undefined) { - openingGraph?.disconnect(); - openingWorker?.terminate(); - openingMicrophone?.stream.getTracks().forEach((track) => track.stop()); - if (openingContext !== undefined) void openingContext.close(); - } setFailureReason(microphoneStartFailureCopy(error)); - setState("error"); + transitionState("error"); } } @@ -354,23 +438,23 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { setPlaybackFailure(undefined); const node = resources.current?.graph.node; if (node === undefined) { - setState("idle"); + transitionState("idle"); return; } if (maximumQualifiedAttempts !== undefined && attemptLedger.current.attempts.length >= maximumQualifiedAttempts) { setFailureReason(`Qualified attempt limit of ${maximumQualifiedAttempts} has been reached`); - setState("failure"); + transitionState("failure"); return; } node.port.postMessage({ type: "RESET" }); - setState("warming"); + transitionState("warming"); } function stop(): void { - const alreadyIdle = state === "idle"; + const alreadyIdle = stateRef.current === "idle"; retainInterruptedQualifiedAttempt(); requestId.current += 1; - disposeResources(); + invalidateLifecycle(); setCapture(undefined); setQuality(undefined); setFingerprint(undefined); @@ -384,17 +468,18 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { setSettings(undefined); setAudioTiming(undefined); } - setState("idle"); + transitionState("idle"); } async function play(samples: Float32Array, sampleRate: number): Promise { - const playback = resources.current?.playback; - if (playback === undefined) return; + const current = resources.current; + const playback = current?.playback; + if (current === undefined || playback === undefined) return; setPlaybackFailure(undefined); try { await playback.play(samples, sampleRate); } catch { - setPlaybackFailure(playbackFailureCopy()); + if (resources.current === current) setPlaybackFailure(playbackFailureCopy()); } } @@ -415,10 +500,13 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { setPlaybackFailure(undefined); if (current.context.state === "running") return postNote(current, midiNote, velocity); void current.context.resume().then(() => { + if (resources.current !== current) return; if (current.context.state !== "running" || !postNote(current, midiNote, velocity)) { setPlaybackFailure(playbackFailureCopy()); } - }).catch(() => setPlaybackFailure(playbackFailureCopy())); + }).catch(() => { + if (resources.current === current) setPlaybackFailure(playbackFailureCopy()); + }); return true; } @@ -431,7 +519,7 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { } useEffect(() => { - const handlePageHide = (): void => silenceAudio(); + const handlePageHide = (): void => stop(); const handleVisibilityChange = (): void => { if (document.visibilityState === "hidden") silenceAudio(); }; @@ -440,7 +528,7 @@ export function useStrikeSession(options: StrikeSessionOptions = {}) { return () => { window.removeEventListener("pagehide", handlePageHide); document.removeEventListener("visibilitychange", handleVisibilityChange); - disposeResources(); + invalidateLifecycle(false); }; }, []); diff --git a/docs/SESSION_LIFECYCLE.md b/docs/SESSION_LIFECYCLE.md new file mode 100644 index 00000000..056d22c7 --- /dev/null +++ b/docs/SESSION_LIFECYCLE.md @@ -0,0 +1,37 @@ +# Empirical session lifecycle + +The frozen empirical acquisition path uses explicit ownership for every asynchronous microphone startup. This is a measurement-integrity and privacy boundary. It does not change acquisition-quality thresholds, fingerprint estimation, validation evidence, campaign accounting, or Gate A/B/C scoring semantics. + +## Ownership model + +Every `start()` receives a monotonically increasing lifecycle generation. A later `start()`, explicit `stop()`, page departure, or component unmount invalidates the previous generation before resources are released. + +Capture-worklet, analysis-worker, and realtime timing callbacks may change application state only when both their generation and their exact resource object remain current. Events queued by an older session are therefore inert after that session loses ownership. + +Lifecycle decisions use a synchronous state reference instead of a stale React render closure. + +## Startup resources + +Microphone acquisition, `AudioContext` creation/resume, capture-graph creation, and analysis-worker construction are owned by `OpeningSessionResources` until a complete startup is transferred atomically to the active session. + +Cancellation releases every partial resource already present. A microphone, graph, context, or worker that resolves after cancellation is rejected by the disposed startup scope and cleaned immediately. Cancellation of a superseded startup is silent and must not surface as a microphone-start failure. + +## Active resources + +Teardown is failure-isolated. Event handlers are cleared and each playback, worklet, graph, microphone-track, worker, and audio-context cleanup is attempted independently so one unavailable browser primitive cannot prevent release of the others. + +`pagehide` stops the complete acquisition session, including microphone tracks, worker, graph, and `AudioContext`. Hidden-page visibility changes silence output but do not themselves reinterpret an in-flight measurement; a real page departure owns the destructive stop boundary. + +## Empirical invariant + +A planned physical attempt is accepted only from the currently owned session. Stale capture or analysis callbacks from a cancelled, departed, or superseded session cannot populate a newer session or alter its qualified-attempt ledger. + +The lifecycle layer changes resource ownership only. It does not modify what constitutes an acquisition-quality-passing attempt or any analytical result. + +## Validation + +Deterministic unit tests cover generation invalidation, exact-resource ownership, stale queued callbacks, late microphone cleanup, partial startup cleanup, late graph/worker cleanup, and one-time startup-to-active ownership transfer. + +The empirical browser journey covers unresolved microphone start → cancel → late-stream cleanup → fresh session ownership → cancel → unresolved start → `pagehide` → late-stream cleanup → active session → `pagehide` → microphone teardown, with no stale microphone-failure state. + +The complete empirical install, typecheck, test, build, route, acquisition, permission, mobile, and post-collection review matrix remains required for any freeze that includes this lifecycle implementation. diff --git a/scripts/browser-smoke.sh b/scripts/browser-smoke.sh old mode 100644 new mode 100755 index 47ea19b2..c85dd0be --- a/scripts/browser-smoke.sh +++ b/scripts/browser-smoke.sh @@ -7,6 +7,10 @@ if [[ -z "$BROWSER" ]]; then exit 1 fi +for script in scripts/*-e2e.mjs; do + node --check "$script" +done + PORT="${PORT:-4173}" BASE_URL="http://127.0.0.1:${PORT}" @@ -68,6 +72,7 @@ check_route "/?gate-b=1&campaign=1" "Post-collection blinded reconstruction" "re echo "Browser route smoke tests passed." BROWSER="$BROWSER" BASE_URL="$BASE_URL" node scripts/consumer-e2e.mjs +BROWSER="$BROWSER" BASE_URL="$BASE_URL" node scripts/session-lifecycle-e2e.mjs BROWSER="$BROWSER" BASE_URL="$BASE_URL" node scripts/permission-e2e.mjs BROWSER="$BROWSER" BASE_URL="$BASE_URL" node scripts/mobile-surfaces-e2e.mjs BROWSER="$BROWSER" BASE_URL="$BASE_URL" VITE_SOFTWARE_REVISION="${VITE_SOFTWARE_REVISION:-}" node scripts/post-collection-review-e2e.mjs diff --git a/scripts/session-lifecycle-e2e.mjs b/scripts/session-lifecycle-e2e.mjs new file mode 100644 index 00000000..92da43c3 --- /dev/null +++ b/scripts/session-lifecycle-e2e.mjs @@ -0,0 +1,204 @@ +import { spawn } from "node:child_process"; +import { rmSync } from "node:fs"; + +const browser = process.env.BROWSER; +const baseUrl = process.env.BASE_URL; +if (!browser || !baseUrl) throw new Error("BROWSER and BASE_URL are required"); + +const profileDir = `/tmp/everything-rings-empirical-lifecycle-${process.pid}`; +const debuggingPort = 10300 + (process.pid % 100); +const chrome = spawn(browser, [ + "--headless=new", + "--no-sandbox", + "--disable-gpu", + "--disable-dev-shm-usage", + "--mute-audio", + "--autoplay-policy=no-user-gesture-required", + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", + `--remote-debugging-port=${debuggingPort}`, + `--user-data-dir=${profileDir}`, + `${baseUrl}/`, +], { stdio: ["ignore", "ignore", "pipe"] }); +const chromeExit = new Promise((resolve) => chrome.once("exit", resolve)); + +let stderr = ""; +chrome.stderr.on("data", (chunk) => { stderr += String(chunk); }); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function browserTarget() { + for (let attempt = 0; attempt < 80; attempt += 1) { + try { + const response = await fetch(`http://127.0.0.1:${debuggingPort}/json`); + const targets = await response.json(); + const target = targets.find((candidate) => candidate.type === "page" && candidate.url.startsWith(baseUrl)); + if (target?.webSocketDebuggerUrl) return target; + } catch { + // Browser is still starting. + } + await sleep(100); + } + throw new Error(`Could not connect to empirical lifecycle browser target. ${stderr}`); +} + +const target = await browserTarget(); +const socket = new WebSocket(target.webSocketDebuggerUrl); +const pending = new Map(); +let nextId = 1; + +await new Promise((resolve, reject) => { + socket.addEventListener("open", resolve, { once: true }); + socket.addEventListener("error", reject, { once: true }); +}); + +socket.addEventListener("message", (event) => { + const message = JSON.parse(String(event.data)); + if (typeof message.id !== "number") return; + const handler = pending.get(message.id); + if (!handler) return; + pending.delete(message.id); + if (message.error) handler.reject(new Error(message.error.message)); + else handler.resolve(message.result); +}); + +function command(method, params = {}) { + const id = nextId++; + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + socket.send(JSON.stringify({ id, method, params })); + }); +} + +async function evaluate(expression, userGesture = false) { + const result = await command("Runtime.evaluate", { + expression, + returnByValue: true, + awaitPromise: true, + userGesture, + }); + if (result.exceptionDetails) { + throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "Browser evaluation failed"); + } + return result.result?.value; +} + +async function waitFor(expression, label, timeoutMs = 10_000) { + const started = Date.now(); + while (Date.now() - started < timeoutMs) { + if (await evaluate(expression)) return; + await sleep(100); + } + throw new Error(`Timed out waiting for ${label}. URL: ${await evaluate("location.href")}. Body: ${await evaluate("document.body?.innerText ?? ''")}`); +} + +async function clickButton(label) { + const clicked = await evaluate(`(() => { + const button = Array.from(document.querySelectorAll("button")).find((candidate) => candidate.textContent?.trim() === ${JSON.stringify(label)}); + if (!button) return false; + button.click(); + return true; + })()`, true); + if (!clicked) throw new Error(`Could not click ${label}`); +} + +async function assertLanding(label) { + await waitFor(`document.body?.innerText.includes("START LISTENING") ?? false`, label); + const body = await evaluate("document.body?.innerText ?? ''"); + if (body.includes("microphone could not be started")) { + throw new Error(`${label} surfaced cancellation as a microphone failure`); + } +} + +async function dispatchPageHide() { + await evaluate(`(() => { + window.dispatchEvent(new Event("pagehide")); + return true; + })()`); +} + +try { + await command("Runtime.enable"); + await command("Page.enable"); + await command("Page.addScriptToEvaluateOnNewDocument", { + source: `(() => { + const mediaDevices = navigator.mediaDevices; + window.__everythingRingsLifecycle = { calls: 0, streams: [], gate: null, release: null }; + window.__everythingRingsHoldNextMic = () => { + let release; + const promise = new Promise((resolve) => { release = resolve; }); + window.__everythingRingsLifecycle.gate = promise; + window.__everythingRingsLifecycle.release = release; + }; + window.__everythingRingsReleaseMic = () => { + const release = window.__everythingRingsLifecycle.release; + window.__everythingRingsLifecycle.release = null; + if (typeof release === "function") release(); + }; + if (mediaDevices && typeof mediaDevices.getUserMedia === "function") { + const originalGetUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); + mediaDevices.getUserMedia = async (...args) => { + window.__everythingRingsLifecycle.calls += 1; + const gate = window.__everythingRingsLifecycle.gate; + window.__everythingRingsLifecycle.gate = null; + if (gate) await gate; + const stream = await originalGetUserMedia(...args); + window.__everythingRingsLifecycle.streams.push(stream); + return stream; + }; + } + })();`, + }); + + await command("Page.reload", { ignoreCache: true }); + await assertLanding("initial landing"); + + await evaluate("window.__everythingRingsHoldNextMic()"); + await clickButton("START LISTENING"); + await waitFor(`document.body?.innerText.includes("Listening to the room") ?? false`, "pending microphone startup"); + await clickButton("CANCEL"); + await assertLanding("landing after cancelling unresolved microphone"); + await evaluate("window.__everythingRingsReleaseMic()"); + await waitFor(`window.__everythingRingsLifecycle.streams.length === 1`, "late first microphone resolution"); + await waitFor(`window.__everythingRingsLifecycle.streams[0].getTracks().every((track) => track.readyState === "ended")`, "late first microphone cleanup"); + await assertLanding("landing after late first microphone cleanup"); + + await clickButton("START LISTENING"); + await waitFor(`window.__everythingRingsLifecycle.calls === 2`, "second microphone request"); + await waitFor(`window.__everythingRingsLifecycle.streams.length === 2`, "second microphone stream"); + await waitFor(`window.__everythingRingsLifecycle.streams[1].getTracks().some((track) => track.readyState === "live")`, "second session owns a live microphone"); + await clickButton("CANCEL"); + await assertLanding("landing after second-session cancel"); + await waitFor(`window.__everythingRingsLifecycle.streams[1].getTracks().every((track) => track.readyState === "ended")`, "second-session cleanup"); + + await evaluate("window.__everythingRingsHoldNextMic()"); + await clickButton("START LISTENING"); + await waitFor(`window.__everythingRingsLifecycle.calls === 3`, "third microphone request"); + await dispatchPageHide(); + await assertLanding("landing after pagehide during unresolved startup"); + await evaluate("window.__everythingRingsReleaseMic()"); + await waitFor(`window.__everythingRingsLifecycle.streams.length === 3`, "late pagehide microphone resolution"); + await waitFor(`window.__everythingRingsLifecycle.streams[2].getTracks().every((track) => track.readyState === "ended")`, "late pagehide microphone cleanup"); + await assertLanding("landing after late pagehide cleanup"); + + await clickButton("START LISTENING"); + await waitFor(`window.__everythingRingsLifecycle.calls === 4`, "fourth microphone request"); + await waitFor(`window.__everythingRingsLifecycle.streams.length === 4`, "fourth microphone stream"); + await waitFor(`window.__everythingRingsLifecycle.streams[3].getTracks().some((track) => track.readyState === "live")`, "fourth session owns a live microphone"); + await dispatchPageHide(); + await assertLanding("landing after active pagehide"); + await waitFor(`window.__everythingRingsLifecycle.streams[3].getTracks().every((track) => track.readyState === "ended")`, "active pagehide microphone cleanup"); + + const body = await evaluate("document.body?.innerText ?? ''"); + if (body.includes("microphone could not be started")) throw new Error("Lifecycle cancellation surfaced as microphone startup failure"); + + console.log("Empirical session lifecycle E2E passed: unresolved start → cancel → late cleanup → new owner → cancel → pagehide teardown → zero stale microphone state."); +} finally { + try { socket.close(); } catch { /* already closed */ } + chrome.kill("SIGTERM"); + await Promise.race([chromeExit, sleep(2_000)]); + try { + rmSync(profileDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } catch { + // Runner-local browser profile cleanup is not an empirical assertion. + } +} From 18b8103bf30cddacb2adef877b170b485ec61a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:07:07 -0700 Subject: [PATCH 002/101] Add canonical Gate B and Gate C review plans --- packages/validation/src/review-plans.ts | 252 ++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 packages/validation/src/review-plans.ts diff --git a/packages/validation/src/review-plans.ts b/packages/validation/src/review-plans.ts new file mode 100644 index 00000000..65a81d64 --- /dev/null +++ b/packages/validation/src/review-plans.ts @@ -0,0 +1,252 @@ +import type { + GateAReleaseVerdict, + GateBReleaseVerdict, + GateBReview, + GateCReleaseVerdict, + GateCReview, + ReviewTarget, +} from "./types"; +import { evaluateGateBRelease, evaluateGateCRelease } from "./evaluate"; + +export interface PlannedReviewTarget extends ReviewTarget { + readonly specimenId: string; + readonly objectLabel: string; +} + +export interface GateBPlanV1 { + readonly schemaVersion: 1; + readonly planContractVersion: "gate-b-plan-1"; + readonly gateARevision: string; + readonly reviewerIds: readonly [string, string]; + readonly targets: readonly PlannedReviewTarget[]; +} + +export interface GateCDevicePlan { + readonly deviceId: string; + readonly deviceClass: "desktop" | "mobile" | "tablet" | "other"; +} + +export interface GateCPlanV1 { + readonly schemaVersion: 1; + readonly planContractVersion: "gate-c-plan-1"; + readonly reviewerIds: readonly [string, string]; + readonly devices: readonly [GateCDevicePlan, GateCDevicePlan]; + readonly targets: readonly PlannedReviewTarget[]; +} + +export interface CanonicalGateBResult { + readonly passed: boolean; + readonly expectedJudgmentCount: 10; + readonly observedJudgmentCount: number; + readonly reasons: readonly string[]; + readonly verdict: GateBReleaseVerdict; +} + +export interface CanonicalGateCResult { + readonly passed: boolean; + readonly expectedJudgmentCount: 16; + readonly observedJudgmentCount: number; + readonly reasons: readonly string[]; + readonly verdict: GateCReleaseVerdict; +} + +function normalize(value: string): string { + return value.trim().toLocaleLowerCase("en-US"); +} + +function targetKey(target: ReviewTarget): string { + return `${target.sessionId}\u0000${target.attemptId}`; +} + +async function sha256Text(value: string): Promise { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +async function rankTargets( + domain: string, + targets: readonly PlannedReviewTarget[], +): Promise { + const ranked = await Promise.all(targets.map(async (target) => ({ + target, + digest: await sha256Text(`${domain}|${normalize(target.specimenId)}|${target.sessionId}|${target.attemptId}`), + }))); + ranked.sort((left, right) => left.digest.localeCompare(right.digest)); + return ranked.map(({ target }) => target); +} + +function gateATargets(gateA: GateAReleaseVerdict): PlannedReviewTarget[] { + return gateA.sessions + .filter((session) => session.passed && session.reviewAttemptId !== null) + .map((session) => ({ + specimenId: session.specimenId, + objectLabel: session.objectLabel, + sessionId: session.sessionId, + attemptId: session.reviewAttemptId as number, + })); +} + +function uniqueSpecimenTargets(targets: readonly PlannedReviewTarget[]): PlannedReviewTarget[] { + const bySpecimen = new Map(); + for (const target of targets) { + const key = normalize(target.specimenId); + if (!bySpecimen.has(key)) bySpecimen.set(key, target); + } + return [...bySpecimen.values()]; +} + +function assertFixedPair(values: readonly string[], field: string): readonly [string, string] { + if (values.length !== 2) throw new Error(`${field} must contain exactly two values`); + const normalized = values.map(normalize); + if (normalized.some((value) => value.length === 0)) throw new Error(`${field} values must be non-empty`); + if (normalized[0] === normalized[1]) throw new Error(`${field} values must be distinct after normalization`); + return [values[0]!.trim(), values[1]!.trim()]; +} + +export async function createGateBPlan( + gateA: GateAReleaseVerdict, + reviewerIds: readonly string[], +): Promise { + if (!gateA.passed || gateA.softwareRevision === null) throw new Error("Gate A must pass before Gate B planning"); + const reviewers = assertFixedPair(reviewerIds, "reviewerIds"); + const all = uniqueSpecimenTargets(await rankTargets("gate-b-target-v1", gateATargets(gateA))); + const byMaterial = new Map(gateA.sessions + .filter((session) => session.passed) + .map((session) => [normalize(session.specimenId), session.material] as const)); + + const selected: PlannedReviewTarget[] = []; + for (const material of ["metal", "glass", "ceramic"] as const) { + const target = all.find((candidate) => byMaterial.get(normalize(candidate.specimenId)) === material); + if (target === undefined) throw new Error(`Gate B planning requires a passing ${material} target`); + selected.push(target); + } + const selectedKeys = new Set(selected.map((target) => normalize(target.specimenId))); + selected.push(...all.filter((target) => !selectedKeys.has(normalize(target.specimenId))).slice(0, 2)); + if (selected.length !== 5) throw new Error("Gate B planning requires exactly five passing specimens"); + + return { + schemaVersion: 1, + planContractVersion: "gate-b-plan-1", + gateARevision: gateA.softwareRevision, + reviewerIds: reviewers, + targets: selected, + }; +} + +export async function createGateCPlan( + gateB: GateBReleaseVerdict, + reviewerIds: readonly string[], + devices: readonly GateCDevicePlan[], +): Promise { + if (!gateB.passed) throw new Error("Gate B must pass before Gate C planning"); + const reviewers = assertFixedPair(reviewerIds, "reviewerIds"); + if (devices.length !== 2) throw new Error("Gate C planning requires exactly two devices"); + const deviceIds = devices.map((device) => normalize(device.deviceId)); + if (deviceIds.some((value) => value.length === 0) || deviceIds[0] === deviceIds[1]) { + throw new Error("Gate C devices must have two distinct non-empty IDs"); + } + if (!devices.some((device) => device.deviceClass === "mobile")) { + throw new Error("Gate C device plan must include a mobile device"); + } + const passingTargets = gateB.objects + .filter((object) => object.passed && object.selectedTarget !== null) + .map((object) => ({ + specimenId: object.specimenId, + objectLabel: object.objectLabel, + sessionId: object.selectedTarget!.sessionId, + attemptId: object.selectedTarget!.attemptId, + })); + const ranked = await rankTargets("gate-c-target-v1", uniqueSpecimenTargets(passingTargets)); + const targets = ranked.slice(0, 4); + if (targets.length !== 4) throw new Error("Gate C planning requires four passing Gate B specimens"); + return { + schemaVersion: 1, + planContractVersion: "gate-c-plan-1", + reviewerIds: reviewers, + devices: [devices[0]!, devices[1]!], + targets, + }; +} + +export function evaluateCanonicalGateB( + plan: GateBPlanV1, + gateA: GateAReleaseVerdict, + reviews: readonly GateBReview[], +): CanonicalGateBResult { + const reasons: string[] = []; + if (!gateA.passed) reasons.push("Gate A has not passed"); + if (gateA.softwareRevision !== plan.gateARevision) reasons.push("Gate B plan revision does not match Gate A"); + if (plan.targets.length !== 5) reasons.push("Gate B plan must contain exactly five targets"); + const expectedReviewers = new Set(plan.reviewerIds.map(normalize)); + const expectedTargets = new Map(plan.targets.map((target) => [targetKey(target), target] as const)); + const canonical = reviews.filter((review) => expectedTargets.has(targetKey(review))); + + for (const review of canonical) { + if (!expectedReviewers.has(normalize(review.reviewerId))) reasons.push(`unexpected Gate B reviewer ${review.reviewerId}`); + if (!review.blinded) reasons.push(`Gate B review ${review.reviewId} is not blinded`); + } + for (const target of plan.targets) { + const targetReviews = canonical.filter((review) => targetKey(review) === targetKey(target)); + const reviewerSet = new Set(targetReviews.map((review) => normalize(review.reviewerId))); + if (targetReviews.length !== 2 || reviewerSet.size !== 2 || [...expectedReviewers].some((id) => !reviewerSet.has(id))) { + reasons.push(`Gate B target ${target.specimenId} does not have the exact fixed two-reviewer panel`); + } + } + if (canonical.length !== 10) reasons.push("Gate B requires exactly ten canonical judgments"); + + const verdict = evaluateGateBRelease(gateA, canonical); + if (!verdict.passed) reasons.push(...verdict.reasons); + return { + passed: reasons.length === 0, + expectedJudgmentCount: 10, + observedJudgmentCount: canonical.length, + reasons: [...new Set(reasons)], + verdict, + }; +} + +export function evaluateCanonicalGateC( + plan: GateCPlanV1, + gateB: GateBReleaseVerdict, + reviews: readonly GateCReview[], +): CanonicalGateCResult { + const reasons: string[] = []; + if (!gateB.passed) reasons.push("Gate B has not passed"); + if (plan.targets.length !== 4) reasons.push("Gate C plan must contain exactly four targets"); + const expectedReviewers = new Set(plan.reviewerIds.map(normalize)); + const expectedDevices = new Map(plan.devices.map((device) => [normalize(device.deviceId), device] as const)); + const expectedTargets = new Map(plan.targets.map((target) => [targetKey(target), target] as const)); + const canonical = reviews.filter((review) => expectedTargets.has(targetKey(review))); + + for (const review of canonical) { + if (!expectedReviewers.has(normalize(review.reviewerId))) reasons.push(`unexpected Gate C reviewer ${review.reviewerId}`); + const device = expectedDevices.get(normalize(review.deviceId)); + if (device === undefined) reasons.push(`unexpected Gate C device ${review.deviceId}`); + else if (device.deviceClass !== review.deviceClass) reasons.push(`Gate C device class mismatch for ${review.deviceId}`); + } + + for (const target of plan.targets) { + const cells = new Set(canonical + .filter((review) => targetKey(review) === targetKey(target)) + .map((review) => `${normalize(review.reviewerId)}\u0000${normalize(review.deviceId)}`)); + for (const reviewerId of expectedReviewers) { + for (const deviceId of expectedDevices.keys()) { + if (!cells.has(`${reviewerId}\u0000${deviceId}`)) { + reasons.push(`Gate C target ${target.specimenId} is missing reviewer/device cell ${reviewerId}/${deviceId}`); + } + } + } + if (cells.size !== 4) reasons.push(`Gate C target ${target.specimenId} must contain exactly four canonical cells`); + } + if (canonical.length !== 16) reasons.push("Gate C requires exactly sixteen canonical judgments"); + + const verdict = evaluateGateCRelease(gateB, canonical); + if (!verdict.passed) reasons.push(...verdict.reasons); + return { + passed: reasons.length === 0, + expectedJudgmentCount: 16, + observedJudgmentCount: canonical.length, + reasons: [...new Set(reasons)], + verdict, + }; +} From 0df1871d43eb32765425e4763f321ee5d580ebe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:07:20 -0700 Subject: [PATCH 003/101] Export canonical review planning API --- packages/validation/src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index f2b23b73..3b497b1c 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -23,6 +23,18 @@ export { type EmpiricalCampaignSpecimenStatus, type EmpiricalCampaignV1, } from "./campaign"; +export { + createGateBPlan, + createGateCPlan, + evaluateCanonicalGateB, + evaluateCanonicalGateC, + type CanonicalGateBResult, + type CanonicalGateCResult, + type GateBPlanV1, + type GateCDevicePlan, + type GateCPlanV1, + type PlannedReviewTarget, +} from "./review-plans"; export { deriveEvidenceRecurrence, deriveMedianModalDriftCents, From 036456ed9ee123c3c205f1ac5c8fd987f39a2702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:07:45 -0700 Subject: [PATCH 004/101] Test exact canonical Gate B and Gate C matrices --- packages/validation/test/review-plans.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 packages/validation/test/review-plans.test.ts diff --git a/packages/validation/test/review-plans.test.ts b/packages/validation/test/review-plans.test.ts new file mode 100644 index 00000000..ef3db36a --- /dev/null +++ b/packages/validation/test/review-plans.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from "vitest"; +import { + createGateBPlan, + createGateCPlan, + evaluateCanonicalGateB, + evaluateCanonicalGateC, + evaluateGateARelease, + evaluateGateBRelease, + type GateBReview, + type GateCReview, +} from "../src"; +import { evidence } from "./helpers"; + +function sixObjects() { + return [ + evidence("metal-a", "metal", { specimenId: "metal-a" }), + evidence("metal-b", "metal", { specimenId: "metal-b" }), + evidence("glass-a", "glass", { specimenId: "glass-a" }), + evidence("glass-b", "glass", { specimenId: "glass-b" }), + evidence("ceramic-a", "ceramic", { specimenId: "ceramic-a" }), + evidence("ceramic-b", "ceramic", { specimenId: "ceramic-b" }), + ]; +} + +function gateBReviews(plan: Awaited>): GateBReview[] { + return plan.targets.flatMap((target) => plan.reviewerIds.map((reviewerId, index) => ({ + reviewId: `b-${target.specimenId}-${index}`, + reviewerId, + objectLabel: target.objectLabel, + sessionId: target.sessionId, + attemptId: target.attemptId, + blinded: true, + presentationOrder: index % 2 === 0 ? "original-model" as const : "model-original" as const, + identity: 4 as const, + brightness: 4 as const, + decayCharacter: 4 as const, + artifactSeverity: 2 as const, + }))); +} + +function gateCReviews(plan: Awaited>): GateCReview[] { + return plan.targets.flatMap((target) => plan.reviewerIds.flatMap((reviewerId) => plan.devices.map((device) => ({ + reviewId: `c-${target.specimenId}-${reviewerId}-${device.deviceId}`, + reviewerId, + objectLabel: target.objectLabel, + sessionId: target.sessionId, + attemptId: target.attemptId, + deviceId: device.deviceId, + deviceClass: device.deviceClass, + identityAcrossRange: 4 as const, + timbreContinuity: 4 as const, + usefulSemitoneSpan: 12, + latencyAcceptable: true, + })))); +} + +describe("canonical post-collection plans", () => { + it("selects exactly five Gate B targets including metal, glass, and ceramic", async () => { + const gateA = evaluateGateARelease(sixObjects()); + expect(gateA.passed).toBe(true); + const plan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + expect(plan.targets).toHaveLength(5); + const materialBySpecimen = new Map(gateA.sessions.map((session) => [session.specimenId, session.material])); + const materials = new Set(plan.targets.map((target) => materialBySpecimen.get(target.specimenId))); + expect(materials.has("metal")).toBe(true); + expect(materials.has("glass")).toBe(true); + expect(materials.has("ceramic")).toBe(true); + }); + + it("rejects Gate B when one fixed-panel judgment is missing", async () => { + const gateA = evaluateGateARelease(sixObjects()); + const plan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + const reviews = gateBReviews(plan); + expect(evaluateCanonicalGateB(plan, gateA, reviews).passed).toBe(true); + const missing = reviews.slice(0, -1); + const result = evaluateCanonicalGateB(plan, gateA, missing); + expect(result.passed).toBe(false); + expect(result.observedJudgmentCount).toBe(9); + }); + + it("requires the full four-target by two-reviewer by two-device Gate C matrix", async () => { + const gateA = evaluateGateARelease(sixObjects()); + const gateBPlan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + const gateBReviewsAll = gateBReviews(gateBPlan); + const canonicalB = evaluateCanonicalGateB(gateBPlan, gateA, gateBReviewsAll); + expect(canonicalB.passed).toBe(true); + + const gateCPlan = await createGateCPlan(canonicalB.verdict, ["reviewer-c", "reviewer-d"], [ + { deviceId: "desktop-1", deviceClass: "desktop" }, + { deviceId: "phone-1", deviceClass: "mobile" }, + ]); + const reviews = gateCReviews(gateCPlan); + expect(reviews).toHaveLength(16); + expect(evaluateCanonicalGateC(gateCPlan, canonicalB.verdict, reviews).passed).toBe(true); + + const missingCell = reviews.filter((_, index) => index !== 0); + const result = evaluateCanonicalGateC(gateCPlan, canonicalB.verdict, missingCell); + expect(result.passed).toBe(false); + expect(result.observedJudgmentCount).toBe(15); + }); + + it("rejects a Gate C plan without a mobile device", async () => { + const gateA = evaluateGateARelease(sixObjects()); + const bPlan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + const bVerdict = evaluateGateBRelease(gateA, gateBReviews(bPlan)); + await expect(createGateCPlan(bVerdict, ["reviewer-c", "reviewer-d"], [ + { deviceId: "desktop-1", deviceClass: "desktop" }, + { deviceId: "desktop-2", deviceClass: "desktop" }, + ])).rejects.toThrow(/mobile/); + }); +}); From 39cc394d6e2df6250efe37844c6ab1605c5c00fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:08:41 -0700 Subject: [PATCH 005/101] Add candidate register and deterministic campaign ordering --- packages/validation/src/precommitment.ts | 157 +++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 packages/validation/src/precommitment.ts diff --git a/packages/validation/src/precommitment.ts b/packages/validation/src/precommitment.ts new file mode 100644 index 00000000..ea1cb7e6 --- /dev/null +++ b/packages/validation/src/precommitment.ts @@ -0,0 +1,157 @@ +import type { EmpiricalCampaignV1 } from "./campaign"; + +export interface CandidateRegisterEntryV1 { + readonly inventoryId: string; + readonly label: string; + readonly objectFamily: string; + readonly material: string; + readonly safetyStatus: "eligible" | "excluded"; + readonly supportDescription: string; + readonly strikeLocationMarkable: boolean; + readonly eligibleSlotIds: readonly string[]; + readonly exclusionReason: string | null; +} + +export interface CandidateRegisterV1 { + readonly schemaVersion: 1; + readonly registerContractVersion: "candidate-register-1"; + readonly createdAt: string; + readonly entries: readonly CandidateRegisterEntryV1[]; + readonly acousticAuditionPerformed: false; +} + +export interface CampaignCollectionOrderEntryV1 { + readonly ordinal: number; + readonly specimenId: string; + readonly cohort: "release-core" | "challenge"; + readonly orderingDigest: string; +} + +export interface CampaignCollectionOrderV1 { + readonly schemaVersion: 1; + readonly orderContractVersion: "campaign-order-1"; + readonly campaignSignature: string; + readonly entries: readonly CampaignCollectionOrderEntryV1[]; +} + +function normalize(value: string): string { + return value.trim().toLocaleLowerCase("en-US"); +} + +function nonempty(value: string): boolean { + return value.trim().length > 0; +} + +export function validateCandidateRegister(register: CandidateRegisterV1): readonly string[] { + const reasons: string[] = []; + if (register.schemaVersion !== 1) reasons.push("candidate register schemaVersion must be 1"); + if (register.registerContractVersion !== "candidate-register-1") reasons.push("candidate register contract is invalid"); + if (!Number.isFinite(Date.parse(register.createdAt))) reasons.push("candidate register createdAt is invalid"); + if (register.acousticAuditionPerformed !== false) reasons.push("candidate register must be frozen before acoustic audition"); + if (register.entries.length === 0) reasons.push("candidate register must contain at least one entry"); + + const seen = new Set(); + for (const entry of register.entries) { + const id = normalize(entry.inventoryId); + if (!nonempty(id)) reasons.push("candidate inventoryId is missing"); + else if (seen.has(id)) reasons.push(`duplicate candidate inventoryId ${entry.inventoryId}`); + else seen.add(id); + if (!nonempty(entry.label)) reasons.push(`candidate ${entry.inventoryId} label is missing`); + if (!nonempty(entry.objectFamily)) reasons.push(`candidate ${entry.inventoryId} objectFamily is missing`); + if (!nonempty(entry.material)) reasons.push(`candidate ${entry.inventoryId} material is missing`); + if (!nonempty(entry.supportDescription)) reasons.push(`candidate ${entry.inventoryId} support description is missing`); + if (entry.safetyStatus === "excluded" && !nonempty(entry.exclusionReason ?? "")) { + reasons.push(`excluded candidate ${entry.inventoryId} requires an exclusion reason`); + } + if (entry.safetyStatus === "eligible" && entry.exclusionReason !== null) { + reasons.push(`eligible candidate ${entry.inventoryId} cannot have an exclusion reason`); + } + if (entry.safetyStatus === "eligible" && entry.eligibleSlotIds.length === 0) { + reasons.push(`eligible candidate ${entry.inventoryId} requires at least one eligible slot`); + } + } + return [...new Set(reasons)]; +} + +export function validateCampaignSelectionAgainstRegister( + campaign: EmpiricalCampaignV1, + register: CandidateRegisterV1, +): readonly string[] { + const reasons = [...validateCandidateRegister(register)]; + const byInventory = new Map(register.entries.map((entry) => [normalize(entry.inventoryId), entry] as const)); + const selectedInventory = new Set(); + const familyByCoreMaterial = new Map>(); + + for (const specimen of campaign.specimens) { + const separator = specimen.specimenId.indexOf("--"); + if (separator < 0) { + reasons.push(`specimen ${specimen.specimenId} does not bind a slot to an inventory ID`); + continue; + } + const slotId = specimen.specimenId.slice(0, separator); + const inventoryId = normalize(specimen.specimenId.slice(separator + 2)); + const entry = byInventory.get(inventoryId); + if (entry === undefined) { + reasons.push(`specimen ${specimen.specimenId} references an inventory ID absent from the candidate register`); + continue; + } + if (selectedInventory.has(inventoryId)) reasons.push(`inventory item ${entry.inventoryId} is assigned to more than one campaign slot`); + selectedInventory.add(inventoryId); + if (entry.safetyStatus !== "eligible") reasons.push(`selected inventory item ${entry.inventoryId} is excluded`); + if (!entry.eligibleSlotIds.includes(slotId)) reasons.push(`inventory item ${entry.inventoryId} was not predeclared eligible for ${slotId}`); + if (normalize(entry.objectFamily) !== normalize(specimen.objectFamily)) reasons.push(`object family mismatch for ${specimen.specimenId}`); + if (normalize(entry.material) !== normalize(specimen.material)) reasons.push(`material mismatch for ${specimen.specimenId}`); + + if (specimen.cohort === "release-core") { + const material = normalize(specimen.material); + const families = familyByCoreMaterial.get(material) ?? new Set(); + families.add(normalize(specimen.objectFamily)); + familyByCoreMaterial.set(material, families); + } + } + + for (const material of ["metal", "glass", "ceramic"]) { + const families = familyByCoreMaterial.get(material); + if (families !== undefined && families.size < 2) { + reasons.push(`release-core ${material} specimens must come from two distinct object families`); + } + } + return [...new Set(reasons)]; +} + +async function sha256Text(value: string): Promise { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export async function deriveCampaignCollectionOrder( + campaign: EmpiricalCampaignV1, + campaignSignature: string, +): Promise { + const releaseCore = campaign.specimens.filter((specimen) => specimen.cohort === "release-core"); + const challenge = campaign.specimens.filter((specimen) => specimen.cohort === "challenge"); + if (releaseCore.length !== challenge.length) { + throw new Error("deterministic interleaving requires equal release-core and challenge cohort sizes"); + } + const rank = async (specimens: typeof releaseCore) => { + const values = await Promise.all(specimens.map(async (specimen) => ({ + specimen, + digest: await sha256Text(`gate-a2-order-v1|${campaignSignature}|${specimen.specimenId}`), + }))); + values.sort((left, right) => left.digest.localeCompare(right.digest)); + return values; + }; + const [coreRanked, challengeRanked] = await Promise.all([rank(releaseCore), rank(challenge)]); + const interleaved = coreRanked.flatMap((core, index) => [core, challengeRanked[index]!]); + return { + schemaVersion: 1, + orderContractVersion: "campaign-order-1", + campaignSignature, + entries: interleaved.map((entry, index) => ({ + ordinal: index + 1, + specimenId: entry.specimen.specimenId, + cohort: entry.specimen.cohort, + orderingDigest: `sha256:${entry.digest}`, + })), + }; +} From 82bc48af635403ad6878b2d4e81d0c60c20e1714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:09:01 -0700 Subject: [PATCH 006/101] Export precommitment validation API --- packages/validation/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 3b497b1c..4b113fca 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -23,6 +23,15 @@ export { type EmpiricalCampaignSpecimenStatus, type EmpiricalCampaignV1, } from "./campaign"; +export { + deriveCampaignCollectionOrder, + validateCampaignSelectionAgainstRegister, + validateCandidateRegister, + type CampaignCollectionOrderEntryV1, + type CampaignCollectionOrderV1, + type CandidateRegisterEntryV1, + type CandidateRegisterV1, +} from "./precommitment"; export { createGateBPlan, createGateCPlan, From e0d08d5d09f280f1e427447529e74ea6d0dc41df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:09:24 -0700 Subject: [PATCH 007/101] Test candidate register and deterministic collection order --- .../validation/test/precommitment.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/validation/test/precommitment.test.ts diff --git a/packages/validation/test/precommitment.test.ts b/packages/validation/test/precommitment.test.ts new file mode 100644 index 00000000..a50d72e7 --- /dev/null +++ b/packages/validation/test/precommitment.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest"; +import { + deriveCampaignCollectionOrder, + empiricalCampaignSignature, + validateCampaignSelectionAgainstRegister, + validateCandidateRegister, + type CandidateRegisterV1, + type EmpiricalCampaignV1, +} from "../src"; +import { SOFTWARE_REVISION } from "./helpers"; + +function campaign(): EmpiricalCampaignV1 { + const core = [ + ["core-metal-1", "inv-001", "metal", "bell"], + ["core-metal-2", "inv-002", "metal", "bowl"], + ["core-glass-1", "inv-003", "glass", "glass"], + ["core-glass-2", "inv-004", "glass", "bottle"], + ["core-ceramic-1", "inv-005", "ceramic", "mug"], + ["core-ceramic-2", "inv-006", "ceramic", "plate"], + ] as const; + const challenge = [ + ["challenge-short-decay", "inv-007", "wood", "block"], + ["challenge-broad", "inv-008", "plastic", "container"], + ["challenge-coupled", "inv-009", "composite", "assembly"], + ["challenge-high-q", "inv-010", "glass", "vessel"], + ["challenge-low-snr", "inv-011", "other", "small-object"], + ["challenge-degenerate", "inv-012", "other", "symmetric-object"], + ] as const; + return { + schemaVersion: 1, + campaignContractVersion: "empirical-campaign-1", + campaignId: "digital-precommitment-test", + createdAt: "2026-08-24T12:00:00.000Z", + authorizedSoftwareRevision: SOFTWARE_REVISION, + specimens: [ + ...core.map(([slot, inventory, material, family]) => ({ + specimenId: `${slot}--${inventory}`, + label: inventory, + material, + cohort: "release-core" as const, + objectFamily: family, + targetSessions: 1, + protocol: { fixedSetup: true as const, microphoneDistanceCm: 20, striker: "wooden dowel", strikeLocation: "mark", supportCondition: "fixed" }, + })), + ...challenge.map(([slot, inventory, material, family]) => ({ + specimenId: `${slot}--${inventory}`, + label: inventory, + material, + cohort: "challenge" as const, + objectFamily: family, + targetSessions: 1, + protocol: { fixedSetup: true as const, microphoneDistanceCm: 20, striker: "wooden dowel", strikeLocation: "mark", supportCondition: "fixed" }, + })), + ], + }; +} + +function register(): CandidateRegisterV1 { + return { + schemaVersion: 1, + registerContractVersion: "candidate-register-1", + createdAt: "2026-08-24T11:00:00.000Z", + acousticAuditionPerformed: false, + entries: campaign().specimens.map((specimen) => { + const separator = specimen.specimenId.indexOf("--"); + const slot = specimen.specimenId.slice(0, separator); + const inventory = specimen.specimenId.slice(separator + 2); + return { + inventoryId: inventory, + label: specimen.label, + objectFamily: specimen.objectFamily, + material: specimen.material, + safetyStatus: "eligible" as const, + supportDescription: "safe fixed support", + strikeLocationMarkable: true, + eligibleSlotIds: [slot], + exclusionReason: null, + }; + }), + }; +} + +describe("physical precommitment artifacts", () => { + it("validates a non-acoustic candidate register and its campaign bindings", () => { + const candidates = register(); + expect(validateCandidateRegister(candidates)).toEqual([]); + expect(validateCampaignSelectionAgainstRegister(campaign(), candidates)).toEqual([]); + }); + + it("rejects reuse of one physical inventory item", () => { + const value = campaign(); + const mutated: EmpiricalCampaignV1 = { + ...value, + specimens: value.specimens.map((specimen, index) => index === 1 + ? { ...specimen, specimenId: "core-metal-2--inv-001", objectFamily: "bell" } + : specimen), + }; + expect(validateCampaignSelectionAgainstRegister(mutated, register()).join("\n")).toMatch(/more than one campaign slot|not predeclared eligible/); + }); + + it("derives a stable 12-entry release-core/challenge interleaving", async () => { + const value = campaign(); + const signature = empiricalCampaignSignature(value); + const first = await deriveCampaignCollectionOrder(value, signature); + const second = await deriveCampaignCollectionOrder(value, signature); + expect(first).toEqual(second); + expect(first.entries).toHaveLength(12); + expect(first.entries.map((entry) => entry.cohort)).toEqual(Array.from({ length: 12 }, (_, index) => index % 2 === 0 ? "release-core" : "challenge")); + expect(new Set(first.entries.map((entry) => entry.specimenId)).size).toBe(12); + expect(first.entries.every((entry) => /^sha256:[0-9a-f]{64}$/.test(entry.orderingDigest))).toBe(true); + }); +}); From 5b52bd32d2ff965727d6a0b04b764e51fe924aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:09:52 -0700 Subject: [PATCH 008/101] Add deterministic digital-twin acoustic stress tests --- .../fingerprint/test/digital-twin.test.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 packages/fingerprint/test/digital-twin.test.ts diff --git a/packages/fingerprint/test/digital-twin.test.ts b/packages/fingerprint/test/digital-twin.test.ts new file mode 100644 index 00000000..44440e58 --- /dev/null +++ b/packages/fingerprint/test/digital-twin.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from "vitest"; +import { analyzeImpact } from "@everything-rings/dsp"; +import { fingerprintRecurrence } from "../src/recurrence"; + +interface TwinMode { + readonly frequencyHz: number; + readonly decaySeconds: number; + readonly amplitude: number; + readonly phase: number; +} + +interface DigitalTwin { + readonly name: string; + readonly modes: readonly TwinMode[]; +} + +interface Nuisance { + readonly gain: number; + readonly dampingScale: number; + readonly excitationTilt: number; + readonly noiseAmplitude: number; + readonly seed: number; +} + +const SAMPLE_RATE = 48_000; +const DURATION_SECONDS = 2.6; + +function prng(seed: number): () => number { + let state = seed >>> 0; + return () => { + state = (1664525 * state + 1013904223) >>> 0; + return state / 0x1_0000_0000; + }; +} + +function renderTwin(twin: DigitalTwin, nuisance: Nuisance): Float32Array { + const count = Math.round(SAMPLE_RATE * DURATION_SECONDS); + const samples = new Float32Array(count); + const random = prng(nuisance.seed); + for (let index = 0; index < count; index += 1) { + const t = index / SAMPLE_RATE; + let value = 0; + for (let modeIndex = 0; modeIndex < twin.modes.length; modeIndex += 1) { + const mode = twin.modes[modeIndex]!; + const positionWeight = Math.max(0.2, 1 + nuisance.excitationTilt * (modeIndex - (twin.modes.length - 1) / 2)); + value += nuisance.gain + * mode.amplitude + * positionWeight + * Math.exp(-t / (mode.decaySeconds * nuisance.dampingScale)) + * Math.sin(2 * Math.PI * mode.frequencyHz * t + mode.phase); + } + value += nuisance.noiseAmplitude * (2 * random() - 1); + samples[index] = Math.max(-0.9, Math.min(0.9, value)); + } + return samples; +} + +const ordinaryTwin: DigitalTwin = { + name: "ordinary-resonant", + modes: [ + { frequencyHz: 437, decaySeconds: 1.5, amplitude: 0.18, phase: 0.1 }, + { frequencyHz: 913, decaySeconds: 1.1, amplitude: 0.13, phase: 0.8 }, + { frequencyHz: 1517, decaySeconds: 0.85, amplitude: 0.10, phase: 1.4 }, + { frequencyHz: 2321, decaySeconds: 0.62, amplitude: 0.07, phase: 2.1 }, + { frequencyHz: 3413, decaySeconds: 0.48, amplitude: 0.05, phase: 2.7 }, + ], +}; + +const highQDigitalTwin: DigitalTwin = { + name: "high-q", + modes: [ + { frequencyHz: 521, decaySeconds: 2.2, amplitude: 0.14, phase: 0.2 }, + { frequencyHz: 1183, decaySeconds: 1.9, amplitude: 0.12, phase: 0.9 }, + { frequencyHz: 1987, decaySeconds: 1.6, amplitude: 0.09, phase: 1.7 }, + { frequencyHz: 2879, decaySeconds: 1.4, amplitude: 0.06, phase: 2.4 }, + ], +}; + +function analyzeTwin(twin: DigitalTwin, nuisance: Nuisance) { + return analyzeImpact(renderTwin(twin, nuisance), SAMPLE_RATE); +} + +describe("digital-twin acoustic stress validation", () => { + it("recovers a stable fingerprint across gain, excitation, damping, and noise nuisance", () => { + const reference = analyzeTwin(ordinaryTwin, { + gain: 1, + dampingScale: 1, + excitationTilt: 0, + noiseAmplitude: 0.0002, + seed: 1, + }); + expect(reference.ok).toBe(true); + if (!reference.ok) return; + + const nuisances: Nuisance[] = [ + { gain: 0.8, dampingScale: 0.95, excitationTilt: -0.08, noiseAmplitude: 0.0004, seed: 2 }, + { gain: 1.1, dampingScale: 1.05, excitationTilt: 0.08, noiseAmplitude: 0.0006, seed: 3 }, + { gain: 0.9, dampingScale: 1.1, excitationTilt: 0.12, noiseAmplitude: 0.0008, seed: 4 }, + { gain: 1.05, dampingScale: 0.9, excitationTilt: -0.12, noiseAmplitude: 0.0005, seed: 5 }, + ]; + + for (const nuisance of nuisances) { + const candidate = analyzeTwin(ordinaryTwin, nuisance); + expect(candidate.ok).toBe(true); + if (!candidate.ok) continue; + const recurrence = fingerprintRecurrence(reference.fingerprint, candidate.fingerprint); + expect(recurrence.matchedCount).toBeGreaterThanOrEqual(3); + expect(recurrence.medianCents).toBeLessThanOrEqual(25); + } + }); + + it("keeps a distinct digital twin acoustically separated from the ordinary twin", () => { + const left = analyzeTwin(ordinaryTwin, { gain: 1, dampingScale: 1, excitationTilt: 0, noiseAmplitude: 0.0002, seed: 6 }); + const right = analyzeTwin(highQDigitalTwin, { gain: 1, dampingScale: 1, excitationTilt: 0, noiseAmplitude: 0.0002, seed: 7 }); + expect(left.ok).toBe(true); + expect(right.ok).toBe(true); + if (!left.ok || !right.ok) return; + const recurrence = fingerprintRecurrence(left.fingerprint, right.fingerprint); + expect(recurrence.medianCents).toBeGreaterThan(50); + }); + + it("turns extreme low-SNR stress into an explicit analysis outcome, never silent corruption", () => { + const result = analyzeTwin(ordinaryTwin, { + gain: 0.03, + dampingScale: 0.7, + excitationTilt: 0.25, + noiseAmplitude: 0.03, + seed: 8, + }); + if (result.ok) { + expect(result.fingerprint.modes.length).toBeGreaterThanOrEqual(3); + expect(result.fingerprint.modes.every((mode) => Number.isFinite(mode.frequencyHz) && mode.frequencyHz > 0)).toBe(true); + } else { + expect(["NO_STABLE_RESONANCES", "SIGNAL_TOO_SHORT"]).toContain(result.reason); + } + }); +}); From 8ed4cc5beda212867acc300d1a19386964932c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:10:42 -0700 Subject: [PATCH 009/101] Add immutable measurement and derivation provenance model --- packages/validation/src/provenance.ts | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/validation/src/provenance.ts diff --git a/packages/validation/src/provenance.ts b/packages/validation/src/provenance.ts new file mode 100644 index 00000000..01f36a47 --- /dev/null +++ b/packages/validation/src/provenance.ts @@ -0,0 +1,106 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import type { CaptureSettingsEvidence, FixedSetupProtocol, MaterialClass } from "./types"; + +export interface MeasurementStationV1 { + readonly stationId: string; + readonly deviceDescription: string; + readonly operatingSystem: string; + readonly runtime: string; + readonly microphoneDescription: string | null; + readonly captureSettings: CaptureSettingsEvidence | null; +} + +export interface MeasurementRecordV1 { + readonly schemaVersion: 1; + readonly measurementContractVersion: "measurement-record-1"; + readonly measurementId: string; + readonly createdAt: string; + readonly specimenId: string; + readonly sessionId: string; + readonly attemptId: number; + readonly material: MaterialClass; + readonly acquisitionContractVersion: string; + readonly softwareRevision: string; + readonly fingerprintAlgorithmVersion: string; + readonly setup: FixedSetupProtocol; + readonly station: MeasurementStationV1; + readonly fingerprint: AcousticFingerprintV1; + readonly rawMicrophoneSamplesIncluded: false; +} + +export type DerivationKind = "renderer" | "instrument" | "similarity" | "embedding" | "visualization" | "atlas-record"; + +export interface DerivationRecordV1 { + readonly schemaVersion: 1; + readonly derivationContractVersion: "derivation-record-1"; + readonly derivationId: string; + readonly measurementId: string; + readonly kind: DerivationKind; + readonly algorithmVersion: string; + readonly configDigest: string; + readonly artifactDigest: string; + readonly createdAt: string; +} + +function canonicalize(value: unknown): string { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`; + const record = value as Record; + return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`).join(",")}}`; +} + +async function sha256(value: string): Promise { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); + return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`; +} + +export async function contentDigest(value: unknown): Promise { + return sha256(canonicalize(value)); +} + +export async function createMeasurementRecord( + input: Omit, +): Promise { + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("measurement createdAt is invalid"); + if (!/^[0-9a-f]{40}$/.test(input.softwareRevision)) throw new Error("measurement softwareRevision must be exact 40-hex Git revision"); + if (input.specimenId.trim().length === 0 || input.sessionId.trim().length === 0) throw new Error("measurement specimen/session identity is required"); + if (!Number.isInteger(input.attemptId) || input.attemptId <= 0) throw new Error("measurement attemptId must be positive"); + if (input.fingerprint.algorithmVersion !== input.fingerprintAlgorithmVersion) { + throw new Error("measurement fingerprint algorithm version mismatch"); + } + const payload = { + ...input, + schemaVersion: 1 as const, + measurementContractVersion: "measurement-record-1" as const, + rawMicrophoneSamplesIncluded: false as const, + }; + const measurementId = await contentDigest(payload); + return { ...payload, measurementId }; +} + +export async function createDerivationRecord( + input: Omit, +): Promise { + if (!/^sha256:[0-9a-f]{64}$/.test(input.measurementId)) throw new Error("derivation measurementId must be a measurement content digest"); + if (!/^sha256:[0-9a-f]{64}$/.test(input.configDigest)) throw new Error("derivation configDigest is invalid"); + if (!/^sha256:[0-9a-f]{64}$/.test(input.artifactDigest)) throw new Error("derivation artifactDigest is invalid"); + if (input.algorithmVersion.trim().length === 0) throw new Error("derivation algorithmVersion is required"); + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("derivation createdAt is invalid"); + const payload = { + ...input, + schemaVersion: 1 as const, + derivationContractVersion: "derivation-record-1" as const, + }; + const derivationId = await contentDigest(payload); + return { ...payload, derivationId }; +} + +export async function verifyMeasurementRecord(record: MeasurementRecordV1): Promise { + const { measurementId, ...payload } = record; + return measurementId === await contentDigest(payload); +} + +export async function verifyDerivationRecord(record: DerivationRecordV1): Promise { + const { derivationId, ...payload } = record; + return derivationId === await contentDigest(payload); +} From 9ffadedb99e63fb10e92d615a26a5475da91e42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:10:53 -0700 Subject: [PATCH 010/101] Export measurement provenance API --- packages/validation/src/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index f2b23b73..110a81dd 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -23,6 +23,17 @@ export { type EmpiricalCampaignSpecimenStatus, type EmpiricalCampaignV1, } from "./campaign"; +export { + contentDigest, + createDerivationRecord, + createMeasurementRecord, + verifyDerivationRecord, + verifyMeasurementRecord, + type DerivationKind, + type DerivationRecordV1, + type MeasurementRecordV1, + type MeasurementStationV1, +} from "./provenance"; export { deriveEvidenceRecurrence, deriveMedianModalDriftCents, From 129e25edfbb8fbf4c56e8d803c9b851f05cff3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:11:08 -0700 Subject: [PATCH 011/101] Test immutable measurement and derivation provenance --- packages/validation/test/provenance.test.ts | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/validation/test/provenance.test.ts diff --git a/packages/validation/test/provenance.test.ts b/packages/validation/test/provenance.test.ts new file mode 100644 index 00000000..0fc0241d --- /dev/null +++ b/packages/validation/test/provenance.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { + contentDigest, + createDerivationRecord, + createMeasurementRecord, + verifyDerivationRecord, + verifyMeasurementRecord, +} from "../src"; +import { fingerprint, SOFTWARE_REVISION } from "./helpers"; + +describe("measurement provenance", () => { + it("content-addresses one immutable measurement independently of later derivations", async () => { + const fp = fingerprint(); + const measurement = await createMeasurementRecord({ + createdAt: "2026-08-24T12:00:00.000Z", + specimenId: "specimen-001", + sessionId: "session-001", + attemptId: 5, + material: "metal", + acquisitionContractVersion: "acquisition-browser-1", + softwareRevision: SOFTWARE_REVISION, + fingerprintAlgorithmVersion: fp.algorithmVersion, + setup: { + fixedSetup: true, + microphoneDistanceCm: 20, + striker: "wooden dowel", + strikeLocation: "marked rim", + supportCondition: "fixed support", + }, + station: { + stationId: "station-001", + deviceDescription: "digital station", + operatingSystem: "test-os", + runtime: "test-browser", + microphoneDescription: "simulated microphone", + captureSettings: null, + }, + fingerprint: fp, + }); + + expect(measurement.rawMicrophoneSamplesIncluded).toBe(false); + expect(measurement.measurementId).toMatch(/^sha256:[0-9a-f]{64}$/); + expect(await verifyMeasurementRecord(measurement)).toBe(true); + + const renderer = await createDerivationRecord({ + measurementId: measurement.measurementId, + kind: "renderer", + algorithmVersion: "modal-renderer-2", + configDigest: await contentDigest({ attackMs: 5, normalization: "peak" }), + artifactDigest: await contentDigest({ renderedFingerprint: measurement.fingerprint }), + createdAt: "2026-08-24T13:00:00.000Z", + }); + const instrument = await createDerivationRecord({ + measurementId: measurement.measurementId, + kind: "instrument", + algorithmVersion: "modal-instrument-3", + configDigest: await contentDigest({ anchor: "dominant-mode" }), + artifactDigest: await contentDigest({ playable: true }), + createdAt: "2026-08-24T14:00:00.000Z", + }); + + expect(renderer.measurementId).toBe(measurement.measurementId); + expect(instrument.measurementId).toBe(measurement.measurementId); + expect(renderer.derivationId).not.toBe(instrument.derivationId); + expect(await verifyDerivationRecord(renderer)).toBe(true); + expect(await verifyDerivationRecord(instrument)).toBe(true); + }); + + it("detects tampering without mutable provenance state", async () => { + const digest = await contentDigest({ a: 1, b: [2, 3] }); + const reordered = await contentDigest({ b: [2, 3], a: 1 }); + expect(digest).toBe(reordered); + }); +}); From 11de404dedb54ef80941c8c1ecf2ef9ee041f264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:11:42 -0700 Subject: [PATCH 012/101] Add abstaining deterministic Sonic Twin baseline --- packages/fingerprint/src/sonic-twin.ts | 185 +++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 packages/fingerprint/src/sonic-twin.ts diff --git a/packages/fingerprint/src/sonic-twin.ts b/packages/fingerprint/src/sonic-twin.ts new file mode 100644 index 00000000..f81ed854 --- /dev/null +++ b/packages/fingerprint/src/sonic-twin.ts @@ -0,0 +1,185 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { fingerprintRecurrence } from "./recurrence"; + +export interface SonicTwinBaselineConfig { + readonly minimumMatchedModes: number; + readonly maximumFrequencyMedianCentsForMatch: number; + readonly minimumFrequencyMedianCentsForNonMatch: number; + readonly maximumMedianDecayLogRatioForMatch: number; + readonly maximumMedianAmplitudeDbForMatch: number; +} + +export const DEFAULT_SONIC_TWIN_BASELINE_CONFIG: SonicTwinBaselineConfig = { + minimumMatchedModes: 3, + maximumFrequencyMedianCentsForMatch: 35, + minimumFrequencyMedianCentsForNonMatch: 120, + maximumMedianDecayLogRatioForMatch: 0.45, + maximumMedianAmplitudeDbForMatch: 8, +}; + +export type SonicTwinDecision = "match" | "nonmatch" | "abstain"; + +export interface SonicTwinBaselineComparison { + readonly baselineVersion: "sonic-twin-baseline-1"; + readonly decision: SonicTwinDecision; + readonly matchedModes: number; + readonly frequencyMedianCents: number; + readonly frequencyMeanCents: number; + readonly medianDecayLogRatio: number | null; + readonly medianAmplitudeDbDifference: number | null; + readonly reasons: readonly string[]; + readonly calibratedProbability: null; +} + +function median(values: readonly number[]): number | null { + if (values.length === 0) return null; + const ordered = [...values].sort((left, right) => left - right); + const middle = Math.floor(ordered.length / 2); + if (ordered.length % 2 === 1) return ordered[middle] ?? null; + return ((ordered[middle - 1] ?? 0) + (ordered[middle] ?? 0)) / 2; +} + +function safeDbRatio(left: number, right: number): number { + const floor = 1e-6; + return 20 * Math.abs(Math.log10(Math.max(left, floor) / Math.max(right, floor))); +} + +export function compareSonicTwinBaseline( + reference: AcousticFingerprintV1, + candidate: AcousticFingerprintV1, + config: SonicTwinBaselineConfig = DEFAULT_SONIC_TWIN_BASELINE_CONFIG, +): SonicTwinBaselineComparison { + const recurrence = fingerprintRecurrence(reference, candidate); + const paired = recurrence.matches.flatMap((match) => { + if (match.candidateIndex === undefined) return []; + const left = reference.modes[match.referenceIndex]; + const right = candidate.modes[match.candidateIndex]; + if (left === undefined || right === undefined) return []; + return [{ left, right }]; + }); + const decay = median(paired.map(({ left, right }) => Math.abs(Math.log(Math.max(left.decaySeconds, 1e-6) / Math.max(right.decaySeconds, 1e-6))))); + const amplitude = median(paired.map(({ left, right }) => safeDbRatio(left.relativeAmplitude, right.relativeAmplitude))); + const reasons: string[] = []; + + if (recurrence.matchedCount < config.minimumMatchedModes) { + reasons.push(`fewer than ${config.minimumMatchedModes} modes are comparable`); + return { + baselineVersion: "sonic-twin-baseline-1", + decision: "abstain", + matchedModes: recurrence.matchedCount, + frequencyMedianCents: recurrence.medianCents, + frequencyMeanCents: recurrence.meanCents, + medianDecayLogRatio: decay, + medianAmplitudeDbDifference: amplitude, + reasons, + calibratedProbability: null, + }; + } + + const frequencyMatch = recurrence.medianCents <= config.maximumFrequencyMedianCentsForMatch; + const decayMatch = decay !== null && decay <= config.maximumMedianDecayLogRatioForMatch; + const amplitudeMatch = amplitude !== null && amplitude <= config.maximumMedianAmplitudeDbForMatch; + if (frequencyMatch && decayMatch && amplitudeMatch) { + return { + baselineVersion: "sonic-twin-baseline-1", + decision: "match", + matchedModes: recurrence.matchedCount, + frequencyMedianCents: recurrence.medianCents, + frequencyMeanCents: recurrence.meanCents, + medianDecayLogRatio: decay, + medianAmplitudeDbDifference: amplitude, + reasons: [], + calibratedProbability: null, + }; + } + + if (recurrence.medianCents >= config.minimumFrequencyMedianCentsForNonMatch) { + reasons.push("modal-frequency separation exceeds the nonmatch threshold"); + return { + baselineVersion: "sonic-twin-baseline-1", + decision: "nonmatch", + matchedModes: recurrence.matchedCount, + frequencyMedianCents: recurrence.medianCents, + frequencyMeanCents: recurrence.meanCents, + medianDecayLogRatio: decay, + medianAmplitudeDbDifference: amplitude, + reasons, + calibratedProbability: null, + }; + } + + if (!frequencyMatch) reasons.push("frequency agreement is outside the match region"); + if (!decayMatch) reasons.push("decay agreement is outside the match region"); + if (!amplitudeMatch) reasons.push("modal-amplitude agreement is outside the match region"); + return { + baselineVersion: "sonic-twin-baseline-1", + decision: "abstain", + matchedModes: recurrence.matchedCount, + frequencyMedianCents: recurrence.medianCents, + frequencyMeanCents: recurrence.meanCents, + medianDecayLogRatio: decay, + medianAmplitudeDbDifference: amplitude, + reasons, + calibratedProbability: null, + }; +} + +export interface LabeledTwinPair { + readonly samePhysicalSpecimen: boolean; + readonly reference: AcousticFingerprintV1; + readonly candidate: AcousticFingerprintV1; +} + +export interface SonicTwinBenchmarkMetrics { + readonly pairCount: number; + readonly coveredPairCount: number; + readonly abstainedPairCount: number; + readonly coverage: number; + readonly truePositiveRate: number | null; + readonly falsePositiveRate: number | null; + readonly trueNegativeRate: number | null; + readonly falseNegativeRate: number | null; +} + +function rate(numerator: number, denominator: number): number | null { + return denominator === 0 ? null : numerator / denominator; +} + +export function benchmarkSonicTwinBaseline( + pairs: readonly LabeledTwinPair[], + config: SonicTwinBaselineConfig = DEFAULT_SONIC_TWIN_BASELINE_CONFIG, +): SonicTwinBenchmarkMetrics { + let tp = 0; + let fp = 0; + let tn = 0; + let fn = 0; + let positives = 0; + let negatives = 0; + let covered = 0; + + for (const pair of pairs) { + const result = compareSonicTwinBaseline(pair.reference, pair.candidate, config); + if (result.decision === "abstain") continue; + covered += 1; + if (pair.samePhysicalSpecimen) { + positives += 1; + if (result.decision === "match") tp += 1; + else fn += 1; + } else { + negatives += 1; + if (result.decision === "nonmatch") tn += 1; + else fp += 1; + } + } + + return { + pairCount: pairs.length, + coveredPairCount: covered, + abstainedPairCount: pairs.length - covered, + coverage: pairs.length === 0 ? 0 : covered / pairs.length, + truePositiveRate: rate(tp, positives), + falsePositiveRate: rate(fp, negatives), + trueNegativeRate: rate(tn, negatives), + falseNegativeRate: rate(fn, positives), + }; +} From 3416b33138503632f681f694ce85c6f8989f47f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:11:50 -0700 Subject: [PATCH 013/101] Export Sonic Twin baseline API --- packages/fingerprint/src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/fingerprint/src/index.ts b/packages/fingerprint/src/index.ts index 9655c16d..cb7d1ecb 100644 --- a/packages/fingerprint/src/index.ts +++ b/packages/fingerprint/src/index.ts @@ -6,3 +6,13 @@ export { type ModeMatch, type ModeRecurrenceConfig, } from "./recurrence"; +export { + DEFAULT_SONIC_TWIN_BASELINE_CONFIG, + benchmarkSonicTwinBaseline, + compareSonicTwinBaseline, + type LabeledTwinPair, + type SonicTwinBaselineComparison, + type SonicTwinBaselineConfig, + type SonicTwinBenchmarkMetrics, + type SonicTwinDecision, +} from "./sonic-twin"; From a09588154e75433b6d7abe5aad63829be4d7640e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:12:05 -0700 Subject: [PATCH 014/101] Test abstaining Sonic Twin baseline and benchmark metrics --- packages/fingerprint/test/sonic-twin.test.ts | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/fingerprint/test/sonic-twin.test.ts diff --git a/packages/fingerprint/test/sonic-twin.test.ts b/packages/fingerprint/test/sonic-twin.test.ts new file mode 100644 index 00000000..bd3e27e3 --- /dev/null +++ b/packages/fingerprint/test/sonic-twin.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { benchmarkSonicTwinBaseline, compareSonicTwinBaseline } from "../src"; + +function fingerprint( + frequencies: readonly number[], + decayScale = 1, + amplitudeScale = 1, +): AcousticFingerprintV1 { + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: frequencies.map((frequencyHz, index) => ({ + frequencyHz, + relativeAmplitude: Math.min(1, amplitudeScale / (index + 1)), + decaySeconds: decayScale * (0.8 / (index + 1)), + q: 100, + confidence: 0.9, + diagnostics: { + prominenceDb: 20, + persistenceSeconds: 0.2, + frequencyStdCents: 2, + decayFitScore: 0.95, + observationCount: 12, + }, + })), + }; +} + +describe("Sonic Twin deterministic baseline", () => { + const base = fingerprint([440, 880, 1320, 1760]); + + it("matches close repeated measurements without emitting a probability", () => { + const repeated = fingerprint([441, 882, 1323, 1764], 1.05, 0.95); + const result = compareSonicTwinBaseline(base, repeated); + expect(result.decision).toBe("match"); + expect(result.matchedModes).toBeGreaterThanOrEqual(3); + expect(result.calibratedProbability).toBeNull(); + }); + + it("rejects clearly separated hard negatives", () => { + const other = fingerprint([520, 1040, 1560, 2080]); + const result = compareSonicTwinBaseline(base, other); + expect(result.decision).toBe("nonmatch"); + expect(result.calibratedProbability).toBeNull(); + }); + + it("abstains in the ambiguous region", () => { + const ambiguous = fingerprint([455, 910, 1365, 1820], 1.7, 0.3); + const result = compareSonicTwinBaseline(base, ambiguous); + expect(result.decision).toBe("abstain"); + }); + + it("reports coverage separately from verification error rates", () => { + const metrics = benchmarkSonicTwinBaseline([ + { samePhysicalSpecimen: true, reference: base, candidate: fingerprint([441, 882, 1323, 1764], 1.02, 0.98) }, + { samePhysicalSpecimen: false, reference: base, candidate: fingerprint([530, 1060, 1590, 2120]) }, + { samePhysicalSpecimen: true, reference: base, candidate: fingerprint([455, 910, 1365, 1820], 1.8, 0.25) }, + ]); + expect(metrics.pairCount).toBe(3); + expect(metrics.coveredPairCount).toBe(2); + expect(metrics.abstainedPairCount).toBe(1); + expect(metrics.truePositiveRate).toBe(1); + expect(metrics.falsePositiveRate).toBe(0); + }); +}); From 0d5320582e68938a2596310f40cf3315ef9383c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:12:22 -0700 Subject: [PATCH 015/101] Add privacy-preserving Resonance Atlas record schema --- packages/validation/src/atlas.ts | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 packages/validation/src/atlas.ts diff --git a/packages/validation/src/atlas.ts b/packages/validation/src/atlas.ts new file mode 100644 index 00000000..d5fbb292 --- /dev/null +++ b/packages/validation/src/atlas.ts @@ -0,0 +1,92 @@ +import type { MaterialClass } from "./types"; +import { contentDigest } from "./provenance"; + +export interface AtlasContributorV1 { + readonly contributorId: string; + readonly displayName: string | null; +} + +export interface AtlasSpecimenV1 { + readonly specimenId: string; + readonly label: string; + readonly objectFamily: string; + readonly material: MaterialClass | "unknown"; + readonly publicDescription: string | null; +} + +export interface AtlasMeasurementReferenceV1 { + readonly measurementId: string; + readonly derivationIds: readonly string[]; +} + +export interface ResonanceAtlasRecordV1 { + readonly schemaVersion: 1; + readonly atlasContractVersion: "resonance-atlas-record-1"; + readonly atlasRecordId: string; + readonly createdAt: string; + readonly contributor: AtlasContributorV1 | null; + readonly specimen: AtlasSpecimenV1; + readonly measurements: readonly AtlasMeasurementReferenceV1[]; + readonly rawMicrophoneSamplesIncluded: false; + readonly publicationConsent: true; +} + +export interface ResonanceAtlasSnapshotV1 { + readonly schemaVersion: 1; + readonly snapshotContractVersion: "resonance-atlas-snapshot-1"; + readonly createdAt: string; + readonly recordIds: readonly string[]; + readonly snapshotId: string; +} + +function assertDigest(value: string, field: string): void { + if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new Error(`${field} must be a sha256 content digest`); +} + +export async function createAtlasRecord( + input: Omit, +): Promise { + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("Atlas record createdAt is invalid"); + if (input.specimen.specimenId.trim().length === 0) throw new Error("Atlas specimenId is required"); + if (input.specimen.label.trim().length === 0) throw new Error("Atlas specimen label is required"); + if (input.measurements.length === 0) throw new Error("Atlas record requires at least one measurement reference"); + const seen = new Set(); + for (const measurement of input.measurements) { + assertDigest(measurement.measurementId, "measurementId"); + if (seen.has(measurement.measurementId)) throw new Error("Atlas record contains a duplicate measurement reference"); + seen.add(measurement.measurementId); + measurement.derivationIds.forEach((digest) => assertDigest(digest, "derivationId")); + } + const payload = { + ...input, + schemaVersion: 1 as const, + atlasContractVersion: "resonance-atlas-record-1" as const, + rawMicrophoneSamplesIncluded: false as const, + publicationConsent: true as const, + }; + const atlasRecordId = await contentDigest(payload); + return { ...payload, atlasRecordId }; +} + +export async function createAtlasSnapshot( + recordIds: readonly string[], + createdAt: string, +): Promise { + if (!Number.isFinite(Date.parse(createdAt))) throw new Error("Atlas snapshot createdAt is invalid"); + if (recordIds.length === 0) throw new Error("Atlas snapshot requires at least one record"); + const canonicalIds = [...new Set(recordIds)].sort(); + canonicalIds.forEach((digest) => assertDigest(digest, "atlasRecordId")); + const payload = { + schemaVersion: 1 as const, + snapshotContractVersion: "resonance-atlas-snapshot-1" as const, + createdAt, + recordIds: canonicalIds, + }; + return { ...payload, snapshotId: await contentDigest(payload) }; +} + +export async function verifyAtlasRecord(record: ResonanceAtlasRecordV1): Promise { + const { atlasRecordId, ...payload } = record; + if (record.rawMicrophoneSamplesIncluded !== false || record.publicationConsent !== true) return false; + return atlasRecordId === await contentDigest(payload); +} From 607b37e5deee88af13e41e0a276473c58f0fa6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:12:37 -0700 Subject: [PATCH 016/101] Export Resonance Atlas schema API --- packages/validation/src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 110a81dd..4a9e5b46 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -34,6 +34,16 @@ export { type MeasurementRecordV1, type MeasurementStationV1, } from "./provenance"; +export { + createAtlasRecord, + createAtlasSnapshot, + verifyAtlasRecord, + type AtlasContributorV1, + type AtlasMeasurementReferenceV1, + type AtlasSpecimenV1, + type ResonanceAtlasRecordV1, + type ResonanceAtlasSnapshotV1, +} from "./atlas"; export { deriveEvidenceRecurrence, deriveMedianModalDriftCents, From 0e0f6ee942f1be984893141b9be5122ee990c2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:12:54 -0700 Subject: [PATCH 017/101] Test privacy-preserving Atlas records and snapshots --- packages/validation/test/atlas.test.ts | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/validation/test/atlas.test.ts diff --git a/packages/validation/test/atlas.test.ts b/packages/validation/test/atlas.test.ts new file mode 100644 index 00000000..f3fd1e84 --- /dev/null +++ b/packages/validation/test/atlas.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { + contentDigest, + createAtlasRecord, + createAtlasSnapshot, + verifyAtlasRecord, +} from "../src"; + +describe("Resonance Atlas provenance schema", () => { + it("publishes only content-addressed measurement references with explicit consent", async () => { + const measurementId = await contentDigest({ measurement: 1 }); + const derivationId = await contentDigest({ renderer: 1 }); + const record = await createAtlasRecord({ + createdAt: "2026-08-24T15:00:00.000Z", + contributor: { contributorId: "contributor-001", displayName: null }, + specimen: { + specimenId: "specimen-001", + label: "Digital twin specimen", + objectFamily: "reference-object", + material: "unknown", + publicDescription: "Synthetic validation specimen", + }, + measurements: [{ measurementId, derivationIds: [derivationId] }], + }); + expect(record.rawMicrophoneSamplesIncluded).toBe(false); + expect(record.publicationConsent).toBe(true); + expect(record.atlasRecordId).toMatch(/^sha256:[0-9a-f]{64}$/); + expect(await verifyAtlasRecord(record)).toBe(true); + }); + + it("builds deterministic corpus snapshots from a canonical record set", async () => { + const ids = [await contentDigest({ record: 2 }), await contentDigest({ record: 1 })]; + const first = await createAtlasSnapshot(ids, "2026-08-24T16:00:00.000Z"); + const second = await createAtlasSnapshot([...ids].reverse(), "2026-08-24T16:00:00.000Z"); + expect(first).toEqual(second); + expect(first.recordIds).toEqual([...ids].sort()); + }); +}); From a6529ef2c9b54c801412e902094960a5a743a6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:13:43 -0700 Subject: [PATCH 018/101] Add runtime-independent SHA-256 helper --- packages/validation/src/sha256.ts | 91 +++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 packages/validation/src/sha256.ts diff --git a/packages/validation/src/sha256.ts b/packages/validation/src/sha256.ts new file mode 100644 index 00000000..d3ed9956 --- /dev/null +++ b/packages/validation/src/sha256.ts @@ -0,0 +1,91 @@ +function utf8Bytes(value: string): Uint8Array { + const bytes: number[] = []; + for (const character of value) { + const codePoint = character.codePointAt(0) ?? 0; + if (codePoint <= 0x7f) bytes.push(codePoint); + else if (codePoint <= 0x7ff) { + bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f)); + } else if (codePoint <= 0xffff) { + bytes.push( + 0xe0 | (codePoint >> 12), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } else { + bytes.push( + 0xf0 | (codePoint >> 18), + 0x80 | ((codePoint >> 12) & 0x3f), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f), + ); + } + } + return Uint8Array.from(bytes); +} + +const INITIAL = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, +] as const; + +const K = [ + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2, +] as const; + +function rotateRight(value: number, bits: number): number { + return (value >>> bits) | (value << (32 - bits)); +} + +export function sha256Hex(value: string): string { + const input = utf8Bytes(value); + const bitLength = input.length * 8; + const paddingLength = (64 - ((input.length + 9) % 64)) % 64; + const bytes = new Uint8Array(input.length + 1 + paddingLength + 8); + bytes.set(input); + bytes[input.length] = 0x80; + const view = new DataView(bytes.buffer); + const high = Math.floor(bitLength / 0x1_0000_0000); + const low = bitLength >>> 0; + view.setUint32(bytes.length - 8, high, false); + view.setUint32(bytes.length - 4, low, false); + + const hash = [...INITIAL]; + const words = new Uint32Array(64); + for (let offset = 0; offset < bytes.length; offset += 64) { + for (let index = 0; index < 16; index += 1) words[index] = view.getUint32(offset + index * 4, false); + for (let index = 16; index < 64; index += 1) { + const x = words[index - 15] ?? 0; + const y = words[index - 2] ?? 0; + const s0 = rotateRight(x, 7) ^ rotateRight(x, 18) ^ (x >>> 3); + const s1 = rotateRight(y, 17) ^ rotateRight(y, 19) ^ (y >>> 10); + words[index] = (((words[index - 16] ?? 0) + s0 + (words[index - 7] ?? 0) + s1) >>> 0); + } + + let [a,b,c,d,e,f,g,h] = hash; + for (let index = 0; index < 64; index += 1) { + const s1 = rotateRight(e!, 6) ^ rotateRight(e!, 11) ^ rotateRight(e!, 25); + const ch = (e! & f!) ^ (~e! & g!); + const temp1 = (h! + s1 + ch + (K[index] ?? 0) + (words[index] ?? 0)) >>> 0; + const s0 = rotateRight(a!, 2) ^ rotateRight(a!, 13) ^ rotateRight(a!, 22); + const maj = (a! & b!) ^ (a! & c!) ^ (b! & c!); + const temp2 = (s0 + maj) >>> 0; + h = g; g = f; f = e; e = (d! + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 0; + } + hash[0] = ((hash[0] ?? 0) + a!) >>> 0; + hash[1] = ((hash[1] ?? 0) + b!) >>> 0; + hash[2] = ((hash[2] ?? 0) + c!) >>> 0; + hash[3] = ((hash[3] ?? 0) + d!) >>> 0; + hash[4] = ((hash[4] ?? 0) + e!) >>> 0; + hash[5] = ((hash[5] ?? 0) + f!) >>> 0; + hash[6] = ((hash[6] ?? 0) + g!) >>> 0; + hash[7] = ((hash[7] ?? 0) + h!) >>> 0; + } + return hash.map((word) => word.toString(16).padStart(8, "0")).join(""); +} From 92bebfe32e0bf17e18a717fcc82a14facbb56302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:14:22 -0700 Subject: [PATCH 019/101] Declare cross-runtime Web Crypto globals for validation hashing --- packages/validation/src/runtime-globals.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/validation/src/runtime-globals.d.ts diff --git a/packages/validation/src/runtime-globals.d.ts b/packages/validation/src/runtime-globals.d.ts new file mode 100644 index 00000000..0939e50d --- /dev/null +++ b/packages/validation/src/runtime-globals.d.ts @@ -0,0 +1,11 @@ +interface ValidationSubtleCrypto { + digest(algorithm: string, data: ArrayBuffer | ArrayBufferView): Promise; +} + +declare const crypto: { + readonly subtle: ValidationSubtleCrypto; +}; + +declare class TextEncoder { + encode(input?: string): Uint8Array; +} From f2c1c5c36deef13c90d4dcadfc60a1c230b1a13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:14:28 -0700 Subject: [PATCH 020/101] Declare cross-runtime Web Crypto globals for provenance hashing --- packages/validation/src/runtime-globals.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/validation/src/runtime-globals.d.ts diff --git a/packages/validation/src/runtime-globals.d.ts b/packages/validation/src/runtime-globals.d.ts new file mode 100644 index 00000000..0939e50d --- /dev/null +++ b/packages/validation/src/runtime-globals.d.ts @@ -0,0 +1,11 @@ +interface ValidationSubtleCrypto { + digest(algorithm: string, data: ArrayBuffer | ArrayBufferView): Promise; +} + +declare const crypto: { + readonly subtle: ValidationSubtleCrypto; +}; + +declare class TextEncoder { + encode(input?: string): Uint8Array; +} From 30c9f127bb02eea850a18b735afff38f4f8e4eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:15:37 -0700 Subject: [PATCH 021/101] Add full-stack digital-twin qualification test --- apps/web/src/DigitalTwinEndToEnd.test.ts | 143 +++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 apps/web/src/DigitalTwinEndToEnd.test.ts diff --git a/apps/web/src/DigitalTwinEndToEnd.test.ts b/apps/web/src/DigitalTwinEndToEnd.test.ts new file mode 100644 index 00000000..9c21ac96 --- /dev/null +++ b/apps/web/src/DigitalTwinEndToEnd.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it } from "vitest"; +import { assessCaptureQuality, type AudioCapture } from "@everything-rings/acquisition"; +import { analyzeImpact, extractImpactRingdown } from "@everything-rings/dsp"; +import { compareSonicTwinBaseline } from "@everything-rings/fingerprint"; +import { renderPlayableNote } from "@everything-rings/instrument"; +import { renderAcousticFingerprint } from "@everything-rings/synth"; +import { + contentDigest, + createAtlasRecord, + createDerivationRecord, + createMeasurementRecord, + verifyAtlasRecord, + verifyDerivationRecord, + verifyMeasurementRecord, +} from "@everything-rings/validation"; + +const SAMPLE_RATE = 48_000; +const REVISION = "0123456789abcdef0123456789abcdef01234567"; + +function deterministicNoise(seed: number): () => number { + let state = seed >>> 0; + return () => { + state = (1664525 * state + 1013904223) >>> 0; + return state / 0x1_0000_0000; + }; +} + +function digitalTwinCapture(seed = 1): AudioCapture { + const triggerSample = Math.round(0.15 * SAMPLE_RATE); + const durationSeconds = 3.0; + const samples = new Float32Array(Math.round(durationSeconds * SAMPLE_RATE)); + const noise = deterministicNoise(seed); + const modes = [ + { f: 431, tau: 1.35, a: 0.22, p: 0.2 }, + { f: 907, tau: 1.05, a: 0.15, p: 0.8 }, + { f: 1499, tau: 0.82, a: 0.11, p: 1.4 }, + { f: 2281, tau: 0.65, a: 0.08, p: 2.1 }, + { f: 3359, tau: 0.52, a: 0.05, p: 2.7 }, + ]; + for (let index = 0; index < samples.length; index += 1) { + const background = 0.00025 * (2 * noise() - 1); + if (index < triggerSample) { + samples[index] = background; + continue; + } + const t = (index - triggerSample) / SAMPLE_RATE; + let value = background; + for (const mode of modes) value += mode.a * Math.exp(-t / mode.tau) * Math.sin(2 * Math.PI * mode.f * t + mode.p); + samples[index] = Math.max(-0.92, Math.min(0.92, value)); + } + return { samples, sampleRate: SAMPLE_RATE, triggerSample }; +} + +function finiteSignal(samples: Float32Array): boolean { + return samples.length > 0 && samples.every((value) => Number.isFinite(value)); +} + +describe("digital-twin full-stack qualification", () => { + it("exercises acquisition quality, DSP, reconstruction, instrument, provenance, Twin baseline, and Atlas without a physical object", async () => { + const capture = digitalTwinCapture(); + const quality = assessCaptureQuality(capture); + expect(quality.ok).toBe(true); + + const ringdown = extractImpactRingdown(capture.samples, capture.sampleRate, capture.triggerSample); + const analysis = analyzeImpact(ringdown.samples, capture.sampleRate); + expect(analysis.ok).toBe(true); + if (!analysis.ok) return; + expect(analysis.fingerprint.modes.length).toBeGreaterThanOrEqual(3); + expect(analysis.fingerprint.algorithmVersion).toBe("er-dsp-2"); + + const reconstructed = renderAcousticFingerprint(analysis.fingerprint, SAMPLE_RATE); + expect(finiteSignal(reconstructed)).toBe(true); + expect(Math.max(...reconstructed.map(Math.abs))).toBeLessThanOrEqual(0.90001); + + const reanalysis = analyzeImpact(reconstructed, SAMPLE_RATE); + expect(reanalysis.ok).toBe(true); + if (reanalysis.ok) { + const reconstructionComparison = compareSonicTwinBaseline(analysis.fingerprint, reanalysis.fingerprint); + expect(reconstructionComparison.decision).not.toBe("nonmatch"); + expect(reconstructionComparison.calibratedProbability).toBeNull(); + } + + for (const midiNote of [48, 60, 72, 84]) { + const note = renderPlayableNote(analysis.fingerprint, midiNote, SAMPLE_RATE); + expect(finiteSignal(note.samples)).toBe(true); + expect(note.frequencyScale).toBeGreaterThan(0); + expect(note.targetFrequencyHz).toBeGreaterThan(0); + } + + const measurement = await createMeasurementRecord({ + createdAt: "2026-08-24T15:00:00.000Z", + specimenId: "digital-twin-001", + sessionId: "digital-session-001", + attemptId: 5, + material: "other", + acquisitionContractVersion: "digital-twin-acquisition-1", + softwareRevision: REVISION, + fingerprintAlgorithmVersion: analysis.fingerprint.algorithmVersion, + setup: { + fixedSetup: true, + microphoneDistanceCm: 20, + striker: "digital impulse", + strikeLocation: "digital coordinate 0", + supportCondition: "digital fixed boundary", + }, + station: { + stationId: "digital-station-001", + deviceDescription: "deterministic synthetic acoustic twin", + operatingSystem: "ci", + runtime: "vitest", + microphoneDescription: null, + captureSettings: { sampleRate: SAMPLE_RATE, channelCount: 1 }, + }, + fingerprint: analysis.fingerprint, + }); + expect(await verifyMeasurementRecord(measurement)).toBe(true); + + const derivation = await createDerivationRecord({ + measurementId: measurement.measurementId, + kind: "renderer", + algorithmVersion: "modal-renderer-current", + configDigest: await contentDigest({ renderer: "default" }), + artifactDigest: await contentDigest(Array.from(reconstructed.slice(0, 4096))), + createdAt: "2026-08-24T15:01:00.000Z", + }); + expect(await verifyDerivationRecord(derivation)).toBe(true); + + const atlas = await createAtlasRecord({ + createdAt: "2026-08-24T15:02:00.000Z", + contributor: null, + specimen: { + specimenId: "digital-twin-001", + label: "CI digital twin", + objectFamily: "synthetic-reference", + material: "unknown", + publicDescription: "Non-physical qualification artifact", + }, + measurements: [{ measurementId: measurement.measurementId, derivationIds: [derivation.derivationId] }], + }); + expect(atlas.rawMicrophoneSamplesIncluded).toBe(false); + expect(await verifyAtlasRecord(atlas)).toBe(true); + }); +}); From c13f778750de61459878ad56bf0255a07efdfa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:16:24 -0700 Subject: [PATCH 022/101] Align Gate A documentation with frozen 12-specimen campaign --- docs/GATE_A.md | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/GATE_A.md b/docs/GATE_A.md index 8045cd56..a0a2c76d 100644 --- a/docs/GATE_A.md +++ b/docs/GATE_A.md @@ -1,6 +1,6 @@ # Gate A2 — physical acoustic validity -Gate A2 is the first physical release gate. It must pass before the measured modal representation is treated as physically validated. +Gate A2 is the first physical release gate. It must pass before the measured modal representation is treated as physically validated. The digital-twin lane is supporting stress validation only and is never release-gate equivalent. ## Protocol @@ -12,7 +12,26 @@ Acquisition-quality failures do not enter the five-attempt experiment and may be This rule eliminates retry-until-success and optional-stopping paths. The experiment is defined by the first five qualified physical attempts, not by the first five successful analyses. -The initial release set is: bell, wine glass, metal bowl, glass bottle, ceramic mug. Equivalent strongly resonant specimens are acceptable if the final passing set still contains distinct metal, glass, and ceramic specimens. +## Frozen campaign cohort + +The canonical first empirical campaign contains exactly twelve precommitted slots. The six release-core slots are: + +- `core-metal-1` and `core-metal-2`, from two distinct metal object families; +- `core-glass-1` and `core-glass-2`, from two distinct glass object families; +- `core-ceramic-1` and `core-ceramic-2`, from two distinct ceramic object families. + +The six challenge slots are: + +- `challenge-short-decay` — strong damping / short audible decay; +- `challenge-broad` — weak or broad resonant structure; +- `challenge-coupled` — heterogeneous or coupled multi-part structure; +- `challenge-high-q` — low damping / long ringdown, intentionally distinct from modal degeneracy; +- `challenge-low-snr` — weakly radiating or near the acquisition SNR floor; +- `challenge-degenerate` — symmetry, near-symmetry, close modes, or strike-location sensitivity. + +Specimen selection is made from a finite non-acoustic candidate register before production audition. The register records physical identity, family, material, safety/support facts, markable strike location, eligible slots, and exclusions; it must not contain fingerprints, frequencies, decay estimates, mode counts, acoustic scores, or audition rankings. A selected specimen binds slot and inventory identity in its `specimenId`, for example `core-metal-1--inv-003`. + +The campaign manifest is frozen before the first production strike. Its exact bytes and campaign signature are retained. Collection order is then derived deterministically from `SHA-256("gate-a2-order-v1||")`, sorting release-core and challenge cohorts separately and interleaving them beginning with release-core. The order is recorded before collection. For every qualified attempt record the native sample rate, actual microphone-processing settings, capture-quality diagnostics, and one terminal analytical outcome: @@ -23,7 +42,7 @@ Raw microphone PCM remains local. ## Frozen release contract — `gate-a-2` -These thresholds are fixed before the local five-specimen Gate A2 dataset is accepted and must not be relaxed after observing measurements. +These thresholds are fixed before the campaign dataset is accepted and must not be relaxed after observing measurements. A specimen session passes only when all of the following hold: @@ -43,9 +62,9 @@ Attempt 1 is never substituted. If attempt 1 fails analysis, the session cannot The exported recurrence rows and aggregate drift are caches for audit and display. The parser requires them to agree with recomputation from the retained qualified-attempt fingerprints, and the release evaluator uses the recomputed values as its source of truth. -Gate A2 passes at release level when all imported sessions come from one software revision and one fingerprint algorithm version, at least **five distinct normalized `specimenId` values** have passing sessions, and the passing specimen set contains metal, glass, and ceramic. Multiple passing sessions for one physical specimen still count as one specimen. Changing only an object label cannot increase release distinctness. Duplicate session IDs invalidate the release verdict. Reusing one normalized `specimenId` with conflicting material classes also invalidates the release-level verdict, including when the conflicting session itself fails physically. +Gate A2 passes at release level when all imported sessions come from one software revision and one fingerprint algorithm version, at least **five distinct normalized `specimenId` values** have passing sessions, and the passing specimen set contains metal, glass, and ceramic. The empirical campaign adds a stronger accounting requirement: all twelve precommitted specimen outcomes must be present or explicitly accounted before canonical adjudication. Multiple passing sessions for one physical specimen still count as one specimen. Changing only an object label cannot increase release distinctness. Duplicate session IDs invalidate the release verdict. Reusing one normalized `specimenId` with conflicting material classes also invalidates the release-level verdict, including when the conflicting session itself fails physically. -The drift bounds are release criteria for repeatable structure. They are not a general perceptual similarity metric. +The drift bounds are release criteria for repeatable structure under the frozen setup. They are not a nuisance-invariance result, a general perceptual similarity metric, an object-identity probability, or a material-identification claim. ## Session-fatal errors @@ -55,13 +74,13 @@ A true session-level internal error is different. The validation lab does not pe ## Evidence and algorithm versioning -Gate A2 uses `validation-evidence-5` with `schemaVersion: 5` and `gateAContractVersion: "gate-a-2"`. Schema v5 requires the exact software commit, and release-level evidence must use one revision and one fingerprint algorithm version. `er-dsp-2` is the canonical estimator for the new physical collection cycle; it adds the frozen -60 dB relative-amplitude measurement-support floor. Historical `er-dsp-1` bundles remain readable for audit but must not be mixed with `er-dsp-2` evidence in one session or release evaluation. +Gate A2 uses `validation-evidence-5` with `schemaVersion: 5` and `gateAContractVersion: "gate-a-2"`. Schema v5 requires the exact software commit, and release-level evidence must use one revision and one fingerprint algorithm version. `er-dsp-2` is the canonical estimator for this physical collection cycle; it includes the frozen -60 dB relative-amplitude measurement-support floor. Historical `er-dsp-1` bundles remain readable for audit but must not be mixed with `er-dsp-2` evidence in one session or release evaluation. Schema v4 was superseded before physical release collection because it lacked implementation provenance; the older record-only Gate A1 format is also superseded because it could not represent qualified analytical failures without selection bias. -## External evidence +## External and digital-twin evidence -External cross-field recurrence is supporting evidence only. It does not satisfy Gate A2 because listener position and other acquisition conditions vary across those measurements. External results must never be used to tune `gate-a-2` after the fact. +External cross-field recurrence and deterministic digital twins are supporting evidence only. Neither satisfies Gate A2 because neither proves the exact physical microphone/transducer/object path under the frozen campaign. They may be used to find software defects, exercise nuisance regimes, and compare future estimators, but must never be imported into the canonical Release Console as physical campaign evidence or used to tune `gate-a-2` after observing campaign data. ## Failure handling From 01e2e5e4ce889251abf8fcd1c85a5f9986a4d43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:16:43 -0700 Subject: [PATCH 023/101] Align Gate B docs with canonical fixed-panel plan --- docs/GATE_B.md | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/GATE_B.md b/docs/GATE_B.md index 95710da9..d88313d5 100644 --- a/docs/GATE_B.md +++ b/docs/GATE_B.md @@ -1,41 +1,60 @@ # Gate B — modal reconstruction -Gate B asks whether the estimated modal fingerprint is sufficient to reconstruct the perceptual identity of a struck physical specimen. +Gate B asks whether the estimated modal fingerprint is sufficient to reconstruct the perceptual identity of a struck physical specimen. It is post-collection and microphone-free. Digital-twin rendering tests are supporting diagnostics only and are not substitutes for blinded human judgments. The baseline renderer is deliberately constrained. Each accepted mode contributes a sinusoid at its estimated frequency with its measured exponential decay. Relative modal amplitudes use the fixed renderer transform, a short attack suppresses synthesis clicks, and the final waveform is normalized. No recorded audio is mixed into the reconstruction. -## Blinded protocol +## Eligibility and canonical target selection Only physical specimens with a passing Gate A2 session are eligible. Gate A2 passes only after exactly five qualified attempts all succeed under the frozen physical contract. Gate B is grouped by stable `specimenId`, not by display label. The review target is the **fifth qualified attempt** of one passing Gate A2 session for that specimen. Each review stores the object label, session ID, and attempt ID. The owning evidence bundle binds that session to its `specimenId`; a review targeting another session, another attempt, or an analytically failed attempt does not count. -If one specimen has more than one passing Gate A2 session, all counted Gate B reviewers for that specimen must hear the **same single session/attempt target**. Reviews from different passing sessions are never pooled. If submitted Gate B reviews for one specimen span multiple eligible targets, that specimen's Gate B state remains OPEN. A label alias on another session does not create another Gate B specimen. +Before any Gate B judgment is collected, create and freeze a `gate-b-plan-1` artifact from the canonical Gate A2 verdict. Exactly five specimens are selected deterministically: + +1. the lowest deterministic selection digest among passing metal specimens; +2. the lowest deterministic selection digest among passing glass specimens; +3. the lowest deterministic selection digest among passing ceramic specimens; +4. the two lowest deterministic selection digests among the remaining passing specimens. + +The plan binds the exact Gate A software revision, the five `(specimenId, sessionId, attemptId)` targets, and exactly two normalized reviewer IDs. Target selection is not changed after listening begins. + +If one specimen has more than one passing Gate A2 session, the plan binds one exact session/attempt target. Reviews from different passing sessions are never pooled. A label alias on another session does not create another Gate B specimen. -A reviewer starts a randomized A/B trial and sees only `A` and `B`; the application stores the hidden original/reconstruction presentation order for audit. Reviewers score: +## Blinded protocol + +Exactly the two reviewer IDs frozen in the plan must each review all five canonical targets, yielding exactly ten canonical judgments. A reviewer starts a randomized A/B trial and sees only `A` and `B`; the application stores the hidden original/reconstruction presentation order for audit. Reviewers score: - same-specimen identity, 1–5; - brightness match, 1–5; - decay-character match, 1–5; - artifact severity, 1–5, where 1 means none and 5 means severe. -At least two distinct reviewer IDs are required per specimen on the selected target. Reviewer IDs are normalized for counting, so capitalization or surrounding whitespace cannot make one reviewer count twice. An unblinded judgment does not count toward the release contract. A reviewer/target judgment is immutable once submitted; conflicting repeated submissions are rejected by the evidence parser and merge path. +Reviewer IDs are normalized for counting, so capitalization or surrounding whitespace cannot make one reviewer count twice. An unblinded judgment does not count toward the release contract. A reviewer/target judgment is immutable once submitted; conflicting repeated submissions are rejected by the evidence parser and merge path. -## Frozen release contract — `gate-b-1` +Extra exploratory judgments may be retained only if they are explicitly classified as non-canonical. They must never alter the canonical ten-judgment release verdict. -These thresholds are fixed before release listening data is collected. +## Frozen release contract — `gate-b-1` with `gate-b-plan-1` -An eligible specimen passes when: +These thresholds and the sampling matrix are fixed before release listening data is collected. -- exactly one passing Gate A2 session/attempt target is represented by its counted review set; -- at least two blinded reviewers are present on that target; +An eligible canonical specimen passes when: + +- it is one of the five targets frozen in the Gate B plan; +- both and only the two fixed reviewers have one blinded judgment on that exact target; - median same-specimen identity is at least 4/5; - median brightness match is at least 3/5; - median decay-character match is at least 3/5; - median artifact severity is at most 2/5. -Gate B passes when at least five Gate A2 specimens have eligible blinded reviews, at least four specimens pass, and the passing reconstruction set contains metal, glass, and ceramic examples. +Canonical Gate B passes only when: + +- all five frozen targets are represented; +- all ten required judgments are present; +- no required reviewer/target cell is missing or substituted; +- at least four of the five specimens pass the score thresholds; and +- the passing reconstruction set contains metal, glass, and ceramic examples. -The numerical regression suite verifies deterministic output, attack continuity, measured decay preservation, and Nyquist filtering. Those checks remain necessary and remain insufficient for Gate B. +The numerical regression suite verifies deterministic output, attack continuity, measured decay preservation, and Nyquist filtering. Digital-twin reconstruction tests additionally exercise objective software behavior. Those checks remain necessary and remain insufficient for Gate B. -Failed listening results remain part of the evidence. Do not change scoring thresholds, renderer parameters, specimen IDs, or review targets solely to convert the existing release set into a pass; a renderer revision requires a new algorithm/versioned evaluation cycle. +Failed listening results remain part of the evidence. Do not change scoring thresholds, renderer parameters, specimen IDs, reviewer IDs, target selection, or the sampling matrix solely to convert the existing release set into a pass; a renderer or protocol revision requires a new versioned evaluation cycle. From f6769ce8c7e3686dcade6f66995a744992da686e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:17:03 -0700 Subject: [PATCH 024/101] Align Gate C docs with canonical 4x2x2 matrix --- docs/GATE_C.md | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/GATE_C.md b/docs/GATE_C.md index 7475aded..ea31968a 100644 --- a/docs/GATE_C.md +++ b/docs/GATE_C.md @@ -1,6 +1,6 @@ # Gate C — playable specimen identity -Gate C tests whether one measured physical specimen can function as a coherent chromatic instrument without losing its acoustic identity. +Gate C tests whether one measured physical specimen can function as a coherent chromatic instrument without losing its acoustic identity. It is post-Gate-B and microphone-free. Digital-twin instrument tests are supporting diagnostics only and are not substitutes for device listening. The instrument chooses one anchor resonance from the fingerprint. A requested note defines one global frequency ratio, and every modal frequency is multiplied by that ratio. Relative amplitudes and measured decay constants remain unchanged. The specimen therefore retains one modal geometry across notes. @@ -17,11 +17,23 @@ The validation evidence keeps timing quantities separate: These are diagnostics. Their sum is not claimed as measured acoustic end-to-end latency. Device listening or loopback measurement remains necessary. -## Device protocol +## Eligibility and canonical plan + +Only specimens that pass canonical Gate B are eligible. Gate B selects one exact `(sessionId, attemptId)` target per passing `specimenId`, and that target is the fifth qualified attempt of a passing Gate A2 session. Gate C inherits it exactly. A device review for another passing Gate A2 session or another attempt of the same specimen does not count, even when its human-readable label is identical. -Only specimens that pass Gate B are eligible. Gate B selects one exact `(sessionId, attemptId)` target per passing `specimenId`, and that target is the fifth qualified attempt of a passing Gate A2 session. Gate C inherits it exactly. A device review for another passing Gate A2 session or another attempt of the same specimen does not count, even when its human-readable label is identical. +Before any Gate C judgment is collected, create and freeze a `gate-c-plan-1` artifact from the canonical Gate B verdict. It binds: -Play the chromatic instrument across the useful range and record the judgment on the device producing the audio. Each review records a stable device ID/class and scores: +- exactly four deterministically selected Gate-B-passing specimen targets; +- exactly two normalized reviewer IDs; +- exactly two distinct device IDs/classes; +- at least one mobile device among those two; +- the exact inherited Gate B `(sessionId, attemptId)` for every specimen. + +The canonical matrix is therefore exactly **4 specimens × 2 reviewers × 2 devices = 16 judgments**. No required cell may be replaced after listening begins. + +## Device protocol + +Each of the two fixed reviewers evaluates each of the four fixed specimens on both fixed devices. Play the chromatic instrument across the useful range and record the judgment on the device producing the audio. Each review records its stable device ID/class and scores: - identity across the useful note range, 1–5; - timbre continuity across adjacent notes, 1–5; @@ -32,22 +44,29 @@ Include low and high transpositions where Nyquist filtering removes modes. A rev Repeated submissions from the same normalized reviewer ID, device ID, and selected target count once defensively; the normal UI and evidence merge path treat that logical judgment as immutable and reject conflicting repeated submissions. A device ID must map consistently to one device class across the release evidence; conflicting desktop/mobile/tablet claims for the same normalized device ID invalidate Gate C. -## Frozen release contract — `gate-c-1` +Extra exploratory device judgments may be retained only if explicitly classified as non-canonical. They must never alter the canonical sixteen-judgment verdict. + +## Frozen release contract — `gate-c-1` with `gate-c-plan-1` -These thresholds are fixed before release device-listening data is collected. +These thresholds and the sampling matrix are fixed before release device-listening data is collected. -An eligible specimen passes when: +An eligible specimen passes only when all four of its canonical reviewer/device judgments are present and: -- every counted review targets the exact Gate B-selected session/attempt for that specimen; +- every judgment targets the exact Gate B-selected session/attempt; - median identity across the range is at least 4/5; - median timbre continuity is at least 4/5; - median useful range spans at least 12 semitones; -- every counted device review for that specimen accepts note-on latency. +- every one of the four judgments accepts note-on latency. -Gate C passes when at least four Gate B specimens have eligible device reviews and all four pass. **Device diversity is computed only from reviews attached to Gate C-passing specimens:** those passing-specimen reviews must cover at least two distinct normalized device IDs, and at least one of those devices must be consistently identified as mobile. A mobile review attached only to a specimen that fails Gate C cannot satisfy the release mobile/device-diversity requirement. +Canonical Gate C passes only when: -Device-class consistency is checked across all eligible selected-target reviews, including reviews for specimens that fail Gate C. A contradictory device identity remains an evidence-integrity failure even when that review does not contribute to the passing-specimen device count. +- all four frozen specimen targets are represented; +- all sixteen required reviewer/device/target cells are present; +- both planned devices are represented for every specimen; +- both planned reviewers are represented on both devices for every specimen; +- the planned devices include a mobile device; and +- all four specimens pass. -Software scheduling telemetry, modal-ratio preservation, and browser latency reports remain supporting diagnostics. They do not independently satisfy Gate C. +Software scheduling telemetry, modal-ratio preservation, browser latency reports, numerical synthesis tests, and digital-twin transposition tests remain supporting diagnostics. They do not independently satisfy Gate C. -A renderer or realtime-engine revision made in response to failure must be versioned and re-evaluated; do not alter `gate-c-1`, specimen identity, or target provenance solely to fit an observed release set. +A renderer or realtime-engine revision made in response to failure must be versioned and re-evaluated; do not alter `gate-c-1`, the canonical plan, specimen identity, reviewer IDs, device IDs, or target provenance solely to fit an observed release set. From 5b8d2a65b2a11cba92299dc8ca07b80a418f6ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:17:38 -0700 Subject: [PATCH 025/101] Tighten frozen campaign slot authoring rules --- apps/web/src/campaignAuthoring.ts | 36 ++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/web/src/campaignAuthoring.ts b/apps/web/src/campaignAuthoring.ts index 1de28988..bc4cf4f5 100644 --- a/apps/web/src/campaignAuthoring.ts +++ b/apps/web/src/campaignAuthoring.ts @@ -47,9 +47,9 @@ export const RECOMMENDED_CAMPAIGN_SLOTS: readonly CampaignAuthoringSlot[] = [ { slotId: "challenge-short-decay", cohort: "challenge", suggestedMaterial: "wood", selectionCriterion: "strongly damped object with short audible decay" }, { slotId: "challenge-broad", cohort: "challenge", suggestedMaterial: "plastic", selectionCriterion: "weak or broad resonant structure" }, { slotId: "challenge-coupled", cohort: "challenge", suggestedMaterial: "composite", selectionCriterion: "heterogeneous or coupled multi-part object" }, - { slotId: "challenge-high-q", cohort: "challenge", suggestedMaterial: "glass", selectionCriterion: "high-Q object with long decay or closely spaced peaks" }, + { slotId: "challenge-high-q", cohort: "challenge", suggestedMaterial: "glass", selectionCriterion: "low-damping high-Q object with long ringdown; do not select on close-mode or degeneracy behavior" }, { slotId: "challenge-low-snr", cohort: "challenge", suggestedMaterial: "other", selectionCriterion: "small or weakly radiating object near the microphone/SNR floor" }, - { slotId: "challenge-degenerate", cohort: "challenge", suggestedMaterial: "other", selectionCriterion: "geometry likely to produce near-degenerate or strike-location-sensitive modes" }, + { slotId: "challenge-degenerate", cohort: "challenge", suggestedMaterial: "other", selectionCriterion: "geometry likely to produce symmetry, near-degenerate modes, or strike-location sensitivity" }, ] as const; export function createRecommendedCampaignDraft( @@ -79,6 +79,10 @@ function nonempty(value: string): boolean { return value.trim().length > 0; } +function normalized(value: string): string { + return value.trim().toLocaleLowerCase("en-US"); +} + function protocolFromDraft(specimen: CampaignAuthoringSpecimen): FixedSetupProtocol { return { fixedSetup: true, @@ -89,6 +93,15 @@ function protocolFromDraft(specimen: CampaignAuthoringSpecimen): FixedSetupProto }; } +function enforceDistinctCoreFamilies(draft: CampaignAuthoringDraft, errors: string[]): void { + for (const material of ["metal", "glass", "ceramic"] as const) { + const core = draft.specimens.filter((specimen) => specimen.cohort === "release-core" && specimen.material === material); + if (core.length !== 2) continue; + const families = new Set(core.map((specimen) => normalized(specimen.objectFamily)).filter(Boolean)); + if (families.size !== 2) errors.push(`release-core ${material} slots must use two distinct object families`); + } +} + export function buildEmpiricalCampaignFromDraft( draft: CampaignAuthoringDraft, createdAt: string, @@ -110,12 +123,8 @@ export function buildEmpiricalCampaignFromDraft( if (expectedSlot === undefined) { errors.push(`${field}: slot is outside the recommended campaign design`); } else { - if (specimen.slotId !== expectedSlot.slotId) { - errors.push(`${field}: expected frozen slot ${expectedSlot.slotId}`); - } - if (specimen.cohort !== expectedSlot.cohort) { - errors.push(`${field}: cohort must remain ${expectedSlot.cohort}`); - } + if (specimen.slotId !== expectedSlot.slotId) errors.push(`${field}: expected frozen slot ${expectedSlot.slotId}`); + if (specimen.cohort !== expectedSlot.cohort) errors.push(`${field}: cohort must remain ${expectedSlot.cohort}`); if (expectedSlot.cohort === "release-core" && specimen.material !== expectedSlot.suggestedMaterial) { errors.push(`${field}: release-core material must remain ${expectedSlot.suggestedMaterial}`); } @@ -132,14 +141,15 @@ export function buildEmpiricalCampaignFromDraft( if (!Number.isInteger(specimen.targetSessions) || specimen.targetSessions <= 0) { errors.push(`${field}: target sessions must be a positive integer`); } - const normalized = specimen.specimenId.trim().toLocaleLowerCase("en-US"); - if (normalized.length > 0) { - if (seen.has(normalized)) errors.push(`${field}: duplicate specimen ID ${specimen.specimenId.trim()}`); - seen.add(normalized); + const normalizedId = normalized(specimen.specimenId); + if (normalizedId.length > 0) { + if (seen.has(normalizedId)) errors.push(`${field}: duplicate specimen ID ${specimen.specimenId.trim()}`); + seen.add(normalizedId); } }); + enforceDistinctCoreFamilies(draft, errors); - if (errors.length > 0) return { ok: false, errors }; + if (errors.length > 0) return { ok: false, errors: [...new Set(errors)] }; const candidate: EmpiricalCampaignV1 = { schemaVersion: 1, From f0006cf7869f4cdf66dd9fbf0ab8721b73ccc6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:18:03 -0700 Subject: [PATCH 026/101] Test distinct release-core family enforcement --- apps/web/src/campaignAuthoring.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/web/src/campaignAuthoring.test.ts b/apps/web/src/campaignAuthoring.test.ts index 8527eee9..ed0d3050 100644 --- a/apps/web/src/campaignAuthoring.test.ts +++ b/apps/web/src/campaignAuthoring.test.ts @@ -30,6 +30,7 @@ describe("campaign authoring", () => { expect(RECOMMENDED_CAMPAIGN_SLOTS.slice(0, 6).map((slot) => slot.suggestedMaterial)).toEqual([ "metal", "metal", "glass", "glass", "ceramic", "ceramic", ]); + expect(RECOMMENDED_CAMPAIGN_SLOTS.find((slot) => slot.slotId === "challenge-high-q")?.selectionCriterion).not.toMatch(/closely spaced/i); }); it("prefills only protocol constants that are intentionally standardized", () => { @@ -73,6 +74,19 @@ describe("campaign authoring", () => { if (!slotResult.ok) expect(slotResult.errors).toContain("recommended campaign must contain exactly 12 frozen slots"); }); + it("requires two distinct object families for each release-core material pair", () => { + const complete = completedDraft(); + const sameMetalFamily = { + ...complete, + specimens: complete.specimens.map((specimen, index) => index === 1 + ? { ...specimen, objectFamily: complete.specimens[0]?.objectFamily ?? "family-1" } + : specimen), + }; + const result = buildEmpiricalCampaignFromDraft(sameMetalFamily, "2026-08-18T09:00:00.000Z"); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.errors).toContain("release-core metal slots must use two distinct object families"); + }); + it("builds a parser-valid manifest only after every physical field is specified", () => { const result = buildEmpiricalCampaignFromDraft(completedDraft(), "2026-08-18T09:00:00.000Z"); expect(result.ok).toBe(true); From bb28fd947a1f884586ad5ddecb0073a32352f7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:18:41 -0700 Subject: [PATCH 027/101] Add strict Gate B and Gate C plan parsers --- packages/validation/src/review-plan-parse.ts | 97 ++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/validation/src/review-plan-parse.ts diff --git a/packages/validation/src/review-plan-parse.ts b/packages/validation/src/review-plan-parse.ts new file mode 100644 index 00000000..0de198df --- /dev/null +++ b/packages/validation/src/review-plan-parse.ts @@ -0,0 +1,97 @@ +import type { DeviceClass } from "./types"; +import type { GateBPlanV1, GateCDevicePlan, GateCPlanV1, PlannedReviewTarget } from "./review-plans"; + +export type GateBPlanParseResult = { readonly ok: true; readonly plan: GateBPlanV1 } | { readonly ok: false; readonly error: string }; +export type GateCPlanParseResult = { readonly ok: true; readonly plan: GateCPlanV1 } | { readonly ok: false; readonly error: string }; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function nonempty(value: unknown, field: string): string { + if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${field} must be a non-empty string`); + return value.trim(); +} + +function positiveInteger(value: unknown, field: string): number { + if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new TypeError(`${field} must be a positive integer`); + return value; +} + +function parseTarget(value: unknown, field: string): PlannedReviewTarget { + if (!isRecord(value)) throw new TypeError(`${field} must be an object`); + return { + specimenId: nonempty(value.specimenId, `${field}.specimenId`), + objectLabel: nonempty(value.objectLabel, `${field}.objectLabel`), + sessionId: nonempty(value.sessionId, `${field}.sessionId`), + attemptId: positiveInteger(value.attemptId, `${field}.attemptId`), + }; +} + +function parseFixedReviewers(value: unknown, field: string): readonly [string, string] { + if (!Array.isArray(value) || value.length !== 2) throw new TypeError(`${field} must contain exactly two reviewer IDs`); + const reviewers: [string, string] = [nonempty(value[0], `${field}[0]`), nonempty(value[1], `${field}[1]`)]; + if (reviewers[0].toLocaleLowerCase("en-US") === reviewers[1].toLocaleLowerCase("en-US")) throw new TypeError(`${field} reviewer IDs must be distinct`); + return reviewers; +} + +function uniqueTargets(targets: readonly PlannedReviewTarget[], expected: number, field: string): void { + if (targets.length !== expected) throw new TypeError(`${field} must contain exactly ${expected} targets`); + const specimens = new Set(targets.map((target) => target.specimenId.trim().toLocaleLowerCase("en-US"))); + const targetKeys = new Set(targets.map((target) => `${target.sessionId}\u0000${target.attemptId}`)); + if (specimens.size !== expected) throw new TypeError(`${field} specimen IDs must be distinct`); + if (targetKeys.size !== expected) throw new TypeError(`${field} session/attempt targets must be distinct`); +} + +export function parseGateBPlan(value: unknown): GateBPlanParseResult { + try { + if (!isRecord(value)) throw new TypeError("Gate B plan must be an object"); + if (value.schemaVersion !== 1) throw new TypeError("Gate B plan schemaVersion must be 1"); + if (value.planContractVersion !== "gate-b-plan-1") throw new TypeError("Gate B plan contract must be gate-b-plan-1"); + const gateARevision = nonempty(value.gateARevision, "gateARevision"); + if (!/^[0-9a-f]{40}$/.test(gateARevision)) throw new TypeError("gateARevision must be an exact 40-hex Git revision"); + const reviewerIds = parseFixedReviewers(value.reviewerIds, "reviewerIds"); + if (!Array.isArray(value.targets)) throw new TypeError("targets must be an array"); + const targets = value.targets.map((target, index) => parseTarget(target, `targets[${index}]`)); + uniqueTargets(targets, 5, "targets"); + return { ok: true, plan: { schemaVersion: 1, planContractVersion: "gate-b-plan-1", gateARevision, reviewerIds, targets } }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } +} + +export function parseGateBPlanJson(json: string): GateBPlanParseResult { + try { return parseGateBPlan(JSON.parse(json) as unknown); } + catch (error) { return { ok: false, error: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` }; } +} + +function parseDevice(value: unknown, field: string): GateCDevicePlan { + if (!isRecord(value)) throw new TypeError(`${field} must be an object`); + const deviceClass = nonempty(value.deviceClass, `${field}.deviceClass`) as DeviceClass; + if (!["desktop", "mobile", "tablet", "other"].includes(deviceClass)) throw new TypeError(`${field}.deviceClass is invalid`); + return { deviceId: nonempty(value.deviceId, `${field}.deviceId`), deviceClass }; +} + +export function parseGateCPlan(value: unknown): GateCPlanParseResult { + try { + if (!isRecord(value)) throw new TypeError("Gate C plan must be an object"); + if (value.schemaVersion !== 1) throw new TypeError("Gate C plan schemaVersion must be 1"); + if (value.planContractVersion !== "gate-c-plan-1") throw new TypeError("Gate C plan contract must be gate-c-plan-1"); + const reviewerIds = parseFixedReviewers(value.reviewerIds, "reviewerIds"); + if (!Array.isArray(value.devices) || value.devices.length !== 2) throw new TypeError("devices must contain exactly two planned devices"); + const devices: [GateCDevicePlan, GateCDevicePlan] = [parseDevice(value.devices[0], "devices[0]"), parseDevice(value.devices[1], "devices[1]")]; + if (devices[0].deviceId.trim().toLocaleLowerCase("en-US") === devices[1].deviceId.trim().toLocaleLowerCase("en-US")) throw new TypeError("planned device IDs must be distinct"); + if (!devices.some((device) => device.deviceClass === "mobile")) throw new TypeError("Gate C plan requires a mobile device"); + if (!Array.isArray(value.targets)) throw new TypeError("targets must be an array"); + const targets = value.targets.map((target, index) => parseTarget(target, `targets[${index}]`)); + uniqueTargets(targets, 4, "targets"); + return { ok: true, plan: { schemaVersion: 1, planContractVersion: "gate-c-plan-1", reviewerIds, devices, targets } }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } +} + +export function parseGateCPlanJson(json: string): GateCPlanParseResult { + try { return parseGateCPlan(JSON.parse(json) as unknown); } + catch (error) { return { ok: false, error: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` }; } +} From 6f9a0fe0b2d06b804ac268a32911591e9096cf97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:20:28 -0700 Subject: [PATCH 028/101] Export canonical review plan parsers --- packages/validation/src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 4b113fca..c467622e 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -44,6 +44,14 @@ export { type GateCPlanV1, type PlannedReviewTarget, } from "./review-plans"; +export { + parseGateBPlan, + parseGateBPlanJson, + parseGateCPlan, + parseGateCPlanJson, + type GateBPlanParseResult, + type GateCPlanParseResult, +} from "./review-plan-parse"; export { deriveEvidenceRecurrence, deriveMedianModalDriftCents, From ec3947cba79f80e3c47c840a553c23c8bc2003e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:20:44 -0700 Subject: [PATCH 029/101] Test strict canonical review plan parsing --- .../validation/test/review-plan-parse.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/validation/test/review-plan-parse.test.ts diff --git a/packages/validation/test/review-plan-parse.test.ts b/packages/validation/test/review-plan-parse.test.ts new file mode 100644 index 00000000..6281de11 --- /dev/null +++ b/packages/validation/test/review-plan-parse.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import { parseGateBPlan, parseGateCPlan } from "../src"; +import { SOFTWARE_REVISION } from "./helpers"; + +const targets = Array.from({ length: 5 }, (_, index) => ({ + specimenId: `specimen-${index + 1}`, + objectLabel: `object ${index + 1}`, + sessionId: `session-${index + 1}`, + attemptId: 5, +})); + +describe("canonical review plan parsers", () => { + it("accepts exactly five distinct Gate B targets and two distinct reviewers", () => { + const result = parseGateBPlan({ + schemaVersion: 1, + planContractVersion: "gate-b-plan-1", + gateARevision: SOFTWARE_REVISION, + reviewerIds: ["reviewer-a", "reviewer-b"], + targets, + }); + expect(result.ok).toBe(true); + }); + + it("rejects duplicate Gate B specimens and non-exact matrix size", () => { + const result = parseGateBPlan({ + schemaVersion: 1, + planContractVersion: "gate-b-plan-1", + gateARevision: SOFTWARE_REVISION, + reviewerIds: ["reviewer-a", "reviewer-b"], + targets: [...targets.slice(0, 4), { ...targets[4], specimenId: targets[0]?.specimenId }], + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatch(/distinct/); + }); + + it("accepts exactly four Gate C targets, two reviewers, and two devices including mobile", () => { + const result = parseGateCPlan({ + schemaVersion: 1, + planContractVersion: "gate-c-plan-1", + reviewerIds: ["reviewer-c", "reviewer-d"], + devices: [ + { deviceId: "desktop-1", deviceClass: "desktop" }, + { deviceId: "phone-1", deviceClass: "mobile" }, + ], + targets: targets.slice(0, 4), + }); + expect(result.ok).toBe(true); + }); + + it("rejects Gate C plans without a mobile device", () => { + const result = parseGateCPlan({ + schemaVersion: 1, + planContractVersion: "gate-c-plan-1", + reviewerIds: ["reviewer-c", "reviewer-d"], + devices: [ + { deviceId: "desktop-1", deviceClass: "desktop" }, + { deviceId: "desktop-2", deviceClass: "desktop" }, + ], + targets: targets.slice(0, 4), + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatch(/mobile/); + }); +}); From 640838b02ad126ab76e1eac2909cfee10f249b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:21:01 -0700 Subject: [PATCH 030/101] Add authoritative canonical v8 release evaluator --- packages/validation/src/canonical-release.ts | 74 ++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/validation/src/canonical-release.ts diff --git a/packages/validation/src/canonical-release.ts b/packages/validation/src/canonical-release.ts new file mode 100644 index 00000000..e56580ce --- /dev/null +++ b/packages/validation/src/canonical-release.ts @@ -0,0 +1,74 @@ +import { buildReleaseVerdictForRevision } from "./current-release"; +import { evaluateCanonicalGateB, evaluateCanonicalGateC, type GateBPlanV1, type GateCPlanV1 } from "./review-plans"; +import type { EmpiricalCampaignProgress } from "./campaign"; +import type { GateBReview, GateCReview, ReleaseVerdict, ValidationEvidenceV5 } from "./types"; + +export interface CanonicalReleaseVerdictV1 { + readonly schemaVersion: 1; + readonly canonicalReleaseContractVersion: "canonical-release-1"; + readonly createdAt: string; + readonly authorizedSoftwareRevision: string; + readonly base: ReleaseVerdict; + readonly campaignAccounted: boolean; + readonly gateBPlanPresent: boolean; + readonly gateCPlanPresent: boolean; + readonly gateB: ReturnType | null; + readonly gateC: ReturnType | null; + readonly releaseReady: boolean; + readonly reasons: readonly string[]; +} + +export function buildCanonicalReleaseVerdictForRevision( + evidence: readonly ValidationEvidenceV5[], + gateBReviews: readonly GateBReview[], + gateCReviews: readonly GateCReview[], + createdAt: string, + expectedSoftwareRevision: string, + campaignProgress: EmpiricalCampaignProgress | undefined, + gateBPlan: GateBPlanV1 | undefined, + gateCPlan: GateCPlanV1 | undefined, +): CanonicalReleaseVerdictV1 { + const base = buildReleaseVerdictForRevision( + evidence, + gateBReviews, + gateCReviews, + createdAt, + expectedSoftwareRevision, + ); + const reasons: string[] = []; + const campaignAccounted = campaignProgress?.collectionComplete === true; + if (!campaignAccounted) reasons.push("the complete precommitted empirical campaign is not accounted for"); + if (!base.gateA.passed) reasons.push("Gate A2 has not passed under the authorized revision"); + if (gateBPlan === undefined) reasons.push("canonical Gate B plan is missing"); + if (gateCPlan === undefined) reasons.push("canonical Gate C plan is missing"); + + const gateB = gateBPlan === undefined + ? null + : evaluateCanonicalGateB(gateBPlan, base.gateA, gateBReviews); + if (gateB !== null && !gateB.passed) reasons.push(...gateB.reasons.map((reason) => `Gate B: ${reason}`)); + + const gateC = gateCPlan === undefined || gateB === null + ? null + : evaluateCanonicalGateC(gateCPlan, gateB.verdict, gateCReviews); + if (gateC !== null && !gateC.passed) reasons.push(...gateC.reasons.map((reason) => `Gate C: ${reason}`)); + + const releaseReady = campaignAccounted + && base.gateA.passed + && gateB?.passed === true + && gateC?.passed === true; + + return { + schemaVersion: 1, + canonicalReleaseContractVersion: "canonical-release-1", + createdAt, + authorizedSoftwareRevision: expectedSoftwareRevision, + base, + campaignAccounted, + gateBPlanPresent: gateBPlan !== undefined, + gateCPlanPresent: gateCPlan !== undefined, + gateB, + gateC, + releaseReady, + reasons: [...new Set(reasons)], + }; +} From f7d4e7b397c92fa53a679cfa30b702794f378e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:21:22 -0700 Subject: [PATCH 031/101] Export authoritative canonical release evaluator --- packages/validation/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index c467622e..5a6c1591 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -11,6 +11,10 @@ export { buildReleaseVerdict, buildReleaseVerdictForRevision, } from "./current-release"; +export { + buildCanonicalReleaseVerdictForRevision, + type CanonicalReleaseVerdictV1, +} from "./canonical-release"; export { empiricalCampaignSignature, evaluateEmpiricalCampaign, From e1ec4ac67ce4f8115c7628982d6630886aff3f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:21:43 -0700 Subject: [PATCH 032/101] Test canonical release cannot bypass frozen plans --- .../validation/test/canonical-release.test.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/validation/test/canonical-release.test.ts diff --git a/packages/validation/test/canonical-release.test.ts b/packages/validation/test/canonical-release.test.ts new file mode 100644 index 00000000..d6eef3d2 --- /dev/null +++ b/packages/validation/test/canonical-release.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from "vitest"; +import { + buildCanonicalReleaseVerdictForRevision, + createGateBPlan, + createGateCPlan, + evaluateCanonicalGateB, + evaluateGateARelease, + type EmpiricalCampaignProgress, + type GateBReview, + type GateCReview, +} from "../src"; +import { evidence, SOFTWARE_REVISION } from "./helpers"; + +function sixEvidence() { + return [ + evidence("metal-a", "metal", { specimenId: "metal-a", softwareRevision: SOFTWARE_REVISION }), + evidence("metal-b", "metal", { specimenId: "metal-b", softwareRevision: SOFTWARE_REVISION }), + evidence("glass-a", "glass", { specimenId: "glass-a", softwareRevision: SOFTWARE_REVISION }), + evidence("glass-b", "glass", { specimenId: "glass-b", softwareRevision: SOFTWARE_REVISION }), + evidence("ceramic-a", "ceramic", { specimenId: "ceramic-a", softwareRevision: SOFTWARE_REVISION }), + evidence("ceramic-b", "ceramic", { specimenId: "ceramic-b", softwareRevision: SOFTWARE_REVISION }), + ]; +} + +function progress(complete: boolean): EmpiricalCampaignProgress { + return { + campaignId: "campaign-1", + campaignSignature: "erc1-0000000000000000", + authorizedSoftwareRevision: SOFTWARE_REVISION, + plannedSpecimenCount: 12, + plannedSessionCount: 12, + collectedPlannedSessionCount: complete ? 12 : 11, + conformingCompleteSessionCount: complete ? 12 : 11, + passingSessionCount: 6, + analyticalFailureCount: 0, + unplannedSpecimenIds: [], + materialCoverage: ["metal", "glass", "ceramic"], + collectionComplete: complete, + specimens: [], + reasons: complete ? [] : ["one planned specimen is incomplete"], + }; +} + +function bReviews(plan: Awaited>): GateBReview[] { + return plan.targets.flatMap((target) => plan.reviewerIds.map((reviewerId, index) => ({ + reviewId: `b-${target.specimenId}-${index}`, + reviewerId, + objectLabel: target.objectLabel, + sessionId: target.sessionId, + attemptId: target.attemptId, + blinded: true, + presentationOrder: index === 0 ? "original-model" as const : "model-original" as const, + identity: 4 as const, + brightness: 4 as const, + decayCharacter: 4 as const, + artifactSeverity: 2 as const, + }))); +} + +function cReviews(plan: Awaited>): GateCReview[] { + return plan.targets.flatMap((target) => plan.reviewerIds.flatMap((reviewerId) => plan.devices.map((device) => ({ + reviewId: `c-${target.specimenId}-${reviewerId}-${device.deviceId}`, + reviewerId, + objectLabel: target.objectLabel, + sessionId: target.sessionId, + attemptId: target.attemptId, + deviceId: device.deviceId, + deviceClass: device.deviceClass, + identityAcrossRange: 4 as const, + timbreContinuity: 4 as const, + usefulSemitoneSpan: 12, + latencyAcceptable: true, + })))); +} + +describe("canonical release authority", () => { + it("cannot become releaseReady without campaign accounting and both frozen plans", () => { + const result = buildCanonicalReleaseVerdictForRevision( + sixEvidence(), [], [], "2026-08-24T16:00:00.000Z", SOFTWARE_REVISION, progress(false), undefined, undefined, + ); + expect(result.base.gateA.passed).toBe(true); + expect(result.releaseReady).toBe(false); + expect(result.reasons.join("\n")).toMatch(/campaign|Gate B plan|Gate C plan/); + }); + + it("becomes ready only with the exact B and C matrices", async () => { + const evidenceSet = sixEvidence(); + const gateA = evaluateGateARelease(evidenceSet); + const bPlan = await createGateBPlan(gateA, ["b-1", "b-2"]); + const gateBReviews = bReviews(bPlan); + const b = evaluateCanonicalGateB(bPlan, gateA, gateBReviews); + expect(b.passed).toBe(true); + const cPlan = await createGateCPlan(b.verdict, ["c-1", "c-2"], [ + { deviceId: "desktop-1", deviceClass: "desktop" }, + { deviceId: "mobile-1", deviceClass: "mobile" }, + ]); + const gateCReviews = cReviews(cPlan); + + const ready = buildCanonicalReleaseVerdictForRevision( + evidenceSet, + gateBReviews, + gateCReviews, + "2026-08-24T16:00:00.000Z", + SOFTWARE_REVISION, + progress(true), + bPlan, + cPlan, + ); + expect(ready.releaseReady).toBe(true); + expect(ready.gateB?.observedJudgmentCount).toBe(10); + expect(ready.gateC?.observedJudgmentCount).toBe(16); + + const missing = buildCanonicalReleaseVerdictForRevision( + evidenceSet, + gateBReviews, + gateCReviews.slice(0, -1), + "2026-08-24T16:00:00.000Z", + SOFTWARE_REVISION, + progress(true), + bPlan, + cPlan, + ); + expect(missing.releaseReady).toBe(false); + }); +}); From 156462e652d48f505c16de83a86be095014ecd99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:22:34 -0700 Subject: [PATCH 033/101] Make canonical review plans authoritative in Release Console --- apps/web/src/ReleaseApp.tsx | 205 ++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 91 deletions(-) diff --git a/apps/web/src/ReleaseApp.tsx b/apps/web/src/ReleaseApp.tsx index 6393e0e1..1c5c3c54 100644 --- a/apps/web/src/ReleaseApp.tsx +++ b/apps/web/src/ReleaseApp.tsx @@ -1,11 +1,16 @@ import { + buildCanonicalReleaseVerdictForRevision, buildReleaseVerdictForRevision, empiricalCampaignSignature, evaluateEmpiricalCampaign, mergeValidationEvidence, parseEmpiricalCampaignJson, + parseGateBPlanJson, + parseGateCPlanJson, parseValidationEvidenceJson, type EmpiricalCampaignV1, + type GateBPlanV1, + type GateCPlanV1, type ValidationEvidenceV5, } from "@everything-rings/validation"; import { useMemo, useState } from "react"; @@ -35,29 +40,62 @@ function Reasons({ reasons }: { readonly reasons: readonly string[] }) { export function ReleaseApp() { const [evidence, setEvidence] = useState([]); const [campaign, setCampaign] = useState(); + const [gateBPlan, setGateBPlan] = useState(); + const [gateCPlan, setGateCPlan] = useState(); const [errors, setErrors] = useState([]); + const gateBReviews = useMemo(() => evidence.flatMap((bundle) => bundle.gateBReviews), [evidence]); const gateCReviews = useMemo(() => evidence.flatMap((bundle) => bundle.gateCReviews), [evidence]); - const verdict = useMemo( - () => buildReleaseVerdictForRevision(evidence, gateBReviews, gateCReviews, "preview", SOFTWARE_REVISION), - [evidence, gateBReviews, gateCReviews], - ); const campaignProgress = useMemo( () => campaign === undefined ? undefined : evaluateEmpiricalCampaign(campaign, evidence), [campaign, evidence], ); + const baseVerdict = useMemo( + () => buildReleaseVerdictForRevision(evidence, gateBReviews, gateCReviews, "preview", SOFTWARE_REVISION), + [evidence, gateBReviews, gateCReviews], + ); + const canonicalVerdict = useMemo( + () => buildCanonicalReleaseVerdictForRevision( + evidence, + gateBReviews, + gateCReviews, + "preview", + SOFTWARE_REVISION, + campaignProgress, + gateBPlan, + gateCPlan, + ), + [evidence, gateBReviews, gateCReviews, campaignProgress, gateBPlan, gateCPlan], + ); + + function reject(filename: string, error: string): void { + setErrors((current) => [...current.filter((item) => !item.startsWith(`${filename}:`)), `${filename}: ${error}`]); + } async function importCampaign(file: File | undefined): Promise { if (file === undefined) return; const parsed = parseEmpiricalCampaignJson(await file.text()); - if (!parsed.ok) { - setErrors((current) => [...current, `${file.name}: ${parsed.error}`]); - return; - } + if (!parsed.ok) return reject(file.name, parsed.error); setCampaign(parsed.campaign); setErrors((current) => current.filter((error) => !error.startsWith(`${file.name}:`))); } + async function importGateBPlan(file: File | undefined): Promise { + if (file === undefined) return; + const parsed = parseGateBPlanJson(await file.text()); + if (!parsed.ok) return reject(file.name, parsed.error); + setGateBPlan(parsed.plan); + setErrors((current) => current.filter((error) => !error.startsWith(`${file.name}:`))); + } + + async function importGateCPlan(file: File | undefined): Promise { + if (file === undefined) return; + const parsed = parseGateCPlanJson(await file.text()); + if (!parsed.ok) return reject(file.name, parsed.error); + setGateCPlan(parsed.plan); + setErrors((current) => current.filter((error) => !error.startsWith(`${file.name}:`))); + } + async function importFiles(files: FileList | null): Promise { if (files === null) return; const loaded: Array<{ readonly filename: string; readonly evidence: ValidationEvidenceV5 }> = []; @@ -76,29 +114,39 @@ export function ReleaseApp() { continue; } const merged = mergeValidationEvidence(existing, item.evidence); - if (!merged.ok) { - nextErrors.push(`${item.filename}: ${merged.error}`); - continue; - } - bySession.set(item.evidence.sessionId, merged.evidence); + if (!merged.ok) nextErrors.push(`${item.filename}: ${merged.error}`); + else bySession.set(item.evidence.sessionId, merged.evidence); } - setEvidence([...bySession.values()].sort((left, right) => left.object.label.localeCompare(right.object.label))); setErrors(nextErrors); } + function clearAll(): void { + setEvidence([]); + setCampaign(undefined); + setGateBPlan(undefined); + setGateCPlan(undefined); + setErrors([]); + } + function exportVerdict(): void { - const finalVerdict = buildReleaseVerdictForRevision( + const createdAt = new Date().toISOString(); + const finalVerdict = buildCanonicalReleaseVerdictForRevision( evidence, gateBReviews, gateCReviews, - new Date().toISOString(), + createdAt, SOFTWARE_REVISION, + campaignProgress, + gateBPlan, + gateCPlan, ); - downloadJson(`everything-rings-release-verdict-${Date.now()}.json`, { + downloadJson(`everything-rings-canonical-release-verdict-${Date.now()}.json`, { ...finalVerdict, evidenceContractVersion: "validation-evidence-5", gateAContractVersion: "gate-a-2", + gateBPlanContractVersion: "gate-b-plan-1", + gateCPlanContractVersion: "gate-c-plan-1", authorizedCollectionSoftwareRevision: SOFTWARE_REVISION_VALID ? SOFTWARE_REVISION : null, empiricalCampaign: campaign === undefined ? null : { campaignContractVersion: campaign.campaignContractVersion, @@ -107,6 +155,8 @@ export function ReleaseApp() { authorizedSoftwareRevision: campaign.authorizedSoftwareRevision, progress: campaignProgress, }, + gateBPlan: gateBPlan ?? null, + gateCPlan: gateCPlan ?? null, evidenceSessions: evidence.map((bundle) => ({ sessionId: bundle.sessionId, object: bundle.object, @@ -116,114 +166,87 @@ export function ReleaseApp() { }); } + const canonicalB = canonicalVerdict.gateB; + const canonicalC = canonicalVerdict.gateC; + return

EVERYTHING RINGS / RELEASE CONSOLE

Empirical release gates

-

Import a precommitted campaign and local validation-evidence-5 bundles. The console accounts for planned failures and evaluates frozen Gate A2/B/C contracts without uploading evidence or microphone audio.

+

Canonical release authority requires the complete precommitted campaign, Gate A2, one frozen five-target/two-reviewer Gate B plan, and one frozen four-target/two-reviewer/two-device Gate C plan. All evaluation remains local and microphone PCM is never imported.

- - - {campaign === undefined ? "no campaign" : campaign.campaignId} · {evidence.length} sessions · {gateBReviews.length} Gate B · {gateCReviews.length} Gate C + + + +
+

{campaign === undefined ? "no campaign" : campaign.campaignId} · {evidence.length} sessions · Gate B plan {gateBPlan === undefined ? "missing" : "loaded"} · Gate C plan {gateCPlan === undefined ? "missing" : "loaded"} · {gateBReviews.length} B judgments · {gateCReviews.length} C judgments

{errors.length > 0 ?

Rejected files

: null} - {campaign !== undefined && campaignProgress !== undefined ?
-
-

EMPIRICAL CAMPAIGN / PRECOMMITTED

{campaignProgress.collectionComplete ? "Collection accounted for" : "Collection still open"}

- -
-
-
CAMPAIGN{campaign.campaignId}
-
SIGNATURE{campaignProgress.campaignSignature}
-
PLANNED SPECIMENS{campaignProgress.plannedSpecimenCount}
-
COMPLETE SESSIONS{campaignProgress.conformingCompleteSessionCount} / {campaignProgress.plannedSessionCount}
-
GATE A2 PASSING{campaignProgress.passingSessionCount}
-
ANALYTICAL FAILURES{campaignProgress.analyticalFailureCount}
-
-

Campaign revision: {campaign.authorizedSoftwareRevision}{campaign.authorizedSoftwareRevision === SOFTWARE_REVISION ? " · matches this build" : " · differs from this build"}

- - {campaignProgress.unplannedSpecimenIds.length > 0 ?

Unplanned evidence: {campaignProgress.unplannedSpecimenIds.join(", ")}

: null} -
-
cohortspecimenmaterialsessionspassingfailuresstatus
- {campaignProgress.specimens.map((specimen) =>
- {specimen.cohort} - {specimen.specimenId}{specimen.label} - {specimen.material} - {specimen.conformingCompleteSessionCount} / {specimen.targetSessions} - {specimen.passingSessionCount} - {specimen.analyticalFailureCount} - {specimen.complete ? "ACCOUNTED" : "OPEN"} - {specimen.reasons.length > 0 ?
: null} -
)} -
-
: null} -
-

RELEASE

{verdict.releaseReady ? "Empirically ready" : "Evidence incomplete"}

-

Release becomes ready only when Gate A2, Gate B, and Gate C all pass their frozen contracts. Campaign accounting is a collection-integrity layer and does not lower or replace those gates.

+

CANONICAL RELEASE / V8

{canonicalVerdict.releaseReady ? "Empirically ready" : "Evidence incomplete"}

+

The older lower-level Gate B/C evaluators remain available for diagnostics, but they cannot make this console release-ready without the exact frozen plans and complete campaign accounting.

Authorized collection revision: {SOFTWARE_REVISION_VALID ? SOFTWARE_REVISION : "invalid / unset"}

-

Evidence revision: {verdict.softwareRevision ?? "mixed / no evidence"}

+ +
+ +
+

CAMPAIGN

Complete accounting

+

{campaignProgress?.conformingCompleteSessionCount ?? 0} / {campaignProgress?.plannedSessionCount ?? 12} complete planned sessions

+
+
-

GATE A2 / PHYSICAL

Repeatable acoustic structure

-

{verdict.gateA.distinctPassingSpecimenCount} / 5 distinct passing specimens

- +

GATE A2 / PHYSICAL

Repeatable acoustic structure

+

{baseVerdict.gateA.distinctPassingSpecimenCount} distinct passing specimens

+
+
-

GATE B / PERCEPTUAL

Reconstruction identity

-

{verdict.gateB.passingSpecimenCount} / 4 required passing specimens

- +

GATE B / CANONICAL

Fixed blinded panel

+

{canonicalB?.observedJudgmentCount ?? 0} / 10 canonical judgments

+
+
-

GATE C / PLAYABLE

Instrument identity

-

{verdict.gateC.passingSpecimenCount} / 4 specimens · {verdict.gateC.distinctDeviceCount} / 2 devices

- +

GATE C / CANONICAL

Complete 4×2×2 matrix

+

{canonicalC?.observedJudgmentCount ?? 0} / 16 canonical judgments

+
+ {campaign !== undefined && campaignProgress !== undefined ?
+

EMPIRICAL CAMPAIGN / PRECOMMITTED

{campaignProgress.collectionComplete ? "Collection accounted for" : "Collection still open"}

+
+
CAMPAIGN{campaign.campaignId}
+
SIGNATURE{campaignProgress.campaignSignature}
+
PLANNED{campaignProgress.plannedSpecimenCount}
+
COMPLETE{campaignProgress.conformingCompleteSessionCount}
+
A2 PASSING{campaignProgress.passingSessionCount}
+
ANALYTICAL FAILURES{campaignProgress.analyticalFailureCount}
+
+

Campaign revision: {campaign.authorizedSoftwareRevision}{campaign.authorizedSoftwareRevision === SOFTWARE_REVISION ? " · matches this build" : " · differs from this build"}

+
: null} +

EVIDENCE

Object sessions

specimenobjectmaterialattemptsdriftBCA2
{evidence.map((bundle) => { - const sessionVerdict = verdict.gateA.sessions.find((session) => session.sessionId === bundle.sessionId); + const sessionVerdict = baseVerdict.gateA.sessions.find((session) => session.sessionId === bundle.sessionId); return
- {bundle.object.specimenId} - {bundle.object.label} - {bundle.object.material} - {bundle.attempts.length} - {sessionVerdict?.metrics.sessionMedianDriftCents === null || sessionVerdict?.metrics.sessionMedianDriftCents === undefined ? "—" : `${sessionVerdict.metrics.sessionMedianDriftCents.toFixed(1)}¢`} - {bundle.gateBReviews.length} - {bundle.gateCReviews.length} - {sessionVerdict === undefined ? "—" : sessionVerdict.passed ? "PASS" : "OPEN"} + {bundle.object.specimenId}{bundle.object.label}{bundle.object.material}{bundle.attempts.length} + {sessionVerdict?.metrics.sessionMedianDriftCents == null ? "—" : `${sessionVerdict.metrics.sessionMedianDriftCents.toFixed(1)}¢`} + {bundle.gateBReviews.length}{bundle.gateCReviews.length}{sessionVerdict?.passed ? "PASS" : "OPEN"}
; })}
- - {verdict.gateA.sessions.length > 0 ?
- {verdict.gateA.sessions.map((session) =>
-

{session.objectLabel}

-
-
specimen ID
{session.specimenId}
-
software revision
{session.softwareRevision}
-
qualified attempts
{session.metrics.qualifiedAttempts} / 5
-
quality passing
{session.metrics.qualityPassingAttempts} / 5
-
analysis success
{session.metrics.successfulAnalyses} / 5
-
analysis failures
{session.metrics.analyticalFailures}
-
matched comparisons
{session.metrics.comparisonsWithEnoughMatches} / 4
-
median drift
{session.metrics.sessionMedianDriftCents === null ? "—" : `${session.metrics.sessionMedianDriftCents.toFixed(1)}¢`}
-
worst comparison
{session.metrics.worstComparisonMedianDriftCents === null ? "—" : `${session.metrics.worstComparisonMedianDriftCents.toFixed(1)}¢`}
-
- -
)} -
: null}
; } From 7b667877952b32589aa1a3384b4361858f24a947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:23:06 -0700 Subject: [PATCH 034/101] Make provenance canonicalization total over JSON-like values --- packages/validation/src/provenance.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/validation/src/provenance.ts b/packages/validation/src/provenance.ts index 01f36a47..a225ce9c 100644 --- a/packages/validation/src/provenance.ts +++ b/packages/validation/src/provenance.ts @@ -43,7 +43,11 @@ export interface DerivationRecordV1 { } function canonicalize(value: unknown): string { - if (value === null || typeof value !== "object") return JSON.stringify(value); + if (value === undefined) return "undefined"; + if (value === null || typeof value !== "object") { + const encoded = JSON.stringify(value); + return encoded === undefined ? String(value) : encoded; + } if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`; const record = value as Record; return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`).join(",")}}`; From 5050930907eee695eb93ec1d696ac4bd82f4f865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:23:32 -0700 Subject: [PATCH 035/101] Avoid large spread in digital-twin peak check --- apps/web/src/DigitalTwinEndToEnd.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/DigitalTwinEndToEnd.test.ts b/apps/web/src/DigitalTwinEndToEnd.test.ts index 9c21ac96..be15e138 100644 --- a/apps/web/src/DigitalTwinEndToEnd.test.ts +++ b/apps/web/src/DigitalTwinEndToEnd.test.ts @@ -55,6 +55,12 @@ function finiteSignal(samples: Float32Array): boolean { return samples.length > 0 && samples.every((value) => Number.isFinite(value)); } +function peakMagnitude(samples: Float32Array): number { + let peak = 0; + for (const value of samples) peak = Math.max(peak, Math.abs(value)); + return peak; +} + describe("digital-twin full-stack qualification", () => { it("exercises acquisition quality, DSP, reconstruction, instrument, provenance, Twin baseline, and Atlas without a physical object", async () => { const capture = digitalTwinCapture(); @@ -70,7 +76,7 @@ describe("digital-twin full-stack qualification", () => { const reconstructed = renderAcousticFingerprint(analysis.fingerprint, SAMPLE_RATE); expect(finiteSignal(reconstructed)).toBe(true); - expect(Math.max(...reconstructed.map(Math.abs))).toBeLessThanOrEqual(0.90001); + expect(peakMagnitude(reconstructed)).toBeLessThanOrEqual(0.90001); const reanalysis = analyzeImpact(reconstructed, SAMPLE_RATE); expect(reanalysis.ok).toBe(true); From 95434be8b7c2b2587424ac0dc14f86a5faee0600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:24:40 -0700 Subject: [PATCH 036/101] Document non-release digital twin qualification lane --- docs/DIGITAL_TWIN_QUALIFICATION.md | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/DIGITAL_TWIN_QUALIFICATION.md diff --git a/docs/DIGITAL_TWIN_QUALIFICATION.md b/docs/DIGITAL_TWIN_QUALIFICATION.md new file mode 100644 index 00000000..edaa474a --- /dev/null +++ b/docs/DIGITAL_TWIN_QUALIFICATION.md @@ -0,0 +1,79 @@ +# Digital-twin qualification lane + +Digital twins are the executable substitute for unavailable physical-object work during the current software-hardening stage. They are deliberately **not** physical Gate A2 evidence. + +## Claim boundary + +Every digital-twin result carries the semantic flag: + +`releaseGateEquivalent: false` + +A digital twin may demonstrate that acquisition-quality logic, deterministic DSP, recurrence, synthesis, playable transposition, evidence/planning code, provenance, or browser/review workflows behave as designed under controlled signals. It cannot demonstrate: + +- a real microphone transducer path; +- a real physical impact interaction; +- room/support/striker nuisance behavior not encoded in the model; +- perceptual Gate B judgments by human reviewers; +- device-listening Gate C judgments; +- physical-object identity or material inference. + +Digital artifacts must never be imported into the canonical physical Release Console as `validation-evidence-5` campaign sessions. + +## Digital specimen model + +The baseline digital specimen is a deterministic sum of damped sinusoidal modes: + +`x(t) = Σ a_i exp(-t/τ_i) sin(2π f_i t + φ_i) + n(t)` + +where modal frequency, damping, excitation amplitude, phase, and deterministic noise are explicit. Test variants may change gain, modal excitation tilt, damping scale, mode spacing, modal density, and noise level. Each variant is a new modeled interaction with known ground truth. + +This is intentionally a simplified acoustic object model. It is useful because the expected mechanism is known exactly; it is not treated as a complete simulation of a physical object. + +## Required digital stress classes + +The software qualification suite should cover: + +1. ordinary multi-mode resonant objects; +2. short-decay / heavily damped responses; +3. broad or weak modal structure; +4. coupled or multi-cluster modal structure; +5. long-decay / high-Q responses; +6. low-SNR responses; +7. close-mode / near-degenerate responses; +8. gain changes; +9. modal-excitation changes approximating strike-location sensitivity; +10. damping variation; +11. hard-negative digital specimens with different modal geometry; +12. explicit analysis failures that remain failures rather than being retried until success. + +## Qualification layers + +### Acquisition and quality + +Construct captures with a pre-trigger noise segment and known trigger. Exercise SNR, clipping, amplitude, and secondary-transient boundaries. Verify deterministic accept/reject behavior and failure reasons. + +### DSP and recurrence + +Analyze deterministic ringdowns with canonical `analyzeImpact()`. Verify only `er-dsp-2` output is evidence-versioned. Under moderate nuisance variation, evaluate mode recurrence and record matched count and modal drift. Under close-mode or weak-mode stress, failures remain informative and must not be converted to a pass by changing frozen parameters. + +### Reconstruction + +Render the fingerprint through the canonical modal renderer. Check finite output, peak bounds, deterministic rendering, and reanalysis behavior. This is an objective software test; it is not a replacement for blinded Gate B perceptual review. + +### Playable instrument + +Render notes across low, central, and high transpositions. Verify finite output, one global modal frequency ratio, Nyquist filtering, and deterministic note parameters. This is not a replacement for Gate C device listening or end-to-end acoustic latency measurement. + +### Canonical review governance + +Use synthetic evidence objects only in unit tests to prove that Gate B cannot pass without the exact five-target/two-reviewer ten-judgment matrix and Gate C cannot pass without the exact four-target/two-reviewer/two-device sixteen-judgment matrix. Synthetic reviews test evaluator semantics; they are not human evidence. + +### Provenance and Atlas + +On the post-freeze research branch, digital measurements may exercise content-addressed MeasurementRecord/DerivationRecord and Atlas schemas. They must be labeled as synthetic/non-physical artifacts and must not be promoted to calibrated Sonic Twin evidence. + +## Exit criterion + +The digital lane is complete for a revision when typecheck, unit tests, deterministic digital stress tests, build, and browser E2E all pass on the exact revision, and the qualification report explicitly states `releaseGateEquivalent: false`. + +Physical issue #64 remains open after a digital qualification pass. When physical work becomes possible, the actual microphone/transducer preflight is still required before any physical campaign evidence can be accepted. From 472f5b2d7a8c17963fed32098f839c1f3fbc2274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:24:48 -0700 Subject: [PATCH 037/101] Add repository code ownership defaults --- .github/CODEOWNERS | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ea434c03 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# Default ownership until subsystem maintainers are added. +* @fraware + +# Empirical protocols and release authority require explicit owner review. +/docs/GATE_*.md @fraware +/docs/EMPIRICAL_CAMPAIGN.md @fraware +/packages/validation/ @fraware +/packages/dsp/ @fraware +/apps/web/src/ReleaseApp.tsx @fraware +/apps/web/src/CampaignAuthorApp.tsx @fraware +/apps/web/src/CampaignCollectionApp.tsx @fraware From da053f304191f62132c07a1326aa976ceca167c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:25:01 -0700 Subject: [PATCH 038/101] Add security and privacy reporting policy --- SECURITY.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..74f3b6e4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security and privacy reporting + +EVERYTHING RINGS handles microphone access locally in the browser and intentionally separates microphone PCM from shareable evidence artifacts. Security reports that could affect microphone privacy, evidence integrity, provenance binding, artifact parsing, or release-gate correctness are treated as high priority. + +## Report scope + +Please report privately before public disclosure when an issue could: + +- cause microphone capture to continue after stop, cancellation, page departure, or ownership transfer; +- upload, persist, or disclose raw microphone samples outside an explicitly local/private path; +- bypass evidence schema validation or revision/algorithm binding; +- forge, substitute, or mismatch Gate B listening companions or provenance digests; +- cause the Release Console to accept an incomplete or non-canonical empirical matrix; +- execute untrusted content through imported evidence, campaign, review-plan, Capsule, or future Atlas artifacts; +- compromise content-addressed measurement or derivation integrity. + +## Reporting + +Use GitHub's private vulnerability reporting facility for this repository when enabled. If that facility is not enabled, contact the repository owner through a private channel rather than publishing exploit details in a public issue. + +Include the affected revision, browser/runtime, reproduction steps, expected and actual behavior, and whether microphone data or release evidence may have been exposed or misclassified. + +## Claim discipline + +A software security fix does not retroactively validate physical evidence. Changes to an empirical authority revision require the project's normal revision-binding and requalification process before new physical evidence is accepted. From 6d4644cd71a88504972a87187bc601d5c46b6bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:25:17 -0700 Subject: [PATCH 039/101] Add contribution and empirical-change discipline --- CONTRIBUTING.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e100ae65 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing + +EVERYTHING RINGS combines a consumer acoustic instrument with empirical measurement and validation infrastructure. Contributions are welcome, but changes must preserve the distinction between product behavior, research diagnostics, and evidence-eligible empirical contracts. + +## Development setup + +Use the repository-declared pnpm version and frozen lockfile: + +```sh +corepack enable +pnpm install --frozen-lockfile +pnpm typecheck +pnpm test +pnpm build +bash scripts/browser-smoke.sh +``` + +## Change classes + +### Product-only changes + +Consumer UI, local history, sharing, visualization, and other non-empirical features belong on the product development line. They must not be merged into a frozen empirical authority revision solely for convenience. + +### Research/diagnostic changes + +Alternative DSP configurations, digital twins, external datasets, similarity baselines, and future embeddings must clearly state whether their output is evidence-eligible. Custom analysis configurations must not claim an `er-dsp-*` canonical algorithm version unless a new algorithm contract is explicitly versioned. + +### Empirical-contract changes + +Changes to acquisition semantics, DSP parameters, evidence schemas, campaign design, gate thresholds, review-plan selection, or Release Console authority require explicit versioning and requalification before new physical evidence is accepted. Do not relax a frozen threshold or omit a failed observation to make an observed dataset pass. + +## Pull requests + +A pull request should state: + +- the intended change class; +- which claim or product behavior changes, if any; +- whether evidence semantics change; +- tests added or updated; +- whether a prior empirical revision must be superseded or requalified. + +Keep machine contracts, documentation, and advertised claims synchronized. A nearby or weaker theorem/test must not be presented as evidence for a stronger proposition. + +## Privacy + +Do not commit microphone PCM, private Gate B listening companions, device identifiers that should remain local, secrets, or user-private artifacts. Synthetic/digital-twin signals are acceptable when clearly labeled non-physical. + +## Licensing + +The repository currently has an open issue to choose an explicit project license. Until that decision is made, public repository visibility should not be interpreted as a grant of reuse rights beyond GitHub's platform terms. Contribution licensing language will be finalized with the repository license decision. From 2791f71538f704f1b00efac6e1dad359cf4b6b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:25:52 -0700 Subject: [PATCH 040/101] Add explicit non-release digital twin qualification workflow --- .../workflows/digital-twin-qualification.yml | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/digital-twin-qualification.yml diff --git a/.github/workflows/digital-twin-qualification.yml b/.github/workflows/digital-twin-qualification.yml new file mode 100644 index 00000000..40726aa4 --- /dev/null +++ b/.github/workflows/digital-twin-qualification.yml @@ -0,0 +1,70 @@ +name: digital-twin-qualification + +on: + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/digital-twin-qualification.yml" + - "packages/acquisition/**" + - "packages/dsp/**" + - "packages/fingerprint/**" + - "packages/instrument/**" + - "packages/synth/**" + - "packages/validation/**" + - "apps/web/**" + - "scripts/**" + - "docs/DIGITAL_TWIN_QUALIFICATION.md" + +permissions: + contents: read + +jobs: + qualify: + runs-on: ubuntu-24.04 + env: + VITE_SOFTWARE_REVISION: ${{ github.sha }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: 22 + - name: Enable Corepack + run: corepack enable + - name: Install frozen dependencies + run: pnpm install --frozen-lockfile + - name: Typecheck + run: pnpm typecheck + - name: Test including digital twins + run: pnpm test + - name: Build + run: pnpm build + - name: Browser qualification + run: bash scripts/browser-smoke.sh + - name: Emit qualification report + run: | + cat > /tmp/everything-rings-digital-twin-qualification.json < Date: Mon, 24 Aug 2026 08:27:06 -0700 Subject: [PATCH 041/101] Remove unused standalone SHA-256 helper --- packages/validation/src/sha256.ts | 91 ------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 packages/validation/src/sha256.ts diff --git a/packages/validation/src/sha256.ts b/packages/validation/src/sha256.ts deleted file mode 100644 index d3ed9956..00000000 --- a/packages/validation/src/sha256.ts +++ /dev/null @@ -1,91 +0,0 @@ -function utf8Bytes(value: string): Uint8Array { - const bytes: number[] = []; - for (const character of value) { - const codePoint = character.codePointAt(0) ?? 0; - if (codePoint <= 0x7f) bytes.push(codePoint); - else if (codePoint <= 0x7ff) { - bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f)); - } else if (codePoint <= 0xffff) { - bytes.push( - 0xe0 | (codePoint >> 12), - 0x80 | ((codePoint >> 6) & 0x3f), - 0x80 | (codePoint & 0x3f), - ); - } else { - bytes.push( - 0xf0 | (codePoint >> 18), - 0x80 | ((codePoint >> 12) & 0x3f), - 0x80 | ((codePoint >> 6) & 0x3f), - 0x80 | (codePoint & 0x3f), - ); - } - } - return Uint8Array.from(bytes); -} - -const INITIAL = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, - 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, -] as const; - -const K = [ - 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, - 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, - 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, - 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, - 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, - 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, - 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, - 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2, -] as const; - -function rotateRight(value: number, bits: number): number { - return (value >>> bits) | (value << (32 - bits)); -} - -export function sha256Hex(value: string): string { - const input = utf8Bytes(value); - const bitLength = input.length * 8; - const paddingLength = (64 - ((input.length + 9) % 64)) % 64; - const bytes = new Uint8Array(input.length + 1 + paddingLength + 8); - bytes.set(input); - bytes[input.length] = 0x80; - const view = new DataView(bytes.buffer); - const high = Math.floor(bitLength / 0x1_0000_0000); - const low = bitLength >>> 0; - view.setUint32(bytes.length - 8, high, false); - view.setUint32(bytes.length - 4, low, false); - - const hash = [...INITIAL]; - const words = new Uint32Array(64); - for (let offset = 0; offset < bytes.length; offset += 64) { - for (let index = 0; index < 16; index += 1) words[index] = view.getUint32(offset + index * 4, false); - for (let index = 16; index < 64; index += 1) { - const x = words[index - 15] ?? 0; - const y = words[index - 2] ?? 0; - const s0 = rotateRight(x, 7) ^ rotateRight(x, 18) ^ (x >>> 3); - const s1 = rotateRight(y, 17) ^ rotateRight(y, 19) ^ (y >>> 10); - words[index] = (((words[index - 16] ?? 0) + s0 + (words[index - 7] ?? 0) + s1) >>> 0); - } - - let [a,b,c,d,e,f,g,h] = hash; - for (let index = 0; index < 64; index += 1) { - const s1 = rotateRight(e!, 6) ^ rotateRight(e!, 11) ^ rotateRight(e!, 25); - const ch = (e! & f!) ^ (~e! & g!); - const temp1 = (h! + s1 + ch + (K[index] ?? 0) + (words[index] ?? 0)) >>> 0; - const s0 = rotateRight(a!, 2) ^ rotateRight(a!, 13) ^ rotateRight(a!, 22); - const maj = (a! & b!) ^ (a! & c!) ^ (b! & c!); - const temp2 = (s0 + maj) >>> 0; - h = g; g = f; f = e; e = (d! + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 0; - } - hash[0] = ((hash[0] ?? 0) + a!) >>> 0; - hash[1] = ((hash[1] ?? 0) + b!) >>> 0; - hash[2] = ((hash[2] ?? 0) + c!) >>> 0; - hash[3] = ((hash[3] ?? 0) + d!) >>> 0; - hash[4] = ((hash[4] ?? 0) + e!) >>> 0; - hash[5] = ((hash[5] ?? 0) + f!) >>> 0; - hash[6] = ((hash[6] ?? 0) + g!) >>> 0; - hash[7] = ((hash[7] ?? 0) + h!) >>> 0; - } - return hash.map((word) => word.toString(16).padStart(8, "0")).join(""); -} From 839e3921f8b37cb9578cd3193e72abf206acc19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:28:03 -0700 Subject: [PATCH 042/101] Bind canonical review plans to exact upstream verdict digests --- packages/validation/src/review-plans.ts | 29 +++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/validation/src/review-plans.ts b/packages/validation/src/review-plans.ts index 65a81d64..666328e4 100644 --- a/packages/validation/src/review-plans.ts +++ b/packages/validation/src/review-plans.ts @@ -17,6 +17,7 @@ export interface GateBPlanV1 { readonly schemaVersion: 1; readonly planContractVersion: "gate-b-plan-1"; readonly gateARevision: string; + readonly gateAVerdictSha256: string; readonly reviewerIds: readonly [string, string]; readonly targets: readonly PlannedReviewTarget[]; } @@ -29,6 +30,7 @@ export interface GateCDevicePlan { export interface GateCPlanV1 { readonly schemaVersion: 1; readonly planContractVersion: "gate-c-plan-1"; + readonly gateBVerdictSha256: string; readonly reviewerIds: readonly [string, string]; readonly devices: readonly [GateCDevicePlan, GateCDevicePlan]; readonly targets: readonly PlannedReviewTarget[]; @@ -58,11 +60,26 @@ function targetKey(target: ReviewTarget): string { return `${target.sessionId}\u0000${target.attemptId}`; } +function canonicalize(value: unknown): string { + if (value === undefined) return "undefined"; + if (value === null || typeof value !== "object") { + const encoded = JSON.stringify(value); + return encoded === undefined ? String(value) : encoded; + } + if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`; + const record = value as Record; + return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`).join(",")}}`; +} + async function sha256Text(value: string): Promise { const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); } +async function verdictDigest(value: unknown): Promise { + return `sha256:${await sha256Text(canonicalize(value))}`; +} + async function rankTargets( domain: string, targets: readonly PlannedReviewTarget[], @@ -128,6 +145,7 @@ export async function createGateBPlan( schemaVersion: 1, planContractVersion: "gate-b-plan-1", gateARevision: gateA.softwareRevision, + gateAVerdictSha256: await verdictDigest(gateA), reviewerIds: reviewers, targets: selected, }; @@ -162,20 +180,22 @@ export async function createGateCPlan( return { schemaVersion: 1, planContractVersion: "gate-c-plan-1", + gateBVerdictSha256: await verdictDigest(gateB), reviewerIds: reviewers, devices: [devices[0]!, devices[1]!], targets, }; } -export function evaluateCanonicalGateB( +export async function evaluateCanonicalGateB( plan: GateBPlanV1, gateA: GateAReleaseVerdict, reviews: readonly GateBReview[], -): CanonicalGateBResult { +): Promise { const reasons: string[] = []; if (!gateA.passed) reasons.push("Gate A has not passed"); if (gateA.softwareRevision !== plan.gateARevision) reasons.push("Gate B plan revision does not match Gate A"); + if (await verdictDigest(gateA) !== plan.gateAVerdictSha256) reasons.push("Gate B plan does not bind the exact Gate A verdict"); if (plan.targets.length !== 5) reasons.push("Gate B plan must contain exactly five targets"); const expectedReviewers = new Set(plan.reviewerIds.map(normalize)); const expectedTargets = new Map(plan.targets.map((target) => [targetKey(target), target] as const)); @@ -205,13 +225,14 @@ export function evaluateCanonicalGateB( }; } -export function evaluateCanonicalGateC( +export async function evaluateCanonicalGateC( plan: GateCPlanV1, gateB: GateBReleaseVerdict, reviews: readonly GateCReview[], -): CanonicalGateCResult { +): Promise { const reasons: string[] = []; if (!gateB.passed) reasons.push("Gate B has not passed"); + if (await verdictDigest(gateB) !== plan.gateBVerdictSha256) reasons.push("Gate C plan does not bind the exact Gate B verdict"); if (plan.targets.length !== 4) reasons.push("Gate C plan must contain exactly four targets"); const expectedReviewers = new Set(plan.reviewerIds.map(normalize)); const expectedDevices = new Map(plan.devices.map((device) => [normalize(device.deviceId), device] as const)); From 9297bcfe795c56e43dfb30cd670294c550081564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:28:33 -0700 Subject: [PATCH 043/101] Add deterministic runtime-independent SHA-256 utility --- packages/validation/src/stable-sha256.ts | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 packages/validation/src/stable-sha256.ts diff --git a/packages/validation/src/stable-sha256.ts b/packages/validation/src/stable-sha256.ts new file mode 100644 index 00000000..22a0b0f2 --- /dev/null +++ b/packages/validation/src/stable-sha256.ts @@ -0,0 +1,83 @@ +function utf8(value: string): Uint8Array { + const output: number[] = []; + for (const character of value) { + const code = character.codePointAt(0) ?? 0; + if (code <= 0x7f) output.push(code); + else if (code <= 0x7ff) output.push(0xc0 | (code >>> 6), 0x80 | (code & 0x3f)); + else if (code <= 0xffff) output.push(0xe0 | (code >>> 12), 0x80 | ((code >>> 6) & 0x3f), 0x80 | (code & 0x3f)); + else output.push(0xf0 | (code >>> 18), 0x80 | ((code >>> 12) & 0x3f), 0x80 | ((code >>> 6) & 0x3f), 0x80 | (code & 0x3f)); + } + return Uint8Array.from(output); +} + +const INITIAL: readonly number[] = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, +]; + +const K: readonly number[] = [ + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2, +]; + +function rotr(value: number, shift: number): number { + return (value >>> shift) | (value << (32 - shift)); +} + +export function stableSha256Hex(value: string): string { + const source = utf8(value); + const bitLength = source.length * 8; + const padding = (64 - ((source.length + 9) % 64)) % 64; + const bytes = new Uint8Array(source.length + 1 + padding + 8); + bytes.set(source); + bytes[source.length] = 0x80; + const view = new DataView(bytes.buffer); + view.setUint32(bytes.length - 8, Math.floor(bitLength / 0x1_0000_0000), false); + view.setUint32(bytes.length - 4, bitLength >>> 0, false); + + const hash: number[] = [...INITIAL]; + const words = new Uint32Array(64); + for (let offset = 0; offset < bytes.length; offset += 64) { + for (let index = 0; index < 16; index += 1) words[index] = view.getUint32(offset + index * 4, false); + for (let index = 16; index < 64; index += 1) { + const x = words[index - 15] ?? 0; + const y = words[index - 2] ?? 0; + const s0 = rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3); + const s1 = rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10); + words[index] = ((words[index - 16] ?? 0) + s0 + (words[index - 7] ?? 0) + s1) >>> 0; + } + + let a = hash[0] ?? 0; + let b = hash[1] ?? 0; + let c = hash[2] ?? 0; + let d = hash[3] ?? 0; + let e = hash[4] ?? 0; + let f = hash[5] ?? 0; + let g = hash[6] ?? 0; + let h = hash[7] ?? 0; + for (let index = 0; index < 64; index += 1) { + const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); + const ch = (e & f) ^ (~e & g); + const temp1 = (h + s1 + ch + (K[index] ?? 0) + (words[index] ?? 0)) >>> 0; + const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); + const maj = (a & b) ^ (a & c) ^ (b & c); + const temp2 = (s0 + maj) >>> 0; + h = g; g = f; f = e; e = (d + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 0; + } + hash[0] = ((hash[0] ?? 0) + a) >>> 0; + hash[1] = ((hash[1] ?? 0) + b) >>> 0; + hash[2] = ((hash[2] ?? 0) + c) >>> 0; + hash[3] = ((hash[3] ?? 0) + d) >>> 0; + hash[4] = ((hash[4] ?? 0) + e) >>> 0; + hash[5] = ((hash[5] ?? 0) + f) >>> 0; + hash[6] = ((hash[6] ?? 0) + g) >>> 0; + hash[7] = ((hash[7] ?? 0) + h) >>> 0; + } + return hash.map((word) => word.toString(16).padStart(8, "0")).join(""); +} From d71bbbcf35f77f813d721a59613564afe95a8631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:29:16 -0700 Subject: [PATCH 044/101] Use deterministic synchronous SHA-256 for canonical plan authority --- packages/validation/src/review-plans.ts | 50 +++++++++++-------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/validation/src/review-plans.ts b/packages/validation/src/review-plans.ts index 666328e4..2b1e1334 100644 --- a/packages/validation/src/review-plans.ts +++ b/packages/validation/src/review-plans.ts @@ -7,6 +7,7 @@ import type { ReviewTarget, } from "./types"; import { evaluateGateBRelease, evaluateGateCRelease } from "./evaluate"; +import { stableSha256Hex } from "./stable-sha256"; export interface PlannedReviewTarget extends ReviewTarget { readonly specimenId: string; @@ -71,25 +72,18 @@ function canonicalize(value: unknown): string { return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`).join(",")}}`; } -async function sha256Text(value: string): Promise { - const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); - return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +function verdictDigest(value: unknown): string { + return `sha256:${stableSha256Hex(canonicalize(value))}`; } -async function verdictDigest(value: unknown): Promise { - return `sha256:${await sha256Text(canonicalize(value))}`; -} - -async function rankTargets( +function rankTargets( domain: string, targets: readonly PlannedReviewTarget[], -): Promise { - const ranked = await Promise.all(targets.map(async (target) => ({ +): readonly PlannedReviewTarget[] { + return targets.map((target) => ({ target, - digest: await sha256Text(`${domain}|${normalize(target.specimenId)}|${target.sessionId}|${target.attemptId}`), - }))); - ranked.sort((left, right) => left.digest.localeCompare(right.digest)); - return ranked.map(({ target }) => target); + digest: stableSha256Hex(`${domain}|${normalize(target.specimenId)}|${target.sessionId}|${target.attemptId}`), + })).sort((left, right) => left.digest.localeCompare(right.digest)).map(({ target }) => target); } function gateATargets(gateA: GateAReleaseVerdict): PlannedReviewTarget[] { @@ -120,13 +114,13 @@ function assertFixedPair(values: readonly string[], field: string): readonly [st return [values[0]!.trim(), values[1]!.trim()]; } -export async function createGateBPlan( +export function createGateBPlan( gateA: GateAReleaseVerdict, reviewerIds: readonly string[], -): Promise { +): GateBPlanV1 { if (!gateA.passed || gateA.softwareRevision === null) throw new Error("Gate A must pass before Gate B planning"); const reviewers = assertFixedPair(reviewerIds, "reviewerIds"); - const all = uniqueSpecimenTargets(await rankTargets("gate-b-target-v1", gateATargets(gateA))); + const all = uniqueSpecimenTargets(rankTargets("gate-b-target-v1", gateATargets(gateA))); const byMaterial = new Map(gateA.sessions .filter((session) => session.passed) .map((session) => [normalize(session.specimenId), session.material] as const)); @@ -145,17 +139,17 @@ export async function createGateBPlan( schemaVersion: 1, planContractVersion: "gate-b-plan-1", gateARevision: gateA.softwareRevision, - gateAVerdictSha256: await verdictDigest(gateA), + gateAVerdictSha256: verdictDigest(gateA), reviewerIds: reviewers, targets: selected, }; } -export async function createGateCPlan( +export function createGateCPlan( gateB: GateBReleaseVerdict, reviewerIds: readonly string[], devices: readonly GateCDevicePlan[], -): Promise { +): GateCPlanV1 { if (!gateB.passed) throw new Error("Gate B must pass before Gate C planning"); const reviewers = assertFixedPair(reviewerIds, "reviewerIds"); if (devices.length !== 2) throw new Error("Gate C planning requires exactly two devices"); @@ -174,28 +168,28 @@ export async function createGateCPlan( sessionId: object.selectedTarget!.sessionId, attemptId: object.selectedTarget!.attemptId, })); - const ranked = await rankTargets("gate-c-target-v1", uniqueSpecimenTargets(passingTargets)); + const ranked = rankTargets("gate-c-target-v1", uniqueSpecimenTargets(passingTargets)); const targets = ranked.slice(0, 4); if (targets.length !== 4) throw new Error("Gate C planning requires four passing Gate B specimens"); return { schemaVersion: 1, planContractVersion: "gate-c-plan-1", - gateBVerdictSha256: await verdictDigest(gateB), + gateBVerdictSha256: verdictDigest(gateB), reviewerIds: reviewers, devices: [devices[0]!, devices[1]!], targets, }; } -export async function evaluateCanonicalGateB( +export function evaluateCanonicalGateB( plan: GateBPlanV1, gateA: GateAReleaseVerdict, reviews: readonly GateBReview[], -): Promise { +): CanonicalGateBResult { const reasons: string[] = []; if (!gateA.passed) reasons.push("Gate A has not passed"); if (gateA.softwareRevision !== plan.gateARevision) reasons.push("Gate B plan revision does not match Gate A"); - if (await verdictDigest(gateA) !== plan.gateAVerdictSha256) reasons.push("Gate B plan does not bind the exact Gate A verdict"); + if (verdictDigest(gateA) !== plan.gateAVerdictSha256) reasons.push("Gate B plan does not bind the exact Gate A verdict"); if (plan.targets.length !== 5) reasons.push("Gate B plan must contain exactly five targets"); const expectedReviewers = new Set(plan.reviewerIds.map(normalize)); const expectedTargets = new Map(plan.targets.map((target) => [targetKey(target), target] as const)); @@ -225,14 +219,14 @@ export async function evaluateCanonicalGateB( }; } -export async function evaluateCanonicalGateC( +export function evaluateCanonicalGateC( plan: GateCPlanV1, gateB: GateBReleaseVerdict, reviews: readonly GateCReview[], -): Promise { +): CanonicalGateCResult { const reasons: string[] = []; if (!gateB.passed) reasons.push("Gate B has not passed"); - if (await verdictDigest(gateB) !== plan.gateBVerdictSha256) reasons.push("Gate C plan does not bind the exact Gate B verdict"); + if (verdictDigest(gateB) !== plan.gateBVerdictSha256) reasons.push("Gate C plan does not bind the exact Gate B verdict"); if (plan.targets.length !== 4) reasons.push("Gate C plan must contain exactly four targets"); const expectedReviewers = new Set(plan.reviewerIds.map(normalize)); const expectedDevices = new Map(plan.devices.map((device) => [normalize(device.deviceId), device] as const)); From acf4397992c3bbe11e831ee732f6f6244fa48954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:29:47 -0700 Subject: [PATCH 045/101] Require exact upstream verdict digests in review plan parsers --- packages/validation/src/review-plan-parse.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/validation/src/review-plan-parse.ts b/packages/validation/src/review-plan-parse.ts index 0de198df..ddf2b6e7 100644 --- a/packages/validation/src/review-plan-parse.ts +++ b/packages/validation/src/review-plan-parse.ts @@ -13,6 +13,12 @@ function nonempty(value: unknown, field: string): string { return value.trim(); } +function sha256Digest(value: unknown, field: string): string { + const digest = nonempty(value, field); + if (!/^sha256:[0-9a-f]{64}$/.test(digest)) throw new TypeError(`${field} must be sha256:<64 lowercase hex>`); + return digest; +} + function positiveInteger(value: unknown, field: string): number { if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new TypeError(`${field} must be a positive integer`); return value; @@ -50,11 +56,12 @@ export function parseGateBPlan(value: unknown): GateBPlanParseResult { if (value.planContractVersion !== "gate-b-plan-1") throw new TypeError("Gate B plan contract must be gate-b-plan-1"); const gateARevision = nonempty(value.gateARevision, "gateARevision"); if (!/^[0-9a-f]{40}$/.test(gateARevision)) throw new TypeError("gateARevision must be an exact 40-hex Git revision"); + const gateAVerdictSha256 = sha256Digest(value.gateAVerdictSha256, "gateAVerdictSha256"); const reviewerIds = parseFixedReviewers(value.reviewerIds, "reviewerIds"); if (!Array.isArray(value.targets)) throw new TypeError("targets must be an array"); const targets = value.targets.map((target, index) => parseTarget(target, `targets[${index}]`)); uniqueTargets(targets, 5, "targets"); - return { ok: true, plan: { schemaVersion: 1, planContractVersion: "gate-b-plan-1", gateARevision, reviewerIds, targets } }; + return { ok: true, plan: { schemaVersion: 1, planContractVersion: "gate-b-plan-1", gateARevision, gateAVerdictSha256, reviewerIds, targets } }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } @@ -77,6 +84,7 @@ export function parseGateCPlan(value: unknown): GateCPlanParseResult { if (!isRecord(value)) throw new TypeError("Gate C plan must be an object"); if (value.schemaVersion !== 1) throw new TypeError("Gate C plan schemaVersion must be 1"); if (value.planContractVersion !== "gate-c-plan-1") throw new TypeError("Gate C plan contract must be gate-c-plan-1"); + const gateBVerdictSha256 = sha256Digest(value.gateBVerdictSha256, "gateBVerdictSha256"); const reviewerIds = parseFixedReviewers(value.reviewerIds, "reviewerIds"); if (!Array.isArray(value.devices) || value.devices.length !== 2) throw new TypeError("devices must contain exactly two planned devices"); const devices: [GateCDevicePlan, GateCDevicePlan] = [parseDevice(value.devices[0], "devices[0]"), parseDevice(value.devices[1], "devices[1]")]; @@ -85,7 +93,7 @@ export function parseGateCPlan(value: unknown): GateCPlanParseResult { if (!Array.isArray(value.targets)) throw new TypeError("targets must be an array"); const targets = value.targets.map((target, index) => parseTarget(target, `targets[${index}]`)); uniqueTargets(targets, 4, "targets"); - return { ok: true, plan: { schemaVersion: 1, planContractVersion: "gate-c-plan-1", reviewerIds, devices, targets } }; + return { ok: true, plan: { schemaVersion: 1, planContractVersion: "gate-c-plan-1", gateBVerdictSha256, reviewerIds, devices, targets } }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } From 549a8bb1bb4eb6a9a18e11ba45b31a1ddfc2f426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:30:14 -0700 Subject: [PATCH 046/101] Test required upstream verdict digests in review plans --- .../validation/test/review-plan-parse.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/validation/test/review-plan-parse.test.ts b/packages/validation/test/review-plan-parse.test.ts index 6281de11..319c8c73 100644 --- a/packages/validation/test/review-plan-parse.test.ts +++ b/packages/validation/test/review-plan-parse.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { parseGateBPlan, parseGateCPlan } from "../src"; import { SOFTWARE_REVISION } from "./helpers"; +const DIGEST = `sha256:${"0".repeat(64)}`; const targets = Array.from({ length: 5 }, (_, index) => ({ specimenId: `specimen-${index + 1}`, objectLabel: `object ${index + 1}`, @@ -10,11 +11,12 @@ const targets = Array.from({ length: 5 }, (_, index) => ({ })); describe("canonical review plan parsers", () => { - it("accepts exactly five distinct Gate B targets and two distinct reviewers", () => { + it("accepts exactly five distinct Gate B targets, two reviewers, and an upstream verdict digest", () => { const result = parseGateBPlan({ schemaVersion: 1, planContractVersion: "gate-b-plan-1", gateARevision: SOFTWARE_REVISION, + gateAVerdictSha256: DIGEST, reviewerIds: ["reviewer-a", "reviewer-b"], targets, }); @@ -26,6 +28,7 @@ describe("canonical review plan parsers", () => { schemaVersion: 1, planContractVersion: "gate-b-plan-1", gateARevision: SOFTWARE_REVISION, + gateAVerdictSha256: DIGEST, reviewerIds: ["reviewer-a", "reviewer-b"], targets: [...targets.slice(0, 4), { ...targets[4], specimenId: targets[0]?.specimenId }], }); @@ -37,6 +40,7 @@ describe("canonical review plan parsers", () => { const result = parseGateCPlan({ schemaVersion: 1, planContractVersion: "gate-c-plan-1", + gateBVerdictSha256: DIGEST, reviewerIds: ["reviewer-c", "reviewer-d"], devices: [ { deviceId: "desktop-1", deviceClass: "desktop" }, @@ -51,6 +55,7 @@ describe("canonical review plan parsers", () => { const result = parseGateCPlan({ schemaVersion: 1, planContractVersion: "gate-c-plan-1", + gateBVerdictSha256: DIGEST, reviewerIds: ["reviewer-c", "reviewer-d"], devices: [ { deviceId: "desktop-1", deviceClass: "desktop" }, @@ -61,4 +66,17 @@ describe("canonical review plan parsers", () => { expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/mobile/); }); + + it("rejects missing or malformed upstream verdict digests", () => { + const result = parseGateBPlan({ + schemaVersion: 1, + planContractVersion: "gate-b-plan-1", + gateARevision: SOFTWARE_REVISION, + gateAVerdictSha256: "not-a-digest", + reviewerIds: ["reviewer-a", "reviewer-b"], + targets, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toMatch(/sha256/); + }); }); From 9ec8288262aec3988bece490ac74b420b1fd6189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:30:23 -0700 Subject: [PATCH 047/101] Verify deterministic SHA-256 implementation against standard vectors --- packages/validation/test/stable-sha256.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/validation/test/stable-sha256.test.ts diff --git a/packages/validation/test/stable-sha256.test.ts b/packages/validation/test/stable-sha256.test.ts new file mode 100644 index 00000000..5fa4a15d --- /dev/null +++ b/packages/validation/test/stable-sha256.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { stableSha256Hex } from "../src/stable-sha256"; + +describe("stable SHA-256", () => { + it("matches standard ASCII test vectors", () => { + expect(stableSha256Hex("")).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + expect(stableSha256Hex("abc")).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + }); + + it("is deterministic for unicode input", () => { + expect(stableSha256Hex("everything rings — 🔔")).toBe(stableSha256Hex("everything rings — 🔔")); + expect(stableSha256Hex("everything rings — 🔔")).not.toBe(stableSha256Hex("everything rings")); + }); +}); From 298e7bd2e70720944462e71e51cea98f9ec97ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:31:08 -0700 Subject: [PATCH 048/101] Add nuisance-characterization and hard-negative benchmark utilities --- packages/fingerprint/src/nuisance.ts | 187 +++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 packages/fingerprint/src/nuisance.ts diff --git a/packages/fingerprint/src/nuisance.ts b/packages/fingerprint/src/nuisance.ts new file mode 100644 index 00000000..7391105e --- /dev/null +++ b/packages/fingerprint/src/nuisance.ts @@ -0,0 +1,187 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { fingerprintRecurrence } from "./recurrence"; + +export interface NuisanceMetadataV1 { + readonly strikeLocation: string; + readonly striker: string; + readonly support: string; + readonly microphoneDistanceClass: string; + readonly room: string; + readonly stationId: string; + readonly operatorId: string; + readonly dayId: string; +} + +export interface LabeledFingerprintObservationV1 { + readonly observationId: string; + readonly specimenId: string; + readonly fingerprint: AcousticFingerprintV1; + readonly nuisance: NuisanceMetadataV1; +} + +export type NuisanceFactor = keyof NuisanceMetadataV1; + +export interface WithinSpecimenPairMetricV1 { + readonly leftObservationId: string; + readonly rightObservationId: string; + readonly specimenId: string; + readonly differingFactors: readonly NuisanceFactor[]; + readonly matchedModes: number; + readonly medianFrequencyDriftCents: number; + readonly meanFrequencyDriftCents: number; +} + +export interface NuisanceFactorSummaryV1 { + readonly factor: NuisanceFactor; + readonly comparisonCount: number; + readonly medianFrequencyDriftCents: number | null; + readonly p90FrequencyDriftCents: number | null; +} + +export interface NuisanceCharacterizationV1 { + readonly schemaVersion: 1; + readonly characterizationVersion: "nuisance-characterization-1"; + readonly observationCount: number; + readonly specimenCount: number; + readonly withinSpecimenPairs: readonly WithinSpecimenPairMetricV1[]; + readonly factorSummaries: readonly NuisanceFactorSummaryV1[]; +} + +function median(values: readonly number[]): number | null { + if (values.length === 0) return null; + const ordered = [...values].sort((left, right) => left - right); + const middle = Math.floor(ordered.length / 2); + if (ordered.length % 2 === 1) return ordered[middle] ?? null; + return ((ordered[middle - 1] ?? 0) + (ordered[middle] ?? 0)) / 2; +} + +function percentile(values: readonly number[], quantile: number): number | null { + if (values.length === 0) return null; + const ordered = [...values].sort((left, right) => left - right); + const index = Math.min(ordered.length - 1, Math.max(0, Math.ceil(quantile * ordered.length) - 1)); + return ordered[index] ?? null; +} + +const NUISANCE_FACTORS: readonly NuisanceFactor[] = [ + "strikeLocation", + "striker", + "support", + "microphoneDistanceClass", + "room", + "stationId", + "operatorId", + "dayId", +]; + +function differingFactors(left: NuisanceMetadataV1, right: NuisanceMetadataV1): NuisanceFactor[] { + return NUISANCE_FACTORS.filter((factor) => left[factor] !== right[factor]); +} + +export function characterizeNuisance( + observations: readonly LabeledFingerprintObservationV1[], +): NuisanceCharacterizationV1 { + const bySpecimen = new Map(); + for (const observation of observations) { + const key = observation.specimenId.trim().toLocaleLowerCase("en-US"); + if (key.length === 0) throw new Error("nuisance observations require specimenId"); + const current = bySpecimen.get(key) ?? []; + current.push(observation); + bySpecimen.set(key, current); + } + + const pairs: WithinSpecimenPairMetricV1[] = []; + for (const specimenObservations of bySpecimen.values()) { + for (let leftIndex = 0; leftIndex < specimenObservations.length; leftIndex += 1) { + for (let rightIndex = leftIndex + 1; rightIndex < specimenObservations.length; rightIndex += 1) { + const left = specimenObservations[leftIndex]!; + const right = specimenObservations[rightIndex]!; + const recurrence = fingerprintRecurrence(left.fingerprint, right.fingerprint); + pairs.push({ + leftObservationId: left.observationId, + rightObservationId: right.observationId, + specimenId: left.specimenId, + differingFactors: differingFactors(left.nuisance, right.nuisance), + matchedModes: recurrence.matchedCount, + medianFrequencyDriftCents: recurrence.medianCents, + meanFrequencyDriftCents: recurrence.meanCents, + }); + } + } + } + + const factorSummaries = NUISANCE_FACTORS.map((factor) => { + const values = pairs + .filter((pair) => pair.differingFactors.length === 1 && pair.differingFactors[0] === factor) + .map((pair) => pair.medianFrequencyDriftCents) + .filter(Number.isFinite); + return { + factor, + comparisonCount: values.length, + medianFrequencyDriftCents: median(values), + p90FrequencyDriftCents: percentile(values, 0.9), + }; + }); + + return { + schemaVersion: 1, + characterizationVersion: "nuisance-characterization-1", + observationCount: observations.length, + specimenCount: bySpecimen.size, + withinSpecimenPairs: pairs, + factorSummaries, + }; +} + +export interface HardNegativePairV1 { + readonly leftObservationId: string; + readonly rightObservationId: string; + readonly leftSpecimenId: string; + readonly rightSpecimenId: string; + readonly medianFrequencyDriftCents: number; + readonly matchedModes: number; +} + +export function selectHardNegativePairs( + observations: readonly LabeledFingerprintObservationV1[], + limit: number, +): readonly HardNegativePairV1[] { + if (!Number.isInteger(limit) || limit <= 0) throw new Error("hard-negative limit must be a positive integer"); + const pairs: HardNegativePairV1[] = []; + for (let leftIndex = 0; leftIndex < observations.length; leftIndex += 1) { + for (let rightIndex = leftIndex + 1; rightIndex < observations.length; rightIndex += 1) { + const left = observations[leftIndex]!; + const right = observations[rightIndex]!; + if (left.specimenId.trim().toLocaleLowerCase("en-US") === right.specimenId.trim().toLocaleLowerCase("en-US")) continue; + const recurrence = fingerprintRecurrence(left.fingerprint, right.fingerprint); + pairs.push({ + leftObservationId: left.observationId, + rightObservationId: right.observationId, + leftSpecimenId: left.specimenId, + rightSpecimenId: right.specimenId, + medianFrequencyDriftCents: recurrence.medianCents, + matchedModes: recurrence.matchedCount, + }); + } + } + return pairs + .sort((left, right) => left.medianFrequencyDriftCents - right.medianFrequencyDriftCents || right.matchedModes - left.matchedModes) + .slice(0, limit); +} + +function fnv1a32(value: string): number { + let hash = 0x811c9dc5; + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + return hash >>> 0; +} + +export type BenchmarkSplit = "train" | "validation" | "test"; + +export function specimenDisjointSplit(specimenId: string): BenchmarkSplit { + const bucket = fnv1a32(specimenId.trim().toLocaleLowerCase("en-US")) % 100; + if (bucket < 70) return "train"; + if (bucket < 85) return "validation"; + return "test"; +} From 4bcd21cd0c19855b2de2ce8769fa7e1af50c790b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:31:25 -0700 Subject: [PATCH 049/101] Export nuisance benchmark utilities --- packages/fingerprint/src/index.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/fingerprint/src/index.ts b/packages/fingerprint/src/index.ts index cb7d1ecb..551a8cb9 100644 --- a/packages/fingerprint/src/index.ts +++ b/packages/fingerprint/src/index.ts @@ -16,3 +16,16 @@ export { type SonicTwinBenchmarkMetrics, type SonicTwinDecision, } from "./sonic-twin"; +export { + characterizeNuisance, + selectHardNegativePairs, + specimenDisjointSplit, + type BenchmarkSplit, + type HardNegativePairV1, + type LabeledFingerprintObservationV1, + type NuisanceCharacterizationV1, + type NuisanceFactor, + type NuisanceFactorSummaryV1, + type NuisanceMetadataV1, + type WithinSpecimenPairMetricV1, +} from "./nuisance"; From 20f879e0f0d74d3d776ac662e14a248108f56bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:31:45 -0700 Subject: [PATCH 050/101] Test nuisance characterization, hard negatives, and specimen-disjoint splits --- packages/fingerprint/test/nuisance.test.ts | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 packages/fingerprint/test/nuisance.test.ts diff --git a/packages/fingerprint/test/nuisance.test.ts b/packages/fingerprint/test/nuisance.test.ts new file mode 100644 index 00000000..8fff4389 --- /dev/null +++ b/packages/fingerprint/test/nuisance.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + characterizeNuisance, + selectHardNegativePairs, + specimenDisjointSplit, + type LabeledFingerprintObservationV1, + type NuisanceMetadataV1, +} from "../src"; + +function fp(frequencies: readonly number[]): AcousticFingerprintV1 { + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: frequencies.map((frequencyHz, index) => ({ + frequencyHz, + relativeAmplitude: 1 / (index + 1), + decaySeconds: 0.8 / (index + 1), + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), + }; +} + +const baseNuisance: NuisanceMetadataV1 = { + strikeLocation: "A", + striker: "wood", + support: "felt", + microphoneDistanceClass: "20cm", + room: "room-1", + stationId: "station-1", + operatorId: "operator-1", + dayId: "day-1", +}; + +function observation(id: string, specimenId: string, frequencies: readonly number[], nuisance: NuisanceMetadataV1): LabeledFingerprintObservationV1 { + return { observationId: id, specimenId, fingerprint: fp(frequencies), nuisance }; +} + +describe("Sonic Twin nuisance benchmark utilities", () => { + it("isolates one-factor repeated-measurement drift", () => { + const data = [ + observation("a1", "specimen-a", [440, 880, 1320], baseNuisance), + observation("a2", "specimen-a", [441, 882, 1323], { ...baseNuisance, strikeLocation: "B" }), + observation("a3", "specimen-a", [442, 884, 1326], { ...baseNuisance, stationId: "station-2" }), + ]; + const report = characterizeNuisance(data); + expect(report.specimenCount).toBe(1); + expect(report.withinSpecimenPairs).toHaveLength(3); + expect(report.factorSummaries.find((summary) => summary.factor === "strikeLocation")?.comparisonCount).toBe(1); + expect(report.factorSummaries.find((summary) => summary.factor === "stationId")?.comparisonCount).toBe(1); + }); + + it("selects the acoustically closest different-specimen observations as hard negatives", () => { + const data = [ + observation("a", "specimen-a", [440, 880, 1320], baseNuisance), + observation("b", "specimen-b", [441, 882, 1323], baseNuisance), + observation("c", "specimen-c", [600, 1200, 1800], baseNuisance), + ]; + const hard = selectHardNegativePairs(data, 1); + expect(hard).toHaveLength(1); + expect(new Set([hard[0]?.leftSpecimenId, hard[0]?.rightSpecimenId])).toEqual(new Set(["specimen-a", "specimen-b"])); + }); + + it("assigns every capture of a specimen to the same deterministic benchmark split", () => { + const first = specimenDisjointSplit("Specimen-001"); + expect(first).toBe(specimenDisjointSplit(" specimen-001 ")); + expect(["train", "validation", "test"]).toContain(first); + }); +}); From 82e43fd91c5b34f2ea241f75939f7589d879d488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:39:24 -0700 Subject: [PATCH 051/101] Fix synchronous Gate C planning rejection test --- packages/validation/test/review-plans.test.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/validation/test/review-plans.test.ts b/packages/validation/test/review-plans.test.ts index ef3db36a..01d330ad 100644 --- a/packages/validation/test/review-plans.test.ts +++ b/packages/validation/test/review-plans.test.ts @@ -22,7 +22,7 @@ function sixObjects() { ]; } -function gateBReviews(plan: Awaited>): GateBReview[] { +function gateBReviews(plan: ReturnType): GateBReview[] { return plan.targets.flatMap((target) => plan.reviewerIds.map((reviewerId, index) => ({ reviewId: `b-${target.specimenId}-${index}`, reviewerId, @@ -38,7 +38,7 @@ function gateBReviews(plan: Awaited>): GateBR }))); } -function gateCReviews(plan: Awaited>): GateCReview[] { +function gateCReviews(plan: ReturnType): GateCReview[] { return plan.targets.flatMap((target) => plan.reviewerIds.flatMap((reviewerId) => plan.devices.map((device) => ({ reviewId: `c-${target.specimenId}-${reviewerId}-${device.deviceId}`, reviewerId, @@ -55,10 +55,10 @@ function gateCReviews(plan: Awaited>): GateCR } describe("canonical post-collection plans", () => { - it("selects exactly five Gate B targets including metal, glass, and ceramic", async () => { + it("selects exactly five Gate B targets including metal, glass, and ceramic", () => { const gateA = evaluateGateARelease(sixObjects()); expect(gateA.passed).toBe(true); - const plan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + const plan = createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); expect(plan.targets).toHaveLength(5); const materialBySpecimen = new Map(gateA.sessions.map((session) => [session.specimenId, session.material])); const materials = new Set(plan.targets.map((target) => materialBySpecimen.get(target.specimenId))); @@ -67,9 +67,9 @@ describe("canonical post-collection plans", () => { expect(materials.has("ceramic")).toBe(true); }); - it("rejects Gate B when one fixed-panel judgment is missing", async () => { + it("rejects Gate B when one fixed-panel judgment is missing", () => { const gateA = evaluateGateARelease(sixObjects()); - const plan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + const plan = createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); const reviews = gateBReviews(plan); expect(evaluateCanonicalGateB(plan, gateA, reviews).passed).toBe(true); const missing = reviews.slice(0, -1); @@ -78,14 +78,14 @@ describe("canonical post-collection plans", () => { expect(result.observedJudgmentCount).toBe(9); }); - it("requires the full four-target by two-reviewer by two-device Gate C matrix", async () => { + it("requires the full four-target by two-reviewer by two-device Gate C matrix", () => { const gateA = evaluateGateARelease(sixObjects()); - const gateBPlan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + const gateBPlan = createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); const gateBReviewsAll = gateBReviews(gateBPlan); const canonicalB = evaluateCanonicalGateB(gateBPlan, gateA, gateBReviewsAll); expect(canonicalB.passed).toBe(true); - const gateCPlan = await createGateCPlan(canonicalB.verdict, ["reviewer-c", "reviewer-d"], [ + const gateCPlan = createGateCPlan(canonicalB.verdict, ["reviewer-c", "reviewer-d"], [ { deviceId: "desktop-1", deviceClass: "desktop" }, { deviceId: "phone-1", deviceClass: "mobile" }, ]); @@ -99,13 +99,13 @@ describe("canonical post-collection plans", () => { expect(result.observedJudgmentCount).toBe(15); }); - it("rejects a Gate C plan without a mobile device", async () => { + it("rejects a Gate C plan without a mobile device", () => { const gateA = evaluateGateARelease(sixObjects()); - const bPlan = await createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); + const bPlan = createGateBPlan(gateA, ["reviewer-a", "reviewer-b"]); const bVerdict = evaluateGateBRelease(gateA, gateBReviews(bPlan)); - await expect(createGateCPlan(bVerdict, ["reviewer-c", "reviewer-d"], [ + expect(() => createGateCPlan(bVerdict, ["reviewer-c", "reviewer-d"], [ { deviceId: "desktop-1", deviceClass: "desktop" }, { deviceId: "desktop-2", deviceClass: "desktop" }, - ])).rejects.toThrow(/mobile/); + ])).toThrow(/mobile/); }); }); From ea9c9a2079d7c9cbf86f721cb91107a10848344b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:41:34 -0700 Subject: [PATCH 052/101] Add acoustic object model aggregation --- packages/fingerprint/src/object-model.ts | 239 +++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 packages/fingerprint/src/object-model.ts diff --git a/packages/fingerprint/src/object-model.ts b/packages/fingerprint/src/object-model.ts new file mode 100644 index 00000000..8ab3061a --- /dev/null +++ b/packages/fingerprint/src/object-model.ts @@ -0,0 +1,239 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { centsDistance } from "./recurrence"; + +export interface AcousticObjectObservationV1 { + readonly observationId: string; + readonly specimenId: string; + readonly fingerprint: AcousticFingerprintV1; +} + +export interface AcousticObjectModelConfigV1 { + readonly clusteringToleranceCents: number; + readonly minimumObservationSupportFraction: number; + readonly minimumObservationSupportCount: number; +} + +export const DEFAULT_ACOUSTIC_OBJECT_MODEL_CONFIG: AcousticObjectModelConfigV1 = { + clusteringToleranceCents: 80, + minimumObservationSupportFraction: 0.5, + minimumObservationSupportCount: 2, +}; + +export interface AcousticObjectModeEstimateV1 { + readonly frequencyHzMedian: number; + readonly frequencyMadCents: number; + readonly decaySecondsMedian: number; + readonly decayLogMad: number; + readonly relativeAmplitudeMedian: number; + readonly relativeAmplitudeDbMad: number; + readonly observationSupportCount: number; + readonly observationSupportFraction: number; + readonly sourceObservationIds: readonly string[]; +} + +export interface AcousticObjectModelV1 { + readonly schemaVersion: 1; + readonly objectModelVersion: "acoustic-object-model-1"; + readonly specimenId: string; + readonly observationCount: number; + readonly fingerprintAlgorithmVersions: readonly string[]; + readonly sampleRates: readonly number[]; + readonly modes: readonly AcousticObjectModeEstimateV1[]; +} + +interface ModeSample { + readonly observationId: string; + readonly frequencyHz: number; + readonly decaySeconds: number; + readonly relativeAmplitude: number; +} + +interface MutableCluster { + readonly samples: ModeSample[]; +} + +function median(values: readonly number[]): number { + if (values.length === 0) throw new Error("median requires at least one value"); + const ordered = [...values].sort((left, right) => left - right); + const middle = Math.floor(ordered.length / 2); + if (ordered.length % 2 === 1) return ordered[middle]!; + return (ordered[middle - 1]! + ordered[middle]!) / 2; +} + +function medianAbsoluteDeviation(values: readonly number[]): number { + const center = median(values); + return median(values.map((value) => Math.abs(value - center))); +} + +function dbAmplitude(value: number): number { + return 20 * Math.log10(Math.max(value, 1e-9)); +} + +function clusterCenterFrequency(cluster: MutableCluster): number { + return median(cluster.samples.map((sample) => sample.frequencyHz)); +} + +function validateConfig(config: AcousticObjectModelConfigV1): void { + if (!(config.clusteringToleranceCents > 0) || !Number.isFinite(config.clusteringToleranceCents)) { + throw new Error("object-model clusteringToleranceCents must be finite and positive"); + } + if (!(config.minimumObservationSupportFraction > 0 && config.minimumObservationSupportFraction <= 1)) { + throw new Error("object-model minimumObservationSupportFraction must be in (0, 1]"); + } + if (!Number.isInteger(config.minimumObservationSupportCount) || config.minimumObservationSupportCount <= 0) { + throw new Error("object-model minimumObservationSupportCount must be a positive integer"); + } +} + +export function buildAcousticObjectModel( + observations: readonly AcousticObjectObservationV1[], + config: AcousticObjectModelConfigV1 = DEFAULT_ACOUSTIC_OBJECT_MODEL_CONFIG, +): AcousticObjectModelV1 { + validateConfig(config); + if (observations.length === 0) throw new Error("object model requires at least one observation"); + const specimenId = observations[0]!.specimenId.trim(); + if (specimenId.length === 0) throw new Error("object model specimenId is required"); + const normalized = specimenId.toLocaleLowerCase("en-US"); + if (observations.some((observation) => observation.specimenId.trim().toLocaleLowerCase("en-US") !== normalized)) { + throw new Error("object model cannot mix different specimen IDs"); + } + const observationIds = new Set(); + for (const observation of observations) { + const id = observation.observationId.trim(); + if (id.length === 0) throw new Error("object model observationId is required"); + if (observationIds.has(id)) throw new Error(`duplicate object-model observationId ${id}`); + observationIds.add(id); + } + + const clusters: MutableCluster[] = []; + for (const observation of observations) { + const usedClusters = new Set(); + const modes = [...observation.fingerprint.modes].sort((left, right) => left.frequencyHz - right.frequencyHz); + for (const mode of modes) { + let bestIndex = -1; + let bestDistance = Number.POSITIVE_INFINITY; + for (let index = 0; index < clusters.length; index += 1) { + if (usedClusters.has(index)) continue; + const distance = centsDistance(clusterCenterFrequency(clusters[index]!), mode.frequencyHz); + if (distance <= config.clusteringToleranceCents && distance < bestDistance) { + bestIndex = index; + bestDistance = distance; + } + } + const sample: ModeSample = { + observationId: observation.observationId, + frequencyHz: mode.frequencyHz, + decaySeconds: mode.decaySeconds, + relativeAmplitude: mode.relativeAmplitude, + }; + if (bestIndex < 0) { + clusters.push({ samples: [sample] }); + usedClusters.add(clusters.length - 1); + } else { + clusters[bestIndex]!.samples.push(sample); + usedClusters.add(bestIndex); + } + } + } + + const minimumSupport = Math.max( + config.minimumObservationSupportCount, + Math.ceil(observations.length * config.minimumObservationSupportFraction), + ); + const modes = clusters + .filter((cluster) => new Set(cluster.samples.map((sample) => sample.observationId)).size >= minimumSupport) + .map((cluster): AcousticObjectModeEstimateV1 => { + const frequencies = cluster.samples.map((sample) => sample.frequencyHz); + const center = median(frequencies); + const frequencyOffsets = frequencies.map((frequency) => centsDistance(center, frequency)); + const decays = cluster.samples.map((sample) => sample.decaySeconds); + const decayLogs = decays.map((value) => Math.log(Math.max(value, 1e-9))); + const amplitudes = cluster.samples.map((sample) => sample.relativeAmplitude); + const amplitudeDb = amplitudes.map(dbAmplitude); + const sourceObservationIds = [...new Set(cluster.samples.map((sample) => sample.observationId))].sort(); + return { + frequencyHzMedian: center, + frequencyMadCents: medianAbsoluteDeviation(frequencyOffsets), + decaySecondsMedian: median(decays), + decayLogMad: medianAbsoluteDeviation(decayLogs), + relativeAmplitudeMedian: median(amplitudes), + relativeAmplitudeDbMad: medianAbsoluteDeviation(amplitudeDb), + observationSupportCount: sourceObservationIds.length, + observationSupportFraction: sourceObservationIds.length / observations.length, + sourceObservationIds, + }; + }) + .sort((left, right) => left.frequencyHzMedian - right.frequencyHzMedian); + + return { + schemaVersion: 1, + objectModelVersion: "acoustic-object-model-1", + specimenId, + observationCount: observations.length, + fingerprintAlgorithmVersions: [...new Set(observations.map((observation) => observation.fingerprint.algorithmVersion))].sort(), + sampleRates: [...new Set(observations.map((observation) => observation.fingerprint.sampleRate))].sort((left, right) => left - right), + modes, + }; +} + +export interface FingerprintToObjectModelComparisonV1 { + readonly comparisonVersion: "fingerprint-object-model-comparison-1"; + readonly specimenId: string; + readonly matchedModelModes: number; + readonly modelModeCount: number; + readonly coverage: number; + readonly medianFrequencyDistanceCents: number | null; + readonly p90FrequencyDistanceCents: number | null; + readonly normalizedDistance: number; + readonly evidenceScore: number; +} + +function percentile(values: readonly number[], quantile: number): number | null { + if (values.length === 0) return null; + const ordered = [...values].sort((left, right) => left - right); + const index = Math.min(ordered.length - 1, Math.max(0, Math.ceil(ordered.length * quantile) - 1)); + return ordered[index] ?? null; +} + +export function compareFingerprintToObjectModel( + fingerprint: AcousticFingerprintV1, + model: AcousticObjectModelV1, + maximumMatchDistanceCents = 180, +): FingerprintToObjectModelComparisonV1 { + if (!(maximumMatchDistanceCents > 0) || !Number.isFinite(maximumMatchDistanceCents)) { + throw new Error("maximumMatchDistanceCents must be finite and positive"); + } + const usedFingerprintModes = new Set(); + const distances: number[] = []; + for (const modelMode of model.modes) { + let bestIndex = -1; + let bestDistance = Number.POSITIVE_INFINITY; + for (let index = 0; index < fingerprint.modes.length; index += 1) { + if (usedFingerprintModes.has(index)) continue; + const distance = centsDistance(modelMode.frequencyHzMedian, fingerprint.modes[index]!.frequencyHz); + if (distance < bestDistance) { + bestIndex = index; + bestDistance = distance; + } + } + if (bestIndex >= 0 && bestDistance <= maximumMatchDistanceCents) { + usedFingerprintModes.add(bestIndex); + distances.push(bestDistance); + } + } + const coverage = model.modes.length === 0 ? 0 : distances.length / model.modes.length; + const medianDistance = distances.length === 0 ? null : median(distances); + const normalizedFrequency = medianDistance === null ? 1 : Math.min(1, medianDistance / maximumMatchDistanceCents); + const normalizedDistance = Math.min(1, 0.65 * normalizedFrequency + 0.35 * (1 - coverage)); + return { + comparisonVersion: "fingerprint-object-model-comparison-1", + specimenId: model.specimenId, + matchedModelModes: distances.length, + modelModeCount: model.modes.length, + coverage, + medianFrequencyDistanceCents: medianDistance, + p90FrequencyDistanceCents: percentile(distances, 0.9), + normalizedDistance, + evidenceScore: 1 - normalizedDistance, + }; +} From e88deaf4cb56402c43635fca8b011dffd26b368a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:41:54 -0700 Subject: [PATCH 053/101] Add Sonic Twin retrieval benchmark --- packages/fingerprint/src/retrieval.ts | 129 ++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 packages/fingerprint/src/retrieval.ts diff --git a/packages/fingerprint/src/retrieval.ts b/packages/fingerprint/src/retrieval.ts new file mode 100644 index 00000000..b51c941d --- /dev/null +++ b/packages/fingerprint/src/retrieval.ts @@ -0,0 +1,129 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + buildAcousticObjectModel, + compareFingerprintToObjectModel, + type AcousticObjectModelV1, + type AcousticObjectObservationV1, +} from "./object-model"; + +export interface SonicTwinIndexEntryV1 { + readonly specimenId: string; + readonly model: AcousticObjectModelV1; +} + +export interface SonicTwinIndexV1 { + readonly schemaVersion: 1; + readonly indexVersion: "sonic-twin-index-1"; + readonly entries: readonly SonicTwinIndexEntryV1[]; +} + +export function buildSonicTwinIndex( + observations: readonly AcousticObjectObservationV1[], +): SonicTwinIndexV1 { + const grouped = new Map(); + for (const observation of observations) { + const normalized = observation.specimenId.trim().toLocaleLowerCase("en-US"); + if (normalized.length === 0) throw new Error("Sonic Twin index observations require specimenId"); + const current = grouped.get(normalized) ?? []; + current.push(observation); + grouped.set(normalized, current); + } + const entries = [...grouped.values()] + .map((group) => ({ specimenId: group[0]!.specimenId.trim(), model: buildAcousticObjectModel(group) })) + .sort((left, right) => left.specimenId.localeCompare(right.specimenId)); + return { schemaVersion: 1, indexVersion: "sonic-twin-index-1", entries }; +} + +export interface SonicTwinRetrievalResultV1 { + readonly rank: number; + readonly specimenId: string; + readonly evidenceScore: number; + readonly normalizedDistance: number; + readonly matchedModelModes: number; + readonly modelModeCount: number; +} + +export function retrieveSonicTwin( + query: AcousticFingerprintV1, + index: SonicTwinIndexV1, + limit = index.entries.length, +): readonly SonicTwinRetrievalResultV1[] { + if (!Number.isInteger(limit) || limit <= 0) throw new Error("retrieval limit must be a positive integer"); + return index.entries + .map((entry) => { + const comparison = compareFingerprintToObjectModel(query, entry.model); + return { + specimenId: entry.specimenId, + evidenceScore: comparison.evidenceScore, + normalizedDistance: comparison.normalizedDistance, + matchedModelModes: comparison.matchedModelModes, + modelModeCount: comparison.modelModeCount, + }; + }) + .sort((left, right) => right.evidenceScore - left.evidenceScore || left.specimenId.localeCompare(right.specimenId)) + .slice(0, Math.min(limit, index.entries.length)) + .map((result, indexValue) => ({ rank: indexValue + 1, ...result })); +} + +export interface SonicTwinRetrievalQueryV1 { + readonly queryId: string; + readonly trueSpecimenId: string; + readonly fingerprint: AcousticFingerprintV1; +} + +export interface SonicTwinRetrievalMetricsV1 { + readonly queryCount: number; + readonly eligibleQueryCount: number; + readonly recallAt1: number | null; + readonly recallAt3: number | null; + readonly recallAt5: number | null; + readonly meanReciprocalRank: number | null; + readonly meanTrueSpecimenRank: number | null; + readonly missingTrueSpecimenCount: number; +} + +function rate(numerator: number, denominator: number): number | null { + return denominator === 0 ? null : numerator / denominator; +} + +export function benchmarkSonicTwinRetrieval( + queries: readonly SonicTwinRetrievalQueryV1[], + index: SonicTwinIndexV1, +): SonicTwinRetrievalMetricsV1 { + const indexedSpecimens = new Set(index.entries.map((entry) => entry.specimenId.trim().toLocaleLowerCase("en-US"))); + let eligible = 0; + let hit1 = 0; + let hit3 = 0; + let hit5 = 0; + let reciprocalRankSum = 0; + let rankSum = 0; + let missing = 0; + + for (const query of queries) { + const target = query.trueSpecimenId.trim().toLocaleLowerCase("en-US"); + if (!indexedSpecimens.has(target)) { + missing += 1; + continue; + } + eligible += 1; + const ranked = retrieveSonicTwin(query.fingerprint, index); + const rank = ranked.findIndex((result) => result.specimenId.trim().toLocaleLowerCase("en-US") === target) + 1; + if (rank <= 0) throw new Error("indexed true specimen disappeared from retrieval ranking"); + if (rank <= 1) hit1 += 1; + if (rank <= 3) hit3 += 1; + if (rank <= 5) hit5 += 1; + reciprocalRankSum += 1 / rank; + rankSum += rank; + } + + return { + queryCount: queries.length, + eligibleQueryCount: eligible, + recallAt1: rate(hit1, eligible), + recallAt3: rate(hit3, eligible), + recallAt5: rate(hit5, eligible), + meanReciprocalRank: eligible === 0 ? null : reciprocalRankSum / eligible, + meanTrueSpecimenRank: eligible === 0 ? null : rankSum / eligible, + missingTrueSpecimenCount: missing, + }; +} From 330b5489556df5420eb573faf4584a04fc9c110c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:42:27 -0700 Subject: [PATCH 054/101] Add data-bound Sonic Twin calibration metrics --- packages/fingerprint/src/calibration.ts | 209 ++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 packages/fingerprint/src/calibration.ts diff --git a/packages/fingerprint/src/calibration.ts b/packages/fingerprint/src/calibration.ts new file mode 100644 index 00000000..31e05cf7 --- /dev/null +++ b/packages/fingerprint/src/calibration.ts @@ -0,0 +1,209 @@ +export interface LabeledSimilarityScoreV1 { + readonly pairId: string; + readonly score: number; + readonly samePhysicalSpecimen: boolean; +} + +export interface IsotonicCalibrationBinV1 { + readonly minimumScore: number; + readonly maximumScore: number; + readonly sampleCount: number; + readonly positiveCount: number; + readonly probability: number; +} + +export interface SonicTwinCalibrationModelV1 { + readonly schemaVersion: 1; + readonly calibrationVersion: "sonic-twin-isotonic-calibration-1"; + readonly trainingPopulation: string; + readonly labelSemantics: "same-physical-specimen"; + readonly trainingSampleCount: number; + readonly trainingPositiveCount: number; + readonly bins: readonly IsotonicCalibrationBinV1[]; +} + +interface MutableBlock { + minimumScore: number; + maximumScore: number; + sampleCount: number; + positiveCount: number; +} + +function assertScore(score: number): void { + if (!Number.isFinite(score)) throw new Error("similarity score must be finite"); +} + +function blockProbability(block: MutableBlock): number { + return block.positiveCount / block.sampleCount; +} + +export function fitIsotonicCalibration( + samples: readonly LabeledSimilarityScoreV1[], + trainingPopulation: string, +): SonicTwinCalibrationModelV1 { + if (samples.length === 0) throw new Error("calibration requires labeled samples"); + if (trainingPopulation.trim().length === 0) throw new Error("calibration trainingPopulation is required"); + const seen = new Set(); + for (const sample of samples) { + assertScore(sample.score); + if (sample.pairId.trim().length === 0) throw new Error("calibration pairId is required"); + if (seen.has(sample.pairId)) throw new Error(`duplicate calibration pairId ${sample.pairId}`); + seen.add(sample.pairId); + } + + const ordered = [...samples].sort((left, right) => left.score - right.score || left.pairId.localeCompare(right.pairId)); + const blocks: MutableBlock[] = ordered.map((sample) => ({ + minimumScore: sample.score, + maximumScore: sample.score, + sampleCount: 1, + positiveCount: sample.samePhysicalSpecimen ? 1 : 0, + })); + + let index = 0; + while (index < blocks.length - 1) { + if (blockProbability(blocks[index]!) <= blockProbability(blocks[index + 1]!)) { + index += 1; + continue; + } + const left = blocks[index]!; + const right = blocks[index + 1]!; + blocks.splice(index, 2, { + minimumScore: left.minimumScore, + maximumScore: right.maximumScore, + sampleCount: left.sampleCount + right.sampleCount, + positiveCount: left.positiveCount + right.positiveCount, + }); + if (index > 0) index -= 1; + } + + return { + schemaVersion: 1, + calibrationVersion: "sonic-twin-isotonic-calibration-1", + trainingPopulation: trainingPopulation.trim(), + labelSemantics: "same-physical-specimen", + trainingSampleCount: samples.length, + trainingPositiveCount: samples.filter((sample) => sample.samePhysicalSpecimen).length, + bins: blocks.map((block) => ({ ...block, probability: blockProbability(block) })), + }; +} + +export function predictCalibratedProbability( + model: SonicTwinCalibrationModelV1, + score: number, +): number { + assertScore(score); + if (model.bins.length === 0) throw new Error("calibration model has no bins"); + if (score <= model.bins[0]!.maximumScore) return model.bins[0]!.probability; + for (let index = 1; index < model.bins.length; index += 1) { + const previous = model.bins[index - 1]!; + const current = model.bins[index]!; + if (score <= current.maximumScore) { + if (score <= current.minimumScore) { + const span = current.minimumScore - previous.maximumScore; + if (!(span > 0)) return current.probability; + const t = (score - previous.maximumScore) / span; + return previous.probability + Math.max(0, Math.min(1, t)) * (current.probability - previous.probability); + } + return current.probability; + } + } + return model.bins[model.bins.length - 1]!.probability; +} + +export interface CalibratedPredictionV1 { + readonly pairId: string; + readonly probability: number; + readonly samePhysicalSpecimen: boolean; +} + +export interface CalibrationMetricsV1 { + readonly sampleCount: number; + readonly brierScore: number | null; + readonly expectedCalibrationError: number | null; + readonly rocAuc: number | null; +} + +export function calibratedPredictions( + model: SonicTwinCalibrationModelV1, + samples: readonly LabeledSimilarityScoreV1[], +): readonly CalibratedPredictionV1[] { + return samples.map((sample) => ({ + pairId: sample.pairId, + probability: predictCalibratedProbability(model, sample.score), + samePhysicalSpecimen: sample.samePhysicalSpecimen, + })); +} + +function rocAuc(predictions: readonly CalibratedPredictionV1[]): number | null { + const positives = predictions.filter((prediction) => prediction.samePhysicalSpecimen); + const negatives = predictions.filter((prediction) => !prediction.samePhysicalSpecimen); + if (positives.length === 0 || negatives.length === 0) return null; + let wins = 0; + for (const positive of positives) { + for (const negative of negatives) { + if (positive.probability > negative.probability) wins += 1; + else if (positive.probability === negative.probability) wins += 0.5; + } + } + return wins / (positives.length * negatives.length); +} + +export function evaluateCalibration( + predictions: readonly CalibratedPredictionV1[], + binCount = 10, +): CalibrationMetricsV1 { + if (!Number.isInteger(binCount) || binCount <= 0) throw new Error("calibration binCount must be a positive integer"); + if (predictions.length === 0) return { sampleCount: 0, brierScore: null, expectedCalibrationError: null, rocAuc: null }; + let squared = 0; + for (const prediction of predictions) { + if (!(prediction.probability >= 0 && prediction.probability <= 1) || !Number.isFinite(prediction.probability)) { + throw new Error("calibrated probability must be finite and in [0,1]"); + } + const label = prediction.samePhysicalSpecimen ? 1 : 0; + squared += (prediction.probability - label) ** 2; + } + + let ece = 0; + for (let bin = 0; bin < binCount; bin += 1) { + const lower = bin / binCount; + const upper = (bin + 1) / binCount; + const members = predictions.filter((prediction) => prediction.probability >= lower && (bin === binCount - 1 ? prediction.probability <= upper : prediction.probability < upper)); + if (members.length === 0) continue; + const meanProbability = members.reduce((sum, member) => sum + member.probability, 0) / members.length; + const positiveRate = members.filter((member) => member.samePhysicalSpecimen).length / members.length; + ece += members.length / predictions.length * Math.abs(meanProbability - positiveRate); + } + + return { + sampleCount: predictions.length, + brierScore: squared / predictions.length, + expectedCalibrationError: ece, + rocAuc: rocAuc(predictions), + }; +} + +export interface RiskCoveragePointV1 { + readonly minimumConfidence: number; + readonly coverage: number; + readonly errorRate: number | null; + readonly coveredCount: number; +} + +export function riskCoverageCurve( + predictions: readonly CalibratedPredictionV1[], + thresholds: readonly number[] = [0, 0.1, 0.2, 0.3, 0.4, 0.49], +): readonly RiskCoveragePointV1[] { + return thresholds.map((minimumConfidence) => { + if (!(minimumConfidence >= 0 && minimumConfidence <= 0.5) || !Number.isFinite(minimumConfidence)) { + throw new Error("minimumConfidence must be finite and in [0,0.5]"); + } + const covered = predictions.filter((prediction) => Math.abs(prediction.probability - 0.5) >= minimumConfidence); + const errors = covered.filter((prediction) => (prediction.probability >= 0.5) !== prediction.samePhysicalSpecimen).length; + return { + minimumConfidence, + coverage: predictions.length === 0 ? 0 : covered.length / predictions.length, + errorRate: covered.length === 0 ? null : errors / covered.length, + coveredCount: covered.length, + }; + }); +} From 6efcdbb3f784335daeec537d0d4ad2b0a27f2906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:42:53 -0700 Subject: [PATCH 055/101] Add Sonic Twin benchmark corpus contract --- packages/fingerprint/src/benchmark-corpus.ts | 163 +++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 packages/fingerprint/src/benchmark-corpus.ts diff --git a/packages/fingerprint/src/benchmark-corpus.ts b/packages/fingerprint/src/benchmark-corpus.ts new file mode 100644 index 00000000..c1bf3cf3 --- /dev/null +++ b/packages/fingerprint/src/benchmark-corpus.ts @@ -0,0 +1,163 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { selectHardNegativePairs, specimenDisjointSplit, type BenchmarkSplit, type NuisanceMetadataV1 } from "./nuisance"; + +export interface TwinBenchmarkObservationV1 { + readonly observationId: string; + readonly specimenId: string; + readonly objectFamily: string; + readonly source: "digital-twin" | "physical" | "external-dataset"; + readonly fingerprint: AcousticFingerprintV1; + readonly nuisance: NuisanceMetadataV1; +} + +export interface TwinBenchmarkCorpusV1 { + readonly schemaVersion: 1; + readonly corpusContractVersion: "sonic-twin-benchmark-corpus-1"; + readonly corpusId: string; + readonly createdAt: string; + readonly observations: readonly TwinBenchmarkObservationV1[]; +} + +export interface TwinBenchmarkCorpusValidationV1 { + readonly valid: boolean; + readonly observationCount: number; + readonly specimenCount: number; + readonly objectFamilyCount: number; + readonly sourceCounts: Readonly>; + readonly splitSpecimenCounts: Readonly>; + readonly reasons: readonly string[]; +} + +function normalized(value: string): string { + return value.trim().toLocaleLowerCase("en-US"); +} + +export function validateTwinBenchmarkCorpus(corpus: TwinBenchmarkCorpusV1): TwinBenchmarkCorpusValidationV1 { + const reasons: string[] = []; + if (corpus.schemaVersion !== 1) reasons.push("schemaVersion must be 1"); + if (corpus.corpusContractVersion !== "sonic-twin-benchmark-corpus-1") reasons.push("corpus contract version mismatch"); + if (corpus.corpusId.trim().length === 0) reasons.push("corpusId is required"); + if (!Number.isFinite(Date.parse(corpus.createdAt))) reasons.push("createdAt is invalid"); + if (corpus.observations.length === 0) reasons.push("corpus must contain observations"); + + const observationIds = new Set(); + const specimenFamily = new Map(); + const specimenSplit = new Map(); + const sourceCounts: Record = { + "digital-twin": 0, + physical: 0, + "external-dataset": 0, + }; + + for (const observation of corpus.observations) { + const observationId = observation.observationId.trim(); + const specimenId = normalized(observation.specimenId); + const family = normalized(observation.objectFamily); + if (observationId.length === 0) reasons.push("observationId is required"); + if (observationIds.has(observationId)) reasons.push(`duplicate observationId ${observationId}`); + observationIds.add(observationId); + if (specimenId.length === 0) reasons.push(`observation ${observationId} has empty specimenId`); + if (family.length === 0) reasons.push(`observation ${observationId} has empty objectFamily`); + const priorFamily = specimenFamily.get(specimenId); + if (priorFamily !== undefined && priorFamily !== family) reasons.push(`specimen ${observation.specimenId} has conflicting objectFamily labels`); + else specimenFamily.set(specimenId, family); + const split = specimenDisjointSplit(specimenId); + const priorSplit = specimenSplit.get(specimenId); + if (priorSplit !== undefined && priorSplit !== split) reasons.push(`specimen ${observation.specimenId} leaks across benchmark splits`); + specimenSplit.set(specimenId, split); + sourceCounts[observation.source] += 1; + if (observation.fingerprint.modes.length === 0) reasons.push(`observation ${observationId} has no fingerprint modes`); + } + + const splitSpecimenCounts: Record = { train: 0, validation: 0, test: 0 }; + for (const split of specimenSplit.values()) splitSpecimenCounts[split] += 1; + return { + valid: reasons.length === 0, + observationCount: corpus.observations.length, + specimenCount: specimenFamily.size, + objectFamilyCount: new Set(specimenFamily.values()).size, + sourceCounts, + splitSpecimenCounts, + reasons: [...new Set(reasons)], + }; +} + +export interface TwinVerificationPairV1 { + readonly pairId: string; + readonly samePhysicalSpecimen: boolean; + readonly leftObservationId: string; + readonly rightObservationId: string; + readonly leftSpecimenId: string; + readonly rightSpecimenId: string; + readonly split: BenchmarkSplit; + readonly reference: AcousticFingerprintV1; + readonly candidate: AcousticFingerprintV1; + readonly pairKind: "same-specimen" | "hard-negative"; +} + +function pairId(left: TwinBenchmarkObservationV1, right: TwinBenchmarkObservationV1, kind: string): string { + return `${kind}:${left.observationId}::${right.observationId}`; +} + +export function buildTwinVerificationPairs( + corpus: TwinBenchmarkCorpusV1, + hardNegativeLimitPerSplit = 200, +): readonly TwinVerificationPairV1[] { + const validation = validateTwinBenchmarkCorpus(corpus); + if (!validation.valid) throw new Error(`invalid benchmark corpus: ${validation.reasons.join("; ")}`); + if (!Number.isInteger(hardNegativeLimitPerSplit) || hardNegativeLimitPerSplit <= 0) { + throw new Error("hardNegativeLimitPerSplit must be a positive integer"); + } + const pairs: TwinVerificationPairV1[] = []; + for (const split of ["train", "validation", "test"] as const) { + const observations = corpus.observations.filter((observation) => specimenDisjointSplit(observation.specimenId) === split); + const bySpecimen = new Map(); + for (const observation of observations) { + const key = normalized(observation.specimenId); + const group = bySpecimen.get(key) ?? []; + group.push(observation); + bySpecimen.set(key, group); + } + for (const group of bySpecimen.values()) { + for (let leftIndex = 0; leftIndex < group.length; leftIndex += 1) { + for (let rightIndex = leftIndex + 1; rightIndex < group.length; rightIndex += 1) { + const left = group[leftIndex]!; + const right = group[rightIndex]!; + pairs.push({ + pairId: pairId(left, right, "same"), + samePhysicalSpecimen: true, + leftObservationId: left.observationId, + rightObservationId: right.observationId, + leftSpecimenId: left.specimenId, + rightSpecimenId: right.specimenId, + split, + reference: left.fingerprint, + candidate: right.fingerprint, + pairKind: "same-specimen", + }); + } + } + } + + const hard = selectHardNegativePairs(observations, Math.min(hardNegativeLimitPerSplit, Math.max(1, observations.length * observations.length))); + const byObservation = new Map(observations.map((observation) => [observation.observationId, observation] as const)); + for (const negative of hard) { + const left = byObservation.get(negative.leftObservationId); + const right = byObservation.get(negative.rightObservationId); + if (left === undefined || right === undefined) continue; + pairs.push({ + pairId: pairId(left, right, "hard-negative"), + samePhysicalSpecimen: false, + leftObservationId: left.observationId, + rightObservationId: right.observationId, + leftSpecimenId: left.specimenId, + rightSpecimenId: right.specimenId, + split, + reference: left.fingerprint, + candidate: right.fingerprint, + pairKind: "hard-negative", + }); + } + } + return pairs.sort((left, right) => left.split.localeCompare(right.split) || left.pairId.localeCompare(right.pairId)); +} From 5c8cc9b08f0b4d3b73c71120c91073fe97afdcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:43:07 -0700 Subject: [PATCH 056/101] Export object, retrieval, corpus, and calibration APIs --- packages/fingerprint/src/index.ts | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/fingerprint/src/index.ts b/packages/fingerprint/src/index.ts index 551a8cb9..3e97e964 100644 --- a/packages/fingerprint/src/index.ts +++ b/packages/fingerprint/src/index.ts @@ -29,3 +29,44 @@ export { type NuisanceMetadataV1, type WithinSpecimenPairMetricV1, } from "./nuisance"; +export { + DEFAULT_ACOUSTIC_OBJECT_MODEL_CONFIG, + buildAcousticObjectModel, + compareFingerprintToObjectModel, + type AcousticObjectModeEstimateV1, + type AcousticObjectModelConfigV1, + type AcousticObjectModelV1, + type AcousticObjectObservationV1, + type FingerprintToObjectModelComparisonV1, +} from "./object-model"; +export { + benchmarkSonicTwinRetrieval, + buildSonicTwinIndex, + retrieveSonicTwin, + type SonicTwinIndexEntryV1, + type SonicTwinIndexV1, + type SonicTwinRetrievalMetricsV1, + type SonicTwinRetrievalQueryV1, + type SonicTwinRetrievalResultV1, +} from "./retrieval"; +export { + buildTwinVerificationPairs, + validateTwinBenchmarkCorpus, + type TwinBenchmarkCorpusV1, + type TwinBenchmarkCorpusValidationV1, + type TwinBenchmarkObservationV1, + type TwinVerificationPairV1, +} from "./benchmark-corpus"; +export { + calibratedPredictions, + evaluateCalibration, + fitIsotonicCalibration, + predictCalibratedProbability, + riskCoverageCurve, + type CalibratedPredictionV1, + type CalibrationMetricsV1, + type IsotonicCalibrationBinV1, + type LabeledSimilarityScoreV1, + type RiskCoveragePointV1, + type SonicTwinCalibrationModelV1, +} from "./calibration"; From 48f65dcf892553c9e4d0540fb70702f33e59169e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:43:43 -0700 Subject: [PATCH 057/101] Test object models, retrieval, corpus, and calibration --- packages/fingerprint/test/full-vision.test.ts | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 packages/fingerprint/test/full-vision.test.ts diff --git a/packages/fingerprint/test/full-vision.test.ts b/packages/fingerprint/test/full-vision.test.ts new file mode 100644 index 00000000..64834020 --- /dev/null +++ b/packages/fingerprint/test/full-vision.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it } from "vitest"; +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + benchmarkSonicTwinRetrieval, + buildAcousticObjectModel, + buildSonicTwinIndex, + buildTwinVerificationPairs, + calibratedPredictions, + compareFingerprintToObjectModel, + evaluateCalibration, + fitIsotonicCalibration, + predictCalibratedProbability, + retrieveSonicTwin, + riskCoverageCurve, + validateTwinBenchmarkCorpus, + type AcousticObjectObservationV1, + type NuisanceMetadataV1, + type TwinBenchmarkCorpusV1, +} from "../src"; + +function fp(frequencies: readonly number[], amplitudeScale = 1, decayScale = 1): AcousticFingerprintV1 { + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: frequencies.map((frequencyHz, index) => ({ + frequencyHz, + relativeAmplitude: amplitudeScale / (index + 1), + decaySeconds: decayScale * 0.9 / (index + 1), + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), + }; +} + +function observations(): AcousticObjectObservationV1[] { + return [ + { observationId: "a-1", specimenId: "specimen-a", fingerprint: fp([440, 880, 1320]) }, + { observationId: "a-2", specimenId: "specimen-a", fingerprint: fp([441, 881.5, 1322]) }, + { observationId: "b-1", specimenId: "specimen-b", fingerprint: fp([610, 1220, 1830]) }, + { observationId: "b-2", specimenId: "specimen-b", fingerprint: fp([611, 1221, 1832]) }, + { observationId: "c-1", specimenId: "specimen-c", fingerprint: fp([442, 884, 1326]) }, + { observationId: "c-2", specimenId: "specimen-c", fingerprint: fp([443, 885, 1329]) }, + ]; +} + +const nuisance: NuisanceMetadataV1 = { + strikeLocation: "A", + striker: "wood", + support: "felt", + microphoneDistanceClass: "20cm", + room: "digital-room", + stationId: "digital-station", + operatorId: "digital-operator", + dayId: "day-1", +}; + +describe("full-vision Sonic Twin infrastructure", () => { + it("aggregates repeated observations into an uncertainty-preserving object model", () => { + const model = buildAcousticObjectModel(observations().filter((observation) => observation.specimenId === "specimen-a")); + expect(model.specimenId).toBe("specimen-a"); + expect(model.observationCount).toBe(2); + expect(model.modes).toHaveLength(3); + expect(model.modes.every((mode) => mode.observationSupportFraction === 1)).toBe(true); + expect(model.modes[0]!.frequencyHzMedian).toBeGreaterThan(440); + expect(model.modes[0]!.frequencyHzMedian).toBeLessThan(441.1); + expect(model.modes[0]!.frequencyMadCents).toBeGreaterThanOrEqual(0); + + const same = compareFingerprintToObjectModel(fp([440.5, 880.5, 1321]), model); + const different = compareFingerprintToObjectModel(fp([610, 1220, 1830]), model); + expect(same.evidenceScore).toBeGreaterThan(different.evidenceScore); + expect(same.coverage).toBe(1); + }); + + it("retrieves a query against specimen-level object models and reports ranking metrics", () => { + const index = buildSonicTwinIndex(observations()); + expect(index.entries).toHaveLength(3); + const ranked = retrieveSonicTwin(fp([440.8, 881, 1321.5]), index); + expect(ranked[0]?.specimenId).toBe("specimen-a"); + expect(ranked[0]?.rank).toBe(1); + + const metrics = benchmarkSonicTwinRetrieval([ + { queryId: "qa", trueSpecimenId: "specimen-a", fingerprint: fp([440.7, 881, 1321]) }, + { queryId: "qb", trueSpecimenId: "specimen-b", fingerprint: fp([610.5, 1220.5, 1831]) }, + ], index); + expect(metrics.recallAt1).toBe(1); + expect(metrics.meanReciprocalRank).toBe(1); + expect(metrics.missingTrueSpecimenCount).toBe(0); + }); + + it("validates a provenance-aware specimen-disjoint benchmark corpus and constructs labeled pairs", () => { + const data = observations(); + const corpus: TwinBenchmarkCorpusV1 = { + schemaVersion: 1, + corpusContractVersion: "sonic-twin-benchmark-corpus-1", + corpusId: "digital-full-vision-corpus-1", + createdAt: "2026-08-24T15:30:00.000Z", + observations: data.map((observation, index) => ({ + ...observation, + objectFamily: observation.specimenId === "specimen-b" ? "bowl" : "bell", + source: "digital-twin" as const, + nuisance: { ...nuisance, strikeLocation: index % 2 === 0 ? "A" : "B" }, + })), + }; + const validation = validateTwinBenchmarkCorpus(corpus); + expect(validation.valid).toBe(true); + expect(validation.specimenCount).toBe(3); + expect(validation.sourceCounts["digital-twin"]).toBe(6); + + const pairs = buildTwinVerificationPairs(corpus, 20); + expect(pairs.some((pair) => pair.samePhysicalSpecimen)).toBe(true); + expect(pairs.every((pair) => pair.split === pairs.find((candidate) => candidate.pairId === pair.pairId)?.split)).toBe(true); + for (const pair of pairs) { + if (pair.samePhysicalSpecimen) expect(pair.leftSpecimenId).toBe(pair.rightSpecimenId); + else expect(pair.leftSpecimenId).not.toBe(pair.rightSpecimenId); + } + }); + + it("fits a data-bound monotonic calibration model and reports calibration/risk metrics", () => { + const training = [ + { pairId: "n1", score: 0.05, samePhysicalSpecimen: false }, + { pairId: "n2", score: 0.15, samePhysicalSpecimen: false }, + { pairId: "n3", score: 0.35, samePhysicalSpecimen: false }, + { pairId: "p1", score: 0.65, samePhysicalSpecimen: true }, + { pairId: "p2", score: 0.85, samePhysicalSpecimen: true }, + { pairId: "p3", score: 0.95, samePhysicalSpecimen: true }, + ]; + const model = fitIsotonicCalibration(training, "digital-twin-corpus-1"); + expect(model.trainingSampleCount).toBe(6); + expect(predictCalibratedProbability(model, 0.9)).toBeGreaterThan(predictCalibratedProbability(model, 0.1)); + for (let index = 1; index < model.bins.length; index += 1) { + expect(model.bins[index]!.probability).toBeGreaterThanOrEqual(model.bins[index - 1]!.probability); + } + + const predictions = calibratedPredictions(model, training); + const metrics = evaluateCalibration(predictions, 5); + expect(metrics.brierScore).not.toBeNull(); + expect(metrics.rocAuc).toBe(1); + const curve = riskCoverageCurve(predictions, [0, 0.25, 0.49]); + expect(curve).toHaveLength(3); + expect(curve[0]!.coverage).toBe(1); + expect(curve[2]!.coverage).toBeLessThanOrEqual(curve[0]!.coverage); + }); +}); From d33196a19e9d9dce87fd916f5b0f9dae26181e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:44:13 -0700 Subject: [PATCH 058/101] Add append-only Atlas registry semantics --- packages/validation/src/atlas-registry.ts | 162 ++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 packages/validation/src/atlas-registry.ts diff --git a/packages/validation/src/atlas-registry.ts b/packages/validation/src/atlas-registry.ts new file mode 100644 index 00000000..d41ce961 --- /dev/null +++ b/packages/validation/src/atlas-registry.ts @@ -0,0 +1,162 @@ +import { + createAtlasSnapshot, + verifyAtlasRecord, + type ResonanceAtlasRecordV1, + type ResonanceAtlasSnapshotV1, +} from "./atlas"; +import { contentDigest } from "./provenance"; + +export interface AtlasSpecimenMergeV1 { + readonly schemaVersion: 1; + readonly mergeContractVersion: "atlas-specimen-merge-1"; + readonly mergeId: string; + readonly createdAt: string; + readonly aliasSpecimenId: string; + readonly canonicalSpecimenId: string; + readonly rationale: string; + readonly supportingRecordIds: readonly string[]; +} + +export interface AtlasRegistryStateV1 { + readonly schemaVersion: 1; + readonly registryContractVersion: "resonance-atlas-registry-1"; + readonly records: readonly ResonanceAtlasRecordV1[]; + readonly specimenMerges: readonly AtlasSpecimenMergeV1[]; +} + +export function emptyAtlasRegistry(): AtlasRegistryStateV1 { + return { schemaVersion: 1, registryContractVersion: "resonance-atlas-registry-1", records: [], specimenMerges: [] }; +} + +function normalized(value: string): string { + return value.trim().toLocaleLowerCase("en-US"); +} + +function assertDigest(value: string, field: string): void { + if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new Error(`${field} must be a sha256 content digest`); +} + +export async function publishAtlasRecord( + state: AtlasRegistryStateV1, + record: ResonanceAtlasRecordV1, +): Promise { + if (!await verifyAtlasRecord(record)) throw new Error("Atlas record failed content verification"); + if (state.records.some((existing) => existing.atlasRecordId === record.atlasRecordId)) return state; + + const measurementOwner = new Map(); + for (const existing of state.records) { + for (const measurement of existing.measurements) measurementOwner.set(measurement.measurementId, normalized(existing.specimen.specimenId)); + } + const incomingSpecimen = normalized(record.specimen.specimenId); + for (const measurement of record.measurements) { + const owner = measurementOwner.get(measurement.measurementId); + if (owner !== undefined && owner !== incomingSpecimen) { + throw new Error(`measurement ${measurement.measurementId} is already published under another specimen`); + } + } + + return { + ...state, + records: [...state.records, record].sort((left, right) => left.atlasRecordId.localeCompare(right.atlasRecordId)), + }; +} + +export async function createAtlasSpecimenMerge( + input: Omit, +): Promise { + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("Atlas specimen merge createdAt is invalid"); + if (normalized(input.aliasSpecimenId).length === 0 || normalized(input.canonicalSpecimenId).length === 0) throw new Error("merge specimen IDs are required"); + if (normalized(input.aliasSpecimenId) === normalized(input.canonicalSpecimenId)) throw new Error("merge alias and canonical specimen must differ"); + if (input.rationale.trim().length === 0) throw new Error("merge rationale is required"); + if (input.supportingRecordIds.length === 0) throw new Error("merge requires supporting Atlas records"); + const supportingRecordIds = [...new Set(input.supportingRecordIds)].sort(); + supportingRecordIds.forEach((id) => assertDigest(id, "supportingRecordId")); + const payload = { + ...input, + supportingRecordIds, + schemaVersion: 1 as const, + mergeContractVersion: "atlas-specimen-merge-1" as const, + }; + return { ...payload, mergeId: await contentDigest(payload) }; +} + +function resolveCanonicalSpecimenId(state: AtlasRegistryStateV1, specimenId: string): string { + let current = normalized(specimenId); + const visited = new Set(); + while (true) { + if (visited.has(current)) throw new Error("Atlas specimen merge cycle detected"); + visited.add(current); + const merge = state.specimenMerges.find((candidate) => normalized(candidate.aliasSpecimenId) === current); + if (merge === undefined) return current; + current = normalized(merge.canonicalSpecimenId); + } +} + +export function applyAtlasSpecimenMerge( + state: AtlasRegistryStateV1, + merge: AtlasSpecimenMergeV1, +): AtlasRegistryStateV1 { + if (state.specimenMerges.some((existing) => existing.mergeId === merge.mergeId)) return state; + const records = new Set(state.records.map((record) => record.atlasRecordId)); + for (const recordId of merge.supportingRecordIds) { + if (!records.has(recordId)) throw new Error(`merge supporting record ${recordId} is absent from the registry`); + } + const knownSpecimens = new Set(state.records.map((record) => normalized(record.specimen.specimenId))); + if (!knownSpecimens.has(normalized(merge.aliasSpecimenId))) throw new Error("merge alias specimen is absent from the registry"); + if (!knownSpecimens.has(normalized(merge.canonicalSpecimenId))) throw new Error("merge canonical specimen is absent from the registry"); + const candidate = { ...state, specimenMerges: [...state.specimenMerges, merge] }; + resolveCanonicalSpecimenId(candidate, merge.aliasSpecimenId); + return candidate; +} + +export interface AtlasSearchQueryV1 { + readonly text?: string; + readonly material?: string; + readonly objectFamily?: string; + readonly contributorId?: string; +} + +export interface AtlasSearchResultV1 { + readonly atlasRecordId: string; + readonly specimenId: string; + readonly canonicalSpecimenId: string; + readonly label: string; + readonly objectFamily: string; + readonly material: string; + readonly measurementCount: number; + readonly contributorId: string | null; +} + +export function searchAtlas( + state: AtlasRegistryStateV1, + query: AtlasSearchQueryV1, +): readonly AtlasSearchResultV1[] { + const text = normalized(query.text ?? ""); + const material = normalized(query.material ?? ""); + const family = normalized(query.objectFamily ?? ""); + const contributor = normalized(query.contributorId ?? ""); + return state.records + .filter((record) => text.length === 0 || normalized(`${record.specimen.label} ${record.specimen.publicDescription ?? ""} ${record.specimen.objectFamily}`).includes(text)) + .filter((record) => material.length === 0 || normalized(record.specimen.material) === material) + .filter((record) => family.length === 0 || normalized(record.specimen.objectFamily) === family) + .filter((record) => contributor.length === 0 || normalized(record.contributor?.contributorId ?? "") === contributor) + .map((record) => ({ + atlasRecordId: record.atlasRecordId, + specimenId: record.specimen.specimenId, + canonicalSpecimenId: resolveCanonicalSpecimenId(state, record.specimen.specimenId), + label: record.specimen.label, + objectFamily: record.specimen.objectFamily, + material: record.specimen.material, + measurementCount: record.measurements.length, + contributorId: record.contributor?.contributorId ?? null, + })) + .sort((left, right) => left.label.localeCompare(right.label) || left.atlasRecordId.localeCompare(right.atlasRecordId)); +} + +export async function snapshotAtlasRegistry( + state: AtlasRegistryStateV1, + createdAt: string, +): Promise { + if (state.records.length === 0) throw new Error("cannot snapshot an empty Atlas registry"); + return createAtlasSnapshot(state.records.map((record) => record.atlasRecordId), createdAt); +} From 2440ce685eda5fadca41fdff96c96463c0e5cb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:44:33 -0700 Subject: [PATCH 059/101] Export Atlas registry APIs --- packages/validation/src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 4a9e5b46..418aed52 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -44,6 +44,18 @@ export { type ResonanceAtlasRecordV1, type ResonanceAtlasSnapshotV1, } from "./atlas"; +export { + applyAtlasSpecimenMerge, + createAtlasSpecimenMerge, + emptyAtlasRegistry, + publishAtlasRecord, + searchAtlas, + snapshotAtlasRegistry, + type AtlasRegistryStateV1, + type AtlasSearchQueryV1, + type AtlasSearchResultV1, + type AtlasSpecimenMergeV1, +} from "./atlas-registry"; export { deriveEvidenceRecurrence, deriveMedianModalDriftCents, From de0b0f1f8f7b46f9ba6b700b6e0c3839f074d326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:44:54 -0700 Subject: [PATCH 060/101] Test Atlas registry publication and merge semantics --- .../validation/test/atlas-registry.test.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/validation/test/atlas-registry.test.ts diff --git a/packages/validation/test/atlas-registry.test.ts b/packages/validation/test/atlas-registry.test.ts new file mode 100644 index 00000000..26506e0f --- /dev/null +++ b/packages/validation/test/atlas-registry.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import { + applyAtlasSpecimenMerge, + contentDigest, + createAtlasRecord, + createAtlasSpecimenMerge, + emptyAtlasRegistry, + publishAtlasRecord, + searchAtlas, + snapshotAtlasRegistry, +} from "../src"; + +async function atlasRecord(specimenId: string, label: string, measurementSeed: string) { + return createAtlasRecord({ + createdAt: "2026-08-24T15:30:00.000Z", + contributor: { contributorId: "contributor-1", displayName: null }, + specimen: { + specimenId, + label, + objectFamily: "digital-reference", + material: "other", + publicDescription: "software qualification record", + }, + measurements: [{ measurementId: await contentDigest({ measurementSeed }), derivationIds: [] }], + }); +} + +describe("Resonance Atlas registry", () => { + it("publishes verified records append-only and creates deterministic snapshots", async () => { + const first = await atlasRecord("specimen-a", "Digital Bell A", "a"); + const second = await atlasRecord("specimen-b", "Digital Bell B", "b"); + let registry = emptyAtlasRegistry(); + registry = await publishAtlasRecord(registry, first); + registry = await publishAtlasRecord(registry, second); + expect(registry.records).toHaveLength(2); + const repeated = await publishAtlasRecord(registry, first); + expect(repeated.records).toHaveLength(2); + + const results = searchAtlas(registry, { text: "bell", material: "other" }); + expect(results).toHaveLength(2); + const snapshot = await snapshotAtlasRegistry(registry, "2026-08-24T16:00:00.000Z"); + expect(snapshot.recordIds).toHaveLength(2); + expect(snapshot.snapshotId).toMatch(/^sha256:/); + }); + + it("prevents one immutable measurement from being published under two specimens", async () => { + const measurementId = await contentDigest({ same: "measurement" }); + const first = await createAtlasRecord({ + createdAt: "2026-08-24T15:30:00.000Z", + contributor: null, + specimen: { specimenId: "specimen-a", label: "A", objectFamily: "bell", material: "other", publicDescription: null }, + measurements: [{ measurementId, derivationIds: [] }], + }); + const second = await createAtlasRecord({ + createdAt: "2026-08-24T15:31:00.000Z", + contributor: null, + specimen: { specimenId: "specimen-b", label: "B", objectFamily: "bell", material: "other", publicDescription: null }, + measurements: [{ measurementId, derivationIds: [] }], + }); + const registry = await publishAtlasRecord(emptyAtlasRegistry(), first); + await expect(publishAtlasRecord(registry, second)).rejects.toThrow(/another specimen/); + }); + + it("supports explicit evidence-backed specimen merges without rewriting historical records", async () => { + const first = await atlasRecord("alias-a", "Alias A", "a"); + const second = await atlasRecord("canonical-a", "Canonical A", "b"); + let registry = await publishAtlasRecord(emptyAtlasRegistry(), first); + registry = await publishAtlasRecord(registry, second); + const merge = await createAtlasSpecimenMerge({ + createdAt: "2026-08-24T16:05:00.000Z", + aliasSpecimenId: "alias-a", + canonicalSpecimenId: "canonical-a", + rationale: "software test of explicit merge policy", + supportingRecordIds: [first.atlasRecordId, second.atlasRecordId], + }); + registry = applyAtlasSpecimenMerge(registry, merge); + expect(registry.records).toHaveLength(2); + expect(registry.specimenMerges).toHaveLength(1); + const alias = searchAtlas(registry, { text: "Alias" })[0]; + expect(alias?.canonicalSpecimenId).toBe("canonical-a"); + }); +}); From 23c702ec25e2e84c976ba29ea930b5802b74dede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:45:56 -0700 Subject: [PATCH 061/101] Add research-only high-resolution damped-mode estimator --- packages/dsp/src/analysis/prony-research.ts | 156 ++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 packages/dsp/src/analysis/prony-research.ts diff --git a/packages/dsp/src/analysis/prony-research.ts b/packages/dsp/src/analysis/prony-research.ts new file mode 100644 index 00000000..89c42c1b --- /dev/null +++ b/packages/dsp/src/analysis/prony-research.ts @@ -0,0 +1,156 @@ +export interface ResearchDampedModeEstimateV1 { + readonly frequencyHz: number; + readonly decaySeconds: number; + readonly poleRadius: number; + readonly poleAngleRadians: number; +} + +export interface PronyResearchResultV1 { + readonly researchEstimatorVersion: "prony-damped-modes-research-1"; + readonly evidenceEligible: false; + readonly requestedModeCount: number; + readonly modes: readonly ResearchDampedModeEstimateV1[]; + readonly residualRms: number; +} + +interface Complex { + re: number; + im: number; +} + +function cadd(a: Complex, b: Complex): Complex { return { re: a.re + b.re, im: a.im + b.im }; } +function csub(a: Complex, b: Complex): Complex { return { re: a.re - b.re, im: a.im - b.im }; } +function cmul(a: Complex, b: Complex): Complex { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; } +function cdiv(a: Complex, b: Complex): Complex { + const d = b.re * b.re + b.im * b.im; + if (!(d > 0)) return { re: Number.POSITIVE_INFINITY, im: Number.POSITIVE_INFINITY }; + return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }; +} +function cabs(a: Complex): number { return Math.hypot(a.re, a.im); } + +function solveLinearSystem(matrix: number[][], rhs: number[]): number[] { + const n = rhs.length; + const a = matrix.map((row, index) => [...row, rhs[index]!]); + for (let column = 0; column < n; column += 1) { + let pivot = column; + for (let row = column + 1; row < n; row += 1) { + if (Math.abs(a[row]![column]!) > Math.abs(a[pivot]![column]!)) pivot = row; + } + if (Math.abs(a[pivot]![column]!) < 1e-14) throw new Error("Prony normal equations are singular"); + [a[column], a[pivot]] = [a[pivot]!, a[column]!]; + const scale = a[column]![column]!; + for (let j = column; j <= n; j += 1) a[column]![j] = a[column]![j]! / scale; + for (let row = 0; row < n; row += 1) { + if (row === column) continue; + const factor = a[row]![column]!; + if (factor === 0) continue; + for (let j = column; j <= n; j += 1) a[row]![j] = a[row]![j]! - factor * a[column]![j]!; + } + } + return a.map((row) => row[n]!); +} + +function linearPredictionCoefficients(samples: readonly number[], order: number): number[] { + const normal = Array.from({ length: order }, () => Array(order).fill(0)); + const rhs = Array(order).fill(0); + for (let n = order; n < samples.length; n += 1) { + const target = -samples[n]!; + for (let i = 0; i < order; i += 1) { + const xi = samples[n - i - 1]!; + rhs[i] += xi * target; + for (let j = 0; j < order; j += 1) normal[i]![j] += xi * samples[n - j - 1]!; + } + } + const trace = normal.reduce((sum, row, index) => sum + Math.abs(row[index] ?? 0), 0); + const ridge = Math.max(1e-14, trace * 1e-12 / Math.max(1, order)); + for (let index = 0; index < order; index += 1) normal[index]![index] = normal[index]![index]! + ridge; + return solveLinearSystem(normal, rhs); +} + +function polynomialValue(coefficients: readonly number[], z: Complex): Complex { + let value: Complex = { re: coefficients[0]!, im: 0 }; + for (let index = 1; index < coefficients.length; index += 1) { + value = cadd(cmul(value, z), { re: coefficients[index]!, im: 0 }); + } + return value; +} + +function durandKerner(coefficients: readonly number[]): Complex[] { + const degree = coefficients.length - 1; + if (degree < 1) return []; + let roots = Array.from({ length: degree }, (_, index) => { + const angle = 2 * Math.PI * (index + 0.37) / degree; + return { re: 0.9 * Math.cos(angle), im: 0.9 * Math.sin(angle) }; + }); + for (let iteration = 0; iteration < 300; iteration += 1) { + let maximumStep = 0; + const next = roots.map((root, index) => { + let denominator: Complex = { re: 1, im: 0 }; + for (let other = 0; other < roots.length; other += 1) { + if (other === index) continue; + denominator = cmul(denominator, csub(root, roots[other]!)); + } + if (cabs(denominator) < 1e-18) denominator = cadd(denominator, { re: 1e-12, im: 1e-12 }); + const step = cdiv(polynomialValue(coefficients, root), denominator); + maximumStep = Math.max(maximumStep, cabs(step)); + return csub(root, step); + }); + roots = next; + if (maximumStep < 1e-11) break; + } + return roots; +} + +function residualRms(samples: readonly number[], coefficients: readonly number[]): number { + const order = coefficients.length; + let squared = 0; + let count = 0; + for (let n = order; n < samples.length; n += 1) { + let prediction = 0; + for (let index = 0; index < order; index += 1) prediction -= coefficients[index]! * samples[n - index - 1]!; + squared += (samples[n]! - prediction) ** 2; + count += 1; + } + return count === 0 ? 0 : Math.sqrt(squared / count); +} + +/** + * Research-only Prony/linear-prediction estimator for synthetic close-mode experiments. + * It is intentionally not an AcousticFingerprintV1 producer and is never evidence-eligible. + */ +export function estimateDampedModesPronyResearch( + samples: readonly number[], + sampleRate: number, + modeCount: number, +): PronyResearchResultV1 { + if (!(sampleRate > 0) || !Number.isFinite(sampleRate)) throw new Error("sampleRate must be finite and positive"); + if (!Number.isInteger(modeCount) || modeCount <= 0 || modeCount > 12) throw new Error("modeCount must be an integer in [1,12]"); + const order = 2 * modeCount; + if (samples.length < Math.max(32, order * 8)) throw new Error("insufficient samples for Prony research estimator"); + if (samples.some((value) => !Number.isFinite(value))) throw new Error("Prony samples must be finite"); + + const coefficients = linearPredictionCoefficients(samples, order); + const roots = durandKerner([1, ...coefficients]); + const modes = roots + .map((root) => { + const radius = cabs(root); + const angle = Math.atan2(root.im, root.re); + if (!(angle > 0 && angle < Math.PI) || !(radius > 0 && radius < 1.0005)) return null; + const frequencyHz = angle * sampleRate / (2 * Math.PI); + const safeRadius = Math.min(radius, 0.999999999); + const decaySeconds = -1 / (sampleRate * Math.log(safeRadius)); + if (!(frequencyHz > 0 && frequencyHz < sampleRate / 2) || !(decaySeconds > 0) || !Number.isFinite(decaySeconds)) return null; + return { frequencyHz, decaySeconds, poleRadius: radius, poleAngleRadians: angle }; + }) + .filter((mode): mode is ResearchDampedModeEstimateV1 => mode !== null) + .sort((left, right) => left.frequencyHz - right.frequencyHz) + .slice(0, modeCount); + + return { + researchEstimatorVersion: "prony-damped-modes-research-1", + evidenceEligible: false, + requestedModeCount: modeCount, + modes, + residualRms: residualRms(samples, coefficients), + }; +} From ebeda9c482004631986d4f97911960349f5eb69c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:46:11 -0700 Subject: [PATCH 062/101] Export research-only Prony mode estimator --- packages/dsp/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/dsp/src/index.ts b/packages/dsp/src/index.ts index e2fd0aa6..6ae8b531 100644 --- a/packages/dsp/src/index.ts +++ b/packages/dsp/src/index.ts @@ -8,6 +8,11 @@ export { analyzeImpact, analyzeImpactWithConfig, } from "./analysis/analyze-impact"; +export { + estimateDampedModesPronyResearch, + type PronyResearchResultV1, + type ResearchDampedModeEstimateV1, +} from "./analysis/prony-research"; export { DEFAULT_DECAY_FIT_CONFIG, type DecayEstimate, From 81c27aeb34a4a521137081d49273bfd49e64b00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:46:30 -0700 Subject: [PATCH 063/101] Test research estimator on close digital modes --- packages/dsp/test/prony-research.test.ts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/dsp/test/prony-research.test.ts diff --git a/packages/dsp/test/prony-research.test.ts b/packages/dsp/test/prony-research.test.ts new file mode 100644 index 00000000..768e1015 --- /dev/null +++ b/packages/dsp/test/prony-research.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { estimateDampedModesPronyResearch } from "../src"; + +function synthesize(sampleRate: number, durationSeconds: number, modes: readonly { f: number; tau: number; amplitude: number; phase: number }[]): Float64Array { + const samples = new Float64Array(Math.round(sampleRate * durationSeconds)); + for (let index = 0; index < samples.length; index += 1) { + const t = index / sampleRate; + let value = 0; + for (const mode of modes) value += mode.amplitude * Math.exp(-t / mode.tau) * Math.sin(2 * Math.PI * mode.f * t + mode.phase); + samples[index] = value; + } + return samples; +} + +describe("research-only Prony damped-mode estimator", () => { + it("separates two close damped modes in a deterministic digital twin", () => { + const sampleRate = 8_000; + const truth = [ + { f: 440, tau: 1.0, amplitude: 0.8, phase: 0.2 }, + { f: 448, tau: 0.72, amplitude: 0.55, phase: 1.1 }, + ]; + const samples = synthesize(sampleRate, 1.5, truth); + const result = estimateDampedModesPronyResearch(samples, sampleRate, 2); + expect(result.evidenceEligible).toBe(false); + expect(result.modes).toHaveLength(2); + expect(result.modes[0]!.frequencyHz).toBeCloseTo(440, 0); + expect(result.modes[1]!.frequencyHz).toBeCloseTo(448, 0); + expect(result.modes[0]!.decaySeconds).toBeGreaterThan(0.5); + expect(result.modes[1]!.decaySeconds).toBeGreaterThan(0.4); + expect(result.residualRms).toBeLessThan(1e-3); + }); + + it("never stamps an AcousticFingerprint algorithm version", () => { + const samples = synthesize(8_000, 0.5, [{ f: 700, tau: 0.3, amplitude: 1, phase: 0 }]); + const result = estimateDampedModesPronyResearch(samples, 8_000, 1); + expect(result.researchEstimatorVersion).toBe("prony-damped-modes-research-1"); + expect(result).not.toHaveProperty("algorithmVersion"); + expect(result.evidenceEligible).toBe(false); + }); +}); From 3c019845c070431440697d1405d345ef25e84d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:46:53 -0700 Subject: [PATCH 064/101] Add integrity-enforcing research repository core --- packages/validation/src/repository.ts | 121 ++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/validation/src/repository.ts diff --git a/packages/validation/src/repository.ts b/packages/validation/src/repository.ts new file mode 100644 index 00000000..cb083290 --- /dev/null +++ b/packages/validation/src/repository.ts @@ -0,0 +1,121 @@ +import { + publishAtlasRecord, + type AtlasRegistryStateV1, + emptyAtlasRegistry, +} from "./atlas-registry"; +import type { ResonanceAtlasRecordV1 } from "./atlas"; +import { + verifyDerivationRecord, + verifyMeasurementRecord, + type DerivationRecordV1, + type MeasurementRecordV1, +} from "./provenance"; + +export interface ResearchRepositoryStateV1 { + readonly schemaVersion: 1; + readonly repositoryContractVersion: "everything-rings-research-repository-1"; + readonly measurements: readonly MeasurementRecordV1[]; + readonly derivations: readonly DerivationRecordV1[]; + readonly atlas: AtlasRegistryStateV1; +} + +export function emptyResearchRepository(): ResearchRepositoryStateV1 { + return { + schemaVersion: 1, + repositoryContractVersion: "everything-rings-research-repository-1", + measurements: [], + derivations: [], + atlas: emptyAtlasRegistry(), + }; +} + +export async function ingestMeasurement( + state: ResearchRepositoryStateV1, + measurement: MeasurementRecordV1, +): Promise { + if (!await verifyMeasurementRecord(measurement)) throw new Error("measurement failed content verification"); + const existing = state.measurements.find((candidate) => candidate.measurementId === measurement.measurementId); + if (existing !== undefined) return state; + return { + ...state, + measurements: [...state.measurements, measurement].sort((left, right) => left.measurementId.localeCompare(right.measurementId)), + }; +} + +export async function ingestDerivation( + state: ResearchRepositoryStateV1, + derivation: DerivationRecordV1, +): Promise { + if (!await verifyDerivationRecord(derivation)) throw new Error("derivation failed content verification"); + if (!state.measurements.some((measurement) => measurement.measurementId === derivation.measurementId)) { + throw new Error("derivation root measurement is absent from repository"); + } + if (state.derivations.some((candidate) => candidate.derivationId === derivation.derivationId)) return state; + return { + ...state, + derivations: [...state.derivations, derivation].sort((left, right) => left.derivationId.localeCompare(right.derivationId)), + }; +} + +export async function publishRepositoryAtlasRecord( + state: ResearchRepositoryStateV1, + record: ResonanceAtlasRecordV1, +): Promise { + for (const reference of record.measurements) { + if (!state.measurements.some((measurement) => measurement.measurementId === reference.measurementId)) { + throw new Error(`Atlas measurement ${reference.measurementId} is absent from repository`); + } + for (const derivationId of reference.derivationIds) { + const derivation = state.derivations.find((candidate) => candidate.derivationId === derivationId); + if (derivation === undefined) throw new Error(`Atlas derivation ${derivationId} is absent from repository`); + if (derivation.measurementId !== reference.measurementId) { + throw new Error(`Atlas derivation ${derivationId} is bound to a different measurement`); + } + } + } + return { ...state, atlas: await publishAtlasRecord(state.atlas, record) }; +} + +export interface ResearchRepositoryIntegrityV1 { + readonly valid: boolean; + readonly measurementCount: number; + readonly derivationCount: number; + readonly atlasRecordCount: number; + readonly reasons: readonly string[]; +} + +export async function verifyResearchRepositoryIntegrity( + state: ResearchRepositoryStateV1, +): Promise { + const reasons: string[] = []; + const measurementIds = new Set(); + for (const measurement of state.measurements) { + if (measurementIds.has(measurement.measurementId)) reasons.push(`duplicate measurement ${measurement.measurementId}`); + measurementIds.add(measurement.measurementId); + if (!await verifyMeasurementRecord(measurement)) reasons.push(`measurement ${measurement.measurementId} failed content verification`); + } + const derivationIds = new Set(); + for (const derivation of state.derivations) { + if (derivationIds.has(derivation.derivationId)) reasons.push(`duplicate derivation ${derivation.derivationId}`); + derivationIds.add(derivation.derivationId); + if (!await verifyDerivationRecord(derivation)) reasons.push(`derivation ${derivation.derivationId} failed content verification`); + if (!measurementIds.has(derivation.measurementId)) reasons.push(`derivation ${derivation.derivationId} has missing root measurement`); + } + for (const record of state.atlas.records) { + for (const reference of record.measurements) { + if (!measurementIds.has(reference.measurementId)) reasons.push(`Atlas record ${record.atlasRecordId} references missing measurement ${reference.measurementId}`); + for (const derivationId of reference.derivationIds) { + const derivation = state.derivations.find((candidate) => candidate.derivationId === derivationId); + if (derivation === undefined) reasons.push(`Atlas record ${record.atlasRecordId} references missing derivation ${derivationId}`); + else if (derivation.measurementId !== reference.measurementId) reasons.push(`Atlas derivation ${derivationId} points to wrong measurement`); + } + } + } + return { + valid: reasons.length === 0, + measurementCount: state.measurements.length, + derivationCount: state.derivations.length, + atlasRecordCount: state.atlas.records.length, + reasons: [...new Set(reasons)], + }; +} From 9d5e1b60c2b4db9035f96a243fe55b91d423d3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:47:15 -0700 Subject: [PATCH 065/101] Export research repository integrity APIs --- packages/validation/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 418aed52..affb8687 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -56,6 +56,15 @@ export { type AtlasSearchResultV1, type AtlasSpecimenMergeV1, } from "./atlas-registry"; +export { + emptyResearchRepository, + ingestDerivation, + ingestMeasurement, + publishRepositoryAtlasRecord, + verifyResearchRepositoryIntegrity, + type ResearchRepositoryIntegrityV1, + type ResearchRepositoryStateV1, +} from "./repository"; export { deriveEvidenceRecurrence, deriveMedianModalDriftCents, From 161d9554cf8b1ab38f2cbae2a1c0d4561e1c0f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:47:36 -0700 Subject: [PATCH 066/101] Test research repository provenance closure --- packages/validation/test/repository.test.ts | 105 ++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/validation/test/repository.test.ts diff --git a/packages/validation/test/repository.test.ts b/packages/validation/test/repository.test.ts new file mode 100644 index 00000000..c7d6164f --- /dev/null +++ b/packages/validation/test/repository.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "vitest"; +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + contentDigest, + createAtlasRecord, + createDerivationRecord, + createMeasurementRecord, + emptyResearchRepository, + ingestDerivation, + ingestMeasurement, + publishRepositoryAtlasRecord, + verifyResearchRepositoryIntegrity, +} from "../src"; + +const fingerprint: AcousticFingerprintV1 = { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: [440, 880, 1320].map((frequencyHz, index) => ({ + frequencyHz, + relativeAmplitude: 1 / (index + 1), + decaySeconds: 0.8 / (index + 1), + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), +}; + +async function measurement() { + return createMeasurementRecord({ + createdAt: "2026-08-24T15:30:00.000Z", + specimenId: "specimen-repo", + sessionId: "session-repo", + attemptId: 1, + material: "other", + acquisitionContractVersion: "digital-acquisition-1", + softwareRevision: "0123456789abcdef0123456789abcdef01234567", + fingerprintAlgorithmVersion: "er-dsp-2", + setup: { fixedSetup: true, microphoneDistanceCm: 20, striker: "digital", strikeLocation: "A", supportCondition: "fixed" }, + station: { + stationId: "digital-station", + deviceDescription: "software", + operatingSystem: "ci", + runtime: "vitest", + microphoneDescription: null, + captureSettings: { sampleRate: 48_000, channelCount: 1 }, + }, + fingerprint, + }); +} + +describe("research repository integrity", () => { + it("closes measurement -> derivation -> Atlas provenance before publication", async () => { + const m = await measurement(); + let repository = await ingestMeasurement(emptyResearchRepository(), m); + const d = await createDerivationRecord({ + measurementId: m.measurementId, + kind: "similarity", + algorithmVersion: "sonic-twin-baseline-1", + configDigest: await contentDigest({ thresholds: "default" }), + artifactDigest: await contentDigest({ score: 0.8 }), + createdAt: "2026-08-24T15:31:00.000Z", + }); + repository = await ingestDerivation(repository, d); + const record = await createAtlasRecord({ + createdAt: "2026-08-24T15:32:00.000Z", + contributor: null, + specimen: { specimenId: m.specimenId, label: "Digital reference", objectFamily: "reference", material: "other", publicDescription: null }, + measurements: [{ measurementId: m.measurementId, derivationIds: [d.derivationId] }], + }); + repository = await publishRepositoryAtlasRecord(repository, record); + const integrity = await verifyResearchRepositoryIntegrity(repository); + expect(integrity.valid).toBe(true); + expect(integrity.measurementCount).toBe(1); + expect(integrity.derivationCount).toBe(1); + expect(integrity.atlasRecordCount).toBe(1); + }); + + it("rejects derivations before their immutable measurement root exists", async () => { + const m = await measurement(); + const d = await createDerivationRecord({ + measurementId: m.measurementId, + kind: "renderer", + algorithmVersion: "renderer-test", + configDigest: await contentDigest({ config: 1 }), + artifactDigest: await contentDigest({ artifact: 1 }), + createdAt: "2026-08-24T15:31:00.000Z", + }); + await expect(ingestDerivation(emptyResearchRepository(), d)).rejects.toThrow(/root measurement/); + }); + + it("rejects Atlas publication when a referenced derivation is absent", async () => { + const m = await measurement(); + const repository = await ingestMeasurement(emptyResearchRepository(), m); + const missingDerivation = await contentDigest({ missing: true }); + const record = await createAtlasRecord({ + createdAt: "2026-08-24T15:32:00.000Z", + contributor: null, + specimen: { specimenId: m.specimenId, label: "Digital reference", objectFamily: "reference", material: "other", publicDescription: null }, + measurements: [{ measurementId: m.measurementId, derivationIds: [missingDerivation] }], + }); + await expect(publishRepositoryAtlasRecord(repository, record)).rejects.toThrow(/absent/); + }); +}); From 164383f82d66f2d2e6d47ccef5b26ab51854e7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:47:58 -0700 Subject: [PATCH 067/101] Add measurement-station calibration contracts --- .../validation/src/station-calibration.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 packages/validation/src/station-calibration.ts diff --git a/packages/validation/src/station-calibration.ts b/packages/validation/src/station-calibration.ts new file mode 100644 index 00000000..5afd202b --- /dev/null +++ b/packages/validation/src/station-calibration.ts @@ -0,0 +1,90 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + compareFingerprintToObjectModel, + type AcousticObjectModelV1, + type FingerprintToObjectModelComparisonV1, +} from "@everything-rings/fingerprint"; +import { contentDigest } from "./provenance"; + +export interface StationCalibrationProtocolV1 { + readonly schemaVersion: 1; + readonly stationCalibrationContractVersion: "station-calibration-1"; + readonly protocolId: string; + readonly createdAt: string; + readonly referencePopulation: string; + readonly referenceSpecimenId: string; + readonly referenceObjectModelDigest: string; + readonly referenceObjectModel: AcousticObjectModelV1; + readonly minimumCoverage: number; + readonly maximumMedianFrequencyDistanceCents: number; + readonly minimumMatchedModes: number; +} + +export async function createStationCalibrationProtocol( + input: Omit, +): Promise { + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("station calibration createdAt is invalid"); + if (input.referencePopulation.trim().length === 0) throw new Error("station calibration referencePopulation is required"); + if (input.referenceSpecimenId.trim().toLocaleLowerCase("en-US") !== input.referenceObjectModel.specimenId.trim().toLocaleLowerCase("en-US")) { + throw new Error("station calibration reference specimen does not match object model"); + } + if (!(input.minimumCoverage > 0 && input.minimumCoverage <= 1)) throw new Error("minimumCoverage must be in (0,1]"); + if (!(input.maximumMedianFrequencyDistanceCents > 0) || !Number.isFinite(input.maximumMedianFrequencyDistanceCents)) { + throw new Error("maximumMedianFrequencyDistanceCents must be finite and positive"); + } + if (!Number.isInteger(input.minimumMatchedModes) || input.minimumMatchedModes <= 0) throw new Error("minimumMatchedModes must be a positive integer"); + const referenceObjectModelDigest = await contentDigest(input.referenceObjectModel); + const payload = { + ...input, + schemaVersion: 1 as const, + stationCalibrationContractVersion: "station-calibration-1" as const, + referenceObjectModelDigest, + }; + return { ...payload, protocolId: await contentDigest(payload) }; +} + +export interface StationCalibrationObservationV1 { + readonly stationId: string; + readonly createdAt: string; + readonly specimenId: string; + readonly fingerprint: AcousticFingerprintV1; +} + +export interface StationCalibrationVerdictV1 { + readonly schemaVersion: 1; + readonly verdictContractVersion: "station-calibration-verdict-1"; + readonly protocolId: string; + readonly stationId: string; + readonly createdAt: string; + readonly passed: boolean; + readonly comparison: FingerprintToObjectModelComparisonV1; + readonly reasons: readonly string[]; +} + +export function evaluateStationCalibration( + protocol: StationCalibrationProtocolV1, + observation: StationCalibrationObservationV1, +): StationCalibrationVerdictV1 { + if (!Number.isFinite(Date.parse(observation.createdAt))) throw new Error("station calibration observation createdAt is invalid"); + if (observation.stationId.trim().length === 0) throw new Error("station calibration stationId is required"); + const reasons: string[] = []; + if (observation.specimenId.trim().toLocaleLowerCase("en-US") !== protocol.referenceSpecimenId.trim().toLocaleLowerCase("en-US")) { + reasons.push("observation specimen does not match the calibration reference specimen"); + } + const comparison = compareFingerprintToObjectModel(observation.fingerprint, protocol.referenceObjectModel); + if (comparison.coverage < protocol.minimumCoverage) reasons.push(`mode coverage ${comparison.coverage.toFixed(3)} is below ${protocol.minimumCoverage}`); + if (comparison.matchedModelModes < protocol.minimumMatchedModes) reasons.push(`matched modes ${comparison.matchedModelModes} is below ${protocol.minimumMatchedModes}`); + if (comparison.medianFrequencyDistanceCents === null || comparison.medianFrequencyDistanceCents > protocol.maximumMedianFrequencyDistanceCents) { + reasons.push(`median modal drift exceeds ${protocol.maximumMedianFrequencyDistanceCents} cents`); + } + return { + schemaVersion: 1, + verdictContractVersion: "station-calibration-verdict-1", + protocolId: protocol.protocolId, + stationId: observation.stationId.trim(), + createdAt: observation.createdAt, + passed: reasons.length === 0, + comparison, + reasons, + }; +} From 8343cfa5be1413bd6f89dd8fbafd0b3836a32e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:48:17 -0700 Subject: [PATCH 068/101] Export station calibration APIs --- packages/validation/src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index affb8687..2cfd0783 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -65,6 +65,13 @@ export { type ResearchRepositoryIntegrityV1, type ResearchRepositoryStateV1, } from "./repository"; +export { + createStationCalibrationProtocol, + evaluateStationCalibration, + type StationCalibrationObservationV1, + type StationCalibrationProtocolV1, + type StationCalibrationVerdictV1, +} from "./station-calibration"; export { deriveEvidenceRecurrence, deriveMedianModalDriftCents, From 9d81934433b0a510a80f44cfd5cb681393325e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:48:35 -0700 Subject: [PATCH 069/101] Test digital station calibration contracts --- .../test/station-calibration.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/validation/test/station-calibration.test.ts diff --git a/packages/validation/test/station-calibration.test.ts b/packages/validation/test/station-calibration.test.ts new file mode 100644 index 00000000..0288b3e8 --- /dev/null +++ b/packages/validation/test/station-calibration.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { buildAcousticObjectModel } from "@everything-rings/fingerprint"; +import { createStationCalibrationProtocol, evaluateStationCalibration } from "../src"; + +function fp(frequencies: readonly number[]): AcousticFingerprintV1 { + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: frequencies.map((frequencyHz, index) => ({ + frequencyHz, + relativeAmplitude: 1 / (index + 1), + decaySeconds: 0.9 / (index + 1), + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), + }; +} + +describe("measurement-station calibration", () => { + it("passes a digital station observation close to its frozen reference object model", async () => { + const model = buildAcousticObjectModel([ + { observationId: "r1", specimenId: "reference-1", fingerprint: fp([440, 880, 1320]) }, + { observationId: "r2", specimenId: "reference-1", fingerprint: fp([441, 882, 1323]) }, + ]); + const protocol = await createStationCalibrationProtocol({ + createdAt: "2026-08-24T15:30:00.000Z", + referencePopulation: "digital-station-reference-v1", + referenceSpecimenId: "reference-1", + referenceObjectModel: model, + minimumCoverage: 1, + maximumMedianFrequencyDistanceCents: 35, + minimumMatchedModes: 3, + }); + const verdict = evaluateStationCalibration(protocol, { + stationId: "station-digital-a", + createdAt: "2026-08-24T15:31:00.000Z", + specimenId: "reference-1", + fingerprint: fp([440.5, 881, 1321.5]), + }); + expect(verdict.passed).toBe(true); + expect(verdict.comparison.coverage).toBe(1); + }); + + it("fails a digital station observation that drifts outside its frozen contract", async () => { + const model = buildAcousticObjectModel([ + { observationId: "r1", specimenId: "reference-1", fingerprint: fp([440, 880, 1320]) }, + { observationId: "r2", specimenId: "reference-1", fingerprint: fp([441, 882, 1323]) }, + ]); + const protocol = await createStationCalibrationProtocol({ + createdAt: "2026-08-24T15:30:00.000Z", + referencePopulation: "digital-station-reference-v1", + referenceSpecimenId: "reference-1", + referenceObjectModel: model, + minimumCoverage: 1, + maximumMedianFrequencyDistanceCents: 35, + minimumMatchedModes: 3, + }); + const verdict = evaluateStationCalibration(protocol, { + stationId: "station-digital-b", + createdAt: "2026-08-24T15:31:00.000Z", + specimenId: "reference-1", + fingerprint: fp([500, 1000, 1500]), + }); + expect(verdict.passed).toBe(false); + expect(verdict.reasons.length).toBeGreaterThan(0); + }); +}); From 14cb218cbdcd1bf9b4fb4abac8cd1ab6af028c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:49:42 -0700 Subject: [PATCH 070/101] Add local Sonic Twin research surface --- apps/web/src/TwinLabApp.tsx | 141 ++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 apps/web/src/TwinLabApp.tsx diff --git a/apps/web/src/TwinLabApp.tsx b/apps/web/src/TwinLabApp.tsx new file mode 100644 index 00000000..d3924904 --- /dev/null +++ b/apps/web/src/TwinLabApp.tsx @@ -0,0 +1,141 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + benchmarkSonicTwinRetrieval, + buildAcousticObjectModel, + buildSonicTwinIndex, + buildTwinVerificationPairs, + calibratedPredictions, + evaluateCalibration, + fitIsotonicCalibration, + retrieveSonicTwin, + riskCoverageCurve, + validateTwinBenchmarkCorpus, + type AcousticObjectObservationV1, + type LabeledSimilarityScoreV1, + type NuisanceMetadataV1, + type TwinBenchmarkCorpusV1, +} from "@everything-rings/fingerprint"; +import { useMemo } from "react"; + +function fp(frequencies: readonly number[]): AcousticFingerprintV1 { + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: frequencies.map((frequencyHz, index) => ({ + frequencyHz, + relativeAmplitude: 1 / (index + 1), + decaySeconds: 0.9 / (index + 1), + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), + }; +} + +const nuisance: NuisanceMetadataV1 = { + strikeLocation: "digital-A", + striker: "digital-impulse", + support: "digital-fixed", + microphoneDistanceClass: "digital-20cm", + room: "digital-room", + stationId: "digital-station", + operatorId: "software", + dayId: "digital-day-1", +}; + +const OBSERVATIONS: readonly AcousticObjectObservationV1[] = [ + { observationId: "bell-a-1", specimenId: "digital-bell-a", fingerprint: fp([440, 880, 1320, 1760]) }, + { observationId: "bell-a-2", specimenId: "digital-bell-a", fingerprint: fp([441, 882, 1322, 1763]) }, + { observationId: "bell-b-1", specimenId: "digital-bell-b", fingerprint: fp([470, 940, 1410, 1880]) }, + { observationId: "bell-b-2", specimenId: "digital-bell-b", fingerprint: fp([471, 942, 1412, 1883]) }, + { observationId: "plate-c-1", specimenId: "digital-plate-c", fingerprint: fp([620, 1010, 1670, 2510]) }, + { observationId: "plate-c-2", specimenId: "digital-plate-c", fingerprint: fp([621, 1012, 1672, 2512]) }, +]; + +function corpus(): TwinBenchmarkCorpusV1 { + return { + schemaVersion: 1, + corpusContractVersion: "sonic-twin-benchmark-corpus-1", + corpusId: "built-in-digital-twin-demo-1", + createdAt: "2026-08-24T15:30:00.000Z", + observations: OBSERVATIONS.map((observation, index) => ({ + ...observation, + objectFamily: observation.specimenId.includes("plate") ? "plate" : "bell", + source: "digital-twin", + nuisance: { ...nuisance, strikeLocation: index % 2 === 0 ? "digital-A" : "digital-B" }, + })), + }; +} + +export function TwinLabApp() { + const report = useMemo(() => { + const benchmarkCorpus = corpus(); + const validation = validateTwinBenchmarkCorpus(benchmarkCorpus); + const index = buildSonicTwinIndex(OBSERVATIONS); + const query = fp([440.6, 881, 1321, 1761]); + const ranking = retrieveSonicTwin(query, index); + const retrievalMetrics = benchmarkSonicTwinRetrieval([ + { queryId: "query-a", trueSpecimenId: "digital-bell-a", fingerprint: query }, + { queryId: "query-b", trueSpecimenId: "digital-bell-b", fingerprint: fp([470.5, 941, 1411, 1881]) }, + { queryId: "query-c", trueSpecimenId: "digital-plate-c", fingerprint: fp([620.5, 1011, 1671, 2511]) }, + ], index); + const pairs = buildTwinVerificationPairs(benchmarkCorpus, 30); + const scored: LabeledSimilarityScoreV1[] = pairs.map((pair) => { + const model = buildAcousticObjectModel(OBSERVATIONS.filter((observation) => observation.specimenId === pair.leftSpecimenId)); + const candidateRanking = retrieveSonicTwin(pair.candidate, { schemaVersion: 1, indexVersion: "sonic-twin-index-1", entries: [{ specimenId: model.specimenId, model }] }); + return { pairId: pair.pairId, score: candidateRanking[0]?.evidenceScore ?? 0, samePhysicalSpecimen: pair.samePhysicalSpecimen }; + }); + const model = fitIsotonicCalibration(scored.length > 0 ? scored : [ + { pairId: "fallback-n", score: 0, samePhysicalSpecimen: false }, + { pairId: "fallback-p", score: 1, samePhysicalSpecimen: true }, + ], "built-in-digital-twin-demo-1"); + const predictions = calibratedPredictions(model, scored); + const calibration = evaluateCalibration(predictions); + const riskCoverage = riskCoverageCurve(predictions); + return { validation, index, ranking, retrievalMetrics, pairs, calibration, riskCoverage }; + }, []); + + return
+
+

EVERYTHING RINGS / SONIC TWIN LAB

+

Object models, retrieval, calibration, abstention

+

A local software-only qualification surface. The built-in corpus is synthetic and is not physical identity evidence.

+
+ +
+
+

CORPUS

+

{report.validation.valid ? "VALID" : "INVALID"}

+

{report.validation.observationCount} observations · {report.validation.specimenCount} specimens

+
+
+

RETRIEVAL

+

Recall@1 {report.retrievalMetrics.recallAt1?.toFixed(3) ?? "—"}

+

MRR {report.retrievalMetrics.meanReciprocalRank?.toFixed(3) ?? "—"}

+
+
+

CALIBRATION / DIGITAL POPULATION

+

Brier {report.calibration.brierScore?.toFixed(3) ?? "—"}

+

AUC {report.calibration.rocAuc?.toFixed(3) ?? "—"}

+
+
+ +
+

QUERY

Ranked specimen models

+
+
rankspecimenscoredistancemodescoveragestatusclaim
+ {report.ranking.map((result) =>
+ {result.rank}{result.specimenId}{result.evidenceScore.toFixed(3)}{result.normalizedDistance.toFixed(3)}{result.matchedModelModes}/{result.modelModeCount}digital{result.rank === 1 ? "TOP" : ""}ranking only +
)} +
+
+ +
+

Verification pairs

{report.pairs.length} deterministic same-specimen / hard-negative pairs.

+

Risk / coverage

{report.riskCoverage.map((point) => `${point.minimumConfidence.toFixed(2)}→${point.coverage.toFixed(2)}`).join(" · ")}

+

Boundary

No built-in result is a calibrated claim about physical objects. Calibration is bound to the named digital population.

+
+
; +} From 51a734c27887413b2c24e2bcf5e0bb0e0edc9f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:50:07 -0700 Subject: [PATCH 071/101] Add local Resonance Atlas research surface --- apps/web/src/AtlasApp.tsx | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 apps/web/src/AtlasApp.tsx diff --git a/apps/web/src/AtlasApp.tsx b/apps/web/src/AtlasApp.tsx new file mode 100644 index 00000000..3b6d34d6 --- /dev/null +++ b/apps/web/src/AtlasApp.tsx @@ -0,0 +1,114 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + contentDigest, + createAtlasRecord, + createDerivationRecord, + createMeasurementRecord, + emptyResearchRepository, + ingestDerivation, + ingestMeasurement, + publishRepositoryAtlasRecord, + searchAtlas, + snapshotAtlasRegistry, + verifyResearchRepositoryIntegrity, + type ResearchRepositoryStateV1, +} from "@everything-rings/validation"; +import { useState } from "react"; + +const REVISION = "0123456789abcdef0123456789abcdef01234567"; + +function fingerprint(base: number): AcousticFingerprintV1 { + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: [1, 2, 3, 4].map((multiplier, index) => ({ + frequencyHz: base * multiplier, + relativeAmplitude: 1 / (index + 1), + decaySeconds: 0.9 / (index + 1), + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), + }; +} + +async function buildDemoRepository(): Promise { + let repository = emptyResearchRepository(); + for (const [index, specimen] of [ + { id: "digital-atlas-bell", label: "Digital Atlas Bell", family: "bell", base: 440 }, + { id: "digital-atlas-plate", label: "Digital Atlas Plate", family: "plate", base: 620 }, + ].entries()) { + const fp = fingerprint(specimen.base); + const measurement = await createMeasurementRecord({ + createdAt: `2026-08-24T15:3${index}:00.000Z`, + specimenId: specimen.id, + sessionId: `atlas-session-${index + 1}`, + attemptId: 1, + material: "other", + acquisitionContractVersion: "digital-atlas-demo-1", + softwareRevision: REVISION, + fingerprintAlgorithmVersion: fp.algorithmVersion, + setup: { fixedSetup: true, microphoneDistanceCm: 20, striker: "digital impulse", strikeLocation: "digital-A", supportCondition: "digital fixed" }, + station: { stationId: "digital-atlas-station", deviceDescription: "software", operatingSystem: "browser", runtime: "local", microphoneDescription: null, captureSettings: { sampleRate: 48_000, channelCount: 1 } }, + fingerprint: fp, + }); + repository = await ingestMeasurement(repository, measurement); + const derivation = await createDerivationRecord({ + measurementId: measurement.measurementId, + kind: "visualization", + algorithmVersion: "atlas-demo-visualization-1", + configDigest: await contentDigest({ visualization: "modal-card" }), + artifactDigest: await contentDigest({ specimen: specimen.id, modes: fp.modes.map((mode) => mode.frequencyHz) }), + createdAt: `2026-08-24T15:4${index}:00.000Z`, + }); + repository = await ingestDerivation(repository, derivation); + const record = await createAtlasRecord({ + createdAt: `2026-08-24T15:5${index}:00.000Z`, + contributor: { contributorId: "digital-software", displayName: "Built-in software qualification" }, + specimen: { specimenId: specimen.id, label: specimen.label, objectFamily: specimen.family, material: "other", publicDescription: "Synthetic Atlas record for software qualification" }, + measurements: [{ measurementId: measurement.measurementId, derivationIds: [derivation.derivationId] }], + }); + repository = await publishRepositoryAtlasRecord(repository, record); + } + return repository; +} + +export function AtlasApp() { + const [repository, setRepository] = useState(); + const [integrity, setIntegrity] = useState("not run"); + const [snapshotId, setSnapshotId] = useState("—"); + + async function run(): Promise { + const next = await buildDemoRepository(); + const report = await verifyResearchRepositoryIntegrity(next); + const snapshot = await snapshotAtlasRegistry(next.atlas, "2026-08-24T16:00:00.000Z"); + setRepository(next); + setIntegrity(report.valid ? `PASS · ${report.measurementCount} measurements · ${report.derivationCount} derivations · ${report.atlasRecordCount} records` : `FAIL · ${report.reasons.join("; ")}`); + setSnapshotId(snapshot.snapshotId); + } + + const records = repository === undefined ? [] : searchAtlas(repository.atlas, {}); + return
+
+

EVERYTHING RINGS / RESONANCE ATLAS LAB

+

Content-addressed acoustic provenance

+

Builds a complete local measurement → derivation → public Atlas graph. The demo is synthetic and contains no microphone PCM.

+
+
Integrity: {integrity}
+
+

MEASUREMENTS

{repository?.measurements.length ?? 0}

+

DERIVATIONS

{repository?.derivations.length ?? 0}

+

ATLAS RECORDS

{repository?.atlas.records.length ?? 0}

+
+
+

PUBLIC INDEX

Specimens

+
+
specimenlabelfamilymaterialmeasurementscanonicalPCMstatus
+ {records.map((record) =>
{record.specimenId}{record.label}{record.objectFamily}{record.material}{record.measurementCount}{record.canonicalSpecimenId}noneverified
)} +
+

Snapshot: {snapshotId}

+
+
; +} From cf606842e41f58de3a03fe3e489e186e476de1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:50:19 -0700 Subject: [PATCH 072/101] Expose Sonic Twin and Atlas research routes --- apps/web/src/App.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 3f7ef17c..25244074 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,9 +1,11 @@ +import { AtlasApp } from "./AtlasApp"; import { CampaignAuthorApp } from "./CampaignAuthorApp"; import { CampaignCollectionApp } from "./CampaignCollectionApp"; import { ConsumerApp } from "./ConsumerApp"; import { LabApp } from "./LabApp"; import { PostCollectionReviewApp } from "./PostCollectionReviewApp"; import { ReleaseApp } from "./ReleaseApp"; +import { TwinLabApp } from "./TwinLabApp"; import "./postCollectionReview.css"; export function App() { @@ -13,6 +15,8 @@ export function App() { if (params.get("gate-c") === "1") return ; if (params.get("campaign-author") === "1") return ; if (params.get("campaign") === "1") return ; + if (params.get("twin") === "1") return ; + if (params.get("atlas") === "1") return ; if (params.get("lab") === "1") return ; return ; } From 1281eef84ce970f3080ea17606c3d12f6071b0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:50:39 -0700 Subject: [PATCH 073/101] Smoke test Sonic Twin and Atlas research routes --- scripts/browser-smoke.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/browser-smoke.sh b/scripts/browser-smoke.sh index 23b2ac26..e73b935a 100755 --- a/scripts/browser-smoke.sh +++ b/scripts/browser-smoke.sh @@ -64,6 +64,8 @@ check_route "/?campaign=1" "Precommitted physical collection" "campaign" check_route "/?release=1" "Empirical release gates" "release" check_route "/?gate-b=1" "Post-collection blinded reconstruction" "gate-b" check_route "/?gate-c=1" "Post-Gate-B playable identity" "gate-c" +check_route "/?twin=1" "Object models, retrieval, calibration, abstention" "twin" +check_route "/?atlas=1" "Content-addressed acoustic provenance" "atlas" check_route "/?campaign-author=1&release=1" "Empirical release gates" "release-author-precedence" check_route "/?gate-b=1&release=1" "Empirical release gates" "release-review-precedence" check_route "/?campaign-author=1&campaign=1" "Freeze the experiment before the first strike." "author-precedence" From b9f25ae14db8cbd5cba55141d4b8d5eb123f8f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:51:08 -0700 Subject: [PATCH 074/101] Add Atlas collections and public graph model --- packages/validation/src/atlas-network.ts | 107 +++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 packages/validation/src/atlas-network.ts diff --git a/packages/validation/src/atlas-network.ts b/packages/validation/src/atlas-network.ts new file mode 100644 index 00000000..9d9509fb --- /dev/null +++ b/packages/validation/src/atlas-network.ts @@ -0,0 +1,107 @@ +import type { AtlasRegistryStateV1 } from "./atlas-registry"; +import { contentDigest } from "./provenance"; + +export interface AtlasCollectionV1 { + readonly schemaVersion: 1; + readonly collectionContractVersion: "atlas-collection-1"; + readonly collectionId: string; + readonly createdAt: string; + readonly contributorId: string; + readonly title: string; + readonly description: string | null; + readonly atlasRecordIds: readonly string[]; + readonly visibility: "public"; +} + +function assertDigest(value: string, field: string): void { + if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new Error(`${field} must be a sha256 digest`); +} + +export async function createAtlasCollection( + input: Omit, +): Promise { + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("Atlas collection createdAt is invalid"); + if (input.contributorId.trim().length === 0) throw new Error("Atlas collection contributorId is required"); + if (input.title.trim().length === 0) throw new Error("Atlas collection title is required"); + if (input.atlasRecordIds.length === 0) throw new Error("Atlas collection requires at least one record"); + const atlasRecordIds = [...new Set(input.atlasRecordIds)].sort(); + atlasRecordIds.forEach((id) => assertDigest(id, "atlasRecordId")); + const payload = { + ...input, + atlasRecordIds, + schemaVersion: 1 as const, + collectionContractVersion: "atlas-collection-1" as const, + visibility: "public" as const, + }; + return { ...payload, collectionId: await contentDigest(payload) }; +} + +export type AtlasGraphNodeKind = "record" | "specimen" | "contributor" | "collection"; +export type AtlasGraphEdgeKind = "describes" | "published-by" | "member-of" | "merged-into"; + +export interface AtlasGraphNodeV1 { + readonly id: string; + readonly kind: AtlasGraphNodeKind; + readonly label: string; +} + +export interface AtlasGraphEdgeV1 { + readonly from: string; + readonly to: string; + readonly kind: AtlasGraphEdgeKind; +} + +export interface AtlasGraphSnapshotV1 { + readonly schemaVersion: 1; + readonly graphContractVersion: "atlas-graph-1"; + readonly nodes: readonly AtlasGraphNodeV1[]; + readonly edges: readonly AtlasGraphEdgeV1[]; +} + +export function buildAtlasGraph( + registry: AtlasRegistryStateV1, + collections: readonly AtlasCollectionV1[] = [], +): AtlasGraphSnapshotV1 { + const nodes = new Map(); + const edges: AtlasGraphEdgeV1[] = []; + const addNode = (node: AtlasGraphNodeV1) => { if (!nodes.has(node.id)) nodes.set(node.id, node); }; + + for (const record of registry.records) { + const recordNode = `record:${record.atlasRecordId}`; + const specimenNode = `specimen:${record.specimen.specimenId.trim().toLocaleLowerCase("en-US")}`; + addNode({ id: recordNode, kind: "record", label: record.specimen.label }); + addNode({ id: specimenNode, kind: "specimen", label: record.specimen.label }); + edges.push({ from: recordNode, to: specimenNode, kind: "describes" }); + if (record.contributor !== null) { + const contributorNode = `contributor:${record.contributor.contributorId.trim().toLocaleLowerCase("en-US")}`; + addNode({ id: contributorNode, kind: "contributor", label: record.contributor.displayName ?? record.contributor.contributorId }); + edges.push({ from: recordNode, to: contributorNode, kind: "published-by" }); + } + } + + for (const collection of collections) { + const collectionNode = `collection:${collection.collectionId}`; + addNode({ id: collectionNode, kind: "collection", label: collection.title }); + const contributorNode = `contributor:${collection.contributorId.trim().toLocaleLowerCase("en-US")}`; + addNode({ id: contributorNode, kind: "contributor", label: collection.contributorId }); + for (const recordId of collection.atlasRecordIds) { + const recordNode = `record:${recordId}`; + if (!nodes.has(recordNode)) throw new Error(`Atlas collection references unknown record ${recordId}`); + edges.push({ from: recordNode, to: collectionNode, kind: "member-of" }); + } + } + + for (const merge of registry.specimenMerges) { + const alias = `specimen:${merge.aliasSpecimenId.trim().toLocaleLowerCase("en-US")}`; + const canonical = `specimen:${merge.canonicalSpecimenId.trim().toLocaleLowerCase("en-US")}`; + if (!nodes.has(alias) || !nodes.has(canonical)) throw new Error("Atlas merge graph refers to unknown specimen node"); + edges.push({ from: alias, to: canonical, kind: "merged-into" }); + } + + return { + schemaVersion: 1, + graphContractVersion: "atlas-graph-1", + nodes: [...nodes.values()].sort((left, right) => left.id.localeCompare(right.id)), + edges: edges.sort((left, right) => `${left.from}|${left.kind}|${left.to}`.localeCompare(`${right.from}|${right.kind}|${right.to}`)), + }; +} From 7f8190b2e062225ceafe0cebe7caaec94b425026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:51:30 -0700 Subject: [PATCH 075/101] Export Atlas collection and graph APIs --- packages/validation/src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 2cfd0783..2ea89d5a 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -56,6 +56,16 @@ export { type AtlasSearchResultV1, type AtlasSpecimenMergeV1, } from "./atlas-registry"; +export { + buildAtlasGraph, + createAtlasCollection, + type AtlasCollectionV1, + type AtlasGraphEdgeKind, + type AtlasGraphEdgeV1, + type AtlasGraphNodeKind, + type AtlasGraphNodeV1, + type AtlasGraphSnapshotV1, +} from "./atlas-network"; export { emptyResearchRepository, ingestDerivation, From 603b445793888a07f37939da1423038c69d610c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:51:43 -0700 Subject: [PATCH 076/101] Test Atlas collections and graph projection --- .../validation/test/atlas-network.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/validation/test/atlas-network.test.ts diff --git a/packages/validation/test/atlas-network.test.ts b/packages/validation/test/atlas-network.test.ts new file mode 100644 index 00000000..a30d63b0 --- /dev/null +++ b/packages/validation/test/atlas-network.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { + buildAtlasGraph, + contentDigest, + createAtlasCollection, + createAtlasRecord, + emptyAtlasRegistry, + publishAtlasRecord, +} from "../src"; + +describe("Atlas collection and graph model", () => { + it("projects public records and collections into an explicit non-identity graph", async () => { + const first = await createAtlasRecord({ + createdAt: "2026-08-24T16:00:00.000Z", + contributor: { contributorId: "alice", displayName: "Alice" }, + specimen: { specimenId: "spec-a", label: "Digital Bell", objectFamily: "bell", material: "other", publicDescription: null }, + measurements: [{ measurementId: await contentDigest({ m: "a" }), derivationIds: [] }], + }); + const second = await createAtlasRecord({ + createdAt: "2026-08-24T16:01:00.000Z", + contributor: { contributorId: "bob", displayName: "Bob" }, + specimen: { specimenId: "spec-b", label: "Digital Plate", objectFamily: "plate", material: "other", publicDescription: null }, + measurements: [{ measurementId: await contentDigest({ m: "b" }), derivationIds: [] }], + }); + let registry = await publishAtlasRecord(emptyAtlasRegistry(), first); + registry = await publishAtlasRecord(registry, second); + const collection = await createAtlasCollection({ + createdAt: "2026-08-24T16:02:00.000Z", + contributorId: "alice", + title: "Digital resonators", + description: "Software-only examples", + atlasRecordIds: [first.atlasRecordId, second.atlasRecordId], + }); + const graph = buildAtlasGraph(registry, [collection]); + expect(graph.nodes.some((node) => node.kind === "collection")).toBe(true); + expect(graph.edges.filter((edge) => edge.kind === "member-of")).toHaveLength(2); + expect(graph.edges.every((edge) => edge.kind !== ("same-object" as never))).toBe(true); + }); +}); From 64670a3699ec810310148d20abd70e7397b23417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:55:03 -0700 Subject: [PATCH 077/101] Add full-vision software qualification workflow --- .../full-vision-software-qualification.yml | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .github/workflows/full-vision-software-qualification.yml diff --git a/.github/workflows/full-vision-software-qualification.yml b/.github/workflows/full-vision-software-qualification.yml new file mode 100644 index 00000000..40e0ef70 --- /dev/null +++ b/.github/workflows/full-vision-software-qualification.yml @@ -0,0 +1,83 @@ +name: full-vision-software-qualification + +on: + pull_request: + branches: + - post-freeze-development + workflow_dispatch: + +permissions: + contents: read + +env: + VITE_SOFTWARE_REVISION: ${{ github.event.pull_request.head.sha || github.sha }} + +jobs: + qualify: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: 22 + - name: Enable Corepack + run: corepack enable + - name: Install frozen dependencies + run: pnpm install --frozen-lockfile + - name: Typecheck all workspaces + run: pnpm typecheck + - name: Run all software and digital-twin tests + run: pnpm test + - name: Build all workspaces + run: pnpm build + - name: Browser and lifecycle qualification + run: pnpm browser:smoke + - name: Emit machine-readable qualification report + run: | + mkdir -p qualification + cat > qualification/full-vision-software.json < Date: Mon, 24 Aug 2026 08:55:33 -0700 Subject: [PATCH 078/101] Run browser qualification through canonical smoke script --- .github/workflows/full-vision-software-qualification.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/full-vision-software-qualification.yml b/.github/workflows/full-vision-software-qualification.yml index 40e0ef70..0618a3ca 100644 --- a/.github/workflows/full-vision-software-qualification.yml +++ b/.github/workflows/full-vision-software-qualification.yml @@ -31,7 +31,7 @@ jobs: - name: Build all workspaces run: pnpm build - name: Browser and lifecycle qualification - run: pnpm browser:smoke + run: bash scripts/browser-smoke.sh - name: Emit machine-readable qualification report run: | mkdir -p qualification From 6ff661a5723afedf2783aeedf1d0d36869f3f2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:56:14 -0700 Subject: [PATCH 079/101] Add multi-specimen full-vision software integration test --- apps/web/src/FullVisionSystem.test.ts | 212 ++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 apps/web/src/FullVisionSystem.test.ts diff --git a/apps/web/src/FullVisionSystem.test.ts b/apps/web/src/FullVisionSystem.test.ts new file mode 100644 index 00000000..858d7bed --- /dev/null +++ b/apps/web/src/FullVisionSystem.test.ts @@ -0,0 +1,212 @@ +import { describe, expect, it } from "vitest"; +import { assessCaptureQuality, type AudioCapture } from "@everything-rings/acquisition"; +import { analyzeImpact, extractImpactRingdown, type AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { + benchmarkSonicTwinRetrieval, + buildAcousticObjectModel, + buildSonicTwinIndex, + calibratedPredictions, + compareFingerprintToObjectModel, + evaluateCalibration, + fitIsotonicCalibration, + riskCoverageCurve, + type AcousticObjectObservationV1, +} from "@everything-rings/fingerprint"; +import { renderPlayableNote } from "@everything-rings/instrument"; +import { + buildAtlasGraph, + contentDigest, + createAtlasCollection, + createAtlasRecord, + createDerivationRecord, + createMeasurementRecord, + createStationCalibrationProtocol, + emptyResearchRepository, + evaluateStationCalibration, + ingestDerivation, + ingestMeasurement, + publishRepositoryAtlasRecord, + searchAtlas, + snapshotAtlasRegistry, + verifyResearchRepositoryIntegrity, +} from "@everything-rings/validation"; + +const SAMPLE_RATE = 48_000; +const REVISION = "0123456789abcdef0123456789abcdef01234567"; + +interface DigitalSpecimen { + readonly id: string; + readonly baseModes: readonly { f: number; tau: number; a: number }[]; +} + +const SPECIMENS: readonly DigitalSpecimen[] = [ + { id: "twin-bell-a", baseModes: [{ f: 431, tau: 1.1, a: 0.20 }, { f: 907, tau: 0.9, a: 0.14 }, { f: 1499, tau: 0.7, a: 0.10 }, { f: 2281, tau: 0.55, a: 0.07 }] }, + { id: "twin-bell-b", baseModes: [{ f: 472, tau: 1.0, a: 0.19 }, { f: 936, tau: 0.8, a: 0.13 }, { f: 1448, tau: 0.65, a: 0.09 }, { f: 2197, tau: 0.50, a: 0.06 }] }, + { id: "twin-plate-c", baseModes: [{ f: 613, tau: 0.8, a: 0.18 }, { f: 1033, tau: 0.65, a: 0.12 }, { f: 1717, tau: 0.5, a: 0.08 }, { f: 2811, tau: 0.4, a: 0.05 }] }, +]; + +function pseudoRandom(seed: number): () => number { + let state = seed >>> 0; + return () => { + state = Math.imul(1664525, state) + 1013904223 >>> 0; + return state / 0x1_0000_0000; + }; +} + +function captureFor(specimen: DigitalSpecimen, replicate: number): AudioCapture { + const triggerSample = Math.round(0.15 * SAMPLE_RATE); + const samples = new Float32Array(Math.round(2.4 * SAMPLE_RATE)); + const random = pseudoRandom(1000 + replicate * 31 + specimen.id.length * 17); + const frequencyScale = 1 + (replicate - 1) * 0.0007; + const decayScale = 1 + (replicate - 1) * 0.025; + const amplitudeScale = 1 - (replicate - 1) * 0.04; + for (let index = 0; index < samples.length; index += 1) { + const noise = (2 * random() - 1) * 0.00018; + if (index < triggerSample) { + samples[index] = noise; + continue; + } + const t = (index - triggerSample) / SAMPLE_RATE; + let value = noise; + specimen.baseModes.forEach((mode, modeIndex) => { + value += mode.a * amplitudeScale * Math.exp(-t / (mode.tau * decayScale)) + * Math.sin(2 * Math.PI * mode.f * frequencyScale * t + 0.35 * modeIndex + replicate * 0.07); + }); + samples[index] = Math.max(-0.95, Math.min(0.95, value)); + } + return { samples, sampleRate: SAMPLE_RATE, triggerSample }; +} + +function fingerprintFromCapture(capture: AudioCapture): AcousticFingerprintV1 { + const quality = assessCaptureQuality(capture); + expect(quality.ok).toBe(true); + const ringdown = extractImpactRingdown(capture.samples, capture.sampleRate, capture.triggerSample); + const result = analyzeImpact(ringdown.samples, ringdown.sampleRate); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error(result.reason); + expect(result.fingerprint.modes.length).toBeGreaterThanOrEqual(3); + return result.fingerprint; +} + +function peak(samples: Float32Array): number { + let maximum = 0; + for (const value of samples) maximum = Math.max(maximum, Math.abs(value)); + return maximum; +} + +describe("EVERYTHING RINGS full-vision software system", () => { + it("qualifies digital capture through object models, retrieval, calibration, station checks, provenance, Atlas, graph, and playable output", async () => { + const observations: AcousticObjectObservationV1[] = []; + for (const specimen of SPECIMENS) { + for (let replicate = 0; replicate < 3; replicate += 1) { + observations.push({ + observationId: `${specimen.id}-r${replicate + 1}`, + specimenId: specimen.id, + fingerprint: fingerprintFromCapture(captureFor(specimen, replicate)), + }); + } + } + + const index = buildSonicTwinIndex(observations); + expect(index.entries).toHaveLength(3); + const heldOutQueries = SPECIMENS.map((specimen, indexValue) => ({ + queryId: `heldout-${specimen.id}`, + trueSpecimenId: specimen.id, + fingerprint: fingerprintFromCapture(captureFor(specimen, 4 + indexValue)), + })); + const retrieval = benchmarkSonicTwinRetrieval(heldOutQueries, index); + expect(retrieval.recallAt1).toBe(1); + expect(retrieval.meanReciprocalRank).toBe(1); + + const labeledScores = heldOutQueries.flatMap((query) => index.entries.map((entry) => { + const comparison = compareFingerprintToObjectModel(query.fingerprint, entry.model); + return { + pairId: `${query.queryId}::${entry.specimenId}`, + score: comparison.evidenceScore, + samePhysicalSpecimen: query.trueSpecimenId === entry.specimenId, + }; + })); + const calibration = fitIsotonicCalibration(labeledScores, "full-vision-digital-twin-system-1"); + const predictions = calibratedPredictions(calibration, labeledScores); + const calibrationMetrics = evaluateCalibration(predictions, 5); + expect(calibrationMetrics.rocAuc).not.toBeNull(); + expect(calibrationMetrics.rocAuc!).toBeGreaterThan(0.9); + const risk = riskCoverageCurve(predictions, [0, 0.2, 0.4]); + expect(risk[0]?.coverage).toBe(1); + + const referenceModel = buildAcousticObjectModel(observations.filter((observation) => observation.specimenId === SPECIMENS[0]!.id)); + const stationProtocol = await createStationCalibrationProtocol({ + createdAt: "2026-08-24T16:00:00.000Z", + referencePopulation: "full-vision-digital-reference-1", + referenceSpecimenId: SPECIMENS[0]!.id, + referenceObjectModel: referenceModel, + minimumCoverage: 0.75, + maximumMedianFrequencyDistanceCents: 45, + minimumMatchedModes: 3, + }); + const stationVerdict = evaluateStationCalibration(stationProtocol, { + stationId: "digital-station-system-test", + createdAt: "2026-08-24T16:01:00.000Z", + specimenId: SPECIMENS[0]!.id, + fingerprint: heldOutQueries[0]!.fingerprint, + }); + expect(stationVerdict.passed).toBe(true); + + let repository = emptyResearchRepository(); + const atlasRecordIds: string[] = []; + for (let indexValue = 0; indexValue < heldOutQueries.length; indexValue += 1) { + const query = heldOutQueries[indexValue]!; + const measurement = await createMeasurementRecord({ + createdAt: `2026-08-24T16:1${indexValue}:00.000Z`, + specimenId: query.trueSpecimenId, + sessionId: `system-session-${indexValue + 1}`, + attemptId: 1, + material: "other", + acquisitionContractVersion: "full-vision-digital-capture-1", + softwareRevision: REVISION, + fingerprintAlgorithmVersion: query.fingerprint.algorithmVersion, + setup: { fixedSetup: true, microphoneDistanceCm: 20, striker: "digital impulse", strikeLocation: "digital A", supportCondition: "digital fixed" }, + station: { stationId: "digital-station-system-test", deviceDescription: "deterministic software twin", operatingSystem: "ci", runtime: "vitest", microphoneDescription: null, captureSettings: { sampleRate: SAMPLE_RATE, channelCount: 1 } }, + fingerprint: query.fingerprint, + }); + repository = await ingestMeasurement(repository, measurement); + const derivation = await createDerivationRecord({ + measurementId: measurement.measurementId, + kind: "similarity", + algorithmVersion: "acoustic-object-model-1+sonic-twin-index-1", + configDigest: await contentDigest({ stationProtocol: stationProtocol.protocolId }), + artifactDigest: await contentDigest({ ranking: index.entries.map((entry) => compareFingerprintToObjectModel(query.fingerprint, entry.model)) }), + createdAt: `2026-08-24T16:2${indexValue}:00.000Z`, + }); + repository = await ingestDerivation(repository, derivation); + const atlas = await createAtlasRecord({ + createdAt: `2026-08-24T16:3${indexValue}:00.000Z`, + contributor: { contributorId: "digital-system", displayName: "Full-vision software qualification" }, + specimen: { specimenId: query.trueSpecimenId, label: query.trueSpecimenId, objectFamily: query.trueSpecimenId.includes("plate") ? "plate" : "bell", material: "other", publicDescription: "Synthetic full-vision system record" }, + measurements: [{ measurementId: measurement.measurementId, derivationIds: [derivation.derivationId] }], + }); + repository = await publishRepositoryAtlasRecord(repository, atlas); + atlasRecordIds.push(atlas.atlasRecordId); + + const playable = renderPlayableNote(query.fingerprint, 60 + indexValue * 7, SAMPLE_RATE); + expect(playable.samples.length).toBeGreaterThan(0); + expect(peak(playable.samples)).toBeLessThanOrEqual(0.90001); + } + + const integrity = await verifyResearchRepositoryIntegrity(repository); + expect(integrity.valid).toBe(true); + expect(searchAtlas(repository.atlas, { text: "twin" }).length).toBeGreaterThanOrEqual(2); + const collection = await createAtlasCollection({ + createdAt: "2026-08-24T16:40:00.000Z", + contributorId: "digital-system", + title: "Full-vision digital resonators", + description: "Software qualification collection", + atlasRecordIds, + }); + const graph = buildAtlasGraph(repository.atlas, [collection]); + expect(graph.nodes.some((node) => node.kind === "collection")).toBe(true); + expect(graph.edges.filter((edge) => edge.kind === "member-of")).toHaveLength(3); + const snapshot = await snapshotAtlasRegistry(repository.atlas, "2026-08-24T16:41:00.000Z"); + expect(snapshot.recordIds).toHaveLength(3); + }); +}); From 38b6301e63b4cb4fe0af83b4c1a3a7e2fe2c53d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:57:39 -0700 Subject: [PATCH 080/101] Add spatial modal sound-field model --- packages/fingerprint/src/modal-field.ts | 159 ++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 packages/fingerprint/src/modal-field.ts diff --git a/packages/fingerprint/src/modal-field.ts b/packages/fingerprint/src/modal-field.ts new file mode 100644 index 00000000..07bc22fb --- /dev/null +++ b/packages/fingerprint/src/modal-field.ts @@ -0,0 +1,159 @@ +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { centsDistance } from "./recurrence"; +import { buildAcousticObjectModel, type AcousticObjectModelV1 } from "./object-model"; + +export interface SpatialPointV1 { + readonly x: number; + readonly y: number; + readonly z: number; +} + +export interface SpatialFingerprintObservationV1 { + readonly observationId: string; + readonly specimenId: string; + readonly strikePoint: SpatialPointV1; + readonly fingerprint: AcousticFingerprintV1; +} + +export interface SpatialModalSampleV1 { + readonly observationId: string; + readonly strikePoint: SpatialPointV1; + readonly relativeAmplitude: number; + readonly sourceFrequencyHz: number; +} + +export interface SpatialModalFieldModeV1 { + readonly frequencyHz: number; + readonly decaySeconds: number; + readonly samples: readonly SpatialModalSampleV1[]; +} + +export interface SpatialModalSoundFieldV1 { + readonly schemaVersion: 1; + readonly modalFieldVersion: "spatial-modal-sound-field-1"; + readonly specimenId: string; + readonly objectModel: AcousticObjectModelV1; + readonly modes: readonly SpatialModalFieldModeV1[]; + readonly observationCount: number; +} + +function assertPoint(point: SpatialPointV1): void { + if (![point.x, point.y, point.z].every(Number.isFinite)) throw new Error("spatial strike point coordinates must be finite"); +} + +export function buildSpatialModalSoundField( + observations: readonly SpatialFingerprintObservationV1[], + maximumModeAssociationCents = 120, +): SpatialModalSoundFieldV1 { + if (observations.length < 2) throw new Error("spatial modal field requires at least two observations"); + if (!(maximumModeAssociationCents > 0) || !Number.isFinite(maximumModeAssociationCents)) throw new Error("maximumModeAssociationCents must be finite and positive"); + const specimenId = observations[0]!.specimenId.trim(); + if (specimenId.length === 0) throw new Error("spatial modal field specimenId is required"); + const normalized = specimenId.toLocaleLowerCase("en-US"); + const observationIds = new Set(); + for (const observation of observations) { + if (observation.specimenId.trim().toLocaleLowerCase("en-US") !== normalized) throw new Error("spatial modal field cannot mix specimen IDs"); + if (observationIds.has(observation.observationId)) throw new Error(`duplicate spatial observationId ${observation.observationId}`); + observationIds.add(observation.observationId); + assertPoint(observation.strikePoint); + } + const objectModel = buildAcousticObjectModel(observations.map((observation) => ({ + observationId: observation.observationId, + specimenId: observation.specimenId, + fingerprint: observation.fingerprint, + }))); + + const modes = objectModel.modes.map((modelMode): SpatialModalFieldModeV1 => { + const samples: SpatialModalSampleV1[] = []; + for (const observation of observations) { + let best: { frequencyHz: number; relativeAmplitude: number; distance: number } | undefined; + for (const mode of observation.fingerprint.modes) { + const distance = centsDistance(modelMode.frequencyHzMedian, mode.frequencyHz); + if (distance <= maximumModeAssociationCents && (best === undefined || distance < best.distance)) { + best = { frequencyHz: mode.frequencyHz, relativeAmplitude: mode.relativeAmplitude, distance }; + } + } + if (best !== undefined) { + samples.push({ + observationId: observation.observationId, + strikePoint: observation.strikePoint, + relativeAmplitude: best.relativeAmplitude, + sourceFrequencyHz: best.frequencyHz, + }); + } + } + return { frequencyHz: modelMode.frequencyHzMedian, decaySeconds: modelMode.decaySecondsMedian, samples }; + }); + + return { + schemaVersion: 1, + modalFieldVersion: "spatial-modal-sound-field-1", + specimenId, + objectModel, + modes, + observationCount: observations.length, + }; +} + +function distance(left: SpatialPointV1, right: SpatialPointV1): number { + return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z); +} + +function interpolateAmplitude(samples: readonly SpatialModalSampleV1[], point: SpatialPointV1, power: number): number { + if (samples.length === 0) return 0; + for (const sample of samples) if (distance(sample.strikePoint, point) <= 1e-9) return sample.relativeAmplitude; + let weighted = 0; + let weightSum = 0; + for (const sample of samples) { + const d = Math.max(1e-9, distance(sample.strikePoint, point)); + const weight = 1 / d ** power; + weighted += weight * sample.relativeAmplitude; + weightSum += weight; + } + return weightSum === 0 ? 0 : weighted / weightSum; +} + +export interface SpatialModalFieldQueryV1 { + readonly point: SpatialPointV1; + readonly interpolationPower?: number; + readonly sampleRate?: number; + readonly durationSeconds?: number; +} + +export function fingerprintAtSpatialPoint( + field: SpatialModalSoundFieldV1, + query: SpatialModalFieldQueryV1, +): AcousticFingerprintV1 { + assertPoint(query.point); + const interpolationPower = query.interpolationPower ?? 2; + if (!(interpolationPower > 0) || !Number.isFinite(interpolationPower)) throw new Error("interpolationPower must be finite and positive"); + const amplitudes = field.modes.map((mode) => interpolateAmplitude(mode.samples, query.point, interpolationPower)); + const maximumAmplitude = Math.max(0, ...amplitudes); + const sampleRate = query.sampleRate ?? field.objectModel.sampleRates[0] ?? 48_000; + const durationSeconds = query.durationSeconds ?? 2; + return { + version: 1, + algorithmVersion: field.objectModel.fingerprintAlgorithmVersions[0] as AcousticFingerprintV1["algorithmVersion"], + sampleRate, + durationSeconds, + modes: field.modes.flatMap((mode, index) => { + const amplitude = amplitudes[index] ?? 0; + if (!(amplitude > 0) || !(maximumAmplitude > 0)) return []; + const relativeAmplitude = amplitude / maximumAmplitude; + return [{ + frequencyHz: mode.frequencyHz, + relativeAmplitude, + decaySeconds: mode.decaySeconds, + q: Math.PI * mode.frequencyHz * mode.decaySeconds, + confidence: Math.min(1, mode.samples.length / Math.max(1, field.observationCount)), + diagnostics: { + prominenceDb: 0, + persistenceSeconds: mode.decaySeconds, + frequencyStdCents: field.objectModel.modes[index]?.frequencyMadCents ?? 0, + decayFitScore: 0, + observationCount: mode.samples.length, + }, + }]; + }), + }; +} From 39532a35c6a44c39049a37b02535947972563eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:58:04 -0700 Subject: [PATCH 081/101] Export spatial modal sound-field APIs --- packages/fingerprint/src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/fingerprint/src/index.ts b/packages/fingerprint/src/index.ts index 3e97e964..e61616c6 100644 --- a/packages/fingerprint/src/index.ts +++ b/packages/fingerprint/src/index.ts @@ -39,6 +39,16 @@ export { type AcousticObjectObservationV1, type FingerprintToObjectModelComparisonV1, } from "./object-model"; +export { + buildSpatialModalSoundField, + fingerprintAtSpatialPoint, + type SpatialFingerprintObservationV1, + type SpatialModalFieldModeV1, + type SpatialModalFieldQueryV1, + type SpatialModalSampleV1, + type SpatialModalSoundFieldV1, + type SpatialPointV1, +} from "./modal-field"; export { benchmarkSonicTwinRetrieval, buildSonicTwinIndex, From 43e17303d5687e3542897d084224c4b23aed2dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:58:23 -0700 Subject: [PATCH 082/101] Test spatial modal sound-field interpolation --- packages/fingerprint/test/modal-field.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/fingerprint/test/modal-field.test.ts diff --git a/packages/fingerprint/test/modal-field.test.ts b/packages/fingerprint/test/modal-field.test.ts new file mode 100644 index 00000000..3eba06a7 --- /dev/null +++ b/packages/fingerprint/test/modal-field.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { buildSpatialModalSoundField, fingerprintAtSpatialPoint } from "../src"; + +function fp(amplitudes: readonly number[]): AcousticFingerprintV1 { + const frequencies = [440, 880, 1320]; + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: frequencies.map((frequencyHz, index) => ({ + frequencyHz: frequencyHz + index * 0.2, + relativeAmplitude: amplitudes[index] ?? 0.1, + decaySeconds: 0.8 / (index + 1), + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), + }; +} + +describe("spatial modal sound field", () => { + it("preserves global modal frequencies while spatially interpolating excitation amplitudes", () => { + const field = buildSpatialModalSoundField([ + { observationId: "left", specimenId: "specimen-field", strikePoint: { x: 0, y: 0, z: 0 }, fingerprint: fp([1, 0.2, 0.1]) }, + { observationId: "right", specimenId: "specimen-field", strikePoint: { x: 1, y: 0, z: 0 }, fingerprint: fp([0.1, 1, 0.4]) }, + { observationId: "top", specimenId: "specimen-field", strikePoint: { x: 0.5, y: 1, z: 0 }, fingerprint: fp([0.3, 0.4, 1]) }, + ]); + expect(field.modes).toHaveLength(3); + expect(field.objectModel.observationCount).toBe(3); + + const left = fingerprintAtSpatialPoint(field, { point: { x: 0, y: 0, z: 0 } }); + const right = fingerprintAtSpatialPoint(field, { point: { x: 1, y: 0, z: 0 } }); + const center = fingerprintAtSpatialPoint(field, { point: { x: 0.5, y: 0.4, z: 0 } }); + expect(left.modes[0]!.relativeAmplitude).toBe(1); + expect(right.modes[1]!.relativeAmplitude).toBe(1); + expect(center.modes.every((mode) => Number.isFinite(mode.relativeAmplitude))).toBe(true); + expect(center.modes.map((mode) => mode.frequencyHz)).toEqual(left.modes.map((mode) => mode.frequencyHz)); + }); + + it("rejects mixed specimen identity in one field", () => { + expect(() => buildSpatialModalSoundField([ + { observationId: "a", specimenId: "a", strikePoint: { x: 0, y: 0, z: 0 }, fingerprint: fp([1, 1, 1]) }, + { observationId: "b", specimenId: "b", strikePoint: { x: 1, y: 0, z: 0 }, fingerprint: fp([1, 1, 1]) }, + ])).toThrow(/mix specimen IDs/); + }); +}); From 1ec9d68a88eb20dd25623a1b47c0ee7b524e4411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:58:59 -0700 Subject: [PATCH 083/101] Add research-only material inference baseline --- packages/fingerprint/src/material-research.ts | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 packages/fingerprint/src/material-research.ts diff --git a/packages/fingerprint/src/material-research.ts b/packages/fingerprint/src/material-research.ts new file mode 100644 index 00000000..e285b5c9 --- /dev/null +++ b/packages/fingerprint/src/material-research.ts @@ -0,0 +1,166 @@ +import type { AcousticObjectModelV1 } from "./object-model"; + +export interface MaterialTrainingExampleV1 { + readonly exampleId: string; + readonly materialLabel: string; + readonly model: AcousticObjectModelV1; +} + +export interface MaterialCentroidV1 { + readonly materialLabel: string; + readonly exampleCount: number; + readonly featureCentroid: readonly number[]; +} + +export interface MaterialInferenceModelV1 { + readonly schemaVersion: 1; + readonly materialInferenceVersion: "material-centroid-research-1"; + readonly evidenceEligible: false; + readonly trainingPopulation: string; + readonly featureVersion: "modal-summary-features-1"; + readonly featureDimension: number; + readonly centroids: readonly MaterialCentroidV1[]; +} + +const MAX_MODES = 8; + +function modelFeatures(model: AcousticObjectModelV1): number[] { + const modes = [...model.modes].sort((left, right) => left.frequencyHzMedian - right.frequencyHzMedian).slice(0, MAX_MODES); + const anchor = modes[0]?.frequencyHzMedian ?? 1; + const features: number[] = [Math.min(1, modes.length / MAX_MODES)]; + for (let index = 0; index < MAX_MODES; index += 1) { + const mode = modes[index]; + if (mode === undefined) { + features.push(0, 0, 0, 0); + continue; + } + features.push( + Math.log2(Math.max(mode.frequencyHzMedian / anchor, 1e-9)) / 8, + Math.log(Math.max(mode.decaySecondsMedian, 1e-9)) / 8, + Math.log10(Math.max(mode.relativeAmplitudeMedian, 1e-9)) / 4, + mode.observationSupportFraction, + ); + } + return features; +} + +function distance(left: readonly number[], right: readonly number[]): number { + if (left.length !== right.length) throw new Error("material feature dimensions differ"); + let squared = 0; + for (let index = 0; index < left.length; index += 1) squared += (left[index]! - right[index]!) ** 2; + return Math.sqrt(squared / Math.max(1, left.length)); +} + +export function fitMaterialCentroidResearch( + examples: readonly MaterialTrainingExampleV1[], + trainingPopulation: string, +): MaterialInferenceModelV1 { + if (examples.length === 0) throw new Error("material research model requires examples"); + if (trainingPopulation.trim().length === 0) throw new Error("material trainingPopulation is required"); + const seen = new Set(); + const grouped = new Map(); + for (const example of examples) { + if (example.exampleId.trim().length === 0) throw new Error("material exampleId is required"); + if (seen.has(example.exampleId)) throw new Error(`duplicate material exampleId ${example.exampleId}`); + seen.add(example.exampleId); + const label = example.materialLabel.trim(); + if (label.length === 0) throw new Error("materialLabel is required"); + const group = grouped.get(label) ?? []; + group.push(modelFeatures(example.model)); + grouped.set(label, group); + } + if (grouped.size < 2) throw new Error("material research model requires at least two material labels"); + const featureDimension = modelFeatures(examples[0]!.model).length; + const centroids = [...grouped.entries()].map(([materialLabel, features]) => ({ + materialLabel, + exampleCount: features.length, + featureCentroid: Array.from({ length: featureDimension }, (_, index) => features.reduce((sum, row) => sum + row[index]!, 0) / features.length), + })).sort((left, right) => left.materialLabel.localeCompare(right.materialLabel)); + return { + schemaVersion: 1, + materialInferenceVersion: "material-centroid-research-1", + evidenceEligible: false, + trainingPopulation: trainingPopulation.trim(), + featureVersion: "modal-summary-features-1", + featureDimension, + centroids, + }; +} + +export interface MaterialPredictionV1 { + readonly predictionVersion: "material-centroid-prediction-1"; + readonly decision: "label" | "abstain"; + readonly materialLabel: string | null; + readonly nearestDistance: number; + readonly secondDistance: number | null; + readonly distanceMargin: number | null; + readonly trainingPopulation: string; + readonly calibratedProbability: null; +} + +export function predictMaterialResearch( + model: MaterialInferenceModelV1, + objectModel: AcousticObjectModelV1, + minimumDistanceMargin = 0.03, +): MaterialPredictionV1 { + if (!(minimumDistanceMargin >= 0) || !Number.isFinite(minimumDistanceMargin)) throw new Error("minimumDistanceMargin must be finite and non-negative"); + const features = modelFeatures(objectModel); + const ranked = model.centroids + .map((centroid) => ({ materialLabel: centroid.materialLabel, distance: distance(features, centroid.featureCentroid) })) + .sort((left, right) => left.distance - right.distance || left.materialLabel.localeCompare(right.materialLabel)); + const nearest = ranked[0]; + if (nearest === undefined) throw new Error("material model has no centroids"); + const second = ranked[1]; + const margin = second === undefined ? null : second.distance - nearest.distance; + const decision = margin !== null && margin >= minimumDistanceMargin ? "label" as const : "abstain" as const; + return { + predictionVersion: "material-centroid-prediction-1", + decision, + materialLabel: decision === "label" ? nearest.materialLabel : null, + nearestDistance: nearest.distance, + secondDistance: second?.distance ?? null, + distanceMargin: margin, + trainingPopulation: model.trainingPopulation, + calibratedProbability: null, + }; +} + +export interface MaterialBenchmarkExampleV1 { + readonly exampleId: string; + readonly trueMaterialLabel: string; + readonly model: AcousticObjectModelV1; +} + +export interface MaterialBenchmarkMetricsV1 { + readonly exampleCount: number; + readonly coveredCount: number; + readonly abstainedCount: number; + readonly coverage: number; + readonly coveredAccuracy: number | null; + readonly errors: readonly { readonly exampleId: string; readonly expected: string; readonly predicted: string }[]; +} + +export function benchmarkMaterialResearch( + model: MaterialInferenceModelV1, + examples: readonly MaterialBenchmarkExampleV1[], + minimumDistanceMargin = 0.03, +): MaterialBenchmarkMetricsV1 { + const errors: Array<{ exampleId: string; expected: string; predicted: string }> = []; + let covered = 0; + let correct = 0; + for (const example of examples) { + const prediction = predictMaterialResearch(model, example.model, minimumDistanceMargin); + if (prediction.decision === "abstain" || prediction.materialLabel === null) continue; + covered += 1; + if (prediction.materialLabel === example.trueMaterialLabel) correct += 1; + else errors.push({ exampleId: example.exampleId, expected: example.trueMaterialLabel, predicted: prediction.materialLabel }); + } + return { + exampleCount: examples.length, + coveredCount: covered, + abstainedCount: examples.length - covered, + coverage: examples.length === 0 ? 0 : covered / examples.length, + coveredAccuracy: covered === 0 ? null : correct / covered, + errors, + }; +} From b78dd59b63d281df97bf311498a31fd2d7c21db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:59:18 -0700 Subject: [PATCH 084/101] Export research-only material inference APIs --- packages/fingerprint/src/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/fingerprint/src/index.ts b/packages/fingerprint/src/index.ts index e61616c6..b2c57537 100644 --- a/packages/fingerprint/src/index.ts +++ b/packages/fingerprint/src/index.ts @@ -80,3 +80,14 @@ export { type RiskCoveragePointV1, type SonicTwinCalibrationModelV1, } from "./calibration"; +export { + benchmarkMaterialResearch, + fitMaterialCentroidResearch, + predictMaterialResearch, + type MaterialBenchmarkExampleV1, + type MaterialBenchmarkMetricsV1, + type MaterialCentroidV1, + type MaterialInferenceModelV1, + type MaterialPredictionV1, + type MaterialTrainingExampleV1, +} from "./material-research"; From 908b51172bfc4f751ff0dc2929e439fa95476172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:59:40 -0700 Subject: [PATCH 085/101] Test research-only material inference baseline --- .../test/material-research.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/fingerprint/test/material-research.test.ts diff --git a/packages/fingerprint/test/material-research.test.ts b/packages/fingerprint/test/material-research.test.ts new file mode 100644 index 00000000..9709de74 --- /dev/null +++ b/packages/fingerprint/test/material-research.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import { benchmarkMaterialResearch, buildAcousticObjectModel, fitMaterialCentroidResearch, predictMaterialResearch } from "../src"; + +function fp(base: number, ratios: readonly number[], decays: readonly number[]): AcousticFingerprintV1 { + return { + version: 1, + algorithmVersion: "er-dsp-2", + sampleRate: 48_000, + durationSeconds: 2, + modes: ratios.map((ratio, index) => ({ + frequencyHz: base * ratio, + relativeAmplitude: 1 / (index + 1), + decaySeconds: decays[index] ?? 0.4, + q: 100, + confidence: 0.9, + diagnostics: { prominenceDb: 20, persistenceSeconds: 0.2, frequencyStdCents: 2, decayFitScore: 0.95, observationCount: 12 }, + })), + }; +} + +function model(specimenId: string, fingerprints: readonly AcousticFingerprintV1[]) { + return buildAcousticObjectModel(fingerprints.map((fingerprint, index) => ({ observationId: `${specimenId}-${index}`, specimenId, fingerprint }))); +} + +describe("research-only material inference", () => { + it("learns population-bound modal centroids and abstains when evidence margin is weak", () => { + const metalA = model("metal-a", [fp(440, [1, 2.05, 3.2], [1.1, 0.8, 0.6]), fp(442, [1, 2.04, 3.19], [1.05, 0.78, 0.58])]); + const metalB = model("metal-b", [fp(500, [1, 2.02, 3.15], [1.0, 0.75, 0.55]), fp(502, [1, 2.03, 3.16], [1.02, 0.77, 0.56])]); + const glassA = model("glass-a", [fp(600, [1, 2.45, 4.1], [1.8, 1.3, 1.0]), fp(602, [1, 2.44, 4.08], [1.75, 1.28, 0.98])]); + const glassB = model("glass-b", [fp(670, [1, 2.48, 4.12], [1.7, 1.25, 0.95]), fp(672, [1, 2.47, 4.11], [1.72, 1.27, 0.96])]); + const research = fitMaterialCentroidResearch([ + { exampleId: "ma", materialLabel: "metal", model: metalA }, + { exampleId: "mb", materialLabel: "metal", model: metalB }, + { exampleId: "ga", materialLabel: "glass", model: glassA }, + { exampleId: "gb", materialLabel: "glass", model: glassB }, + ], "digital-material-population-1"); + expect(research.evidenceEligible).toBe(false); + expect(research.centroids).toHaveLength(2); + + const metalQuery = model("metal-q", [fp(550, [1, 2.04, 3.18], [1.03, 0.77, 0.57]), fp(552, [1, 2.03, 3.17], [1.01, 0.76, 0.56])]); + const prediction = predictMaterialResearch(research, metalQuery, 0.005); + expect(prediction.decision).toBe("label"); + expect(prediction.materialLabel).toBe("metal"); + expect(prediction.calibratedProbability).toBeNull(); + expect(prediction.trainingPopulation).toBe("digital-material-population-1"); + + const ambiguous = predictMaterialResearch(research, metalQuery, 10); + expect(ambiguous.decision).toBe("abstain"); + expect(ambiguous.materialLabel).toBeNull(); + }); + + it("reports coverage and covered accuracy without turning software labels into a physical claim", () => { + const metal = model("metal", [fp(440, [1, 2, 3], [1, 0.8, 0.6]), fp(441, [1, 2, 3], [1, 0.8, 0.6])]); + const glass = model("glass", [fp(600, [1, 2.5, 4], [1.8, 1.3, 1]), fp(601, [1, 2.5, 4], [1.8, 1.3, 1])]); + const fitted = fitMaterialCentroidResearch([ + { exampleId: "m", materialLabel: "metal", model: metal }, + { exampleId: "g", materialLabel: "glass", model: glass }, + ], "digital-two-class-1"); + const report = benchmarkMaterialResearch(fitted, [ + { exampleId: "mt", trueMaterialLabel: "metal", model: metal }, + { exampleId: "gt", trueMaterialLabel: "glass", model: glass }, + ], 0); + expect(report.coverage).toBe(1); + expect(report.coveredAccuracy).toBe(1); + expect(report.errors).toHaveLength(0); + }); +}); From 607723dcc39d8eaa4d871f60920211a22a213c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:00:21 -0700 Subject: [PATCH 086/101] Add testable Atlas service boundary --- packages/validation/src/atlas-service.ts | 79 ++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 packages/validation/src/atlas-service.ts diff --git a/packages/validation/src/atlas-service.ts b/packages/validation/src/atlas-service.ts new file mode 100644 index 00000000..4b910250 --- /dev/null +++ b/packages/validation/src/atlas-service.ts @@ -0,0 +1,79 @@ +import type { ResonanceAtlasRecordV1, ResonanceAtlasSnapshotV1 } from "./atlas"; +import { + emptyAtlasRegistry, + publishAtlasRecord, + searchAtlas, + snapshotAtlasRegistry, + type AtlasRegistryStateV1, + type AtlasSearchQueryV1, + type AtlasSearchResultV1, +} from "./atlas-registry"; + +export interface ResonanceAtlasServiceV1 { + readonly serviceContractVersion: "resonance-atlas-service-1"; + publish(record: ResonanceAtlasRecordV1): Promise<{ readonly inserted: boolean; readonly atlasRecordId: string }>; + get(atlasRecordId: string): Promise; + search(query: AtlasSearchQueryV1): Promise; + snapshot(createdAt: string): Promise; + count(): Promise; +} + +/** + * Deterministic reference adapter for tests, local research tools, and contract + * conformance. A production network service can implement the same interface. + */ +export class MemoryResonanceAtlasServiceV1 implements ResonanceAtlasServiceV1 { + readonly serviceContractVersion = "resonance-atlas-service-1" as const; + private state: AtlasRegistryStateV1 = emptyAtlasRegistry(); + + async publish(record: ResonanceAtlasRecordV1): Promise<{ readonly inserted: boolean; readonly atlasRecordId: string }> { + const before = this.state.records.length; + this.state = await publishAtlasRecord(this.state, record); + return { inserted: this.state.records.length > before, atlasRecordId: record.atlasRecordId }; + } + + async get(atlasRecordId: string): Promise { + return this.state.records.find((record) => record.atlasRecordId === atlasRecordId) ?? null; + } + + async search(query: AtlasSearchQueryV1): Promise { + return searchAtlas(this.state, query); + } + + async snapshot(createdAt: string): Promise { + return snapshotAtlasRegistry(this.state, createdAt); + } + + async count(): Promise { + return this.state.records.length; + } + + exportState(): AtlasRegistryStateV1 { + return { + ...this.state, + records: [...this.state.records], + specimenMerges: [...this.state.specimenMerges], + }; + } +} + +export async function verifyAtlasServiceConformance( + service: ResonanceAtlasServiceV1, + records: readonly ResonanceAtlasRecordV1[], +): Promise<{ readonly passed: boolean; readonly reasons: readonly string[] }> { + const reasons: string[] = []; + for (const record of records) { + const first = await service.publish(record); + const second = await service.publish(record); + if (!first.inserted) reasons.push(`first publication of ${record.atlasRecordId} was not inserted`); + if (second.inserted) reasons.push(`duplicate publication of ${record.atlasRecordId} was not idempotent`); + const fetched = await service.get(record.atlasRecordId); + if (fetched?.atlasRecordId !== record.atlasRecordId) reasons.push(`record ${record.atlasRecordId} could not be read back`); + } + if (await service.count() !== new Set(records.map((record) => record.atlasRecordId)).size) reasons.push("service count disagrees with unique published records"); + if (records.length > 0) { + const snapshot = await service.snapshot("2026-08-24T16:59:00.000Z"); + if (snapshot.recordIds.length !== new Set(records.map((record) => record.atlasRecordId)).size) reasons.push("service snapshot omitted published records"); + } + return { passed: reasons.length === 0, reasons }; +} From 9a347c55b036214fefac8dcb337b106f2e4479e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:00:43 -0700 Subject: [PATCH 087/101] Export Atlas service contract and reference adapter --- packages/validation/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 2ea89d5a..4a9dac43 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -66,6 +66,11 @@ export { type AtlasGraphNodeV1, type AtlasGraphSnapshotV1, } from "./atlas-network"; +export { + MemoryResonanceAtlasServiceV1, + verifyAtlasServiceConformance, + type ResonanceAtlasServiceV1, +} from "./atlas-service"; export { emptyResearchRepository, ingestDerivation, From 318ea40e38659ed55cdf016a421b983906ec2210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:00:54 -0700 Subject: [PATCH 088/101] Test Atlas service contract conformance --- .../validation/test/atlas-service.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/validation/test/atlas-service.test.ts diff --git a/packages/validation/test/atlas-service.test.ts b/packages/validation/test/atlas-service.test.ts new file mode 100644 index 00000000..e96e09f9 --- /dev/null +++ b/packages/validation/test/atlas-service.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { contentDigest, createAtlasRecord, MemoryResonanceAtlasServiceV1, verifyAtlasServiceConformance } from "../src"; + +async function record(specimenId: string, seed: string) { + return createAtlasRecord({ + createdAt: "2026-08-24T16:00:00.000Z", + contributor: { contributorId: "service-test", displayName: null }, + specimen: { specimenId, label: specimenId, objectFamily: "digital", material: "other", publicDescription: "service conformance fixture" }, + measurements: [{ measurementId: await contentDigest({ seed }), derivationIds: [] }], + }); +} + +describe("Resonance Atlas service contract", () => { + it("is idempotent, readable, searchable, and snapshot-capable", async () => { + const service = new MemoryResonanceAtlasServiceV1(); + const records = [await record("service-a", "a"), await record("service-b", "b")]; + const report = await verifyAtlasServiceConformance(service, records); + expect(report.passed).toBe(true); + expect(await service.count()).toBe(2); + expect(await service.get(records[0]!.atlasRecordId)).not.toBeNull(); + expect(await service.search({ text: "service" })).toHaveLength(2); + const snapshot = await service.snapshot("2026-08-24T16:59:00.000Z"); + expect(snapshot.recordIds).toHaveLength(2); + }); +}); From 9e4df3fde05c39df070e73e2ac6266cec5c86866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:01:38 -0700 Subject: [PATCH 089/101] Add content-addressed artifact transport envelopes --- packages/validation/src/artifact-envelope.ts | 128 +++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 packages/validation/src/artifact-envelope.ts diff --git a/packages/validation/src/artifact-envelope.ts b/packages/validation/src/artifact-envelope.ts new file mode 100644 index 00000000..5a443883 --- /dev/null +++ b/packages/validation/src/artifact-envelope.ts @@ -0,0 +1,128 @@ +import { contentDigest } from "./provenance"; + +export type EverythingRingsArtifactKind = + | "measurement" + | "derivation" + | "atlas-record" + | "atlas-snapshot" + | "atlas-collection" + | "station-calibration-protocol" + | "station-calibration-verdict" + | "sonic-twin-benchmark" + | "software-qualification"; + +export interface ArtifactEnvelopeV1 { + readonly schemaVersion: 1; + readonly envelopeContractVersion: "everything-rings-artifact-envelope-1"; + readonly artifactKind: EverythingRingsArtifactKind; + readonly artifactContractVersion: string; + readonly createdAt: string; + readonly payloadDigest: string; + readonly envelopeId: string; + readonly payload: T; +} + +export async function createArtifactEnvelope( + artifactKind: EverythingRingsArtifactKind, + artifactContractVersion: string, + payload: T, + createdAt: string, +): Promise> { + if (artifactContractVersion.trim().length === 0) throw new Error("artifactContractVersion is required"); + if (!Number.isFinite(Date.parse(createdAt))) throw new Error("artifact envelope createdAt is invalid"); + const payloadDigest = await contentDigest(payload); + const identity = { + schemaVersion: 1 as const, + envelopeContractVersion: "everything-rings-artifact-envelope-1" as const, + artifactKind, + artifactContractVersion: artifactContractVersion.trim(), + createdAt, + payloadDigest, + }; + const envelopeId = await contentDigest(identity); + return { ...identity, envelopeId, payload }; +} + +export async function verifyArtifactEnvelope(envelope: ArtifactEnvelopeV1): Promise { + if (envelope.schemaVersion !== 1 || envelope.envelopeContractVersion !== "everything-rings-artifact-envelope-1") return false; + if (!/^sha256:[0-9a-f]{64}$/.test(envelope.payloadDigest) || !/^sha256:[0-9a-f]{64}$/.test(envelope.envelopeId)) return false; + if (await contentDigest(envelope.payload) !== envelope.payloadDigest) return false; + const identity = { + schemaVersion: envelope.schemaVersion, + envelopeContractVersion: envelope.envelopeContractVersion, + artifactKind: envelope.artifactKind, + artifactContractVersion: envelope.artifactContractVersion, + createdAt: envelope.createdAt, + payloadDigest: envelope.payloadDigest, + }; + return envelope.envelopeId === await contentDigest(identity); +} + +export type ArtifactEnvelopeParseResult = + | { readonly ok: true; readonly envelope: ArtifactEnvelopeV1 } + | { readonly ok: false; readonly error: string }; + +const KINDS = new Set([ + "measurement", "derivation", "atlas-record", "atlas-snapshot", "atlas-collection", + "station-calibration-protocol", "station-calibration-verdict", "sonic-twin-benchmark", "software-qualification", +]); + +export async function parseArtifactEnvelope(value: unknown): Promise { + try { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("artifact envelope must be an object"); + const record = value as Record; + if (record.schemaVersion !== 1) throw new Error("artifact envelope schemaVersion must be 1"); + if (record.envelopeContractVersion !== "everything-rings-artifact-envelope-1") throw new Error("artifact envelope contract mismatch"); + if (typeof record.artifactKind !== "string" || !KINDS.has(record.artifactKind as EverythingRingsArtifactKind)) throw new Error("artifactKind is invalid"); + if (typeof record.artifactContractVersion !== "string" || record.artifactContractVersion.trim().length === 0) throw new Error("artifactContractVersion is required"); + if (typeof record.createdAt !== "string" || !Number.isFinite(Date.parse(record.createdAt))) throw new Error("createdAt is invalid"); + if (typeof record.payloadDigest !== "string" || typeof record.envelopeId !== "string") throw new Error("artifact envelope digests are required"); + const envelope = record as unknown as ArtifactEnvelopeV1; + if (!await verifyArtifactEnvelope(envelope)) throw new Error("artifact envelope content digest verification failed"); + return { ok: true, envelope }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } +} + +export async function parseArtifactEnvelopeJson(json: string): Promise { + try { + return parseArtifactEnvelope(JSON.parse(json) as unknown); + } catch (error) { + return { ok: false, error: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` }; + } +} + +export interface ArtifactMigrationStepV1 { + readonly artifactKind: EverythingRingsArtifactKind; + readonly fromContractVersion: string; + readonly toContractVersion: string; + readonly migrate: (payload: unknown) => unknown; +} + +export class ArtifactMigrationRegistryV1 { + private readonly steps = new Map(); + + register(step: ArtifactMigrationStepV1): void { + if (step.fromContractVersion === step.toContractVersion) throw new Error("migration must change contract version"); + const key = `${step.artifactKind}\u0000${step.fromContractVersion}`; + if (this.steps.has(key)) throw new Error(`duplicate migration from ${step.artifactKind}/${step.fromContractVersion}`); + this.steps.set(key, step); + } + + migrate(artifactKind: EverythingRingsArtifactKind, fromVersion: string, targetVersion: string, payload: unknown): unknown { + let currentVersion = fromVersion; + let currentPayload = payload; + const visited = new Set(); + while (currentVersion !== targetVersion) { + const marker = `${artifactKind}\u0000${currentVersion}`; + if (visited.has(marker)) throw new Error("artifact migration cycle detected"); + visited.add(marker); + const step = this.steps.get(marker); + if (step === undefined) throw new Error(`no migration path from ${artifactKind}/${currentVersion} to ${targetVersion}`); + currentPayload = step.migrate(currentPayload); + currentVersion = step.toContractVersion; + } + return currentPayload; + } +} From 71f6965735b343572adba1f3f8f886e210b139f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:02:04 -0700 Subject: [PATCH 090/101] Export transport envelope and migration APIs --- packages/validation/src/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 4a9dac43..6b18f2c0 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -34,6 +34,17 @@ export { type MeasurementRecordV1, type MeasurementStationV1, } from "./provenance"; +export { + ArtifactMigrationRegistryV1, + createArtifactEnvelope, + parseArtifactEnvelope, + parseArtifactEnvelopeJson, + verifyArtifactEnvelope, + type ArtifactEnvelopeParseResult, + type ArtifactEnvelopeV1, + type ArtifactMigrationStepV1, + type EverythingRingsArtifactKind, +} from "./artifact-envelope"; export { createAtlasRecord, createAtlasSnapshot, From 7f08102ed5cc7a59654b590eb2c766ae03f38d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:02:16 -0700 Subject: [PATCH 091/101] Test artifact envelopes and deterministic migrations --- .../validation/test/artifact-envelope.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/validation/test/artifact-envelope.test.ts diff --git a/packages/validation/test/artifact-envelope.test.ts b/packages/validation/test/artifact-envelope.test.ts new file mode 100644 index 00000000..4f6ecda0 --- /dev/null +++ b/packages/validation/test/artifact-envelope.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { ArtifactMigrationRegistryV1, createArtifactEnvelope, parseArtifactEnvelopeJson, verifyArtifactEnvelope } from "../src"; + +describe("versioned artifact transport", () => { + it("detects payload tampering through content-addressed envelopes", async () => { + const envelope = await createArtifactEnvelope("software-qualification", "qualification-1", { passed: true, checks: ["test", "build"] }, "2026-08-24T17:00:00.000Z"); + expect(await verifyArtifactEnvelope(envelope)).toBe(true); + const parsed = await parseArtifactEnvelopeJson(JSON.stringify(envelope)); + expect(parsed.ok).toBe(true); + const tampered = { ...envelope, payload: { passed: false, checks: ["test", "build"] } }; + expect(await verifyArtifactEnvelope(tampered)).toBe(false); + }); + + it("migrates explicit artifact versions without mutating the original payload", () => { + const registry = new ArtifactMigrationRegistryV1(); + registry.register({ artifactKind: "atlas-record", fromContractVersion: "atlas-0", toContractVersion: "atlas-1", migrate: (payload) => ({ ...(payload as object), migrated: 1 }) }); + registry.register({ artifactKind: "atlas-record", fromContractVersion: "atlas-1", toContractVersion: "atlas-2", migrate: (payload) => ({ ...(payload as object), migrated: 2 }) }); + const original = { specimen: "a" }; + const migrated = registry.migrate("atlas-record", "atlas-0", "atlas-2", original) as { specimen: string; migrated: number }; + expect(migrated).toEqual({ specimen: "a", migrated: 2 }); + expect(original).toEqual({ specimen: "a" }); + expect(() => registry.migrate("measurement", "measurement-0", "measurement-1", {})).toThrow(/no migration path/); + }); +}); From 6f9301cf59b5919c2390ce9d03100181a0df478e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:02:53 -0700 Subject: [PATCH 092/101] Add held-out algorithm promotion governance --- packages/fingerprint/src/promotion.ts | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/fingerprint/src/promotion.ts diff --git a/packages/fingerprint/src/promotion.ts b/packages/fingerprint/src/promotion.ts new file mode 100644 index 00000000..caf1fb8c --- /dev/null +++ b/packages/fingerprint/src/promotion.ts @@ -0,0 +1,87 @@ +export interface SimilarityAlgorithmHeldOutMetricsV1 { + readonly algorithmVersion: string; + readonly evaluationPopulation: string; + readonly specimenDisjoint: boolean; + readonly queryCount: number; + readonly recallAt1: number | null; + readonly meanReciprocalRank: number | null; + readonly rocAuc: number | null; + readonly brierScore: number | null; + readonly coverage: number; +} + +export interface SimilarityPromotionPolicyV1 { + readonly policyVersion: "similarity-promotion-policy-1"; + readonly minimumQueryCount: number; + readonly minimumCoverage: number; + readonly maximumRecallAt1Regression: number; + readonly maximumMrrRegression: number; + readonly maximumAucRegression: number; + readonly maximumBrierIncrease: number; + readonly minimumAnyPrimaryImprovement: number; +} + +export const DEFAULT_SIMILARITY_PROMOTION_POLICY: SimilarityPromotionPolicyV1 = { + policyVersion: "similarity-promotion-policy-1", + minimumQueryCount: 20, + minimumCoverage: 0.5, + maximumRecallAt1Regression: 0, + maximumMrrRegression: 0, + maximumAucRegression: 0, + maximumBrierIncrease: 0, + minimumAnyPrimaryImprovement: 0.01, +}; + +export interface SimilarityPromotionVerdictV1 { + readonly verdictVersion: "similarity-promotion-verdict-1"; + readonly promoted: boolean; + readonly baselineAlgorithmVersion: string; + readonly candidateAlgorithmVersion: string; + readonly evaluationPopulation: string; + readonly reasons: readonly string[]; + readonly improvements: { + readonly recallAt1: number | null; + readonly meanReciprocalRank: number | null; + readonly rocAuc: number | null; + readonly brierScoreReduction: number | null; + }; +} + +function delta(candidate: number | null, baseline: number | null): number | null { + return candidate === null || baseline === null ? null : candidate - baseline; +} + +export function evaluateSimilarityAlgorithmPromotion( + baseline: SimilarityAlgorithmHeldOutMetricsV1, + candidate: SimilarityAlgorithmHeldOutMetricsV1, + policy: SimilarityPromotionPolicyV1 = DEFAULT_SIMILARITY_PROMOTION_POLICY, +): SimilarityPromotionVerdictV1 { + const reasons: string[] = []; + if (baseline.evaluationPopulation !== candidate.evaluationPopulation) reasons.push("baseline and candidate evaluation populations differ"); + if (!baseline.specimenDisjoint || !candidate.specimenDisjoint) reasons.push("promotion requires specimen-disjoint held-out evaluation"); + if (candidate.queryCount < policy.minimumQueryCount) reasons.push(`candidate query count is below ${policy.minimumQueryCount}`); + if (candidate.coverage < policy.minimumCoverage) reasons.push(`candidate coverage is below ${policy.minimumCoverage}`); + + const recall = delta(candidate.recallAt1, baseline.recallAt1); + const mrr = delta(candidate.meanReciprocalRank, baseline.meanReciprocalRank); + const auc = delta(candidate.rocAuc, baseline.rocAuc); + const brierReduction = baseline.brierScore === null || candidate.brierScore === null ? null : baseline.brierScore - candidate.brierScore; + if (recall !== null && recall < -policy.maximumRecallAt1Regression) reasons.push("candidate regresses Recall@1 beyond policy"); + if (mrr !== null && mrr < -policy.maximumMrrRegression) reasons.push("candidate regresses MRR beyond policy"); + if (auc !== null && auc < -policy.maximumAucRegression) reasons.push("candidate regresses ROC AUC beyond policy"); + if (brierReduction !== null && brierReduction < -policy.maximumBrierIncrease) reasons.push("candidate increases Brier score beyond policy"); + + const primaryImprovements = [recall, mrr, auc, brierReduction].filter((value): value is number => value !== null); + if (primaryImprovements.length === 0) reasons.push("no comparable primary held-out metric exists"); + else if (Math.max(...primaryImprovements) < policy.minimumAnyPrimaryImprovement) reasons.push(`candidate does not improve any primary metric by ${policy.minimumAnyPrimaryImprovement}`); + + return { + verdictVersion: "similarity-promotion-verdict-1", + promoted: reasons.length === 0, + baselineAlgorithmVersion: baseline.algorithmVersion, + candidateAlgorithmVersion: candidate.algorithmVersion, + evaluationPopulation: candidate.evaluationPopulation, + reasons, + improvements: { recallAt1: recall, meanReciprocalRank: mrr, rocAuc: auc, brierScoreReduction: brierReduction }, + }; +} From 0c5407b7f5856bc8667837bcef5af5db68de6602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:03:14 -0700 Subject: [PATCH 093/101] Export held-out promotion governance APIs --- packages/fingerprint/src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/fingerprint/src/index.ts b/packages/fingerprint/src/index.ts index b2c57537..d9ad78bb 100644 --- a/packages/fingerprint/src/index.ts +++ b/packages/fingerprint/src/index.ts @@ -91,3 +91,10 @@ export { type MaterialPredictionV1, type MaterialTrainingExampleV1, } from "./material-research"; +export { + DEFAULT_SIMILARITY_PROMOTION_POLICY, + evaluateSimilarityAlgorithmPromotion, + type SimilarityAlgorithmHeldOutMetricsV1, + type SimilarityPromotionPolicyV1, + type SimilarityPromotionVerdictV1, +} from "./promotion"; From e2cd08a7af549b2f1874585170480dd845714f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:03:25 -0700 Subject: [PATCH 094/101] Test held-out algorithm promotion governance --- packages/fingerprint/test/promotion.test.ts | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/fingerprint/test/promotion.test.ts diff --git a/packages/fingerprint/test/promotion.test.ts b/packages/fingerprint/test/promotion.test.ts new file mode 100644 index 00000000..b4bfd152 --- /dev/null +++ b/packages/fingerprint/test/promotion.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { evaluateSimilarityAlgorithmPromotion } from "../src"; + +const baseline = { + algorithmVersion: "baseline-1", + evaluationPopulation: "digital-heldout-1", + specimenDisjoint: true, + queryCount: 100, + recallAt1: 0.80, + meanReciprocalRank: 0.86, + rocAuc: 0.90, + brierScore: 0.16, + coverage: 0.8, +}; + +describe("similarity algorithm promotion governance", () => { + it("promotes only a held-out candidate with a real primary improvement and no prohibited regression", () => { + const result = evaluateSimilarityAlgorithmPromotion(baseline, { + ...baseline, + algorithmVersion: "candidate-2", + recallAt1: 0.84, + meanReciprocalRank: 0.88, + rocAuc: 0.92, + brierScore: 0.14, + }); + expect(result.promoted).toBe(true); + expect(result.improvements.recallAt1).toBeCloseTo(0.04); + }); + + it("rejects evaluation leakage and a candidate that merely changes its version name", () => { + const leakage = evaluateSimilarityAlgorithmPromotion(baseline, { ...baseline, algorithmVersion: "candidate-leaky", specimenDisjoint: false, recallAt1: 0.95 }); + expect(leakage.promoted).toBe(false); + expect(leakage.reasons.some((reason) => reason.includes("specimen-disjoint"))).toBe(true); + + const noImprovement = evaluateSimilarityAlgorithmPromotion(baseline, { ...baseline, algorithmVersion: "candidate-same" }); + expect(noImprovement.promoted).toBe(false); + expect(noImprovement.reasons.some((reason) => reason.includes("does not improve"))).toBe(true); + }); +}); From 552760d95f6c9352c7181172b584094dd237b1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:03:48 -0700 Subject: [PATCH 095/101] Add machine-readable software assurance claims --- packages/validation/src/assurance.ts | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 packages/validation/src/assurance.ts diff --git a/packages/validation/src/assurance.ts b/packages/validation/src/assurance.ts new file mode 100644 index 00000000..44133feb --- /dev/null +++ b/packages/validation/src/assurance.ts @@ -0,0 +1,95 @@ +import { contentDigest } from "./provenance"; + +export type EvidenceDomain = "software" | "digital-twin" | "external-dataset" | "physical" | "human-perceptual" | "playback-transducer"; + +export interface AssuranceEvidenceReferenceV1 { + readonly evidenceId: string; + readonly domain: EvidenceDomain; + readonly description: string; +} + +export interface CapabilityClaimV1 { + readonly schemaVersion: 1; + readonly claimContractVersion: "capability-claim-1"; + readonly claimId: string; + readonly createdAt: string; + readonly capability: string; + readonly proposition: string; + readonly scope: string; + readonly exclusions: readonly string[]; + readonly evidence: readonly AssuranceEvidenceReferenceV1[]; + readonly maturity: "implemented" | "software-qualified" | "empirically-qualified"; +} + +function assertEvidenceId(id: string): void { + if (!/^sha256:[0-9a-f]{64}$/.test(id) && !/^[0-9a-f]{40}$/.test(id) && !/^github-run:\d+$/.test(id)) { + throw new Error("assurance evidenceId must be a SHA-256 digest, Git revision, or github-run:"); + } +} + +export async function createCapabilityClaim( + input: Omit, +): Promise { + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("capability claim createdAt is invalid"); + if (input.capability.trim().length === 0 || input.proposition.trim().length === 0 || input.scope.trim().length === 0) throw new Error("capability claim requires capability, proposition, and scope"); + if (input.evidence.length === 0) throw new Error("capability claim requires evidence references"); + for (const reference of input.evidence) { + assertEvidenceId(reference.evidenceId); + if (reference.description.trim().length === 0) throw new Error("assurance evidence description is required"); + } + if (input.maturity === "software-qualified" && input.evidence.every((reference) => reference.domain !== "software" && reference.domain !== "digital-twin")) { + throw new Error("software-qualified claim requires software or digital-twin evidence"); + } + if (input.maturity === "empirically-qualified" && !input.evidence.some((reference) => reference.domain === "physical" || reference.domain === "human-perceptual" || reference.domain === "playback-transducer")) { + throw new Error("empirically-qualified claim requires empirical-domain evidence"); + } + const payload = { + ...input, + exclusions: [...input.exclusions], + evidence: [...input.evidence], + schemaVersion: 1 as const, + claimContractVersion: "capability-claim-1" as const, + }; + return { ...payload, claimId: await contentDigest(payload) }; +} + +export async function verifyCapabilityClaim(claim: CapabilityClaimV1): Promise { + const { claimId, ...payload } = claim; + return /^sha256:[0-9a-f]{64}$/.test(claimId) && claimId === await contentDigest(payload); +} + +export interface SoftwareQualificationManifestV1 { + readonly schemaVersion: 1; + readonly qualificationContractVersion: "full-vision-software-manifest-1"; + readonly manifestId: string; + readonly sourceRevision: string; + readonly createdAt: string; + readonly testRunIds: readonly string[]; + readonly capabilityClaimIds: readonly string[]; + readonly physicalObjectTested: false; + readonly humanPerceptualValidationPerformed: false; + readonly realPlaybackTransducerValidated: false; + readonly releaseGateEquivalent: false; +} + +export async function createSoftwareQualificationManifest( + input: Omit, +): Promise { + if (!/^[0-9a-f]{40}$/.test(input.sourceRevision)) throw new Error("software qualification sourceRevision must be exact 40-hex Git revision"); + if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("software qualification createdAt is invalid"); + if (input.testRunIds.length === 0) throw new Error("software qualification requires at least one test run"); + input.testRunIds.forEach((id) => { if (!/^github-run:\d+$/.test(id)) throw new Error("testRunIds must use github-run:"); }); + input.capabilityClaimIds.forEach((id) => assertEvidenceId(id)); + const payload = { + ...input, + testRunIds: [...input.testRunIds].sort(), + capabilityClaimIds: [...input.capabilityClaimIds].sort(), + schemaVersion: 1 as const, + qualificationContractVersion: "full-vision-software-manifest-1" as const, + physicalObjectTested: false as const, + humanPerceptualValidationPerformed: false as const, + realPlaybackTransducerValidated: false as const, + releaseGateEquivalent: false as const, + }; + return { ...payload, manifestId: await contentDigest(payload) }; +} From 85e07d83b8884921ebe4929602b488b047836735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:25:39 -0700 Subject: [PATCH 096/101] Harden full-vision integrity and held-out qualification --- .github/workflows/ci.yml | 13 ++- .../full-vision-software-qualification.yml | 25 ++++- apps/web/src/FullVisionSystem.test.ts | 40 ++++---- apps/web/src/TwinLabApp.tsx | 94 ++++++++++--------- docs/FULL_VISION_SOFTWARE.md | 75 +++++++++++++++ packages/fingerprint/src/calibration.ts | 86 +++++++++++++++++ packages/fingerprint/src/index.ts | 5 + packages/fingerprint/src/modal-field.ts | 33 ++++++- .../test/calibration-heldout.test.ts | 37 ++++++++ packages/fingerprint/test/modal-field.test.ts | 6 +- packages/validation/src/assurance.ts | 38 +++++++- packages/validation/src/atlas-network.ts | 25 ++++- packages/validation/src/atlas-registry.ts | 29 +++++- packages/validation/src/index.ts | 14 +++ packages/validation/src/repository.ts | 43 ++++++++- .../validation/src/station-calibration.ts | 22 +++++ packages/validation/test/assurance.test.ts | 56 +++++++++++ .../validation/test/atlas-network.test.ts | 27 +++++- .../validation/test/atlas-registry.test.ts | 23 ++++- packages/validation/test/repository.test.ts | 18 ++++ .../test/station-calibration.test.ts | 70 ++++++++------ 21 files changed, 657 insertions(+), 122 deletions(-) create mode 100644 docs/FULL_VISION_SOFTWARE.md create mode 100644 packages/fingerprint/test/calibration-heldout.test.ts create mode 100644 packages/validation/test/assurance.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adc11cca..93f53832 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,19 +3,24 @@ name: ci on: pull_request: push: - branches: [main] + branches: [main, post-freeze-development] permissions: contents: read +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: validate: runs-on: ubuntu-latest + timeout-minutes: 30 env: - VITE_SOFTWARE_REVISION: ${{ github.sha }} + VITE_SOFTWARE_REVISION: ${{ github.event.pull_request.head.sha || github.sha }} steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 with: node-version: 22 - name: Enable Corepack diff --git a/.github/workflows/full-vision-software-qualification.yml b/.github/workflows/full-vision-software-qualification.yml index 0618a3ca..bc4c286b 100644 --- a/.github/workflows/full-vision-software-qualification.yml +++ b/.github/workflows/full-vision-software-qualification.yml @@ -4,20 +4,28 @@ on: pull_request: branches: - post-freeze-development + push: + branches: + - post-freeze-development workflow_dispatch: permissions: contents: read +concurrency: + group: full-vision-software-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + env: VITE_SOFTWARE_REVISION: ${{ github.event.pull_request.head.sha || github.sha }} jobs: qualify: runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 with: node-version: 22 - name: Enable Corepack @@ -37,8 +45,9 @@ jobs: mkdir -p qualification cat > qualification/full-vision-software.json < { - it("qualifies digital capture through object models, retrieval, calibration, station checks, provenance, Atlas, graph, and playable output", async () => { + it("qualifies digital capture through held-out calibration, station checks, provenance, Atlas, graph, and playable output", async () => { const observations: AcousticObjectObservationV1[] = []; for (const specimen of SPECIMENS) { for (let replicate = 0; replicate < 3; replicate += 1) { @@ -118,21 +116,28 @@ describe("EVERYTHING RINGS full-vision software system", () => { expect(retrieval.recallAt1).toBe(1); expect(retrieval.meanReciprocalRank).toBe(1); - const labeledScores = heldOutQueries.flatMap((query) => index.entries.map((entry) => { + const labeledScores: PartitionedSimilarityScoreV1[] = heldOutQueries.flatMap((query, queryIndex) => index.entries.map((entry) => { const comparison = compareFingerprintToObjectModel(query.fingerprint, entry.model); return { pairId: `${query.queryId}::${entry.specimenId}`, + groupId: query.trueSpecimenId, + partition: queryIndex < 2 ? "calibration" as const : "evaluation" as const, score: comparison.evidenceScore, samePhysicalSpecimen: query.trueSpecimenId === entry.specimenId, }; })); - const calibration = fitIsotonicCalibration(labeledScores, "full-vision-digital-twin-system-1"); - const predictions = calibratedPredictions(calibration, labeledScores); - const calibrationMetrics = evaluateCalibration(predictions, 5); - expect(calibrationMetrics.rocAuc).not.toBeNull(); - expect(calibrationMetrics.rocAuc!).toBeGreaterThan(0.9); - const risk = riskCoverageCurve(predictions, [0, 0.2, 0.4]); - expect(risk[0]?.coverage).toBe(1); + const calibration = fitAndEvaluateHeldOutCalibration( + labeledScores, + "full-vision-digital-calibration-population-1", + "full-vision-digital-held-out-population-1", + 5, + [0, 0.2, 0.4], + ); + expect(calibration.calibrationGroups).toHaveLength(2); + expect(calibration.evaluationGroups).toHaveLength(1); + expect(calibration.metrics.rocAuc).not.toBeNull(); + expect(calibration.metrics.rocAuc!).toBeGreaterThan(0.9); + expect(calibration.riskCoverage[0]?.coverage).toBe(1); const referenceModel = buildAcousticObjectModel(observations.filter((observation) => observation.specimenId === SPECIMENS[0]!.id)); const stationProtocol = await createStationCalibrationProtocol({ @@ -144,7 +149,7 @@ describe("EVERYTHING RINGS full-vision software system", () => { maximumMedianFrequencyDistanceCents: 45, minimumMatchedModes: 3, }); - const stationVerdict = evaluateStationCalibration(stationProtocol, { + const stationVerdict = await evaluateVerifiedStationCalibration(stationProtocol, { stationId: "digital-station-system-test", createdAt: "2026-08-24T16:01:00.000Z", specimenId: SPECIMENS[0]!.id, @@ -174,7 +179,7 @@ describe("EVERYTHING RINGS full-vision software system", () => { measurementId: measurement.measurementId, kind: "similarity", algorithmVersion: "acoustic-object-model-1+sonic-twin-index-1", - configDigest: await contentDigest({ stationProtocol: stationProtocol.protocolId }), + configDigest: await contentDigest({ stationProtocol: stationProtocol.protocolId, calibration: calibration.model.trainingPopulation }), artifactDigest: await contentDigest({ ranking: index.entries.map((entry) => compareFingerprintToObjectModel(query.fingerprint, entry.model)) }), createdAt: `2026-08-24T16:2${indexValue}:00.000Z`, }); @@ -203,9 +208,10 @@ describe("EVERYTHING RINGS full-vision software system", () => { description: "Software qualification collection", atlasRecordIds, }); - const graph = buildAtlasGraph(repository.atlas, [collection]); + const graph = await buildAtlasGraph(repository.atlas, [collection]); expect(graph.nodes.some((node) => node.kind === "collection")).toBe(true); expect(graph.edges.filter((edge) => edge.kind === "member-of")).toHaveLength(3); + expect(graph.edges.filter((edge) => edge.kind === "created-by")).toHaveLength(1); const snapshot = await snapshotAtlasRegistry(repository.atlas, "2026-08-24T16:41:00.000Z"); expect(snapshot.recordIds).toHaveLength(3); }); diff --git a/apps/web/src/TwinLabApp.tsx b/apps/web/src/TwinLabApp.tsx index d3924904..03b81a63 100644 --- a/apps/web/src/TwinLabApp.tsx +++ b/apps/web/src/TwinLabApp.tsx @@ -1,18 +1,15 @@ import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; import { benchmarkSonicTwinRetrieval, - buildAcousticObjectModel, buildSonicTwinIndex, buildTwinVerificationPairs, - calibratedPredictions, - evaluateCalibration, - fitIsotonicCalibration, + compareFingerprintToObjectModel, + fitAndEvaluateHeldOutCalibration, retrieveSonicTwin, - riskCoverageCurve, validateTwinBenchmarkCorpus, type AcousticObjectObservationV1, - type LabeledSimilarityScoreV1, type NuisanceMetadataV1, + type PartitionedSimilarityScoreV1, type TwinBenchmarkCorpusV1, } from "@everything-rings/fingerprint"; import { useMemo } from "react"; @@ -45,24 +42,35 @@ const nuisance: NuisanceMetadataV1 = { dayId: "digital-day-1", }; -const OBSERVATIONS: readonly AcousticObjectObservationV1[] = [ - { observationId: "bell-a-1", specimenId: "digital-bell-a", fingerprint: fp([440, 880, 1320, 1760]) }, - { observationId: "bell-a-2", specimenId: "digital-bell-a", fingerprint: fp([441, 882, 1322, 1763]) }, - { observationId: "bell-b-1", specimenId: "digital-bell-b", fingerprint: fp([470, 940, 1410, 1880]) }, - { observationId: "bell-b-2", specimenId: "digital-bell-b", fingerprint: fp([471, 942, 1412, 1883]) }, - { observationId: "plate-c-1", specimenId: "digital-plate-c", fingerprint: fp([620, 1010, 1670, 2510]) }, - { observationId: "plate-c-2", specimenId: "digital-plate-c", fingerprint: fp([621, 1012, 1672, 2512]) }, -]; +const SPECIMENS = [ + { id: "digital-bell-a", frequencies: [440, 880, 1320, 1760] }, + { id: "digital-bell-b", frequencies: [470, 940, 1410, 1880] }, + { id: "digital-plate-c", frequencies: [620, 1010, 1670, 2510] }, + { id: "digital-bell-d", frequencies: [523, 1046, 1570, 2093] }, + { id: "digital-plate-e", frequencies: [710, 1190, 1860, 2740] }, + { id: "digital-bowl-f", frequencies: [355, 760, 1285, 2010] }, +] as const; + +const OBSERVATIONS: readonly AcousticObjectObservationV1[] = SPECIMENS.flatMap((specimen) => [ + { observationId: `${specimen.id}-1`, specimenId: specimen.id, fingerprint: fp(specimen.frequencies) }, + { observationId: `${specimen.id}-2`, specimenId: specimen.id, fingerprint: fp(specimen.frequencies.map((frequency, index) => frequency * (1 + 0.0015 + index * 0.00015))) }, +]); + +function objectFamily(specimenId: string): string { + if (specimenId.includes("plate")) return "plate"; + if (specimenId.includes("bowl")) return "bowl"; + return "bell"; +} function corpus(): TwinBenchmarkCorpusV1 { return { schemaVersion: 1, corpusContractVersion: "sonic-twin-benchmark-corpus-1", - corpusId: "built-in-digital-twin-demo-1", + corpusId: "built-in-digital-twin-demo-2", createdAt: "2026-08-24T15:30:00.000Z", observations: OBSERVATIONS.map((observation, index) => ({ ...observation, - objectFamily: observation.specimenId.includes("plate") ? "plate" : "bell", + objectFamily: objectFamily(observation.specimenId), source: "digital-twin", nuisance: { ...nuisance, strikeLocation: index % 2 === 0 ? "digital-A" : "digital-B" }, })), @@ -74,33 +82,33 @@ export function TwinLabApp() { const benchmarkCorpus = corpus(); const validation = validateTwinBenchmarkCorpus(benchmarkCorpus); const index = buildSonicTwinIndex(OBSERVATIONS); - const query = fp([440.6, 881, 1321, 1761]); - const ranking = retrieveSonicTwin(query, index); - const retrievalMetrics = benchmarkSonicTwinRetrieval([ - { queryId: "query-a", trueSpecimenId: "digital-bell-a", fingerprint: query }, - { queryId: "query-b", trueSpecimenId: "digital-bell-b", fingerprint: fp([470.5, 941, 1411, 1881]) }, - { queryId: "query-c", trueSpecimenId: "digital-plate-c", fingerprint: fp([620.5, 1011, 1671, 2511]) }, - ], index); - const pairs = buildTwinVerificationPairs(benchmarkCorpus, 30); - const scored: LabeledSimilarityScoreV1[] = pairs.map((pair) => { - const model = buildAcousticObjectModel(OBSERVATIONS.filter((observation) => observation.specimenId === pair.leftSpecimenId)); - const candidateRanking = retrieveSonicTwin(pair.candidate, { schemaVersion: 1, indexVersion: "sonic-twin-index-1", entries: [{ specimenId: model.specimenId, model }] }); - return { pairId: pair.pairId, score: candidateRanking[0]?.evidenceScore ?? 0, samePhysicalSpecimen: pair.samePhysicalSpecimen }; - }); - const model = fitIsotonicCalibration(scored.length > 0 ? scored : [ - { pairId: "fallback-n", score: 0, samePhysicalSpecimen: false }, - { pairId: "fallback-p", score: 1, samePhysicalSpecimen: true }, - ], "built-in-digital-twin-demo-1"); - const predictions = calibratedPredictions(model, scored); - const calibration = evaluateCalibration(predictions); - const riskCoverage = riskCoverageCurve(predictions); - return { validation, index, ranking, retrievalMetrics, pairs, calibration, riskCoverage }; + const queries = SPECIMENS.map((specimen, indexValue) => ({ + queryId: `query-${specimen.id}`, + trueSpecimenId: specimen.id, + fingerprint: fp(specimen.frequencies.map((frequency, modeIndex) => frequency * (1 + 0.0007 + indexValue * 0.00005 + modeIndex * 0.00003))), + })); + const ranking = retrieveSonicTwin(queries[0]!.fingerprint, index); + const retrievalMetrics = benchmarkSonicTwinRetrieval(queries, index); + const pairs = buildTwinVerificationPairs(benchmarkCorpus, 60); + const scores: PartitionedSimilarityScoreV1[] = queries.flatMap((query, queryIndex) => index.entries.map((entry) => ({ + pairId: `${query.queryId}::${entry.specimenId}`, + groupId: query.trueSpecimenId, + partition: queryIndex < 3 ? "calibration" as const : "evaluation" as const, + score: compareFingerprintToObjectModel(query.fingerprint, entry.model).evidenceScore, + samePhysicalSpecimen: query.trueSpecimenId === entry.specimenId, + }))); + const heldOutCalibration = fitAndEvaluateHeldOutCalibration( + scores, + "built-in-digital-calibration-population-2", + "built-in-digital-held-out-population-2", + ); + return { validation, index, ranking, retrievalMetrics, pairs, heldOutCalibration }; }, []); return

EVERYTHING RINGS / SONIC TWIN LAB

-

Object models, retrieval, calibration, abstention

+

Object models, retrieval, held-out calibration, abstention

A local software-only qualification surface. The built-in corpus is synthetic and is not physical identity evidence.

@@ -116,9 +124,9 @@ export function TwinLabApp() {

MRR {report.retrievalMetrics.meanReciprocalRank?.toFixed(3) ?? "—"}

-

CALIBRATION / DIGITAL POPULATION

-

Brier {report.calibration.brierScore?.toFixed(3) ?? "—"}

-

AUC {report.calibration.rocAuc?.toFixed(3) ?? "—"}

+

HELD-OUT CALIBRATION / DIGITAL POPULATION

+

Brier {report.heldOutCalibration.metrics.brierScore?.toFixed(3) ?? "—"}

+

AUC {report.heldOutCalibration.metrics.rocAuc?.toFixed(3) ?? "—"} · {report.heldOutCalibration.evaluationGroups.length} held-out groups

@@ -134,8 +142,8 @@ export function TwinLabApp() {

Verification pairs

{report.pairs.length} deterministic same-specimen / hard-negative pairs.

-

Risk / coverage

{report.riskCoverage.map((point) => `${point.minimumConfidence.toFixed(2)}→${point.coverage.toFixed(2)}`).join(" · ")}

-

Boundary

No built-in result is a calibrated claim about physical objects. Calibration is bound to the named digital population.

+

Risk / coverage

{report.heldOutCalibration.riskCoverage.map((point) => `${point.minimumConfidence.toFixed(2)}→${point.coverage.toFixed(2)}`).join(" · ")}

+

Boundary

No built-in result is a calibrated claim about physical objects. Calibration fitting and evaluation use disjoint digital query groups and remain bound to the named digital populations.

; } diff --git a/docs/FULL_VISION_SOFTWARE.md b/docs/FULL_VISION_SOFTWARE.md new file mode 100644 index 00000000..8730fda6 --- /dev/null +++ b/docs/FULL_VISION_SOFTWARE.md @@ -0,0 +1,75 @@ +# Full-Vision Software Architecture + +This document defines the software-qualified architecture for the long-term EVERYTHING RINGS system. It does not redefine the empirical release contracts on `main`, and it does not treat digital twins as physical evidence. + +## System model + +The mature data model is deliberately observation-centric: + +`specimen -> interaction -> capture -> immutable measurement -> derivation -> acoustic object model -> retrieval / spatial model -> Atlas publication` + +A capture is one observation. A fingerprint is one deterministic analysis of one observation. Neither is a canonical physical-object identifier. + +## Measurement and derivation boundary + +`MeasurementRecordV1` content-addresses acquisition contract, exact software revision, specimen/session/attempt identity, station metadata, setup metadata, fingerprint algorithm, and fingerprint. Raw microphone samples are excluded. + +Renderer, instrument, similarity, embedding, visualization, and Atlas products are separate `DerivationRecordV1` artifacts rooted in the immutable measurement. Updating a downstream model does not rewrite measurement identity. + +The research repository verifies every measurement, derivation, Atlas record, and specimen-merge content address before treating the repository as integrity-valid. + +## Sonic Twin + +The deterministic reference path provides: + +- repeated-observation `AcousticObjectModelV1` estimates; +- nuisance characterization for strike, support, room, station, operator, and day; +- specimen-disjoint benchmark splitting and hard-negative construction; +- match / nonmatch / abstain verification; +- specimen-level ranked retrieval with Recall@K and MRR; +- group-disjoint calibration fitting and held-out calibration evaluation; +- Brier score, calibration error, ROC AUC, and risk-coverage reporting; +- promotion policy that requires held-out improvement without prohibited regressions. + +Calibrated probabilities are scoped to their named calibration population. They are not universal physical-object probabilities. + +## Spatial acoustic model + +`SpatialModalSoundFieldV1` separates relatively stable modal frequency/decay estimates from strike-location-dependent excitation amplitudes. `fingerprintAtSpatialPoint()` returns a `SpatialPredictedFingerprintV1` that is explicitly `evidenceEligible: false` and carries source algorithm provenance instead of stamping a canonical measurement algorithm version. + +## Research-only estimators and inference + +The high-resolution damped-mode estimator and material-inference baseline are research paths. They are not release fingerprint algorithms and do not change the current scientific claim. Promotion requires separate held-out evidence and an explicit version transition. + +## Station qualification + +Station calibration protocols are content-addressed against a frozen reference object model. Authoritative evaluation first verifies the protocol digest, then evaluates mode coverage, matched-mode support, and modal drift. + +## Resonance Atlas + +Atlas publication is privacy-preserving and content-addressed. The software model includes: + +- immutable Atlas records and snapshots; +- append-only record publication; +- measurement-to-specimen ownership checks; +- explicit, evidence-backed and content-verified specimen merges; +- content-verified collections; +- search and deterministic service conformance; +- contributor, specimen, record, and collection graph projection; +- explicit edges only; no inferred same-object edge is created by graph projection. + +A future network/database adapter must satisfy the same service and artifact contracts rather than redefining them. + +## Assurance boundary + +Software qualification may support propositions about deterministic execution, artifact integrity, synthetic acoustic recovery, retrieval on named digital populations, browser flows, and local service semantics. + +Software qualification does not by itself support propositions about physical-object identity, material identity in the world, human perceptual fidelity, microphone-transducer behavior, or real playback-chain behavior. + +Machine-readable capability claims bind one proposition to its scope, exclusions, evidence references, and maturity. Software qualification manifests are content-addressed, require at least one explicit capability claim, and retain the non-empirical boundary flags. + +## Software qualification + +The full-vision workflow uses frozen dependency installation, repository-wide typechecking, all unit and digital-twin tests, complete builds, browser/lifecycle qualification, and a machine-readable qualification artifact. Workflow actions are pinned to immutable revisions and concurrent superseded runs are cancelled for the same branch or pull request. + +A full-vision software baseline is promotable only when one exact revision passes the complete qualification workflow. The subsequently merged product/research revision must itself be qualified before a named software freeze is created. diff --git a/packages/fingerprint/src/calibration.ts b/packages/fingerprint/src/calibration.ts index 31e05cf7..7fd58f3c 100644 --- a/packages/fingerprint/src/calibration.ts +++ b/packages/fingerprint/src/calibration.ts @@ -37,6 +37,11 @@ function blockProbability(block: MutableBlock): number { return block.positiveCount / block.sampleCount; } +function assertBinarySupport(samples: readonly LabeledSimilarityScoreV1[], label: string): void { + if (!samples.some((sample) => sample.samePhysicalSpecimen)) throw new Error(`${label} requires at least one positive example`); + if (!samples.some((sample) => !sample.samePhysicalSpecimen)) throw new Error(`${label} requires at least one negative example`); +} + export function fitIsotonicCalibration( samples: readonly LabeledSimilarityScoreV1[], trainingPopulation: string, @@ -207,3 +212,84 @@ export function riskCoverageCurve( }; }); } + +export type CalibrationPartition = "calibration" | "evaluation"; + +export interface PartitionedSimilarityScoreV1 extends LabeledSimilarityScoreV1 { + readonly groupId: string; + readonly partition: CalibrationPartition; +} + +export interface HeldOutCalibrationEvaluationV1 { + readonly schemaVersion: 1; + readonly evaluationVersion: "sonic-twin-held-out-calibration-1"; + readonly calibrationPopulation: string; + readonly evaluationPopulation: string; + readonly calibrationGroups: readonly string[]; + readonly evaluationGroups: readonly string[]; + readonly calibrationSampleCount: number; + readonly evaluationSampleCount: number; + readonly model: SonicTwinCalibrationModelV1; + readonly predictions: readonly CalibratedPredictionV1[]; + readonly metrics: CalibrationMetricsV1; + readonly riskCoverage: readonly RiskCoveragePointV1[]; +} + +/** + * Fits probability calibration only on the calibration partition and reports + * metrics only on group-disjoint evaluation samples. A group is normally a + * physical specimen or held-out query identity; it may not cross partitions. + */ +export function fitAndEvaluateHeldOutCalibration( + samples: readonly PartitionedSimilarityScoreV1[], + calibrationPopulation: string, + evaluationPopulation: string, + binCount = 10, + riskThresholds: readonly number[] = [0, 0.1, 0.2, 0.3, 0.4, 0.49], +): HeldOutCalibrationEvaluationV1 { + if (calibrationPopulation.trim().length === 0 || evaluationPopulation.trim().length === 0) { + throw new Error("held-out calibration requires named calibration and evaluation populations"); + } + const seenPairIds = new Set(); + const calibrationGroups = new Set(); + const evaluationGroups = new Set(); + for (const sample of samples) { + assertScore(sample.score); + if (sample.pairId.trim().length === 0 || sample.groupId.trim().length === 0) throw new Error("held-out calibration requires pairId and groupId"); + if (seenPairIds.has(sample.pairId)) throw new Error(`duplicate held-out calibration pairId ${sample.pairId}`); + seenPairIds.add(sample.pairId); + const group = sample.groupId.trim().toLocaleLowerCase("en-US"); + (sample.partition === "calibration" ? calibrationGroups : evaluationGroups).add(group); + } + for (const group of calibrationGroups) { + if (evaluationGroups.has(group)) throw new Error(`calibration leakage: group ${group} appears in both partitions`); + } + + const calibrationSamples = samples.filter((sample) => sample.partition === "calibration"); + const evaluationSamples = samples.filter((sample) => sample.partition === "evaluation"); + if (calibrationSamples.length === 0 || evaluationSamples.length === 0) throw new Error("held-out calibration requires non-empty calibration and evaluation partitions"); + assertBinarySupport(calibrationSamples, "calibration partition"); + assertBinarySupport(evaluationSamples, "evaluation partition"); + + const strip = (sample: PartitionedSimilarityScoreV1): LabeledSimilarityScoreV1 => ({ + pairId: sample.pairId, + score: sample.score, + samePhysicalSpecimen: sample.samePhysicalSpecimen, + }); + const model = fitIsotonicCalibration(calibrationSamples.map(strip), calibrationPopulation); + const predictions = calibratedPredictions(model, evaluationSamples.map(strip)); + return { + schemaVersion: 1, + evaluationVersion: "sonic-twin-held-out-calibration-1", + calibrationPopulation: calibrationPopulation.trim(), + evaluationPopulation: evaluationPopulation.trim(), + calibrationGroups: [...calibrationGroups].sort(), + evaluationGroups: [...evaluationGroups].sort(), + calibrationSampleCount: calibrationSamples.length, + evaluationSampleCount: evaluationSamples.length, + model, + predictions, + metrics: evaluateCalibration(predictions, binCount), + riskCoverage: riskCoverageCurve(predictions, riskThresholds), + }; +} diff --git a/packages/fingerprint/src/index.ts b/packages/fingerprint/src/index.ts index d9ad78bb..6eccefc6 100644 --- a/packages/fingerprint/src/index.ts +++ b/packages/fingerprint/src/index.ts @@ -48,6 +48,7 @@ export { type SpatialModalSampleV1, type SpatialModalSoundFieldV1, type SpatialPointV1, + type SpatialPredictedFingerprintV1, } from "./modal-field"; export { benchmarkSonicTwinRetrieval, @@ -70,13 +71,17 @@ export { export { calibratedPredictions, evaluateCalibration, + fitAndEvaluateHeldOutCalibration, fitIsotonicCalibration, predictCalibratedProbability, riskCoverageCurve, type CalibratedPredictionV1, type CalibrationMetricsV1, + type CalibrationPartition, + type HeldOutCalibrationEvaluationV1, type IsotonicCalibrationBinV1, type LabeledSimilarityScoreV1, + type PartitionedSimilarityScoreV1, type RiskCoveragePointV1, type SonicTwinCalibrationModelV1, } from "./calibration"; diff --git a/packages/fingerprint/src/modal-field.ts b/packages/fingerprint/src/modal-field.ts index 07bc22fb..5534599c 100644 --- a/packages/fingerprint/src/modal-field.ts +++ b/packages/fingerprint/src/modal-field.ts @@ -1,4 +1,4 @@ -import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import type { AcousticFingerprintV1, AcousticMode, AcousticFingerprintAlgorithmVersion } from "@everything-rings/dsp"; import { centsDistance } from "./recurrence"; import { buildAcousticObjectModel, type AcousticObjectModelV1 } from "./object-model"; @@ -120,10 +120,27 @@ export interface SpatialModalFieldQueryV1 { readonly durationSeconds?: number; } +/** + * A spatial-field prediction is a derived model output, not a fresh acoustic + * measurement. It deliberately cannot be mistaken for an evidence-eligible + * AcousticFingerprintV1 because it carries no canonical algorithmVersion. + */ +export interface SpatialPredictedFingerprintV1 { + readonly schemaVersion: 1; + readonly predictionContractVersion: "spatial-predicted-fingerprint-1"; + readonly evidenceEligible: false; + readonly sourceFingerprintAlgorithmVersions: readonly AcousticFingerprintAlgorithmVersion[]; + readonly specimenId: string; + readonly sampleRate: number; + readonly durationSeconds: number; + readonly strikePoint: SpatialPointV1; + readonly modes: readonly AcousticMode[]; +} + export function fingerprintAtSpatialPoint( field: SpatialModalSoundFieldV1, query: SpatialModalFieldQueryV1, -): AcousticFingerprintV1 { +): SpatialPredictedFingerprintV1 { assertPoint(query.point); const interpolationPower = query.interpolationPower ?? 2; if (!(interpolationPower > 0) || !Number.isFinite(interpolationPower)) throw new Error("interpolationPower must be finite and positive"); @@ -131,12 +148,18 @@ export function fingerprintAtSpatialPoint( const maximumAmplitude = Math.max(0, ...amplitudes); const sampleRate = query.sampleRate ?? field.objectModel.sampleRates[0] ?? 48_000; const durationSeconds = query.durationSeconds ?? 2; + if (!(sampleRate > 0) || !Number.isFinite(sampleRate)) throw new Error("spatial predicted sampleRate must be finite and positive"); + if (!(durationSeconds > 0) || !Number.isFinite(durationSeconds)) throw new Error("spatial predicted durationSeconds must be finite and positive"); return { - version: 1, - algorithmVersion: field.objectModel.fingerprintAlgorithmVersions[0] as AcousticFingerprintV1["algorithmVersion"], + schemaVersion: 1, + predictionContractVersion: "spatial-predicted-fingerprint-1", + evidenceEligible: false, + sourceFingerprintAlgorithmVersions: [...field.objectModel.fingerprintAlgorithmVersions], + specimenId: field.specimenId, sampleRate, durationSeconds, - modes: field.modes.flatMap((mode, index) => { + strikePoint: { ...query.point }, + modes: field.modes.flatMap((mode, index): AcousticMode[] => { const amplitude = amplitudes[index] ?? 0; if (!(amplitude > 0) || !(maximumAmplitude > 0)) return []; const relativeAmplitude = amplitude / maximumAmplitude; diff --git a/packages/fingerprint/test/calibration-heldout.test.ts b/packages/fingerprint/test/calibration-heldout.test.ts new file mode 100644 index 00000000..f08b95d8 --- /dev/null +++ b/packages/fingerprint/test/calibration-heldout.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { fitAndEvaluateHeldOutCalibration, type PartitionedSimilarityScoreV1 } from "../src"; + +const samples: readonly PartitionedSimilarityScoreV1[] = [ + { pairId: "cal-a-pos", groupId: "cal-a", partition: "calibration", score: 0.92, samePhysicalSpecimen: true }, + { pairId: "cal-a-neg", groupId: "cal-a", partition: "calibration", score: 0.18, samePhysicalSpecimen: false }, + { pairId: "cal-b-pos", groupId: "cal-b", partition: "calibration", score: 0.84, samePhysicalSpecimen: true }, + { pairId: "cal-b-neg", groupId: "cal-b", partition: "calibration", score: 0.25, samePhysicalSpecimen: false }, + { pairId: "eval-c-pos", groupId: "eval-c", partition: "evaluation", score: 0.88, samePhysicalSpecimen: true }, + { pairId: "eval-c-neg", groupId: "eval-c", partition: "evaluation", score: 0.12, samePhysicalSpecimen: false }, + { pairId: "eval-d-pos", groupId: "eval-d", partition: "evaluation", score: 0.79, samePhysicalSpecimen: true }, + { pairId: "eval-d-neg", groupId: "eval-d", partition: "evaluation", score: 0.30, samePhysicalSpecimen: false }, +]; + +describe("held-out Sonic Twin calibration", () => { + it("fits only on calibration groups and reports only group-disjoint evaluation metrics", () => { + const report = fitAndEvaluateHeldOutCalibration(samples, "digital-calibration-population", "digital-evaluation-population", 4, [0, 0.25]); + expect(report.calibrationSampleCount).toBe(4); + expect(report.evaluationSampleCount).toBe(4); + expect(report.model.trainingSampleCount).toBe(4); + expect(report.predictions).toHaveLength(4); + expect(report.metrics.sampleCount).toBe(4); + expect(report.metrics.rocAuc).toBe(1); + expect(new Set(report.calibrationGroups)).toEqual(new Set(["cal-a", "cal-b"])); + expect(new Set(report.evaluationGroups)).toEqual(new Set(["eval-c", "eval-d"])); + }); + + it("rejects group leakage across calibration and evaluation partitions", () => { + const leaking = [...samples, { pairId: "leak", groupId: "cal-a", partition: "evaluation" as const, score: 0.5, samePhysicalSpecimen: false }]; + expect(() => fitAndEvaluateHeldOutCalibration(leaking, "cal", "eval")).toThrow(/leakage/); + }); + + it("rejects evaluation sets that cannot measure both classes", () => { + const oneClass = samples.filter((sample) => sample.partition === "calibration" || sample.samePhysicalSpecimen); + expect(() => fitAndEvaluateHeldOutCalibration(oneClass, "cal", "eval")).toThrow(/negative/); + }); +}); diff --git a/packages/fingerprint/test/modal-field.test.ts b/packages/fingerprint/test/modal-field.test.ts index 3eba06a7..6375bede 100644 --- a/packages/fingerprint/test/modal-field.test.ts +++ b/packages/fingerprint/test/modal-field.test.ts @@ -21,7 +21,7 @@ function fp(amplitudes: readonly number[]): AcousticFingerprintV1 { } describe("spatial modal sound field", () => { - it("preserves global modal frequencies while spatially interpolating excitation amplitudes", () => { + it("preserves modal structure while keeping spatial predictions outside measurement evidence", () => { const field = buildSpatialModalSoundField([ { observationId: "left", specimenId: "specimen-field", strikePoint: { x: 0, y: 0, z: 0 }, fingerprint: fp([1, 0.2, 0.1]) }, { observationId: "right", specimenId: "specimen-field", strikePoint: { x: 1, y: 0, z: 0 }, fingerprint: fp([0.1, 1, 0.4]) }, @@ -33,6 +33,10 @@ describe("spatial modal sound field", () => { const left = fingerprintAtSpatialPoint(field, { point: { x: 0, y: 0, z: 0 } }); const right = fingerprintAtSpatialPoint(field, { point: { x: 1, y: 0, z: 0 } }); const center = fingerprintAtSpatialPoint(field, { point: { x: 0.5, y: 0.4, z: 0 } }); + expect(left.evidenceEligible).toBe(false); + expect(left.predictionContractVersion).toBe("spatial-predicted-fingerprint-1"); + expect(left.sourceFingerprintAlgorithmVersions).toContain("er-dsp-2"); + expect("algorithmVersion" in left).toBe(false); expect(left.modes[0]!.relativeAmplitude).toBe(1); expect(right.modes[1]!.relativeAmplitude).toBe(1); expect(center.modes.every((mode) => Number.isFinite(mode.relativeAmplitude))).toBe(true); diff --git a/packages/validation/src/assurance.ts b/packages/validation/src/assurance.ts index 44133feb..59ac15e6 100644 --- a/packages/validation/src/assurance.ts +++ b/packages/validation/src/assurance.ts @@ -21,8 +21,12 @@ export interface CapabilityClaimV1 { readonly maturity: "implemented" | "software-qualified" | "empirically-qualified"; } +function isContentDigest(id: string): boolean { + return /^sha256:[0-9a-f]{64}$/.test(id); +} + function assertEvidenceId(id: string): void { - if (!/^sha256:[0-9a-f]{64}$/.test(id) && !/^[0-9a-f]{40}$/.test(id) && !/^github-run:\d+$/.test(id)) { + if (!isContentDigest(id) && !/^[0-9a-f]{40}$/.test(id) && !/^github-run:\d+$/.test(id)) { throw new Error("assurance evidenceId must be a SHA-256 digest, Git revision, or github-run:"); } } @@ -54,8 +58,15 @@ export async function createCapabilityClaim( } export async function verifyCapabilityClaim(claim: CapabilityClaimV1): Promise { + if (claim.schemaVersion !== 1 || claim.claimContractVersion !== "capability-claim-1") return false; + if (!isContentDigest(claim.claimId) || !Number.isFinite(Date.parse(claim.createdAt))) return false; + if (claim.capability.trim().length === 0 || claim.proposition.trim().length === 0 || claim.scope.trim().length === 0 || claim.evidence.length === 0) return false; + if (!claim.evidence.every((reference) => { + try { assertEvidenceId(reference.evidenceId); } catch { return false; } + return reference.description.trim().length > 0; + })) return false; const { claimId, ...payload } = claim; - return /^sha256:[0-9a-f]{64}$/.test(claimId) && claimId === await contentDigest(payload); + return claimId === await contentDigest(payload); } export interface SoftwareQualificationManifestV1 { @@ -78,12 +89,17 @@ export async function createSoftwareQualificationManifest( if (!/^[0-9a-f]{40}$/.test(input.sourceRevision)) throw new Error("software qualification sourceRevision must be exact 40-hex Git revision"); if (!Number.isFinite(Date.parse(input.createdAt))) throw new Error("software qualification createdAt is invalid"); if (input.testRunIds.length === 0) throw new Error("software qualification requires at least one test run"); + if (input.capabilityClaimIds.length === 0) throw new Error("software qualification requires at least one capability claim"); input.testRunIds.forEach((id) => { if (!/^github-run:\d+$/.test(id)) throw new Error("testRunIds must use github-run:"); }); - input.capabilityClaimIds.forEach((id) => assertEvidenceId(id)); + input.capabilityClaimIds.forEach((id) => { if (!isContentDigest(id)) throw new Error("capabilityClaimIds must be SHA-256 claim digests"); }); + const testRunIds = [...new Set(input.testRunIds)].sort(); + const capabilityClaimIds = [...new Set(input.capabilityClaimIds)].sort(); + if (testRunIds.length !== input.testRunIds.length) throw new Error("software qualification testRunIds must be unique"); + if (capabilityClaimIds.length !== input.capabilityClaimIds.length) throw new Error("software qualification capabilityClaimIds must be unique"); const payload = { ...input, - testRunIds: [...input.testRunIds].sort(), - capabilityClaimIds: [...input.capabilityClaimIds].sort(), + testRunIds, + capabilityClaimIds, schemaVersion: 1 as const, qualificationContractVersion: "full-vision-software-manifest-1" as const, physicalObjectTested: false as const, @@ -93,3 +109,15 @@ export async function createSoftwareQualificationManifest( }; return { ...payload, manifestId: await contentDigest(payload) }; } + +export async function verifySoftwareQualificationManifest(manifest: SoftwareQualificationManifestV1): Promise { + if (manifest.schemaVersion !== 1 || manifest.qualificationContractVersion !== "full-vision-software-manifest-1") return false; + if (!isContentDigest(manifest.manifestId) || !/^[0-9a-f]{40}$/.test(manifest.sourceRevision)) return false; + if (!Number.isFinite(Date.parse(manifest.createdAt)) || manifest.testRunIds.length === 0 || manifest.capabilityClaimIds.length === 0) return false; + if (!manifest.testRunIds.every((id) => /^github-run:\d+$/.test(id))) return false; + if (!manifest.capabilityClaimIds.every(isContentDigest)) return false; + if (new Set(manifest.testRunIds).size !== manifest.testRunIds.length || new Set(manifest.capabilityClaimIds).size !== manifest.capabilityClaimIds.length) return false; + if (manifest.physicalObjectTested !== false || manifest.humanPerceptualValidationPerformed !== false || manifest.realPlaybackTransducerValidated !== false || manifest.releaseGateEquivalent !== false) return false; + const { manifestId, ...payload } = manifest; + return manifestId === await contentDigest(payload); +} diff --git a/packages/validation/src/atlas-network.ts b/packages/validation/src/atlas-network.ts index 9d9509fb..ae3450d4 100644 --- a/packages/validation/src/atlas-network.ts +++ b/packages/validation/src/atlas-network.ts @@ -1,4 +1,4 @@ -import type { AtlasRegistryStateV1 } from "./atlas-registry"; +import { verifyAtlasSpecimenMerge, type AtlasRegistryStateV1 } from "./atlas-registry"; import { contentDigest } from "./provenance"; export interface AtlasCollectionV1 { @@ -36,8 +36,19 @@ export async function createAtlasCollection( return { ...payload, collectionId: await contentDigest(payload) }; } +export async function verifyAtlasCollection(collection: AtlasCollectionV1): Promise { + if (collection.schemaVersion !== 1 || collection.collectionContractVersion !== "atlas-collection-1" || collection.visibility !== "public") return false; + if (!/^sha256:[0-9a-f]{64}$/.test(collection.collectionId)) return false; + if (!Number.isFinite(Date.parse(collection.createdAt))) return false; + if (collection.contributorId.trim().length === 0 || collection.title.trim().length === 0 || collection.atlasRecordIds.length === 0) return false; + if (new Set(collection.atlasRecordIds).size !== collection.atlasRecordIds.length) return false; + if (!collection.atlasRecordIds.every((id) => /^sha256:[0-9a-f]{64}$/.test(id))) return false; + const { collectionId, ...payload } = collection; + return collectionId === await contentDigest(payload); +} + export type AtlasGraphNodeKind = "record" | "specimen" | "contributor" | "collection"; -export type AtlasGraphEdgeKind = "describes" | "published-by" | "member-of" | "merged-into"; +export type AtlasGraphEdgeKind = "describes" | "published-by" | "created-by" | "member-of" | "merged-into"; export interface AtlasGraphNodeV1 { readonly id: string; @@ -58,10 +69,10 @@ export interface AtlasGraphSnapshotV1 { readonly edges: readonly AtlasGraphEdgeV1[]; } -export function buildAtlasGraph( +export async function buildAtlasGraph( registry: AtlasRegistryStateV1, collections: readonly AtlasCollectionV1[] = [], -): AtlasGraphSnapshotV1 { +): Promise { const nodes = new Map(); const edges: AtlasGraphEdgeV1[] = []; const addNode = (node: AtlasGraphNodeV1) => { if (!nodes.has(node.id)) nodes.set(node.id, node); }; @@ -79,11 +90,16 @@ export function buildAtlasGraph( } } + const collectionIds = new Set(); for (const collection of collections) { + if (!await verifyAtlasCollection(collection)) throw new Error(`Atlas collection ${collection.collectionId} failed content verification`); + if (collectionIds.has(collection.collectionId)) throw new Error(`duplicate Atlas collection ${collection.collectionId}`); + collectionIds.add(collection.collectionId); const collectionNode = `collection:${collection.collectionId}`; addNode({ id: collectionNode, kind: "collection", label: collection.title }); const contributorNode = `contributor:${collection.contributorId.trim().toLocaleLowerCase("en-US")}`; addNode({ id: contributorNode, kind: "contributor", label: collection.contributorId }); + edges.push({ from: collectionNode, to: contributorNode, kind: "created-by" }); for (const recordId of collection.atlasRecordIds) { const recordNode = `record:${recordId}`; if (!nodes.has(recordNode)) throw new Error(`Atlas collection references unknown record ${recordId}`); @@ -92,6 +108,7 @@ export function buildAtlasGraph( } for (const merge of registry.specimenMerges) { + if (!await verifyAtlasSpecimenMerge(merge)) throw new Error(`Atlas merge ${merge.mergeId} failed content verification`); const alias = `specimen:${merge.aliasSpecimenId.trim().toLocaleLowerCase("en-US")}`; const canonical = `specimen:${merge.canonicalSpecimenId.trim().toLocaleLowerCase("en-US")}`; if (!nodes.has(alias) || !nodes.has(canonical)) throw new Error("Atlas merge graph refers to unknown specimen node"); diff --git a/packages/validation/src/atlas-registry.ts b/packages/validation/src/atlas-registry.ts index d41ce961..5d299f0a 100644 --- a/packages/validation/src/atlas-registry.ts +++ b/packages/validation/src/atlas-registry.ts @@ -80,6 +80,19 @@ export async function createAtlasSpecimenMerge( return { ...payload, mergeId: await contentDigest(payload) }; } +export async function verifyAtlasSpecimenMerge(merge: AtlasSpecimenMergeV1): Promise { + if (merge.schemaVersion !== 1 || merge.mergeContractVersion !== "atlas-specimen-merge-1") return false; + if (!/^sha256:[0-9a-f]{64}$/.test(merge.mergeId)) return false; + if (!Number.isFinite(Date.parse(merge.createdAt))) return false; + if (normalized(merge.aliasSpecimenId).length === 0 || normalized(merge.canonicalSpecimenId).length === 0) return false; + if (normalized(merge.aliasSpecimenId) === normalized(merge.canonicalSpecimenId)) return false; + if (merge.rationale.trim().length === 0 || merge.supportingRecordIds.length === 0) return false; + if (new Set(merge.supportingRecordIds).size !== merge.supportingRecordIds.length) return false; + if (!merge.supportingRecordIds.every((id) => /^sha256:[0-9a-f]{64}$/.test(id))) return false; + const { mergeId, ...payload } = merge; + return mergeId === await contentDigest(payload); +} + function resolveCanonicalSpecimenId(state: AtlasRegistryStateV1, specimenId: string): string { let current = normalized(specimenId); const visited = new Set(); @@ -92,11 +105,15 @@ function resolveCanonicalSpecimenId(state: AtlasRegistryStateV1, specimenId: str } } -export function applyAtlasSpecimenMerge( +export async function applyAtlasSpecimenMerge( state: AtlasRegistryStateV1, merge: AtlasSpecimenMergeV1, -): AtlasRegistryStateV1 { +): Promise { + if (!await verifyAtlasSpecimenMerge(merge)) throw new Error("Atlas specimen merge failed content verification"); if (state.specimenMerges.some((existing) => existing.mergeId === merge.mergeId)) return state; + if (state.specimenMerges.some((existing) => normalized(existing.aliasSpecimenId) === normalized(merge.aliasSpecimenId))) { + throw new Error("Atlas alias specimen already has a canonical merge"); + } const records = new Set(state.records.map((record) => record.atlasRecordId)); for (const recordId of merge.supportingRecordIds) { if (!records.has(recordId)) throw new Error(`merge supporting record ${recordId} is absent from the registry`); @@ -104,7 +121,7 @@ export function applyAtlasSpecimenMerge( const knownSpecimens = new Set(state.records.map((record) => normalized(record.specimen.specimenId))); if (!knownSpecimens.has(normalized(merge.aliasSpecimenId))) throw new Error("merge alias specimen is absent from the registry"); if (!knownSpecimens.has(normalized(merge.canonicalSpecimenId))) throw new Error("merge canonical specimen is absent from the registry"); - const candidate = { ...state, specimenMerges: [...state.specimenMerges, merge] }; + const candidate = { ...state, specimenMerges: [...state.specimenMerges, merge].sort((left, right) => left.mergeId.localeCompare(right.mergeId)) }; resolveCanonicalSpecimenId(candidate, merge.aliasSpecimenId); return candidate; } @@ -158,5 +175,11 @@ export async function snapshotAtlasRegistry( createdAt: string, ): Promise { if (state.records.length === 0) throw new Error("cannot snapshot an empty Atlas registry"); + for (const record of state.records) { + if (!await verifyAtlasRecord(record)) throw new Error(`cannot snapshot invalid Atlas record ${record.atlasRecordId}`); + } + for (const merge of state.specimenMerges) { + if (!await verifyAtlasSpecimenMerge(merge)) throw new Error(`cannot snapshot invalid Atlas merge ${merge.mergeId}`); + } return createAtlasSnapshot(state.records.map((record) => record.atlasRecordId), createdAt); } diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 6b18f2c0..f1093fa8 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -45,6 +45,16 @@ export { type ArtifactMigrationStepV1, type EverythingRingsArtifactKind, } from "./artifact-envelope"; +export { + createCapabilityClaim, + createSoftwareQualificationManifest, + verifyCapabilityClaim, + verifySoftwareQualificationManifest, + type AssuranceEvidenceReferenceV1, + type CapabilityClaimV1, + type EvidenceDomain, + type SoftwareQualificationManifestV1, +} from "./assurance"; export { createAtlasRecord, createAtlasSnapshot, @@ -62,6 +72,7 @@ export { publishAtlasRecord, searchAtlas, snapshotAtlasRegistry, + verifyAtlasSpecimenMerge, type AtlasRegistryStateV1, type AtlasSearchQueryV1, type AtlasSearchResultV1, @@ -70,6 +81,7 @@ export { export { buildAtlasGraph, createAtlasCollection, + verifyAtlasCollection, type AtlasCollectionV1, type AtlasGraphEdgeKind, type AtlasGraphEdgeV1, @@ -94,6 +106,8 @@ export { export { createStationCalibrationProtocol, evaluateStationCalibration, + evaluateVerifiedStationCalibration, + verifyStationCalibrationProtocol, type StationCalibrationObservationV1, type StationCalibrationProtocolV1, type StationCalibrationVerdictV1, diff --git a/packages/validation/src/repository.ts b/packages/validation/src/repository.ts index cb083290..f7330eb5 100644 --- a/packages/validation/src/repository.ts +++ b/packages/validation/src/repository.ts @@ -1,9 +1,10 @@ import { publishAtlasRecord, + verifyAtlasSpecimenMerge, type AtlasRegistryStateV1, emptyAtlasRegistry, } from "./atlas-registry"; -import type { ResonanceAtlasRecordV1 } from "./atlas"; +import { verifyAtlasRecord, type ResonanceAtlasRecordV1 } from "./atlas"; import { verifyDerivationRecord, verifyMeasurementRecord, @@ -61,6 +62,7 @@ export async function publishRepositoryAtlasRecord( state: ResearchRepositoryStateV1, record: ResonanceAtlasRecordV1, ): Promise { + if (!await verifyAtlasRecord(record)) throw new Error("Atlas record failed content verification"); for (const reference of record.measurements) { if (!state.measurements.some((measurement) => measurement.measurementId === reference.measurementId)) { throw new Error(`Atlas measurement ${reference.measurementId} is absent from repository`); @@ -81,19 +83,32 @@ export interface ResearchRepositoryIntegrityV1 { readonly measurementCount: number; readonly derivationCount: number; readonly atlasRecordCount: number; + readonly atlasMergeCount: number; readonly reasons: readonly string[]; } +function normalize(value: string): string { + return value.trim().toLocaleLowerCase("en-US"); +} + export async function verifyResearchRepositoryIntegrity( state: ResearchRepositoryStateV1, ): Promise { const reasons: string[] = []; + if (state.schemaVersion !== 1 || state.repositoryContractVersion !== "everything-rings-research-repository-1") { + reasons.push("research repository contract metadata is invalid"); + } + if (state.atlas.schemaVersion !== 1 || state.atlas.registryContractVersion !== "resonance-atlas-registry-1") { + reasons.push("Atlas registry contract metadata is invalid"); + } + const measurementIds = new Set(); for (const measurement of state.measurements) { if (measurementIds.has(measurement.measurementId)) reasons.push(`duplicate measurement ${measurement.measurementId}`); measurementIds.add(measurement.measurementId); if (!await verifyMeasurementRecord(measurement)) reasons.push(`measurement ${measurement.measurementId} failed content verification`); } + const derivationIds = new Set(); for (const derivation of state.derivations) { if (derivationIds.has(derivation.derivationId)) reasons.push(`duplicate derivation ${derivation.derivationId}`); @@ -101,8 +116,18 @@ export async function verifyResearchRepositoryIntegrity( if (!await verifyDerivationRecord(derivation)) reasons.push(`derivation ${derivation.derivationId} failed content verification`); if (!measurementIds.has(derivation.measurementId)) reasons.push(`derivation ${derivation.derivationId} has missing root measurement`); } + + const atlasRecordIds = new Set(); + const measurementOwners = new Map(); for (const record of state.atlas.records) { + if (atlasRecordIds.has(record.atlasRecordId)) reasons.push(`duplicate Atlas record ${record.atlasRecordId}`); + atlasRecordIds.add(record.atlasRecordId); + if (!await verifyAtlasRecord(record)) reasons.push(`Atlas record ${record.atlasRecordId} failed content verification`); + const owner = normalize(record.specimen.specimenId); for (const reference of record.measurements) { + const priorOwner = measurementOwners.get(reference.measurementId); + if (priorOwner !== undefined && priorOwner !== owner) reasons.push(`Atlas measurement ${reference.measurementId} is assigned to multiple specimens`); + measurementOwners.set(reference.measurementId, owner); if (!measurementIds.has(reference.measurementId)) reasons.push(`Atlas record ${record.atlasRecordId} references missing measurement ${reference.measurementId}`); for (const derivationId of reference.derivationIds) { const derivation = state.derivations.find((candidate) => candidate.derivationId === derivationId); @@ -111,11 +136,27 @@ export async function verifyResearchRepositoryIntegrity( } } } + + const mergeIds = new Set(); + const mergeAliases = new Set(); + for (const merge of state.atlas.specimenMerges) { + if (mergeIds.has(merge.mergeId)) reasons.push(`duplicate Atlas merge ${merge.mergeId}`); + mergeIds.add(merge.mergeId); + if (!await verifyAtlasSpecimenMerge(merge)) reasons.push(`Atlas merge ${merge.mergeId} failed content verification`); + const alias = normalize(merge.aliasSpecimenId); + if (mergeAliases.has(alias)) reasons.push(`Atlas specimen ${alias} has multiple canonical merges`); + mergeAliases.add(alias); + for (const recordId of merge.supportingRecordIds) { + if (!atlasRecordIds.has(recordId)) reasons.push(`Atlas merge ${merge.mergeId} references missing record ${recordId}`); + } + } + return { valid: reasons.length === 0, measurementCount: state.measurements.length, derivationCount: state.derivations.length, atlasRecordCount: state.atlas.records.length, + atlasMergeCount: state.atlas.specimenMerges.length, reasons: [...new Set(reasons)], }; } diff --git a/packages/validation/src/station-calibration.ts b/packages/validation/src/station-calibration.ts index 5afd202b..150f212a 100644 --- a/packages/validation/src/station-calibration.ts +++ b/packages/validation/src/station-calibration.ts @@ -43,6 +43,19 @@ export async function createStationCalibrationProtocol( return { ...payload, protocolId: await contentDigest(payload) }; } +export async function verifyStationCalibrationProtocol(protocol: StationCalibrationProtocolV1): Promise { + if (protocol.schemaVersion !== 1 || protocol.stationCalibrationContractVersion !== "station-calibration-1") return false; + if (!/^sha256:[0-9a-f]{64}$/.test(protocol.protocolId) || !/^sha256:[0-9a-f]{64}$/.test(protocol.referenceObjectModelDigest)) return false; + if (!Number.isFinite(Date.parse(protocol.createdAt)) || protocol.referencePopulation.trim().length === 0) return false; + if (protocol.referenceSpecimenId.trim().toLocaleLowerCase("en-US") !== protocol.referenceObjectModel.specimenId.trim().toLocaleLowerCase("en-US")) return false; + if (!(protocol.minimumCoverage > 0 && protocol.minimumCoverage <= 1)) return false; + if (!(protocol.maximumMedianFrequencyDistanceCents > 0) || !Number.isFinite(protocol.maximumMedianFrequencyDistanceCents)) return false; + if (!Number.isInteger(protocol.minimumMatchedModes) || protocol.minimumMatchedModes <= 0) return false; + if (protocol.referenceObjectModelDigest !== await contentDigest(protocol.referenceObjectModel)) return false; + const { protocolId, ...payload } = protocol; + return protocolId === await contentDigest(payload); +} + export interface StationCalibrationObservationV1 { readonly stationId: string; readonly createdAt: string; @@ -61,6 +74,7 @@ export interface StationCalibrationVerdictV1 { readonly reasons: readonly string[]; } +/** Diagnostic evaluator. Use evaluateVerifiedStationCalibration for authoritative station qualification. */ export function evaluateStationCalibration( protocol: StationCalibrationProtocolV1, observation: StationCalibrationObservationV1, @@ -88,3 +102,11 @@ export function evaluateStationCalibration( reasons, }; } + +export async function evaluateVerifiedStationCalibration( + protocol: StationCalibrationProtocolV1, + observation: StationCalibrationObservationV1, +): Promise { + if (!await verifyStationCalibrationProtocol(protocol)) throw new Error("station calibration protocol failed content verification"); + return evaluateStationCalibration(protocol, observation); +} diff --git a/packages/validation/test/assurance.test.ts b/packages/validation/test/assurance.test.ts new file mode 100644 index 00000000..2058f4af --- /dev/null +++ b/packages/validation/test/assurance.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { + createCapabilityClaim, + createSoftwareQualificationManifest, + verifyCapabilityClaim, + verifySoftwareQualificationManifest, +} from "../src"; + +const REVISION = "0123456789abcdef0123456789abcdef01234567"; + +describe("machine-readable assurance", () => { + it("binds a software-qualified proposition to explicit evidence and exclusions", async () => { + const claim = await createCapabilityClaim({ + createdAt: "2026-08-24T16:00:00.000Z", + capability: "sonic-twin-retrieval", + proposition: "The deterministic retrieval implementation ranks the correct digital specimen first in the named software qualification corpus.", + scope: "built-in digital qualification corpus only", + exclusions: ["physical-object identity", "material identity", "human perceptual validity"], + evidence: [{ evidenceId: REVISION, domain: "software", description: "exact source revision" }], + maturity: "software-qualified", + }); + expect(await verifyCapabilityClaim(claim)).toBe(true); + expect(await verifyCapabilityClaim({ ...claim, proposition: "mutated" })).toBe(false); + }); + + it("creates and verifies a non-empirical qualification manifest with non-empty claim coverage", async () => { + const claim = await createCapabilityClaim({ + createdAt: "2026-08-24T16:00:00.000Z", + capability: "atlas-integrity", + proposition: "Content-addressed Atlas artifacts detect mutation in software tests.", + scope: "software qualification", + exclusions: ["physical provenance truth"], + evidence: [{ evidenceId: "github-run:12345", domain: "software", description: "qualification workflow" }], + maturity: "software-qualified", + }); + const manifest = await createSoftwareQualificationManifest({ + sourceRevision: REVISION, + createdAt: "2026-08-24T16:05:00.000Z", + testRunIds: ["github-run:12345"], + capabilityClaimIds: [claim.claimId], + }); + expect(await verifySoftwareQualificationManifest(manifest)).toBe(true); + expect(manifest.physicalObjectTested).toBe(false); + expect(manifest.releaseGateEquivalent).toBe(false); + expect(await verifySoftwareQualificationManifest({ ...manifest, sourceRevision: "f".repeat(40) })).toBe(false); + }); + + it("refuses qualification manifests without explicit capability claims", async () => { + await expect(createSoftwareQualificationManifest({ + sourceRevision: REVISION, + createdAt: "2026-08-24T16:05:00.000Z", + testRunIds: ["github-run:12345"], + capabilityClaimIds: [], + })).rejects.toThrow(/capability claim/); + }); +}); diff --git a/packages/validation/test/atlas-network.test.ts b/packages/validation/test/atlas-network.test.ts index a30d63b0..bad649af 100644 --- a/packages/validation/test/atlas-network.test.ts +++ b/packages/validation/test/atlas-network.test.ts @@ -6,10 +6,11 @@ import { createAtlasRecord, emptyAtlasRegistry, publishAtlasRecord, + verifyAtlasCollection, } from "../src"; describe("Atlas collection and graph model", () => { - it("projects public records and collections into an explicit non-identity graph", async () => { + it("projects verified public records and collections into an explicit non-identity graph", async () => { const first = await createAtlasRecord({ createdAt: "2026-08-24T16:00:00.000Z", contributor: { contributorId: "alice", displayName: "Alice" }, @@ -31,9 +32,31 @@ describe("Atlas collection and graph model", () => { description: "Software-only examples", atlasRecordIds: [first.atlasRecordId, second.atlasRecordId], }); - const graph = buildAtlasGraph(registry, [collection]); + expect(await verifyAtlasCollection(collection)).toBe(true); + const graph = await buildAtlasGraph(registry, [collection]); expect(graph.nodes.some((node) => node.kind === "collection")).toBe(true); expect(graph.edges.filter((edge) => edge.kind === "member-of")).toHaveLength(2); + expect(graph.edges.filter((edge) => edge.kind === "created-by")).toHaveLength(1); expect(graph.edges.every((edge) => edge.kind !== ("same-object" as never))).toBe(true); }); + + it("rejects tampered collections before graph projection", async () => { + const record = await createAtlasRecord({ + createdAt: "2026-08-24T16:00:00.000Z", + contributor: null, + specimen: { specimenId: "spec-a", label: "A", objectFamily: "bell", material: "other", publicDescription: null }, + measurements: [{ measurementId: await contentDigest({ m: "a" }), derivationIds: [] }], + }); + const registry = await publishAtlasRecord(emptyAtlasRegistry(), record); + const collection = await createAtlasCollection({ + createdAt: "2026-08-24T16:02:00.000Z", + contributorId: "alice", + title: "Original", + description: null, + atlasRecordIds: [record.atlasRecordId], + }); + const tampered = { ...collection, title: "Changed" }; + expect(await verifyAtlasCollection(tampered)).toBe(false); + await expect(buildAtlasGraph(registry, [tampered])).rejects.toThrow(/content verification/); + }); }); diff --git a/packages/validation/test/atlas-registry.test.ts b/packages/validation/test/atlas-registry.test.ts index 26506e0f..9effce5b 100644 --- a/packages/validation/test/atlas-registry.test.ts +++ b/packages/validation/test/atlas-registry.test.ts @@ -8,6 +8,7 @@ import { publishAtlasRecord, searchAtlas, snapshotAtlasRegistry, + verifyAtlasSpecimenMerge, } from "../src"; async function atlasRecord(specimenId: string, label: string, measurementSeed: string) { @@ -61,7 +62,7 @@ describe("Resonance Atlas registry", () => { await expect(publishAtlasRecord(registry, second)).rejects.toThrow(/another specimen/); }); - it("supports explicit evidence-backed specimen merges without rewriting historical records", async () => { + it("supports verified evidence-backed specimen merges without rewriting historical records", async () => { const first = await atlasRecord("alias-a", "Alias A", "a"); const second = await atlasRecord("canonical-a", "Canonical A", "b"); let registry = await publishAtlasRecord(emptyAtlasRegistry(), first); @@ -73,10 +74,28 @@ describe("Resonance Atlas registry", () => { rationale: "software test of explicit merge policy", supportingRecordIds: [first.atlasRecordId, second.atlasRecordId], }); - registry = applyAtlasSpecimenMerge(registry, merge); + expect(await verifyAtlasSpecimenMerge(merge)).toBe(true); + registry = await applyAtlasSpecimenMerge(registry, merge); expect(registry.records).toHaveLength(2); expect(registry.specimenMerges).toHaveLength(1); const alias = searchAtlas(registry, { text: "Alias" })[0]; expect(alias?.canonicalSpecimenId).toBe("canonical-a"); }); + + it("rejects a content-tampered merge", async () => { + const first = await atlasRecord("alias-a", "Alias A", "a"); + const second = await atlasRecord("canonical-a", "Canonical A", "b"); + let registry = await publishAtlasRecord(emptyAtlasRegistry(), first); + registry = await publishAtlasRecord(registry, second); + const merge = await createAtlasSpecimenMerge({ + createdAt: "2026-08-24T16:05:00.000Z", + aliasSpecimenId: "alias-a", + canonicalSpecimenId: "canonical-a", + rationale: "original rationale", + supportingRecordIds: [first.atlasRecordId, second.atlasRecordId], + }); + const tampered = { ...merge, rationale: "mutated after signing" }; + expect(await verifyAtlasSpecimenMerge(tampered)).toBe(false); + await expect(applyAtlasSpecimenMerge(registry, tampered)).rejects.toThrow(/content verification/); + }); }); diff --git a/packages/validation/test/repository.test.ts b/packages/validation/test/repository.test.ts index c7d6164f..405d8e4b 100644 --- a/packages/validation/test/repository.test.ts +++ b/packages/validation/test/repository.test.ts @@ -75,6 +75,7 @@ describe("research repository integrity", () => { expect(integrity.measurementCount).toBe(1); expect(integrity.derivationCount).toBe(1); expect(integrity.atlasRecordCount).toBe(1); + expect(integrity.atlasMergeCount).toBe(0); }); it("rejects derivations before their immutable measurement root exists", async () => { @@ -102,4 +103,21 @@ describe("research repository integrity", () => { }); await expect(publishRepositoryAtlasRecord(repository, record)).rejects.toThrow(/absent/); }); + + it("detects post-ingest Atlas content tampering during whole-repository verification", async () => { + const m = await measurement(); + let repository = await ingestMeasurement(emptyResearchRepository(), m); + const record = await createAtlasRecord({ + createdAt: "2026-08-24T15:32:00.000Z", + contributor: null, + specimen: { specimenId: m.specimenId, label: "Original", objectFamily: "reference", material: "other", publicDescription: null }, + measurements: [{ measurementId: m.measurementId, derivationIds: [] }], + }); + repository = await publishRepositoryAtlasRecord(repository, record); + const tamperedRecord = { ...record, specimen: { ...record.specimen, label: "Tampered" } }; + const tampered = { ...repository, atlas: { ...repository.atlas, records: [tamperedRecord] } }; + const integrity = await verifyResearchRepositoryIntegrity(tampered); + expect(integrity.valid).toBe(false); + expect(integrity.reasons.some((reason) => reason.includes("failed content verification"))).toBe(true); + }); }); diff --git a/packages/validation/test/station-calibration.test.ts b/packages/validation/test/station-calibration.test.ts index 0288b3e8..97952f5e 100644 --- a/packages/validation/test/station-calibration.test.ts +++ b/packages/validation/test/station-calibration.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from "vitest"; import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; import { buildAcousticObjectModel } from "@everything-rings/fingerprint"; -import { createStationCalibrationProtocol, evaluateStationCalibration } from "../src"; +import { + createStationCalibrationProtocol, + evaluateStationCalibration, + evaluateVerifiedStationCalibration, + verifyStationCalibrationProtocol, +} from "../src"; function fp(frequencies: readonly number[]): AcousticFingerprintV1 { return { @@ -20,22 +25,27 @@ function fp(frequencies: readonly number[]): AcousticFingerprintV1 { }; } +async function protocol() { + const model = buildAcousticObjectModel([ + { observationId: "r1", specimenId: "reference-1", fingerprint: fp([440, 880, 1320]) }, + { observationId: "r2", specimenId: "reference-1", fingerprint: fp([441, 882, 1323]) }, + ]); + return createStationCalibrationProtocol({ + createdAt: "2026-08-24T15:30:00.000Z", + referencePopulation: "digital-station-reference-v1", + referenceSpecimenId: "reference-1", + referenceObjectModel: model, + minimumCoverage: 1, + maximumMedianFrequencyDistanceCents: 35, + minimumMatchedModes: 3, + }); +} + describe("measurement-station calibration", () => { - it("passes a digital station observation close to its frozen reference object model", async () => { - const model = buildAcousticObjectModel([ - { observationId: "r1", specimenId: "reference-1", fingerprint: fp([440, 880, 1320]) }, - { observationId: "r2", specimenId: "reference-1", fingerprint: fp([441, 882, 1323]) }, - ]); - const protocol = await createStationCalibrationProtocol({ - createdAt: "2026-08-24T15:30:00.000Z", - referencePopulation: "digital-station-reference-v1", - referenceSpecimenId: "reference-1", - referenceObjectModel: model, - minimumCoverage: 1, - maximumMedianFrequencyDistanceCents: 35, - minimumMatchedModes: 3, - }); - const verdict = evaluateStationCalibration(protocol, { + it("passes a digital station observation close to its verified frozen reference object model", async () => { + const frozen = await protocol(); + expect(await verifyStationCalibrationProtocol(frozen)).toBe(true); + const verdict = await evaluateVerifiedStationCalibration(frozen, { stationId: "station-digital-a", createdAt: "2026-08-24T15:31:00.000Z", specimenId: "reference-1", @@ -46,20 +56,8 @@ describe("measurement-station calibration", () => { }); it("fails a digital station observation that drifts outside its frozen contract", async () => { - const model = buildAcousticObjectModel([ - { observationId: "r1", specimenId: "reference-1", fingerprint: fp([440, 880, 1320]) }, - { observationId: "r2", specimenId: "reference-1", fingerprint: fp([441, 882, 1323]) }, - ]); - const protocol = await createStationCalibrationProtocol({ - createdAt: "2026-08-24T15:30:00.000Z", - referencePopulation: "digital-station-reference-v1", - referenceSpecimenId: "reference-1", - referenceObjectModel: model, - minimumCoverage: 1, - maximumMedianFrequencyDistanceCents: 35, - minimumMatchedModes: 3, - }); - const verdict = evaluateStationCalibration(protocol, { + const frozen = await protocol(); + const verdict = evaluateStationCalibration(frozen, { stationId: "station-digital-b", createdAt: "2026-08-24T15:31:00.000Z", specimenId: "reference-1", @@ -68,4 +66,16 @@ describe("measurement-station calibration", () => { expect(verdict.passed).toBe(false); expect(verdict.reasons.length).toBeGreaterThan(0); }); + + it("rejects a protocol mutated after its content address was created", async () => { + const frozen = await protocol(); + const tampered = { ...frozen, maximumMedianFrequencyDistanceCents: 500 }; + expect(await verifyStationCalibrationProtocol(tampered)).toBe(false); + await expect(evaluateVerifiedStationCalibration(tampered, { + stationId: "station-digital-c", + createdAt: "2026-08-24T15:31:00.000Z", + specimenId: "reference-1", + fingerprint: fp([440.5, 881, 1321.5]), + })).rejects.toThrow(/content verification/); + }); }); From c2d6c1a4a4f6e148a9d3505ead659369d02212f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:08:35 -0700 Subject: [PATCH 097/101] Harden research estimator indexed access --- packages/dsp/src/analysis/prony-research.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dsp/src/analysis/prony-research.ts b/packages/dsp/src/analysis/prony-research.ts index 89c42c1b..514df178 100644 --- a/packages/dsp/src/analysis/prony-research.ts +++ b/packages/dsp/src/analysis/prony-research.ts @@ -57,8 +57,8 @@ function linearPredictionCoefficients(samples: readonly number[], order: number) const target = -samples[n]!; for (let i = 0; i < order; i += 1) { const xi = samples[n - i - 1]!; - rhs[i] += xi * target; - for (let j = 0; j < order; j += 1) normal[i]![j] += xi * samples[n - j - 1]!; + rhs[i] = rhs[i]! + xi * target; + for (let j = 0; j < order; j += 1) normal[i]![j] = normal[i]![j]! + xi * samples[n - j - 1]!; } } const trace = normal.reduce((sum, row, index) => sum + Math.abs(row[index] ?? 0), 0); From 3c3b4a55ecaa3bd8878b77281c9d13bbd18d4eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:09:50 -0700 Subject: [PATCH 098/101] Strengthen object-model algorithm provenance typing --- packages/fingerprint/src/object-model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fingerprint/src/object-model.ts b/packages/fingerprint/src/object-model.ts index 8ab3061a..fe52e742 100644 --- a/packages/fingerprint/src/object-model.ts +++ b/packages/fingerprint/src/object-model.ts @@ -1,4 +1,4 @@ -import type { AcousticFingerprintV1 } from "@everything-rings/dsp"; +import type { AcousticFingerprintV1, AcousticFingerprintAlgorithmVersion } from "@everything-rings/dsp"; import { centsDistance } from "./recurrence"; export interface AcousticObjectObservationV1 { @@ -36,7 +36,7 @@ export interface AcousticObjectModelV1 { readonly objectModelVersion: "acoustic-object-model-1"; readonly specimenId: string; readonly observationCount: number; - readonly fingerprintAlgorithmVersions: readonly string[]; + readonly fingerprintAlgorithmVersions: readonly AcousticFingerprintAlgorithmVersion[]; readonly sampleRates: readonly number[]; readonly modes: readonly AcousticObjectModeEstimateV1[]; } From 265a6b6972ce3b24037c66357dac6ea55a9018fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:11:13 -0700 Subject: [PATCH 099/101] Use capture sample rate in full-system qualification --- apps/web/src/FullVisionSystem.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/FullVisionSystem.test.ts b/apps/web/src/FullVisionSystem.test.ts index b0a1f3a2..61ac09aa 100644 --- a/apps/web/src/FullVisionSystem.test.ts +++ b/apps/web/src/FullVisionSystem.test.ts @@ -79,7 +79,7 @@ function fingerprintFromCapture(capture: AudioCapture): AcousticFingerprintV1 { const quality = assessCaptureQuality(capture); expect(quality.ok).toBe(true); const ringdown = extractImpactRingdown(capture.samples, capture.sampleRate, capture.triggerSample); - const result = analyzeImpact(ringdown.samples, ringdown.sampleRate); + const result = analyzeImpact(ringdown.samples, capture.sampleRate); expect(result.ok).toBe(true); if (!result.ok) throw new Error(result.reason); expect(result.fingerprint.modes.length).toBeGreaterThanOrEqual(3); @@ -215,4 +215,4 @@ describe("EVERYTHING RINGS full-vision software system", () => { const snapshot = await snapshotAtlasRegistry(repository.atlas, "2026-08-24T16:41:00.000Z"); expect(snapshot.recordIds).toHaveLength(3); }); -}); +}); \ No newline at end of file From 94481c7837dc4c54a8dc648fbddc2727823ebfba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:13:47 -0700 Subject: [PATCH 100/101] Bound full-system qualification runtime explicitly --- apps/web/src/FullVisionSystem.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/FullVisionSystem.test.ts b/apps/web/src/FullVisionSystem.test.ts index 61ac09aa..bc434f89 100644 --- a/apps/web/src/FullVisionSystem.test.ts +++ b/apps/web/src/FullVisionSystem.test.ts @@ -214,5 +214,5 @@ describe("EVERYTHING RINGS full-vision software system", () => { expect(graph.edges.filter((edge) => edge.kind === "created-by")).toHaveLength(1); const snapshot = await snapshotAtlasRegistry(repository.atlas, "2026-08-24T16:41:00.000Z"); expect(snapshot.recordIds).toHaveLength(3); - }); + }, 20_000); }); \ No newline at end of file From 4531217b2bff9f5842285104295f6ddf29a8bb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:15:35 -0700 Subject: [PATCH 101/101] Align browser qualification with held-out calibration surface --- scripts/browser-smoke.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/browser-smoke.sh b/scripts/browser-smoke.sh index e73b935a..0758ec6e 100755 --- a/scripts/browser-smoke.sh +++ b/scripts/browser-smoke.sh @@ -64,7 +64,7 @@ check_route "/?campaign=1" "Precommitted physical collection" "campaign" check_route "/?release=1" "Empirical release gates" "release" check_route "/?gate-b=1" "Post-collection blinded reconstruction" "gate-b" check_route "/?gate-c=1" "Post-Gate-B playable identity" "gate-c" -check_route "/?twin=1" "Object models, retrieval, calibration, abstention" "twin" +check_route "/?twin=1" "Object models, retrieval, held-out calibration, abstention" "twin" check_route "/?atlas=1" "Content-addressed acoustic provenance" "atlas" check_route "/?campaign-author=1&release=1" "Empirical release gates" "release-author-precedence" check_route "/?gate-b=1&release=1" "Empirical release gates" "release-review-precedence"