gabriel / muse public
check.py python
95 lines 3.2 KB
f7645c07 feat(store): self-describing HEAD format with typed read/write API (#163) Gabriel Cardona <cgcardona@gmail.com> 3d ago
1 """``muse check`` — generic domain invariant enforcement.
2
3 Dispatches to the domain plugin's registered :class:`~muse.core.invariants.InvariantChecker`
4 and reports all violations. Works for any domain that has registered a checker.
5
6 Currently supported domains:
7
8 - ``midi`` — polyphony, pitch range, key consistency, parallel fifths.
9 - ``code`` — complexity, circular imports, dead exports, test coverage.
10
11 Usage::
12
13 muse check # check HEAD with auto-detected domain
14 muse check abc1234 # check specific commit
15 muse check --strict # exit 1 on any error-severity violation
16 muse check --json # machine-readable JSON output
17 muse check --rules my.toml # custom rules file
18 """
19
20 from __future__ import annotations
21
22 import json
23 import logging
24 import pathlib
25
26 import typer
27
28 from muse.core.invariants import InvariantChecker, format_report
29 from muse.core.repo import require_repo
30 from muse.core.store import get_head_commit_id, read_current_branch
31 from muse.plugins.registry import read_domain
32
33 logger = logging.getLogger(__name__)
34
35 app = typer.Typer()
36
37
38 def _resolve_head(root: pathlib.Path) -> str | None:
39 branch = read_current_branch(root)
40 return get_head_commit_id(root, branch)
41
42
43 def _get_checker(domain: str) -> InvariantChecker | None:
44 """Return the domain's InvariantChecker instance, or None."""
45 if domain == "code":
46 from muse.plugins.code._invariants import CodeChecker
47 return CodeChecker()
48 if domain == "midi":
49 from muse.plugins.midi._invariants import MidiChecker
50 return MidiChecker()
51 return None
52
53
54 @app.callback(invoke_without_command=True)
55 def check(
56 ctx: typer.Context,
57 commit_arg: str | None = typer.Argument(None, help="Commit ID to check (default: HEAD)."),
58 strict: bool = typer.Option(False, "--strict", help="Exit 1 when any error-severity violation is found."),
59 output_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
60 rules_file: str | None = typer.Option(None, "--rules", help="Path to a TOML invariants file."),
61 ) -> None:
62 """Run invariant checks for the current domain against a commit.
63
64 Auto-detects the repository domain (code or midi) and dispatches to the
65 appropriate checker. Use ``muse code-check`` or ``muse midi-check`` for
66 domain-specific options.
67 """
68 root = require_repo()
69 domain = read_domain(root)
70
71 commit_id = commit_arg or _resolve_head(root)
72 if commit_id is None:
73 typer.echo("❌ No commit found.")
74 raise typer.Exit(code=1)
75
76 checker = _get_checker(domain)
77 if checker is None:
78 typer.echo(f"⚠️ No invariant checker registered for domain {domain!r}.")
79 typer.echo(" Supported domains: code, midi")
80 raise typer.Exit(code=0)
81
82 rules_path = pathlib.Path(rules_file) if rules_file else None
83 report = checker.check(root, commit_id, rules_file=rules_path)
84
85 if output_json:
86 typer.echo(json.dumps(report))
87 if strict and report["has_errors"]:
88 raise typer.Exit(code=1)
89 return
90
91 typer.echo(f"\ncheck [{domain}] {commit_id[:8]} — {report['rules_checked']} rules")
92 typer.echo(format_report(report))
93
94 if strict and report["has_errors"]:
95 raise typer.Exit(code=1)