This repository has no description
6.6 kB
184 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 res.writeHead(200);
25 res.end("ok");
26 return;
27 }
28
29 // ── Sync ──
30 if (url.pathname === "/sync" && req.method === "POST") {
31 try {
32 await syncAll();
33 res.writeHead(200, { "Content-Type": "application/json" });
34 res.end(JSON.stringify({ ok: true }));
35 } catch (e: any) {
36 res.writeHead(500, { "Content-Type": "application/json" });
37 res.end(JSON.stringify({ error: e.message }));
38 }
39 return;
40 }
41
42 // ── Derive Islands ──
43 // Input: { connections: Array<{source, target, connectionType, note, did, rkey}>, seed?: string }
44 // - With seed: derive single island containing that vertex
45 // - Without seed: detect all islands (all connected components)
46 // Output: { islands: Array<{id, lexmin, vertices, edges}> }
47 if (url.pathname === "/deriveIsland" && req.method === "POST") {
48 const chunks: Buffer[] = [];
49 for await (const chunk of req) chunks.push(chunk);
50 const body = Buffer.concat(chunks).toString();
51
52 try {
53 const input = JSON.parse(body);
54 const { connections, seed } = input;
55
56 if (!Array.isArray(connections)) {
57 res.writeHead(400, { "Content-Type": "application/json" });
58 res.end(JSON.stringify({ error: "connections[] required" }));
59 return;
60 }
61
62 // Build adjacency list
63 const adj = new Map<string, Array<typeof connections[0]>>();
64 const allVertices = new Set<string>();
65 for (const conn of connections) {
66 const s = conn.source as string;
67 const t = conn.target as string;
68 if (!adj.has(s)) adj.set(s, []);
69 if (!adj.has(t)) adj.set(t, []);
70 adj.get(s)!.push(conn);
71 adj.get(t)!.push(conn);
72 allVertices.add(s);
73 allVertices.add(t);
74 }
75
76 // BFS from a seed vertex to find one connected component
77 function bfsComponent(start: string): {
78 vertices: string[];
79 edges: any[];
80 } {
81 const visited = new Set<string>();
82 const queue = [start];
83 const componentEdges: any[] = [];
84
85 while (queue.length > 0) {
86 const current = queue.shift()!;
87 if (visited.has(current)) continue;
88 visited.add(current);
89
90 for (const conn of adj.get(current) || []) {
91 const s = conn.source as string;
92 const t = conn.target as string;
93 componentEdges.push({
94 uri: `at://${conn.did}/network.cosmik.connection/${conn.rkey}`,
95 did: conn.did,
96 rkey: conn.rkey,
97 source: s,
98 target: t,
99 connectionType: conn.connectionType || "relates",
100 note: conn.note || "",
101 });
102 if (!visited.has(s)) queue.push(s);
103 if (!visited.has(t)) queue.push(t);
104 }
105 }
106
107 // Dedupe edges
108 const vertexSet = new Set(visited);
109 const edges = componentEdges.filter(e => vertexSet.has(e.source) && vertexSet.has(e.target));
110 const seenEdges = new Set<string>();
111 const dedupedEdges = edges.filter(e => {
112 const key = `${e.source}|${e.target}|${e.rkey}`;
113 if (seenEdges.has(key)) return false;
114 seenEdges.add(key);
115 return true;
116 });
117
118 const sortedVertices = [...visited].sort();
119 return { vertices: sortedVertices, edges: dedupedEdges };
120 }
121
122 const { createHash } = await import("crypto");
123
124 if (seed) {
125 // Single island from seed vertex
126 const { vertices, edges } = bfsComponent(seed);
127 const lexmin = vertices[0];
128 const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16);
129 res.writeHead(200, { "Content-Type": "application/json" });
130 res.end(JSON.stringify({ islands: [{ id, lexmin, vertices, edges }] }));
131 } else {
132 // All islands — find all connected components
133 const globalVisited = new Set<string>();
134 const islands: any[] = [];
135
136 for (const vertex of allVertices) {
137 if (globalVisited.has(vertex)) continue;
138 const { vertices, edges } = bfsComponent(vertex);
139 for (const v of vertices) globalVisited.add(v);
140 const lexmin = vertices[0];
141 const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16);
142 islands.push({ id, lexmin, vertices, edges });
143 }
144
145 // Sort by vertex count descending
146 islands.sort((a, b) => b.vertices.length - a.vertices.length);
147
148 res.writeHead(200, { "Content-Type": "application/json" });
149 res.end(JSON.stringify({ islands }));
150 }
151 } catch (e: any) {
152 console.error("deriveIsland error:", e);
153 res.writeHead(500, { "Content-Type": "application/json" });
154 res.end(JSON.stringify({ error: e.message }));
155 }
156 return;
157 }
158
159 // ── Analyze ──
160 if (url.pathname === "/analyze" && req.method === "POST") {
161 const chunks: Buffer[] = [];
162 for await (const chunk of req) chunks.push(chunk);
163 const body = Buffer.concat(chunks).toString();
164
165 try {
166 const input = JSON.parse(body);
167 const result = await analyze(input);
168 res.writeHead(200, { "Content-Type": "application/json" });
169 res.end(JSON.stringify(result));
170 } catch (e: any) {
171 console.error("Analysis error:", e);
172 res.writeHead(500, { "Content-Type": "application/json" });
173 res.end(JSON.stringify({ error: e.message }));
174 }
175 return;
176 }
177
178 res.writeHead(404);
179 res.end("Not found");
180}).listen(PORT, () => {
181 console.log(`Strata analysis container listening on port ${PORT}`);
182 // Sync on startup
183 syncAll().catch(e => console.error("Initial sync failed:", e));
184});