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: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs`
7c3ebac6c13c79aee6c79c917a09363ff6c8fa9d # prettier, 13 files
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 5
Expand All @@ -20,7 +20,7 @@ updates:
update-types: [minor, patch]

- package-ecosystem: github-actions
directory: "/"
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 3
31 changes: 31 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Build output and vendored trees — formatting these is noise.
node_modules
.next
dist
build
out
coverage
.turbo
.vercel
*.min.js
*.min.css

# Generated during a build, so it is absent locally and present in CI — which
# makes a clean local --check no evidence at all. Contentlayer's output also
# uses import assertions, which prettier's parser rejects outright.
.contentlayer
.astro
.svelte-kit
storybook-static
test-results
playwright-report

# Lockfiles are generated; prettier would rewrite them wholesale.
package-lock.json
pnpm-lock.yaml
yarn.lock

# Markdown is deliberately out of scope for now. Prettier rewraps prose, which
# 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
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all",
"arrowParens": "always",
"endOfLine": "lf"
}
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {};

Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

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

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"verify": "npm run lint && npm run typecheck && npm run test",
"verify": "npm run format:check && npm run lint && npm run typecheck && npm run test",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev"
"db:migrate": "prisma migrate dev",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@prisma/client": "^6.2.1",
Expand All @@ -27,6 +29,7 @@
"@types/react": "^19.2.18",
"eslint": "^9.39.5",
"eslint-config-next": "^15.5.20",
"prettier": "3.9.6",
"prisma": "^6.2.1",
"typescript": "^5.7.3",
"vitest": "^4.1.11"
Expand Down
2 changes: 1 addition & 1 deletion src/app/EvidenceVerifier.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
}

.algo {
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace;
font-size: 0.75rem;
color: var(--color-text-muted);
}
20 changes: 6 additions & 14 deletions src/app/EvidenceVerifier.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export function EvidenceVerifier() {
<div>
<div className={styles.heading}>Verify a claim from its evidence</div>
<p className={styles.sub}>
The verdict is a tally of supporting vs. contradicting evidence. Add
evidence on each side and see the explainable result — nothing hidden.
The verdict is a tally of supporting vs. contradicting evidence. Add evidence on each side
and see the explainable result — nothing hidden.
</p>
</div>

Expand Down Expand Up @@ -118,12 +118,7 @@ export function EvidenceVerifier() {
</div>

<div className={styles.actions}>
<button
type="button"
className={styles.verifyBtn}
onClick={verify}
disabled={loading}
>
<button type="button" className={styles.verifyBtn} onClick={verify} disabled={loading}>
{loading ? 'Verifying…' : 'Verify'}
</button>
{error && <span className={styles.error}>{error}</span>}
Expand All @@ -132,9 +127,7 @@ export function EvidenceVerifier() {
{result && display && (
<div className={styles.result}>
<div className={styles.verdictRow}>
<span className={`${styles.verdict} ${TONE_CLASS[display.tone]}`}>
{display.label}
</span>
<span className={`${styles.verdict} ${TONE_CLASS[display.tone]}`}>{display.label}</span>
{result.confidence !== null && (
<span className={styles.confidence}>
{Math.round(result.confidence * 100)}% confidence
Expand All @@ -148,9 +141,8 @@ export function EvidenceVerifier() {
</div>
<p className={styles.explain}>
A verdict of Supported or Contradicted needs at least{' '}
{Math.round(MIN_AGREEMENT_TO_CONCLUDE * 100)}% of evidence to agree;
below that the claim stays Unverifiable rather than assert a
weakly-backed conclusion.
{Math.round(MIN_AGREEMENT_TO_CONCLUDE * 100)}% of evidence to agree; below that the
claim stays Unverifiable rather than assert a weakly-backed conclusion.
</p>
<span className={styles.algo}>algorithm: {result.algorithm}</span>
</div>
Expand Down
5 changes: 1 addition & 4 deletions src/app/api/claims/[id]/verify/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { NextResponse } from 'next/server';

import type { ApiResult } from '@/lib/api/result';
import { prisma } from '@/lib/db/prisma';
import {
scoreClaimVerification,
type ClaimVerification,
} from '@/lib/domain/claim-verification';
import { scoreClaimVerification, type ClaimVerification } from '@/lib/domain/claim-verification';

/**
* GET /api/claims/[id]/verify
Expand Down
9 changes: 2 additions & 7 deletions src/app/api/claims/verify/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { NextResponse } from 'next/server';

import type { ApiResult } from '@/lib/api/result';
import {
scoreClaimVerification,
type ClaimVerification,
} from '@/lib/domain/claim-verification';
import { scoreClaimVerification, type ClaimVerification } from '@/lib/domain/claim-verification';
import { parseVerifyRequest } from '@/lib/domain/verify-request';

/**
Expand All @@ -21,9 +18,7 @@ import { parseVerifyRequest } from '@/lib/domain/verify-request';
* The response carries the tally, confidence, and algorithm id straight from the
* domain, so a caller can reproduce the verdict from what they sent.
*/
export async function POST(
request: Request,
): Promise<NextResponse<ApiResult<ClaimVerification>>> {
export async function POST(request: Request): Promise<NextResponse<ApiResult<ClaimVerification>>> {
let body: unknown;
try {
body = await request.json();
Expand Down
2 changes: 1 addition & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
--color-tone-negative: var(--primitive-red-700);
--color-tone-neutral: var(--primitive-stone-500);

--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--radius-card: 16px;
--radius-btn: 8px;
}
Expand Down
12 changes: 5 additions & 7 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { Metadata } from "next";
import "./globals.css";
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
title: "BiasLens",
title: 'BiasLens',
description:
"Media bias analysis engine — evidence before conclusions, facts separated from framing.",
'Media bias analysis engine — evidence before conclusions, facts separated from framing.',
};

export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
Expand Down
28 changes: 14 additions & 14 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { EvidenceVerifier } from "./EvidenceVerifier";
import { EvidenceVerifier } from './EvidenceVerifier';

export default function Home() {
return (
<main
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: "1.5rem",
padding: "2rem",
maxWidth: "42rem",
marginInline: "auto",
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: '1.5rem',
padding: '2rem',
maxWidth: '42rem',
marginInline: 'auto',
}}
>
<header style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<h1 style={{ color: "var(--color-brand)", fontSize: "2rem" }}>BiasLens</h1>
<p style={{ color: "var(--color-text-muted)" }}>
Media bias analysis engine. Evidence before conclusions; facts separated
from framing; every score explainable and reproducible.
<header style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<h1 style={{ color: 'var(--color-brand)', fontSize: '2rem' }}>BiasLens</h1>
<p style={{ color: 'var(--color-text-muted)' }}>
Media bias analysis engine. Evidence before conclusions; facts separated from framing;
every score explainable and reproducible.
</p>
</header>
<EvidenceVerifier />
Expand Down
4 changes: 1 addition & 3 deletions src/lib/api/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,4 @@
* every client parses. Defining it once keeps the HTTP contract from drifting
* between producer (route handlers) and consumer (UI/hooks).
*/
export type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export type ApiResult<T> = { success: true; data: T } | { success: false; error: string };
3 changes: 1 addition & 2 deletions src/lib/config/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ export const VERIFICATION_STATUS = {
/** Evidence exists but is too balanced to conclude either way. */
UNVERIFIABLE: 'unverifiable',
} as const;
export type VerificationStatus =
(typeof VERIFICATION_STATUS)[keyof typeof VERIFICATION_STATUS];
export type VerificationStatus = (typeof VERIFICATION_STATUS)[keyof typeof VERIFICATION_STATUS];

/**
* How each verdict is presented to a human: its label and a semantic *tone*
Expand Down
18 changes: 3 additions & 15 deletions src/lib/domain/claim-verification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ describe('scoreClaimVerification', () => {

it('is SUPPORTED when the supporting majority meets the threshold', () => {
// 3/5 = 0.6 ≥ MIN_AGREEMENT_TO_CONCLUDE
const result = scoreClaimVerification([
supports,
supports,
supports,
contradicts,
contradicts,
]);
const result = scoreClaimVerification([supports, supports, supports, contradicts, contradicts]);
expect(result.status).toBe(VERIFICATION_STATUS.SUPPORTED);
expect(result.confidence).toBeCloseTo(0.6);
expect(result.inputs).toEqual({ supports: 3, contradicts: 2, total: 5 });
Expand Down Expand Up @@ -57,20 +51,14 @@ describe('scoreClaimVerification', () => {
});

it('ignores unknown stance values rather than trusting them', () => {
const result = scoreClaimVerification([
supports,
supports,
{ stance: 'garbage' },
]);
const result = scoreClaimVerification([supports, supports, { stance: 'garbage' }]);
expect(result.inputs).toEqual({ supports: 2, contradicts: 0, total: 2 });
expect(result.status).toBe(VERIFICATION_STATUS.SUPPORTED);
expect(result.confidence).toBe(1);
});

it('stamps the algorithm version for reproducibility', () => {
expect(scoreClaimVerification([]).algorithm).toBe(
CLAIM_VERIFICATION_ALGORITHM,
);
expect(scoreClaimVerification([]).algorithm).toBe(CLAIM_VERIFICATION_ALGORITHM);
});

it('is deterministic — identical input yields identical output', () => {
Expand Down
4 changes: 1 addition & 3 deletions src/lib/domain/verify-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import { EVIDENCE_STANCE, type EvidenceStance } from '../config/verification';
* uses — so an unknown stance is rejected here with a helpful message rather than
* being silently dropped downstream.
*/
export type ParseResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export type ParseResult<T> = { success: true; data: T } | { success: false; error: string };

export interface VerifyRequest {
evidence: ReadonlyArray<{ stance: EvidenceStance }>;
Expand Down