import {PROJECT_VERSION,PUBLICATION_API,WB_API,INDICATORS,COUNTRIES,uniqueId,clean,doiId,doiLink,validateUrl,parseCrossref,parseWorldBank,validGraph,normalizeSession,audit,nodeCounts,relevantLinks,shortestPath,graphCSV,linksCSV,graphML,reportMarkdown} from './engine.js'; import {DEMO} from './demo.js'; const $=(sel)=>document.querySelector(sel); const esc=v=>String(v??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",'''); let state={graph:structuredClone(DEMO.graph),demo:true,source:'fictional demo',note:DEMO.notes,view:'network',selected:'c_access',path:[],results:[],query:'',queryError:'',searching:false,fetching:false,indicatorError:'',filter:'all',term:'',flash:'',flashBad:false}; const kindColor={publication:'#70b7e5',claim:'#6fe1c6',indicator:'#c2a0f1',hypothesis:'#f4bd80'}; const kindLabel={publication:'Publication',claim:'Claim',indicator:'Indicator',hypothesis:'Hypothesis'}; const byId=id=>state.graph.nodes.find(n=>n.id===id); const label=n=>n?.title||'(missing)'; const setFlash=(text,bad=false)=>{state.flash=String(text);state.flashBad=bad;const f=$('#flash');f.textContent=state.flash;f.className='flash'+(bad?' bad':'');}; const stamp=()=>new Date().toISOString().slice(0,10); function updateState(fn,msg=''){try{fn();validGraph(state.graph);render();if(msg)setFlash(msg)}catch(e){setFlash(e.message,true)}} function download(name,text,mime='text/plain;charset=utf-8'){const blob=new Blob([text],{type:mime});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1200)} function exportData(type){try{const g=state.graph;const suffix=new Date().toISOString().slice(0,10);if(type==='json')download(`evidencegraph-session-${suffix}.json`,JSON.stringify({schemaVersion:1,exportedAt:new Date().toISOString(),source:state.source,demo:state.demo,notes:state.note,graph:g},null,2),'application/json');if(type==='nodes')download(`evidencegraph-nodes-${suffix}.csv`,graphCSV(g),'text/csv');if(type==='edges')download(`evidencegraph-evidence-${suffix}.csv`,linksCSV(g),'text/csv');if(type==='graphml')download(`evidencegraph-network-${suffix}.graphml`,graphML(g),'application/xml');if(type==='md')download(`evidencegraph-report-${suffix}.md`,reportMarkdown(g,{origin:state.source,note:state.note}),'text/markdown');setFlash('File download initiated. Check your Downloads folder.');}catch(e){setFlash(e.message,true)}} function setView(view){if(!['network','desk','literature','indicators','hypotheses','audit','exports'].includes(view))return;state.view=view;document.querySelectorAll('.nav-link').forEach(b=>b.classList.toggle('active',b.dataset.view===view));document.querySelectorAll('.view').forEach(p=>p.classList.toggle('active',p.id==='panel'+view[0].toUpperCase()+view.slice(1)));render();} function render(){const a=audit(state.graph);$('#summary').innerHTML=`
Research nodes
${a.nodeTotal}
${a.counts.publication} papers · ${a.counts.indicator} indicators
Evidence links
${a.linkTotal}
Sources linked to claims and hypotheses
Located excerpts
${a.completeness}%
Links with both excerpt and locator
Review flags
${a.flags.length}
Unsubstantiated or mixed-evidence claims
`; const badge=$('#dataStatus');badge.textContent=state.demo?'● SYNTHETIC DEMO / MIXED SESSION':'● USER / API METADATA SESSION';badge.className='dataset-status'+(!state.demo?' live':''); const panels={network:renderNetwork,desk:renderDesk,literature:renderLiterature,indicators:renderIndicators,hypotheses:renderHypotheses,audit:renderAudit,exports:renderExports};panels[state.view]?.(a); } const viewHead=(title,desc,action='')=>`

${title}

${desc}

${action}
`; const tag=(t)=>`${esc(t)}`; const option=(n,selected='')=>``; const rowHTML=(n,withButton=true)=>`
${tag(n.kind)}${n.origin==='SYNTHETIC DEMO'?tag('fictional'):''}${esc(n.title)}
${esc(n.origin||'User-entered')} ${n.year?'· '+esc(n.year):''}${n.doi?' · DOI '+esc(n.doi):''}${n.kind==='indicator'?' · '+esc(n.value)+' '+esc(n.unit):''}${withButton?``:''}
`; function graphSVG(g){const kinds=['publication','indicator','claim','hypothesis'];const groups={};for(const k of kinds)groups[k]=g.nodes.filter(n=>n.kind===k&&((state.filter==='all'||state.filter===k)&&(!state.term||n.title.toLowerCase().includes(state.term.toLowerCase()))));const visible=Object.values(groups).flat();const visibleIds=new Set(visible.map(n=>n.id));const W=970,H=520;const cols=[130,335,575,810];const pos=new Map();kinds.forEach((k,col)=>{const ns=groups[k];ns.forEach((n,i)=>pos.set(n.id,{x:cols[col],y:ns.length<=1?260:65+(390/(ns.length-1))*i}));}); const edges=g.links.filter(l=>visibleIds.has(l.from)&&visibleIds.has(l.to)).map(l=>{const a=pos.get(l.from),b=pos.get(l.to);const color=l.relation==='supports'?'#59d7b8':l.relation==='challenges'?'#ed8f91':'#95a7ef';return ``}).join('');const nodes=visible.map(n=>{const p=pos.get(n.id);const selected=n.id===state.selected;const r=n.kind==='claim'?24:n.kind==='hypothesis'?23:18;const title=n.title.length>24?n.title.slice(0,22)+'…':n.title;return `${esc(title)}`}).join('');return `${kinds.map((k,i)=>`${k.toUpperCase()}S`).join('')}${edges}${nodes}`} function renderNetwork(a){const root=$('#panelNetwork');const n=byId(state.selected)||state.graph.nodes[0];if(n&&!byId(state.selected))state.selected=n.id;const links=n?relevantLinks(state.graph,n.id):[];root.innerHTML=viewHead('Evidence network','Inspect how publication metadata, located excerpts and policy hypotheses connect.',`
`)+`

Network map · ${a.nodeTotal} nodes / ${a.linkTotal} links

${graphSVG(state.graph)}
${Object.entries(kindColor).map(([k,c])=>`${kindLabel[k]}`).join('')}SupportsChallengesContext

Network connections document research judgments. They are not independent verification or causal proof. Select a node to inspect its provenance.

`+ `

Trace a research connection

Shortest evidence path · undirected
${state.path.length?`
${state.path.map(id=>esc(label(byId(id)))).join(' → ')}
`:'

Trace shows graph connectivity only, not proof that one proposition implies another.

'}
`+ `

Selected node

${n?`
${tag(n.kind)} ${esc(n.title)}
Origin${esc(n.origin||'User-entered')}
${n.year?`
Year${esc(n.year)}
`:''}${n.doi?`
DOI${esc(n.doi)}
`:''}${n.kind==='indicator'?`
Value${esc(n.value)} ${esc(n.unit)}
`:''}

${esc(n.description||n.scope||'No detailed description available.')}

${n.url&&validateUrl(n.url)?`

Open original source ↗

`:''}`:'
No nodes yet. Create one in the Evidence desk.
'}

Direct links (${links.length})

${links.length?links.map(l=>`
${tag(l.relation)}${esc(label(byId(l.from===n.id?l.to:l.from)))}
${esc(l.locator||'No source locator supplied')}${esc(l.excerpt||'No excerpt supplied')}
`).join(''):'
This node has no documented links.
'}
`; root.querySelector('#graphFilter').addEventListener('change',e=>{state.filter=e.target.value;render()});} function renderDesk(a){const g=state.graph;const sources=g.nodes.filter(n=>n.kind==='publication'||n.kind==='indicator'||n.kind==='claim');const targets=g.nodes.filter(n=>n.kind==='claim'||n.kind==='hypothesis');const links=g.links.slice().reverse();$('#panelDesk').innerHTML=viewHead('Evidence desk','Write explicit research claims and attach source-located supporting, challenging or contextual passages.')+ `

01 / Add claim or hypothesis

02 / Link located evidence

Metadata alone cannot support a finding. Only user-provided excerpts are linked, with a locator and explicit review status.

Evidence registry

${links.length} recorded connections
${links.length?links.map(l=>`
${tag(l.relation)}${tag(l.review||'unverified')}${esc(label(byId(l.from)))} → ${esc(label(byId(l.to)))}
Excerpt: ${esc(l.excerpt||'[missing]')}Locator: ${esc(l.locator||'[missing]')}
`).join(''):'
No links yet. Start by creating a claim and adding a source excerpt.
'}
`; } function renderLiterature(a){const pubs=state.graph.nodes.filter(n=>n.kind==='publication');$('#panelLiterature').innerHTML=viewHead('Literature hub','Search Crossref publication metadata or enter a bibliographic source manually. Titles are not evidence.')+ `

Search real publication metadata

Live metadata requires internet and the public Crossref service. Abstracts/full texts are not automatically retrieved.

${state.queryError?`
${esc(state.queryError)}
`:''}

Add a publication manually

Search results ${state.results.length} returned

${state.results.length?state.results.map((n,i)=>`
${esc(n.title)}${esc((n.authors||[]).slice(0,3).join(', ')||'Authors unavailable')} · ${esc(n.year||'Year unknown')} · ${esc(n.venue||'Venue unspecified')}
${n.doi?`${esc(n.doi)}`:''}
`).join(''):'
Results will appear here after a successful public search.
'}

Saved bibliography

${pubs.length} records
${pubs.length?pubs.map(n=>rowHTML(n)).join(''):'
No publication metadata saved.
'}
`;} function renderIndicators(a){const indicators=state.graph.nodes.filter(n=>n.kind==='indicator');$('#panelIndicators').innerHTML=viewHead('Indicator hub','Bring quantitative context into your literature map without mistaking correlation for causal evidence.')+ `

Import latest available World Bank observation

${state.indicatorError?`
${esc(state.indicatorError)}
`:''}
Reports the latest observed year, not an inferred current value. Data may lag by several years.

Add an explicitly sourced custom indicator

Indicator registry

These numbers provide quantitative context. Any links to hypotheses must include a researcher's explanation.

${indicators.length?indicators.map(n=>rowHTML(n)).join(''):'
No indicators imported yet.
'}
`;} function renderHypotheses(a){const h=state.graph.nodes.filter(n=>n.kind==='hypothesis');const all=a.claimSummaries;$('#panelHypotheses').innerHTML=viewHead('Hypothesis comparison','Compare competing propositions and identify under-documented or contested links. This is not a causal estimator.')+ `
${h.length?h.map(n=>{const k=all.find(s=>s.id===n.id);return `

${esc(n.title)}

${tag('hypothesis')}

${esc(n.description||'Research hypothesis awaiting examination.')}

Supportive links${k?.positive||0}
Challenging links${k?.negative||0}
Context links${k?.context||0}
Located excerpts${k?.documented||0}
${k?.documented?'Links recorded for researcher review; factual accuracy not independently verified.':'No located evidence yet. Do not infer support from this graph.'}
`}).join(''):'
Create competing hypotheses in the Evidence desk to compare them.
'}

Methodological caution

Simple counts are a discovery aid only. A single high-quality study might outweigh many weak or duplicated claims. No automatic validity or evidence-weight inference is performed. Mixed evidence is flagged rather than silently resolved.

`;} function renderAudit(a){const counts=a.counts;const all=a.claimSummaries;$('#panelAudit').innerHTML=viewHead('Quality audit','Transparent documentation diagnostics, missing-source review and contradiction signals. No truth or safety certification.')+ `
${a.completeness}%
Excerpt + locator coverage

Share of links with a nonempty excerpt and source locator.

${a.unresolved.length}
Incomplete links

Links missing the documented passage or its exact location.

${a.orphans.length}
Unconnected nodes

Records not yet linked to the research network.

Documentation coverage

This is documentation completeness, not research correctness or strength of evidence.

Audit flags

${a.flags.length} items
${a.flags.length?a.flags.map(f=>`
${tag(f.kind)}${esc(f.text)}
`).join(''):'
No missing-documentation or mixed-relation flags detected. This does not validate the underlying research.
'}

Source audit

${all.map(n=>``).join('')}
StatementSupportsChallengesLocatedDistinct upstream nodes
${esc(n.title)}${n.positive}${n.negative}${n.documented}${n.sourceCount}
`;} function renderExports(a){$('#panelExports').innerHTML=viewHead('Export & methods','Download a fully portable research session or transparent evidence and graph exports.')+ `

Research data exports

Download your session to keep it. The app deliberately does not upload notes or browser documents to an EvidenceGraph server.

Restore a saved session

Imported files are validated. Invalid links, duplicated IDs and unsupported shapes are rejected without replacing your current session.

Field notes

Notes remain in memory until exported. Closing this tab without downloading the session loses changes.

Research integrity rules

01 · Source metadata

Crossref records identify publications. A DOI or title never automatically establishes what the study concludes.

02 · Located passages

Supporting and challenging connections require researcher-entered excerpts and locators. Review status is explicit.

03 · Official indicators

World Bank figures preserve observed years and values; they are not a substitute for causal identification.

04 · Fictional examples

Demo records are fabricated to teach the workflow and carry synthetic provenance labels.

05 · Privacy and security

No secret keys, no local browser storage, no server database. External API searches send only the requested query.

06 · Reproducibility

JSON round trips, CSV registers, GraphML interoperability, and tested deterministic audit logic.

`;} // Navigation and delegated controls. document.querySelectorAll('[data-view]').forEach(b=>b.addEventListener('click',()=>setView(b.dataset.view))); $('#saveSessionHeader').addEventListener('click',()=>exportData('json')); $('#application').addEventListener('click',async e=>{const n=e.target.closest('[data-select-node]');if(n){state.selected=n.dataset.selectNode;setView('network');return}const g=e.target.closest('[data-goto]');if(g){setView(g.dataset.goto);return}const ex=e.target.closest('[data-export]');if(ex){exportData(ex.dataset.export);return}const rm=e.target.closest('[data-remove-link]');if(rm){if(!window.confirm('Remove this evidence link from the current session?'))return;updateState(()=>{state.graph.links=state.graph.links.filter(l=>l.id!==rm.dataset.removeLink)},'Connection removed from this in-memory session.');return}const ar=e.target.closest('[data-add-result]');if(ar){const item=state.results[Number(ar.dataset.addResult)];if(!item)return;updateState(()=>{if(item.doi&&state.graph.nodes.some(n=>n.kind==='publication'&&n.doi===item.doi))throw new Error('A publication with this DOI is already in your library.');state.graph.nodes.push(structuredClone(item));state.selected=item.id;},'Publication metadata added. Evidence still requires a researcher-entered excerpt.');return}const act=e.target.closest('[data-action]');if(act){if(act.dataset.action==='demo'){if(!window.confirm('Replace the current in-memory session with the fictional demonstration? Export changes first if needed.'))return;state.graph=structuredClone(DEMO.graph);state.note=DEMO.notes;state.demo=true;state.source='fictional demo';state.selected='c_access';state.path=[];state.results=[];render();setFlash('Fictional demonstration restored. All example values and papers are synthetic.');} if(act.dataset.action==='trace-path'){const from=$('#traceFrom')?.value,to=$('#traceTo')?.value;state.path=shortestPath(state.graph,from,to);render();setFlash(state.path.length?`Research path found with ${state.path.length-1} link(s). Connectivity is not proof.`:'No connection found between these nodes.',!state.path.length);} if(act.dataset.action==='reset'){if(!window.confirm('Start a new empty session? Export any changes first.'))return;state.graph={nodes:[],links:[]};state.note='';state.demo=false;state.source='manual research session';state.selected='';state.results=[];render();setFlash('Empty session started. Existing Hugging Face projects are unaffected.');} if(act.dataset.action==='save-notes'){state.note=$('#notesInput')?.value||'';setFlash('Notes retained in memory. Export JSON to save them to your computer.');} return;}}); $('#application').addEventListener('keydown',e=>{if((e.key==='Enter'||e.key===' ')&&e.target.matches('[data-select-node]')&&e.target.tagName.toLowerCase()==='g'){e.preventDefault();state.selected=e.target.dataset.selectNode;setView('network');}}); $('#application').addEventListener('submit',async e=>{const form=e.target;e.preventDefault();const f=Object.fromEntries(new FormData(form));try{ if(form.id==='nodeForm'){const title=clean(f.title,260);if(!title)throw new Error('Enter a statement.');updateState(()=>{const n={id:uniqueId(f.kind==='claim'?'c':'h'),kind:f.kind,title,description:clean(f.description,1500),origin:'Researcher-entered',dateAdded:stamp()};state.graph.nodes.push(n);state.selected=n.id},'Research statement added. It still needs located evidence.');} if(form.id==='linkForm'){if(f.from===f.to)throw new Error('A statement cannot link to itself.');if(!clean(f.excerpt)||!clean(f.locator))throw new Error('An excerpt and a precise locator are required.');updateState(()=>{state.graph.links.push({id:uniqueId('e'),from:f.from,to:f.to,relation:f.relation,excerpt:clean(f.excerpt,1800),locator:clean(f.locator,220),review:f.review,origin:'Researcher-entered',dateAdded:stamp()});state.selected=f.to},'Located evidence link saved. Relation is a research judgment, not automatic verification.');} if(form.id==='manualPubForm'){const title=clean(f.title,260),doi=f.doi?doiId(f.doi):'';if(!title)throw new Error('Enter a publication title.');if(f.doi&&!doi)throw new Error('The DOI is invalid. Check its format.');if(doi&&state.graph.nodes.some(n=>n.doi===doi))throw new Error('Publication DOI already added.');updateState(()=>{const n={id:uniqueId('p'),kind:'publication',title,doi,url:doiLink(doi),year:f.year?Number(f.year):null,authors:clean(f.authors,220).split(';').map(x=>x.trim()).filter(Boolean),origin:'User-provided metadata',scope:'unverified bibliography record',dateAdded:stamp()};state.graph.nodes.push(n);state.selected=n.id},'Publication metadata saved. Verify it against the original work.');} if(form.id==='manualIndicatorForm'){const value=Number(f.value),year=Number(f.year);if(!Number.isFinite(value)||year<1800||year>2200)throw new Error('Enter a numeric value and a valid observed year.');if(f.url&&!validateUrl(f.url))throw new Error('Indicator URL must begin with http:// or https://');updateState(()=>{const n={id:uniqueId('i'),kind:'indicator',title:clean(f.title,200),value,year,unit:clean(f.unit,35),url:validateUrl(f.url),origin:'User-supplied indicator (not independently verified)',scope:'descriptive observation',dateAdded:stamp()};state.graph.nodes.push(n);state.selected=n.id;},'Indicator saved with user-supplied provenance.');} if(form.id==='crossrefForm'){const query=clean(f.query,140);state.query=query;state.queryError='';state.searching=true;render();try{const ctrl=new AbortController();const tm=setTimeout(()=>ctrl.abort(),16000);let result;try{const isDoi=doiId(query);const url=isDoi?`${PUBLICATION_API}/${encodeURIComponent(isDoi).replace(/%2F/ig,'/')}`:`${PUBLICATION_API}?query.bibliographic=${encodeURIComponent(query)}&rows=7&select=DOI,title,author,published,issued,created,container-title,URL`;const res=await fetch(url,{signal:ctrl.signal,headers:{Accept:'application/json'}});if(!res.ok)throw new Error(`Crossref HTTP ${res.status}`);result=await res.json();}finally{clearTimeout(tm)}const items=Array.isArray(result?.message?.items)?result.message.items:[result.message];state.results=items.map(x=>{try{return parseCrossref(x)}catch{return null}}).filter(Boolean);if(!state.results.length)throw new Error('No usable bibliography results. Try a broader topic or manual entry.');setFlash(`Crossref returned ${state.results.length} metadata records. Review before using.`);}catch(error){state.queryError=error?.name==='AbortError'?'Search timed out. Try again later or enter metadata manually.':`Crossref unavailable: ${error.message}. You can add records manually.`;state.results=[];setFlash(state.queryError,true)}finally{state.searching=false;render();}} if(form.id==='wbForm'){state.fetching=true;state.indicatorError='';render();try{const {country,indicator}=f;const url=`${WB_API}/country/${country}/indicator/${indicator}?format=json&per_page=80&date=2005:2026`;const ctrl=new AbortController();const tm=setTimeout(()=>ctrl.abort(),16000);let json;try{const res=await fetch(url,{signal:ctrl.signal});if(!res.ok)throw new Error(`World Bank HTTP ${res.status}`);json=await res.json();}finally{clearTimeout(tm)}const n=parseWorldBank(json,{country,indicator});updateState(()=>{const match=state.graph.nodes.find(x=>x.kind==='indicator'&&x.code===n.code&&x.country===n.country&&x.year===n.year);if(match)throw new Error('This indicator/country/year combination is already in the network.');state.graph.nodes.push(n);state.selected=n.id;},`Official World Bank observation imported (year ${n.year}). Indicators do not establish causal effects.`);}catch(error){state.indicatorError=error?.name==='AbortError'?'World Bank request timed out.':'Could not import an official observation: '+error.message;setFlash(state.indicatorError,true)}finally{state.fetching=false;render();}} }catch(error){setFlash(error.message,true)}}); $('#application').addEventListener('change',async e=>{if(e.target.id==='sessionFile'){const file=e.target.files?.[0];if(!file)return;try{if(file.size>2_000_000)throw new Error('Session JSON is too large (2 MB limit).');const obj=JSON.parse(await file.text());const loaded=normalizeSession(obj);const source=typeof obj.source==='string'?clean(obj.source,100):'restored JSON session';if(!window.confirm('Replace this in-memory session with the selected validated JSON?'))return;state.graph=loaded.graph;state.note=loaded.notes;state.demo=Boolean(obj.demo||state.graph.nodes.some(n=>n.origin==='SYNTHETIC DEMO'));state.source=source;state.selected=state.graph.nodes[0]?.id||'';render();setFlash('Validated JSON research session restored. Review all imported provenance.');}catch(error){setFlash('Session import rejected: '+error.message,true)}finally{e.target.value=''}}}); render();