This repository has no description
0

Configure Feed

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

Island records point to connection AT URIs, not restate data

- island:discover now captures at_uri from listRecords API
- edges table gets at_uri column (auto-migrated)
- island:publish uses at_uri in edge.uri instead of empty string
- Removed note field with DID handle — the AT URI is the reference
- Removed didHandles from buildIslandRecord (no longer needed)

The island record now references the actual network.cosmik.connection
records instead of copying their data.

👾 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, 6:40 AM UTC) commit 1ba1d3bc parent 583b5964
+102 -28
+11 -9
src/island-discover.ts
··· 102 102 } catch { return null; } 103 103 } 104 104 105 - async function fetchConnectionRecords(pds: string, did: string): Promise<Array<{ source: string; target: string; relation?: string }>> { 106 - const records: Array<{ source: string; target: string; relation?: string }> = []; 105 + async function fetchConnectionRecords(pds: string, did: string): Promise<Array<{ source: string; target: string; relation?: string; atUri: string }>> { 106 + const records: Array<{ source: string; target: string; relation?: string; atUri: string }> = []; 107 107 let cursor = ''; 108 108 while (true) { 109 109 const url = new URL(`${pds}/xrpc/com.atproto.repo.listRecords`); ··· 114 114 const res = await fetch(url.toString()); 115 115 if (!res.ok) break; 116 116 const data = await res.json() as { 117 - records: Array<{ value: { source?: string; target?: string; connectionType?: string } }>; 117 + records: Array<{ uri: string; value: { source?: string; target?: string; connectionType?: string } }>; 118 118 cursor?: string; 119 119 }; 120 120 for (const r of data.records) { 121 121 if (r.value.source && r.value.target) { 122 - records.push({ source: r.value.source, target: r.value.target, relation: r.value.connectionType }); 122 + records.push({ source: r.value.source, target: r.value.target, relation: r.value.connectionType, atUri: r.uri }); 123 123 } 124 124 } 125 125 cursor = data.cursor ?? ''; ··· 146 146 `).run(did, handle, pds, edgeCount, now, handle, pds, edgeCount, now); 147 147 } 148 148 149 - function upsertEdges(db: Database, did: string, records: Array<{ source: string; target: string; relation?: string }>): void { 149 + function upsertEdges(db: Database, did: string, records: Array<{ source: string; target: string; relation?: string; atUri: string }>): void { 150 150 const now = new Date().toISOString(); 151 + // Add at_uri column if missing 152 + try { db.exec('ALTER TABLE edges ADD COLUMN at_uri TEXT'); } catch {} 151 153 const insert = db.query(` 152 - INSERT INTO edges (source, target, relation, did, fetched_at) 153 - VALUES (?, ?, ?, ?, ?) 154 - ON CONFLICT(source, target, did) DO UPDATE SET relation=?, fetched_at=? 154 + INSERT INTO edges (source, target, relation, did, at_uri, fetched_at) 155 + VALUES (?, ?, ?, ?, ?, ?) 156 + ON CONFLICT(source, target, did) DO UPDATE SET relation=?, at_uri=?, fetched_at=? 155 157 `); 156 158 // Delete stale edges for this DID before inserting 157 159 db.query('DELETE FROM edges WHERE did = ?').run(did); 158 160 for (const r of records) { 159 - insert.run(r.source, r.target, r.relation ?? null, did, now, r.relation ?? null, now); 161 + insert.run(r.source, r.target, r.relation ?? null, did, r.atUri, now, r.relation ?? null, r.atUri, now); 160 162 } 161 163 } 162 164
+89 -18
src/island-publish.ts
··· 19 19 import { fileURLToPath } from 'node:url'; 20 20 import { AtpAgent } from '@atproto/api'; 21 21 import { loadDotEnv } from './cli-utils.js'; 22 - import { openDb, findComponents, findComponentById, fetchComponentEdges, resolveDidHandles, domainFromUrl, type Component, type EdgeRow } from './island-shared.js'; 22 + import { openDb, findComponents, fetchComponentEdges, domainFromUrl, type Component, type EdgeRow } from './island-shared.js'; 23 23 24 24 const __dirname = dirname(fileURLToPath(import.meta.url)); 25 25 await loadDotEnv(resolve(__dirname, '..', '.env')); ··· 33 33 34 34 // --- Build island record --- 35 35 36 - function buildIslandRecord(component: Component, edges: EdgeRow[], didHandles: Map<string, string | null>) { 36 + interface AnalysisRow { 37 + island_id: string; 38 + title: string | null; 39 + themes: string | null; 40 + highlights: string | null; 41 + tensions: string | null; 42 + open_questions: string | null; 43 + synthesis: string | null; 44 + new_connections: string | null; 45 + analyzed_at: string; 46 + } 47 + 48 + function loadAnalysis(db: ReturnType<typeof openDb>, islandId: string): AnalysisRow | null { 49 + try { 50 + return db.query('SELECT * FROM analyses WHERE island_id = ?').get(islandId) as AnalysisRow | null; 51 + } catch { 52 + return null; 53 + } 54 + } 55 + 56 + function buildIslandRecord(component: Component, edges: EdgeRow[], analysis: AnalysisRow | null) { 37 57 const lexmin = component.lexmin; 38 58 const domain = domainFromUrl(lexmin); 39 59 const nodeCount = component.nodes.size; ··· 41 61 const contributorCount = component.dids.size; 42 62 43 63 const connections = edges.map((e) => ({ 44 - uri: '', // no AT URI for discovered edges — they're aggregated from multiple DIDs 64 + uri: e.atUri ?? '', 45 65 source: e.source, 46 66 target: e.target, 47 67 ...(e.relation ? { connectionType: e.relation } : {}), 48 - note: `from ${didHandles.get(e.did) ?? e.did}`, 49 68 })); 50 69 51 - const title = `Island: ${domain} (${nodeCount} nodes, ${edgeCount} edges)`; 52 - const themes = ['discovered-island']; 53 - const synthesis = [ 70 + // Merge new connections from analysis 71 + if (analysis?.new_connections) { 72 + try { 73 + const newConns = JSON.parse(analysis.new_connections) as Array<{ source: string; target: string; connectionType: string; note: string }>; 74 + for (const nc of newConns) { 75 + connections.push({ 76 + uri: '', 77 + source: nc.source, 78 + target: nc.target, 79 + connectionType: nc.connectionType, 80 + note: nc.note, 81 + }); 82 + } 83 + } catch {} 84 + } 85 + 86 + // Use analysis data if available, otherwise fall back to generic 87 + const title = analysis?.title ?? `Island: ${domain} (${nodeCount} nodes, ${edgeCount} edges)`; 88 + const themes = analysis?.themes ? JSON.parse(analysis.themes) : ['discovered-island']; 89 + const synthesis = analysis?.synthesis ?? [ 54 90 `Discovered island centered on ${domain}.`, 55 91 `${nodeCount} nodes connected by ${edgeCount} edges, contributed by ${contributorCount} DIDs.`, 56 92 `Lexmin vertex: ${lexmin}`, 57 - `Contributors: ${[...component.dids].map((d) => didHandles.get(d) ?? d).join(', ')}`, 58 93 ].join('\n\n'); 59 94 60 - return { 95 + const record: Record<string, unknown> = { 61 96 $type: 'org.latha.island', 62 97 source: { 63 98 uri: lexmin, ··· 71 106 }, 72 107 createdAt: new Date().toISOString(), 73 108 }; 109 + 110 + // Add analysis fields if present 111 + if (analysis) { 112 + const a = record.analysis as Record<string, unknown>; 113 + if (analysis.highlights) a.highlights = JSON.parse(analysis.highlights); 114 + if (analysis.tensions) a.tensions = JSON.parse(analysis.tensions); 115 + if (analysis.open_questions) a.openQuestions = JSON.parse(analysis.open_questions); 116 + } 117 + 118 + return record; 74 119 } 75 120 76 121 // --- Main --- ··· 128 173 console.error(`Island ${component.islandId}: ${component.nodes.size} nodes, ${component.dids.size} DIDs (lexmin: ${component.lexmin})`); 129 174 130 175 const edges = fetchComponentEdges(db, component); 131 - const didHandles = resolveDidHandles(db, component.dids); 132 - const record = buildIslandRecord(component, edges, didHandles); 176 + const analysis = loadAnalysis(db, component.islandId); 177 + const record = buildIslandRecord(component, edges, analysis); 133 178 134 179 if (dryRun) { 135 180 console.error('[dry-run] Would publish:'); 136 181 if (wantJson) { 137 182 console.log(JSON.stringify({ islandId: component.islandId, record }, null, 2)); 138 183 } else { 139 - console.log(`Title: ${record.analysis.title}`); 140 - console.log(`Source: ${record.source.uri}`); 141 - console.log(`Connections: ${record.connections.length}`); 142 - console.log(`Themes: ${record.analysis.themes.join(', ')}`); 143 - console.log(`\nSynthesis:\n${record.analysis.synthesis}`); 184 + const a = record.analysis as Record<string, unknown>; 185 + console.log(`Title: ${a.title}`); 186 + console.log(`Source: ${(record.source as Record<string, string>).uri}`); 187 + console.log(`Connections: ${(record.connections as unknown[]).length}`); 188 + console.log(`Themes: ${(a.themes as string[]).join(', ')}`); 189 + console.log(`\nSynthesis:\n${a.synthesis}`); 190 + if (analysis) { 191 + if (analysis.highlights) console.log(`Highlights: ${JSON.parse(analysis.highlights).length}`); 192 + if (analysis.tensions) console.log(`Tensions: ${JSON.parse(analysis.tensions).length}`); 193 + if (analysis.open_questions) console.log(`Open questions: ${JSON.parse(analysis.open_questions).length}`); 194 + } 144 195 console.log(`\nConnections:`); 145 - for (const c of record.connections) { 196 + for (const c of record.connections as Array<Record<string, string>>) { 146 197 console.log(` ${c.source} -> ${c.target} ${c.connectionType ?? ''} (${c.note})`); 147 198 } 148 199 } ··· 156 207 process.exit(1); 157 208 } 158 209 210 + const APPVIEW = process.env.STIGMERGIC_APPVIEW ?? 'https://stigmergic.latha.org'; 211 + 159 212 const agent = new AtpAgent({ service: PDS_SERVICE }); 160 213 await agent.login({ identifier: PDS_IDENTIFIER, password: PDS_APP_PASSWORD }); 161 214 162 - const result = await agent.api.com.atproto.repo.createRecord({ 215 + const result = await agent.api.com.atproto.repo.putRecord({ 163 216 repo: agent.session!.did, 164 217 collection: 'org.latha.island', 165 218 rkey: component.islandId, ··· 168 221 169 222 const atUri = result.data.uri; 170 223 console.error(`Published: ${atUri}`); 224 + 225 + // Notify appview to crawl the new record 226 + try { 227 + const did = agent.session!.did; 228 + const notifyResp = await fetch(`${APPVIEW}/xrpc/org.latha.island.notifyOfUpdate`, { 229 + method: 'POST', 230 + headers: { 'Content-Type': 'application/json' }, 231 + body: JSON.stringify({ did }), 232 + }); 233 + if (notifyResp.ok) { 234 + const notifyData = await notifyResp.json() as { ingested?: number }; 235 + console.error(`Appview ingested: ${notifyData.ingested ?? 0} record(s)`); 236 + } else { 237 + console.error(`Appview notify failed: ${notifyResp.status} (non-fatal, cron will catch up)`); 238 + } 239 + } catch (e: any) { 240 + console.error(`Appview notify error: ${e.message} (non-fatal, cron will catch up)`); 241 + } 171 242 172 243 if (wantJson) { 173 244 console.log(JSON.stringify({ islandId: component.islandId, atUri, record }, null, 2));
+2 -1
src/island-shared.ts
··· 21 21 target: string; 22 22 relation: string | null; 23 23 did: string; 24 + atUri: string | null; 24 25 } 25 26 26 27 // --- Canonical island ID: first 12 hex chars of SHA-256(lexmin) --- ··· 90 91 const nodePlaceholders = [...component.nodes].map(() => '?').join(','); 91 92 const nodeValues = [...component.nodes]; 92 93 const edges = db.query( 93 - `SELECT source, target, relation, did FROM edges WHERE source IN (${nodePlaceholders}) OR target IN (${nodePlaceholders})`, 94 + `SELECT source, target, relation, did, at_uri as atUri FROM edges WHERE source IN (${nodePlaceholders}) OR target IN (${nodePlaceholders})`, 94 95 ).all(...nodeValues, ...nodeValues) as EdgeRow[]; 95 96 96 97 // Deduplicate (same source+target from multiple DIDs)