/** * Pure helper: coalesce a set of S3 key prefixes into the minimal set that * covers the same paths with no redundancy. * * Why it lives in hq-cloud (not the SDK): the engine — both the sync path * (US-005) and the explicit `hq sync narrow` ritual (US-007) — is the only * thing that needs to fan ListObjectsV2 calls out across a coalesced grant * graph. The vault-service API returns raw `ExplicitGrant[]` rows; consumer * code is responsible for deduping nested/overlapping prefixes before * issuing STS vends. * * Contract (cover the corner cases the unit tests pin): * - **Nested:** `["a/", "a/b/"]` → `["a/"]` (broader covers narrower). * - **Overlapping (siblings):** `["a/b/", "a/c/"]` → both kept. * - **Identical:** `["a/", "a/"]` → `["a/"]` (dedup). * - **Case-sensitive:** `["A/", "a/"]` → both kept (S3 keys are case-sensitive). * - **Empty input:** `[]` → `[]`. * - **Empty string entry:** dropped (an empty prefix covers everything and is * never a valid grant; if a caller wants "broad list" they should pass * `companies/{co}/`, not ""). * - **Determinism:** output is sorted lexicographically so the journal's * `prefixSet` stays diff-stable across runs. * * Coverage rule: exact-file entries cover only themselves. Directory prefixes * (ending in `/`) and bare glob prefixes cover descendants with literal * `startsWith` matching. When a string is ambiguous (e.g. `foo` from `foo*`, * which must be a prefix, not an exact file), the coalesced `prefixSet` stores * a small in-band marker in the string itself so the match type survives * `[...prefixSet]` copies and JSON persistence. */ export type ScopePrefixMatch = "exact" | "prefix"; export interface ScopePrefixEntry { prefix: string; match: ScopePrefixMatch; } export type ScopePrefixInput = string | ScopePrefixEntry; export declare function toScopePrefixEntries(prefixSet: readonly ScopePrefixInput[]): ScopePrefixEntry[]; export declare function pathToScopePrefix(path: string): ScopePrefixEntry; export declare function coalescePrefixes(prefixes: readonly ScopePrefixInput[]): string[]; /** * Predicate companion: does any prefix in `prefixSet` cover `path`? * * Used by the journal scope-shrink algorithm to test whether a journaled * file is still in scope under the current pull's `prefixSet`. Exact-file * entries match only themselves; directory/glob prefixes use `startsWith`. */ export declare function isCoveredByAny(path: string, prefixSet: readonly ScopePrefixInput[]): boolean; /** * Intersect two scope prefix sets: the coalesced set of paths covered by BOTH * inputs. For each cross pair the NARROWER entry survives when the broader one * covers it (`["a/"] ∩ ["a/b/"]` → `["a/b/"]`); disjoint pairs contribute * nothing (`["a/"] ∩ ["c/"]` → `[]`). Exact-file entries intersect a directory * prefix to the exact file, and another exact entry only on equality. * * Used by targeted sync (`--scope-path`) to narrow a run's remote listing / * push walk to the requested subtree WITHOUT widening beyond the resolved * membership scope. NOTE the literal contract: `[]` on either side yields `[]` * ("nothing is in scope") — a caller that means "no narrowing requested" must * short-circuit before calling. */ export declare function intersectPrefixSets(a: readonly ScopePrefixInput[], b: readonly ScopePrefixInput[]): string[]; /** * Directory companion to `isCoveredByAny`: should the push/pull walk DESCEND * into directory `relDir` (company-relative, no leading slash) given the * granted `prefixSet`? * * A file uses plain `startsWith` (`isCoveredByAny`), but a directory must be * descended whenever it COULD contain an in-scope file — which is true in two * directions: * - the directory sits INSIDE a granted prefix (`knowledge/sub` under * `knowledge/`), or * - a granted prefix sits INSIDE the directory (`knowledge/` under the * company root `""`, or `knowledge/README.md` under `knowledge/`). * Without the second case the walk would refuse to descend into `knowledge/` * to reach a `knowledge/README.md` exact-file grant, scoping the whole tree * to nothing. * * The directory is normalized to a trailing-slash form so the `startsWith` * comparisons line up with coalesced prefixes (which are themselves either * trailing-slash dir prefixes or exact-file keys). The empty string (company * root) always descends when any prefix exists. */ export declare function isDirInScope(relDir: string, prefixSet: readonly ScopePrefixInput[]): boolean; /** * Normalize a raw ACL grant `path` into a COMPANY-RELATIVE prefix suitable * for `coalescePrefixes` + `isCoveredByAny` (which do literal `startsWith` * matching against company-relative `RemoteFile.key`s). * * Real-world grant paths are inconsistent — observed forms in production: * - `*` → everything (company-wide glob) * - `companies//design-pack/*` → full-anchored + trailing glob * - `companies//knowledge/README.md` → full-anchored exact file * - `/data/vyg/old-meetings/*` → slug-anchored + trailing glob * - `data/vyg/*` → company-relative + trailing glob * - `company.yaml` → company-relative exact file * * The engine's keys are ALWAYS company-relative (`design-pack/x`, * `company.yaml`). So we: * 1. strip a leading `companies//` or `/` anchor, and * 2. fold the ACL glob suffix into a `startsWith`-friendly prefix: * - `*` (bare) → `""` (covers everything) * - `foo/bar/*` or `foo/bar*` → `foo/bar/` / `foo/bar` * - `foo/bar/` (dir) → unchanged * - `foo/bar.md` (exact) → unchanged (an exact key is its own prefix) * * Without this, `coalescePrefixes(grants.map(g => g.path))` produced prefixes * with trailing `*` (which never `startsWith`-match a real key) and * full/slug-anchored prefixes (which never match company-relative keys) — so * `syncMode: shared` would download nothing and prune everything the caller * actually has access to. See the live grant dump in the hq-pro vault. */ export declare function grantPathToScopePrefix(grantPath: string, slug: string): ScopePrefixEntry; export declare function grantPathToPrefix(grantPath: string, slug: string): string; //# sourceMappingURL=prefix-coalesce.d.ts.map