#!/usr/bin/env python3 """library_drop_classify.py Re-derive the library-drop triage roster from drops/*/MANIFEST.md. Deterministic; used by _check_library_drop_roster.sh to diff against the committed library-drop-roster.json. Exits 0 only if the committed roster matches the source manifests. """ import json import os import re import sys BASE = '/root/stackchain-library-updates/drops' def classify(txt): # Ordered, low-level rules first. if 'HTTP 402' in txt or 'Insufficient Balance' in txt: return 'blocked', 'HTTP 402 / Insufficient Balance' if 'prepared and delivered to Telegram review' in txt and 'not posted' in txt.lower(): return 'prepared_not_posted', 'prepared_and_delivered + not_posted' if ('**PUBLISHED**' in txt) or ('**PUBLISHED.**' in txt) or ('PUBLISHED by Alexander' in txt) \ or re.search(r'PUBLISHED\.\*\*\s+Posted', txt): return 'published', 'Status: PUBLISHED / Published by Alexander' if ('No public change' in txt) or ('No episode' in txt) or ('NOT PUBLISHED' in txt) \ or ('Nothing on the site' in txt) or ('not published' in txt.lower()) \ or ('unpublished' in txt.lower()) or ('public change' in txt.lower() and 'none' in txt.lower()): return 'no_episode', 'no-public-change / not-unpublished' return 'unclassified', 'unclassified' def main(): dirs = sorted([d for d in os.listdir(BASE) if d.startswith('2026-') and os.path.isdir(os.path.join(BASE, d))]) rows = [] for d in dirs: txt = open(os.path.join(BASE, d, 'MANIFEST.md'), encoding='utf-8', errors='replace').read() bucket, matched = classify(txt) rows.append({'dir': d, 'bucket': bucket, 'matched': matched}) json.dump(rows, sys.stdout, indent=1) if __name__ == '__main__': main()