This repository has no description
0

Configure Feed

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

Route /strata → /island/<hash>, fix island:analyze null guards

- Frontend: /island/<12-hex-hash> replaces /strata?id=X, with legacy redirect
- Worker: SPA fallback includes /island/*, computes islandHash from lexmin
- island:analyze: fix execFileSync import (node:child_process), guard null takeaway/domain

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

author
nandi
co-author
Letta Code
date (May 15, 2026, 4:57 AM UTC) commit 0e91ab0c parent e4fd4c18
+55 -88
+18 -7
src/frontend/app.ts
··· 27 27 28 28 interface Island { 29 29 id: string; 30 + islandHash?: string; 30 31 lexmin?: string; 31 32 vertices?: string[]; 32 33 edges?: IslandEdge[]; ··· 480 481 const islandId = target.dataset.islandId; 481 482 if (!islandId) return; 482 483 const island = islands.find(i => i.id === islandId); 483 - window.location.href = `/strata?id=${encodeURIComponent(island?.recordUri || islandId)}`; 484 + window.location.href = `/island/${encodeURIComponent(island?.islandHash || islandId)}`; 484 485 }); 485 486 }); 486 487 } ··· 614 615 } 615 616 616 617 document.getElementById("island-strata-btn")?.addEventListener("click", async () => { 617 - window.location.href = `/strata?id=${encodeURIComponent(island.recordUri || islandId)}`; 618 + window.location.href = `/island/${encodeURIComponent(island.islandHash || islandId)}`; 618 619 }); 619 620 } 620 621 ··· 634 635 island = data.island; 635 636 island.recordUri = data.recordUri; 636 637 } catch { 637 - content.innerHTML = '<div class="empty">Strata record not found.</div>'; 638 + content.innerHTML = '<div class="empty">Island record not found.</div>'; 638 639 return; 639 640 } 640 641 } else { 641 - // Legacy: sha256 ID — find from loaded islands 642 - island = islands.find(i => i.id === islandId) || null; 642 + // Island hash or legacy ID — find from loaded islands 643 + island = islands.find(i => i.islandHash === islandId || i.id === islandId) || null; 643 644 644 645 // If not in memory, fetch from API 645 646 if (!island) { 646 647 try { 647 648 const data = await apiGet<{ islands: Island[] }>("/xrpc/org.latha.island.getIslands"); 648 - island = data.islands.find(i => i.id === islandId) || null; 649 + island = data.islands.find(i => i.islandHash === islandId || i.id === islandId) || null; 649 650 if (island) islands = data.islands; 650 651 } catch {} 651 652 } ··· 1078 1079 if (path === "/island") { 1079 1080 return { page: "island", params: { id: url.searchParams.get("id") || "" } }; 1080 1081 } 1082 + const islandMatch = path.match(/^\/island\/([0-9a-f]{12})$/); 1083 + if (islandMatch) { 1084 + return { page: "strata", params: { id: islandMatch[1] } }; 1085 + } 1081 1086 if (path === "/strata") { 1082 - return { page: "strata", params: { id: url.searchParams.get("id") || "" } }; 1087 + // Legacy redirect 1088 + const legacyId = url.searchParams.get("id") || ""; 1089 + if (legacyId) { 1090 + window.location.replace(`/island/${encodeURIComponent(legacyId)}`); 1091 + return { page: "strata", params: { id: legacyId } }; 1092 + } 1093 + return { page: "strata", params: { id: "" } }; 1083 1094 } 1084 1095 if (path === "/connect") { 1085 1096 return { page: "connect", params: { source: url.searchParams.get("source") || "" } };
+29 -79
src/island-analyze.ts
··· 1 1 /** 2 - * island:analyze <component-id> 2 + * island:analyze <island-id> 3 3 * 4 4 * Analyze a discovered component against carry data. 5 5 * Writes analysis results to SQLite (not PDS — use publish for that). 6 6 * 7 7 * Usage: 8 - * bun island:analyze 1 # analyze component 1 9 - * bun island:analyze 1 --force # re-analyze even if cached 10 - * bun island:analyze --all # analyze all components 11 - * bun island:analyze --all --min-size 5 # only components with >= 5 nodes 12 - * bun island:analyze 1 --json # machine-readable output 8 + * bun island:analyze a1b2c3d4e5f6 # analyze by island hash 9 + * bun island:analyze a1b2c3d4e5f6 --force # re-analyze even if cached 10 + * bun island:analyze --all # analyze all components 11 + * bun island:analyze --all --min-size 5 # only components with >= 5 nodes 12 + * bun island:analyze a1b2c3d4e5f6 --json # machine-readable output 13 13 */ 14 14 15 15 import { resolve, dirname } from 'node:path'; 16 16 import { fileURLToPath } from 'node:url'; 17 - import { execFileSync } from 'node:fs'; 17 + import { execFileSync } from 'node:child_process'; 18 18 import { Database } from 'bun:sqlite'; 19 19 import { loadDotEnv } from './cli-utils.js'; 20 + import { findComponents, islandId, domainFromUrl, type Component } from './island-shared.js'; 20 21 21 22 const __dirname = dirname(fileURLToPath(import.meta.url)); 22 23 await loadDotEnv(resolve(__dirname, '..', '.env')); 23 24 24 25 const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; 25 26 const DB_PATH = process.env.ISLAND_DETECT_DB ?? resolve(CARRY_DIR, '.island-detect.db'); 26 - const COLLECTION = 'network.cosmik.connection'; 27 27 28 28 // --- Types --- 29 29 30 - interface Component { 31 - id: number; 32 - nodes: Set<string>; 33 - dids: Set<string>; 34 - } 35 - 36 30 interface CarryEntity { name: string; url: string; description: string } 37 31 interface CarryCitation { url: string; title: string; takeaway: string; domain: string } 38 32 interface CarryConnection { source: string; target: string; relation: string; context: string } 39 33 40 34 interface AnalysisResult { 41 - componentId: number; 35 + islandId: string; 36 + lexmin: string; 42 37 title: string; 43 38 themes: string[]; 44 39 highlights: string[]; ··· 57 52 db.exec('PRAGMA synchronous = NORMAL'); 58 53 db.exec(` 59 54 CREATE TABLE IF NOT EXISTS analyses ( 60 - component_id INTEGER PRIMARY KEY, 55 + island_id TEXT PRIMARY KEY, 61 56 title TEXT, 62 57 themes TEXT, 63 58 highlights TEXT, ··· 97 92 return index; 98 93 } 99 94 100 - // --- Connected components (from SQLite edges) --- 101 - 102 - function findComponents(db: Database): Component[] { 103 - const adj = new Map<string, Set<string>>(); 104 - const edgeDids = new Map<string, Set<string>>(); 105 - 106 - const rows = db.query('SELECT source, target, did FROM edges').all() as Array<{ source: string; target: string; did: string }>; 107 - for (const row of rows) { 108 - const s = row.source; 109 - const t = row.target; 110 - if (!adj.has(s)) adj.set(s, new Set()); 111 - if (!adj.has(t)) adj.set(t, new Set()); 112 - adj.get(s)!.add(t); 113 - adj.get(t)!.add(s); 114 - if (!edgeDids.has(s)) edgeDids.set(s, new Set()); 115 - if (!edgeDids.has(t)) edgeDids.set(t, new Set()); 116 - edgeDids.get(s)!.add(row.did); 117 - edgeDids.get(t)!.add(row.did); 118 - } 119 - 120 - const visited = new Set<string>(); 121 - const components: Component[] = []; 122 - let compId = 0; 123 - 124 - for (const node of adj.keys()) { 125 - if (visited.has(node)) continue; 126 - compId++; 127 - const compNodes = new Set<string>(); 128 - const compDids = new Set<string>(); 129 - const queue = [node]; 130 - while (queue.length > 0) { 131 - const current = queue.pop()!; 132 - if (visited.has(current)) continue; 133 - visited.add(current); 134 - compNodes.add(current); 135 - for (const did of edgeDids.get(current) ?? []) compDids.add(did); 136 - for (const neighbor of adj.get(current) ?? []) { 137 - if (!visited.has(neighbor)) queue.push(neighbor); 138 - } 139 - } 140 - components.push({ id: compId, nodes: compNodes, dids: compDids }); 141 - } 142 - 143 - return components.sort((a, b) => b.nodes.size - a.nodes.size); 144 - } 145 - 146 95 // --- Analysis --- 147 96 148 97 const THEME_KEYWORDS: Record<string, string[]> = { ··· 261 210 const lower = islandText.toLowerCase(); 262 211 263 212 for (const c of carryCitations) { 213 + if (!c.takeaway) continue; 264 214 const t = c.takeaway.toLowerCase(); 265 215 if (t.includes('contradict') || t.includes('however') || t.includes('tension') || t.includes('but')) { 266 216 // Check if citation URL is in the island 267 - if (lower.includes(c.url.toLowerCase()) || lower.includes(c.domain.toLowerCase())) { 217 + if ((c.url && lower.includes(c.url.toLowerCase())) || (c.domain && lower.includes(c.domain.toLowerCase()))) { 268 218 tensions.push(c.takeaway.slice(0, 500)); 269 219 } 270 220 } ··· 379 329 : synthesis.slice(0, 200); 380 330 381 331 return { 382 - componentId: comp.id, 332 + islandId: comp.islandId, 333 + lexmin: comp.lexmin, 383 334 title, 384 335 themes, 385 336 highlights, ··· 395 346 396 347 function saveAnalysis(db: Database, result: AnalysisResult): void { 397 348 db.query(` 398 - INSERT INTO analyses (component_id, title, themes, highlights, tensions, open_questions, synthesis, new_connections, analyzed_at) 349 + INSERT INTO analyses (island_id, title, themes, highlights, tensions, open_questions, synthesis, new_connections, analyzed_at) 399 350 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 400 - ON CONFLICT(component_id) DO UPDATE SET 351 + ON CONFLICT(island_id) DO UPDATE SET 401 352 title=excluded.title, themes=excluded.themes, highlights=excluded.highlights, 402 353 tensions=excluded.tensions, open_questions=excluded.open_questions, 403 354 synthesis=excluded.synthesis, new_connections=excluded.new_connections, analyzed_at=excluded.analyzed_at 404 355 `).run( 405 - result.componentId, 356 + result.islandId, 406 357 result.title, 407 358 JSON.stringify(result.themes), 408 359 JSON.stringify(result.highlights), ··· 424 375 const minSizeArg = args.find((a) => a.startsWith('--min-size=')); 425 376 const minSize = minSizeArg ? Number(minSizeArg.split('=')[1]) : 3; 426 377 427 - // Get component ID from args 428 - const compArg = args.find((a) => !a.startsWith('--') && /^\d+$/.test(a)); 429 - const compId = compArg ? Number(compArg) : null; 378 + // Get island hash from args 379 + const targetId = args.find((a) => !a.startsWith('--')); 430 380 431 - if (!compId && !analyzeAll) { 432 - console.error('Usage: bun island:analyze <component-id> [--force] [--json]'); 381 + if (!targetId && !analyzeAll) { 382 + console.error('Usage: bun island:analyze <island-id> [--force] [--json]'); 433 383 console.error(' bun island:analyze --all [--min-size 5] [--force] [--json]'); 434 384 process.exit(1); 435 385 } ··· 460 410 if (analyzeAll) { 461 411 targets = components.filter((c) => c.nodes.size >= minSize); 462 412 } else { 463 - const comp = components.find((c) => c.id === compId); 413 + const comp = components.find((c) => c.islandId === targetId); 464 414 if (!comp) { 465 - console.error(`Component ${compId} not found. Available: ${components.slice(0, 10).map((c) => c.id).join(', ')}...`); 415 + console.error(`Island ${targetId} not found. Run \`bun island:discover\` to list available islands.`); 466 416 process.exit(1); 467 417 } 468 418 targets = [comp]; 469 419 } 470 420 471 - console.error(`Analyzing ${targets.length} component(s)...`); 421 + console.error(`Analyzing ${targets.length} island(s)...`); 472 422 473 423 const results: AnalysisResult[] = []; 474 424 475 425 for (const comp of targets) { 476 426 // Check if already analyzed 477 427 if (!force) { 478 - const existing = db.query('SELECT analyzed_at FROM analyses WHERE component_id = ?').get(comp.id) as { analyzed_at: string } | null; 428 + const existing = db.query('SELECT analyzed_at FROM analyses WHERE island_id = ?').get(comp.islandId) as { analyzed_at: string } | null; 479 429 if (existing) { 480 - console.error(`Component ${comp.id}: already analyzed at ${existing.analyzed_at} (use --force to re-analyze)`); 430 + console.error(`${comp.islandId}: already analyzed at ${existing.analyzed_at} (use --force to re-analyze)`); 481 431 continue; 482 432 } 483 433 } 484 434 485 - console.error(`Component ${comp.id}: ${comp.nodes.size} nodes, ${comp.dids.size} DIDs`); 435 + console.error(`${comp.islandId}: ${comp.nodes.size} nodes, ${comp.dids.size} DIDs`); 486 436 const result = analyzeComponent(db, comp, carryConnections, entityUrlIndex, carryCitations); 487 437 saveAnalysis(db, result); 488 438 results.push(result); ··· 498 448 } 499 449 500 450 for (const r of results) { 501 - console.log(`\nComponent ${r.componentId}: ${r.title}`); 451 + console.log(`\n${r.islandId}: ${r.title}`); 502 452 console.log(` Themes: ${r.themes.join(', ') || 'none'}`); 503 453 console.log(` New connections: ${r.newConnections.length}`); 504 454 if (r.newConnections.length > 0) {
+8 -2
src/worker.ts
··· 1109 1109 }); 1110 1110 1111 1111 // ── SPA fallback: serve landing page for client-side routes ────── 1112 - for (const path of ["/island", "/strata", "/connect"]) { 1112 + for (const path of ["/island", "/island/*", "/strata", "/connect"]) { 1113 1113 app.get(path, async (c) => { 1114 1114 let islandsJson = "[]"; 1115 1115 try { ··· 1124 1124 const lexmin = rec.source?.uri; 1125 1125 if (!lexmin) continue; 1126 1126 const id = row.uri.split("/").pop() || row.uri; 1127 - islands.push({ id, lexmin, title: rec.analysis?.title || null, recordUri: row.uri }); 1127 + // Compute island hash from lexmin (same as island-shared.ts) 1128 + const encoder = new TextEncoder(); 1129 + const data = encoder.encode(lexmin); 1130 + const hashBuffer = await crypto.subtle.digest("SHA-256", data); 1131 + const hashArray = Array.from(new Uint8Array(hashBuffer)); 1132 + const islandHash = hashArray.map(b => b.toString(16).padStart(2, "0")).join("").slice(0, 12); 1133 + islands.push({ id, islandHash, lexmin, title: rec.analysis?.title || null, recordUri: row.uri }); 1128 1134 } catch {} 1129 1135 } 1130 1136 islandsJson = JSON.stringify(islands);