|
34 | 34 | | `message` | string | No | Message shown on violation (auto-generated if omitted) | |
35 | 35 | | `suggestion` | string | No | Remediation guidance | |
36 | 36 |
|
| 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 | + |
37 | 71 | ## Conditions |
38 | 72 |
|
39 | 73 | ### Layer Conditions |
|
59 | 93 | - to.layer == application |
60 | 94 | ``` |
61 | 95 |
|
62 | | -## Example: Clean Architecture Rules |
| 96 | +## Example: Clean Architecture Rules (v1) |
63 | 97 |
|
64 | 98 | ```yaml |
65 | 99 | # Domain cannot depend on Infrastructure |
@@ -121,3 +155,68 @@ rule: |
121 | 155 | message: UI layer should not directly depend on Infrastructure layer |
122 | 156 | suggestion: Access infrastructure through application services instead |
123 | 157 | ``` |
| 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. |
0 commit comments