/**
 * One-shot legacy→umbrella migration (T-CHAT-AS-TERMINAL CAT.1d).
 *
 * The legacy chat stack keyed routing + inbox by per-cwd `project_uuid`:
 *   - `~/.opensquid/projects/<uuid>/chat-routing.json` (one per project_uuid)
 *   - `~/.opensquid/projects.json` registry (`{ projects: { <uuid>: { id,
 *     last_seen_path, ... } } }`)
 *   - `~/.opensquid/projects/<uuid>/inbox/<platform>.jsonl` (per-uuid inbox)
 *
 * The new stack keys EVERYTHING by UMBRELLA (`src/channels/routing.ts`):
 *   - one authoritative `~/.opensquid/channels.json`
 *   - `~/.opensquid/umbrellas/<id>/inbox/<platform>.jsonl`
 *
 * This module synthesizes the umbrella config FROM the legacy per-project
 * files and copies the inbox rows into the umbrella keyspace, NON-DESTRUCTIVELY
 * (the legacy files are left intact; src.legacy deletion is CAT.8).
 *
 * Grouping algorithm (the da96≡0742 mirror collapse):
 *   Every uuid is grouped by its telegram destination `(report_channel,
 *   report_topic_id)`. Two mirror uuids that report to the SAME (channel,
 *   topic) — the da96≡0742 hack — fall into ONE group, hence ONE umbrella, and
 *   their `last_seen_path`s become that umbrella's `members`. The umbrella id
 *   is a slug of the shared project `id` (from the registry).
 *
 * DM rule (locked, invariant #5): ANY `inbound_dm_user_ids` found on ANY
 * project go to the TOP-LEVEL `general.telegram.dm_user_ids` — never onto a
 * project umbrella. DMs route to general, deliberately, even though raumpilates
 * carries the DM allowlist today.
 *
 * Everything pure where possible; fs only at the edges (`synthChannelsConfig`
 * reads, `writeChannelsConfig` + `migrateInboxData` write).
 *
 * Imports from: node:fs/promises, node:path, ../runtime/paths, ./routing.
 * Imported by: CAT.1d CLI (`opensquid chat migrate`) + tests.
 */
import { type ChannelsConfig } from './routing.js';
/** A single project's telegram routing block (`chat-routing.json`). */
interface LegacyTelegramRouting {
    report_channel?: string;
    report_topic_id?: number;
    inbound_dm_user_ids?: string[];
}
interface LegacyChatRouting {
    telegram?: LegacyTelegramRouting;
}
/**
 * Slugify a project `id` into a stable umbrella id: lowercase, every run of
 * non-`[a-z0-9]` collapses to a single `-`, leading/trailing `-` trimmed.
 * `RaumPilates-FE` → `raumpilates-fe`, `loop` → `loop`. Never returns the
 * reserved `general` (the caller substitutes a fallback for empties).
 */
export declare function slugifyUmbrellaId(id: string): string;
/** Parse `report_channel` ("telegram:<chat_id>") → the bare chat_id, or null. */
export declare function chatIdFromReportChannel(reportChannel: string | undefined): string | null;
interface LegacyProject {
    uuid: string;
    id: string;
    lastSeenPath: string;
    routing: LegacyChatRouting;
}
/**
 * Pure core of `synthChannelsConfig`: given the loaded legacy projects, group
 * them into umbrellas + the general row. Exposed for testing without fs.
 *
 * Returns both the `ChannelsConfig` AND the umbrella-id → legacy-uuids map so
 * `migrateInboxData` can copy each umbrella's rows from its member uuids.
 */
export declare function synthFromLegacy(projects: LegacyProject[]): {
    config: ChannelsConfig;
    uuidsByUmbrella: Record<string, string[]>;
};
/**
 * Load every legacy project from disk: the registry (`projects.json`) is the
 * authority for `id` + `last_seen_path`; each uuid's `chat-routing.json`
 * supplies the telegram destination. A uuid present in the registry but with
 * no routing file is loaded with an empty routing (it forms no umbrella but is
 * still visible to the caller).
 */
export declare function loadLegacyProjects(): Promise<LegacyProject[]>;
/**
 * Read the legacy per-project routing + registry and synthesize the umbrella
 * `ChannelsConfig`. The mirror-collapse + DM→general rules are in
 * `synthFromLegacy`; this is the fs wrapper.
 */
export declare function synthChannelsConfig(): Promise<ChannelsConfig>;
/**
 * Resolve an umbrella id → its legacy uuids (the inverse of the synth
 * grouping). Re-derives from disk so callers don't have to thread the synth
 * result. Returns [] for an unknown umbrella id.
 */
export declare function legacyUuidsForUmbrella(umbrellaId: string): Promise<string[]>;
/**
 * Write the synthesized `channels.json` to `channelsConfigPath()` atomically
 * (tmp + rename). By default refuses to overwrite an existing file (idempotent
 * — a second migrate run is a no-op); pass `{ force: true }` to overwrite.
 * Returns whether the file was actually written + the path.
 */
export declare function writeChannelsConfig(cfg: ChannelsConfig, opts?: {
    force?: boolean;
}): Promise<{
    written: boolean;
    path: string;
}>;
/**
 * Copy every umbrella's inbox rows from its member uuids' legacy per-project
 * inbox into the umbrella keyspace. NON-DESTRUCTIVE (the source files are never
 * touched) + idempotent (re-running adds no duplicate rows — dedup is seeded
 * from the destination). Dedup keys: inbox rows by `(platform, id)`, ack rows
 * by `message_id`. Returns the per-umbrella count of NEWLY-copied inbox rows.
 */
export declare function migrateInboxData(cfg: ChannelsConfig): Promise<{
    copied: Record<string, number>;
}>;
export interface MigrateResult {
    config: ChannelsConfig;
    /** Whether channels.json was written (false = already present, no --force). */
    configWritten: boolean;
    configPath: string;
    /** Per-umbrella count of newly-copied inbox rows. */
    copied: Record<string, number>;
}
/**
 * Run the full migration: synthesize the umbrella config, write it (honoring
 * `force`), then copy the inbox data. READ-ONLY w.r.t. the legacy keyspace.
 */
export declare function runMigration(opts?: {
    force?: boolean;
}): Promise<MigrateResult>;
/** Discover every uuid directory under `projects/` (diagnostic helper; the
 *  registry is the authority but this surfaces orphan dirs to the operator). */
export declare function listProjectUuids(): Promise<string[]>;
export {};
//# sourceMappingURL=migrate.d.ts.map