Product
Idea Codex
A pipeline that collects market signals from four public sources, refines them into queryable tables, and keeps every row attached to the run that produced it.
What it collects, and what it refuses to
Four public sources: Reddit, Hacker News, Steam and Google Trends. Each one has an adapter behind a shared port, and every payload lands in a raw lake under a versioned envelope before anything reads it.
The rule that shapes the rest is that an unsourced number is worth
zero. Claims that cannot be traced to a stored payload are marked
[UNCITED] and carry no weight in scoring. That is not internal hygiene:
the thing being built on top is an analyst that has to be checkable, and a
figure nobody can walk back is the failure it exists to avoid.
5
Bounded contexts
Collection, refinement, analysis, candidates, content safety.
152
Test files
Against 221 source files, under strict typing.
9
Import fences
Contracts a linter checks, so a layering slip fails CI.
Refinement and analysis are different jobs
The boundary took a decision record to settle. Refinement derives attributes of a thing: cleaning, noun-chunk extraction, sentiment, velocity. Analysis derives structure between things: the demand graph, orderings, clusters.
Putting sentiment in analysis would have been easier and wrong. It describes one row, so a rebuild of one source has to recompute it, and a graph spanning four sources must not.
Sampling that costs less the longer it runs
Snapshot density is driven by score, not by a schedule. Something moving fast gets resampled often; something nobody reads falls out of the rotation. A Steam review is scored against the popularity of the game it sits on, inversely, so a handful of enormous titles cannot crowd out the queue.
Paid sources are metered per pull against a spend log, and a paid source is only reached for after a free one has been tried and written up.
System
Collection
Source collectors
Python
Why
Reddit, Hacker News, Steam and Google Trends. One adapter each, behind one port, so a fifth source is an adapter rather than a second pipeline.
Raw lake
Python
Why
Every payload is stored as fetched, under a versioned envelope, before anything reads it. The refiners can be rebuilt and re-run against it, and a determinism test proves the rebuild lands in the same place.
Spend log
Python · DuckDB
Why
Paid sources are metered per pull. The rule above it is that a paid source is only reached for once a free one has been tried and written up as insufficient.
Refinement
Per-source refiners
Python · spaCy
Why
One per source, over a shared spine. Cleaning, noun-chunk extraction and sentiment happen here, and nothing at this layer invents a relationship between two entities.
Resample queue
Python · DuckDB
Analysis
Signals and graph
Python
Content safety
Python
Substrate
Curated store
DuckDB · Python
Why
Narrow tables and a thin signals layer on top of them. Every row carries the pull run that produced it, so any figure can be walked back to the fetch it came from.
Surface
Product surface
TypeScript · Next.js · NestJS · PostgreSQL
Why
A separate repository, forked from the SaaS substrate on this site, so it opens with tenancy, two-tier auth, audit and data rights already in place. What it adds is the reading surface: briefs in, cited answers out.
1"""Migration refinement:0013 — per-pull-run provenance on entity tables (ADR-0029).23Adds write-once `pull_run_id` (the pull run that produced the row, from the raw4doc) + `first_refined_at` (net-new marker; backfilled = refined_at for existing5rows) to the 6 single-PK entity tables. Composite-PK/snapshot/trends tables are6out of scope (Phase 1b-ii). steam_apps already has first_refined_at (m_0010).7"""89from __future__ import annotations1011import duckdb1213ID = "0013_curated_provenance"1415# (table, needs_first_refined_at) — steam_apps already has it (m_0010);16# steam_reviews does NOT have it (confirmed: m_0004_steam has no first_refined_at17# on steam_reviews, and no subsequent migration adds it before m_0013).18_TABLES = [19 ("reddit_submissions", True),20 ("reddit_comments", True),21 ("hn_stories", True),22 ("hn_comments", True),23 ("steam_apps", False),24 ("steam_reviews", True),25]262728def up(con: duckdb.DuckDBPyConnection) -> None:29 # Ordered in three passes within the migration transaction. DuckDB rejects30 # CREATE INDEX while there are outstanding row UPDATEs in the same31 # transaction, so ALL index creation must happen BEFORE the backfill UPDATEs32 # (ADD COLUMN is DDL and does not count as an outstanding update). On a33 # populated DB the naive add->update->index ordering raises34 # "Cannot create index with outstanding updates".35 # Pass 1 — add columns (DDL).36 for table, needs_fra in _TABLES:37 con.execute(f"ALTER TABLE {table} ADD COLUMN pull_run_id VARCHAR")38 if needs_fra:39 con.execute(f"ALTER TABLE {table} ADD COLUMN first_refined_at TIMESTAMPTZ")40 # Pass 2 — create indexes (before any UPDATE; built on the fresh NULL columns).41 for table, _ in _TABLES:42 con.execute(43 f"CREATE INDEX IF NOT EXISTS {table}_first_refined_idx ON {table}(first_refined_at)"44 )45 con.execute(f"CREATE INDEX IF NOT EXISTS {table}_pull_run_idx ON {table}(pull_run_id)")46 # Pass 3 — backfill first_refined_at for existing rows (UPDATEs last).47 for table, needs_fra in _TABLES:48 if needs_fra:49 con.execute(50 f"UPDATE {table} SET first_refined_at = refined_at WHERE first_refined_at IS NULL"51 )idea-codex430b9e9src/market_research/contexts/refinement/adapters/migrations/m_0013_curated_provenance.pyLines 1 to 5151 lines
"""Migration refinement:0013 — per-pull-run provenance on entity tables (ADR-0029).
Adds write-once `pull_run_id` (the pull run that produced the row, from the raw
doc) + `first_refined_at` (net-new marker; backfilled = refined_at for existing
rows) to the 6 single-PK entity tables. Composite-PK/snapshot/trends tables are
out of scope (Phase 1b-ii). steam_apps already has first_refined_at (m_0010).
"""
from __future__ import annotations
import duckdb
ID = "0013_curated_provenance"
# (table, needs_first_refined_at) — steam_apps already has it (m_0010);
# steam_reviews does NOT have it (confirmed: m_0004_steam has no first_refined_at
# on steam_reviews, and no subsequent migration adds it before m_0013).
_TABLES = [
("reddit_submissions", True),
("reddit_comments", True),
("hn_stories", True),
("hn_comments", True),
("steam_apps", False),
("steam_reviews", True),
]
def up(con: duckdb.DuckDBPyConnection) -> None:
# Ordered in three passes within the migration transaction. DuckDB rejects
# CREATE INDEX while there are outstanding row UPDATEs in the same
# transaction, so ALL index creation must happen BEFORE the backfill UPDATEs
# (ADD COLUMN is DDL and does not count as an outstanding update). On a
# populated DB the naive add->update->index ordering raises
# "Cannot create index with outstanding updates".
# Pass 1 — add columns (DDL).
for table, needs_fra in _TABLES:
con.execute(f"ALTER TABLE {table} ADD COLUMN pull_run_id VARCHAR")
if needs_fra:
con.execute(f"ALTER TABLE {table} ADD COLUMN first_refined_at TIMESTAMPTZ")
# Pass 2 — create indexes (before any UPDATE; built on the fresh NULL columns).
for table, _ in _TABLES:
con.execute(
f"CREATE INDEX IF NOT EXISTS {table}_first_refined_idx ON {table}(first_refined_at)"
)
con.execute(f"CREATE INDEX IF NOT EXISTS {table}_pull_run_idx ON {table}(pull_run_id)")
# Pass 3 — backfill first_refined_at for existing rows (UPDATEs last).
for table, needs_fra in _TABLES:
if needs_fra:
con.execute(
f"UPDATE {table} SET first_refined_at = refined_at WHERE first_refined_at IS NULL"
)- Backwards, and on purpose. What gets pulled again is decided from what is already curated, so a thing nobody is reading falls out of the rotation and a thing moving fast gets sampled more often. A fixed schedule would spend the same money on both.
Lookup or compute
The product the engine feeds is a curated database with an analyst sitting on it. A question either has an answer already in the substrate, in which case it is a lookup, or it does not, in which case the pipeline computes one and the substrate keeps it. The second path is what stops the database going stale, and it is why collection is score-driven rather than scheduled.
The surface is its own repository, forked from the SaaS substrate, so it starts with tenancy, two-tier auth, audit and data rights and adds the part that is specific to this product: briefs in, cited answers out. The two sides meet at a written contract rather than at each other’s internals, which is what lets the engine keep changing shape underneath.
Contribution
- Commits authored
- 420
- Decisions recorded
- 38
4 of 6 weeks active · 4 May 2026 – 14 Jun 2026
Longest run · 2 weeks · May – May
The engine only. The surface repository shares its history with the substrate, and that work is counted there.
Measured 2026-09-09