Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PyCONTAaC - Python implementation of Continuous Architecting as Code

PyCONTAaC is the Python implementation of Continuous Architecting as Code framework. It treats architecture as a machine-readable and executable artifact that you can generate, render for different stakeholders' views, manage in the two ecosystem- and architecture-driven evolution flows and include in CI/CD pipelines.

Project Structure

pycontaac/
├── mobility_example/             # Working use case (smart mobility)
│   ├── software/                 # Python modules (simplified mobility DT)
│   ├── calm/                     # Architecture CALM versioned records (AVR)
│   ├── decisions/                # Architecture Decision Records (ADR, markdown)
│   └── views/                    # Generated architecture descriptions (HTML per each versioned view)
│
├── tool/                         # PyCONTAaC engine and CLI
│   ├── policies/                 # JSON policies and architectural pattern rules (APR)
│   ├── templates/                # Templates for decisions, AD rendering, and stakeholder views
│   │   ├── decisions/            # Templates for ADR/CALM decision generation
│   │   ├── views/                # Stakeholder view templates
│   │   │   ├── filters/          # Filters controlling visible content per stakeholder type
│   │   │   ├── generators/       # HTML and D2 (diagram) generation templates
│   │   │   └── viewsets.yaml     # Stakeholder presets (PM, dev, sys-user, ops, etc.)
│   ├── calm_ui.py                # Proxy/bridge to FINOS CALM CLI
│   ├── calm_utils.py             # Orchestration utilities for CALM Hub UI
│   ├── cli.py                    # CLI entry points (aac commands)
│   ├── conformance_engine.py     # Policy and conformance evaluation engine
│   ├── constants.py              # Tool-wide constants and identifiers
│   ├── decision_manager.py       # Create/read/update decisions (ADR)
│   ├── detector.py               # Code change detection
│   ├── extractor.py              # Architecture extraction from existing codebase
│   ├── monitor.py                # Monitoring logic for continuous architecting
│   ├── orchestrator.py           # Main workflow manager for evolution flows
│   ├── policy_manager.py         # Load, validate, and apply architecture policies
│   ├── scaffolder.py             # Project scaffolding and file generation
│   └── view_manager.py           # Stakeholder view (CALM) generation and export
│
├── LICENSE
├── pyproject.toml
├── requirements.txt
└── README.md                     # This documentation

Requirements and Installation

Requirements

  • Python 3.10+
  • pip (or pipx)
  • git
For CALM Hub UI
  • Node.js + npm
  • Java 11+
  • On Windows, use PowerShell or CMD (Hub’s scripts support mvnw.cmd under the hood).

Install on:

Linux/macOS
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
Windows (PowerShell)
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .

To check if installation went well, run:

aac --help

If the CLI is installed correctly, you’ll see the following usage:

usage: aac [-h] {arch:generate,gen:view,adr:new,adr:apply,guard:run,
                sync:from-code,sync:from-arch,check:pattern,calm,calm:ui} 
 ...

How to run the two evolution flows:

In this section, the two types of ecosystem-driven and architecture-driven evolution flows are described, with respect to the smart mobility example. For a full description of the available commands offered by the PyCONTAaC tool, please refee to following section.

The repository includes a smart mobility example ecosystem in mobility_example/software, which represents an urban tiny digital twin. More in detail, the twin simulates vehicles moving through traffic lights and persists state in a simple storage service. It contains different Python modules (app.py, simulator.py, data_manager.py, sharedstorage_db.py, traffic_light.py, vehicle.py) that interact to form a small but realistic mobility ecosystem.

Ecosystem-driven Flow

The goal of this case is to extract the architectural description from the existing codebase directly, document the decision that justifies its creation and modify some aspects in the ecosystem codebase to check if these are effectively propagated in the architecture description.

Let's start from the creation of an ADR for creating the first version of the architecture description, to record the rationale that justifies the bootstrapping. The ADR is documented as a markdown file by using the documentation framework of [REF]. In particular, the ADR is characterized by mandatory features that are the ID, the version, and a state (like decided, idea, tentative). An ADR may include several optional but highly informative descriptive sections: for example, the problem motivating the decision, the decision itself, the alternatives that were considered, and the positive or negative consequences associated with the choice. Additional optional attributes can capture business goals, quality attributes, risks, or relationships with other ADRs, enabling richer traceability across architectural evolution.

For example (please ensure the tool is properly installed before running the following command):

aac adr:new \
  --id ADR-001 \
  --version 1.0 \
  --name "Bootstrap architecture from code" \
  --state Decided \
  --decision "Initial generation of architecture description from existing codebase" \
  --problem "We need an architecture baseline." \
  --alternatives "Manual architecture diagrams" \
  --positive "Single source of truth; versioned architecture" \
  --negative "Initial overhead of CALM integration" \
  --business-goals "governance" "traceability" \
  --quality-attributes "evolvability" "modifiability"

This command creates the folder decisions (if it does not already exist) and inside it generates the file ADR-001.md, which documents the initial architectural decision.

Once we documented a first decision, we can extract the architectural elements, relations and constraints from the existing codebase and serialize the in CALM language (JSON-based) format.

aac arch:generate --arch-name smart-mobility --version 1.0.0 --adr ADR-001

Output:

[OK] architecture.json generated @ version 1.0.0 (ADR: ADR-001) 

This creates the file mobility_example/calm/architecture.json, which contains the extracted architectural elements and relations. For every future update, older versions of the architecture will be archived under mobility_example/calm/versions/.

PyCONTAaC also provides a guarded execution mode, which allows the ecosystem to be run only when the architecture description and the codebase are aligned. In practice, the ecosystem may evolve independently from the architecture description, the guard acts as a control point before running the system. When the guarded run command is invoked, the tool activates the framework components (monitor, detector, etc) to check whether any changes occurred either in the architecture description or in the codebase. If, at that moment, the two representations are aligned, the system is allowed to run.

aac guard:run --entry mobility_example/software/app.py --arch-name smart-mobility --version 1.0.0 --adr ADR-001

If the code and the architecture elements and relations (version 1.0.0) are aligned, the output will confirm compliance and execute the system:

[OK] compliant — running entrypoint
[STEP 0] snapshot=...
[STEP 1] snapshot=...
[STEP 2] snapshot=...
[DONE] smart-mobility example finished.

At this point, we have:

  • the ecosystem codebase,
  • the architecture elements and relations extracted from it,
  • and an ADR documenting the rationale for this version.

As introduced earlier, the architecture description is represented as a triplet consisting of architectural views, architectural decisions, and architectural policies. In this implementation, we defined three types of views, each aligned with a specific stakeholder concern:

  • Project Manager View: provides a high-level overview of the architecture structure and highlights its evolution over time, by showing ADRs (if existing) and their relationships, helping the PMs understand the rationale behind architectural decisions and changes.
  • System-User View: focuses on the externally visible behavior of the ecosystem, such as how to run the system, which entrypoints or endpoints exist, and which components are exposed. It is designed for readers who interact with the system operationally rather than through its code.
  • Developer View: offers a more technical perspective, listing components, their interfaces, and their relations. It also allows to visualize the CALM-based view using CALM UI.

For example, to generate a developer view, we can run the command:

aac gen:view --view developer

Expected output:

[OK] view 'developer' → path/to/example/views/developer/v<ARCH_VERSION>/<timestamp>.html

The overall view, for a specific stakeholder, is rendered as an HTML webpage generated in the mobility_example/views/VIEW_POINT folder, containing the architecture descriptions for that viewpoint. If views are updated, the older views are versioned in the version folder within the views folder. For the developer, we may also visualize the diagram for the architectural view described in CALM, by using CALM UI.

The tool can clone and run the CALM Hub UI locally. Run command:

aac calm:ui --repo-dir third_party/architecture-as-code --open

The first run may clone/build the UI into third_party/architecture-as-code. By default, the UI runs on http://localhost:8080 (Quarkus dev default). If the browser does not open automatically, open this address manually. Once open, you may run the UI directly on port 8080 or you can click on the "CALM UI" button in the developer view, to open the CALM interface. Then, use the file picker in the UI to load the architectural view file of the developer. This will render in a visual architectural diagram the current uploaded architectural view.

Let us suppose to add a new analytics service that uses DataManager component. We intentionally make the code change first, which may cause architecture drift, if the architecture is not updated accordingly. We can think to both document the ecosystem codebase change in a new ADR or not document it and use the already existing ADRs. In this example, we will insert a new ADRs to show also the relationships among architectural decisions. With the following command, we will define a new ADR which amends the first one:

aac adr:new \
  --id ADR-002 \
  --version 1.0 \
  --name "Introduce analytics-service" \
  --state Decided \
  --decision "Add analytics-service to compute/report analytics using DataManager." \
  --problem "We need analytics over simulation data." \
  --alternatives "Offline batch pipeline" \
  --positive "Separation of concerns; evolvable analytics capability" \
  --negative "More components to build/deploy/operate" \
  --business-goals "insight" "decision-support" \
  --quality-attributes "modifiability" "extensibility" \
  --risks "operational overhead" "service coordination" \
  --rel ADR-001:amends

Just we create the new module in mobility_example/software/analytics_service.py and structure it as follows:

from __future__ import annotations
from .data_manager import DataManager

class AnalyticsService:
    def __init__(self) -> None:
        self.dm = DataManager()
    def run(self) -> dict | None:
        self.dm.ingest("demo/1", {"value": 42})
        return self.dm.query("demo/1")

# Optional: detected by AST, not executed during generation
from fastapi import FastAPI
fastapi_app = FastAPI(title="analytics-service")

@fastapi_app.get("/health")
def health():
    return {"status": "ok"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(fastapi_app, host="127.0.0.1", port=9001)

To interconnect it within the entire application, update mobility_example/software/app.py (optional):

# example/software/app.py
from .analytics_service import AnalyticsService

def run(steps: int = 3) -> None:
    aserv = AnalyticsService()
    print("analytics run:", aserv.run())
    # ... the rest of your existing run() logic ...

If we try to run the ecosystem (using the old version of the architecture description and the new ADRs), through the guarded run it will block execution and solve the drift:

aac guard:run --entry mobility_example/software/app.py --arch-name smart-mobility --version 1.0.0 --adr ADR-002

Expected outcome:

[BLOCKED] non-compliant:
 - Missing in architecture: node 'analytics-service' (found in code).
 - Missing relationship in architecture: container app composed-of ['analytics-service', 'data-manager', 'sharedstorage-db', 'simulator']
 - Missing relationship in architecture: analytics-service connects data-manager
 - Missing relationship in architecture: app connects analytics-service
[UPDATED] architecture → 1.0.1 (from 1.0.0) (ADR ADR-002)
[INFO] violations reduced: 4 → 0
[OK] compliant after update — running entrypoint
[STEP 0] ...

If we check the architecture extracted elements and relations (in the mobility_example/calm folder), we can see that a new architecture CALM based description has been extracted (the old one has been stored) and by checking the new architecture.json file we can see the new analytics_service node and its relationship with other nodes.

For example, to generate a new developer view, we can run the command:

aac gen:view --view developer

Expected output:

[OK] view 'developer' → path/to/example/views/developer/v<ARCH_VERSION>/<timestamp>.html

And we can upload the new architectural view description and visually check also the evolution within the diagram, alongside with architectural components and their interfaces list.

For the project-manager view, we can run the command:

aac gen:view --view project-manager

Expected output:

[OK] view 'project-manager' → path/to/example/views/project-manager/v<ARCH_VERSION>/<timestamp>.html

And we can visualize an overview of the structure of the current architecture description, and also a simple diagram showing ADRs as blocks and arrows as relations among descriptions.

Architecture-driven flow:

We change the architecture description to introduce a new component, then use the tool to scaffold and update ecosystem codebase. In this example, the goal is to add a new cache service and connect data-manager to it.

aac adr:new \
  --id ADR-003 \
  --version 1.0 \
  --name "Add Cache service for DataManager" \
  --state Decided \
  --decision "Introduce a cache service to reduce read latency in DataManager." \
  --problem "Frequent reads cause directly hitting storage; need faster responses." \
  --alternatives "In-memory LRU inside DataManager" "DB-level caching" \
  --positive "Lower latency; reduced load on storage" \
  --negative "Cache invalidation complexity; operational cost" \
  --business-goals "performance" "scalability" \
  --quality-attributes "performance" "throughput" \
  --risks "stale data" "invalidations" \
  --rel ADR-001:amends \
  --rel ADR-002:depends-on

Open mobility_example/calm/architecture.json and add a new node:

{
  "unique-id": "cache",
  "node-type": "service",
  "name": "cache",
  "description": "",
  "interfaces": [
    { "unique-id": "cache-http", "host": "localhost", "port": 9010 },
    { "unique-id": "default", "path": "/cache" }
  ]
}

And save the architecture.json file. Please note that when modifying the file, the structure required in the CALM format has to be respected, e.g., firstly adding the node, and then adding the relationship. A composite relationship should be put after a connects relationship.

Architecture description declares a cache node and a connection relationship, but there’s no cache.py module yet. The guard should detect this drift and block execution. To check, run command:

aac guard:run \
  --entry mobility_example/software/app.py \
  --arch-name smart-mobility \
  --version 1.1.0 \
  --on-arch-drift auto-sync \
  --adr ADR-003

Then we can think to add some behavioral details for the cache module and its interaction with the data manager. For example: The logic has to be manually added, for example:

# example/software/cache.py
class Cache:
    def __init__(self):
        self._kv = {}

    def get(self, key: str):
        return self._kv.get(key)

    def set(self, key: str, value):
        self._kv[key] = value
        return True

Decomment functions in data manager. And re-run the command. aac guard:run --entry mobility_example/software/app.py --arch-name smart-mobility --version 1.1.0 --adr ADR-003

Example of system user view:

aac gen:view --view system-user

Expected output:

[OK] view 'system-user' → path/to/example/views/system-user/v<ARCH_VERSION>/<timestamp>.html

Example of checking conformance to policies:

aac check:pattern --pattern ./tool/policies/service-connects-db.json

Expected output:

[OK] pattern satisfied
aac check:pattern --pattern ./tool/policies/pub-sub.json

Expected output:

rule #1 failed
[FAIL] pattern not satisfied

Available Commands:

  • adr:new: Create a new ADR. Generates a Markdown ADR file in mobility_example/decisions/. Metadata includes: id, version, name, state, decision, problem, alternatives, etc. Supports Kruchten’s decision states: Idea, Tentative, Decided, Approved, Challenged, Rejected.

  • arch:generate: Generate/update architecture with versioning. Extracts architecture from the codebase into mobility_example/calm/architecture.json. If an older architecture exists, it is archived into mobility_example/calm/versions/. You must specify an ADR (--adr) documenting why this architecture snapshot is valid. Use --version to control semantic version (e.g., 1.0.1), otherwise the patch version auto-bumps.

  • guard:run: Guarded execution (block on drift). Runs the example entrypoint (--entry) but first enforces compliance:

    • If architecture.json doesn’t exist, you must provide an ADR to bootstrap (--adr).
    • If drift is detected:
      • code→arch (code changed first): requires --adr, archives and regenerates CALM from code, re-checks, then runs.
      • arch→code (architecture changed first): by default blocks and asks you to run aac sync:from-arch --adr ADR-XXX. Or run with --on-arch-drift auto-sync to scaffold from architecture automatically during guard. Note: guard:run requires an ADR (but if not documented, simply a previous one can be used).
  • gen:view: Generate stakeholder view (versioned). Views available are loaded dynamically from tool/templates/views/viewsets.yaml. Defaults typically include developer, project-manager, system-user. If you add others (e.g., compliance) to the YAML, you can use them here. Please note that views are versioned per architecture (e.g., mobility_example/views/developer/v1.0.0/...). New or changed nodes are annotated with [NEW vX.Y.Z] and optionally tagged with ADR IDs.

  • check:pattern: Validate architecture against governance rules. Compares current architecture against a JSON pattern (e.g., “every service must connect to a database”). Fails with [FAIL] pattern not satisfied if rules are broken. Enables compliance checks (e.g., DB-per-service, security constraints).

  • adr:apply: Apply an ADR to evolve system. Takes an ADR (--id) and updates the system accordingly: --strategy from-code regenerates architecture from code (bottom-up evolution); --strategy from-arch: scaffold missing modules from CALM model (top-down evolution). After application, runs guard:run to monitor alignment.

  • sync:from-code: Force sync from code (ADR, even previous one, required). Immediately regenerates architecture from current code, bumping version and archiving previous snapshot. Use when you’ve made code changes that must now be reflected in CALM.

  • sync:from-arch: Force sync from architecture (ADR, even previous one, required). Scaffold modules for nodes declared in architecture.json but missing in mobility_example/software/. Useful for top-down evolution: you model the architecture first, then generate stubs.

  • calm: Proxy into FINOS CALM CLI. Passes raw arguments to the upstream CALM CLI if installed. Use this when you want to run CALM validation.

  • calm:ui: Launch CALM Hub (Visualizer). Clones the CALM Hub UI (if not already in third_party/) and runs it locally. Use --open to auto-launch in your browser. Great for interactive exploration of versioned architectures.

About

Python implementation of CONTAaC - Continuous Architecting as Code for Smart Ecosystem.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages