This repository has no description
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});