2026-06-19 20:24:20 +02:00
|
|
|
|
---
|
|
|
|
|
|
id: STATE-WP-0062
|
|
|
|
|
|
type: workplan
|
|
|
|
|
|
title: "Two-dimension service catalog (hosting × development) + Services nav section"
|
2026-06-22 23:16:28 +02:00
|
|
|
|
domain: infotech
|
2026-06-19 20:24:20 +02:00
|
|
|
|
repo: state-hub
|
2026-06-19 21:16:37 +02:00
|
|
|
|
status: finished
|
2026-06-19 20:24:20 +02:00
|
|
|
|
owner: codex
|
|
|
|
|
|
topic_slug: custodian
|
|
|
|
|
|
created: "2026-06-19"
|
|
|
|
|
|
updated: "2026-06-19"
|
2026-06-19 21:16:37 +02:00
|
|
|
|
state_hub_workstream_id: "b5c9d93f-9f5e-4d10-bb3b-90322c7419b7"
|
2026-06-19 20:24:20 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
2026-06-19 21:16:37 +02:00
|
|
|
|
status: done
|
2026-06-19 20:24:20 +02:00
|
|
|
|
priority: high
|
2026-06-19 21:16:37 +02:00
|
|
|
|
state_hub_task_id: "f74612f6-b442-4b5f-ac4d-7bc6d1d4883f"
|
2026-06-19 20:24:20 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] `api/models/service_catalog.py`: `ServiceCatalog` core + `ServiceThirdParty`,
|
2026-06-19 20:24:20 +02:00
|
|
|
|
`ServiceFirstParty`, `ServiceCloud`, `ServiceSelfHosted` extensions.
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Alembic migration; register in `api/models/__init__.py`.
|
|
|
|
|
|
- [x] Data migration: copy `tpsc_catalog` → `service_catalog` + `service_cloud`
|
2026-06-19 20:24:20 +02:00
|
|
|
|
with `(cloud_hosted, third_party)`; backfill `service_id` on TPSC entries.
|
|
|
|
|
|
|
|
|
|
|
|
### T2 — API + MCP
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: STATE-WP-0062-T02
|
2026-06-19 21:16:37 +02:00
|
|
|
|
status: done
|
2026-06-19 20:24:20 +02:00
|
|
|
|
priority: high
|
2026-06-19 21:16:37 +02:00
|
|
|
|
state_hub_task_id: "373cddfa-c85b-47a7-bb8e-c86e9341c237"
|
2026-06-19 20:24:20 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] `GET /services/catalog` with `hosting_type` / `development_type` /
|
2026-06-19 20:24:20 +02:00
|
|
|
|
`maturity_level` filters; `GET /services/{slug}` returns core + applicable
|
|
|
|
|
|
extensions.
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Write path to register/update a service and its extensions (generalise
|
2026-06-19 20:24:20 +02:00
|
|
|
|
`register_service`; keep a `third_party`-shaped compatibility wrapper).
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Keep `/tpsc/*` working as a `development_type=third_party` view.
|
2026-06-19 20:24:20 +02:00
|
|
|
|
|
|
|
|
|
|
### T3 — Services nav section + four-quadrant pages
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: STATE-WP-0062-T03
|
2026-06-19 21:16:37 +02:00
|
|
|
|
status: done
|
2026-06-19 20:24:20 +02:00
|
|
|
|
priority: high
|
2026-06-19 21:16:37 +02:00
|
|
|
|
state_hub_task_id: "b14bbbdd-347e-4f62-9ac3-ad42360fa766"
|
2026-06-19 20:24:20 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Replace the top-level "Services (TPSC)" entry with a **Services** section.
|
|
|
|
|
|
- [x] Pages: **Third Party** (current TPSC view), **First Party** (with internal
|
2026-06-19 20:24:20 +02:00
|
|
|
|
repo link + **Service Maturity Level** column), **Self Hosted** (three-helix
|
|
|
|
|
|
infra view). Each is a filtered view over `service_catalog`.
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Surface the two dimensions explicitly (e.g. a quadrant filter) so the four
|
2026-06-19 20:24:20 +02:00
|
|
|
|
classes are navigable.
|
|
|
|
|
|
|
|
|
|
|
|
### T4 — Terminology: Tier → Level (Service DoM only)
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: STATE-WP-0062-T04
|
2026-06-19 21:16:37 +02:00
|
|
|
|
status: done
|
2026-06-19 20:24:20 +02:00
|
|
|
|
priority: medium
|
2026-06-19 21:16:37 +02:00
|
|
|
|
state_hub_task_id: "1c296d7a-c090-430f-8d0d-27b548d88d9e"
|
2026-06-19 20:24:20 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] `policies/service-dom.md`: rename "Tier 1/2/3" → "Level 1/2/3"; column
|
2026-06-19 20:24:20 +02:00
|
|
|
|
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
|
2026-06-19 21:16:37 +02:00
|
|
|
|
status: done
|
2026-06-19 20:24:20 +02:00
|
|
|
|
priority: medium
|
2026-06-19 21:16:37 +02:00
|
|
|
|
state_hub_task_id: "2edd68ee-0d9a-431d-9891-73a0f6be6a41"
|
2026-06-19 20:24:20 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Tests: model + migration round-trip, dimension filters, TPSC back-compat
|
2026-06-19 20:24:20 +02:00
|
|
|
|
view, first-party↔repo link.
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Docs: `/docs/services` reference; update `/docs/tpsc` to point at the
|
2026-06-19 20:24:20 +02:00
|
|
|
|
generalised model.
|
2026-06-19 21:16:37 +02:00
|
|
|
|
- [x] Seed the known classes: TPSC SaaS (cloud/third-party), self-hosted OSS from
|
2026-06-19 20:24:20 +02:00
|
|
|
|
`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
|