gitnexus 1.2.7 → 1.2.8

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.
@@ -22,11 +22,11 @@ export async function closeWikiDb() {
22
22
  * Get all source files with their exported symbol names and types.
23
23
  */
24
24
  export async function getFilesWithExports() {
25
- const rows = await executeQuery(REPO_ID, `
26
- MATCH (f:File)-[:CodeRelation {type: 'DEFINES'}]->(n)
27
- WHERE n.isExported = true
28
- RETURN f.filePath AS filePath, n.name AS name, labels(n)[0] AS type
29
- ORDER BY f.filePath
25
+ const rows = await executeQuery(REPO_ID, `
26
+ MATCH (f:File)-[:CodeRelation {type: 'DEFINES'}]->(n)
27
+ WHERE n.isExported = true
28
+ RETURN f.filePath AS filePath, n.name AS name, labels(n)[0] AS type
29
+ ORDER BY f.filePath
30
30
  `);
31
31
  const fileMap = new Map();
32
32
  for (const row of rows) {
@@ -46,10 +46,10 @@ export async function getFilesWithExports() {
46
46
  * Get all files tracked in the graph (including those with no exports).
47
47
  */
48
48
  export async function getAllFiles() {
49
- const rows = await executeQuery(REPO_ID, `
50
- MATCH (f:File)
51
- RETURN f.filePath AS filePath
52
- ORDER BY f.filePath
49
+ const rows = await executeQuery(REPO_ID, `
50
+ MATCH (f:File)
51
+ RETURN f.filePath AS filePath
52
+ ORDER BY f.filePath
53
53
  `);
54
54
  return rows.map(r => r.filePath || r[0]);
55
55
  }
@@ -57,11 +57,11 @@ export async function getAllFiles() {
57
57
  * Get inter-file call edges (calls between different files).
58
58
  */
59
59
  export async function getInterFileCallEdges() {
60
- const rows = await executeQuery(REPO_ID, `
61
- MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
62
- WHERE a.filePath <> b.filePath
63
- RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
64
- b.filePath AS toFile, b.name AS toName
60
+ const rows = await executeQuery(REPO_ID, `
61
+ MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
62
+ WHERE a.filePath <> b.filePath
63
+ RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
64
+ b.filePath AS toFile, b.name AS toName
65
65
  `);
66
66
  return rows.map(r => ({
67
67
  fromFile: r.fromFile || r[0],
@@ -77,11 +77,11 @@ export async function getIntraModuleCallEdges(filePaths) {
77
77
  if (filePaths.length === 0)
78
78
  return [];
79
79
  const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
80
- const rows = await executeQuery(REPO_ID, `
81
- MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
82
- WHERE a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
83
- RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
84
- b.filePath AS toFile, b.name AS toName
80
+ const rows = await executeQuery(REPO_ID, `
81
+ MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
82
+ WHERE a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
83
+ RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
84
+ b.filePath AS toFile, b.name AS toName
85
85
  `);
86
86
  return rows.map(r => ({
87
87
  fromFile: r.fromFile || r[0],
@@ -97,19 +97,19 @@ export async function getInterModuleCallEdges(filePaths) {
97
97
  if (filePaths.length === 0)
98
98
  return { outgoing: [], incoming: [] };
99
99
  const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
100
- const outRows = await executeQuery(REPO_ID, `
101
- MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
102
- WHERE a.filePath IN [${fileList}] AND NOT b.filePath IN [${fileList}]
103
- RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
104
- b.filePath AS toFile, b.name AS toName
105
- LIMIT 30
100
+ const outRows = await executeQuery(REPO_ID, `
101
+ MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
102
+ WHERE a.filePath IN [${fileList}] AND NOT b.filePath IN [${fileList}]
103
+ RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
104
+ b.filePath AS toFile, b.name AS toName
105
+ LIMIT 30
106
106
  `);
107
- const inRows = await executeQuery(REPO_ID, `
108
- MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
109
- WHERE NOT a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
110
- RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
111
- b.filePath AS toFile, b.name AS toName
112
- LIMIT 30
107
+ const inRows = await executeQuery(REPO_ID, `
108
+ MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
109
+ WHERE NOT a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
110
+ RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
111
+ b.filePath AS toFile, b.name AS toName
112
+ LIMIT 30
113
113
  `);
114
114
  return {
115
115
  outgoing: outRows.map(r => ({
@@ -135,13 +135,13 @@ export async function getProcessesForFiles(filePaths, limit = 5) {
135
135
  return [];
136
136
  const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
137
137
  // Find processes that have steps in the given files
138
- const procRows = await executeQuery(REPO_ID, `
139
- MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
140
- WHERE s.filePath IN [${fileList}]
141
- RETURN DISTINCT p.id AS id, p.heuristicLabel AS label,
142
- p.processType AS type, p.stepCount AS stepCount
143
- ORDER BY stepCount DESC
144
- LIMIT ${limit}
138
+ const procRows = await executeQuery(REPO_ID, `
139
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
140
+ WHERE s.filePath IN [${fileList}]
141
+ RETURN DISTINCT p.id AS id, p.heuristicLabel AS label,
142
+ p.processType AS type, p.stepCount AS stepCount
143
+ ORDER BY stepCount DESC
144
+ LIMIT ${limit}
145
145
  `);
146
146
  const processes = [];
147
147
  for (const row of procRows) {
@@ -150,10 +150,10 @@ export async function getProcessesForFiles(filePaths, limit = 5) {
150
150
  const type = row.type || row[2] || 'unknown';
151
151
  const stepCount = row.stepCount || row[3] || 0;
152
152
  // Get the full step trace for this process
153
- const stepRows = await executeQuery(REPO_ID, `
154
- MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${procId.replace(/'/g, "''")}'})
155
- RETURN s.name AS name, s.filePath AS filePath, labels(s)[0] AS type, r.step AS step
156
- ORDER BY r.step
153
+ const stepRows = await executeQuery(REPO_ID, `
154
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${procId.replace(/'/g, "''")}'})
155
+ RETURN s.name AS name, s.filePath AS filePath, labels(s)[0] AS type, r.step AS step
156
+ ORDER BY r.step
157
157
  `);
158
158
  processes.push({
159
159
  id: procId,
@@ -174,12 +174,12 @@ export async function getProcessesForFiles(filePaths, limit = 5) {
174
174
  * Get all processes in the graph (for overview page).
175
175
  */
176
176
  export async function getAllProcesses(limit = 20) {
177
- const procRows = await executeQuery(REPO_ID, `
178
- MATCH (p:Process)
179
- RETURN p.id AS id, p.heuristicLabel AS label,
180
- p.processType AS type, p.stepCount AS stepCount
181
- ORDER BY stepCount DESC
182
- LIMIT ${limit}
177
+ const procRows = await executeQuery(REPO_ID, `
178
+ MATCH (p:Process)
179
+ RETURN p.id AS id, p.heuristicLabel AS label,
180
+ p.processType AS type, p.stepCount AS stepCount
181
+ ORDER BY stepCount DESC
182
+ LIMIT ${limit}
183
183
  `);
184
184
  const processes = [];
185
185
  for (const row of procRows) {
@@ -187,10 +187,10 @@ export async function getAllProcesses(limit = 20) {
187
187
  const label = row.label || row[1] || procId;
188
188
  const type = row.type || row[2] || 'unknown';
189
189
  const stepCount = row.stepCount || row[3] || 0;
190
- const stepRows = await executeQuery(REPO_ID, `
191
- MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${procId.replace(/'/g, "''")}'})
192
- RETURN s.name AS name, s.filePath AS filePath, labels(s)[0] AS type, r.step AS step
193
- ORDER BY r.step
190
+ const stepRows = await executeQuery(REPO_ID, `
191
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${procId.replace(/'/g, "''")}'})
192
+ RETURN s.name AS name, s.filePath AS filePath, labels(s)[0] AS type, r.step AS step
193
+ ORDER BY r.step
194
194
  `);
195
195
  processes.push({
196
196
  id: procId,
@@ -100,198 +100,198 @@ const BOOK_SVG = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" st
100
100
  '<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/>' +
101
101
  '<path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/>' +
102
102
  '</svg>';
103
- const CSS = `
104
- *{margin:0;padding:0;box-sizing:border-box}
105
- :root{
106
- --bg:#ffffff;--sidebar-bg:#f8f9fb;--border:#e5e7eb;
107
- --text:#1e293b;--text-muted:#64748b;--primary:#2563eb;
108
- --primary-soft:#eff6ff;--hover:#f1f5f9;--code-bg:#f1f5f9;
109
- --radius:8px;--shadow:0 1px 3px rgba(0,0,0,.08);
110
- }
111
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
112
- line-height:1.65;color:var(--text);background:var(--bg)}
113
-
114
- .layout{display:flex;min-height:100vh}
115
- .sidebar{width:280px;background:var(--sidebar-bg);border-right:1px solid var(--border);
116
- position:fixed;top:0;left:0;bottom:0;overflow-y:auto;padding:24px 16px;
117
- display:flex;flex-direction:column;z-index:10}
118
- .content{margin-left:280px;flex:1;padding:48px 64px;max-width:960px}
119
-
120
- .sidebar-header{margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}
121
- .sidebar-title{font-size:16px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}
122
- .sidebar-title svg{flex-shrink:0}
123
- .sidebar-meta{font-size:11px;color:var(--text-muted);margin-top:6px}
124
- .nav-section{margin-bottom:2px}
125
- .nav-item{display:block;padding:7px 12px;border-radius:var(--radius);cursor:pointer;
126
- font-size:13px;color:var(--text);text-decoration:none;transition:all .15s;
127
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
128
- .nav-item:hover{background:var(--hover)}
129
- .nav-item.active{background:var(--primary-soft);color:var(--primary);font-weight:600}
130
- .nav-item.overview{font-weight:600;margin-bottom:4px}
131
- .nav-children{padding-left:14px;border-left:1px solid var(--border);margin-left:12px}
132
- .nav-group-label{font-size:11px;font-weight:600;color:var(--text-muted);
133
- text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 4px;user-select:none}
134
- .sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);
135
- font-size:11px;color:var(--text-muted);text-align:center}
136
-
137
- .content h1{font-size:28px;font-weight:700;margin-bottom:8px;line-height:1.3}
138
- .content h2{font-size:22px;font-weight:600;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)}
139
- .content h3{font-size:17px;font-weight:600;margin:24px 0 8px}
140
- .content h4{font-size:15px;font-weight:600;margin:20px 0 6px}
141
- .content p{margin:12px 0}
142
- .content ul,.content ol{margin:12px 0 12px 24px}
143
- .content li{margin:4px 0}
144
- .content a{color:var(--primary);text-decoration:none}
145
- .content a:hover{text-decoration:underline}
146
- .content blockquote{border-left:3px solid var(--primary);padding:8px 16px;margin:16px 0;
147
- background:var(--primary-soft);border-radius:0 var(--radius) var(--radius) 0;
148
- color:var(--text-muted);font-size:14px}
149
- .content code{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:13px;
150
- background:var(--code-bg);padding:2px 6px;border-radius:4px}
151
- .content pre{background:#1e293b;color:#e2e8f0;border-radius:var(--radius);padding:16px;
152
- overflow-x:auto;margin:16px 0}
153
- .content pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:inherit}
154
- .content table{border-collapse:collapse;width:100%;margin:16px 0}
155
- .content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left;font-size:14px}
156
- .content th{background:var(--sidebar-bg);font-weight:600}
157
- .content img{max-width:100%;border-radius:var(--radius)}
158
- .content hr{border:none;border-top:1px solid var(--border);margin:32px 0}
159
- .content .mermaid{margin:20px 0;text-align:center}
160
-
161
- .menu-toggle{display:none;position:fixed;top:12px;left:12px;z-index:20;
162
- background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);
163
- padding:8px 12px;cursor:pointer;font-size:18px;box-shadow:var(--shadow)}
164
- @media(max-width:768px){
165
- .sidebar{transform:translateX(-100%);transition:transform .2s}
166
- .sidebar.open{transform:translateX(0);box-shadow:2px 0 12px rgba(0,0,0,.1)}
167
- .content{margin-left:0;padding:24px 20px;padding-top:56px}
168
- .menu-toggle{display:block}
169
- }
170
- .empty-state{text-align:center;padding:80px 20px;color:var(--text-muted)}
171
- .empty-state h2{font-size:20px;margin-bottom:8px;border:none}
103
+ const CSS = `
104
+ *{margin:0;padding:0;box-sizing:border-box}
105
+ :root{
106
+ --bg:#ffffff;--sidebar-bg:#f8f9fb;--border:#e5e7eb;
107
+ --text:#1e293b;--text-muted:#64748b;--primary:#2563eb;
108
+ --primary-soft:#eff6ff;--hover:#f1f5f9;--code-bg:#f1f5f9;
109
+ --radius:8px;--shadow:0 1px 3px rgba(0,0,0,.08);
110
+ }
111
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
112
+ line-height:1.65;color:var(--text);background:var(--bg)}
113
+
114
+ .layout{display:flex;min-height:100vh}
115
+ .sidebar{width:280px;background:var(--sidebar-bg);border-right:1px solid var(--border);
116
+ position:fixed;top:0;left:0;bottom:0;overflow-y:auto;padding:24px 16px;
117
+ display:flex;flex-direction:column;z-index:10}
118
+ .content{margin-left:280px;flex:1;padding:48px 64px;max-width:960px}
119
+
120
+ .sidebar-header{margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}
121
+ .sidebar-title{font-size:16px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}
122
+ .sidebar-title svg{flex-shrink:0}
123
+ .sidebar-meta{font-size:11px;color:var(--text-muted);margin-top:6px}
124
+ .nav-section{margin-bottom:2px}
125
+ .nav-item{display:block;padding:7px 12px;border-radius:var(--radius);cursor:pointer;
126
+ font-size:13px;color:var(--text);text-decoration:none;transition:all .15s;
127
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
128
+ .nav-item:hover{background:var(--hover)}
129
+ .nav-item.active{background:var(--primary-soft);color:var(--primary);font-weight:600}
130
+ .nav-item.overview{font-weight:600;margin-bottom:4px}
131
+ .nav-children{padding-left:14px;border-left:1px solid var(--border);margin-left:12px}
132
+ .nav-group-label{font-size:11px;font-weight:600;color:var(--text-muted);
133
+ text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 4px;user-select:none}
134
+ .sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);
135
+ font-size:11px;color:var(--text-muted);text-align:center}
136
+
137
+ .content h1{font-size:28px;font-weight:700;margin-bottom:8px;line-height:1.3}
138
+ .content h2{font-size:22px;font-weight:600;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)}
139
+ .content h3{font-size:17px;font-weight:600;margin:24px 0 8px}
140
+ .content h4{font-size:15px;font-weight:600;margin:20px 0 6px}
141
+ .content p{margin:12px 0}
142
+ .content ul,.content ol{margin:12px 0 12px 24px}
143
+ .content li{margin:4px 0}
144
+ .content a{color:var(--primary);text-decoration:none}
145
+ .content a:hover{text-decoration:underline}
146
+ .content blockquote{border-left:3px solid var(--primary);padding:8px 16px;margin:16px 0;
147
+ background:var(--primary-soft);border-radius:0 var(--radius) var(--radius) 0;
148
+ color:var(--text-muted);font-size:14px}
149
+ .content code{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:13px;
150
+ background:var(--code-bg);padding:2px 6px;border-radius:4px}
151
+ .content pre{background:#1e293b;color:#e2e8f0;border-radius:var(--radius);padding:16px;
152
+ overflow-x:auto;margin:16px 0}
153
+ .content pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:inherit}
154
+ .content table{border-collapse:collapse;width:100%;margin:16px 0}
155
+ .content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left;font-size:14px}
156
+ .content th{background:var(--sidebar-bg);font-weight:600}
157
+ .content img{max-width:100%;border-radius:var(--radius)}
158
+ .content hr{border:none;border-top:1px solid var(--border);margin:32px 0}
159
+ .content .mermaid{margin:20px 0;text-align:center}
160
+
161
+ .menu-toggle{display:none;position:fixed;top:12px;left:12px;z-index:20;
162
+ background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);
163
+ padding:8px 12px;cursor:pointer;font-size:18px;box-shadow:var(--shadow)}
164
+ @media(max-width:768px){
165
+ .sidebar{transform:translateX(-100%);transition:transform .2s}
166
+ .sidebar.open{transform:translateX(0);box-shadow:2px 0 12px rgba(0,0,0,.1)}
167
+ .content{margin-left:0;padding:24px 20px;padding-top:56px}
168
+ .menu-toggle{display:block}
169
+ }
170
+ .empty-state{text-align:center;padding:80px 20px;color:var(--text-muted)}
171
+ .empty-state h2{font-size:20px;margin-bottom:8px;border:none}
172
172
  `;
173
173
  // The client-side JS is kept as a plain string to avoid template literal conflicts
174
- const JS_APP = `
175
- (function() {
176
- var activePage = 'overview';
177
-
178
- document.addEventListener('DOMContentLoaded', function() {
179
- mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'loose' });
180
- renderMeta();
181
- renderNav();
182
- document.getElementById('menu-toggle').addEventListener('click', function() {
183
- document.getElementById('sidebar').classList.toggle('open');
184
- });
185
- if (location.hash && location.hash.length > 1) {
186
- activePage = decodeURIComponent(location.hash.slice(1));
187
- }
188
- navigateTo(activePage);
189
- });
190
-
191
- function renderMeta() {
192
- if (!META) return;
193
- var el = document.getElementById('meta-info');
194
- var parts = [];
195
- if (META.generatedAt) {
196
- parts.push(new Date(META.generatedAt).toLocaleDateString());
197
- }
198
- if (META.model) parts.push(META.model);
199
- if (META.fromCommit) parts.push(META.fromCommit.slice(0, 8));
200
- el.textContent = parts.join(' \\u00b7 ');
201
- }
202
-
203
- function renderNav() {
204
- var container = document.getElementById('nav-tree');
205
- var html = '<div class="nav-section">';
206
- html += '<a class="nav-item overview" data-page="overview" href="#overview">Overview</a>';
207
- html += '</div>';
208
- if (TREE.length > 0) {
209
- html += '<div class="nav-group-label">Modules</div>';
210
- html += buildNavTree(TREE);
211
- }
212
- container.innerHTML = html;
213
- container.addEventListener('click', function(e) {
214
- var target = e.target;
215
- while (target && !target.dataset.page) { target = target.parentElement; }
216
- if (target && target.dataset.page) {
217
- e.preventDefault();
218
- navigateTo(target.dataset.page);
219
- }
220
- });
221
- }
222
-
223
- function buildNavTree(nodes) {
224
- var html = '';
225
- for (var i = 0; i < nodes.length; i++) {
226
- var node = nodes[i];
227
- html += '<div class="nav-section">';
228
- html += '<a class="nav-item" data-page="' + escH(node.slug) + '" href="#' + encodeURIComponent(node.slug) + '">' + escH(node.name) + '</a>';
229
- if (node.children && node.children.length > 0) {
230
- html += '<div class="nav-children">' + buildNavTree(node.children) + '</div>';
231
- }
232
- html += '</div>';
233
- }
234
- return html;
235
- }
236
-
237
- function escH(s) {
238
- var d = document.createElement('div');
239
- d.textContent = s;
240
- return d.innerHTML;
241
- }
242
-
243
- function navigateTo(page) {
244
- activePage = page;
245
- location.hash = encodeURIComponent(page);
246
-
247
- var items = document.querySelectorAll('.nav-item');
248
- for (var i = 0; i < items.length; i++) {
249
- if (items[i].dataset.page === page) {
250
- items[i].classList.add('active');
251
- } else {
252
- items[i].classList.remove('active');
253
- }
254
- }
255
-
256
- var contentEl = document.getElementById('content');
257
- var md = PAGES[page];
258
-
259
- if (!md) {
260
- contentEl.innerHTML = '<div class="empty-state"><h2>Page not found</h2><p>' + escH(page) + '.md does not exist.</p></div>';
261
- return;
262
- }
263
-
264
- contentEl.innerHTML = marked.parse(md);
265
-
266
- // Rewrite .md links to hash navigation
267
- var links = contentEl.querySelectorAll('a[href]');
268
- for (var i = 0; i < links.length; i++) {
269
- var href = links[i].getAttribute('href');
270
- if (href && href.endsWith('.md') && href.indexOf('://') === -1) {
271
- var slug = href.replace(/\\.md$/, '');
272
- links[i].setAttribute('href', '#' + encodeURIComponent(slug));
273
- (function(s) {
274
- links[i].addEventListener('click', function(e) {
275
- e.preventDefault();
276
- navigateTo(s);
277
- });
278
- })(slug);
279
- }
280
- }
281
-
282
- // Convert mermaid code blocks into mermaid divs
283
- var mermaidBlocks = contentEl.querySelectorAll('pre code.language-mermaid');
284
- for (var i = 0; i < mermaidBlocks.length; i++) {
285
- var pre = mermaidBlocks[i].parentElement;
286
- var div = document.createElement('div');
287
- div.className = 'mermaid';
288
- div.textContent = mermaidBlocks[i].textContent;
289
- pre.parentNode.replaceChild(div, pre);
290
- }
291
- try { mermaid.run({ querySelector: '.mermaid' }); } catch(e) {}
292
-
293
- window.scrollTo(0, 0);
294
- document.getElementById('sidebar').classList.remove('open');
295
- }
296
- })();
174
+ const JS_APP = `
175
+ (function() {
176
+ var activePage = 'overview';
177
+
178
+ document.addEventListener('DOMContentLoaded', function() {
179
+ mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'loose' });
180
+ renderMeta();
181
+ renderNav();
182
+ document.getElementById('menu-toggle').addEventListener('click', function() {
183
+ document.getElementById('sidebar').classList.toggle('open');
184
+ });
185
+ if (location.hash && location.hash.length > 1) {
186
+ activePage = decodeURIComponent(location.hash.slice(1));
187
+ }
188
+ navigateTo(activePage);
189
+ });
190
+
191
+ function renderMeta() {
192
+ if (!META) return;
193
+ var el = document.getElementById('meta-info');
194
+ var parts = [];
195
+ if (META.generatedAt) {
196
+ parts.push(new Date(META.generatedAt).toLocaleDateString());
197
+ }
198
+ if (META.model) parts.push(META.model);
199
+ if (META.fromCommit) parts.push(META.fromCommit.slice(0, 8));
200
+ el.textContent = parts.join(' \\u00b7 ');
201
+ }
202
+
203
+ function renderNav() {
204
+ var container = document.getElementById('nav-tree');
205
+ var html = '<div class="nav-section">';
206
+ html += '<a class="nav-item overview" data-page="overview" href="#overview">Overview</a>';
207
+ html += '</div>';
208
+ if (TREE.length > 0) {
209
+ html += '<div class="nav-group-label">Modules</div>';
210
+ html += buildNavTree(TREE);
211
+ }
212
+ container.innerHTML = html;
213
+ container.addEventListener('click', function(e) {
214
+ var target = e.target;
215
+ while (target && !target.dataset.page) { target = target.parentElement; }
216
+ if (target && target.dataset.page) {
217
+ e.preventDefault();
218
+ navigateTo(target.dataset.page);
219
+ }
220
+ });
221
+ }
222
+
223
+ function buildNavTree(nodes) {
224
+ var html = '';
225
+ for (var i = 0; i < nodes.length; i++) {
226
+ var node = nodes[i];
227
+ html += '<div class="nav-section">';
228
+ html += '<a class="nav-item" data-page="' + escH(node.slug) + '" href="#' + encodeURIComponent(node.slug) + '">' + escH(node.name) + '</a>';
229
+ if (node.children && node.children.length > 0) {
230
+ html += '<div class="nav-children">' + buildNavTree(node.children) + '</div>';
231
+ }
232
+ html += '</div>';
233
+ }
234
+ return html;
235
+ }
236
+
237
+ function escH(s) {
238
+ var d = document.createElement('div');
239
+ d.textContent = s;
240
+ return d.innerHTML;
241
+ }
242
+
243
+ function navigateTo(page) {
244
+ activePage = page;
245
+ location.hash = encodeURIComponent(page);
246
+
247
+ var items = document.querySelectorAll('.nav-item');
248
+ for (var i = 0; i < items.length; i++) {
249
+ if (items[i].dataset.page === page) {
250
+ items[i].classList.add('active');
251
+ } else {
252
+ items[i].classList.remove('active');
253
+ }
254
+ }
255
+
256
+ var contentEl = document.getElementById('content');
257
+ var md = PAGES[page];
258
+
259
+ if (!md) {
260
+ contentEl.innerHTML = '<div class="empty-state"><h2>Page not found</h2><p>' + escH(page) + '.md does not exist.</p></div>';
261
+ return;
262
+ }
263
+
264
+ contentEl.innerHTML = marked.parse(md);
265
+
266
+ // Rewrite .md links to hash navigation
267
+ var links = contentEl.querySelectorAll('a[href]');
268
+ for (var i = 0; i < links.length; i++) {
269
+ var href = links[i].getAttribute('href');
270
+ if (href && href.endsWith('.md') && href.indexOf('://') === -1) {
271
+ var slug = href.replace(/\\.md$/, '');
272
+ links[i].setAttribute('href', '#' + encodeURIComponent(slug));
273
+ (function(s) {
274
+ links[i].addEventListener('click', function(e) {
275
+ e.preventDefault();
276
+ navigateTo(s);
277
+ });
278
+ })(slug);
279
+ }
280
+ }
281
+
282
+ // Convert mermaid code blocks into mermaid divs
283
+ var mermaidBlocks = contentEl.querySelectorAll('pre code.language-mermaid');
284
+ for (var i = 0; i < mermaidBlocks.length; i++) {
285
+ var pre = mermaidBlocks[i].parentElement;
286
+ var div = document.createElement('div');
287
+ div.className = 'mermaid';
288
+ div.textContent = mermaidBlocks[i].textContent;
289
+ pre.parentNode.replaceChild(div, pre);
290
+ }
291
+ try { mermaid.run({ querySelector: '.mermaid' }); } catch(e) {}
292
+
293
+ window.scrollTo(0, 0);
294
+ document.getElementById('sidebar').classList.remove('open');
295
+ }
296
+ })();
297
297
  `;