initial public release

This commit is contained in:
2026-07-06 11:05:50 -04:00
commit ca518c5f8a
94 changed files with 15699 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
package store
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"sort"
"strings"
_ "modernc.org/sqlite"
)
//go:embed migrations/*.sql
var migrationFS embed.FS
// DB wraps the SQLite connection and migrations.
type DB struct {
conn *sql.DB
}
// NewDB opens or creates the SQLite database and runs migrations.
func NewDB(dbPath string) (*DB, error) {
conn, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// SQLite writer serialization: one open conn avoids BUSY retries under parallel goroutines.
conn.SetMaxOpenConns(1)
// Test connection
if err := conn.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
if _, err := conn.Exec("PRAGMA foreign_keys = ON"); err != nil {
conn.Close()
return nil, fmt.Errorf("enable foreign keys: %w", err)
}
db := &DB{conn: conn}
// Run migrations
if err := db.runMigrations(); err != nil {
conn.Close()
return nil, err
}
return db, nil
}
// runMigrations applies all SQL migrations from the migrations/ directory.
// Each migration is applied exactly once — tracked via the schema_migrations table.
// This is required because ALTER TABLE ADD COLUMN is not idempotent in SQLite.
func (db *DB) runMigrations() error {
if _, err := db.conn.Exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`); err != nil {
return fmt.Errorf("failed to create schema_migrations: %w", err)
}
applied := make(map[string]bool)
rows, err := db.conn.Query(`SELECT name FROM schema_migrations`)
if err != nil {
return fmt.Errorf("failed to read schema_migrations: %w", err)
}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
rows.Close()
return fmt.Errorf("failed to scan migration name: %w", err)
}
applied[name] = true
}
rows.Close()
entries, err := fs.ReadDir(migrationFS, "migrations")
if err != nil {
return fmt.Errorf("failed to read migrations: %w", err)
}
var files []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sql") {
files = append(files, entry.Name())
}
}
sort.Strings(files)
for _, file := range files {
if applied[file] {
continue
}
content, err := fs.ReadFile(migrationFS, fmt.Sprintf("migrations/%s", file))
if err != nil {
return fmt.Errorf("failed to read migration %s: %w", file, err)
}
tx, err := db.conn.Begin()
if err != nil {
return fmt.Errorf("failed to begin tx for %s: %w", file, err)
}
if _, err := tx.Exec(string(content)); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute migration %s: %w", file, err)
}
if _, err := tx.Exec(`INSERT INTO schema_migrations (name) VALUES (?)`, file); err != nil {
tx.Rollback()
return fmt.Errorf("failed to record migration %s: %w", file, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit migration %s: %w", file, err)
}
}
return nil
}
// Close closes the database connection.
func (db *DB) Close() error {
return db.conn.Close()
}
// Conn returns the underlying sql.DB connection.
func (db *DB) Conn() *sql.DB {
return db.conn
}
@@ -0,0 +1,88 @@
-- Users / Profile
CREATE TABLE IF NOT EXISTS profile (
id INTEGER PRIMARY KEY CHECK (id = 1),
name TEXT,
email TEXT,
location TEXT,
timezone TEXT,
salary_target_min REAL,
salary_target_max REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Job postings
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT UNIQUE NOT NULL,
company TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
raw_html TEXT,
source TEXT, -- "greenhouse", "ashby", "lever", "indeed", etc.
discovered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
indexed BOOLEAN DEFAULT 0,
archived BOOLEAN DEFAULT 0
);
-- Applications / Tracker
CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'Evaluated', -- Evaluated, Applied, Responded, Interview, Offer, Rejected, Discarded, SKIP
score REAL, -- 0-5 rating from Claude
pdf_generated BOOLEAN DEFAULT 0,
applied_at TIMESTAMP,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (job_id) REFERENCES jobs(id)
);
-- Evaluation reports
CREATE TABLE IF NOT EXISTS reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
application_id INTEGER NOT NULL,
report_md TEXT, -- Full markdown report (A-G blocks)
report_html TEXT, -- HTML rendering
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (application_id) REFERENCES applications(id)
);
-- Portal scan history (dedup)
CREATE TABLE IF NOT EXISTS scan_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL, -- portal name
query_hash TEXT NOT NULL, -- hash of query params
last_scanned TIMESTAMP,
jobs_found INTEGER,
new_jobs INTEGER,
UNIQUE(source, query_hash)
);
-- User voice samples (from DOCX intake)
CREATE TABLE IF NOT EXISTS voice_samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_file TEXT, -- DOCX filename uploaded
transcription TEXT,
extracted_proof_points TEXT, -- JSON: ["point1", "point2", ...]
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Follow-ups
CREATE TABLE IF NOT EXISTS follow_ups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
application_id INTEGER NOT NULL,
follow_up_at TIMESTAMP,
contacted_at TIMESTAMP,
notes TEXT,
FOREIGN KEY (application_id) REFERENCES applications(id)
);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_jobs_source ON jobs(source);
CREATE INDEX IF NOT EXISTS idx_jobs_company ON jobs(company);
CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status);
CREATE INDEX IF NOT EXISTS idx_applications_job_id ON applications(job_id);
CREATE INDEX IF NOT EXISTS idx_scan_history_source ON scan_history(source);
CREATE INDEX IF NOT EXISTS idx_follow_ups_application_id ON follow_ups(application_id);
@@ -0,0 +1,23 @@
-- Intake sessions
CREATE TABLE IF NOT EXISTS intake_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
current_phase INTEGER DEFAULT 0,
is_complete BOOLEAN DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Intake answers (one per phase per session)
CREATE TABLE IF NOT EXISTS intake_answers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
phase INTEGER NOT NULL,
fields_json TEXT, -- JSON: {field_name: value}
nested_items_json TEXT, -- JSON: [{field1: val1, field2: val2}, ...]
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES intake_sessions(id),
UNIQUE(session_id, phase)
);
CREATE INDEX IF NOT EXISTS idx_intake_answers_session_id ON intake_answers(session_id);
@@ -0,0 +1,16 @@
-- Per-URL scan history for dedup against previous scans (beyond jobs.url UNIQUE).
-- Tracks every URL ever seen with status so we can skip expired/filtered without re-fetch.
CREATE TABLE IF NOT EXISTS scanned_urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT UNIQUE NOT NULL,
company TEXT NOT NULL,
title TEXT,
source TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'added',
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_scanned_urls_company ON scanned_urls(company);
CREATE INDEX IF NOT EXISTS idx_scanned_urls_status ON scanned_urls(status);
@@ -0,0 +1,25 @@
-- Phase 3: A-G evaluation pipeline.
-- Extends applications + reports with block-level detail, legitimacy tier, archetype,
-- and per-block markdown segments so the TUI can re-render without re-invoking Claude.
ALTER TABLE applications ADD COLUMN archetype TEXT;
ALTER TABLE applications ADD COLUMN legitimacy TEXT; -- high | caution | suspicious
ALTER TABLE applications ADD COLUMN evaluated_at TIMESTAMP;
ALTER TABLE reports ADD COLUMN seq INTEGER; -- 3-digit sequential number used in filename
ALTER TABLE reports ADD COLUMN slug TEXT; -- company slug used in filename
ALTER TABLE reports ADD COLUMN legitimacy TEXT; -- mirrors applications.legitimacy
ALTER TABLE reports ADD COLUMN score REAL;
ALTER TABLE reports ADD COLUMN block_a TEXT;
ALTER TABLE reports ADD COLUMN block_b TEXT;
ALTER TABLE reports ADD COLUMN block_c TEXT;
ALTER TABLE reports ADD COLUMN block_d TEXT;
ALTER TABLE reports ADD COLUMN block_e TEXT;
ALTER TABLE reports ADD COLUMN block_f TEXT;
ALTER TABLE reports ADD COLUMN block_g TEXT;
ALTER TABLE reports ADD COLUMN file_path TEXT; -- absolute path to on-disk .md report
CREATE INDEX IF NOT EXISTS idx_reports_application ON reports(application_id);
CREATE INDEX IF NOT EXISTS idx_reports_seq ON reports(seq);
CREATE INDEX IF NOT EXISTS idx_applications_job ON applications(job_id);
CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status);
@@ -0,0 +1,4 @@
-- Supports EvalModel.loadPending: filters jobs by archived, orders by discovered_at.
-- Without this, a table scan of jobs is required every time the Evaluate tab refreshes.
CREATE INDEX IF NOT EXISTS idx_jobs_archived_discovered
ON jobs(archived, discovered_at DESC);
+18
View File
@@ -0,0 +1,18 @@
-- Phase 5: Application tracker — event log + indexes for state-machine queries.
-- Existing applications table already has the status field with the right values.
-- This migration adds an immutable event log for audit + follow-up cadence support.
CREATE TABLE IF NOT EXISTS tracker_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
application_id INTEGER NOT NULL,
from_status TEXT,
to_status TEXT NOT NULL,
note TEXT,
occurred_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (application_id) REFERENCES applications(id)
);
CREATE INDEX IF NOT EXISTS idx_tracker_events_application ON tracker_events(application_id);
CREATE INDEX IF NOT EXISTS idx_tracker_events_occurred ON tracker_events(occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_applications_applied_at ON applications(applied_at);
CREATE INDEX IF NOT EXISTS idx_follow_ups_at ON follow_ups(follow_up_at);
@@ -0,0 +1,4 @@
-- Phase 3E: Extract STAR stories from Block F into structured data.
-- Stores stories as JSON array for querying and UI reconstruction.
ALTER TABLE reports ADD COLUMN stories TEXT; -- JSON array of STAR story objects
@@ -0,0 +1,13 @@
-- Phase 4: Pipeline tracking for batch eval jobs.
-- Tracks queued, running, and completed batch evaluations.
CREATE TABLE IF NOT EXISTS pipeline (
id INTEGER PRIMARY KEY,
job_id INTEGER NOT NULL REFERENCES jobs(id),
queued_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
completed_at DATETIME
);
CREATE INDEX IF NOT EXISTS idx_pipeline_job ON pipeline(job_id);
CREATE INDEX IF NOT EXISTS idx_pipeline_completed ON pipeline(completed_at);