/** * Live edge reconstruction (#778, the crux of epic #776). * * A source-derived IR gets its edges from declared AttrRefs. A live IR * (`chant graph --live`) has none — observed resources reference each other by * **physical identifier** buried in their attributes (a subnet's `VpcId`, an * ALB listener's `TargetGroupArn`, an ECS service's `ClusterArn`). This module * reconstructs those relationships from a per-lexicon **reference catalog**. * * Provider-agnostic: the engine here is pure and knows nothing about AWS; each * lexicon ships its own `ReferenceCatalog` (data). Given the live nodes and a * catalog, `reconstructEdges` returns: * - `edges` — peer references → IR edges (holder → referenced) * - `containment` — "inside" references (subnet ∈ VPC) → boundary hints for #779 * - `dangling` — references whose target isn't in the observed set * * The containment / edge split keeps subnet-in-VPC a boundary box (#779), not a * cluttering line. Deterministic given a fixed node set. */ import type { IRNode, IREdge } from "./graph-ir.js"; /** Which attribute paths identify a resource kind (its id / ARN / name / DNS). */ export interface IdentityRule { kind: string; /** Attr paths whose values are identifiers others reference this kind by. */ ids: string[]; } /** A reference: an attr path on `from` whose value points at another resource. */ export interface RefRule { /** Holder kind. */ from: string; /** Attr path — supports `a.b` and `arr[].id`. */ path: string; /** Which identifier the value is (currently informational; matching is exact). */ match?: "id" | "arn" | "name" | "any"; /** Constrain the target kind (disambiguates identifier collisions). */ targetKind?: string; /** `reference` → an edge; `containment` → a boundary hint (#779), not an edge. */ relation: "reference" | "containment"; /** Edge / containment label (e.g. "in VPC", "sg", "targets"). */ label?: string; /** * What the reconstructed edge's `viaAttr` should be, when traversal needs a * different string than rendering does (#1275). * * `viaAttr` defaulted to `label ?? path`, which serves a renderer well and a * traversal badly. The labels here are human-facing — "sg", "via", "in VPC" — * while a fold like `enrichEffectiveTopology` matches provider attribute * names: `SecurityGroupIds`, `SubnetId`, `LaunchTemplateId`. One field could * not be both, so a rule that is traversed declares the name explicitly and * keeps its label for the picture. * * On a `containment` rule this additionally opts the relation into producing * an edge, on top of the boundary pair it already produces. Containment is * not an edge by default and should not become one — but a fold's first hop * is sometimes exactly a containment relation (an instance is *in* a subnet), * and that hop has to be traversable without duplicating the rule as a * reference and drawing the line twice. */ viaAttr?: string; } /** A lexicon's reference knowledge — its identity map and reference rules. */ export interface ReferenceCatalog { identities: IdentityRule[]; refs: RefRule[]; } /** `child` is contained by `parent` (subnet ∈ VPC). For #779's boundary boxes. */ export interface ContainmentPair { child: string; parent: string; label?: string; } /** A reference whose target isn't in the observed set (cross-account, unmanaged, * deleted) — surfaced, never turned into a wrong edge. */ export interface DanglingRef { from: string; path: string; value: string; targetKind?: string; } export interface ReconstructedEdges { edges: IREdge[]; containment: ContainmentPair[]; /** * The same containment pairs, as edges a query can walk. * * Containment is a boundary when you are drawing it and a relationship when * you are asking about it, and those two consumers had been served by one * decision. `edges` is what a renderer draws as lines, so putting "is in this * VPC" there would draw a line from every resource to its VPC and undo the * boxes; that is why containment is kept out of it, and why it stays out. * * But `->`/`<-` is asking which nodes reach which, and being inside something * is a way of reaching it. "Which subnets have no network interfaces in them" * and "which VPCs have no instances in them" are the same question, and both * are containment. With only `edges` to walk, the negation matched everything * and reported an estate where nothing is anywhere. * * The escape hatch this replaces was per-rule: a containment rule could set * `viaAttr` and become a real edge. That put the query layer's needs in a * field the renderer also reads, and it had to be remembered per rule — * `AWS::EC2::Instance -> Subnet` had it and `AWS::EC2::NetworkInterface -> * Subnet` did not, which is the kind of gap hand-maintained lists always * develop. Deriving them here means a containment rule is traversable because * it is a containment rule, not because someone remembered. */ containmentEdges: IREdge[]; dangling: DanglingRef[]; } /** * Read all scalar values at an attr path. Supports nested keys (`a.b`) and array * fan-out (`arr[]`, `arr[].id`). Returns every scalar found — a path through an * array yields one value per element. */ export declare function readPath(obj: unknown, path: string): string[]; /** Merge several lexicons' catalogs into one (concatenate identities + refs). */ export declare function mergeCatalogs(catalogs: ReferenceCatalog[]): ReferenceCatalog; /** * Reconstruct edges + containment from live nodes and a catalog. Pure and * deterministic. Matching is exact on identifier value; identifier collisions * across kinds are disambiguated by `targetKind`. Self-references are dropped. */ export declare function reconstructEdges(nodes: IRNode[], catalog: ReferenceCatalog): ReconstructedEdges; /** * Invert containment pairs into grouping metadata (#779): container node id → the * node ids directly inside it. The result is the `IRGroups.byContainer` shape a * renderer draws as boundary boxes. It represents the full nesting *flatly* — a * subnet is both a member of its VPC's entry and a key with its own members — * so a boundary-box renderer recurses it (VPC ⊃ subnet ⊃ resources). Sorted for * determinism. */ export declare function containmentGroups(pairs: ContainmentPair[]): Record; //# sourceMappingURL=graph-refs.d.ts.map