From 676a045d2ed47a0e3d8949026b3b87eadf630dde Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 00:54:15 +0000 Subject: [PATCH 1/2] fix(audiobook-curator): normalize Audiolocate results (#331 r3919535592) --- examples/audiobook-curator/src/evidence.ts | 5 ++-- .../tests/evidence-parity.test.ts | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/examples/audiobook-curator/src/evidence.ts b/examples/audiobook-curator/src/evidence.ts index 53a9b446f..1476da35c 100644 --- a/examples/audiobook-curator/src/evidence.ts +++ b/examples/audiobook-curator/src/evidence.ts @@ -147,9 +147,8 @@ const pythonMatcher = (python: string, process: MediaProcess): AcousticMatcher = const result = await process(python, ['-c', script, source, sample, String(options.chunkSeconds), options.verbose ? '1' : '0'], { signal: options.signal }); const line = result.stdout.split(/\r?\n/u).findLast((candidate) => candidate.startsWith(resultMarker)); if (line === undefined) throw new CuratorError('Audiolocate emitted no structured result.'); - const parsed = JSON.parse(line.slice(resultMarker.length)) as JsonObject; - asRecord(parsed); - return Object.freeze(parsed); + const parsed = JSON.parse(line.slice(resultMarker.length)) as JsonValue; + return Object.freeze(asRecord(parsed)) as JsonObject; } catch (error) { throw new CuratorError(`Audiolocate is optional; install it for ${python}, or inject an acoustic matcher. ${errorMessage(error, '')}`.trim()); } diff --git a/examples/audiobook-curator/tests/evidence-parity.test.ts b/examples/audiobook-curator/tests/evidence-parity.test.ts index 5af48c8ca..f701963d8 100644 --- a/examples/audiobook-curator/tests/evidence-parity.test.ts +++ b/examples/audiobook-curator/tests/evidence-parity.test.ts @@ -59,6 +59,32 @@ describe('optional identity evidence parity', () => { expect(receipt).toMatchObject({ exitCode: 0, verifiedRecording: true }); }); + it('normalizes non-object Audiolocate results to an empty fingerprint record', async () => { + const root = await mkdtemp(join(tmpdir(), 'curator-audiolocate-non-object-')); + roots.push(root); + const file = join(root, 'book.m4b'); + await writeFile(file, 'book'); + + for (const output of ['null', '[1,2]']) { + const process: MediaProcess = async () => ({ + stderr: '', + stdout: `__AGENT_BUNDLE_AUDIOLOCATE_RESULT__${output}\n`, + }); + const receipt = await verifyAudibleSample({ asin: 'ASIN', file, sampleUrl: 'https://sample.example/book.mp3' }, { + audiolocatePython: 'python-test', + http: async () => Buffer.from('sample'), + process, + }); + + expect(receipt).toMatchObject({ + exitCode: 2, + fingerprint: {}, + verifiedRecording: false, + }); + expect(Array.isArray(receipt.fingerprint)).toBe(false); + } + }); + it('deduplicates ranked ASINs and isolates candidate failures', async () => { const root = await mkdtemp(join(tmpdir(), 'curator-identify-')); roots.push(root); From 629ce24bf355e38a7ac41b79b0c291faaddfd456 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 00:54:27 +0000 Subject: [PATCH 2/2] fix(audiobook-curator): cap inventory error callouts (#331 r3919535600) --- .../src/components/library-shelf.tsx | 6 +++- .../tests/route-unit/routes.test.ts | 33 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/examples/audiobook-curator/src/components/library-shelf.tsx b/examples/audiobook-curator/src/components/library-shelf.tsx index 1cdf1d88c..ce71b64e3 100644 --- a/examples/audiobook-curator/src/components/library-shelf.tsx +++ b/examples/audiobook-curator/src/components/library-shelf.tsx @@ -13,6 +13,7 @@ export interface InspectionShelfProps { } const maximumCards = 20; +const maximumCallouts = 20; interface RemainingFilesProps { readonly count: number; @@ -53,9 +54,12 @@ export const InventoryShelf = ({ receipt }: InventoryShelfProps) => ( ))} - {receipt.errors.map((row, index) => ( + {receipt.errors.slice(0, maximumCallouts).map((row, index) => ( {`${row.path}: ${row.error}`} ))} + {receipt.errors.length > maximumCallouts + ? {`_+${String(receipt.errors.length - maximumCallouts)} more probe errors retained in the structured receipt._`} + : null} ); diff --git a/examples/audiobook-curator/tests/route-unit/routes.test.ts b/examples/audiobook-curator/tests/route-unit/routes.test.ts index 18a4e5e1c..ea380633c 100644 --- a/examples/audiobook-curator/tests/route-unit/routes.test.ts +++ b/examples/audiobook-curator/tests/route-unit/routes.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -110,6 +110,37 @@ it('renders composed inspection and inventory tool documents with unchanged valu } }); +it('caps rendered inventory probe errors while retaining the complete receipt', async () => { + const directory = await mkdtemp(join(tmpdir(), 'curator-route-unit-inventory-errors-')); + const previousPath = process.env['PATH']; + try { + const source = join(directory, 'library'); + await mkdir(source); + await Promise.all(Array.from({ length: 25 }, (_, index) => + writeFile(join(source, `broken-${String(index)}.mp3`), 'not audio'))); + process.env['PATH'] = directory; + + const rendered = await renderRoute('tool:curator/inventory_sources', { + input: { source, strict: true }, + }); + const receipt = rendered.document.value as { + readonly errors: readonly unknown[]; + }; + if (rendered.document.root.kind !== 'result') throw new Error('expected inventory result document'); + const errorCallouts = rendered.document.root.children.filter((node) => node.kind === 'error'); + + expect(receipt.errors).toHaveLength(25); + expect(errorCallouts).toHaveLength(20); + expectDocument(rendered) + .toContainMarkdown('_+5 more probe errors retained in the structured receipt._') + .toHaveValue(receipt); + } finally { + if (previousPath === undefined) delete process.env['PATH']; + else process.env['PATH'] = previousPath; + await rm(directory, { force: true, recursive: true }); + } +}); + it('renders the library-audit CLI route with in-flight progress and the canonical receipt (#102 stage 3)', async () => { const directory = await mkdtemp(join(tmpdir(), 'curator-route-unit-audit-')); try {