Files
skills/orchestrator/kvb_orchestrator/tasks.py
T

167 lines
6.5 KiB
Python
Raw Normal View History

from __future__ import annotations
import itertools
import statistics
from .council import agent
from .records import iso_epoch, shown
from .register import ENDED, OPERATOR, parties, register_row, status_of
from .signed import signing
from .tree import inside
ENDED_SHOWN = 100
OPEN_SHOWN = 300
QUEUE_SHOWN = 100
PARTIES_SHOWN = 40
TEXT_SHOWN = 500
FIBONACCI = (1, 2, 3, 5, 8, 13, 21, 34, 55, 89)
LETTERS = "RASCI"
HOTFIX = "hotfix/"
QUEUE = "queue/"
def kind_of(task: str) -> str:
if task.startswith(OPERATOR):
return "command"
return "hotfix" if task.startswith(HOTFIX) else "task"
def editable(task: str, row: dict) -> bool:
shown_whole = len(task) <= TEXT_SHOWN and signing.TASK.fullmatch(task) is not None
return shown_whole and kind_of(task) == "task" and status_of(row) not in ENDED
def fibonacci(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool) and value in FIBONACCI
def estimates_of(row: dict) -> dict[str, dict]:
raw = row.get("estimates")
found = {}
for party, entry in itertools.islice(raw.items(), PARTIES_SHOWN) if isinstance(raw, dict) else ():
if isinstance(entry, dict) and fibonacci(entry.get("value")) and fibonacci(entry.get("complexity")):
found[str(party)[:TEXT_SHOWN]] = {"value": entry["value"], "complexity": entry["complexity"]}
return found
def wsjf(estimates: dict[str, dict]) -> dict | None:
"""Returns:
Median business value, median complexity and their quotient, the
same reading as rasci.py's wsjf(); None without an estimate.
"""
if not estimates:
return None
value = statistics.median(entry["value"] for entry in estimates.values())
complexity = statistics.median(entry["complexity"] for entry in estimates.values())
return {"value": value, "complexity": complexity, "score": round(value / complexity, 2)}
def prio_of(row: dict) -> int | None:
prio = row.get("prio")
return prio if isinstance(prio, int) and not isinstance(prio, bool) else None
def roles_of(row: dict) -> dict[str, str]:
letters = {party[:TEXT_SHOWN]: "".join(letter for letter in LETTERS if letter in held) for party, held in itertools.islice(parties(row).items(), PARTIES_SHOWN)}
return {party: held for party, held in letters.items() if held}
def worktrees(scan: dict) -> list[dict]:
found = []
for repo in scan["repos"]:
trees = [(entry["name"], tree) for entry in repo["branches"] for tree in entry["worktrees"]] + [("", tree) for tree in repo["detached"]]
found += [{"path": tree["path"], "branch": name, "repo": repo["name"], "flow": tree["flow"], "open_work": tree["open_work"]} for name, tree in trees]
return found
def placed(row: dict, trees: list[dict]) -> dict | None:
"""Returns:
The worktree a register row or queue entry works in: the innermost
one holding its workdir, else the only one on its branch.
"""
workdir, name = row.get("workdir"), row.get("branch")
if isinstance(workdir, str) and workdir.startswith("/"):
holding = [tree for tree in trees if inside(workdir, tree["path"])]
if holding:
return max(holding, key=lambda tree: len(tree["path"]))
named = [tree for tree in trees if isinstance(name, str) and name and tree["branch"] == name]
return named[0] if len(named) == 1 else None
def located(row: dict, trees: list[dict], open_task: bool) -> dict:
tree = placed(row, trees)
own = ["task"] if open_task else []
if tree is None:
return {"worktree": "", "worktree_branch": "", "repo": "", "flow": None, "open_work": own}
return {"worktree": tree["path"], "worktree_branch": tree["branch"], "repo": tree["repo"], "flow": tree["flow"], "open_work": [*own, *tree["open_work"]]}
def task_row(task: str, row: dict, trees: list[dict]) -> dict:
estimates = estimates_of(row)
ended = status_of(row) in ENDED
shown_row = {key: value[:TEXT_SHOWN] if isinstance(value, str) else value for key, value in register_row(task, row).items()}
return {
**shown_row,
"kind": kind_of(task),
"ended": ended,
"editable": editable(task, row),
"prio": prio_of(row),
"estimates": estimates,
"wsjf": wsjf(estimates),
"roles": roles_of(row),
"queue": None,
**located(row, trees, not ended),
}
def queue_row(entry: dict, created: float, trees: list[dict]) -> dict:
text = {name: shown(entry.get(name))[:TEXT_SHOWN] for name in ("id", "cmd", "kind", "task", "workdir")}
return {
"task": f"{QUEUE}{text['id']}",
"title": text["cmd"],
"status": "running" if entry.get("status") == "active" else "awaiting",
"worker": agent(entry.get("name"))[:TEXT_SHOWN] or "-",
"responsible": "-",
"accountable": "-",
"objective": "-",
"branch": "-",
"workdir": text["workdir"],
"met": 0,
"total": 0,
"kind": "queue",
"ended": False,
"editable": False,
"prio": None,
"estimates": {},
"wsjf": None,
"roles": {},
"queue": {"kind": text["kind"], "parent": text["task"], "enqueued": created},
**located(entry, trees, False),
}
def rank(task: str, row: dict) -> tuple:
prio, score = prio_of(row), wsjf(estimates_of(row))
return (prio is None, prio or 0, score is None, -(score["score"] if score else 0), task)
def task_rows(tasks: dict[str, dict], queue: list[tuple[dict, float]], scan: dict) -> list[dict]:
"""Args:
tasks: the register rows by task.
queue: the kvb-org-queue entries with their enqueue time.
scan: the pkgmgr tree as read_tree() maps it.
Returns:
The OPEN_SHOWN first open tasks in register rank (pin, then WSJF,
then name), the QUEUE_SHOWN oldest queue jobs, then the ENDED_SHOWN
most recently ended tasks; each with its worktree's flow and open work.
"""
trees = worktrees(scan)
current = sorted((task for task, row in tasks.items() if status_of(row) not in ENDED), key=lambda task: rank(task, tasks[task]))
ended = sorted((task for task, row in tasks.items() if status_of(row) in ENDED), key=lambda task: (-(iso_epoch(tasks[task].get("end")) or 0), task))
jobs = sorted(queue, key=lambda pair: pair[1])[:QUEUE_SHOWN]
return [
*(task_row(task, tasks[task], trees) for task in current[:OPEN_SHOWN]),
*(queue_row(entry, created, trees) for entry, created in jobs),
*(task_row(task, tasks[task], trees) for task in ended[:ENDED_SHOWN]),
]