feat(portfolio): complete RESOURCE-WP-0003 T06 optimization cases and T07 reporting
T06: optimization-case schema, fail-closed evaluator, cadence and decision template. Every option including the baseline must present all ten decision fields; one unknown blocks the comparison. Validated on the storage case (Hetzner computes and loses to Scaleway by EUR 29.14/month on operator labour; Host Europe blocks on four named gaps) and on the non-storage reef-railiance k3s rightsizing case (low utilization is real, but nothing is costable while the railiance01 price is unknown). T07: portfolio report over coverage, lifecycle, utilization, cost, renewals, risks, open cases, and next actions, derived only from committed evidence. Portfolio spend is reported null rather than as a partial sum, unattributed cost is a named list rather than a spread, and unmeasurable resources are reported rather than dropped. RESOURCE-WP-0003 is finished; both cases remain blocked_on_evidence against live delegated records in other repositories. RESOURCE-WP-0002 is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
54dd45c926
commit
2c2a6073ff
63 changed files with 4662 additions and 69 deletions
83
tools/control_cycle.py
Normal file
83
tools/control_cycle.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Compare a generic immutable resource forecast with an actual observation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
COST_FIELDS = ("infrastructure", "internal_labor", "external_labor", "total")
|
||||
ATTRIBUTIONS = {"demand", "provider_price", "allocation", "labor", "model", "data_quality"}
|
||||
|
||||
|
||||
def delta(forecast: float, actual: float) -> dict:
|
||||
error = actual - forecast
|
||||
return {
|
||||
"forecast": forecast,
|
||||
"actual": actual,
|
||||
"error": round(error, 4),
|
||||
"absolute_percentage_error": None if forecast == 0 else round(abs(error) / forecast * 100, 2),
|
||||
}
|
||||
|
||||
|
||||
def compare(forecast: dict, actual: dict) -> dict:
|
||||
if forecast["record_type"] != "forecast" or actual["record_type"] != "actual":
|
||||
raise ValueError("expected forecast and actual records")
|
||||
for field in ("resource_id", "resource_class", "period"):
|
||||
if forecast[field] != actual[field]:
|
||||
raise ValueError(f"{field} mismatch")
|
||||
if actual.get("forecast_ref") != forecast["record_id"]:
|
||||
raise ValueError("actual forecast_ref must identify the immutable forecast")
|
||||
|
||||
attribution = actual.get("variance_attribution", {})
|
||||
unknown = set(attribution.values()) - ATTRIBUTIONS
|
||||
if unknown:
|
||||
raise ValueError(f"unknown variance attribution: {sorted(unknown)}")
|
||||
|
||||
proxies = {}
|
||||
all_proxies = sorted(set(forecast["usage_proxies"]) | set(actual["usage_proxies"]))
|
||||
for name in all_proxies:
|
||||
planned = forecast["usage_proxies"].get(name)
|
||||
observed = actual["usage_proxies"].get(name)
|
||||
if planned is None or observed is None:
|
||||
proxies[name] = {"status": "missing", "category": "data_quality"}
|
||||
elif planned["unit"] != observed["unit"]:
|
||||
proxies[name] = {"status": "unit-mismatch", "category": "data_quality"}
|
||||
else:
|
||||
proxies[name] = {**delta(planned["value"], observed["value"]), "unit": planned["unit"], "category": attribution.get(name, "demand")}
|
||||
|
||||
costs = {}
|
||||
for name in COST_FIELDS:
|
||||
default_category = "labor" if "labor" in name else "provider_price"
|
||||
costs[name] = {**delta(forecast["costs"][name], actual["costs"][name]), "currency": "EUR", "category": attribution.get(f"costs.{name}", default_category)}
|
||||
|
||||
if forecast["allocation"] != actual["allocation"]:
|
||||
costs["allocation_method"] = {"status": "changed", "category": attribution.get("allocation", "allocation")}
|
||||
|
||||
return {
|
||||
"forecast_ref": forecast["record_id"],
|
||||
"actual_ref": actual["record_id"],
|
||||
"resource_id": forecast["resource_id"],
|
||||
"resource_class": forecast["resource_class"],
|
||||
"period": forecast["period"],
|
||||
"usage_proxies": proxies,
|
||||
"costs": costs,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print(f"usage: {sys.argv[0]} FORECAST.json ACTUAL.json", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
result = compare(json.loads(Path(sys.argv[1]).read_text()), json.loads(Path(sys.argv[2]).read_text()))
|
||||
except (KeyError, ValueError) as exc:
|
||||
print(f"control-cycle error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
99
tools/cost_model.py
Normal file
99
tools/cost_model.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Calculate comparable 12-month object-storage forecasts from JSON evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def quote_provider(provider: dict, scenario: dict, stored_gb: float, restore_gb: float) -> dict:
|
||||
excess_storage = max(0, stored_gb - provider["included_storage_gb"])
|
||||
excess_egress = max(0, restore_gb - provider["included_egress_gb"])
|
||||
missing = []
|
||||
if excess_storage and provider["storage_eur_per_gb_month"] is None:
|
||||
missing.append("storage_eur_per_gb_month")
|
||||
if excess_egress and provider["egress_eur_per_gb"] is None:
|
||||
missing.append("egress_eur_per_gb")
|
||||
for field in ("monthly_minimum_eur", "operations_eur_per_month", "support_eur_per_month"):
|
||||
if provider[field] is None:
|
||||
missing.append(field)
|
||||
|
||||
infrastructure = None
|
||||
labor_hours = max(scenario["operator_hours_per_month"], provider["operator_hours_per_month"])
|
||||
labor = labor_hours * scenario["operator_hourly_eur"]
|
||||
recurring = None
|
||||
exit_cost = None
|
||||
if not missing:
|
||||
usage = excess_storage * (provider["storage_eur_per_gb_month"] or 0)
|
||||
usage += excess_egress * (provider["egress_eur_per_gb"] or 0)
|
||||
usage += scenario.get("write_requests_per_month", 0) / 1000 * provider.get("write_eur_per_1000", 0)
|
||||
usage += scenario.get("read_requests_per_month", 0) / 1000 * provider.get("read_eur_per_1000", 0)
|
||||
service = max(provider["monthly_minimum_eur"], usage)
|
||||
infrastructure = service + provider["operations_eur_per_month"] + provider["support_eur_per_month"]
|
||||
recurring = infrastructure + labor
|
||||
exit_excess = max(0, stored_gb - provider["included_egress_gb"])
|
||||
if exit_excess and provider["egress_eur_per_gb"] is None:
|
||||
missing.append("egress_eur_per_gb_for_exit")
|
||||
else:
|
||||
exit_cost = exit_excess * (provider["egress_eur_per_gb"] or 0) + 4 * scenario["operator_hourly_eur"]
|
||||
|
||||
setup_internal_hours = provider.get("setup_operator_hours", 0)
|
||||
setup_internal = setup_internal_hours * scenario["operator_hourly_eur"]
|
||||
setup_external = provider.get("setup_external_eur", None if "garage" in provider["id"] else 0)
|
||||
return {
|
||||
"monthly_infrastructure_eur": None if infrastructure is None else round(infrastructure, 2),
|
||||
"monthly_internal_labor_hours": labor_hours,
|
||||
"monthly_internal_labor_eur": round(labor, 2),
|
||||
"recurring_total_eur": None if recurring is None else round(recurring, 2),
|
||||
"setup_internal_labor_hours": setup_internal_hours,
|
||||
"setup_internal_labor_eur": round(setup_internal, 2),
|
||||
"setup_external_services_eur": setup_external,
|
||||
"setup_known_total_eur": None if setup_external is None else round(setup_internal + setup_external, 2),
|
||||
"exit_cost_eur": None if exit_cost is None else round(exit_cost, 2),
|
||||
"missing_price_fields": sorted(set(missing)),
|
||||
}
|
||||
|
||||
|
||||
def forecast(demand: dict, catalog: dict) -> dict:
|
||||
result = {"currency": demand["currency"], "months": 12, "scenarios": {}, "comparison_320gb": []}
|
||||
retention = demand["retention_days"]
|
||||
backups_per_day = demand["base_backups_per_day"]
|
||||
for scenario_name, scenario in demand["scenarios"].items():
|
||||
rows = []
|
||||
db_gb = scenario["initial_database_gb"]
|
||||
for month in range(1, 13):
|
||||
stored_gb = db_gb * retention * backups_per_day + scenario["wal_gb_per_day"] * retention
|
||||
restore_gb = scenario["restore_egress_gb"]
|
||||
for provider in catalog["providers"]:
|
||||
rows.append({
|
||||
"month": month, "provider_id": provider["id"],
|
||||
"database_gb": round(db_gb, 3), "stored_gb": round(stored_gb, 3),
|
||||
"restore_egress_gb": restore_gb,
|
||||
**quote_provider(provider, scenario, stored_gb, restore_gb),
|
||||
})
|
||||
db_gb *= 1 + scenario["monthly_database_growth_pct"] / 100
|
||||
result["scenarios"][scenario_name] = rows
|
||||
normalized = demand["scenarios"]["base"]
|
||||
for provider in catalog["providers"]:
|
||||
result["comparison_320gb"].append({
|
||||
"provider_id": provider["id"], "stored_gb": 320,
|
||||
"restore_egress_gb": normalized["restore_egress_gb"],
|
||||
**quote_provider(provider, normalized, 320, normalized["restore_egress_gb"]),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print(f"usage: {sys.argv[0]} DEMAND.json PROVIDERS.json", file=sys.stderr)
|
||||
return 2
|
||||
demand = json.loads(Path(sys.argv[1]).read_text())
|
||||
catalog = json.loads(Path(sys.argv[2]).read_text())
|
||||
print(json.dumps(forecast(demand, catalog), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
144
tools/financial_exchange.py
Normal file
144
tools/financial_exchange.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Produce planning evidence and consume fin-hub booked-cost projections."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from calendar import monthrange
|
||||
from datetime import date
|
||||
from decimal import Decimal, ROUND_HALF_EVEN
|
||||
from pathlib import Path
|
||||
|
||||
CENT = Decimal("0.01")
|
||||
|
||||
|
||||
def money(value: object) -> Decimal:
|
||||
return Decimal(str(value)).quantize(CENT, rounding=ROUND_HALF_EVEN)
|
||||
|
||||
|
||||
def money_text(value: object) -> str:
|
||||
return format(money(value), ".2f")
|
||||
|
||||
|
||||
def forecast_records(payload: dict, *, resource_id: str, source_ref: str) -> list[dict]:
|
||||
if payload.get("schema_version") != "0.1" or payload.get("record_type") != "forecast":
|
||||
raise ValueError("expected a resource-control forecast v0.1")
|
||||
if not resource_id.startswith("resource:"):
|
||||
raise ValueError("resource_id must use the resource: namespace")
|
||||
created_at = payload["created_at"]
|
||||
version = f"{payload['provider_id']}:{created_at}"
|
||||
records = []
|
||||
for row in payload["rows"]:
|
||||
infrastructure = money(row["infrastructure_eur"])
|
||||
labor = money(row["internal_labor_eur"])
|
||||
if money(row["total_eur"]) != infrastructure + labor:
|
||||
raise ValueError("total_eur does not match infrastructure plus internal labor")
|
||||
year, month = (int(part) for part in row["period"].split("-"))
|
||||
period_start = date(year, month, 1)
|
||||
period_end = date(year, month, monthrange(year, month)[1])
|
||||
records.append({
|
||||
"schema_version": "0.1",
|
||||
"record_type": "forecast",
|
||||
"record_id": f"forecast:{payload['provider_id']}:{payload['cost_attribution_key']}:{row['period']}:{created_at}",
|
||||
"revision_of": payload.get("forecast_ref"),
|
||||
"resource_id": resource_id,
|
||||
"service_id": payload["provider_id"],
|
||||
"workload_id": payload["workload"],
|
||||
"tenant_id": None,
|
||||
"environment": "production",
|
||||
"cost_attribution_key": payload["cost_attribution_key"],
|
||||
"period_start": period_start.isoformat(),
|
||||
"period_end": period_end.isoformat(),
|
||||
"currency": "EUR",
|
||||
"source_evidence": [source_ref, *row.get("evidence", [])],
|
||||
"created_at": created_at,
|
||||
"scenario": payload.get("scenario") or "base",
|
||||
"forecast_version": version,
|
||||
"costs": {
|
||||
"infrastructure": money_text(infrastructure),
|
||||
"internal_labor": money_text(labor),
|
||||
"external_services": "0.00",
|
||||
"setup": "0.00",
|
||||
"other": "0.00"
|
||||
},
|
||||
"uncertainty": None,
|
||||
"assumptions": [
|
||||
f"database_gb={row['database_gb']}",
|
||||
f"stored_gb={row['stored_gb']}",
|
||||
f"wal_gb={row['wal_gb']}",
|
||||
f"restore_egress_gb={row['restore_egress_gb']}",
|
||||
f"write_requests={row['write_requests']}",
|
||||
f"read_requests={row['read_requests']}",
|
||||
f"internal_labor_hours={row['internal_labor_hours']}"
|
||||
]
|
||||
})
|
||||
return records
|
||||
|
||||
|
||||
def validate_booked_cost(record: dict) -> dict:
|
||||
required = {
|
||||
"schema_version", "record_type", "financial_fact_id", "adjustment_kind",
|
||||
"source_document_id", "source_line_id", "content_fingerprint", "provider",
|
||||
"accounting_period", "currency", "gross_amount", "adjustment_amount",
|
||||
"effective_amount", "source_evidence_ref", "recorded_at"
|
||||
}
|
||||
missing = required - record.keys()
|
||||
if missing:
|
||||
raise ValueError(f"booked-cost evidence missing {sorted(missing)}")
|
||||
if record["schema_version"] != "0.1" or record["record_type"] != "booked_cost":
|
||||
raise ValueError("unsupported booked-cost schema")
|
||||
if len(record["currency"]) != 3 or record["currency"] != record["currency"].upper():
|
||||
raise ValueError("currency must be an uppercase three-letter code")
|
||||
if money(record["effective_amount"]) != money(record["gross_amount"]) + money(record["adjustment_amount"]):
|
||||
raise ValueError("effective_amount must equal gross plus adjustment")
|
||||
if record.get("tax_status") == "unknown" and record.get("tax_amount") is not None:
|
||||
raise ValueError("unknown tax must not have a tax amount")
|
||||
return record
|
||||
|
||||
|
||||
def reconcile(forecasts: list[dict], booked_costs: list[dict]) -> list[dict]:
|
||||
planned = {
|
||||
(record["period_start"][:7], record["currency"], record.get("cost_attribution_key")):
|
||||
money(record["costs"]["infrastructure"])
|
||||
for record in forecasts if record["record_type"] == "forecast"
|
||||
}
|
||||
observed: dict[tuple[str, str, str | None], Decimal] = {}
|
||||
fact_ids: dict[tuple[str, str, str | None], list[str]] = {}
|
||||
for raw in booked_costs:
|
||||
record = validate_booked_cost(raw)
|
||||
key = (record["accounting_period"], record["currency"], record.get("cost_attribution_key"))
|
||||
observed[key] = observed.get(key, Decimal("0")) + money(record["effective_amount"])
|
||||
fact_ids.setdefault(key, []).append(record["financial_fact_id"])
|
||||
rows = []
|
||||
for key in sorted(set(planned) | set(observed), key=lambda item: (item[0], item[1], item[2] or "")):
|
||||
forecast = planned.get(key)
|
||||
actual = observed.get(key)
|
||||
rows.append({
|
||||
"period": key[0], "currency": key[1], "cost_attribution_key": key[2],
|
||||
"forecast_infrastructure": None if forecast is None else money_text(forecast),
|
||||
"booked_effective": None if actual is None else money_text(actual),
|
||||
"variance": None if forecast is None or actual is None else money_text(actual - forecast),
|
||||
"financial_fact_ids": fact_ids.get(key, []),
|
||||
"status": "reconciled" if forecast is not None and actual is not None else "missing-booked-cost" if forecast is not None else "missing-forecast"
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 3 or sys.argv[1] not in {"forecast", "reconcile"}:
|
||||
print(f"usage: {sys.argv[0]} forecast FORECAST.json [RESOURCE_ID] | reconcile FORECAST_EVIDENCE.json BOOKED_COST.json", file=sys.stderr)
|
||||
return 2
|
||||
if sys.argv[1] == "forecast":
|
||||
payload = json.loads(Path(sys.argv[2]).read_text())
|
||||
resource_id = sys.argv[3] if len(sys.argv) > 3 else "resource:platform_audit_storage"
|
||||
print(json.dumps(forecast_records(payload, resource_id=resource_id, source_ref=sys.argv[2]), indent=2))
|
||||
return 0
|
||||
forecasts = json.loads(Path(sys.argv[2]).read_text())
|
||||
booked = json.loads(Path(sys.argv[3]).read_text())
|
||||
print(json.dumps(reconcile(forecasts, booked), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
209
tools/optimization.py
Normal file
209
tools/optimization.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Evaluate resource optimization cases.
|
||||
|
||||
The evaluator is fail-closed: any decision field that is unknown makes the
|
||||
affected comparison unknown rather than optimistic. A case only reaches a
|
||||
recommendation when every field the decision template requires is present for
|
||||
the baseline and for the alternative being compared.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CASE_TYPES = {
|
||||
"rightsizing", "consolidation", "commitment", "renewal",
|
||||
"migration", "retirement", "provider_switch",
|
||||
}
|
||||
DECISION_STATES = {
|
||||
"blocked_on_evidence", "proposed", "approved", "rejected", "superseded",
|
||||
}
|
||||
COST_FIELDS = (
|
||||
"recurring_infrastructure_eur_month",
|
||||
"recurring_internal_labor_eur_month",
|
||||
"recurring_external_labor_eur_month",
|
||||
)
|
||||
# Fields the decision template requires before any recommendation is made.
|
||||
REQUIRED_FOR_DECISION = COST_FIELDS + ("one_time_eur", "exit_path")
|
||||
|
||||
# A saving below this monthly threshold is not material enough to act on.
|
||||
MATERIALITY_EUR_MONTH = 5.0
|
||||
|
||||
|
||||
def recurring_total(option: dict) -> float | None:
|
||||
"""Monthly recurring cost, or None when any component is unknown."""
|
||||
values = [option[field] for field in COST_FIELDS]
|
||||
if any(value is None for value in values):
|
||||
return None
|
||||
return round(sum(values), 2)
|
||||
|
||||
|
||||
def missing_fields(option: dict) -> list[str]:
|
||||
missing = [field for field in REQUIRED_FOR_DECISION if option[field] is None]
|
||||
if not option["utilization"]:
|
||||
missing.append("utilization")
|
||||
if not option["service_constraints"]:
|
||||
missing.append("service_constraints")
|
||||
return sorted(missing)
|
||||
|
||||
|
||||
def utilization_ratios(option: dict) -> dict:
|
||||
"""Used-over-provisioned per metric; None where either side is unknown."""
|
||||
ratios = {}
|
||||
for metric, pair in option["utilization"].items():
|
||||
provisioned = pair["provisioned"]["value"]
|
||||
used = pair["used"]["value"]
|
||||
if provisioned in (None, 0) or used is None:
|
||||
ratios[metric] = None
|
||||
else:
|
||||
ratios[metric] = round(used / provisioned, 4)
|
||||
return ratios
|
||||
|
||||
|
||||
def compare_option(baseline: dict, alternative: dict) -> dict:
|
||||
"""Compare one alternative against the baseline on the decision fields."""
|
||||
blocking = sorted(set(
|
||||
[f"baseline.{field}" for field in missing_fields(baseline)]
|
||||
+ [f"{alternative['option_id']}.{field}" for field in missing_fields(alternative)]
|
||||
+ [f"baseline.unknown:{item}" for item in baseline["unknowns"]]
|
||||
+ [f"{alternative['option_id']}.unknown:{item}" for item in alternative["unknowns"]]
|
||||
))
|
||||
|
||||
base_recurring = recurring_total(baseline)
|
||||
alt_recurring = recurring_total(alternative)
|
||||
monthly_delta = None
|
||||
monthly_saving = None
|
||||
if base_recurring is not None and alt_recurring is not None:
|
||||
monthly_delta = round(alt_recurring - base_recurring, 2)
|
||||
monthly_saving = round(-monthly_delta, 2)
|
||||
|
||||
one_time = alternative["one_time_eur"]
|
||||
payback_months = None
|
||||
payback_note = None
|
||||
if monthly_saving is None or one_time is None:
|
||||
payback_note = "unknown: incomplete cost evidence"
|
||||
elif monthly_saving <= 0:
|
||||
payback_note = "never: the alternative does not reduce recurring cost"
|
||||
elif one_time == 0:
|
||||
payback_months = 0.0
|
||||
payback_note = "immediate: no one-time cost"
|
||||
else:
|
||||
payback_months = round(one_time / monthly_saving, 1)
|
||||
|
||||
# Failure domains the alternative removes, and ones it newly introduces.
|
||||
base_domains = set(baseline["failure_domains"])
|
||||
alt_domains = set(alternative["failure_domains"])
|
||||
|
||||
if blocking:
|
||||
verdict = "blocked_on_evidence"
|
||||
elif monthly_saving is not None and monthly_saving > MATERIALITY_EUR_MONTH:
|
||||
verdict = "recommend"
|
||||
elif monthly_delta is not None and abs(monthly_delta) <= MATERIALITY_EUR_MONTH:
|
||||
verdict = "no_material_change"
|
||||
else:
|
||||
verdict = "reject"
|
||||
|
||||
return {
|
||||
"option_id": alternative["option_id"],
|
||||
"label": alternative["label"],
|
||||
"verdict": verdict,
|
||||
"baseline_recurring_eur_month": base_recurring,
|
||||
"alternative_recurring_eur_month": alt_recurring,
|
||||
"monthly_delta_eur": monthly_delta,
|
||||
"monthly_saving_eur": monthly_saving,
|
||||
"one_time_eur": one_time,
|
||||
"payback_months": payback_months,
|
||||
"payback_note": payback_note,
|
||||
"baseline_utilization": utilization_ratios(baseline),
|
||||
"alternative_utilization": utilization_ratios(alternative),
|
||||
"uncertainty": alternative["uncertainty"]["level"],
|
||||
"failure_domains_removed": sorted(base_domains - alt_domains),
|
||||
"failure_domains_added": sorted(alt_domains - base_domains),
|
||||
"exit_path_known": alternative["exit_path"] is not None,
|
||||
"blocking_evidence": blocking,
|
||||
}
|
||||
|
||||
|
||||
def validate_case(case: dict) -> None:
|
||||
if case["schema_version"] != "0.1":
|
||||
raise ValueError("unsupported optimization-case schema version")
|
||||
if case["record_scope"] not in {"operational", "illustrative"}:
|
||||
raise ValueError("record_scope must be operational or illustrative")
|
||||
if case["case_type"] not in CASE_TYPES:
|
||||
raise ValueError(f"unknown case_type {case['case_type']}")
|
||||
if not case["case_id"].startswith("opt:"):
|
||||
raise ValueError("case_id must start with 'opt:'")
|
||||
if not case["resource_ids"]:
|
||||
raise ValueError("a case must name at least one resource")
|
||||
if any(not rid.startswith("resource:") for rid in case["resource_ids"]):
|
||||
raise ValueError("resource_ids must be portfolio resource identifiers")
|
||||
if not case["evidence"]:
|
||||
raise ValueError("a case must cite evidence")
|
||||
|
||||
decision = case["decision"]
|
||||
if decision["state"] not in DECISION_STATES:
|
||||
raise ValueError(f"unknown decision state {decision['state']}")
|
||||
|
||||
option_ids = [case["baseline"]["option_id"]] + [a["option_id"] for a in case["alternatives"]]
|
||||
if len(set(option_ids)) != len(option_ids):
|
||||
raise ValueError("option identifiers must be unique within a case")
|
||||
|
||||
report = evaluate(case)
|
||||
blocked = any(r["verdict"] == "blocked_on_evidence" for r in report["comparisons"])
|
||||
|
||||
# A case cannot be approved while its own evidence is incomplete, and an
|
||||
# approval must name the authority that gave it.
|
||||
if decision["state"] in {"approved", "rejected"}:
|
||||
if decision["approver"] is None or decision["approved_on"] is None:
|
||||
raise ValueError("a decided case must record approver and approved_on")
|
||||
if decision["state"] == "approved":
|
||||
if blocked:
|
||||
raise ValueError("cannot approve a case with blocking evidence gaps")
|
||||
if decision["recommended_option_id"] not in option_ids:
|
||||
raise ValueError("approved case must recommend a known option")
|
||||
if decision["state"] == "blocked_on_evidence" and not blocked:
|
||||
raise ValueError("case is marked blocked but every decision field is known")
|
||||
if decision["state"] == "proposed" and blocked:
|
||||
raise ValueError("case has blocking evidence gaps and cannot be proposed")
|
||||
|
||||
|
||||
def evaluate(case: dict) -> dict:
|
||||
baseline = case["baseline"]
|
||||
comparisons = [compare_option(baseline, alt) for alt in case["alternatives"]]
|
||||
recommended = [c for c in comparisons if c["verdict"] == "recommend"]
|
||||
recommended.sort(key=lambda c: (c["payback_months"] is None, c["payback_months"]))
|
||||
return {
|
||||
"case_id": case["case_id"],
|
||||
"case_type": case["case_type"],
|
||||
"record_scope": case["record_scope"],
|
||||
"review_period": case["review_period"],
|
||||
"baseline": {
|
||||
"option_id": baseline["option_id"],
|
||||
"label": baseline["label"],
|
||||
"recurring_eur_month": recurring_total(baseline),
|
||||
"utilization": utilization_ratios(baseline),
|
||||
},
|
||||
"comparisons": comparisons,
|
||||
"best_option_id": recommended[0]["option_id"] if recommended else None,
|
||||
"decision_state": case["decision"]["state"],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
paths = sys.argv[1:] or sorted(str(p) for p in Path("data/optimization").glob("*.json"))
|
||||
if not paths:
|
||||
print("no optimization cases found", file=sys.stderr)
|
||||
return 2
|
||||
reports = []
|
||||
for path in paths:
|
||||
case = json.loads(Path(path).read_text())
|
||||
validate_case(case)
|
||||
reports.append(evaluate(case))
|
||||
print(json.dumps(reports, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
97
tools/portfolio.py
Normal file
97
tools/portfolio.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Semantic validation for managed-infrastructure portfolio records."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
RESOURCE_ID = re.compile(r"^resource:[a-z0-9][a-z0-9:_-]+$")
|
||||
STATUSES = {
|
||||
"proposed", "ordered", "commissioning", "active", "suspended",
|
||||
"retiring", "retired", "rejected",
|
||||
}
|
||||
TRANSITIONS = {
|
||||
"proposed": {"ordered", "rejected"},
|
||||
"ordered": {"commissioning", "rejected"},
|
||||
"commissioning": {"active", "rejected"},
|
||||
"active": {"suspended", "retiring"},
|
||||
"suspended": {"active", "retiring"},
|
||||
"retiring": {"retired", "active"},
|
||||
"retired": set(),
|
||||
"rejected": {"proposed"},
|
||||
}
|
||||
|
||||
|
||||
def _day(value: str | None) -> date | None:
|
||||
return date.fromisoformat(value) if value else None
|
||||
|
||||
|
||||
def validate_transition(current: str, target: str) -> None:
|
||||
if current not in STATUSES or target not in STATUSES:
|
||||
raise ValueError("unknown lifecycle status")
|
||||
if target not in TRANSITIONS[current]:
|
||||
raise ValueError(f"invalid lifecycle transition {current} -> {target}")
|
||||
|
||||
|
||||
def validate_record(record: dict) -> None:
|
||||
if record.get("schema_version") != "0.2":
|
||||
raise ValueError("portfolio records must use schema_version 0.2")
|
||||
if not RESOURCE_ID.fullmatch(record.get("id", "")):
|
||||
raise ValueError("invalid resource id")
|
||||
if record.get("status") not in STATUSES:
|
||||
raise ValueError("unknown lifecycle status")
|
||||
if record.get("record_scope") not in {"inventory", "example"}:
|
||||
raise ValueError("record_scope must be inventory or example")
|
||||
|
||||
allocation = record["ownership"]["allocation"]
|
||||
if allocation["mode"] == "unattributed":
|
||||
if allocation["cost_attribution_key"] is not None:
|
||||
raise ValueError("unattributed resources cannot have an attribution key")
|
||||
elif not allocation["cost_attribution_key"]:
|
||||
raise ValueError("dedicated/shared resources require an attribution key")
|
||||
if allocation["mode"] == "shared" and (
|
||||
not allocation["driver"] or not allocation["method_version"]
|
||||
):
|
||||
raise ValueError("shared resources require a driver and method version")
|
||||
|
||||
dimensions = [(item["metric"], item["kind"]) for item in record["capacity"]]
|
||||
if len(dimensions) != len(set(dimensions)):
|
||||
raise ValueError("capacity metric/kind pairs must be unique")
|
||||
if any(rel["resource_id"] == record["id"] for rel in record["relationships"]):
|
||||
raise ValueError("a resource cannot relate to itself")
|
||||
|
||||
lifecycle = record["lifecycle"]
|
||||
proposed = _day(lifecycle["proposed_on"])
|
||||
ordered = _day(lifecycle["ordered_on"])
|
||||
commissioned = _day(lifecycle["commissioned_on"])
|
||||
retired = _day(lifecycle["retired_on"])
|
||||
dated = [value for value in (proposed, ordered, commissioned, retired) if value]
|
||||
if dated != sorted(dated):
|
||||
raise ValueError("lifecycle dates are out of order")
|
||||
if record["status"] == "retired" and retired is None:
|
||||
raise ValueError("retired resources require retired_on")
|
||||
# Discovery must preserve unknown commercial and commissioning dates as
|
||||
# null instead of manufacturing precision. Date ordering is enforced when
|
||||
# the authoritative repositories or provider evidence supply the values.
|
||||
if record["record_scope"] == "example" and not any(
|
||||
item["kind"] == "example" for item in record["evidence"]
|
||||
):
|
||||
raise ValueError("examples require explicit example evidence")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print(f"usage: {sys.argv[0]} RECORD.json ...", file=sys.stderr)
|
||||
return 2
|
||||
for name in sys.argv[1:]:
|
||||
validate_record(json.loads(Path(name).read_text()))
|
||||
print(f"portfolio records valid: {len(sys.argv) - 1}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
292
tools/portfolio_report.py
Normal file
292
tools/portfolio_report.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
#!/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",
|
||||
}
|
||||
|
||||
|
||||
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"]
|
||||
],
|
||||
"unresolved_gaps": [
|
||||
{"owner": gap["owner"], "gap": gap["gap"], "delegated_workplan": gap["delegated_workplan"]}
|
||||
for gap in coverage["owned_gaps"]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
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']}")
|
||||
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())
|
||||
79
tools/validate.py
Normal file
79
tools/validate.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Dependency-free validation for resource-control JSON declarations."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from optimization import validate_case
|
||||
from portfolio import validate_record
|
||||
from portfolio_report import build as build_portfolio_report
|
||||
|
||||
|
||||
def load(path: str) -> dict:
|
||||
return json.loads(Path(path).read_text())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
demand = load("data/demand/platform-audit-storage.json")
|
||||
providers = load("data/providers/object-storage.json")
|
||||
schema = load("schemas/resource-inventory.schema.json")
|
||||
observation_schema = load("schemas/monthly-resource-observation.schema.json")
|
||||
planning_schema = load("schemas/planning-evidence.schema.json")
|
||||
control_schema = load("schemas/resource-control-cycle.schema.json")
|
||||
forecasts = [load(str(path)) for path in Path("data/forecasts").glob("*.json")]
|
||||
resource_paths = list(Path("data/resources").glob("*.json"))
|
||||
resource_paths += list(Path("examples/portfolio").glob("*.json"))
|
||||
resources = [load(str(path)) for path in resource_paths]
|
||||
control_records = [load(str(path)) for path in Path("examples/control-cycle").glob("*.json")]
|
||||
assert demand["schema_version"] == providers["schema_version"] == "0.1"
|
||||
assert demand["retention_days"] >= 30
|
||||
assert set(demand["scenarios"]) == {"low", "base", "high"}
|
||||
assert len({p["id"] for p in providers["providers"]}) == len(providers["providers"])
|
||||
assert {"Host Europe", "Scaleway", "Hetzner", "AWS", "Microsoft Azure", "Google Cloud", "STACKIT"} <= {p["provider"] for p in providers["providers"]}
|
||||
assert schema["$schema"].endswith("2020-12/schema")
|
||||
assert observation_schema["$schema"].endswith("2020-12/schema")
|
||||
assert planning_schema["$schema"].endswith("2020-12/schema")
|
||||
assert control_schema["$schema"].endswith("2020-12/schema")
|
||||
assert len(planning_schema["oneOf"]) == 5
|
||||
for forecast in forecasts:
|
||||
assert forecast["record_type"] == "forecast"
|
||||
assert len({row["period"] for row in forecast["rows"]}) == len(forecast["rows"])
|
||||
assert all(row["total_eur"] == round(row["infrastructure_eur"] + row["internal_labor_eur"], 2) for row in forecast["rows"])
|
||||
assert schema["properties"]["schema_version"]["const"] == "0.2"
|
||||
required = set(schema["required"])
|
||||
for resource in resources:
|
||||
assert not required - resource.keys(), f"missing fields: {required - resource.keys()}"
|
||||
validate_record(resource)
|
||||
record_ids = {record["record_id"] for record in control_records}
|
||||
assert len(record_ids) == len(control_records)
|
||||
assert {record["resource_class"] for record in control_records} == {"storage", "cluster_compute", "shared_platform_service"}
|
||||
for record in control_records:
|
||||
assert record["schema_version"] == "0.1"
|
||||
assert record["resource_id"].startswith("resource:")
|
||||
costs = record["costs"]
|
||||
assert costs["total"] == round(costs["infrastructure"] + costs["internal_labor"] + costs["external_labor"], 2)
|
||||
if record["record_type"] == "actual":
|
||||
assert record["forecast_ref"] in record_ids
|
||||
case_schema = load("schemas/optimization-case.schema.json")
|
||||
assert case_schema["$schema"].endswith("2020-12/schema")
|
||||
assert case_schema["properties"]["schema_version"]["const"] == "0.1"
|
||||
cases = [load(str(path)) for path in Path("data/optimization").glob("*.json")]
|
||||
assert len({case["case_id"] for case in cases}) == len(cases)
|
||||
for case in cases:
|
||||
assert not set(case_schema["required"]) - case.keys()
|
||||
validate_case(case)
|
||||
# The optimization process must be validated on the backup case and on at
|
||||
# least one non-storage portfolio candidate (RESOURCE-WP-0003-T06).
|
||||
case_resources = {rid for case in cases for rid in case["resource_ids"]}
|
||||
assert "resource:platform:audit-storage" in case_resources
|
||||
assert case_resources - {"resource:platform:audit-storage"}
|
||||
report = build_portfolio_report(Path("."))
|
||||
assert report["resource_count"] == len(resource_paths) - len(list(Path("examples/portfolio").glob("*.json")))
|
||||
assert report["cost"]["known_monthly_spend_eur"] is None
|
||||
assert report["next_actions"]
|
||||
print("resource-control declarations: valid")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
55
tools/variance.py
Normal file
55
tools/variance.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Compare monthly resource actuals with the immutable decision forecast."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
METRICS = (
|
||||
"database_gb", "stored_gb", "wal_gb", "restore_egress_gb",
|
||||
"write_requests", "read_requests", "infrastructure_eur",
|
||||
"internal_labor_hours", "internal_labor_eur", "total_eur",
|
||||
)
|
||||
|
||||
|
||||
def compare(forecast: dict, actual: dict) -> dict:
|
||||
expected = {row["period"]: row for row in forecast["rows"]}
|
||||
rows = []
|
||||
for observed in actual["rows"]:
|
||||
period = observed["period"]
|
||||
if period not in expected:
|
||||
rows.append({"period": period, "status": "no-forecast", "metrics": {}})
|
||||
continue
|
||||
metrics = {}
|
||||
for metric in METRICS:
|
||||
planned = expected[period][metric]
|
||||
measured = observed[metric]
|
||||
error = measured - planned
|
||||
metrics[metric] = {
|
||||
"forecast": planned,
|
||||
"actual": measured,
|
||||
"error": round(error, 4),
|
||||
"absolute_percentage_error": None if planned == 0 else round(abs(error) / planned * 100, 2),
|
||||
}
|
||||
rows.append({"period": period, "status": "compared", "metrics": metrics})
|
||||
return {
|
||||
"forecast_created_at": forecast["created_at"],
|
||||
"provider_id": actual["provider_id"],
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print(f"usage: {sys.argv[0]} FORECAST.json ACTUAL.json", file=sys.stderr)
|
||||
return 2
|
||||
forecast = json.loads(Path(sys.argv[1]).read_text())
|
||||
actual = json.loads(Path(sys.argv[2]).read_text())
|
||||
print(json.dumps(compare(forecast, actual), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue