/** * 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 += `${escHtml(src)}`; } // 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); } }; })();