import type { ScanEntryKind } from '../utils/scan.js'; import { type QualityProfile } from '../utils/visual-profile.js'; /** Path-granular facts about the regions a directory scan could not observe. */ export interface SyncScanCoverage { unreadableDirs: { dirPath: string; code: string; }[]; depthLimitedDirs: string[]; skippedSymlinks: string[]; } /** * Everything one run could not observe: the scan facts plus the files whose * bytes were never read because they exceed the configured size limit. Every * path here is an unobserved prefix, so the rows under it are protected from * prune. */ export interface SyncCoverage extends SyncScanCoverage { /** Files skipped by `hashFile` for exceeding the configured size limit. */ oversizedFiles: string[]; } /** One bounded directory scan: the supported files found plus its coverage facts. */ export interface SyncScanResult extends SyncScanCoverage { files: string[]; } /** * One stored chunk row (or row group) of the database manifest. `filePath` is * the verbatim stored spelling — the only value valid for deletion. A `null` or * absent `contentHash` marks the row hashless, which makes its file dirty. * `visualProfile` is the raw stored value, unvalidated: the vocabulary check * belongs to the planner, not to the storage projection. */ export interface SyncManifestRow { filePath: string; contentHash?: string | null; visualProfile?: string | null; } /** One supported file found on disk, with the hash of its current bytes. */ export interface SyncDiskFile { filePath: string; contentHash: string; } /** How a sync invocation was addressed, after validation and classification. */ export type SyncRequest = { kind: 'roots'; } | { kind: 'directory'; path: string; } | { kind: 'file'; path: string; }; /** * Result of classifying a requested path on disk. Anything other than * `directory` or `file` is refused before the path is read: the union is the * walker's own {@link ScanEntryKind} plus `missing`, so a path a caller names and * a path the walk discovers pass the identical predicates. */ export type SyncPathKind = ScanEntryKind | 'missing'; /** Re-ingest one disk file, then drop the other stored spellings of its key. */ export interface SyncUpsertAction { /** Verbatim disk path handed to `ingestFile`. */ filePath: string; /** Verbatim stored spellings of the same comparison key, excluding `filePath`. */ staleStoredPaths: string[]; /** * The profile this file must be re-ingested with, resolved once here so the * CLI and MCP adapters cannot reach different conclusions. `null` means * normal text ingestion, and is the only value a non-PDF ever carries. */ visualProfile: QualityProfile | null; } /** Remove every stored spelling of one comparison key that left the disk. */ export interface SyncPruneAction { storedPaths: string[]; } export interface SyncPlan { upserts: SyncUpsertAction[]; /** Files whose stored content identity already matches the disk bytes. */ skipped: number; prunes: SyncPruneAction[]; } export interface SyncPlanInput { roots: readonly string[]; dbPath: string; /** Configured database/cache prefixes that must never be pruned. */ excludePaths: readonly string[]; platform: NodeJS.Platform; request: SyncRequest; diskFiles: readonly SyncDiskFile[]; dbRows: readonly SyncManifestRow[]; coverage: SyncCoverage; /** * Explicit per-invocation visual request. Present means every eligible PDF in * scope is desired at this profile regardless of what is stored, which is also * what repairs conflicting or unreadable stored state. Absent means each PDF * inherits its own recorded profile. */ visualProfile?: QualityProfile | undefined; } /** The one controlled error a failed run exposes. */ export interface SyncError { message: string; /** The file, root, or requested path a failure is attributable to. */ filePath: string | null; } /** Append an attributable path only when the underlying message does not already contain it. */ export declare function formatSyncError({ message, filePath }: SyncError): string; export interface SyncCounters { upserted: number; skipped: number; empty: number; /** Comparison keys removed from the index. Counts files, not rows, and is not part of `completed`. */ pruned: number; } /** One stored spelling per pruned comparison key, so a count of N reports N paths. */ interface PrunedPaths { prunedPaths: string[]; } export interface SyncExecutionResult extends SyncCounters, PrunedPaths { error: SyncError | null; } export interface SyncResult extends SyncCounters, PrunedPaths { /** Scanner facts as data. Formatting and reporting belong to the adapters. */ coverage: SyncCoverage; error: SyncError | null; } /** How one planned file must be ingested: run-level images, per-file profile. */ export interface SyncIngestOptions { /** `true` stores images for this file, from the current invocation alone. */ images: boolean; /** The planner's resolved profile; `null` means normal text ingestion. */ visualProfile: QualityProfile | null; } /** Mutating collaborators, injected by the CLI and MCP adapters. */ export interface SyncExecutor { /** * Parse, chunk, embed, build vectors, then delete-and-insert for this one * file, returning the inserted chunk count. Returning `0` must leave the * store untouched: the executor relies on that to keep a zero-chunk file's * prior rows and hash intact. */ ingestFile(filePath: string, options: SyncIngestOptions): Promise; /** Delete the rows of exactly one stored path spelling. */ deleteExactPath(filePath: string): Promise; optimize(): Promise; } /** Everything {@link runSync} needs from the outside world. */ export interface SyncCollaborators extends SyncExecutor { /** * Canonical form of the requested path — its parent chain resolved through * symbolic links, the final component verbatim — or `null` when that chain * cannot be resolved. Injected because this module performs no filesystem * access; both adapters supply `canonicalizeRequestedPath` from `utils/scan.ts`. */ canonicalizeRequestedPath(path: string): Promise; /** * Classify the requested path WITHOUT reading it, applying the walker's collect * predicates (`classifyRequestedPath` in `utils/scan.ts`) so both surfaces * refuse the same paths. */ classifyPath(path: string): Promise; /** * Bounded scan of one root. Deliberately takes no scope predicate: a * scope-pruned directory is reported in none of the coverage arrays, so a * scope filter here would make unobserved regions invisible and prune unsafe. */ scanDir(rootPath: string): Promise; /** * Hash the file's current bytes, or return `null` to decline reading it because * it exceeds the configured size limit. A declined file is left out of the disk * manifest and recorded in `coverage.oversizedFiles`, which protects its stored * rows from prune — omitting it without that record would make it look deleted. */ hashFile(filePath: string): Promise; /** Every stored chunk row's verbatim path and hash, for the whole table. */ loadDbManifest(): Promise; } export interface RunSyncInput { /** Configured roots in the `resolve()`-only spelling the DB keys live in. */ roots: readonly string[]; /** * The same configured roots, canonicalized (realpath'd) — the security domain * the requested path's canonical form is checked against. Both adapters already * hold this list: the MCP server as `baseDirs`, the CLI as * `config.baseDirs.baseDirs`, each the counterpart of its `roots` entry. */ canonicalRoots: readonly string[]; dbPath: string; excludePaths: readonly string[]; platform: NodeJS.Platform; /** Omitted means "every configured root". */ requestedPath?: string | undefined; /** `true` stores images for files already selected as new or changed. */ images?: boolean | undefined; /** Explicit visual request, as on {@link SyncPlanInput}. Only the CLI supplies one. */ visualProfile?: QualityProfile | undefined; collaborators: SyncCollaborators; } /** * Decide the skip, upsert, and prune actions for one sync run. * * A key is pruned only when every one of four conditions holds: inside the * requested scope, absent from disk, outside the excluded and managed paths, * and outside every unobserved prefix. Dropping any one protects the rows. * * Throws when an eligible PDF's stored profiles admit no single answer; the * caller must surface that before any mutation runs. */ export declare function planSync(input: SyncPlanInput): SyncPlan; export declare function executeSyncPlan(plan: SyncPlan, executor: SyncExecutor, images?: boolean): Promise; /** * Run one full sync. Counters and coverage facts are plain data — the caller * owns printing and exit status. A run with nothing to do calls neither * `ingestFile` nor `optimize`, so a true no-op never loads the model. */ export declare function runSync(input: RunSyncInput): Promise; export {}; //# sourceMappingURL=sync.d.ts.map