decomposer: generate deliverable files for Inspect the single_agent template and target repository conventions to identify the required project structure, configuration, interfaces, and implementation patterns for the application landing zone agent.; Define the application landing zone agent's behavior, input and output contracts, document-generation workflow, and architecture-diagram requirements using the extracted template conventions.; Validate the implemented application_landing_zone_agent by running repository tests and checking the TSD document generation, draw.io diagram generation, agent packaging, configuration, and committed implementation against the defined contracts.; Commit and push the validated application_landing_zone_agent implementation to the target repository.

This commit is contained in:
2026-08-31 14:46:50 +00:00
parent 221f31d824
commit fb20521880
14 changed files with 258 additions and 172 deletions

View File

@@ -1,5 +1,7 @@
"""Application landing zone agent public API."""
from .agent import LandingZoneAgent, GenerationResult, ValidationError
"""Application landing zone document and architecture generator."""
__all__ = ["LandingZoneAgent", "GenerationResult", "ValidationError"]
from .agent import ApplicationLandingZoneAgent
from .models import GenerationResult, Requirements
__all__ = ["ApplicationLandingZoneAgent", "GenerationResult", "Requirements"]
__version__ = "0.1.0"

View File

@@ -1,75 +1,22 @@
import html
import json
from pathlib import Path
from typing import Any
from .model import Component, Requirements, GenerationResult, ValidationReport
from .generator import render_drawio, render_tsd
from .models import GenerationResult, Requirements
class ValidationError(ValueError):
"""Raised when requirements cannot satisfy the input contract."""
class LandingZoneAgent:
def __init__(self, config: dict[str, Any] | None = None):
self.config = config or {}
class ApplicationLandingZoneAgent:
"""Transform validated application requirements into reviewable artifacts."""
def normalize(self, data: dict[str, Any]) -> Requirements:
report = self.validate(data)
if report.errors:
raise ValidationError("; ".join(report.errors))
components = tuple(Component(str(c["name"]), str(c.get("type", "component")), str(c.get("technology", "Not specified")), str(c.get("description", ""))) for c in data.get("components", []))
def strings(key: str) -> tuple[str, ...]:
return tuple(str(x) for x in data.get(key, []))
integrations = tuple(str(x.get("name", "")) if isinstance(x, dict) else str(x) for x in data.get("integrations", []))
return Requirements(str(data["application_name"]).strip(), str(data["business_context"]).strip(), strings("environments"), components, integrations, strings("constraints"), strings("non_functional_requirements"), strings("assumptions"), dict(data))
def validate(self, data: Any) -> ValidationReport:
errors: list[str] = []
if not isinstance(data, dict): return ValidationReport(False, ["Input must be a JSON object"])
for key in ("application_name", "business_context"):
if not isinstance(data.get(key), str) or not data[key].strip(): errors.append(f"'{key}' is required and must be a non-empty string")
for key in ("environments", "components", "integrations", "constraints", "non_functional_requirements", "assumptions"):
if key in data and not isinstance(data[key], list): errors.append(f"'{key}' must be an array")
for i, component in enumerate(data.get("components", [])):
if not isinstance(component, dict) or not isinstance(component.get("name"), str) or not component["name"].strip(): errors.append(f"components[{i}].name must be a non-empty string")
for i, item in enumerate(data.get("integrations", [])):
if isinstance(item, dict) and (not isinstance(item.get("name"), str) or not item["name"].strip()): errors.append(f"integrations[{i}].name must be a non-empty string")
elif not isinstance(item, (str, int, float)) or not str(item).strip(): errors.append(f"integrations[{i}] must be a string or named object")
warnings = []
if not data.get("components"): warnings.append("No application components supplied; diagram contains only the boundary")
return ValidationReport(not errors, errors, warnings)
def generate(self, data: dict[str, Any]) -> GenerationResult:
req = self.normalize(data)
report = self.validate(data)
tsd = self._tsd(req)
diagram = self._diagram(req)
if not all(h in tsd for h in ("# Technical Solution Design", "## Component inventory", "## Operational considerations")):
raise RuntimeError("TSD renderer omitted a required heading")
return GenerationResult(tsd, diagram, report)
def _bullets(self, values: tuple[str, ...]) -> str:
return "\n".join(f"- {v}" for v in values) if values else "- None provided"
def _tsd(self, r: Requirements) -> str:
components = "\n".join(f"| {c.name} | {c.type} | {c.technology} | {c.description or 'Not specified'} |" for c in r.components) or "| None provided | - | - | - |"
envs = ", ".join(r.environments) if r.environments else "Not specified"
return f"""# Technical Solution Design: {r.application_name}\n\n## Scope and business context\n{r.business_context}\n\n## Environment strategy\n{envs}\n\n## Component inventory\n| Name | Type | Technology | Description |\n|---|---|---|---|\n{components}\n\n## Integrations\n{self._bullets(r.integrations)}\n\n## Constraints\n{self._bullets(r.constraints)}\n\n## Non-functional requirements\n{self._bullets(r.non_functional_requirements)}\n\n## Security and data considerations\n- Apply least-privilege identity to each workload.\n- Keep secrets in a managed secret store; no credentials are embedded in this design.\n- Confirm data classification, retention, network boundaries, and ingress/egress policy during review.\n\n## Operational considerations\n- Define health checks, structured logs, metrics, alert thresholds, backup/restore, and rollback ownership before release.\n- Confirm availability, recovery objectives, capacity, and cost guardrails against the stated requirements.\n\n## Assumptions and decisions needed\n{self._bullets(r.assumptions)}\n- Decisions needed: hosting region, identity integration, data classification, and deployment ownership.\n"""
def _diagram(self, r: Requirements) -> str:
cells = ['<mxCell id="0"/>', '<mxCell id="1" parent="0"/>']
def cell_id(prefix: str, i: int) -> str: return f"{prefix}-{i}"
cells.append('<mxCell id="boundary" value="Application landing zone" style="swimlane;horizontal=0;rounded=1;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="900" height="650" as="geometry"/></mxCell>')
for i, c in enumerate(r.components):
x, y = 90 + (i % 3) * 270, 130 + (i // 3) * 150
label = html.escape(f"{c.name}\\n{c.type}\\n{c.technology}")
cells.append(f'<mxCell id="{cell_id("component", i)}" value="{label}" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;" vertex="1" parent="boundary"><mxGeometry x="{x}" y="{y}" width="210" height="80" as="geometry"/></mxCell>')
for i, name in enumerate(r.integrations):
label = html.escape(name)
cells.append(f'<mxCell id="{cell_id("integration", i)}" value="{label}" style="shape=cloud;whiteSpace=wrap;html=1;fillColor=#fff2cc;" vertex="1" parent="1"><mxGeometry x="1020" y="{120 + i * 130}" width="190" height="80" as="geometry"/></mxCell>')
if r.components: cells.append(f'<mxCell id="edge-integration-{i}" value="integration" style="edgeStyle=orthogonalEdgeStyle;rounded=0;" edge="1" parent="1" source="component-0" target="{cell_id("integration", i)}"><mxGeometry relative="1" as="geometry"/></mxCell>')
for i in range(max(0, len(r.components) - 1)):
cells.append(f'<mxCell id="edge-component-{i}" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;" edge="1" parent="boundary" source="component-{i}" target="component-{i+1}"><mxGeometry relative="1" as="geometry"/></mxCell>')
body = "".join(cells)
return f'<?xml version="1.0" encoding="UTF-8"?><mxfile host="app.diagrams.net" version="24.7.17"><diagram id="landing-zone" name="Architecture"><mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" page="1" pageWidth="1600" pageHeight="1000"><root>{body}</root></mxGraphModel></diagram></mxfile>'
def load_config(path: str | None) -> dict[str, Any]:
return json.loads(Path(path).read_text()) if path else {}
def generate(self, input_data: dict, output_dir: str | Path) -> GenerationResult:
req = Requirements.from_dict(input_data)
destination = Path(output_dir)
destination.mkdir(parents=True, exist_ok=True)
tsd = render_tsd(req)
diagram = render_drawio(req)
tsd_path, diagram_path = destination / "tsd.md", destination / "architecture.drawio"
manifest_path = destination / "manifest.json"
tsd_path.write_text(tsd, encoding="utf-8")
diagram_path.write_text(diagram, encoding="utf-8")
manifest = {"application_name": req.application_name, "artifacts": {"tsd": str(tsd_path), "drawio": str(diagram_path)}, "format": {"tsd": "markdown", "drawio": "mxGraph XML"}}
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
return GenerationResult(str(tsd_path), str(diagram_path), str(manifest_path), tsd, diagram)

View File

@@ -1,23 +1,24 @@
import argparse, json, sys
from pathlib import Path
from .agent import LandingZoneAgent, ValidationError, load_config
import argparse
import json
import sys
from .agent import ApplicationLandingZoneAgent
def main(argv=None) -> int:
p = argparse.ArgumentParser(description="Generate a TSD and draw.io architecture diagram")
p.add_argument("--input", required=True, help="requirements JSON")
p.add_argument("--output-dir", required=True)
p.add_argument("--config", default="config/default.json")
args = p.parse_args(argv)
def main() -> int:
parser = argparse.ArgumentParser(description="Generate a TSD and draw.io architecture diagram")
parser.add_argument("--input", required=True, help="Requirements JSON file")
parser.add_argument("--output-dir", required=True)
args = parser.parse_args()
try:
data = json.loads(Path(args.input).read_text())
result = LandingZoneAgent(load_config(args.config)).generate(data)
out = Path(args.output_dir); out.mkdir(parents=True, exist_ok=True)
result_files = {"tsd.md": result.tsd_markdown, "architecture.drawio": result.drawio_xml, "validation.json": json.dumps(result.validation.__dict__, indent=2) + "\n"}
for name, content in result_files.items(): (out / name).write_text(content)
with open(args.input, encoding="utf-8") as handle:
payload = json.load(handle)
result = ApplicationLandingZoneAgent().generate(payload, args.output_dir)
print(json.dumps({"tsd": result.tsd_path, "drawio": result.diagram_path, "manifest": result.manifest_path}))
return 0
except ValidationError as exc:
print(f"validation error: {exc}", file=sys.stderr); return 2
except (OSError, json.JSONDecodeError, RuntimeError) as exc:
print(f"generation error: {exc}", file=sys.stderr); return 1
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__": sys.exit(main())
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,93 @@
from html import escape
from .models import Requirements
def _bullets(items: list[str], empty: str = "Not specified") -> str:
return "\n".join(f"- {item}" for item in items) if items else f"- {empty}"
def render_tsd(req: Requirements) -> str:
envs = ", ".join(req.environments)
return f'''# Technical Solution Design: {req.application_name}
> **Status:** Draft generated from supplied requirements
> **Data classification:** {req.data_classification}
> **Target region:** {req.region}
## 1. Executive summary
**Objective:** {req.business_objective}
This design establishes an application landing zone with separated environments ({envs}), controlled ingress, private application and data tiers, centralized observability, identity, security controls, and repeatable infrastructure delivery.
## 2. Requirements and assumptions
### Functional and quality requirements
{_bullets(req.requirements)}
### Constraints
{_bullets(req.constraints)}
### Integrations
{_bullets(req.integrations)}
Assumptions and unresolved decisions must be confirmed before production approval. The generated design is provider-neutral and uses managed services where available.
## 3. Architecture
The request path is: users or external systems → DNS/WAF/load balancer → application services → managed data services. Identity, secrets, audit logging, metrics, and tracing apply across all tiers. See `architecture.drawio` for the editable diagram.
## 4. Environment and network model
| Environment | Isolation | Deployment intent |
|---|---|---|
{chr(10).join(f'| {e} | Dedicated account/subscription and network boundary | {"Production" if e.lower() in ("prod", "production") else "Non-production"} |' for e in req.environments)}
Use private subnets for workloads and data, deny-by-default security groups/firewall rules, egress controls, and no direct public database access. Promote immutable artifacts through environments rather than rebuilding them.
## 5. Security, privacy, and compliance
- Federated identity with least-privilege role assignments and MFA.
- Secrets stored in a managed secret store; never committed to source control.
- Encryption in transit and at rest; keys and rotation ownership are explicit.
- Central audit logs are immutable, retained per policy, and monitored.
- {req.data_classification} data requires approved classification, retention, backup, and deletion procedures.
## 6. Reliability, backup, and disaster recovery
Target availability is **{req.availability_target}**. Define service-level objectives, health checks, autoscaling limits, multi-zone placement, backup frequency, retention, restore testing, RPO, and RTO before go-live. Failure modes include dependency outage, zone loss, credential compromise, and bad deployment; use rollback and tested recovery runbooks.
## 7. Delivery and operations
Infrastructure and application changes flow through code review, security scanning, unit/integration tests, plan review, progressive deployment, and automated rollback. Dashboards must cover latency, traffic, errors, saturation, dependency health, cost, and security events. Page on actionable symptoms and link alerts to runbooks.
## 8. Decisions required
- Confirm cloud/provider, region, RPO/RTO, retention, and compliance obligations.
- Confirm integration protocols, ownership, data flows, and throughput.
- Confirm sizing, scaling limits, budget, and on-call model.
## 9. Traceability
This document is generated from the supplied JSON requirements. Re-run the agent when requirements change and review the draft with application, security, networking, and operations owners.
'''
def render_drawio(req: Requirements) -> str:
# Native mxGraph XML; labels are escaped to remain valid for arbitrary names.
name = escape(req.application_name)
env = escape(", ".join(req.environments))
cells = [
'<mxCell id="0"/>', '<mxCell id="1" parent="0"/>',
f'<mxCell id="user" value="Users / integrations" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf" vertex="1" parent="1"><mxGeometry x="40" y="180" width="140" height="60" as="geometry"/></mxCell>',
'<mxCell id="edge" value="HTTPS" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656" vertex="1" parent="1"><mxGeometry x="230" y="180" width="150" height="60" as="geometry"/></mxCell>',
f'<mxCell id="app" value="{name}\\nApplication tier" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366" vertex="1" parent="1"><mxGeometry x="430" y="180" width="180" height="60" as="geometry"/></mxCell>',
'<mxCell id="data" value="Managed data tier" style="shape=cylinder;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6" vertex="1" parent="1"><mxGeometry x="680" y="180" width="150" height="60" as="geometry"/></mxCell>',
f'<mxCell id="ops" value="Identity | secrets | logs\\nEnvironments: {env}" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450" vertex="1" parent="1"><mxGeometry x="430" y="310" width="250" height="70" as="geometry"/></mxCell>',
'<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block" edge="1" parent="1" source="user" target="edge"><mxGeometry relative="1" as="geometry"/></mxCell>',
'<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block" edge="1" parent="1" source="edge" target="app"><mxGeometry relative="1" as="geometry"/></mxCell>',
'<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block" edge="1" parent="1" source="app" target="data"><mxGeometry relative="1" as="geometry"/></mxCell>',
'<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;dashed=1;endArrow=block" edge="1" parent="1" source="ops" target="app"><mxGeometry relative="1" as="geometry"/></mxCell>',
]
body = "".join(cells)
return f'<?xml version="1.0" encoding="UTF-8"?><mxfile host="app.diagrams.net" version="24.7.17"><diagram id="landing-zone" name="Architecture"><mxGraphModel><root>{body}</root></mxGraphModel></diagram></mxfile>'

View File

@@ -0,0 +1,47 @@
from dataclasses import dataclass, field
from typing import Any
@dataclass
class Requirements:
application_name: str
business_objective: str
environments: list[str]
requirements: list[str] = field(default_factory=list)
constraints: list[str] = field(default_factory=list)
integrations: list[str] = field(default_factory=list)
data_classification: str = "Internal"
availability_target: str = "99.9%"
region: str = "To be confirmed"
@classmethod
def from_dict(cls, value: dict[str, Any]) -> "Requirements":
required = ("application_name", "business_objective", "environments")
missing = [key for key in required if not value.get(key)]
if missing:
raise ValueError("Missing required fields: " + ", ".join(missing))
environments = value["environments"]
if not isinstance(environments, list) or not all(isinstance(x, str) and x.strip() for x in environments):
raise ValueError("environments must be a non-empty list of strings")
def strings(key: str) -> list[str]:
item = value.get(key, [])
if not isinstance(item, list) or not all(isinstance(x, str) for x in item):
raise ValueError(f"{key} must be a list of strings")
return item
return cls(
application_name=str(value["application_name"]),
business_objective=str(value["business_objective"]),
environments=environments,
requirements=strings("requirements"), constraints=strings("constraints"),
integrations=strings("integrations"), data_classification=str(value.get("data_classification", "Internal")),
availability_target=str(value.get("availability_target", "99.9%")), region=str(value.get("region", "To be confirmed")),
)
@dataclass
class GenerationResult:
tsd_path: str
diagram_path: str
manifest_path: str
tsd: str
diagram: str