wikifier 4.4.0__tar.gz → 4.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {wikifier-4.4.0/wikifier.egg-info → wikifier-4.5.1}/PKG-INFO +5 -1
  2. {wikifier-4.4.0 → wikifier-4.5.1}/README.md +4 -0
  3. {wikifier-4.4.0 → wikifier-4.5.1}/index.html +169 -35
  4. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/__init__.py +1 -1
  5. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/index.html +169 -35
  6. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/library.py +151 -1
  7. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/python.py +37 -1
  8. {wikifier-4.4.0 → wikifier-4.5.1/wikifier.egg-info}/PKG-INFO +5 -1
  9. {wikifier-4.4.0 → wikifier-4.5.1}/CONTRIBUTING.md +0 -0
  10. {wikifier-4.4.0 → wikifier-4.5.1}/LICENSE +0 -0
  11. {wikifier-4.4.0 → wikifier-4.5.1}/MANIFEST.in +0 -0
  12. {wikifier-4.4.0 → wikifier-4.5.1}/diagnostics.html +0 -0
  13. {wikifier-4.4.0 → wikifier-4.5.1}/docs/spec.md +0 -0
  14. {wikifier-4.4.0 → wikifier-4.5.1}/pyproject.toml +0 -0
  15. {wikifier-4.4.0 → wikifier-4.5.1}/setup.cfg +0 -0
  16. {wikifier-4.4.0 → wikifier-4.5.1}/skills/run.md +0 -0
  17. {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_barrel_invalidation.py +0 -0
  18. {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_health.py +0 -0
  19. {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_import_cache.py +0 -0
  20. {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_parsers.py +0 -0
  21. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/__main__.py +0 -0
  22. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/cli.py +0 -0
  23. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/contracts.py +0 -0
  24. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/daemon.py +0 -0
  25. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/diagnostics.py +0 -0
  26. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/health.py +0 -0
  27. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/import_cache.py +0 -0
  28. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/locking.py +0 -0
  29. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/mcp/__init__.py +0 -0
  30. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/mcp/server.py +0 -0
  31. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/__init__.py +0 -0
  32. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/bree.py +0 -0
  33. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/cdia.py +0 -0
  34. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/javascript.py +0 -0
  35. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/resolution.py +0 -0
  36. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/scripts/wikifier.bat +0 -0
  37. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/scripts/wikifier.ps1 +0 -0
  38. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/scripts/wikifier.sh +0 -0
  39. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/serve.py +0 -0
  40. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/SOURCES.txt +0 -0
  41. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/dependency_links.txt +0 -0
  42. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/entry_points.txt +0 -0
  43. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/requires.txt +0 -0
  44. {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wikifier
3
- Version: 4.4.0
3
+ Version: 4.5.1
4
4
  Summary: Zero-dependency agent-to-agent codebase wiki for LLMs and AI agents. Autonomously maintained via record-change and mark-green for token-efficient lookup of files, dependencies, health, and summaries — across tiny scripts to 50k+ monorepos. Optional MCP server with rich tools for agents.
5
5
  Author-email: Aron Amos <aron@example.com>
6
6
  Maintainer: Aron Amos
@@ -137,6 +137,10 @@ Works with Claude Desktop, Claude Code, Cursor, Cline, and any Model Context Pro
137
137
 
138
138
  ## The Human Layer (Secondary, by Design)
139
139
 
140
+ ![Wikifier human dashboard — the project file tree with agent-wiki descriptions, health pills, and one-click Run/Stop controls, served locally by wikifier serve](https://raw.githubusercontent.com/IronAdamant/wikifier/main/screenshot/front_page_review.png)
141
+
142
+ *The dashboard served by `wikifier serve`: live file tree with wiki descriptions, health status pills, and action buttons that run `update-maps`/`check-changes` directly (localhost-only) — plus a Stop server button for when you're done.*
143
+
140
144
  `wikifier init` copies a single static `index.html` into your project: a dashboard showing the Mermaid code-structure chart, files with short descriptions, and a folder browser — useful for humans investigating what the agents know. View it with **`wikifier serve`** (then open http://localhost:8787/index.html) — browsers block `file://` fetches, so a double-clicked index.html can't read the wiki files; the page detects that case and shows you the fix. When served by `wikifier serve`, the dashboard's buttons genuinely **run** `update-maps`/`check-changes` (whitelisted commands only, localhost-only) and a **Stop server** button shuts the process down when you're done; behind any plain static server the same buttons fall back to copy + auto-refresh. The agent-facing markdown files and tools remain the single source of truth.
141
145
 
142
146
  ## Intended Use: Strictly Agent-to-Agent
@@ -105,6 +105,10 @@ Works with Claude Desktop, Claude Code, Cursor, Cline, and any Model Context Pro
105
105
 
106
106
  ## The Human Layer (Secondary, by Design)
107
107
 
108
+ ![Wikifier human dashboard — the project file tree with agent-wiki descriptions, health pills, and one-click Run/Stop controls, served locally by wikifier serve](https://raw.githubusercontent.com/IronAdamant/wikifier/main/screenshot/front_page_review.png)
109
+
110
+ *The dashboard served by `wikifier serve`: live file tree with wiki descriptions, health status pills, and action buttons that run `update-maps`/`check-changes` directly (localhost-only) — plus a Stop server button for when you're done.*
111
+
108
112
  `wikifier init` copies a single static `index.html` into your project: a dashboard showing the Mermaid code-structure chart, files with short descriptions, and a folder browser — useful for humans investigating what the agents know. View it with **`wikifier serve`** (then open http://localhost:8787/index.html) — browsers block `file://` fetches, so a double-clicked index.html can't read the wiki files; the page detects that case and shows you the fix. When served by `wikifier serve`, the dashboard's buttons genuinely **run** `update-maps`/`check-changes` (whitelisted commands only, localhost-only) and a **Stop server** button shuts the process down when you're done; behind any plain static server the same buttons fall back to copy + auto-refresh. The agent-facing markdown files and tools remain the single source of truth.
109
113
 
110
114
  ## Intended Use: Strictly Agent-to-Agent
@@ -136,13 +136,34 @@
136
136
  .console.show { display: block; }
137
137
  .hint { font-size: 12px; color: var(--faint); margin-top: 8px; }
138
138
 
139
- /* ---------- mermaid ---------- */
140
- #map-wrap {
141
- margin-top: 12px; padding: 10px; min-height: 160px;
139
+ /* ---------- file tree (primary structure view) ---------- */
140
+ #tree-pre {
141
+ margin: 12px 0 0; padding: 14px 16px; max-height: 520px; overflow: auto;
142
142
  background: var(--panel-2); border: 1px solid var(--border-soft);
143
- border-radius: 12px; overflow: auto;
143
+ border-radius: 12px; font-family: var(--mono); font-size: 12.5px;
144
+ line-height: 1.5; color: #c4cedb; white-space: pre;
145
+ }
146
+
147
+ /* ---------- dependency graph (collapsible, pan/zoom) ---------- */
148
+ details.graph { margin-top: 14px; }
149
+ details.graph > summary {
150
+ cursor: pointer; font-size: 13px; font-weight: 600; color: var(--muted);
151
+ padding: 9px 12px; border: 1px solid var(--border); border-radius: 10px;
152
+ list-style: none; user-select: none;
153
+ }
154
+ details.graph > summary::before { content: '▸ '; color: var(--accent); }
155
+ details.graph[open] > summary::before { content: '▾ '; }
156
+ details.graph > summary:hover { background: #15202f; }
157
+ #graph-viewport {
158
+ margin-top: 10px; height: 460px; overflow: hidden; cursor: grab;
159
+ background: var(--panel-2); border: 1px solid var(--border-soft);
160
+ border-radius: 12px; position: relative;
144
161
  }
162
+ #graph-viewport.dragging { cursor: grabbing; }
163
+ #graph-canvas { transform-origin: 0 0; width: max-content; }
164
+ #map-wrap { padding: 10px; min-height: 120px; }
145
165
  #map-wrap svg { max-width: none; }
166
+ .graph-help { font-size: 11.5px; color: var(--faint); margin-top: 6px; }
146
167
  .errbox {
147
168
  font-size: 13px; padding: 14px; border: 1px solid #92600c;
148
169
  border-radius: 10px; background: #171207;
@@ -246,15 +267,23 @@
246
267
  <div class="hint" id="actions-hint"></div>
247
268
  </section>
248
269
 
249
- <!-- ============ structure map ============ -->
270
+ <!-- ============ structure: file tree (primary) + dependency graph ============ -->
250
271
  <section class="card">
251
- <h2>Code structure <span class="sub">imports &amp; dependencies, rebuilt from your code</span></h2>
252
- <div id="map-wrap"><div class="hint">loading map…</div></div>
272
+ <h2>Code structure <span class="sub">file tree with wiki descriptions — rebuilt from your code</span></h2>
273
+ <pre id="tree-pre">loading…</pre>
253
274
  <div class="btnrow" style="margin-top:12px">
254
- <button id="btn-copy-mermaid">⧉ Copy structure as text</button>
275
+ <button id="btn-copy-tree">⧉ Copy structure as text</button>
255
276
  <button id="btn-copy-snapshot">⧉ Copy full snapshot (tree + files + descriptions)</button>
256
- <span class="hint" style="margin:0">Paste directly into docs or LLM chats.</span>
277
+ <span class="hint" style="margin:0">Clean, readable text — paste directly into docs or LLM chats.</span>
257
278
  </div>
279
+ <details class="graph" id="graph-details">
280
+ <summary>Dependency graph (drag to pan · scroll to zoom · double-click to reset)</summary>
281
+ <div id="graph-viewport"><div id="graph-canvas"><div id="map-wrap"><div class="hint" style="padding:10px">expand to render…</div></div></div></div>
282
+ <div class="btnrow" style="margin-top:8px">
283
+ <button id="btn-copy-mermaid">⧉ Copy diagram source (Mermaid)</button>
284
+ <span class="graph-help">Arrows: solid = high-confidence import · dashed = medium · “?” = low/unresolved.</span>
285
+ </div>
286
+ </details>
258
287
  </section>
259
288
 
260
289
  <!-- ============ files + folders ============ -->
@@ -281,8 +310,8 @@
281
310
 
282
311
  <div id="toast"></div>
283
312
  <div id="overlay"><div class="panel">
284
- <h2>Server stopped</h2>
285
- <p>The <code>wikifier serve</code> process has shut down and this page is now frozen.
313
+ <h2 id="overlay-title">Server stopped</h2>
314
+ <p id="overlay-msg">The <code>wikifier serve</code> process has shut down and this page is now frozen.
286
315
  To come back, run <code>wikifier serve</code> in the project folder again and reload.</p>
287
316
  </div></div>
288
317
 
@@ -303,7 +332,13 @@ const state = {
303
332
  jsonMissing: false, // remember a file_health.json 404 — stop re-polling it
304
333
  running: false,
305
334
  liveWaitTimer: null,
335
+ pollTimer: null,
306
336
  lastDataSig: '',
337
+ failStreak: 0, // consecutive artifact-fetch failures (server gone?)
338
+ halted: false,
339
+ graphRendered: false, // dependency graph renders lazily on first expand
340
+ graphDirty: true,
341
+ panZoom: { scale: 1, x: 0, y: 0 },
307
342
  };
308
343
 
309
344
  const $ = id => document.getElementById(id);
@@ -316,12 +351,26 @@ async function safeFetch(path, fallbackText = '') {
316
351
  const res = await fetch(path, ctrl ? { signal: ctrl.signal, cache: 'no-store' } : { cache: 'no-store' });
317
352
  if (timer) clearTimeout(timer);
318
353
  if (!res.ok) throw new Error('HTTP ' + res.status);
354
+ state.failStreak = 0;
319
355
  return await res.text();
320
356
  } catch (e) {
357
+ // Network-level failures (server killed) count toward the halt
358
+ // threshold; HTTP errors (404 on optional files) do not.
359
+ if (!String(e.message || '').startsWith('HTTP ')) state.failStreak++;
321
360
  return fallbackText;
322
361
  }
323
362
  }
324
363
 
364
+ function haltDashboard(title, msg) {
365
+ if (state.halted) return;
366
+ state.halted = true;
367
+ if (state.pollTimer) clearInterval(state.pollTimer);
368
+ if (state.liveWaitTimer) clearInterval(state.liveWaitTimer);
369
+ $('overlay-title').textContent = title;
370
+ $('overlay-msg').innerHTML = msg;
371
+ $('overlay').classList.add('show');
372
+ }
373
+
325
374
  function toast(msg, ms = 2200) {
326
375
  const t = $('toast');
327
376
  t.textContent = msg;
@@ -383,12 +432,13 @@ async function detectServer() {
383
432
  async function stopServer() {
384
433
  if (!state.execMode) return;
385
434
  if (!confirm('Stop the wikifier serve process? The dashboard will go offline.')) return;
435
+ const done = () => haltDashboard('Server stopped',
436
+ 'The <code>wikifier serve</code> process has shut down and this page is now frozen. ' +
437
+ 'To come back, run <code>wikifier serve</code> in the project folder again and reload.');
386
438
  try {
387
439
  const res = await fetch('/__wikifier/shutdown', { method: 'POST' });
388
- if (res.ok) { $('overlay').classList.add('show'); return; }
389
- } catch (e) { /* connection may drop as it stops — that also means success */
390
- $('overlay').classList.add('show'); return;
391
- }
440
+ if (res.ok) { done(); return; }
441
+ } catch (e) { done(); return; } // connection drop while stopping = success
392
442
  toast('Could not stop the server');
393
443
  }
394
444
 
@@ -581,20 +631,35 @@ function renderFolders() {
581
631
  }).join('') || '<div class="hint">No files tracked yet — run check-changes / update-maps.</div>';
582
632
  }
583
633
 
584
- // ---------- structure map ----------------------------------------------------
585
- async function loadMap() {
586
- const wrap = $('map-wrap');
634
+ // ---------- structure: file tree (primary) + lazy dependency graph -----------
635
+ function treeText() {
636
+ const m = (state.libraryMd || '').match(/## File Tree[\s\S]*?```text\n([\s\S]*?)```/);
637
+ return m ? m[1].replace(/\s+$/, '') : '';
638
+ }
639
+
640
+ async function loadStructure() {
587
641
  const md = await safeFetch('library.md?cb=' + Date.now(), '');
588
642
  state.libraryMd = md;
643
+ const pre = $('tree-pre');
644
+ const tree = treeText();
589
645
  if (!md) {
590
- wrap.innerHTML = `<div class="errbox"><b>No structure map yet.</b><br>
591
- Click <i>Update map</i> above (or run <code>wikifier update-maps</code> in this folder), then refresh.</div>`;
592
- return;
646
+ pre.textContent = 'No structure data yet — click “Update map above (or run wikifier update-maps), then refresh.';
647
+ } else if (tree) {
648
+ pre.textContent = tree;
649
+ } else {
650
+ // Older library.md without the File Tree section: regenerating fixes it.
651
+ pre.textContent = 'This library.md predates the File Tree view — click “Update map” to regenerate it.';
593
652
  }
594
- const match = md.match(/```mermaid\n([\s\S]*?)```/);
653
+ state.graphDirty = true;
654
+ if ($('graph-details').open) await renderGraph();
655
+ }
656
+
657
+ async function renderGraph() {
658
+ if (!state.graphDirty && state.graphRendered) return;
659
+ const wrap = $('map-wrap');
660
+ const match = (state.libraryMd || '').match(/```mermaid\n([\s\S]*?)```/);
595
661
  if (!match || !match[1].trim()) {
596
- wrap.innerHTML = `<div class="errbox"><b>library.md has no diagram block.</b><br>
597
- Run <code>wikifier update-maps</code> to regenerate it.</div>`;
662
+ wrap.innerHTML = '<div class="errbox" style="margin:10px"><b>No diagram block in library.md.</b> Run “Update map” to regenerate.</div>';
598
663
  return;
599
664
  }
600
665
  // textContent (never innerHTML) so the Mermaid source can't be parsed as HTML.
@@ -609,15 +674,58 @@ async function loadMap() {
609
674
  // re-read it per render; keep both maxEdges placements).
610
675
  mermaid.initialize({ startOnLoad: false, theme: 'dark', maxEdges: 1500, flowchart: { maxEdges: 1500 } });
611
676
  await mermaid.run({ nodes: [wrap.querySelector('.mermaid')] });
677
+ state.graphRendered = true;
678
+ state.graphDirty = false;
679
+ resetPanZoom();
612
680
  } catch (e) {
613
681
  // Never fail silently: show what broke + where the intact data lives.
614
682
  const msg = (e && (e.message || e.str)) || String(e);
615
- wrap.innerHTML = `<div class="errbox"><b>Structure map could not be rendered</b><br>
683
+ wrap.innerHTML = `<div class="errbox"><b>Dependency graph could not be rendered</b><br>
616
684
  <code>${esc(String(msg)).slice(0, 300)}</code><br>
617
- <span class="hint">The underlying map data in library.md is intact — use “Copy structure as text”.</span></div>`;
685
+ <span class="hint">The underlying data in library.md is intact — the File Tree above is unaffected.</span></div>`;
618
686
  }
619
687
  }
620
688
 
689
+ // Zero-dep pan/zoom for the graph viewport: drag to pan, wheel to zoom
690
+ // toward the cursor, double-click to reset.
691
+ function applyPanZoom() {
692
+ const p = state.panZoom;
693
+ $('graph-canvas').style.transform = `translate(${p.x}px, ${p.y}px) scale(${p.scale})`;
694
+ }
695
+ function resetPanZoom() {
696
+ state.panZoom = { scale: 1, x: 0, y: 0 };
697
+ applyPanZoom();
698
+ }
699
+ function initPanZoom() {
700
+ const vp = $('graph-viewport');
701
+ let dragging = false, lastX = 0, lastY = 0;
702
+ vp.addEventListener('wheel', e => {
703
+ e.preventDefault();
704
+ const p = state.panZoom;
705
+ const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
706
+ const ns = Math.min(8, Math.max(0.1, p.scale * factor));
707
+ const rect = vp.getBoundingClientRect();
708
+ const cx = e.clientX - rect.left, cy = e.clientY - rect.top;
709
+ p.x = cx - (cx - p.x) * (ns / p.scale);
710
+ p.y = cy - (cy - p.y) * (ns / p.scale);
711
+ p.scale = ns;
712
+ applyPanZoom();
713
+ }, { passive: false });
714
+ vp.addEventListener('mousedown', e => {
715
+ dragging = true; lastX = e.clientX; lastY = e.clientY;
716
+ vp.classList.add('dragging'); e.preventDefault();
717
+ });
718
+ window.addEventListener('mousemove', e => {
719
+ if (!dragging) return;
720
+ state.panZoom.x += e.clientX - lastX;
721
+ state.panZoom.y += e.clientY - lastY;
722
+ lastX = e.clientX; lastY = e.clientY;
723
+ applyPanZoom();
724
+ });
725
+ window.addEventListener('mouseup', () => { dragging = false; vp.classList.remove('dragging'); });
726
+ vp.addEventListener('dblclick', resetPanZoom);
727
+ }
728
+
621
729
  // ---------- exports ----------------------------------------------------------
622
730
  function mermaidSource() {
623
731
  const m = (state.libraryMd || '').match(/```mermaid\n([\s\S]*?)```/);
@@ -636,12 +744,12 @@ function buildSnapshot() {
636
744
  const mark = { g: '🟢', y: '🟡', r: '🔴' }[r.status];
637
745
  lines.push(`- ${mark} ${r.file} — ${r.reason}`);
638
746
  });
639
- const src = mermaidSource();
640
- if (src) {
747
+ const tree = treeText();
748
+ if (tree) {
641
749
  lines.push('');
642
- lines.push('## Structure (Mermaid)');
643
- lines.push('```mermaid');
644
- lines.push(src);
750
+ lines.push('## Structure');
751
+ lines.push('```text');
752
+ lines.push(tree);
645
753
  lines.push('```');
646
754
  }
647
755
  return lines.join('\n');
@@ -675,7 +783,7 @@ async function loadAll() {
675
783
  renderSummary();
676
784
  renderFiles();
677
785
  renderFolders();
678
- await loadMap();
786
+ await loadStructure();
679
787
  state.lastDataSig = await dataSignature();
680
788
  }
681
789
 
@@ -683,11 +791,19 @@ function wire() {
683
791
  $('btn-refresh').onclick = () => { loadAll(); toast('Refreshed'); };
684
792
  $('btn-stop').onclick = stopServer;
685
793
  $('btn-copy-serve').onclick = e => copyText('wikifier serve', e.target);
794
+ $('btn-copy-tree').onclick = e => {
795
+ const tree = treeText();
796
+ copyText(tree || 'No structure yet — run wikifier update-maps first.', e.target);
797
+ };
686
798
  $('btn-copy-mermaid').onclick = e => {
687
799
  const src = mermaidSource();
688
- copyText(src || 'No map yet — run wikifier update-maps first.', e.target);
800
+ copyText(src || 'No diagram yet — run wikifier update-maps first.', e.target);
689
801
  };
690
802
  $('btn-copy-snapshot').onclick = e => copyText(buildSnapshot(), e.target);
803
+ $('graph-details').addEventListener('toggle', () => {
804
+ if ($('graph-details').open) renderGraph();
805
+ });
806
+ initPanZoom();
691
807
  ['btn-update', 'btn-update-full', 'btn-check'].forEach(id => {
692
808
  $(id).onclick = ev => runCommand(ev.target.dataset.cmd, ev.target);
693
809
  });
@@ -706,8 +822,26 @@ async function boot() {
706
822
  wire();
707
823
  await detectServer();
708
824
  await loadAll();
709
- // Gentle background poll so monitor/agent updates appear automatically.
710
- setInterval(() => { if (!state.running) loadAll(); }, 32000);
825
+ // Gentle background poll so monitor/agent updates appear automatically
826
+ // and halts with an overlay once the server is gone, instead of spamming
827
+ // connection-refused errors forever.
828
+ state.pollTimer = setInterval(async () => {
829
+ if (state.running || state.halted) return;
830
+ if (state.execMode) {
831
+ try { await fetch('/__wikifier/status?cb=' + Date.now(), { cache: 'no-store' }); }
832
+ catch (e) {
833
+ haltDashboard('Server offline',
834
+ 'The <code>wikifier serve</code> process is no longer reachable. ' +
835
+ 'Run <code>wikifier serve</code> in the project folder and reload this page.');
836
+ return;
837
+ }
838
+ }
839
+ await loadAll();
840
+ if (state.failStreak >= 6) {
841
+ haltDashboard('Connection lost',
842
+ 'The server stopped responding. Restart it (e.g. <code>wikifier serve</code>) and reload this page.');
843
+ }
844
+ }, 32000);
711
845
  }
712
846
 
713
847
  window.onload = boot;
@@ -61,4 +61,4 @@ from .contracts import (
61
61
  compute_acs_confidence,
62
62
  )
63
63
 
64
- __version__ = "4.4.0"
64
+ __version__ = "4.5.1"
@@ -136,13 +136,34 @@
136
136
  .console.show { display: block; }
137
137
  .hint { font-size: 12px; color: var(--faint); margin-top: 8px; }
138
138
 
139
- /* ---------- mermaid ---------- */
140
- #map-wrap {
141
- margin-top: 12px; padding: 10px; min-height: 160px;
139
+ /* ---------- file tree (primary structure view) ---------- */
140
+ #tree-pre {
141
+ margin: 12px 0 0; padding: 14px 16px; max-height: 520px; overflow: auto;
142
142
  background: var(--panel-2); border: 1px solid var(--border-soft);
143
- border-radius: 12px; overflow: auto;
143
+ border-radius: 12px; font-family: var(--mono); font-size: 12.5px;
144
+ line-height: 1.5; color: #c4cedb; white-space: pre;
145
+ }
146
+
147
+ /* ---------- dependency graph (collapsible, pan/zoom) ---------- */
148
+ details.graph { margin-top: 14px; }
149
+ details.graph > summary {
150
+ cursor: pointer; font-size: 13px; font-weight: 600; color: var(--muted);
151
+ padding: 9px 12px; border: 1px solid var(--border); border-radius: 10px;
152
+ list-style: none; user-select: none;
153
+ }
154
+ details.graph > summary::before { content: '▸ '; color: var(--accent); }
155
+ details.graph[open] > summary::before { content: '▾ '; }
156
+ details.graph > summary:hover { background: #15202f; }
157
+ #graph-viewport {
158
+ margin-top: 10px; height: 460px; overflow: hidden; cursor: grab;
159
+ background: var(--panel-2); border: 1px solid var(--border-soft);
160
+ border-radius: 12px; position: relative;
144
161
  }
162
+ #graph-viewport.dragging { cursor: grabbing; }
163
+ #graph-canvas { transform-origin: 0 0; width: max-content; }
164
+ #map-wrap { padding: 10px; min-height: 120px; }
145
165
  #map-wrap svg { max-width: none; }
166
+ .graph-help { font-size: 11.5px; color: var(--faint); margin-top: 6px; }
146
167
  .errbox {
147
168
  font-size: 13px; padding: 14px; border: 1px solid #92600c;
148
169
  border-radius: 10px; background: #171207;
@@ -246,15 +267,23 @@
246
267
  <div class="hint" id="actions-hint"></div>
247
268
  </section>
248
269
 
249
- <!-- ============ structure map ============ -->
270
+ <!-- ============ structure: file tree (primary) + dependency graph ============ -->
250
271
  <section class="card">
251
- <h2>Code structure <span class="sub">imports &amp; dependencies, rebuilt from your code</span></h2>
252
- <div id="map-wrap"><div class="hint">loading map…</div></div>
272
+ <h2>Code structure <span class="sub">file tree with wiki descriptions — rebuilt from your code</span></h2>
273
+ <pre id="tree-pre">loading…</pre>
253
274
  <div class="btnrow" style="margin-top:12px">
254
- <button id="btn-copy-mermaid">⧉ Copy structure as text</button>
275
+ <button id="btn-copy-tree">⧉ Copy structure as text</button>
255
276
  <button id="btn-copy-snapshot">⧉ Copy full snapshot (tree + files + descriptions)</button>
256
- <span class="hint" style="margin:0">Paste directly into docs or LLM chats.</span>
277
+ <span class="hint" style="margin:0">Clean, readable text — paste directly into docs or LLM chats.</span>
257
278
  </div>
279
+ <details class="graph" id="graph-details">
280
+ <summary>Dependency graph (drag to pan · scroll to zoom · double-click to reset)</summary>
281
+ <div id="graph-viewport"><div id="graph-canvas"><div id="map-wrap"><div class="hint" style="padding:10px">expand to render…</div></div></div></div>
282
+ <div class="btnrow" style="margin-top:8px">
283
+ <button id="btn-copy-mermaid">⧉ Copy diagram source (Mermaid)</button>
284
+ <span class="graph-help">Arrows: solid = high-confidence import · dashed = medium · “?” = low/unresolved.</span>
285
+ </div>
286
+ </details>
258
287
  </section>
259
288
 
260
289
  <!-- ============ files + folders ============ -->
@@ -281,8 +310,8 @@
281
310
 
282
311
  <div id="toast"></div>
283
312
  <div id="overlay"><div class="panel">
284
- <h2>Server stopped</h2>
285
- <p>The <code>wikifier serve</code> process has shut down and this page is now frozen.
313
+ <h2 id="overlay-title">Server stopped</h2>
314
+ <p id="overlay-msg">The <code>wikifier serve</code> process has shut down and this page is now frozen.
286
315
  To come back, run <code>wikifier serve</code> in the project folder again and reload.</p>
287
316
  </div></div>
288
317
 
@@ -303,7 +332,13 @@ const state = {
303
332
  jsonMissing: false, // remember a file_health.json 404 — stop re-polling it
304
333
  running: false,
305
334
  liveWaitTimer: null,
335
+ pollTimer: null,
306
336
  lastDataSig: '',
337
+ failStreak: 0, // consecutive artifact-fetch failures (server gone?)
338
+ halted: false,
339
+ graphRendered: false, // dependency graph renders lazily on first expand
340
+ graphDirty: true,
341
+ panZoom: { scale: 1, x: 0, y: 0 },
307
342
  };
308
343
 
309
344
  const $ = id => document.getElementById(id);
@@ -316,12 +351,26 @@ async function safeFetch(path, fallbackText = '') {
316
351
  const res = await fetch(path, ctrl ? { signal: ctrl.signal, cache: 'no-store' } : { cache: 'no-store' });
317
352
  if (timer) clearTimeout(timer);
318
353
  if (!res.ok) throw new Error('HTTP ' + res.status);
354
+ state.failStreak = 0;
319
355
  return await res.text();
320
356
  } catch (e) {
357
+ // Network-level failures (server killed) count toward the halt
358
+ // threshold; HTTP errors (404 on optional files) do not.
359
+ if (!String(e.message || '').startsWith('HTTP ')) state.failStreak++;
321
360
  return fallbackText;
322
361
  }
323
362
  }
324
363
 
364
+ function haltDashboard(title, msg) {
365
+ if (state.halted) return;
366
+ state.halted = true;
367
+ if (state.pollTimer) clearInterval(state.pollTimer);
368
+ if (state.liveWaitTimer) clearInterval(state.liveWaitTimer);
369
+ $('overlay-title').textContent = title;
370
+ $('overlay-msg').innerHTML = msg;
371
+ $('overlay').classList.add('show');
372
+ }
373
+
325
374
  function toast(msg, ms = 2200) {
326
375
  const t = $('toast');
327
376
  t.textContent = msg;
@@ -383,12 +432,13 @@ async function detectServer() {
383
432
  async function stopServer() {
384
433
  if (!state.execMode) return;
385
434
  if (!confirm('Stop the wikifier serve process? The dashboard will go offline.')) return;
435
+ const done = () => haltDashboard('Server stopped',
436
+ 'The <code>wikifier serve</code> process has shut down and this page is now frozen. ' +
437
+ 'To come back, run <code>wikifier serve</code> in the project folder again and reload.');
386
438
  try {
387
439
  const res = await fetch('/__wikifier/shutdown', { method: 'POST' });
388
- if (res.ok) { $('overlay').classList.add('show'); return; }
389
- } catch (e) { /* connection may drop as it stops — that also means success */
390
- $('overlay').classList.add('show'); return;
391
- }
440
+ if (res.ok) { done(); return; }
441
+ } catch (e) { done(); return; } // connection drop while stopping = success
392
442
  toast('Could not stop the server');
393
443
  }
394
444
 
@@ -581,20 +631,35 @@ function renderFolders() {
581
631
  }).join('') || '<div class="hint">No files tracked yet — run check-changes / update-maps.</div>';
582
632
  }
583
633
 
584
- // ---------- structure map ----------------------------------------------------
585
- async function loadMap() {
586
- const wrap = $('map-wrap');
634
+ // ---------- structure: file tree (primary) + lazy dependency graph -----------
635
+ function treeText() {
636
+ const m = (state.libraryMd || '').match(/## File Tree[\s\S]*?```text\n([\s\S]*?)```/);
637
+ return m ? m[1].replace(/\s+$/, '') : '';
638
+ }
639
+
640
+ async function loadStructure() {
587
641
  const md = await safeFetch('library.md?cb=' + Date.now(), '');
588
642
  state.libraryMd = md;
643
+ const pre = $('tree-pre');
644
+ const tree = treeText();
589
645
  if (!md) {
590
- wrap.innerHTML = `<div class="errbox"><b>No structure map yet.</b><br>
591
- Click <i>Update map</i> above (or run <code>wikifier update-maps</code> in this folder), then refresh.</div>`;
592
- return;
646
+ pre.textContent = 'No structure data yet — click “Update map above (or run wikifier update-maps), then refresh.';
647
+ } else if (tree) {
648
+ pre.textContent = tree;
649
+ } else {
650
+ // Older library.md without the File Tree section: regenerating fixes it.
651
+ pre.textContent = 'This library.md predates the File Tree view — click “Update map” to regenerate it.';
593
652
  }
594
- const match = md.match(/```mermaid\n([\s\S]*?)```/);
653
+ state.graphDirty = true;
654
+ if ($('graph-details').open) await renderGraph();
655
+ }
656
+
657
+ async function renderGraph() {
658
+ if (!state.graphDirty && state.graphRendered) return;
659
+ const wrap = $('map-wrap');
660
+ const match = (state.libraryMd || '').match(/```mermaid\n([\s\S]*?)```/);
595
661
  if (!match || !match[1].trim()) {
596
- wrap.innerHTML = `<div class="errbox"><b>library.md has no diagram block.</b><br>
597
- Run <code>wikifier update-maps</code> to regenerate it.</div>`;
662
+ wrap.innerHTML = '<div class="errbox" style="margin:10px"><b>No diagram block in library.md.</b> Run “Update map” to regenerate.</div>';
598
663
  return;
599
664
  }
600
665
  // textContent (never innerHTML) so the Mermaid source can't be parsed as HTML.
@@ -609,15 +674,58 @@ async function loadMap() {
609
674
  // re-read it per render; keep both maxEdges placements).
610
675
  mermaid.initialize({ startOnLoad: false, theme: 'dark', maxEdges: 1500, flowchart: { maxEdges: 1500 } });
611
676
  await mermaid.run({ nodes: [wrap.querySelector('.mermaid')] });
677
+ state.graphRendered = true;
678
+ state.graphDirty = false;
679
+ resetPanZoom();
612
680
  } catch (e) {
613
681
  // Never fail silently: show what broke + where the intact data lives.
614
682
  const msg = (e && (e.message || e.str)) || String(e);
615
- wrap.innerHTML = `<div class="errbox"><b>Structure map could not be rendered</b><br>
683
+ wrap.innerHTML = `<div class="errbox"><b>Dependency graph could not be rendered</b><br>
616
684
  <code>${esc(String(msg)).slice(0, 300)}</code><br>
617
- <span class="hint">The underlying map data in library.md is intact — use “Copy structure as text”.</span></div>`;
685
+ <span class="hint">The underlying data in library.md is intact — the File Tree above is unaffected.</span></div>`;
618
686
  }
619
687
  }
620
688
 
689
+ // Zero-dep pan/zoom for the graph viewport: drag to pan, wheel to zoom
690
+ // toward the cursor, double-click to reset.
691
+ function applyPanZoom() {
692
+ const p = state.panZoom;
693
+ $('graph-canvas').style.transform = `translate(${p.x}px, ${p.y}px) scale(${p.scale})`;
694
+ }
695
+ function resetPanZoom() {
696
+ state.panZoom = { scale: 1, x: 0, y: 0 };
697
+ applyPanZoom();
698
+ }
699
+ function initPanZoom() {
700
+ const vp = $('graph-viewport');
701
+ let dragging = false, lastX = 0, lastY = 0;
702
+ vp.addEventListener('wheel', e => {
703
+ e.preventDefault();
704
+ const p = state.panZoom;
705
+ const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
706
+ const ns = Math.min(8, Math.max(0.1, p.scale * factor));
707
+ const rect = vp.getBoundingClientRect();
708
+ const cx = e.clientX - rect.left, cy = e.clientY - rect.top;
709
+ p.x = cx - (cx - p.x) * (ns / p.scale);
710
+ p.y = cy - (cy - p.y) * (ns / p.scale);
711
+ p.scale = ns;
712
+ applyPanZoom();
713
+ }, { passive: false });
714
+ vp.addEventListener('mousedown', e => {
715
+ dragging = true; lastX = e.clientX; lastY = e.clientY;
716
+ vp.classList.add('dragging'); e.preventDefault();
717
+ });
718
+ window.addEventListener('mousemove', e => {
719
+ if (!dragging) return;
720
+ state.panZoom.x += e.clientX - lastX;
721
+ state.panZoom.y += e.clientY - lastY;
722
+ lastX = e.clientX; lastY = e.clientY;
723
+ applyPanZoom();
724
+ });
725
+ window.addEventListener('mouseup', () => { dragging = false; vp.classList.remove('dragging'); });
726
+ vp.addEventListener('dblclick', resetPanZoom);
727
+ }
728
+
621
729
  // ---------- exports ----------------------------------------------------------
622
730
  function mermaidSource() {
623
731
  const m = (state.libraryMd || '').match(/```mermaid\n([\s\S]*?)```/);
@@ -636,12 +744,12 @@ function buildSnapshot() {
636
744
  const mark = { g: '🟢', y: '🟡', r: '🔴' }[r.status];
637
745
  lines.push(`- ${mark} ${r.file} — ${r.reason}`);
638
746
  });
639
- const src = mermaidSource();
640
- if (src) {
747
+ const tree = treeText();
748
+ if (tree) {
641
749
  lines.push('');
642
- lines.push('## Structure (Mermaid)');
643
- lines.push('```mermaid');
644
- lines.push(src);
750
+ lines.push('## Structure');
751
+ lines.push('```text');
752
+ lines.push(tree);
645
753
  lines.push('```');
646
754
  }
647
755
  return lines.join('\n');
@@ -675,7 +783,7 @@ async function loadAll() {
675
783
  renderSummary();
676
784
  renderFiles();
677
785
  renderFolders();
678
- await loadMap();
786
+ await loadStructure();
679
787
  state.lastDataSig = await dataSignature();
680
788
  }
681
789
 
@@ -683,11 +791,19 @@ function wire() {
683
791
  $('btn-refresh').onclick = () => { loadAll(); toast('Refreshed'); };
684
792
  $('btn-stop').onclick = stopServer;
685
793
  $('btn-copy-serve').onclick = e => copyText('wikifier serve', e.target);
794
+ $('btn-copy-tree').onclick = e => {
795
+ const tree = treeText();
796
+ copyText(tree || 'No structure yet — run wikifier update-maps first.', e.target);
797
+ };
686
798
  $('btn-copy-mermaid').onclick = e => {
687
799
  const src = mermaidSource();
688
- copyText(src || 'No map yet — run wikifier update-maps first.', e.target);
800
+ copyText(src || 'No diagram yet — run wikifier update-maps first.', e.target);
689
801
  };
690
802
  $('btn-copy-snapshot').onclick = e => copyText(buildSnapshot(), e.target);
803
+ $('graph-details').addEventListener('toggle', () => {
804
+ if ($('graph-details').open) renderGraph();
805
+ });
806
+ initPanZoom();
691
807
  ['btn-update', 'btn-update-full', 'btn-check'].forEach(id => {
692
808
  $(id).onclick = ev => runCommand(ev.target.dataset.cmd, ev.target);
693
809
  });
@@ -706,8 +822,26 @@ async function boot() {
706
822
  wire();
707
823
  await detectServer();
708
824
  await loadAll();
709
- // Gentle background poll so monitor/agent updates appear automatically.
710
- setInterval(() => { if (!state.running) loadAll(); }, 32000);
825
+ // Gentle background poll so monitor/agent updates appear automatically
826
+ // and halts with an overlay once the server is gone, instead of spamming
827
+ // connection-refused errors forever.
828
+ state.pollTimer = setInterval(async () => {
829
+ if (state.running || state.halted) return;
830
+ if (state.execMode) {
831
+ try { await fetch('/__wikifier/status?cb=' + Date.now(), { cache: 'no-store' }); }
832
+ catch (e) {
833
+ haltDashboard('Server offline',
834
+ 'The <code>wikifier serve</code> process is no longer reachable. ' +
835
+ 'Run <code>wikifier serve</code> in the project folder and reload this page.');
836
+ return;
837
+ }
838
+ }
839
+ await loadAll();
840
+ if (state.failStreak >= 6) {
841
+ haltDashboard('Connection lost',
842
+ 'The server stopped responding. Restart it (e.g. <code>wikifier serve</code>) and reload this page.');
843
+ }
844
+ }, 32000);
711
845
  }
712
846
 
713
847
  window.onload = boot;
@@ -27,6 +27,7 @@ table (500-row cap), circular dependencies, ACS risk snapshot, reverse
27
27
  dependencies, barrel expansions, and conditional/dynamic intelligence.
28
28
  """
29
29
 
30
+ import json
30
31
  import os
31
32
  import re
32
33
  from pathlib import Path
@@ -83,6 +84,10 @@ def _collect_edges(cache: Dict[str, Any]) -> List[Tuple[str, str, str, str, bool
83
84
  resolved = str(pair.get("resolved") or "").strip()
84
85
  if not raw and not resolved:
85
86
  continue
87
+ if "\n" in raw or "\n" in resolved:
88
+ # Parser artifact (e.g. comment/doc text captured as a dynamic
89
+ # specifier) — never let multi-line "modules" become graph nodes.
90
+ continue
86
91
  internal = bool(resolved)
87
92
  target = resolved if internal else raw
88
93
  conf = _pair_confidence(pair)
@@ -108,7 +113,10 @@ def _sanitize_node_id(name: str) -> str:
108
113
 
109
114
  def _node_label(path: str) -> str:
110
115
  base = os.path.basename(path.rstrip("/")) or path
111
- return base.replace('"', "'")
116
+ # Labels must never carry newlines/quotes/brackets — garbage raw modules
117
+ # once leaked multi-line comment text into the rendered graph.
118
+ base = re.sub(r"\s+", " ", base).replace('"', "'").replace("[", "(").replace("]", ")")
119
+ return (base[:48] + "…") if len(base) > 49 else base
112
120
 
113
121
 
114
122
  def _top_level_group(path: str) -> str:
@@ -470,6 +478,147 @@ def _generate_conditional_dynamic_section(cache: Dict[str, Any]) -> List[str]:
470
478
  # Public API
471
479
  # ---------------------------------------------------------------------------
472
480
 
481
+ # ---------------------------------------------------------------------------
482
+ # File tree (the primary human/agent view of project shape)
483
+ # ---------------------------------------------------------------------------
484
+
485
+ def _clean_reason(reason: str) -> Optional[str]:
486
+ """Normalise a health reason into a short description, or None for
487
+ auto-yellow boilerplate (it describes the tracking system, not the file)."""
488
+ reason = re.sub(r"\s+", " ", str(reason or "")).strip()
489
+ if not reason or reason.startswith("mtime changed") or "auto-detected" in reason:
490
+ return None
491
+ return (reason[:90] + "…") if len(reason) > 91 else reason
492
+
493
+
494
+ def _load_health_descriptions(root: Path) -> Dict[str, str]:
495
+ """file -> short description, from file_health.json or file_health.md.
496
+
497
+ Projects maintained through the shell workflow only have the .md table
498
+ (the JSON is written by the Python health module), so both are read.
499
+ """
500
+ desc: Dict[str, str] = {}
501
+ try:
502
+ with open(root / "file_health.json", "r", encoding="utf-8") as f:
503
+ data = json.load(f)
504
+ entries = data.get("entries", data) if isinstance(data, dict) else {}
505
+ for file, e in entries.items():
506
+ r = _clean_reason((e or {}).get("reason"))
507
+ if r:
508
+ desc[str(file)] = r
509
+ except Exception:
510
+ pass
511
+ if desc:
512
+ return desc
513
+ try: # .md fallback: | File | Status | Last Updated | Reason / Intent |
514
+ with open(root / "file_health.md", "r", encoding="utf-8") as f:
515
+ for line in f:
516
+ line = line.strip()
517
+ if not line.startswith("|") or line.startswith("|--") or "| File | Status" in line:
518
+ continue
519
+ parts = [p.strip() for p in line.split("|")]
520
+ if len(parts) >= 6 and parts[1]:
521
+ r = _clean_reason(parts[4])
522
+ if r:
523
+ desc[parts[1]] = r
524
+ except Exception:
525
+ pass
526
+ return desc
527
+
528
+
529
+ TREE_DIR_FILE_CAP = 40 # files listed per directory before "… +N more"
530
+ TREE_TOTAL_LINE_CAP = 900 # hard ceiling so huge monorepos stay readable
531
+
532
+
533
+ def _generate_tree_section(root: Path, cache: Dict[str, Any]) -> List[str]:
534
+ """Indented file tree of parsed + wiki-tracked files, with descriptions.
535
+
536
+ The Mermaid graph shows *dependencies* (which reads as soup at monorepo
537
+ scale); this tree is the readable answer to "what is in this project":
538
+
539
+ wikifier/
540
+ ├── parsers/
541
+ │ ├── bree.py — barrel & re-export chain engine
542
+ │ └── python.py
543
+ └── cli.py — entry point + full update pipeline
544
+ """
545
+ descriptions = _load_health_descriptions(root)
546
+ parsed = {k for k in cache.keys() if isinstance(k, str) and not k.startswith("_")}
547
+ # Health keys are free text (audit records may use whole sentences as the
548
+ # "file"; some entries point at directories): only real on-disk files —
549
+ # or parser-confirmed ones — belong in the tree.
550
+ tracked = set()
551
+ for k in descriptions:
552
+ k2 = k.rstrip("/")
553
+ if not k2 or k2 in parsed:
554
+ continue
555
+ try:
556
+ if (root / k2).is_file():
557
+ tracked.add(k2)
558
+ except OSError:
559
+ continue
560
+ files = sorted(parsed | tracked)
561
+ if not files:
562
+ return ["## File Tree", "", "```text",
563
+ "(no files parsed yet — run update-maps)", "```", ""]
564
+
565
+ # Nested {name: subtree}; directory keys end with '/', file leaves are None.
566
+ tree: Dict[str, Any] = {}
567
+ for path in files:
568
+ node = tree
569
+ parts = path.split("/")
570
+ for part in parts[:-1]:
571
+ node = node.setdefault(part + "/", {})
572
+ node[parts[-1]] = None
573
+
574
+ def count_leaves(node: Dict[str, Any]) -> int:
575
+ return sum(count_leaves(v) if isinstance(v, dict) else 1 for v in node.values())
576
+
577
+ out: List[str] = []
578
+ truncated = False
579
+
580
+ def render(node: Dict[str, Any], prefix: str, path_prefix: str) -> None:
581
+ nonlocal truncated
582
+ dirs = sorted(k for k in node if k.endswith("/"))
583
+ leaves = sorted(k for k in node if not k.endswith("/"))
584
+ shown = leaves[:TREE_DIR_FILE_CAP]
585
+ hidden = len(leaves) - len(shown)
586
+ items = dirs + shown + (["\x00more"] if hidden > 0 else [])
587
+ for i, name in enumerate(items):
588
+ if len(out) >= TREE_TOTAL_LINE_CAP:
589
+ truncated = True
590
+ return
591
+ last = i == len(items) - 1
592
+ connector = "└── " if last else "├── "
593
+ child_prefix = prefix + (" " if last else "│ ")
594
+ if name == "\x00more":
595
+ out.append(prefix + connector + "… +{0} more files".format(hidden))
596
+ elif name.endswith("/"):
597
+ sub = node[name]
598
+ n = count_leaves(sub)
599
+ suffix = " ({0} files)".format(n) if n > TREE_DIR_FILE_CAP else ""
600
+ out.append(prefix + connector + name + suffix)
601
+ render(sub, child_prefix, path_prefix + name)
602
+ else:
603
+ d = descriptions.get(path_prefix + name)
604
+ out.append(prefix + connector + name + ((" — " + d) if d else ""))
605
+
606
+ render(tree, "", "")
607
+
608
+ lines = [
609
+ "## File Tree", "",
610
+ "> Every parsed/tracked file, organised by folder. Descriptions come from",
611
+ "> the agent wiki (file_health). The dependency graph is further below.",
612
+ "", "```text",
613
+ "{0}/ ({1} files)".format(root.name, len(files)),
614
+ ]
615
+ lines.extend(out)
616
+ if truncated:
617
+ lines.append("… (tree capped at {0} lines — full file list lives in the import cache)".format(TREE_TOTAL_LINE_CAP))
618
+ lines.extend(["```", ""])
619
+ return lines
620
+
621
+
473
622
  def generate_library_md(root: Path, cache: Dict[str, Any]) -> str:
474
623
  """Render library.md content from an import cache (see module docstring)."""
475
624
  if not isinstance(cache, dict):
@@ -483,6 +632,7 @@ def generate_library_md(root: Path, cache: Dict[str, Any]) -> str:
483
632
  '> Run `wikifier record-change library.md "..."` if you need to annotate.',
484
633
  "",
485
634
  ]
635
+ lines.extend(_generate_tree_section(root, cache))
486
636
  lines.extend(_generate_mermaid_section(edges))
487
637
  lines.extend(_generate_table_section(edges))
488
638
  lines.extend(_generate_cycles_section(cache))
@@ -261,6 +261,40 @@ def _strip_docstrings(content: str) -> str:
261
261
  return content
262
262
 
263
263
 
264
+ def _strip_comments(content: str) -> str:
265
+ """Blank out # comments (outside string literals), preserving line structure.
266
+
267
+ The dynamic-import scanner matches anywhere in the text (unlike the
268
+ line-anchored static patterns), so a comment like
269
+ `# importlib.import_module() now supported` produced a garbage dynamic
270
+ edge whose raw_module was the comment text — which then leaked into
271
+ library.md as a phantom graph node. Docstrings were already stripped;
272
+ comments were not.
273
+ """
274
+ out_lines = []
275
+ for line in content.split("\n"):
276
+ in_str = None
277
+ cut = None
278
+ i = 0
279
+ n = len(line)
280
+ while i < n:
281
+ c = line[i]
282
+ if in_str:
283
+ if c == "\\":
284
+ i += 2
285
+ continue
286
+ if c == in_str:
287
+ in_str = None
288
+ elif c in ("'", '"'):
289
+ in_str = c
290
+ elif c == "#":
291
+ cut = i
292
+ break
293
+ i += 1
294
+ out_lines.append(line if cut is None else line[:cut])
295
+ return "\n".join(out_lines)
296
+
297
+
264
298
  def parse_python_imports(filepath: str) -> List[Dict[str, Any]]:
265
299
  """
266
300
  Parse a Python file and return structured import information.
@@ -295,8 +329,10 @@ def parse_python_imports(filepath: str) -> List[Dict[str, Any]]:
295
329
  except Exception:
296
330
  return []
297
331
 
298
- # Remove docstrings to avoid matching imports inside documentation
332
+ # Remove docstrings + comments to avoid matching imports inside
333
+ # documentation (the dynamic scanner matches anywhere in the text).
299
334
  content = _strip_docstrings(content)
335
+ content = _strip_comments(content)
300
336
 
301
337
  imports: List[Dict[str, Any]] = []
302
338
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wikifier
3
- Version: 4.4.0
3
+ Version: 4.5.1
4
4
  Summary: Zero-dependency agent-to-agent codebase wiki for LLMs and AI agents. Autonomously maintained via record-change and mark-green for token-efficient lookup of files, dependencies, health, and summaries — across tiny scripts to 50k+ monorepos. Optional MCP server with rich tools for agents.
5
5
  Author-email: Aron Amos <aron@example.com>
6
6
  Maintainer: Aron Amos
@@ -137,6 +137,10 @@ Works with Claude Desktop, Claude Code, Cursor, Cline, and any Model Context Pro
137
137
 
138
138
  ## The Human Layer (Secondary, by Design)
139
139
 
140
+ ![Wikifier human dashboard — the project file tree with agent-wiki descriptions, health pills, and one-click Run/Stop controls, served locally by wikifier serve](https://raw.githubusercontent.com/IronAdamant/wikifier/main/screenshot/front_page_review.png)
141
+
142
+ *The dashboard served by `wikifier serve`: live file tree with wiki descriptions, health status pills, and action buttons that run `update-maps`/`check-changes` directly (localhost-only) — plus a Stop server button for when you're done.*
143
+
140
144
  `wikifier init` copies a single static `index.html` into your project: a dashboard showing the Mermaid code-structure chart, files with short descriptions, and a folder browser — useful for humans investigating what the agents know. View it with **`wikifier serve`** (then open http://localhost:8787/index.html) — browsers block `file://` fetches, so a double-clicked index.html can't read the wiki files; the page detects that case and shows you the fix. When served by `wikifier serve`, the dashboard's buttons genuinely **run** `update-maps`/`check-changes` (whitelisted commands only, localhost-only) and a **Stop server** button shuts the process down when you're done; behind any plain static server the same buttons fall back to copy + auto-refresh. The agent-facing markdown files and tools remain the single source of truth.
141
145
 
142
146
  ## Intended Use: Strictly Agent-to-Agent
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes