/** * Auto-sync (Beta) remote-pull decisions. * * Pure helper that decides which remote keys to download, which local files * to delete (tombstones from another machine), and which to skip — given a * remote listing, the journal, and a set of paths currently flagged as * conflicts. Isolating the decision keeps the watch-mode poll loop in * sync-runner.ts trivial: list S3 → call `decideRemotePulls` → drive S3 + * filesystem from the result. * * Pairs with the TreeWatcher push path — together they implement the * bidirectional auto-sync the Settings toggle exposes. */ import type { RemoteFile } from "./s3.js"; import type { EntityContext, PullRecord, SyncJournal } from "./types.js"; import type { ExplicitGrant, MembershipSyncConfig } from "./vault-client.js"; import { type ApplyScopeShrinkResult, type ScopeShrinkPlan } from "./scope-shrink.js"; /** Minimal shape every entry in `skip` has — `key` is the only field * guaranteed to be populated. Remote-listing skips carry the full RemoteFile; * conflict-tombstone skips (no remote counterpart) carry only the path. */ export interface SkippedKey { key: string; } export interface RemotePullDecision { /** Remote files to download to disk. */ download: RemoteFile[]; /** * Relative paths of local files whose remote counterpart has been deleted * since the last sync. The watcher should remove them locally and drop * the journal entry. */ deleteLocal: string[]; /** * Entries left untouched this pass — either because the local journal * already matches the remote ETag (idempotent), the path is currently * flagged in the conflict store (auto-pull never clobbers conflicts), or * a remote tombstone arrived for a conflicting file (auto-pull never * deletes a file the user is mid-resolving). */ skip: SkippedKey[]; } export interface DecideRemotePullsInput { remoteFiles: RemoteFile[]; journal: SyncJournal; /** * Relative paths currently in the conflict store. Auto-sync skips these * entirely — neither downloads nor deletes — so the user's in-progress * conflict resolution can't be silently overwritten. */ conflictKeys: Set; /** * Journal keys intentionally retained by scope-shrink authorship guards * even though they are outside the current remote listing scope. */ protectedMissingKeys?: Set; } export declare function decideRemotePulls({ remoteFiles, journal, conflictKeys, protectedMissingKeys, }: DecideRemotePullsInput): RemotePullDecision; /** * Hard cap on coalesced prefixes per STS vend (US-001-D). The vault-service * `validateVendRequest` rejects `paths.length > 10`, so the engine MUST * either shard into multiple vends + ListObjectsV2 calls when the coalesced * grant set exceeds this OR fall back to a broad list + post-filter. */ export declare const VEND_PATH_CAP = 10; /** * Threshold above which the engine prefers a single broad ListObjectsV2 + * client-side post-filter instead of fanning out N vends. Tuned for the * US-001-B p99 finding (TBD live) — N coalesced prefixes <= 50 is cheaper as * vend-fanout (~5 STS calls); > 50 is cheaper as one broad list. */ export declare const POST_FILTER_THRESHOLD = 50; /** Bounded parallelism for vend fan-out (5 concurrent vends/list paginators). */ export declare const VEND_FANOUT_CONCURRENCY = 5; /** * Effective per-company sync scope, resolved from the membership's sync-config * + (if `shared`) the caller's explicit grants. Returned by * `resolveCompanyScope` and consumed by `pullCompany`. * * `strategy: "vend-fanout"` issues 1..N narrowed STS+ListObjectsV2 calls, * union'd. `strategy: "broad-postfilter"` issues one wide list and filters * client-side. `strategy: "all"` is the legacy syncMode='all' path. */ export interface CompanyScope { companyUid: string; syncMode: MembershipSyncConfig["syncMode"]; /** Coalesced prefix set. For `all`, this is the single company prefix. */ prefixSet: string[]; /** * Strategy chosen by `resolveCompanyScope` based on coalesced count vs * `VEND_PATH_CAP` and `POST_FILTER_THRESHOLD`. */ strategy: "all" | "vend-fanout" | "broad-postfilter"; } export interface ResolveCompanyScopeInput { companyUid: string; companyPrefix: string; syncConfig: MembershipSyncConfig; /** Required when `syncConfig.syncMode === 'shared'`. */ explicitGrants?: ExplicitGrant[]; } /** * Resolve the effective sync scope for one per-company leg. * * Decision table: * - `syncMode === 'all'` → strategy `all`, prefixSet [companyPrefix] * - `syncMode === 'shared'` → coalesce explicit grants. If count * ≤ VEND_PATH_CAP → `vend-fanout`. * If ≤ POST_FILTER_THRESHOLD → still * `vend-fanout` (sharded). Else * `broad-postfilter`. * - `syncMode === 'custom'` → coalesce customPaths, same decision * table as `shared`. * * Pure function. No network, no journal mutation. */ export declare function resolveCompanyScope(input: ResolveCompanyScopeInput): CompanyScope; /** * Split a coalesced prefix set into batches of at most `VEND_PATH_CAP` * prefixes each. Each batch maps to a single STS vend + ListObjectsV2 call. */ export declare function batchPrefixesForVend(prefixes: string[], cap?: number): string[][]; export interface ListRemoteForScopeInput { ctx: EntityContext; scope: CompanyScope; /** * Override for tests / alternative S3 surfaces. Defaults to the package's * own `listRemoteFiles`. Signature matches `(ctx, prefix?) => RemoteFile[]`. */ listFn?: (ctx: EntityContext, prefix?: string) => Promise; /** * Override for tests to vend a per-batch narrowed EntityContext. Default: * reuse `ctx` (which the orchestrator is expected to have already vended * appropriately for the scope). The full per-batch STS vend wiring will * land in US-006 along with the CLI. */ vendForBatchFn?: (ctx: EntityContext, paths: string[]) => Promise; } /** * List remote objects in scope, applying the chosen strategy: * - `all` — one broad ListObjectsV2 under the company prefix. * - `vend-fanout` — one ListObjectsV2 per coalesced batch (≤ VEND_PATH_CAP), * bounded parallel, results union'd. The caller is * responsible for vending narrowed credentials when * this path is taken (`vendForBatchFn`). * - `broad-postfilter`— one broad ListObjectsV2 + client-side filter * against `scope.prefixSet`. * * Dedup by key so multi-batch overlaps don't double-download. */ export declare function listRemoteForScope(input: ListRemoteForScopeInput): Promise; export interface PullCompanyInput { ctx: EntityContext; journal: SyncJournal; hqRoot: string; scope: CompanyScope; /** Set of conflict-store keys to forward to `decideRemotePulls`. */ conflictKeys?: Set; /** Honor the operator override on dirty orphans (US-005 contract). */ forceScopeShrink?: boolean; /** * The caller's own Cognito `sub` for the scope-shrink authorship guard — a * scope shrink never prunes content the caller authored. Defaults to the * cached session sub (`resolveCallerSubFromCache()`); pass explicitly when * the runner already decoded its idToken claims. */ callerSub?: string; /** Listing override hook — see `ListRemoteForScopeInput.listFn`. */ listFn?: ListRemoteForScopeInput["listFn"]; vendForBatchFn?: ListRemoteForScopeInput["vendForBatchFn"]; /** Time injector for tests; defaults to real wall clock. */ now?: () => Date; } export interface PullCompanyResult { /** Effective scope used. */ scope: CompanyScope; /** Remote files listed under the scope (post-dedup, post-filter). */ remoteFiles: RemoteFile[]; /** Pure download/delete/skip decision from `decideRemotePulls`. */ decision: RemotePullDecision; /** Scope-shrink plan computed before listing. */ scopeShrinkPlan: ScopeShrinkPlan; /** Applied scope-shrink action (counts). `null` when no shrink was needed. */ scopeShrinkApplied: ApplyScopeShrinkResult | null; /** Pull record appended to `journal.pulls`. */ pullRecord: PullRecord; /** Tombstones GC'd at the start of this leg. */ tombstonesGcd: number; } /** * Per-company sync leg — the engine half of `pullAll` for ONE company. * * Flow: * 1. GC expired tombstones (cheap; bounds journal growth). * 2. Resolve last-pull scope (or `["companyPrefix"]` if no record exists). * 3. Build scope-shrink plan + abort on dirty orphans (unless force). * 4. Apply scope-shrink (delete clean orphans, tombstone entries). * 5. List remote under current scope (vend-fanout / broad-postfilter / all). * 6. Compute download/delete/skip via `decideRemotePulls`. * 7. Append a `PullRecord` capturing the actual `syncMode` + `prefixSet`. * * Step 6 returns the decision plan — the actual S3 GETs and FS writes * remain in the CLI layer (`hq-cli/src/commands/cloud.ts`'s `pullAll`), * which threads conflict detection + remoteEtag stamping on completion. * US-006 wires this orchestrator into the CLI. */ export declare function pullCompany(input: PullCompanyInput): Promise; //# sourceMappingURL=remote-pull.d.ts.map