Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b330650
docs: plan evidence-backed paper citation indexing
ThePyProgrammer Nov 4, 2026
d8040ad
feat(db): add durable citation evidence and leased jobs
ThePyProgrammer Nov 4, 2026
ce5e3d1
feat(citations): resolve identities and fetch provider references
ThePyProgrammer Nov 4, 2026
71a0dc1
feat(citations): add resumable indexing and import capture
ThePyProgrammer Nov 4, 2026
ca4b933
feat(api): expose local citations and indexing status
ThePyProgrammer Nov 4, 2026
6efc1f5
feat(ui): show paper references and incoming citations
ThePyProgrammer Nov 4, 2026
9183413
docs(citations): record coverage pilot and operating procedures
ThePyProgrammer Nov 4, 2026
8fe983c
feat(db): version PDF replacements and add opt-in citation jobs
ThePyProgrammer Nov 4, 2026
f695caf
feat(citations): extract evidenced PDF references with GROBID
ThePyProgrammer Nov 4, 2026
5afdbe5
feat(citations): expose opt-in PDF indexing and pilot controls
ThePyProgrammer Nov 4, 2026
7f5c86e
feat(db): add independently opted-in local PDF citation jobs
ThePyProgrammer Nov 4, 2026
6928e9b
feat(citations): extract numbered PDF bibliographies locally
ThePyProgrammer Nov 4, 2026
e69b2f8
docs(citations): record local PDF pilot and revise rollout plan
ThePyProgrammer Nov 4, 2026
4d9e046
fix(ui): remove citation implementation notes
ThePyProgrammer Nov 4, 2026
e779930
fix(citations): distinguish retrieved references from library links
ThePyProgrammer Nov 4, 2026
30fd7b6
fix(db): upgrade earlier citation identity installations safely
ThePyProgrammer Nov 4, 2026
8d4c3bb
feat(db): guard automatic PDF citation fallback after provider attempts
ThePyProgrammer Nov 4, 2026
e5f16e0
feat(citations): automatically schedule local PDF fallback for imports
ThePyProgrammer Nov 4, 2026
515143a
fix(ui): consolidate citation indexing controls
ThePyProgrammer Nov 4, 2026
5d8911f
style(citations): align Index with header and lab button styling
ThePyProgrammer Nov 4, 2026
9b337ee
style(citations): reduce note and paper title text size
ThePyProgrammer Nov 4, 2026
18198f9
style(citations): simplify reference list and space unmatched count
ThePyProgrammer Nov 4, 2026
ff38802
style(citations): match detail headings and remove divider
ThePyProgrammer Nov 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ on:
branches: [main]

jobs:
citation-database:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install disposable PostgreSQL engine
run: npm install --prefix backend/data/citation-validation --no-audit --no-fund --ignore-scripts @electric-sql/pglite@0.5.14
- name: Verify citation migration and transactions
run: node backend/tests/run_citation_sql.mjs

backend:
runs-on: ubuntu-latest
defaults:
Expand Down
3 changes: 3 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
OPENAI_API_KEY=*
OPENALEX_API_KEY=*
# Optional alternate extractor; local PyMuPDF indexing needs no service.
# PDFs are sent here only for explicitly selected GROBID jobs.
GROBID_URL=
SUPABASE_URL=*
SUPABASE_KEY=*

Expand Down
2 changes: 2 additions & 0 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from routers import papers, collections, workflows, runs, proposals, activity, search, libraries, websites, notes, chat, authors, github_repos, settings, notes_copilot, usage, projects, research_questions, experiments, project_notes_copilot, tasks, gap_analysis, batch
from services.db import get_client
from routers import labs
from routers import citations

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -46,6 +47,7 @@ async def storage_migration_maintenance(request: Request, call_next):

app.include_router(libraries.router)
app.include_router(papers.router)
app.include_router(citations.router)
app.include_router(websites.router)
app.include_router(github_repos.router)
app.include_router(collections.router)
Expand Down
330 changes: 330 additions & 0 deletions backend/migrations/033_paper_citations.sql

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions backend/migrations/034_pdf_citations.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
-- Optional PDF bibliography indexing. Requires migration 033.
BEGIN;
ALTER TABLE public.papers ADD COLUMN IF NOT EXISTS pdf_revision BIGINT NOT NULL DEFAULT 0;
ALTER TABLE public.paper_reference_runs DROP CONSTRAINT IF EXISTS paper_reference_runs_provider_check;
ALTER TABLE public.paper_reference_runs ADD CONSTRAINT paper_reference_runs_provider_check
CHECK (provider IN ('openalex','crossref','grobid'));
ALTER TABLE public.paper_citation_jobs DROP CONSTRAINT IF EXISTS paper_citation_jobs_provider_check;
ALTER TABLE public.paper_citation_jobs ADD CONSTRAINT paper_citation_jobs_provider_check
CHECK (provider IN ('openalex','crossref','grobid'));

CREATE OR REPLACE FUNCTION public.citation_fingerprint(p public.papers) RETURNS TEXT
LANGUAGE sql IMMUTABLE AS $$
SELECT md5(jsonb_build_array(p.doi,p.arxiv_id,p.title,p.authors,p.year,p.library_id,p.pdf_url,p.pdf_revision)::text);
$$;

CREATE OR REPLACE FUNCTION public.bump_citation_pdf_revision() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
-- The upload route writes pdf_url even when replacing the same storage key.
NEW.pdf_revision := OLD.pdf_revision + 1;
RETURN NEW;
END $$;
DROP TRIGGER IF EXISTS citation_pdf_revision ON public.papers;
CREATE TRIGGER citation_pdf_revision BEFORE UPDATE OF pdf_url ON public.papers
FOR EACH ROW EXECUTE FUNCTION public.bump_citation_pdf_revision();

CREATE OR REPLACE FUNCTION public.enqueue_pdf_citations(p_paper_id TEXT,p_force BOOLEAN DEFAULT false)
RETURNS SETOF public.paper_citation_jobs
LANGUAGE plpgsql SECURITY INVOKER SET search_path=public AS $$
DECLARE p papers;
BEGIN
SELECT * INTO p FROM papers WHERE id=p_paper_id FOR UPDATE;
IF NOT FOUND THEN RETURN; END IF;
INSERT INTO paper_citation_jobs(paper_id,provider,fingerprint)
VALUES (p.id,'grobid',citation_fingerprint(p))
ON CONFLICT (paper_id,provider) DO UPDATE SET
fingerprint=EXCLUDED.fingerprint,status='pending',attempts=0,error=NULL,
lease_token=NULL,lease_until=NULL,next_attempt_at=now(),updated_at=now()
WHERE paper_citation_jobs.fingerprint<>EXCLUDED.fingerprint
OR (p_force AND paper_citation_jobs.status NOT IN ('pending','running'));
RETURN QUERY SELECT * FROM paper_citation_jobs WHERE paper_id=p.id AND provider='grobid';
END $$;

CREATE OR REPLACE FUNCTION public.refresh_opted_in_pdf_citations() RETURNS trigger
LANGUAGE plpgsql SECURITY INVOKER SET search_path=public AS $$
BEGIN
IF citation_fingerprint(OLD)<>citation_fingerprint(NEW)
AND EXISTS(SELECT 1 FROM paper_citation_jobs WHERE paper_id=NEW.id AND provider='grobid') THEN
PERFORM enqueue_pdf_citations(NEW.id);
END IF;
RETURN NEW;
END $$;
DROP TRIGGER IF EXISTS paper_pdf_citations_changed ON public.papers;
CREATE TRIGGER paper_pdf_citations_changed AFTER UPDATE ON public.papers
FOR EACH ROW EXECUTE FUNCTION public.refresh_opted_in_pdf_citations();

-- Rebase existing queued fingerprints without disturbing published provider evidence.
UPDATE paper_citation_jobs j SET fingerprint=citation_fingerprint(p),status='pending',attempts=0,
lease_token=NULL,lease_until=NULL,next_attempt_at=now(),updated_at=now()
FROM papers p WHERE p.id=j.paper_id AND j.fingerprint<>citation_fingerprint(p);
COMMIT;
37 changes: 37 additions & 0 deletions backend/migrations/035_local_pdf_citations.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- Local PDF extraction, opt-in independently of GROBID. Requires 033 and 034.
BEGIN;
ALTER TABLE public.paper_reference_runs DROP CONSTRAINT IF EXISTS paper_reference_runs_provider_check;
ALTER TABLE public.paper_reference_runs ADD CONSTRAINT paper_reference_runs_provider_check CHECK (provider IN ('openalex','crossref','grobid','pymupdf'));
ALTER TABLE public.paper_citation_jobs DROP CONSTRAINT IF EXISTS paper_citation_jobs_provider_check;
ALTER TABLE public.paper_citation_jobs ADD CONSTRAINT paper_citation_jobs_provider_check CHECK (provider IN ('openalex','crossref','grobid','pymupdf'));
CREATE OR REPLACE FUNCTION public.enqueue_local_pdf_citations(p_paper_id TEXT,p_force BOOLEAN DEFAULT false)
RETURNS SETOF public.paper_citation_jobs
LANGUAGE plpgsql SECURITY INVOKER SET search_path=public AS $$
DECLARE p papers;
BEGIN
SELECT * INTO p FROM papers WHERE id=p_paper_id FOR UPDATE;
IF NOT FOUND THEN RETURN; END IF;
INSERT INTO paper_citation_jobs(paper_id,provider,fingerprint)
VALUES (p.id,'pymupdf',citation_fingerprint(p))
ON CONFLICT (paper_id,provider) DO UPDATE SET
fingerprint=EXCLUDED.fingerprint,status='pending',attempts=0,error=NULL,
lease_token=NULL,lease_until=NULL,next_attempt_at=now(),updated_at=now()
WHERE paper_citation_jobs.fingerprint<>EXCLUDED.fingerprint
OR (p_force AND paper_citation_jobs.status NOT IN ('pending','running'));
RETURN QUERY SELECT * FROM paper_citation_jobs WHERE paper_id=p.id AND provider='pymupdf';
END $$;

CREATE OR REPLACE FUNCTION public.refresh_opted_in_pdf_citations() RETURNS trigger
LANGUAGE plpgsql SECURITY INVOKER SET search_path=public AS $$
BEGIN
IF citation_fingerprint(OLD)<>citation_fingerprint(NEW) THEN
IF EXISTS(SELECT 1 FROM paper_citation_jobs WHERE paper_id=NEW.id AND provider='grobid') THEN
PERFORM enqueue_pdf_citations(NEW.id);
END IF;
IF EXISTS(SELECT 1 FROM paper_citation_jobs WHERE paper_id=NEW.id AND provider='pymupdf') THEN
PERFORM enqueue_local_pdf_citations(NEW.id);
END IF;
END IF;
RETURN NEW;
END $$;
COMMIT;
Loading
Loading