diff --git a/src/utils/billConverters.ts b/src/utils/billConverters.ts deleted file mode 100644 index d4f0e4c..0000000 --- a/src/utils/billConverters.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { BillDocument } from "@/models/Bill"; -import type { ApiBillDetail } from "@/services/billApi"; -import { - summarizeBillText, - fetchBillMarkdown, - onBillNotInDatabase, - type BillAnalysis, -} from "@/services/billApi"; -import { socialIssueGrader } from "@/services/social-issue-grader"; - -// Unified bill data structure -export interface UnifiedBill { - billId: string; - title: string; - short_title?: string; - summary: string; - status: string; - sponsorParty?: string; - chamber?: string; - supportedRegion?: string; - introducedOn?: Date; - lastUpdatedOn?: Date; - stages: { - stage: string; - state: string; - house: string; - date: Date; - }[]; - genres?: string[]; - parliamentNumber?: number; - sessionNumber?: number; - votes?: Array<{ motion?: string; result: string }>; - fullTextMarkdown?: string | null; - isSocialIssue?: boolean; - question_period_questions?: Array<{ question: string }>; - // Analysis data from AI - tenet_evaluations?: Array<{ - id: number; - title: string; - alignment: "aligns" | "conflicts" | "neutral"; - explanation: string; - }>; - final_judgment: "yes" | "no" | "abstain"; - rationale?: string; - needs_more_info?: boolean; - missing_details?: string[]; - steel_man?: string; -} - -// Convert Build Canada DB bill to unified format -export function fromBuildCanadaDbBill(bill: BillDocument): UnifiedBill { - return { - billId: bill.billId, - title: bill.title, - short_title: bill.short_title, - summary: bill.summary, - status: bill.status, - sponsorParty: bill.sponsorParty, - chamber: bill.chamber, - supportedRegion: bill.supportedRegion, - introducedOn: bill.introducedOn, - lastUpdatedOn: bill.lastUpdatedOn, - stages: bill.stages - ? bill.stages.map((stage) => ({ - stage: stage.stage, - state: stage.state, - house: stage.house, - date: stage.date, - })) - : [], - genres: bill.genres ? [...bill.genres] : undefined, - parliamentNumber: bill.parliamentNumber, - sessionNumber: bill.sessionNumber, - votes: bill.votes?.map((v) => ({ - motion: v.motion, - result: v.result, - })), - isSocialIssue: bill.isSocialIssue, - // Properly serialize question_period_questions to remove MongoDB ObjectIds - question_period_questions: bill.question_period_questions?.map((q) => ({ - question: q.question, - })), - // Include analysis data - ensure proper serialization - tenet_evaluations: bill.tenet_evaluations?.map((te) => ({ - id: te.id, - title: te.title, - alignment: te.alignment, - explanation: te.explanation, - })), - final_judgment: - (bill.final_judgment ?? "").toString().trim().toLowerCase() === "yes" || - (bill.final_judgment ?? "").toString().trim().toLowerCase() === "no" - ? (bill.final_judgment as "yes" | "no") - : "abstain", - rationale: bill.rationale, - needs_more_info: bill.needs_more_info, - missing_details: bill.missing_details - ? [...bill.missing_details] - : undefined, - steel_man: bill.steel_man, - }; -} - -// Convert Civics Project API bill to unified format -export async function fromCivicsProjectApiBill( - bill: ApiBillDetail, -): Promise { - const { env } = await import("@/env"); - const uri = env.MONGO_URI || ""; - const hasValidMongoUri = - uri.startsWith("mongodb://") || uri.startsWith("mongodb+srv://"); - let existingBill: BillDocument | null = null; - const latestStageDate = - bill.stages && bill.stages.length > 0 - ? bill.stages[bill.stages.length - 1].date - : (bill.updatedAt ?? bill.date); - const house = - bill.stages && bill.stages.length > 0 - ? bill.stages[bill.stages.length - 1].house - : undefined; - - let billMarkdown: string | null = null; - - const latestBillSource = - bill.source || (bill.billTexts?.[0] as { url?: string })?.url; - - if (latestBillSource) { - billMarkdown = await fetchBillMarkdown(latestBillSource); - } - - // Check if we need to regenerate summary based on source changes from Civics Project API - let analysis: BillAnalysis = { - summary: bill.header || "", - tenet_evaluations: [ - { - id: 1, - title: "Canada should aim to be the world's most prosperous country", - alignment: "neutral", - explanation: "Not analyzed", - }, - { - id: 2, - title: - "Promote economic freedom, ambition, and breaking from bureaucratic inertia", - alignment: "neutral", - explanation: "Not analyzed", - }, - { - id: 3, - title: "Drive national productivity and global competitiveness", - alignment: "neutral", - explanation: "Not analyzed", - }, - { - id: 4, - title: "Grow exports of Canadian products and resources", - alignment: "neutral", - explanation: "Not analyzed", - }, - { - id: 5, - title: "Encourage investment, innovation, and resource development", - alignment: "neutral", - explanation: "Not analyzed", - }, - { - id: 6, - title: - "Deliver better public services at lower cost (government efficiency)", - alignment: "neutral", - explanation: "Not analyzed", - }, - { - id: 7, - title: "Reform taxes to incentivize work, risk-taking, and innovation", - alignment: "neutral", - explanation: "Not analyzed", - }, - { - id: 8, - title: "Focus on large-scale prosperity, not incrementalism", - alignment: "neutral", - explanation: "Not analyzed", - }, - ], - final_judgment: "no", - rationale: undefined, - needs_more_info: false, - missing_details: [], - steel_man: "Not analyzed", - }; - - // Check existing bill in database to see if source changed - try { - const { connectToDatabase } = await import("@/lib/mongoose"); - const { Bill } = await import("@/models/Bill"); - - if (hasValidMongoUri) { - await connectToDatabase(); - existingBill = (await Bill.findOne({ billId: bill.billID }) - .lean() - .exec()) as BillDocument | null; - - if (existingBill && !analysis.rationale) { - // Source hasn't changed, use existing analysis - analysis = { - summary: existingBill.summary, - tenet_evaluations: - existingBill.tenet_evaluations || analysis.tenet_evaluations, - final_judgment: (() => { - const raw = String(existingBill.final_judgment || "") - .trim() - .toLowerCase(); - if (raw === "yes" || raw === "no") return raw as "yes" | "no"; - // Treat legacy "neutral" and any unknown value as "abstain" - if (raw === "abstain" || raw === "neutral") return "abstain"; - return analysis.final_judgment; - })(), - rationale: existingBill.rationale || analysis.rationale, - needs_more_info: - existingBill.needs_more_info || analysis.needs_more_info, - missing_details: - existingBill.missing_details || analysis.missing_details, - steel_man: existingBill.steel_man || analysis.steel_man, - }; - console.log( - `Using existing analysis for ${bill.billID} (source unchanged)`, - ); - } - } - } catch (error) { - console.error("Error checking existing bill:", error); - // Continue with regeneration if DB check fails - } - - if (!analysis?.rationale && billMarkdown) { - console.log(`Regenerating analysis for ${bill.billID} (source changed)`); - analysis = await summarizeBillText(billMarkdown); - } - - // Only classify if missing (new bill or classification absent). Avoid calling otherwise. - let isSocialIssueFinal: boolean = - typeof existingBill?.isSocialIssue === "boolean" - ? ((existingBill as BillDocument).isSocialIssue as boolean) - : false; - if ( - hasValidMongoUri && - (existingBill === null || typeof existingBill.isSocialIssue !== "boolean") - ) { - isSocialIssueFinal = await socialIssueGrader( - billMarkdown || analysis.summary || bill.header || bill.title, - ); - } - - await onBillNotInDatabase({ - billId: bill.billID, - source: bill.source, - markdown: billMarkdown, - bill, - analysis, - billTextsCount: Array.isArray(bill.billTexts) ? bill.billTexts.length : 0, - isSocialIssue: isSocialIssueFinal, - }); - - return { - billId: bill.billID, - title: bill.title, - short_title: bill.shortTitle, - summary: analysis.summary, - status: bill.status, - stages: bill.stages - ? bill.stages.map((stage) => ({ - stage: stage.stage, - state: stage.state, - house: stage.house, - date: new Date(stage.date), - })) - : [], - sponsorParty: bill.sponsorParty, - chamber: house, - supportedRegion: bill.supportedRegion, - introducedOn: new Date(bill.date), - lastUpdatedOn: new Date(latestStageDate), - genres: bill.genres, - parliamentNumber: bill.parliamentNumber, - sessionNumber: bill.sessionNumber, - fullTextMarkdown: billMarkdown, - question_period_questions: analysis.question_period_questions, - // Include analysis data - 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, - }; -} diff --git a/src/utils/billConverters/default-analysis.ts b/src/utils/billConverters/default-analysis.ts new file mode 100644 index 0000000..035c6b6 --- /dev/null +++ b/src/utils/billConverters/default-analysis.ts @@ -0,0 +1,71 @@ +import type { BillAnalysis } from "@/services/billApi"; + +// The Build Canada tenets used as the baseline ("Not analyzed") evaluation set +// when a bill has not yet been run through the AI analysis. +export const DEFAULT_TENET_EVALUATIONS: BillAnalysis["tenet_evaluations"] = [ + { + id: 1, + title: "Canada should aim to be the world's most prosperous country", + alignment: "neutral", + explanation: "Not analyzed", + }, + { + id: 2, + title: + "Promote economic freedom, ambition, and breaking from bureaucratic inertia", + alignment: "neutral", + explanation: "Not analyzed", + }, + { + id: 3, + title: "Drive national productivity and global competitiveness", + alignment: "neutral", + explanation: "Not analyzed", + }, + { + id: 4, + title: "Grow exports of Canadian products and resources", + alignment: "neutral", + explanation: "Not analyzed", + }, + { + id: 5, + title: "Encourage investment, innovation, and resource development", + alignment: "neutral", + explanation: "Not analyzed", + }, + { + id: 6, + title: + "Deliver better public services at lower cost (government efficiency)", + alignment: "neutral", + explanation: "Not analyzed", + }, + { + id: 7, + title: "Reform taxes to incentivize work, risk-taking, and innovation", + alignment: "neutral", + explanation: "Not analyzed", + }, + { + id: 8, + title: "Focus on large-scale prosperity, not incrementalism", + alignment: "neutral", + explanation: "Not analyzed", + }, +]; + +/** + * Build the default analysis used before a bill has been analyzed by the AI. + */ +export function createDefaultAnalysis(summary: string): BillAnalysis { + return { + summary, + tenet_evaluations: DEFAULT_TENET_EVALUATIONS.map((te) => ({ ...te })), + final_judgment: "no", + rationale: undefined, + needs_more_info: false, + missing_details: [], + steel_man: "Not analyzed", + }; +} diff --git a/src/utils/billConverters/from-build-canada-db-bill.test.ts b/src/utils/billConverters/from-build-canada-db-bill.test.ts new file mode 100644 index 0000000..00a4cc6 --- /dev/null +++ b/src/utils/billConverters/from-build-canada-db-bill.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from "vitest"; +import type { BillDocument } from "@/models/Bill"; +import { fromBuildCanadaDbBill } from "./from-build-canada-db-bill"; + +function makeBill(overrides: Partial = {}): BillDocument { + return { + billId: "C-18", + title: "An Act respecting things", + summary: "A summary", + status: "In progress", + chamber: "House of Commons", + tenet_evaluations: [], + final_judgment: "yes", + rationale: "because", + needs_more_info: false, + missing_details: [], + steel_man: "steel man", + ...overrides, + } as unknown as BillDocument; +} + +describe("fromBuildCanadaDbBill", () => { + it("maps core fields straight through", () => { + const result = fromBuildCanadaDbBill( + makeBill({ billId: "S-5", title: "Title", summary: "Sum" }), + ); + expect(result.billId).toBe("S-5"); + expect(result.title).toBe("Title"); + expect(result.summary).toBe("Sum"); + }); + + it("normalizes the final_judgment", () => { + expect( + fromBuildCanadaDbBill(makeBill({ final_judgment: "YES" })).final_judgment, + ).toBe("yes"); + expect( + fromBuildCanadaDbBill(makeBill({ final_judgment: "neutral" })) + .final_judgment, + ).toBe("abstain"); + expect( + fromBuildCanadaDbBill(makeBill({ final_judgment: "garbage" })) + .final_judgment, + ).toBe("abstain"); + }); + + it("defaults stages to an empty array when absent", () => { + expect( + fromBuildCanadaDbBill(makeBill({ stages: undefined })).stages, + ).toEqual([]); + }); + + it("maps stages preserving fields", () => { + const date = new Date("2024-01-01"); + const result = fromBuildCanadaDbBill( + makeBill({ + stages: [ + { stage: "First reading", state: "completed", house: "HoC", date }, + ] as unknown as BillDocument["stages"], + }), + ); + expect(result.stages).toEqual([ + { stage: "First reading", state: "completed", house: "HoC", date }, + ]); + }); + + it("copies genres and missing_details into fresh arrays", () => { + const genres = ["tax", "trade"]; + const missing = ["cost"]; + const result = fromBuildCanadaDbBill( + makeBill({ genres, missing_details: missing }), + ); + expect(result.genres).toEqual(genres); + expect(result.genres).not.toBe(genres); + expect(result.missing_details).toEqual(missing); + expect(result.missing_details).not.toBe(missing); + }); + + it("strips extra fields from question_period_questions", () => { + const result = fromBuildCanadaDbBill( + makeBill({ + question_period_questions: [ + { question: "Q1", _id: "abc" }, + ] as unknown as BillDocument["question_period_questions"], + }), + ); + expect(result.question_period_questions).toEqual([{ question: "Q1" }]); + }); + + it("maps votes to motion and result only", () => { + const result = fromBuildCanadaDbBill( + makeBill({ + votes: [ + { motion: "M1", result: "passed", yeas: 100 }, + ] as unknown as BillDocument["votes"], + }), + ); + expect(result.votes).toEqual([{ motion: "M1", result: "passed" }]); + }); +}); diff --git a/src/utils/billConverters/from-build-canada-db-bill.ts b/src/utils/billConverters/from-build-canada-db-bill.ts new file mode 100644 index 0000000..01d38ba --- /dev/null +++ b/src/utils/billConverters/from-build-canada-db-bill.ts @@ -0,0 +1,53 @@ +import type { BillDocument } from "@/models/Bill"; +import { normalizeFinalJudgment } from "./normalize-final-judgment"; +import type { UnifiedBill } from "./types"; + +// Convert Build Canada DB bill to unified format +export function fromBuildCanadaDbBill(bill: BillDocument): UnifiedBill { + return { + billId: bill.billId, + title: bill.title, + short_title: bill.short_title, + summary: bill.summary, + status: bill.status, + sponsorParty: bill.sponsorParty, + chamber: bill.chamber, + supportedRegion: bill.supportedRegion, + introducedOn: bill.introducedOn, + lastUpdatedOn: bill.lastUpdatedOn, + stages: bill.stages + ? bill.stages.map((stage) => ({ + stage: stage.stage, + state: stage.state, + house: stage.house, + date: stage.date, + })) + : [], + genres: bill.genres ? [...bill.genres] : undefined, + parliamentNumber: bill.parliamentNumber, + sessionNumber: bill.sessionNumber, + votes: bill.votes?.map((v) => ({ + motion: v.motion, + result: v.result, + })), + isSocialIssue: bill.isSocialIssue, + // Properly serialize question_period_questions to remove MongoDB ObjectIds + question_period_questions: bill.question_period_questions?.map((q) => ({ + question: q.question, + })), + // Include analysis data - ensure proper serialization + tenet_evaluations: bill.tenet_evaluations?.map((te) => ({ + id: te.id, + title: te.title, + alignment: te.alignment, + explanation: te.explanation, + })), + final_judgment: normalizeFinalJudgment(bill.final_judgment), + rationale: bill.rationale, + needs_more_info: bill.needs_more_info, + missing_details: bill.missing_details + ? [...bill.missing_details] + : undefined, + steel_man: bill.steel_man, + }; +} diff --git a/src/utils/billConverters/from-civics-project-api-bill.ts b/src/utils/billConverters/from-civics-project-api-bill.ts new file mode 100644 index 0000000..f9475e3 --- /dev/null +++ b/src/utils/billConverters/from-civics-project-api-bill.ts @@ -0,0 +1,145 @@ +import type { BillDocument } from "@/models/Bill"; +import type { ApiBillDetail } from "@/services/billApi"; +import { + type BillAnalysis, + fetchBillMarkdown, + onBillNotInDatabase, + summarizeBillText, +} from "@/services/billApi"; +import { socialIssueGrader } from "@/services/social-issue-grader"; +import { createDefaultAnalysis } from "./default-analysis"; +import { normalizeFinalJudgment } from "./normalize-final-judgment"; +import type { UnifiedBill } from "./types"; + +// Convert Civics Project API bill to unified format +export async function fromCivicsProjectApiBill( + bill: ApiBillDetail, +): Promise { + const { env } = await import("@/env"); + const uri = env.MONGO_URI || ""; + const hasValidMongoUri = + uri.startsWith("mongodb://") || uri.startsWith("mongodb+srv://"); + let existingBill: BillDocument | null = null; + const latestStageDate = + bill.stages && bill.stages.length > 0 + ? bill.stages[bill.stages.length - 1].date + : (bill.updatedAt ?? bill.date); + const house = + bill.stages && bill.stages.length > 0 + ? bill.stages[bill.stages.length - 1].house + : undefined; + + let billMarkdown: string | null = null; + + const latestBillSource = + bill.source || (bill.billTexts?.[0] as { url?: string })?.url; + + if (latestBillSource) { + billMarkdown = await fetchBillMarkdown(latestBillSource); + } + + // Check if we need to regenerate summary based on source changes from Civics Project API + let analysis: BillAnalysis = createDefaultAnalysis(bill.header || ""); + + // Check existing bill in database to see if source changed + try { + const { connectToDatabase } = await import("@/lib/mongoose"); + const { Bill } = await import("@/models/Bill"); + + if (hasValidMongoUri) { + await connectToDatabase(); + existingBill = (await Bill.findOne({ billId: bill.billID }) + .lean() + .exec()) as BillDocument | null; + + if (existingBill && !analysis.rationale) { + // Source hasn't changed, use existing analysis + analysis = { + summary: existingBill.summary, + tenet_evaluations: + existingBill.tenet_evaluations || analysis.tenet_evaluations, + // Treat legacy "neutral" and any unknown value as "abstain", + // falling back to the default analysis judgment when empty. + final_judgment: normalizeFinalJudgment( + existingBill.final_judgment, + analysis.final_judgment, + ), + rationale: existingBill.rationale || analysis.rationale, + needs_more_info: + existingBill.needs_more_info || analysis.needs_more_info, + missing_details: + existingBill.missing_details || analysis.missing_details, + steel_man: existingBill.steel_man || analysis.steel_man, + }; + console.log( + `Using existing analysis for ${bill.billID} (source unchanged)`, + ); + } + } + } catch (error) { + console.error("Error checking existing bill:", error); + // Continue with regeneration if DB check fails + } + + if (!analysis?.rationale && billMarkdown) { + console.log(`Regenerating analysis for ${bill.billID} (source changed)`); + analysis = await summarizeBillText(billMarkdown); + } + + // Only classify if missing (new bill or classification absent). Avoid calling otherwise. + let isSocialIssueFinal: boolean = + typeof existingBill?.isSocialIssue === "boolean" + ? ((existingBill as BillDocument).isSocialIssue as boolean) + : false; + if ( + hasValidMongoUri && + (existingBill === null || typeof existingBill.isSocialIssue !== "boolean") + ) { + isSocialIssueFinal = await socialIssueGrader( + billMarkdown || analysis.summary || bill.header || bill.title, + ); + } + + await onBillNotInDatabase({ + billId: bill.billID, + source: bill.source, + markdown: billMarkdown, + bill, + analysis, + billTextsCount: Array.isArray(bill.billTexts) ? bill.billTexts.length : 0, + isSocialIssue: isSocialIssueFinal, + }); + + return { + billId: bill.billID, + title: bill.title, + short_title: bill.shortTitle, + summary: analysis.summary, + status: bill.status, + stages: bill.stages + ? bill.stages.map((stage) => ({ + stage: stage.stage, + state: stage.state, + house: stage.house, + date: new Date(stage.date), + })) + : [], + sponsorParty: bill.sponsorParty, + chamber: house, + supportedRegion: bill.supportedRegion, + introducedOn: new Date(bill.date), + lastUpdatedOn: new Date(latestStageDate), + genres: bill.genres, + parliamentNumber: bill.parliamentNumber, + sessionNumber: bill.sessionNumber, + fullTextMarkdown: billMarkdown, + question_period_questions: analysis.question_period_questions, + // Include analysis data + 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, + }; +} diff --git a/src/utils/billConverters/index.ts b/src/utils/billConverters/index.ts new file mode 100644 index 0000000..ab218d4 --- /dev/null +++ b/src/utils/billConverters/index.ts @@ -0,0 +1,8 @@ +export { createDefaultAnalysis } from "./default-analysis"; +export { fromBuildCanadaDbBill } from "./from-build-canada-db-bill"; +export { fromCivicsProjectApiBill } from "./from-civics-project-api-bill"; +export { + type FinalJudgment, + normalizeFinalJudgment, +} from "./normalize-final-judgment"; +export type { UnifiedBill } from "./types"; diff --git a/src/utils/billConverters/normalize-final-judgment.test.ts b/src/utils/billConverters/normalize-final-judgment.test.ts new file mode 100644 index 0000000..6a9ce9a --- /dev/null +++ b/src/utils/billConverters/normalize-final-judgment.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { normalizeFinalJudgment } from "./normalize-final-judgment"; + +describe("normalizeFinalJudgment", () => { + it("passes through 'yes' and 'no'", () => { + expect(normalizeFinalJudgment("yes")).toBe("yes"); + expect(normalizeFinalJudgment("no")).toBe("no"); + }); + + it("lowercases and trims yes/no", () => { + expect(normalizeFinalJudgment(" YES ")).toBe("yes"); + expect(normalizeFinalJudgment("No")).toBe("no"); + }); + + it("maps 'abstain' and legacy 'neutral' to 'abstain'", () => { + expect(normalizeFinalJudgment("abstain")).toBe("abstain"); + expect(normalizeFinalJudgment("NEUTRAL")).toBe("abstain"); + }); + + it("falls back to 'abstain' by default for unknown/empty values", () => { + expect(normalizeFinalJudgment("")).toBe("abstain"); + expect(normalizeFinalJudgment("maybe")).toBe("abstain"); + expect(normalizeFinalJudgment(null)).toBe("abstain"); + expect(normalizeFinalJudgment(undefined)).toBe("abstain"); + }); + + it("uses the provided fallback only for unknown values", () => { + expect(normalizeFinalJudgment("maybe", "no")).toBe("no"); + expect(normalizeFinalJudgment("", "yes")).toBe("yes"); + // abstain/neutral always normalize to abstain, ignoring the fallback + expect(normalizeFinalJudgment("neutral", "no")).toBe("abstain"); + // a valid value still wins over the fallback + expect(normalizeFinalJudgment("yes", "no")).toBe("yes"); + }); +}); diff --git a/src/utils/billConverters/normalize-final-judgment.ts b/src/utils/billConverters/normalize-final-judgment.ts new file mode 100644 index 0000000..e892a46 --- /dev/null +++ b/src/utils/billConverters/normalize-final-judgment.ts @@ -0,0 +1,21 @@ +export type FinalJudgment = "yes" | "no" | "abstain"; + +/** + * Normalize an arbitrary stored/incoming `final_judgment` value into one of the + * three supported judgments. + * + * - "yes" / "no" (any casing or surrounding whitespace) pass through. + * - "abstain" and legacy "neutral" map to "abstain". + * - Anything else (empty, unknown, null/undefined) falls back to `fallback`. + */ +export function normalizeFinalJudgment( + value: unknown, + fallback: FinalJudgment = "abstain", +): FinalJudgment { + const raw = String(value ?? "") + .trim() + .toLowerCase(); + if (raw === "yes" || raw === "no") return raw; + if (raw === "abstain" || raw === "neutral") return "abstain"; + return fallback; +} diff --git a/src/utils/billConverters/types.ts b/src/utils/billConverters/types.ts new file mode 100644 index 0000000..a9d6038 --- /dev/null +++ b/src/utils/billConverters/types.ts @@ -0,0 +1,38 @@ +// Unified bill data structure shared across the app +export interface UnifiedBill { + billId: string; + title: string; + short_title?: string; + summary: string; + status: string; + sponsorParty?: string; + chamber?: string; + supportedRegion?: string; + introducedOn?: Date; + lastUpdatedOn?: Date; + stages: { + stage: string; + state: string; + house: string; + date: Date; + }[]; + genres?: string[]; + parliamentNumber?: number; + sessionNumber?: number; + votes?: Array<{ motion?: string; result: string }>; + fullTextMarkdown?: string | null; + isSocialIssue?: boolean; + question_period_questions?: Array<{ question: string }>; + // Analysis data from AI + tenet_evaluations?: Array<{ + id: number; + title: string; + alignment: "aligns" | "conflicts" | "neutral"; + explanation: string; + }>; + final_judgment: "yes" | "no" | "abstain"; + rationale?: string; + needs_more_info?: boolean; + missing_details?: string[]; + steel_man?: string; +}