UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

1,082 lines (1,030 loc) 48.7 kB
import { MCard } from "./chunk-PW4XS7M3.js"; // src/model/Handle.ts var MAX_HANDLE_LENGTH = 255; function isValidStartChar(char) { return /^\p{L}$/u.test(char); } function isValidBodyChar(char) { return /^[\p{L}\p{N}_./ -]$/u.test(char); } var HandleValidationError = class extends Error { constructor(message) { super(message); this.name = "HandleValidationError"; } }; function validateHandle(handle) { if (!handle) { throw new HandleValidationError("Handle cannot be empty."); } const normalized = handle.trim().normalize("NFC").toLowerCase(); if (normalized.length === 0) { throw new HandleValidationError("Handle cannot be empty after normalization."); } if (normalized.length > MAX_HANDLE_LENGTH) { throw new HandleValidationError( `Handle '${handle}' is too long (${normalized.length} chars). Maximum is ${MAX_HANDLE_LENGTH}.` ); } if (!isValidStartChar(normalized[0])) { throw new HandleValidationError( `Invalid handle '${handle}'. Must start with a letter (any language).` ); } for (let i = 1; i < normalized.length; i++) { if (!isValidBodyChar(normalized[i])) { throw new HandleValidationError( `Invalid character '${normalized[i]}' at position ${i} in handle '${handle}'.` ); } } return normalized; } var ContentHandle = class { handle; currentHash; createdAt; updatedAt; constructor(handle, currentHash, createdAt, updatedAt) { this.handle = validateHandle(handle); this.currentHash = currentHash; this.createdAt = createdAt ?? /* @__PURE__ */ new Date(); this.updatedAt = updatedAt ?? this.createdAt; } /** * Update handle to point to new hash * @returns Previous hash for history tracking */ update(newHash) { const previousHash = this.currentHash; this.currentHash = newHash; this.updatedAt = /* @__PURE__ */ new Date(); return previousHash; } toObject() { return { handle: this.handle, currentHash: this.currentHash, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString() }; } }; // src/storage/schema_constants.ts var MCARD_SCHEMA_SQL = `-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- MCard Unified Database Schema (Monadic Core) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- -- STABILITY WARNING: This schema is the invariant "seeding meta-language" for all -- Domain Specific Languages (DSLs). It must remain as stable as possible. -- -- THE UNIFICATION THESIS: -- This design unites three distinct namespaces into a single relational network -- by leveraging the power of cryptographic hash functions: -- 1. CONTENT SPACE (Intrinsic Identity): Content-Addressable Storage (CAS). -- 2. HANDLE SPACE (Reserved Words): Mapping human logic to content hashes. -- 3. VERSION SPACE (Temporal Evolution): Tracking state transitions via hash history. -- -- TURING MACHINE ANALOGY (The Infinitely Long Tape): -- The \`card\` table stores content blobs with unique identities, emulating the -- "Infinitely Long Tape" of the Turing Machine formalism. By using relational -- queries, this tape can be dynamically constructed and traversed for different -- DSLs, providing a flexible substrate for practically all computable languages. -- -- By using hash values as the universal primitives across content, handles, and -- time, we create a "Wordless Book" \u2014 a minimal substrate capable of seeding any -- domain-specific knowledge system without requiring schema changes. -- -- Monadic Mapping (See: Monadic Justification for Schema Design.md): -- 1. Card = Monad (Perception/State/Exponent) - Intrinsic Identity -- 2. Handle = Registry (Appetition/Reader/Sum) - Mutable Reference -- 3. Version = History (Harmony/Writer/Product) - Coordinated Evolution -- -- Version: 3.0.2 (Turing Tape Refinement) -- Last Updated: 2025-12-20 -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- LAYER 1: CORE CONTENT-ADDRESSABLE STORAGE (The Monad / Exponent) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- The fundamental MCard storage: content-addressed by cryptographic hash. -- "Each Monad ... mirrors the universe" -- @table card -- @description Core content-addressable storage table (Monad/State) -- @column hash - SHA-256 hash of content (primary key) -- @column content - The actual content (BLOB for binary safety) -- @column g_time - Generation timestamp (ISO 8601) CREATE TABLE IF NOT EXISTS card ( hash TEXT PRIMARY KEY, content BLOB NOT NULL, g_time TEXT NOT NULL ); -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- LAYER 2: HANDLE SYSTEM (Appetition / Sum) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- The mutable pointer that "desires" to reference new content. -- "Appetition: the tendency to move to new states" -- @table handle_registry (Monadic Role: Handle) -- @description Maps human-readable handles to current content hashes -- @column handle - UTF-8 handle name (primary key) -- @column current_hash - FK to card.hash of current version -- @column created_at - When handle was first created -- @column updated_at - When handle was last updated CREATE TABLE IF NOT EXISTS handle_registry ( handle TEXT PRIMARY KEY, current_hash TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (current_hash) REFERENCES card(hash) ); -- @index idx_handle_current_hash -- @description Efficient reverse lookup from hash to handles CREATE INDEX IF NOT EXISTS idx_handle_current_hash ON handle_registry(current_hash); -- @table handle_history (Monadic Role: Version / Product) -- @description Audit trail for handle pointer changes (Pre-Established Harmony) -- @column id - Auto-increment primary key -- @column handle - The handle that was updated -- @column previous_hash - Hash it pointed to before update -- @column changed_at - When the change occurred CREATE TABLE IF NOT EXISTS handle_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, handle TEXT NOT NULL, previous_hash TEXT NOT NULL, changed_at TEXT NOT NULL, FOREIGN KEY (handle) REFERENCES handle_registry(handle), FOREIGN KEY (previous_hash) REFERENCES card(hash) ); -- @index idx_handle_history_handle -- @description Efficient lookup of history by handle CREATE INDEX IF NOT EXISTS idx_handle_history_handle ON handle_history(handle); -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- LEGACY SUPPORT: FTS5 Documents Table -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- KEPT FOR BACKWARD COMPATIBILITY with existing codebase. -- New implementations should use mcard_vector_schema.sql -> mcard_fts -- @virtual_table documents -- @description Legacy FTS table for backward compatibility -- @note Synced with card table via triggers CREATE VIRTUAL TABLE IF NOT EXISTS documents USING fts5(content); -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- SCHEMA METADATA -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- @table schema_version -- @description Tracks schema version for migrations CREATE TABLE IF NOT EXISTS schema_version ( version TEXT PRIMARY KEY, applied_at TEXT NOT NULL, description TEXT ); -- Insert current schema version INSERT OR IGNORE INTO schema_version (version, applied_at, description) VALUES ('3.0.0', datetime('now'), 'Monadic Core Schema (split vectors to mcard_vector_schema.sql)'); `; var MCARD_VECTOR_SCHEMA_SQL = `-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- MCard Vector Database Schema (mcard_vectors.db) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- -- This schema defines the structure for "Secondary Qualities" (Extrinsic Embeddings). -- It is separated from the core mcard.db (Monadic Intrinsic Properties) to ensure: -- 1. Separation of Concerns (Intrinsic vs Extrinsic) -- 2. Linearity (Vectors are observer-dependent and large) -- 3. Upgradability (Embedding models change frequently) -- -- See: Monadic Justification for Schema Design.md -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- LAYER 3: VECTOR STORAGE (Semantic Embeddings) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- Storage for vector embeddings enabling semantic search. -- Links to content via hash for content-addressing. -- @table mcard_vector_metadata -- @description Metadata for stored embeddings -- @column id - Auto-increment primary key -- @column hash - FK to card.hash -- @column model_name - Embedding model used (e.g., "nomic-embed-text") -- @column dimensions - Vector dimensions -- @column chunk_index - Index for chunked documents (0 = first/whole) -- @column chunk_total - Total chunks for this document -- @column chunk_text - Preview text for this chunk -- @column created_at - When embedding was created CREATE TABLE IF NOT EXISTS mcard_vector_metadata ( id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL, model_name TEXT NOT NULL, dimensions INTEGER NOT NULL, chunk_index INTEGER DEFAULT 0, chunk_total INTEGER DEFAULT 1, chunk_text TEXT, created_at TEXT NOT NULL, UNIQUE(hash, chunk_index) ); -- @index idx_vector_metadata_hash -- @description Efficient lookup of embeddings by content hash CREATE INDEX IF NOT EXISTS idx_vector_metadata_hash ON mcard_vector_metadata(hash); -- @table mcard_embeddings -- @description Fallback embedding storage when sqlite-vec is unavailable -- @column id - Auto-increment primary key -- @column metadata_id - FK to mcard_vector_metadata.id -- @column embedding - Serialized float32 vector as BLOB CREATE TABLE IF NOT EXISTS mcard_embeddings ( id INTEGER PRIMARY KEY AUTOINCREMENT, metadata_id INTEGER NOT NULL, embedding BLOB NOT NULL, UNIQUE(metadata_id), FOREIGN KEY (metadata_id) REFERENCES mcard_vector_metadata(id) ); -- @virtual_table mcard_fts -- @description Full-text search for hybrid retrieval -- @note Uses Porter stemming with Unicode support CREATE VIRTUAL TABLE IF NOT EXISTS mcard_fts USING fts5( hash, content, tokenize='porter unicode61' ); -- Note: sqlite-vec virtual table is created dynamically with dimensions: -- CREATE VIRTUAL TABLE IF NOT EXISTS mcard_vec USING vec0( -- metadata_id INTEGER PRIMARY KEY, -- embedding float[\${dimensions}] -- ); -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- LAYER 4: SEMANTIC VERSIONING (Handle-Vector Bridge) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- Bridges handles to vector embeddings for semantic version comparison. -- Enables measuring semantic drift between document versions. -- @table handle_version_vectors -- @description Links handle versions to their semantic embeddings -- @column id - Auto-increment primary key -- @column handle - FK to handle_registry.handle -- @column hash - FK to card.hash (this version) -- @column parent_hash - FK to card.hash (previous version) -- @column version_order - 0 = current, 1 = previous, etc. -- @column is_current - TRUE if this is the current version -- @column embedding_id - FK to mcard_vector_metadata.id -- @column semantic_delta_from_parent - Cosine similarity to parent [-1, 1] -- @column upgrade_type - Classification: 'trivial' | 'minor' | 'major' | 'breaking' -- @column created_at - When this version was linked CREATE TABLE IF NOT EXISTS handle_version_vectors ( id INTEGER PRIMARY KEY AUTOINCREMENT, handle TEXT NOT NULL, hash TEXT NOT NULL, parent_hash TEXT, version_order INTEGER NOT NULL, is_current BOOLEAN DEFAULT FALSE, embedding_id INTEGER, semantic_delta_from_parent REAL, upgrade_type TEXT, created_at TEXT NOT NULL, UNIQUE(handle, hash), FOREIGN KEY (embedding_id) REFERENCES mcard_vector_metadata(id) ); -- @index idx_hvv_handle -- @description Efficient lookup of versions by handle CREATE INDEX IF NOT EXISTS idx_hvv_handle ON handle_version_vectors(handle); -- @index idx_hvv_hash -- @description Efficient lookup of versions by hash CREATE INDEX IF NOT EXISTS idx_hvv_hash ON handle_version_vectors(hash); -- @index idx_hvv_current -- @description Efficient lookup of current versions CREATE INDEX IF NOT EXISTS idx_hvv_current ON handle_version_vectors(is_current); -- @index idx_hvv_parent -- @description Efficient lookup by parent hash CREATE INDEX IF NOT EXISTS idx_hvv_parent ON handle_version_vectors(parent_hash); -- @table version_similarity_cache -- @description Precomputed pairwise similarities for performance -- @column id - Auto-increment primary key -- @column handle - Handle these versions belong to -- @column hash_a - First version hash -- @column hash_b - Second version hash -- @column similarity_score - Cosine similarity [-1, 1] -- @column distance_euclidean - L2 distance [0, \u221E) -- @column computed_at - When similarity was computed CREATE TABLE IF NOT EXISTS version_similarity_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, handle TEXT NOT NULL, hash_a TEXT NOT NULL, hash_b TEXT NOT NULL, similarity_score REAL NOT NULL, distance_euclidean REAL, computed_at TEXT NOT NULL, UNIQUE(handle, hash_a, hash_b) ); -- @index idx_vsc_handle -- @description Efficient lookup of cached similarities by handle CREATE INDEX IF NOT EXISTS idx_vsc_handle ON version_similarity_cache(handle); -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- LAYER 5: KNOWLEDGE GRAPH (GraphRAG) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- Storage for extracted knowledge graph entities and relationships. -- @table graph_entities -- @description Nodes in the knowledge graph -- @column id - Auto-increment primary key -- @column name - Entity name -- @column type - Entity type (e.g., "Person", "Organization") -- @column description - Optional description -- @column source_hash - FK to card.hash where entity was extracted -- @column embedding - Optional entity embedding -- @column created_at - When entity was created CREATE TABLE IF NOT EXISTS graph_entities ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL, description TEXT, source_hash TEXT NOT NULL, embedding BLOB, created_at TEXT NOT NULL, UNIQUE(name, type, source_hash) ); -- @index idx_entity_name -- @description Efficient entity lookup by name CREATE INDEX IF NOT EXISTS idx_entity_name ON graph_entities(name); -- @index idx_entity_type -- @description Efficient entity lookup by type CREATE INDEX IF NOT EXISTS idx_entity_type ON graph_entities(type); -- @index idx_entity_source -- @description Efficient entity lookup by source document CREATE INDEX IF NOT EXISTS idx_entity_source ON graph_entities(source_hash); -- @table graph_relationships -- @description Edges in the knowledge graph -- @column id - Auto-increment primary key -- @column source_entity_id - FK to graph_entities.id (from) -- @column target_entity_id - FK to graph_entities.id (to) -- @column relationship - Relationship type/label -- @column description - Optional description -- @column weight - Relationship strength -- @column source_hash - FK to card.hash where relationship was extracted -- @column created_at - When relationship was created CREATE TABLE IF NOT EXISTS graph_relationships ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_entity_id INTEGER NOT NULL, target_entity_id INTEGER NOT NULL, relationship TEXT NOT NULL, description TEXT, weight REAL DEFAULT 1.0, source_hash TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE(source_entity_id, target_entity_id, relationship, source_hash), FOREIGN KEY (source_entity_id) REFERENCES graph_entities(id), FOREIGN KEY (target_entity_id) REFERENCES graph_entities(id) ); -- @index idx_rel_source -- @description Efficient lookup of relationships by source entity CREATE INDEX IF NOT EXISTS idx_rel_source ON graph_relationships(source_entity_id); -- @index idx_rel_target -- @description Efficient lookup of relationships by target entity CREATE INDEX IF NOT EXISTS idx_rel_target ON graph_relationships(target_entity_id); -- @table graph_communities -- @description Hierarchical community summaries (GraphRAG) -- @column id - Auto-increment primary key -- @column level - Hierarchy level (0 = leaf) -- @column title - Community title -- @column summary - AI-generated summary -- @column embedding - Community embedding -- @column member_entity_ids - JSON array of entity IDs -- @column parent_community_id - FK to parent community -- @column created_at - When community was created CREATE TABLE IF NOT EXISTS graph_communities ( id INTEGER PRIMARY KEY AUTOINCREMENT, level INTEGER NOT NULL DEFAULT 0, title TEXT, summary TEXT NOT NULL, embedding BLOB, member_entity_ids TEXT, parent_community_id INTEGER, created_at TEXT NOT NULL, FOREIGN KEY (parent_community_id) REFERENCES graph_communities(id) ); -- @index idx_community_level -- @description Efficient lookup of communities by level CREATE INDEX IF NOT EXISTS idx_community_level ON graph_communities(level); -- @table graph_extractions -- @description Tracks which documents have been processed for entity extraction -- @column hash - FK to card.hash (primary key) -- @column entity_count - Number of entities extracted -- @column relationship_count - Number of relationships extracted -- @column extracted_at - When extraction was performed CREATE TABLE IF NOT EXISTS graph_extractions ( hash TEXT PRIMARY KEY, entity_count INTEGER DEFAULT 0, relationship_count INTEGER DEFAULT 0, extracted_at TEXT NOT NULL ); -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- SCHEMA METADATA (Vector DB) -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -- @table schema_version -- @description Tracks schema version for migrations CREATE TABLE IF NOT EXISTS vector_schema_version ( version TEXT PRIMARY KEY, applied_at TEXT NOT NULL, description TEXT ); -- Insert current schema version INSERT OR IGNORE INTO vector_schema_version (version, applied_at, description) VALUES ('1.0.0', datetime('now'), 'Initial MCard Vector Schema'); `; // src/storage/schema.ts var TABLE_LAYERS = { // Layer 1: Core "card": "core", // Layer 2: Handle System "handle_registry": "handle", "handle_history": "handle", // Layer 3: Vector Storage "mcard_vector_metadata": "vector", "mcard_embeddings": "vector", "mcard_fts": "vector", // Layer 4: Semantic Versioning "handle_version_vectors": "semantic", "version_similarity_cache": "semantic", // Layer 5: Knowledge Graph "graph_entities": "graph", "graph_relationships": "graph", "graph_communities": "graph", "graph_extractions": "graph", // Metadata "schema_version": "metadata" }; var MCardSchema = class _MCardSchema { static instance = null; schemaPath = ""; rawSql = ""; statements = []; tables = /* @__PURE__ */ new Map(); indexes = /* @__PURE__ */ new Map(); loaded = false; constructor() { } /** * Get the singleton instance. */ static getInstance() { if (!_MCardSchema.instance) { _MCardSchema.instance = new _MCardSchema(); _MCardSchema.instance.load(); } return _MCardSchema.instance; } /** * Reset the singleton (for testing). */ static resetInstance() { _MCardSchema.instance = null; } load() { if (this.loaded) return; this.schemaPath = "IN_MEMORY_CONSTANTS"; this.rawSql = MCARD_SCHEMA_SQL; if (MCARD_VECTOR_SCHEMA_SQL) { this.rawSql += "\n\n" + MCARD_VECTOR_SCHEMA_SQL; } this.statements = this.parseStatements(this.rawSql); for (const stmt of this.statements) { const name = this.extractName(stmt); if (name) { const upper = stmt.toUpperCase(); if (upper.includes("CREATE TABLE") || upper.includes("CREATE VIRTUAL TABLE")) { this.tables.set(name.toLowerCase(), stmt); } else if (upper.includes("CREATE INDEX")) { this.indexes.set(name.toLowerCase(), stmt); } } } this.loaded = true; } parseStatements(sql) { sql = sql.replace(/\/\*[\s\S]*?\*\//g, ""); const statements = []; let current = []; for (const line of sql.split("\n")) { const stripped = line.split("--")[0].trim(); if (!stripped) continue; current.push(stripped); if (stripped.endsWith(";")) { const statement = current.join(" "); if (!statement.trim().toUpperCase().startsWith("INSERT")) { statements.push(statement); } current = []; } } return statements.filter((s) => s.trim()); } extractName(statement) { let match = statement.match(/CREATE\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)/i); if (match) return match[1]; match = statement.match(/CREATE\s+INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)/i); if (match) return match[1]; return null; } // ───────────────────────────────────────────────────────────────────────── // Schema Access // ───────────────────────────────────────────────────────────────────────── getSchemaPath() { return this.schemaPath; } getTable(tableName) { return this.tables.get(tableName.toLowerCase()); } getIndex(indexName) { return this.indexes.get(indexName.toLowerCase()); } getAllTables() { return new Map(this.tables); } getAllIndexes() { return new Map(this.indexes); } getAllStatements() { return [...this.statements]; } getTablesByLayer(layer) { return Object.entries(TABLE_LAYERS).filter(([_, l]) => l === layer).map(([name, _]) => name); } getLayerStatements(layer) { const statements = []; const tables = this.getTablesByLayer(layer); for (const table of tables) { const stmt = this.getTable(table); if (stmt) statements.push(stmt); } for (const [_, stmt] of this.indexes) { const match = stmt.match(/ON\s+(\w+)/i); if (match && tables.includes(match[1].toLowerCase())) { statements.push(stmt); } } return statements; } // ───────────────────────────────────────────────────────────────────────── // Database Initialization // ───────────────────────────────────────────────────────────────────────── execStatements(db, statements) { for (const stmt of statements) { db.exec(stmt); } return statements.length; } initLayer(db, layer) { return this.execStatements(db, this.getLayerStatements(layer)); } initCoreTables(db) { return this.initLayer(db, "core"); } initHandleTables(db) { return this.initLayer(db, "handle"); } initVectorTables(db, enableFts = true) { const statements = this.getLayerStatements("vector").filter((s) => enableFts || !s.toLowerCase().includes("fts")); return this.execStatements(db, statements); } initSemanticTables(db) { return this.initLayer(db, "semantic"); } initGraphTables(db) { return this.initLayer(db, "graph"); } initAllTables(db, options = {}) { const { enableFts = true, enableGraph = true, enableSemantic = true } = options; let count = 0; count += this.initCoreTables(db); count += this.initHandleTables(db); count += this.initVectorTables(db, enableFts); if (enableSemantic) { count += this.initSemanticTables(db); } if (enableGraph) { count += this.initGraphTables(db); } return count; } initVec0Table(db, dimensions) { db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS mcard_vec USING vec0( metadata_id INTEGER PRIMARY KEY, embedding float[${dimensions}] ) `); } }; function getSchemaInstance() { return MCardSchema.getInstance(); } var CARD_TABLE_SCHEMA = getSchemaInstance().getTable("card") || ""; var HANDLE_REGISTRY_SCHEMA = getSchemaInstance().getTable("handle_registry") || ""; var HANDLE_HISTORY_SCHEMA = getSchemaInstance().getTable("handle_history") || ""; var HANDLE_INDEX_SCHEMA = getSchemaInstance().getIndex("idx_handle_current_hash") || ""; var VECTOR_METADATA_SCHEMA = getSchemaInstance().getTable("mcard_vector_metadata") || ""; var VECTOR_METADATA_INDEX = getSchemaInstance().getIndex("idx_vector_metadata_hash") || ""; var VECTOR_EMBEDDINGS_SCHEMA = getSchemaInstance().getTable("mcard_embeddings") || ""; var VECTOR_FTS_SCHEMA = getSchemaInstance().getTable("mcard_fts") || ""; var HANDLE_VERSION_VECTORS_SCHEMA = getSchemaInstance().getTable("handle_version_vectors") || ""; var HANDLE_VERSION_VECTORS_INDEXES = [ getSchemaInstance().getIndex("idx_hvv_handle"), getSchemaInstance().getIndex("idx_hvv_hash"), getSchemaInstance().getIndex("idx_hvv_current"), getSchemaInstance().getIndex("idx_hvv_parent") ].filter(Boolean).join("; "); var VERSION_SIMILARITY_CACHE_SCHEMA = getSchemaInstance().getTable("version_similarity_cache") || ""; var VERSION_SIMILARITY_CACHE_INDEX = getSchemaInstance().getIndex("idx_vsc_handle") || ""; var GRAPH_ENTITY_SCHEMA = getSchemaInstance().getTable("graph_entities") || ""; var GRAPH_ENTITY_INDEX_NAME = getSchemaInstance().getIndex("idx_entity_name") || ""; var GRAPH_ENTITY_INDEX_TYPE = getSchemaInstance().getIndex("idx_entity_type") || ""; var GRAPH_ENTITY_INDEX_SOURCE = getSchemaInstance().getIndex("idx_entity_source") || ""; var GRAPH_RELATIONSHIP_SCHEMA = getSchemaInstance().getTable("graph_relationships") || ""; var GRAPH_RELATIONSHIP_INDEX_SOURCE = getSchemaInstance().getIndex("idx_rel_source") || ""; var GRAPH_RELATIONSHIP_INDEX_TARGET = getSchemaInstance().getIndex("idx_rel_target") || ""; var GRAPH_COMMUNITY_SCHEMA = getSchemaInstance().getTable("graph_communities") || ""; var GRAPH_COMMUNITY_INDEX_LEVEL = getSchemaInstance().getIndex("idx_community_level") || ""; var GRAPH_EXTRACTION_SCHEMA = getSchemaInstance().getTable("graph_extractions") || ""; var CORE_SCHEMAS = { card: CARD_TABLE_SCHEMA, handleRegistry: HANDLE_REGISTRY_SCHEMA, handleHistory: HANDLE_HISTORY_SCHEMA, handleIndex: HANDLE_INDEX_SCHEMA }; function initCoreSchemas(db) { const schema = MCardSchema.getInstance(); schema.initCoreTables(db); schema.initHandleTables(db); } // src/storage/SqliteNodeEngine.ts import { createRequire } from "module"; var DEFAULT_PAGE_SIZE = 10; var SqliteNodeEngine = class { db; dbPath; /** * Create a new SqliteNodeEngine * @param dbPath Path to database file, or ':memory:' for in-memory database */ constructor(dbPath = ":memory:") { this.dbPath = dbPath; const require2 = createRequire(import.meta.url); const Database = require2("better-sqlite3"); this.db = new Database(dbPath); this.setupDatabase(); } /** * Initialize database schema */ setupDatabase() { initCoreSchemas(this.db); } /** * Close the database connection */ close() { this.db.close(); } /** * Get the database path */ getDbPath() { return this.dbPath; } // =========== Card Operations =========== async add(card) { const stmt = this.db.prepare( "INSERT OR REPLACE INTO card (hash, content, g_time) VALUES (?, ?, ?)" ); stmt.run(card.hash, Buffer.from(card.content), card.g_time); return card.hash; } /** * Add a card synchronously (for performance-critical paths) */ addSync(card) { const stmt = this.db.prepare( "INSERT OR REPLACE INTO card (hash, content, g_time) VALUES (?, ?, ?)" ); stmt.run(card.hash, Buffer.from(card.content), card.g_time); return card.hash; } async get(hash) { return this.getSync(hash); } /** * Get a card synchronously */ getSync(hash) { const stmt = this.db.prepare("SELECT hash, content, g_time FROM card WHERE hash = ?"); const row = stmt.get(hash); if (!row) return null; const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); } async delete(hash) { this.deleteSync(hash); } /** * Delete a card synchronously */ deleteSync(hash) { const stmt = this.db.prepare("DELETE FROM card WHERE hash = ?"); const result = stmt.run(hash); return result.changes > 0; } async getPage(pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { return this.getPageSync(pageNumber, pageSize); } /** * Get a page of cards synchronously */ getPageSync(pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { if (pageNumber < 1) throw new Error("Page number must be >= 1"); if (pageSize < 1) throw new Error("Page size must be >= 1"); const totalItems = this.countSync(); const totalPages = Math.ceil(totalItems / pageSize) || 1; const offset = (pageNumber - 1) * pageSize; const stmt = this.db.prepare( "SELECT hash, content, g_time FROM card ORDER BY g_time DESC LIMIT ? OFFSET ?" ); const rows = stmt.all(pageSize, offset); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: offset + items.length < totalItems, hasPrevious: pageNumber > 1 }; } async count() { return this.countSync(); } /** * Count cards synchronously */ countSync() { const stmt = this.db.prepare("SELECT COUNT(*) as count FROM card"); const row = stmt.get(); return row.count; } async searchByHash(hashPrefix) { const rows = this.db.prepare("SELECT hash, content, g_time FROM card WHERE hash LIKE ?").all(`${hashPrefix}%`); return rows.map( (row) => MCard.fromData(new Uint8Array(row.content), row.hash, row.g_time) ); } async search(query, pageNumber, pageSize) { const offset = (pageNumber - 1) * pageSize; const searchPattern = `%${query}%`; const countResult = this.db.prepare("SELECT COUNT(*) as count FROM card WHERE CAST(content AS TEXT) LIKE ?").get(searchPattern); const totalItems = countResult.count; const totalPages = Math.ceil(totalItems / pageSize) || 1; const rows = this.db.prepare("SELECT hash, content, g_time FROM card WHERE CAST(content AS TEXT) LIKE ? LIMIT ? OFFSET ?").all(searchPattern, pageSize, offset); const items = rows.map( (row) => MCard.fromData(new Uint8Array(row.content), row.hash, row.g_time) ); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: pageNumber < totalPages, hasPrevious: pageNumber > 1 }; } async getAll() { const rows = this.db.prepare("SELECT hash, content, g_time FROM card").all(); return rows.map( (row) => MCard.fromData(new Uint8Array(row.content), row.hash, row.g_time) ); } async clear() { this.clearSync(); } /** * Clear all data synchronously * Delete in FK-safe order: children first, then parents */ clearSync() { this.db.exec("DELETE FROM handle_history"); this.db.exec("DELETE FROM handle_registry"); this.db.exec("DELETE FROM card"); } // =========== Search Operations =========== /** * Search cards by string in content, hash, or g_time */ searchByString(searchString, pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { if (pageNumber < 1) throw new Error("Page number must be >= 1"); if (pageSize < 1) throw new Error("Page size must be >= 1"); const pattern = `%${searchString}%`; const offset = (pageNumber - 1) * pageSize; const countStmt = this.db.prepare(` SELECT COUNT(*) as count FROM card WHERE hash LIKE ? OR g_time LIKE ? OR CAST(content AS TEXT) LIKE ? `); const countRow = countStmt.get(pattern, pattern, pattern); const totalItems = countRow.count; const totalPages = Math.ceil(totalItems / pageSize) || 1; const stmt = this.db.prepare(` SELECT hash, content, g_time FROM card WHERE hash LIKE ? OR g_time LIKE ? OR CAST(content AS TEXT) LIKE ? ORDER BY g_time DESC LIMIT ? OFFSET ? `); const rows = stmt.all(pattern, pattern, pattern, pageSize, offset); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: offset + items.length < totalItems, hasPrevious: pageNumber > 1 }; } /** * Search cards by content pattern (binary-safe) */ searchByContent(searchPattern, pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { if (pageNumber < 1) throw new Error("Page number must be >= 1"); if (pageSize < 1) throw new Error("Page size must be >= 1"); if (!searchPattern || typeof searchPattern === "string" && searchPattern.length === 0) { throw new Error("Search pattern cannot be empty"); } const searchBytes = typeof searchPattern === "string" ? Buffer.from(searchPattern, "utf-8") : Buffer.from(searchPattern); const offset = (pageNumber - 1) * pageSize; const countStmt = this.db.prepare("SELECT COUNT(*) as count FROM card WHERE INSTR(content, ?) > 0"); const countRow = countStmt.get(searchBytes); const totalItems = countRow.count; const totalPages = Math.ceil(totalItems / pageSize) || 1; const stmt = this.db.prepare(` SELECT hash, content, g_time FROM card WHERE INSTR(content, ?) > 0 ORDER BY g_time DESC LIMIT ? OFFSET ? `); const rows = stmt.all(searchBytes, pageSize, offset); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: offset + items.length < totalItems, hasPrevious: pageNumber > 1 }; } /** * Get all cards (single page with all items) */ getAllCards() { const stmt = this.db.prepare("SELECT hash, content, g_time FROM card ORDER BY g_time DESC"); const rows = stmt.all(); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems: items.length, pageNumber: 1, pageSize: items.length || 1, totalPages: 1, hasNext: false, hasPrevious: false }; } // =========== Handle Operations =========== async registerHandle(handle, hash) { this.registerHandleSync(handle, hash); } /** * Register a handle synchronously */ registerHandleSync(handle, hash) { const normalized = validateHandle(handle); const now = (/* @__PURE__ */ new Date()).toISOString(); const checkStmt = this.db.prepare("SELECT handle FROM handle_registry WHERE handle = ?"); const existing = checkStmt.get(normalized); if (existing) { throw new Error(`Handle '${handle}' already exists.`); } const stmt = this.db.prepare( "INSERT INTO handle_registry (handle, current_hash, created_at, updated_at) VALUES (?, ?, ?, ?)" ); stmt.run(normalized, hash, now, now); } async resolveHandle(handle) { return this.resolveHandleSync(handle); } /** * Resolve a handle synchronously */ resolveHandleSync(handle) { const normalized = validateHandle(handle); const stmt = this.db.prepare("SELECT current_hash FROM handle_registry WHERE handle = ?"); const row = stmt.get(normalized); return row?.current_hash || null; } async getByHandle(handle) { return this.getByHandleSync(handle); } /** * Get card by handle synchronously */ getByHandleSync(handle) { const hash = this.resolveHandleSync(handle); if (!hash) return null; return this.getSync(hash); } async updateHandle(handle, newHash) { return this.updateHandleSync(handle, newHash); } /** * Update a handle synchronously */ updateHandleSync(handle, newHash) { const normalized = validateHandle(handle); const now = (/* @__PURE__ */ new Date()).toISOString(); const getStmt = this.db.prepare("SELECT current_hash FROM handle_registry WHERE handle = ?"); const row = getStmt.get(normalized); if (!row) { throw new Error(`Handle '${handle}' not found.`); } const previousHash = row.current_hash; const historyStmt = this.db.prepare( "INSERT INTO handle_history (handle, previous_hash, changed_at) VALUES (?, ?, ?)" ); historyStmt.run(normalized, previousHash, now); const updateStmt = this.db.prepare( "UPDATE handle_registry SET current_hash = ?, updated_at = ? WHERE handle = ?" ); updateStmt.run(newHash, now, normalized); return previousHash; } async getHandleHistory(handle) { return this.getHandleHistorySync(handle); } /** * Get handle history synchronously */ getHandleHistorySync(handle) { const normalized = validateHandle(handle); const stmt = this.db.prepare( "SELECT previous_hash, changed_at FROM handle_history WHERE handle = ? ORDER BY id DESC" ); const rows = stmt.all(normalized); return rows.map((row) => ({ previousHash: row.previous_hash, changedAt: row.changed_at })); } async pruneHandleHistory(handle, options = {}) { return this.pruneHandleHistorySync(handle, options); } /** * Prune handle history synchronously */ pruneHandleHistorySync(handle, options = {}) { const normalized = validateHandle(handle); let stmt; if (options.deleteAll) { stmt = this.db.prepare("DELETE FROM handle_history WHERE handle = ?"); const result = stmt.run(normalized); return result.changes; } else if (options.olderThan) { stmt = this.db.prepare("DELETE FROM handle_history WHERE handle = ? AND changed_at < ?"); const result = stmt.run(normalized, options.olderThan); return result.changes; } return 0; } }; export { HandleValidationError, validateHandle, ContentHandle, CORE_SCHEMAS, SqliteNodeEngine };