--- id: CUST-WP-0005 type: workplan title: "State Hub v0.5 — Dynamic Domains & Multi-Repo" domain: custodian status: completed owner: custodian topic_slug: custodian state_hub_workstream_id: 2271eb55-62ca-4bcc-8d9f-f9aacd6922d6 created: "2026-02-28" updated: "2026-02-28" completed: "2026-02-28" --- # State Hub v0.5 — Dynamic Domains & Multi-Repo ## Summary Replace the hardcoded 6-domain Python enum with a first-class `domains` DB table, enabling new domains to be added and existing ones to be renamed without schema migrations. Alongside: introduce a `managed_repos` table so multiple git repositories can be registered per domain, with a corresponding registration workflow update. ## Context Domains are currently baked into a PostgreSQL ENUM type (`domain` with 6 fixed values). Adding a domain requires a schema migration; renaming is impossible at runtime. The registration workflow (`make register-project`) assumes one repository per domain. Neither constraint fits the actual project structure, where a domain may span multiple repos and the domain set itself should be open. ## Dependencies - Blocks: `CUST-WP-0003` (v0.3 Contribution Tracking) — `managed_repos` table, `/repos/` router, `register_repo` MCP tool, and base registration workflow in v0.3 are superseded by P2.1–P2.3 here. - state_hub_dependency_id: 2033172d-2462-4253-acb7-cb64c7432480 ## Phase 1 — Domain as a DB Entity ### P1.1 — Create `domains` table + Alembic migration ```task id: CUST-WP-0005-T01 state_hub_task_id: 456a0252-9a34-43a6-8244-c3a2caf95d51 status: done priority: high ``` New table: `domains` (id UUID PK, slug VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, description TEXT, status VARCHAR(20) DEFAULT 'active' CHECK IN ('active','archived'), created_at, updated_at). Migration (down_revision: a3f1c2d4e5b6): 1. Create `domains` table. 2. INSERT the 6 canonical rows from the existing enum values. 3. ADD COLUMN `domain_id UUID REFERENCES domains(id)` to `topics`. 4. UPDATE topics SET domain_id = (SELECT id FROM domains WHERE slug = domain::text). 5. ALTER TABLE topics ALTER COLUMN domain_id SET NOT NULL. 6. DROP COLUMN `domain` (old enum column) from topics. 7. DROP TYPE `domain` (the PostgreSQL enum type). 8. Leave EP and TD `domain` columns as String(50) — validated at API layer against live domains list, not via FK. Document as acceptable loose coupling (avoids cascading complexity). Downgrade: reverse all steps (recreate enum, repopulate, drop domain_id). ### P1.2 — Domain ORM model + Pydantic schemas ```task id: CUST-WP-0005-T02 state_hub_task_id: eddff1ae-b263-4d3e-af95-91b4c3c2ecea status: done priority: high ``` New file: `state-hub/api/models/domain.py` - `Domain(Base)`: id, slug, name, description, status, created_at, updated_at - Relationship: `topics: list[Topic]` Update `state-hub/api/models/topic.py`: - Remove `Domain(str, enum.Enum)` class - Change `domain` column → `domain_id: Mapped[uuid.UUID]` (FK to domains.id) - Add `domain: relationship(Domain)` backref New file: `state-hub/api/schemas/domain.py` - `DomainCreate`: slug, name, description? - `DomainRead`: id, slug, name, description, status, created_at, updated_at - `DomainUpdate`: name?, description?, status? - `DomainRename`: new_slug, new_name (cascades EP/TD string columns) ### P1.3 — Domain API router: list, get, create, rename, archive ```task id: CUST-WP-0005-T03 state_hub_task_id: b91cd0b5-c647-41e5-abf5-98f6f5eb3333 status: done priority: high ``` New file: `state-hub/api/routers/domains.py` Endpoints: - `GET /domains/` — list all (filter: ?status=active|archived|all) - `GET /domains/{slug}/` — get by slug with counts (topics, workstreams, EPs, TDs, repos) - `POST /domains/` — create new (slug: unique, lowercase, underscored) - `PATCH /domains/{slug}/rename` — update slug + name; cascade EP/TD string columns - `PATCH /domains/{slug}/archive` — soft-delete; 409 if active topics exist Register in `state-hub/api/main.py`. ### P1.4 — Update seed.py + TopicCreate schema for new domain model ```task id: CUST-WP-0005-T04 state_hub_task_id: 0825fa45-ec3a-43b5-8482-4a819db75c6a status: done priority: medium ``` Update `state-hub/scripts/seed.py`: - Insert 6 domains before topics (idempotent: ON CONFLICT DO NOTHING). - Topic insertion uses `domain_id` FK (lookup by slug) instead of enum. - Remove Domain enum import. Update `state-hub/api/schemas/topic.py`: - `TopicCreate.domain`: `Domain` enum → `str` (validated at router via DB lookup). - `TopicRead`: expose `domain_slug: str` from FK relationship. Update state router: any `Domain.custodian` etc. references → slug string. --- ## Phase 2 — Multi-Repo Support ### P2.1 — Create `managed_repos` table + migration ```task id: CUST-WP-0005-T05 state_hub_task_id: 50dc7eec-2b45-4a0d-b99e-27aca8a55a67 status: done priority: high ``` New table: `managed_repos` Columns: id UUID PK, domain_id UUID FK domains(id) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, name VARCHAR(200) NOT NULL, local_path TEXT, remote_url TEXT, description TEXT, status VARCHAR(20) DEFAULT 'active', topic_id UUID FK topics(id) NULLABLE, created_at, updated_at. Design: `domain_id` is the primary association (not `topic_id`) — a domain can have multiple repos without a topic for each. `topic_id` is optional, for workstream-level linkage. Coordinate with v0.3 P2.2: if v0.3 runs first, extend rather than replace. Add to `models/__init__.py` and register router. ### P2.2 — Repo API router: register, list, update, archive ```task id: CUST-WP-0005-T06 state_hub_task_id: daca1636-0187-4fb7-a515-8a2cf84f89d9 status: done priority: medium ``` New file: `state-hub/api/routers/repos.py` Endpoints: - `GET /repos/` — list all (filter: ?domain=slug) - `GET /repos/{slug}/` — get detail - `POST /repos/` — register (domain_slug or domain_id required) - `PATCH /repos/{slug}/` — update (name, local_path, remote_url, description, topic_id) - `PATCH /repos/{slug}/archive` — soft-delete Include repos list in `GET /domains/{slug}/` response. Schemas: `RepoCreate`, `RepoRead`, `RepoUpdate`. ### P2.3 — Update registration workflow: multi-repo + dynamic domains ```task id: CUST-WP-0005-T07 state_hub_task_id: 930fd3fa-2e1d-401c-9e33-0378f33cb14d status: done priority: medium ``` Update `state-hub/scripts/register_project.sh`: - Verify domain via `GET /domains/{slug}/` (not just topic lookup). - POST to `/repos/` after writing CLAUDE.md. - Support second repo for existing domain without overwriting CLAUDE.md (`--force` flag or existence check). Update `state-hub/Makefile`: - `make add-domain DOMAIN= NAME=` — POST /domains/ - `make rename-domain DOMAIN= NEW_SLUG= NEW_NAME=` — PATCH rename - `make add-repo DOMAIN= REPO_PATH=` — POST /repos/ + CLAUDE.md - `make list-repos DOMAIN=` — GET /repos/?domain=slug Update `state-hub/scripts/project_claude_md.template`: - Add section listing other known repos in the same domain. --- ## Phase 3 — MCP & Validation ### P3.1 — MCP tools: domain lifecycle + repo registration ```task id: CUST-WP-0005-T08 state_hub_task_id: e99e6bd7-f6f2-496f-acbf-d2b013f37c73 status: done priority: medium ``` Add to `state-hub/mcp_server/server.py`: 1. `list_domains(status="active")` — GET /domains/?status=… 2. `create_domain(slug, name, description?)` — POST /domains/ 3. `rename_domain(slug, new_slug, new_name)` — PATCH /domains/{slug}/rename 4. `archive_domain(slug)` — PATCH /domains/{slug}/archive 5. `list_domain_repos(domain_slug)` — GET /repos/?domain=… 6. `register_repo(domain_slug, name, local_path?, remote_url?, description?)` — POST /repos/ Update `state-hub/mcp_server/TOOLS.md` to document all 6 new tools. ### P3.2 — Live domain validation for EP/TD + domain stats in state summary ```task id: CUST-WP-0005-T09 state_hub_task_id: f2b352b6-4b9e-47b3-b629-48485245390e status: done priority: low ``` Replace hardcoded `VALID_DOMAINS` set in EP/TD routers with a per-request DB lookup: `get_valid_domain_slugs(db: AsyncSession) -> set[str]`. Return 422 with a helpful message for unknown domains. Add to `/state/summary` response: - `domains: list[DomainSummary]` — slug, name, repo_count, active_workstream_count, ep_count, td_count. Update global CLAUDE.md and project CLAUDE.md template to note `list_domains()` is now available. --- ## Phase 4 — Dashboard ### P4.1 — Dashboard: domains.md page ```task id: CUST-WP-0005-T10 state_hub_task_id: ddb91669-5297-45d9-8b5e-10213135a96d status: done priority: low ``` New file: `state-hub/dashboard/src/domains.md` Layout: - KPI row: total domains, total repos, newest domain. - Domain cards: name, slug, status badge, repo count, workstream count, EP count, TD count. - Repos sub-list per card: name, local_path, remote_url (linked), date. - Click → entity modal (extend `entity-modal.js` for domain type). Data loaders: - `src/data/domains.json.py` — GET /domains/?status=all - `src/data/repos.json.py` — GET /repos/ Add to `observablehq.config.js` nav. ### P4.2 — Dashboard: domain filter on workstreams, EP, TD pages ```task id: CUST-WP-0005-T11 state_hub_task_id: 3fec152c-00e1-4cdb-b6e5-26136c8cb6c1 status: done priority: low ``` Add `Inputs.select` domain filter to: - `workstreams.md` — filter by domain via topic.domain_slug - `extensions.md` — wire existing API domain filter to UI - `techdept.md` — wire existing API domain filter to UI Each filter: load domain list from `/domains/?status=active`, default "All", client-side reactive re-filter, URL hash persistence (`?domain=railiance`). Check if `WorkstreamRead` already exposes `domain_slug`; add if missing. --- ## Notes - v0.5 P2.1 supersedes the `managed_repos` design in v0.3 P2.2. v0.3 is annotated accordingly and depends on this workplan. - EP/TD `domain` columns remain `String(50)` (not FK) for simplicity. The rename endpoint cascades updates to these columns. - `sync_workplans.py` (future, v0.3 Phase 4) will be able to ingest this file and reconcile it with the DB rows created during the planning session. --- ## Closure Review — 2026-03-02 **Outcome:** All 11 tasks completed. No carry-forwards. No dropped tasks. **Context:** This workplan was created DB-first on 2026-02-28, before ADR-001 was formalised. The workplan file correctly recorded all tasks as `status: done`, but the DB rows were never synced from the file — they remained in their initial `todo` state in the database. The daily stale-task cleanup script (`scripts/cleanup_stale_tasks.py`) detected these 11 stale DB rows and cancelled them on 2026-03-02. No actual work was lost: all deliverables in Phase 1–4 were shipped as part of State Hub v0.5. ### Completed (DB updated at delivery time; file status = done) - CUST-WP-0005-T01 — Create `domains` table + Alembic migration - CUST-WP-0005-T02 — Domain ORM model + Pydantic schemas - CUST-WP-0005-T03 — Domain API router: list, get, create, rename, archive - CUST-WP-0005-T04 — Update seed.py + TopicCreate schema for new domain model - CUST-WP-0005-T05 — Create `managed_repos` table + migration - CUST-WP-0005-T06 — Repo API router: register, list, update, archive - CUST-WP-0005-T07 — Update registration workflow: multi-repo + dynamic domains - CUST-WP-0005-T08 — MCP tools: domain lifecycle + repo registration - CUST-WP-0005-T09 — Live domain validation for EP/TD + domain stats in state summary - CUST-WP-0005-T10 — Dashboard: domains.md page - CUST-WP-0005-T11 — Dashboard: domain filter on workstreams, EP, TD pages ### Cancelled (DB records only — legacy stale rows, not real cancellations) All 11 DB task rows were cancelled by the cleanup script. The workplan file was authoritative; the DB rows were artefacts of the pre-ADR-001 DB-first creation pattern. This does not reflect a change in work outcome.