/** * Reconcile successfully pulled cloud companies into the local manifest. * * The personal-vault leg also carries `companies/manifest.yaml`, so this runs * only after the complete fanout has settled. That makes the locally * materialized cloud companies authoritative for their own manifest entries * without discarding entries the personal vault already had. * * Two layers live here on purpose: * * - `reconcileManifest(hqRoot, targets)` is the pure reconciler. It takes * already-resolved targets and does nothing but validate slugs, merge the * fields it owns, and (only when something would actually change) write the * manifest. * - `reconcileCompanyManifest(options)` is the sync-runner adapter. It decides * WHICH targets are eligible (personal mode, fanout completion, on-disk * materialization, and — only when an entity resolver is injected — live * entity resolution) and then delegates the mutation. * * The adapter keeps its name and options-object shape because `sync-runner` * injects it through `deps.reconcileManifest`. That injection seam is an * options-object callback with a different shape from the pure reconciler and * must not be conflated with it — renaming the adapter would break the seam. */ import { type EntityInfo } from "./vault-client.js"; export interface ManifestReconcileTarget { uid: string; slug: string; /** * Carried by the fanout plan, which already resolved the entity to build the * target. Present so the adapter can write a manifest entry with zero extra * API calls; still optional because a plan built from a degraded lookup keeps * the uid as its only identifier. */ name?: string; bucketName?: string; /** * Entity type/status as captured AT PLAN TIME. These are the liveness * evidence for the no-`getEntity` path and are checked with exactly the same * predicate the `getEntity` path applies to a freshly fetched entity. Both * are optional in the type and REQUIRED in practice: a target that carries * neither is "unknown", and unknown is rejected. */ entityType?: string; entityStatus?: string; personalMode?: boolean; } /** * Reported when the injected entity resolver fails in a way that is NOT an * ordinary "this entity is gone" lookup outcome. The target is skipped either * way; this exists so a systematic resolver failure cannot present as the * silent "joiners never get manifest entries" bug this module was written to * fix. */ export interface ManifestReconcileDiagnostic { event: string; message: string; err: unknown; context: Record; } export interface ManifestReconcileOptions { hqRoot: string; targets: readonly ManifestReconcileTarget[]; completedCompanySlugs: ReadonlySet; /** * OPTIONAL entity resolver. * * When supplied, every eligible target is re-verified against a freshly * fetched entity ({@link isLiveCompany}) before it may contribute an entry. * * When omitted — the sync-runner path — each eligible target's entry is * resolved from the plan-carried `uid`/`slug`/`name`/`bucketName`, costing * zero API calls. This does NOT discharge the liveness requirement: the same * type/status assertion still runs, against `entityType`/`entityStatus` * captured by `buildFanoutPlan` at the moment it fetched the entity (see * {@link isLiveTargetSnapshot}). The only thing given up is freshness — the * evidence is from earlier in the same run rather than from a second fetch. * * A target carrying no liveness fields is REJECTED. Absence is treated as * "unknown", never as "fine": the degraded lookup path in `buildFanoutPlan` * produces exactly that shape, and admitting it would let an unverified * entity into a vault-synced file. */ getEntity?: (uid: string) => Promise; /** * Optional sink for non-fatal problems. Never affects control flow. */ reportDiagnostic?: (diagnostic: ManifestReconcileDiagnostic) => void; } /** * A target that has already been resolved to the values the manifest should * carry. `name` and `bucketName` are optional: a company that lacks either is * still a real, routable company and must get its `cloud_uid` entry. */ export interface ManifestReconcileEntryTarget { slug: string; uid: string; bucketName?: string; name?: string; } /** * `added` — slugs newly inserted into `companies:`. * `updated` — slugs whose pre-existing entry had a field changed. * `skipped` — slugs rejected outright (unsafe slug, non-record existing entry). * `written` — whether the manifest file was actually rewritten. */ export interface ManifestReconcileResult { written: boolean; added: string[]; updated: string[]; skipped: string[]; } /** * Merge resolved targets into `companies/manifest.yaml` and report what moved. * * Only the three fields this module owns are considered: `cloud_uid` (always * written — it is the routing key), plus `name` and `bucket_name` when the * target carries them. Omitting a field means "do not set this key"; it never * means "delete it", so a manifest value already on disk survives a target that * has nothing to say about it. * * The skip-if-unchanged comparison is load-bearing rather than an optimization. * `yaml.dump` cannot round-trip comments, so an unconditional rewrite strips the * manifest header — and because `manifest.yaml` rides the personal vault, that * rewrite propagates to every machine and drives a recurring sync conflict loop. * When nothing would change, the file is left byte-for-byte identical and its * mtime untouched. */ export declare function reconcileManifest(hqRoot: string, targets: readonly ManifestReconcileEntryTarget[]): ManifestReconcileResult; /** * Atomically merge live, successfully pulled company targets into * `companies/manifest.yaml`. Targets that are personal, incomplete, not * materialized on disk, or not a verifiably active company are ignored. * * Every one of those filters applies on BOTH paths. The paths differ only in * where the liveness evidence comes from: a fresh `getEntity` fetch when one is * injected, or the type/status the fanout plan captured when it is not. */ export declare function reconcileCompanyManifest(options: ManifestReconcileOptions): Promise; //# sourceMappingURL=manifest-reconcile.d.ts.map