resource-control/tools/portfolio_report.py
tegwick f8d1c542d5 feat: operational resource procurement facility
Implement RESOURCE-WP-0005: entity register, V0.1 terms parameters,
entity association on inventory and planning records, transfer-price
and credit-state arithmetic, monthly settlement, and entity views on
the portfolio report. Live close emits nothing until delivered cost
is known. Handoffs are FIN-WP-0006 and RAILIANCE-WP-0017.
2026-08-14 13:15:02 +02:00

354 lines
14 KiB
Python

#!/usr/bin/env python3
"""Portfolio view: coverage, lifecycle, capacity, cost, renewals, risk, and openings.
The report never fills a gap with a guess. Every section separates what is known
from what is unknown, names the owner of each unknown, and reports unattributed
cost as its own figure rather than spreading it across resources.
"""
from __future__ import annotations
import json
import sys
from datetime import date
from pathlib import Path
from entities import REQUIRED_ENTITY_IDS, load_register
from optimization import evaluate, validate_case
from portfolio import validate_record
# How far ahead a contract date counts as "approaching".
RENEWAL_HORIZON_DAYS = 90
# Utilization at or below this fraction is flagged as idle capacity.
IDLE_THRESHOLD = 0.35
# Utilization at or above this fraction is flagged as saturated.
SATURATED_THRESHOLD = 0.85
USAGE_PAIRS = {
"cpu": "cpu_usage",
"memory": "memory_usage",
"root_filesystem": "root_filesystem_used",
"storage": "storage_used",
}
def _load_all(pattern: Path) -> list[dict]:
return [json.loads(path.read_text()) for path in sorted(pattern.parent.glob(pattern.name))]
def load_portfolio(root: Path) -> dict:
resources = _load_all(root / "data" / "resources" / "*.json")
for resource in resources:
validate_record(resource)
coverage_files = sorted((root / "data").glob("portfolio-coverage-*.json"))
coverage = json.loads(coverage_files[-1].read_text()) if coverage_files else None
cases = _load_all(root / "data" / "optimization" / "*.json")
for case in cases:
validate_case(case)
return {"resources": resources, "coverage": coverage, "cases": cases}
def coverage_section(coverage: dict | None) -> dict:
if coverage is None:
return {"observed_at": None, "groups": [], "unresolved_gaps": [], "note": "no coverage observation recorded"}
return {
"observed_at": coverage["observed_at"],
"inventory_records": coverage["inventory_records"],
"groups": [
{"group": group["group"], "status": group["status"], "resources": len(group["resource_ids"])}
for group in coverage["coverage"]
],
# A delivered gap stays visible with its residual unknowns rather than
# disappearing, so partial delivery is not read as full coverage.
"unresolved_gaps": [
{"owner": gap["owner"], "gap": gap["gap"], "delegated_workplan": gap["delegated_workplan"]}
for gap in coverage["owned_gaps"]
if gap.get("status", "open") == "open"
],
"delivered_gaps": [
{
"owner": gap["owner"],
"delegated_workplan": gap["delegated_workplan"],
"delivered_on": gap.get("delivered_on"),
"interface": gap.get("interface", []),
"residual_unknowns": gap.get("residual_unknowns", []),
}
for gap in coverage["owned_gaps"]
if gap.get("status", "open") == "delivered"
],
}
def lifecycle_section(resources: list[dict]) -> dict:
counts: dict[str, int] = {}
for resource in resources:
counts[resource["status"]] = counts.get(resource["status"], 0) + 1
unowned = [r["id"] for r in resources if not r["ownership"]["owner"]]
return {"by_status": dict(sorted(counts.items())), "without_owner": unowned}
def _capacity_index(resource: dict) -> dict:
return {entry["metric"]: entry for entry in resource["capacity"]}
def utilization_section(resources: list[dict]) -> dict:
measured, unmeasured = [], []
for resource in resources:
metrics = _capacity_index(resource)
rows = []
for metric, usage_metric in USAGE_PAIRS.items():
if metric not in metrics or usage_metric not in metrics:
continue
provisioned = metrics[metric]["value"]
used = metrics[usage_metric]["value"]
if not provisioned:
continue
ratio = round(used / provisioned, 4)
rows.append({
"metric": metric,
"provisioned": provisioned,
"used": used,
"unit": metrics[metric]["unit"],
"ratio": ratio,
"observed_at": metrics[metric].get("observed_at"),
"signal": (
"idle" if ratio <= IDLE_THRESHOLD
else "saturated" if ratio >= SATURATED_THRESHOLD
else "normal"
),
})
if rows:
measured.append({"resource_id": resource["id"], "metrics": rows})
else:
unmeasured.append({
"resource_id": resource["id"],
"owner": resource["ownership"]["owner"],
"reason": "no paired provisioned and observed capacity metric",
})
return {
"measured": measured,
"unmeasured": unmeasured,
"idle": sorted({
row["resource_id"] for row in measured
for metric in row["metrics"] if metric["signal"] == "idle"
}),
"saturated": sorted({
row["resource_id"] for row in measured
for metric in row["metrics"] if metric["signal"] == "saturated"
}),
}
def cost_section(resources: list[dict]) -> dict:
priced, unpriced, unattributed = [], [], []
for resource in resources:
entry = {
"resource_id": resource["id"],
"owner": resource["ownership"]["owner"],
"billing_model": resource["cost"]["billing_model"],
}
if resource["cost"]["price_evidence"]:
priced.append({**entry, "price_evidence": resource["cost"]["price_evidence"]})
else:
unpriced.append(entry)
if resource["ownership"]["allocation"]["mode"] == "unattributed":
unattributed.append(entry)
return {
"priced": priced,
"unpriced": unpriced,
"unattributed_allocation": unattributed,
# Deliberately not a number: no booked cost has reached this repository,
# so any portfolio spend total would be invented rather than measured.
"known_monthly_spend_eur": None,
"spend_note": (
f"{len(priced)} of {len(resources)} resources carry price evidence and none carries a booked "
"cost from fin-hub, so total portfolio spend is not computable and is reported as unknown "
"rather than as zero."
),
}
def renewals_section(resources: list[dict], today: date) -> dict:
approaching, undated = [], []
for resource in resources:
lifecycle = resource["lifecycle"]
dates = {k: lifecycle[k] for k in ("renews_on", "cancel_by") if lifecycle[k]}
if not dates:
if resource["status"] in {"active", "ordered", "commissioning"}:
undated.append({
"resource_id": resource["id"],
"owner": resource["ownership"]["owner"],
"risk": "no renewal or cancellation date recorded; the cancellation window cannot be respected",
})
continue
for field, value in dates.items():
days = (date.fromisoformat(value) - today).days
if days <= RENEWAL_HORIZON_DAYS:
approaching.append({
"resource_id": resource["id"],
"field": field,
"date": value,
"days_remaining": days,
})
approaching.sort(key=lambda row: row["days_remaining"])
return {"horizon_days": RENEWAL_HORIZON_DAYS, "approaching": approaching, "undated": undated}
def risk_section(resources: list[dict], utilization: dict, cost: dict) -> list[dict]:
risks = []
domains: dict[str, list[str]] = {}
for resource in resources:
for domain in resource["location"]["failure_domains"]:
domains.setdefault(domain, []).append(resource["id"])
for domain, members in sorted(domains.items()):
if len(members) > 2:
risks.append({
"kind": "concentrated_failure_domain",
"detail": f"{len(members)} resources share {domain}",
"resource_ids": sorted(members),
})
if cost["unpriced"]:
risks.append({
"kind": "unpriced_resources",
"detail": f"{len(cost['unpriced'])} resources have no price evidence, so spend cannot be measured",
"resource_ids": sorted(row["resource_id"] for row in cost["unpriced"]),
})
if cost["unattributed_allocation"]:
risks.append({
"kind": "unattributed_cost",
"detail": f"{len(cost['unattributed_allocation'])} resources have no allocation method, so their cost reaches no consumer",
"resource_ids": sorted(row["resource_id"] for row in cost["unattributed_allocation"]),
})
if utilization["idle"]:
risks.append({
"kind": "idle_capacity",
"detail": f"utilization at or below {IDLE_THRESHOLD:.0%} of provisioned capacity",
"resource_ids": utilization["idle"],
})
if utilization["saturated"]:
risks.append({
"kind": "saturated_capacity",
"detail": f"utilization at or above {SATURATED_THRESHOLD:.0%} of provisioned capacity",
"resource_ids": utilization["saturated"],
})
return risks
def optimization_section(cases: list[dict]) -> dict:
open_cases, blocked_on = [], []
for case in cases:
report = evaluate(case)
open_cases.append({
"case_id": report["case_id"],
"case_type": report["case_type"],
"state": report["decision_state"],
"best_option_id": report["best_option_id"],
"verdicts": {c["option_id"]: c["verdict"] for c in report["comparisons"]},
})
for comparison in report["comparisons"]:
for item in comparison["blocking_evidence"]:
if ".unknown:" in item:
blocked_on.append({"case_id": report["case_id"], "missing": item.split(".unknown:", 1)[1]})
return {
"cases": open_cases,
"undecided": [c["case_id"] for c in open_cases if c["state"] in {"blocked_on_evidence", "proposed"}],
"blocked_on_evidence": blocked_on,
}
def entities_section(resources: list[dict], register: dict[str, dict]) -> dict:
by_id = {entity_id: {
"financial_entity_id": entity_id,
"display_name": register[entity_id]["display_name"],
"role": register[entity_id]["role"],
"dedicated_resources": [],
"shared_shares": [],
"priced_resources": 0,
"consumption_mode": None,
"consumption_mode_note": "no settlement close yet; mode is unknown, not open",
} for entity_id in REQUIRED_ENTITY_IDS}
unattributed = []
for resource in resources:
entity_id = resource.get("financial_entity_id")
entry = {
"resource_id": resource["id"],
"price_evidence": bool(resource["cost"]["price_evidence"]),
}
if entity_id:
by_id[entity_id]["dedicated_resources"].append(resource["id"])
if resource["cost"]["price_evidence"]:
by_id[entity_id]["priced_resources"] += 1
else:
unattributed.append({
"resource_id": resource["id"],
"entity_gap": resource.get("entity_gap"),
"allocation_mode": resource["ownership"]["allocation"]["mode"],
})
for share in resource["ownership"]["allocation"].get("entity_shares") or []:
share_id = share["financial_entity_id"]
by_id[share_id]["shared_shares"].append({
"resource_id": resource["id"],
"note": share["note"],
})
return {
"entities": [by_id[entity_id] for entity_id in REQUIRED_ENTITY_IDS],
"unattributed_resources": unattributed,
"known_monthly_spend_eur": None,
}
def next_actions(report: dict) -> list[str]:
"""The smallest set of evidence that would unblock the most decisions."""
actions = []
for gap in report["coverage"]["unresolved_gaps"]:
actions.append(f"{gap['owner']}: deliver {gap['delegated_workplan']}{gap['gap']}")
for gap in report["coverage"].get("delivered_gaps", []):
for residual in gap["residual_unknowns"]:
actions.append(f"{gap['owner']}: {gap['delegated_workplan']} delivered, still open — {residual}")
if report["cost"]["unpriced"]:
actions.append(
"resource-control: no portfolio spend figure exists until at least one booked cost arrives "
"from fin-hub under the exchange contract"
)
for row in report["renewals"]["undated"]:
actions.append(f"{row['owner']}: record renewal and cancellation dates for {row['resource_id']}")
return actions
def build(root: Path, today: date | None = None) -> dict:
today = today or date.today()
data = load_portfolio(root)
_, register = load_register(root)
resources = data["resources"]
utilization = utilization_section(resources)
cost = cost_section(resources)
report = {
"schema_version": "0.1",
"generated_on": today.isoformat(),
"resource_count": len(resources),
"coverage": coverage_section(data["coverage"]),
"lifecycle": lifecycle_section(resources),
"utilization": utilization,
"cost": cost,
"entities": entities_section(resources, register),
"renewals": renewals_section(resources, today),
"optimization": optimization_section(data["cases"]),
}
report["risks"] = risk_section(resources, utilization, cost)
report["next_actions"] = next_actions(report)
return report
def main() -> int:
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
print(json.dumps(build(root), indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())