|
1 | 1 | from dataclasses import dataclass |
2 | 2 |
|
3 | | -from pacta.model.types import ArchitectureModel, Container |
| 3 | +from pacta.model.types import ArchitectureModel, Container, ContainerKind |
4 | 4 | from pacta.reporting.types import EngineError |
5 | 5 |
|
6 | 6 |
|
@@ -47,33 +47,85 @@ def validate(self, model: ArchitectureModel) -> list[EngineError]: |
47 | 47 | ) |
48 | 48 | ) |
49 | 49 |
|
50 | | - # containers |
51 | | - for cid, c in model.containers.items(): |
| 50 | + # containers (flat includes nested) |
| 51 | + flat = model.containers_flat |
| 52 | + for cid, c in flat.items(): |
52 | 53 | self._validate_container(model, cid, c, errors) |
53 | 54 |
|
54 | | - # relations reference known containers |
| 55 | + # v2: validate kind and child containment |
| 56 | + if model.version >= 2: |
| 57 | + self._validate_v2_containers(flat, errors) |
| 58 | + |
| 59 | + # v2: check for duplicate IDs in flat namespace |
| 60 | + # (already impossible if _collect_containers works correctly, but guard against |
| 61 | + # children whose local IDs collide when dot-qualified) |
| 62 | + |
| 63 | + # relations reference known containers (use flat namespace) |
| 64 | + all_ids = set(flat.keys()) |
55 | 65 | for r in model.relations: |
56 | | - if r.from_container not in model.containers: |
| 66 | + if r.from_container not in all_ids: |
57 | 67 | errors.append( |
58 | 68 | EngineError( |
59 | 69 | type="config_error", |
60 | | - message="Relation references unknown from_container.", |
| 70 | + message=f"Relation references unknown from_container '{r.from_container}'." |
| 71 | + f" Available: {sorted(all_ids)}.", |
61 | 72 | location=None, |
62 | | - details={"from_container": r.from_container}, |
| 73 | + details={"from_container": r.from_container, "available": sorted(all_ids)}, |
63 | 74 | ) |
64 | 75 | ) |
65 | | - if r.to_container not in model.containers: |
| 76 | + if r.to_container not in all_ids: |
66 | 77 | errors.append( |
67 | 78 | EngineError( |
68 | 79 | type="config_error", |
69 | | - message="Relation references unknown to_container.", |
| 80 | + message=f"Relation references unknown to_container '{r.to_container}'." |
| 81 | + f" Available: {sorted(all_ids)}.", |
70 | 82 | location=None, |
71 | | - details={"to_container": r.to_container}, |
| 83 | + details={"to_container": r.to_container, "available": sorted(all_ids)}, |
72 | 84 | ) |
73 | 85 | ) |
74 | 86 |
|
75 | 87 | return errors |
76 | 88 |
|
| 89 | + def _validate_v2_containers( |
| 90 | + self, |
| 91 | + flat: dict[str, Container] | object, |
| 92 | + errors: list[EngineError], |
| 93 | + ) -> None: |
| 94 | + for cid, c in flat.items(): # type: ignore[union-attr] |
| 95 | + # kind must be valid |
| 96 | + if c.kind is not None and c.kind not in ContainerKind: |
| 97 | + errors.append( |
| 98 | + EngineError( |
| 99 | + type="config_error", |
| 100 | + message=f"Container '{cid}' has invalid kind '{c.kind}'." |
| 101 | + f" Must be one of: {', '.join(k.value for k in ContainerKind)}.", |
| 102 | + location=None, |
| 103 | + details={"container_id": cid, "kind": str(c.kind)}, |
| 104 | + ) |
| 105 | + ) |
| 106 | + |
| 107 | + # child code.roots must be sub-paths of parent code.roots |
| 108 | + if c.parent is not None and c.code is not None: |
| 109 | + parent = flat.get(c.parent) # type: ignore[union-attr] |
| 110 | + if parent is not None and parent.code is not None: |
| 111 | + parent_roots = parent.code.roots |
| 112 | + for root in c.code.roots: |
| 113 | + if not any(root.startswith(pr) for pr in parent_roots): |
| 114 | + errors.append( |
| 115 | + EngineError( |
| 116 | + type="config_error", |
| 117 | + message=f"Container '{cid}' code root '{root}' is not" |
| 118 | + f" under parent '{c.parent}' roots {list(parent_roots)}.", |
| 119 | + location=None, |
| 120 | + details={ |
| 121 | + "container_id": cid, |
| 122 | + "root": root, |
| 123 | + "parent_id": c.parent, |
| 124 | + "parent_roots": list(parent_roots), |
| 125 | + }, |
| 126 | + ) |
| 127 | + ) |
| 128 | + |
77 | 129 | def _validate_container( |
78 | 130 | self, |
79 | 131 | model: ArchitectureModel, |
|
0 commit comments