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:
parent
279be4ffbd
commit
0949d4c0d8
84 changed files with 4494 additions and 1111 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue