labelapp scaffold
This commit is contained in:
parent
b1503a1942
commit
3260a9c5d9
32
.gitignore
vendored
32
.gitignore
vendored
@ -18,3 +18,35 @@ __pycache__/
|
|||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|||||||
29
CLAUDE.md
Normal file
29
CLAUDE.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# sec-cyBERT
|
||||||
|
|
||||||
|
Bun workspace monorepo. Three packages:
|
||||||
|
|
||||||
|
- `packages/schemas/` — shared Zod schemas (`@sec-cybert/schemas`). Import directly by path: `from "@sec-cybert/schemas/label.ts"`
|
||||||
|
- `ts/` — GenAI labeling pipeline (CLI scripts, Vercel AI SDK, OpenRouter)
|
||||||
|
- `labelapp/` — Next.js human labeling webapp (Drizzle, Postgres, shadcn/ui, Playwright)
|
||||||
|
|
||||||
|
## Quick reference
|
||||||
|
|
||||||
|
| What | Where |
|
||||||
|
|------|-------|
|
||||||
|
| Shared schemas (Zod) | `packages/schemas/src/` |
|
||||||
|
| Labeling codebook (source of truth for all category/specificity definitions) | `docs/LABELING-CODEBOOK.md` |
|
||||||
|
| Project narrative (decisions, roadblocks, lessons) | `docs/NARRATIVE.md` |
|
||||||
|
| Implementation plan for labelapp | `docs/labelapp-plan.md` |
|
||||||
|
| Labelapp-specific agent guide | `labelapp/AGENTS.md` |
|
||||||
|
| Docker compose (Postgres) | `docker-compose.yaml` (root) |
|
||||||
|
| DB credentials | `sec_cybert` / `sec_cybert` / `sec_cybert` on localhost:5432 |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- `bun` for all JS/TS. `uv` for Python.
|
||||||
|
- No barrel files. Direct path-based imports only.
|
||||||
|
- No TODO comments. Finish what you start.
|
||||||
|
- No parallel codepaths. Find and extend existing code before writing new.
|
||||||
|
- Schemas live in `packages/schemas/` — do not duplicate type definitions elsewhere.
|
||||||
|
- `labelapp/` uses flat layout (no `src/` dir): `app/`, `db/`, `lib/`, `components/` at root.
|
||||||
|
- Tests: `bun test` for backend route integration (`__test__/` dirs adjacent to routes), Playwright for E2E (`tests/`).
|
||||||
620
docs/labelapp-plan.md
Normal file
620
docs/labelapp-plan.md
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
# Human Labeling Webapp — Implementation Plan
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The SEC cyBERT project needs 1,200 human-labeled paragraphs as a gold holdout set. 6 student annotators, 3 per paragraph, 600 per person. The narrative specifies a "quiz-gated labeling web tool that enforces codebook knowledge before each session." No webapp or monorepo structure exists yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0: Monorepo Restructure
|
||||||
|
|
||||||
|
Convert the repo to a Bun workspace monorepo and extract shared schemas into a package.
|
||||||
|
|
||||||
|
### New repo structure
|
||||||
|
|
||||||
|
```
|
||||||
|
sec-cyBERT/
|
||||||
|
package.json # workspace root: { "workspaces": ["packages/*", "ts", "labelapp"] }
|
||||||
|
bun.lock # moved from ts/bun.lock
|
||||||
|
packages/
|
||||||
|
schemas/
|
||||||
|
package.json # { "name": "@sec-cybert/schemas" }
|
||||||
|
tsconfig.json
|
||||||
|
src/
|
||||||
|
label.ts # moved from ts/src/schemas/label.ts
|
||||||
|
paragraph.ts
|
||||||
|
annotation.ts
|
||||||
|
consensus.ts
|
||||||
|
gold.ts
|
||||||
|
benchmark.ts
|
||||||
|
session.ts
|
||||||
|
ts/
|
||||||
|
package.json # adds dependency: "@sec-cybert/schemas": "workspace:*"
|
||||||
|
tsconfig.json # add project reference to ../packages/schemas
|
||||||
|
src/
|
||||||
|
schemas/ # DELETED (moved to packages/schemas)
|
||||||
|
...
|
||||||
|
scripts/
|
||||||
|
...
|
||||||
|
labelapp/
|
||||||
|
package.json # Next.js app, depends on @sec-cybert/schemas
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import rewrite
|
||||||
|
|
||||||
|
All imports in `ts/src/` and `ts/scripts/` change from relative schema paths to the package:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before (ts/src/label/annotate.ts)
|
||||||
|
import { LabelOutputRaw, toLabelOutput } from "../schemas/label.ts";
|
||||||
|
import type { Paragraph } from "../schemas/paragraph.ts";
|
||||||
|
|
||||||
|
// After
|
||||||
|
import { LabelOutputRaw, toLabelOutput } from "@sec-cybert/schemas/label.ts";
|
||||||
|
import type { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
```
|
||||||
|
|
||||||
|
**No barrel file.** Direct path-based imports. The existing `ts/src/schemas/index.ts` is deleted.
|
||||||
|
|
||||||
|
### Files to rewrite imports in (~30 files)
|
||||||
|
|
||||||
|
**ts/src/ (15 files):** cli.ts, label/annotate.ts, label/batch.ts, label/consensus.ts, label/prompts.ts, analyze/corpus-stats.ts, analyze/data-quality.ts, analyze/dedup-analysis.ts, extract/pipeline.ts, extract/segment.ts, extract/fast-reparse.ts
|
||||||
|
|
||||||
|
**ts/scripts/ (14 files):** dispute-crosstab.ts, model-bench.ts, sample-disputes.ts, mimo-pilot.ts, model-bias-analysis.ts, segment-analysis.ts, mimo-raw-test.ts, mimo-test.ts, judge-bench.ts, judge-diag-batch.ts, judge-diag.ts, pilot.ts, stage1-run.ts, model-probe.ts
|
||||||
|
|
||||||
|
Can be done with a sed command per pattern:
|
||||||
|
- `"../schemas/` → `"@sec-cybert/schemas/`
|
||||||
|
- `"./schemas/` → `"@sec-cybert/schemas/`
|
||||||
|
- `"../src/schemas/` → `"@sec-cybert/schemas/`
|
||||||
|
|
||||||
|
### tsconfig setup
|
||||||
|
|
||||||
|
**packages/schemas/tsconfig.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"composite": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ts/tsconfig.json** — add project reference:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": { ... },
|
||||||
|
"references": [{ "path": "../packages/schemas" }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- `bun install` from root resolves all workspaces
|
||||||
|
- `bun run --filter ts typecheck` passes
|
||||||
|
- Existing scripts still work: `bun run --filter ts sec -- label:cost`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Labelapp — Next.js + Drizzle + Postgres
|
||||||
|
|
||||||
|
### Stack
|
||||||
|
- **Next.js** (App Router, turbopack dev)
|
||||||
|
- **Tailwind CSS v4** + **shadcn/ui** (components: RadioGroup, Button, Card, Sidebar, Collapsible, Table, Charts)
|
||||||
|
- **lucide-react** for icons (shadcn default)
|
||||||
|
- **Drizzle ORM** + `drizzle-kit` (schema push, no migration files needed for this)
|
||||||
|
- **Postgres 18** via docker-compose (named volume for data)
|
||||||
|
- **Bun** as package manager (auto-uses Node for Next.js dev/build)
|
||||||
|
- **@sec-cybert/schemas** workspace dependency
|
||||||
|
- **Playwright** for E2E tests
|
||||||
|
|
||||||
|
### Postgres (already running)
|
||||||
|
Docker compose is already set up and running at the repo root. DB: `sec_cybert`, user: `sec_cybert`, password: `sec_cybert`, port 5432.
|
||||||
|
|
||||||
|
```
|
||||||
|
DATABASE_URL=postgresql://sec_cybert:sec_cybert@localhost:5432/sec_cybert
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drizzle schema (labelapp/db/schema.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { pgTable, text, integer, real, timestamp, boolean, unique } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const paragraphs = pgTable("paragraphs", {
|
||||||
|
id: text("id").primaryKey(), // UUID from Paragraph.id
|
||||||
|
text: text("text").notNull(),
|
||||||
|
wordCount: integer("word_count").notNull(),
|
||||||
|
paragraphIndex: integer("paragraph_index").notNull(),
|
||||||
|
companyName: text("company_name").notNull(),
|
||||||
|
cik: text("cik").notNull(),
|
||||||
|
ticker: text("ticker"),
|
||||||
|
filingType: text("filing_type").notNull(),
|
||||||
|
filingDate: text("filing_date").notNull(),
|
||||||
|
fiscalYear: integer("fiscal_year").notNull(),
|
||||||
|
accessionNumber: text("accession_number").notNull(),
|
||||||
|
secItem: text("sec_item").notNull(),
|
||||||
|
// Stage 1 consensus (for stratification, not shown to annotators during labeling)
|
||||||
|
stage1Category: text("stage1_category"),
|
||||||
|
stage1Specificity: integer("stage1_specificity"),
|
||||||
|
stage1Method: text("stage1_method"),
|
||||||
|
stage1Confidence: real("stage1_confidence"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const annotators = pgTable("annotators", {
|
||||||
|
id: text("id").primaryKey(), // slug: "joey", "alice", etc.
|
||||||
|
displayName: text("display_name").notNull(),
|
||||||
|
password: text("password").notNull(), // plaintext (just their name)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const assignments = pgTable("assignments", {
|
||||||
|
paragraphId: text("paragraph_id").notNull().references(() => paragraphs.id),
|
||||||
|
annotatorId: text("annotator_id").notNull().references(() => annotators.id),
|
||||||
|
assignedAt: timestamp("assigned_at").notNull().defaultNow(),
|
||||||
|
isWarmup: boolean("is_warmup").notNull().default(false),
|
||||||
|
}, (t) => [unique().on(t.paragraphId, t.annotatorId)]);
|
||||||
|
|
||||||
|
export const humanLabels = pgTable("human_labels", {
|
||||||
|
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
paragraphId: text("paragraph_id").notNull().references(() => paragraphs.id),
|
||||||
|
annotatorId: text("annotator_id").notNull().references(() => annotators.id),
|
||||||
|
contentCategory: text("content_category").notNull(),
|
||||||
|
specificityLevel: integer("specificity_level").notNull(),
|
||||||
|
notes: text("notes"),
|
||||||
|
labeledAt: timestamp("labeled_at").notNull().defaultNow(),
|
||||||
|
sessionId: text("session_id").notNull(),
|
||||||
|
durationMs: integer("duration_ms"),
|
||||||
|
}, (t) => [unique().on(t.paragraphId, t.annotatorId)]);
|
||||||
|
|
||||||
|
export const quizSessions = pgTable("quiz_sessions", {
|
||||||
|
id: text("id").primaryKey(), // UUID
|
||||||
|
annotatorId: text("annotator_id").notNull().references(() => annotators.id),
|
||||||
|
startedAt: timestamp("started_at").notNull().defaultNow(),
|
||||||
|
completedAt: timestamp("completed_at"),
|
||||||
|
passed: boolean("passed").notNull().default(false),
|
||||||
|
score: integer("score").notNull().default(0),
|
||||||
|
totalQuestions: integer("total_questions").notNull(),
|
||||||
|
answers: text("answers").notNull().default("[]"), // JSON
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adjudications = pgTable("adjudications", {
|
||||||
|
paragraphId: text("paragraph_id").primaryKey().references(() => paragraphs.id),
|
||||||
|
finalCategory: text("final_category").notNull(),
|
||||||
|
finalSpecificity: integer("final_specificity").notNull(),
|
||||||
|
method: text("method").notNull(), // consensus | majority | discussion
|
||||||
|
adjudicatorId: text("adjudicator_id"),
|
||||||
|
notes: text("notes"),
|
||||||
|
resolvedAt: timestamp("resolved_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Labelapp file structure (no src/ — flat root, consistent with shadcn)
|
||||||
|
|
||||||
|
```
|
||||||
|
labelapp/
|
||||||
|
package.json
|
||||||
|
next.config.ts
|
||||||
|
tsconfig.json # @/* maps to ./*
|
||||||
|
drizzle.config.ts
|
||||||
|
playwright.config.ts
|
||||||
|
components.json # shadcn config
|
||||||
|
.env.local # DATABASE_URL
|
||||||
|
db/
|
||||||
|
index.ts # drizzle client
|
||||||
|
schema.ts # tables above
|
||||||
|
lib/
|
||||||
|
utils.ts # shadcn cn() helper
|
||||||
|
sampling.ts # stratified sampling logic
|
||||||
|
assignment.ts # BIBD assignment generation
|
||||||
|
metrics.ts # Cohen's kappa, Krippendorff's alpha
|
||||||
|
quiz-questions.ts # question bank
|
||||||
|
components/
|
||||||
|
ui/ # shadcn components
|
||||||
|
app/
|
||||||
|
layout.tsx # root layout
|
||||||
|
globals.css # Tailwind + shadcn theme
|
||||||
|
page.tsx # login screen
|
||||||
|
dashboard/
|
||||||
|
page.tsx # annotator dashboard (progress, start session)
|
||||||
|
quiz/
|
||||||
|
page.tsx # quiz flow
|
||||||
|
label/
|
||||||
|
page.tsx # main labeling interface
|
||||||
|
admin/
|
||||||
|
page.tsx # adjudication queue + metrics dashboard
|
||||||
|
api/
|
||||||
|
auth/route.ts # login/logout
|
||||||
|
quiz/route.ts # start quiz, submit answers
|
||||||
|
label/route.ts # get next paragraph, submit label
|
||||||
|
warmup/route.ts # get warmup paragraph, submit + get feedback
|
||||||
|
adjudicate/route.ts # get queue, resolve
|
||||||
|
metrics/route.ts # IRR metrics
|
||||||
|
export/route.ts # trigger gold label export
|
||||||
|
scripts/
|
||||||
|
seed.ts # import paragraphs + consensus from JSONL, create annotators
|
||||||
|
sample.ts # stratified sample → 1,200 paragraphs
|
||||||
|
assign.ts # BIBD assignment → 3,600 rows
|
||||||
|
export.ts # dump gold labels to JSONL (GoldLabel schema format)
|
||||||
|
tests/
|
||||||
|
helpers/
|
||||||
|
reset-db.ts
|
||||||
|
login.ts
|
||||||
|
00-setup.spec.ts
|
||||||
|
01-auth.spec.ts
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package.json scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"seed": "bun run scripts/seed.ts",
|
||||||
|
"sample": "bun run scripts/sample.ts",
|
||||||
|
"assign": "bun run scripts/assign.ts",
|
||||||
|
"export": "bun run scripts/export.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Core Features
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- Login page: annotator ID dropdown + password field (password = their name)
|
||||||
|
- Server-side session via Next.js cookies (signed, httpOnly)
|
||||||
|
- Middleware checks auth on all `/dashboard`, `/quiz`, `/label`, `/admin` routes
|
||||||
|
- No external auth library needed — just `cookies()` API
|
||||||
|
|
||||||
|
### Quiz System
|
||||||
|
- **Question bank** (~30 questions) in `lib/quiz-questions.ts`, 4 types:
|
||||||
|
- Person-vs-function (8-10): "Is this Management Role or RMP?"
|
||||||
|
- Materiality disclaimers (6-8): "Strategy Integration or None/Other?"
|
||||||
|
- QV fact counting (6-8): "Specificity 3 or 4?"
|
||||||
|
- SPAC exception (3-4): "What category for this shell company?"
|
||||||
|
- **Per session:** 8 questions (2 per type, random draw), pass = 7/8
|
||||||
|
- Immediate feedback with codebook explanation after each answer
|
||||||
|
- Failed → review mistakes → retry. Passed → proceed to warmup.
|
||||||
|
- Session stored in `quiz_sessions`, referenced by `human_labels.session_id`
|
||||||
|
- Session expires on 2-hour idle (checked server-side)
|
||||||
|
|
||||||
|
### Warm-up (5 paragraphs per session)
|
||||||
|
- Pre-selected paragraphs with known gold labels + feedback text
|
||||||
|
- Identical UI to labeling, but after submit: shows gold answer + explanation
|
||||||
|
- Not counted toward gold set (`assignments.is_warmup = true`)
|
||||||
|
|
||||||
|
### Labeling Interface
|
||||||
|
- **Top bar:** Filing metadata (company, ticker, filing type, date)
|
||||||
|
- **Center:** Paragraph text, large and readable
|
||||||
|
- **Form:**
|
||||||
|
- Content category: 7 radio buttons with short labels
|
||||||
|
- Specificity level: 4 radio buttons (Generic Boilerplate / Sector-Adapted / Firm-Specific / Quantified-Verifiable)
|
||||||
|
- Notes: optional textarea
|
||||||
|
- Submit button
|
||||||
|
- **Keyboard shortcuts:** 1-7 category, Shift+1-4 specificity, Enter submit
|
||||||
|
- **Sidebar (collapsible):** Codebook quick-reference (category defs, IS/NOT lists, decision rules)
|
||||||
|
- **Progress bar:** "47 / 600 completed"
|
||||||
|
- **Hidden timer:** tracks `duration_ms` per paragraph
|
||||||
|
|
||||||
|
### Sampling (1,200 from ~50K)
|
||||||
|
Script reads Stage 1 consensus from JSONL, stratifies:
|
||||||
|
|
||||||
|
| Stratum | Count | Source |
|
||||||
|
|---------|-------|--------|
|
||||||
|
| Mgmt↔RMP split votes | 120 | Paragraphs where Stage 1 annotators disagreed on this axis |
|
||||||
|
| None/Other↔Strategy splits | 80 | Materiality disclaimer boundary |
|
||||||
|
| Spec [3,4] splits | 80 | QV counting boundary |
|
||||||
|
| Board↔Mgmt splits | 80 | Board/management boundary |
|
||||||
|
| Rare category guarantee | 120 | ≥15 per category, extra for Incident Disclosure |
|
||||||
|
| Proportional stratified random | 720 | Fill from category×specificity cells |
|
||||||
|
|
||||||
|
### Assignment: BIBD
|
||||||
|
C(6,3)=20 unique triples of 6 annotators. Assign 60 paragraphs to each triple. Each annotator appears in C(5,2)=10 triples → 10×60 = 600 paragraphs. Every annotator pair shares equal overlap → pairwise kappa is statistically valid.
|
||||||
|
|
||||||
|
### Adjudication
|
||||||
|
**Auto-resolve** after 3 labels:
|
||||||
|
- 3/3 agree both dims → consensus
|
||||||
|
- 2/3 agree both dims → majority
|
||||||
|
- Otherwise → flagged for admin
|
||||||
|
|
||||||
|
**Admin page:**
|
||||||
|
- Queue sorted by severity (3-way splits first)
|
||||||
|
- Shows paragraph + all 3 labels side-by-side + notes + Stage 1 reference
|
||||||
|
- Resolution: pick a label, enter custom, or mark for team discussion
|
||||||
|
- Stores in `adjudications` table
|
||||||
|
|
||||||
|
### Metrics Dashboard (admin page)
|
||||||
|
- Overall progress: N/1,200 fully labeled, N adjudicated
|
||||||
|
- Cohen's Kappa (category): pairwise 6×6 matrix + average. Target ≥ 0.75
|
||||||
|
- Krippendorff's Alpha (specificity): single number. Target ≥ 0.67
|
||||||
|
- Raw consensus rate. Target ≥ 75%
|
||||||
|
- Per-category confusion matrix (7×7)
|
||||||
|
- Per-annotator stats: completion, agreement rate, distribution
|
||||||
|
- Confusion axis disagreement rates
|
||||||
|
|
||||||
|
### Export
|
||||||
|
Script dumps adjudicated gold labels to `data/gold/gold-labels.jsonl` in the existing `GoldLabel` schema format, readable by `readJsonl(path, GoldLabel)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reused Existing Code
|
||||||
|
|
||||||
|
| What | Source | Used by |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `ContentCategory`, `SpecificityLevel`, `LabelOutput` | `@sec-cybert/schemas/label.ts` | Label validation, quiz answers |
|
||||||
|
| `Paragraph`, `FilingMeta` | `@sec-cybert/schemas/paragraph.ts` | Seed script, display |
|
||||||
|
| `HumanLabel`, `GoldLabel` | `@sec-cybert/schemas/gold.ts` | Export script format |
|
||||||
|
| `Annotation` | `@sec-cybert/schemas/annotation.ts` | Seed script (Stage 1 data) |
|
||||||
|
| `readJsonl()` | `ts/src/lib/jsonl.ts` | Seed script (import from JSONL) |
|
||||||
|
| Codebook content | `docs/LABELING-CODEBOOK.md` | Quiz questions, sidebar reference |
|
||||||
|
|
||||||
|
Note: `readJsonl` stays in `ts/src/lib/` — the seed script imports it directly via relative path or we extract it to the schemas package if needed. Since it depends on Zod (which schemas already has), it could live there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup Sequence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Monorepo setup
|
||||||
|
bun install # from repo root, resolves all workspaces
|
||||||
|
|
||||||
|
# 2. Verify existing pipeline still works
|
||||||
|
bun run --filter ts typecheck
|
||||||
|
|
||||||
|
# 3. Start Postgres
|
||||||
|
cd labelapp && docker compose up -d
|
||||||
|
|
||||||
|
# 4. Push schema
|
||||||
|
bun run db:push
|
||||||
|
|
||||||
|
# 5. Seed data
|
||||||
|
bun run seed # imports paragraphs + consensus + creates annotators
|
||||||
|
|
||||||
|
# 6. Sample + assign
|
||||||
|
bun run sample # stratified sample → 1,200 paragraphs marked
|
||||||
|
bun run assign # BIBD → 3,600 assignment rows
|
||||||
|
|
||||||
|
# 7. Start dev server
|
||||||
|
bun run dev # Next.js on :3000
|
||||||
|
|
||||||
|
# After labeling complete:
|
||||||
|
bun run export # → data/gold/gold-labels.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. **Monorepo restructure** — root package.json, extract schemas, rewrite imports, verify typecheck
|
||||||
|
2. **Labelapp scaffold** — Next.js init, Drizzle schema, db connection, Playwright setup
|
||||||
|
3. **Seed + sample + assign scripts** — data pipeline into Postgres
|
||||||
|
4. **Auth** — login page, session cookies, middleware
|
||||||
|
5. **Quiz system** — question bank, quiz flow page, session gating
|
||||||
|
6. **Labeling UI** — the core: next paragraph, submit label, progress tracking, keyboard shortcuts, codebook sidebar
|
||||||
|
7. **Warm-up flow** — 5 pre-labeled paragraphs with feedback
|
||||||
|
8. **Admin: adjudication** — queue, resolution UI
|
||||||
|
9. **Admin: metrics dashboard** — kappa, alpha, confusion matrix, per-annotator stats
|
||||||
|
10. **Export script** — gold labels to JSONL
|
||||||
|
|
||||||
|
**Each phase ends with Playwright tests that verify it works E2E before moving on.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Philosophy
|
||||||
|
No unit tests. Integration/E2E only. Two layers:
|
||||||
|
1. **Backend route tests** (`bun test`) — hit real API routes against real Postgres, verify responses/DB state
|
||||||
|
2. **Playwright E2E** — click through the real UI in a real browser
|
||||||
|
|
||||||
|
### Backend Route Tests (bun test)
|
||||||
|
- Colocated `__test__` dirs adjacent to each route handler
|
||||||
|
- Tests import the route handler directly (or use `fetch` against the dev server)
|
||||||
|
- Run against real Postgres (same `DATABASE_URL`)
|
||||||
|
- Each test file resets relevant tables via Drizzle before running
|
||||||
|
|
||||||
|
```
|
||||||
|
labelapp/app/api/
|
||||||
|
auth/
|
||||||
|
route.ts
|
||||||
|
__test__/
|
||||||
|
auth.test.ts # login/logout, session validation
|
||||||
|
quiz/
|
||||||
|
route.ts
|
||||||
|
__test__/
|
||||||
|
quiz.test.ts # start quiz, submit answers, pass/fail logic
|
||||||
|
label/
|
||||||
|
route.ts
|
||||||
|
__test__/
|
||||||
|
label.test.ts # get next paragraph, submit label, skip completed
|
||||||
|
warmup/
|
||||||
|
route.ts
|
||||||
|
__test__/
|
||||||
|
warmup.test.ts # get warmup, submit + get feedback
|
||||||
|
adjudicate/
|
||||||
|
route.ts
|
||||||
|
__test__/
|
||||||
|
adjudicate.test.ts # get queue, resolve, verify DB state
|
||||||
|
metrics/
|
||||||
|
route.ts
|
||||||
|
__test__/
|
||||||
|
metrics.test.ts # kappa/alpha values, progress counts
|
||||||
|
export/
|
||||||
|
route.ts
|
||||||
|
__test__/
|
||||||
|
export.test.ts # trigger export, verify JSONL output
|
||||||
|
```
|
||||||
|
|
||||||
|
### Playwright E2E
|
||||||
|
- Playwright installed in `labelapp/` as dev dependency
|
||||||
|
- Tests in `labelapp/tests/` (Playwright default)
|
||||||
|
- `playwright.config.ts` configured to:
|
||||||
|
- Start Next.js dev server automatically via `webServer` config
|
||||||
|
- Use the real Postgres (same `DATABASE_URL`)
|
||||||
|
- Run tests serially (stateful DB)
|
||||||
|
- **Test DB reset:** Each test file starts by truncating relevant tables (not dropping — schema stays). A `tests/helpers/reset-db.ts` util handles this via Drizzle.
|
||||||
|
|
||||||
|
### Test Files & What They Cover
|
||||||
|
|
||||||
|
**`tests/00-setup.spec.ts` — Data pipeline scripts**
|
||||||
|
- Run `bun run seed` via `execSync`, verify paragraphs table has rows
|
||||||
|
- Run `bun run sample`, verify exactly 1,200 paragraphs marked (or a `sampled` flag / separate table)
|
||||||
|
- Run `bun run assign`, verify 3,600 assignment rows, each annotator has 600
|
||||||
|
- Verify BIBD property: every annotator pair shares equal paragraph count
|
||||||
|
|
||||||
|
**`tests/01-auth.spec.ts` — Login flow**
|
||||||
|
- Navigate to `/`, see login form
|
||||||
|
- Login with wrong password → error message shown
|
||||||
|
- Login with correct password → redirected to `/dashboard`
|
||||||
|
- Access `/dashboard` without login → redirected to `/`
|
||||||
|
- Logout → session cleared, redirected to `/`
|
||||||
|
|
||||||
|
**`tests/02-quiz.spec.ts` — Quiz gating**
|
||||||
|
- Login, navigate to dashboard, click "Start Session"
|
||||||
|
- Verify quiz page loads with 8 questions
|
||||||
|
- Answer all correctly → see "Passed" message, "Begin labeling" button appears
|
||||||
|
- Start new session, answer 2 wrong → see "Failed", can retry
|
||||||
|
- Verify cannot access `/label` without a passed quiz session
|
||||||
|
|
||||||
|
**`tests/03-warmup.spec.ts` — Warm-up flow**
|
||||||
|
- After passing quiz, verify 5 warm-up paragraphs shown first
|
||||||
|
- Submit a label → gold answer + explanation revealed
|
||||||
|
- After 5 warm-ups → transition to real labeling
|
||||||
|
- Verify warm-up labels are NOT counted in progress stats
|
||||||
|
|
||||||
|
**`tests/04-labeling.spec.ts` — Core labeling flow**
|
||||||
|
- Verify paragraph text + filing metadata displayed
|
||||||
|
- Select category via radio button, select specificity, submit
|
||||||
|
- Verify redirected to next paragraph, progress increments
|
||||||
|
- Verify keyboard shortcuts work (press "1" → first category selected, etc.)
|
||||||
|
- Submit several labels, verify they're stored in DB
|
||||||
|
- Verify codebook sidebar toggles open/close
|
||||||
|
- Verify "next" skips already-completed paragraphs (label one, refresh, get a different one)
|
||||||
|
|
||||||
|
**`tests/05-adjudication.spec.ts` — Multi-annotator + admin flow**
|
||||||
|
- Seed 3 test annotators with assignments for the same paragraph
|
||||||
|
- Login as each, pass quiz, label the same paragraph with DIFFERENT labels
|
||||||
|
- Login as admin, navigate to `/admin`
|
||||||
|
- Verify the disputed paragraph appears in the adjudication queue
|
||||||
|
- Resolve it (pick one label), verify adjudication stored
|
||||||
|
- Verify it no longer appears in queue
|
||||||
|
|
||||||
|
**`tests/06-metrics.spec.ts` — Dashboard metrics**
|
||||||
|
- Seed known label data (pre-computed expected kappa/alpha values)
|
||||||
|
- Navigate to admin metrics page
|
||||||
|
- Verify progress numbers match expected
|
||||||
|
- Verify consensus rate displayed and reasonable
|
||||||
|
- Verify per-annotator stats shown
|
||||||
|
|
||||||
|
**`tests/07-export.spec.ts` — Gold export**
|
||||||
|
- Ensure some adjudicated labels exist (from prior tests or seeded)
|
||||||
|
- Run `bun run export` via `execSync`
|
||||||
|
- Read the output JSONL file
|
||||||
|
- Parse each line, verify it matches GoldLabel schema
|
||||||
|
- Verify paragraph count matches expected
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd labelapp
|
||||||
|
|
||||||
|
# Backend route tests (fast, no browser)
|
||||||
|
bun test # runs all __test__/*.test.ts
|
||||||
|
bun test app/api/label/__test__/ # run one route's tests
|
||||||
|
|
||||||
|
# Playwright E2E (browser)
|
||||||
|
bunx playwright install --with-deps chromium # one-time browser install
|
||||||
|
bunx playwright test # runs all tests serially
|
||||||
|
bunx playwright test tests/04-labeling.spec.ts # run one file
|
||||||
|
|
||||||
|
# Both
|
||||||
|
bun test && bunx playwright test # full suite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package.json test scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test && playwright test",
|
||||||
|
"test:api": "bun test",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How Agents Use This
|
||||||
|
|
||||||
|
Each implementation phase follows this cycle:
|
||||||
|
1. Write the feature code
|
||||||
|
2. Write the backend route test (`__test__/` adjacent to route) AND/OR Playwright test
|
||||||
|
3. Run `bun test` for route logic, `bunx playwright test` for UI flows
|
||||||
|
4. If tests fail, fix the code and re-run
|
||||||
|
5. Only move to the next phase when all tests pass
|
||||||
|
|
||||||
|
The test suite is cumulative — tests from earlier phases keep running, ensuring nothing regresses. An agent completing phase 6 (labeling UI) runs the full suite to confirm everything still works.
|
||||||
|
|
||||||
|
### Test Helpers
|
||||||
|
|
||||||
|
**Shared** (`src/lib/__test__/helpers.ts` or similar):
|
||||||
|
- `resetDb()` — truncate tables between test files via Drizzle
|
||||||
|
- `seedTestData()` — insert known paragraphs/assignments/labels for test scenarios
|
||||||
|
|
||||||
|
**Playwright-specific** (`tests/helpers/`):
|
||||||
|
- `login.ts` — reusable: login as annotator X, pass quiz, get to labeling
|
||||||
|
- `reset-db.ts` — calls resetDb() for Playwright test setup
|
||||||
|
|
||||||
|
The `login.ts` helper is critical — it encapsulates the login → quiz → warmup flow so that labeling/adjudication tests don't have to repeat that ceremony.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification (automated)
|
||||||
|
|
||||||
|
Two test suites, both must pass:
|
||||||
|
|
||||||
|
### `bun test` — Backend route integration tests
|
||||||
|
| Route | Test file | Verifies |
|
||||||
|
|-------|-----------|----------|
|
||||||
|
| `api/auth` | `__test__/auth.test.ts` | Login/logout, bad password rejection, session cookies |
|
||||||
|
| `api/quiz` | `__test__/quiz.test.ts` | Start quiz, submit answers, pass/fail threshold, session creation |
|
||||||
|
| `api/label` | `__test__/label.test.ts` | Get next paragraph, submit label to DB, skip completed, enforce quiz gate |
|
||||||
|
| `api/warmup` | `__test__/warmup.test.ts` | Get warmup paragraph, submit + receive gold feedback |
|
||||||
|
| `api/adjudicate` | `__test__/adjudicate.test.ts` | Get disagreement queue, resolve, verify DB state |
|
||||||
|
| `api/metrics` | `__test__/metrics.test.ts` | Kappa/alpha with known data, progress counts |
|
||||||
|
| `api/export` | `__test__/export.test.ts` | Trigger export, verify JSONL matches GoldLabel schema |
|
||||||
|
|
||||||
|
### `bunx playwright test` — Browser E2E
|
||||||
|
| Test file | Verifies |
|
||||||
|
|-----------|----------|
|
||||||
|
| `00-setup.spec.ts` | Seed/sample/assign scripts produce correct DB state |
|
||||||
|
| `01-auth.spec.ts` | Login form, redirect on auth failure, logout |
|
||||||
|
| `02-quiz.spec.ts` | Quiz renders, pass/fail gating, retry flow |
|
||||||
|
| `03-warmup.spec.ts` | 5 warm-ups with feedback, transition to real labeling |
|
||||||
|
| `04-labeling.spec.ts` | Paragraph display, radio buttons, keyboard shortcuts, progress bar, codebook sidebar |
|
||||||
|
| `05-adjudication.spec.ts` | 3 annotators disagree → admin queue → resolution |
|
||||||
|
| `06-metrics.spec.ts` | Dashboard renders with correct numbers |
|
||||||
|
| `07-export.spec.ts` | Export script produces valid JSONL |
|
||||||
|
|
||||||
|
### Pre-test gates
|
||||||
|
- `bun install` from root succeeds
|
||||||
|
- `bun run --filter ts typecheck` passes (monorepo didn't break existing pipeline)
|
||||||
|
- Postgres is reachable, `bun run db:push` succeeds
|
||||||
|
|
||||||
|
**Success criteria:** `cd labelapp && bun test && bunx playwright test` exits 0.
|
||||||
41
labelapp/.gitignore
vendored
Normal file
41
labelapp/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
5
labelapp/AGENTS.md
Normal file
5
labelapp/AGENTS.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
1
labelapp/CLAUDE.md
Normal file
1
labelapp/CLAUDE.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
@AGENTS.md
|
||||||
36
labelapp/README.md
Normal file
36
labelapp/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
BIN
labelapp/app/favicon.ico
Normal file
BIN
labelapp/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
130
labelapp/app/globals.css
Normal file
130
labelapp/app/globals.css
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
labelapp/app/layout.tsx
Normal file
33
labelapp/app/layout.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
|
>
|
||||||
|
<body className="min-h-full flex flex-col">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
labelapp/app/page.tsx
Normal file
65
labelapp/app/page.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||||
|
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/next.svg"
|
||||||
|
alt="Next.js logo"
|
||||||
|
width={100}
|
||||||
|
height={20}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||||
|
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||||
|
To get started, edit the page.tsx file.
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||||
|
Looking for a starting point or more instructions? Head over to{" "}
|
||||||
|
<a
|
||||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
>
|
||||||
|
Templates
|
||||||
|
</a>{" "}
|
||||||
|
or the{" "}
|
||||||
|
<a
|
||||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
>
|
||||||
|
Learning
|
||||||
|
</a>{" "}
|
||||||
|
center.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||||
|
<a
|
||||||
|
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||||
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/vercel.svg"
|
||||||
|
alt="Vercel logomark"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Deploy Now
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||||
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
labelapp/components.json
Normal file
25
labelapp/components.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "base-nova",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
60
labelapp/components/ui/button.tsx
Normal file
60
labelapp/components/ui/button.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
outline:
|
||||||
|
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||||
|
icon: "size-8",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm":
|
||||||
|
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||||
|
"icon-lg": "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||||
|
return (
|
||||||
|
<ButtonPrimitive
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
8
labelapp/db/index.ts
Normal file
8
labelapp/db/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL!;
|
||||||
|
|
||||||
|
const client = postgres(connectionString);
|
||||||
|
export const db = drizzle(client, { schema });
|
||||||
95
labelapp/db/schema.ts
Normal file
95
labelapp/db/schema.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
integer,
|
||||||
|
real,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
unique,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const paragraphs = pgTable("paragraphs", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
text: text("text").notNull(),
|
||||||
|
wordCount: integer("word_count").notNull(),
|
||||||
|
paragraphIndex: integer("paragraph_index").notNull(),
|
||||||
|
companyName: text("company_name").notNull(),
|
||||||
|
cik: text("cik").notNull(),
|
||||||
|
ticker: text("ticker"),
|
||||||
|
filingType: text("filing_type").notNull(),
|
||||||
|
filingDate: text("filing_date").notNull(),
|
||||||
|
fiscalYear: integer("fiscal_year").notNull(),
|
||||||
|
accessionNumber: text("accession_number").notNull(),
|
||||||
|
secItem: text("sec_item").notNull(),
|
||||||
|
// Stage 1 consensus (for stratification, not shown to annotators during labeling)
|
||||||
|
stage1Category: text("stage1_category"),
|
||||||
|
stage1Specificity: integer("stage1_specificity"),
|
||||||
|
stage1Method: text("stage1_method"),
|
||||||
|
stage1Confidence: real("stage1_confidence"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const annotators = pgTable("annotators", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
displayName: text("display_name").notNull(),
|
||||||
|
password: text("password").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const assignments = pgTable(
|
||||||
|
"assignments",
|
||||||
|
{
|
||||||
|
paragraphId: text("paragraph_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => paragraphs.id),
|
||||||
|
annotatorId: text("annotator_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => annotators.id),
|
||||||
|
assignedAt: timestamp("assigned_at").notNull().defaultNow(),
|
||||||
|
isWarmup: boolean("is_warmup").notNull().default(false),
|
||||||
|
},
|
||||||
|
(t) => [unique().on(t.paragraphId, t.annotatorId)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const humanLabels = pgTable(
|
||||||
|
"human_labels",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
paragraphId: text("paragraph_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => paragraphs.id),
|
||||||
|
annotatorId: text("annotator_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => annotators.id),
|
||||||
|
contentCategory: text("content_category").notNull(),
|
||||||
|
specificityLevel: integer("specificity_level").notNull(),
|
||||||
|
notes: text("notes"),
|
||||||
|
labeledAt: timestamp("labeled_at").notNull().defaultNow(),
|
||||||
|
sessionId: text("session_id").notNull(),
|
||||||
|
durationMs: integer("duration_ms"),
|
||||||
|
},
|
||||||
|
(t) => [unique().on(t.paragraphId, t.annotatorId)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const quizSessions = pgTable("quiz_sessions", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
annotatorId: text("annotator_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => annotators.id),
|
||||||
|
startedAt: timestamp("started_at").notNull().defaultNow(),
|
||||||
|
completedAt: timestamp("completed_at"),
|
||||||
|
passed: boolean("passed").notNull().default(false),
|
||||||
|
score: integer("score").notNull().default(0),
|
||||||
|
totalQuestions: integer("total_questions").notNull(),
|
||||||
|
answers: text("answers").notNull().default("[]"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adjudications = pgTable("adjudications", {
|
||||||
|
paragraphId: text("paragraph_id")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => paragraphs.id),
|
||||||
|
finalCategory: text("final_category").notNull(),
|
||||||
|
finalSpecificity: integer("final_specificity").notNull(),
|
||||||
|
method: text("method").notNull(),
|
||||||
|
adjudicatorId: text("adjudicator_id"),
|
||||||
|
notes: text("notes"),
|
||||||
|
resolvedAt: timestamp("resolved_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
9
labelapp/drizzle.config.ts
Normal file
9
labelapp/drizzle.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./db/schema.ts",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
});
|
||||||
18
labelapp/eslint.config.mjs
Normal file
18
labelapp/eslint.config.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
6
labelapp/lib/utils.ts
Normal file
6
labelapp/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
7
labelapp/next.config.ts
Normal file
7
labelapp/next.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
56
labelapp/package.json
Normal file
56
labelapp/package.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "labelapp",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"seed": "bun run scripts/seed.ts",
|
||||||
|
"sample": "bun run scripts/sample.ts",
|
||||||
|
"assign": "bun run scripts/assign.ts",
|
||||||
|
"export": "bun run scripts/export.ts",
|
||||||
|
"test": "bun test && playwright test",
|
||||||
|
"test:api": "bun test",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.3.0",
|
||||||
|
"@sec-cybert/schemas": "workspace:*",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"lucide-react": "^1.7.0",
|
||||||
|
"next": "16.2.1",
|
||||||
|
"postgres": "^3.4.8",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"shadcn": "^4.1.1",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tw-animate-css": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"drizzle-kit": "^0.31.10",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.2.1",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"ignoreScripts": [
|
||||||
|
"sharp",
|
||||||
|
"unrs-resolver"
|
||||||
|
],
|
||||||
|
"trustedDependencies": [
|
||||||
|
"sharp",
|
||||||
|
"unrs-resolver"
|
||||||
|
]
|
||||||
|
}
|
||||||
25
labelapp/playwright.config.ts
Normal file
25
labelapp/playwright.config.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
fullyParallel: false,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: 0,
|
||||||
|
workers: 1,
|
||||||
|
reporter: "html",
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: "bun run dev",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
7
labelapp/postcss.config.mjs
Normal file
7
labelapp/postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
labelapp/public/file.svg
Normal file
1
labelapp/public/file.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
labelapp/public/globe.svg
Normal file
1
labelapp/public/globe.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
labelapp/public/next.svg
Normal file
1
labelapp/public/next.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
labelapp/public/vercel.svg
Normal file
1
labelapp/public/vercel.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
labelapp/public/window.svg
Normal file
1
labelapp/public/window.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
34
labelapp/tsconfig.json
Normal file
34
labelapp/tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "sec-cybert-monorepo",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": ["packages/*", "ts", "labelapp"]
|
||||||
|
}
|
||||||
12
packages/schemas/package.json
Normal file
12
packages/schemas/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@sec-cybert/schemas",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./*.ts": "./src/*.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/schemas/tsconfig.json
Normal file
16
packages/schemas/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openrouter/ai-sdk-provider": "^2.3.3",
|
"@openrouter/ai-sdk-provider": "^2.3.3",
|
||||||
|
"@sec-cybert/schemas": "workspace:*",
|
||||||
"ai": "^6.0.141",
|
"ai": "^6.0.141",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"p-limit": "^7.3.0",
|
"p-limit": "^7.3.0",
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Usage: bun ts/scripts/dispute-crosstab.ts
|
* Usage: bun ts/scripts/dispute-crosstab.ts
|
||||||
*/
|
*/
|
||||||
import { readJsonlRaw, readJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonlRaw, readJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
const ANN_PATH = new URL("../../data/annotations/stage1.jsonl", import.meta.url).pathname;
|
const ANN_PATH = new URL("../../data/annotations/stage1.jsonl", import.meta.url).pathname;
|
||||||
const PARA_PATH = new URL("../../data/paragraphs/paragraphs-clean.jsonl", import.meta.url).pathname;
|
const PARA_PATH = new URL("../../data/paragraphs/paragraphs-clean.jsonl", import.meta.url).pathname;
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
import { generateText, tool, Output } from "ai";
|
import { generateText, tool, Output } from "ai";
|
||||||
import { openrouter, providerOf } from "../src/lib/openrouter.ts";
|
import { openrouter, providerOf } from "../src/lib/openrouter.ts";
|
||||||
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { LabelOutputRaw, toLabelOutput } from "../src/schemas/label.ts";
|
import { LabelOutputRaw, toLabelOutput } from "@sec-cybert/schemas/label.ts";
|
||||||
import { SYSTEM_PROMPT, buildJudgePrompt, PROMPT_VERSION } from "../src/label/prompts.ts";
|
import { SYSTEM_PROMPT, buildJudgePrompt, PROMPT_VERSION } from "../src/label/prompts.ts";
|
||||||
import { withRetry } from "../src/lib/retry.ts";
|
import { withRetry } from "../src/lib/retry.ts";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
import { generateText, Output } from "ai";
|
import { generateText, Output } from "ai";
|
||||||
import { openrouter } from "../src/lib/openrouter.ts";
|
import { openrouter } from "../src/lib/openrouter.ts";
|
||||||
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { LabelOutputRaw } from "../src/schemas/label.ts";
|
import { LabelOutputRaw } from "@sec-cybert/schemas/label.ts";
|
||||||
import { SYSTEM_PROMPT, buildJudgePrompt } from "../src/label/prompts.ts";
|
import { SYSTEM_PROMPT, buildJudgePrompt } from "../src/label/prompts.ts";
|
||||||
|
|
||||||
const MODEL = process.argv[2] ?? "z-ai/glm-5";
|
const MODEL = process.argv[2] ?? "z-ai/glm-5";
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
import { generateText, Output } from "ai";
|
import { generateText, Output } from "ai";
|
||||||
import { openrouter } from "../src/lib/openrouter.ts";
|
import { openrouter } from "../src/lib/openrouter.ts";
|
||||||
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { LabelOutputRaw } from "../src/schemas/label.ts";
|
import { LabelOutputRaw } from "@sec-cybert/schemas/label.ts";
|
||||||
import { SYSTEM_PROMPT, buildJudgePrompt } from "../src/label/prompts.ts";
|
import { SYSTEM_PROMPT, buildJudgePrompt } from "../src/label/prompts.ts";
|
||||||
|
|
||||||
const PID = process.argv[2];
|
const PID = process.argv[2];
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* Usage: bun ts/scripts/mimo-pilot.ts
|
* Usage: bun ts/scripts/mimo-pilot.ts
|
||||||
*/
|
*/
|
||||||
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
||||||
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
import { generateText } from "ai";
|
import { generateText } from "ai";
|
||||||
import { openrouter } from "../src/lib/openrouter.ts";
|
import { openrouter } from "../src/lib/openrouter.ts";
|
||||||
import { readJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { SYSTEM_PROMPT, buildUserPrompt } from "../src/label/prompts.ts";
|
import { SYSTEM_PROMPT, buildUserPrompt } from "../src/label/prompts.ts";
|
||||||
import { LabelOutputRaw } from "../src/schemas/label.ts";
|
import { LabelOutputRaw } from "@sec-cybert/schemas/label.ts";
|
||||||
|
|
||||||
const INPUT = new URL("../../data/paragraphs/training.jsonl", import.meta.url).pathname;
|
const INPUT = new URL("../../data/paragraphs/training.jsonl", import.meta.url).pathname;
|
||||||
const MODEL = "xiaomi/mimo-v2-flash";
|
const MODEL = "xiaomi/mimo-v2-flash";
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* Usage: bun ts/scripts/mimo-test.ts
|
* Usage: bun ts/scripts/mimo-test.ts
|
||||||
*/
|
*/
|
||||||
import { readJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { annotateParagraph } from "../src/label/annotate.ts";
|
import { annotateParagraph } from "../src/label/annotate.ts";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
* --smoke: run only 5 paragraphs to check schema compliance
|
* --smoke: run only 5 paragraphs to check schema compliance
|
||||||
*/
|
*/
|
||||||
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
||||||
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
* Usage: bun ts/scripts/model-bias-analysis.ts
|
* Usage: bun ts/scripts/model-bias-analysis.ts
|
||||||
*/
|
*/
|
||||||
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
const PARAGRAPHS_PATH = new URL(
|
const PARAGRAPHS_PATH = new URL(
|
||||||
"../../data/paragraphs/paragraphs-clean.jsonl",
|
"../../data/paragraphs/paragraphs-clean.jsonl",
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
import { generateText, Output } from "ai";
|
import { generateText, Output } from "ai";
|
||||||
import { openrouter } from "../src/lib/openrouter.ts";
|
import { openrouter } from "../src/lib/openrouter.ts";
|
||||||
import { LabelOutput } from "../src/schemas/label.ts";
|
import { LabelOutput } from "@sec-cybert/schemas/label.ts";
|
||||||
import { SYSTEM_PROMPT, buildUserPrompt } from "../src/label/prompts.ts";
|
import { SYSTEM_PROMPT, buildUserPrompt } from "../src/label/prompts.ts";
|
||||||
import type { Paragraph } from "../src/schemas/paragraph.ts";
|
import type { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
const TEST_PARAGRAPH: Paragraph = {
|
const TEST_PARAGRAPH: Paragraph = {
|
||||||
id: "00000000-0000-0000-0000-000000000001",
|
id: "00000000-0000-0000-0000-000000000001",
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { readJsonl, writeJsonl, appendJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonl, writeJsonl, appendJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { STAGE1_MODELS } from "../src/lib/openrouter.ts";
|
import { STAGE1_MODELS } from "../src/lib/openrouter.ts";
|
||||||
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
||||||
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
* Usage: bun ts/scripts/sample-disputes.ts
|
* Usage: bun ts/scripts/sample-disputes.ts
|
||||||
*/
|
*/
|
||||||
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
const PARAGRAPHS = new URL("../../data/paragraphs/paragraphs-clean.jsonl", import.meta.url).pathname;
|
const PARAGRAPHS = new URL("../../data/paragraphs/paragraphs-clean.jsonl", import.meta.url).pathname;
|
||||||
const ANNOTATIONS = new URL("../../data/annotations/stage1.jsonl", import.meta.url).pathname;
|
const ANNOTATIONS = new URL("../../data/annotations/stage1.jsonl", import.meta.url).pathname;
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
* Usage: bun ts/scripts/segment-analysis.ts
|
* Usage: bun ts/scripts/segment-analysis.ts
|
||||||
*/
|
*/
|
||||||
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
const PARAGRAPHS = new URL("../../data/paragraphs/paragraphs-clean.jsonl", import.meta.url).pathname;
|
const PARAGRAPHS = new URL("../../data/paragraphs/paragraphs-clean.jsonl", import.meta.url).pathname;
|
||||||
const ANNOTATIONS = new URL("../../data/annotations/stage1.jsonl", import.meta.url).pathname;
|
const ANNOTATIONS = new URL("../../data/annotations/stage1.jsonl", import.meta.url).pathname;
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
* ../data/annotations/stage1.jsonl — one Annotation per (paragraph, model) pair
|
* ../data/annotations/stage1.jsonl — one Annotation per (paragraph, model) pair
|
||||||
*/
|
*/
|
||||||
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
import { readJsonl, readJsonlRaw, appendJsonl } from "../src/lib/jsonl.ts";
|
||||||
import { Paragraph } from "../src/schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { STAGE1_MODELS } from "../src/lib/openrouter.ts";
|
import { STAGE1_MODELS } from "../src/lib/openrouter.ts";
|
||||||
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
import { annotateParagraph, type AnnotateOpts } from "../src/label/annotate.ts";
|
||||||
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
import { PROMPT_VERSION } from "../src/label/prompts.ts";
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
* Produces a comprehensive breakdown saved as JSON + human-readable report.
|
* Produces a comprehensive breakdown saved as JSON + human-readable report.
|
||||||
*/
|
*/
|
||||||
import { readJsonl } from "../lib/jsonl.ts";
|
import { readJsonl } from "../lib/jsonl.ts";
|
||||||
import { Paragraph } from "../schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import type { Paragraph as ParagraphType } from "../schemas/paragraph.ts";
|
import type { Paragraph as ParagraphType } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { writeFile, mkdir } from "node:fs/promises";
|
import { writeFile, mkdir } from "node:fs/promises";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
* - data/analysis/quality-report.json (machine-readable)
|
* - data/analysis/quality-report.json (machine-readable)
|
||||||
*/
|
*/
|
||||||
import { readJsonl, writeJsonl } from "../lib/jsonl.ts";
|
import { readJsonl, writeJsonl } from "../lib/jsonl.ts";
|
||||||
import { Paragraph } from "../schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import type { Paragraph as ParagraphType } from "../schemas/paragraph.ts";
|
import type { Paragraph as ParagraphType } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { writeFile, mkdir } from "node:fs/promises";
|
import { writeFile, mkdir } from "node:fs/promises";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
* Tracks cross-filing and cross-year persistence of boilerplate text.
|
* Tracks cross-filing and cross-year persistence of boilerplate text.
|
||||||
*/
|
*/
|
||||||
import { readJsonl } from "../lib/jsonl.ts";
|
import { readJsonl } from "../lib/jsonl.ts";
|
||||||
import { Paragraph } from "../schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import type { Paragraph as ParagraphType } from "../schemas/paragraph.ts";
|
import type { Paragraph as ParagraphType } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { writeFile, mkdir } from "node:fs/promises";
|
import { writeFile, mkdir } from "node:fs/promises";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { readJsonl } from "./lib/jsonl.ts";
|
import { readJsonl } from "./lib/jsonl.ts";
|
||||||
import { Paragraph } from "./schemas/paragraph.ts";
|
import { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { Annotation } from "./schemas/annotation.ts";
|
import { Annotation } from "@sec-cybert/schemas/annotation.ts";
|
||||||
import { STAGE1_MODELS } from "./lib/openrouter.ts";
|
import { STAGE1_MODELS } from "./lib/openrouter.ts";
|
||||||
import { runBatch } from "./label/batch.ts";
|
import { runBatch } from "./label/batch.ts";
|
||||||
import { computeConsensus } from "./label/consensus.ts";
|
import { computeConsensus } from "./label/consensus.ts";
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { segmentParagraphs } from "./segment.ts";
|
import { segmentParagraphs } from "./segment.ts";
|
||||||
import type { FilingMeta, Paragraph } from "../schemas/paragraph.ts";
|
import type { FilingMeta, Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
const HTML_CACHE_DIR = "../data/raw/html";
|
const HTML_CACHE_DIR = "../data/raw/html";
|
||||||
const OUTPUT_PATH = "../data/paragraphs/paragraphs.jsonl";
|
const OUTPUT_PATH = "../data/paragraphs/paragraphs.jsonl";
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { appendJsonl } from "../lib/jsonl.ts";
|
|||||||
import { loadCompletedIds } from "../lib/checkpoint.ts";
|
import { loadCompletedIds } from "../lib/checkpoint.ts";
|
||||||
import { writeFile, mkdir } from "node:fs/promises";
|
import { writeFile, mkdir } from "node:fs/promises";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import type { FilingMeta } from "../schemas/paragraph.ts";
|
import type { FilingMeta } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
interface ExtractOpts {
|
interface ExtractOpts {
|
||||||
outputPath: string;
|
outputPath: string;
|
||||||
@ -272,7 +272,7 @@ export async function reparse10K(opts: { outputPath: string }): Promise<void> {
|
|||||||
let totalParagraphs = 0;
|
let totalParagraphs = 0;
|
||||||
|
|
||||||
const { writeJsonl } = await import("../lib/jsonl.ts");
|
const { writeJsonl } = await import("../lib/jsonl.ts");
|
||||||
const allParagraphs: import("../schemas/paragraph.ts").Paragraph[] = [];
|
const allParagraphs: import("@sec-cybert/schemas/paragraph.ts").Paragraph[] = [];
|
||||||
|
|
||||||
for (const file of htmlFiles) {
|
for (const file of htmlFiles) {
|
||||||
const accession = file.replace(".html", "");
|
const accession = file.replace(".html", "");
|
||||||
@ -359,7 +359,7 @@ export async function reparse8K(opts: { outputPath: string }): Promise<void> {
|
|||||||
let totalParagraphs = 0;
|
let totalParagraphs = 0;
|
||||||
|
|
||||||
const { writeJsonl } = await import("../lib/jsonl.ts");
|
const { writeJsonl } = await import("../lib/jsonl.ts");
|
||||||
const allParagraphs: import("../schemas/paragraph.ts").Paragraph[] = [];
|
const allParagraphs: import("@sec-cybert/schemas/paragraph.ts").Paragraph[] = [];
|
||||||
|
|
||||||
for (const accession of htmlFiles) {
|
for (const accession of htmlFiles) {
|
||||||
const meta = accMeta[accession]!;
|
const meta = accMeta[accession]!;
|
||||||
@ -418,7 +418,7 @@ export async function mergeTrainingData(opts: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { writeJsonl } = await import("../lib/jsonl.ts");
|
const { writeJsonl } = await import("../lib/jsonl.ts");
|
||||||
const { readJsonl } = await import("../lib/jsonl.ts");
|
const { readJsonl } = await import("../lib/jsonl.ts");
|
||||||
const { Paragraph } = await import("../schemas/paragraph.ts");
|
const { Paragraph } = await import("@sec-cybert/schemas/paragraph.ts");
|
||||||
|
|
||||||
// Load both corpora
|
// Load both corpora
|
||||||
const { records: tenK, skipped: s1 } = await readJsonl(opts.tenKPath, Paragraph);
|
const { records: tenK, skipped: s1 } = await readJsonl(opts.tenKPath, Paragraph);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import type { Paragraph, FilingMeta } from "../schemas/paragraph.ts";
|
import type { Paragraph, FilingMeta } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
const MIN_WORDS = 20;
|
const MIN_WORDS = 20;
|
||||||
const MAX_WORDS = 500;
|
const MAX_WORDS = 500;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { generateText, Output } from "ai";
|
import { generateText, Output } from "ai";
|
||||||
import { openrouter, providerOf } from "../lib/openrouter.ts";
|
import { openrouter, providerOf } from "../lib/openrouter.ts";
|
||||||
import { LabelOutputRaw, toLabelOutput } from "../schemas/label.ts";
|
import { LabelOutputRaw, toLabelOutput } from "@sec-cybert/schemas/label.ts";
|
||||||
import type { Annotation } from "../schemas/annotation.ts";
|
import type { Annotation } from "@sec-cybert/schemas/annotation.ts";
|
||||||
import type { Paragraph } from "../schemas/paragraph.ts";
|
import type { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { SYSTEM_PROMPT, buildUserPrompt, buildJudgePrompt, PROMPT_VERSION } from "./prompts.ts";
|
import { SYSTEM_PROMPT, buildUserPrompt, buildJudgePrompt, PROMPT_VERSION } from "./prompts.ts";
|
||||||
import { withRetry } from "../lib/retry.ts";
|
import { withRetry } from "../lib/retry.ts";
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,9 @@ import { v4 as uuidv4 } from "uuid";
|
|||||||
import { loadCompletedIds } from "../lib/checkpoint.ts";
|
import { loadCompletedIds } from "../lib/checkpoint.ts";
|
||||||
import { appendJsonl } from "../lib/jsonl.ts";
|
import { appendJsonl } from "../lib/jsonl.ts";
|
||||||
import { classifyError } from "../lib/retry.ts";
|
import { classifyError } from "../lib/retry.ts";
|
||||||
import type { Annotation } from "../schemas/annotation.ts";
|
import type { Annotation } from "@sec-cybert/schemas/annotation.ts";
|
||||||
import type { SessionLog } from "../schemas/session.ts";
|
import type { SessionLog } from "@sec-cybert/schemas/session.ts";
|
||||||
import type { Paragraph } from "../schemas/paragraph.ts";
|
import type { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
import { annotateParagraph, type AnnotateOpts } from "./annotate.ts";
|
import { annotateParagraph, type AnnotateOpts } from "./annotate.ts";
|
||||||
import { PROMPT_VERSION } from "./prompts.ts";
|
import { PROMPT_VERSION } from "./prompts.ts";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Annotation } from "../schemas/annotation.ts";
|
import type { Annotation } from "@sec-cybert/schemas/annotation.ts";
|
||||||
import type { ConsensusResult } from "../schemas/consensus.ts";
|
import type { ConsensusResult } from "@sec-cybert/schemas/consensus.ts";
|
||||||
import type { LabelOutput } from "../schemas/label.ts";
|
import type { LabelOutput } from "@sec-cybert/schemas/label.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute consensus from 3 Stage 1 annotations for a single paragraph.
|
* Compute consensus from 3 Stage 1 annotations for a single paragraph.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { Paragraph } from "../schemas/paragraph.ts";
|
import type { Paragraph } from "@sec-cybert/schemas/paragraph.ts";
|
||||||
|
|
||||||
export const PROMPT_VERSION = "v2.5";
|
export const PROMPT_VERSION = "v2.5";
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
export {
|
|
||||||
ContentCategory,
|
|
||||||
SpecificityLevel,
|
|
||||||
Confidence,
|
|
||||||
LabelOutput,
|
|
||||||
} from "./label.ts";
|
|
||||||
|
|
||||||
export { FilingMeta, Paragraph } from "./paragraph.ts";
|
|
||||||
|
|
||||||
export { Provenance, Annotation } from "./annotation.ts";
|
|
||||||
|
|
||||||
export { ConsensusResult } from "./consensus.ts";
|
|
||||||
|
|
||||||
export { HumanLabel, GoldLabel } from "./gold.ts";
|
|
||||||
|
|
||||||
export { BenchmarkResult } from "./benchmark.ts";
|
|
||||||
|
|
||||||
export { SessionLog } from "./session.ts";
|
|
||||||
@ -26,5 +26,5 @@
|
|||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts", "../packages/schemas/src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user