feat(state-hub): v0.3 MCP tools + dashboard pages for contributions and SBOM

MCP server additions (5 tools + 3 resources):
- register_contribution(), update_contribution_status(), get_contributions()
- ingest_sbom_tool(repo_slug, lockfile_path) — shells out to ingest_sbom.py
- get_licence_report()
- state://contributions, state://sbom/aggregated, state://sbom/{repo_slug}

Dashboard pages:
- contributions.md — live-polled Kanban by status (draft→merged), filter bar
  (type/status/repo), KPI grid (total + per type), follow-up banner, full table
- sbom.md — licence distribution bar chart (Plot), copyleft risk section,
  package table with ecosystem/direct/dev filters, repo-slug resolution
- data/contributions.json.py, data/sbom.json.py — Observable data loaders
- index.md — added Contribution & SBOM Health KPI row (total, follow-up count,
  copyleft risk indicator; sourced from state summary fields)
- observablehq.config.js — added Contributions + SBOM to nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 17:28:41 +01:00
parent 8d38110275
commit afac54ec09
7 changed files with 532 additions and 0 deletions

View File

@@ -156,6 +156,32 @@ if (openWs.length === 0) {
}
```
## Contribution & SBOM Health
```js
const contribCounts = summary.contribution_counts ?? {};
const licenceRisk = summary.licence_risk_count ?? 0;
const totalContribs = ["br","fr","ep","upr"].reduce((s, t) => s + (contribCounts[t] ?? 0), 0);
const needsFollowUp = (contribCounts["submitted"] ?? 0) + (contribCounts["acknowledged"] ?? 0);
display(html`<div class="grid grid-cols-3" style="gap:1rem;margin-bottom:1.5rem">
<a class="card card-link" href="./contributions">
<h3>Contributions</h3>
<p class="big-num">${totalContribs}</p>
<small>${needsFollowUp > 0 ? html`<span style="color:orange">${needsFollowUp} awaiting upstream response</span>` : "all up to date"}</small>
</a>
<a class="card card-link ${licenceRisk > 0 ? 'warn' : ''}" href="./sbom">
<h3>Licence Risk</h3>
<p class="big-num">${licenceRisk}</p>
<small>${licenceRisk === 0 ? html`<span style="color:green">✓ no copyleft in direct deps</span>` : html`<span style="color:red">copyleft in direct prod deps</span>`}</small>
</a>
<a class="card card-link" href="./sbom">
<h3>SBOM</h3>
<p class="big-num" style="font-size:1rem;padding-top:0.5rem">By type: ${["br","fr","ep","upr"].filter(t => contribCounts[t]).map(t => html`<span style="margin-right:0.4rem">${t.toUpperCase()} ${contribCounts[t]}</span>`).join("") || "—"}</p>
</a>
</div>`);
```
## Status
```js