disclosure-bureau/web/app/api/h/[hypothesisId]/red-team/route.ts

62 lines
2.1 KiB
TypeScript
Raw Normal View History

W3.8: Schneier red-team detective + /h/[hypothesisId] dossier page Adds the fourth AI detective in the Investigation Bureau runtime: Bruce Schneier, who attacks an existing hypothesis as a red-team operator. Runtime: - prompts/schneier.md — discipline (don't disprove, just attack; structured output with hidden_assumptions, failure_modes, alternative_explanations, recommended_tests, verdict_one_sentence; severity ∈ {low, medium, high}; emit INSUFFICIENT_HYPOTHESIS when the input is too thin) - src/detectives/schneier.ts — reads the hypothesis row + evidence chain (joined via evidence_refs FK), feeds Claude with the arguments + verbatim quotes, parses strict JSON object - src/tools/write_red_team_review.ts — UPDATEs hypotheses.reviewed_by + updated_at; APPENDS (or replaces if re-reviewed) a structured "## Red-team review (Schneier · X severity)" section to case/hypotheses/H-NNNN.md. Caps each list at 5 entries × 240 chars, validates verdict ≤ 280 chars. - orchestrator: new `red_team_review` kind dispatching to runSchneier Chat + UI: - request_investigation gains kind=red_team_review + hypothesis_id arg (validated against H-NNNN regex); detective auto-resolves to schneier - chat-bubble inline card paints Schneier in red (#ff3344) - /jobs/[id] page swaps title/subtitle/tone per detective; the "Question" label becomes "Hypothesis under attack" for red_team_review New /h/[hypothesisId] page (hypothesis dossier): - Server-rendered from public.hypotheses + public.evidence (joined via evidence_refs FK + chunk lookup) - Header: ID + creator + reviewer (highlighted when Schneier has visited), position as headline, question subtitle, Tetlock band - Prior + posterior bars with Δ-delta indicator - Argument grid: argument_for (green) vs argument_against (pink) side-by-side with [[wiki-link]] auto-linking to source chunks - Evidence chain: each E-NNNN with Grade A/B/C badge, verbatim blockquote, link to source page - Red-team review panel: parses the markdown section in the case file (severity badge, verdict, 4 bullet panels for hidden_assumptions / failure_modes / alternative_explanations / recommended_tests). Empty state when not yet reviewed. RedTeamRequestButton client component + POST /api/h/[id]/red-team — authenticated user can trigger Schneier in one click; UI swaps to "acompanhar" link to /jobs/[id] once queued. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:48:12 +00:00
/**
* POST /api/h/[hypothesisId]/red-team enqueue a Schneier red_team_review
* investigation job for the given hypothesis. Returns { job_id, status_url }.
*
* Requires an authenticated user (any role). Audit captures the requester's
* email in triggered_by so the runtime can attribute the cost.
*/
import { NextResponse } from "next/server";
import { pgQuery } from "@/lib/retrieval/db";
import { createClient, isSupabaseConfigured } from "@/lib/supabase/server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(
_request: Request,
ctx: { params: Promise<{ hypothesisId: string }> },
) {
const { hypothesisId } = await ctx.params;
if (!/^H-\d{4}$/.test(hypothesisId)) {
return NextResponse.json({ error: "bad_hypothesis_id" }, { status: 400 });
}
let triggered_by = "user:anonymous";
if (isSupabaseConfigured()) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
}
triggered_by = `user:${user.email ?? user.id}`;
}
// Verify hypothesis exists.
const found = await pgQuery<{ hypothesis_id: string }>(
`SELECT hypothesis_id FROM public.hypotheses WHERE hypothesis_id = $1`,
[hypothesisId],
).catch(() => []);
if (found.length === 0) {
return NextResponse.json({ error: "not_found", hypothesis_id: hypothesisId }, { status: 404 });
}
try {
const rows = await pgQuery<{ job_id: string }>(
`INSERT INTO public.investigation_jobs (kind, payload, triggered_by, status)
VALUES ('red_team_review', $1::jsonb, $2, 'queued')
RETURNING job_id`,
[JSON.stringify({ hypothesis_id: hypothesisId }), triggered_by],
);
const job_id = rows[0]?.job_id;
if (!job_id) return NextResponse.json({ error: "insert_failed" }, { status: 500 });
return NextResponse.json({
job_id,
status: "queued",
status_url: `/jobs/${job_id}`,
eta_seconds: 30,
});
} catch (e) {
return NextResponse.json({ error: "db_unavailable", message: (e as Error).message }, { status: 503 });
}
}