Commit a00066b1 by PLN (Algolia)

feat(rights): enumerate the banks a release contains, and rank the questions

PLN: "cant push the single tracks when samples hit, but full mix might well
pass!" — which matches the measurement (an isolated track flagged where the
87-minute mix passed). So the gate on a per-track release is per BANK, and this
tool answers "what is in here and where did it come from". The verdict stays
his, recorded once in rights_ledger.json so it is never re-litigated.

THE DISCRIMINATOR IS THE WHOLE COMMIT. "Lives in the Dirt-Samples folder" is not
"is a Dirt-Samples bank": 56 of OPAL-26's 60 banks resolve to that directory,
including the_revolution, like_sugar, desire, wap and crimewave — because local
banks were dropped in beside the quark's own. Classifying by location would have
returned a confident, comprehensively wrong LOW-RISK verdict on the most
obviously sampled material in the set. That is the same shape as reading a role
off a sample's name.

The quark is a git checkout, so `git ls-files` settles it exactly:
217 upstream banks, 509 added locally. Tracked = ships with the quark under its
license, auto-verdict `dirt_samples`. Untracked = provenance unknown to the
tool, so `unknown`, which blocks by design. A bank in the score but not on disk
reports UNRESOLVED rather than passing as safe.

RANKED BY LEVERAGE, not alphabetically. 41 open banks reads as 41 equal chores;
in fact five gate almost the whole record — jungle_breaks (14 tracks), h2ogmhh
(9), kick (9), risers (8), vec1_claps (6). Answering those five changes the
shape of the release; answering `take5` changes one track. Keystone first.

First run on OPAL-26: 60 distinct banks, 19 upstream and clear, 41 open, so all
15 tracks currently block. That is the honest starting position, not a failure —
and it is per-track only. The continuous mix remains the lower-risk artefact and
is a separate question.
parent 56a402ed
#!/usr/bin/env python3
"""rights_audit — which sample banks does a release actually contain, and who owns them.
PLN: *"cant push the single tracks when samples hit, but full mix might well
pass!"* — the measurement agrees (an isolated track was flagged where the
87-minute mix passed). So before any per-track release, the question is per
BANK: is this ours, is it licensed, or is it somebody's record.
The machine's job here is enumeration and joining, not judgement. It answers
"what is in this release and where did each bank come from"; the verdict is
PLN's and is stored once in `rights_ledger.json` so it is never re-litigated.
## The discriminator that matters
"Lives in the Dirt-Samples folder" is NOT "is a Dirt-Samples bank". 56 of
OPAL-26's 60 banks resolve to that directory, including `the_revolution`,
`desire` and `crimewave` — because local banks were dropped in beside the
quark's own. Classifying by location would have returned a confident,
comprehensively wrong low-risk verdict on the most obviously sampled material
in the set.
Dirt-Samples is a git checkout, so `git ls-files` gives the exact upstream set:
**217 upstream banks, 509 added locally.** Tracked = shipped with the quark
under its own license. Untracked = PLN's, provenance unknown to the machine,
and therefore a question for him.
python3 rights_audit.py judge_specs/opal26.json # report
python3 rights_audit.py judge_specs/opal26.json --init # seed the ledger
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from datetime import date
from pathlib import Path
HERE = Path(__file__).parent
LEDGER = HERE / "rights_ledger.json"
WWW = Path("/home/pln/Work/Web/www")
# Where SuperDirt actually loads from — read off start_and_midi.scd. If that
# boot file changes, this must too; a bank that resolves nowhere is reported
# as UNRESOLVED rather than silently treated as safe.
ROOTS = {
"dirt": Path("/home/pln/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples"),
"extra": Path("/home/pln/Work/Sound/Samples/extra"),
"machines": Path("/home/pln/Work/Sound/Samples/tidal-drum-machines/machines"),
}
# The verdicts PLN can record. Only `original` and `cleared` are releasable
# per-track; `unknown` is the honest default and blocks by design.
STATUSES = ("original", "cleared", "dirt_samples", "third_party", "unknown")
RELEASABLE = ("original", "cleared", "dirt_samples")
def norm(name: str) -> str:
"""SuperDirt strips hyphens when naming banks (see the boot's namingFunction)."""
return name.replace("-", "").lower()
def upstream_banks(root: Path) -> set[str]:
"""The banks the quark itself ships, straight from git."""
r = subprocess.run(["git", "-C", str(root), "ls-files"],
capture_output=True, text=True)
if r.returncode != 0:
return set()
return {norm(p.split("/")[0]) for p in r.stdout.splitlines() if "/" in p}
def index() -> dict[str, dict]:
"""bank -> {root, origin, dir, files}. Origin is the answer that matters."""
up = upstream_banks(ROOTS["dirt"])
out: dict[str, dict] = {}
for label, root in ROOTS.items():
if not root.exists():
continue
for d in sorted(root.iterdir()):
if not d.is_dir() or d.name.startswith("."):
continue
key = norm(d.name)
if key in out:
continue # first root wins, as SuperDirt does
out[key] = {
"root": label,
"origin": "upstream" if (label == "dirt" and key in up) else "local",
"dir": str(d),
"files": len(list(d.glob("*.wav")) + list(d.glob("*.flac"))),
}
return out
def load_ledger() -> dict:
if LEDGER.exists():
return json.loads(LEDGER.read_text())
return {"_comment": [], "banks": {}}
def gig_banks(spec: dict) -> dict[str, list[str]]:
"""bank -> the tracks that use it, from the CANONICAL tracklist."""
tj = json.loads(Path(spec["tracksJson"]).read_text())
ts = tj.get("tracks", tj) if isinstance(tj, dict) else tj
out: dict[str, list[str]] = {}
for t in ts:
for b in t.get("samples", []):
out.setdefault(norm(b), []).append(t["name"])
return out
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("spec")
ap.add_argument("--init", action="store_true",
help="add this gig's banks to the ledger as `unknown`")
ap.add_argument("--show", default="open", choices=["open", "all"],
help="`open` lists only banks still needing a verdict")
a = ap.parse_args()
spec = json.loads(Path(a.spec).read_text())
idx = index()
used = gig_banks(spec)
ledger = load_ledger()
banks = ledger.setdefault("banks", {})
if a.init:
added = 0
for b in sorted(used):
if b in banks:
continue
info = idx.get(b, {})
# An upstream bank gets its verdict for free: it ships with the
# quark under the quark's license. Everything else starts UNKNOWN,
# which is the honest default and the one that blocks.
banks[b] = {
"status": "dirt_samples" if info.get("origin") == "upstream" else "unknown",
"origin": info.get("origin", "unresolved"),
"files": info.get("files", 0),
"note": "",
"decided": date.today().isoformat() if info.get("origin") == "upstream" else None,
}
added += 1
ledger["_comment"] = [
"Per-BANK rights verdicts. The machine enumerates; PLN adjudicates.",
"`origin` is measured (upstream = tracked in the Dirt-Samples git",
"checkout; local = added beside it, provenance unknown to the tool).",
"`status` is a human verdict: " + " | ".join(STATUSES) + ".",
"Only " + ", ".join(RELEASABLE) + " are releasable per-track.",
"A bank answered here is answered for every release that uses it.",
]
LEDGER.write_text(json.dumps(ledger, indent=1, ensure_ascii=False) + "\n")
print(f"ledger: +{added} bank(s) -> {LEDGER}")
# -- the report ---------------------------------------------------------
rows = []
for b in sorted(used):
e = banks.get(b, {})
info = idx.get(b, {})
rows.append((b, e.get("status", "unknown"),
info.get("origin", "UNRESOLVED"),
info.get("files", 0), used[b]))
by_status: dict[str, int] = {}
for _, st, *_ in rows:
by_status[st] = by_status.get(st, 0) + 1
print(f"\n{spec['gig']} · {len(used)} distinct banks")
for st in STATUSES:
if by_status.get(st):
print(f" {st:<14} {by_status[st]:>3}")
unresolved = [r for r in rows if r[2] == "UNRESOLVED"]
if unresolved:
print(f" {'UNRESOLVED':<14} {len(unresolved):>3} (in the score, not on disk)")
# Sorted by LEVERAGE: how many tracks each verdict unblocks. Alphabetical
# makes a 41-item list look like 41 equal chores, when in fact five banks
# gate almost the whole record — answer those and the shape of the release
# changes. Keystone first.
open_rows = sorted((r for r in rows if r[1] not in RELEASABLE),
key=lambda r: (-len(r[4]), r[0]))
if a.show == "open":
show = open_rows
print(f"\nBanks still needing a verdict ({len(show)}), most leverage first:")
else:
show = sorted(rows, key=lambda r: (-len(r[4]), r[0]))
print(f"\nAll banks ({len(show)}):")
for b, st, origin, files, tracks in show:
print(f" {b:<22} {st:<13} {origin:<10} {files:>4} files "
f"{len(tracks)} track(s): {', '.join(tracks[:3])}"
+ (" …" if len(tracks) > 3 else ""))
# -- the per-track verdict, which is what gates the release -------------
blocked: dict[str, list[str]] = {}
for b, st, *_rest in rows:
if st in RELEASABLE:
continue
for t in used[b]:
blocked.setdefault(t, []).append(b)
print(f"\n{len(blocked)} track(s) contain an unadjudicated or third-party bank:")
for t, bs in sorted(blocked.items()):
print(f" {t[:38]:<38} {', '.join(sorted(bs))}")
clean = [t for t in {x for v in used.values() for x in v} if t not in blocked]
print(f"\n{len(clean)} track(s) clear for per-track release: "
+ (", ".join(sorted(clean)) if clean else "none yet"))
print("\nThe continuous mix is a separate question and a lower-risk one — "
"the long form has passed where isolated tracks were flagged.")
return 1 if blocked else 0
if __name__ == "__main__":
sys.exit(main())
{
"_comment": [
"Per-BANK rights verdicts. The machine enumerates; PLN adjudicates.",
"`origin` is measured (upstream = tracked in the Dirt-Samples git",
"checkout; local = added beside it, provenance unknown to the tool).",
"`status` is a human verdict: original | cleared | dirt_samples | third_party | unknown.",
"Only original, cleared, dirt_samples are releasable per-track.",
"A bank answered here is answered for every release that uses it."
],
"banks": {
"armora": {
"status": "dirt_samples",
"origin": "upstream",
"files": 7,
"note": "",
"decided": "2026-08-16"
},
"bd": {
"status": "dirt_samples",
"origin": "upstream",
"files": 24,
"note": "",
"decided": "2026-08-16"
},
"break": {
"status": "unknown",
"origin": "local",
"files": 33,
"note": "",
"decided": null
},
"breaks165": {
"status": "dirt_samples",
"origin": "upstream",
"files": 0,
"note": "",
"decided": "2026-08-16"
},
"bskick": {
"status": "unknown",
"origin": "local",
"files": 9,
"note": "",
"decided": null
},
"clubkick": {
"status": "dirt_samples",
"origin": "upstream",
"files": 5,
"note": "",
"decided": "2026-08-16"
},
"cp": {
"status": "dirt_samples",
"origin": "upstream",
"files": 2,
"note": "",
"decided": "2026-08-16"
},
"crimewave": {
"status": "unknown",
"origin": "local",
"files": 24,
"note": "",
"decided": null
},
"d": {
"status": "dirt_samples",
"origin": "upstream",
"files": 4,
"note": "",
"decided": "2026-08-16"
},
"daft": {
"status": "unknown",
"origin": "local",
"files": 5,
"note": "",
"decided": null
},
"desire": {
"status": "unknown",
"origin": "local",
"files": 13,
"note": "",
"decided": null
},
"diams_dj": {
"status": "unknown",
"origin": "local",
"files": 16,
"note": "",
"decided": null
},
"drum": {
"status": "dirt_samples",
"origin": "upstream",
"files": 6,
"note": "",
"decided": "2026-08-16"
},
"drums_nes": {
"status": "unknown",
"origin": "local",
"files": 30,
"note": "",
"decided": null
},
"drumtraks": {
"status": "dirt_samples",
"origin": "upstream",
"files": 13,
"note": "",
"decided": "2026-08-16"
},
"e": {
"status": "dirt_samples",
"origin": "upstream",
"files": 8,
"note": "",
"decided": "2026-08-16"
},
"fbreak120": {
"status": "unknown",
"origin": "local",
"files": 29,
"note": "",
"decided": null
},
"gfunk_bass": {
"status": "unknown",
"origin": "local",
"files": 8,
"note": "",
"decided": null
},
"gfunk_lead": {
"status": "unknown",
"origin": "local",
"files": 28,
"note": "",
"decided": null
},
"ghost": {
"status": "unknown",
"origin": "local",
"files": 7,
"note": "",
"decided": null
},
"giorgio_syn": {
"status": "unknown",
"origin": "local",
"files": 89,
"note": "",
"decided": null
},
"h": {
"status": "dirt_samples",
"origin": "upstream",
"files": 7,
"note": "",
"decided": "2026-08-16"
},
"h2ogmcp": {
"status": "unknown",
"origin": "local",
"files": 1,
"note": "",
"decided": null
},
"h2ogmcy": {
"status": "unknown",
"origin": "local",
"files": 25,
"note": "",
"decided": null
},
"h2ogmhh": {
"status": "unknown",
"origin": "local",
"files": 20,
"note": "",
"decided": null
},
"hardkick_rha": {
"status": "unknown",
"origin": "local",
"files": 100,
"note": "",
"decided": null
},
"hh": {
"status": "dirt_samples",
"origin": "upstream",
"files": 13,
"note": "",
"decided": "2026-08-16"
},
"jazz": {
"status": "dirt_samples",
"origin": "upstream",
"files": 8,
"note": "",
"decided": "2026-08-16"
},
"jbk_kick": {
"status": "unknown",
"origin": "local",
"files": 508,
"note": "",
"decided": null
},
"jungle_breaks": {
"status": "unknown",
"origin": "local",
"files": 224,
"note": "",
"decided": null
},
"kick": {
"status": "unknown",
"origin": "local",
"files": 21,
"note": "",
"decided": null
},
"like_sugar": {
"status": "unknown",
"origin": "local",
"files": 22,
"note": "",
"decided": null
},
"meth_bass": {
"status": "unknown",
"origin": "local",
"files": 28,
"note": "",
"decided": null
},
"molotov": {
"status": "unknown",
"origin": "local",
"files": 9,
"note": "",
"decided": null
},
"moog": {
"status": "dirt_samples",
"origin": "upstream",
"files": 7,
"note": "",
"decided": "2026-08-16"
},
"movie_paris": {
"status": "unknown",
"origin": "local",
"files": 22,
"note": "",
"decided": null
},
"movie_wolf": {
"status": "unknown",
"origin": "local",
"files": 88,
"note": "",
"decided": null
},
"no_sunshine": {
"status": "unknown",
"origin": "local",
"files": 32,
"note": "",
"decided": null
},
"oil": {
"status": "unknown",
"origin": "local",
"files": 9,
"note": "",
"decided": null
},
"org_jungle_breaks": {
"status": "unknown",
"origin": "local",
"files": 582,
"note": "",
"decided": null
},
"perfect": {
"status": "unknown",
"origin": "local",
"files": 22,
"note": "",
"decided": null
},
"ramples34": {
"status": "unknown",
"origin": "local",
"files": 8,
"note": "",
"decided": null
},
"ramples37": {
"status": "unknown",
"origin": "local",
"files": 12,
"note": "",
"decided": null
},
"realclaps": {
"status": "dirt_samples",
"origin": "upstream",
"files": 4,
"note": "",
"decided": "2026-08-16"
},
"rhadamanthe_vocal": {
"status": "unknown",
"origin": "local",
"files": 77,
"note": "",
"decided": null
},
"risers": {
"status": "unknown",
"origin": "local",
"files": 24,
"note": "",
"decided": null
},
"sd": {
"status": "dirt_samples",
"origin": "upstream",
"files": 2,
"note": "",
"decided": "2026-08-16"
},
"sine": {
"status": "dirt_samples",
"origin": "upstream",
"files": 6,
"note": "",
"decided": "2026-08-16"
},
"snare": {
"status": "unknown",
"origin": "local",
"files": 90,
"note": "",
"decided": null
},
"speedupdown": {
"status": "dirt_samples",
"origin": "upstream",
"files": 9,
"note": "",
"decided": "2026-08-16"
},
"take5": {
"status": "unknown",
"origin": "local",
"files": 27,
"note": "",
"decided": null
},
"techno": {
"status": "dirt_samples",
"origin": "upstream",
"files": 7,
"note": "",
"decided": "2026-08-16"
},
"the_revolution": {
"status": "unknown",
"origin": "local",
"files": 36,
"note": "",
"decided": null
},
"trance_loops": {
"status": "unknown",
"origin": "local",
"files": 15,
"note": "",
"decided": null
},
"v": {
"status": "dirt_samples",
"origin": "upstream",
"files": 6,
"note": "",
"decided": "2026-08-16"
},
"vec1_acid": {
"status": "unknown",
"origin": "local",
"files": 72,
"note": "",
"decided": null
},
"vec1_claps": {
"status": "unknown",
"origin": "local",
"files": 120,
"note": "",
"decided": null
},
"vec1_snare": {
"status": "unknown",
"origin": "local",
"files": 100,
"note": "",
"decided": null
},
"vocal_bordel": {
"status": "unknown",
"origin": "local",
"files": 6,
"note": "",
"decided": null
},
"wap": {
"status": "unknown",
"origin": "local",
"files": 32,
"note": "",
"decided": null
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment