281 lines
16 KiB
SQL
281 lines
16 KiB
SQL
-- Ten31 Signal Engine — SQLite schema (pilot)
|
|
-- Source of truth: ten31-signal-engine-handoff.md §4 (pipeline layers), §6.7 (ledger),
|
|
-- §3.1 (conviction log), §13.4 (backfill queue).
|
|
-- Design principle (§5, §10): boring, inspectable tables. The whole system state is a SELECT away.
|
|
|
|
PRAGMA journal_mode = WAL;
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
-- ============================================================================
|
|
-- CANONICAL TOPIC VOCABULARY (§4.2) — HYBRID (operator decision):
|
|
-- seeded controlled list + emergent topics merged in on a schedule.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS topics (
|
|
topic_canonical TEXT PRIMARY KEY,
|
|
status TEXT CHECK (status IN ('controlled','emergent','merged')) DEFAULT 'emergent',
|
|
merged_into TEXT REFERENCES topics(topic_canonical),
|
|
seam TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- SOURCES & DOCUMENTS (§4.1)
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS sources (
|
|
source_id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
kind TEXT NOT NULL CHECK (kind IN ('podcast','youtube','filing','earnings_call')),
|
|
source_cluster TEXT CHECK (source_cluster IN
|
|
('macro','ai_tech','energy','bitcoin','vc_consensus','generalist','banks','credit','fintech')),
|
|
role TEXT CHECK (role IN ('CB','IND','DX','none')) DEFAULT 'none', -- §7.4
|
|
rss_url TEXT,
|
|
channel_url TEXT,
|
|
ticker TEXT,
|
|
-- §8 credibility: neutral prior that DECAYS in favor of earned track record from the ledger.
|
|
bootstrap_prior REAL DEFAULT 1.0,
|
|
earned_credibility REAL,
|
|
cluster_capped_low INTEGER DEFAULT 0, -- §4.5 bitcoin cluster deliberately under-weighted
|
|
backtest_2022_2023 TEXT, -- §7.1 reach: rss_full | rss_2023_only | youtube_only | launched_later | unavailable
|
|
notes TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
doc_id TEXT PRIMARY KEY,
|
|
source_id TEXT NOT NULL REFERENCES sources(source_id),
|
|
kind TEXT NOT NULL, -- podcast|youtube|filing|earnings_call
|
|
external_id TEXT, -- rss guid / yt video id / EDGAR accession / transcript id
|
|
url TEXT,
|
|
title TEXT,
|
|
date TEXT, -- ISO publication/filing date
|
|
duration_sec REAL,
|
|
raw_path TEXT, -- downloaded audio / raw filing
|
|
transcript_path TEXT,
|
|
-- DEDUP MODEL (layered):
|
|
-- (1) UNIQUE(source_id, external_id) below = the ROBUST guard. external_id is the stable item id
|
|
-- (RSS GUID / YouTube video id / EDGAR accession). Checked at ingest, BEFORE any GPU work.
|
|
-- (2) dedup_key = normalized title+date → catches the SAME episode arriving via a different
|
|
-- feed/mirror (different external_id). Computed pre-transcription. NOT from the transcript.
|
|
-- content_hash is ONLY an audit fingerprint of the transcript (did a re-run change?) — it is NOT
|
|
-- a dedup key (ASR is non-deterministic, so one differing word flips the hash).
|
|
dedup_key TEXT,
|
|
content_hash TEXT,
|
|
processed_at TEXT, -- set when transcription/extraction completes
|
|
ingested_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE (source_id, external_id) -- idempotent ingest (§13.4 dedup)
|
|
);
|
|
-- indexes for dedup_key / content_hash are created in db._migrate (after columns exist on older DBs).
|
|
|
|
-- ============================================================================
|
|
-- CLAIMS / PROPOSITIONS (§4.2) — the atomic unit of the whole system.
|
|
-- One passage emits 0..N claims; MOST of a podcast hour is 0 (§4.2). The
|
|
-- extractor must be willing to find nothing.
|
|
-- NOTE: thesis_seam is a TAG, never a hard filter (§5.7) — off-thesis &
|
|
-- anti-thesis claims MUST survive.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS claims (
|
|
claim_id TEXT PRIMARY KEY,
|
|
doc_id TEXT NOT NULL REFERENCES documents(doc_id),
|
|
source_id TEXT NOT NULL REFERENCES sources(source_id),
|
|
proposition TEXT NOT NULL, -- normalized subject-assertion-object
|
|
topic_canonical TEXT REFERENCES topics(topic_canonical),
|
|
topic_raw TEXT,
|
|
claimant TEXT,
|
|
source_cluster TEXT,
|
|
date TEXT,
|
|
claim_type TEXT CHECK (claim_type IN ('interpretive','predictive','descriptive','reactive')),
|
|
time_horizon TEXT CHECK (time_horizon IN ('near','medium','long','unspecified')),
|
|
confidence TEXT CHECK (confidence IN ('low','med','high')),
|
|
-- §4.2 relation: stance is EXTRACTED, never inferred from vector distance (§2.2/§5.3).
|
|
rel_target_claim_id TEXT REFERENCES claims(claim_id),
|
|
rel_polarity TEXT CHECK (rel_polarity IN ('affirms','denies','qualifies','none')) DEFAULT 'none',
|
|
engages_consensus INTEGER DEFAULT 0,
|
|
counters_position TEXT,
|
|
thesis_seam TEXT CHECK (thesis_seam IN
|
|
('energy_compute','debasement_bitcoin','ai_data_ownership','none')) DEFAULT 'none',
|
|
salience TEXT CHECK (salience IN ('central','secondary','aside')) DEFAULT 'secondary',
|
|
qdrant_point_id TEXT, -- link to the embedded proposition vector (§4.3)
|
|
extracted_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_claims_topic ON claims(topic_canonical);
|
|
CREATE INDEX IF NOT EXISTS idx_claims_date ON claims(date);
|
|
CREATE INDEX IF NOT EXISTS idx_claims_seam ON claims(thesis_seam);
|
|
CREATE INDEX IF NOT EXISTS idx_claims_type ON claims(claim_type);
|
|
|
|
-- ============================================================================
|
|
-- SOURCE-INDEPENDENCE GRAPH (§4.5) — discount convergence by connectedness.
|
|
-- Cross-cluster convergence = gold; within-cluster = near-noise.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS source_edges (
|
|
src_a TEXT NOT NULL REFERENCES sources(source_id),
|
|
src_b TEXT NOT NULL REFERENCES sources(source_id),
|
|
edge_type TEXT NOT NULL CHECK (edge_type IN ('shared_guest','citation','community')),
|
|
weight REAL DEFAULT 1.0,
|
|
evidence TEXT, -- voiceprint_id / show-note ref / url
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
PRIMARY KEY (src_a, src_b, edge_type)
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- VOICEPRINT LIBRARY (§4.5, §4.1) — same-guest-across-shows BY VOICE.
|
|
-- 192-dim TitaNet voiceprints; cosine ~0.7 distance threshold for same speaker.
|
|
-- This is the highest-leverage automated input to the independence graph.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS voiceprints (
|
|
voiceprint_id TEXT PRIMARY KEY,
|
|
vector BLOB NOT NULL, -- 192 x float32
|
|
person_label TEXT, -- resolved name if known
|
|
first_doc_id TEXT REFERENCES documents(doc_id),
|
|
first_seen TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS voiceprint_observations (
|
|
obs_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
voiceprint_id TEXT NOT NULL REFERENCES voiceprints(voiceprint_id),
|
|
doc_id TEXT NOT NULL REFERENCES documents(doc_id),
|
|
chunk_idx INTEGER,
|
|
segment_start REAL,
|
|
segment_end REAL
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- CONVICTION LOG (§3.1) — human-owned seed nodes for Job B.
|
|
-- Structural rule (§3.1): separate the TRACKABLE thematic proposition (corpus
|
|
-- can corroborate) from TEAM conviction (context only). The engine must NEVER
|
|
-- present theme corroboration as validation of the team bet beneath it.
|
|
-- Exposure scored as coarse NAV bands (operator decision): none | lt2 | 2to10 | gt10 | unset.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS conviction_log (
|
|
conviction_id TEXT PRIMARY KEY, -- R1, E1, A1, B1 ...
|
|
seam TEXT, -- root|energy_compute|debasement_bitcoin|ai_data_ownership
|
|
thematic_proposition TEXT NOT NULL, -- the TRACKABLE half
|
|
team_conviction_note TEXT, -- context ONLY, never scored as theme validation
|
|
conviction_level TEXT CHECK (conviction_level IN ('low','med','med-high','high')),
|
|
current_exposure TEXT CHECK (current_exposure IN ('none','lt2','2to10','gt10','unset')) DEFAULT 'unset',
|
|
exposure_note TEXT, -- original §3.1 prose ("pervasive", "MED-HIGH") pending NAV-band finalization
|
|
disconfirming_signal TEXT,
|
|
is_thesis_breaker INTEGER DEFAULT 0, -- §3.1 B1-B3: engine must surface these AGAINST the thesis (§5.7)
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- Conviction fan-out tree (§4.6). A derivative is a HYPOTHESIS until independent
|
|
-- corpus corroboration AND the exposure gap both clear the bar — then 'signal'.
|
|
CREATE TABLE IF NOT EXISTS fanout_nodes (
|
|
node_id TEXT PRIMARY KEY,
|
|
parent_conviction_id TEXT REFERENCES conviction_log(conviction_id),
|
|
parent_node_id TEXT REFERENCES fanout_nodes(node_id),
|
|
derivative_proposition TEXT NOT NULL,
|
|
depth INTEGER DEFAULT 1,
|
|
status TEXT CHECK (status IN ('hypothesis','corroborated','signal')) DEFAULT 'hypothesis',
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- DUAL-EVALUATION LEDGER (§4.7, §6) — START DAY ONE; the clock can't be backfilled.
|
|
-- Log EVERY candidate that clears the quantitative bar (§6.6 — you need a denominator).
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS ledger (
|
|
signal_id TEXT PRIMARY KEY,
|
|
type TEXT NOT NULL CHECK (type IN ('theme','event','under_acted_conviction')),
|
|
proposition TEXT NOT NULL,
|
|
date_logged TEXT NOT NULL DEFAULT (datetime('now')),
|
|
discourse_metric TEXT, -- JSON: acceleration, cross-cluster source set, independence-discounted count
|
|
external_check TEXT, -- JSON: resolution spec / nested clean events the model proposed (§6.5)
|
|
resolution_date TEXT,
|
|
discourse_outcome TEXT CHECK (discourse_outcome IN
|
|
('up_cross_cluster','up_single_cluster','flat','down')),
|
|
external_outcome TEXT CHECK (external_outcome IN
|
|
('correct','partial','wrong','unresolved_expired','too_early')),
|
|
lead_time_days INTEGER, -- §6.3 THE alpha measurement (to the DERIVATIVE node for Job B)
|
|
model_confidence REAL, -- §6.7 logged ONLY to measure its uselessness — NEVER fed into scoring
|
|
origin_conviction_id TEXT REFERENCES conviction_log(conviction_id), -- Job B traceability
|
|
origin_node_id TEXT REFERENCES fanout_nodes(node_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_ledger_type ON ledger(type);
|
|
CREATE INDEX IF NOT EXISTS idx_ledger_logged ON ledger(date_logged);
|
|
|
|
-- Human eval on a SEPARATE write path (§6.7): "keep them in separate columns and do not let the
|
|
-- model see Grant's rating before it logs its prediction." The model-facing code reads `ledger`;
|
|
-- ONLY the eval UI writes here. A separate table makes that separation structural, not a convention.
|
|
CREATE TABLE IF NOT EXISTS human_evaluations (
|
|
signal_id TEXT PRIMARY KEY REFERENCES ledger(signal_id),
|
|
grant_rating INTEGER, -- "non-obvious and relevant to me?" (e.g. 1-5)
|
|
non_obvious INTEGER, -- 0/1
|
|
notes TEXT,
|
|
rated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- Reporting view — the valuable cell is DISAGREEMENT (§6.7). Used for analysis, NOT by the model path.
|
|
CREATE VIEW IF NOT EXISTS v_ledger_eval AS
|
|
SELECT l.*, h.grant_rating, h.non_obvious, h.notes AS grant_notes, h.rated_at
|
|
FROM ledger l LEFT JOIN human_evaluations h ON h.signal_id = l.signal_id;
|
|
|
|
-- ============================================================================
|
|
-- BACKFILL QUEUE (§13.4) — client-side, measured in GPU-HOURS.
|
|
-- Extraction (one LLM pass per chunk over the whole corpus) is the HEAVIER serial load.
|
|
-- Audio is SEQUENTIAL (parallel → 503). Leases give crash-safe resumability.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS backfill_jobs (
|
|
job_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
job_type TEXT NOT NULL CHECK (job_type IN ('transcribe','diarize','extract','embed')),
|
|
target_id TEXT NOT NULL, -- doc_id or chunk id
|
|
parent_doc_id TEXT,
|
|
state TEXT NOT NULL CHECK (state IN
|
|
('pending','leased','running','done','failed','skipped')) DEFAULT 'pending',
|
|
priority INTEGER DEFAULT 100, -- lower = sooner (backtest corpus jumps the queue, §7.1)
|
|
attempts INTEGER DEFAULT 0,
|
|
max_attempts INTEGER DEFAULT 5,
|
|
lease_owner TEXT,
|
|
lease_expires_at TEXT,
|
|
input_hash TEXT NOT NULL, -- hash(content + model/prompt version) — idempotency
|
|
output_ref TEXT,
|
|
gpu_seconds REAL, -- measured per job → self-calibrating GPU-hours estimate
|
|
error TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE (job_type, input_hash)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_state_priority ON backfill_jobs(state, priority, job_id);
|
|
|
|
-- ============================================================================
|
|
-- SCORING BRAIN state (the "brain", build blueprint). Candidate state lands here +
|
|
-- ledger + fanout_nodes.status; existing tables unchanged.
|
|
-- ============================================================================
|
|
|
|
-- Temporal layer: one row per (topic, as_of, window). 28d non-overlapping windows.
|
|
CREATE TABLE IF NOT EXISTS topic_window_stats (
|
|
topic_canonical TEXT NOT NULL,
|
|
as_of TEXT NOT NULL,
|
|
window_idx INTEGER NOT NULL, -- 0 = window ending at as_of, 1 = prior, 2 = baseline
|
|
window_start TEXT NOT NULL,
|
|
window_end TEXT NOT NULL,
|
|
n_interp_pred INTEGER NOT NULL DEFAULT 0,
|
|
n_descr_react INTEGER NOT NULL DEFAULT 0,
|
|
n_distinct_src INTEGER NOT NULL DEFAULT 0,
|
|
n_distinct_clu INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (topic_canonical, as_of, window_idx)
|
|
);
|
|
|
|
-- Audit trail: one row per (scorer, key, as_of). Deterministic score_id → re-run reproduces.
|
|
CREATE TABLE IF NOT EXISTS candidate_scores (
|
|
score_id TEXT PRIMARY KEY,
|
|
scorer TEXT NOT NULL, -- emergence|contrarian|intersection|convergence|under_acted
|
|
as_of TEXT NOT NULL,
|
|
topic_canonical TEXT,
|
|
node_id TEXT,
|
|
conviction_id TEXT,
|
|
score REAL NOT NULL,
|
|
cleared_evidence_bar INTEGER NOT NULL DEFAULT 0, -- tier 1: logged to ledger (the denominator)
|
|
cleared_promotion_bar INTEGER NOT NULL DEFAULT 0, -- tier 2: sent to frontier judge
|
|
inputs_json TEXT NOT NULL, -- every term that produced the score (full audit)
|
|
computed_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_cs_asof ON candidate_scores(scorer, as_of, cleared_promotion_bar);
|
|
|
|
-- Tunable bar config so the backtest can sweep thresholds without code edits.
|
|
CREATE TABLE IF NOT EXISTS score_thresholds (
|
|
scorer TEXT PRIMARY KEY,
|
|
min_score REAL,
|
|
gates_json TEXT,
|
|
version TEXT
|
|
);
|