This repository has no description
0

Configure Feed

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

Remove container, add OAuth editing, parallelize discover, add island:search

- Remove Docker container, StrataContainer DO, PDS_APP_PASSWORD, and all
server-side PDS write routes (updateRecord, analyze, deriveIsland,
containerHealth, syncContainer, getCarryData)
- Frontend edits now write via OAuth session: putRecord for owner,
createRecord (fork) for non-owner with forkOf provenance
- OAuth callback preserves return page via state + sessionStorage
- Parallelize island:discover: batch 10 DIDs concurrently via
Promise.allSettled, merge resolvePDS+resolveHandle into single
resolveDidDoc call
- Add island:search CLI command for text-based island discovery
- Add synthesis-to-oxa migration script
- Edit chip moved below graph, shown/hidden based on auth state
- Login dialog uses placeholder instead of prefilled handle

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

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

author
nandi
co-author
Letta Code
date (May 16, 2026, 1:58 AM UTC) commit e314b1ba parent 19562e6a
+2149 -1483
-38
container/Dockerfile
··· 1 - FROM node:22-slim 2 - 3 - # Install git, curl, patchelf, and other deps 4 - RUN apt-get update && apt-get install -y \ 5 - git \ 6 - curl \ 7 - ca-certificates \ 8 - patchelf \ 9 - && rm -rf /var/lib/apt/lists/* 10 - 11 - # Install carry (local-first semantic DB) — patch Nix-built binary for Debian 12 - RUN curl -fsSL -o /usr/local/bin/carry \ 13 - https://github.com/tonk-labs/tonk/releases/download/latest/carry-linux-x86_64 \ 14 - && chmod +x /usr/local/bin/carry \ 15 - && patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 /usr/local/bin/carry \ 16 - && apt-get purge -y patchelf && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* 17 - 18 - WORKDIR /app 19 - 20 - # Install ob (Obsidian headless CLI) globally 21 - RUN npm install -g obsidian-headless 22 - 23 - # Install deps 24 - COPY package.json ./ 25 - RUN npm install --production 26 - 27 - # Copy analysis engine (ARG to bust cache on source changes) 28 - ARG BUILD_DATE=unknown 29 - COPY server.ts ./ 30 - COPY analysis.ts ./ 31 - COPY sync.ts ./ 32 - 33 - # Create data directories 34 - RUN mkdir -p /data/vault /data/carry 35 - 36 - EXPOSE 8080 37 - 38 - CMD ["node", "--experimental-strip-types", "server.ts"]
-504
container/analysis.ts
··· 1 - // ─── Strata Analysis Engine ───────────────────────────────────────── 2 - // Cross-references an island (cluster of linked URLs) against: 3 - // 1. Carry semantic triples from `carry query --format json` 4 - // 2. Structured carry records from D1 (entities, citations, reasonings) 5 - // Follows carry semantics: connections are source → relation → target. 6 - // If one side is in the island, the other side becomes a new connection. 7 - // Returns structured analysis + new connection records to be created on PDS. 8 - 9 - import { readFileSync, existsSync } from "fs"; 10 - import { join } from "path"; 11 - 12 - const CARRY_PATH = process.env.CARRY_PATH || "/data/carry"; 13 - 14 - // ─── Types ────────────────────────────────────────────────────────── 15 - 16 - interface VertexInfo { 17 - uri: string; 18 - title: string; 19 - description: string; 20 - type: string; 21 - } 22 - 23 - interface EdgeInfo { 24 - uri: string; 25 - did: string; 26 - source: string; 27 - target: string; 28 - connectionType: string; 29 - note: string; 30 - } 31 - 32 - interface CarryEntity { 33 - atUri: string; 34 - name: string; 35 - entityType: string; 36 - description?: string; 37 - url?: string; // The entity's canonical https:// URL 38 - } 39 - 40 - interface CarryCitation { 41 - atUri: string; 42 - url: string; 43 - title: string; 44 - takeaway: string; 45 - confidence?: string; 46 - } 47 - 48 - interface CarryReasoning { 49 - atUri: string; 50 - claim: string; 51 - evidence: string; 52 - reasoningType?: string; 53 - url?: string; // The reasoning's source https:// URL 54 - } 55 - 56 - interface CarryData { 57 - entities: CarryEntity[]; 58 - citations: CarryCitation[]; 59 - reasonings: CarryReasoning[]; 60 - connections: CarryConnection[]; 61 - } 62 - 63 - // Structured carry connection — a semantic triple 64 - interface CarryConnection { 65 - sourceName: string; // e.g. "Khayame" 66 - targetName: string; // e.g. "Vickers Appreciative Systems" 67 - sourceUrl: string; // e.g. "https://link.springer.com/..." 68 - targetUrl: string; // e.g. "https://pubadmin.institute/..." 69 - relation: string; // e.g. "intellectual_influence" 70 - context: string; // e.g. "Khayame draws on Vickers' appreciative systems..." 71 - } 72 - 73 - interface NewConnection { 74 - source: string; // island vertex URI 75 - target: string; // carry record AT URI 76 - connectionType: string; // e.g. "network.cosmik.connection#annotates" 77 - note: string; // why this connection exists 78 - } 79 - 80 - interface AnalysisInput { 81 - did: string; 82 - island: { 83 - vertices: VertexInfo[]; 84 - edges: EdgeInfo[]; 85 - }; 86 - carryData?: CarryData; 87 - } 88 - 89 - interface AnalysisResult { 90 - themes: string[]; 91 - highlights: string[]; 92 - tensions: string[]; 93 - openQuestions: string[]; 94 - synthesis: string; 95 - newConnections: NewConnection[]; 96 - } 97 - 98 - // ─── Main analysis function ──────────────────────────────────────── 99 - 100 - export async function analyze(input: AnalysisInput): Promise<AnalysisResult> { 101 - const { island, carryData } = input; 102 - 103 - // 1. Read carry JSON exports (from `carry query --format json` during sync) 104 - const carryEntities = readCarryEntities(); 105 - const carryCitations = readCarryCitations(); 106 - const carryConnections = readCarryConnections(); 107 - const entityUrlIndex = buildEntityUrlIndex(); 108 - 109 - // 2. Build island URL set for fast lookup 110 - const islandText = buildIslandText(island); 111 - const islandUrls = new Set(island.vertices.map(v => v.uri)); 112 - 113 - // 3. Extract themes 114 - const themes = extractThemes(islandText); 115 - 116 - // 4. Match carry data against island — follow carry semantics 117 - const newConnections = matchCarryData( 118 - island, islandUrls, 119 - carryEntities, carryCitations, carryConnections, entityUrlIndex, 120 - carryData || { entities: [], citations: [], reasonings: [], connections: [] }, 121 - ); 122 - 123 - // 6. Flag hot edges 124 - const highlights = findHotEdges(islandText, island, newConnections); 125 - 126 - // 7. Find tensions from carry reasonings 127 - const tensions = findTensionsFromReasonings(islandText, carryData?.reasonings || []); 128 - 129 - // Deduplicate newConnections 130 - const seen = new Set<string>(); 131 - const deduped = newConnections.filter(c => { 132 - const key = `${c.source}|${c.target}`; 133 - if (seen.has(key)) return false; 134 - seen.add(key); 135 - return true; 136 - }).slice(0, 20); 137 - 138 - // Regenerate synthesis and openQuestions with deduped count 139 - const finalQuestions = generateOpenQuestions(islandText, island, highlights, tensions, deduped); 140 - const finalSynthesis = synthesize(island, themes, highlights, tensions, finalQuestions, deduped); 141 - 142 - return { themes, highlights, tensions, openQuestions: finalQuestions, synthesis: finalSynthesis, newConnections: deduped }; 143 - } 144 - 145 - // ─── Carry JSON readers ──────────────────────────────────────────── 146 - // Reads carry data exported by `carry query --format json` during sync. 147 - 148 - interface CarryEntityRecord { 149 - id: string; 150 - name: string; 151 - url?: string; 152 - type?: string; 153 - description?: string; 154 - } 155 - 156 - interface CarryCitationRecord { 157 - id: string; 158 - title: string; 159 - url?: string; 160 - takeaway?: string; 161 - } 162 - 163 - interface CarryConnectionRecord { 164 - id: string; 165 - relation: string; 166 - source?: string; 167 - target?: string; 168 - context?: string; 169 - } 170 - 171 - function readCarryJson<T>(filename: string): T[] { 172 - const path = join(CARRY_PATH, filename); 173 - try { 174 - if (!existsSync(path)) return []; 175 - const data = readFileSync(path, "utf-8"); 176 - return JSON.parse(data) as T[]; 177 - } catch (e: any) { 178 - console.error(`[carry] Failed to read ${filename}:`, e.message); 179 - return []; 180 - } 181 - } 182 - 183 - function readCarryEntities(): CarryEntityRecord[] { 184 - return readCarryJson<CarryEntityRecord>("org.latha.entity.json"); 185 - } 186 - 187 - function readCarryCitations(): CarryCitationRecord[] { 188 - return readCarryJson<CarryCitationRecord>("org.latha.citation.json"); 189 - } 190 - 191 - function readCarryConnections(): CarryConnectionRecord[] { 192 - return readCarryJson<CarryConnectionRecord>("org.latha.connection.json"); 193 - } 194 - 195 - // Build entity name → URL lookup from carry export 196 - function buildEntityUrlIndex(): Map<string, string> { 197 - const index = new Map<string, string>(); 198 - for (const e of readCarryEntities()) { 199 - const url = e.url?.trim().replace(/^"|"$/g, ""); 200 - if (url && url.startsWith("http")) { 201 - const name = e.name?.trim().replace(/^"|"$/g, ""); 202 - if (name) index.set(name, url); 203 - } 204 - } 205 - return index; 206 - } 207 - 208 - // ─── Build island text corpus ────────────────────────────────────── 209 - 210 - function buildIslandText(island: { vertices: VertexInfo[]; edges: EdgeInfo[] }): string { 211 - const parts: string[] = []; 212 - 213 - for (const v of island.vertices) { 214 - parts.push(v.title); 215 - if (v.description) parts.push(v.description); 216 - } 217 - 218 - for (const e of island.edges) { 219 - if (e.note) parts.push(e.note); 220 - } 221 - 222 - return parts.join(" "); 223 - } 224 - 225 - // ─── Core mapper: match carry semantic triples against island ────── 226 - 227 - function matchCarryData( 228 - island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 229 - islandUrls: Set<string>, 230 - carryEntities: CarryEntityRecord[], 231 - _carryCitations: CarryCitationRecord[], 232 - carryConnections: CarryConnectionRecord[], 233 - entityUrlIndex: Map<string, string>, 234 - _d1CarryData: CarryData, 235 - ): NewConnection[] { 236 - const connections: NewConnection[] = []; 237 - 238 - // ── 1. Carry connections (semantic triples) ────────────────────── 239 - // A carry connection is source → relation → target. 240 - // If one side's URL is in the island, the other side becomes a new connection. 241 - // If both sides are in the island, it's an existing edge (skip). 242 - // If neither side is in the island, it's a nearby carry connection 243 - // that could expand the island if linked. 244 - for (const conn of carryConnections) { 245 - const srcName = conn.source || ""; 246 - const tgtName = conn.target || ""; 247 - const srcUrl = entityUrlIndex.get(srcName) || ""; 248 - const tgtUrl = entityUrlIndex.get(tgtName) || ""; 249 - 250 - if (!srcUrl && !tgtUrl) continue; // Neither side has a URL — skip 251 - 252 - const srcInIsland = srcUrl && islandUrls.has(srcUrl); 253 - const tgtInIsland = tgtUrl && islandUrls.has(tgtUrl); 254 - 255 - // Both in island → already connected, skip 256 - if (srcInIsland && tgtInIsland) continue; 257 - 258 - // One side in island → bridge connection 259 - if (srcInIsland && tgtUrl) { 260 - connections.push({ 261 - source: srcUrl, 262 - target: tgtUrl, 263 - connectionType: `network.cosmik.connection#${conn.relation}`, 264 - note: conn.context || `${srcName} → ${conn.relation} → ${tgtName}`, 265 - }); 266 - } else if (tgtInIsland && srcUrl) { 267 - connections.push({ 268 - source: tgtUrl, 269 - target: srcUrl, 270 - connectionType: `network.cosmik.connection#${conn.relation}`, 271 - note: conn.context || `${srcName} → ${conn.relation} → ${tgtName}`, 272 - }); 273 - } else if (srcUrl && tgtUrl) { 274 - // Neither side in island — emit as a nearby carry connection 275 - // (both sides have URLs, so it's a real edge that could expand the island) 276 - connections.push({ 277 - source: srcUrl, 278 - target: tgtUrl, 279 - connectionType: `network.cosmik.connection#${conn.relation}`, 280 - note: conn.context || `${srcName} → ${conn.relation} → ${tgtName}`, 281 - }); 282 - } 283 - // One side has URL but the other doesn't — can't create a full connection 284 - } 285 - 286 - // ── 2. Carry entities with URLs not in island ─────────────────── 287 - // These are entities that the user tracks but aren't yet in the island. 288 - // We don't try to match them by keyword — we just list them as 289 - // "nearby entities" for the synthesis to reference. 290 - // (No NewConnection created — entities without a connection triple 291 - // don't have a semantic relation to the island.) 292 - 293 - return connections; 294 - } 295 - 296 - // ─── Analysis functions ───────────────────────────────────────────── 297 - 298 - function extractThemes(text: string): string[] { 299 - const keywords: Record<string, string[]> = { 300 - "open science": ["open science", "open access", "oa", "preprint"], 301 - "data sharing": ["data sharing", "shared data", "open data", "dataset"], 302 - "reproducibility": ["reproducib", "replicab", "replication"], 303 - "decentralization": ["decentraliz", "federat", "self-host"], 304 - "knowledge graphs": ["knowledge graph", "ontolog", "linked data", "semantic"], 305 - "ai/ml": ["machine learning", "deep learning", "neural", "llm", "gpt", "claude"], 306 - "community": ["community", "collective", "cooperative", "commons"], 307 - "governance": ["governance", "policy", "regulation", "standards"], 308 - "infrastructure": ["infrastructure", "platform", "tooling", "pipeline"], 309 - "stigmergy": ["stigmerg", "pheromone", "emergent", "self-organiz"], 310 - "provenance": ["provenance", "lineage", "traceability", "attribution"], 311 - "doi": ["doi", "zenodo", "citeable", "citation"], 312 - "privacy": ["privacy", "encryption", "e2ee", "consent"], 313 - "sovereignty": ["sovereignty", "self-determin", "autonomy", "agency"], 314 - "content authenticity": ["content authenticity", "c2pa", "provenance watermark", "deepfake"], 315 - "at protocol": ["atproto", "at protocol", "bluesky", "pds", "appview"], 316 - "semantic web": ["semantic web", "rdf", "sparql", "linked data"], 317 - }; 318 - 319 - const lower = text.toLowerCase(); 320 - return Object.entries(keywords) 321 - .filter(([_, terms]) => terms.some(t => lower.includes(t))) 322 - .map(([theme]) => theme); 323 - } 324 - 325 - function findHotEdges( 326 - islandText: string, 327 - island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 328 - newConnections: NewConnection[], 329 - ): string[] { 330 - const highlights: string[] = []; 331 - 332 - // Score each edge by semantic significance 333 - const edgeScores = new Map<string, number>(); 334 - 335 - for (const edge of island.edges) { 336 - let score = 0; 337 - 338 - // Edges with notes are more interesting 339 - if (edge.note && edge.note.length > 10) score += 1; 340 - 341 - // Edges with semantic connection types are more interesting 342 - const semanticTypes = ["supports", "contradicts", "extends", "contextualizes", "exemplifies"]; 343 - if (semanticTypes.some(t => edge.connectionType.toLowerCase().includes(t))) score += 2; 344 - 345 - // Boost edges whose source or target is also connected to carry data 346 - for (const nc of newConnections) { 347 - if (edge.source === nc.source || edge.target === nc.source) score += 3; 348 - if (edge.source === nc.target || edge.target === nc.target) score += 3; 349 - } 350 - 351 - if (score > 0) { 352 - edgeScores.set(edge.uri, score); 353 - } 354 - } 355 - 356 - // Return top edges by score 357 - const sorted = [...edgeScores.entries()].sort((a, b) => b[1] - a[1]); 358 - for (const [uri] of sorted.slice(0, 10)) { 359 - highlights.push(uri); 360 - } 361 - 362 - return highlights; 363 - } 364 - 365 - function findTensionsFromReasonings( 366 - islandText: string, 367 - reasonings: CarryReasoning[], 368 - ): string[] { 369 - const tensions: string[] = []; 370 - const lowerSet = tokenize(islandText); 371 - 372 - for (const r of reasonings) { 373 - const rLower = `${r.claim} ${r.evidence}`.toLowerCase(); 374 - if (rLower.includes("contradict") || rLower.includes("however") || rLower.includes("but") || rLower.includes("tension")) { 375 - const overlap = keywordOverlap(lowerSet, rLower); 376 - if (overlap.length >= 1) { 377 - tensions.push(r.claim.slice(0, 500)); 378 - } 379 - } 380 - } 381 - 382 - return tensions.slice(0, 5); 383 - } 384 - 385 - function generateOpenQuestions( 386 - islandText: string, 387 - island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 388 - highlights: string[], 389 - tensions: string[], 390 - newConnections: NewConnection[], 391 - ): string[] { 392 - const questions: string[] = []; 393 - const lower = islandText.toLowerCase(); 394 - 395 - if (newConnections.length > 0) { 396 - questions.push( 397 - `${newConnections.length} carry touchpoints found. How does your existing knowledge reshape this island?` 398 - ); 399 - } 400 - 401 - if (tensions.length > 0) { 402 - questions.push( 403 - `What evidence would resolve the ${tensions.length} tension(s) identified?` 404 - ); 405 - } 406 - 407 - const vertexCount = island.vertices.length; 408 - const edgeTypes = new Set(island.edges.map(e => e.connectionType)); 409 - 410 - if (edgeTypes.size > 1) { 411 - questions.push( 412 - `This island has ${edgeTypes.size} connection types. What's the dominant relationship?` 413 - ); 414 - } 415 - 416 - if (vertexCount > 5) { 417 - questions.push( 418 - `This is a ${vertexCount}-vertex cluster. Are there sub-clusters within it?` 419 - ); 420 - } 421 - 422 - if (lower.includes("data") || lower.includes("dataset")) { 423 - questions.push("What data artifacts does this cluster produce, and are they in Zenodo?"); 424 - } 425 - if (lower.includes("standard") || lower.includes("protocol")) { 426 - questions.push("Does this cluster propose a new standard, and who are the adopters?"); 427 - } 428 - 429 - return questions.slice(0, 5); 430 - } 431 - 432 - function synthesize( 433 - island: { vertices: VertexInfo[]; edges: EdgeInfo[] }, 434 - themes: string[], 435 - highlights: string[], 436 - tensions: string[], 437 - openQuestions: string[], 438 - newConnections: NewConnection[], 439 - ): string { 440 - const vertexCount = island.vertices.length; 441 - const edgeCount = island.edges.length; 442 - const edgeTypes = new Set(island.edges.map(e => e.connectionType)); 443 - 444 - const clusterDesc = `${vertexCount} vertices, ${edgeCount} edges, ${edgeTypes.size} connection types`; 445 - 446 - const themeStr = themes.length > 0 447 - ? `Themes: ${themes.join(", ")}.` 448 - : ""; 449 - 450 - const touchpointStr = newConnections.length > 0 451 - ? `${newConnections.length} carry touchpoints — new edges connecting this island to your knowledge base.` 452 - : "No carry touchpoints found."; 453 - 454 - const highlightStr = highlights.length > 0 455 - ? `${highlights.length} existing edges flagged as semantically significant.` 456 - : ""; 457 - 458 - const tensionStr = tensions.length > 0 459 - ? `There ${tensions.length === 1 ? "is 1 tension" : `are ${tensions.length} tensions`} with your existing claims.` 460 - : ""; 461 - 462 - const questionStr = openQuestions.length > 0 463 - ? `Open questions: ${openQuestions.join(" ")}` 464 - : ""; 465 - 466 - return [`Island (${clusterDesc}).`, themeStr, touchpointStr, highlightStr, tensionStr, questionStr].filter(Boolean).join(" "); 467 - } 468 - 469 - // ─── Utilities ────────────────────────────────────────────────────── 470 - 471 - const STOP_WORDS = new Set([ 472 - "the", "a", "an", "is", "are", "was", "were", "be", "been", "being", 473 - "have", "has", "had", "do", "does", "did", "will", "would", "could", 474 - "should", "may", "might", "shall", "can", "to", "of", "in", "for", 475 - "on", "with", "at", "by", "from", "as", "into", "through", "during", 476 - "before", "after", "above", "below", "between", "and", "but", "or", 477 - "not", "no", "nor", "so", "if", "then", "that", "this", "it", "its", 478 - "http", "https", "www", "com", "org", 479 - // Common vault noise terms 480 - "checkin", "check", "daily", "today", "yesterday", "tomorrow", 481 - "morning", "evening", "week", "month", "year", 482 - "also", "just", "like", "really", "very", "much", "some", 483 - "more", "most", "other", "about", "which", "when", "what", 484 - "there", "their", "these", "those", "each", "every", "all", 485 - "make", "made", "thing", "things", "something", "anything", 486 - "need", "want", "know", "think", "going", "come", "get", 487 - "good", "great", "nice", "well", "still", "even", "back", 488 - "first", "last", "new", "old", "own", "same", "another", 489 - "many", "such", "only", "over", "than", "too", "very", 490 - "because", "since", "while", "where", "how", "why", "who", 491 - "work", "working", "time", "people", "using", "used", "use", 492 - ]); 493 - 494 - function tokenize(text: string): Set<string> { 495 - return new Set( 496 - text.toLowerCase().split(/\W+/).filter(w => w.length > 3 && !STOP_WORDS.has(w)) 497 - ); 498 - } 499 - 500 - 501 - function keywordOverlap(setA: Set<string>, textB: string): string[] { 502 - const wordsB = textB.toLowerCase().split(/\W+/).filter(w => w.length > 3 && !STOP_WORDS.has(w)); 503 - return wordsB.filter(w => setA.has(w)); 504 - }
-7
container/package.json
··· 1 - { 2 - "name": "stigmergic-analysis", 3 - "version": "0.2.0", 4 - "private": true, 5 - "type": "module", 6 - "dependencies": {} 7 - }
-239
container/server.ts
··· 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 - 9 - import { analyze } from "./analysis.ts"; 10 - import { syncAll, syncVault, syncCarry } from "./sync.ts"; 11 - import { createServer } from "http"; 12 - import { 13 - execSync, 14 - } from "child_process"; 15 - 16 - const PORT = 8080; 17 - const CARRY_PATH = process.env.CARRY_PATH || "/data/carry"; 18 - 19 - createServer(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, existsSync } = 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({ 53 - status: "ok", 54 - vaultFiles, 55 - carryFiles, 56 - carryDomains, 57 - carryTokenSet: !!process.env.CARRY_GITHUB_TOKEN, 58 - obsidianTokenSet: !!process.env.OBSIDIAN_AUTH_TOKEN, 59 - obsidianConfigDir: existsSync("/root/.config/obsidian-headless/auth_token"), 60 - })); 61 - return; 62 - } 63 - 64 - // ── Sync ── 65 - if (url.pathname === "/sync" && req.method === "POST") { 66 - try { 67 - const { execSync } = await import("child_process"); 68 - const { existsSync, readFileSync } = await import("fs"); 69 - 70 - // Sync vault 71 - let vaultLog = ""; 72 - try { 73 - const out = execSync("ob sync --path /data/vault 2>&1", { timeout: 120000, encoding: "utf-8" }); 74 - vaultLog = out.slice(-500); 75 - } catch (e: any) { 76 - vaultLog = e.stdout?.slice(-500) || "" + e.stderr?.slice(-500) || "" || e.message?.slice(-500) || ""; 77 - } 78 - 79 - // Sync carry 80 - let carryLog = "already synced"; 81 - try { 82 - const out = execSync("git -C /data/carry pull origin main 2>&1", { timeout: 30000, encoding: "utf-8" }); 83 - carryLog = out.slice(-200); 84 - } catch (e: any) { 85 - carryLog = e.message?.slice(-200) || "pull failed"; 86 - } 87 - 88 - res.writeHead(200, { "Content-Type": "application/json" }); 89 - res.end(JSON.stringify({ ok: true, vaultLog, carryLog })); 90 - } catch (e: any) { 91 - res.writeHead(500, { "Content-Type": "application/json" }); 92 - res.end(JSON.stringify({ error: e.message })); 93 - } 94 - return; 95 - } 96 - 97 - // ── Derive Islands ── 98 - // Input: { connections: Array<{source, target, connectionType, note, did, rkey}>, seed?: string } 99 - // - With seed: derive single island containing that vertex 100 - // - Without seed: detect all islands (all connected components) 101 - // Output: { islands: Array<{id, lexmin, vertices, edges}> } 102 - if (url.pathname === "/deriveIsland" && req.method === "POST") { 103 - const chunks: Buffer[] = []; 104 - for await (const chunk of req) chunks.push(chunk); 105 - const body = Buffer.concat(chunks).toString(); 106 - 107 - try { 108 - const input = JSON.parse(body); 109 - const { connections, seed } = input; 110 - 111 - if (!Array.isArray(connections)) { 112 - res.writeHead(400, { "Content-Type": "application/json" }); 113 - res.end(JSON.stringify({ error: "connections[] required" })); 114 - return; 115 - } 116 - 117 - // Build adjacency list 118 - const adj = new Map<string, Array<typeof connections[0]>>(); 119 - const allVertices = new Set<string>(); 120 - for (const conn of connections) { 121 - const s = conn.source as string; 122 - const t = conn.target as string; 123 - if (!adj.has(s)) adj.set(s, []); 124 - if (!adj.has(t)) adj.set(t, []); 125 - adj.get(s)!.push(conn); 126 - adj.get(t)!.push(conn); 127 - allVertices.add(s); 128 - allVertices.add(t); 129 - } 130 - 131 - // BFS from a seed vertex to find one connected component 132 - function bfsComponent(start: string): { 133 - vertices: string[]; 134 - edges: any[]; 135 - } { 136 - const visited = new Set<string>(); 137 - const queue = [start]; 138 - const componentEdges: any[] = []; 139 - 140 - while (queue.length > 0) { 141 - const current = queue.shift()!; 142 - if (visited.has(current)) continue; 143 - visited.add(current); 144 - 145 - for (const conn of adj.get(current) || []) { 146 - const s = conn.source as string; 147 - const t = conn.target as string; 148 - componentEdges.push({ 149 - uri: `at://${conn.did}/network.cosmik.connection/${conn.rkey}`, 150 - did: conn.did, 151 - rkey: conn.rkey, 152 - source: s, 153 - target: t, 154 - connectionType: conn.connectionType || "relates", 155 - note: conn.note || "", 156 - }); 157 - if (!visited.has(s)) queue.push(s); 158 - if (!visited.has(t)) queue.push(t); 159 - } 160 - } 161 - 162 - // Dedupe edges 163 - const vertexSet = new Set(visited); 164 - const edges = componentEdges.filter(e => vertexSet.has(e.source) && vertexSet.has(e.target)); 165 - const seenEdges = new Set<string>(); 166 - const dedupedEdges = edges.filter(e => { 167 - const key = `${e.source}|${e.target}|${e.rkey}`; 168 - if (seenEdges.has(key)) return false; 169 - seenEdges.add(key); 170 - return true; 171 - }); 172 - 173 - const sortedVertices = [...visited].sort(); 174 - return { vertices: sortedVertices, edges: dedupedEdges }; 175 - } 176 - 177 - const { createHash } = await import("crypto"); 178 - 179 - if (seed) { 180 - // Single island from seed vertex 181 - const { vertices, edges } = bfsComponent(seed); 182 - const lexmin = vertices[0]; 183 - const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16); 184 - res.writeHead(200, { "Content-Type": "application/json" }); 185 - res.end(JSON.stringify({ islands: [{ id, lexmin, vertices, edges }] })); 186 - } else { 187 - // All islands — find all connected components 188 - const globalVisited = new Set<string>(); 189 - const islands: any[] = []; 190 - 191 - for (const vertex of allVertices) { 192 - if (globalVisited.has(vertex)) continue; 193 - const { vertices, edges } = bfsComponent(vertex); 194 - for (const v of vertices) globalVisited.add(v); 195 - const lexmin = vertices[0]; 196 - const id = createHash("sha256").update(lexmin).digest("hex").slice(0, 16); 197 - islands.push({ id, lexmin, vertices, edges }); 198 - } 199 - 200 - // Sort by vertex count descending 201 - islands.sort((a, b) => b.vertices.length - a.vertices.length); 202 - 203 - res.writeHead(200, { "Content-Type": "application/json" }); 204 - res.end(JSON.stringify({ islands })); 205 - } 206 - } catch (e: any) { 207 - console.error("deriveIsland error:", e); 208 - res.writeHead(500, { "Content-Type": "application/json" }); 209 - res.end(JSON.stringify({ error: e.message })); 210 - } 211 - return; 212 - } 213 - 214 - // ── Analyze ── 215 - if (url.pathname === "/analyze" && req.method === "POST") { 216 - const chunks: Buffer[] = []; 217 - for await (const chunk of req) chunks.push(chunk); 218 - const body = Buffer.concat(chunks).toString(); 219 - 220 - try { 221 - const input = JSON.parse(body); 222 - const result = await analyze(input); 223 - res.writeHead(200, { "Content-Type": "application/json" }); 224 - res.end(JSON.stringify(result)); 225 - } catch (e: any) { 226 - console.error("Analysis error:", e); 227 - res.writeHead(500, { "Content-Type": "application/json" }); 228 - res.end(JSON.stringify({ error: e.message })); 229 - } 230 - return; 231 - } 232 - 233 - res.writeHead(404); 234 - res.end("Not found"); 235 - }).listen(PORT, () => { 236 - console.log(`Strata analysis container listening on port ${PORT}`); 237 - // Sync on startup 238 - syncAll().catch(e => console.error("Initial sync failed:", e)); 239 - });
-151
container/sync.ts
··· 1 - // ─── Sync: pull vault + carry into container ────────────────────── 2 - // 3 - // vault: ob sync (Obsidian CLI) — needs auth token + vault config 4 - // carry: git pull from GitHub private repo (auth via CARRY_GITHUB_TOKEN) 5 - 6 - import { execSync } from "child_process"; 7 - import { existsSync, writeFileSync, mkdirSync } from "fs"; 8 - import { join } from "path"; 9 - 10 - const VAULT_PATH = process.env.VAULT_PATH || "/data/vault"; 11 - const CARRY_PATH = process.env.CARRY_PATH || "/data/carry"; 12 - const CARRY_GITHUB_TOKEN = process.env.CARRY_GITHUB_TOKEN || ""; 13 - const CARRY_BRANCH = process.env.CARRY_BRANCH || "main"; 14 - const CARRY_REPO = "codegod100/carry-vault.git"; 15 - const OBSIDIAN_AUTH_TOKEN = process.env.OBSIDIAN_AUTH_TOKEN || ""; 16 - const OBSIDIAN_VAULT_ID = process.env.OBSIDIAN_VAULT_ID || "6472c50492efb918c2878e61048218f2"; 17 - const OBSIDIAN_VAULT_NAME = process.env.OBSIDIAN_VAULT_NAME || "notes"; 18 - const OBSIDIAN_SYNC_HOST = process.env.OBSIDIAN_SYNC_HOST || "sync-53.obsidian.md"; 19 - const OBSIDIAN_ENCRYPTION_KEY = process.env.OBSIDIAN_ENCRYPTION_KEY || ""; 20 - const OBSIDIAN_ENCRYPTION_SALT = process.env.OBSIDIAN_ENCRYPTION_SALT || ""; 21 - 22 - function getCarryRemote(): string { 23 - if (CARRY_GITHUB_TOKEN) { 24 - return `https://${CARRY_GITHUB_TOKEN}@github.com/${CARRY_REPO}`; 25 - } 26 - return ""; 27 - } 28 - 29 - function setupObsidianAuth(): boolean { 30 - if (!OBSIDIAN_AUTH_TOKEN) { 31 - console.error("[sync] OBSIDIAN_AUTH_TOKEN not set, skipping vault sync"); 32 - return false; 33 - } 34 - 35 - try { 36 - // Write auth token 37 - const configDir = "/root/.config/obsidian-headless"; 38 - mkdirSync(configDir, { recursive: true }); 39 - writeFileSync(join(configDir, "auth_token"), OBSIDIAN_AUTH_TOKEN, { mode: 0o600 }); 40 - 41 - // Write vault sync config 42 - if (OBSIDIAN_ENCRYPTION_KEY && OBSIDIAN_ENCRYPTION_SALT) { 43 - const syncDir = join(configDir, "sync", OBSIDIAN_VAULT_ID); 44 - mkdirSync(syncDir, { recursive: true }); 45 - const vaultConfig = { 46 - vaultId: OBSIDIAN_VAULT_ID, 47 - vaultName: OBSIDIAN_VAULT_NAME, 48 - vaultPath: VAULT_PATH, 49 - host: OBSIDIAN_SYNC_HOST, 50 - encryptionVersion: 3, 51 - encryptionKey: OBSIDIAN_ENCRYPTION_KEY, 52 - encryptionSalt: OBSIDIAN_ENCRYPTION_SALT, 53 - conflictStrategy: "merge", 54 - deviceName: "strata-container", 55 - }; 56 - writeFileSync(join(syncDir, "config.json"), JSON.stringify(vaultConfig, null, 2), { mode: 0o600 }); 57 - } 58 - 59 - console.log("[sync] obsidian auth configured"); 60 - return true; 61 - } catch (e: any) { 62 - console.error("[sync] obsidian auth setup failed:", e.message); 63 - return false; 64 - } 65 - } 66 - 67 - export async function syncVault(): Promise<void> { 68 - if (!setupObsidianAuth()) return; 69 - 70 - try { 71 - // First time: setup sync link 72 - if (!existsSync(join(VAULT_PATH, ".obsidian"))) { 73 - try { 74 - execSync(`ob sync-setup --vault ${OBSIDIAN_VAULT_ID} --path ${VAULT_PATH}`, { 75 - stdio: "pipe", 76 - timeout: 30000, 77 - }); 78 - } catch (e: any) { 79 - // sync-setup may fail if already linked; try sync directly 80 - console.error("[sync] sync-setup failed (may already be linked):", e.message?.slice(0, 100)); 81 - } 82 - } 83 - 84 - execSync(`ob sync --path ${VAULT_PATH}`, { stdio: "pipe", timeout: 120000 }); 85 - console.log("[sync] vault synced via ob"); 86 - } catch (e: any) { 87 - console.error("[sync] vault sync failed:", e.message?.slice(0, 200)); 88 - } 89 - } 90 - 91 - export async function syncCarry(): Promise<void> { 92 - const remote = getCarryRemote(); 93 - if (!remote) { 94 - console.error("[sync] CARRY_GITHUB_TOKEN not set, skipping carry sync"); 95 - return; 96 - } 97 - 98 - try { 99 - // If not cloned yet, clone; otherwise pull 100 - try { 101 - execSync(`git -C ${CARRY_PATH} rev-parse --git-dir`, { stdio: "pipe" }); 102 - execSync(`git -C ${CARRY_PATH} pull origin ${CARRY_BRANCH}`, { 103 - stdio: "pipe", 104 - timeout: 30000, 105 - env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, 106 - }); 107 - console.log("[sync] carry pulled via git"); 108 - } catch { 109 - // Not a git repo — clone 110 - execSync(`git clone --branch ${CARRY_BRANCH} --single-branch ${remote} ${CARRY_PATH}`, { 111 - stdio: "pipe", 112 - timeout: 60000, 113 - env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, 114 - }); 115 - console.log("[sync] carry cloned via git"); 116 - } 117 - } catch (e: any) { 118 - console.error("[sync] carry sync failed:", e.message?.slice(0, 200)); 119 - } 120 - } 121 - 122 - function exportCarryJson(): void { 123 - try { 124 - const domains = [ 125 - { domain: "org.latha.entity", fields: "name url type description" }, 126 - { domain: "org.latha.citation", fields: "title url takeaway" }, 127 - { domain: "org.latha.connection", fields: "relation source target context" }, 128 - ]; 129 - 130 - for (const { domain, fields } of domains) { 131 - const outPath = join(CARRY_PATH, `${domain}.json`); 132 - const cmd = `carry query ${domain} ${fields} --format json`; 133 - const result = execSync(cmd, { 134 - cwd: CARRY_PATH, 135 - encoding: "utf-8", 136 - timeout: 30000, 137 - }); 138 - writeFileSync(outPath, result); 139 - const count = JSON.parse(result).length; 140 - console.log(`[sync] exported ${domain}: ${count} records`); 141 - } 142 - } catch (e: any) { 143 - console.error("[sync] carry export failed:", e.message?.slice(0, 200)); 144 - } 145 - } 146 - 147 - export async function syncAll(): Promise<void> { 148 - await Promise.all([syncVault(), syncCarry()]); 149 - // Always export carry JSON if carry vault exists — even without git sync 150 - exportCarryJson(); 151 - }
+5 -1
lexicons/org/latha/island/island.json
··· 133 133 "type": "string", 134 134 "maxGraphemes": 2000, 135 135 "maxLength": 20000, 136 - "description": "Prose synthesis connecting the source to existing knowledge." 136 + "description": "Prose synthesis connecting the source to existing knowledge. Legacy plain-text format." 137 + }, 138 + "synthesisDoc": { 139 + "type": "unknown", 140 + "description": "Rich-text synthesis as a pub.oxa.document record. Takes precedence over the plain-text synthesis field when present." 137 141 } 138 142 } 139 143 },
+6 -3
package.json
··· 11 11 "island:explore": "bun run src/island-explore.ts", 12 12 "island:detect": "bun run src/island-detect.ts", 13 13 "island:discover": "bun run src/island-discover.ts", 14 + "island:search": "bun run src/island-search.ts", 14 15 "island:analyze": "bun run src/island-analyze.ts", 15 16 "connection:detect": "bun run src/connection-detect.ts", 16 17 "island:publish": "bun run src/island-publish.ts", 17 18 "island:cleanup": "bun run src/island-cleanup.ts", 18 - "vault:analyze": "bun run src/vault-analyze.ts" 19 + "vault:analyze": "bun run src/vault-analyze.ts", 20 + "synthesis:to-oxa": "bun run src/synthesis-to-oxa.ts" 19 21 }, 20 22 "dependencies": { 21 23 "@atmo-dev/contrail": "^0.6.0", 24 + "@atproto/api": "^0.19.17", 22 25 "@cloudflare/containers": "^0.1.0", 23 - "hono": "^4.7.0", 24 - "@atproto/api": "^0.19.17", 25 26 "@letta-ai/letta-code-sdk": "^0.1.14", 27 + "@nandithebull/myst-oxa": "^0.1.8", 28 + "hono": "^4.7.0", 26 29 "jsdom": "^29.1.1" 27 30 }, 28 31 "devDependencies": {
+926
pnpm-lock.yaml
··· 20 20 '@letta-ai/letta-code-sdk': 21 21 specifier: ^0.1.14 22 22 version: 0.1.14(ink@7.0.3(react@19.2.6)) 23 + '@nandithebull/myst-oxa': 24 + specifier: ^0.1.8 25 + version: 0.1.8 23 26 hono: 24 27 specifier: ^4.7.0 25 28 version: 4.12.18 ··· 858 861 '@mary-ext/simple-event-emitter@1.0.1': 859 862 resolution: {integrity: sha512-9+VvZisxZ/gSg+JJH7hmXaA8Qj42Qjz3O58RSB+INYc8iLA0icATZxHB9vKbj59ojDGZjO3hCKzMXocx3L0H8w==} 860 863 864 + '@msgpack/msgpack@3.1.3': 865 + resolution: {integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==} 866 + engines: {node: '>= 18'} 867 + 868 + '@nandithebull/myst-oxa@0.1.8': 869 + resolution: {integrity: sha512-KmYcTRhDmtTVv8gvilu9sH205wPbBt0kC+so0D59gNTajx9NgGnrvmqarLv2maBMZX92yOBG2qW7lctiEv6Rnw==} 870 + 861 871 '@noble/secp256k1@3.1.0': 862 872 resolution: {integrity: sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==} 863 873 ··· 869 879 resolution: {integrity: sha512-0Wc+zC8SLGV8zXQX+pk+o0c6wE/ddx/36CHZ0toTh5lApsjruUuGhqbxvljerAAG5un1xQbOLxzksBVC6UPgSg==} 870 880 engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} 871 881 882 + '@panproto/core@0.43.3': 883 + resolution: {integrity: sha512-tk+G4AFobdplytiLqusahsVGtChzelTUowEGKMo2bN7CUloVpUKt3kmahJRITPOZZfzm8717lx2++rg75XjX+w==} 884 + engines: {node: '>=20'} 885 + 872 886 '@poppinss/colors@4.1.6': 873 887 resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} 874 888 ··· 888 902 '@standard-schema/spec@1.1.0': 889 903 resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 890 904 905 + '@types/extend@3.0.4': 906 + resolution: {integrity: sha512-ArMouDUTJEz1SQRpFsT2rIw7DeqICFv5aaVzLSIYMYQSLcwcGOfT3VyglQs/p7K3F7fT4zxr0NWxYZIdifD6dA==} 907 + 908 + '@types/hast@2.3.10': 909 + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} 910 + 891 911 '@types/hast@3.0.4': 892 912 resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} 893 913 914 + '@types/linkify-it@3.0.5': 915 + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} 916 + 917 + '@types/markdown-it@13.0.9': 918 + resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} 919 + 920 + '@types/mdast@3.0.15': 921 + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} 922 + 923 + '@types/mdurl@1.0.5': 924 + resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} 925 + 926 + '@types/parse5@6.0.3': 927 + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} 928 + 929 + '@types/unist@2.0.11': 930 + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} 931 + 894 932 '@types/unist@3.0.3': 895 933 resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 896 934 ··· 969 1007 resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} 970 1008 engines: {node: '>=12'} 971 1009 1010 + argparse@2.0.1: 1011 + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 1012 + 972 1013 auto-bind@5.0.1: 973 1014 resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} 974 1015 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 975 1016 976 1017 await-lock@2.2.2: 977 1018 resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 1019 + 1020 + bail@2.0.2: 1021 + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} 978 1022 979 1023 balanced-match@4.0.4: 980 1024 resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} ··· 986 1030 blake3-wasm@2.1.5: 987 1031 resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 988 1032 1033 + boolbase@1.0.0: 1034 + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} 1035 + 989 1036 brace-expansion@5.0.6: 990 1037 resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} 991 1038 engines: {node: 18 || 20 || >=22} ··· 997 1044 cac@7.0.0: 998 1045 resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} 999 1046 engines: {node: '>=20.19.0'} 1047 + 1048 + ccount@2.0.1: 1049 + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} 1000 1050 1001 1051 chalk@5.6.2: 1002 1052 resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} 1003 1053 engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 1004 1054 1055 + character-entities-html4@2.1.0: 1056 + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} 1057 + 1058 + character-entities-legacy@3.0.0: 1059 + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} 1060 + 1061 + classnames@2.5.1: 1062 + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} 1063 + 1005 1064 cli-boxes@4.0.1: 1006 1065 resolution: {integrity: sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==} 1007 1066 engines: {node: '>=18.20 <19 || >=20.10'} ··· 1018 1077 resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} 1019 1078 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1020 1079 1080 + comma-separated-tokens@2.0.3: 1081 + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 1082 + 1021 1083 convert-to-spaces@2.0.1: 1022 1084 resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} 1023 1085 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ··· 1029 1091 core-js@3.49.0: 1030 1092 resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} 1031 1093 1094 + credit-roles@2.1.0: 1095 + resolution: {integrity: sha512-wt1jw7lDnzY1Ob4cDHpXboN4Bfu6l7reKal0zxtKnxogqw916l+Iu862LxIze4U4y44ssAd0TOQqVg2cXY2V6Q==} 1096 + 1097 + css-selector-parser@1.4.1: 1098 + resolution: {integrity: sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==} 1099 + 1032 1100 css-tree@3.2.1: 1033 1101 resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} 1034 1102 engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} 1103 + 1104 + csv-parse@5.6.0: 1105 + resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==} 1035 1106 1036 1107 data-urls@7.0.0: 1037 1108 resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} ··· 1063 1134 devlop@1.1.0: 1064 1135 resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 1065 1136 1137 + doi-utils@2.0.6: 1138 + resolution: {integrity: sha512-QkaDmWbr5lIugDgl9fCxDMos2OJJ9Rp4c9XB7wgRGm6hJQ+hgeqI78DqToU8+vmtU+1XAyyVYJoLZUAgBdsFtg==} 1139 + 1140 + entities@3.0.1: 1141 + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} 1142 + engines: {node: '>=0.12'} 1143 + 1066 1144 entities@8.0.0: 1067 1145 resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} 1068 1146 engines: {node: '>=20.19.0'} ··· 1091 1169 resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} 1092 1170 engines: {node: '>=8'} 1093 1171 1172 + escape-string-regexp@5.0.0: 1173 + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} 1174 + engines: {node: '>=12'} 1175 + 1094 1176 esm-env@1.2.2: 1095 1177 resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 1096 1178 1097 1179 event-target-polyfill@0.0.4: 1098 1180 resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} 1099 1181 1182 + extend@3.0.2: 1183 + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} 1184 + 1100 1185 fsevents@2.3.3: 1101 1186 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1102 1187 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} ··· 1114 1199 resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} 1115 1200 engines: {node: '>=12'} 1116 1201 1202 + hast-util-embedded@2.0.1: 1203 + resolution: {integrity: sha512-QUdSOP1/o+/TxXtpPFXR2mUg2P+ySrmlX7QjwHZCXqMFyYk7YmcGSvqRW+4XgXAoHifdE1t2PwFaQK33TqVjSw==} 1204 + 1205 + hast-util-from-parse5@7.1.2: 1206 + resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} 1207 + 1208 + hast-util-has-property@2.0.1: 1209 + resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==} 1210 + 1211 + hast-util-is-body-ok-link@2.0.0: 1212 + resolution: {integrity: sha512-S58hCexyKdD31vMsErvgLfflW6vYWo/ixRLPJTtkOvLld24vyI8vmYmkgLA5LG3la2ME7nm7dLGdm48gfLRBfw==} 1213 + 1214 + hast-util-is-element@2.1.3: 1215 + resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} 1216 + 1217 + hast-util-parse-selector@3.1.1: 1218 + resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} 1219 + 1220 + hast-util-phrasing@2.0.2: 1221 + resolution: {integrity: sha512-yGkCfPkkfCyiLfK6KEl/orMDr/zgCnq/NaO9HfULx6/Zga5fso5eqQA5Ov/JZVqACygvw9shRYWgXNcG2ilo7w==} 1222 + 1223 + hast-util-raw@7.2.3: 1224 + resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==} 1225 + 1226 + hast-util-to-html@8.0.4: 1227 + resolution: {integrity: sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==} 1228 + 1229 + hast-util-to-mdast@8.4.1: 1230 + resolution: {integrity: sha512-tfmBLASuCgyhCzpkTXM5kU8xeuS5jkMZ17BYm2YftGT5wvgc7uHXTZ/X8WfNd6F5NV/IGmrLsuahZ+jXQir4zQ==} 1231 + 1232 + hast-util-to-parse5@7.1.0: 1233 + resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} 1234 + 1235 + hast-util-to-text@3.1.2: 1236 + resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==} 1237 + 1238 + hast-util-whitespace@2.0.1: 1239 + resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} 1240 + 1241 + hast@1.0.0: 1242 + resolution: {integrity: sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==} 1243 + deprecated: Renamed to rehype 1244 + 1245 + hastscript@7.2.0: 1246 + resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} 1247 + 1248 + he@1.2.0: 1249 + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 1250 + hasBin: true 1251 + 1117 1252 highlight.js@11.11.1: 1118 1253 resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} 1119 1254 engines: {node: '>=12.0.0'} ··· 1126 1261 resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} 1127 1262 engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1128 1263 1264 + html-void-elements@2.0.1: 1265 + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} 1266 + 1267 + html-whitespace-sensitive-tag-names@2.0.0: 1268 + resolution: {integrity: sha512-SQdIvTTtnHAx72xGUIUudvVOCjeWvV1U7rvSFnNGxTGRw3ZC7RES4Gw6dm1nMYD60TXvm6zjk/bWqgNc5pjQaw==} 1269 + 1129 1270 indent-string@5.0.0: 1130 1271 resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} 1131 1272 engines: {node: '>=12'} ··· 1149 1290 react-devtools-core: 1150 1291 optional: true 1151 1292 1293 + is-buffer@2.0.5: 1294 + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} 1295 + engines: {node: '>=4'} 1296 + 1152 1297 is-docker@3.0.0: 1153 1298 resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} 1154 1299 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ··· 1168 1313 engines: {node: '>=14.16'} 1169 1314 hasBin: true 1170 1315 1316 + is-plain-obj@4.1.0: 1317 + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} 1318 + engines: {node: '>=12'} 1319 + 1171 1320 is-potential-custom-element-name@1.0.1: 1172 1321 resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} 1173 1322 ··· 1185 1334 jose@5.10.0: 1186 1335 resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} 1187 1336 1337 + js-yaml@4.1.1: 1338 + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 1339 + hasBin: true 1340 + 1188 1341 jsdom@29.1.1: 1189 1342 resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} 1190 1343 engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} ··· 1194 1347 canvas: 1195 1348 optional: true 1196 1349 1350 + json5@1.0.2: 1351 + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} 1352 + hasBin: true 1353 + 1197 1354 kleur@4.1.5: 1198 1355 resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 1199 1356 engines: {node: '>=6'} 1357 + 1358 + linkify-it@4.0.1: 1359 + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} 1200 1360 1201 1361 lowlight@3.3.0: 1202 1362 resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} ··· 1208 1368 resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} 1209 1369 engines: {node: 20 || >=22} 1210 1370 1371 + markdown-it-amsmath@0.4.0: 1372 + resolution: {integrity: sha512-wWGX5bpHu9iq4cqi/U58srCH0js5ws7X3BrQZcN2/anQ9S4P8MpvTxHLCZC2rmGHA6mSZf4PKWF6caBc+nxH2g==} 1373 + engines: {node: '>=16', npm: '>=6'} 1374 + peerDependencies: 1375 + markdown-it: ^12 || ^13 1376 + 1377 + markdown-it-deflist@2.1.0: 1378 + resolution: {integrity: sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==} 1379 + 1380 + markdown-it-dollarmath@0.5.0: 1381 + resolution: {integrity: sha512-W+8se6cx6vowjzRAfDbHDPBQ+Y9G/8M/JZSFiaYvT0HqfwyFK1hIA1Xj360Nc3ymknKgdDVoL5fI8XjAqZF3tg==} 1382 + engines: {node: '>=16', npm: '>=6'} 1383 + peerDependencies: 1384 + markdown-it: ^12 || ^13 1385 + 1386 + markdown-it-footnote@4.0.0: 1387 + resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==} 1388 + 1389 + markdown-it-front-matter@0.2.4: 1390 + resolution: {integrity: sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w==} 1391 + 1392 + markdown-it-myst-extras@0.3.0: 1393 + resolution: {integrity: sha512-678qviK97MEzSM9Hr0jlX5nBPzMcKZo6Ixgh4nEf/WYpii8LXQ72FametoXkzyDy77qNKDE3vlqYhqfbbCGHrw==} 1394 + engines: {node: '>=16', npm: '>=6'} 1395 + peerDependencies: 1396 + markdown-it: ^12 || ^13 1397 + 1398 + markdown-it-myst@1.0.16: 1399 + resolution: {integrity: sha512-iwyL6J/1bN8rUO5RpzLIcyQut3W7dO8eCl6uj8B5tkrCCgKXJ71gWr+/JWxeAi9VRNp3TMiRf+OjMy0Bh7yZLQ==} 1400 + peerDependencies: 1401 + '@types/markdown-it': ^13.0.1 1402 + markdown-it: ^13.0.1 1403 + 1404 + markdown-it-task-lists@2.1.1: 1405 + resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==} 1406 + 1407 + markdown-it@13.0.2: 1408 + resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} 1409 + hasBin: true 1410 + 1411 + mdast-util-definitions@5.1.2: 1412 + resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} 1413 + 1414 + mdast-util-find-and-replace@2.2.2: 1415 + resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} 1416 + 1417 + mdast-util-phrasing@3.0.1: 1418 + resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} 1419 + 1420 + mdast-util-to-hast@12.3.0: 1421 + resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} 1422 + 1423 + mdast-util-to-string@3.2.0: 1424 + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} 1425 + 1426 + mdast@3.0.0: 1427 + resolution: {integrity: sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g==} 1428 + deprecated: '`mdast` was renamed to `remark`' 1429 + 1211 1430 mdn-data@2.27.1: 1212 1431 resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} 1213 1432 1433 + mdurl@1.0.1: 1434 + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} 1435 + 1436 + micromark-util-character@1.2.0: 1437 + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} 1438 + 1439 + micromark-util-encode@1.1.0: 1440 + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} 1441 + 1442 + micromark-util-sanitize-uri@1.2.0: 1443 + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} 1444 + 1445 + micromark-util-symbol@1.1.0: 1446 + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} 1447 + 1448 + micromark-util-types@1.1.0: 1449 + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} 1450 + 1214 1451 mimic-fn@2.1.0: 1215 1452 resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} 1216 1453 engines: {node: '>=6'} ··· 1223 1460 minimatch@10.2.5: 1224 1461 resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} 1225 1462 engines: {node: 18 || 20 || >=22} 1463 + 1464 + minimist@1.2.8: 1465 + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 1226 1466 1227 1467 minipass@7.1.3: 1228 1468 resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} ··· 1231 1471 multiformats@9.9.0: 1232 1472 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 1233 1473 1474 + myst-common@1.9.5: 1475 + resolution: {integrity: sha512-GacGYFZVqCfed1pYLg9ilGLhZYYc/Q75w5jEl+ceHlZ8G4h2oGwexiQD4s/hwARSgnaMMeyqSwLMiG5ckAtNww==} 1476 + 1477 + myst-directives@1.7.2: 1478 + resolution: {integrity: sha512-BxTSzir+qxLyzreESUgm5FGofdGY3RbW0EqOExrXwoYt/Gagifp/TGqSbXda37MGc/IHJondeIO7ep58nWfIlA==} 1479 + 1480 + myst-frontmatter@1.9.5: 1481 + resolution: {integrity: sha512-Zu/5dTUqWWOdEvcM/yLA2GJ1MJXa/wDf4+dUAc4n9eNUhFNZB326CawFZugSflYDgulBQmWNXCFozmg9e6Rnlg==} 1482 + 1483 + myst-parser@1.7.2: 1484 + resolution: {integrity: sha512-I4y51fqRXnGqnUB6kafedRPITYAQRUiokrZz4IjOwNpkV1xc55by592y1ouMCf317qvkvKxK/jY+y+QXcKjwtA==} 1485 + 1486 + myst-roles@1.7.2: 1487 + resolution: {integrity: sha512-BeWRzb1kiTmOLYS8L1bjFv3URPWaL44Nqux7MnQEQ4Eo65IRDwMZQqcrIGFvBb5oF/JPIX2dc7OyaG2ombgN1A==} 1488 + 1489 + myst-spec-ext@1.9.5: 1490 + resolution: {integrity: sha512-7PxsapR+ClT0BxbGwitdXXaRmJ1j44hqi2dvvxMxbNTOrgNTsFskHbNyABAxqE6gs8hpZTkJcoApJ5y03i3nMA==} 1491 + 1492 + myst-spec@0.0.5: 1493 + resolution: {integrity: sha512-L/4TV1l5ZbWUOgSnXqiYrx192SV4I+HqjX7TBQ4k02/heeNFckpkUIyLulraap5heTyLcJs8UYBxu+Kv5JiiRw==} 1494 + 1495 + myst-to-html@1.7.2: 1496 + resolution: {integrity: sha512-AMwhTJEt10HcYlWBhylCYDiaJ9gMslINTlTBUVRrRDSGFbyQdSg8rteVL/QwZdqmgDKJbV3X3pgG+ZpUG5O/ww==} 1497 + 1498 + myst-toc@0.1.4: 1499 + resolution: {integrity: sha512-/SVB4GDv3A+pdkliYEdJv6cUj5RnDX+Qb7zuzNs3COUhGUaUK/T8bX/tDFqOquxFBX/RVoOstQVHOkVu6ui6nQ==} 1500 + 1234 1501 nanoid@5.1.11: 1235 1502 resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==} 1236 1503 engines: {node: ^18 || >=20} ··· 1242 1509 node-pty@1.1.0: 1243 1510 resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} 1244 1511 1512 + nth-check@2.1.1: 1513 + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} 1514 + 1245 1515 onetime@5.1.2: 1246 1516 resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} 1247 1517 engines: {node: '>=6'} ··· 1250 1520 resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} 1251 1521 engines: {node: '>=18'} 1252 1522 1523 + orcid@1.0.0: 1524 + resolution: {integrity: sha512-zdRFh+1FAZt0Vy9XaKNu+5h4zXC2pkunZH8MV7tSGoq54skik8nxn5ks3E9f33m+tPxuliSRzEHz7owb1Z0HYg==} 1525 + hasBin: true 1526 + 1527 + parse5@6.0.1: 1528 + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} 1529 + 1253 1530 parse5@8.0.1: 1254 1531 resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} 1255 1532 ··· 1283 1560 engines: {node: '>=14'} 1284 1561 hasBin: true 1285 1562 1563 + property-information@6.5.0: 1564 + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} 1565 + 1286 1566 punycode@2.3.1: 1287 1567 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1288 1568 engines: {node: '>=6'} ··· 1297 1577 resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} 1298 1578 engines: {node: '>=0.10.0'} 1299 1579 1580 + rehype-format@4.0.1: 1581 + resolution: {integrity: sha512-HA92WeqFri00yiClrz54IIpM9no2DH9Mgy5aFmInNODoAYn+hN42a6oqJTIie2nj0HwFyV7JvOYx5YHBphN8mw==} 1582 + 1583 + rehype-minify-whitespace@5.0.1: 1584 + resolution: {integrity: sha512-PPp4lWJiBPlePI/dv1BeYktbwkfgXkrK59MUa+tYbMPgleod+4DvFK2PLU0O0O60/xuhHfiR9GUIUlXTU8sRIQ==} 1585 + 1586 + rehype-parse@8.0.5: 1587 + resolution: {integrity: sha512-Ds3RglaY/+clEX2U2mHflt7NlMA72KspZ0JLUJgBBLpRddBcEw3H8uYZQliQriku22NZpYMfjDdSgHcjxue24A==} 1588 + 1589 + rehype-remark@9.1.2: 1590 + resolution: {integrity: sha512-c0fG3/CrJ95zAQ07xqHSkdpZybwdsY7X5dNWvgL2XqLKZuqmG3+vk6kP/4miCnp+R+x/0uKKRSpfXb9aGR8Z5w==} 1591 + 1592 + rehype-stringify@9.0.4: 1593 + resolution: {integrity: sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ==} 1594 + 1300 1595 require-from-string@2.0.2: 1301 1596 resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 1302 1597 engines: {node: '>=0.10.0'} ··· 1328 1623 signal-exit@3.0.7: 1329 1624 resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 1330 1625 1626 + simple-validators@1.2.0: 1627 + resolution: {integrity: sha512-YzlgCBDukw6OeKN5QjliD/3jERQBH0zGr5WPUrEPD8pteOcg+QGJFP++T7XOwjhKKLZBPLWEmDv+pwjKPzHLVg==} 1628 + 1331 1629 slice-ansi@9.0.0: 1332 1630 resolution: {integrity: sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==} 1333 1631 engines: {node: '>=22'} ··· 1336 1634 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1337 1635 engines: {node: '>=0.10.0'} 1338 1636 1637 + space-separated-tokens@2.0.2: 1638 + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} 1639 + 1640 + spdx-correct@3.2.0: 1641 + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} 1642 + 1643 + spdx-exceptions@2.5.0: 1644 + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} 1645 + 1646 + spdx-expression-parse@3.0.1: 1647 + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} 1648 + 1649 + spdx-license-ids@3.0.23: 1650 + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} 1651 + 1339 1652 stack-utils@2.0.6: 1340 1653 resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} 1341 1654 engines: {node: '>=10'} ··· 1343 1656 string-width@8.2.1: 1344 1657 resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} 1345 1658 engines: {node: '>=20'} 1659 + 1660 + stringify-entities@4.0.4: 1661 + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} 1346 1662 1347 1663 strip-ansi@7.2.0: 1348 1664 resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} ··· 1390 1706 resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} 1391 1707 engines: {node: '>=20'} 1392 1708 1709 + trim-lines@3.0.1: 1710 + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} 1711 + 1712 + trim-trailing-lines@2.1.0: 1713 + resolution: {integrity: sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==} 1714 + 1715 + trough@2.2.0: 1716 + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} 1717 + 1393 1718 tslib@2.8.1: 1394 1719 resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1395 1720 ··· 1406 1731 engines: {node: '>=14.17'} 1407 1732 hasBin: true 1408 1733 1734 + uc.micro@1.0.6: 1735 + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} 1736 + 1409 1737 uint8arrays@3.0.0: 1410 1738 resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} 1411 1739 ··· 1423 1751 unicode-segmenter@0.14.5: 1424 1752 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 1425 1753 1754 + unified@10.1.2: 1755 + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} 1756 + 1757 + unist-builder@3.0.1: 1758 + resolution: {integrity: sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==} 1759 + 1760 + unist-util-find-after@4.0.1: 1761 + resolution: {integrity: sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==} 1762 + 1763 + unist-util-generated@2.0.1: 1764 + resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} 1765 + 1766 + unist-util-is@5.2.1: 1767 + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} 1768 + 1769 + unist-util-position@4.0.4: 1770 + resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} 1771 + 1772 + unist-util-remove@3.1.1: 1773 + resolution: {integrity: sha512-kfCqZK5YVY5yEa89tvpl7KnBBHu2c6CzMkqHUrlOqaRgGOMp0sMvwWOVrbAtj03KhovQB7i96Gda72v/EFE0vw==} 1774 + 1775 + unist-util-select@4.0.3: 1776 + resolution: {integrity: sha512-1074+K9VyR3NyUz3lgNtHKm7ln+jSZXtLJM4E22uVuoFn88a/Go2pX8dusrt/W+KWH1ncn8jcd8uCQuvXb/fXA==} 1777 + 1778 + unist-util-stringify-position@3.0.3: 1779 + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} 1780 + 1781 + unist-util-visit-parents@5.1.3: 1782 + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} 1783 + 1784 + unist-util-visit@4.1.2: 1785 + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} 1786 + 1787 + vfile-location@4.1.0: 1788 + resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} 1789 + 1790 + vfile-message@3.1.4: 1791 + resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} 1792 + 1793 + vfile@5.3.7: 1794 + resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} 1795 + 1426 1796 w3c-xmlserializer@5.0.0: 1427 1797 resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} 1428 1798 engines: {node: '>=18'} 1799 + 1800 + web-namespaces@2.0.1: 1801 + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} 1429 1802 1430 1803 webidl-conversions@8.0.1: 1431 1804 resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} ··· 1512 1885 1513 1886 zod@3.25.76: 1514 1887 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 1888 + 1889 + zwitch@2.0.4: 1890 + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 1515 1891 1516 1892 snapshots: 1517 1893 ··· 2231 2607 2232 2608 '@mary-ext/simple-event-emitter@1.0.1': {} 2233 2609 2610 + '@msgpack/msgpack@3.1.3': {} 2611 + 2612 + '@nandithebull/myst-oxa@0.1.8': 2613 + dependencies: 2614 + '@panproto/core': 0.43.3 2615 + js-yaml: 4.1.1 2616 + myst-frontmatter: 1.9.5 2617 + myst-parser: 1.7.2 2618 + myst-to-html: 1.7.2 2619 + rehype-stringify: 9.0.4 2620 + unified: 10.1.2 2621 + 2234 2622 '@noble/secp256k1@3.1.0': {} 2235 2623 2236 2624 '@optique/core@1.0.2': {} ··· 2238 2626 '@optique/run@1.0.2': 2239 2627 dependencies: 2240 2628 '@optique/core': 1.0.2 2629 + 2630 + '@panproto/core@0.43.3': 2631 + dependencies: 2632 + '@msgpack/msgpack': 3.1.3 2241 2633 2242 2634 '@poppinss/colors@4.1.6': 2243 2635 dependencies: ··· 2256 2648 '@speed-highlight/core@1.2.15': {} 2257 2649 2258 2650 '@standard-schema/spec@1.1.0': {} 2651 + 2652 + '@types/extend@3.0.4': {} 2653 + 2654 + '@types/hast@2.3.10': 2655 + dependencies: 2656 + '@types/unist': 2.0.11 2259 2657 2260 2658 '@types/hast@3.0.4': 2261 2659 dependencies: 2262 2660 '@types/unist': 3.0.3 2263 2661 2662 + '@types/linkify-it@3.0.5': {} 2663 + 2664 + '@types/markdown-it@13.0.9': 2665 + dependencies: 2666 + '@types/linkify-it': 3.0.5 2667 + '@types/mdurl': 1.0.5 2668 + 2669 + '@types/mdast@3.0.15': 2670 + dependencies: 2671 + '@types/unist': 2.0.11 2672 + 2673 + '@types/mdurl@1.0.5': {} 2674 + 2675 + '@types/parse5@6.0.3': {} 2676 + 2677 + '@types/unist@2.0.11': {} 2678 + 2264 2679 '@types/unist@3.0.3': {} 2265 2680 2266 2681 '@vscode/ripgrep-darwin-arm64@1.18.0': ··· 2323 2738 2324 2739 ansi-styles@6.2.3: {} 2325 2740 2741 + argparse@2.0.1: {} 2742 + 2326 2743 auto-bind@5.0.1: {} 2327 2744 2328 2745 await-lock@2.2.2: {} 2746 + 2747 + bail@2.0.2: {} 2329 2748 2330 2749 balanced-match@4.0.4: {} 2331 2750 ··· 2335 2754 2336 2755 blake3-wasm@2.1.5: {} 2337 2756 2757 + boolbase@1.0.0: {} 2758 + 2338 2759 brace-expansion@5.0.6: 2339 2760 dependencies: 2340 2761 balanced-match: 4.0.4 ··· 2345 2766 2346 2767 cac@7.0.0: {} 2347 2768 2769 + ccount@2.0.1: {} 2770 + 2348 2771 chalk@5.6.2: {} 2349 2772 2773 + character-entities-html4@2.1.0: {} 2774 + 2775 + character-entities-legacy@3.0.0: {} 2776 + 2777 + classnames@2.5.1: {} 2778 + 2350 2779 cli-boxes@4.0.1: {} 2351 2780 2352 2781 cli-cursor@4.0.0: ··· 2361 2790 code-excerpt@4.0.0: 2362 2791 dependencies: 2363 2792 convert-to-spaces: 2.0.1 2793 + 2794 + comma-separated-tokens@2.0.3: {} 2364 2795 2365 2796 convert-to-spaces@2.0.1: {} 2366 2797 ··· 2368 2799 2369 2800 core-js@3.49.0: {} 2370 2801 2802 + credit-roles@2.1.0: {} 2803 + 2804 + css-selector-parser@1.4.1: {} 2805 + 2371 2806 css-tree@3.2.1: 2372 2807 dependencies: 2373 2808 mdn-data: 2.27.1 2374 2809 source-map-js: 1.2.1 2810 + 2811 + csv-parse@5.6.0: {} 2375 2812 2376 2813 data-urls@7.0.0: 2377 2814 dependencies: ··· 2398 2835 devlop@1.1.0: 2399 2836 dependencies: 2400 2837 dequal: 2.0.3 2838 + 2839 + doi-utils@2.0.6: {} 2840 + 2841 + entities@3.0.1: {} 2401 2842 2402 2843 entities@8.0.0: {} 2403 2844 ··· 2467 2908 2468 2909 escape-string-regexp@2.0.0: {} 2469 2910 2911 + escape-string-regexp@5.0.0: {} 2912 + 2470 2913 esm-env@1.2.2: {} 2471 2914 2472 2915 event-target-polyfill@0.0.4: {} 2916 + 2917 + extend@3.0.2: {} 2473 2918 2474 2919 fsevents@2.3.3: 2475 2920 optional: true ··· 2484 2929 2485 2930 has-flag@5.0.1: {} 2486 2931 2932 + hast-util-embedded@2.0.1: 2933 + dependencies: 2934 + hast-util-is-element: 2.1.3 2935 + 2936 + hast-util-from-parse5@7.1.2: 2937 + dependencies: 2938 + '@types/hast': 2.3.10 2939 + '@types/unist': 2.0.11 2940 + hastscript: 7.2.0 2941 + property-information: 6.5.0 2942 + vfile: 5.3.7 2943 + vfile-location: 4.1.0 2944 + web-namespaces: 2.0.1 2945 + 2946 + hast-util-has-property@2.0.1: {} 2947 + 2948 + hast-util-is-body-ok-link@2.0.0: 2949 + dependencies: 2950 + '@types/hast': 2.3.10 2951 + hast-util-has-property: 2.0.1 2952 + hast-util-is-element: 2.1.3 2953 + 2954 + hast-util-is-element@2.1.3: 2955 + dependencies: 2956 + '@types/hast': 2.3.10 2957 + '@types/unist': 2.0.11 2958 + 2959 + hast-util-parse-selector@3.1.1: 2960 + dependencies: 2961 + '@types/hast': 2.3.10 2962 + 2963 + hast-util-phrasing@2.0.2: 2964 + dependencies: 2965 + '@types/hast': 2.3.10 2966 + hast-util-embedded: 2.0.1 2967 + hast-util-has-property: 2.0.1 2968 + hast-util-is-body-ok-link: 2.0.0 2969 + hast-util-is-element: 2.1.3 2970 + 2971 + hast-util-raw@7.2.3: 2972 + dependencies: 2973 + '@types/hast': 2.3.10 2974 + '@types/parse5': 6.0.3 2975 + hast-util-from-parse5: 7.1.2 2976 + hast-util-to-parse5: 7.1.0 2977 + html-void-elements: 2.0.1 2978 + parse5: 6.0.1 2979 + unist-util-position: 4.0.4 2980 + unist-util-visit: 4.1.2 2981 + vfile: 5.3.7 2982 + web-namespaces: 2.0.1 2983 + zwitch: 2.0.4 2984 + 2985 + hast-util-to-html@8.0.4: 2986 + dependencies: 2987 + '@types/hast': 2.3.10 2988 + '@types/unist': 2.0.11 2989 + ccount: 2.0.1 2990 + comma-separated-tokens: 2.0.3 2991 + hast-util-raw: 7.2.3 2992 + hast-util-whitespace: 2.0.1 2993 + html-void-elements: 2.0.1 2994 + property-information: 6.5.0 2995 + space-separated-tokens: 2.0.2 2996 + stringify-entities: 4.0.4 2997 + zwitch: 2.0.4 2998 + 2999 + hast-util-to-mdast@8.4.1: 3000 + dependencies: 3001 + '@types/extend': 3.0.4 3002 + '@types/hast': 2.3.10 3003 + '@types/mdast': 3.0.15 3004 + '@types/unist': 2.0.11 3005 + extend: 3.0.2 3006 + hast-util-has-property: 2.0.1 3007 + hast-util-is-element: 2.1.3 3008 + hast-util-phrasing: 2.0.2 3009 + hast-util-to-text: 3.1.2 3010 + mdast-util-phrasing: 3.0.1 3011 + mdast-util-to-string: 3.2.0 3012 + rehype-minify-whitespace: 5.0.1 3013 + trim-trailing-lines: 2.1.0 3014 + unist-util-is: 5.2.1 3015 + unist-util-visit: 4.1.2 3016 + 3017 + hast-util-to-parse5@7.1.0: 3018 + dependencies: 3019 + '@types/hast': 2.3.10 3020 + comma-separated-tokens: 2.0.3 3021 + property-information: 6.5.0 3022 + space-separated-tokens: 2.0.2 3023 + web-namespaces: 2.0.1 3024 + zwitch: 2.0.4 3025 + 3026 + hast-util-to-text@3.1.2: 3027 + dependencies: 3028 + '@types/hast': 2.3.10 3029 + '@types/unist': 2.0.11 3030 + hast-util-is-element: 2.1.3 3031 + unist-util-find-after: 4.0.1 3032 + 3033 + hast-util-whitespace@2.0.1: {} 3034 + 3035 + hast@1.0.0: {} 3036 + 3037 + hastscript@7.2.0: 3038 + dependencies: 3039 + '@types/hast': 2.3.10 3040 + comma-separated-tokens: 2.0.3 3041 + hast-util-parse-selector: 3.1.1 3042 + property-information: 6.5.0 3043 + space-separated-tokens: 2.0.2 3044 + 3045 + he@1.2.0: {} 3046 + 2487 3047 highlight.js@11.11.1: {} 2488 3048 2489 3049 hono@4.12.18: {} ··· 2493 3053 '@exodus/bytes': 1.15.0 2494 3054 transitivePeerDependencies: 2495 3055 - '@noble/hashes' 3056 + 3057 + html-void-elements@2.0.1: {} 3058 + 3059 + html-whitespace-sensitive-tag-names@2.0.0: {} 2496 3060 2497 3061 indent-string@5.0.0: {} 2498 3062 ··· 2533 3097 - bufferutil 2534 3098 - utf-8-validate 2535 3099 3100 + is-buffer@2.0.5: {} 3101 + 2536 3102 is-docker@3.0.0: {} 2537 3103 2538 3104 is-fullwidth-code-point@5.1.0: ··· 2545 3111 dependencies: 2546 3112 is-docker: 3.0.0 2547 3113 3114 + is-plain-obj@4.1.0: {} 3115 + 2548 3116 is-potential-custom-element-name@1.0.1: {} 2549 3117 2550 3118 is-wsl@3.1.1: ··· 2556 3124 jiti@2.7.0: {} 2557 3125 2558 3126 jose@5.10.0: {} 3127 + 3128 + js-yaml@4.1.1: 3129 + dependencies: 3130 + argparse: 2.0.1 2559 3131 2560 3132 jsdom@29.1.1: 2561 3133 dependencies: ··· 2583 3155 transitivePeerDependencies: 2584 3156 - '@noble/hashes' 2585 3157 3158 + json5@1.0.2: 3159 + dependencies: 3160 + minimist: 1.2.8 3161 + 2586 3162 kleur@4.1.5: {} 2587 3163 3164 + linkify-it@4.0.1: 3165 + dependencies: 3166 + uc.micro: 1.0.6 3167 + 2588 3168 lowlight@3.3.0: 2589 3169 dependencies: 2590 3170 '@types/hast': 3.0.4 ··· 2595 3175 2596 3176 lru-cache@11.3.6: {} 2597 3177 3178 + markdown-it-amsmath@0.4.0(markdown-it@13.0.2): 3179 + dependencies: 3180 + markdown-it: 13.0.2 3181 + 3182 + markdown-it-deflist@2.1.0: {} 3183 + 3184 + markdown-it-dollarmath@0.5.0(markdown-it@13.0.2): 3185 + dependencies: 3186 + markdown-it: 13.0.2 3187 + 3188 + markdown-it-footnote@4.0.0: {} 3189 + 3190 + markdown-it-front-matter@0.2.4: {} 3191 + 3192 + markdown-it-myst-extras@0.3.0(markdown-it@13.0.2): 3193 + dependencies: 3194 + markdown-it: 13.0.2 3195 + 3196 + markdown-it-myst@1.0.16(@types/markdown-it@13.0.9)(markdown-it@13.0.2): 3197 + dependencies: 3198 + '@types/markdown-it': 13.0.9 3199 + js-yaml: 4.1.1 3200 + markdown-it: 13.0.2 3201 + vfile: 5.3.7 3202 + 3203 + markdown-it-task-lists@2.1.1: {} 3204 + 3205 + markdown-it@13.0.2: 3206 + dependencies: 3207 + argparse: 2.0.1 3208 + entities: 3.0.1 3209 + linkify-it: 4.0.1 3210 + mdurl: 1.0.1 3211 + uc.micro: 1.0.6 3212 + 3213 + mdast-util-definitions@5.1.2: 3214 + dependencies: 3215 + '@types/mdast': 3.0.15 3216 + '@types/unist': 2.0.11 3217 + unist-util-visit: 4.1.2 3218 + 3219 + mdast-util-find-and-replace@2.2.2: 3220 + dependencies: 3221 + '@types/mdast': 3.0.15 3222 + escape-string-regexp: 5.0.0 3223 + unist-util-is: 5.2.1 3224 + unist-util-visit-parents: 5.1.3 3225 + 3226 + mdast-util-phrasing@3.0.1: 3227 + dependencies: 3228 + '@types/mdast': 3.0.15 3229 + unist-util-is: 5.2.1 3230 + 3231 + mdast-util-to-hast@12.3.0: 3232 + dependencies: 3233 + '@types/hast': 2.3.10 3234 + '@types/mdast': 3.0.15 3235 + mdast-util-definitions: 5.1.2 3236 + micromark-util-sanitize-uri: 1.2.0 3237 + trim-lines: 3.0.1 3238 + unist-util-generated: 2.0.1 3239 + unist-util-position: 4.0.4 3240 + unist-util-visit: 4.1.2 3241 + 3242 + mdast-util-to-string@3.2.0: 3243 + dependencies: 3244 + '@types/mdast': 3.0.15 3245 + 3246 + mdast@3.0.0: {} 3247 + 2598 3248 mdn-data@2.27.1: {} 2599 3249 3250 + mdurl@1.0.1: {} 3251 + 3252 + micromark-util-character@1.2.0: 3253 + dependencies: 3254 + micromark-util-symbol: 1.1.0 3255 + micromark-util-types: 1.1.0 3256 + 3257 + micromark-util-encode@1.1.0: {} 3258 + 3259 + micromark-util-sanitize-uri@1.2.0: 3260 + dependencies: 3261 + micromark-util-character: 1.2.0 3262 + micromark-util-encode: 1.1.0 3263 + micromark-util-symbol: 1.1.0 3264 + 3265 + micromark-util-symbol@1.1.0: {} 3266 + 3267 + micromark-util-types@1.1.0: {} 3268 + 2600 3269 mimic-fn@2.1.0: {} 2601 3270 2602 3271 miniflare@4.20260508.0: ··· 2615 3284 dependencies: 2616 3285 brace-expansion: 5.0.6 2617 3286 3287 + minimist@1.2.8: {} 3288 + 2618 3289 minipass@7.1.3: {} 2619 3290 2620 3291 multiformats@9.9.0: {} 2621 3292 3293 + myst-common@1.9.5: 3294 + dependencies: 3295 + mdast: 3.0.0 3296 + myst-frontmatter: 1.9.5 3297 + myst-spec: 0.0.5 3298 + nanoid: 5.1.11 3299 + unified: 10.1.2 3300 + unist-util-remove: 3.1.1 3301 + unist-util-select: 4.0.3 3302 + unist-util-visit: 4.1.2 3303 + vfile: 5.3.7 3304 + vfile-message: 3.1.4 3305 + 3306 + myst-directives@1.7.2: 3307 + dependencies: 3308 + classnames: 2.5.1 3309 + csv-parse: 5.6.0 3310 + js-yaml: 4.1.1 3311 + json5: 1.0.2 3312 + myst-common: 1.9.5 3313 + myst-spec-ext: 1.9.5 3314 + nanoid: 5.1.11 3315 + unist-util-select: 4.0.3 3316 + vfile: 5.3.7 3317 + 3318 + myst-frontmatter@1.9.5: 3319 + dependencies: 3320 + credit-roles: 2.1.0 3321 + doi-utils: 2.0.6 3322 + myst-toc: 0.1.4 3323 + orcid: 1.0.0 3324 + simple-validators: 1.2.0 3325 + spdx-correct: 3.2.0 3326 + 3327 + myst-parser@1.7.2: 3328 + dependencies: 3329 + '@types/markdown-it': 13.0.9 3330 + he: 1.2.0 3331 + markdown-it: 13.0.2 3332 + markdown-it-amsmath: 0.4.0(markdown-it@13.0.2) 3333 + markdown-it-deflist: 2.1.0 3334 + markdown-it-dollarmath: 0.5.0(markdown-it@13.0.2) 3335 + markdown-it-footnote: 4.0.0 3336 + markdown-it-front-matter: 0.2.4 3337 + markdown-it-myst: 1.0.16(@types/markdown-it@13.0.9)(markdown-it@13.0.2) 3338 + markdown-it-myst-extras: 0.3.0(markdown-it@13.0.2) 3339 + markdown-it-task-lists: 2.1.1 3340 + myst-common: 1.9.5 3341 + myst-directives: 1.7.2 3342 + myst-roles: 1.7.2 3343 + myst-spec: 0.0.5 3344 + unified: 10.1.2 3345 + unist-builder: 3.0.1 3346 + unist-util-remove: 3.1.1 3347 + unist-util-select: 4.0.3 3348 + unist-util-visit: 4.1.2 3349 + vfile: 5.3.7 3350 + 3351 + myst-roles@1.7.2: 3352 + dependencies: 3353 + myst-common: 1.9.5 3354 + myst-spec-ext: 1.9.5 3355 + 3356 + myst-spec-ext@1.9.5: 3357 + dependencies: 3358 + myst-spec: 0.0.5 3359 + 3360 + myst-spec@0.0.5: {} 3361 + 3362 + myst-to-html@1.7.2: 3363 + dependencies: 3364 + '@types/markdown-it': 13.0.9 3365 + classnames: 2.5.1 3366 + hast: 1.0.0 3367 + hast-util-to-mdast: 8.4.1 3368 + markdown-it: 13.0.2 3369 + mdast: 3.0.0 3370 + mdast-util-find-and-replace: 2.2.2 3371 + mdast-util-to-hast: 12.3.0 3372 + myst-common: 1.9.5 3373 + rehype-format: 4.0.1 3374 + rehype-parse: 8.0.5 3375 + rehype-remark: 9.1.2 3376 + rehype-stringify: 9.0.4 3377 + unified: 10.1.2 3378 + unist-builder: 3.0.1 3379 + unist-util-find-after: 4.0.1 3380 + unist-util-remove: 3.1.1 3381 + unist-util-select: 4.0.3 3382 + unist-util-visit: 4.1.2 3383 + 3384 + myst-toc@0.1.4: 3385 + dependencies: 3386 + simple-validators: 1.2.0 3387 + 2622 3388 nanoid@5.1.11: {} 2623 3389 2624 3390 node-addon-api@7.1.1: {} ··· 2627 3393 dependencies: 2628 3394 node-addon-api: 7.1.1 2629 3395 3396 + nth-check@2.1.1: 3397 + dependencies: 3398 + boolbase: 1.0.0 3399 + 2630 3400 onetime@5.1.2: 2631 3401 dependencies: 2632 3402 mimic-fn: 2.1.0 ··· 2637 3407 define-lazy-prop: 3.0.0 2638 3408 is-inside-container: 1.0.0 2639 3409 wsl-utils: 0.1.0 3410 + 3411 + orcid@1.0.0: {} 3412 + 3413 + parse5@6.0.1: {} 2640 3414 2641 3415 parse5@8.0.1: 2642 3416 dependencies: ··· 2663 3437 2664 3438 prettier@3.8.3: {} 2665 3439 3440 + property-information@6.5.0: {} 3441 + 2666 3442 punycode@2.3.1: {} 2667 3443 2668 3444 react-reconciler@0.33.0(react@19.2.6): ··· 2672 3448 2673 3449 react@19.2.6: {} 2674 3450 3451 + rehype-format@4.0.1: 3452 + dependencies: 3453 + '@types/hast': 2.3.10 3454 + hast-util-embedded: 2.0.1 3455 + hast-util-is-element: 2.1.3 3456 + hast-util-phrasing: 2.0.2 3457 + hast-util-whitespace: 2.0.1 3458 + html-whitespace-sensitive-tag-names: 2.0.0 3459 + rehype-minify-whitespace: 5.0.1 3460 + unified: 10.1.2 3461 + unist-util-visit-parents: 5.1.3 3462 + 3463 + rehype-minify-whitespace@5.0.1: 3464 + dependencies: 3465 + '@types/hast': 2.3.10 3466 + hast-util-embedded: 2.0.1 3467 + hast-util-is-element: 2.1.3 3468 + hast-util-whitespace: 2.0.1 3469 + unified: 10.1.2 3470 + unist-util-is: 5.2.1 3471 + 3472 + rehype-parse@8.0.5: 3473 + dependencies: 3474 + '@types/hast': 2.3.10 3475 + hast-util-from-parse5: 7.1.2 3476 + parse5: 6.0.1 3477 + unified: 10.1.2 3478 + 3479 + rehype-remark@9.1.2: 3480 + dependencies: 3481 + '@types/hast': 2.3.10 3482 + '@types/mdast': 3.0.15 3483 + hast-util-to-mdast: 8.4.1 3484 + unified: 10.1.2 3485 + 3486 + rehype-stringify@9.0.4: 3487 + dependencies: 3488 + '@types/hast': 2.3.10 3489 + hast-util-to-html: 8.0.4 3490 + unified: 10.1.2 3491 + 2675 3492 require-from-string@2.0.2: {} 2676 3493 2677 3494 restore-cursor@4.0.0: ··· 2722 3539 2723 3540 signal-exit@3.0.7: {} 2724 3541 3542 + simple-validators@1.2.0: {} 3543 + 2725 3544 slice-ansi@9.0.0: 2726 3545 dependencies: 2727 3546 ansi-styles: 6.2.3 ··· 2729 3548 2730 3549 source-map-js@1.2.1: {} 2731 3550 3551 + space-separated-tokens@2.0.2: {} 3552 + 3553 + spdx-correct@3.2.0: 3554 + dependencies: 3555 + spdx-expression-parse: 3.0.1 3556 + spdx-license-ids: 3.0.23 3557 + 3558 + spdx-exceptions@2.5.0: {} 3559 + 3560 + spdx-expression-parse@3.0.1: 3561 + dependencies: 3562 + spdx-exceptions: 2.5.0 3563 + spdx-license-ids: 3.0.23 3564 + 3565 + spdx-license-ids@3.0.23: {} 3566 + 2732 3567 stack-utils@2.0.6: 2733 3568 dependencies: 2734 3569 escape-string-regexp: 2.0.0 ··· 2737 3572 dependencies: 2738 3573 get-east-asian-width: 1.6.0 2739 3574 strip-ansi: 7.2.0 3575 + 3576 + stringify-entities@4.0.4: 3577 + dependencies: 3578 + character-entities-html4: 2.1.0 3579 + character-entities-legacy: 3.0.0 2740 3580 2741 3581 strip-ansi@7.2.0: 2742 3582 dependencies: ··· 2776 3616 dependencies: 2777 3617 punycode: 2.3.1 2778 3618 3619 + trim-lines@3.0.1: {} 3620 + 3621 + trim-trailing-lines@2.1.0: {} 3622 + 3623 + trough@2.2.0: {} 3624 + 2779 3625 tslib@2.8.1: {} 2780 3626 2781 3627 type-fest@4.41.0: {} ··· 2785 3631 tagged-tag: 1.0.0 2786 3632 2787 3633 typescript@5.9.3: {} 3634 + 3635 + uc.micro@1.0.6: {} 2788 3636 2789 3637 uint8arrays@3.0.0: 2790 3638 dependencies: ··· 2800 3648 2801 3649 unicode-segmenter@0.14.5: {} 2802 3650 3651 + unified@10.1.2: 3652 + dependencies: 3653 + '@types/unist': 2.0.11 3654 + bail: 2.0.2 3655 + extend: 3.0.2 3656 + is-buffer: 2.0.5 3657 + is-plain-obj: 4.1.0 3658 + trough: 2.2.0 3659 + vfile: 5.3.7 3660 + 3661 + unist-builder@3.0.1: 3662 + dependencies: 3663 + '@types/unist': 2.0.11 3664 + 3665 + unist-util-find-after@4.0.1: 3666 + dependencies: 3667 + '@types/unist': 2.0.11 3668 + unist-util-is: 5.2.1 3669 + 3670 + unist-util-generated@2.0.1: {} 3671 + 3672 + unist-util-is@5.2.1: 3673 + dependencies: 3674 + '@types/unist': 2.0.11 3675 + 3676 + unist-util-position@4.0.4: 3677 + dependencies: 3678 + '@types/unist': 2.0.11 3679 + 3680 + unist-util-remove@3.1.1: 3681 + dependencies: 3682 + '@types/unist': 2.0.11 3683 + unist-util-is: 5.2.1 3684 + unist-util-visit-parents: 5.1.3 3685 + 3686 + unist-util-select@4.0.3: 3687 + dependencies: 3688 + '@types/unist': 2.0.11 3689 + css-selector-parser: 1.4.1 3690 + nth-check: 2.1.1 3691 + zwitch: 2.0.4 3692 + 3693 + unist-util-stringify-position@3.0.3: 3694 + dependencies: 3695 + '@types/unist': 2.0.11 3696 + 3697 + unist-util-visit-parents@5.1.3: 3698 + dependencies: 3699 + '@types/unist': 2.0.11 3700 + unist-util-is: 5.2.1 3701 + 3702 + unist-util-visit@4.1.2: 3703 + dependencies: 3704 + '@types/unist': 2.0.11 3705 + unist-util-is: 5.2.1 3706 + unist-util-visit-parents: 5.1.3 3707 + 3708 + vfile-location@4.1.0: 3709 + dependencies: 3710 + '@types/unist': 2.0.11 3711 + vfile: 5.3.7 3712 + 3713 + vfile-message@3.1.4: 3714 + dependencies: 3715 + '@types/unist': 2.0.11 3716 + unist-util-stringify-position: 3.0.3 3717 + 3718 + vfile@5.3.7: 3719 + dependencies: 3720 + '@types/unist': 2.0.11 3721 + is-buffer: 2.0.5 3722 + unist-util-stringify-position: 3.0.3 3723 + vfile-message: 3.1.4 3724 + 2803 3725 w3c-xmlserializer@5.0.0: 2804 3726 dependencies: 2805 3727 xml-name-validator: 5.0.0 3728 + 3729 + web-namespaces@2.0.1: {} 2806 3730 2807 3731 webidl-conversions@8.0.1: {} 2808 3732 ··· 2881 3805 youch-core: 0.3.3 2882 3806 2883 3807 zod@3.25.76: {} 3808 + 3809 + zwitch@2.0.4: {}
-40
src/container.ts
··· 1 - import { Container } from "@cloudflare/containers"; 2 - import type { DurableObject } from "cloudflare:workers"; 3 - 4 - interface ContainerEnv { 5 - CARRY_GITHUB_TOKEN: string; 6 - OBSIDIAN_AUTH_TOKEN: string; 7 - OBSIDIAN_ENCRYPTION_KEY: string; 8 - OBSIDIAN_ENCRYPTION_SALT: string; 9 - } 10 - 11 - export class StrataContainer extends Container<ContainerEnv> { 12 - defaultPort = 8080; 13 - sleepAfter = "5m"; 14 - 15 - constructor(ctx: DurableObject["ctx"], env: ContainerEnv) { 16 - super(ctx, env); 17 - // Set env vars before container starts — inject secrets from worker bindings 18 - this.envVars = { 19 - VAULT_PATH: "/data/vault", 20 - CARRY_PATH: "/data/carry", 21 - CARRY_BRANCH: "main", 22 - ...(env.CARRY_GITHUB_TOKEN ? { CARRY_GITHUB_TOKEN: env.CARRY_GITHUB_TOKEN } : {}), 23 - ...(env.OBSIDIAN_AUTH_TOKEN ? { OBSIDIAN_AUTH_TOKEN: env.OBSIDIAN_AUTH_TOKEN } : {}), 24 - OBSIDIAN_VAULT_ID: "6472c50492efb918c2878e61048218f2", 25 - OBSIDIAN_VAULT_NAME: "notes", 26 - OBSIDIAN_SYNC_HOST: "sync-53.obsidian.md", 27 - ...(env.OBSIDIAN_ENCRYPTION_KEY ? { OBSIDIAN_ENCRYPTION_KEY: env.OBSIDIAN_ENCRYPTION_KEY } : {}), 28 - ...(env.OBSIDIAN_ENCRYPTION_SALT ? { OBSIDIAN_ENCRYPTION_SALT: env.OBSIDIAN_ENCRYPTION_SALT } : {}), 29 - }; 30 - } 31 - 32 - async onStart() { 33 - const has = (k: string) => !!this.envVars?.[k]; 34 - console.log("[strata-container] started, carry:", has("CARRY_GITHUB_TOKEN") ? "set" : "missing", "obsidian:", has("OBSIDIAN_AUTH_TOKEN") ? "set" : "missing"); 35 - } 36 - 37 - async onStop() { 38 - console.log("[strata-container] stopped"); 39 - } 40 - }
+423 -49
src/frontend/app.ts
··· 1 + import { BrowserOAuthClient, type OAuthSession } from "@atproto/oauth-client-browser"; 2 + import type { IdentityInfo, IdentityResolver } from "@atproto-labs/identity-resolver"; 3 + import { render as renderMyst } from "@nandithebull/myst-oxa"; 4 + 1 5 /** 2 - * Stigmergic frontend — island-centric SPA. 6 + * Stigmergic frontend — island-centric SPA. v2 3 7 * 4 8 * The knowledge graph is a set of islands (connected components). 5 9 * Each island is a cluster of linked URLs discovered via ··· 36 40 title?: string | null; 37 41 strata: StrataData | null; 38 42 recordUri?: string | null; 43 + rawRecord?: any; 39 44 handle?: string | null; 40 45 } 41 46 ··· 46 51 tensions: string[]; 47 52 open_questions: string[]; 48 53 synthesis: string; 54 + synthesisDoc?: OxaRecord | null; 55 + } 56 + 57 + // ── OXA document types (pub.oxa.document) ────────────────────────────────── 58 + 59 + interface OxaFacet { 60 + index: { byteStart: number; byteEnd: number }; 61 + features: Array<{ $type: string; [key: string]: unknown }>; 62 + } 63 + 64 + interface OxaRecord { 65 + $type: "pub.oxa.document"; 66 + title: { text: string; facets: OxaFacet[] }; 67 + children: OxaBlock[]; 68 + createdAt: string; 69 + metadata?: Record<string, unknown>; 70 + } 71 + 72 + type OxaBlock = OxaParagraph | OxaHeading | OxaCode | OxaThematicBreak | OxaBlockquote | OxaImage | OxaMath | OxaList | OxaTable | OxaDirective; 73 + 74 + interface OxaParagraph { $type: "pub.oxa.blocks.defs#paragraph"; text: string; facets: OxaFacet[] } 75 + interface OxaHeading { $type: "pub.oxa.blocks.defs#heading"; level: number; text: string; facets: OxaFacet[] } 76 + interface OxaCode { $type: "pub.oxa.blocks.defs#code"; value: string; language?: string } 77 + interface OxaThematicBreak { $type: "pub.oxa.blocks.defs#thematicBreak" } 78 + interface OxaBlockquote { $type: "pub.oxa.blocks.defs#blockquote"; children: OxaBlock[] } 79 + interface OxaImage { $type: "pub.oxa.blocks.defs#image"; src: string; alt?: string } 80 + interface OxaMath { $type: "pub.oxa.blocks.defs#math"; value: string } 81 + interface OxaList { $type: "pub.oxa.blocks.defs#list"; ordered: boolean; startIndex?: number; children: OxaListItem[] } 82 + interface OxaListItem { $type: "pub.oxa.blocks.defs#listItem"; children: OxaBlock[] } 83 + interface OxaTable { $type: "pub.oxa.blocks.defs#table"; headerRows?: number; children: OxaTableRow[] } 84 + interface OxaTableRow { $type: "pub.oxa.blocks.defs#tableRow"; children: OxaTableCell[] } 85 + interface OxaTableCell { $type: "pub.oxa.blocks.defs#tableCell"; text: string; facets: OxaFacet[] } 86 + interface OxaDirective { $type: "pub.oxa.blocks.defs#directive"; name: string; argument?: string; options?: Record<string, string>; body?: string; children?: OxaBlock[] } 87 + 88 + // ── OXA → HTML renderer ───────────────────────────────────────────────────── 89 + 90 + const encoder = new TextEncoder(); 91 + 92 + function byteSlice(text: string, start: number, end: number): string { 93 + const bytes = encoder.encode(text); 94 + return new TextDecoder().decode(bytes.slice(start, end)); 95 + } 96 + 97 + function oxaInlineToHtml(text: string, facets: OxaFacet[]): string { 98 + if (!facets || facets.length === 0) return escHtml(text); 99 + 100 + // Build annotated segments from facets 101 + type Boundary = { pos: number; starts: string[]; ends: string[] }; 102 + const boundaries: Boundary[] = []; 103 + const linkTargets = new Map<number, string>(); 104 + 105 + for (const facet of facets) { 106 + const { byteStart, byteEnd } = facet.index; 107 + let bStart = boundaries.find(b => b.pos === byteStart); 108 + if (!bStart) { bStart = { pos: byteStart, starts: [], ends: [] }; boundaries.push(bStart); } 109 + let bEnd = boundaries.find(b => b.pos === byteEnd); 110 + if (!bEnd) { bEnd = { pos: byteEnd, starts: [], ends: [] }; boundaries.push(bEnd); } 111 + 112 + for (const f of facet.features) { 113 + const t = f.$type; 114 + if (t === "pub.oxa.richtext.facet#strong") bStart.starts.push("strong"); 115 + else if (t === "pub.oxa.richtext.facet#emphasis") bStart.starts.push("em"); 116 + else if (t === "pub.oxa.richtext.facet#link") { 117 + bStart.starts.push("a"); 118 + if (typeof f.uri === "string") linkTargets.set(byteStart, f.uri); 119 + } else if (t === "pub.oxa.richtext.facet#inlineCode") bStart.starts.push("code"); 120 + else if (t === "pub.leaflet.richtext.facet#bold") bStart.starts.push("strong"); 121 + else if (t === "pub.leaflet.richtext.facet#italic") bStart.starts.push("em"); 122 + else if (t === "pub.leaflet.richtext.facet#code") bStart.starts.push("code"); 123 + } 124 + bEnd.ends.push(...bStart.starts.slice().reverse()); // close in reverse order 125 + } 126 + 127 + boundaries.sort((a, b) => a.pos - b.pos); 128 + 129 + const active: string[] = []; 130 + let prev = 0; 131 + let html = ""; 132 + 133 + for (const boundary of boundaries) { 134 + if (boundary.pos > prev) { 135 + html += escHtml(byteSlice(text, prev, boundary.pos)); 136 + } 137 + // Close tags 138 + for (const tag of boundary.ends) { 139 + const idx = active.lastIndexOf(tag); 140 + if (idx !== -1) { 141 + active.splice(idx, 1); 142 + if (tag === "a") html += "</a>"; 143 + else if (tag === "code") html += "</code>"; 144 + else html += `</${tag}>`; 145 + } 146 + } 147 + // Open tags 148 + for (const tag of boundary.starts) { 149 + active.push(tag); 150 + if (tag === "a") { 151 + const href = linkTargets.get(boundary.pos) || "#"; 152 + html += `<a href="${escHtml(href)}" target="_blank" rel="noopener">`; 153 + } else if (tag === "code") { 154 + html += `<code>`; 155 + } else { 156 + html += `<${tag}>`; 157 + } 158 + } 159 + prev = boundary.pos; 160 + } 161 + 162 + if (prev < encoder.encode(text).byteLength) { 163 + html += escHtml(byteSlice(text, prev, encoder.encode(text).byteLength)); 164 + } 165 + 166 + // Close any remaining open tags 167 + for (const tag of active.slice().reverse()) { 168 + if (tag === "a") html += "</a>"; 169 + else if (tag === "code") html += "</code>"; 170 + else html += `</${tag}>`; 171 + } 172 + 173 + return html; 174 + } 175 + 176 + function oxaBlockToHtml(block: OxaBlock): string { 177 + switch (block.$type) { 178 + case "pub.oxa.blocks.defs#paragraph": 179 + return `<p>${oxaInlineToHtml(block.text, block.facets)}</p>`; 180 + case "pub.oxa.blocks.defs#heading": 181 + return `<h${block.level}>${oxaInlineToHtml(block.text, block.facets)}</h${block.level}>`; 182 + case "pub.oxa.blocks.defs#code": 183 + return `<pre><code${block.language ? ` class="language-${escHtml(block.language)}"` : ""}>${escHtml(block.value)}</code></pre>`; 184 + case "pub.oxa.blocks.defs#thematicBreak": 185 + return `<hr>`; 186 + case "pub.oxa.blocks.defs#blockquote": 187 + return `<blockquote>${block.children.map(oxaBlockToHtml).join("")}</blockquote>`; 188 + case "pub.oxa.blocks.defs#image": 189 + return `<img src="${escHtml(block.src)}" alt="${escHtml(block.alt || "")}" />`; 190 + case "pub.oxa.blocks.defs#math": 191 + return `<div class="math-block">${escHtml(block.value)}</div>`; 192 + case "pub.oxa.blocks.defs#list": { 193 + const tag = block.ordered ? "ol" : "ul"; 194 + const start = block.startIndex && block.startIndex > 1 ? ` start="${block.startIndex}"` : ""; 195 + return `<${tag}${start}>${block.children.map(item => `<li>${item.children.map(oxaBlockToHtml).join("")}</li>`).join("")}</${tag}>`; 196 + } 197 + case "pub.oxa.blocks.defs#table": { 198 + const rows = block.children || []; 199 + const headerCount = Math.max(1, block.headerRows || 1); 200 + let html = "<table>"; 201 + for (let i = 0; i < rows.length; i++) { 202 + const tag = i < headerCount ? "thead" : "tbody"; 203 + if (i === 0 || i === headerCount) html += i === 0 ? "<thead><tr>" : "</tr></thead><tbody><tr>"; 204 + else html += "<tr>"; 205 + for (const cell of rows[i].children) { 206 + const cellTag = i < headerCount ? "th" : "td"; 207 + html += `<${cellTag}>${oxaInlineToHtml(cell.text, cell.facets)}</${cellTag}>`; 208 + } 209 + html += "</tr>"; 210 + } 211 + if (rows.length > 0) html += rows.length <= headerCount ? "</tr></thead>" : "</tr></tbody>"; 212 + return html + "</table>"; 213 + } 214 + case "pub.oxa.blocks.defs#directive": 215 + if (block.body) return `<div class="directive directive-${escHtml(block.name)}"><pre>${escHtml(block.body)}</pre></div>`; 216 + if (block.children) return `<div class="directive directive-${escHtml(block.name)}">${block.children.map(oxaBlockToHtml).join("")}</div>`; 217 + return `<div class="directive directive-${escHtml(block.name)}"></div>`; 218 + default: 219 + return ""; 220 + } 221 + } 222 + 223 + function oxaDocToHtml(doc: OxaRecord): string { 224 + return (doc.children || []).map(oxaBlockToHtml).join("\n"); 49 225 } 50 226 51 227 // ── State ────────────────────────────────────────────────────────────────── 52 228 53 229 let session: { did: string; handle: string } | null = null; 230 + let oauthClient: BrowserOAuthClient | null = null; 231 + let oauthSession: OAuthSession | null = null; 232 + 233 + const browserIdentityResolver: IdentityResolver = { 234 + async resolve(identifier: string): Promise<IdentityInfo> { 235 + const input = identifier.trim(); 236 + const did = input.startsWith("did:") 237 + ? input 238 + : (await (await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(input)}`)).json() as any).did; 239 + const didDoc = await (await fetch(did.startsWith("did:plc:") 240 + ? `https://plc.directory/${encodeURIComponent(did)}` 241 + : `https://${did.slice("did:web:".length).replace(/:/g, "/")}/.well-known/did.json`)).json() as any; 242 + const handle = didDoc.alsoKnownAs?.find((aka: string) => aka.startsWith("at://"))?.slice(5) || input; 243 + return { did, didDoc, handle } as IdentityInfo; 244 + }, 245 + }; 54 246 let currentPage: "explore" | "island" | "strata" | "connect" = "explore"; 55 247 let islands: Island[] = []; 56 248 ··· 182 374 183 375 // Warm up the simulation 184 376 for (let i = 0; i < 200; i++) this.tick(); 377 + 378 + // Fit graph to canvas — compute bounding box and scale/translate 379 + this.fitToView(); 380 + 185 381 this.draw(); 186 382 } 187 383 ··· 192 388 this.canvas.removeEventListener("mouseup", this.onMouseUp); 193 389 this.canvas.removeEventListener("mouseleave", this.onMouseLeave); 194 390 this.canvas.removeEventListener("dblclick", this.onDblClick); 391 + } 392 + 393 + /** After simulation converges, scale and translate nodes to fill the canvas. */ 394 + fitToView() { 395 + if (this.nodes.length === 0) return; 396 + 397 + const pad = 40; // padding from edges 398 + const usableW = this.width - pad * 2; 399 + const usableH = this.height - pad * 2; 400 + 401 + // Bounding box of all nodes 402 + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; 403 + for (const n of this.nodes) { 404 + if (n.x < minX) minX = n.x; 405 + if (n.y < minY) minY = n.y; 406 + if (n.x > maxX) maxX = n.x; 407 + if (n.y > maxY) maxY = n.y; 408 + } 409 + 410 + const graphW = maxX - minX || 1; 411 + const graphH = maxY - minY || 1; 412 + const graphCX = (minX + maxX) / 2; 413 + const graphCY = (minY + maxY) / 2; 414 + 415 + // Scale to fit, maintaining aspect ratio 416 + const scale = Math.min(usableW / graphW, usableH / graphH, 2.0); // cap at 2x zoom 417 + const canvasCX = this.width / 2; 418 + const canvasCY = this.height / 2; 419 + 420 + for (const n of this.nodes) { 421 + // Translate so graph center = canvas center, then scale 422 + n.x = canvasCX + (n.x - graphCX) * scale; 423 + n.y = canvasCY + (n.y - graphCY) * scale; 424 + } 195 425 } 196 426 197 427 tick() { ··· 410 640 return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); 411 641 } 412 642 643 + function mystToHtml(title: string, mystText: string): string { 644 + try { 645 + return renderMyst(title, mystText); 646 + } catch (e) { 647 + console.warn("MyST render failed, falling back to escaped text", e); 648 + return escHtml(mystText); 649 + } 650 + } 651 + 413 652 function truncateUri(uri: string): string { 414 653 if (uri.startsWith("http")) { 415 654 try { ··· 425 664 return parts.slice(3).join("/") || uri.slice(0, 60); 426 665 } 427 666 return uri.slice(0, 60); 667 + } 668 + 669 + function canonicalUrlLabel(uri: string): string { 670 + if (!uri.startsWith("http")) return truncateUri(uri); 671 + try { 672 + const u = new URL(uri); 673 + const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, ""); 674 + return `${u.hostname.replace(/^www\./, "")}${path}`; 675 + } catch { 676 + return uri; 677 + } 428 678 } 429 679 430 680 function connectionTypeLabel(t: string): string { ··· 656 906 const content = document.getElementById("page-content"); 657 907 if (!content) return; 658 908 909 + // Use server-injected data if available (no API call needed) 910 + const preloaded = (window as any).__ISLAND_DATA__; 911 + if (preloaded && preloaded.island) { 912 + await renderStrataPage(preloaded.island, preloaded.recordUri); 913 + return; 914 + } 915 + 659 916 try { 660 917 const data = await apiGet<{ island: Island; recordUri: string }>(`/xrpc/org.latha.island.getIsland?id=${encodeURIComponent(atUri)}`); 661 918 await renderStrataPage(data.island, data.recordUri); ··· 754 1011 const s = island.strata!; 755 1012 756 1013 const summary = island.summary || summarizeIsland(island); 1014 + const highlightEdges = island.edges!.filter(e => 1015 + s.highlights.includes(e.uri) || 1016 + s.highlights.some(h => e.source.includes(h) || e.target.includes(h) || h.includes(e.source) || h.includes(e.target)) 1017 + ); 757 1018 758 - // Build editable strata page 1019 + // Build strata page. Editing is toggled explicitly so links remain clickable by default. 759 1020 content.innerHTML = ` 760 - <div class="strata-page"> 1021 + <div class="strata-page" id="strata-page"> 761 1022 <a href="/" class="back-link">&larr; Explore</a> 762 - <p class="island-summary-large" data-field="title" contenteditable="true">${escHtml(summary)}</p> 1023 + <div class="edit-mode-banner">Edit mode is on. Links are editable now; toggle Edit off to open them normally.</div> 1024 + <p class="island-summary-large editable" data-field="title" spellcheck="false">${escHtml(summary)}</p> 763 1025 <p class="island-meta">${island.vertices!.length} vertices &middot; ${island.edges!.length} edges${island.recordUri ? ` &middot; <a href="https://pdsls.dev/${island.recordUri}#record" target="_blank" rel="noopener" class="pds-link">View on PDS &nearr;</a>` : ""}</p> 764 1026 765 1027 <canvas class="island-graph-large" id="strata-graph"></canvas> 1028 + <div class="graph-actions"><button class="edit-toggle hidden" id="edit-toggle" type="button" aria-pressed="false"><span aria-hidden="true">&#9998;</span><span>Edit</span></button></div> 766 1029 767 1030 <div class="strata-section"> 768 - <h3>Synthesis <span class="edit-hint" title="Click to edit">&#9998;</span></h3> 769 - <div class="synthesis-text editable" data-field="synthesis" contenteditable="true">${escHtml(s.synthesis)}</div> 1031 + <h3>Synthesis</h3> 1032 + <div class="synthesis-text editable" data-field="synthesis" spellcheck="false">${s.synthesisDoc ? oxaDocToHtml(s.synthesisDoc) : mystToHtml(summary || "Synthesis", s.synthesis)}</div> 770 1033 </div> 771 1034 772 1035 <div class="strata-section"> 773 - <h3>Themes <span class="edit-hint" title="Click to edit">&#9998;</span></h3> 1036 + <h3>Themes</h3> 774 1037 <div class="theme-tags editable" data-field="themes"> 775 1038 ${s.themes.map(t => `<span class="theme-tag" data-value="${escHtml(t)}">${escHtml(t)}<span class="theme-remove">&times;</span></span>`).join("")} 776 1039 <span class="theme-add">+</span> ··· 778 1041 </div> 779 1042 780 1043 <div class="strata-section"> 781 - <h3>Tensions <span class="edit-hint" title="Click to edit">&#9998;</span></h3> 1044 + <h3>Tensions</h3> 782 1045 <div class="editable-list" data-field="tensions"> 783 - ${s.tensions.map(t => `<div class="analysis-tension editable" contenteditable="true">${escHtml(t)}</div>`).join("")} 1046 + ${s.tensions.map(t => `<div class="analysis-tension editable" spellcheck="false">${escHtml(t)}</div>`).join("")} 784 1047 <div class="list-add" data-field="tensions">+ Add tension</div> 785 1048 </div> 786 1049 </div> 787 1050 788 1051 <div class="strata-section"> 789 - <h3>Open Questions <span class="edit-hint" title="Click to edit">&#9998;</span></h3> 1052 + <h3>Open Questions</h3> 790 1053 <div class="editable-list" data-field="openQuestions"> 791 - ${s.open_questions.map(q => `<div class="analysis-question editable" contenteditable="true">${escHtml(q)}</div>`).join("")} 1054 + ${s.open_questions.map(q => `<div class="analysis-question editable" spellcheck="false">${escHtml(q)}</div>`).join("")} 792 1055 <div class="list-add" data-field="openQuestions">+ Add question</div> 793 1056 </div> 794 1057 </div> 795 1058 796 1059 <div class="strata-actions"> 797 - <button class="strata-btn save-btn hidden" id="save-btn">Save to PDS</button> 1060 + <button class="strata-btn save-btn hidden" id="save-btn">Save</button> 798 1061 <span class="save-status" id="save-status"></span> 799 1062 </div> 800 1063 801 1064 ${s.highlights.length > 0 ? `<div class="strata-section"> 802 - <h3>Highlights <span class="highlights-count">${s.highlights.length} hot edges</span></h3> 803 - <div class="strata-edges">${island.edges!.filter(e => s.highlights.includes(e.uri)).map(e => { 1065 + <h3>Highlights <span class="highlights-count">${highlightEdges.length} hot edges</span></h3> 1066 + <div class="strata-edges">${highlightEdges.map(e => { 804 1067 const sourceMeta = island.vertexMeta![e.source]; 805 1068 const targetMeta = island.vertexMeta![e.target]; 806 1069 const sourceTitle = sourceMeta?.title || truncateUri(e.source); ··· 810 1073 const seemsUrl = e.source.startsWith("http") ? `https://semble.so/url/${encodeURIComponent(e.source)}` : ""; 811 1074 return `<a href="${escHtml(seemsUrl)}" class="strata-edge hot-edge" target="_blank" rel="noopener"${!seemsUrl ? ' onclick="event.preventDefault()"' : ""}> 812 1075 <span class="hot-badge">&#9733;</span> 813 - <span class="strata-edge-source">${escHtml(sourceTitle)}</span> 1076 + <span class="strata-edge-node"><span class="strata-edge-title">${escHtml(sourceTitle)}</span><span class="strata-edge-url">${escHtml(canonicalUrlLabel(e.source))}</span></span> 814 1077 <span class="strata-edge-type">${icon} ${escHtml(type)}</span> 815 - <span class="strata-edge-target">${escHtml(targetTitle)}</span> 1078 + <span class="strata-edge-node"><span class="strata-edge-title">${escHtml(targetTitle)}</span><span class="strata-edge-url">${escHtml(canonicalUrlLabel(e.target))}</span></span> 816 1079 ${e.note ? `<span class="strata-edge-note">${escHtml(e.note)}</span>` : ""} 817 1080 ${e.handle ? `<span class="strata-edge-handle">@${escHtml(e.handle)}</span>` : ""} 818 1081 </a>`; 819 - }).join("")}</div> 1082 + }).join("") || `<div class="empty">No highlighted edges matched the current edge URIs.</div>`}</div> 820 1083 </div>` : ""} 821 1084 822 1085 <div class="strata-section"> ··· 830 1093 const icon = connectionTypeIcon(e.connectionType); 831 1094 const seemsUrl = e.source.startsWith("http") ? `https://semble.so/url/${encodeURIComponent(e.source)}` : ""; 832 1095 return `<a href="${escHtml(seemsUrl)}" class="strata-edge" target="_blank" rel="noopener"${!seemsUrl ? ' onclick="event.preventDefault()"' : ""}> 833 - <span class="strata-edge-source">${escHtml(sourceTitle)}</span> 1096 + <span class="strata-edge-node"><span class="strata-edge-title">${escHtml(sourceTitle)}</span><span class="strata-edge-url">${escHtml(canonicalUrlLabel(e.source))}</span></span> 834 1097 <span class="strata-edge-type">${icon} ${escHtml(type)}</span> 835 - <span class="strata-edge-target">${escHtml(targetTitle)}</span> 1098 + <span class="strata-edge-node"><span class="strata-edge-title">${escHtml(targetTitle)}</span><span class="strata-edge-url">${escHtml(canonicalUrlLabel(e.target))}</span></span> 836 1099 ${e.note ? `<span class="strata-edge-note">${escHtml(e.note)}</span>` : ""} 837 1100 ${e.handle ? `<span class="strata-edge-handle">@${escHtml(e.handle)}</span>` : ""} 838 1101 </a>`; ··· 849 1112 850 1113 // ── Edit tracking ────────────────────────────────────────────── 851 1114 let dirty = false; 852 - const saveBtn = document.getElementById("save-btn"); 1115 + const saveBtn = document.getElementById("save-btn") as HTMLButtonElement | null; 853 1116 const saveStatus = document.getElementById("save-status"); 1117 + const strataPage = document.getElementById("strata-page"); 1118 + const editToggle = document.getElementById("edit-toggle") as HTMLButtonElement | null; 1119 + let editMode = false; 854 1120 855 1121 function markDirty() { 856 1122 dirty = true; 857 1123 saveBtn?.classList.remove("hidden"); 858 - saveStatus!.textContent = ""; 1124 + if (saveStatus) saveStatus.textContent = ""; 859 1125 } 860 1126 861 - // Track contenteditable changes 1127 + function setEditMode(enabled: boolean) { 1128 + editMode = enabled; 1129 + strataPage?.classList.toggle("edit-mode", enabled); 1130 + if (editToggle) { 1131 + editToggle.setAttribute("aria-pressed", String(enabled)); 1132 + editToggle.innerHTML = enabled 1133 + ? '<span aria-hidden="true">&#10003;</span><span>Done editing</span>' 1134 + : '<span aria-hidden="true">&#9998;</span><span>Edit</span>'; 1135 + } 1136 + editContainer.querySelectorAll<HTMLElement>(".editable").forEach(el => { 1137 + el.setAttribute("contenteditable", enabled ? "true" : "false"); 1138 + el.setAttribute("spellcheck", "false"); 1139 + }); 1140 + } 1141 + 1142 + editToggle?.addEventListener("click", () => setEditMode(!editMode)); 1143 + 1144 + // Track editable changes 862 1145 const editContainer = content!; 863 - editContainer.querySelectorAll("[contenteditable]").forEach(el => { 1146 + editContainer.querySelectorAll(".editable").forEach(el => { 864 1147 el.addEventListener("input", markDirty); 865 1148 }); 1149 + setEditMode(false); 866 1150 867 1151 // Theme tag removal 868 1152 editContainer.querySelectorAll(".theme-remove").forEach(btn => { ··· 880 1164 tag.dataset.value = "new theme"; 881 1165 tag.innerHTML = 'new theme<span class="theme-remove">&times;</span>'; 882 1166 tag.setAttribute("contenteditable", "true"); 1167 + tag.setAttribute("spellcheck", "false"); 883 1168 tag.addEventListener("input", () => { tag.dataset.value = tag.textContent?.replace("×", "").trim() || ""; markDirty(); }); 884 1169 tag.querySelector(".theme-remove")!.addEventListener("click", () => { tag.remove(); markDirty(); }); 885 1170 (btn as Element as ChildNode).before(tag); ··· 895 1180 const div = document.createElement("div"); 896 1181 div.className = field === "tensions" ? "analysis-tension editable" : "analysis-question editable"; 897 1182 div.setAttribute("contenteditable", "true"); 1183 + div.setAttribute("spellcheck", "false"); 898 1184 div.textContent = "New item..."; 899 1185 div.addEventListener("input", markDirty); 900 1186 (btn as Element as ChildNode).before(div); ··· 903 1189 }); 904 1190 }); 905 1191 906 - // ── Collect current analysis state ───────────────────────────── 907 1192 function collectAnalysis(): Record<string, any> { 908 1193 const titleEl = editContainer.querySelector('[data-field="title"]'); 909 1194 const synthesisEl = editContainer.querySelector('[data-field="synthesis"]'); 910 1195 const themeTags = editContainer.querySelectorAll('.theme-tag'); 911 1196 const tensionEls = editContainer.querySelectorAll('[data-field="tensions"] .editable'); 912 1197 const questionEls = editContainer.querySelectorAll('[data-field="openQuestions"] .editable'); 913 - 914 1198 return { 915 1199 title: titleEl?.textContent?.trim() || "", 916 1200 synthesis: synthesisEl?.textContent?.trim() || "", 917 - themes: Array.from(themeTags).map(t => t.getAttribute("data-value") || t.textContent?.replace("×", "").trim() || ""), 1201 + themes: Array.from(themeTags).map(t => t.getAttribute("data-value") || t.textContent?.replace("×", "").trim() || "").filter(Boolean), 918 1202 tensions: Array.from(tensionEls).map(t => t.textContent?.trim() || "").filter(Boolean), 919 1203 openQuestions: Array.from(questionEls).map(q => q.textContent?.trim() || "").filter(Boolean), 920 1204 highlights: island.strata?.highlights || [], 921 1205 }; 922 1206 } 923 1207 924 - // ── Save to PDS ──────────────────────────────────────────────── 925 1208 saveBtn?.addEventListener("click", async () => { 926 - if (!island.recordUri) return; 1209 + if (!island.recordUri || !oauthSession || !session) return; 927 1210 saveBtn.textContent = "Saving..."; 928 - saveBtn.setAttribute("disabled", "true"); 929 - 1211 + saveBtn.disabled = true; 930 1212 try { 931 - const analysis = collectAnalysis(); 932 - const res = await fetch("/xrpc/org.latha.island.updateRecord", { 933 - method: "PUT", 1213 + const [ownerDid, collection, rkey] = island.recordUri.replace("at://", "").split("/"); 1214 + const record = { 1215 + ...(island.rawRecord || {}), 1216 + $type: "org.latha.island", 1217 + analysis: { ...(island.rawRecord?.analysis || {}), ...collectAnalysis() }, 1218 + updatedAt: new Date().toISOString(), 1219 + } as any; 1220 + delete record.connections; 1221 + delete record.analysis.connections; 1222 + const isOwner = ownerDid === session.did; 1223 + if (!isOwner) record.forkOf = island.recordUri; 1224 + const endpoint = isOwner ? "/xrpc/com.atproto.repo.putRecord" : "/xrpc/com.atproto.repo.createRecord"; 1225 + const body = isOwner ? { repo: session.did, collection, rkey, record } : { repo: session.did, collection, record }; 1226 + const res = await oauthSession.fetchHandler(endpoint, { 1227 + method: "POST", 934 1228 headers: { "Content-Type": "application/json" }, 935 - body: JSON.stringify({ uri: island.recordUri, analysis }), 1229 + body: JSON.stringify(body), 936 1230 }); 937 - if (!res.ok) { 938 - const err = (await res.json()) as { message?: string }; 939 - throw new Error(err.message || "Save failed"); 940 - } 1231 + if (!res.ok) throw new Error(await res.text()); 1232 + const out = await res.json() as { uri?: string }; 941 1233 dirty = false; 942 1234 saveBtn.classList.add("hidden"); 943 - saveStatus!.textContent = "Saved ✓"; 944 - setTimeout(() => { saveStatus!.textContent = ""; }, 3000); 1235 + saveStatus!.textContent = isOwner ? "Saved ✓" : `Forked to your PDS ✓ ${out.uri || ""}`; 945 1236 } catch (e: any) { 946 - saveStatus!.textContent = `Error: ${e.message}`; 1237 + saveStatus!.textContent = `Error: ${e.message || e}`; 947 1238 } finally { 948 - saveBtn.textContent = "Save to PDS"; 949 - saveBtn.removeAttribute("disabled"); 1239 + saveBtn.textContent = "Save"; 1240 + saveBtn.disabled = false; 950 1241 } 951 1242 }); 952 1243 ··· 1100 1391 1101 1392 // ── Auth ─────────────────────────────────────────────────────────────────── 1102 1393 1394 + async function handleLogin(): Promise<void> { 1395 + const existing = document.getElementById("login-dialog"); 1396 + if (existing) existing.remove(); 1397 + 1398 + const dialog = document.createElement("dialog"); 1399 + dialog.id = "login-dialog"; 1400 + dialog.innerHTML = ` 1401 + <form method="dialog" class="login-form"> 1402 + <h3>Sign in with AT Protocol</h3> 1403 + <p>Enter your Bluesky handle to authenticate via OAuth.</p> 1404 + <input type="text" id="login-handle" placeholder="handle.bsky.social" autocomplete="off" spellcheck="false" /> 1405 + <div class="login-actions"> 1406 + <button type="button" class="login-cancel">Cancel</button> 1407 + <button type="submit" class="login-submit">Sign in</button> 1408 + </div> 1409 + </form> 1410 + `; 1411 + document.body.appendChild(dialog); 1412 + dialog.showModal(); 1413 + 1414 + const handle = await new Promise<string | null>((resolve) => { 1415 + const form = dialog.querySelector("form")!; 1416 + const cancel = dialog.querySelector(".login-cancel")!; 1417 + form.addEventListener("submit", (event) => { 1418 + event.preventDefault(); 1419 + const val = (document.getElementById("login-handle") as HTMLInputElement).value.trim(); 1420 + resolve(val || null); 1421 + }); 1422 + cancel.addEventListener("click", () => { dialog.close(); resolve(null); }); 1423 + dialog.addEventListener("close", () => resolve(null), { once: true }); 1424 + }); 1425 + 1426 + dialog.remove(); 1427 + if (!handle) return; 1428 + 1429 + if (!oauthClient) { 1430 + oauthClient = await BrowserOAuthClient.load({ 1431 + clientId: `${window.location.origin}/oauth-client-metadata.json`, 1432 + identityResolver: browserIdentityResolver, 1433 + }); 1434 + } 1435 + const returnTo = window.location.pathname + window.location.search + window.location.hash; 1436 + sessionStorage.setItem("stigmergic_return_to", returnTo); 1437 + await oauthClient.signInRedirect(handle, { state: returnTo }); 1438 + } 1439 + 1440 + async function handleLogout(): Promise<void> { 1441 + try { await oauthSession?.signOut(); } catch {} 1442 + oauthSession = null; 1443 + session = null; 1444 + updateAuthUI(); 1445 + } 1446 + 1447 + async function resolveHandle(did: string): Promise<string> { 1448 + try { 1449 + const res = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`); 1450 + if (res.ok) { 1451 + const data: any = await res.json(); 1452 + return data.handle || did; 1453 + } 1454 + } catch (e) { 1455 + console.warn("OAuth init failed", e); 1456 + } 1457 + return did; 1458 + } 1459 + 1103 1460 async function initAuth(): Promise<void> { 1104 1461 try { 1105 - const res = await fetch("/xrpc/org.latha.island.getProfile", { 1106 - headers: { Authorization: "Bearer " + localStorage.getItem("stigmergic_at") }, 1462 + oauthClient = await BrowserOAuthClient.load({ 1463 + clientId: `${window.location.origin}/oauth-client-metadata.json`, 1464 + identityResolver: browserIdentityResolver, 1107 1465 }); 1108 - if (res.ok) { 1109 - const data: any = await res.json(); 1110 - session = { did: data.did, handle: data.handle }; 1466 + const result = await oauthClient.init(); 1467 + if (result?.session) { 1468 + oauthSession = result.session; 1469 + session = { did: result.session.did, handle: await resolveHandle(result.session.did) }; 1470 + const returnTo = result.state || sessionStorage.getItem("stigmergic_return_to"); 1471 + sessionStorage.removeItem("stigmergic_return_to"); 1472 + if (returnTo && returnTo !== window.location.pathname + window.location.search + window.location.hash) { 1473 + history.replaceState(null, "", returnTo); 1474 + setTimeout(() => route(), 0); 1475 + } else if (window.location.hash.includes("state=") || window.location.search.includes("state=")) { 1476 + history.replaceState(null, "", window.location.pathname); 1477 + } 1111 1478 updateAuthUI(); 1112 1479 return; 1113 1480 } ··· 1122 1489 if (!status || !loginBtn) return; 1123 1490 1124 1491 if (session) { 1125 - status.textContent = session.handle; 1492 + status.innerHTML = `<span class="profile-chip">@${escHtml(session.handle)}</span>`; 1126 1493 status.classList.add("visible"); 1127 1494 loginBtn.classList.add("hidden"); 1495 + document.getElementById("edit-toggle")?.classList.remove("hidden"); 1128 1496 } else { 1129 1497 status.classList.remove("visible"); 1130 1498 loginBtn.classList.remove("hidden"); 1499 + document.getElementById("edit-toggle")?.classList.add("hidden"); 1131 1500 } 1501 + 1502 + loginBtn.onclick = handleLogin; 1503 + status.onclick = handleLogout; 1132 1504 } 1133 1505 1134 1506 // ── Init ─────────────────────────────────────────────────────────────────── 1135 1507 1136 1508 document.addEventListener("DOMContentLoaded", () => { 1509 + initAuth(); 1510 + updateAuthUI(); 1137 1511 route(); 1138 1512 }); 1139 1513
+111 -20
src/island-analyze.ts
··· 15 15 import { resolve, dirname } from 'node:path'; 16 16 import { fileURLToPath } from 'node:url'; 17 17 import { Database } from 'bun:sqlite'; 18 + import { AtpAgent } from '@atproto/api'; 18 19 import { 19 20 createAgent, 20 21 createSession, ··· 27 28 28 29 const __dirname = dirname(fileURLToPath(import.meta.url)); 29 30 await loadDotEnv(resolve(__dirname, '..', '.env')); 31 + await loadDotEnv('/home/nandi/code/swarm/.env'); // fallback for shared credentials 30 32 31 33 const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; 32 34 const DB_PATH = process.env.ISLAND_DETECT_DB ?? resolve(CARRY_DIR, '.island-detect.db'); 35 + const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? ''; 36 + const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? ''; 37 + const PDS_IDENTIFIER = process.env.PDS_IDENTIFIER ?? BLUESKY_HANDLE; 38 + const PDS_APP_PASSWORD = process.env.PDS_APP_PASSWORD ?? BLUESKY_APP_PASSWORD; 39 + const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network'; 33 40 34 41 // --- Types --- 35 42 ··· 108 115 newConnections: JSON.parse(row.new_connections ?? '[]'), 109 116 analyzedAt: row.analyzed_at, 110 117 }; 118 + } 119 + 120 + function buildIslandRecord(db: Database, component: Component, result: AnalysisResult): Record<string, unknown> { 121 + const edges = fetchComponentEdges(db, component); 122 + const connections = edges 123 + .filter((e) => e.atUri) 124 + .map((e) => ({ uri: e.atUri })); 125 + 126 + return { 127 + $type: 'org.latha.island', 128 + source: { 129 + uri: component.centroid, 130 + collection: 'network.cosmik.connection', 131 + }, 132 + connections, 133 + analysis: { 134 + title: result.title, 135 + themes: result.themes, 136 + highlights: result.highlights, 137 + tensions: result.tensions, 138 + openQuestions: result.openQuestions, 139 + synthesis: result.synthesis, 140 + }, 141 + createdAt: result.analyzedAt, 142 + updatedAt: new Date().toISOString(), 143 + }; 144 + } 145 + 146 + async function publishIslandAnalysis(db: Database, component: Component, result: AnalysisResult): Promise<string> { 147 + if (!PDS_IDENTIFIER || !PDS_APP_PASSWORD) { 148 + throw new Error('PDS_IDENTIFIER and PDS_APP_PASSWORD (or BLUESKY_IDENTIFIER/BLUESKY_APP_PASSWORD) must be set'); 149 + } 150 + 151 + const record = buildIslandRecord(db, component, result); 152 + const agent = new AtpAgent({ service: PDS_SERVICE }); 153 + await agent.login({ identifier: PDS_IDENTIFIER, password: PDS_APP_PASSWORD }); 154 + 155 + const published = await agent.api.com.atproto.repo.putRecord({ 156 + repo: agent.session!.did, 157 + collection: 'org.latha.island', 158 + rkey: component.islandId, 159 + record, 160 + }); 161 + 162 + const appview = process.env.STIGMERGIC_APPVIEW ?? 'https://stigmergic.latha.org'; 163 + try { 164 + const resp = await fetch(`${appview}/xrpc/org.latha.island.notifyOfUpdate`, { 165 + method: 'POST', 166 + headers: { 'Content-Type': 'application/json' }, 167 + body: JSON.stringify({ did: agent.session!.did }), 168 + }); 169 + if (!resp.ok) { 170 + console.error(` Appview notify failed: ${resp.status} (non-fatal, cron will catch up)`); 171 + } 172 + } catch (e: any) { 173 + console.error(` Appview notify error: ${e.message} (non-fatal, cron will catch up)`); 174 + } 175 + 176 + return published.data.uri; 111 177 } 112 178 113 179 // --- Build island context from SQLite data --- ··· 161 227 const edgeSample = edges.slice(0, 40) 162 228 .map((e) => { 163 229 const handle = didHandles.get(e.did) ?? e.did; 164 - return `${domainFromUrl(e.source)} → ${domainFromUrl(e.target)} [${e.relation || 'relates'}] (by ${handle})`; 230 + return `${e.atUri ?? ''}: ${domainFromUrl(e.source)} → ${domainFromUrl(e.target)} [${e.relation || 'relates'}] (by ${handle})`; 165 231 }) 166 232 .join('\n'); 167 233 ··· 196 262 properties: { 197 263 title: { type: 'string', description: 'Concise sentence capturing the core narrative' }, 198 264 themes: { type: 'string', description: 'Comma-separated key themes' }, 199 - highlights: { type: 'string', description: 'Comma-separated semantically significant vertices or edges' }, 265 + highlights: { type: 'string', description: 'Comma-separated AT-URIs of semantically significant EDGE records only. Use exact at://.../network.cosmik.connection/... URIs from the Edges section. Do not output domains, vertex URLs, project names, or descriptions.' }, 200 266 tensions: { type: 'string', description: 'Comma-separated contradictions or unresolved tensions' }, 201 - openQuestions: { type: 'string', description: 'Comma-separated questions raised by this island' }, 202 - synthesis: { type: 'string', description: 'A single paragraph (3-5 sentences) synthesizing the island' }, 267 + openQuestions: { type: 'string', description: 'Semicolon-separated questions raised by this island. IMPORTANT: each item must be one complete question ending in ?; do not use commas as separators because questions often contain commas.' }, 268 + synthesis: { type: 'string', description: 'Rich synthesis in MyST markdown. Use [label](url) for links, **bold** for key terms, *emphasis* for distinctions. 2-4 paragraphs.' }, 203 269 newConnections: { type: 'string', description: 'Semicolon-separated new edges as: source|target|type|note' }, 204 270 }, 205 271 required: ['title', 'themes', 'synthesis'], ··· 207 273 execute: async (_id, args): Promise<AgentToolResult<unknown>> => { 208 274 const a = args as Record<string, string>; 209 275 const parseList = (s: string | undefined) => (s ? s.split(',').map(x => x.trim()).filter(Boolean) : []); 276 + const parseQuestions = (s: string | undefined) => { 277 + if (!s) return []; 278 + const parts = s.includes(';') ? s.split(';') : s.match(/[^?]+\?/g) ?? [s]; 279 + return parts.map(x => x.trim()).filter(Boolean); 280 + }; 210 281 const parseConnections = (s: string | undefined) => { 211 282 if (!s) return []; 212 283 return s.split(';').map(x => x.trim()).filter(Boolean).map(part => { ··· 219 290 centroid: '', 220 291 title: a.title ?? '', 221 292 themes: parseList(a.themes), 222 - highlights: parseList(a.highlights), 293 + highlights: parseList(a.highlights).filter(h => h.startsWith('at://')), 223 294 tensions: parseList(a.tensions), 224 - openQuestions: parseList(a.openQuestions), 295 + openQuestions: parseQuestions(a.openQuestions), 225 296 synthesis: a.synthesis ?? '', 226 297 newConnections: parseConnections(a.newConnections), 227 298 analyzedAt: new Date().toISOString(), ··· 238 309 const args = process.argv.slice(2); 239 310 const wantJson = args.includes('--json'); 240 311 const force = args.includes('--force'); 312 + const publish = args.includes('--publish'); 241 313 const analyzeAll = args.includes('--all'); 242 314 const minSizeArg = args.find((a) => a.startsWith('--min-size=')); 243 315 const minSize = minSizeArg ? Number(minSizeArg.split('=')[1]) : 3; ··· 247 319 const targetId = args.find((a) => !a.startsWith('--')); 248 320 249 321 if (!targetId && !analyzeAll) { 250 - console.error('Usage: bun island:analyze <island-id> [--force] [--json] [--model=auto-fast]'); 251 - console.error(' bun island:analyze --all [--min-size 5] [--force] [--json]'); 322 + console.error('Usage: bun island:analyze <island-id> [--force] [--publish] [--json] [--model=auto-fast]'); 323 + console.error(' bun island:analyze --all [--min-size 5] [--force] [--publish] [--json]'); 252 324 process.exit(1); 253 325 } 254 326 ··· 304 376 Your output must be a single island_save tool call with these fields: 305 377 - title: concise sentence capturing the island's core narrative 306 378 - themes: list of key themes 307 - - highlights: semantically significant vertices or edges 379 + - highlights: comma-separated AT-URIs of semantically significant EDGE records only. Use exact at://.../network.cosmik.connection/... URIs from the Edges section. Do not output domains, vertex URLs, project names, or descriptions. 308 380 - tensions: contradictions or unresolved tensions 309 - - openQuestions: questions raised by this island 310 - - synthesis: a single paragraph (3-5 sentences) connecting the vertices and edges into a coherent narrative. Do NOT write more than one paragraph. 381 + - openQuestions: semicolon-separated questions raised by this island. Each item must be exactly one complete question ending in ?. Do not separate questions with commas; use semicolons. 382 + - synthesis: a rich synthesis in MyST markdown format. Use proper markdown links [label](url) for every URL mentioned, **bold** for key terms and names, and *emphasis* for important distinctions. Write 2-4 paragraphs. Structure: open with the central question, develop each major thread, close with the core tension or synthesis. Every tool, project, person, and URL in the island should be linked. 311 383 - newConnections: suggested new edges between vertices 312 384 313 385 CRITICAL: You MUST call island_save. Do not just describe your analysis in text. Call the tool.`, ··· 329 401 await session.initialize(); 330 402 331 403 const results: AnalysisResult[] = []; 404 + const published = new Map<string, string>(); 332 405 333 406 for (const comp of targets) { 334 407 // Check if already analyzed ··· 340 413 if (cached) { 341 414 cached.centroid = comp.centroid; 342 415 results.push(cached); 416 + if (publish) { 417 + const atUri = await publishIslandAnalysis(db, comp, cached); 418 + published.set(comp.islandId, atUri); 419 + console.error(` Published: ${atUri}`); 420 + } 343 421 } 344 422 continue; 345 423 } ··· 372 450 continue; 373 451 } 374 452 453 + const analysisResult = pendingResult; 454 + 375 455 // Fill in island metadata 376 - pendingResult.islandId = comp.islandId; 377 - pendingResult.centroid = comp.centroid; 456 + analysisResult.islandId = comp.islandId; 457 + analysisResult.centroid = comp.centroid; 378 458 379 - saveAnalysis(db, pendingResult); 380 - results.push(pendingResult); 459 + saveAnalysis(db, analysisResult); 460 + results.push(analysisResult); 381 461 382 - console.error(` Title: ${pendingResult.title.slice(0, 80)}`); 383 - console.error(` Themes: ${pendingResult.themes.join(', ') || 'none'}`); 384 - console.error(` New connections: ${pendingResult.newConnections.length}`); 385 - console.error(` Tensions: ${pendingResult.tensions.length}`); 462 + if (publish) { 463 + const atUri = await publishIslandAnalysis(db, comp, analysisResult); 464 + published.set(comp.islandId, atUri); 465 + console.error(` Published: ${atUri}`); 466 + } 467 + 468 + console.error(` Title: ${analysisResult.title.slice(0, 80)}`); 469 + console.error(` Themes: ${analysisResult.themes.join(', ') || 'none'}`); 470 + console.error(` New connections: ${analysisResult.newConnections.length}`); 471 + console.error(` Tensions: ${analysisResult.tensions.length}`); 386 472 } 387 473 388 474 await session[Symbol.asyncDispose](); 389 475 390 476 if (wantJson) { 391 - console.log(JSON.stringify(results, null, 2)); 477 + console.log(JSON.stringify(results.map((r) => ({ 478 + ...r, 479 + publishedAtUri: published.get(r.islandId), 480 + })), null, 2)); 392 481 db.close(); 393 482 return; 394 483 } 395 484 396 485 for (const r of results) { 397 486 console.log(`\n${r.islandId}: ${r.title}`); 487 + const atUri = published.get(r.islandId); 488 + if (atUri) console.log(` Published: ${atUri}`); 398 489 console.log(` Themes: ${r.themes.join(', ') || 'none'}`); 399 490 console.log(` New connections: ${r.newConnections.length}`); 400 491 if (r.newConnections.length > 0) {
+31 -24
src/island-discover.ts
··· 77 77 return dids; 78 78 } 79 79 80 - async function resolvePDS(did: string): Promise<string | null> { 80 + async function resolveDidDoc(did: string): Promise<{ pds: string | null; handle: string | null } | null> { 81 81 try { 82 82 const res = await fetch(`https://plc.directory/${did}`); 83 83 if (!res.ok) return null; 84 - const doc = await res.json() as { service?: Array<{ id: string; serviceEndpoint: string }> }; 85 - return doc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint ?? null; 86 - } catch { return null; } 87 - } 88 - 89 - async function resolveHandle(did: string): Promise<string | null> { 90 - try { 91 - const res = await fetch(`https://plc.directory/${did}`); 92 - if (!res.ok) return null; 93 - const doc = await res.json() as { alsoKnownAs?: string[] }; 94 - return doc.alsoKnownAs?.find((h) => h.startsWith('at://'))?.replace('at://', '') ?? null; 84 + const doc = await res.json() as { service?: Array<{ id: string; serviceEndpoint: string }>; alsoKnownAs?: string[] }; 85 + const pds = doc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint ?? null; 86 + const handle = doc.alsoKnownAs?.find((h) => h.startsWith('at://'))?.replace('at://', '') ?? null; 87 + return { pds, handle }; 95 88 } catch { return null; } 96 89 } 97 90 ··· 219 212 const networkDids = await discoverDIDs(); 220 213 console.error(`Found ${networkDids.length} DIDs`); 221 214 222 - // Fetch edges for each DID (use cache unless --refresh) 215 + // Fetch edges for each DID (use cache unless --refresh), parallelized 223 216 let fetched = 0; 224 217 let cached = 0; 225 - for (let i = 0; i < networkDids.length; i++) { 226 - const did = networkDids[i]; 218 + const toFetch: string[] = []; 219 + for (const did of networkDids) { 227 220 if (!refresh && isCached(db, did)) { 228 221 cached++; 229 - continue; 222 + } else { 223 + toFetch.push(did); 230 224 } 231 - console.error(`[${i + 1}/${networkDids.length}] Fetching ${did}...`); 232 - const pds = await resolvePDS(did); 233 - if (!pds) { console.error(' Could not resolve PDS'); continue; } 234 - const handle = await resolveHandle(did); 235 - const records = await fetchConnectionRecords(pds, did); 236 - upsertDid(db, did, handle, pds, records.length); 237 - upsertEdges(db, did, records); 238 - fetched++; 225 + } 226 + console.error(`Cached: ${cached}, Fetching: ${toFetch.length}`); 227 + 228 + const CONCURRENCY = 10; 229 + for (let i = 0; i < toFetch.length; i += CONCURRENCY) { 230 + const batch = toFetch.slice(i, i + CONCURRENCY); 231 + const results = await Promise.allSettled(batch.map(async (did) => { 232 + const didDoc = await resolveDidDoc(did); 233 + if (!didDoc || !didDoc.pds) return null; 234 + const records = await fetchConnectionRecords(didDoc.pds, did); 235 + return { did, handle: didDoc.handle, pds: didDoc.pds, records }; 236 + })); 237 + for (const r of results) { 238 + if (r.status === 'fulfilled' && r.value) { 239 + const { did, handle, pds, records } = r.value; 240 + upsertDid(db, did, handle, pds, records.length); 241 + upsertEdges(db, did, records); 242 + fetched++; 243 + } 244 + } 245 + console.error(`[${Math.min(i + CONCURRENCY, toFetch.length)}/${toFetch.length}] fetched=${fetched}`); 239 246 } 240 247 console.error(`Fetched: ${fetched}, Cached: ${cached}`); 241 248
+157
src/island-search.ts
··· 1 + /** 2 + * island:search 3 + * 4 + * Search discovered islands by a free-text query and return closest components. 5 + * Uses the SQLite cache from island:discover. 6 + * 7 + * Usage: 8 + * bun island:search "inlay web tiles" 9 + * bun island:search "collective sensemaking" --top 10 10 + * bun island:search "atproto ui" --json 11 + */ 12 + 13 + import { openDb, findComponents, domainFromUrl, fetchComponentEdges, resolveDidHandles } from './island-shared.js'; 14 + 15 + interface SearchResult { 16 + islandId: string; 17 + centroid: string; 18 + nodeCount: number; 19 + edgeCount: number; 20 + didCount: number; 21 + score: number; 22 + matchedTerms: string[]; 23 + matchedUrls: string[]; 24 + sampleEdges: string[]; 25 + } 26 + 27 + function usage(): never { 28 + console.error('Usage: bun island:search <search terms> [--top N|--top=N] [--json]'); 29 + process.exit(1); 30 + } 31 + 32 + function tokenize(s: string): string[] { 33 + return [...new Set( 34 + s.toLowerCase() 35 + .replace(/https?:\/\//g, ' ') 36 + .replace(/[^a-z0-9.\-]+/g, ' ') 37 + .split(/\s+/) 38 + .map(t => t.trim()) 39 + .filter(t => t.length >= 2) 40 + )]; 41 + } 42 + 43 + function urlText(url: string): string { 44 + try { 45 + const u = new URL(url); 46 + return `${u.hostname.replace(/^www\./, '')} ${u.pathname.replace(/[\/_\-.]+/g, ' ')}`.toLowerCase(); 47 + } catch { 48 + return url.toLowerCase(); 49 + } 50 + } 51 + 52 + function scoreUrl(url: string, terms: string[]): { score: number; matched: string[] } { 53 + const text = urlText(url); 54 + const host = domainFromUrl(url).toLowerCase(); 55 + let score = 0; 56 + const matched: string[] = []; 57 + for (const term of terms) { 58 + if (host === term || host.startsWith(`${term}.`) || host.includes(`.${term}.`)) { 59 + score += 6; 60 + matched.push(term); 61 + } else if (host.includes(term)) { 62 + score += 4; 63 + matched.push(term); 64 + } else if (text.includes(term)) { 65 + score += 2; 66 + matched.push(term); 67 + } 68 + } 69 + return { score, matched }; 70 + } 71 + 72 + function parseArgs(): { query: string; topN: number; json: boolean } { 73 + const args = process.argv.slice(2); 74 + const json = args.includes('--json'); 75 + const topEq = args.find(a => a.startsWith('--top=')); 76 + const topIdx = args.indexOf('--top'); 77 + const topN = topEq ? Number(topEq.split('=')[1]) : topIdx >= 0 ? Number(args[topIdx + 1]) : 5; 78 + const query = args.filter((a, i) => a !== '--json' && !a.startsWith('--top=') && a !== '--top' && i !== topIdx + 1).join(' ').trim(); 79 + if (!query) usage(); 80 + return { query, topN: Number.isFinite(topN) && topN > 0 ? topN : 5, json }; 81 + } 82 + 83 + async function main(): Promise<void> { 84 + const { query, topN, json } = parseArgs(); 85 + const terms = tokenize(query); 86 + if (terms.length === 0) usage(); 87 + 88 + const db = openDb(); 89 + const edgeCount = (db.query('SELECT COUNT(*) as c FROM edges').get() as { c: number } | null)?.c ?? 0; 90 + if (edgeCount === 0) { 91 + console.error('No island cache found. Run `bun island:discover` first.'); 92 + process.exit(1); 93 + } 94 + 95 + const components = findComponents(db); 96 + const results: SearchResult[] = []; 97 + 98 + for (const comp of components) { 99 + let score = 0; 100 + const matchedTerms = new Set<string>(); 101 + const matchedUrls: Array<{ url: string; score: number }> = []; 102 + 103 + for (const node of comp.nodes) { 104 + const s = scoreUrl(node, terms); 105 + if (s.score > 0) { 106 + score += s.score; 107 + for (const t of s.matched) matchedTerms.add(t); 108 + matchedUrls.push({ url: node, score: s.score }); 109 + } 110 + } 111 + 112 + if (score === 0) continue; 113 + 114 + // Normalize lightly so tiny exact matches can beat huge noisy components. 115 + const normalized = score / Math.log2(comp.nodes.size + 2); 116 + const edges = fetchComponentEdges(db, comp); 117 + const handles = resolveDidHandles(db, comp.dids); 118 + const topEdges = edges.slice(0, 5).map(e => `${domainFromUrl(e.source)} → ${domainFromUrl(e.target)} [${e.relation || 'relates'}]${handles.get(e.did) ? ` by @${handles.get(e.did)}` : ''}`); 119 + 120 + results.push({ 121 + islandId: comp.islandId, 122 + centroid: comp.centroid, 123 + nodeCount: comp.nodes.size, 124 + edgeCount: edges.length, 125 + didCount: comp.dids.size, 126 + score: normalized, 127 + matchedTerms: [...matchedTerms].sort(), 128 + matchedUrls: matchedUrls.sort((a, b) => b.score - a.score).slice(0, 10).map(m => m.url), 129 + sampleEdges: topEdges, 130 + }); 131 + } 132 + 133 + results.sort((a, b) => b.score - a.score); 134 + const top = results.slice(0, topN); 135 + 136 + if (json) { 137 + console.log(JSON.stringify({ query, terms, componentCount: components.length, resultCount: results.length, results: top }, null, 2)); 138 + } else { 139 + console.log(`\nIsland search: "${query}" (${terms.join(', ')})`); 140 + console.log(`Matched ${results.length} of ${components.length} islands\n`); 141 + for (const [i, r] of top.entries()) { 142 + console.log(`${i + 1}. ${r.islandId} score=${r.score.toFixed(2)} nodes=${r.nodeCount} edges=${r.edgeCount} dids=${r.didCount}`); 143 + console.log(` centroid: ${r.centroid}`); 144 + console.log(` matched terms: ${r.matchedTerms.join(', ')}`); 145 + for (const url of r.matchedUrls.slice(0, 5)) console.log(` - ${url}`); 146 + for (const edge of r.sampleEdges.slice(0, 3)) console.log(` ${edge}`); 147 + console.log(''); 148 + } 149 + } 150 + 151 + db.close(); 152 + } 153 + 154 + main().catch((e) => { 155 + console.error(e); 156 + process.exit(1); 157 + });
+129 -4
src/landing-page.ts
··· 13 13 --text-muted: #8b949e; 14 14 --accent: #58a6ff; 15 15 --accent-hover: #79c0ff; 16 + --prose-link: #d6a657; 17 + --prose-link-hover: #f0c674; 16 18 --green: #3fb950; 17 19 --red: #f85149; 18 20 --yellow: #d29922; ··· 34 36 gap: 1.5rem; 35 37 flex-wrap: wrap; 36 38 } 39 + .header-spacer { flex: 1; } 40 + .auth-area { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; } 41 + .login-btn, .profile-chip { 42 + border: 1px solid var(--border); 43 + border-radius: 999px; 44 + background: var(--surface); 45 + color: var(--text-muted); 46 + padding: 0.35rem 0.75rem; 47 + font-size: 0.8125rem; 48 + cursor: pointer; 49 + } 50 + .login-btn:hover, .profile-chip:hover { color: var(--prose-link-hover); border-color: var(--prose-link); } 37 51 header h1 { 38 52 font-size: 1.25rem; 39 53 font-weight: 600; ··· 742 756 #auth-status.visible { display: inline; } 743 757 #login-btn { display: inline; } 744 758 #login-btn.hidden { display: none; } 759 + dialog#login-dialog { 760 + border: 1px solid var(--border); 761 + border-radius: 12px; 762 + background: var(--surface); 763 + color: var(--text); 764 + padding: 1.5rem; 765 + max-width: 380px; 766 + width: 90%; 767 + } 768 + dialog#login-dialog::backdrop { background: rgba(0,0,0,0.55); } 769 + dialog#login-dialog h3 { margin: 0 0 0.5rem; font-size: 1.1rem; } 770 + dialog#login-dialog p { margin: 0 0 1rem; color: var(--text-muted); font-size: 0.875rem; } 771 + dialog#login-dialog input[type="text"] { 772 + width: 100%; 773 + padding: 0.5rem 0.75rem; 774 + border: 1px solid var(--border); 775 + border-radius: 8px; 776 + background: var(--bg); 777 + color: var(--text); 778 + font-size: 0.9rem; 779 + margin-bottom: 1rem; 780 + outline: none; 781 + } 782 + dialog#login-dialog input[type="text"]:focus { border-color: var(--prose-link); } 783 + .login-actions { display: flex; gap: 0.5rem; justify-content: flex-end; } 784 + .login-cancel, .login-submit { 785 + border: 1px solid var(--border); 786 + border-radius: 8px; 787 + padding: 0.4rem 1rem; 788 + font-size: 0.8125rem; 789 + cursor: pointer; 790 + } 791 + .login-cancel { background: var(--surface); color: var(--text-muted); } 792 + .login-cancel:hover { color: var(--text); } 793 + .login-submit { background: var(--prose-link); color: #0d1117; border-color: var(--prose-link); font-weight: 600; } 794 + .login-submit:hover { background: var(--prose-link-hover); } 745 795 746 796 /* ── Islands feed ─────────────────────────────────────────────── */ 747 797 .islands-feed { ··· 972 1022 line-height: 1.7; 973 1023 color: var(--text); 974 1024 } 1025 + .synthesis-text a { 1026 + color: var(--prose-link); 1027 + text-decoration: none; 1028 + border-bottom: 1px solid rgba(214,166,87,0.45); 1029 + } 1030 + .synthesis-text a:hover { 1031 + color: var(--prose-link-hover); 1032 + border-bottom-color: var(--prose-link-hover); 1033 + } 1034 + .synthesis-text p { margin-bottom: 1rem; } 1035 + .synthesis-text p:last-child { margin-bottom: 0; } 975 1036 .strata-btn.has-strata { 976 1037 background: var(--purple); 977 1038 color: #fff; ··· 1011 1072 white-space: nowrap; 1012 1073 max-width: 60%; 1013 1074 } 1075 + .strata-edge-node { 1076 + display: inline-flex; 1077 + flex-direction: row; 1078 + align-items: baseline; 1079 + min-width: 0; 1080 + max-width: 42%; 1081 + gap: 0.4rem; 1082 + } 1083 + .strata-edge-title { 1084 + color: var(--text); 1085 + overflow: hidden; 1086 + text-overflow: ellipsis; 1087 + white-space: nowrap; 1088 + font-weight: 500; 1089 + } 1090 + .strata-edge-url { 1091 + color: var(--prose-link); 1092 + overflow: hidden; 1093 + text-overflow: ellipsis; 1094 + white-space: nowrap; 1095 + font-size: 0.6875rem; 1096 + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; 1097 + } 1098 + .strata-edge-url::before { content: "("; color: var(--text-muted); } 1099 + .strata-edge-url::after { content: ")"; color: var(--text-muted); } 1014 1100 .strata-edge-type { 1015 1101 font-size: 0.6875rem; 1016 1102 padding: 0.0625rem 0.375rem; ··· 1092 1178 .theme-add:hover { background: var(--accent); color: #fff; } 1093 1179 1094 1180 /* Editable fields */ 1095 - .editable { cursor: text; border-radius: 4px; transition: outline 0.15s; outline: 1px solid transparent; } 1096 - .editable:focus { outline: 1px solid var(--accent); background: rgba(88,166,255,0.05); } 1097 - .edit-hint { opacity: 0; font-size: 0.75rem; color: var(--text-muted); cursor: pointer; transition: opacity 0.15s; } 1098 - .strata-section:hover .edit-hint { opacity: 1; } 1181 + .editable { border-radius: 4px; transition: outline 0.15s, background 0.15s; outline: 1px solid transparent; } 1182 + .strata-page.edit-mode .editable { cursor: text; outline: 1px dashed rgba(214,166,87,0.5); background: rgba(214,166,87,0.04); } 1183 + .strata-page.edit-mode .editable:focus { outline: 1px solid var(--prose-link); background: rgba(214,166,87,0.08); } 1184 + .strata-page.edit-mode [contenteditable="true"] { spellcheck: false; } 1185 + .edit-toggle { 1186 + display: inline-flex; 1187 + align-items: center; 1188 + gap: 0.35rem; 1189 + margin-left: 0.75rem; 1190 + padding: 0.25rem 0.625rem; 1191 + border-radius: 999px; 1192 + border: 1px solid var(--border); 1193 + background: var(--surface); 1194 + color: var(--text-muted); 1195 + font-size: 0.75rem; 1196 + cursor: pointer; 1197 + } 1198 + .edit-toggle:hover { color: var(--prose-link-hover); border-color: var(--prose-link); } 1199 + .strata-page.edit-mode .edit-toggle { color: #0d1117; background: var(--prose-link); border-color: var(--prose-link); font-weight: 600; } 1200 + .edit-mode-banner { 1201 + display: none; 1202 + margin: 0 0 1rem; 1203 + padding: 0.625rem 0.875rem; 1204 + border: 1px solid rgba(214,166,87,0.45); 1205 + border-radius: 8px; 1206 + background: rgba(214,166,87,0.08); 1207 + color: var(--prose-link-hover); 1208 + font-size: 0.8125rem; 1209 + } 1210 + .strata-page.edit-mode .edit-mode-banner { display: block; } 1211 + .graph-actions { 1212 + display: flex; 1213 + justify-content: flex-end; 1214 + margin: -0.5rem 0 1.25rem; 1215 + } 1216 + .strata-page:not(.edit-mode) .theme-remove, 1217 + .strata-page:not(.edit-mode) .theme-add, 1218 + .strata-page:not(.edit-mode) .list-add { display: none; } 1099 1219 .editable-list .list-add { 1100 1220 color: var(--text-muted); 1101 1221 font-size: 0.8rem; ··· 1155 1275 <header> 1156 1276 <h1>Stigmergic</h1> 1157 1277 <p class="header-blurb">All content derived from <code>network.cosmik.connection</code> records on the AT Protocol</p> 1278 + <div class="header-spacer"></div> 1279 + <div class="auth-area"> 1280 + <button id="login-btn" class="login-btn" type="button">Login with ATProto</button> 1281 + <span id="auth-status" title="Click to sign out"></span> 1282 + </div> 1158 1283 </header> 1159 1284 <main> 1160 1285 <div id="page-content"></div>
+173
src/synthesis-to-oxa.ts
··· 1 + /** 2 + * Convert existing plain-text synthesis fields to pub.oxa.document records. 3 + * Reads all org.latha.island records from the PDS, converts the synthesis 4 + * text to OXA format using @nandithebull/myst-oxa, and writes back with 5 + * the new synthesisDoc field. 6 + * 7 + * Usage: bun run src/synthesis-to-oxa.ts [--dry-run] [--rkey <rkey>] 8 + */ 9 + import { mystTextToOxaRecord } from "@nandithebull/myst-oxa"; 10 + import { loadDotEnv } from "./cli-utils.js"; 11 + 12 + const args = process.argv.slice(2); 13 + const dryRun = args.includes("--dry-run"); 14 + const force = args.includes("--force"); 15 + const rkeyArg = args.find((a, i) => args[i - 1] === "--rkey"); 16 + 17 + await loadDotEnv(".env"); 18 + 19 + // Also try loading from swarm's .env as fallback 20 + await loadDotEnv("../swarm/.env"); 21 + 22 + const PDS_HOST = "amanita.us-east.host.bsky.network"; 23 + const DID = "did:plc:ngokl2gnmpbvuvrfckja3g7p"; 24 + const APP_PASSWORD = process.env.PDS_APP_PASSWORD || process.env.BLUESKY_APP_PASSWORD; 25 + const IDENTIFIER = process.env.PDS_IDENTIFIER || process.env.BLUESKY_IDENTIFIER || "nandi.latha.org"; 26 + if (!APP_PASSWORD) { 27 + console.error("PDS_APP_PASSWORD or BLUESKY_APP_PASSWORD not found in env"); 28 + process.exit(1); 29 + } 30 + 31 + // Create auth session 32 + const authResp = await fetch(`https://${PDS_HOST}/xrpc/com.atproto.server.createSession`, { 33 + method: "POST", 34 + headers: { "Content-Type": "application/json" }, 35 + body: JSON.stringify({ identifier: IDENTIFIER, password: APP_PASSWORD }), 36 + }); 37 + if (!authResp.ok) { 38 + console.error("Auth failed:", authResp.status, await authResp.text()); 39 + process.exit(1); 40 + } 41 + const { accessJwt } = await authResp.json() as { accessJwt: string }; 42 + console.log("Authenticated successfully"); 43 + 44 + async function pdsPut(collection: string, rkey: string, record: any) { 45 + const res = await fetch( 46 + `https://${PDS_HOST}/xrpc/com.atproto.repo.putRecord`, 47 + { 48 + method: "POST", 49 + headers: { 50 + "Content-Type": "application/json", 51 + Authorization: `Bearer ${accessJwt}`, 52 + }, 53 + body: JSON.stringify({ 54 + repo: DID, 55 + collection, 56 + rkey, 57 + record, 58 + }), 59 + } 60 + ); 61 + if (!res.ok) { 62 + const text = await res.text(); 63 + throw new Error(`PDS PUT ${collection}/${rkey}: ${res.status} ${text}`); 64 + } 65 + return res.json(); 66 + } 67 + 68 + async function main() { 69 + console.log("Fetching island records..."); 70 + 71 + // List all org.latha.island records (public API, no auth needed) 72 + const listRes = await fetch( 73 + `https://${PDS_HOST}/xrpc/com.atproto.repo.listRecords?repo=${DID}&collection=org.latha.island&limit=100` 74 + ).then(r => r.json()); 75 + 76 + const records = listRes.records || []; 77 + console.log(`Found ${records.length} island records`); 78 + 79 + for (const rec of records) { 80 + const rkey = rec.uri.split("/").pop(); 81 + if (rkeyArg && rkey !== rkeyArg) continue; 82 + 83 + const analysis = rec.value?.analysis; 84 + if (!analysis) { 85 + console.log(` ${rkey}: no analysis, skipping`); 86 + continue; 87 + } 88 + 89 + const synthesis = analysis.synthesis; 90 + if (!synthesis) { 91 + console.log(` ${rkey}: no synthesis text, skipping`); 92 + continue; 93 + } 94 + 95 + if (analysis.synthesisDoc && !force) { 96 + console.log(` ${rkey}: already has synthesisDoc, skipping (use --force to re-convert)`); 97 + continue; 98 + } 99 + 100 + console.log( 101 + ` ${rkey}: converting synthesis (${synthesis.length} chars)...` 102 + ); 103 + 104 + try { 105 + const title = analysis.title || ""; 106 + const oxaDoc = await mystTextToOxaRecord(synthesis, title); 107 + 108 + // Post-process: detect URLs in block text and create link facets 109 + const encoder = new TextEncoder(); 110 + // Match https:// URLs and bare domain.name patterns 111 + const URL_RE = /https?:\/\/[^\s,\.)\]]+|\b([a-z][a-z0-9-]+\.(?:com|org|net|io|at|ai|es|place|ing|directory|dev|app|so|me|link|pub|social))\b/gi; 112 + for (const block of oxaDoc.children) { 113 + if (!block.text) continue; 114 + const text = block.text; 115 + const bytes = encoder.encode(text); 116 + let match: RegExpExecArray | null; 117 + while ((match = URL_RE.exec(text)) !== null) { 118 + let url = match[0]; 119 + let domainOnly = false; 120 + // Bare domain like inlay.at — match[1] is the domain 121 + if (match[1]) { 122 + url = `https://${match[1]}`; 123 + domainOnly = true; 124 + } 125 + // For bare domains, link text is just the domain 126 + const charStart = domainOnly ? match.index : match.index; 127 + const charEnd = domainOnly ? match.index + match[1].length : match.index + url.length; 128 + const byteStart = encoder.encode(text.slice(0, charStart)).byteLength; 129 + const byteEnd = encoder.encode(text.slice(0, charEnd)).byteLength; 130 + 131 + // Check if there's already a facet covering this range 132 + const alreadyFaceted = (block.facets || []).some( 133 + (f: any) => f.index.byteStart === byteStart && f.index.byteEnd === byteEnd 134 + ); 135 + if (!alreadyFaceted) { 136 + if (!block.facets) block.facets = []; 137 + block.facets.push({ 138 + index: { byteStart, byteEnd }, 139 + features: [{ $type: "pub.oxa.richtext.facet#link", uri: url }], 140 + }); 141 + } 142 + } 143 + } 144 + 145 + if (dryRun) { 146 + const totalFacets = oxaDoc.children.reduce( 147 + (sum: number, b: any) => sum + (b.facets?.length || 0), 0 148 + ); 149 + console.log(` [DRY RUN] Would add synthesisDoc with ${oxaDoc.children.length} blocks, ${totalFacets} facets`); 150 + console.log( 151 + ` First block: ${JSON.stringify(oxaDoc.children[0]).slice(0, 200)}` 152 + ); 153 + continue; 154 + } 155 + 156 + // Update the record with synthesisDoc 157 + const updatedRecord = { ...rec.value }; 158 + updatedRecord.analysis = { 159 + ...updatedRecord.analysis, 160 + synthesisDoc: oxaDoc, 161 + }; 162 + 163 + await pdsPut("org.latha.island", rkey, updatedRecord); 164 + console.log(` ✓ Updated ${rkey} with synthesisDoc`); 165 + } catch (e) { 166 + console.error(` ✗ Failed to convert ${rkey}:`, e); 167 + } 168 + } 169 + 170 + console.log("Done"); 171 + } 172 + 173 + main().catch(console.error);
+184 -384
src/worker.ts
··· 5 5 import { config } from "./contrail.config.js"; 6 6 import { lexicons } from "../lexicons/generated/index.js"; 7 7 import { LANDING_PAGE } from "./landing-page.js"; 8 - import { StrataContainer } from "./container.js"; 9 - 10 8 // ─── Env ──────────────────────────────────────────────────────────── 11 9 12 10 interface Env { 13 11 DB: D1Database; 14 12 VAULT_BUCKET: R2Bucket; 15 13 CARRY_BUCKET: R2Bucket; 16 - STRATA_CONTAINER: DurableObjectNamespace; 17 - PDS_APP_PASSWORD: string; 18 - CARRY_GITHUB_TOKEN: string; 19 - OBSIDIAN_AUTH_TOKEN: string; 20 - OBSIDIAN_ENCRYPTION_KEY: string; 21 - OBSIDIAN_ENCRYPTION_SALT: string; 22 14 } 23 15 24 16 // ─── Contrail setup ───────────────────────────────────────────────── ··· 570 562 islands.push({ 571 563 id, centroid, vertices, edges: trimmedEdges, vertexMeta, 572 564 title: analysis.title || null, 573 - strata: analysis.synthesis ? { 565 + strata: analysis.synthesis || analysis.synthesisDoc ? { 574 566 prose: analysis.synthesis || "", title: analysis.title || "", 575 567 themes: analysis.themes || [], 576 568 relationships: [], ··· 578 570 tensions: analysis.tensions || [], 579 571 open_questions: analysis.openQuestions || [], 580 572 synthesis: analysis.synthesis || "", 573 + synthesisDoc: analysis.synthesisDoc || null, 581 574 } : null, 582 575 recordUri: row.uri, 583 576 handle: null as string | null, ··· 619 612 return c.json({ error: "MissingRequiredParameter", message: "id is required" }, 400); 620 613 } 621 614 622 - await ensureContrailReady(db); 623 - 624 615 // Normalize handle-based AT URIs to DID-based URIs 625 616 // e.g. at://nandi.latha.org/org.latha.island/rkey → at://did:plc:.../org.latha.island/rkey 626 617 let normalizedId = id; ··· 642 633 } 643 634 } 644 635 645 - // If it's an AT URI, look up the strata record and derive from its centroid 636 + // If it's an AT URI, look up the strata record 646 637 if (normalizedId.startsWith("at://")) { 647 - const row = await db 648 - .prepare("SELECT uri, record FROM records_strata WHERE uri = ?") 649 - .bind(normalizedId) 650 - .first<{ uri: string; record: string }>(); 638 + // Batch: fetch record + resolve handle in one D1 round trip 639 + const did = normalizedId.split("/")[2]; 640 + const stmts: D1PreparedStatement[] = [ 641 + db.prepare("SELECT uri, record FROM records_strata WHERE uri = ?").bind(normalizedId), 642 + ]; 643 + if (!resolvedHandle && did?.startsWith("did:")) { 644 + stmts.push(db.prepare("SELECT handle FROM identities WHERE did = ?").bind(did)); 645 + } 646 + const batchResults = await db.batch(stmts); 651 647 648 + const row = (batchResults[0] as { results: { uri: string; record: string }[] }).results?.[0]; 652 649 if (!row) { 653 650 return c.json({ error: "RecordNotFound" }, 404); 654 651 } 655 652 656 - // Resolve handle if not already known 657 - if (!resolvedHandle) { 658 - const did = normalizedId.split("/")[2]; 659 - if (did?.startsWith("did:")) { 660 - const identity = await db 661 - .prepare("SELECT handle FROM identities WHERE did = ?") 662 - .bind(did) 663 - .first<{ handle: string }>(); 664 - resolvedHandle = identity?.handle || null; 665 - } 653 + if (!resolvedHandle && batchResults[1]) { 654 + const identity = (batchResults[1] as { results: { handle: string }[] }).results?.[0]; 655 + resolvedHandle = identity?.handle || null; 666 656 } 667 657 668 658 const rec = JSON.parse(row.record); ··· 671 661 return c.json({ error: "InvalidRecord" }, 400); 672 662 } 673 663 674 - const island = await deriveIsland(db, centroid); 675 - if (!island) { 676 - return c.json({ error: "IslandNotFound" }, 404); 664 + // Build graph from record's connections — no BFS needed 665 + const connections: any[] = rec.connections || []; 666 + const resolvedEdges = await resolveEdgeUris(db, connections); 667 + const vertexSet = new Set<string>(); 668 + const edges: any[] = []; 669 + for (const conn of resolvedEdges) { 670 + if (conn.source && conn.target) { 671 + vertexSet.add(conn.source); 672 + vertexSet.add(conn.target); 673 + edges.push(conn); 674 + } 675 + } 676 + const vertices = [...vertexSet].sort(); 677 + 678 + // Compute centroid and island ID 679 + const adj = new Map<string, Set<string>>(); 680 + for (const e of edges) { 681 + if (!adj.has(e.source)) adj.set(e.source, new Set()); 682 + if (!adj.has(e.target)) adj.set(e.target, new Set()); 683 + adj.get(e.source)!.add(e.target); 684 + adj.get(e.target)!.add(e.source); 677 685 } 686 + const actualCentroid = computeCentroid(adj, vertexSet); 687 + const islandId = await islandHash(actualCentroid); 678 688 679 - const meta = await resolveVertexMeta(db, island.vertices); 689 + const meta = await resolveVertexMeta(db, vertices); 680 690 const analysis = rec.analysis || {}; 681 691 682 692 return c.json({ 683 693 island: { 684 - ...island, 685 - centroid: island.centroid, 694 + id: islandId, 695 + centroid: actualCentroid, 696 + vertices, 697 + edges, 686 698 vertexMeta: Object.fromEntries(meta), 687 699 summary: analysis.title || null, 688 700 handle: resolvedHandle, 689 - strata: { 701 + rawRecord: rec, 702 + strata: analysis.synthesis ? { 690 703 prose: analysis.synthesis || "", 691 704 title: analysis.title || "", 692 705 themes: analysis.themes || [], ··· 695 708 tensions: analysis.tensions || [], 696 709 open_questions: analysis.openQuestions || [], 697 710 synthesis: analysis.synthesis || "", 698 - }, 711 + synthesisDoc: analysis.synthesisDoc || null, 712 + } : null, 699 713 }, 700 714 recordUri: row.uri, 701 715 }); ··· 746 760 tensions: analysis.tensions || [], 747 761 open_questions: analysis.openQuestions || [], 748 762 synthesis: analysis.synthesis || "", 763 + synthesisDoc: analysis.synthesisDoc || null, 749 764 }, 750 765 recordUri: row.uri, 751 766 }, ··· 798 813 tensions: analysis.tensions || [], 799 814 open_questions: analysis.openQuestions || [], 800 815 synthesis: analysis.synthesis || "", 816 + synthesisDoc: analysis.synthesisDoc || null, 801 817 }, 802 818 }, 803 819 recordUri: row.uri, ··· 901 917 tensions: analysis.tensions || [], 902 918 open_questions: analysis.openQuestions || [], 903 919 synthesis: analysis.synthesis || "", 920 + synthesisDoc: analysis.synthesisDoc || null, 904 921 }; 905 922 906 923 return c.json({ ··· 908 925 ...island, 909 926 vertexMeta: Object.fromEntries(meta), 910 927 summary: analysis.title || null, 911 - strata: strataData, 928 + strata: strataData, 929 + rawRecord: strataRecord, 912 930 }, 913 931 record: strataRecord, 914 932 recordUri: uri, 915 933 }); 916 934 }); 917 935 918 - // ── Container-backed xrpc methods ──────────────────────────────── 919 - 920 - // Helper: read carry data from D1 for analysis 921 - async function getCarryData(db: D1Database, did?: string): Promise<{ 922 - entities: Array<{ atUri: string; name: string; entityType: string; description?: string; url?: string }>; 923 - citations: Array<{ atUri: string; url: string; title: string; takeaway: string; confidence?: string }>; 924 - reasonings: Array<{ atUri: string; claim: string; evidence: string; reasoningType?: string; url?: string }>; 925 - connections: Array<{ atUri: string; source: string; target: string; relation: string; sourceUrl: string; targetUrl: string; context: string }>; 926 - }> { 927 - const entities: Array<{ atUri: string; name: string; entityType: string; description?: string; url?: string }> = []; 928 - const citations: Array<{ atUri: string; url: string; title: string; takeaway: string; confidence?: string }> = []; 929 - const reasonings: Array<{ atUri: string; claim: string; evidence: string; reasoningType?: string; url?: string }> = []; 930 - const connections: Array<{ atUri: string; source: string; target: string; relation: string; sourceUrl: string; targetUrl: string; context: string }> = []; 931 - 932 - try { 933 - // Read entities 934 - const entityRows = await db 935 - .prepare("SELECT uri, record FROM records_entity ORDER BY time_us DESC LIMIT 100") 936 - .all<{ uri: string; record: string }>(); 937 - for (const row of entityRows.results || []) { 938 - try { 939 - const r = JSON.parse(row.record); 940 - entities.push({ 941 - atUri: row.uri, 942 - name: r.name || "", 943 - entityType: r.entityType || "", 944 - description: r.description || "", 945 - url: r.url || "", 946 - }); 947 - } catch {} 948 - } 949 - 950 - // Read citations 951 - const citeRows = await db 952 - .prepare("SELECT uri, record FROM records_citation ORDER BY time_us DESC LIMIT 100") 953 - .all<{ uri: string; record: string }>(); 954 - for (const row of citeRows.results || []) { 955 - try { 956 - const r = JSON.parse(row.record); 957 - citations.push({ 958 - atUri: row.uri, 959 - url: r.url || "", 960 - title: r.title || "", 961 - takeaway: r.takeaway || "", 962 - confidence: r.confidence || "", 963 - }); 964 - } catch {} 965 - } 966 - 967 - // Read reasonings 968 - const reasonRows = await db 969 - .prepare("SELECT uri, record FROM records_reasoning ORDER BY time_us DESC LIMIT 100") 970 - .all<{ uri: string; record: string }>(); 971 - for (const row of reasonRows.results || []) { 972 - try { 973 - const r = JSON.parse(row.record); 974 - reasonings.push({ 975 - atUri: row.uri, 976 - claim: r.claim || "", 977 - evidence: r.evidence || "", 978 - reasoningType: r.reasoningType || "", 979 - url: r.url || "", 980 - }); 981 - } catch {} 982 - } 983 - } catch (e: any) { 984 - console.error("getCarryData error:", e.message); 985 - } 986 - 987 - // Read connections from D1 988 - try { 989 - const connRows = await db 990 - .prepare("SELECT uri, record FROM records_connection ORDER BY time_us DESC LIMIT 100") 991 - .all<{ uri: string; record: string }>(); 992 - for (const row of connRows.results || []) { 993 - try { 994 - const r = JSON.parse(row.record); 995 - connections.push({ 996 - atUri: row.uri, 997 - source: r.source || "", 998 - target: r.target || "", 999 - relation: r.relation || "", 1000 - sourceUrl: r.sourceUrl || "", 1001 - targetUrl: r.targetUrl || "", 1002 - context: r.context || "", 1003 - }); 1004 - } catch {} 1005 - } 1006 - } catch (e: any) { 1007 - console.error("getCarryData connections error:", e.message); 1008 - } 1009 - 1010 - return { entities, citations, reasonings, connections }; 1011 - } 1012 - 1013 - // Helper: call the strata container 1014 - async function callContainer(path: string, method: string, body?: any): Promise<any> { 1015 - const id = env.STRATA_CONTAINER.idFromName("strata"); 1016 - const stub = env.STRATA_CONTAINER.get(id); 1017 - const url = new URL(path, "http://container"); 1018 - const init: RequestInit = { method }; 1019 - if (body) { 1020 - init.body = JSON.stringify(body); 1021 - init.headers = { "Content-Type": "application/json" }; 1022 - } 1023 - const resp = await stub.fetch(new Request(url.toString(), init)); 1024 - const text = await resp.text(); 1025 - try { 1026 - return JSON.parse(text); 1027 - } catch { 1028 - throw new Error(`Container returned non-JSON: ${text.slice(0, 500)}`); 1029 - } 1030 - } 1031 - 1032 - // Sync container data (vault + carry) 1033 - app.post("/xrpc/org.latha.island.syncContainer", async (c) => { 1034 - try { 1035 - const result = await callContainer("/sync", "POST"); 1036 - return c.json(result); 1037 - } catch (e: any) { 1038 - return c.json({ error: "ContainerError", message: e.message }, 500); 1039 - } 1040 - }); 1041 - 1042 936 // Notify appview of a newly published record — crawl the DID's PDS and upsert into D1 1043 937 app.post("/xrpc/org.latha.island.notifyOfUpdate", async (c) => { 1044 938 const body = await c.req.json().catch(() => ({})) as { did?: string; uri?: string }; ··· 1084 978 return c.json({ ingested }); 1085 979 }); 1086 980 1087 - // Container health + data status 1088 - app.get("/xrpc/org.latha.island.containerHealth", async (c) => { 1089 - try { 1090 - const result = await callContainer("/health", "GET"); 1091 - return c.json(result); 1092 - } catch (e: any) { 1093 - return c.json({ error: "ContainerError", message: e.message }, 500); 1094 - } 1095 - }); 1096 - 1097 - // Derive islands — POST, delegates to container 1098 - // With seed: single island. Without: all islands. 1099 - app.post("/xrpc/org.latha.island.deriveIsland", async (c) => { 1100 - const body = await c.req.json().catch(() => ({})) as { seed?: string }; 1101 - const seed = body.seed; 1102 - 1103 - // Load all connections from D1 for the container to traverse 1104 - await ensureContrailReady(db); 1105 - const rows = await db 1106 - .prepare( 1107 - `SELECT r.record, r.did, r.rkey, i.handle 1108 - FROM records_connection r 1109 - LEFT JOIN identities i ON r.did = i.did`, 1110 - ) 1111 - .all<{ record: string; did: string; rkey: string; handle: string | null }>(); 1112 - 1113 - const connections = (rows.results || []).map(r => { 1114 - try { 1115 - const value = JSON.parse(r.record); 1116 - return { 1117 - source: value.source, 1118 - target: value.target, 1119 - connectionType: value.connectionType || "relates", 1120 - note: value.note || "", 1121 - did: r.did, 1122 - rkey: r.rkey, 1123 - }; 1124 - } catch { 1125 - return null; 1126 - } 1127 - }).filter(Boolean); 1128 - 1129 - try { 1130 - const result = await callContainer("/deriveIsland", "POST", { connections, seed: seed || undefined }); 1131 - return c.json(result); 1132 - } catch (e: any) { 1133 - return c.json({ error: "ContainerError", message: e.message }, 500); 1134 - } 1135 - }); 1136 - 1137 - // Run strata analysis — delegates to container 1138 - app.post("/xrpc/org.latha.island.analyze", async (c) => { 1139 - const body = await c.req.json().catch(() => ({})); 1140 - if (!body.island) { 1141 - return c.json({ error: "MissingRequiredField", message: "island is required" }, 400); 1142 - } 1143 - 1144 - try { 1145 - // Read carry data from D1 for the user's DID 1146 - const carryData = await getCarryData(db, body.did); 1147 - 1148 - // Pass carry data to container alongside island 1149 - const payload = { ...body, carryData }; 1150 - const result = await callContainer("/analyze", "POST", payload); 1151 - 1152 - // Create new connection records on PDS 1153 - const newConnections: Array<{ source: string; target: string; connectionType: string; note: string }> = result.newConnections || []; 1154 - const createdUris: string[] = []; 1155 - 1156 - if (newConnections.length > 0) { 1157 - const appPassword = env.PDS_APP_PASSWORD; 1158 - if (appPassword && body.did) { 1159 - const pdsHost = "amanita.us-east.host.bsky.network"; 1160 - const authResp = await fetch(`https://${pdsHost}/xrpc/com.atproto.server.createSession`, { 1161 - method: "POST", 1162 - headers: { "Content-Type": "application/json" }, 1163 - body: JSON.stringify({ identifier: body.did, password: appPassword }), 1164 - }); 1165 - if (authResp.ok) { 1166 - const { accessJwt } = await authResp.json() as { accessJwt: string }; 1167 - 1168 - for (const conn of newConnections) { 1169 - const rkey = crypto.randomUUID().replace(/-/g, "").slice(0, 13); // TID format 1170 - const record = { 1171 - "$type": "network.cosmik.connection", 1172 - source: conn.source, 1173 - target: conn.target, 1174 - connectionType: conn.connectionType, 1175 - note: conn.note, 1176 - createdAt: new Date().toISOString(), 1177 - }; 1178 - 1179 - const createResp = await fetch(`https://${pdsHost}/xrpc/com.atproto.repo.createRecord`, { 1180 - method: "POST", 1181 - headers: { 1182 - "Content-Type": "application/json", 1183 - "Authorization": `Bearer ${accessJwt}`, 1184 - }, 1185 - body: JSON.stringify({ 1186 - repo: body.did, 1187 - collection: "network.cosmik.connection", 1188 - rkey, 1189 - record, 1190 - }), 1191 - }); 1192 - 1193 - if (createResp.ok) { 1194 - const createResult = await createResp.json() as { uri: string }; 1195 - createdUris.push(createResult.uri); 1196 - } 1197 - } 1198 - } 1199 - } 1200 - } 1201 - 1202 - // Add new connection URIs to highlights 1203 - result.highlights = [...(result.highlights || []), ...createdUris]; 1204 - result.createdConnections = createdUris; 1205 - 1206 - return c.json(result); 1207 - } catch (e: any) { 1208 - return c.json({ error: "ContainerError", message: e.message }, 500); 1209 - } 1210 - }); 1211 - 1212 - // ── Update strata record (write back to PDS) ──────────────────── 1213 - app.put("/xrpc/org.latha.island.updateRecord", async (c) => { 1214 - const body = await c.req.json().catch(() => ({})) as { 1215 - uri?: string; 1216 - analysis?: Record<string, any>; 1217 - }; 1218 - if (!body.uri || !body.analysis) { 1219 - return c.json({ error: "MissingRequiredField", message: "uri and analysis are required" }, 400); 1220 - } 1221 - 1222 - const appPassword = env.PDS_APP_PASSWORD; 1223 - if (!appPassword) { 1224 - return c.json({ error: "NotConfigured", message: "PDS_APP_PASSWORD not set" }, 500); 1225 - } 1226 - 1227 - // Parse the AT URI to extract repo DID, collection, rkey 1228 - const parts = body.uri.replace("at://", "").split("/"); 1229 - const repoDid = parts[0]; 1230 - const collection = parts[1]; 1231 - const rkey = parts[2]; 1232 - if (!repoDid || !collection || !rkey) { 1233 - return c.json({ error: "InvalidUri", message: "Expected at://did:.../org.latha.island/rkey" }, 400); 1234 - } 1235 - 1236 - // Read existing record from D1 1237 - const row = await db 1238 - .prepare("SELECT record FROM records_strata WHERE uri = ?") 1239 - .bind(body.uri) 1240 - .first<{ record: string }>(); 1241 - if (!row) { 1242 - return c.json({ error: "RecordNotFound" }, 404); 1243 - } 1244 - 1245 - // Merge updated analysis into existing record 1246 - const record = JSON.parse(row.record); 1247 - record.analysis = { ...record.analysis, ...body.analysis }; 1248 - // Remove stale fields that are no longer in the lexicon 1249 - delete record.analysis.connections; 1250 - delete record.connections; 1251 - record.updatedAt = new Date().toISOString(); 1252 - 1253 - // Write back to PDS via com.atproto.repo.putRecord 1254 - const pdsHost = "amanita.us-east.host.bsky.network"; 1255 - const authResp = await fetch(`https://${pdsHost}/xrpc/com.atproto.server.createSession`, { 1256 - method: "POST", 1257 - headers: { "Content-Type": "application/json" }, 1258 - body: JSON.stringify({ identifier: repoDid, password: appPassword }), 1259 - }); 1260 - if (!authResp.ok) { 1261 - const err = await authResp.text(); 1262 - return c.json({ error: "AuthFailed", message: err }, 500); 1263 - } 1264 - const { accessJwt } = await authResp.json() as { accessJwt: string }; 1265 - 1266 - const putResp = await fetch(`https://${pdsHost}/xrpc/com.atproto.repo.putRecord`, { 1267 - method: "POST", 1268 - headers: { 1269 - "Content-Type": "application/json", 1270 - "Authorization": `Bearer ${accessJwt}`, 1271 - }, 1272 - body: JSON.stringify({ 1273 - repo: repoDid, 1274 - collection, 1275 - rkey, 1276 - record, 1277 - }), 1278 - }); 1279 - if (!putResp.ok) { 1280 - const err = await putResp.text(); 1281 - return c.json({ error: "PutRecordFailed", message: err }, 500); 1282 - } 1283 - 1284 - // Update D1 cache 1285 - await db 1286 - .prepare("UPDATE records_strata SET record = ? WHERE uri = ?") 1287 - .bind(JSON.stringify(record), body.uri) 1288 - .run(); 1289 - 1290 - return c.json({ ok: true, uri: body.uri }); 1291 - }); 1292 - 1293 981 // ── R2 sync endpoints ─────────────────────────────────────────── 1294 982 1295 983 app.put("/api/sync/vault/:did/*", async (c) => { ··· 1367 1055 client_id: `https://${host}/oauth-client-metadata.json`, 1368 1056 client_name: "Stigmergic", 1369 1057 client_uri: `https://${host}`, 1370 - redirect_uris: [`https://${host}/`], 1058 + redirect_uris: [`https://${host}/`, `https://${host}/oauth/callback`], 1371 1059 scope: "atproto transition:generic", 1372 1060 grant_types: ["authorization_code", "refresh_token"], 1373 1061 response_types: ["code"], ··· 1377 1065 }); 1378 1066 }); 1379 1067 1068 + app.get("/oauth/callback", (c) => c.html(landingPage())); 1069 + 1380 1070 // ── SPA fallback: serve landing page for client-side routes ────── 1381 - for (const path of ["/island", "/island/*", "/strata", "/connect"]) { 1071 + 1072 + // /island/<handle>/<rkey> — inject island data for instant hydration 1073 + app.get("/island/:repo/:rkey", async (c) => { 1074 + const repo = c.req.param("repo"); 1075 + const rkey = c.req.param("rkey"); 1076 + 1077 + let islandsJson = "[]"; 1078 + let islandDataJson = "null"; 1079 + try { 1080 + // Batch: island list + specific island record 1081 + const atUri = repo.startsWith("did:") 1082 + ? `at://${repo}/org.latha.island/${rkey}` 1083 + : `at://${repo}/org.latha.island/${rkey}`; 1084 + 1085 + // Resolve handle → DID if needed 1086 + let did = repo; 1087 + let handle: string | null = null; 1088 + if (!repo.startsWith("did:")) { 1089 + const identity = await db 1090 + .prepare("SELECT did, handle FROM identities WHERE handle = ?") 1091 + .bind(repo) 1092 + .first<{ did: string; handle: string }>(); 1093 + if (identity) { 1094 + did = identity.did; 1095 + handle = identity.handle; 1096 + } 1097 + } else { 1098 + const identity = await db 1099 + .prepare("SELECT handle FROM identities WHERE did = ?") 1100 + .bind(repo) 1101 + .first<{ handle: string }>(); 1102 + handle = identity?.handle || null; 1103 + } 1104 + 1105 + const fullAtUri = `at://${did}/org.latha.island/${rkey}`; 1106 + 1107 + // Batch: fetch record + island list 1108 + const stmts = [ 1109 + db.prepare("SELECT uri, record FROM records_strata WHERE uri = ?").bind(fullAtUri), 1110 + db.prepare("SELECT uri, json_extract(record, '$.source.uri') as centroid, json_extract(record, '$.analysis.title') as title FROM records_strata ORDER BY time_us DESC LIMIT 50"), 1111 + ]; 1112 + const [recordResult, listResult] = await db.batch(stmts); 1113 + 1114 + // Build island list 1115 + const listRows = (listResult as { results: { uri: string; centroid: string | null; title: string | null }[] }).results || []; 1116 + const islands: any[] = []; 1117 + for (const row of listRows) { 1118 + if (!row.centroid) continue; 1119 + const id = row.uri.split("/").pop() || row.uri; 1120 + const listDid = row.uri.split("/")[2]; 1121 + // Resolve handle for list items lazily — skip for now, use DID 1122 + islands.push({ id, centroid: row.centroid, title: row.title || null, recordUri: row.uri, handle: listDid === did ? handle : null }); 1123 + } 1124 + islandsJson = JSON.stringify(islands); 1125 + 1126 + // Build island data from record 1127 + const recordRow = (recordResult as { results: { uri: string; record: string }[] }).results?.[0]; 1128 + if (recordRow) { 1129 + const rec = JSON.parse(recordRow.record); 1130 + const centroid = rec.source?.uri; 1131 + if (centroid) { 1132 + const connections: any[] = rec.connections || []; 1133 + const resolvedEdges = await resolveEdgeUris(db, connections); 1134 + const vertexSet = new Set<string>(); 1135 + const edges: any[] = []; 1136 + for (const conn of resolvedEdges) { 1137 + if (conn.source && conn.target) { 1138 + vertexSet.add(conn.source); 1139 + vertexSet.add(conn.target); 1140 + edges.push(conn); 1141 + } 1142 + } 1143 + const vertices = [...vertexSet].sort(); 1144 + const adj = new Map<string, Set<string>>(); 1145 + for (const e of edges) { 1146 + if (!adj.has(e.source)) adj.set(e.source, new Set()); 1147 + if (!adj.has(e.target)) adj.set(e.target, new Set()); 1148 + adj.get(e.source)!.add(e.target); 1149 + adj.get(e.target)!.add(e.source); 1150 + } 1151 + const actualCentroid = computeCentroid(adj, vertexSet); 1152 + const islandId = await islandHash(actualCentroid); 1153 + const meta = await resolveVertexMeta(db, vertices); 1154 + const analysis = rec.analysis || {}; 1155 + 1156 + islandDataJson = JSON.stringify({ 1157 + island: { 1158 + id: islandId, 1159 + centroid: actualCentroid, 1160 + vertices, 1161 + edges, 1162 + vertexMeta: Object.fromEntries(meta), 1163 + summary: analysis.title || null, 1164 + handle, 1165 + strata: analysis.synthesis || analysis.synthesisDoc ? { 1166 + prose: analysis.synthesis || "", 1167 + title: analysis.title || "", 1168 + themes: analysis.themes || [], 1169 + relationships: [], 1170 + highlights: analysis.highlights || [], 1171 + tensions: analysis.tensions || [], 1172 + open_questions: analysis.openQuestions || [], 1173 + synthesis: analysis.synthesis || "", 1174 + synthesisDoc: analysis.synthesisDoc || null, 1175 + } : null, 1176 + }, 1177 + recordUri: recordRow.uri, 1178 + }); 1179 + } 1180 + } 1181 + } catch (e) { 1182 + console.error("Failed to load island data:", e); 1183 + } 1184 + 1185 + const page = LANDING_PAGE.replace( 1186 + "</head>", 1187 + `<script>window.__ISLANDS__=${islandsJson};window.__ISLAND_DATA__=${islandDataJson};</script></head>`, 1188 + ); 1189 + return c.html(page); 1190 + }); 1191 + 1192 + // Other SPA routes — just inject island list 1193 + for (const path of ["/island", "/strata", "/connect"]) { 1382 1194 app.get(path, async (c) => { 1383 1195 let islandsJson = "[]"; 1384 1196 try { 1385 - await ensureContrailReady(db); 1386 1197 const rows = await db 1387 - .prepare("SELECT uri, record FROM records_strata ORDER BY time_us DESC LIMIT 50") 1388 - .all<{ uri: string; record: string }>(); 1198 + .prepare("SELECT uri, json_extract(record, '$.source.uri') as centroid, json_extract(record, '$.analysis.title') as title FROM records_strata ORDER BY time_us DESC LIMIT 50") 1199 + .all<{ uri: string; centroid: string | null; title: string | null }>(); 1389 1200 const islands: any[] = []; 1390 1201 for (const row of rows.results || []) { 1391 - try { 1392 - const rec = JSON.parse(row.record); 1393 - const centroid = rec.source?.uri; 1394 - if (!centroid) continue; 1395 - const id = row.uri.split("/").pop() || row.uri; 1396 - // Compute island hash from centroid (same as island-shared.ts) 1397 - const encoder = new TextEncoder(); 1398 - const data = encoder.encode(centroid); 1399 - const hashBuffer = await crypto.subtle.digest("SHA-256", data); 1400 - const hashArray = Array.from(new Uint8Array(hashBuffer)); 1401 - const islandHash = hashArray.map(b => b.toString(16).padStart(2, "0")).join("").slice(0, 12); 1402 - islands.push({ id, islandHash, centroid, title: rec.analysis?.title || null, recordUri: row.uri }); 1403 - } catch {} 1202 + if (!row.centroid) continue; 1203 + const id = row.uri.split("/").pop() || row.uri; 1204 + islands.push({ id, centroid: row.centroid, title: row.title || null, recordUri: row.uri }); 1404 1205 } 1405 1206 islandsJson = JSON.stringify(islands); 1406 1207 } catch {} ··· 1444 1245 ); 1445 1246 }, 1446 1247 }; 1447 -
+4 -19
wrangler.jsonc
··· 13 13 } 14 14 ], 15 15 16 - // Container for strata analysis 17 - "containers": [ 18 - { 19 - "name": "strata-analysis", 20 - "class_name": "StrataContainer", 21 - "image": "./container/Dockerfile", 22 - "max_instances": 1 23 - } 24 - ], 25 - 26 - "durable_objects": { 27 - "bindings": [ 28 - { 29 - "name": "STRATA_CONTAINER", 30 - "class_name": "StrataContainer" 31 - } 32 - ] 33 - }, 34 - 35 16 "migrations": [ 36 17 { 37 18 "tag": "v1", 38 19 "new_sqlite_classes": ["StrataContainer"] 20 + }, 21 + { 22 + "tag": "v2", 23 + "deleted_classes": ["StrataContainer"] 39 24 } 40 25 ], 41 26