/** * WikiIngestService — write into a vault's `llm-curated/`. * * Two kinds of write: * - **log entry** (default): append to `llm-curated/log.md`. Cheap, * append-only, no gate beyond confidentiality. This is where "maybe * useful" goes — the default is NOT to keep. * - **page** (`targetRelativePath` under llm-curated/): a piece of * knowledge. Passes the retention gate (title + one-line summary + * keep_because), the vault's write policy (a `proposed_only` role's * page lands in `_proposed/` until a canonical role accepts), the * confidentiality gate (secrets never; PII per vault), gets YAML * frontmatter, an index line, and a history snapshot of what it * replaced. * * Frozen-path contract (§2): refuses ANY write whose target lives under a * `hardcoded:` folder. SchemaLoaderService.isFrozenPath() is the gate. * * @module services/wiki/wiki-ingest.service */ import { SchemaLoaderService } from './schema-loader.service.js'; import { type WikiKeepBecause } from '../../constants.js'; import { WikiIndexService } from './wiki-index.service.js'; import { WikiHistoryService } from './wiki-history.service.js'; /** * Categories of sources that trigger ingest. Mirrors §4 (ingest trigger * taxonomy) plus `user_chat` for the demo path. */ export type WikiSourceType = 'user_chat' | 'slack_message' | 'spec_file' | 'pr_merge' | 'record_learning' | 'task_verified' /** An experiment result (backend only; specs/experiment-cards.md) */ | 'experiment' /** A daily ticket-autopilot retro (backend only; specs/2026-10-03-autopilot-experiments.md) */ | 'autopilot_retro' /** An automatic per-task work record (backend only; specs/2026-10-17-work-records.md) */ | 'work_record'; export interface WikiIngestInput { /** Absolute path to the vault root (containing SCHEMA.md). */ vaultPath: string; /** Where the content came from. */ sourceType: WikiSourceType; /** Stable reference for audit (URL, file path, WI id, chat msg id). */ sourceRef: string; /** Body content to ingest. Empty bodies are rejected. */ sourceBody: string; /** Session/user that authored the source — appears in the log header. */ callerSession?: string; /** Writer's role as resolved by the server (undefined = owner/UI). Drives write_policy. */ callerRole?: string; /** Optional override of the relative target path; defaults to `llm-curated/log.md`. */ targetRelativePath?: string; /** Page writes only: the page's name. */ title?: string; /** Page writes only: the one-line conclusion — what this means for us. */ summary?: string; /** Page writes only: why it earns a page. */ keepBecause?: WikiKeepBecause | string; tags?: string[]; /** Page writes only: roles that may read it (default: the vault's default_visibility). */ visibility?: string[]; /** Page writes only: replace the page instead of appending to it (default: append). */ replace?: boolean; } export interface WikiIngestResult { ok: true; pagesWritten: string[]; logEntry: string; frozenPathsTouched: string[]; /** True when the page landed in `_proposed/` (writer is a proposed_only role). */ proposed?: boolean; /** PII pattern names that were masked (vault privacy `mask`). */ masked?: string[]; indexUpdated?: boolean; } export interface WikiIngestRefusedGate { ok: false; reason: 'retention_gate' | 'secret_detected' | 'pii_refused'; message: string; /** retention_gate: missing fields; privacy: matched pattern names. */ details: string[]; } export interface WikiIngestRefusedFrozen { ok: false; reason: 'frozen_path'; attemptedPath: string; frozenFolders: string[]; } export interface WikiIngestRefusedInvalid { ok: false; /** * `content_free`: a templated record with nothing a reader could use * (an auto-learning state transition); `duplicate`: the same body was * appended to this log recently (#1345). Both are refused, not written. */ reason: 'invalid_input' | 'schema_missing' | 'empty_body' | 'content_free' | 'duplicate'; message: string; } export type WikiIngestOutcome = WikiIngestResult | WikiIngestRefusedFrozen | WikiIngestRefusedInvalid | WikiIngestRefusedGate; /** * Writes ingest pages to a vault. Construct one per process; stateless. */ export declare class WikiIngestService { private static instance; private readonly logger; private readonly schemaLoader; private readonly index; private readonly history; /** * Per log file: keys of the bodies appended most recently (insertion * ordered, bounded), seeded from the file tail on first use. An append * whose body key is present is refused as a duplicate (#1345). */ private readonly recentBodies; constructor(schemaLoader?: SchemaLoaderService, index?: WikiIndexService, history?: WikiHistoryService); static getInstance(): WikiIngestService; /** Test-only reset. */ static _resetForTesting(): void; /** * Ingest a single source into the vault. * * Errors are returned as structured outcomes — this method never throws * on legitimate refusals (frozen path, missing schema, empty body) so * callers can fold the result into their normal control flow. */ ingest(input: WikiIngestInput): Promise; /** * Write (create, append to, or replace) a knowledge page. See the module * header for the gates it passes. * * @param schema - Vault schema * @param target - Vault-relative page path (validated, not frozen) * @param input - Ingest input with a body that already passed the privacy gate * @param masked - PII pattern names masked in the body * @returns Outcome */ private writePage; /** * The provenance block appended to a page for each ingest. * * @param input - Ingest input * @returns Markdown block */ private formatPageEntry; private titleFromBody; private validateInput; /** * Build the markdown entry. Format is chosen to be safely append-only * (no preceding section to rewrite) and grep-friendly: * * ## [] | * * ref: * * */ private formatLogEntry; private appendOrCreate; /** * Whether `body` was appended to this log recently. The first call for a * log seeds the set from the file's tail; every accepted append records * its key; the set is bounded FIFO. A rotation replaces the file, so the * set is re-seeded when the file disappears or shrinks. * * @param absolutePath - Log file * @param body - Candidate body * @returns True when an identical body is in the recent set (and records the key otherwise) */ private isRecentDuplicate; /** * Body keys of the entries in the last `DEDUPE_SEED_TAIL_BYTES` of a log. * * @param absolutePath - Log file * @returns Keys in file order (empty when the file is missing) */ private seedRecentKeys; /** * Forget the recent-body set of a log (a rotation replaced the file). * * @param absolutePath - Log file */ forgetRecentBodies(absolutePath: string): void; /** * Defuse markers a future skill might mis-route on: `[CHAT]`, `[NOTIFY]`, * `[EVENT]`, `[ESCALATION]` get a zero-width space inserted so they're * still human-readable but don't trigger regex matchers downstream. * (Mirrors the escalation-router sanitizer from PR #606.) */ private sanitizeBody; private sanitizeOneLine; } //# sourceMappingURL=wiki-ingest.service.d.ts.map