Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Postgres connection for Prisma. Copy to .env and fill in.
# Postgres connection for Drizzle. Copy to .env and fill in.
DATABASE_URL="postgresql://user:password@localhost:5432/biaslens"
17 changes: 7 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Golden CI floor — see dotfiles/templates/ci/README.md.
# biaslens: npm + vitest + Prisma. Floor = verify (lint + typecheck + test);
# the 14-case suite now runs on every push & PR. Prisma has no postinstall, so
# the client is generated explicitly before typecheck needs its types. `next build`
# is hermetic (no live DB): all DB access is inside dynamic route handlers, so the
# prerender stage never connects — verified locally against an unreachable DB. It
# runs after verify to catch build-only breakage.
# biaslens: npm + vitest + Drizzle. Floor = verify (lint + typecheck + test);
# the 14-case suite runs on every push & PR. Drizzle needs no codegen — types
# derive from src/lib/db/schema.ts directly, so nothing runs between install and
# verify. `next build` is hermetic (no live DB): all DB access is inside dynamic
# route handlers and the client pool is lazy, so the prerender stage never
# connects. It runs after verify to catch build-only breakage.
name: CI

on:
Expand Down Expand Up @@ -35,15 +35,12 @@ jobs:
- name: Install
run: npm ci

- name: Prisma generate (types for typecheck)
run: npx prisma generate

# SSOT: lint + typecheck + test, defined once in package.json `verify`.
- name: Verify
run: npm run verify

# Build gate: catches build-only breakage that verify (lint+typecheck+test)
# can miss. Hermetic — no live DB needed (all queries are in dynamic route
# handlers). Reuses the client generated by the prisma-generate step above.
# handlers; the Drizzle client initializes lazily on first request).
- name: Build
run: npm run build
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ build/
.env*.local
*.log
.DS_Store
/prisma/*.db
next-env.d.ts
*.tsbuildinfo
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ yarn.lock
# is where it is most opinionated and least useful, and it would bury the real
# diff. Remove this line when you want docs formatted too.
*.md
# drizzle-kit generated migration snapshots — machine-written, not hand-formatted
drizzle/meta/
17 changes: 10 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Media bias analysis engine. See `CLAUDE.md` for the product contract and
## Stack

- Next.js (App Router) + TypeScript (strict)
- Prisma / PostgreSQL — `prisma/schema.prisma` is the SSOT for domain types
- Drizzle ORM / PostgreSQL — `src/lib/db/schema.ts` is the SSOT for domain types
- Vitest for tests
- Node >= 20

Expand All @@ -15,8 +15,8 @@ Media bias analysis engine. See `CLAUDE.md` for the product contract and
```bash
npm install
cp .env.example .env # set DATABASE_URL
npm run db:generate # prisma generate — run before typecheck/build
npm run db:migrate # prisma migrate dev (needs a live Postgres)
npm run db:generate # drizzle-kit generate — new migration from schema changes
npm run db:migrate # drizzle-kit migrate (needs a live Postgres)
npm run dev # local dev server
npm run build # next build (production compile; no live DB needed)
npm run verify # SSOT gate: lint + typecheck + test
Expand All @@ -28,16 +28,19 @@ npm run verify # SSOT gate: lint + typecheck + test
"green". CI calls it verbatim; run it locally before declaring any change done.
`next build` also runs in CI after verify to catch build-only breakage.

Prisma has no `postinstall`, so run `npm run db:generate` (or `npx prisma
generate`) before `typecheck`/`build` — the generated client provides the types.
Drizzle has no codegen: types derive from `src/lib/db/schema.ts` at typecheck
time, so `npm install` is the only prerequisite for `typecheck`/`build`.
`npm run db:generate` is only needed after changing the schema (it writes a new
SQL migration into `drizzle/`).

## Layout

```
prisma/schema.prisma SSOT for domain types (Outlet, Article, Claim, Evidence, Narrative, ...)
src/lib/db/schema.ts SSOT for domain types (Outlet, Article, Claim, Evidence, Narrative, ...)
drizzle/ SQL migrations generated by drizzle-kit
src/app/ App Router pages + API route handlers
src/lib/domain/ pure scoring logic (explainable, reproducible) + tests
src/lib/db/prisma.ts the single PrismaClient (SSOT for DB access)
src/lib/db/client.ts the single Drizzle client (SSOT for DB access)
src/lib/api/ HTTP result shape
src/lib/config/ verification config
```
Expand Down
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ contract every change follows. It mirrors the FleetCrown project brief.

## Conventions

- Next.js (App Router) + TypeScript strict + Prisma/Postgres.
- `prisma/schema.prisma` is the SSOT for domain types — derive TS types from it,
never redefine them.
- Next.js (App Router) + TypeScript strict + Drizzle/Postgres.
- `src/lib/db/schema.ts` is the SSOT for domain types — derive TS types from it
(`$inferSelect`), never redefine them.
- Facts live in `Claim`/`Evidence`; framing lives in `Narrative`/`Frame`. Do not
collapse the two.
- Scores are functions of stored evidence — never hard-coded — so they recompute
Expand All @@ -29,9 +29,9 @@ pass. (FleetCrown's DoD gate judges against this.)

## Roadmap (current milestone first)

1. **Core data model** — seeded in `prisma/schema.prisma` (Outlet, Article, Claim,
Evidence, Narrative, EditorialDna, HomepageSnapshot). Acceptance: `prisma
migrate dev` runs clean and the client generates.
1. **Core data model** — seeded in `src/lib/db/schema.ts` (Outlet, Article, Claim,
Evidence, Narrative, EditorialDna, HomepageSnapshot). Acceptance: `npm run
db:migrate` runs clean against a fresh Postgres.
2. Extraction + Claim agents: parse an article into structured claims with sources.
3. Bias + Evidence scoring engine — explainable scores.
4. Hourly Homepage Loop: crawl + compare outlets.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Argument Arena vision.

## Stack

Next.js + TypeScript + Prisma/Postgres. Multi-agent pipeline for
Next.js + TypeScript + Drizzle/Postgres. Multi-agent pipeline for
crawl/extract/score. FleetCrown for orchestration + loops; OrangeCat for funding
+ public transparency.

Expand All @@ -40,8 +40,9 @@ and on-demand Article Loop (extract → analyze → report → feedback).
## Roadmap

1. ~~**Define the core data model** — Outlet, Article, Claim, Evidence, Narrative,
Editorial DNA as Prisma schemas (this scaffold seeds `prisma/schema.prisma`).~~
**✅ DONE** (`0d46066`) — App Router root + initial Prisma migration applied;
Editorial DNA as schema definitions (now `src/lib/db/schema.ts`, Drizzle ORM —
migrated from the original Prisma scaffold with byte-identical tables).~~
**✅ DONE** (`0d46066`) — App Router root + initial migration applied;
build, lint, and `tsc` all green.
2. Build the Extraction + Claim agents to parse an article into structured claims.
3. Bias + Evidence scoring engine — every score explainable.
Expand All @@ -54,7 +55,7 @@ and on-demand Article Loop (extract → analyze → report → feedback).
```bash
npm install
cp .env.example .env # set DATABASE_URL
npx prisma migrate dev
npm run db:migrate # drizzle-kit migrate — applies drizzle/*.sql
npm run dev
```

Expand Down
19 changes: 19 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Drizzle Kit configuration — fleet house pattern (see hirnli, vitareba).
*
* `src/lib/db/schema.ts` is the SSOT for domain types; migrations are generated
* into `drizzle/` with `npm run db:generate` and applied with `npm run
* db:migrate`. DATABASE_URL comes from the environment (no dotenv magic —
* export it or prefix the command).
*/

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
schema: './src/lib/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
78 changes: 78 additions & 0 deletions drizzle/0000_nosy_thor_girl.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
CREATE TABLE "Article" (
"id" text PRIMARY KEY NOT NULL,
"outletId" text NOT NULL,
"headline" text NOT NULL,
"author" text,
"section" text,
"url" text NOT NULL,
"body" text NOT NULL,
"publishedAt" timestamp (3),
"createdAt" timestamp (3) DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Claim" (
"id" text PRIMARY KEY NOT NULL,
"articleId" text NOT NULL,
"text" text NOT NULL,
"speaker" text,
"verificationStatus" text DEFAULT 'pending' NOT NULL,
"confidence" double precision,
"createdAt" timestamp (3) DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "EditorialDna" (
"id" text PRIMARY KEY NOT NULL,
"outletId" text NOT NULL,
"dimensions" jsonb NOT NULL,
"updatedAt" timestamp (3) NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Evidence" (
"id" text PRIMARY KEY NOT NULL,
"claimId" text NOT NULL,
"stance" text NOT NULL,
"source" text NOT NULL,
"excerpt" text,
"url" text,
"createdAt" timestamp (3) DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "HomepageSnapshot" (
"id" text PRIMARY KEY NOT NULL,
"outletId" text NOT NULL,
"capturedAt" timestamp (3) DEFAULT now() NOT NULL,
"items" jsonb NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Narrative" (
"id" text PRIMARY KEY NOT NULL,
"articleId" text NOT NULL,
"frame" text NOT NULL,
"summary" text,
"createdAt" timestamp (3) DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "Outlet" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"country" text,
"ownership" text,
"funding" text,
"isPublic" boolean DEFAULT false NOT NULL,
"createdAt" timestamp (3) DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "Article" ADD CONSTRAINT "Article_outletId_fkey" FOREIGN KEY ("outletId") REFERENCES "public"."Outlet"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Claim" ADD CONSTRAINT "Claim_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "EditorialDna" ADD CONSTRAINT "EditorialDna_outletId_fkey" FOREIGN KEY ("outletId") REFERENCES "public"."Outlet"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Evidence" ADD CONSTRAINT "Evidence_claimId_fkey" FOREIGN KEY ("claimId") REFERENCES "public"."Claim"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "HomepageSnapshot" ADD CONSTRAINT "HomepageSnapshot_outletId_fkey" FOREIGN KEY ("outletId") REFERENCES "public"."Outlet"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "Narrative" ADD CONSTRAINT "Narrative_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
CREATE UNIQUE INDEX "Article_url_key" ON "Article" USING btree ("url");--> statement-breakpoint
CREATE INDEX "Article_outletId_idx" ON "Article" USING btree ("outletId");--> statement-breakpoint
CREATE INDEX "Claim_articleId_idx" ON "Claim" USING btree ("articleId");--> statement-breakpoint
CREATE UNIQUE INDEX "EditorialDna_outletId_key" ON "EditorialDna" USING btree ("outletId");--> statement-breakpoint
CREATE INDEX "Evidence_claimId_idx" ON "Evidence" USING btree ("claimId");--> statement-breakpoint
CREATE INDEX "HomepageSnapshot_outletId_capturedAt_idx" ON "HomepageSnapshot" USING btree ("outletId","capturedAt");--> statement-breakpoint
CREATE INDEX "Narrative_articleId_idx" ON "Narrative" USING btree ("articleId");--> statement-breakpoint
CREATE UNIQUE INDEX "Outlet_name_key" ON "Outlet" USING btree ("name");
Loading