/** * Identity-pin remediation core (D9 Phase 3) — the pure half of * `fjall migrate identity` and the deploy-flow pin arm for * `rename-detected` findings that carry a capacity slot hint. * * Three pure functions, no IO: * - {@link decodeLegacyAnchor} — recover the DEPLOYED anchor from the * rename pair's logical IDs (ground truth beats the config-derived * `legacyAnchor` the manifest alias predicts); * - {@link captureLegacySurfaces} — lift the createOnly/verbatim surfaces * (capacity-SG `GroupDescription`, PDV `fjall:OwnerLogicalId` tag) out of * the live template; * - {@link runIdentitySurfaceGate} — the I2 gate: a pinned synth must * reproduce every identity surface of the live template byte-for-byte. * * Failure messages are identifier-only (logical IDs, anchors, resource * types) — nothing value-bearing crosses a sink boundary. */ import { type Result } from "@fjall/generator"; /** One confirmed rename pair (delete-side and create-side logical IDs). */ export interface PinnableRenamePair { deleteAddress: string; createAddress: string; } /** * Decode the deployed (legacy) anchor from a slot's rename pairs — ground * truth read from the live logical IDs, never trusted from the config-side * prediction. * * Anchors can occur more than once in one ID (the Ec2Instance scope AND its * `${physicalServiceName}…` children both carry it), so the decode solves * the general substitution shape rather than reading a single residue: the * create-side ID split by the anchor fixes a segment skeleton, and the * delete-side ID must be that skeleton rejoined with one consistent legacy * token. Solving runs on matcher (d)'s minimal residue middles first (which * neutralises anchor text sitting in the shared scope prefix) and on the * full hash-stripped IDs as fallback; every candidate must survive the * matcher-(a)-identical substitution check * (`old.split(L).join(anchor) === new`). Every pair must agree — * disagreement means the pairing crosses two capacity subtrees and nothing * is safe to pin. * * Two-phase against the 240-char logical-ID clip: CDK truncates human * prefixes at 240 chars, which can cut a delete-side ID mid-token while the * shorter create-side twin escapes the clip entirely — no exact substitution * can reproduce such a pair. Pairs that decode exactly establish the * consensus token first; the remaining (clipped or otherwise inexact) pairs * must then be reproduced by that consensus under the truncation-aware * {@link substitutionReproducesId}. When NO pair decodes exactly there is no * ground truth to anchor the tolerant check to — refuse, fail closed. */ export declare function decodeLegacyAnchor(pairs: readonly PinnableRenamePair[], anchor: string): Result; /** Surfaces captured verbatim from the live template for the pin entry. */ export interface CapturedLegacySurfaces { /** * Absent in the cluster-supplied-SG composition (e.g. the ClickHouse * pattern): the capacity construct creates no `${anchor}AsgSecurityGroup` * there — `cluster.securityGroup` wins in `ecsServiceFactory` — so the live * template has nothing to capture and the pin never needs the value. */ legacySgDescription?: string; legacyOwnerId?: string; } /** * Capture the pin's verbatim surfaces from the live template. * * The capacity SG's `GroupDescription` is createOnly — CloudFormation * replaces the SG on ANY change to it, so the pin must reproduce the * deployed string byte-for-byte. Zero anchored SGs is NOT a failure: the * cluster-supplied-SG composition (ClickHouse pattern — production * Analytics) never creates one, on either side of the pin, so the * description is simply omitted. Two or more is ambiguous and refuses. * The PDV volume's `fjall:OwnerLogicalId` tag is the volume-adoption key; * a slot with no PDV simply has no owner id to carry. Both lookups are * left-bounded so a sibling slot whose anchor extends this one (e.g. * `BigPrimary` vs `Primary`) cannot masquerade: the SG by the cluster scope * (`${clusterName}Ecs${legacyAnchor}AsgSecurityGroup` — the SG is a direct * child of the `${clusterName}Ecs` cluster construct), the volume by the * physical service name * (`${clusterName}${legacyAnchor}PersistentDataVolume`). */ export declare function captureLegacySurfaces(liveTemplate: unknown, params: { legacyAnchor: string; clusterName: string; }): Result; /** I2 gate verdict: pass, or every surface inequality found. */ export interface IdentitySurfaceGateReport { pass: boolean; failures: string[]; } export interface IdentitySurfaceGateParams { liveTemplate: unknown; pinnedTemplate: unknown; /** The anchor in effect under the pin (equals the decoded legacy anchor). */ anchor: string; /** The physical service name the slot serves: `${clusterName}${anchor}`. */ physicalServiceName: string; } /** * The I2 identity-surface gate: with a pin applied, the capacity subtree's * synth must reproduce the deployed template on every identity surface. * * Collection is anchor-rooted and bidirectional: every logical ID in EITHER * template whose hash-stripped form contains the anchor is collected (CDK * logical IDs concatenate the full construct path, so descendants like the * GracefulTermination and PersistentDataVolume members carry the anchor * mid-string and ride the same collection). The gate then requires set * equality of the collected full IDs plus byte-equality of the named * physical surfaces (SG GroupDescription, LaunchTemplateName, lifecycle * hook names, the volume owner tag, the Lambda owner env, ServiceName). * * The ECS service is the lookup exception: its construct ID derives from * the fjall service name (unchanged under a rename), so it never enters the * anchor collection — it is compared by direct logical-ID lookup on * ServiceName + CapacityProviderStrategy. * * Non-identity deltas (instance type, AMI, UpdatePolicy, …) are NOT this * gate's concern — they flow to the normal plan and destruction gate. */ export declare function runIdentitySurfaceGate(params: IdentitySurfaceGateParams): IdentitySurfaceGateReport;