from pathlib import Path
import re, yaml
base=Path('/opt/struktur/social-media-radar')
doc=yaml.safe_load((base/'architecture/data-ownership.yaml').read_text())
policy=set()
assigned=[]
for group in doc['field_groups'].values():
policy.update(group.get('known_writers',[]) or [])
if group.get('status')=='ASSIGNED': assigned.extend(group.get('known_writers',[]) or [])
inv=(base/'architecture/writer-inventory.md').read_text()
# Explicit backtick paths and the inline shell writer named in the inventory.
expected=set(re.findall(r'`([^`]+\.(?:py|sh)(?::inline_backfill)?)`',inv))
# Include bullet paths that are not fenced individually.
for line in inv.splitlines():
for m in re.findall(r'(?:social-media-(?:agent|radar)/[^ ,`()]+(?:\.py|\.sh)(?::inline_backfill)?)',line): expected.add(m)
print('POLICY_WRITERS',len(policy))
print('INVENTORY_WRITERS_PARSED',len(expected))
print('MISSING_FROM_POLICY',sorted(expected-policy))
print('POLICY_NOT_IN_INVENTORY',sorted(policy-expected))
print('ASSIGNED_WRITERS',assigned)
# all listed writers must remain denied while statuses are audit/open
for writer in sorted(policy):
for group_name,group in doc['field_groups'].items():
fields=group.get('examples') or []
if fields and group.get('status')=='ASSIGNED': raise SystemExit('UNEXPECTED_ASSIGNED '+writer+' '+group_name)
print('ALL_POLICY_GROUPS_NON_AUTHORIZING',True)