362 lines
13 KiB
Python
362 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Fleet progression evaluator for the Paperclips-inspired infrastructure epic.
|
|
|
|
Refs: timmy-home #547
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from urllib import request
|
|
from typing import Any
|
|
|
|
DEFAULT_BASE_URL = "https://forge.alexanderwhitestone.com/api/v1"
|
|
DEFAULT_OWNER = "Timmy_Foundation"
|
|
DEFAULT_REPO = "timmy-home"
|
|
DEFAULT_TOKEN_FILE = Path.home() / ".config" / "gitea" / "token"
|
|
DEFAULT_SPEC_FILE = Path(__file__).resolve().parent.parent / "configs" / "fleet_progression.json"
|
|
DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
DEFAULT_RESOURCES = {
|
|
"uptime_percent_30d": 0.0,
|
|
"capacity_utilization": 0.0,
|
|
"innovation": 0.0,
|
|
"all_models_local": False,
|
|
"sovereign_stable_days": 0,
|
|
"human_free_days": 0,
|
|
}
|
|
|
|
|
|
class GiteaClient:
|
|
def __init__(self, token: str, owner: str = DEFAULT_OWNER, repo: str = DEFAULT_REPO, base_url: str = DEFAULT_BASE_URL):
|
|
self.token = token
|
|
self.owner = owner
|
|
self.repo = repo
|
|
self.base_url = base_url.rstrip("/")
|
|
|
|
def get_issue(self, issue_number: int):
|
|
headers = {"Authorization": f"token {self.token}"}
|
|
req = request.Request(
|
|
f"{self.base_url}/repos/{self.owner}/{self.repo}/issues/{issue_number}",
|
|
headers=headers,
|
|
)
|
|
with request.urlopen(req, timeout=30) as resp:
|
|
return json.loads(resp.read().decode())
|
|
|
|
|
|
def load_spec(path: Path | None = None):
|
|
target = path or DEFAULT_SPEC_FILE
|
|
return json.loads(target.read_text())
|
|
|
|
|
|
def load_issue_states(spec: dict[str, Any], token_file: Path = DEFAULT_TOKEN_FILE):
|
|
if not token_file.exists():
|
|
raise FileNotFoundError(f"Token file not found: {token_file}")
|
|
|
|
token = token_file.read_text().strip()
|
|
client = GiteaClient(token=token)
|
|
issue_states = {}
|
|
for phase in spec["phases"]:
|
|
issue = client.get_issue(phase["issue_number"])
|
|
issue_states[phase["issue_number"]] = issue["state"]
|
|
return issue_states
|
|
|
|
|
|
def _evaluate_rule(rule: dict[str, Any], issue_states: dict[int, str], resources: dict[str, Any]):
|
|
rule_type = rule["type"]
|
|
rule_id = rule["id"]
|
|
|
|
if rule_type == "always":
|
|
return {"rule": rule_id, "passed": True, "actual": True, "expected": True}
|
|
|
|
if rule_type == "issue_closed":
|
|
issue_number = int(rule["issue"])
|
|
actual = str(issue_states.get(issue_number, "open"))
|
|
return {
|
|
"rule": rule_id,
|
|
"passed": actual == "closed",
|
|
"actual": actual,
|
|
"expected": "closed",
|
|
}
|
|
|
|
if rule_type == "resource_gte":
|
|
resource = rule["resource"]
|
|
actual = resources.get(resource, 0)
|
|
expected = rule["value"]
|
|
return {
|
|
"rule": rule_id,
|
|
"passed": actual >= expected,
|
|
"actual": actual,
|
|
"expected": f">={expected}",
|
|
}
|
|
|
|
if rule_type == "resource_gt":
|
|
resource = rule["resource"]
|
|
actual = resources.get(resource, 0)
|
|
expected = rule["value"]
|
|
return {
|
|
"rule": rule_id,
|
|
"passed": actual > expected,
|
|
"actual": actual,
|
|
"expected": f">{expected}",
|
|
}
|
|
|
|
if rule_type == "resource_true":
|
|
resource = rule["resource"]
|
|
actual = bool(resources.get(resource, False))
|
|
return {
|
|
"rule": rule_id,
|
|
"passed": actual is True,
|
|
"actual": actual,
|
|
"expected": True,
|
|
}
|
|
|
|
raise ValueError(f"Unsupported rule type: {rule_type}")
|
|
|
|
|
|
def _collect_repo_evidence(phase: dict[str, Any], repo_root: Path):
|
|
present = []
|
|
missing = []
|
|
for entry in phase.get("repo_evidence", []):
|
|
path = entry["path"]
|
|
description = entry.get("description", "")
|
|
label = f"`{path}` — {description}" if description else f"`{path}`"
|
|
if (repo_root / path).exists():
|
|
present.append(label)
|
|
else:
|
|
missing.append(label)
|
|
return present, missing
|
|
|
|
|
|
def _phase_status_label(phase_result: dict[str, Any]) -> str:
|
|
if phase_result["completed"]:
|
|
return "COMPLETE"
|
|
if phase_result["available_to_work"]:
|
|
return "ACTIVE"
|
|
return "LOCKED"
|
|
|
|
|
|
def evaluate_progression(
|
|
spec: dict[str, Any],
|
|
issue_states: dict[int, str],
|
|
resources: dict[str, Any] | None = None,
|
|
repo_root: Path | None = None,
|
|
):
|
|
merged_resources = {**DEFAULT_RESOURCES, **(resources or {})}
|
|
repo_root = repo_root or DEFAULT_REPO_ROOT
|
|
phase_results = []
|
|
|
|
for phase in spec["phases"]:
|
|
issue_number = phase["issue_number"]
|
|
issue_state = str(issue_states.get(issue_number, "open"))
|
|
completed = issue_state == "closed"
|
|
rule_results = [
|
|
_evaluate_rule(rule, issue_states, merged_resources)
|
|
for rule in phase.get("unlock_rules", [])
|
|
]
|
|
blocking = [item for item in rule_results if not item["passed"]]
|
|
unlocked = not blocking
|
|
repo_evidence_present, repo_evidence_missing = _collect_repo_evidence(phase, repo_root)
|
|
phase_result = {
|
|
"number": phase["number"],
|
|
"issue_number": issue_number,
|
|
"issue_state": issue_state,
|
|
"key": phase["key"],
|
|
"name": phase["name"],
|
|
"summary": phase["summary"],
|
|
"completed": completed,
|
|
"unlocked": unlocked,
|
|
"available_to_work": unlocked and not completed,
|
|
"passed_requirements": [item for item in rule_results if item["passed"]],
|
|
"blocking_requirements": blocking,
|
|
"repo_evidence_present": repo_evidence_present,
|
|
"repo_evidence_missing": repo_evidence_missing,
|
|
}
|
|
phase_result["status"] = _phase_status_label(phase_result)
|
|
phase_results.append(phase_result)
|
|
|
|
unlocked_phases = [phase for phase in phase_results if phase["unlocked"]]
|
|
current_phase = unlocked_phases[-1] if unlocked_phases else phase_results[0]
|
|
next_locked_phase = next((phase for phase in phase_results if not phase["unlocked"]), None)
|
|
epic_complete = all(phase["completed"] for phase in phase_results) and phase_results[-1]["unlocked"]
|
|
|
|
return {
|
|
"epic_issue": spec["epic_issue"],
|
|
"epic_title": spec["epic_title"],
|
|
"resources": merged_resources,
|
|
"issue_states": {str(k): v for k, v in issue_states.items()},
|
|
"phases": phase_results,
|
|
"current_phase": current_phase,
|
|
"next_locked_phase": next_locked_phase,
|
|
"epic_complete": epic_complete,
|
|
}
|
|
|
|
|
|
def render_markdown(result: dict[str, Any]) -> str:
|
|
current_phase = result["current_phase"]
|
|
next_locked_phase = result["next_locked_phase"]
|
|
resources = result["resources"]
|
|
|
|
lines = [
|
|
f"# [FLEET-EPIC] {result['epic_title']}",
|
|
"",
|
|
"This report grounds the fleet epic in executable state: live issue gates, current resource inputs, and repo evidence for each phase.",
|
|
"",
|
|
"## Current Phase",
|
|
"",
|
|
f"- Current unlocked phase: {current_phase['number']} — {current_phase['name']}",
|
|
f"- Current phase status: {current_phase['status']}",
|
|
f"- Epic complete: {'yes' if result['epic_complete'] else 'no'}",
|
|
]
|
|
|
|
if next_locked_phase:
|
|
lines.append(f"- Next locked phase: {next_locked_phase['number']} — {next_locked_phase['name']}")
|
|
else:
|
|
lines.append("- Next locked phase: none")
|
|
|
|
lines.extend([
|
|
"",
|
|
"## Resource Snapshot",
|
|
"",
|
|
f"- Uptime (30d): {resources['uptime_percent_30d']}",
|
|
f"- Capacity utilization: {resources['capacity_utilization']}",
|
|
f"- Innovation: {resources['innovation']}",
|
|
f"- All models local: {resources['all_models_local']}",
|
|
f"- Sovereign stable days: {resources['sovereign_stable_days']}",
|
|
f"- Human-free days: {resources['human_free_days']}",
|
|
"",
|
|
"## Phase Matrix",
|
|
"",
|
|
])
|
|
|
|
for phase in result["phases"]:
|
|
lines.extend([
|
|
f"### Phase {phase['number']} — {phase['name']}",
|
|
"",
|
|
f"- Issue: #{phase['issue_number']} ({phase['issue_state']})",
|
|
f"- Status: {phase['status']}",
|
|
f"- Summary: {phase['summary']}",
|
|
])
|
|
|
|
if phase["repo_evidence_present"]:
|
|
lines.append("- Repo evidence present:")
|
|
lines.extend(f" - {item}" for item in phase["repo_evidence_present"])
|
|
if phase["repo_evidence_missing"]:
|
|
lines.append("- Repo evidence missing:")
|
|
lines.extend(f" - {item}" for item in phase["repo_evidence_missing"])
|
|
if phase["blocking_requirements"]:
|
|
lines.append("- Blockers:")
|
|
for blocker in phase["blocking_requirements"]:
|
|
lines.append(
|
|
f" - blocked by `{blocker['rule']}`: actual={blocker['actual']} expected={blocker['expected']}"
|
|
)
|
|
else:
|
|
lines.append("- Blockers: none")
|
|
lines.append("")
|
|
|
|
lines.extend([
|
|
"## Why This Epic Remains Open",
|
|
"",
|
|
"- The progression manifest and evaluator exist, but multiple child phases are still open or only partially implemented.",
|
|
"- Several child lanes already have active PRs; this report is the parent-level grounding slice that keeps the epic honest without duplicating those lanes.",
|
|
"- This epic only closes when the child phase gates are actually satisfied in code and in live operation.",
|
|
])
|
|
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Evaluate current fleet progression against the Paperclips-inspired epic.")
|
|
parser.add_argument("--spec-file", type=Path, default=DEFAULT_SPEC_FILE)
|
|
parser.add_argument("--token-file", type=Path, default=DEFAULT_TOKEN_FILE)
|
|
parser.add_argument("--issue-state-file", type=Path, help="Optional JSON file of issue_number -> state overrides")
|
|
parser.add_argument("--resource-file", type=Path, help="Optional JSON file with resource values")
|
|
parser.add_argument("--uptime-percent-30d", type=float)
|
|
parser.add_argument("--capacity-utilization", type=float)
|
|
parser.add_argument("--innovation", type=float)
|
|
parser.add_argument("--all-models-local", action="store_true")
|
|
parser.add_argument("--sovereign-stable-days", type=int)
|
|
parser.add_argument("--human-free-days", type=int)
|
|
parser.add_argument("--json", action="store_true")
|
|
parser.add_argument("--markdown", action="store_true", help="Render a markdown report instead of the terse CLI summary")
|
|
parser.add_argument("--output", type=Path, help="Optional file path for markdown output")
|
|
return parser.parse_args()
|
|
|
|
|
|
def _load_resources(args):
|
|
resources = dict(DEFAULT_RESOURCES)
|
|
if args.resource_file:
|
|
resources.update(json.loads(args.resource_file.read_text()))
|
|
|
|
overrides = {
|
|
"uptime_percent_30d": args.uptime_percent_30d,
|
|
"capacity_utilization": args.capacity_utilization,
|
|
"innovation": args.innovation,
|
|
"sovereign_stable_days": args.sovereign_stable_days,
|
|
"human_free_days": args.human_free_days,
|
|
}
|
|
for key, value in overrides.items():
|
|
if value is not None:
|
|
resources[key] = value
|
|
if args.all_models_local:
|
|
resources["all_models_local"] = True
|
|
return resources
|
|
|
|
|
|
def _load_issue_states(args, spec):
|
|
if args.issue_state_file:
|
|
raw = json.loads(args.issue_state_file.read_text())
|
|
return {int(k): v for k, v in raw.items()}
|
|
return load_issue_states(spec, token_file=args.token_file)
|
|
|
|
|
|
def _render_cli_summary(result: dict[str, Any]) -> str:
|
|
lines = [
|
|
"--- Fleet Progression Evaluator ---",
|
|
f"Epic #{result['epic_issue']}: {result['epic_title']}",
|
|
f"Current phase: {result['current_phase']['number']} — {result['current_phase']['name']}",
|
|
f"Epic complete: {result['epic_complete']}",
|
|
]
|
|
if result["next_locked_phase"]:
|
|
lines.append(
|
|
f"Next locked phase: {result['next_locked_phase']['number']} — {result['next_locked_phase']['name']}"
|
|
)
|
|
lines.append("")
|
|
for phase in result["phases"]:
|
|
lines.append(f"Phase {phase['number']} [{phase['status']}] {phase['name']}")
|
|
if phase["blocking_requirements"]:
|
|
for blocker in phase["blocking_requirements"]:
|
|
lines.append(
|
|
f" - blocked by {blocker['rule']}: actual={blocker['actual']} expected={blocker['expected']}"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
spec = load_spec(args.spec_file)
|
|
issue_states = _load_issue_states(args, spec)
|
|
resources = _load_resources(args)
|
|
result = evaluate_progression(spec, issue_states, resources, repo_root=DEFAULT_REPO_ROOT)
|
|
|
|
if args.json:
|
|
rendered = json.dumps(result, indent=2)
|
|
elif args.markdown or args.output:
|
|
rendered = render_markdown(result)
|
|
else:
|
|
rendered = _render_cli_summary(result)
|
|
|
|
if args.output:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(rendered, encoding="utf-8")
|
|
print(f"Fleet progression report written to {args.output}")
|
|
else:
|
|
print(rendered)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|