feat(gems): three-pass schema migration aligning state-hub with GEMS

Implements CUST-WP-0007. Resolves inconsistencies I-1, I-2, I-5, I-6
identified in the GEMS audit (GenericEntityModellingSystem.md).

Pass 1 (e1f2a3b4c5d6): domain_id FK on extension_points and
technical_debt (replaces raw string column); repo_id FK on contributions.
Fixes domain-filtering bugs in EP/TD dashboard pages.

Pass 2 (f2a3b4c5d6e7): repo_id nullable FK on workstreams, aligning
the GEMS primary attachment with ADR-001 (repo > topic). Dashboard
pages updated to prefer repo->domain over topic->domain.

Pass 3 (a3b4c5d6e7f8): SBOMSnapshot container entity (GEMS Complex
between Repository and SBOMEntry). Ingest is now additive — each call
creates a new snapshot; history is retained. List/report endpoints
filter to latest snapshot per repo via _latest_snapshot_ids_subquery().
New endpoints: GET /sbom/snapshots/, GET /sbom/snapshots/{id}/.
Dashboard gains a Snapshot History section.

Also adds GEMS analysis artefacts: wiki/GEMS-StateHub-TypeRegistry.md,
wiki/GEMS-StateHub-SWOT.md, workplans/CUST-WP-0006 (analysis),
workplans/CUST-WP-0007 (migration, now completed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 23:39:17 +01:00
parent 23bf21d40d
commit 8ab6e6c9c5
35 changed files with 1838 additions and 84 deletions

View File

@@ -0,0 +1,134 @@
# GEMS State-Hub Type Registry
Domain-specific instantiation of the Generic Entity Modelling System
(`wiki/GenericEntityModellingSystem.md`) for the Custodian State Hub.
**Status:** Draft — subject to revision pending decision DEC-GEMS-001 through DEC-GEMS-006.
**Created:** 2026-03-02
---
## Hierarchy Overview
```
Ecosystem (implicit root — singleton)
└── Domain (Complex)
├── Topic (Complex) organizes focus areas
└── Repository (Complex)
├── Workstream (Complex) organizes tasks
├── SBOMSnapshot (Complex) organizes SBOM entries
├── Task (Atom) ← secondary: Workstream
├── Decision (Atom) ← secondary: Topic | Workstream
├── TechnicalDebt (Atom)
├── ExtensionPoint (Atom)
├── Contribution (Atom)
└── ProgressEvent (Atom) ← secondary: Workstream | Task | Decision
SBOMSnapshot
└── SBOMEntry (Atom)
DependsOn (Relation, primary=Domain)
from: Workstream → to: Workstream
```
---
## Type Registry Table
| Type | Kind | Primary Attachment Type | Allowed Secondary Attachments | Payload / Key Fields |
|---|---|---|---|---|
| **Ecosystem** | Complex | — (root) | — | name, description |
| **Domain** | Complex | Ecosystem | — | slug, name, status |
| **Topic** | Complex | Domain | — | slug, title, status |
| **Repository** | Complex | Domain | Topic (optional scope annotation) | slug, name, local_path, remote_url |
| **Workstream** | Complex | Repository | Topic (organizer) | slug, title, status, owner, due_date |
| **SBOMSnapshot** | Complex | Repository | — | snapshot_at, source |
| **Task** | Atom | Workstream | — | title, status, priority, assignee, due_date |
| **Decision** | Atom | Repository | Topic \| Workstream (context) | title, type, status, rationale, deadline |
| **TechnicalDebt** | Atom | Repository | Topic \| Workstream (context) | td_id, debt_type, severity, status |
| **ExtensionPoint** | Atom | Repository | Topic \| Workstream (context) | ep_id, ep_type, priority, status |
| **Contribution** | Atom | Repository | — | type, target_org, target_repo, status |
| **ProgressEvent** | Atom | Workstream | Task \| Decision (context) | summary, event_type, author |
| **SBOMEntry** | Atom | SBOMSnapshot | — | package_name, version, ecosystem, license_spdx |
| **DependsOn** | Relation | Domain | — | attachments[1]=from_ws, attachments[2]=to_ws, description |
---
## Validation Invariants
Following GEMS §5.2:
1. **Primary chain must be acyclic.** No entity may be its own ancestor via primary
attachments.
2. **Primary attachment kind/type must match the registry.** A Task's primary must be
a Workstream; a Workstream's primary must be a Repository, etc.
3. **Context-consistency for secondary attachments.** If Task has a secondary attachment
to a Workstream, that Workstream's primary must be the same Repository as the Task's
context (inherited via Workstream.primary).
4. **Relation endpoint types must match the relation's type definition.** DependsOn.from
and DependsOn.to must both be Workstream entities within the same Domain.
5. **Relation primary must be a Complex.** DependsOn.primary = Domain (the relation-space
that "owns" the inter-workstream dependency graph).
---
## Mapping: Current Tables → GEMS
| Current Table | Target GEMS Type | Status | Change Required |
|---|---|---|---|
| `domains` | Domain (Complex) | Correct | None |
| `topics` | Topic (Complex) | Correct | None |
| `managed_repos` | Repository (Complex) | Mostly correct | Remove nullable topic_id; add optional secondary |
| `workstreams` | Workstream (Complex) | **Broken** | Change primary from topic_id to repo_id |
| `tasks` | Task (Atom) | Correct | None |
| `decisions` | Decision (Atom) | Ambiguous | Change primary to repo_id; topic/workstream become secondaries |
| `technical_debt` | TechnicalDebt (Atom) | **Broken** | domain string → repo_id FK |
| `extension_points` | ExtensionPoint (Atom) | **Broken** | domain string → repo_id FK |
| `contributions` | Contribution (Atom) | Incomplete | Add repo_id FK |
| `progress_events` | ProgressEvent (Atom) | Ambiguous | Clarify primary vs. secondary |
| `sbom_entries` | SBOMEntry (Atom) | **Broken** | Add SBOMSnapshot container |
| `workstream_dependencies` | DependsOn (Relation) | Acceptable | Consider Relation entity model |
| *(missing)* | SBOMSnapshot (Complex) | **Missing** | New table required |
| *(missing)* | Ecosystem (Complex) | Missing | Optional singleton |
---
## Query Patterns Enabled
After full GEMS alignment:
```
# All workstreams in a domain
Workstream WHERE primary.primary.slug = "railiance"
# All open tasks for a given repo
Task WHERE primary.primary.slug = "activity-core" AND status != "done"
# Dependency graph for a domain
DependsOn WHERE primary.slug = "custodian"
# SBOM history for a repo
SBOMSnapshot WHERE primary.slug = "the-custodian" ORDER BY snapshot_at DESC
# All tech debt in a domain (currently broken — domain is a string)
TechnicalDebt WHERE primary.primary.slug = "custodian"
```
---
## Notes
- `Topic` and `Repository` are both children of `Domain` but are distinct organizers.
Topic = "focus area / project agenda"; Repository = "git repo / code artefact boundary".
A Topic may align with one or more repositories, but neither owns the other.
- `Workstream` moving from Topic → Repository is the most disruptive change. It resolves
the ADR-001 contradiction (workplans must live in repos, but workstreams live under topics).
- `ProgressEvent` retains its multi-attach flexibility (topic, workstream, task, decision)
but those become secondary attachments. The primary should be the Workstream (or Repository
if no workstream context).