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:
tegwick 2026-06-22 13:52:13 +02:00
parent 279be4ffbd
commit 0949d4c0d8
84 changed files with 4494 additions and 1111 deletions

View file

@ -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,