trellis 2.0.13 → 2.1.2

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 (96) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/embeddings/index.js +1 -1
  3. package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
  4. package/package.json +2 -10
  5. package/dist/transformers.node-bx3q9d7k.js +0 -33130
  6. package/src/cli/index.ts +0 -3356
  7. package/src/core/agents/harness.ts +0 -380
  8. package/src/core/agents/index.ts +0 -18
  9. package/src/core/agents/types.ts +0 -90
  10. package/src/core/index.ts +0 -118
  11. package/src/core/kernel/middleware.ts +0 -44
  12. package/src/core/kernel/trellis-kernel.ts +0 -593
  13. package/src/core/ontology/builtins.ts +0 -248
  14. package/src/core/ontology/index.ts +0 -34
  15. package/src/core/ontology/registry.ts +0 -209
  16. package/src/core/ontology/types.ts +0 -124
  17. package/src/core/ontology/validator.ts +0 -382
  18. package/src/core/persist/backend.ts +0 -74
  19. package/src/core/persist/sqlite-backend.ts +0 -298
  20. package/src/core/plugins/index.ts +0 -17
  21. package/src/core/plugins/registry.ts +0 -322
  22. package/src/core/plugins/types.ts +0 -126
  23. package/src/core/query/datalog.ts +0 -188
  24. package/src/core/query/engine.ts +0 -370
  25. package/src/core/query/index.ts +0 -34
  26. package/src/core/query/parser.ts +0 -481
  27. package/src/core/query/types.ts +0 -200
  28. package/src/core/store/eav-store.ts +0 -467
  29. package/src/decisions/auto-capture.ts +0 -136
  30. package/src/decisions/hooks.ts +0 -163
  31. package/src/decisions/index.ts +0 -261
  32. package/src/decisions/types.ts +0 -103
  33. package/src/embeddings/auto-embed.ts +0 -248
  34. package/src/embeddings/chunker.ts +0 -327
  35. package/src/embeddings/index.ts +0 -48
  36. package/src/embeddings/model.ts +0 -112
  37. package/src/embeddings/search.ts +0 -305
  38. package/src/embeddings/store.ts +0 -313
  39. package/src/embeddings/types.ts +0 -92
  40. package/src/engine.ts +0 -1125
  41. package/src/garden/cluster.ts +0 -330
  42. package/src/garden/garden.ts +0 -306
  43. package/src/garden/index.ts +0 -29
  44. package/src/git/git-exporter.ts +0 -286
  45. package/src/git/git-importer.ts +0 -329
  46. package/src/git/git-reader.ts +0 -189
  47. package/src/git/index.ts +0 -22
  48. package/src/identity/governance.ts +0 -211
  49. package/src/identity/identity.ts +0 -224
  50. package/src/identity/index.ts +0 -30
  51. package/src/identity/signing-middleware.ts +0 -97
  52. package/src/index.ts +0 -29
  53. package/src/links/index.ts +0 -49
  54. package/src/links/lifecycle.ts +0 -400
  55. package/src/links/parser.ts +0 -484
  56. package/src/links/ref-index.ts +0 -186
  57. package/src/links/resolver.ts +0 -314
  58. package/src/links/types.ts +0 -108
  59. package/src/mcp/index.ts +0 -22
  60. package/src/mcp/server.ts +0 -1278
  61. package/src/semantic/csharp-parser.ts +0 -493
  62. package/src/semantic/go-parser.ts +0 -585
  63. package/src/semantic/index.ts +0 -34
  64. package/src/semantic/java-parser.ts +0 -456
  65. package/src/semantic/python-parser.ts +0 -659
  66. package/src/semantic/ruby-parser.ts +0 -446
  67. package/src/semantic/rust-parser.ts +0 -784
  68. package/src/semantic/semantic-merge.ts +0 -210
  69. package/src/semantic/ts-parser.ts +0 -681
  70. package/src/semantic/types.ts +0 -175
  71. package/src/sync/http-transport.ts +0 -144
  72. package/src/sync/index.ts +0 -43
  73. package/src/sync/memory-transport.ts +0 -66
  74. package/src/sync/multi-repo.ts +0 -200
  75. package/src/sync/reconciler.ts +0 -237
  76. package/src/sync/sync-engine.ts +0 -258
  77. package/src/sync/types.ts +0 -104
  78. package/src/sync/ws-transport.ts +0 -145
  79. package/src/ui/client.html +0 -695
  80. package/src/ui/server.ts +0 -419
  81. package/src/vcs/blob-store.ts +0 -124
  82. package/src/vcs/branch.ts +0 -150
  83. package/src/vcs/checkpoint.ts +0 -64
  84. package/src/vcs/decompose.ts +0 -469
  85. package/src/vcs/diff.ts +0 -409
  86. package/src/vcs/engine-context.ts +0 -26
  87. package/src/vcs/index.ts +0 -23
  88. package/src/vcs/issue.ts +0 -800
  89. package/src/vcs/merge.ts +0 -425
  90. package/src/vcs/milestone.ts +0 -124
  91. package/src/vcs/ops.ts +0 -59
  92. package/src/vcs/types.ts +0 -213
  93. package/src/vcs/vcs-middleware.ts +0 -81
  94. package/src/watcher/fs-watcher.ts +0 -255
  95. package/src/watcher/index.ts +0 -9
  96. package/src/watcher/ingestion.ts +0 -116
@@ -1,695 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Trellis — Graph Explorer</title>
7
- <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
8
- <style>
9
- :root {
10
- --bg: #0d1117;
11
- --surface: #161b22;
12
- --border: #30363d;
13
- --text: #e6edf3;
14
- --text-dim: #8b949e;
15
- --accent: #58a6ff;
16
- --green: #3fb950;
17
- --yellow: #d29922;
18
- --red: #f85149;
19
- --purple: #bc8cff;
20
- --cyan: #79c0ff;
21
- --orange: #d18616;
22
- --radius: 8px;
23
- }
24
- * { margin: 0; padding: 0; box-sizing: border-box; }
25
- body {
26
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
27
- background: var(--bg);
28
- color: var(--text);
29
- overflow: hidden;
30
- height: 100vh;
31
- width: 100vw;
32
- }
33
-
34
- /* Top bar */
35
- #topbar {
36
- position: fixed; top: 0; left: 0; right: 0;
37
- height: 52px;
38
- background: var(--surface);
39
- border-bottom: 1px solid var(--border);
40
- display: flex; align-items: center;
41
- padding: 0 16px; gap: 12px;
42
- z-index: 100;
43
- }
44
- #topbar .logo {
45
- font-weight: 700; font-size: 15px;
46
- color: var(--accent);
47
- white-space: nowrap;
48
- letter-spacing: -0.3px;
49
- }
50
- #topbar .logo span { color: var(--text-dim); font-weight: 400; }
51
- #search-box {
52
- flex: 1; max-width: 480px;
53
- background: var(--bg);
54
- border: 1px solid var(--border);
55
- border-radius: var(--radius);
56
- padding: 7px 12px;
57
- color: var(--text);
58
- font-size: 13px;
59
- outline: none;
60
- transition: border-color 0.15s;
61
- }
62
- #search-box:focus { border-color: var(--accent); }
63
- #search-box::placeholder { color: var(--text-dim); }
64
- .stats {
65
- font-size: 12px; color: var(--text-dim);
66
- white-space: nowrap;
67
- }
68
- .legend {
69
- display: flex; gap: 12px; margin-left: auto;
70
- font-size: 11px; color: var(--text-dim);
71
- }
72
- .legend-item {
73
- display: flex; align-items: center; gap: 4px;
74
- }
75
- .legend-dot {
76
- width: 8px; height: 8px; border-radius: 50%;
77
- }
78
-
79
- /* Graph canvas */
80
- #graph {
81
- position: fixed; top: 52px; left: 0; right: 0; bottom: 0;
82
- }
83
- svg { width: 100%; height: 100%; }
84
-
85
- /* Detail sidebar */
86
- #sidebar {
87
- position: fixed; top: 52px; right: 0; bottom: 0;
88
- width: 360px;
89
- background: var(--surface);
90
- border-left: 1px solid var(--border);
91
- transform: translateX(100%);
92
- transition: transform 0.2s ease;
93
- z-index: 50;
94
- overflow-y: auto;
95
- padding: 0;
96
- }
97
- #sidebar.open { transform: translateX(0); }
98
- #sidebar-header {
99
- position: sticky; top: 0;
100
- background: var(--surface);
101
- padding: 16px;
102
- border-bottom: 1px solid var(--border);
103
- display: flex; align-items: center; justify-content: space-between;
104
- }
105
- #sidebar-header h3 {
106
- font-size: 14px; font-weight: 600;
107
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
108
- max-width: 280px;
109
- }
110
- #sidebar-close {
111
- background: none; border: none; color: var(--text-dim);
112
- cursor: pointer; font-size: 18px; padding: 4px;
113
- }
114
- #sidebar-close:hover { color: var(--text); }
115
- #sidebar-body { padding: 16px; }
116
- .detail-section { margin-bottom: 16px; }
117
- .detail-section h4 {
118
- font-size: 11px; text-transform: uppercase;
119
- color: var(--text-dim); margin-bottom: 6px;
120
- letter-spacing: 0.5px;
121
- }
122
- .detail-badge {
123
- display: inline-block;
124
- padding: 2px 8px;
125
- border-radius: 12px;
126
- font-size: 11px;
127
- font-weight: 500;
128
- margin-right: 4px;
129
- margin-bottom: 4px;
130
- }
131
- .badge-file { background: rgba(88,166,255,0.15); color: var(--accent); }
132
- .badge-milestone { background: rgba(63,185,80,0.15); color: var(--green); }
133
- .badge-issue { background: rgba(210,153,34,0.15); color: var(--yellow); }
134
- .badge-branch { background: rgba(188,140,255,0.15); color: var(--purple); }
135
- .detail-meta {
136
- font-size: 12px; color: var(--text-dim);
137
- line-height: 1.8;
138
- }
139
- .detail-meta strong { color: var(--text); font-weight: 500; }
140
- .detail-ops {
141
- font-size: 12px; line-height: 1.7;
142
- }
143
- .detail-ops .op-kind {
144
- font-family: 'SF Mono', SFMono-Regular, Consolas, monospace;
145
- font-size: 11px;
146
- color: var(--cyan);
147
- }
148
- .detail-ops .op-time { color: var(--text-dim); }
149
-
150
- /* Search results overlay */
151
- #search-results {
152
- position: fixed; top: 52px; left: 0; right: 0;
153
- max-height: 50vh;
154
- background: var(--surface);
155
- border-bottom: 1px solid var(--border);
156
- overflow-y: auto;
157
- display: none;
158
- z-index: 80;
159
- }
160
- #search-results.visible { display: block; }
161
- .search-result {
162
- padding: 10px 16px;
163
- border-bottom: 1px solid var(--border);
164
- cursor: pointer;
165
- transition: background 0.1s;
166
- }
167
- .search-result:hover { background: rgba(88,166,255,0.08); }
168
- .search-result .sr-score {
169
- font-size: 11px; font-weight: 600;
170
- color: var(--green); margin-right: 8px;
171
- }
172
- .search-result .sr-type {
173
- font-size: 11px; color: var(--text-dim);
174
- margin-right: 8px;
175
- }
176
- .search-result .sr-file {
177
- font-size: 12px; color: var(--accent);
178
- }
179
- .search-result .sr-preview {
180
- font-size: 12px; color: var(--text-dim);
181
- margin-top: 4px;
182
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
183
- }
184
-
185
- /* Loading state */
186
- #loading {
187
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
188
- background: var(--bg);
189
- display: flex; align-items: center; justify-content: center;
190
- z-index: 200;
191
- transition: opacity 0.3s;
192
- }
193
- #loading.hidden { opacity: 0; pointer-events: none; }
194
- .spinner {
195
- width: 32px; height: 32px;
196
- border: 3px solid var(--border);
197
- border-top-color: var(--accent);
198
- border-radius: 50%;
199
- animation: spin 0.8s linear infinite;
200
- }
201
- @keyframes spin { to { transform: rotate(360deg); } }
202
-
203
- /* Tooltip */
204
- .tooltip {
205
- position: fixed;
206
- background: var(--surface);
207
- border: 1px solid var(--border);
208
- border-radius: 6px;
209
- padding: 6px 10px;
210
- font-size: 12px;
211
- color: var(--text);
212
- pointer-events: none;
213
- z-index: 150;
214
- max-width: 280px;
215
- white-space: nowrap;
216
- overflow: hidden;
217
- text-overflow: ellipsis;
218
- display: none;
219
- box-shadow: 0 4px 12px rgba(0,0,0,0.4);
220
- }
221
- </style>
222
- </head>
223
- <body>
224
-
225
- <div id="loading"><div class="spinner"></div></div>
226
-
227
- <div id="topbar">
228
- <div class="logo">Trellis <span>Graph Explorer</span></div>
229
- <input id="search-box" type="text" placeholder="Search (semantic)… press Enter" autocomplete="off">
230
- <div class="stats" id="stats"></div>
231
- <div class="legend">
232
- <div class="legend-item"><div class="legend-dot" style="background:var(--accent)"></div> File</div>
233
- <div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> Milestone</div>
234
- <div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div> Issue</div>
235
- <div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div> Branch</div>
236
- </div>
237
- </div>
238
-
239
- <div id="graph"></div>
240
- <div id="sidebar">
241
- <div id="sidebar-header">
242
- <h3 id="sidebar-title"></h3>
243
- <button id="sidebar-close">&times;</button>
244
- </div>
245
- <div id="sidebar-body"></div>
246
- </div>
247
- <div id="search-results"></div>
248
- <div class="tooltip" id="tooltip"></div>
249
-
250
- <script>
251
- (async function() {
252
- // -------------------------------------------------------------------------
253
- // Config
254
- // -------------------------------------------------------------------------
255
- const TYPE_COLORS = {
256
- file: '#58a6ff',
257
- milestone: '#3fb950',
258
- issue: '#d29922',
259
- branch: '#bc8cff',
260
- };
261
- const TYPE_RADIUS = {
262
- file: 4,
263
- milestone: 8,
264
- issue: 7,
265
- branch: 6,
266
- };
267
- const EDGE_COLORS = {
268
- milestone_file: '#3fb95044',
269
- issue_branch: '#d2992244',
270
- wikilink: '#58a6ff44',
271
- causal: '#30363d',
272
- };
273
-
274
- // -------------------------------------------------------------------------
275
- // Fetch graph
276
- // -------------------------------------------------------------------------
277
- let graphData;
278
- try {
279
- const res = await fetch('/api/graph');
280
- graphData = await res.json();
281
- } catch (e) {
282
- document.getElementById('loading').innerHTML =
283
- '<div style="color:var(--red);font-size:14px">Failed to load graph data</div>';
284
- return;
285
- }
286
-
287
- const { nodes, edges } = graphData;
288
- document.getElementById('stats').textContent =
289
- `${nodes.length} nodes · ${edges.length} edges`;
290
-
291
- // -------------------------------------------------------------------------
292
- // D3 Force Simulation
293
- // -------------------------------------------------------------------------
294
- const container = document.getElementById('graph');
295
- const width = container.clientWidth;
296
- const height = container.clientHeight;
297
-
298
- const svg = d3.select('#graph')
299
- .append('svg')
300
- .attr('width', width)
301
- .attr('height', height);
302
-
303
- // Zoom group
304
- const g = svg.append('g');
305
- const zoom = d3.zoom()
306
- .scaleExtent([0.1, 8])
307
- .on('zoom', (e) => g.attr('transform', e.transform));
308
- svg.call(zoom);
309
-
310
- // Build link/node data — D3 mutates these
311
- const simLinks = edges.map(e => ({
312
- source: e.source,
313
- target: e.target,
314
- type: e.type,
315
- label: e.label,
316
- }));
317
- const simNodes = nodes.map(n => ({ ...n }));
318
-
319
- // Create a lookup for quick access
320
- const nodeMap = new Map();
321
- simNodes.forEach(n => nodeMap.set(n.id, n));
322
-
323
- const simulation = d3.forceSimulation(simNodes)
324
- .force('link', d3.forceLink(simLinks).id(d => d.id).distance(60).strength(0.3))
325
- .force('charge', d3.forceManyBody().strength(-80).distanceMax(300))
326
- .force('center', d3.forceCenter(width / 2, height / 2))
327
- .force('collision', d3.forceCollide().radius(d => TYPE_RADIUS[d.type] + 2))
328
- .force('x', d3.forceX(width / 2).strength(0.03))
329
- .force('y', d3.forceY(height / 2).strength(0.03));
330
-
331
- // Edges
332
- const link = g.append('g')
333
- .selectAll('line')
334
- .data(simLinks)
335
- .join('line')
336
- .attr('stroke', d => EDGE_COLORS[d.type] || '#30363d')
337
- .attr('stroke-width', d => d.type === 'wikilink' ? 1.5 : 0.8);
338
-
339
- // Nodes
340
- const node = g.append('g')
341
- .selectAll('circle')
342
- .data(simNodes)
343
- .join('circle')
344
- .attr('r', d => TYPE_RADIUS[d.type] || 4)
345
- .attr('fill', d => TYPE_COLORS[d.type] || '#8b949e')
346
- .attr('stroke', 'none')
347
- .attr('cursor', 'pointer')
348
- .call(drag(simulation));
349
-
350
- // Labels (only for milestones, issues, branches — not files to avoid clutter)
351
- const labels = g.append('g')
352
- .selectAll('text')
353
- .data(simNodes.filter(d => d.type !== 'file'))
354
- .join('text')
355
- .text(d => {
356
- const maxLen = 24;
357
- return d.label.length > maxLen ? d.label.slice(0, maxLen) + '…' : d.label;
358
- })
359
- .attr('font-size', 10)
360
- .attr('fill', d => TYPE_COLORS[d.type] || '#8b949e')
361
- .attr('dx', d => TYPE_RADIUS[d.type] + 4)
362
- .attr('dy', 3)
363
- .attr('pointer-events', 'none')
364
- .attr('opacity', 0.8);
365
-
366
- simulation.on('tick', () => {
367
- link
368
- .attr('x1', d => d.source.x)
369
- .attr('y1', d => d.source.y)
370
- .attr('x2', d => d.target.x)
371
- .attr('y2', d => d.target.y);
372
- node
373
- .attr('cx', d => d.x)
374
- .attr('cy', d => d.y);
375
- labels
376
- .attr('x', d => d.x)
377
- .attr('y', d => d.y);
378
- });
379
-
380
- // Drag behavior
381
- function drag(simulation) {
382
- return d3.drag()
383
- .on('start', (event, d) => {
384
- if (!event.active) simulation.alphaTarget(0.3).restart();
385
- d.fx = d.x; d.fy = d.y;
386
- })
387
- .on('drag', (event, d) => {
388
- d.fx = event.x; d.fy = event.y;
389
- })
390
- .on('end', (event, d) => {
391
- if (!event.active) simulation.alphaTarget(0);
392
- d.fx = null; d.fy = null;
393
- });
394
- }
395
-
396
- // Hide loading
397
- document.getElementById('loading').classList.add('hidden');
398
-
399
- // -------------------------------------------------------------------------
400
- // Tooltip on hover
401
- // -------------------------------------------------------------------------
402
- const tooltip = document.getElementById('tooltip');
403
- node.on('mouseover', (event, d) => {
404
- tooltip.textContent = d.label;
405
- tooltip.style.display = 'block';
406
- tooltip.style.left = (event.clientX + 12) + 'px';
407
- tooltip.style.top = (event.clientY - 8) + 'px';
408
- }).on('mousemove', (event) => {
409
- tooltip.style.left = (event.clientX + 12) + 'px';
410
- tooltip.style.top = (event.clientY - 8) + 'px';
411
- }).on('mouseout', () => {
412
- tooltip.style.display = 'none';
413
- });
414
-
415
- // -------------------------------------------------------------------------
416
- // Click → sidebar detail
417
- // -------------------------------------------------------------------------
418
- const sidebar = document.getElementById('sidebar');
419
- const sidebarTitle = document.getElementById('sidebar-title');
420
- const sidebarBody = document.getElementById('sidebar-body');
421
-
422
- document.getElementById('sidebar-close').addEventListener('click', () => {
423
- sidebar.classList.remove('open');
424
- clearHighlight();
425
- });
426
-
427
- node.on('click', async (event, d) => {
428
- event.stopPropagation();
429
- highlightNode(d);
430
-
431
- sidebarTitle.textContent = d.label;
432
- sidebarBody.innerHTML = '<div style="color:var(--text-dim);font-size:12px">Loading…</div>';
433
- sidebar.classList.add('open');
434
-
435
- try {
436
- const res = await fetch(`/api/node/${encodeURIComponent(d.id)}`);
437
- const detail = await res.json();
438
- renderDetail(detail, d);
439
- } catch {
440
- sidebarBody.innerHTML = '<div style="color:var(--red)">Failed to load</div>';
441
- }
442
- });
443
-
444
- svg.on('click', () => {
445
- sidebar.classList.remove('open');
446
- clearHighlight();
447
- });
448
-
449
- function renderDetail(detail, d) {
450
- let html = '';
451
-
452
- // Type badge
453
- const badgeClass = `badge-${d.type}`;
454
- html += `<div class="detail-section">
455
- <span class="detail-badge ${badgeClass}">${d.type}</span>
456
- </div>`;
457
-
458
- if (detail.type === 'file') {
459
- html += `<div class="detail-section">
460
- <h4>Path</h4>
461
- <div class="detail-meta"><strong>${detail.path}</strong></div>
462
- </div>`;
463
- if (detail.contentHash) {
464
- html += `<div class="detail-section">
465
- <h4>Content Hash</h4>
466
- <div class="detail-meta" style="font-family:monospace;font-size:11px">${detail.contentHash}</div>
467
- </div>`;
468
- }
469
- if (detail.recentOps && detail.recentOps.length > 0) {
470
- html += `<div class="detail-section"><h4>Recent Ops</h4><div class="detail-ops">`;
471
- for (const op of detail.recentOps) {
472
- html += `<div><span class="op-kind">${op.kind}</span> <span class="op-time">${formatTime(op.timestamp)}</span></div>`;
473
- }
474
- html += `</div></div>`;
475
- }
476
- } else if (detail.type === 'milestone') {
477
- html += `<div class="detail-section">
478
- <h4>Message</h4>
479
- <div class="detail-meta"><strong>${detail.message || '(none)'}</strong></div>
480
- </div>`;
481
- if (detail.createdAt) {
482
- html += `<div class="detail-section"><h4>Created</h4>
483
- <div class="detail-meta">${formatTime(detail.createdAt)}</div></div>`;
484
- }
485
- if (detail.affectedFiles && detail.affectedFiles.length > 0) {
486
- html += `<div class="detail-section"><h4>Affected Files (${detail.affectedFiles.length})</h4>
487
- <div class="detail-meta">${detail.affectedFiles.map(f => `<div>${f}</div>`).join('')}</div></div>`;
488
- }
489
- } else if (detail.type === 'issue') {
490
- if (detail.description) {
491
- html += `<div class="detail-section"><h4>Description</h4>
492
- <div class="detail-meta">${detail.description}</div></div>`;
493
- }
494
- html += `<div class="detail-section"><h4>Details</h4><div class="detail-meta">`;
495
- if (detail.status) html += `<div><strong>Status:</strong> ${detail.status}</div>`;
496
- if (detail.priority) html += `<div><strong>Priority:</strong> ${detail.priority}</div>`;
497
- if (detail.assignee) html += `<div><strong>Assignee:</strong> ${detail.assignee}</div>`;
498
- if (detail.labels && detail.labels.length) html += `<div><strong>Labels:</strong> ${detail.labels.join(', ')}</div>`;
499
- if (detail.createdAt) html += `<div><strong>Created:</strong> ${formatTime(detail.createdAt)}</div>`;
500
- if (detail.branchName) html += `<div><strong>Branch:</strong> ${detail.branchName}</div>`;
501
- html += `</div></div>`;
502
- if (detail.criteria && detail.criteria.length > 0) {
503
- html += `<div class="detail-section"><h4>Acceptance Criteria</h4><div class="detail-meta">`;
504
- for (const c of detail.criteria) {
505
- const icon = c.status === 'passed' ? '✓' : c.status === 'failed' ? '✗' : '○';
506
- const color = c.status === 'passed' ? 'var(--green)' : c.status === 'failed' ? 'var(--red)' : 'var(--text-dim)';
507
- html += `<div style="color:${color}">${icon} ${c.description || c.id}</div>`;
508
- }
509
- html += `</div></div>`;
510
- }
511
- } else if (detail.type === 'branch') {
512
- html += `<div class="detail-section"><h4>Details</h4><div class="detail-meta">`;
513
- html += `<div><strong>Name:</strong> ${detail.name}</div>`;
514
- if (detail.isCurrent) html += `<div><strong>Current:</strong> yes</div>`;
515
- if (detail.createdAt) html += `<div><strong>Created:</strong> ${formatTime(detail.createdAt)}</div>`;
516
- html += `</div></div>`;
517
- }
518
-
519
- sidebarBody.innerHTML = html;
520
- }
521
-
522
- function formatTime(iso) {
523
- if (!iso) return '';
524
- const d = new Date(iso);
525
- const now = Date.now();
526
- const diff = now - d.getTime();
527
- if (diff < 60000) return 'just now';
528
- if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
529
- if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
530
- return Math.floor(diff / 86400000) + 'd ago';
531
- }
532
-
533
- // -------------------------------------------------------------------------
534
- // Highlight
535
- // -------------------------------------------------------------------------
536
- let highlightedId = null;
537
-
538
- function highlightNode(d) {
539
- highlightedId = d.id;
540
- const connected = new Set();
541
- connected.add(d.id);
542
- simLinks.forEach(l => {
543
- const sid = typeof l.source === 'object' ? l.source.id : l.source;
544
- const tid = typeof l.target === 'object' ? l.target.id : l.target;
545
- if (sid === d.id) connected.add(tid);
546
- if (tid === d.id) connected.add(sid);
547
- });
548
-
549
- node.attr('opacity', n => connected.has(n.id) ? 1 : 0.15);
550
- link.attr('opacity', l => {
551
- const sid = typeof l.source === 'object' ? l.source.id : l.source;
552
- const tid = typeof l.target === 'object' ? l.target.id : l.target;
553
- return (sid === d.id || tid === d.id) ? 1 : 0.05;
554
- });
555
- labels.attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
556
- }
557
-
558
- function clearHighlight() {
559
- highlightedId = null;
560
- node.attr('opacity', 1);
561
- link.attr('opacity', 1);
562
- labels.attr('opacity', 0.8);
563
- }
564
-
565
- // -------------------------------------------------------------------------
566
- // Search
567
- // -------------------------------------------------------------------------
568
- const searchBox = document.getElementById('search-box');
569
- const searchResults = document.getElementById('search-results');
570
- let searchTimeout = null;
571
-
572
- searchBox.addEventListener('keydown', (e) => {
573
- if (e.key === 'Enter') {
574
- doSearch(searchBox.value.trim());
575
- }
576
- if (e.key === 'Escape') {
577
- searchResults.classList.remove('visible');
578
- searchBox.blur();
579
- }
580
- });
581
-
582
- async function doSearch(query) {
583
- if (!query) {
584
- searchResults.classList.remove('visible');
585
- return;
586
- }
587
-
588
- searchResults.innerHTML = '<div style="padding:16px;color:var(--text-dim);font-size:12px">Searching…</div>';
589
- searchResults.classList.add('visible');
590
-
591
- try {
592
- const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=15`);
593
- const data = await res.json();
594
-
595
- if (data.message) {
596
- searchResults.innerHTML = `<div style="padding:16px;color:var(--text-dim);font-size:12px">${data.message}</div>`;
597
- return;
598
- }
599
-
600
- if (!data.results || data.results.length === 0) {
601
- searchResults.innerHTML = '<div style="padding:16px;color:var(--text-dim);font-size:12px">No results</div>';
602
- return;
603
- }
604
-
605
- // Highlight matching nodes on the graph
606
- const matchedFiles = new Set(data.results.filter(r => r.filePath).map(r => `file:${r.filePath}`));
607
- node.attr('opacity', n => matchedFiles.size === 0 || matchedFiles.has(n.id) ? 1 : 0.15);
608
- link.attr('opacity', 0.1);
609
- labels.attr('opacity', n => matchedFiles.has(n.id) ? 1 : 0.1);
610
-
611
- searchResults.innerHTML = data.results.map(r => {
612
- const score = (r.score * 100).toFixed(1);
613
- const preview = (r.content || '').slice(0, 120).replace(/</g, '&lt;');
614
- return `<div class="search-result" data-file="${r.filePath || ''}" data-entity="${r.entityId || ''}">
615
- <span class="sr-score">${score}%</span>
616
- <span class="sr-type">[${r.chunkType}]</span>
617
- ${r.filePath ? `<span class="sr-file">${r.filePath}</span>` : ''}
618
- <div class="sr-preview">${preview}</div>
619
- </div>`;
620
- }).join('');
621
-
622
- // Click search result → focus that node
623
- searchResults.querySelectorAll('.search-result').forEach(el => {
624
- el.addEventListener('click', () => {
625
- const fileId = `file:${el.dataset.file}`;
626
- const n = nodeMap.get(fileId);
627
- if (n) {
628
- highlightNode(n);
629
- // Pan to node
630
- const transform = d3.zoomTransform(svg.node());
631
- const x = transform.applyX(n.x);
632
- const y = transform.applyY(n.y);
633
- svg.transition().duration(500).call(
634
- zoom.translateTo, n.x, n.y
635
- );
636
- }
637
- searchResults.classList.remove('visible');
638
- });
639
- });
640
- } catch (err) {
641
- searchResults.innerHTML = `<div style="padding:16px;color:var(--red);font-size:12px">Search failed: ${err.message}</div>`;
642
- }
643
- }
644
-
645
- // Click outside search results to close
646
- document.addEventListener('click', (e) => {
647
- if (!searchResults.contains(e.target) && e.target !== searchBox) {
648
- searchResults.classList.remove('visible');
649
- }
650
- });
651
-
652
- // -------------------------------------------------------------------------
653
- // Keyboard shortcuts
654
- // -------------------------------------------------------------------------
655
- document.addEventListener('keydown', (e) => {
656
- // "/" to focus search
657
- if (e.key === '/' && document.activeElement !== searchBox) {
658
- e.preventDefault();
659
- searchBox.focus();
660
- }
661
- // Escape to clear
662
- if (e.key === 'Escape') {
663
- sidebar.classList.remove('open');
664
- searchResults.classList.remove('visible');
665
- clearHighlight();
666
- }
667
- });
668
-
669
- // -------------------------------------------------------------------------
670
- // Fit to viewport on load
671
- // -------------------------------------------------------------------------
672
- simulation.on('end', () => {
673
- // Auto-fit after simulation settles
674
- const bounds = g.node().getBBox();
675
- if (bounds.width === 0 || bounds.height === 0) return;
676
- const pad = 60;
677
- const scale = Math.min(
678
- (width - pad * 2) / bounds.width,
679
- (height - pad * 2) / bounds.height,
680
- 2
681
- );
682
- const cx = bounds.x + bounds.width / 2;
683
- const cy = bounds.y + bounds.height / 2;
684
- svg.transition().duration(750).call(
685
- zoom.transform,
686
- d3.zoomIdentity
687
- .translate(width / 2, height / 2)
688
- .scale(scale)
689
- .translate(-cx, -cy)
690
- );
691
- });
692
- })();
693
- </script>
694
- </body>
695
- </html>