Inspiration

I study anthropology, and the sentence I keep meeting is some version of "Anatolian Neolithic individuals with more than 0.5x coverage, first-degree relatives excluded." In a paper that is one line in the methods. In practice it is a hundred lines of R, a filtered EIGENSTRAT file, and a cohort definition that lives in one person's script and nowhere else. The definition is the actual scientific object, and it is the one thing the tooling refuses to treat as an object.

The other half of the problem is that exploratory work here is visual. You look at a PCA plot, you see three points sitting outside a cluster, and that is the finding. No filter query produces it, you had to see it.

Meanwhile the part that follows, "who are these three, what are their nearest populations, what publications do they come from," is exactly the part a model is good at and a person is slow at.

So the two of us are each holding half of the work, and every tool I know makes us hold it in different places. That made the tool worth building.

What it does

PaleoScope is a local-first workbench over 22,202 ancient and present-day individuals from the Allen Ancient DNA Resource (v66.p1, compatibility_HO, 276,725 SNPs, CC0).

Three linked canvas panels "Where (map), Who (two PCA bases), When (timeline)" plus a table, cohort list and research journal, all over one state. You filter by population, place, date, coverage, haplogroup, sex, quality assessment and source; you save the result as a named cohort; you compare cohorts, find neighbours and outliers, attach typed findings, and export data, a workspace or a Markdown research report.

An agent works the same workspace at the same time, through 22 imperative WebMCP tools and three declarative export forms. Same state, same methods, same history.

There is no backend and no runtime dependency. Local, unpublished samples can be overlaid as CSV, TSV or EIGENVEC and never leave the tab.

How I built it

Vanilla TypeScript and Vite 8, deployed as a Cloudflare Worker serving static assets, with _headers supplying Origin-Agent-Cluster: ?1 and Permissions-Policy: tools=(self) so the WebMCP surface stays same-origin and cross-origin frames can't reach the tools.

The architecture is a tower where each layer is expressible in the one below: source → packed columnar data (typed arrays, dictionary-coded strings) → view → sets → pure analysis → digest → tools → journal. Panels sit beside layers 2–4, not above them: a lasso and an agent's select call the same store method. A panel is not privileged over an agent.

Every mutating tool supports dryRun, every result carries the same envelope and state-specific next actions, arguments are validated in application code including rejection of undeclared fields, and nothing throws across the WebMCP boundary. The app is fully usable with no agent at all.

What's next for PaleoScope

  • Uncertainty as a first-class axis. Radiocarbon dates carry error ranges and contextual dates carry more; the timeline currently draws a point where there is a distribution. Making that visible is the difference between a nice plot and one you can argue from.
  • f-statistics in the tab. f3 and f4 over the bundled panel would turn "these cohorts look close" into a statement with a standard error.
  • Provenance that survives export. A cohort should carry its filter definition, its dataset version and its per-individual citations into the exported report and back in again, so a saved workspace reproduces rather than merely describes.
  • Multiple named PCA bases from user reference panels, so the projection is a choice the researcher makes and states, not a property of the app.
  • A shared compare_sessions model. Two people, two agents, one workspace is where the actor field and the change log stop being bookkeeping and start being the product.

Built With

Share this project:

Updates

Submission history