130 lines
6.5 KiB
Python
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))
|