llm-wiki-compiler 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/constants.ts","../src/utils/claude-settings.ts","../src/utils/provider-guard.ts","../src/sdk/wiki.ts","../src/utils/output.ts","../src/commands/ingest.ts","../src/utils/markdown.ts","../src/utils/source-writer.ts","../src/utils/path-confine.ts","../src/viewer/path-safety.ts","../src/utils/activity-log.ts","../src/ingest/web.ts","../src/ingest/file.ts","../src/ingest/shared.ts","../src/ingest/pdf.ts","../src/ingest/image.ts","../src/providers/anthropic.ts","../src/providers/voyage-embed.ts","../src/ingest/transcript.ts","../src/compiler/index.ts","../src/utils/state.ts","../src/compiler/source-state.ts","../src/compiler/hasher.ts","../src/providers/openai.ts","../src/providers/ollama.ts","../src/providers/minimax.ts","../src/providers/copilot.ts","../src/providers/claude-agent.ts","../src/providers/json-schema-to-zod.ts","../src/utils/provider.ts","../src/utils/llm.ts","../src/utils/lock.ts","../src/utils/output-language.ts","../src/compiler/prompts.ts","../src/schema/types.ts","../src/schema/defaults.ts","../src/schema/loader.ts","../src/schema/helpers.ts","../src/compiler/deps.ts","../src/compiler/orphan.ts","../src/compiler/resolver.ts","../src/compiler/indexgen.ts","../src/compiler/prompt-budget.ts","../src/compiler/obsidian.ts","../src/compiler/provenance.ts","../src/utils/embeddings.ts","../src/utils/retrieval.ts","../src/compiler/candidates.ts","../src/utils/candidate-store.ts","../src/linter/rules.ts","../src/freshness/index.ts","../src/compiler/page-renderer.ts","../src/commands/query.ts","../src/linter/index.ts","../src/viewer/snapshot.ts","../src/wiki/collect.ts","../src/viewer/collect.ts","../src/viewer/graph.ts","../src/project/state.ts","../src/linter/cache.ts","../src/project/recommendations.ts","../src/viewer/search.ts","../src/context/provenance.ts","../src/context/ranking.ts","../src/context/retrieval.ts","../src/context/graph.ts","../src/context/budget.ts","../src/context/types.ts","../src/context/build.ts","../src/commands/export.ts","../src/export/collect.ts","../src/export/provenance.ts","../src/export/project-id.ts","../src/export/json-export.ts","../src/export/graphml.ts","../src/eval/health.ts","../src/eval/citation-coverage.ts","../src/eval/source-path.ts","../src/eval/source-utilization.ts","../src/eval/citation-depth.ts","../src/eval/citation-support.ts","../src/eval/stats.ts","../src/eval/delta.ts","../src/eval/thresholds.ts","../src/eval/index.ts","../src/status/collect.ts","../src/search/retrieval.ts","../src/pages/read.ts","../src/pages/list.ts","../src/sources/store.ts"],"sourcesContent":["/**\n * Shared constants for the llmwiki knowledge compiler.\n * Centralized config values to avoid magic numbers scattered across the codebase.\n */\n\n/** Maximum source file size in characters before truncation. */\nexport const MAX_SOURCE_CHARS = 100_000;\n\n/** Minimum source content length to ingest without a warning. */\nexport const MIN_SOURCE_CHARS = 50;\n\n/**\n * Default character budget for the combined source content sent to the LLM\n * during page generation for a single concept (issue #39).\n *\n * Caps the per-prompt content at ~200,000 chars (~50k tokens). When two or\n * more sources contribute to the same concept and their combined raw size\n * exceeds this budget, each source's slice is proportionally truncated so\n * the prompt fits the model's context window. Without this cap, popular\n * concepts that appear in many overlapping documents reliably blow past\n * the LLM provider's context limit and the compile crashes.\n *\n * Override via the LLMWIKI_PROMPT_BUDGET_CHARS env var when running against\n * larger-context (raise) or smaller-context (lower) models.\n */\nexport const DEFAULT_PROMPT_BUDGET_CHARS = 200_000;\n\n/** Env var that overrides DEFAULT_PROMPT_BUDGET_CHARS at runtime. */\nexport const PROMPT_BUDGET_ENV_VAR = \"LLMWIKI_PROMPT_BUDGET_CHARS\";\n\n/** Number of most relevant wiki pages to load for query context. */\nexport const QUERY_PAGE_LIMIT = 5;\n\n/** Maximum concurrent API calls during page generation. */\nexport const COMPILE_CONCURRENCY = 5;\n\n/** API retry configuration. */\nexport const RETRY_COUNT = 3;\nexport const RETRY_BASE_MS = 1000;\nexport const RETRY_MULTIPLIER = 4;\n\n/** Default provider when LLMWIKI_PROVIDER is not set. */\nexport const DEFAULT_PROVIDER = \"anthropic\";\n\n/** Default model per provider. */\nexport const PROVIDER_MODELS: Record<string, string> = {\n anthropic: \"claude-sonnet-4-6\",\n \"claude-agent\": \"claude-sonnet-4-6\",\n openai: \"gpt-4o\",\n ollama: \"llama3.1\",\n minimax: \"MiniMax-M2.7\",\n copilot: \"gpt-4o\",\n};\n\n/** Default Ollama API base URL. */\nexport const OLLAMA_DEFAULT_HOST = \"http://localhost:11434/v1\";\n\n/** GitHub Copilot API base URL (OpenAI-compatible, requires OAuth token). */\nexport const COPILOT_BASE_URL = \"https://api.githubcopilot.com\";\n\n/**\n * Default request timeout for cloud OpenAI-compatible providers (10 minutes).\n * Matches the OpenAI SDK's own default; called out here so it's explicit.\n */\nexport const OPENAI_DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;\n\n/**\n * Default request timeout for Ollama (30 minutes). Local models on modest\n * hardware can take well over the cloud-provider default for a single\n * compile-time completion. Configurable via LLMWIKI_REQUEST_TIMEOUT_MS or\n * OLLAMA_TIMEOUT_MS env vars.\n */\nexport const OLLAMA_DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;\n\n/** Directory names relative to the project root. */\nexport const SOURCES_DIR = \"sources\";\nexport const CONCEPTS_DIR = \"wiki/concepts\";\nexport const QUERIES_DIR = \"wiki/queries\";\nexport const LLMWIKI_DIR = \".llmwiki\";\nexport const STATE_FILE = \".llmwiki/state.json\";\nexport const LOCK_FILE = \".llmwiki/lock\";\nexport const INDEX_FILE = \"wiki/index.md\";\nexport const MOC_FILE = \"wiki/MOC.md\";\n\n/**\n * Append-only activity journal at the project root, per Karpathy's llm-wiki\n * gist: a chronological record of what happened and when (ingests, compiles,\n * queries, lint passes). Lives at the root rather than under wiki/ because it\n * spans project-level operations — ingest writes before any wiki/ exists.\n */\nexport const LOG_FILE = \"log.md\";\n\n/** Max characters for a single log.md entry description before truncation. */\nexport const LOG_DESCRIPTION_MAX_CHARS = 200;\n\n/**\n * Max number of page wikilinks listed in a single log.md entry detail line.\n * Beyond this the list is truncated with a \"(+N more)\" suffix so a large\n * compile doesn't write a wall of links into the journal.\n */\nexport const LOG_MAX_PAGE_LINKS = 20;\nexport const EMBEDDINGS_FILE = \".llmwiki/embeddings.json\";\nexport const LAST_LINT_FILE = \".llmwiki/last-lint.json\";\n\n/** Supported image file extensions for vision-based ingest. */\nexport const IMAGE_EXTENSIONS = new Set([\".jpg\", \".jpeg\", \".png\", \".gif\", \".webp\"]);\n\n/** Supported transcript file extensions (content-sniff .txt separately). */\nexport const TRANSCRIPT_EXTENSIONS = new Set([\".vtt\", \".srt\"]);\n\n/** Max tokens for image-description completions. */\nexport const IMAGE_DESCRIBE_MAX_TOKENS = 2048;\n\n/** Pending review candidates awaiting approval/rejection. */\nexport const CANDIDATES_DIR = \".llmwiki/candidates\";\n\n/** Rejected review candidates archived for audit (not deleted). */\nexport const CANDIDATES_ARCHIVE_DIR = \".llmwiki/candidates/archive\";\n\n/**\n * Per-source hashes already processed by `rules extract` (rule pipeline). Kept\n * separate from STATE_FILE so rule extraction and concept compilation advance\n * their change-detection cursors independently.\n */\nexport const RULE_STATE_FILE = \".llmwiki/rule-state.json\";\n\n/** Pending rule candidates (rule pipeline) awaiting approve/reject. */\nexport const RULE_CANDIDATES_DIR = \".llmwiki/rule-candidates\";\n\n/** Rejected rule candidates archived for audit (not deleted). */\nexport const RULE_CANDIDATES_ARCHIVE_DIR = \".llmwiki/rule-candidates/archive\";\n\n/** Number of most similar pages to return from embedding-based pre-filter. */\nexport const EMBEDDING_TOP_K = 15;\n\n/** Number of chunk candidates to retain after the semantic-similarity step. */\nexport const CHUNK_TOP_K = 30;\n\n/** Number of chunk candidates to keep after reranking. */\nexport const CHUNK_RERANK_KEEP = 12;\n\n/** Target chunk size in characters; chunks try to land near this length. */\nexport const CHUNK_TARGET_CHARS = 800;\n\n/** Hard upper bound on a single chunk's character length. */\nexport const CHUNK_MAX_CHARS = 1_400;\n\n/** Minimum standalone chunk size; smaller trailing fragments are merged back. */\nexport const CHUNK_MIN_CHARS = 200;\n\n/** Provenance metadata thresholds used by lint rules. */\nexport const LOW_CONFIDENCE_THRESHOLD = 0.5;\nexport const MAX_INFERRED_PARAGRAPHS_WITHOUT_CITATIONS = 2;\n\n/** Embedding model to use per provider. */\nexport const EMBEDDING_MODELS: Record<string, string> = {\n anthropic: \"voyage-3-lite\",\n \"claude-agent\": \"voyage-3-lite\",\n openai: \"text-embedding-3-small\",\n ollama: \"nomic-embed-text\",\n};\n","/**\n * Claude settings fallback helpers.\n *\n * Provides a narrow, read-only integration with `~/.claude/settings.json`.\n * We only read the `env` object and only extract Anthropic-related values that\n * llmwiki can safely consume. Explicit process env values remain higher priority.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nconst CLAUDE_SETTINGS_PATH_ENV = \"LLMWIKI_CLAUDE_SETTINGS_PATH\";\n\ninterface ClaudeSettingsEnv {\n ANTHROPIC_API_KEY?: string;\n ANTHROPIC_AUTH_TOKEN?: string;\n ANTHROPIC_BASE_URL?: string;\n ANTHROPIC_MODEL?: string;\n}\n\ninterface AnthropicAuthConfig {\n apiKey?: string;\n authToken?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction normalize(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction resolveClaudeSettingsPath(env: NodeJS.ProcessEnv): string {\n return env[CLAUDE_SETTINGS_PATH_ENV] ?? path.join(homedir(), \".claude\", \"settings.json\");\n}\n\nfunction readClaudeSettingsFile(settingsPath: string): string | undefined {\n try {\n return readFileSync(settingsPath, \"utf8\");\n } catch (err) {\n if (isRecord(err) && err.code === \"ENOENT\") {\n return undefined;\n }\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to read Claude settings at \"${settingsPath}\": ${message}`);\n }\n}\n\nexport function readClaudeSettingsEnv(env: NodeJS.ProcessEnv = process.env): ClaudeSettingsEnv | undefined {\n const settingsPath = resolveClaudeSettingsPath(env);\n const raw = readClaudeSettingsFile(settingsPath);\n if (!raw) return undefined;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to parse Claude settings at \"${settingsPath}\": ${message}`);\n }\n\n if (!isRecord(parsed) || !isRecord(parsed.env)) {\n return undefined;\n }\n\n const values: ClaudeSettingsEnv = {\n ANTHROPIC_API_KEY: normalize(parsed.env.ANTHROPIC_API_KEY),\n ANTHROPIC_AUTH_TOKEN: normalize(parsed.env.ANTHROPIC_AUTH_TOKEN),\n ANTHROPIC_BASE_URL: normalize(parsed.env.ANTHROPIC_BASE_URL),\n ANTHROPIC_MODEL: normalize(parsed.env.ANTHROPIC_MODEL),\n };\n\n if (!values.ANTHROPIC_API_KEY && !values.ANTHROPIC_AUTH_TOKEN && !values.ANTHROPIC_BASE_URL && !values.ANTHROPIC_MODEL) {\n return undefined;\n }\n return values;\n}\n\nfunction tryReadClaudeSettingsEnv(env: NodeJS.ProcessEnv): ClaudeSettingsEnv | undefined {\n try {\n return readClaudeSettingsEnv(env);\n } catch {\n return undefined;\n }\n}\n\nfunction validateAnthropicBaseURL(value: string): string {\n const normalized = value.trim();\n try {\n const parsed = new URL(normalized);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new Error(\"Must use http:// or https:// protocol.\");\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Must be a valid http(s) URL.\";\n throw new Error(`Invalid ANTHROPIC_BASE_URL: \"${normalized}\". ${message}`);\n }\n return normalized;\n}\n\nexport function resolveAnthropicAuthFromEnv(env: NodeJS.ProcessEnv = process.env): AnthropicAuthConfig {\n const explicitApiKey = normalize(env.ANTHROPIC_API_KEY);\n if (explicitApiKey) return { apiKey: explicitApiKey };\n\n const explicitAuthToken = normalize(env.ANTHROPIC_AUTH_TOKEN);\n if (explicitAuthToken) return { authToken: explicitAuthToken };\n\n const fallback = readClaudeSettingsEnv(env);\n if (fallback?.ANTHROPIC_API_KEY) return { apiKey: fallback.ANTHROPIC_API_KEY };\n if (fallback?.ANTHROPIC_AUTH_TOKEN) return { authToken: fallback.ANTHROPIC_AUTH_TOKEN };\n return {};\n}\n\nexport function resolveAnthropicModelFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {\n const explicitModel = env.LLMWIKI_MODEL;\n if (explicitModel !== undefined) return explicitModel;\n return tryReadClaudeSettingsEnv(env)?.ANTHROPIC_MODEL;\n}\n\nexport function resolveAnthropicBaseURLFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {\n const explicitBaseURL = normalize(env.ANTHROPIC_BASE_URL);\n if (explicitBaseURL) return validateAnthropicBaseURL(explicitBaseURL);\n\n const fallbackBaseURL = tryReadClaudeSettingsEnv(env)?.ANTHROPIC_BASE_URL;\n if (!fallbackBaseURL) return undefined;\n return validateAnthropicBaseURL(fallbackBaseURL);\n}\n","/**\n * Single provider-credential guard shared by every entry point that\n * needs an LLM call (CLI compile/query/watch, MCP tools, the upcoming\n * `quickstart` command).\n *\n * The guard throws on failure instead of calling `process.exit(1)`,\n * which lets every caller decide how to surface the failure:\n *\n * - CLI verbs catch the throw and print the message + exit 1.\n * - MCP tools let the throw propagate as a tool error.\n * - `quickstart` catches the throw and translates it into the\n * `compile.error = { code: \"provider_unavailable\", ... }` shape\n * documented in the next-quickstart implementation plan.\n *\n * Error messages mirror the rich CLI form (with `Set it with: export X=...`\n * hints) so the user always sees actionable guidance no matter which\n * surface fired the guard.\n */\n\nimport { DEFAULT_PROVIDER } from \"./constants.js\";\nimport { resolveAnthropicAuthFromEnv } from \"./claude-settings.js\";\n\n/** Thrown when the active provider has no usable credentials. */\nexport class ProviderUnavailableError extends Error {\n readonly code = \"provider_unavailable\" as const;\n constructor(readonly provider: string, readonly missing: string[], message: string) {\n super(message);\n this.name = \"ProviderUnavailableError\";\n }\n}\n\n/** Thrown when LLMWIKI_PROVIDER names an unsupported provider. */\nexport class UnknownProviderError extends Error {\n readonly code = \"unknown_provider\" as const;\n constructor(readonly provider: string, readonly supported: string[], message: string) {\n super(message);\n this.name = \"UnknownProviderError\";\n }\n}\n\n/** Map of provider name to the env var that satisfies it. Null = no key needed. */\nconst PROVIDER_KEY_VARS: Record<string, string | null> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n \"claude-agent\": null,\n openai: \"OPENAI_API_KEY\",\n ollama: null,\n minimax: \"MINIMAX_API_KEY\",\n copilot: \"GITHUB_TOKEN\",\n};\n\n/**\n * Throw if the active LLM provider is missing credentials.\n * Anthropic accepts either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN\n * (resolved through the Claude Code settings fallback chain).\n */\nexport function ensureProviderAvailable(): void {\n const provider = process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER;\n\n if (provider === \"anthropic\") {\n const auth = resolveAnthropicAuthFromEnv();\n if (!auth.apiKey && !auth.authToken) {\n throw new ProviderUnavailableError(\n \"anthropic\",\n [\"ANTHROPIC_API_KEY\", \"ANTHROPIC_AUTH_TOKEN\"],\n `Anthropic credentials are required for the \"anthropic\" provider.\\n` +\n ` Set one of: export ANTHROPIC_API_KEY=<your-key> OR export ANTHROPIC_AUTH_TOKEN=<your-token>`,\n );\n }\n return;\n }\n\n const keyVar = PROVIDER_KEY_VARS[provider];\n if (keyVar === undefined) {\n const supported = Object.keys(PROVIDER_KEY_VARS);\n throw new UnknownProviderError(\n provider,\n supported,\n `Unknown provider \"${provider}\".\\n` + ` Supported: ${supported.join(\", \")}`,\n );\n }\n\n if (keyVar && !process.env[keyVar]) {\n throw new ProviderUnavailableError(\n provider,\n [keyVar],\n `${keyVar} environment variable is required for the \"${provider}\" provider.\\n` +\n ` Set it with: export ${keyVar}=<your-key>`,\n );\n }\n}\n","/**\n * @file src/sdk/wiki.ts\n * @description In-process SDK facade for llmwiki.\n *\n * `createWiki(options)` returns a `Wiki` object that delegates every\n * method to the SDK-safe core functions built across Tasks 1–9. All\n * methods run silently (no console output) by scoping quiet mode to the\n * async call tree via AsyncLocalStorage, so concurrent calls are fully\n * isolated — no global flag is mutated, eliminating the concurrency caveat\n * described in earlier design drafts.\n *\n * Provider-gating rules:\n * - `compile`, `search`, `query` — always guard (throw ProviderUnavailableError if no creds)\n * - `runEval({ mode: \"full\" })` — guards only when mode is \"full\"\n * - All other methods — no credential check; safe to call without an LLM provider\n *\n * Root-path validation: `createWiki` normalizes the root once via `path.resolve`.\n * A non-existent root is accepted — `ingest`/`ingestText` create `sources/` via\n * recursive `mkdir` on first write. If the path already exists but is NOT a\n * directory (e.g. a regular file was passed by mistake), construction throws\n * immediately with a clear error message.\n */\n\nimport path from \"node:path\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { withQuiet } from \"../utils/output.js\";\nimport { ingestSource, ingestTextSource } from \"../commands/ingest.js\";\nimport { compileAndReport } from \"../compiler/index.js\";\nimport { generateAnswer } from \"../commands/query.js\";\nimport { lint } from \"../linter/index.js\";\nimport { buildContextPack } from \"../context/build.js\";\nimport { exportJson } from \"../commands/export.js\";\nimport { runEval, DEFAULT_SAMPLE_SIZE } from \"../eval/index.js\";\nimport { ensureProviderAvailable } from \"../utils/provider-guard.js\";\nimport { collectStatus } from \"../status/collect.js\";\nimport { pickSearchSlugs, loadPageRecords } from \"../search/retrieval.js\";\nimport { getPage, listPages } from \"../pages/list.js\";\nimport { listSources, getSource, deleteSource } from \"../sources/store.js\";\nimport type { CreateWikiOptions, Wiki, SdkCompileOptions } from \"./types.js\";\n\n/**\n * Run `fn` with output scoped quiet via AsyncLocalStorage.\n * Concurrent calls are fully isolated — no global flag is mutated.\n * Declared async so synchronous throws inside `fn` become rejected promises.\n */\nasync function runQuiet<T>(fn: () => Promise<T>): Promise<T> {\n return withQuiet(fn);\n}\n\n/**\n * Create an in-process wiki facade bound to the given project root.\n *\n * @param options - `{ root }` — path to the wiki project directory.\n * @returns A `Wiki` facade whose methods delegate to the SDK-safe core.\n */\nexport function createWiki(options: CreateWikiOptions): Wiki {\n // Normalize once so every delegated call works from an absolute path,\n // independent of any subsequent cwd changes in the calling process.\n const root = path.resolve(options.root);\n\n // A missing root is valid — ingest/ingestText create sources/ via recursive mkdir.\n // But if the path already exists and is NOT a directory, it is always a caller mistake.\n if (existsSync(root) && !statSync(root).isDirectory()) {\n throw new Error(`createWiki: root exists but is not a directory: ${root}`);\n }\n\n return {\n ingest: ({ source }) => runQuiet(() => ingestSource(root, source)),\n\n ingestText: (input) => runQuiet(() => ingestTextSource(root, input)),\n\n compile: (opts: SdkCompileOptions = {}) =>\n runQuiet(() => {\n ensureProviderAvailable();\n return compileAndReport(root, opts);\n }),\n\n search: (question) =>\n runQuiet(async () => {\n ensureProviderAvailable();\n const slugs = await pickSearchSlugs(root, question);\n return loadPageRecords(root, slugs);\n }),\n\n query: (question, opts = {}) =>\n runQuiet(() => {\n ensureProviderAvailable();\n return generateAnswer(root, question, opts);\n }),\n\n getPage: (ref) => runQuiet(() => getPage(root, ref)),\n\n listPages: (opts) => runQuiet(() => listPages(root, opts)),\n\n listSources: (opts) => runQuiet(() => listSources(root, opts)),\n\n getSource: (id) => runQuiet(() => getSource(root, id)),\n\n deleteSource: (id) => runQuiet(() => deleteSource(root, id)),\n\n status: () => runQuiet(() => collectStatus(root)),\n\n lint: () => runQuiet(() => lint(root)),\n\n // buildContextPack uses `prompt`/`budget` field names (NOT question/tokenBudget).\n // Semantic retrieval is opportunistic: falls back to lexical when no embeddings\n // are available, so no credential guard is needed here.\n getContextPack: (opts) =>\n runQuiet(() =>\n buildContextPack({\n root,\n prompt: opts.prompt,\n ...(opts.budget !== undefined && { budget: opts.budget }),\n ...(opts.depth !== undefined && { depth: opts.depth }),\n ...(opts.topPages !== undefined && { topPages: opts.topPages }),\n ...(opts.topChunks !== undefined && { topChunks: opts.topChunks }),\n }),\n ),\n\n exportJson: (opts = {}) => runQuiet(() => exportJson(root, opts)),\n\n // \"full\" mode calls the LLM judge; \"fast\" mode is credential-free.\n runEval: ({ mode, record = false }) =>\n runQuiet(() => {\n if (mode === \"full\") ensureProviderAvailable();\n return runEval(root, mode, DEFAULT_SAMPLE_SIZE, record);\n }),\n };\n}\n","/**\n * ANSI colored terminal output helpers.\n * Provides consistent styling for compilation progress, status messages,\n * and streaming token display.\n *\n * Quiet-mode is scoped per async call tree via AsyncLocalStorage so that\n * concurrent SDK calls (e.g. `Promise.all([wiki.lint(), wiki.search()])`)\n * never corrupt each other's quiet state. The process-wide `quietMode` flag\n * is preserved for the `quickstart --json` code path that calls `setQuiet`\n * directly; `isQuiet()` checks the scoped value first and falls back to the\n * global flag.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nconst RESET = \"\\x1b[0m\";\nconst BOLD = \"\\x1b[1m\";\nconst DIM = \"\\x1b[2m\";\nconst GREEN = \"\\x1b[32m\";\nconst YELLOW = \"\\x1b[33m\";\nconst BLUE = \"\\x1b[34m\";\nconst MAGENTA = \"\\x1b[35m\";\nconst CYAN = \"\\x1b[36m\";\nconst RED = \"\\x1b[31m\";\n\nexport function bold(text: string): string {\n return `${BOLD}${text}${RESET}`;\n}\n\nexport function dim(text: string): string {\n return `${DIM}${text}${RESET}`;\n}\n\nexport function success(text: string): string {\n return `${GREEN}${text}${RESET}`;\n}\n\nexport function warn(text: string): string {\n return `${YELLOW}${text}${RESET}`;\n}\n\nexport function info(text: string): string {\n return `${BLUE}${text}${RESET}`;\n}\n\nexport function error(text: string): string {\n return `${RED}${text}${RESET}`;\n}\n\nexport function source(text: string): string {\n return `${CYAN}${text}${RESET}`;\n}\n\n/**\n * Process-wide quiet flag. Toggled by `quickstart --json` so the\n * structured envelope is the only thing on stdout — every status/header\n * call short-circuits while the flag is set.\n *\n * Default is false, preserving byte-for-byte behaviour for every other\n * command. SDK callers should use `withQuiet` instead of mutating this\n * flag directly, to avoid concurrent call corruption.\n */\nlet quietMode = false;\n\n/** ALS store scopes quietness to a single async call tree. */\nconst quietScope = new AsyncLocalStorage<boolean>();\n\n/**\n * Run `fn` with quiet mode scoped to the current async call tree.\n * Concurrent calls are fully isolated — no global flag is mutated.\n * Returns whatever `fn` returns (preserves Promise for async functions).\n */\nexport function withQuiet<T>(fn: () => T): T {\n return quietScope.run(true, fn);\n}\n\n/**\n * Returns true if the current async context is quiet (via ALS scope)\n * or the process-wide quiet flag is set. The scoped value takes priority.\n */\nexport function isQuiet(): boolean {\n return quietScope.getStore() ?? quietMode;\n}\n\n/** Toggle the process-wide quiet flag. */\nexport function setQuiet(quiet: boolean): void {\n quietMode = quiet;\n}\n\n/** Print a status line with an icon. No-op while quiet mode is enabled. */\nexport function status(icon: string, message: string): void {\n if (isQuiet()) return;\n console.log(`${icon} ${message}`);\n}\n\n/** Print a section header. No-op while quiet mode is enabled. */\nexport function header(title: string): void {\n if (isQuiet()) return;\n console.log(`\\n${BOLD}${title}${RESET}`);\n console.log(dim(\"─\".repeat(Math.min(title.length + 4, 60))));\n}\n\n/**\n * Quiet-aware warning line (stderr-style notices). No-op while quiet.\n * Writes to stderr via `console.warn` — distinct from `status()`/`header()`,\n * which write progress to stdout via `console.log`.\n */\nexport function note(message: string): void {\n if (isQuiet()) return;\n console.warn(message);\n}\n\n/** Read the current process-wide quiet flag. */\nexport function getQuiet(): boolean {\n return quietMode;\n}\n","/**\n * Commander action for `llmwiki ingest <source>`.\n *\n * Detects the source type (URL, image, PDF, transcript, or generic file),\n * delegates to the appropriate ingestion module, and saves the result as a\n * markdown file with YAML frontmatter in the sources/ directory.\n *\n * Source type is persisted in frontmatter under the `sourceType` key for\n * downstream tooling and human readers.\n */\n\nimport path from \"path\";\nimport { readFile } from \"fs/promises\";\nimport { createHash } from \"node:crypto\";\nimport { buildFrontmatter } from \"../utils/markdown.js\";\nimport { saveSource } from \"../utils/source-writer.js\";\nimport { appendLog } from \"../utils/activity-log.js\";\nimport { MAX_SOURCE_CHARS, MIN_SOURCE_CHARS, SOURCES_DIR, IMAGE_EXTENSIONS, TRANSCRIPT_EXTENSIONS } from \"../utils/constants.js\";\nimport * as output from \"../utils/output.js\";\nimport ingestWeb from \"../ingest/web.js\";\nimport ingestFile from \"../ingest/file.js\";\nimport ingestPdf from \"../ingest/pdf.js\";\nimport ingestImage from \"../ingest/image.js\";\nimport ingestTranscript, { isYoutubeUrl } from \"../ingest/transcript.js\";\nimport type { IngestResult, SourceType } from \"../utils/types.js\";\n\n/** Check whether a source string looks like a URL. */\nfunction isUrl(source: string): boolean {\n return source.startsWith(\"http://\") || source.startsWith(\"https://\");\n}\n\n/** Number of bytes to peek at when sniffing .txt content for transcript signals. */\nconst TXT_SNIFF_BYTES = 2048;\n\n/**\n * Regex for a speaker-tag line: captures the speaker name before the colon.\n * Allows names up to ~40 chars with letters, spaces, dots, apostrophes, hyphens.\n * The `gm` flags let us find ALL occurrences in the sample.\n */\nconst SPEAKER_TAG_PATTERN = /^([A-Z][a-zA-Z .'-]{0,40}):\\s/gm;\n\n/**\n * Regex for a bare timestamp at the start of a line (allowing leading\n * whitespace): \"H:MM\", \"HH:MM\", or \"HH:MM:SS\". Anchored to line starts so\n * incidental times in prose (e.g. \"the meeting at 3:00 was productive\")\n * don't trip the transcript heuristic.\n */\nconst TIMESTAMP_PATTERN = /^\\s*\\d{1,2}:\\d{2}(:\\d{2})?/;\n\n/** Minimum number of timestamp-like matches to treat a file as a transcript. */\nconst MIN_TIMESTAMP_MATCHES = 3;\n\n/**\n * Minimum number of times a single speaker name must appear to signal dialogue\n * (rules out one-off section headers like \"Summary:\" that appear only once).\n */\nconst MIN_SPEAKER_REPEAT_COUNT = 2;\n\n/**\n * Minimum number of distinct speaker names required alongside the repeat\n * condition (rules out single-speaker monologues).\n */\nconst MIN_DISTINCT_SPEAKERS = 2;\n\n/**\n * Count how many times each speaker name appears in the collected tag matches.\n * Returns a Map from name → occurrence count.\n */\nfunction countSpeakerOccurrences(sample: string): Map<string, number> {\n const counts = new Map<string, number>();\n // Reset lastIndex since SPEAKER_TAG_PATTERN has the `g` flag.\n SPEAKER_TAG_PATTERN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = SPEAKER_TAG_PATTERN.exec(sample)) !== null) {\n const name = match[1].trim();\n counts.set(name, (counts.get(name) ?? 0) + 1);\n }\n return counts;\n}\n\n/**\n * Decide whether speaker-tag occurrences in a sample look like dialogue.\n *\n * A file passes when both of the following are true:\n * - At least {@link MIN_DISTINCT_SPEAKERS} distinct speaker names appear.\n * - At least one name appears {@link MIN_SPEAKER_REPEAT_COUNT}+ times,\n * indicating back-and-forth turns rather than a list of section headers\n * (e.g. \"Summary: …\", \"Details: …\") where every label is unique.\n */\nfunction hasSpeakerDialoguePattern(sample: string): boolean {\n const counts = countSpeakerOccurrences(sample);\n\n const distinctSpeakers = counts.size;\n const hasEnoughSpeakers = distinctSpeakers >= MIN_DISTINCT_SPEAKERS;\n\n const hasRepeatedSpeaker = [...counts.values()].some(\n (n) => n >= MIN_SPEAKER_REPEAT_COUNT,\n );\n\n return hasEnoughSpeakers && hasRepeatedSpeaker;\n}\n\n/**\n * Peek at the first {@link TXT_SNIFF_BYTES} of a plain-text file and decide\n * whether it looks like a conversation transcript.\n *\n * Heuristic: at least one of the following must be true in the sampled content:\n *\n * 1. **Speaker-tag dialogue pattern** — lines of the form \"Name: …\" where:\n * - At least {@link MIN_DISTINCT_SPEAKERS} distinct names appear, AND\n * - At least one name appears {@link MIN_SPEAKER_REPEAT_COUNT}+ times.\n * This rejects lone section headers (\"Summary: …\") and lists of unique\n * labels (\"Summary:\", \"Details:\", \"Notes:\") that have no repetition, while\n * accepting real back-and-forth dialogue (\"Alice: …\\nBob: …\\nAlice: …\").\n *\n * 2. **Timestamp density** — three or more bare timestamp patterns (e.g.\n * \"01:23\" / \"1:23:45\"), the signature of time-coded scripts or subtitles.\n *\n * When neither signal fires the caller routes the file as a generic text file.\n *\n * @param filePath - Absolute or relative path to the .txt file.\n * @returns `true` when transcript signals are detected, `false` otherwise.\n */\nasync function looksLikeTxtTranscript(filePath: string): Promise<boolean> {\n const raw = await readFile(filePath, \"utf-8\");\n const sample = raw.slice(0, TXT_SNIFF_BYTES);\n\n if (hasSpeakerDialoguePattern(sample)) return true;\n\n const timestampMatches = sample.match(new RegExp(TIMESTAMP_PATTERN.source, \"gm\"));\n return (timestampMatches?.length ?? 0) >= MIN_TIMESTAMP_MATCHES;\n}\n\n/** Truncate result including whether truncation occurred and original length. */\ninterface TruncateResult {\n content: string;\n truncated: boolean;\n originalChars: number;\n}\n\n/** Truncate content if it exceeds the character limit, logging a warning. */\nexport function enforceCharLimit(content: string): TruncateResult {\n if (content.length <= MAX_SOURCE_CHARS) {\n return { content, truncated: false, originalChars: content.length };\n }\n\n output.status(\n \"!\",\n output.warn(\n `Content truncated from ${content.length.toLocaleString()} to ${MAX_SOURCE_CHARS.toLocaleString()} characters.`\n )\n );\n return {\n content: content.slice(0, MAX_SOURCE_CHARS),\n truncated: true,\n originalChars: content.length,\n };\n}\n\n/** Reject empty content and warn when content is trivially short. */\nfunction enforceMinContent(content: string): void {\n const length = content.trim().length;\n\n if (length === 0) {\n throw new Error(\n \"No readable content could be extracted from the source.\"\n );\n }\n\n if (length < MIN_SOURCE_CHARS) {\n output.status(\n \"!\",\n output.warn(\n `Content seems very short (${length} chars, minimum recommended is ${MIN_SOURCE_CHARS}).`\n )\n );\n }\n}\n\n/**\n * Determine the source type for a given source string.\n *\n * For `.txt` files, content-sniffing is used instead of a pure extension check.\n * The file's first {@link TXT_SNIFF_BYTES} bytes are inspected for transcript\n * signals (speaker-tag lines or repeated timestamps). Only when both heuristics\n * fail is the file routed to the generic `file` adapter. `.vtt` and `.srt` are\n * always treated as transcripts regardless of content.\n *\n * @param source - A URL, local file path, or image path.\n * @returns The detected SourceType.\n */\nexport async function detectSourceType(source: string): Promise<SourceType> {\n if (!isUrl(source)) {\n const ext = path.extname(source).toLowerCase();\n if (ext === \".pdf\") return \"pdf\";\n if (IMAGE_EXTENSIONS.has(ext)) return \"image\";\n if (TRANSCRIPT_EXTENSIONS.has(ext)) return \"transcript\";\n if (ext === \".txt\") {\n const isTranscript = await looksLikeTxtTranscript(source);\n return isTranscript ? \"transcript\" : \"file\";\n }\n return \"file\";\n }\n\n if (isYoutubeUrl(source)) return \"transcript\";\n return \"web\";\n}\n\n/** Build the full markdown document with frontmatter. */\nexport function buildDocument(\n title: string,\n source: string,\n result: TruncateResult,\n sourceType?: SourceType,\n): string {\n const meta: Record<string, unknown> = {\n title,\n source,\n ingestedAt: new Date().toISOString(),\n };\n if (sourceType !== undefined) {\n meta.sourceType = sourceType;\n }\n if (result.truncated) {\n meta.truncated = true;\n meta.originalChars = result.originalChars;\n }\n const frontmatter = buildFrontmatter(meta);\n\n return `${frontmatter}\\n\\n${result.content}\\n`;\n}\n\n/** Fetch content from the appropriate ingestion module based on source type. */\nasync function fetchContent(\n source: string,\n sourceType: SourceType,\n): Promise<{ title: string; content: string }> {\n switch (sourceType) {\n case \"web\":\n return ingestWeb(source);\n case \"pdf\":\n return ingestPdf(source);\n case \"image\":\n return ingestImage(source);\n case \"transcript\":\n return ingestTranscript(source);\n case \"file\":\n return ingestFile(source);\n }\n}\n\n/**\n * Append a root-bound ingest entry to the activity journal (`log.md`).\n * Shared by `ingestSource` and `ingestTextSource` so both ingest paths journal\n * identically: under the project `root` (not cwd) with a root-relative `Saved`\n * path, keeping the entry portable regardless of the caller's working directory.\n */\nasync function journalIngest(\n root: string,\n title: string,\n source: string,\n savedPath: string,\n charCount: number,\n): Promise<void> {\n await appendLog(root, \"ingest\", title, {\n details: [\n `Source: ${source}`,\n `Saved: ${path.join(SOURCES_DIR, path.basename(savedPath))}`,\n `Chars: ${charCount.toLocaleString()}`,\n ],\n });\n}\n\n/**\n * Programmatic ingest entry point. Identical fetch + write logic to the CLI\n * command but returns a structured IngestResult instead of writing to stdout.\n * Used by the MCP server's ingest_source tool.\n *\n * @param source - A URL (http/https), YouTube URL, local file, PDF, or image path.\n * @returns Saved filename, character count, truncation flag, source URI, and detected source type.\n */\nexport async function ingestSource(root: string, source: string): Promise<IngestResult> {\n const sourceType = await detectSourceType(source);\n output.status(\"*\", output.info(`Ingesting [${sourceType}]: ${source}`));\n\n const { title, content } = await fetchContent(source, sourceType);\n\n const result = enforceCharLimit(content);\n enforceMinContent(result.content);\n const document = buildDocument(title, source, result, sourceType);\n const { path: savedPath, writeStatus } = await saveSource(root, title, document, source);\n\n // Journal only real writes — a no-op re-ingest must not append a log line.\n if (writeStatus !== \"unchanged\") {\n await journalIngest(root, title, source, savedPath, result.content.length);\n }\n\n return {\n filename: path.basename(savedPath),\n charCount: result.content.length,\n truncated: result.truncated,\n source,\n sourceType,\n writeStatus,\n };\n}\n\n/** Input shape for raw-text ingestion. */\nexport interface IngestTextInput { title: string; text: string; source?: string }\n\n/**\n * Ingest raw text directly into the wiki sources directory.\n *\n * When `source` is omitted a deterministic synthetic identity\n * `manual:<sha256(title,text)>` is derived so that identical content is\n * idempotent and differing content coexists (mirrors saveSource collision rules).\n * The hash is fed a length-prefixed title so the title/text boundary is\n * unambiguous — a raw separator (newline) or bare concatenation would let a\n * boundary-shifted pair collide (e.g. title=\"a\",text=\"b\" vs title=\"ab\",text=\"\").\n *\n * @param root - Absolute path to the wiki root directory.\n * @param input - Title, raw text body, and optional explicit source identity.\n * @returns Structured ingest result with filename, char count, and source URI.\n */\nexport async function ingestTextSource(root: string, input: IngestTextInput): Promise<IngestResult> {\n const digest = createHash(\"sha256\")\n .update(`${input.title.length}\\n`)\n .update(input.title)\n .update(input.text)\n .digest(\"hex\");\n const source = input.source ?? `manual:${digest}`;\n const result = enforceCharLimit(input.text);\n enforceMinContent(result.content);\n const document = buildDocument(input.title, source, result, \"file\");\n const { path: savedPath, writeStatus } = await saveSource(root, input.title, document, source);\n\n // Journal only real writes — a no-op re-ingest must not append a log line.\n if (writeStatus !== \"unchanged\") {\n await journalIngest(root, input.title, source, savedPath, result.content.length);\n }\n\n return {\n filename: path.basename(savedPath),\n charCount: result.content.length,\n truncated: result.truncated,\n source,\n sourceType: \"file\",\n writeStatus,\n };\n}\n\n/**\n * Ingest a source and save it to the sources/ directory.\n * @param source - A URL (http/https), YouTube URL, local file, PDF, or image path.\n */\nexport default async function ingest(source: string): Promise<void> {\n const cwd = process.cwd();\n const result = await ingestSource(cwd, source);\n const savedPath = path.join(cwd, SOURCES_DIR, result.filename);\n\n output.status(\n \"+\",\n output.success(`Saved ${output.bold(result.filename)} → ${output.source(savedPath)}`)\n );\n output.status(\"→\", output.dim(\"Next: llmwiki compile\"));\n}\n","/**\n * Markdown parsing and manipulation helpers.\n * Handles YAML frontmatter extraction, slugification, and atomic file writes\n * for wiki pages.\n */\n\nimport { writeFile, rename, readFile, mkdir } from \"fs/promises\";\nimport path from \"path\";\nimport yaml from \"js-yaml\";\nimport type {\n ClaimCitation,\n ContradictionRef,\n ProvenanceMetadata,\n ProvenanceState,\n SourceSpan,\n} from \"./types.js\";\n\n/** Regex matching `^[...]` citation markers (paragraph or claim-level). */\nconst CITATION_MARKER_PATTERN = /\\^\\[([^\\]]+)\\]/g;\n\n/** Regex matching the optional `:start-end` or `#Lstart-Lend` span suffix on a citation entry. */\nconst SPAN_SUFFIX_PATTERN = /^(?<file>[^:#]+)(?:(?::(?<colonStart>\\d+)(?:[,-]\\s*(?<colonEnd>\\d+))?)|(?:#L(?<hashStart>\\d+)(?:-L(?<hashEnd>\\d+))?))?$/;\n\n/**\n * Regex matching a colon-form entry with two or more comma-separated line numbers,\n * e.g. `source.md:1, 12` or `source.md:3,7,42`. Captured `lines` is the raw\n * digit-and-comma string; each token expands into its own single-line SourceSpan.\n */\nconst COLON_MULTILINE_PATTERN = /^(?<file>[^:#]+):(?<lines>\\d+(?:,\\s*\\d+)+)$/;\n\n/** The minimum valid line number in a source span (lines are 1-indexed). */\nconst MIN_LINE_NUMBER = 1;\n\n/** The set of valid provenance state strings, used to reject unknown values. */\nconst VALID_PROVENANCE_STATES: ReadonlySet<ProvenanceState> = new Set([\n \"extracted\",\n \"merged\",\n \"inferred\",\n \"ambiguous\",\n]);\n\n/**\n * Convert a human-readable concept title to a filename slug.\n *\n * Unicode-aware: keeps letters and numbers from any script (Latin, CJK,\n * Cyrillic, Greek, Arabic, etc.). Strips punctuation, emoji, and other\n * symbols. The previous implementation used `\\w` without the `u` flag,\n * which only matches `[A-Za-z0-9_]` — that silently dropped CJK titles\n * to the empty string and caused the bug fixed in #35.\n *\n * Returns an empty string when the title contains no letters or numbers\n * at all (callers that write files should detect this and fail loudly\n * instead of writing a dotfile).\n */\nexport function slugify(title: string): string {\n return title\n .toLowerCase()\n .replace(/['']/g, \"\")\n .replace(/[^\\p{L}\\p{N}\\s-]/gu, \"\")\n .replace(/\\s+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\n\nconst PROSE_LEAD_RE = /^\\p{L}/u;\n\n/**\n * Split a markdown body into prose paragraphs for eval metrics.\n * Headings, code blocks, list items, and blank lines are excluded\n * so that only human-readable claim text is counted. Used by\n * citation-coverage, citation-support, source-utilization, and\n * citation-depth so they agree on what counts as prose.\n */\nexport function splitProseParagraphs(body: string): string[] {\n return body.split(/\\n\\s*\\n/).filter((p) => PROSE_LEAD_RE.test(p.trim()));\n}\n\n\n/** Build YAML frontmatter string from key-value pairs. */\nexport function buildFrontmatter(fields: Record<string, unknown>): string {\n const dumped = yaml.dump(fields, { lineWidth: -1, quotingType: '\"' }).trimEnd();\n return `---\\n${dumped}\\n---`;\n}\n\n/** Parse YAML frontmatter from a markdown string. Returns { meta, body }. */\nexport function parseFrontmatter(content: string): {\n meta: Record<string, unknown>;\n body: string;\n} {\n const { meta, body } = parseFrontmatterStatus(content);\n return { meta, body };\n}\n\n/**\n * Like `parseFrontmatter` but also reports whether a frontmatter block was\n * present and whether the YAML inside it parsed cleanly. Callers that need\n * to distinguish \"no frontmatter block\" from \"malformed YAML\" (e.g. the\n * viewer collector, which surfaces these as different warnings) use this\n * variant; the plain `parseFrontmatter` stays a thin wrapper so existing\n * callers are unaffected.\n */\nexport function parseFrontmatterStatus(content: string): {\n meta: Record<string, unknown>;\n body: string;\n hasFrontmatterBlock: boolean;\n malformedFrontmatter: boolean;\n} {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);\n if (!match) {\n return { meta: {}, body: content, hasFrontmatterBlock: false, malformedFrontmatter: false };\n }\n\n let meta: Record<string, unknown> = {};\n let malformedFrontmatter = false;\n try {\n const parsed = yaml.load(match[1]);\n if (parsed && typeof parsed === \"object\") {\n meta = parsed as Record<string, unknown>;\n } else if (parsed !== null && parsed !== undefined) {\n // YAML parsed to a scalar/array — frontmatter must be a mapping.\n malformedFrontmatter = true;\n }\n } catch {\n malformedFrontmatter = true;\n }\n return { meta, body: match[2], hasFrontmatterBlock: true, malformedFrontmatter };\n}\n\n/** Atomically write a file (write to .tmp, then rename). */\nexport async function atomicWrite(filePath: string, content: string): Promise<void> {\n await mkdir(path.dirname(filePath), { recursive: true });\n const tmpPath = filePath + \".tmp\";\n await writeFile(tmpPath, content, \"utf-8\");\n await rename(tmpPath, filePath);\n}\n\n/**\n * Extract all source filenames from ^[filename.md] citation markers in a page body.\n * Handles paragraph form (`^[source.md]`), multi-source (`^[a.md, b.md]`), and the\n * claim-level extension that pins a line range (`^[source.md:42-58]` or\n * `^[source.md#L42-L58]`). Only the filename component is returned — span data is\n * discarded so existing callers continue to receive a flat filename list.\n * @param body - The markdown body text to parse.\n * @returns Array of unique source filenames.\n */\nexport function extractCitations(body: string): string[] {\n const filenames = new Set<string>();\n for (const citation of extractClaimCitations(body)) {\n for (const span of citation.spans) {\n if (span.file.length > 0) filenames.add(span.file);\n }\n }\n return [...filenames];\n}\n\n/**\n * Extract claim-level citations from a markdown body. Each `^[...]` marker\n * becomes one `ClaimCitation`; comma-separated entries inside a single marker\n * become multiple spans on that citation. Entries that fail to parse against\n * the span grammar are returned as bare-file spans so callers can still tell\n * the marker was present (the linter inspects `raw` to flag malformed forms).\n * @param body - The markdown body text to parse.\n * @returns Array of ClaimCitation objects in document order.\n */\nexport function extractClaimCitations(body: string): ClaimCitation[] {\n const citations: ClaimCitation[] = [];\n let match: RegExpExecArray | null;\n CITATION_MARKER_PATTERN.lastIndex = 0;\n while ((match = CITATION_MARKER_PATTERN.exec(body)) !== null) {\n const raw = match[1];\n const spans = parseCitationEntries(raw);\n if (spans.length > 0) citations.push({ raw, spans });\n }\n return citations;\n}\n\n/**\n * Split a raw citation marker interior (the content between `^[` and `]`) into\n * individual source-entry strings, without separating comma-separated line\n * numbers like the `12` in `source.md:1, 12`.\n *\n * The rule: split on every comma EXCEPT those followed by a purely-digit token\n * (which must be a line-number continuation). This correctly handles\n * digit-leading filenames such as `2024-notes.md`, `99problems.md`, and `1.md`.\n */\nexport function splitCitationMarker(inner: string): string[] {\n return inner.split(/,(?!\\s*\\d+\\s*(?:,|$))/);\n}\n\n/**\n * Parse the inside of `^[...]` into one or more SourceSpan entries.\n * Delegates splitting to {@link splitCitationMarker} so `source.md:1, 12`\n * stays as one entry while digit-leading filenames like `2024-notes.md` are\n * correctly recognised as separate entries.\n */\nfunction parseCitationEntries(inner: string): SourceSpan[] {\n const spans: SourceSpan[] = [];\n for (const part of splitCitationMarker(inner)) {\n const trimmed = part.trim();\n if (trimmed.length === 0) continue;\n spans.push(...parseSpanEntries(trimmed));\n }\n return spans;\n}\n\n/**\n * Dispatch a single trimmed citation entry to the right span parser.\n * Comma-separated line numbers (e.g. `file.md:1, 12`) expand into one\n * SourceSpan per line; everything else delegates to parseSpanEntry.\n */\nfunction parseSpanEntries(entry: string): SourceSpan[] {\n const multi = COLON_MULTILINE_PATTERN.exec(entry);\n if (multi?.groups) return parseCommaLines(multi.groups.file, multi.groups.lines);\n const single = parseSpanEntry(entry);\n return single !== undefined ? [single] : [];\n}\n\n/** Expand a comma-separated line-number string into individual single-line spans. */\nfunction parseCommaLines(file: string, linesStr: string): SourceSpan[] {\n const spans: SourceSpan[] = [];\n for (const token of linesStr.split(/,\\s*/)) {\n const lineNum = Number(token);\n if (isValidLineRange(lineNum, lineNum)) {\n spans.push({ file, lines: { start: lineNum, end: lineNum } });\n }\n }\n return spans;\n}\n\n/**\n * Parse a single citation entry (`file.md` / `file.md:1-3` / `file.md#L1-L3`).\n * Returns undefined when the parsed line range is semantically invalid (line\n * numbers must be >= 1 and end must be >= start).\n */\nfunction parseSpanEntry(entry: string): SourceSpan | undefined {\n const match = SPAN_SUFFIX_PATTERN.exec(entry);\n if (!match || !match.groups) {\n return { file: entry };\n }\n const { file, colonStart, colonEnd, hashStart, hashEnd } = match.groups;\n const start = colonStart ?? hashStart;\n const end = colonEnd ?? hashEnd;\n if (start === undefined) return { file };\n const startLine = Number(start);\n const endLine = end === undefined ? startLine : Number(end);\n if (!isValidLineRange(startLine, endLine)) return undefined;\n return { file, lines: { start: startLine, end: endLine } };\n}\n\n/** Returns true when both lines are >= 1 and end is not before start. */\nfunction isValidLineRange(start: number, end: number): boolean {\n return start >= MIN_LINE_NUMBER && end >= start;\n}\n\n/**\n * Detect whether a citation entry is malformed: bracket text that contains\n * `:` or `#` characters but does not match the documented span grammar, or\n * contains a semantically invalid line range (line 0 or end before start).\n * Used by the linter to flag broken claim-level provenance markers.\n */\nexport function isMalformedCitationEntry(entry: string): boolean {\n const trimmed = entry.trim();\n if (trimmed.length === 0) return true;\n if (!trimmed.includes(\":\") && !trimmed.includes(\"#\")) return false;\n const match = SPAN_SUFFIX_PATTERN.exec(trimmed);\n if (!match || !match.groups) return true;\n const { colonStart, colonEnd, hashStart, hashEnd } = match.groups;\n const start = colonStart ?? hashStart;\n const end = colonEnd ?? hashEnd;\n if (start === undefined) return false;\n const startLine = Number(start);\n const endLine = end === undefined ? startLine : Number(end);\n return !isValidLineRange(startLine, endLine);\n}\n\n/**\n * Inspect provenance for a page body, grouping every parsed span by source file.\n * Useful for tooling that wants to render a \"this page draws from\" panel without\n * worrying about how the markers were formatted in source. Each filename maps to\n * a deduplicated list of `{start, end}` line ranges (paragraph-only citations\n * appear as the empty array, signalling \"no specific span\").\n */\nexport function inspectProvenance(body: string): Map<string, Array<{ start: number; end: number }>> {\n const grouped = new Map<string, Array<{ start: number; end: number }>>();\n for (const citation of extractClaimCitations(body)) {\n for (const span of citation.spans) {\n const ranges = grouped.get(span.file) ?? [];\n if (span.lines && !rangeAlreadyTracked(ranges, span.lines)) {\n ranges.push(span.lines);\n }\n grouped.set(span.file, ranges);\n }\n }\n return grouped;\n}\n\n/** Has this start/end pair already been recorded for a file? */\nfunction rangeAlreadyTracked(\n ranges: Array<{ start: number; end: number }>,\n candidate: { start: number; end: number },\n): boolean {\n return ranges.some((r) => r.start === candidate.start && r.end === candidate.end);\n}\n\n/** Read a file, returning empty string if it doesn't exist. */\nexport async function safeReadFile(filePath: string): Promise<string> {\n try {\n return await readFile(filePath, \"utf-8\");\n } catch {\n return \"\";\n }\n}\n\n/** Parse a numeric confidence value, clamping to 0..1 and rejecting non-numbers. */\nfunction parseConfidence(raw: unknown): number | undefined {\n if (typeof raw !== \"number\" || !Number.isFinite(raw)) return undefined;\n if (raw < 0) return 0;\n if (raw > 1) return 1;\n return raw;\n}\n\n/** Parse a provenance state string, returning undefined for unknown values. */\nfunction parseProvenanceState(raw: unknown): ProvenanceState | undefined {\n if (typeof raw !== \"string\") return undefined;\n return VALID_PROVENANCE_STATES.has(raw as ProvenanceState)\n ? (raw as ProvenanceState)\n : undefined;\n}\n\n/** Coerce a single contradiction entry to a ContradictionRef, or null if invalid. */\nfunction coerceContradictionEntry(entry: unknown): ContradictionRef | null {\n if (typeof entry === \"string\" && entry.trim().length > 0) {\n return { slug: entry.trim() };\n }\n if (entry && typeof entry === \"object\" && \"slug\" in entry) {\n const obj = entry as { slug: unknown; reason?: unknown };\n if (typeof obj.slug !== \"string\" || obj.slug.trim().length === 0) return null;\n const ref: ContradictionRef = { slug: obj.slug.trim() };\n if (typeof obj.reason === \"string\") ref.reason = obj.reason;\n return ref;\n }\n return null;\n}\n\n/** Parse a contradictedBy array, accepting strings or objects with slug. */\nfunction parseContradictedBy(raw: unknown): ContradictionRef[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const refs = raw\n .map(coerceContradictionEntry)\n .filter((ref): ref is ContradictionRef => ref !== null);\n return refs.length > 0 ? refs : undefined;\n}\n\n/**\n * Extract provenance metadata fields from a parsed frontmatter record.\n * Defensively handles missing or malformed values so existing pages without\n * the new fields continue to parse correctly.\n *\n * Note: legacy pages may also carry an `inferredParagraphs` frontmatter\n * field from earlier compiles. It is intentionally not parsed here —\n * the lint rule derives the count from the rendered body instead, so\n * the cached field is ignored.\n * @param meta - Raw frontmatter object as returned by parseFrontmatter.\n * @returns Typed provenance metadata with only the fields that were present.\n */\nexport function parseProvenanceMetadata(\n meta: Record<string, unknown>,\n): ProvenanceMetadata {\n return {\n confidence: parseConfidence(meta.confidence),\n provenanceState: parseProvenanceState(meta.provenanceState),\n contradictedBy: parseContradictedBy(meta.contradictedBy),\n };\n}\n\n/**\n * Validate that a wiki page has non-empty content and valid frontmatter.\n * Returns true if the page is valid.\n */\nexport function validateWikiPage(content: string): boolean {\n if (!content || content.trim().length === 0) return false;\n\n const { meta, body } = parseFrontmatter(content);\n if (!meta.title) return false;\n if (body.trim().length === 0) return false;\n\n return true;\n}\n","/**\n * Shared writer for the `sources/` directory.\n *\n * Centralises the slug → filename resolution every ingest path needs, so\n * `ingest`, `ingest-session`, and any future ingester get the same\n * protections:\n *\n * - Empty-slug guard (#35): titles that strip to \"\" (e.g. pure emoji)\n * fail with an actionable error instead of writing `sources/.md`.\n * - Stable basename-collision suffix (#36): two distinct sources that\n * slugify to the same name coexist as `name.md` and\n * `name-<8-hex-of-source>.md`; re-ingesting the same source still\n * overwrites in place because the existing file's frontmatter\n * `source` field is consulted before suffixing.\n */\n\nimport { mkdir, readdir, readFile, writeFile, lstat } from \"fs/promises\";\nimport path from \"path\";\nimport { createHash } from \"crypto\";\nimport { parseFrontmatter, slugify } from \"./markdown.js\";\nimport { safeRealpath, confinedRegularFile } from \"./path-confine.js\";\nimport { SOURCES_DIR } from \"./constants.js\";\nimport { PathSafetyError } from \"../viewer/path-safety.js\";\nimport type { WriteStatus } from \"./types.js\";\n\n/** Length of the hex hash suffix appended to disambiguate basename collisions. */\nconst COLLISION_HASH_LEN = 8;\n\n/**\n * Compute a short, stable hex hash of a source identifier. Stability\n * matters — re-ingesting the same source must always produce the same\n * hash so existing files are overwritten cleanly rather than\n * accumulating duplicates.\n */\nfunction shortHashOfSource(source: string): string {\n return createHash(\"sha256\").update(source).digest(\"hex\").slice(0, COLLISION_HASH_LEN);\n}\n\n/**\n * Resolve the destination filename for a slug + source identity when no\n * existing file owns this source (source-identity matching is handled upstream\n * by `findFileBySourceIdentity` before this is called):\n *\n * - When `${slug}.md` does not exist, return `${slug}.md`.\n * - When it exists (necessarily owned by a DIFFERENT source), return\n * `${slug}-<hash>.md` so two distinct sources that share a basename\n * coexist instead of one silently overwriting the other.\n */\nasync function resolveCollisionFreeFilename(\n sourcesDir: string,\n slug: string,\n source: string,\n): Promise<string> {\n const candidate = `${slug}.md`;\n const candidatePath = path.join(sourcesDir, candidate);\n try {\n // No-follow existence probe: lstat never reads or follows a planted symlink\n // at sources/<slug>.md (readFile here would EISDIR on a symlinked dir or leak\n // an outside file). The actual write is still guarded by wx/lstat downstream.\n await lstat(candidatePath);\n } catch (err) {\n if ((err as { code?: string }).code === \"ENOENT\") return candidate;\n throw err;\n }\n // An entry already occupies sources/<slug>.md (a different source, or a planted\n // symlink) — append a stable hash suffix instead of touching it.\n return `${slug}-${shortHashOfSource(source)}.md`;\n}\n\n/**\n * Find the existing source file whose frontmatter `source` matches `source`,\n * regardless of its title-derived filename — the source identity is the key,\n * so re-ingesting under a renamed title updates the same file instead of\n * forking. O(number of sources) per ingest — reads each existing source, so\n * bulk ingest is O(n^2); a `source`->filename manifest index is the planned\n * v1.x remedy for large source sets.\n *\n * Regular files only (via `confinedRegularFile`): any symlink entry — whether\n * escaping `sourcesDir` or an in-tree alias — is skipped, keeping the scan\n * consistent with list/get/delete and preventing arbitrary-file-reads and\n * confused-deputy write-through via `saveSource`.\n */\nasync function findFileBySourceIdentity(sourcesDir: string, source: string): Promise<string | null> {\n let names: string[];\n try {\n names = (await readdir(sourcesDir)).filter((f) => f.endsWith(\".md\")).sort();\n } catch (err) {\n if ((err as { code?: string }).code === \"ENOENT\") return null;\n throw err;\n }\n for (const name of names) {\n // Regular files only: symlinks (whether escaping or in-tree aliases) are never\n // sources, keeping the scan consistent with list/get/delete.\n const real = await confinedRegularFile(sourcesDir, name);\n if (real === null) continue;\n const { meta } = parseFrontmatter(await readFile(real, \"utf-8\"));\n if (typeof meta.source === \"string\" && meta.source === source) return name;\n }\n return null;\n}\n\n/**\n * Canonical comparison key for a source document: stable frontmatter + body,\n * excluding volatile fields (`ingestedAt`). So a re-ingest with identical\n * content but a fresh timestamp is `unchanged`, while a changed title /\n * sourceType / truncation is `updated`.\n */\nfunction stableContent(document: string): string {\n const { meta, body } = parseFrontmatter(document);\n const stable: Record<string, unknown> = {};\n for (const key of Object.keys(meta).sort()) {\n if (key === \"ingestedAt\") continue;\n stable[key] = meta[key];\n }\n return `${JSON.stringify(stable)}\\n${body}`;\n}\n\n/**\n * Write a markdown document into `sources/` under a slug derived from\n * the title, applying the empty-slug guard and basename-collision\n * disambiguation.\n *\n * The source identity (not the title-derived slug) is the key: if the same\n * source was previously saved under a different title, the existing file is\n * updated in place rather than forking a new file. Stable frontmatter fields\n * (title, sourceType, truncated, originalChars) are included in the\n * comparison so changed metadata is detected, but volatile fields (`ingestedAt`)\n * are excluded so a re-ingest with fresh timestamp is still `\"unchanged\"`.\n *\n * @param title - Human-readable title used to derive the filename for new sources.\n * @param document - Full markdown content (frontmatter + body) to write.\n * @param source - Source identity (URL, file path, etc.) used as the canonical\n * key for finding existing files and collision disambiguation.\n * @returns An object with the resolved destination `path` and a\n * `writeStatus` of `\"created\"`, `\"updated\"`, or `\"unchanged\"`.\n */\nexport async function saveSource(\n root: string,\n title: string,\n document: string,\n source: string,\n): Promise<{ path: string; writeStatus: WriteStatus }> {\n const slug = slugify(title);\n // Defense in depth — even with the Unicode-aware slugifier (#35), a\n // title made entirely of punctuation/emoji/symbols still slugifies to\n // \"\". Without this guard the file would land at sources/.md.\n if (!slug) {\n throw new Error(\n `Could not derive a filename from title \"${title}\". ` +\n `The title contains no letter or number characters. ` +\n `Rename the source file to one with at least one letter or digit.`,\n );\n }\n // Create <root>/sources first — recursive mkdir creates a missing root + sources as\n // real dirs (the SDK contract: a fresh, not-yet-existing root is accepted on first\n // ingest). mkdir never follows a trailing symlink, so it won't write through a\n // symlinked sources/.\n await mkdir(path.join(root, SOURCES_DIR), { recursive: true });\n // Canonicalize root and re-derive the TRUSTED sources path; reject a symlinked-away sources/.\n const canonicalRoot = await safeRealpath(root);\n if (canonicalRoot === null) throw new PathSafetyError(`root does not exist: ${root}`); // defensive; mkdir created it\n const sourcesDir = path.join(canonicalRoot, SOURCES_DIR);\n if (!(await lstat(sourcesDir)).isDirectory()) {\n throw new PathSafetyError(`sources/ must be a real directory, not a symlink`);\n }\n\n // The source identity (not the title-derived slug) is the key: reuse the\n // existing file for this source even if the title changed; only allocate a\n // new (slug-based) filename when this is a genuinely new source.\n const existingByIdentity = await findFileBySourceIdentity(sourcesDir, source);\n const filename = existingByIdentity ?? (await resolveCollisionFreeFilename(sourcesDir, slug, source));\n const destPath = path.join(sourcesDir, filename);\n\n if (existingByIdentity === null) {\n // New source: exclusive create — never write through a pre-existing entry\n // (e.g. a planted symlink at the slug/hash destination).\n try {\n await writeFile(destPath, document, { encoding: \"utf-8\", flag: \"wx\" });\n } catch (err) {\n if ((err as { code?: string }).code === \"EEXIST\") {\n throw new PathSafetyError(`refusing to write: ${filename} already exists in sources/`);\n }\n throw err;\n }\n return { path: destPath, writeStatus: \"created\" };\n }\n // Update: the identity scan returned an in-tree match — require a regular file\n // (reject an intra-sources symlink target; never writeFile through a symlink).\n if (!(await lstat(destPath)).isFile()) {\n throw new PathSafetyError(`refusing to write: ${filename} is not a regular file`);\n }\n const existing = await readFile(destPath, \"utf-8\");\n if (stableContent(existing) === stableContent(document)) {\n return { path: destPath, writeStatus: \"unchanged\" };\n }\n await writeFile(destPath, document, \"utf-8\");\n return { path: destPath, writeStatus: \"updated\" };\n}\n","/**\n * Path-confinement helpers shared by source/wiki readers. Used to drop any\n * entry whose `realpath` escapes its intended directory (e.g. a symlinked\n * `sources/leak.md` pointing outside the project), preventing local-file-read.\n *\n * Also exports `resolveSourcesDir` which additionally guards against the\n * `sources/` directory itself being a symlink to an outside location, and\n * `confinedRegularFile` — the single definition of \"a source file\" used by\n * the scan, list, get, and delete paths to stay in agreement.\n */\nimport { realpath, lstat } from \"fs/promises\";\nimport path from \"path\";\nimport { SOURCES_DIR } from \"./constants.js\";\n\n/** `realpath` that returns null instead of throwing on missing/broken paths. */\nexport async function safeRealpath(p: string): Promise<string | null> {\n try {\n return await realpath(p);\n } catch {\n return null;\n }\n}\n\n/** True when `child` equals `dir` or sits beneath it. */\nexport function isInsideDir(child: string, dir: string): boolean {\n if (child === dir) return true;\n const prefix = dir.endsWith(path.sep) ? dir : dir + path.sep;\n return child.startsWith(prefix);\n}\n\n/**\n * Resolve `dir`/`name` to its real path IF it is a confined REGULAR FILE — i.e.\n * a real file (not a symlink, not a directory) whose realpath stays within `dir`.\n * Returns null otherwise. This is the single definition of \"a source file\": a\n * symlink in `sources/` (whether escaping or an in-tree alias) is never a source,\n * keeping reads, the identity scan, and writes in agreement.\n */\nexport async function confinedRegularFile(dir: string, name: string): Promise<string | null> {\n const entryPath = path.join(dir, name);\n let st;\n try {\n st = await lstat(entryPath); // lstat: do NOT follow — a symlink must be rejected\n } catch {\n return null;\n }\n if (!st.isFile()) return null; // symlink / directory / other → not a source\n const real = await safeRealpath(entryPath);\n if (real === null || !isInsideDir(real, dir)) return null; // defense-in-depth\n return real;\n}\n\n/**\n * Resolve the trusted on-disk sources directory for `root`: `<realpath(root)>/sources`,\n * requiring `sources/` to be a REAL directory at that literal path. A symlinked\n * `sources/` (which would redirect every read/write outside the project) yields null.\n * Returns null when root is missing or `sources/` is absent / not a real directory.\n */\nexport async function resolveSourcesDir(root: string): Promise<string | null> {\n const canonicalRoot = await safeRealpath(root);\n if (canonicalRoot === null) return null;\n const sourcesDir = path.join(canonicalRoot, SOURCES_DIR);\n try {\n const st = await lstat(sourcesDir);\n if (!st.isDirectory()) return null; // symlink or non-dir → not trusted\n } catch {\n return null; // missing\n }\n return sourcesDir;\n}\n","/**\n * Path-safety primitives for the local web viewer.\n *\n * Three layered checks form the v1 path-confinement chain. The HTTP layer\n * (Slice 2) is responsible for calling them in order at route entry:\n *\n * 1. Decode the URL path segment exactly once with `decodeURIComponent`.\n * 2. `assertSafeSlug(decoded)` — reject separators, NUL, traversal-as-slug.\n * 3. `resolveUnderRoot(root, ...segments)` — `realpath` both sides, confirm\n * the joined target stays inside `realpath(root)`. Catches symlink\n * escapes that survive a clean slug.\n * 4. `assertViewerSubtree(root, resolved)` — named-allowlist confinement\n * to `wiki/`, `sources/`, and `.llmwiki/last-lint.json` only. Anything\n * else under root (including other `.llmwiki/*`, `.git/`, `node_modules/`)\n * is rejected even though `realpath` resolved it cleanly.\n */\n\nimport { realpath } from \"fs/promises\";\nimport path from \"path\";\nimport {\n CONCEPTS_DIR,\n QUERIES_DIR,\n SOURCES_DIR,\n LAST_LINT_FILE,\n} from \"../utils/constants.js\";\n\n/** Error thrown when any path-safety check rejects an input. */\nexport class PathSafetyError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PathSafetyError\";\n }\n}\n\n/**\n * Reject decoded slug values that would let a request escape its intended\n * file. Run AFTER `decodeURIComponent` — percent-encoded traversal becomes\n * literal `..` here and is caught. Unicode slugs (any letter or number\n * code point) are accepted; only structural metacharacters are rejected.\n */\nexport function assertSafeSlug(decodedSlug: string): void {\n if (typeof decodedSlug !== \"string\") {\n throw new PathSafetyError(\"slug must be a string\");\n }\n if (decodedSlug.length === 0) {\n throw new PathSafetyError(\"slug must not be empty\");\n }\n if (decodedSlug === \".\" || decodedSlug === \"..\") {\n throw new PathSafetyError(`slug must not be \"${decodedSlug}\"`);\n }\n if (decodedSlug.includes(\"/\") || decodedSlug.includes(\"\\\\\")) {\n throw new PathSafetyError(\"slug must not contain path separators\");\n }\n if (decodedSlug.includes(\"\\0\")) {\n throw new PathSafetyError(\"slug must not contain NUL bytes\");\n }\n if (path.sep !== \"/\" && decodedSlug.includes(path.sep)) {\n throw new PathSafetyError(`slug must not contain platform separator \"${path.sep}\"`);\n }\n}\n\n/**\n * Join `root` and `safeSegments`, resolve symlinks on both ends, and\n * confirm the target stays inside `realpath(root)`. Absolute segments are\n * rejected up-front so a single bad input cannot pivot the join away from\n * `root`. Callers MUST run `assertSafeSlug` on every untrusted segment\n * before this — `resolveUnderRoot` does not re-validate slug shape.\n */\nexport async function resolveUnderRoot(\n root: string,\n ...safeSegments: string[]\n): Promise<string> {\n for (const segment of safeSegments) {\n if (typeof segment !== \"string\" || segment.length === 0) {\n throw new PathSafetyError(\"path segment must be a non-empty string\");\n }\n if (path.isAbsolute(segment)) {\n throw new PathSafetyError(\"path segment must not be absolute\");\n }\n }\n const joined = path.join(root, ...safeSegments);\n const realRoot = await realpath(root);\n const realTarget = await realpath(joined);\n const rootWithSep = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep;\n if (realTarget !== realRoot && !realTarget.startsWith(rootWithSep)) {\n throw new PathSafetyError(\"resolved path escapes project root\");\n }\n return realTarget;\n}\n\n/**\n * Confine an already-resolved path to the viewer's read allowlist. The\n * spec says \"read-only metadata under `.llmwiki/`\" — encoded here as a\n * named allowlist (the single file `.llmwiki/last-lint.json`) rather than\n * the full subtree, so a future viewer addition cannot accidentally\n * expose arbitrary `.llmwiki/*` contents over HTTP.\n *\n * The function is async because both `root` and the allowlist entries\n * are canonicalized via `fs.realpath` before comparison — without that,\n * a project root that itself is a symlink (a common workflow: cloning\n * into `~/code/project` which is a symlink to `/Volumes/Work/project`)\n * would false-reject every legitimate file. Callers may pass the raw\n * project root from `process.cwd()` and trust this helper to do the\n * canonicalization. Allowlist entries that do not exist on disk fall\n * back to a non-canonicalized join, so an empty project (no `sources/`\n * yet) still has a sensible allowlist.\n */\nexport async function assertViewerSubtree(root: string, resolvedPath: string): Promise<void> {\n const canonicalRoot = await canonicalizeOrFallback(root);\n const allowlistDirs = await Promise.all([\n canonicalizeOrFallback(path.join(canonicalRoot, \"wiki\")),\n canonicalizeOrFallback(path.join(canonicalRoot, CONCEPTS_DIR)),\n canonicalizeOrFallback(path.join(canonicalRoot, QUERIES_DIR)),\n canonicalizeOrFallback(path.join(canonicalRoot, SOURCES_DIR)),\n ]);\n const lintCachePath = await canonicalizeOrFallback(path.join(canonicalRoot, LAST_LINT_FILE));\n\n for (const dir of allowlistDirs) {\n if (isInsideOrEqual(resolvedPath, dir)) return;\n }\n if (resolvedPath === lintCachePath) return;\n\n throw new PathSafetyError(\"path is outside the viewer-approved subtrees\");\n}\n\n/**\n * Canonicalize `candidate` via `realpath`. Falls back to a normalized\n * stripped-trailing-separator string when the entry does not exist —\n * the allowlist tolerates absent directories (an empty project may have\n * no `sources/` yet) without rejecting every later check.\n */\nasync function canonicalizeOrFallback(candidate: string): Promise<string> {\n try {\n return await realpath(candidate);\n } catch {\n return candidate.endsWith(path.sep) ? candidate.slice(0, -1) : candidate;\n }\n}\n\n/** True when `candidate` equals `parent` or sits beneath it. */\nfunction isInsideOrEqual(candidate: string, parent: string): boolean {\n if (candidate === parent) return true;\n const parentWithSep = parent.endsWith(path.sep) ? parent : parent + path.sep;\n return candidate.startsWith(parentWithSep);\n}\n","/**\n * Append-only activity log (log.md).\n *\n * Implements the chronological journal described in Karpathy's llm-wiki gist:\n * a single append-only file recording what happened and when — ingests,\n * compiles, and queries. (lint is intentionally excluded: it is a read-only\n * check and the MCP `lint_wiki` tool is documented as non-mutating.) Each entry\n * is a heading line with a fixed prefix —\n * `## [YYYY-MM-DDThh:mm:ssZ] operation | description` — optionally followed by\n * a markdown bullet body carrying detail (page wikilinks, counts).\n *\n * The heading prefix is what keeps the log parseable: because only headings\n * start with `## [`, the gist's recipe `grep \"^## \\[\" log.md | tail -5` still\n * returns the five most recent operations even though entries now have bodies.\n * The log complements wiki/index.md: the index organizes content for\n * discovery, the log tracks temporal progression.\n */\n\nimport { appendFile } from \"fs/promises\";\nimport path from \"path\";\nimport {\n LOG_FILE,\n LOG_DESCRIPTION_MAX_CHARS,\n LOG_MAX_PAGE_LINKS,\n} from \"./constants.js\";\nimport * as output from \"./output.js\";\n\n/** Unicode ellipsis appended when a description is truncated. */\nconst TRUNCATION_MARKER = \"…\";\n\n/** Optional extras for a log entry. */\nexport interface LogEntryOptions {\n /** Markdown bullet detail lines rendered under the heading (page links, counts). */\n details?: string[];\n /** Entry timestamp; rendered as ISO 8601 UTC to second precision. Defaults to now. */\n date?: Date;\n}\n\n/**\n * Collapse whitespace (including newlines) and cap length so the heading stays\n * a single, grep-friendly line regardless of what the caller passes in.\n */\nfunction sanitizeDescription(description: string): string {\n const singleLine = description.replace(/\\s+/g, \" \").trim();\n if (singleLine.length <= LOG_DESCRIPTION_MAX_CHARS) return singleLine;\n return singleLine.slice(0, LOG_DESCRIPTION_MAX_CHARS - 1) + TRUNCATION_MARKER;\n}\n\n/**\n * Render items as a comma-separated string, truncating past `cap` with a\n * \"(+N more)\" suffix. Returns an empty string for an empty list so callers can\n * skip the detail line entirely.\n *\n * @param items - Pre-rendered list items (filenames, wikilinks, …).\n * @param cap - Max items to show before truncating; defaults to the configured limit.\n */\nexport function formatList(items: string[], cap: number = LOG_MAX_PAGE_LINKS): string {\n const shown = items.slice(0, cap);\n const remaining = items.length - shown.length;\n const suffix = remaining > 0 ? `, … (+${remaining} more)` : \"\";\n return shown.join(\", \") + suffix;\n}\n\n/**\n * Render a list of page slugs as a comma-separated wikilink string, truncating\n * past {@link LOG_MAX_PAGE_LINKS}. Returns an empty string for an empty list.\n *\n * @param slugs - Page slugs to render as `[[slug]]` wikilinks.\n * @param cap - Max links to show before truncating; defaults to the configured limit.\n */\nexport function formatWikilinkList(\n slugs: string[],\n cap: number = LOG_MAX_PAGE_LINKS,\n): string {\n return formatList(slugs.map((slug) => `[[${slug}]]`), cap);\n}\n\n/**\n * Format one log entry: the parseable `## [YYYY-MM-DDThh:mm:ssZ] operation |\n * description` heading, plus a `- detail` bullet for each provided detail line.\n * Pure and date-injectable so it can be unit-tested deterministically.\n *\n * @param operation - Short operation tag, e.g. \"ingest\", \"compile\", \"query\".\n * @param description - Human-readable heading detail (title, counts, question).\n * @param date - The entry's timestamp; rendered as ISO 8601 UTC to second precision.\n * @param details - Optional bullet lines rendered under the heading.\n */\nexport function formatLogEntry(\n operation: string,\n description: string,\n date: Date,\n details: string[] = [],\n): string {\n // ISO 8601 UTC to second precision, e.g. 2026-06-05T14:58:41Z. Sorts\n // lexicographically and still matches the gist's `^## \\[` grep recipe.\n const timestamp = date.toISOString().slice(0, 19) + \"Z\";\n const heading = `## [${timestamp}] ${operation} | ${sanitizeDescription(description)}`;\n if (details.length === 0) return heading;\n const body = details.map((line) => `- ${line}`).join(\"\\n\");\n return `${heading}\\n${body}`;\n}\n\n/**\n * Append a single entry to the project's log.md, blank-line separated from the\n * previous one.\n *\n * Resilient by design: the log is a side journal, so a write failure warns but\n * never throws — recording an event must not break the operation it records.\n *\n * @param root - Absolute path to the project root directory.\n * @param operation - Short operation tag, e.g. \"ingest\", \"compile\", \"query\".\n * @param description - Human-readable heading detail for the entry.\n * @param opts - Optional bullet body and entry date.\n */\nexport async function appendLog(\n root: string,\n operation: string,\n description: string,\n opts: LogEntryOptions = {},\n): Promise<void> {\n try {\n const entry = formatLogEntry(operation, description, opts.date ?? new Date(), opts.details);\n await appendFile(path.join(root, LOG_FILE), `${entry}\\n\\n`, \"utf-8\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n output.status(\"!\", output.warn(`Skipped log.md update: ${message}`));\n }\n}\n","/**\n * Web URL ingestion module.\n * Fetches a URL, extracts readable content using Mozilla Readability,\n * and converts the result to clean markdown via Turndown.\n *\n * Throws descriptive errors on network failures or when the page\n * cannot be parsed into readable content.\n */\n\nimport { JSDOM } from \"jsdom\";\nimport { Readability } from \"@mozilla/readability\";\nimport TurndownService from \"turndown\";\n\ninterface WebIngestResult {\n title: string;\n content: string;\n}\n\n/** Fetch a URL and return its readable content as markdown. */\nasync function fetchAndParse(url: string): Promise<Response> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);\n }\n return response;\n}\n\n/** Extract readable content from raw HTML using Readability. */\nfunction extractReadableContent(html: string, url: string): { title: string; htmlContent: string } {\n const dom = new JSDOM(html, { url });\n const reader = new Readability(dom.window.document);\n const article = reader.parse();\n\n if (!article || !article.content) {\n throw new Error(`Could not extract readable content from ${url}`);\n }\n\n return {\n title: article.title || \"Untitled\",\n htmlContent: article.content,\n };\n}\n\n/** Convert HTML to clean markdown using Turndown. */\nfunction convertToMarkdown(html: string): string {\n const turndown = new TurndownService({ headingStyle: \"atx\" });\n return turndown.turndown(html);\n}\n\n/**\n * Ingest a web URL and return its content as markdown.\n * @param url - The URL to fetch and convert.\n * @returns An object with the extracted title and markdown content.\n * @throws On network failure or unparseable content.\n */\nexport default async function ingestWeb(url: string): Promise<WebIngestResult> {\n const response = await fetchAndParse(url);\n const html = await response.text();\n const { title, htmlContent } = extractReadableContent(html, url);\n const content = convertToMarkdown(htmlContent);\n\n return { title, content };\n}\n","/**\n * Local file ingestion module.\n * Reads .md and .txt files from the local filesystem and returns their\n * content as markdown. Markdown files are returned as-is; plain text files\n * are wrapped in a markdown code block. All other extensions are rejected.\n */\n\nimport { readFile } from \"fs/promises\";\nimport path from \"path\";\nimport { titleFromFilename, type IngestedSource } from \"./shared.js\";\n\n/** Plain-text file extensions handled directly by this module. */\nconst SUPPORTED_EXTENSIONS = new Set([\".md\", \".txt\"]);\n\n/** Wrap plain text content in a markdown fenced block. */\nfunction wrapPlainText(text: string): string {\n return `\\`\\`\\`\\n${text}\\n\\`\\`\\``;\n}\n\n/**\n * Ingest a local file and return its content as markdown.\n * @param filePath - Absolute or relative path to a .md or .txt file.\n * @returns An object with a title derived from the filename and the markdown content.\n * @throws On unsupported file type or read failure.\n */\nexport default async function ingestFile(filePath: string): Promise<IngestedSource> {\n const ext = path.extname(filePath).toLowerCase();\n\n if (!SUPPORTED_EXTENSIONS.has(ext)) {\n throw new Error(\n `Unsupported file type \"${ext}\". Only .md and .txt files are supported.`\n );\n }\n\n const raw = await readFile(filePath, \"utf-8\");\n const title = titleFromFilename(filePath);\n const content = ext === \".md\" ? raw : wrapPlainText(raw);\n\n return { title, content };\n}\n","/**\n * Shared helpers used by every ingest module.\n *\n * Centralizes the IngestedSource result shape and title-derivation logic so\n * each per-format ingester (file, pdf, image, transcript, web) doesn't\n * reimplement the same primitives.\n */\n\nimport path from \"path\";\n\n/** Common shape returned by every ingest module. */\nexport interface IngestedSource {\n title: string;\n content: string;\n}\n\n/**\n * Derive a human-readable title from a filename.\n *\n * Strips the extension and converts dashes/underscores to spaces so that\n * \"quarterly_report.pdf\" becomes \"quarterly report\".\n *\n * @param filePath - Path to a source file.\n * @returns Humanized title (lowercase preserved, no extension).\n */\nexport function titleFromFilename(filePath: string): string {\n const basename = path.basename(filePath, path.extname(filePath));\n return basename.replace(/[-_]+/g, \" \").trim();\n}\n","/**\n * PDF ingestion module.\n *\n * Reads a local PDF file using the pdf-parse v2 PDFParse class, extracts the\n * text content via getText() and the document metadata via getInfo(). The\n * title comes from the PDF's Info dictionary when present, falling back to\n * the filename. Pages are joined into a single markdown body.\n *\n * pdf-parse (and its transitive pdfjs-dist) is imported dynamically so the\n * cost of loading the PDF parser is only paid when a PDF is actually being\n * ingested — `node dist/cli.js --help` and every other non-PDF code path\n * stays lean.\n */\n\nimport { readFile } from \"fs/promises\";\nimport { titleFromFilename, type IngestedSource } from \"./shared.js\";\n\n/** Extract the title from PDF metadata or fall back to the filename. */\nexport function resolveTitle(filePath: string, info: unknown): string {\n if (info && typeof info === \"object\") {\n const titleField = (info as Record<string, unknown>)[\"Title\"];\n if (typeof titleField === \"string\" && titleField.trim().length > 0) {\n return titleField.trim();\n }\n }\n return titleFromFilename(filePath);\n}\n\n/**\n * Ingest a local PDF file and return its text content with the document title.\n *\n * pdf-parse is imported dynamically so this module's load cost stays minimal\n * for non-PDF code paths (the parser pulls in pdfjs-dist which is sizeable).\n *\n * @param filePath - Absolute or relative path to a .pdf file.\n * @returns An object with the document title and extracted text content.\n * @throws On read failure or unparseable PDF.\n */\nexport default async function ingestPdf(filePath: string): Promise<IngestedSource> {\n const { PDFParse } = await import(\"pdf-parse\");\n\n const buffer = await readFile(filePath);\n const parser = new PDFParse({ data: new Uint8Array(buffer) });\n\n try {\n // Sequential calls are required: pdfjs-dist's LoopbackPort.postMessage\n // uses structuredClone internally; concurrent calls cause a DataCloneError\n // when the port tries to transfer the same underlying state simultaneously.\n const textResult = await parser.getText();\n const infoResult = await parser.getInfo();\n\n const title = resolveTitle(filePath, infoResult.info);\n const content = textResult.text.trim();\n return { title, content };\n } finally {\n await parser.destroy();\n }\n}\n","/**\n * Image ingestion module using LLM vision capabilities.\n *\n * Reads a local image file, encodes it as base64, and sends it to the\n * configured LLM provider's vision endpoint for OCR-plus-description\n * extraction. Requires the active provider to support image content blocks\n * (currently: Anthropic).\n *\n * Throws a clear error when the provider does not support vision, rather\n * than falling back silently.\n */\n\nimport { readFile } from \"fs/promises\";\nimport path from \"path\";\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { buildAnthropicClientOptions } from \"../providers/anthropic.js\";\nimport { IMAGE_DESCRIBE_MAX_TOKENS } from \"../utils/constants.js\";\nimport { resolveAnthropicAuthFromEnv, resolveAnthropicBaseURLFromEnv, resolveAnthropicModelFromEnv } from \"../utils/claude-settings.js\";\nimport { PROVIDER_MODELS } from \"../utils/constants.js\";\nimport { titleFromFilename, type IngestedSource } from \"./shared.js\";\n\n/** Mime types supported by Anthropic vision. */\ntype AnthropicImageMediaType = \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\";\n\nconst EXTENSION_TO_MIME: Record<string, AnthropicImageMediaType> = {\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".png\": \"image/png\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n};\n\n/** Return the MIME type for an image file, or throw on unknown extension. */\nfunction mimeTypeForExtension(ext: string): AnthropicImageMediaType {\n const mimeType = EXTENSION_TO_MIME[ext.toLowerCase()];\n if (!mimeType) {\n throw new Error(\n `Unsupported image extension \"${ext}\". Supported: ${Object.keys(EXTENSION_TO_MIME).join(\", \")}`,\n );\n }\n return mimeType;\n}\n\n/** Build an Anthropic client from the current environment config. */\nfunction buildClient(): Anthropic {\n const baseURL = resolveAnthropicBaseURLFromEnv();\n const auth = resolveAnthropicAuthFromEnv();\n return new Anthropic(buildAnthropicClientOptions({ baseURL, ...auth }));\n}\n\n/** Send an image to Anthropic vision and return the extracted description. */\nasync function describeImageWithVision(\n client: Anthropic,\n model: string,\n imageData: string,\n mimeType: AnthropicImageMediaType,\n): Promise<string> {\n const response = await client.messages.create({\n model,\n max_tokens: IMAGE_DESCRIBE_MAX_TOKENS,\n messages: [\n {\n role: \"user\",\n content: [\n {\n type: \"image\",\n source: { type: \"base64\", media_type: mimeType, data: imageData },\n },\n {\n type: \"text\",\n text: \"Extract and transcribe all text visible in this image. Then provide a detailed description of any non-text visual content. Format your response as markdown.\",\n },\n ],\n },\n ],\n });\n\n const textBlock = response.content.find((block) => block.type === \"text\");\n return textBlock?.type === \"text\" ? textBlock.text : \"\";\n}\n\n/**\n * Ingest a local image file using LLM vision for OCR and description.\n *\n * Only Anthropic is supported for vision. The active provider must be\n * Anthropic; if not, a clear error is thrown rather than degrading silently.\n *\n * @param filePath - Absolute or relative path to an image file.\n * @returns An object with a title derived from the filename and the extracted content.\n * @throws When the provider does not support vision or on read/API failure.\n */\nexport default async function ingestImage(filePath: string): Promise<IngestedSource> {\n const providerName = process.env.LLMWIKI_PROVIDER ?? \"anthropic\";\n\n if (providerName !== \"anthropic\") {\n throw new Error(\n `Image ingest requires the Anthropic provider (vision). ` +\n `Current provider: \"${providerName}\". ` +\n `Set LLMWIKI_PROVIDER=anthropic and ANTHROPIC_API_KEY to use image ingest.`,\n );\n }\n\n const ext = path.extname(filePath).toLowerCase();\n const mimeType = mimeTypeForExtension(ext);\n const imageBuffer = await readFile(filePath);\n const imageData = imageBuffer.toString(\"base64\");\n\n const client = buildClient();\n const model = resolveAnthropicModelFromEnv() ?? PROVIDER_MODELS.anthropic;\n const content = await describeImageWithVision(client, model, imageData, mimeType);\n const title = titleFromFilename(filePath);\n\n return { title, content };\n}\n","/**\n * Anthropic LLM provider implementation.\n *\n * Wraps the @anthropic-ai/sdk to implement the LLMProvider interface.\n * Handles complete, streaming, and tool-use calls against Claude models.\n */\n\nimport Anthropic, { type ClientOptions } from \"@anthropic-ai/sdk\";\nimport type { LLMProvider, LLMMessage, LLMTool } from \"../utils/provider.js\";\nimport { voyageEmbed } from \"./voyage-embed.js\";\n\n/**\n * Builds the client options for the Anthropic SDK.\n *\n * Handles optional baseURL and filters out empty values so the SDK\n * can fall back to its internal defaults when not specified.\n */\ninterface AnthropicProviderOptions {\n apiKey?: string;\n authToken?: string;\n baseURL?: string;\n}\n\nexport function buildAnthropicClientOptions(\n options: AnthropicProviderOptions = {},\n): ClientOptions {\n const trimmedBaseURL = options.baseURL?.trim();\n const trimmedApiKey = options.apiKey?.trim();\n const trimmedAuthToken = options.authToken?.trim();\n\n const result: ClientOptions = {};\n\n if (trimmedApiKey) {\n result.apiKey = trimmedApiKey;\n }\n if (trimmedAuthToken) {\n result.authToken = trimmedAuthToken;\n }\n\n if (!trimmedBaseURL) {\n return result;\n }\n\n const normalizedBaseURL =\n trimmedBaseURL.endsWith(\"/\") && trimmedBaseURL.length > 1\n ? trimmedBaseURL.slice(0, -1)\n : trimmedBaseURL;\n\n result.baseURL = normalizedBaseURL;\n return result;\n}\n\n\n/** Anthropic-backed LLM provider using the official SDK. */\nexport class AnthropicProvider implements LLMProvider {\n private readonly client: Anthropic;\n private readonly model: string;\n\n constructor(model: string, options: AnthropicProviderOptions = {}) {\n this.model = model;\n this.client = new Anthropic(buildAnthropicClientOptions(options));\n }\n\n /** Send a single non-streaming completion request. */\n async complete(system: string, messages: LLMMessage[], maxTokens: number): Promise<string> {\n const response = await this.client.messages.create({\n model: this.model,\n max_tokens: maxTokens,\n system,\n messages,\n });\n\n const textBlock = response.content.find((block) => block.type === \"text\");\n return textBlock?.type === \"text\" ? textBlock.text : \"\";\n }\n\n /** Stream a completion, invoking onToken for each text chunk. */\n async stream(\n system: string,\n messages: LLMMessage[],\n maxTokens: number,\n onToken?: (text: string) => void,\n ): Promise<string> {\n const stream = this.client.messages.stream({\n model: this.model,\n max_tokens: maxTokens,\n system,\n messages,\n });\n\n let fullText = \"\";\n for await (const event of stream) {\n if (event.type === \"content_block_delta\" && event.delta.type === \"text_delta\") {\n fullText += event.delta.text;\n onToken?.(event.delta.text);\n }\n }\n\n return fullText;\n }\n\n /** Call Claude with tool definitions and return the parsed tool input as JSON. */\n async toolCall(\n system: string,\n messages: LLMMessage[],\n tools: LLMTool[],\n maxTokens: number,\n ): Promise<string> {\n const anthropicTools: Anthropic.Tool[] = tools.map((t) => ({\n name: t.name,\n description: t.description,\n input_schema: t.input_schema as Anthropic.Tool.InputSchema,\n }));\n\n const response = await this.client.messages.create({\n model: this.model,\n max_tokens: maxTokens,\n system,\n messages,\n tools: anthropicTools,\n tool_choice: { type: \"any\" },\n });\n\n const toolBlock = response.content.find((block) => block.type === \"tool_use\");\n if (toolBlock?.type === \"tool_use\") {\n return JSON.stringify(toolBlock.input);\n }\n\n const textBlock = response.content.find((block) => block.type === \"text\");\n return textBlock?.type === \"text\" ? textBlock.text : \"\";\n }\n\n /**\n * Produce a single embedding vector via the Voyage API.\n *\n * Anthropic does not ship a first-party embeddings endpoint, so we delegate\n * to Voyage (their recommended partner). Requires VOYAGE_API_KEY.\n */\n async embed(text: string): Promise<number[]> {\n return voyageEmbed(text);\n }\n}\n","/**\n * Voyage embeddings helper.\n *\n * Anthropic ships no first-party embeddings endpoint, so the Claude-backed\n * providers (`anthropic` and `claude-agent`) delegate embeddings to Voyage —\n * Anthropic's recommended partner. Shared here so both providers reuse the\n * exact same request logic. Requires the VOYAGE_API_KEY environment variable.\n */\n\nimport { EMBEDDING_MODELS } from \"../utils/constants.js\";\n\nconst VOYAGE_EMBEDDINGS_URL = \"https://api.voyageai.com/v1/embeddings\";\n\n/**\n * Produce a single embedding vector for the given text via the Voyage API.\n *\n * @param text - The text to embed.\n * @param model - Voyage model name; defaults to the Anthropic embedding model.\n * @returns The embedding vector.\n * @throws If VOYAGE_API_KEY is unset or the Voyage request fails.\n */\nexport async function voyageEmbed(\n text: string,\n model: string = EMBEDDING_MODELS.anthropic,\n): Promise<number[]> {\n const apiKey = process.env.VOYAGE_API_KEY?.trim();\n if (!apiKey) {\n throw new Error(\n \"VOYAGE_API_KEY is not set. Anthropic embeddings use Voyage — set VOYAGE_API_KEY to enable semantic search.\",\n );\n }\n\n const response = await fetch(VOYAGE_EMBEDDINGS_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ input: text, model }),\n });\n\n if (!response.ok) {\n const detail = await response.text();\n throw new Error(`Voyage embeddings request failed (${response.status}): ${detail}`);\n }\n\n const json = (await response.json()) as { data?: Array<{ embedding?: number[] }> };\n const vector = json.data?.[0]?.embedding;\n if (!Array.isArray(vector)) {\n throw new Error(\"Voyage embeddings response did not include a vector.\");\n }\n return vector;\n}\n","/**\n * Transcript ingestion module.\n *\n * Handles three transcript source types:\n * 1. YouTube URLs — fetched via the youtube-transcript package.\n * 2. WebVTT (.vtt) — speaker/time markers preserved in output.\n * 3. SubRip (.srt) — speaker/time markers preserved in output.\n * 4. Plain-text (.txt) with speaker tags (e.g. \"Speaker: text\").\n *\n * Speaker and timing metadata are kept in the output so downstream\n * compilation can reference them. Content is returned as markdown.\n */\n\nimport { readFile } from \"fs/promises\";\nimport path from \"path\";\nimport { titleFromFilename, type IngestedSource } from \"./shared.js\";\n// youtube-transcript exports proper ESM via its package.json `exports` map.\n// Import from the package root; the \"import\" condition resolves to\n// ./dist/esm/index.js and includes bundled .d.ts types.\nimport { YoutubeTranscript } from \"youtube-transcript\";\n\n/** Pattern that identifies a YouTube URL. */\nconst YOUTUBE_URL_PATTERN = /^https?:\\/\\/(www\\.)?(youtube\\.com\\/watch|youtu\\.be\\/)/;\n\n/** Pattern for SRT sequence number lines (numeric-only). */\nconst SRT_SEQUENCE_PATTERN = /^\\d+$/;\n\n/** Pattern for SRT/VTT timestamp lines. */\nconst TIMESTAMP_PATTERN = /\\d{2}:\\d{2}[:.]\\d{2}/;\n\n/** Number of milliseconds in one minute. */\nconst MS_PER_MINUTE = 60_000;\n\n/** Number of milliseconds in one second. */\nconst MS_PER_SECOND = 1_000;\n\n/** Check whether a source string is a YouTube URL. */\nexport function isYoutubeUrl(source: string): boolean {\n return YOUTUBE_URL_PATTERN.test(source);\n}\n\n/** Extract the YouTube video ID from a URL. */\nfunction extractVideoId(url: string): string {\n const match = url.match(/(?:v=|youtu\\.be\\/)([^&?/]+)/);\n if (!match) {\n throw new Error(`Could not extract video ID from YouTube URL: ${url}`);\n }\n return match[1];\n}\n\n/** Format a millisecond offset as a \"MM:SS\" timestamp. */\nfunction formatOffset(offsetMs: number): string {\n const minutes = Math.floor(offsetMs / MS_PER_MINUTE);\n const seconds = Math.floor((offsetMs % MS_PER_MINUTE) / MS_PER_SECOND);\n return `${String(minutes).padStart(2, \"0\")}:${String(seconds).padStart(2, \"0\")}`;\n}\n\n/** Fetch and format a YouTube transcript as markdown. */\nasync function fetchYoutubeTranscript(url: string): Promise<IngestedSource> {\n const videoId = extractVideoId(url);\n const segments = await YoutubeTranscript.fetchTranscript(videoId);\n\n if (!segments || segments.length === 0) {\n throw new Error(`No transcript available for YouTube video: ${url}`);\n }\n\n const lines = segments.map((seg) => `[${formatOffset(seg.offset)}] ${seg.text}`);\n\n return {\n title: `YouTube Transcript ${videoId}`,\n content: lines.join(\"\\n\"),\n };\n}\n\n/** Decide whether a trimmed line is a VTT/SRT cue timestamp marker. */\nfunction isCueTimestamp(trimmed: string): boolean {\n return TIMESTAMP_PATTERN.test(trimmed) && trimmed.includes(\"-->\");\n}\n\n/** Parse a VTT file, preserving speaker cues and timestamps. */\nfunction parseVtt(raw: string, filePath: string): IngestedSource {\n const lines = raw.split(\"\\n\");\n const output: string[] = [];\n let inCue = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === \"WEBVTT\" || trimmed === \"\") {\n inCue = false;\n continue;\n }\n if (isCueTimestamp(trimmed)) {\n output.push(`\\n**[${trimmed}]**`);\n inCue = true;\n continue;\n }\n if (inCue && trimmed.length > 0) {\n output.push(trimmed);\n }\n }\n\n return { title: titleFromFilename(filePath), content: output.join(\"\\n\").trim() };\n}\n\n/** Parse an SRT file, preserving speaker cues and timestamps. */\nfunction parseSrt(raw: string, filePath: string): IngestedSource {\n const lines = raw.split(\"\\n\");\n const output: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === \"\" || SRT_SEQUENCE_PATTERN.test(trimmed)) {\n continue;\n }\n if (isCueTimestamp(trimmed)) {\n output.push(`\\n**[${trimmed}]**`);\n continue;\n }\n if (trimmed.length > 0) {\n output.push(trimmed);\n }\n }\n\n return { title: titleFromFilename(filePath), content: output.join(\"\\n\").trim() };\n}\n\n/** Parse a plain-text transcript, preserving speaker tags. */\nfunction parsePlainTranscript(raw: string, filePath: string): IngestedSource {\n // Plain .txt transcripts are returned as-is; speaker lines like \"Alice: ...\"\n // are naturally readable.\n return { title: titleFromFilename(filePath), content: raw.trim() };\n}\n\n/**\n * Ingest a transcript source: a YouTube URL or a local .vtt/.srt/.txt file.\n *\n * @param source - YouTube URL or path to a transcript file.\n * @returns Title and markdown-formatted content with speaker/time markers.\n * @throws On network failure, missing transcript, or unsupported file type.\n */\nexport default async function ingestTranscript(source: string): Promise<IngestedSource> {\n if (isYoutubeUrl(source)) {\n return fetchYoutubeTranscript(source);\n }\n\n const ext = path.extname(source).toLowerCase();\n const raw = await readFile(source, \"utf-8\");\n\n if (ext === \".vtt\") return parseVtt(raw, source);\n if (ext === \".srt\") return parseSrt(raw, source);\n if (ext === \".txt\") return parsePlainTranscript(raw, source);\n\n throw new Error(\n `Unsupported transcript file type \"${ext}\". Supported: .vtt, .srt, .txt`,\n );\n}\n","/**\n * Compilation orchestrator for the llmwiki knowledge compiler.\n *\n * Coordinates the full pipeline: lock acquisition, change detection,\n * concept extraction via LLM, wiki page generation with streaming output,\n * orphan marking for deleted sources, interlink resolution, and index\n * generation. Supports incremental compilation — only new or changed\n * sources are processed through the LLM pipeline.\n */\n\nimport { readFile, readdir } from \"fs/promises\";\nimport path from \"path\";\nimport { readState, updateSourceState } from \"../utils/state.js\";\nimport {\n buildExtractionSourceStates,\n pickStatesForSources,\n} from \"./source-state.js\";\nimport {\n atomicWrite,\n buildFrontmatter,\n parseFrontmatter,\n safeReadFile,\n validateWikiPage,\n slugify,\n} from \"../utils/markdown.js\";\nimport { callClaude } from \"../utils/llm.js\";\nimport { acquireLock, releaseLock } from \"../utils/lock.js\";\nimport {\n CONCEPT_EXTRACTION_TOOL,\n buildExtractionPrompt,\n buildSeedPagePrompt,\n parseConcepts,\n} from \"./prompts.js\";\nimport { loadSchema, type SchemaConfig, type SeedPage } from \"../schema/index.js\";\nimport { detectChanges, hashFile } from \"./hasher.js\";\nimport {\n findAffectedSources,\n findFrozenSlugs,\n findLateAffectedSources,\n freezeFailedExtractions,\n persistFrozenSlugs,\n type ExtractionResult,\n} from \"./deps.js\";\nimport { markOrphaned, orphanUnownedFrozenPages } from \"./orphan.js\";\nimport { resolveLinks } from \"./resolver.js\";\nimport { generateIndex } from \"./indexgen.js\";\nimport { buildBudgetedCombinedContent, type SourceSlice } from \"./prompt-budget.js\";\nimport { addObsidianMeta, generateMOC } from \"./obsidian.js\";\nimport { addModelProvenanceMeta } from \"./provenance.js\";\nimport { updateEmbeddings } from \"../utils/embeddings.js\";\nimport { writeCandidate } from \"./candidates.js\";\nimport { appendLog, formatList, formatWikilinkList } from \"../utils/activity-log.js\";\nimport {\n checkPageBrokenCitations,\n checkPageCrossLinks,\n checkPageMalformedCitations,\n} from \"../linter/rules.js\";\nimport type { LintResult } from \"../linter/types.js\";\nimport { renderMergedPageContent } from \"./page-renderer.js\";\nimport * as output from \"../utils/output.js\";\nimport {\n COMPILE_CONCURRENCY,\n CONCEPTS_DIR,\n INDEX_FILE,\n QUERIES_DIR,\n SOURCES_DIR,\n} from \"../utils/constants.js\";\nimport pLimit from \"p-limit\";\nimport type {\n CompileOptions,\n CompileResult,\n ExtractedConcept,\n ReviewCandidate,\n SourceChange,\n SourceState,\n WikiFrontmatter,\n WikiState,\n} from \"../utils/types.js\";\n\n/** Per-source state snapshots keyed by source filename. */\ntype SourceStateMap = Record<string, SourceState>;\n\n/** Empty CompileResult used when no pipeline work runs (e.g. lock contention). */\nfunction emptyCompileResult(): CompileResult {\n return { compiled: 0, skipped: 0, deleted: 0, concepts: [], pages: [], errors: [] };\n}\n\n/**\n * Run the full compilation pipeline with lock protection.\n * Acquires .llmwiki/lock, detects changes, compiles new/changed sources,\n * marks orphaned pages, resolves interlinks, and rebuilds the index.\n * @param root - Project root directory.\n * @param options - Optional pipeline overrides (e.g. --review mode).\n */\nexport async function compile(root: string, options: CompileOptions = {}): Promise<void> {\n await compileAndReport(root, options);\n}\n\n/**\n * Run the full compilation pipeline and return a structured result.\n * Same behaviour as compile() but exposes counts, slugs, and errors so\n * non-CLI consumers (the MCP server, programmatic callers) can report\n * meaningful data without scraping terminal output.\n * @param root - Project root directory.\n * @param options - Optional pipeline overrides (e.g. --review mode).\n * @returns Structured result describing what was compiled.\n */\nexport async function compileAndReport(\n root: string,\n options: CompileOptions = {},\n): Promise<CompileResult> {\n output.header(\"llmwiki compile\");\n\n const locked = await acquireLock(root);\n if (!locked) {\n output.status(\"!\", output.error(\"Could not acquire lock. Try again later.\"));\n return {\n ...emptyCompileResult(),\n errors: [\"Could not acquire .llmwiki/lock — another compile is in progress.\"],\n };\n }\n\n try {\n return await runCompilePipeline(root, options);\n } finally {\n await releaseLock(root);\n }\n}\n\n/** Buckets of source changes used by the compile pipeline. */\ninterface ChangeBuckets {\n toCompile: SourceChange[];\n deleted: SourceChange[];\n unchanged: SourceChange[];\n}\n\n/** Sort source changes into the buckets the pipeline acts on. */\nfunction bucketChanges(changes: SourceChange[]): ChangeBuckets {\n return {\n toCompile: changes.filter((c) => c.status === \"new\" || c.status === \"changed\"),\n deleted: changes.filter((c) => c.status === \"deleted\"),\n unchanged: changes.filter((c) => c.status === \"unchanged\"),\n };\n}\n\n/** Result of phase 2: page writes plus any errors collected along the way. */\ninterface PageGenerationResult {\n pages: MergedConcept[];\n errors: string[];\n /** Candidate ids written when running in --review mode. Empty otherwise. */\n candidates: string[];\n /**\n * Slugs of seed pages written this run (overview / comparison / entity).\n * Concept pages live on `pages`; seed pages don't fit MergedConcept's\n * source-list shape, so they're tracked separately here. summarizeCompile\n * concatenates these into CompileResult.pages so downstream consumers\n * (MCP, embeddings, programmatic callers) see seed-page changes too.\n */\n seedSlugs: string[];\n}\n\n/** Phase 2: generate pages for merged concepts in parallel, capturing errors. */\nasync function generatePagesPhase(\n root: string,\n extractions: ExtractionResult[],\n frozenSlugs: Set<string>,\n schema: SchemaConfig,\n options: CompileOptions,\n): Promise<PageGenerationResult> {\n const merged = mergeExtractions(extractions, frozenSlugs);\n // Build the per-source state snapshot once so each candidate can carry the\n // exact data needed to mark its sources compiled on approval.\n const sourceStates = options.review\n ? await buildExtractionSourceStates(root, extractions)\n : {};\n const limit = pLimit(COMPILE_CONCURRENCY);\n const errors: string[] = [];\n const candidates: string[] = [];\n const pages = await Promise.all(\n merged.map((entry) => limit(async () => {\n const result = await generateMergedPage(root, entry, schema, options, sourceStates);\n if (result.error) errors.push(result.error);\n if (result.candidateId) candidates.push(result.candidateId);\n return entry;\n })),\n );\n return { pages, errors, candidates, seedSlugs: [] };\n}\n\n/** Persist source state for every extraction that produced concepts. */\nasync function persistExtractionStates(\n root: string,\n extractions: ExtractionResult[],\n): Promise<void> {\n for (const result of extractions) {\n if (result.concepts.length === 0) continue;\n await persistSourceState(root, result.sourcePath, result.sourceFile, result.concepts);\n }\n}\n\n/**\n * Snapshot page IDs already on disk before a compile run, namespaced by\n * directory (e.g. `wiki/concepts/foo`, `wiki/queries/foo`). Namespacing keeps\n * the created/updated split correct when a concept and a query share a slug.\n */\nasync function listExistingPageIds(root: string): Promise<Set<string>> {\n const ids = new Set<string>();\n for (const dir of [CONCEPTS_DIR, QUERIES_DIR]) {\n try {\n const files = await readdir(path.join(root, dir));\n for (const file of files) {\n if (file.endsWith(\".md\")) ids.add(`${dir}/${file.slice(0, -3)}`);\n }\n } catch {\n // Directory may not exist yet on a first compile — nothing to snapshot.\n }\n }\n return ids;\n}\n\n/**\n * Journal a non-review compile to log.md: the source files consumed, plus the\n * produced pages split into created (new on disk) and updated (overwritten).\n * Compile only ever writes concept-namespace pages (concept and seed pages both\n * land under wiki/concepts/), so existence is checked against that namespace.\n */\nasync function logCompile(\n root: string,\n buckets: ChangeBuckets,\n generation: PageGenerationResult,\n existingIds: Set<string>,\n): Promise<void> {\n if (buckets.toCompile.length === 0 && buckets.deleted.length === 0) return;\n const produced = [...new Set([...generation.pages.map((entry) => entry.slug), ...generation.seedSlugs])];\n const existed = (slug: string): boolean => existingIds.has(`${CONCEPTS_DIR}/${slug}`);\n const created = produced.filter((slug) => !existed(slug));\n const updated = produced.filter((slug) => existed(slug));\n const heading = `${buckets.toCompile.length} source(s) → ${produced.length} page(s)`;\n const details: string[] = [];\n if (buckets.toCompile.length > 0) {\n details.push(`Sources: ${formatList(buckets.toCompile.map((change) => change.file))}`);\n }\n if (created.length > 0) details.push(`Created: ${formatWikilinkList(created)}`);\n if (updated.length > 0) details.push(`Updated: ${formatWikilinkList(updated)}`);\n if (buckets.deleted.length > 0) details.push(`Deleted sources: ${buckets.deleted.length}`);\n await appendLog(root, \"compile\", heading, { details });\n}\n\n/** Build the structured CompileResult and emit the CLI completion banner. */\nfunction summarizeCompile(\n buckets: ChangeBuckets,\n generation: PageGenerationResult,\n extractions: ExtractionResult[],\n options: CompileOptions,\n): CompileResult {\n output.header(\"Compilation complete\");\n output.status(\"✓\", output.success(\n `${buckets.toCompile.length} compiled, ${buckets.unchanged.length} skipped, ${buckets.deleted.length} deleted`,\n ));\n if (options.review && generation.candidates.length > 0) {\n output.status(\"?\", output.info(\n `${generation.candidates.length} candidate(s) awaiting review — run \\`llmwiki review list\\``,\n ));\n } else if (buckets.toCompile.length > 0) {\n output.status(\"→\", output.dim('Next: llmwiki query \"your question here\"'));\n }\n\n const errors = [...generation.errors];\n for (const result of extractions) {\n if (result.concepts.length === 0) {\n errors.push(`No concepts extracted from ${result.sourceFile}`);\n }\n }\n\n // Concept-page slugs first, then seed-page slugs from the same run, so\n // downstream consumers see every page the compile actually produced.\n // Seed pages are deterministic schema-driven writes; before this they\n // landed on disk silently and never appeared on CompileResult.pages.\n const conceptSlugs = generation.pages.map((entry) => entry.slug);\n const baseResult: CompileResult = {\n compiled: buckets.toCompile.length,\n skipped: buckets.unchanged.length,\n deleted: buckets.deleted.length,\n concepts: generation.pages.map((entry) => entry.concept.concept),\n pages: [...conceptSlugs, ...generation.seedSlugs],\n errors,\n };\n if (options.review) {\n baseResult.candidates = generation.candidates;\n }\n return baseResult;\n}\n\n/**\n * Apply `options.changeFilter` to the full detected change set.\n * When no filter is provided, returns the original list unchanged.\n * Separating this keeps `runCompilePipeline` below the cyclomatic threshold.\n */\nfunction applyChangeFilter(\n detected: SourceChange[],\n filter: CompileOptions[\"changeFilter\"],\n): SourceChange[] {\n return filter ? detected.filter(filter) : detected;\n}\n\n/**\n * Generate seed pages unless `skipSeedPages` is set or we are in review mode.\n * Centralises both guard conditions so each call site in the pipeline is a\n * single unconditional statement instead of an inline if-block.\n */\nasync function maybeSeedPages(\n root: string,\n schema: SchemaConfig,\n generation: PageGenerationResult,\n options: CompileOptions,\n): Promise<void> {\n if (!options.review && !options.skipSeedPages) {\n await generateSeedPages(root, schema, generation);\n }\n}\n\n/** Inner pipeline, runs under lock protection. Returns structured CompileResult. */\nasync function runCompilePipeline(\n root: string,\n options: CompileOptions,\n): Promise<CompileResult> {\n const schema = await loadSchema(root);\n reportSchemaStatus(schema);\n const state = await readState(root);\n const detected = await detectChanges(root, state);\n const changes = applyChangeFilter(detected, options.changeFilter);\n augmentWithAffectedSources(changes, findAffectedSources(state, changes));\n\n const buckets = bucketChanges(changes);\n if (buckets.toCompile.length === 0 && buckets.deleted.length === 0) {\n output.status(\"✓\", output.success(\"Nothing to compile — all sources up to date.\"));\n // Seed pages are cheap deterministic writes — always run them even when\n // no source files changed, so adding a seed page to schema.json takes\n // effect on the next compile without needing a source file edit.\n if (!options.review) {\n const emptyGeneration: PageGenerationResult = {\n pages: [],\n errors: [],\n candidates: [],\n seedSlugs: [],\n };\n await maybeSeedPages(root, schema, emptyGeneration, options);\n // Rebuild index/MOC so the newly-written seed pages become discoverable,\n // and propagate any seed-page validation errors into the returned result.\n await finalizeWiki(root, emptyGeneration.pages, emptyGeneration.seedSlugs);\n return {\n ...emptyCompileResult(),\n skipped: buckets.unchanged.length,\n // Surface seed-page slugs alongside any errors so downstream\n // consumers (MCP, embeddings, programmatic callers) can see what\n // landed even on the no-source-changes early-return path.\n pages: [...emptyGeneration.seedSlugs],\n errors: emptyGeneration.errors,\n };\n }\n return { ...emptyCompileResult(), skipped: buckets.unchanged.length };\n }\n\n printChangesSummary(changes);\n // In review mode the pipeline contract is \"write candidates instead of\n // mutating wiki/\". Deletion bookkeeping (orphan marking + frozen-slug\n // persistence) writes directly into wiki/ and updates state.json, so we\n // defer it to the next non-review compile pass. Source-state persistence\n // for compiled sources is also review-deferred — those entries land at\n // approve time so unapproved candidates remain re-detectable on subsequent\n // compiles.\n if (!options.review) {\n await markDeletedAsOrphaned(root, buckets.deleted, state);\n }\n\n const frozenSlugs = findFrozenSlugs(state, changes);\n reportFrozenSlugs(frozenSlugs);\n\n const extractions = await runExtractionPhases(root, buckets.toCompile, state, changes);\n if (!options.review) {\n await freezeFailedExtractions(root, extractions, frozenSlugs);\n }\n\n // Snapshot pages on disk before generation so the journal can tell which\n // produced pages are new (created) versus overwritten (updated).\n const existingIds = await listExistingPageIds(root);\n const generation = await generatePagesPhase(root, extractions, frozenSlugs, schema, options);\n\n if (!options.review) {\n await persistExtractionStates(root, extractions);\n if (frozenSlugs.size > 0) {\n await orphanUnownedFrozenPages(root, frozenSlugs);\n }\n await persistFrozenSlugs(root, frozenSlugs, extractions);\n // Seed pages write directly into wiki/, so skip them in review mode\n // to honour the \"no wiki/ mutation\" contract of that mode.\n await maybeSeedPages(root, schema, generation, options);\n await finalizeWiki(root, generation.pages, generation.seedSlugs);\n await logCompile(root, buckets, generation, existingIds);\n }\n return summarizeCompile(buckets, generation, extractions, options);\n}\n\n/** Log where the schema was loaded from so the user can confirm it was picked up. */\nfunction reportSchemaStatus(schema: SchemaConfig): void {\n if (schema.loadedFrom) {\n output.status(\"i\", output.dim(`Schema: ${schema.loadedFrom}`));\n }\n}\n\n/** Append affected-source changes (logging each addition) to the change list. */\nfunction augmentWithAffectedSources(changes: SourceChange[], affected: string[]): void {\n for (const file of affected) {\n output.status(\"~\", output.info(`${file} [affected by shared concept]`));\n changes.push({ file, status: \"changed\" });\n }\n}\n\n/** Mark wiki pages owned solely by deleted sources as orphaned. */\nasync function markDeletedAsOrphaned(\n root: string,\n deleted: SourceChange[],\n state: WikiState,\n): Promise<void> {\n for (const del of deleted) {\n await markOrphaned(root, del.file, state);\n }\n}\n\n/** Log frozen slugs (shared concepts whose deletion-pinned content must persist). */\nfunction reportFrozenSlugs(frozenSlugs: Set<string>): void {\n for (const slug of frozenSlugs) {\n output.status(\"i\", output.dim(`Frozen: ${slug} (shared with deleted source)`));\n }\n}\n\n/**\n * Phase 1: extract concepts for the directly-changed batch, then expand to\n * any unchanged sources whose concepts overlap with newly extracted slugs.\n */\nasync function runExtractionPhases(\n root: string,\n toCompile: SourceChange[],\n state: WikiState,\n allChanges: SourceChange[],\n): Promise<ExtractionResult[]> {\n const extractions: ExtractionResult[] = [];\n for (const change of toCompile) {\n extractions.push(await extractForSource(root, change.file));\n }\n\n const lateAffected = findLateAffectedSources(extractions, state, allChanges);\n for (const file of lateAffected) {\n output.status(\"~\", output.info(`${file} [shares concept with new source]`));\n extractions.push(await extractForSource(root, file));\n }\n\n return extractions;\n}\n\n/**\n * Resolve interlinks, regenerate index/MOC, refresh embeddings\n * post-write. Seed-page slugs are folded into both the changed-slug\n * set (so embeddings refresh covers them) and the new-slug set (so\n * inbound-link resolution scans existing pages for mentions of seed\n * titles). Without that, schema-declared seed pages would land on\n * disk but stay unlinked and absent from the embedding store.\n */\nasync function finalizeWiki(\n root: string,\n pages: MergedConcept[],\n seedSlugs: string[] = [],\n): Promise<void> {\n const conceptChangedSlugs = pages.map((entry) => entry.slug);\n const conceptNewSlugs = pages\n .filter((entry) => entry.concept.is_new)\n .map((entry) => entry.slug);\n const allChangedSlugs = [...conceptChangedSlugs, ...seedSlugs];\n const allNewSlugs = [...conceptNewSlugs, ...seedSlugs];\n\n if (allChangedSlugs.length > 0) {\n output.status(\"🔗\", output.info(\"Resolving interlinks...\"));\n await resolveLinks(root, allChangedSlugs, allNewSlugs);\n }\n\n await generateIndex(root);\n await generateMOC(root);\n await safelyUpdateEmbeddings(root, allChangedSlugs);\n}\n\n/** Print a summary of detected source file changes. */\nfunction printChangesSummary(changes: SourceChange[]): void {\n const iconMap: Record<string, string> = {\n new: \"+\", changed: \"~\", unchanged: \".\", deleted: \"-\",\n };\n const fmtMap: Record<string, (s: string) => string> = {\n new: output.success, changed: output.warn, unchanged: output.dim, deleted: output.error,\n };\n\n for (const c of changes) {\n const icon = iconMap[c.status] ?? \"?\";\n const fmt = fmtMap[c.status] ?? output.dim;\n output.status(icon, fmt(`${c.file} [${c.status}]`));\n }\n}\n\n/**\n * Phase 1: Extract concepts from a source without generating pages.\n * Returns extraction data for the generation phase.\n */\nasync function extractForSource(\n root: string,\n sourceFile: string,\n): Promise<ExtractionResult> {\n output.status(\"*\", output.info(`Extracting: ${sourceFile}`));\n\n const sourcePath = path.join(root, SOURCES_DIR, sourceFile);\n const sourceContent = await readFile(sourcePath, \"utf-8\");\n const existingIndex = await safeReadFile(path.join(root, INDEX_FILE));\n const concepts = await extractConcepts(sourceContent, existingIndex);\n\n if (concepts.length > 0) {\n const names = concepts.map((c) => c.concept).join(\", \");\n output.status(\"*\", output.dim(` Found ${concepts.length} concepts: ${names}`));\n }\n return { sourceFile, sourcePath, sourceContent, concepts };\n}\n\n/** A concept with all contributing sources merged for generation. */\ninterface MergedConcept {\n slug: string;\n concept: ExtractedConcept;\n sourceFiles: string[];\n combinedContent: string;\n}\n\n/**\n * Reconcile metadata from a later-extracted concept into an existing merged entry.\n * Called when multiple sources contribute the same slug — produces the most\n * pessimistic aggregate view of confidence, provenance, and contradictions.\n *\n * Rules:\n * - confidence: min (most pessimistic value wins)\n * - provenanceState: always 'merged' once two sources are involved\n * - contradictedBy: union by slug (deduplicating on slug identity)\n *\n * `inferredParagraphs` is no longer reconciled — it is derived from the\n * rendered page body at lint time, not from extraction metadata.\n */\nexport function reconcileConceptMetadata(\n existing: ExtractedConcept,\n incoming: ExtractedConcept,\n): ExtractedConcept {\n const reconciled = { ...existing };\n\n // Minimum confidence — the weaker source's score governs the whole page.\n if (typeof incoming.confidence === \"number\") {\n reconciled.confidence = typeof existing.confidence === \"number\"\n ? Math.min(existing.confidence, incoming.confidence)\n : incoming.confidence;\n }\n\n // Merged state is the canonical answer when multiple sources contribute.\n reconciled.provenanceState = \"merged\";\n\n // Union contradictedBy entries, deduplicating by slug.\n const refs = [...(existing.contradictedBy ?? [])];\n const seenSlugs = new Set(refs.map((r) => r.slug));\n for (const ref of incoming.contradictedBy ?? []) {\n if (!seenSlugs.has(ref.slug)) {\n refs.push(ref);\n seenSlugs.add(ref.slug);\n }\n }\n reconciled.contradictedBy = refs.length > 0 ? refs : undefined;\n\n return reconciled;\n}\n\n/**\n * Merge extractions so each concept slug maps to ALL contributing sources.\n * When sources A and B both extract concept X, the LLM receives combined\n * content from both sources, producing a single page that reflects all\n * contributing material rather than just the last source processed.\n * Metadata is reconciled across all contributing concepts via\n * reconcileConceptMetadata so contradictions from later sources are not lost.\n *\n * Combined content is then run through {@link buildBudgetedCombinedContent}\n * so popular concepts that appear in many overlapping sources do not blow\n * past the LLM provider's context window (issue #39). When the raw total\n * fits the budget, the output is byte-identical to the previous unbudgeted\n * concatenation.\n */\nfunction mergeExtractions(\n extractions: ExtractionResult[],\n frozenSlugs: Set<string>,\n): MergedConcept[] {\n const bySlug = new Map<string, MergedConcept>();\n const slicesBySlug = new Map<string, SourceSlice[]>();\n\n for (const result of extractions) {\n if (result.concepts.length === 0) continue;\n\n for (const concept of result.concepts) {\n const slug = slugify(concept.concept);\n if (frozenSlugs.has(slug)) continue;\n\n const existing = bySlug.get(slug);\n if (existing) {\n existing.concept = reconcileConceptMetadata(existing.concept, concept);\n existing.sourceFiles.push(result.sourceFile);\n } else {\n bySlug.set(slug, {\n slug,\n concept,\n sourceFiles: [result.sourceFile],\n combinedContent: \"\",\n });\n slicesBySlug.set(slug, []);\n }\n slicesBySlug.get(slug)!.push({\n file: result.sourceFile,\n content: result.sourceContent,\n });\n }\n }\n\n for (const merged of bySlug.values()) {\n const slices = slicesBySlug.get(merged.slug) ?? [];\n merged.combinedContent = buildBudgetedCombinedContent(\n merged.concept.concept,\n slices,\n );\n }\n\n return Array.from(bySlug.values());\n}\n\n/** Outcome of generating a single merged concept page. */\ninterface MergedPageOutcome {\n error?: string;\n candidateId?: string;\n}\n\n/**\n * Generate a wiki page from merged source content.\n * For shared concepts, the LLM sees content from all contributing sources\n * and frontmatter records every source file. When `options.review` is set,\n * the rendered page is persisted as a review candidate instead of being\n * written into `wiki/`.\n */\nasync function generateMergedPage(\n root: string,\n entry: MergedConcept,\n schema: SchemaConfig,\n options: CompileOptions,\n sourceStates: SourceStateMap,\n): Promise<MergedPageOutcome> {\n const fullPage = await renderMergedPageContent(root, entry, schema);\n\n if (options.review) {\n return await persistReviewCandidate(root, entry, fullPage, sourceStates, schema);\n }\n\n const pagePath = path.join(root, CONCEPTS_DIR, `${entry.slug}.md`);\n const error = await writePageIfValid(pagePath, fullPage, entry.concept.concept);\n return { error: error ?? undefined };\n}\n\n/** Persist a candidate JSON record for later review and report it on stdout. */\nasync function persistReviewCandidate(\n root: string,\n entry: MergedConcept,\n fullPage: string,\n sourceStates: SourceStateMap,\n schema: SchemaConfig,\n): Promise<MergedPageOutcome> {\n // Run schema-aware AND provenance-aware lint against the candidate body so\n // both classes of violation are visible in `review show` before a reviewer\n // approves the page. The virtual file path uses the slug so diagnostics\n // are identifiable without a real disk path. Provenance lint covers the\n // citation rules that previously only ran on the post-promotion compile.\n const virtualPath = `wiki/concepts/${entry.slug}.md`;\n const schemaViolations = checkPageCrossLinks(fullPage, virtualPath, schema);\n const provenanceViolations = await collectCandidateProvenanceViolations(\n root,\n fullPage,\n virtualPath,\n );\n\n const candidate: ReviewCandidate = await writeCandidate(root, {\n title: entry.concept.concept,\n slug: entry.slug,\n summary: entry.concept.summary,\n sources: entry.sourceFiles,\n body: fullPage,\n sourceStates: pickStatesForSources(sourceStates, entry.sourceFiles),\n schemaViolations: schemaViolations.length > 0 ? schemaViolations : undefined,\n provenanceViolations:\n provenanceViolations.length > 0 ? provenanceViolations : undefined,\n });\n output.status(\"?\", output.info(`Candidate ready: ${candidate.id} (${entry.slug})`));\n return { candidateId: candidate.id };\n}\n\n/**\n * Run the in-memory provenance lint rules against a candidate body:\n * malformed claim citations + broken-source / out-of-bounds line spans.\n * Returns the combined diagnostics so writeCandidate can persist them.\n */\nasync function collectCandidateProvenanceViolations(\n root: string,\n fullPage: string,\n virtualPath: string,\n): Promise<LintResult[]> {\n const malformed = checkPageMalformedCitations(fullPage, virtualPath);\n const broken = await checkPageBrokenCitations(\n fullPage,\n virtualPath,\n path.join(root, SOURCES_DIR),\n );\n return [...malformed, ...broken];\n}\n\n/**\n * Materialise schema-declared seed pages (overview, comparison, entity).\n * Each seed page is written under wiki/concepts/ next to concept pages so\n * existing tooling (index, MOC, lint, embeddings) treats them uniformly.\n * Slugs from generated pages this run are added so seed pages can be linked\n * deterministically without waiting for a second compile pass.\n * @param root - Project root directory.\n * @param schema - Resolved schema config.\n * @param generation - Result of the concept-page generation phase.\n */\nasync function generateSeedPages(\n root: string,\n schema: SchemaConfig,\n generation: PageGenerationResult,\n): Promise<void> {\n if (schema.seedPages.length === 0) return;\n for (const seed of schema.seedPages) {\n const result = await generateSingleSeedPage(root, schema, seed);\n if (result.error) {\n generation.errors.push(result.error);\n continue;\n }\n generation.seedSlugs.push(result.slug);\n }\n}\n\n/** Outcome of a single seed-page generation: slug always, error when the write failed validation. */\ninterface SeedPageOutcome {\n slug: string;\n error?: string;\n}\n\n/** Build, prompt, and persist a single seed page. */\nasync function generateSingleSeedPage(\n root: string,\n schema: SchemaConfig,\n seed: SeedPage,\n): Promise<SeedPageOutcome> {\n const slug = slugify(seed.title);\n const pagePath = path.join(root, CONCEPTS_DIR, `${slug}.md`);\n const relatedContent = await loadSeedRelatedPages(root, seed.relatedSlugs ?? []);\n const rule = schema.kinds[seed.kind];\n const system = buildSeedPagePrompt(seed, rule, relatedContent);\n const pageBody = await callClaude({\n system,\n messages: [{ role: \"user\", content: `Write the ${seed.kind} page titled \"${seed.title}\".` }],\n });\n\n const now = new Date().toISOString();\n const existing = await safeReadFile(pagePath);\n const existingMeta = existing ? parseFrontmatter(existing).meta : null;\n const createdAt = typeof existingMeta?.createdAt === \"string\" ? existingMeta.createdAt : now;\n const typedFields: WikiFrontmatter = {\n title: seed.title,\n summary: seed.summary,\n sources: [],\n kind: seed.kind,\n createdAt,\n updatedAt: now,\n };\n const frontmatterFields: Record<string, unknown> = { ...typedFields };\n addObsidianMeta(frontmatterFields, seed.title, []);\n addModelProvenanceMeta(frontmatterFields);\n const frontmatter = buildFrontmatter(frontmatterFields);\n const error = await writePageIfValid(pagePath, `${frontmatter}\\n\\n${pageBody}\\n`, seed.title);\n return error ? { slug, error } : { slug };\n}\n\n/** Load the bodies of the related concept pages a seed page should weave together. */\nasync function loadSeedRelatedPages(root: string, slugs: string[]): Promise<string> {\n if (slugs.length === 0) return \"\";\n const contents: string[] = [];\n for (const slug of slugs) {\n const pagePath = path.join(root, CONCEPTS_DIR, `${slug}.md`);\n const content = await safeReadFile(pagePath);\n if (content) contents.push(content);\n }\n return contents.join(\"\\n\\n---\\n\\n\");\n}\n\n/**\n * Call Claude to extract concepts from a source document.\n * @param sourceContent - Full source document text.\n * @param existingIndex - Current wiki index for deduplication.\n * @returns Parsed array of extracted concepts.\n */\nasync function extractConcepts(\n sourceContent: string,\n existingIndex: string,\n): Promise<ExtractedConcept[]> {\n const system = buildExtractionPrompt(sourceContent, existingIndex);\n const rawOutput = await callClaude({\n system,\n messages: [{ role: \"user\", content: \"Extract the key concepts from this source.\" }],\n tools: [CONCEPT_EXTRACTION_TOOL],\n });\n\n return parseConcepts(rawOutput);\n}\n\n/**\n * Validate and atomically write a wiki page, logging the result.\n * @param pagePath - Absolute path to write the page.\n * @param content - Full page content including frontmatter.\n * @param conceptTitle - Title for logging purposes.\n */\nasync function writePageIfValid(\n pagePath: string,\n content: string,\n conceptTitle: string,\n): Promise<string | null> {\n if (!validateWikiPage(content)) {\n output.status(\"!\", output.warn(`Invalid page for \"${conceptTitle}\" — skipped.`));\n return `Invalid page for \"${conceptTitle}\" — failed validation`;\n }\n\n await atomicWrite(pagePath, content);\n return null;\n}\n\n/**\n * Refresh the embeddings store without failing compilation.\n * Semantic search is a non-critical enhancement — missing API keys or\n * transient provider errors should produce a warning, not a broken build.\n */\nasync function safelyUpdateEmbeddings(root: string, changedSlugs: string[]): Promise<void> {\n try {\n await updateEmbeddings(root, changedSlugs);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n output.status(\"!\", output.warn(`Skipped embeddings update: ${message}`));\n }\n}\n\n/**\n * Update the persisted state for a compiled source file.\n * @param root - Project root directory.\n * @param sourcePath - Absolute path to the source file.\n * @param sourceFile - Filename within sources/.\n * @param concepts - Concepts extracted from this source.\n */\nasync function persistSourceState(\n root: string,\n sourcePath: string,\n sourceFile: string,\n concepts: ReturnType<typeof parseConcepts>,\n): Promise<void> {\n const hash = await hashFile(sourcePath);\n const entry: SourceState = {\n hash,\n concepts: concepts.map((c) => slugify(c.concept)),\n compiledAt: new Date().toISOString(),\n };\n\n await updateSourceState(root, sourceFile, entry);\n}\n","/**\n * Manages .llmwiki/state.json — the persistent compilation state that tracks\n * source file hashes and their compiled concepts. Enables incremental\n * compilation by detecting which sources have changed since last compile.\n *\n * Uses atomic writes (write to .tmp, then rename) to prevent corruption\n * from interrupted compiles.\n */\n\nimport { readFile, writeFile, rename, mkdir, copyFile } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { LLMWIKI_DIR, STATE_FILE } from \"./constants.js\";\nimport { note } from \"./output.js\";\nimport type { WikiState, SourceState } from \"./types.js\";\n\nfunction emptyState(): WikiState {\n return { version: 1, indexHash: \"\", sources: {} };\n}\n\n/** State file read outcome: ok = parsed, missing = no file, corrupt = unparseable. */\nexport interface ClassifiedState {\n status: \"ok\" | \"missing\" | \"corrupt\";\n state: WikiState;\n}\n\n/**\n * Read .llmwiki/state.json and classify the outcome WITHOUT side effects.\n * Unlike readState(), this never writes a .bak on corrupt input, so read-only\n * callers (freshness/lint/view/export) can safely use it.\n */\nexport async function readStateClassified(root: string): Promise<ClassifiedState> {\n const filePath = path.join(root, STATE_FILE);\n if (!existsSync(filePath)) return { status: \"missing\", state: emptyState() };\n try {\n const raw = await readFile(filePath, \"utf-8\");\n return { status: \"ok\", state: JSON.parse(raw) as WikiState };\n } catch {\n return { status: \"corrupt\", state: emptyState() };\n }\n}\n\n/** Read .llmwiki/state.json, recovering from corruption gracefully (writes a .bak). */\nexport async function readState(root: string): Promise<WikiState> {\n const classified = await readStateClassified(root);\n if (classified.status === \"corrupt\") {\n const filePath = path.join(root, STATE_FILE);\n const bakPath = filePath + \".bak\";\n note(`⚠ Corrupt state.json — backed up to ${bakPath}, starting fresh.`);\n await copyFile(filePath, bakPath);\n }\n return classified.state;\n}\n\n/** Atomically write state.json (write .tmp then rename). */\nexport async function writeState(root: string, state: WikiState): Promise<void> {\n const dir = path.join(root, LLMWIKI_DIR);\n await mkdir(dir, { recursive: true });\n\n const filePath = path.join(root, STATE_FILE);\n const tmpPath = filePath + \".tmp\";\n\n await writeFile(tmpPath, JSON.stringify(state, null, 2), \"utf-8\");\n await rename(tmpPath, filePath);\n}\n\n/**\n * Update a single source's entry in state after successful compilation.\n * Per-source granularity means interrupted compiles only reprocess incomplete sources.\n */\nexport async function updateSourceState(\n root: string,\n sourceFile: string,\n entry: SourceState,\n): Promise<void> {\n const state = await readState(root);\n state.sources[sourceFile] = entry;\n await writeState(root, state);\n}\n\n/** Remove a source entry from state (for deleted sources). */\nexport async function removeSourceState(\n root: string,\n sourceFile: string,\n): Promise<void> {\n const state = await readState(root);\n delete state.sources[sourceFile];\n await writeState(root, state);\n}\n","/**\n * Source-state snapshot helpers shared between the live compile path and the\n * review-candidate path.\n *\n * The compile pipeline normally persists a `SourceState` entry for every\n * extracted source so subsequent compiles can skip unchanged inputs. When\n * compile runs in `--review` mode, page writes are deferred — but the same\n * per-source state still needs to land on approval, otherwise approved\n * sources stay marked as \"new/changed\" forever and reproduce duplicate\n * candidates on every compile.\n *\n * This module produces a `Record<sourceFile, SourceState>` snapshot from the\n * extraction results so it can ride along inside each `ReviewCandidate` and\n * be flushed to `.llmwiki/state.json` at approval time.\n */\n\nimport path from \"path\";\nimport { hashFile } from \"./hasher.js\";\nimport { slugify } from \"../utils/markdown.js\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\nimport type { ExtractionResult } from \"./deps.js\";\nimport type { SourceState } from \"../utils/types.js\";\n\n/**\n * Compute a per-source state snapshot keyed by source filename.\n *\n * Hashes every contributing source once so each candidate carries the\n * incremental-state payload required to mark its sources compiled on\n * approval. Sources with no extracted concepts are skipped — we only mark\n * sources compiled when extraction succeeded, mirroring the live path's\n * behaviour.\n *\n * @param root - Project root directory.\n * @param extractions - Extraction results from the compile pipeline.\n * @returns Map of source filename → SourceState ready for state.json.\n */\nexport async function buildExtractionSourceStates(\n root: string,\n extractions: ExtractionResult[],\n): Promise<Record<string, SourceState>> {\n const snapshot: Record<string, SourceState> = {};\n const compiledAt = new Date().toISOString();\n\n for (const result of extractions) {\n if (result.concepts.length === 0) continue;\n snapshot[result.sourceFile] = await buildEntry(root, result, compiledAt);\n }\n\n return snapshot;\n}\n\n/** Build a single SourceState entry for one extraction result. */\nasync function buildEntry(\n root: string,\n result: ExtractionResult,\n compiledAt: string,\n): Promise<SourceState> {\n const filePath = path.join(root, SOURCES_DIR, result.sourceFile);\n const hash = await hashFile(filePath);\n return {\n hash,\n concepts: result.concepts.map((concept) => slugify(concept.concept)),\n compiledAt,\n };\n}\n\n/**\n * Filter a global source-state snapshot down to entries relevant to a\n * specific candidate. A candidate carries only the source-state entries\n * for sources that actually contributed to it, so on approval we can\n * persist a minimal, accurate slice into state.json.\n *\n * @param allStates - Global per-source snapshot from buildExtractionSourceStates.\n * @param sourceFiles - Source filenames that contributed to the candidate.\n */\nexport function pickStatesForSources(\n allStates: Record<string, SourceState>,\n sourceFiles: string[],\n): Record<string, SourceState> {\n const picked: Record<string, SourceState> = {};\n for (const file of sourceFiles) {\n const entry = allStates[file];\n if (entry) picked[file] = entry;\n }\n return picked;\n}\n","/**\n * Source file hashing for change detection.\n * Computes SHA-256 hashes of source files and compares them against\n * previously stored state to determine which files need recompilation.\n * This enables incremental compilation — only changed or new sources\n * are sent through the LLM pipeline.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { readFile, readdir } from \"fs/promises\";\nimport path from \"path\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\nimport type { WikiState, SourceChange } from \"../utils/types.js\";\n\n/**\n * Read a file and compute its SHA-256 hash.\n * @param filePath - Absolute path to the file to hash.\n * @returns Hex-encoded SHA-256 digest of the file contents.\n */\nexport async function hashFile(filePath: string): Promise<string> {\n const content = await readFile(filePath, \"utf-8\");\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n/**\n * Scan the sources/ directory and compare file hashes against previous state\n * to identify new, changed, unchanged, and deleted source files.\n * @param root - Project root directory containing the sources/ folder.\n * @param prevState - The previously persisted WikiState to compare against.\n * @returns Array of SourceChange entries describing each file's status.\n */\nexport async function detectChanges(\n root: string,\n prevState: WikiState,\n): Promise<SourceChange[]> {\n const sourcesPath = path.join(root, SOURCES_DIR);\n const currentFiles = await listSourceFiles(sourcesPath);\n const changes: SourceChange[] = [];\n\n for (const file of currentFiles) {\n const status = await classifyFile(root, file, prevState);\n changes.push({ file, status });\n }\n\n const deletedChanges = findDeletedFiles(currentFiles, prevState);\n changes.push(...deletedChanges);\n\n return changes;\n}\n\n/**\n * List all markdown files in the sources directory.\n * @param sourcesPath - Absolute path to the sources/ directory.\n * @returns Array of filenames (not full paths).\n */\nasync function listSourceFiles(sourcesPath: string): Promise<string[]> {\n try {\n const entries = await readdir(sourcesPath);\n return entries.filter((f) => f.endsWith(\".md\"));\n } catch {\n return [];\n }\n}\n\n/**\n * Classify a single source file as new, changed, or unchanged.\n * @param root - Project root directory.\n * @param file - Filename within sources/.\n * @param prevState - Previous compilation state.\n * @returns The change status for this file.\n */\nasync function classifyFile(\n root: string,\n file: string,\n prevState: WikiState,\n): Promise<SourceChange[\"status\"]> {\n const filePath = path.join(root, SOURCES_DIR, file);\n const hash = await hashFile(filePath);\n const prev = prevState.sources[file];\n\n if (!prev) return \"new\";\n if (prev.hash !== hash) return \"changed\";\n return \"unchanged\";\n}\n\n/**\n * Find source files present in previous state but missing from disk.\n * @param currentFiles - Files currently on disk.\n * @param prevState - Previous compilation state.\n * @returns Array of SourceChange entries for deleted files.\n */\nfunction findDeletedFiles(\n currentFiles: string[],\n prevState: WikiState,\n): SourceChange[] {\n const currentSet = new Set(currentFiles);\n return Object.keys(prevState.sources)\n .filter((file) => !currentSet.has(file))\n .map((file) => ({ file, status: \"deleted\" as const }));\n}\n","/**\n * OpenAI LLM provider implementation.\n *\n * Wraps the openai npm package to implement the LLMProvider interface.\n * Translates Anthropic-style tool schemas (input_schema) to OpenAI format (parameters).\n */\n\nimport OpenAI from \"openai\";\nimport type { LLMProvider, LLMMessage, LLMTool } from \"../utils/provider.js\";\nimport { EMBEDDING_MODELS, OPENAI_DEFAULT_TIMEOUT_MS } from \"../utils/constants.js\";\n\n/** Construction options for an OpenAI-compatible provider. */\ninterface OpenAIProviderOptions {\n baseURL?: string;\n apiKey?: string;\n embeddingsBaseURL?: string;\n embeddingModel?: string;\n /**\n * Per-request timeout in milliseconds. Defaults to 10 minutes for cloud\n * OpenAI (matches the SDK default). Long compile-time completions on\n * slower local models can exceed this — see {@link OllamaProvider} which\n * raises the default and reads LLMWIKI_REQUEST_TIMEOUT_MS / OLLAMA_TIMEOUT_MS.\n */\n timeoutMs?: number;\n}\n\n/**\n * Read an integer-millisecond timeout from an env var. Returns undefined when\n * the env var is unset, empty, non-numeric, zero, or negative — so the caller\n * silently falls back to the next source in its resolution chain (env-var\n * typos like `OLLAMA_TIMEOUT_MS=30m` are not surfaced to the user).\n */\nexport function readTimeoutEnv(name: string): number | undefined {\n const raw = process.env[name]?.trim();\n if (!raw) return undefined;\n const parsed = Number(raw);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/** Resolve the OpenAI client timeout from LLMWIKI_REQUEST_TIMEOUT_MS, if set. */\nfunction resolveOpenAITimeoutMs(): number | undefined {\n return readTimeoutEnv(\"LLMWIKI_REQUEST_TIMEOUT_MS\");\n}\n\n/**\n * Placeholder passed to the OpenAI client when no real key is set. The SDK (v6+)\n * throws on a missing/empty key at construction, but real credential validation\n * is owned by `ensureProviderAvailable` (the provider guard); deferring here\n * keeps that the single source of truth. Local servers that ignore auth accept\n * any value anyway.\n */\nconst PLACEHOLDER_API_KEY = \"llmwiki-unset\";\n\n/** Translate an Anthropic-style LLMTool to an OpenAI ChatCompletionTool. */\nexport function translateToolToOpenAI(\n tool: LLMTool,\n): OpenAI.ChatCompletionTool {\n return {\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.input_schema,\n },\n };\n}\n\n/** OpenAI-backed LLM provider. */\nexport class OpenAIProvider implements LLMProvider {\n protected readonly client: OpenAI;\n protected readonly embeddingsClient: OpenAI;\n protected readonly model: string;\n protected readonly configuredEmbeddingModel?: string;\n\n constructor(model: string, options: OpenAIProviderOptions = {}) {\n this.model = model;\n this.configuredEmbeddingModel = options.embeddingModel;\n // The OpenAI SDK (v6+) throws on a missing/empty key at construction. Real\n // credential validation is owned by the provider guard, so pass a\n // placeholder when unset to defer the check to that single source of truth.\n const resolvedKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? PLACEHOLDER_API_KEY;\n const timeout = options.timeoutMs ?? resolveOpenAITimeoutMs() ?? OPENAI_DEFAULT_TIMEOUT_MS;\n this.client = new OpenAI({\n apiKey: resolvedKey,\n baseURL: options.baseURL ?? null,\n timeout,\n });\n this.embeddingsClient = options.embeddingsBaseURL\n ? new OpenAI({ apiKey: resolvedKey, baseURL: options.embeddingsBaseURL, timeout })\n : this.client;\n }\n\n /** Send a single non-streaming completion request. */\n async complete(system: string, messages: LLMMessage[], maxTokens: number): Promise<string> {\n const response = await this.client.chat.completions.create({\n model: this.model,\n max_tokens: maxTokens,\n messages: [{ role: \"system\", content: system }, ...messages],\n });\n\n return response.choices[0]?.message?.content ?? \"\";\n }\n\n /** Stream a completion, invoking onToken for each text chunk. */\n async stream(\n system: string,\n messages: LLMMessage[],\n maxTokens: number,\n onToken?: (text: string) => void,\n ): Promise<string> {\n const stream = await this.client.chat.completions.create({\n model: this.model,\n max_tokens: maxTokens,\n messages: [{ role: \"system\", content: system }, ...messages],\n stream: true,\n });\n\n let fullText = \"\";\n for await (const chunk of stream) {\n const delta = chunk.choices[0]?.delta?.content;\n if (delta) {\n fullText += delta;\n onToken?.(delta);\n }\n }\n\n return fullText;\n }\n\n /** Call the model with tool definitions and return the parsed tool input as JSON. */\n async toolCall(\n system: string,\n messages: LLMMessage[],\n tools: LLMTool[],\n maxTokens: number,\n ): Promise<string> {\n const openaiTools = tools.map(translateToolToOpenAI);\n\n const response = await this.client.chat.completions.create({\n model: this.model,\n max_tokens: maxTokens,\n messages: [{ role: \"system\", content: system }, ...messages],\n tools: openaiTools,\n tool_choice: \"required\",\n });\n\n // openai v6 made tool_calls a union of function and custom calls; only the\n // function variant carries `.function`.\n const toolCall = response.choices[0]?.message?.tool_calls?.[0];\n if (toolCall?.type === \"function\") {\n return toolCall.function.arguments;\n }\n\n return response.choices[0]?.message?.content ?? \"\";\n }\n\n /**\n * Produce a single embedding vector via the OpenAI embeddings API.\n * Subclasses (e.g. Ollama) override embeddingModel() to pick a different model.\n */\n async embed(text: string): Promise<number[]> {\n const response = await this.embeddingsClient.embeddings.create({\n model: this.embeddingModel(),\n input: text,\n });\n\n const vector = response.data[0]?.embedding;\n if (!Array.isArray(vector)) {\n throw new Error(\"OpenAI embeddings response did not include a vector.\");\n }\n return vector;\n }\n\n /** Default embedding model for this provider. Subclasses may override. */\n protected embeddingModel(): string {\n return this.configuredEmbeddingModel ?? EMBEDDING_MODELS.openai;\n }\n}\n","/**\n * Ollama LLM provider implementation.\n *\n * Extends OpenAIProvider since Ollama exposes an OpenAI-compatible API.\n * Overrides only the constructor to set baseURL and disable API key auth.\n */\n\nimport { OpenAIProvider, readTimeoutEnv } from \"./openai.js\";\nimport { EMBEDDING_MODELS, OLLAMA_DEFAULT_TIMEOUT_MS } from \"../utils/constants.js\";\n\n/** Construction options for an Ollama-compatible provider. */\ninterface OllamaProviderOptions {\n baseURL: string;\n embeddingsBaseURL?: string;\n embeddingModel?: string;\n /**\n * Per-request timeout in milliseconds. Defaults to 30 minutes for Ollama\n * because local models on modest hardware can take much longer than the\n * cloud-OpenAI default of 10. Override with OLLAMA_TIMEOUT_MS or the\n * provider-agnostic LLMWIKI_REQUEST_TIMEOUT_MS env var.\n */\n timeoutMs?: number;\n}\n\n/** Resolve the Ollama timeout: explicit option → OLLAMA_TIMEOUT_MS → LLMWIKI_REQUEST_TIMEOUT_MS → default. */\nfunction resolveOllamaTimeoutMs(explicit?: number): number {\n return (\n explicit ??\n readTimeoutEnv(\"OLLAMA_TIMEOUT_MS\") ??\n readTimeoutEnv(\"LLMWIKI_REQUEST_TIMEOUT_MS\") ??\n OLLAMA_DEFAULT_TIMEOUT_MS\n );\n}\n\n/** Ollama-backed LLM provider using the OpenAI-compatible endpoint. */\nexport class OllamaProvider extends OpenAIProvider {\n constructor(model: string, options: OllamaProviderOptions) {\n super(model, {\n baseURL: options.baseURL,\n apiKey: \"ollama\",\n embeddingsBaseURL: options.embeddingsBaseURL,\n embeddingModel: options.embeddingModel,\n timeoutMs: resolveOllamaTimeoutMs(options.timeoutMs),\n });\n }\n\n /** Ollama ships a dedicated embedding model (nomic-embed-text). */\n protected override embeddingModel(): string {\n return this.configuredEmbeddingModel ?? EMBEDDING_MODELS.ollama;\n }\n}\n","/**\n * MiniMax LLM provider implementation.\n *\n * Extends OpenAIProvider since MiniMax exposes an OpenAI-compatible API.\n * Overrides only the constructor to set MiniMax's base URL and API key.\n */\n\nimport { OpenAIProvider } from \"./openai.js\";\n\n/** MiniMax API base URL. */\nconst MINIMAX_BASE_URL = \"https://api.minimax.io/v1\";\n\n/** MiniMax-backed LLM provider using the OpenAI-compatible endpoint. */\nexport class MiniMaxProvider extends OpenAIProvider {\n constructor(model: string, apiKey: string) {\n super(model, { baseURL: MINIMAX_BASE_URL, apiKey });\n }\n}\n","/**\n * GitHub Copilot LLM provider implementation.\n *\n * Uses the GitHub Copilot API (https://api.githubcopilot.com), which exposes\n * an OpenAI-compatible chat endpoint. Requires a GitHub OAuth token with\n * Copilot access — use `gh auth token` to obtain one. Classic PATs are NOT\n * supported by this endpoint.\n *\n * Note: GitHub Copilot does not expose an embeddings API. Calling embed() will\n * throw with a helpful message. For workflows that require semantic search\n * (query with chunked retrieval), use the openai provider with OPENAI_API_KEY.\n */\n\nimport { OpenAIProvider } from \"./openai.js\";\nimport { COPILOT_BASE_URL } from \"../utils/constants.js\";\n\n/** GitHub Copilot-backed LLM provider using the OpenAI-compatible endpoint. */\nexport class CopilotProvider extends OpenAIProvider {\n constructor(model: string, apiKey: string) {\n super(model, { baseURL: COPILOT_BASE_URL, apiKey });\n }\n\n /**\n * GitHub Copilot has no native embeddings API.\n * Throws an informative error directing the user to an alternative.\n */\n override async embed(_text: string): Promise<number[]> {\n throw new Error(\n \"GitHub Copilot does not support embeddings.\\n\" +\n \" For semantic search (llmwiki query), switch to the OpenAI provider:\\n\" +\n \" export LLMWIKI_PROVIDER=openai\\n\" +\n \" export OPENAI_API_KEY=sk-...\",\n );\n }\n}\n","/**\n * Claude Agent SDK LLM provider implementation.\n *\n * Wraps `@anthropic-ai/claude-agent-sdk`'s `query()` to implement the\n * LLMProvider interface. Unlike the `anthropic` provider (which calls the raw\n * Messages API), this backend routes through the Agent SDK and therefore\n * authenticates with the user's local Claude Code login (OAuth/subscription) —\n * no standalone ANTHROPIC_API_KEY is required.\n *\n * Generation runs in single-shot mode (one turn, no agentic file tools) so it\n * behaves like a plain completion. Structured output (`toolCall`) is handled\n * faithfully by registering the requested tool as an in-process SDK MCP tool\n * and capturing the model's `tool_use` input. Embeddings delegate to Voyage,\n * mirroring the `anthropic` provider.\n */\n\nimport { query, createSdkMcpServer, tool } from \"@anthropic-ai/claude-agent-sdk\";\nimport type { LLMProvider, LLMMessage, LLMTool } from \"../utils/provider.js\";\nimport { voyageEmbed } from \"./voyage-embed.js\";\nimport { jsonSchemaToZodShape } from \"./json-schema-to-zod.js\";\n\n/** Name for the throwaway in-process MCP server used to host a tool. */\nconst TOOL_SERVER_NAME = \"llmwiki\";\n\n/**\n * Turn ceiling for a single-shot request. The SDK reports an error result when\n * this ceiling is *reached*, so it must sit above the one turn a clean\n * completion or forced tool call actually consumes — `1` errors on every call.\n */\nconst MAX_TURNS = 4;\n\n/**\n * Appended to text-generation prompts. The Agent SDK runs an agentic model that\n * otherwise prefixes answers with conversational scaffolding (\"I'll write…\");\n * this keeps the output to the requested document, matching the raw API path.\n */\nconst OUTPUT_ONLY_DIRECTIVE =\n \"Respond with only the requested content — no preamble, explanation, or sign-off.\";\n\n/** A content block on an assistant message (text or tool_use). */\ninterface AssistantBlock {\n type: string;\n text?: string;\n name?: string;\n input?: unknown;\n}\n\n/** Join the user-role message contents into a single prompt string. */\nfunction buildPrompt(messages: LLMMessage[]): string {\n return messages.map((message) => message.content).join(\"\\n\\n\");\n}\n\n/** High-frequency SDK message types/subtypes that add noise without insight. */\nconst NOISY_MESSAGES = new Set([\"thinking_tokens\", \"rate_limit_event\"]);\n\n/** Resolve the LLMWIKI_DEBUG level: off, a concise trace, or the full SDK firehose. */\nfunction debugLevel(): \"off\" | \"on\" | \"verbose\" {\n const value = process.env.LLMWIKI_DEBUG?.trim().toLowerCase();\n if (!value) return \"off\";\n return value === \"verbose\" || value === \"2\" ? \"verbose\" : \"on\";\n}\n\n/**\n * Extra query options that surface what the Agent SDK is doing. Always streams\n * the `claude` subprocess stderr when debugging; the full SDK verbose logs are\n * added only at the `verbose` level. Empty object unless LLMWIKI_DEBUG is set.\n */\nfunction debugOptions(): { debug?: boolean; stderr?: (data: string) => void } {\n const level = debugLevel();\n if (level === \"off\") return {};\n return { debug: level === \"verbose\", stderr: (data) => process.stderr.write(data) };\n}\n\n/** Print a one-line trace of a meaningful SDK message (type and subtype). */\nfunction traceMessage(raw: unknown): void {\n if (debugLevel() === \"off\") return;\n const message = raw as { type?: string; subtype?: string };\n if (NOISY_MESSAGES.has(message.subtype ?? \"\") || NOISY_MESSAGES.has(message.type ?? \"\")) return;\n const subtype = message.subtype ? `:${message.subtype}` : \"\";\n process.stderr.write(`[claude-agent] ${message.type ?? \"?\"}${subtype}\\n`);\n}\n\n/** Accumulate text from assistant content blocks, optionally emitting each chunk. */\nfunction accumulateText(blocks: AssistantBlock[], onToken?: (text: string) => void): string {\n let text = \"\";\n for (const block of blocks) {\n if (block.type === \"text\" && block.text) {\n text += block.text;\n onToken?.(block.text);\n }\n }\n return text;\n}\n\n/**\n * Extract the text of a success result, or throw on any error result subtype\n * (auth failure, max turns, execution failure, …). Failing loudly here keeps a\n * bad SDK run from silently producing empty or partial wiki output.\n */\nfunction resultText(message: { subtype?: string; result?: string; errors?: string[] }): string {\n if (message.subtype === \"success\") return message.result ?? \"\";\n const detail = message.errors?.join(\"; \") || message.subtype || \"unknown error\";\n throw new Error(`Claude Agent SDK returned an error result: ${detail}`);\n}\n\n/** Return the input of the first matching `tool_use` block, or undefined. */\nfunction findToolInput(\n blocks: AssistantBlock[],\n toolName: string,\n qualifiedName: string,\n): unknown {\n const match = blocks.find(\n (block) =>\n block.type === \"tool_use\" && (block.name === toolName || block.name === qualifiedName),\n );\n return match?.input;\n}\n\n/** Claude Agent SDK-backed LLM provider using the local Claude Code login. */\nexport class ClaudeAgentProvider implements LLMProvider {\n private readonly model: string;\n\n constructor(model: string) {\n this.model = model;\n }\n\n /** Send a single non-streaming completion request. */\n async complete(system: string, messages: LLMMessage[], _maxTokens: number): Promise<string> {\n return this.runText(system, buildPrompt(messages));\n }\n\n /** Stream a completion, invoking onToken for each text chunk. */\n async stream(\n system: string,\n messages: LLMMessage[],\n _maxTokens: number,\n onToken?: (text: string) => void,\n ): Promise<string> {\n return this.runText(system, buildPrompt(messages), onToken);\n }\n\n /** Run a single-shot, tool-free query and accumulate the assistant's text. */\n private async runText(\n system: string,\n prompt: string,\n onToken?: (text: string) => void,\n ): Promise<string> {\n const response = query({\n prompt,\n options: {\n systemPrompt: `${system}\\n\\n${OUTPUT_ONLY_DIRECTIVE}`,\n model: this.model,\n maxTurns: MAX_TURNS,\n tools: [],\n allowedTools: [],\n ...debugOptions(),\n },\n });\n\n let streamed = \"\";\n let finalText = \"\";\n for await (const message of response) {\n traceMessage(message);\n if (message.type === \"assistant\") {\n streamed += accumulateText(message.message.content as AssistantBlock[], onToken);\n } else if (message.type === \"result\") {\n finalText = resultText(message);\n }\n }\n return finalText || streamed;\n }\n\n /**\n * Force a single structured tool call and return its input as a JSON string,\n * matching the shape callers parse from the other providers.\n */\n async toolCall(\n system: string,\n messages: LLMMessage[],\n tools: LLMTool[],\n _maxTokens: number,\n ): Promise<string> {\n const requested = tools[0];\n const qualifiedName = `mcp__${TOOL_SERVER_NAME}__${requested.name}`;\n const sdkTool = tool(\n requested.name,\n requested.description,\n jsonSchemaToZodShape(requested.input_schema),\n async () => ({ content: [{ type: \"text\", text: \"ok\" }] }),\n );\n const mcpServer = createSdkMcpServer({ name: TOOL_SERVER_NAME, tools: [sdkTool] });\n\n const response = query({\n prompt: buildPrompt(messages),\n options: {\n systemPrompt: `${system}\\n\\nRespond by calling the \\`${requested.name}\\` tool.`,\n model: this.model,\n maxTurns: MAX_TURNS,\n mcpServers: { [TOOL_SERVER_NAME]: mcpServer },\n // Disable every built-in tool; only the in-process MCP tool is reachable.\n tools: [],\n allowedTools: [qualifiedName],\n permissionMode: \"bypassPermissions\",\n allowDangerouslySkipPermissions: true,\n ...debugOptions(),\n },\n });\n return collectToolInput(response, requested.name, qualifiedName);\n }\n\n /** Produce a single embedding vector via Voyage (Anthropic has no endpoint). */\n async embed(text: string): Promise<number[]> {\n return voyageEmbed(text);\n }\n}\n\n/** An SDK message: assistant content, or a result whose error subtype must throw. */\ninterface StreamMessage {\n type: string;\n subtype?: string;\n result?: string;\n errors?: string[];\n message?: { content: AssistantBlock[] };\n}\n\n/**\n * Read the first matching `tool_use` input from a query stream as JSON. Throws\n * on an SDK error result, and throws if the model never called the tool (prose\n * or empty output) — callers expect JSON, so a silent fallback would degrade\n * into zero extracted concepts or broken eval output.\n */\nasync function collectToolInput(\n response: AsyncIterable<unknown>,\n toolName: string,\n qualifiedName: string,\n): Promise<string> {\n let prose = \"\";\n for await (const raw of response) {\n traceMessage(raw);\n const message = raw as StreamMessage;\n if (message.type === \"result\") {\n resultText(message); // throws on any error result; success is ignored here\n continue;\n }\n if (message.type !== \"assistant\" || !message.message) continue;\n const input = findToolInput(message.message.content, toolName, qualifiedName);\n if (input !== undefined) return JSON.stringify(input);\n prose += accumulateText(message.message.content);\n }\n const suffix = prose ? `; it responded with prose instead: ${prose.slice(0, 200)}` : \".\";\n throw new Error(`Claude Agent SDK did not call the \"${toolName}\" tool${suffix}`);\n}\n","/**\n * Minimal JSON-Schema → Zod converter for the Claude Agent SDK.\n *\n * The Agent SDK's `tool()` helper takes a Zod *raw shape* (a record of Zod\n * types keyed by property name), but llmwiki declares its tools as JSON Schema\n * (`LLMTool.input_schema`). This converts the subset of JSON Schema that every\n * llmwiki tool uses — object / array / string / number / boolean / enum, with\n * a `required` list — into the raw shape the SDK expects. It is intentionally\n * narrow: unsupported nodes fall back to `z.unknown()` rather than throwing.\n *\n * Two contract details are preserved so the SDK path matches the raw API path:\n * numeric/boolean enums keep their literal types (e.g. eval scores `0 | 1 | 2`,\n * not `\"0\" | \"1\" | \"2\"`), and field `description`s carry through via `.describe`.\n */\n\nimport { z, type ZodType } from \"zod\";\n\n/** The slice of JSON Schema this converter understands. */\ninterface JsonSchemaNode {\n type?: string;\n properties?: Record<string, JsonSchemaNode>;\n required?: string[];\n items?: JsonSchemaNode;\n enum?: unknown[];\n description?: string;\n}\n\n/** Builders for primitive JSON Schema types, keyed by `type`. */\nconst SCALAR_BUILDERS: Record<string, () => ZodType> = {\n string: () => z.string(),\n number: () => z.number(),\n integer: () => z.number(),\n boolean: () => z.boolean(),\n};\n\n/** Build a Zod type for an enum, keeping numeric/boolean values as literals. */\nfunction enumToZod(values: unknown[]): ZodType {\n if (values.every((value) => typeof value === \"string\")) {\n return z.enum(values as [string, ...string[]]);\n }\n const literals = values.map((value) => z.literal(value as string | number | boolean));\n return literals.length === 1\n ? literals[0]\n : z.union(literals as unknown as [ZodType, ZodType, ...ZodType[]]);\n}\n\n/** Map a JSON Schema node to a Zod type, ignoring any field description. */\nfunction baseType(node: JsonSchemaNode): ZodType {\n if (Array.isArray(node.enum) && node.enum.length > 0) return enumToZod(node.enum);\n const scalar = node.type ? SCALAR_BUILDERS[node.type] : undefined;\n if (scalar) return scalar();\n if (node.type === \"array\") {\n return z.array(node.items ? nodeToZod(node.items) : z.unknown());\n }\n if (node.type === \"object\") return z.object(shapeFromProperties(node));\n return z.unknown();\n}\n\n/** Convert a node to a Zod type, carrying its description through via .describe(). */\nfunction nodeToZod(node: JsonSchemaNode): ZodType {\n const zodType = baseType(node);\n return node.description ? zodType.describe(node.description) : zodType;\n}\n\n/** Build a Zod raw shape from an object schema's properties + required list. */\nfunction shapeFromProperties(schema: JsonSchemaNode): Record<string, ZodType> {\n const properties = schema.properties ?? {};\n const required = new Set(schema.required ?? []);\n const shape: Record<string, ZodType> = {};\n for (const [key, value] of Object.entries(properties)) {\n const zodType = nodeToZod(value);\n shape[key] = required.has(key) ? zodType : zodType.optional();\n }\n return shape;\n}\n\n/**\n * Convert an Anthropic-style tool `input_schema` (a JSON Schema object) into\n * the Zod raw shape accepted by the Agent SDK's `tool()` helper.\n */\nexport function jsonSchemaToZodShape(\n inputSchema: Record<string, unknown>,\n): Record<string, ZodType> {\n return shapeFromProperties(inputSchema as JsonSchemaNode);\n}\n","/**\n * LLM provider abstraction layer.\n *\n * Defines the LLMProvider interface and a factory function that reads\n * LLMWIKI_PROVIDER and LLMWIKI_MODEL env vars to instantiate the\n * appropriate backend (Anthropic, OpenAI, Ollama, or MiniMax).\n */\n\nimport { DEFAULT_PROVIDER, PROVIDER_MODELS, OLLAMA_DEFAULT_HOST } from \"./constants.js\";\nimport { AnthropicProvider } from \"../providers/anthropic.js\";\nimport { OpenAIProvider } from \"../providers/openai.js\";\nimport { OllamaProvider } from \"../providers/ollama.js\";\nimport { MiniMaxProvider } from \"../providers/minimax.js\";\nimport { CopilotProvider } from \"../providers/copilot.js\";\nimport { ClaudeAgentProvider } from \"../providers/claude-agent.js\";\nimport {\n resolveAnthropicAuthFromEnv,\n resolveAnthropicBaseURLFromEnv,\n resolveAnthropicModelFromEnv,\n} from \"./claude-settings.js\";\n\n/** A single message in an LLM conversation. */\nexport interface LLMMessage {\n role: \"user\" | \"assistant\";\n content: string;\n}\n\n/** A tool definition in Anthropic-style format (used as the canonical shape). */\nexport interface LLMTool {\n name: string;\n description: string;\n input_schema: Record<string, unknown>;\n}\n\n/** Provider-agnostic interface for LLM backends. */\nexport interface LLMProvider {\n complete(system: string, messages: LLMMessage[], maxTokens: number): Promise<string>;\n stream(\n system: string,\n messages: LLMMessage[],\n maxTokens: number,\n onToken?: (text: string) => void,\n ): Promise<string>;\n toolCall(\n system: string,\n messages: LLMMessage[],\n tools: LLMTool[],\n maxTokens: number,\n ): Promise<string>;\n /** Return a single embedding vector for the given text. */\n embed(text: string): Promise<number[]>;\n}\n\nconst SUPPORTED_PROVIDERS: ReadonlySet<string> = new Set([\"anthropic\", \"claude-agent\", \"openai\", \"ollama\", \"minimax\", \"copilot\"]);\n\n/**\n * Factory that returns the appropriate LLMProvider based on env vars.\n * Reads LLMWIKI_PROVIDER (default \"anthropic\") and LLMWIKI_MODEL\n * (defaults per provider from PROVIDER_MODELS).\n *\n * Direct process.env access is acceptable here as this is a system boundary.\n */\nexport function getProvider(): LLMProvider {\n const providerName = getProviderName();\n\n switch (providerName) {\n case \"anthropic\":\n return getAnthropicProvider();\n case \"claude-agent\":\n return getClaudeAgentProvider();\n case \"openai\":\n return new OpenAIProvider(getModelForProvider(\"openai\"), {\n baseURL: readOptionalEnv(\"OPENAI_BASE_URL\"),\n embeddingsBaseURL: readOptionalEnv(\"OPENAI_EMBEDDINGS_BASE_URL\"),\n embeddingModel: readOptionalEnv(\"LLMWIKI_EMBEDDING_MODEL\"),\n });\n case \"ollama\":\n return new OllamaProvider(getModelForProvider(\"ollama\"), {\n baseURL: readOptionalEnv(\"OLLAMA_HOST\") ?? OLLAMA_DEFAULT_HOST,\n embeddingsBaseURL: readOptionalEnv(\"OLLAMA_EMBEDDINGS_HOST\"),\n embeddingModel: readOptionalEnv(\"LLMWIKI_EMBEDDING_MODEL\"),\n });\n case \"minimax\":\n return getMiniMaxProvider();\n case \"copilot\":\n return getCopilotProvider();\n default:\n throw new Error(`Unhandled provider: ${providerName}`);\n }\n}\n\nfunction readOptionalEnv(name: string): string | undefined {\n const value = process.env[name]?.trim();\n return value ? value : undefined;\n}\n\nfunction getModelForProvider(providerName: \"openai\" | \"ollama\" | \"minimax\" | \"copilot\"): string {\n return process.env.LLMWIKI_MODEL ?? PROVIDER_MODELS[providerName];\n}\n\nfunction getMiniMaxProvider(): MiniMaxProvider {\n const apiKey = process.env.MINIMAX_API_KEY;\n if (!apiKey) {\n throw new Error(\n \"MiniMax provider requires MINIMAX_API_KEY environment variable.\\n\" +\n ' Set it with: export MINIMAX_API_KEY=your_key',\n );\n }\n return new MiniMaxProvider(getModelForProvider(\"minimax\"), apiKey);\n}\n\nfunction getCopilotProvider(): CopilotProvider {\n const apiKey = process.env.GITHUB_TOKEN;\n if (!apiKey) {\n throw new Error(\n \"GitHub Copilot provider requires GITHUB_TOKEN environment variable.\\n\" +\n \" Run: gh auth refresh --scopes copilot\\n\" +\n \" Then set it with: export GITHUB_TOKEN=$(gh auth token)\\n\" +\n \" The token must belong to a GitHub account with an active Copilot subscription.\",\n );\n }\n return new CopilotProvider(getModelForProvider(\"copilot\"), apiKey);\n}\n\nfunction getAnthropicProvider(): AnthropicProvider {\n const model = resolveAnthropicModelFromEnv() ?? PROVIDER_MODELS.anthropic;\n const baseURL = resolveAnthropicBaseURLFromEnv();\n const auth = resolveAnthropicAuthFromEnv();\n\n return new AnthropicProvider(model, {\n baseURL,\n ...auth,\n });\n}\n\n/**\n * Build the Claude Agent SDK provider. Auth is handled by the local Claude Code\n * login, so no API key is read here; LLMWIKI_MODEL (or the Claude settings\n * model) still overrides the default model.\n */\nfunction getClaudeAgentProvider(): ClaudeAgentProvider {\n const model = resolveAnthropicModelFromEnv() ?? PROVIDER_MODELS[\"claude-agent\"];\n return new ClaudeAgentProvider(model);\n}\n\nfunction getProviderName(): string {\n const providerName = process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER;\n if (!SUPPORTED_PROVIDERS.has(providerName)) {\n throw new Error(\n `Unknown provider \"${providerName}\". Supported: ${[...SUPPORTED_PROVIDERS].join(\", \")}`,\n );\n }\n return providerName;\n}\n\n/** Expose the resolved provider name for callers that need model lookup. */\nexport function getActiveProviderName(): string {\n return getProviderName();\n}\n\n/**\n * Resolve the model id the compile pipeline would call, without\n * instantiating a provider (which can require API credentials).\n *\n * Used by the export provenance stamp so a downstream auditor can tie a\n * compiled page back to the exact model that produced it. Mirrors the\n * per-provider model resolution in {@link getProvider} so the reported id\n * matches what an actual compile call would use.\n */\nexport function resolveActiveModelId(): string {\n const providerName = getProviderName();\n if (providerName === \"anthropic\") {\n return resolveAnthropicModelFromEnv() ?? PROVIDER_MODELS.anthropic;\n }\n return getModelForProvider(providerName as \"openai\" | \"ollama\" | \"minimax\" | \"copilot\");\n}\n","/**\n * Shared LLM helper with provider abstraction.\n *\n * Provides callClaude() for backward compatibility — delegates to the\n * active LLMProvider while preserving retry logic with exponential backoff.\n * The provider is selected via LLMWIKI_PROVIDER env var (see provider.ts).\n */\n\nimport { RETRY_COUNT, RETRY_BASE_MS, RETRY_MULTIPLIER } from \"./constants.js\";\nimport { getProvider } from \"./provider.js\";\nimport type { LLMMessage, LLMTool } from \"./provider.js\";\nimport { note } from \"./output.js\";\n\n/** Sleep for a given number of milliseconds. */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// Matches 4xx status codes at the start of an error message, excluding 429\n// (rate-limit), which is transient and worth retrying.\nconst NON_RETRIABLE_RE = /^4(?!29)\\d\\d\\b/;\n\n/** Return true for client errors that will never succeed on retry (e.g. 401, 403). */\nfunction isNonRetriable(error: unknown): boolean {\n const msg = error instanceof Error ? error.message : String(error);\n return NON_RETRIABLE_RE.test(msg);\n}\n\ninterface CallClaudeOptions {\n system: string;\n messages: LLMMessage[];\n tools?: LLMTool[];\n maxTokens?: number;\n stream?: boolean;\n onToken?: (text: string) => void;\n}\n\n/**\n * Call the active LLM provider with retry logic.\n * Supports streaming, tool-use, and basic completion modes.\n * Preserves the original callClaude interface for backward compatibility.\n */\nexport async function callClaude(options: CallClaudeOptions): Promise<string> {\n const { system, messages, tools, maxTokens = 4096, stream = false, onToken } = options;\n const provider = getProvider();\n\n for (let attempt = 0; attempt <= RETRY_COUNT; attempt++) {\n try {\n if (stream) {\n return await provider.stream(system, messages, maxTokens, onToken);\n }\n\n if (tools && tools.length > 0) {\n return await provider.toolCall(system, messages, tools, maxTokens);\n }\n\n return await provider.complete(system, messages, maxTokens);\n } catch (error) {\n if (attempt === RETRY_COUNT || isNonRetriable(error)) throw error;\n\n const delayMs = RETRY_BASE_MS * Math.pow(RETRY_MULTIPLIER, attempt);\n const errMsg = error instanceof Error ? error.message : String(error);\n note(`⚠ API call failed (attempt ${attempt + 1}/${RETRY_COUNT + 1}): ${errMsg}`);\n note(` Retrying in ${delayMs / 1000}s...`);\n await sleep(delayMs);\n }\n }\n\n throw new Error(\"Unreachable\");\n}\n","/**\n * PID-based lock file for preventing concurrent compilation.\n *\n * Fresh acquisition uses O_CREAT | O_EXCL (the 'wx' flag) for atomic lock\n * creation — the kernel guarantees only one process can create the file.\n *\n * Stale lock reclamation uses a two-lock protocol:\n * 1. Acquire a reclamation lock (.llmwiki/lock.reclaim) via 'wx' to serialize\n * all processes attempting to reclaim the same stale main lock.\n * 2. Re-verify the main lock is still stale (another reclaimer may have\n * already fixed it).\n * 3. unlink + tryCreateLock('wx') on the main lock — safe because we hold\n * exclusive reclamation access.\n * 4. Release the reclamation lock in a finally block.\n *\n * The reclamation lock itself can become stale if a process crashes during\n * the brief reclamation window. When that happens, acquireReclaimLock only\n * cleans up the stale file — it does NOT retry acquisition in the same call.\n * This eliminates the unlink-then-create race that would allow two processes\n * to both hold the reclaim lock. The outer retry loop in acquireLock handles\n * convergence: first pass cleans up the stale reclaim lock, second pass\n * acquires it cleanly via 'wx'.\n */\n\nimport { open, readFile, unlink, mkdir } from \"fs/promises\";\nimport path from \"path\";\nimport { LLMWIKI_DIR, LOCK_FILE } from \"./constants.js\";\nimport * as output from \"./output.js\";\n\nconst RECLAIM_SUFFIX = \".reclaim\";\nconst MAX_ACQUIRE_ATTEMPTS = 2;\n\n/** Check whether a process with the given PID is still running. */\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Acquire the compilation lock. Returns true if acquired, false if busy.\n *\n * Retries up to MAX_ACQUIRE_ATTEMPTS times to handle the case where the\n * first attempt cleans up a stale reclamation lock but cannot acquire it\n * in the same call (to avoid the double-winner race).\n */\nexport async function acquireLock(root: string): Promise<boolean> {\n const lockPath = path.join(root, LOCK_FILE);\n await mkdir(path.join(root, LLMWIKI_DIR), { recursive: true });\n\n for (let attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; attempt++) {\n // Try atomic create — fails if file already exists\n const created = await tryCreateLock(lockPath);\n if (created) return true;\n\n // Lock exists. Check if the holding process is dead.\n const stale = await isLockStale(lockPath);\n if (!stale) {\n output.status(\"!\", output.warn(\"Another compilation is running.\"));\n return false;\n }\n\n // Stale lock — serialize reclamation via a second lock.\n const reclaimed = await reclaimStaleLock(root, lockPath);\n if (reclaimed) return true;\n\n // Reclamation failed (e.g. cleaned up stale reclaim lock). Retry.\n }\n\n output.status(\"!\", output.warn(\"Could not acquire lock after retrying.\"));\n return false;\n}\n\n/**\n * Reclaim a stale main lock using a serialized two-lock protocol.\n *\n * Acquires .llmwiki/lock.reclaim (via 'wx') so that only one process performs\n * the unlink + recreate sequence at a time. Re-verifies staleness under\n * the reclamation lock in case another process already fixed it.\n * @param root - Project root directory.\n * @param lockPath - Absolute path to the main lock file.\n */\nasync function reclaimStaleLock(root: string, lockPath: string): Promise<boolean> {\n const reclaimPath = lockPath + RECLAIM_SUFFIX;\n\n const gotReclaimLock = await acquireReclaimLock(reclaimPath);\n if (!gotReclaimLock) return false;\n\n try {\n // Re-verify under exclusive reclamation access.\n // Another reclaimer may have already fixed the main lock.\n if (!(await isLockStale(lockPath))) {\n return false;\n }\n\n // Still stale. Safe to reclaim — we're the only reclaimer.\n try { await unlink(lockPath); } catch { /* already gone */ }\n\n const acquired = await tryCreateLock(lockPath);\n if (acquired) {\n output.status(\"i\", output.dim(\"Reclaimed stale lock from dead process.\"));\n }\n return acquired;\n } finally {\n try { await unlink(reclaimPath); } catch { /* cleanup best-effort */ }\n }\n}\n\n/**\n * Acquire the reclamation lock. Uses 'wx' for atomic creation.\n *\n * If the reclaim lock is stale (holder crashed during reclamation), this\n * function ONLY cleans up the stale file and returns false. It does NOT\n * retry acquisition in the same call. This is the key safety property:\n * unlink and create never happen in the same call, so two processes that\n * both see a stale reclaim lock will both clean up (harmless — second\n * unlink gets ENOENT) and both return false. Neither holds the reclaim\n * lock, so neither proceeds to touch the main lock. The outer retry loop\n * in acquireLock converges on the next attempt via a clean 'wx'.\n * @param reclaimPath - Absolute path to the reclamation lock file.\n */\nasync function acquireReclaimLock(reclaimPath: string): Promise<boolean> {\n if (await tryCreateLock(reclaimPath)) return true;\n\n // Reclaim lock exists. If its holder is alive, back off.\n if (!(await isLockStale(reclaimPath))) return false;\n\n // Stale reclaim lock — clean it up but do NOT retry in this call.\n // Retrying here would reintroduce the unlink+create race.\n try { await unlink(reclaimPath); } catch { /* already gone */ }\n return false;\n}\n\n/**\n * Atomically create the lock file with our PID.\n * Returns true if we created it, false if it already exists.\n */\nasync function tryCreateLock(lockPath: string): Promise<boolean> {\n try {\n const fd = await open(lockPath, \"wx\");\n await fd.writeFile(String(process.pid), \"utf-8\");\n await fd.close();\n return true;\n } catch (err: unknown) {\n if (err instanceof Error && \"code\" in err && (err as NodeJS.ErrnoException).code === \"EEXIST\") {\n return false;\n }\n throw err;\n }\n}\n\n/** Check if an existing lock is stale (holding process is dead). */\nasync function isLockStale(lockPath: string): Promise<boolean> {\n try {\n const content = await readFile(lockPath, \"utf-8\");\n const pid = parseInt(content.trim(), 10);\n if (isNaN(pid)) return true;\n return !isProcessAlive(pid);\n } catch {\n return true;\n }\n}\n\n/** Release the compilation lock. Safe to call even if lock doesn't exist. */\nexport async function releaseLock(root: string): Promise<void> {\n const lockPath = path.join(root, LOCK_FILE);\n try {\n await unlink(lockPath);\n } catch {\n // Lock already removed or never existed\n }\n}\n","/**\n * Output-language configuration for LLM-generated wiki content (issue #37).\n *\n * Resolves the user's chosen target language for compile and query\n * prompts. The CLI's `--lang <code>` flag and the `LLMWIKI_OUTPUT_LANG`\n * environment variable both write into the same env slot, so prompt\n * builders only need to read one source of truth.\n *\n * When unset, the resolver returns null — preserving the historical\n * behaviour where the LLM follows its own default (typically the\n * source-document language, often English).\n */\n\nconst LANG_ENV_VAR = \"LLMWIKI_OUTPUT_LANG\";\n\n/**\n * Read the configured output language. Returns null when the user has\n * not opted into a specific target language.\n */\nexport function getOutputLanguage(): string | null {\n const raw = process.env[LANG_ENV_VAR];\n if (!raw) return null;\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/**\n * Build the language-directive line to inject into a system prompt.\n * Returns an empty string when no language is configured, which lets\n * callers concatenate unconditionally without producing an extra blank\n * line in the default case.\n */\nexport function languageDirective(): string {\n const lang = getOutputLanguage();\n if (!lang) return \"\";\n return `Write the output in ${lang}.`;\n}\n\n/**\n * Apply a CLI `--lang <code>` value into the shared env slot so prompt\n * builders pick it up downstream. No-op when the caller did not pass the\n * flag. Trims whitespace so accidental padding does not leak into the\n * language directive.\n *\n * Lives in this module (not cli.ts) so every CLI verb plus the upcoming\n * quickstart command share one implementation and the resolution order\n * cannot drift between them.\n */\nexport function applyLanguageOption(lang: string | undefined): void {\n if (lang && lang.trim().length > 0) {\n process.env.LLMWIKI_OUTPUT_LANG = lang.trim();\n }\n}\n","/**\n * LLM prompt templates and tool schemas for the compilation pipeline.\n * Contains the Anthropic tool definition for concept extraction,\n * prompt builders for both extraction and page generation phases,\n * and a parser for the structured tool output.\n */\n\nimport type {\n ContradictionRef,\n ExtractedConcept,\n ProvenanceState,\n} from \"../utils/types.js\";\nimport type { PageKindRule, SeedPage } from \"../schema/index.js\";\nimport { languageDirective } from \"../utils/output-language.js\";\n\n/**\n * Build a list of optional prompt lines, omitting empty entries so the\n * default-case prompt is byte-identical to the previous version. Used by\n * the prompt builders to splice in the output-language directive only\n * when the user opted in.\n */\nfunction withLangLine(...lines: string[]): string[] {\n const lang = languageDirective();\n return lang ? [...lines, lang] : lines;\n}\n\n/**\n * Named version of the extraction + page-generation prompt contract.\n *\n * Bump this whenever the wording of the extraction tool schema, the\n * extraction system prompt, or the page-generation prompt changes in a way\n * that could alter compiled page content. The export provenance stamp\n * (`promptVersion` in the JSON export envelope) carries this value so a\n * downstream auditor can distinguish pages produced under different prompt\n * generations even when the model id is identical. Format is `vMAJOR`.\n */\nexport const PROMPT_VERSION = \"v1\";\n\n/** Allowed provenance state strings emitted by the LLM tool schema. */\nconst PROVENANCE_STATE_VALUES: ProvenanceState[] = [\n \"extracted\",\n \"merged\",\n \"inferred\",\n \"ambiguous\",\n];\n\n/**\n * Anthropic Tool definition for extracting knowledge concepts from a source.\n * Used with callClaude's tool_use mode to get structured concept data.\n */\nexport const CONCEPT_EXTRACTION_TOOL = {\n name: \"extract_concepts\",\n description: \"Extract knowledge concepts from a source document\",\n input_schema: {\n type: \"object\" as const,\n properties: {\n concepts: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n concept: {\n type: \"string\",\n description: \"Human-readable concept title\",\n },\n summary: {\n type: \"string\",\n description: \"One-line description\",\n },\n is_new: {\n type: \"boolean\",\n description: \"True if this is a new concept not in existing wiki\",\n },\n tags: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"2-4 categorical tags for organizing this concept (e.g., 'machine-learning', 'optimization')\",\n },\n confidence: {\n type: \"number\",\n description:\n \"Confidence in this concept on a 0..1 scale (1 = directly stated, 0 = highly speculative).\",\n },\n provenance_state: {\n type: \"string\",\n enum: PROVENANCE_STATE_VALUES,\n description:\n \"How this concept was produced: 'extracted' (direct from source), 'merged' (synthesised across sources), 'inferred' (model deduction), or 'ambiguous' (sources disagree).\",\n },\n contradicted_by: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n slug: { type: \"string\", description: \"Slug of the contradicting concept.\" },\n reason: { type: \"string\", description: \"Brief reason for the contradiction.\" },\n },\n required: [\"slug\"],\n },\n description: \"Slugs of other concepts whose evidence contradicts this one.\",\n },\n },\n required: [\"concept\", \"summary\", \"is_new\"],\n },\n },\n },\n required: [\"concepts\"],\n },\n};\n\n/**\n * Build the system prompt for the concept extraction phase.\n * Instructs the LLM to analyze a source document and identify distinct concepts.\n * @param sourceContent - The full text of the source document.\n * @param existingIndex - The current wiki index.md contents (may be empty).\n * @returns System prompt string for the extraction call.\n */\nexport function buildExtractionPrompt(\n sourceContent: string,\n existingIndex: string,\n): string {\n const indexSection = existingIndex\n ? `\\n\\nHere is the existing wiki index — avoid duplicating concepts already covered:\\n\\n${existingIndex}`\n : \"\\n\\nNo existing wiki pages yet.\";\n\n return [\n ...withLangLine(\n \"You are a knowledge extraction engine. Analyze the following source document\",\n \"and identify 3-8 distinct, meaningful concepts worth documenting as wiki pages.\",\n \"Each concept should be a standalone topic that someone might look up.\",\n \"Focus on key ideas, techniques, patterns, or entities — not trivial details.\",\n \"Use the extract_concepts tool to return your findings.\",\n ),\n \"\",\n \"For every concept, emit provenance metadata so downstream tools can reason\",\n \"about reliability:\",\n \" - confidence: 0..1 — how certain you are the source supports this concept.\",\n \" - provenance_state: 'extracted' if directly stated, 'merged' if synthesised\",\n \" from multiple parts of the source, 'inferred' if reasoned from context,\",\n \" or 'ambiguous' if the source is contradictory or unclear.\",\n \" - contradicted_by: slugs of other concepts (in this batch or the index)\",\n \" whose evidence conflicts with this one.\",\n indexSection,\n \"\\n\\n--- SOURCE DOCUMENT ---\\n\\n\",\n sourceContent,\n ].join(\"\\n\");\n}\n\n/**\n * Build the system prompt for wiki page generation.\n * Instructs the LLM to write a complete wiki page for a single concept.\n * @param concept - The concept title to write about.\n * @param sourceContent - The source material to draw from.\n * @param existingPage - The current page content if updating (empty for new pages).\n * @param relatedPages - Concatenated content of related wiki pages for context.\n * @returns System prompt string for the page generation call.\n */\nexport function buildPagePrompt(\n concept: string,\n sourceContent: string,\n existingPage: string,\n relatedPages: string,\n): string {\n const existingSection = existingPage\n ? `\\n\\nExisting page to update:\\n\\n${existingPage}`\n : \"\";\n\n const relatedSection = relatedPages\n ? `\\n\\nRelated wiki pages for cross-referencing:\\n\\n${relatedPages}`\n : \"\";\n\n return [\n ...withLangLine(\n `You are a wiki author. Write a clear, well-structured markdown page about \"${concept}\".`,\n \"Draw facts only from the provided source material.\",\n \"Include a ## Sources section at the end listing the source document.\",\n \"Suggest [[wikilinks]] to related concepts where appropriate.\",\n \"Write in a neutral, informative tone. Be concise but thorough.\",\n ),\n \"\",\n \"Source attribution: at the end of each prose paragraph, append a citation\",\n \"marker identifying which source file(s) and line range the paragraph drew from.\",\n \"PREFERRED format: ^[filename.md:START-END] where START and END are the line numbers\",\n \"shown in the numbered source content below (e.g. ' 42 | some text' → line 42).\",\n \"Use this whenever you can identify the specific numbered lines supporting the claim.\",\n \"Fallback format: ^[filename.md] when the claim draws from the source broadly and\",\n \"no specific line range applies. For multi-source paragraphs: ^[a.md:1-5, b.md:10-12].\",\n \"Place citations only at the end of prose paragraphs or sentences — not on\",\n \"headings, list items, or code blocks.\",\n \"Do not cite YAML frontmatter lines (the --- ... --- block at the top of a file) as\",\n \"source evidence for substantive claims — those lines are metadata, not content.\",\n \"If a claim relates to a metadata field (e.g. document date or author), leave it uncited.\",\n \"Source filenames are visible as `--- SOURCE: filename.md ---` headers in the content below.\",\n \"\",\n \"If a paragraph is your inference rather than a direct extraction, leave it\",\n \"uncited — downstream lint rules will count uncited paragraphs as 'inferred'\",\n \"so lint can surface excess-inferred-paragraphs warnings on review.\",\n existingSection,\n relatedSection,\n \"\\n\\n--- SOURCE MATERIAL ---\\n\\n\",\n sourceContent,\n ].join(\"\\n\");\n}\n\n/** Raw concept shape as it arrives from the tool JSON. */\ninterface RawConcept {\n concept: unknown;\n summary: unknown;\n is_new: unknown;\n tags?: unknown;\n confidence?: unknown;\n provenance_state?: unknown;\n contradicted_by?: unknown;\n}\n\n/** True if the raw concept has the required string/boolean fields. */\nfunction isValidRawConcept(c: RawConcept): boolean {\n return (\n typeof c.concept === \"string\" &&\n typeof c.summary === \"string\" &&\n typeof c.is_new === \"boolean\" &&\n (c.tags === undefined || Array.isArray(c.tags))\n );\n}\n\n/** Coerce raw contradiction entries from the tool into typed refs. */\nfunction coerceContradictedBy(raw: unknown): ContradictionRef[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const refs: ContradictionRef[] = [];\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\") continue;\n const obj = entry as { slug?: unknown; reason?: unknown };\n if (typeof obj.slug !== \"string\" || obj.slug.trim().length === 0) continue;\n const ref: ContradictionRef = { slug: obj.slug.trim() };\n if (typeof obj.reason === \"string\") ref.reason = obj.reason;\n refs.push(ref);\n }\n return refs.length > 0 ? refs : undefined;\n}\n\n/** Map a validated raw concept into an ExtractedConcept. */\nfunction mapRawConcept(c: RawConcept): ExtractedConcept {\n const provenance = typeof c.provenance_state === \"string\" &&\n PROVENANCE_STATE_VALUES.includes(c.provenance_state as ProvenanceState)\n ? (c.provenance_state as ProvenanceState)\n : undefined;\n return {\n concept: c.concept as string,\n summary: c.summary as string,\n is_new: c.is_new as boolean,\n tags: Array.isArray(c.tags) ? (c.tags as string[]) : undefined,\n confidence: typeof c.confidence === \"number\" ? c.confidence : undefined,\n provenanceState: provenance,\n contradictedBy: coerceContradictedBy(c.contradicted_by),\n };\n}\n\n/**\n * Build a system prompt for generating a seed page (overview / comparison /\n * entity) declared in the project's schema config. Seed pages weave together\n * material from related concept pages rather than from raw source files.\n * @param seed - Seed page definition pulled from the schema.\n * @param rule - Per-kind rule (used for the description and link minimum).\n * @param relatedPagesContent - Concatenated content of related concept pages.\n * @returns System prompt string for the page generation call.\n */\nexport function buildSeedPagePrompt(\n seed: SeedPage,\n rule: PageKindRule,\n relatedPagesContent: string,\n): string {\n const minLinks = rule.minWikilinks;\n const linkExpectation = minLinks > 0\n ? `Include at least ${minLinks} [[wikilinks]] to related pages.`\n : \"Use [[wikilinks]] when referencing other pages.\";\n return [\n ...withLangLine(\n `You are a wiki author. Write a ${seed.kind} page titled \"${seed.title}\".`,\n `Page-kind guidance: ${rule.description}`,\n `Summary line for context: ${seed.summary}`,\n \"Draw facts only from the related wiki pages provided below.\",\n linkExpectation,\n \"Write in a neutral, informative tone. Be concise but thorough.\",\n ),\n \"\\n\\n--- RELATED PAGES ---\\n\\n\",\n relatedPagesContent,\n ].join(\"\\n\");\n}\n\n/**\n * Parse the JSON tool output from concept extraction into typed objects.\n * @param toolOutput - Raw JSON string returned from the extract_concepts tool.\n * @returns Array of ExtractedConcept objects.\n */\nexport function parseConcepts(toolOutput: string): ExtractedConcept[] {\n try {\n const parsed = JSON.parse(toolOutput);\n const concepts: RawConcept[] = parsed.concepts ?? [];\n return concepts.filter(isValidRawConcept).map(mapRawConcept);\n } catch {\n return [];\n }\n}\n","/**\n * Type definitions for the wiki schema layer.\n *\n * The schema layer turns llmwiki from a flat compiler pipeline into a shaped\n * knowledge system. It declares the kinds of pages a project supports\n * (`concept`, `entity`, `comparison`, `overview`) and the cross-link\n * expectations that lint and review enforce per kind.\n *\n * Types live in their own module so that compile, lint, CLI, and tests can\n * depend on the schema vocabulary without pulling in YAML/JSON loaders.\n */\n\n/** All page kinds the schema layer recognises. */\nexport type PageKind = \"concept\" | \"entity\" | \"comparison\" | \"overview\";\n\n/** All recognised page kinds, exported for validation and CLI display. */\nexport const PAGE_KINDS: readonly PageKind[] = [\n \"concept\",\n \"entity\",\n \"comparison\",\n \"overview\",\n] as const;\n\n/** Per-kind policy: minimum cross-links and a human description used in prompts. */\nexport interface PageKindRule {\n /** Minimum number of [[wikilinks]] a page of this kind should contain. */\n minWikilinks: number;\n /** Short human-readable description; surfaced in prompts and review output. */\n description: string;\n}\n\n/** Optional declarative seed for non-concept pages the compiler can generate. */\nexport interface SeedPage {\n /** Display title; also used to derive the page slug. */\n title: string;\n /** Page kind — must be one of `PAGE_KINDS`. */\n kind: PageKind;\n /** One-line summary written into frontmatter. */\n summary: string;\n /**\n * For `overview` and `comparison` kinds, the slugs the page should weave\n * together. The compiler passes these to the LLM as the source material.\n */\n relatedSlugs?: string[];\n}\n\n/** Resolved schema config the rest of the compiler reads. */\nexport interface SchemaConfig {\n /** Schema format version. Currently always `1`. */\n version: 1;\n /** Kind assigned to pages that don't declare a kind in frontmatter. */\n defaultKind: PageKind;\n /** Per-kind rules keyed by `PageKind`. */\n kinds: Record<PageKind, PageKindRule>;\n /** Optional seed pages the compiler should materialise on each run. */\n seedPages: SeedPage[];\n /** Path the schema was loaded from, or `null` when defaults are used. */\n loadedFrom: string | null;\n}\n\n/** Raw schema file contents — every field is optional so partial files work. */\nexport interface PartialSchemaFile {\n version?: number;\n defaultKind?: string;\n kinds?: Partial<Record<string, Partial<PageKindRule>>>;\n seedPages?: Array<Partial<SeedPage>>;\n}\n","/**\n * Default schema constants.\n *\n * Projects without a schema file fall back to these defaults so the compiler\n * keeps working on day one. Every existing wiki — created before the schema\n * layer existed — is treated as a wiki of `concept` pages with no\n * cross-link minimums, preserving backward compatibility.\n */\n\nimport type { PageKind, PageKindRule, SchemaConfig } from \"./types.js\";\n\n/** Minimum cross-links per kind, chosen to match each kind's purpose. */\nconst DEFAULT_MIN_LINKS: Record<PageKind, number> = {\n concept: 0,\n entity: 1,\n comparison: 2,\n overview: 3,\n};\n\n/** Human-readable descriptions used in prompts and review output. */\nconst DEFAULT_DESCRIPTIONS: Record<PageKind, string> = {\n concept: \"A standalone idea, technique, or pattern worth documenting.\",\n entity: \"A specific thing — a person, product, organization, or named artifact.\",\n comparison: \"A side-by-side analysis weighing two or more concepts or entities.\",\n overview: \"A top-down map page that situates several concepts within a domain.\",\n};\n\n/** Build the default per-kind rule table. */\nfunction buildDefaultKindRules(): Record<PageKind, PageKindRule> {\n return {\n concept: { minWikilinks: DEFAULT_MIN_LINKS.concept, description: DEFAULT_DESCRIPTIONS.concept },\n entity: { minWikilinks: DEFAULT_MIN_LINKS.entity, description: DEFAULT_DESCRIPTIONS.entity },\n comparison: {\n minWikilinks: DEFAULT_MIN_LINKS.comparison,\n description: DEFAULT_DESCRIPTIONS.comparison,\n },\n overview: {\n minWikilinks: DEFAULT_MIN_LINKS.overview,\n description: DEFAULT_DESCRIPTIONS.overview,\n },\n };\n}\n\n/** The schema returned when no schema file exists. */\nexport function buildDefaultSchema(): SchemaConfig {\n return {\n version: 1,\n defaultKind: \"concept\",\n kinds: buildDefaultKindRules(),\n seedPages: [],\n loadedFrom: null,\n };\n}\n","/**\n * Schema config loader.\n *\n * Discovers a project's schema file from a fixed list of candidate paths,\n * parses it (JSON or YAML), and merges it onto the default schema. Missing\n * files are not an error — the compiler falls back to defaults so existing\n * projects continue to work without any migration.\n */\n\nimport { existsSync } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport path from \"path\";\nimport yaml from \"js-yaml\";\nimport type {\n PageKind,\n PageKindRule,\n PartialSchemaFile,\n SchemaConfig,\n SeedPage,\n} from \"./types.js\";\nimport { PAGE_KINDS } from \"./types.js\";\nimport { buildDefaultSchema } from \"./defaults.js\";\n\n/** Candidate schema file paths searched in priority order. */\nconst SCHEMA_CANDIDATE_PATHS = [\n \".llmwiki/schema.json\",\n \".llmwiki/schema.yaml\",\n \".llmwiki/schema.yml\",\n \"wiki/.schema.yaml\",\n \"wiki/.schema.yml\",\n];\n\n/** Find the first existing schema candidate path under `root`, or null. */\nfunction findSchemaPath(root: string): string | null {\n for (const candidate of SCHEMA_CANDIDATE_PATHS) {\n const absolute = path.join(root, candidate);\n if (existsSync(absolute)) return absolute;\n }\n return null;\n}\n\n/** Decide whether to parse the file as JSON or YAML based on its extension. */\nfunction parseSchemaFile(filePath: string, content: string): PartialSchemaFile {\n const isJson = filePath.endsWith(\".json\");\n const parsed = isJson ? JSON.parse(content) : yaml.load(content);\n if (parsed && typeof parsed === \"object\") return parsed as PartialSchemaFile;\n return {};\n}\n\n/** Type-guard checking whether a string is one of the supported page kinds. */\nfunction isPageKind(value: unknown): value is PageKind {\n return typeof value === \"string\" && (PAGE_KINDS as readonly string[]).includes(value);\n}\n\n/** Merge a single per-kind rule from the file onto the default rule. */\nfunction mergeKindRule(\n defaults: PageKindRule,\n override: Partial<PageKindRule> | undefined,\n): PageKindRule {\n if (!override) return defaults;\n const minWikilinks = typeof override.minWikilinks === \"number\"\n ? override.minWikilinks\n : defaults.minWikilinks;\n const description = typeof override.description === \"string\"\n ? override.description\n : defaults.description;\n return { minWikilinks, description };\n}\n\n/** Merge per-kind rule overrides onto the default rule table. */\nfunction mergeKinds(\n defaults: Record<PageKind, PageKindRule>,\n overrides: PartialSchemaFile[\"kinds\"],\n): Record<PageKind, PageKindRule> {\n const merged = { ...defaults };\n if (!overrides) return merged;\n\n for (const kind of PAGE_KINDS) {\n merged[kind] = mergeKindRule(defaults[kind], overrides[kind]);\n }\n return merged;\n}\n\n/** Validate and coerce a single seed page entry. Returns null when invalid. */\nfunction normalizeSeedPage(entry: Partial<SeedPage>): SeedPage | null {\n if (typeof entry.title !== \"string\" || entry.title.trim() === \"\") return null;\n if (!isPageKind(entry.kind)) return null;\n const summary = typeof entry.summary === \"string\" ? entry.summary : \"\";\n const relatedSlugs = Array.isArray(entry.relatedSlugs)\n ? entry.relatedSlugs.filter((slug): slug is string => typeof slug === \"string\")\n : undefined;\n return { title: entry.title, kind: entry.kind, summary, relatedSlugs };\n}\n\n/** Coerce raw seed page entries into validated SeedPage objects. */\nfunction normalizeSeedPages(entries: PartialSchemaFile[\"seedPages\"]): SeedPage[] {\n if (!Array.isArray(entries)) return [];\n return entries\n .map(normalizeSeedPage)\n .filter((entry): entry is SeedPage => entry !== null);\n}\n\n/** Apply a parsed partial-schema onto the defaults, returning the resolved config. */\nfunction applyOverrides(\n defaults: SchemaConfig,\n overrides: PartialSchemaFile,\n loadedFrom: string,\n): SchemaConfig {\n const defaultKind = isPageKind(overrides.defaultKind)\n ? overrides.defaultKind\n : defaults.defaultKind;\n return {\n version: 1,\n defaultKind,\n kinds: mergeKinds(defaults.kinds, overrides.kinds),\n seedPages: normalizeSeedPages(overrides.seedPages),\n loadedFrom,\n };\n}\n\n/**\n * Load the schema for `root`, falling back to defaults when no file is present.\n * Throws on parse failure so the user sees a clear error rather than a silent\n * default — silent fallback would mask real config bugs.\n * @param root - Project root directory.\n * @returns Resolved schema config.\n */\nexport async function loadSchema(root: string): Promise<SchemaConfig> {\n const defaults = buildDefaultSchema();\n const schemaPath = findSchemaPath(root);\n if (!schemaPath) return defaults;\n\n const raw = await readFile(schemaPath, \"utf-8\");\n const parsed = parseSchemaFile(schemaPath, raw);\n return applyOverrides(defaults, parsed, schemaPath);\n}\n\n/** Expose candidate paths so the CLI `schema init` command can pick one. */\nexport function defaultSchemaInitPath(root: string): string {\n return path.join(root, SCHEMA_CANDIDATE_PATHS[0]);\n}\n","/**\n * Schema helper utilities shared by compile, lint, and CLI.\n *\n * Kept separate from `loader.ts` so callers that just need to interpret a\n * page's kind or count its wikilinks don't pull the YAML/JSON parser into\n * their dependency graph.\n */\n\nimport yaml from \"js-yaml\";\nimport type { PageKind, SchemaConfig } from \"./types.js\";\nimport { PAGE_KINDS } from \"./types.js\";\n\n/** Pattern matching [[Wikilink Title]] references in markdown content. */\nconst WIKILINK_PATTERN = /\\[\\[([^\\]]+)\\]\\]/g;\n\n/**\n * Resolve a page's kind from its raw frontmatter value, falling back to the\n * schema default when no explicit kind is set or the value is invalid.\n * @param rawKind - Raw `kind` value pulled from frontmatter (untyped).\n * @param schema - Resolved schema config.\n * @returns The resolved page kind.\n */\nexport function resolvePageKind(rawKind: unknown, schema: SchemaConfig): PageKind {\n if (typeof rawKind === \"string\" && (PAGE_KINDS as readonly string[]).includes(rawKind)) {\n return rawKind as PageKind;\n }\n return schema.defaultKind;\n}\n\n/**\n * Count the [[wikilinks]] in a page body.\n * Pure function so the linter can apply per-kind minimums without re-parsing.\n * @param body - Markdown body text.\n * @returns Number of wikilink references found.\n */\nexport function countWikilinks(body: string): number {\n const matches = body.match(WIKILINK_PATTERN);\n return matches ? matches.length : 0;\n}\n\n/**\n * Serialise a schema config to YAML for `llmwiki schema init` to write to disk.\n * The `loadedFrom` field is omitted because it's a runtime-only annotation.\n * @param schema - Resolved schema config.\n * @returns YAML string suitable for writing to a schema file.\n */\nexport function serializeSchemaToYaml(schema: SchemaConfig): string {\n const serializable = {\n version: schema.version,\n defaultKind: schema.defaultKind,\n kinds: schema.kinds,\n seedPages: schema.seedPages,\n };\n return yaml.dump(serializable, { lineWidth: -1, quotingType: '\"' });\n}\n","/**\n * Semantic dependency tracking for cross-source concept sharing.\n *\n * When multiple source files contribute to the same concept, a change in one\n * source should trigger recompilation of that concept using content from ALL\n * contributing sources. This module builds a reverse index from concepts back\n * to their source files, then identifies which unchanged sources are affected\n * by changes to other sources that share concepts with them.\n *\n * Without this, if sources A and B both produce concept X and source A changes,\n * concept X would be regenerated using only source A's content — losing source\n * B's contribution entirely.\n */\n\nimport { readState, updateSourceState, writeState } from \"../utils/state.js\";\nimport { slugify } from \"../utils/markdown.js\";\nimport * as output from \"../utils/output.js\";\nimport type { WikiState, SourceChange, ExtractedConcept } from \"../utils/types.js\";\n\nexport interface ExtractionResult {\n sourceFile: string;\n sourcePath: string;\n sourceContent: string;\n concepts: ExtractedConcept[];\n}\n\n/**\n * Build a reverse map from concept slugs to the source files that produced them.\n * @param sources - The sources record from WikiState.\n * @returns Map where keys are concept slugs and values are arrays of source filenames.\n */\nfunction buildConceptToSourcesMap(\n sources: WikiState[\"sources\"],\n): Map<string, string[]> {\n const conceptMap = new Map<string, string[]>();\n\n for (const [sourceFile, entry] of Object.entries(sources)) {\n for (const slug of entry.concepts) {\n const existing = conceptMap.get(slug);\n if (existing) {\n existing.push(sourceFile);\n } else {\n conceptMap.set(slug, [sourceFile]);\n }\n }\n }\n\n return conceptMap;\n}\n\n/** Extract filenames from changes matching a given status. */\nfunction filesByStatus(\n changes: SourceChange[],\n ...statuses: SourceChange[\"status\"][]\n): Set<string> {\n const statusSet = new Set(statuses);\n return new Set(\n changes.filter((c) => statusSet.has(c.status)).map((c) => c.file),\n );\n}\n\n/**\n * Collect co-contributors for a source's concepts, skipping files in the\n * exclusion sets. Mutates `out` by adding newly discovered contributors.\n */\nfunction collectSharedContributors(\n sourceFile: string,\n state: WikiState,\n conceptMap: Map<string, string[]>,\n excludeSets: Set<string>[],\n out: Set<string>,\n): void {\n const sourceEntry = state.sources[sourceFile];\n if (!sourceEntry) return;\n\n for (const slug of sourceEntry.concepts) {\n const contributors = conceptMap.get(slug);\n if (!contributors || contributors.length < 2) continue;\n\n for (const contributor of contributors) {\n const isExcluded = excludeSets.some((s) => s.has(contributor));\n if (!isExcluded) out.add(contributor);\n }\n }\n}\n\n/**\n * Identify unchanged sources that need recompilation because they share\n * concepts with directly changed sources. This enables correct cross-source\n * concept regeneration — ensuring shared concepts are rebuilt with content\n * from ALL contributing sources.\n *\n * Deleted sources are intentionally excluded: recompiling a concept-mate of\n * a deleted source would regenerate the page from fewer sources, losing\n * content. Shared concepts from deleted sources are preserved as-is by\n * markOrphaned (which skips shared concepts).\n *\n * @param state - The current persisted WikiState.\n * @param directChanges - Changes detected by hash comparison.\n * @returns Filenames of indirectly affected sources not already in the changed list.\n */\nexport function findAffectedSources(\n state: WikiState,\n directChanges: SourceChange[],\n): string[] {\n const changedFiles = filesByStatus(directChanges, \"new\", \"changed\");\n const deletedFiles = filesByStatus(directChanges, \"deleted\");\n const conceptMap = buildConceptToSourcesMap(state.sources);\n const affected = new Set<string>();\n\n for (const changedFile of changedFiles) {\n collectSharedContributors(\n changedFile, state, conceptMap,\n [changedFiles, deletedFiles, affected],\n affected,\n );\n }\n\n return Array.from(affected);\n}\n\n/**\n * Find concept slugs that must NOT be regenerated during this compile batch.\n * A slug is \"frozen\" when it was shared between a deleted source and at least\n * one surviving source. Regenerating it would overwrite the existing page\n * (which has combined content from all prior contributors) with content from\n * only the surviving sources, silently losing the deleted source's contribution.\n * @param state - Current persisted state.\n * @param changes - All detected source changes in this batch.\n * @returns Set of concept slugs that compileSource should skip.\n */\nexport function findFrozenSlugs(\n state: WikiState,\n changes: SourceChange[],\n): Set<string> {\n // Start with persisted frozen slugs from prior batches.\n const frozen = new Set<string>(state.frozenSlugs ?? []);\n\n // Add new frozen slugs from deletions in this batch.\n const deletedFiles = changes\n .filter((c) => c.status === \"deleted\")\n .map((c) => c.file);\n\n const conceptMap = buildConceptToSourcesMap(state.sources);\n\n for (const file of deletedFiles) {\n const entry = state.sources[file];\n if (!entry) continue;\n\n for (const slug of entry.concepts) {\n const contributors = conceptMap.get(slug);\n if (contributors && contributors.length > 1) {\n frozen.add(slug);\n }\n }\n }\n\n return frozen;\n}\n\n/**\n * Unfreeze slugs that were successfully regenerated by all their current\n * contributors, then persist the remaining frozen set to state.\n * A slug is safe to unfreeze when every source that claims it in state\n * was compiled in this batch and successfully extracted it.\n */\nexport async function persistFrozenSlugs(\n root: string,\n frozenSlugs: Set<string>,\n successfulExtractions: ExtractionResult[],\n): Promise<void> {\n const currentState = await readState(root);\n const conceptMap = buildConceptToSourcesMap(currentState.sources);\n\n // Concepts successfully extracted in this batch, keyed by slug.\n const extractedBy = new Set<string>();\n for (const result of successfulExtractions) {\n if (result.concepts.length === 0) continue;\n for (const c of result.concepts) {\n extractedBy.add(slugify(c.concept));\n }\n }\n const compiledFiles = new Set(\n successfulExtractions\n .filter((r) => r.concepts.length > 0)\n .map((r) => r.sourceFile),\n );\n\n const remaining = new Set<string>();\n for (const slug of frozenSlugs) {\n const owners = conceptMap.get(slug) ?? [];\n // Unfreeze only if ALL current owners were compiled and extracted it.\n const allOwnersCompiled = owners.length > 0\n && owners.every((f) => compiledFiles.has(f))\n && extractedBy.has(slug);\n\n if (!allOwnersCompiled) remaining.add(slug);\n }\n\n const stateToSave = { ...currentState, frozenSlugs: Array.from(remaining) };\n await writeState(root, stateToSave);\n}\n\n/**\n * Collect concept slugs from extractions that were not in the source's\n * previous concept list — these are \"newly gained\" concepts that\n * findAffectedSources could not have matched pre-extraction.\n */\nfunction collectFreshSlugs(\n extractions: ExtractionResult[],\n state: WikiState,\n): Set<string> {\n const freshSlugs = new Set<string>();\n\n for (const result of extractions) {\n const oldConcepts = new Set(state.sources[result.sourceFile]?.concepts ?? []);\n for (const c of result.concepts) {\n const slug = slugify(c.concept);\n if (!oldConcepts.has(slug)) freshSlugs.add(slug);\n }\n }\n\n return freshSlugs;\n}\n\n/**\n * Find unchanged sources that own any of the given slugs, excluding files\n * present in the provided exclusion sets.\n */\nfunction findSlugOwners(\n slugs: Set<string>,\n conceptMap: Map<string, string[]>,\n excludeSets: Set<string>[],\n): string[] {\n const affected = new Set<string>();\n\n for (const slug of slugs) {\n const owners = conceptMap.get(slug);\n if (!owners) continue;\n for (const owner of owners) {\n const isExcluded = excludeSets.some((s) => s.has(owner));\n if (!isExcluded) affected.add(owner);\n }\n }\n\n return Array.from(affected);\n}\n\n/**\n * Post-extraction check for compiled sources whose freshly extracted concepts\n * overlap with unchanged sources not already in the batch. Covers two cases\n * that findAffectedSources (pre-extraction) cannot detect:\n * 1. New sources have no state entry, so their concepts are unknown.\n * 2. Changed sources may gain concepts they didn't previously have.\n * @param extractions - Results from Phase 1 extraction.\n * @param state - Current persisted state.\n * @param allChanges - Full changes array including deleted/unchanged entries.\n * @returns Filenames of unchanged sources that share concepts with compiled sources.\n */\nexport function findLateAffectedSources(\n extractions: ExtractionResult[],\n state: WikiState,\n allChanges: SourceChange[],\n): string[] {\n const compilingFiles = filesByStatus(allChanges, \"new\", \"changed\");\n const deletedFiles = filesByStatus(allChanges, \"deleted\");\n const conceptMap = buildConceptToSourcesMap(state.sources);\n const freshSlugs = collectFreshSlugs(extractions, state);\n\n return findSlugOwners(freshSlugs, conceptMap, [compilingFiles, deletedFiles]);\n}\n\n/**\n * Find concept slugs from a source that are also produced by other sources.\n * Used by markOrphaned to skip orphaning shared concepts when a source is\n * deleted — preserving combined content from prior compilations.\n * @param sourceFile - The source being checked.\n * @param state - Current persisted state.\n * @returns Set of slugs that have at least one other contributing source.\n */\nexport function findSharedConcepts(\n sourceFile: string,\n state: WikiState,\n): Set<string> {\n const shared = new Set<string>();\n const sourceEntry = state.sources[sourceFile];\n if (!sourceEntry) return shared;\n\n const conceptMap = buildConceptToSourcesMap(state.sources);\n\n for (const slug of sourceEntry.concepts) {\n const contributors = conceptMap.get(slug);\n if (contributors && contributors.length > 1) {\n shared.add(slug);\n }\n }\n\n return shared;\n}\n\n/**\n * Freeze concepts from failed extractions and persist their state with a\n * blank hash so they retry on the next compile. Preserves old concept lists\n * to keep dependency tracking intact.\n */\nexport async function freezeFailedExtractions(\n root: string,\n results: ExtractionResult[],\n frozenSlugs: Set<string>,\n): Promise<void> {\n for (const result of results) {\n if (result.concepts.length > 0) continue;\n\n output.status(\"!\", output.warn(`${result.sourceFile}: no concepts — will retry.`));\n const currentState = await readState(root);\n const oldConcepts = currentState.sources[result.sourceFile]?.concepts ?? [];\n for (const slug of oldConcepts) frozenSlugs.add(slug);\n\n await updateSourceState(root, result.sourceFile, {\n hash: \"\",\n concepts: oldConcepts,\n compiledAt: new Date().toISOString(),\n });\n }\n}\n","/**\n * Orphan management for deleted source files.\n *\n * When a source is deleted, its exclusively-owned concept pages are marked\n * orphaned (orphaned: true in frontmatter). Shared concepts are preserved\n * to avoid losing combined content from prior compilations.\n *\n * After compilation, frozen slugs (shared concepts that lost a contributor)\n * are checked against the updated state. Any that lost ALL owners are\n * orphaned as a cleanup pass.\n */\n\nimport path from \"path\";\nimport { readState, removeSourceState } from \"../utils/state.js\";\nimport {\n atomicWrite,\n safeReadFile,\n parseFrontmatter,\n} from \"../utils/markdown.js\";\nimport { findSharedConcepts } from \"./deps.js\";\nimport * as output from \"../utils/output.js\";\nimport { CONCEPTS_DIR } from \"../utils/constants.js\";\n\n/**\n * Mark wiki pages as orphaned when their source is deleted.\n * Only orphans concepts exclusively owned by the deleted source.\n * Shared concepts (contributed to by other live sources) are preserved\n * as-is to avoid losing combined content from prior compilations.\n */\nexport async function markOrphaned(\n root: string,\n sourceFile: string,\n state: Awaited<ReturnType<typeof readState>>,\n): Promise<void> {\n const sourceEntry = state.sources[sourceFile];\n if (!sourceEntry) return;\n\n const sharedSlugs = findSharedConcepts(sourceFile, state);\n\n for (const slug of sourceEntry.concepts) {\n if (sharedSlugs.has(slug)) {\n output.status(\"i\", output.dim(`Kept: ${slug}.md (shared with other sources)`));\n continue;\n }\n\n await orphanPage(root, slug, \"source deleted\");\n }\n\n await removeSourceState(root, sourceFile);\n}\n\n/**\n * Check frozen slugs against the updated state after compilation.\n * If no source still claims a frozen slug, orphan its page so it doesn't\n * linger as an untracked stale file.\n */\nexport async function orphanUnownedFrozenPages(\n root: string,\n frozenSlugs: Set<string>,\n): Promise<void> {\n const currentState = await readState(root);\n const ownedSlugs = new Set<string>();\n for (const entry of Object.values(currentState.sources)) {\n for (const slug of entry.concepts) ownedSlugs.add(slug);\n }\n\n for (const slug of frozenSlugs) {\n if (ownedSlugs.has(slug)) continue;\n await orphanPage(root, slug, \"no remaining sources\");\n }\n}\n\n/**\n * Mark a single concept page as orphaned if it exists and isn't already marked.\n * @param root - Project root directory.\n * @param slug - Concept slug to orphan.\n * @param reason - Human-readable reason for the log message.\n */\nasync function orphanPage(root: string, slug: string, reason: string): Promise<void> {\n const pagePath = path.join(root, CONCEPTS_DIR, `${slug}.md`);\n const content = await safeReadFile(pagePath);\n if (!content) return;\n\n const { meta } = parseFrontmatter(content);\n if (meta.orphaned === true) return;\n\n const updated = content.replace(\"---\\n\", \"---\\norphaned: true\\n\");\n await atomicWrite(pagePath, updated);\n output.status(\"⚠\", output.warn(`Orphaned: ${slug}.md (${reason})`));\n}\n","/**\n * Interlink resolution for wiki pages.\n *\n * Rule-based (not LLM-based) pass that scans wiki pages for concept title\n * mentions and wraps them in [[slug|Title]] wikilinks. The piped alias form\n * keeps Obsidian link resolution stable when a page's filename (slug) differs\n * from its display title.\n *\n * Complexity: O(changed * total) per incremental compile.\n * Full recompile degrades to O(total^2).\n */\n\nimport { readdir, readFile } from \"fs/promises\";\nimport path from \"path\";\nimport { existsSync } from \"fs\";\nimport { atomicWrite, parseFrontmatter } from \"../utils/markdown.js\";\nimport { CONCEPTS_DIR } from \"../utils/constants.js\";\nimport * as output from \"../utils/output.js\";\n\ninterface PageInfo {\n slug: string;\n title: string;\n filePath: string;\n}\n\n/** Build an index of all wiki page titles from the concepts directory. */\nasync function buildTitleIndex(root: string): Promise<PageInfo[]> {\n const conceptsDir = path.join(root, CONCEPTS_DIR);\n if (!existsSync(conceptsDir)) return [];\n\n const files = await readdir(conceptsDir);\n const pages: PageInfo[] = [];\n\n for (const file of files) {\n if (!file.endsWith(\".md\")) continue;\n\n const filePath = path.join(conceptsDir, file);\n const content = await readFile(filePath, \"utf-8\");\n const { meta } = parseFrontmatter(content);\n\n if (meta.title && typeof meta.title === \"string\" && !meta.orphaned) {\n pages.push({\n slug: file.replace(/\\.md$/, \"\"),\n title: meta.title,\n filePath,\n });\n }\n }\n\n return pages;\n}\n\n/** Check if a position is inside an existing [[wikilink]]. */\nfunction isInsideWikilink(text: string, position: number): boolean {\n const before = text.lastIndexOf(\"[[\", position);\n const after = text.indexOf(\"]]\", position);\n if (before === -1 || after === -1) return false;\n\n const closeBefore = text.indexOf(\"]]\", before);\n return closeBefore >= position;\n}\n\n/** Check if a position is inside a ^[...] citation marker. */\nfunction isInsideCitation(text: string, position: number): boolean {\n const before = text.lastIndexOf(\"^[\", position);\n const after = text.indexOf(\"]\", position);\n if (before === -1 || after === -1) return false;\n\n const closeBefore = text.indexOf(\"]\", before);\n return closeBefore >= position;\n}\n\n/** Check if a match is at a word boundary. */\nfunction isWordBoundary(text: string, start: number, end: number): boolean {\n const before = start === 0 || /[\\s,.:;!?()\\[\\]{}/\"']/.test(text[start - 1]);\n const after = end >= text.length || /[\\s,.:;!?()\\[\\]{}/\"']/.test(text[end]);\n return before && after;\n}\n\n/** Find all regex matches for a title in the text, returned as position spans. */\nfunction findTitleMatches(text: string, title: string): { start: number; end: number }[] {\n const escaped = title.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const regex = new RegExp(escaped, \"gi\");\n const matches: { start: number; end: number }[] = [];\n let match;\n\n while ((match = regex.exec(text)) !== null) {\n matches.push({ start: match.index, end: match.index + match[0].length });\n }\n\n return matches;\n}\n\n/** Determine whether a match position is eligible for wikilink insertion. */\nfunction isLinkablePosition(text: string, start: number, end: number): boolean {\n if (isInsideWikilink(text, start)) return false;\n if (isInsideCitation(text, start)) return false;\n return isWordBoundary(text, start, end);\n}\n\n/**\n * Add [[wikilinks]] to a page's body for any title mentions.\n * Skips already-linked text and non-word-boundary matches.\n */\nfunction addWikilinks(body: string, titles: PageInfo[], selfTitle: string): string {\n let result = body;\n const selfLower = selfTitle.toLowerCase();\n\n for (const page of titles) {\n if (page.title.toLowerCase() === selfLower) continue;\n\n const matches = findTitleMatches(result, page.title);\n\n // Process matches in reverse to preserve positions\n for (const m of matches.reverse()) {\n if (!isLinkablePosition(result, m.start, m.end)) continue;\n result = result.slice(0, m.start) + `[[${page.slug}|${page.title}]]` + result.slice(m.end);\n }\n }\n\n return result;\n}\n\n/**\n * Run interlink resolution on changed and affected pages.\n *\n * Two passes:\n * 1. Outbound: changed pages get [[wikilinks]] for any title they mention.\n * 2. Inbound: ALL pages get scanned for mentions of newly created titles.\n * This ensures existing pages link to new concepts without a full recompile.\n *\n * Complexity: O(changed * total) for outbound, O(newTitles * total) for inbound.\n */\nexport async function resolveLinks(\n root: string,\n changedSlugs: string[],\n newSlugs: string[],\n): Promise<number> {\n const titleIndex = await buildTitleIndex(root);\n if (titleIndex.length === 0) return 0;\n\n let linkCount = 0;\n\n // Pass 1: outbound links on changed pages\n linkCount += await resolveOutboundLinks(titleIndex, changedSlugs);\n\n // Pass 2: inbound links on all pages for new titles\n linkCount += await resolveInboundLinks(titleIndex, newSlugs);\n\n if (linkCount > 0) {\n output.status(\"🔗\", output.dim(`Resolved links in ${linkCount} page(s)`));\n }\n\n return linkCount;\n}\n\n/** Add outbound [[wikilinks]] to changed pages for any title they mention. */\nasync function resolveOutboundLinks(\n titleIndex: PageInfo[],\n changedSlugs: string[],\n): Promise<number> {\n let count = 0;\n\n for (const page of titleIndex) {\n if (!changedSlugs.includes(page.slug)) continue;\n const didLink = await linkPage(page, titleIndex);\n if (didLink) count++;\n }\n\n return count;\n}\n\n/** Scan ALL pages for mentions of newly created concept titles. */\nasync function resolveInboundLinks(\n titleIndex: PageInfo[],\n newSlugs: string[],\n): Promise<number> {\n if (newSlugs.length === 0) return 0;\n\n const newTitles = titleIndex.filter((p) => newSlugs.includes(p.slug));\n if (newTitles.length === 0) return 0;\n\n let count = 0;\n\n for (const page of titleIndex) {\n // Skip pages that were already processed in outbound pass\n if (newSlugs.includes(page.slug)) continue;\n\n const content = await readFile(page.filePath, \"utf-8\");\n const { body } = parseFrontmatter(content);\n const linked = addWikilinks(body, newTitles, page.title);\n\n if (linked !== body) {\n const newContent = content.replace(body, linked);\n await atomicWrite(page.filePath, newContent);\n count++;\n }\n }\n\n return count;\n}\n\n/** Add wikilinks to a single page, writing atomically if changed. */\nasync function linkPage(page: PageInfo, titleIndex: PageInfo[]): Promise<boolean> {\n const content = await readFile(page.filePath, \"utf-8\");\n const { body } = parseFrontmatter(content);\n const linked = addWikilinks(body, titleIndex, page.title);\n\n if (linked === body) return false;\n\n const newContent = content.replace(body, linked);\n await atomicWrite(page.filePath, newContent);\n return true;\n}\n","/**\n * Wiki index generator.\n *\n * Scans all concept pages in wiki/concepts/, extracts frontmatter metadata,\n * and produces wiki/index.md with a sorted list of all concepts and their\n * summaries. Used after each compilation pass.\n */\n\nimport { readdir } from \"fs/promises\";\nimport path from \"path\";\nimport { atomicWrite, safeReadFile, parseFrontmatter } from \"../utils/markdown.js\";\nimport { CONCEPTS_DIR, QUERIES_DIR, INDEX_FILE } from \"../utils/constants.js\";\nimport * as output from \"../utils/output.js\";\nimport type { PageSummary } from \"../utils/types.js\";\n\n/**\n * Generate the wiki/index.md listing all concept pages with summaries.\n * @param root - Project root directory.\n */\nexport async function generateIndex(root: string): Promise<void> {\n output.status(\"*\", output.info(\"Generating index...\"));\n\n const conceptsPath = path.join(root, CONCEPTS_DIR);\n const queriesPath = path.join(root, QUERIES_DIR);\n const concepts = await collectPageSummaries(conceptsPath);\n const queries = await collectPageSummaries(queriesPath);\n\n concepts.sort((a, b) => a.title.localeCompare(b.title));\n queries.sort((a, b) => a.title.localeCompare(b.title));\n\n const indexContent = buildIndexContent(concepts, queries);\n const indexPath = path.join(root, INDEX_FILE);\n await atomicWrite(indexPath, indexContent);\n\n const total = concepts.length + queries.length;\n output.status(\"+\", output.success(`Index updated with ${total} pages.`));\n}\n\n/** A scanned page paired with its parsed frontmatter. */\ninterface ScannedPage {\n slug: string;\n meta: Record<string, unknown>;\n}\n\n/**\n * Scan a wiki directory and return every .md page paired with its parsed\n * frontmatter. Read-only utility shared by index generation and the MCP\n * server's status tool.\n * @param dirPath - Absolute path to a wiki page directory.\n * @returns Array of {slug, meta} entries — empty when the directory is missing.\n */\nexport async function scanWikiPages(dirPath: string): Promise<ScannedPage[]> {\n let files: string[];\n try {\n files = await readdir(dirPath);\n } catch {\n return [];\n }\n\n const scanned: ScannedPage[] = [];\n for (const file of files.filter((f) => f.endsWith(\".md\"))) {\n const content = await safeReadFile(path.join(dirPath, file));\n const { meta } = parseFrontmatter(content);\n scanned.push({ slug: file.replace(/\\.md$/, \"\"), meta });\n }\n return scanned;\n}\n\n/**\n * Project a wiki directory into PageSummary entries (excludes orphaned and\n * untitled pages). Built on top of scanWikiPages so the MCP server can share\n * the underlying scan logic without re-reading the directory.\n * @param conceptsPath - Absolute path to wiki/concepts/.\n * @returns Array of page summary objects.\n */\nexport async function collectPageSummaries(\n conceptsPath: string,\n): Promise<PageSummary[]> {\n const scanned = await scanWikiPages(conceptsPath);\n return scanned\n .filter(({ meta }) => meta.title && typeof meta.title === \"string\" && !meta.orphaned)\n .map(({ slug, meta }) => ({\n title: meta.title as string,\n slug,\n summary: typeof meta.summary === \"string\" ? meta.summary : \"\",\n }));\n}\n\n/** Strip [[wikilink]] brackets from text, leaving the inner text intact. */\nfunction stripWikilinks(text: string): string {\n return text.replace(/\\[\\[([^\\]]+)\\]\\]/g, \"$1\");\n}\n\n/**\n * Build the index.md markdown content from page summaries.\n * @param pages - Sorted array of page summaries.\n * @returns Full index.md content string.\n */\nfunction buildIndexContent(concepts: PageSummary[], queries: PageSummary[]): string {\n const lines = [\"# Knowledge Wiki\", \"\", \"## Concepts\", \"\"];\n\n for (const page of concepts) {\n lines.push(`- **[[${page.slug}|${page.title}]]** — ${stripWikilinks(page.summary)}`);\n }\n\n if (queries.length > 0) {\n lines.push(\"\", \"## Saved Queries\", \"\");\n for (const page of queries) {\n lines.push(`- **[[${page.slug}|${page.title}]]** — ${stripWikilinks(page.summary)}`);\n }\n }\n\n const total = concepts.length + queries.length;\n lines.push(\"\");\n lines.push(`_${total} pages | Generated ${new Date().toISOString()}_`);\n lines.push(\"\");\n\n return lines.join(\"\\n\");\n}\n","/**\n * Per-concept prompt-budget enforcement (issue #39).\n *\n * When the same concept is extracted from many overlapping sources, the\n * page-generation prompt would otherwise concatenate every full source\n * — linear in source count — and reliably blow past the LLM provider's\n * context window. This module clips each contributing source's slice to\n * a fair share of a configurable total budget and emits a single warning\n * when truncation kicks in.\n *\n * The fix is deliberately defensive (proportional truncation) rather than\n * smart (semantic ranking / summarisation). It prevents crashes while a\n * deeper retrieval-driven solution is designed.\n */\n\nimport * as output from \"../utils/output.js\";\nimport {\n DEFAULT_PROMPT_BUDGET_CHARS,\n PROMPT_BUDGET_ENV_VAR,\n} from \"../utils/constants.js\";\n\n/** Marker appended to a source slice when it was truncated to fit the budget. */\nconst TRUNCATION_MARKER = \"\\n\\n[…truncated for prompt budget — see #39…]\";\n\n/** A single source's contribution to the combined per-concept content. */\nexport interface SourceSlice {\n /** Source filename (e.g. \"ml-paper.md\") shown as a section header in the prompt. */\n file: string;\n /** Raw extracted source content, before any budgeting. */\n content: string;\n}\n\n/**\n * Resolve the active prompt-budget character cap. Reads the\n * `LLMWIKI_PROMPT_BUDGET_CHARS` env var when present and parseable; falls\n * back to `DEFAULT_PROMPT_BUDGET_CHARS`. Invalid values (non-numeric or\n * <= 0) are ignored so a typo can't accidentally truncate every prompt\n * to nothing.\n */\nexport function resolvePromptBudgetChars(): number {\n const raw = process.env[PROMPT_BUDGET_ENV_VAR];\n if (!raw) return DEFAULT_PROMPT_BUDGET_CHARS;\n const parsed = Number.parseInt(raw, 10);\n if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_PROMPT_BUDGET_CHARS;\n return parsed;\n}\n\n/**\n * Combine per-source slices into the single content blob the LLM prompt\n * receives, applying a fair-share budget when the raw total would exceed\n * the configured ceiling. When no truncation is needed the output is\n * byte-identical to the previous unbudgeted concatenation, so existing\n * compile output is unchanged for typical workloads.\n *\n * @param concept - Human-readable concept title (used in the warning only).\n * @param slices - One entry per contributing source, in arrival order.\n * @returns The combined content string suitable for buildPagePrompt.\n */\nexport function buildBudgetedCombinedContent(\n concept: string,\n slices: SourceSlice[],\n): string {\n const budget = resolvePromptBudgetChars();\n const totalRaw = slices.reduce((sum, s) => sum + s.content.length, 0);\n\n if (totalRaw <= budget) {\n return formatSlices(slices);\n }\n\n const perSource = Math.max(1, Math.floor(budget / slices.length));\n warnTruncation(concept, totalRaw, slices.length, perSource, budget);\n\n const trimmed = slices.map((s) =>\n s.content.length > perSource\n ? { ...s, content: s.content.slice(0, perSource) + TRUNCATION_MARKER }\n : s,\n );\n return formatSlices(trimmed);\n}\n\n/**\n * Prepend right-aligned 1-indexed line numbers to each line of source content.\n * Gives the LLM explicit anchors so its ^[file.md:N-M] citations are accurate.\n */\nfunction numberLines(content: string): string {\n const lines = content.split(\"\\n\");\n const width = String(lines.length).length;\n return lines\n .map((line, i) => `${String(i + 1).padStart(width)} | ${line}`)\n .join(\"\\n\");\n}\n\n/**\n * Clip a single source's content to the active prompt budget and prepend line\n * numbers, so a prompt that asks the model for line spans actually shows the\n * model numbered lines (and never exceeds the budget). Used by the rule\n * extractor, which feeds one source per call rather than a merged concept.\n *\n * @param file - Source filename, for the truncation warning only.\n * @param content - Raw source content.\n * @returns Numbered (and, when over budget, truncated) content.\n */\nexport function budgetAndNumberSource(file: string, content: string): string {\n const budget = resolvePromptBudgetChars();\n if (content.length <= budget) {\n return numberLines(content);\n }\n warnTruncation(file, content.length, 1, budget, budget);\n return numberLines(content.slice(0, budget) + TRUNCATION_MARKER);\n}\n\n/** Render the slice list using the same `--- SOURCE: ---` headers the LLM is taught to read. */\nfunction formatSlices(slices: SourceSlice[]): string {\n return slices\n .map((s) => `--- SOURCE: ${s.file} ---\\n\\n${numberLines(s.content)}`)\n .join(\"\\n\\n\");\n}\n\n/** Emit a single, actionable warning when the budget kicks in for a concept. */\nfunction warnTruncation(\n concept: string,\n totalRaw: number,\n sourceCount: number,\n perSource: number,\n budget: number,\n): void {\n output.status(\n \"!\",\n output.warn(\n `Combined source content for \"${concept}\" (${totalRaw.toLocaleString()} chars across ` +\n `${sourceCount} sources) exceeds the ${budget.toLocaleString()}-char prompt budget; ` +\n `truncating each source to ~${perSource.toLocaleString()} chars. ` +\n `Raise via ${PROMPT_BUDGET_ENV_VAR} when running against larger-context models.`,\n ),\n );\n}\n","/**\n * Obsidian integration helpers for the llmwiki knowledge compiler.\n *\n * Provides two capabilities:\n * 1. Enriching wiki page frontmatter with tags and aliases for better\n * Obsidian graph navigation and search.\n * 2. Generating a Map of Content (MOC) page that groups concept pages\n * by tag for easy browsing.\n */\n\nimport { readdir } from \"fs/promises\";\nimport path from \"path\";\nimport { slugify, atomicWrite, safeReadFile, parseFrontmatter } from \"../utils/markdown.js\";\nimport { CONCEPTS_DIR, MOC_FILE } from \"../utils/constants.js\";\n\n/** Minimum word count to generate an abbreviation alias. */\nconst ABBREVIATION_MIN_WORDS = 3;\n\n/** Conjunctions that trigger a word-swap alias. */\nconst SWAP_CONJUNCTIONS = [\" and \", \" or \"];\n\n/**\n * Enrich a frontmatter object with Obsidian-specific tags and aliases.\n * Mutates the frontmatter object in place.\n * @param frontmatter - The frontmatter object to enrich.\n * @param conceptTitle - The human-readable concept title.\n * @param tags - Tags from extraction (may be empty).\n */\nexport function addObsidianMeta(\n frontmatter: Record<string, unknown>,\n conceptTitle: string,\n tags: string[],\n): void {\n frontmatter.tags = tags;\n frontmatter.aliases = generateAliases(conceptTitle);\n}\n\n/**\n * Generate deterministic aliases from a concept title.\n * Produces up to three alias variants:\n * - Slug form (e.g., \"gradient-descent\")\n * - Word-swap around conjunctions (e.g., \"Optimization and Gradient Descent\")\n * - Abbreviation from first letters for 3+ word titles (e.g., \"RAG\")\n * @param title - The concept title to derive aliases from.\n * @returns Array of aliases that differ from the original title.\n */\nfunction generateAliases(title: string): string[] {\n const aliases: string[] = [];\n const slug = slugify(title);\n\n if (slug !== title) {\n aliases.push(slug);\n }\n\n const swapAlias = generateSwapAlias(title);\n if (swapAlias) {\n aliases.push(swapAlias);\n }\n\n const abbreviation = generateAbbreviation(title);\n if (abbreviation) {\n aliases.push(abbreviation);\n }\n\n return aliases;\n}\n\n/**\n * Generate a word-swap alias by reversing parts around a conjunction.\n * E.g., \"Gradient Descent and Optimization\" becomes \"Optimization and Gradient Descent\".\n * @param title - The concept title.\n * @returns The swapped alias, or null if no conjunction found.\n */\nfunction generateSwapAlias(title: string): string | null {\n for (const conjunction of SWAP_CONJUNCTIONS) {\n const index = title.toLowerCase().indexOf(conjunction);\n if (index === -1) continue;\n\n const before = title.slice(0, index);\n const after = title.slice(index + conjunction.length);\n const originalConjunction = title.slice(index, index + conjunction.length);\n return `${after}${originalConjunction}${before}`;\n }\n return null;\n}\n\n/**\n * Generate an abbreviation from first letters of each word for titles with 3+ words.\n * E.g., \"Retrieval Augmented Generation\" becomes \"RAG\".\n * @param title - The concept title.\n * @returns The abbreviation, or null if title has fewer than 3 words.\n */\nfunction generateAbbreviation(title: string): string | null {\n const words = title.split(/\\s+/);\n if (words.length < ABBREVIATION_MIN_WORDS) return null;\n\n const abbreviation = words.map((w) => w[0].toUpperCase()).join(\"\");\n if (abbreviation === title) return null;\n\n return abbreviation;\n}\n\n/**\n * Generate a Map of Content (MOC) page grouping concept pages by tag.\n * Reads all concept pages, extracts their tags from frontmatter, and writes\n * a structured MOC.md with sections per tag and an Uncategorized section.\n * @param root - Project root directory.\n */\nexport async function generateMOC(root: string): Promise<void> {\n const conceptsPath = path.join(root, CONCEPTS_DIR);\n const pages = await loadConceptPages(conceptsPath);\n\n const tagGroups = groupPagesByTag(pages);\n const content = buildMOCContent(tagGroups);\n\n await atomicWrite(path.join(root, MOC_FILE), content);\n}\n\n/** Minimal page info needed for MOC generation. */\ninterface PageInfo {\n slug: string;\n title: string;\n tags: string[];\n}\n\n/**\n * Load all concept pages and extract their title and tags.\n * @param conceptsPath - Absolute path to the concepts directory.\n * @returns Array of page info objects.\n */\nasync function loadConceptPages(conceptsPath: string): Promise<PageInfo[]> {\n let files: string[];\n try {\n files = await readdir(conceptsPath);\n } catch {\n return [];\n }\n\n const pages: PageInfo[] = [];\n for (const file of files) {\n if (!file.endsWith(\".md\")) continue;\n\n const content = await safeReadFile(path.join(conceptsPath, file));\n if (!content) continue;\n\n const { meta } = parseFrontmatter(content);\n if (meta.orphaned) continue;\n\n const slug = file.replace(/\\.md$/, \"\");\n const title = typeof meta.title === \"string\" ? meta.title : slug;\n const tags = Array.isArray(meta.tags) ? (meta.tags as string[]) : [];\n pages.push({ slug, title, tags });\n }\n\n return pages;\n}\n\n/**\n * Group pages by their tags into a map. Pages with no tags go under \"Uncategorized\".\n * @param pages - Array of page info objects.\n * @returns Map of tag name to array of page titles.\n */\nfunction groupPagesByTag(pages: PageInfo[]): Map<string, PageInfo[]> {\n const groups = new Map<string, PageInfo[]>();\n\n for (const page of pages) {\n if (page.tags.length === 0) {\n appendToGroup(groups, \"Uncategorized\", page);\n continue;\n }\n\n for (const tag of page.tags) {\n appendToGroup(groups, tag, page);\n }\n }\n\n return groups;\n}\n\n/** Append a page to a group, creating the group if needed. */\nfunction appendToGroup(groups: Map<string, PageInfo[]>, key: string, page: PageInfo): void {\n const existing = groups.get(key);\n if (existing) {\n existing.push(page);\n } else {\n groups.set(key, [page]);\n }\n}\n\n/**\n * Build the MOC markdown content from grouped pages.\n * @param tagGroups - Map of tag name to array of page titles.\n * @returns Complete MOC markdown string.\n */\nfunction buildMOCContent(tagGroups: Map<string, PageInfo[]>): string {\n const lines: string[] = [\"# Map of Content\", \"\"];\n\n const sortedTags = [...tagGroups.keys()].sort((a, b) => {\n // \"Uncategorized\" always goes last\n if (a === \"Uncategorized\") return 1;\n if (b === \"Uncategorized\") return -1;\n return a.localeCompare(b);\n });\n\n for (const tag of sortedTags) {\n const pages = tagGroups.get(tag) ?? [];\n lines.push(`## ${tag}`, \"\");\n for (const page of pages.sort((a, b) => a.title.localeCompare(b.title))) {\n lines.push(`- [[${page.slug}|${page.title}]]`);\n }\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Helpers for surfacing provenance metadata during compilation.\n *\n * Keeps the compile orchestrator small by isolating the logic that copies\n * confidence/contradiction signals from extracted concepts onto wiki page\n * frontmatter and emits compile-time warnings when contradictions are\n * reported.\n */\n\nimport * as output from \"../utils/output.js\";\nimport { resolveActiveModelId } from \"../utils/provider.js\";\nimport { PROMPT_VERSION } from \"./prompts.js\";\nimport type { ExtractedConcept } from \"../utils/types.js\";\n\n/**\n * Stamp compile-time lineage onto a page's frontmatter: the model id that the\n * active provider would use and the named prompt-contract version. Written when\n * the page is (re)generated, so it records the model/prompt that actually\n * produced the page's current content — unlike an export-time env read, which\n * can attribute a page to a model that never touched it. Surfaced per-page in\n * the JSON export (`ExportPage.modelId` / `promptVersion`).\n * @param fields - Mutable frontmatter record being assembled for a page.\n */\nexport function addModelProvenanceMeta(fields: Record<string, unknown>): void {\n fields.modelId = resolveActiveModelId();\n fields.promptVersion = PROMPT_VERSION;\n}\n\n/**\n * Copy provenance metadata fields from an extracted concept onto the\n * frontmatter record, omitting fields the LLM did not provide so existing\n * pages without these fields stay clean.\n * @param fields - Mutable frontmatter record being assembled for a page.\n * @param concept - Source concept whose provenance metadata to apply.\n */\nexport function addProvenanceMeta(\n fields: Record<string, unknown>,\n concept: ExtractedConcept,\n): void {\n if (typeof concept.confidence === \"number\") {\n fields.confidence = concept.confidence;\n }\n if (concept.provenanceState) {\n fields.provenanceState = concept.provenanceState;\n }\n if (concept.contradictedBy && concept.contradictedBy.length > 0) {\n fields.contradictedBy = concept.contradictedBy;\n }\n}\n\n/**\n * Print a compile-time warning when a concept reports contradictions with\n * other pages. Returns silently when there is nothing to report.\n * @param conceptTitle - Human-readable title of the concept being compiled.\n * @param concept - The extracted concept whose contradictions to surface.\n */\nexport function reportContradictionWarnings(\n conceptTitle: string,\n concept: ExtractedConcept,\n): void {\n const refs = concept.contradictedBy;\n if (!refs || refs.length === 0) return;\n const slugs = refs.map((r) => r.slug).join(\", \");\n output.status(\n \"!\",\n output.warn(`Contradiction reported on \"${conceptTitle}\" — conflicts with: ${slugs}`),\n );\n}\n","/**\n * Embedding-based semantic search utilities.\n *\n * Maintains a persistent store of page and chunk embeddings in\n * .llmwiki/embeddings.json and provides cosine-similarity retrieval so the\n * query command can narrow hundreds of pages down to a small top-K before\n * calling the selection LLM.\n *\n * The store is additive: successful embedding calls update entries; failures\n * degrade gracefully (caller falls back to full-index selection).\n *\n * The store has two on-disk versions:\n * - v1: page-level entries only (legacy; still readable).\n * - v2: page-level entries plus optional chunk-level entries that enable\n * paragraph-precision retrieval, content-hash-aware incremental updates,\n * and reranking before final page selection.\n */\n\nimport { readFile, readdir } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { getProvider, getActiveProviderName } from \"./provider.js\";\nimport { atomicWrite, safeReadFile, parseFrontmatter } from \"./markdown.js\";\nimport {\n CONCEPTS_DIR,\n QUERIES_DIR,\n EMBEDDINGS_FILE,\n EMBEDDING_TOP_K,\n EMBEDDING_MODELS,\n} from \"./constants.js\";\nimport { hashChunkText, splitIntoChunks } from \"./retrieval.js\";\nimport * as output from \"./output.js\";\n\n/** Current store version; bumped from 1 → 2 when chunk entries were added. */\nconst STORE_VERSION = 2 as const;\n\n/** A single embedded page record. */\nexport interface EmbeddingEntry {\n slug: string;\n title: string;\n summary: string;\n vector: number[];\n updatedAt: string;\n}\n\n/** A single embedded chunk drawn from a page body. */\nexport interface ChunkEmbeddingEntry {\n slug: string;\n title: string;\n chunkIndex: number;\n contentHash: string;\n text: string;\n vector: number[];\n updatedAt: string;\n}\n\n/** Root shape of .llmwiki/embeddings.json. */\nexport interface EmbeddingStore {\n version: 1 | 2;\n model: string;\n dimensions: number;\n entries: EmbeddingEntry[];\n /** Optional in v2 stores; absent in v1 stores. */\n chunks?: ChunkEmbeddingEntry[];\n}\n\n/** A retrievable page record on disk (concepts/ or queries/). */\ninterface PageRecord {\n slug: string;\n title: string;\n summary: string;\n body: string;\n}\n\n/**\n * Cosine similarity between two equal-length vectors.\n * Returns 0 when either vector has zero magnitude (safer than NaN for ranking).\n */\nexport function cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length || a.length === 0) return 0;\n\n let dot = 0;\n let magA = 0;\n let magB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n magA += a[i] * a[i];\n magB += b[i] * b[i];\n }\n\n if (magA === 0 || magB === 0) return 0;\n return dot / (Math.sqrt(magA) * Math.sqrt(magB));\n}\n\n/** Return the top-K entries most similar to the query vector, sorted descending. */\nexport function findTopK(\n queryVec: number[],\n store: EmbeddingStore,\n k: number,\n): EmbeddingEntry[] {\n const scored = store.entries.map((entry) => ({\n entry,\n score: cosineSimilarity(queryVec, entry.vector),\n }));\n scored.sort((left, right) => right.score - left.score);\n return scored.slice(0, k).map((item) => item.entry);\n}\n\n/** Score and sort chunk entries by cosine similarity, returning the top-K. */\nexport function findTopKChunks(\n queryVec: number[],\n chunks: ChunkEmbeddingEntry[],\n k: number,\n): Array<{ chunk: ChunkEmbeddingEntry; score: number }> {\n const scored = chunks.map((chunk) => ({\n chunk,\n score: cosineSimilarity(queryVec, chunk.vector),\n }));\n scored.sort((left, right) => right.score - left.score);\n return scored.slice(0, k);\n}\n\n/** Read .llmwiki/embeddings.json, returning null if it does not exist. */\nexport async function readEmbeddingStore(root: string): Promise<EmbeddingStore | null> {\n const filePath = path.join(root, EMBEDDINGS_FILE);\n if (!existsSync(filePath)) return null;\n const raw = await readFile(filePath, \"utf-8\");\n return JSON.parse(raw) as EmbeddingStore;\n}\n\n/** Atomically persist the embedding store. */\nexport async function writeEmbeddingStore(root: string, store: EmbeddingStore): Promise<void> {\n const filePath = path.join(root, EMBEDDINGS_FILE);\n await atomicWrite(filePath, JSON.stringify(store, null, 2));\n}\n\n/**\n * Embed the question, look up top-K matches, and return lightweight page records.\n * Returns [] when no store exists so callers can transparently fall back.\n */\nexport async function findRelevantPages(\n root: string,\n question: string,\n): Promise<Array<{ slug: string; title: string; summary: string }>> {\n const store = await loadActiveStore(root, (s) => s.entries.length > 0);\n if (!store) return [];\n\n const queryVec = await getProvider().embed(question);\n return findTopK(queryVec, store, EMBEDDING_TOP_K).map((entry) => ({\n slug: entry.slug,\n title: entry.title,\n summary: entry.summary,\n }));\n}\n\n/**\n * Look up top-K chunks similar to the question. Returns [] when no chunk-level\n * store exists so callers can fall back to page-level retrieval.\n */\nexport async function findRelevantChunks(\n root: string,\n question: string,\n k: number,\n): Promise<Array<{ chunk: ChunkEmbeddingEntry; score: number }>> {\n const store = await loadActiveStore(root, (s) => Boolean(s.chunks && s.chunks.length > 0));\n if (!store) return [];\n const queryVec = await getProvider().embed(question);\n return findTopKChunks(queryVec, store.chunks ?? [], k);\n}\n\n/**\n * Read the embedding store, returning null when it is missing, empty (per the\n * caller's predicate), or built with a stale model. Centralises the \"is this\n * store usable for semantic lookup right now?\" check.\n */\nasync function loadActiveStore(\n root: string,\n hasContent: (store: EmbeddingStore) => boolean,\n): Promise<EmbeddingStore | null> {\n const store = await readEmbeddingStore(root);\n if (!store || !hasContent(store)) return null;\n const activeModel = resolveEmbeddingModel();\n if (store.model !== activeModel) {\n warnStaleEmbeddingStore(store.model, activeModel);\n return null;\n }\n return store;\n}\n\n/** Scan concepts/ and queries/ directories, returning retrievable pages. */\nasync function collectPageRecords(root: string): Promise<PageRecord[]> {\n const records: PageRecord[] = [];\n for (const dir of [CONCEPTS_DIR, QUERIES_DIR]) {\n const absDir = path.join(root, dir);\n let files: string[];\n try {\n files = await readdir(absDir);\n } catch {\n continue;\n }\n for (const file of files.filter((f) => f.endsWith(\".md\"))) {\n const record = await readPageRecord(absDir, file);\n if (record) records.push(record);\n }\n }\n return records;\n}\n\n/** Parse a single page file into a PageRecord, skipping orphans/untitled pages. */\nasync function readPageRecord(absDir: string, file: string): Promise<PageRecord | null> {\n const content = await safeReadFile(path.join(absDir, file));\n const { meta, body } = parseFrontmatter(content);\n if (meta.orphaned || typeof meta.title !== \"string\") return null;\n return {\n slug: file.replace(/\\.md$/, \"\"),\n title: meta.title,\n summary: typeof meta.summary === \"string\" ? meta.summary : \"\",\n body,\n };\n}\n\n/** Build the text that represents a page in the embedding space. */\nfunction buildEmbeddingText(record: PageRecord): string {\n return record.summary\n ? `${record.title}\\n\\n${record.summary}`\n : record.title;\n}\n\n/**\n * Embed every page in `records` whose slug appears in `slugsToEmbed`,\n * returning the new entries. Failures bubble up to the caller.\n */\nasync function embedPages(\n records: PageRecord[],\n slugsToEmbed: Set<string>,\n): Promise<EmbeddingEntry[]> {\n const provider = getProvider();\n const now = new Date().toISOString();\n const fresh: EmbeddingEntry[] = [];\n\n for (const record of records) {\n if (!slugsToEmbed.has(record.slug)) continue;\n const vector = await provider.embed(buildEmbeddingText(record));\n fresh.push({\n slug: record.slug,\n title: record.title,\n summary: record.summary,\n vector,\n updatedAt: now,\n });\n }\n return fresh;\n}\n\n/** Tracks which (stored, active) model pairs have already been warned about. */\nconst warnedStaleModels = new Set<string>();\n\n/** Warn once per (stored, active) model pair so queries stay quiet on repeat runs. */\nfunction warnStaleEmbeddingStore(storedModel: string, activeModel: string): void {\n const key = `${storedModel}→${activeModel}`;\n if (warnedStaleModels.has(key)) return;\n warnedStaleModels.add(key);\n output.status(\n \"!\",\n output.warn(\n `Embedding store was built with \"${storedModel}\" but active embedding model is \"${activeModel}\". ` +\n `Falling back to full-index selection. Run 'llmwiki compile' to rebuild embeddings.`,\n ),\n );\n}\n\n/** Test-only hook: clear the warned-pair cache so each test sees a fresh warning. */\nexport function resetStaleEmbeddingWarnings(): void {\n warnedStaleModels.clear();\n}\n\n/** Choose the active embedding model name, defaulting to anthropic's voyage model. */\nexport function resolveEmbeddingModel(): string {\n const providerName = getActiveProviderName();\n const configuredModel = process.env.LLMWIKI_EMBEDDING_MODEL?.trim();\n if (configuredModel && (providerName === \"openai\" || providerName === \"ollama\")) {\n return configuredModel;\n }\n return EMBEDDING_MODELS[providerName] ?? EMBEDDING_MODELS.anthropic;\n}\n\n/** Merge fresh embeddings into an existing store, dropping slugs not in liveSlugs. */\nfunction mergeEntries(\n existing: EmbeddingEntry[],\n fresh: EmbeddingEntry[],\n liveSlugs: Set<string>,\n): EmbeddingEntry[] {\n const bySlug = new Map<string, EmbeddingEntry>();\n for (const entry of existing) {\n if (liveSlugs.has(entry.slug)) bySlug.set(entry.slug, entry);\n }\n for (const entry of fresh) {\n bySlug.set(entry.slug, entry);\n }\n return Array.from(bySlug.values());\n}\n\n/**\n * Refresh chunk embeddings for the given pages, reusing existing chunk vectors\n * whose contentHash still matches. Pages absent from `records` are pruned.\n */\nasync function refreshChunkEmbeddings(\n records: PageRecord[],\n existing: ChunkEmbeddingEntry[],\n forceAll: boolean,\n): Promise<ChunkEmbeddingEntry[]> {\n const liveSlugs = new Set(records.map((r) => r.slug));\n const existingByKey = indexChunksByKey(existing.filter((c) => liveSlugs.has(c.slug)));\n const now = new Date().toISOString();\n const fresh: ChunkEmbeddingEntry[] = [];\n\n for (const record of records) {\n const pageChunks = await embedRecordChunks(record, existingByKey, forceAll, now);\n fresh.push(...pageChunks);\n }\n return fresh;\n}\n\n/**\n * Embed (or reuse) every chunk for a single page, in order. Reused chunks have\n * their `title` refreshed so a renamed page propagates to the chunk metadata.\n */\nasync function embedRecordChunks(\n record: PageRecord,\n existingByKey: Map<string, ChunkEmbeddingEntry>,\n forceAll: boolean,\n now: string,\n): Promise<ChunkEmbeddingEntry[]> {\n const provider = getProvider();\n const chunkTexts = splitIntoChunks(record.body);\n const out: ChunkEmbeddingEntry[] = [];\n\n for (let i = 0; i < chunkTexts.length; i++) {\n const text = chunkTexts[i];\n const contentHash = hashChunkText(text);\n const reused = pickReusableChunk(existingByKey, record.slug, i, contentHash, forceAll);\n if (reused) {\n out.push({ ...reused, title: record.title });\n continue;\n }\n const vector = await provider.embed(text);\n out.push({\n slug: record.slug, title: record.title, chunkIndex: i,\n contentHash, text, vector, updatedAt: now,\n });\n }\n return out;\n}\n\n/** Index existing chunks by `${slug}#${chunkIndex}` for O(1) reuse lookup. */\nfunction indexChunksByKey(chunks: ChunkEmbeddingEntry[]): Map<string, ChunkEmbeddingEntry> {\n const byKey = new Map<string, ChunkEmbeddingEntry>();\n for (const chunk of chunks) byKey.set(chunkKey(chunk.slug, chunk.chunkIndex), chunk);\n return byKey;\n}\n\n/** Compose the index key for a chunk lookup. */\nfunction chunkKey(slug: string, chunkIndex: number): string {\n return `${slug}#${chunkIndex}`;\n}\n\n/** Return the existing chunk vector when its hash still matches and reuse is allowed. */\nfunction pickReusableChunk(\n byKey: Map<string, ChunkEmbeddingEntry>,\n slug: string,\n chunkIndex: number,\n contentHash: string,\n forceAll: boolean,\n): ChunkEmbeddingEntry | null {\n if (forceAll) return null;\n const existing = byKey.get(chunkKey(slug, chunkIndex));\n if (!existing) return null;\n return existing.contentHash === contentHash ? existing : null;\n}\n\n/**\n * Re-embed the given changed slugs and prune any entries whose pages no longer\n * exist on disk. Changed slugs not present as live pages are silently skipped.\n */\nexport async function updateEmbeddings(root: string, changedSlugs: string[]): Promise<void> {\n const records = await collectPageRecords(root);\n const liveSlugs = new Set(records.map((r) => r.slug));\n const embeddingModel = resolveEmbeddingModel();\n const existingStore = await readEmbeddingStore(root);\n const modelChanged = Boolean(existingStore && existingStore.model !== embeddingModel);\n const toEmbed = new Set(changedSlugs.filter((slug) => liveSlugs.has(slug)));\n const previousEntries = modelChanged ? [] : existingStore?.entries ?? [];\n const previousChunks = modelChanged ? [] : existingStore?.chunks ?? [];\n\n // Cold start: embed every page so the store is immediately useful.\n // Also treat an empty on-disk store as a cold start so that a project\n // with no ingested pages yet (or a wiped store) gets populated the next\n // time `compile` runs without needing an explicit slug change.\n const isEmptyStore = isStoreEmpty(existingStore);\n if (!existingStore || modelChanged || (isEmptyStore && liveSlugs.size > 0)) {\n for (const record of records) toEmbed.add(record.slug);\n }\n\n if (!shouldRunEmbedding(modelChanged, toEmbed, previousEntries, previousChunks, liveSlugs)) {\n return;\n }\n\n const freshEntries = await embedPages(records, toEmbed);\n const mergedEntries = mergeEntries(previousEntries, freshEntries, liveSlugs);\n const mergedChunks = await refreshChunkEmbeddings(records, previousChunks, modelChanged);\n\n await persistRefreshedStore(root, embeddingModel, mergedEntries, mergedChunks);\n}\n\n/** Persist a freshly merged store and emit a friendly status line. */\nasync function persistRefreshedStore(\n root: string,\n embeddingModel: string,\n entries: EmbeddingEntry[],\n chunks: ChunkEmbeddingEntry[],\n): Promise<void> {\n const dimensions = entries[0]?.vector.length ?? chunks[0]?.vector.length ?? 0;\n const store: EmbeddingStore = {\n version: STORE_VERSION,\n model: embeddingModel,\n dimensions,\n entries,\n chunks,\n };\n await writeEmbeddingStore(root, store);\n output.status(\n \"*\",\n output.dim(`Embeddings updated (${entries.length} pages, ${chunks.length} chunks).`),\n );\n}\n\n/** Return true when a store exists on disk but has neither page nor chunk entries. */\nfunction isStoreEmpty(store: EmbeddingStore | null): boolean {\n if (!store) return false;\n return store.entries.length === 0 && (!store.chunks || store.chunks.length === 0);\n}\n\n/** Decide whether updateEmbeddings has work to do beyond a no-op. */\nfunction shouldRunEmbedding(\n modelChanged: boolean,\n toEmbed: Set<string>,\n previousEntries: EmbeddingEntry[],\n previousChunks: ChunkEmbeddingEntry[],\n liveSlugs: Set<string>,\n): boolean {\n if (modelChanged) return true;\n if (toEmbed.size > 0) return true;\n if (!previousEntries.every((e) => liveSlugs.has(e.slug))) return true;\n if (!previousChunks.every((c) => liveSlugs.has(c.slug))) return true;\n // Cold-start case where we have entries but no chunks yet.\n if (previousEntries.length > 0 && previousChunks.length === 0 && liveSlugs.size > 0) return true;\n return false;\n}\n","/**\n * Chunked retrieval helpers: text splitting, content hashing, and BM25 reranking.\n *\n * The query pipeline relies on these utilities to:\n * 1. Split a wiki page into paragraph-aligned chunks for embedding.\n * 2. Detect unchanged chunks via a stable content hash so embedding refreshes\n * can skip work.\n * 3. Rerank a candidate set with a lightweight BM25 score over chunk text,\n * improving precision over pure cosine similarity for keyword-heavy\n * questions.\n *\n * No network calls happen here — these are deterministic CPU-side helpers\n * that are easy to unit test and safe to invoke from any code path.\n */\n\nimport { createHash } from \"crypto\";\nimport {\n CHUNK_MAX_CHARS,\n CHUNK_MIN_CHARS,\n CHUNK_TARGET_CHARS,\n} from \"./constants.js\";\n\n/** Stable content hash used to detect chunk-level changes between runs. */\nexport function hashChunkText(text: string): string {\n return createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Split a page body into paragraph-aligned chunks bounded by CHUNK_TARGET_CHARS.\n * Trailing fragments smaller than CHUNK_MIN_CHARS are merged into the previous\n * chunk so we never emit a tiny dangling piece. Paragraphs longer than\n * CHUNK_MAX_CHARS are sentence-split before being added.\n *\n * @param body - Raw page body (frontmatter already stripped).\n * @returns Ordered chunk strings; empty array when body has no usable text.\n */\nexport function splitIntoChunks(body: string): string[] {\n const paragraphs = extractParagraphs(body);\n if (paragraphs.length === 0) return [];\n\n const chunks: string[] = [];\n let buffer = \"\";\n\n for (const paragraph of paragraphs) {\n for (const piece of splitOversizedParagraph(paragraph)) {\n buffer = appendParagraph(buffer, piece, chunks);\n }\n }\n\n if (buffer.length > 0) chunks.push(buffer);\n return mergeTrailingFragment(chunks);\n}\n\n/** Append a paragraph to the buffer, flushing when the target size is exceeded. */\nfunction appendParagraph(buffer: string, paragraph: string, chunks: string[]): string {\n const candidate = buffer ? `${buffer}\\n\\n${paragraph}` : paragraph;\n if (candidate.length <= CHUNK_TARGET_CHARS) return candidate;\n\n if (buffer.length > 0) {\n chunks.push(buffer);\n return paragraph;\n }\n // Single paragraph already exceeds target — emit it as a standalone chunk.\n chunks.push(candidate);\n return \"\";\n}\n\n/**\n * Merge a too-small trailing chunk back into its predecessor for cleaner\n * ranking. We only merge when the combined size would still respect\n * CHUNK_MAX_CHARS — otherwise the tiny tail stays standalone.\n */\nfunction mergeTrailingFragment(chunks: string[]): string[] {\n if (chunks.length < 2) return chunks;\n const last = chunks[chunks.length - 1];\n if (last.length >= CHUNK_MIN_CHARS) return chunks;\n const previous = chunks[chunks.length - 2];\n // +2 covers the \"\\n\\n\" separator length we insert between paragraphs.\n if (previous.length + last.length + 2 > CHUNK_MAX_CHARS) return chunks;\n const merged = chunks.slice(0, -2);\n merged.push(`${previous}\\n\\n${last}`);\n return merged;\n}\n\n/** Strip whitespace-only paragraphs from a markdown body. */\nfunction extractParagraphs(body: string): string[] {\n return body\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter((p) => p.length > 0);\n}\n\n/**\n * Sentence-split a paragraph that exceeds CHUNK_MAX_CHARS so the resulting\n * pieces still respect the upper bound. A single sentence longer than the\n * cap is hard-cut at CHUNK_MAX_CHARS — preferable to dropping content.\n */\nfunction splitOversizedParagraph(paragraph: string): string[] {\n if (paragraph.length <= CHUNK_MAX_CHARS) return [paragraph];\n\n const sentences = paragraph.split(/(?<=[.!?])\\s+/);\n const pieces: string[] = [];\n let buffer = \"\";\n\n for (const sentence of sentences) {\n if ((buffer + \" \" + sentence).length > CHUNK_MAX_CHARS && buffer.length > 0) {\n pieces.push(buffer.trim());\n buffer = sentence;\n } else {\n buffer = buffer ? `${buffer} ${sentence}` : sentence;\n }\n }\n\n if (buffer.length > 0) pieces.push(buffer.trim());\n return pieces.flatMap(hardCut);\n}\n\n/** Hard-cut a string longer than CHUNK_MAX_CHARS into fixed-size pieces. */\nfunction hardCut(text: string): string[] {\n if (text.length <= CHUNK_MAX_CHARS) return [text];\n const pieces: string[] = [];\n for (let start = 0; start < text.length; start += CHUNK_MAX_CHARS) {\n pieces.push(text.slice(start, start + CHUNK_MAX_CHARS));\n }\n return pieces;\n}\n\n/** A scored candidate that the BM25 reranker accepts. */\ninterface RankableCandidate {\n text: string;\n /** Initial similarity score; preserved for debug output and tie-breaking. */\n baseScore: number;\n}\n\n/** Result of a BM25 rerank: original candidate plus the rerank score. */\ninterface RankedCandidate<T extends RankableCandidate> {\n candidate: T;\n score: number;\n}\n\n/**\n * Rerank candidates with BM25 over their `text` field given a free-text query.\n * BM25 is a deterministic keyword-overlap metric that complements semantic\n * similarity well: it boosts chunks that literally mention the query terms.\n *\n * @param query - Natural-language query.\n * @param candidates - Items to rerank; their `baseScore` is used as a tiebreaker.\n * @returns Sorted descending by combined score.\n */\nexport function rerankWithBm25<T extends RankableCandidate>(\n query: string,\n candidates: T[],\n): Array<RankedCandidate<T>> {\n if (candidates.length === 0) return [];\n const queryTerms = tokenize(query);\n if (queryTerms.length === 0) {\n return candidates.map((candidate) => ({ candidate, score: candidate.baseScore }));\n }\n\n const docs = candidates.map((c) => tokenize(c.text));\n const stats = buildCorpusStats(docs);\n return rankByBm25Score(candidates, docs, queryTerms, stats);\n}\n\n/** Rank candidates by combined BM25 + base semantic score. */\nfunction rankByBm25Score<T extends RankableCandidate>(\n candidates: T[],\n docs: string[][],\n queryTerms: string[],\n stats: CorpusStats,\n): Array<RankedCandidate<T>> {\n const scored = candidates.map((candidate, index) => {\n const lexical = bm25Score(queryTerms, docs[index], stats);\n return { candidate, score: lexical + candidate.baseScore * BASE_SCORE_WEIGHT };\n });\n scored.sort((a, b) => b.score - a.score);\n return scored;\n}\n\n/** Tokenise a string into lowercase alphanumeric tokens for BM25. */\nfunction tokenize(text: string): string[] {\n return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];\n}\n\ninterface CorpusStats {\n /** Document frequency per term: how many docs contain the term. */\n docFreq: Map<string, number>;\n /** Average document length across the corpus. */\n avgDocLen: number;\n /** Total document count. */\n totalDocs: number;\n}\n\n/** Precompute BM25 corpus statistics from the tokenised candidate set. */\nfunction buildCorpusStats(docs: string[][]): CorpusStats {\n const docFreq = new Map<string, number>();\n let totalLen = 0;\n for (const tokens of docs) {\n totalLen += tokens.length;\n const unique = new Set(tokens);\n for (const term of unique) docFreq.set(term, (docFreq.get(term) ?? 0) + 1);\n }\n const totalDocs = docs.length;\n const avgDocLen = totalDocs > 0 ? totalLen / totalDocs : 0;\n return { docFreq, avgDocLen, totalDocs };\n}\n\n/** BM25 saturation parameter; higher = slower term-frequency saturation. */\nconst BM25_K1 = 1.5;\n/** BM25 length normalisation strength; 0 disables, 1 is full normalisation. */\nconst BM25_B = 0.75;\n/** How much weight the original semantic score retains in the rerank tie-break. */\nconst BASE_SCORE_WEIGHT = 0.5;\n\n/** Compute BM25 score for one document against a tokenised query. */\nfunction bm25Score(queryTerms: string[], docTokens: string[], stats: CorpusStats): number {\n if (docTokens.length === 0 || stats.totalDocs === 0) return 0;\n const termFreq = countTerms(docTokens);\n const lengthRatio = docTokens.length / (stats.avgDocLen || 1);\n\n let total = 0;\n for (const term of queryTerms) {\n const tf = termFreq.get(term) ?? 0;\n if (tf === 0) continue;\n const idf = idfWeight(stats.docFreq.get(term) ?? 0, stats.totalDocs);\n const numerator = tf * (BM25_K1 + 1);\n const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * lengthRatio);\n total += idf * (numerator / denominator);\n }\n return total;\n}\n\n/** BM25 inverse-document-frequency component (Robertson-Spärck Jones form). */\nfunction idfWeight(docFrequency: number, totalDocs: number): number {\n const numerator = totalDocs - docFrequency + 0.5;\n const denominator = docFrequency + 0.5;\n // +1 inside log keeps idf non-negative even when a term appears in every doc.\n return Math.log(1 + numerator / denominator);\n}\n\n/** Count token occurrences in a single document. */\nfunction countTerms(tokens: string[]): Map<string, number> {\n const counts = new Map<string, number>();\n for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);\n return counts;\n}\n","/**\n * Review candidate persistence for the llmwiki compile pipeline.\n *\n * When `llmwiki compile --review` runs, generated wiki pages are routed\n * here as JSON candidate records under `.llmwiki/candidates/` instead of\n * being written directly to `wiki/`. Reviewers then approve or reject the\n * proposals via the `llmwiki review` subcommands.\n *\n * Candidates are deliberately kept as standalone JSON so they survive across\n * compile runs and can be inspected manually without the CLI. Each record\n * stores the full page body so approval is a pure copy — the LLM is never\n * called again at approval time.\n */\n\nimport { unlink } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { randomBytes } from \"crypto\";\nimport { atomicWrite, safeReadFile } from \"../utils/markdown.js\";\nimport {\n listCandidateFileIds,\n moveCandidateToArchive,\n} from \"../utils/candidate-store.js\";\nimport * as output from \"../utils/output.js\";\nimport {\n CANDIDATES_DIR,\n CANDIDATES_ARCHIVE_DIR,\n} from \"../utils/constants.js\";\nimport type { ReviewCandidate, SourceState } from \"../utils/types.js\";\nimport type { LintResult } from \"../linter/types.js\";\n\n/** Length (bytes) of the random suffix appended to candidate ids. */\nconst ID_SUFFIX_BYTES = 4;\n\n/** Filesystem extension used for candidate JSON files. */\nconst CANDIDATE_EXT = \".json\";\n\n/** Input shape for creating a new candidate (id + timestamp generated here). */\ninterface CandidateDraft {\n title: string;\n slug: string;\n summary: string;\n sources: string[];\n body: string;\n /**\n * Per-source state entries to persist into `.llmwiki/state.json` when this\n * candidate is approved. Keyed by source filename. Optional so callers that\n * never need incremental tracking (legacy / tests) can omit it.\n */\n sourceStates?: Record<string, SourceState>;\n /**\n * Schema lint violations for the candidate body detected at compile time.\n * Omit (or pass `undefined`) when the candidate body is clean.\n */\n schemaViolations?: LintResult[];\n /**\n * Provenance lint violations for the candidate body — malformed claim\n * citations, out-of-bounds spans, or missing source files. Surfaced\n * alongside schema violations so reviewers see citation issues before\n * approving.\n */\n provenanceViolations?: LintResult[];\n}\n\n/** Build a deterministic-but-unique id from a slug and a short random suffix. */\nfunction buildCandidateId(slug: string): string {\n const suffix = randomBytes(ID_SUFFIX_BYTES).toString(\"hex\");\n return `${slug}-${suffix}`;\n}\n\n/** Absolute path to a candidate's JSON file. */\nfunction candidatePath(root: string, id: string): string {\n return path.join(root, CANDIDATES_DIR, `${id}${CANDIDATE_EXT}`);\n}\n\n/** Absolute path to the archived JSON file for a rejected candidate. */\nfunction archivePath(root: string, id: string): string {\n return path.join(root, CANDIDATES_ARCHIVE_DIR, `${id}${CANDIDATE_EXT}`);\n}\n\n/**\n * Persist a new candidate record and return it. The id is generated from the\n * slug plus a short random suffix so multiple compile runs can co-exist.\n * @param root - Project root directory.\n * @param draft - The candidate fields to persist.\n * @returns The full ReviewCandidate (with id + generatedAt populated).\n */\nexport async function writeCandidate(\n root: string,\n draft: CandidateDraft,\n): Promise<ReviewCandidate> {\n const candidate: ReviewCandidate = {\n id: buildCandidateId(draft.slug),\n title: draft.title,\n slug: draft.slug,\n summary: draft.summary,\n sources: draft.sources,\n body: draft.body,\n generatedAt: new Date().toISOString(),\n ...(draft.sourceStates ? { sourceStates: draft.sourceStates } : {}),\n ...(draft.schemaViolations ? { schemaViolations: draft.schemaViolations } : {}),\n ...(draft.provenanceViolations ? { provenanceViolations: draft.provenanceViolations } : {}),\n };\n\n await atomicWrite(candidatePath(root, candidate.id), JSON.stringify(candidate, null, 2));\n return candidate;\n}\n\n/**\n * Emit a CLI error, set exit code 1, and return null. Used by candidate load\n * helpers to avoid duplicating the error-path boilerplate.\n * @param message - Error message to display.\n */\nfunction failWithError(message: string): null {\n output.status(\"!\", output.error(message));\n process.exitCode = 1;\n return null;\n}\n\n/**\n * Load a candidate by id and, if missing, emit the standard \"not found\" CLI\n * error and set process.exitCode = 1. Returns null when the candidate is\n * missing so callers can early-return without re-implementing the same\n * error block in every review subcommand.\n * @param root - Project root directory.\n * @param id - Candidate id to look up.\n */\nexport async function loadCandidateOrFail(\n root: string,\n id: string,\n): Promise<ReviewCandidate | null> {\n const candidate = await readCandidate(root, id);\n if (!candidate) return failWithError(`Candidate not found: ${id}`);\n return candidate;\n}\n\n/**\n * Re-read a candidate under the lock and abort if it has disappeared.\n *\n * This is the authoritative TOCTOU guard: a concurrent approve or reject may\n * have removed the candidate after the pre-lock fast-fail but before the lock\n * was acquired. Returning `null` signals the caller to abort without writing\n * any output artefact.\n * @param root - Project root directory.\n * @param id - Candidate id to load.\n * @returns The candidate if still present, or `null` after setting exit code 1.\n */\nexport async function loadCandidateUnderLockOrFail(\n root: string,\n id: string,\n): Promise<ReviewCandidate | null> {\n const candidate = await readCandidate(root, id);\n if (!candidate) {\n return failWithError(`Candidate ${id} was removed by another process during review.`);\n }\n return candidate;\n}\n\n/** Parse a single candidate JSON file. Returns null when the file is missing or malformed. */\nexport async function readCandidate(\n root: string,\n id: string,\n): Promise<ReviewCandidate | null> {\n const raw = await safeReadFile(candidatePath(root, id));\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as ReviewCandidate;\n if (!isValidCandidate(parsed)) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\n/** Defensive type-guard so corrupted candidate files don't blow up the CLI. */\nfunction isValidCandidate(value: unknown): value is ReviewCandidate {\n if (!value || typeof value !== \"object\") return false;\n const candidate = value as Record<string, unknown>;\n return (\n typeof candidate.id === \"string\" &&\n typeof candidate.title === \"string\" &&\n typeof candidate.slug === \"string\" &&\n typeof candidate.body === \"string\" &&\n Array.isArray(candidate.sources)\n );\n}\n\n/**\n * List every candidate currently pending review, sorted by generation time.\n * Skips files that aren't candidate JSON (e.g. the archive subdirectory).\n * @param root - Project root directory.\n * @returns All pending review candidates.\n */\nexport async function listCandidates(root: string): Promise<ReviewCandidate[]> {\n const dir = path.join(root, CANDIDATES_DIR);\n const ids = await listCandidateFileIds(dir);\n const candidates: ReviewCandidate[] = [];\n for (const id of ids) {\n const candidate = await readCandidate(root, id);\n if (candidate) candidates.push(candidate);\n }\n\n candidates.sort((a, b) => a.generatedAt.localeCompare(b.generatedAt));\n return candidates;\n}\n\n/**\n * Count pending candidates using the same validity filter as listCandidates,\n * so consumers (e.g. `wiki_status.pendingCandidates`) never report counts\n * that disagree with what `review list` actually shows. Malformed JSON files\n * are skipped here exactly as they are by listCandidates.\n */\nexport async function countCandidates(root: string): Promise<number> {\n const candidates = await listCandidates(root);\n return candidates.length;\n}\n\n/** Remove a pending candidate from disk. Returns false when nothing existed to remove. */\nexport async function deleteCandidate(root: string, id: string): Promise<boolean> {\n const filePath = candidatePath(root, id);\n if (!existsSync(filePath)) return false;\n await unlink(filePath);\n return true;\n}\n\n/**\n * Move a candidate from the pending area into the archive subdirectory so\n * rejected proposals stay auditable without touching `wiki/`.\n * @param root - Project root directory.\n * @param id - Candidate id to archive.\n * @returns True when the candidate was found and archived.\n */\nexport async function archiveCandidate(root: string, id: string): Promise<boolean> {\n return moveCandidateToArchive(candidatePath(root, id), archivePath(root, id));\n}\n","/**\n * Shared filesystem primitives for candidate queues.\n *\n * Both the concept review queue (`compiler/candidates.ts`) and the rule\n * candidate queue (`compiler/rule-candidates.ts`) persist one JSON file per\n * candidate under a directory, list those files, and move rejected records\n * into an archive subdirectory. These two operations were identical across the\n * queues; extracting them here removes the duplication while keeping each\n * queue's own id/shape logic local to its module.\n */\n\nimport { readdir, rename, unlink, writeFile, mkdir } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { safeReadFile } from \"./markdown.js\";\n\n/** Extension used for all candidate JSON files. */\nexport const CANDIDATE_JSON_EXT = \".json\";\n\n/**\n * Turn a dotted candidate id into a single filesystem-safe path segment.\n *\n * Only characters outside `[a-z0-9._-]` are replaced (with `_`); dots are\n * PRESERVED. Collapsing dots to `-` (the old behavior) made `rulecand.a.b-c`\n * and `rulecand.a-b.c` map to the same file, silently overwriting one\n * candidate with the other. Keeping dots makes the mapping injective for the\n * ids this codebase emits (category in `[a-z0-9_]`, slug in `[a-z0-9-]`).\n * @param candidateId - The dotted candidate id.\n */\nexport function candidateFileId(candidateId: string): string {\n return candidateId.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n}\n\n/**\n * List the file ids (basename without `.json`) of every candidate JSON file in\n * a directory, ignoring subdirectories (e.g. an `archive/` folder). Returns an\n * empty list when the directory does not exist.\n * @param dir - Absolute path to the candidate directory.\n */\nexport async function listCandidateFileIds(dir: string): Promise<string[]> {\n if (!existsSync(dir)) return [];\n const entries = await readdir(dir, { withFileTypes: true });\n const ids: string[] = [];\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith(CANDIDATE_JSON_EXT)) continue;\n ids.push(entry.name.slice(0, -CANDIDATE_JSON_EXT.length));\n }\n return ids;\n}\n\n/**\n * Move a candidate JSON file into an archive location, creating the archive\n * directory if needed. Falls back to copy + unlink when `rename` fails across\n * filesystems. Returns false when the source file does not exist.\n * @param sourcePath - Absolute path of the pending candidate file.\n * @param targetPath - Absolute archive destination path.\n */\nexport async function moveCandidateToArchive(\n sourcePath: string,\n targetPath: string,\n): Promise<boolean> {\n if (!existsSync(sourcePath)) return false;\n await mkdir(path.dirname(targetPath), { recursive: true });\n try {\n await rename(sourcePath, targetPath);\n } catch {\n const raw = await safeReadFile(sourcePath);\n await writeFile(targetPath, raw, \"utf-8\");\n await unlink(sourcePath);\n }\n return true;\n}\n","/**\n * Lint rules for wiki quality checks.\n *\n * Each rule is a function that takes a project root path and returns\n * an array of LintResult diagnostics. Rules perform pure static analysis\n * with no LLM calls — they inspect frontmatter, wikilinks, citations,\n * and file structure to find potential issues.\n */\n\nimport { readdir, readFile } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport {\n isMalformedCitationEntry,\n parseFrontmatter,\n parseProvenanceMetadata,\n safeReadFile,\n slugify,\n splitCitationMarker,\n} from \"../utils/markdown.js\";\nimport {\n CONCEPTS_DIR,\n LOW_CONFIDENCE_THRESHOLD,\n MAX_INFERRED_PARAGRAPHS_WITHOUT_CITATIONS,\n QUERIES_DIR,\n SOURCES_DIR,\n} from \"../utils/constants.js\";\nimport type { LintResult } from \"./types.js\";\nimport {\n countWikilinks,\n resolvePageKind,\n type SchemaConfig,\n} from \"../schema/index.js\";\nimport { computeFreshness } from \"../freshness/index.js\";\nimport type { FreshnessSnapshot } from \"../freshness/types.js\";\n\n/** Minimum body length (in characters) for a page to be considered non-empty. */\nconst MIN_BODY_LENGTH = 50;\n\n/** Pattern matching [[Wikilink Title]] references in markdown content. */\nconst WIKILINK_PATTERN = /\\[\\[([^\\]]+)\\]\\]/g;\n\n/** Pattern matching ^[filename.md] citation markers in markdown content. */\nconst CITATION_PATTERN = /\\^\\[([^\\]]+)\\]/g;\n\n/** Match result with its line number and captured group. */\ninterface LineMatch {\n captured: string;\n line: number;\n}\n\n/**\n * Scan all lines of a page's content and return regex matches with line numbers.\n * Shared by rules that need to locate patterns within page bodies.\n */\nfunction findMatchesInContent(content: string, pattern: RegExp): LineMatch[] {\n const results: LineMatch[] = [];\n const lines = content.split(\"\\n\");\n for (let i = 0; i < lines.length; i++) {\n const matches = lines[i].matchAll(pattern);\n for (const match of matches) {\n results.push({ captured: match[1], line: i + 1 });\n }\n }\n return results;\n}\n\n/**\n * Read all .md files from a directory, returning their paths and parsed content.\n * Returns an empty array if the directory does not exist.\n */\nasync function readMarkdownFiles(\n dirPath: string,\n): Promise<Array<{ filePath: string; content: string }>> {\n if (!existsSync(dirPath)) return [];\n\n const entries = await readdir(dirPath);\n const mdFiles = entries.filter((f) => f.endsWith(\".md\"));\n\n const results = await Promise.all(\n mdFiles.map(async (fileName) => {\n const filePath = path.join(dirPath, fileName);\n const content = await readFile(filePath, \"utf-8\");\n return { filePath, content };\n }),\n );\n\n return results;\n}\n\n/**\n * Collect all wiki pages from both concepts/ and queries/ directories.\n */\nexport async function collectAllPages(\n root: string,\n): Promise<Array<{ filePath: string; content: string }>> {\n const conceptPages = await readMarkdownFiles(path.join(root, CONCEPTS_DIR));\n const queryPages = await readMarkdownFiles(path.join(root, QUERIES_DIR));\n return [...conceptPages, ...queryPages];\n}\n\n/**\n * Build a set of slugs for all existing wiki pages.\n * Used to verify that wikilink targets actually exist.\n */\nfunction buildPageSlugSet(\n pages: Array<{ filePath: string }>,\n): Set<string> {\n const slugs = new Set<string>();\n for (const page of pages) {\n const baseName = path.basename(page.filePath, \".md\");\n slugs.add(baseName.toLowerCase());\n }\n return slugs;\n}\n\n/** Find [[Title]] wikilinks that don't match any existing wiki page. */\nexport async function checkBrokenWikilinks(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const existingSlugs = buildPageSlugSet(pages);\n const results: LintResult[] = [];\n\n for (const page of pages) {\n for (const { captured, line } of findMatchesInContent(page.content, WIKILINK_PATTERN)) {\n const linkTarget = captured.split(\"|\")[0].trim();\n const linkSlug = slugify(linkTarget);\n if (!existingSlugs.has(linkSlug)) {\n results.push({\n rule: \"broken-wikilink\",\n severity: \"error\",\n file: page.filePath,\n message: `Broken wikilink [[${captured}]] — no matching page found`,\n line,\n });\n }\n }\n }\n\n return results;\n}\n\n/** Find pages with `orphaned: true` in their frontmatter. */\nexport async function checkOrphanedPages(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n\n for (const page of pages) {\n const { meta } = parseFrontmatter(page.content);\n if (meta.orphaned === true) {\n results.push({\n rule: \"orphaned-page\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page is marked as orphaned`,\n });\n }\n }\n\n return results;\n}\n\n/**\n * Report pages whose computed freshness is actionable: `stale` (a source\n * changed since compile) or `orphaned` (every source it derived from was\n * deleted but no compile has cleaned the page up yet). Pages already flagged\n * `orphaned: true` in frontmatter are left to {@link checkOrphanedPages} so the\n * two rules never double-report. `fresh` and `unverified` are not findings —\n * unverified covers legitimately unowned pages (query pages, hand-authored\n * content) that should not warn. Receives the shared freshness snapshot so the\n * source-hash pass happens once per lint run.\n */\nexport async function checkStalePages(root: string, snapshot: FreshnessSnapshot): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n for (const page of pages) {\n const { meta } = parseFrontmatter(page.content);\n if (meta.orphaned === true) continue;\n const slug = path.basename(page.filePath, \".md\");\n const pageDirectory = path.basename(path.dirname(page.filePath)) === \"queries\" ? \"queries\" : \"concepts\";\n const { freshnessStatus } = computeFreshness({ slug, pageDirectory, frontmatter: meta }, snapshot);\n if (freshnessStatus === \"stale\") {\n results.push({\n rule: \"stale-page\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page is stale — a source it was compiled from has changed since the last compile`,\n });\n } else if (freshnessStatus === \"orphaned\") {\n results.push({\n rule: \"orphaned-page\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page is orphaned — every source it was compiled from has been deleted; recompile to clean it up`,\n });\n }\n }\n return results;\n}\n\n/** Find pages with empty or missing `summary` in frontmatter. */\nexport async function checkMissingSummaries(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n\n for (const page of pages) {\n const { meta } = parseFrontmatter(page.content);\n const summary = meta.summary;\n const isMissing = !summary || (typeof summary === \"string\" && summary.trim() === \"\");\n\n if (isMissing) {\n results.push({\n rule: \"missing-summary\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page has no summary in frontmatter`,\n });\n }\n }\n\n return results;\n}\n\n/** Find multiple pages whose titles match case-insensitively. */\nexport async function checkDuplicateConcepts(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const titleMap = new Map<string, string[]>();\n\n for (const page of pages) {\n const { meta } = parseFrontmatter(page.content);\n const title = typeof meta.title === \"string\" ? meta.title : \"\";\n if (!title) continue;\n\n const normalizedTitle = title.toLowerCase().trim();\n const existing = titleMap.get(normalizedTitle) ?? [];\n existing.push(page.filePath);\n titleMap.set(normalizedTitle, existing);\n }\n\n const results: LintResult[] = [];\n for (const [title, files] of titleMap) {\n if (files.length <= 1) continue;\n for (const file of files) {\n results.push({\n rule: \"duplicate-concept\",\n severity: \"error\",\n file,\n message: `Duplicate title \"${title}\" — also in ${files.filter((f) => f !== file).join(\", \")}`,\n });\n }\n }\n\n return results;\n}\n\n/** Find pages with frontmatter but very short or empty body content. */\nexport async function checkEmptyPages(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n\n for (const page of pages) {\n const { meta, body } = parseFrontmatter(page.content);\n const hasTitle = typeof meta.title === \"string\" && meta.title.trim() !== \"\";\n const isBodyEmpty = body.trim().length < MIN_BODY_LENGTH;\n\n if (hasTitle && isBodyEmpty) {\n results.push({\n rule: \"empty-page\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page body is empty or too short (< ${MIN_BODY_LENGTH} chars)`,\n });\n }\n }\n\n return results;\n}\n\n/** Strip an optional `:start-end` or `#Lstart-Lend` span suffix from a citation entry. */\nfunction stripSpanSuffix(entry: string): string {\n const colonIdx = entry.indexOf(\":\");\n const hashIdx = entry.indexOf(\"#\");\n const cuts = [colonIdx, hashIdx].filter((i) => i >= 0);\n if (cuts.length === 0) return entry;\n return entry.slice(0, Math.min(...cuts));\n}\n\n/**\n * Flag pages whose frontmatter declares confidence below the threshold.\n * Pages without a confidence field are silently skipped to preserve\n * backward-compatibility with pre-existing wikis.\n */\nexport async function checkLowConfidencePages(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n\n for (const page of pages) {\n const { meta } = parseFrontmatter(page.content);\n const { confidence } = parseProvenanceMetadata(meta);\n if (confidence === undefined || confidence >= LOW_CONFIDENCE_THRESHOLD) continue;\n results.push({\n rule: \"low-confidence\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page confidence ${confidence.toFixed(2)} is below ${LOW_CONFIDENCE_THRESHOLD}`,\n });\n }\n\n return results;\n}\n\n/** Flag pages whose frontmatter records contradictions with other pages. */\nexport async function checkContradictedPages(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n\n for (const page of pages) {\n const { meta } = parseFrontmatter(page.content);\n const { contradictedBy } = parseProvenanceMetadata(meta);\n if (!contradictedBy || contradictedBy.length === 0) continue;\n const slugs = contradictedBy.map((r) => r.slug).join(\", \");\n results.push({\n rule: \"contradicted-page\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page contradicts: ${slugs}`,\n });\n }\n\n return results;\n}\n\n/**\n * Flag pages with too many inferred paragraphs unsupported by direct\n * citations. Always derived from the rendered page body — the body is\n * the single source of truth, no metadata field is consulted. Earlier\n * versions trusted an LLM-estimated `inferredParagraphs` frontmatter\n * field, but that estimate was made before the page even existed and\n * routinely disagreed with what the model actually produced. Counting\n * uncited prose paragraphs in the rendered body matches what a\n * reviewer would see and survives hand-edits.\n */\nexport async function checkInferredWithoutCitations(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n\n for (const page of pages) {\n const { body } = parseFrontmatter(page.content);\n const inferred = countUncitedProseParagraphs(body);\n if (inferred <= MAX_INFERRED_PARAGRAPHS_WITHOUT_CITATIONS) continue;\n results.push({\n rule: \"excess-inferred-paragraphs\",\n severity: \"warning\",\n file: page.filePath,\n message: `Page has ${inferred} inferred paragraphs without citations (max ${MAX_INFERRED_PARAGRAPHS_WITHOUT_CITATIONS})`,\n });\n }\n\n return results;\n}\n\n/**\n * Match a paragraph that looks like prose (not a heading, list, or code\n * block). Uses the Unicode `Letter` property so non-ASCII pages\n * generated via `--lang Chinese`, `--lang Japanese`, etc. (#46) are\n * still detected — the previous `[A-Za-z]` form silently dropped CJK,\n * Cyrillic, Greek, and Arabic prose, leaving\n * `excess-inferred-paragraphs` blind on those pages.\n */\nconst PROSE_PARAGRAPH_LEAD = /^\\p{L}/u;\n\n/** Count prose paragraphs in a body that lack a ^[citation] marker. */\nfunction countUncitedProseParagraphs(body: string): number {\n const paragraphs = body.split(/\\n\\s*\\n/);\n let count = 0;\n for (const block of paragraphs) {\n const trimmed = block.trim();\n if (trimmed.length === 0) continue;\n if (!PROSE_PARAGRAPH_LEAD.test(trimmed)) continue;\n if (CITATION_PATTERN.test(trimmed)) {\n CITATION_PATTERN.lastIndex = 0;\n continue;\n }\n CITATION_PATTERN.lastIndex = 0;\n count += 1;\n }\n return count;\n}\n\n/** Regex matching the `:start-end` span suffix on a citation entry. */\nconst COLON_SPAN_PATTERN = /^[^:#]+:(\\d+)(?:[,-]\\s*(\\d+))?$/;\n\n/** Regex matching the `#Lstart-Lend` span suffix on a citation entry. */\nconst HASH_SPAN_PATTERN = /^[^:#]+#L(\\d+)(?:-L(\\d+))?$/;\n\n/** Parsed line range from a citation entry, or null if no range is present. */\ninterface ParsedLineRange {\n start: number;\n end: number;\n}\n\n/**\n * Enforce per-kind cross-link minimums declared in the schema.\n * For each page, resolve its kind, look up the rule, and warn when the page\n * body has fewer wikilinks than the rule requires. Pages with kind `concept`\n * and a minimum of 0 (the default) generate no diagnostics, so existing\n * projects without a schema file see no behaviour change.\n *\n * Implementation delegates to {@link checkPageCrossLinks} per page so the\n * actual rule logic lives in exactly one place — the on-disk walker just\n * fans the per-page helper across `collectAllPages`.\n * @param root - Project root directory.\n * @param schema - Resolved schema config.\n */\nexport async function checkSchemaCrossLinks(\n root: string,\n schema: SchemaConfig,\n): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n for (const page of pages) {\n results.push(...checkPageCrossLinks(page.content, page.filePath, schema));\n }\n return results;\n}\n\n/**\n * Check cross-link minimums for a single page given as a raw content string.\n *\n * Unlike `checkSchemaCrossLinks`, this function operates on content already in\n * memory without reading from disk. Used by the review pipeline to attach\n * schema violations to a candidate at write time so `review show` can surface\n * them before the reviewer approves the page.\n *\n * The `filePath` parameter is embedded verbatim in each `LintResult.file` so\n * callers control how the candidate is identified in diagnostic output.\n *\n * @param content - Full page content including frontmatter.\n * @param filePath - Logical file path to embed in diagnostics (may be virtual).\n * @param schema - Resolved schema config.\n * @returns Lint results for this single page, empty when no violations found.\n */\nexport function checkPageCrossLinks(\n content: string,\n filePath: string,\n schema: SchemaConfig,\n): LintResult[] {\n const { meta, body } = parseFrontmatter(content);\n const kind = resolvePageKind(meta.kind, schema);\n const rule = schema.kinds[kind];\n if (rule.minWikilinks <= 0) return [];\n\n const linkCount = countWikilinks(body);\n if (linkCount >= rule.minWikilinks) return [];\n\n return [\n {\n rule: \"schema-cross-link-minimum\",\n severity: \"warning\",\n file: filePath,\n message:\n `Page kind \"${kind}\" requires at least ${rule.minWikilinks} ` +\n `[[wikilinks]] but only ${linkCount} found.`,\n },\n ];\n}\n\n/** Extract the line range from a citation entry string, or return null if there is none. */\nfunction parseLineRange(entry: string): ParsedLineRange | null {\n const colonMatch = COLON_SPAN_PATTERN.exec(entry);\n if (colonMatch) {\n const start = Number(colonMatch[1]);\n const end = colonMatch[2] !== undefined ? Number(colonMatch[2]) : start;\n return { start, end };\n }\n const hashMatch = HASH_SPAN_PATTERN.exec(entry);\n if (hashMatch) {\n const start = Number(hashMatch[1]);\n const end = hashMatch[2] !== undefined ? Number(hashMatch[2]) : start;\n return { start, end };\n }\n return null;\n}\n\n/** Count the number of lines in a file's text content. */\nfunction countLines(content: string): number {\n if (content.length === 0) return 0;\n return content.split(\"\\n\").length;\n}\n\n/**\n * Find ^[filename.md] citations referencing source files that don't exist, and\n * flag claim-level spans whose line ranges exceed the source file's actual length.\n * Handles both single-source ^[file.md] and multi-source ^[a.md, b.md] forms,\n * plus the claim-level extension `^[file.md:42-58]` / `^[file.md#L42-L58]`.\n * Line counts are cached per source file to avoid redundant reads.\n */\nexport async function checkBrokenCitations(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const sourcesDir = path.join(root, SOURCES_DIR);\n const results: LintResult[] = [];\n const lineCountCache = new Map<string, number>();\n\n for (const page of pages) {\n const pageFindings = await checkPageBrokenCitations(\n page.content,\n page.filePath,\n sourcesDir,\n lineCountCache,\n );\n results.push(...pageFindings);\n }\n\n return results;\n}\n\n/**\n * Pure-body variant of {@link checkBrokenCitations} that inspects a single\n * page's content against an in-memory or on-disk sources directory. Used\n * by the on-disk lint walker above, and by the in-memory candidate-lint\n * path so `compile --review` surfaces broken-source-file and out-of-bounds\n * span findings before a reviewer approves the candidate.\n *\n * @param content - Full page markdown including frontmatter.\n * @param filePath - Logical path embedded in diagnostics (may be virtual).\n * @param sourcesDir - Absolute path to the project's sources/ directory.\n * @param lineCountCache - Optional cross-page cache; provide one when\n * linting many pages so source file line counts aren't re-read.\n */\nexport async function checkPageBrokenCitations(\n content: string,\n filePath: string,\n sourcesDir: string,\n lineCountCache: Map<string, number> = new Map(),\n): Promise<LintResult[]> {\n const results: LintResult[] = [];\n for (const { captured, line } of findMatchesInContent(content, CITATION_PATTERN)) {\n await collectBrokenForMarker(captured, line, filePath, sourcesDir, lineCountCache, results);\n }\n return results;\n}\n\n/** Append broken-citation diagnostics for every entry inside a single ^[...] marker. */\nasync function collectBrokenForMarker(\n captured: string,\n line: number,\n pageFile: string,\n sourcesDir: string,\n lineCountCache: Map<string, number>,\n out: LintResult[],\n): Promise<void> {\n for (const part of splitCitationMarker(captured)) {\n const trimmed = part.trim();\n if (trimmed.length === 0) continue;\n const filename = stripSpanSuffix(trimmed);\n const citedPath = path.join(sourcesDir, filename);\n if (!existsSync(citedPath)) {\n out.push({\n rule: \"broken-citation\",\n severity: \"error\",\n file: pageFile,\n message: `Broken citation ^[${filename}] — source file not found`,\n line,\n });\n continue;\n }\n const range = parseLineRange(trimmed);\n if (range === null) continue;\n const lineCount = await resolveLineCount(citedPath, filename, lineCountCache);\n if (range.end <= lineCount) continue;\n out.push({\n rule: \"broken-citation\",\n severity: \"error\",\n file: pageFile,\n message: `Claim-level span ^[${trimmed}] is out of bounds (source has only ${lineCount} lines)`,\n line,\n });\n }\n}\n\n/** Return the line count for a source file, reading and caching if necessary. */\nasync function resolveLineCount(\n citedPath: string,\n filename: string,\n cache: Map<string, number>,\n): Promise<number> {\n const cached = cache.get(filename);\n if (cached !== undefined) return cached;\n const content = await safeReadFile(citedPath);\n const lineCount = countLines(content);\n cache.set(filename, lineCount);\n return lineCount;\n}\n\n/**\n * Find ^[...] markers whose entries do not parse against the documented\n * paragraph or claim-level grammar (e.g. `^[file.md:abc]` or `^[file.md#X]`).\n * Detects malformed claim-level citations without breaking the paragraph form.\n */\nexport async function checkMalformedClaimCitations(root: string): Promise<LintResult[]> {\n const pages = await collectAllPages(root);\n const results: LintResult[] = [];\n for (const page of pages) {\n results.push(...checkPageMalformedCitations(page.content, page.filePath));\n }\n return results;\n}\n\n/**\n * Pure-body variant of {@link checkMalformedClaimCitations} that inspects\n * a single page's content. Used by both the on-disk lint walker above and\n * the in-memory candidate-lint path so `compile --review` surfaces\n * malformed claim citations before a reviewer approves the candidate.\n */\nexport function checkPageMalformedCitations(content: string, filePath: string): LintResult[] {\n const results: LintResult[] = [];\n for (const { captured, line } of findMatchesInContent(content, CITATION_PATTERN)) {\n for (const part of splitCitationMarker(captured)) {\n if (!isMalformedCitationEntry(part)) continue;\n results.push({\n rule: \"malformed-claim-citation\",\n severity: \"error\",\n file: filePath,\n message: `Malformed claim citation ^[${captured}] — expected file.md, file.md:N-N, or file.md#LN-LN`,\n line,\n });\n }\n }\n return results;\n}\n","/**\n * Computed source-freshness layer.\n *\n * Derives a page's freshness on demand from a FreshnessSnapshot (state.json\n * hashes + current source hashes). Pure — never reads or writes the filesystem\n * and never persists freshness. See localdocs/specs/2026-06-04-source-freshness-design.md.\n */\n\nimport type { FreshnessSnapshot, PageFreshness, PageFreshnessInput, FreshnessStatus, SourceFreshness } from \"./types.js\";\nimport { existsSync } from \"fs\";\nimport { realpath } from \"fs/promises\";\nimport path from \"path\";\nimport { readStateClassified } from \"../utils/state.js\";\nimport type { ClassifiedState } from \"../utils/state.js\";\nimport { hashFile } from \"../compiler/hasher.js\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\n\n/** Compute the three orthogonal freshness signals for one page. */\nexport function computeFreshness(page: PageFreshnessInput, snapshot: FreshnessSnapshot): PageFreshness {\n return {\n freshnessStatus: classify(page, snapshot),\n contradicted: Array.isArray(page.frontmatter.contradictedBy) && page.frontmatter.contradictedBy.length > 0,\n archived: page.frontmatter.archived === true,\n };\n}\n\n/** Source-derived freshness status, per the spec's ordered ownership algorithm. */\nfunction classify(page: PageFreshnessInput, snapshot: FreshnessSnapshot): FreshnessStatus {\n if (snapshot.stateStatus !== \"ok\") return \"unverified\";\n // Query pages are generated answers, not source projections — always unverified,\n // never orphaned/fresh (they also never receive `orphaned` frontmatter).\n if (page.pageDirectory === \"queries\") return \"unverified\";\n if (page.frontmatter.orphaned === true) return \"orphaned\";\n\n const owners = ownersOf(page.slug, snapshot);\n if (owners.length === 0) return \"unverified\";\n\n const live = owners.filter((o) => o.exists);\n if (live.length === 0) return \"orphaned\";\n if (live.length < owners.length) return \"stale\";\n if (live.some((o) => o.currentHash !== o.recordedHash)) return \"stale\";\n return \"fresh\";\n}\n\n/** Sources whose recorded concept set includes this page's slug (state is authoritative). */\nfunction ownersOf(slug: string, snapshot: FreshnessSnapshot) {\n return Object.values(snapshot.sources).filter((s) => s.concepts.includes(slug));\n}\n\n/**\n * Build the per-run freshness snapshot: one read-only state read + one hash\n * pass over sources/. Shared by every freshness consumer so hashing happens once.\n *\n * @param classified - Optional pre-read classified state. When supplied the\n * disk read is skipped, letting callers that already have state avoid a\n * redundant read. Omit (or pass `undefined`) to read from disk as usual.\n */\nexport async function buildFreshnessSnapshot(\n root: string,\n classified?: ClassifiedState,\n): Promise<FreshnessSnapshot> {\n const { status, state } = classified ?? (await readStateClassified(root));\n const sources: FreshnessSnapshot[\"sources\"] = {};\n if (status === \"ok\") {\n const sourcesRoot = path.resolve(root, SOURCES_DIR);\n // Resolve the real sources root ONCE — the per-source containment check reuses it,\n // so a snapshot over N sources does at most N (not 2N) realpath syscalls.\n const realSourcesRoot = await realSourcesRootOrFallback(sourcesRoot);\n for (const [file, entry] of Object.entries(state.sources)) {\n sources[file] = await classifySource(sourcesRoot, realSourcesRoot, file, entry);\n }\n }\n return { stateStatus: status, sources };\n}\n\n/**\n * Real path of the sources root, falling back to the lexical path if realpath\n * throws (e.g. sources/ does not exist yet). The fallback is safe: a missing\n * sources root means no source file can resolve inside it, and per-file\n * existsSync already gates hashing.\n */\nasync function realSourcesRootOrFallback(sourcesRoot: string): Promise<string> {\n try {\n return await realpath(sourcesRoot);\n } catch {\n return sourcesRoot;\n }\n}\n\n/**\n * Resolve a file's symlinks and check whether its real path is inside the\n * pre-resolved real sources root. The root is realpath'd once by the caller so\n * projects under symlinked temp dirs (e.g. macOS /tmp → /private/tmp) still pass.\n * Returns false on any error (treat as not inside, refuse to hash).\n */\nasync function resolvesInsideSources(resolved: string, realSourcesRoot: string): Promise<boolean> {\n try {\n const realFile = await realpath(resolved);\n return realFile === realSourcesRoot || realFile.startsWith(realSourcesRoot + path.sep);\n } catch {\n return false;\n }\n}\n\n/**\n * Classify one source entry from state.json, guarding against both path traversal\n * and symlink escapes. A poisoned key like `../../etc/passwd` is rejected by the\n * cheap lexical check (first gate). A symlink inside sources/ pointing outside is\n * rejected by the realpath check (second gate). Both cases return exists:false.\n */\nasync function classifySource(\n sourcesRoot: string,\n realSourcesRoot: string,\n file: string,\n entry: { hash: string; concepts: string[] },\n): Promise<SourceFreshness> {\n const resolved = path.resolve(sourcesRoot, file);\n const lexicallyInside = resolved === sourcesRoot || resolved.startsWith(sourcesRoot + path.sep);\n if (!lexicallyInside) {\n return { recordedHash: entry.hash, currentHash: null, exists: false, concepts: entry.concepts };\n }\n const exists = existsSync(resolved);\n if (exists && !(await resolvesInsideSources(resolved, realSourcesRoot))) {\n // Symlink escapes sources/ — treat as unverifiable, never hash/read it.\n return { recordedHash: entry.hash, currentHash: null, exists: false, concepts: entry.concepts };\n }\n return {\n recordedHash: entry.hash,\n currentHash: exists ? await hashFile(resolved) : null,\n exists,\n concepts: entry.concepts,\n };\n}\n","/**\n * Wiki page rendering for the llmwiki compile pipeline.\n *\n * Encapsulates the single-page generation step: gather related pages, call\n * the LLM, build frontmatter, and produce the final markdown blob. Splitting\n * this away from the orchestrator (`compiler/index.ts`) keeps the orchestrator\n * focused on phase sequencing and lets the review-candidate code path reuse\n * the exact same renderer used for direct writes.\n */\n\nimport { readdir } from \"fs/promises\";\nimport path from \"path\";\nimport {\n buildFrontmatter,\n parseFrontmatter,\n safeReadFile,\n} from \"../utils/markdown.js\";\nimport { callClaude } from \"../utils/llm.js\";\nimport { buildPagePrompt } from \"./prompts.js\";\nimport { addObsidianMeta } from \"./obsidian.js\";\nimport { addModelProvenanceMeta, addProvenanceMeta, reportContradictionWarnings } from \"./provenance.js\";\nimport { CONCEPTS_DIR } from \"../utils/constants.js\";\nimport type { SchemaConfig } from \"../schema/index.js\";\nimport type { ExtractedConcept } from \"../utils/types.js\";\n\n/** Maximum number of existing concept pages to include as cross-reference context. */\nconst RELATED_PAGE_CONTEXT_LIMIT = 5;\n\n/** A merged-concept input from the orchestrator (multiple sources merged into one). */\ninterface RenderableConcept {\n slug: string;\n concept: ExtractedConcept;\n sourceFiles: string[];\n combinedContent: string;\n}\n\n/**\n * Render a wiki page (frontmatter + body) for a merged concept by calling\n * the LLM with cross-referencing context from existing concept pages.\n * @param root - Project root directory.\n * @param entry - The merged concept to render.\n * @param schema - Resolved schema config, used to stamp `kind` on frontmatter.\n * @returns Full markdown content (frontmatter + body, trailing newline).\n */\nexport async function renderMergedPageContent(\n root: string,\n entry: RenderableConcept,\n schema: SchemaConfig,\n): Promise<string> {\n const pagePath = path.join(root, CONCEPTS_DIR, `${entry.slug}.md`);\n const existingPage = await safeReadFile(pagePath);\n const relatedPages = await loadRelatedPages(root, entry.slug);\n\n const system = buildPagePrompt(\n entry.concept.concept,\n entry.combinedContent,\n existingPage,\n relatedPages,\n );\n\n const pageBody = await callClaude({\n system,\n messages: [\n { role: \"user\", content: `Write the wiki page for \"${entry.concept.concept}\".` },\n ],\n });\n\n const frontmatter = buildMergedFrontmatter(entry, existingPage, schema);\n reportContradictionWarnings(entry.concept.concept, entry.concept);\n return `${frontmatter}\\n\\n${pageBody}\\n`;\n}\n\n/**\n * Construct the frontmatter block for a merged concept, preserving createdAt\n * and stamping the `kind` field from the schema's default kind.\n */\nfunction buildMergedFrontmatter(\n entry: RenderableConcept,\n existingPage: string,\n schema: SchemaConfig,\n): string {\n const now = new Date().toISOString();\n const existing = existingPage ? parseFrontmatter(existingPage) : null;\n const createdAt = (existing?.meta.createdAt && typeof existing.meta.createdAt === \"string\")\n ? existing.meta.createdAt\n : now;\n const frontmatterFields: Record<string, unknown> = {\n title: entry.concept.concept,\n summary: entry.concept.summary,\n sources: entry.sourceFiles,\n kind: schema.defaultKind,\n createdAt,\n updatedAt: now,\n };\n addObsidianMeta(frontmatterFields, entry.concept.concept, entry.concept.tags ?? []);\n addProvenanceMeta(frontmatterFields, entry.concept);\n addModelProvenanceMeta(frontmatterFields);\n return buildFrontmatter(frontmatterFields);\n}\n\n/**\n * Load related wiki pages to provide cross-referencing context.\n * Returns concatenated content of up to RELATED_PAGE_CONTEXT_LIMIT pages.\n * @param root - Project root directory.\n * @param excludeSlug - Slug of the current page to exclude.\n * @returns Concatenated related page contents (empty when concepts dir is missing).\n */\nasync function loadRelatedPages(root: string, excludeSlug: string): Promise<string> {\n const conceptsPath = path.join(root, CONCEPTS_DIR);\n let files: string[];\n\n try {\n files = await readdir(conceptsPath);\n } catch {\n return \"\";\n }\n\n const related = files\n .filter((f) => f.endsWith(\".md\") && f !== `${excludeSlug}.md`)\n .slice(0, RELATED_PAGE_CONTEXT_LIMIT);\n\n const contents: string[] = [];\n for (const f of related) {\n const content = await safeReadFile(path.join(conceptsPath, f));\n if (!content) continue;\n const { meta } = parseFrontmatter(content);\n if (meta.orphaned) continue;\n contents.push(content);\n }\n\n return contents.join(\"\\n\\n---\\n\\n\");\n}\n","/**\n * Commander action for `llmwiki query <question>`.\n * Two-step LLM-powered wiki query that first selects relevant pages from the\n * wiki index, then streams an answer grounded in those pages. Optionally saves\n * the response as a new page in wiki/queries/.\n *\n * Step 1 - Page Selection: Reads wiki/index.md and asks Claude (via tool_use)\n * to pick the most relevant concept pages for the question.\n *\n * Step 2 - Answer Generation: Loads the selected pages in full and streams\n * a cited answer to the terminal.\n */\n\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { callClaude } from \"../utils/llm.js\";\nimport type { LLMTool } from \"../utils/provider.js\";\nimport { atomicWrite, safeReadFile, slugify, buildFrontmatter, parseFrontmatter } from \"../utils/markdown.js\";\nimport { languageDirective } from \"../utils/output-language.js\";\nimport { generateIndex } from \"../compiler/indexgen.js\";\nimport * as output from \"../utils/output.js\";\nimport {\n QUERY_PAGE_LIMIT,\n INDEX_FILE,\n CONCEPTS_DIR,\n QUERIES_DIR,\n CHUNK_TOP_K,\n CHUNK_RERANK_KEEP,\n} from \"../utils/constants.js\";\nimport {\n findRelevantPages,\n findRelevantChunks,\n updateEmbeddings,\n type ChunkEmbeddingEntry,\n} from \"../utils/embeddings.js\";\nimport { rerankWithBm25 } from \"../utils/retrieval.js\";\nimport { appendLog, formatWikilinkList } from \"../utils/activity-log.js\";\nimport type { ChunkCitation, QueryResult, RetrievalDebug } from \"../utils/types.js\";\n\n/** Directories to search when loading selected pages, in priority order. */\nconst PAGE_DIRS = [CONCEPTS_DIR, QUERIES_DIR];\n\n/** Tool schema for page selection (provider-agnostic). */\nconst PAGE_SELECTION_TOOL: LLMTool = {\n name: \"select_pages\",\n description: \"Select the most relevant wiki pages to answer a question\",\n input_schema: {\n type: \"object\" as const,\n properties: {\n pages: {\n type: \"array\",\n items: {\n type: \"string\",\n description: \"Slug of a relevant wiki page (e.g. 'llm-knowledge-bases')\",\n },\n maxItems: QUERY_PAGE_LIMIT,\n },\n reasoning: {\n type: \"string\",\n description: \"Brief explanation of why these pages were selected\",\n },\n },\n required: [\"pages\", \"reasoning\"],\n },\n};\n\ninterface PageSelectionResult {\n pages: string[];\n reasoning: string;\n}\n\n/**\n * Select the most relevant wiki pages for a question using Claude tool_use.\n * @param question - The user's natural language question.\n * @param indexContent - The full text of wiki/index.md.\n * @returns Parsed page slugs and reasoning from Claude.\n */\nexport async function selectPages(\n question: string,\n indexContent: string,\n): Promise<PageSelectionResult> {\n const systemPrompt =\n \"You are a knowledge base assistant. Given a question and a wiki index, select the most relevant pages.\";\n\n const userMessage = `Question: ${question}\\n\\nWiki Index:\\n${indexContent}`;\n\n const rawResult = await callClaude({\n system: systemPrompt,\n messages: [{ role: \"user\", content: userMessage }],\n tools: [PAGE_SELECTION_TOOL],\n });\n\n try {\n const parsed = JSON.parse(rawResult);\n return {\n pages: Array.isArray(parsed.pages) ? parsed.pages.filter((p: unknown) => typeof p === \"string\") : [],\n reasoning: typeof parsed.reasoning === \"string\" ? parsed.reasoning : \"No reasoning provided\",\n };\n } catch {\n return { pages: [], reasoning: \"Failed to parse page selection response\" };\n }\n}\n\n/** Render a list of candidate pages in the same bullet format selectPages() consumes. */\nfunction buildFilteredIndex(\n candidates: Array<{ slug: string; title: string; summary: string }>,\n): string {\n return candidates\n .map((entry) => `- **${entry.slug}**: ${entry.title} — ${entry.summary}`)\n .join(\"\\n\");\n}\n\ninterface SelectedPages {\n pages: string[];\n rawPages: string[];\n reasoning: string;\n /** Chunk citations driving the selection — empty when chunk store is absent. */\n chunks: ChunkCitation[];\n /** Debug snapshot of the retrieval pipeline (only populated in debug mode). */\n debug?: RetrievalDebug;\n}\n\n/**\n * Pick relevant pages using a chunk-aware embedding pre-filter when available,\n * falling back to page-level embeddings, then to sending the full wiki index.\n */\nasync function selectRelevantPages(\n root: string,\n question: string,\n debug: boolean,\n): Promise<SelectedPages> {\n const chunkSelection = await trySelectViaChunks(root, question, debug);\n if (chunkSelection) return chunkSelection;\n\n const candidates = await tryFindRelevantPages(root, question);\n\n if (candidates.length > 0) {\n const filteredIndex = buildFilteredIndex(candidates);\n const { pages: rawPages, reasoning } = await selectPages(question, filteredIndex);\n // Tool output holds slugs directly in the semantic path — no slugify needed.\n return { pages: rawPages, rawPages, reasoning, chunks: [] };\n }\n\n const indexContent = await safeReadFile(path.join(root, INDEX_FILE));\n const { pages: rawPages, reasoning } = await selectPages(question, indexContent);\n return { pages: rawPages.map((p) => slugify(p)), rawPages, reasoning, chunks: [] };\n}\n\n/**\n * Attempt chunk-level retrieval + reranking. Returns null when no chunk store\n * is available (caller falls back to page-level retrieval transparently).\n */\nasync function trySelectViaChunks(\n root: string,\n question: string,\n debug: boolean,\n): Promise<SelectedPages | null> {\n const ranked = await tryFindRelevantChunks(root, question);\n if (ranked.length === 0) return null;\n\n const reranked = rerankWithBm25(\n question,\n ranked.map(({ chunk, score }) => ({ text: chunk.text, baseScore: score, chunk })),\n );\n const kept = reranked.slice(0, CHUNK_RERANK_KEEP);\n const reorderingHappened = wasReordered(ranked, kept.map((k) => k.candidate.chunk));\n const chunkCitations = toChunkCitations(kept);\n const pageSlugs = collapseToPages(chunkCitations, QUERY_PAGE_LIMIT);\n const reasoning = buildChunkReasoning(chunkCitations, pageSlugs);\n\n return {\n pages: pageSlugs,\n rawPages: pageSlugs,\n reasoning,\n chunks: chunkCitations,\n debug: debug ? buildDebug(chunkCitations, pageSlugs, reorderingHappened) : undefined,\n };\n}\n\n/** Detect whether reranking actually changed the chunk order. */\nfunction wasReordered(\n before: Array<{ chunk: ChunkEmbeddingEntry }>,\n after: ChunkEmbeddingEntry[],\n): boolean {\n const limit = Math.min(before.length, after.length);\n for (let i = 0; i < limit; i++) {\n if (before[i].chunk !== after[i]) return true;\n }\n return false;\n}\n\ninterface RankedChunk {\n candidate: { chunk: ChunkEmbeddingEntry };\n score: number;\n}\n\n/** Convert reranked candidates into citation records consumed downstream. */\nfunction toChunkCitations(ranked: RankedChunk[]): ChunkCitation[] {\n return ranked.map(({ candidate, score }) => ({\n slug: candidate.chunk.slug,\n title: candidate.chunk.title,\n chunkIndex: candidate.chunk.chunkIndex,\n score,\n text: candidate.chunk.text,\n }));\n}\n\n/** Collapse chunk citations down to a deduplicated list of parent page slugs. */\nfunction collapseToPages(chunks: ChunkCitation[], limit: number): string[] {\n const slugs: string[] = [];\n const seen = new Set<string>();\n for (const chunk of chunks) {\n if (seen.has(chunk.slug)) continue;\n seen.add(chunk.slug);\n slugs.push(chunk.slug);\n if (slugs.length >= limit) break;\n }\n return slugs;\n}\n\n/** Human-readable reasoning trail for the chunk-driven selection. */\nfunction buildChunkReasoning(chunks: ChunkCitation[], pages: string[]): string {\n const top = chunks.slice(0, pages.length);\n const summary = top.map((c) => `${c.slug}#${c.chunkIndex} (${c.score.toFixed(3)})`).join(\", \");\n return `Selected ${pages.length} page(s) from ${chunks.length} reranked chunks: ${summary}`;\n}\n\n/** Snapshot used by debug mode — pure data, no side-effects. */\nfunction buildDebug(\n chunks: ChunkCitation[],\n pageSlugs: string[],\n reranked: boolean,\n): RetrievalDebug {\n const bestPerPage = new Map<string, number>();\n for (const c of chunks) {\n const prev = bestPerPage.get(c.slug);\n if (prev === undefined || c.score > prev) bestPerPage.set(c.slug, c.score);\n }\n return {\n pages: pageSlugs.map((slug) => ({ slug, score: bestPerPage.get(slug) ?? 0 })),\n chunks,\n usedChunks: true,\n reranked,\n };\n}\n\n/** Chunk-level candidate lookup that never throws. */\nasync function tryFindRelevantChunks(\n root: string,\n question: string,\n): Promise<Array<{ chunk: ChunkEmbeddingEntry; score: number }>> {\n try {\n return await findRelevantChunks(root, question, CHUNK_TOP_K);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n output.status(\"!\", output.dim(`Chunk pre-filter unavailable (${message}); falling back.`));\n return [];\n }\n}\n\n/** Embedding-based candidate lookup that never throws. */\nasync function tryFindRelevantPages(\n root: string,\n question: string,\n): Promise<Array<{ slug: string; title: string; summary: string }>> {\n try {\n return await findRelevantPages(root, question);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n output.status(\"!\", output.dim(`Semantic pre-filter unavailable (${message}); using full index.`));\n return [];\n }\n}\n\n/**\n * Load the full content of each selected wiki page.\n * Skips pages that don't exist and warns the user.\n * @param root - Absolute path to the project root directory.\n * @param slugs - Array of page slugs to load from wiki/concepts/.\n * @returns Combined page contents with slug headers for context.\n */\nexport async function loadSelectedPages(root: string, slugs: string[]): Promise<string> {\n const sections: string[] = [];\n\n for (const slug of slugs) {\n let content = \"\";\n for (const dir of PAGE_DIRS) {\n const candidate = await safeReadFile(path.join(root, dir, `${slug}.md`));\n if (!candidate) continue;\n const { meta } = parseFrontmatter(candidate);\n if (meta.orphaned) continue;\n content = candidate;\n break;\n }\n\n if (!content) {\n output.status(\"?\", output.warn(`Page not found: ${slug}.md — skipping`));\n continue;\n }\n\n sections.push(`--- Page: ${slug} ---\\n${content}`);\n }\n\n return sections.join(\"\\n\\n\");\n}\n\n/** Base system prompt body. The output-language directive is appended at call time. */\nconst ANSWER_SYSTEM_PROMPT_BASE =\n \"You are a knowledge assistant. Answer the question using ONLY the wiki content provided. \" +\n \"Cite specific pages using [[Page Title]] wikilinks. \" +\n \"If the wiki doesn't contain enough information, say so.\";\n\n/**\n * Build the answer-generation system prompt, appending the configured\n * output-language directive when present (issue #37).\n */\nfunction buildAnswerSystemPrompt(): string {\n const lang = languageDirective();\n return lang ? `${ANSWER_SYSTEM_PROMPT_BASE} ${lang}` : ANSWER_SYSTEM_PROMPT_BASE;\n}\n\n/**\n * Call the LLM with the loaded wiki pages as grounding context. When chunk\n * citations are available, they are attached as a \"Most relevant excerpts\"\n * section so the model can prioritise the precise paragraphs that drove\n * page selection.\n */\nasync function callAnswerLLM(\n question: string,\n pagesContent: string,\n chunks: ChunkCitation[],\n onToken?: (text: string) => void,\n): Promise<string> {\n const provenance = chunks.length > 0 ? buildChunkProvenance(chunks) : \"\";\n const userMessage =\n `Question: ${question}\\n\\nRelevant wiki pages:\\n${pagesContent}${provenance}`;\n return callClaude({\n system: buildAnswerSystemPrompt(),\n messages: [{ role: \"user\", content: userMessage }],\n stream: Boolean(onToken),\n onToken,\n });\n}\n\n/** Render the top chunk excerpts as a labelled section appended to the prompt. */\nfunction buildChunkProvenance(chunks: ChunkCitation[]): string {\n const sections = chunks.map(\n (chunk) => `--- ${chunk.slug} (chunk ${chunk.chunkIndex}) ---\\n${chunk.text}`,\n );\n return `\\n\\nMost relevant excerpts (from chunk-level retrieval):\\n${sections.join(\"\\n\\n\")}`;\n}\n\n/**\n * Generate a one-line summary from the answer for use in the wiki index.\n * Takes the first sentence (up to 120 chars) so the page-selection LLM\n * has retrieval signal beyond just the title.\n * @param answer - The full answer text.\n * @returns A short summary string.\n */\nexport function summarizeAnswer(answer: string): string {\n const firstLine = answer.trim().split(/\\n/)[0] ?? \"\";\n const firstSentence = firstLine.split(/(?<=[.!?])\\s/)[0] ?? firstLine;\n return firstSentence.slice(0, 120);\n}\n\n/**\n * Save a query answer as a wiki page in the queries/ directory,\n * then regenerate the wiki index so the answer is immediately retrievable.\n * @param root - Absolute path to the project root directory.\n * @param question - The original question used as the page title.\n * @param answer - The generated answer body.\n */\nasync function saveQueryPage(root: string, question: string, answer: string): Promise<string> {\n const slug = slugify(question);\n const filePath = path.join(root, QUERIES_DIR, `${slug}.md`);\n\n const frontmatter = buildFrontmatter({\n title: question,\n summary: summarizeAnswer(answer),\n type: \"query\",\n createdAt: new Date().toISOString(),\n });\n\n const document = `${frontmatter}\\n\\n${answer}\\n`;\n await atomicWrite(filePath, document);\n\n output.status(\n \"+\",\n output.success(`Saved query → ${output.source(filePath)}`),\n );\n\n // Regenerate the index so the saved query is immediately discoverable\n // by the next query's page-selection step.\n await generateIndex(root);\n\n // Index the new query so semantic search retrieves it on the next question.\n // Non-critical: embedding failures (e.g. missing VOYAGE_API_KEY) don't block save.\n try {\n await updateEmbeddings(root, [slug]);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n output.status(\"!\", output.warn(`Skipped embeddings update: ${message}`));\n }\n\n return slug;\n}\n\n/** Options for generateAnswer — programmatic-friendly. */\ninterface GenerateAnswerOptions {\n /** Persist the answer as a wiki query page when set. */\n save?: boolean;\n /** Per-token callback for streaming. Omit for non-streaming usage. */\n onToken?: (text: string) => void;\n /** Callback fired once page selection completes — lets CLIs print reasoning before streaming. */\n onPageSelection?: (pages: string[], reasoning: string) => void;\n /** Capture chunk-level provenance + scoring detail in the result. */\n debug?: boolean;\n}\n\n/**\n * Run the two-step page-selection + answer-generation pipeline and return\n * a structured QueryResult. This is the programmatic entry point used by\n * the MCP server and any non-CLI consumer.\n *\n * @param root - Absolute path to the project root directory.\n * @param question - The natural language question to answer.\n * @param options - Streaming + save behaviour controls.\n * @returns Answer text, selected slugs, reasoning, and saved slug if applicable.\n */\nexport async function generateAnswer(\n root: string,\n question: string,\n options: GenerateAnswerOptions = {},\n): Promise<QueryResult> {\n if (!existsSync(path.join(root, INDEX_FILE))) {\n throw new Error(\"Wiki index not found. Run `llmwiki compile` first.\");\n }\n\n const selection = await selectRelevantPages(root, question, Boolean(options.debug));\n options.onPageSelection?.(selection.pages, selection.reasoning);\n\n const pagesContent = await loadSelectedPages(root, selection.pages);\n\n if (!pagesContent) {\n return buildEmptyResult(selection);\n }\n\n const answer = await callAnswerLLM(question, pagesContent, selection.chunks, options.onToken);\n const saved = options.save ? await saveQueryPage(root, question, answer) : undefined;\n\n // Journal the query only after the answer is produced — matching compile,\n // which logs after finalization — so a mid-flight LLM failure records nothing.\n // Logged here (not in the CLI command) so MCP queries are captured too.\n await appendLog(root, \"query\", question, {\n details: selection.pages.length > 0 ? [`Pages: ${formatWikilinkList(selection.pages)}`] : [],\n });\n\n return {\n answer,\n selectedPages: selection.pages,\n reasoning: selection.reasoning,\n saved,\n debug: selection.debug,\n };\n}\n\n/** Build the empty-pages result while preserving any debug/chunk context. */\nfunction buildEmptyResult(selection: SelectedPages): QueryResult {\n return {\n answer: \"\",\n selectedPages: selection.pages,\n reasoning: selection.reasoning,\n debug: selection.debug,\n };\n}\n\n/**\n * Run a two-step LLM-powered query against the knowledge wiki.\n * @param root - Absolute path to the project root directory.\n * @param question - The natural language question to answer.\n * @param options - Command options (e.g. --save to persist the answer).\n */\nexport default async function queryCommand(\n root: string,\n question: string,\n options: { save?: boolean; debug?: boolean },\n): Promise<void> {\n if (!existsSync(path.join(root, INDEX_FILE))) {\n output.status(\"!\", output.error(\"Wiki index not found. Run `llmwiki compile` first.\"));\n return;\n }\n\n output.header(\"Selecting relevant pages\");\n\n const result = await generateAnswer(root, question, {\n save: options.save,\n debug: options.debug,\n onToken: (text) => process.stdout.write(text),\n onPageSelection: (pages, reasoning) => {\n output.status(\"i\", output.dim(`Reasoning: ${reasoning}`));\n output.status(\"*\", output.info(`Selected ${pages.length} page(s): ${pages.join(\", \")}`));\n output.header(\"Generating answer\");\n },\n });\n\n // Newline after streamed answer so subsequent terminal output formats cleanly.\n process.stdout.write(\"\\n\");\n\n if (result.debug) printDebugSnapshot(result.debug);\n\n if (!result.answer) {\n output.status(\"!\", output.error(\"No matching pages found. Try refining your question.\"));\n return;\n }\n\n if (result.saved) {\n output.status(\"→\", output.dim(\"Saved. Future queries will use this answer as context.\"));\n } else {\n output.status(\"→\", output.dim(\"Tip: use --save to add this answer to your wiki\"));\n }\n}\n\n/** Render the retrieval debug snapshot to the terminal for human inspection. */\nfunction printDebugSnapshot(debug: RetrievalDebug): void {\n output.header(\"Retrieval debug\");\n output.status(\n \"i\",\n output.dim(\n `Source: ${debug.usedChunks ? \"chunk-level\" : \"page-level\"}; ` +\n `reranked: ${debug.reranked ? \"yes\" : \"no\"}`,\n ),\n );\n for (const page of debug.pages) {\n output.status(\"•\", `${page.slug} (best chunk score ${page.score.toFixed(3)})`);\n }\n for (const chunk of debug.chunks) {\n const preview = chunk.text.slice(0, DEBUG_CHUNK_PREVIEW_CHARS).replace(/\\s+/g, \" \").trim();\n output.status(\n \"·\",\n output.dim(`${chunk.slug}#${chunk.chunkIndex} score=${chunk.score.toFixed(3)} :: ${preview}…`),\n );\n }\n}\n\n/** Maximum chunk preview length printed in --debug output. */\nconst DEBUG_CHUNK_PREVIEW_CHARS = 120;\n","/**\n * Wiki linter orchestrator.\n *\n * Imports all lint rules, runs them concurrently, and aggregates\n * results into a summary with error/warning/info counts.\n * This is the main entry point for programmatic lint access.\n */\n\nimport type { LintResult, LintRule, LintSummary, SchemaAwareLintRule } from \"./types.js\";\nimport {\n checkBrokenWikilinks,\n checkOrphanedPages,\n checkMissingSummaries,\n checkDuplicateConcepts,\n checkEmptyPages,\n checkBrokenCitations,\n checkMalformedClaimCitations,\n checkLowConfidencePages,\n checkContradictedPages,\n checkInferredWithoutCitations,\n checkSchemaCrossLinks,\n checkStalePages,\n} from \"./rules.js\";\nimport { loadSchema } from \"../schema/index.js\";\nimport { buildFreshnessSnapshot } from \"../freshness/index.js\";\nimport type { FreshnessSnapshot } from \"../freshness/types.js\";\n\n/** Rule-only lint checks that don't depend on the schema layer. */\nconst RULES_WITHOUT_SCHEMA: LintRule[] = [\n checkBrokenWikilinks,\n checkOrphanedPages,\n checkMissingSummaries,\n checkDuplicateConcepts,\n checkEmptyPages,\n checkBrokenCitations,\n checkMalformedClaimCitations,\n checkLowConfidencePages,\n checkContradictedPages,\n checkInferredWithoutCitations,\n];\n\n/** Lint rules that need the resolved schema to know per-kind expectations. */\nconst RULES_WITH_SCHEMA: SchemaAwareLintRule[] = [checkSchemaCrossLinks];\n\ntype FreshnessLintRule = (root: string, snapshot: FreshnessSnapshot) => Promise<LintResult[]>;\nconst RULES_WITH_FRESHNESS: FreshnessLintRule[] = [checkStalePages];\n\n/**\n * Count occurrences of a specific severity level in the results.\n */\nfunction countBySeverity(\n results: LintResult[],\n severity: LintResult[\"severity\"],\n): number {\n return results.filter((r) => r.severity === severity).length;\n}\n\n/**\n * Run all lint rules concurrently against the wiki at the given root.\n * Loads the project schema (or defaults) so schema-aware rules can enforce\n * per-kind cross-link minimums alongside structural checks.\n * @param root - Absolute path to the project root directory.\n * @returns A summary containing all diagnostics and severity counts.\n */\nexport async function lint(root: string): Promise<LintSummary> {\n const schema = await loadSchema(root);\n const freshness = await buildFreshnessSnapshot(root);\n const [plainResults, schemaResults, freshnessResults] = await Promise.all([\n Promise.all(RULES_WITHOUT_SCHEMA.map((rule) => rule(root))),\n Promise.all(RULES_WITH_SCHEMA.map((rule) => rule(root, schema))),\n Promise.all(RULES_WITH_FRESHNESS.map((rule) => rule(root, freshness))),\n ]);\n\n const results = [...plainResults.flat(), ...schemaResults.flat(), ...freshnessResults.flat()];\n\n const summary: LintSummary = {\n errors: countBySeverity(results, \"error\"),\n warnings: countBySeverity(results, \"warning\"),\n info: countBySeverity(results, \"info\"),\n results,\n };\n\n // lint is intentionally not journaled to log.md — it is a read-only check,\n // and the MCP `lint_wiki` tool documents it as non-mutating.\n return summary;\n}\n","/**\n * Build the frozen-at-startup `ViewerSnapshot` consumed by every viewer\n * endpoint. Every count, page list, and index payload that the HTTP\n * layer needs is captured here exactly once — v1 deliberately does not\n * live-watch the filesystem, so post-startup mutations are intentionally\n * invisible to the running viewer until it restarts.\n *\n * The snapshot consolidates five data sources:\n * - `collectViewerPages` for the decorated page list AND the\n * concept/query counts (deriving counts from the already-confined\n * page list means symlinked entries dropped by the collector\n * cannot quietly inflate the counts via a second unconfined scan)\n * - `readStateClassified` (read-only, never writes a `.bak`) for the\n * compiled-source count AND as the input to the freshness snapshot\n * - `buildFreshnessSnapshot` for per-page freshness and the aggregate\n * stale/orphaned counts (one state read + one hash pass over sources/)\n * - `countCandidates` for the pending-reviews count\n * - `readdir(sources/)` for the cheap source-file count\n */\n\nimport { readdir, readFile, realpath } from \"fs/promises\";\nimport path from \"path\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\nimport { countCandidates } from \"../compiler/candidates.js\";\nimport { readStateClassified } from \"../utils/state.js\";\nimport { collectViewerPages, resolveBareSlugList } from \"./collect.js\";\nimport { extractWikilinkSlugs } from \"../wiki/collect.js\";\nimport { isMalformedCitationEntry } from \"../utils/markdown.js\";\nimport { buildGraphData } from \"./graph.js\";\nimport { buildFreshnessSnapshot, computeFreshness } from \"../freshness/index.js\";\nimport type { FreshnessSnapshot } from \"../freshness/types.js\";\nimport type {\n ViewerCounts,\n ViewerIndex,\n ViewerPage,\n ViewerProject,\n ViewerRecentPage,\n ViewerSnapshot,\n ViewerWarning,\n} from \"./types.js\";\n\nconst RECENT_PAGES_LIMIT = 8;\nconst INDEX_HREF = \"/#/index\";\n\n/**\n * Build the immutable startup snapshot for a project root. Reads pages,\n * counts, source state, candidates, and the optional `wiki/index.md`\n * exactly once and returns a fully populated `ViewerSnapshot`. Callers\n * must NOT re-derive any of these from disk on a per-request path —\n * `readLintCache` in `src/viewer/health.ts` is the sole exception.\n */\nexport async function buildViewerSnapshot(root: string): Promise<ViewerSnapshot> {\n const [pages, classified, pendingReviews, sourceFilenames, index] = await Promise.all([\n collectViewerPages(root),\n readStateClassified(root),\n countCandidates(root),\n listSourceFiles(root),\n readIndexFile(root),\n ]);\n const freshnessSnapshot = await buildFreshnessSnapshot(root, classified);\n const project = buildProject(root);\n const fullIndex: ViewerIndex = {\n available: index.available,\n href: INDEX_HREF,\n body: index.body,\n outgoingLinks: resolveBareSlugList(extractWikilinkSlugs(index.body), pages),\n };\n const sourceFileSet = new Set(sourceFilenames);\n // Concept/query counts are derived from `pages`, the already-confined\n // viewer page list, NOT from a second unconfined directory scan.\n // Anything the collector dropped for path-safety reasons (symlinked\n // file or directory) is therefore also excluded from the counts.\n const annotatedPages = pages\n .map((page) => annotateCitationWarnings(page, sourceFileSet))\n .map((page) => attachFreshness(page, freshnessSnapshot));\n const counts = buildCounts(annotatedPages, sourceFilenames, pendingReviews, classified.state);\n const graph = buildGraphData(annotatedPages);\n return {\n root,\n generatedAt: new Date().toISOString(),\n stateStatus: classified.status,\n project,\n counts,\n index: fullIndex,\n recentPages: buildRecentPages(annotatedPages),\n pages: annotatedPages,\n sourceFilenames,\n graph,\n };\n}\n\n/**\n * Append `unresolved_citation` and `malformed_citation` warnings to a\n * page based on its parsed citations and the project's source-file\n * list. Slice 1 only produced parser-level warnings; citation\n * resolvability needs the snapshot's source-file list, so this is the\n * earliest layer that can decide.\n *\n * The body is re-scanned for raw `^[…]` markers (rather than iterating\n * `page.citations`) because `extractClaimCitations` drops citations\n * whose ONLY entry has an invalid line range — but those still need a\n * `malformed_citation` warning. Scanning the body gives every marker a\n * chance to be classified.\n */\nfunction annotateCitationWarnings(page: ViewerPage, sourceFiles: ReadonlySet<string>): ViewerPage {\n const extra: ViewerWarning[] = [];\n const markerPattern = /\\^\\[([^\\]\\n]+)\\]/g;\n let match: RegExpExecArray | null;\n while ((match = markerPattern.exec(page.body)) !== null) {\n appendCitationWarningsForMarker(match[1], sourceFiles, extra);\n }\n if (extra.length === 0) return page;\n return { ...page, warnings: [...page.warnings, ...extra] };\n}\n\n/**\n * Derive the frozen counts from the annotated page list, source filenames,\n * candidates count, and state. Concept/query counts are derived from pages\n * (the already-confined collector list) so symlinked drops don't inflate them.\n */\nfunction buildCounts(\n pages: ViewerPage[],\n sourceFilenames: string[],\n pendingReviews: number,\n state: { sources: Record<string, unknown> },\n): ViewerCounts {\n return {\n concepts: pages.filter((p) => p.pageDirectory === \"concepts\").length,\n queries: pages.filter((p) => p.pageDirectory === \"queries\").length,\n sourceFiles: sourceFilenames.length,\n pendingReviews,\n compiledSources: Object.keys(state.sources).length,\n stale: pages.filter((p) => p.freshness.freshnessStatus === \"stale\").length,\n orphaned: pages.filter((p) => p.freshness.freshnessStatus === \"orphaned\").length,\n };\n}\n\n/**\n * Attach computed source-freshness to a page. Called once per page during\n * snapshot build so freshness is frozen at startup alongside all other\n * snapshot data.\n */\nfunction attachFreshness(page: ViewerPage, snapshot: FreshnessSnapshot): ViewerPage {\n return {\n ...page,\n freshness: computeFreshness(\n { slug: page.slug, pageDirectory: page.pageDirectory, frontmatter: page.frontmatter },\n snapshot,\n ),\n };\n}\n\n/** Classify every comma-separated entry inside one `^[…]` marker. */\nfunction appendCitationWarningsForMarker(\n raw: string,\n sourceFiles: ReadonlySet<string>,\n into: ViewerWarning[],\n): void {\n for (const entry of raw.split(\",\")) {\n const trimmed = entry.trim();\n if (trimmed.length === 0) continue;\n if (isMalformedCitationEntry(trimmed)) {\n into.push({\n code: \"malformed_citation\",\n message: `Malformed citation entry: ${trimmed}`,\n });\n continue;\n }\n const file = trimmed.split(/[:#]/)[0];\n if (file.length > 0 && !sourceFiles.has(file)) {\n into.push({\n code: \"unresolved_citation\",\n message: `Source not found: ${file}`,\n });\n }\n }\n}\n\n\n/** Project title and bare directory name for the dashboard header. */\nfunction buildProject(root: string): ViewerProject {\n const rootName = path.basename(root);\n return { title: rootName, rootName };\n}\n\n/**\n * List filenames directly under `sources/`. Returns an empty array when\n * the directory is missing. The Slice 4 citation renderer uses this list\n * to mark each chip `data-resolved` without per-request directory scans;\n * `counts.sourceFiles` is the cheap `.length` of the same list.\n *\n * Stricter than \"stays under project root\": `realpath(<root>/sources)`\n * must equal the literal canonical path `<canonicalRoot>/sources`. A\n * symlinked `sources/` directory — even pointing in-root — returns an\n * empty list, matching the same containment posture the wiki collector\n * uses for `wiki/concepts/` and `wiki/queries/`. Symlinked entries\n * inside the directory are excluded by `Dirent.isFile()` (which returns\n * false for symlinks since `withFileTypes` does not follow them).\n */\nasync function listSourceFiles(root: string): Promise<string[]> {\n let canonicalRoot: string;\n try {\n canonicalRoot = await realpath(root);\n } catch {\n return [];\n }\n const expectedDir = path.join(canonicalRoot, SOURCES_DIR);\n let realDir: string;\n try {\n realDir = await realpath(expectedDir);\n } catch {\n return [];\n }\n if (realDir !== expectedDir) return [];\n try {\n const entries = await readdir(realDir, { withFileTypes: true });\n return entries.filter((e) => e.isFile()).map((e) => e.name);\n } catch {\n return [];\n }\n}\n\n/**\n * Read `wiki/index.md` if present. Missing index is not an error: many\n * projects compile without an index page, and the viewer renders an\n * \"index unavailable\" placeholder for the `/#/index` route.\n *\n * Stricter than \"stays under project root\": `realpath(wiki/index.md)`\n * must equal the literal canonical path `<root>/wiki/index.md`. A\n * symlinked `wiki/index.md` is treated as unavailable, even when the\n * link target also lives inside the project — pointing the index at\n * (say) `<root>/README.md` would let the index endpoint render\n * content that has no business being the project's compiled index.\n * A symlinked `wiki/` directory is dropped by the same equality check.\n */\nasync function readIndexFile(root: string): Promise<{ available: boolean; body: string }> {\n let canonicalRoot: string;\n try {\n canonicalRoot = await realpath(root);\n } catch {\n return { available: false, body: \"\" };\n }\n const expectedIndex = path.join(canonicalRoot, \"wiki\", \"index.md\");\n let resolved: string;\n try {\n resolved = await realpath(expectedIndex);\n } catch {\n return { available: false, body: \"\" };\n }\n if (resolved !== expectedIndex) {\n return { available: false, body: \"\" };\n }\n try {\n const body = await readFile(resolved, \"utf-8\");\n return { available: true, body };\n } catch {\n return { available: false, body: \"\" };\n }\n}\n\n/**\n * Top-N recently updated pages for the dashboard. Pages without an\n * `updatedAt` frontmatter field sort to the end with an empty string so\n * the list remains deterministic.\n */\nfunction buildRecentPages(pages: ViewerPage[]): ViewerRecentPage[] {\n const rows: ViewerRecentPage[] = pages.map((page) => ({\n id: page.id,\n pageDirectory: page.pageDirectory,\n slug: page.slug,\n title: page.title,\n updatedAt:\n typeof page.frontmatter.updatedAt === \"string\" ? (page.frontmatter.updatedAt as string) : \"\",\n }));\n rows.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));\n return rows.slice(0, RECENT_PAGES_LIMIT);\n}\n\n","/**\n * Shared low-level wiki page collector.\n *\n * Walks `wiki/concepts/` and `wiki/queries/`, derives the slug from each\n * filename stem (NOT through `slugify()` — filename slugs are the canonical\n * filesystem-truth identifier; slugifying them would shift routes, exports,\n * and citation lookups), parses frontmatter via `parseFrontmatterStatus`,\n * and returns one `RawWikiPage` per readable `.md` file with a `parseStatus`\n * field describing structural problems.\n *\n * Content semantics: this layer does not drop pages for parse-level\n * failures (missing frontmatter, malformed YAML, missing title, orphaned\n * flag). Those are surfaced as `parseStatus` flags so the caller decides.\n *\n * Path-safety: this layer DOES drop entries that fail confinement to\n * their expected canonical directory. Specifically — a symlinked\n * `wiki/concepts/` directory (even pointing in-root), a symlinked\n * `.md` file whose `realpath` resolves anywhere other than under the\n * expected concepts/queries directory, and any unreadable entry — are\n * silently excluded. Two callers consume it:\n *\n * - `src/export/collect.ts` filters on `parseStatus.orphaned` and\n * `parseStatus.hasTitle` to preserve the existing export semantics.\n * - `src/viewer/collect.ts` retains every record and maps `parseStatus`\n * flags into `ViewerWarning` objects so users can diagnose malformed\n * pages in the UI.\n */\n\nimport { readdir, readFile } from \"fs/promises\";\nimport path from \"path\";\nimport { parseFrontmatterStatus, slugify } from \"../utils/markdown.js\";\nimport { CONCEPTS_DIR, QUERIES_DIR } from \"../utils/constants.js\";\nimport type { PageDirectory } from \"../export/types.js\";\nimport { safeRealpath, isInsideDir } from \"../utils/path-confine.js\";\n\n/** Regex that matches `[[wikilink]]` or `[[wikilink|alias]]` patterns. */\nconst WIKILINK_RE = /\\[\\[([^\\]|]+)(?:\\|([^\\]]+))?\\]\\]/g;\n\n/**\n * Structural status of a single page's frontmatter, surfaced to callers so\n * they can decide whether to filter, warn, or pass through.\n */\ninterface RawPageParseStatus {\n /** True when the file begins with a `---\\n…\\n---` block. */\n hasFrontmatterBlock: boolean;\n /** True when the frontmatter block exists but YAML failed to parse. */\n malformedFrontmatter: boolean;\n /** True when frontmatter contains a non-empty string `title`. */\n hasTitle: boolean;\n /** True when frontmatter explicitly sets `orphaned: true`. */\n orphaned: boolean;\n}\n\n/**\n * Raw page record returned by the shared collector. Lower-level than\n * `ExportPage` or `ViewerPage`: no decoration, no filtering, no warnings.\n */\nexport interface RawWikiPage {\n /** Filename stem (filename without the trailing `.md`). */\n slug: string;\n /** Which wiki/ subdirectory the page came from. */\n pageDirectory: PageDirectory;\n /** Absolute path on disk, useful for diagnostics and editor links. */\n filePath: string;\n /** Title from frontmatter when present; undefined otherwise. */\n title?: string;\n /** Parsed frontmatter (empty object when missing or malformed). */\n frontmatter: Record<string, unknown>;\n /** Markdown body with the frontmatter block stripped. */\n body: string;\n /** Structural status flags consumed by export and viewer callers. */\n parseStatus: RawPageParseStatus;\n}\n\n/**\n * Extract the slugs of all pages linked via `[[wikilinks]]` in the body.\n * Wikilink targets ARE slugified — the human-typed link text may not match\n * the on-disk filename verbatim, so we normalize to the same shape `slugify`\n * produces. Returns deduplicated targets.\n */\nexport function extractWikilinkSlugs(body: string): string[] {\n const slugs = new Set<string>();\n WIKILINK_RE.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = WIKILINK_RE.exec(body)) !== null) {\n slugs.add(slugify(match[1].trim()));\n }\n return [...slugs];\n}\n\n/**\n * Like `extractWikilinkSlugs` but also preserves the original human-typed\n * text for each target. Used to give dangling-link ghost nodes a readable\n * title instead of a slugified identifier.\n */\nexport function extractWikilinkTargets(body: string): { slug: string; display: string }[] {\n const seen = new Set<string>();\n const targets: { slug: string; display: string }[] = [];\n WIKILINK_RE.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = WIKILINK_RE.exec(body)) !== null) {\n const target = match[1].trim();\n const alias = match[2]?.trim();\n const slug = slugify(target);\n const display = alias ?? target;\n if (!seen.has(slug)) {\n seen.add(slug);\n targets.push({ slug, display });\n }\n }\n return targets;\n}\n\n/**\n * Parse a single markdown file into a `RawWikiPage`. Returns null only when\n * the file cannot be read — every other failure mode (missing frontmatter,\n * malformed YAML, missing title, orphaned flag) is preserved as a\n * `parseStatus` flag so the caller decides how to handle it.\n */\nasync function parsePageFile(\n filePath: string,\n slug: string,\n pageDirectory: PageDirectory,\n): Promise<RawWikiPage | null> {\n let raw: string;\n try {\n raw = await readFile(filePath, \"utf-8\");\n } catch {\n return null;\n }\n\n const { meta, body, hasFrontmatterBlock, malformedFrontmatter } = parseFrontmatterStatus(raw);\n const title = typeof meta.title === \"string\" && meta.title.length > 0 ? meta.title : undefined;\n return {\n slug,\n pageDirectory,\n filePath,\n title,\n frontmatter: meta,\n body,\n parseStatus: {\n hasFrontmatterBlock,\n malformedFrontmatter,\n hasTitle: title !== undefined,\n orphaned: meta.orphaned === true,\n },\n };\n}\n\n/**\n * Collect every readable `.md` file from a single wiki subdirectory.\n *\n * Confinement is stricter than \"stays under project root\": the\n * directory itself must resolve via `realpath` to the exact expected\n * path under `canonicalRoot` (so a symlinked `wiki/concepts/` is\n * skipped wholesale even when its target is also inside the project),\n * and each `.md` entry must resolve to a path under that canonical\n * expected directory (so a symlinked `wiki/concepts/leak.md` pointing\n * at `<root>/README.md` or `<root>/wiki/queries/x.md` is dropped).\n */\nasync function collectFromDir(\n canonicalRoot: string,\n pageDirectory: PageDirectory,\n subdir: string,\n): Promise<RawWikiPage[]> {\n const expectedDir = path.join(canonicalRoot, subdir);\n const realDir = await safeRealpath(expectedDir);\n if (realDir !== expectedDir) return [];\n let files: string[];\n try {\n files = await readdir(realDir);\n } catch {\n return [];\n }\n const pages: RawWikiPage[] = [];\n for (const file of files.filter((f) => f.endsWith(\".md\"))) {\n const candidate = path.join(realDir, file);\n const resolved = await safeRealpath(candidate);\n if (!resolved || !isInsideDir(resolved, realDir)) continue;\n const slug = file.replace(/\\.md$/, \"\");\n const page = await parsePageFile(resolved, slug, pageDirectory);\n if (page) pages.push(page);\n }\n return pages;\n}\n\n/**\n * Collect all readable wiki pages from `wiki/concepts/` and `wiki/queries/`.\n * Entries dropped for path-safety reasons (see `collectFromDir`) are\n * silently excluded. Pages are returned in filesystem order within each\n * directory, with concepts before queries; callers that need a stable\n * total order should sort.\n */\nexport async function collectRawWikiPages(root: string): Promise<RawWikiPage[]> {\n const canonicalRoot = await safeRealpath(root);\n if (!canonicalRoot) return [];\n const [concepts, queries] = await Promise.all([\n collectFromDir(canonicalRoot, \"concepts\", CONCEPTS_DIR),\n collectFromDir(canonicalRoot, \"queries\", QUERIES_DIR),\n ]);\n return [...concepts, ...queries];\n}\n","/**\n * Viewer-facing page collector.\n *\n * Consumes the structural records produced by `src/wiki/collect.ts` and\n * decorates each one with the fields the HTTP server needs:\n * - namespaced `id` (`concepts/<slug>` or `queries/<slug>`)\n * - `outgoingLinks` resolved against the in-memory page list using the\n * bare-slug precedence rule (concepts win over queries)\n * - `citations` extracted via `extractClaimCitations`\n * - stable `ViewerWarning` objects derived from `parseStatus` flags\n *\n * Unlike the export collector, this layer never drops a page: pages with\n * missing or malformed frontmatter are retained with a warning so users\n * can navigate to them and see what is wrong.\n */\n\nimport { collectRawWikiPages, extractWikilinkSlugs, extractWikilinkTargets } from \"../wiki/collect.js\";\nimport type { RawWikiPage } from \"../wiki/collect.js\";\nimport { extractClaimCitations, slugify } from \"../utils/markdown.js\";\nimport type { PageId, ViewerPage, ViewerWarning } from \"./types.js\";\nimport type { PageFreshness } from \"../freshness/types.js\";\n\n/** Minimal page shape `resolveBareSlug` needs to find a target. */\ntype PageIndexEntry = {\n id: PageId;\n pageDirectory: ViewerPage[\"pageDirectory\"];\n slug: string;\n /** Declared frontmatter aliases, used as a resolution fallback after exact slug. */\n aliases?: readonly string[];\n};\n\n/**\n * Build the decorated page list for a project root. Each `ViewerPage`\n * carries its namespaced id, resolved outgoing links, citations, and any\n * `ViewerWarning` objects derived from the underlying `parseStatus` flags.\n * Returns pages in collector order (concepts then queries).\n */\nexport async function collectViewerPages(root: string): Promise<ViewerPage[]> {\n const raw = await collectRawWikiPages(root);\n return decoratePages(raw);\n}\n\n/**\n * Resolve a bare-slug wikilink target to a namespaced `PageId`. The\n * precedence rule (concepts before queries) matches the spec and is the\n * same logic used for both per-page outgoing links and `/api/index` link\n * resolution; exporting it here keeps callers from re-implementing the\n * order and accidentally diverging.\n */\nexport function resolveBareSlug(\n slug: string,\n pages: ReadonlyArray<PageIndexEntry>,\n): PageId | null {\n if (slug.length === 0) return null;\n const concept = pages.find((p) => p.pageDirectory === \"concepts\" && p.slug === slug);\n if (concept) return concept.id;\n const query = pages.find((p) => p.pageDirectory === \"queries\" && p.slug === slug);\n if (query) return query.id;\n // Alias fallback: a page whose declared aliases slugify to this target. An\n // exact slug match always wins (above) so a real page is never shadowed by\n // another page's alias; concepts still take precedence over queries.\n const conceptAlias = pages.find((p) => p.pageDirectory === \"concepts\" && hasAliasSlug(p, slug));\n if (conceptAlias) return conceptAlias.id;\n const queryAlias = pages.find((p) => p.pageDirectory === \"queries\" && hasAliasSlug(p, slug));\n if (queryAlias) return queryAlias.id;\n return null;\n}\n\n/** True when any of the page's declared aliases slugifies to `slug`. */\nfunction hasAliasSlug(page: PageIndexEntry, slug: string): boolean {\n return (page.aliases ?? []).some((alias) => slugify(alias) === slug);\n}\n\n/**\n * Resolve a list of bare-slug wikilink targets against an in-memory page\n * index and deduplicate the resulting `PageId`s while preserving first-\n * occurrence order. Unresolved targets are dropped.\n */\nexport function resolveBareSlugList(\n targets: string[],\n pages: ReadonlyArray<PageIndexEntry>,\n): PageId[] {\n const seen = new Set<PageId>();\n const ordered: PageId[] = [];\n for (const target of targets) {\n const resolved = resolveBareSlug(target, pages);\n if (resolved && !seen.has(resolved)) {\n seen.add(resolved);\n ordered.push(resolved);\n }\n }\n return ordered;\n}\n\n/**\n * Two-pass decoration: build the namespaced id/title/warnings shell for\n * every page first, then resolve wikilink targets against the completed\n * shell. Single-pass would let a page miss links to pages later in the\n * list; the index has to be complete before resolution begins.\n */\nfunction decoratePages(raw: RawWikiPage[]): ViewerPage[] {\n const shells = raw.map(buildPageShell);\n for (const page of shells) {\n const slugTargets = extractWikilinkSlugs(page.body);\n const richTargets = extractWikilinkTargets(page.body);\n page.outgoingLinks = resolveBareSlugList(slugTargets, shells);\n page.danglingLinks = collectDanglingLinks(richTargets, shells);\n }\n return shells;\n}\n\n/**\n * Return targets from `targets` that `resolveBareSlug` could not find,\n * deduplicated by slug and in first-occurrence order.\n */\nfunction collectDanglingLinks(\n targets: { slug: string; display: string }[],\n pages: ReadonlyArray<PageIndexEntry>,\n): { slug: string; display: string }[] {\n const seen = new Set<string>();\n const dangling: { slug: string; display: string }[] = [];\n for (const t of targets) {\n if (resolveBareSlug(t.slug, pages) === null && !seen.has(t.slug)) {\n seen.add(t.slug);\n dangling.push(t);\n }\n }\n return dangling;\n}\n\n/**\n * Build the parts of a `ViewerPage` that do not need cross-page resolution\n * (id, title, citations, warnings). `outgoingLinks` starts empty and is\n * filled in once every shell is built.\n */\nfunction buildPageShell(page: RawWikiPage): ViewerPage {\n const id: PageId = `${page.pageDirectory}/${page.slug}`;\n return {\n id,\n slug: page.slug,\n pageDirectory: page.pageDirectory,\n title: page.title ?? page.slug,\n filePath: page.filePath,\n frontmatter: page.frontmatter,\n body: page.body,\n aliases: readAliases(page.frontmatter),\n outgoingLinks: [],\n citations: extractClaimCitations(page.body),\n warnings: warningsFromParseStatus(page),\n // Placeholder: overwritten by attachFreshness() in buildViewerSnapshot.\n freshness: UNVERIFIED_FRESHNESS,\n };\n}\n\n/** Default freshness placeholder — overwritten in the snapshot build. */\nconst UNVERIFIED_FRESHNESS: PageFreshness = {\n freshnessStatus: \"unverified\",\n contradicted: false,\n archived: false,\n};\n\n/** Read the `aliases` frontmatter field as a string list (empty when absent/malformed). */\nfunction readAliases(frontmatter: Record<string, unknown>): string[] {\n const raw = frontmatter.aliases;\n if (!Array.isArray(raw)) return [];\n return raw.filter((entry): entry is string => typeof entry === \"string\");\n}\n\n/**\n * Map the structural `parseStatus` flags from `src/wiki/collect.ts` into\n * stable viewer warnings. Multiple conditions on one page produce\n * multiple warnings; the order here is the order they appear on the\n * page's `warnings[]`.\n */\nfunction warningsFromParseStatus(page: RawWikiPage): ViewerWarning[] {\n const warnings: ViewerWarning[] = [];\n if (!page.parseStatus.hasFrontmatterBlock) {\n warnings.push({\n code: \"missing_frontmatter\",\n message: `Page \"${page.slug}\" has no frontmatter block.`,\n });\n } else if (page.parseStatus.malformedFrontmatter) {\n warnings.push({\n code: \"malformed_frontmatter\",\n message: `Page \"${page.slug}\" has malformed YAML frontmatter.`,\n });\n }\n if (!page.parseStatus.hasTitle) {\n warnings.push({\n code: \"missing_title\",\n message: `Page \"${page.slug}\" has no frontmatter title; displaying slug.`,\n });\n }\n return warnings;\n}\n\n","/**\n * Graph data builder for the viewer's `#/graph` route.\n *\n * Converts the flat `ViewerPage[]` list from the startup snapshot into an\n * adjacency representation suitable for D3 force-directed rendering. Called\n * once by `buildViewerSnapshot` — the result is frozen in the snapshot and\n * served directly by `/api/graph` with no per-request computation.\n */\n\nimport type { ViewerPage } from \"./types.js\";\nimport type { GraphData, GraphEdge, GraphNode, PageId } from \"./types.js\";\n\nconst DEFAULT_KIND = \"concept\";\n\n/** Resolve the display kind for a page, defaulting to \"concept\" when absent or non-string. */\nexport function resolvePageKind(frontmatter: Record<string, unknown>): string {\n return typeof frontmatter.kind === \"string\" && frontmatter.kind.length > 0\n ? frontmatter.kind\n : DEFAULT_KIND;\n}\n\n/**\n * Build graph adjacency data from the page list. All outgoing links become\n * edges — including dangling ones whose target has no backing page. Dangling\n * targets appear as ghost nodes with `isDangling: true`. Real-node degree\n * counts only valid edges (dangling out-links do not inflate the source\n * node's out-degree).\n */\nexport function buildGraphData(pages: ViewerPage[]): GraphData {\n const pageIds = new Set<PageId>(pages.map((p) => p.id));\n const edges = buildEdges(pages);\n const ghostDisplayMap = buildGhostDisplayMap(pages);\n const inDegreeMap = buildInDegreeMap(edges);\n const realNodes = pages.map((p) => buildNode(p, pageIds, inDegreeMap));\n const ghostNodes = buildGhostNodes(edges, pageIds, inDegreeMap, ghostDisplayMap);\n return { nodes: [...realNodes, ...ghostNodes], edges };\n}\n\nfunction buildGhostDisplayMap(pages: ViewerPage[]): Map<PageId, string> {\n const map = new Map<PageId, string>();\n for (const page of pages) {\n for (const { slug, display } of page.danglingLinks ?? []) {\n const id = ghostId(slug);\n if (!map.has(id)) map.set(id, display);\n }\n }\n return map;\n}\n\nconst GHOST_DIRECTORY = \"concepts\";\n\n/** Synthesise a stable PageId for an unresolved slug so ghost nodes have a unique key. */\nfunction ghostId(slug: string): PageId {\n return `${GHOST_DIRECTORY}/${slug}` as PageId;\n}\n\nfunction buildEdges(pages: ViewerPage[]): GraphEdge[] {\n const edges: GraphEdge[] = [];\n for (const page of pages) {\n for (const target of page.outgoingLinks) {\n edges.push({ source: page.id, target });\n }\n for (const { slug } of page.danglingLinks ?? []) {\n edges.push({ source: page.id, target: ghostId(slug) });\n }\n }\n return edges;\n}\n\nfunction buildGhostNodes(\n edges: GraphEdge[],\n pageIds: Set<PageId>,\n inDegreeMap: Map<PageId, number>,\n displayMap: Map<PageId, string>,\n): GraphNode[] {\n const seen = new Set<PageId>();\n const ghosts: GraphNode[] = [];\n for (const { target } of edges) {\n if (pageIds.has(target) || seen.has(target)) continue;\n seen.add(target);\n const [directory, ...rest] = target.split(\"/\");\n const slug = rest.join(\"/\");\n ghosts.push({\n id: target,\n title: displayMap.get(target) ?? slug,\n slug,\n directory,\n kind: \"dangling\",\n degree: inDegreeMap.get(target) ?? 0,\n isDangling: true,\n });\n }\n return ghosts;\n}\n\nfunction buildInDegreeMap(edges: GraphEdge[]): Map<PageId, number> {\n const map = new Map<PageId, number>();\n for (const edge of edges) {\n map.set(edge.target, (map.get(edge.target) ?? 0) + 1);\n }\n return map;\n}\n\nfunction buildNode(\n page: ViewerPage,\n pageIds: Set<PageId>,\n inDegreeMap: Map<PageId, number>,\n): GraphNode {\n const outDegree = page.outgoingLinks.filter((t) => pageIds.has(t)).length;\n const inDegree = inDegreeMap.get(page.id) ?? 0;\n const kind = resolvePageKind(page.frontmatter);\n return {\n id: page.id,\n title: page.title,\n slug: page.slug,\n directory: page.pageDirectory,\n kind,\n degree: outDegree + inDegree,\n };\n}\n","/**\n * Read-only project-state collector for `llmwiki next`.\n *\n * Inspects an llmwiki project root and reports counts, lint cache status,\n * and structural warnings without acquiring locks, mutating disk, or\n * running compile/lint. Designed to be cheap on small projects and to\n * stay cheap on large wikis by using directory mtimes as a stale-lint\n * proxy instead of stat-ing every page.\n *\n * Stale-lint detection is intentionally approximate per the spec:\n * directory-structure changes after the last lint run produce a\n * `lint-cache-stale` warning. Content-only edits may not trigger it on\n * every filesystem; exact lint freshness belongs to `llmwiki lint`.\n *\n * Candidate reads are race-tolerant: this collector delegates to\n * `countCandidates`, which skips malformed/partial candidate JSON the\n * same way `review list` does.\n */\n\nimport { stat, readdir, readFile } from \"fs/promises\";\nimport path from \"path\";\nimport {\n SOURCES_DIR,\n CONCEPTS_DIR,\n QUERIES_DIR,\n LLMWIKI_DIR,\n LAST_LINT_FILE,\n INDEX_FILE,\n} from \"../utils/constants.js\";\nimport { countCandidates } from \"../compiler/candidates.js\";\nimport { LINT_CACHE_TIMESTAMP_PATTERN } from \"../linter/cache.js\";\nimport type { LintCacheEntry } from \"../linter/cache.js\";\nimport { readStateClassified } from \"../utils/state.js\";\n\n/** Markdown extension treated as a wiki page for counting purposes. */\nconst MARKDOWN_EXT = \".md\";\n\n/** Warning codes the v1 collector may emit. Pinned by tests. */\ntype ProjectStateWarningCode =\n | \"lint-cache-stale\"\n | \"lint-cache-unparseable\"\n | \"freshness-cache-stale\"\n | \"index-missing\"\n | \"sources-not-compiled\"\n | \"pending-candidates\"\n | \"project-unreadable\"\n | \"state-unreadable\"\n | \"stale-pages\";\n\n/** One structural warning surfaced alongside the primary state. */\nexport interface ProjectStateWarning {\n code: ProjectStateWarningCode;\n message: string;\n}\n\n/** Three-state lint-cache shape per the JSON contract. */\ninterface LintCacheStatus {\n /** True when a cache file exists on disk, even if it failed to parse. */\n present: boolean;\n /** Parsed cache entry; null when no file exists OR file failed to parse. */\n entry: LintCacheEntry | null;\n}\n\n/** Full project-state snapshot consumed by `recommendNextAction` and renderers. */\nexport interface ProjectState {\n root: string;\n hasSourcesDir: boolean;\n hasWikiDir: boolean;\n hasInternalDir: boolean;\n sourceCount: number;\n conceptCount: number;\n queryCount: number;\n pendingCandidates: number;\n hasIndex: boolean;\n lint: LintCacheStatus;\n latestWikiMtimeMs: number | null;\n latestSourceMtimeMs: number | null;\n warnings: ProjectStateWarning[];\n}\n\n/**\n * Collect a full ProjectState snapshot for `root`. Never throws on\n * expected absent-file paths — those become `false`/`0`/`null` fields.\n * Returns a `project-unreadable` warning when the root itself cannot be\n * stat-ed; callers escalate that to the `broken-project` primary state.\n */\nexport async function collectProjectState(root: string): Promise<ProjectState> {\n const rootReadable = await isDirectory(root);\n if (!rootReadable) return brokenProjectState(root);\n const dirs = await collectDirPresence(root);\n const counts = await collectPageCounts(root, dirs);\n const lint = await collectLintCacheStatus(root);\n const mtimes = await collectMtimes(root, dirs);\n // Read-only; never writes a .bak file — safe for the cheap orientation command.\n const stateStatus = (await readStateClassified(root)).status;\n return assembleState({ root, dirs, counts, lint, mtimes, stateStatus });\n}\n\n/** State returned when `root` itself cannot be inspected. */\nfunction brokenProjectState(root: string): ProjectState {\n return {\n root,\n hasSourcesDir: false,\n hasWikiDir: false,\n hasInternalDir: false,\n sourceCount: 0,\n conceptCount: 0,\n queryCount: 0,\n pendingCandidates: 0,\n hasIndex: false,\n lint: { present: false, entry: null },\n latestWikiMtimeMs: null,\n latestSourceMtimeMs: null,\n warnings: [\n {\n code: \"project-unreadable\",\n message: `Project root is unreadable or could not be inspected: ${root}`,\n },\n ],\n };\n}\n\n/** Cheap presence probes for the three structural directories the recommender keys on. */\ninterface DirPresence {\n hasSourcesDir: boolean;\n hasWikiDir: boolean;\n hasInternalDir: boolean;\n}\n\n/** Raw page counts surfaced into ProjectState; computed in one pass to keep collectProjectState short. */\ninterface PageCounts {\n sourceCount: number;\n conceptCount: number;\n queryCount: number;\n pendingCandidates: number;\n hasIndex: boolean;\n}\n\n/** Mtime proxies used for the cheap stale-lint approximation. */\ninterface MtimePair {\n latestWikiMtimeMs: number | null;\n latestSourceMtimeMs: number | null;\n}\n\n/** Probe the three structural directories without listing their contents. */\nasync function collectDirPresence(root: string): Promise<DirPresence> {\n const [hasSourcesDir, hasWikiDir, hasInternalDir] = await Promise.all([\n isDirectory(path.join(root, SOURCES_DIR)),\n isDirectory(path.join(root, \"wiki\")),\n isDirectory(path.join(root, LLMWIKI_DIR)),\n ]);\n return { hasSourcesDir, hasWikiDir, hasInternalDir };\n}\n\n/** Compute page counts plus the wiki/index.md presence flag. */\nasync function collectPageCounts(root: string, dirs: DirPresence): Promise<PageCounts> {\n const [sourceCount, conceptCount, queryCount, pendingCandidates, hasIndex] = await Promise.all([\n dirs.hasSourcesDir ? countMarkdownFiles(path.join(root, SOURCES_DIR)) : 0,\n dirs.hasWikiDir ? countMarkdownFiles(path.join(root, CONCEPTS_DIR)) : 0,\n dirs.hasWikiDir ? countMarkdownFiles(path.join(root, QUERIES_DIR)) : 0,\n dirs.hasInternalDir ? safeCountCandidates(root) : 0,\n dirs.hasWikiDir ? isFile(path.join(root, INDEX_FILE)) : false,\n ]);\n return { sourceCount, conceptCount, queryCount, pendingCandidates, hasIndex };\n}\n\n/** Read directory mtimes only when the directories exist; absent dirs report null. */\nasync function collectMtimes(root: string, dirs: DirPresence): Promise<MtimePair> {\n const [latestWikiMtimeMs, latestSourceMtimeMs] = await Promise.all([\n dirs.hasWikiDir ? safeMtime(path.join(root, \"wiki\")) : Promise.resolve(null),\n dirs.hasSourcesDir ? safeMtime(path.join(root, SOURCES_DIR)) : Promise.resolve(null),\n ]);\n return { latestWikiMtimeMs, latestSourceMtimeMs };\n}\n\n/** Read and validate the lint cache, distinguishing missing from corrupt. */\nasync function collectLintCacheStatus(root: string): Promise<LintCacheStatus> {\n const cachePath = path.join(root, LAST_LINT_FILE);\n const exists = await isFile(cachePath);\n if (!exists) return { present: false, entry: null };\n const entry = await readLintCacheEntry(cachePath);\n return { present: true, entry };\n}\n\n/** Strict parse + validate, returning null for any unreadable/corrupt cache file. */\nasync function readLintCacheEntry(cachePath: string): Promise<LintCacheEntry | null> {\n let raw: string;\n try {\n raw = await readFile(cachePath, \"utf-8\");\n } catch {\n return null;\n }\n try {\n const parsed = JSON.parse(raw) as unknown;\n return validateCacheShape(parsed);\n } catch {\n return null;\n }\n}\n\n/**\n * Validate the lint cache JSON shape, carrying the optional `freshness` field\n * through when it is present and well-formed. Returns null for any missing or\n * corrupt field in the required subset ({@link LintCacheEntry}).\n */\nfunction validateCacheShape(value: unknown): LintCacheEntry | null {\n if (typeof value !== \"object\" || value === null) return null;\n const candidate = value as Record<string, unknown>;\n const { warnings, errors, at, freshness } = candidate;\n if (!isNonNegativeInteger(warnings)) return null;\n if (!isNonNegativeInteger(errors)) return null;\n if (typeof at !== \"string\" || !LINT_CACHE_TIMESTAMP_PATTERN.test(at)) return null;\n const entry: LintCacheEntry = { warnings, errors, at };\n // Intentionally lenient (diverges from cache.ts:isValidEntry): a malformed\n // `freshness` sub-field is silently dropped rather than rejecting the whole\n // entry, so `next` still surfaces valid lint counts. Do not \"align\" the two.\n if (isValidFreshnessShape(freshness)) entry.freshness = freshness;\n return entry;\n}\n\n/** True when `value` is a well-formed freshness sub-object. */\nfunction isValidFreshnessShape(\n value: unknown,\n): value is { stalePages: number; orphanedPages: number } {\n if (typeof value !== \"object\" || value === null) return false;\n const c = value as Record<string, unknown>;\n return isNonNegativeInteger(c.stalePages) && isNonNegativeInteger(c.orphanedPages);\n}\n\n/** True for finite, non-negative integers. NaN and Infinity fail Number.isInteger. */\nfunction isNonNegativeInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\n/** Inputs to {@link assembleState}, grouped to keep the function's arity within limits. */\ninterface AssembleStateInput {\n root: string;\n dirs: DirPresence;\n counts: PageCounts;\n lint: LintCacheStatus;\n mtimes: MtimePair;\n stateStatus: \"ok\" | \"missing\" | \"corrupt\";\n}\n\n/** Combine the per-section reads into the final ProjectState + warning list. */\nfunction assembleState(input: AssembleStateInput): ProjectState {\n const { root, dirs, counts, lint, mtimes, stateStatus } = input;\n const warnings = buildWarnings({ dirs, counts, lint, mtimes, stateStatus });\n return { root, ...dirs, ...counts, lint, ...mtimes, warnings };\n}\n\n/** Inputs for warning derivation; passed as a record to keep argument lists short. */\ninterface WarningInput {\n dirs: DirPresence;\n counts: PageCounts;\n lint: LintCacheStatus;\n mtimes: MtimePair;\n stateStatus: \"ok\" | \"missing\" | \"corrupt\";\n}\n\n/** Derive every structural warning from the already-collected state slices. */\nfunction buildWarnings(input: WarningInput): ProjectStateWarning[] {\n const warnings: ProjectStateWarning[] = [];\n appendLintWarnings(warnings, input.lint, input.mtimes.latestWikiMtimeMs);\n appendStructuralWarnings(warnings, input.dirs, input.counts);\n appendFreshnessWarnings(warnings, input.lint, input.stateStatus, input.mtimes.latestSourceMtimeMs);\n return warnings;\n}\n\n/** Lint-cache warnings: unparseable file or stale-vs-wiki-mtime. */\nfunction appendLintWarnings(\n warnings: ProjectStateWarning[],\n lint: LintCacheStatus,\n latestWikiMtimeMs: number | null,\n): void {\n if (lint.present && lint.entry === null) {\n warnings.push({\n code: \"lint-cache-unparseable\",\n message: \"Lint cache file exists but could not be parsed. Re-run `llmwiki lint`.\",\n });\n return;\n }\n if (lint.entry && isLintCacheStale(lint.entry, latestWikiMtimeMs)) {\n warnings.push({\n code: \"lint-cache-stale\",\n message: \"Lint cache is older than the wiki directory; pages were added or removed since the last lint.\",\n });\n }\n}\n\n/** Structural warnings: missing index, pending candidates, uncompiled sources. */\nfunction appendStructuralWarnings(\n warnings: ProjectStateWarning[],\n dirs: DirPresence,\n counts: PageCounts,\n): void {\n const hasPages = counts.conceptCount > 0 || counts.queryCount > 0;\n if (hasPages && !counts.hasIndex) {\n warnings.push({\n code: \"index-missing\",\n message: \"wiki/index.md is missing even though pages exist.\",\n });\n }\n if (counts.pendingCandidates > 0) {\n warnings.push({\n code: \"pending-candidates\",\n message: `${counts.pendingCandidates} generated candidate${counts.pendingCandidates === 1 ? \"\" : \"s\"} waiting for review.`,\n });\n }\n if (dirs.hasSourcesDir && counts.sourceCount > 0 && !hasPages) {\n warnings.push({\n code: \"sources-not-compiled\",\n message: \"Sources exist but no wiki pages were found. Run `llmwiki compile`.\",\n });\n }\n}\n\n/**\n * Freshness warnings: corrupt state.json (fail-loud, not silent), stale/orphaned\n * page counts from the lint cache, and a heuristic freshness-cache-stale warning\n * when sources/ changed after the last lint.\n *\n * Missing state stays quiet — it is normal before the first compile.\n * Only corrupt state warns because it silently zeros all findings.\n */\nfunction appendFreshnessWarnings(\n warnings: ProjectStateWarning[],\n lint: LintCacheStatus,\n stateStatus: \"ok\" | \"missing\" | \"corrupt\",\n latestSourceMtimeMs: number | null,\n): void {\n if (stateStatus === \"corrupt\") {\n warnings.push({\n code: \"state-unreadable\",\n message:\n \".llmwiki/state.json is corrupt — run `llmwiki compile` to rebuild it (freshness figures shown are from the last lint and may be stale).\",\n });\n }\n const f = lint.entry?.freshness;\n if (f && (f.stalePages > 0 || f.orphanedPages > 0)) {\n warnings.push({\n code: \"stale-pages\",\n message: `${f.stalePages} stale, ${f.orphanedPages} orphaned page(s) as of the last lint — run \\`llmwiki compile\\` to refresh.`,\n });\n }\n if (lint.entry && isFreshnessCacheStale(lint.entry, latestSourceMtimeMs)) {\n warnings.push({\n code: \"freshness-cache-stale\",\n message:\n \"sources/ changed since the last lint — freshness counts may be out of date; run `llmwiki lint` to refresh.\",\n });\n }\n}\n\n/**\n * Heuristic: the sources/ directory mtime is newer than the last lint run.\n * Catches added/removed source files; may NOT detect in-place content edits on\n * all filesystems (same limitation as the wiki-mtime lint-cache-stale check).\n */\nfunction isFreshnessCacheStale(entry: LintCacheEntry, latestSourceMtimeMs: number | null): boolean {\n if (latestSourceMtimeMs === null) return false;\n const lintMs = Date.parse(entry.at);\n if (Number.isNaN(lintMs)) return false;\n return latestSourceMtimeMs > lintMs;\n}\n\n/** Cheap structural staleness: last-lint timestamp older than wiki dir mtime. */\nfunction isLintCacheStale(entry: LintCacheEntry, latestWikiMtimeMs: number | null): boolean {\n if (latestWikiMtimeMs === null) return false;\n const lintMs = Date.parse(entry.at);\n if (Number.isNaN(lintMs)) return false;\n return latestWikiMtimeMs > lintMs;\n}\n\n/** True when `target` is a directory; false for missing paths or any other type. */\nasync function isDirectory(target: string): Promise<boolean> {\n try {\n const stats = await stat(target);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/** True when `target` is a regular file; false for missing paths or directories. */\nasync function isFile(target: string): Promise<boolean> {\n try {\n const stats = await stat(target);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Directory mtime in ms epoch, or null when the directory cannot be stat-ed. */\nasync function safeMtime(target: string): Promise<number | null> {\n try {\n const stats = await stat(target);\n return stats.mtimeMs;\n } catch {\n return null;\n }\n}\n\n/** Count `.md` files in a directory; 0 for missing dirs or any other error. */\nasync function countMarkdownFiles(dir: string): Promise<number> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n let count = 0;\n for (const entry of entries) {\n if (entry.isFile() && entry.name.endsWith(MARKDOWN_EXT)) count += 1;\n }\n return count;\n } catch {\n return 0;\n }\n}\n\n/** Race-tolerant candidate count; defers to `countCandidates` and swallows IO errors. */\nasync function safeCountCandidates(root: string): Promise<number> {\n try {\n return await countCandidates(root);\n } catch {\n return 0;\n }\n}\n","/**\n * Persistent cache of the most recent `llmwiki lint` run.\n *\n * Written by the lint command after a completed run, before any non-zero exit\n * for lint findings, so the cache always reflects the run the user just saw.\n * Crashed or partial runs leave the prior cache untouched.\n *\n * Consumers (e.g., the upcoming viewer's /api/health endpoint) read the cache\n * to surface lint counts without re-running lint per request. A missing or\n * malformed cache reads as null, which means \"lint has not been run yet.\"\n */\n\nimport { mkdir, readFile } from \"fs/promises\";\nimport path from \"path\";\nimport { atomicWrite } from \"../utils/markdown.js\";\nimport { LLMWIKI_DIR, LAST_LINT_FILE } from \"../utils/constants.js\";\nimport type { LintSummary } from \"./types.js\";\n\n/** Per-rule freshness counts, derived from the lint results. Optional so a pre-upgrade cache still parses. */\nexport interface LintFreshnessCounts {\n stalePages: number;\n orphanedPages: number;\n}\n\n/** One persisted lint summary. Shape is part of the public viewer-cache contract. */\nexport interface LintCacheEntry {\n warnings: number;\n errors: number;\n /** ISO-8601 timestamp of the run that produced these counts. */\n at: string;\n /** Stale/orphaned page counts from the freshness lint rule. Absent on pre-0.9 caches. */\n freshness?: LintFreshnessCounts;\n}\n\n/**\n * The exact ISO-8601 shape `writeLintCache` produces and `readLintCache` accepts.\n * Exported so tests can assert against the same regex the validator enforces and\n * never drift from the documented contract.\n */\nexport const LINT_CACHE_TIMESTAMP_PATTERN = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n\n/** Count results emitted by a given lint rule. */\nfunction countByRule(results: LintSummary[\"results\"], rule: string): number {\n return results.filter((r) => r.rule === rule).length;\n}\n\n/**\n * Persist a lint summary to `.llmwiki/last-lint.json` after a completed run.\n * Creates the `.llmwiki/` directory if missing. Overwrites any prior entry so\n * the cache reflects the most recent run, including zero-issue runs.\n */\nexport async function writeLintCache(root: string, summary: LintSummary): Promise<void> {\n await mkdir(path.join(root, LLMWIKI_DIR), { recursive: true });\n const entry: LintCacheEntry = {\n warnings: summary.warnings,\n errors: summary.errors,\n at: new Date().toISOString(),\n freshness: {\n stalePages: countByRule(summary.results, \"stale-page\"),\n orphanedPages: countByRule(summary.results, \"orphaned-page\"),\n },\n };\n await atomicWrite(path.join(root, LAST_LINT_FILE), `${JSON.stringify(entry, null, 2)}\\n`);\n}\n\n/**\n * Read the cached lint summary, returning null for missing or malformed files.\n * Validation is strict: every field must have its expected type, otherwise the\n * cache is treated as absent so callers do not surface garbage counts.\n */\nexport async function readLintCache(root: string): Promise<LintCacheEntry | null> {\n let raw: string;\n try {\n raw = await readFile(path.join(root, LAST_LINT_FILE), \"utf-8\");\n } catch {\n return null;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isValidEntry(parsed)) return null;\n return {\n warnings: parsed.warnings,\n errors: parsed.errors,\n at: parsed.at,\n ...(parsed.freshness !== undefined ? { freshness: parsed.freshness } : {}),\n };\n}\n\n/** True for finite non-negative integers, including zero. NaN and Infinity fail Number.isInteger. */\nfunction isNonNegativeInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\n/** Validates the optional freshness sub-object; rejects if present but malformed. */\nfunction isValidFreshness(value: unknown): value is LintFreshnessCounts {\n if (typeof value !== \"object\" || value === null) return false;\n const c = value as Record<string, unknown>;\n return isNonNegativeInteger(c.stalePages) && isNonNegativeInteger(c.orphanedPages);\n}\n\n/**\n * Strict type guard for the persisted cache entry.\n *\n * Counts must be finite non-negative integers (the writer only ever persists\n * `LintSummary` severity counts, which originate from a length on an array, so\n * anything else means the file was hand-edited or corrupted). The timestamp\n * must match the exact ISO-8601 shape the writer produces, otherwise downstream\n * consumers risk surfacing values like \"2026-01-01\" as full timestamps.\n * The `freshness` field is optional — pre-upgrade caches omitting it still parse.\n */\nfunction isValidEntry(value: unknown): value is LintCacheEntry {\n if (typeof value !== \"object\" || value === null) return false;\n const candidate = value as Record<string, unknown>;\n if (\n !isNonNegativeInteger(candidate.warnings) ||\n !isNonNegativeInteger(candidate.errors) ||\n typeof candidate.at !== \"string\" ||\n !LINT_CACHE_TIMESTAMP_PATTERN.test(candidate.at)\n ) {\n return false;\n }\n if (candidate.freshness !== undefined && !isValidFreshness(candidate.freshness)) return false;\n return true;\n}\n","/**\n * Pure recommendation rules for `llmwiki next`.\n *\n * Classifies a {@link ProjectState} snapshot into exactly one of seven\n * primary states and produces a primary {@link RecommendedAction} plus\n * the per-state `otherActions` table from the implementation plan.\n *\n * Actions with user-supplied input (a source path, a question, a\n * candidate id) are templates: the display `command` carries a\n * `<placeholder>` and `executable.placeholders` lists the slots. Agents\n * must populate placeholders themselves; the contract never returns a\n * shell-ready command line.\n */\n\nimport type { ProjectState } from \"./state.js\";\n\n/** Primary state enum surfaced as `state` in the JSON envelope. */\nexport type ProjectStateKind =\n | \"fresh\"\n | \"sources-only\"\n | \"review-pending\"\n | \"lint-attention\"\n | \"wiki-ready\"\n | \"empty-wiki\"\n | \"broken-project\";\n\n/** Single recommended action; `command` is for display, `executable` is for agents. */\nexport interface RecommendedAction {\n command: string | null;\n reason: string;\n executable: ExecutableSpec | null;\n}\n\n/** Structured form of an executable command. Placeholders are slot names, not literals. */\ninterface ExecutableSpec {\n binary: \"llmwiki\";\n args: string[];\n placeholders?: string[];\n}\n\n/** Full recommendation envelope returned to the renderer. */\nexport interface Recommendation {\n state: ProjectStateKind;\n recommended: RecommendedAction;\n otherActions: RecommendedAction[];\n}\n\n/**\n * Classify a ProjectState and produce its primary + secondary actions.\n * Precedence (top wins): broken-project, fresh, sources-only,\n * review-pending, lint-attention, wiki-ready, empty-wiki.\n */\nexport function recommendNextAction(state: ProjectState): Recommendation {\n const kind = classifyState(state);\n return { state: kind, recommended: primaryAction(kind), otherActions: otherActionsFor(kind) };\n}\n\n/** Apply the precedence rules from the spec to choose the primary state kind. */\nfunction classifyState(state: ProjectState): ProjectStateKind {\n if (isBrokenProject(state)) return \"broken-project\";\n if (isSourcesOnly(state)) return \"sources-only\";\n if (state.pendingCandidates > 0) return \"review-pending\";\n if (hasLintErrors(state)) return \"lint-attention\";\n if (hasWikiPages(state)) return \"wiki-ready\";\n if (isEmptyWiki(state)) return \"empty-wiki\";\n return \"fresh\";\n}\n\n/** True when the collector flagged the root as unreadable. */\nfunction isBrokenProject(state: ProjectState): boolean {\n return state.warnings.some((w) => w.code === \"project-unreadable\");\n}\n\n/**\n * `empty-wiki` requires `wiki/` to actually exist with zero pages. The\n * spec wording is \"wiki/ exists but page count is zero\", and the human\n * renderer says so — we must not surface that line when wiki/ is missing.\n */\nfunction isEmptyWiki(state: ProjectState): boolean {\n return state.hasWikiDir && !hasWikiPages(state);\n}\n\n/** Sources exist but no concept or query pages have been compiled yet. */\nfunction isSourcesOnly(state: ProjectState): boolean {\n return state.sourceCount > 0 && !hasWikiPages(state);\n}\n\n/** True when the lint cache parsed and reports at least one error. */\nfunction hasLintErrors(state: ProjectState): boolean {\n return state.lint.entry !== null && state.lint.entry.errors > 0;\n}\n\n/** True when the wiki carries any concept or query page. */\nfunction hasWikiPages(state: ProjectState): boolean {\n return state.conceptCount > 0 || state.queryCount > 0;\n}\n\n/** Map a state kind to its primary recommended action. */\nfunction primaryAction(kind: ProjectStateKind): RecommendedAction {\n return PRIMARY_ACTIONS[kind];\n}\n\n/** Map a state kind to its `otherActions` list. */\nfunction otherActionsFor(kind: ProjectStateKind): RecommendedAction[] {\n return OTHER_ACTIONS[kind].map((a) => ({ ...a }));\n}\n\n/** Template action: `llmwiki quickstart <source>` with a `source` placeholder. */\nconst QUICKSTART_ACTION: RecommendedAction = {\n command: \"llmwiki quickstart <source>\",\n reason: \"Ingest a source and compile a wiki in one step.\",\n executable: { binary: \"llmwiki\", args: [\"quickstart\"], placeholders: [\"source\"] },\n};\n\n/** Template action: `llmwiki ingest <source>`. */\nconst INGEST_ACTION: RecommendedAction = {\n command: \"llmwiki ingest <source>\",\n reason: \"Add sources manually before compiling.\",\n executable: { binary: \"llmwiki\", args: [\"ingest\"], placeholders: [\"source\"] },\n};\n\n/** Bare action: `llmwiki compile`. */\nconst COMPILE_ACTION: RecommendedAction = {\n command: \"llmwiki compile\",\n reason: \"Compile sources/ into wiki pages.\",\n executable: { binary: \"llmwiki\", args: [\"compile\"] },\n};\n\n/** Bare action: `llmwiki review list`. */\nconst REVIEW_LIST_ACTION: RecommendedAction = {\n command: \"llmwiki review list\",\n reason: \"List pending candidate pages.\",\n executable: { binary: \"llmwiki\", args: [\"review\", \"list\"] },\n};\n\n/** Template action: `llmwiki review approve <id>`. */\nconst REVIEW_APPROVE_ACTION: RecommendedAction = {\n command: \"llmwiki review approve <id>\",\n reason: \"Approve a candidate after inspecting it with `llmwiki review show <id>`.\",\n executable: { binary: \"llmwiki\", args: [\"review\", \"approve\"], placeholders: [\"id\"] },\n};\n\n/** Bare action: `llmwiki lint`. */\nconst LINT_ACTION: RecommendedAction = {\n command: \"llmwiki lint\",\n reason: \"Re-run lint to inspect outstanding errors.\",\n executable: { binary: \"llmwiki\", args: [\"lint\"] },\n};\n\n/** Bare action: `llmwiki view --open`. */\nconst VIEW_OPEN_ACTION: RecommendedAction = {\n command: \"llmwiki view --open\",\n reason: \"Browse the compiled wiki in the local viewer.\",\n executable: { binary: \"llmwiki\", args: [\"view\", \"--open\"] },\n};\n\n/** Template action: `llmwiki query \"<question>\"`. */\nconst QUERY_ACTION: RecommendedAction = {\n command: 'llmwiki query \"<question>\"',\n reason: \"Ask a natural-language question against the compiled wiki.\",\n executable: { binary: \"llmwiki\", args: [\"query\"], placeholders: [\"question\"] },\n};\n\n/** Sentinel for the broken-project state: no command, no executable. */\nconst BROKEN_PROJECT_ACTION: RecommendedAction = {\n command: null,\n reason: \"Project root is unreadable or could not be inspected.\",\n executable: null,\n};\n\n/** Primary recommendation for each state kind. */\nconst PRIMARY_ACTIONS: Record<ProjectStateKind, RecommendedAction> = {\n \"broken-project\": BROKEN_PROJECT_ACTION,\n fresh: { ...QUICKSTART_ACTION, reason: \"No sources or wiki pages were found.\" },\n \"sources-only\": { ...COMPILE_ACTION, reason: \"Sources exist but no wiki pages have been compiled.\" },\n \"review-pending\": { ...REVIEW_LIST_ACTION, reason: \"Generated candidates are waiting for review.\" },\n \"lint-attention\": { ...LINT_ACTION, reason: \"Lint has reported errors; rerun lint to inspect them.\" },\n \"wiki-ready\": { ...VIEW_OPEN_ACTION, reason: \"Wiki pages are ready to browse.\" },\n \"empty-wiki\": {\n ...COMPILE_ACTION,\n reason: \"wiki/ exists but is empty; run compile or add sources first.\",\n },\n};\n\n/** Per-state otherActions table from the implementation plan (Slice 1 scope). */\nconst OTHER_ACTIONS: Record<ProjectStateKind, RecommendedAction[]> = {\n fresh: [INGEST_ACTION, QUICKSTART_ACTION],\n \"sources-only\": [COMPILE_ACTION, QUICKSTART_ACTION],\n \"empty-wiki\": [COMPILE_ACTION, INGEST_ACTION],\n \"review-pending\": [REVIEW_LIST_ACTION, REVIEW_APPROVE_ACTION],\n \"lint-attention\": [LINT_ACTION, VIEW_OPEN_ACTION],\n \"wiki-ready\": [VIEW_OPEN_ACTION, QUERY_ACTION],\n \"broken-project\": [],\n};\n","/**\n * Server-side title/body search over the startup `ViewerSnapshot`.\n *\n * V1 semantics (spec §Slice 5 \"Search semantics\"):\n * - case-insensitive\n * - whitespace-tokenized\n * - multi-token AND: every token must appear in either title or body\n * - title matches rank before body matches\n * - 200-char query cap, 50-result cap\n * - concept and query pages only — `wiki/index.md` is excluded by\n * construction (it never lives in `snapshot.pages`)\n * - no fuzzy matching, stemming, regex, or client-side search\n *\n * The search reads from the snapshot exclusively — no per-request disk\n * I/O — so it inherits the same \"frozen at startup, restart to refresh\"\n * lifecycle as the rest of the viewer's API.\n */\n\nimport type { PageId, ViewerPage, ViewerSnapshot } from \"./types.js\";\nimport type { PageDirectory } from \"../export/types.js\";\n\nconst MAX_QUERY_LENGTH = 200;\nconst MAX_RESULTS = 50;\nconst SNIPPET_RADIUS = 60;\nconst SNIPPET_ELLIPSIS = \"…\";\n\n/** Where the query matched in a result page. */\ntype SearchMatch = \"title\" | \"body\";\n\n/** One row in the `/api/search` response. */\ninterface SearchResult {\n id: PageId;\n pageDirectory: PageDirectory;\n title: string;\n snippet: string;\n matchedIn: SearchMatch;\n}\n\n/**\n * Run a search over the snapshot and return the results envelope. Pure\n * over `(snapshot, rawQuery)` — the same inputs always produce the same\n * output, which lets the route handler stay a one-line adapter.\n */\nexport function searchPages(\n snapshot: ViewerSnapshot,\n rawQuery: string,\n): { results: SearchResult[] } {\n const tokens = tokenizeQuery(rawQuery);\n if (tokens.length === 0) return { results: [] };\n const matches = collectMatches(snapshot.pages, tokens);\n matches.sort(compareResults);\n return { results: matches.slice(0, MAX_RESULTS) };\n}\n\n/**\n * Trim, lowercase, cap at 200 characters, then split on any run of\n * whitespace. Empty tokens are dropped so trailing/leading spaces or\n * a runaway over-cap query still produce sensible tokens.\n */\nfunction tokenizeQuery(rawQuery: string): string[] {\n if (typeof rawQuery !== \"string\") return [];\n const trimmed = rawQuery.trim();\n if (trimmed.length === 0) return [];\n const capped = trimmed.slice(0, MAX_QUERY_LENGTH).toLowerCase();\n return capped.split(/\\s+/).filter((t) => t.length > 0);\n}\n\n/** Iterate the snapshot pages and emit one result per match. */\nfunction collectMatches(pages: ReadonlyArray<ViewerPage>, tokens: string[]): SearchResult[] {\n const matches: SearchResult[] = [];\n for (const page of pages) {\n const result = matchPage(page, tokens);\n if (result) matches.push(result);\n }\n return matches;\n}\n\n/**\n * Decide whether `page` matches `tokens`. Per spec §Slice 5 Search\n * Semantics: \"every token must appear in title or body\" — each token\n * individually must appear in the title-or-body union. Classification\n * (`matchedIn`) is \"title\" only when every token is found in the title;\n * any token that only matched the body downgrades the page to a body\n * hit, which then ranks below title hits.\n */\nfunction matchPage(page: ViewerPage, tokens: string[]): SearchResult | null {\n const titleLower = page.title.toLowerCase();\n const bodyLower = page.body.toLowerCase();\n for (const token of tokens) {\n if (!titleLower.includes(token) && !bodyLower.includes(token)) return null;\n }\n const allInTitle = tokens.every((t) => titleLower.includes(t));\n if (allInTitle) return rowFromPage(page, page.title, \"title\");\n const snippet = buildBodySnippet(page.body, bodyLower, tokens);\n return rowFromPage(page, snippet, \"body\");\n}\n\n/** Assemble a SearchResult from a page + computed snippet. */\nfunction rowFromPage(page: ViewerPage, snippet: string, matchedIn: SearchMatch): SearchResult {\n return {\n id: page.id,\n pageDirectory: page.pageDirectory,\n title: page.title,\n snippet,\n matchedIn,\n };\n}\n\n/**\n * Extract ±SNIPPET_RADIUS chars around the earliest token match in the\n * body. Newlines are flattened to single spaces so the snippet renders\n * inline in the results panel. `…` is prepended/appended when the\n * window was truncated at either end.\n */\nfunction buildBodySnippet(body: string, bodyLower: string, tokens: string[]): string {\n const matchPos = earliestTokenPosition(bodyLower, tokens);\n const start = Math.max(0, matchPos - SNIPPET_RADIUS);\n const end = Math.min(body.length, matchPos + SNIPPET_RADIUS);\n const cleaned = stripInlineMarkdownNoise(body.slice(start, end))\n .replace(/\\s+/g, \" \")\n .trim();\n const prefix = start > 0 ? SNIPPET_ELLIPSIS : \"\";\n const suffix = end < body.length ? SNIPPET_ELLIPSIS : \"\";\n return `${prefix}${cleaned}${suffix}`;\n}\n\n/**\n * Strip common inline-markdown markers from a snippet so the search\n * results panel shows readable prose rather than `**keyword**` and\n * `[label](url)` noise. Intentionally narrow: only handles the\n * inline-marker forms a reader would mistake for typos. Block-level\n * markers (`#` headings, `>` blockquotes) are left alone — they sit at\n * the start of a line and rarely land inside a ±60-char window.\n */\nfunction stripInlineMarkdownNoise(text: string): string {\n return text\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\")\n .replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, \"$1\")\n .replace(/\\[\\[([^\\]|\\n]+)\\|([^\\]\\n]+)\\]\\]/g, \"$2\")\n .replace(/\\[\\[([^\\]\\n]+)\\]\\]/g, \"$1\")\n .replace(/\\*\\*([^*]+)\\*\\*/g, \"$1\")\n .replace(/__([^_]+)__/g, \"$1\")\n .replace(/(?<!\\w)\\*([^*\\n]+)\\*(?!\\w)/g, \"$1\")\n .replace(/(?<!\\w)_([^_\\n]+)_(?!\\w)/g, \"$1\")\n .replace(/`([^`\\n]+)`/g, \"$1\")\n .replace(/~~([^~\\n]+)~~/g, \"$1\");\n}\n\n/** Earliest index where any token first appears in the body. */\nfunction earliestTokenPosition(bodyLower: string, tokens: string[]): number {\n let earliest = bodyLower.length;\n for (const token of tokens) {\n const idx = bodyLower.indexOf(token);\n if (idx >= 0 && idx < earliest) earliest = idx;\n }\n return earliest;\n}\n\n/**\n * Title hits sort before body hits. Within the same `matchedIn` bucket\n * the order is stable by title (alphabetical, locale-aware) so the\n * result list does not shift between requests against the same snapshot.\n */\nfunction compareResults(a: SearchResult, b: SearchResult): number {\n if (a.matchedIn !== b.matchedIn) {\n return a.matchedIn === \"title\" ? -1 : 1;\n }\n return a.title.localeCompare(b.title);\n}\n","/**\n * Provenance helpers for `llmwiki context`.\n *\n * Slice 4 ships two related pieces of work:\n * 1. Flatten `ViewerPage.citations` (`ClaimCitation[]`, each with one\n * or more `SourceSpan` entries) into the documented\n * `ContextPrimary.citations[]` shape: one object per span,\n * `file`/`start`/`end` lifted from `span.lines` when present,\n * paragraph-only citations omit `start`/`end`, de-duped by\n * `(file, start, end)`, preserved in first-seen document order\n * (plan §Provenance And Source Windows).\n * 2. Materialize bounded `ContextSourceWindow[]` for `--include-sources`\n * by reading short line ranges out of `sources/`. Path-confined:\n * traversal, absolute paths, and symlink escapes are rejected.\n * Only claim-level spans (`lines` populated) become windows;\n * paragraph-only citations are intentionally skipped because the\n * caller asked for SPECIFIC line context, not whole files.\n *\n * Citation flattening is the inner contract for `primary[].citations`;\n * source-window materialization is the outer guard rail for\n * `--include-sources` per-pack and per-window caps.\n */\n\nimport { promises as fs } from \"fs\";\nimport path from \"path\";\nimport type { ClaimCitation, SourceSpan } from \"../utils/types.js\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\n\n/** Per-pack ceiling on emitted source windows. */\nexport const MAX_SOURCE_WINDOWS = 20;\n/** Per-window ceiling on lines read out of a source file. */\nexport const MAX_LINES_PER_WINDOW = 30;\n/**\n * Visible ASCII delimiter for {@link citationKey} dedup tuples. Plain\n * spaces and dashes can legally appear inside `file` names so the key\n * must use a separator that filenames cannot contain. `' <#> '` is\n * unambiguous, ASCII-clean (no control bytes — graph.ts had the same\n * class of bug with NUL delimiters), and obvious in diagnostic output.\n */\nconst CITATION_KEY_SEPARATOR = \" <#> \";\n\n/**\n * Flat citation shape consumed by `ContextPrimary.citations[]` AND by\n * the JSON export's `ExportPage.citations[]`. Exported so both\n * surfaces share one normalized shape rather than drifting — consumers\n * can reuse the same flattening rule across `llmwiki context` and\n * `llmwiki export`.\n */\nexport interface FlatCitation {\n file: string;\n start?: number;\n end?: number;\n}\n\n/**\n * Materialized source window consumed by\n * `ContextPrimary.sourceWindows[]`. Always carries explicit line\n * numbers — Slice 4 only ever materializes claim-level spans, so\n * `start`/`end` are mandatory.\n */\ninterface SourceWindow {\n file: string;\n start: number;\n end: number;\n text: string;\n}\n\n/**\n * Flatten a page's `ClaimCitation[]` into the JSON-shape citation list.\n * Multi-source markers (`^[a.md, b.md]`) produce one object per span.\n * Paragraph-only citations stay (with no `start`/`end`).\n */\nexport function flattenCitations(citations: ClaimCitation[]): FlatCitation[] {\n const out: FlatCitation[] = [];\n const seen = new Set<string>();\n for (const citation of citations) {\n for (const span of citation.spans) {\n const flat = toFlatCitation(span);\n const key = citationKey(flat);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(flat);\n }\n }\n return out;\n}\n\n/** Lift one `SourceSpan` into the public flat shape. */\nfunction toFlatCitation(span: SourceSpan): FlatCitation {\n if (!span.lines) return { file: span.file };\n return { file: span.file, start: span.lines.start, end: span.lines.end };\n}\n\n/** Deduplication key. Uses `-` for missing line ranges; safe because lines are positive integers. */\nfunction citationKey(citation: FlatCitation): string {\n const start = citation.start ?? \"-\";\n const end = citation.end ?? \"-\";\n return [citation.file, String(start), String(end)].join(CITATION_KEY_SEPARATOR);\n}\n\n/**\n * Per-pack source-window budget accumulator passed into\n * {@link materializeSourceWindows}. The orchestrator owns one\n * instance and shares it across all primary pages so the global\n * 20-window ceiling is enforced even when many pages each carry many\n * citations.\n */\ninterface SourceWindowBudget {\n remaining: number;\n}\n\n/** Create a fresh budget seeded with the documented per-pack cap. */\nexport function createSourceWindowBudget(): SourceWindowBudget {\n return { remaining: MAX_SOURCE_WINDOWS };\n}\n\n/**\n * Materialize source windows for one page's citations. Returns an\n * empty array when the citation list is empty, when no entries carry\n * line ranges (paragraph-only), or when the budget is exhausted.\n */\nexport async function materializeSourceWindows(\n root: string,\n citations: FlatCitation[],\n budget: SourceWindowBudget,\n): Promise<SourceWindow[]> {\n if (budget.remaining <= 0) return [];\n const windows: SourceWindow[] = [];\n for (const citation of citations) {\n if (budget.remaining <= 0) break;\n if (citation.start === undefined || citation.end === undefined) continue;\n const window = await readSourceWindow(root, citation);\n if (!window) continue;\n windows.push(window);\n budget.remaining -= 1;\n }\n return windows;\n}\n\n/** Read one source window or return null when the read is rejected by the path guard. */\nasync function readSourceWindow(\n root: string,\n citation: FlatCitation,\n): Promise<SourceWindow | null> {\n if (citation.start === undefined || citation.end === undefined) return null;\n const sourcesRoot = await resolveSourcesRoot(root);\n if (!sourcesRoot) return null;\n const realPath = await resolveSafePath(sourcesRoot, citation.file);\n if (!realPath) return null;\n return readClampedWindow(realPath, citation);\n}\n\n/**\n * Resolve the canonical `sources/` directory. Returns null when the\n * directory doesn't exist; otherwise returns the realpath so\n * `isInside` checks against canonical paths on platforms with\n * symlinked parents (macOS `/var` → `/private/var`, Linux container\n * bind mounts, etc.). Path confinement still rejects files whose\n * realpath escapes this canonical root, so a symlinked sources/ tree\n * remains safe.\n */\nasync function resolveSourcesRoot(root: string): Promise<string | null> {\n const candidate = path.join(root, SOURCES_DIR);\n try {\n return await fs.realpath(candidate);\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve `file` under `sourcesRoot`, rejecting:\n * - absolute paths (`/etc/passwd`)\n * - ANY `..` segment, even one that would normalize back inside\n * sources/ (`nested/../paper.md` is rejected — the spec says\n * \"reject parent traversal\", and \"looks safe after normalization\"\n * is still traversal a reviewer must be able to spot in the raw\n * citation marker)\n * - paths whose realpath escapes `sourcesRoot` (symlinks pointing\n * outside `sources/`)\n */\nasync function resolveSafePath(sourcesRoot: string, file: string): Promise<string | null> {\n if (file.length === 0) return null;\n if (path.isAbsolute(file)) return null;\n if (containsParentSegment(file)) return null;\n const joined = path.join(sourcesRoot, file);\n const resolved = path.resolve(joined);\n // Defense in depth: even after the segment guard, if the resolved\n // path escapes sources/ (e.g., a Windows drive-letter trick or a\n // separator we did not recognize) refuse it.\n if (!isInside(sourcesRoot, resolved)) return null;\n try {\n const realPath = await fs.realpath(resolved);\n if (!isInside(sourcesRoot, realPath)) return null;\n return realPath;\n } catch {\n return null;\n }\n}\n\n/**\n * True when any segment of `file` is literally `..`. Splits on BOTH\n * `/` and the platform separator so a malicious `nested\\..\\paper.md`\n * on Linux (where `\\` is a legal filename char) is still rejected when\n * served to a Windows agent reading the same context pack.\n */\nfunction containsParentSegment(file: string): boolean {\n const segments = file.split(/[/\\\\]/);\n return segments.some((segment) => segment === \"..\");\n}\n\n/**\n * True when `candidate` is `parent` itself or a descendant of it.\n * Adds a path separator suffix to `parent` before the prefix check so\n * `/parent` does not incorrectly contain `/parentish`.\n */\nfunction isInside(parent: string, candidate: string): boolean {\n if (candidate === parent) return true;\n const normalizedParent = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`;\n return candidate.startsWith(normalizedParent);\n}\n\n/**\n * Read the requested line window, clamping to a maximum length so a\n * runaway citation cannot pull hundreds of lines into the pack. Both\n * `start` and `end` are 1-indexed inclusive (matching the\n * `SourceSpan` convention).\n */\nasync function readClampedWindow(\n realPath: string,\n citation: FlatCitation,\n): Promise<SourceWindow | null> {\n if (citation.start === undefined || citation.end === undefined) return null;\n let raw: string;\n try {\n raw = await fs.readFile(realPath, \"utf-8\");\n } catch {\n return null;\n }\n const lines = raw.split(/\\r?\\n/);\n const startIndex = Math.max(0, citation.start - 1);\n const inclusiveEnd = Math.min(lines.length, citation.end);\n if (startIndex >= inclusiveEnd) return null;\n const clampedEnd = Math.min(inclusiveEnd, startIndex + MAX_LINES_PER_WINDOW);\n const text = lines.slice(startIndex, clampedEnd).join(\"\\n\");\n return { file: citation.file, start: citation.start, end: startIndex + (clampedEnd - startIndex), text };\n}\n","/**\n * Multi-signal ranking for `llmwiki context`.\n *\n * Slice 1 wired lexical + exact-match signals. Slice 2 layers semantic\n * chunk retrieval on top of the same combiner (plan §Ranking Model);\n * the wrapper in `src/context/retrieval.ts` keeps the orchestrator\n * unaware of whether the embedding store contributed.\n *\n * Signals fed into the combiner:\n * - `searchPages().results[].matchedIn` distinguishes title vs body\n * hits without re-parsing the body.\n * - Exact slug equality (case-insensitive after lowercasing) adds a\n * strong bonus and the `exact-slug` reason.\n * - Exact title equality (case-insensitive, trimmed) adds the\n * strongest bonus and the `exact-title` reason.\n * - Semantic chunk hits add a high-weight `semantic-chunk` reason\n * and attach the chunk text/score/contentHash to the primary page;\n * additional chunks on the same page contribute a capped bonus so\n * a popular page can't shadow exact-title hits.\n *\n * Scores are normalized into [0, 1] best-effort — they're explainable\n * to the agent but not a quality guarantee. Stable tie-sort: descending\n * score, then ascending title, then ascending page ID.\n */\n\nimport { searchPages } from \"../viewer/search.js\";\nimport type { ViewerPage, ViewerSnapshot } from \"../viewer/types.js\";\nimport type { SemanticChunkHit } from \"./retrieval.js\";\nimport { flattenCitations } from \"./provenance.js\";\nimport type { ContextPrimary, PrimaryReason } from \"./types.js\";\n\n/** Per-signal weight (sums normalize so an exact-title hit lands near 1.0). */\nconst WEIGHT_TITLE_MATCH = 0.5;\nconst WEIGHT_BODY_MATCH = 0.3;\nconst WEIGHT_EXACT_SLUG = 0.4;\nconst WEIGHT_EXACT_TITLE = 0.5;\n\n/** Base weight for the first semantic chunk a page contributes. Peer of title-match. */\nconst WEIGHT_SEMANTIC_CHUNK = 0.5;\n/** Per-additional-chunk bonus once the first semantic hit on a page has landed. */\nconst WEIGHT_SEMANTIC_CHUNK_BONUS = 0.05;\n/** Max additional chunks (beyond the first) that contribute the per-chunk bonus. */\nconst MAX_SEMANTIC_BONUS_CHUNKS = 3;\n\n/** Cap so combined scores stay inside the normalized [0, 1] range. */\nconst MAX_NORMALIZED_SCORE = 1;\n\n/**\n * One semantic chunk surfaced for a primary page. Matches the inline\n * `ContextPrimary.chunks[]` shape in `types.ts`; declared locally so the\n * ranker can build it without exporting the structural anonymous type.\n */\ninterface PrimaryChunk {\n text: string;\n score: number;\n contentHash: string;\n}\n\n/** Internal accumulator: one row per candidate page. */\ninterface RankingRow {\n page: ViewerPage;\n reasons: Set<PrimaryReason>;\n weight: number;\n snippet: string;\n /** Chunks attached by `applySemanticSignals`; empty in lexical-only flows. */\n chunks: PrimaryChunk[];\n}\n\n/**\n * Rank candidate pages for `prompt` against `snapshot`, returning up to\n * `topN` populated {@link ContextPrimary} entries.\n *\n * `semanticHits` is the post-retrieval chunk list from Slice 2; pass an\n * empty array (the default) for lexical-only behaviour. Citations and\n * sourceWindows stay empty in Slice 2 — those fields land in Slice 4.\n * Page-local `warnings` is sourced from the viewer collector.\n */\nexport function rankPages(\n snapshot: ViewerSnapshot,\n prompt: string,\n topN: number,\n semanticHits: SemanticChunkHit[] = [],\n): ContextPrimary[] {\n const rows = new Map<string, RankingRow>();\n applyLexicalSignals(rows, snapshot, prompt);\n applyExactSignals(rows, snapshot, prompt);\n applySemanticSignals(rows, snapshot, semanticHits);\n const sorted = Array.from(rows.values()).sort(compareRows);\n return sorted.slice(0, Math.max(0, topN)).map(rowToPrimary);\n}\n\n/** Push every `searchPages()` hit into the ranking map. */\nfunction applyLexicalSignals(\n rows: Map<string, RankingRow>,\n snapshot: ViewerSnapshot,\n prompt: string,\n): void {\n const { results } = searchPages(snapshot, prompt);\n for (const result of results) {\n const page = snapshot.pages.find((p) => p.id === result.id);\n if (!page) continue;\n const row = ensureRow(rows, page);\n row.snippet = row.snippet || result.snippet;\n if (result.matchedIn === \"title\") {\n addReason(row, \"title-match\", WEIGHT_TITLE_MATCH);\n } else {\n addReason(row, \"body-match\", WEIGHT_BODY_MATCH);\n }\n }\n}\n\n/** Bonus pass for exact-slug and exact-title matches across all pages. */\nfunction applyExactSignals(\n rows: Map<string, RankingRow>,\n snapshot: ViewerSnapshot,\n prompt: string,\n): void {\n const normalized = prompt.trim().toLowerCase();\n if (normalized.length === 0) return;\n for (const page of snapshot.pages) {\n if (page.slug.toLowerCase() === normalized) {\n addReason(ensureRow(rows, page), \"exact-slug\", WEIGHT_EXACT_SLUG);\n }\n if (page.title.trim().toLowerCase() === normalized) {\n addReason(ensureRow(rows, page), \"exact-title\", WEIGHT_EXACT_TITLE);\n }\n }\n}\n\n/**\n * Apply semantic chunk signals: each retrieved chunk is grouped by page\n * slug, the page gets the `semantic-chunk` reason once, a capped\n * multi-chunk bonus accrues for additional chunks on the same page, and\n * the chunk records are attached to the row for `primary[].chunks[]`.\n *\n * Chunks for slugs that no longer exist in the viewer snapshot are\n * dropped silently — the embedding store can lag behind page deletions\n * and we don't want to fabricate a phantom primary entry just because\n * the store carries a stale chunk.\n */\nfunction applySemanticSignals(\n rows: Map<string, RankingRow>,\n snapshot: ViewerSnapshot,\n hits: SemanticChunkHit[],\n): void {\n if (hits.length === 0) return;\n const bySlug = groupHitsBySlug(hits);\n for (const [slug, slugHits] of bySlug) {\n const page = findPageBySlug(snapshot, slug);\n if (!page) continue;\n const row = ensureRow(rows, page);\n addReason(row, \"semantic-chunk\", WEIGHT_SEMANTIC_CHUNK);\n row.weight += semanticMultiChunkBonus(slugHits.length);\n for (const hit of slugHits) {\n row.chunks.push({\n text: hit.text,\n score: hit.score,\n contentHash: hit.contentHash,\n });\n }\n }\n}\n\n/** Group chunk hits by page slug while preserving the score-desc input order. */\nfunction groupHitsBySlug(hits: SemanticChunkHit[]): Map<string, SemanticChunkHit[]> {\n const bySlug = new Map<string, SemanticChunkHit[]>();\n for (const hit of hits) {\n const existing = bySlug.get(hit.slug);\n if (existing) existing.push(hit);\n else bySlug.set(hit.slug, [hit]);\n }\n return bySlug;\n}\n\n/** Score bump for the 2nd..N-th chunk on the same page, capped. */\nfunction semanticMultiChunkBonus(chunkCount: number): number {\n const extra = Math.max(0, Math.min(chunkCount - 1, MAX_SEMANTIC_BONUS_CHUNKS));\n return extra * WEIGHT_SEMANTIC_CHUNK_BONUS;\n}\n\n/**\n * Resolve a bare chunk slug to a snapshot page, with concepts winning\n * over queries when both contain the same slug — matches the bare-slug\n * precedence rule used by the viewer's wikilink resolver.\n */\nfunction findPageBySlug(snapshot: ViewerSnapshot, slug: string): ViewerPage | null {\n const concept = snapshot.pages.find(\n (p) => p.pageDirectory === \"concepts\" && p.slug === slug,\n );\n if (concept) return concept;\n const query = snapshot.pages.find(\n (p) => p.pageDirectory === \"queries\" && p.slug === slug,\n );\n return query ?? null;\n}\n\n/** Fetch the row for `page`, lazily allocating it on first use. */\nfunction ensureRow(rows: Map<string, RankingRow>, page: ViewerPage): RankingRow {\n const existing = rows.get(page.id);\n if (existing) return existing;\n const created: RankingRow = {\n page,\n reasons: new Set(),\n weight: 0,\n snippet: \"\",\n chunks: [],\n };\n rows.set(page.id, created);\n return created;\n}\n\n/** Record one reason + weight; reasons set de-dupes naturally. */\nfunction addReason(row: RankingRow, reason: PrimaryReason, weight: number): void {\n row.reasons.add(reason);\n row.weight += weight;\n}\n\n/** Stable sort: descending score, ascending title, ascending page ID. */\nfunction compareRows(a: RankingRow, b: RankingRow): number {\n if (a.weight !== b.weight) return b.weight - a.weight;\n const byTitle = a.page.title.localeCompare(b.page.title);\n if (byTitle !== 0) return byTitle;\n return a.page.id.localeCompare(b.page.id);\n}\n\n/**\n * Build the `warnings` array for a primary page, forwarding viewer-level\n * warnings and appending freshness-derived warnings for stale, contradicted,\n * and archived pages. These codes are context-only — the viewer surfaces\n * freshness as badges, not warnings. A page can carry multiple warnings\n * (e.g. stale AND contradicted → both codes appear).\n */\nfunction buildPrimaryWarnings(page: ViewerPage): ContextPrimary[\"warnings\"] {\n const warnings = page.warnings.map((w) => ({ code: w.code, message: w.message }));\n if (page.freshness.freshnessStatus === \"stale\") {\n warnings.push({\n code: \"stale-page\",\n message:\n \"A source this page was compiled from has changed since the last compile; treat with caution.\",\n });\n }\n if (page.freshness.contradicted) {\n warnings.push({\n code: \"contradicted-page\",\n message: \"This page is contradicted by another page; treat its claims with caution.\",\n });\n }\n if (page.freshness.archived) {\n warnings.push({\n code: \"archived-page\",\n message: \"This page is archived and may be outdated or deprecated.\",\n });\n }\n return warnings;\n}\n\n/**\n * Convert a ranking row into a ContextPrimary.\n *\n * Slice 4 fills `citations` from the viewer collector's already-parsed\n * `ClaimCitation[]` (one object per source span, paragraph-only spans\n * keep no line range, multi-source markers split, deduped by\n * `(file,start,end)`, document-order preserved).\n *\n * Page-local `warnings` was wired in Slice 1; it forwards the same\n * `ViewerWarning` objects the viewer surfaces, so any malformed-frontmatter\n * or unresolved-citation diagnostic the viewer already knows about\n * lands in the context pack automatically. A `stale-page` warning is\n * synthesized here when the page's freshness status is `stale`.\n *\n * `sourceWindows` stays empty here — the orchestrator owns the\n * per-pack budget and writes windows back into the primary entries\n * after ranking, so the ranker remains a pure function over the\n * snapshot.\n */\nfunction rowToPrimary(row: RankingRow): ContextPrimary {\n const { freshness } = row.page;\n return {\n id: row.page.id,\n title: row.page.title,\n pageDirectory: row.page.pageDirectory,\n score: normalizeWeight(row.weight),\n reasons: Array.from(row.reasons).sort(),\n summary: row.snippet,\n chunks: row.chunks,\n citations: flattenCitations(row.page.citations),\n sourceWindows: [],\n warnings: buildPrimaryWarnings(row.page),\n freshnessStatus: freshness.freshnessStatus,\n contradicted: freshness.contradicted,\n archived: freshness.archived,\n };\n}\n\n/** Squash accumulated weight into [0, 1] without inventing precision. */\nfunction normalizeWeight(weight: number): number {\n if (weight <= 0) return 0;\n if (weight >= MAX_NORMALIZED_SCORE) return MAX_NORMALIZED_SCORE;\n return Math.round(weight * 100) / 100;\n}\n","/**\n * Semantic retrieval wrapper for `llmwiki context` Slice 2.\n *\n * Wraps `readEmbeddingStore()` + `findRelevantChunks()` so the\n * orchestrator never has to special-case provider failures, missing\n * stores, or stale-model stores. Semantic retrieval is opportunistic\n * here — context packs must keep working on lexical signals alone when\n * the embedding store is absent OR the active provider has no\n * credentials. The wrapper translates both failure modes into stable\n * warning codes (`embedding-store-missing` / `query-embedding-unavailable`)\n * so the JSON contract stays predictable for agents regardless of which\n * branch fired.\n *\n * Stale-model stores are folded into the `embedding-store-missing`\n * branch. We detect them HERE — not by waiting for `findRelevantChunks`\n * to return `[]` — because the embeddings module emits a\n * `output.status(\"!\", ...)` warning to stdout via `console.log` when\n * `loadActiveStore` sees a stale model. That leak would corrupt\n * `llmwiki context --json` output (stdout must be pure JSON). Catching\n * the model mismatch up front avoids the embeddings module's warning\n * path entirely.\n *\n * Malformed embedding store files (truncated writes, hand-edits, etc.)\n * are also folded into `embedding-store-missing` rather than propagated\n * as crashes. `readEmbeddingStore` does not catch its own JSON parse\n * failures, so the wrapper guards both the read and the parse so\n * `context` keeps producing parseable output even when the store on\n * disk is broken.\n */\n\nimport {\n findRelevantChunks,\n readEmbeddingStore,\n resolveEmbeddingModel,\n} from \"../utils/embeddings.js\";\nimport type { EmbeddingStore } from \"../utils/embeddings.js\";\n\n/** Stable warning code returned when semantic retrieval did not contribute. */\nexport type SemanticRetrievalWarning =\n | \"embedding-store-missing\"\n | \"query-embedding-unavailable\"\n | \"semantic-retrieval-error\";\n\n/**\n * Slimmed chunk record passed from retrieval into ranking. Keeps\n * `ranking.ts` independent of the underlying `ChunkEmbeddingEntry`\n * shape so the embedding store can evolve without churn here.\n */\nexport interface SemanticChunkHit {\n /** Source page slug; bare-slug resolved against the viewer snapshot in ranking. */\n slug: string;\n /** Chunk body text — surfaced verbatim in `primary[].chunks[].text`. */\n text: string;\n /** Cosine similarity from `findTopKChunks`; pass-through into the chunk entry. */\n score: number;\n /** Stable hash of the chunk text; pass-through into the chunk entry. */\n contentHash: string;\n}\n\n/**\n * Outcome of one semantic retrieval call. Either `hits` is populated and\n * `warning` is `null`, or `hits` is empty and `warning` carries the\n * stable code. We use mutually-exclusive null/value rather than a tagged\n * union so callers can spread both fields into the envelope without\n * widening narrow types.\n */\nexport interface SemanticRetrievalOutcome {\n hits: SemanticChunkHit[];\n warning: SemanticRetrievalWarning | null;\n}\n\n/**\n * Best-effort semantic retrieval. Returns the top-k chunks the active\n * embedding store can offer for `prompt`, OR a warning that explains\n * why semantic retrieval contributed nothing this call.\n *\n * Precondition: caller already knows the original prompt should be\n * passed (not the truncated display copy). Slice 2 wires the orchestrator\n * to pass `NormalizedOptions.rankingPrompt`.\n */\nexport async function retrieveSemanticChunks(\n root: string,\n prompt: string,\n topChunks: number,\n): Promise<SemanticRetrievalOutcome> {\n if (topChunks <= 0) return emptyOutcome(null);\n if (await isStoreUnusable(root)) return emptyOutcome(\"embedding-store-missing\");\n\n let raw: Awaited<ReturnType<typeof findRelevantChunks>>;\n try {\n raw = await findRelevantChunks(root, prompt, topChunks);\n } catch (err) {\n // Provider/config failures are expected in credential-free context\n // runs. Unknown exceptions still fall back to lexical, but receive\n // a distinct warning so real bugs are not mislabeled as auth.\n return emptyOutcome(classifyRetrievalError(err));\n }\n\n if (raw.length === 0) {\n // Defensive: with the upfront stale-model + chunk-count checks the\n // only way to land here is a TOCTOU race where the store changed\n // between the pre-check read and `findRelevantChunks`. Surface as\n // `embedding-store-missing` so the warning vocabulary stays small.\n return emptyOutcome(\"embedding-store-missing\");\n }\n\n return { hits: raw.map(toSemanticChunkHit), warning: null };\n}\n\n/** Build the empty-hits outcome shape; centralised to keep callers terse. */\nfunction emptyOutcome(warning: SemanticRetrievalWarning | null): SemanticRetrievalOutcome {\n return { hits: [], warning };\n}\n\n/**\n * True when the on-disk embedding store cannot supply chunks: file\n * missing, JSON malformed, v1 / empty v2 store, OR built with a\n * different embedding model than the active provider is using.\n *\n * The stale-model check MUST happen here (before any `findRelevantChunks`\n * call) so the embeddings module's stale-store warning — which writes\n * to stdout via `output.status` — never fires. That warning would\n * corrupt `--json` output. Same reasoning applies to malformed-JSON\n * reads: `readEmbeddingStore` lets `JSON.parse` throw, which would\n * crash the command with exit 1 unless we catch it here.\n */\nasync function isStoreUnusable(root: string): Promise<boolean> {\n const store = await tryReadEmbeddingStore(root);\n if (!store) return true;\n if (!store.chunks || store.chunks.length === 0) return true;\n if (isStaleModel(store)) return true;\n return false;\n}\n\n/**\n * Wrap `readEmbeddingStore` so a missing OR malformed file both reduce\n * to `null`. The reader does `await readFile` + `JSON.parse` without\n * its own catch, so a broken store would otherwise surface as an\n * unhandled rejection to the caller.\n */\nasync function tryReadEmbeddingStore(root: string): Promise<EmbeddingStore | null> {\n try {\n return await readEmbeddingStore(root);\n } catch {\n return null;\n }\n}\n\n/**\n * Compare the persisted store's embedding model against the active\n * provider's resolved model. Returns true when they disagree so the\n * caller can fall back without triggering the embeddings module's\n * stdout warning. Defensive: a thrown `resolveEmbeddingModel` (e.g.\n * unknown `LLMWIKI_PROVIDER`) is also treated as stale.\n */\nfunction isStaleModel(store: EmbeddingStore): boolean {\n try {\n return store.model !== resolveEmbeddingModel();\n } catch {\n return true;\n }\n}\n\n/** Classify failures without leaking raw provider or stack text into JSON. */\nfunction classifyRetrievalError(err: unknown): SemanticRetrievalWarning {\n const message = err instanceof Error ? err.message : String(err);\n return looksLikeProviderFailure(message)\n ? \"query-embedding-unavailable\"\n : \"semantic-retrieval-error\";\n}\n\n/** Known provider/config/network failures that should keep the legacy warning code. */\nfunction looksLikeProviderFailure(message: string): boolean {\n return /api[_ -]?key|auth|credential|token|provider|voyage|openai|ollama|timeout|fetch|econn|enotfound/i\n .test(message);\n}\n\n/** Project an embedding-store chunk hit onto the ranking-facing shape. */\nfunction toSemanticChunkHit(\n raw: Awaited<ReturnType<typeof findRelevantChunks>>[number],\n): SemanticChunkHit {\n return {\n slug: raw.chunk.slug,\n text: raw.chunk.text,\n score: raw.score,\n contentHash: raw.chunk.contentHash,\n };\n}\n","/**\n * Graph neighborhood expansion for `llmwiki context` Slice 3.\n *\n * Pure function over `ViewerSnapshot.graph` (built once at snapshot\n * time by `src/viewer/graph.ts::buildGraphData`) plus the orchestrator's\n * resolved primary IDs. Returns the documented `neighbors[]` and\n * `gaps[]` arrays without rebuilding adjacency from disk and without\n * touching the embeddings store.\n *\n * Topology rules pinned by the implementation plan §Graph Expansion:\n * - real-page-only neighbors (ghosts go to `gaps[]` instead, with\n * `code: \"dangling-link\"` and `pageId` set to the page that linked\n * to the missing target)\n * - primary pages never appear in `neighbors[]`\n * - bidirectional edges (A->B and B->A) collapse to a single neighbor\n * entry via the canonical unordered key `(min(a,b), max(a,b))`\n * - depth 1 expands directly from each primary page\n * - depth 2 expands from surviving depth-1 real neighbors, skipping\n * primary back-edges and pages already at depth 1; cycles are\n * blocked by a visited set\n * - `--no-neighbors` and `--depth 0` produce empty `neighbors` /\n * `gaps` arrays without crashing\n *\n * Scores are deterministic and normalized into [0, 1]; direct neighbors\n * outrank second-hop, pages with multiple primary connections get a\n * small additive bonus, and same-kind page pairs get a smaller schema\n * affinity bonus. Stable tie-sort: descending score, then\n * ascending `to` page id (Slice 3 does not have neighbor titles handy\n * without an extra lookup, and id is already monotonic per directory).\n */\n\nimport type {\n GraphData,\n GraphNode,\n PageId,\n ViewerPage,\n} from \"../viewer/types.js\";\n\n/** Closed v1 neighbor edge label. */\nconst NEIGHBOR_REASON_WIKILINK = \"wikilink\";\n\n/**\n * Delimiter joining the two PageIds in a canonical pair key. Visible\n * ASCII (not a control byte) so the source file stays free of NUL\n * bytes that confuse grep/fallow/git tooling, and the substring\n * \" <-> \" cannot collide with any directory/slug character allowed in\n * a `PageId` (`concepts/<slug>` / `queries/<slug>`).\n */\nconst CANONICAL_PAIR_SEPARATOR = \" <-> \";\n\n/**\n * Hard cap on the total number of emitted neighbor entries (depth-1 +\n * depth-2 combined). Applied BEFORE depth-2 expansion so a high-degree\n * primary page cannot blow the context pack open by way of fan-out\n * through trimmed depth-1 bridges. Re-applied at the end so the\n * sorted, merged output never exceeds the bound either.\n */\nconst MAX_GRAPH_NEIGHBORS = 20;\n\n/** Base score for a direct (depth-1) neighbor edge. */\nconst WEIGHT_NEIGHBOR_DIRECT = 0.5;\n\n/** Base score for a second-hop (depth-2) neighbor edge. */\nconst WEIGHT_NEIGHBOR_SECOND_HOP = 0.25;\n\n/**\n * Additive bonus per additional primary connection beyond the first\n * (capped). Keeps centrally-linked neighbors above peripherally-linked\n * ones without letting a \"popular\" page accumulate unbounded weight.\n */\nconst WEIGHT_PRIMARY_CONNECTION_BONUS = 0.05;\nconst MAX_PRIMARY_CONNECTION_BONUS_HITS = 3;\n\n/** Small schema-affinity bump when both endpoints share the same page kind. */\nconst WEIGHT_SAME_KIND_BONUS = 0.03;\nconst DEFAULT_PAGE_KIND = \"concept\";\n\n/** Cap so combined scores stay inside the normalized [0, 1] range. */\nconst MAX_NORMALIZED_SCORE = 1;\n\n/** Reasons set on emitted neighbor entries. Closed v1 enum. */\ntype NeighborReason = typeof NEIGHBOR_REASON_WIKILINK;\n\n/** Direction of the underlying wikilink relative to `from`. */\ntype NeighborDirection = \"outgoing\" | \"incoming\";\n\n/** One neighbor entry in the v1 context-pack envelope. */\ninterface GraphNeighbor {\n from: PageId;\n to: PageId;\n direction: NeighborDirection;\n distance: number;\n score: number;\n reason: NeighborReason;\n}\n\n/** Gap entry surfaced when a primary page links to a missing target. */\ninterface GraphGap {\n code: \"dangling-link\";\n message: string;\n pageId: PageId;\n}\n\n/** Output of {@link expandGraphNeighborhood}. */\nexport interface GraphExpansionOutput {\n neighbors: GraphNeighbor[];\n gaps: GraphGap[];\n}\n\n/** Inputs grouped to keep argument lists short. */\ninterface GraphExpansionInput {\n graph: GraphData;\n pages: ViewerPage[];\n primaryIds: ReadonlySet<PageId>;\n /** 0 = expansion off; 1 = direct only; 2 = direct + second-hop. */\n depth: number;\n}\n\n/**\n * Expand `primaryIds` into the documented `{ neighbors, gaps }` shape.\n * Returns empty arrays — never `undefined`/`null` — so the orchestrator\n * can splice the result into the JSON envelope unconditionally.\n */\nexport function expandGraphNeighborhood(\n input: GraphExpansionInput,\n): GraphExpansionOutput {\n // Depth 0 OR no primary IDs: both `neighbors[]` and `gaps[]` stay\n // empty (plan §CLI Acceptance Criteria). `--no-neighbors` short-\n // circuits before this function is called; this branch covers\n // `--depth 0` and the empty-wiki path.\n if (input.depth <= 0 || input.primaryIds.size === 0) {\n return { neighbors: [], gaps: [] };\n }\n const adjacency = buildAdjacency(input.graph);\n const ghostIds = collectGhostIds(input.graph.nodes);\n const pageKinds = buildPageKindMap(input.pages);\n // Sort + cap depth-1 BEFORE depth-2 expansion so a trimmed-out\n // bridge can never contribute second-hop emissions. Without this\n // gate, a primary page with high fan-out could fan out further at\n // depth 2 and balloon `neighbors[]` past the documented cap.\n const depth1Raw = expandDepthOne({\n primaryIds: input.primaryIds,\n adjacency,\n ghostIds,\n pageKinds,\n });\n const depth1 = depth1Raw.sort(compareNeighbors).slice(0, MAX_GRAPH_NEIGHBORS);\n const depth2 = input.depth >= 2\n ? expandDepthTwo({\n primaryIds: input.primaryIds,\n adjacency,\n ghostIds,\n pageKinds,\n depthOneTargets: collectDepthOneTargets(depth1),\n })\n : [];\n // Re-cap after merging so depth-2 entries cannot push the total\n // above the bound either. Direct (distance 1) entries outrank\n // second-hop in the comparator so the trim drops second-hop first.\n const neighbors = [...depth1, ...depth2]\n .sort(compareNeighbors)\n .slice(0, MAX_GRAPH_NEIGHBORS);\n return { neighbors, gaps: emitGapsFromPrimary(input) };\n}\n\n/** Surface dangling-link gaps for every primary page that owns one. */\nfunction emitGapsFromPrimary(input: GraphExpansionInput): GraphGap[] {\n const gaps: GraphGap[] = [];\n for (const page of input.pages) {\n if (!input.primaryIds.has(page.id)) continue;\n for (const dangling of page.danglingLinks ?? []) {\n gaps.push({\n code: \"dangling-link\",\n message: `Page links to [[${dangling.display}]], but no page exists.`,\n pageId: page.id,\n });\n }\n }\n return gaps;\n}\n\n/** Outgoing + incoming edge maps for fast neighbor lookup keyed by PageId. */\ninterface Adjacency {\n outgoing: Map<PageId, Set<PageId>>;\n incoming: Map<PageId, Set<PageId>>;\n}\n\n/** Build the bidirectional adjacency from the snapshot's edge list. */\nfunction buildAdjacency(graph: GraphData): Adjacency {\n const outgoing = new Map<PageId, Set<PageId>>();\n const incoming = new Map<PageId, Set<PageId>>();\n for (const edge of graph.edges) {\n addToSetMap(outgoing, edge.source, edge.target);\n addToSetMap(incoming, edge.target, edge.source);\n }\n return { outgoing, incoming };\n}\n\n/** Insert `value` into the set at `key`, creating the set on first use. */\nfunction addToSetMap<K, V>(map: Map<K, Set<V>>, key: K, value: V): void {\n const existing = map.get(key);\n if (existing) existing.add(value);\n else map.set(key, new Set([value]));\n}\n\n/** Collect every ghost-node id so we can drop dangling targets from neighbors. */\nfunction collectGhostIds(nodes: GraphNode[]): Set<PageId> {\n const ghosts = new Set<PageId>();\n for (const node of nodes) if (node.isDangling) ghosts.add(node.id);\n return ghosts;\n}\n\n/** Shared input for the depth-1 expansion path. */\ninterface DepthOneInput {\n primaryIds: ReadonlySet<PageId>;\n adjacency: Adjacency;\n ghostIds: ReadonlySet<PageId>;\n pageKinds: ReadonlyMap<PageId, string>;\n}\n\n/**\n * Depth-1 expansion: for each primary page, walk outgoing + incoming\n * edges and emit one entry per real-page neighbor. Bidirectional\n * duplicates collapse via the canonical-pair key.\n */\nfunction expandDepthOne(input: DepthOneInput): GraphNeighbor[] {\n const emitted = new Map<string, GraphNeighbor>();\n const connectionCount = new Map<PageId, number>();\n for (const primary of input.primaryIds) {\n addNeighborsForPrimary({ ...input, primary, emitted, connectionCount });\n }\n applyPrimaryConnectionBonus(emitted, connectionCount);\n return Array.from(emitted.values());\n}\n\n/** Helpers passed into the per-primary depth-1 walker. */\ninterface DepthOnePerPrimary extends DepthOneInput {\n primary: PageId;\n emitted: Map<string, GraphNeighbor>;\n connectionCount: Map<PageId, number>;\n}\n\n/** Walk every edge incident to one primary page and emit canonicalized neighbors. */\nfunction addNeighborsForPrimary(ctx: DepthOnePerPrimary): void {\n walkIncidentEdges(ctx.adjacency, ctx.primary, (other, direction) => {\n tryEmitDirect({ ctx, other, direction });\n });\n}\n\n/** Inputs for the per-edge depth-1 emission check. */\ninterface EmitDirectInput {\n ctx: DepthOnePerPrimary;\n other: PageId;\n direction: NeighborDirection;\n}\n\n/**\n * Emit a depth-1 neighbor edge if it survives the no-ghost / no-primary\n * / canonical-key filters. Bumps the per-target connection counter\n * either way so multi-primary connections get the documented bonus\n * even when the second edge would otherwise be skipped as a duplicate.\n */\nfunction tryEmitDirect(input: EmitDirectInput): void {\n const { ctx, other, direction } = input;\n if (ctx.ghostIds.has(other)) return;\n if (ctx.primaryIds.has(other)) return;\n bumpConnection(ctx.connectionCount, other);\n mergeOrInsertNeighbor(ctx.emitted, {\n from: ctx.primary,\n to: other,\n direction,\n distance: 1,\n score: scoreWithSameKindBonus(\n WEIGHT_NEIGHBOR_DIRECT,\n ctx.primary,\n other,\n ctx.pageKinds,\n ),\n });\n}\n\n/** Increment the per-target connection counter (used for the multi-primary bonus). */\nfunction bumpConnection(counter: Map<PageId, number>, target: PageId): void {\n counter.set(target, (counter.get(target) ?? 0) + 1);\n}\n\n/** Empty PageId set reused across walks to avoid allocating per call. */\nconst EMPTY_NEIGHBOR_SET: ReadonlySet<PageId> = new Set<PageId>();\n\n/**\n * Walk every wikilink edge incident to `node` and invoke `onEdge` with\n * the other endpoint plus the direction relative to `node`. Centralised\n * so depth-1 and depth-2 walkers share the outgoing-then-incoming\n * iteration order without copy-pasting the boilerplate.\n */\nfunction walkIncidentEdges(\n adjacency: Adjacency,\n node: PageId,\n onEdge: (other: PageId, direction: NeighborDirection) => void,\n): void {\n const outgoing = adjacency.outgoing.get(node) ?? EMPTY_NEIGHBOR_SET;\n const incoming = adjacency.incoming.get(node) ?? EMPTY_NEIGHBOR_SET;\n for (const target of outgoing) onEdge(target, \"outgoing\");\n for (const source of incoming) onEdge(source, \"incoming\");\n}\n\n/** Candidate neighbor about to be merged into the canonical-key map. */\ninterface NeighborCandidate {\n from: PageId;\n to: PageId;\n direction: NeighborDirection;\n distance: number;\n score: number;\n}\n\n/**\n * Insert `candidate` into `emitted` under its canonical-pair key OR,\n * if an entry for that pair already exists, promote its direction\n * from `incoming` to `outgoing` when this edge represents the natural\n * `from -> to` link. The dedup keeps bidirectional pairs (A->B AND\n * B->A) collapsed to a single neighbor entry per the plan.\n */\nfunction mergeOrInsertNeighbor(\n emitted: Map<string, GraphNeighbor>,\n candidate: NeighborCandidate,\n): void {\n const key = canonicalPairKey(candidate.from, candidate.to);\n const existing = emitted.get(key);\n if (existing) {\n if (existing.direction === \"incoming\" && candidate.direction === \"outgoing\") {\n existing.direction = \"outgoing\";\n }\n return;\n }\n emitted.set(key, { ...candidate, reason: NEIGHBOR_REASON_WIKILINK });\n}\n\n/**\n * After every primary contributes, apply the per-additional-connection\n * score bonus to each neighbor entry. The first connection is the base\n * weight; only the 2nd..(MAX+1)-th add the per-hit bonus.\n */\nfunction applyPrimaryConnectionBonus(\n emitted: Map<string, GraphNeighbor>,\n connectionCount: Map<PageId, number>,\n): void {\n for (const neighbor of emitted.values()) {\n const hits = connectionCount.get(neighbor.to) ?? 0;\n const extraHits = Math.min(\n Math.max(0, hits - 1),\n MAX_PRIMARY_CONNECTION_BONUS_HITS,\n );\n neighbor.score = clampScore(\n neighbor.score + extraHits * WEIGHT_PRIMARY_CONNECTION_BONUS,\n );\n }\n}\n\n/** Inputs for depth-2 expansion. */\ninterface DepthTwoInput {\n primaryIds: ReadonlySet<PageId>;\n adjacency: Adjacency;\n ghostIds: ReadonlySet<PageId>;\n pageKinds: ReadonlyMap<PageId, string>;\n depthOneTargets: ReadonlySet<PageId>;\n}\n\n/**\n * Depth-2 expansion: walk from each surviving depth-1 neighbor and\n * emit edges to NEW real pages (not primary, not already at depth 1,\n * not ghosts). Bidirectional pairs at depth 2 also collapse via the\n * canonical-pair key. Back-edges to primary are dropped because that\n * relationship is already represented by the primary's depth-1 entry.\n */\nfunction expandDepthTwo(input: DepthTwoInput): GraphNeighbor[] {\n const emitted = new Map<string, GraphNeighbor>();\n for (const bridge of input.depthOneTargets) {\n walkDepthTwoFromBridge({ ...input, bridge, emitted });\n }\n return Array.from(emitted.values());\n}\n\n/** Per-bridge depth-2 walker context; bridge is the depth-1 neighbor we expand from. */\ninterface DepthTwoPerBridge extends DepthTwoInput {\n bridge: PageId;\n emitted: Map<string, GraphNeighbor>;\n}\n\n/** Walk every edge incident to one depth-1 bridge node. */\nfunction walkDepthTwoFromBridge(ctx: DepthTwoPerBridge): void {\n walkIncidentEdges(ctx.adjacency, ctx.bridge, (other, direction) => {\n tryEmitSecondHop({ ctx, other, direction });\n });\n}\n\n/** Inputs for the per-edge depth-2 emission check. */\ninterface EmitSecondHopInput {\n ctx: DepthTwoPerBridge;\n other: PageId;\n direction: NeighborDirection;\n}\n\n/**\n * Emit a depth-2 neighbor edge if the target is a real page that is\n * neither primary nor already in the depth-1 neighbor set. Same\n * canonical-key de-dupe rule as depth 1; direction prefers outgoing\n * on bidirectional collisions to keep the natural reading order.\n */\nfunction tryEmitSecondHop(input: EmitSecondHopInput): void {\n const { ctx, other, direction } = input;\n if (ctx.ghostIds.has(other)) return;\n if (ctx.primaryIds.has(other)) return;\n if (ctx.depthOneTargets.has(other)) return;\n mergeOrInsertNeighbor(ctx.emitted, {\n from: ctx.bridge,\n to: other,\n direction,\n distance: 2,\n score: scoreWithSameKindBonus(\n WEIGHT_NEIGHBOR_SECOND_HOP,\n ctx.bridge,\n other,\n ctx.pageKinds,\n ),\n });\n}\n\n/** Map page ID to schema/page kind, defaulting legacy pages to concept. */\nfunction buildPageKindMap(pages: ViewerPage[]): Map<PageId, string> {\n const kinds = new Map<PageId, string>();\n for (const page of pages) {\n const kind = page.frontmatter.kind;\n kinds.set(page.id, typeof kind === \"string\" && kind.length > 0 ? kind : DEFAULT_PAGE_KIND);\n }\n return kinds;\n}\n\n/** Add the small kind-affinity bonus when both real endpoints share a kind. */\nfunction scoreWithSameKindBonus(\n base: number,\n from: PageId,\n to: PageId,\n pageKinds: ReadonlyMap<PageId, string>,\n): number {\n return samePageKind(from, to, pageKinds)\n ? clampScore(base + WEIGHT_SAME_KIND_BONUS)\n : base;\n}\n\n/** True when both endpoints have an equal page kind in the real-page map. */\nfunction samePageKind(\n from: PageId,\n to: PageId,\n pageKinds: ReadonlyMap<PageId, string>,\n): boolean {\n const fromKind = pageKinds.get(from);\n const toKind = pageKinds.get(to);\n return fromKind !== undefined && toKind !== undefined && fromKind === toKind;\n}\n\n/** Collect just the `to` ids from a depth-1 neighbor list for fast lookups. */\nfunction collectDepthOneTargets(neighbors: GraphNeighbor[]): Set<PageId> {\n const ids = new Set<PageId>();\n for (const n of neighbors) ids.add(n.to);\n return ids;\n}\n\n/**\n * Canonical key for an unordered page pair so A->B and B->A collapse\n * to a single neighbor entry. `String.localeCompare` keeps the\n * ordering platform-stable for any unicode-bearing PageId.\n */\nfunction canonicalPairKey(a: PageId, b: PageId): string {\n return a < b\n ? `${a}${CANONICAL_PAIR_SEPARATOR}${b}`\n : `${b}${CANONICAL_PAIR_SEPARATOR}${a}`;\n}\n\n/** Squash a score into [0, 1] without inventing precision. */\nfunction clampScore(weight: number): number {\n if (weight <= 0) return 0;\n if (weight >= MAX_NORMALIZED_SCORE) return MAX_NORMALIZED_SCORE;\n return Math.round(weight * 100) / 100;\n}\n\n/**\n * Stable sort: descending score, then ascending `to` id. We don't have\n * neighbor titles at this layer; the namespaced PageId already sorts\n * deterministically per directory so agents see a fixed ordering.\n */\nfunction compareNeighbors(a: GraphNeighbor, b: GraphNeighbor): number {\n if (a.score !== b.score) return b.score - a.score;\n return a.to.localeCompare(b.to);\n}\n","/**\n * Token budget estimation + deterministic trimming for `llmwiki context`.\n *\n * V1 uses a deterministic `Math.ceil(chars / 4)` heuristic so the token\n * count is stable across runs and identical between CLI and MCP. The\n * helper is isolated so a real tokenizer can replace it later without\n * touching the orchestrator (see plan §Budgeting).\n *\n * The estimator is intentionally pessimistic on the high side: a 4-char\n * average is slightly low for English prose, so packets that pass\n * `estimatedTokens <= requestedTokens` will fit comfortably in a real\n * tokenizer's count as well.\n *\n * Trimming order (plan §Budgeting §Trimming order):\n * 1. neighbors\n * 2. source windows\n * 3. semantic chunks / excerpts\n * 4. primary pages (last resort)\n *\n * Trim functions mutate a deep-cloned copy of the pack so the caller's\n * draft is never observably modified. JSON validity is preserved at\n * every step because we drop entire array elements rather than\n * surgically slicing strings.\n */\n\nimport type { ContextBudget, ContextPack } from \"./types.js\";\n\n/** Average chars-per-token used by the cheap heuristic. */\nconst APPROX_CHARS_PER_TOKEN = 4;\n\n/** Section names that can land in `budget.trimmedSections`. */\ntype TrimmedSection = \"neighbors\" | \"sourceWindows\" | \"chunks\" | \"primary\";\n\n/**\n * Estimate token count for an arbitrary string. Always returns a\n * non-negative integer. The empty string and `null`/`undefined` map to\n * zero; non-string inputs are coerced.\n */\nexport function estimateTokens(text: unknown): number {\n if (text === null || text === undefined) return 0;\n const stringified = typeof text === \"string\" ? text : String(text);\n if (stringified.length === 0) return 0;\n return Math.ceil(stringified.length / APPROX_CHARS_PER_TOKEN);\n}\n\n/**\n * Estimate the total token weight of a serialized context pack. Uses\n * the same heuristic as {@link estimateTokens}; runs on the\n * already-rendered JSON string so trimming decisions can iterate\n * without rebuilding the structured envelope from scratch.\n */\nexport function estimatePackTokens(pack: ContextPack): number {\n return estimateTokens(JSON.stringify(pack));\n}\n\n/**\n * Build a fresh budget envelope before estimation/trimming runs.\n * Defaults preserve the v1 contract (`truncated: false`,\n * `trimmedSections: []`); the orchestrator overwrites these in\n * `finalizeBudget` once trimming decisions are known.\n */\nexport function buildBudget(requestedTokens: number, estimatedTokens: number): ContextBudget {\n return {\n requestedTokens,\n estimatedTokens,\n truncated: false,\n trimmedSections: [],\n };\n}\n\n/** Result returned by {@link trimToBudget}; pack is a NEW object, never the input. */\ninterface TrimResult {\n pack: ContextPack;\n trimmedSections: TrimmedSection[];\n}\n\n/**\n * Deterministically trim `pack` until `estimatePackTokens(pack) <=\n * requestedTokens`, in the documented order:\n * neighbors -> sourceWindows -> chunks -> primary\n *\n * Each section is drained from the END of its array so the highest-\n * scored entries survive longest (ranking output is already sorted\n * descending). The returned pack is always a deep clone — the caller's\n * draft is left untouched.\n *\n * If the minimal envelope (no neighbors, no sourceWindows, no chunks,\n * no primary) still exceeds the budget, the pack is returned as-is\n * with `trimmedSections` listing every section we touched so consumers\n * can see the failure mode.\n */\nexport function trimToBudget(pack: ContextPack, requestedTokens: number): TrimResult {\n const clone = clonePack(pack);\n const trimmed = new Set<TrimmedSection>();\n if (estimatePackTokens(clone) <= requestedTokens) {\n return { pack: clone, trimmedSections: [] };\n }\n trimNeighbors(clone, requestedTokens, trimmed);\n trimSourceWindows(clone, requestedTokens, trimmed);\n trimChunks(clone, requestedTokens, trimmed);\n trimPrimary(clone, requestedTokens, trimmed);\n return { pack: clone, trimmedSections: orderedSections(trimmed) };\n}\n\n/** Deep-clone via `structuredClone` so per-step mutations cannot leak. */\nfunction clonePack(pack: ContextPack): ContextPack {\n return structuredClone(pack);\n}\n\n/** Re-emit trimmed sections in the documented trim order, not insertion order. */\nfunction orderedSections(trimmed: Set<TrimmedSection>): TrimmedSection[] {\n const order: TrimmedSection[] = [\"neighbors\", \"sourceWindows\", \"chunks\", \"primary\"];\n return order.filter((section) => trimmed.has(section));\n}\n\n/** Drop neighbors from the end of the sorted list until the pack fits or none remain. */\nfunction trimNeighbors(\n pack: ContextPack,\n budget: number,\n trimmed: Set<TrimmedSection>,\n): void {\n while (pack.neighbors.length > 0 && estimatePackTokens(pack) > budget) {\n pack.neighbors.pop();\n trimmed.add(\"neighbors\");\n }\n}\n\n/**\n * Drop source windows page-by-page from the bottom-ranked primary\n * entry first so the most relevant page keeps its provenance evidence\n * longest. Walks until either the pack fits or every page has zero\n * windows.\n */\nfunction trimSourceWindows(\n pack: ContextPack,\n budget: number,\n trimmed: Set<TrimmedSection>,\n): void {\n for (let i = pack.primary.length - 1; i >= 0; i--) {\n while (pack.primary[i].sourceWindows.length > 0 && estimatePackTokens(pack) > budget) {\n pack.primary[i].sourceWindows.pop();\n trimmed.add(\"sourceWindows\");\n }\n if (estimatePackTokens(pack) <= budget) return;\n }\n}\n\n/** Drop semantic chunks page-by-page using the same bottom-up strategy. */\nfunction trimChunks(\n pack: ContextPack,\n budget: number,\n trimmed: Set<TrimmedSection>,\n): void {\n for (let i = pack.primary.length - 1; i >= 0; i--) {\n while (pack.primary[i].chunks.length > 0 && estimatePackTokens(pack) > budget) {\n pack.primary[i].chunks.pop();\n trimmed.add(\"chunks\");\n }\n if (estimatePackTokens(pack) <= budget) return;\n }\n}\n\n/** Final resort: drop primary pages from the bottom of the ranked list. */\nfunction trimPrimary(\n pack: ContextPack,\n budget: number,\n trimmed: Set<TrimmedSection>,\n): void {\n while (pack.primary.length > 0 && estimatePackTokens(pack) > budget) {\n pack.primary.pop();\n trimmed.add(\"primary\");\n }\n}\n","/**\n * Stable v1 JSON contract for `llmwiki context` and the future\n * `get_context_pack` MCP tool.\n *\n * Every top-level field and every documented nested key is present from\n * Slice 1 onward, even when later-slice features have not populated\n * them yet (see `localdocs/context-graph-packs-implementation-plan.md`\n * §JSON Contract). Unpopulated list fields are empty arrays; absent\n * object fields are `null`. Slices may fill data into these fields,\n * but must NEVER add or remove top-level keys without bumping\n * `version`.\n */\n\nimport type { PageId } from \"../viewer/types.js\";\nimport type { PageDirectory } from \"../export/types.js\";\nimport type { RecommendedAction } from \"../project/recommendations.js\";\nimport type { FreshnessStatus } from \"../freshness/types.js\";\n\n/** Closed v1 enum for why a page landed in `primary[]`. */\nexport type PrimaryReason =\n | \"semantic-chunk\"\n | \"title-match\"\n | \"body-match\"\n | \"exact-slug\"\n | \"exact-title\"\n | \"graph-neighbor\";\n\n/** Closed v1 enum for the edge label used in `neighbors[]`. */\ntype NeighborReason = \"wikilink\";\n\n/** Closed v1 enum for top-level `warnings[]` codes. */\ntype ContextWarningCode =\n | \"embedding-store-missing\"\n | \"query-embedding-unavailable\"\n | \"semantic-retrieval-error\"\n | \"lint-errors\"\n | \"pending-candidates\"\n | \"source-window-unavailable\"\n | \"truncated-prompt\";\n\n/** Closed v1 enum for `gaps[]` codes. */\ntype ContextGapCode = \"dangling-link\" | \"page-warning\";\n\n/**\n * Budget envelope. `estimatedTokens` uses a tokens ≈ chars/4 heuristic in v1.\n * Because `estimatedTokens` is itself serialized inside the measured JSON, the\n * reported value may differ from `estimatePackTokens(returnedPack)` by at most\n * one token of digit-count drift.\n */\nexport interface ContextBudget {\n requestedTokens: number;\n estimatedTokens: number;\n truncated: boolean;\n /** Section keys (`primary`, `neighbors`, `sourceWindows`, `chunks`) that lost data. */\n trimmedSections: string[];\n}\n\n/**\n * Cached lint summary surfaced inside `project.lint`. Matches\n * `LintCacheEntry` in `src/linter/cache.ts` but typed locally so the\n * context contract does not depend on the linter's internal shape.\n */\ninterface ContextLintSummary {\n warnings: number;\n errors: number;\n at: string;\n}\n\n/** Project block. `root` is set to `null` when `--omit-root` is supplied. */\nexport interface ContextProject {\n root: string | null;\n pages: number;\n pendingCandidates: number;\n lint: ContextLintSummary | null;\n}\n\n/** One semantic chunk surfaced for a primary page. Slice 2 populates it. */\ninterface ContextChunk {\n text: string;\n score: number;\n contentHash?: string;\n}\n\n/**\n * Flattened citation. Produced by lifting `ClaimCitation.spans` into one\n * object per span. Paragraph-only citations omit `start` and `end`.\n */\ninterface ContextCitation {\n file: string;\n start?: number;\n end?: number;\n}\n\n/** Source line window emitted only when `--include-sources` is set in Slice 4. */\ninterface ContextSourceWindow {\n file: string;\n start: number;\n end: number;\n text: string;\n}\n\n/** Page-local warning surfaced from the viewer collector. */\ninterface ContextPageWarning {\n code: string;\n message: string;\n}\n\n/** One primary page entry. `reasons` is sorted alphabetically for stable output. */\nexport interface ContextPrimary {\n id: PageId;\n title: string;\n pageDirectory: PageDirectory;\n score: number;\n reasons: PrimaryReason[];\n summary: string;\n chunks: ContextChunk[];\n citations: ContextCitation[];\n sourceWindows: ContextSourceWindow[];\n warnings: ContextPageWarning[];\n // The three freshness signals are FLATTENED here (rather than nested as a\n // `freshness: PageFreshness` object like `ViewerPage`) for an idiomatic JSON\n // wire shape for agents. Consequently, any future field added to\n // `PageFreshness` must be mirrored here by hand — it won't auto-propagate.\n /** Computed source-freshness of this page (advisory snapshot, not a guarantee). */\n freshnessStatus: FreshnessStatus;\n /** Disputed by another page (`contradictedBy` non-empty). */\n contradicted: boolean;\n /** Explicitly archived (`archived: true` frontmatter). */\n archived: boolean;\n}\n\n/** One graph neighbor edge. `distance` is 1 for direct, 2 for second-hop. */\ninterface ContextNeighbor {\n from: PageId;\n to: PageId;\n direction: \"outgoing\" | \"incoming\";\n distance: number;\n score: number;\n reason: NeighborReason;\n}\n\n/** Top-level context-pack state warning. */\nexport interface ContextWarning {\n code: ContextWarningCode;\n message: string;\n}\n\n/**\n * Missing-knowledge gap. `pageId` is required in v1; every documented\n * gap code (`dangling-link`, `page-warning`) is tied to a specific\n * page. A future project-wide gap would either bump `version` or\n * introduce a new sibling field rather than retrofitting nullability\n * onto this one.\n */\ninterface ContextGap {\n code: ContextGapCode;\n message: string;\n pageId: PageId;\n}\n\n/** Top-level v1 envelope. */\nexport interface ContextPack {\n version: 1;\n prompt: string;\n budget: ContextBudget;\n project: ContextProject;\n primary: ContextPrimary[];\n neighbors: ContextNeighbor[];\n warnings: ContextWarning[];\n gaps: ContextGap[];\n suggestedActions: RecommendedAction[];\n}\n\n/** Hard cap on the echoed prompt; ranking still uses the original. */\nexport const PROMPT_ECHO_MAX_LENGTH = 1024;\n\n/** Default budget when `--budget` is omitted. */\nexport const DEFAULT_BUDGET_TOKENS = 8000;\n\n/** Default depth for graph expansion (1 = direct neighbors only). */\nexport const DEFAULT_DEPTH = 1;\n\n/** Hard upper bound on `--depth` per plan §Graph Expansion. */\nexport const MAX_DEPTH = 2;\n\n/** Default cap on `primary[]` page count. */\nexport const DEFAULT_TOP_PAGES = 5;\n\n/** Hard cap on `primary[]` page count to keep trimming and output bounded. */\nexport const MAX_TOP_PAGES = 20;\n\n/** Default value pinned for `--top-chunks` (plan §Ranking Model). */\nexport const DEFAULT_TOP_CHUNKS = 8;\n\n/** Hard cap on semantic chunks to keep budget trimming predictably small. */\nexport const MAX_TOP_CHUNKS = 50;\n","/**\n * `buildContextPack()` — Slice 1 orchestrator.\n *\n * Composes the v1 context-pack envelope by:\n * 1. Loading the frozen viewer snapshot (page metadata, frontmatter,\n * citations, warnings, etc).\n * 2. Collecting project state via the shared `collectProjectState`\n * helper so the lint cache and pending-candidate counts match\n * what `llmwiki next` would report.\n * 3. Lexically ranking pages against the prompt via the Slice-1\n * ranker.\n * 4. Delegating the recommendation prefix to\n * `recommendNextAction(state)` so `context` does not introduce a\n * second project-state engine.\n *\n * Semantic retrieval, graph expansion, source windows, and MCP are\n * intentionally left for later slices; this entry point already emits\n * the full stable v1 JSON field set with empty arrays / `null`\n * placeholders so agents written against Slice 1 will not break when\n * later slices add data.\n */\n\nimport { buildViewerSnapshot } from \"../viewer/snapshot.js\";\nimport { collectProjectState } from \"../project/state.js\";\nimport { recommendNextAction } from \"../project/recommendations.js\";\nimport type { Recommendation, RecommendedAction } from \"../project/recommendations.js\";\nimport type { ProjectState } from \"../project/state.js\";\nimport type { ViewerSnapshot } from \"../viewer/types.js\";\nimport { rankPages } from \"./ranking.js\";\nimport { retrieveSemanticChunks } from \"./retrieval.js\";\nimport type { SemanticRetrievalOutcome, SemanticRetrievalWarning } from \"./retrieval.js\";\nimport { expandGraphNeighborhood } from \"./graph.js\";\nimport type { GraphExpansionOutput } from \"./graph.js\";\nimport {\n createSourceWindowBudget,\n materializeSourceWindows,\n} from \"./provenance.js\";\nimport type { GraphData, PageId } from \"../viewer/types.js\";\nimport { buildBudget, estimatePackTokens, trimToBudget } from \"./budget.js\";\nimport {\n DEFAULT_BUDGET_TOKENS,\n DEFAULT_DEPTH,\n DEFAULT_TOP_CHUNKS,\n DEFAULT_TOP_PAGES,\n MAX_DEPTH,\n MAX_TOP_CHUNKS,\n MAX_TOP_PAGES,\n PROMPT_ECHO_MAX_LENGTH,\n} from \"./types.js\";\nimport type {\n ContextPack,\n ContextProject,\n ContextWarning,\n} from \"./types.js\";\n\n/** Caller-supplied build options; all fields except `prompt` are optional. */\ninterface BuildContextPackOptions {\n /** Project root; defaults to `process.cwd()` at the call site. */\n root: string;\n /** Free-text prompt the agent supplied. */\n prompt: string;\n /** Token budget; defaults to {@link DEFAULT_BUDGET_TOKENS}. */\n budget?: number;\n /** Graph depth; clamped to {@link MAX_DEPTH} when supplied. */\n depth?: number;\n /** Max primary pages; clamped into the documented safe range. */\n topPages?: number;\n /** Max semantic chunks; pinned to the documented safe range. */\n topChunks?: number;\n /** When true, `project.root` is emitted as `null` for privacy. */\n omitRoot?: boolean;\n /** When false, graph expansion is suppressed (neighbors + gaps stay empty). */\n neighbors?: boolean;\n /** When true, populate `primary[].sourceWindows` from claim-level citation spans. */\n includeSources?: boolean;\n}\n\n/**\n * Build the v1 context pack. Never throws on read-only filesystem\n * issues — the project-state collector returns conservative defaults\n * (`broken-project` state) which the orchestrator faithfully surfaces.\n */\nexport async function buildContextPack(options: BuildContextPackOptions): Promise<ContextPack> {\n const normalized = normalizeOptions(options);\n const snapshot = await buildViewerSnapshot(options.root);\n const state = await collectProjectState(options.root);\n const recommendation = recommendNextAction(state);\n // Semantic retrieval is opportunistic — failures surface as stable\n // warning codes on the returned outcome rather than thrown errors so\n // lexical-only flows stay the default behaviour for credential-free\n // users.\n const semantic = await retrieveSemanticChunks(\n options.root,\n normalized.rankingPrompt,\n normalized.topChunks,\n );\n const draft = assembleDraft({\n snapshot,\n state,\n recommendation,\n options: normalized,\n semantic,\n });\n // Source windows are materialized AFTER ranking so the per-pack\n // budget sees the final primary list, not every candidate.\n // Skipped entirely when `--include-sources` is off.\n const withSources = normalized.includeSources\n ? await attachSourceWindows(draft, options.root)\n : draft;\n // Project-state warnings (pending candidates, lint errors,\n // source-window unavailability) need the post-materialization\n // primary list, so they land after attachSourceWindows and BEFORE\n // budget trimming. Trimming may drop sourceWindows for budget\n // reasons; the warning still correctly reports \"windows missing\".\n const withProjectWarnings = appendProjectWarnings(withSources, state, normalized);\n const graph = normalized.neighborsEnabled && normalized.depth >= 1\n ? snapshot.graph\n : null;\n return finalizeBudget(withProjectWarnings, normalized.budget, graph);\n}\n\n/**\n * Walk the ranked primary list once with a shared\n * {@link createSourceWindowBudget}, filling in each entry's\n * `sourceWindows` from its (already-flattened) claim-level citations.\n * Pages without claim-level spans (paragraph-only or no citations)\n * return empty windows; the global 20-window cap is enforced as the\n * walk progresses.\n */\nasync function attachSourceWindows(pack: ContextPack, root: string): Promise<ContextPack> {\n const budget = createSourceWindowBudget();\n // Serial loop — `budget.remaining` is mutated between awaits inside\n // `materializeSourceWindows`, so running pages in parallel would\n // race on the shared counter and overshoot the 20-window cap.\n const primary: ContextPack[\"primary\"] = [];\n for (const entry of pack.primary) {\n const windows = await materializeSourceWindows(root, entry.citations, budget);\n primary.push({ ...entry, sourceWindows: windows });\n }\n return { ...pack, primary };\n}\n\n/**\n * Frozen, validated copy of the user-supplied options.\n *\n * The prompt is intentionally split into two fields:\n * - `displayPrompt` is the echo-safe form that lands in\n * `ContextPack.prompt` (truncated at PROMPT_ECHO_MAX_LENGTH so the\n * envelope cannot balloon on a 10KB agent input).\n * - `rankingPrompt` is the original, untruncated prompt that flows\n * into every retrieval signal — lexical, semantic (Slice 2+), and\n * exact match. Truncating before ranking would silently drop\n * content the agent expected to drive selection.\n *\n * In Slice 1 the two values are observationally equivalent at the\n * lexical layer because `searchPages` caps queries at its own internal\n * MAX_QUERY_LENGTH (200 chars) and exact-match requires whole-prompt\n * equality. Slice 2's semantic retrieval will see the full\n * `rankingPrompt` and produce different scores than it would against\n * `displayPrompt`.\n */\ninterface NormalizedOptions {\n displayPrompt: string;\n rankingPrompt: string;\n budget: number;\n depth: number;\n topPages: number;\n topChunks: number;\n omitRoot: boolean;\n neighborsEnabled: boolean;\n includeSources: boolean;\n promptTruncated: boolean;\n}\n\n/** Apply defaults and clamps so downstream code can trust the field types. */\nfunction normalizeOptions(options: BuildContextPackOptions): NormalizedOptions {\n const rankingPrompt = options.prompt ?? \"\";\n const { display, truncated } = truncatePrompt(rankingPrompt);\n return {\n displayPrompt: display,\n rankingPrompt,\n budget: clampPositive(options.budget, DEFAULT_BUDGET_TOKENS),\n depth: clampDepth(options.depth),\n topPages: clampBounded(options.topPages, DEFAULT_TOP_PAGES, MAX_TOP_PAGES),\n topChunks: clampBounded(options.topChunks, DEFAULT_TOP_CHUNKS, MAX_TOP_CHUNKS),\n omitRoot: options.omitRoot === true,\n // `--no-neighbors` is a Commander negated flag: absence means\n // expansion is ON; only `options.neighbors === false` disables it.\n neighborsEnabled: options.neighbors !== false,\n includeSources: options.includeSources === true,\n promptTruncated: truncated,\n };\n}\n\n/** Truncate the echoed prompt without mutating the prompt used for ranking. */\nfunction truncatePrompt(raw: string): { display: string; truncated: boolean } {\n if (raw.length <= PROMPT_ECHO_MAX_LENGTH) return { display: raw, truncated: false };\n return { display: raw.slice(0, PROMPT_ECHO_MAX_LENGTH), truncated: true };\n}\n\n/** Clamp a numeric option to a non-negative integer with a fallback default. */\nfunction clampPositive(value: number | undefined, fallback: number): number {\n if (value === undefined || !Number.isFinite(value)) return fallback;\n return Math.max(0, Math.floor(value));\n}\n\n/** Clamp a numeric option into `[0, max]` with a fallback default. */\nfunction clampBounded(value: number | undefined, fallback: number, max: number): number {\n return Math.min(max, clampPositive(value, fallback));\n}\n\n/** Clamp `--depth` into `[0, MAX_DEPTH]`. */\nfunction clampDepth(value: number | undefined): number {\n if (value === undefined || !Number.isFinite(value)) return DEFAULT_DEPTH;\n return Math.max(0, Math.min(MAX_DEPTH, Math.floor(value)));\n}\n\n/** Composite input passed to the assembler; grouped to keep argument lists short. */\ninterface AssembleInput {\n snapshot: ViewerSnapshot;\n state: ProjectState;\n recommendation: Recommendation;\n options: NormalizedOptions;\n semantic: SemanticRetrievalOutcome;\n}\n\n/** Build the unbudgeted draft pack before token-budget finalization. */\nfunction assembleDraft(input: AssembleInput): ContextPack {\n const { snapshot, state, recommendation, options, semantic } = input;\n const project = buildProject(snapshot, state, options.omitRoot);\n // Rank against the ORIGINAL prompt — the truncated echo is a\n // display-only courtesy and must not silently drop ranking signal.\n const primary = rankPages(snapshot, options.rankingPrompt, options.topPages, semantic.hits);\n const graphEnabled = options.neighborsEnabled && options.depth >= 1;\n const expansion = graphEnabled\n ? expandGraphNeighborhood({\n graph: snapshot.graph,\n pages: snapshot.pages,\n primaryIds: collectPrimaryIds(primary),\n depth: options.depth,\n })\n : emptyExpansion();\n // Additive `graph-neighbor` reason only: cannot promote a page into\n // primary[], only annotate entries that earned their slot via\n // semantic/lexical/exact signals AND link to another primary page.\n // Gated on `graphEnabled` so `--no-neighbors` / `--depth 0` runs\n // never surface the reason (no graph context to justify it).\n const annotatedPrimary = graphEnabled\n ? annotateGraphNeighbors(primary, snapshot.graph)\n : primary;\n return {\n version: 1,\n prompt: options.displayPrompt,\n budget: buildBudget(options.budget, 0),\n project,\n primary: annotatedPrimary,\n neighbors: expansion.neighbors,\n warnings: buildTopLevelWarnings(options.promptTruncated, semantic.warning),\n gaps: expansion.gaps,\n suggestedActions: collectSuggestedActions(recommendation, {\n hasPages: snapshot.pages.length > 0,\n semanticWarning: semantic.warning,\n }),\n };\n}\n\n/**\n * Append `graph-neighbor` to existing primary entries that have a\n * wikilink edge (outgoing OR incoming) to another primary entry. Pure:\n * never reorders, rescores, or admits new entries — only widens the\n * `reasons[]` set on entries that already passed ranking. Reasons stay\n * sorted alphabetically and de-duped so snapshot comparisons remain\n * stable.\n */\nfunction annotateGraphNeighbors(\n primary: ContextPack[\"primary\"],\n graph: GraphData,\n): ContextPack[\"primary\"] {\n if (primary.length < 2) return primary;\n const primaryIds = collectPrimaryIds(primary);\n const connected = new Set<PageId>();\n for (const edge of graph.edges) {\n if (primaryIds.has(edge.source) && primaryIds.has(edge.target)) {\n connected.add(edge.source);\n connected.add(edge.target);\n }\n }\n if (connected.size === 0) return primary;\n return primary.map((entry) => {\n if (!connected.has(entry.id)) return entry;\n if (entry.reasons.includes(\"graph-neighbor\")) return entry;\n const widened = Array.from(new Set([...entry.reasons, \"graph-neighbor\" as const])).sort();\n return { ...entry, reasons: widened };\n });\n}\n\n/** Collapse the ranked primary list into a Set keyed by PageId for fast lookups. */\nfunction collectPrimaryIds(primary: ContextPack[\"primary\"]): Set<PageId> {\n const ids = new Set<PageId>();\n for (const entry of primary) ids.add(entry.id);\n return ids;\n}\n\n/** Strip graph-derived reasons so they can be recomputed after budget trimming. */\nfunction stripGraphNeighborReason(\n primary: ContextPack[\"primary\"],\n): ContextPack[\"primary\"] {\n return primary.map((entry) => ({\n ...entry,\n reasons: entry.reasons.filter((reason) => reason !== \"graph-neighbor\"),\n }));\n}\n\n/** Reconcile graph-neighbor after trimToBudget may have removed a connected peer. */\nfunction reconcileGraphNeighborReasons(\n pack: ContextPack,\n graph: GraphData | null,\n): ContextPack {\n const stripped = stripGraphNeighborReason(pack.primary);\n const primary = graph ? annotateGraphNeighbors(stripped, graph) : stripped;\n return primary === pack.primary ? pack : { ...pack, primary };\n}\n\n/** Empty expansion used when `--no-neighbors` suppresses graph traversal. */\nfunction emptyExpansion(): GraphExpansionOutput {\n return { neighbors: [], gaps: [] };\n}\n\n/** Materialize the `project` block; `root` honors the `--omit-root` flag. */\nfunction buildProject(\n snapshot: ViewerSnapshot,\n state: ProjectState,\n omitRoot: boolean,\n): ContextProject {\n return {\n root: omitRoot ? null : snapshot.root,\n pages: snapshot.pages.length,\n pendingCandidates: state.pendingCandidates,\n lint: state.lint.entry,\n };\n}\n\n/**\n * Top-level state warnings. Slice 1 wired `truncated-prompt`; Slice 2\n * adds the two semantic-retrieval fallback codes so consumers can tell\n * \"lexical-only because no embeddings on disk\" apart from \"lexical-only\n * because the provider rejected our embed call.\"\n */\nfunction buildTopLevelWarnings(\n promptTruncated: boolean,\n retrievalWarning: SemanticRetrievalWarning | null,\n): ContextWarning[] {\n const warnings: ContextWarning[] = [];\n if (promptTruncated) {\n warnings.push({\n code: \"truncated-prompt\",\n message: `Prompt exceeded ${PROMPT_ECHO_MAX_LENGTH} characters; the echoed copy was truncated.`,\n });\n }\n if (retrievalWarning === \"embedding-store-missing\") {\n warnings.push({\n code: \"embedding-store-missing\",\n message:\n \"No usable embedding store found; semantic retrieval skipped. \" +\n \"Run `llmwiki compile` to populate embeddings.\",\n });\n } else if (retrievalWarning === \"query-embedding-unavailable\") {\n warnings.push({\n code: \"query-embedding-unavailable\",\n message:\n \"Could not embed the prompt with the active provider; \" +\n \"semantic retrieval skipped, lexical signals still applied.\",\n });\n } else if (retrievalWarning === \"semantic-retrieval-error\") {\n warnings.push({\n code: \"semantic-retrieval-error\",\n message:\n \"Semantic retrieval failed unexpectedly; \" +\n \"lexical signals still applied and raw provider errors were not exposed.\",\n });\n }\n return warnings;\n}\n\n/**\n * Flatten the recommendation engine's output into the array-shaped\n * `suggestedActions[]`. Per plan §Suggested Actions:\n * - index 0 is the primary recommendation\n * - subsequent entries are `otherActions` in declared order\n * - tests pin the prefix only; context-specific additions land in\n * later slices.\n */\ninterface ContextActionInput {\n hasPages: boolean;\n semanticWarning: SemanticRetrievalWarning | null;\n}\n\n/** Context-specific suffix: rebuild embeddings when pages exist but no store is usable. */\nconst CONTEXT_COMPILE_ACTION: RecommendedAction = {\n command: \"llmwiki compile\",\n reason: \"Refresh compiled pages and rebuild the embedding store for semantic context.\",\n executable: { binary: \"llmwiki\", args: [\"compile\"] },\n};\n\n/** Context-specific suffix: open the selected wiki when shared recommendations omit it. */\nconst CONTEXT_VIEW_ACTION: RecommendedAction = {\n command: \"llmwiki view --open\",\n reason: \"Browse the compiled wiki in the local viewer.\",\n executable: { binary: \"llmwiki\", args: [\"view\", \"--open\"] },\n};\n\n/** Context-specific suffix: use the same prompt to generate an answer after reviewing evidence. */\nconst CONTEXT_QUERY_ACTION: RecommendedAction = {\n command: 'llmwiki query \"<prompt>\"',\n reason: \"Generate an answer with the same prompt after reviewing this context pack.\",\n executable: { binary: \"llmwiki\", args: [\"query\"], placeholders: [\"prompt\"] },\n};\n\nfunction collectSuggestedActions(\n recommendation: Recommendation,\n input: ContextActionInput,\n): RecommendedAction[] {\n const actions: RecommendedAction[] = [];\n for (const action of [recommendation.recommended, ...recommendation.otherActions]) {\n appendUniqueAction(actions, action);\n }\n if (input.hasPages && input.semanticWarning === \"embedding-store-missing\") {\n appendUniqueAction(actions, CONTEXT_COMPILE_ACTION);\n }\n if (input.hasPages) {\n appendUniqueAction(actions, CONTEXT_VIEW_ACTION);\n appendUniqueAction(actions, CONTEXT_QUERY_ACTION);\n }\n return actions;\n}\n\n/** Add `candidate` unless an equivalent executable action is already present. */\nfunction appendUniqueAction(actions: RecommendedAction[], candidate: RecommendedAction): void {\n if (actions.some((action) => actionKey(action) === actionKey(candidate))) return;\n actions.push(candidate);\n}\n\n/** Dedupe by executable intent, falling back to display command for non-executable actions. */\nfunction actionKey(action: RecommendedAction): string {\n if (!action.executable) return action.command ?? action.reason;\n return `${action.executable.binary} ${action.executable.args.join(\" \")}`;\n}\n\n/**\n * Compute `budget.estimatedTokens` from the serialized draft and\n * trim down the lower-priority sections deterministically when the\n * draft overshoots `requestedTokens`. See `src/context/budget.ts`\n * for the trim order. Always emits valid JSON; the structured\n * envelope is mutated section-by-section rather than slicing the\n * serialized string.\n *\n * `budget.truncated` is `true` when EITHER any section was trimmed\n * OR the final estimated envelope still exceeds `requestedTokens`\n * (irreducible-envelope edge case: empty wiki + very small budget).\n * In the latter case `trimmedSections` stays `[]` — the closed\n * section-key contract is not bent to record \"nothing was trimmable\".\n *\n * `budget.estimatedTokens` is recomputed against the FINAL envelope\n * shape (post-trim, post-truncated-flag, post-trimmedSections) via a\n * two-pass estimate that converges on the digit-count fixed point of\n * `estimatedTokens` itself. The residual drift between the reported\n * `estimatedTokens` and `estimatePackTokens(returnedPack)` is bounded\n * to a single character of the integer's decimal representation —\n * well under one token for any realistic budget.\n */\nfunction finalizeBudget(\n draft: ContextPack,\n requestedTokens: number,\n graph: GraphData | null = null,\n): ContextPack {\n const initialEstimate = estimatePackTokens(draft);\n const trim = initialEstimate <= requestedTokens\n ? { pack: draft, trimmedSections: [] as string[] }\n : trimToBudget(draft, requestedTokens);\n const trimmedAny = trim.trimmedSections.length > 0;\n const reconciledPack = reconcileGraphNeighborReasons(trim.pack, graph);\n // First pass: write the eventual `truncated`/`trimmedSections`\n // strings into the budget block with a placeholder zero estimate so\n // the JSON shape the estimator sees matches what we'll ultimately\n // ship. Second pass: write the first estimate back and re-measure\n // so the reported value converges on the digit-count fixed point.\n const e1 = estimatePackTokens(\n applyBudget(reconciledPack, {\n requestedTokens, estimatedTokens: 0,\n truncated: trimmedAny, trimmedSections: trim.trimmedSections,\n }),\n );\n const e2 = estimatePackTokens(\n applyBudget(reconciledPack, {\n requestedTokens, estimatedTokens: e1,\n truncated: trimmedAny, trimmedSections: trim.trimmedSections,\n }),\n );\n const truncated = trimmedAny || e2 > requestedTokens;\n return applyBudget(reconciledPack, {\n requestedTokens,\n estimatedTokens: e2,\n truncated,\n trimmedSections: trim.trimmedSections,\n });\n}\n\n/** Replace `pack.budget` with `budget`, returning a fresh shallow-cloned envelope. */\nfunction applyBudget(pack: ContextPack, budget: ContextPack[\"budget\"]): ContextPack {\n return { ...pack, budget };\n}\n\n/**\n * Append `pending-candidates`, `lint-errors`, and\n * `source-window-unavailable` to the top-level warnings list.\n *\n * Runs AFTER source-window materialization so we can detect the gap\n * where `--include-sources` was on but a line-range citation\n * produced no window (path-confined rejection, missing source file,\n * 20-window cap reached, …). Runs BEFORE budget trimming so a\n * budget-induced window drop does NOT spuriously add the warning;\n * the trimmer mutates sourceWindows only after this pass.\n */\nfunction appendProjectWarnings(\n pack: ContextPack,\n state: ProjectState,\n options: NormalizedOptions,\n): ContextPack {\n const warnings = [...pack.warnings];\n if (state.pendingCandidates > 0) {\n warnings.push({\n code: \"pending-candidates\",\n message:\n `${state.pendingCandidates} review candidate${state.pendingCandidates === 1 ? \"\" : \"s\"} ` +\n \"pending approval. Run `llmwiki review list` to inspect.\",\n });\n }\n const lintErrors = state.lint.entry?.errors ?? 0;\n if (lintErrors > 0) {\n warnings.push({\n code: \"lint-errors\",\n message: `Last lint run reported ${lintErrors} error${lintErrors === 1 ? \"\" : \"s\"}.`,\n });\n }\n if (options.includeSources && hasUnmaterializedSpans(pack)) {\n warnings.push({\n code: \"source-window-unavailable\",\n message:\n \"One or more line-range citations did not produce a source window \" +\n \"(path-confined, missing source file, or per-pack window cap reached).\",\n });\n }\n return { ...pack, warnings };\n}\n\n/** True when any primary page has line-range citations missing matching sourceWindows. */\nfunction hasUnmaterializedSpans(pack: ContextPack): boolean {\n for (const entry of pack.primary) {\n const lineRangeCount = entry.citations.filter(\n (c) => c.start !== undefined && c.end !== undefined,\n ).length;\n if (lineRangeCount > entry.sourceWindows.length) return true;\n }\n return false;\n}\n","/**\n * Commander action for `llmwiki export [--target <name>]`.\n *\n * Transforms existing wiki content into portable export artifacts and writes\n * them into dist/exports/ (relative to the project root). Supports six formats:\n *\n * llms-txt — concise index per llmstxt.org spec → llms.txt\n * llms-full-txt — full content export → llms-full.txt\n * json — pages + metadata as JSON → wiki.json\n * json-ld — Schema.org JSON-LD graph → wiki.jsonld\n * graphml — directed link graph as XML → wiki.graphml\n * marp — Marp slide deck → wiki.md\n *\n * No LLM calls are made — export is a pure transformation of wiki content.\n */\n\nimport path from \"path\";\nimport { createRequire } from \"module\";\nimport { atomicWrite } from \"../utils/markdown.js\";\nimport * as output from \"../utils/output.js\";\nimport { collectExportPages } from \"../export/collect.js\";\nimport { buildLlmsTxt, buildLlmsFullTxt } from \"../export/llms-txt.js\";\nimport {\n buildJsonExport,\n buildJsonExportDocument,\n type BuildJsonExportOptions,\n type JsonExportDocument,\n} from \"../export/json-export.js\";\nimport { validateProjectId } from \"../export/project-id.js\";\nimport { buildJsonLd } from \"../export/json-ld.js\";\nimport { buildGraphml } from \"../export/graphml.js\";\nimport { buildMarp } from \"../export/marp.js\";\nimport { EXPORT_TARGETS, MARP_SOURCES } from \"../export/types.js\";\nimport type { ExportPage, ExportTarget, MarpSource } from \"../export/types.js\";\n\nconst require = createRequire(import.meta.url);\n\n/** Output paths relative to dist/exports/ within the project root. */\nconst EXPORT_DIR = \"dist/exports\";\n\n/** Map each target to its output filename. */\nconst TARGET_FILENAMES: Record<ExportTarget, string> = {\n \"llms-txt\": \"llms.txt\",\n \"llms-full-txt\": \"llms-full.txt\",\n json: \"wiki.json\",\n \"json-ld\": \"wiki.jsonld\",\n graphml: \"wiki.graphml\",\n marp: \"wiki.md\",\n};\n\n/** Options accepted by exportCommand and its programmatic entry point. */\nexport interface ExportOptions {\n /** Limit export to a single target. When absent all targets are produced. */\n target?: string;\n /**\n * For the marp target: which page kinds to include.\n * Accepts \"concepts\", \"queries\", or \"all\" (default when absent).\n */\n source?: string;\n /**\n * Optional bridge identifier embedded in the JSON export envelope.\n * Validated against the bridge contract regex\n * (`/^[a-z0-9][a-z0-9-]{0,62}$/`); invalid values throw before any\n * file is written.\n */\n projectId?: string;\n}\n\n/** Result returned by runExport for testing and MCP consumers. */\nexport interface ExportResult {\n /** Absolute paths of files that were written. */\n written: string[];\n /** Number of pages included in each export. */\n pageCount: number;\n}\n\n/** Resolve the human-readable project title from package.json, defaulting gracefully. */\nfunction resolveProjectTitle(root: string): string {\n try {\n const pkg = require(path.join(root, \"package.json\")) as { name?: string };\n return typeof pkg.name === \"string\" ? pkg.name : \"Knowledge Wiki\";\n } catch {\n return \"Knowledge Wiki\";\n }\n}\n\n/** Return true when the given string is a valid ExportTarget. */\nfunction isValidTarget(value: string): value is ExportTarget {\n return (EXPORT_TARGETS as readonly string[]).includes(value);\n}\n\n/** Return true when the given string is a valid MarpSource. */\nfunction isValidMarpSource(value: string): value is MarpSource {\n return (MARP_SOURCES as readonly string[]).includes(value);\n}\n\n/** Resolve and validate the marp source filter. Throws for unknown values. */\nfunction resolveMarpSource(rawSource: string | undefined): MarpSource {\n if (!rawSource) return \"all\";\n if (!isValidMarpSource(rawSource)) {\n throw new Error(\n `Unknown --source value \"${rawSource}\". Valid values: ${MARP_SOURCES.join(\", \")}`,\n );\n }\n return rawSource;\n}\n\n/** Inputs to {@link buildContent}. Grouped to keep the argument list short. */\ninterface BuildContentInputs {\n target: ExportTarget;\n pages: ExportPage[];\n projectTitle: string;\n marpSource: MarpSource;\n /** Optional bridge identifier; only consumed by the json target. */\n projectId?: string;\n}\n\n/** Build the content string for a single target. */\nfunction buildContent(inputs: BuildContentInputs): string {\n const { target, pages, projectTitle, marpSource, projectId } = inputs;\n switch (target) {\n case \"llms-txt\":\n return buildLlmsTxt(pages, projectTitle);\n case \"llms-full-txt\":\n return buildLlmsFullTxt(pages, projectTitle);\n case \"json\":\n return buildJsonExport(pages, projectId !== undefined ? { projectId } : {});\n case \"json-ld\":\n return buildJsonLd(pages);\n case \"graphml\":\n return buildGraphml(pages);\n case \"marp\":\n return buildMarp(pages, projectTitle, marpSource);\n }\n}\n\n/**\n * Compute the page count to report in the CLI summary. When marp is the\n * only target and --source narrows the deck, report the filtered count so\n * the summary doesn't overstate what was exported. Multi-target runs keep\n * the collected total because non-marp targets always include every page.\n */\nfunction computeReportedPageCount(\n pages: ExportPage[],\n targets: ExportTarget[],\n marpSource: MarpSource,\n): number {\n const onlyMarpTarget = targets.length === 1 && targets[0] === \"marp\";\n if (onlyMarpTarget && marpSource !== \"all\") {\n return pages.filter((p) => p.pageDirectory === marpSource).length;\n }\n return pages.length;\n}\n\n/**\n * Programmatic entry point for the export pipeline.\n * @param root - Absolute path to the project root directory.\n * @param options - Export options (optional target filter).\n * @returns Paths written and page count.\n */\nexport async function runExport(root: string, options: ExportOptions = {}): Promise<ExportResult> {\n const projectId =\n options.projectId !== undefined ? validateProjectId(options.projectId) : undefined;\n const pages = await collectExportPages(root);\n const projectTitle = resolveProjectTitle(root);\n\n const targets = resolveTargets(options.target);\n const marpSource = resolveMarpSource(options.source);\n const written: string[] = [];\n\n for (const target of targets) {\n const content = buildContent({ target, pages, projectTitle, marpSource, projectId });\n const outPath = path.join(root, EXPORT_DIR, TARGET_FILENAMES[target]);\n await atomicWrite(outPath, content);\n written.push(outPath);\n output.status(\"+\", output.success(`Exported ${target} → ${output.source(outPath)}`));\n }\n\n return { written, pageCount: computeReportedPageCount(pages, targets, marpSource) };\n}\n\n/**\n * Resolve the list of targets to run.\n * When a specific target is given it is validated; an error is thrown for unknown values.\n * Defaults to all targets.\n */\nfunction resolveTargets(rawTarget: string | undefined): ExportTarget[] {\n if (!rawTarget) return [...EXPORT_TARGETS];\n\n if (!isValidTarget(rawTarget)) {\n throw new Error(\n `Unknown export target \"${rawTarget}\". Valid targets: ${EXPORT_TARGETS.join(\", \")}`,\n );\n }\n\n return [rawTarget];\n}\n\n/**\n * Pure in-memory entry point — collects pages and returns the export document\n * object without writing any files or emitting console output.\n * @param root - Absolute path to the project root directory.\n * @param options - Optional export options (e.g. `projectId`).\n * @returns The JsonExportDocument object ready for programmatic consumption.\n */\nexport async function exportJson(\n root: string,\n options: BuildJsonExportOptions = {},\n): Promise<JsonExportDocument> {\n const pages = await collectExportPages(root);\n return buildJsonExportDocument(pages, options);\n}\n\n/**\n * CLI action for `llmwiki export`.\n * @param root - Project root directory (defaults to cwd).\n * @param options - Commander-parsed options.\n */\nexport default async function exportCommand(\n root: string,\n options: ExportOptions,\n): Promise<void> {\n output.header(\"Exporting wiki\");\n const { written, pageCount } = await runExport(root, options);\n output.status(\n \"✓\",\n output.success(`Done — ${pageCount} pages exported to ${written.length} file(s).`),\n );\n}\n","/**\n * Wiki page collector for the export subsystem.\n *\n * Thin wrapper over `src/wiki/collect.ts::collectRawWikiPages()` that\n * applies export-specific filters (drop orphaned and untitled pages) and\n * decorates each surviving record with the export-facing fields (summary,\n * sources, tags, timestamps, link slugs). The wikilink extraction regex\n * and slug-normalization helper live in `src/wiki/collect.ts` so both\n * export and viewer callers share one source.\n */\n\nimport path from \"path\";\nimport { collectRawWikiPages, extractWikilinkSlugs } from \"../wiki/collect.js\";\nimport type { RawWikiPage } from \"../wiki/collect.js\";\nimport { buildFreshnessSnapshot, computeFreshness } from \"../freshness/index.js\";\nimport type { FreshnessSnapshot } from \"../freshness/types.js\";\nimport { extractClaimCitations } from \"../utils/markdown.js\";\nimport { flattenCitations } from \"../context/provenance.js\";\nimport type { PageKind } from \"../schema/types.js\";\nimport type { ProvenanceState, ContradictionRef } from \"../utils/types.js\";\nimport { CONCEPTS_DIR, QUERIES_DIR } from \"../utils/constants.js\";\nimport {\n hashPageBody,\n resolveSourceHashes,\n sourceHashLookupFromSnapshot,\n type SourceHashLookup,\n} from \"./provenance.js\";\nimport type { ExportPage, PageDirectory } from \"./types.js\";\n\nexport { extractWikilinkSlugs };\n\n/** Resolve the project-relative path for a wiki page (e.g. `wiki/concepts/retrieval.md`). */\nfunction buildPagePath(pageDirectory: PageDirectory, slug: string): string {\n const dir = pageDirectory === \"concepts\" ? CONCEPTS_DIR : QUERIES_DIR;\n return path.posix.join(dir, `${slug}.md`);\n}\n\n/** Pull a typed string array out of frontmatter or return `[]` for missing/malformed input. */\nfunction readStringArray(meta: Record<string, unknown>, field: string): string[] {\n const value = meta[field];\n if (!Array.isArray(value)) return [];\n return (value as unknown[]).filter((s): s is string => typeof s === \"string\");\n}\n\n/** Read a numeric confidence value if present and in the [0, 1] range. */\nfunction readAdvisoryConfidence(meta: Record<string, unknown>): number | undefined {\n const value = meta.confidence;\n if (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n if (value < 0 || value > 1) return undefined;\n return value;\n}\n\n/** Validate and return a ProvenanceState from frontmatter, or undefined. */\nfunction readProvenanceState(meta: Record<string, unknown>): ProvenanceState | undefined {\n const value = meta.provenanceState;\n if (value === \"extracted\" || value === \"merged\" || value === \"inferred\" || value === \"ambiguous\") {\n return value;\n }\n return undefined;\n}\n\n/** Filter ContradictionRef entries to the documented `{ slug, reason? }` shape. */\nfunction readContradictedBy(meta: Record<string, unknown>): ContradictionRef[] | undefined {\n const raw = meta.contradictedBy;\n if (!Array.isArray(raw)) return undefined;\n const refs: ContradictionRef[] = [];\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Record<string, unknown>;\n if (typeof candidate.slug !== \"string\" || candidate.slug.length === 0) continue;\n const ref: ContradictionRef = { slug: candidate.slug };\n if (typeof candidate.reason === \"string\") ref.reason = candidate.reason;\n refs.push(ref);\n }\n return refs.length > 0 ? refs : undefined;\n}\n\n/** Validate and return PageKind from frontmatter, or undefined. */\nfunction readPageKind(meta: Record<string, unknown>): PageKind | undefined {\n const value = meta.kind;\n if (value === \"concept\" || value === \"entity\" || value === \"comparison\" || value === \"overview\") {\n return value;\n }\n return undefined;\n}\n\n/**\n * Normalize a kept page into the shape every export writer consumes.\n * Caller is responsible for filtering out records that fail the export\n * gate (orphaned, untitled, unreadable). The bridge contract additions\n * (path, kind, advisory*, citations, aliases) are populated here so\n * every export format gets the same enriched payload.\n */\nfunction toExportPage(\n raw: RawWikiPage,\n snapshot: FreshnessSnapshot,\n sourceHashes: SourceHashLookup,\n): ExportPage {\n const meta = raw.frontmatter;\n const aliases = readStringArray(meta, \"aliases\");\n const sources = readStringArray(meta, \"sources\");\n const freshness = computeFreshness(\n { slug: raw.slug, pageDirectory: raw.pageDirectory, frontmatter: meta },\n snapshot,\n );\n return {\n title: raw.title as string,\n slug: raw.slug,\n pageDirectory: raw.pageDirectory,\n path: buildPagePath(raw.pageDirectory, raw.slug),\n summary: typeof meta.summary === \"string\" ? meta.summary : \"\",\n sources,\n tags: readStringArray(meta, \"tags\"),\n createdAt: typeof meta.createdAt === \"string\" ? meta.createdAt : new Date().toISOString(),\n updatedAt: typeof meta.updatedAt === \"string\" ? meta.updatedAt : new Date().toISOString(),\n links: extractWikilinkSlugs(raw.body),\n body: raw.body,\n kind: readPageKind(meta),\n advisoryConfidence: readAdvisoryConfidence(meta),\n provenanceState: readProvenanceState(meta),\n contradictedBy: readContradictedBy(meta),\n citations: flattenCitations(extractClaimCitations(raw.body)),\n ...(aliases.length > 0 ? { aliases } : {}),\n freshnessStatus: freshness.freshnessStatus,\n contradicted: freshness.contradicted,\n archived: freshness.archived,\n contentHash: hashPageBody(raw.body),\n sourceHashes: resolveSourceHashes(sources, sourceHashes),\n ...(typeof meta.modelId === \"string\" ? { modelId: meta.modelId } : {}),\n ...(typeof meta.promptVersion === \"string\" ? { promptVersion: meta.promptVersion } : {}),\n };\n}\n\n/**\n * Collect all exportable wiki pages from `wiki/concepts/` and `wiki/queries/`.\n * Drops untitled records, frontmatter-orphaned records, and computed-orphaned\n * records (every owning source deleted) — those are diagnosed by lint/the\n * viewer, not exported. The freshness snapshot is built once and shared across\n * all pages. Returns the surviving pages sorted by title.\n */\nexport async function collectExportPages(root: string): Promise<ExportPage[]> {\n const raw = await collectRawWikiPages(root);\n const snapshot = await buildFreshnessSnapshot(root);\n const sourceHashes = sourceHashLookupFromSnapshot(snapshot);\n const kept = raw.filter((page) => page.parseStatus.hasTitle && !page.parseStatus.orphaned);\n const pages = kept\n .map((page) => toExportPage(page, snapshot, sourceHashes))\n .filter((page) => page.freshnessStatus !== \"orphaned\");\n pages.sort((a, b) => a.title.localeCompare(b.title));\n return pages;\n}\n","/**\n * Export provenance helpers (export provenance).\n *\n * Surfaces the auditable lineage a downstream consumer (e.g. a downstream rule importer)\n * needs to answer \"this page came from these sources, via this model and\n * prompt version\":\n *\n * - {@link hashPageBody} derives a deterministic SHA-256 over a page body so\n * a consumer can detect content drift without re-reading the markdown.\n * - {@link sourceHashLookupFromSnapshot} reuses a freshness snapshot's\n * recorded per-source SHA-256 hashes as a filename → hash map.\n * - {@link resolveSourceHashes} maps a page's `sources` frontmatter list to\n * those committed hashes, preserving order and de-duplicating.\n *\n * The hashes here are the SAME digests `hasher.ts` writes to state.json — we\n * surface them rather than recompute, so the export stays consistent with the\n * compiler's own change-detection view and stays deterministic (no filesystem\n * re-reads, no wall-clock, no map-iteration order dependence).\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { FreshnessSnapshot } from \"../freshness/types.js\";\n\n/** Map of source filename → committed SHA-256 hash from state.json. */\nexport type SourceHashLookup = Record<string, string>;\n\n/**\n * Deterministic SHA-256 (hex) of a page body. Stable for identical input:\n * the same body string always yields the same digest regardless of when or\n * where the export runs.\n * @param body - Full markdown page body (without frontmatter).\n * @returns Hex-encoded SHA-256 digest.\n */\nexport function hashPageBody(body: string): string {\n return createHash(\"sha256\").update(body, \"utf-8\").digest(\"hex\");\n}\n\n/**\n * Build a filename -> source-hash lookup from an existing freshness snapshot.\n * The snapshot already contains state.json's recorded hashes, so export callers\n * that compute freshness can reuse the same read-only state pass.\n */\nexport function sourceHashLookupFromSnapshot(snapshot: FreshnessSnapshot): SourceHashLookup {\n const lookup: SourceHashLookup = {};\n for (const [file, source] of Object.entries(snapshot.sources)) {\n lookup[file] = source.recordedHash;\n }\n return lookup;\n}\n\n/**\n * Resolve the source-file SHA-256 hashes a page derived from.\n *\n * Maps each entry in the page's `sources` frontmatter list to its committed\n * hash. Sources without a recorded hash (e.g. seed pages with an empty\n * source list, or a source removed from state) contribute nothing. Order\n * follows the `sources` list and duplicates collapse, so the output is\n * deterministic for a given (sources, state) pair.\n *\n * @param sources - Source filenames cited by the page (frontmatter `sources`).\n * @param lookup - Filename → hash map from {@link sourceHashLookupFromSnapshot}.\n * @returns Ordered, de-duplicated list of source hashes.\n */\nexport function resolveSourceHashes(\n sources: string[],\n lookup: SourceHashLookup,\n): string[] {\n const hashes: string[] = [];\n const seen = new Set<string>();\n for (const file of sources) {\n const hash = lookup[file];\n if (hash === undefined || seen.has(hash)) continue;\n hashes.push(hash);\n seen.add(hash);\n }\n return hashes;\n}\n","/**\n * Project ID validation for the JSON export contract.\n *\n * The regex `PROJECT_ID_PATTERN` is mirrored byte-for-byte in\n * `@atomicmemory/llmwiki`'s importer-side `project-id.ts`. When you\n * change either, change both.\n *\n * `projectId` participates in the deterministic external IDs\n * (`llmwiki/<projectId>/<pageDirectory>/<slug>`) used by downstream\n * importers and the Atomic Memory namespace scope. It is therefore a\n * **security boundary**, not just a dedup aid: two projects supplying\n * the same `projectId` collide and silently overwrite each other\n * through any upsert primitive.\n *\n * Three guards apply:\n *\n * 1. Strict regex at the boundary (export side here; import side\n * mirrors).\n * 2. The Atomic Memory write scope must authorize the namespace\n * (enforced server-side by the adapter; out of scope for the\n * exporter).\n * 3. Collision detection on import when an existing record's\n * `source` field differs (also import side).\n *\n * The regex below pins the on-wire format: lowercase letters, digits,\n * and hyphens; must begin with letter or digit; 1..63 characters. The\n * length cap matches DNS-label conventions and keeps external IDs\n * fitting comfortably inside Atomic Memory's metadata indexing limits.\n */\n\n/** Regex pinning the on-wire `projectId` format. Mirrored on the importer side. */\nexport const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;\n\n/**\n * Validate a candidate `projectId`. Returns the input string when the\n * input is valid; throws `Error` with a clear, actionable message\n * otherwise. The thrown message names the rejected value verbatim so\n * shell users see what they typed.\n */\nexport function validateProjectId(candidate: unknown): string {\n if (typeof candidate !== \"string\") {\n throw new Error(`projectId must be a string; received ${typeof candidate}`);\n }\n if (candidate.length === 0) {\n throw new Error(\"projectId must not be empty\");\n }\n if (!PROJECT_ID_PATTERN.test(candidate)) {\n throw new Error(\n `Invalid projectId \"${candidate}\". ` +\n `Must match /^[a-z0-9][a-z0-9-]{0,62}$/ (lowercase letters, digits, hyphens; ` +\n `starts with letter or digit; max 63 characters).`,\n );\n }\n return candidate;\n}\n","/**\n * JSON export format writer.\n *\n * Produces a structured JSON document containing all wiki pages and their\n * metadata. The schema is intentionally simple and human-readable so it can\n * be consumed directly by scripts, agents, or downstream pipelines without\n * additional transformation.\n *\n * Schema:\n * { schemaVersion, exportedAt, pageCount, projectId?, pages: ExportPage[] }\n *\n * W4 provenance lives PER PAGE (`ExportPage.modelId` / `promptVersion` plus\n * `contentHash` / `sourceHashes`), stamped into each page at compile time.\n * It is deliberately not summarized at the envelope level: a single\n * export-time model id would misattribute pages compiled under a different\n * model, which is exactly the lineage bug this avoids.\n *\n * `schemaVersion` lets downstream consumers (e.g. the rule importer) pin to a known\n * contract. Increment when a breaking field change lands; additive fields\n * do not require a bump.\n *\n * `projectId` is the optional bridge identifier. When present it pins the\n * on-disk export to a stable identity that downstream consumers (the\n * Atomic Memory adapter especially) use to derive deterministic external\n * IDs. Validation happens at the CLI/programmatic boundary, not here —\n * by the time we serialize, the value has been checked.\n */\n\nimport { validateProjectId } from \"./project-id.js\";\nimport type { ExportPage } from \"./types.js\";\n\n/**\n * Monotonically-incremented envelope version.\n * Bump when a breaking field change lands; additive additions do not require a bump.\n */\nexport const EXPORT_SCHEMA_VERSION = 1;\n\n/** Top-level shape of the JSON export file. */\nexport interface JsonExportDocument {\n /**\n * Contract version for downstream consumers. Start at 1; increment only on\n * breaking envelope changes so consumers can pin a supported range.\n */\n schemaVersion: number;\n exportedAt: string;\n pageCount: number;\n /** Optional bridge identifier. See `src/export/project-id.ts` for the validation rule. */\n projectId?: string;\n pages: ExportPage[];\n}\n\n/** Options accepted by {@link buildJsonExportDocument}. */\nexport interface BuildJsonExportOptions {\n /**\n * Optional project identifier. Validated against the bridge contract\n * regex; throws if invalid so a malformed value never reaches disk.\n */\n projectId?: string;\n}\n\n/**\n * Build the JSON export document object from a list of export pages.\n * @param pages - Sorted array of export pages.\n * @param options - Optional bridge envelope fields (e.g. `projectId`).\n * @returns The structured JsonExportDocument object (not serialized).\n */\nexport function buildJsonExportDocument(\n pages: ExportPage[],\n options: BuildJsonExportOptions = {},\n): JsonExportDocument {\n const doc: JsonExportDocument = {\n schemaVersion: EXPORT_SCHEMA_VERSION,\n exportedAt: new Date().toISOString(),\n pageCount: pages.length,\n pages,\n };\n if (options.projectId !== undefined) {\n doc.projectId = validateProjectId(options.projectId);\n }\n return doc;\n}\n\n/**\n * Build the JSON export document from a list of export pages.\n * @param pages - Sorted array of export pages.\n * @param options - Optional bridge envelope fields (e.g. `projectId`).\n * @returns Pretty-printed JSON string.\n */\nexport function buildJsonExport(\n pages: ExportPage[],\n options: BuildJsonExportOptions = {},\n): string {\n return JSON.stringify(buildJsonExportDocument(pages, options), null, 2);\n}\n","/**\n * GraphML export format writer.\n *\n * Produces a GraphML XML document representing the wiki link graph.\n * Each page becomes a node; each [[wikilink]] between pages becomes a\n * directed edge. Node attributes carry page metadata (title, summary, tags).\n *\n * GraphML is the standard XML format for graph exchange and is supported by\n * Gephi, yEd, NetworkX, and many other graph tools.\n */\n\nimport type { ExportPage } from \"./types.js\";\n\n/** XML special characters that must be escaped in attribute values and text. */\nconst XML_ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&apos;\",\n};\n\n/** Escape a string for safe inclusion in XML. */\nfunction escapeXml(value: string): string {\n return value.replace(/[&<>\"']/g, (ch) => XML_ESCAPES[ch] ?? ch);\n}\n\n/** GraphML attribute key definitions. */\nconst KEY_DEFS = [\n '<key id=\"title\" for=\"node\" attr.name=\"title\" attr.type=\"string\"/>',\n '<key id=\"summary\" for=\"node\" attr.name=\"summary\" attr.type=\"string\"/>',\n '<key id=\"tags\" for=\"node\" attr.name=\"tags\" attr.type=\"string\"/>',\n '<key id=\"sources\" for=\"node\" attr.name=\"sources\" attr.type=\"string\"/>',\n '<key id=\"createdAt\" for=\"node\" attr.name=\"createdAt\" attr.type=\"string\"/>',\n '<key id=\"updatedAt\" for=\"node\" attr.name=\"updatedAt\" attr.type=\"string\"/>',\n].join(\"\\n \");\n\n/** Serialise one ExportPage as a GraphML <node> element. */\nfunction pageToNode(page: ExportPage): string {\n const tags = page.tags.join(\", \");\n const sources = page.sources.join(\", \");\n return [\n ` <node id=\"${escapeXml(page.slug)}\">`,\n ` <data key=\"title\">${escapeXml(page.title)}</data>`,\n ` <data key=\"summary\">${escapeXml(page.summary)}</data>`,\n ` <data key=\"tags\">${escapeXml(tags)}</data>`,\n ` <data key=\"sources\">${escapeXml(sources)}</data>`,\n ` <data key=\"createdAt\">${escapeXml(page.createdAt)}</data>`,\n ` <data key=\"updatedAt\">${escapeXml(page.updatedAt)}</data>`,\n ` </node>`,\n ].join(\"\\n\");\n}\n\n/** Build all <edge> elements for a single source page's outgoing links. */\nfunction pageToEdges(page: ExportPage, knownSlugs: Set<string>): string[] {\n return page.links\n .filter((slug) => knownSlugs.has(slug))\n .map(\n (slug) =>\n ` <edge source=\"${escapeXml(page.slug)}\" target=\"${escapeXml(slug)}\"/>`,\n );\n}\n\n/**\n * Build the GraphML document from a list of export pages.\n * Only edges whose target slug exists in the page set are included so the\n * graph contains no dangling references.\n * @param pages - Sorted array of export pages.\n * @returns GraphML XML string.\n */\nexport function buildGraphml(pages: ExportPage[]): string {\n const knownSlugs = new Set(pages.map((p) => p.slug));\n const nodes = pages.map(pageToNode).join(\"\\n\");\n const edges = pages.flatMap((p) => pageToEdges(p, knownSlugs)).join(\"\\n\");\n\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<graphml xmlns=\"http://graphml.graphdrawing.org/graphml\"',\n ' xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"',\n ' xsi:schemaLocation=\"http://graphml.graphdrawing.org/graphml',\n ' http://graphml.graphdrawing.org/graphml/1.0/graphml.xsd\">',\n ` ${KEY_DEFS}`,\n ' <graph id=\"wiki\" edgedefault=\"directed\">',\n nodes,\n edges,\n \" </graph>\",\n \"</graphml>\",\n \"\",\n ].join(\"\\n\");\n}\n","/**\n * Health score evaluator for the llmwiki eval harness.\n *\n * Aggregates the output of all 10+ lint rules into a single 0–100 score.\n * Errors on critical rules (broken links/citations, duplicates) cost 4 pts\n * each; contradicted pages cost 2 pts; all other warnings/info cost 1 pt each.\n * The final score is clamped to [0, 100].\n */\n\nimport {\n checkBrokenWikilinks,\n checkBrokenCitations,\n checkMalformedClaimCitations,\n checkOrphanedPages,\n checkMissingSummaries,\n checkDuplicateConcepts,\n checkEmptyPages,\n checkLowConfidencePages,\n checkContradictedPages,\n checkInferredWithoutCitations,\n checkSchemaCrossLinks,\n} from \"../linter/rules.js\";\nimport { loadSchema } from \"../schema/loader.js\";\nimport type { LintResult } from \"../linter/types.js\";\nimport type { HealthResult, HealthRuleResult } from \"./types.js\";\n\nconst MAX_SCORE = 100;\nconst ERROR_DEDUCTION = 4;\nconst CONTRADICTED_DEDUCTION = 2;\nconst DEFAULT_DEDUCTION = 1;\n\n/** Rules treated as high-severity errors (−4 pts per violation). */\nconst ERROR_RULES = new Set([\n \"broken-wikilink\",\n \"broken-citation\",\n \"duplicate-concept\",\n]);\n\n/** Compute the point deduction for a single lint result. */\nfunction deductionFor(result: LintResult): number {\n if (ERROR_RULES.has(result.rule)) return ERROR_DEDUCTION;\n if (result.rule === \"contradicted-page\") return CONTRADICTED_DEDUCTION;\n return DEFAULT_DEDUCTION;\n}\n\n/** Aggregate lint results into a per-rule summary with deduction totals. */\nfunction aggregateRules(results: LintResult[]): HealthRuleResult[] {\n const map = new Map<string, HealthRuleResult>();\n for (const result of results) {\n const existing = map.get(result.rule);\n const deduction = deductionFor(result);\n if (existing) {\n existing.count++;\n existing.deduction += deduction;\n } else {\n map.set(result.rule, {\n rule: result.rule,\n count: 1,\n severity: result.severity,\n deduction,\n });\n }\n }\n return Array.from(map.values());\n}\n\n/**\n * Run all lint rules against the project root and return an aggregated\n * health score plus per-rule breakdown.\n * @param root - Absolute path to the project root.\n */\nexport async function evaluateHealth(root: string): Promise<HealthResult> {\n const schema = await loadSchema(root);\n\n const allResults = (\n await Promise.all([\n checkBrokenWikilinks(root),\n checkBrokenCitations(root),\n checkMalformedClaimCitations(root),\n checkOrphanedPages(root),\n checkMissingSummaries(root),\n checkDuplicateConcepts(root),\n checkEmptyPages(root),\n checkLowConfidencePages(root),\n checkContradictedPages(root),\n checkInferredWithoutCitations(root),\n checkSchemaCrossLinks(root, schema),\n ])\n ).flat();\n\n const rules = aggregateRules(allResults);\n const totalDeduction = rules.reduce((sum, r) => sum + r.deduction, 0);\n const score = Math.max(0, MAX_SCORE - totalDeduction);\n\n return { score, maxScore: MAX_SCORE, rules };\n}\n","/**\n * Citation coverage evaluator for the llmwiki eval harness.\n *\n * Measures two things:\n * - Coverage: what fraction of prose paragraphs contain at least one citation.\n * - Precision: what fraction of citations point to a source file that exists.\n *\n * Non-prose paragraphs (headings, code blocks, lists, blank lines) are excluded\n * so that only human-readable claim text is counted.\n */\n\nimport path from \"path\";\nimport { collectAllPages } from \"../linter/rules.js\";\nimport { parseFrontmatter, extractClaimCitations, splitProseParagraphs } from \"../utils/markdown.js\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\nimport { resolveSourceFile } from \"./source-path.js\";\nimport type { CitationCoverageResult, CitationPageResult } from \"./types.js\";\n\n/** Prose paragraphs start with a Unicode letter. */\ninterface PageStats {\n pageResult: CitationPageResult;\n proseParagraphs: number;\n citedParagraphs: number;\n totalCitations: number;\n validCitations: number;\n}\n\n/** Evaluate citation coverage and precision for a single page body. */\nasync function evaluatePage(slug: string, body: string, sourcesDir: string): Promise<PageStats> {\n const paragraphs = splitProseParagraphs(body);\n let citedParagraphs = 0;\n let totalCitations = 0;\n let validCitations = 0;\n\n for (const para of paragraphs) {\n const citations = extractClaimCitations(para);\n if (citations.length === 0) continue;\n citedParagraphs++;\n for (const { spans } of citations) {\n for (const span of spans) {\n totalCitations++;\n if ((await resolveSourceFile(sourcesDir, span.file)) !== null) validCitations++;\n }\n }\n }\n\n return {\n pageResult: { slug, proseParagraphs: paragraphs.length, citedParagraphs },\n proseParagraphs: paragraphs.length,\n citedParagraphs,\n totalCitations,\n validCitations,\n };\n}\n\n/**\n * Measure citation coverage and precision across all wiki pages.\n * @param root - Absolute path to the project root.\n */\nexport async function evaluateCitationCoverage(\n root: string,\n): Promise<CitationCoverageResult> {\n const pages = await collectAllPages(root);\n const sourcesDir = path.join(root, SOURCES_DIR);\n\n let totalProse = 0;\n let totalCited = 0;\n let totalCitations = 0;\n let totalValid = 0;\n const perPage: CitationPageResult[] = [];\n\n for (const { filePath, content } of pages) {\n const { body } = parseFrontmatter(content);\n const slug = path.basename(filePath, \".md\");\n const stats = await evaluatePage(slug, body, sourcesDir);\n totalProse += stats.proseParagraphs;\n totalCited += stats.citedParagraphs;\n totalCitations += stats.totalCitations;\n totalValid += stats.validCitations;\n perPage.push(stats.pageResult);\n }\n\n const coveragePercent = totalProse === 0 ? 0 : (totalCited / totalProse) * 100;\n const precisionPercent = totalCitations === 0 ? 0 : (totalValid / totalCitations) * 100;\n\n return {\n totalProseParagraphs: totalProse,\n citedParagraphs: totalCited,\n coveragePercent,\n totalCitations,\n validCitations: totalValid,\n precisionPercent,\n perPage,\n };\n}\n","/**\n * Safe source-file resolver for the eval harness.\n *\n * Citation markers in wiki pages contain free-form file paths. Before the\n * eval harness reads any file or checks its existence, the path must be\n * confined to the project's `sources/` directory. Three independent layers\n * of defense are applied in order:\n *\n * 1. Structural: reject empty strings, absolute paths, and any `..` segment.\n * 2. Pre-realpath: resolve the joined path and verify it stays under sourcesDir.\n * 3. Symlink: call `fs.realpath` on both sides and verify again, catching\n * symlinks inside sources/ that point outside the tree.\n *\n * The confinement pattern mirrors `resolveSafePath` in src/context/provenance.ts.\n */\n\nimport { realpath } from \"fs/promises\";\nimport path from \"path\";\n\n/**\n * True when any `/`- or `\\`-separated segment of `file` is literally `..`.\n * Splitting on both separators ensures a `nested\\..\\escape.md` path written\n * for a Windows consumer is also caught on Linux where `\\` is a valid filename\n * character.\n */\nfunction containsParentSegment(file: string): boolean {\n return file.split(/[/\\\\]/).some((seg) => seg === \"..\");\n}\n\n/** True when `candidate` equals `parent` or sits beneath it. */\nfunction isInside(parent: string, candidate: string): boolean {\n if (candidate === parent) return true;\n const parentWithSep = parent.endsWith(path.sep) ? parent : parent + path.sep;\n return candidate.startsWith(parentWithSep);\n}\n\n/**\n * Resolve `file` under `sourcesDir`, rejecting:\n * - empty strings and absolute paths (`/etc/passwd`)\n * - any path segment that is literally `..` (traversal, even normalizing back\n * inside sources/ is rejected — the raw marker must be unambiguous)\n * - paths that resolve outside `sourcesDir` after normalization\n * - paths whose `realpath` escapes `sourcesDir` (symlink escapes)\n *\n * Returns the canonical absolute path on success, or `null` if the path is\n * rejected, the sources directory does not exist, or the file is not found.\n *\n * @param sourcesDir - Absolute path to the project's sources/ directory.\n * @param file - Relative file path from a citation marker (untrusted input).\n */\nexport async function resolveSourceFile(\n sourcesDir: string,\n file: string,\n): Promise<string | null> {\n if (file.length === 0 || path.isAbsolute(file)) return null;\n if (containsParentSegment(file)) return null;\n const joined = path.join(sourcesDir, file);\n if (!isInside(sourcesDir, path.resolve(joined))) return null;\n try {\n const realDir = await realpath(sourcesDir);\n const realFile = await realpath(joined);\n if (!isInside(realDir, realFile)) return null;\n return realFile;\n } catch {\n return null;\n }\n}\n","/**\n * Source utilization evaluator for the llmwiki eval harness.\n *\n * Measures whether every ingested source has been compiled into the wiki.\n * An uncited source means its concepts were either not extracted or not\n * linked to any generated page — a silent failure mode that no existing\n * lint rule catches.\n *\n * Algorithm: enumerate on-disk source files, resolve each through\n * resolveSourceFile to a canonical realpath, then cross-reference with\n * citation targets resolved the same way. Both sides use the same confined\n * resolver so symlinks and path differences are handled consistently.\n * When no valid sources remain after filtering, utilizationRate is null\n * (\"not measured\").\n */\n\nimport { readdir, lstat } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { collectAllPages } from \"../linter/rules.js\";\nimport { parseFrontmatter, extractClaimCitations, splitProseParagraphs } from \"../utils/markdown.js\";\nimport { resolveSourceFile } from \"./source-path.js\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\nimport type { SourceUtilizationResult } from \"./types.js\";\n\nfunction collectRawCitedFiles(body: string): Set<string> {\n const files = new Set<string>();\n for (const para of splitProseParagraphs(body)) {\n const citations = extractClaimCitations(para);\n for (const { spans } of citations) {\n for (const span of spans) files.add(span.file);\n }\n }\n return files;\n}\n\nasync function listSourceFiles(dir: string): Promise<string[]> {\n if (!existsSync(dir)) return [];\n const entries = await readdir(dir);\n return entries.filter((e) => e.endsWith(\".md\"));\n}\n\nfunction pageSlug(filePath: string): string {\n const dir = path.basename(path.dirname(filePath));\n return dir + \"/\" + path.basename(filePath, \".md\");\n}\n\ninterface InventoryResult {\n /** Names of source files that passed confinement (the valid inventory). */\n validFiles: string[];\n /** Map from valid source filename to its canonical realpath. */\n fileToReal: Map<string, string>;\n /** Non-fatal issues (e.g. out-of-tree symlinks excluded). */\n warnings: string[];\n}\n\n/**\n * Build the filtered source inventory by resolving every on-disk source\n * through resolveSourceFile — the same confined resolver used for citation\n * targets. Files that fail resolution are excluded from the inventory and\n * reported as warnings. The caller MUST derive totalSources, perSource,\n * and cited/uncited counts from the returned validFiles, never from the\n * raw readdir() result.\n */\n/**\n * Explain why a source file was excluded from the inventory. The common case is\n * a symlink whose target is missing or resolves outside the sources tree;\n * everything else (bad path, unreadable) collapses to a generic reason.\n */\nasync function exclusionReason(sourcesDir: string, file: string): Promise<string> {\n const stat = await lstat(path.join(sourcesDir, file)).catch(() => null);\n return stat?.isSymbolicLink()\n ? \"symlink target missing or outside sources/ (excluded)\"\n : \"could not be resolved (excluded)\";\n}\n\nasync function resolveSourceInventory(\n sourcesDir: string,\n sourceFiles: string[],\n): Promise<InventoryResult> {\n const fileToReal = new Map<string, string>();\n const validFiles: string[] = [];\n const warnings: string[] = [];\n for (const f of sourceFiles) {\n const resolved = await resolveSourceFile(sourcesDir, f);\n if (resolved === null) {\n // Filename first so the actionable detail survives any display\n // truncation; distinguish the common symlink case from other failures.\n warnings.push(`${f}: ${await exclusionReason(sourcesDir, f)}`);\n } else {\n fileToReal.set(f, resolved);\n validFiles.push(f);\n }\n }\n return { validFiles, fileToReal, warnings };\n}\n\nasync function collectCitedRealpaths(\n sourcesDir: string,\n pages: Array<{ filePath: string; content: string }>,\n): Promise<Map<string, Set<string>>> {\n const citedRealToPages = new Map<string, Set<string>>();\n for (const { filePath, content } of pages) {\n const { body } = parseFrontmatter(content);\n const slug = pageSlug(filePath);\n for (const rawFile of collectRawCitedFiles(body)) {\n const resolved = await resolveSourceFile(sourcesDir, rawFile);\n if (resolved === null) continue;\n const entry = citedRealToPages.get(resolved);\n if (entry) entry.add(slug);\n else citedRealToPages.set(resolved, new Set([slug]));\n }\n }\n return citedRealToPages;\n}\n\nfunction buildPerSource(\n validFiles: string[],\n fileToReal: Map<string, string>,\n citedRealToPages: Map<string, Set<string>>,\n): SourceUtilizationResult[\"perSource\"] {\n const records = validFiles.map((sourceFile) => {\n const real = fileToReal.get(sourceFile);\n const pageSlugs = real ? citedRealToPages.get(real) : undefined;\n return {\n sourceFile,\n citingPageCount: pageSlugs ? pageSlugs.size : 0,\n citingPages: pageSlugs ? [...pageSlugs].sort() : ([] as string[]),\n };\n });\n records.sort((a, b) => {\n if (a.citingPageCount !== b.citingPageCount) return b.citingPageCount - a.citingPageCount;\n return a.sourceFile.localeCompare(b.sourceFile);\n });\n return records;\n}\n\nexport async function evaluateSourceUtilization(\n root: string,\n): Promise<SourceUtilizationResult> {\n const sourcesDir = path.join(root, SOURCES_DIR);\n const rawFiles = await listSourceFiles(sourcesDir);\n\n // Build the filtered inventory — all downstream counts must use this,\n // never rawFiles.length.\n const { validFiles, fileToReal, warnings } = await resolveSourceInventory(sourcesDir, rawFiles);\n const totalSources = validFiles.length;\n\n if (totalSources === 0) {\n // When raw readdir found files but the confined resolver filtered\n // them all out, carry those warnings forward so the caller can see\n // *why* the inventory is empty.\n const rawWarnings = rawFiles.length > 0 && warnings.length > 0\n ? warnings\n : [];\n return {\n totalSources: 0, citedSources: 0, uncitedSources: 0,\n utilizationRate: null, perSource: [], warnings: rawWarnings,\n };\n }\n\n const pages = await collectAllPages(root);\n const citedRealToPages = await collectCitedRealpaths(sourcesDir, pages);\n const perSource = buildPerSource(validFiles, fileToReal, citedRealToPages);\n\n const citedCount = perSource.filter((e) => e.citingPageCount > 0).length;\n return {\n totalSources,\n citedSources: citedCount,\n uncitedSources: totalSources - citedCount,\n utilizationRate: Math.round((citedCount / totalSources) * 1000) / 1000,\n perSource,\n warnings,\n };\n}\n","/**\n * Citation depth evaluator for the llmwiki eval harness.\n *\n * Measures how precise citations are. A bare ^[file.md] says \"this paragraph\n * came from somewhere in that source\" — useful but vague. A claim-level\n * citation ^[file.md:42-58] pins the exact lines, which is auditable.\n *\n * Metrics:\n * - claimLevelRate: fraction of citations that include a line range.\n * - avgCitationsPerParagraph: citation density across prose paragraphs.\n */\n\nimport { collectAllPages } from \"../linter/rules.js\";\nimport { parseFrontmatter, extractClaimCitations, splitProseParagraphs } from \"../utils/markdown.js\";\nimport type { CitationDepthResult } from \"./types.js\";\n\ninterface PageCounts { total: number; precise: number; proseParagraphs: number; }\n\nfunction countPageCitations(body: string): PageCounts {\n const paragraphs = splitProseParagraphs(body);\n let total = 0;\n let precise = 0;\n for (const para of paragraphs) {\n const citations = extractClaimCitations(para);\n for (const { spans } of citations) {\n for (const span of spans) {\n total++;\n if (span.lines) precise++;\n }\n }\n }\n return { total, precise, proseParagraphs: paragraphs.length };\n}\n\nexport async function evaluateCitationDepth(\n root: string,\n): Promise<CitationDepthResult> {\n const pages = await collectAllPages(root);\n let totalCitations = 0;\n let citationsWithLineRange = 0;\n let totalProseParagraphs = 0;\n for (const { content } of pages) {\n const { body } = parseFrontmatter(content);\n const counts = countPageCitations(body);\n totalCitations += counts.total;\n citationsWithLineRange += counts.precise;\n totalProseParagraphs += counts.proseParagraphs;\n }\n const claimLevelRate = totalCitations === 0 ? 0 : citationsWithLineRange / totalCitations;\n const avgCitationsPerParagraph = totalProseParagraphs === 0 ? 0\n : totalCitations / totalProseParagraphs;\n return {\n totalCitations,\n preciseCitations: citationsWithLineRange,\n vagueCitations: totalCitations - citationsWithLineRange,\n claimLevelRate: Math.round(claimLevelRate * 1000) / 1000,\n avgCitationsPerParagraph,\n };\n}\n","/**\n * LLM citation judge for the llmwiki eval harness.\n *\n * For each cited prose paragraph in the wiki, extracts the (claim, source span)\n * pair and asks an LLM judge whether the source actually supports the claim.\n * Scoring: 0 = unsupported, 1 = partial, 2 = fully supported.\n *\n * Uses deterministic hash-based sampling so the same N citations are evaluated\n * on every run (reproducible). Results are cached in .llmwiki/eval/citation-cache.jsonl\n * so subsequent runs skip already-judged pairs.\n */\n\nimport { createHash } from \"crypto\";\nimport { readFile, appendFile, mkdir } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { collectAllPages } from \"../linter/rules.js\";\nimport { parseFrontmatter, extractClaimCitations, splitProseParagraphs } from \"../utils/markdown.js\";\nimport { callClaude } from \"../utils/llm.js\";\nimport { SOURCES_DIR, DEFAULT_PROVIDER, PROVIDER_MODELS } from \"../utils/constants.js\";\nimport { resolveSourceFile } from \"./source-path.js\";\nimport type { LLMTool } from \"../utils/provider.js\";\nimport type { CitationJudgement, CitationSupportResult } from \"./types.js\";\nimport type { SourceSpan } from \"../utils/types.js\";\n\nconst CACHE_DIR = path.join(\".llmwiki\", \"eval\");\nconst CACHE_FILE = path.join(CACHE_DIR, \"citation-cache.jsonl\");\n/** Internal pair combining a claim paragraph with its cited source span. */\ninterface CitationPair {\n claimHash: string;\n pageSlug: string;\n claimText: string;\n citedFile: string;\n spanText: string;\n lineStart: number;\n lineEnd: number;\n}\n\nconst JUDGE_TOOL: LLMTool = {\n name: \"judge_citation\",\n description: \"Rate how well the source excerpt supports the claim.\",\n input_schema: {\n type: \"object\",\n properties: {\n score: {\n type: \"integer\",\n enum: [0, 1, 2],\n description:\n \"0=not supported or contradicted, 1=partially supported, 2=fully supported\",\n },\n reason: { type: \"string\", description: \"One sentence explaining the rating.\" },\n },\n required: [\"score\", \"reason\"],\n },\n};\n\nconst JUDGE_SYSTEM =\n \"You are an expert fact-checker. Given a claim from a wiki article and a source excerpt, rate whether the source supports the claim. Be strict: partial credit only if the source addresses the claim but is incomplete. Important: if the source excerpt consists entirely of YAML frontmatter (metadata fields such as title, date, author, or tags between --- delimiters), treat it as non-evidence for any substantive claim and score it 0, unless the claim is explicitly about that metadata field.\";\n\n// Content-addressable fingerprint of the judge prompt + tool schema.\n// Any edit to JUDGE_SYSTEM or JUDGE_TOOL produces a new hash, automatically\n// invalidating cached judgements without requiring a manual version bump.\nconst JUDGE_CONFIG_HASH = createHash(\"sha256\")\n .update(JUDGE_SYSTEM + JSON.stringify(JUDGE_TOOL))\n .digest(\"hex\")\n .slice(0, 8);\n\n/** Compute a stable 16-char hex hash for a (claim, span) pair. */\nfunction hashPair(claimText: string, spanText: string): string {\n return createHash(\"sha256\").update(claimText + spanText).digest(\"hex\").slice(0, 16);\n}\n\n/** Derive a full cache key from content hash + judge config fingerprint + model. */\nfunction makeCacheKey(contentHash: string, model: string): string {\n return createHash(\"sha256\")\n .update(contentHash + JUDGE_CONFIG_HASH + model)\n .digest(\"hex\")\n .slice(0, 16);\n}\n\n/** Extract the text of specific lines from a file (1-indexed, inclusive). */\nasync function readSourceLines(filePath: string, start: number, end: number): Promise<string> {\n const content = await readFile(filePath, \"utf-8\");\n return content\n .split(\"\\n\")\n .slice(start - 1, end)\n .join(\"\\n\");\n}\n\n/** Strip `^[...]` markers from a paragraph to get the bare claim text. */\nfunction stripCitationMarkers(paragraph: string): string {\n return paragraph.replace(/\\^\\[[^\\]]+\\]/g, \"\").trim();\n}\n\n/** Build a CitationPair from one span, or null if the source file is missing or has no line range. */\nasync function buildSpanPair(\n slug: string,\n claimText: string,\n span: SourceSpan,\n sourcesDir: string,\n): Promise<CitationPair | null> {\n if (!span.lines) return null;\n const sourceFile = await resolveSourceFile(sourcesDir, span.file);\n if (sourceFile === null) return null;\n const spanText = await readSourceLines(sourceFile, span.lines.start, span.lines.end);\n return {\n claimHash: hashPair(claimText, spanText),\n pageSlug: slug,\n claimText,\n citedFile: span.file,\n spanText,\n lineStart: span.lines.start,\n lineEnd: span.lines.end,\n };\n}\n\n/** Extract citation pairs from a single cited paragraph. */\nasync function extractParagraphPairs(\n slug: string,\n para: string,\n sourcesDir: string,\n): Promise<CitationPair[]> {\n const citations = extractClaimCitations(para);\n if (citations.length === 0) return [];\n const claimText = stripCitationMarkers(para);\n const spans = citations.flatMap((c) => c.spans);\n const pairs = await Promise.all(spans.map((s) => buildSpanPair(slug, claimText, s, sourcesDir)));\n return pairs.filter((p): p is CitationPair => p !== null);\n}\n\n/** Extract citation pairs from a single page body. */\nasync function extractPagePairs(\n slug: string,\n body: string,\n sourcesDir: string,\n): Promise<CitationPair[]> {\n const paragraphs = splitProseParagraphs(body);\n const results = await Promise.all(paragraphs.map((p) => extractParagraphPairs(slug, p, sourcesDir)));\n return results.flat();\n}\n\n/**\n * Extract all (claim, source span) pairs from the wiki.\n * Only pairs with valid line ranges pointing to existing source files are included.\n * @param root - Absolute path to the project root.\n */\nexport async function extractCitationPairs(root: string): Promise<CitationPair[]> {\n const pages = await collectAllPages(root);\n const sourcesDir = path.join(root, SOURCES_DIR);\n const all: CitationPair[] = [];\n for (const { filePath, content } of pages) {\n const { body } = parseFrontmatter(content);\n const slug = path.basename(filePath, \".md\");\n const pairs = await extractPagePairs(slug, body, sourcesDir);\n all.push(...pairs);\n }\n return all;\n}\n\n/**\n * Select a stable sample of N pairs that resists churn as the corpus grows.\n *\n * Previously-sampled hashes are retained first; only empty slots are filled\n * from new pairs (sorted by claimHash for determinism). This means adding new\n * citations to the corpus never displaces an already-evaluated pair, so score\n * movement in reports reflects quality change rather than sample turnover.\n *\n * @param pairs - All extracted citation pairs.\n * @param sampleSize - Maximum number of pairs to return.\n * @param previousHashes - Hashes selected in the prior run (from the saved report).\n */\nexport function selectDeterministicSample(\n pairs: CitationPair[],\n sampleSize: number,\n previousHashes: string[] = [],\n): CitationPair[] {\n const pairByHash = new Map(pairs.map((p) => [p.claimHash, p]));\n const retained = previousHashes.flatMap((h) => {\n const p = pairByHash.get(h);\n return p ? [p] : [];\n });\n if (retained.length >= sampleSize) return retained.slice(0, sampleSize);\n const retainedSet = new Set(previousHashes);\n const newPairs = pairs\n .filter((p) => !retainedSet.has(p.claimHash))\n .sort((a, b) => a.claimHash.localeCompare(b.claimHash));\n return [...retained, ...newPairs].slice(0, sampleSize);\n}\n\n/** Load previously cached judgements keyed by claimHash. */\nasync function loadCachedJudgements(root: string): Promise<Map<string, CitationJudgement>> {\n const cachePath = path.join(root, CACHE_FILE);\n if (!existsSync(cachePath)) return new Map();\n const content = await readFile(cachePath, \"utf-8\");\n const map = new Map<string, CitationJudgement>();\n for (const line of content.trim().split(\"\\n\").filter(Boolean)) {\n try {\n const entry = JSON.parse(line) as CitationJudgement;\n map.set(entry.claimHash, entry);\n } catch {\n // Skip malformed cache lines\n }\n }\n return map;\n}\n\n/** Append a single judgement to the cache file. */\nasync function appendCachedJudgement(root: string, judgement: CitationJudgement): Promise<void> {\n await mkdir(path.join(root, CACHE_DIR), { recursive: true });\n await appendFile(path.join(root, CACHE_FILE), JSON.stringify(judgement) + \"\\n\");\n}\n\n/** Resolve the current model identifier for recording in judgements. */\nfunction resolveModel(): string {\n const provider = process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER;\n return process.env.LLMWIKI_MODEL ?? PROVIDER_MODELS[provider] ?? provider;\n}\n\n/** Call the LLM judge for a single (claim, span) pair. */\nasync function callJudge(pair: CitationPair, cacheKey: string, model: string): Promise<CitationJudgement> {\n const userMessage =\n `Claim: ${pair.claimText}\\n\\nSource (${pair.citedFile}, lines ${pair.lineStart}–${pair.lineEnd}):\\n${pair.spanText}`;\n\n const raw = await callClaude({\n system: JUDGE_SYSTEM,\n messages: [{ role: \"user\", content: userMessage }],\n tools: [JUDGE_TOOL],\n maxTokens: 256,\n });\n\n const parsed = JSON.parse(raw) as { score: 0 | 1 | 2; reason: string };\n return {\n claimHash: cacheKey,\n pageSlug: pair.pageSlug,\n citedFile: pair.citedFile,\n lineStart: pair.lineStart,\n lineEnd: pair.lineEnd,\n claimText: pair.claimText,\n spanText: pair.spanText,\n score: parsed.score,\n reason: parsed.reason,\n model,\n timestamp: new Date().toISOString(),\n };\n}\n\n/** Aggregate raw judgement scores into summary statistics. */\nfunction aggregateJudgements(judgements: CitationJudgement[]): Pick<\n CitationSupportResult,\n \"meanScore\" | \"fullySupported\" | \"partiallySupported\" | \"unsupported\"\n> {\n const fullySupported = judgements.filter((j) => j.score === 2).length;\n const partiallySupported = judgements.filter((j) => j.score === 1).length;\n const unsupported = judgements.filter((j) => j.score === 0).length;\n const meanScore =\n judgements.length === 0\n ? 0\n : judgements.reduce((sum, j) => sum + j.score, 0) / judgements.length;\n return { meanScore, fullySupported, partiallySupported, unsupported };\n}\n\n/**\n * Judge each non-cached pair in the sample, returning all collected judgements\n * and an error count. Throws if every non-cached call fails (credentials missing,\n * provider down, etc.) — an empty result would be meaningless.\n */\nasync function judgeNewPairs(\n sample: CitationPair[],\n cache: Map<string, CitationJudgement>,\n root: string,\n): Promise<{ judgements: CitationJudgement[]; judgeErrors: number }> {\n const model = resolveModel();\n const judgements: CitationJudgement[] = [];\n let judgeErrors = 0;\n let newPairsAttempted = 0;\n let firstError: unknown;\n\n for (const pair of sample) {\n const cacheKey = makeCacheKey(pair.claimHash, model);\n const cached = cache.get(cacheKey);\n if (cached) {\n judgements.push(cached);\n } else {\n newPairsAttempted++;\n try {\n const judgement = await callJudge(pair, cacheKey, model);\n await appendCachedJudgement(root, judgement);\n judgements.push(judgement);\n } catch (err) {\n judgeErrors++;\n if (firstError === undefined) firstError = err;\n }\n }\n }\n\n if (newPairsAttempted > 0 && judgeErrors === newPairsAttempted) {\n const msg = firstError instanceof Error ? firstError.message : String(firstError);\n throw new Error(`Citation judge failed for all ${judgeErrors} sampled pair(s): ${msg}`);\n }\n\n return { judgements, judgeErrors };\n}\n\n/**\n * Run the citation support judge across a stable sample of wiki citations.\n * Returns null if no citable paragraphs exist.\n * @param root - Absolute path to the project root.\n * @param sampleSize - Number of citation pairs to judge per run.\n * @param previousHashes - Hashes sampled in the prior run; retained to prevent sample churn.\n */\nexport async function evaluateCitationSupport(\n root: string,\n sampleSize = 20,\n previousHashes: string[] = [],\n): Promise<CitationSupportResult | null> {\n const allPairs = await extractCitationPairs(root);\n if (allPairs.length === 0) return null;\n\n const sample = selectDeterministicSample(allPairs, sampleSize, previousHashes);\n const cache = await loadCachedJudgements(root);\n const { judgements, judgeErrors } = await judgeNewPairs(sample, cache, root);\n\n return {\n sampledCount: judgements.length,\n sampledHashes: sample.map((p) => p.claimHash),\n totalCitations: allPairs.length,\n judgeErrors,\n ...aggregateJudgements(judgements),\n judgements,\n };\n}\n","/**\n * Corpus size stats collector for the llmwiki eval harness.\n *\n * Snapshots source count, page count, total wiki character count, and\n * embedding counts. Each run appends one JSON line to .llmwiki/eval/history.jsonl\n * for trend analysis over time.\n */\n\nimport { readdir, appendFile, mkdir, readFile } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport { collectAllPages } from \"../linter/rules.js\";\nimport { parseFrontmatter } from \"../utils/markdown.js\";\nimport { readEmbeddingStore } from \"../utils/embeddings.js\";\nimport { SOURCES_DIR } from \"../utils/constants.js\";\nimport type { StatsResult, EvalReport } from \"./types.js\";\n\nconst HISTORY_DIR = path.join(\".llmwiki\", \"eval\");\nconst HISTORY_FILE = path.join(HISTORY_DIR, \"history.jsonl\");\n\n/** Count the number of files in a directory (non-recursive, ignores missing dir). */\nasync function countFiles(dir: string): Promise<number> {\n if (!existsSync(dir)) return 0;\n const entries = await readdir(dir);\n return entries.filter((e) => e.endsWith(\".md\")).length;\n}\n\n/**\n * Collect a corpus size snapshot for the current project state.\n * @param root - Absolute path to the project root.\n */\nexport async function collectStats(root: string): Promise<StatsResult> {\n const [sourceCount, pages, embeddingStore] = await Promise.all([\n countFiles(path.join(root, SOURCES_DIR)),\n collectAllPages(root),\n readEmbeddingStore(root),\n ]);\n\n let totalWikiChars = 0;\n for (const { content } of pages) {\n const { body } = parseFrontmatter(content);\n totalWikiChars += body.length;\n }\n\n const pageCount = pages.length;\n const avgPageLengthChars = pageCount === 0 ? 0 : Math.round(totalWikiChars / pageCount);\n const embeddingCount = embeddingStore?.entries.length ?? 0;\n const chunkEmbeddingCount = embeddingStore?.chunks?.length ?? 0;\n\n return {\n timestamp: new Date().toISOString(),\n sourceCount,\n pageCount,\n totalWikiChars,\n embeddingCount,\n chunkEmbeddingCount,\n avgPageLengthChars,\n };\n}\n\n/**\n * Append the current eval report as a single JSON line to history.jsonl.\n * Creates the directory if it does not exist.\n * @param root - Absolute path to the project root.\n * @param report - The completed EvalReport to persist.\n */\nexport async function appendHistory(root: string, report: EvalReport): Promise<void> {\n const historyDir = path.join(root, HISTORY_DIR);\n await mkdir(historyDir, { recursive: true });\n await appendFile(path.join(root, HISTORY_FILE), JSON.stringify(report) + \"\\n\");\n}\n\n/**\n * Load the last N eval reports from history.jsonl, oldest first.\n * Returns an empty array if no history file exists or the file is empty.\n * @param root - Absolute path to the project root.\n * @param n - Maximum number of reports to return (default 10).\n */\nexport async function loadHistory(root: string, n = 10): Promise<EvalReport[]> {\n const historyPath = path.join(root, HISTORY_FILE);\n if (!existsSync(historyPath)) return [];\n\n const content = await readFile(historyPath, \"utf-8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n const reports: EvalReport[] = [];\n for (const line of lines.slice(-n)) {\n try {\n reports.push(JSON.parse(line) as EvalReport);\n } catch {\n // Skip malformed lines\n }\n }\n return reports;\n}\n\n/** Read the history file and return its non-empty lines, or null if it doesn't exist. */\nasync function readHistoryLines(root: string): Promise<string[] | null> {\n const historyPath = path.join(root, HISTORY_FILE);\n if (!existsSync(historyPath)) return null;\n const content = await readFile(historyPath, \"utf-8\");\n return content.trim().split(\"\\n\").filter(Boolean);\n}\n\n/**\n * Load the most recent eval report from history.jsonl, or null if none exists.\n * @param root - Absolute path to the project root.\n */\nexport async function loadPreviousReport(root: string): Promise<EvalReport | null> {\n const lines = await readHistoryLines(root);\n if (!lines || lines.length === 0) return null;\n try {\n return JSON.parse(lines[lines.length - 1]) as EvalReport;\n } catch {\n return null;\n }\n}\n\n/**\n * Load the most recent full-suite eval report from history.jsonl, skipping fast-suite\n * entries. This ensures citation-support sampledHashes are not lost when a fast run\n * occurs between two full runs.\n * @param root - Absolute path to the project root.\n */\nexport async function loadLastFullReport(root: string): Promise<EvalReport | null> {\n const lines = await readHistoryLines(root);\n if (!lines) return null;\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const report = JSON.parse(lines[i]) as EvalReport;\n if (report.suite === \"full\") return report;\n } catch {\n // Skip malformed lines\n }\n }\n return null;\n}\n","/**\n * Regression detection for the llmwiki eval harness.\n *\n * Computes signed metric deltas between the current eval report and the\n * previous one from history.jsonl. Positive = improvement, negative = regression.\n * Fields are omitted when either report lacks the data (e.g. citationSupportMean\n * is only present on full-suite runs).\n */\n\nimport type { EvalReport, EvalDelta } from \"./types.js\";\n\n/**\n * Compute the signed difference between current and previous eval metrics.\n * @param current - The just-completed report.\n * @param previous - The last report loaded from history.jsonl.\n */\nexport function computeDelta(current: EvalReport, previous: EvalReport): EvalDelta {\n const delta: EvalDelta = {\n healthScore: current.health.score - previous.health.score,\n citationCoveragePercent:\n current.citationCoverage.coveragePercent - previous.citationCoverage.coveragePercent,\n citationPrecisionPercent:\n current.citationCoverage.precisionPercent - previous.citationCoverage.precisionPercent,\n };\n\n if (current.citationSupport !== undefined && previous.citationSupport !== undefined) {\n delta.citationSupportMean =\n current.citationSupport.meanScore - previous.citationSupport.meanScore;\n }\n\n return delta;\n}\n","/**\n * CI threshold gating for the llmwiki eval harness.\n *\n * Reads an optional .llmwiki/eval/thresholds.yaml config and checks whether\n * the current report meets each configured threshold. Returns a list of\n * human-readable violation messages; an empty list means all thresholds pass.\n *\n * Supported threshold keys:\n * health_score — minimum health score (0–100)\n * citation_coverage_percent — minimum citation coverage (0–100)\n * citation_precision_percent — minimum citation precision (0–100)\n * citation_support_mean — minimum mean judge score (0.0–2.0)\n * source_utilization_rate — minimum fraction of sources cited (0.0–1.0)\n * source_warnings_max — maximum source-inventory warnings allowed (integer)\n * claim_level_citation_rate — minimum fraction of citations with line ranges (0.0–1.0)\n */\n\nimport { readFile } from \"fs/promises\";\nimport { existsSync } from \"fs\";\nimport path from \"path\";\nimport yaml from \"js-yaml\";\nimport type { EvalReport } from \"./types.js\";\n\nconst THRESHOLDS_FILE = path.join(\".llmwiki\", \"eval\", \"thresholds.yaml\");\n\ninterface ThresholdConfig {\n health_score?: number;\n citation_coverage_percent?: number;\n citation_precision_percent?: number;\n citation_support_mean?: number;\n /** Maximum number of judge call failures allowed per run (0 = any error fails CI). */\n citation_judge_error_max?: number;\n /** Minimum fraction of sources cited by ≥1 wiki page (0.0–1.0). */\n source_utilization_rate?: number;\n /**\n * Maximum source-inventory warnings allowed (e.g. out-of-tree symlinks or\n * unresolvable files excluded from the count). Gates inventory health, which\n * is distinct from utilization — an excluded source is an invalid entry, not\n * an uncited one. 0 = any excluded source fails CI.\n */\n source_warnings_max?: number;\n claim_level_citation_rate?: number;\n}\n\n/** Load the threshold config from disk, or return an empty config if absent. */\nasync function loadThresholds(root: string): Promise<ThresholdConfig> {\n const configPath = path.join(root, THRESHOLDS_FILE);\n if (!existsSync(configPath)) return {};\n const raw = await readFile(configPath, \"utf-8\");\n return (yaml.load(raw) as ThresholdConfig) ?? {};\n}\n\n/** Check the source-utilization and citation-depth thresholds. */\nfunction checkNewThresholds(\n violations: string[],\n config: ThresholdConfig,\n report: EvalReport,\n): void {\n const util = report.sourceUtilization;\n if (\n config.source_utilization_rate !== undefined &&\n util.utilizationRate !== null &&\n util.totalSources > 0 &&\n util.utilizationRate < config.source_utilization_rate\n ) {\n violations.push(\n `source_utilization_rate ${(util.utilizationRate * 100).toFixed(1)}% is below threshold ${(config.source_utilization_rate * 100).toFixed(1)}%`,\n );\n }\n\n if (config.source_warnings_max !== undefined && util.warnings.length > config.source_warnings_max) {\n violations.push(\n `source_warnings ${util.warnings.length} exceeds max ${config.source_warnings_max}`,\n );\n }\n\n if (\n config.claim_level_citation_rate !== undefined &&\n report.citationDepth.claimLevelRate < config.claim_level_citation_rate\n ) {\n violations.push(\n `claim_level_citation_rate ${(report.citationDepth.claimLevelRate * 100).toFixed(1)}% is below threshold ${(config.claim_level_citation_rate * 100).toFixed(1)}%`,\n );\n }\n}\n\n/**\n * Check the report against configured thresholds.\n * @param report - The completed eval report.\n * @param root - Absolute path to the project root.\n * @returns List of violation messages (empty = all pass).\n */\nexport async function checkThresholds(\n report: EvalReport,\n root: string,\n): Promise<string[]> {\n const config = await loadThresholds(root);\n const violations: string[] = [];\n\n if (config.health_score !== undefined && report.health.score < config.health_score) {\n violations.push(\n `health_score ${report.health.score} is below threshold ${config.health_score}`,\n );\n }\n\n if (\n config.citation_coverage_percent !== undefined &&\n report.citationCoverage.coveragePercent < config.citation_coverage_percent\n ) {\n violations.push(\n `citation_coverage_percent ${report.citationCoverage.coveragePercent.toFixed(1)}% is below threshold ${config.citation_coverage_percent}%`,\n );\n }\n\n if (\n config.citation_precision_percent !== undefined &&\n report.citationCoverage.precisionPercent < config.citation_precision_percent\n ) {\n violations.push(\n `citation_precision_percent ${report.citationCoverage.precisionPercent.toFixed(1)}% is below threshold ${config.citation_precision_percent}%`,\n );\n }\n\n if (\n config.citation_support_mean !== undefined &&\n report.citationSupport !== undefined &&\n report.citationSupport.meanScore < config.citation_support_mean\n ) {\n violations.push(\n `citation_support_mean ${report.citationSupport.meanScore.toFixed(2)} is below threshold ${config.citation_support_mean}`,\n );\n }\n\n if (\n config.citation_judge_error_max !== undefined &&\n report.citationSupport !== undefined &&\n report.citationSupport.judgeErrors > config.citation_judge_error_max\n ) {\n violations.push(\n `citation_judge_errors ${report.citationSupport.judgeErrors} exceeds max ${config.citation_judge_error_max}`,\n );\n }\n\n checkNewThresholds(violations, config, report);\n\n return violations;\n}\n","/**\n * Report assembly for the llmwiki eval harness.\n *\n * Combines evaluated components (health, citation coverage, stats, citation support)\n * into a final EvalReport with delta tracking and threshold validation.\n * Shared by the CLI eval command and the MCP run_eval tool.\n */\n\nimport { evaluateHealth } from \"./health.js\";\nimport { evaluateCitationCoverage } from \"./citation-coverage.js\";\nimport { evaluateSourceUtilization } from \"./source-utilization.js\";\nimport { evaluateCitationDepth } from \"./citation-depth.js\";\nimport { evaluateCitationSupport } from \"./citation-support.js\";\nimport { collectStats, appendHistory, loadPreviousReport, loadLastFullReport } from \"./stats.js\";\nimport { computeDelta } from \"./delta.js\";\nimport { checkThresholds } from \"./thresholds.js\";\nimport { ensureProviderAvailable } from \"../utils/provider-guard.js\";\nimport type { EvalReport, HealthResult, CitationCoverageResult, SourceUtilizationResult, CitationDepthResult, CitationSupportResult, StatsResult } from \"./types.js\";\n\nexport const DEFAULT_SAMPLE_SIZE = 20;\n\ninterface EvalComponents {\n health: HealthResult;\n citationCoverage: CitationCoverageResult;\n sourceUtilization: SourceUtilizationResult;\n citationDepth: CitationDepthResult;\n stats: StatsResult;\n previousReport: EvalReport | null;\n citationSupport?: CitationSupportResult | null;\n}\n\nasync function buildReport(root: string, components: EvalComponents, suite: \"fast\" | \"full\"): Promise<EvalReport> {\n const { health, citationCoverage, sourceUtilization, citationDepth, stats, previousReport, citationSupport } = components;\n const partial = {\n suite,\n timestamp: new Date().toISOString(),\n health,\n citationCoverage,\n sourceUtilization,\n citationDepth,\n stats,\n ...(citationSupport ? { citationSupport } : {}),\n };\n const delta = previousReport ? computeDelta(partial as EvalReport, previousReport) : undefined;\n const thresholdViolations = await checkThresholds(partial as EvalReport, root);\n return { ...partial, ...(delta ? { delta } : {}), thresholdViolations };\n}\n\n/** Run the full eval pipeline, optionally append to history, and return the report. */\nexport async function runEval(root: string, suite: \"fast\" | \"full\", sampleSize: number, record = true): Promise<EvalReport> {\n if (suite === \"full\") ensureProviderAvailable();\n const [health, citationCoverage, sourceUtilization, citationDepth, stats, previousReport, previousFullReport] = await Promise.all([\n evaluateHealth(root),\n evaluateCitationCoverage(root),\n evaluateSourceUtilization(root),\n evaluateCitationDepth(root),\n collectStats(root),\n loadPreviousReport(root),\n suite === \"full\" ? loadLastFullReport(root) : Promise.resolve(null),\n ]);\n const citationSupport = suite === \"full\"\n ? await evaluateCitationSupport(root, sampleSize, previousFullReport?.citationSupport?.sampledHashes ?? [])\n : undefined;\n const report = await buildReport(root, { health, citationCoverage, sourceUtilization, citationDepth, stats, previousReport, citationSupport }, suite);\n if (record) await appendHistory(root, report);\n return report;\n}\n","/**\n * Read-only project-status collector shared by the MCP `wiki_status` tool and the in-process SDK.\n *\n * Derives stale/orphaned page classification from the freshness module so\n * agents get source-level accuracy rather than frontmatter-only orphans.\n * Uses `readStateClassified` throughout — never `readState` — so corrupt\n * state.json never produces a `.bak` side-effect.\n *\n * `pendingChanges` is derived directly from the freshness snapshot (which\n * already has per-source currentHash/recordedHash/exists) plus a cheap\n * directory listing — no second hash pass. `detectChanges` is NOT called.\n */\n\nimport path from \"path\";\nimport { readdir } from \"fs/promises\";\nimport { collectPageSummaries, scanWikiPages } from \"../compiler/indexgen.js\";\nimport { countCandidates } from \"../compiler/candidates.js\";\nimport { readStateClassified } from \"../utils/state.js\";\nimport { buildFreshnessSnapshot, computeFreshness } from \"../freshness/index.js\";\nimport { CONCEPTS_DIR, QUERIES_DIR, SOURCES_DIR } from \"../utils/constants.js\";\nimport type { FreshnessSnapshot } from \"../freshness/types.js\";\n\n/**\n * Maximum number of items returned in each agent-facing list (stalePages,\n * orphanedPages, pendingChanges). Bounds the MCP response size and agent\n * context consumption on large wikis. True totals are in the matching *Count fields.\n */\nexport const MAX_STATUS_LIST = 100;\n\n/** Shape returned by `collectStatus` and surfaced by the `wiki_status` tool. */\nexport interface WikiStatus {\n pages: { concepts: number; queries: number; total: number };\n sources: number;\n lastCompiledAt: string | null;\n /**\n * Concept slugs whose source changed or partially disappeared since compile.\n * Capped at MAX_STATUS_LIST (sorted ascending); see staleCount for the true total.\n */\n stalePages: string[];\n /** True total of stale pages (may exceed stalePages.length when capped). */\n staleCount: number;\n /**\n * Concept slugs whose every owning source was deleted, or frontmatter-flagged orphaned.\n * Capped at MAX_STATUS_LIST (sorted ascending); see orphanedCount for the true total.\n */\n orphanedPages: string[];\n /** True total of orphaned pages (may exceed orphanedPages.length when capped). */\n orphanedCount: number;\n /** Readability of .llmwiki/state.json — surfaced so corrupt state is never silent. */\n stateStatus: \"ok\" | \"missing\" | \"corrupt\";\n /** Number of compile candidates awaiting human review. */\n pendingCandidates: number;\n /**\n * Source files with changes since last compile (new/changed/deleted).\n * Capped at MAX_STATUS_LIST (sorted by file); see pendingChangesCount for the true total.\n */\n pendingChanges: Array<{ file: string; status: string }>;\n /** True total of pending changes (may exceed pendingChanges.length when capped). */\n pendingChangesCount: number;\n}\n\n/** Classify scanned concept pages into stale/orphaned arrays using the freshness snapshot. */\nfunction classifyConceptPages(\n scanned: { slug: string; meta: Record<string, unknown> }[],\n snapshot: FreshnessSnapshot,\n): { stalePages: string[]; orphanedPages: string[] } {\n const stalePages: string[] = [];\n const orphanedPages: string[] = [];\n for (const { slug, meta } of scanned) {\n const { freshnessStatus } = computeFreshness(\n { slug, pageDirectory: \"concepts\", frontmatter: meta },\n snapshot,\n );\n if (freshnessStatus === \"stale\") stalePages.push(slug);\n else if (freshnessStatus === \"orphaned\") orphanedPages.push(slug);\n }\n return { stalePages, orphanedPages };\n}\n\n/** Derive the last compile time from state sources, or null if no sources. */\nfunction lastCompileTime(sources: Record<string, { compiledAt: string }>): string | null {\n const times = Object.values(sources).map((s) => s.compiledAt);\n return times.length > 0 ? times.sort().slice(-1)[0] : null;\n}\n\n/** List markdown filenames in sources/ without hashing — cheap directory scan. */\nasync function listSourceFilesOnDisk(root: string): Promise<string[]> {\n try {\n const entries = await readdir(path.join(root, SOURCES_DIR));\n return entries.filter((f) => f.endsWith(\".md\"));\n } catch {\n return [];\n }\n}\n\n/**\n * Derive pending source changes (new/changed/deleted) from the freshness snapshot — no extra hashing.\n * The snapshot already contains recordedHash, currentHash, and exists for each tracked source,\n * so we only need a cheap directory listing to discover untracked (new) files.\n */\nfunction pendingChangesFromSnapshot(\n snapshot: FreshnessSnapshot,\n sourceFilesOnDisk: string[],\n): Array<{ file: string; status: string }> {\n const out: Array<{ file: string; status: string }> = [];\n for (const [file, s] of Object.entries(snapshot.sources)) {\n if (!s.exists) out.push({ file, status: \"deleted\" });\n else if (s.currentHash !== s.recordedHash) out.push({ file, status: \"changed\" });\n }\n const recorded = new Set(Object.keys(snapshot.sources));\n for (const file of sourceFilesOnDisk) {\n if (!recorded.has(file)) out.push({ file, status: \"new\" });\n }\n return out;\n}\n\n/**\n * Sort a slug list ascending and cap it at MAX_STATUS_LIST.\n * Deterministic truncation ensures agents see the same subset on repeated calls.\n */\nfunction capSlugs(slugs: string[]): string[] {\n return slugs.slice().sort().slice(0, MAX_STATUS_LIST);\n}\n\n/**\n * Sort a pending-change list by file name ascending and cap at MAX_STATUS_LIST.\n * Deterministic truncation matches capSlugs behaviour for consistency.\n */\nfunction capPendingChanges(\n changes: Array<{ file: string; status: string }>,\n): Array<{ file: string; status: string }> {\n return changes.slice().sort((a, b) => a.file.localeCompare(b.file)).slice(0, MAX_STATUS_LIST);\n}\n\n/** Build a read-only status snapshot used by the `wiki_status` MCP tool. */\nexport async function collectStatus(root: string): Promise<WikiStatus> {\n const classified = await readStateClassified(root);\n const snapshot = await buildFreshnessSnapshot(root, classified);\n\n const [conceptSummaries, queries, scannedConcepts, pendingCandidates, sourceFilesOnDisk] = await Promise.all([\n collectPageSummaries(path.join(root, CONCEPTS_DIR)),\n collectPageSummaries(path.join(root, QUERIES_DIR)),\n scanWikiPages(path.join(root, CONCEPTS_DIR)),\n countCandidates(root),\n listSourceFilesOnDisk(root),\n ]);\n\n const { stalePages, orphanedPages } = classifyConceptPages(scannedConcepts, snapshot);\n\n // Suppress pendingChanges only on corrupt state: comparing against an empty snapshot\n // on corrupt state would classify every source file as \"new\", which is false precision.\n // On missing state the snapshot is legitimately empty, so every on-disk source is \"new\" — correct.\n const pendingChanges = classified.status === \"corrupt\"\n ? []\n : pendingChangesFromSnapshot(snapshot, sourceFilesOnDisk);\n\n return {\n pages: { concepts: conceptSummaries.length, queries: queries.length, total: conceptSummaries.length + queries.length },\n sources: Object.keys(classified.state.sources).length,\n lastCompiledAt: lastCompileTime(classified.state.sources),\n stalePages: capSlugs(stalePages),\n staleCount: stalePages.length,\n orphanedPages: capSlugs(orphanedPages),\n orphanedCount: orphanedPages.length,\n stateStatus: classified.status,\n pendingCandidates,\n pendingChanges: capPendingChanges(pendingChanges),\n pendingChangesCount: pendingChanges.length,\n };\n}\n","/**\n * Semantic and LLM-based page retrieval for llmwiki.\n *\n * Exports `pickSearchSlugs`, which resolves relevant page slugs for a\n * question by trying chunk-level retrieval first (highest precision), then\n * page-level embedding search, then LLM-driven selection over the wiki index.\n * Exports `loadPageRecords`, which hydrates a list of slugs into full\n * `PageRecord` objects, silently dropping missing or orphaned entries.\n *\n * This module is shared between the MCP tool layer and the in-process SDK.\n */\n\nimport path from \"path\";\nimport { findRelevantChunks, findRelevantPages } from \"../utils/embeddings.js\";\nimport { selectPages } from \"../commands/query.js\";\nimport { safeReadFile } from \"../utils/markdown.js\";\nimport { CHUNK_TOP_K, INDEX_FILE } from \"../utils/constants.js\";\nimport { readPageRecord, type PageRecord } from \"../pages/read.js\";\n\n/** Deduplicate slugs while preserving the first-seen ordering. */\nfunction dedupePreservingOrder(slugs: string[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const slug of slugs) {\n if (seen.has(slug)) continue;\n seen.add(slug);\n out.push(slug);\n }\n return out;\n}\n\n/**\n * Resolve search candidates. Tries chunk-level retrieval first (highest\n * precision), then falls back to page-level embeddings, then to LLM-driven\n * selection over the wiki index.\n *\n * @param root - Absolute path to the wiki workspace root.\n * @param question - The query used to rank pages.\n * @returns Ordered list of relevant page slugs.\n */\nexport async function pickSearchSlugs(root: string, question: string): Promise<string[]> {\n try {\n const chunks = await findRelevantChunks(root, question, CHUNK_TOP_K);\n if (chunks.length > 0) return dedupePreservingOrder(chunks.map((c) => c.chunk.slug));\n } catch {\n // Chunk store unavailable — fall through to page-level embeddings.\n }\n\n try {\n const candidates = await findRelevantPages(root, question);\n if (candidates.length > 0) return candidates.map((c) => c.slug);\n } catch {\n // Embeddings unavailable — fall through to index-based selection.\n }\n\n const indexContent = await safeReadFile(path.join(root, INDEX_FILE));\n const { pages } = await selectPages(question, indexContent);\n return pages;\n}\n\n/**\n * Load full content for a list of slugs, skipping missing/orphaned pages.\n *\n * @param root - Absolute path to the wiki workspace root.\n * @param slugs - List of page slugs to hydrate.\n * @returns Populated page records for all found, non-orphaned pages.\n */\nexport async function loadPageRecords(root: string, slugs: string[]): Promise<PageRecord[]> {\n const records: PageRecord[] = [];\n for (const slug of slugs) {\n const page = await readPageRecord(root, slug);\n if (page) records.push(page);\n }\n return records;\n}\n","/**\n * Page-reading utilities for llmwiki.\n *\n * Exposes `readPageRecord`, which locates a wiki page by slug across the\n * priority-ordered page directories (concepts first, then queries), parses\n * its frontmatter, and returns a structured `PageRecord`. Orphaned pages are\n * silently skipped to match the query pipeline's behaviour.\n *\n * This module is shared between the MCP tool layer and the in-process SDK so\n * both consumers work from identical read semantics.\n */\n\nimport path from \"path\";\nimport { safeReadFile, parseFrontmatter } from \"../utils/markdown.js\";\nimport { CONCEPTS_DIR, QUERIES_DIR } from \"../utils/constants.js\";\n\n/** Directories searched (in priority order) when resolving a page slug. */\nconst PAGE_DIRS = [CONCEPTS_DIR, QUERIES_DIR];\n\n/** Shape returned by readPageRecord and search_pages for each matching page. */\nexport interface PageRecord {\n slug: string;\n title: string;\n summary: string;\n body: string;\n}\n\n/**\n * Locate a page by slug across the priority-ordered page directories,\n * skipping orphaned entries to match the query pipeline's behaviour.\n *\n * @param root - Absolute path to the wiki workspace root.\n * @param slug - Page slug without the `.md` extension.\n * @returns The parsed page record, or `null` if not found or orphaned.\n */\nexport async function readPageRecord(root: string, slug: string): Promise<PageRecord | null> {\n for (const dir of PAGE_DIRS) {\n const content = await safeReadFile(path.join(root, dir, `${slug}.md`));\n if (!content) continue;\n\n const { meta, body } = parseFrontmatter(content);\n if (meta.orphaned) continue;\n\n return {\n slug,\n title: typeof meta.title === \"string\" ? meta.title : slug,\n summary: typeof meta.summary === \"string\" ? meta.summary : \"\",\n body: body.trim(),\n };\n }\n return null;\n}\n","/**\n * Path-safe page access primitives for the llmwiki in-process SDK.\n *\n * Exposes two public functions:\n * - `getPage(root, ref)` — fetch a single page by directory + slug; returns\n * the full `Page` shape (body included) or null when the file is absent.\n * - `listPages(root, options)` — scan both page directories, read each page's\n * body so wikilinks can be extracted, apply archive/orphan filters, sort,\n * and return a cursor-paged slice.\n *\n * Design notes:\n * - `links` are derived from the Markdown **body** via `extractWikilinkSlugs`,\n * NOT from frontmatter.\n * - `archived` and `orphaned` are boolean **frontmatter** flags.\n * - `scanWikiPages` returns `{ slug, meta }` only (no body), so `listPages`\n * always re-reads each file to extract body links even when `includeBody`\n * is false.\n * - Path safety is enforced at `getPage` entry via `assertSafeSlug`; symlink\n * confinement is handled at a lower level by `scanWikiPages`.\n */\n\nimport path from \"path\";\nimport { scanWikiPages } from \"../compiler/indexgen.js\";\nimport { safeReadFile, parseFrontmatter } from \"../utils/markdown.js\";\nimport { extractWikilinkSlugs } from \"../wiki/collect.js\";\nimport { assertSafeSlug } from \"../viewer/path-safety.js\";\nimport { CONCEPTS_DIR, QUERIES_DIR } from \"../utils/constants.js\";\nimport type { PageDirectory } from \"../export/types.js\";\n\nexport type { PageDirectory };\n\n/** A reference to a specific page by its directory and slug. */\nexport interface PageRef {\n pageDirectory: PageDirectory;\n slug: string;\n}\n\n/** A fully-resolved in-memory representation of a single wiki page. */\nexport interface Page {\n slug: string;\n pageDirectory: PageDirectory;\n title: string;\n summary: string;\n tags: string[];\n /** Slugs of pages linked via `[[wikilinks]]` in the body. */\n links: string[];\n createdAt?: string;\n updatedAt?: string;\n /** True when frontmatter contains `orphaned: true`. */\n orphaned: boolean;\n /** True when frontmatter contains `archived: true`. */\n archived: boolean;\n /** Full markdown body, present only when `includeBody` is true or via `getPage`. */\n body?: string;\n}\n\n/** Options for filtering and paginating `listPages`. */\nexport interface ListPagesOptions {\n cursor?: string;\n limit?: number;\n includeBody?: boolean;\n includeArchived?: boolean;\n includeOrphaned?: boolean;\n}\n\n/** Result returned by `listPages`. */\nexport interface ListPagesResult {\n pages: Page[];\n /** Opaque cursor for the next page; absent when the listing is exhausted. */\n cursor?: string;\n}\n\n/** Maps each PageDirectory to its project-relative path. */\nconst DIR_NAMES: Record<PageDirectory, string> = {\n concepts: CONCEPTS_DIR,\n queries: QUERIES_DIR,\n};\n\n/** All page directories in listing order. */\nconst PAGE_DIRECTORIES: PageDirectory[] = [\"concepts\", \"queries\"];\n\n/**\n * Build a Page from its directory, slug, parsed frontmatter, and body text.\n * Links are always extracted from the body so they reflect real wikilinks,\n * not any frontmatter list.\n */\nfunction buildPage(\n dir: PageDirectory,\n slug: string,\n meta: Record<string, unknown>,\n body: string,\n includeBody: boolean,\n): Page {\n return {\n slug,\n pageDirectory: dir,\n title: typeof meta.title === \"string\" ? meta.title : slug,\n summary: typeof meta.summary === \"string\" ? meta.summary : \"\",\n tags: Array.isArray(meta.tags) ? meta.tags.map(String) : [],\n links: extractWikilinkSlugs(body),\n createdAt: typeof meta.createdAt === \"string\" ? meta.createdAt : undefined,\n updatedAt: typeof meta.updatedAt === \"string\" ? meta.updatedAt : undefined,\n orphaned: meta.orphaned === true,\n archived: meta.archived === true,\n ...(includeBody ? { body } : {}),\n };\n}\n\n/**\n * Fetch a single page by directory and slug.\n *\n * Throws `PathSafetyError` for unsafe slug values. Returns `null` when the\n * page does not exist on disk.\n *\n * @param root - Absolute path to the wiki workspace root.\n * @param ref - Which directory and slug to load.\n */\nexport async function getPage(root: string, ref: PageRef): Promise<Page | null> {\n assertSafeSlug(ref.slug);\n const filePath = path.join(root, DIR_NAMES[ref.pageDirectory], `${ref.slug}.md`);\n const content = await safeReadFile(filePath);\n if (!content) return null;\n const { meta, body } = parseFrontmatter(content);\n return buildPage(ref.pageDirectory, ref.slug, meta, body, true);\n}\n\n/**\n * List pages from both wiki directories with optional filtering and cursor\n * pagination.\n *\n * Pages are sorted by `(pageDirectory, slug)` before slicing. Bodies are\n * omitted by default; pass `includeBody: true` to include them. Orphaned\n * and archived pages are excluded by default; pass the corresponding flag\n * to include them.\n *\n * @param root - Absolute path to the wiki workspace root.\n * @param options - Filtering, pagination, and inclusion options.\n */\nexport async function listPages(\n root: string,\n options: ListPagesOptions = {},\n): Promise<ListPagesResult> {\n const all = await collectPages(root, options);\n all.sort(\n (a, b) =>\n a.pageDirectory.localeCompare(b.pageDirectory) || a.slug.localeCompare(b.slug),\n );\n return paginate(all, options);\n}\n\n/**\n * Read every page from both directories, applying the archive/orphan filters.\n * Bodies are always read so wikilinks can be extracted; they are only retained\n * on the returned Page when `includeBody` is set.\n */\nasync function collectPages(root: string, options: ListPagesOptions): Promise<Page[]> {\n const all: Page[] = [];\n for (const dir of PAGE_DIRECTORIES) {\n const dirPath = path.join(root, DIR_NAMES[dir]);\n for (const { slug, meta } of await scanWikiPages(dirPath)) {\n const content = await safeReadFile(path.join(dirPath, `${slug}.md`));\n const { body } = parseFrontmatter(content);\n const page = buildPage(dir, slug, meta, body, options.includeBody === true);\n if (page.orphaned && !options.includeOrphaned) continue;\n if (page.archived && !options.includeArchived) continue;\n all.push(page);\n }\n }\n return all;\n}\n\n/**\n * Slice an already-sorted page list into a cursor-paged result. A non-positive\n * or absent limit is treated as unbounded (avoids a `limit: 0` empty-slice loop);\n * a non-integer or negative cursor is rejected rather than silently recycled.\n */\nfunction paginate(all: Page[], options: ListPagesOptions): ListPagesResult {\n const offset = options.cursor !== undefined ? Number(options.cursor) : 0;\n if (!Number.isInteger(offset) || offset < 0) {\n throw new Error(`invalid listPages cursor: ${options.cursor}`);\n }\n const limit = options.limit && options.limit > 0 ? options.limit : all.length;\n const slice = all.slice(offset, offset + limit);\n const nextOffset = offset + slice.length;\n const cursor = nextOffset < all.length ? String(nextOffset) : undefined;\n return cursor !== undefined ? { pages: slice, cursor } : { pages: slice };\n}\n","/**\n * Source-store API for the llmwiki SDK: list / get sources under `sources/`.\n * Source IDs are bare basenames including `.md` (e.g. \"note.md\") — opaque,\n * path-safe, never joined with an extra extension. Read-only, no LLM.\n */\nimport path from \"path\";\nimport { readdir, readFile, unlink } from \"fs/promises\";\nimport { parseFrontmatter } from \"../utils/markdown.js\";\nimport { PathSafetyError } from \"../viewer/path-safety.js\";\nimport { confinedRegularFile, resolveSourcesDir } from \"../utils/path-confine.js\";\n\n/** A single source file under `sources/`, with frontmatter metadata. */\nexport interface SourceRecord {\n id: string; // basename incl. \".md\"\n title: string;\n source: string; // frontmatter `source` identity\n sourceType: string;\n ingestedAt?: string;\n body?: string;\n}\n\n/** Options for paginating `listSources` and opting into source bodies. */\nexport interface ListSourcesOptions { cursor?: string; limit?: number; includeBody?: boolean }\n\n/** Result returned by `listSources`. */\nexport interface ListSourcesResult { sources: SourceRecord[]; cursor?: string }\n\n/** Reject anything that isn't a bare `sources/` basename ending in `.md`. */\nfunction assertSafeSourceId(id: string): void {\n if (typeof id !== \"string\" || id.length === 0) throw new PathSafetyError(\"source id must be a non-empty string\");\n if (!id.endsWith(\".md\")) throw new PathSafetyError(`source id must end in .md: \"${id}\"`);\n // Conservative: real source IDs are `slug-<hex>.md` (slugified title + collision\n // hash) and never contain `..`, so rejecting any `..` substring is safe and\n // simpler than component-level normalization.\n if (id.includes(\"/\") || id.includes(\"\\\\\") || id.includes(\"\\0\") || id.includes(\"..\"))\n throw new PathSafetyError(`source id must be a bare basename: \"${id}\"`);\n}\n\nfunction toRecord(id: string, content: string, includeBody: boolean): SourceRecord {\n const { meta, body } = parseFrontmatter(content);\n return {\n id,\n title: typeof meta.title === \"string\" ? meta.title : id,\n source: typeof meta.source === \"string\" ? meta.source : \"\",\n sourceType: typeof meta.sourceType === \"string\" ? meta.sourceType : \"file\",\n ingestedAt: typeof meta.ingestedAt === \"string\" ? meta.ingestedAt : undefined,\n ...(includeBody ? { body } : {}),\n };\n}\n\nexport async function listSources(root: string, options: ListSourcesOptions = {}): Promise<ListSourcesResult> {\n const dir = await resolveSourcesDir(root);\n if (dir === null) return { sources: [] };\n let files: string[];\n try {\n files = (await readdir(dir)).filter((f) => f.endsWith(\".md\")).sort();\n } catch (err) {\n if ((err as { code?: string }).code === \"ENOENT\") return { sources: [] };\n throw err;\n }\n const offset = options.cursor !== undefined ? Number(options.cursor) : 0;\n if (!Number.isInteger(offset) || offset < 0) throw new Error(`invalid listSources cursor: ${options.cursor}`);\n const limit = options.limit && options.limit > 0 ? options.limit : files.length;\n const page = files.slice(offset, offset + limit);\n const sources: SourceRecord[] = [];\n for (const id of page) {\n // Skip any entry that is not a confined regular file (symlinks — whether\n // escaping or in-tree aliases — are never sources).\n const real = await confinedRegularFile(dir, id);\n if (real === null) continue;\n const content = await readFile(real, \"utf-8\");\n sources.push(toRecord(id, content, options.includeBody === true));\n }\n const next = offset + page.length < files.length ? String(offset + page.length) : undefined;\n return next !== undefined ? { sources, cursor: next } : { sources };\n}\n\n/**\n * Delete the source file `sources/<id>` (the id already includes `.md`).\n * Returns `true` if a file was removed, `false` if the (valid) id had no file.\n * Reconciliation of the now-orphaned compiled page is deferred to the next\n * `compile`, consistent with how llmwiki already handles deleted sources —\n * this does not touch `wiki/`.\n */\nexport async function deleteSource(root: string, id: string): Promise<boolean> {\n assertSafeSourceId(id);\n const dir = await resolveSourcesDir(root);\n if (dir === null) return false;\n // Not a confined regular file (missing, symlink, or directory) → nothing to delete.\n // Keeps delete coherent with getSource/listSources.\n const real = await confinedRegularFile(dir, id);\n if (real === null) return false;\n try {\n await unlink(path.join(dir, id));\n return true;\n } catch (err) {\n if ((err as { code?: string }).code === \"ENOENT\") return false;\n throw err;\n }\n}\n\nexport async function getSource(root: string, id: string): Promise<SourceRecord | null> {\n assertSafeSourceId(id);\n const dir = await resolveSourcesDir(root);\n if (dir === null) return null;\n // Only confined regular files are valid sources (symlinks — in-tree or escaping — are not).\n const real = await confinedRegularFile(dir, id);\n if (real === null) return null;\n let content: string;\n try {\n content = await readFile(real, \"utf-8\");\n } catch (err) {\n if ((err as { code?: string }).code === \"ENOENT\") return null;\n throw err;\n }\n return toRecord(id, content, true);\n}\n"],"mappings":"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","names":["path","existsSync","path","readFile","createHash","path","mkdir","readFile","writeFile","lstat","path","path","path","path","path","source","candidatePath","path","lstat","readFile","mkdir","writeFile","path","path","readFile","path","path","path","readFile","readFile","info","readFile","readFile","path","Anthropic","Anthropic","path","readFile","readFile","path","source","path","readFile","source","TIMESTAMP_PATTERN","readFile","path","createHash","readFile","readdir","path","readFile","writeFile","rename","mkdir","path","path","readFile","mkdir","writeFile","rename","path","createHash","readFile","readdir","path","readFile","createHash","path","status","readdir","path","tool","error","readFile","mkdir","path","path","mkdir","readFile","existsSync","readFile","path","yaml","path","existsSync","yaml","readFile","yaml","path","path","readdir","readFile","path","existsSync","path","existsSync","readdir","readFile","readdir","path","path","readdir","TRUNCATION_MARKER","readdir","path","path","readdir","readFile","readdir","existsSync","path","createHash","createHash","query","path","existsSync","readFile","readdir","path","readdir","rename","unlink","writeFile","mkdir","existsSync","existsSync","readdir","path","path","readdir","readFile","existsSync","path","existsSync","realpath","path","status","path","realpath","existsSync","WIKILINK_PATTERN","existsSync","readdir","path","readFile","readdir","path","path","readdir","readdir","path","readFile","error","existsSync","path","rawPages","reasoning","path","existsSync","readdir","readFile","realpath","path","readdir","readFile","path","readFile","path","readdir","query","resolvePageKind","listSourceFiles","path","realpath","readdir","readFile","readdir","readFile","path","lint","path","readFile","readdir","path","path","query","MAX_NORMALIZED_SCORE","source","buildProject","path","createHash","source","path","require","path","realpath","path","containsParentSegment","isInside","path","readdir","lstat","existsSync","path","listSourceFiles","existsSync","readdir","path","stat","lstat","createHash","readFile","appendFile","mkdir","existsSync","path","path","createHash","readFile","existsSync","mkdir","appendFile","readdir","appendFile","mkdir","readFile","existsSync","path","path","existsSync","readdir","mkdir","appendFile","path","existsSync","readFile","readFile","existsSync","path","yaml","path","readdir","readdir","path","path","path","PAGE_DIRS","readPageRecord","path","path","readPageRecord","path","path","path","readdir","readFile","unlink","readdir","readFile","unlink","path","path","existsSync","source"]}