A map of what humanity hasn't figured out yet.
Most knowledge maps show what we know. lacuna tries to show where knowledge stops. The knowledge tree is scaffolding — the holes are the product.
Status: the computed layer does not work yet, and this README says so before it says anything else. The method lacuna was built around failed the test it was pre-registered against. The curated layers work, the pipeline works, and the negative result is published rather than buried. Details in the validation report.
| kind | what it is | how it's found |
|---|---|---|
open |
A question a field has explicitly acknowledged it cannot answer — the Riemann hypothesis, P vs NP, the hard problem of consciousness. | Curated. Every entry must cite a source. |
blocked |
A well-posed question nobody is short of ideas about, stopped by an instrument, a cost, an ethical limit, or a timescale. | Curated, tagged with the blocker. |
gap |
Two areas of research that should probably have met and never did. | Computed. This is the part that doesn't work yet. |
The gap layer was the reason to build this: it can be computed rather than hand-written, so it
can find things nobody thought to write down. That is also why its failure is the headline.
The live map includes an interactive lab for auditing the failed metric rather than hiding its output:
- search all 500 exported measurements by topic name or OpenAlex topic ID;
- separate exact counts from API-derived upper bounds;
- sort by original metric rank, observed/expected ratio, or structural closeness;
- open the pair query and both source-row queries behind any measurement;
- copy a deep link to one pair or export the current filtered view as CSV.
The validation verdict renders before the controls and every card. Searchability does not promote these rows into candidate discoveries; it makes a negative result easier to inspect and falsify.
Every curated open question, blocked question, and declared blind spot has a canonical page under
/holes/. The production build generates those pages directly
from the validated, versioned curated artifact rather than maintaining a second copy of the text.
Each page works without JavaScript, exposes its sources and provenance label, carries distinct
Open Graph/Twitter metadata, and links back to the exact card in the full map.
The sitemap is generated from the same artifact. Computed pairs are deliberately excluded: the current method failed validation, so social previews and search landing pages must not recast its measurements as discoveries. A sourced-hole issue form lets readers propose additions while requiring evidence and preserving the boundary between absent academic coverage and absent human knowledge.
In 1986 Don Swanson noticed that papers on fish oil described effects on blood — lower viscosity, less platelet aggregation — and, separately, papers on Raynaud's syndrome described patients whose blood had exactly those problems. Neither literature cited the other. Nobody had written the sentence "fish oil might help Raynaud's". The connection was sitting in public, unassembled. Clinical trials later supported it.
lacuna looks for that shape at scale. For every pair of research topics it asks two questions:
- Do they meet? Count papers filed under both. Compare that to how many you would expect if topics were assigned independently — two topics covering 1% of the literature each should share about 0.01% of it. Far fewer than expected means they don't meet.
- Do they keep the same company? Find topics that both associate strongly with. If A and C both connect to the same intermediates but never to each other, that is Swanson's shape.
A gap is a pair scoring high on both. Distance alone is not interesting — most pairs are unrelated. Closeness alone is just similarity, which other tools already do well.
On pre-1986 data the fish oil / Raynaud's pair scored top 30.8% of pairs. The bar, fixed in advance, was top 5%. Two different designs for question 2 both failed.
Question 1 worked perfectly. Before 1986 the two topics appeared together in zero papers where chance predicts about 21 — odds of roughly 1 in 1.5 billion. The bridge then genuinely formed afterwards, 0 papers becoming 9. The gap was real, it was measurable, and it closed.
Question 2 is what breaks. "Keeps similar company but rarely co-occurs" turns out to describe, for the most part, adjacent clinical specialties that split papers between them — bladder cancer versus renal cancer, appendicitis versus gastrointestinal tumours. A paper goes to one topic or the other, rarely both, so they look like a gap without anything being undiscovered. The metric measures how OpenAlex partitions subject matter, not where knowledge stops.
Swanson worked with individual MeSH terms inside a curated vocabulary where terms are not alternative labels for one another. OpenAlex topics are not that, and that difference appears to be the whole problem.
- Measured and written content stay visually and structurally distinct, everywhere. Numbers render in monospace and tinted; human-written entries in body text with citations. Nothing that came out of a model inherits the authority of a measurement.
- Every computed number traces back to runnable queries and pinned inputs. Each exported pair carries the two row queries behind its measured or bounded count plus a targeted query that can resolve the exact count. The manifest pins canonical content digests for the taxonomy, co-occurrence rows, and exported files. A number a reader cannot check is decoration.
- The validation tests are load-bearing. They pin the measured outcome including the failure.
If a change makes the target pair suddenly rank well, that is a reason to investigate, not to
celebrate — see
tests/test_swanson_validation.py. - OpenAlex covers academic publishing only. Humanities reference linkage runs ~70% against ~95% for STEM; pre-1970 literature thins out sharply; craft, practitioner and indigenous knowledge are absent entirely. Those blind spots are entries in the map, not footnotes under it.
pip install -e ".[dev]"
python -m pipeline.ingest.fetch_taxonomy # ~31 calls, asserts 4/26/252/4516
python -m pipeline.ingest.fetch_cooccurrence --slice pre1986 # resumable; re-run to continue
python -m pipeline.validate.validate_swanson # the pre-registered test
python -m pipeline.export.build_artifacts # writes artifacts/{date}/
python -m pipeline.export.build_project_status # writes contributor gate status
python -m pipeline.export.verify_artifacts # checks committed file digests
cd web && npm install && npm run devfetch_cooccurrence needs one request per topic and the free tier allows about 1,000 credits a
day, so a full 1,458-topic sweep spans two days. It resumes where it stopped; just run it again.
Setting OPENALEX_API_KEY is reported to raise the ceiling substantially — unverified, and the
research that claimed it was wrong by 10× about the anonymous limit.
OPENALEX_MAILTO is optional and identifies a local run to OpenAlex. It is used only on requests;
credentials and email addresses are stripped from cached provenance and published artifacts.
Tests:
python -m pytest -m "not slow" # unit tests, under a second
python -m pytest # adds regression tests over a fetched sweep; minutesInspect one fetched pair without allocating the full ranking:
python -m pipeline.inspect_gap "Fatty Acid Research" "Systemic Sclerosis"This command deliberately produces no hypothesis. The current metric failed validation, so its output is diagnostic evidence only.
Install the repository hook after installing development dependencies:
pre-commit installIt runs the fast Python suite and TypeScript typecheck before each commit. GitHub Actions runs the
tests available in a clean clone, curated-content validation, committed-artifact integrity, and
the production web build on pushes and pull requests. The five slow regression tests require the
gitignored fetched sweep and therefore run locally through $validate; CI does not represent them
as having passed.
pipeline/
openalex_client.py cached, resumable, records the URL behind every response
ingest/ taxonomy and co-occurrence sweeps
metric/gap_score.py both metric versions; the failed one is kept deliberately
validate/ the pre-registered test
export/ versioned static artifacts + curated content validation
curated/ open.json, blocked.json, blind-spots.json
artifacts/{snapshot}/{metric-version}/ what the site reads; no backend
artifacts/project-status.json generated v3 source/benchmark gate status
web/ TypeScript, static build
docs/metric-validation-preregistration.md criteria, committed before any score existed
One request returns a whole row of the co-occurrence matrix, because OpenAlex lets filter and
group_by compose. That makes a full topic-level matrix 4,516 requests instead of a 400 GB
snapshot download — the single most useful thing discovered while building this.
Persistent project rules live in AGENTS.md. Repeatable workflows are repository skills:
$validateruns every available validation gate and reports skips or drift.$gapinspects one topic pair and prints raw evidence and source queries.$honestaudits the latest change for a claim that outruns its evidence.
The highest-impact contributions are historical MEDLINE source leads, metric-blind benchmark-case
review, and provenance or validation engineering. Read CONTRIBUTING.md before
opening an issue: a vocabulary file is not a historical citation baseline, and a plausible pair is
not automatically a positive case. The live site's Open work board is generated from
artifacts/project-status.json; its counts and contract fingerprints are rebuilt from the same
validators used by CI rather than maintained as website copy.
The interpretation layer remains gated off. The proposed replacement experiment is documented in
plans/metric-v3-validation-plan.md; it moves the biomedical
pilot to period-appropriate MeSH terms and a multi-case held-out benchmark rather than tuning a
third formula on the canonical pair. Its
benchmarks/v3/cases.json contract is deliberately still a draft:
2/8 positives, 0/8 hard negatives, 0/8 distant negatives, and no eligible held-out cutoff. Run
python -m pipeline.benchmark.validate_v3 to see the blockers; only --require-ready is a
shipping gate. pipeline.pubmed_client can batch citation/MeSH metadata for mapping audits, but its
output is explicitly maintained-current and cannot satisfy the historical-indexing gate.
Potential cases first enter the separate
benchmarks/v3/candidates.json ledger. Its five proposed cases
from the classic replication catalog, five proposed post-2002 cases from LION's nominal cancer
set, and two rejected noise examples do not count toward readiness; only accepted entries with an
independent LBD replication may be copied into cases.json. Run
python -m pipeline.benchmark.validate_candidates to audit that boundary.
Negative controls have a separate, frozen selection protocol in
benchmarks/v3/negative-selection.json. Its deterministic
sampler uses only pinned 2012 and 2013 MeSH tree structure and a fixed pre-metric seed to propose
8 ontology-adjacent hard negatives and 8 cross-branch distant negatives. Run
python -m pipeline.benchmark.negative_controls --build to reproduce
artifacts/negative-candidates.json, or omit --build to
validate the committed queue. All 16 records are generated review proposals: they contribute zero
to readiness, assert no absence of knowledge, and cannot enter cases.json without public,
metric-blind human adjudication. An accepted negative must preserve its generated proposal ID and
direct public issue-comment decision; validate_v3 audits the commit-pinned frozen queue and
rejects any case whose kind, split, cutoff, or descriptor identity drifts from that proposal.
The live Metric-blind review desk publishes all 14 contract-validated intake records directly
from that ledger, including their evidence links, mapping limitations, adjudication rationale, and
unresolved questions. Structural validity is not scientific acceptance: it defaults to the ten
proposed records and labels each one as contributing zero to readiness; accepted and rejected
records remain visible as the audit trail. Deep links let reviewers discuss one stable candidate
identity in issue #7 without duplicating the source data.
The historical-input gate is also red: NLM's legacy MBR download endpoint no longer resolves. The
official NLM file inventories are now recovered for all four required releases—archived snapshots
for 2007 and 2011, plus the live 2012 and 2013 pages—and pinned in
benchmarks/v3/inventories.json. Run
python -m pipeline.benchmark.source_inventories --probe --require-match to fetch every page, sum
every file-size row, and fail on drift. This is completeness metadata only: it establishes targets
of 538, 653, 684, and 717 files, but supplies neither the raw XML nor per-file checksums. The
retired MBR homepage itself is now preserved as a digest-addressed Common Crawl WARC range in
benchmarks/v3/mbr-capture.json. It independently confirms the
historical Download/Baselines/{year} directory rows and the same release totals. Run
python -m pipeline.benchmark.mbr_capture --probe --require-match to check the index record and
independently replay the exact compressed WARC range, verify its payload digest, and parse those
rows. A transient index outage no longer prevents the pinned WARC from being audited, but the
strict command still exits non-zero unless both components match. This is repository metadata
only: no required XML payload was recovered, and an unreachable preservation service is reported
as reachability failure rather than data drift.
The required MeSH descriptor archives are separately pinned by SHA-256, and that vocabulary also does
not replace the missing citation records. benchmarks/v3/sources.json records the dated
observation; python -m pipeline.benchmark.validate_sources --require-ready must keep failing until
the matching historical records have stable URLs and checksums. The streaming
baseline reader and targeted pair/ABC accumulator are implemented and fixture-tested, but their
production entry point is deliberately closed: it accepts a baseline only when every local file
matches the complete pinned release by filename, byte count, and SHA-256. Release-manifest
generation also requires measured file, byte, and record totals to match independently recorded
official-inventory totals, so a local subset that disagrees with the reviewed inventory values
cannot self-certify.
The static site is deployed from main by the Git-integrated Cloudflare Pages project lacuna.
Cloudflare builds from the repository root with:
npm --prefix web ci && npm --prefix web run buildThe published directory is web/dist; Vite copies the committed, versioned artifacts into that
directory without reshaping their contents. The production site is
lacuna.tangvu.dev; its Cloudflare Pages origin is
lacuna-a2y.pages.dev.
The build also publishes a root sitemap, permissive robots.txt, a real noindex 404 page, social
preview metadata, JSON-LD, and the favicon/install-icon set from web/public. The initial HTML
contains an honest, number-free account of the method status and known coverage limits, so that
crawlers and readers without JavaScript do not receive an empty loading shell.
Search-engine ownership tokens are intentionally not committed. Set either optional Cloudflare Pages build variable to inject its verification meta tag at build time:
GOOGLE_SITE_VERIFICATION=<Search Console HTML-tag token>
BING_SITE_VERIFICATION=<Bing Webmaster Tools meta-tag token>
After ownership is verified, submit https://lacuna.tangvu.dev/sitemap.xml in the relevant
webmaster console. The sitemap contains the canonical homepage, the generated hole-atlas index,
and one page per curated entry. Hash sections, failed-metric pair views, and versioned JSON evidence
files are not separate search landing pages.
Code is released under the MIT License. Data from OpenAlex is CC0.