Bootstrap fin-hub scaffold from hub-core (CUST-WP-0025-T22/T23)

Add hub-core editable dependency, fin-specific SQLAlchemy models, and smoke tests.
This commit is contained in:
tegwick 2026-07-08 00:50:24 +02:00
commit b6993d4a05
10 changed files with 1737 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.venv/
__pycache__/
*.pyc
.pytest_cache/
.env

8
INTENT.md Normal file
View file

@ -0,0 +1,8 @@
# Fin Hub Intent
Provide the **Financial Allocator** surface for the FOS federation: make
resource pressure, burn rate, and runway visible so dev-hub can deprioritize
work and canon can receive viability alerts when thresholds are breached.
Fin-hub does not execute financial transactions. It tracks, projects, and
signals.

28
README.md Normal file
View file

@ -0,0 +1,28 @@
# Fin Hub
Resource viability hub for the FOS federation — budgets, commitments, burn rate,
runway projection, and token spend tracking.
Fin-hub extends `hub-core` with financial models and read surfaces. Generic hub
primitives (domains, repos, messages, progress events) come from hub-core;
fin-specific models (budget, commitment, burn rate, runway, token spend) live
here.
## Status
Bootstrap scaffold (`CUST-WP-0025-T22`). Models and ingestion pipelines are
tracked in `CUST-WP-0025-T23``T24`.
## Quick Start
```bash
cd /home/worsch/fin-hub
uv sync
uv run pytest
```
## Related Workplans
- `the-custodian/workplans/CUST-WP-0025-fos-hub-bootstrap.md` — umbrella
- `canon/constitution/bootstrap-protocol_v0.1.md` — funding and roles
- `canon/projects/railiance/business-model-canvas_v0.1.md` — monetization path

15
SCOPE.md Normal file
View file

@ -0,0 +1,15 @@
# Fin Hub Scope
## In Scope
- Budget, commitment, burn rate, runway projection, and token spend models
- Manual CSV import for cloud and API costs (v0.1)
- Runway calculator with alert thresholds
- FOS §9 cross-hub signals: fin→dev (budget pressure), fin→ops (cost attribution), fin→canon (viability alerts)
- hub-core generic primitives (domains, repos, messages, progress)
## Out of Scope
- Payment execution, invoicing, or banking integration
- Tax filing or legal entity management
- Multi-tenant customer billing (deferred to RaaS T26)

31
pyproject.toml Normal file
View file

@ -0,0 +1,31 @@
[project]
name = "fin-hub"
version = "0.1.0"
description = "Financial viability hub for the FOS federation"
requires-python = ">=3.12"
dependencies = [
"hub-core",
"fastapi>=0.115.0",
"sqlalchemy[asyncio]>=2.0.0",
"pydantic>=2.10.0",
]
[tool.uv.sources]
hub-core = { path = "../hub-core", editable = true }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["fin_hub"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
[dependency-groups]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0",
]

3
src/fin_hub/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""Fin Hub — resource viability models and surfaces for the FOS federation."""
__version__ = "0.1.0"

View file

@ -0,0 +1,11 @@
"""Fin-specific SQLAlchemy models (T23 implementation target)."""
from fin_hub.models.budget import Budget, BurnRate, Commitment, RunwayProjection, TokenSpend
__all__ = [
"Budget",
"Commitment",
"BurnRate",
"RunwayProjection",
"TokenSpend",
]

View file

@ -0,0 +1,77 @@
"""Financial viability models — initial schema for CUST-WP-0025-T23."""
from __future__ import annotations
import uuid
from datetime import date, datetime
from sqlalchemy import Date, DateTime, Float, Integer, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from hub_core.models.base import Base, TimestampMixin
class Budget(Base, TimestampMixin):
__tablename__ = "fin_budgets"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
domain_slug: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
period_start: Mapped[date] = mapped_column(Date, nullable=False)
period_end: Mapped[date] = mapped_column(Date, nullable=False)
allocated: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
committed: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
spent: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
class Commitment(Base, TimestampMixin):
__tablename__ = "fin_commitments"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
domain_slug: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
commitment_type: Mapped[str] = mapped_column(String(32), nullable=False)
title: Mapped[str] = mapped_column(String(256), nullable=False)
amount: Mapped[float] = mapped_column(Float, nullable=False)
cadence: Mapped[str] = mapped_column(String(16), nullable=False, default="monthly")
start_date: Mapped[date] = mapped_column(Date, nullable=False)
end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
class BurnRate(Base, TimestampMixin):
__tablename__ = "fin_burn_rates"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
domain_slug: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
period_month: Mapped[str] = mapped_column(String(7), nullable=False, index=True)
actual_spend: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
projected_spend: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
class RunwayProjection(Base, TimestampMixin):
__tablename__ = "fin_runway_projections"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
current_balance: Mapped[float] = mapped_column(Float, nullable=False)
monthly_burn: Mapped[float] = mapped_column(Float, nullable=False)
months_remaining: Mapped[float] = mapped_column(Float, nullable=False)
alert_threshold_months: Mapped[float] = mapped_column(Float, nullable=False, default=3.0)
computed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
class TokenSpend(Base, TimestampMixin):
__tablename__ = "fin_token_spends"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
provider: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
model: Mapped[str] = mapped_column(String(64), nullable=False)
tokens_in: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
tokens_out: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
cost: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
session_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")

21
tests/test_models.py Normal file
View file

@ -0,0 +1,21 @@
"""Smoke tests for fin-hub model registration."""
from fin_hub.models import Budget, BurnRate, Commitment, RunwayProjection, TokenSpend
from hub_core.models.base import Base
def test_fin_models_register_on_metadata():
tables = {table.name for table in Base.metadata.sorted_tables}
assert "fin_budgets" in tables
assert "fin_commitments" in tables
assert "fin_burn_rates" in tables
assert "fin_runway_projections" in tables
assert "fin_token_spends" in tables
def test_model_classes_importable():
assert Budget.__tablename__ == "fin_budgets"
assert Commitment.__tablename__ == "fin_commitments"
assert BurnRate.__tablename__ == "fin_burn_rates"
assert RunwayProjection.__tablename__ == "fin_runway_projections"
assert TokenSpend.__tablename__ == "fin_token_spends"

1538
uv.lock generated Normal file

File diff suppressed because it is too large Load diff