fix(takes): emit JSON for page extraction (#4004)

Fixes #3962

Return the structured extraction result for --json callers while preserving the existing human summary. Add a behavior-level regression test that proves stdout is parseable JSON.
This commit is contained in:
Ziyang Guo
2026-08-13 05:01:29 -07:00
committed by GitHub
parent 52389dbe5b
commit 758a2d4293
2 changed files with 67 additions and 2 deletions
+11 -2
View File
@@ -624,12 +624,13 @@ async function cmdExtract(engine: BrainEngine, rest: string[]): Promise<void> {
const sub = rest[0];
if (sub !== '--from-pages') {
process.stderr.write(
'Usage: gbrain takes extract --from-pages [--yes] [--dry-run] [--source-id <id>] [--max-pages N (clamped to 1000)] [--include-covered] [--holder <name>]\n' +
'Usage: gbrain takes extract --from-pages [--yes] [--dry-run] [--json] [--source-id <id>] [--max-pages N (clamped to 1000)] [--include-covered] [--holder <name>]\n' +
'Runs progress: pages that already hold takes are skipped, so repeat runs sweep a large corpus in slices. --include-covered rescans everything (refresh).\n',
);
process.exit(1);
}
const dryRun = rest.includes('--dry-run');
const json = rest.includes('--json');
const skipConfirm = rest.includes('--yes');
const sourceIdx = rest.indexOf('--source-id');
const sourceIdFilter = sourceIdx >= 0 ? rest[sourceIdx + 1] : undefined;
@@ -667,9 +668,17 @@ async function cmdExtract(engine: BrainEngine, rest: string[]): Promise<void> {
holder,
});
if (result.llm_unavailable) {
process.stderr.write(`[takes extract] chat gateway unavailable (no API key configured).\n`);
if (json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} else {
process.stderr.write(`[takes extract] chat gateway unavailable (no API key configured).\n`);
}
process.exit(2);
}
if (json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
process.stdout.write(
`takes extract --from-pages: ${result.claims_extracted} claim(s) from ${result.pages_scanned} page(s)` +
(dryRun ? ' (dry-run)' : '') + '\n',
+56
View File
@@ -0,0 +1,56 @@
/**
* #3962 — `takes extract --from-pages --json` must emit the structured
* extraction result instead of the human summary line.
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { runTakes } from '../src/commands/takes.ts';
import {
configureGateway,
resetGateway,
} from '../src/core/ai/gateway.ts';
import type { BrainEngine } from '../src/core/engine.ts';
const engine = {
getConfig: async (key: string) => key === 'takes.bootstrap_enabled' ? 'true' : null,
executeRaw: async () => [],
} as unknown as BrainEngine;
async function captureStdout(fn: () => Promise<void>): Promise<string> {
const chunks: string[] = [];
const originalWrite = process.stdout.write;
process.stdout.write = ((chunk: string | Uint8Array) => {
chunks.push(String(chunk));
return true;
}) as typeof process.stdout.write;
try {
await fn();
} finally {
process.stdout.write = originalWrite;
}
return chunks.join('');
}
beforeAll(() => {
configureGateway({
chat_model: 'openai:gpt-test',
env: { OPENAI_API_KEY: 'sk-test-takes-json' },
});
});
afterAll(() => {
resetGateway();
});
describe('gbrain takes extract --from-pages --json (#3962)', () => {
test('emits the extraction result as parseable JSON', async () => {
const stdout = await captureStdout(() =>
runTakes(engine, ['extract', '--from-pages', '--dry-run', '--json']));
expect(JSON.parse(stdout)).toEqual({
pages_scanned: 0,
claims_extracted: 0,
consent_gate_blocked: false,
llm_unavailable: false,
});
});
});