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:
6
CHANGELOG.md
Normal file
6
CHANGELOG.md
Normal 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.
|
||||||
15
README.md
15
README.md
@@ -1,20 +1,17 @@
|
|||||||
# Application Landing Zone Agent
|
# 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
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m landing_zone_agent.cli --requirements examples/requirements.json --output-dir out
|
python -m application_landing_zone_agent --input examples/requirements.json --output-dir out
|
||||||
python -m unittest discover -s tests -v
|
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
|
## 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`.
|
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.
|
||||||
## 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.
|
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
# Application landing zone agent specification
|
# Application Landing Zone Agent specification
|
||||||
|
|
||||||
**Name:** `application_landing_zone_agent`
|
## 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.
|
||||||
**Purpose:** Transform application requirements into a reviewable Technical Solution Design and an editable draw.io architecture diagram.
|
|
||||||
|
|
||||||
## Input contract
|
## 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
|
## 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
|
## Processing workflow
|
||||||
1. Parse and validate the requirements.
|
1. Parse JSON and validate required fields and array types.
|
||||||
2. Apply safe baseline assumptions and identify unresolved decisions.
|
2. Normalize defaults for omitted governance fields.
|
||||||
3. Generate TSD sections for context, scope, architecture, environments, security, resilience, operations, constraints, and acceptance criteria.
|
3. Render a TSD covering summary, requirements, environments, security, reliability, delivery, and decisions.
|
||||||
4. Generate an XML draw.io file with users, application boundary, components, and orthogonal relationships.
|
4. Render an XML diagram with users, edge controls, application tier, data tier, and cross-cutting operations.
|
||||||
5. Validate artifact structure and return the manifest.
|
5. Persist artifacts and a machine-readable manifest.
|
||||||
|
|
||||||
## Diagram requirements
|
## Validation and error handling
|
||||||
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.
|
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
|
## Conventions discovered
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
# Repository conventions and discovery record
|
# Repository conventions and discovery record
|
||||||
|
|
||||||
## Discovery scope
|
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.
|
||||||
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.
|
|
||||||
|
|||||||
3
docs/system-prompt.md
Normal file
3
docs/system-prompt.md
Normal 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.
|
||||||
@@ -1,11 +1,3 @@
|
|||||||
# Validation plan and report
|
# Validation report
|
||||||
|
|
||||||
The repository validation covers:
|
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.
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"application_name": "Customer Portal",
|
"application_name": "Order Portal",
|
||||||
"business_context": "Self-service customer account management",
|
"business_objective": "Provide a secure self-service ordering experience.",
|
||||||
"environments": ["dev", "test", "prod"],
|
"environments": ["dev", "test", "prod"],
|
||||||
"data_classification": "confidential",
|
"requirements": ["Support 10,000 concurrent users", "Expose health and metrics endpoints"],
|
||||||
"availability_target": "99.9%",
|
"constraints": ["Private connectivity to the ERP"],
|
||||||
"components": ["web frontend", "api service", "relational database"],
|
"integrations": ["ERP over private API", "Corporate identity provider"],
|
||||||
"integrations": ["CRM", "email provider"],
|
"data_classification": "Confidential",
|
||||||
"constraints": ["private network access", "audit logging"]
|
"availability_target": "99.95%",
|
||||||
|
"region": "Primary region"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61"]
|
requires = ["setuptools>=68"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "application-landing-zone-agent"
|
name = "application-landing-zone-agent"
|
||||||
version = "0.1.0"
|
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"
|
requires-python = ">=3.10"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
[project.scripts]
|
[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]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Application landing zone agent public API."""
|
"""Application landing zone document and architecture generator."""
|
||||||
from .agent import LandingZoneAgent, GenerationResult, ValidationError
|
|
||||||
|
|
||||||
__all__ = ["LandingZoneAgent", "GenerationResult", "ValidationError"]
|
from .agent import ApplicationLandingZoneAgent
|
||||||
|
from .models import GenerationResult, Requirements
|
||||||
|
|
||||||
|
__all__ = ["ApplicationLandingZoneAgent", "GenerationResult", "Requirements"]
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|||||||
@@ -1,75 +1,22 @@
|
|||||||
import html
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from .generator import render_drawio, render_tsd
|
||||||
from .model import Component, Requirements, GenerationResult, ValidationReport
|
from .models import GenerationResult, Requirements
|
||||||
|
|
||||||
class ValidationError(ValueError):
|
|
||||||
"""Raised when requirements cannot satisfy the input contract."""
|
|
||||||
|
|
||||||
class LandingZoneAgent:
|
class ApplicationLandingZoneAgent:
|
||||||
def __init__(self, config: dict[str, Any] | None = None):
|
"""Transform validated application requirements into reviewable artifacts."""
|
||||||
self.config = config or {}
|
|
||||||
|
|
||||||
def normalize(self, data: dict[str, Any]) -> Requirements:
|
def generate(self, input_data: dict, output_dir: str | Path) -> GenerationResult:
|
||||||
report = self.validate(data)
|
req = Requirements.from_dict(input_data)
|
||||||
if report.errors:
|
destination = Path(output_dir)
|
||||||
raise ValidationError("; ".join(report.errors))
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
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", []))
|
tsd = render_tsd(req)
|
||||||
def strings(key: str) -> tuple[str, ...]:
|
diagram = render_drawio(req)
|
||||||
return tuple(str(x) for x in data.get(key, []))
|
tsd_path, diagram_path = destination / "tsd.md", destination / "architecture.drawio"
|
||||||
integrations = tuple(str(x.get("name", "")) if isinstance(x, dict) else str(x) for x in data.get("integrations", []))
|
manifest_path = destination / "manifest.json"
|
||||||
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))
|
tsd_path.write_text(tsd, encoding="utf-8")
|
||||||
|
diagram_path.write_text(diagram, encoding="utf-8")
|
||||||
def validate(self, data: Any) -> ValidationReport:
|
manifest = {"application_name": req.application_name, "artifacts": {"tsd": str(tsd_path), "drawio": str(diagram_path)}, "format": {"tsd": "markdown", "drawio": "mxGraph XML"}}
|
||||||
errors: list[str] = []
|
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||||
if not isinstance(data, dict): return ValidationReport(False, ["Input must be a JSON object"])
|
return GenerationResult(str(tsd_path), str(diagram_path), str(manifest_path), tsd, diagram)
|
||||||
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 {}
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import argparse, json, sys
|
import argparse
|
||||||
from pathlib import Path
|
import json
|
||||||
from .agent import LandingZoneAgent, ValidationError, load_config
|
import sys
|
||||||
|
from .agent import ApplicationLandingZoneAgent
|
||||||
|
|
||||||
def main(argv=None) -> int:
|
|
||||||
p = argparse.ArgumentParser(description="Generate a TSD and draw.io architecture diagram")
|
def main() -> int:
|
||||||
p.add_argument("--input", required=True, help="requirements JSON")
|
parser = argparse.ArgumentParser(description="Generate a TSD and draw.io architecture diagram")
|
||||||
p.add_argument("--output-dir", required=True)
|
parser.add_argument("--input", required=True, help="Requirements JSON file")
|
||||||
p.add_argument("--config", default="config/default.json")
|
parser.add_argument("--output-dir", required=True)
|
||||||
args = p.parse_args(argv)
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
data = json.loads(Path(args.input).read_text())
|
with open(args.input, encoding="utf-8") as handle:
|
||||||
result = LandingZoneAgent(load_config(args.config)).generate(data)
|
payload = json.load(handle)
|
||||||
out = Path(args.output_dir); out.mkdir(parents=True, exist_ok=True)
|
result = ApplicationLandingZoneAgent().generate(payload, args.output_dir)
|
||||||
result_files = {"tsd.md": result.tsd_markdown, "architecture.drawio": result.drawio_xml, "validation.json": json.dumps(result.validation.__dict__, indent=2) + "\n"}
|
print(json.dumps({"tsd": result.tsd_path, "drawio": result.diagram_path, "manifest": result.manifest_path}))
|
||||||
for name, content in result_files.items(): (out / name).write_text(content)
|
|
||||||
return 0
|
return 0
|
||||||
except ValidationError as exc:
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||||
print(f"validation error: {exc}", file=sys.stderr); return 2
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
except (OSError, json.JSONDecodeError, RuntimeError) as exc:
|
return 2
|
||||||
print(f"generation error: {exc}", file=sys.stderr); return 1
|
|
||||||
|
|
||||||
if __name__ == "__main__": sys.exit(main())
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|||||||
93
src/application_landing_zone_agent/generator.py
Normal file
93
src/application_landing_zone_agent/generator.py
Normal 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>'
|
||||||
47
src/application_landing_zone_agent/models.py
Normal file
47
src/application_landing_zone_agent/models.py
Normal 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
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
import json, tempfile, unittest
|
import json
|
||||||
from pathlib import Path
|
import xml.etree.ElementTree as ET
|
||||||
from landing_zone_agent.generator import generate
|
from application_landing_zone_agent import ApplicationLandingZoneAgent
|
||||||
from landing_zone_agent.validation import validate_requirements
|
|
||||||
|
|
||||||
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):
|
def payload():
|
||||||
result = generate(self.req)
|
return {"application_name": "Demo <Portal>", "business_objective": "Serve users", "environments": ["dev", "prod"], "requirements": ["R1"]}
|
||||||
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 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):
|
def test_generates_all_artifacts(tmp_path):
|
||||||
from landing_zone_agent.cli import main
|
result = ApplicationLandingZoneAgent().generate(payload(), tmp_path)
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
assert (tmp_path / "tsd.md").exists()
|
||||||
req_path = Path(directory) / "req.json"; out = Path(directory) / "out"
|
assert "Demo <Portal>" in result.tsd
|
||||||
req_path.write_text(json.dumps(self.req), encoding="utf-8")
|
assert "## 5. Security" in result.tsd
|
||||||
self.assertEqual(main(["--requirements", str(req_path), "--output-dir", str(out)]), 0)
|
assert (tmp_path / "manifest.json").exists()
|
||||||
self.assertEqual({p.name for p in out.iterdir()}, {"tsd.md", "architecture.drawio", "manifest.json"})
|
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"}
|
||||||
|
|||||||
Reference in New Issue
Block a user