the-custodian/docs/evidence/2026-10-03-dependency-graph-triage/graph_hub_view.py
codex de4593c386
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
CUST-WP-0075: dependency-graph triage evidence and monitor workplan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 03:06:28 +02:00

130 lines
6.5 KiB
Python

import json,subprocess,collections,datetime,sys
import os; S=os.environ.get('TRIAGE_SCRATCH','.')
L=lambda f: (lambda d: d if isinstance(d,list) else d.get('items',d))(json.load(open(f'{S}/{f}')))
wps=L('wps.json'); repos=L('repos.json')
repo={r['id']:r for r in repos}
WP={w['id']:w for w in wps}
tasks={}
for f in ['waits.json','tasks_todo.json','tasks_progress.json']:
for t in L(f): tasks[t['id']]=t
deps=json.load(open(f'{S}/deps.json'))
edges={}
for v in deps.values():
for e in v: edges[e['id']]=e
# resolve unknown target tasks
need=set(e['to_task_id'] for e in edges.values() if e['to_task_id'] and e['to_task_id'] not in tasks)
need|=set(e['from_task_id'] for e in edges.values() if e['from_task_id'] and e['from_task_id'] not in tasks)
cache={}
try: cache=json.load(open(f'{S}/taskcache.json'))
except: pass
for tid in need:
if tid in cache: tasks[tid]=cache[tid]; continue
d=subprocess.run(['curl','-s',f'http://127.0.0.1:8000/tasks/{tid}'],capture_output=True,text=True).stdout
try: t=json.loads(d); cache[tid]=t; tasks[tid]=t
except: pass
json.dump(cache,open(f'{S}/taskcache.json','w'))
TERM_T={'done','cancel'}; TERM_W={'finished','archived'}
def wslug(i): return WP[i]['slug'].upper() if i in WP else i
def wstat(i): return WP[i]['status'] if i in WP else '?'
def trec(i): return (tasks[i].get('record_id') or tasks[i].get('title','?')[:30]) if i in tasks else i
def tstat(i): return tasks[i]['status'] if i in tasks else '?'
def wrepo(i):
w=WP.get(i); r=repo.get(w['repo_id']) if w else None; return r['slug'] if r else '?'
# dependent -> blockers
dep_of=collections.defaultdict(list) # key: ('t',id) or ('w',id) -> list of target keys
for e in edges.values():
src=('t',e['from_task_id']) if e['from_task_id'] else ('w',e['from_workplan_id'])
dst=('t',e['to_task_id']) if e['to_task_id'] else ('w',e['to_workplan_id'])
dep_of[src].append(dst)
def terminal(k):
return tstat(k[1]) in TERM_T if k[0]=='t' else wstat(k[1]) in TERM_W
def name(k): return trec(k[1]) if k[0]=='t' else wslug(k[1])
def status(k): return tstat(k[1]) if k[0]=='t' else wstat(k[1])
today=datetime.date.today()
print("=== A. wait tasks whose every dependency target is terminal (clearable now) ===")
A=[]
for tid,t in tasks.items():
if t['status']!='wait': continue
tg=dep_of.get(('t',tid),[])
reasons=[]
if tg and all(terminal(k) for k in tg): reasons.append('deps terminal: '+', '.join(name(k) for k in tg))
if t.get('wait_until') and datetime.date.fromisoformat(str(t['wait_until'])[:10])<today: reasons.append(f"wait_until {t['wait_until']} passed")
if reasons:
A.append(t); print(f"{str(t.get('record_id') or t.get('title'))[:28]:28} wp={wslug(t['workplan_id'])} repo={wrepo(t['workplan_id'])} human={t.get('needs_human')} | {'; '.join(reasons)}")
print(len(A))
print("\n=== B. blocked workplans with no open wait task ===")
openw=[w for w in wps if w['status'] in ('blocked','active','proposed','backlog','ready')]
bywp=collections.defaultdict(list)
for t in tasks.values(): bywp[t['workplan_id']].append(t)
for w in openw:
if w['status']!='blocked': continue
ts=bywp.get(w['id'],[])
if not any(t['status']=='wait' for t in ts):
print(f"{w['slug'].upper():30} repo={wrepo(w['id'])} flavor={w.get('flavor')} open={[ (t['record_id'],t['status']) for t in ts]}")
print("\n=== C. unqualified waits (no deps, no human, no blocked_on, no wait_until) in open workplans ===")
for t in tasks.values():
if t['status']!='wait': continue
if not dep_of.get(('t',t['id'])) and not t.get('needs_human') and not t.get('blocked_on') and not t.get('wait_until'):
w=WP.get(t['workplan_id'],{})
print(f"{str(t.get('record_id') or t.get('title'))[:28]:28} wp={wslug(t['workplan_id'])} flavor={w.get('flavor')} repo={wrepo(t['workplan_id'])} reason={str(t.get('blocking_reason'))[:90]}")
print("\n=== D. owner-return waits (blocked_on) ===")
for t in tasks.values():
if t['status']=='wait' and t.get('blocked_on'):
print(f"{str(t.get('record_id') or t.get('title'))[:28]:28} wp={wslug(t['workplan_id'])} repo={wrepo(t['workplan_id'])} on={t['blocked_on']} human={t.get('needs_human')} upd={t['updated_at'][:10]}")
# transitive fan-in: for each node, count distinct open workplans that transitively depend on it
rev=collections.defaultdict(set)
for src,dsts in dep_of.items():
for d in dsts: rev[d].add(src)
def wp_of(k): return k[1] if k[0]=='w' else tasks.get(k[1],{}).get('workplan_id')
def fanin(k):
seen=set(); stack=[k]; wpset=set()
while stack:
n=stack.pop()
for p in rev.get(n,()):
if p in seen: continue
seen.add(p); stack.append(p)
w=wp_of(p)
if w and w!=wp_of(k) and wstat(w) not in TERM_W: wpset.add(w)
# also: tasks within the same workplan propagate to the workplan node
if n[0]=='t':
w=wp_of(n)
if w and ('w',w) not in seen:
seen.add(('w',w)); stack.append(('w',w))
return wpset
print("\n=== E. gate roots: open, non-terminal nodes with no open blockers, ranked by transitive open-workplan fan-in ===")
nodes=set(dep_of)|set(k for v in dep_of.values() for k in v)
roots=[]
for k in nodes:
if terminal(k): continue
blockers=[d for d in dep_of.get(k,[]) if not terminal(d)]
if blockers: continue
f=fanin(k)
if f: roots.append((len(f),k))
roots.sort(reverse=True)
for n,k in roots[:40]:
extra=''
if k[0]=='t':
t=tasks.get(k[1],{}); extra=f"status={t.get('status')} human={t.get('needs_human')} on={t.get('blocked_on')} dec={t.get('decision_id')} | {str(t.get('blocking_reason') or t.get('title'))[:80]}"
else: extra=f"status={wstat(k[1])} repo={wrepo(k[1])}"
print(f"{n:3} {name(k):30} {extra}")
print("\n=== F. cycles ===")
color={}
def dfs(n,path):
color[n]=1; path.append(n)
for d in dep_of.get(n,[]):
if terminal(d): continue
if color.get(d)==1:
cyc=path[path.index(d):]+[d]; print(' -> '.join(name(x) for x in cyc))
elif d not in color: dfs(d,path)
path.pop(); color[n]=2
for n in list(dep_of):
if n not in color and not terminal(n): dfs(n,[])
print("\n=== G. human gates ranked by fan-in ===")
H=[]
for t in tasks.values():
if t['status']=='wait' and t.get('needs_human'):
H.append((len(fanin(('t',t['id']))),t))
H.sort(key=lambda x:-x[0])
for n,t in H:
print(f"{n:3} {str(t.get('record_id') or t.get('title'))[:28]:28} repo={wrepo(t['workplan_id']):22} dec={t.get('decision_id')} | {str(t.get('blocking_reason') or t.get('title'))[:100]}")
print(len(H))