W4.1+W4.2: anti-AI-tics house style + bureau nav (back/home everywhere)
Two complaints in one wave:
(W4.1) User: "Não pode ter vícios de IA como uso excessivo de '-' que a IA
coloca geralmente no lugar de vírgulas por exemplo. Isso deve fazer parte
do prompt geral."
- New prompts/_house-style.md banning the 9 most common AI prose tells
in both EN and PT-BR:
1. Em dashes as comma replacements (—)
2. Rule-of-three lists ("concrete, rigorous, and grounded")
3. Conjunctive openers ("Moreover", "Notably", "Ademais")
4. Superficial -ing analyses ("marking a shift", "destacando")
5. Inflated symbolism + AI vocab (tapestry, navigate, delve,
underscore, robust, multifaceted, marco histórico, ...)
6. Negative parallelisms ("Not just X but Y")
7. Vague attribution ("Some scholars say...")
8. Summary closers ("In summary...", "Em suma...")
9. Hedging fluff ("It's important to note...")
Verbatim chunk quotes are explicitly exempt; preserve as-is.
- claude.ts callClaude() lazily loads _house-style.md once per process
and PREPENDS it to every detective's system prompt:
composedSystem = houseStyle + "---" + detective.systemPrompt
This means all 7 detectives + future ones get the rules without any
per-prompt change.
(W4.2) User: "Quando entra em uma página da investigação não tem como
voltar! UX terrível!"
- New <BureauNav> sticky topbar with explicit "← home" + "🔎 bureau"
buttons + clickable breadcrumb trail. Always visible at the top of
every bureau page so the user can escape in one click.
- Wired into /bureau, /h/[hypothesisId], /c/[slug], /jobs/[id]. Each
page passes its sensible parent crumb (/bureau#hypotheses,
/bureau#reports, /bureau#jobs).
- Replaces the previous plain-text "disclosure.top / hypothesis /
H-0004" line which had no visual affordance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0a5c03c29a
commit
24f12a27f4
7 changed files with 232 additions and 32 deletions
117
investigator-runtime/prompts/_house-style.md
Normal file
117
investigator-runtime/prompts/_house-style.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# House style — bureau voice (mandatory)
|
||||
|
||||
This preamble is injected before every detective's system prompt. It applies
|
||||
to **all narrative prose you emit, in both EN and PT-BR**. Verbatim chunk
|
||||
quotes from the source corpus are exempt — preserve those as-is.
|
||||
|
||||
Your output will be read by an editor who will reject anything that smells
|
||||
of AI-generated prose. The rules below are the editor's red pen.
|
||||
|
||||
## 1. NO em dashes as comma replacements
|
||||
|
||||
Forbidden: "...sobre o Novo México — território que abrigava os programas..."
|
||||
Forbidden: "He was the director — a man of severe temperament — who..."
|
||||
|
||||
Use a comma. Or end the sentence and start a new one. If you genuinely need
|
||||
parentheses, use parentheses. Em dashes are allowed **only** for true range
|
||||
notation (e.g. `1948–1949`) and for verbatim quotes from the corpus that
|
||||
already contain them.
|
||||
|
||||
In practice: if you can replace the dash with a comma without changing the
|
||||
meaning, you should have used the comma.
|
||||
|
||||
## 2. NO rule-of-three lists
|
||||
|
||||
Forbidden: "concrete, quantitative, and grounded"
|
||||
Forbidden: "rigorous, methodical, and exhaustive"
|
||||
Forbidden: "uma análise cuidadosa, metódica e exaustiva"
|
||||
|
||||
Two adjectives is enough. Four is fine when you have four real items. The
|
||||
default three is filler — drop the weakest term.
|
||||
|
||||
## 3. NO conjunctive fluff at sentence starts
|
||||
|
||||
Forbidden openers: "Moreover", "Furthermore", "Notably", "Importantly",
|
||||
"It is worth noting", "It should be mentioned", "Crucially", "Indeed",
|
||||
"Ademais", "Além disso", "Vale destacar", "É importante notar",
|
||||
"Notadamente", "Cumpre observar".
|
||||
|
||||
Start the sentence with the content. If the link is real, use plain "But",
|
||||
"And", "However", "So" — sparingly.
|
||||
|
||||
## 4. NO superficial -ing analyses
|
||||
|
||||
Forbidden: "marking a shift in policy", "highlighting the agency's concern",
|
||||
"reflecting a deeper anxiety", "underscoring the importance",
|
||||
"demonstrating the scale", "marcando uma mudança", "destacando a
|
||||
preocupação", "refletindo uma ansiedade", "sublinhando a importância".
|
||||
|
||||
State the conclusion as a finite verb. "The agency changed its policy."
|
||||
Not "the document, marking a shift in policy, ...". The -ing tail is
|
||||
almost always filler that hedges the claim into mush.
|
||||
|
||||
## 5. NO inflated symbolism / promotional adjectives
|
||||
|
||||
Forbidden: "stands as a testament", "a beacon of", "speaks volumes",
|
||||
"watershed moment", "innovative", "groundbreaking", "remarkable",
|
||||
"unprecedented" (unless the corpus literally uses it), "robust",
|
||||
"comprehensive", "multifaceted", "nuanced", "rich tapestry", "complex
|
||||
landscape", "navigate the complexities", "leverage", "delve into",
|
||||
"shed light on", "paints a picture", "extensive", "myriad".
|
||||
|
||||
PT-BR equivalents also forbidden: "marco histórico", "verdadeira riqueza",
|
||||
"complexa tapeçaria", "panorama complexo", "navegar pelas complexidades",
|
||||
"lança luz sobre", "demonstra de forma robusta", "abrangente",
|
||||
"multifacetado", "pinta um quadro".
|
||||
|
||||
Show, don't characterize. If a result is remarkable, the reader will see
|
||||
that from the evidence — you don't need the adjective.
|
||||
|
||||
## 6. NO negative parallelisms
|
||||
|
||||
Forbidden: "Not just X, but Y." / "It's not X, it's Y."
|
||||
Forbidden: "Não apenas X, mas Y." / "Não se trata de X, mas de Y."
|
||||
|
||||
Just say Y. The negation of X is rhetorical scaffolding the editor will
|
||||
delete.
|
||||
|
||||
## 7. NO vague attribution
|
||||
|
||||
Forbidden: "Some scholars argue...", "Many believe...", "It is widely
|
||||
held...", "Críticos argumentam...", "Muitos sustentam...".
|
||||
|
||||
Cite the chunk: `[[doc-id/pNNN#cNNNN]]`. If no chunk supports the
|
||||
attribution, you don't get to make the claim.
|
||||
|
||||
## 8. NO summary closers
|
||||
|
||||
Forbidden last sentences: "In summary...", "In conclusion...",
|
||||
"Ultimately...", "Em suma...", "Em última análise...", "Em conclusão...",
|
||||
"Resumindo...".
|
||||
|
||||
End on the last substantive sentence. The reader doesn't need to be told
|
||||
the section is ending.
|
||||
|
||||
## 9. NO hedging fluff (separate from calibrated confidence_band)
|
||||
|
||||
Forbidden: "It's important to note that...", "It bears mentioning...",
|
||||
"Of course...", "Naturally...", "Cabe ressaltar que...", "Naturalmente...",
|
||||
"É claro que...".
|
||||
|
||||
The hedging you ARE allowed: posterior probability + Tetlock band, and
|
||||
explicit `[no evidence in corpus]` markers. Anything else hedge-shaped is
|
||||
filler.
|
||||
|
||||
---
|
||||
|
||||
## Quick self-check before emitting
|
||||
|
||||
Read your draft and ask:
|
||||
- Did I use any em dash that could be a comma? Replace it.
|
||||
- Did I write any list of exactly three items? Drop the weakest one.
|
||||
- Did I start any sentence with "Moreover/Notably/Furthermore"? Cut it.
|
||||
- Did I use any word from the forbidden list? Find a plain alternative.
|
||||
- Did I write "in summary" / "em suma"? Delete that sentence.
|
||||
|
||||
The bureau's voice is **plainspoken investigative**. Like a senior detective
|
||||
reporting facts to a colleague, not like a Wikipedia introduction.
|
||||
|
|
@ -7,8 +7,28 @@
|
|||
* so the orchestrator can enforce the per-job budget cap.
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { env } from "./env";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const HOUSE_STYLE_PATH = path.resolve(HERE, "..", "..", "prompts", "_house-style.md");
|
||||
|
||||
// Lazy-load the house style once per process. Detective system prompts get
|
||||
// this preamble prepended automatically (the bureau's anti-AI-tics rules:
|
||||
// no em dash as comma, no rule-of-three, no "Moreover", no inflated vocab).
|
||||
let _houseStyleCache: string | null | undefined;
|
||||
async function loadHouseStyle(): Promise<string | null> {
|
||||
if (_houseStyleCache !== undefined) return _houseStyleCache;
|
||||
try {
|
||||
_houseStyleCache = await readFile(HOUSE_STYLE_PATH, "utf-8");
|
||||
} catch {
|
||||
_houseStyleCache = null;
|
||||
}
|
||||
return _houseStyleCache;
|
||||
}
|
||||
|
||||
export interface ClaudeCallArgs {
|
||||
/** Free-form prompt body to send. */
|
||||
prompt: string;
|
||||
|
|
@ -69,8 +89,15 @@ export async function callClaude(args: ClaudeCallArgs): Promise<ClaudeCallResult
|
|||
// long multi-line content reliably (the CLI complained "Input must be
|
||||
// provided either through stdin or as a prompt argument when using --print"
|
||||
// for prompts past a few KB). Stdin is unambiguous.
|
||||
const fullPrompt = args.systemPrompt
|
||||
? `${args.systemPrompt}\n\n---\n\n${args.prompt}`
|
||||
// Compose system prompt: house style (anti-AI-tics) + detective persona.
|
||||
// House style goes FIRST so the model treats it as a hard constraint that
|
||||
// wraps the detective's discipline.
|
||||
const houseStyle = await loadHouseStyle();
|
||||
const composedSystem = [houseStyle, args.systemPrompt]
|
||||
.filter((s): s is string => typeof s === "string" && s.trim().length > 0)
|
||||
.join("\n\n---\n\n");
|
||||
const fullPrompt = composedSystem
|
||||
? `${composedSystem}\n\n---\n\n${args.prompt}`
|
||||
: args.prompt;
|
||||
|
||||
const t0 = Date.now();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import Link from "next/link";
|
||||
import { pgQuery } from "@/lib/retrieval/db";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
|
@ -141,13 +142,9 @@ export default async function BureauPage() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav crumbs={[{ label: "bureau" }]} />
|
||||
<AuthBar />
|
||||
<div className="mx-auto max-w-6xl px-4 py-8 pt-16">
|
||||
<div className="text-[11px] text-[#5a6678] font-mono mb-2">
|
||||
<Link href="/" className="hover:text-[#7fdbff]">disclosure.top</Link>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="text-[#e0c080]">bureau</span>
|
||||
</div>
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 pt-4">
|
||||
|
||||
<header className="mb-8 border-b border-[rgba(224,192,128,0.32)] pb-4">
|
||||
<h1 className="font-mono text-3xl text-[#e0c080]">▍ The Investigation Bureau</h1>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { readFile } from "node:fs/promises";
|
|||
import path from "node:path";
|
||||
import { MarkdownBody } from "@/components/markdown-body";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
|
@ -74,15 +75,13 @@ export default async function CaseReportPage({
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav crumbs={[
|
||||
{ label: "bureau", href: "/bureau" },
|
||||
{ label: "reports", href: "/bureau#reports" },
|
||||
{ label: slug },
|
||||
]} />
|
||||
<AuthBar />
|
||||
<div className="mx-auto max-w-3xl px-4 py-8 pt-16">
|
||||
<div className="text-[11px] text-[#5a6678] font-mono mb-2">
|
||||
<Link href="/" className="hover:text-[#7fdbff]">disclosure.top</Link>
|
||||
<span className="mx-1">/</span>
|
||||
<span>case-report</span>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="text-[#e0c080]">{slug}</span>
|
||||
</div>
|
||||
<div className="mx-auto max-w-3xl px-4 py-6 pt-4">
|
||||
|
||||
<div className="rounded-lg border border-[rgba(224,192,128,0.18)] bg-gradient-to-br from-[rgba(224,192,128,0.06)] to-transparent p-4 mb-6">
|
||||
<div className="text-[10px] font-mono text-[#5a6678] uppercase mb-2">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { readFile } from "node:fs/promises";
|
|||
import path from "node:path";
|
||||
import { pgQuery } from "@/lib/retrieval/db";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { RedTeamRequestButton } from "@/components/red-team-request-button";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
|
@ -196,15 +197,13 @@ export default async function HypothesisPage({
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav crumbs={[
|
||||
{ label: "bureau", href: "/bureau" },
|
||||
{ label: "hypotheses", href: "/bureau#hypotheses" },
|
||||
{ label: hypothesisId },
|
||||
]} />
|
||||
<AuthBar />
|
||||
<div className="mx-auto max-w-5xl px-4 py-8 pt-16">
|
||||
<div className="text-[11px] text-[#5a6678] font-mono mb-2">
|
||||
<Link href="/" className="hover:text-[#7fdbff]">disclosure.top</Link>
|
||||
<span className="mx-1">/</span>
|
||||
<span>hypothesis</span>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="text-[#7fdbff]">{hypothesisId}</span>
|
||||
</div>
|
||||
<div className="mx-auto max-w-5xl px-4 py-6 pt-4">
|
||||
|
||||
{/* Header */}
|
||||
<div className="rounded-lg border border-[rgba(127,219,255,0.18)] bg-gradient-to-br from-[rgba(127,219,255,0.08)] to-transparent p-5">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { notFound } from "next/navigation";
|
|||
import Link from "next/link";
|
||||
import { pgQuery } from "@/lib/retrieval/db";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { JobStatusPoller } from "@/components/job-status-poller";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
|
@ -109,15 +110,13 @@ export default async function JobPage({
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav crumbs={[
|
||||
{ label: "bureau", href: "/bureau" },
|
||||
{ label: "jobs", href: "/bureau#jobs" },
|
||||
{ label: job.job_id.slice(0, 8) },
|
||||
]} />
|
||||
<AuthBar />
|
||||
<div className="mx-auto max-w-5xl px-4 py-8 pt-16">
|
||||
<div className="text-[11px] text-[#5a6678] font-mono mb-2">
|
||||
<Link href="/" className="hover:text-[#7fdbff]">disclosure.top</Link>
|
||||
<span className="mx-1">/</span>
|
||||
<span>investigation</span>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="text-[#7fdbff]">{job.job_id.slice(0, 8)}</span>
|
||||
</div>
|
||||
<div className="mx-auto max-w-5xl px-4 py-6 pt-4">
|
||||
|
||||
<div className={`rounded-lg border border-[rgba(127,219,255,0.18)] bg-gradient-to-br ${detectiveBg} to-transparent p-5`}>
|
||||
<div className="flex items-baseline justify-between gap-4 flex-wrap">
|
||||
|
|
|
|||
62
web/components/bureau-nav.tsx
Normal file
62
web/components/bureau-nav.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* BureauNav — top navigation bar present on every bureau page.
|
||||
*
|
||||
* Solves "I'm stuck on this page, there's no way back" UX. Shows:
|
||||
* - "← Home" (always → /)
|
||||
* - "🔎 Bureau" (always → /bureau)
|
||||
* - Breadcrumb trail of current page (passed via props)
|
||||
*
|
||||
* Server component. No client-side history needed because the parent links
|
||||
* are deterministic (every bureau page has a known parent in the hierarchy).
|
||||
*/
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Home } from "lucide-react";
|
||||
|
||||
export interface Crumb {
|
||||
/** Display label. */
|
||||
label: string;
|
||||
/** When omitted the crumb is rendered as the current page (no link). */
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function BureauNav({ crumbs }: { crumbs: Crumb[] }) {
|
||||
return (
|
||||
<nav className="sticky top-0 z-30 bg-[#0a0e1a]/95 backdrop-blur border-b border-[rgba(224,192,128,0.18)]">
|
||||
<div className="mx-auto max-w-6xl px-4 py-2 flex items-center gap-3 flex-wrap text-[12px] font-mono">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-[rgba(127,219,255,0.30)] text-[#7fdbff] hover:bg-[rgba(127,219,255,0.08)]"
|
||||
aria-label="Voltar para a home"
|
||||
>
|
||||
<ArrowLeft size={12} />
|
||||
<Home size={12} />
|
||||
<span>home</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/bureau"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-[#e0c080] text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)]"
|
||||
aria-label="Hub do bureau"
|
||||
>
|
||||
🔎 <span>bureau</span>
|
||||
</Link>
|
||||
|
||||
{crumbs.length > 0 && (
|
||||
<div className="text-[11px] text-[#5a6678] flex items-center gap-1.5 flex-wrap">
|
||||
<span className="mx-1">/</span>
|
||||
{crumbs.map((c, i) => (
|
||||
<span key={`${c.label}-${i}`} className="inline-flex items-center gap-1.5">
|
||||
{c.href ? (
|
||||
<Link href={c.href} className="hover:text-[#7fdbff]">{c.label}</Link>
|
||||
) : (
|
||||
<span className="text-[#e7ecf3]">{c.label}</span>
|
||||
)}
|
||||
{i < crumbs.length - 1 && <span>/</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue