pi-mega-compact 0.11.6 → 0.11.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.
Files changed (75) hide show
  1. package/dist/extensions/dashboard-server/routes-memory-map.js +8 -15
  2. package/dist/extensions/mega-commands.js +65 -5
  3. package/dist/extensions/mega-config.js +6 -0
  4. package/dist/extensions/mega-events/context-handler.js +65 -0
  5. package/dist/src/config/turns.js +2 -1
  6. package/dist/src/memoryGraph/embedding.js +50 -0
  7. package/dist/src/memoryGraph/gates.js +379 -0
  8. package/dist/src/memoryGraph/sources.js +390 -0
  9. package/dist/src/memoryGraph.js +133 -259
  10. package/dist/src/recall.js +136 -1
  11. package/dist/src/store/sqlite/embedding-cache.js +71 -0
  12. package/dist/src/store/sqlite.js +1 -0
  13. package/dist/src/topics/cluster.js +10 -2
  14. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js → AchievementTiles-BESF1HT1.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js.map → AchievementTiles-BESF1HT1.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js → AchievementsTab-8RzNnz8u.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js.map → AchievementsTab-8RzNnz8u.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js → CacheTab-CUIiUgh3.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js.map → CacheTab-CUIiUgh3.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js → ConfigTab-D85fB9HP.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js.map → ConfigTab-D85fB9HP.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js → EventsTab-TCLR81G5.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js.map → EventsTab-TCLR81G5.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js → GameTab-BqneNRYH.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js.map → GameTab-BqneNRYH.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js → MaintenanceTab-LblMg3NB.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js.map → MaintenanceTab-LblMg3NB.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js +2 -0
  29. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js.map +1 -0
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js → MetricsTab-LxNtNzLQ.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js.map → MetricsTab-LxNtNzLQ.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js → OverviewTab-BXKUZbT6.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js.map → OverviewTab-BXKUZbT6.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js → ReposTab-DTooKeAi.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js.map → ReposTab-DTooKeAi.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js → SessionsTab-Devulgm9.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js.map → SessionsTab-Devulgm9.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js → SetupTab-By9eqUoE.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js.map → SetupTab-By9eqUoE.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js → TimeSavedCard-z4M8TDLm.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js.map → TimeSavedCard-z4M8TDLm.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js +2 -0
  43. package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js.map +1 -0
  44. package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js → TurnsTab-m_XzE0H4.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js.map → TurnsTab-m_XzE0H4.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js → index-PRkilPXw.js} +3 -3
  47. package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js.map → index-PRkilPXw.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js → useSSE-CGzgc2IK.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js.map → useSSE-CGzgc2IK.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/index.html +1 -1
  51. package/extensions/dashboard-client/src/memory-map-layout.ts +161 -0
  52. package/extensions/dashboard-client/src/memory-map-shapes.tsx +126 -0
  53. package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +201 -165
  54. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +21 -0
  55. package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +1 -1
  56. package/extensions/dashboard-server/api-contracts/index.ts +2 -1
  57. package/extensions/dashboard-server/api-contracts/memory-map.ts +14 -6
  58. package/extensions/dashboard-server/routes-memory-map.ts +21 -29
  59. package/extensions/mega-commands.ts +63 -6
  60. package/extensions/mega-config.ts +18 -0
  61. package/extensions/mega-events/context-handler.ts +80 -0
  62. package/package.json +1 -1
  63. package/src/config/turns.ts +5 -2
  64. package/src/memoryGraph/embedding.ts +63 -0
  65. package/src/memoryGraph/gates.ts +451 -0
  66. package/src/memoryGraph/sources.ts +484 -0
  67. package/src/memoryGraph.ts +188 -350
  68. package/src/recall.ts +168 -4
  69. package/src/store/sqlite/embedding-cache.ts +81 -0
  70. package/src/store/sqlite.ts +1 -0
  71. package/src/topics/cluster.ts +11 -1
  72. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +0 -2
  73. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +0 -1
  74. package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js +0 -2
  75. package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as e}from"./index-DrktNOoT.js";function R(i={}){const{maxEvents:c=500,maxReconnects:l=5}=i,[f,v]=e.useState([]),[E,n]=e.useState("connecting"),[S,p]=e.useState(0),[m,d]=e.useState(null),o=e.useRef(0),s=e.useRef(null),r=e.useCallback(()=>{s.current&&s.current.close(),n("connecting");const t=new EventSource("/api/events");s.current=t,t.onopen=()=>{n("connected"),o.current=0},t.onmessage=g=>{try{const x=JSON.parse(g.data);v(u=>{const a=[...u,x];return a.length>c?a.slice(-c):a}),p(u=>u+1),d(Date.now())}catch{}},t.onerror=()=>{t.close(),o.current<l?(o.current+=1,n("connecting"),setTimeout(r,1e3)):n("disconnected")}},[c,l]);return e.useEffect(()=>(r(),()=>{s.current?.close()}),[r]),{events:f,status:E,eventCount:S,lastEventAt:m}}export{R as u};
2
- //# sourceMappingURL=useSSE-Bf5LOLSi.js.map
1
+ import{r as e}from"./index-PRkilPXw.js";function R(i={}){const{maxEvents:c=500,maxReconnects:l=5}=i,[f,v]=e.useState([]),[E,n]=e.useState("connecting"),[S,p]=e.useState(0),[m,d]=e.useState(null),o=e.useRef(0),s=e.useRef(null),r=e.useCallback(()=>{s.current&&s.current.close(),n("connecting");const t=new EventSource("/api/events");s.current=t,t.onopen=()=>{n("connected"),o.current=0},t.onmessage=g=>{try{const x=JSON.parse(g.data);v(u=>{const a=[...u,x];return a.length>c?a.slice(-c):a}),p(u=>u+1),d(Date.now())}catch{}},t.onerror=()=>{t.close(),o.current<l?(o.current+=1,n("connecting"),setTimeout(r,1e3)):n("disconnected")}},[c,l]);return e.useEffect(()=>(r(),()=>{s.current?.close()}),[r]),{events:f,status:E,eventCount:S,lastEventAt:m}}export{R as u};
2
+ //# sourceMappingURL=useSSE-CGzgc2IK.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSSE-Bf5LOLSi.js","sources":["../../src/hooks/useSSE.ts"],"sourcesContent":["/**\n * dashboard-client/src/hooks/useSSE.ts — Server-Sent Events hook.\n *\n * Connects to /api/events, provides real-time event stream.\n * SPRINT-B1: basic connection. SPRINT-D1: reconnect with backoff.\n */\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport type { SseEvent } from '../../../dashboard-server/api-contracts';\n\nexport type SseStatus = 'connecting' | 'connected' | 'disconnected' | 'error';\n\nexport interface UseSSEResult {\n events: SseEvent[];\n status: SseStatus;\n eventCount: number;\n lastEventAt: number | null;\n}\n\nexport interface UseSSEOptions {\n /** Max events to keep in memory (ring buffer). */\n maxEvents?: number;\n /** Max reconnect attempts before giving up. */\n maxReconnects?: number;\n}\n\n// SPRINT-D1-REMAINING: exponential backoff (1s → 2s → 4s → max 30s).\n// SPRINT-D1-REMAINING: after 5 failures, set status='disconnected'.\n\nexport function useSSE(options: UseSSEOptions = {}): UseSSEResult {\n const { maxEvents = 500, maxReconnects = 5 } = options;\n const [events, setEvents] = useState<SseEvent[]>([]);\n const [status, setStatus] = useState<SseStatus>('connecting');\n const [eventCount, setEventCount] = useState(0);\n const [lastEventAt, setLastEventAt] = useState<number | null>(null);\n const reconnectCount = useRef(0);\n const sourceRef = useRef<EventSource | null>(null);\n\n const connect = useCallback(() => {\n if (sourceRef.current) {\n sourceRef.current.close();\n }\n setStatus('connecting');\n const source = new EventSource('/api/events');\n sourceRef.current = source;\n\n source.onopen = () => {\n setStatus('connected');\n reconnectCount.current = 0;\n };\n\n source.onmessage = (event) => {\n try {\n const parsed = JSON.parse(event.data) as SseEvent;\n setEvents(prev => {\n const next = [...prev, parsed];\n return next.length > maxEvents ? next.slice(-maxEvents) : next;\n });\n setEventCount(c => c + 1);\n setLastEventAt(Date.now());\n } catch {\n // non-fatal: skip malformed events\n }\n };\n\n source.onerror = () => {\n source.close();\n if (reconnectCount.current < maxReconnects) {\n reconnectCount.current += 1;\n setStatus('connecting');\n // SPRINT-D1: use exponential backoff instead of fixed delay\n setTimeout(connect, 1000);\n } else {\n setStatus('disconnected');\n }\n };\n }, [maxEvents, maxReconnects]);\n\n useEffect(() => {\n connect();\n return () => {\n sourceRef.current?.close();\n };\n }, [connect]);\n\n return { events, status, eventCount, lastEventAt };\n}\n"],"names":["useSSE","options","maxEvents","maxReconnects","events","setEvents","useState","status","setStatus","eventCount","setEventCount","lastEventAt","setLastEventAt","reconnectCount","useRef","sourceRef","connect","useCallback","source","event","parsed","prev","next","c","useEffect"],"mappings":"wCA6BO,SAASA,EAAOC,EAAyB,GAAkB,CAChE,KAAM,CAAE,UAAAC,EAAY,IAAK,cAAAC,EAAgB,GAAMF,EACzC,CAACG,EAAQC,CAAS,EAAIC,EAAAA,SAAqB,CAAA,CAAE,EAC7C,CAACC,EAAQC,CAAS,EAAIF,EAAAA,SAAoB,YAAY,EACtD,CAACG,EAAYC,CAAa,EAAIJ,EAAAA,SAAS,CAAC,EACxC,CAACK,EAAaC,CAAc,EAAIN,EAAAA,SAAwB,IAAI,EAC5DO,EAAiBC,EAAAA,OAAO,CAAC,EACzBC,EAAYD,EAAAA,OAA2B,IAAI,EAE3CE,EAAUC,EAAAA,YAAY,IAAM,CAC5BF,EAAU,SACZA,EAAU,QAAQ,MAAA,EAEpBP,EAAU,YAAY,EACtB,MAAMU,EAAS,IAAI,YAAY,aAAa,EAC5CH,EAAU,QAAUG,EAEpBA,EAAO,OAAS,IAAM,CACpBV,EAAU,WAAW,EACrBK,EAAe,QAAU,CAC3B,EAEAK,EAAO,UAAaC,GAAU,CAC5B,GAAI,CACF,MAAMC,EAAS,KAAK,MAAMD,EAAM,IAAI,EACpCd,EAAUgB,GAAQ,CAChB,MAAMC,EAAO,CAAC,GAAGD,EAAMD,CAAM,EAC7B,OAAOE,EAAK,OAASpB,EAAYoB,EAAK,MAAM,CAACpB,CAAS,EAAIoB,CAC5D,CAAC,EACDZ,EAAca,GAAKA,EAAI,CAAC,EACxBX,EAAe,KAAK,KAAK,CAC3B,MAAQ,CAER,CACF,EAEAM,EAAO,QAAU,IAAM,CACrBA,EAAO,MAAA,EACHL,EAAe,QAAUV,GAC3BU,EAAe,SAAW,EAC1BL,EAAU,YAAY,EAEtB,WAAWQ,EAAS,GAAI,GAExBR,EAAU,cAAc,CAE5B,CACF,EAAG,CAACN,EAAWC,CAAa,CAAC,EAE7BqB,OAAAA,EAAAA,UAAU,KACRR,EAAA,EACO,IAAM,CACXD,EAAU,SAAS,MAAA,CACrB,GACC,CAACC,CAAO,CAAC,EAEL,CAAE,OAAAZ,EAAQ,OAAAG,EAAQ,WAAAE,EAAY,YAAAE,CAAA,CACvC"}
1
+ {"version":3,"file":"useSSE-CGzgc2IK.js","sources":["../../src/hooks/useSSE.ts"],"sourcesContent":["/**\n * dashboard-client/src/hooks/useSSE.ts — Server-Sent Events hook.\n *\n * Connects to /api/events, provides real-time event stream.\n * SPRINT-B1: basic connection. SPRINT-D1: reconnect with backoff.\n */\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport type { SseEvent } from '../../../dashboard-server/api-contracts';\n\nexport type SseStatus = 'connecting' | 'connected' | 'disconnected' | 'error';\n\nexport interface UseSSEResult {\n events: SseEvent[];\n status: SseStatus;\n eventCount: number;\n lastEventAt: number | null;\n}\n\nexport interface UseSSEOptions {\n /** Max events to keep in memory (ring buffer). */\n maxEvents?: number;\n /** Max reconnect attempts before giving up. */\n maxReconnects?: number;\n}\n\n// SPRINT-D1-REMAINING: exponential backoff (1s → 2s → 4s → max 30s).\n// SPRINT-D1-REMAINING: after 5 failures, set status='disconnected'.\n\nexport function useSSE(options: UseSSEOptions = {}): UseSSEResult {\n const { maxEvents = 500, maxReconnects = 5 } = options;\n const [events, setEvents] = useState<SseEvent[]>([]);\n const [status, setStatus] = useState<SseStatus>('connecting');\n const [eventCount, setEventCount] = useState(0);\n const [lastEventAt, setLastEventAt] = useState<number | null>(null);\n const reconnectCount = useRef(0);\n const sourceRef = useRef<EventSource | null>(null);\n\n const connect = useCallback(() => {\n if (sourceRef.current) {\n sourceRef.current.close();\n }\n setStatus('connecting');\n const source = new EventSource('/api/events');\n sourceRef.current = source;\n\n source.onopen = () => {\n setStatus('connected');\n reconnectCount.current = 0;\n };\n\n source.onmessage = (event) => {\n try {\n const parsed = JSON.parse(event.data) as SseEvent;\n setEvents(prev => {\n const next = [...prev, parsed];\n return next.length > maxEvents ? next.slice(-maxEvents) : next;\n });\n setEventCount(c => c + 1);\n setLastEventAt(Date.now());\n } catch {\n // non-fatal: skip malformed events\n }\n };\n\n source.onerror = () => {\n source.close();\n if (reconnectCount.current < maxReconnects) {\n reconnectCount.current += 1;\n setStatus('connecting');\n // SPRINT-D1: use exponential backoff instead of fixed delay\n setTimeout(connect, 1000);\n } else {\n setStatus('disconnected');\n }\n };\n }, [maxEvents, maxReconnects]);\n\n useEffect(() => {\n connect();\n return () => {\n sourceRef.current?.close();\n };\n }, [connect]);\n\n return { events, status, eventCount, lastEventAt };\n}\n"],"names":["useSSE","options","maxEvents","maxReconnects","events","setEvents","useState","status","setStatus","eventCount","setEventCount","lastEventAt","setLastEventAt","reconnectCount","useRef","sourceRef","connect","useCallback","source","event","parsed","prev","next","c","useEffect"],"mappings":"wCA6BO,SAASA,EAAOC,EAAyB,GAAkB,CAChE,KAAM,CAAE,UAAAC,EAAY,IAAK,cAAAC,EAAgB,GAAMF,EACzC,CAACG,EAAQC,CAAS,EAAIC,EAAAA,SAAqB,CAAA,CAAE,EAC7C,CAACC,EAAQC,CAAS,EAAIF,EAAAA,SAAoB,YAAY,EACtD,CAACG,EAAYC,CAAa,EAAIJ,EAAAA,SAAS,CAAC,EACxC,CAACK,EAAaC,CAAc,EAAIN,EAAAA,SAAwB,IAAI,EAC5DO,EAAiBC,EAAAA,OAAO,CAAC,EACzBC,EAAYD,EAAAA,OAA2B,IAAI,EAE3CE,EAAUC,EAAAA,YAAY,IAAM,CAC5BF,EAAU,SACZA,EAAU,QAAQ,MAAA,EAEpBP,EAAU,YAAY,EACtB,MAAMU,EAAS,IAAI,YAAY,aAAa,EAC5CH,EAAU,QAAUG,EAEpBA,EAAO,OAAS,IAAM,CACpBV,EAAU,WAAW,EACrBK,EAAe,QAAU,CAC3B,EAEAK,EAAO,UAAaC,GAAU,CAC5B,GAAI,CACF,MAAMC,EAAS,KAAK,MAAMD,EAAM,IAAI,EACpCd,EAAUgB,GAAQ,CAChB,MAAMC,EAAO,CAAC,GAAGD,EAAMD,CAAM,EAC7B,OAAOE,EAAK,OAASpB,EAAYoB,EAAK,MAAM,CAACpB,CAAS,EAAIoB,CAC5D,CAAC,EACDZ,EAAca,GAAKA,EAAI,CAAC,EACxBX,EAAe,KAAK,KAAK,CAC3B,MAAQ,CAER,CACF,EAEAM,EAAO,QAAU,IAAM,CACrBA,EAAO,MAAA,EACHL,EAAe,QAAUV,GAC3BU,EAAe,SAAW,EAC1BL,EAAU,YAAY,EAEtB,WAAWQ,EAAS,GAAI,GAExBR,EAAU,cAAc,CAE5B,CACF,EAAG,CAACN,EAAWC,CAAa,CAAC,EAE7BqB,OAAAA,EAAAA,UAAU,KACRR,EAAA,EACO,IAAM,CACXD,EAAU,SAAS,MAAA,CACrB,GACC,CAACC,CAAO,CAAC,EAEL,CAAE,OAAAZ,EAAQ,OAAAG,EAAQ,WAAAE,EAAY,YAAAE,CAAA,CACvC"}
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>mega-compact dashboard</title>
7
- <script type="module" crossorigin src="./assets/index-DrktNOoT.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-PRkilPXw.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-CUNn22iV.css">
9
9
  </head>
10
10
  <body>
@@ -0,0 +1,161 @@
1
+ /**
2
+ * memory-map-layout.ts — Force-directed layout engine for MemoryMapTab (D3).
3
+ *
4
+ * Uses Coulomb repulsion + Hooke attraction + center gravity to arrange nodes.
5
+ * No external graph library. Mutates layout in place.
6
+ * Self-contained types: only needs id + edges from graph data.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Types (self-contained — mirrors the subset used by the layout engine)
11
+ // ---------------------------------------------------------------------------
12
+
13
+ interface GraphNodeMin {
14
+ readonly id: string;
15
+ readonly nodeType: string;
16
+ readonly label: string;
17
+ readonly summaryTruncated: string;
18
+ readonly tokenEstimate: number;
19
+ readonly timestamp: number;
20
+ readonly dedupStatus: string | undefined;
21
+ readonly raptorLevel: number;
22
+ readonly topicSummary: string | undefined;
23
+ readonly decisionCount: number;
24
+ readonly textSnippet: string;
25
+ readonly sessionId: string;
26
+ }
27
+
28
+ interface GraphEdgeMin {
29
+ readonly source: string;
30
+ readonly target: string;
31
+ readonly weight: number;
32
+ readonly type: "temporal" | "semantic" | "raptor_parent";
33
+ }
34
+
35
+ export interface Position {
36
+ x: number;
37
+ y: number;
38
+ }
39
+
40
+ export interface LayoutNode extends GraphNodeMin {
41
+ pos: Position;
42
+ vx: number;
43
+ vy: number;
44
+ pinned: boolean;
45
+ }
46
+
47
+ export interface LayoutEdge {
48
+ source: number;
49
+ target: number;
50
+ weight: number;
51
+ type: GraphEdge["type"];
52
+ }
53
+
54
+ export interface Layout {
55
+ nodes: LayoutNode[];
56
+ edges: LayoutEdge[];
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Constants
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const REPULSION = 5_000;
64
+ const ATTRACTION = 0.005;
65
+ const CENTER_GRAVITY = 0.01;
66
+ const DAMPING = 0.9;
67
+ const MIN_VELOCITY = 0.1;
68
+ const SPEED_LIMIT = 50;
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Build
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /** Build initial layout from graph data. */
75
+ export function buildLayout(data: { nodes: GraphNodeMin[]; edges: GraphEdgeMin[] }): Layout {
76
+ const nodes: LayoutNode[] = data.nodes.map((n, i) => {
77
+ const angle = (2 * Math.PI * i) / Math.max(data.nodes.length, 1);
78
+ const radius = 200;
79
+ return {
80
+ ...n,
81
+ pos: { x: Math.cos(angle) * radius + 300, y: Math.sin(angle) * radius + 200 },
82
+ vx: 0,
83
+ vy: 0,
84
+ pinned: false,
85
+ };
86
+ });
87
+
88
+ const nodeIndex = new Map<string, number>();
89
+ nodes.forEach((n, i) => nodeIndex.set(n.id, i));
90
+
91
+ const edges: LayoutEdge[] = data.edges
92
+ .map((e) => {
93
+ const si = nodeIndex.get(e.source);
94
+ const ti = nodeIndex.get(e.target);
95
+ if (si === undefined || ti === undefined || si === ti) return null;
96
+ return { source: si, target: ti, weight: e.weight, type: e.type };
97
+ })
98
+ .filter((e): e is LayoutEdge => e !== null);
99
+
100
+ return { nodes, edges };
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Apply forces
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /** Apply one iteration of the force simulation. Mutates layout in place. */
108
+ export function applyForces(layout: Layout): void {
109
+ const { nodes, edges } = layout;
110
+ const n = nodes.length;
111
+ if (n === 0) return;
112
+
113
+ for (let i = 0; i < n; i++) {
114
+ if (nodes[i].pinned) continue;
115
+ let fx = 0;
116
+ let fy = 0;
117
+
118
+ // Coulomb repulsion
119
+ for (let j = 0; j < n; j++) {
120
+ if (i === j) continue;
121
+ const dx = nodes[i].pos.x - nodes[j].pos.x;
122
+ const dy = nodes[i].pos.y - nodes[j].pos.y;
123
+ const distSq = dx * dx + dy * dy || 1;
124
+ const force = REPULSION / distSq;
125
+ const dist = Math.sqrt(distSq);
126
+ fx += (dx / dist) * force;
127
+ fy += (dy / dist) * force;
128
+ }
129
+
130
+ // Hooke attraction along edges
131
+ for (const e of edges) {
132
+ let other: number;
133
+ if (e.source === i) other = e.target;
134
+ else if (e.target === i) other = e.source;
135
+ else continue;
136
+ const dx = nodes[other].pos.x - nodes[i].pos.x;
137
+ const dy = nodes[other].pos.y - nodes[i].pos.y;
138
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
139
+ fx += dx * ATTRACTION * e.weight;
140
+ fy += dy * ATTRACTION * e.weight;
141
+ }
142
+
143
+ // Center gravity
144
+ fx -= nodes[i].pos.x * CENTER_GRAVITY;
145
+ fy -= nodes[i].pos.y * CENTER_GRAVITY;
146
+
147
+ nodes[i].vx = (nodes[i].vx + fx) * DAMPING;
148
+ nodes[i].vy = (nodes[i].vy + fy) * DAMPING;
149
+
150
+ const speed = Math.sqrt(nodes[i].vx * nodes[i].vx + nodes[i].vy * nodes[i].vy);
151
+ if (speed > SPEED_LIMIT) {
152
+ nodes[i].vx = (nodes[i].vx / speed) * SPEED_LIMIT;
153
+ nodes[i].vy = (nodes[i].vy / speed) * SPEED_LIMIT;
154
+ }
155
+
156
+ if (speed > MIN_VELOCITY) {
157
+ nodes[i].pos.x += nodes[i].vx;
158
+ nodes[i].pos.y += nodes[i].vy;
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * memory-map-shapes.tsx — Node shape rendering utilities for MemoryMapTab (D3).
3
+ *
4
+ * Node shape encodes nodeType: checkpoint = filled circle, turn = hollow circle,
5
+ * turn-content = hollow+ring, memory = diamond.
6
+ * Delegate-shell pattern: thin rendering helpers, no state, no side effects.
7
+ */
8
+ import type React from "react";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export type NodeType = "checkpoint" | "turn" | "turn-content" | "memory";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Constants
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const NODE_COLORS: Record<NodeType, string> = {
21
+ checkpoint: "#6366f1",
22
+ turn: "#58a6ff",
23
+ "turn-content": "#22c55e",
24
+ memory: "#f59e0b",
25
+ };
26
+
27
+ export const NODE_TYPE_LABELS: Record<NodeType, string> = {
28
+ checkpoint: "Checkpoint",
29
+ turn: "Turn",
30
+ "turn-content": "Turn Content",
31
+ memory: "Memory",
32
+ };
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /** SVG path for a diamond centered at (0,0) with given half-width/height. */
39
+ function diamondPath(hw: number, hh: number): string {
40
+ return `M 0,-${hh} L ${hw},0 L 0,${hh} L -${hw},0 Z`;
41
+ }
42
+
43
+ function nodeOpacity(dedupStatus: string | undefined): number {
44
+ if (!dedupStatus || dedupStatus === "active") return 1;
45
+ return 0.5;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Render function
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /** Render an SVG shape for a node based on its nodeType. */
53
+ export function renderNodeShape(
54
+ cx: number,
55
+ cy: number,
56
+ radius: number,
57
+ nodeType: NodeType,
58
+ isSelected: boolean,
59
+ isFiltered: boolean,
60
+ dedupStatus: string | undefined,
61
+ ): React.ReactElement {
62
+ const fill = isFiltered ? NODE_COLORS[nodeType] ?? "#6366f1" : "#d1d5db";
63
+ const strokeColor = isSelected ? "#ef4444" : "none";
64
+ const opacity = nodeOpacity(dedupStatus);
65
+
66
+ switch (nodeType) {
67
+ case "checkpoint":
68
+ return (
69
+ <circle
70
+ cx={cx}
71
+ cy={cy}
72
+ r={isSelected ? radius + 3 : radius}
73
+ fill={fill}
74
+ opacity={opacity}
75
+ stroke={strokeColor}
76
+ strokeWidth={isSelected ? 2 : 0}
77
+ />
78
+ );
79
+ case "turn":
80
+ return (
81
+ <circle
82
+ cx={cx}
83
+ cy={cy}
84
+ r={isSelected ? radius + 3 : radius}
85
+ fill="none"
86
+ stroke={fill}
87
+ strokeWidth={2.5}
88
+ opacity={opacity}
89
+ />
90
+ );
91
+ case "turn-content":
92
+ return (
93
+ <g>
94
+ <circle
95
+ cx={cx}
96
+ cy={cy}
97
+ r={isSelected ? radius + 3 : radius}
98
+ fill="none"
99
+ stroke={fill}
100
+ strokeWidth={2.5}
101
+ opacity={opacity}
102
+ />
103
+ <circle
104
+ cx={cx}
105
+ cy={cy}
106
+ r={Math.max(3, radius * 0.3)}
107
+ fill={fill}
108
+ opacity={opacity}
109
+ />
110
+ </g>
111
+ );
112
+ case "memory":
113
+ const hw = radius * 0.85;
114
+ const hh = radius * 0.85;
115
+ return (
116
+ <path
117
+ d={diamondPath(hw, hh)}
118
+ transform={`translate(${cx}, ${cy})`}
119
+ fill={fill}
120
+ opacity={opacity}
121
+ stroke={strokeColor}
122
+ strokeWidth={isSelected ? 2 : 0}
123
+ />
124
+ );
125
+ }
126
+ }