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.
- {wikifier-4.4.0/wikifier.egg-info → wikifier-4.5.1}/PKG-INFO +5 -1
- {wikifier-4.4.0 → wikifier-4.5.1}/README.md +4 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/index.html +169 -35
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/__init__.py +1 -1
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/index.html +169 -35
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/library.py +151 -1
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/python.py +37 -1
- {wikifier-4.4.0 → wikifier-4.5.1/wikifier.egg-info}/PKG-INFO +5 -1
- {wikifier-4.4.0 → wikifier-4.5.1}/CONTRIBUTING.md +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/LICENSE +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/MANIFEST.in +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/diagnostics.html +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/docs/spec.md +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/pyproject.toml +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/setup.cfg +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/skills/run.md +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_health.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_import_cache.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/tests/test_parsers.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/__main__.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/cli.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/contracts.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/daemon.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/diagnostics.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/health.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/import_cache.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/locking.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/mcp/server.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/resolution.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier/serve.py +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/SOURCES.txt +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.4.0 → wikifier-4.5.1}/wikifier.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
+

|
|
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
|
+

|
|
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
|
-
/* ----------
|
|
140
|
-
#
|
|
141
|
-
margin
|
|
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;
|
|
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
|
|
270
|
+
<!-- ============ structure: file tree (primary) + dependency graph ============ -->
|
|
250
271
|
<section class="card">
|
|
251
|
-
<h2>Code structure <span class="sub">
|
|
252
|
-
<
|
|
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-
|
|
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">
|
|
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) {
|
|
389
|
-
} catch (e) {
|
|
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
|
|
585
|
-
|
|
586
|
-
const
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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 =
|
|
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>
|
|
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
|
|
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
|
|
640
|
-
if (
|
|
747
|
+
const tree = treeText();
|
|
748
|
+
if (tree) {
|
|
641
749
|
lines.push('');
|
|
642
|
-
lines.push('## Structure
|
|
643
|
-
lines.push('```
|
|
644
|
-
lines.push(
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
-
/* ----------
|
|
140
|
-
#
|
|
141
|
-
margin
|
|
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;
|
|
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
|
|
270
|
+
<!-- ============ structure: file tree (primary) + dependency graph ============ -->
|
|
250
271
|
<section class="card">
|
|
251
|
-
<h2>Code structure <span class="sub">
|
|
252
|
-
<
|
|
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-
|
|
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">
|
|
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) {
|
|
389
|
-
} catch (e) {
|
|
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
|
|
585
|
-
|
|
586
|
-
const
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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 =
|
|
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>
|
|
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
|
|
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
|
|
640
|
-
if (
|
|
747
|
+
const tree = treeText();
|
|
748
|
+
if (tree) {
|
|
641
749
|
lines.push('');
|
|
642
|
-
lines.push('## Structure
|
|
643
|
-
lines.push('```
|
|
644
|
-
lines.push(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+

|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|