#!/usr/bin/env npx tsx /** * Seed external source claims to StemeDB. * * This script: * 1. Loads external sources from data/external-sources.json * 2. Creates agents with varying trust levels * 3. Registers sources and submits assertions to StemeDB * 4. Ensures curated conflicts are created for demo purposes * * Usage: * npx tsx scripts/seed-external.ts * npx tsx scripts/seed-external.ts --dry-run * * Environment: * STEMEDB_API_URL - API base URL (default: http://127.0.0.1:18180) */ import * as ed from "@noble/ed25519"; import { sha512 } from "@noble/hashes/sha512"; import { readFileSync } from "fs"; import { join } from "path"; // Configure ed25519 to use sha512 ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); const API_URL = process.env.STEMEDB_API_URL || "http://127.0.0.1:18180"; // ============================================================================ // Types // ============================================================================ interface Agent { name: string; privateKey: Uint8Array; publicKey: Uint8Array; } type ObjectType = "Text" | "Number" | "Boolean" | "Reference"; interface ObjectValue { type: ObjectType; value: string | number | boolean; } interface ExternalClaim { subject: string; predicate: string; object: ObjectValue; confidence: number; note?: string; } interface ExternalSource { id: string; label: string; url: string; tier: number; tierLabel: string; category: string; claims: ExternalClaim[]; } interface CuratedConflict { id: string; subject: string; predicate: string; description: string; sources: string[]; values: Array<{ source: string; value: string; interpretation: string; }>; demoNote: string; } interface ExternalSourcesData { sources: ExternalSource[]; curatedConflicts: CuratedConflict[]; } // ============================================================================ // Helpers // ============================================================================ function toHex(bytes: Uint8Array): string { return Array.from(bytes) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } function sha256(data: string): Uint8Array { const encoder = new TextEncoder(); const bytes = encoder.encode(data); const hash = new Uint8Array(32); for (let i = 0; i < bytes.length; i++) { hash[i % 32] ^= bytes[i]; hash[(i + 1) % 32] = (hash[(i + 1) % 32] + bytes[i]) % 256; } return hash; } function generateSourceHash(sourceId: string): string { return toHex(sha256(`external-source-${sourceId}`)); } async function createAgent(name: string, seed: string): Promise { const seedHash = sha256(`external-seed-agent-${name}-${seed}`); const privateKey = seedHash; const publicKey = await ed.getPublicKeyAsync(privateKey); return { name, privateKey, publicKey }; } async function signAssertion( agent: Agent, subject: string, predicate: string ): Promise<{ signature: string; timestamp: number }> { const timestamp = Math.floor(Date.now() / 1000); const message = `${subject}:${predicate}`; const messageBytes = new TextEncoder().encode(message); const signature = await ed.signAsync(messageBytes, agent.privateKey); return { signature: toHex(signature), timestamp }; } // ============================================================================ // API Functions // ============================================================================ async function registerSource( hash: string, label: string, tier: number, tierLabel: string, url?: string ): Promise { const response = await fetch(`${API_URL}/v1/sources`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ hash, label, tier, tier_label: tierLabel, url, }), }); if (!response.ok && response.status !== 409) { const text = await response.text(); console.warn(` Warning: Failed to register source ${label}: ${text}`); } } async function createAssertion( agent: Agent, subject: string, predicate: string, object: ObjectValue, confidence: number, sourceHash: string, sourceClass: string ): Promise { const { signature, timestamp } = await signAssertion(agent, subject, predicate); const request = { subject, predicate, object, confidence, source_hash: sourceHash, source_class: sourceClass, signatures: [ { agent_id: toHex(agent.publicKey), signature, timestamp, version: 1, }, ], }; const response = await fetch(`${API_URL}/v1/assert`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), }); if (!response.ok) { const text = await response.text(); console.warn(` Warning: Failed to create assertion: ${text}`); return null; } const data = await response.json(); return data.hash; } // ============================================================================ // Agent Pool // ============================================================================ interface AgentPool { regulatory: Agent; clinical: Agent; observational: Agent; expert: Agent; community: Agent; } async function createAgentPool(): Promise { return { regulatory: await createAgent("regulatory_authority", "reg-001"), clinical: await createAgent("clinical_researcher", "clin-002"), observational: await createAgent("observational_analyst", "obs-003"), expert: await createAgent("domain_expert", "exp-004"), community: await createAgent("community_contributor", "comm-005"), }; } function getAgentForTier(pool: AgentPool, tier: number): Agent { switch (tier) { case 0: return pool.regulatory; case 1: return pool.clinical; case 2: return pool.observational; case 3: return pool.expert; case 4: case 5: return pool.community; default: return pool.expert; } } // ============================================================================ // Main // ============================================================================ async function main(): Promise { const args = process.argv.slice(2); const dryRun = args.includes("--dry-run") || args.includes("-d"); console.log("StemeDB External Sources Seed Script"); console.log("====================================\n"); if (dryRun) { console.log("DRY RUN MODE - No data will be submitted\n"); } // Load external sources data const dataPath = join(process.cwd(), "data", "external-sources.json"); const data: ExternalSourcesData = JSON.parse(readFileSync(dataPath, "utf-8")); console.log(`Loaded ${data.sources.length} external sources`); console.log(`Loaded ${data.curatedConflicts.length} curated conflicts\n`); // Create agent pool console.log("Creating agent pool..."); const agents = await createAgentPool(); console.log(` regulatory: ${toHex(agents.regulatory.publicKey).slice(0, 16)}...`); console.log(` clinical: ${toHex(agents.clinical.publicKey).slice(0, 16)}...`); console.log(` observational: ${toHex(agents.observational.publicKey).slice(0, 16)}...`); console.log(` expert: ${toHex(agents.expert.publicKey).slice(0, 16)}...`); console.log(` community: ${toHex(agents.community.publicKey).slice(0, 16)}...`); console.log(); // Register all sources console.log("Registering sources..."); for (const source of data.sources) { const hash = generateSourceHash(source.id); if (dryRun) { console.log(` [DRY] Would register: ${source.label.slice(0, 50)}...`); } else { await registerSource(hash, source.label, source.tier, source.tierLabel, source.url); console.log(` + T${source.tier} ${source.label.slice(0, 50)}...`); } } console.log(); // Create assertions from all sources console.log("Creating assertions from external sources..."); let totalCreated = 0; let totalFailed = 0; for (const source of data.sources) { const sourceHash = generateSourceHash(source.id); const agent = getAgentForTier(agents, source.tier); console.log(`\n ${source.label}:`); for (const claim of source.claims) { if (dryRun) { console.log( ` [DRY] ${claim.subject}/${claim.predicate} = "${String(claim.object.value).slice(0, 25)}..."` ); totalCreated++; } else { const hash = await createAssertion( agent, claim.subject, claim.predicate, claim.object, claim.confidence, sourceHash, source.tierLabel ); if (hash) { totalCreated++; console.log(` + ${claim.subject}/${claim.predicate} -> ${hash.slice(0, 12)}...`); } else { totalFailed++; } } } } console.log(`\nCreated ${totalCreated} assertions${totalFailed > 0 ? ` (${totalFailed} failed)` : ""}`); // Log curated conflicts for reference console.log("\n--- Curated Conflicts for Demo ---"); for (const conflict of data.curatedConflicts) { console.log(`\n ${conflict.id}:`); console.log(` Subject: ${conflict.subject}/${conflict.predicate}`); console.log(` ${conflict.description}`); console.log(` Demo note: ${conflict.demoNote.slice(0, 60)}...`); } if (!dryRun) { // Wait for materialization console.log("\nWaiting for materialization..."); await new Promise((resolve) => setTimeout(resolve, 2000)); console.log("Done!"); } } main().catch((error) => { console.error("Error:", error.message); process.exit(1); });