← Files Marketing CouncilARCHIVED FILE
scripts/validate_neural_graph.py
3.01 KB · Oct 2, 2026 · 00:31 UTC
#!/usr/bin/env python3
"""Validate Marketing Council neural graph integrity without third-party dependencies."""
from __future__ import annotations
import argparse, json
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ALLOWED = {"belongs_to","informs","operationalizes","activates","routes_to","challenges","counterbalances","requires","measured_by","hands_off_to"}
def validate(root: Path) -> dict:
errors=[]; warnings=[]
path=root/'neural'/'graph.json'
if not path.is_file():
return {'valid':False,'errors':['missing neural/graph.json'],'warnings':[]}
graph=json.loads(path.read_text(encoding='utf-8'))
registry_path=root/'references'/'sources.yml'
registry_text=registry_path.read_text(encoding='utf-8') if registry_path.is_file() else ''
import re
registered_sources=set(re.findall(r'^ ([a-z0-9-]+):$', registry_text, flags=re.M))
nodes=graph.get('nodes',[]); edges=graph.get('edges',[])
ids=[n.get('id') for n in nodes]
if len(ids)!=len(set(ids)): errors.append('duplicate node ids')
known=set(ids)
for n in nodes:
if not n.get('id') or not n.get('type') or not n.get('label'): errors.append(f'invalid node: {n}')
if n.get('path') and not (root/n['path']).is_file(): errors.append(f"missing node path: {n['id']} -> {n['path']}")
outgoing=defaultdict(list)
for e in edges:
if e.get('from') not in known or e.get('to') not in known: errors.append(f'dangling edge: {e}')
if e.get('relation') not in ALLOWED: errors.append(f'unknown relation: {e}')
outgoing[e.get('from')].append(e)
for n in nodes:
if n.get('type') in {'figure','theory'} and not outgoing[n['id']]: errors.append(f"unconnected {n['type']}: {n['id']}")
if n.get('type') == 'evidence':
if not str(n.get('as_of','')).startswith('2026-'): errors.append(f"evidence node missing 2026 as_of: {n['id']}")
if not n.get('source_ids'): errors.append(f"evidence node missing source_ids: {n['id']}")
unknown=set(n.get('source_ids',[]))-registered_sources
if unknown: errors.append(f"evidence node has unknown source_ids: {n['id']} -> {sorted(unknown)}")
counts=Counter(n.get('type') for n in nodes)
for t,min_count in {'figure':24,'school':19,'principle':44,'theory':46,'signal':44,'agent':24,'skill':29,'hook':24,'evidence':6}.items():
if counts[t] < min_count: errors.append(f'{t} count {counts[t]} < {min_count}')
return {'valid':not errors,'counts':dict(counts),'edge_count':len(edges),'errors':errors,'warnings':warnings}
def main():
p=argparse.ArgumentParser(); p.add_argument('root',nargs='?',type=Path,default=ROOT); p.add_argument('--json',action='store_true'); a=p.parse_args()
r=validate(a.root.resolve())
print(json.dumps(r,indent=2,sort_keys=True) if a.json else f"valid={r['valid']} nodes={sum(r.get('counts',{}).values())} edges={r.get('edge_count',0)}")
raise SystemExit(0 if r['valid'] else 1)
if __name__=='__main__': main()
SHA-256: 10b5e105b5ab6add9b8df12c1779c1c026be7741d4ff1207f8229136c5d76660