resource-control/tools/portfolio_report.py
tegwick 17de8b831e feat(portfolio): fold in RAILIANCE-WP-0016 apps-pg evidence
First delegated evidence from RESOURCE-WP-0003-T04 to land. railiance-platform
delivered apps-pg capacity, utilization, consumers, and the apps-pg-dbbytes-v1
allocation driver, and correctly delivered no EUR.

- data/resources/apps-pg.json: real capacity; allocation unattributed -> shared
  under apps-pg-dbbytes-v1; second consumer vergabe-teilnahme registered
- data/control-cycle/apps-pg-2026-09-base.json: first operational control-cycle
  record in the repository
- examples/control-cycle/apps-pg-*.json retired; the invented fixture collided
  with the real record's identifier
- data/portfolio-coverage-2026-08-14.json: gap marked delivered with three
  residual unknowns still open

The real evidence exposed a design gap in the T05 schema: v0.1 required a number
for every cost field, so recording genuine usage without a booked cost meant
inventing one. Schema 0.2 permits null costs, null unattributed_eur, a technical
unattributed_share, and null measurements. Null is unknown, never zero; an
unknown component makes the total null rather than the sum of the known parts;
and the comparator classifies unknown amounts as data_quality instead of
computing a variance. Existing 0.1 records are not rewritten.

apps-pg is now measured (idle at 5.8% of volume) and attributed, and remains
unpriced: delivered technical evidence does not create a booked cost.

86 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 09:36:57 +02:00

310 lines
12 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 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 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)
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,
"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())