import type { AccessLogEntry, ConnectionId, MigrationProgress, MigrationStraggler, PlatformActorId, Scope, ScopeId, TenantId, DrainedEvent } from '@substrat-run/contracts'; import type { ExportBreak, ManifestExports, EdgeHealthReport, ExportReadInput, ExportedBatch, ImportBatch, ImportResult, ImportState, ManifestImports, WantedEvent } from '@substrat-run/contracts'; import type { ExecutorDrainReport, FetchLike, HostAdmin, ReconcileOutcome, ScopeHost, SweepRunInput, TelemetryPruneReport } from './scope-host.js'; import type { FindingPruneReport } from './findings.js'; /** * A connector's reconcile sweep — the unit `runPlatformSweep` calls per live * connection of a given provider (`sweepScriveReconciliations` is one). * * INJECTED, never imported: the driver — and the deployment that runs it — * depends on no specific connector. A connector contributes `{ [provider]: its * sweeper }` to the registry, and a provider with no entry is simply skipped. */ export interface ConnectorSweeper { (host: ScopeHost, connectionId: ConnectionId, opts: { fetch: FetchLike; }): Promise; } /** * Where drained access-log rows go — Tier 2 (K-24), the durable place a row lives * once it has left the directory. * * INJECTED for the same reason `ConnectorSweeper` is: the kernel names the seam and * knows nothing about the target. The control plane binds an R2 implementation * (`createR2AccessLogSink`); a self-host may bind a file, an object store, or nothing * at all — and nothing at all is a supported answer, it just means the log is never * pruned. * * `ship` MUST be durable before it resolves. Everything downstream — the `drainedAt` * stamp, and the prune the stamp licenses — treats a resolved `ship` as proof the * evidence survives outside the directory. A sink that buffers and returns early turns * a retention policy back into data loss. */ export interface AccessLogSink { /** * Ship one batch and return an opaque reference to where it landed (an object key, * a URL — the sweep only records it). The reference is what makes the admin-log row * actionable: "these rows left, and here is where they are". */ ship(entries: AccessLogEntry[]): Promise<{ ref: string; }>; } /** * Where a scope's drained domain events go — Tier 2 proper (#1334, master-plan * §5.3: "domain events → Pipelines → Iceberg on R2, queried via R2 SQL"). * * The `AccessLogSink` twin, and injected for the same reason: the kernel names the * seam and knows nothing about the target. The control plane binds an R2 * implementation; a self-host may bind a file, an object store, or nothing at all * — and nothing at all is supported, it just means the outbox is never drained. * * `ship` MUST be durable before it resolves, and the stakes are higher here than * for the access log: a resolved `ship` is what licenses stamping `drainedAt`, and * the whole promise of Tier 2 is EXACT history. A sink that buffers and returns * early turns "the lake has everything" into a claim nobody can check. * * Unlike the access log, a drained event is NOT pruned from the scope afterwards. * Pruning the outbox is its own decision — consumers, replay and `readHistory` all * read it — so this seam only ever ships and stamps. What the stamp buys today is * knowing what has left; what it licenses later is a retention policy that does * not exist yet. */ export interface EventSink { /** * Ship one scope's batch and return an opaque reference to where it landed. The * scope is passed alongside because a lake partitions by it — the sink decides * how, the kernel only reports the reference back. */ ship(scope: { tenantId: TenantId; scopeId: ScopeId; }, events: DrainedEvent[]): Promise<{ ref: string; }>; } export interface PlatformSweepOptions { /** * The durable sweep record (#1232): called once per unit outcome — each * connection swept/skipped/failed, each schedule run — with the signals stamp * the frame can honestly carry. UNSET ⇒ the pass records nothing, exactly as * before the seam existed. Sync fire-and-forget: a recorder that throws must * never sink the pass, so callers hand in a closure that swallows its own * errors (the ops-failure recorder's shape, worker.ts). */ recordSweepRun?: (entry: SweepRunInput) => void; /** The platform actor the enumeration reads run as (`listScopes`/`listConnections`). */ actor: PlatformActorId; /** Sanctioned egress handed to each connector sweeper. */ fetch: FetchLike; /** provider slug → its reconcile sweeper. A connection whose provider is absent is skipped. */ sweepers: Record; /** Max scope drains / connection sweeps in flight at once. Default 8. */ concurrency?: number; /** * Also drain each active scope's due executor deliveries — the retry driver * (connections.md §2.1), which has been landed and equally lacks a caller. * Default `true`; set `false` to sweep only connectors. */ drainRetries?: boolean; /** * Also drain each active scope's pending PLATFORM INTENTS (platform-intents.md) — the requests a * vertical enqueued via `ctx.requestPlatform`. Injected like `reapScopeFn`: the kernel cannot * reach a vertical's scope DO (it lives in the vertical's own deployment), so the control plane * supplies a fn that pulls + executes each intent over the vertical's `/internal` surface. UNSET * (the default) skips the phase; returns per-scope counts, summed into `platformRequestTotals`. */ drainPlatformRequestsFn?: (tenantId: TenantId, scopeId: ScopeId) => Promise<{ drained: number; done: number; failed: number; pending: number; /** No deployment could be reached for this scope, so nothing was drained or counted (#1840). */ unreachable?: boolean; /** Pending intents nothing could run or settle — their stored id is not an id (#1637). */ unsettleable?: { count: number; ids: (string | null)[]; }; }>; /** * Re-run one scope's provision in the vertical's own deployment (#1172). * * Injected like `drainPlatformRequestsFn` and for the same reason: the kernel cannot * reach a vertical (its scope DO lives in the vertical's deployment), so the control * plane supplies the fn that goes over `/internal/reconcile`. UNSET skips the phase. * * What makes the phase necessary: a vertical's `onProvision` runs ONCE per scope, at * install. Anything the vertical mints for itself there — a service principal, a site * registration — therefore never reaches an install that predates it. The new code * deploys, and the thing it depends on was never created. Comparing the version whose * code RUNS on the scope (`runningVersionOf`, #1653) against the one its provision last * ran against is how the platform sees that, and this fn is how it fixes it. * * `expected` is the version the phase will record on success — the one running on the * scope. A fn whose deployment for the scope runs a DIFFERENT version (its resolver fell * back from the serving script to the bound version's, say) must throw rather than * resolve: the receipt would otherwise name a hook that never ran, and the scope would * read as repaired while the code it runs was never provisioned for. A throw leaves it * unmarked and counted `failed`, which is where it belongs. * * Resolves `'unsupported'` when the vertical answered that it implements no reconcile * (a 501 from `/internal/reconcile`). That is neither a success nor a failure: no * receipt is written, since nothing ran, and it is counted apart from `failed` so a * vertical that simply lacks the route does not drown the refusals somebody must read. * * #2185: resolves a {@link ReconcileOutcome} when it can say what the reconcile finished, and * the receipt records it. `void` records the receipt with its retirements not finished, so the * scope stays behind and is reconciled again next pass. */ reconcileScopeFn?: (tenantId: TenantId, scopeId: ScopeId, expected: string) => Promise; /** * The most scopes the #1172 phase reconciles in ONE pass (#1653). Default * {@link PROVISION_RECONCILE_BATCH}. * * A promote of a LISTED vertical puts every install of it behind at once — each one is * another tenant's scope, and a popular vertical has thousands. Reconciling them all in * one tick is how a cron becomes an incident, so the phase takes at most this many and * the rest wait for the next pass (`deferred`). `0` reconciles nothing: the phase still * counts who is behind, which makes it the pause switch. */ provisionReconcileBatch?: number; /** * Where the #1172 window starts, as a number in [0, 1) — `Math.random` unless given. * * The behind scopes are ordered by id and the pass takes a contiguous window of * `provisionReconcileBatch` from a random starting point, wrapping. A FIXED start would * let installs that fail on every pass hold the same slots forever and starve every * healthy one behind them; a random start gives each behind scope a chance of * batch/behind per pass however many of the others keep failing. Injectable so that * property is tested rather than hoped for. */ provisionReconcileRng?: () => number; /** * Also reap expired snapshots (preview-and-snapshots.md §3/§9): any FORK * (`forkedFrom` set) whose `expiresAt` has passed is hard-deleted via * `deleteSnapshot`. Default `true` — an expiry is only ever present because the * snapshot's creator asked for one, so sweeping it is honoring that request, and * `deleteSnapshot` refuses non-forks regardless. Set `false` to skip the phase. */ gcSnapshots?: boolean; /** * How the GC phase deletes one expired fork. Defaults to `host.deleteSnapshot` — * right whenever the host and the scope's storage share a deployment (self-host, * a vertical's own sweep). The CONTROL-PLANE cron overrides it with the * orchestrated delete (§9): wipe the fork's storage in the vertical deployment * that actually holds it, then the in-process delete for directory row + audit. */ deleteSnapshotFn?: (tenantId: TenantId, scopeId: ScopeId) => Promise; /** * Also reap long-archived scopes (control-plane.md §4.4): any scope in `archived` * whose `archivedAt` is older than this many days has its DO storage wiped and moves * to `reaped` (Cloudflare never GCs a Durable Object, so nothing else frees it). The * directory row survives as a tombstone. UNSET (the default) skips the phase entirely * — auto-reap is opt-in, and irreversible, so a deployment must name a retention * window before the sweep will ever delete an app's data. A scope with a null * `archivedAt` (archived before this column shipped) is never auto-reaped — it has no * knowable age — and must be reaped by hand. */ reapArchivedAfterDays?: number; /** * How the reap phase reaps one archived scope. Defaults to `host.admin.reapScope` — * right when host and storage share a deployment (self-host, a vertical's own sweep). * The CONTROL-PLANE cron overrides it with the orchestrated reap (§4.4): wipe the * scope's storage in the vertical deployment that actually holds it (its DO is * CP-less), then the in-process reap for the directory transition + audit. */ reapScopeFn?: (tenantId: TenantId, scopeId: ScopeId) => Promise; /** * Also drain the staff access log to Tier 2 and prune what it drained (K-24, * control-plane.md §4.4). UNSET (the default) skips the phase entirely, exactly like * `reapArchivedAfterDays`: a deployment that has named no durable target must not have * its evidence deleted on a schedule, and "no sink configured" is a supported posture * (the log then grows unbounded — stated, not silent). * * Bounded per pass by `accessLogBatch`, not run to exhaustion: a sweep tick has a * budget, and an unbounded first pass over a year of rows is how a cron becomes an * incident. The window closes over several ticks instead of one. */ accessLogSink?: AccessLogSink; /** * Where each scope's domain events are shipped (#1334). UNSET ⇒ no scope is * drained, exactly as before the seam existed — the same "absent is a supported * answer" shape `accessLogSink` and `recordSweepRun` already have. */ eventSink?: EventSink; /** Events drained per scope per pass. Default 200. */ eventDrainBatch?: number; /** * Deliver cross-vertical events (#1705). For every active primary scope whose running code * imports from another vertical, resolve that vertical's instance in the SAME tenant, read * its outbox after the watermark the consumer holds, and hand the batch to the consumer. * * UNSET skips the phase, and opting in is deliberate. On the shared control plane this host's * own namespace is the module-less placeholder, so the default reach would construct an empty * DO per active scope per tick and report a fleet with no edges. The control plane opts in * with a reach over `/internal`. A self-host, whose host IS the deployment, opts in with `{}`. */ crossVertical?: CrossVerticalOptions; /** Rows shipped (and pruned) per pass. Default 500. */ accessLogBatch?: number; /** Rows of each telemetry table pruned per pass (#1632). Default `TELEMETRY_PRUNE_BATCH`. */ telemetryBatch?: number; /** * Sample scope database sizes into the stored storage gauge (#1524). UNSET (the default) * skips the phase, as does a host without `recordScopeStorage`/`listScopeStorageAttempts`. * Injected for the reason `drainPlatformRequestsFn` is: on the hosted plane a scope's DO * lives in its vertical's deployment, and only the control plane can reach it there. */ storageGauge?: StorageGaugeSweepOptions; /** * Also reap tenants past their grace window (control-plane.md §4.8): any tenant in * `deleting` whose `deletingAt` is older than this many days has every scope reaped * and its PII/config directory rows cleared, moving it to `reaped` (a tombstone). * UNSET (the default) skips the phase entirely — like `reapArchivedAfterDays`, the * reap is irreversible, so a deployment must name a retention window before the sweep * will ever destroy a tenant's data. A tenant with a null `deletingAt` (flipped before * the column shipped) is never auto-reaped and must be reaped by hand. */ reapDeletingAfterDays?: number; /** * How the reap phase reaps one due tenant. The default composes the existing * `reapScopeFn` seam: for each of the tenant's non-reaped scopes it archives (if * needed) then reaps via `reapScopeFn`, and finally clears the directory via * `host.admin.reapTenant`. Because it rides `reapScopeFn`, the CONTROL-PLANE cron * gets the orchestrated per-scope reap (wipe each DO in its vertical deployment) for * free just by setting `reapScopeFn` — it need not override this. Supplied only to * fully replace the tenant-reap behavior. */ reapTenantFn?: (tenantId: TenantId) => Promise; /** * Also reconcile migrations (kernel-design §5.3, #49): walk the directory for * live scopes behind this host's frontier or failed, wake each with * `migrateScope`, back off between retries of a failing scope, and report * "release N: X/Y migrated, P pending, F failed". Default `true`; the phase * also quietly skips itself on a host that predates `migrateScope`. */ reconcileMigrations?: boolean; /** * Also run each vertical's DUE recurring schedules (#383): for every module that * declares `schedules`, enumerate its live scopes and invoke each due operation * under a system actor via `runDueSchedules`. Default `true`; the phase * feature-detects `host.registeredSchedules` and quietly yields `schedules: null` * on a fake or pre-#383 host, and does nothing when no module declares any. */ runSchedules?: boolean; /** * Backoff between retries of a FAILED scope, keyed off the directory's * consecutive-attempt count: `baseDelayMs * 2^(attempts-1)`, capped at * `maxDelayMs`, jittered ±20% (the same curve executor retries use). There is * no max-attempts: a broken migration heals only by a patched forward release, * so the sweep retries at the capped cadence until one arrives — flagging past * `migrationFlagThreshold` is the human signal, not a stop. Defaults: 60s * base, 1h cap. Never-attempted stragglers are always due — waking them IS the * sweep's job. */ migrationBackoff?: { baseDelayMs?: number; maxDelayMs?: number; }; /** Consecutive failures before a scope is flagged/paged. Default 3. */ migrationFlagThreshold?: number; /** * The paging seam (§5.3 "pages past a threshold"): called once per pass with * the failed scopes at/over the threshold, only when there are any. The * deployment wires it to whatever alerting it has; the same list always rides * `report.migrations.stragglers` (`flagged: true`), so ignoring the callback * loses nothing but immediacy. A throwing pager is caught — it must never * sink the pass it is reporting on. */ onMigrationsFlagged?: (flagged: MigrationStraggler[]) => void; } /** * What the migration-reconciliation phase did in one pass: the fleet progress * AFTER the pass (the §5.3 "release N: X/Y migrated…" numbers, shared shape * with the ops-console view), plus the pass's own work. */ export interface MigrationSweepReport extends MigrationProgress { /** `migrateScope` calls made this pass (stragglers that were due). */ attempted: number; /** Stragglers this pass brought to the frontier. */ repaired: number; /** Failed scopes skipped this pass — their backoff window has not elapsed. */ deferred: number; /** Attempts where this host had nothing pending — the scope's modules run in another deployment. */ noops: number; } /** * Which family a `_substrat_schedule_state` row belongs to (#1288) — the two of * `sweepRunKind`'s three names that a SCOPE can hold gating state for. (`connector` * is the third and is deliberately absent: a connection is swept host-wide and its * state lives nowhere in a scope.) */ export type ScheduleStateKind = 'schedule' | 'freshness'; /** * The platform sweep's per-scope gating state (#383), as both adapters build it. * * Shared rather than spelled twice because `lint:spine-ddl` can compare the copies * only where they are DDL a `KERNEL_DDL` executes — and this table is also rebuilt * by `SCHEDULE_STATE_REBUILD` below, on a store that predates the key. One * definition is what keeps the rebuilt shape and the created shape the same shape; * the gate then holds each adapter to including it. */ export declare const SCHEDULE_STATE_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_schedule_state (\n -- #1288: WHICH of the two families this row belongs to, named with the same two\n -- words _substrat_sweep_runs already records its entries under:\n -- * 'schedule' -- keyed by the operation (module/verb). last_run_at and\n -- last_status are when that operation RAN here and how it ended.\n -- * 'freshness' -- keyed freshness: (#1232). last_run_at and\n -- last_status are when the evaluator last RECORDED a verdict for that event\n -- type and what it was. Nothing ran; the row gates what the sweep records.\n kind TEXT NOT NULL,\n schedule_op TEXT NOT NULL,\n last_run_at TEXT,\n last_status TEXT,\n -- #1525: the invocation this row's last run happened in, or null. Null is a fact\n -- here, not a gap: a 'freshness' row never invokes anything (the evaluator only\n -- records a verdict), a 'schedule' row a pass SKIPPED carries no fresh call to\n -- attribute and keeps whatever it already held, and a row written before this\n -- column existed predates the possibility. Only a due schedule's own invoke\n -- mints one and writes it here, in the same call that stamps it onto whatever\n -- the operation emits \u2014 see runDueSchedules in each adapter.\n invocation_id TEXT,\n -- #2096: a purge horizon's lap in progress (entity-trash.ts, PurgeLap) -- where its\n -- next pass resumes in the bin, and how many purges failed earlier in the lap. Only a\n -- 'schedule' row of a purge schedule holds one, and only mid-lap: NULL is the start\n -- of a lap, which is also the honest reading of every row written before the column.\n purge_cursor TEXT,\n -- The key LEADS with kind, and that is the point of #1288 rather than a tidy-up.\n -- Until it did, the two families were told apart by the spelling of one column:\n -- a freshness key could never LOOK like an operation (an event type has passed\n -- contracts' eventType regex -- lowercase ns.verb, no colon, no slash), but the\n -- other direction was convention only, because scheduleSpec.operation is\n -- z.string().min(1). A module declaring a schedule literally named\n -- \"freshness:orders.placed\" shared the evaluator's row and nothing refused it:\n -- each write clobbered the other's verdict, and the sweep read back whichever\n -- ran last. With kind in the key those are two rows that cannot meet.\n PRIMARY KEY (kind, schedule_op)\n );\n"; /** * The `kind` a pre-#1288 row belongs to, derived from its `schedule_op` — the backfill's one * rule, shared by `SCHEDULE_STATE_REBUILD` below and by a restore of a dump taken before the * column (`spineRowsInsert`, #1883), so the two cannot file the same row differently. */ export declare const SCHEDULE_STATE_KIND_OF_OP = "CASE WHEN substr(schedule_op, 1, 10) = 'freshness:' THEN 'freshness' ELSE 'schedule' END"; /** * `_substrat_schedule_state`, rebuilt with its #1288 key on a store created before * it. Create-copy-drop-rename, because `kind` joins the PRIMARY KEY and SQLite * cannot widen a key in place — the same shape the directory's `ensureIdentityKey` * uses, and detected the same way (from `sqlite_master.sql`, which DO SQLite serves * and `PRAGMA` does not, so both adapters migrate by one strategy). * * The new table is `SCHEDULE_STATE_DDL` under a temporary name, so the rebuilt shape * cannot drift from the created one. * * The backfill derives `kind` from the `freshness:` prefix because that prefix IS how * the two families were told apart until now: every row the evaluator ever wrote * carries it, and no operation name in existence does. `substr(...) = 'freshness:'` * rather than `LIKE`, which is case-insensitive over ASCII in SQLite and would file a * schedule named `FRESHNESS:x` under the evaluator. * * Keys are copied VERBATIM, prefix included. What changes is which rows can coexist, * not what any row says — so a deployment rolled back to code that looks a freshness * key up under its old name still finds it. * * **Run these inside the adapter's transaction API**, which is what both callers do — * `db.transaction` on the pure side, `ctx.storage.transactionSync` on the DO side. * Un-wrapped, a stop between the CREATE and the DROP leaves the scratch table behind * and the NEXT wake dies on `table _substrat_schedule_state_new already exists`, which * is a scope that cannot open; a stop between the DROP and the RENAME is worse and * quieter, because the next wake's `CREATE TABLE IF NOT EXISTS` puts an EMPTY table * of the new shape in place, the detection below then reads it as already migrated, * and every copied row stays orphaned in the scratch table. Atomically, neither state * is reachable — which is why there is no recovery path here to go with them. * * The leading `DROP TABLE IF EXISTS` is belt to that braces, not the fix: it costs one * statement and makes the rebuild idempotent against a scratch table left by anything * below the transaction (a torn copy of the file). A restore cannot carry one in: since * #1883 it refuses a spine table the kernel does not build. * * A restore never reaches this rebuild either: it builds the table from the current DDL and * derives `kind` for a legacy dump's rows with `SCHEDULE_STATE_KIND_OF_OP`. This runs only * for a store that already holds the old table when it wakes. */ export declare const SCHEDULE_STATE_REBUILD: string; /** * Whether a store's `_substrat_schedule_state` already carries the #1288 key, read * off the `sql` column of `sqlite_master`. `false` means the rebuild is due. */ export declare function scheduleStateHasKind(tableSql: string): boolean; /** * What the recurring-schedule phase did in one pass (#383), summed across every * module's live scopes. `null` on `PlatformSweepReport` means the phase was * disabled or the host predates it — distinct from a report of all zeros, which is * "ran, nothing was due." */ export interface ScheduleSweepReport { /** Scopes `runDueSchedules` ran on (had ≥1 declared schedule). */ scopes: number; /** Due schedules whose operation ran successfully. */ fired: number; /** Schedules skipped this pass — still inside their cadence window. */ skipped: number; /** Due schedules whose operation failed — recorded, stepped over. */ failed: number; } /** What the #1172 phase did — see `reconcileScopeFn`. */ export interface ProvisionReconcileReport { /** * Scopes whose running version differed from the one their provision last ran against * this pass. Always `reconciled + failed + unsupported + deferred`. */ behind: number; /** Of those, the ones whose reconcile succeeded and were marked. */ reconciled: number; /** Of those, the ones whose reconcile threw. They stay behind and retry next pass. */ failed: number; /** * Of those, the ones whose vertical implements no reconcile (a 501). Unmarked like a * failure, so they are asked again next pass; counted apart so they cannot pass for one. * `scopeIds` is capped at {@link PROVISION_RECONCILE_REPORTED_IDS}; `count` is exact. */ unsupported: { count: number; scopeIds: string[]; }; /** Of those, the ones past this pass's `provisionReconcileBatch`. Next pass's work. */ deferred: number; } /** The #1172 phase's per-pass bound when `provisionReconcileBatch` is unset (#1653). */ export declare const PROVISION_RECONCILE_BATCH = 50; /** How many `unsupported` scope ids one report carries — the count stays exact. */ export declare const PROVISION_RECONCILE_REPORTED_IDS = 50; /** * A scope the platform may treat as the REAL install, rather than a copy of one (#1172, * #1653). The directory is the only oracle for this, and it takes BOTH tests, not one. * * A fork is an archive or a preview of somebody else's data, and re-provisioning one * runs the vertical's install-side hook against a copy, minting a second set of whatever * it mints — or, since #1656, putting a copy of production on a sweeper that reaps and * assigns. But a CLEAN-ROOM preview (#509) is an empty scope with no source to copy, so * it carries `kind: 'preview'` and NO `forkedFrom` — the reap sweep keys on `kind` for * exactly that reason. Filtering on lineage alone would let those through, which is the * one shape of scope where the hook's effects are least wanted. */ export declare function isPrimaryScope(scope: Pick): boolean; /** * `isPrimaryScope` over a raw directory row, in the snake_case both adapters store (#2005) — * so a reader holding a row asks the same one rule rather than restating it. No row is not * primary: the answer that runs effects with platform authority is the one to be proven. */ export declare function isPrimaryScopeRow(row: { kind: string | null; forked_from: string | null; } | undefined): boolean; /** * Why an effect a NON-primary scope asked for was settled without running (#2005). A fork, * a snapshot and a preview of either kind are inert: they cause no outbound effects, so * whatever they queue — a platform intent, an executor or connector delivery — is journaled * as not executed, with this reason, rather than left pending forever or run off a copy. * One string for every door, so a reader who has seen it once recognizes it everywhere. */ export declare const INERT_SCOPE_REASON = "not executed: this scope is a preview or a fork, and those cause no outbound effects"; /** What a vertical's stable serving script runs (#286) — `Vertical.servingRef`/`servingVersionId`. */ export interface ServingPointer { ref: string; versionId: string; } /** * The version whose code actually executes for `scope` (#1653) — which is not always the * one it is bound to. * * A scope on its vertical's stable serving script (`servingRef`, #286) runs whatever that * script serves, and a promote re-uploads the script in place: the code changes under * the scope at that moment, whatever its version pointer says. For a PRIVATE vertical * the promote moves the pointer too, so the two agree. For a LISTED one it does not — * the pointer is the tenant's, and moves only on their Update — so an install can run * version N+1 with its provision state still at N. Measuring against the running version * is what lets the reconcile follow the code instead of the pointer. * * Falls back to the bound version whenever the serving script cannot be named: a scope * with no `servingRef` (legacy per-version dispatch, where the bound version's own * script IS what runs), a vertical with nothing served in place yet, or a `servingRef` * that is not the vertical's current one. The fallback is exactly the pre-#1653 answer, * so an unknown never manufactures a reconcile. */ export declare function runningVersionOf(scope: Pick, serving: ServingPointer | null | undefined): string | null; export interface PlatformSweepReport { /** Active scopes `drainDue` ran on. */ scopesDrained: number; /** Drain outcomes summed across scopes. */ drainTotals: ExecutorDrainReport; /** Connections a sweeper ran for. */ connectionsSwept: number; /** Connections skipped — revoked, or their provider has no registered sweeper. */ connectionsSkipped: number; /** Expired forks reaped by `deleteSnapshot` this pass. */ snapshotsReaped: number; /** Long-archived primary scopes reaped by `reapScope` this pass (§4.4). */ archivedScopesReaped: number; /** Tenants past their grace window reaped this pass (§4.8). */ tenantsReaped: number; /** Platform-intent drain outcomes summed across scopes (platform-intents.md). */ platformRequestTotals: PlatformRequestDrainTotals; /** * WHERE `platformRequestTotals.unsettleable` stands (#1637): one entry per scope whose queue * holds a row nothing can settle — ABSENT when no scope's does. Like `eventDrain.skipped`, a * standing condition, so it is reported on every pass and never an `errors` entry. */ platformRequestUnsettleable?: PlatformRequestUnsettleable[]; /** * Scopes whose provision was re-run because the version running on them had moved past * the one it last ran against (#1172, #1653), or null when no `reconcileScopeFn` was * supplied. * * Null and `{ reconciled: 0 }` are different facts, as everywhere else in this report: * the first is "nobody looked". */ provisionReconcile: ProvisionReconcileReport | null; /** * The migration-reconciliation phase's report (§5.3, #49), or null when the * phase was disabled or the host predates `migrateScope`. Note null vs a * report saying "everything migrated" are different facts — the first is * "nobody looked", which is exactly the unfalsifiable state the sweep exists * to end. */ migrations: MigrationSweepReport | null; /** * The recurring-schedule phase's report (#383), or null when the phase was * disabled or the host predates `registeredSchedules`. Null vs a report of zeros * are different facts — null is "nobody ran schedules", zeros is "ran, nothing * due" — the same distinction `migrations` draws. */ schedules: ScheduleSweepReport | null; /** * The access-log drain's report (K-24), or null when no sink was configured. Null vs * zeros is the same distinction `migrations` draws: null is "this deployment ships * nothing and its log grows by design", zeros is "shipped, nothing was waiting". */ accessLog: AccessLogSweepReport | null; /** What the event drain shipped this pass (#1334). Null when no sink is bound. */ eventDrain: EventDrainReport | null; /** Every cross-vertical edge this pass looked at (#1705). Null when the phase is off. */ crossVertical?: CrossVerticalReport | null; /** * What the telemetry-retention phase deleted (#1632), or null when the host predates * `pruneTelemetry`. Null and zeros are different facts, as everywhere in this report. */ telemetry?: TelemetryPruneReport | null; /** * What the findings retention phase did (#1748), or null when the host predates * `pruneFindings`. */ findings?: FindingPruneReport | null; /** What the storage-gauge phase sampled and pruned (#1524). Null when the phase is off. */ storage?: StorageGaugeSweepReport | null; /** Per-unit failures; the pass records and steps over each rather than aborting. */ errors: { kind: 'drain' | 'sweep' | 'gc' | 'reap' | 'reap-tenant' | 'migrate' | 'platform-request' | 'provision-reconcile' | 'schedule' | 'freshness' | 'access-log' | 'telemetry' | 'findings' | 'event-drain' | 'vertical-events' | 'storage'; id: string; error: string; }[]; } /** * How the storage-gauge phase samples (#1524). Every number here is a bound on DO requests. * * It samples every scope that holds a store (`STORED_SCOPE_STATUSES`), from two sources: * - an ACTIVE scope only when an earlier phase of the same pass already reached it: the * platform-intent drain when `drainPlatformRequestsFn` is set (on the hosted plane it is the * phase that wakes the vertical's DO), the executor drain otherwise. An active scope nothing * reached is not read, so the gauge adds no wake to the serving fleet: a read is one more * request to a DO that is already awake. * - a NON-SERVING scope (suspended, archiving, archived) always. Nothing else wakes it, and it * still holds its storage until reap, so it belongs on the bill. This is the one wake the * gauge adds: at most once a day per such scope, inside the same batch. * A provisioning scope has no store yet and is never read, and neither is one archived * straight from provisioning (`holdsStore`). */ export interface StorageGaugeSweepOptions { /** * One scope's database size, in bytes. A throw counts that scope `failed`: its last sample * stands, the failure is recorded as its attempt, and it is tried again once `maxAgeMs` has * passed rather than on every pass. `null` says this scope has no database this reader can * measure (on the hosted plane, a scope bound to no vertical, which the drain counted without * waking anything): it is `skipped`, never an error, and asked again a day later. */ read: (scope: Scope) => Promise; /** The most scopes read in one pass, stalest first. Default `STORAGE_SAMPLE_BATCH`. `0` pauses sampling. */ batch?: number; /** A scope is due once its latest attempt is at least this old. Default one day. */ maxAgeMs?: number; /** Samples past retention deleted per pass. Default `STORAGE_GAUGE_PRUNE_BATCH`. */ pruneBatch?: number; /** * Scope stores read in the same pass as the database, and only for a scope that pass * already decided was due (#2212). Absent means this deployment does not sample stores. * `null` declines, the way `read` does. A throw records the database sample and * leaves that day's store rows as they are: the attempt carries the error. A deployment * that cannot read stores (`StorageReadUnsupported`) is neither: its attempt carries no * error, so the gauge does not count the scope as failing, and it stays out of the digest. * A read past `storeReadTimeoutMs` is an ordinary failure. */ readStores?: (scope: Scope) => Promise<{ binding: string; bytes: number; }[] | null>; /** * How long one scope's store read may take, in ms (#2212). Past it the read counts as failed * and the database sample is kept: one hung store must not hold the pass past its invocation * limit and lose every sample the batch read. Default `STORE_READ_TIMEOUT_MS`. */ storeReadTimeoutMs?: number; /** The instant the pass judges age against. Default the wall clock. */ now?: () => string; } /** * What a storage reader throws when the scope's deployment cannot read a size at all (#1524): * a vertical built before `/internal/database-size`. That is a standing condition until the * vertical is redeployed, like a paused cross-vertical edge, so the phase records it as the * scope's attempt error (the meter still counts the scope `failing`, with this reason) and * keeps it OUT of `report.errors`, which the failure digest mails every pass. Recognised by its * `unsupported` flag rather than `instanceof`, so a copy of the kernel bundled elsewhere still * matches. */ export declare class StorageReadUnsupported extends Error { readonly unsupported: true; } /** The storage-gauge phase's per-pass bound when `batch` is unset. */ export declare const STORAGE_SAMPLE_BATCH = 100; /** How old a reading may get before the phase reads the scope again, by default. */ export declare const STORAGE_SAMPLE_MAX_AGE_MS = 86400000; /** How long one scope's store read may take before it counts as failed, by default (#2212). */ export declare const STORE_READ_TIMEOUT_MS = 15000; /** What the storage-gauge phase did in one pass (#1524). */ export interface StorageGaugeSweepReport { /** Active scopes an earlier phase reached this pass: the only active ones the phase may read. */ reached: number; /** Non-serving scopes that hold a store (suspended, archiving, archived): read without a drain. */ resting: number; /** Reached scopes never tried, or last tried at least `maxAgeMs` ago. */ due: number; /** Due scopes left for a later pass because the batch was full. */ deferred: number; /** * Scopes tried, by how their attempt was recorded: without an error, with one, or declined * (`null`). `read + failed + skipped` is the scopes tried. A store read that fails counts in * `failed` even though the scope's database sample is recorded with it (#2212). */ read: number; failed: number; skipped: number; /** * Reads the deployment cannot make (`StorageReadUnsupported`): a standing condition, kept out * of `errors`. A database read that is unsupported is also in `failed`. A store read that is * unsupported is not a failure and not a size of 0 (#2212): its scope counts in `read`, and its * attempt carries no error, so the gauge does not show the scope as failing. */ unsupported: number; /** Sample rows the directory wrote (a reading for a scope reaped meanwhile writes none). */ recorded: number; /** Samples past retention deleted. */ pruned: number; } /** One pass of the access-log drain (K-24, control-plane.md §4.4). */ /** What one pass drained, across every scope it reached. */ export interface EventDrainReport { /** Scopes that had at least one undrained event and were shipped. */ scopes: number; /** Events handed to the sink and confirmed durable. */ shipped: number; /** * Scopes whose batch filled the budget — more remains, and the next tick takes * it. Reported rather than looped, so one busy scope cannot starve the pass. */ incomplete: number; /** * Undrained rows a scope's read stepped over because they would not decode (#1636), plus * any event this sweep refused itself because the published `drainedEvent` schema did not * accept it (#1641 — what an older vertical, which does not validate, can still send), * one entry per scope that had any — ABSENT when no scope did. * * These rows are never shipped and never stamped: the lake is append-only, so a row * built from stand-ins could not be taken back, and the stamp is the only record of * what left. So they are missing from the lake for as long as they stay undecodable, * and every pass reads them again and reports them again. The event is still in * Tier 1, where `readHistory` returns it with a `decodeError`. * * Not an `errors` entry: nothing about this pass failed, and the failure digest mails * every error — a standing condition would be re-sent on every tick. */ skipped?: EventDrainSkipped[]; } /** One scope's share of `EventDrainReport.skipped`. */ export interface EventDrainSkipped { tenantId: TenantId; scopeId: ScopeId; /** Exact. */ count: number; /** The first of them, oldest first; capped (`UNDRAINED_SKIPPED_IDS`), so `count` may be larger. */ eventIds: string[]; } /** What the phase tells `CrossVerticalReach.candidates` (#1705 PR 2). */ export interface CandidatesHint { /** Only this producer's edges run on this pass (the router kick). */ from?: string; /** * Report a unit the narrowing could not judge, and the scopes it kept as candidates for it. * Such a scope answering that it imports nothing is an answer, not a disagreement. */ doubt?(unit: string, reason: string, scopeIds: readonly ScopeId[]): void; /** * Report scopes the narrowing KNOWS import (their running code declares it). A candidate * reported here that answers "imports nothing" disagrees with the registry, and is a failed * edge. A candidate not reported (a superset `candidates`, which the contract allows, or a * doubtful one) answering "nothing" has simply answered. */ known?(scopeIds: readonly ScopeId[]): void; } /** How the cross-vertical phase reaches the two scopes of an edge (#1705). */ export interface CrossVerticalReach { /** * The listed scopes whose RUNNING code may import anything, found WITHOUT opening one. * * This is what keeps the phase off the fleet. It runs on a cron over every active scope, * and each per-scope call is a Durable Object wake (plus an `/internal` hop on the hosted * path). Asking every scope "do you import?" would be O(fleet) calls per tick even with no * edge anywhere. The answer is a code fact: a module declares `consumes: [{ from }]`, and a * push carries it in the version's permission registry (`imports`). So it is read where the * code is described, never from the scope. A scope this drops is never called. * * Optional. Absent, the default applies: this host's own registered modules * (`ScopeHost.registeredImports`). A host that imports nothing gets no candidates; one that * does gets every listed scope, which is right where one host is the deployment for all of * them (a self-host). The control plane passes the registry's answer per scope. * * `hint.from` is set when only one producer's edges are being run (the router kick, * `runCrossVerticalFrom`): a scope whose code imports nothing from that vertical may then be * dropped too. Ignoring the hint is correct, only dearer: an edge from another producer is * never run on such a pass anyway. `hint.doubt` is how a narrowing reports a unit it could not * judge and so kept (a version whose manifest the registry cannot read). The phase records a * failed `vertical-events` sweep-run row for it. */ candidates?(scopes: readonly Scope[], hint?: CandidatesHint): Promise | readonly Scope[]; /** The consumer's running imports and its watermark per producer. */ importState(tenantId: TenantId, scopeId: ScopeId): Promise; /** The producer's release after a watermark, decided by the producer's own code. */ readExports(tenantId: TenantId, scopeId: ScopeId, input: ExportReadInput): Promise; /** Hand a batch to the consumer, which applies it under its watermark's compare-and-set. */ deliver(tenantId: TenantId, scopeId: ScopeId, batch: ImportBatch): Promise; } export interface CrossVerticalOptions { /** * How the two sides are reached. Defaults to this host's own verbs: `admin.importState`, * `admin.readExportedEvents` and `deliverToPeer`. That is right when one host serves both * scopes. Overridden where they live in different deployments: the control plane reaches * each over `/internal`, and a two-deployment dev setup routes by scope. */ reach?: CrossVerticalReach; /** Events one edge moves per pass, at most. Default 200. */ budget?: number; /** * Consumer scopes one pass visits, at most. Default {@link CROSS_VERTICAL_CONSUMERS_PER_PASS}. * The rest are `deferred` to the next pass, not dropped: their watermarks hold, and their * producers' outboxes keep the backlog. The window starts at a random point and wraps, the * provision reconcile's rule (#1653), so consumers that fail on every pass cannot hold the * same slots forever. `0` visits none, which makes it the pause switch. */ maxConsumers?: number; /** Where the window starts, in [0, 1). `Math.random` unless given, and injectable to test fairness. */ rng?: () => number; } /** * How many consumer scopes one pass of the cross-vertical phase visits, by default (#1705). * * Per visited consumer a pass costs one `importState` call, then per source it imports from, * one `readExportedEvents` on the producer and, only when something is new, one `deliverToPeer`. * Each call is one Durable Object round trip, plus an `/internal` hop when hosted. So a pass * is bounded by `maxConsumers × (1 + 2 × sources)` scope calls, and with no candidates it * makes none. */ export declare const CROSS_VERTICAL_CONSUMERS_PER_PASS = 100; /** * One edge's pass (#1705). * * - `delivered`: a batch was applied (its events delivered, dead-lettered or withheld). * - `idle`: nothing new since the watermark. * - `paused`: one side refused the other. The producer's principal check or the consumer's * door answered no, so nothing moved and the backlog waits in the producer's outbox. * - `unresolved`: the platform could not name both ends. The producer is not installed in * the tenant, or either vertical has more than one primary instance there. * - `stale`: the consumer's watermark moved under this pass, and the batch was refused. * - `failed`: a transport or host error. Also in `errors`. */ export interface CrossVerticalEdge { tenantId: TenantId; consumer: { scopeId: ScopeId; vertical: string; }; producer: { vertical: string; scopeId: ScopeId | null; }; state: 'delivered' | 'idle' | 'paused' | 'unresolved' | 'stale' | 'failed'; delivered: number; deadLettered: number; withheld: number; duplicates: number; /** The sentence a person reads. Set on every state but `delivered` and `idle`. */ reason?: string; /** Types the consumer imports that the producer does not export (reported, not paused). */ unexported?: WantedEvent[]; /** The producer had more than one batch waiting. The next pass takes the rest. */ more?: boolean; } export interface CrossVerticalReport { edges: CrossVerticalEdge[]; delivered: number; withheld: number; paused: number; unresolved: number; /** Scopes whose running code may import, per `candidates`: the only ones any pass calls. */ candidates: number; /** Candidates past this pass's `maxConsumers`, left for the next pass. */ deferred: number; } export interface AccessLogSweepReport { /** Rows handed to the sink and confirmed durable. */ shipped: number; /** Rows stamped `drainedAt` as a result. Below `shipped` only on a re-run. */ marked: number; /** Drained rows deleted from the directory this pass. */ pruned: number; /** Where the batch landed, as the sink reported it. Null when nothing shipped. */ ref: string | null; } /** One scope's share of `PlatformSweepReport.platformRequestUnsettleable` (#1637). */ export interface PlatformRequestUnsettleable { tenantId: TenantId; scopeId: ScopeId; /** Exact. */ count: number; /** The stored ids, as text (`null` for SQL NULL); capped by the drain, so `count` may be larger. */ ids: (string | null)[]; } /** Platform-intent drain counts, summed across scopes in one pass. */ export interface PlatformRequestDrainTotals { /** Active scopes that had at least one intent drained. */ scopes: number; /** Intents seen across all scopes. */ drained: number; /** Executed successfully. */ done: number; /** Terminally failed (unknown kind, or a handler that gave up). */ failed: number; /** Left pending for a later drain (transient failure). */ pending: number; /** * Active scopes the phase stepped over because their migration failed this pass (#1840). * Their queues are not in the counts above. */ skipped: number; /** * Active scopes whose drain reported it could reach no deployment for them (#1840) — the * drain fn returned `unreachable: true`. Their queues are not in the counts above either. */ unreachable: number; /** * Pending intents a drain could neither run nor settle, because the stored id of a row whose * identity did not decode is not an id (#1637). They stay pending, so every pass counts them * again until the row is repaired. Not in `drained`. */ unsettleable: number; } export declare function runPlatformSweep(host: ScopeHost, options: PlatformSweepOptions): Promise; /** * Run ONE producer's outgoing edges now (#1705 PR 2): the half of the router kick that makes a * cross-vertical event arrive in seconds rather than at the next sweep. * * The same phase, under the same rules, narrowed. It reads the producer's tenant only, runs only * edges whose producer RESOLVES to `producer` (so a fork, a preview or a second install runs * nothing), asks `candidates` with `{ from }` so a consumer that imports nothing from this * vertical is never called, and keeps the per-pass consumer cap. What it moves is exactly what * the next sweep would have moved: the watermark's compare-and-set makes the two safe to overlap, * and an edge a kick has taken reads as `idle` to the sweep that follows. */ export declare function runCrossVerticalFrom(host: ScopeHost, options: Pick & { crossVertical: CrossVerticalOptions; }, producer: { tenantId: TenantId; scopeId: ScopeId; }): Promise<{ crossVertical: CrossVerticalReport; errors: PlatformSweepReport['errors']; }>; export declare function registryImportCandidates(input: { admin: Pick; actor: PlatformActorId; readImports: (verticalSlug: string, versionId: string) => Promise; /** Registry reads in flight at once. Default 8, the phase's own concurrency. */ concurrency?: number; }): NonNullable; /** * Who a promote breaks (#1705 PR 3): the installed consumers whose RUNNING code imports an * exported (type, schemaVersion) that the outgoing version promised and the incoming one drops * or exports at another version. The promote gate refuses on a non-empty answer unless it is * acknowledged (`exportBreak`), and the refusal lists them. * * - **What changed** is outgoing versus incoming, from the two versions' registries. An outgoing * version whose registry states no exports (a first promote, or a push by a CLI older than * 0.34.0) promised nothing the registry can name, so nothing is judged broken. Nothing it * exported at run time could be listed either, which is what the permission-digest gate * beside this one is for. * - **Who is a consumer** is the edge's own rule: a primary, active scope of ANOTHER vertical, in * a tenant where the producer is installed (any primary, active install, so two installs still * count), whose running version (`runningVersionOf`) declares the import. That over-approximates * on purpose, for a listed vertical's tenants still pinned to an older producer version: a * refusal that names one tenant too many costs an acknowledgement, and one that names too few * costs a silent stall. * - **A consumer version whose manifest does not parse** is not judged. It cannot be said to * break, and refusing every promote over a manifest nobody can parse would make the gate one * nobody can pass. A read that THREW is different: the registry could not be asked, so whom the * promote breaks is unknown, and this throws. The gate then refuses rather than failing open. * A failed directory read throws the same way. * * Cost: nothing at all unless an export changed. Then one fleet `listScopes`, one * `listVerticals` when a scope is on a serving script, and one `readImports` per distinct * running consumer version, at most `concurrency` in flight. */ export declare function exportBreaksOf(input: { admin: Pick; actor: PlatformActorId; producer: string; outgoing: ManifestExports; incoming: ManifestExports; readImports: (verticalSlug: string, versionId: string) => Promise; concurrency?: number; /** * #1756: judge ONE tenant's consumers only — a bind moves one install, and an edge never * crosses a tenant, so no other tenant's app can be broken by it. The tenant is taken to run * the producer (the scope being bound is that install), whatever its status. */ tenantId?: TenantId; /** What is being judged, for the wording of a refusal to judge it. Default `'promotion'`. */ act?: 'promotion' | 'bind'; }): Promise; /** Whether a throw is the promote gate's export-break refusal (`exportBreakRefusal`). */ export declare function isExportBreakRefusal(err: unknown): err is Error; /** * The refusal a promote gets for a non-empty `exportBreaksOf` (#1705 PR 3). Counts only: the * adapters do not know who is promoting, and a builder must not learn from an error which other * tenants import its events. The route that knows the caller lists what they may see. */ export declare function exportBreakRefusal(breaks: readonly ExportBreak[]): string; /** How `bindExportBreakRefusal` begins: what lets the bind route recognise it (#1756). */ export declare const BIND_EXPORT_BREAK_REFUSAL = "this bind drops or re-versions"; /** Whether a throw is the bind gate's export-break refusal (`bindExportBreakRefusal`). */ export declare function isBindExportBreakRefusal(err: unknown): err is Error; /** * The refusal a bind gets for a non-empty `bindExportBreaksOf` (#1756). Every break is in the * bound scope's own tenant, which the caller addressed, so it names no one else — but it stays in * counts, as the promote refusal does, and the route that knows the caller adds the listing. */ export declare function bindExportBreakRefusal(breaks: readonly ExportBreak[]): string; /** * What a bind or a route change moves `scope` between (#1756, #2178): the version it runs before * and the one it runs after (`runningVersionOf` on both of its inputs), or null when the move * changes nothing it runs — a re-point that stays on the serving script — or it ran nothing before * (a first bind). `servingRef` omitted: the route is unchanged. */ export declare function scopeMoveOf(scope: Pick, incomingVersionId: string, servingRef: string | null | undefined, serving: ServingPointer | null): { before: string; after: string; } | null; /** * #2178: the versions the active primary scopes of a vertical that a promote or a serve moves were * last provisioned against (`provisionedVersionId`, written only after a provision or reconcile * succeeds): those routed on a serving ref, and those of an owner tenant. A retirement reaches a * scope only at its reconcile, so a scope provisioned behind the retiring version still holds the * keys. #2185: and whether that reconcile FINISHED the version's retirements * (`retirementsFinishedVersionId` equal to the receipt), since one that left a retirement open still * writes the receipt. One row per (version, finished), with how many installs and one of them to * name, however many installs there are. Installs with NO receipt are a row too (`version_id` null): * a switch clears the receipt (#2045), a rewind and a restore do (#2185), and a new install has none until its first * reconcile. The sweep reconciles every unfinished or receiptless install on its next pass, so a * refusal they cause is transient unless that reconcile keeps failing, answers 'unsupported', or * cannot report its retirements (an `adapter-cloudflare` or `vertical-host` that predates the report). Binds * `(vertical, servingRef, ownerTenant)`; a null matches nothing. The directory has no index on * `vertical`, so this scans its scopes table: once per promote or serve. * * #2184: and which binding shapes the reconcile reported the installs have bound through a link * (`bound_shapes`, JSON `{version, types}`), counted only while its version is the receipt's, else * null (unknown). Grouped by it too: each set is a subset of one version's table-held bootstrap * shapes, sorted, so a fleet of one vertical has a handful. * * Review of #2220: and by the version each install RUNS (`runningVersionOf`: the served version on * the serving script, else its pointer), since a receipt behind it speaks for neither the bindings * nor the retirements of what it runs now. Bind its parameters with {@link provisionedVersionsParams}. * * So the rows are at most (receipt versions × running versions × 2 × distinct type sets), still * independent of how many installs there are. */ export declare const PROVISIONED_VERSIONS_SQL = "SELECT provisioned_version_id AS version_id,\n CASE WHEN serving_ref = ? THEN ? ELSE vertical_version_id END AS running_version_id,\n COALESCE(retirements_finished_version_id = provisioned_version_id, 0) AS finished,\n CASE WHEN json_extract(bound_shapes, '$.version') = provisioned_version_id\n THEN json_extract(bound_shapes, '$.types') END AS bound_types,\n COUNT(*) AS installs, MIN(scope_id) AS example\n FROM scopes\n WHERE vertical = ? AND status = 'active' AND forked_from IS NULL AND COALESCE(kind, '') != 'preview'\n AND (serving_ref = ? OR tenant_id = ?)\n GROUP BY provisioned_version_id, running_version_id, finished, bound_types"; /** * `PROVISIONED_VERSIONS_SQL`'s parameters: the installs of `vertical` on `serving`'s script (which * run its version) or owned by `ownerTenant`. Null for either matches nothing. */ export declare function provisionedVersionsParams(vertical: string, serving: ServingPointer | null, ownerTenant: string | null): (string | null)[]; /** * #2184: the directory's `bound_shapes` value — the version the reconcile ran against and the types * it reported bound, as one JSON value, so the pair is written and cleared together and a receipt * rewritten without it (a control plane that predates the column) reads as unknown. Null when * there is no receipt or the outcome does not say. */ export declare function boundShapesColumn(versionId: string | null, types: readonly string[] | undefined): string | null; /** #2184: `Scope.boundShapes` from the directory's `bound_shapes` value; null for null or anything unreadable. */ export declare function boundShapesOfColumn(raw: string | null | undefined): { versionId: string; types: string[]; } | null; /** One row of `PROVISIONED_VERSIONS_SQL`: the installs that last reconciled one version. */ export interface InstallsReconciled { /** Null: installs with no receipt, which may not have reconciled anything they run. */ versionId: string | null; /** * Review of #2220: the version they run now, when the caller knows it. One that differs from * `versionId` is a receipt lagging what runs: judged as if nothing about it had been reported. */ runningVersionId: string | null; /** * #2185: whether their reconcile of `versionId` finished every retirement it declares. False * judges them as still holding the keys it retires. Meaningless when `versionId` is null. */ retirementsFinished: boolean; /** * #2184: the types of `versionId`'s table-held bootstrap shapes their reconcile reported bound * through a link, or null when it did not report (or `versionId` is null): the gate then counts * every such shape as bound. */ boundShapes: string[] | null; installs: number; /** One of them, so a refusal can say which install to look at. */ example: string; } /** `PROVISIONED_VERSIONS_SQL`'s rows, as either adapter's driver returns them. */ export declare function installsReconciledOf(rows: readonly Record[]): InstallsReconciled[]; /** * #2185: one install's own receipt as a {@link InstallsReconciled} group, for a bind or a route of * that install, judged by the same rule as the promote's `PROVISIONED_VERSIONS_SQL` rows, with the * version it runs before the move (`scopeMoveOf`'s `before`). */ export declare function installReconciled(scope: Pick, runningVersionId: string | null): InstallsReconciled; /** * #2184: what the gate counts as bound for a group: the types its reconcile reported, or every * table-held bootstrap shape (`'unknown'`) when it did not, or when the group has no receipt. */ export declare function boundEverOf(g: InstallsReconciled): readonly string[] | 'unknown'; /** One judgment of the binding-shape gate: a version to judge a move from, and how (`assertNoBindingShapeBreak`). */ export interface StrandJudgment { from: string; opts: { judgedFrom: string; retiredStillHeld: boolean; boundEver: readonly string[] | 'unknown'; }; } /** * What the binding-shape gate judges a move onto `afterId` from, for one group of installs, given * the versions the move leaves (`froms`). One rule for both adapters' doors: * * - no receipt: every one of `froms`, as if nothing about it had run or been reported (the installs * run one of them; judging the others too can only over-refuse, and only while it lasts); * - a receipt behind the version they run (review of #2220): that running version the same way, * since whatever it bound or retired since the receipt is not in it — a version that added `bind` * may have reconciled and been claimed through without its receipt landing; * - a receipt: its own version, from its own report (#2185, #2184). * * Each is transient: the sweep reconciles every such install on its next pass. */ export declare function strandJudgmentsOf(g: InstallsReconciled, froms: readonly string[], afterId: string): StrandJudgment[]; /** What a refusal judged from installs' receipts says it was judged from. */ export declare function installsReconciledNote(g: InstallsReconciled): string; /** * Whom moving `scope` breaks (#1756): the promote gate's question (`exportBreaksOf`), asked of * ONE install in its own tenant, since an edge never crosses one. * * A scope's exports leave from the code it RUNS, and `runningVersionOf` names that code from two * inputs: the version pointer and the serving ref. So a move is either or both of them changing — * a bind (`next.versionId`), or a route onto or off a serving script (`next.servingRef`, which is * how an adopt moves a scope before its pointer ever does). Both are judged by the same rule, on * the running version before against the running version after, the rule the consumers are judged * on too: * * - **Re-pointing a scope that stays on its vertical's serving script** changes nothing it runs, * so it is never refused. The change reached it when the serving script was replaced — at a * promote, which judged every tenant then. A route ONTO the serving script is not that case: * the scope goes from its own version to whatever is served, and that is judged. * - **A fork or a preview** is never an edge's producer (`resolveVerticalInstanceFrom` takes the * primary install only), so nothing it runs can break an edge. * - **A scope still provisioning** has never been active, so no edge has ever delivered from it. * - **A first bind** runs nothing before it, so it promised nothing. * - **A lineage crossing** (the version belongs to another vertical than the scope's, or the route * goes onto a script that is not this vertical's serving script) is not judged here, and nothing * else judges it either: every consumer importing `from` the old slug * loses its producer, and `rebind-vertical`'s acknowledgements are about migrations, not about * that. A known gap, older than this gate. */ export declare function bindExportBreaksOf(input: { admin: Pick; actor: PlatformActorId; scope: Pick; /** The version the scope is bound to after the move (its vertical's slug beside it). */ incoming: { id: string; verticalSlug: string; }; /** The serving ref it routes by after the move. Omitted: unchanged. */ servingRef?: string | null; /** The serving pointer of the scope's vertical, or null when nothing is served in place. */ serving: ServingPointer | null; readExports: (versionId: string) => Promise; readImports: (verticalSlug: string, versionId: string) => Promise; }): Promise; /** Where a consumer's door stands for each peer, as `admin.peerGrantsStatus` answers it. */ type DoorRead = (tenantId: TenantId, scopeId: ScopeId) => Promise; /** * Where every cross-vertical edge of one tenant stands, read LIVE (#1705 PR 3): the view behind * the console's and the dashboard's edge health. * * It is the sweep's own walk with the delivery taken out. It uses the same reach, the same * narrowing (`candidates`, which keeps it off scopes that import nothing), and the same resolution * of both ends (`edgeEnds`). So it cannot disagree with the next pass about which edges exist or * where they point. Per edge it makes one probe read of the producer after the watermark, * `limit: 1`, which tells behind from caught up and dates the oldest waiting event. That read is the * producer's own `readExportedEvents`, gated and audited as on a pass, and nothing it returns is * delivered or kept. The consumer's door is read from its peer switch (`peerGrantsStatus`), because * a consumer refusing the producer is visible only at delivery otherwise. The tenant's recent * `vertical-events` sweep-run rows add the last pass that delivered and the last that did not. * * `focus` narrows it to one scope's edges, the ones into it and out of it: a per-app view asks * nothing about the rest of the tenant. Out of it means consumers the narrowing names for the * focus's vertical (`hint.from`), so the registry, not a scope, decides who is asked. * * Cost per view: one directory read, the narrowing, then per importing scope one `importState` * and one door read, per edge one probe read and one or two sweep-run reads. It is bounded by the * tenant, never the fleet. * * Who the reads are audited as: the directory, door and history reads as `actor`, the viewer the * caller names. The reach's reads (`importState`, the probe) are audited as whatever actor the * reach was built with. On the control plane that is the sweep's own actor, since it is the * sweep's reach, so those rows read as the platform's, not the viewer's. * * `unavailable` is its own state: a side that could not be asked gets no health, and no * surface may show it as healthy. */ export declare function crossVerticalHealth(host: ScopeHost, options: { actor: PlatformActorId; tenantId: TenantId; crossVertical?: CrossVerticalOptions; /** Only this scope's edges: into it, and out of it. */ focus?: ScopeId; /** * Where the consumer's door stands for each peer. Default: `admin.peerGrantsStatus`, which on * the shared control plane reaches the consumer's deployment through its peer-switch * delegation and adds who switched a peer off, and why. A host that serves the scope itself * without that delegation may pass its own far end (`peerGrantsStatusLocal`) instead. */ door?: DoorRead; /** Epoch ms. Injectable so a test pins the lag. */ now?: () => number; concurrency?: number; }): Promise; /** A running sweeper; `stop()` prevents the next pass and cancels the pending timer. */ export interface PlatformSweeperHandle { stop(): void; } export interface StartPlatformSweeperOptions extends PlatformSweepOptions { /** * Milliseconds between the END of one pass and the START of the next — a gap, * not a fixed rate. Rescheduling only after a pass settles means two passes can * never overlap, even when a pass runs longer than the interval. */ intervalMs: number; /** Observe each pass — for logging or a health metric. Never throws into the loop. */ onPass?: (outcome: PlatformSweepReport | { error: string; }) => void; /** Injected for tests; default to the runtime's timer. */ setTimer?: (cb: () => void, ms: number) => unknown; clearTimer?: (handle: unknown) => void; } /** * Drive `runPlatformSweep` on a self-rescheduling timer — the node/long-lived * runtime's trigger. (A Cloudflare deployment uses `scheduled()`/an alarm instead * and calls `runPlatformSweep` directly; both share the one unit of work.) * * Non-overlapping by construction: the next pass is scheduled only once the * current one settles, so a slow pass delays the next rather than stacking on it. */ export declare function startPlatformSweeper(host: ScopeHost, options: StartPlatformSweeperOptions): PlatformSweeperHandle; export {}; //# sourceMappingURL=platform-sweep.d.ts.map