source.fact.ngo a coherence.ngo project

scripts/council.js

raw ↗ · AGPL-3.0

#!/usr/bin/env node // council.js — the fact-checking lane, BUILT AND INACTIVE. Runs a multi-seat, // multi-round deliberation on one claim and emits a verdict record. Without // --activate it refuses and explains: nothing feeds the verified lane yet. // // node scripts/council.js --claim "..." --activate // // Council: <seats> seats of the council model, independent round 1, cross- // examination round 2, chair synthesis. Verdicts land in verified/<date>/ // verdicts.jsonl; the full transcript in verified/sessions/<id>.json. import fs from "node:fs"; import path from "node:path"; import { appendJsonl, DATA_ROOT, hashId, loadConfig, logSpend, nowIso, parseArgs, runModel, utcDate, } from "./lib.js"; const { args } = parseArgs(process.argv.slice(2), { claim: { takes: "value", default: null }, activate: { takes: "none", default: false }, seats: { takes: "value", default: null }, }); if (!args.claim) { console.error("--claim is required"); process.exit(2); } if (!args.activate) { console.log(`council: INACTIVE by design. The verified lane is not fed yet. This harness is complete and budget-checked; activating it is a deliberate act: node scripts/council.js --claim "..." --activate Until then the site renders the verified lane as in-development, and every emergence record stays status: unverified.`); process.exit(3); } const config = loadConfig(); const model = config.council_model; const seats = args.seats ? parseInt(args.seats, 10) : config.council_seats; const councilPrompt = fs.readFileSync(path.join(import.meta.dirname, "../prompts/council.md"), "utf8"); const id = hashId("vrd", args.claim); const date = utcDate(); const sessionFile = path.join(DATA_ROOT, "verified", "sessions", `${id}.json`); fs.mkdirSync(path.dirname(sessionFile), { recursive: true }); const session = { id, claim: args.claim, model, seats, rounds: config.council_rounds, started: nowIso(), turns: [], totals: { tokens_in: 0, tokens_out: 0, cost_usd: 0 }, }; async function seatCall(seatNo, messages, temperature) { const r = await runModel(model, messages, { temperature, max_tokens: 1200 }); logSpend({ script: "council", model, tokensIn: r.tokensIn, tokensOut: r.tokensOut, cost: r.cost }); session.totals.tokens_in += r.tokensIn || 0; session.totals.tokens_out += r.tokensOut || 0; session.totals.cost_usd += r.cost || 0; session.turns.push({ seat: seatNo, content: r.text, tokens_in: r.tokensIn, tokens_out: r.tokensOut }); fs.writeFileSync(sessionFile, JSON.stringify(session, null, 1) + "\n"); return r.text; } console.log(`council: ${seats} seats, ${config.council_rounds} rounds, model ${model}`); console.log(`claim: ${args.claim}`); // Round 1 — independent verdicts const round1 = []; for (let s = 1; s <= seats; s++) { const t = await seatCall(s, [ { role: "system", content: councilPrompt }, { role: "user", content: `Round 1 (independent). The claim:\n\n"${args.claim}"` }, ], 0.7); round1.push(t); console.log(` seat ${s} round 1 done`); } // Round 2 — cross-examination const finals = []; for (let s = 1; s <= seats; s++) { const others = round1.filter((_, i) => i !== s - 1).map((t, i) => `Seat ${i < s - 1 ? i + 1 : i + 2}:\n${t}`).join("\n\n"); const t = await seatCall(s, [ { role: "system", content: councilPrompt }, { role: "user", content: `Round 1 (independent). The claim:\n\n"${args.claim}"\n\nYou answered:\n${round1[s - 1]}` }, { role: "user", content: `Round 2 (cross-examination). The other seats' round-1 verdicts:\n\n${others}\n\nRevise or hold. End with "FINAL STANCE:" and "FINAL CONFIDENCE:".` }, ], 0.3); finals.push(t); console.log(` seat ${s} round 2 done`); } // Chair synthesis const chairText = await seatCall(0, [ { role: "system", content: "You are the chair of a fact-checking council. Synthesize the seats' final verdicts into ONE verdict record. Output only JSON: {\"stance\": \"supported|refuted|unclear\", \"confidence\": 0-1, \"reasoning\": \"3-6 sentences\", \"evidence_needed\": [\"...\"], \"dissent\": \"one line or null\"}. Majority stance wins; record genuine dissent." }, { role: "user", content: `Claim:\n"${args.claim}"\n\nSeats' final verdicts:\n${finals.map((t, i) => `Seat ${i + 1}:\n${t}`).join("\n\n")}` }, ], 0.2); let verdict; try { const a = chairText.indexOf("{"), b = chairText.lastIndexOf("}"); verdict = JSON.parse(chairText.slice(a, b + 1)); } catch { console.error("council: chair synthesis failed to parse — no verdict written"); process.exit(1); } const statusMap = { supported: "verified", refuted: "refuted", unclear: "unclear" }; const record = { id, status: statusMap[verdict.stance] || "unclear", lane: "council", claim: args.claim, verdict: { stance: verdict.stance || "unclear", confidence: typeof verdict.confidence === "number" ? verdict.confidence : null, reasoning: String(verdict.reasoning || ""), evidence_needed: Array.isArray(verdict.evidence_needed) ? verdict.evidence_needed.map(String) : [], dissent: verdict.dissent || null, }, council: { model, seats, rounds: config.council_rounds, tokens_in: session.totals.tokens_in, tokens_out: session.totals.tokens_out, cost_usd: session.totals.cost_usd }, session: `verified/sessions/${id}.json`, deliberated_at: nowIso(), }; appendJsonl(path.join(DATA_ROOT, "verified", date, "verdicts.jsonl"), record); fs.writeFileSync(sessionFile, JSON.stringify(session, null, 1) + "\n"); console.log(`council verdict: ${record.status} (confidence ${verdict.confidence}) — $${session.totals.cost_usd.toFixed(4)}, session archived`);