Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 0 additions & 44 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,3 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.0.2] - 2025-01-20

### Added

- History tracking with content-addressed snapshot store
- `pacta history show` command to view architecture timeline
- `pacta history export` command for JSON export of history data
- `pacta history trends` command with ASCII charts for visualizing metrics over time
- Optional matplotlib support for PNG/SVG chart export (`pip install pacta[viz]`)
- Human-readable violation explanations in text output
- Expanded contributing guide with project structure and development workflow

### Changed

- License changed to Apache-2.0
- README repositioned to focus on architecture governance

## [0.0.1] - 2025-01-15

### Added

- Initial release
- Python AST analyzer for static code analysis
- Architecture model definition via `architecture.yml`
- System and container definitions
- Layer definitions with glob patterns
- Code mapping configuration
- Rules DSL via `rules.pacta.yml`
- Layer dependency constraints
- Severity levels (error, warning, info)
- Custom messages and suggestions
- `pacta scan` command for architecture validation
- Snapshot support for versioning architecture state
- Baseline mode for incremental adoption (fail only on new violations)
- `pacta snapshot save` and `pacta diff` commands
- Text and JSON output formats
- MkDocs documentation site

[Unreleased]: https://github.com/akhundMurad/pacta/compare/v0.0.2...HEAD
[0.0.2]: https://github.com/akhundMurad/pacta/compare/v0.0.1...v0.0.2
[0.0.1]: https://github.com/akhundMurad/pacta/releases/tag/v0.0.1
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ Codebases rot. Architecture degrades through small changes no one tracks. Pacta

| Git | Pacta |
|-----|-------|
| `git commit` | `pacta scan` — capture an architectural snapshot |
| `git add` | `pacta snapshot save` — capture an architectural snapshot |
| `git commit --verify` | `pacta check` — evaluate rules against a snapshot |
| `git commit` | `pacta scan` — snapshot + check in one step |
| `git log` | `pacta history` — timeline and trends of architectural states |
| `git diff` | `pacta diff` — compare two snapshots |
| branch protection | `rules.pacta.yml` — governance that prevents drift |
Expand Down
14 changes: 11 additions & 3 deletions docs/ci-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,23 @@ repos:
Add Pacta to your Makefile for easy local runs:

```makefile
.PHONY: arch arch-baseline
.PHONY: arch arch-snapshot arch-check arch-baseline arch-ci

# Run architecture check
# Full scan (snapshot + check in one step)
arch:
pacta scan . --model architecture.yml --rules rules.pacta.yml

# Two-step workflow
arch-snapshot:
pacta snapshot save . --model architecture.yml

arch-check:
pacta check . --rules rules.pacta.yml

# Update baseline
arch-baseline:
pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref baseline
pacta snapshot save . --model architecture.yml --ref baseline
pacta check . --ref baseline --rules rules.pacta.yml

# Check against baseline (CI mode)
arch-ci:
Expand Down
58 changes: 58 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,64 @@ Violations are displayed with human-readable explanations:
- **For dependency violations:** Shows which module imports/calls/uses another, with their respective layers
- **For node violations:** Shows the element type and where it was found (layer, container, context)

## check

Evaluate architectural rules against an existing snapshot and write violations back into it.

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.

```bash
pacta check [PATH] [OPTIONS]
```

**Arguments:**

| Argument | Default | Description |
|----------|---------|-------------|
| `PATH` | `.` | Repository root |

**Options:**

| Option | Default | Description |
|--------|---------|-------------|
| `--ref REF` | `latest` | Snapshot ref to check |
| `--model FILE` | `architecture.yml` | Architecture model file |
| `--rules FILE` | `rules.pacta.yml` | Rules file (repeatable) |
| `--format {text,json}` | `text` | Output format |
| `--baseline REF` | - | Compare against baseline snapshot |
| `--save-ref REF` | - | Also save result under this ref |
| `-q, --quiet` | - | Summary only |
| `-v, --verbose` | - | Include all details |

**Examples:**

```bash
# Check latest snapshot against rules
pacta check . --rules rules.pacta.yml

# Check a specific snapshot ref
pacta check . --ref v1 --rules rules.pacta.yml

# Check against baseline (only new violations fail)
pacta check . --baseline baseline --rules rules.pacta.yml

# JSON output
pacta check . --rules rules.pacta.yml --format json
```

**Typical workflow:**

```bash
# Step 1: Capture architecture
pacta snapshot save . --model architecture.yml

# Step 2: Evaluate rules against the snapshot
pacta check . --rules rules.pacta.yml

# Or do both in one step:
pacta scan . --model architecture.yml --rules rules.pacta.yml
```

## snapshot save

Save architecture snapshot without running rules.
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ References are named pointers to snapshot hashes:

| Ref | Description |
|-----|-------------|
| `latest` | Automatically updated on each scan |
| `latest` | Automatically updated on each scan/check |
| `baseline` | Created with `--save-ref baseline` |
| Custom | Any name you choose with `--save-ref <name>` |

Expand Down
19 changes: 15 additions & 4 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ containers:
Now capture a snapshot:

```bash
pacta snapshot . --model architecture.yml
pacta snapshot save . --model architecture.yml
```

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.
Expand All @@ -50,7 +50,7 @@ Pacta just analyzed every module and dependency in your codebase and stored a co
A week passes. Your team ships features, fixes bugs, refactors code. Run another snapshot:

```bash
pacta snapshot . --model architecture.yml
pacta snapshot save . --model architecture.yml
```

Now you have two points in time. See what changed:
Expand Down Expand Up @@ -122,9 +122,13 @@ rule:
message: Domain layer must not import from Infrastructure
```

Run a scan with rules:
Run a check against your snapshot:

```bash
# Option A: Check the snapshot you already have
pacta check . --rules rules.pacta.yml

# Option B: Or do snapshot + check in one step
pacta scan . --model architecture.yml --rules rules.pacta.yml
```

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

```bash
# One-step:
pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref baseline

# Or two-step:
pacta snapshot save . --model architecture.yml --ref baseline
pacta check . --ref baseline --rules rules.pacta.yml
```

Now future scans compare against this baseline:
Now future checks compare against this baseline:

```bash
pacta check . --rules rules.pacta.yml --baseline baseline
# Or equivalently:
pacta scan . --model architecture.yml --rules rules.pacta.yml --baseline baseline
```

Expand Down
3 changes: 3 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ Error: Baseline 'baseline' not found
1. Create a baseline first:
```bash
pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref baseline
# Or using the two-step workflow:
pacta snapshot save . --model architecture.yml --ref baseline
pacta check . --ref baseline --rules rules.pacta.yml
```

2. Check that `.pacta/` directory exists and contains snapshots:
Expand Down
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ To run any example:

```bash
cd examples/<example-name>

# One-step (scan = snapshot + check):
pacta scan . --model architecture.yml --rules rules.pacta.yml

# Or two-step:
pacta snapshot save . --model architecture.yml
pacta check . --rules rules.pacta.yml
```

## Creating Your Own
Expand Down
79 changes: 79 additions & 0 deletions pacta/cli/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from pathlib import Path

from pacta.cli._io import default_model_file, default_rules_files, ensure_repo_root
from pacta.cli.exitcodes import exit_code_from_report_dict
from pacta.core.config import EngineConfig
from pacta.core.engine import DefaultPactaEngine
from pacta.reporting.renderers.json import JsonReportRenderer
from pacta.reporting.renderers.text import TextReportRenderer
from pacta.snapshot.store import FsSnapshotStore


def run(
*,
path: str,
ref: str,
fmt: str,
rules: tuple[str, ...] | None,
model: str | None,
baseline: str | None,
save_ref: str | None,
verbosity: str = "normal",
tool_version: str | None,
) -> int:
"""
Evaluate rules against an existing snapshot.

Loads the snapshot identified by --ref, evaluates architecture rules,
and saves the updated snapshot (with violations) back to the store.
"""
repo_root = ensure_repo_root(path)

rules_files = rules if rules is not None else default_rules_files(repo_root)
model_file = model if model is not None else default_model_file(repo_root)

# Load existing snapshot
store = FsSnapshotStore(repo_root=repo_root)
if not store.exists(ref):
import sys

print(f"pacta: error: snapshot ref '{ref}' not found. Run `pacta snapshot save` first.", file=sys.stderr)
from pacta.cli.exitcodes import EXIT_ENGINE_ERROR

return EXIT_ENGINE_ERROR

snapshot = store.load(ref)

# Build config
cfg = EngineConfig(
repo_root=Path(repo_root),
model_file=Path(model_file) if model_file else None,
rules_files=tuple(Path(f) for f in rules_files),
baseline=baseline,
changed_only=False,
save_ref=save_ref,
)

# Run check
engine = DefaultPactaEngine()
result = engine.check(cfg, snapshot)

# Update existing snapshot object in-place with violations
short_hash = store.resolve_ref(ref)
if short_hash is None:
# ref was a direct hash
short_hash = ref
store.update_object(short_hash, result.snapshot)

# Optionally save under an additional ref
if save_ref and save_ref != ref:
store.save(result.snapshot, refs=[save_ref])

# Render report
if fmt == "json":
out = JsonReportRenderer().render(result.report)
else:
out = TextReportRenderer(verbosity=verbosity).render(result.report) # type: ignore[arg-type]
print(out, end="")

return exit_code_from_report_dict(result.report.to_dict())
30 changes: 29 additions & 1 deletion pacta/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import argparse
import sys

from pacta.cli import diff, history, scan, snapshot
from pacta.cli import check, diff, history, scan, snapshot
from pacta.cli.exitcodes import EXIT_ENGINE_ERROR


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

# check
check_p = sub.add_parser("check", help="Evaluate rules against a snapshot.")
check_p.add_argument("path", nargs="?", default=".", help="Repository root (default: .)")
check_p.add_argument("--ref", default="latest", help="Snapshot ref to check (default: latest).")
check_p.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
check_p.add_argument("--rules", action="append", default=None, help="Rules file path (repeatable).")
check_p.add_argument("--model", default=None, help="Architecture model file (architecture.yaml).")
check_p.add_argument("--baseline", default=None, help="Baseline snapshot ref.")
check_p.add_argument("--save-ref", dest="save_ref", default=None, help="Also save snapshot under this ref.")
check_verbosity = check_p.add_mutually_exclusive_group()
check_verbosity.add_argument("-q", "--quiet", action="store_true", help="Minimal output (summary only).")
check_verbosity.add_argument("-v", "--verbose", action="store_true", help="Verbose output (include all details).")

# snapshot
snap = sub.add_parser("snapshot", help="Snapshot operations.")
snap_sub = snap.add_subparsers(dest="snapshot_cmd", required=True)
Expand Down Expand Up @@ -96,6 +109,21 @@ def main(argv: list[str] | None = None) -> int:
tool_version=args.tool_version,
)

if args.cmd == "check":
rules = tuple(args.rules) if args.rules is not None else None
verbosity = "quiet" if args.quiet else ("verbose" if args.verbose else "normal")
return check.run(
path=args.path,
ref=args.ref,
fmt=args.format,
rules=rules,
model=args.model,
baseline=args.baseline,
save_ref=args.save_ref,
verbosity=verbosity,
tool_version=args.tool_version,
)

if args.cmd == "snapshot":
if args.snapshot_cmd == "save":
return snapshot.save(
Expand Down
Loading