stemedb/community/scripts/seed-whitepaper.ts
jordan 116bad1de3 feat: Ingestor deadlock fix + blessed assertion tracking + patent docs
Key changes:
- Fix Ingestor background task to release lock per iteration, preventing
  deadlock when process_pending() needs the lock during shutdown
- Add blessed assertion predicate index and fetch_blessed_assertions()
  for policy export workflows in Aphoria
- Add patent documentation (markdown + Word exports) for probabilistic
  knowledge graph system
- Update community scripts for claim extraction pipeline

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 03:41:08 -07:00

805 lines
25 KiB
TypeScript

#!/usr/bin/env npx tsx
/**
* Seed whitepaper claims to StemeDB.
*
* This script:
* 1. Loads whitepaper sections from data/whitepaper-sections.json
* 2. Extracts claims from each section (using hardcoded curated claims)
* 3. Creates agents with deterministic keys
* 4. Registers sources and submits assertions to StemeDB
*
* Usage:
* npx tsx scripts/seed-whitepaper.ts
* npx tsx scripts/seed-whitepaper.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 SourceClass = "Regulatory" | "Clinical" | "Observational" | "Expert" | "Community" | "Anecdotal";
type ObjectType = "Text" | "Number" | "Boolean" | "Reference";
interface ObjectValue {
type: ObjectType;
value: string | number | boolean;
}
interface CuratedClaim {
subject: string;
predicate: string;
object: ObjectValue;
confidence: number;
sourceClass: SourceClass;
sourceLabel: string;
sourceUrl?: string;
note?: string;
}
// ============================================================================
// 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(label: string): string {
return toHex(sha256(`source-whitepaper-${label}`));
}
async function createAgent(name: string): Promise<Agent> {
const seedHash = sha256(`whitepaper-seed-agent-${name}`);
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 };
}
// ============================================================================
// Curated Claims from Whitepaper
// These are hand-curated to ensure quality and relevance
// ============================================================================
const WHITEPAPER_CLAIMS: CuratedClaim[] = [
// ===========================================================================
// Introduction Section - Claims about COMPETING databases mentioned in text
// "Every mainstream database, from PostgreSQL to MongoDB to Neo4j, enforces
// a fundamental assumption: at any given time, a key maps to exactly one value"
// ===========================================================================
// PostgreSQL claims from Introduction
{
subject: "PostgreSQL",
predicate: "conflict_resolution",
object: { type: "Text", value: "overwrite or reject" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Explicit claim about PostgreSQL's approach to conflicting writes"
},
{
subject: "PostgreSQL",
predicate: "storage_assumption",
object: { type: "Text", value: "single value per key" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Core assumption of PostgreSQL's data model"
},
{
subject: "PostgreSQL",
predicate: "is_mainstream",
object: { type: "Boolean", value: true },
confidence: 0.85,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Implicit: listed as example of mainstream database"
},
// MongoDB claims from Introduction
{
subject: "MongoDB",
predicate: "conflict_resolution",
object: { type: "Text", value: "overwrite or reject" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Explicit claim about MongoDB's approach to conflicting writes"
},
{
subject: "MongoDB",
predicate: "storage_assumption",
object: { type: "Text", value: "single value per key" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Core assumption of MongoDB's data model"
},
{
subject: "MongoDB",
predicate: "is_mainstream",
object: { type: "Boolean", value: true },
confidence: 0.85,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Implicit: listed as example of mainstream database"
},
// Neo4j claims from Introduction
{
subject: "Neo4j",
predicate: "conflict_resolution",
object: { type: "Text", value: "overwrite or reject" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Explicit claim about Neo4j's approach to conflicting writes"
},
{
subject: "Neo4j",
predicate: "storage_assumption",
object: { type: "Text", value: "single value per key" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Core assumption of Neo4j's data model"
},
{
subject: "Neo4j",
predicate: "is_mainstream",
object: { type: "Boolean", value: true },
confidence: 0.85,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "Implicit: listed as example of mainstream database"
},
// Category-level claims from Introduction
{
subject: "mainstream_databases",
predicate: "storage_assumption",
object: { type: "Text", value: "single value per key" },
confidence: 0.90,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "General claim about mainstream database category"
},
{
subject: "mainstream_databases",
predicate: "conflict_resolution",
object: { type: "Text", value: "overwrite or reject" },
confidence: 0.90,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Introduction",
note: "How mainstream databases handle conflicting values"
},
// ===========================================================================
// Storage & Architecture (use "Episteme" to match page.tsx queries)
// NOTE: All claim values should be COMPLETE SENTENCES that can stand alone
// ===========================================================================
{
subject: "Episteme",
predicate: "storage_model",
object: { type: "Text", value: "Episteme stores assertions in an append-only Merkle DAG" },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 5.1",
note: "Core architectural claim"
},
{
subject: "StemeDB",
predicate: "hash_algorithm",
object: { type: "Text", value: "StemeDB uses BLAKE3 for content-addressing" },
confidence: 0.99,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.3",
note: "Content-addressing algorithm"
},
{
subject: "Episteme",
predicate: "signature_algorithm",
object: { type: "Text", value: "Episteme uses Ed25519 signatures for agent attribution" },
confidence: 0.99,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.4",
note: "Cryptographic signature algorithm"
},
{
subject: "Episteme",
predicate: "serialization_format",
object: { type: "Text", value: "Episteme uses rkyv for zero-copy deserialization" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 5.5"
},
{
subject: "Episteme",
predicate: "data_model",
object: { type: "Text", value: "Episteme stores subject-predicate-object triples with full provenance metadata" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.1"
},
{
subject: "Episteme",
predicate: "content_addressing",
object: { type: "Text", value: "Episteme uses BLAKE3 content-addressing which provides deduplication, integrity verification, and efficient comparison" },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.3",
note: "Content-addressing provides deduplication, integrity, and efficient comparison"
},
{
subject: "Episteme",
predicate: "storage_growth",
object: { type: "Text", value: "Episteme's append-only storage grows without bound, mitigated by semantic decay" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 6.1",
note: "Fundamental tradeoff mitigated by semantic decay"
},
// ===========================================================================
// Background Section - CRDT claims
// ===========================================================================
{
subject: "CRDT",
predicate: "replica_assumption",
object: { type: "Text", value: "CRDTs assume all replicas are authoritative copies of the same logical data" },
confidence: 0.90,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 2.3",
note: "StemeDB's claims are genuinely different assertions from different sources"
},
{
subject: "CRDT",
predicate: "merge_semantics",
object: { type: "Text", value: "automatic merge via mathematical properties" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 2.3",
note: "CRDTs use commutative, associative, idempotent operations"
},
{
subject: "CRDT",
predicate: "consistency_model",
object: { type: "Text", value: "eventual consistency" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 2.3",
note: "All replicas converge to same state eventually"
},
{
subject: "CRDT",
predicate: "conflict_handling",
object: { type: "Text", value: "conflicts are resolved automatically via merge function" },
confidence: 0.90,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 2.3",
note: "No human intervention needed for conflict resolution"
},
// Lens Complexity Claims (use "complexity" to match page.tsx)
{
subject: "RecencyLens",
predicate: "complexity",
object: { type: "Text", value: "RecencyLens has O(n) time complexity and O(1) space complexity" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.1",
note: "Where n = number of candidates"
},
{
subject: "RecencyLens",
predicate: "time_complexity",
object: { type: "Text", value: "RecencyLens runs in O(n) time where n is the number of candidate assertions" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.1",
note: "Where n = number of candidates"
},
{
subject: "RecencyLens",
predicate: "space_complexity",
object: { type: "Text", value: "RecencyLens uses O(1) space" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.1"
},
{
subject: "ConsensusLens",
predicate: "time_complexity",
object: { type: "Text", value: "ConsensusLens runs in O(n) time for grouping and finding the majority" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.2"
},
{
subject: "ConsensusLens",
predicate: "space_complexity",
object: { type: "Text", value: "ConsensusLens uses O(k) space complexity where k is the number of distinct object values" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.2",
note: "Where k = distinct object values"
},
{
subject: "AuthorityLens",
predicate: "time_complexity",
object: { type: "Text", value: "AuthorityLens runs in O(n) time complexity where n is the number of candidates" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.3"
},
{
subject: "SkepticLens",
predicate: "resolution_type",
object: { type: "Text", value: "SkepticLens performs conflict analysis without selecting a winner, preserving all competing claims" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.4"
},
{
subject: "SkepticLens",
predicate: "conflict_metric",
object: { type: "Text", value: "SkepticLens uses normalized Shannon entropy to measure conflict between competing claims" },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.4"
},
// Lens Properties
{
subject: "Lens",
predicate: "property_stateless",
object: { type: "Boolean", value: true },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4"
},
{
subject: "Lens",
predicate: "property_deterministic",
object: { type: "Boolean", value: true },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4"
},
{
subject: "Lens",
predicate: "property_composable",
object: { type: "Boolean", value: true },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4"
},
// Trust Parameters (contested - honest limitation)
// Page queries EigenTrust/parameters
{
subject: "EigenTrust",
predicate: "parameters",
object: { type: "Text", value: "EigenTrust uses 0.5 initial trust with +0.05 reward and -0.1 penalty deltas" },
confidence: 0.72,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 7.1",
note: "Heuristic without theoretical foundation - needs domain-specific calibration"
},
{
subject: "EigenTrust",
predicate: "initial_trust_score",
object: { type: "Number", value: 0.5 },
confidence: 0.72,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 7.1",
note: "Heuristic without theoretical foundation"
},
{
subject: "EigenTrust",
predicate: "reward_delta",
object: { type: "Number", value: 0.05 },
confidence: 0.72,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 7.1",
note: "Heuristic for correct assertions"
},
{
subject: "EigenTrust",
predicate: "penalty_delta",
object: { type: "Number", value: 0.1 },
confidence: 0.72,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 7.1",
note: "Heuristic for incorrect assertions"
},
// Source Tier Weights
{
subject: "SourceClass",
predicate: "tier_0_weight",
object: { type: "Number", value: 1.0 },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.2",
note: "Regulatory tier"
},
{
subject: "SourceClass",
predicate: "tier_1_weight",
object: { type: "Number", value: 0.9 },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.2",
note: "Clinical tier"
},
{
subject: "SourceClass",
predicate: "tier_2_weight",
object: { type: "Number", value: 0.7 },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.2",
note: "Observational tier"
},
{
subject: "SourceClass",
predicate: "tier_3_weight",
object: { type: "Number", value: 0.5 },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.2",
note: "Expert tier"
},
{
subject: "SourceClass",
predicate: "tier_4_weight",
object: { type: "Number", value: 0.2 },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.2",
note: "Community tier"
},
{
subject: "SourceClass",
predicate: "tier_5_weight",
object: { type: "Number", value: 0.1 },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.2",
note: "Anecdotal tier"
},
// MaterializedView
{
subject: "MaterializedView",
predicate: "read_complexity",
object: { type: "Text", value: "MaterializedViews provide O(1) read complexity for pre-computed lens results" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.4"
},
{
subject: "MaterializedView",
predicate: "consistency_model",
object: { type: "Text", value: "MaterializedViews use eventual consistency, updating asynchronously after writes" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 6.3"
},
// Content Addressing Properties
{
subject: "content_addressing",
predicate: "provides_deduplication",
object: { type: "Boolean", value: true },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.3"
},
{
subject: "content_addressing",
predicate: "provides_integrity",
object: { type: "Boolean", value: true },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.3"
},
{
subject: "content_addressing",
predicate: "enables_efficient_comparison",
object: { type: "Boolean", value: true },
confidence: 0.98,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 3.3"
},
// Tradeoffs (use Episteme to match page queries)
{
subject: "Episteme",
predicate: "storage_tradeoff",
object: { type: "Text", value: "Episteme's append-only storage grows without bound, requiring semantic decay for long-term management" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 6.1"
},
{
subject: "Episteme",
predicate: "not_suitable_for",
object: { type: "Text", value: "Episteme is not suitable for ACID transactions requiring strict consistency guarantees" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 6.4"
},
{
subject: "Episteme",
predicate: "not_suitable_for",
object: { type: "Text", value: "Episteme is not designed for high-frequency CRUD workloads" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 6.4"
},
// Write/Read Paths
{
subject: "Episteme",
predicate: "write_path_includes",
object: { type: "Text", value: "Episteme's write path uses a Write-Ahead Log (WAL) with fsync for durability" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 5.2"
},
{
subject: "Episteme",
predicate: "fast_read_path",
object: { type: "Text", value: "Episteme provides O(1) reads via pre-computed materialized views" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 5.3"
},
{
subject: "Episteme",
predicate: "full_resolution_path",
object: { type: "Text", value: "Episteme's full resolution path runs in O(n) when using custom lenses" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 5.3"
},
// Conflict Status Thresholds
{
subject: "SkepticLens",
predicate: "unanimous_threshold",
object: { type: "Text", value: "SkepticLens marks claims as Unanimous when the conflict score is below 0.1" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.4"
},
{
subject: "SkepticLens",
predicate: "agreed_threshold",
object: { type: "Text", value: "SkepticLens marks claims as Agreed when the conflict score is between 0.1 and 0.4" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.4"
},
{
subject: "SkepticLens",
predicate: "contested_threshold",
object: { type: "Text", value: "SkepticLens marks claims as Contested when the conflict score is 0.4 or higher" },
confidence: 0.95,
sourceClass: "Expert",
sourceLabel: "StemeDB Whitepaper - Section 4.2.4"
},
];
// ============================================================================
// API Functions
// ============================================================================
async function registerSource(hash: string, label: string, tier: number, url?: string): Promise<void> {
const SOURCE_CLASS_MAP: Record<number, string> = {
0: "Regulatory",
1: "Clinical",
2: "Observational",
3: "Expert",
4: "Community",
5: "Anecdotal",
};
const response = await fetch(`${API_URL}/v1/sources`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
hash,
label,
tier,
tier_label: SOURCE_CLASS_MAP[tier],
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,
claim: CuratedClaim,
sourceHash: string
): Promise<string | null> {
const { signature, timestamp } = await signAssertion(agent, claim.subject, claim.predicate);
const request = {
subject: claim.subject,
predicate: claim.predicate,
object: claim.object,
confidence: claim.confidence,
source_hash: sourceHash,
source_class: claim.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;
}
// ============================================================================
// Main
// ============================================================================
async function main(): Promise<void> {
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run") || args.includes("-d");
console.log("StemeDB Whitepaper Seed Script");
console.log("==============================\n");
if (dryRun) {
console.log("DRY RUN MODE - No data will be submitted\n");
}
// Create agent
console.log("Creating agent...");
const agent = await createAgent("whitepaper-author");
console.log(` Agent: ${toHex(agent.publicKey).slice(0, 16)}...`);
console.log();
// Group claims by source for registration
const sourceMap = new Map<string, { label: string; tier: number; url?: string }>();
const SOURCE_CLASS_TO_TIER: Record<SourceClass, number> = {
Regulatory: 0,
Clinical: 1,
Observational: 2,
Expert: 3,
Community: 4,
Anecdotal: 5,
};
for (const claim of WHITEPAPER_CLAIMS) {
const hash = generateSourceHash(claim.sourceLabel);
if (!sourceMap.has(hash)) {
sourceMap.set(hash, {
label: claim.sourceLabel,
tier: SOURCE_CLASS_TO_TIER[claim.sourceClass],
url: claim.sourceUrl,
});
}
}
// Register sources
console.log(`Registering ${sourceMap.size} sources...`);
if (!dryRun) {
for (const [hash, source] of sourceMap) {
await registerSource(hash, source.label, source.tier, source.url);
console.log(` + ${source.label.slice(0, 50)}...`);
}
} else {
for (const [hash, source] of sourceMap) {
console.log(` [DRY] Would register: ${source.label.slice(0, 50)}...`);
}
}
console.log();
// Create assertions
console.log(`Creating ${WHITEPAPER_CLAIMS.length} assertions...`);
let created = 0;
let failed = 0;
for (const claim of WHITEPAPER_CLAIMS) {
const sourceHash = generateSourceHash(claim.sourceLabel);
if (dryRun) {
console.log(` [DRY] ${claim.subject}/${claim.predicate} = "${String(claim.object.value).slice(0, 30)}..."`);
created++;
} else {
const hash = await createAssertion(agent, claim, sourceHash);
if (hash) {
created++;
console.log(` + ${claim.subject}/${claim.predicate} -> ${hash.slice(0, 16)}...`);
} else {
failed++;
}
}
}
console.log(`\nCreated ${created} assertions${failed > 0 ? ` (${failed} failed)` : ""}`);
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);
});