Skip to content

Commit 04bb0c3

Browse files
authored
Merge pull request #36 from pacta-dev/feature/fractal-architecture-modeling
Support Fractal Architecture Modeling
2 parents 3285b9f + 03a92a7 commit 04bb0c3

42 files changed

Lines changed: 2106 additions & 479 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/architecture/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Architecture Model
2+
3+
The architecture model defines your system structure in YAML.
4+
5+
Pacta supports two schema versions:
6+
7+
- **v1** — Flat containers with layers. Simple and sufficient for single-service architectures.
8+
- **v2** — Nested containers with `kind` and `contains`. Designed for microservices and modular monoliths.
9+
10+
The loader auto-detects the version from the `version:` key. If absent, v1 is assumed.
Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
# Architecture Model
1+
# v1 Schema
22

3-
The architecture model defines your system structure in YAML.
4-
5-
## File Format
3+
### File Format
64

75
Create `architecture.yml` in your repository root:
86

@@ -41,16 +39,16 @@ containers:
4139
contexts: {}
4240
```
4341
44-
## Schema
42+
### Schema Reference (v1)
4543
46-
### system
44+
#### system
4745
4846
| Field | Type | Required | Description |
4947
|-------|------|----------|-------------|
5048
| `id` | string | Yes | Unique system identifier |
5149
| `name` | string | Yes | Human-readable name |
5250

53-
### containers
51+
#### containers
5452

5553
Map of container definitions.
5654

@@ -60,15 +58,16 @@ Map of container definitions.
6058
| `description` | string | No | Container description |
6159
| `context` | string | No | Bounded context reference |
6260
| `code` | object | No | Code mapping configuration |
61+
| `tags` | list | No | Tags inherited by nodes in this container |
6362

64-
### code
63+
#### code
6564

6665
| Field | Type | Required | Description |
6766
|-------|------|----------|-------------|
6867
| `roots` | list | Yes | Source root directories |
6968
| `layers` | map | No | Layer definitions |
7069

71-
### layers
70+
#### layers
7271

7372
Map of layer definitions.
7473

@@ -78,7 +77,17 @@ Map of layer definitions.
7877
| `description` | string | No | Layer description |
7978
| `patterns` | list | Yes | Glob patterns matching layer files |
8079

81-
## Example: Clean Architecture
80+
#### relations
81+
82+
```yaml
83+
relations:
84+
- from: container-a
85+
to: container-b
86+
protocol: http
87+
description: A calls B
88+
```
89+
90+
### Example: Clean Architecture (v1)
8291

8392
```yaml
8493
version: 1

docs/architecture/v2.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# v2 Schema
2+
3+
v2 adds **nested containers**, explicit **container kinds**, and the `interactions:` alias for relations. v1 files remain fully valid — no migration is required.
4+
5+
### What's New in v2
6+
7+
| Feature | Description |
8+
|---------|-------------|
9+
| `kind` | Required on every container: `service`, `module`, or `library` |
10+
| `contains` | Nest child containers inside a parent |
11+
| `interactions` | Alias for `relations` (both accepted) |
12+
| Dot-qualified IDs | Nested containers get IDs like `billing-service.invoice-module` |
13+
| Context inheritance | Children inherit parent's `context` unless they override it |
14+
15+
### File Format (v2)
16+
17+
```yaml
18+
version: 2
19+
20+
system:
21+
id: my-platform
22+
name: My Platform
23+
24+
contexts:
25+
billing:
26+
name: Billing Context
27+
28+
containers:
29+
billing-service:
30+
kind: service
31+
name: Billing Service
32+
context: billing
33+
code:
34+
roots: [services/billing]
35+
layers:
36+
api: [services/billing/api/**]
37+
domain: [services/billing/domain/**]
38+
contains:
39+
invoice-module:
40+
kind: module
41+
name: Invoice Module
42+
code:
43+
roots: [services/billing/domain/invoice]
44+
layers:
45+
model: [services/billing/domain/invoice/model/**]
46+
repo: [services/billing/domain/invoice/repo/**]
47+
48+
shared-utils:
49+
kind: library
50+
name: Shared Utilities
51+
code:
52+
roots: [libs/shared]
53+
54+
interactions:
55+
- from: billing-service
56+
to: shared-utils
57+
protocol: import
58+
```
59+
60+
### Schema Reference (v2)
61+
62+
v2 containers extend the v1 schema with these additional fields:
63+
64+
| Field | Type | Required | Description |
65+
|-------|------|----------|-------------|
66+
| `kind` | string | **Yes** | `service`, `module`, or `library` |
67+
| `contains` | map | No | Nested child containers (same schema, recursive) |
68+
69+
All v1 container fields (`name`, `description`, `context`, `code`, `tags`) remain unchanged.
70+
71+
### Container Kinds
72+
73+
| Kind | Meaning |
74+
|------|---------|
75+
| `service` | Deployable service or application |
76+
| `module` | Logical module within a service |
77+
| `library` | Shared library used by other containers |
78+
79+
### Enriched Fields: `kind` vs `within`
80+
81+
When code is analyzed, each node and edge is enriched with container metadata. For nested containers, there are two important fields:
82+
83+
| Field | Meaning | Example Value |
84+
|-------|---------|---------------|
85+
| `kind` | Immediate container's kind | `module` for code in `billing-service.invoice-module` |
86+
| `within` | Top-level container's kind | `service` for any code inside `billing-service` |
87+
88+
**Example:** For code at `services/billing/domain/invoice/model/invoice.py`:
89+
90+
```
91+
billing-service (kind: service)
92+
└── invoice-module (kind: module)
93+
└── model/invoice.py ← this file
94+
```
95+
96+
The enriched fields would be:
97+
98+
| Field | Value |
99+
|-------|-------|
100+
| `container` | `billing-service.invoice-module` |
101+
| `service` | `billing-service` |
102+
| `kind` | `module` |
103+
| `within` | `service` |
104+
105+
**When to use each in rules:**
106+
107+
- Use `kind` to match the specific container type (e.g., target only code directly in modules)
108+
- Use `within` to match anything inside a service hierarchy (e.g., catch all service code, including nested modules)
109+
110+
See [Rules DSL: kind vs within](../rules.md#kind-vs-within) for detailed examples.
111+
112+
### Dot-Qualified IDs
113+
114+
Nested containers are addressed using dot-qualified IDs. For the example above:
115+
116+
- `billing-service` — the top-level service
117+
- `billing-service.invoice-module` — the nested module
118+
119+
These IDs are used in `interactions`, rules, and enrichment output.
120+
121+
### Context Inheritance
122+
123+
Children inherit their parent's `context:` unless they explicitly set their own:
124+
125+
```yaml
126+
containers:
127+
billing-service:
128+
kind: service
129+
context: billing # set here
130+
contains:
131+
invoice-module:
132+
kind: module # inherits context: billing
133+
payments-module:
134+
kind: module
135+
context: payments # overrides with own context
136+
```
137+
138+
### Version Detection
139+
140+
| `version:` value | Behavior |
141+
|------------------|----------|
142+
| absent | v1 (default) |
143+
| `1` | v1 |
144+
| `2` | v2 — `kind` required, `contains` and `interactions` supported |
145+
| anything else | Error |
146+
147+
### v1 → v2 Key Mapping
148+
149+
| v1 key | v2 key | Notes |
150+
|--------|--------|-------|
151+
| `context:` | `context:` | Unchanged |
152+
| `relations:` | `interactions:` | Both accepted in v2; `relations:` takes precedence if both present |
153+
| _(n/a)_ | `contains:` | New in v2 |
154+
| _(n/a)_ | `kind:` | Required in v2 |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--8<-- "examples/microservices-platform/README.md"

docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,6 @@ myproject/
229229
**Next steps:**
230230

231231
- [CLI Reference](cli.md) — Commands and options
232-
- [Architecture Model](architecture.md) — Configuration schema
232+
- [Architecture Model](architecture/index.md) — Configuration schema
233233
- [Rules DSL](rules.md) — Rule conditions
234234
- [CI Integration](ci-integration.md) — Automate in your pipeline

docs/rules.md

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ rule:
3434
| `message` | string | No | Message shown on violation (auto-generated if omitted) |
3535
| `suggestion` | string | No | Remediation guidance |
3636

37+
## Available Fields
38+
39+
### Node Fields (`target: node`)
40+
41+
| Field | Description |
42+
|-------|-------------|
43+
| `node.symbol_kind` | Symbol type: `file`, `module`, `class`, `function`, etc. |
44+
| `node.kind` | Immediate container kind: `service`, `module`, `library` (v2 only) |
45+
| `node.within` | Top-level container's kind (v2 only) — see [kind vs within](#kind-vs-within) |
46+
| `node.service` | Top-level container ancestor ID (v2 only) |
47+
| `node.path` | File path |
48+
| `node.name` | Symbol name |
49+
| `node.layer` | Architectural layer |
50+
| `node.context` | Bounded context |
51+
| `node.container` | Container ID (dot-qualified for nested containers in v2) |
52+
| `node.tags` | Tags inherited from container |
53+
| `node.fqname` | Fully qualified name |
54+
| `node.language` | Source language |
55+
56+
### Dependency Fields (`target: dependency`)
57+
58+
| Field | Description |
59+
|-------|-------------|
60+
| `from.layer` / `to.layer` | Source/target layer |
61+
| `from.context` / `to.context` | Source/target bounded context |
62+
| `from.container` / `to.container` | Source/target container ID |
63+
| `from.service` / `to.service` | Source/target top-level service ID (v2 only) |
64+
| `from.kind` / `to.kind` | Source/target immediate container kind (v2 only) |
65+
| `from.within` / `to.within` | Source/target top-level container's kind (v2 only) — see [kind vs within](#kind-vs-within) |
66+
| `from.fqname` / `to.fqname` | Fully qualified names |
67+
| `from.id` / `to.id` | Full canonical ID strings |
68+
| `dep.type` | Dependency type (`import`, `call`, etc.) |
69+
| `loc.file` | Source location file |
70+
3771
## Conditions
3872

3973
### Layer Conditions
@@ -59,7 +93,7 @@ when:
5993
- to.layer == application
6094
```
6195

62-
## Example: Clean Architecture Rules
96+
## Example: Clean Architecture Rules (v1)
6397

6498
```yaml
6599
# Domain cannot depend on Infrastructure
@@ -121,3 +155,68 @@ rule:
121155
message: UI layer should not directly depend on Infrastructure layer
122156
suggestion: Access infrastructure through application services instead
123157
```
158+
159+
## kind vs within
160+
161+
In v2 schemas with nested containers, there's an important distinction:
162+
163+
| Field | Meaning | Example |
164+
|-------|---------|---------|
165+
| `kind` | Immediate container's kind | `module` for code in `billing-service.invoice-module` |
166+
| `within` | Top-level container's kind | `service` for any code inside `billing-service` |
167+
168+
**When to use each:**
169+
170+
- Use **`kind`** when you want to match the specific container type (e.g., "only code directly in a module")
171+
- Use **`within`** when you want to match anything inside a service hierarchy (e.g., "any code belonging to a service, including nested modules")
172+
173+
**Example:** Code at `services/billing/domain/invoice/model/invoice.py` inside `billing-service.invoice-module`:
174+
175+
```
176+
billing-service (kind: service)
177+
└── invoice-module (kind: module)
178+
└── invoice.py ← this file
179+
```
180+
181+
| Field | Value |
182+
|-------|-------|
183+
| `kind` | `module` (immediate container) |
184+
| `within` | `service` (top-level container) |
185+
186+
## Example: Cross-Service Rules (v2)
187+
188+
These rules use v2-only fields (`from.service`, `to.service`, `from.kind`, `to.kind`, `from.within`, `to.within`):
189+
190+
```yaml
191+
# Forbid cross-service domain dependencies
192+
rule:
193+
id: no-cross-service-domain-deps
194+
name: Domain must not depend on other services
195+
severity: error
196+
target: dependency
197+
action: forbid
198+
when:
199+
all:
200+
- from.service != to.service
201+
- from.layer == domain
202+
message: Domain code must not depend on another service
203+
204+
# Libraries must not depend on services (using 'within' for nested containers)
205+
rule:
206+
id: library-no-service-deps
207+
name: Libraries must be independent of services
208+
severity: error
209+
target: dependency
210+
action: forbid
211+
when:
212+
all:
213+
- from.within == library
214+
- to.within == service
215+
message: Library code must not import service code
216+
suggestion: Move shared code to a library or create a proper API contract
217+
```
218+
219+
!!! note "Why use `within` instead of `kind`?"
220+
Using `to.kind == service` would NOT match code inside nested modules like `billing-service.invoice-module`, because the immediate container's kind is `module`, not `service`.
221+
222+
Using `to.within == service` matches ANY code inside a service hierarchy, including nested modules.

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Pacta includes several example projects demonstrating different architectural pa
99
| [Simple Layered App](simple-layered-app.md) | Classic N-tier architecture | Teams familiar with layered architecture |
1010
| [Hexagonal Architecture](hexagonal-app.md) | Ports and Adapters pattern | Domain-driven design, high testability |
1111
| [Legacy Migration](legacy-migration.md) | Baseline workflow for brownfield | Existing codebases, incremental adoption |
12+
| [Microservices Platform](microservices-platform.md) | v2 schema with nested containers | Multi-service systems, modular monoliths |
1213

1314
## Quick Start
1415

0 commit comments

Comments
 (0)