From fb20521880fbf00c51f3ac439cf60ed6e3f1b01b Mon Sep 17 00:00:00 2001 From: demo-bot Date: Mon, 31 Aug 2026 14:46:50 +0000 Subject: [PATCH] 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. --- CHANGELOG.md | 6 ++ README.md | 15 ++- docs/agent-specification.md | 31 +++---- docs/repository-conventions.md | 10 +- docs/system-prompt.md | 3 + docs/validation-report.md | 12 +-- examples/requirements.json | 15 +-- pyproject.toml | 11 ++- .../__init__.py | 8 +- src/application_landing_zone_agent/agent.py | 87 ++++------------- src/application_landing_zone_agent/cli.py | 39 ++++---- .../generator.py | 93 +++++++++++++++++++ src/application_landing_zone_agent/models.py | 47 ++++++++++ tests/test_agent.py | 53 +++++------ 14 files changed, 258 insertions(+), 172 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/system-prompt.md create mode 100644 src/application_landing_zone_agent/generator.py create mode 100644 src/application_landing_zone_agent/models.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..64d5a3b --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 8701ab5..e168bb8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/agent-specification.md b/docs/agent-specification.md index 1ad0a5d..8492dc8 100644 --- a/docs/agent-specification.md +++ b/docs/agent-specification.md @@ -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 `` root and ``, 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. diff --git a/docs/repository-conventions.md b/docs/repository-conventions.md index 6eb088f..c58ecb6 100644 --- a/docs/repository-conventions.md +++ b/docs/repository-conventions.md @@ -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/` 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/` 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. diff --git a/docs/system-prompt.md b/docs/system-prompt.md new file mode 100644 index 0000000..01b9506 --- /dev/null +++ b/docs/system-prompt.md @@ -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. diff --git a/docs/validation-report.md b/docs/validation-report.md index 4d9156e..adb14d5 100644 --- a/docs/validation-report.md +++ b/docs/validation-report.md @@ -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. diff --git a/examples/requirements.json b/examples/requirements.json index 4850865..bb215d3 100644 --- a/examples/requirements.json +++ b/examples/requirements.json @@ -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" } diff --git a/pyproject.toml b/pyproject.toml index ff1a227..f7578ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/application_landing_zone_agent/__init__.py b/src/application_landing_zone_agent/__init__.py index b0278e5..cd024d8 100644 --- a/src/application_landing_zone_agent/__init__.py +++ b/src/application_landing_zone_agent/__init__.py @@ -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" diff --git a/src/application_landing_zone_agent/agent.py b/src/application_landing_zone_agent/agent.py index 9734cfe..7b133c2 100644 --- a/src/application_landing_zone_agent/agent.py +++ b/src/application_landing_zone_agent/agent.py @@ -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 = ['', ''] - def cell_id(prefix: str, i: int) -> str: return f"{prefix}-{i}" - cells.append('') - 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'') - for i, name in enumerate(r.integrations): - label = html.escape(name) - cells.append(f'') - if r.components: cells.append(f'') - for i in range(max(0, len(r.components) - 1)): - cells.append(f'') - body = "".join(cells) - return f'{body}' - -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) diff --git a/src/application_landing_zone_agent/cli.py b/src/application_landing_zone_agent/cli.py index fb6ff4b..1c35e2b 100644 --- a/src/application_landing_zone_agent/cli.py +++ b/src/application_landing_zone_agent/cli.py @@ -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()) diff --git a/src/application_landing_zone_agent/generator.py b/src/application_landing_zone_agent/generator.py new file mode 100644 index 0000000..63bab5d --- /dev/null +++ b/src/application_landing_zone_agent/generator.py @@ -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 = [ + '', '', + f'', + '', + f'', + '', + f'', + '', + '', + '', + '', + ] + body = "".join(cells) + return f'{body}' diff --git a/src/application_landing_zone_agent/models.py b/src/application_landing_zone_agent/models.py new file mode 100644 index 0000000..917ba28 --- /dev/null +++ b/src/application_landing_zone_agent/models.py @@ -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 diff --git a/tests/test_agent.py b/tests/test_agent.py index e34696c..05fc3f8 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -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("", result["drawio"]) - self.assertTrue(result["manifest"]["validation"]["drawio"]) +def payload(): + return {"application_name": "Demo ", "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 " 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"}