@reqlan/analytical 0.8.2 → 0.9.1

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 (50) hide show
  1. package/README.md +14 -0
  2. package/out/analysis/contextual-search.d.ts +26 -0
  3. package/out/analysis/contextual-search.js +94 -0
  4. package/out/analysis/contextual-search.js.map +1 -0
  5. package/out/analysis/fuzzy-search.d.ts +24 -0
  6. package/out/analysis/fuzzy-search.js +254 -0
  7. package/out/analysis/fuzzy-search.js.map +1 -1
  8. package/out/analysis/git-dates-analyser.d.ts +3 -0
  9. package/out/analysis/git-dates-analyser.js +32 -13
  10. package/out/analysis/git-dates-analyser.js.map +1 -1
  11. package/out/analysis-api.d.ts +5 -1
  12. package/out/analysis-api.js +12 -3
  13. package/out/analysis-api.js.map +1 -1
  14. package/out/core/base-discovery.js +10 -3
  15. package/out/core/base-discovery.js.map +1 -1
  16. package/out/core/context-signals.d.ts +5 -0
  17. package/out/core/context-signals.js +13 -4
  18. package/out/core/context-signals.js.map +1 -1
  19. package/out/core/file-reference-resolve.js +6 -2
  20. package/out/core/file-reference-resolve.js.map +1 -1
  21. package/out/core/types.d.ts +8 -0
  22. package/out/core/types.js.map +1 -1
  23. package/out/core/workspace-paths.d.ts +1 -0
  24. package/out/core/workspace-paths.js +9 -5
  25. package/out/core/workspace-paths.js.map +1 -1
  26. package/out/export/html-export-assets.d.ts +1 -1
  27. package/out/export/html-export-assets.js +388 -20
  28. package/out/export/html-export-assets.js.map +1 -1
  29. package/out/export/html-export-template.js +15 -0
  30. package/out/export/html-export-template.js.map +1 -1
  31. package/out/index-store/file-treatment.d.ts +40 -0
  32. package/out/index-store/file-treatment.js +144 -0
  33. package/out/index-store/file-treatment.js.map +1 -0
  34. package/out/index-store/schema.d.ts +1 -1
  35. package/out/index-store/schema.js +6 -2
  36. package/out/index-store/schema.js.map +1 -1
  37. package/out/index-store/sqlite-store.d.ts +4 -2
  38. package/out/index-store/sqlite-store.js +39 -21
  39. package/out/index-store/sqlite-store.js.map +1 -1
  40. package/out/index-store/webview-graph-queries.d.ts +2 -0
  41. package/out/index-store/webview-graph-queries.js.map +1 -1
  42. package/out/index-store/webview-table-queries.d.ts +1 -1
  43. package/out/index-store/webview-table-queries.js +9 -0
  44. package/out/index-store/webview-table-queries.js.map +1 -1
  45. package/out/index-store/workspace-index-file.js +2 -6
  46. package/out/index-store/workspace-index-file.js.map +1 -1
  47. package/out/index.d.ts +8 -4
  48. package/out/index.js +4 -2
  49. package/out/index.js.map +1 -1
  50. package/package.json +10 -2
@@ -309,6 +309,27 @@ a.pill:hover { color: var(--accent-strong); border-color: color-mix(in srgb, var
309
309
  border-color: var(--rust);
310
310
  color: var(--fg);
311
311
  }
312
+ .graph-file-treatment {
313
+ display: inline-flex;
314
+ align-items: center;
315
+ }
316
+ .graph-file-treatment .graph-select,
317
+ select.graph-action {
318
+ border-radius: 8px;
319
+ padding-right: 1.6rem;
320
+ cursor: pointer;
321
+ }
322
+ .visually-hidden {
323
+ position: absolute;
324
+ width: 1px;
325
+ height: 1px;
326
+ padding: 0;
327
+ margin: -1px;
328
+ overflow: hidden;
329
+ clip: rect(0, 0, 0, 0);
330
+ white-space: nowrap;
331
+ border: 0;
332
+ }
312
333
  .graph-status {
313
334
  color: var(--muted);
314
335
  font-size: 0.92rem;
@@ -1200,6 +1221,80 @@ const GRAPH_HIT_PAD = 6;
1200
1221
  const GRAPH_LABEL_FADE_START = 0.62;
1201
1222
  const GRAPH_LABEL_FADE_END = 0.82;
1202
1223
  const GRAPH_LABEL_MODES = ['auto', 'on', 'off'];
1224
+ const FILE_TREATMENT_MODES = ['invisible', 'compound', 'linked'];
1225
+
1226
+ function fileIdeasetNodeId(fileUri) {
1227
+ return 'rq-file:' + fileUri;
1228
+ }
1229
+
1230
+ function isFileIdeasetNode(node) {
1231
+ return Boolean(node && (node.isFileIdeaset || String(node.id || '').startsWith('rq-file:')));
1232
+ }
1233
+
1234
+ function fileIdeasetDisplayName(fileUri) {
1235
+ const trimmed = String(fileUri || '').replace(/\\\\/g, '/');
1236
+ const slash = trimmed.lastIndexOf('/');
1237
+ const fileName = slash >= 0 ? trimmed.slice(slash + 1) : trimmed;
1238
+ return fileName.endsWith('.rq') ? fileName.slice(0, -3) : fileName;
1239
+ }
1240
+
1241
+ function fileTreatmentLabel(mode) {
1242
+ if (mode === 'compound') return 'Files: compound';
1243
+ if (mode === 'linked') return 'Files: linked';
1244
+ return 'Files: hidden';
1245
+ }
1246
+
1247
+ function applyFileTreatment(graph, treatment) {
1248
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
1249
+ const edges = Array.isArray(graph.edges) ? graph.edges : [];
1250
+ const baseNodes = nodes.filter(node => !isFileIdeasetNode(node));
1251
+ const keep = new Set(baseNodes.map(node => node.id));
1252
+ const baseEdges = edges.filter(edge => keep.has(edge.sourceId) && keep.has(edge.targetId));
1253
+ if (treatment !== 'linked') {
1254
+ return { ...graph, nodes: baseNodes, edges: baseEdges };
1255
+ }
1256
+ const membersByFile = new Map();
1257
+ for (const node of baseNodes) {
1258
+ if (node.isExternal || !node.fileUri) continue;
1259
+ const list = membersByFile.get(node.fileUri) || [];
1260
+ list.push(node);
1261
+ membersByFile.set(node.fileUri, list);
1262
+ }
1263
+ const extraNodes = [];
1264
+ const extraEdges = [];
1265
+ for (const fileUri of [...membersByFile.keys()].sort()) {
1266
+ const members = membersByFile.get(fileUri) || [];
1267
+ if (!members.length) continue;
1268
+ const id = fileIdeasetNodeId(fileUri);
1269
+ const name = fileIdeasetDisplayName(fileUri);
1270
+ extraNodes.push({
1271
+ id,
1272
+ name,
1273
+ kind: 'ideaset',
1274
+ fileUri,
1275
+ lineStart: 0,
1276
+ tags: [],
1277
+ isFileIdeaset: true,
1278
+ pageUrl: members.find(member => member.hostFilePageUrl)?.hostFilePageUrl
1279
+ || members.find(member => member.pageUrl)?.pageUrl
1280
+ || undefined
1281
+ });
1282
+ for (const member of members) {
1283
+ extraEdges.push({
1284
+ id: id + '->ideaset_member:' + member.id,
1285
+ sourceId: id,
1286
+ targetId: member.id,
1287
+ kind: 'ideaset_member',
1288
+ label: name
1289
+ });
1290
+ }
1291
+ }
1292
+ return {
1293
+ ...graph,
1294
+ nodes: baseNodes.concat(extraNodes),
1295
+ edges: baseEdges.concat(extraEdges)
1296
+ };
1297
+ }
1203
1298
 
1204
1299
  function wireGraph(root) {
1205
1300
  const graphRoots = root.querySelectorAll('[data-graph-json]');
@@ -1227,6 +1322,7 @@ function wireGraph(root) {
1227
1322
  const tagHost = controls.querySelector('[data-graph-tag-scd]');
1228
1323
  const toggleExternal = controls.querySelector('[data-graph-toggle-external]');
1229
1324
  const toggleIdeasets = controls.querySelector('[data-graph-toggle-ideasets]');
1325
+ const fileTreatmentSelect = controls.querySelector('[data-graph-file-treatment]');
1230
1326
  const toggleWildcard = controls.querySelector('[data-graph-toggle-wildcard]');
1231
1327
  const toggleLabels = controls.querySelector('[data-graph-toggle-labels]');
1232
1328
  const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');
@@ -1237,6 +1333,7 @@ function wireGraph(root) {
1237
1333
  element.classList.add('is-booting');
1238
1334
  let hideExternal = false;
1239
1335
  let hideIdeasets = false;
1336
+ let fileTreatment = fileTreatmentSelect?.value || 'linked';
1240
1337
  let includeWildcardRefs = true;
1241
1338
  let labelMode = 'auto';
1242
1339
  let statusSelected = [];
@@ -1253,6 +1350,10 @@ function wireGraph(root) {
1253
1350
  tagSelected = tagFilter.getSelected();
1254
1351
  let livePhysics = false;
1255
1352
  let animationFrame = 0;
1353
+ let settleFrame = 0;
1354
+ let settleGeneration = 0;
1355
+ // While true, auto labels stay at opacity 0 so text paint does not lag settle.
1356
+ let settlingLayout = false;
1256
1357
  let calmTicks = 0;
1257
1358
  let simNodes = [];
1258
1359
  let simEdges = [];
@@ -1266,12 +1367,15 @@ function wireGraph(root) {
1266
1367
  let view = { x: 0, y: 0, w: 1200, h: 640 };
1267
1368
  let userViewport = false;
1268
1369
  let dragNodeId = null;
1370
+ let dragCompound = null;
1269
1371
  let dragMoved = false;
1270
1372
  let panMode = false;
1271
1373
  let panOrigin = null;
1272
1374
  let suppressClickUntil = 0;
1273
1375
  let hoverNodeId = null;
1376
+ let hoverCompound = null;
1274
1377
  let pulsePhase = 0;
1378
+ let compoundRegions = [];
1275
1379
 
1276
1380
  function resolveGraphNodeUrl(pageUrl) {
1277
1381
  let cleaned = String(pageUrl || '').replace(/^\\.\\//, '');
@@ -1493,12 +1597,93 @@ function wireGraph(root) {
1493
1597
  ctx.fillRect(0, 0, cssWidth, cssHeight);
1494
1598
 
1495
1599
  const { scale, offsetX, offsetY } = viewportTransform();
1496
- const ambientLabelOpacity = labelOpacityAtScale(scale);
1600
+ // Auto labels stay invisible during batch settle ([graph_label_auto] / html_export_graph_label_auto).
1601
+ const ambientLabelOpacity =
1602
+ settlingLayout && labelMode === 'auto' ? 0 : labelOpacityAtScale(scale);
1497
1603
  ctx.save();
1498
1604
  ctx.translate(offsetX, offsetY);
1499
1605
  ctx.scale(scale, scale);
1500
1606
  ctx.translate(-view.x, -view.y);
1501
1607
 
1608
+ if (fileTreatment === 'compound') {
1609
+ compoundRegions = [];
1610
+ const byFile = new Map();
1611
+ for (const node of simNodes) {
1612
+ if (node.isExternal || isFileIdeasetNode(node) || !node.fileUri) continue;
1613
+ const list = byFile.get(node.fileUri) || [];
1614
+ list.push(node);
1615
+ byFile.set(node.fileUri, list);
1616
+ }
1617
+ for (const [fileUri, members] of byFile) {
1618
+ let minX = Infinity;
1619
+ let minY = Infinity;
1620
+ let maxX = -Infinity;
1621
+ let maxY = -Infinity;
1622
+ let any = false;
1623
+ let pageUrl;
1624
+ for (const node of members) {
1625
+ const pos = positions.get(node.id);
1626
+ if (!pos) continue;
1627
+ const r = nodeRadius(node) + 14;
1628
+ any = true;
1629
+ minX = Math.min(minX, pos.x - r);
1630
+ minY = Math.min(minY, pos.y - r);
1631
+ maxX = Math.max(maxX, pos.x + r);
1632
+ maxY = Math.max(maxY, pos.y + r);
1633
+ pageUrl = pageUrl || node.hostFilePageUrl || node.pageUrl;
1634
+ }
1635
+ if (!any) continue;
1636
+ const pad = 8;
1637
+ const x = minX - pad;
1638
+ const y = minY - pad;
1639
+ const w = (maxX - minX) + pad * 2;
1640
+ const h = (maxY - minY) + pad * 2;
1641
+ const titleH = 18;
1642
+ const label = fileIdeasetDisplayName(fileUri);
1643
+ compoundRegions.push({
1644
+ fileUri,
1645
+ pageUrl,
1646
+ memberIds: members.map(member => member.id),
1647
+ x,
1648
+ y,
1649
+ w,
1650
+ h,
1651
+ titleY: y - titleH,
1652
+ titleH,
1653
+ label
1654
+ });
1655
+ const radius = 10;
1656
+ const hover = hoverCompound && hoverCompound.fileUri === fileUri;
1657
+ ctx.beginPath();
1658
+ ctx.moveTo(x + radius, y);
1659
+ ctx.arcTo(x + w, y, x + w, y + h, radius);
1660
+ ctx.arcTo(x + w, y + h, x, y + h, radius);
1661
+ ctx.arcTo(x, y + h, x, y, radius);
1662
+ ctx.arcTo(x, y, x + w, y, radius);
1663
+ ctx.closePath();
1664
+ ctx.fillStyle = hover ? 'rgba(209, 134, 22, 0.14)' : 'rgba(209, 134, 22, 0.08)';
1665
+ ctx.fill();
1666
+ ctx.strokeStyle = hover ? 'rgba(224, 162, 74, 0.85)' : 'rgba(209, 134, 22, 0.45)';
1667
+ ctx.lineWidth = (hover ? 1.8 : 1.2) / scale;
1668
+ ctx.stroke();
1669
+ ctx.fillStyle = 'rgba(224, 162, 74, 0.95)';
1670
+ ctx.font = \`600 \${11 / scale}px "IBM Plex Sans", system-ui, sans-serif\`;
1671
+ ctx.textAlign = 'left';
1672
+ ctx.textBaseline = 'bottom';
1673
+ ctx.fillText(label, x + 6, y - 4 / scale);
1674
+ // Underline title to signal clickability.
1675
+ const titleWidth = ctx.measureText(label).width;
1676
+ ctx.beginPath();
1677
+ ctx.moveTo(x + 6, y - 2 / scale);
1678
+ ctx.lineTo(x + 6 + titleWidth, y - 2 / scale);
1679
+ ctx.strokeStyle = 'rgba(224, 162, 74, 0.85)';
1680
+ ctx.lineWidth = 1 / scale;
1681
+ ctx.stroke();
1682
+ }
1683
+ } else {
1684
+ compoundRegions = [];
1685
+ }
1686
+
1502
1687
  ctx.lineWidth = 1.1 / scale;
1503
1688
  ctx.strokeStyle = 'rgba(61,47,40,0.85)';
1504
1689
  ctx.globalAlpha = 0.85;
@@ -1585,6 +1770,14 @@ function wireGraph(root) {
1585
1770
  }
1586
1771
  }
1587
1772
 
1773
+ function cancelSettle() {
1774
+ settleGeneration += 1;
1775
+ if (settleFrame) {
1776
+ cancelAnimationFrame(settleFrame);
1777
+ settleFrame = 0;
1778
+ }
1779
+ }
1780
+
1588
1781
  function scheduleTick() {
1589
1782
  if (animationFrame) return;
1590
1783
  animationFrame = requestAnimationFrame(() => {
@@ -1612,16 +1805,54 @@ function wireGraph(root) {
1612
1805
  if (livePhysics) scheduleTick();
1613
1806
  }
1614
1807
 
1615
- function batchSettle(nodes, edges) {
1808
+ function settleChunkSize(nodeCount) {
1809
+ if (nodeCount > 400) return 1;
1810
+ if (nodeCount > 200) return 2;
1811
+ if (nodeCount > 80) return 4;
1812
+ return 8;
1813
+ }
1814
+
1815
+ function batchSettleAsync(nodes, edges, onDone) {
1816
+ cancelSettle();
1817
+ const generation = settleGeneration;
1616
1818
  const iterations = Math.min(96, 36 + Math.floor(nodes.length * 0.45));
1617
- for (let i = 0; i < iterations; i += 1) {
1618
- physicsStep(nodes, edges);
1819
+ const chunkSize = settleChunkSize(nodes.length);
1820
+ let step = 0;
1821
+
1822
+ function finish() {
1823
+ if (generation !== settleGeneration) return;
1824
+ for (const velocity of velocities.values()) {
1825
+ velocity.vx = 0;
1826
+ velocity.vy = 0;
1827
+ }
1828
+ calmTicks = EXPORT_PHYSICS_SETTINGS.restTicks;
1829
+ settleFrame = 0;
1830
+ onDone();
1831
+ }
1832
+
1833
+ function runChunk() {
1834
+ if (generation !== settleGeneration) return;
1835
+ const end = Math.min(step + chunkSize, iterations);
1836
+ for (; step < end; step += 1) {
1837
+ physicsStep(nodes, edges);
1838
+ }
1839
+ paint();
1840
+ if (statusText && iterations > 0) {
1841
+ const pct = Math.min(99, Math.round((step / iterations) * 100));
1842
+ statusText.textContent = \`Settling layout… \${pct}%\`;
1843
+ }
1844
+ if (step >= iterations) {
1845
+ finish();
1846
+ return;
1847
+ }
1848
+ settleFrame = requestAnimationFrame(runChunk);
1619
1849
  }
1620
- for (const velocity of velocities.values()) {
1621
- velocity.vx = 0;
1622
- velocity.vy = 0;
1850
+
1851
+ if (iterations <= 0) {
1852
+ finish();
1853
+ return;
1623
1854
  }
1624
- calmTicks = EXPORT_PHYSICS_SETTINGS.restTicks;
1855
+ settleFrame = requestAnimationFrame(runChunk);
1625
1856
  }
1626
1857
 
1627
1858
  function hitTest(point) {
@@ -1642,6 +1873,29 @@ function wireGraph(root) {
1642
1873
  return best;
1643
1874
  }
1644
1875
 
1876
+ function hitTestCompound(point) {
1877
+ // Prefer title hits, then body (for drag).
1878
+ let bodyHit = null;
1879
+ for (let i = compoundRegions.length - 1; i >= 0; i -= 1) {
1880
+ const region = compoundRegions[i];
1881
+ const inTitle = point.x >= region.x
1882
+ && point.x <= region.x + region.w
1883
+ && point.y >= region.titleY
1884
+ && point.y <= region.y + 2;
1885
+ if (inTitle) {
1886
+ return { region, titleHit: true };
1887
+ }
1888
+ const inBody = point.x >= region.x
1889
+ && point.x <= region.x + region.w
1890
+ && point.y >= region.y
1891
+ && point.y <= region.y + region.h;
1892
+ if (inBody && !bodyHit) {
1893
+ bodyHit = { region, titleHit: false };
1894
+ }
1895
+ }
1896
+ return bodyHit;
1897
+ }
1898
+
1645
1899
  function resizeCanvas() {
1646
1900
  const rect = element.getBoundingClientRect();
1647
1901
  cssWidth = Math.max(320, Math.floor(rect.width || 1200));
@@ -1654,6 +1908,7 @@ function wireGraph(root) {
1654
1908
 
1655
1909
  function mountGraph(filteredNodes, filteredEdges) {
1656
1910
  stopLoop();
1911
+ cancelSettle();
1657
1912
  simNodes = filteredNodes;
1658
1913
  simEdges = filteredEdges;
1659
1914
  const validIds = new Set(filteredNodes.map(node => node.id));
@@ -1670,15 +1925,22 @@ function wireGraph(root) {
1670
1925
  element.appendChild(canvas);
1671
1926
  resizeCanvas();
1672
1927
  wireViewport(canvas);
1673
- if (statusText) {
1674
- statusText.textContent = \`\${filteredNodes.length} nodes, \${filteredEdges.length} edges\`;
1675
- }
1676
1928
  userViewport = false;
1929
+ const finishStatus = () => {
1930
+ settlingLayout = false;
1931
+ if (statusText) {
1932
+ statusText.textContent = \`\${filteredNodes.length} nodes, \${filteredEdges.length} edges\`;
1933
+ }
1934
+ paint();
1935
+ };
1677
1936
  if (!livePhysics) {
1678
- batchSettle(filteredNodes, filteredEdges);
1937
+ settlingLayout = true;
1938
+ if (statusText) statusText.textContent = 'Settling layout…';
1679
1939
  paint();
1940
+ batchSettleAsync(filteredNodes, filteredEdges, finishStatus);
1680
1941
  } else {
1681
- paint();
1942
+ settlingLayout = false;
1943
+ finishStatus();
1682
1944
  wake();
1683
1945
  }
1684
1946
  }
@@ -1713,6 +1975,7 @@ function wireGraph(root) {
1713
1975
  const hit = hitTest(point);
1714
1976
  if (hit) {
1715
1977
  dragNodeId = hit.id;
1978
+ dragCompound = null;
1716
1979
  dragMoved = false;
1717
1980
  pinnedIds.add(hit.id);
1718
1981
  velocities.set(hit.id, { vx: 0, vy: 0 });
@@ -1722,6 +1985,31 @@ function wireGraph(root) {
1722
1985
  paint();
1723
1986
  return;
1724
1987
  }
1988
+ const compoundHit = hitTestCompound(point);
1989
+ if (compoundHit) {
1990
+ dragCompound = {
1991
+ ...compoundHit.region,
1992
+ titleHit: compoundHit.titleHit,
1993
+ origin: point,
1994
+ memberOrigins: Object.fromEntries(
1995
+ compoundHit.region.memberIds.map(id => {
1996
+ const pos = positions.get(id) || { x: 0, y: 0 };
1997
+ return [id, { x: pos.x, y: pos.y }];
1998
+ })
1999
+ )
2000
+ };
2001
+ dragNodeId = null;
2002
+ dragMoved = false;
2003
+ for (const id of compoundHit.region.memberIds) {
2004
+ pinnedIds.add(id);
2005
+ velocities.set(id, { vx: 0, vy: 0 });
2006
+ }
2007
+ target.classList.add('is-dragging-node');
2008
+ target.setPointerCapture(event.pointerId);
2009
+ event.preventDefault();
2010
+ paint();
2011
+ return;
2012
+ }
1725
2013
  panMode = true;
1726
2014
  panOrigin = { x: event.clientX, y: event.clientY, viewX: view.x, viewY: view.y };
1727
2015
  target.classList.add('is-panning');
@@ -1738,6 +2026,22 @@ function wireGraph(root) {
1738
2026
  paint();
1739
2027
  return;
1740
2028
  }
2029
+ if (dragCompound) {
2030
+ const point = clientToGraph(event);
2031
+ const dx = point.x - dragCompound.origin.x;
2032
+ const dy = point.y - dragCompound.origin.y;
2033
+ if (Math.abs(dx) + Math.abs(dy) > 1) {
2034
+ dragMoved = true;
2035
+ }
2036
+ for (const id of dragCompound.memberIds) {
2037
+ const origin = dragCompound.memberOrigins[id];
2038
+ if (!origin) continue;
2039
+ positions.set(id, { x: origin.x + dx, y: origin.y + dy });
2040
+ }
2041
+ wake();
2042
+ paint();
2043
+ return;
2044
+ }
1741
2045
  if (panMode && panOrigin) {
1742
2046
  const rect = target.getBoundingClientRect();
1743
2047
  const { scale } = viewportTransform();
@@ -1749,10 +2053,14 @@ function wireGraph(root) {
1749
2053
  paint();
1750
2054
  return;
1751
2055
  }
1752
- const hit = hitTest(clientToGraph(event));
2056
+ const point = clientToGraph(event);
2057
+ const hit = hitTest(point);
1753
2058
  const nextHover = hit?.id || null;
1754
- if (nextHover !== hoverNodeId) {
2059
+ const compoundHit = hit ? null : hitTestCompound(point);
2060
+ const nextCompound = compoundHit?.region || null;
2061
+ if (nextHover !== hoverNodeId || nextCompound?.fileUri !== hoverCompound?.fileUri) {
1755
2062
  hoverNodeId = nextHover;
2063
+ hoverCompound = nextCompound;
1756
2064
  paint();
1757
2065
  }
1758
2066
  });
@@ -1774,6 +2082,23 @@ function wireGraph(root) {
1774
2082
  }
1775
2083
  return;
1776
2084
  }
2085
+ if (dragCompound) {
2086
+ const compound = dragCompound;
2087
+ const moved = dragMoved;
2088
+ const openTitle = !moved && compound.titleHit;
2089
+ for (const id of compound.memberIds) {
2090
+ pinnedIds.delete(id);
2091
+ }
2092
+ dragCompound = null;
2093
+ wake();
2094
+ paint();
2095
+ if (openTitle && compound.pageUrl && Date.now() > suppressClickUntil) {
2096
+ window.location.href = resolveGraphNodeUrl(compound.pageUrl);
2097
+ } else if (moved) {
2098
+ suppressClickUntil = Date.now() + 250;
2099
+ }
2100
+ return;
2101
+ }
1777
2102
  if (panMode) {
1778
2103
  panMode = false;
1779
2104
  panOrigin = null;
@@ -1788,6 +2113,13 @@ function wireGraph(root) {
1788
2113
  dragNodeId = null;
1789
2114
  wake();
1790
2115
  }
2116
+ if (dragCompound) {
2117
+ for (const id of dragCompound.memberIds) {
2118
+ pinnedIds.delete(id);
2119
+ }
2120
+ dragCompound = null;
2121
+ wake();
2122
+ }
1791
2123
  panMode = false;
1792
2124
  panOrigin = null;
1793
2125
  target.classList.remove('is-panning');
@@ -1805,7 +2137,7 @@ function wireGraph(root) {
1805
2137
  const query = lower(searchInput?.value?.trim());
1806
2138
  const pathQuery = lower(pathInput?.value?.trim());
1807
2139
  if (hideExternal && node.isExternal) return false;
1808
- if (hideIdeasets && node.kind === 'ideaset') return false;
2140
+ if (hideIdeasets && node.kind === 'ideaset' && !isFileIdeasetNode(node)) return false;
1809
2141
  const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');
1810
2142
  if (query && !haystack.includes(query)) return false;
1811
2143
  if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;
@@ -1822,10 +2154,20 @@ function wireGraph(root) {
1822
2154
  return true;
1823
2155
  }
1824
2156
 
2157
+ function syncFilesSelect() {
2158
+ if (!fileTreatmentSelect) return;
2159
+ fileTreatmentSelect.value = fileTreatment;
2160
+ const selected = fileTreatmentSelect.selectedOptions?.[0];
2161
+ if (selected?.title) {
2162
+ fileTreatmentSelect.title = selected.title;
2163
+ }
2164
+ }
2165
+
1825
2166
  function refresh() {
1826
- const visibleNodes = (graph.nodes || []).filter(matches);
2167
+ const treated = applyFileTreatment(graph, fileTreatment);
2168
+ const visibleNodes = (treated.nodes || []).filter(matches);
1827
2169
  const visibleIds = new Set(visibleNodes.map(node => node.id));
1828
- const visibleEdges = (graph.edges || []).filter(edge => {
2170
+ const visibleEdges = (treated.edges || []).filter(edge => {
1829
2171
  if (!visibleIds.has(edge.sourceId) || !visibleIds.has(edge.targetId)) return false;
1830
2172
  if (!includeWildcardRefs && edge.kind === 'wildcard_reference') return false;
1831
2173
  return true;
@@ -1849,6 +2191,11 @@ function wireGraph(root) {
1849
2191
  toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';
1850
2192
  refresh();
1851
2193
  });
2194
+ fileTreatmentSelect?.addEventListener('change', () => {
2195
+ fileTreatment = fileTreatmentSelect.value || 'invisible';
2196
+ syncFilesSelect();
2197
+ refresh();
2198
+ });
1852
2199
  toggleWildcard?.addEventListener('click', () => {
1853
2200
  includeWildcardRefs = !includeWildcardRefs;
1854
2201
  toggleWildcard.classList.toggle('is-active', includeWildcardRefs);
@@ -1866,6 +2213,10 @@ function wireGraph(root) {
1866
2213
  togglePhysics.classList.toggle('is-active', livePhysics);
1867
2214
  togglePhysics.setAttribute('aria-pressed', livePhysics ? 'true' : 'false');
1868
2215
  if (livePhysics) {
2216
+ cancelSettle();
2217
+ if (statusText) {
2218
+ statusText.textContent = \`\${simNodes.length} nodes, \${simEdges.length} edges\`;
2219
+ }
1869
2220
  wake();
1870
2221
  } else {
1871
2222
  stopLoop();
@@ -1885,6 +2236,7 @@ function wireGraph(root) {
1885
2236
  tagFilter.clear();
1886
2237
  hideExternal = false;
1887
2238
  hideIdeasets = false;
2239
+ fileTreatment = 'linked';
1888
2240
  includeWildcardRefs = true;
1889
2241
  labelMode = 'auto';
1890
2242
  toggleExternal?.classList.remove('is-active');
@@ -1892,6 +2244,7 @@ function wireGraph(root) {
1892
2244
  toggleWildcard?.classList.add('is-active');
1893
2245
  toggleWildcard?.setAttribute('aria-pressed', 'true');
1894
2246
  syncLabelsButton();
2247
+ syncFilesSelect();
1895
2248
  if (toggleExternal) toggleExternal.textContent = 'Hide external';
1896
2249
  if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';
1897
2250
  positions.clear();
@@ -1901,8 +2254,22 @@ function wireGraph(root) {
1901
2254
  refresh();
1902
2255
  });
1903
2256
  syncLabelsButton();
1904
- refresh();
2257
+ syncFilesSelect();
2258
+ // Defer first mount so page chrome paints before JSON/layout work.
2259
+ requestAnimationFrame(() => {
2260
+ refresh();
2261
+ });
2262
+ }
2263
+ }
2264
+
2265
+ function scheduleBackground(task) {
2266
+ if (typeof requestIdleCallback === 'function') {
2267
+ requestIdleCallback(() => task(), { timeout: 250 });
2268
+ return;
1905
2269
  }
2270
+ requestAnimationFrame(() => {
2271
+ setTimeout(task, 0);
2272
+ });
1906
2273
  }
1907
2274
 
1908
2275
  function boot() {
@@ -1910,8 +2277,9 @@ function boot() {
1910
2277
  // Interactive Status/Tags triggers before heavy table/graph init.
1911
2278
  enhanceScdPlaceholders(root);
1912
2279
  wireTables(root);
1913
- wireGraph(root);
1914
2280
  void wireGlobalSearch(root);
2281
+ // Graph boot + settle run in the background so large exports stay responsive.
2282
+ scheduleBackground(() => wireGraph(root));
1915
2283
  }
1916
2284
 
1917
2285
  if (document.readyState === 'loading') {
@@ -1 +1 @@
1
- {"version":3,"file":"html-export-assets.js","sourceRoot":"","sources":["../../src/export/html-export-assets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAE9E;;;;;;GAMG;AACH,SAAS,sBAAsB;IAC3B,OAAO,2BAA2B,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8lB5B,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;EACpB,sBAAsB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsxCzB,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,SAAkB;IACrD,OAAO,wCAAwC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC;AAC3G,CAAC"}
1
+ {"version":3,"file":"html-export-assets.js","sourceRoot":"","sources":["../../src/export/html-export-assets.ts"],"names":[],"mappings":"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"}
@@ -466,6 +466,14 @@ export function renderGraphPage(snapshot) {
466
466
  ${renderMultiFilterSelect('tag', tags, 'Filter by tag')}
467
467
  <button type="button" class="graph-action" data-graph-toggle-external="true">Hide external</button>
468
468
  <button type="button" class="graph-action" data-graph-toggle-ideasets="true">Hide ideasets</button>
469
+ <label class="graph-file-treatment">
470
+ <span class="visually-hidden">File treatment</span>
471
+ <select class="graph-action graph-select" data-graph-file-treatment aria-label="Hosting .rq file treatment" title="How hosting .rq files appear in the graph">
472
+ <option value="invisible" title="Do not show hosting .rq files — ideas float freely.">Files: hidden</option>
473
+ <option value="compound" title="Draw each hosting .rq file as a container. Drag the box; click the title to open the file.">Files: compound</option>
474
+ <option value="linked" selected title="Show each hosting .rq file as a linked ideaset node. Click the node to open the file.">Files: linked</option>
475
+ </select>
476
+ </label>
469
477
  <button type="button" class="graph-action is-active" data-graph-toggle-wildcard aria-pressed="true" title="Show edges expanded from path+idea wildcard references">Wildcard refs</button>
470
478
  <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
471
479
  <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
@@ -1022,6 +1030,13 @@ function enrichGraphUrls(snapshot, graph, _currentPath) {
1022
1030
  else {
1023
1031
  delete node.attributeKeys;
1024
1032
  }
1033
+ const hostFile = resolveExportFilePage(snapshot, { fileUri: idea.fileUri });
1034
+ if (hostFile && filePageEnabled(snapshot, hostFile.kind)) {
1035
+ node.hostFilePageUrl = hostFile.page.url;
1036
+ }
1037
+ else {
1038
+ delete node.hostFilePageUrl;
1039
+ }
1025
1040
  }
1026
1041
  else {
1027
1042
  const fileTarget = resolveExportFilePage(snapshot, {