Hexagonal Architecture Stub Generator
Implements: ADR-0011, ADR-0013 Source:
tools/generate_stubs.py
Purpose
generate_stubs.py bridges the gap between a Gherkin feature file and
working Python code. Given a directory of .feature files, it scaffolds
the four-layer Hexagonal Architecture skeleton so developers start with the
correct structure rather than discovering the pattern through trial and error.
Spec-First Workflow
The generator enforces a spec-first discipline:
- Write a
.featurefile that describes desired behaviour in Gherkin. - Run
generate_stubs.py <features_dir> <src_dir>. - Implement the generated stubs.
- Run
pytest --bddto verify the feature scenarios pass.
This order guarantees that every bounded context has both a human-readable behaviour specification and an implementation scaffold from its first commit.
Generated Structure
For each *.feature file with stem <name>, the following files are created:
<src_dir>/
domain/
__init__.py
<name>_service.py ← pure business logic
ports/
__init__.py
<name>_port.py ← typing.Protocol interface
adapters/
inbound/
__init__.py
mcp_<name>.py ← FastMCP tool / FastAPI route
outbound/
__init__.py
store_<name>.py ← concrete persistence adapter
Layer Responsibilities
Domain (domain/<name>_service.py)
Contains pure Python business logic with no I/O, no framework imports.
The generated stub raises NotImplementedError, which forces the developer
to implement real logic before any test can pass — the error cannot be silently
swallowed.
def process_<name>():
"""
Generated from: <name>.feature
"""
raise NotImplementedError
Ports (ports/<name>_port.py)
A typing.Protocol class that defines the contract (interface) between the
domain and its adapters.
Why Protocol over ABC?
Protocol enables structural sub-typing (PEP 544): any class that implements
the required methods satisfies the contract without inheriting from the base
class. This decouples adapters from the domain completely — the domain never
imports from adapters/, so the dependency graph stays clean.
class I<Name>Port(Protocol):
def execute(self) -> None: ...
Inbound Adapter (adapters/inbound/mcp_<name>.py)
Translates external triggers (HTTP requests, MCP tool calls, CLI arguments)
into domain calls. The mcp_ prefix signals that the primary integration
surface is FastMCP, but a FastAPI route or CLI handler is equally valid here.
Outbound Adapter (adapters/outbound/store_<name>.py)
Implements the port Protocol and translates domain calls into external
side-effects (database writes, HTTP calls to third-party APIs, file I/O).
The store_ prefix is a convention, not a constraint.
Idempotency
Every file write is guarded by an existence check:
if not domain_file.exists():
domain_file.write_text(...)
Re-running the generator on a partially implemented bounded context is safe — existing files (which may already contain real logic) are never overwritten.
Interface Name Derivation
The Protocol class name is derived from feature_name using title-case
transformation with I prefix and Port suffix:
| Feature stem | Interface name |
|---|---|
user_auth |
IUserAuthPort |
invoice_export |
IInvoiceExportPort |
product |
IProductPort |
The same derivation logic runs in both the port stub and the outbound adapter comment, so the two files stay consistent.
Exit Codes
| Code | Meaning |
|---|---|
0 |
Stubs generated successfully |
1 |
features_dir does not exist |
2 |
Insufficient CLI arguments |
Exit codes allow CI pipeline steps to treat "no features directory" as a hard failure without parsing stdout.