Skip to content

Commit 8a9fbce

Browse files
authored
Merge pull request #13 from akhundMurad/feature/pacta-check
feat(cli): add check command to evaluate rules against saved snapshots
2 parents a5908d8 + 9fd5738 commit 8a9fbce

13 files changed

Lines changed: 595 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,3 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7-
8-
## [Unreleased]
9-
10-
## [0.0.2] - 2025-01-20
11-
12-
### Added
13-
14-
- History tracking with content-addressed snapshot store
15-
- `pacta history show` command to view architecture timeline
16-
- `pacta history export` command for JSON export of history data
17-
- `pacta history trends` command with ASCII charts for visualizing metrics over time
18-
- Optional matplotlib support for PNG/SVG chart export (`pip install pacta[viz]`)
19-
- Human-readable violation explanations in text output
20-
- Expanded contributing guide with project structure and development workflow
21-
22-
### Changed
23-
24-
- License changed to Apache-2.0
25-
- README repositioned to focus on architecture governance
26-
27-
## [0.0.1] - 2025-01-15
28-
29-
### Added
30-
31-
- Initial release
32-
- Python AST analyzer for static code analysis
33-
- Architecture model definition via `architecture.yml`
34-
- System and container definitions
35-
- Layer definitions with glob patterns
36-
- Code mapping configuration
37-
- Rules DSL via `rules.pacta.yml`
38-
- Layer dependency constraints
39-
- Severity levels (error, warning, info)
40-
- Custom messages and suggestions
41-
- `pacta scan` command for architecture validation
42-
- Snapshot support for versioning architecture state
43-
- Baseline mode for incremental adoption (fail only on new violations)
44-
- `pacta snapshot save` and `pacta diff` commands
45-
- Text and JSON output formats
46-
- MkDocs documentation site
47-
48-
[Unreleased]: https://github.com/akhundMurad/pacta/compare/v0.0.2...HEAD
49-
[0.0.2]: https://github.com/akhundMurad/pacta/compare/v0.0.1...v0.0.2
50-
[0.0.1]: https://github.com/akhundMurad/pacta/releases/tag/v0.0.1

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ Codebases rot. Architecture degrades through small changes no one tracks. Pacta
5656

5757
| Git | Pacta |
5858
|-----|-------|
59-
| `git commit` | `pacta scan` — capture an architectural snapshot |
59+
| `git add` | `pacta snapshot save` — capture an architectural snapshot |
60+
| `git commit --verify` | `pacta check` — evaluate rules against a snapshot |
61+
| `git commit` | `pacta scan` — snapshot + check in one step |
6062
| `git log` | `pacta history` — timeline and trends of architectural states |
6163
| `git diff` | `pacta diff` — compare two snapshots |
6264
| branch protection | `rules.pacta.yml` — governance that prevents drift |

docs/ci-integration.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,23 @@ repos:
221221
Add Pacta to your Makefile for easy local runs:
222222

223223
```makefile
224-
.PHONY: arch arch-baseline
224+
.PHONY: arch arch-snapshot arch-check arch-baseline arch-ci
225225
226-
# Run architecture check
226+
# Full scan (snapshot + check in one step)
227227
arch:
228228
pacta scan . --model architecture.yml --rules rules.pacta.yml
229229
230+
# Two-step workflow
231+
arch-snapshot:
232+
pacta snapshot save . --model architecture.yml
233+
234+
arch-check:
235+
pacta check . --rules rules.pacta.yml
236+
230237
# Update baseline
231238
arch-baseline:
232-
pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref baseline
239+
pacta snapshot save . --model architecture.yml --ref baseline
240+
pacta check . --ref baseline --rules rules.pacta.yml
233241
234242
# Check against baseline (CI mode)
235243
arch-ci:

docs/cli.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,64 @@ Violations are displayed with human-readable explanations:
6161
- **For dependency violations:** Shows which module imports/calls/uses another, with their respective layers
6262
- **For node violations:** Shows the element type and where it was found (layer, container, context)
6363

64+
## check
65+
66+
Evaluate architectural rules against an existing snapshot and write violations back into it.
67+
68+
This separates the "capture" step (`snapshot save`) from the "verify" step (`check`), allowing you to snapshot your architecture once and check it against different rule sets or at different times. The existing snapshot object is updated in-place — no new snapshot is created.
69+
70+
```bash
71+
pacta check [PATH] [OPTIONS]
72+
```
73+
74+
**Arguments:**
75+
76+
| Argument | Default | Description |
77+
|----------|---------|-------------|
78+
| `PATH` | `.` | Repository root |
79+
80+
**Options:**
81+
82+
| Option | Default | Description |
83+
|--------|---------|-------------|
84+
| `--ref REF` | `latest` | Snapshot ref to check |
85+
| `--model FILE` | `architecture.yml` | Architecture model file |
86+
| `--rules FILE` | `rules.pacta.yml` | Rules file (repeatable) |
87+
| `--format {text,json}` | `text` | Output format |
88+
| `--baseline REF` | - | Compare against baseline snapshot |
89+
| `--save-ref REF` | - | Also save result under this ref |
90+
| `-q, --quiet` | - | Summary only |
91+
| `-v, --verbose` | - | Include all details |
92+
93+
**Examples:**
94+
95+
```bash
96+
# Check latest snapshot against rules
97+
pacta check . --rules rules.pacta.yml
98+
99+
# Check a specific snapshot ref
100+
pacta check . --ref v1 --rules rules.pacta.yml
101+
102+
# Check against baseline (only new violations fail)
103+
pacta check . --baseline baseline --rules rules.pacta.yml
104+
105+
# JSON output
106+
pacta check . --rules rules.pacta.yml --format json
107+
```
108+
109+
**Typical workflow:**
110+
111+
```bash
112+
# Step 1: Capture architecture
113+
pacta snapshot save . --model architecture.yml
114+
115+
# Step 2: Evaluate rules against the snapshot
116+
pacta check . --rules rules.pacta.yml
117+
118+
# Or do both in one step:
119+
pacta scan . --model architecture.yml --rules rules.pacta.yml
120+
```
121+
64122
## snapshot save
65123

66124
Save architecture snapshot without running rules.

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ References are named pointers to snapshot hashes:
295295
296296
| Ref | Description |
297297
|-----|-------------|
298-
| `latest` | Automatically updated on each scan |
298+
| `latest` | Automatically updated on each scan/check |
299299
| `baseline` | Created with `--save-ref baseline` |
300300
| Custom | Any name you choose with `--save-ref <name>` |
301301

docs/getting-started.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ containers:
3838
Now capture a snapshot:
3939
4040
```bash
41-
pacta snapshot . --model architecture.yml
41+
pacta snapshot save . --model architecture.yml
4242
```
4343

4444
Pacta just analyzed every module and dependency in your codebase and stored a content-addressed snapshot in `.pacta/`. This snapshot is immutable — a permanent record of your architecture at this moment.
@@ -50,7 +50,7 @@ Pacta just analyzed every module and dependency in your codebase and stored a co
5050
A week passes. Your team ships features, fixes bugs, refactors code. Run another snapshot:
5151

5252
```bash
53-
pacta snapshot . --model architecture.yml
53+
pacta snapshot save . --model architecture.yml
5454
```
5555

5656
Now you have two points in time. See what changed:
@@ -122,9 +122,13 @@ rule:
122122
message: Domain layer must not import from Infrastructure
123123
```
124124
125-
Run a scan with rules:
125+
Run a check against your snapshot:
126126
127127
```bash
128+
# Option A: Check the snapshot you already have
129+
pacta check . --rules rules.pacta.yml
130+
131+
# Option B: Or do snapshot + check in one step
128132
pacta scan . --model architecture.yml --rules rules.pacta.yml
129133
```
130134

@@ -145,12 +149,19 @@ Two violations. But wait — this is a legacy codebase. You can't fix everything
145149
Save the current state as a baseline:
146150

147151
```bash
152+
# One-step:
148153
pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref baseline
154+
155+
# Or two-step:
156+
pacta snapshot save . --model architecture.yml --ref baseline
157+
pacta check . --ref baseline --rules rules.pacta.yml
149158
```
150159

151-
Now future scans compare against this baseline:
160+
Now future checks compare against this baseline:
152161

153162
```bash
163+
pacta check . --rules rules.pacta.yml --baseline baseline
164+
# Or equivalently:
154165
pacta scan . --model architecture.yml --rules rules.pacta.yml --baseline baseline
155166
```
156167

docs/troubleshooting.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ Error: Baseline 'baseline' not found
115115
1. Create a baseline first:
116116
```bash
117117
pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref baseline
118+
# Or using the two-step workflow:
119+
pacta snapshot save . --model architecture.yml --ref baseline
120+
pacta check . --ref baseline --rules rules.pacta.yml
118121
```
119122

120123
2. Check that `.pacta/` directory exists and contains snapshots:

examples/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ To run any example:
2222

2323
```bash
2424
cd examples/<example-name>
25+
26+
# One-step (scan = snapshot + check):
2527
pacta scan . --model architecture.yml --rules rules.pacta.yml
28+
29+
# Or two-step:
30+
pacta snapshot save . --model architecture.yml
31+
pacta check . --rules rules.pacta.yml
2632
```
2733

2834
## Creating Your Own

pacta/cli/check.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from pathlib import Path
2+
3+
from pacta.cli._io import default_model_file, default_rules_files, ensure_repo_root
4+
from pacta.cli.exitcodes import exit_code_from_report_dict
5+
from pacta.core.config import EngineConfig
6+
from pacta.core.engine import DefaultPactaEngine
7+
from pacta.reporting.renderers.json import JsonReportRenderer
8+
from pacta.reporting.renderers.text import TextReportRenderer
9+
from pacta.snapshot.store import FsSnapshotStore
10+
11+
12+
def run(
13+
*,
14+
path: str,
15+
ref: str,
16+
fmt: str,
17+
rules: tuple[str, ...] | None,
18+
model: str | None,
19+
baseline: str | None,
20+
save_ref: str | None,
21+
verbosity: str = "normal",
22+
tool_version: str | None,
23+
) -> int:
24+
"""
25+
Evaluate rules against an existing snapshot.
26+
27+
Loads the snapshot identified by --ref, evaluates architecture rules,
28+
and saves the updated snapshot (with violations) back to the store.
29+
"""
30+
repo_root = ensure_repo_root(path)
31+
32+
rules_files = rules if rules is not None else default_rules_files(repo_root)
33+
model_file = model if model is not None else default_model_file(repo_root)
34+
35+
# Load existing snapshot
36+
store = FsSnapshotStore(repo_root=repo_root)
37+
if not store.exists(ref):
38+
import sys
39+
40+
print(f"pacta: error: snapshot ref '{ref}' not found. Run `pacta snapshot save` first.", file=sys.stderr)
41+
from pacta.cli.exitcodes import EXIT_ENGINE_ERROR
42+
43+
return EXIT_ENGINE_ERROR
44+
45+
snapshot = store.load(ref)
46+
47+
# Build config
48+
cfg = EngineConfig(
49+
repo_root=Path(repo_root),
50+
model_file=Path(model_file) if model_file else None,
51+
rules_files=tuple(Path(f) for f in rules_files),
52+
baseline=baseline,
53+
changed_only=False,
54+
save_ref=save_ref,
55+
)
56+
57+
# Run check
58+
engine = DefaultPactaEngine()
59+
result = engine.check(cfg, snapshot)
60+
61+
# Update existing snapshot object in-place with violations
62+
short_hash = store.resolve_ref(ref)
63+
if short_hash is None:
64+
# ref was a direct hash
65+
short_hash = ref
66+
store.update_object(short_hash, result.snapshot)
67+
68+
# Optionally save under an additional ref
69+
if save_ref and save_ref != ref:
70+
store.save(result.snapshot, refs=[save_ref])
71+
72+
# Render report
73+
if fmt == "json":
74+
out = JsonReportRenderer().render(result.report)
75+
else:
76+
out = TextReportRenderer(verbosity=verbosity).render(result.report) # type: ignore[arg-type]
77+
print(out, end="")
78+
79+
return exit_code_from_report_dict(result.report.to_dict())

pacta/cli/main.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import argparse
22
import sys
33

4-
from pacta.cli import diff, history, scan, snapshot
4+
from pacta.cli import check, diff, history, scan, snapshot
55
from pacta.cli.exitcodes import EXIT_ENGINE_ERROR
66

77

@@ -26,6 +26,19 @@ def build_parser() -> argparse.ArgumentParser:
2626
verbosity.add_argument("-q", "--quiet", action="store_true", help="Minimal output (summary only).")
2727
verbosity.add_argument("-v", "--verbose", action="store_true", help="Verbose output (include all details).")
2828

29+
# check
30+
check_p = sub.add_parser("check", help="Evaluate rules against a snapshot.")
31+
check_p.add_argument("path", nargs="?", default=".", help="Repository root (default: .)")
32+
check_p.add_argument("--ref", default="latest", help="Snapshot ref to check (default: latest).")
33+
check_p.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
34+
check_p.add_argument("--rules", action="append", default=None, help="Rules file path (repeatable).")
35+
check_p.add_argument("--model", default=None, help="Architecture model file (architecture.yaml).")
36+
check_p.add_argument("--baseline", default=None, help="Baseline snapshot ref.")
37+
check_p.add_argument("--save-ref", dest="save_ref", default=None, help="Also save snapshot under this ref.")
38+
check_verbosity = check_p.add_mutually_exclusive_group()
39+
check_verbosity.add_argument("-q", "--quiet", action="store_true", help="Minimal output (summary only).")
40+
check_verbosity.add_argument("-v", "--verbose", action="store_true", help="Verbose output (include all details).")
41+
2942
# snapshot
3043
snap = sub.add_parser("snapshot", help="Snapshot operations.")
3144
snap_sub = snap.add_subparsers(dest="snapshot_cmd", required=True)
@@ -96,6 +109,21 @@ def main(argv: list[str] | None = None) -> int:
96109
tool_version=args.tool_version,
97110
)
98111

112+
if args.cmd == "check":
113+
rules = tuple(args.rules) if args.rules is not None else None
114+
verbosity = "quiet" if args.quiet else ("verbose" if args.verbose else "normal")
115+
return check.run(
116+
path=args.path,
117+
ref=args.ref,
118+
fmt=args.format,
119+
rules=rules,
120+
model=args.model,
121+
baseline=args.baseline,
122+
save_ref=args.save_ref,
123+
verbosity=verbosity,
124+
tool_version=args.tool_version,
125+
)
126+
99127
if args.cmd == "snapshot":
100128
if args.snapshot_cmd == "save":
101129
return snapshot.save(

0 commit comments

Comments
 (0)