import Database from 'better-sqlite3'; import type { CorpusStats } from '../domain/radar.js'; import type { FetchResult } from './harvest.js'; export interface Voice { pillars: string[]; banned: string[]; vocabulary: { preferred: string[]; banned: string[]; }; bannedConstructions: string[]; aesthetic: string; examples: string[]; lessons: { at: string; reason: string; }[]; styleRules: string[]; preferredTerms: Record; lint: { maxSentenceWords?: number; minSentenceWords?: number; maxSentenceChars?: number; }; } export declare const DEFAULT_VOICE: Voice; export interface Radar { coreOffer: string; goals: string[]; topics: string[]; avoid: string[]; location: string; directUrls: string[]; freshnessDays: number; locale: string; audience: string; } export declare const DEFAULT_RADAR: Radar; /** Qualified, resource-prefixed id — every numeric id the tools return also * carries a `ref` (brf-15, dft-15, nt-15, art-15) so an agent can never * mistake one resource's id for another's. */ declare const REF_PREFIX: { readonly note: "nt"; readonly draft: "dft"; readonly briefing: "brf"; readonly artifact: "art"; }; export type RefType = keyof typeof REF_PREFIX; export declare function refOf(type: RefType, id: number): string; /** * SQLite `datetime('now')` stores UTC timestamps as 'YYYY-MM-DD HH:MM:SS' * while every other resource column is written from JS as ISO 8601 — drafts * read back a different timestamp format than notes/briefings/artifacts. * Normalize to ISO on read so clients can compare timestamps across * resources; already-ISO values pass through untouched. */ export declare function isoize(ts: string): string; export interface HarvestCacheRow { provider: string; items: any[]; error: string | null; fresh: boolean; ageMs: number; } export declare class DatabaseManager { private db; constructor(dbPath: string); get raw(): Database.Database; close(): void; private tableCols; private migrate; /** * One-time migration of the harvest cache: the pre-0.5.0 format stored the * whole run as one row (workspace_id, date, results, fetched_at); the * per-provider format stores each provider (feed URL or provider name) as * its own row so a failed provider can be re-attempted independently. * Old rows are split into per-provider rows preserving their fetched_at. */ private migrateHarvestCache; /** * One-time backfill: stored brief items predating the seen-flag * persistence lack a `seen` field, so brief get would report them all as * unseen. Recompute deterministically — an item is seen when its link was * already surfaced in an earlier brief of the same workspace within the * prior 7 days (the same window brief run uses). Idempotent; rewrites only * rows that still contain legacy items. */ private backfillBriefSeen; /** * One-time backfill of the brief_seen table from existing brief records. * INSERT OR IGNORE keeps the earliest first-seen date, matching the * "first surfaced" semantics of briefLinksSince. Idempotent. */ private backfillSeenLinks; /** * All user-facing resources (notes, drafts, briefings, artifacts) draw ids * from one shared sequence, so an id is never ambiguous across tools. * Run as an IMMEDIATE transaction: the read-modify-write of id_seq must be * atomic even across processes sharing the DB (two clients on one HOME) — * a plain read-then-write would hand both processes the same id and the * second INSERT would die on a primary-key conflict. */ private nextId; private parseVoice; private parseRadar; listWorkspaces(): any[]; getWorkspace(id: string): any; createWorkspace(id: string, name: string, segment: string, voice?: any, radar?: any): any; updateWorkspace(id: string, patch: { name?: string; segment?: string; voice?: any; radar?: any; examples?: string[]; reason?: string; }): any; deleteWorkspace(id: string): void; lastBriefDate(id: string): string | null; listSources(id: string): any[]; addSources(id: string, urls: string[], type: string): { added: number; skipped: number; skippedUrls: { url: string; reason: string; }[]; }; removeSources(id: string, sourceIds: number[]): { removed: number; }; setBriefItems(id: string, date: string, items: any[]): void; setBriefIdeas(id: string, date: string, ideas: any[]): void; /** Radar-generated suggestions from brief run — overwritten on every fresh * run (upsert), never curated: curated ideas go through setBriefIdeas. */ setBriefSuggestions(id: string, date: string, suggestions: any[]): void; getBrief(id: string, date: string): any; listBriefs(id: string, limit?: number, offset?: number): any[]; countBriefs(id: string): number; /** Links the client has already been shown since a date — backed by the * immutable brief_seen table so deleting a brief record never resets the * dedup memory. */ briefLinksSince(id: string, sinceDate: string): Set; briefsBetween(id: string, from: string, to: string): any[]; deleteBrief(id: string, date: string): void; saveDraft(id: string, format: string, content: string, briefDate?: string, ideaIndex?: number): number; getDraft(id: string, draftId: number): any; /** Cross-workspace draft lookup by id — lets callers classify a foreign id. */ getDraftFromAny(selfId: string, draftId: number): any; listDrafts(id: string, filter?: { status?: string; format?: string; from?: string; to?: string; q?: string; limit?: number; }, offset?: number): any[]; countDrafts(id: string, filter?: { status?: string; format?: string; from?: string; to?: string; q?: string; }): number; private draftConditions; updateDraft(id: string, draftId: number, content: string): boolean; setDraftStatus(id: string, draftId: number, status: string, reason?: string): boolean; deleteDraft(id: string, draftId: number): boolean; saveNote(id: string, input: { title: string; body?: string; kind?: string; tags?: string[]; source?: string; }): any; getNote(id: string, noteId: number): any; /** Cross-workspace note lookup by id — lets callers classify a foreign id * ("exists, but in another client") instead of reporting a plain miss. */ getNoteFromAny(selfId: string, noteId: number): any; listNotes(id: string, filter?: { kind?: string; tag?: string; q?: string; limit?: number; includeBody?: boolean; }, offset?: number): any[]; countNotes(id: string, filter?: { kind?: string; tag?: string; q?: string; }): number; private noteConditions; updateNote(id: string, noteId: number, patch: { title?: string; body?: string; kind?: string; tags?: string[]; source?: string; }): any; deleteNote(id: string, noteId: number): boolean; saveBriefing(id: string, input: { title: string; kind: string; sections: any[]; noteIds: number[]; }): any; briefingTitleExists(id: string, title: string): boolean; getBriefing(id: string, briefingId: number): any; /** Cross-workspace briefing lookup by id — lets callers classify a foreign id. */ getBriefingFromAny(selfId: string, briefingId: number): any; listBriefings(id: string, kind?: string, limit?: number, offset?: number): any[]; countBriefings(id: string, kind?: string): number; setBriefingStatus(id: string, briefingId: number, status: string): boolean; deleteBriefing(id: string, briefingId: number): { deleted: boolean; deletedArtifacts: number; }; saveArtifact(id: string, input: { briefingId?: number; title: string; content: string; }): any; getArtifact(id: string, artifactId: number): any; /** Cross-workspace artifact lookup by id — lets callers classify a foreign id. */ getArtifactFromAny(selfId: string, artifactId: number): any; updateArtifact(id: string, artifactId: number, title: string, content: string): any; listArtifacts(id: string, briefingId?: number, limit?: number, offset?: number): any[]; countArtifacts(id: string, briefingId?: number): number; deleteArtifact(id: string, artifactId: number): boolean; /** * Per-provider cache read: one row per provider (feed URL or provider * name), each with its own age, so a stale or failed provider can be * re-attempted independently of the rows still inside their TTL. Returns * null when nothing was cached for the date at all (a run that never * happened), not when rows merely expired — expiry is the `fresh` flag. */ getHarvestCache(id: string, date: string, maxAgeMs?: number): HarvestCacheRow[] | null; /** Full overwrite of a date's cache rows — used by fresh harvests, where * every current provider's row is re-written. */ setHarvestCache(id: string, date: string, results: FetchResult[]): void; /** Rewrite only the given providers' rows — used when a cached run * re-fetches stale or previously-failed providers. Other rows keep their * fetched_at, so serving them from cache never extends their TTL. */ upsertHarvestCache(id: string, date: string, results: FetchResult[]): void; /** Drop cached harvest results — profile or source changes make them stale. */ clearHarvestCache(id: string): void; /** Age out stale cached harvests — one ~300KB row per client-day otherwise * accumulates forever. Runs alongside every fresh harvest. */ pruneHarvestCache(maxAgeMs?: number): void; /** Half-life for the persistent corpus: a client's older harvests keep * half their influence after a month, so genericity tracks the client's * steady-state vocabulary while stale runs fade out. */ private static readonly TERM_HALF_LIFE_MS; /** Weights below this are dropped on read — a term not seen in ~7 * half-lives has effectively left the client's vocabulary. */ private static readonly TERM_FLOOR; /** Rows older than this have decayed below TERM_FLOOR even at df=1 — * safe to sweep on every fold, keeping the table bounded. */ private static readonly TERM_MAX_AGE_MS; /** The client's accumulated corpus stats as of now: every row and the * doc count are time-decayed from their last write, and rows faded below * the floor are dropped. Terms never hold spaces, so a space in the key * unambiguously marks a phrase. Returns undefined when the workspace * never had a qualifying harvest. */ getTermStats(id: string): CorpusStats | undefined; /** Fold a run's corpus into the client's history: prior counts decay by * age before the run's document frequencies are added, and rows older * than the floor horizon are swept. */ accumulateTermStats(id: string, stats: CorpusStats): void; } export {}; //# sourceMappingURL=db.d.ts.map