Files
state-hub/workplans/STATE-WP-0062-service-catalog-two-dimensions.md
tegwick ce82ada0fa STATE-WP-0062 T5: docs, first-party↔repo test, mark workplan finished
- Add /docs/services reference (two-dimension model, persistence, API) and a
  pointer note from /docs/tpsc; add it to the Reference nav.
- Add a test asserting first_party.repo_slug resolves to a managed_repos FK
  (8 services tests green).
- Mark STATE-WP-0062 tasks done / status finished.

Known classes seeded in the live catalog via the API (Gitea, Postgres as
self-hosted/third-party; State Hub as self-hosted/first-party at Level 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:16:37 +02:00

191 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: STATE-WP-0062
type: workplan
title: "Two-dimension service catalog (hosting × development) + Services nav section"
domain: custodian
repo: state-hub
status: finished
owner: codex
topic_slug: custodian
created: "2026-06-19"
updated: "2026-06-19"
state_hub_workstream_id: "b5c9d93f-9f5e-4d10-bb3b-90322c7419b7"
---
# STATE-WP-0062 — Two-dimension service catalog + Services nav section
**Origin:** UI request to reorganise the single "Services (TPSC)" page into a
**Services** section. During refinement the model was generalised: services vary
along **two independent dimensions**, not one list of three, so the catalog and
its persistence are restructured to reflect that.
## Concept
Two orthogonal dimensions classify every service:
- **Hosting** — `self_hosted` (coulomb operates the service) vs `cloud_hosted`
(coulomb consumes someone else's running service).
- **Development** — `first_party` (coulomb is development-responsible) vs
`third_party` (coulomb is not development-responsible).
Their product gives four classes:
| | third-party | first-party |
|---|---|---|
| **cloud-hosted** | SaaS/APIs (today's TPSC) | our service deployed to a cloud |
| **self-hosted** | OSS we run (Gitea, Postgres…) | our service on coulomb infra |
**Design principle:** the "extra data" attaches to a **dimension, not a class**,
so extension data composes (a self-hosted first-party service gets the
self-hosted *and* the first-party extensions) rather than forcing four bespoke
record shapes.
## Persistence model (decided)
A single common **`service_catalog`** core table plus per-dimension extension
tables (each 1:1, optional, keyed by `service_id`):
- **`service_catalog`** (core, all services):
`id, slug (unique), name, owner_or_provider, category, description,
website_url, status (active|deprecated), hosting_type (self_hosted|cloud_hosted),
development_type (first_party|third_party), maturity_level (1..3 | null — the
Service DoM Level), created_at, updated_at`.
- **`service_third_party`** (development_type = third_party): upstream package
references, upstream service/support contacts, source URL, license.
- **`service_first_party`** (development_type = first_party): FK to the internal
dev repo (`managed_repos.id`), owning domain.
- **`service_cloud`** (hosting_type = cloud_hosted): GDPR maturity, DPA available,
ToS/privacy URLs, data-processing regions, retention notes — the existing TPSC
data-processor block (relevant whenever data leaves coulomb infra,
independent of who built the service).
- **`service_self_hosted`** (hosting_type = self_hosted): three-helix
instance/host, deployment ref, runbook ref, upstream OSS project (when
third-party).
> **Read-model boundary (ADR-001):** catalog writes are administrative
> bootstrap data (like the TPSC catalog today), not derived state. They reuse the
> existing TPSC write surface pattern; no new sanctioned read-model write is
> introduced.
## Migration & back-compat
- Existing `tpsc_catalog` rows migrate into `service_catalog` as
`(hosting_type=cloud_hosted, development_type=third_party)`; the GDPR/DPA/
ToS/privacy/region/retention fields move into `service_cloud`.
- `tpsc_entries` / `tpsc_snapshots` (per-repo dependency declarations) keep
pointing at the catalog via `service_id`; the `/tpsc/*` endpoints remain as a
filtered view (`development_type=third_party`) so existing ingestion and the
current page keep working during transition.
## Open questions (resolve in T1, do not block proposal)
- Whether to generalise the `tpsc_catalog` table in place vs. introduce
`service_catalog` and keep `tpsc_catalog` as a view/alias. Lean: new
`service_catalog`, repoint TPSC reads.
- `maturity_level` source of truth: operator-set vs. derived from a future
`check-dom` evaluation. Start operator-set; leave room to derive later.
---
## Tasks
### T1 — Core + extension data model and migration
```task
id: STATE-WP-0062-T01
status: done
priority: high
state_hub_task_id: "f74612f6-b442-4b5f-ac4d-7bc6d1d4883f"
```
- [x] `api/models/service_catalog.py`: `ServiceCatalog` core + `ServiceThirdParty`,
`ServiceFirstParty`, `ServiceCloud`, `ServiceSelfHosted` extensions.
- [x] Alembic migration; register in `api/models/__init__.py`.
- [x] Data migration: copy `tpsc_catalog``service_catalog` + `service_cloud`
with `(cloud_hosted, third_party)`; backfill `service_id` on TPSC entries.
### T2 — API + MCP
```task
id: STATE-WP-0062-T02
status: done
priority: high
state_hub_task_id: "373cddfa-c85b-47a7-bb8e-c86e9341c237"
```
- [x] `GET /services/catalog` with `hosting_type` / `development_type` /
`maturity_level` filters; `GET /services/{slug}` returns core + applicable
extensions.
- [x] Write path to register/update a service and its extensions (generalise
`register_service`; keep a `third_party`-shaped compatibility wrapper).
- [x] Keep `/tpsc/*` working as a `development_type=third_party` view.
### T3 — Services nav section + four-quadrant pages
```task
id: STATE-WP-0062-T03
status: done
priority: high
state_hub_task_id: "b14bbbdd-347e-4f62-9ac3-ad42360fa766"
```
- [x] Replace the top-level "Services (TPSC)" entry with a **Services** section.
- [x] Pages: **Third Party** (current TPSC view), **First Party** (with internal
repo link + **Service Maturity Level** column), **Self Hosted** (three-helix
infra view). Each is a filtered view over `service_catalog`.
- [x] Surface the two dimensions explicitly (e.g. a quadrant filter) so the four
classes are navigable.
### T4 — Terminology: Tier → Level (Service DoM only)
```task
id: STATE-WP-0062-T04
status: done
priority: medium
state_hub_task_id: "1c296d7a-c090-430f-8d0d-27b548d88d9e"
```
- [x] `policies/service-dom.md`: rename "Tier 1/2/3" → "Level 1/2/3"; column
header "Service Maturity Level". **Do not** touch the DoI tier subsystem
(`repos.md`, `check_doi.py`, `doi_cache` migration) — that is a separate,
established concept.
### T5 — Tests, docs, seed
```task
id: STATE-WP-0062-T05
status: done
priority: medium
state_hub_task_id: "2edd68ee-0d9a-431d-9891-73a0f6be6a41"
```
- [x] Tests: model + migration round-trip, dimension filters, TPSC back-compat
view, first-party↔repo link.
- [x] Docs: `/docs/services` reference; update `/docs/tpsc` to point at the
generalised model.
- [x] Seed the known classes: TPSC SaaS (cloud/third-party), self-hosted OSS from
`tools.md` (Gitea, Postgres — self-hosted/third-party), and State Hub itself
(self-hosted/first-party).
---
## Acceptance
- Every service carries an explicit `hosting_type` and `development_type`; the
four classes are queryable and navigable.
- Common fields live once in `service_catalog`; dimension-specific data lives in
composable extension tables (third-party upstream contacts, first-party repo
link, cloud data-processing/GDPR, self-hosted infra).
- The existing TPSC page and ingestion keep working through the back-compat view.
- The Services nav section exposes Third Party / First Party / Self Hosted, with a
Service Maturity **Level** column on First Party.
- The Service DoM policy uses "Level", not "Tier".
## See also
- `api/models/tpsc.py`, `api/routers/tpsc.py` — prior art being generalised
- `policies/service-dom.md` — Service Definition of Mature (Level source)
- `dashboard/src/tools.md` — current static self-hosted listing (seed source)
- `policies/repo-doi.md` / `dashboard/src/repos.md` — the DoI **tier** subsystem
that is intentionally left unchanged