From 5e406d3a84146c9f038041a72362fb119a788791 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 04:36:19 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20third-wave=20findings=20=E2=80=94=20iden?= =?UTF-8?q?tify=20staging=20follows=20the=20receipt,=20JSON-safe=20attempt?= =?UTF-8?q?=20receipts,=20canvas=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent-bundle-walkthrough.canvas.tsx | 7 +++-- examples/audiobook-curator/src/evidence.ts | 21 ++++++++++---- .../tests/evidence-parity.test.ts | 28 +++++++++++++++++++ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/canvases/agent-bundle-walkthrough.canvas.tsx b/docs/canvases/agent-bundle-walkthrough.canvas.tsx index 5bc27be66..557649336 100644 --- a/docs/canvases/agent-bundle-walkthrough.canvas.tsx +++ b/docs/canvases/agent-bundle-walkthrough.canvas.tsx @@ -403,8 +403,9 @@ export default function AgentBundleWalkthrough() { inputSchema, and resultSchema exports โ€” there is no execute/render split (exporting either is the AB4811 error). Other - route kinds (events, CLI commands, skills) carry their own export - contracts. + route kinds (events, CLI commands) carry their own export + contracts; skills are Markdown documents parsed from frontmatter + unless the rendered SKILL.tsx form is used. @@ -643,7 +644,7 @@ export default function AgentBundleWalkthrough() { n={5} title="Thin client prints the host-native response and exits 0" channel="wrapper โ†’ Claude ยท stdout" - note="Claude blocks the Write and surfaces the reason to the model. If the route had decided to allow it, the wrapper prints an explicit hookSpecificOutput.permissionDecision: 'allow' (optionally with updatedInput / additionalContext); a route that renders no decision prints nothing." + note="Claude blocks the Write and surfaces the reason to the model. On tool/before the wrapper always answers: an explicit hookSpecificOutput.permissionDecision ('allow' unless the route denied, optionally with updatedInput / additionalContext) โ€” even when the route renders no decision. Silence is reserved for observation-only families such as session/end." payload={WIRE_STDOUT} last /> diff --git a/examples/audiobook-curator/src/evidence.ts b/examples/audiobook-curator/src/evidence.ts index 1476da35c..4dab858ef 100644 --- a/examples/audiobook-curator/src/evidence.ts +++ b/examples/audiobook-curator/src/evidence.ts @@ -260,11 +260,13 @@ export const identifyAudibleSample = async ( for (const candidate of selected) { const asin = String(candidate.asin ?? ''); const region = String(candidate.region ?? 'us') as AudibleRegion; + const score = asRecord(candidate.evidence).score as JsonValue | undefined; + // Receipts are strict JSON; absent fields are omitted, never undefined. const base = { - asin: asin || undefined, + ...(asin === '' ? {} : { asin }), region, - score: asRecord(candidate.evidence).score as JsonValue | undefined, - title: candidate.title, + ...(score === undefined ? {} : { score }), + ...(candidate.title === undefined ? {} : { title: candidate.title }), }; if (asin === '') { attempts.push({ ...base, reason: 'candidate has no ASIN', status: 'skipped' }); @@ -276,20 +278,27 @@ export const identifyAudibleSample = async ( attempts: input.attempts, chunkSeconds: input.chunkSeconds, file: input.file, + // Staging follows the receipt's disk; without this, identification + // would stage beside source media the receipt was chosen to avoid. + ...(input.receipt === undefined ? {} : { receipt: input.receipt }), region, ...(typeof candidate.sample_url === 'string' ? { sampleUrl: candidate.sample_url } : {}), verbose: input.verbose, }, dependencies); const found = outcome.fingerprint.found === true; + const attemptTitle = candidate.title ?? outcome.audible.title; attempts.push({ ...base, fingerprint: outcome.fingerprint, - sampleUrl: outcome.audible.sampleUrl, + ...(outcome.audible.sampleUrl === undefined ? {} : { sampleUrl: outcome.audible.sampleUrl }), status: found ? 'matched' : 'no-match', - title: candidate.title ?? outcome.audible.title, + ...(attemptTitle === undefined ? {} : { title: attemptTitle }), }); if (found) { - identified ??= { asin, region, title: candidate.title ?? outcome.audible.title }; + const title = candidate.title ?? outcome.audible.title; + // A titleless identification must not place an undefined (non-JSON) + // value into the durable receipt. + identified ??= { asin, region, ...(title === undefined ? {} : { title }) }; if (input.all !== true) break; } } catch (error) { diff --git a/examples/audiobook-curator/tests/evidence-parity.test.ts b/examples/audiobook-curator/tests/evidence-parity.test.ts index f701963d8..ae9d252bf 100644 --- a/examples/audiobook-curator/tests/evidence-parity.test.ts +++ b/examples/audiobook-curator/tests/evidence-parity.test.ts @@ -168,4 +168,32 @@ describe('optional identity evidence parity', () => { expect(receipt).toMatchObject({ exitCode: 0, operation: 'whisper-identity', status: 'transcript-ready', usableWindows: 5 }); expect(whisperCalls).toBe(5); }); + + + it('stages acoustic-identify candidate work beside the receipt when one is requested', async () => { + const root = await mkdtemp(join(tmpdir(), 'curator-identify-receipt-')); + roots.push(root); + const file = join(root, 'library', 'book.m4b'); + const receiptPath = join(root, 'receipts', 'identify.json'); + await mkdir(dirname(file), { recursive: true }); + await writeFile(file, 'book'); + const sampleDirs: string[] = []; + const matcher: AcousticMatcher = async (_source, sample) => { + sampleDirs.push(dirname(sample)); + return { found: true }; + }; + const http: CuratorHttpClient = async (url, options) => { + if (options?.binary === true) return Buffer.from(url); + throw new Error('unexpected product request'); + }; + await identifyAudibleSample({ + candidates: [{ asin: 'MATCH', evidence: { score: 90 }, region: 'us', sample_url: 'https://samples/match.mp3' }], + candidatesReport: join(root, 'candidates.json'), + file, + receipt: receiptPath, + top: 1, + }, { http, matcher }); + expect(sampleDirs).toHaveLength(1); + expect(sampleDirs[0]!.startsWith(join(dirname(receiptPath), '.audiobook-curator-acoustic-'))).toBe(true); + }); });