Skip to content

Latest commit

 

History

239 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pepiros: Be the source.

Turn a research PDF into a knowledge graph where every generated claim stays bound to a located quote, and hand that grounding to Claude as a callable MCP service.

CI License: MIT Node Next.js 15 TypeScript

Quick start · What works today · Architecture · MCP · Design system · Configuration · Contributing


What it does

Upload a paper, or paste an arXiv / PMC / DOI link. Pepiros extracts its structure, generates a summary and thematic "pillars," and backs every generated claim with a quote it can point at in the source PDF: page, rect, and match score included.

The verification is deterministic. No LLM judges another LLM:

for each claimed {ref, quote}:
  chunk = resolve(ref)                                  # stable citation id, not a vector lookup
  if !chunk                        -> drop, log hallucinated_ref
  score = token_set_ratio(quote, chunk.text)
  if   score >= 0.92               -> quote located
  elif score >= 0.75               -> paraphrase   (badged, kept)
  else                             -> drop the anchor, strip [^eN] from the prose

On top of that sits an entailment overlap floor: every number, unit, and comparator a claim asserts must also appear in the anchored chunk's numerics. That is what catches the real failure mode, a genuine quote attached to a reversed or overstated conclusion, which fuzzy matching alone scores 1.0.

No claim is ever labelled "verified." The badge reads quote located, because a fuzzy-matched quote proves quotation provenance, not entailment. Claim and quote render side by side so the reader adjudicates. This is a deliberate, load-bearing constraint, not hedging.

plan.md is the canonical spec: architecture, data model, locked decisions, and the list of things deliberately not built. This README is the entry point.


What works today

Early build, moving fast. Honest status:

Working

Area State
Grounding spine (lib/grounding/*) Quote verification, entailment floor, reverse audit, citation-ref resolution. Deterministic, no model calls.
Data model (lib/db/schema.ts) All 18 tables from plan.md §5, in Drizzle, with real migrations applied and CI running against a real ephemeral Postgres on every push.
UI (components/*, app/(app)/*) React Flow canvas (5 node types, ghost citation nodes, kind-coloured edges), doc-reader, outline, audit, learn, and share views -- all four reader subpages share one nav/spacing shell and a consistent guest banner. Renders the bundled fixture or any real ingested workspace alike.
Citation APIs (lib/services/related.ts, lib/services/citationExpand.ts) Real Semantic Scholar + OpenAlex calls (free, no key) for the related-papers rail and canvas citation expansion. Typed ok/no_match/rate_limited/error status, never fabricated fallback data.
LLM layer (lib/agents/*) Archetype classifier, archetype-conditioned pillar planner, and 6 of the 21 node generators (summary, methodology, statistical_validity, stated_limitations, weaknesses, does_not_establish), fanned out via p-queue with per-node failure isolation. Runs on Groq (primary) + Featherless (fallback), not Anthropic: see Configuration. Every claimed quote is re-verified through the grounding spine before it becomes a real evidence row. Verified end to end against both live APIs, not just mocked.
MCP server (mcp/*) All 12 tools in lib/mcp/registry.ts are live over stdio (search_paper, verify_claim, get_outline, get_node, create_node, find_contradictions, paper_facts, list_papers, list_workspaces, create_workspace, add_paper, get_job), plus 3 resource templates for @-mentioning a workspace/paper/node and the 4 docs/PLAN-V1.md §13.3 prompts. create_node re-verifies submitted evidence server-side, so a client cannot assert quote_located. A token requires a live session (or is refused outright in a production-configured process with none) rather than defaulting to unrestricted access. Verified over the real protocol with the SDK's own client.
Grounded chat (lib/services/chat.ts, POST /api/chat) Query rewrite → route classifier → stable-id context block → answer → citation re-verification. Refusal path with an explicit "answer without sources" opt-in; ungrounded answers render visually distinct. Verified against the live Groq API.
Canvas layout (lib/layout/*) Deterministic server-computed positions: radial for one paper, layered columns for several. Verified to produce no overlapping cards at 1, 2, or 3 papers. Pillars open collapsed, edges render only when both endpoints do, cards shed detail as you zoom out (lib/graph/lod.ts), and a closed-by-default legend explains every colour and line style the graph actually uses.
Auth (lib/auth/*, app/auth/callback) Google sign-in via Supabase as the OAuth broker, plus real username/password accounts (a required real email, so password recovery has somewhere to send to, and email confirmation is real -- Supabase sends it, sign-in gates on it). Password sessions are server-side revocable (logout kills that one session; a "sign out everywhere" action kills every session for the account), translated into the app's own signed cookie so there is one answer to "who is signed in". Sign-in buys persistence, not access: the reader and upload are open to guests, with a banner saying plainly that guest work is not kept. Every workspace-scoped mutation route requires a session for anything other than the public demo workspace.
Upload validation + ingest (lib/services/upload.ts, lib/services/ingest.ts, POST /api/ingest) Size (50MB), magic bytes, page cap (120), text-layer check, duplicate detection (DOI then fuzzy title), and arXiv/PMC/direct-PDF URL resolution (DOI resolution still needs an Unpaywall-style resolver). GET /api/jobs/[id] streams the 9-stage progress list over SSE. A 202 kicks off the real pipeline: scripts/parse.py (PyMuPDF) extracts sections/chunks/numerics/authors/publication year/archetype, then lib/agents/orchestrator.ts classifies, plans pillars, and fans out generators, all re-verified against the source before the graph updates. A scanned/image-only PDF fails loudly with a clear diagnosis instead of silently ingesting nothing. The parsed PDF itself is stored locally and served back for the reader's real page view (react-pdf), not a styled mock.
Cross-paper synthesis (lib/services/synthesis.ts, POST /api/compare) Pairwise comparison writes real agrees/contradicts/extends/shares_method/relates edges (two-sided evidence required) and 4 of the 6 docs/PLAN-V1.md §10 synthesis node types: Consensus, Contradictions (LLM-classified), Timeline of Findings, and Methodological Divergence (both deterministic, from real per-paper year/archetype). Dataset Overlap and Open Questions still need signals nothing in the pipeline extracts yet.
Node mutation (lib/services/nodes.ts, PATCH/DELETE /api/nodes/[id]) Editing a node's body re-verifies every citation against the edited text (downgrading/stripping one that no longer matches) and records a version history row; deleting cascades edges/evidence and marks referencing nodes stale.
Export & promote (GET /api/export, POST /api/chat/promote) Markdown and BibTeX export; promoting a chat answer that draws on more than one paper into a cross-paper thread node.
API POST /api/verify, POST /api/audit, POST /api/chat, POST /api/ingest, POST /api/compare, POST /api/share, GET /api/jobs/[id], GET /api/graph/[workspaceId], GET /api/related, GET /api/expand, GET /api/export, full nodes/nodes/[id] CRUD, GET /api/papers/[paperId]/pdf
Tooling Typecheck, lint, test, and build all gated in CI on every push and PR, against a real ephemeral Postgres service container. 297 Vitest cases across 34 files, including the LLM and chat layers tested against a hand-rolled mock model (no API key or network call needed).

Not built yet

Each of these is a one-line TODO at the top of its file, describing what belongs there.

Area Missing
OCR fallback + seed script scripts/ocr_fallback.py (PaddleOCR-VL, for scanned/table-heavy pages) and scripts/seed.ts (bulk-loading a corpus into Postgres once one exists) are still stubs -- a scanned/image-only PDF now fails the job with a clear message instead of silently ingesting nothing, but there's no OCR pass yet to actually recover text from one. scripts/parse.py (PyMuPDF) and lib/services/ingest.ts are otherwise real -- pip install pymupdf is the one setup step, since it's a local script per plan.md §2, never a deployed service.
Remaining generators 20 of the ~22 real types in docs/PLAN-V1.md §8 are implemented (lib/agents/generators/), including equations -- scripts/parse.py detects display-equation regions and converts them to LaTeX via Pix2Text (free, open-source, runs entirely locally), bbox-anchored like any other chunk -- and figures, which needed the same Pix2Text pass to also crop each detected figure region and pair it with its nearby caption (a real figure_caption-kind chunk), plus a third model tier: visionModel() in lib/ai/client.ts (OpenRouter, a free :free-suffixed model, since Groq has no vision model at all and Featherless's returned capacity_exhausted on every attempt checked live). Left: concept_links (needs cross-paper context a single-paper GeneratorContext doesn't carry, plus a way to write real relates edges from a generator's output -- already solved a different way by lib/services/synthesis.ts's relates classification, so this isn't a real gap so much as a pointer to where the equivalent already lives). quiz/flashcards are solved a different way -- lib/services/quiz.ts and components/learn/FlashcardDeck.tsx derive them from already-verified leaf nodes rather than being their own generator.
Synthesis, remaining node types Dataset Overlap and Open Questions (docs/PLAN-V1.md §10) need signals nothing in the pipeline extracts yet (dataset identifiers, a genuine gap-in-the-literature judgment) -- not a metadata gap like the two that shipped, new extraction work.
MCP remote transport npx pepiros-mcp is live on npm (published as pepiros-mcp, the tool layer bundled to one file via esbuild) and verified end to end with a real initialize call over a fresh install. What's left is the remote streamable-HTTP + OAuth transport, for a hosted connector rather than a locally-run stdio process; docs/PLAN-V1.md §13.4 says verify connector requirements against Anthropic's current docs before building that.
Session refresh A signed-in session simply expires at its 7-day lifetime with no silent renewal. Revocation exists (see Auth, above); refresh doesn't yet.
Measurement evals/, scripts/measure-drop-rate.ts

Chat and the generators need at least one of GROQ_API_KEY or FEATHERLESS_API_KEY; everything else runs without a key. PDF ingest itself only runs locally (npm run dev) -- Vercel's Node runtime has no Python interpreter, so the hosted deployment serves the demo workspace's already-ingested content but can't parse a new upload.


Quick start

git clone https://github.com/StudentSuite/pepiros.git
cd pepiros
npm install
npm run dev

Open http://localhost:3000/w/ws-1. ws-1 is the only workspace id the fixture defines.

No environment file, database, or API key is needed to run it: the app is fully functional against fixtures/workspace.json, which ships 3 papers, a contradiction pair, a cross-paper cites link, and one planted misattribution that the verifier correctly demotes to unsupported.

Requires Node 20 or newer. Uploading a real PDF additionally needs Python 3 with PyMuPDF (pip install -r scripts/requirements.txt) and a GROQ_API_KEY (or FEATHERLESS_API_KEY) in .env -- neither is needed just to browse the fixture workspace.

Scripts

Command Does
npm run dev Next.js dev server
npm run build / npm start Production build / serve
npm run typecheck tsc --noEmit across the repo
npm run lint ESLint
npm test / npm run test:watch Vitest over lib/**/*.test.ts
npm run db:generate / db:migrate / db:studio Drizzle Kit against DATABASE_URL
npm run seed scripts/seed.ts (still a stub)
npm run mcp:stdio MCP server over stdio, see Using the MCP server

Architecture

Next.js 15 (App Router, TS, React 19)          -> Vercel
  app/api/*         HTTP surface for the UI
  mcp/server.ts     MCP surface for Claude (stdio today, remote HTTP later)
  lib/services/*    <- BOTH of the above call only this
  lib/grounding/*   deterministic verification, no model calls
  lib/agents/*      archetype classifier, pillar planner, node generators -> Groq, falling back to Featherless (lib/ai/client.ts)
  lib/layout/*      deterministic server-computed node positions (radial | layered)
  lib/chat/*        citation-marker parsing, shared by the chat server and client

Supabase          Postgres (no vector column), Storage, Auth, Realtime (job status)
scripts/parse.py  local PyMuPDF run: sections, chunks, figures, equations, refs, numerics
scripts/ocr_fallback.py   local PaddleOCR-VL, for scanned or table-heavy pages

Three things that look like omissions and are not. There are no embeddings and no vector column: a paper is 8-20k tokens, so the whole thing goes in context behind a prompt cache, addressed by stable citation ids (search_paper scores keyword coverage rather than cosine distance). There is no deployed Python service: PyMuPDF and PaddleOCR run as local scripts, so there is no second deploy target. There is no force-directed layout: positions come from lib/layout/* as a pure function of the graph's shape, so the same graph always lands identically. See plan.md §2 and §11 before proposing any of them.

Working in this repo with an AI coding agent? Read CLAUDE.md first.


Using the MCP server

Point Claude Code (or Desktop) at the stdio server to call the grounding layer from a conversation. Published on npm, no local clone needed:

{
  "mcpServers": {
    "pepiros": {
      "command": "npx",
      "args": ["-y", "pepiros-mcp"]
    }
  }
}

Working from a clone instead (e.g. to test a local change to mcp/* before it's published):

{
  "mcpServers": {
    "pepiros": {
      "command": "npx",
      "args": ["tsx", "mcp/stdio.ts"],
      "cwd": "/absolute/path/to/pepiros"
    }
  }
}

Then the docs/PLAN-V1.md §13.5 beat works for real: ask Claude to summarize the fixture's RCT, then "now verify every claim you just made." It calls verify_claim on its own sentences and reports back which ones the source actually supports.

Worth knowing what the tiers mean, because the distinction is the product: quote located means the quote was found in the source. It does not mean the claim follows from the quote. Nothing here judges entailment: that is deliberately left to the reader, and there is no tool that will do it for you.


Configuration

Nothing below is required to run against the fixture. Copy .env.example to .env once a real backend exists.

LLM provider: Groq primary, Featherless fallback, not Anthropic. Either works alone:

Variable For
GROQ_API_KEY Primary. Defaults to openai/gpt-oss-20b/-120b, the only two Groq models confirmed to support the structured outputs every call here needs; see lib/ai/client.ts's comment before changing either id
FEATHERLESS_API_KEY Fallback, used only when Groq 401/402/403/429s or marks its own error retryable (lib/ai/fallbackModel.ts). Featherless is OpenAI-compatible with no first-party AI SDK provider: lib/ai/client.ts goes through @ai-sdk/openai-compatible. Its $25/mo flat-rate plan is contractually for human-driven use in Featherless's own UI, not the programmatic API traffic this is; the metered Developer plan is the one its terms describe as intended for that
FEATHERLESS_MODEL_FAST, FEATHERLESS_MODEL_STRONG Default to two Qwen2.5 models verified live against a real account (meta-llama's repos there are gated behind a HuggingFace org connection and 403 without it)

Free account:

Variable For
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY Postgres, Storage, Auth, Realtime
DATABASE_URL Drizzle's direct Postgres connection
SESSION_SECRET Signs the app's own session cookie. Any long random string; falls back to a known constant in development and refuses to start without one in production

Enabling Google sign-in

The app is wired for it, but Google itself is enabled per-Supabase-project, not per-env-var, so this is two dashboard steps, not a code change:

  1. Google Cloud Console → create an OAuth 2.0 client (Web application), and set the authorised redirect URI to https://<your-project>.supabase.co/auth/v1/callback.
  2. Supabase dashboard → Authentication → Providers → Google → paste the client ID and secret, and add your app's origin (http://localhost:3000 in development) to Authentication → URL Configuration → Redirect URLs.

Until that is done the button still renders and fails honestly: the callback bounces back to /login with a message rather than dead-ending on a provider error page. Email/password sign-in and guest browsing are unaffected either way.

Free, no key required:

Variable For
NEXT_PUBLIC_APP_URL MCP results carry deep links back into the canvas, so the app needs to know its own origin
OPENALEX_MAILTO, CROSSREF_MAILTO Courtesy identifiers that move requests into the polite rate-limit pool
SEMANTIC_SCHOLAR_API_KEY Optional. Unauthenticated access is enough unless the related-papers rail starts returning 429
MCP_TOKEN_SECRET Self-generated, any long random string

Design system

Direction: Editorial Paper (Are.na × Instapaper/NYT Reader), on top of the "lab notebook at night" thesis: dark chrome, a warm paper-white reading surface, pillar colour as a structural system. Full brief and the canonical palette: design/DIRECTIONS.md. Live token reference: /dev/tokens.

Layer Where
Tokens (color, spacing, radius, elevation, motion, layout dims) app/globals.css, tailwind.config.ts
Fonts (Source Serif 4, Inter, JetBrains Mono via next/font) app/layout.tsx
Primitives (Button, Input, Dialog, Drawer, Tooltip, Popover, Tabs, Menu, Toast, Skeleton, ErrorBanner, Badge, Icon, Logo, ...) components/ui/
Icons Lucide, only through components/ui/Icon.tsx (never a raw lucide-react import in a feature component)
Brand assets (favicon, app icon, OG image) app/favicon.ico, app/icon.png, app/apple-icon.png, app/opengraph-image.png, app/twitter-image.png
Image-gen prompts (brand kit + all 8 app surfaces) design/prompts/, zipped at design/pepiros-editorial-paper-prompts.zip
Platform-vision scope (accounts, publish, discovery, discussion) docs/PLAN-V1.md §22

Two conventions worth knowing before adding a component: pillar hues have two accessors in components/ui/PillarChip.tsx, pillarColor() for borders/dots/edge strokes (canonical hex, 3:1 threshold) and pillarTextColor() for anywhere a pillar hue is literal text colour (lightened for WCAG AA's 4.5:1, three of the seven canonical hues fail it unmixed). And motion reaches for lib/motion.ts's named helpers or the keyframes in globals.css, never a hand-picked duration: Editorial Paper is ease-out only, never spring.


Project docs

File What
plan.md Canonical spec: product, architecture, locked decisions, cut list
docs/PLAN-V1.md Long-form reference behind plan.md, cited by the TODO comments
CLAUDE.md Conventions and invariants for AI coding agents
CONTRIBUTING.md Setup, PR gates, code conventions
CHANGELOG.md Keep a Changelog format
SECURITY.md Private vulnerability reporting

License

MIT © Anay Dhawan and Yash Kewlani

About

Turns a research PDF into a living knowledge graph where every generated claim is bound to a located quote, exposed to Claude as an MCP service.

Resources

Code of conduct

Contributing

Security policy

Stars

4 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages