Files
the-custodian/workplans/CUST-WP-0005-dynamic-domains.md
tegwick d96ed44c57 feat(maintenance): add stale-task cleanup scheme
- scripts/cleanup_stale_tasks.py: daily script that cancels open tasks
  in completed/archived workstreams; handles 307 redirects; emits a
  cleanup progress event summarising results
- Makefile: add cleanup-stale target (also suitable for cron)
- ADR-001: append Workstream Closure Protocol section — mandatory closure
  review before marking workstream completed, with task classification
  table (done/cancelled/carry-forward) and Closure Review file format
- WP-0002 + WP-0005: append Closure Review sections documenting the
  2026-03-02 cleanup run (26 stale DB rows cancelled — all were legacy
  pre-ADR-001 DB-first records; file status was already done)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 00:32:35 +01:00

333 lines
12 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: 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.1P2.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=<slug> NAME=<name>` — POST /domains/
- `make rename-domain DOMAIN=<old> NEW_SLUG=<s> NEW_NAME=<n>` — PATCH rename
- `make add-repo DOMAIN=<slug> REPO_PATH=<path>` — POST /repos/ + CLAUDE.md
- `make list-repos DOMAIN=<slug>` — 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 14 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.