import { Pool } from 'pg'; /** * On-device vs. cloud resolution for every generative capability * (ADR 2026-08-09c). * * ADR 2026-07-31 gave speech a device tier, a three-way preference and a * *reason* attached to every decision, because a fallback the learner is not * told about makes the preference dishonest. This module is that same model * generalised: recall evaluation, card text, image import and embeddings get * the identical vocabulary, and `voice` keeps its existing resolver by * delegating the shared primitive here. * * Pure functions only. What each tier can serve *right now* is measured by the * surfaces (a Gemini Nano status check, a configured cloud row) and passed in. */ type AiTier = "local" | "cloud"; /** Capabilities a learner can steer independently. */ type AiCapability = "recall" | "text" | "image" | "voice" | "embedding"; declare const AI_CAPABILITIES: readonly AiCapability[]; /** * Same three values speech has used since ADR 2026-07-31, so a learner meets * one vocabulary rather than two. */ type AiTierPreference = "device-only" | "device-first" | "quality-first"; declare const AI_TIER_PREFERENCES: readonly AiTierPreference[]; declare function isAiTierPreference(value: unknown): value is AiTierPreference; /** * Defaults differ per capability because the risk does (ADR 2026-08-09c §2). * * Recall and voice run many times a day, the learner sees the result and can * overrule it, and a weaker judgement costs one card — device-first. Card text * and image import produce content that is authored once and reviewed for * years, where a mistranslated term compounds on every repetition and the * learner cannot spot it later — quality-first, on a good cloud model. * * These are a judgement about the models of 2026. When on-device quality * catches up, this table changes; the mechanism does not. */ declare const DEFAULT_AI_TIER_PREFERENCES: Readonly>; type AiPlatform = "android" | "ios" | "desktop"; /** * Whether a platform has an on-device implementation *at all* — a structural * fact about today's APIs, not a runtime measurement. * * A `false` here means Settings states "not possible on this device" instead * of offering a choice that silently does nothing (ADR 2026-08-09c §4). * * - **Android**: ML Kit GenAI's Prompt API is text in, text out, so it serves * recall and card text. Image *description* is a different feature and does * not extract card structure; there is no on-device embedding API. * - **iOS**: Apple's Foundation Models framework needs A17 Pro / M-series, and * no device in the field-test range qualifies — the slot stays reserved * (ADR 2026-08-08 §6). Platform speech is available regardless. * - **Desktop**: Foundry/Ollama already cover text, image and embeddings, but * only on accelerated hardware — the runtime half of that answer comes from * ADR 2026-08-02's classification, through `availability`. */ declare const DEVICE_TIER_SUPPORT: Readonly>>>; declare function hasDeviceTier(platform: AiPlatform, capability: AiCapability): boolean; /** * Whether Settings should offer the preference control for this capability. * * Without a device tier there is nothing to prefer, and a control that cannot * change the outcome is worse than no control. */ declare function isAiPreferenceConfigurable(platform: AiPlatform, capability: AiCapability): boolean; /** Which tiers can actually serve a capability right now. */ interface AiTierAvailability { local: boolean; cloud: boolean; } /** * Why a capability ended up where it did. Surfaces turn this into copy, so a * learner is never silently switched to a paid third party. */ type AiTierReason = "preferred" | "fell-back-to-cloud" | "fell-back-to-local" | "unavailable-device-only" | "unavailable"; interface AiTierDecision { tier: AiTier | null; reason: AiTierReason; } /** * Resolve one preference against what the two tiers can serve. * * Shared with `resolveVoiceEnginePlan`, which is where these semantics were * first settled: `device-only` never reaches the cloud, and any other * preference that cannot use its first choice reports the fallback rather * than performing it quietly. */ declare function decideAiTier(preference: AiTierPreference, availability: AiTierAvailability): AiTierDecision; /** * Resolve a capability on a platform, folding in whether a device tier exists. * * A platform without one cannot serve `local` however the runtime answers, so * the availability it reports for that tier is ignored rather than trusted: * a stub that answers is not a feature (the lesson from the iPad reporting a * denied microphone for a subsystem that was not there). */ declare function resolveAiCapabilityTier(platform: AiPlatform, capability: AiCapability, preference: AiTierPreference, availability: AiTierAvailability): AiTierDecision; type AiTierPlan = Record; /** Resolve every capability at once, for a Settings screen or a session start. */ declare function resolveAiTierPlan(platform: AiPlatform, preferences: Partial>, availability: Partial>): AiTierPlan; type DatabaseValue = string | number | bigint | null | Uint8Array; interface RunResult { changes: number; lastInsertRowid: number | bigint; } /** * Asynchronous database contract used by the learning kernel. * * Every provider (local SQLite, optional libsql embedded replica, remote * Turso over HTTP) implements this interface; no kernel or public API type * depends on a concrete database package. */ interface Statement { run(...params: unknown[]): Promise; get(...params: unknown[]): Promise; all(...params: unknown[]): Promise; } interface Database { prepare(sql: string): Statement; /** Execute one or more SQL statements without reading results. */ exec(sql: string): Promise; /** * Run a PRAGMA. Local providers support the full pragma surface; the * remote provider only supports read pragmas that have a table-valued * function equivalent (e.g. `table_info(...)`). */ pragma(source: string): Promise; /** * Run `fn` inside a BEGIN IMMEDIATE … COMMIT/ROLLBACK transaction. * Transactions are serialized per connection; nested calls deadlock by * design rather than silently interleaving writes. */ transaction(fn: (db: Database) => Promise): Promise; /** Pull changes from the cloud primary (embedded replicas only). */ sync?(): Promise; close(): Promise; } /** * Learning Progress Analytics (ADR 2026-08-01) * * Activity series over the immutable review log: how many cards a user * reviewed per day/week/month and how much study time those reviews took. * * Aggregation happens in SQL over `idx_review_logs_user (user_id, * reviewed_at)` — no aggregate table, no write path. Stored timestamps stay * UTC; buckets are formed in the learner's local time via SQLite's * 'localtime' modifier, and the `window` bound is cut on the same local * calendar so "last N buckets" means exactly N local periods. */ type ActivityPeriod = "day" | "week" | "month"; /** Default windows per period when no explicit `window` is requested. */ declare const DEFAULT_ACTIVITY_WINDOWS: Record; /** * Upper bound a single rating may contribute to study time (ADR 2026-08-01 * Decision 7). * * Every surface measures "card shown → rating submitted" in wall-clock time, * so a card left open — a locked phone, a backgrounded app resuming its * persisted session, a terminal abandoned mid-prompt — books hours of "study * time" for one card and swamps the statistic. The review log keeps the raw * measurement (it is an immutable audit trail); the interpretation is capped * here, at read time, so the cap also repairs rows written before it existed. * Ten minutes is well past any honest single-card answer, including a slow * cloud evaluation and a spoken answer. */ declare const STUDY_TIME_CAP_MS: number; interface ReviewActivityBucket { /** * Local-time bucket start: * - day: "YYYY-MM-DD" (date(reviewed_at, 'localtime')) * - week: "YYYY-Www" (ISO week-year/week, strftime %G-W%V) * - month: "YYYY-MM" */ bucket: string; /** Number of rating events — one rating equals one card worked. */ reviewedCards: number; /** * Sum of response_time_ms over those ratings, each capped at * `STUDY_TIME_CAP_MS`. NULL (never measured) contributes 0. */ studyTimeMs: number; } interface ReviewActivity { period: ActivityPeriod; /** The effective bucket count the query was bounded to. */ window: number; buckets: ReviewActivityBucket[]; } interface GetReviewActivityOptions { period?: ActivityPeriod; /** * Keep only the `window` most recent buckets, cut on the same local * calendar the buckets use (default: `DEFAULT_ACTIVITY_WINDOWS`). The * current partial week/month counts as one bucket, so a week view with * `window: 12` covers the current ISO week plus the 11 before it. * `window: 0` disables the bound (useful together with `since`). */ window?: number; /** * Optional lower bound as a UTC calendar date "YYYY-MM-DD", compared on * the row's UTC date — format-agnostic because `reviewed_at` is written * as both ISO-8601 and SQLite datetime strings depending on the caller. * A documented escape hatch for explicit ranges and tests; production * surfaces use `window`, which is exact in local time. */ since?: string; } /** * Get the review activity series for a user, bucketed per day/week/month. * * Buckets with no reviews are omitted; a chart can fill gaps itself. Study * time only exists from the release that started logging response times on * every surface (ADR 2026-08-01 Decision 2); older rows contribute counts * but no time. Each rating contributes at most `STUDY_TIME_CAP_MS`. */ declare function getReviewActivity(db: Database, userId: string, options?: GetReviewActivityOptions): Promise; /** * A bucket key taken apart for display. * * The keys are stable and machine-facing (`zam stats --json`, the bridge and * the MCP tool all emit them verbatim); turning one into "Fri, Jul 31" or * "KW 31" is each client's job. Parsing them is not, so the desktop app and * the mobile companion share this instead of each re-deriving the shapes. */ type ParsedActivityBucket = { period: "day"; date: Date; } | { period: "week"; isoYear: number; isoWeek: number; } | { period: "month"; date: Date; }; /** * Parse a bucket key produced by `getReviewActivity`, or return `null` when it * does not match the period's shape — a caller can then fall back to showing * the raw key rather than a wrong date. * * Dates are built in local time, matching how the buckets were formed. */ declare function parseActivityBucket(bucket: string, period: ActivityPeriod): ParsedActivityBucket | null; interface ActivityBucketLabelOptions { /** BCP-47 tag the learner reads in, e.g. "de" or "en". */ locale: string; /** * Week wording, supplied by the caller's translation layer — "KW 31" in * German, "Week 31" in English. `Intl` has no format for ISO week numbers. */ weekLabel: (isoWeek: number) => string; } /** * Render a bucket key as a chart label in the learner's language. * * Shared by the desktop app and the mobile companion so a bar reads the same * on every device. Unparseable keys fall back to the raw key rather than to a * wrong date. The CLI deliberately keeps the raw keys — they are stable and * greppable, which is what a terminal surface wants. */ declare function formatActivityBucketLabel(bucket: string, period: ActivityPeriod, options: ActivityBucketLabelOptions): string; /** * Learning Analytics * * Progress statistics, competence tracking, and session summaries. * Ported from PoC's `stats` command with additions for FSRS and symbiosis modes. */ interface UserStats { userId: string; totalTokens: number; cardsInDeck: number; dueToday: number; blocked: number; mature: number; avgStability: number | null; totalSessions: number; lastSession: string | null; } interface DomainCompetence { domain: string; totalCards: number; matureCards: number; avgStability: number; retentionRate: number; suggestedMode: "shadowing" | "copilot" | "autonomy"; } /** * Get overall learning stats for a user (ported from PoC's `stats` command). */ declare function getUserStats(db: Database, userId: string): Promise; /** * Get competence per domain for a user. * Used to suggest symbiosis mode transitions. */ declare function getDomainCompetence(db: Database, userId: string): Promise; /** * Azure DevOps connector — fetches work items from ADO boards. */ interface ADOConfig { orgUrl: string; project: string; pat: string; } interface WorkItem { id: number; title: string; state: string; type: string; assignedTo: string; } /** Load ADO config from credentials file. Returns null if not configured. */ declare function loadADOConfig(): ADOConfig | null; /** * Fetch active work items assigned to the current user. * Uses WIQL to query, then batch-fetches work item details. */ declare function fetchActiveWorkItems(config: ADOConfig): Promise; /** * Credential secret backends — vault references resolved at process start. * * See ADR 2026-07-30b. A stored secret is either a literal string or a * reference (`{ "$secret": "bw://item/field" }`). Accessors stay synchronous * by reading an in-memory snapshot filled once by `resolveCredentials()`. */ /** Backend-qualified locator, e.g. `"bw://zam-turso/token"`. */ interface SecretRef { $secret: string; } /** On-disk form of a secret field: plain string or vault reference. */ type StoredSecret = string | SecretRef; /** Why resolving a vault reference failed. Each reason needs different guidance. */ type SecretResolutionReason = "not-installed" | "locked" | "not-found" | "backend-error"; declare class SecretResolutionError extends Error { readonly reason: SecretResolutionReason; readonly ref: string; constructor(reason: SecretResolutionReason, ref: string, message: string); } /** * Pluggable vault reader. Deliberately read-only: ZAM never creates or * modifies vault items, and never holds a master password or session token. */ interface SecretBackend { /** Scheme this backend claims, e.g. `"bw"`. */ readonly id: string; /** CLI present and vault reachable — cheap, no secret access. */ isAvailable(): Promise; /** Resolve one locator (the part after `scheme://`). Throws SecretResolutionError. */ resolve(locator: string): Promise; } declare function isSecretRef(value: unknown): value is SecretRef; /** Parse `"bw://item/field"` into scheme + locator. Returns null if malformed. */ declare function parseSecretUri(uri: string): { scheme: string; locator: string; } | null; /** * Bitwarden vault backend — first and only shipped vault (ADR 2026-07-30b). * * Resolves via the learner's `bw` CLI. Master passwords are never stored; * BW_SESSION may be restored from a machine-local 30-day file. A locked or * dead session surfaces as `locked` and clears the stored session. * * Locator form: `/` where field is a standard property * (`password`, `username`, `notes`) or a custom field name under `fields[]`. * `bw get ` only accepts Bitwarden's own object names, so * custom fields require reading the full item JSON. */ type BwRunner = (args: string[]) => Promise<{ stdout: string; stderr: string; }>; declare function createBitwardenBackend(run?: BwRunner): SecretBackend; /** * Scheme → SecretBackend registry (mirrors the provider-registry pattern). */ /** Register (or replace) a secret backend by its scheme id. */ declare function registerSecretBackend(backend: SecretBackend): void; /** Remove a backend. Intended for tests. */ declare function unregisterSecretBackend(id: string): void; /** Clear every registered backend. Intended for tests. */ declare function clearSecretBackends(): void; declare function getSecretBackend(id: string): SecretBackend | undefined; declare function listSecretBackends(): SecretBackend[]; /** * Resolve a full reference URI (`bw://item/field`) through the registered * backend for its scheme. Unknown schemes fail hard — a ref-shaped string * must never be treated as a literal token. */ declare function resolveSecretUri(uri: string): Promise; /** * Secret backends public surface (ADR 2026-07-30b). */ /** Register built-in backends once (idempotent). */ declare function ensureDefaultSecretBackends(): void; /** * Credential store — reads/writes ~/.zam/credentials.json * * Connector secrets (Turso URL/token, ADO PAT, etc.) live here instead of * inside the SQLite database. This ensures credentials survive db deletion, * which is required when migrating from plain SQLite to a libsql embedded * replica (Turso cloud sync). * * Secret fields may be literal strings or vault references * (`{ "$secret": "bw://item/field" }`). `resolveCredentials()` resolves * references once into an in-memory snapshot; synchronous accessors read * from that snapshot (ADR 2026-07-30b). */ interface TursoCredentials { url: string; token: string; /** * Database access mode: "native" uses the legacy libsql driver, "remote" * uses the HTTP provider (no native bindings; required on Windows ARM64). */ mode?: "native" | "remote"; } interface ADOCredentials { org_url: string; project: string; pat: string; } /** Resolved view — every secret field is a plain string. Accessor return type. */ interface Credentials { turso?: Partial; ado?: Partial; /** * API keys for named LLM providers, keyed by the provider's reference name * (the `apiKeyRef` in the `llm.providers` setting). Kept here — not in the * database — so workspace exports / DB snapshots never carry provider keys. */ llmProviders?: Record; } /** On-disk document — secret fields may be literals or vault references. */ interface StoredCredentials { turso?: { url?: string; token?: StoredSecret; mode?: TursoCredentials["mode"]; }; ado?: { org_url?: string; project?: string; pat?: StoredSecret; }; llmProviders?: Record; } /** Drop the in-memory snapshot so the next read re-materializes from disk. */ declare function invalidateCredentialsSnapshot(path?: string): void; /** * Test helper: wipe all snapshots and pre-resolve warning state so tests * do not leak across files. */ declare function resetCredentialsResolutionState(): void; /** * Walk the on-disk document, resolve every vault reference in parallel, and * cache the result for synchronous accessors. Idempotent. Never writes * resolved plaintext back to disk. */ declare function resolveCredentials(path?: string): Promise; /** * Status of every secret field — for `zam credentials check`. Never includes * secret values. */ interface CredentialCheckEntry { field: string; kind: "literal" | "reference" | "missing"; ref?: string; ok: boolean; reason?: string; message?: string; } declare function checkCredentials(path?: string): CredentialCheckEntry[]; /** Load the on-disk document (literals and references). Empty if missing. */ declare function loadStoredCredentials(path?: string): StoredCredentials; /** * Load credentials. After `resolveCredentials()` this returns the resolved * snapshot; otherwise literals only (references omitted). Prefer the * typed accessors for production call sites. */ declare function loadCredentials(path?: string): Credentials; /** Save credentials to ~/.zam/credentials.json. Invalidates any snapshot. */ declare function saveCredentials(creds: StoredCredentials | Credentials, path?: string): void; /** Get complete Turso credentials, or null if incomplete. */ declare function getTursoCredentials(path?: string): TursoCredentials | null; /** Set Turso credentials. `token` may be a literal or a vault reference. */ declare function setTursoCredentials(url: string, token: StoredSecret, path?: string, mode?: TursoCredentials["mode"]): void; /** Clear Turso credentials. */ declare function clearTursoCredentials(path?: string): void; /** Get complete ADO credentials, or null if incomplete. */ declare function getADOCredentials(path?: string): ADOCredentials | null; /** Set ADO credentials. `pat` may be a literal or a vault reference. */ declare function setADOCredentials(orgUrl: string, project: string, pat: StoredSecret, path?: string): void; /** Clear ADO credentials. */ declare function clearADOCredentials(path?: string): void; /** Get a named LLM provider's API key (by `apiKeyRef`), or null if unset. */ declare function getProviderApiKey(name: string, path?: string): string | null; /** Store a named LLM provider's API key. May be a literal or vault reference. */ declare function setProviderApiKey(name: string, apiKey: StoredSecret, path?: string): void; /** Remove a named LLM provider's stored API key. No-op if it was unset. */ declare function clearProviderApiKey(name: string, path?: string): void; /** List the reference names (`apiKeyRef`) that currently have a stored key. */ declare function listProviderApiKeyRefs(path?: string): string[]; /** True when `value` looks like a vault reference URI (scheme://…). */ declare function looksLikeSecretUri(value: string): boolean; /** * True when credentials.json holds at least one vault reference. Desktop and * openDatabase use this to require Bitwarden access before falling back to an * empty local DB. */ declare function credentialsNeedVaultAccess(path?: string): boolean; /** * True when a Turso vault ref is configured but not yet resolved into a usable * token (vault locked / not logged in / resolve failed). */ declare function tursoVaultAccessPending(path?: string): boolean; /** Build a SecretRef from a URI, or throw if the URI is malformed. */ declare function secretRefFromUri(uri: string): SecretRef; /** * - `local`: better-sqlite3 file database (default without cloud credentials) * - `native`: legacy native libsql driver (remote URLs and embedded replicas) * - `remote`: Turso over HTTP, no native bindings (works on Windows ARM64) */ type DatabaseProvider = "local" | "native" | "remote"; interface ConnectionOptions { /** Path to the SQLite database file. Defaults to ~/.zam/zam.db */ dbPath?: string; /** If true, run the schema even when the database already exists. */ initialize?: boolean; /** Turso sync URL for embedded replica mode (e.g. libsql://db-name.turso.io) */ syncUrl?: string; /** Turso auth token for direct remote or embedded replica access */ authToken?: string; /** If false, ignore ~/.zam/credentials.json and force the local/default database. */ useConfiguredCloud?: boolean; /** Explicit provider; overrides ZAM_DB_PROVIDER and the credentials mode. */ provider?: DatabaseProvider; } interface DatabaseTargetInfo { /** User-facing category of database target selected for this connection. */ kind: "local" | "turso-native" | "turso-remote" | "turso-replica"; /** Driver/provider that will be used for the selected target. */ provider: DatabaseProvider; /** Local filesystem path or remote URL selected as the database target. */ location: string; /** Turso primary URL when the selected target is an embedded replica. */ syncUrl?: string; } declare function getDatabaseTargetInfo(options?: ConnectionOptions): DatabaseTargetInfo; /** * Open an existing foreign SQLite file without provisioning or write access. * * This narrow driver boundary lets CLI importers inspect untrusted package * databases while preserving the rule that concrete SQLite drivers never * leak outside `src/kernel/db/`. */ declare function openReadOnlySqliteDatabase(dbPath: string): Promise; /** * Open (or create) the ZAM database. * Uses configured Turso credentials for the default database when present. * Falls back to local SQLite and WAL mode when no cloud credentials exist. * When syncUrl is provided explicitly, enables embedded replica sync with Turso. */ declare function openDatabase(options?: ConnectionOptions): Promise; /** * Open the database with Turso cloud credentials auto-detected. * Credentials live in ~/.zam/credentials.json (NOT in the db), so a fresh * machine only has to collect missing secrets instead of bootstrapping local * state first. */ declare function openDatabaseWithSync(options?: Omit): Promise; /** Get the default database path */ declare function getDefaultDbPath(): string; /** * PostgreSQL provider implementing the kernel's Database interface. * * Translates SQLite syntax defaults (e.g. parameter placeholders `?` -> `$n`, * `datetime('now')` -> `CURRENT_TIMESTAMP`, pragma table_info) so kernel code * and contract tests run transparently against PostgreSQL. */ interface PostgresDatabaseOptions { connectionString?: string; host?: string; port?: number; database?: string; user?: string; password?: string; pool?: Pool; } declare function openPostgresDatabase(options: PostgresDatabaseOptions): Database; /** * Schema provisioning — the one place that turns an empty database into a ZAM * database, expressed purely through the async `Database` contract. * * This module is deliberately **free of Node built-ins**. `connection.ts` owns * file paths, drivers and credentials and therefore imports `node:fs`, which a * WebView cannot load. The mobile companion runs the same kernel inside that * WebView, so from the moment iOS opens its own local database (ADR * 2026-08-08) it needs the schema and the migration chain without dragging the * driver layer along. * * `connection.ts` calls straight into here, so there is exactly one migration * path for every platform — a second copy would drift the day someone adds * M021 on one side only. */ /** * Run incremental schema migrations. Every migration is idempotent — safe to * run on every open, on a fresh file and on a decade-old library alike. */ declare function runMigrations(db: Database): Promise; /** * Bring any database up to the current schema. Safe to call unconditionally: * every statement is `IF NOT EXISTS` or guarded, so this is the whole setup on * an empty database and a no-op on a current one. * * The three-step order is load-bearing. Indexes come **after** the migrations * because some of them cover columns a migration adds: `idx_tokens_title` * needs `tokens.title`, which M010 introduces. Creating tables and indexes in * one pass works on an empty database and fails on one provisioned before that * migration — the case the companion hits when it attaches an older server * database. */ declare function applySchemaAndMigrations(db: Database): Promise; /** * Minimal Hrana v3 over HTTP transport for Turso/libsql servers. * * Implements exactly the subset ZAM needs — execute, sequence, close, and * baton-scoped streams for transactions — over plain `fetch`, so it runs on * every architecture Node.js supports (including Windows ARM64, which has no * native libsql binding). * * Protocol reference: https://github.com/tursodatabase/libsql/blob/main/docs/HRANA_3_SPEC.md */ interface HranaTransportOptions { /** Database URL (libsql://, https:// or http://). */ url: string; /** Turso auth token; omitted for unauthenticated local servers. */ authToken?: string; /** Per-request timeout in milliseconds. */ timeoutMs?: number; /** * Total attempts for requests that failed at the transport level before a * response was received. Stateful stream requests (open batons) are never * retried. */ maxAttempts?: number; } /** * RemoteTursoProvider — implements the async `Database` contract directly * against a Turso/libsql server over HTTP (Hrana v3). * * No native bindings and no extra runtime dependencies, so this provider is * the cloud path for architectures without a native libsql artifact (for * example Windows ARM64). Under ZAM's online-first assumption it is suitable * for interactive sessions: the per-review LLM call dominates latency. */ type RemoteDatabaseOptions = HranaTransportOptions; /** Open a remote Turso database over HTTP. */ declare function openRemoteDatabase(options: RemoteDatabaseOptions): Database; /** * Portable database snapshots — Increment 12, Phase 4. * * A snapshot is portable SQL text: a one-line JSON manifest comment followed by * `INSERT` statements for every data row. It deliberately does NOT copy the * live WAL database file, so a user can move their learning history between * machines through a file-sync folder (Google Drive, OneDrive, iCloud, …) * without risking the corruption that comes from syncing an open SQLite/WAL * file directly. * * The schema is NOT embedded. Importing into a freshly initialized database — * which always runs the current SCHEMA + migrations on open — keeps snapshots * forward compatible across schema changes. Columns are written explicitly so a * later-added column never breaks an older snapshot. */ declare const SNAPSHOT_VERSION = 1; interface SnapshotManifest { format: string; version: number; createdAt: string; /** Row count per table at export time. */ tables: Record; /** SHA-256 of the snapshot body (everything after the manifest line). */ checksum: string; } interface ImportResult { /** Row count per table after the restore. */ tables: Record; total: number; } /** * Serialize the active database to a portable SQL-text snapshot. */ declare function exportSnapshot(db: Database, options?: { createdAt?: string; }): Promise; /** Split a snapshot into its manifest and body; validates the header only. */ declare function parseSnapshot(snapshot: string): { manifest: SnapshotManifest; body: string; }; /** Parse a snapshot and verify its body checksum. Returns the manifest. */ declare function verifySnapshot(snapshot: string): SnapshotManifest; /** * Restore a snapshot into `db`. The database must already carry the current * schema (open it with `initialize: true`). Refuses to overwrite a non-empty * database unless `force` is set, and verifies row counts inside the * transaction so any mismatch rolls the whole restore back. */ declare function importSnapshot(db: Database, snapshot: string, options?: { force?: boolean; }): Promise; /** * Goal file parser — reads markdown files with YAML-style frontmatter. * * Goals are persisted as markdown files in the personal repo. * Each file has simple key: value frontmatter (no nested structures) * and a markdown body with description, tasks, and token references. */ type GoalStatus = "active" | "completed" | "paused" | "abandoned"; interface Goal { slug: string; title: string; status: GoalStatus; parent: string | null; created: string; updated: string; body: string; filePath: string; } interface GoalFrontmatter { title?: string; status?: string; parent?: string; created?: string; updated?: string; } /** * Parse a goal markdown file into a Goal object. * * Expected format: * ``` * --- * title: Learn Rust fundamentals * status: active * parent: become-systems-programmer * created: 2026-03-28 * updated: 2026-03-28 * --- * * ## Description * ... * ``` * * @param content - Raw file content * @param slug - Goal slug (derived from filename by caller) * @param filePath - Absolute path to the file */ declare function parseGoalFile(content: string, slug: string, filePath: string): Goal; /** * Serialize a Goal back to markdown with frontmatter. */ declare function serializeGoal(goal: Goal): string; /** * Extract tasks (checklist items) from goal body. * Returns items like { text: "Complete Rustlings", done: false }. */ declare function extractTasks(body: string): Array<{ text: string; done: boolean; }>; /** * Extract token references from goal body. * Looks for lines like `- token/slug` under a "## Tokens" section. */ declare function extractTokenRefs(body: string): string[]; /** * Goal Engine — manages goal lifecycle via markdown files. * * Goals live as markdown files in a directory (typically the personal repo's * goals/ folder). The engine reads, creates, and updates these files. * It does not depend on the database — goals are git-tracked, not DB-tracked. */ interface GoalSummary { slug: string; title: string; status: GoalStatus; parent: string | null; taskCount: number; tasksDone: number; tokenCount: number; } interface CreateGoalInput { slug: string; title: string; status?: GoalStatus; parent?: string; description?: string; } /** * List all goals in the goals directory. * Returns summaries sorted by status (active first) then title. */ declare function listGoals(goalsDir: string): GoalSummary[]; /** * Get a single goal by slug (filename without .md). * Returns undefined if the file doesn't exist. */ declare function getGoal(goalsDir: string, slug: string): Goal | undefined; /** * Create a new goal file. Throws if a goal with this slug already exists. */ declare function createGoal(goalsDir: string, input: CreateGoalInput): Goal; /** * Update a goal's status. Writes the updated file back to disk. */ declare function updateGoalStatus(goalsDir: string, slug: string, status: GoalStatus): Goal; /** * Get the goal tree — goals organized by parent relationships. * Returns root goals (no parent) with nested children. */ declare function getGoalTree(goalsDir: string): Array; /** Content-addressed, presentation-safe media attached to learning tokens. */ type TokenMediaSide = "question" | "answer"; type TokenMediaKind = "image" | "audio"; interface ImageOcclusionShape { shape: "rect" | "ellipse"; left: number; top: number; width: number; height: number; } interface TokenMedia { assetHash: string; side: TokenMediaSide; kind: TokenMediaKind; ordinal: number; originalName: string; altText: string | null; mimeType: string; byteSize: number; data: Uint8Array; occlusions: ImageOcclusionShape[]; } /** Load media bytes only for the card currently being presented. */ declare function getTokenMedia(db: Database, tokenId: string, side?: TokenMediaSide): Promise; /** * Deterministic, model-free text-card import (ADR 2026-08-09). * * File and archive parsing live in the CLI layer. The kernel receives plain, * sanitized card candidates, classifies a preview against stable external * bindings, and commits the exact preview in one transaction. No LLM, HTTP, * filesystem, or concrete database driver belongs here. */ type TextImportFormat = "apkg" | "csv" | "tsv"; type TextImportAction = "create" | "update" | "skip" | "conflict"; interface TextImportNotice { code: string; message: string; externalId?: string; deckPath?: string; } interface TextImportCardInput { /** Globally stable for Anki; source-scoped for delimited files. */ externalId: string; question: string; answer: string; title?: string | null; deckPath?: string; tags?: string[]; source?: string | null; author?: string | null; license?: string | null; noteGuid?: string | null; cardOrdinal?: number | null; media?: TextImportMediaReference[]; warnings?: TextImportNotice[]; } interface TextImportAssetInput { name: string; mimeType: string; kind: TokenMediaKind; data: Uint8Array; } interface TextImportMediaReference { assetName: string; side: TokenMediaSide; kind: TokenMediaKind; altText?: string | null; occlusions?: ImageOcclusionShape[]; } interface TextImportDocument { format: TextImportFormat; /** Display-only basename. Absolute local paths never enter shared storage. */ sourceName: string; cards: TextImportCardInput[]; assets?: TextImportAssetInput[]; warnings?: TextImportNotice[]; unsupported?: TextImportNotice[]; } interface TextImportPreviewCard { externalId: string; question: string; answer: string; deckPath: string; action: TextImportAction; reason: string; tokenId: string | null; cardAction: "create" | "keep" | "none"; contentChanged: boolean; mediaCount: number; warnings: TextImportNotice[]; } interface TextImportDeckPreview { path: string; cards: number; } interface TextImportCounts { create: number; update: number; skip: number; conflict: number; unsupported: number; cardsToCreate: number; valid: number; total: number; } interface TextImportPreview { format: TextImportFormat; sourceName: string; planHash: string; counts: TextImportCounts; decks: TextImportDeckPreview[]; media: { assets: number; references: number; totalBytes: number; }; cards: TextImportPreviewCard[]; warnings: TextImportNotice[]; unsupported: TextImportNotice[]; } interface TextImportCommitResult { planHash: string; counts: TextImportCounts; cardsCreated: number; } /** Emitted after each committed card so a surface can show real progress. */ interface TextImportProgress { done: number; total: number; externalId: string; } interface TextImportCommitOptions { /** * Called synchronously inside the write transaction. A large import against * a remote library is minutes of silence otherwise — the learner reads a * still spinner as a hang (field report, 2026-08-09, 440 cards over Turso). */ onProgress?: (progress: TextImportProgress) => void; } /** Classify a deterministic import preview without mutating the library. */ declare function previewTextImport(db: Database, userId: string, document: TextImportDocument): Promise; /** * Recompute and atomically commit a previously shown preview. * * A changed file or library state changes the plan hash and aborts before the * first write, preventing a stale confirmation from importing unseen data. */ declare function commitTextImport(db: Database, userId: string, document: TextImportDocument, expectedPlanHash: string, options?: TextImportCommitOptions): Promise; /** * Bonus candidates: atoms outside the learner's cell that sit at the edge of * what they can already do. * * ADR 2026-08-14 Decision 6 offers such atoms; it never schedules them. This * module answers only *which* atoms are offerable and in what order. Nothing * here writes: no card, no FSRS field, no persisted score. * * The three definitions the Codex hardening review (R2) asked for are the three * exported functions, and they are deliberately **derived** rather than stored. * A persisted "mastery" value beside the card is the second source of truth the * whole design has refused twice. */ interface BonusCandidate { atomId: string; title: string; /** Atoms that become eligible the moment this one is held. Learner-relative. */ unlockCount: number; /** Hard-edge descendants in the whole graph. Static, and see the caveats. */ reachabilityCount: number; /** The atoms already held that make this one offerable — the "because". */ restsOn: string[]; /** Titles for `restsOn`, same order, for the learner-facing sentence. */ restsOnTitles: string[]; } interface BonusOptions { /** * Atoms the learner's own curriculum already covers. They are not bonuses, * whether or not they are held. * * Passed in rather than derived: "which overlay am I following" is a personal * enrolment, and that object does not exist yet (ADR 2026-08-14b). */ inScopeAtomIds: string[]; /** Cap on returned candidates. Ranking is defined; the cut is the caller's. */ limit?: number; } /** * Does this learner hold this atom? * * **One observed retrieval of the atom's representative item, and the card is * not blocked.** Deliberately the same predicate `unblockReady` uses for * "prerequisite satisfied", so the system does not carry two different meanings * of "you have this". * * Consequences worth stating, because each was a choice: * * - A card buried by precondition self-assessment has `reps = 0` and is * therefore **not** held. The bonus surface never rides on an assumption — * only on observed retrieval. * - There is **no retrievability threshold**. A card whose stability has decayed * still counts. Adding a threshold would create a second, competing notion of * mastery next to FSRS, and the card comes due on its own anyway. The * falsification is measurable: bonus atoms accepted on the back of a decayed * foundation should fail at a noticeably higher rate. * - Evidence comes from the atom's **representative** item, not from all of * them. Requiring every item would make an atom with a Tier 2 essay harder to * hold than one with a single Tier 1 tap — punishing richer curation. The * representative is currently the lowest stored item id, which is * deterministic but not a didactic statement (ADR 2026-08-14b, question 4). */ declare function heldAtomIds(db: Database, userId: string): Promise>; /** * Atoms outside the learner's curriculum whose hard prerequisites they hold. * * Ranked by {@link unlockCounts}, then static reachability, then atom id so the * order is stable. * * **Only hard edges gate.** Alternative prerequisite sets ("A or B suffices") * cannot be expressed by the current AND-only graph (ADR 2026-08-14b, * question 3 / Codex 6.2). For a bonus that is the safe direction: an atom * genuinely reachable by an unmodelled second route is simply not offered. * Under-offering costs an option the learner never sees; over-offering costs a * dead end they accepted. Those are not symmetric. */ declare function bonusCandidates(db: Database, userId: string, options: BonusOptions): Promise; interface EnrolBonusResult { success: boolean; atomId: string; cardsCreated: number; cardIds: string[]; } /** * Enrol a learner in a bonus atom by creating cards for its practice items. * * Eligibility is re-checked here rather than trusted from the caller. The one * safety property the bonus surface has is that an offer stands on foundations * the learner demonstrably holds; a stale list, a replayed request or a UI bug * would otherwise drop them into an atom they cannot do, which is the dead end * {@link bonusCandidates} exists to avoid. Only hard edges gate — the same rule * as the derivation. */ declare function enrolBonusAtom(db: Database, userId: string, atomId: string): Promise; /** * Install a published Knowledge Vector Tile, and materialise cards separately. * * SPIKE — not a production release contract. What is missing is deliberate and * tracked in the Codex follow-up review: no release manifest, no digests, no * signature or publisher trust, no declarative removal of statements a newer * release dropped, and no cross-tile references (every prerequisite atom must * ship in the same tile). Do not build learner features on this until the * release/provenance contract exists. * * What this module does guarantee (ADR 2026-08-14, arbitration 2026-08-14): * * - **Installing content enrols nobody.** `installKvtTile` writes atoms, * alignments, bindings, edges and practice items. It creates zero cards. * `materialiseKvtCards` is the separate, explicit step. * - **Content updates go through the revision contract.** A changed question or * answer on an existing item runs `publishTokenRevisionInTransaction`, so * `content_version` moves and learners of the old wording are re-tested. FSRS * state is never rewritten. * - **Order does not matter — for compatible releases.** The legacy * `provider`/`topic_id` projection and the derived token edges are computed * and *reconciled* from the full stored state, not from the position of an * entry in the tile being installed, so a later release that changes which * item represents an atom does not leave the previous edge behind. * * The limit is explicit: releases that make **contradictory scalar claims** * about the same object — a different atom title, reduction, alignment type * or edge rationale for the same id — are still last-writer-wins, and their * result therefore does depend on install order. Resolving that needs the * release contract (per-row provenance and ownership), not another rule here. * Only a differing *slug* for an already-installed item is rejected outright, * because a published address must not change silently. * - **Item succession is declared, never guessed.** Cards and review logs hang * on the practice-item id, so a successor that arrives under a fresh id would * orphan a learner's history. The publisher says so with `replaces`, and only * then is the history moved (ADR 2026-08-14 Decision 9). Nothing here infers * succession from wording: an earlier version of this module refused a * republished question text, which was both too loose — any rewording slipped * past it — and too tight, because a Tier 1 fast check and a Tier 2 item may * legitimately ask the same thing. */ /** * A published atom id is a ULID (AGENTS.md), never a semantic string. * * The earlier `atom:zam::` form put a subject partition into * the primary key, so renaming a partition would have been an identity * migration across every published tile — the pattern ADR 2026-07-04 already * rejected for tokens. Namespace and slug are now mutable attributes; the * published identity is the opaque `atom_uri` (ADR 2026-08-14, Decision 8). */ declare const ATOM_ID_PATTERN: RegExp; interface KvtAlignment { target_uri: string; target_label?: string; alignment_type: string; provenance?: string; } interface KvtCurriculumBinding { provider: string; school_type?: string; grade?: number; track?: string; subject?: string; topic_code: string; topic_title?: string; exam_relevant?: boolean; } interface KvtAtomPrerequisite { atom_id: string; type: "hard" | "soft"; rationale?: string; } interface KvtPracticeItem { id: string; /** Published address. Immutable once installed; derived when absent. */ slug?: string; /** Language the item is asked in. Substance, persisted. */ language?: string; bloom_level: number; /** 'tier1_fast' | 'tier2_synthesis'. Substance, persisted. */ tier?: string; /** Structured fast-check payload. Substance, persisted verbatim as JSON. */ fast_check?: unknown; question: string; concept: string; /** * How a changed question/answer affects people who already learned it. * Absent means `material` — an unannotated content change must never pass * silently under a learner's existing stability. */ materiality?: "cosmetic" | "material"; /** * Item ids this one supersedes — the publisher's explicit statement that the * learning state of the old item belongs to this one (ADR 2026-08-14 * Decision 9). * * This is the *only* thing that moves a card and its review history to a new * id. Similarity of question, slug or embedding may propose a mapping to a * human; none may decide one. Declaring a replacement is therefore an * editorial act, recorded with the tile that made it. * * Use it for the "same practice item under a new id" case only. A split, a * merge or a genuinely uncertain match must not be declared here: those * preserve the old history but transfer no mastery, so the new item is asked * for real. */ replaces?: string[]; } interface KvtAtom { /** ULID. Opaque on purpose — see {@link ATOM_ID_PATTERN}. */ id: string; /** Published identity; defaults to `urn:zam:atom:` for ZAM-minted atoms. */ atom_uri?: string; /** Readable address. Mutable: renaming these breaks no reference. */ namespace?: string; slug?: string; title: string; domain?: string; reduction?: string; typical_age_min?: number; prerequisites?: KvtAtomPrerequisite[]; alignments?: KvtAlignment[]; curricula?: KvtCurriculumBinding[]; practice_items: KvtPracticeItem[]; } interface KvtTile { tile_id: string; version: string; title?: string; publisher?: string; atoms: KvtAtom[]; } interface InstallKvtResult { tileId: string; version: string; atomsUpserted: number; tokensCreated: number; /** Existing items whose substance changed and were published as a revision. */ tokensRevised: number; /** Existing items that were byte-identical in substance. */ tokensUnchanged: number; bindings: number; alignments: number; atomPrereqs: number; tokenPrereqs: number; /** Superseded items whose card and review history moved to a successor. */ itemsSuperseded: number; } interface MaterialiseKvtResult { cardsCreated: number; cardsReused: number; } /** * Install every atom and practice item in `tile`. Creates no cards. * * Idempotent: installing the same tile twice changes no row, no version and no * due date. */ declare function installKvtTile(db: Database, tileInput: unknown): Promise; /** * Give `userId` cards for the practice items of `atomIds`. * * The deliberate second step: installing a release must not enrol anyone, so * a Realschule learner does not receive the optional BOS formula item that * ships in the same tile as their own atoms. */ declare function materialiseKvtCards(db: Database, userId: string, atomIds: string[]): Promise; interface BundledTile extends KvtTile { description?: string; published_at?: string; sources?: Array<{ uri: string; label?: string; checked?: string; }>; } interface CurriculumScope { provider: string; schoolType?: string; grade?: number; track?: string; subject?: string; } interface BundledCellInfo { id: string; title: string; gradeLabel: string; description: string; publisher: string; publishedAt: string; atomCount: number; inScopeAtomIds: string[]; /** Curriculum positions this cell covers, used by every discovery surface. */ curriculumScopes: CurriculumScope[]; } interface BundledCellStatus extends BundledCellInfo { installed: boolean; enrolled: boolean; cardCount: number; } interface BundledCellEnrolResult { success: boolean; cellId: string; installed: boolean; cardsCreated: number; cardsReused: number; alreadyEnrolled: boolean; } declare const BUNDLED_TILES: Record; declare const BUNDLED_CELLS: BundledCellInfo[]; declare function listBundledCells(): (BundledCellInfo & { tile_id: string; version: string; atoms: BundledTile["atoms"]; })[]; declare function getBundledCell(cellId: string): BundledCellInfo | undefined; /** * Cells that cover a curriculum position, best match first. * * **The cell has precedence** (owner decision 2026-08-15). Import goes through * a cell whenever one exists for the learner's position; the generic curriculum * importer is the fallback for positions no cell covers yet. */ declare function findBundledCellsForScope(scope: CurriculumScope): BundledCellInfo[]; /** * Whether the generic curriculum importer is still the right tool here. * * `false` means a cell covers this position and should be offered instead. */ declare function needsGenericCurriculumImport(scope: CurriculumScope): boolean; /** Get the raw KVT tile definition for a bundled cell. */ declare function getBundledCellTile(cellId: string): BundledTile | undefined; /** Check if every atom and practice item of a bundled cell is installed. */ declare function isBundledCellInstalled(db: Database, cellId: string): Promise; /** Check if a learner has enrolled (holds cards) in a bundled cell. */ declare function getBundledCellEnrolment(db: Database, userId: string, cellId: string): Promise<{ installed: boolean; enrolled: boolean; cardCount: number; }>; /** * Retrieve bundled cells with live status in a handful of bulk queries. * * Mobile used to make two or three Tauri IPC round trips for every cell. That * was tolerable for the four pilot cells and unusable for the 228-cell library. * Keeping the aggregation in the kernel gives Desktop, MCP and both mobile * platforms the same scalable answer. */ declare function getBundledCellsWithStatus(db: Database, userId: string, requestedCells?: readonly BundledCellInfo[]): Promise; /** * Enrol a learner in a bundled cell. */ declare function enrolBundledCell(db: Database, userId: string, cellId: string): Promise; /** * Precondition Self-Assessment (Entry Problem). * * Implements the voluntary self-assessment flow for foundational prerequisites * of a learning cell (ADR 2026-08-14, arbitration 2026-08-14): * * - When a learner states they already know a foundational prerequisite ("Kann ich schon"): * Every live card for that atom is buried until a finite date * ({@link preconditionBuriedUntil}) with `buried_reason = 'precondition'`. FSRS * fields are never modified, so `heldAtomIds` still refuses the atom until a * real retrieval — and the claim is checked once the deferral runs out. * * - When a learner chooses to learn the prerequisite ("Bitte mitlernen"): * Any precondition bury on those cards is lifted, so they can enter the queue. * * A card that exists after enrolment is not a decision. `unassessed` is `reps = 0` * and no precondition bury — enrolment must not look like "chose to learn". */ declare const PRECONDITION_BURIED_REASON = "precondition"; /** * The learner explicitly pulled a deferred precondition into recall. * * The marker keeps that intent across a reload until the real review clears * all bury metadata. Without it the now-unburied, still-new card is * indistinguishable from an unassessed prerequisite and every surface asks the * same entry question again instead of showing the requested recall. */ declare const PRECONDITION_READY_REASON = "precondition_ready"; /** * How long a self-assessed precondition waits before it is asked for real. * * **This horizon is the whole mechanism.** The owner's rule is that a card is * deferred, not removed — "even at maximum self-assessment it gets asked * eventually". Without a finite horizon the feature is not a deferral but an * opt-out, and the safety argument for allowing self-assessment at all * disappears: what makes an unverified claim acceptable is that it is verified * later, cheaply, once it no longer blocks anything. * * Three weeks is a pilot value, not a finding. It is long enough that four * declined preconditions do not delay today's work, short enough that a wrong * claim surfaces inside one field-test cycle rather than after it. Change it * when learner feedback says so — that is what it is here for. */ declare const PRECONDITION_HORIZON_DAYS = 21; /** * Extra days per precondition the learner already deferred. * * Burying every declined precondition to the same day only moves the pile-up * the feature exists to prevent: four preconditions declined on Monday would * all come back on the same Monday three weeks later. Each further deferral * lands a few days after the previous one, so they return as a trickle. */ declare const PRECONDITION_STAGGER_DAYS = 4; /** * When a newly declined precondition should come back, given how many the * learner has already deferred. Deterministic — no randomness in scheduling. */ declare function preconditionBuriedUntil(existingDeferrals: number, now?: Date): string; interface PreconditionCandidate { atomId: string; title: string; slug: string; description?: string; assessmentState: "unassessed" | "buried_known" | "ready" | "learning"; cardId?: string; tokenId?: string; buriedUntil?: string | null; buriedReason?: string | null; reps: number; } interface AssessPreconditionInput { userId: string; atomId: string; decision: "known" | "learn"; } interface AssessPreconditionResult { success: boolean; atomId: string; decision: "known" | "learn"; cardId: string; buried: boolean; /** When the claim gets checked. Never null for a `known` decision. */ buriedUntil: string | null; buriedReason: string | null; } /** * Get all foundational precondition atoms for a cell or set of atoms, * with the learner's current assessment and card status. */ declare function getPreconditionCandidates(db: Database, userId: string, cellId?: string): Promise; /** * Record a learner's self-assessment decision for a foundational precondition atom. */ declare function assessPrecondition(db: Database, input: AssessPreconditionInput): Promise; /** * Lift precondition burying on an atom or card so it can enter the queue. */ declare function liftPreconditionBury(db: Database, userId: string, atomId: string): Promise; /** * Pull Forward on Empty Queue (Entry & Scheduling Problem, Phase 4). * * ADR 2026-08-14, Research Note Section 6.5: * When a learner's due queue is empty, ZAM provides a voluntary "Pull-Forward" * mechanism. The queue is soft: * * 1. New curriculum items the daily limit held back come first — that is what * "keep going" is asking for. * 2. Future-due reviews follow. Answering one early is not a corruption: FSRS * derives elapsed time from `last_review_at`, never from `due_at`, so an * early answer is scored on the real interval it was given. * 3. Preconditions the learner declined come last. They said they already have * those, and the deferral expires on its own date; offering them first * would answer "keep going" with the very thing they set aside. * 4. Selecting a new card grants a session-local admission budget and writes * nothing. Pulling an active precondition deferral clears its date and * leaves a `precondition_ready` intent marker until retrieval; pulling a * future review sets `due_at = now`. */ interface PullForwardCandidate { cardId: string; tokenId: string; tokenSlug: string; tokenTitle: string; atomId: string | null; atomTitle: string | null; reason: "precondition_buried" | "future_due" | "new_in_scope"; dueAt: string; buriedUntil: string | null; buriedReason: string | null; state: string; reps: number; priorityScore: number; } interface PullForwardOptions { limit?: number; includeFutureDue?: boolean; } interface PullForwardResult { pulledCount: number; cardIds: string[]; } /** * Get prioritized candidates that can be pulled forward into the review queue. */ declare function getPullForwardCandidates(db: Database, userId: string, options?: PullForwardOptions): Promise; /** * Execute pull-forward on selected cards for a learner. * * New cards need no scheduling mutation; the caller carries their count into * the next queue snapshot as `maxNew`. Active precondition cards are unburied * with a durable ready marker, and future non-new reviews are made due now. */ declare function pullForwardCards(db: Database, userId: string, cardIds: string[]): Promise; /** * Publishing a revision of curated learning content * (ADR 2026-07-04 Decision 3, "Closed-Group Learning Library"). * * The point of a curated library is that a correction reaches the people who * learned the broken version. Without that, a curator fixes a wrong card and * everyone who already memorised the wrong answer stays confidently wrong on a * comfortable review interval. * * So every publish is classified, and ZAM never guesses: * * - **cosmetic** — typo, clearer wording, formatting. Learners keep their FSRS * state untouched and the card simply updates. * - **material** — the answer changed, the scope changed, it was wrong. The * token's `content_version` is bumped and every card that learned an older * version becomes **due now**. * * A material change deliberately does *not* reset stability. It **re-tests**: * the learner answers, and their rating recalibrates FSRS from real evidence. * A hard reset would throw away history the scheduler could use and punish * people who already knew the correction; a "soft reset" would need a * stability penalty nobody can justify. FSRS already distinguishes "still knew * it" from "did not" — so let it. * * Kernel-only: no LLM, no HTTP, no notion of who a curator is. Deciding *that* * a change is material is the caller's job (a curator in the Studio); applying * the consequence is this module's. */ /** How a publish affects people who already learned the token. */ type RevisionMateriality = "cosmetic" | "material"; /** Token fields a revision may change. Omitted fields are left alone. */ interface RevisionChanges { title?: string; question?: string; concept?: string; context?: string; domain?: string; bloomLevel?: number; sourceLink?: string | null; /** PracticeItem substance (ADR 2026-08-14): language, tier, fast check. */ language?: string | null; tier?: string | null; fastCheck?: string | null; } interface PublishRevisionInput { tokenId: string; /** Never defaulted — the caller must decide (ADR Decision 3). */ materiality: RevisionMateriality; changes?: RevisionChanges; /** Optional author/curator who published this revision. */ publishedBy?: string; } interface PublishRevisionResult { tokenId: string; materiality: RevisionMateriality; /** The token's content version after publishing. */ contentVersion: number; /** Cards set due by this publish; 0 for a cosmetic change. */ cardsRetested: number; publishedBy?: string | null; publishedAt?: string | null; } /** * Publish a revision of a token and apply its consequence for learners. * * Runs in one transaction: a material bump and the cards it re-tests must not * be observable apart, or a learner could be handed the new wording while * still counted as having learned the old version. */ declare function publishTokenRevision(db: Database, input: PublishRevisionInput): Promise; /** * Apply a revision when the caller already owns the surrounding transaction. * * The public import pipeline needs this form so a source binding, token * revision, and personal card are committed or rolled back together. Callers * that are not already inside `db.transaction()` must use * `publishTokenRevision()` instead. */ declare function publishTokenRevisionInTransaction(db: Database, input: PublishRevisionInput): Promise; /** * True when this card's owner has not yet been re-tested since a material * change. Callers use it to explain *why* a card came back — an unexplained * reappearance is the reset feeling like a bug. */ declare function isAwaitingRetest(db: Database, cardId: string): Promise; interface RevisionImpact { tokenId: string; currentContentVersion: number; totalCards: number; affectedLearners: number; } /** * Calculate the release impact of publishing a revision for a token. * Shows how many existing cards/learners will be affected if a material * change is published. */ declare function getRevisionImpact(db: Database, tokenId: string): Promise; /** * Agent skills: task recipes the agent learns from user guidance. * * When the agent cannot execute a step, it admits it, asks for guidance, * and saves the successful approach here. Skills are linked to tokens so * FSRS decay naturally resurfaces them for review — automation ≠ retention. */ type SkillSource = "learned" | "builtin"; interface AgentSkill { id: string; slug: string; description: string; steps: string[]; token_slugs: string[]; source: SkillSource; created_at: string; updated_at: string; } interface CreateAgentSkillInput { slug: string; description: string; steps: string[]; token_slugs?: string[]; source?: SkillSource; } declare function createAgentSkill(db: Database, input: CreateAgentSkillInput): Promise; declare function getAgentSkill(db: Database, slug: string): Promise; declare function listAgentSkills(db: Database): Promise; /** * Assignment repository — typed wrappers around the assignments table. * * ADR 2026-07-04 Decision 10: * An assignment binds while it stands (learner cannot detach the card). * When withdrawn or completed, the card and its full FSRS history stay * with the learner to keep, detach, or delete. */ interface Assignment { id: string; token_id: string; assigner_id: string; assignee_id: string; due_date: string | null; created_at: string; withdrawn_at: string | null; } interface CreateAssignmentInput { tokenId: string; assignerId: string; assigneeId: string; dueDate?: string | null; } /** * Create a new assignment for a token to a learner. * Automatically ensures a card exists for the learner and binds it. */ declare function createAssignment(db: Database, input: CreateAssignmentInput): Promise; /** * Withdraw an assignment. * Once withdrawn, the card and full learning history remain with the learner, * but the card is no longer bound (can be detached or deleted by the learner). */ declare function withdrawAssignment(db: Database, assignmentId: string, assignerId?: string): Promise; /** * Get an assignment by ID. */ declare function getAssignment(db: Database, id: string): Promise; /** * List all assignments assigned to a specific learner. */ declare function listAssignmentsForLearner(db: Database, assigneeId: string): Promise; /** * List all assignments created by a specific assigner. */ declare function listAssignmentsByAssigner(db: Database, assignerId: string): Promise; /** * Card repository — typed wrappers around the cards table. * * Each card tracks one user's scheduling state for one token, * using FSRS fields (stability, difficulty, elapsed_days, etc.). */ type CardState$1 = "new" | "learning" | "review" | "relearning"; interface Card { id: string; token_id: string; user_id: string; stability: number; difficulty: number; elapsed_days: number; scheduled_days: number; reps: number; lapses: number; state: CardState$1; learning_step: number | null; buried_until: string | null; buried_reason: string | null; due_at: string; last_review_at: string | null; blocked: number; assigned_by?: string | null; assignment_id?: string | null; /** "Not for me": declined by the learner; kept, but not scheduled. */ detached_at?: string | null; } interface UpdateCardInput { stability?: number; difficulty?: number; elapsed_days?: number; scheduled_days?: number; reps?: number; lapses?: number; state?: CardState$1; learning_step?: number | null; buried_until?: string | null; buried_reason?: string | null; due_at?: string; last_review_at?: string | null; blocked?: number; } interface CardDeletionImpact { review_logs: number; } interface DeleteCardResult { card: Card; impact: CardDeletionImpact; } /** A due card joined with its token details. */ interface DueCard extends Card { slug: string; concept: string; domain: string; bloom_level: number; } /** A blocked card joined with its token details. */ interface BlockedCard extends Card { slug: string; concept: string; domain: string; bloom_level: number; } declare function ensureCard(db: Database, tokenId: string, userId: string): Promise; /** * Get a card by token+user. Returns undefined if no card exists. */ declare function getCard(db: Database, tokenId: string, userId: string): Promise; /** * Get a card by its ULID. */ declare function getCardById(db: Database, cardId: string): Promise; /** * Update a card's scheduling fields. * * Only the fields present in `updates` are changed. Throws if the card * does not exist. */ declare function updateCard(db: Database, cardId: string, updates: UpdateCardInput): Promise; /** * Preview the review-log rows that will be removed when deleting a user's card. */ /** * Reset the learning state of every user's card for a token back to the * beginning (ADR 2026-07-18): when a concept changed on re-import, the old * knowledge is irrelevant and must be learned fresh. Values mirror a * brand-new card's schema defaults. `blocked` is left untouched — it is * derived from prerequisites, not from learning progress. * * Returns the number of cards reset. */ declare function resetCardsForToken(db: Database, tokenId: string, now?: string): Promise; declare function getCardDeletionImpact(db: Database, tokenId: string, userId: string): Promise; /** * "Not for me" — decline a card without destroying anything * (ADR 2026-07-04 Decision 10). * * Detaching stops scheduling but keeps the card row and every review log * attached to it. That is the whole difference from {@link deleteCardForUser}: * a learner who decides a piece of shared content is not for them should not * have to erase the work they already did on it to say so, and should be able * to change their mind ({@link reattachCardForUser}). * * Idempotent; refused while an assignment still binds the card. */ declare function detachCardForUser(db: Database, tokenId: string, userId: string): Promise; /** * Undo a detach. Scheduling state is untouched throughout, so a card picked * back up resumes where it left off rather than starting over. Idempotent. */ declare function reattachCardForUser(db: Database, tokenId: string, userId: string): Promise; /** * Delete one user's card for a token. Review logs cascade via FK. */ declare function deleteCardForUser(db: Database, tokenId: string, userId: string): Promise; /** * Get all cards that are due for review. * * A card is due when it is not blocked/buried and due_at <= now. * Results are ordered by bloom_level ascending (fundamentals first), * then by due_at ascending (oldest first). * * Ported from the PoC's due-tokens command. * * When `domain` or `knowledgeContext` is set, only matching due cards are * returned. */ declare function getDueCards(db: Database, userId: string, now?: string, domain?: string, knowledgeContext?: string): Promise; /** * Get all blocked cards for a user. * * Returns cards joined with their token details so the caller can * see what is waiting and why. */ declare function getBlockedCards(db: Database, userId: string): Promise; /** * Knowledge Context repository — typed wrappers around the contexts and token_contexts tables. * * A context represents a first-class facet (e.g. work, school, private) that can have * attributes like default generation language. */ interface KnowledgeContext { id: string; name: string; label: string | null; language: string | null; created_at: string; } interface CreateKnowledgeContextInput { name: string; label?: string | null; language?: string | null; } interface UpdateKnowledgeContextInput { name?: string; label?: string | null; language?: string | null; } /** * Create a new knowledge context. */ declare function createKnowledgeContext(db: Database, input: CreateKnowledgeContextInput): Promise; /** * Retrieve a knowledge context by its unique name. */ declare function getKnowledgeContextByName(db: Database, name: string): Promise; /** * Retrieve a knowledge context by its ULID. */ declare function getKnowledgeContextById(db: Database, id: string): Promise; /** * List all knowledge contexts ordered by creation date (oldest first) or name. */ declare function listKnowledgeContexts(db: Database): Promise; /** * Update mutable fields on a knowledge context. */ declare function updateKnowledgeContext(db: Database, id: string, updates: UpdateKnowledgeContextInput): Promise; /** * Delete a knowledge context. Cascades deletion of associated token mappings. */ declare function deleteKnowledgeContext(db: Database, id: string): Promise; /** * Assign a token to a knowledge context. * Safe to call repeatedly (noop if already assigned). */ declare function assignTokenToContext(db: Database, tokenId: string, contextId: string): Promise; /** * Remove a token from a knowledge context. */ declare function unassignTokenFromContext(db: Database, tokenId: string, contextId: string): Promise; /** * List all knowledge contexts assigned to a given token. */ declare function listContextsForToken(db: Database, tokenId: string): Promise; /** * Start personas (ADR 2026-07-24 §2) — the only branching variable in * first-run onboarding. The same product serves different learning economies, * and what differs per persona is the *default import path*, not the * scheduler or the UI. Kept as a descriptor list, not a switch: adding a * fifth persona is a new row (plus its i18n copy), never wizard control-flow. * * A persona selects defaults, it locks nothing — every import path stays * reachable for every persona. Its only lasting data-model side effect is * seeding a matching knowledge context (ADR 2026-07-04) if absent. */ type PersonaId = "school" | "study" | "work" | "private"; /** Default content path on onboarding page 6 (wired in plan Phase 8). */ type PersonaImportPath = "curriculum" | "free-import" | "okf-import" | "goal-import"; interface PersonaDescriptor { id: PersonaId; /** Desktop i18n key for the persona card label. */ labelKey: string; /** Desktop i18n key for the card's one-line "why this matters". */ descriptionKey: string; /** Desktop i18n key for the seeded knowledge context's human label. */ contextLabelKey: string; /** `contexts.name` row seeded on selection (ADR 2026-07-04). */ knowledgeContextSlug: string; defaultImportPath: PersonaImportPath; } declare const PERSONA_DESCRIPTORS: readonly PersonaDescriptor[]; /** ADR open question 4, resolved in the plan: skipping yields "free learner". */ declare const DEFAULT_PERSONA_ID: PersonaId; declare function isPersonaId(value: string): value is PersonaId; declare function getPersonaDescriptor(id: PersonaId): PersonaDescriptor; interface PersonaContextSeedResult { context: KnowledgeContext; created: boolean; } /** * Seed the persona's knowledge context if absent. Idempotent by name: an * existing context with the persona's slug is returned untouched (its label * may have been edited by the user and must not be overwritten), so re-running * onboarding or clicking through personas never duplicates or resets contexts. */ declare function seedPersonaKnowledgeContext(db: Database, personaId: PersonaId, contextLabel?: string): Promise; /** * Prerequisite repository — typed wrappers around the prerequisites table. * * Models the dependency graph: "to learn token A, first know token B." * The graph must remain acyclic — cycles are rejected at insert time. */ interface Prerequisite { token_id: string; requires_id: string; } /** A prerequisite row joined with the token it points to. */ interface PrerequisiteWithToken extends Prerequisite { slug: string; title: string; concept: string; domain: string; bloom_level: number; } /** * Collect all prerequisite edges as an adjacency map: child → parent set. * Only used for cycle detection; the full graph is loaded once per * addPrerequisite call (small N in practice). */ declare function buildAncestorMap(db: Database): Promise>>; /** * Returns true if adding edge (tokenId → requiresId) would create a cycle. * Uses BFS from requiresId: if tokenId is reachable, adding the edge closes * a loop. */ declare function wouldCreateCycle(db: Database, tokenId: string, requiresId: string, ancestors?: Map>): Promise; /** * Add a prerequisite edge: tokenId requires requiresId. * * Idempotent — silently ignores duplicate edges. * Throws if either token ID does not exist (FK constraint). * Throws if a token is declared as its own prerequisite. * Throws if the edge would create a cycle in the prerequisite graph. */ declare function addPrerequisite(db: Database, tokenId: string, requiresId: string): Promise; /** * Remove one prerequisite edge. Idempotent when the edge does not exist. * Callers that reconcile several edges should wrap the full change in one * Database transaction so a later validation failure restores the old graph. */ declare function removePrerequisite(db: Database, tokenId: string, requiresId: string): Promise; /** * Get the direct prerequisites of a token — "what does token X require?" * * Returns prerequisite rows joined with the required token's details. */ declare function getPrerequisites(db: Database, tokenId: string): Promise; /** * Get the direct dependents of a token — "what depends on token X?" * * Returns prerequisite rows joined with the dependent token's details. */ declare function getDependents(db: Database, tokenId: string): Promise; /** Token + optional per-user card snapshot, tailored for visual encoding (mastery, blocked state, bloom). */ interface NeighborhoodToken { id: string; slug: string; title: string; concept: string; domain: string; bloom_level: number; card: { state: CardState$1; reps: number; stability: number; difficulty: number; blocked: boolean; due_at: string; last_review_at: string | null; } | null; } /** * The direct neighborhood for a focus-centric 3D view: * - center: the token in focus * - prerequisites: direct "basis" tokens required by the center (foundations, placed "below") * - dependents: direct tokens that require the center (higher-order abilities, placed "above") * * When userId is supplied, every node includes the user's Card state so the viz can * encode personal mastery (e.g. color by stability/reps, highlight blocked or due). */ interface Neighborhood { center: NeighborhoodToken; prerequisites: NeighborhoodToken[]; dependents: NeighborhoodToken[]; } /** * Fetch the direct (depth-1) prerequisite neighborhood around one token. * This is the primary data source for the experimental 3D knowledge graph. */ declare function getTokenNeighborhood(db: Database, tokenId: string, userId?: string): Promise; /** * Review log repository — typed wrappers around the review_logs table. * * The review log is immutable: every rating event is appended, never * updated or deleted. This provides a complete audit trail of a user's * learning history. */ interface ReviewLog { id: string; card_id: string; token_id: string; user_id: string; rating: number; response_time_ms: number | null; reviewed_at: string; scheduled_at: string; session_id: string | null; /** Token `content_version` at answer time; NULL for rows predating M027. */ content_version: number | null; } interface CreateReviewInput { card_id: string; token_id: string; user_id: string; rating: number; scheduled_at: string; response_time_ms?: number | null; session_id?: string | null; } interface ListReviewsOptions { /** Maximum number of reviews to return. */ limit?: number; /** Return reviews after this ISO timestamp. */ after?: string; /** Return reviews before this ISO timestamp. */ before?: string; } /** * Log an immutable review event. * * Validates that the rating is between 1 and 4 (matching the schema CHECK). * Returns the created review log entry. */ declare function logReview(db: Database, input: CreateReviewInput): Promise; /** * Get all reviews for a specific card, ordered by reviewed_at ascending. */ declare function getReviewsForCard(db: Database, cardId: string): Promise; /** * Get reviews for a user, with optional filtering. * * Results are ordered by reviewed_at descending (most recent first). */ declare function getReviewsForUser(db: Database, userId: string, options?: ListReviewsOptions): Promise; /** * Session repository — typed wrappers around sessions and session_steps. * * A session represents a work+learning episode. Steps within a session * record which tokens were touched and by whom (user or agent). */ type ExecutionContext = "shell" | "ui" | "reallife"; interface Session { id: string; user_id: string; task: string; execution_context: ExecutionContext; started_at: string; completed_at: string | null; } interface SessionStep { id: string; session_id: string; token_id: string; done_by: "user" | "agent"; rating: number | null; notes: string | null; created_at: string; } interface CreateSessionInput { user_id: string; task: string; execution_context?: ExecutionContext; } interface LogStepInput { session_id: string; token_id: string; done_by: "user" | "agent"; rating?: number | null; notes?: string | null; } /** A step joined with its token details, returned by getSessionSummary. */ interface StepWithToken extends SessionStep { slug: string; concept: string; domain: string; bloom_level: number; } interface SessionSummary { session: Session; steps: StepWithToken[]; } /** * Start a new session. Returns the created session. * * Ported from the PoC's start-session command. */ declare function startSession(db: Database, input: CreateSessionInput): Promise; /** * End a session by setting its completed_at timestamp. * * Throws if the session does not exist or is already completed. * * Ported from the PoC's end-session command. */ declare function endSession(db: Database, sessionId: string): Promise; /** * Log a step within a session. * * Validates that done_by is 'user' or 'agent' and that the rating * (if provided) is between 1 and 4. * * Ported from the PoC's log-step command. */ declare function logStep(db: Database, input: LogStepInput): Promise; /** * Get a full session summary: the session record plus all steps * joined with their token details. * * Ported from the PoC's session-summary command. * Throws if the session does not exist. */ declare function getSessionSummary(db: Database, sessionId: string): Promise; /** * User settings — key/value store backed by the user_config table. */ interface UserSetting { key: string; value: string; updated_at: string; } /** Get a single setting by key. Returns undefined if not set. */ declare function getSetting(db: Database, key: string): Promise; /** Get all settings as a key-value map. */ declare function getAllSettings(db: Database): Promise>; /** Get all settings with metadata. */ declare function getAllSettingsDetailed(db: Database): Promise; /** Set a setting (insert or update). */ declare function setSetting(db: Database, key: string, value: string): Promise; /** Delete a setting. Returns true if it existed. */ declare function deleteSetting(db: Database, key: string): Promise; /** * Token repository — typed wrappers around the tokens table. * * Tokens are atomic knowledge concepts with Bloom taxonomy levels * and optional symbiosis modes (shadowing / copilot / autonomy). */ type BloomLevel$1 = 1 | 2 | 3 | 4 | 5; type SymbiosisMode = "shadowing" | "copilot" | "autonomy"; type EditorialState = "draft" | "in_review" | "published" | "deprecated"; /** * Who authored a token's current recall question. LLM healing only * overwrites questions whose source is not 'manual'. */ type QuestionSource = "manual" | "llm" | "template"; interface Token { id: string; slug: string; title: string; concept: string; domain: string; bloom_level: BloomLevel$1; context: string; symbiosis_mode: SymbiosisMode | null; source_link: string | null; question: string | null; question_source: QuestionSource; created_at: string; updated_at: string; deprecated_at: string | null; provider: string | null; topic_id: string | null; /** Maintenance state (ADR 2026-07-18): set when the token's source * binding is unclear (stale source_link, ambiguous re-import). Cards of * a token in maintenance leave the review queue; learning state is * preserved. NULL = healthy. */ maintenance_at: string | null; maintenance_reason: string | null; /** Editorial state (ADR 2026-07-04 Phase 3: 'draft' | 'in_review' | 'published' | 'deprecated'). */ editorial_state: EditorialState; /** Published learning atom this practice item realises (ADR 2026-08-14). */ atom_id: string | null; /** Language this item is asked in — PracticeItem substance. */ language: string | null; /** Interaction tier ('tier1_fast' | 'tier2_synthesis') — substance. */ tier: string | null; /** Structured fast-check payload as JSON — substance. */ fast_check: string | null; } interface CreateTokenInput { id?: string; slug: string; title?: string; concept: string; domain?: string; bloom_level?: BloomLevel$1; context?: string; symbiosis_mode?: SymbiosisMode | null; source_link?: string | null; question?: string | null; question_source?: QuestionSource; provider?: string | null; topic_id?: string | null; editorial_state?: EditorialState; atom_id?: string | null; language?: string | null; tier?: string | null; fast_check?: string | null; } interface UpdateTokenInput { title?: string | null; concept?: string; domain?: string; bloom_level?: BloomLevel$1; context?: string; symbiosis_mode?: SymbiosisMode | null; source_link?: string | null; question?: string | null; question_source?: QuestionSource; provider?: string | null; topic_id?: string | null; editorial_state?: EditorialState; } interface ListTokensOptions { domain?: string; editorialState?: EditorialState; /** * Filter by domain prefix using `/` as separator (e.g. "company-team"). * Matches exact or startsWith(prefix + "/"). */ domainPrefix?: string; /** * Filter by knowledge context name (e.g. "work-company"). */ knowledgeContext?: string; /** * Filter by source-link base(s): tokens whose `source_link` is one of the * bases exactly or `#` (the anchored form OKF imports * write). Same matching rule as `getTokensBySourceLinkBase`, OR-ed over * all bases. An empty array matches nothing. */ sourceLinkBases?: string[]; } interface TokenDeleteImpact { cards: number; review_logs: number; prerequisite_edges_from_token: number; prerequisite_edges_to_token: number; session_steps: number; sessions_touched: number; agent_skills: number; } interface DeleteTokenResult { token: Token; impact: TokenDeleteImpact; } interface ScoredToken extends Token { score: number; } declare function createToken(db: Database, input: CreateTokenInput): Promise; /** * Look up a token by its unique slug. * Returns undefined if not found. */ declare function getTokenBySlug(db: Database, slug: string): Promise; /** * Look up a token by its ULID. * Returns undefined if not found. */ declare function getTokenById(db: Database, id: string): Promise; /** * Update mutable fields on a token. * * Slug is intentionally immutable in v1 because it is referenced by other * parts of the system (for example agent skill metadata). */ declare function updateToken(db: Database, slug: string, updates: UpdateTokenInput): Promise; /** * Mark a token as deprecated. Deprecated tokens are excluded from review queues * and search results but are not deleted — they can still be consulted. * * Throws if the token does not exist or is already deprecated. */ declare function deprecateToken(db: Database, slug: string): Promise; /** * All non-deprecated tokens whose source_link is `base` or `base#` * — i.e. the tokens previously imported from one OKF article (ADR * 2026-07-18). `base` is matched literally, not as a pattern. */ declare function getTokensBySourceLinkBase(db: Database, base: string): Promise; /** * Put a token into maintenance (ADR 2026-07-18): its source binding needs * repair — manually or via doctor auto-heal — and its cards leave the * review queue until cleared. Learning state is preserved. Idempotent: * re-entering maintenance refreshes the timestamp and reason. */ declare function setTokenMaintenance(db: Database, slug: string, reason: string): Promise; /** Clear a token's maintenance state — its cards re-enter scheduling. */ declare function clearTokenMaintenance(db: Database, slug: string): Promise; /** * Preview the rows that will be removed or updated when deleting a token. */ declare function getTokenDeleteImpact(db: Database, slug: string): Promise; /** * Hard-delete a token and clean up non-FK references that point at its slug. */ declare function deleteToken(db: Database, slug: string): Promise; /** * Fuzzy search for tokens by keyword query. * * Uses SQLite LIKE queries on slug, concept, and domain to avoid loading * every non-deprecated token into memory. Each search term runs its own * LIKE query; results are aggregated in JS with a word-overlap score plus * a substring bonus on the concept field. Results are returned sorted by * relevance score descending. * * For very small search terms (< 3 chars) a light in-memory fallback is * used to avoid matching every token. */ declare function findTokens(db: Database, query: string): Promise; /** * List all tokens, optionally filtered by domain or knowledge context. * Results are ordered by bloom_level then domain then slug (or bloom_level then slug if domain-filtered). */ declare function listTokens(db: Database, options?: ListTokensOptions): Promise; interface PersonalCard { tokenId: string; slug: string; title: string; concept: string; domain: string; bloomLevel: BloomLevel$1; context: string; symbiosisMode: SymbiosisMode | null; sourceLink: string | null; question: string | null; createdAt: string; updatedAt: string; cardId: string | null; state: CardState$1 | null; dueAt: string | null; /** * When the learner set this card aside (ADR 2026-07-04 Decision 10), or null * while it is scheduled. The row is returned either way — a detached card * keeps its history and can be picked up again — so a caller that does not * read this cannot tell the two apart. */ detachedAt: string | null; stability: number | null; difficulty: number | null; reps: number | null; lapses: number | null; elapsedDays: number | null; scheduledDays: number | null; blocked: number | null; provider: string | null; topicId: string | null; } declare function slugify(text: string): string; /** * Strip domain prefix (using / separator) from slug for display. */ declare function getShortSlug(slug: string, domainPrefix?: string | null): string; /** * Primary display name for a token: human title if present, else short slug. * Never falls back to concept (which is a spoiler). */ declare function getDisplayTitle(t: { title?: string | null; slug: string; }, activeDomainScope?: string | null): string; /** * Slug derivation without a database, given a way to test for collisions. * * Split out so a bulk importer can resolve hundreds of slugs against one * preloaded set instead of one query per card: on a remote (Turso) library * every one of those queries is a network round trip. `generateTokenSlug` is * this function with a database-backed predicate, so both paths can never * disagree about the naming rules. */ declare function buildTokenSlug(domain: string, concept: string, question: string | null | undefined, isTaken: (slug: string) => boolean): string; declare function generateTokenSlug(db: Database, domain: string, concept: string, question?: string | null): Promise; declare function listPersonalCards(db: Database, userId: string, options?: { query?: string; domain?: string; knowledgeContext?: string; }): Promise; interface CurriculumCardInput { question: string; concept: string; title?: string; domain: string; source_link?: string | null; context?: string; bloom_level?: number; symbiosis_mode?: string | null; provider?: string | null; topic_id?: string | null; /** Explicit prerequisite slugs (in-batch or existing). If omitted, auto-inferred. */ prerequisites?: string[]; } interface ImportCurriculumResult { createdCount: number; ensuredCount: number; } /** Whether a token belongs to a curriculum provider + Lernbereich scope. */ declare function tokenMatchesCurriculumTopicScope(token: Pick, provider: string, topicId: string): boolean; /** * Delete a user's FSRS card only when the token matches curriculum scope. * Throws when the slug exists but is outside the allowed topic scope. */ declare function deleteCurriculumCardForUser(db: Database, userId: string, slug: string, provider: string, topicId: string): Promise; /** * Count FSRS cards a user already has for a curriculum topic scope. */ declare function countUserCardsForCurriculumTopic(db: Database, userId: string, provider: string, topicId: string): Promise; interface CurriculumTopicCard { slug: string; question: string | null; concept: string; domain: string; bloomLevel: number; symbiosisMode: string | null; topicId: string | null; } /** * List a user's FSRS cards for a curriculum topic scope (Lernbereich + sub-units). */ declare function listUserCardsForCurriculumTopic(db: Database, userId: string, provider: string, topicId: string): Promise; /** * Import curriculum cards in a single transaction. * Reuses existing tokens on slug match and ensures FSRS cards exist. * Prerequisite edges are created from explicit `prerequisites` slugs or * inferred from bloom level and domain when omitted. */ declare function importCurriculumCards(db: Database, userId: string, cards: CurriculumCardInput[]): Promise; interface SplitProposalInput { question: string; concept: string; domain: string; context?: string; bloom_level?: number; symbiosis_mode?: string | null; source_link?: string | null; } /** * Confirm a card split transaction. * Creates proposal cards, links them as prerequisites to the original card, * and either blocks the original card (surfacing proposals) or deletes it. */ declare function confirmCardSplit(db: Database, userId: string, originalSlug: string, action: "block" | "remove", originalQuestion: string, originalConcept: string, proposals: SplitProposalInput[]): Promise; interface FoundationProposalInput { question: string; concept: string; domain: string; title?: string; context?: string; bloom_level?: number; symbiosis_mode?: string | null; source_link?: string | null; exists: boolean; slug?: string | null; } interface ConfirmFoundationsResult { createdCount: number; linkedCount: number; } /** * Confirm foundations import. * Reuses existing tokens or creates new ones, then links them as prerequisites to the original card. */ declare function confirmFoundations(db: Database, userId: string, originalSlug: string, proposals: FoundationProposalInput[]): Promise; interface SourceProposalInput { question: string; concept: string; domain: string; title?: string; bloom_level: number; symbiosis_mode: string; excerpt: string; page_number?: string | null; provider?: string | null; topic_id?: string | null; source_id?: string | null; /** Explicit prerequisite slugs (in-batch or existing). If omitted, auto-inferred. */ prerequisites?: string[]; } /** * Apply source proposals on an open database handle (caller may wrap in a transaction). * * Creates tokens, ensures cards, and wires prerequisite edges. When a proposal * carries explicit `prerequisites`, those slugs are used (in-batch or existing). * Otherwise prerequisites are inferred from bloom level and domain: a token * links to the highest-bloom same-domain token that precedes it in the batch, * or to existing lower-bloom tokens in the same domain from the database. */ declare function applySourceProposals(db: Database, userId: string, sourceId: string, proposals: SourceProposalInput[]): Promise; /** * Confirm source import transaction. * Saves tokens, maps them to the source in token_sources, and ensures cards exist for the user. */ declare function confirmSourceImport(db: Database, userId: string, sourceId: string, proposals: SourceProposalInput[]): Promise; /** * Token embedding repository — stores per-token vectors for semantic search * (ADR 2026-07-03) and derives staleness from a content hash. * * This module is pure storage + classification: no HTTP, no LLM calls. The * CLI layer (`src/cli/llm/embedder.ts`) generates vectors and calls in here. */ interface TokenEmbedding { token_id: string; model: string; dims: number; content_hash: string; embedded_at: string; embedding: Float32Array; } type EmbeddingStaleness = "missing" | "content-changed" | "model-changed" | "dimension-changed"; interface TokenNeedingEmbedding { token: Token; /** Canonical text to embed — already hashed the same way. */ text: string; reason: EmbeddingStaleness; } interface EmbeddingCoverage { tokens: number; embedded: number; missing: number; stale: number; } interface EmbeddedTokenRow { token: Token; embedding: Float32Array; } /** * The canonical text embedded for a token. Every stored hash and every stored * vector derives from exactly this string — never the slug, which is an * identifier, not meaning. */ declare function embeddingContentForToken(t: Pick & { title?: string | null; }): string; declare function computeContentHash(text: string): string; /** * Encode a vector as a little-endian float32 BLOB. Builds a fresh buffer (no * aliasing into the caller's array) so the row can be stored independently of * whatever produced the vector. */ declare function encodeEmbedding(vec: ArrayLike): Uint8Array; /** * Decode a stored BLOB back into a Float32Array. * * BLOB values come back as `Buffer` (better-sqlite3) or `Uint8Array` (remote * provider). better-sqlite3 Buffers are views into a shared pool and may have * a non-zero, non-4-aligned `byteOffset` — constructing a Float32Array * directly over such a buffer throws (RangeError) or silently reads garbage. * Copying guarantees a fresh, 0-offset backing buffer — but `blob.slice()` * cannot be used for this: `Buffer.prototype.slice` overrides * `Uint8Array.prototype.slice` to return a *view* into the same backing * buffer (a legacy Node.js Buffer API quirk), not a copy, so it would still * carry the misaligned offset. Uint8Array's own `slice` must be borrowed * explicitly to force an actual copy. */ declare function decodeEmbedding(blob: Uint8Array): Float32Array; declare function upsertTokenEmbedding(db: Database, input: { tokenId: string; embedding: ArrayLike; model: string; contentHash: string; }): Promise; declare function getTokenEmbedding(db: Database, tokenId: string): Promise; /** * Classify every non-deprecated token against a target embedding model: * missing (no stored row), model-changed (stored under a different model * id), or content-changed (stored hash no longer matches the canonical * text). `force: true` returns every token regardless of freshness, tagged * `content-changed` since that is the closest-fitting reason to re-embed. */ declare function listTokensNeedingEmbedding(db: Database, model: string, opts?: { limit?: number; force?: boolean; dims?: number; }): Promise; /** Same classification scan as {@link listTokensNeedingEmbedding}, counts only. */ declare function getEmbeddingCoverage(db: Database, model: string, opts?: { dims?: number; }): Promise; /** * All tokens with a fresh vector under `model` to enter the vector search leg. * Re-hashes rows here because lazy top-up is intentionally bounded: a stale * row beyond the current batch must never participate with its old meaning. */ declare function listEmbeddedTokens(db: Database, model: string): Promise; /** * Monitor log analyzer — maps observed shell commands to token ratings. * * Pure functions, no DB or filesystem access. Takes parsed command records * and a token-to-pattern mapping, returns ratings with evidence. */ interface MonitorEvent { type: "command_start" | "command_end" | "monitor_meta"; ts: string; seq?: number; pid?: number; command?: string; cwd?: string; exit_code?: number; event?: "start" | "stop"; session_id?: string; shell?: string; } interface CommandRecord { seq: number; pid: number; command: string; cwd: string; startedAt: string; endedAt: string | null; durationMs: number | null; exitCode: number | null; } interface TokenPattern { slug: string; patterns: string[]; } interface ObservationRating { tokenSlug: string; rating: 1 | 2 | 3 | 4 | null; confidence: "high" | "medium" | "low"; evidence: { matchedCommands: number; helpSeeking: boolean; errorCount: number; selfCorrections: number; medianGapMs: number | null; thinkingGapMs: number | null; }; matchedCommandTexts: string[]; } interface AnalysisResult { ratings: ObservationRating[]; unmatchedCommands: string[]; timeSpan: { start: string; end: string; durationMs: number; } | null; } /** * Parse a JSONL string into MonitorEvent objects. * Skips malformed lines silently. */ declare function parseMonitorLog(jsonl: string): MonitorEvent[]; /** * Pair command_start and command_end events by (pid, seq) into CommandRecords. */ declare function pairCommands(events: MonitorEvent[]): CommandRecord[]; /** * Analyze observed commands against token patterns and produce ratings. */ declare function analyzeObservation(commands: CommandRecord[], tokenPatterns: TokenPattern[]): AnalysisResult; /** * Monitor I/O — read/write JSONL files for shell observation. * * Monitor logs live at ~/.zam/monitor/.jsonl. * Separated from analyzer.ts so the analyzer remains pure-function testable. */ /** Get the monitor directory path. */ declare function getMonitorDir(): string; /** Get the JSONL file path for a session. */ declare function getMonitorPath(sessionId: string): string; /** Ensure the monitor directory exists (mode 0700 for privacy). */ declare function ensureMonitorDir(): void; /** Append a single event to the session's JSONL file. */ declare function writeMonitorEvent(sessionId: string, event: MonitorEvent): void; /** Read and parse all events from a session's monitor log. */ declare function readMonitorLog(sessionId: string): MonitorEvent[]; /** Check if a monitor log exists for a session. */ declare function monitorLogExists(sessionId: string): boolean; /** Get basic stats about a monitor log without full parsing. */ declare function getMonitorLogStats(sessionId: string): { exists: boolean; sizeBytes: number; lineCount: number; }; /** * Observer permission policy — Layer 2 of the two-layer consent model * (see docs/adr/0001-observer-permission-model.md). * * A host (the CLI permission system today, MCP tool-consent later) decides * WHETHER an agent may invoke the observer. This module decides WHAT a given * capture is then allowed to see — enforced by ZAM, because ZAM holds the * camera. The policy is user-configurable through `zam settings` (the * `observer.*` keys in `user_config`) and resolved here into a typed value with * safe defaults. * * The decision functions are pure so they can be unit-tested without a DB or a * live screen, and reused unchanged under a future `zam mcp serve`. */ declare const OBSERVER_POLICY_VERSION: 1; type ObserverScope = "off" | "window" | "fullscreen"; type ObserverConsent = "per-capture" | "per-session" | "standing"; type ObserverRetention = "none" | "session" | "persist"; interface ObserverPolicy { version: typeof OBSERVER_POLICY_VERSION; /** "off" disables capture; "window" requires a target; "fullscreen" permits an untargeted grab. */ scope: ObserverScope; /** Lower-cased process names permitted under window scope (empty = any non-denied window). */ allowlist: string[]; /** Lower-cased process/title fragments the user never wants captured (added to the built-in set). */ denylist: string[]; consent: ObserverConsent; retention: ObserverRetention; redactWindowTitles: boolean; audioOptIn: boolean; } declare const DEFAULT_OBSERVER_POLICY: ObserverPolicy; /** * Built-in sensitive process/title fragments that are ALWAYS non-observable. * User config may extend the effective denylist but can never re-enable capture * of these — a user `allowlist` cannot override the built-in floor. Matching is * case-insensitive substring against process name and window title. This mirrors * a conservative subset of the native Rust observer's sensitive-context set. */ declare const BUILT_IN_SENSITIVE_MATCHERS: readonly string[]; type ObserverSettingKey = "observer.scope" | "observer.allowlist" | "observer.denylist" | "observer.consent" | "observer.retention" | "observer.redact_titles" | "observer.audio"; /** * Public wrapper around the allow/denylist normalizer so CLI list mutation * (`zam observer grant/revoke`) parses entries exactly like resolveObserverPolicy. */ declare function parseObserverList(raw: string | undefined): string[]; /** Pure: build a policy from raw setting strings (no DB access). */ declare function parseObserverPolicy(raw: Partial>, defaults?: { scope: ObserverScope; consent: ObserverConsent; }): ObserverPolicy; /** Read the policy from `user_config`, falling back to active symbiosis mode presets, then safe defaults. */ declare function resolveObserverPolicy(db: Database): Promise; type CaptureDenialReason = "scope-off" | "scope-requires-target" | "denylisted" | "not-allowlisted" | "sensitive"; type CaptureDecision = { allowed: true; } | { allowed: false; reason: string; denialReason: CaptureDenialReason; }; interface CaptureRequest { /** Whether the caller specified a concrete window target (--hwnd or --process-name). */ hasExplicitTarget: boolean; /** The requested process name, if any (comparison is case-insensitive). */ requestedProcessName: string | null; } interface ResolvedCaptureTarget { /** printwindow | copyfromscreen | fullscreen | provided | screencapture-* */ method: string; processName: string | null; windowTitle: string | null; } /** Built-in sensitive match (authoritative — user config cannot override). */ declare function matchBuiltInSensitive(processName: string | null, windowTitle: string | null): string | null; /** User-denylist match. */ declare function matchDenylist(policy: ObserverPolicy, processName: string | null, windowTitle: string | null): string | null; /** * Phase 1 — decide before any pixels are grabbed, from scope plus the requested * target. Cheap denials (disabled observer, missing target, an explicitly named * sensitive/denied process) happen here so no screenshot is taken at all. */ declare function decidePreCapture(policy: ObserverPolicy, request: CaptureRequest): CaptureDecision; /** * Phase 2 — decide after the window was resolved into a concrete target. This * is the first point where the real process/title are known, so the * sensitive/denylist check against the *actual* captured window happens here; * the caller discards the pixels if it fails. */ declare function decidePostCapture(policy: ObserverPolicy, target: ResolvedCaptureTarget): CaptureDecision; /** True if the user has set any `observer.*` key in user_config. */ declare function isObserverPolicyConfigured(db: Database): Promise; /** Hint surfaced when a UI session starts with no observer policy configured. */ declare const OBSERVER_POLICY_UNSET_HINT: string; /** * Bridge between the ObserverPolicy (Layer 2) and the native Rust observer * sidecar. The sidecar reads a kernel-written policy file at * `/policy.json` instead of its own `ZAM_OBSERVER_PRIVACY_POLICY` * env var, so the headless `capture-ui` path and the live sidecar share one * source of truth. See docs/adr/0001-observer-permission-model.md (item 4). */ /** Filename, under the observer dir, that the Rust sidecar reads. */ declare const SIDECAR_POLICY_FILE = "policy.json"; /** * The Rust observer's `WindowPrivacyPolicy` wire shape (serde camelCase). These * are only the user-configurable lists; the sidecar enforces its own * authoritative built-in sensitive set on top, exactly like the TS side. */ interface SidecarPrivacyPolicy { allowProcesses: string[]; denyProcesses: string[]; denyTitleMarkers: string[]; } /** * Pure mapping from an ObserverPolicy to the sidecar's `WindowPrivacyPolicy`. * A denylist term should block on process OR title, so it feeds both the * process and title-marker lists. */ declare function toSidecarPrivacyPolicy(policy: ObserverPolicy): SidecarPrivacyPolicy; /** * Resolve the policy from settings and write the sidecar file. Returns the * path written and the serialized policy. The directory mirrors * `getUiObserverDir()`, which the Rust observer resolves identically * (`ZAM_OBSERVER_DIR`, else `~/.zam/observer`). */ declare function syncObserverSidecarPolicy(db: Database, dir?: string): Promise<{ path: string; policy: SidecarPrivacyPolicy; }>; /** * Cascade Block & Unblock — prerequisite-aware blocking logic. * * Ported from the PoC's cascade-block and unblock-ready commands. * * When a user rates a token as "forgot" (rating 1) and that token has * prerequisites, we block the token and surface its prerequisites into * the active deck. When all prerequisites are met, we unblock. */ interface CascadeBlockResult { blockedSlug: string; prerequisites: Array<{ slug: string; concept: string; bloomLevel: number; }>; } interface UnblockResult { unblocked: Array<{ slug: string; concept: string; }>; } /** * Block a token and surface its prerequisites. * * Called when a user rates a token as "forgot" (rating 1). The token is * marked as blocked so it won't appear in review queues. All direct * prerequisites are ensured to have cards (unblocked, due now) so they * appear in the user's next review session. * * @param db - Database connection * @param userId - The user whose card to block * @param tokenSlug - Slug of the token the user forgot * @returns Info about what was blocked and which prerequisites were surfaced */ declare function cascadeBlock(db: Database, userId: string, tokenSlug: string): Promise; /** * Scan all blocked cards for a user and unblock any whose prerequisites are met. * * A blocked card is ready to unblock when ALL of its direct prerequisites have: * - reps >= 1 (the user has successfully recalled it at least once) * - blocked = 0 (the prerequisite itself is not blocked) * * If a blocked card has no prerequisites at all, it is unblocked immediately * (it was likely blocked in error or its prerequisites were removed). * * Unblocking cascades: when unblocking a card satisfies the last unmet * prerequisite of another blocked card, that card unblocks in the same call. * * @param db - Database connection * @param userId - The user whose blocked cards to check * @returns List of cards that were unblocked */ declare function unblockReady(db: Database, userId: string): Promise; /** * FSRS-6 — Free Spaced Repetition Scheduler * * Pure-function implementation of the long-term FSRS-6 memory model plus * deterministic short learning and relearning steps. The kernel owns the * scheduling semantics; persistence and UI surfaces only store/render the * returned state. * * Reference: https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm */ /** 1 = Again (forgot), 2 = Hard, 3 = Good, 4 = Easy. */ type Rating = 1 | 2 | 3 | 4; type CardState = "new" | "learning" | "review" | "relearning"; interface SchedulingCard { /** Memory stability in days — the interval at which recall reaches 90%. */ stability: number; /** Intrinsic difficulty on a 1–10 scale. */ difficulty: number; /** Days elapsed since the last review; fractional for same-day reviews. */ elapsedDays: number; /** Current interval in days; fractional while a short step is active. */ scheduledDays: number; /** Count of successful consecutive reviews. */ reps: number; /** Times the card was forgotten (rated Again). */ lapses: number; /** Current learning state. */ state: CardState; /** Zero-based cursor into the active learning/relearning steps. */ learningStep: number | null; /** When the card is next due. */ dueAt: Date; /** When the card was last reviewed (null for new cards). */ lastReviewAt: Date | null; } interface FSRSParameters { /** The 21 FSRS-6 model weights (w0–w20). */ readonly w: readonly number[]; /** Target recall probability used to calculate long-term intervals. */ readonly requestRetention: number; /** Ascending short steps for new cards, expressed in minutes. */ readonly learningStepsMinutes: readonly number[]; /** Ascending short steps after a lapse, expressed in minutes. */ readonly relearningStepsMinutes: readonly number[]; /** Upper bound for a long-term review interval. */ readonly maximumIntervalDays: number; } interface FSRS { /** Return a fully updated card after applying a rating. Pure function. */ schedule(card: SchedulingCard, rating: Rating, now?: Date): SchedulingCard; /** The immutable parameters baked into this instance. */ readonly params: Readonly; } /** * Create a deterministic FSRS-6 scheduler instance. * * The operation has no side effects, database access, random fuzzing, or model * calls. The same card, rating, time, and parameters always produce the same * result on every ZAM surface. */ declare function createFSRS(params?: Partial): FSRS; /** * Session synthesis connects shell observation to durable learning state. * * A preview analyzes monitor commands without mutating the database. Applying * one confirmed candidate updates the card, review log, session step, blocking * state, and synthesis audit record in a single transaction. */ type SynthesisConfidence = "medium" | "high"; interface SessionSynthesisCandidate { tokenId: string; tokenSlug: string; concept: string; domain: string; inferredRating: Rating; confidence: SynthesisConfidence; evidence: ObservationRating["evidence"]; matchedCommandTexts: string[]; } interface PrepareSessionSynthesisInput { sessionId: string; explicitPatterns?: TokenPattern[]; minConfidence?: SynthesisConfidence; /** Test and integration hook; normal callers read the monitor log. */ commands?: CommandRecord[]; } interface SessionSynthesisPreview { sessionId: string; userId: string; patternCount: number; commandCount: number; alreadyApplied: number; skippedLowConfidence: number; candidates: SessionSynthesisCandidate[]; unmatchedCommands: string[]; timeSpan: { start: string; end: string; durationMs: number; } | null; } interface SessionSynthesisEvidence { signals: ObservationRating["evidence"]; matchedCommandTexts: string[]; } interface SessionSynthesisRecord { session_id: string; token_id: string; card_id: string; inferred_rating: Rating; confirmed_rating: Rating; confidence: SynthesisConfidence; evidence: SessionSynthesisEvidence; review_log_id: string; session_step_id: string; created_at: string; } interface ApplySessionSynthesisInput { sessionId: string; tokenSlug: string; inferredRating: Rating; confirmedRating: Rating; confidence: SynthesisConfidence; evidence: ObservationRating["evidence"]; matchedCommandTexts: string[]; } interface ApplySessionSynthesisResult { applied: boolean; record: SessionSynthesisRecord; blocked?: Awaited>; } declare function getSessionSynthesisRecords(db: Database, sessionId: string): Promise; declare function prepareSessionSynthesis(db: Database, input: PrepareSessionSynthesisInput): Promise; declare function applySessionSynthesis(db: Database, input: ApplySessionSynthesisInput): Promise; /** * Shell hook code generation for zsh, bash, and PowerShell. * * Pure functions that return shell code strings. The CLI command * `zam monitor start/stop` calls these and prints to stdout. */ /** * Generate zsh hooks that capture commands to a JSONL file. * Uses $EPOCHREALTIME for sub-second timestamp precision. */ declare function generateZshHooks(monitorFile: string, sessionId: string): string; /** * Generate bash hooks that capture commands to a JSONL file. * Uses DEBUG trap for preexec, PROMPT_COMMAND for precmd. */ declare function generateBashHooks(monitorFile: string, sessionId: string): string; /** * Generate PowerShell hooks that capture completed commands to a JSONL file. * PowerShell has no zsh-style preexec hook, so this records the most recent * history item from the prompt function after each command completes. */ declare function generatePowerShellHooks(monitorFile: string, sessionId: string): string; /** Generate zsh code to remove monitor hooks. */ declare function generateZshUnhooks(): string; /** Generate bash code to remove monitor hooks. */ declare function generateBashUnhooks(): string; /** Generate PowerShell code to remove monitor hooks. */ declare function generatePowerShellUnhooks(): string; /** * Skill Discovery — identifies recurring non-standard command patterns * across multiple sessions and proposes them as minimal reusable skills. * * The key insight from Increment 2: "The human's demonstrated competence * is the gate for automation — not the other way around." A pattern must * appear consistently across sessions before being proposed as a skill. * * Pure functions — no DB access. Callers provide command records and * existing skills; this module returns proposed skills. */ interface CommandSequence { /** The ordered command prefixes forming the pattern (e.g., ["git checkout", "npm install", "npm run build"]) */ steps: string[]; /** How many sessions contained this sequence */ sessionCount: number; /** Total occurrences across all sessions */ totalOccurrences: number; /** Example full commands from the most recent occurrence */ examples: string[]; } interface SkillProposal { /** Suggested slug for the skill */ slug: string; /** Human-readable description of what the pattern does */ description: string; /** The command steps forming the skill */ steps: string[]; /** How many sessions demonstrated this pattern */ sessionCount: number; /** Confidence that this is a real, repeatable skill */ confidence: "high" | "medium" | "low"; /** Example commands from actual usage */ examples: string[]; } interface DiscoveryOptions { /** Minimum number of sessions a pattern must appear in (default: 2) */ minSessions?: number; /** Minimum sequence length to consider (default: 2) */ minSequenceLength?: number; /** Maximum sequence length to consider (default: 5) */ maxSequenceLength?: number; /** Existing skill slugs to exclude from proposals */ existingSkillSlugs?: string[]; } /** * Discover recurring command patterns across multiple sessions. * * Takes a map of session ID → command records, finds command sequences * that appear in multiple sessions, and proposes them as skills. * * @param sessionCommands - Map of session ID to that session's commands * @param options - Discovery configuration * @returns Array of skill proposals, sorted by confidence then session count */ declare function discoverSkills(sessionCommands: Map, options?: DiscoveryOptions): SkillProposal[]; /** * Provider-neutral protocol for the native UI observer sidecar. * * Reports are evidence only. They must not update cards or FSRS state without * passing through confirmed session synthesis. */ declare const UI_OBSERVATION_PROTOCOL_VERSION: 1; type UiObservationKind = "progress" | "step-completed" | "error" | "help-seeking" | "uncertain" | "privacy-pause" | "heartbeat"; type UiActionType = "click" | "shortcut" | "typing" | "scroll" | "window-change"; type UiEvidenceType = "uia" | "keyframe" | "clip" | "window"; interface UiApplicationContext { processName: string; processId?: number; windowTitle?: string; } interface UiObservedAction { type: UiActionType; target?: string; result?: string; } interface UiEvidenceRef { type: UiEvidenceType; ref: string; redacted: boolean; } interface UiCandidateToken { slug: string; confidence: number; rationale: string; } interface UiObservationReport { version: typeof UI_OBSERVATION_PROTOCOL_VERSION; sessionId: string; sequence: number; observedFrom: string; observedTo: string; kind: UiObservationKind; application: UiApplicationContext; summary: string; actions: UiObservedAction[]; evidence: UiEvidenceRef[]; candidateTokens: UiCandidateToken[]; confidence: number; } declare function isUiObservationReport(value: unknown): value is UiObservationReport; /** Parse report JSONL and skip malformed or unsupported records. */ declare function parseUiObservationLog(jsonl: string): UiObservationReport[]; /** * Read and append observer-agent reports in ~/.zam/observer/.reports.jsonl. */ declare function getUiObserverDir(): string; declare function getUiObservationPath(sessionId: string): string; declare function ensureUiObserverDir(): void; declare function uiObservationLogExists(sessionId: string): boolean; declare function readUiObservationLog(sessionId: string): UiObservationReport[]; declare function appendUiObservationReport(report: UiObservationReport): void; /** * Map persisted UI observer reports into session synthesis candidates. */ declare function buildUiSynthesisCandidates(reports: UiObservationReport[], tokens: Map, applied: Set, minConfidence: SynthesisConfidence): { candidates: SessionSynthesisCandidate[]; skippedLowConfidence: number; }; declare function uiObservationTimeSpan(reports: UiObservationReport[]): { start: string; end: string; durationMs: number; } | null; /** * Rating Evaluator * * Processes a user's self-assessment rating after a recall attempt. * Coordinates between FSRS scheduling, review logging, and blocking. */ interface EvaluateInput { cardId: string; tokenId: string; userId: string; rating: Rating; sessionId?: string; responseTimeMs?: number; reviewLogId?: string; now?: Date; } interface EvaluateResult { nextDueAt: string; stability: number; difficulty: number; state: string; learningStep: number | null; scheduledDays: number; reps: number; lapses: number; buriedSiblings: number; buriedUntil: string | null; } /** * Process a rating: update the card via FSRS, log the review. * Returns the updated scheduling state. * * Note: blocking logic (cascade-block) is handled separately by the caller * when rating === 1 and the token has prerequisites. */ declare function evaluateRating(db: Database, input: EvaluateInput): Promise; type ReviewActionType = "rate" | "skip" | "edit-token" | "deprecate-token" | "delete-token" | "delete-card" | "stop"; interface ExecuteReviewActionInput { cardId: string; userId: string; action: ReviewActionType; rating?: Rating; sessionId?: string; responseTimeMs?: number; tokenUpdates?: UpdateTokenInput; now?: Date; } interface ReviewActionResult { action: ReviewActionType; token: Token; evaluation?: EvaluateResult; blocked?: CascadeBlockResult; sessionStep?: SessionStep; updatedToken?: Token; deletedToken?: DeleteTokenResult; deletedCard?: DeleteCardResult; skipped?: boolean; stopped?: boolean; } declare function executeReviewAction(db: Database, input: ExecuteReviewActionInput): Promise; /** * Active Recall Prompt Generation * * Generates review prompts from tokens, adapting the question style * to the token's Bloom taxonomy level. This is NOT an LLM call — * it's template-based prompt assembly for the CLI and bridge. */ type BloomLevel = 1 | 2 | 3 | 4 | 5; interface RecallPrompt { cardId: string; tokenId: string; slug: string; question: string; concept: string; domain: string; bloomLevel: BloomLevel; bloomVerb: string; hints: string[]; sourceLink?: string | null; } interface PromptInput { cardId: string; tokenId: string; slug: string; concept: string; domain: string; bloomLevel: BloomLevel; sourceLink?: string | null; question?: string | null; } /** * Generate a template-based concept-free recall cue using the slug and domain. */ declare function generateConceptFreeCue(bloomLevel: BloomLevel, slug: string, _domain: string): string; /** * Generate a recall prompt for a token at its Bloom level. * When called from the CLI, the prompt is rendered in the terminal. * When called from the AI bridge, the JSON is returned for the AI to present conversationally. */ declare function generatePrompt(input: PromptInput): RecallPrompt; interface ResolvedReference { sourceType: "local" | "remote_web" | "dynamic_search"; content: string; filePath?: string; url?: string; } /** * A source reference resolved and bounded for inclusion in a review payload. * Same shape as ResolvedReference plus the originating link and a truncation flag. */ interface ReviewContext { sourceLink: string; sourceType: ResolvedReference["sourceType"]; content: string; filePath?: string; url?: string; truncated: boolean; } /** Default cap on resolved content length, so bridge JSON / terminal output stays bounded. */ declare const DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6000; /** How long resolved review context stays in the in-process cache (5 minutes). */ declare const REVIEW_CONTEXT_CACHE_TTL_MS: number; /** Clear the in-process review-context cache (mainly for tests). */ declare function clearReviewContextCache(): void; /** * Resolves a given token's source_link into readable textual content. */ declare function resolveReference(sourceLink: string): Promise; /** * Resolve a token's source_link into bounded, review-ready context. * * Wraps {@link resolveReference} for the review/bridge flow: returns `null` * for empty links and caps content length so the surrounding payload (bridge * JSON or terminal output) stays manageable, flagging when truncation occurred. */ declare function resolveReviewContext(sourceLink: string | null | undefined, opts?: { maxChars?: number; }): Promise; /** * Normalizes a path, stripping anchors and converting separators. */ declare function normalizePath(p: string): string; /** * Checks if a token's source_link references a changed file. */ declare function matchesFilePath(sourceLink: string | null, changedFile: string): boolean; /** * Hands-free review orchestration over an injected speech port (ADR * 2026-07-31). * * This module is deliberately platform-free: it drives the review loop and * decides *which* speech tier to use, but never touches a microphone, a * speaker, or the network. Every surface — the Android/iOS companion, the * macOS/Windows desktop app — supplies its own {@link VoicePort} and keeps the * loop identical. It first shipped inside the Android companion * (`mobile/src/voice.ts`) and moved here unchanged when the desktop app gained * voice mode in 0.24.0. */ type VoiceLocale = "de-DE" | "en-US"; interface VoicePort { start(locale: VoiceLocale): Promise; stop(): Promise; speak(text: string, locale: VoiceLocale): Promise; listen(locale: VoiceLocale): Promise; } interface VoiceReviewCard { question: string; expectedAnswer: string; revealed: boolean; draftAnswer: string; } /** Optional smart-evaluation result for the current answer. */ interface VoiceEvaluationSpeech { /** Full TTS block (feedback + suggested rating + rating prompt). */ speech: string; suggestedRating: Rating; } interface VoiceReviewAdapter { currentCard(): VoiceReviewCard | null; captureAnswer(transcript: string): void; /** * May be asynchronous: the desktop reveal runs an LLM evaluation and * repaints the card, and speaking before it settles would read a stale one. * The loop awaits this before re-reading {@link currentCard}. */ revealAnswer(): void | Promise; /** * Optional intelligent evaluation after reveal. Return null to fall back * to reading the expected answer and self-rating. */ evaluateAnswer?(): Promise; rate(rating: Rating): Promise; setStatus(message: string, isError?: boolean): void; } /** * Where a single speech capability runs. `local` is the platform's own speech * stack (Apple Speech/AVSpeechSynthesizer, Windows WinRT, Android * SpeechRecognizer/TextToSpeech) — no third party, no per-use cost, quality and * availability bounded by the device. `cloud` is an entry in the capability * model registry with the `stt`/`tts` flag set. */ type VoiceEngineTier = "local" | "cloud"; /** The two speech capabilities voice mode needs. */ type VoiceCapability = "stt" | "tts"; /** * User preference, chosen in Settings (ADR 2026-07-31). * * The default is `device-first`: ZAM prefers the device because it keeps audio * out of third-party hands and costs nothing, but a learner whose device has no * usable recognizer should still get to review on a walk rather than not review * at all. `device-only` is the strict-privacy choice and accepts that voice mode * may be unavailable; `quality-first` accepts per-use cost and a third party in * exchange for better recognition and more natural voices. */ type VoiceEnginePreference = "device-only" | "device-first" | "quality-first"; declare const DEFAULT_VOICE_ENGINE_PREFERENCE: VoiceEnginePreference; declare const VOICE_ENGINE_PREFERENCES: readonly VoiceEnginePreference[]; declare function isVoiceEnginePreference(value: unknown): value is VoiceEnginePreference; /** Which tiers can actually serve a capability right now. */ interface VoiceTierAvailability { local: boolean; cloud: boolean; } type VoiceAvailability = Record; /** * Why a capability ended up where it did. Surfaces turn this into copy so a * learner is never silently switched to a paid, third-party path — the one * failure mode that would make the preference dishonest. */ type VoiceEngineReason = "preferred" | "fell-back-to-cloud" | "fell-back-to-local" | "unavailable-device-only" | "unavailable"; interface VoiceEngineDecision { tier: VoiceEngineTier | null; reason: VoiceEngineReason; } type VoiceEnginePlan = Record; /** * Resolve the user's preference against what this device and configuration can * actually do. Speech-to-text and text-to-speech are resolved independently: * Linux has local synthesis but no local recognizer, and a learner with no * cloud model configured still gets local reading-aloud. */ declare function resolveVoiceEnginePlan(preference: VoiceEnginePreference, availability: VoiceAvailability): VoiceEnginePlan; /** * Voice mode needs both halves of the loop: a card is read aloud and an answer * is spoken back. Either half missing means the session cannot run. */ declare function isVoiceModeUsable(plan: VoiceEnginePlan): boolean; /** True when any capability leaves the device, i.e. a third party is involved. */ declare function planLeavesDevice(plan: VoiceEnginePlan): boolean; declare function resolveVoiceLocale(locale: string | null | undefined): VoiceLocale; declare function parseSpokenRating(transcript: string, locale: VoiceLocale): Rating | null; declare class HandsFreeReviewController { private readonly port; private readonly adapter; private generation; private running; constructor(port: VoicePort, adapter: VoiceReviewAdapter); get active(): boolean; start(locale: VoiceLocale): Promise; pause(): Promise; private isCurrent; } /** * Reorder items so no domain appears more than `maxConsecutive` times in a row. * * Algorithm: group items by domain, then round-robin across domain groups. * Each round picks one item from each non-exhausted domain. Within a domain, * the original order is preserved (so urgency sorting survives). * * If a domain has more items than others, its extras will appear after all * other domains are exhausted — but the `maxConsecutive` cap is still * respected by inserting items from the largest remaining domains first. * * @param items - Array of items to interleave. Not mutated. * @param maxConsecutive - Max consecutive items from the same domain. Defaults to 2. * @returns A new array with the same items in interleaved order. */ declare function interleave(items: T[], maxConsecutive?: number): T[]; /** * Review Queue Builder — assembles a session's review queue. * * Combines due-card fetching, new-card selection, urgency sorting, * and cross-domain interleaving into a single ready-to-review queue. */ interface ReviewQueueOptions { userId: string; maxNew?: number; maxReviews?: number; buryNewSiblings?: boolean; buryReviewSiblings?: boolean; now?: Date; domain?: string; knowledgeContext?: string; } interface ReviewFastCheck { type: "binary_choice"; options: string[]; correctIndex: number; } interface ReviewQueueItem { cardId: string; tokenId: string; slug: string; title: string; concept: string; domain: string; bloomLevel: number; state: string; dueAt: string; sourceLink: string | null; question: string | null; questionSource: string; siblingGroup: string | null; hasQuestionMedia: boolean; hasAnswerMedia: boolean; contentChanged?: boolean; publishedBy?: string | null; publishedAt?: string | null; atomId: string | null; tier: string | null; fastCheck: ReviewFastCheck | null; } /** * Pilot rule `tier1-first` (field-test): a new Tier-2 item stays out of the * queue while a new Tier-1 item of the same atom is still unreviewed. * * Enforced in the new-card SQL below, deliberately in one place. It first * existed as a filter over the fetched batch, which agreed with the rule only * as long as both items fell inside the same `LIMIT` window — a Tier-1 card * pushed past the window would have admitted its Tier-2 sibling. */ declare const TIER1_FIRST_RULE = "tier1-first"; interface ReviewQueue { items: ReviewQueueItem[]; newCount: number; reviewCount: number; relearnCount: number; totalDomains: string[]; } /** * Build a review queue for a user's study session. * * The queue is assembled in stages: * 1. Fetch all due cards (not blocked, due_at <= now, state in review/relearning/learning) * 2. Fetch new cards (state = 'new', not blocked) * 3. Sort overdue cards by urgency — most overdue first * 4. Apply cross-domain interleaving to prevent same-domain streaks * 5. Intersperse new cards at regular intervals (every 5th position) * 6. Apply sibling controls and the learner's persisted workload limits * * @param db - Database connection * @param options - Queue building options * @returns The assembled review queue with metadata */ declare function buildReviewQueue(db: Database, options: ReviewQueueOptions): Promise; /** * Order the options a learner sees, and move `correctIndex` with them. * * Every fast check currently authored puts the correct answer first. Rendered * in stored order that is not a retrieval task: after two cards the learner * has learned the button position, taps it without reading, and the rating * that follows is evidence of nothing — worse than a missing check, because * FSRS then schedules on it. * * Fixing the content alone would not hold; the next author defaults to index 0 * again. So the presentation permutes, and no surface can forget to. * * The permutation is **derived, not random**: the kernel performs no random * operations, and a re-render inside one presentation must not move a button * under a learner's finger. Seeding on the card's due date means the order is * fixed while the card is being answered and differs the next time it comes * round, so the position cannot be memorised either. */ declare function presentFastCheck(fastCheck: ReviewFastCheck | null, seed: string): ReviewFastCheck | null; /** * Parse the persisted, editorial fast-check payload into the review contract. * * Malformed optional metadata must never make the whole queue unavailable. * Installation validation can report bad content separately; a learner still * gets the ordinary question/answer card as the graceful fallback. */ declare function parseReviewFastCheck(raw: unknown): ReviewFastCheck | null; /** Sibling-aware queue suppression for cards rendered from the same note. */ interface BurySiblingResult { buried: number; until: string | null; } /** Start of the next local calendar day, represented as an ISO instant. */ declare function nextLocalDay(now: Date): string; /** * Bury eligible new/review siblings after a rating. * * Short-step learning and relearning siblings intentionally remain available: * their same-day steps are already in progress and must not be deferred. */ declare function burySiblingCards(db: Database, input: { cardId: string; tokenId: string; userId: string; now: Date; }): Promise; /** Make all temporarily buried sibling cards visible again for a learner. */ declare function unburySiblingCards(db: Database, userId: string): Promise; /** Persistent, per-learner review workload controls (ADR 2026-08-09). */ type StudyWorkloadPreset = "balanced" | "exam" | "problems" | "custom"; interface StudyWorkloadSettings { preset: StudyWorkloadPreset; maxNew: number; maxReviews: number; buryNewSiblings: boolean; buryReviewSiblings: boolean; } interface UpdateStudyWorkloadInput { preset?: StudyWorkloadPreset; maxNew?: number; maxReviews?: number; buryNewSiblings?: boolean; buryReviewSiblings?: boolean; } declare const STUDY_WORKLOAD_PRESETS: Readonly, StudyWorkloadSettings>>; declare const DEFAULT_STUDY_WORKLOAD: StudyWorkloadSettings; declare function isStudyWorkloadPreset(value: unknown): value is StudyWorkloadPreset; /** Read a learner's settings, degrading safely to the balanced defaults. */ declare function getStudyWorkloadSettings(db: Database, userId: string): Promise; /** Persist a preset or custom workload after validating bounded limits. */ declare function setStudyWorkloadSettings(db: Database, userId: string, input: UpdateStudyWorkloadInput): Promise; /** * Hybrid lexical/vector token search using reciprocal-rank fusion (RRF). * (ADR 2026-07-03) * * This module is pure math + database queries — zero LLM dependencies, no HTTP. */ interface HybridSearchOptions { queryEmbedding?: ArrayLike; /** Model the stored vectors must match; required when queryEmbedding is set. */ model?: string; limit?: number; rrfK?: number; vectorTopK?: number; } interface HybridScoredToken extends Token { score: number; lexicalRank: number | null; vectorRank: number | null; similarity: number | null; } /** Calculates the cosine similarity between two float vectors. Returns 0 if either norm is 0. */ declare function cosineSimilarity(a: Float32Array, b: Float32Array): number; /** * Performs a hybrid search over active tokens. * Combines lexical relevance rankings with vector cosine similarities. */ declare function searchTokensHybrid(db: Database, query: string, opts?: HybridSearchOptions): Promise; /** * Foundation suggestions for tokens. * Suggests existing tokens that are semantically related as prerequisite candidates. * * This module is pure math + database queries — zero LLM dependencies, no HTTP. */ interface FoundationSuggestion { token: Token; similarity: number; alreadyPrerequisite: boolean; wouldCreateCycle: boolean; /** Candidate's bloom_level is higher than the target's — unusual for a foundation. */ bloomAboveTarget: boolean; } interface SuggestFoundationsOptions { queryEmbedding: ArrayLike; /** Canonical embedding model id the stored vectors must match. */ model: string; /** Set when the target token already exists (register-after / rating-1 flow). */ targetTokenId?: string; /** Used for the bloomAboveTarget flag; defaults to 5 (nothing flagged). */ targetBloomLevel?: BloomLevel$1; limit?: number; minSimilarity?: number; maxSimilarity?: number; } /** * Suggests existing tokens that are semantically related as prerequisite candidates. * Filters by similarity range and ranks descending by similarity. */ declare function suggestFoundations(db: Database, opts: SuggestFoundationsOptions): Promise; /** * Get the path to ZAM's internal package SKILL.md. */ declare function getPackageSkillPath(agent?: "default" | "claude" | "codex"): string; /** * Distribute the ZAM active-recall training skill globally. * Copies SKILL.md into global directories for supported coding agents. */ declare function distributeGlobalSkills(home?: string): Array<{ name: string; path: string; success: boolean; }>; /** * Add opt-in helpers for starting a monitored session to user shell profiles. */ declare function injectShellHooks(home?: string): Array<{ shell: string; file: string; success: boolean; alreadyHooked: boolean; }>; type SupportedLocale = "en" | "de" | "es" | "fr" | "pt" | "zh" | "ja"; /** * Clean and map raw locale string (e.g., "de_DE.UTF-8" or "en-US") to SupportedLocale. */ declare function normalizeLocale(raw: string): SupportedLocale; /** * Detect the operating system's active language code dynamically. */ declare function detectSystemLocale(): SupportedLocale; type TranslationKey = "welcome" | "new_review_relearn" | "domains" | "instruction" | "quit_hint" | "offline_warning" | "offline_instruction" | "nothing_due" | "evaluating" | "generating_question" | "translating" | "prompt_answer" | "session_ended" | "session_complete" | "cards_rated" | "avg_rating" | "forgot" | "feedback_title" | "answer_title" | "keep_waiting" | "local_ai_working" | "wait_warning" | "wait_info" | "keep_waiting_llm" | "proceeding_offline" | "eval_skipped"; /** * Format and interpolate a translation string with key-value params. */ declare function t(locale: SupportedLocale, key: TranslationKey, params?: Record): string; /** * Update-decision logic — Increment 12, Phase 5. * * The brain behind "the app noticed a newer version": given the current and * latest versions and how this copy was installed, decide what the UI should * do. Deliberately network-free and pure, so it is fully unit-tested; the * actual version fetch (e.g. GitHub releases) and the Tauri self-update live in * the CLI/desktop layers that call this. */ type InstallChannel = "developer" | "direct" | "winget" | "homebrew"; /** Provisional package identifiers; finalized when channels ship (Phase 2). */ declare const WINGET_PACKAGE_ID = "ZAM.ZAM"; declare const HOMEBREW_CASK = "zam"; type UpdateActionKind = "none" | "self-update" | "run-command" | "inform"; interface UpdateDecision { updateAvailable: boolean; currentVersion: string; latestVersion: string; channel: InstallChannel; /** What the UI should do about the update. */ action: UpdateActionKind; /** For "run-command"/"inform": the command to surface to the user. */ command?: string; /** Locale-agnostic explanation; the UI provides its own localized copy. */ reason: string; } /** * Compare two semver-ish versions. Returns 1 if `a` is newer than `b`, -1 if * older, 0 if equal. A version with a prerelease tag (1.0.0-beta) sorts below * the same core release (1.0.0), per semver. */ declare function compareVersions(a: string, b: string): -1 | 0 | 1; /** * Decide what to do given current/latest versions and the install channel. * The mechanism follows how the copy was installed so we never self-replace a * package-managed install or a source checkout. */ declare function decideUpdate(input: { currentVersion: string; latestVersion: string; channel: InstallChannel; }): UpdateDecision; /** A single step an executor runs to APPLY an update, in order. */ type UpdateStepKind = "git-pull" | "npm-install" | "npm-build" | "smoke-test" | "distribute-skills" | "run-command" | "self-update"; interface UpdateStep { kind: UpdateStepKind; /** Human-readable description of the step. */ label: string; /** For "run-command": the exact command to run. */ command?: string; } /** * Turn a decision into the ordered steps that APPLY it — the counterpart to * `decideUpdate`, which only decides whether and how. Pure and side-effect * free, so the sequencing is unit-tested; the CLI resolves paths and runs each * step. Empty when no update is available. */ declare function planUpdate(decision: UpdateDecision): UpdateStep[]; /** * Per-machine install configuration — Increment 12, Phase 3. * * Records whether this machine runs ZAM in "developer" mode (source checkout, * git-backed workspace, manual `git`/`npm` updates) or "default" mode (an * installed application updated through a package manager or the in-app * updater). Stored in ~/.zam/config.json — a per-machine file, NOT the database * and NOT the personal folder, so the mode never travels through a shared Turso * database or a synced folder, where it would be wrong for the other machine. * * Workspace selection is machine-local too: `activeWorkspaceId` points at one * entry in `workspaces`, while legacy database settings are migrated by the CLI. */ type InstallMode = "developer" | "default"; interface InstallConfig { mode?: InstallMode; /** How this copy was installed; drives the self-update mechanism. */ channel?: InstallChannel; /** Machine-local AI provider choices; never synchronized through the DB. */ ai?: MachineAiConfig; /** Machine-local agent-connect state; harness installs are per-machine. */ agent?: MachineAgentConfig; /** * Machine-local Companion UI preferences (ADR 2026-07-16 §Decision 4, * 0.11.0 Phase 2): selected learner, selected evaluator, and per-surface * collapsed state. Deliberately never the Turso-shared learning database — * changing the Companion learner must not rewrite the database-wide * `user.id` default used by unrelated CLI or harness sessions. */ companion?: MachineCompanionConfig; /** Machine-local first-run onboarding state (ADR 2026-07-24). */ onboarding?: MachineOnboardingConfig; /** * Machine-local Bitwarden secret sync (ADR 2026-07-30b). Opt-in: when * `autoSync` is true, ZAM pushes machine-local secrets (mainly the server * DB token) into the learner's vault after unlock. Never the shared DB — * vault login is per machine / per install. */ bitwarden?: MachineBitwardenConfig; /** * Machine-local voice-mode preferences (ADR 2026-07-31). Never the * Turso-shared database: whether on-device speech is the right choice * depends on the hardware in front of the learner, so a phone's answer must * not be pushed onto their desktop. */ voice?: MachineVoiceConfig; /** Machine-local paths to existing personal/team/community workspaces. */ workspaces?: WorkspaceConfig[]; /** Machine-local id of the workspace currently active in this install. */ activeWorkspaceId?: string; /** App version that last ran the install verify/repair pass on this machine. */ lastRepairedVersion?: string; } interface MachineAgentConfig { /** True once first-run agent auto-connect ran on THIS machine (`--auto-once`). */ connectAutoDone?: boolean; } /** Bitwarden vault sync preferences for this install (ADR 2026-07-30b). */ interface MachineBitwardenConfig { /** * Master switch for the whole alpha vault feature. Absent or false means * off: Settings shows only the opt-in checkbox, and no vault code runs on * any path — in particular the dashboard never probes the vault or asks * for a master password. A learner who has not asked for this must never * meet it. */ enabled?: boolean; /** After a successful sync, keep pushing secret changes while unlocked. */ autoSync?: boolean; /** Preferred cloud region for CLI config (eu | us). */ region?: "eu" | "us"; /** ISO timestamp of the last successful vault seed/sync. */ lastSyncAt?: string; } /** * Machine-local first-run onboarding state (ADR 2026-07-24). Whether the * guided first-run flow has completed is per-install, not per-learner: a * paired phone or a second machine runs its own first-run. Deliberately never * the (Turso-shareable) database. */ interface MachineOnboardingConfig { /** True once the first-run flow reached its final page on THIS machine. */ done?: boolean; /** * Chosen start persona (ADR 2026-07-24 §2). Machine-local like the rest of * this section; when unset or invalid, readers fall back to the "free * learner" default (`private`). */ persona?: PersonaId; } /** * Machine-local Companion preferences (0.11.0 Phase 2). `selectedEvaluatorId` * is stored as a plain string, not the `EvaluatorId` union from * `src/vscode-extension/companion-evaluator.ts` — this module is part of the * AI-agnostic kernel, so it never imports harness/evaluator types; the CLI * layer validates the string against `isEvaluatorId` on read. */ interface MachineCompanionConfig { /** Persisted Companion learner selection — never the shared `user.id`. */ selectedUserId?: string; /** Persisted Companion evaluator selection (generic/fallback). */ selectedEvaluatorId?: string; /** Persisted explicit VS Code evaluator selection. */ selectedVscodeEvaluatorId?: string; /** Persisted explicit Antigravity evaluator selection. */ selectedAntigravityEvaluatorId?: string; /** * Persisted explicit VS Code language-model choice for the `vscode-lm` * evaluator adapter (0.11.0 Phase 3) — the model's `vscode.lm` id * (`LanguageModelChat.id`). Kept separate from `selectedEvaluatorId` * because choosing "vscode-lm" as the evaluator and choosing *which* * VS Code model it uses are two different decisions (ADR 2026-07-16 * §Decision 5: "an explicit model choice"). Machine-local only, like the * rest of this section — never inferred by picking the first result of * `selectChatModels` on every call. */ selectedVscodeModelId?: string; /** * Persisted explicit Antigravity model choice for the `vscode-lm` * evaluator adapter (0.11.0 Phase 3) — the model's `vscode.lm` id. */ selectedAntigravityModelId?: string; /** Collapsed state for the shared context bar, keyed by surface name. */ collapsed?: Record; } interface MachineVoiceConfig { /** * Which speech tier voice mode prefers on this machine. Values are the * `VoiceEnginePreference` union from `recall/voice-review.ts`; stored as a * plain string so this module stays free of recall imports. */ enginePreference?: string; } type MachineAiRole = "vision" | "recall" | "text" | "embedding"; type MachineApiFlavor = "chat-completions" | "anthropic-messages"; interface MachineProviderRecord { label?: string; url?: string; model?: string; apiFlavor?: MachineApiFlavor; apiKeyRef?: string; local?: boolean; runner?: string; } interface MachineRoleBinding { primary?: string; fallback?: string; } /** * Model capabilities in the unified registry (ADR 2026-07-12). `text` covers * every chat-completions job (recall coaching, curriculum import, translation); * `image`/`video` are the Observer vision paths; `stt`/`tts` are future audio. */ type ModelCapability = "text" | "embedding" | "image" | "video" | "stt" | "tts"; declare const ALL_CAPABILITIES: ModelCapability[]; type CapabilityFlags = Record; declare function emptyCapabilityFlags(): CapabilityFlags; /** * One endpoint in the ordered capability registry. Runtime selection walks the * list by `order` and picks the first entry that is user-enabled and probe- * detected for the requested capability (ADR 2026-07-12). */ interface ModelEntry { /** Stable id (ULID) for this config row. */ id: string; /** Human label shown in Settings. */ label: string; url: string; model: string; local: boolean; apiFlavor: MachineApiFlavor; /** Optional runner hint for local stacks (foundry, ollama, …). */ runner?: string; /** Credential ref into ~/.zam/credentials.json — never inline. */ apiKeyRef?: string; /** Sort key: lower = higher priority. */ order: number; /** User-selected capabilities (may only shrink after the first probe). */ capabilities: CapabilityFlags; /** Last successful metadata probe; drives the checkbox ceiling. */ detectedCapabilities: CapabilityFlags; /** ISO timestamp of the last probe; undefined until probed. */ probedAt?: string; /** * How ZAM reaches this model (ADR 2026-07-12a). Absent/"http" is the direct * HTTP path (local or cloud). "agent" delegates generation through a connected * agent harness named by {@link agentHarness}; `url`/`apiFlavor` are then * ignored. Pure config — the kernel never acts on it; the CLI's agent-llm * layer interprets it. */ transport?: "http" | "agent"; /** * Harness id (e.g. "claude-code") that backs an `agent`-transport entry. * Matches an `AgentHarnessId` in the CLI layer; stored as a plain string so * the kernel stays harness-agnostic. */ agentHarness?: string; /** * Optional reasoning effort for harnesses that accept it (e.g. Copilot * `--effort`). Pure config — interpreted by the CLI agent-llm adapters. * When absent, adapters pick a default from the model id. */ effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; } interface MachineAiConfig { /** @deprecated Legacy named endpoints; superseded by `models` (ADR 2026-07-12). */ providers?: Record; /** @deprecated Legacy role bindings; superseded by `models` (ADR 2026-07-12). */ roles?: Partial>; /** * Unified capability-based model registry (ADR 2026-07-12). An ordered list * that supersedes `providers` + `roles`; runtime selection walks it by * `order` and returns the first entry enabled and detected for a capability. */ models?: ModelEntry[]; } type WorkspaceKind = "personal" | "team" | "family" | "community" | "organization" | "custom"; type WorkspaceSourceControl = "github" | "azure-devops" | "git" | "none"; interface WorkspaceConfig { id: string; label?: string; kind: WorkspaceKind; path: string; sourceControl?: WorkspaceSourceControl; knowledgeScopes?: string[]; defaultAgent?: string; activeKnowledgeContext?: string; } /** Load ~/.zam/config.json. Returns an empty config if missing or unreadable. */ declare function loadInstallConfig(path?: string): InstallConfig; /** * Persist the install config, preserving any unrelated keys already on disk. * * Writes atomically: the JSON is written to a temp file in the same * directory (same volume, so the following rename is a single filesystem * operation) and then renamed over the target — the same handoff pattern * `writeUiIntent` uses for the UI-intent file (`src/cli/ui-intent.ts`). A * reader (or a process crash mid-write) can therefore never observe a * half-written `config.json`; the worst case is losing this one write, never * a torn/truncated file. `renameSync` replaces an existing destination on * both POSIX and Windows (libuv issues `MoveFileExW` with * `MOVEFILE_REPLACE_EXISTING` on Windows), so no unlink-first step is needed * in the common case — the fallback below only matters if some other * process (antivirus/indexer) transiently holds the destination open. */ declare function saveInstallConfig(config: InstallConfig, path?: string): void; /** * Load, mutate, and save the config as one cross-process-atomic step. * * Every setter in this module goes through here: the load happens *inside* * the lock, so a concurrent writer's change is read back rather than * overwritten. Returns whatever `mutate` returns. */ declare function updateInstallConfig(mutate: (config: InstallConfig) => T, path?: string): T; /** * This machine's install mode. Defaults to "developer" — the only historical * mode — so existing source/CLI installs keep their behavior. A packaged * "default" install writes mode explicitly at install time. */ declare function getInstallMode(path?: string): InstallMode; declare function setInstallMode(mode: InstallMode, path?: string): void; /** * How this copy was installed, used to pick the self-update mechanism. Falls * back to "developer" for developer mode and "direct" for an installed app * whose channel was not recorded. */ declare function getInstallChannel(path?: string): InstallChannel; declare function setInstallChannel(channel: InstallChannel, path?: string): void; declare function getMachineAiConfig(path?: string): MachineAiConfig; declare function saveMachineAiConfig(ai: MachineAiConfig, path?: string): void; /** Drop deprecated per-machine text bindings (text always follows recall). */ declare function ensureMachineProviderRolesSanitized(path?: string): void; /** Read the ordered model registry from `~/.zam/config.json` (`ai.models`). */ declare function getMachineAiModels(path?: string): ModelEntry[]; /** Persist the ordered model registry, preserving other `ai.*` keys. */ declare function saveMachineAiModels(models: ModelEntry[], path?: string): void; /** * Flatten legacy machine `providers` + `roles` into an ordered capability * registry. Each provider's capabilities are inferred from the roles that * pointed at it (`recall`/`text` → text, `vision` → image, `embedding` → * embedding). Order follows former role priority — primary then fallback across * recall, text, vision, embedding — with any unbound providers appended. Until * a probe runs, legacy bindings are authoritative, so `detectedCapabilities` * mirrors `capabilities` (ADR migration §4). Returns `null` when there is * nothing to migrate. */ declare function migrateMachineRolesToModels(ai: MachineAiConfig): ModelEntry[] | null; /** * One-time, idempotent migration of legacy machine `providers`/`roles` into the * ordered `ai.models` registry. A no-op once `ai.models` exists or when there is * nothing to migrate. Legacy `providers`/`roles` are preserved for now and only * removed in the ADR's Phase 4 cleanup. Returns the resolved registry. */ declare function ensureMachineAiModelsMigrated(path?: string): ModelEntry[]; /** * First-run agent auto-connect marker. Machine-local by design: the database * can be shared across machines (Turso), but which harnesses are installed * and configured is a property of this machine, so the marker must not travel. */ declare function getAgentConnectAutoDone(path?: string): boolean; declare function setAgentConnectAutoDone(done: boolean, path?: string): void; /** * Whether the guided first-run onboarding flow has completed on THIS machine * (ADR 2026-07-24). Machine-local, following the `agentConnectAutoDone` * precedent: the learning database can be shared across machines, but whether * a given install has been walked through first-run is a property of the * install, so the marker must not travel. */ declare function getOnboardingDone(path?: string): boolean; declare function setOnboardingDone(done: boolean, path?: string): void; declare function getBitwardenSyncConfig(path?: string): MachineBitwardenConfig; declare function setBitwardenSyncConfig(patch: MachineBitwardenConfig, path?: string): void; /** * Is the alpha vault feature switched on for this install? Off by default — * the learner has to tick the box in Settings first. */ declare function isBitwardenVaultEnabled(path?: string): boolean; /** * Turn the alpha vault feature on or off. * * Switching it off also stops auto-sync, so an unlocked session cannot keep * pushing secrets after the learner has said no. Existing `{$secret}` * references are left untouched: they are the learner's data, and * `zam credentials disconnect` is the deliberate way to resolve them back to * literals. */ declare function setBitwardenVaultEnabled(enabled: boolean, path?: string): void; /** Enable auto-sync after a successful first transfer, or turn it off. */ declare function setBitwardenAutoSync(enabled: boolean, path?: string): void; /** Clear Bitwarden linkage for this install (offboarding). */ declare function clearBitwardenSyncConfig(path?: string): void; /** * The start persona chosen during first-run onboarding (ADR 2026-07-24 §2). * Defaults to `private` ("free learner") when never chosen — the plan's * resolution of ADR open question 4 — and when the on-disk value is not a * known persona (hand-edited config), so callers always get a valid id. */ declare function getOnboardingPersona(path?: string): PersonaId; declare function setOnboardingPersona(persona: PersonaId | undefined, path?: string): void; /** * Machine-local Companion preferences (0.11.0 Phase 2). Defensively * normalizes whatever is on disk instead of trusting the raw JSON shape: a * hand-edited or partially-written `companion` section (wrong types, a stray * array) must fall back to sensible defaults rather than crash the `zam mcp` * process the Companion depends on for first paint. */ declare function getMachineCompanionConfig(path?: string): MachineCompanionConfig; /** Persist the Companion preferences, preserving other top-level config keys. */ declare function saveMachineCompanionConfig(companion: MachineCompanionConfig, path?: string): void; /** * Read this machine's voice-mode preference (ADR 2026-07-31). * * Returns `undefined` rather than a default when nothing is stored, so the * caller decides what the default is — the kernel's * `DEFAULT_VOICE_ENGINE_PREFERENCE` owns that, not the config file. */ declare function getMachineVoicePreference(path?: string): string | undefined; /** Persist the voice-mode preference, preserving other top-level config keys. */ declare function setMachineVoicePreference(preference: string | undefined, path?: string): void; /** * One batched Companion preference change. A key is only touched when * present on the update object — `"selectedUserId" in update` (not a plain * truthiness check), so `{ selectedUserId: undefined }` still clears the * field, mirroring `setCompanionSelectedUserId(undefined, ...)`. `collapsed` * merges into the existing per-surface map rather than replacing it, like * `setCompanionCollapsed`. */ interface MachineCompanionConfigUpdate { selectedUserId?: string; selectedEvaluatorId?: string; selectedVscodeEvaluatorId?: string; selectedAntigravityEvaluatorId?: string; selectedVscodeModelId?: string; selectedAntigravityModelId?: string; collapsed?: { surface: string; value: boolean; }; } /** * Apply a batch of Companion preference changes with one load and one save, * instead of calling the individual setters below in sequence (each of which * does its own load-apply-save). A write that touches the learner, the * evaluator, and the collapsed state all at once — e.g. * `writeCompanionContext` — does one read and one write here instead of * three. Fields absent from `update` are left exactly as they were; this is * the same merge behavior as the individual setters, just batched. */ declare function updateMachineCompanionConfig(update: MachineCompanionConfigUpdate, path?: string): MachineCompanionConfig; /** The persisted Companion learner, independent of the shared `user.id`. */ declare function getCompanionSelectedUserId(path?: string): string | undefined; declare function setCompanionSelectedUserId(userId: string | undefined, path?: string): void; /** The persisted Companion evaluator id (validated against `EvaluatorId` by callers). */ declare function getCompanionSelectedEvaluatorId(path?: string): string | undefined; declare function setCompanionSelectedEvaluatorId(evaluatorId: string | undefined, path?: string): void; declare function getCompanionSelectedVscodeEvaluatorId(path?: string): string | undefined; declare function setCompanionSelectedVscodeEvaluatorId(evaluatorId: string | undefined, path?: string): void; declare function getCompanionSelectedAntigravityEvaluatorId(path?: string): string | undefined; declare function setCompanionSelectedAntigravityEvaluatorId(evaluatorId: string | undefined, path?: string): void; /** The persisted explicit VS Code model choice for the `vscode-lm` adapter. */ declare function getCompanionSelectedVscodeModelId(path?: string): string | undefined; declare function setCompanionSelectedVscodeModelId(modelId: string | undefined, path?: string): void; /** The persisted explicit Antigravity model choice for the `vscode-lm` adapter. */ declare function getCompanionSelectedAntigravityModelId(path?: string): string | undefined; declare function setCompanionSelectedAntigravityModelId(modelId: string | undefined, path?: string): void; /** Collapsed state for every surface that has been explicitly set. */ declare function getCompanionCollapsed(path?: string): Record; declare function setCompanionCollapsed(surface: string, collapsed: boolean, path?: string): void; /** * Version stamp of the last install verify/repair pass. Machine-local: shims, * PATH entries, and companion extensions are properties of this machine, so * the marker must not travel through a shared database. */ declare function getLastRepairedVersion(path?: string): string | undefined; declare function setLastRepairedVersion(version: string, path?: string): void; declare function getConfiguredWorkspaces(path?: string): WorkspaceConfig[]; declare function saveConfiguredWorkspaces(workspaces: WorkspaceConfig[], path?: string): void; declare function getActiveWorkspaceId(path?: string): string | undefined; declare function setActiveWorkspaceId(id: string | undefined, path?: string): void; declare function getActiveWorkspace(path?: string): WorkspaceConfig | undefined; declare function upsertConfiguredWorkspace(workspace: WorkspaceConfig, path?: string): WorkspaceConfig[]; declare function removeConfiguredWorkspace(id: string, path?: string): WorkspaceConfig[]; /** * Best-effort detection of the file-sync provider a folder lives in, from its * path. Used only for friendly messaging ("this folder syncs via OneDrive — * good for moving snapshots between machines"), never for behavior. */ declare function detectSyncProvider(dir: string): string | null; declare function getActiveWorkspaceContext(path?: string): string | undefined; declare function setActiveWorkspaceContext(contextName: string | undefined, path?: string): boolean; interface InstallResult { success: boolean; message: string; } type LocalLLMRunner = "fastflowlm" | "ollama" | "generic"; /** A resolved way to install a tool: a human label and the command to run. */ interface InstallPlan { method: string; command: string; } interface OllamaDetectionOptions { platform?: NodeJS.Platform; homeDir?: string; commandAvailable?: (command: string) => boolean; pathExists?: (path: string) => boolean; } /** * Check if a command is executable on the system. */ declare function hasCommand(cmd: string): boolean; /** * Install FastFlowLM via winget on Windows. */ declare function installFastFlowLM(): InstallResult; /** * Install Ollama via Homebrew on macOS. */ declare function installOllama(): InstallResult; declare function resolveOllamaCommand(options?: OllamaDetectionOptions): string | undefined; declare function isOllamaInstalled(options?: OllamaDetectionOptions): boolean; /** * Prepare the recommended model after installing a local LLM runner. */ declare function prepareLocalModel(runner: LocalLLMRunner, model: string): InstallResult; /** * Pick how to install the opencode agent for the current machine. * * npm is preferred on every platform: ZAM already requires Node, and the * `opencode-ai` package pulls the correct native binary for Apple Silicon and * Windows on ARM — avoiding the bash-on-Windows and Homebrew-tap caveats. * Returns null when no automatic method is available (e.g. Windows without npm, * Scoop, or Chocolatey). */ declare function planOpenCodeInstall(env: { platform: NodeJS.Platform; hasNpm: boolean; hasBrew: boolean; hasScoop: boolean; hasChoco: boolean; }): InstallPlan | null; /** * Install the opencode agent (the default agent ZAM provisions). opencode reads * the AGENTS.md that `zam setup` writes, so it picks up the ZAM skill once both * are present. */ declare function installOpenCode(): InstallResult; /** * English names of the supported locales, for naming the answer language inside * a prompt. * * Deliberately free of runtime imports — `./locale.js` reaches for * `node:child_process` to detect the OS language, which the mobile frontend * cannot bundle. The type import below is erased at compile time. */ declare const LANGUAGE_NAMES: Record; /** * English name of the language to answer in, from any locale-ish string * ("de", "de-DE", "de_DE.UTF-8", a `navigator.language` value, null). * * Takes a loose string rather than `SupportedLocale` on purpose: the callers * are a database column, a pairing payload and a browser, none of which can * promise the narrow type. Unknown input falls back to English. */ declare function languageName(locale: string | null | undefined): string; type LocalAiHardware = "ryzen-ai" | "snapdragon-x" | "apple-silicon" | "discrete-gpu" | "unsupported"; type LocalAiAcceleration = "npu" | "gpu" | "none"; interface SystemProfile { os: "windows" | "macos" | "linux" | "unknown"; arch: "x64" | "arm64" | "unknown"; /** Backward-compatible AMD-specific detection; never true for Intel NPUs. */ hasRyzenNPU: boolean; hasSnapdragonX: boolean; hasAppleSilicon: boolean; /** Only hardware with an explicitly supported accelerated inference route. */ localAiHardware: LocalAiHardware; localAiAcceleration: LocalAiAcceleration; recommendedRunner: "fastflowlm" | "ollama" | "generic"; recommendedModel: string; } interface LocalAiHardwareFingerprint { platform: NodeJS.Platform; arch: string; processorName?: string; acceleratorNames?: string; gpuNames?: string; } /** * Recognize only hardware with an accelerated inference route ZAM can actually * drive. This answers "is there a supported accelerated route here", not "does * this machine contain an accelerator" — an NPU with no usable runtime and an * integrated GPU are both `unsupported`, because a route ZAM cannot drive is * indistinguishable, for the learner, from no route at all. * * NPU classifications win over a discrete GPU only because they are the * established routes; a machine with both keeps the behaviour it had before GPU * detection existed. */ declare function classifyLocalAiHardware(fingerprint: LocalAiHardwareFingerprint): LocalAiHardware; /** * Whether ZAM offers its guided local text and image setup on this hardware. * * CPU-only generation is fast enough for embeddings and too slow to review * with, so the guided path is withheld rather than handing the learner a local * model that makes them stop reviewing. Adding a model by hand stays possible. */ declare function supportsLocalGeneration(acceleration: LocalAiAcceleration): boolean; /** * Profile the active system hardware and software capabilities. */ declare function getSystemProfile(): SystemProfile; interface RepoPaths { personal: string | null; team: string | null; org: string | null; } /** * Resolve absolute paths for personal, team, and organization repositories. * Personal falls back to the active machine-local workspace if repo.personal is * not set. */ declare function getRepoPaths(db: Database): Promise; /** * Resolve a specific repo's path, or null if not configured. */ declare function resolveRepoPath(db: Database, type: "personal" | "team" | "org"): Promise; /** * Resolve paths to all existing "/beliefs" directories in the hierarchy, * sorted from most specific (personal) to most general (org). */ declare function resolveAllBeliefPaths(db: Database): Promise; /** * Resolve paths to all existing "/goals" directories in the hierarchy, * sorted from most specific (personal) to most general (org). */ declare function resolveAllGoalPaths(db: Database): Promise; /** * SHA-256 over UTF-8 text, without Node built-ins. * * The kernel hashes in two places that both have to run inside the mobile * WebView: embedding content hashes (`models/token-embedding.ts`, which decides * whether a token needs re-embedding) and snapshot checksums (`db/snapshot.ts`, * the local→server migration path). Both used `node:crypto`, which does not * exist in a WebView. * * Why a hand-rolled digest rather than Web Crypto: `crypto.subtle.digest` is * **async**, and `computeContentHash` is called from synchronous code all over * the kernel. Making it async would ripple through the embedding pipeline for * no gain. * * Output is byte-identical to `createHash("sha256").update(text, "utf8")`, * which is not cosmetic — a different digest would mark every stored embedding * stale and re-embed every library on the next search. `tests/kernel/sha256.test.ts` * pins that equivalence against `node:crypto`. */ /** SHA-256 of raw bytes, as a lowercase hex string. */ declare function sha256HexBytes(input: Uint8Array): string; /** SHA-256 of a UTF-8 encoded string, as a lowercase hex string. */ declare function sha256Hex(text: string): string; export { type ADOConfig, type ADOCredentials, AI_CAPABILITIES, AI_TIER_PREFERENCES, ALL_CAPABILITIES, ATOM_ID_PATTERN, type ActivityBucketLabelOptions, type ActivityPeriod, type AgentSkill, type AiCapability, type AiPlatform, type AiTier, type AiTierAvailability, type AiTierDecision, type AiTierPlan, type AiTierPreference, type AiTierReason, type AnalysisResult, type ApplySessionSynthesisInput, type ApplySessionSynthesisResult, type AssessPreconditionInput, type AssessPreconditionResult, type Assignment, BUILT_IN_SENSITIVE_MATCHERS, BUNDLED_CELLS, BUNDLED_TILES, type BloomLevel$1 as BloomLevel, type BonusCandidate, type BonusOptions, type BundledCellEnrolResult, type BundledCellInfo, type BundledCellStatus, type BurySiblingResult, type CapabilityFlags, type CaptureDecision, type CaptureDenialReason, type CaptureRequest, type Card, type CardDeletionImpact, type CardState$1 as CardState, type CascadeBlockResult, type CommandRecord, type CommandSequence, type ConfirmFoundationsResult, type ConnectionOptions, type CreateAgentSkillInput, type CreateAssignmentInput, type CreateGoalInput, type CreateKnowledgeContextInput, type CreateReviewInput, type CreateSessionInput, type CreateTokenInput, type CredentialCheckEntry, type Credentials, type CurriculumCardInput, type CurriculumScope, type CurriculumTopicCard, DEFAULT_ACTIVITY_WINDOWS, DEFAULT_AI_TIER_PREFERENCES, DEFAULT_OBSERVER_POLICY, DEFAULT_PERSONA_ID, DEFAULT_REVIEW_CONTEXT_MAX_CHARS, DEFAULT_STUDY_WORKLOAD, DEFAULT_VOICE_ENGINE_PREFERENCE, DEVICE_TIER_SUPPORT, type Database, type DatabaseProvider, type DatabaseTargetInfo, type DatabaseValue, type DeleteCardResult, type DeleteTokenResult, type DiscoveryOptions, type DomainCompetence, type EditorialState, type EmbeddedTokenRow, type EmbeddingCoverage, type EmbeddingStaleness, type EnrolBonusResult, type EvaluateInput, type EvaluateResult, type ExecuteReviewActionInput, type ExecutionContext, type FSRSParameters, type FoundationProposalInput, type FoundationSuggestion, type GetReviewActivityOptions, type Goal, type GoalFrontmatter, type GoalStatus, type GoalSummary, HOMEBREW_CASK, HandsFreeReviewController, type HybridScoredToken, type HybridSearchOptions, type ImageOcclusionShape, type ImportCurriculumResult, type ImportResult, type InstallChannel, type InstallConfig, type InstallKvtResult, type InstallMode, type InstallPlan, type InstallResult, type KnowledgeContext, type KvtAtom, type KvtPracticeItem, type KvtTile, LANGUAGE_NAMES, type ListTokensOptions, type LocalAiAcceleration, type LocalAiHardware, type LocalAiHardwareFingerprint, type LocalLLMRunner, type LogStepInput, type MachineAgentConfig, type MachineAiConfig, type MachineCompanionConfig, type MachineCompanionConfigUpdate, type MachineOnboardingConfig, type MachineProviderRecord, type MachineRoleBinding, type MachineVoiceConfig, type MaterialiseKvtResult, type ModelCapability, type ModelEntry, type MonitorEvent, type Neighborhood, type NeighborhoodToken, OBSERVER_POLICY_UNSET_HINT, OBSERVER_POLICY_VERSION, type ObservationRating, type ObserverConsent, type ObserverPolicy, type ObserverRetention, type ObserverScope, type ObserverSettingKey, type OllamaDetectionOptions, PERSONA_DESCRIPTORS, PRECONDITION_BURIED_REASON, PRECONDITION_HORIZON_DAYS, PRECONDITION_READY_REASON, PRECONDITION_STAGGER_DAYS, type ParsedActivityBucket, type PersonaContextSeedResult, type PersonaDescriptor, type PersonaId, type PersonaImportPath, type PersonalCard, type PostgresDatabaseOptions, type PreconditionCandidate, type PrepareSessionSynthesisInput, type Prerequisite, type PrerequisiteWithToken, type PromptInput, type PublishRevisionInput, type PublishRevisionResult, type PullForwardCandidate, type PullForwardOptions, type PullForwardResult, type QuestionSource, REVIEW_CONTEXT_CACHE_TTL_MS, type Rating, type RecallPrompt, type RemoteDatabaseOptions, type RepoPaths, type ResolvedCaptureTarget, type ResolvedReference, type ReviewActionResult, type ReviewActionType, type ReviewActivity, type ReviewActivityBucket, type ReviewContext, type ReviewFastCheck, type ReviewLog, type ReviewQueue, type ReviewQueueItem, type ReviewQueueOptions, type RevisionChanges, type RevisionImpact, type RevisionMateriality, type RunResult, SIDECAR_POLICY_FILE, SNAPSHOT_VERSION, STUDY_TIME_CAP_MS, STUDY_WORKLOAD_PRESETS, type SchedulingCard, type SecretBackend, type SecretRef, SecretResolutionError, type SecretResolutionReason, type Session, type SessionStep, type SessionSummary, type SessionSynthesisCandidate, type SessionSynthesisEvidence, type SessionSynthesisPreview, type SessionSynthesisRecord, type SidecarPrivacyPolicy, type SkillProposal, type SkillSource, type SnapshotManifest, type SourceProposalInput, type SplitProposalInput, type Statement, type StoredCredentials, type StoredSecret, type StudyWorkloadPreset, type StudyWorkloadSettings, type SuggestFoundationsOptions, type SupportedLocale, type SymbiosisMode, type SynthesisConfidence, type SystemProfile, TIER1_FIRST_RULE, type TextImportAction, type TextImportAssetInput, type TextImportCardInput, type TextImportCommitOptions, type TextImportCommitResult, type TextImportCounts, type TextImportDeckPreview, type TextImportDocument, type TextImportFormat, type TextImportMediaReference, type TextImportNotice, type TextImportPreview, type TextImportPreviewCard, type TextImportProgress, type Token, type TokenDeleteImpact, type TokenEmbedding, type TokenMedia, type TokenMediaKind, type TokenMediaSide, type TokenNeedingEmbedding, type TokenPattern, type TranslationKey, type TursoCredentials, UI_OBSERVATION_PROTOCOL_VERSION, type UiActionType, type UiApplicationContext, type UiCandidateToken, type UiEvidenceRef, type UiEvidenceType, type UiObservationKind, type UiObservationReport, type UiObservedAction, type UnblockResult, type UpdateActionKind, type UpdateCardInput, type UpdateDecision, type UpdateKnowledgeContextInput, type UpdateStep, type UpdateStepKind, type UpdateStudyWorkloadInput, type UpdateTokenInput, type UserSetting, type UserStats, VOICE_ENGINE_PREFERENCES, type VoiceAvailability, type VoiceCapability, type VoiceEngineDecision, type VoiceEnginePlan, type VoiceEnginePreference, type VoiceEngineReason, type VoiceEngineTier, type VoiceEvaluationSpeech, type VoiceLocale, type VoicePort, type VoiceReviewAdapter, type VoiceReviewCard, type VoiceTierAvailability, WINGET_PACKAGE_ID, type WorkItem, type WorkspaceConfig, type WorkspaceKind, type WorkspaceSourceControl, addPrerequisite, analyzeObservation, appendUiObservationReport, applySchemaAndMigrations, applySessionSynthesis, applySourceProposals, assessPrecondition, assignTokenToContext, bonusCandidates, buildAncestorMap, buildReviewQueue, buildTokenSlug, buildUiSynthesisCandidates, burySiblingCards, cascadeBlock, checkCredentials, classifyLocalAiHardware, clearADOCredentials, clearBitwardenSyncConfig, clearProviderApiKey, clearReviewContextCache, clearSecretBackends, clearTokenMaintenance, clearTursoCredentials, commitTextImport, compareVersions, computeContentHash, confirmCardSplit, confirmFoundations, confirmSourceImport, cosineSimilarity, countUserCardsForCurriculumTopic, createAgentSkill, createAssignment, createBitwardenBackend, createFSRS, createGoal, createKnowledgeContext, createToken, credentialsNeedVaultAccess, decideAiTier, decidePostCapture, decidePreCapture, decideUpdate, decodeEmbedding, deleteCardForUser, deleteCurriculumCardForUser, deleteKnowledgeContext, deleteSetting, deleteToken, deprecateToken, detachCardForUser, detectSyncProvider, detectSystemLocale, discoverSkills, distributeGlobalSkills, embeddingContentForToken, emptyCapabilityFlags, encodeEmbedding, endSession, enrolBonusAtom, enrolBundledCell, ensureCard, ensureDefaultSecretBackends, ensureMachineAiModelsMigrated, ensureMachineProviderRolesSanitized, ensureMonitorDir, ensureUiObserverDir, evaluateRating, executeReviewAction, exportSnapshot, extractTasks, extractTokenRefs, fetchActiveWorkItems, findBundledCellsForScope, findTokens, formatActivityBucketLabel, generateBashHooks, generateBashUnhooks, generateConceptFreeCue, generatePowerShellHooks, generatePowerShellUnhooks, generatePrompt, generateTokenSlug, generateZshHooks, generateZshUnhooks, getADOCredentials, getActiveWorkspace, getActiveWorkspaceContext, getActiveWorkspaceId, getAgentConnectAutoDone, getAgentSkill, getAllSettings, getAllSettingsDetailed, getAssignment, getBitwardenSyncConfig, getBlockedCards, getBundledCell, getBundledCellEnrolment, getBundledCellTile, getBundledCellsWithStatus, getCard, getCardById, getCardDeletionImpact, getCompanionCollapsed, getCompanionSelectedAntigravityEvaluatorId, getCompanionSelectedAntigravityModelId, getCompanionSelectedEvaluatorId, getCompanionSelectedUserId, getCompanionSelectedVscodeEvaluatorId, getCompanionSelectedVscodeModelId, getConfiguredWorkspaces, getDatabaseTargetInfo, getDefaultDbPath, getDependents, getDisplayTitle, getDomainCompetence, getDueCards, getEmbeddingCoverage, getGoal, getGoalTree, getInstallChannel, getInstallMode, getKnowledgeContextById, getKnowledgeContextByName, getLastRepairedVersion, getMachineAiConfig, getMachineAiModels, getMachineCompanionConfig, getMachineVoicePreference, getMonitorDir, getMonitorLogStats, getMonitorPath, getOnboardingDone, getOnboardingPersona, getPackageSkillPath, getPersonaDescriptor, getPreconditionCandidates, getPrerequisites, getProviderApiKey, getPullForwardCandidates, getRepoPaths, getReviewActivity, getReviewsForCard, getReviewsForUser, getRevisionImpact, getSecretBackend, getSessionSummary, getSessionSynthesisRecords, getSetting, getShortSlug, getStudyWorkloadSettings, getSystemProfile, getTokenById, getTokenBySlug, getTokenDeleteImpact, getTokenEmbedding, getTokenMedia, getTokenNeighborhood, getTokensBySourceLinkBase, getTursoCredentials, getUiObservationPath, getUiObserverDir, getUserStats, hasCommand, hasDeviceTier, heldAtomIds, importCurriculumCards, importSnapshot, injectShellHooks, installFastFlowLM, installKvtTile, installOllama, installOpenCode, interleave, invalidateCredentialsSnapshot, isAiPreferenceConfigurable, isAiTierPreference, isAwaitingRetest, isBitwardenVaultEnabled, isBundledCellInstalled, isObserverPolicyConfigured, isOllamaInstalled, isPersonaId, isSecretRef, isStudyWorkloadPreset, isUiObservationReport, isVoiceEnginePreference, isVoiceModeUsable, languageName, liftPreconditionBury, listAgentSkills, listAssignmentsByAssigner, listAssignmentsForLearner, listBundledCells, listContextsForToken, listEmbeddedTokens, listGoals, listKnowledgeContexts, listPersonalCards, listProviderApiKeyRefs, listSecretBackends, listTokens, listTokensNeedingEmbedding, listUserCardsForCurriculumTopic, loadADOConfig, loadCredentials, loadInstallConfig, loadStoredCredentials, logReview, logStep, looksLikeSecretUri, matchBuiltInSensitive, matchDenylist, matchesFilePath, materialiseKvtCards, migrateMachineRolesToModels, monitorLogExists, needsGenericCurriculumImport, nextLocalDay, normalizeLocale, normalizePath, openDatabase, openDatabaseWithSync, openPostgresDatabase, openReadOnlySqliteDatabase, openRemoteDatabase, pairCommands, parseActivityBucket, parseGoalFile, parseMonitorLog, parseObserverList, parseObserverPolicy, parseReviewFastCheck, parseSecretUri, parseSnapshot, parseSpokenRating, parseUiObservationLog, planLeavesDevice, planOpenCodeInstall, planUpdate, preconditionBuriedUntil, prepareLocalModel, prepareSessionSynthesis, presentFastCheck, previewTextImport, publishTokenRevision, publishTokenRevisionInTransaction, pullForwardCards, readMonitorLog, readUiObservationLog, reattachCardForUser, registerSecretBackend, removeConfiguredWorkspace, removePrerequisite, resetCardsForToken, resetCredentialsResolutionState, resolveAiCapabilityTier, resolveAiTierPlan, resolveAllBeliefPaths, resolveAllGoalPaths, resolveCredentials, resolveObserverPolicy, resolveOllamaCommand, resolveReference, resolveRepoPath, resolveReviewContext, resolveSecretUri, resolveVoiceEnginePlan, resolveVoiceLocale, runMigrations, saveConfiguredWorkspaces, saveCredentials, saveInstallConfig, saveMachineAiConfig, saveMachineAiModels, saveMachineCompanionConfig, searchTokensHybrid, secretRefFromUri, seedPersonaKnowledgeContext, serializeGoal, setADOCredentials, setActiveWorkspaceContext, setActiveWorkspaceId, setAgentConnectAutoDone, setBitwardenAutoSync, setBitwardenSyncConfig, setBitwardenVaultEnabled, setCompanionCollapsed, setCompanionSelectedAntigravityEvaluatorId, setCompanionSelectedAntigravityModelId, setCompanionSelectedEvaluatorId, setCompanionSelectedUserId, setCompanionSelectedVscodeEvaluatorId, setCompanionSelectedVscodeModelId, setInstallChannel, setInstallMode, setLastRepairedVersion, setMachineVoicePreference, setOnboardingDone, setOnboardingPersona, setProviderApiKey, setSetting, setStudyWorkloadSettings, setTokenMaintenance, setTursoCredentials, sha256Hex, sha256HexBytes, slugify, startSession, suggestFoundations, supportsLocalGeneration, syncObserverSidecarPolicy, t, toSidecarPrivacyPolicy, tokenMatchesCurriculumTopicScope, tursoVaultAccessPending, uiObservationLogExists, uiObservationTimeSpan, unassignTokenFromContext, unblockReady, unburySiblingCards, unregisterSecretBackend, updateCard, updateGoalStatus, updateInstallConfig, updateKnowledgeContext, updateMachineCompanionConfig, updateToken, upsertConfiguredWorkspace, upsertTokenEmbedding, verifySnapshot, withdrawAssignment, wouldCreateCycle, writeMonitorEvent };