/** * The revision-1 backfill for legacy bundled loops (hasna/apps#1724 §11 P3). * * A loop that predates bundles is a row with no version history: nothing in * `loop_revisions` names it, and — for command and agent loops — no object * anywhere holds its definition. `POST /v1/loops/{id}/versions` closes that * gap one loop at a time from a client-supplied bundle; the one-shot * `loops-serve backfill-revisions` job closes it for every existing loop at * once, from the rows themselves. * * For every loop that can be named and has no revision yet, the job appends * revision 1 exactly the way the push route would: * * 1. project the row to `loop.json` (`loopToDefinition`), * 2. pack it as a one-file bundle (`loop.json`, mode 0600), * 3. write the archive + manifest + `latest.json` through the artifact * storage, and * 4. record the ledger row with `createLoopRevision` — the same append-only * insert the push route uses, allocating version 1 under the row lock. * * Naming follows the bundle-name contract, never an invention: a loop that * already has a `bundle_name` keeps it; otherwise the loop's `name` is used * when it satisfies `assertBundleName` and no OTHER loop in the tenant already * holds it. Unnameable loops are skipped and reported, never guessed. * * Idempotence and resumability fall out of the ledger: a loop that already has * any revision is skipped, so rerunning the job after a crash (or after a * partial `--limit` run) is a no-op for everything it already backfilled. The * job is written against the storage CONTRACT rather than raw SQL so the * SQLite store and the hosted Postgres control plane run the same code. * * Prompt policy matches the push route: an agent prompt is carried in the * bundle (it is the only round trip), `carriesPrompt` is derived server-side, * and anything that fails the credential scan is refused — a skipped loop, * never a published secret. */ import type { Loop } from "../../types.js"; import type { LoopStorageContract } from "../storage/contract.js"; import { BundleArtifactStorage } from "./artifact-storage.js"; /** Processing cohorts, ordered by the P3 plan: hygiene-flagged script-backed loops first. */ export type RevisionBackfillCohort = "scriptBacked" | "command" | "agent" | "workflow"; /** The P3 processing order. The script-backed cohort is the portability-priority set. */ export declare const REVISION_BACKFILL_COHORT_ORDER: readonly RevisionBackfillCohort[]; /** * Which backfill cohort a loop belongs to. * * `undefined` means the loop's target cannot be bundled at all and the loop is * skipped as unclassifiable rather than guessed at. */ export declare function classifyRevisionBackfillCohort(loop: Loop): RevisionBackfillCohort | undefined; /** Why a loop was not (or not yet) backfilled. */ export type RevisionBackfillSkipReason = "archived" | "alreadyBackfilled" | "nameUnsafe" | "nameTaken" | "containsSecret" | "unclassifiable"; export interface RevisionBackfillAttempt { loopId: string; loopName: string; cohort?: RevisionBackfillCohort; outcome: "created" | "wouldCreate" | "skipped"; skipReason?: RevisionBackfillSkipReason; bundleName?: string; version?: number; bundleDigest?: string; archiveSha256?: string; carriesPrompt?: boolean; detail?: string; } export interface RevisionBackfillContext { storage: LoopStorageContract; artifacts: BundleArtifactStorage; /** Structural S3/local key segment; the sqlite store has no tenant, tests pass a label. */ tenantId: string; /** True: classify, name and digest, but write nothing anywhere. */ dryRun: boolean; author: string; reason: string; sourceStation?: string; sourceAgent?: string; now?: () => Date; } export interface RevisionBackfillResult { attempts: RevisionBackfillAttempt[]; created: number; wouldCreate: number; skipped: Record; } /** * Every non-archived loop in the tenant, in P3 processing order. * * `listLoops` already excludes archived loops by default; an archived loop * keeps whatever history it has and is not a backfill candidate. */ export declare function collectRevisionBackfillCandidates(storage: LoopStorageContract): Promise; /** * Backfill ONE loop: revision 1 with its current definition. * * Every skip is decided BEFORE anything is written, so a skip can never be * rolled back out from under an earlier attempt in the same transaction. Any * error after the first write is unexpected and propagates — the caller's * transaction boundary decides whether earlier attempts in the same batch * survive it. */ export declare function attemptRevisionBackfill(loop: Loop, ctx: RevisionBackfillContext): Promise; /** * Attempt a full pass over a pre-collected candidate list, in order. * * The caller decides how many loops share one transaction (the hosted job * passes one batch per tenant-scoped transaction) and when to stop (the * `--limit` accounting lives in the caller, which counts `created` + * `wouldCreate` the way a dry run reports a real one). */ export declare function runRevisionBackfill(ctx: RevisionBackfillContext, loops: readonly Loop[]): Promise;