"""``muse code code-check`` — code invariant enforcement. Evaluates semantic rules declared in ``.muse/code_invariants.toml`` against the code snapshot of the specified commit and reports violations. Built-in rule types (declared in TOML):: [[rule]] name = "complexity_gate" severity = "error" rule_type = "max_complexity" [rule.params] threshold = 10 [[rule]] name = "no_cycles" severity = "error" rule_type = "no_circular_imports" [[rule]] name = "dead_exports" severity = "warning" rule_type = "no_dead_exports" [[rule]] name = "coverage_floor" severity = "warning" rule_type = "test_coverage_floor" [rule.params] min_ratio = 0.30 Usage:: muse code code-check # check HEAD muse code code-check abc1234 # check specific commit muse code code-check --strict # exit 1 on any error-severity violation muse code code-check --json # machine-readable JSON output muse code code-check --rules my_rules.toml # custom rules file muse code code-check --filter error # show only error-severity violations muse code code-check --diff HEAD~1 # show only NEW violations vs reference muse code code-check --diff main --strict # CI ratchet: fail only on regressions """ import argparse import json import logging import pathlib import sys from typing import TypedDict from muse.core.types import long_id from muse.core.invariants import diff_reports, format_report, make_report from muse.core.repo import require_repo from muse.core.refs import ( get_head_commit_id, read_current_branch, ) from muse.core.commits import find_commits_by_prefix from muse.core.snapshots import get_commit_snapshot_manifest from muse.core.timing import start_timer from muse.core.envelope import EnvelopeJson, make_envelope from muse.core.validation import contain_path from muse.plugins.code._invariants import load_invariant_rules, run_invariants logger = logging.getLogger(__name__) _SEVERITIES = ("error", "warning", "info") class _ViolationDict(TypedDict): rule_name: str severity: str address: str description: str class _CodeCheckOutputJson(EnvelopeJson, total=False): """Top-level JSON envelope emitted by ``muse code code-check --json``. Fields ------ commit_id Full commit ID that was checked. domain Domain tag (always ``"code"``). violations List of violation dicts (rule_name, severity, address, description). rules_checked Number of rules that were evaluated. has_errors True when any violation has severity ``"error"``. has_warnings True when any violation has severity ``"warning"``. diff_ref The --diff reference commit, if provided; else absent. severity_filter The --filter severity, if provided; else absent. """ commit_id: str domain: str violations: list[_ViolationDict] rules_checked: int has_errors: bool has_warnings: bool diff_ref: str severity_filter: str def _resolve_commit(root: pathlib.Path, ref: str | None) -> str | None: """Resolve *ref* to a full commit ID. Resolution order: 1. ``None`` → HEAD of the current branch. 2. Branch name that exists in the store → HEAD commit of that branch. 3. Anything else → returned as-is (assumed to be a full or partial commit ID; the caller validates via the snapshot manifest). """ if ref is None: branch = read_current_branch(root) return get_head_commit_id(root, branch) # If ref looks like a commit ID (sha256: prefix or raw hex), resolve it. bare = long_id(ref, strip=True) if all(c in "0123456789abcdefABCDEF" for c in bare) and 1 <= len(bare) <= 64: if len(bare) == 64: return ref # full ID — pass as-is # Short prefix — expand to full commit ID via prefix lookup. matches = find_commits_by_prefix(root, bare) if matches: return matches[0].commit_id return ref # not found; let caller surface the error # Try treating ref as a branch name first. try: branch_commit = get_head_commit_id(root, ref) except (ValueError, KeyError): branch_commit = None if branch_commit is not None: return branch_commit # Fall back: treat as a literal commit ID. return ref def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: """Register the code-check subcommand.""" parser = subparsers.add_parser( "code-check", help="Enforce code invariant rules against a commit snapshot.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "commit_arg", nargs="?", default=None, metavar="commit", help="Commit ID to check (default: HEAD).", ) parser.add_argument( "--strict", action="store_true", help="Exit 1 when any error-severity violation is found.", ) parser.add_argument( "--json", "-j", action="store_true", dest="json_out", help="Emit machine-readable JSON.", ) parser.add_argument( "--rules", default=None, dest="rules_file", metavar="RULES_FILE", help=( "Path to a TOML invariants file inside the repo " "(default: .muse/code_invariants.toml)." ), ) parser.add_argument( "--filter", dest="severity_filter", choices=_SEVERITIES, default=None, metavar="SEVERITY", help="Show only violations of this severity (error | warning | info).", ) parser.add_argument( "--diff", dest="diff_ref", default=None, metavar="REF", help=( "Show only violations that are NEW compared to REF. " "Ideal for CI: muse code code-check --diff HEAD~1 --strict " "fails only when you introduce a new violation." ), ) parser.set_defaults(func=run) def run(args: argparse.Namespace) -> None: """Enforce code invariant rules against a commit snapshot. Reports cyclomatic complexity violations, import cycles, dead exports, and test coverage shortfalls based on rules in ``.muse/code_invariants.toml`` (or built-in defaults when the file is absent). Use ``--diff`` to report only regressions relative to a baseline commit. Agent quickstart ---------------- :: muse code check --json muse code check --strict --json muse code check --diff HEAD~1 --json JSON fields ----------- commit_id Full commit ID checked. has_errors ``true`` if any error-severity violations were found. violation_count Total number of violations. violations List of violation objects: ``rule``, ``path``, ``message``, ``severity``. exit_code 0 = clean (or only warnings without ``--strict``); 1 = errors (or warnings with ``--strict``). Exit codes ---------- 0 No violations (or only warnings without ``--strict``). 1 Errors found; or warnings found with ``--strict``. 2 Not inside a Muse repository. """ elapsed = start_timer() commit_arg: str | None = args.commit_arg strict: bool = args.strict json_out: bool = args.json_out rules_file: str | None = args.rules_file severity_filter: str | None = args.severity_filter diff_ref: str | None = args.diff_ref root = require_repo() commit_id = _resolve_commit(root, commit_arg) if commit_id is None: print("❌ No commit found.", file=sys.stderr) raise SystemExit(1) rules_path: pathlib.Path | None = None if rules_file: try: rules_path = contain_path(root, rules_file) except ValueError as exc: print(f"❌ {exc}", file=sys.stderr) raise SystemExit(1) if not rules_path.exists(): print(f"❌ Rules file not found: {rules_path}", file=sys.stderr) raise SystemExit(1) rules = load_invariant_rules(root, rules_path) report = run_invariants(root, commit_id, rules) # --diff: subtract violations already present in the reference commit so # only regressions are reported. if diff_ref is not None: ref_id = _resolve_commit(root, diff_ref) if ref_id is None: print(f"❌ Could not resolve --diff ref: {diff_ref!r}", file=sys.stderr) raise SystemExit(1) if get_commit_snapshot_manifest(root, ref_id) is None: print( f"❌ Commit {diff_ref!r} not found — cannot use as --diff base.", file=sys.stderr, ) raise SystemExit(1) ref_report = run_invariants(root, ref_id, rules) report = diff_reports(report, ref_report) # --filter: drop violations that don't match the requested severity. if severity_filter is not None: filtered = [v for v in report["violations"] if v["severity"] == severity_filter] report = make_report( report["commit_id"], report["domain"], filtered, report["rules_checked"], ) if json_out: _exit_code = 1 if (strict and report["has_errors"]) else 0 out = _CodeCheckOutputJson( **make_envelope(elapsed, exit_code=_exit_code), commit_id=report["commit_id"], domain=report["domain"], violations=report["violations"], rules_checked=report["rules_checked"], has_errors=report["has_errors"], has_warnings=report["has_warnings"], ) if diff_ref is not None: out["diff_ref"] = diff_ref if severity_filter is not None: out["severity_filter"] = severity_filter print(json.dumps(out)) if _exit_code: raise SystemExit(_exit_code) return diff_label = f" (new since {diff_ref})" if diff_ref else "" filter_label = f" [{severity_filter} only]" if severity_filter else "" print( f"\ncode-check {commit_id}{diff_label}{filter_label}" f" — {report['rules_checked']} rules" ) print(format_report(report)) if strict and report["has_errors"]: raise SystemExit(1)