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

6
CHANGELOG.md Normal file
View File

@@ -0,0 +1,6 @@
# Changelog
## 0.1.0
- Added validated requirements model and callable landing zone agent.
- Added TSD Markdown and native draw.io XML generators.
- Added CLI, example input, contracts, and tests.

View File

@@ -1,20 +1,17 @@
# Application Landing Zone Agent
A deterministic, dependency-light agent that converts application requirements into a Technical Solution Design (TSD) and an editable draw.io architecture diagram.
A deterministic, callable agent that converts application requirements into a Technical Solution Design (TSD) document and a diagrams.net/draw.io architecture diagram.
## Quick start
```bash
python -m landing_zone_agent.cli --requirements examples/requirements.json --output-dir out
python -m unittest discover -s tests -v
python -m application_landing_zone_agent --input examples/requirements.json --output-dir out
pytest
```
The CLI writes `tsd.md`, `architecture.drawio`, and `manifest.json`. The generator does not call an LLM or cloud APIs; it produces a reviewable baseline that can be refined by an architect.
The CLI writes `tsd.md`, `architecture.drawio`, and `manifest.json`. No network access or provider credentials are required.
## Contract
Input is JSON with `application_name`, `business_context`, `environments`, `data_classification`, `availability_target`, and optional `components`, `integrations`, and `constraints`. Output is a TSD Markdown document, draw.io XML, and a manifest containing assumptions and validation results. See `docs/agent-specification.md`.
## Configuration
`config/agent.json` contains defaults and capability declarations. Configuration is loaded by `landing_zone_agent.config.load_config`; no secrets are stored in the repository.
Input is JSON with required `application_name`, `business_objective`, and `environments`; optional `requirements`, `constraints`, `integrations`, `data_classification`, `availability_target`, and `region`.
Output is a manifest containing paths and validation metadata. The TSD is Markdown and the diagram is native, editable draw.io XML.

View File

@@ -1,24 +1,23 @@
# Application landing zone agent specification
# Application Landing Zone Agent specification
**Name:** `application_landing_zone_agent`
**Purpose:** Transform application requirements into a reviewable Technical Solution Design and an editable draw.io architecture diagram.
## Purpose
Transform structured application requirements into a provider-neutral Technical Solution Design (TSD) and an editable draw.io architecture diagram. The agent is deterministic, review-first, and does not invent provider-specific resources when the input is silent.
## Input contract
JSON object fields: required `application_name`, `business_context`, `environments` (non-empty array), `data_classification`, `availability_target`; optional array fields `components`, `integrations`, `constraints`.
JSON object: `application_name` (string), `business_objective` (string), and `environments` (non-empty string array) are required. Optional arrays are `requirements`, `constraints`, and `integrations`; optional strings are `data_classification`, `availability_target`, and `region`.
## Output contract
`generate()` returns `{tsd: string, drawio: string, manifest: object}`. The CLI writes `tsd.md`, `architecture.drawio`, and `manifest.json`.
The agent writes `tsd.md`, `architecture.drawio` (native mxGraph XML), and `manifest.json` to the requested output directory. It returns their paths plus in-memory content. The manifest identifies formats and artifact paths.
## Workflow
1. Parse and validate the requirements.
2. Apply safe baseline assumptions and identify unresolved decisions.
3. Generate TSD sections for context, scope, architecture, environments, security, resilience, operations, constraints, and acceptance criteria.
4. Generate an XML draw.io file with users, application boundary, components, and orthogonal relationships.
5. Validate artifact structure and return the manifest.
## Processing workflow
1. Parse JSON and validate required fields and array types.
2. Normalize defaults for omitted governance fields.
3. Render a TSD covering summary, requirements, environments, security, reliability, delivery, and decisions.
4. Render an XML diagram with users, edge controls, application tier, data tier, and cross-cutting operations.
5. Persist artifacts and a machine-readable manifest.
## Diagram requirements
The diagram must be importable by draw.io, use an `<mxfile>` root and `<mxGraphModel>`, include vertex cells for the users, application, and components, and include relationships. It must remain editable XML rather than an image.
## Validation and error handling
Reject missing required fields, non-list environment values, and arrays containing non-strings with `ValueError`; CLI reports an error and exits 2. XML must parse, the diagram must contain an `mxGraphModel`, and the TSD must contain security and reliability sections. Ambiguous architecture decisions are recorded as decisions required, not silently resolved.
## Validation and errors
Missing required fields, malformed types, and empty environments raise `ValueError` with actionable field names. A diagram contract failure raises `RuntimeError`. No credentials are inferred or emitted.
## Conventions discovered
The implementation uses a `src/` package layout, `pyproject.toml` configuration, a thin public agent class, a CLI entry point, Markdown documentation, pytest tests, and native draw.io XML. These conventions are recorded here because the source template/target URLs were not provided in the request; the repository remains self-contained and provider-neutral.

View File

@@ -1,11 +1,3 @@
# Repository conventions and discovery record
## Discovery scope
The requested template and target repository URLs were not supplied to the build request. This repository therefore uses the standard Python agent landing-zone convention: `src/` package layout, `pyproject.toml`, JSON configuration, a callable package API, CLI wrapper, tests, and human-readable docs.
## Extracted conventions applied
- Keep runtime code under `src/<package>` and tests under `tests`.
- Expose a small programmatic interface and a CLI entry point.
- Keep configuration declarative and secret-free.
- Produce deterministic, reviewable artifacts in a caller-selected output directory.
- Document contracts, assumptions, validation, and diagram editing expectations.
The requested template and target repository URLs were not included, so discovery could not inspect external source trees. This project follows the expected single-agent conventions: `src/<package>` implementation, `tests/` pytest coverage, `docs/` contract/prompt material, `examples/` runnable input, `pyproject.toml` packaging, and a CLI entry point. Configuration is declarative in `pyproject.toml`; interfaces are typed dataclasses plus a public `ApplicationLandingZoneAgent.generate` method. Diagram output is editable draw.io mxGraph XML rather than an image.

3
docs/system-prompt.md Normal file
View File

@@ -0,0 +1,3 @@
# System prompt
You are the Application Landing Zone Agent. Convert the user's application requirements into a reviewable Technical Solution Design and an editable draw.io architecture diagram. Validate the required fields before generation. Preserve stated constraints and integrations. Use provider-neutral architecture language unless a provider is explicitly named. Always cover environment isolation, network boundaries, identity, secrets, logging, monitoring, encryption, backup/recovery, deployment, cost, and unresolved decisions. Never claim a compliance certification or invent capacity numbers. Produce `tsd.md`, `architecture.drawio`, and `manifest.json`; ensure the diagram is native mxGraph XML and all user-provided labels are XML-escaped. If information is missing, state an assumption or decision required. Return concise artifact paths and validation status.

View File

@@ -1,11 +1,3 @@
# Validation plan and report
# Validation report
The repository validation covers:
- Unit tests for required-field/type validation, TSD sections, and draw.io XML markers.
- CLI packaging smoke test that writes all three expected files.
- Human review checklist for assumptions, security controls, environment isolation, and editable diagram structure.
Run `python -m unittest discover -s tests -v` and `python -m landing_zone_agent.cli --requirements examples/requirements.json --output-dir /tmp/landing-zone-agent-check`.
The generated baseline is provider-neutral by design; provider-specific landing-zone controls require confirmation of cloud, identity, network, RTO/RPO, retention, and sizing.
Validation is performed by `pytest` and by the checks in `tests/test_agent.py`: required-field rejection, TSD section presence, manifest JSON shape, XML parsing, and `mxGraphModel` presence. The CLI is packaged through the `application-landing-zone-agent` script and can be smoke-tested with the example input.

View File

@@ -1,10 +1,11 @@
{
"application_name": "Customer Portal",
"business_context": "Self-service customer account management",
"application_name": "Order Portal",
"business_objective": "Provide a secure self-service ordering experience.",
"environments": ["dev", "test", "prod"],
"data_classification": "confidential",
"availability_target": "99.9%",
"components": ["web frontend", "api service", "relational database"],
"integrations": ["CRM", "email provider"],
"constraints": ["private network access", "audit logging"]
"requirements": ["Support 10,000 concurrent users", "Expose health and metrics endpoints"],
"constraints": ["Private connectivity to the ERP"],
"integrations": ["ERP over private API", "Corporate identity provider"],
"data_classification": "Confidential",
"availability_target": "99.95%",
"region": "Primary region"
}

View File

@@ -1,15 +1,20 @@
[build-system]
requires = ["setuptools>=61"]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "application-landing-zone-agent"
version = "0.1.0"
description = "Generate TSD documents and draw.io diagrams from application requirements"
description = "Generate TSD documents and draw.io architecture diagrams from application requirements"
requires-python = ">=3.10"
dependencies = []
[project.scripts]
application-landing-zone-agent = "landing_zone_agent.cli:main"
application-landing-zone-agent = "application_landing_zone_agent.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
[tool.setuptools.packages.find]
where = ["src"]

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

View File

@@ -1,31 +1,32 @@
import json, tempfile, unittest
from pathlib import Path
from landing_zone_agent.generator import generate
from landing_zone_agent.validation import validate_requirements
import json
import xml.etree.ElementTree as ET
from application_landing_zone_agent import ApplicationLandingZoneAgent
class AgentTests(unittest.TestCase):
def setUp(self):
self.req = {"application_name":"Demo", "business_context":"Demo service", "environments":["dev","prod"], "data_classification":"internal", "availability_target":"99.9%", "components":["API","DB"]}
def test_generates_tsd_and_drawio(self):
result = generate(self.req)
self.assertIn("# Technical Solution Design: Demo", result["tsd"])
self.assertIn("Security, resilience, and operations", result["tsd"])
self.assertTrue(result["drawio"].startswith("<mxfile"))
self.assertIn("<mxGraphModel>", result["drawio"])
self.assertTrue(result["manifest"]["validation"]["drawio"])
def payload():
return {"application_name": "Demo <Portal>", "business_objective": "Serve users", "environments": ["dev", "prod"], "requirements": ["R1"]}
def test_missing_field_is_actionable(self):
with self.assertRaisesRegex(ValueError, "data_classification"):
validate_requirements({**self.req, "data_classification": ""})
def test_cli_writes_contract_files(self):
from landing_zone_agent.cli import main
with tempfile.TemporaryDirectory() as directory:
req_path = Path(directory) / "req.json"; out = Path(directory) / "out"
req_path.write_text(json.dumps(self.req), encoding="utf-8")
self.assertEqual(main(["--requirements", str(req_path), "--output-dir", str(out)]), 0)
self.assertEqual({p.name for p in out.iterdir()}, {"tsd.md", "architecture.drawio", "manifest.json"})
def test_generates_all_artifacts(tmp_path):
result = ApplicationLandingZoneAgent().generate(payload(), tmp_path)
assert (tmp_path / "tsd.md").exists()
assert "Demo <Portal>" in result.tsd
assert "## 5. Security" in result.tsd
assert (tmp_path / "manifest.json").exists()
ET.fromstring(result.diagram)
assert "mxGraphModel" in result.diagram
if __name__ == "__main__":
unittest.main()
def test_input_validation(tmp_path):
try:
ApplicationLandingZoneAgent().generate({"application_name": "x"}, tmp_path)
except ValueError as exc:
assert "business_objective" in str(exc)
else:
raise AssertionError("invalid input accepted")
def test_manifest_is_json(tmp_path):
ApplicationLandingZoneAgent().generate(payload(), tmp_path)
manifest = json.loads((tmp_path / "manifest.json").read_text())
assert set(manifest["artifacts"]) == {"tsd", "drawio"}