Implement FIN-WP-0001 ledger, evaluate, and evidence lane (T03–T05)

Add SQLite-backed cost ledger with import/summary CLI, cron-friendly
evaluate command with federation emit, and dogfood evidence artefact
generation from fixture CSVs.
This commit is contained in:
tegwick 2026-07-08 22:44:07 +02:00
parent d6f88cd70a
commit 277c2d05ca
12 changed files with 760 additions and 7 deletions

View file

@ -14,9 +14,21 @@ from fin_hub.ingest.anthropic import parse_anthropic_billing_csv
from fin_hub.ingest.cloud import parse_cloud_cost_csv
from fin_hub.ingest.hosteurope import parse_hosteurope_csv
from fin_hub.services.alerts import evaluate_budget_alerts
from fin_hub.services.evaluate import evaluate_runway
from fin_hub.services.evidence import write_runway_evidence
from fin_hub.services.ledger import (
default_ledger_path,
import_csv,
ledger_stats_json,
set_opening_balance,
)
from fin_hub.services.runway import compute_runway
def _ledger_path(args: argparse.Namespace) -> Path:
return Path(args.ledger) if args.ledger else default_ledger_path()
def _cmd_import_cloud(args: argparse.Namespace) -> int:
rows = parse_cloud_cost_csv(Path(args.path))
print(json.dumps([row.__dict__ for row in rows], indent=2, default=str))
@ -82,6 +94,61 @@ def _cmd_ops_costs(args: argparse.Namespace) -> int:
return 0
def _cmd_ledger_import(args: argparse.Namespace) -> int:
result = import_csv(
Path(args.path),
args.type,
ledger_path=_ledger_path(args),
force=args.force,
)
print(json.dumps(result.as_dict(), indent=2))
return 0
def _cmd_ledger_summary(args: argparse.Namespace) -> int:
print(ledger_stats_json(ledger_path=_ledger_path(args)))
return 0
def _cmd_ledger_set_balance(args: argparse.Namespace) -> int:
set_opening_balance(_ledger_path(args), args.balance, currency=args.currency)
print(
json.dumps(
{"opening_balance": args.balance, "currency": args.currency},
indent=2,
)
)
return 0
def _cmd_evaluate(args: argparse.Namespace) -> int:
report = evaluate_runway(
ledger_path=_ledger_path(args),
opening_balance=args.balance,
alert_threshold_months=args.threshold,
currency=args.currency,
allocated=args.allocated,
spent=args.spent,
emit=args.emit,
api_base=args.api_base,
)
print(json.dumps(report, indent=2, default=str))
return 0
def _cmd_evidence(args: argparse.Namespace) -> int:
output = write_runway_evidence(
ledger_path=_ledger_path(args),
output_dir=Path(args.output_dir) if args.output_dir else None,
opening_balance=args.balance,
seed_fixtures=args.seed_fixtures,
alert_threshold_months=args.threshold,
currency=args.currency,
)
print(json.dumps({"written": str(output.resolve())}, indent=2))
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Fin Hub operator CLI")
sub = parser.add_subparsers(dest="command", required=True)
@ -113,6 +180,53 @@ def build_parser() -> argparse.ArgumentParser:
ops.add_argument("path")
ops.set_defaults(func=_cmd_ops_costs)
ledger = sub.add_parser("ledger", help="Persistent SQLite cost ledger")
ledger_sub = ledger.add_subparsers(dest="ledger_command", required=True)
ledger_import = ledger_sub.add_parser("import", help="Import a cost CSV into the ledger")
ledger_import.add_argument("type", choices=["cloud", "anthropic", "hosteurope"])
ledger_import.add_argument("path")
ledger_import.add_argument("--ledger", help="Ledger database path (default: .fin-hub/ledger.db)")
ledger_import.add_argument("--force", action="store_true", help="Re-import even if file unchanged")
ledger_import.set_defaults(func=_cmd_ledger_import)
ledger_summary = ledger_sub.add_parser("summary", help="Monthly rollup summary from ledger")
ledger_summary.add_argument("--ledger", help="Ledger database path (default: .fin-hub/ledger.db)")
ledger_summary.set_defaults(func=_cmd_ledger_summary)
ledger_balance = ledger_sub.add_parser("set-balance", help="Store opening cash balance in ledger meta")
ledger_balance.add_argument("balance", type=float)
ledger_balance.add_argument("--currency", default="EUR")
ledger_balance.add_argument("--ledger", help="Ledger database path (default: .fin-hub/ledger.db)")
ledger_balance.set_defaults(func=_cmd_ledger_set_balance)
evaluate = sub.add_parser(
"evaluate",
help="Evaluate runway from ledger burns (cron/systemd friendly)",
)
evaluate.add_argument("--ledger", help="Ledger database path (default: .fin-hub/ledger.db)")
evaluate.add_argument("--balance", type=float, help="Override stored opening balance")
evaluate.add_argument("--threshold", type=float, default=3.0)
evaluate.add_argument("--currency", default="EUR")
evaluate.add_argument("--allocated", type=float)
evaluate.add_argument("--spent", type=float)
evaluate.add_argument("--emit", action="store_true", help="Emit fin→dev and fin→canon signals")
evaluate.add_argument("--api-base")
evaluate.set_defaults(func=_cmd_evaluate)
evidence = sub.add_parser("evidence", help="Write runway dogfood evidence artefact")
evidence.add_argument("--ledger", help="Ledger database path (default: .fin-hub/ledger.db)")
evidence.add_argument("--balance", type=float, default=12000.0)
evidence.add_argument("--threshold", type=float, default=3.0)
evidence.add_argument("--currency", default="EUR")
evidence.add_argument("--output-dir", help="Output directory (default: docs/evidence)")
evidence.add_argument(
"--seed-fixtures",
action="store_true",
help="Import tests/fixtures CSVs before generating the report",
)
evidence.set_defaults(func=_cmd_evidence)
return parser