This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

stigmergic / container / server.ts
7.7 kB 211 lines
1// ─── Strata Analysis Container Server ──────────────────────────────── 2// HTTP server running inside the Cloudflare Container. 3// Endpoints: 4// POST /analyze — run strata analysis on an island 5// POST /deriveIsland — derive island (connected component) from a lexmin vertex 6// POST /sync — sync vault + carry data 7// GET /health — health check 8 9import { analyze } from "./analysis.ts"; 10import { syncAll, syncVault, syncCarry } from "./sync.ts"; 11import { createServer } from "http"; 12import { 13 execSync, 14} from "child_process"; 15 16const PORT = 8080; 17const CARRY_PATH = process.env.CARRY_PATH || "/data/carry"; 18 19createServer(async (req, res) => { 20 const url = new URL(req.url || "/", `http://localhost:${PORT}`); 21 22 // ── Health ── 23 if (url.pathname === "/health" && req.method === "GET") { 24 // Include data status in health check 25 const { readdirSync, statSync } = await import("fs"); 26 const { join } = await import("path"); 27 let vaultFiles = 0, carryFiles = 0, carryDomains: string[] = []; 28 try { 29 const walk = (dir: string): number => { 30 let count = 0; 31 for (const e of readdirSync(dir, { withFileTypes: true })) { 32 if (e.name.startsWith(".")) continue; 33 if (e.isDirectory()) count += walk(join(dir, e.name)); 34 else if (e.name.endsWith(".md")) count++; 35 } 36 return count; 37 }; 38 vaultFiles = walk("/data/vault"); 39 const carryBase = "/data/carry"; 40 try { 41 for (const d of readdirSync(carryBase)) { 42 if (d.startsWith(".")) continue; 43 const p = join(carryBase, d); 44 if (statSync(p).isDirectory()) { 45 carryDomains.push(d); 46 carryFiles += walk(p); 47 } 48 } 49 } catch {} 50 } catch {} 51 res.writeHead(200, { "Content-Type": "application/json" }); 52 res.end(JSON.stringify({ status: "ok", vaultFiles, carryFiles, carryDomains, carryTokenSet: !!process.env.CARRY_GITHUB_TOKEN })); 53 return; 54 } 55 56 // ── Sync ── 57 if (url.pathname === "/sync" && req.method === "POST") { 58 try { 59 await syncAll(); 60 res.writeHead(200, { "Content-Type": "application/json" }); 61 res.end(JSON.stringify({ ok: true })); 62 } catch (e: any) { 63 res.writeHead(500, { "Content-Type": "application/json" }); 64 res.end(JSON.stringify({ error: e.message })); 65 } 66 return; 67 } 68 69 // ── Derive Islands ── 70 // Input: { connections: Array<{source, target, connectionType, note, did, rkey}>, seed?: string } 71 // - With seed: derive single island containing that vertex 72 // - Without seed: detect all islands (all connected components) 73 // Output: { islands: Array<{id, lexmin, vertices, edges}> } 74 if (url.pathname === "/deriveIsland" && req.method === "POST") { 75 const chunks: Buffer[] = []; 76 for await (const chunk of req) chunks.push(chunk); 77 const body = Buffer.concat(chunks).toString(); 78 79 try { 80 const input = JSON.parse(body); 81 const { connections, seed } = input; 82 83 if (!Array.isArray(connections)) { 84 res.writeHead(400, { "Content-Type": "application/json" }); 85 res.end(JSON.stringify({ error: "connections[] required" })); 86 return; 87 } 88 89 // Build adjacency list 90 const adj = new Map<string, Array<typeof connections[0]>>(); 91 const allVertices = new Set<string>(); 92 for (const conn of connections) { 93 const s = conn.source as string; 94 const t = conn.target as string; 95 if (!adj.has(s)) adj.set(s, []); 96 if (!adj.has(t)) adj.set(t, []); 97 adj.get(s)!.push(conn); 98 adj.get(t)!.push(conn); 99 allVertices.add(s); 100 allVertices.add(t); 101 } 102 103 // BFS from a seed vertex to find one connected component 104 function bfsComponent(start: string): { 105 vertices: string[]; 106 edges: any[]; 107 } { 108 const visited = new Set<string>(); 109 const queue = [start]; 110 const componentEdges: any[] = []; 111 112 while (queue.length > 0) { 113 const current = queue.shift()!; 114 if (visited.has(current)) continue; 115 visited.add(current); 116 117 for (const conn of adj.get(current) || []) { 118 const s = conn.source as string; 119 const t = conn.target as string; 120 componentEdges.push({ 121 uri: `at://${conn.did}/network.cosmik.connection/${conn.rkey}`, 122 did: conn.did, 123 rkey: conn.rkey, 124 source: s, 125 target: t, 126 connectionType: conn.connectionType || "relates", 127 note: conn.note || "", 128 }); 129 if (!visited.has(s)) queue.push(s); 130 if (!visited.has(t)) queue.push(t); 131 } 132 } 133 134 // Dedupe edges 135 const vertexSet = new Set(visited); 136 const edges = componentEdges.filter(e => vertexSet.has(e.source) && vertexSet.has(e.target)); 137 const seenEdges = new Set<string>(); 138 const dedupedEdges = edges.filter(e => { 139 const key = `${e.source}|${e.target}|${e.rkey}`; 140 if (seenEdges.has(key)) return false; 141 seenEdges.add(key); 142 return true; 143 }); 144 145 const sortedVertices = [...visited].sort(); 146 return { vertices: sortedVertices, edges: dedupedEdges }; 147 } 148 149 const { createHash } = await import("crypto"); 150 151 if (seed) { 152 // Single island from seed vertex 153 const { vertices, edges } = bfsComponent(seed); 154 const lexmin = vertices[0]; 155 const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16); 156 res.writeHead(200, { "Content-Type": "application/json" }); 157 res.end(JSON.stringify({ islands: [{ id, lexmin, vertices, edges }] })); 158 } else { 159 // All islands — find all connected components 160 const globalVisited = new Set<string>(); 161 const islands: any[] = []; 162 163 for (const vertex of allVertices) { 164 if (globalVisited.has(vertex)) continue; 165 const { vertices, edges } = bfsComponent(vertex); 166 for (const v of vertices) globalVisited.add(v); 167 const lexmin = vertices[0]; 168 const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16); 169 islands.push({ id, lexmin, vertices, edges }); 170 } 171 172 // Sort by vertex count descending 173 islands.sort((a, b) => b.vertices.length - a.vertices.length); 174 175 res.writeHead(200, { "Content-Type": "application/json" }); 176 res.end(JSON.stringify({ islands })); 177 } 178 } catch (e: any) { 179 console.error("deriveIsland error:", e); 180 res.writeHead(500, { "Content-Type": "application/json" }); 181 res.end(JSON.stringify({ error: e.message })); 182 } 183 return; 184 } 185 186 // ── Analyze ── 187 if (url.pathname === "/analyze" && req.method === "POST") { 188 const chunks: Buffer[] = []; 189 for await (const chunk of req) chunks.push(chunk); 190 const body = Buffer.concat(chunks).toString(); 191 192 try { 193 const input = JSON.parse(body); 194 const result = await analyze(input); 195 res.writeHead(200, { "Content-Type": "application/json" }); 196 res.end(JSON.stringify(result)); 197 } catch (e: any) { 198 console.error("Analysis error:", e); 199 res.writeHead(500, { "Content-Type": "application/json" }); 200 res.end(JSON.stringify({ error: e.message })); 201 } 202 return; 203 } 204 205 res.writeHead(404); 206 res.end("Not found"); 207}).listen(PORT, () => { 208 console.log(`Strata analysis container listening on port ${PORT}`); 209 // Sync on startup 210 syncAll().catch(e => console.error("Initial sync failed:", e)); 211});