Closes a UX gap the user surfaced: W3.5-3.8 built 8 detectives, 4 new
URL endpoints (/jobs/[id], /h/[id], /c/[slug], /api/h/[id]/red-team)
and a chat tool, but the homepage was unchanged — the bureau was
invisible unless you knew the URL or asked the chat to invoke
request_investigation.
Homepage (web/app/page.tsx):
- Title `▍ war.gov/ufo — Investigative Wiki` → `▍ The Disclosure Bureau`
- Subtitle expanded from "Holmes · Poirot · Dupin · Locard" to all 8
detectives (Holmes · Locard · Dupin · Schneier · Poirot · Taleb ·
Tetlock · Case-Writer)
- New `🔎 bureau` topbar link (gold, between graph/stats and batch)
- BureauSnapshot inserted right after the header
BureauSnapshot (web/components/bureau-snapshot.tsx) — server component:
- 8 detective tiles with role labels (each in its tone color)
- 6 clickable counters (evidence / hypotheses / contradictions /
witnesses / outliers / case reports) — anchor to /bureau#section
- 6 "recent artefacts" columns surfacing the last 3-4 of each kind:
hypotheses with prior→posterior + band + ↳reviewed_by marker,
contradictions with topic + resolution_status, evidence with
Grade badge + verbatim quote, outliers with title + scope.kind,
witness analyses with canonical_name + credibility + verdict,
case reports with slug + link to /c/<slug>
- "Recent jobs" strip linking to /jobs/[id] color-coded by status
- Reports read from /data/ufo/case/reports/ via fs.readdir + stat,
sorted by mtime — no DB round-trip needed for that section
/bureau (web/app/bureau/page.tsx) — full hub:
- Header with full counts
- 7 sections (anchored to homepage counter links): Case reports,
Hypotheses, Evidence, Contradictions, Outliers, Witnesses,
Recent jobs table — each rendering up to 100 rows
- Reports section parses frontmatter from each .md to surface topic
+ n_hypotheses + n_evidence on the card
Runtime fixes batched in:
- Poirot: coerce entity_pk via Number() — node-postgres returns
BIGINT as string by default; writer's Number.isFinite() rejected
it as "person_entity_pk required" (j-edgar-hoover retry path)
- Tetlock: write_calibration rationale cap 600 → 1200 chars. Prompt
still asks ≤ 600 but a 2× slack beats failing the job on honest
analysis. Observed live: Tetlock emitted ~620 chars on H-0003 and
the writer rejected the entire calibration.
- Case-Writer: Promise.all of 5 queries × max_parallel=2 jobs
demanded up to 10 connections against the investigator role's
rolconnlimit=4 → "too many connections for role investigator".
Sequentialized — the LLM call is the hot path, not these queries.
Smoke results visible now on the homepage:
- 3 hypotheses (H-0001/2/3) about green fireballs origin
- 3 contradictions (R-0001/2/3) about color, geographic confinement,
exclusive-green vs multicolored
- 2 evidence cards (E-0002/3) Grade B
- 3 outliers (G-0001/2/3) — including Taleb's deliberate
meteor-shower-camouflage flag
- 1 case report at /c/green-fireballs-sandia (Watson 13.4 KB,
five-act narrative, fully cited)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| app | ||
| components | ||
| lib | ||
| .dockerignore | ||
| .env.local.example | ||
| Dockerfile | ||
| instrumentation.ts | ||
| middleware.ts | ||
| next-env.d.ts | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| README.md | ||
| sentry.client.config.ts | ||
| sentry.server.config.ts | ||
| tailwind.config.ts | ||
| tsconfig.json | ||
web — Disclosure Bureau Next.js app
Next.js 15 + React 19 + Tailwind + Supabase + assistant-ui.
Quick start (local dev)
# 1. Install deps
npm install
# 2. (Optional) Start local Supabase
# Requires Docker. Skip if pointing at remote Supabase.
npx supabase init # first time only — creates supabase/ folder
npx supabase start # spins up Postgres/GoTrue/Storage on :54321
# 3. Configure env
cp .env.local.example .env.local
# Edit .env.local — paste local Supabase keys (printed by `supabase start`)
# 4. Apply migrations
psql postgresql://postgres:postgres@localhost:54322/postgres \
-f ../infra/supabase/migrations/0001_chat_schema.sql
# 5. Start dev
npm run dev
# http://localhost:3030
Without Supabase
The app degrades gracefully if Supabase env vars are unset:
- Wiki browsing works (read-only from filesystem)
- Auth bar shows "auth: disabled (dev)"
- Chat bubble shows "Auth not configured"
Useful for quick UI work without spinning up Docker.
Production (Coolify on VPS)
See ../infra/coolify/. Stack:
- Coolify orchestrates everything
- Supabase self-hosted:
db.disclosure.top,studio.disclosure.top - Next.js:
disclosure.top - Meilisearch (shared):
search.disclosure.top - Imgproxy (shared):
img.disclosure.top - Caddy: TLS + reverse proxy (built into Coolify)
Architecture
app/
├── page.tsx # home — 116 docs grouped by collection
├── auth/
│ ├── signin/page.tsx # magic-link form
│ ├── callback/route.ts # exchanges code for session
│ └── signout/route.ts
├── d/[docId]/
│ ├── page.tsx # doc detail
│ └── [page]/page.tsx # page reader (OCR + entity highlights + crops + sidebar PNG)
├── api/
│ ├── me/route.ts # GET current profile
│ ├── sessions/route.ts # GET list, POST new
│ ├── sessions/[id]/route.ts # GET detail, PATCH, DELETE
│ ├── sessions/[id]/messages/route.ts # POST send → assistant reply
│ ├── documents/, pages/, entities/, tables/ # read-only data
│ └── static/[...path]/route.ts # sandboxed file serve
components/
├── chat-bubble.tsx # floating Sherlock — auth-aware, session list
├── entity-modal.tsx # opens on entity click
├── reader-content.tsx # OCR + highlights + crops
└── auth-bar.tsx # sign in / out + budget tracker
lib/
├── wiki.ts # markdown reader (gray-matter)
├── entity-index.ts # match loader + text segmentation
└── supabase/{server,client}.ts # SSR helpers
middleware.ts # session refresh on every request
Tech notes
- No RAG: chat agent reads markdown directly. Wiki-link traversal substitutes for vector search.
- RLS-first: Supabase Row Level Security enforces "user sees only own sessions" at the DB layer.
- Magic-link auth: no passwords. GoTrue handles email delivery.
- Anti-abuse: per-user budget cap (default $5) + daily message quota (default 100) enforced via
check_budgetRPC before each Claude call.
Cost
Each chat turn costs ~$0.005-0.05 depending on context size (mostly Haiku $1/M input, $5/M output).