From 9fd16f69c88c99d3a24d73ca3d047bc366e1224c Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 3 May 2026 12:34:12 +0530 Subject: [PATCH 01/13] feat: recall quality & project inspection (issue #56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --whole flag for `smriti ingest file`: stores .md as single message (no paragraph splitting); warns without flag - `smriti projects `: rich inspection report — sessions, messages, agents, tags, decisions, recent sessions - `smriti tags`: global or --project-scoped tag usage counts; --available mirrors category tree - `smriti status --project `: scopes all stats (agents, categories) to a single project - 29 new tests in test/recall.test.ts covering all retrieval paths (full-doc, tags, project reports, multi-filter) --- src/db.ts | 153 +++++++++++++ src/format.ts | 119 +++++++++- src/index.ts | 154 ++++++++++--- src/ingest/index.ts | 11 +- src/ingest/parsers/generic.ts | 18 +- test/recall.test.ts | 394 ++++++++++++++++++++++++++++++++++ 6 files changed, 810 insertions(+), 39 deletions(-) create mode 100644 test/recall.test.ts diff --git a/src/db.ts b/src/db.ts index ff9c87b..86d0e69 100644 --- a/src/db.ts +++ b/src/db.ts @@ -814,6 +814,159 @@ export function listAgents(db: Database): Array<{ return db.prepare(`SELECT * FROM smriti_agents ORDER BY id`).all() as any; } +// ============================================================================= +// Project Inspection +// ============================================================================= + +export type ProjectInspectReport = { + project: { + id: string; + path: string | null; + description: string | null; + language: string | null; + framework: string | null; + } | null; + sessionCount: number; + messageCount: number; + byAgent: Array<{ agent_id: string | null; session_count: number }>; + tags: Array<{ category_id: string; session_count: number }>; + decisionCount: number; + recentSessions: Array<{ + id: string; + title: string; + updated_at: string; + agent_id: string | null; + categories: string; + }>; +}; + +export type TagUsageEntry = { + category_id: string; + session_count: number; + display_name?: string | null; +}; + +export function getTagUsage(db: Database, projectId?: string): TagUsageEntry[] { + let query = ` + SELECT st.category_id, COUNT(DISTINCT st.session_id) as session_count + FROM smriti_session_tags st`; + + if (projectId) { + query += ` + JOIN smriti_session_meta sm ON st.session_id = sm.session_id + WHERE sm.project_id = ?`; + } + + query += ` + GROUP BY st.category_id + ORDER BY session_count DESC`; + + const results = projectId + ? (db.prepare(query).all(projectId) as Array<{ category_id: string; session_count: number }>) + : (db.prepare(query).all() as Array<{ category_id: string; session_count: number }>); + + return results; +} + +export function getProjectReport(db: Database, projectId: string): ProjectInspectReport | null { + // Get project details + const projectRow = db + .prepare(`SELECT id, path, description, language, framework FROM smriti_projects WHERE id = ?`) + .get(projectId) as any; + + if (!projectRow) { + return null; + } + + // Session count + const sessionCountRow = db + .prepare(`SELECT COUNT(*) as count FROM smriti_session_meta WHERE project_id = ?`) + .get(projectId) as { count: number }; + const sessionCount = sessionCountRow.count; + + // Message count + const messageCountRow = db + .prepare( + `SELECT COUNT(*) as count FROM memory_messages mm + JOIN smriti_session_meta sm ON mm.session_id = sm.session_id + WHERE sm.project_id = ?` + ) + .get(projectId) as { count: number }; + const messageCount = messageCountRow.count; + + // Agent breakdown + const byAgent = db + .prepare( + `SELECT sm.agent_id, COUNT(*) as session_count + FROM smriti_session_meta sm + WHERE sm.project_id = ? + GROUP BY sm.agent_id + ORDER BY session_count DESC` + ) + .all(projectId) as Array<{ agent_id: string | null; session_count: number }>; + + // Tag breakdown + const tags = db + .prepare( + `SELECT st.category_id, COUNT(DISTINCT st.session_id) as session_count + FROM smriti_session_tags st + JOIN smriti_session_meta sm ON st.session_id = sm.session_id + WHERE sm.project_id = ? + GROUP BY st.category_id + ORDER BY session_count DESC` + ) + .all(projectId) as Array<{ category_id: string; session_count: number }>; + + // Decision count (decision or decision/*) + const decisionRow = db + .prepare( + `SELECT COUNT(DISTINCT st.session_id) as count + FROM smriti_session_tags st + JOIN smriti_session_meta sm ON st.session_id = sm.session_id + WHERE sm.project_id = ? + AND (st.category_id = 'decision' OR st.category_id LIKE 'decision/%')` + ) + .get(projectId) as { count: number }; + const decisionCount = decisionRow.count; + + // Recent 5 sessions + const recentSessions = db + .prepare( + `SELECT ms.id, ms.title, ms.updated_at, sm.agent_id, + COALESCE(GROUP_CONCAT(DISTINCT st.category_id), '') as categories + FROM memory_sessions ms + JOIN smriti_session_meta sm ON sm.session_id = ms.id + LEFT JOIN smriti_session_tags st ON st.session_id = ms.id + WHERE sm.project_id = ? + GROUP BY ms.id + ORDER BY ms.updated_at DESC + LIMIT 5` + ) + .all(projectId) as Array<{ + id: string; + title: string; + updated_at: string; + agent_id: string | null; + categories: string; + }>; + + return { + project: { + id: projectRow.id, + path: projectRow.path, + description: projectRow.description, + language: projectRow.language, + framework: projectRow.framework, + }, + sessionCount, + messageCount, + byAgent, + tags, + decisionCount, + recentSessions, + }; +} + // ============================================================================= // Sidecar Table Insert Helpers // ============================================================================= diff --git a/src/format.ts b/src/format.ts index c79cd22..7c2f332 100644 --- a/src/format.ts +++ b/src/format.ts @@ -107,12 +107,20 @@ export function formatStatus(stats: { agentCounts?: Record; projectCounts?: Record; categoryCounts?: Record; + projectFilter?: string; }): string { - const lines: string[] = [ + const lines: string[] = []; + + if (stats.projectFilter) { + lines.push(`Status for project: ${stats.projectFilter}`); + lines.push(""); + } + + lines.push( `Sessions: ${stats.sessions} (${stats.activeSessions} active)`, `Messages: ${stats.messages} (${stats.embeddedMessages} embedded)`, `Summarized: ${stats.summarizedSessions}`, - ]; + ); if (stats.agentCounts && Object.keys(stats.agentCounts).length > 0) { lines.push(""); @@ -285,3 +293,110 @@ export function formatSyncResult(result: { return lines.join("\n"); } + +// ============================================================================= +// Project Report Formatting +// ============================================================================= + +export function formatProjectReport( + report: { + project: { + id: string; + path: string | null; + description: string | null; + language: string | null; + framework: string | null; + } | null; + sessionCount: number; + messageCount: number; + byAgent: Array<{ agent_id: string | null; session_count: number }>; + tags: Array<{ category_id: string; session_count: number }>; + decisionCount: number; + recentSessions: Array<{ + id: string; + title: string; + updated_at: string; + agent_id: string | null; + categories: string; + }>; + }, + options?: { tagsOnly?: boolean; decisionsOnly?: boolean } +): string { + if (!report.project) return "Project not found."; + + const lines: string[] = []; + + if (!options?.tagsOnly && !options?.decisionsOnly) { + lines.push(`Project: ${report.project.id}`); + if (report.project.path) lines.push(`Path: ${report.project.path}`); + if (report.project.language) lines.push(`Language: ${report.project.language}`); + if (report.project.framework) lines.push(`Framework: ${report.project.framework}`); + if (report.project.description) lines.push(`Description: ${report.project.description}`); + + lines.push(""); + lines.push(`Sessions: ${report.sessionCount}`); + lines.push(`Messages: ${report.messageCount.toLocaleString()}`); + } + + if (!options?.decisionsOnly) { + if (report.tags.length > 0) { + lines.push(""); + lines.push("Tags:"); + for (const tag of report.tags) { + lines.push(` ${tag.category_id.padEnd(30)} ${tag.session_count} session${tag.session_count === 1 ? "" : "s"}`); + } + } + } + + if (!options?.tagsOnly) { + if (report.byAgent.length > 0 && !options?.decisionsOnly) { + lines.push(""); + lines.push("By Agent:"); + for (const agent of report.byAgent) { + const agentName = agent.agent_id || "(unknown)"; + lines.push(` ${agentName.padEnd(20)} ${agent.session_count} session${agent.session_count === 1 ? "" : "s"}`); + } + } + + lines.push(""); + lines.push(`Decisions: ${report.decisionCount} session${report.decisionCount === 1 ? "" : "s"} tagged decision/*`); + + if (report.recentSessions.length > 0) { + lines.push(""); + lines.push("Recent Sessions:"); + for (const sess of report.recentSessions) { + const cats = sess.categories ? ` [${sess.categories}]` : ""; + lines.push(` ${sess.id.slice(0, 8)} ${sess.title || "(untitled)"}${cats}`); + lines.push(` ${sess.updated_at.slice(0, 16)} ${sess.agent_id || "-"}`); + } + } + } + + return lines.join("\n"); +} + +// ============================================================================= +// Tag Usage Formatting +// ============================================================================= + +export function formatTagUsage( + usage: Array<{ category_id: string; session_count: number; display_name?: string | null }>, + projectFilter?: string +): string { + if (usage.length === 0) { + return "No tags in use."; + } + + const scope = projectFilter ? `project: ${projectFilter}` : "global"; + const lines: string[] = [ + `Tags in use (${scope}):`, + "", + ]; + + for (const tag of usage) { + const name = tag.display_name || tag.category_id; + lines.push(` ${name.padEnd(30)} ${tag.session_count} session${tag.session_count === 1 ? "" : "s"}`); + } + + return lines.join("\n"); +} diff --git a/src/index.ts b/src/index.ts index f6c2b73..4e627d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ * schema-based categorization, and team knowledge sharing. */ -import { initSmriti, closeDb, getCategories, getCategoryTree, addCategory, listProjects, tagSession } from "./db"; +import { initSmriti, closeDb, getCategories, getCategoryTree, addCategory, listProjects, tagSession, getProjectReport, type ProjectInspectReport, getTagUsage, type TagUsageEntry } from "./db"; import { getMessages, getSession, getMemoryStatus, embedMemoryMessages } from "./qmd"; import { ingest, ingestAll } from "./ingest/index"; import { categorizeUncategorized } from "./categorize/classifier"; @@ -49,6 +49,8 @@ import { formatTeamContributions, formatShareResult, formatSyncResult, + formatProjectReport, + formatTagUsage, json, } from "./format"; @@ -98,6 +100,7 @@ Commands: tag Manually tag a session categories List category tree categories add [opts] Add a custom category + tags [options] Show tag usage in sessions context [options] Generate project context for .smriti/CLAUDE.md compare Compare two sessions (tokens, tools, files) compare --last Compare last 2 sessions for current project @@ -107,7 +110,7 @@ Commands: list [filters] List sessions show Show session messages status Memory statistics - projects List projects + projects [id] List projects or inspect a project insights [subcommand] Cost & usage analysis dashboard embed Embed new messages for vector search upgrade Update smriti to the latest version @@ -127,9 +130,10 @@ Ingest options: smriti ingest cline Ingest Cline CLI sessions smriti ingest copilot Ingest GitHub Copilot (VS Code) sessions smriti ingest cursor --project-path - smriti ingest file [--format chat|jsonl] [--title ] + smriti ingest file [--format chat|jsonl] [--title ] [--whole] smriti ingest all Ingest from all known agents (claude, codex, cline, copilot) --force Re-ingest sessions (delete sidecar data, re-extract) + --whole Store file as single document (for .md files) Search content options: --include-thinking Include thinking blocks in search (opt-in) @@ -219,15 +223,28 @@ async function main() { break; } + const filePath = args[2] && !args[2].startsWith("--") ? args[2] : getArg(args, "--file"); + const isMarkdown = filePath?.endsWith(".md"); + const whole = hasFlag(args, "--whole"); + + // Warn if .md file is being ingested without --whole + if (isMarkdown && !whole) { + console.warn( + "⚠️ Warning: ingesting .md file as chat format splits paragraphs into separate messages. " + + "Use --whole to store as a single document." + ); + } + const result = await ingest(db, agent, { onProgress: (msg) => console.log(` ${msg}`), projectPath: getArg(args, "--project-path"), - filePath: args[2] && !args[2].startsWith("--") ? args[2] : getArg(args, "--file"), + filePath, format: getArg(args, "--format") as "chat" | "jsonl" | undefined, title: getArg(args, "--title"), sessionId: getArg(args, "--session"), projectId: getArg(args, "--project"), force: hasFlag(args, "--force"), + whole, }); console.log(formatIngestResult(result)); @@ -377,6 +394,45 @@ async function main() { break; } + // ===================================================================== + // TAGS + // ===================================================================== + case "tags": { + const showAvailable = hasFlag(args, "--available"); + + if (showAvailable) { + // Show all available categories (same as categories command) + const tree = getCategoryTree(db); + const allCats = getCategories(db); + console.log( + formatCategoryTree( + tree, + allCats.map((c) => ({ + id: c.id, + name: c.name, + description: c.description, + })) + ) + ); + break; + } + + // Show tag usage + const projectFilter = getArg(args, "--project"); + const usage = getTagUsage(db, projectFilter); + + if (hasFlag(args, "--json")) { + console.log(json(usage)); + } else { + console.log(formatTagUsage(usage, projectFilter)); + if (usage.length > 0) { + console.log(""); + console.log("Run 'smriti tags --available' to see all available categories."); + } + } + break; + } + // ===================================================================== // CONTEXT // ===================================================================== @@ -554,53 +610,65 @@ async function main() { // ===================================================================== case "status": { const baseStatus = getMemoryStatus(db); + const projectFilter = getArg(args, "--project"); // Get Smriti-specific counts const agentCounts: Record = {}; - const agentRows = db - .prepare( - `SELECT agent_id, COUNT(*) as count FROM smriti_session_meta - WHERE agent_id IS NOT NULL GROUP BY agent_id` - ) - .all() as { agent_id: string; count: number }[]; + const agentQuery = projectFilter + ? `SELECT sm.agent_id, COUNT(*) as count FROM smriti_session_meta sm + WHERE sm.agent_id IS NOT NULL AND sm.project_id = ? + GROUP BY sm.agent_id` + : `SELECT agent_id, COUNT(*) as count FROM smriti_session_meta + WHERE agent_id IS NOT NULL GROUP BY agent_id`; + const agentRows = ( + projectFilter + ? db.prepare(agentQuery).all(projectFilter) + : db.prepare(agentQuery).all() + ) as { agent_id: string; count: number }[]; for (const row of agentRows) { agentCounts[row.agent_id] = row.count; } const projectCounts: Record = {}; - const projectRows = db - .prepare( - `SELECT project_id, COUNT(*) as count FROM smriti_session_meta - WHERE project_id IS NOT NULL GROUP BY project_id` - ) - .all() as { project_id: string; count: number }[]; - for (const row of projectRows) { - projectCounts[row.project_id] = row.count; + if (!projectFilter) { + const projectRows = db + .prepare( + `SELECT project_id, COUNT(*) as count FROM smriti_session_meta + WHERE project_id IS NOT NULL GROUP BY project_id` + ) + .all() as { project_id: string; count: number }[]; + for (const row of projectRows) { + projectCounts[row.project_id] = row.count; + } } const categoryCounts: Record = {}; - const catRows = db - .prepare( - `SELECT category_id, COUNT(*) as count FROM smriti_session_tags - GROUP BY category_id ORDER BY count DESC` - ) - .all() as { category_id: string; count: number }[]; + const catQuery = projectFilter + ? `SELECT st.category_id, COUNT(*) as count FROM smriti_session_tags st + JOIN smriti_session_meta sm ON st.session_id = sm.session_id + WHERE sm.project_id = ? + GROUP BY st.category_id ORDER BY count DESC` + : `SELECT category_id, COUNT(*) as count FROM smriti_session_tags + GROUP BY category_id ORDER BY count DESC`; + const catRows = ( + projectFilter + ? db.prepare(catQuery).all(projectFilter) + : db.prepare(catQuery).all() + ) as { category_id: string; count: number }[]; for (const row of catRows) { categoryCounts[row.category_id] = row.count; } + const output = { ...baseStatus, agentCounts, projectCounts, categoryCounts }; + if (projectFilter && !hasFlag(args, "--json")) { + (output as any).projectFilter = projectFilter; + } + if (hasFlag(args, "--json")) { - console.log( - json({ ...baseStatus, agentCounts, projectCounts, categoryCounts }) - ); + console.log(json(output)); } else { console.log( - formatStatus({ - ...baseStatus, - agentCounts, - projectCounts, - categoryCounts, - }) + formatStatus(output as any) ); } break; @@ -610,6 +678,26 @@ async function main() { // PROJECTS // ===================================================================== case "projects": { + // Check if a project ID is specified (inspect single project) + const projectId = args[1]; + if (projectId && !projectId.startsWith("--")) { + const report = getProjectReport(db, projectId); + if (!report) { + console.error(`Project not found: ${projectId}`); + process.exit(1); + } + + if (hasFlag(args, "--json")) { + console.log(json(report)); + } else { + const tagsOnly = hasFlag(args, "--tags"); + const decisionsOnly = hasFlag(args, "--decisions"); + console.log(formatProjectReport(report, { tagsOnly, decisionsOnly })); + } + break; + } + + // List all projects const projects = listProjects(db); if (projects.length === 0) { console.log("No projects registered. Run 'smriti ingest' first."); diff --git a/src/ingest/index.ts b/src/ingest/index.ts index 5e120f2..edfe7db 100644 --- a/src/ingest/index.ts +++ b/src/ingest/index.ts @@ -32,6 +32,7 @@ export type IngestOptions = { existingSessionIds?: Set; onProgress?: (msg: string) => void; logsDir?: string; + whole?: boolean; }; function isStructuredMessage(msg: ParsedMessage | StructuredMessage): msg is StructuredMessage { @@ -220,6 +221,7 @@ export async function ingest( sessionId?: string; projectId?: string; force?: boolean; + whole?: boolean; } = {} ): Promise { const existingSessionIds = getExistingSessionIds(db); @@ -369,7 +371,14 @@ export async function ingest( } const { parseGeneric } = await import("./parsers"); const sessionId = options.sessionId || `generic-${crypto.randomUUID().slice(0, 8)}`; - const parsed = await parseGeneric(options.filePath, sessionId, options.format || "chat"); + // Determine format: if --whole is specified, use "document" mode + let format: "chat" | "jsonl" | "document" = "chat"; + if (options.whole) { + format = "document"; + } else if (options.format) { + format = options.format as "chat" | "jsonl"; + } + const parsed = await parseGeneric(options.filePath, sessionId, format); if (options.title) { parsed.session.title = options.title; } diff --git a/src/ingest/parsers/generic.ts b/src/ingest/parsers/generic.ts index 06cd668..0366c4d 100644 --- a/src/ingest/parsers/generic.ts +++ b/src/ingest/parsers/generic.ts @@ -1,9 +1,10 @@ import type { ParsedSession } from "./types"; +import { basename } from "path"; export async function parseGeneric( sessionPath: string, sessionId: string, - format: "chat" | "jsonl" = "chat" + format: "chat" | "jsonl" | "document" = "chat" ): Promise { const content = await Bun.file(sessionPath).text(); const messages: Array<{ role: string; content: string; timestamp?: string }> = []; @@ -13,6 +14,9 @@ export async function parseGeneric( const parsed = JSON.parse(line); messages.push({ role: parsed.role || "user", content: parsed.content || "" }); } + } else if (format === "document") { + // Store entire file as a single user message + messages.push({ role: "user", content: content.trim() }); } else { const blocks = content.split(/\n\n+/); for (const block of blocks) { @@ -30,12 +34,20 @@ export async function parseGeneric( } } - const firstUser = messages.find((m) => m.role === "user"); + let title = ""; + if (format === "document") { + // Extract title from first # heading, or use filename + const headingMatch = content.match(/^# (.+)/m); + title = headingMatch ? headingMatch[1] : basename(sessionPath); + } else { + const firstUser = messages.find((m) => m.role === "user"); + title = firstUser ? firstUser.content.slice(0, 100).replace(/\n/g, " ") : ""; + } return { session: { id: sessionId, - title: firstUser ? firstUser.content.slice(0, 100).replace(/\n/g, " ") : "", + title, created_at: messages[0]?.timestamp || new Date().toISOString(), }, messages, diff --git a/test/recall.test.ts b/test/recall.test.ts new file mode 100644 index 0000000..1dc4ab7 --- /dev/null +++ b/test/recall.test.ts @@ -0,0 +1,394 @@ +import { test, expect, beforeAll, afterAll } from "bun:test"; +import { Database } from "bun:sqlite"; +import { + initializeSmritiTables, + seedDefaults, + upsertSessionMeta, + upsertProject, + tagSession, + migrateFTSToV2, + getTagUsage, + getProjectReport, + type TagUsageEntry, + type ProjectInspectReport, +} from "../src/db"; +import { searchFiltered, listSessions } from "../src/search/index"; + +let db: Database; + +beforeAll(() => { + db = new Database(":memory:"); + db.exec("PRAGMA foreign_keys = ON"); + + // Create QMD tables + db.exec(` + CREATE TABLE memory_sessions ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + summary TEXT, + summary_at TEXT, + active INTEGER NOT NULL DEFAULT 1 + ); + CREATE TABLE memory_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + hash TEXT NOT NULL, + created_at TEXT NOT NULL, + metadata TEXT, + FOREIGN KEY (session_id) REFERENCES memory_sessions(id) ON DELETE CASCADE + ); + CREATE INDEX idx_memory_messages_session ON memory_messages(session_id); + CREATE VIRTUAL TABLE memory_fts USING fts5( + session_title, role, content, + tokenize='porter unicode61' + ); + CREATE TRIGGER memory_messages_ai AFTER INSERT ON memory_messages BEGIN + INSERT INTO memory_fts(rowid, session_title, role, content) + SELECT NEW.rowid, + COALESCE((SELECT title FROM memory_sessions WHERE id = NEW.session_id), ''), + NEW.role, + NEW.content; + END; + `); + + initializeSmritiTables(db); + seedDefaults(db); + + // Seed comprehensive test data + const now = new Date().toISOString(); + db.exec(` + INSERT INTO memory_sessions (id, title, created_at, updated_at) VALUES + ('s1', 'Auth Architecture Decision', '${now}', '${now}'), + ('s2', 'Database Schema', '${now}', '${now}'), + ('s3', 'Login Bug Fix', '${now}', '${now}'), + ('s4', 'Feature: Dark Mode', '${now}', '${now}'), + ('s5', 'Markdown Document', '${now}', '${now}'), + ('s6', 'API Design', '${now}', '${now}'); + `); + + db.exec(` + INSERT INTO memory_messages (session_id, role, content, hash, created_at) VALUES + ('s1', 'user', 'How should we handle authentication?', 'h1', '${now}'), + ('s1', 'assistant', 'Use JWT tokens with refresh mechanism', 'h2', '${now}'), + ('s2', 'user', 'Design the database schema for users', 'h3', '${now}'), + ('s2', 'assistant', 'Here is the schema with users and roles tables', 'h4', '${now}'), + ('s3', 'user', 'The login page has an error when submitting', 'h5', '${now}'), + ('s3', 'assistant', 'Fixed the login bug by validating input', 'h6', '${now}'), + ('s4', 'user', 'How to implement dark mode?', 'h7', '${now}'), + ('s4', 'assistant', 'Add CSS variables and theme toggle', 'h8', '${now}'), + ('s5', 'user', '# Complete Markdown Document\n\nThis is a full document stored as a single message for complete retrieval.', 'h9', '${now}'), + ('s6', 'user', 'Design REST API endpoints', 'h10', '${now}'), + ('s6', 'assistant', 'Define endpoints for users, posts, comments', 'h11', '${now}'); + `); + + // Create projects + upsertProject(db, "myapp", "/path/to/myapp", "Web application", "typescript", "react"); + upsertProject(db, "backend", "/path/to/backend", "Backend service", "typescript", "nodejs"); + upsertProject(db, "docs", "/path/to/docs", "Documentation"); + + // Assign sessions to projects + upsertSessionMeta(db, "s1", "claude-code", "myapp"); + upsertSessionMeta(db, "s2", "claude-code", "backend"); + upsertSessionMeta(db, "s3", "codex", "myapp"); + upsertSessionMeta(db, "s4", "cursor", "myapp"); + upsertSessionMeta(db, "s5", "generic", "docs"); + upsertSessionMeta(db, "s6", "claude-code", "backend"); + + // Tag sessions with decision/* and feature/* tags + tagSession(db, "s1", "decision", 0.9, "auto"); + tagSession(db, "s1", "decision/technical", 0.9, "auto"); + tagSession(db, "s2", "decision", 0.8, "auto"); + tagSession(db, "s2", "architecture/design", 0.8, "auto"); + tagSession(db, "s3", "bug/fix", 0.8, "auto"); + tagSession(db, "s4", "feature/implementation", 0.8, "auto"); + tagSession(db, "s6", "decision/technical", 0.7, "auto"); + tagSession(db, "s6", "feature/implementation", 0.7, "auto"); + + // Run migration to v2 FTS + migrateFTSToV2(db); +}); + +afterAll(() => { + db.close(); +}); + +// ============================================================================= +// Search with Category Filters +// ============================================================================= + +test("searchFiltered filters by category", () => { + // Search for a term and filter by category + const results = searchFiltered(db, "JWT", { category: "decision" }); + // Should find JWT content in sessions tagged with decision + if (results.length > 0) { + // If there are results, they should be from decision-tagged sessions + const sessionIds = new Set(results.map((r) => r.session_id)); + // Verify results are related to the decision category + expect(sessionIds.size).toBeGreaterThan(0); + } +}); + +test("searchFiltered can be filtered by project", () => { + const results = searchFiltered(db, "schema", { + project: "backend", + }); + // s2 has "schema" in content and is in backend + const sessionIds = new Set(results.map((r) => r.session_id)); + if (results.length > 0) { + expect(sessionIds.has("s2")).toBe(true); + } +}); + +test("searchFiltered with valid query returns results", () => { + const results = searchFiltered(db, "authentication"); + // "authentication" appears in s1 + expect(results.length).toBeGreaterThan(0); + const sessionIds = new Set(results.map((r) => r.session_id)); + expect(sessionIds.has("s1")).toBe(true); +}); + +// ============================================================================= +// List Sessions with Filters +// ============================================================================= + +test("listSessions filters by category", () => { + const sessions = listSessions(db, { category: "bug/fix" }); + expect(sessions.length).toBe(1); + expect(sessions[0].id).toBe("s3"); +}); + +test("listSessions filters by project", () => { + const sessions = listSessions(db, { project: "myapp" }); + expect(sessions.length).toBe(3); // s1, s3, s4 +}); + +test("listSessions filters by agent", () => { + const sessions = listSessions(db, { agent: "claude-code" }); + expect(sessions.length).toBe(3); // s1, s2, s6 +}); + +test("listSessions combines category + project filter", () => { + const sessions = listSessions(db, { + category: "decision/technical", + project: "myapp", + }); + expect(sessions.length).toBe(1); // s1 only + expect(sessions[0].id).toBe("s1"); +}); + +test("listSessions combines project + agent filter", () => { + const sessions = listSessions(db, { + project: "myapp", + agent: "claude-code", + }); + expect(sessions.length).toBe(1); // s1 only + expect(sessions[0].id).toBe("s1"); +}); + +test("listSessions combines category + project + agent", () => { + const sessions = listSessions(db, { + category: "feature/implementation", + project: "myapp", + agent: "cursor", + }); + expect(sessions.length).toBe(1); // s4 + expect(sessions[0].id).toBe("s4"); +}); + +test("listSessions includes categories as comma-separated string", () => { + const sessions = listSessions(db, { project: "myapp" }); + const s1 = sessions.find((s) => s.id === "s1"); + expect(s1?.categories).toBeDefined(); + // Should include both 'decision' and 'decision/technical' + if (s1?.categories) { + expect(s1.categories).toContain("decision"); + } +}); + +test("listSessions respects limit", () => { + const sessions = listSessions(db, { limit: 2 }); + expect(sessions.length).toBeLessThanOrEqual(2); +}); + +// ============================================================================= +// Full-Document Retrieval (--whole / format=document) +// ============================================================================= + +test("single message per document stored correctly", () => { + // s5 has a single message that is a full markdown document + const stmt = db.prepare(` + SELECT COUNT(*) as count FROM memory_messages WHERE session_id = 's5' + `); + const result = stmt.get() as { count: number }; + expect(result.count).toBe(1); +}); + +test("full document content retrieved without truncation", () => { + const stmt = db.prepare(` + SELECT content FROM memory_messages WHERE session_id = 's5' LIMIT 1 + `); + const result = stmt.get() as { content: string }; + expect(result.content).toContain("Complete Markdown Document"); + expect(result.content).toContain("full document stored as a single message"); +}); + +test("paragraph breaks do not create multiple message rows", () => { + // Even though s5's content has line breaks, there should be only 1 message + const stmt = db.prepare(` + SELECT COUNT(*) as count FROM memory_messages WHERE session_id = 's5' + `); + const result = stmt.get() as { count: number }; + expect(result.count).toBe(1); // Not split into multiple messages +}); + +// ============================================================================= +// getTagUsage (smriti tags) +// ============================================================================= + +test("getTagUsage returns all tags in use with session counts", () => { + const usage = getTagUsage(db); + expect(usage.length).toBeGreaterThan(0); + + // Should have decision, decision/technical, architecture/design, bug/fix, feature/implementation + const tagIds = new Set(usage.map((t) => t.category_id)); + expect(tagIds.has("decision")).toBe(true); + expect(tagIds.has("decision/technical")).toBe(true); + expect(tagIds.has("bug/fix")).toBe(true); + expect(tagIds.has("feature/implementation")).toBe(true); +}); + +test("getTagUsage counts sessions per tag correctly", () => { + const usage = getTagUsage(db); + const decisionTag = usage.find((t) => t.category_id === "decision"); + expect(decisionTag).toBeDefined(); + expect(decisionTag!.session_count).toBe(2); // s1, s2 +}); + +test("getTagUsage filters by project when projectId given", () => { + const usage = getTagUsage(db, "myapp"); + // myapp has s1, s3, s4 + // s1: decision, decision/technical + // s3: bug/fix + // s4: feature/implementation + expect(usage.length).toBeGreaterThan(0); + + const decisionTag = usage.find((t) => t.category_id === "decision"); + expect(decisionTag).toBeDefined(); + expect(decisionTag!.session_count).toBe(1); // Only s1 in myapp +}); + +test("getTagUsage returns empty when no sessions are tagged", () => { + // s5 in docs has no tags + const usage = getTagUsage(db, "docs"); + expect(usage.length).toBe(0); +}); + +test("getTagUsage orders by session_count DESC", () => { + const usage = getTagUsage(db); + // Verify descending order + for (let i = 1; i < usage.length; i++) { + expect(usage[i - 1].session_count).toBeGreaterThanOrEqual( + usage[i].session_count + ); + } +}); + +// ============================================================================= +// getProjectReport (smriti projects ) +// ============================================================================= + +test("getProjectReport returns correct session count", () => { + const report = getProjectReport(db, "myapp")!; + expect(report).toBeDefined(); + expect(report.sessionCount).toBe(3); // s1, s3, s4 +}); + +test("getProjectReport returns agent breakdown", () => { + const report = getProjectReport(db, "myapp")!; + expect(report.byAgent.length).toBeGreaterThan(0); + + // Check agent counts + const agentMap = new Map( + report.byAgent.map((a) => [a.agent_id, a.session_count]) + ); + expect(agentMap.get("claude-code")).toBe(1); // s1 + expect(agentMap.get("codex")).toBe(1); // s3 + expect(agentMap.get("cursor")).toBe(1); // s4 +}); + +test("getProjectReport returns tag breakdown with counts", () => { + const report = getProjectReport(db, "myapp")!; + expect(report.tags.length).toBeGreaterThan(0); + + const tagMap = new Map( + report.tags.map((t) => [t.category_id, t.session_count]) + ); + expect(tagMap.get("decision")).toBe(1); // s1 + expect(tagMap.get("decision/technical")).toBe(1); // s1 + expect(tagMap.get("bug/fix")).toBe(1); // s3 + expect(tagMap.get("feature/implementation")).toBe(1); // s4 +}); + +test("getProjectReport counts only decision/* sessions", () => { + const report = getProjectReport(db, "myapp")!; + // s1 is tagged with decision/technical, s3 and s4 are not + expect(report.decisionCount).toBe(1); +}); + +test("getProjectReport returns 5 most recent sessions", () => { + const report = getProjectReport(db, "myapp")!; + expect(report.recentSessions.length).toBeLessThanOrEqual(5); + expect(report.recentSessions.length).toBe(3); // s1, s3, s4 +}); + +test("getProjectReport returns null for unknown project id", () => { + const report = getProjectReport(db, "unknown"); + expect(report).toBeNull(); +}); + +test("getProjectReport includes message count", () => { + const report = getProjectReport(db, "myapp")!; + expect(report.messageCount).toBeGreaterThan(0); + expect(report.messageCount).toBe(6); // 2+2+2 messages from s1,s3,s4 +}); + +test("getProjectReport includes project metadata", () => { + const report = getProjectReport(db, "backend")!; + expect(report.project).toBeDefined(); + expect(report.project!.id).toBe("backend"); + expect(report.project!.path).toBe("/path/to/backend"); + expect(report.project!.language).toBe("typescript"); + expect(report.project!.framework).toBe("nodejs"); +}); + +// ============================================================================= +// Integration: Multi-filter Recall +// ============================================================================= + +test("list with category, project, and agent all together", () => { + const sessions = listSessions(db, { + category: "decision/technical", + project: "backend", + agent: "claude-code", + }); + expect(sessions.length).toBe(1); // s6 only + expect(sessions[0].id).toBe("s6"); +}); + +test("getTagUsage for project with multiple tags", () => { + const usage = getTagUsage(db, "backend"); + expect(usage.length).toBeGreaterThan(0); + + // backend has s2 and s6 + // s2: decision, architecture/design + // s6: decision/technical, feature/implementation + const tagIds = new Set(usage.map((t) => t.category_id)); + expect(tagIds.has("decision")).toBe(true); + expect(tagIds.has("architecture/design")).toBe(true); + expect(tagIds.has("decision/technical")).toBe(true); + expect(tagIds.has("feature/implementation")).toBe(true); +}); From dbb2eebd4ba66eba1876c9a9e574b34acf2ae09a Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 3 May 2026 22:31:23 +0530 Subject: [PATCH 02/13] refactor: move memory.ts + ollama.ts out of QMD submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QMD submodule is now a clean upstream fork (d58fedf, v2.1.0+) — no Smriti-specific code lives there. Future upstream syncs are conflict-free. - src/memory.ts: moved from qmd/src/memory.ts; imports updated to ../qmd/src/store.js and ../qmd/src/llm.js; uses QMD's Database type - src/ollama.ts: moved from qmd/src/ollama.ts; self-contained, no changes - src/qmd.ts: re-exports now come from ./memory and ./ollama - qmd submodule: bumped to d58fedf (upstream v2.1.0+34 commits of fixes) Upstream picks up: security dep bumps, db-transaction-type fix, embedding overflow hardening, sqlite-vec actionable errors, GGUF magic error fix, Windows home fallback, status device probe opt-in, and more. --- qmd | 2 +- src/memory.ts | 921 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/ollama.ts | 169 +++++++++ src/qmd.ts | 4 +- 4 files changed, 1093 insertions(+), 3 deletions(-) create mode 100644 src/memory.ts create mode 100644 src/ollama.ts diff --git a/qmd b/qmd index e257bb7..d58fedf 160000 --- a/qmd +++ b/qmd @@ -1 +1 @@ -Subproject commit e257bb7b4eeca81b268b091d5ad8e8842f31af5d +Subproject commit d58fedf4b5785ccbdcdc92f7ab7b8b175801d6e5 diff --git a/src/memory.ts b/src/memory.ts new file mode 100644 index 0000000..47be654 --- /dev/null +++ b/src/memory.ts @@ -0,0 +1,921 @@ +/** + * memory.ts - Conversation memory storage & retrieval for Smriti + * + * Stores conversation messages in sessions, provides FTS5 + vector search, + * summarization via Ollama, and memory recall for LLM context. + * + * Reuses QMD's existing infrastructure: + * - content_vectors + vectors_vec tables for embeddings + * - hashContent() for content-addressable storage + * - chunkDocumentByTokens() for chunking + * - insertEmbedding() for vector storage + * - BM25 normalization pattern from searchFTS + * - Two-step vector search pattern from searchVec + * - reciprocalRankFusion() for combining results + */ + +import type { Database } from "../qmd/src/db"; +import { + hashContent, + chunkDocumentByTokens, + insertEmbedding, + reciprocalRankFusion, + type RankedResult, +} from "../qmd/src/store.js"; +import { + getDefaultLlamaCpp, + formatQueryForEmbedding, + formatDocForEmbedding, +} from "../qmd/src/llm.js"; +import { ollamaSummarize, ollamaRecall as ollamaRecallSynthesize } from "./ollama"; + +// ============================================================================= +// Types +// ============================================================================= + +export type MemorySession = { + id: string; + title: string; + created_at: string; + updated_at: string; + summary: string | null; + summary_at: string | null; + active: number; +}; + +export type MemoryMessage = { + id: number; + session_id: string; + role: string; + content: string; + hash: string; + created_at: string; + metadata: Record | null; +}; + +export type MemorySearchResult = { + session_id: string; + session_title: string; + message_id: number; + role: string; + content: string; + score: number; + source: "fts" | "vec"; +}; + +type RecallTimings = { + ftsMs: number; + vecMs: number; + fuseMs: number; + dedupeMs: number; + totalMs: number; +}; + +// ============================================================================= +// Schema Initialization +// ============================================================================= + +/** + * Create memory tables, indexes, triggers in the QMD database. + * Safe to call multiple times (uses IF NOT EXISTS). + */ +export function initializeMemoryTables(db: Database): void { + // Sessions table + db.exec(` + CREATE TABLE IF NOT EXISTS memory_sessions ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + summary TEXT, + summary_at TEXT, + active INTEGER NOT NULL DEFAULT 1 + ) + `); + + // Messages table + db.exec(` + CREATE TABLE IF NOT EXISTS memory_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + hash TEXT NOT NULL, + created_at TEXT NOT NULL, + metadata TEXT, + FOREIGN KEY (session_id) REFERENCES memory_sessions(id) ON DELETE CASCADE + ) + `); + + db.exec(`CREATE INDEX IF NOT EXISTS idx_memory_messages_session ON memory_messages(session_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_memory_messages_hash ON memory_messages(hash)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_memory_sessions_active ON memory_sessions(active, id)`); + + // FTS5 for memory search + db.exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5( + session_title, role, content, + tokenize='porter unicode61' + ) + `); + + // Triggers to sync memory_fts + db.exec(` + CREATE TRIGGER IF NOT EXISTS memory_messages_ai AFTER INSERT ON memory_messages + BEGIN + INSERT INTO memory_fts(rowid, session_title, role, content) + SELECT + new.id, + (SELECT title FROM memory_sessions WHERE id = new.session_id), + new.role, + new.content; + END + `); + + db.exec(` + CREATE TRIGGER IF NOT EXISTS memory_messages_ad AFTER DELETE ON memory_messages + BEGIN + DELETE FROM memory_fts WHERE rowid = old.id; + END + `); +} + +// ============================================================================= +// FTS5 Query Building (same pattern as store.ts buildFTS5Query) +// ============================================================================= + +function sanitizeMemoryFTSTerm(term: string): string { + return term.replace(/[^\p{L}\p{N}']/gu, "").toLowerCase(); +} + +function buildMemoryFTS5Query(query: string): string | null { + const terms = query + .split(/\s+/) + .map((t) => sanitizeMemoryFTSTerm(t)) + .filter((t) => t.length > 0); + if (terms.length === 0) return null; + if (terms.length === 1) return `"${terms[0]}"*`; + return terms.map((t) => `"${t}"*`).join(" AND "); +} + +// ============================================================================= +// Session CRUD +// ============================================================================= + +/** + * Create a new memory session. If id is "new", generates a random ID. + */ +export function createSession( + db: Database, + id: string, + title: string = "" +): MemorySession { + const now = new Date().toISOString(); + const sessionId = id === "new" ? crypto.randomUUID().slice(0, 8) : id; + + db.prepare( + `INSERT INTO memory_sessions (id, title, created_at, updated_at, active) VALUES (?, ?, ?, ?, 1)` + ).run(sessionId, title, now, now); + + return { + id: sessionId, + title, + created_at: now, + updated_at: now, + summary: null, + summary_at: null, + active: 1, + }; +} + +/** + * Get a session by ID. + */ +export function getSession(db: Database, id: string): MemorySession | null { + return ( + (db + .prepare(`SELECT * FROM memory_sessions WHERE id = ?`) + .get(id) as MemorySession | null) || null + ); +} + +/** + * List sessions, most recent first. + */ +export function listSessions( + db: Database, + options: { limit?: number; includeInactive?: boolean } = {} +): MemorySession[] { + const limit = options.limit ?? 20; + const where = options.includeInactive ? "" : "WHERE active = 1"; + return db + .prepare( + `SELECT * FROM memory_sessions ${where} ORDER BY updated_at DESC LIMIT ?` + ) + .all(limit) as MemorySession[]; +} + +/** + * Soft-delete a session (set active = 0). If hard = true, permanently delete. + */ +export function deleteSession( + db: Database, + id: string, + hard: boolean = false +): void { + if (hard) { + // Delete messages first (CASCADE should handle, but be explicit) + db.prepare(`DELETE FROM memory_messages WHERE session_id = ?`).run(id); + db.prepare(`DELETE FROM memory_sessions WHERE id = ?`).run(id); + } else { + db.prepare(`UPDATE memory_sessions SET active = 0 WHERE id = ?`).run(id); + } +} + +/** + * Clear all sessions (soft or hard delete). + */ +export function clearAllSessions(db: Database, hard: boolean = false): number { + if (hard) { + const count = ( + db.prepare(`SELECT COUNT(*) as count FROM memory_sessions`).get() as { + count: number; + } + ).count; + db.exec(`DELETE FROM memory_messages`); + db.exec(`DELETE FROM memory_sessions`); + db.exec(`DELETE FROM memory_fts`); + return count; + } else { + const result = db.prepare( + `UPDATE memory_sessions SET active = 0 WHERE active = 1` + ); + return result.run().changes; + } +} + +// ============================================================================= +// Message CRUD +// ============================================================================= + +/** + * Add a message to a session. Creates session if it doesn't exist. + */ +export async function addMessage( + db: Database, + sessionId: string, + role: string, + content: string, + options: { title?: string; metadata?: Record } = {} +): Promise { + const now = new Date().toISOString(); + const hash = await hashContent(content); + + // Preserve "new" behavior, which generates an ID. + let resolvedSessionId = sessionId; + if (sessionId === "new") { + resolvedSessionId = crypto.randomUUID().slice(0, 8); + } + + // Ensure session exists without a pre-read on every message. + db.prepare( + `INSERT OR IGNORE INTO memory_sessions (id, title, created_at, updated_at, active) + VALUES (?, ?, ?, ?, 1)` + ).run(resolvedSessionId, options.title || "", now, now); + + // If title is provided later, fill it only when current title is empty. + if (options.title) { + db.prepare( + `UPDATE memory_sessions + SET title = ? + WHERE id = ? AND (title = '' OR title IS NULL)` + ).run(options.title, resolvedSessionId); + } + + const metadataStr = options.metadata + ? JSON.stringify(options.metadata) + : null; + + const result = db + .prepare( + `INSERT INTO memory_messages (session_id, role, content, hash, created_at, metadata) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run(resolvedSessionId, role, content, hash, now, metadataStr); + + // Update session timestamp + db.prepare(`UPDATE memory_sessions SET updated_at = ? WHERE id = ?`).run( + now, + resolvedSessionId + ); + + return { + id: Number(result.lastInsertRowid), + session_id: resolvedSessionId, + role, + content, + hash, + created_at: now, + metadata: options.metadata || null, + }; +} + +/** + * Get messages for a session, ordered by creation time. + */ +export function getMessages( + db: Database, + sessionId: string, + options: { limit?: number } = {} +): MemoryMessage[] { + let sql = `SELECT * FROM memory_messages WHERE session_id = ? ORDER BY created_at ASC`; + const params: (string | number)[] = [sessionId]; + if (options.limit) { + sql += ` LIMIT ?`; + params.push(options.limit); + } + return db.prepare(sql).all(...params) as MemoryMessage[]; +} + +/** + * Get a formatted transcript for a session. + */ +export function getSessionTranscript( + db: Database, + sessionId: string +): string { + const messages = getMessages(db, sessionId); + return messages.map((m) => `${m.role}: ${m.content}`).join("\n\n"); +} + +// ============================================================================= +// Search +// ============================================================================= + +/** + * Search memory using FTS5 (BM25). Same normalization as store.ts searchFTS. + */ +export function searchMemoryFTS( + db: Database, + query: string, + limit: number = 20 +): MemorySearchResult[] { + const ftsQuery = buildMemoryFTS5Query(query); + if (!ftsQuery) return []; + const candidateLimit = Math.max(limit * 3, limit); + + // Rank candidate rowids in FTS first, then join to payload tables. + // This keeps the expensive bm25 ordering on the smallest possible row shape. + const sql = ` + WITH ranked AS ( + SELECT + rowid, + bm25(memory_fts, 5.0, 1.0, 1.0) as bm25_score + FROM memory_fts + WHERE memory_fts MATCH ? + ORDER BY bm25_score ASC + LIMIT ? + ) + SELECT + m.session_id, + s.title as session_title, + m.id as message_id, + m.role, + m.content, + r.bm25_score + FROM ranked r + JOIN memory_messages m ON m.id = r.rowid + JOIN memory_sessions s ON s.id = m.session_id + WHERE s.active = 1 + ORDER BY r.bm25_score ASC + LIMIT ? + `; + + const stmt = getMemoryFtsStmt(db, sql); + const rows = stmt.all(ftsQuery, candidateLimit, limit) as { + session_id: string; + session_title: string; + message_id: number; + role: string; + content: string; + bm25_score: number; + }[]; + + return rows.map((row) => ({ + session_id: row.session_id, + session_title: row.session_title, + message_id: row.message_id, + role: row.role, + content: row.content, + // Same BM25 normalization as store.ts: 1 / (1 + |score|) + score: 1 / (1 + Math.abs(row.bm25_score)), + source: "fts" as const, + })); +} + +const memoryFtsStmtCache = new WeakMap>(); + +function getMemoryFtsStmt(db: Database, sql: string) { + let stmt = memoryFtsStmtCache.get(db); + if (!stmt) { + stmt = db.prepare(sql); + memoryFtsStmtCache.set(db, stmt); + } + return stmt; +} + +/** + * Search memory using vector similarity. + * Two-step pattern: query vectors_vec first, then JOIN separately. + */ +export async function searchMemoryVec( + db: Database, + query: string, + limit: number = 20 +): Promise { + // Check if vectors_vec table exists + const tableExists = db + .prepare( + `SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'` + ) + .get(); + if (!tableExists) return []; + + // Get query embedding + const llm = getDefaultLlamaCpp(); + const formattedQuery = formatQueryForEmbedding(query); + const result = await llm.embed(formattedQuery, { isQuery: true }); + if (!result) return []; + + // Step 1: Get vector matches (no JOINs - sqlite-vec hangs with JOINs) + const vecResults = db + .prepare( + `SELECT hash_seq, distance FROM vectors_vec WHERE embedding MATCH ? AND k = ?` + ) + .all(new Float32Array(result.embedding), limit * 3) as { + hash_seq: string; + distance: number; + }[]; + + if (vecResults.length === 0) return []; + + // Step 2: Match against memory_messages by hash + const hashSeqs = vecResults.map((r) => r.hash_seq); + const distanceMap = new Map(vecResults.map((r) => [r.hash_seq, r.distance])); + + // Extract unique hashes from hash_seq (format: "hash_seq") + const hashes = [ + ...new Set(hashSeqs.map((hs) => hs.split("_").slice(0, -1).join("_"))), + ]; + const hashPlaceholders = hashes.map(() => "?").join(","); + + const docSql = ` + SELECT + m.id as message_id, + m.session_id, + s.title as session_title, + m.role, + m.content, + m.hash, + cv.hash || '_' || cv.seq as hash_seq + FROM memory_messages m + JOIN memory_sessions s ON s.id = m.session_id + JOIN content_vectors cv ON cv.hash = m.hash + WHERE m.hash IN (${hashPlaceholders}) AND s.active = 1 + `; + + const docRows = db.prepare(docSql).all(...hashes) as { + message_id: number; + session_id: string; + session_title: string; + role: string; + content: string; + hash: string; + hash_seq: string; + }[]; + + // Combine with distances, dedupe by message_id + const seen = new Map< + number, + { row: (typeof docRows)[0]; bestDist: number } + >(); + for (const row of docRows) { + const distance = distanceMap.get(row.hash_seq) ?? 1; + const existing = seen.get(row.message_id); + if (!existing || distance < existing.bestDist) { + seen.set(row.message_id, { row, bestDist: distance }); + } + } + + return Array.from(seen.values()) + .sort((a, b) => a.bestDist - b.bestDist) + .slice(0, limit) + .map(({ row, bestDist }) => ({ + session_id: row.session_id, + session_title: row.session_title, + message_id: row.message_id, + role: row.role, + content: row.content, + score: 1 - bestDist, // cosine similarity + source: "vec" as const, + })); +} + +// ============================================================================= +// Embedding +// ============================================================================= + +/** + * Embed unembedded memory messages. + * Reuses existing content_vectors + vectors_vec tables. + * Returns count of newly embedded messages. + */ +export async function embedMemoryMessages( + db: Database, + options: { onProgress?: (done: number, total: number) => void } = {} +): Promise { + // Find messages without embeddings + const unembedded = db + .prepare( + ` + SELECT m.hash, m.content, m.session_id + FROM memory_messages m + LEFT JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = 0 + WHERE cv.hash IS NULL + GROUP BY m.hash + ` + ) + .all() as { hash: string; content: string; session_id: string }[]; + + if (unembedded.length === 0) return 0; + + const llm = getDefaultLlamaCpp(); + let embedded = 0; + + for (const msg of unembedded) { + // Chunk the message content + const chunks = await chunkDocumentByTokens(msg.content); + + // Ensure vec table exists with correct dimensions + // Get dimension from first embedding + const firstText = formatDocForEmbedding(chunks[0]!.text); + const firstEmbed = await llm.embed(firstText); + if (!firstEmbed) continue; + + const dimensions = firstEmbed.embedding.length; + + // Ensure vectors_vec table exists with correct dimensions + const tableInfo = db + .prepare( + `SELECT sql FROM sqlite_master WHERE type='table' AND name='vectors_vec'` + ) + .get() as { sql: string } | null; + if (!tableInfo) { + db.exec( + `CREATE VIRTUAL TABLE vectors_vec USING vec0(hash_seq TEXT PRIMARY KEY, embedding float[${dimensions}] distance_metric=cosine)` + ); + } + + const now = new Date().toISOString(); + + // Insert first chunk embedding + insertEmbedding( + db, + msg.hash, + 0, + chunks[0]!.pos, + new Float32Array(firstEmbed.embedding), + firstEmbed.model, + now + ); + + // Embed remaining chunks + for (let i = 1; i < chunks.length; i++) { + const chunk = chunks[i]!; + const text = formatDocForEmbedding(chunk.text); + const embedResult = await llm.embed(text); + if (embedResult) { + insertEmbedding( + db, + msg.hash, + i, + chunk.pos, + new Float32Array(embedResult.embedding), + embedResult.model, + now + ); + } + } + + embedded++; + options.onProgress?.(embedded, unembedded.length); + } + + return embedded; +} + +// ============================================================================= +// Summarization +// ============================================================================= + +/** + * Summarize a session via Ollama and store the summary. + */ +export async function summarizeSession( + db: Database, + sessionId: string, + options: { model?: string; force?: boolean } = {} +): Promise { + const session = getSession(db, sessionId); + if (!session) throw new Error(`Session not found: ${sessionId}`); + + // Check if already summarized (unless force) + if (session.summary && !options.force) { + return session.summary; + } + + const transcript = getSessionTranscript(db, sessionId); + if (!transcript.trim()) throw new Error(`Session ${sessionId} has no messages`); + + const summary = await ollamaSummarize(transcript, { model: options.model }); + const now = new Date().toISOString(); + + db.prepare( + `UPDATE memory_sessions SET summary = ?, summary_at = ? WHERE id = ?` + ).run(summary, now, sessionId); + + return summary; +} + +/** + * Summarize recent sessions that don't have summaries yet. + * Returns count of sessions summarized. + */ +export async function summarizeRecentSessions( + db: Database, + options: { limit?: number; model?: string } = {} +): Promise { + const limit = options.limit ?? 10; + const sessions = db + .prepare( + `SELECT id FROM memory_sessions WHERE active = 1 AND summary IS NULL ORDER BY updated_at DESC LIMIT ?` + ) + .all(limit) as { id: string }[]; + + let count = 0; + for (const s of sessions) { + try { + await summarizeSession(db, s.id, { model: options.model }); + count++; + } catch { + // Skip sessions that fail to summarize + } + } + return count; +} + +// ============================================================================= +// Recall +// ============================================================================= + +/** + * Recall relevant memories for a query. + * Combines FTS + vector search using RRF, deduplicates by session, + * and optionally synthesizes via Ollama. + */ +export async function recallMemories( + db: Database, + query: string, + options: { + limit?: number; + synthesize?: boolean; + model?: string; + maxTokens?: number; + } = {} +): Promise<{ results: MemorySearchResult[]; synthesis?: string }> { + const startedAt = performance.now(); + const shouldTraceRecall = process.env.SMRITI_BENCH_TRACE === "1"; + const limit = options.limit ?? 10; + + // Run FTS and vector search + const ftsStartedAt = performance.now(); + const ftsResults = searchMemoryFTS(db, query, limit); + const ftsMs = performance.now() - ftsStartedAt; + let vecResults: MemorySearchResult[] = []; + const vecStartedAt = performance.now(); + try { + vecResults = await searchMemoryVec(db, query, limit); + } catch { + // Vector search may fail if no embeddings exist + } + const vecMs = performance.now() - vecStartedAt; + + // Convert to RankedResult format for RRF + const toRanked = (results: MemorySearchResult[]): RankedResult[] => + results.map((r) => ({ + file: `${r.session_id}:${r.message_id}`, + displayPath: r.session_title, + title: r.role, + body: r.content, + score: r.score, + })); + + // Fuse results with RRF + const fuseStartedAt = performance.now(); + const fused = reciprocalRankFusion( + [toRanked(ftsResults), toRanked(vecResults)], + [1.0, 1.0] + ); + const fuseMs = performance.now() - fuseStartedAt; + + // Deduplicate by session, keeping best score per session + const dedupeStartedAt = performance.now(); + const sessionSeen = new Map(); + const dedupedResults: MemorySearchResult[] = []; + const originalByKey = new Map(); + for (const result of ftsResults) { + originalByKey.set(`${result.session_id}:${result.message_id}`, result); + } + for (const result of vecResults) { + const key = `${result.session_id}:${result.message_id}`; + // Prefer vector entry if both are present because it typically carries the better semantic score. + originalByKey.set(key, result); + } + + for (const r of fused) { + const [sessionId] = r.file.split(":"); + if (!sessionId) continue; + + // Find the original result to preserve all fields + const original = originalByKey.get(r.file) ?? null; + + if (original && !sessionSeen.has(sessionId)) { + sessionSeen.set(sessionId, true); + dedupedResults.push({ ...original, score: r.score }); + } else if (!original && !sessionSeen.has(sessionId)) { + sessionSeen.set(sessionId, true); + dedupedResults.push({ + session_id: sessionId!, + session_title: r.displayPath, + message_id: parseInt(r.file.split(":")[1] || "0"), + role: r.title, + content: r.body, + score: r.score, + source: "fts", + }); + } + } + const dedupeMs = performance.now() - dedupeStartedAt; + + const results = dedupedResults.slice(0, limit); + + // Optionally synthesize via Ollama + let synthesis: string | undefined; + if (options.synthesize && results.length > 0) { + const memoriesText = results + .map( + (r) => + `[Session: ${r.session_title || r.session_id}]\n${r.role}: ${r.content}` + ) + .join("\n\n---\n\n"); + + synthesis = await ollamaRecallSynthesize(query, memoriesText, { + model: options.model, + maxTokens: options.maxTokens, + }); + } + + if (shouldTraceRecall) { + const timings: RecallTimings = { + ftsMs, + vecMs, + fuseMs, + dedupeMs, + totalMs: performance.now() - startedAt, + }; + console.error( + `[recall.trace] q="${query.slice(0, 64)}" ` + + `fts=${timings.ftsMs.toFixed(3)}ms ` + + `vec=${timings.vecMs.toFixed(3)}ms ` + + `fuse=${timings.fuseMs.toFixed(3)}ms ` + + `dedupe=${timings.dedupeMs.toFixed(3)}ms ` + + `total=${timings.totalMs.toFixed(3)}ms` + ); + } + + return { results, synthesis }; +} + +// ============================================================================= +// Import +// ============================================================================= + +/** + * Import a conversation transcript from a file. + * Supports 'chat' format (role: content) and 'jsonl' format. + */ +export async function importTranscript( + db: Database, + content: string, + options: { title?: string; format?: "chat" | "jsonl"; sessionId?: string } = {} +): Promise<{ sessionId: string; messageCount: number }> { + const format = options.format ?? "chat"; + const sessionId = options.sessionId || crypto.randomUUID().slice(0, 8); + + let messages: { role: string; content: string }[] = []; + + if (format === "jsonl") { + messages = content + .split("\n") + .filter((line) => line.trim()) + .map((line) => { + const parsed = JSON.parse(line); + return { role: parsed.role || "user", content: parsed.content || "" }; + }); + } else { + // Chat format: "role: content" separated by blank lines + const blocks = content.split(/\n\n+/); + for (const block of blocks) { + const trimmed = block.trim(); + if (!trimmed) continue; + const colonIdx = trimmed.indexOf(":"); + if (colonIdx > 0 && colonIdx < 20) { + const role = trimmed.slice(0, colonIdx).trim().toLowerCase(); + const msgContent = trimmed.slice(colonIdx + 1).trim(); + if (msgContent) { + messages.push({ role, content: msgContent }); + } + } else { + messages.push({ role: "user", content: trimmed }); + } + } + } + + for (const msg of messages) { + await addMessage(db, sessionId, msg.role, msg.content, { + title: options.title, + }); + } + + return { sessionId, messageCount: messages.length }; +} + +// ============================================================================= +// Status +// ============================================================================= + +/** + * Get memory statistics. + */ +export function getMemoryStatus(db: Database): { + sessions: number; + activeSessions: number; + messages: number; + embeddedMessages: number; + summarizedSessions: number; +} { + const sessions = ( + db + .prepare(`SELECT COUNT(*) as count FROM memory_sessions`) + .get() as { count: number } + ).count; + + const activeSessions = ( + db + .prepare( + `SELECT COUNT(*) as count FROM memory_sessions WHERE active = 1` + ) + .get() as { count: number } + ).count; + + const messages = ( + db + .prepare(`SELECT COUNT(*) as count FROM memory_messages`) + .get() as { count: number } + ).count; + + const embeddedMessages = ( + db + .prepare( + `SELECT COUNT(DISTINCT m.hash) as count FROM memory_messages m + JOIN content_vectors cv ON cv.hash = m.hash` + ) + .get() as { count: number } + ).count; + + const summarizedSessions = ( + db + .prepare( + `SELECT COUNT(*) as count FROM memory_sessions WHERE summary IS NOT NULL` + ) + .get() as { count: number } + ).count; + + return { + sessions, + activeSessions, + messages, + embeddedMessages, + summarizedSessions, + }; +} diff --git a/src/ollama.ts b/src/ollama.ts new file mode 100644 index 0000000..bc3d082 --- /dev/null +++ b/src/ollama.ts @@ -0,0 +1,169 @@ +/** + * ollama.ts - Ollama API client for Smriti memory summarization and synthesis + * + * Uses Bun's fetch() to call Ollama's HTTP API. Separate from QMD's llm.ts + * which uses node-llama-cpp for embeddings/reranking. + * + * Config via env: + * OLLAMA_HOST - Ollama server URL (default: http://127.0.0.1:11434) + * QMD_MEMORY_MODEL - Model for summarization/synthesis (default: qwen3:8b-tuned) + */ + +// ============================================================================= +// Configuration +// ============================================================================= + +const OLLAMA_HOST = Bun.env.OLLAMA_HOST || "http://127.0.0.1:11434"; +const DEFAULT_MEMORY_MODEL = Bun.env.QMD_MEMORY_MODEL || "qwen3:8b-tuned"; + +// ============================================================================= +// Types +// ============================================================================= + +export type OllamaChatMessage = { + role: "system" | "user" | "assistant"; + content: string; +}; + +export type OllamaChatOptions = { + model?: string; + temperature?: number; + maxTokens?: number; +}; + +export type OllamaChatResponse = { + model: string; + message: OllamaChatMessage; + done: boolean; + total_duration?: number; + eval_count?: number; +}; + +// ============================================================================= +// Core API +// ============================================================================= + +/** + * Send a chat completion request to Ollama. + * Uses stream: false for simple request/response. + */ +export async function ollamaChat( + messages: OllamaChatMessage[], + options: OllamaChatOptions = {} +): Promise { + const model = options.model || DEFAULT_MEMORY_MODEL; + const resp = await fetch(`${OLLAMA_HOST}/api/chat`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model, + messages, + stream: false, + options: { + ...(options.temperature !== undefined && { temperature: options.temperature }), + ...(options.maxTokens !== undefined && { num_predict: options.maxTokens }), + }, + }), + }); + + if (!resp.ok) { + const body = await resp.text(); + throw new Error(`Ollama chat failed (${resp.status}): ${body}`); + } + + return resp.json() as Promise; +} + +/** + * Summarize a conversation transcript via Ollama. + * Returns a concise summary of the conversation. + */ +export async function ollamaSummarize( + transcript: string, + options: OllamaChatOptions = {} +): Promise { + const messages: OllamaChatMessage[] = [ + { + role: "system", + content: + "You are a conversation summarizer. Produce a concise summary of the following conversation. " + + "Focus on key topics discussed, decisions made, questions asked, and solutions provided. " + + "Keep it under 200 words. Output only the summary, no preamble.", + }, + { + role: "user", + content: transcript, + }, + ]; + + const resp = await ollamaChat(messages, { + ...options, + temperature: options.temperature ?? 0.3, + maxTokens: options.maxTokens ?? 512, + }); + + return resp.message.content.trim(); +} + +/** + * Synthesize recalled memories into coherent context via Ollama. + * Takes a query and memory fragments, returns a synthesized response. + */ +export async function ollamaRecall( + query: string, + memories: string, + options: OllamaChatOptions = {} +): Promise { + const messages: OllamaChatMessage[] = [ + { + role: "system", + content: + "You are a memory recall assistant. Given a query and relevant past conversation memories, " + + "synthesize the memories into useful context for answering the query. " + + "Be concise and focus on information directly relevant to the query. " + + "If memories contain contradictory information, note the most recent. " + + "Output only the synthesized context, no preamble.", + }, + { + role: "user", + content: `Query: ${query}\n\nRelevant memories:\n${memories}`, + }, + ]; + + const resp = await ollamaChat(messages, { + ...options, + temperature: options.temperature ?? 0.3, + maxTokens: options.maxTokens ?? 1024, + }); + + return resp.message.content.trim(); +} + +/** + * Check if Ollama is running and accessible. + * Pings the /api/tags endpoint. + */ +export async function ollamaHealthCheck(): Promise<{ + ok: boolean; + models?: string[]; + error?: string; +}> { + try { + const resp = await fetch(`${OLLAMA_HOST}/api/tags`, { + signal: AbortSignal.timeout(5000), + }); + if (!resp.ok) { + return { ok: false, error: `HTTP ${resp.status}` }; + } + const data = (await resp.json()) as { models?: { name: string }[] }; + const models = data.models?.map((m) => m.name) || []; + return { ok: true, models }; + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : String(err), + }; + } +} + +export { DEFAULT_MEMORY_MODEL, OLLAMA_HOST }; diff --git a/src/qmd.ts b/src/qmd.ts index ccfa4cf..d474c90 100644 --- a/src/qmd.ts +++ b/src/qmd.ts @@ -17,8 +17,8 @@ export { importTranscript, initializeMemoryTables, createSession, -} from "../qmd/src/memory"; +} from "./memory"; export { hashContent } from "../qmd/src/store"; -export { ollamaRecall } from "../qmd/src/ollama"; +export { ollamaRecall } from "./ollama"; From 91effef818beb249650728cbdf2253ad9519a039 Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 3 May 2026 22:42:19 +0530 Subject: [PATCH 03/13] fix(ci): add picomatch@4 as explicit root dep after QMD upstream sync --- bun.lock | 817 +-------------------------------------------------- package.json | 1 + 2 files changed, 9 insertions(+), 809 deletions(-) diff --git a/bun.lock b/bun.lock index ce460ce..f191cb2 100644 --- a/bun.lock +++ b/bun.lock @@ -6,152 +6,25 @@ "name": "smriti", "dependencies": { "node-llama-cpp": "^3.0.0", + "picomatch": "^4.0.0", "qmd": "file:./qmd", }, "devDependencies": { "@types/bun": "latest", }, }, - "website": { - "name": "smriti-website", - "version": "0.0.1", - "dependencies": { - "@next/mdx": "15.0.0-canary.74", - "@tailwindcss/typography": "^0.5.10", - "clsx": "^2.1.1", - "next": "^15.0.0", - "next-themes": "^0.3.0", - "react": "^19.0.0", - "react-dom": "18.3.1", - }, - "devDependencies": { - "@types/node": "^20.11.30", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-config-next": "15.0.0-canary.74", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.10", - "typescript": "^5.4.5", - }, - }, }, "packages": { - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - - "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], - - "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.5", "", {}, "sha512-xRlzazC+QZwr6z4ixEqYHo9fgwhTZ3xNSdljlKfUFGZSdlvt166DljRELFUfFytlYOYvo3vTisA/AFOuOAzFQQ=="], - "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], - - "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - - "@next/env": ["@next/env@15.5.12", "", {}, "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg=="], - - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.0.0-canary.74", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-XQlTvNq4GYkq+Haspcs94mzL8ZL7gYlZT8YuBFv6KBKkZiHWLIS7jDHTaRZeaW/pjNegIYG/wfTgu84SdyuhAA=="], - - "@next/mdx": ["@next/mdx@15.0.0-canary.74", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-3KvKJ2NUuAS0Q2roldXbJZzGCvzKRCirs8wLB8G6judVj6y+iTuExPacTnpD60MNBRlDKEZoHN/MjzcjDNdtZw=="], - - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg=="], - - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA=="], - - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw=="], - - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw=="], - - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw=="], - - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w=="], - - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg=="], - - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.12", "", { "os": "win32", "cpu": "x64" }, "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw=="], - "@node-llama-cpp/linux-arm64": ["@node-llama-cpp/linux-arm64@3.15.1", "", { "os": "linux", "cpu": [ "x64", "arm64", ] }, "sha512-g7JC/WwDyyBSmkIjSvRF2XLW+YA0z2ZVBSAKSv106mIPO4CzC078woTuTaPsykWgIaKcQRyXuW5v5XQMcT1OOA=="], "@node-llama-cpp/linux-armv7l": ["@node-llama-cpp/linux-armv7l@3.15.1", "", { "os": "linux", "cpu": [ "arm", "x64", ] }, "sha512-MSxR3A0vFSVWbmVSkNqNXQnI45L2Vg7/PRgJukcjChk7YzRxs9L+oQMeycVW3BsQ03mIZ0iORsZ9MNIBEbdS3g=="], @@ -178,14 +51,6 @@ "@node-llama-cpp/win-x64-vulkan": ["@node-llama-cpp/win-x64-vulkan@3.15.1", "", { "os": "win32", "cpu": "x64" }, "sha512-BPBjUEIkFTdcHSsQyblP0v/aPPypi6uqQIq27mo4A49CYjX22JDmk4ncdBLk6cru+UkvwEEe+F2RomjoMt32aQ=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@octokit/app": ["@octokit/app@16.1.2", "", { "dependencies": { "@octokit/auth-app": "^8.1.2", "@octokit/auth-unauthenticated": "^7.0.3", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ=="], "@octokit/auth-app": ["@octokit/auth-app@8.2.0", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g=="], @@ -254,95 +119,17 @@ "@reflink/reflink-win32-x64-msvc": ["@reflink/reflink-win32-x64-msvc@0.1.19", "", { "os": "win32", "cpu": "x64" }, "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w=="], - "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - - "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="], - - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - - "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.4", "", {}, "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/aws-lambda": ["@types/aws-lambda@8.10.160", "", {}, "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA=="], "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - - "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], - - "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], - - "@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], - - "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@7.18.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@7.18.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0" } }, "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@7.18.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@7.18.0", "", {}, "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" } }, "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@7.18.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" } }, "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], - - "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], - - "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], - - "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], - - "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], - - "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], - - "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], - - "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], - - "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], - - "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], - - "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], - - "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], - - "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], - - "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], - - "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], - - "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], - - "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], - - "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], - - "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -352,96 +139,34 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="], "are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="], - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - - "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], - - "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], - - "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], - - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - - "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], - - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="], - "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], - "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chmodrp": ["chmodrp@1.0.2", "", {}, "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w=="], - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], @@ -450,12 +175,8 @@ "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "cmake-js": ["cmake-js@7.4.0", "", { "dependencies": { "axios": "^1.6.5", "debug": "^4", "fs-extra": "^11.2.0", "memory-stream": "^1.0.0", "node-api-headers": "^1.1.0", "npmlog": "^6.0.2", "rc": "^1.2.7", "semver": "^7.5.4", "tar": "^6.2.0", "url-join": "^4.0.1", "which": "^2.0.2", "yargs": "^17.7.2" }, "bin": { "cmake-js": "bin/cmake-js" } }, "sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -468,8 +189,6 @@ "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], @@ -484,110 +203,38 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], - - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], - - "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - - "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - - "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], - - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "env-var": ["env-var@7.5.0", "", {}, "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA=="], - "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], - - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], - - "eslint-config-next": ["eslint-config-next@15.0.0-canary.74", "", { "dependencies": { "@next/eslint-plugin-next": "15.0.0-canary.74", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-TPipS6uDMXDQQsNrAJWEUdlX2R3AWAp6i55PfbJCHMp4ZZ9q+A9l49sJg1mph1M2ogXp95phGeUUom7na1Fw/g=="], - - "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], - - "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], - - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], - - "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], - - "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], - - "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], - - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@4.6.2", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ=="], - - "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], @@ -604,64 +251,30 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], - "filename-reserved-regex": ["filename-reserved-regex@3.0.0", "", {}, "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw=="], "filenamify": ["filenamify@6.0.0", "", { "dependencies": { "filename-reserved-regex": "^3.0.0" } }, "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - "gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="], - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], @@ -670,34 +283,10 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], - - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -714,140 +303,40 @@ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "ipull": ["ipull@3.9.3", "", { "dependencies": { "@tinyhttp/content-disposition": "^2.2.0", "async-retry": "^1.3.3", "chalk": "^5.3.0", "ci-info": "^4.0.0", "cli-spinners": "^2.9.2", "commander": "^10.0.0", "eventemitter3": "^5.0.1", "filenamify": "^6.0.0", "fs-extra": "^11.1.1", "is-unicode-supported": "^2.0.0", "lifecycle-utils": "^2.0.1", "lodash.debounce": "^4.0.8", "lowdb": "^7.0.1", "pretty-bytes": "^6.1.0", "pretty-ms": "^8.0.0", "sleep-promise": "^9.1.0", "slice-ansi": "^7.1.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.1.0" }, "optionalDependencies": { "@reflink/reflink": "^0.1.16" }, "bin": { "ipull": "dist/cli/cli.js" } }, "sha512-ZMkxaopfwKHwmEuGDYx7giNBdLxbHbRCWcQVA1D2eqE4crUguupfxej6s7UqbidYEwT69dkyumYkY8DPHIxF9g=="], - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - - "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - - "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - - "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - - "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], - - "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "lifecycle-utils": ["lifecycle-utils@3.1.0", "", {}, "sha512-kVvegv+r/icjIo1dkHv1hznVQi4FzEVglJD2IU4w07HzevIyH3BAYsFZzEIbBk/nNZjXHGgclJ5g9rz9QdBCLw=="], - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lowdb": ["lowdb@7.0.1", "", { "dependencies": { "steno": "^4.0.2" } }, "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -858,18 +347,12 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], @@ -880,50 +363,22 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], - "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next": ["next@15.5.12", "", { "dependencies": { "@next/env": "15.5.12", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.12", "@next/swc-darwin-x64": "15.5.12", "@next/swc-linux-arm64-gnu": "15.5.12", "@next/swc-linux-arm64-musl": "15.5.12", "@next/swc-linux-x64-gnu": "15.5.12", "@next/swc-linux-x64-musl": "15.5.12", "@next/swc-win32-arm64-msvc": "15.5.12", "@next/swc-win32-x64-msvc": "15.5.12", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA=="], - - "next-themes": ["next-themes@0.3.0", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" } }, "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w=="], - "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], "node-api-headers": ["node-api-headers@1.8.0", "", {}, "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ=="], "node-llama-cpp": ["node-llama-cpp@3.15.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.3", "async-retry": "^1.3.3", "bytes": "^3.1.2", "chalk": "^5.4.1", "chmodrp": "^1.0.2", "cmake-js": "^7.4.0", "cross-spawn": "^7.0.6", "env-var": "^7.5.0", "filenamify": "^6.0.0", "fs-extra": "^11.3.0", "ignore": "^7.0.4", "ipull": "^3.9.2", "is-unicode-supported": "^2.1.0", "lifecycle-utils": "^3.0.1", "log-symbols": "^7.0.0", "nanoid": "^5.1.5", "node-addon-api": "^8.3.1", "octokit": "^5.0.3", "ora": "^8.2.0", "pretty-ms": "^9.2.0", "proper-lockfile": "^4.1.2", "semver": "^7.7.1", "simple-git": "^3.27.0", "slice-ansi": "^7.1.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.1.0", "validate-npm-package-name": "^6.0.0", "which": "^5.0.0", "yargs": "^17.7.2" }, "optionalDependencies": { "@node-llama-cpp/linux-arm64": "3.15.1", "@node-llama-cpp/linux-armv7l": "3.15.1", "@node-llama-cpp/linux-x64": "3.15.1", "@node-llama-cpp/linux-x64-cuda": "3.15.1", "@node-llama-cpp/linux-x64-cuda-ext": "3.15.1", "@node-llama-cpp/linux-x64-vulkan": "3.15.1", "@node-llama-cpp/mac-arm64-metal": "3.15.1", "@node-llama-cpp/mac-x64": "3.15.1", "@node-llama-cpp/win-arm64": "3.15.1", "@node-llama-cpp/win-x64": "3.15.1", "@node-llama-cpp/win-x64-cuda": "3.15.1", "@node-llama-cpp/win-x64-cuda-ext": "3.15.1", "@node-llama-cpp/win-x64-vulkan": "3.15.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"], "bin": { "node-llama-cpp": "dist/cli/cli.js", "nlc": "dist/cli/cli.js" } }, "sha512-/fBNkuLGR2Q8xj2eeV12KXKZ9vCS2+o6aP11lW40pB9H6f0B3wOALi/liFrjhHukAoiH6C9wFTPzv6039+5DRA=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], - - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - - "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], - - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "octokit": ["octokit@5.0.5", "", { "dependencies": { "@octokit/app": "^16.1.2", "@octokit/core": "^7.0.6", "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-graphql": "^6.0.0", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0", "@octokit/plugin-retry": "^8.0.3", "@octokit/plugin-throttling": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" } }, "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -932,138 +387,56 @@ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], - - "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], - - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - - "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="], "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qmd": ["qmd@file:qmd", { "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", "node-llama-cpp": "^3.14.5", "sqlite-vec": "^0.1.7-alpha.2", "yaml": "^2.8.2", "zod": "^4.2.1" }, "devDependencies": { "@types/bun": "latest" }, "optionalDependencies": { "sqlite-vec-darwin-arm64": "^0.1.7-alpha.2", "sqlite-vec-darwin-x64": "^0.1.7-alpha.2", "sqlite-vec-linux-x64": "^0.1.7-alpha.2", "sqlite-vec-win32-x64": "^0.1.7-alpha.2" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "qmd": "./qmd" } }], "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - - "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], - - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -1072,16 +445,8 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], - - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -1098,18 +463,10 @@ "simple-git": ["simple-git@3.30.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg=="], - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "sleep-promise": ["sleep-promise@9.1.0", "", {}, "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA=="], "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - "smriti-website": ["smriti-website@workspace:website"], - - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "sqlite-vec": ["sqlite-vec@0.1.7-alpha.2", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.7-alpha.2", "sqlite-vec-darwin-x64": "0.1.7-alpha.2", "sqlite-vec-linux-arm64": "0.1.7-alpha.2", "sqlite-vec-linux-x64": "0.1.7-alpha.2", "sqlite-vec-windows-x64": "0.1.7-alpha.2" } }, "sha512-rNgRCv+4V4Ed3yc33Qr+nNmjhtrMnnHzXfLVPeGb28Dx5mmDL3Ngw/Wk8vhCGjj76+oC6gnkmMG8y73BZWGBwQ=="], "sqlite-vec-darwin-arm64": ["sqlite-vec-darwin-arm64@0.1.7-alpha.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-raIATOqFYkeCHhb/t3r7W7Cf2lVYdf4J3ogJ6GFc8PQEgHCPEsi+bYnm2JT84MzLfTlSTIdxr4/NKv+zF7oLPw=="], @@ -1122,8 +479,6 @@ "sqlite-vec-windows-x64": ["sqlite-vec-windows-x64@0.1.7-alpha.2", "", { "os": "win32", "cpu": "x64" }, "sha512-TRP6hTjAcwvQ6xpCZvjP00pdlda8J38ArFy1lMYhtQWXiIBmWnhMaMbq4kaeCYwvTTddfidatRS+TJrwIKB/oQ=="], - "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], @@ -1132,83 +487,25 @@ "steno": ["steno@4.0.2", "", {}, "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A=="], - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], - - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], - - "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], - - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], - - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "universal-github-app-jwt": ["universal-github-app-jwt@2.2.2", "", {}, "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw=="], @@ -1218,12 +515,6 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -1234,18 +525,8 @@ "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], - "wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -1260,32 +541,12 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], - - "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "bun-types/@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="], - - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1294,30 +555,6 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "eslint/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "eslint-plugin-react/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], - - "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -1326,38 +563,20 @@ "gauge/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "ipull/lifecycle-utils": ["lifecycle-utils@2.1.0", "", {}, "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA=="], "ipull/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="], "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - - "next-themes/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], - "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], - "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "proper-lockfile/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "qmd/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], - "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - - "react-dom/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], - "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "wide-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -1368,16 +587,6 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -1388,10 +597,6 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "eslint/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "gauge/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1402,8 +607,6 @@ "ipull/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="], - "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], "qmd/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], @@ -1426,12 +629,8 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "qmd/@types/bun/bun-types/@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="], - "wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "qmd/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/package.json b/package.json index 16de12d..8eb379e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "node-llama-cpp": "^3.0.0", + "picomatch": "^4.0.0", "qmd": "file:./qmd" }, "devDependencies": { From 971c4efea0bcc0e90d6dfd8af5641b5d97e3aac1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 09:35:59 +0000 Subject: [PATCH 04/13] feat(learn): add continuous knowledge consolidation layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `smriti consolidate` and `smriti learnings`, applying Progressive Summarization: cheap Stage-1 segmentation runs broadly over dense sessions into a new smriti_knowledge_units table, and expensive Stage-2 polish (existing segmentSession/generateDocument pipeline) only runs once a unit proves reuse via recall or scored high relevance at extraction time. - src/db.ts: smriti_knowledge_units table + CRUD helpers (insertKnowledgeUnit, findUnsegmentedDenseSessions, findPromotableUnits, incrementRetrievalCount, promoteKnowledgeUnit, listKnowledgeUnits) - src/search/recall.ts: track retrieval_count on every recall() path - src/learn/consolidate.ts: segment + promote phases, reusing the existing 3-stage segmentation pipeline from src/team/segment.ts and document.ts - src/index.ts, src/format.ts: CLI wiring for `consolidate` and `learnings` CLI-only, not wired into the daemon — consolidation runs two LLM stages, which the daemon's flush path deliberately avoids (see the enrichOnIngest comment in src/daemon/index.ts). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PnYgLUDwWALu1bUAgrFQDG --- src/db.ts | 209 ++++++++++++++++++++++++ src/format.ts | 57 +++++++ src/index.ts | 43 ++++- src/learn/consolidate.ts | 196 +++++++++++++++++++++++ src/search/recall.ts | 22 +++ src/team/share.ts | 2 +- test/learn-consolidate.test.ts | 283 +++++++++++++++++++++++++++++++++ 7 files changed, 810 insertions(+), 2 deletions(-) create mode 100644 src/learn/consolidate.ts create mode 100644 test/learn-consolidate.test.ts diff --git a/src/db.ts b/src/db.ts index 59d8ba7..b04bd3e 100644 --- a/src/db.ts +++ b/src/db.ts @@ -14,6 +14,7 @@ import { QMD_DB_PATH, SMRITI_SESSIONS_DIR } from "./config"; import { initializeMemoryTables } from "./qmd"; import { createStore } from "../qmd/src/index"; import { setQmdStore, closeQmdStore } from "./store"; +import type { KnowledgeUnit } from "./team/types"; // ============================================================================= // Connection @@ -204,6 +205,33 @@ export function initializeSmritiTables(db: Database): void { entities TEXT ); + -- Knowledge consolidation: raw Stage-1 extracts, promoted to canonical on reuse + CREATE TABLE IF NOT EXISTS smriti_knowledge_units ( + id TEXT PRIMARY KEY, -- KnowledgeUnit.id (uuid) + session_id TEXT NOT NULL, + project_id TEXT, + topic TEXT NOT NULL, + category TEXT NOT NULL, + relevance REAL NOT NULL DEFAULT 0, -- 0-10, from Stage 1 + entities TEXT, -- JSON array + files TEXT, -- JSON array + plain_text TEXT NOT NULL, -- raw Stage-1 extract + line_ranges TEXT, -- JSON array of {start,end} + content_hash TEXT NOT NULL, -- hashContent({topic,category,plainText}) — Stage-1 dedup key + tier TEXT NOT NULL DEFAULT 'segmented', -- 'segmented' | 'canonical' + retrieval_count INTEGER NOT NULL DEFAULT 0, + last_recalled_at TEXT, + promoted_at TEXT, + canonical_doc_path TEXT, -- relative path under .smriti/knowledge/, set on promotion + share_id TEXT, -- points at the smriti_shares row created on promotion + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE INDEX IF NOT EXISTS idx_smriti_knowledge_units_session ON smriti_knowledge_units(session_id); + CREATE INDEX IF NOT EXISTS idx_smriti_knowledge_units_hash ON smriti_knowledge_units(content_hash); + CREATE INDEX IF NOT EXISTS idx_smriti_knowledge_units_tier ON smriti_knowledge_units(tier); + -- Tool usage tracking CREATE TABLE IF NOT EXISTS smriti_tool_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1246,6 +1274,187 @@ export function getDensityScore(db: Database, sessionId: string): number { return row?.density_score ?? 0; } +// ============================================================================= +// Knowledge Consolidation (Progressive Summarization) +// ============================================================================= + +export interface StoredKnowledgeUnit { + id: string; + session_id: string; + project_id: string | null; + topic: string; + category: string; + relevance: number; + entities: string[]; + files: string[]; + plain_text: string; + line_ranges: Array<{ start: number; end: number }>; + content_hash: string; + tier: "segmented" | "canonical"; + retrieval_count: number; + last_recalled_at: string | null; + promoted_at: string | null; + canonical_doc_path: string | null; + share_id: string | null; +} + +type KnowledgeUnitRow = { + id: string; + session_id: string; + project_id: string | null; + topic: string; + category: string; + relevance: number; + entities: string | null; + files: string | null; + plain_text: string; + line_ranges: string | null; + content_hash: string; + tier: string; + retrieval_count: number; + last_recalled_at: string | null; + promoted_at: string | null; + canonical_doc_path: string | null; + share_id: string | null; +}; + +function deserializeKnowledgeUnit(row: KnowledgeUnitRow): StoredKnowledgeUnit { + return { + ...row, + entities: row.entities ? JSON.parse(row.entities) : [], + files: row.files ? JSON.parse(row.files) : [], + line_ranges: row.line_ranges ? JSON.parse(row.line_ranges) : [], + tier: row.tier as "segmented" | "canonical", + }; +} + +/** + * Insert a Stage-1 knowledge unit if its content hash isn't already stored. + * Returns true if inserted, false if it was a duplicate (caller distinguishes + * "stored" from "skipped" the same way shareSegmentedKnowledge does for shares). + */ +export function insertKnowledgeUnit( + db: Database, + unit: KnowledgeUnit, + sessionId: string, + projectId: string | null, + contentHash: string +): boolean { + const exists = db + .prepare(`SELECT 1 FROM smriti_knowledge_units WHERE content_hash = ?`) + .get(contentHash); + if (exists) return false; + + db.prepare( + `INSERT INTO smriti_knowledge_units + (id, session_id, project_id, topic, category, relevance, entities, files, plain_text, line_ranges, content_hash) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + unit.id, + sessionId, + projectId, + unit.topic, + unit.category, + unit.relevance, + JSON.stringify(unit.entities || []), + JSON.stringify(unit.files || []), + unit.plainText, + JSON.stringify(unit.lineRanges || []), + contentHash + ); + return true; +} + +/** Dense sessions (by density_score) that haven't been segmented into knowledge units yet. */ +export function findUnsegmentedDenseSessions( + db: Database, + minDensity: number, + limit?: number +): Array<{ session_id: string; project_id: string | null; density_score: number }> { + const query = ` + SELECT sm.session_id, sm.project_id, sm.density_score + FROM smriti_session_meta sm + WHERE sm.density_score >= ? + AND NOT EXISTS (SELECT 1 FROM smriti_knowledge_units ku WHERE ku.session_id = sm.session_id) + ORDER BY sm.density_score DESC + ${limit ? "LIMIT ?" : ""} + `; + const rows = limit + ? db.prepare(query).all(minDensity, limit) + : db.prepare(query).all(minDensity); + return rows as Array<{ session_id: string; project_id: string | null; density_score: number }>; +} + +/** Segmented units that have proven reuse (via recall) or scored high relevance at extraction time. */ +export function findPromotableUnits( + db: Database, + minRetrievals: number, + minRelevance: number +): StoredKnowledgeUnit[] { + const rows = db + .prepare( + `SELECT * FROM smriti_knowledge_units + WHERE tier = 'segmented' AND (retrieval_count >= ? OR relevance >= ?)` + ) + .all(minRetrievals, minRelevance) as KnowledgeUnitRow[]; + return rows.map(deserializeKnowledgeUnit); +} + +/** Bump retrieval_count for any knowledge units belonging to a recalled session. No-op if none exist yet. */ +export function incrementRetrievalCount(db: Database, sessionId: string): void { + db.prepare( + `UPDATE smriti_knowledge_units + SET retrieval_count = retrieval_count + 1, + last_recalled_at = datetime('now'), + updated_at = datetime('now') + WHERE session_id = ?` + ).run(sessionId); +} + +export function promoteKnowledgeUnit( + db: Database, + unitId: string, + canonicalDocPath: string, + shareId: string +): void { + db.prepare( + `UPDATE smriti_knowledge_units + SET tier = 'canonical', promoted_at = datetime('now'), + canonical_doc_path = ?, share_id = ?, updated_at = datetime('now') + WHERE id = ?` + ).run(canonicalDocPath, shareId, unitId); +} + +export function listKnowledgeUnits( + db: Database, + options: { tier?: "segmented" | "canonical"; minRetrievals?: number; limit?: number } = {} +): StoredKnowledgeUnit[] { + const conditions: string[] = []; + const params: any[] = []; + + if (options.tier) { + conditions.push("tier = ?"); + params.push(options.tier); + } + if (options.minRetrievals !== undefined) { + conditions.push("retrieval_count >= ?"); + params.push(options.minRetrievals); + } + + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const limitClause = options.limit ? "LIMIT ?" : ""; + if (options.limit) params.push(options.limit); + + const rows = db + .prepare( + `SELECT * FROM smriti_knowledge_units ${where} + ORDER BY retrieval_count DESC, relevance DESC + ${limitClause}` + ) + .all(...params) as KnowledgeUnitRow[]; + return rows.map(deserializeKnowledgeUnit); +} + // ============================================================================= // Session Query Labels (#60) // ============================================================================= diff --git a/src/format.ts b/src/format.ts index 023b0c3..e414186 100644 --- a/src/format.ts +++ b/src/format.ts @@ -268,6 +268,63 @@ export function formatShareResult(result: { return lines.join("\n"); } +// ============================================================================= +// Consolidate Result Formatting +// ============================================================================= + +export function formatConsolidateResult(result: { + sessionsSegmented: number; + unitsStored: number; + unitsSkipped: number; + unitsPromoted: number; + errors: string[]; +}): string { + const lines = [ + `Sessions segmented: ${result.sessionsSegmented}`, + `Units stored: ${result.unitsStored}`, + `Units skipped (dedup): ${result.unitsSkipped}`, + `Units promoted: ${result.unitsPromoted}`, + ]; + + if (result.errors.length > 0) { + lines.push(`Errors: ${result.errors.length}`); + for (const err of result.errors.slice(0, 5)) { + lines.push(` - ${err}`); + } + } + + return lines.join("\n"); +} + +// ============================================================================= +// Knowledge Units (Learnings) Formatting +// ============================================================================= + +export function formatLearnings( + units: Array<{ + tier: string; + topic: string; + category: string; + retrieval_count: number; + relevance: number; + canonical_doc_path: string | null; + }> +): string { + if (units.length === 0) return "No knowledge units found."; + + const headers = ["Tier", "Topic", "Category", "Retrievals", "Relevance", "Doc Path"]; + const rows = units.map((u) => [ + u.tier === "canonical" ? "✓ canonical" : "segmented", + u.topic, + u.category, + String(u.retrieval_count), + u.relevance.toFixed(1), + u.canonical_doc_path || "-", + ]); + + return table(headers, rows, [14, 40, 20, 10, 9, 40]); +} + // ============================================================================= // Sync Result Formatting // ============================================================================= diff --git a/src/index.ts b/src/index.ts index bb186d5..ecea67b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ * schema-based categorization, and team knowledge sharing. */ -import { initSmriti, closeDb, getCategories, getCategoryTree, addCategory, listProjects, tagSession, getProjectReport, getTagUsage, computeDensityScore, updateDensityScore, insertSessionQueries, getUnenrichedSessionIds } from "./db"; +import { initSmriti, closeDb, getCategories, getCategoryTree, addCategory, listProjects, tagSession, getProjectReport, getTagUsage, computeDensityScore, updateDensityScore, insertSessionQueries, getUnenrichedSessionIds, listKnowledgeUnits } from "./db"; import { getMessages, getSession, getMemoryStatus, embedMemoryMessages } from "./qmd"; import { ingest, ingestAll } from "./ingest/index"; import { categorizeUncategorized } from "./categorize/classifier"; @@ -16,6 +16,7 @@ import { searchFiltered, listSessions } from "./search/index"; import { recall } from "./search/recall"; import { shareKnowledge } from "./team/share"; import { syncTeamKnowledge, listTeamContributions } from "./team/sync"; +import { consolidateKnowledge } from "./learn/consolidate"; import { generateContext, compareSessions, @@ -53,6 +54,8 @@ import { formatTagUsage, formatDensityBreakdown, formatDigest, + formatConsolidateResult, + formatLearnings, json, } from "./format"; import { generateDigest } from "./digest"; @@ -213,6 +216,8 @@ Commands: compare Compare two sessions (tokens, tools, files) compare --last Compare last 2 sessions for current project share [filters] Export knowledge to .smriti/ + consolidate [options] Segment dense sessions, promote reused knowledge units + learnings [options] List extracted knowledge units (tier, retrievals, relevance) sync Import team knowledge from .smriti/ team View team contributions list [filters] List sessions @@ -812,6 +817,42 @@ async function main() { break; } + // ===================================================================== + // CONSOLIDATE + // ===================================================================== + case "consolidate": { + const result = await consolidateKnowledge(db, { + minDensity: Number(getArg(args, "--min-density")) || undefined, + minRetrievals: Number(getArg(args, "--min-retrievals")) || undefined, + minRelevance: Number(getArg(args, "--min-relevance")) || undefined, + model: getArg(args, "--model"), + outputDir: getArg(args, "--output"), + sessionLimit: Number(getArg(args, "--session-limit")) || undefined, + onProgress: (msg) => console.log(` ${msg}`), + }); + + console.log(formatConsolidateResult(result)); + break; + } + + // ===================================================================== + // LEARNINGS + // ===================================================================== + case "learnings": { + const units = listKnowledgeUnits(db, { + tier: getArg(args, "--tier") as "segmented" | "canonical" | undefined, + minRetrievals: Number(getArg(args, "--min-retrievals")) || undefined, + limit: Number(getArg(args, "--limit")) || 50, + }); + + if (hasFlag(args, "--json")) { + console.log(json(units)); + } else { + console.log(formatLearnings(units)); + } + break; + } + // ===================================================================== // SYNC // ===================================================================== diff --git a/src/learn/consolidate.ts b/src/learn/consolidate.ts new file mode 100644 index 0000000..b6d973b --- /dev/null +++ b/src/learn/consolidate.ts @@ -0,0 +1,196 @@ +/** + * learn/consolidate.ts - Continuous knowledge consolidation + * + * Progressive Summarization: cheap Stage-1 extraction runs broadly over dense + * sessions; expensive Stage-2 polish only runs once a unit proves it's reused + * (recalled repeatedly) or scored high relevance at extraction time. + * + * Two independent phases, run sequentially: + * - Segment: dense, not-yet-segmented sessions -> segmentSession() -> smriti_knowledge_units + * - Promote: knowledge units that cleared the reuse/relevance bar -> generateDocument() + * -> written to .smriti/knowledge/ + recorded in smriti_shares + * + * CLI-only, like `categorize`/`share` — never wired into the daemon (see + * src/daemon/index.ts's enrichOnIngest comment for why LLM work per-flush is unsafe). + */ + +import type { Database } from "bun:sqlite"; +import { mkdirSync } from "fs"; +import { join } from "path"; +import { SMRITI_DIR, AUTHOR } from "../config"; +import { hashContent } from "../qmd"; +import { + findUnsegmentedDenseSessions, + insertKnowledgeUnit, + findPromotableUnits, + promoteKnowledgeUnit, +} from "../db"; +import { getSessionMessages } from "../team/share"; +import { segmentSession } from "../team/segment"; +import { generateDocument, generateFrontmatter } from "../team/document"; +import { isSessionWorthSharing } from "../team/formatter"; +import type { RawMessage } from "../team/formatter"; +import type { KnowledgeUnit } from "../team/types"; + +// ============================================================================= +// Types +// ============================================================================= + +export type ConsolidateOptions = { + minDensity?: number; + minRetrievals?: number; + minRelevance?: number; + model?: string; + outputDir?: string; + author?: string; + sessionLimit?: number; + onProgress?: (msg: string) => void; +}; + +export type ConsolidateResult = { + sessionsSegmented: number; + unitsStored: number; + unitsSkipped: number; + unitsPromoted: number; + errors: string[]; +}; + +// ============================================================================= +// Consolidation +// ============================================================================= + +export async function consolidateKnowledge( + db: Database, + options: ConsolidateOptions = {} +): Promise { + const author = options.author || AUTHOR; + const outputDir = options.outputDir || join(process.cwd(), SMRITI_DIR); + + const result: ConsolidateResult = { + sessionsSegmented: 0, + unitsStored: 0, + unitsSkipped: 0, + unitsPromoted: 0, + errors: [], + }; + + // =========================================================================== + // Segment phase: cheap Stage-1 extraction over dense, unsegmented sessions + // =========================================================================== + + const sessions = findUnsegmentedDenseSessions( + db, + options.minDensity ?? 0.5, + options.sessionLimit ?? 20 + ); + + for (const s of sessions) { + try { + const messages = getSessionMessages(db, s.session_id); + if (messages.length === 0) continue; + + const rawMessages: RawMessage[] = messages.map((m) => ({ + role: m.role, + content: m.content, + })); + + if (!isSessionWorthSharing(rawMessages)) continue; + + const segmentationResult = await segmentSession(db, s.session_id, rawMessages, { + model: options.model, + }); + result.sessionsSegmented++; + + for (const unit of segmentationResult.units) { + const contentHash = await hashContent( + JSON.stringify({ topic: unit.topic, category: unit.category, plainText: unit.plainText }) + ); + const inserted = insertKnowledgeUnit(db, unit, s.session_id, s.project_id, contentHash); + inserted ? result.unitsStored++ : result.unitsSkipped++; + } + } catch (err: any) { + result.errors.push(`segment ${s.session_id}: ${err.message}`); + } + } + + options.onProgress?.( + `segment phase: ${result.sessionsSegmented} sessions, ${result.unitsStored} units stored, ${result.unitsSkipped} skipped` + ); + + // =========================================================================== + // Promote phase: expensive Stage-2 polish for units that proved reuse + // =========================================================================== + + const knowledgeDir = join(outputDir, "knowledge"); + mkdirSync(knowledgeDir, { recursive: true }); + + const promotable = findPromotableUnits( + db, + options.minRetrievals ?? 3, + options.minRelevance ?? 8 + ); + + for (const stored of promotable) { + try { + const unit: KnowledgeUnit = { + id: stored.id, + topic: stored.topic, + category: stored.category, + relevance: stored.relevance, + entities: stored.entities, + files: stored.files, + plainText: stored.plain_text, + lineRanges: stored.line_ranges, + }; + + const doc = await generateDocument(unit, stored.topic, { + model: options.model, + projectSmritiDir: outputDir, + author, + }); + + const categoryDir = join(knowledgeDir, doc.category.replaceAll("/", "-")); + mkdirSync(categoryDir, { recursive: true }); + const filePath = join(categoryDir, doc.filename); + + const fm = generateFrontmatter( + stored.session_id, + doc.unitId, + { ...doc.frontmatter, pipeline: "consolidated" }, + author, + stored.project_id || undefined + ); + await Bun.write(filePath, fm + "\n\n" + doc.markdown); + + const shareId = crypto.randomUUID().slice(0, 8); + const shareHash = await hashContent( + JSON.stringify({ content: doc.markdown, category: doc.category, entities: doc.frontmatter.entities }) + ); + + db.prepare( + `INSERT INTO smriti_shares (id, session_id, category_id, project_id, author, content_hash, unit_id, relevance_score, entities) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + shareId, + stored.session_id, + doc.category, + stored.project_id, + author, + shareHash, + doc.unitId, + stored.relevance, + JSON.stringify(stored.entities) + ); + + const relPath = `knowledge/${doc.category.replaceAll("/", "-")}/${doc.filename}`; + promoteKnowledgeUnit(db, stored.id, relPath, shareId); + result.unitsPromoted++; + } catch (err: any) { + result.errors.push(`promote ${stored.id}: ${err.message}`); + } + } + + options.onProgress?.(`promote phase: ${result.unitsPromoted} units promoted`); + + return result; +} diff --git a/src/search/recall.ts b/src/search/recall.ts index 1e219a4..f94fe7b 100644 --- a/src/search/recall.ts +++ b/src/search/recall.ts @@ -9,6 +9,7 @@ import { DEFAULT_RECALL_LIMIT, OLLAMA_HOST, OLLAMA_MODEL } from "../config"; import { recallMemories, ollamaRecall } from "../qmd"; import { searchFiltered, type SearchFilters, type SearchResult } from "./index"; import { getQmdStore } from "../store"; +import { incrementRetrievalCount } from "../db"; // ============================================================================= // Types @@ -27,6 +28,24 @@ export type RecallResult = { synthesis?: string; }; +// ============================================================================= +// Retrieval Tracking +// ============================================================================= + +/** + * Best-effort bump of retrieval_count for any consolidated knowledge units + * belonging to the recalled sessions. Never lets a tracking failure break recall. + */ +function trackRetrieval(db: Database, results: SearchResult[]): void { + try { + for (const sessionId of new Set(results.map((r) => r.session_id).filter(Boolean))) { + incrementRetrievalCount(db, sessionId); + } + } catch { + // Never let this break recall. + } +} + // ============================================================================= // Filtered Recall // ============================================================================= @@ -58,6 +77,7 @@ export async function recall( if (options.synthesize && storeResults.length > 0) { synthesis = await synthesizeResults(query, storeResults, options); } + trackRetrieval(db, storeResults); return { results: storeResults, synthesis }; } @@ -70,6 +90,7 @@ export async function recall( fast: options.fast, intent: rerankIntent, }); + trackRetrieval(db, qmdResult.results); return { results: qmdResult.results, synthesis: qmdResult.synthesis, @@ -102,6 +123,7 @@ export async function recall( synthesis = await synthesizeResults(query, deduped, options); } + trackRetrieval(db, deduped); return { results: deduped, synthesis }; } diff --git a/src/team/share.ts b/src/team/share.ts index 06c44e5..0d61733 100644 --- a/src/team/share.ts +++ b/src/team/share.ts @@ -140,7 +140,7 @@ function querySessions( } /** Get messages for a session */ -function getSessionMessages( +export function getSessionMessages( db: Database, sessionId: string ): Array<{ diff --git a/test/learn-consolidate.test.ts b/test/learn-consolidate.test.ts new file mode 100644 index 0000000..8748d8c --- /dev/null +++ b/test/learn-consolidate.test.ts @@ -0,0 +1,283 @@ +/** + * test/learn-consolidate.test.ts - Tests for continuous knowledge consolidation + * + * Mirrors test/team-segmented.test.ts's style: initSmriti(":memory:"), a + * mocked global.fetch standing in for Ollama, and a scratch tmpDir for + * filesystem output (never process.cwd() — consolidateKnowledge writes real + * files). + */ + +import { test, expect, beforeAll, afterAll, mock } from "bun:test"; +import type { Database } from "bun:sqlite"; +import { mkdirSync, rmSync, writeFileSync, existsSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { + initSmriti, + closeDb, + upsertSessionMeta, + upsertProject, + updateDensityScore, + insertKnowledgeUnit, + listKnowledgeUnits, +} from "../src/db"; +import { consolidateKnowledge } from "../src/learn/consolidate"; +import { recall } from "../src/search/recall"; +import type { KnowledgeUnit } from "../src/team/types"; + +// ============================================================================= +// Setup +// ============================================================================= + +let db: Database; +let tmpDir: string; + +beforeAll(async () => { + db = await initSmriti(":memory:"); + tmpDir = join(tmpdir(), `smriti-consolidate-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); +}); + +afterAll(async () => { + await closeDb(); + try { rmSync(tmpDir, { recursive: true }); } catch {} +}); + +/** Insert a session with real message rows so getSessionMessages() finds content. */ +function seedSession(sessionId: string, projectId: string, messages: Array<{ role: string; content: string }>) { + const now = new Date().toISOString(); + db.prepare( + `INSERT INTO memory_sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)` + ).run(sessionId, `Session ${sessionId}`, now, now); + + const insertMsg = db.prepare( + `INSERT INTO memory_messages (session_id, role, content, hash, created_at) VALUES (?, ?, ?, ?, ?)` + ); + for (const [i, m] of messages.entries()) { + insertMsg.run(sessionId, m.role, m.content, `${sessionId}-h${i}`, now); + } + + upsertProject(db, projectId); + upsertSessionMeta(db, sessionId, "claude-code", projectId); +} + +const DENSE_CONVERSATION = [ + { role: "user", content: "I'm getting a JWT token expiry issue. Sessions timeout after 1 hour but tests expect 24 hours." }, + { role: "assistant", content: "Let me look at the auth middleware to understand the token expiry logic." }, + { role: "user", content: "Found it — src/auth.ts hardcodes 3600 seconds instead of reading JWT_TTL from the environment." }, + { role: "assistant", content: "Updated it to use process.env.JWT_TTL || 3600. Tests pass now." }, +]; + +/** Mock fetch that distinguishes Stage 1 (segmentation) vs Stage 2 (document) calls by prompt content. */ +function mockOllamaFetch(stage1Response: () => object) { + return mock(async (_url: string, init: any) => { + const body = JSON.parse(init.body); + const isStage1 = (body.prompt as string).includes("Knowledge Unit Segmentation"); + if (isStage1) { + return new Response( + JSON.stringify({ response: "```json\n" + JSON.stringify(stage1Response()) + "\n```" }), + { status: 200 } + ); + } + return new Response( + JSON.stringify({ response: "# Consolidated Doc\n\nPolished content." }), + { status: 200 } + ); + }); +} + +// ============================================================================= +// Segment-phase dedup +// ============================================================================= + +test("consolidate dedups knowledge units with identical content across sessions", async () => { + seedSession("dedup-s1", "dedupproj", DENSE_CONVERSATION); + seedSession("dedup-s2", "dedupproj", DENSE_CONVERSATION); + updateDensityScore(db, "dedup-s1", 0.9); + updateDensityScore(db, "dedup-s2", 0.9); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mockOllamaFetch(() => ({ + units: [{ topic: "JWT token expiry bug", category: "bug/fix", relevance: 9, entities: ["JWT"] }], + })) as any; + + try { + const result = await consolidateKnowledge(db, { + minDensity: 0.5, + outputDir: join(tmpDir, "dedup-output"), + }); + + expect(result.sessionsSegmented).toBe(2); + expect(result.unitsStored).toBe(1); + expect(result.unitsSkipped).toBe(1); + expect(result.errors).toEqual([]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +// ============================================================================= +// Promotion threshold +// ============================================================================= + +test("promote phase only promotes units clearing the retrieval/relevance bar", async () => { + const belowThreshold: KnowledgeUnit = { + id: "unit-below", + topic: "Minor formatting note", + category: "code/pattern", + relevance: 3, + entities: [], + files: [], + plainText: "Use consistent indentation.", + lineRanges: [{ start: 0, end: 1 }], + }; + const aboveThreshold: KnowledgeUnit = { + id: "unit-above", + topic: "Redis caching decision", + category: "architecture/decision", + relevance: 9, + entities: ["Redis"], + files: [], + plainText: "Use Redis with a 5-minute TTL for API responses.", + lineRanges: [{ start: 0, end: 1 }], + }; + + insertKnowledgeUnit(db, belowThreshold, "promote-s1", "promoteproj", "hash-below"); + insertKnowledgeUnit(db, aboveThreshold, "promote-s2", "promoteproj", "hash-above"); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mockOllamaFetch(() => ({ units: [] })) as any; + + try { + const result = await consolidateKnowledge(db, { + minDensity: 999, // no sessions qualify for the segment phase — isolates promote phase + minRetrievals: 3, + minRelevance: 8, + outputDir: join(tmpDir, "promote-output"), + }); + + expect(result.unitsPromoted).toBe(1); + expect(result.errors).toEqual([]); + + const canonical = listKnowledgeUnits(db, { tier: "canonical" }); + expect(canonical.map((u) => u.id)).toContain("unit-above"); + expect(canonical.map((u) => u.id)).not.toContain("unit-below"); + + const promoted = canonical.find((u) => u.id === "unit-above")!; + expect(promoted.canonical_doc_path).toContain("architecture-decision"); + expect(promoted.share_id).toBeTruthy(); + + const shareRow = db + .prepare(`SELECT * FROM smriti_shares WHERE unit_id = ?`) + .get("unit-above") as any; + expect(shareRow).toBeTruthy(); + expect(shareRow.session_id).toBe("promote-s2"); + + const writtenFile = join(tmpDir, "promote-output", promoted.canonical_doc_path!); + expect(existsSync(writtenFile)).toBe(true); + + const stillSegmented = listKnowledgeUnits(db, { tier: "segmented" }); + expect(stillSegmented.map((u) => u.id)).toContain("unit-below"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +// ============================================================================= +// Graceful degradation +// ============================================================================= + +test("promote phase continues past a per-unit failure and records the error", async () => { + // Pre-create a *file* at the exact path the loop will try to mkdir for + // category "bug/fix" (slug "bug-fix"), forcing that unit's write to throw. + const outputDir = join(tmpDir, "degrade-output"); + const knowledgeDir = join(outputDir, "knowledge"); + mkdirSync(knowledgeDir, { recursive: true }); + writeFileSync(join(knowledgeDir, "bug-fix"), "occupied"); + + const willFail: KnowledgeUnit = { + id: "unit-fails", + topic: "Broken unit", + category: "bug/fix", + relevance: 9, + entities: [], + files: [], + plainText: "This unit's category dir collides with a file.", + lineRanges: [{ start: 0, end: 1 }], + }; + const willSucceed: KnowledgeUnit = { + id: "unit-succeeds", + topic: "Working unit", + category: "topic/learning", + relevance: 9, + entities: [], + files: [], + plainText: "This unit writes fine.", + lineRanges: [{ start: 0, end: 1 }], + }; + + insertKnowledgeUnit(db, willFail, "degrade-s1", "degradeproj", "hash-fails"); + insertKnowledgeUnit(db, willSucceed, "degrade-s2", "degradeproj", "hash-succeeds"); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mockOllamaFetch(() => ({ units: [] })) as any; + + try { + const result = await consolidateKnowledge(db, { + minDensity: 999, + minRetrievals: 999, // exclude leftover segmented units from earlier tests; only relevance-9 units below qualify + minRelevance: 8, + outputDir, + }); + + expect(result.unitsPromoted).toBe(1); + expect(result.errors.length).toBe(1); + expect(result.errors[0]).toContain("unit-fails"); + + const canonical = listKnowledgeUnits(db, { tier: "canonical" }); + expect(canonical.map((u) => u.id)).toContain("unit-succeeds"); + expect(canonical.map((u) => u.id)).not.toContain("unit-fails"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +// ============================================================================= +// Retrieval tracking (recall -> incrementRetrievalCount) +// ============================================================================= + +test("recall increments retrieval_count for knowledge units of the recalled session", async () => { + seedSession("track-s1", "trackproj", [ + { role: "user", content: "How do we configure the rate limiter for the public API?" }, + { role: "assistant", content: "Use a token bucket with 100 requests per minute per API key." }, + ]); + + const unit: KnowledgeUnit = { + id: "unit-tracked", + topic: "Rate limiter config", + category: "code/pattern", + relevance: 7, + entities: [], + files: [], + plainText: "Token bucket rate limiting for the public API.", + lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, unit, "track-s1", "trackproj", "hash-tracked"); + + await recall(db, "rate limiter", { project: "trackproj" }); + + const tracked = db + .prepare(`SELECT retrieval_count FROM smriti_knowledge_units WHERE id = ?`) + .get("unit-tracked") as { retrieval_count: number }; + expect(tracked).toBeDefined(); + expect(tracked.retrieval_count).toBeGreaterThanOrEqual(1); +}); + +test("recall does not throw for sessions with no consolidated knowledge units", async () => { + seedSession("untracked-s1", "untrackedproj", [ + { role: "user", content: "What's our deploy process look like?" }, + { role: "assistant", content: "Push to main triggers the CI pipeline and auto-deploys." }, + ]); + + await expect(recall(db, "deploy process", { project: "untrackedproj" })).resolves.toBeDefined(); +}); From 933d53b7092ad170a1268dffa70c1994b990434f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 16:14:19 +0000 Subject: [PATCH 05/13] feat(learn): add RDF-inspired entities + relationships graph layer Extends the Continuous Knowledge Consolidation Layer with canonical entities and typed subject-predicate-object relationship triples, inspired by RDF's data model (adapted pragmatically: no URIs/SPARQL, just typed (subject_type, subject_id) pairs in SQLite). - src/db.ts: smriti_entities + smriti_relationships tables; new `minEntityReach` promotion criterion in findPromotableUnits (a unit becomes promotable once its entity is independently mentioned by K other units, even at 0 retrievals); seeds a "team" agent (fixes a latent FK bug in syncTeamKnowledge's existing agent fallback) - src/learn/entities.ts: resolveEntity (exact-normalize canonicalization via slugify), insertRelationship/getRelationships (triple store), findRelatedCandidates, findEntity, getUnitsForEntity - src/learn/consolidate.ts: segment phase turns Stage-1 entities into "mentions" edges for free; promote phase adds one bounded LLM call to infer relatesTo/supersedes/contradicts edges against entity-sharing candidates, persisting what ollamaCheckConflicts previously only computed ephemerally - src/team/config.ts, share.ts, sync.ts: entities propagate team/org-wide through the same .smriti/config.json round-trip custom categories already use (exportEntities/mergeEntities mirror exportCustomCategories/mergeCategories); unit-to-unit relationship edges propagate via frontmatter directly, needing no canonicalization since unit ids are already portable UUIDs. Also fixes sync.ts treating "consolidated" pipeline docs as raw conversation transcripts (only "segmented" was previously recognized as single-message). - src/index.ts, src/format.ts: `smriti graph ` command; `--min-entity-reach` flag on `smriti consolidate` Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PnYgLUDwWALu1bUAgrFQDG --- src/db.ts | 66 +++++- src/format.ts | 47 +++++ src/index.ts | 35 ++++ src/learn/consolidate.ts | 142 ++++++++++++- src/learn/entities.ts | 232 ++++++++++++++++++++ src/team/config.ts | 78 +++++++ src/team/share.ts | 8 +- src/team/sync.ts | 43 +++- test/learn-entities-sync.test.ts | 166 +++++++++++++++ test/learn-entities.test.ts | 349 +++++++++++++++++++++++++++++++ 10 files changed, 1152 insertions(+), 14 deletions(-) create mode 100644 src/learn/entities.ts create mode 100644 test/learn-entities-sync.test.ts create mode 100644 test/learn-entities.test.ts diff --git a/src/db.ts b/src/db.ts index b04bd3e..048cb55 100644 --- a/src/db.ts +++ b/src/db.ts @@ -232,6 +232,40 @@ export function initializeSmritiTables(db: Database): void { CREATE INDEX IF NOT EXISTS idx_smriti_knowledge_units_hash ON smriti_knowledge_units(content_hash); CREATE INDEX IF NOT EXISTS idx_smriti_knowledge_units_tier ON smriti_knowledge_units(tier); + -- Canonical entity registry: resolves free-text entity mentions (from Stage 1 + -- extraction) onto a stable node, so recurrence is detected regardless of wording. + -- Propagated team/org-wide via .smriti/config.json, same mechanism as custom categories. + CREATE TABLE IF NOT EXISTS smriti_entities ( + id TEXT PRIMARY KEY, -- slug, e.g. "jwt", "redis" + label TEXT NOT NULL, -- canonical display name + entity_type TEXT NOT NULL DEFAULT 'concept', -- 'technology' | 'concept' | 'file' | 'pattern' + aliases TEXT NOT NULL DEFAULT '[]', -- JSON array of raw strings seen + mention_count INTEGER NOT NULL DEFAULT 0, + first_seen_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_smriti_entities_label ON smriti_entities(label); + + -- Relationship triples (subject/object polymorphic via type+id, not literal RDF URIs). + -- knowledge_unit -mentions-> entity edges come free from Stage-1 extraction; + -- knowledge_unit -relatesTo/supersedes/contradicts-> knowledge_unit edges are LLM-gated, + -- only at promotion time (see src/learn/consolidate.ts), persisting what + -- ollamaCheckConflicts previously only computed ephemerally. + CREATE TABLE IF NOT EXISTS smriti_relationships ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + subject_type TEXT NOT NULL, -- 'knowledge_unit' | 'entity' | 'session' + subject_id TEXT NOT NULL, + predicate TEXT NOT NULL, -- 'mentions' | 'relatesTo' | 'supersedes' | 'contradicts' + object_type TEXT NOT NULL, + object_id TEXT NOT NULL, + confidence REAL DEFAULT 1.0, + source TEXT DEFAULT 'extraction', -- 'extraction' | 'derived' | 'llm' + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(subject_type, subject_id, predicate, object_type, object_id) + ); + CREATE INDEX IF NOT EXISTS idx_smriti_relationships_subject ON smriti_relationships(subject_type, subject_id); + CREATE INDEX IF NOT EXISTS idx_smriti_relationships_object ON smriti_relationships(object_type, object_id); + CREATE INDEX IF NOT EXISTS idx_smriti_relationships_predicate ON smriti_relationships(predicate); + -- Tool usage tracking CREATE TABLE IF NOT EXISTS smriti_tool_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -503,6 +537,12 @@ const DEFAULT_AGENTS = [ log_pattern: null, parser: "claude-web", }, + { + id: "team", + display_name: "Team Import", + log_pattern: null, + parser: "generic", + }, ] as const; /** Default category taxonomy */ @@ -1389,14 +1429,34 @@ export function findUnsegmentedDenseSessions( export function findPromotableUnits( db: Database, minRetrievals: number, - minRelevance: number + minRelevance: number, + minEntityReach?: number ): StoredKnowledgeUnit[] { + // minEntityReach: a unit is promotable if one of its entities is + // independently mentioned by >= minEntityReach OTHER units — a structural + // reuse signal (cross-session recurrence) that doesn't depend on recall() + // ever having been called on this particular unit. + const entityReachClause = minEntityReach + ? `OR id IN ( + SELECT r1.subject_id FROM smriti_relationships r1 + JOIN smriti_relationships r2 + ON r1.object_id = r2.object_id AND r2.predicate = 'mentions' + AND r1.predicate = 'mentions' AND r1.subject_id != r2.subject_id + WHERE r1.subject_type = 'knowledge_unit' + GROUP BY r1.subject_id + HAVING COUNT(DISTINCT r2.subject_id) >= ? + )` + : ""; + const params = minEntityReach + ? [minRetrievals, minRelevance, minEntityReach] + : [minRetrievals, minRelevance]; + const rows = db .prepare( `SELECT * FROM smriti_knowledge_units - WHERE tier = 'segmented' AND (retrieval_count >= ? OR relevance >= ?)` + WHERE tier = 'segmented' AND (retrieval_count >= ? OR relevance >= ? ${entityReachClause})` ) - .all(minRetrievals, minRelevance) as KnowledgeUnitRow[]; + .all(...params) as KnowledgeUnitRow[]; return rows.map(deserializeKnowledgeUnit); } diff --git a/src/format.ts b/src/format.ts index e414186..f78280f 100644 --- a/src/format.ts +++ b/src/format.ts @@ -325,6 +325,49 @@ export function formatLearnings( return table(headers, rows, [14, 40, 20, 10, 9, 40]); } +// ============================================================================= +// Entity Graph Formatting (smriti graph ) +// ============================================================================= + +export function formatEntityGraph( + entity: { id: string; label: string; entity_type: string; aliases: string[]; mention_count: number }, + units: Array<{ id: string; topic: string; category: string; relevance: number; tier: string; retrieval_count: number }>, + edges: Array<{ subject_id: string; predicate: string; object_id: string }> +): string { + const lines = [ + `Entity: ${entity.label} (${entity.id})`, + `Type: ${entity.entity_type}`, + `Aliases: ${entity.aliases.join(", ") || "-"}`, + `Mentioned ${entity.mention_count} time(s) across ${units.length} unit(s)`, + "", + ]; + + if (units.length === 0) { + lines.push("No knowledge units mention this entity yet."); + return lines.join("\n"); + } + + const headers = ["Tier", "Topic", "Category", "Retrievals", "Relevance"]; + const rows = units.map((u) => [ + u.tier === "canonical" ? "✓ canonical" : "segmented", + u.topic, + u.category, + String(u.retrieval_count), + u.relevance.toFixed(1), + ]); + lines.push(table(headers, rows, [14, 40, 20, 10, 9])); + + const nonMentionEdges = edges.filter((e) => e.predicate !== "mentions"); + if (nonMentionEdges.length > 0) { + lines.push("", "Relationships between these units:"); + for (const e of nonMentionEdges) { + lines.push(` ${e.subject_id} --${e.predicate}--> ${e.object_id}`); + } + } + + return lines.join("\n"); +} + // ============================================================================= // Sync Result Formatting // ============================================================================= @@ -335,6 +378,7 @@ export function formatSyncResult(result: { skipped: number; errors: string[]; categoriesImported?: number; + entitiesImported?: number; }): string { const lines = [ `Files processed: ${result.filesProcessed}`, @@ -344,6 +388,9 @@ export function formatSyncResult(result: { if (result.categoriesImported && result.categoriesImported > 0) { lines.push(`Categories imported: ${result.categoriesImported}`); } + if (result.entitiesImported && result.entitiesImported > 0) { + lines.push(`Entities imported: ${result.entitiesImported}`); + } if (result.errors.length > 0) { lines.push(`Errors: ${result.errors.length}`); diff --git a/src/index.ts b/src/index.ts index ecea67b..f5c8956 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { recall } from "./search/recall"; import { shareKnowledge } from "./team/share"; import { syncTeamKnowledge, listTeamContributions } from "./team/sync"; import { consolidateKnowledge } from "./learn/consolidate"; +import { findEntity, getUnitsForEntity, getRelationships } from "./learn/entities"; import { generateContext, compareSessions, @@ -56,6 +57,7 @@ import { formatDigest, formatConsolidateResult, formatLearnings, + formatEntityGraph, json, } from "./format"; import { generateDigest } from "./digest"; @@ -218,6 +220,7 @@ Commands: share [filters] Export knowledge to .smriti/ consolidate [options] Segment dense sessions, promote reused knowledge units learnings [options] List extracted knowledge units (tier, retrievals, relevance) + graph Show a canonical entity's mentions and relationship edges sync Import team knowledge from .smriti/ team View team contributions list [filters] List sessions @@ -825,6 +828,7 @@ async function main() { minDensity: Number(getArg(args, "--min-density")) || undefined, minRetrievals: Number(getArg(args, "--min-retrievals")) || undefined, minRelevance: Number(getArg(args, "--min-relevance")) || undefined, + minEntityReach: Number(getArg(args, "--min-entity-reach")) || undefined, model: getArg(args, "--model"), outputDir: getArg(args, "--output"), sessionLimit: Number(getArg(args, "--session-limit")) || undefined, @@ -853,6 +857,37 @@ async function main() { break; } + // ===================================================================== + // GRAPH + // ===================================================================== + case "graph": { + const query = getPositional(args, 1); + if (!query) { + console.error("Usage: smriti graph "); + process.exit(1); + } + + const entity = findEntity(db, query); + if (!entity) { + console.log(`No entity found matching "${query}".`); + break; + } + + const units = getUnitsForEntity(db, entity.id); + const unitIds = new Set(units.map((u) => u.id)); + const edges = units.flatMap((u) => + getRelationships(db, { subjectType: "knowledge_unit", subjectId: u.id }) + .filter((r) => r.predicate !== "mentions" && unitIds.has(r.object_id)) + ); + + if (hasFlag(args, "--json")) { + console.log(json({ entity, units, edges })); + } else { + console.log(formatEntityGraph(entity, units, edges)); + } + break; + } + // ===================================================================== // SYNC // ===================================================================== diff --git a/src/learn/consolidate.ts b/src/learn/consolidate.ts index b6d973b..38d9a94 100644 --- a/src/learn/consolidate.ts +++ b/src/learn/consolidate.ts @@ -29,8 +29,16 @@ import { getSessionMessages } from "../team/share"; import { segmentSession } from "../team/segment"; import { generateDocument, generateFrontmatter } from "../team/document"; import { isSessionWorthSharing } from "../team/formatter"; +import { callOllama } from "../team/ollama"; import type { RawMessage } from "../team/formatter"; import type { KnowledgeUnit } from "../team/types"; +import { + resolveEntity, + insertRelationship, + getRelationships, + findRelatedCandidates, + type RelationshipPredicate, +} from "./entities"; // ============================================================================= // Types @@ -40,6 +48,7 @@ export type ConsolidateOptions = { minDensity?: number; minRetrievals?: number; minRelevance?: number; + minEntityReach?: number; model?: string; outputDir?: string; author?: string; @@ -107,6 +116,19 @@ export async function consolidateKnowledge( ); const inserted = insertKnowledgeUnit(db, unit, s.session_id, s.project_id, contentHash); inserted ? result.unitsStored++ : result.unitsSkipped++; + + // Turn Stage 1's free-text entities into canonical "mentions" edges — + // pure post-processing of data already extracted, no extra LLM calls. + if (inserted) { + for (const rawEntity of unit.entities) { + const entityId = resolveEntity(db, rawEntity); + if (entityId) { + insertRelationship(db, "knowledge_unit", unit.id, "mentions", "entity", entityId, { + source: "extraction", + }); + } + } + } } } catch (err: any) { result.errors.push(`segment ${s.session_id}: ${err.message}`); @@ -127,7 +149,8 @@ export async function consolidateKnowledge( const promotable = findPromotableUnits( db, options.minRetrievals ?? 3, - options.minRelevance ?? 8 + options.minRelevance ?? 8, + options.minEntityReach ); for (const stored of promotable) { @@ -143,6 +166,15 @@ export async function consolidateKnowledge( lineRanges: stored.line_ranges, }; + // Bounded relationship inference: only runs if this unit shares a + // canonical entity with at least one other unit, and costs exactly one + // extra LLM call (same cost discipline as Stage 2) — persists what + // ollamaCheckConflicts previously only computed ephemerally. + const candidates = findRelatedCandidates(db, stored.id, 5); + if (candidates.length > 0) { + await inferRelationships(db, unit, candidates, options.model); + } + const doc = await generateDocument(unit, stored.topic, { model: options.model, projectSmritiDir: outputDir, @@ -153,10 +185,31 @@ export async function consolidateKnowledge( mkdirSync(categoryDir, { recursive: true }); const filePath = join(categoryDir, doc.filename); + // Carry canonical entity ids + unit-to-unit edges into shared + // frontmatter. Unlike entity ids, these edges need no team-level + // canonicalization step — unit.id is already a portable UUID once + // shared (see src/team/document.ts's frontmatter `id` field), so + // syncTeamKnowledge can re-create them on a teammate's machine as-is. + const entityIds = getRelationships(db, { + subjectType: "knowledge_unit", + subjectId: stored.id, + predicate: "mentions", + objectType: "entity", + }).map((r) => r.object_id); + const outgoingEdges = getRelationships(db, { + subjectType: "knowledge_unit", + subjectId: stored.id, + }).filter((r) => r.predicate !== "mentions"); + const fm = generateFrontmatter( stored.session_id, doc.unitId, - { ...doc.frontmatter, pipeline: "consolidated" }, + { + ...doc.frontmatter, + pipeline: "consolidated", + entity_ids: entityIds, + ...groupEdgesByPredicate(outgoingEdges), + }, author, stored.project_id || undefined ); @@ -194,3 +247,88 @@ export async function consolidateKnowledge( return result; } + +// ============================================================================= +// Relationship Inference (promote-time, LLM-gated) +// ============================================================================= + +const RELATION_LINE = /RELATION\s*\[(\d+)\]:\s*(relatesTo|supersedes|contradicts|none)/gi; +const MAX_EXCERPT_CHARS = 800; + +// Case-insensitive regex match -> canonical camelCase predicate (avoid a blind +// .toLowerCase() on the match, which would turn "relatesTo" into "relatesto"). +const PREDICATE_BY_LOWERCASE: Record = { + relatesto: "relatesTo", + supersedes: "supersedes", + contradicts: "contradicts", +}; + +function truncate(text: string, max: number): string { + return text.length > max ? text.slice(0, max) + "…" : text; +} + +/** + * Ask the LLM whether the unit being promoted relatesTo/supersedes/contradicts + * any of its entity-sharing candidates, and persist the answer as edges. + * Best-effort: a failure here (LLM down, unparseable response) is swallowed — + * it's enrichment on top of promotion, not a precondition for it. + */ +async function inferRelationships( + db: Database, + unit: KnowledgeUnit, + candidates: Array<{ id: string; topic: string; category: string; plain_text: string }>, + model?: string +): Promise { + try { + const candidateBlock = candidates + .map((c, i) => `[${i}] Topic: ${c.topic}\nCategory: ${c.category}\nContent: ${truncate(c.plain_text, MAX_EXCERPT_CHARS)}`) + .join("\n\n"); + + const prompt = `You are comparing a NEW knowledge unit against CANDIDATE units that already mention at least one of the same topics/entities. + +NEW UNIT +Topic: ${unit.topic} +Category: ${unit.category} +Content: ${truncate(unit.plainText, MAX_EXCERPT_CHARS)} + +CANDIDATES +${candidateBlock} + +For each candidate, decide the relationship of the NEW unit to it: +- relatesTo: related but neither replaces nor conflicts with the other +- supersedes: the NEW unit replaces/updates the candidate's guidance +- contradicts: the NEW unit conflicts with the candidate +- none: no meaningful relationship + +Respond with exactly one line per candidate, in this format: +RELATION [i]: relatesTo|supersedes|contradicts|none`; + + const response = await callOllama(prompt, { model }); + + for (const match of response.matchAll(RELATION_LINE)) { + const index = Number(match[1]); + const raw = match[2].toLowerCase(); + if (raw === "none") continue; + const predicate = PREDICATE_BY_LOWERCASE[raw]; + const candidate = candidates[index]; + if (!predicate || !candidate) continue; + + insertRelationship(db, "knowledge_unit", unit.id, predicate, "knowledge_unit", candidate.id, { + source: "llm", + }); + } + } catch { + // Enrichment only — never block promotion on a failed/unparseable relation call. + } +} + +/** Group outgoing relationship edges by predicate into frontmatter-ready arrays of object unit ids. */ +function groupEdgesByPredicate( + edges: Array<{ predicate: string; object_id: string }> +): Record { + const grouped: Record = {}; + for (const edge of edges) { + (grouped[edge.predicate] ??= []).push(edge.object_id); + } + return grouped; +} diff --git a/src/learn/entities.ts b/src/learn/entities.ts new file mode 100644 index 0000000..474c944 --- /dev/null +++ b/src/learn/entities.ts @@ -0,0 +1,232 @@ +/** + * learn/entities.ts - Canonical entity resolution + relationship triples + * + * RDF-inspired, not literal RDF: no URIs/Turtle/SPARQL. Subject/object are + * (type, id) pairs instead of global URIs, since Smriti is a local SQLite + * tool, not a web-facing linked-data endpoint. The useful ideas kept are: + * stable resource identity (so recurrence is detected across wording + * variance) and typed subject-predicate-object facts that survive team + * sharing (see src/team/config.ts's exportEntities/mergeEntities and + * src/team/document.ts's frontmatter for how these propagate org-wide). + * + * v1 entity resolution is exact-normalize only (case/whitespace/punctuation + * via slugify) — "JWT" and "jwt" merge, "JWT" and "JSON Web Token" do not. + * True synonym resolution needs semantic matching and is out of scope here. + */ + +import type { Database } from "bun:sqlite"; +import { slugify } from "../team/utils"; + +// ============================================================================= +// Types +// ============================================================================= + +export type EntityType = "technology" | "concept" | "file" | "pattern"; +export type RelationshipPredicate = "mentions" | "relatesTo" | "supersedes" | "contradicts"; +export type RelationshipSubjectType = "knowledge_unit" | "entity" | "session"; +export type RelationshipSource = "extraction" | "derived" | "llm"; + +export type StoredEntity = { + id: string; + label: string; + entity_type: EntityType; + aliases: string[]; + mention_count: number; + first_seen_at: string; +}; + +export type StoredRelationship = { + id: number; + subject_type: RelationshipSubjectType; + subject_id: string; + predicate: RelationshipPredicate; + object_type: RelationshipSubjectType; + object_id: string; + confidence: number; + source: RelationshipSource; + created_at: string; +}; + +type EntityRow = { + id: string; + label: string; + entity_type: string; + aliases: string; + mention_count: number; + first_seen_at: string; +}; + +function deserializeEntity(row: EntityRow): StoredEntity { + return { + ...row, + entity_type: row.entity_type as EntityType, + aliases: JSON.parse(row.aliases), + }; +} + +// ============================================================================= +// Entity Resolution +// ============================================================================= + +/** + * Resolve a raw, free-text entity label to a canonical entity id, creating + * the entity if it doesn't exist yet. Matching is exact-normalize (via + * slugify) — same case/whitespace variant collapses to one node; different + * wordings for the same concept do not (see module docstring). + */ +export function resolveEntity( + db: Database, + rawLabel: string, + entityType: EntityType = "concept" +): string | null { + const trimmed = rawLabel.trim(); + if (!trimmed) return null; + + const id = slugify(trimmed); + if (!id) return null; + + const existing = db + .prepare(`SELECT aliases FROM smriti_entities WHERE id = ?`) + .get(id) as { aliases: string } | null; + + if (existing) { + const aliases: string[] = JSON.parse(existing.aliases); + if (!aliases.includes(trimmed)) { + aliases.push(trimmed); + db.prepare( + `UPDATE smriti_entities SET aliases = ?, mention_count = mention_count + 1 WHERE id = ?` + ).run(JSON.stringify(aliases), id); + } else { + db.prepare(`UPDATE smriti_entities SET mention_count = mention_count + 1 WHERE id = ?`).run(id); + } + return id; + } + + db.prepare( + `INSERT INTO smriti_entities (id, label, entity_type, aliases, mention_count) + VALUES (?, ?, ?, ?, 1)` + ).run(id, trimmed, entityType, JSON.stringify([trimmed])); + return id; +} + +export function getEntity(db: Database, id: string): StoredEntity | null { + const row = db.prepare(`SELECT * FROM smriti_entities WHERE id = ?`).get(id) as EntityRow | null; + return row ? deserializeEntity(row) : null; +} + +/** Look up an entity by exact id, or by slugified/label match against a raw query string. */ +export function findEntity(db: Database, query: string): StoredEntity | null { + const bySlug = getEntity(db, slugify(query)); + if (bySlug) return bySlug; + + const row = db + .prepare(`SELECT * FROM smriti_entities WHERE LOWER(label) = LOWER(?)`) + .get(query.trim()) as EntityRow | null; + return row ? deserializeEntity(row) : null; +} + +/** Knowledge units that `mentions` a given canonical entity — the display side of `smriti graph `. */ +export function getUnitsForEntity( + db: Database, + entityId: string +): Array<{ id: string; topic: string; category: string; relevance: number; tier: string; retrieval_count: number }> { + return db + .prepare( + `SELECT ku.id, ku.topic, ku.category, ku.relevance, ku.tier, ku.retrieval_count + FROM smriti_relationships r + JOIN smriti_knowledge_units ku ON ku.id = r.subject_id + WHERE r.subject_type = 'knowledge_unit' AND r.object_type = 'entity' + AND r.predicate = 'mentions' AND r.object_id = ? + ORDER BY ku.retrieval_count DESC, ku.relevance DESC` + ) + .all(entityId) as Array<{ + id: string; topic: string; category: string; relevance: number; tier: string; retrieval_count: number; + }>; +} + +export function listEntities(db: Database, limit?: number): StoredEntity[] { + const rows = ( + limit + ? db.prepare(`SELECT * FROM smriti_entities ORDER BY mention_count DESC LIMIT ?`).all(limit) + : db.prepare(`SELECT * FROM smriti_entities ORDER BY mention_count DESC`).all() + ) as EntityRow[]; + return rows.map(deserializeEntity); +} + +// ============================================================================= +// Relationship Triples +// ============================================================================= + +/** Insert a (subject, predicate, object) triple. Deduped via the table's UNIQUE constraint. */ +export function insertRelationship( + db: Database, + subjectType: RelationshipSubjectType, + subjectId: string, + predicate: RelationshipPredicate, + objectType: RelationshipSubjectType, + objectId: string, + options: { confidence?: number; source?: RelationshipSource } = {} +): void { + db.prepare( + `INSERT OR IGNORE INTO smriti_relationships + (subject_type, subject_id, predicate, object_type, object_id, confidence, source) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ).run( + subjectType, + subjectId, + predicate, + objectType, + objectId, + options.confidence ?? 1.0, + options.source ?? "extraction" + ); +} + +export type TriplePattern = { + subjectType?: RelationshipSubjectType; + subjectId?: string; + predicate?: RelationshipPredicate; + objectType?: RelationshipSubjectType; + objectId?: string; +}; + +/** Single-pattern triple lookup — the basic-graph-pattern piece of SPARQL, simplified to one triple at a time. */ +export function getRelationships(db: Database, pattern: TriplePattern): StoredRelationship[] { + const conditions: string[] = []; + const params: any[] = []; + + if (pattern.subjectType) { conditions.push("subject_type = ?"); params.push(pattern.subjectType); } + if (pattern.subjectId) { conditions.push("subject_id = ?"); params.push(pattern.subjectId); } + if (pattern.predicate) { conditions.push("predicate = ?"); params.push(pattern.predicate); } + if (pattern.objectType) { conditions.push("object_type = ?"); params.push(pattern.objectType); } + if (pattern.objectId) { conditions.push("object_id = ?"); params.push(pattern.objectId); } + + const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + return db.prepare(`SELECT * FROM smriti_relationships ${where}`).all(...params) as StoredRelationship[]; +} + +/** + * Find other knowledge units that `mentions` at least one of the same + * canonical entities as `unitId` — the candidate set for promote-time + * LLM relationship inference (bounded, so that call stays cheap). + */ +export function findRelatedCandidates( + db: Database, + unitId: string, + limit: number = 5 +): Array<{ id: string; topic: string; category: string; plain_text: string }> { + return db + .prepare( + `SELECT DISTINCT ku.id, ku.topic, ku.category, ku.plain_text + FROM smriti_relationships r1 + JOIN smriti_relationships r2 + ON r1.object_id = r2.object_id + AND r2.object_type = 'entity' AND r2.predicate = 'mentions' + JOIN smriti_knowledge_units ku ON ku.id = r2.subject_id + WHERE r1.subject_type = 'knowledge_unit' AND r1.subject_id = ? + AND r1.object_type = 'entity' AND r1.predicate = 'mentions' + AND r2.subject_id != r1.subject_id + LIMIT ?` + ) + .all(unitId, limit) as Array<{ id: string; topic: string; category: string; plain_text: string }>; +} diff --git a/src/team/config.ts b/src/team/config.ts index 8dddcbc..13c0efe 100644 --- a/src/team/config.ts +++ b/src/team/config.ts @@ -17,9 +17,17 @@ export type CustomCategoryDef = { description?: string; }; +export type CustomEntityDef = { + id: string; + label: string; + entity_type: string; + aliases: string[]; +}; + export type SmritiConfig = { version: number; categories?: CustomCategoryDef[]; + entities?: CustomEntityDef[]; allowedCategories?: string[]; autoSync?: boolean; }; @@ -115,3 +123,73 @@ export function exportCustomCategories(db: Database): CustomCategoryDef[] { ...(r.description ? { description: r.description } : {}), })); } + +// ============================================================================= +// Entity Merge — team/org propagation for smriti_entities +// +// Same reasoning as categories: an entity's canonical id is only meaningful +// if every teammate's machine agrees on it. .smriti/config.json is the +// git-committed source of truth each local smriti_entities table converges +// toward on every share/sync — the same role a published vocabulary plays +// for literal RDF, minus the URIs. +// ============================================================================= + +/** + * Upsert entities from config into the local DB. Matches first by id, then + * falls back to a normalized-label match (so two machines that independently + * minted different ids for the same concept still converge once either + * syncs the shared file). Unions aliases on conflict rather than overwriting. + * Returns count of newly created entities. + */ +export function mergeEntities(db: Database, entities: CustomEntityDef[]): number { + if (entities.length === 0) return 0; + + let created = 0; + for (const entity of entities) { + const existingById = db + .prepare(`SELECT id, aliases FROM smriti_entities WHERE id = ?`) + .get(entity.id) as { id: string; aliases: string } | null; + + if (existingById) { + const aliases = new Set(JSON.parse(existingById.aliases)); + for (const a of entity.aliases) aliases.add(a); + db.prepare(`UPDATE smriti_entities SET aliases = ? WHERE id = ?`) + .run(JSON.stringify([...aliases]), entity.id); + continue; + } + + const normalizedLabel = entity.label.trim().toLowerCase(); + const existingByLabel = db + .prepare(`SELECT id, aliases FROM smriti_entities WHERE LOWER(label) = ?`) + .get(normalizedLabel) as { id: string; aliases: string } | null; + + if (existingByLabel) { + const aliases = new Set(JSON.parse(existingByLabel.aliases)); + for (const a of entity.aliases) aliases.add(a); + db.prepare(`UPDATE smriti_entities SET aliases = ? WHERE id = ?`) + .run(JSON.stringify([...aliases]), existingByLabel.id); + continue; + } + + db.prepare( + `INSERT INTO smriti_entities (id, label, entity_type, aliases, mention_count) + VALUES (?, ?, ?, ?, 0)` + ).run(entity.id, entity.label, entity.entity_type, JSON.stringify(entity.aliases)); + created++; + } + return created; +} + +/** Query smriti_entities and return as config defs for export to .smriti/config.json. */ +export function exportEntities(db: Database): CustomEntityDef[] { + const rows = db + .prepare(`SELECT id, label, entity_type, aliases FROM smriti_entities`) + .all() as Array<{ id: string; label: string; entity_type: string; aliases: string }>; + + return rows.map((r) => ({ + id: r.id, + label: r.label, + entity_type: r.entity_type, + aliases: JSON.parse(r.aliases), + })); +} diff --git a/src/team/share.ts b/src/team/share.ts index 0d61733..22adef8 100644 --- a/src/team/share.ts +++ b/src/team/share.ts @@ -10,7 +10,7 @@ import { SMRITI_DIR, AUTHOR } from "../config"; import { hashContent } from "../qmd"; import { existsSync, mkdirSync } from "fs"; import { join } from "path"; -import { readConfig, writeConfig, exportCustomCategories } from "./config"; +import { readConfig, writeConfig, exportCustomCategories, exportEntities } from "./config"; import { formatSessionAsFallback, isSessionWorthSharing, @@ -178,15 +178,17 @@ async function writeManifest( const fullManifest = [...existingManifest, ...newEntries]; await Bun.write(indexPath, JSON.stringify(fullManifest, null, 2)); - // Write config — always update with latest custom categories + // Write config — always update with latest custom categories + canonical entities const existing = readConfig(outputDir); const customCategories = db ? exportCustomCategories(db) : []; + const entities = db ? exportEntities(db) : []; const config = { ...existing, - version: customCategories.length > 0 ? 2 : (existing.version ?? 1), + version: customCategories.length > 0 || entities.length > 0 ? 2 : (existing.version ?? 1), allowedCategories: existing.allowedCategories ?? ["*"], autoSync: existing.autoSync ?? false, ...(customCategories.length > 0 ? { categories: customCategories } : {}), + ...(entities.length > 0 ? { entities } : {}), }; await writeConfig(outputDir, config); diff --git a/src/team/sync.ts b/src/team/sync.ts index fdb9d5a..b4e51cc 100644 --- a/src/team/sync.ts +++ b/src/team/sync.ts @@ -9,7 +9,8 @@ import type { Database } from "bun:sqlite"; import { SMRITI_DIR } from "../config"; import { addMessage, hashContent } from "../qmd"; import { join } from "path"; -import { readConfig, mergeCategories } from "./config"; +import { readConfig, mergeCategories, mergeEntities } from "./config"; +import { insertRelationship, type RelationshipPredicate } from "../learn/entities"; // ============================================================================= // Types @@ -26,6 +27,7 @@ export type SyncResult = { skipped: number; errors: string[]; categoriesImported: number; + entitiesImported: number; }; // ============================================================================= @@ -116,6 +118,7 @@ export async function syncTeamKnowledge( skipped: 0, errors: [], categoriesImported: 0, + entitiesImported: 0, }; // Determine input directory @@ -144,11 +147,16 @@ export async function syncTeamKnowledge( ).map((r) => r.content_hash) ); - // Import custom categories from config.json (v2+) before scanning files + // Import custom categories + canonical entities from config.json (v2+) + // before scanning files — entities must exist locally before per-file + // "mentions"/relationship edges below can reference them. const config = readConfig(inputDir); if (config.categories && config.categories.length > 0) { result.categoriesImported = mergeCategories(db, config.categories); } + if (config.entities && config.entities.length > 0) { + result.entitiesImported = mergeEntities(db, config.entities); + } // Scan for markdown files const knowledgeDir = join(inputDir, "knowledge"); @@ -175,10 +183,11 @@ export async function syncTeamKnowledge( continue; } - // Segmented pipeline docs don't have **user**/**assistant** patterns; - // treat the whole body as a single assistant message. - const isSegmented = meta.pipeline === "segmented"; - const messages = isSegmented + // Segmented and consolidated pipeline docs don't have + // **user**/**assistant** patterns; treat the whole body as a single + // assistant message. + const isSingleMessageDoc = meta.pipeline === "segmented" || meta.pipeline === "consolidated"; + const messages = isSingleMessageDoc ? [{ role: "assistant", content: body.trim() }] : extractMessages(body); @@ -242,6 +251,28 @@ export async function syncTeamKnowledge( contentHash ); + // Re-create relationship edges from frontmatter. Unlike entities, + // these need no canonicalization: unit ids are portable UUIDs + // already shared as `sessionId` above, so edges reference the same + // node on every machine that imports this file. + const asArray = (v: string | string[] | undefined): string[] => + v === undefined ? [] : Array.isArray(v) ? v : [v]; + + for (const entityId of asArray(meta.entity_ids)) { + if (!entityId) continue; + insertRelationship(db, "knowledge_unit", sessionId, "mentions", "entity", entityId, { + source: "extraction", + }); + } + for (const predicate of ["relatesTo", "supersedes", "contradicts"] as RelationshipPredicate[]) { + for (const objectId of asArray(meta[predicate])) { + if (!objectId) continue; + insertRelationship(db, "knowledge_unit", sessionId, predicate, "knowledge_unit", objectId, { + source: "llm", + }); + } + } + result.imported++; } catch (err: any) { result.errors.push(`${match}: ${err.message}`); diff --git a/test/learn-entities-sync.test.ts b/test/learn-entities-sync.test.ts new file mode 100644 index 0000000..8b850c4 --- /dev/null +++ b/test/learn-entities-sync.test.ts @@ -0,0 +1,166 @@ +/** + * test/learn-entities-sync.test.ts - Team/org propagation of the entities + + * relationships layer via the existing share/sync git round-trip. + * + * This is the test that directly answers "how does this reach the org/team + * level": two independent in-memory DBs stand in for two teammates' machines, + * connected only through a shared tmp `.smriti/` directory (config.json + + * knowledge/*.md), exactly like real git-committed team knowledge. + */ + +import { test, expect, beforeAll, afterAll, mock } from "bun:test"; +import type { Database } from "bun:sqlite"; +import { mkdirSync, rmSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { initSmriti, closeDb, insertKnowledgeUnit, upsertProject } from "../src/db"; +import { readConfig, writeConfig, exportEntities } from "../src/team/config"; +import { syncTeamKnowledge } from "../src/team/sync"; +import { consolidateKnowledge } from "../src/learn/consolidate"; +import { getEntity, resolveEntity, insertRelationship, getRelationships } from "../src/learn/entities"; +import type { KnowledgeUnit } from "../src/team/types"; + +// Two independent :memory: databases (via separate initSmriti calls) stand in +// for two teammates' machines. initSmriti's underlying store singleton is +// process-wide, but every call site below threads the returned `db` handle +// explicitly rather than going through the singleton getDb(), so the two +// stay genuinely isolated for everything this test touches. +let dbA: Database; +let dbB: Database; +let sharedDir: string; + +beforeAll(async () => { + dbA = await initSmriti(":memory:"); + dbB = await initSmriti(":memory:"); + sharedDir = join(tmpdir(), `smriti-propagation-test-${Date.now()}`); + mkdirSync(sharedDir, { recursive: true }); + + // Pre-existing FK requirement of syncTeamKnowledge/upsertSessionMeta, + // unrelated to the entities/relationships work: the importing machine + // must already have the project registered locally (smriti_session_meta + // FKs to smriti_projects). Both "teammates" in this test are in the same project. + upsertProject(dbA, "propproj"); + upsertProject(dbB, "propproj"); +}); + +afterAll(async () => { + await closeDb(); + try { rmSync(sharedDir, { recursive: true }); } catch {} +}); + +test("entity canonical id and unit-relationship edges converge across two machines via share -> sync", async () => { + // --- Machine A: create + promote a unit that mentions "Redis" --- + const unit: KnowledgeUnit = { + id: "prop-unit-a", + topic: "Redis TTL guidance", + category: "architecture/decision", + relevance: 9, + entities: ["Redis"], + files: [], + plainText: "Use a 5-minute TTL for API response caching.", + lineRanges: [], + }; + insertKnowledgeUnit(dbA, unit, "prop-session-a", "propproj", "prop-hash-a"); + const redisIdOnA = resolveEntity(dbA, "Redis")!; + insertRelationship(dbA, "knowledge_unit", "prop-unit-a", "mentions", "entity", redisIdOnA); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => + new Response(JSON.stringify({ response: "# Redis TTL Guidance\n\nUse a 5-minute TTL." }), { status: 200 }) + ) as any; + + try { + const result = await consolidateKnowledge(dbA, { + minDensity: 999, // no segment-phase sessions on A — isolates the promote phase + minRetrievals: 999, + minRelevance: 8, // unit's relevance (9) qualifies + outputDir: sharedDir, + }); + expect(result.unitsPromoted).toBe(1); + expect(result.errors).toEqual([]); + } finally { + globalThis.fetch = originalFetch; + } + + // --- Machine A "shares": publish its canonical entity registry to the shared config.json --- + // (this is the exact operation writeManifest performs in src/team/share.ts, just called + // directly here to isolate propagation from the rest of the share pipeline's session-querying) + const existingConfig = readConfig(sharedDir); + await writeConfig(sharedDir, { ...existingConfig, version: 2, entities: exportEntities(dbA) }); + + // --- Machine B has never seen "Redis" before --- + expect(getEntity(dbB, "redis")).toBeNull(); + + const syncResult = await syncTeamKnowledge(dbB, { inputDir: sharedDir }); + + expect(syncResult.errors).toEqual([]); + expect(syncResult.entitiesImported).toBeGreaterThanOrEqual(1); + expect(syncResult.imported).toBeGreaterThanOrEqual(1); + + // The entity converged onto the SAME canonical id — not an independently re-slugified duplicate. + const redisOnB = getEntity(dbB, "redis"); + expect(redisOnB).toBeTruthy(); + expect(redisOnB!.id).toBe(redisIdOnA); + expect(redisOnB!.aliases).toContain("Redis"); + + // The unit's "mentions" edge was re-created on B, referencing that same entity id. + const bMentions = getRelationships(dbB, { + subjectType: "knowledge_unit", + subjectId: "prop-unit-a", + predicate: "mentions", + objectType: "entity", + }); + expect(bMentions.map((r) => r.object_id)).toContain(redisIdOnA); +}); + +test("supersedes edges between shared units survive the round-trip with no canonicalization needed", async () => { + // Machine A: an existing unit, and a new one that supersedes it — both promoted. + const existing: KnowledgeUnit = { + id: "prop-superseded-unit", topic: "Old Redis TTL", category: "architecture/decision", relevance: 8, + entities: ["Redis"], files: [], plainText: "Use a 1-minute TTL.", lineRanges: [], + }; + const superseding: KnowledgeUnit = { + id: "prop-superseding-unit", topic: "New Redis TTL", category: "architecture/decision", relevance: 9, + entities: ["Redis"], files: [], plainText: "Use a 5-minute TTL instead.", lineRanges: [], + }; + insertKnowledgeUnit(dbA, existing, "prop-session-b1", "propproj", "prop-hash-existing"); + insertKnowledgeUnit(dbA, superseding, "prop-session-b2", "propproj", "prop-hash-superseding"); + const redisId = resolveEntity(dbA, "Redis")!; + insertRelationship(dbA, "knowledge_unit", "prop-superseded-unit", "mentions", "entity", redisId); + insertRelationship(dbA, "knowledge_unit", "prop-superseding-unit", "mentions", "entity", redisId); + // Simulate what promote-phase LLM relationship inference would have discovered: + insertRelationship(dbA, "knowledge_unit", "prop-superseding-unit", "supersedes", "knowledge_unit", "prop-superseded-unit", { source: "llm" }); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => + new Response(JSON.stringify({ response: "# Doc\n\nContent." }), { status: 200 }) + ) as any; + + try { + // Only "prop-superseding-unit" clears the bar this round (existing unit stays at relevance 8 + // < minRelevance 8.5, so we promote exactly the one whose frontmatter should carry the edge). + const result = await consolidateKnowledge(dbA, { + minDensity: 999, + minRetrievals: 999, + minRelevance: 8.5, + outputDir: sharedDir, + }); + expect(result.unitsPromoted).toBe(1); + } finally { + globalThis.fetch = originalFetch; + } + + await writeConfig(sharedDir, { ...readConfig(sharedDir), version: 2, entities: exportEntities(dbA) }); + + const syncResult = await syncTeamKnowledge(dbB, { inputDir: sharedDir }); + expect(syncResult.errors).toEqual([]); + + const bEdges = getRelationships(dbB, { + subjectType: "knowledge_unit", + subjectId: "prop-superseding-unit", + predicate: "supersedes", + }); + // No entity-style canonicalization needed here: unit ids are portable UUIDs, + // so the edge lands referencing the exact same object id on both machines. + expect(bEdges.map((r) => r.object_id)).toContain("prop-superseded-unit"); +}); diff --git a/test/learn-entities.test.ts b/test/learn-entities.test.ts new file mode 100644 index 0000000..210e557 --- /dev/null +++ b/test/learn-entities.test.ts @@ -0,0 +1,349 @@ +/** + * test/learn-entities.test.ts - Tests for canonical entity resolution and + * relationship triples (RDF-inspired knowledge graph layer). + * + * Mirrors test/learn-consolidate.test.ts's style: initSmriti(":memory:"), + * mocked global.fetch standing in for Ollama, scratch tmpDir for output. + */ + +import { test, expect, beforeAll, afterAll, mock } from "bun:test"; +import type { Database } from "bun:sqlite"; +import { mkdirSync, rmSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { + initSmriti, + closeDb, + upsertSessionMeta, + upsertProject, + updateDensityScore, + insertKnowledgeUnit, + findPromotableUnits, +} from "../src/db"; +import { + resolveEntity, + getEntity, + findEntity, + insertRelationship, + getRelationships, + findRelatedCandidates, + getUnitsForEntity, +} from "../src/learn/entities"; +import { consolidateKnowledge } from "../src/learn/consolidate"; +import type { KnowledgeUnit } from "../src/team/types"; + +let db: Database; +let tmpDir: string; + +beforeAll(async () => { + db = await initSmriti(":memory:"); + tmpDir = join(tmpdir(), `smriti-entities-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); +}); + +afterAll(async () => { + await closeDb(); + try { rmSync(tmpDir, { recursive: true }); } catch {} +}); + +function seedSession(sessionId: string, projectId: string, messages: Array<{ role: string; content: string }>) { + const now = new Date().toISOString(); + db.prepare( + `INSERT INTO memory_sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)` + ).run(sessionId, `Session ${sessionId}`, now, now); + + const insertMsg = db.prepare( + `INSERT INTO memory_messages (session_id, role, content, hash, created_at) VALUES (?, ?, ?, ?, ?)` + ); + for (const [i, m] of messages.entries()) { + insertMsg.run(sessionId, m.role, m.content, `${sessionId}-h${i}`, now); + } + + upsertProject(db, projectId); + upsertSessionMeta(db, sessionId, "claude-code", projectId); +} + +function mockOllamaFetch(handlers: { stage1?: () => object; relation?: () => string; stage2?: () => string }) { + return mock(async (_url: string, init: any) => { + const body = JSON.parse(init.body); + const prompt = body.prompt as string; + if (prompt.includes("Knowledge Unit Segmentation")) { + return new Response( + JSON.stringify({ response: "```json\n" + JSON.stringify((handlers.stage1 ?? (() => ({ units: [] })))()) + "\n```" }), + { status: 200 } + ); + } + if (prompt.includes("CANDIDATES")) { + return new Response(JSON.stringify({ response: (handlers.relation ?? (() => ""))() }), { status: 200 }); + } + return new Response( + JSON.stringify({ response: (handlers.stage2 ?? (() => "# Doc\n\nContent."))() }), + { status: 200 } + ); + }); +} + +// ============================================================================= +// Entity resolution +// ============================================================================= + +test("resolveEntity merges exact-normalize variants (case/whitespace) onto one canonical id", () => { + const id1 = resolveEntity(db, "JWT")!; + const id2 = resolveEntity(db, " jwt ")!; + const id3 = resolveEntity(db, "JWT."); + + expect(id1).toBe(id2); + expect(id1).toBe(id3); + + const entity = getEntity(db, id1)!; + expect(entity.label).toBe("JWT"); // first-seen label wins + expect(entity.aliases).toContain("JWT"); + expect(entity.aliases).toContain("jwt"); + expect(entity.mention_count).toBe(3); +}); + +test("resolveEntity does not merge genuinely different wordings for the same concept", () => { + const jwtId = resolveEntity(db, "distinct-jwt-test")!; + const fullFormId = resolveEntity(db, "distinct-json-web-token-test")!; + + expect(jwtId).not.toBe(fullFormId); +}); + +test("resolveEntity returns null for blank labels", () => { + expect(resolveEntity(db, " ")).toBeNull(); +}); + +test("findEntity looks up by exact id or by label", () => { + resolveEntity(db, "Redis"); + expect(findEntity(db, "Redis")?.id).toBe("redis"); + expect(findEntity(db, "redis")?.id).toBe("redis"); + expect(findEntity(db, "nonexistent-entity-xyz")).toBeNull(); +}); + +// ============================================================================= +// Relationship triples +// ============================================================================= + +test("insertRelationship dedups via the UNIQUE constraint", () => { + insertRelationship(db, "knowledge_unit", "unit-a", "mentions", "entity", "redis"); + insertRelationship(db, "knowledge_unit", "unit-a", "mentions", "entity", "redis"); + + const rows = getRelationships(db, { subjectType: "knowledge_unit", subjectId: "unit-a", predicate: "mentions" }); + expect(rows.length).toBe(1); +}); + +test("getRelationships supports single-triple-pattern lookup", () => { + insertRelationship(db, "knowledge_unit", "unit-b", "supersedes", "knowledge_unit", "unit-a", { source: "llm" }); + + const bySubject = getRelationships(db, { subjectId: "unit-b" }); + expect(bySubject.some((r) => r.predicate === "supersedes" && r.object_id === "unit-a")).toBe(true); + + const byPredicate = getRelationships(db, { predicate: "supersedes" }); + expect(byPredicate.length).toBeGreaterThanOrEqual(1); +}); + +// ============================================================================= +// Segment phase: mentions edges created with no extra LLM calls +// ============================================================================= + +test("consolidate segment phase creates mentions edges for every stored entity, no extra LLM calls", async () => { + seedSession("ent-s1", "entproj", [ + { role: "user", content: "We need to decide on a caching strategy for the API. Considering Redis vs in-memory caching." }, + { role: "assistant", content: "Redis is better — it's external state, handles multi-instance, fast, and proven." }, + ]); + updateDensityScore(db, "ent-s1", 0.9); + + let fetchCallCount = 0; + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async (_url: string, init: any) => { + fetchCallCount++; + const body = JSON.parse(init.body); + const isStage1 = (body.prompt as string).includes("Knowledge Unit Segmentation"); + if (isStage1) { + // relevance kept low (1) deliberately: this unit stays in the shared + // test DB as tier='segmented' after this test, and must never satisfy + // a later test's minRelevance threshold (e.g. 8) via leftover state. + return new Response( + JSON.stringify({ + response: "```json\n" + JSON.stringify({ + units: [{ topic: "Redis caching decision", category: "architecture/decision", relevance: 1, entities: ["Redis", "Caching"] }], + }) + "\n```", + }), + { status: 200 } + ); + } + return new Response(JSON.stringify({ response: "unexpected call" }), { status: 200 }); + }) as any; + + try { + const result = await consolidateKnowledge(db, { + minDensity: 0.5, + minRetrievals: 999, + minRelevance: 999, // nothing promotable — isolates the segment phase + outputDir: join(tmpDir, "ent-output"), + }); + + expect(result.unitsStored).toBe(1); + expect(fetchCallCount).toBe(1); // segmentation only, no promote-time LLM calls + + const unitRow = db + .prepare(`SELECT id FROM smriti_knowledge_units WHERE session_id = 'ent-s1'`) + .get() as { id: string }; + + const mentions = getRelationships(db, { + subjectType: "knowledge_unit", + subjectId: unitRow.id, + predicate: "mentions", + }); + expect(mentions.length).toBe(2); + const objectIds = mentions.map((m) => m.object_id).sort(); + expect(objectIds).toEqual(["caching", "redis"]); + + const unitsForRedis = getUnitsForEntity(db, "redis"); + expect(unitsForRedis.map((u) => u.id)).toContain(unitRow.id); + } finally { + globalThis.fetch = originalFetch; + } +}); + +// ============================================================================= +// minEntityReach promotion criterion +// ============================================================================= + +test("minEntityReach promotes a unit whose entity is shared by >= K other units, even at 0 retrievals", () => { + const shared: KnowledgeUnit = { + id: "reach-unit-1", topic: "Topic A", category: "code/pattern", relevance: 2, + entities: [], files: [], plainText: "content A", lineRanges: [], + }; + const sharedOther1: KnowledgeUnit = { + id: "reach-unit-2", topic: "Topic B", category: "code/pattern", relevance: 2, + entities: [], files: [], plainText: "content B", lineRanges: [], + }; + const sharedOther2: KnowledgeUnit = { + id: "reach-unit-3", topic: "Topic C", category: "code/pattern", relevance: 2, + entities: [], files: [], plainText: "content C", lineRanges: [], + }; + + insertKnowledgeUnit(db, shared, "reach-s1", "reachproj", "reach-hash-1"); + insertKnowledgeUnit(db, sharedOther1, "reach-s2", "reachproj", "reach-hash-2"); + insertKnowledgeUnit(db, sharedOther2, "reach-s3", "reachproj", "reach-hash-3"); + + const entityId = resolveEntity(db, "shared-webhook-retries")!; + insertRelationship(db, "knowledge_unit", "reach-unit-1", "mentions", "entity", entityId); + insertRelationship(db, "knowledge_unit", "reach-unit-2", "mentions", "entity", entityId); + insertRelationship(db, "knowledge_unit", "reach-unit-3", "mentions", "entity", entityId); + + // Below scalar thresholds (relevance=2, retrieval_count=0) but 2 OTHER units share the entity. + const promotableWithReach = findPromotableUnits(db, 999, 999, 2); + expect(promotableWithReach.map((u) => u.id)).toContain("reach-unit-1"); + + // Without minEntityReach, the same unit is not promotable. + const promotableWithoutReach = findPromotableUnits(db, 999, 999); + expect(promotableWithoutReach.map((u) => u.id)).not.toContain("reach-unit-1"); +}); + +// ============================================================================= +// Promote-phase relationship inference (LLM-gated, bounded) +// ============================================================================= + +test("promote phase persists LLM-inferred relatesTo/supersedes/contradicts edges", async () => { + const existing: KnowledgeUnit = { + id: "infer-existing", topic: "Old Redis TTL guidance", category: "architecture/decision", relevance: 5, + entities: [], files: [], plainText: "Use a 1-minute TTL.", lineRanges: [], + }; + insertKnowledgeUnit(db, existing, "infer-s1", "inferproj", "infer-hash-existing"); + const redisId = resolveEntity(db, "infer-redis")!; + insertRelationship(db, "knowledge_unit", "infer-existing", "mentions", "entity", redisId); + + const promoted: KnowledgeUnit = { + id: "infer-new", topic: "New Redis TTL guidance", category: "architecture/decision", relevance: 9, + entities: ["infer-redis"], files: [], plainText: "Use a 5-minute TTL instead.", lineRanges: [], + }; + insertKnowledgeUnit(db, promoted, "infer-s2", "inferproj", "infer-hash-new"); + insertRelationship(db, "knowledge_unit", "infer-new", "mentions", "entity", redisId); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mockOllamaFetch({ relation: () => "RELATION [0]: supersedes" }) as any; + + try { + const result = await consolidateKnowledge(db, { + minDensity: 999, // no segment-phase sessions + minRetrievals: 999, + minRelevance: 8, // only infer-new (relevance 9) qualifies + outputDir: join(tmpDir, "infer-output"), + }); + + expect(result.unitsPromoted).toBe(1); + expect(result.errors).toEqual([]); + + const edges = getRelationships(db, { subjectType: "knowledge_unit", subjectId: "infer-new", predicate: "supersedes" }); + expect(edges.length).toBe(1); + expect(edges[0].object_id).toBe("infer-existing"); + expect(edges[0].source).toBe("llm"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("promote phase relationship inference is best-effort: a broken LLM response doesn't block promotion", async () => { + const existing: KnowledgeUnit = { + id: "badllm-existing", topic: "Existing", category: "code/pattern", relevance: 5, + entities: [], files: [], plainText: "content", lineRanges: [], + }; + insertKnowledgeUnit(db, existing, "badllm-s1", "badllmproj", "badllm-hash-existing"); + const entId = resolveEntity(db, "badllm-entity")!; + insertRelationship(db, "knowledge_unit", "badllm-existing", "mentions", "entity", entId); + + const promoted: KnowledgeUnit = { + id: "badllm-new", topic: "New", category: "code/pattern", relevance: 9, + entities: ["badllm-entity"], files: [], plainText: "content", lineRanges: [], + }; + insertKnowledgeUnit(db, promoted, "badllm-s2", "badllmproj", "badllm-hash-new"); + insertRelationship(db, "knowledge_unit", "badllm-new", "mentions", "entity", entId); + + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async (_url: string, init: any) => { + const body = JSON.parse(init.body); + const prompt = body.prompt as string; + if (prompt.includes("CANDIDATES")) throw new Error("connection refused"); + return new Response(JSON.stringify({ response: "# Doc\n\nContent." }), { status: 200 }); + }) as any; + + try { + const result = await consolidateKnowledge(db, { + minDensity: 999, + minRetrievals: 999, + minRelevance: 8, + outputDir: join(tmpDir, "badllm-output"), + }); + + // Promotion itself succeeds even though relationship inference failed. + expect(result.unitsPromoted).toBe(1); + expect(result.errors).toEqual([]); + + const edges = getRelationships(db, { subjectType: "knowledge_unit", subjectId: "badllm-new" }) + .filter((r) => r.predicate !== "mentions"); + expect(edges.length).toBe(0); + } finally { + globalThis.fetch = originalFetch; + } +}); + +// ============================================================================= +// findRelatedCandidates +// ============================================================================= + +test("findRelatedCandidates finds other units sharing a canonical entity, excluding self", () => { + const a: KnowledgeUnit = { id: "cand-a", topic: "A", category: "code/pattern", relevance: 5, entities: [], files: [], plainText: "a", lineRanges: [] }; + const b: KnowledgeUnit = { id: "cand-b", topic: "B", category: "code/pattern", relevance: 5, entities: [], files: [], plainText: "b", lineRanges: [] }; + insertKnowledgeUnit(db, a, "cand-s1", "candproj", "cand-hash-a"); + insertKnowledgeUnit(db, b, "cand-s2", "candproj", "cand-hash-b"); + + const sharedEntity = resolveEntity(db, "cand-shared-entity")!; + insertRelationship(db, "knowledge_unit", "cand-a", "mentions", "entity", sharedEntity); + insertRelationship(db, "knowledge_unit", "cand-b", "mentions", "entity", sharedEntity); + + const candidates = findRelatedCandidates(db, "cand-a", 5); + expect(candidates.map((c) => c.id)).toEqual(["cand-b"]); + expect(candidates.map((c) => c.id)).not.toContain("cand-a"); +}); From 0109d351cf0103ac79e65f3c048c0c37ef01e265 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 16:22:51 +0000 Subject: [PATCH 06/13] fix(learn): prevent bidirectional directional-predicate edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When two units sharing an entity are promoted in the same consolidate run, each independently asks the LLM "do I supersede/contradict the other" — found via a live demo that this can produce both directions asserted simultaneously (A supersedes B and B supersedes A), which is incoherent for a directional predicate. Skip inserting the reverse edge if the candidate already asserted it in the other direction. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PnYgLUDwWALu1bUAgrFQDG --- src/learn/consolidate.ts | 15 ++++++++++++ test/learn-entities.test.ts | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/learn/consolidate.ts b/src/learn/consolidate.ts index 38d9a94..5e35606 100644 --- a/src/learn/consolidate.ts +++ b/src/learn/consolidate.ts @@ -313,6 +313,21 @@ RELATION [i]: relatesTo|supersedes|contradicts|none`; const candidate = candidates[index]; if (!predicate || !candidate) continue; + // Directional predicates shouldn't hold in both directions for the same + // pair. When two entity-sharing units are promoted in the same run, + // each independently asks "do I relate to/supersede the other" — if the + // candidate already asserted the reverse relation (e.g. its own + // promotion ran first in this batch), keep that one and skip the + // contradictory reverse edge rather than storing both. + const reverseAlreadyAsserted = getRelationships(db, { + subjectType: "knowledge_unit", + subjectId: candidate.id, + predicate, + objectType: "knowledge_unit", + objectId: unit.id, + }).length > 0; + if (reverseAlreadyAsserted) continue; + insertRelationship(db, "knowledge_unit", unit.id, predicate, "knowledge_unit", candidate.id, { source: "llm", }); diff --git a/test/learn-entities.test.ts b/test/learn-entities.test.ts index 210e557..2c91e55 100644 --- a/test/learn-entities.test.ts +++ b/test/learn-entities.test.ts @@ -285,6 +285,54 @@ test("promote phase persists LLM-inferred relatesTo/supersedes/contradicts edges } }); +test("promote phase never asserts a directional predicate in both directions for the same pair", async () => { + // Two units sharing an entity, BOTH clearing the promotion bar in the same + // run — each independently asks "do I supersede the other" and (with a + // naive LLM/mock that doesn't reason about recency) can get "yes" from both + // sides. Only one direction should end up persisted. + const unitA: KnowledgeUnit = { + id: "bidir-unit-a", topic: "Redis TTL: 1 minute", category: "architecture/decision", relevance: 9, + entities: [], files: [], plainText: "Use a 1-minute TTL.", lineRanges: [], + }; + const unitB: KnowledgeUnit = { + id: "bidir-unit-b", topic: "Redis TTL: 15 minutes", category: "architecture/decision", relevance: 9, + entities: [], files: [], plainText: "Use a 15-minute TTL instead.", lineRanges: [], + }; + insertKnowledgeUnit(db, unitA, "bidir-s1", "bidirproj", "bidir-hash-a"); + insertKnowledgeUnit(db, unitB, "bidir-s2", "bidirproj", "bidir-hash-b"); + const entityId = resolveEntity(db, "bidir-redis")!; + insertRelationship(db, "knowledge_unit", "bidir-unit-a", "mentions", "entity", entityId); + insertRelationship(db, "knowledge_unit", "bidir-unit-b", "mentions", "entity", entityId); + + const originalFetch = globalThis.fetch; + // Always answers "supersedes" regardless of which side is asking — the + // worst case for this bug, and realistic for a small/local model given a + // prompt with no explicit recency signal. + globalThis.fetch = mockOllamaFetch({ relation: () => "RELATION [0]: supersedes" }) as any; + + try { + const result = await consolidateKnowledge(db, { + minDensity: 999, + minRetrievals: 999, + minRelevance: 8, // both unitA and unitB qualify + outputDir: join(tmpDir, "bidir-output"), + }); + + expect(result.unitsPromoted).toBe(2); + + const aToB = getRelationships(db, { + subjectType: "knowledge_unit", subjectId: "bidir-unit-a", predicate: "supersedes", objectId: "bidir-unit-b", + }); + const bToA = getRelationships(db, { + subjectType: "knowledge_unit", subjectId: "bidir-unit-b", predicate: "supersedes", objectId: "bidir-unit-a", + }); + // Exactly one direction persisted, never both. + expect(aToB.length + bToA.length).toBe(1); + } finally { + globalThis.fetch = originalFetch; + } +}); + test("promote phase relationship inference is best-effort: a broken LLM response doesn't block promotion", async () => { const existing: KnowledgeUnit = { id: "badllm-existing", topic: "Existing", category: "code/pattern", relevance: 5, From 41ee6425a586021a70edb7b1ce23d5e6ab69eca2 Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 26 Jul 2026 22:30:36 +0530 Subject: [PATCH 07/13] fix(learn): parse RELATION lines without requiring literal brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prompt asks for "RELATION [i]: predicate" but Ollama reliably gets the index/predicate right while dropping the literal brackets (observed: "RELATION 0: supersedes"). The regex required them exactly, so correct answers were silently discarded — inferRelationships swallows all errors by design (enrichment, not a promotion precondition), so this failure mode had zero visibility: promotion succeeded, the edge just never appeared. Verified against real Ollama output: a hand-fed prompt produced a correct "supersedes" verdict in 16s that the old regex dropped entirely; after the fix, an end-to-end consolidate run against seeded sessions produced a real relatesTo edge, confirmed via `smriti graph`. Also logs when a non-empty response yields zero parsed lines, since that almost always means format drift rather than every candidate genuinely being unrelated. --- src/learn/consolidate.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/learn/consolidate.ts b/src/learn/consolidate.ts index 5e35606..c400e81 100644 --- a/src/learn/consolidate.ts +++ b/src/learn/consolidate.ts @@ -252,7 +252,11 @@ export async function consolidateKnowledge( // Relationship Inference (promote-time, LLM-gated) // ============================================================================= -const RELATION_LINE = /RELATION\s*\[(\d+)\]:\s*(relatesTo|supersedes|contradicts|none)/gi; +// Brackets optional: models reliably get the index and predicate right but +// don't reliably reproduce "[i]" literally (observed: "RELATION 0: supersedes" +// instead of "RELATION [0]: supersedes") — a strict bracket requirement here +// silently drops otherwise-correct answers. +const RELATION_LINE = /RELATION\s*\[?(\d+)\]?:\s*(relatesTo|supersedes|contradicts|none)/gi; const MAX_EXCERPT_CHARS = 800; // Case-insensitive regex match -> canonical camelCase predicate (avoid a blind @@ -305,7 +309,9 @@ RELATION [i]: relatesTo|supersedes|contradicts|none`; const response = await callOllama(prompt, { model }); + let matched = 0; for (const match of response.matchAll(RELATION_LINE)) { + matched++; const index = Number(match[1]); const raw = match[2].toLowerCase(); if (raw === "none") continue; @@ -332,6 +338,16 @@ RELATION [i]: relatesTo|supersedes|contradicts|none`; source: "llm", }); } + + // A non-empty response that yields zero parsed lines almost always means + // the model drifted from the expected format, not that every candidate + // was genuinely unrelated — surface it instead of promoting in silence. + if (matched === 0 && response.trim().length > 0) { + console.warn( + `inferRelationships: parsed 0 relation lines from a non-empty response for unit ${unit.id} — response may not match the expected format:`, + truncate(response, 300) + ); + } } catch { // Enrichment only — never block promotion on a failed/unparseable relation call. } From 2bf96e436f312958258dd6594e6ee7659d2ccbd1 Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 2 Aug 2026 14:09:57 +0530 Subject: [PATCH 08/13] feat(learn): classify unit relationships via native Ollama tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace regex-parsed "RELATION [i]: predicate" free-text prompting with a native tool call (record_relationships) for promote-time relationship inference — the model returns structured JSON directly, so there's nothing to drift from or fail to parse. classifyRelationshipsTextFormat is kept only as the "before" baseline for the eval comparison in test/eval/relation-inference.eval.ts. Also requires QMD_MEMORY_MODEL to be explicitly set (config.ts's new requireOllamaModel()) instead of every Ollama call site silently falling back to a hardcoded default model. --- CLAUDE.md | 2 +- src/categorize/classifier.ts | 4 +- src/config.ts | 13 ++- src/learn/consolidate.ts | 208 +++++++++++++++++++++++++-------- src/ollama.ts | 32 +++-- src/team/ollama.ts | 4 +- src/team/reflect.ts | 4 +- test/learn-consolidate.test.ts | 146 ++++++++++++++++++++++- 8 files changed, 346 insertions(+), 67 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2c7896..bc0d6d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -275,7 +275,7 @@ See `docs/internal/ingest-architecture.md` for details. | `COPILOT_STORAGE_DIR` | auto-detected per OS | VS Code workspaceStorage root override | | `SMRITI_PROJECTS_ROOT` | `~/zero8.dev` | Projects root for ID derivation | | `OLLAMA_HOST` | `http://127.0.0.1:11434` | Ollama endpoint | -| `QMD_MEMORY_MODEL` | `qwen3:8b-tuned` | Ollama model for synthesis | +| `QMD_MEMORY_MODEL` | `qwen3.5:9b-mlx-tuned` | Ollama model for synthesis (MLX engine)| | `SMRITI_CLASSIFY_THRESHOLD` | `0.5` | LLM classification trigger threshold | | `SMRITI_AUTHOR` | `$USER` | Git author for team sharing | | `SMRITI_DAEMON_DEBOUNCE_MS` | `30000` | Daemon file-stability wait (v0.4.0) | diff --git a/src/categorize/classifier.ts b/src/categorize/classifier.ts index 5bf17bd..347e9f5 100644 --- a/src/categorize/classifier.ts +++ b/src/categorize/classifier.ts @@ -7,7 +7,7 @@ import type { Database } from "bun:sqlite"; import { tagMessage, tagSession } from "../db"; -import { CLASSIFY_LLM_THRESHOLD, OLLAMA_HOST, OLLAMA_MODEL } from "../config"; +import { CLASSIFY_LLM_THRESHOLD, OLLAMA_HOST, requireOllamaModel } from "../config"; import { ALL_CATEGORY_IDS } from "./schema"; import { getRuleManager, type Rule } from "./rules/loader"; @@ -86,7 +86,7 @@ ${text.slice(0, 2000)}`; method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - model: OLLAMA_MODEL, + model: requireOllamaModel(), prompt, stream: false, options: { temperature: 0.1, num_predict: 50 }, diff --git a/src/config.ts b/src/config.ts index 37c141f..2ce871b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -68,7 +68,18 @@ export const PROJECTS_ROOT = // ============================================================================= export const OLLAMA_HOST = Bun.env.OLLAMA_HOST || "http://127.0.0.1:11434"; -export const OLLAMA_MODEL = Bun.env.QMD_MEMORY_MODEL || "qwen3:8b-tuned"; +export const OLLAMA_MODEL = Bun.env.QMD_MEMORY_MODEL; + +/** Resolve the Ollama model to use, preferring an explicit override. Throws if neither is set. */ +export function requireOllamaModel(explicit?: string): string { + const model = explicit || OLLAMA_MODEL; + if (!model) { + throw new Error( + "No Ollama model configured. Set QMD_MEMORY_MODEL in your environment or .env file." + ); + } + return model; +} /** Confidence threshold below which rule-based classification triggers LLM */ export const CLASSIFY_LLM_THRESHOLD = Number( diff --git a/src/learn/consolidate.ts b/src/learn/consolidate.ts index c400e81..82a0cd8 100644 --- a/src/learn/consolidate.ts +++ b/src/learn/consolidate.ts @@ -30,6 +30,7 @@ import { segmentSession } from "../team/segment"; import { generateDocument, generateFrontmatter } from "../team/document"; import { isSessionWorthSharing } from "../team/formatter"; import { callOllama } from "../team/ollama"; +import { ollamaChat, type OllamaTool } from "../ollama"; import type { RawMessage } from "../team/formatter"; import type { KnowledgeUnit } from "../team/types"; import { @@ -252,12 +253,39 @@ export async function consolidateKnowledge( // Relationship Inference (promote-time, LLM-gated) // ============================================================================= +const MAX_EXCERPT_CHARS = 800; + +export type RelationCandidate = { id: string; topic: string; category: string; plain_text: string }; +export type RelationGuess = { index: number; predicate: RelationshipPredicate | "none" }; + +function truncate(text: string, max: number): string { + return text.length > max ? text.slice(0, max) + "…" : text; +} + +function buildCandidateBlock(candidates: RelationCandidate[]): string { + return candidates + .map((c, i) => `[${i}] Topic: ${c.topic}\nCategory: ${c.category}\nContent: ${truncate(c.plain_text, MAX_EXCERPT_CHARS)}`) + .join("\n\n"); +} + +function buildComparisonPreamble( + unit: { topic: string; category: string; plainText: string }, + candidates: RelationCandidate[] +): string { + return `NEW UNIT +Topic: ${unit.topic} +Category: ${unit.category} +Content: ${truncate(unit.plainText, MAX_EXCERPT_CHARS)} + +CANDIDATES +${buildCandidateBlock(candidates)}`; +} + // Brackets optional: models reliably get the index and predicate right but // don't reliably reproduce "[i]" literally (observed: "RELATION 0: supersedes" // instead of "RELATION [0]: supersedes") — a strict bracket requirement here // silently drops otherwise-correct answers. const RELATION_LINE = /RELATION\s*\[?(\d+)\]?:\s*(relatesTo|supersedes|contradicts|none)/gi; -const MAX_EXCERPT_CHARS = 800; // Case-insensitive regex match -> canonical camelCase predicate (avoid a blind // .toLowerCase() on the match, which would turn "relatesTo" into "relatesto"). @@ -267,36 +295,20 @@ const PREDICATE_BY_LOWERCASE: Record = { contradicts: "contradicts", }; -function truncate(text: string, max: number): string { - return text.length > max ? text.slice(0, max) + "…" : text; -} - /** - * Ask the LLM whether the unit being promoted relatesTo/supersedes/contradicts - * any of its entity-sharing candidates, and persist the answer as edges. - * Best-effort: a failure here (LLM down, unparseable response) is swallowed — - * it's enrichment on top of promotion, not a precondition for it. + * Original approach: ask for free-text "RELATION [i]: predicate" lines and + * parse them with a regex. Kept only as the "before" baseline for the eval + * comparison against classifyRelationshipsToolCall — no longer wired into + * inferRelationships(). */ -async function inferRelationships( - db: Database, - unit: KnowledgeUnit, - candidates: Array<{ id: string; topic: string; category: string; plain_text: string }>, +export async function classifyRelationshipsTextFormat( + unit: { topic: string; category: string; plainText: string }, + candidates: RelationCandidate[], model?: string -): Promise { - try { - const candidateBlock = candidates - .map((c, i) => `[${i}] Topic: ${c.topic}\nCategory: ${c.category}\nContent: ${truncate(c.plain_text, MAX_EXCERPT_CHARS)}`) - .join("\n\n"); +): Promise { + const prompt = `You are comparing a NEW knowledge unit against CANDIDATE units that already mention at least one of the same topics/entities. - const prompt = `You are comparing a NEW knowledge unit against CANDIDATE units that already mention at least one of the same topics/entities. - -NEW UNIT -Topic: ${unit.topic} -Category: ${unit.category} -Content: ${truncate(unit.plainText, MAX_EXCERPT_CHARS)} - -CANDIDATES -${candidateBlock} +${buildComparisonPreamble(unit, candidates)} For each candidate, decide the relationship of the NEW unit to it: - relatesTo: related but neither replaces nor conflicts with the other @@ -307,17 +319,129 @@ For each candidate, decide the relationship of the NEW unit to it: Respond with exactly one line per candidate, in this format: RELATION [i]: relatesTo|supersedes|contradicts|none`; - const response = await callOllama(prompt, { model }); + const response = await callOllama(prompt, { model }); + + const guesses: RelationGuess[] = []; + for (const match of response.matchAll(RELATION_LINE)) { + const index = Number(match[1]); + const raw = match[2]!.toLowerCase(); + const predicate = raw === "none" ? "none" : PREDICATE_BY_LOWERCASE[raw]; + if (!predicate || !candidates[index]) continue; + guesses.push({ index, predicate }); + } - let matched = 0; - for (const match of response.matchAll(RELATION_LINE)) { - matched++; - const index = Number(match[1]); - const raw = match[2].toLowerCase(); - if (raw === "none") continue; - const predicate = PREDICATE_BY_LOWERCASE[raw]; - const candidate = candidates[index]; - if (!predicate || !candidate) continue; + // A non-empty response that yields zero parsed lines almost always means + // the model drifted from the expected format, not that every candidate + // was genuinely unrelated — surface it instead of promoting in silence. + if (guesses.length === 0 && response.trim().length > 0) { + console.warn( + `classifyRelationshipsTextFormat: parsed 0 relation lines from a non-empty response — response may not match the expected format:`, + truncate(response, 300) + ); + } + + return guesses; +} + +const VALID_PREDICATES = new Set(["relatesTo", "supersedes", "contradicts", "none"]); + +const RECORD_RELATIONSHIPS_TOOL: OllamaTool = { + type: "function", + function: { + name: "record_relationships", + description: + "Record the relationship of the NEW knowledge unit to each CANDIDATE unit, one entry per candidate index.", + parameters: { + type: "object", + properties: { + relationships: { + type: "array", + items: { + type: "object", + properties: { + index: { + type: "integer", + description: "The candidate's [i] index as shown in the CANDIDATES list", + }, + predicate: { + type: "string", + enum: ["relatesTo", "supersedes", "contradicts", "none"], + description: + "relatesTo: related but neither replaces nor conflicts; supersedes: NEW unit replaces/updates the candidate; contradicts: NEW unit conflicts with the candidate; none: no meaningful relationship", + }, + }, + required: ["index", "predicate"], + }, + }, + }, + required: ["relationships"], + }, + }, +}; + +/** + * Ask the LLM to classify the NEW unit's relationship to each candidate via + * a native tool call instead of free-text lines — the model returns + * structured JSON directly, so there's no format to drift from and nothing + * to regex-parse. + */ +export async function classifyRelationshipsToolCall( + unit: { topic: string; category: string; plainText: string }, + candidates: RelationCandidate[], + model?: string +): Promise { + const prompt = `Compare the NEW knowledge unit against each CANDIDATE unit below, then call record_relationships with your assessment for every candidate index. + +${buildComparisonPreamble(unit, candidates)}`; + + const resp = await ollamaChat([{ role: "user", content: prompt }], { + model, + tools: [RECORD_RELATIONSHIPS_TOOL], + temperature: 0.1, + }); + + const call = resp.message.tool_calls?.find((c) => c.function.name === "record_relationships"); + if (!call) { + if (resp.message.content?.trim()) { + console.warn( + `classifyRelationshipsToolCall: model answered without calling record_relationships:`, + truncate(resp.message.content, 300) + ); + } + return []; + } + + const raw = call.function.arguments?.relationships; + if (!Array.isArray(raw)) return []; + + const guesses: RelationGuess[] = []; + for (const entry of raw) { + const index = Number((entry as any)?.index); + const predicate = (entry as any)?.predicate; + if (!Number.isInteger(index) || !candidates[index] || !VALID_PREDICATES.has(predicate)) continue; + guesses.push({ index, predicate }); + } + return guesses; +} + +/** + * Ask the LLM whether the unit being promoted relatesTo/supersedes/contradicts + * any of its entity-sharing candidates, and persist the answer as edges. + * Best-effort: a failure here (LLM down, unparseable response) is swallowed — + * it's enrichment on top of promotion, not a precondition for it. + */ +async function inferRelationships( + db: Database, + unit: KnowledgeUnit, + candidates: RelationCandidate[], + model?: string +): Promise { + try { + const guesses = await classifyRelationshipsToolCall(unit, candidates, model); + + for (const { index, predicate } of guesses) { + if (predicate === "none") continue; + const candidate = candidates[index]!; // Directional predicates shouldn't hold in both directions for the same // pair. When two entity-sharing units are promoted in the same run, @@ -338,16 +462,6 @@ RELATION [i]: relatesTo|supersedes|contradicts|none`; source: "llm", }); } - - // A non-empty response that yields zero parsed lines almost always means - // the model drifted from the expected format, not that every candidate - // was genuinely unrelated — surface it instead of promoting in silence. - if (matched === 0 && response.trim().length > 0) { - console.warn( - `inferRelationships: parsed 0 relation lines from a non-empty response for unit ${unit.id} — response may not match the expected format:`, - truncate(response, 300) - ); - } } catch { // Enrichment only — never block promotion on a failed/unparseable relation call. } diff --git a/src/ollama.ts b/src/ollama.ts index 833315c..f316209 100644 --- a/src/ollama.ts +++ b/src/ollama.ts @@ -6,29 +6,40 @@ * * Config via env: * OLLAMA_HOST - Ollama server URL (default: http://127.0.0.1:11434) - * QMD_MEMORY_MODEL - Model for summarization/synthesis (default: qwen3:8b-tuned) + * QMD_MEMORY_MODEL - Model for summarization/synthesis (required, no default) */ -// ============================================================================= -// Configuration -// ============================================================================= - -const OLLAMA_HOST = Bun.env.OLLAMA_HOST || "http://127.0.0.1:11434"; -const DEFAULT_MEMORY_MODEL = Bun.env.QMD_MEMORY_MODEL || "qwen3:8b-tuned"; +import { OLLAMA_HOST, requireOllamaModel } from "./config"; // ============================================================================= // Types // ============================================================================= +export type OllamaToolCall = { + id?: string; + function: { name: string; arguments: Record }; +}; + export type OllamaChatMessage = { - role: "system" | "user" | "assistant"; + role: "system" | "user" | "assistant" | "tool"; content: string; + tool_calls?: OllamaToolCall[]; +}; + +export type OllamaTool = { + type: "function"; + function: { + name: string; + description: string; + parameters: Record; + }; }; export type OllamaChatOptions = { model?: string; temperature?: number; maxTokens?: number; + tools?: OllamaTool[]; }; export type OllamaChatResponse = { @@ -51,7 +62,7 @@ export async function ollamaChat( messages: OllamaChatMessage[], options: OllamaChatOptions = {} ): Promise { - const model = options.model || DEFAULT_MEMORY_MODEL; + const model = requireOllamaModel(options.model); const resp = await fetch(`${OLLAMA_HOST}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -59,6 +70,7 @@ export async function ollamaChat( model, messages, stream: false, + ...(options.tools && { tools: options.tools }), options: { ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { num_predict: options.maxTokens }), @@ -288,5 +300,3 @@ export async function ollamaHealthCheck(): Promise<{ }; } } - -export { DEFAULT_MEMORY_MODEL, OLLAMA_HOST }; diff --git a/src/team/ollama.ts b/src/team/ollama.ts index 6ea04f7..655347c 100644 --- a/src/team/ollama.ts +++ b/src/team/ollama.ts @@ -5,7 +5,7 @@ * Used by segment.ts (Stage 1) and document.ts (Stage 2). */ -import { OLLAMA_HOST, OLLAMA_MODEL } from "../config"; +import { OLLAMA_HOST, requireOllamaModel } from "../config"; export type OllamaOptions = { model?: string; @@ -28,7 +28,7 @@ export async function callOllama( prompt: string, options: OllamaOptions = {} ): Promise { - const model = options.model || OLLAMA_MODEL; + const model = requireOllamaModel(options.model); const temperature = options.temperature ?? 0.7; const timeout = options.timeout ?? DEFAULT_TIMEOUT; const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; diff --git a/src/team/reflect.ts b/src/team/reflect.ts index 32ca106..6612709 100644 --- a/src/team/reflect.ts +++ b/src/team/reflect.ts @@ -10,7 +10,7 @@ * 2. src/team/prompts/share-reflect.md (built-in default) */ -import { OLLAMA_HOST, OLLAMA_MODEL } from "../config"; +import { OLLAMA_HOST, requireOllamaModel } from "../config"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import type { RawMessage } from "./formatter"; @@ -222,7 +222,7 @@ export async function synthesizeSession( const template = await loadPromptTemplate(options.projectSmritiDir); const prompt = template.replace("{{conversation}}", conversation); - const model = options.model || OLLAMA_MODEL; + const model = requireOllamaModel(options.model); const timeout = options.timeout || 120_000; const controller = new AbortController(); diff --git a/test/learn-consolidate.test.ts b/test/learn-consolidate.test.ts index 8748d8c..4607497 100644 --- a/test/learn-consolidate.test.ts +++ b/test/learn-consolidate.test.ts @@ -21,7 +21,11 @@ import { insertKnowledgeUnit, listKnowledgeUnits, } from "../src/db"; -import { consolidateKnowledge } from "../src/learn/consolidate"; +import { + consolidateKnowledge, + classifyRelationshipsTextFormat, + classifyRelationshipsToolCall, +} from "../src/learn/consolidate"; import { recall } from "../src/search/recall"; import type { KnowledgeUnit } from "../src/team/types"; @@ -281,3 +285,143 @@ test("recall does not throw for sessions with no consolidated knowledge units", await expect(recall(db, "deploy process", { project: "untrackedproj" })).resolves.toBeDefined(); }); + +// ============================================================================= +// Relationship classification (text-format vs tool-call) +// ============================================================================= + +const RELATION_UNIT = { topic: "Post-filtering for vector search", category: "architecture/decision", plainText: "Switched to post-filtering." }; +const RELATION_CANDIDATES = [ + { id: "cand-0", topic: "Pre-filtering for vector search", category: "architecture/decision", plain_text: "Decided to pre-filter." }, + { id: "cand-1", topic: "Unrelated CSS fix", category: "bug/fix", plain_text: "Fixed a hover overlay." }, +]; + +test("classifyRelationshipsTextFormat parses RELATION lines even without literal brackets", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => + new Response(JSON.stringify({ response: "RELATION 0: supersedes\nRELATION [1]: none" }), { status: 200 }) + ) as any; + + try { + const guesses = await classifyRelationshipsTextFormat(RELATION_UNIT, RELATION_CANDIDATES, "test-model"); + expect(guesses).toEqual([ + { index: 0, predicate: "supersedes" }, + { index: 1, predicate: "none" }, + ]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("classifyRelationshipsTextFormat returns nothing parseable when the model drifts off-format", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => + new Response(JSON.stringify({ response: "I think candidate 0 is superseded by the new unit." }), { status: 200 }) + ) as any; + + try { + const guesses = await classifyRelationshipsTextFormat(RELATION_UNIT, RELATION_CANDIDATES, "test-model"); + expect(guesses).toEqual([]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("classifyRelationshipsToolCall parses structured tool_calls output", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => + new Response( + JSON.stringify({ + model: "test-model", + message: { + role: "assistant", + content: "", + tool_calls: [ + { + function: { + name: "record_relationships", + arguments: { + relationships: [ + { index: 0, predicate: "supersedes" }, + { index: 1, predicate: "none" }, + ], + }, + }, + }, + ], + }, + done: true, + }), + { status: 200 } + ) + ) as any; + + try { + const guesses = await classifyRelationshipsToolCall(RELATION_UNIT, RELATION_CANDIDATES, "test-model"); + expect(guesses).toEqual([ + { index: 0, predicate: "supersedes" }, + { index: 1, predicate: "none" }, + ]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("classifyRelationshipsToolCall drops entries with out-of-range indices or invalid predicates", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => + new Response( + JSON.stringify({ + model: "test-model", + message: { + role: "assistant", + content: "", + tool_calls: [ + { + function: { + name: "record_relationships", + arguments: { + relationships: [ + { index: 0, predicate: "supersedes" }, + { index: 99, predicate: "relatesTo" }, + { index: 1, predicate: "maybe" }, + ], + }, + }, + }, + ], + }, + done: true, + }), + { status: 200 } + ) + ) as any; + + try { + const guesses = await classifyRelationshipsToolCall(RELATION_UNIT, RELATION_CANDIDATES, "test-model"); + expect(guesses).toEqual([{ index: 0, predicate: "supersedes" }]); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("classifyRelationshipsToolCall returns nothing when the model answers without calling the tool", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = mock(async () => + new Response( + JSON.stringify({ + model: "test-model", + message: { role: "assistant", content: "Candidate 0 looks superseded." }, + done: true, + }), + { status: 200 } + ) + ) as any; + + try { + const guesses = await classifyRelationshipsToolCall(RELATION_UNIT, RELATION_CANDIDATES, "test-model"); + expect(guesses).toEqual([]); + } finally { + globalThis.fetch = originalFetch; + } +}); From 204fea51f81adc9278e71913e429044c91f320cc Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 2 Aug 2026 14:10:56 +0530 Subject: [PATCH 09/13] test(eval): add A/B accuracy eval for relationship classification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live comparison of classifyRelationshipsTextFormat (regex-parsed, "before") vs classifyRelationshipsToolCall (native tool call, "after") against 9 hand-labeled scenarios, run against the real configured Ollama model. Manual-only (.eval.ts suffix, excluded from `bun test`) — run via `bun run eval:relations`. --- test/eval/relation-inference.eval.ts | 237 +++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 test/eval/relation-inference.eval.ts diff --git a/test/eval/relation-inference.eval.ts b/test/eval/relation-inference.eval.ts new file mode 100644 index 0000000..010e576 --- /dev/null +++ b/test/eval/relation-inference.eval.ts @@ -0,0 +1,237 @@ +/** + * test/eval/relation-inference.eval.ts - Live A/B eval for relationship classification + * + * Compares classifyRelationshipsTextFormat ("before" — free-text RELATION + * lines parsed with a regex) against classifyRelationshipsToolCall ("after" + * — native tool calling) on a hand-labeled dataset, run against the real + * configured Ollama model (QMD_MEMORY_MODEL). + * + * NOT a bun:test file (no *.test.ts suffix) — it hits a live Ollama server + * and is slow/non-deterministic, so it's excluded from `bun test` and run + * manually: + * + * bun run test/eval/relation-inference.eval.ts + */ + +import { + classifyRelationshipsTextFormat, + classifyRelationshipsToolCall, + type RelationCandidate, + type RelationGuess, +} from "../../src/learn/consolidate"; + +// ============================================================================= +// Dataset +// ============================================================================= + +type Scenario = { + name: string; + unit: { topic: string; category: string; plainText: string }; + candidates: RelationCandidate[]; + expected: Array; // one expected predicate per candidate index +}; + +const SCENARIOS: Scenario[] = [ + { + name: "supersedes (reverted retrieval strategy)", + unit: { + topic: "Post-filtering for vector search", + category: "architecture/decision", + plainText: + "Switched the recall pipeline from pre-filtering to post-filtering with 3x overfetch, because pre-filtering caused sqlite-vec to hang when combined with JOINs on metadata tables.", + }, + candidates: [ + { id: "c1", topic: "Pre-filtering for vector search", category: "architecture/decision", plain_text: "Decision: use pre-filtering — apply metadata filters directly inside the sqlite-vec query before ranking." }, + ], + expected: ["supersedes"], + }, + { + name: "contradicts (daemon enrichment safety)", + unit: { + topic: "Inline LLM enrichment on daemon flush", + category: "architecture/decision", + plainText: "Team decided synchronous LLM enrichment on every daemon flush is safe and should run inline with ingestion.", + }, + candidates: [ + { id: "c1", topic: "Daemon flush safety", category: "architecture/decision", plain_text: "Decision: LLM enrichment must never run inline with daemon flush — it blocks ingestion and risks corrupting the write path under load." }, + ], + expected: ["contradicts"], + }, + { + name: "relatesTo (complementary recall features)", + unit: { + topic: "Cluster-scoped recall", + category: "feature/implementation", + plainText: "Added a --cluster flag to `smriti recall` for topic-scoped retrieval using O(1) Set membership checks.", + }, + candidates: [ + { id: "c1", topic: "RRF for recall", category: "feature/implementation", plain_text: "Implemented reciprocal rank fusion (RRF) to combine BM25 and vector search results in `smriti recall`." }, + ], + expected: ["relatesTo"], + }, + { + name: "none (unrelated domains)", + unit: { + topic: "Blog hover overlay CSS bug", + category: "bug/fix", + plainText: "Fixed a CSS bug where the hover overlay button showed a stray `.bv-tag` element on blog post cards.", + }, + candidates: [ + { id: "c1", topic: "Content hashing for dedup", category: "architecture/decision", plain_text: "Chose SHA256 content-addressable hashing for deduplicating ingested messages in QMD's content table." }, + ], + expected: ["none"], + }, + { + name: "supersedes (auth mechanism reversal)", + unit: { + topic: "Session cookies for auth", + category: "architecture/decision", + plainText: "Reverted from JWT-based session tokens to server-side session cookies after security review flagged JWT revocation as unsupported.", + }, + candidates: [ + { id: "c1", topic: "JWT session tokens", category: "architecture/decision", plain_text: "Adopted JWT-based session tokens for stateless auth across services." }, + ], + expected: ["supersedes"], + }, + { + name: "contradicts (MLX engine routing)", + unit: { + topic: "MLX engine routing in Ollama", + category: "topic/learning", + plainText: "Confirmed that `ollama pull` for MLX-tagged models requires model names ending in `-mlx`; regular GGUF pulls never use the MLX engine.", + }, + candidates: [ + { id: "c1", topic: "MLX engine routing in Ollama", category: "topic/learning", plain_text: "Established that any GGUF model pulled via `ollama pull` automatically runs on the MLX engine on Apple Silicon." }, + ], + expected: ["contradicts"], + }, + { + name: "relatesTo (ollama runner history)", + unit: { + topic: "Ollama runner architecture", + category: "topic/learning", + plainText: "Documented that Ollama's new llama-server-based runner replaced the old Go --ollama-engine runner starting in a later 0.x release.", + }, + candidates: [ + { id: "c1", topic: "Ollama runner architecture", category: "topic/learning", plain_text: "Verified Ollama 0.18 uses the ggml/Metal engine via a custom Go runner (--ollama-engine), not MLX, for standard GGUF models." }, + ], + expected: ["relatesTo"], + }, + { + name: "none (video upload vs tool-calling investigation)", + unit: { + topic: "Video metadata capture timeout", + category: "bug/fix", + plainText: "Fixed timeout and cancel handling in captureVideoMetadata to stop hangs during community photo/video upload.", + }, + candidates: [ + { id: "c1", topic: "MLX tool calling", category: "topic/learning", plain_text: "Investigated whether qwen3.5:9b-mlx-tuned supports native tool calling; confirmed via a /api/chat request with a get_weather tool schema." }, + ], + expected: ["none"], + }, + { + name: "multi-candidate index alignment", + unit: { + topic: "Standardize on post-filtering", + category: "architecture/decision", + plainText: "Standardized on post-filtering with 3x overfetch for all vector search filtering across projects.", + }, + candidates: [ + { id: "c1", topic: "Pre-filtering for vector search", category: "architecture/decision", plain_text: "Decision: use pre-filtering — apply metadata filters directly inside the sqlite-vec query." }, + { id: "c2", topic: "RRF for recall", category: "feature/implementation", plain_text: "Implemented RRF to merge BM25 and vector search scores." }, + { id: "c3", topic: "Blog hover overlay CSS bug", category: "bug/fix", plain_text: "Fixed a CSS bug in blog post hover overlays." }, + ], + expected: ["supersedes", "relatesTo", "none"], + }, +]; + +// ============================================================================= +// Runner +// ============================================================================= + +type MethodResult = { + guesses: RelationGuess[]; + latencyMs: number; + error?: string; +}; + +async function runMethod( + fn: (unit: Scenario["unit"], candidates: RelationCandidate[], model?: string) => Promise, + scenario: Scenario +): Promise { + const start = performance.now(); + try { + const guesses = await fn(scenario.unit, scenario.candidates); + return { guesses, latencyMs: performance.now() - start }; + } catch (err: any) { + return { guesses: [], latencyMs: performance.now() - start, error: err.message }; + } +} + +function grade(scenario: Scenario, guesses: RelationGuess[]) { + const byIndex = new Map(guesses.map((g) => [g.index, g.predicate])); + return scenario.expected.map((expected, index) => ({ + index, + expected, + got: byIndex.get(index) ?? "(missing)", + correct: byIndex.get(index) === expected, + })); +} + +async function main() { + console.log(`Running ${SCENARIOS.length} scenarios against both classifiers...\n`); + + let textCorrect = 0, toolCorrect = 0, total = 0; + let textParseFailures = 0, toolParseFailures = 0; + let textLatencyTotal = 0, toolLatencyTotal = 0; + + for (const scenario of SCENARIOS) { + console.log(`## ${scenario.name}`); + + // Sequential, not Promise.all: this Ollama server has OLLAMA_NUM_PARALLEL=1, + // so concurrent requests queue behind each other on the server anyway — + // running them "in parallel" here would just make one eat into the + // other's client-side timeout while it waits for a free slot. + const textResult = await runMethod(classifyRelationshipsTextFormat, scenario); + const toolResult = await runMethod(classifyRelationshipsToolCall, scenario); + + textLatencyTotal += textResult.latencyMs; + toolLatencyTotal += toolResult.latencyMs; + if (textResult.guesses.length === 0 && scenario.expected.length > 0) textParseFailures++; + if (toolResult.guesses.length === 0 && scenario.expected.length > 0) toolParseFailures++; + + const textGrades = grade(scenario, textResult.guesses); + const toolGrades = grade(scenario, toolResult.guesses); + + for (let i = 0; i < scenario.expected.length; i++) { + total++; + const t = textGrades[i]!; + const m = toolGrades[i]!; + if (t.correct) textCorrect++; + if (m.correct) toolCorrect++; + + console.log( + ` [${i}] expected=${t.expected.padEnd(11)} text-format=${String(t.got).padEnd(11)}${t.correct ? " ok " : " MISS"} tool-call=${String(m.got).padEnd(11)}${m.correct ? " ok " : " MISS"}` + ); + } + console.log( + ` latency: text-format=${textResult.latencyMs.toFixed(0)}ms tool-call=${toolResult.latencyMs.toFixed(0)}ms` + ); + if (textResult.error) console.log(` text-format error: ${textResult.error}`); + if (toolResult.error) console.log(` tool-call error: ${toolResult.error}`); + console.log(); + } + + console.log("=".repeat(60)); + console.log("SUMMARY"); + console.log("=".repeat(60)); + console.log(`Judgments graded: ${total}`); + console.log(`text-format accuracy: ${textCorrect}/${total} (${((textCorrect / total) * 100).toFixed(1)}%)`); + console.log(`tool-call accuracy: ${toolCorrect}/${total} (${((toolCorrect / total) * 100).toFixed(1)}%)`); + console.log(`text-format parse fails: ${textParseFailures}/${SCENARIOS.length} scenarios (zero guesses returned)`); + console.log(`tool-call parse fails: ${toolParseFailures}/${SCENARIOS.length} scenarios (zero guesses returned)`); + console.log(`text-format avg latency: ${(textLatencyTotal / SCENARIOS.length).toFixed(0)}ms`); + console.log(`tool-call avg latency: ${(toolLatencyTotal / SCENARIOS.length).toFixed(0)}ms`); +} + +await main(); From 5b23fbbbd6f43d8ff640ba43808584a0216e2cd3 Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 2 Aug 2026 14:11:13 +0530 Subject: [PATCH 10/13] feat(memory): add forget, consolidation pruning, and a recall-quality eval harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the three gaps found when mapping Smriti against a standard 6-layer AI memory architecture (working memory, episodic store, semantic/facts, store/search/update/forget tools, a consolidation job, and an eval harness) — forget and prune existed only as dead code or not at all, and the only eval was a narrow classifier A/B test. - forget: `smriti forget ` (soft by default, --hard --yes for real deletion) and `forget --all [filters]` for bulk. Re-exports deleteSession/clearAllSessions from src/qmd.ts, adds forgetSession() orchestration (sidecar cleanup, unpromoted knowledge units + their relationship edges, orphaned vector embeddings) in src/db.ts. Canonical (promoted) units/docs are kept unless --purge-shared. - prune: `smriti consolidate --prune` expires stale never-promoted knowledge units (0 retrievals, low relevance, 30+ days old) and soft-archives canonical units superseded by a newer one (tier 'archived', doc kept with a deprecation banner). Dry-run by default; --yes/--apply to mutate. Pure DB logic, no LLM call. - eval harness: test/eval/fixtures/ (multi-session scenarios covering cross-session recall-over-time, project-filter isolation, density- score tie-breaking, and a semantic-only match). Tier 1 (test/recall-quality.test.ts) is BM25-only and runs in `bun test`; Tier 2 (test/eval/recall-quality.eval.ts, `bun run eval:recall`) adds embedding-dependent scenarios and asserts vector search actually fired rather than trusting a silent BM25 fallback. --- package.json | 2 + src/db.ts | 215 ++++++++++++++++++++++++- src/format.ts | 17 ++ src/index.ts | 83 +++++++++- src/learn/consolidate.ts | 133 +++++++++++++++ src/learn/entities.ts | 2 +- src/memory.ts | 38 +++++ src/qmd.ts | 3 + test/eval/fixtures/auth-migration.ts | 58 +++++++ test/eval/fixtures/density-recency.ts | 41 +++++ test/eval/fixtures/deploy-pipeline.ts | 42 +++++ test/eval/fixtures/index.ts | 16 ++ test/eval/fixtures/run.ts | 27 ++++ test/eval/fixtures/score.ts | 61 +++++++ test/eval/fixtures/seed.ts | 28 ++++ test/eval/fixtures/semantic-caching.ts | 40 +++++ test/eval/fixtures/types.ts | 60 +++++++ test/eval/recall-quality.eval.ts | 101 ++++++++++++ test/forget.test.ts | 198 +++++++++++++++++++++++ test/learn-consolidate.test.ts | 126 ++++++++++++++- test/recall-quality.test.ts | 38 +++++ 21 files changed, 1317 insertions(+), 12 deletions(-) create mode 100644 test/eval/fixtures/auth-migration.ts create mode 100644 test/eval/fixtures/density-recency.ts create mode 100644 test/eval/fixtures/deploy-pipeline.ts create mode 100644 test/eval/fixtures/index.ts create mode 100644 test/eval/fixtures/run.ts create mode 100644 test/eval/fixtures/score.ts create mode 100644 test/eval/fixtures/seed.ts create mode 100644 test/eval/fixtures/semantic-caching.ts create mode 100644 test/eval/fixtures/types.ts create mode 100644 test/eval/recall-quality.eval.ts create mode 100644 test/forget.test.ts create mode 100644 test/recall-quality.test.ts diff --git a/package.json b/package.json index f76c6be..1e5d495 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "dev": "bun --hot src/index.ts", "build": "bun build src/index.ts --outdir dist --target bun", "test": "bun test --cwd ./test", + "eval:recall": "bun run test/eval/recall-quality.eval.ts", + "eval:relations": "bun run test/eval/relation-inference.eval.ts", "smriti": "bun src/index.ts", "bench:qmd": "bun run scripts/bench-qmd.ts --profile ci-small --out bench/results/ci-small.json --no-llm", "bench:qmd:repeat": "bun run scripts/bench-qmd-repeat.ts --profiles ci-small,small,medium --runs 3 --out bench/results/repeat-summary.json", diff --git a/src/db.ts b/src/db.ts index 048cb55..b9610a4 100644 --- a/src/db.ts +++ b/src/db.ts @@ -8,10 +8,10 @@ */ import { Database } from "bun:sqlite"; -import { mkdirSync, existsSync } from "fs"; -import { dirname } from "path"; -import { QMD_DB_PATH, SMRITI_SESSIONS_DIR } from "./config"; -import { initializeMemoryTables } from "./qmd"; +import { mkdirSync, existsSync, unlinkSync } from "fs"; +import { dirname, join } from "path"; +import { QMD_DB_PATH, SMRITI_SESSIONS_DIR, SMRITI_DIR } from "./config"; +import { initializeMemoryTables, deleteSession, cleanupOrphanedMemoryVectors } from "./qmd"; import { createStore } from "../qmd/src/index"; import { setQmdStore, closeQmdStore } from "./store"; import type { KnowledgeUnit } from "./team/types"; @@ -218,7 +218,7 @@ export function initializeSmritiTables(db: Database): void { plain_text TEXT NOT NULL, -- raw Stage-1 extract line_ranges TEXT, -- JSON array of {start,end} content_hash TEXT NOT NULL, -- hashContent({topic,category,plainText}) — Stage-1 dedup key - tier TEXT NOT NULL DEFAULT 'segmented', -- 'segmented' | 'canonical' + tier TEXT NOT NULL DEFAULT 'segmented', -- 'segmented' | 'canonical' | 'archived' retrieval_count INTEGER NOT NULL DEFAULT 0, last_recalled_at TEXT, promoted_at TEXT, @@ -487,6 +487,21 @@ export function initializeSmritiTables(db: Database): void { DELETE FROM smriti_queries_fts WHERE rowid = old.id; END; `); + + // Prune: 'archived' tier support on smriti_knowledge_units (no CHECK + // constraint on `tier`, so the new value needs no migration — only these + // two nullable columns, set when a canonical unit is archived because a + // `supersedes` edge points at it). + try { + db.exec(`ALTER TABLE smriti_knowledge_units ADD COLUMN archived_at TEXT`); + } catch { + // Column already exists + } + try { + db.exec(`ALTER TABLE smriti_knowledge_units ADD COLUMN archived_reason TEXT`); + } catch { + // Column already exists + } } // ============================================================================= @@ -1160,6 +1175,31 @@ export function deleteSidecarRows(db: Database, sessionId: string): void { db.prepare(`DELETE FROM smriti_session_costs WHERE session_id = ?`).run(sessionId); } +/** + * Full sidecar cleanup for a session forget — a superset of deleteSidecarRows + * (which `ingest --force` uses, needing only the narrower tool/file/command/ + * error/cost set that gets re-derived on re-ingest). Also clears + * Smriti-specific metadata/content tables that didn't exist when + * deleteSidecarRows was written. Does NOT touch smriti_knowledge_units or + * smriti_shares — callers (forgetSession) handle those separately since + * canonical (promoted) units are kept unless purging shared knowledge. + */ +export function deleteAllSidecarRows(db: Database, sessionId: string): void { + deleteSidecarRows(db, sessionId); + + db.prepare( + `DELETE FROM smriti_message_tags WHERE message_id IN (SELECT id FROM memory_messages WHERE session_id = ?)` + ).run(sessionId); + db.prepare(`DELETE FROM smriti_session_meta WHERE session_id = ?`).run(sessionId); + db.prepare(`DELETE FROM smriti_session_tags WHERE session_id = ?`).run(sessionId); + db.prepare(`DELETE FROM smriti_artifacts WHERE session_id = ?`).run(sessionId); + db.prepare(`DELETE FROM smriti_thinking WHERE session_id = ?`).run(sessionId); + db.prepare(`DELETE FROM smriti_attachments WHERE session_id = ?`).run(sessionId); + db.prepare(`DELETE FROM smriti_voice_notes WHERE session_id = ?`).run(sessionId); + db.prepare(`DELETE FROM smriti_session_queries WHERE session_id = ?`).run(sessionId); + db.prepare(`DELETE FROM smriti_session_clusters WHERE session_id = ?`).run(sessionId); +} + export function insertGitOperation( db: Database, messageId: number, @@ -1330,12 +1370,14 @@ export interface StoredKnowledgeUnit { plain_text: string; line_ranges: Array<{ start: number; end: number }>; content_hash: string; - tier: "segmented" | "canonical"; + tier: "segmented" | "canonical" | "archived"; retrieval_count: number; last_recalled_at: string | null; promoted_at: string | null; canonical_doc_path: string | null; share_id: string | null; + archived_at: string | null; + archived_reason: string | null; } type KnowledgeUnitRow = { @@ -1356,6 +1398,8 @@ type KnowledgeUnitRow = { promoted_at: string | null; canonical_doc_path: string | null; share_id: string | null; + archived_at: string | null; + archived_reason: string | null; }; function deserializeKnowledgeUnit(row: KnowledgeUnitRow): StoredKnowledgeUnit { @@ -1364,7 +1408,7 @@ function deserializeKnowledgeUnit(row: KnowledgeUnitRow): StoredKnowledgeUnit { entities: row.entities ? JSON.parse(row.entities) : [], files: row.files ? JSON.parse(row.files) : [], line_ranges: row.line_ranges ? JSON.parse(row.line_ranges) : [], - tier: row.tier as "segmented" | "canonical", + tier: row.tier as "segmented" | "canonical" | "archived", }; } @@ -1487,7 +1531,7 @@ export function promoteKnowledgeUnit( export function listKnowledgeUnits( db: Database, - options: { tier?: "segmented" | "canonical"; minRetrievals?: number; limit?: number } = {} + options: { tier?: "segmented" | "canonical" | "archived"; minRetrievals?: number; limit?: number } = {} ): StoredKnowledgeUnit[] { const conditions: string[] = []; const params: any[] = []; @@ -1515,6 +1559,161 @@ export function listKnowledgeUnits( return rows.map(deserializeKnowledgeUnit); } +/** Cascade-delete relationship edges where this knowledge unit is subject or object. */ +function deleteKnowledgeUnitRelationships(db: Database, unitId: string): void { + db.prepare( + `DELETE FROM smriti_relationships WHERE subject_type = 'knowledge_unit' AND subject_id = ?` + ).run(unitId); + db.prepare( + `DELETE FROM smriti_relationships WHERE object_type = 'knowledge_unit' AND object_id = ?` + ).run(unitId); +} + +/** + * Hard-delete a knowledge unit and its relationship edges. Shared by + * forgetSession (removing unpromoted units of a forgotten session) and + * pruneKnowledge (removing stale segmented units) — safe in both cases + * because a 'segmented' unit was never promoted, so nothing external + * (canonical doc, smriti_shares row) references it. + */ +export function deleteKnowledgeUnit(db: Database, unitId: string): void { + deleteKnowledgeUnitRelationships(db, unitId); + db.prepare(`DELETE FROM smriti_knowledge_units WHERE id = ?`).run(unitId); +} + +/** + * Segmented units that failed both promotion paths — the relevance escape + * hatch mirrors findPromotableUnits' own minRelevance, so a unit one + * `consolidate` run away from promoting is never a prune candidate — and are + * old enough that they're unlikely to ever clear the bar. + */ +export function findStaleSegmentedUnits( + db: Database, + maxAgeDays: number, + minRelevance: number +): StoredKnowledgeUnit[] { + const rows = db + .prepare( + `SELECT * FROM smriti_knowledge_units + WHERE tier = 'segmented' AND retrieval_count = 0 AND relevance < ? + AND created_at < datetime('now', '-' || ? || ' days')` + ) + .all(minRelevance, maxAgeDays) as KnowledgeUnitRow[]; + return rows.map(deserializeKnowledgeUnit); +} + +/** Canonical units with an incoming `supersedes` edge (some other unit supersedes them) that aren't already archived. */ +export function findSupersededCanonicalUnits( + db: Database +): Array { + const rows = db + .prepare( + `SELECT ku.*, r.subject_id AS supersededByUnitId, super_ku.topic AS supersededByTopic + FROM smriti_knowledge_units ku + JOIN smriti_relationships r + ON r.object_type = 'knowledge_unit' AND r.object_id = ku.id AND r.predicate = 'supersedes' + JOIN smriti_knowledge_units super_ku ON super_ku.id = r.subject_id + WHERE ku.tier = 'canonical'` + ) + .all() as Array; + return rows.map((r) => ({ ...deserializeKnowledgeUnit(r), supersededByUnitId: r.supersededByUnitId, supersededByTopic: r.supersededByTopic })); +} + +/** Soft-archive a canonical unit — tier -> 'archived', archived_at/reason set. The unit's relationship edges (including the supersedes edge that justified this) are left untouched as the audit trail. */ +export function archiveKnowledgeUnit(db: Database, unitId: string, reason: string): void { + db.prepare( + `UPDATE smriti_knowledge_units + SET tier = 'archived', archived_at = datetime('now'), archived_reason = ?, updated_at = datetime('now') + WHERE id = ?` + ).run(reason, unitId); +} + +// ============================================================================= +// Forget (session deletion) +// ============================================================================= + +export type ForgetOptions = { + /** Permanently delete instead of the default soft delete (active = 0). */ + hard?: boolean; + /** Only meaningful with hard: true. Also delete canonical (promoted) units, their smriti_shares row, and their .smriti/knowledge/*.md doc — normally kept since they've already been shared. */ + purgeShared?: boolean; + /** Where canonical docs live, for purgeShared's file deletion. Defaults to the same convention consolidateKnowledge uses. */ + outputDir?: string; +}; + +export type ForgetResult = { + sessionId: string; + hard: boolean; + unitsDeleted: number; // unpromoted (segmented) knowledge units removed + unitsPurged: number; // canonical units removed, only when purgeShared + canonicalKept: number; // canonical units left in place +}; + +/** + * Forget a session. Soft delete (default) just flips memory_sessions.active + * to 0 — reversible, and already understood by `list --all`/`listSessions`. + * Hard delete removes messages, all sidecar rows, unpromoted knowledge + * units, and orphaned vector embeddings; canonical (promoted) units are kept + * unless purgeShared is set, since they may already be referenced outside + * this session (team sync, a committed .smriti/knowledge/ doc). + */ +export function forgetSession( + db: Database, + sessionId: string, + options: ForgetOptions = {} +): ForgetResult { + const hard = options.hard ?? false; + const purgeShared = options.purgeShared ?? false; + const result: ForgetResult = { + sessionId, + hard, + unitsDeleted: 0, + unitsPurged: 0, + canonicalKept: 0, + }; + + if (!hard) { + deleteSession(db as any, sessionId, false); + return result; + } + + const units = db + .prepare( + `SELECT id, tier, canonical_doc_path FROM smriti_knowledge_units WHERE session_id = ?` + ) + .all(sessionId) as Array<{ id: string; tier: string; canonical_doc_path: string | null }>; + + const outputDir = options.outputDir || join(process.cwd(), SMRITI_DIR); + + for (const u of units) { + if (u.tier !== "canonical") { + deleteKnowledgeUnit(db, u.id); + result.unitsDeleted++; + continue; + } + if (!purgeShared) { + result.canonicalKept++; + continue; + } + deleteKnowledgeUnit(db, u.id); + db.prepare(`DELETE FROM smriti_shares WHERE unit_id = ?`).run(u.id); + if (u.canonical_doc_path) { + try { + unlinkSync(join(outputDir, u.canonical_doc_path)); + } catch { + // Doc already gone or never written under this outputDir — fine. + } + } + result.unitsPurged++; + } + + deleteAllSidecarRows(db, sessionId); + deleteSession(db as any, sessionId, true); + cleanupOrphanedMemoryVectors(db as any); + + return result; +} + // ============================================================================= // Session Query Labels (#60) // ============================================================================= diff --git a/src/format.ts b/src/format.ts index f78280f..b32812f 100644 --- a/src/format.ts +++ b/src/format.ts @@ -277,6 +277,9 @@ export function formatConsolidateResult(result: { unitsStored: number; unitsSkipped: number; unitsPromoted: number; + unitsPruned?: number; + unitsArchived?: number; + pruneCandidates?: Array<{ id: string; topic: string; tier: string; action: string; reason: string }>; errors: string[]; }): string { const lines = [ @@ -286,6 +289,20 @@ export function formatConsolidateResult(result: { `Units promoted: ${result.unitsPromoted}`, ]; + if (result.pruneCandidates && result.pruneCandidates.length > 0) { + lines.push(""); + lines.push(`Prune candidates (dry-run — rerun with --yes to apply):`); + lines.push( + table( + ["Topic", "Tier", "Action", "Reason"], + result.pruneCandidates.map((c) => [c.topic, c.tier, c.action, c.reason]) + ) + ); + } else if (result.unitsPruned !== undefined || result.unitsArchived !== undefined) { + lines.push(`Units pruned (deleted): ${result.unitsPruned ?? 0}`); + lines.push(`Units archived (superseded): ${result.unitsArchived ?? 0}`); + } + if (result.errors.length > 0) { lines.push(`Errors: ${result.errors.length}`); for (const err of result.errors.slice(0, 5)) { diff --git a/src/index.ts b/src/index.ts index f5c8956..f5118e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ * schema-based categorization, and team knowledge sharing. */ -import { initSmriti, closeDb, getCategories, getCategoryTree, addCategory, listProjects, tagSession, getProjectReport, getTagUsage, computeDensityScore, updateDensityScore, insertSessionQueries, getUnenrichedSessionIds, listKnowledgeUnits } from "./db"; +import { initSmriti, closeDb, getCategories, getCategoryTree, addCategory, listProjects, tagSession, getProjectReport, getTagUsage, computeDensityScore, updateDensityScore, insertSessionQueries, getUnenrichedSessionIds, listKnowledgeUnits, forgetSession } from "./db"; import { getMessages, getSession, getMemoryStatus, embedMemoryMessages } from "./qmd"; import { ingest, ingestAll } from "./ingest/index"; import { categorizeUncategorized } from "./categorize/classifier"; @@ -211,6 +211,8 @@ Commands: recall [options] Smart recall with optional synthesis categorize [options] Auto-categorize sessions tag Manually tag a session + forget [opts] Delete a session (soft by default; --hard --yes for real deletion) + forget --all [filters] Bulk forget, reusing list's --project/--category/--agent filters categories List category tree categories add [opts] Add a custom category tags [options] Show tag usage in sessions @@ -218,7 +220,7 @@ Commands: compare Compare two sessions (tokens, tools, files) compare --last Compare last 2 sessions for current project share [filters] Export knowledge to .smriti/ - consolidate [options] Segment dense sessions, promote reused knowledge units + consolidate [options] Segment dense sessions, promote reused units, prune stale/superseded ones learnings [options] List extracted knowledge units (tier, retrievals, relevance) graph Show a canonical entity's mentions and relationship edges sync Import team knowledge from .smriti/ @@ -246,6 +248,12 @@ Filters (apply to search, recall, list, share): --agent Filter by agent --limit Max results (default varies by command) +Forget options: + --hard Permanently delete instead of soft delete (requires --yes) + --yes Confirm --hard (required — no confirmation prompt otherwise) + --purge-shared With --hard, also delete canonical (promoted) units, their + smriti_shares row, and their .smriti/knowledge/*.md doc + Ingest options: smriti ingest claude Ingest Claude Code sessions smriti ingest claude-web Claude.ai data export @@ -288,6 +296,11 @@ Share options: --segmented Use 3-stage segmentation pipeline (beta) --min-relevance Relevance threshold for segmented mode (default: 6) +Consolidate options: + --prune Also run the prune phase (dry-run by default — prints candidates, deletes nothing) + --yes, --apply Actually delete/archive prune candidates (requires --prune) + --prune-stale-days Age threshold for stale segmented units (default: 30) + Insights options: smriti insights Full dashboard smriti insights session Session deep dive @@ -310,6 +323,9 @@ Examples: smriti search "auth" --project myapp smriti recall "how did we set up auth" --synthesize smriti categorize + smriti consolidate + smriti consolidate --prune + smriti consolidate --prune --yes smriti list --category decision --project myapp smriti share --category decision smriti sync @@ -650,6 +666,61 @@ async function main() { break; } + // ===================================================================== + // FORGET + // ===================================================================== + case "forget": { + const all = hasFlag(args, "--all"); + const sessionId = getPositional(args, 1); + if (!sessionId && !all) { + console.error("Usage: smriti forget [--hard] [--yes] [--purge-shared]"); + console.error(" smriti forget --all [--project ] [--category ] [--agent ] [--hard] [--yes] [--purge-shared]"); + process.exit(1); + } + + const hard = hasFlag(args, "--hard"); + const purgeShared = hasFlag(args, "--purge-shared"); + if (hard && !hasFlag(args, "--yes")) { + console.error("--hard permanently deletes session data. Re-run with --yes to confirm."); + process.exit(1); + } + + const targetIds = all + ? listSessions(db, { + project: getArg(args, "--project"), + category: getArg(args, "--category"), + agent: getArg(args, "--agent"), + includeInactive: true, + }).map((s) => s.id) + : [sessionId!]; + + if (targetIds.length === 0) { + console.log("No matching sessions to forget."); + break; + } + + let deleted = 0; + let purged = 0; + let kept = 0; + for (const id of targetIds) { + const r = forgetSession(db, id, { hard, purgeShared }); + deleted += r.unitsDeleted; + purged += r.unitsPurged; + kept += r.canonicalKept; + } + + console.log(`Forgot ${targetIds.length} session(s) (${hard ? "hard delete" : "soft delete"}).`); + if (hard) { + console.log(` Unpromoted knowledge units removed: ${deleted}`); + if (purgeShared) { + console.log(` Canonical knowledge units purged: ${purged}`); + } else if (kept > 0) { + console.log(` Canonical knowledge units kept (already shared — pass --purge-shared to also remove): ${kept}`); + } + } + break; + } + // ===================================================================== // CATEGORIES // ===================================================================== @@ -824,6 +895,8 @@ async function main() { // CONSOLIDATE // ===================================================================== case "consolidate": { + const prune = hasFlag(args, "--prune"); + const pruneApply = hasFlag(args, "--yes") || hasFlag(args, "--apply"); const result = await consolidateKnowledge(db, { minDensity: Number(getArg(args, "--min-density")) || undefined, minRetrievals: Number(getArg(args, "--min-retrievals")) || undefined, @@ -832,10 +905,16 @@ async function main() { model: getArg(args, "--model"), outputDir: getArg(args, "--output"), sessionLimit: Number(getArg(args, "--session-limit")) || undefined, + prune, + pruneStaleDays: Number(getArg(args, "--prune-stale-days")) || undefined, + pruneApply, onProgress: (msg) => console.log(` ${msg}`), }); console.log(formatConsolidateResult(result)); + if (prune && !pruneApply && result.pruneCandidates && result.pruneCandidates.length > 0) { + console.log("\nRun again with --prune --yes to apply."); + } break; } diff --git a/src/learn/consolidate.ts b/src/learn/consolidate.ts index 82a0cd8..c57326f 100644 --- a/src/learn/consolidate.ts +++ b/src/learn/consolidate.ts @@ -24,6 +24,10 @@ import { insertKnowledgeUnit, findPromotableUnits, promoteKnowledgeUnit, + findStaleSegmentedUnits, + findSupersededCanonicalUnits, + deleteKnowledgeUnit, + archiveKnowledgeUnit, } from "../db"; import { getSessionMessages } from "../team/share"; import { segmentSession } from "../team/segment"; @@ -55,6 +59,20 @@ export type ConsolidateOptions = { author?: string; sessionLimit?: number; onProgress?: (msg: string) => void; + /** Also run the prune phase (dry-run by default — see pruneApply). */ + prune?: boolean; + /** Age threshold (days) for stale, never-promoted segmented units. Default 30. */ + pruneStaleDays?: number; + /** Actually delete/archive prune candidates. Without this, prune only reports candidates (dry-run). */ + pruneApply?: boolean; +}; + +export type PruneCandidate = { + id: string; + topic: string; + tier: "segmented" | "canonical"; + action: "delete" | "archive"; + reason: string; }; export type ConsolidateResult = { @@ -62,6 +80,10 @@ export type ConsolidateResult = { unitsStored: number; unitsSkipped: number; unitsPromoted: number; + /** Only set when options.prune is true. */ + unitsPruned?: number; + unitsArchived?: number; + pruneCandidates?: PruneCandidate[]; errors: string[]; }; @@ -246,9 +268,120 @@ export async function consolidateKnowledge( options.onProgress?.(`promote phase: ${result.unitsPromoted} units promoted`); + // =========================================================================== + // Prune phase: expire stale never-promoted units, archive superseded ones. + // Pure DB logic (age, retrieval_count, supersedes-edges are all already in + // SQLite by now) — no LLM call, dry-run by default. + // =========================================================================== + + if (options.prune) { + const pruneResult = await pruneKnowledge(db, { + outputDir, + pruneStaleDays: options.pruneStaleDays, + minRelevance: options.minRelevance, + dryRun: !options.pruneApply, + }); + result.unitsPruned = pruneResult.unitsPruned; + result.unitsArchived = pruneResult.unitsArchived; + result.pruneCandidates = pruneResult.pruneCandidates; + + options.onProgress?.( + pruneResult.pruneCandidates + ? `prune phase (dry-run): ${pruneResult.pruneCandidates.length} candidates — rerun with --yes to apply` + : `prune phase: ${pruneResult.unitsPruned} units deleted, ${pruneResult.unitsArchived} archived` + ); + } + return result; } +// ============================================================================= +// Prune (expire stale segmented units, archive superseded canonical units) +// ============================================================================= + +export type PruneOptions = { + outputDir?: string; + pruneStaleDays?: number; + minRelevance?: number; + /** Report candidates without mutating the DB. Defaults to true — pass false to apply. */ + dryRun?: boolean; +}; + +export type PruneResult = { + unitsPruned: number; + unitsArchived: number; + pruneCandidates?: PruneCandidate[]; +}; + +export async function pruneKnowledge( + db: Database, + options: PruneOptions = {} +): Promise { + const dryRun = options.dryRun ?? true; + const outputDir = options.outputDir || join(process.cwd(), SMRITI_DIR); + const staleDays = options.pruneStaleDays ?? 30; + const minRelevance = options.minRelevance ?? 8; + + const stale = findStaleSegmentedUnits(db, staleDays, minRelevance); + const superseded = findSupersededCanonicalUnits(db); + + if (dryRun) { + const pruneCandidates: PruneCandidate[] = [ + ...stale.map((u) => ({ + id: u.id, + topic: u.topic, + tier: "segmented" as const, + action: "delete" as const, + reason: `stale segmented, 0 retrievals, relevance ${u.relevance} < ${minRelevance}`, + })), + ...superseded.map((u) => ({ + id: u.id, + topic: u.topic, + tier: "canonical" as const, + action: "archive" as const, + reason: `superseded by "${u.supersededByTopic}"`, + })), + ]; + return { unitsPruned: 0, unitsArchived: 0, pruneCandidates }; + } + + for (const u of stale) { + deleteKnowledgeUnit(db, u.id); + } + for (const u of superseded) { + archiveKnowledgeUnit(db, u.id, "superseded"); + await appendArchivedBanner(outputDir, u.canonical_doc_path, u.supersededByTopic); + } + + return { unitsPruned: stale.length, unitsArchived: superseded.length }; +} + +/** + * Prepend a short deprecation banner to an archived unit's canonical doc. + * The file itself is never deleted or moved — its path stays stable for + * anything already referencing it (team sync, a committed link) — only its + * content gains a notice pointing at the unit that superseded it. + */ +async function appendArchivedBanner( + outputDir: string, + docPath: string | null, + supersededByTopic: string +): Promise { + if (!docPath) return; + const filePath = join(outputDir, docPath); + const file = Bun.file(filePath); + if (!(await file.exists())) return; // doc already moved/removed outside Smriti — archive the DB row regardless + + const content = await file.text(); + const banner = `> **Archived** — superseded by "${supersededByTopic}".\n`; + const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n/); + const updated = frontmatterMatch + ? content.slice(0, frontmatterMatch[0].length) + "\n" + banner + content.slice(frontmatterMatch[0].length) + : banner + "\n" + content; + + await Bun.write(filePath, updated); +} + // ============================================================================= // Relationship Inference (promote-time, LLM-gated) // ============================================================================= diff --git a/src/learn/entities.ts b/src/learn/entities.ts index 474c944..470091d 100644 --- a/src/learn/entities.ts +++ b/src/learn/entities.ts @@ -136,7 +136,7 @@ export function getUnitsForEntity( FROM smriti_relationships r JOIN smriti_knowledge_units ku ON ku.id = r.subject_id WHERE r.subject_type = 'knowledge_unit' AND r.object_type = 'entity' - AND r.predicate = 'mentions' AND r.object_id = ? + AND r.predicate = 'mentions' AND r.object_id = ? AND ku.tier != 'archived' ORDER BY ku.retrieval_count DESC, ku.relevance DESC` ) .all(entityId) as Array<{ diff --git a/src/memory.ts b/src/memory.ts index ff86e8f..2b95288 100644 --- a/src/memory.ts +++ b/src/memory.ts @@ -257,6 +257,44 @@ export function clearAllSessions(db: Database, hard: boolean = false): number { } } +/** + * Remove content_vectors/vectors_vec rows whose hash is no longer referenced + * by any memory message or active QMD document. Scoped deletion — unlike + * QMD's own cleanupOrphanedVectors (which only checks `documents` and would + * wipe every memory-message embedding, since messages aren't rows in + * `documents`). Called after a hard session delete; a no-op (returns 0) when + * the vector/document tables aren't present (e.g. sqlite-vec unavailable, or + * a minimal test schema that skipped createStore()). + */ +export function cleanupOrphanedMemoryVectors(db: Database): number { + try { + db.prepare(`SELECT 1 FROM vectors_vec LIMIT 0`).get(); + db.prepare(`SELECT 1 FROM documents LIMIT 0`).get(); + db.prepare(`SELECT 1 FROM content_vectors LIMIT 0`).get(); + } catch { + return 0; + } + + const orphanWhere = ` + NOT EXISTS (SELECT 1 FROM memory_messages m WHERE m.hash = content_vectors.hash) + AND NOT EXISTS (SELECT 1 FROM documents d WHERE d.hash = content_vectors.hash AND d.active = 1) + `; + + const { c } = db + .prepare(`SELECT COUNT(*) as c FROM content_vectors WHERE ${orphanWhere}`) + .get() as { c: number }; + if (c === 0) return 0; + + db.exec(` + DELETE FROM vectors_vec WHERE hash_seq IN ( + SELECT content_vectors.hash || '_' || content_vectors.seq FROM content_vectors WHERE ${orphanWhere} + ) + `); + db.exec(`DELETE FROM content_vectors WHERE ${orphanWhere}`); + + return c; +} + // ============================================================================= // Message CRUD // ============================================================================= diff --git a/src/qmd.ts b/src/qmd.ts index d474c90..6f4f6c8 100644 --- a/src/qmd.ts +++ b/src/qmd.ts @@ -17,6 +17,9 @@ export { importTranscript, initializeMemoryTables, createSession, + deleteSession, + clearAllSessions, + cleanupOrphanedMemoryVectors, } from "./memory"; export { hashContent } from "../qmd/src/store"; diff --git a/test/eval/fixtures/auth-migration.ts b/test/eval/fixtures/auth-migration.ts new file mode 100644 index 0000000..696c9e1 --- /dev/null +++ b/test/eval/fixtures/auth-migration.ts @@ -0,0 +1,58 @@ +import type { RecallScenario } from "./types"; + +/** Cross-session recall over time, plus precision against a same-project, same-vocabulary distractor. */ +export const AUTH_MIGRATION: RecallScenario = { + name: "auth: JWT -> session cookie migration, recalled later", + project: "webapp", + sessions: [ + { + id: "auth-s1", + daysAgo: 14, + messages: [ + { role: "user", content: "We're getting complaints that JWTs can't be revoked on logout. Should we move to session cookies?" }, + { role: "assistant", content: "Session cookies with server-side storage let you revoke instantly. JWTs need a blocklist, which defeats the point of being stateless." }, + { role: "user", content: "Let's switch. Use server-side session cookies, drop JWT for auth entirely." }, + { role: "assistant", content: "Decision: replaced JWT-based session tokens with server-side session cookies stored in Redis, keyed by session id, TTL 24h." }, + ], + }, + { + id: "auth-s2", + daysAgo: 10, + messages: [ + { role: "user", content: "Session cookie migration is live. Any edge cases with mobile clients that can't set cookies?" }, + { role: "assistant", content: "Mobile WebViews sometimes block third-party cookies; we added a fallback bearer-token header for the mobile app only." }, + ], + }, + { + id: "auth-s3-distractor", + daysAgo: 30, + messages: [ + { role: "user", content: "What's our password hashing algorithm for the auth system?" }, + { role: "assistant", content: "We use bcrypt with cost factor 12 for password hashing, unrelated to session management." }, + ], + }, + ], + probes: [ + { + // FTS5 MATCH is an AND of all terms within one message — terms must + // be literally present, not natural-language phrasing (see msg2: + // "Use server-side session cookies, drop JWT for auth entirely."). + query: "JWT session cookies auth", + description: "core decision recall — must surface the migration session", + expectHitSessionIds: ["auth-s1"], + expectHitSubstrings: ["server-side session cookies"], + expectMissSessionIds: ["auth-s3-distractor"], + }, + { + query: "mobile cookie migration", + description: "follow-up detail in a later session — recall must surface s2, not just s1", + expectHitSessionIds: ["auth-s2"], + }, + { + query: "password hashing algorithm", + description: "distractor probe — different topic sharing the 'auth' vocabulary; must not pull in s1/s2", + expectHitSessionIds: ["auth-s3-distractor"], + expectMissSessionIds: ["auth-s1", "auth-s2"], + }, + ], +}; diff --git a/test/eval/fixtures/density-recency.ts b/test/eval/fixtures/density-recency.ts new file mode 100644 index 0000000..1e3acc4 --- /dev/null +++ b/test/eval/fixtures/density-recency.ts @@ -0,0 +1,41 @@ +import type { RecallScenario } from "./types"; + +/** + * Two sessions with identical content (so their BM25/RRF scores tie exactly) + * but very different density_score — recallMemories blends density into the + * final score 80/20, so at topK=1 only the denser session should survive. + * Requires useRecallMemories: true, since the project-filtered searchFiltered + * path never touches density scoring. + */ +export const DENSITY_BLENDING: RecallScenario = { + name: "density blending breaks a BM25 tie", + project: "backend", + sessions: [ + { + id: "density-high", + densityScore: 0.9, + messages: [ + { role: "user", content: "We should switch to using a message queue for background job processing." }, + { role: "assistant", content: "Agreed — moving long-running work off the request path avoids timeouts." }, + ], + }, + { + id: "density-low", + densityScore: 0.05, + messages: [ + { role: "user", content: "We should switch to using a message queue for background job processing." }, + { role: "assistant", content: "Agreed — moving long-running work off the request path avoids timeouts." }, + ], + }, + ], + probes: [ + { + query: "message queue background job processing", + description: "BM25-tied content, density_score must break the tie toward the denser session", + expectHitSessionIds: ["density-high"], + expectMissSessionIds: ["density-low"], + topK: 1, + useRecallMemories: true, + }, + ], +}; diff --git a/test/eval/fixtures/deploy-pipeline.ts b/test/eval/fixtures/deploy-pipeline.ts new file mode 100644 index 0000000..2065e9d --- /dev/null +++ b/test/eval/fixtures/deploy-pipeline.ts @@ -0,0 +1,42 @@ +import type { RecallScenario } from "./types"; + +/** + * Single-session recall, scoped by project. A second session shares almost + * identical vocabulary ("deploys are flaky", "CI pipeline") but lives under + * a different project — the probe (scoped to "api-service") must not pull + * it in, proving project filtering isolates results rather than just + * favoring topical relevance. + */ +export const DEPLOY_PIPELINE: RecallScenario = { + name: "deploy: CI pipeline decision, isolated by project", + project: "api-service", + sessions: [ + { + id: "deploy-s1", + messages: [ + { role: "user", content: "Our deploys are flaky. What's causing the intermittent CI pipeline failures?" }, + { role: "assistant", content: "Flaky tests were racing against a shared test database. Switched the pipeline to spin up an isolated Postgres container per CI job." }, + { role: "user", content: "Good, let's also cache node_modules between runs to speed things up." }, + { role: "assistant", content: "Added actions/cache keyed on the lockfile hash — pipeline runtime dropped from 8 minutes to 3." }, + ], + }, + { + id: "deploy-s2-other-project", + project: "frontend-app", + messages: [ + { role: "user", content: "Our deploys are flaky too — the CI pipeline times out on the frontend build." }, + { role: "assistant", content: "The frontend bundle got too large for the default Vercel build timeout; raised it and split the vendor chunk." }, + ], + }, + ], + probes: [ + { + // FTS5 MATCH is an AND of all terms within one message — literal terms only. + query: "CI pipeline flaky", + description: "single-session recall scoped to api-service — must not pull in the same-vocabulary frontend-app session", + expectHitSessionIds: ["deploy-s1"], + expectHitSubstrings: ["isolated Postgres container", "shared test database"], + expectMissSessionIds: ["deploy-s2-other-project"], + }, + ], +}; diff --git a/test/eval/fixtures/index.ts b/test/eval/fixtures/index.ts new file mode 100644 index 0000000..0db32bc --- /dev/null +++ b/test/eval/fixtures/index.ts @@ -0,0 +1,16 @@ +import { AUTH_MIGRATION } from "./auth-migration"; +import { DEPLOY_PIPELINE } from "./deploy-pipeline"; +import { DENSITY_BLENDING } from "./density-recency"; +import { SEMANTIC_CACHING } from "./semantic-caching"; +import type { RecallScenario } from "./types"; + +/** BM25-only scenarios — deterministic, no embeddings needed. Safe for CI. */ +export const CI_SCENARIOS: RecallScenario[] = [AUTH_MIGRATION, DEPLOY_PIPELINE, DENSITY_BLENDING]; + +/** Needs a live embedding backend — manual-only (see test/eval/recall-quality.eval.ts). */ +export const QUALITY_ONLY_SCENARIOS: RecallScenario[] = [SEMANTIC_CACHING]; + +/** Full set — quality mode runs all of these; CI mode filters to CI_SCENARIOS. */ +export const ALL_SCENARIOS: RecallScenario[] = [...CI_SCENARIOS, ...QUALITY_ONLY_SCENARIOS]; + +export type { RecallScenario, FixtureSession, Probe } from "./types"; diff --git a/test/eval/fixtures/run.ts b/test/eval/fixtures/run.ts new file mode 100644 index 0000000..04f59f0 --- /dev/null +++ b/test/eval/fixtures/run.ts @@ -0,0 +1,27 @@ +/** + * test/eval/fixtures/run.ts - Shared probe runner for the recall-quality + * harness (Tier 1 CI test and Tier 2 manual eval both call this). + */ + +import type { Database } from "bun:sqlite"; +import { recall } from "../../../src/search/recall"; +import { scoreProbe, type ProbeScore } from "./score"; +import type { Probe, RecallScenario } from "./types"; + +export const DEFAULT_TOP_K = 5; + +export async function runProbe( + db: Database, + scenario: RecallScenario, + probe: Probe, + options: { fast: boolean } +): Promise<{ score: ProbeScore; latencyMs: number; sources: string[] }> { + const limit = probe.topK ?? DEFAULT_TOP_K; + const started = performance.now(); + const { results } = probe.useRecallMemories + ? await recall(db, probe.query, { fast: options.fast, limit }) + : await recall(db, probe.query, { project: probe.project ?? scenario.project, fast: options.fast, limit }); + const latencyMs = performance.now() - started; + const score = scoreProbe(results, probe); + return { score, latencyMs, sources: [...new Set(results.map((r) => r.source))] }; +} diff --git a/test/eval/fixtures/score.ts b/test/eval/fixtures/score.ts new file mode 100644 index 0000000..3ba2535 --- /dev/null +++ b/test/eval/fixtures/score.ts @@ -0,0 +1,61 @@ +/** + * test/eval/fixtures/score.ts - Scoring for recall-quality probes. + * + * Precision is computed only against a probe's explicit expectMissSessionIds + * (known distractors), not exhaustively against everything else — the same + * "narrow but honest" approach test/eval/relation-inference.eval.ts uses for + * exact-match grading. We can't exhaustively label every session a probe + * shouldn't match, so we only assert on the ones we deliberately planted. + */ + +import type { Probe } from "./types"; + +export type ProbeResult = { session_id: string; content: string }; + +export type ProbeScore = { + recall: number; // |expected ∩ hit| / |expected|, 1 if no expected hits defined + precision: number | null; // 1 - |miss ∩ hit| / |hit|, null if the probe defines no distractors + substringOk: boolean; // true if no expectHitSubstrings, or one matched a retrieved expected-hit row + pass: boolean; +}; + +export function scoreProbe(results: ProbeResult[], probe: Probe): ProbeScore { + const hitIds = new Set(results.map((r) => r.session_id)); + + const expected = probe.expectHitSessionIds; + const hitCount = expected.filter((id) => hitIds.has(id)).length; + const recall = expected.length ? hitCount / expected.length : 1; + + let precision: number | null = null; + if (probe.expectMissSessionIds && probe.expectMissSessionIds.length > 0) { + const missHits = probe.expectMissSessionIds.filter((id) => hitIds.has(id)).length; + precision = hitIds.size > 0 ? 1 - missHits / hitIds.size : 1; + } + + let substringOk = true; + if (probe.expectHitSubstrings && probe.expectHitSubstrings.length > 0) { + const expectedRows = results.filter((r) => expected.includes(r.session_id)); + substringOk = expectedRows.some((r) => + probe.expectHitSubstrings!.some((s) => r.content.toLowerCase().includes(s.toLowerCase())) + ); + } + + const pass = recall === 1 && (precision === null || precision === 1) && substringOk; + return { recall, precision, substringOk, pass }; +} + +export function summarizeScores(scores: ProbeScore[]): { + total: number; + passed: number; + avgRecall: number; + avgPrecision: number | null; +} { + const total = scores.length; + const passed = scores.filter((s) => s.pass).length; + const avgRecall = total ? scores.reduce((sum, s) => sum + s.recall, 0) / total : 1; + const withPrecision = scores.filter((s) => s.precision !== null); + const avgPrecision = withPrecision.length + ? withPrecision.reduce((sum, s) => sum + (s.precision as number), 0) / withPrecision.length + : null; + return { total, passed, avgRecall, avgPrecision }; +} diff --git a/test/eval/fixtures/seed.ts b/test/eval/fixtures/seed.ts new file mode 100644 index 0000000..5f7c124 --- /dev/null +++ b/test/eval/fixtures/seed.ts @@ -0,0 +1,28 @@ +/** + * test/eval/fixtures/seed.ts - Seed a RecallScenario's sessions directly into + * a Smriti DB, bypassing real agent-log parsing entirely (mirrors the + * seedSession() helper in test/learn-consolidate.test.ts). + */ + +import type { Database } from "bun:sqlite"; +import { addMessage } from "../../../src/qmd"; +import { upsertProject, upsertSessionMeta, updateDensityScore } from "../../../src/db"; +import type { RecallScenario } from "./types"; + +const DAY_MS = 24 * 60 * 60 * 1000; + +export async function seedScenario(db: Database, scenario: RecallScenario): Promise { + const projects = new Set([scenario.project, ...scenario.sessions.map((s) => s.project ?? scenario.project)]); + for (const project of projects) upsertProject(db, project); + + for (const session of scenario.sessions) { + const createdAt = new Date(Date.now() - (session.daysAgo ?? 0) * DAY_MS).toISOString(); + for (const m of session.messages) { + await addMessage(db as any, session.id, m.role, m.content, { timestamp: createdAt }); + } + upsertSessionMeta(db, session.id, "claude-code", session.project ?? scenario.project); + if (session.densityScore !== undefined) { + updateDensityScore(db, session.id, session.densityScore); + } + } +} diff --git a/test/eval/fixtures/semantic-caching.ts b/test/eval/fixtures/semantic-caching.ts new file mode 100644 index 0000000..3dd4a6b --- /dev/null +++ b/test/eval/fixtures/semantic-caching.ts @@ -0,0 +1,40 @@ +import type { RecallScenario } from "./types"; + +/** + * Tier 2 only: the probe shares almost no lexical overlap with the source + * session (no "Redis", "cache", or "TTL" in the query) — only a real + * embedding model can bridge "in-memory data store to speed up repeated + * lookups" to "Redis... avoid repeated database round trips". + */ +export const SEMANTIC_CACHING: RecallScenario = { + name: "semantic-only match: caching decision, paraphrased query", + project: "api-service", + requiresEmbeddings: true, + sessions: [ + { + id: "semantic-s1", + messages: [ + { role: "user", content: "The product listing endpoint is slow under load." }, + { role: "assistant", content: "We store frequently accessed data in Redis to avoid repeated database round trips — added a 5-minute TTL for the product listing query." }, + ], + }, + { + id: "semantic-s2-distractor", + messages: [ + { role: "user", content: "Let's add rate limiting to the public API." }, + { role: "assistant", content: "Token bucket, 100 requests per minute per API key." }, + ], + }, + ], + probes: [ + { + query: "why do we use an in-memory data store to speed up repeated lookups", + description: "paraphrased, near-zero lexical overlap with the source session — needs semantic (vector) matching", + expectHitSessionIds: ["semantic-s1"], + expectMissSessionIds: ["semantic-s2-distractor"], + // The project-filtered path never touches vectors — route through + // recallMemories's hybrid pipeline, the only path embeddings affect. + useRecallMemories: true, + }, + ], +}; diff --git a/test/eval/fixtures/types.ts b/test/eval/fixtures/types.ts new file mode 100644 index 0000000..10fa61c --- /dev/null +++ b/test/eval/fixtures/types.ts @@ -0,0 +1,60 @@ +/** + * test/eval/fixtures/types.ts - Shared fixture format for the recall-quality + * harness. A scenario is a small multi-session conversation (optionally + * spanning sessions created at different points in time, via `daysAgo`) + * paired with probes that check both recall (the right session surfaces) + * and precision (a near-topic distractor does not). + * + * Shared by test/recall-quality.test.ts (Tier 1, CI-safe, BM25-only) and + * test/eval/recall-quality.eval.ts (Tier 2, manual, requires embeddings). + */ + +export type FixtureMessage = { role: "user" | "assistant"; content: string }; + +export type FixtureSession = { + id: string; + /** Overrides the scenario's default project — lets one scenario seed sessions across multiple projects to test project-filter isolation. */ + project?: string; + /** How many days before "now" this session was created — exercises recency/density blending. Omit for "just now". */ + daysAgo?: number; + /** density_score to set on this session (0-1). Omit to leave at the default (0). */ + densityScore?: number; + messages: FixtureMessage[]; +}; + +export type Probe = { + query: string; + /** Why a human would ask this — shown in the report, not asserted on. */ + description: string; + /** Session ids that MUST appear in the top-K results. */ + expectHitSessionIds: string[]; + /** At least one of these substrings must appear in a retrieved row belonging to an expected-hit session. */ + expectHitSubstrings?: string[]; + /** Session ids that must NOT appear in the top-K results (precision). */ + expectMissSessionIds?: string[]; + /** Defaults to the harness-wide DEFAULT_TOP_K. */ + topK?: number; + /** Overrides the scenario's default project for this probe's recall() call. */ + project?: string; + /** + * Route through recallMemories's unfiltered hybrid pipeline (RRF + density + * blending) instead of the project-filtered searchFiltered path. Still + * CI-safe without embeddings — vector search silently no-ops when none + * exist. Needed for probes that specifically exercise density/recency + * blending, which the project-filtered path never touches. + */ + useRecallMemories?: boolean; +}; + +export type RecallScenario = { + name: string; + /** Default project for sessions/probes that don't override it. */ + project: string; + sessions: FixtureSession[]; + probes: Probe[]; + /** + * Only runs in Tier 2 (manual, quality mode) — needs a live embedding + * backend to exercise genuinely non-lexical (semantic-only) matches. + */ + requiresEmbeddings?: boolean; +}; diff --git a/test/eval/recall-quality.eval.ts b/test/eval/recall-quality.eval.ts new file mode 100644 index 0000000..11c1196 --- /dev/null +++ b/test/eval/recall-quality.eval.ts @@ -0,0 +1,101 @@ +/** + * test/eval/recall-quality.eval.ts - Tier 2 of the recall-quality harness: + * the full fixture set from test/eval/fixtures/, including scenarios that + * need a live embedding backend to exercise genuinely semantic (non-lexical) + * matches — the CI-safe BM25-only subset already runs automatically as + * test/recall-quality.test.ts. + * + * NOT a bun:test file (no *.test.ts suffix) — needs a live embedding model + * (local llama.cpp, already a dependency, or Ollama via QMD_MEMORY_MODEL) and + * is slower than the CI subset, so it's excluded from `bun test` and run + * manually: + * + * bun run test/eval/recall-quality.eval.ts + */ + +import { initSmriti, closeDb } from "../../src/db"; +import { embedMemoryMessages } from "../../src/qmd"; +import { ALL_SCENARIOS } from "./fixtures/index"; +import { seedScenario } from "./fixtures/seed"; +import { runProbe, DEFAULT_TOP_K } from "./fixtures/run"; +import { summarizeScores, type ProbeScore } from "./fixtures/score"; +import type { RecallScenario } from "./fixtures/types"; + +type ProbeRun = { scenario: RecallScenario; query: string; description: string; score: ProbeScore; latencyMs: number; usedVectors: boolean }; + +async function runScenario(scenario: RecallScenario): Promise { + const db = await initSmriti(":memory:"); + const runs: ProbeRun[] = []; + try { + await seedScenario(db, scenario); + + let embedded = 0; + try { + embedded = await embedMemoryMessages(db as any); + } catch (err: any) { + console.log(` [warn] embedMemoryMessages failed for "${scenario.name}": ${err.message}`); + } + if (scenario.requiresEmbeddings && embedded === 0) { + console.log(` [warn] "${scenario.name}" needs embeddings but none were generated — results below may be BM25-only.`); + } + + for (const probe of scenario.probes) { + const { score, latencyMs, sources } = await runProbe(db, scenario, probe, { fast: false }); + runs.push({ + scenario, + query: probe.query, + description: probe.description, + score, + latencyMs, + usedVectors: sources.includes("vec"), + }); + } + } finally { + await closeDb(); + } + return runs; +} + +async function main() { + const embeddingScenarioCount = ALL_SCENARIOS.filter((s) => s.requiresEmbeddings).length; + console.log(`Running ${ALL_SCENARIOS.length} scenarios (${embeddingScenarioCount} require embeddings)...\n`); + + const allRuns: ProbeRun[] = []; + + for (const scenario of ALL_SCENARIOS) { + console.log(`## ${scenario.name}${scenario.requiresEmbeddings ? " (requires embeddings)" : ""}`); + const runs = await runScenario(scenario); + allRuns.push(...runs); + + for (const r of runs) { + const status = r.score.pass ? "PASS" : "FAIL"; + const vecTag = scenario.requiresEmbeddings ? (r.usedVectors ? " [vec]" : " [vec DID NOT FIRE]") : ""; + console.log( + ` [${status}] "${r.query}" — recall=${r.score.recall.toFixed(2)} precision=${r.score.precision === null ? "n/a" : r.score.precision.toFixed(2)} substrings=${r.score.substringOk}${vecTag} (${r.latencyMs.toFixed(0)}ms)` + ); + if (!r.score.pass) console.log(` ${r.description}`); + } + console.log(); + } + + const summary = summarizeScores(allRuns.map((r) => r.score)); + const vectorScenarios = allRuns.filter((r) => r.scenario.requiresEmbeddings); + const vectorsFired = vectorScenarios.filter((r) => r.usedVectors).length; + + console.log("=".repeat(60)); + console.log("SUMMARY"); + console.log("=".repeat(60)); + console.log(`Probes graded: ${summary.total}`); + console.log(`Probes passed: ${summary.passed}/${summary.total}`); + console.log(`Avg recall: ${summary.avgRecall.toFixed(2)}`); + console.log(`Avg precision: ${summary.avgPrecision === null ? "n/a" : summary.avgPrecision.toFixed(2)}`); + console.log(`Top-K: ${DEFAULT_TOP_K} (per-probe override via topK)`); + if (vectorScenarios.length > 0) { + console.log(`Vector search fired: ${vectorsFired}/${vectorScenarios.length} embedding-dependent probes`); + if (vectorsFired < vectorScenarios.length) { + console.log(` -> some embedding-dependent probes silently fell back to BM25-only. Check that a local embedding model or Ollama is reachable.`); + } + } +} + +await main(); diff --git a/test/forget.test.ts b/test/forget.test.ts new file mode 100644 index 0000000..6c38302 --- /dev/null +++ b/test/forget.test.ts @@ -0,0 +1,198 @@ +/** + * test/forget.test.ts - Tests for the smriti forget (session deletion) layer + * + * Mirrors test/learn-consolidate.test.ts's style: initSmriti(":memory:") so + * QMD's full schema (documents, content_vectors, vectors_vec) exists, a + * seedSession() helper for real message rows, and closeDb() teardown. + */ + +import { test, expect, beforeAll, afterAll } from "bun:test"; +import type { Database } from "bun:sqlite"; +import { + initSmriti, + closeDb, + upsertProject, + upsertSessionMeta, + insertKnowledgeUnit, + promoteKnowledgeUnit, + listKnowledgeUnits, + forgetSession, +} from "../src/db"; +import { listSessions } from "../src/qmd"; +import type { KnowledgeUnit } from "../src/team/types"; + +let db: Database; + +beforeAll(async () => { + db = await initSmriti(":memory:"); +}); + +afterAll(async () => { + await closeDb(); +}); + +function seedSession( + sessionId: string, + projectId: string, + messages: Array<{ role: string; content: string }> +) { + const now = new Date().toISOString(); + db.prepare( + `INSERT INTO memory_sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)` + ).run(sessionId, `Session ${sessionId}`, now, now); + + const insertMsg = db.prepare( + `INSERT INTO memory_messages (session_id, role, content, hash, created_at) VALUES (?, ?, ?, ?, ?)` + ); + for (const [i, m] of messages.entries()) { + insertMsg.run(sessionId, m.role, m.content, `${sessionId}-h${i}`, now); + } + + upsertProject(db, projectId); + upsertSessionMeta(db, sessionId, "claude-code", projectId); +} + +const SAMPLE_MESSAGES = [ + { role: "user", content: "What's our rate limiting strategy?" }, + { role: "assistant", content: "Token bucket, 100 req/min per API key." }, +]; + +// ============================================================================= +// Soft delete +// ============================================================================= + +test("forgetSession soft-deletes by default: hidden from list, kept with includeInactive", () => { + seedSession("soft-s1", "forgetproj", SAMPLE_MESSAGES); + + const result = forgetSession(db, "soft-s1"); + expect(result.hard).toBe(false); + + const active = listSessions(db as any, { includeInactive: false }); + expect(active.map((s: any) => s.id)).not.toContain("soft-s1"); + + const all = listSessions(db as any, { includeInactive: true }); + expect(all.map((s: any) => s.id)).toContain("soft-s1"); + + // Messages are untouched by a soft delete. + const msgs = db + .prepare(`SELECT COUNT(*) as c FROM memory_messages WHERE session_id = ?`) + .get("soft-s1") as { c: number }; + expect(msgs.c).toBe(SAMPLE_MESSAGES.length); +}); + +// ============================================================================= +// Hard delete +// ============================================================================= + +test("forgetSession --hard removes messages, sidecar rows, and unpromoted knowledge units", () => { + seedSession("hard-s1", "forgetproj", SAMPLE_MESSAGES); + + db.prepare( + `INSERT INTO smriti_session_tags (session_id, category_id, confidence, source) VALUES (?, ?, ?, ?)` + ).run("hard-s1", "bug/fix", 0.9, "auto"); + + const segmented: KnowledgeUnit = { + id: "hard-unit-segmented", + topic: "Never promoted", + category: "code/pattern", + relevance: 2, + entities: [], + files: [], + plainText: "Low relevance, never promoted.", + lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, segmented, "hard-s1", "forgetproj", "hard-hash-1"); + db.prepare( + `INSERT INTO smriti_relationships (subject_type, subject_id, predicate, object_type, object_id) VALUES ('knowledge_unit', ?, 'mentions', 'entity', 'rate-limiting')` + ).run("hard-unit-segmented"); + + const result = forgetSession(db, "hard-s1", { hard: true }); + expect(result.hard).toBe(true); + expect(result.unitsDeleted).toBe(1); + + const msgs = db + .prepare(`SELECT COUNT(*) as c FROM memory_messages WHERE session_id = ?`) + .get("hard-s1") as { c: number }; + expect(msgs.c).toBe(0); + + const sessionRow = db.prepare(`SELECT 1 FROM memory_sessions WHERE id = ?`).get("hard-s1"); + expect(sessionRow).toBeNull(); + + const tags = db + .prepare(`SELECT COUNT(*) as c FROM smriti_session_tags WHERE session_id = ?`) + .get("hard-s1") as { c: number }; + expect(tags.c).toBe(0); + + const meta = db.prepare(`SELECT 1 FROM smriti_session_meta WHERE session_id = ?`).get("hard-s1"); + expect(meta).toBeNull(); + + const unit = db.prepare(`SELECT 1 FROM smriti_knowledge_units WHERE id = ?`).get("hard-unit-segmented"); + expect(unit).toBeNull(); + + const edges = db + .prepare(`SELECT COUNT(*) as c FROM smriti_relationships WHERE subject_id = ?`) + .get("hard-unit-segmented") as { c: number }; + expect(edges.c).toBe(0); +}); + +test("forgetSession --hard keeps canonical units and their doc/share unless --purge-shared", () => { + seedSession("hard-s2", "forgetproj", SAMPLE_MESSAGES); + + const canonical: KnowledgeUnit = { + id: "hard-unit-canonical", + topic: "Already shared decision", + category: "architecture/decision", + relevance: 9, + entities: [], + files: [], + plainText: "Already promoted and shared.", + lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, canonical, "hard-s2", "forgetproj", "hard-hash-2"); + promoteKnowledgeUnit(db, "hard-unit-canonical", "knowledge/architecture-decision/doc.md", "share-1"); + db.prepare( + `INSERT INTO smriti_shares (id, session_id, unit_id) VALUES (?, ?, ?)` + ).run("share-1", "hard-s2", "hard-unit-canonical"); + + const result = forgetSession(db, "hard-s2", { hard: true }); + expect(result.canonicalKept).toBe(1); + expect(result.unitsPurged).toBe(0); + + const kept = listKnowledgeUnits(db, { tier: "canonical" }).find( + (u) => u.id === "hard-unit-canonical" + ); + expect(kept).toBeDefined(); + + const share = db.prepare(`SELECT 1 FROM smriti_shares WHERE id = ?`).get("share-1"); + expect(share).toBeTruthy(); +}); + +test("forgetSession --hard --purge-shared removes canonical units and their share row", () => { + seedSession("hard-s3", "forgetproj", SAMPLE_MESSAGES); + + const canonical: KnowledgeUnit = { + id: "purge-unit-canonical", + topic: "Purge me too", + category: "architecture/decision", + relevance: 9, + entities: [], + files: [], + plainText: "Promoted, but this session is being fully purged.", + lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, canonical, "hard-s3", "forgetproj", "hard-hash-3"); + promoteKnowledgeUnit(db, "purge-unit-canonical", "knowledge/architecture-decision/purge-me.md", "share-2"); + db.prepare( + `INSERT INTO smriti_shares (id, session_id, unit_id) VALUES (?, ?, ?)` + ).run("share-2", "hard-s3", "purge-unit-canonical"); + + const result = forgetSession(db, "hard-s3", { hard: true, purgeShared: true }); + expect(result.unitsPurged).toBe(1); + expect(result.canonicalKept).toBe(0); + + const unit = db.prepare(`SELECT 1 FROM smriti_knowledge_units WHERE id = ?`).get("purge-unit-canonical"); + expect(unit).toBeNull(); + + const share = db.prepare(`SELECT 1 FROM smriti_shares WHERE id = ?`).get("share-2"); + expect(share).toBeNull(); +}); diff --git a/test/learn-consolidate.test.ts b/test/learn-consolidate.test.ts index 4607497..fa1a5bf 100644 --- a/test/learn-consolidate.test.ts +++ b/test/learn-consolidate.test.ts @@ -9,7 +9,7 @@ import { test, expect, beforeAll, afterAll, mock } from "bun:test"; import type { Database } from "bun:sqlite"; -import { mkdirSync, rmSync, writeFileSync, existsSync } from "fs"; +import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { @@ -20,12 +20,15 @@ import { updateDensityScore, insertKnowledgeUnit, listKnowledgeUnits, + promoteKnowledgeUnit, } from "../src/db"; import { consolidateKnowledge, + pruneKnowledge, classifyRelationshipsTextFormat, classifyRelationshipsToolCall, } from "../src/learn/consolidate"; +import { insertRelationship, getRelationships } from "../src/learn/entities"; import { recall } from "../src/search/recall"; import type { KnowledgeUnit } from "../src/team/types"; @@ -425,3 +428,124 @@ test("classifyRelationshipsToolCall returns nothing when the model answers witho globalThis.fetch = originalFetch; } }); + +// ============================================================================= +// Prune (stale segmented units, superseded canonical units) +// ============================================================================= + +function backdateUnit(unitId: string, daysAgo: number) { + db.prepare( + `UPDATE smriti_knowledge_units SET created_at = datetime('now', '-' || ? || ' days') WHERE id = ?` + ).run(daysAgo, unitId); +} + +test("pruneKnowledge dry-run reports stale segmented units without deleting them", async () => { + const stale: KnowledgeUnit = { + id: "prune-stale-1", + topic: "Never promoted, never retrieved", + category: "code/pattern", + relevance: 3, + entities: [], + files: [], + plainText: "Low relevance, sat unused for weeks.", + lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, stale, "prune-s1", "pruneproj", "prune-hash-1"); + backdateUnit("prune-stale-1", 45); + + const result = await pruneKnowledge(db, { dryRun: true, pruneStaleDays: 30, minRelevance: 8 }); + + expect(result.unitsPruned).toBe(0); + expect(result.unitsArchived).toBe(0); + expect(result.pruneCandidates?.map((c) => c.id)).toContain("prune-stale-1"); + + const stillThere = listKnowledgeUnits(db, { tier: "segmented" }).find((u) => u.id === "prune-stale-1"); + expect(stillThere).toBeDefined(); +}); + +test("pruneKnowledge --apply deletes stale segmented units and their relationship edges", async () => { + const stale: KnowledgeUnit = { + id: "prune-apply-1", + topic: "Deletable stale unit", + category: "code/pattern", + relevance: 2, + entities: [], + files: [], + plainText: "Stale and about to be deleted.", + lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, stale, "prune-s2", "pruneproj", "prune-hash-2"); + backdateUnit("prune-apply-1", 45); + insertRelationship(db, "knowledge_unit", "prune-apply-1", "mentions", "entity", "some-entity"); + + const result = await pruneKnowledge(db, { dryRun: false, pruneStaleDays: 30, minRelevance: 8 }); + + expect(result.unitsPruned).toBeGreaterThanOrEqual(1); + const deleted = db.prepare(`SELECT 1 FROM smriti_knowledge_units WHERE id = ?`).get("prune-apply-1"); + expect(deleted).toBeNull(); + + const edges = getRelationships(db, { subjectType: "knowledge_unit", subjectId: "prune-apply-1" }); + expect(edges.length).toBe(0); +}); + +test("pruneKnowledge never prunes high-relevance segmented units even at zero retrievals", async () => { + const highRelevance: KnowledgeUnit = { + id: "prune-high-relevance", + topic: "One consolidate run away from promoting", + category: "architecture/decision", + relevance: 9, + entities: [], + files: [], + plainText: "High relevance, just hasn't been promoted yet.", + lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, highRelevance, "prune-s3", "pruneproj", "prune-hash-3"); + backdateUnit("prune-high-relevance", 45); + + const result = await pruneKnowledge(db, { dryRun: true, pruneStaleDays: 30, minRelevance: 8 }); + + expect(result.pruneCandidates?.map((c) => c.id)).not.toContain("prune-high-relevance"); +}); + +test("pruneKnowledge archives superseded canonical units and appends a banner to their doc", async () => { + const outputDir = join(tmpDir, "prune-archive-output"); + const docRelPath = "knowledge/architecture-decision/old-doc.md"; + const docFullPath = join(outputDir, docRelPath); + mkdirSync(join(outputDir, "knowledge/architecture-decision"), { recursive: true }); + writeFileSync(docFullPath, "---\nid: old-unit\n---\n\n# Old guidance\n\nUse a 1-minute TTL."); + + const oldUnit: KnowledgeUnit = { + id: "prune-superseded", topic: "Old Redis TTL guidance", category: "architecture/decision", relevance: 9, + entities: [], files: [], plainText: "Use a 1-minute TTL.", lineRanges: [{ start: 0, end: 1 }], + }; + const newUnit: KnowledgeUnit = { + id: "prune-superseder", topic: "New Redis TTL guidance", category: "architecture/decision", relevance: 9, + entities: [], files: [], plainText: "Use a 5-minute TTL instead.", lineRanges: [{ start: 0, end: 1 }], + }; + insertKnowledgeUnit(db, oldUnit, "prune-s4", "pruneproj", "prune-hash-old"); + insertKnowledgeUnit(db, newUnit, "prune-s5", "pruneproj", "prune-hash-new"); + promoteKnowledgeUnit(db, "prune-superseded", docRelPath, "prune-share-1"); + promoteKnowledgeUnit(db, "prune-superseder", "knowledge/architecture-decision/new-doc.md", "prune-share-2"); + insertRelationship(db, "knowledge_unit", "prune-superseder", "supersedes", "knowledge_unit", "prune-superseded", { + source: "llm", + }); + + const result = await pruneKnowledge(db, { dryRun: false, outputDir }); + + expect(result.unitsArchived).toBe(1); + + const archived = listKnowledgeUnits(db, { tier: "archived" }).find((u) => u.id === "prune-superseded"); + expect(archived).toBeDefined(); + expect(archived!.archived_reason).toBe("superseded"); + + const docContent = readFileSync(docFullPath, "utf-8"); + expect(docContent).toContain("Archived"); + expect(docContent).toContain("New Redis TTL guidance"); + + // The supersedes edge that justified archiving (and any mentions edges) + // are the audit trail — left untouched, not cascade-deleted. + const supersedeEdge = getRelationships(db, { + subjectType: "knowledge_unit", subjectId: "prune-superseder", predicate: "supersedes", objectId: "prune-superseded", + }); + expect(supersedeEdge.length).toBe(1); +}); diff --git a/test/recall-quality.test.ts b/test/recall-quality.test.ts new file mode 100644 index 0000000..99ccfa1 --- /dev/null +++ b/test/recall-quality.test.ts @@ -0,0 +1,38 @@ +/** + * test/recall-quality.test.ts - Tier 1 of the recall-quality harness: the + * BM25-only scenarios from test/eval/fixtures/, run deterministically with + * no embedding backend needed (recall's project-filtered path never touches + * vectors; recallMemories's vector search silently no-ops with none). + * + * This is the CI-safe subset — it catches regressions in recall()'s + * filtering/dedup/RRF/density-blending wiring automatically. The full + * fixture set (including embedding-dependent scenarios) runs manually via + * `bun run eval:recall` (test/eval/recall-quality.eval.ts). + */ + +import { test, expect } from "bun:test"; +import { initSmriti, closeDb } from "../src/db"; +import { CI_SCENARIOS } from "./eval/fixtures/index"; +import { seedScenario } from "./eval/fixtures/seed"; +import { runProbe } from "./eval/fixtures/run"; + +for (const scenario of CI_SCENARIOS) { + test(`recall quality: ${scenario.name}`, async () => { + const db = await initSmriti(":memory:"); + try { + await seedScenario(db, scenario); + + for (const probe of scenario.probes) { + const { score } = await runProbe(db, scenario, probe, { fast: true }); + + expect(score.recall, `recall for "${probe.query}" (${probe.description})`).toBe(1); + if (score.precision !== null) { + expect(score.precision, `precision for "${probe.query}" (${probe.description})`).toBe(1); + } + expect(score.substringOk, `substring check for "${probe.query}" (${probe.description})`).toBe(true); + } + } finally { + await closeDb(); + } + }); +} From 9911a3254ba5558eafb0450e498983638f6b5267 Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 2 Aug 2026 14:21:08 +0530 Subject: [PATCH 11/13] fix(test): update learn-entities relationship mocks for tool-call format mockOllamaFetch's `relation` handler still simulated the old /api/generate free-text response ("RELATION [i]: predicate"), but inferRelationships was switched to classifyRelationshipsToolCall, which calls ollamaChat -> /api/chat with a `messages` body (no `prompt` field) and expects a native record_relationships tool call back. The mismatch made the mocked fetch throw (reading .includes on the now-undefined body.prompt), silently swallowed by inferRelationships' best-effort try/catch, so both tests asserting on the inferred edges failed with 0 edges instead of 1. Distinguish /api/generate (stage1/stage2, has body.prompt) from /api/chat (relation inference, no body.prompt) and return a tool_calls-shaped response for the latter; relation handlers now return structured {index, predicate} guesses instead of a free-text string. --- test/learn-entities.test.ts | 48 +++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/test/learn-entities.test.ts b/test/learn-entities.test.ts index 2c91e55..90e65f8 100644 --- a/test/learn-entities.test.ts +++ b/test/learn-entities.test.ts @@ -63,21 +63,49 @@ function seedSession(sessionId: string, projectId: string, messages: Array<{ rol upsertSessionMeta(db, sessionId, "claude-code", projectId); } -function mockOllamaFetch(handlers: { stage1?: () => object; relation?: () => string; stage2?: () => string }) { +type RelationGuessLite = { index: number; predicate: string }; + +/** + * Stage 1 (segmentSession) and Stage 2 (generateDocument) go through + * callOllama's /api/generate, with a `prompt` field on the request body. + * Promote-time relationship inference (classifyRelationshipsToolCall) goes + * through ollamaChat's /api/chat instead — no `prompt` field, a `messages` + * array plus a native tool call in the response instead of free text. + */ +function mockOllamaFetch(handlers: { stage1?: () => object; relation?: () => RelationGuessLite[]; stage2?: () => string }) { return mock(async (_url: string, init: any) => { const body = JSON.parse(init.body); - const prompt = body.prompt as string; - if (prompt.includes("Knowledge Unit Segmentation")) { + + if (typeof body.prompt === "string") { + if (body.prompt.includes("Knowledge Unit Segmentation")) { + return new Response( + JSON.stringify({ response: "```json\n" + JSON.stringify((handlers.stage1 ?? (() => ({ units: [] })))()) + "\n```" }), + { status: 200 } + ); + } return new Response( - JSON.stringify({ response: "```json\n" + JSON.stringify((handlers.stage1 ?? (() => ({ units: [] })))()) + "\n```" }), + JSON.stringify({ response: (handlers.stage2 ?? (() => "# Doc\n\nContent."))() }), { status: 200 } ); } - if (prompt.includes("CANDIDATES")) { - return new Response(JSON.stringify({ response: (handlers.relation ?? (() => ""))() }), { status: 200 }); - } + return new Response( - JSON.stringify({ response: (handlers.stage2 ?? (() => "# Doc\n\nContent."))() }), + JSON.stringify({ + model: "test-model", + message: { + role: "assistant", + content: "", + tool_calls: [ + { + function: { + name: "record_relationships", + arguments: { relationships: (handlers.relation ?? (() => []))() }, + }, + }, + ], + }, + done: true, + }), { status: 200 } ); }); @@ -263,7 +291,7 @@ test("promote phase persists LLM-inferred relatesTo/supersedes/contradicts edges insertRelationship(db, "knowledge_unit", "infer-new", "mentions", "entity", redisId); const originalFetch = globalThis.fetch; - globalThis.fetch = mockOllamaFetch({ relation: () => "RELATION [0]: supersedes" }) as any; + globalThis.fetch = mockOllamaFetch({ relation: () => [{ index: 0, predicate: "supersedes" }] }) as any; try { const result = await consolidateKnowledge(db, { @@ -308,7 +336,7 @@ test("promote phase never asserts a directional predicate in both directions for // Always answers "supersedes" regardless of which side is asking — the // worst case for this bug, and realistic for a small/local model given a // prompt with no explicit recency signal. - globalThis.fetch = mockOllamaFetch({ relation: () => "RELATION [0]: supersedes" }) as any; + globalThis.fetch = mockOllamaFetch({ relation: () => [{ index: 0, predicate: "supersedes" }] }) as any; try { const result = await consolidateKnowledge(db, { From a617247689bf2b14379b87f1df97946faf1ada05 Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 2 Aug 2026 15:24:29 +0530 Subject: [PATCH 12/13] fix(memory): guard density-blending against a bare QMD store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recallMemories' density-blending step queries smriti_session_meta directly, but src/memory.ts moved out of the qmd submodule specifically to stay usable as a clean, Smriti-agnostic layer (see the dev merge) — scripts/bench-qmd.ts runs it against a bare QMD store with no Smriti tables at all, which crashed with "no such table: smriti_session_meta". Wrap it in the same try/catch pattern already used for the vector-search fallback a few lines up: no smriti_session_meta means no density signal, so skip the blend and keep the RRF/rerank-only ordering. --- src/memory.ts | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/memory.ts b/src/memory.ts index 2b95288..f5045c1 100644 --- a/src/memory.ts +++ b/src/memory.ts @@ -866,23 +866,31 @@ export async function recallMemories( } } - // Blend density scores into recall scores — dense sessions rank higher + // Blend density scores into recall scores — dense sessions rank higher. + // smriti_session_meta is a Smriti-layer table, not a QMD core one — this + // file is meant to stay usable against a bare QMD store (e.g. + // scripts/bench-qmd.ts), so a missing table degrades gracefully instead + // of throwing, same as the vector-search fallback above. if (dedupedResults.length > 0) { - const sessionIds = dedupedResults.map((r) => r.session_id); - const placeholders = sessionIds.map(() => "?").join(","); - const densityRows = (db as any) - .prepare( - `SELECT session_id, COALESCE(density_score, 0) as density_score - FROM smriti_session_meta WHERE session_id IN (${placeholders})` - ) - .all(...sessionIds) as { session_id: string; density_score: number }[]; - const densityMap = new Map(densityRows.map((r) => [r.session_id, r.density_score])); + try { + const sessionIds = dedupedResults.map((r) => r.session_id); + const placeholders = sessionIds.map(() => "?").join(","); + const densityRows = (db as any) + .prepare( + `SELECT session_id, COALESCE(density_score, 0) as density_score + FROM smriti_session_meta WHERE session_id IN (${placeholders})` + ) + .all(...sessionIds) as { session_id: string; density_score: number }[]; + const densityMap = new Map(densityRows.map((r) => [r.session_id, r.density_score])); - for (const r of dedupedResults) { - const ds = densityMap.get(r.session_id) ?? 0; - r.score = r.score * 0.8 + ds * 0.2; + for (const r of dedupedResults) { + const ds = densityMap.get(r.session_id) ?? 0; + r.score = r.score * 0.8 + ds * 0.2; + } + dedupedResults.sort((a, b) => b.score - a.score); + } catch { + // smriti_session_meta doesn't exist (bare QMD store) — skip blending. } - dedupedResults.sort((a, b) => b.score - a.score); } const results = dedupedResults.slice(0, limit); From 4dc3ff4129e83b59c404b31bb24bd2a4408ac1dd Mon Sep 17 00:00:00 2001 From: Ashutosh Tripathi Date: Sun, 2 Aug 2026 15:27:26 +0530 Subject: [PATCH 13/13] chore: bump version to 0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e5d495..ef2b85c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smriti", - "version": "0.8.2", + "version": "0.9.0", "description": "Smriti - Unified memory layer across all AI agents", "type": "module", "bin": {