Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@ next-env.d.ts
.mcp.json

# DB Backups
db-backups
db-backups

# Eval run artifacts
/evals/results
52 changes: 52 additions & 0 deletions evals/golden.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Golden dataset for bill-analysis evals.
*
* Each entry pins human-agreed expectations for a fixture bill. Reuses the XML
* fixtures already in tests/fixtures/, so adding a bill is just: drop its XML in
* tests/fixtures/, add an entry here. `expectedJudgment`/`expectedSocialIssue`
* are best-effort ground truth — adjust them as the team's view settles.
*/
export interface GoldenBill {
/** Fixture file basename in tests/fixtures (without .xml). */
fixture: string;
billId: string;
/** Expected final judgment, or "any" when reasonable people could differ. */
expectedJudgment: "yes" | "no" | "abstain" | "any";
expectedSocialIssue: "yes" | "no";
/** Lowercased substrings the summary must contain. */
mustMention: string[];
}

export const GOLDEN_BILLS: GoldenBill[] = [
{
fixture: "s-230",
billId: "S-230",
// Creates a new national-strategy reporting obligation (adds process).
expectedJudgment: "any",
expectedSocialIssue: "no",
mustMention: ["soil"],
},
{
fixture: "s-231",
billId: "S-231",
expectedJudgment: "any",
expectedSocialIssue: "no",
mustMention: ["corporation"],
},
{
fixture: "c-208",
billId: "C-208",
// Eases intergenerational small-business/farm transfers via the tax code.
expectedJudgment: "yes",
expectedSocialIssue: "no",
mustMention: ["tax"],
},
{
fixture: "c-206",
billId: "C-206",
// Carbon-pricing exemption for farmers — reduces a cost on producers.
expectedJudgment: "any",
expectedSocialIssue: "no",
mustMention: ["carbon"],
},
];
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"prompt": "tsx src/scripts/copy-bill-prompt.ts",
"eval": "tsx src/scripts/run-evals.ts",
"prepare": "husky"
},
"dependencies": {
Expand Down Expand Up @@ -72,7 +73,8 @@
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"vaul": "^1.1.2"
"vaul": "^1.1.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
Expand Down
13 changes: 11 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions src/app/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,6 @@ export default async function EditBillPage({ params }: Params) {
className="w-full min-h-32 border rounded p-2"
/>
</div>
<div className="space-y-2">
<label
className="block text-sm font-medium"
htmlFor="missing_details"
>
Missing Details (comma-separated)
</label>
<textarea
id="missing_details"
name="missing_details"
defaultValue={(bill.missing_details || []).join(", ")}
className="w-full min-h-20 border rounded p-2"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium" htmlFor="genres">
Genres (comma-separated)
Expand Down
48 changes: 9 additions & 39 deletions src/app/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import Link from "next/link";
import { getBillByIdFromDB } from "@/server/get-bill-by-id-from-db";
import { getBillFromCivicsProjectApi } from "@/services/billApi";
import {
fromBuildCanadaDbBill,
fromCivicsProjectApiBill,
type UnifiedBill,
} from "@/utils/billConverters";
import { getUnifiedBillByIdWithApiFallback } from "@/server/get-unified-bill-by-id";
import type { Metadata, ResolvingMetadata } from "next";
import { headers } from "next/headers";
import { env } from "@/env";
import {
BillHeader,
Expand All @@ -18,8 +11,7 @@ import {
} from "@/components/BillDetail";
import { BillQuestions } from "@/components/BillDetail/BillQuestions";
import { Separator } from "@/components/ui/separator";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { EditBillLink } from "@/components/BillDetail/EditBillLink";
import { BillTenets } from "@/components/BillDetail/BillTenets";
import { JudgementValue } from "@/components/Judgement/judgement.component";
import { buildAbsoluteUrl, buildRelativePath } from "@/utils/basePath";
Expand All @@ -40,29 +32,12 @@ interface Params {
export default async function BillDetail({ params }: Params) {
const { id } = await params;

const session = await getServerSession(authOptions);
const headerList = await headers();
const host =
headerList.get("x-forwarded-host") || headerList.get("host") || "";
const proto = (headerList.get("x-forwarded-proto") || "https").split(",")[0];
const requestOrigin = host ? `${proto}://${host}` : "";
const origin = env.NEXT_PUBLIC_APP_URL || requestOrigin;
const shareOrigin =
env.NODE_ENV === "production"
? BUILD_CANADA_URL
: origin || BUILD_CANADA_URL;
// Try database first, then fallback to API
const dbBill = await getBillByIdFromDB(id);
let unifiedBill: UnifiedBill | null = null;

if (dbBill) {
unifiedBill = fromBuildCanadaDbBill(dbBill);
} else {
const apiBill = await getBillFromCivicsProjectApi(id);
if (apiBill) {
unifiedBill = await fromCivicsProjectApiBill(apiBill);
}
}
: env.NEXT_PUBLIC_APP_URL || BUILD_CANADA_URL;
// Try database first, then fall back to the Civics Project API
const unifiedBill = await getUnifiedBillByIdWithApiFallback(id);

if (!unifiedBill) {
return (
Expand All @@ -89,11 +64,7 @@ export default async function BillDetail({ params }: Params) {
<Link href="/" className="text-sm underline mb-6">
← Back to bills
</Link>
{session?.user && (
<Link href={`/${id}/edit`} className="ml-4 text-sm underline">
Edit
</Link>
)}
<EditBillLink id={id} />
</div>
<BillHeader bill={unifiedBill} />

Expand Down Expand Up @@ -154,13 +125,12 @@ export async function generateMetadata(
const q = sp?.q;
const title = id;
const description = `Bill ${id} analysis and judgement`;
const h = headers();
const host = (await h).get("x-forwarded-host") || (await h).get("host") || "";
const proto = ((await h).get("x-forwarded-proto") || "https").split(",")[0];
// Ensure we always have a base URL for absolute image URLs (required for Twitter Cards)
const baseUrl =
env.NEXT_PUBLIC_APP_URL ||
(host ? `${proto}://${host}` : "http://localhost:3000");
(env.NODE_ENV === "production"
? BUILD_CANADA_URL
: "http://localhost:3000");
const pagePath = buildRelativePath(id);
const pageUrl = `${baseUrl}${pagePath}`;
const pageUrlWithQuery = q
Expand Down
15 changes: 12 additions & 3 deletions src/app/api/[id]/reprocess/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,17 @@ export async function POST(
);
}

const analysis = await summarizeBillText(markdown);
let analysis: Awaited<ReturnType<typeof summarizeBillText>>;
try {
analysis = await summarizeBillText(markdown);
} catch (error) {
// Never overwrite a good stored analysis with a failure stub.
console.error(`Reprocess ${id}: AI analysis failed`, error);
return NextResponse.json(
{ error: "AI analysis failed; existing analysis left unchanged" },
{ status: 502 },
);
}

const latestStageDate =
apiBill.stages && apiBill.stages.length > 0
Expand Down Expand Up @@ -123,10 +133,9 @@ export async function POST(
tenet_evaluations: analysis.tenet_evaluations,
final_judgment: analysis.final_judgment,
rationale: analysis.rationale,
needs_more_info: analysis.needs_more_info,
missing_details: analysis.missing_details,
steel_man: analysis.steel_man,
question_period_questions: analysis.question_period_questions ?? [],
isSocialIssue: analysis.is_social_issue === "yes",
lastUpdatedOn: new Date(latestStageDate),
},
},
Expand Down
23 changes: 0 additions & 23 deletions src/app/api/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,8 @@ export async function POST(
let final_judgment: string | undefined;
let rationale: string | undefined;
let steel_man: string | undefined;
let missing_details_input: unknown;
let genres_input: unknown;
let question_period_questions_input: unknown;
let hasMissingDetails = false;
let hasGenres = false;
let hasQuestionPeriodQuestions = false;
let tenet_ids: string[] = [];
Expand All @@ -53,10 +51,6 @@ export async function POST(
final_judgment = params.get("final_judgment") || undefined;
rationale = params.get("rationale") || undefined;
steel_man = params.get("steel_man") || undefined;
if (params.has("missing_details")) {
hasMissingDetails = true;
missing_details_input = params.get("missing_details") || "";
}
if (params.has("genres")) {
hasGenres = true;
genres_input = params.get("genres") || "";
Expand All @@ -80,10 +74,6 @@ export async function POST(
final_judgment = asString(json.final_judgment);
rationale = asString(json.rationale);
steel_man = asString(json.steel_man);
if ("missing_details" in json) {
hasMissingDetails = true;
missing_details_input = (json as any).missing_details;
}
if ("genres" in json) {
hasGenres = true;
genres_input = (json as any).genres;
Expand Down Expand Up @@ -115,11 +105,6 @@ export async function POST(
(form.get("final_judgment") as string | null) || undefined;
rationale = (form.get("rationale") as string | null) || undefined;
steel_man = (form.get("steel_man") as string | null) || undefined;
if (typeof form.has === "function" && form.has("missing_details")) {
hasMissingDetails = true;
missing_details_input =
(form.get("missing_details") as string | null) || "";
}
if (typeof form.has === "function" && form.has("genres")) {
hasGenres = true;
genres_input = (form.get("genres") as string | null) || "";
Expand Down Expand Up @@ -259,11 +244,6 @@ export async function POST(
return questions.map((question) => ({ question }));
};

let missing_details: string[] | undefined;
if (hasMissingDetails) {
missing_details = parseCommaSeparated(missing_details_input);
}

let genres: string[] | undefined;
if (hasGenres) {
genres = parseCommaSeparated(genres_input);
Expand All @@ -285,9 +265,6 @@ export async function POST(
if (final_judgment !== undefined) update.final_judgment = final_judgment;
if (rationale !== undefined) update.rationale = rationale;
if (steel_man !== undefined) update.steel_man = steel_man;
if (missing_details !== undefined) {
update.missing_details = missing_details;
}
if (genres !== undefined) {
update.genres = genres;
}
Expand Down
8 changes: 0 additions & 8 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { BillSummary } from "./types";
import BillExplorer from "./BillExplorer";
import { getAllBillsFromDB } from "@/server/get-all-bills-from-db";
import { fromBuildCanadaDbBill } from "@/utils/billConverters";
import { getParliament45Header } from "@/components/BillDetail/BillHeader";
import Markdown from "react-markdown";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { env } from "@/env";
Expand Down Expand Up @@ -137,8 +135,6 @@ async function getMergedBills(): Promise<BillSummary[]> {
isSocialIssue: dbBill.isSocialIssue,
final_judgment: dbBill.final_judgment as BillSummary["final_judgment"],
rationale: dbBill.rationale,
needs_more_info: dbBill.needs_more_info,
missing_details: dbBill.missing_details,
genres: dbBill.genres,
parliamentNumber: dbBill.parliamentNumber,
sessionNumber: dbBill.sessionNumber,
Expand Down Expand Up @@ -173,8 +169,6 @@ async function getMergedBills(): Promise<BillSummary[]> {
isSocialIssue: dbBill.isSocialIssue,
final_judgment: dbBill.final_judgment as BillSummary["final_judgment"],
rationale: dbBill.rationale,
needs_more_info: dbBill.needs_more_info,
missing_details: dbBill.missing_details,
genres: dbBill.genres,
parliamentNumber: dbBill.parliamentNumber,
sessionNumber: dbBill.sessionNumber,
Expand Down Expand Up @@ -225,8 +219,6 @@ export default async function Home({
<FAQModalTrigger />
</header>

<Markdown>{getParliament45Header()}</Markdown>

<section className="mt-6">
<BillExplorer bills={bills} />
</section>
Expand Down
2 changes: 0 additions & 2 deletions src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ export interface BillSummary {
summary?: string;
final_judgment?: "yes" | "no" | "abstain";
rationale?: string;
needs_more_info?: boolean;
missing_details?: string[];
genres?: string[];
tenet_evaluations?: Array<{
id?: number;
Expand Down
Loading
Loading