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
JavaScript
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
};