midi_check.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:c docs(KD-STAGING): sync governance after KD-6b DONE Mirror workspace go… · aaronrene · Jul 10, 2026
1 """``muse midi-check`` — MIDI invariant enforcement.
2
3 Evaluates the invariant rules declared in ``.muse/midi_invariants.toml``
4 against every MIDI track in the specified commit and reports violations with
5 severity, location, and description.
6
7 Built-in rule types (declared in TOML)::
8
9 [[rule]]
10 name = "max_polyphony"
11 severity = "error"
12 rule_type = "max_polyphony"
13 [rule.params]
14 max_simultaneous = 6
15
16 [[rule]]
17 name = "pitch_range"
18 severity = "warning"
19 rule_type = "pitch_range"
20 [rule.params]
21 min_pitch = 24
22 max_pitch = 108
23
24 [[rule]]
25 name = "key_consistency"
26 severity = "info"
27 rule_type = "key_consistency"
28 [rule.params]
29 threshold = 0.15
30
31 [[rule]]
32 name = "no_parallel_fifths"
33 severity = "warning"
34 rule_type = "no_parallel_fifths"
35
36 Usage::
37
38 muse midi-check # check HEAD
39 muse midi-check abc1234 # check specific commit
40 muse midi-check --track piano.mid # check one track
41 muse midi-check --strict # exit 1 on any error-severity violation
42 muse midi-check --json # machine-readable output
43 """
44
45 from __future__ import annotations
46
47 import argparse
48 import json
49 import logging
50 import pathlib
51 import sys
52
53 from muse.core.repo import require_repo
54 from muse.core.store import get_head_commit_id, read_current_branch
55 from muse.plugins.midi._invariants import (
56 InvariantReport,
57 load_invariant_rules,
58 run_invariants,
59 )
60
61 logger = logging.getLogger(__name__)
62
63
64 def _read_branch(root: pathlib.Path) -> str:
65 return read_current_branch(root)
66
67
68 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
69 """Register the midi-check subcommand."""
70 parser = subparsers.add_parser("check", help="Enforce MIDI invariant rules against a commit's MIDI tracks.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
71 parser.add_argument("commit", nargs="?", metavar="COMMIT", default=None, help="Commit ID to check (default: HEAD).")
72 parser.add_argument("--track", "-t", metavar="PATH", default=None, help="Restrict check to a single MIDI file path.")
73 parser.add_argument("--rules", "-r", metavar="FILE", default=None, dest="rules_file", help="Path to a TOML invariant rules file (default: .muse/midi_invariants.toml).")
74 parser.add_argument("--strict", action="store_true", help="Exit with code 1 when any error-severity violations are found.")
75 parser.add_argument("--json", action="store_true", dest="as_json", help="Output machine-readable JSON instead of formatted text.")
76 parser.set_defaults(func=run)
77
78
79 def run(args: argparse.Namespace) -> None:
80 """Enforce MIDI invariant rules against a commit's MIDI tracks."""
81 commit: str | None = args.commit
82 track: str | None = args.track
83 rules_file: str | None = args.rules_file
84 strict: bool = args.strict
85 as_json: bool = args.as_json
86
87 root = require_repo()
88
89 commit_id = commit
90 if commit_id is None:
91 branch = _read_branch(root)
92 commit_id = get_head_commit_id(root, branch)
93 if commit_id is None:
94 print("❌ No commits in this repository.", file=sys.stderr)
95 raise SystemExit(1)
96
97 # Load rules.
98 rules_path: pathlib.Path | None = None
99 if rules_file:
100 rules_path = pathlib.Path(rules_file)
101 else:
102 default = root / ".muse" / "midi_invariants.toml"
103 if default.exists():
104 rules_path = default
105
106 rules = load_invariant_rules(rules_path)
107 report = run_invariants(root, commit_id, rules, track_filter=track)
108
109 if as_json:
110 sys.stdout.write(json.dumps(report, indent=2) + "\n")
111 else:
112 _print_report(report)
113
114 if strict and report["has_errors"]:
115 raise SystemExit(1)
116
117
118 _SEVERITY_ICON = {
119 "error": "❌",
120 "warning": "⚠️",
121 "info": "ℹ️",
122 }
123
124
125 def _print_report(report: InvariantReport) -> None:
126 """Format and print an invariant report to stdout."""
127 violations = report["violations"]
128
129 if not violations:
130 print(
131 f"✅ No violations found ({report['rules_checked']} rule-track checks)"
132 )
133 return
134
135 current_track: str | None = None
136 for v in violations:
137 if v["track"] != current_track:
138 current_track = v["track"]
139 print(f"\n {current_track}")
140 icon = _SEVERITY_ICON.get(v["severity"], "•")
141 bar_label = f"bar {v['bar']}" if v["bar"] > 0 else "track"
142 print(
143 f" {icon} [{v['rule_name']}] {bar_label}: {v['description']}"
144 )
145
146 error_count = sum(1 for v in violations if v["severity"] == "error")
147 warn_count = sum(1 for v in violations if v["severity"] == "warning")
148 info_count = sum(1 for v in violations if v["severity"] == "info")
149
150 parts: list[str] = []
151 if error_count:
152 parts.append(f"{error_count} error{'s' if error_count != 1 else ''}")
153 if warn_count:
154 parts.append(f"{warn_count} warning{'s' if warn_count != 1 else ''}")
155 if info_count:
156 parts.append(f"{info_count} info")
157
158 summary = ", ".join(parts)
159 icon = "❌" if error_count else "⚠️" if warn_count else "ℹ️"
160 print(
161 f"\n{icon} {summary} in commit {report['commit_id'][:8]} "
162 f"({report['rules_checked']} rule-track checks)"
163 )