diff --git a/workplans/STATE-WP-0062-service-catalog-two-dimensions.md b/workplans/STATE-WP-0062-service-catalog-two-dimensions.md new file mode 100644 index 0000000..1e67b8a --- /dev/null +++ b/workplans/STATE-WP-0062-service-catalog-two-dimensions.md @@ -0,0 +1,184 @@ +--- +id: STATE-WP-0062 +type: workplan +title: "Two-dimension service catalog (hosting × development) + Services nav section" +domain: custodian +repo: state-hub +status: proposed +owner: codex +topic_slug: custodian +created: "2026-06-19" +updated: "2026-06-19" +--- + +# 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: todo +priority: high +``` + +- [ ] `api/models/service_catalog.py`: `ServiceCatalog` core + `ServiceThirdParty`, + `ServiceFirstParty`, `ServiceCloud`, `ServiceSelfHosted` extensions. +- [ ] Alembic migration; register in `api/models/__init__.py`. +- [ ] 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: todo +priority: high +``` + +- [ ] `GET /services/catalog` with `hosting_type` / `development_type` / + `maturity_level` filters; `GET /services/{slug}` returns core + applicable + extensions. +- [ ] Write path to register/update a service and its extensions (generalise + `register_service`; keep a `third_party`-shaped compatibility wrapper). +- [ ] Keep `/tpsc/*` working as a `development_type=third_party` view. + +### T3 — Services nav section + four-quadrant pages + +```task +id: STATE-WP-0062-T03 +status: todo +priority: high +``` + +- [ ] Replace the top-level "Services (TPSC)" entry with a **Services** section. +- [ ] 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`. +- [ ] 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: todo +priority: medium +``` + +- [ ] `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: todo +priority: medium +``` + +- [ ] Tests: model + migration round-trip, dimension filters, TPSC back-compat + view, first-party↔repo link. +- [ ] Docs: `/docs/services` reference; update `/docs/tpsc` to point at the + generalised model. +- [ ] 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