/**
* crossref.js — Multiview tissue layer
*
* Connects the siloed single-window views. Scans the page for entities,
* enriches them with cross-source context, adds margin provenance notes.
*
* Citation visual language:
* .cite-primary = SOLID underline → external ground truth (SEC, FEC, FAA...)
* .cite-internal = DOTTED underline → our interpretation layer (entityhub, watchers)
* .xref = DOTTED underline → entity cross-ref with source count superscript
*
* Include with:
*
* Requires: data-crossref-token attribute on
or a global CROSSREF_TOKEN.
* Optional: data-crossref-source attribute on to identify the current app.
*/
(function () {
const AW_BASE = 'https://anomalywatch.tools.ejfox.com';
const EH_BASE = 'https://entityhub.tools.ejfox.com';
const DOMAIN = 'tools.ejfox.com';
const token = document.body?.getAttribute('data-crossref-token') || window.CROSSREF_TOKEN || '';
const currentApp = window.location.hostname.split('.')[0];
if (!token) {
console.log('[crossref] No token found. Set data-crossref-token on or window.CROSSREF_TOKEN.');
return;
}
// ─── PRIMARY vs INTERNAL SOURCE CLASSIFICATION ──────────────
const PRIMARY_DOMAINS = new Set([
'sec.gov', 'efts.sec.gov', 'edgar.sec.gov',
'fec.gov', 'api.open.fec.gov',
'data.ny.gov', 'data.cms.gov',
'reddit.com', 'old.reddit.com',
'faa.gov',
'weather.gov', 'api.weather.gov',
'propublica.org', 'projects.propublica.org',
'courtlistener.com',
'opencorporates.com',
'broadcastify.com',
'adsb.lol',
'usaspending.gov',
'fpds.gov',
'sam.gov',
'regulations.gov',
'congress.gov',
'govinfo.gov',
'nrc.gov',
'epa.gov',
'usgs.gov',
'nasa.gov'
]);
// Source names from our watcher apps (internal cross-refs)
const INTERNAL_SOURCES = new Set([
'anomalywatch', 'entityhub', 'skywatch', 'filingwatch', 'countywatch',
'donorwatch', 'redditwatch', '990watch', 'contractwatch', 'changewatch',
'masintwatch', 'transitwatch', 'riverwatch', 'newswatch', 'honeypot',
'feartimeline', 'fearscape'
]);
function isPrimaryUrl(url) {
if (!url) return false;
try {
const hostname = new URL(url).hostname;
return [...PRIMARY_DOMAINS].some(d => hostname === d || hostname.endsWith('.' + d));
} catch { return false; }
}
function isPrimarySource(sourceName) {
if (!sourceName) return false;
const s = sourceName.toLowerCase().trim();
if (INTERNAL_SOURCES.has(s)) return false;
// Anything that's not our own app is presumed primary
if (s.endsWith('watch') || s === 'entityhub') return false;
return true;
}
function classifySource(name, url) {
if (url && isPrimaryUrl(url)) return 'primary';
if (isPrimarySource(name)) return 'primary';
return 'internal';
}
// ─── DEEP LINK REGISTRY ─────────────────────────────────────
let appRegistry = null;
async function loadRegistry() {
try {
const resp = await fetch(`https://assets.${DOMAIN}/apps.json`, { signal: AbortSignal.timeout(3000) });
if (resp.ok) {
const data = await resp.json();
const map = {};
for (const cat of data.categories) {
for (const app of cat.apps) {
if (app.links) {
map[app.id] = {
...app,
baseUrl: `https://${app.id}.${data.domain || DOMAIN}`
};
}
}
}
appRegistry = map;
}
} catch {}
}
function getDeepLink(appId, linkType, params) {
if (!appRegistry || !appRegistry[appId]) return null;
const app = appRegistry[appId];
let template = app.links[linkType];
if (!template) return null;
let url = app.baseUrl + template;
for (const [k, v] of Object.entries(params)) {
url = url.replace(`{${k}}`, encodeURIComponent(v));
}
return url;
}
// ─── FOOTNOTE COLLECTOR ─────────────────────────────────────
const footnotes = {
sources: [], // [1], [2] — external, verifiable
crossrefs: [], // [a], [b] — internal, interpretive
_sourceSet: new Set(),
_crossrefSet: new Set(),
addSource(label, url) {
const key = label + '|' + (url || '');
if (this._sourceSet.has(key)) return this.sources.findIndex(s => s.label === label && s.url === url) + 1;
this._sourceSet.add(key);
this.sources.push({ label, url });
return this.sources.length;
},
addCrossref(label, url) {
const key = label + '|' + (url || '');
if (this._crossrefSet.has(key)) {
const idx = this.crossrefs.findIndex(c => c.label === label && c.url === url);
return String.fromCharCode(97 + idx); // a, b, c...
}
this._crossrefSet.add(key);
this.crossrefs.push({ label, url });
return String.fromCharCode(97 + this.crossrefs.length - 1);
},
render() {
if (this.sources.length === 0 && this.crossrefs.length === 0) return;
// Don't duplicate if already rendered
if (document.querySelector('.fn-sources, .fn-crossrefs')) return;
const container = document.createElement('footer');
container.style.marginTop = '48px';
if (this.sources.length > 0) {
const section = document.createElement('dl');
section.className = 'fn-sources';
let html = 'Sources
';
this.sources.forEach((s, i) => {
const num = i + 1;
const link = s.url ? `${s.url.length > 80 ? s.url.slice(0, 80) + '...' : s.url}` : '';
html += `[${num}]${s.label} ${link}`;
});
section.innerHTML = html;
container.appendChild(section);
}
if (this.crossrefs.length > 0) {
const section = document.createElement('dl');
section.className = 'fn-crossrefs';
let html = 'Cross-references
';
this.crossrefs.forEach((c, i) => {
const letter = String.fromCharCode(97 + i);
const link = c.url ? `${c.label}` : c.label;
html += `[${letter}]${link}`;
});
section.innerHTML = html;
container.appendChild(section);
}
document.body.appendChild(container);
}
};
// ─── STYLES ────────────────────────────────────────────────
// Minimal — delegates to academic.css for .link-popup, .margin-aside,
// .cite-primary, .cite-internal, .xref, .fn-sources, .fn-crossrefs.
// Only defines crossref-specific layout that academic.css doesn't cover.
const style = document.createElement('style');
style.textContent = `
/* Entity tooltip — reuses .link-popup from academic.css but shown inline */
.xref-tooltip {
display: none;
position: absolute;
bottom: 100%; left: 0;
background: var(--bg); border: 1px solid var(--border);
padding: 8px 12px; font-size: 0.85em; max-width: 400px;
z-index: 100; line-height: 1.4;
box-shadow: 0 2px 8px color-mix(in srgb, var(--text) 10%, transparent);
}
.xref:hover .xref-tooltip { display: block; }
.xref-tooltip a { color: var(--link); }
.xref-tooltip .tt-section {
font-family: var(--font-sans); font-size: 0.85em;
color: var(--text-muted); margin-top: 4px; padding-top: 4px;
}
.xref-tooltip .tt-section-primary { border-top: 1px solid var(--border); }
.xref-tooltip .tt-section-internal { border-top: 1px dotted var(--text-faint); }
.xref-tooltip .tt-src { font-family: var(--font-mono); font-size: 0.9em; }
/* Margin sidebar toggle button — sidebar layout via .margin-aside in academic.css */
.margin-aside .xref-toggle {
position: absolute; left: -24px; top: 8px;
width: 20px; height: 20px;
border: 1px solid var(--border); background: var(--bg);
cursor: pointer; font-size: 10px;
display: flex; align-items: center; justify-content: center;
font-family: var(--font-mono);
}
.margin-aside.collapsed { transform: translateX(240px); }
.margin-aside p { margin: 2px 0; line-height: 1.4; }
.xref-overlap-item {
margin: 4px 0; padding: 4px 0;
border-bottom: 1px dotted var(--border-light);
}
`;
document.head.appendChild(style);
// ─── ENTITY ENRICHMENT ─────────────────────────────────────
async function enrichEntities() {
// Collect entity names from marked elements
const entityEls = document.querySelectorAll('[data-entity], .entity-name, .entity, .xref[data-entity]');
const entityNames = new Set();
const entityMap = new Map(); // name -> [elements]
for (const el of entityEls) {
const name = el.getAttribute('data-entity') || el.textContent.trim();
if (name.length < 3 || name.length > 60) continue;
entityNames.add(name);
if (!entityMap.has(name)) entityMap.set(name, []);
entityMap.get(name).push(el);
}
// Also scan table cells and signal titles for entity-like patterns
const cells = document.querySelectorAll('td:first-child, .signal-title, h3 a');
for (const el of cells) {
const text = el.textContent.trim();
const match = text.match(/^(?:\[.*?\]\s*)?([A-Z][^:]{2,40}):/);
if (match) {
const name = match[1].trim();
entityNames.add(name);
if (!entityMap.has(name)) entityMap.set(name, []);
entityMap.get(name).push(el);
}
}
if (entityNames.size === 0) return;
// Batch-check entities against anomalywatch cross-DB layer (limit 10)
const names = [...entityNames].slice(0, 10);
for (const name of names) {
try {
const resp = await fetch(
`${AW_BASE}/api/cross/entity/${encodeURIComponent(name)}?token=${token}&since=30d`,
{ signal: AbortSignal.timeout(5000) }
);
if (!resp.ok) continue;
const data = await resp.json();
// Classify sources into primary vs internal
const primarySources = [];
const internalSources = [];
for (const [key, val] of Object.entries(data)) {
if (Array.isArray(val) && val.length > 0 && key !== 'watchers_available') {
const type = classifySource(key);
(type === 'primary' ? primarySources : internalSources).push({
name: key,
count: val.length,
items: val.slice(0, 3) // keep first few for tooltips
});
}
}
const totalSources = primarySources.length + internalSources.length;
if (totalSources === 0) continue;
// Add to footnotes
for (const s of primarySources) {
footnotes.addSource(s.name, s.items[0]?.url || null);
}
for (const s of internalSources) {
const url = getDeepLink(s.name, 'search', { name }) ||
`https://${s.name}.${DOMAIN}/?q=${encodeURIComponent(name)}`;
footnotes.addCrossref(s.name, url);
}
// Enrich DOM elements
const els = entityMap.get(name) || [];
for (const el of els) {
// Add .xref class if not already
if (!el.classList.contains('xref') && !el.classList.contains('xref-entity')) {
el.classList.add('xref');
}
el.setAttribute('data-n', totalSources);
el.setAttribute('data-entity', name);
el.title = `${totalSources} sources: ${[...primarySources, ...internalSources].map(s => s.name).join(', ')}`;
// Thread Wikidata QID if available
if (data.entity_profile?.length && data.entity_profile[0].wikidata_id) {
el.setAttribute('data-qid', data.entity_profile[0].wikidata_id);
}
// Build tooltip with primary/internal split
const tooltip = document.createElement('span');
tooltip.className = 'xref-tooltip';
const ehEntity = data.entity_profile?.[0];
const ehLink = ehEntity
? `entityhub`
: `search entityhub`;
const qidBadge = ehEntity?.wikidata_id
? ` ${ehEntity.wikidata_id}`
: '';
let html = `${escHtml(name)}${qidBadge} — ${totalSources} sources ${ehLink}
`;
if (primarySources.length > 0) {
html += `Primary
`;
for (const s of primarySources.slice(0, 4)) {
const link = s.items[0]?.url
? ` [→]`
: '';
html += `${escHtml(s.name)}: ${s.count} result${s.count > 1 ? 's' : ''}${link}
`;
}
}
if (internalSources.length > 0) {
html += `Internal
`;
for (const s of internalSources.slice(0, 4)) {
const deepUrl = getDeepLink(s.name, 'search', { name }) ||
`https://${s.name}.${DOMAIN}/?q=${encodeURIComponent(name)}`;
html += `${escHtml(s.name)}: ${s.count} result${s.count > 1 ? 's' : ''} [→]
`;
}
}
if (data.blind_spots) {
html += `${escHtml(data.blind_spots.slice(0, 100))}`;
}
tooltip.innerHTML = html;
el.style.position = 'relative';
el.appendChild(tooltip);
}
} catch (e) {
// Silently skip failed enrichments
}
}
}
// ─── AUTO-CLASSIFY EXISTING LINKS ──────────────────────────
function classifyPageLinks() {
// Find links that aren't already classified and auto-add citation classes
const links = document.querySelectorAll('a:not(.cite-primary):not(.cite-internal):not(.xref):not(.xref-entity)');
for (const a of links) {
const href = a.getAttribute('href');
if (!href || href.startsWith('#') || href.startsWith('javascript:')) continue;
// Skip nav, header, and internal navigation links
if (a.closest('nav, header, .margin-aside, .xref-tooltip')) continue;
try {
const url = new URL(href, window.location.origin);
if (isPrimaryUrl(url.href)) {
a.classList.add('cite-primary');
// Extract source label from hostname
const label = url.hostname.replace(/^(www|api|efts|projects)\./, '').split('.')[0].toUpperCase();
if (!a.getAttribute('data-src')) a.setAttribute('data-src', label);
footnotes.addSource(label, url.href);
} else if (url.hostname.endsWith(DOMAIN) && url.hostname !== window.location.hostname) {
a.classList.add('cite-internal');
const appName = url.hostname.split('.')[0];
if (!a.getAttribute('data-src')) a.setAttribute('data-src', appName);
footnotes.addCrossref(appName, url.href);
}
} catch {}
}
}
// ─── MARGIN SIDEBAR ────────────────────────────────────────
async function buildMargin() {
const margin = document.createElement('aside');
margin.className = 'margin-aside';
const toggle = document.createElement('div');
toggle.className = 'xref-toggle';
toggle.textContent = '\u25C0';
toggle.onclick = () => {
margin.classList.toggle('collapsed');
toggle.textContent = margin.classList.contains('collapsed') ? '\u25B6' : '\u25C0';
};
margin.appendChild(toggle);
// Oracle flip
try {
const resp = await fetch(`${AW_BASE}/api/cross/oracle?token=${token}`, { signal: AbortSignal.timeout(8000) });
if (resp.ok) {
const oracle = await resp.json();
const section = document.createElement('div');
let html = 'Oracle
';
html += `${escHtml(oracle.note)}
`;
if (oracle.top_signal) {
html += `${escHtml(oracle.top_signal.source)} ${escHtml(oracle.top_signal.title.slice(0, 60))}
`;
}
if (oracle.oracle_flip) {
html += `${escHtml(oracle.oracle_flip.source)} ${escHtml(oracle.oracle_flip.title.slice(0, 60))}
`;
}
if (oracle.from_the_periphery) {
const p = oracle.from_the_periphery;
html += `↳ ${escHtml(p.type)}: ${escHtml((p.name || '').slice(0, 40))}
`;
}
section.innerHTML = html;
margin.appendChild(section);
}
} catch {}
// Overlap (entities in multiple sources)
try {
const resp = await fetch(`${AW_BASE}/api/cross/overlap?token=${token}&hours=72&min=2`, { signal: AbortSignal.timeout(8000) });
if (resp.ok) {
const data = await resp.json();
if (data.overlaps?.length > 0) {
const section = document.createElement('div');
let html = `Overlaps (${data.overlaps_found})
`;
for (const o of data.overlaps.slice(0, 5)) {
const ehUrl = getDeepLink('entityhub', 'search', { name: o.entity }) ||
`${EH_BASE}/?q=${encodeURIComponent(o.entity)}`;
html += ``;
html += `
${escHtml(o.entity)} ${o.source_count} sources`;
html += `
${escHtml(o.sources.join(', '))}`;
html += `
`;
}
section.innerHTML = html;
margin.appendChild(section);
}
}
} catch {}
// Freshness summary
try {
const resp = await fetch(`${AW_BASE}/api/cross/freshness?token=${token}`, { signal: AbortSignal.timeout(5000) });
if (resp.ok) {
const data = await resp.json();
const section = document.createElement('div');
let html = `Array Status
`;
html += `${escHtml(data.summary)}
`;
const problems = Object.entries(data.sources)
.filter(([_, v]) => v.status !== 'fresh')
.map(([k, v]) => `${k}: ${v.status}`);
if (problems.length > 0) {
html += `${escHtml(problems.join(', '))}
`;
}
section.innerHTML = html;
margin.appendChild(section);
}
} catch {}
// Page citation summary
const section = document.createElement('div');
section.innerHTML = 'Citations
scanning...
';
margin.appendChild(section);
// Only add margin if we got content
if (margin.children.length > 1) {
document.body.appendChild(margin);
document.body.classList.add('has-margin');
}
}
function updateCiteSummary() {
const el = document.getElementById('xref-cite-summary');
if (!el) return;
const primaryCount = document.querySelectorAll('.cite-primary').length;
const internalCount = document.querySelectorAll('.cite-internal').length;
const xrefCount = document.querySelectorAll('.xref[data-n]').length;
el.textContent = `${primaryCount} primary, ${internalCount} internal, ${xrefCount} entities`;
}
// ─── HTML ESCAPING ──────────────────────────────────────────
function escHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
// ─── GWERN-STYLE LINK POPUPS ──────────────────────────────
// Hover over any cite-primary or cite-internal link to get a preview popup
let activePopup = null;
let popupTimer = null;
function createPopup(anchor) {
if (activePopup) removePopup();
const popup = document.createElement('div');
popup.className = 'link-popup';
const href = anchor.getAttribute('href');
const src = anchor.getAttribute('data-src') || '';
const entity = anchor.getAttribute('data-entity') || '';
const isPrimary = anchor.classList.contains('cite-primary');
const isXref = anchor.classList.contains('xref');
// Build popup content
let html = '';
// Source badge
const badgeClass = isPrimary ? 'primary' : 'internal';
if (src) {
html += ``;
}
// Title — use link text or entity name
const title = entity || anchor.textContent.trim();
html += ``;
// Meta — show URL domain
if (href) {
try {
const url = new URL(href, window.location.origin);
html += ``;
} catch {}
}
// For entity xrefs, show source count
const n = anchor.getAttribute('data-n');
const qid = anchor.getAttribute('data-qid');
if (n) {
html += ``;
}
// For internal links, show which app and what it does
if (!isPrimary && src && appRegistry && appRegistry[src]) {
const app = appRegistry[src];
html += ``;
}
popup.innerHTML = html;
// Position near the anchor
const rect = anchor.getBoundingClientRect();
popup.style.position = 'fixed';
popup.style.left = Math.min(rect.left, window.innerWidth - 440) + 'px';
popup.style.top = (rect.top - 8) + 'px';
popup.style.transform = 'translateY(-100%)';
// If too close to top, show below
if (rect.top < 120) {
popup.style.top = (rect.bottom + 8) + 'px';
popup.style.transform = 'none';
}
document.body.appendChild(popup);
activePopup = popup;
// Fade in
requestAnimationFrame(() => popup.classList.add('visible'));
// Allow clicking links inside popup
popup.addEventListener('mouseenter', () => clearTimeout(popupTimer));
popup.addEventListener('mouseleave', () => { popupTimer = setTimeout(removePopup, 200); });
}
function removePopup() {
if (activePopup) {
activePopup.remove();
activePopup = null;
}
clearTimeout(popupTimer);
}
function setupPopups() {
// Attach to all citation links
const selectors = '.cite-primary, .cite-internal, .xref[data-entity]';
document.querySelectorAll(selectors).forEach(el => {
// Skip elements that already have xref-tooltips (they have their own hover)
if (el.querySelector('.xref-tooltip')) return;
el.addEventListener('mouseenter', () => {
clearTimeout(popupTimer);
popupTimer = setTimeout(() => createPopup(el), 300); // 300ms delay like Gwern
});
el.addEventListener('mouseleave', () => {
clearTimeout(popupTimer);
popupTimer = setTimeout(removePopup, 200);
});
});
}
// ─── SIDENOTE CONVERSION ─────────────────────────────────
// Convert .fn-ref footnote references to Gwern-style sidenotes on wide screens
function convertToSidenotes() {
if (window.innerWidth < 1201) return; // Only on wide screens
const fnRefs = document.querySelectorAll('.fn-ref, a[href^="#fn-"]');
fnRefs.forEach((ref, i) => {
const href = ref.getAttribute('href');
if (!href || !href.startsWith('#fn-')) return;
const targetId = href.slice(1);
const target = document.getElementById(targetId);
if (!target) return;
// Create sidenote
const sidenote = document.createElement('span');
sidenote.className = 'sidenote';
const num = i + 1;
sidenote.innerHTML = `${num}${target.innerHTML}`;
// Find the parent paragraph or block
const parent = ref.closest('p, td, li, div');
if (parent) {
parent.style.position = 'relative';
parent.appendChild(sidenote);
// Convert ref to sidenote-ref style
ref.classList.add('sidenote-ref');
ref.textContent = num.toString();
ref.removeAttribute('href');
}
});
}
// ─── INIT ──────────────────────────────────────────────────
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
async function init() {
// Load registry first, then enrich
await loadRegistry();
// Small delay to let the page render
setTimeout(async () => {
classifyPageLinks();
await enrichEntities();
buildMargin();
// Render footnotes and set up popups after enrichment
setTimeout(() => {
footnotes.render();
convertToSidenotes();
setupPopups();
updateCiteSummary();
}, 1000);
}, 500);
}
// ─── PUBLIC API ──────────────────────────────────────────────
// Allow apps to programmatically add citations
window.crossref = {
entityLink(name, ehId, qid) {
const href = ehId
? `${EH_BASE}/entity/${ehId}`
: `${EH_BASE}/?q=${encodeURIComponent(name)}`;
const qidAttr = qid ? ` data-qid="${escHtml(qid)}"` : '';
return `${escHtml(name)}`;
},
primaryCite(label, url) {
return `${escHtml(label)}`;
},
internalCite(label, url) {
return `${escHtml(label)}`;
},
addSource: (label, url) => footnotes.addSource(label, url),
addCrossref: (label, url) => footnotes.addCrossref(label, url),
isPrimaryUrl,
classifySource,
getDeepLink,
reEnrich() {
classifyPageLinks();
enrichEntities();
setTimeout(() => { footnotes.render(); updateCiteSummary(); }, 1000);
}
};
})();