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/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);
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 {