import { existsSync } from "node:fs"; import { join } from "node:path"; import type { KnowledgeDiagnostic } from "./knowledge-document.js"; import type { Registry } from "./metadata.js"; import { readJson } from "./utils.js"; import type { VaultPaths } from "./utils.js"; import type { KnowledgeFormat } from "./vault-format.js"; import { compareCodePoint, discoverKnowledgeDocuments, inspectVaultFormat, } from "./vault-format.js"; /** * Shared wiki service functions consumed by Pi and MCP. * * These are pure adapters over the shared document model and registry. * No YAML parsing, file scanning, or page serialization here. */ export interface RegistrySearchResult { matches: Array<{ id: string; title: string; type: string }>; diagnostics: KnowledgeDiagnostic[]; } export interface WikiStatusSnapshot { knowledgeFormat: KnowledgeFormat; totalPages: number; byType: Record; blockingDiagnostics: KnowledgeDiagnostic[]; lastUpdated: string; } /** * Search the registry for matching concepts. * * Matches ID, semantic title, type, category, domain, tags, aliases, and recall triggers. * Preserves unknown types as strings. */ export function searchRegistry( paths: VaultPaths, query: string, typeFilter?: string, ): RegistrySearchResult { const diagnostics: KnowledgeDiagnostic[] = []; const vaultState = inspectVaultFormat(paths); diagnostics.push(...vaultState.diagnostics); const registryPath = join(paths.meta, "registry.json"); if (!existsSync(registryPath)) { return { matches: [], diagnostics }; } const registry = readJson(registryPath, { version: "1.0", last_updated: "", pages: {}, }); const normalizedQuery = query.toLowerCase(); const matches: Array<{ id: string; title: string; type: string }> = []; for (const [id, entry] of Object.entries(registry.pages)) { // Apply type filter if (typeFilter && String(entry.type).toLowerCase() !== typeFilter.toLowerCase()) { continue; } // Check if query matches any searchable field if (matchesField(id, entry, normalizedQuery)) { matches.push({ id, title: String(entry.title || id), type: String(entry.type || "unknown"), }); } } // Sort by code point order for determinism matches.sort((a, b) => compareCodePoint(a.id, b.id)); return { matches, diagnostics }; } function matchesField(id: string, entry: Record, query: string): boolean { // Match ID if (id.toLowerCase().includes(query)) return true; // Match title if ( String(entry.title || "") .toLowerCase() .includes(query) ) return true; // Match type if ( String(entry.type || "") .toLowerCase() .includes(query) ) return true; // Match category/domain if ( String(entry.category || "") .toLowerCase() .includes(query) ) return true; if ( String(entry.domain || "") .toLowerCase() .includes(query) ) return true; // Match tags (array or string) const tags = entry.tags; if (Array.isArray(tags)) { if (tags.some((t) => String(t).toLowerCase().includes(query))) return true; } else if (typeof tags === "string" && tags.toLowerCase().includes(query)) { return true; } // Match aliases (array or string) const aliases = entry.aliases; if (Array.isArray(aliases)) { if (aliases.some((a) => String(a).toLowerCase().includes(query))) return true; } else if (typeof aliases === "string" && aliases.toLowerCase().includes(query)) { return true; } // Match recall_triggers (array or string) const triggers = entry.recall_triggers; if (Array.isArray(triggers)) { if (triggers.some((t) => String(t).toLowerCase().includes(query))) return true; } else if (typeof triggers === "string" && triggers.toLowerCase().includes(query)) { return true; } return false; } /** * Get a status snapshot of the wiki. * * Reports resolved knowledge_format, page counts, and blocking diagnostics. */ export function getWikiStatus(paths: VaultPaths): WikiStatusSnapshot { const vaultState = inspectVaultFormat(paths); const diagnostics = [...vaultState.diagnostics]; // Also check discovery for current concept health const discovery = discoverKnowledgeDocuments(paths); diagnostics.push(...discovery.diagnostics); const registryPath = join(paths.meta, "registry.json"); let registry: Registry | undefined; if (existsSync(registryPath)) { registry = readJson(registryPath, { version: "1.0", last_updated: "", pages: {} }); } const byType: Record = {}; let totalPages = 0; if (registry) { for (const entry of Object.values(registry.pages)) { totalPages++; const type = String(entry.type || "unknown"); byType[type] = (byType[type] || 0) + 1; } } return { knowledgeFormat: vaultState.knowledgeFormat, totalPages, byType, blockingDiagnostics: diagnostics.filter((d) => d.severity === "error"), lastUpdated: registry?.last_updated || "", }; }