generated from coulomb/repo-seed
feat(classification-spine): implement STATE-WP-0065 repo-anchored model
Replace the ad-hoc coordination-domain spine with the Repo Classification Standard: 14 market domains, classification columns on managed_repos, and workplans anchored by repo_id (topic_id optional). - Add Alembic migration d8e9f0a1b2c3 with data backfill and workstream→workplan rename - Add api/classification.py validation and register-from-classification tooling - Expose workplan-first REST/MCP surface with legacy workstream aliases - Add C-24 consistency rule and legacy domain frontmatter mapping - Update dashboard repos page with category/capability/stake filters - Update orientation docs; mark STATE-WP-0065 finished
This commit is contained in:
@@ -4,27 +4,36 @@ title: Domains — Reference
|
||||
|
||||
# Domains — Reference
|
||||
|
||||
The Domains page shows all registered project domains and the repositories
|
||||
associated with each one. Domains are the top-level organisational unit of the
|
||||
Custodian ecosystem.
|
||||
The Domains page shows the **14 fixed market domains** from the Repo
|
||||
Classification Standard. These replaced the old ad-hoc coordination domains
|
||||
(custodian, railiance, markitect, …) in STATE-WP-0065.
|
||||
|
||||
---
|
||||
|
||||
## What is a domain?
|
||||
|
||||
A domain corresponds to one of the six tracked project areas:
|
||||
A domain is an intended **market / user segment** — not a project org unit.
|
||||
Each registered repo has exactly one primary domain (from its
|
||||
`.repo-classification.yaml`), stored on `managed_repos.domain_id`.
|
||||
|
||||
| Slug | Project |
|
||||
| Slug | Segment |
|
||||
|------|---------|
|
||||
| `custodian` | The Custodian agent system itself |
|
||||
| `railiance` | DevOps & infrastructure reliability |
|
||||
| `markitect` | Knowledge artifact management |
|
||||
| `coulomb_social` | Co-creation marketplace |
|
||||
| `personhood` | Rights & obligations framework |
|
||||
| `foerster_capabilities` | Agency capability taxonomy |
|
||||
| `infotech` | Developers, platforms, internal tooling users |
|
||||
| `financials` | Finance, trading, payments |
|
||||
| `communication` | Messaging, social, collaboration |
|
||||
| `consumer` | General consumers |
|
||||
| `health` | Healthcare, wellness |
|
||||
| `industrials` | Manufacturing, logistics |
|
||||
| `energy` | Energy sector |
|
||||
| `utilities` | Utilities infrastructure |
|
||||
| `materials` | Materials / commodities |
|
||||
| `realestate` | Property, housing |
|
||||
| `crypto` | Crypto / web3 |
|
||||
| `agents` | AI-native agent users |
|
||||
| `space` | Space industry |
|
||||
| `government` | Civic, public sector |
|
||||
|
||||
Each domain has a slug (URL-friendly identifier), a human-readable name, an
|
||||
optional description, and a status.
|
||||
Canon: `the-custodian/canon/standards/repo-classification-standard_v1.0.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -32,63 +41,21 @@ optional description, and a status.
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| **active** | Live domain — topics, workstreams, and tasks are being tracked |
|
||||
| **archived** | Soft-deleted; no active work. Fails to archive if active topics exist |
|
||||
| **active** | Live domain — repos and workplans may reference it |
|
||||
| **archived** | Retired; no new registrations |
|
||||
|
||||
---
|
||||
|
||||
## KPI row
|
||||
## Relationship to repos and workplans
|
||||
|
||||
Four counters at the top of the page:
|
||||
|
||||
| Counter | Meaning |
|
||||
|---------|---------|
|
||||
| Total domains | All registered domains regardless of status |
|
||||
| Active | Domains with status `active` |
|
||||
| Total repos | Sum of all registered repositories across all domains |
|
||||
| Newest domain | Name of the most recently created domain |
|
||||
- **Repos** are the primary anchor — classification file is source of truth.
|
||||
- **Workplans** require `repo_id`; market domain is derived from the repo.
|
||||
- **Topics** are optional legacy tags; workplan frontmatter `domain:` may still
|
||||
use old coordination slugs — the consistency checker maps these to market domains.
|
||||
|
||||
---
|
||||
|
||||
## Domain cards
|
||||
## Related
|
||||
|
||||
One card per domain showing:
|
||||
|
||||
- **Slug** — monospace identifier
|
||||
- **Status badge** — green `active` or grey `archived`
|
||||
- **Name** — display name
|
||||
- **Description** — first 160 characters
|
||||
- **Repos** — list of registered repositories for this domain, each showing name, local path, and remote URL
|
||||
|
||||
---
|
||||
|
||||
## RecentlyOnScope
|
||||
|
||||
The `Domains / RecentlyOnScope` page generates deterministic Markdown digests
|
||||
for a selected domain. The range parameter defaults to `1h` and accepts compact
|
||||
durations such as `15m`, `6h`, or `1d`.
|
||||
|
||||
Generated reports are written under the configured State Hub report directory,
|
||||
defaulting to `reports/recently-on-scope/<domain_slug>/`. The dashboard lists
|
||||
those Markdown files and previews the raw report content.
|
||||
|
||||
---
|
||||
|
||||
## Managing domains
|
||||
|
||||
Via MCP:
|
||||
|
||||
```
|
||||
create_domain(slug="my_project", name="My Project", description="…")
|
||||
rename_domain(slug="old_slug", new_slug="new_slug", new_name="New Name")
|
||||
archive_domain(slug="my_project") # fails if active topics exist
|
||||
```
|
||||
|
||||
Via Makefile:
|
||||
|
||||
```bash
|
||||
make add-domain SLUG=my_project NAME="My Project"
|
||||
make rename-domain OLD_SLUG=my_project NEW_SLUG=myproject NEW_NAME="My Project"
|
||||
```
|
||||
|
||||
*Domains are never hard-deleted — only archived.*
|
||||
- **[Repos](/docs/repos)** — portfolio view with category / capability filters
|
||||
- **[Repo Integration](/docs/repo-integration)** — onboarding with classification file
|
||||
@@ -5,18 +5,25 @@ title: Repos — Reference
|
||||
# Repos — Reference
|
||||
|
||||
The Repos page shows every repository registered in the Custodian ecosystem,
|
||||
their SBOM ingestion status, and a domain-grouped coverage map.
|
||||
their **classification** (category, market domain, capabilities, business stake),
|
||||
SBOM ingestion status, and a domain-grouped coverage map.
|
||||
|
||||
---
|
||||
|
||||
## What is a managed repo?
|
||||
|
||||
A managed repo is a git repository that has been registered with the state hub
|
||||
via `custodian register-project` or `register_repo()`. Registration records the
|
||||
repo's slug, domain, local path, and optional remote URL. Once registered, the
|
||||
repo receives a `CLAUDE.custodian.md` integration suggestion, an onboarding
|
||||
workstream with 4 tasks for the repo agent, and is eligible for SBOM ingestion
|
||||
and the ADR-001 workplan validator.
|
||||
A managed repo is a git repository registered with State Hub. Registration is
|
||||
**classification-driven**:
|
||||
|
||||
1. Commit `.repo-classification.yaml` per the Repo Classification Standard.
|
||||
2. Run `make register-from-classification REPO=<slug>` (or use the MCP tool
|
||||
`register_repo_from_classification`).
|
||||
|
||||
The file is the source of truth; the hub stores a validated copy on
|
||||
`managed_repos` (category, domain, capability_tags, business_stake, provenance).
|
||||
|
||||
Legacy `custodian register-project` still works for agent onboarding but should
|
||||
be followed by classification registration.
|
||||
|
||||
For the full onboarding journey see **[Repo Integration](/docs/repo-integration)**.
|
||||
|
||||
@@ -27,69 +34,56 @@ For the full onboarding journey see **[Repo Integration](/docs/repo-integration)
|
||||
| Card | Meaning |
|
||||
|------|---------|
|
||||
| **Registered Repos** | Active repos only (status = active) |
|
||||
| **Domains** | Count of distinct domain slugs across registered repos |
|
||||
| **Market Domains** | Distinct primary domains across registered repos |
|
||||
| **Categories** | Distinct work categories (experimental, tooling, product, …) |
|
||||
| **SBOM Ingested** | Repos with at least one SBOM snapshot |
|
||||
| **SBOM Gaps** | Repos with no ingested SBOM — red border when > 0 |
|
||||
|
||||
---
|
||||
|
||||
## Coverage Map
|
||||
## Portfolio by Category
|
||||
|
||||
Groups repos by domain. Each domain block shows:
|
||||
|
||||
- **Domain name** with SBOM, EP, and TD chip indicators
|
||||
- **SBOM chip** — green ✓ if all repos in the domain are ingested, amber ⚠ if any gap exists
|
||||
- **EPs chip** — count of open/in-progress extension points for this domain
|
||||
- **TDs chip** — count of open/in-progress technical debt items for this domain
|
||||
- **Repo table** — one row per repo with SBOM status, package count, and local path
|
||||
|
||||
Rows with no SBOM are highlighted in amber.
|
||||
Groups repos by `category` (experimental, research, project, tooling, product,
|
||||
business). Each block shows domain, capabilities, business stake, and who
|
||||
classified the repo (`human` vs `migration`).
|
||||
|
||||
---
|
||||
|
||||
## Filters
|
||||
## Coverage Map
|
||||
|
||||
Groups repos by **market domain**. Each domain block shows SBOM, EP, and TD
|
||||
chips plus per-repo classification columns.
|
||||
|
||||
---
|
||||
|
||||
## Filters (All Repos Table)
|
||||
|
||||
| Filter | Effect |
|
||||
|--------|--------|
|
||||
| **Domain** | Show repos for a single domain only |
|
||||
| **Gaps only** | Toggle to show only repos without an ingested SBOM |
|
||||
| **Market domain** | Primary domain slug |
|
||||
| **Category** | Repo work category |
|
||||
| **Capability** | Repos tagged with a capability |
|
||||
| **Business stake** | Repos affecting a business responsibility area |
|
||||
| **DoI tier** | Definition of Integrated tier |
|
||||
| **Gaps only** | Repos without ingested SBOM |
|
||||
|
||||
---
|
||||
|
||||
## Consistency (C-24)
|
||||
|
||||
The ADR-001 consistency checker warns when a registered repo lacks a valid
|
||||
`.repo-classification.yaml` on disk. Migration-derived rows (`classified_by:
|
||||
migration`) get an explanatory note until a human-reviewed file is committed.
|
||||
|
||||
---
|
||||
|
||||
## Onboarding a new repo
|
||||
|
||||
See **[Repo Integration](/docs/repo-integration)** for the full journey.
|
||||
|
||||
Quick reference:
|
||||
Use the **Add Repo** form or:
|
||||
|
||||
```bash
|
||||
# From the repo root — registers, writes CLAUDE.custodian.md, creates onboarding tasks
|
||||
custodian register-project --domain <slug>
|
||||
```
|
||||
|
||||
## Ingesting a repo's SBOM
|
||||
|
||||
```bash
|
||||
# Auto-detects lockfile at repo root
|
||||
cd ~/state-hub
|
||||
make ingest-sbom REPO=<slug> REPO_PATH=/absolute/path
|
||||
|
||||
# Multi-ecosystem repo — scan all lockfiles recursively
|
||||
make ingest-sbom REPO=<slug> SCAN=1 REPO_PATH=/absolute/path
|
||||
```
|
||||
|
||||
Supported lockfile formats: `uv.lock`, `requirements.txt`, `package-lock.json`,
|
||||
`yarn.lock`, `Cargo.lock`, `.terraform.lock.hcl`.
|
||||
|
||||
---
|
||||
|
||||
## Infra-only repos
|
||||
|
||||
Repos with no lockfile (Ansible, shell scripts) can be registered for inventory
|
||||
purposes. The SBOM gap is expected and can be left as-is. Terraform providers
|
||||
are auto-detected via `.terraform.lock.hcl` when using `--scan`.
|
||||
|
||||
---
|
||||
|
||||
*SBOM snapshots are replaced on each ingest — not appended. The last ingestion
|
||||
timestamp is recorded on the managed_repo row.*
|
||||
# 1. Author classification file in the repo
|
||||
# 2. Register / reclassify
|
||||
make register-from-classification PATH=/path/to/repo
|
||||
make fix-consistency REPO=<slug>
|
||||
```
|
||||
@@ -102,14 +102,28 @@ const repoRows = repos
|
||||
const integrating = !!integratingBySlug[r.slug];
|
||||
const doiEntry = doiBySlug[r.slug] ?? null;
|
||||
const doiTier = doiEntry?.tier ?? "none";
|
||||
const category = r.category ?? "—";
|
||||
const capList = r.capability_tags ?? [];
|
||||
const stakeList = r.business_stake ?? [];
|
||||
const capTags = capList.length
|
||||
? capList.slice(0, 3).join(", ") + (capList.length > 3 ? "…" : "")
|
||||
: "—";
|
||||
const classifiedBy = r.classified_by ?? "—";
|
||||
return {
|
||||
_id: r.id,
|
||||
_domSlug: domSlug,
|
||||
_category: category,
|
||||
_capList: capList,
|
||||
_stakeList: stakeList,
|
||||
_hasSbom: hasSbom,
|
||||
_integrating: integrating,
|
||||
_doiTier: doiTier,
|
||||
repo: r.slug,
|
||||
domain: domName,
|
||||
category: category,
|
||||
capTags: capTags,
|
||||
businessStake: stakeList.length ? stakeList.slice(0, 3).join(", ") : "—",
|
||||
classifiedBy: classifiedBy,
|
||||
status: integrating ? "⚙ integrating" : "ready",
|
||||
path: r.local_path ?? "—",
|
||||
sbom: hasSbom ? `✓ ${lastScan}` : "⚠ not ingested",
|
||||
@@ -153,9 +167,13 @@ display(html`<div class="kpi-row">
|
||||
<p class="big-num">${repoRows.length}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Domains</h3>
|
||||
<h3>Market Domains</h3>
|
||||
<p class="big-num">${new Set(repoRows.map(r => r._domSlug)).size}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Categories</h3>
|
||||
<p class="big-num">${new Set(repoRows.map(r => r._category).filter(c => c !== "—")).size}</p>
|
||||
</div>
|
||||
<div class="card ${integratingCount > 0 ? 'card-integrating' : ''}">
|
||||
<h3>Integrating</h3>
|
||||
<p class="big-num">${integratingCount}</p>
|
||||
@@ -240,6 +258,8 @@ if (domainBlocks.length === 0) {
|
||||
<table class="repo-table">
|
||||
<thead><tr>
|
||||
<th>Repo</th>
|
||||
<th>Category</th>
|
||||
<th>Capabilities</th>
|
||||
<th>DoI Tier</th>
|
||||
<th>Status</th>
|
||||
<th>SBOM</th>
|
||||
@@ -249,6 +269,8 @@ if (domainBlocks.length === 0) {
|
||||
<tbody>
|
||||
${rows.map(r => html`<tr class="${r._integrating ? 'row-integrating' : r._hasSbom ? '' : 'row-gap'}">
|
||||
<td class="repo-cell"><code>${r.repo}</code></td>
|
||||
<td>${r.category}</td>
|
||||
<td class="path-cell" title=${r.capTags}>${r.capTags}</td>
|
||||
<td>${_doiBadge(r._doiTier)}</td>
|
||||
<td>${r._integrating
|
||||
? html`<span class="chip chip-integrating">⚙ integrating</span>`
|
||||
@@ -266,25 +288,76 @@ if (domainBlocks.length === 0) {
|
||||
}
|
||||
```
|
||||
|
||||
## Portfolio by Category
|
||||
|
||||
```js
|
||||
const byCategory = {};
|
||||
for (const r of repoRows) {
|
||||
const key = r._category === "—" ? "unclassified" : r._category;
|
||||
(byCategory[key] = byCategory[key] ?? []).push(r);
|
||||
}
|
||||
const categoryBlocks = Object.entries(byCategory).sort(([a], [b]) => a.localeCompare(b));
|
||||
if (categoryBlocks.length > 0) {
|
||||
display(html`<h2 style="margin-top:2rem">Portfolio by Category</h2>
|
||||
<div class="domain-list">
|
||||
${categoryBlocks.map(([cat, rows]) => html`
|
||||
<div class="domain-block">
|
||||
<div class="domain-header">
|
||||
<span class="domain-name">${cat}</span>
|
||||
<span class="domain-chips">
|
||||
<span class="chip chip-neutral">${rows.length} repo(s)</span>
|
||||
<span class="chip chip-neutral">${new Set(rows.map(r => r._domSlug)).size} domain(s)</span>
|
||||
</span>
|
||||
</div>
|
||||
<table class="repo-table">
|
||||
<thead><tr>
|
||||
<th>Repo</th><th>Domain</th><th>Capabilities</th><th>Business stake</th><th>Classified</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${rows.map(r => html`<tr>
|
||||
<td class="repo-cell"><code>${r.repo}</code></td>
|
||||
<td>${r.domain}</td>
|
||||
<td class="path-cell">${r.capTags}</td>
|
||||
<td class="path-cell">${r.businessStake}</td>
|
||||
<td>${r.classifiedBy}</td>
|
||||
</tr>`)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`)}
|
||||
</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
## All Repos Table
|
||||
|
||||
```js
|
||||
const domainFilter = Inputs.select(["all", ...new Set(repoRows.map(r => r._domSlug)).values()], {label: "Domain", value: "all"});
|
||||
const doiFilter = Inputs.select(["all", "none", "core", "standard", "full"], {label: "DoI tier", value: "all"});
|
||||
const gapFilter = Inputs.toggle({label: "Gaps only (no SBOM)", value: false});
|
||||
display(html`<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem">${domainFilter}${doiFilter}${gapFilter}</div>`);
|
||||
const domainFilter = Inputs.select(["all", ...new Set(repoRows.map(r => r._domSlug)).values()], {label: "Market domain", value: "all"});
|
||||
const categoryFilter = Inputs.select(["all", ...new Set(repoRows.map(r => r._category).filter(c => c !== "—")).values()], {label: "Category", value: "all"});
|
||||
const capabilityFilter = Inputs.select(["all", ...new Set(repoRows.flatMap(r => r._capList)).values()].sort(), {label: "Capability", value: "all"});
|
||||
const stakeFilter = Inputs.select(["all", ...new Set(repoRows.flatMap(r => r._stakeList)).values()].sort(), {label: "Business stake", value: "all"});
|
||||
const doiFilter = Inputs.select(["all", "none", "core", "standard", "full"], {label: "DoI tier", value: "all"});
|
||||
const gapFilter = Inputs.toggle({label: "Gaps only (no SBOM)", value: false});
|
||||
display(html`<div class="filter-bar">${domainFilter}${categoryFilter}${capabilityFilter}${stakeFilter}${doiFilter}${gapFilter}</div>`);
|
||||
```
|
||||
|
||||
```js
|
||||
const filteredRows = repoRows.filter(r =>
|
||||
(domainFilter.value === "all" || r._domSlug === domainFilter.value) &&
|
||||
(doiFilter.value === "all" || r._doiTier === doiFilter.value) &&
|
||||
(categoryFilter.value === "all" || r._category === categoryFilter.value) &&
|
||||
(capabilityFilter.value === "all" || r._capList.includes(capabilityFilter.value)) &&
|
||||
(stakeFilter.value === "all" || r._stakeList.includes(stakeFilter.value)) &&
|
||||
(doiFilter.value === "all" || r._doiTier === doiFilter.value) &&
|
||||
(!gapFilter.value || !r._hasSbom)
|
||||
);
|
||||
|
||||
display(Inputs.table(filteredRows.map(r => ({
|
||||
Repo: r.repo,
|
||||
Domain: r.domain,
|
||||
Category: r.category,
|
||||
Capabilities: r.capTags,
|
||||
"Business stake": r.businessStake,
|
||||
Classified: r.classifiedBy,
|
||||
"DoI Tier": DOI_TIER_LABEL[r._doiTier] ?? r._doiTier,
|
||||
Status: r.status,
|
||||
SBOM: r.sbom,
|
||||
|
||||
Reference in New Issue
Block a user