""" rules/deployment_rules.py -------------------------- Decides *which* services are active and how they relate to each other, based on a user's InfraConfig. This module contains no rendering logic (no Jinja2, no YAML, no Mermaid syntax) — it only answers questions like "what does the request path look like?" and "what should the security report flag?". Generators consume these decisions and render them into their respective formats. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Literal from utils.config import InfraConfig NodeKind = Literal["entry", "network", "compute", "cache", "database", "vector", "monitoring", "cicd"] @dataclass(frozen=True) class ServiceNode: """A single box in the architecture graph.""" id: str label: str kind: NodeKind @dataclass(frozen=True) class ServiceEdge: """A connection between two ServiceNodes.""" source: str target: str style: Literal["solid", "dashed"] = "solid" label: str = "" @dataclass(frozen=True) class ArchitectureGraph: nodes: list[ServiceNode] = field(default_factory=list) edges: list[ServiceEdge] = field(default_factory=list) def build_architecture_graph(config: InfraConfig) -> ArchitectureGraph: """ Build the full architecture graph (nodes + edges) for a given config. Main request path (always present, top to bottom): Client -> Load Balancer -> Deployment Target -> App Framework Then, only if the user selected them (i.e. not "None"), each data service connects DIRECTLY from App Framework — as independent, parallel dependencies, not a chain: App Framework -> Cache App Framework -> Database App Framework -> Vector Database (A prior version chained these off each other — App -> Cache -> Database -> Vector Database — which is topologically wrong: it implies, for example, that the vector database sits between the app and the relational database. Since this graph feeds both the Mermaid diagram and the ArchitectureAdvisor's prompt, that wrong shape was visible in the diagram and could mislead the AI's reasoning about the actual request path.) Side branches (dashed, since they're not on the live request path): App Framework -> Monitoring CI/CD -> Deployment Target (deploy pipeline, not user traffic) """ nodes: list[ServiceNode] = [ ServiceNode("client", "Client", "entry"), ServiceNode("lb", "Load Balancer", "network"), ServiceNode("deploy_target", config.deployment_target, "compute"), ServiceNode("app", config.app_framework, "compute"), ] edges: list[ServiceEdge] = [ ServiceEdge("client", "lb"), ServiceEdge("lb", "deploy_target"), ServiceEdge("deploy_target", "app"), ] # Data-layer dependencies — each is an independent, parallel child of # "app", only wired in if the user actually chose one. Order in this # tuple only affects node/edge list ordering (cosmetic), not topology. for field_value, node_id, kind in ( (config.cache, "cache", "cache"), (config.database, "database", "database"), (config.vector_db, "vector_db", "vector"), ): if field_value and field_value != "None": nodes.append(ServiceNode(node_id, field_value, kind)) edges.append(ServiceEdge("app", node_id)) # Side branches if config.monitoring and config.monitoring != "None": nodes.append(ServiceNode("monitoring", config.monitoring, "monitoring")) edges.append(ServiceEdge("app", "monitoring", style="dashed", label="metrics")) if config.cicd and config.cicd != "None": nodes.append(ServiceNode("cicd", config.cicd, "cicd")) edges.append(ServiceEdge("cicd", "deploy_target", style="dashed", label="deploy")) return ArchitectureGraph(nodes=nodes, edges=edges) def active_data_services(config: InfraConfig) -> list[str]: """ Convenience helper for generators that just need the list of active data-layer services (used by docker_generator / kubernetes_generator in later steps to decide which containers/manifests to emit). """ services = [] if config.cache and config.cache != "None": services.append(config.cache) if config.database and config.database != "None": services.append(config.database) if config.vector_db and config.vector_db != "None": services.append(config.vector_db) return services def high_availability_requested(config: InfraConfig) -> bool: """Single source of truth for HA branching across generators.""" return config.high_availability