Repository navigation
Expand file tree
/
Copy pathapp.js
More file actions
142 lines (140 loc) · 38.2 KB
/
Copy pathapp.js
File metadata and controls
142 lines (140 loc) · 38.2 KB
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { analyzeFiles, readProjectFiles, readZipBytes, IGNORED_DIRS, PROJECT_LIMITS, ZIP_LIMITS } from './core/analyzer.mjs';
import { buildGraphModel } from './core/graph.mjs';
const $ = (s) => document.querySelector(s);
const zipInput = $('#zipInput');
let analysis = null;
const scanSnapshotKey='codedna.scan.v1';
function saveAnalysisSnapshot(result){try{const symbolCount=result.files.reduce((sum,file)=>sum+(file.symbols?.length||0),0);if(result.files.length>5000||result.edges.length>50000||symbolCount>100000){sessionStorage.removeItem(scanSnapshotKey);return}const snapshot={version:1,projectName:result.projectName,sourceFiles:result.sourceFiles,totalCode:result.totalCode,totalFiles:result.totalFiles,skippedFiles:result.skippedFiles,metadataOnlyFiles:result.metadataOnlyFiles,edges:result.edges,cycles:result.cycles,dependencies:result.dependencies,manager:result.manager,technologies:result.technologies,manifest:result.manifest,rootFiles:result.rootFiles,languages:Object.fromEntries(Object.entries(result.languages).map(([name,stat])=>[name,{...stat,dirs:[...stat.dirs]}])),files:result.files.map(({content,...file})=>file)};const encoded=JSON.stringify(snapshot);if(encoded.length>3_000_000){sessionStorage.removeItem(scanSnapshotKey);return}sessionStorage.setItem(scanSnapshotKey,encoded)}catch{try{sessionStorage.removeItem(scanSnapshotKey)}catch{}}}
function restoreAnalysisSnapshot(){try{const saved=JSON.parse(sessionStorage.getItem(scanSnapshotKey)||'null');if(!saved||saved.version!==1||!Array.isArray(saved.files))return;const languages=Object.fromEntries(Object.entries(saved.languages||{}).map(([name,stat])=>[name,{...stat,dirs:new Set(stat.dirs||[])}]));const files=saved.files;files.skippedFiles=saved.skippedFiles||[];analysis={...saved,languages,files};render();toast('Son proje özeti bu sekmede geri yüklendi. Kaynak içerikleri saklanmadı.')}catch{try{sessionStorage.removeItem(scanSnapshotKey)}catch{}}}
const colors = ['#a5ec74','#5bcdb1','#b5c982','#8cbded','#c19be8','#f0b678'];
function toast(message, error=false){const el=$('#toast');el.textContent=message;el.classList.toggle('error',error);el.classList.add('visible');clearTimeout(toast.timer);toast.timer=setTimeout(()=>el.classList.remove('visible'),4500)}
let pendingPicker=null;
function openPicker(input,label='project files'){
if(!(input instanceof HTMLInputElement)||input.type!=='file'){
toast('Dosya seçici bulunamadı. Sayfayı yenileyip yeniden deneyin.',true);
return;
}
pendingPicker=input;
setIntake(0,'Dosya seçimi bekleniyor',label,'Dosya penceresinde seçim yap');
// input.click() is the most compatible path across Chromium builds. Keep it
// synchronous in the trusted button click so the browser retains user activation.
try{input.click()}catch(error){
try{if(typeof input.showPicker==='function')input.showPicker();else throw error}
catch{pendingPicker=null;hideIntake();toast('Dosya penceresi açılamadı. Sayfayı yenileyip yeniden deneyin.',true)}
}
}
function pickerCancelled(input){if(pendingPicker!==input)return;pendingPicker=null;hideIntake();toast('Dosya seçimi kapatıldı; bir ZIP, klasör veya dosya seçince tarama başlayacak.')}
window.addEventListener('focus',()=>{const input=pendingPicker;if(!input)return;setTimeout(()=>{if(pendingPicker===input&&!input.files?.length)pickerCancelled(input)},350)});
let completionTimer=null;
function setIntake(percent,title,detail,count=''){const card=$('#intakeProgress'),track=$('#intakeTrack');if(!card||!track)return;const value=Math.max(0,Math.min(99,Math.round(percent)));clearTimeout(completionTimer);if($('#completionCard'))$('#completionCard').hidden=true;card.hidden=false;$('#intakeTitle').textContent=title;$('#intakeDetail').textContent=detail;$('#intakePercent').textContent=value+'%';track.setAttribute('aria-valuenow',value);$('#intakeFill').style.width=value+'%';$('#intakeCount').textContent=count}
function hideIntake(){const card=$('#intakeProgress');if(card)card.hidden=true}
function finishIntake(detail){window.scrollTo({top:0,behavior:'smooth'});hideIntake();const card=$('#completionCard');if(!card){toast('Yüklendi, DNA’sı çıkarıldı · '+detail);return}$('#completionDetail').textContent=detail;card.hidden=false;clearTimeout(completionTimer);completionTimer=setTimeout(()=>{card.hidden=true},12000)}
$('#completionClose')?.addEventListener('click',()=>{$('#completionCard').hidden=true;clearTimeout(completionTimer)});
async function readFileBytes(file,onProgress){const bytes=new Uint8Array(file.size),reader=file.stream().getReader();let offset=0,lastReported=0;while(true){const{value,done}=await reader.read();if(done)break;if(offset+value.byteLength>bytes.length)throw new Error('Archive changed while it was being read.');bytes.set(value,offset);offset+=value.byteLength;if(offset-lastReported>=1024*1024||offset===file.size){lastReported=offset;onProgress(offset,file.size)}}return bytes}
function resolveTarget(source,spec,paths){if(!spec.startsWith('.'))return null;let base=source.split('/');base.pop();for(const p of spec.split('/')){if(p==='..')base.pop();else if(p!=='.')base.push(p)}let path=base.join('/');for(const c of [path,path+'.ts',path+'.tsx',path+'.js',path+'.jsx',path+'.mjs',path+'.cjs',path+'.json',path+'/index.ts',path+'/index.tsx',path+'/index.js',path+'/index.jsx'])if(paths.has(c))return c;return null}
async function analyze(file){
if(file.size>ZIP_LIMITS.archiveBytes){$('#completionCard').hidden=true;clearTimeout(completionTimer);toast('ZIP exceeds the '+formatBytes(ZIP_LIMITS.archiveBytes)+' archive limit. Choose the folder to scan large files directly.',true);return}
try{
$('#completionCard').hidden=true;clearTimeout(completionTimer);
setIntake(0,'ZIP okunuyor',file.name,'Başlıyor…');
const bytes=await readFileBytes(file,(done,total)=>setIntake(75*done/Math.max(total,1),'ZIP okunuyor',file.name,formatBytes(Math.max(0,total-done))+' kaldı'));
setIntake(76,'Arşiv taranıyor','Dosyalar ve klasör yapısı okunuyor…');
const files=await readZipBytes(bytes,ZIP_LIMITS,state=>{
const total=Math.max(state.totalEntries||1,1),fraction=state.phase==='scanning'||state.phase==='unpacking'?Math.min(1,(state.entry-1+state.done/Math.max(state.bytesTotal||1,1))/total):Math.max(0,(state.done-1)/total);
const detail=state.path?state.path.split(/[\\/]/).pop():(state.phase==='unpacking'?'Büyük dosya açılıyor…':'Arşiv girdileri inceleniyor…');
const entry=state.entry??state.done;
const remaining=state.phase==='scanning'||state.phase==='unpacking'?formatBytes(Math.max(0,(state.bytesTotal||0)-state.done))+' · '+Math.max(0,total-entry)+' dosya sonra':Math.max(0,total-entry)+' dosya kaldı';
setIntake(76+18*fraction,state.phase==='unpacking'?'Büyük dosya açılıyor':state.phase==='scanning'?'Büyük dosya inceleniyor':'Arşiv taranıyor',detail,state.totalEntries?entry+' / '+state.totalEntries+' · '+remaining:'');
});
if(!files.length)throw new Error('No readable project files were found in this ZIP.');
setIntake(96,'DNA haritası oluşturuluyor','Diller, bağımlılıklar ve bağlantılar bir araya getiriliyor…');
await new Promise(requestAnimationFrame);analysis=analyzeFiles(files);saveAnalysisSnapshot(analysis);render();
finishIntake(analysis.projectName+' · '+files.length.toLocaleString()+' dosya incelendi');
}catch(e){hideIntake();$('#analysisStatus').textContent='ANALYSIS FAILED';toast(e.message,true)}
}async function analyzeLocal(inputFiles){
const chosen=[...inputFiles];if(!chosen.length)return;
const pathOf=file=>file.webkitRelativePath||file.name,completedSizes=new Map();let totalBytes=0;
for(const file of chosen){completedSizes.set(pathOf(file),totalBytes);totalBytes+=file.size}
$('#analysisStatus').textContent='READING PROJECT FILES…';setIntake(0,'Dosyalar okunuyor',chosen.length.toLocaleString()+' dosya seçildi','0 / '+formatBytes(totalBytes));
try{
const files=await readProjectFiles(chosen,{onProgress:(done,total,path,bytes,size)=>{
const completed=completedSizes.get(path)||0,percent=totalBytes?5+87*(completed+bytes)/totalBytes:5+87*done/Math.max(total,1);
const shortName=path.split(/[\\/]/).pop();setIntake(percent,'Dosyalar inceleniyor',shortName+' · '+formatBytes(bytes)+' / '+formatBytes(size),Math.min(done+1,total)+' / '+total+' dosya · '+formatBytes(Math.max(0,totalBytes-completed-bytes))+' kaldı');
$('#analysisStatus').textContent='READING '+Math.min(done+1,total)+'/'+total+' · '+shortName;
}});
if(!files.length)throw new Error('No readable text files were found in the selected project.');
setIntake(96,'DNA haritası oluşturuluyor','Diller, bağımlılıklar ve bağlantılar bir araya getiriliyor…',files.length.toLocaleString()+' dosya');
await new Promise(requestAnimationFrame);analysis=analyzeFiles(files);saveAnalysisSnapshot(analysis);render();
const streamed=analysis.metadataOnlyFiles?analysis.metadataOnlyFiles+' büyük dosya tarandı · ':'',excluded=analysis.skippedFiles.length?analysis.skippedFiles.length+' dosya dışarıda · ':'';
finishIntake(analysis.projectName+' · '+streamed+excluded+files.length.toLocaleString()+' dosya incelendi');
}catch(e){hideIntake();$('#analysisStatus').textContent='ANALYSIS FAILED';toast(e.message,true)}
}function render(){const a=analysis;$('#projectCrumb').textContent=a.projectName.toUpperCase();$('#analysisStatus').textContent='ANALYSIS COMPLETE';$('#fileMetric').textContent=a.sourceFiles.toLocaleString();$('#langMetric').textContent=Object.keys(a.languages).length.toLocaleString();$('#depMetric').textContent=Object.keys(a.dependencies).length.toLocaleString();$('#edgeMetric').textContent=a.edges.length.toLocaleString();const chart=Object.entries(a.languages).sort((x,y)=>y[1].code-x[1].code);$('#languageChart').innerHTML=chart.length?`<div class="chart">${chart.map(([n,x],i)=>`<div class="chart-row"><span>${esc(n)}</span><div class="chart-track"><div class="chart-fill" style="width:${a.totalCode?Math.max(2,x.code/a.totalCode*100):0}%;background:${colors[i%colors.length]}"></div></div><span class="chart-value">${a.totalCode?(x.code/a.totalCode*100).toFixed(1):'0.0'}%</span></div>`).join('')}</div>`:'<div class="chart-empty">No supported programming languages detected.</div>';$('#techList').innerHTML=a.technologies.length?`<div class="tech-list">${a.technologies.slice(0,8).map(t=>`<div class="tech-item"><span class="tech-name">${esc(t.name)}</span><span class="tech-meta">${esc(t.category)}</span></div>`).join('')}</div>`:'<div class="tech-empty">No supported frameworks found in the available evidence.</div>';renderAnatomy();renderFiles();renderGraph();}
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
function card(title,eyebrow,body,wide=false){return`<article class="panel anatomy-card${wide?' anatomy-card-wide':''}"><div class="panel-title"><div><div class="eyebrow">${eyebrow}</div><h3>${title}</h3></div><span class="evidence-label">EVIDENCE</span></div>${body}</article>`}
function anatomyRows(rows){return rows.length?rows.map(([a,b])=>`<div class="anatomy-line"><span>${esc(a)}</span><span>${esc(b)}</span></div>`).join(''):'<div class="anatomy-line"><span>No evidence detected</span><span>UNDETERMINED</span></div>'}
function anatomyStat(label,value,detail){return`<div class="anatomy-stat"><span>${label}</span><strong>${value}</strong><small>${detail}</small></div>`}
function evidenceTag(text){return`<span class="evidence-tag">${esc(text)}</span>`}
function dependencyTiles(entries,kind){return entries.length?`<div class="dependency-grid">${entries.slice(0,80).map(([name,version])=>`<div class="dependency-tile"><span>${esc(name)}</span><code>${esc(version)}</code><small>${kind}</small></div>`).join('')}${entries.length>80?`<div class="dependency-overflow">+${entries.length-80} more entries</div>`:''}</div>`:`<div class="anatomy-empty-inline"><b>No ${kind.toLowerCase()} dependencies were detected.</b><span>This means no corresponding entries were readable in the available manifest.</span></div>`}
function renderAnatomy(){const a=analysis,manifestFile=a.files.find(file=>file.path==='package.json'||file.path.endsWith('/package.json'));let manifest=a.manifest||{};if(!a.manifest&&manifestFile?.content)try{manifest=JSON.parse(manifestFile.content)}catch{}const production=Object.entries(manifest.dependencies||{}),development=Object.entries(manifest.devDependencies||{}),languages=Object.entries(a.languages).sort((x,y)=>y[1].code-x[1].code),palette=['#b7e986','#65c9ae','#92b7dc','#d1b875','#c598bf','#79a8a0'],configs=a.files.filter(file=>/(^|\/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|tsconfig[^/]*\.json|[^/]*(?:\.config\.[cm]?[jt]sx?|\.ya?ml|\.toml|\.ini|\.env\.example)|Dockerfile|docker-compose[^/]*|\.github\/workflows\/[^/]+)$/i.test(file.path)),declarations=a.files.reduce((sum,file)=>sum+(file.symbols?.length||0),0),unresolved=a.files.reduce((sum,file)=>sum+(file.imports||[]).filter(item=>!resolveTarget(file.path,item.specifier,new Set(a.files.map(candidate=>candidate.path)))&&!item.specifier.startsWith('.')).length,0),directories=new Map();for(const file of a.files){const first=file.path.includes('/')?file.path.split('/')[0]:'(project root)',entry=directories.get(first)||{files:0,bytes:0,code:0};entry.files++;entry.bytes+=file.size||0;entry.code+=file.metrics.code;directories.set(first,entry)}const languageBar=languages.map(([name,stat],index)=>`<i title="${esc(name)} · ${a.totalCode?(stat.code/a.totalCode*100).toFixed(1):0}%" style="width:${a.totalCode?stat.code/a.totalCode*100:0}%;background:${palette[index%palette.length]}"></i>`).join('');const languageRows=languages.map(([name,stat],index)=>`<div class="language-profile-row"><span class="language-swatch" style="--swatch:${palette[index%palette.length]}"></span><div class="language-main"><div class="language-heading"><b>${esc(name)}</b><span>${stat.files} files · ${stat.code.toLocaleString()} code lines</span></div><div class="language-track"><i style="width:${a.totalCode?Math.max(1,stat.code/a.totalCode*100):0}%;background:${palette[index%palette.length]}"></i></div><small>${[...stat.dirs].slice(0,4).map(esc).join(' · ')||'Project root'}</small></div><strong>${a.totalCode?(stat.code/a.totalCode*100).toFixed(1):'0.0'}%</strong></div>`).join('');const techCards=a.technologies.map(tech=>`<article class="technology-tile"><div class="technology-mark">${esc(tech.name.slice(0,2).toUpperCase())}</div><div class="technology-copy"><div><b>${esc(tech.name)}</b>${evidenceTag(tech.category)}</div><small>${esc(tech.evidence)}</small></div><code>${esc(tech.version)}</code></article>`).join('');const scripts=Object.entries(manifest.scripts||{});const directoryRows=[...directories].sort((x,y)=>y[1].files-x[1].files).slice(0,10).map(([path,item])=>`<div class="directory-row"><span class="directory-glyph">⌑</span><b>${esc(path)}</b><span>${item.files} readable files</span><small>${item.code.toLocaleString()} code lines · ${formatBytes(item.bytes)}</small></div>`).join('');const configRows=configs.slice(0,24).map(file=>`<div class="evidence-file"><span class="config-glyph">↳</span><code>${esc(file.path)}</code><small>${formatBytes(file.size)} · ${esc(file.language)}</small></div>`).join('');$('#anatomyContent').className='anatomy-content anatomy-report';$('#anatomyContent').innerHTML=`<div class="anatomy-summary anatomy-card-wide"><div class="anatomy-summary-head"><div><div class="eyebrow">PROJECT READOUT</div><h2>${esc(a.projectName)}</h2><p>A static inventory built from ${a.totalFiles.toLocaleString()} readable files. Values below link back to the evidence found in this scan.</p></div>${evidenceTag('LOCAL · STATIC ANALYSIS')}</div><div class="anatomy-stats">${anatomyStat('SOURCE FILES',a.sourceFiles.toLocaleString(),'supported code languages')}${anatomyStat('CODE LINES',a.totalCode.toLocaleString(),'nonblank, noncomment')}${anatomyStat('LANGUAGES',Object.keys(a.languages).length.toLocaleString(),'detected in source')}${anatomyStat('DEPENDENCIES',Object.keys(a.dependencies).length.toLocaleString(),'manifest entries')}${anatomyStat('IMPORT LINKS',a.edges.length.toLocaleString(),'resolved local references')}</div></div><article class="panel anatomy-card anatomy-card-wide language-card"><div class="panel-title"><div><div class="eyebrow">01 / COMPOSITION</div><h3>Language DNA</h3><p>Share is calculated from supported-language code lines, excluding blank and comment lines.</p></div>${evidenceTag('DERIVED')}</div>${languages.length?`<div class="language-stack">${languageBar}</div><div class="language-profile">${languageRows}</div>`:'<div class="anatomy-empty-inline"><b>No supported source languages found.</b><span>Readable files may still appear in the inventory below.</span></div>'}</article><article class="panel anatomy-card"><div class="panel-title"><div><div class="eyebrow">02 / TECHNOLOGY</div><h3>Frameworks & tools</h3><p>Every item names the file that supports its detection.</p></div>${evidenceTag(`${a.technologies.length} FOUND`)}</div><div class="technology-grid">${techCards||'<div class="anatomy-empty-inline"><b>No framework evidence detected.</b><span>No supported framework or tool was found in the readable manifests or configuration files.</span></div>'}</div></article><article class="panel anatomy-card"><div class="panel-title"><div><div class="eyebrow">03 / EXECUTION</div><h3>Runtime & build</h3><p>Commands are read as text from manifests; CODE DNA never runs them.</p></div>${evidenceTag(a.manager.toUpperCase())}</div><div class="runtime-facts">${anatomyRows([['Package manager',a.manager],['Lockfile evidence',a.files.find(file=>/\.(?:lock|lockb|sum)$/.test(file.path)||/^(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/.test(file.path))?.path||'No supported lockfile found'],['Runtime constraint',manifest.engines?.node||a.technologies.find(tech=>tech.name==='Node.js ecosystem')?.version||'Undetermined'],['Manifest',manifestFile?.path||a.manifest?.path||'No readable package.json']])}</div>${scripts.length?`<div class="script-list"><div class="eyebrow">DECLARED SCRIPTS · NOT EXECUTED</div>${scripts.slice(0,8).map(([name,command])=>`<div><b>${esc(name)}</b><code>${esc(command)}</code></div>`).join('')}</div>`:''}</article><article class="panel anatomy-card"><div class="panel-title"><div><div class="eyebrow">04 / REPOSITORY</div><h3>Project structure</h3><p>Top-level folders and files summarized from readable paths.</p></div>${evidenceTag(`${a.rootFiles} ROOT FILES`)}</div><div class="directory-list">${directoryRows||'<div class="anatomy-empty-inline"><b>No directory paths detected.</b><span>All readable items are listed at the project root.</span></div>'}</div></article><article class="panel anatomy-card anatomy-card-wide"><div class="panel-title"><div><div class="eyebrow">05 / DEPENDENCY MANIFESTS</div><h3>Packages</h3><p>Names and version ranges are copied from the available package manifest.</p></div>${evidenceTag(`${production.length} PROD · ${development.length} DEV`)}</div><div class="dependency-columns"><section><h4>Production</h4>${dependencyTiles(production,'PRODUCTION')}</section><section><h4>Development</h4>${dependencyTiles(development,'DEVELOPMENT')}</section></div>${!a.manifest&&(!manifestFile||!manifestFile.content)?'<p class="anatomy-caveat">No readable package manifest was available; direct package lists are undetermined.</p>':''}</article><article class="panel anatomy-card anatomy-card-wide"><div class="panel-title"><div><div class="eyebrow">06 / CONFIGURATION EVIDENCE</div><h3>Project settings</h3><p>Configuration, workflow, and lock files found during the scan.</p></div>${evidenceTag(`${configs.length} FILES`)}</div><div class="evidence-file-grid">${configRows||'<div class="anatomy-empty-inline"><b>No supported configuration files detected.</b><span>Only files included in the scan can provide configuration evidence.</span></div>'}</div>${configs.length>24?`<div class="anatomy-caveat">Showing 24 of ${configs.length} matching files. Use the DNA map inventory to find the rest.</div>`:''}</article><article class="panel anatomy-card anatomy-card-wide analysis-quality"><div class="panel-title"><div><div class="eyebrow">07 / ANALYSIS BOUNDARIES</div><h3>What the scan could establish</h3></div>${evidenceTag('TRANSPARENT')}</div><div class="quality-grid">${anatomyStat('DECLARATIONS',declarations.toLocaleString(),'lightweight TS/JS matches')}${anatomyStat('IMPORT CYCLES',a.cycles.length.toLocaleString(),'resolved relative imports')}${anatomyStat('UNRESOLVED',unresolved.toLocaleString(),'external or alias references')}${anatomyStat('STREAMED',a.metadataOnlyFiles.toLocaleString(),'large files read in chunks')}${anatomyStat('EXCLUDED',a.skippedFiles.length.toLocaleString(),'shown with a reason')}</div><p class="anatomy-caveat">Line and comment counts are approximate lexical metrics. Declaration matches are not AST parsing; import analysis does not prove runtime behavior or resolve aliases. The scan is static and does not execute project code.</p></article></div>`}
function formatBytes(bytes){if(!bytes)return '0 B';const units=['B','KB','MB','GB'];const i=Math.min(Math.floor(Math.log(bytes)/Math.log(1024)),units.length-1);return`${(bytes/Math.pow(1024,i)).toFixed(i?1:0)} ${units[i]}`}
function renderFiles(){const term=$('#fileSearch').value.trim().toLowerCase();const rows=analysis.files.filter(f=>!term||f.path.toLowerCase().includes(term)||f.language.toLowerCase().includes(term)||String(f.metrics.lines).includes(term)||formatBytes(f.size).toLowerCase().includes(term)||f.ext.toLowerCase()===term).slice(0,1000);$('#fileList').innerHTML=rows.length?`<div class="file-list">${rows.map(f=>`<div class="file-row"><span>${esc(f.path)}</span><span>${esc(f.language)}${f.metadataOnly?' · STREAMED':''}</span><span>${f.metrics.lines.toLocaleString()} lines</span><span>${formatBytes(f.size)}</span></div>`).join('')}${analysis.files.length>1000?`<div class="file-empty">Showing 1,000 of ${analysis.files.length.toLocaleString()} files. Filter to narrow results.</div>`:''}</div>`:`<div class="inventory-no-results"><b>No matching files</b><span>Try another filename or language.</span><button class="button button-muted" id="clearFileSearch">Clear filter</button></div>`;$('#clearFileSearch')?.addEventListener('click',()=>{$('#fileSearch').value='';renderFiles()})}
let graphZoom=1,selectedGraphNode='',graphMode='structure',graphLookup=new Map();
function graphLayout(nodes,edges){const w=1000,h=560,cx=w/2,cy=h/2,byId=new Map(nodes.map(node=>[node.id,node])),points=nodes.map(node=>({...node,x:cx,y:cy})),pointByPath=new Map(points.map(point=>[point.id,point])),indexByPath=new Map(points.map((point,index)=>[point.id,index])),treeEdges=edges.filter(edge=>edge.kind==='contains'),children=new Map();for(const edge of treeEdges){if(!children.has(edge.source))children.set(edge.source,[]);children.get(edge.source).push(edge.target);}const visited=new Set();function place(parentId,x,y,depth){const list=(children.get(parentId)||[]).map(id=>byId.get(id)).filter(Boolean).sort((a,b)=>a.kind===b.kind?a.label.localeCompare(b.label):a.kind==='folder'?-1:1);if(!list.length)return;const radius=(depth===0?104:74)+Math.sqrt(list.length)*13;list.forEach((node,index)=>{let hash=2166136261;for(const char of node.id)hash=Math.imul(hash^char.charCodeAt(0),16777619);const angle=(index/list.length)*Math.PI*2+(hash>>>0)/4294967295*.3-Math.PI/2,point=pointByPath.get(node.id);point.x=x+Math.cos(angle)*radius;point.y=y+Math.sin(angle)*radius;visited.add(node.id);if(node.kind==='folder')place(node.id,point.x,point.y,depth+1);});}if(byId.has('folder://root')){visited.add('folder://root');place('folder://root',cx,cy,0);}const loose=points.filter(point=>!visited.has(point.id));loose.forEach((point,index)=>{const angle=index/Math.max(1,loose.length)*Math.PI*2;point.x=cx+Math.cos(angle)*125;point.y=cy+Math.sin(angle)*125;});const links=[...new Map(edges.filter(edge=>pointByPath.has(edge.source)&&pointByPath.has(edge.target)).map(edge=>[`${edge.kind}\0${edge.source}\0${edge.target}`,edge])).values()];for(let step=0;step<48;step++){const fx=new Float64Array(points.length),fy=new Float64Array(points.length);for(let i=0;i<points.length;i++){const a=points[i];fx[i]+=(cx-a.x)*.003;fy[i]+=(cy-a.y)*.003;for(let j=i+1;j<points.length;j++){const b=points[j],dx=a.x-b.x,dy=a.y-b.y,d2=Math.max(dx*dx+dy*dy,100),force=Math.min(1.4,360/d2);fx[i]+=dx*force;fy[i]+=dy*force;fx[j]-=dx*force;fy[j]-=dy*force;}}for(const edge of links){const ai=indexByPath.get(edge.source),bi=indexByPath.get(edge.target),a=points[ai],b=points[bi],dx=b.x-a.x,dy=b.y-a.y,d=Math.max(1,Math.hypot(dx,dy)),goal=edge.kind==='contains'?90:155,force=(d-goal)*.002;fx[ai]+=dx/d*force;fy[ai]+=dy/d*force;fx[bi]-=dx/d*force;fy[bi]-=dy/d*force;}for(let i=0;i<points.length;i++){points[i].x=Math.max(26,Math.min(w-26,points[i].x+fx[i]));points[i].y=Math.max(30,Math.min(h-30,points[i].y+fy[i]));}}return{w,h,points,pointByPath}}
function renderGraph(){if(!analysis)return;const query=$('#graphSearch').value.trim().toLowerCase(),all=analysis.files,matched=all.filter(file=>!query||file.path.toLowerCase().includes(query)||file.language.toLowerCase().includes(query)),visibleFiles=matched.slice(0,220),model=buildGraphModel(visibleFiles,analysis.edges,analysis.projectName);let nodes=model.nodes,edges=model.edges;if(graphMode==='imports'){nodes=nodes.filter(node=>node.kind==='file');edges=edges.filter(edge=>edge.kind==='imports');}graphLookup=new Map(nodes.map(node=>[node.id,node]));$('#graphCount').textContent=`${visibleFiles.length}/${all.length} FILES · ${analysis.edges.length} IMPORTS`;$('#graphNote').textContent=matched.length>visibleFiles.length?`Showing ${visibleFiles.length} of ${matched.length} matching files. Structure links show actual folders; import links show source references only.`:`${graphMode==='structure'?'Folder rings group files by their real paths. ':''}Animated import trails show detected references only; no code relationship is invented.`;const host=$('#graphCanvas');if(!visibleFiles.length){host.className='graph-empty';host.textContent=query?'No files match that search.':'No readable project files to map.';return;}host.className='graph-canvas';const{w,h,points,pointByPath}=graphLayout(nodes,edges),zoomW=w/graphZoom,zoomH=h/graphZoom,view=`${(w-zoomW)/2} ${(h-zoomH)/2} ${zoomW} ${zoomH}`;const clusters=nodes.filter(node=>node.kind==='folder'&&node.id!=='folder://root'&&node.children.length>1).map(folder=>{const point=pointByPath.get(folder.id),radius=Math.min(205,78+Math.sqrt(folder.children.length)*24);return`<circle class="graph-cluster" cx="${point.x.toFixed(1)}" cy="${point.y.toFixed(1)}" r="${radius.toFixed(1)}"><title>${esc(folder.path)} · ${folder.fileCount} files · ${folder.code.toLocaleString()} code lines</title></circle>`}).join('');const lineEdges=edges.filter(edge=>edge.kind==='contains').slice(0,1400),importEdges=edges.filter(edge=>edge.kind==='imports').slice(0,1400);const structureHtml=lineEdges.map(edge=>{const a=pointByPath.get(edge.source),b=pointByPath.get(edge.target);return`<line class="graph-edge graph-edge-structure" x1="${a.x.toFixed(1)}" y1="${a.y.toFixed(1)}" x2="${b.x.toFixed(1)}" y2="${b.y.toFixed(1)}"><title>${esc(byPathLabel(model.nodes,edge.source))} contains ${esc(byPathLabel(model.nodes,edge.target))}</title></line>`}).join('');const importHtml=importEdges.map((edge,index)=>{const a=pointByPath.get(edge.source),b=pointByPath.get(edge.target),path=`M ${a.x.toFixed(1)} ${a.y.toFixed(1)} L ${b.x.toFixed(1)} ${b.y.toFixed(1)}`;return`<line class="graph-edge graph-edge-import" x1="${a.x.toFixed(1)}" y1="${a.y.toFixed(1)}" x2="${b.x.toFixed(1)}" y2="${b.y.toFixed(1)}"><title>${esc(edge.source.slice(5))} imports ${esc(edge.target.slice(5))} · line ${edge.line}</title></line>${index<180?`<circle class="graph-particle" r="2.4"><animateMotion dur="${4+index%5}s" repeatCount="indefinite" path="${path}"/></circle>`:''}`}).join('');const nodeHtml=points.map(node=>{const isFolder=node.kind==='folder',file=node.file,r=isFolder?12:Math.max(7,Math.min(14,7+Math.log10((file?.metrics?.code||1)+1)*1.5)),selected=node.id===selectedGraphNode?' selected':'',language=(node.language||'folder').toLowerCase().replace(/[^a-z0-9]+/g,'-'),name=node.label.length>30?node.label.slice(0,27)+'…':node.label;return`<g class="graph-node${selected}" tabindex="0" role="button" aria-label="Inspect ${esc(node.path||node.label)}" data-node="${esc(node.id)}" data-kind="${node.kind}" data-language="${language}"><circle class="node-halo" cx="${node.x.toFixed(1)}" cy="${node.y.toFixed(1)}" r="${(r+5).toFixed(1)}"/><circle class="node-core" cx="${node.x.toFixed(1)}" cy="${node.y.toFixed(1)}" r="${r.toFixed(1)}"/><text class="node-glyph" x="${node.x.toFixed(1)}" y="${(node.y+3).toFixed(1)}">${isFolder?'⌂':''}</text><text class="node-label" x="${node.x.toFixed(1)}" y="${(node.y+r+16).toFixed(1)}">${esc(name)}</text><title>${esc(node.path||node.label)}${isFolder?` · ${node.fileCount} files`:` · ${file.metrics.lines.toLocaleString()} lines · ${file.language}`}</title></g>`}).join('');host.innerHTML=`<svg class="graph-svg" viewBox="${view}" role="img" aria-label="Interactive project structure and import graph">${clusters}${structureHtml}${importHtml}${nodeHtml}</svg>`;host.querySelectorAll('.graph-node').forEach(el=>{const activate=()=>selectGraphNode(el.dataset.node);el.addEventListener('click',activate);el.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();activate();}});});}
function byPathLabel(nodes,id){return nodes.find(node=>node.id===id)?.path||nodes.find(node=>node.id===id)?.label||id}
function selectGraphNode(id){selectedGraphNode=id;const node=graphLookup.get(id);if(!node)return;document.querySelectorAll('.graph-node').forEach(el=>el.classList.toggle('selected',el.dataset.node===id));if(node.kind==='folder'){const children=node.children.map(child=>graphLookup.get(child)).filter(Boolean),files=children.filter(child=>child.kind==='file'),folders=children.filter(child=>child.kind==='folder');$('#nodeDetail').innerHTML=`<div class="detail-file-icon folder-icon">DIR</div><h3>${esc(node.path||analysis.projectName)}</h3><p class="detail-subtitle">Folder · ${node.fileCount} files across this directory</p><div class="detail-stats"><div><b>${node.fileCount}</b><small>FILES</small></div><div><b>${node.lines.toLocaleString()}</b><small>LINES</small></div><div><b>${node.code.toLocaleString()}</b><small>CODE</small></div><div><b>${formatBytes(node.size)}</b><small>SIZE</small></div></div><div class="detail-section"><b>Languages <small>FROM FILE EVIDENCE</small></b><p>${[...node.languages].map(esc).join(' · ')||'No language evidence'}</p></div><div class="detail-section"><b>Direct contents</b><ul>${[...folders.map(child=>`<li>▱ ${esc(child.label)} <small>folder · ${child.fileCount} files</small></li>`),...files.map(child=>`<li>○ ${esc(child.label)} <small>${esc(child.language)} · ${child.metrics.lines} lines</small></li>`)].slice(0,50).join('')}</ul></div>`;return;}const file=node.file;if(!file)return;const path=file.path,incoming=analysis.edges.filter(edge=>edge.target===path),outgoing=analysis.edges.filter(edge=>edge.source===path),symbols=file.symbols||[];$('#nodeDetail').innerHTML=`<div class="detail-file-icon">${esc((file.ext||'?').toUpperCase())}</div><h3>${esc(path)}</h3><p class="detail-subtitle">${esc(file.language||'Unknown language')}${file.metadataOnly?' · streamed large file':''}</p><div class="detail-stats"><div><b>${file.metrics.lines.toLocaleString()}</b><small>LINES</small></div><div><b>${file.metrics.code.toLocaleString()}</b><small>CODE</small></div><div><b>${formatBytes(file.size)}</b><small>SIZE</small></div><div><b>${symbols.length}${symbols.length>=600?'+':''}</b><small>DECLARATIONS</small></div></div><div class="detail-count">${outgoing.length} outgoing imports <span>·</span> ${incoming.length} modules depend on this file</div><div class="detail-section"><b>Imports</b>${outgoing.length?`<ul>${outgoing.map(edge=>`<li>${esc(edge.target)} <small>line ${edge.line}</small></li>`).join('')}</ul>`:'<p>No resolved local imports detected.</p>'}</div><div class="detail-section"><b>Imported by</b>${incoming.length?`<ul>${incoming.map(edge=>`<li>${esc(edge.source)}</li>`).join('')}</ul>`:'<p>No incoming imports detected.</p>'}</div><div class="detail-section"><b>Detected declarations <small>STATIC SOURCE MATCHES</small></b>${symbols.length?`<ul class="symbol-list">${symbols.slice(0,45).map(symbol=>`<li><span class="symbol-kind">${esc(symbol.kind)}</span> ${esc(symbol.name)} <small>line ${symbol.line}</small></li>`).join('')}</ul>${symbols.length>45?`<p>${symbols.length-45} more declarations in the analyzed file.</p>`:''}`:'<p>No supported declaration patterns found in this source file.</p>'}</div>`;}
function navigate(page){document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));document.getElementById(page+'Page').classList.add('active');document.querySelectorAll('.nav-link').forEach(a=>a.classList.toggle('active',a.dataset.page===page));document.querySelectorAll('.workspace-tab').forEach(a=>{const active=a.dataset.page===page;a.classList.toggle('active',active);a.setAttribute('aria-selected',String(active))});window.scrollTo({top:0,behavior:'smooth'})}
document.querySelectorAll('[data-page]').forEach(a=>a.addEventListener('click',e=>{e.preventDefault();navigate(a.dataset.page)}));
document.querySelectorAll('[data-upload],#heroUpload,#openButton').forEach(el=>el.addEventListener('click',()=>openPicker(zipInput,'ZIP archive')));
$('#folderUpload')?.addEventListener('click',()=>openPicker($('#folderInput'),'project folder'));
$('#anatomyFolderUpload')?.addEventListener('click',()=>openPicker($('#folderInput'),'project folder'));
$('#filePanelFolderUpload')?.addEventListener('click',()=>openPicker($('#folderInput'),'project folder'));
$('#filesUpload')?.addEventListener('click',()=>openPicker($('#sourceFilesInput'),'source files'));
zipInput.addEventListener('cancel',()=>pickerCancelled(zipInput));
$('#folderInput').addEventListener('cancel',e=>pickerCancelled(e.target));
$('#sourceFilesInput').addEventListener('cancel',e=>pickerCancelled(e.target));
zipInput.addEventListener('change',e=>{const file=e.target.files[0];pendingPicker=null;e.target.value='';e.target.blur();if(file)analyze(file);else{hideIntake();toast('ZIP seçilmedi.',true)}});
$('#folderInput').addEventListener('change',e=>{const input=e.target,files=[...input.files];pendingPicker=null;input.value='';input.blur();if(files.length)analyzeLocal(files);else{hideIntake();toast('Klasörde okunabilir dosya bulunamadı.',true)}});
$('#sourceFilesInput').addEventListener('change',e=>{const input=e.target,files=[...input.files];pendingPicker=null;input.value='';input.blur();if(files.length)analyzeLocal(files);else{hideIntake();toast('Dosya seçilmedi.',true)}});
$('#fileSearch').addEventListener('input',()=>analysis&&renderFiles());
$('#graphSearch').addEventListener('input',()=>analysis&&renderGraph());
$('#graphStructure').addEventListener('click',()=>setGraphMode('structure'));
$('#graphImports').addEventListener('click',()=>setGraphMode('imports'));
function setGraphMode(mode){graphMode=mode;for(const [id,value] of [['graphStructure','structure'],['graphImports','imports']]){const active=mode===value;$('#'+id).classList.toggle('active',active);$('#'+id).setAttribute('aria-pressed',String(active));}renderGraph()}
$('#zoomIn').addEventListener('click',()=>{graphZoom=Math.min(2.5,graphZoom*1.2);renderGraph()});
$('#zoomOut').addEventListener('click',()=>{graphZoom=Math.max(.65,graphZoom/1.2);renderGraph()});
$('#zoomReset').addEventListener('click',()=>{graphZoom=1;renderGraph()});
let dragDepth=0;
const isFileDrag=e=>Array.from(e.dataTransfer?.types||[]).includes('Files');
async function collectDroppedFiles(transfer){
const entries=Array.from(transfer.items||[]).map(item=>item.webkitGetAsEntry?.()).filter(Boolean);
if(!entries.some(entry=>entry.isDirectory))return Array.from(transfer.files||[]);
const files=[],queue=entries.map(entry=>({entry,path:entry.name,depth:0}));let visited=0;
while(queue.length){
const current=queue.pop(),entry=current.entry;
if(++visited>PROJECT_LIMITS.files)throw new Error('Dropped folder contains too many files or folders to inspect.');
if(current.depth>100)throw new Error('Dropped folder nesting is too deep to inspect safely.');
if(entry.isDirectory&&IGNORED_DIRS.has(entry.name.toLowerCase()))continue;
if(entry.isFile){
const file=await new Promise((resolve,reject)=>entry.file(resolve,reject));
try{Object.defineProperty(file,'webkitRelativePath',{value:current.path})}catch{}
files.push(file);
}else if(entry.isDirectory){
const reader=entry.createReader();
while(true){
const batch=await new Promise((resolve,reject)=>reader.readEntries(resolve,reject));
if(!batch.length)break;
for(const child of batch)queue.push({entry:child,path:current.path+'/'+child.name,depth:current.depth+1});
}
}
if(visited===1||visited%50===0||!queue.length)setIntake(Math.min(1,visited/PROJECT_LIMITS.files),'Klasör listeleniyor',entry.name,files.length+' dosya bulundu');
}
return files;
}
document.addEventListener('dragenter',e=>{if(!isFileDrag(e))return;e.preventDefault();dragDepth++;document.body.classList.add('dragging')});
document.addEventListener('dragover',e=>{if(isFileDrag(e))e.preventDefault()});
document.addEventListener('dragleave',e=>{if(!isFileDrag(e))return;dragDepth=Math.max(0,dragDepth-1);if(!dragDepth)document.body.classList.remove('dragging')});
document.addEventListener('drop',async e=>{if(!isFileDrag(e))return;e.preventDefault();dragDepth=0;document.body.classList.remove('dragging');try{const dropped=await collectDroppedFiles(e.dataTransfer);if(!dropped.length){hideIntake();toast('Bu öğeden dosya okunamadı. ZIP, klasör veya dosya bırakmayı deneyin.',true);return}const zip=dropped.find(file=>file.name.toLowerCase().endsWith('.zip'));if(zip)analyze(zip);else analyzeLocal(dropped)}catch(error){hideIntake();toast(error.message,true)}});
function renderAnalysisNotes(){const host=$('#analysisNotes');if(!host||!analysis)return;const rows=[];if(analysis.metadataOnlyFiles)rows.push([`${analysis.metadataOnlyFiles} large file${analysis.metadataOnlyFiles===1?'':'s'} streamed`,'Line counts and import references scanned without retaining full file text']);for(const item of analysis.skippedFiles.slice(0,20))rows.push([item.path,item.reason]);host.innerHTML=rows.length?`<article class="panel"><div class="panel-title"><div><div class="eyebrow">TRANSPARENT FILE HANDLING</div><h3>Analysis notes</h3></div><span class="evidence-label">${analysis.skippedFiles.length} EXCLUDED</span></div>${anatomyRows(rows)}</article>`:''}
const renderWithNotes=render;render=()=>{renderWithNotes();renderAnalysisNotes()};
restoreAnalysisSnapshot();