Initial commit: Arduino Uno Q USB Audio Synth project
- USB gadget (UAC1 + MIDI) with boot persistence - USB host mode with auto-detection - Pure Data synth patch with ALSA + UDP netreceive MIDI input - MIDI rawmidi-to-UDP bridge (midi-bridge.py) - LED matrix visualization (led-matrix-viz.py via arduino-router RPC) - MCU sketch for LED matrix control (Arduino_RouterBridge + ArduinoGraphics) - Conftest/OPA Rego policy validation suite for .pd files - gen_synth_pd.py: programmatic .pd file generator - System services: midi-bridge, led-matrix-viz, pd-synth-auto, usb-role-detect - Justfile with deploy, enable, diagnostic recipes - Diagnostic script (diag.sh)
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
package main
|
||||
|
||||
# ─── Pd .pd file format validator (OPA v1.x syntax) ───
|
||||
# Based on puredata.info/docs/developer/PdFileFormat
|
||||
# OPA version: 1.15.2 (bundled with conftest 0.68.2)
|
||||
|
||||
# Input structure (from pd2json parser):
|
||||
# {
|
||||
# "x_record_count": N,
|
||||
# "records": [{ "type": "obj"|"connect"|..., "x_index": N, ... }],
|
||||
# "parse_errors": [...]
|
||||
# }
|
||||
|
||||
# Get a record by its x_index
|
||||
record_by_idx(idx) := r if {
|
||||
some r_rec in input.records
|
||||
r_rec.x_index == idx
|
||||
r := r_rec
|
||||
}
|
||||
|
||||
xcount := input.x_record_count
|
||||
|
||||
# Check if an object is a signal (~) type
|
||||
is_signal(name) if {
|
||||
endswith(name, "~")
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# VIOLATIONS
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
# Conftest evaluates rules named `violation`, `violations`, `deny`, `warn`
|
||||
|
||||
# Out-of-bounds source index
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
rec.src >= xcount
|
||||
msg := sprintf("connect[%d]: src index %d out of range (max %d)", [rec.x_index, rec.src, xcount - 1])
|
||||
}
|
||||
|
||||
# Out-of-bounds destination index
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
rec.dst >= xcount
|
||||
msg := sprintf("connect[%d]: dst index %d out of range (max %d)", [rec.x_index, rec.dst, xcount - 1])
|
||||
}
|
||||
|
||||
# Negative source
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
rec.src < 0
|
||||
msg := sprintf("connect[%d]: negative src index %d", [rec.x_index, rec.src])
|
||||
}
|
||||
|
||||
# Negative destination
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
rec.dst < 0
|
||||
msg := sprintf("connect[%d]: negative dst index %d", [rec.x_index, rec.dst])
|
||||
}
|
||||
|
||||
# Negative outlet
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
rec.outlet < 0
|
||||
msg := sprintf("connect[%d]: negative outlet %d", [rec.x_index, rec.outlet])
|
||||
}
|
||||
|
||||
# Negative inlet
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
rec.inlet < 0
|
||||
msg := sprintf("connect[%d]: negative inlet %d", [rec.x_index, rec.inlet])
|
||||
}
|
||||
|
||||
# Self-loop
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
rec.src == rec.dst
|
||||
msg := sprintf("connect[%d]: self-loop (src=%d dst=%d)", [rec.x_index, rec.src, rec.dst])
|
||||
}
|
||||
|
||||
# Duplicate connections
|
||||
violation contains msg if {
|
||||
some a, b in input.records
|
||||
a.type == "connect"
|
||||
b.type == "connect"
|
||||
a.x_index < b.x_index
|
||||
a.src == b.src
|
||||
a.outlet == b.outlet
|
||||
a.dst == b.dst
|
||||
a.inlet == b.inlet
|
||||
msg := sprintf("connect[%d]/[%d]: duplicate (%d,%d)->(%d,%d)", [a.x_index, b.x_index, a.src, a.outlet, a.dst, a.inlet])
|
||||
}
|
||||
|
||||
# Signal-to-control connection
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
src_rec := record_by_idx(rec.src)
|
||||
dst_rec := record_by_idx(rec.dst)
|
||||
src_rec.type == "obj"
|
||||
dst_rec.type == "obj"
|
||||
is_signal(src_rec.name)
|
||||
not is_signal(dst_rec.name)
|
||||
msg := sprintf("connect[%d]: signal (~) outlet %d (%s) to control inlet %d (%s)", [rec.x_index, rec.src, src_rec.name, rec.dst, dst_rec.name])
|
||||
}
|
||||
|
||||
# No canvas definition
|
||||
violation contains msg if {
|
||||
not has_canvas
|
||||
msg := "No #N canvas definition found"
|
||||
}
|
||||
|
||||
has_canvas if {
|
||||
some rec in input.records
|
||||
rec.type == "canvas"
|
||||
}
|
||||
|
||||
# Outlet exceeds known count (skip -1 = variable)
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
src_rec := record_by_idx(rec.src)
|
||||
src_rec.type == "obj"
|
||||
known_outlets[src_rec.name]
|
||||
known_outlets[src_rec.name] != -1
|
||||
rec.outlet >= known_outlets[src_rec.name]
|
||||
msg := sprintf("connect[%d]: obj %d (%s) has %d outlets, referenced outlet %d", [rec.x_index, rec.src, src_rec.name, known_outlets[src_rec.name], rec.outlet])
|
||||
}
|
||||
|
||||
# Inlet exceeds known count (skip -1 = variable)
|
||||
violation contains msg if {
|
||||
some rec in input.records
|
||||
rec.type == "connect"
|
||||
dst_rec := record_by_idx(rec.dst)
|
||||
dst_rec.type == "obj"
|
||||
known_inlets[dst_rec.name]
|
||||
known_inlets[dst_rec.name] != -1
|
||||
rec.inlet >= known_inlets[dst_rec.name]
|
||||
msg := sprintf("connect[%d]: obj %d (%s) has %d inlets, referenced inlet %d", [rec.x_index, rec.dst, dst_rec.name, known_inlets[dst_rec.name], rec.inlet])
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# KNOWLEDGE BASE: inlet/outlet counts by object name
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# -1 means variable/unknown
|
||||
|
||||
known_inlets := {
|
||||
# Audio signal objects
|
||||
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 0,
|
||||
"*~": 2, "+~": 2, "-~": 2, "/~": 2, "pow~": 2,
|
||||
"lop~": 2, "hip~": 2, "bp~": 3, "vcf~": 3,
|
||||
"cos~": 1, "wrap~": 1, "abs~": 1,
|
||||
"clip~": 3, "env~": 4,
|
||||
"dbtopower~": 1, "powertodb~": 1,
|
||||
"mtof": 1, "ftom": 1,
|
||||
"dac~": 2, "adc~": 0,
|
||||
"sig~": 1, "snapshot~": 1,
|
||||
"line~": 2, "vline~": 2,
|
||||
"tabread4~": 2, "tabwrite~": 2, "tabread~": 2,
|
||||
"send~": 1, "receive~": 1,
|
||||
"delwrite~": 2, "delread~": 1, "vd~": 2,
|
||||
"rfft~": 1, "rifft~": 1,
|
||||
"inlet~": 0, "outlet~": 0,
|
||||
"block~": 0, "switch~": 1,
|
||||
# MIDI
|
||||
"notein": 0, "noteout": 1,
|
||||
"ctlin": 0, "ctlout": 1,
|
||||
"midiin": 0, "midiout": 1,
|
||||
# Data flow
|
||||
"pack": -1, "unpack": 1,
|
||||
"makenote": 2, "stripnote": 2,
|
||||
"route": 1, "select": 1, "spigot": 2,
|
||||
"trigger": 1, "t": 1,
|
||||
"float": 1, "f": 1, "symbol": 1, "int": 1,
|
||||
# Math
|
||||
"+": 2, "-": 2, "*": 2, "/": 2, "pow": 2,
|
||||
"div": 2, "mod": 2,
|
||||
"max": 2, "min": 2,
|
||||
"moses": 2, "until": 1,
|
||||
"timer": 1, "random": 1,
|
||||
# Data
|
||||
"array": 1, "tabread": 1, "tabwrite": 2,
|
||||
"text": 1, "textfile": 1,
|
||||
"value": 0, "v": 0,
|
||||
"soundfiler": 1,
|
||||
# GUI
|
||||
"openpanel": 0, "savepanel": 0,
|
||||
"cnv": 0, "hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
|
||||
"bng": 0, "tgl": 0, "nbx": 1, "vu": 0,
|
||||
"print": 1,
|
||||
# Time
|
||||
"pipe": 1, "delay": 1, "del": 1,
|
||||
"metro": 1, "tempo": 1,
|
||||
"line": 2, "poly": 2,
|
||||
# Send/receive
|
||||
"s": 1, "send": 1, "r": 0, "receive": 0,
|
||||
"netsend": 2, "netreceive": 1,
|
||||
# Misc
|
||||
"loadbang": 0, "loadb": 0, "bang": 0,
|
||||
"inlet": 0, "outlet": 0,
|
||||
"expr": 1, "clone": 1,
|
||||
}
|
||||
|
||||
# Conftest checks deny, warn, and violation (singular)
|
||||
|
||||
known_outlets := {
|
||||
# Audio signal
|
||||
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 1,
|
||||
"*~": 1, "+~": 1, "-~": 1, "/~": 1, "pow~": 1,
|
||||
"lop~": 1, "hip~": 1, "bp~": 1, "vcf~": 1,
|
||||
"cos~": 1, "wrap~": 1, "abs~": 1,
|
||||
"clip~": 1, "env~": 1,
|
||||
"dbtopower~": 1, "powertodb~": 1,
|
||||
"mtof": 1, "ftom": 1,
|
||||
"dac~": 0, "adc~": 2,
|
||||
"sig~": 1, "snapshot~": 1,
|
||||
"line~": 1, "vline~": 1,
|
||||
"tabread4~": 1, "tabwrite~": 0, "tabread~": 1,
|
||||
"send~": 0, "receive~": 1,
|
||||
"delwrite~": 0, "delread~": 1, "vd~": 1,
|
||||
"rfft~": 2, "rifft~": 1,
|
||||
"inlet~": 1, "outlet~": 0,
|
||||
"block~": 0, "switch~": 0,
|
||||
# MIDI
|
||||
"notein": 2, "noteout": 0,
|
||||
"ctlin": 1, "ctlout": 0,
|
||||
"midiin": 1, "midiout": 0,
|
||||
# Data flow
|
||||
"pack": 1, "unpack": -1,
|
||||
"makenote": 0, "stripnote": 2,
|
||||
"route": -1, "select": 1, "spigot": 2,
|
||||
"trigger": -1, "t": -1,
|
||||
"float": 1, "f": 1, "symbol": 1, "int": 1,
|
||||
# Math
|
||||
"+": 1, "-": 1, "*": 1, "/": 1,
|
||||
"div": 1, "mod": 1, "pow": 1,
|
||||
"max": 1, "min": 1,
|
||||
"moses": 2, "until": 1,
|
||||
"timer": 2, "random": 1,
|
||||
# Data
|
||||
"array": 2, "tabread": 1, "tabwrite": 0,
|
||||
"text": 2, "textfile": 2,
|
||||
"value": 1, "v": 1,
|
||||
"soundfiler": 0,
|
||||
# GUI
|
||||
"openpanel": 1, "savepanel": 1,
|
||||
"cnv": 1,
|
||||
"hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
|
||||
"bng": 1, "tgl": 1, "nbx": 2, "vu": 0,
|
||||
"print": 0,
|
||||
# Time
|
||||
"pipe": 1, "delay": 1, "del": 1,
|
||||
"metro": 1, "tempo": 2,
|
||||
"line": 1, "poly": 1,
|
||||
# Send/receive
|
||||
"s": 0, "send": 0, "r": 1, "receive": 1,
|
||||
"netsend": 0, "netreceive": 1,
|
||||
# Misc
|
||||
"loadbang": 1, "loadb": 1, "bang": 1,
|
||||
"inlet": 0, "outlet": 0,
|
||||
"expr": 1, "clone": -1,
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Parse a .pd file into JSON for Conftest/Rego validation."""
|
||||
import json, sys, re
|
||||
from pathlib import Path
|
||||
|
||||
def parse_pd(filepath):
|
||||
with open(filepath) as f:
|
||||
text = f.read()
|
||||
|
||||
lines = text.split("\n")
|
||||
records = []
|
||||
x_index = 0
|
||||
errors = []
|
||||
|
||||
for i, raw in enumerate(lines):
|
||||
line = raw.strip()
|
||||
if not line or line == ";":
|
||||
continue
|
||||
if not line.startswith("#"):
|
||||
errors.append(f"line {i+1}: expected # prefix")
|
||||
continue
|
||||
|
||||
raw = line[1:].strip()
|
||||
if not raw.endswith(";"):
|
||||
errors.append(f"line {i+1}: missing terminating semicolon")
|
||||
continue
|
||||
body = raw[:-1].strip()
|
||||
|
||||
if line.startswith("#N"):
|
||||
nbody = body[1:].strip() # remove 'N'
|
||||
parts = nbody.split(None, 1)
|
||||
if parts[0] == "canvas":
|
||||
tokens = parts[1].split()
|
||||
rec = {
|
||||
"type": "canvas",
|
||||
"x": safe_int(tokens[0]),
|
||||
"y": safe_int(tokens[1]),
|
||||
"width": safe_int(tokens[2]),
|
||||
"height": safe_int(tokens[3]),
|
||||
"name": tokens[4] if len(tokens) > 4 else "",
|
||||
}
|
||||
records.append(rec)
|
||||
else:
|
||||
records.append({"type": "new", "data": rest})
|
||||
|
||||
elif line.startswith("#A"):
|
||||
values = [safe_float(v) for v in rest.split()]
|
||||
records.append({"type": "array_data", "values": values})
|
||||
|
||||
elif line.startswith("#X connect"):
|
||||
xbody = body[1:].strip()
|
||||
elems = xbody.split()
|
||||
if len(elems) >= 5:
|
||||
rec = {
|
||||
"type": "connect",
|
||||
"x_index": x_index,
|
||||
"src": safe_int(elems[1]),
|
||||
"outlet": safe_int(elems[2]),
|
||||
"dst": safe_int(elems[3]),
|
||||
"inlet": safe_int(elems[4]),
|
||||
}
|
||||
records.append(rec)
|
||||
# Connect records do NOT increment Pd's object index
|
||||
|
||||
elif line.startswith("#X obj"):
|
||||
xbody = body[1:].strip()
|
||||
# xbody = "obj 20 20 notein"
|
||||
elem, x_str, y_str, rest = xbody.split(None, 3)
|
||||
obj_parts = rest.split()
|
||||
rec = {
|
||||
"type": "obj",
|
||||
"x_index": x_index,
|
||||
"x": safe_int(x_str),
|
||||
"y": safe_int(y_str),
|
||||
"name": obj_parts[0] if obj_parts else "",
|
||||
"args": obj_parts[1:],
|
||||
}
|
||||
records.append(rec)
|
||||
x_index += 1
|
||||
|
||||
elif line.startswith("#X msg"):
|
||||
xbody = body[1:].strip()
|
||||
elem, x_str, y_str, content = xbody.split(None, 3)
|
||||
records.append({
|
||||
"type": "msg",
|
||||
"x_index": x_index,
|
||||
"x": safe_int(x_str),
|
||||
"y": safe_int(y_str),
|
||||
"content": content,
|
||||
})
|
||||
x_index += 1
|
||||
|
||||
elif line.startswith("#X floatatom"):
|
||||
xbody = body[1:].strip()
|
||||
elems = xbody.split()
|
||||
rec = {"type": "floatatom", "x_index": x_index, "x": safe_int(elems[1]), "y": safe_int(elems[2])}
|
||||
if len(elems) > 3: rec["width"] = safe_int(elems[3])
|
||||
if len(elems) > 4: rec["min"] = safe_float(elems[4])
|
||||
if len(elems) > 5: rec["max"] = safe_float(elems[5])
|
||||
records.append(rec)
|
||||
x_index += 1
|
||||
|
||||
elif line.startswith("#X symbolatom"):
|
||||
records.append({"type": "symbolatom", "x_index": x_index})
|
||||
x_index += 1
|
||||
|
||||
elif line.startswith("#X text"):
|
||||
xbody = body[1:].strip()
|
||||
records.append({"type": "text", "x_index": x_index, "content": xbody})
|
||||
x_index += 1
|
||||
|
||||
elif line.startswith("#X restore"):
|
||||
xbody = body[1:].strip()
|
||||
records.append({"type": "restore", "x_index": x_index, "data": xbody})
|
||||
x_index += 1
|
||||
|
||||
elif line.startswith("#X coords"):
|
||||
xbody = body[1:].strip()
|
||||
records.append({"type": "coords", "x_index": x_index, "data": xbody})
|
||||
x_index += 1
|
||||
|
||||
elif line.startswith("#X array"):
|
||||
xbody = body[1:].strip()
|
||||
records.append({"type": "array", "x_index": x_index, "data": xbody})
|
||||
x_index += 1
|
||||
|
||||
else:
|
||||
errors.append(f"line {i+1}: unknown record type: {line[:60]}")
|
||||
|
||||
return {
|
||||
"file": str(filepath),
|
||||
"x_record_count": x_index,
|
||||
"records": records,
|
||||
"parse_errors": errors,
|
||||
}
|
||||
|
||||
def safe_int(s):
|
||||
try:
|
||||
# Handle Pd special values like "-" for empty
|
||||
if s == "-":
|
||||
return 0
|
||||
return int(s)
|
||||
except (ValueError, IndexError):
|
||||
return 0
|
||||
|
||||
def safe_float(s):
|
||||
try:
|
||||
if s == "-":
|
||||
return 0.0
|
||||
return float(s)
|
||||
except (ValueError, IndexError):
|
||||
return 0.0
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: pd2json.py <file.pd> [file2.pd ...]")
|
||||
sys.exit(1)
|
||||
results = []
|
||||
for arg in sys.argv[1:]:
|
||||
results.append(parse_pd(arg))
|
||||
print(json.dumps(results if len(results) > 1 else results[0], indent=2))
|
||||
@@ -0,0 +1,14 @@
|
||||
#N canvas 0 0 400 300 10;
|
||||
#X obj 20 20 notein;
|
||||
#X obj 80 20 osc~;
|
||||
#X connect 0 0 1 0;
|
||||
#X connect 0 1 1 1;
|
||||
#X connect 0 0 1 0;
|
||||
#X connect 99 0 1 0;
|
||||
#X connect 0 0 99 0;
|
||||
#X connect 0 0 0 0;
|
||||
#X connect 0 -1 1 0;
|
||||
#X obj 20 100 dac~;
|
||||
#X connect 1 99 3 0;
|
||||
#X connect 1 0 3 99;
|
||||
#X connect 1 0 3 0;
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate .pd files using Conftest/OPA Rego policies."""
|
||||
import json, sys, subprocess, tempfile, os, glob
|
||||
from pathlib import Path
|
||||
from pd2json import parse_pd
|
||||
|
||||
def main():
|
||||
policy_dir = Path(__file__).parent
|
||||
|
||||
# Find conftest/opa (check mise install dirs first)
|
||||
mise_dir = os.path.expanduser("~/.local/share/mise/installs")
|
||||
for tool in ["conftest", "opa"]:
|
||||
for ver_dir in sorted(glob.glob(os.path.join(mise_dir, tool, "*")), reverse=True):
|
||||
for p in [os.path.join(ver_dir, tool), os.path.join(ver_dir, "bin", tool)]:
|
||||
if os.path.isfile(p):
|
||||
os.environ["PATH"] = os.path.dirname(p) + ":" + os.environ.get("PATH", "")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
|
||||
runner = None
|
||||
for cmd in ["conftest", "opa"]:
|
||||
try:
|
||||
subprocess.run([cmd, "--version"], capture_output=True, timeout=5)
|
||||
runner = cmd
|
||||
break
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
continue
|
||||
|
||||
if not runner:
|
||||
print("ERROR: neither 'conftest' nor 'opa' found in PATH.")
|
||||
print("Install: pip install conftest or https://www.conftest.dev/")
|
||||
sys.exit(1)
|
||||
|
||||
pd_files = sys.argv[1:] or list(Path(".").glob("*.pd"))
|
||||
if not pd_files:
|
||||
print("Usage: validate.py <file.pd> [file2.pd ...]")
|
||||
sys.exit(1)
|
||||
|
||||
all_passed = True
|
||||
for pf in pd_files:
|
||||
pf = Path(pf)
|
||||
data = parse_pd(pf)
|
||||
|
||||
if runner == "conftest":
|
||||
result = run_conftest(data, policy_dir)
|
||||
else:
|
||||
result = run_opa(data, policy_dir)
|
||||
|
||||
if result["passed"]:
|
||||
print(f" ✓ {pf.name} — all checks passed")
|
||||
else:
|
||||
all_passed = False
|
||||
print(f" ✗ {pf.name} — {len(result['violations'])} violation(s):")
|
||||
for v in result["violations"]:
|
||||
print(f" • {v}")
|
||||
|
||||
sys.exit(0 if all_passed else 1)
|
||||
|
||||
|
||||
def run_conftest(data, policy_dir):
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
|
||||
json.dump(data, f)
|
||||
json_path = f.name
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["conftest", "test", "--policy", str(policy_dir), json_path],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
out = result.stdout + result.stderr
|
||||
violations = []
|
||||
for l in out.split("\n"):
|
||||
if "FAIL" in l or "failed" in l.lower():
|
||||
violations.append(l.strip())
|
||||
# Also parse structured output if json
|
||||
if result.stdout and not violations:
|
||||
try:
|
||||
import json as j
|
||||
report = j.loads(result.stdout)
|
||||
for fname, checks in report.get("failures", {}).items():
|
||||
for c in checks:
|
||||
violations.append(c)
|
||||
except:
|
||||
pass
|
||||
return {"passed": result.returncode == 0, "violations": violations}
|
||||
finally:
|
||||
os.unlink(json_path)
|
||||
|
||||
|
||||
def run_opa(data, policy_dir):
|
||||
pkg = "main"
|
||||
query = "data.main.violations"
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
|
||||
json.dump(data, f)
|
||||
json_path = f.name
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["opa", "eval", "-d", str(policy_dir), "-d", json_path, "--format", "json", query],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return {"passed": False, "violations": [result.stderr.strip()]}
|
||||
|
||||
out = json.loads(result.stdout)
|
||||
violations = []
|
||||
for expr in out.get("result", []):
|
||||
for binding in expr.get("expressions", []):
|
||||
vals = binding.get("value", [])
|
||||
if isinstance(vals, list):
|
||||
violations.extend(vals)
|
||||
return {"passed": len(violations) == 0, "violations": violations}
|
||||
finally:
|
||||
os.unlink(json_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user