gabriel / muse public
code_check.py python
100 lines 3.1 KB
e6786943 feat: upgrade to Python 3.14, drop from __future__ import annotations Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """``muse code-check`` — code invariant enforcement.
2
3 Evaluates semantic rules declared in ``.muse/code_invariants.toml`` against
4 the code snapshot of the specified commit and reports violations.
5
6 Built-in rule types (declared in TOML)::
7
8 [[rule]]
9 name = "complexity_gate"
10 severity = "error"
11 rule_type = "max_complexity"
12 [rule.params]
13 threshold = 10
14
15 [[rule]]
16 name = "no_cycles"
17 severity = "error"
18 rule_type = "no_circular_imports"
19
20 [[rule]]
21 name = "dead_exports"
22 severity = "warning"
23 rule_type = "no_dead_exports"
24
25 [[rule]]
26 name = "coverage_floor"
27 severity = "warning"
28 rule_type = "test_coverage_floor"
29 [rule.params]
30 min_ratio = 0.30
31
32 Usage::
33
34 muse code-check # check HEAD
35 muse code-check abc1234 # check specific commit
36 muse code-check --strict # exit 1 on any error-severity violation
37 muse code-check --json # machine-readable JSON output
38 muse code-check --rules my_rules.toml
39 """
40
41 import json
42 import logging
43 import pathlib
44 import sys
45
46 import typer
47
48 from muse.core.invariants import format_report
49 from muse.core.repo import require_repo
50 from muse.core.store import get_head_commit_id
51 from muse.plugins.code._invariants import CodeChecker, load_invariant_rules, run_invariants
52
53 logger = logging.getLogger(__name__)
54
55 app = typer.Typer()
56
57
58 def _resolve_head(root: pathlib.Path) -> str | None:
59 head_ref = (root / ".muse" / "HEAD").read_text().strip()
60 branch = head_ref.removeprefix("refs/heads/").strip()
61 return get_head_commit_id(root, branch)
62
63
64 @app.callback(invoke_without_command=True)
65 def code_check(
66 ctx: typer.Context,
67 commit_arg: str | None = typer.Argument(None, help="Commit ID to check (default: HEAD)."),
68 strict: bool = typer.Option(False, "--strict", help="Exit 1 when any error-severity violation is found."),
69 output_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
70 rules_file: str | None = typer.Option(None, "--rules", help="Path to a TOML invariants file (default: .muse/code_invariants.toml)."),
71 ) -> None:
72 """Enforce code invariant rules against a commit snapshot.
73
74 Reports cyclomatic complexity violations, import cycles, dead exports,
75 and test coverage shortfalls based on the rules in
76 ``.muse/code_invariants.toml`` (or built-in defaults when the file is
77 absent).
78 """
79 root = require_repo()
80
81 commit_id = commit_arg or _resolve_head(root)
82 if commit_id is None:
83 typer.echo("❌ No commit found.")
84 raise typer.Exit(code=1)
85
86 rules_path = pathlib.Path(rules_file) if rules_file else None
87 rules = load_invariant_rules(rules_path)
88 report = run_invariants(root, commit_id, rules)
89
90 if output_json:
91 typer.echo(json.dumps(report))
92 if strict and report["has_errors"]:
93 raise typer.Exit(code=1)
94 return
95
96 typer.echo(f"\ncode-check {commit_id[:8]} — {report['rules_checked']} rules")
97 typer.echo(format_report(report))
98
99 if strict and report["has_errors"]:
100 raise typer.Exit(code=1)