File size: 10,547 Bytes
2816118 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /** EvidenceGraph Core v1.0 • framework-free / reproducible deterministic utilities. */
export const PROJECT_VERSION='1.0.0';
export const NODE_TYPES=['publication','claim','indicator','hypothesis'];
export const RELATIONS=['supports','challenges','context'];
export const PUBLICATION_API='https://api.crossref.org/works';
export const WB_API='https://api.worldbank.org/v2';
export const INDICATORS={
'SE.PRM.CMPT.ZS':{label:'Primary completion rate',unit:'%'},
'SE.TER.ENRR':{label:'Tertiary enrollment (gross)',unit:'%'},
'NY.GDP.PCAP.CD':{label:'GDP per capita (current US$)',unit:'US$'},
'SP.DYN.LE00.IN':{label:'Life expectancy at birth',unit:'years'},
'IT.NET.USER.ZS':{label:'Individuals using the Internet',unit:'%'},
'SL.UEM.TOTL.ZS':{label:'Unemployment rate (modeled ILO)',unit:'%'}
};
export const COUNTRIES={ARG:'Argentina',BOL:'Bolivia',BRA:'Brazil',CHL:'Chile',COL:'Colombia',ECU:'Ecuador',MEX:'Mexico',PRY:'Paraguay',PER:'Peru',URY:'Uruguay'};
export const uniqueId=(prefix='n')=>`${prefix}_${(globalThis.crypto?.randomUUID?.()||`${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`).replaceAll('-','')}`;
export const clean=(v,max=5000)=>String(v??'').trim().slice(0,max);
export function validateUrl(url){try{const u=new URL(url);return ['https:','http:'].includes(u.protocol)?u.href:''}catch{return ''}}
export function doiId(raw){let x=clean(raw,256).replace(/^https?:\/\/(dx\.)?doi\.org\//i,'').replace(/^doi:\s*/i,'');try{x=decodeURIComponent(x)}catch{}return /^10\.\d{4,9}\/[\w.()/:;+-]+$/i.test(x)?x.toLowerCase():''}
export function doiLink(value){const d=doiId(value);return d?`https://doi.org/${encodeURIComponent(d).replace(/%2F/ig,'/')}`:''}
export const clamp=(n,lo=0,hi=1)=>Math.min(hi,Math.max(lo,Number.isFinite(+n)?+n:lo));
export function parseCrossref(record){const x=record?.message||record||{};const title=clean(Array.isArray(x.title)?x.title[0]:x.title,280);if(!title)throw new Error('No title returned by the publication service.');const authors=(Array.isArray(x.author)?x.author:[]).slice(0,15).map(a=>clean([a.given,a.family].filter(Boolean).join(' '),90)).filter(Boolean);
const year=Number(x.published?.['date-parts']?.[0]?.[0]||x.issued?.['date-parts']?.[0]?.[0]||x.created?.['date-parts']?.[0]?.[0])||null;
const doi=doiId(x.DOI||'');const url=doi?doiLink(doi):validateUrl(x.URL||'');
return {kind:'publication',id:uniqueId('p'),title,authors,year:year&&year>1600&&year<=2200?year:null,doi,url,venue:clean(Array.isArray(x['container-title'])?x['container-title'][0]:x['container-title'],140),origin:'Crossref metadata',scope:'bibliographic metadata only',abstract:'',dateAdded:new Date().toISOString().slice(0,10)};
}
export function parseWorldBank(json,{country,indicator}){if(!COUNTRIES[country]||!INDICATORS[indicator])throw new Error('Select an available country and indicator.');const rows=Array.isArray(json)?json[1]:null;if(!Array.isArray(rows))throw new Error('No World Bank observations found.');const matching=rows.filter(x=>x?.value!==null&&x?.value!==undefined&&Number.isFinite(Number(x.value))&&String(x.date||'').match(/^\d{4}$/)).sort((a,b)=>Number(b.date)-Number(a.date));if(!matching.length)throw new Error('No numeric observations found.');const entry=matching[0];return {kind:'indicator',id:uniqueId('i'),title:`${INDICATORS[indicator].label} · ${COUNTRIES[country]}`,country,code:indicator,year:Number(entry.date),value:Number(entry.value),unit:INDICATORS[indicator].unit,origin:'World Bank Indicators API',url:`https://api.worldbank.org/v2/country/${country}/indicator/${indicator}?format=json`,dateAdded:new Date().toISOString().slice(0,10),scope:'descriptive indicator, not causal evidence'};}
export function samePublication(a,b){return !!(a?.doi&&b?.doi&&doiId(a.doi)===doiId(b.doi))}
export function validGraph(graph){if(!graph||!Array.isArray(graph.nodes)||!Array.isArray(graph.links))throw new Error('Session must contain nodes and links arrays.');if(graph.nodes.length>300||graph.links.length>700)throw new Error('Session exceeds maximum size (300 nodes, 700 links).');const ids=new Set();let sourceCount=0;
for(const n of graph.nodes){if(!n||!NODE_TYPES.includes(n.kind)||typeof n.id!=='string'||!/^[-\w]{1,100}$/.test(n.id)||ids.has(n.id))throw new Error('Invalid or duplicated node ID/type.');ids.add(n.id);if(!clean(n.title,500))throw new Error('A node is missing its title.');if(n.kind==='publication'&&n.doi&&!doiId(n.doi))throw new Error('Invalid DOI in imported publication.');if(n.kind==='indicator'&&(!Number.isFinite(Number(n.value))||!/^\d{4}$/.test(String(n.year))))throw new Error('Invalid indicator value/year.');if(n.kind==='publication')sourceCount++;}
for(const l of graph.links){if(!l||typeof l.id!=='string'||!ids.has(l.from)||!ids.has(l.to)||l.from===l.to||!RELATIONS.includes(l.relation))throw new Error('Invalid edge or dangling source.');}
return {nodes:graph.nodes.length,links:graph.links.length,publications:sourceCount};}
export function normalizeSession(payload){const data=payload?.graph||payload;validGraph(data);return {schemaVersion:1,exportedAt:new Date().toISOString(),graph:{nodes:data.nodes.map(n=>structuredClone(n)),links:data.links.map(l=>structuredClone(l))},notes:clean(payload?.notes||'',5000)};}
export function nodeCounts(g){const out={publication:0,claim:0,indicator:0,hypothesis:0};for(const n of g.nodes)out[n.kind]=(out[n.kind]||0)+1;return out;}
export function audit(g){validGraph(g);const referenced=new Set(g.links.flatMap(e=>[e.from,e.to]));const orphans=g.nodes.filter(n=>!referenced.has(n.id));const unresolved=g.links.filter(l=>!clean(l.excerpt)||!clean(l.locator));const flags=[];const claimSummaries=[];
for(const node of g.nodes.filter(n=>['claim','hypothesis'].includes(n.kind))){const edges=g.links.filter(l=>l.to===node.id);const pos=edges.filter(l=>l.relation==='supports');const neg=edges.filter(l=>l.relation==='challenges');const withEvidence=edges.filter(l=>clean(l.excerpt)&&clean(l.locator));const uniqueSources=new Set(withEvidence.map(l=>l.from));const contested=pos.length>0&&neg.length>0;const finding={id:node.id,title:node.title,positive:pos.length,negative:neg.length,context:edges.filter(l=>l.relation==='context').length,documented:withEvidence.length,sourceCount:uniqueSources.size,contested,needsAttention:!withEvidence.length||contested};claimSummaries.push(finding);if(contested)flags.push({kind:'contested',nodeId:node.id,text:`Mixed supportive and challenging links: ${node.title}`});if(!withEvidence.length)flags.push({kind:'missing-evidence',nodeId:node.id,text:`No locator-backed excerpt: ${node.title}`});}
const completeness=g.links.length?Math.round((g.links.length-unresolved.length)/g.links.length*100):0;
return {counts:nodeCounts(g),nodeTotal:g.nodes.length,linkTotal:g.links.length,orphans,unresolved,flags,claimSummaries,completeness};}
export function relevantLinks(g,id){return g.links.filter(l=>l.to===id||l.from===id)}
export function shortestPath(g,a,b){if(a===b)return [a];const visited=new Set([a]);const q=[[a]];while(q.length){const path=q.shift();const tail=path.at(-1);for(const l of relevantLinks(g,tail)){const next=l.from===tail?l.to:l.from;if(visited.has(next))continue;const p=path.concat(next);if(next===b)return p;visited.add(next);q.push(p);}}return []}
export function graphCSV(g){const header=['id','kind','title','source','year','doi','url'];return [header,...g.nodes.map(n=>[n.id,n.kind,n.title,n.origin||'',n.year||'',n.doi||'',n.url||''])].map(row=>row.map(csvCell).join(',')).join('\n')+'\n'}
export function linksCSV(g){return [['id','source_id','target_id','relation','excerpt','locator','review_status','note'],...g.links.map(l=>[l.id,l.from,l.to,l.relation,l.excerpt||'',l.locator||'',l.review||'unverified',l.note||''])].map(row=>row.map(csvCell).join(',')).join('\n')+'\n'}
function csvCell(v){const t=String(v??'');return /[",\r\n]/.test(t)?'"'+t.replaceAll('"','""')+'"':t}
const esc=(v)=>String(v??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",''');
export function graphML(g){validGraph(g);return `<?xml version="1.0" encoding="UTF-8"?>\n<graphml xmlns="http://graphml.graphdrawing.org/xmlns"><key id="kind" for="node" attr.name="kind" attr.type="string"/><key id="label" for="node" attr.name="label" attr.type="string"/><key id="relation" for="edge" attr.name="relation" attr.type="string"/><graph edgedefault="directed">${g.nodes.map(n=>`<node id="${esc(n.id)}"><data key="kind">${esc(n.kind)}</data><data key="label">${esc(n.title)}</data></node>`).join('')}${g.links.map(l=>`<edge id="${esc(l.id)}" source="${esc(l.from)}" target="${esc(l.to)}"><data key="relation">${esc(l.relation)}</data></edge>`).join('')}</graph></graphml>`;}
export function reportMarkdown(g,{note='',origin='user-session'}={}){const a=audit(g);const map=new Map(g.nodes.map(n=>[n.id,n]));const line=['# EvidenceGraph Research Report','','**Scope:** Evidence-link documentation and exploration, not causal inference or independent fact verification.','**Session origin:** '+clean(origin,100),'**Generated:** '+new Date().toISOString(),'','## Inventory',`- ${a.counts.publication} publication records; ${a.counts.indicator} indicators; ${a.counts.claim} claims; ${a.counts.hypothesis} hypotheses.`,`- ${a.linkTotal} links, ${a.completeness}% with excerpts and locators.`,`- ${a.unresolved.length} links missing excerpt and/or locator.`,`- ${a.orphans.length} unconnected nodes.`,'','## Evidence statements'];
for(const s of a.claimSummaries){line.push('',`### ${s.title}`,`- Supporting links: ${s.positive}; challenging links: ${s.negative}; context links: ${s.context}; located excerpts: ${s.documented}.`,s.contested?'- Mixed viewpoints were linked: requires researcher review.':'- The presence of links does not establish factual truth.');for(const l of g.links.filter(l=>l.to===s.id)){const n=map.get(l.from);line.push(`- ${l.relation.toUpperCase()}: ${n?.title||'Unknown source'}; locator: ${l.locator||'[not provided]'}; excerpt: ${l.excerpt||'[not provided]'}; review: ${l.review||'unverified'}`)}}
line.push('','## Caveats','- Publication titles and DOI metadata are not evidence of the paper\'s findings.','- Excerpts entered by users have not been independently checked against original documents.','- Indicators are descriptive series, not automatically connected to causality.','- Synthetic demonstrations are fictional and are never offered as real-world findings.', '', '## Research notes',clean(note)||'None.','');return line.join('\n')}
|