This repository has no description
0

Configure Feed

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

refactor: one kind of edge — replace connections with highlights

Remove the separate connections/relationships layer from strata analysis.
All edges are now network.cosmik.connection records. The analysis flags
existing edges as "hot" (semantically significant) via a highlights array
of AT URIs. Frontend renders hot edges with a star badge and accent border.

Lexicon changes:
- Remove #connection def and token defs (supports, contradicts, etc.)
- Remove connections from structuredAnalysis
- Add highlights (at-uri[]) to structuredAnalysis

Analysis changes:
- Replace findConnections with findHotEdges — scores edges by
vault/carry overlap and connection type, returns top AT URIs
- Remove validateConnections (no longer needed)
- Remove Connection interface

Worker changes:
- Replace relationships flattening with highlights passthrough

Frontend changes:
- Remove Relationships section
- Hot edges get .hot-edge class, star badge, accent border
- Edges header shows "N hot" count

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

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

author
nandi
co-author
Letta Code
date (May 14, 2026, 5:37 AM UTC) commit 390c4864 parent bff0e35b
+87 -173
+62 -64
container/analysis.ts
··· 40 40 note: string; 41 41 } 42 42 43 - interface Connection { 44 - description: string; 45 - targetUri: string; 46 - relation: string; 47 - source: "vault" | "carry"; 48 - sourcePath?: string; 49 - } 50 - 51 43 interface CarryRef { 52 44 carryId: string; 53 45 atUri: string; ··· 64 56 65 57 interface AnalysisResult { 66 58 themes: string[]; 67 - connections: Connection[]; 59 + highlights: string[]; 68 60 tensions: string[]; 69 61 openQuestions: string[]; 70 62 synthesis: string; ··· 88 80 // 4. Extract themes from the island 89 81 const themes = extractThemes(islandText); 90 82 91 - // 5. Find connections to vault + carry 92 - const connections = findConnections(islandText, island, vaultNotes, carryNotes); 83 + // 5. Flag hot edges — existing edges that are semantically significant 84 + const highlights = findHotEdges(islandText, island, vaultNotes, carryNotes); 93 85 94 86 // 6. Find tensions 95 87 const tensions = findTensions(islandText, carryNotes); 96 88 97 89 // 7. Generate open questions 98 - const openQuestions = generateOpenQuestions(islandText, island, connections, tensions); 90 + const openQuestions = generateOpenQuestions(islandText, island, highlights, tensions); 99 91 100 92 // 8. Synthesize prose 101 - const synthesis = synthesize(island, themes, connections, tensions, openQuestions); 93 + const synthesis = synthesize(island, themes, highlights, tensions, openQuestions); 102 94 103 95 // 9. Build carry cross-refs 104 96 const carryRefs = buildCarryRefs(connections); 105 97 106 - return { themes, connections, tensions, openQuestions, synthesis, carryRefs }; 98 + return { themes, highlights, tensions, openQuestions, synthesis, carryRefs }; 107 99 } 108 100 109 101 // ─── Filesystem readers ──────────────────────────────────────────── ··· 200 192 .map(([theme]) => theme); 201 193 } 202 194 203 - function findConnections( 195 + function findHotEdges( 204 196 islandText: string, 205 197 island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 206 198 vaultNotes: VaultNote[], 207 199 carryNotes: CarryNote[], 208 - ): Connection[] { 209 - const connections: Connection[] = []; 200 + ): string[] { 201 + const highlights: string[] = []; 210 202 const lower = islandText.toLowerCase(); 211 203 212 - // Check vault notes for overlapping themes 213 - for (const note of vaultNotes) { 214 - const noteLower = note.content.toLowerCase(); 215 - const overlap = keywordOverlap(lower, noteLower); 216 - if (overlap.length >= 2) { 217 - connections.push({ 218 - description: `Vault note "${note.path}" shares themes: ${overlap.join(", ")}`, 219 - targetUri: "", 220 - relation: "contextualizes", 221 - source: "vault", 222 - sourcePath: note.path, 223 - }); 204 + // Score each edge by how much it connects to vault/carry knowledge 205 + const edgeScores = new Map<string, number>(); 206 + 207 + for (const edge of island.edges) { 208 + let score = 0; 209 + 210 + // Check if source or target appears in vault notes 211 + for (const note of vaultNotes) { 212 + const noteLower = note.content.toLowerCase(); 213 + const overlap = keywordOverlap(lower, noteLower); 214 + if (overlap.length >= 2) { 215 + // Does this edge's source or target title appear in the vault note? 216 + const sourceTitle = island.vertices.find(v => v.uri === edge.source)?.title?.toLowerCase() || ""; 217 + const targetTitle = island.vertices.find(v => v.uri === edge.target)?.title?.toLowerCase() || ""; 218 + if (sourceTitle && noteLower.includes(sourceTitle)) score += 2; 219 + if (targetTitle && noteLower.includes(targetTitle)) score += 2; 220 + } 224 221 } 225 - } 226 222 227 - // Check carry notes for related claims 228 - for (const note of carryNotes) { 229 - const noteLower = note.content.toLowerCase(); 230 - const overlap = keywordOverlap(lower, noteLower); 231 - if (overlap.length >= 1) { 232 - // Extract wikilinks as potential AT URIs 233 - const wikilinks = note.content.match(/\[\[([^\]]+)\]\]/g) || []; 234 - const atUri = wikilinks[0]?.replace("[[", "").replace("]]", "") || ""; 223 + // Check if source or target appears in carry notes 224 + for (const note of carryNotes) { 225 + const noteLower = note.content.toLowerCase(); 226 + const sourceTitle = island.vertices.find(v => v.uri === edge.source)?.title?.toLowerCase() || ""; 227 + const targetTitle = island.vertices.find(v => v.uri === edge.target)?.title?.toLowerCase() || ""; 228 + if (sourceTitle && noteLower.includes(sourceTitle)) score += 1; 229 + if (targetTitle && noteLower.includes(targetTitle)) score += 1; 230 + } 235 231 236 - connections.push({ 237 - description: `Carry (${note.domain}): "${note.path}" — overlapping: ${overlap.join(", ")}`, 238 - targetUri: atUri, 239 - relation: "supports", 240 - source: "carry", 241 - sourcePath: note.path, 242 - }); 232 + // Edges with notes are more interesting 233 + if (edge.note && edge.note.length > 10) score += 1; 234 + 235 + // Edges with semantic connection types are more interesting 236 + const semanticTypes = ["supports", "contradicts", "extends", "contextualizes", "exemplifies"]; 237 + if (semanticTypes.some(t => edge.connectionType.toLowerCase().includes(t))) score += 2; 238 + 239 + if (score > 0) { 240 + edgeScores.set(edge.uri, score); 243 241 } 244 242 } 245 243 246 - return connections.slice(0, 10); 244 + // Return top edges by score 245 + const sorted = [...edgeScores.entries()].sort((a, b) => b[1] - a[1]); 246 + for (const [uri] of sorted.slice(0, 10)) { 247 + highlights.push(uri); 248 + } 249 + 250 + return highlights; 247 251 } 248 252 249 253 function findTensions( ··· 272 276 function generateOpenQuestions( 273 277 islandText: string, 274 278 island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 275 - connections: Connection[], 279 + highlights: string[], 276 280 tensions: string[], 277 281 ): string[] { 278 282 const questions: string[] = []; 279 283 const lower = islandText.toLowerCase(); 280 284 281 - if (connections.length > 0) { 282 - const carryCount = connections.filter(c => c.source === "carry").length; 283 - const vaultCount = connections.filter(c => c.source === "vault").length; 285 + if (highlights.length > 0) { 284 286 questions.push( 285 - `How does this island extend or challenge your existing ${carryCount} carry and ${vaultCount} vault connections?` 287 + `How does this island extend or challenge your existing knowledge? ${highlights.length} edges flagged as significant.` 286 288 ); 287 289 } 288 290 ··· 320 322 function synthesize( 321 323 island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 322 324 themes: string[], 323 - connections: Connection[], 325 + highlights: string[], 324 326 tensions: string[], 325 327 openQuestions: string[], 326 328 ): string { ··· 334 336 ? `Themes: ${themes.join(", ")}.` 335 337 : ""; 336 338 337 - const connStr = connections.length > 0 338 - ? `Connects to ${connections.length} items in your knowledge base: ${connections.slice(0, 3).map(c => c.description).join("; ")}.` 339 - : "No direct connections found in your existing knowledge."; 339 + const highlightStr = highlights.length > 0 340 + ? `${highlights.length} edges flagged as semantically significant — connecting to your existing knowledge.` 341 + : "No edges flagged as significant against your existing knowledge."; 340 342 341 343 const tensionStr = tensions.length > 0 342 344 ? `There ${tensions.length === 1 ? "is 1 tension" : `are ${tensions.length} tensions`} with your existing claims.` ··· 346 348 ? `Open questions: ${openQuestions.join(" ")}` 347 349 : ""; 348 350 349 - return [`Island (${clusterDesc}).`, themeStr, connStr, tensionStr, questionStr].filter(Boolean).join(" "); 351 + return [`Island (${clusterDesc}).`, themeStr, highlightStr, tensionStr, questionStr].filter(Boolean).join(" "); 350 352 } 351 353 352 - function buildCarryRefs(connections: Connection[]): CarryRef[] { 353 - return connections 354 - .filter(c => c.source === "carry") 355 - .map(c => ({ 356 - carryId: c.sourcePath || "", 357 - atUri: c.targetUri || "", 358 - summary: c.description, 359 - })); 354 + function buildCarryRefs(highlights: string[]): CarryRef[] { 355 + // No carry-specific refs without the old connection objects 356 + // Carry cross-refs are now implicit via the hot edge flags 357 + return []; 360 358 } 361 359 362 360 // ─── Utilities ──────────────────────────────────────────────────────
+4 -52
lexicons/org/latha/strata/strata.json
··· 114 114 }, 115 115 "description": "Key themes identified in the source." 116 116 }, 117 - "connections": { 117 + "highlights": { 118 118 "type": "array", 119 119 "maxLength": 20, 120 120 "items": { 121 - "type": "ref", 122 - "ref": "#connection" 121 + "type": "string", 122 + "format": "at-uri" 123 123 }, 124 - "description": "Connections to existing knowledge in vault and carry." 124 + "description": "AT URIs of network.cosmik.connection records flagged as semantically significant by the analysis. Hot edges." 125 125 }, 126 126 "tensions": { 127 127 "type": "array", ··· 149 149 } 150 150 } 151 151 }, 152 - "connection": { 153 - "type": "object", 154 - "description": "A connection between the source and an existing knowledge item.", 155 - "required": ["description", "targetUri"], 156 - "properties": { 157 - "description": { 158 - "type": "string", 159 - "maxGraphemes": 500, 160 - "description": "Human-readable description of the connection." 161 - }, 162 - "targetUri": { 163 - "type": "string", 164 - "format": "at-uri", 165 - "description": "AT URI of the connected record." 166 - }, 167 - "relation": { 168 - "type": "string", 169 - "knownValues": [ 170 - "org.latha.strata#supports", 171 - "org.latha.strata#contradicts", 172 - "org.latha.strata#extends", 173 - "org.latha.strata#contextualizes", 174 - "org.latha.strata#exemplifies" 175 - ], 176 - "description": "Type of connection." 177 - } 178 - } 179 - }, 180 152 "carryCrossRef": { 181 153 "type": "object", 182 154 "description": "Cross-reference to a carry entity, translated to AT Protocol addressable format.", ··· 206 178 } 207 179 } 208 180 }, 209 - "supports": { 210 - "type": "token", 211 - "description": "Connection relation: the source supports an existing claim." 212 - }, 213 - "contradicts": { 214 - "type": "token", 215 - "description": "Connection relation: the source contradicts an existing claim." 216 - }, 217 - "extends": { 218 - "type": "token", 219 - "description": "Connection relation: the source extends an existing claim." 220 - }, 221 - "contextualizes": { 222 - "type": "token", 223 - "description": "Connection relation: the source provides context for an existing claim." 224 - }, 225 - "exemplifies": { 226 - "type": "token", 227 - "description": "Connection relation: the source is an example of an existing claim." 228 - } 229 181 } 230 182 }
+8 -53
src/frontend/app.ts
··· 40 40 interface StrataData { 41 41 prose: string; 42 42 themes: string[]; 43 - relationships: string[]; 43 + highlights: string[]; 44 44 tensions: string[]; 45 45 open_questions: string[]; 46 46 synthesis: string; ··· 698 698 </div> 699 699 </div> 700 700 701 - ${s.relationships.length > 0 ? `<div class="strata-section"> 702 - <h3>Relationships <span class="edit-hint" title="Click to edit">&#9998;</span></h3> 703 - <div class="editable-list" data-field="relationships"> 704 - ${s.relationships.map(r => { 705 - // Link entity names to their Semble pages 706 - let styled = escHtml(r); 707 - const titleToUri = new Map<string, string>(); 708 - for (const uri of island.vertices!) { 709 - const m = island.vertexMeta![uri]; 710 - const title = m?.title; 711 - if (title && title.length > 3 && !title.startsWith("http") && !title.startsWith("at://")) { 712 - titleToUri.set(title, uri); 713 - } 714 - } 715 - const sortedTitles = [...titleToUri.entries()].sort((a, b) => b[0].length - a[0].length); 716 - for (const [title, uri] of sortedTitles) { 717 - const escaped = escHtml(title); 718 - if (styled.includes(escaped)) { 719 - const href = `https://semble.so/url/${encodeURIComponent(uri)}`; 720 - styled = styled.replace(escaped, `<a href="${href}" target="_blank" rel="noopener" class="entity-link">${escaped}</a>`); 721 - } 722 - } 723 - for (const [title, uri] of sortedTitles) { 724 - const href = `https://semble.so/url/${encodeURIComponent(uri)}`; 725 - const shortNames = [ 726 - title.split(/[:—–\-]/)[0].trim(), 727 - title.split("(")[0].trim(), 728 - ].filter(n => n.length > 3 && n.length < title.length); 729 - for (const shortName of shortNames) { 730 - const escaped = escHtml(shortName); 731 - if (styled.includes(escaped) && !styled.includes(`>${escaped}<`)) { 732 - styled = styled.replace(escaped, `<a href="${href}" target="_blank" rel="noopener" class="entity-link">${escaped}</a>`); 733 - } 734 - } 735 - } 736 - styled = styled 737 - .replace(/→/g, '<span class="arrow arrow-forward">→</span>') 738 - .replace(/←/g, '<span class="arrow arrow-back">←</span>') 739 - .replace(/↔/g, '<span class="arrow arrow-bidi">↔</span>') 740 - .replace(/⇏/g, '<span class="arrow arrow-blocked">⇏</span>'); 741 - return `<div class="strata-rel-line editable" contenteditable="true">${styled}</div>`; 742 - }).join("")} 743 - <div class="list-add" data-field="relationships">+ Add relationship</div> 744 - </div> 745 - </div>` : ""} 746 - 747 701 <div class="strata-section"> 748 702 <h3>Tensions <span class="edit-hint" title="Click to edit">&#9998;</span></h3> 749 703 <div class="editable-list" data-field="tensions"> ··· 767 721 </div> 768 722 769 723 <div class="strata-section"> 770 - <h3>Edges</h3> 724 + <h3>Edges${s.highlights.length > 0 ? ` <span class="highlights-count">${s.highlights.length} hot</span>` : ""}</h3> 771 725 <div class="strata-edges">${island.edges!.map(e => { 772 726 const sourceMeta = island.vertexMeta![e.source]; 773 727 const targetMeta = island.vertexMeta![e.target]; ··· 775 729 const targetTitle = targetMeta?.title || truncateUri(e.target); 776 730 const type = connectionTypeLabel(e.connectionType); 777 731 const icon = connectionTypeIcon(e.connectionType); 778 - const sembleUrl = e.source.startsWith("http") ? `https://semble.so/url/${encodeURIComponent(e.source)}` : ""; 779 - return `<a href="${escHtml(sembleUrl)}" class="strata-edge" target="_blank" rel="noopener"${!sembleUrl ? ' onclick="event.preventDefault()"' : ""}> 732 + const seemsUrl = e.source.startsWith("http") ? `https://semble.so/url/${encodeURIComponent(e.source)}` : ""; 733 + const isHot = s.highlights.includes(e.uri); 734 + return `<a href="${escHtml(seemsUrl)}" class="strata-edge${isHot ? " hot-edge" : ""}" target="_blank" rel="noopener"${!seemsUrl ? ' onclick="event.preventDefault()"' : ""}> 735 + ${isHot ? '<span class="hot-badge">&#9733;</span>' : ""} 780 736 <span class="strata-edge-source">${escHtml(sourceTitle)}</span> 781 737 <span class="strata-edge-type">${icon} ${escHtml(type)}</span> 782 738 <span class="strata-edge-target">${escHtml(targetTitle)}</span> ··· 840 796 btn.addEventListener("click", () => { 841 797 const field = (btn as HTMLElement).dataset.field!; 842 798 const div = document.createElement("div"); 843 - div.className = field === "tensions" ? "analysis-tension editable" : field === "relationships" ? "strata-rel-line editable" : "analysis-question editable"; 799 + div.className = field === "tensions" ? "analysis-tension editable" : "analysis-question editable"; 844 800 div.setAttribute("contenteditable", "true"); 845 801 div.textContent = "New item..."; 846 802 div.addEventListener("input", markDirty); ··· 857 813 const themeTags = editContainer.querySelectorAll('.theme-tag'); 858 814 const tensionEls = editContainer.querySelectorAll('[data-field="tensions"] .editable'); 859 815 const questionEls = editContainer.querySelectorAll('[data-field="openQuestions"] .editable'); 860 - const relEls = editContainer.querySelectorAll('[data-field="relationships"] .editable'); 861 816 862 817 return { 863 818 title: titleEl?.textContent?.trim() || "", ··· 865 820 themes: Array.from(themeTags).map(t => t.getAttribute("data-value") || t.textContent?.replace("×", "").trim() || ""), 866 821 tensions: Array.from(tensionEls).map(t => t.textContent?.trim() || "").filter(Boolean), 867 822 openQuestions: Array.from(questionEls).map(q => q.textContent?.trim() || "").filter(Boolean), 868 - relationships: Array.from(relEls).map(r => r.textContent?.trim() || "").filter(Boolean), 823 + highlights: island.strata?.highlights || [], 869 824 }; 870 825 } 871 826
+5
src/landing-page.ts
··· 1144 1144 .save-btn:disabled { opacity: 0.5; cursor: wait; } 1145 1145 .save-status { font-size: 0.8rem; color: var(--text-muted); } 1146 1146 .hidden { display: none !important; } 1147 + 1148 + /* Hot edges */ 1149 + .highlights-count { font-size: 0.7rem; color: var(--accent); font-weight: 400; margin-left: 0.25rem; } 1150 + .hot-edge { border-left: 2px solid var(--accent); padding-left: 0.5rem; } 1151 + .hot-badge { color: var(--accent); font-size: 0.75rem; margin-right: 0.25rem; } 1147 1152 </style> 1148 1153 </head> 1149 1154 <body>
+8 -4
src/worker.ts
··· 458 458 strata: analysis.synthesis ? { 459 459 prose: analysis.synthesis || "", title: analysis.title || "", 460 460 themes: analysis.themes || [], 461 - relationships: (analysis.connections || []).map((conn: any) => conn.description || ""), 461 + relationships: [], 462 + highlights: analysis.highlights || [], 462 463 tensions: analysis.tensions || [], 463 464 open_questions: analysis.openQuestions || [], 464 465 synthesis: analysis.synthesis || "", ··· 516 517 prose: analysis.synthesis || "", 517 518 title: analysis.title || "", 518 519 themes: analysis.themes || [], 519 - relationships: (analysis.connections || []).map((conn: any) => conn.description || ""), 520 + relationships: [], 521 + highlights: analysis.highlights || [], 520 522 tensions: analysis.tensions || [], 521 523 open_questions: analysis.openQuestions || [], 522 524 synthesis: analysis.synthesis || "", ··· 565 567 prose: analysis.synthesis || "", 566 568 title: analysis.title || "", 567 569 themes: analysis.themes || [], 568 - relationships: (analysis.connections || []).map((conn: any) => conn.description || ""), 570 + relationships: [], 571 + highlights: analysis.highlights || [], 569 572 tensions: analysis.tensions || [], 570 573 open_questions: analysis.openQuestions || [], 571 574 synthesis: analysis.synthesis || "", ··· 665 668 prose: analysis.synthesis || "", 666 669 title: analysis.title || "", 667 670 themes: analysis.themes || [], 668 - relationships: (analysis.connections || []).map((c: any) => c.description || ""), 671 + relationships: [], 672 + highlights: analysis.highlights || [], 669 673 tensions: analysis.tensions || [], 670 674 open_questions: analysis.openQuestions || [], 671 675 synthesis: analysis.synthesis || "",