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>
2026-02-28 17:28:41 +01:00
---
title: SBOM
---
```js
2026-03-11 01:40:52 +01:00
import {API} from "./components/config.js";
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>
2026-02-28 17:28:41 +01:00
```
```js
// Fetch SBOM data on load
2026-03-02 23:39:17 +01:00
let _entries = [], _report = {groups: [], copyleft_direct_count: 0}, _repos = [], _domains = [], _snapshots = [];
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>
2026-02-28 17:28:41 +01:00
try {
2026-03-02 23:39:17 +01:00
[_entries, _report, _repos, _domains, _snapshots] = await Promise.all([
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>
2026-02-28 17:28:41 +01:00
fetch(`${API}/sbom/` ).then(r => r.ok ? r.json() : []),
fetch(`${API}/sbom/report/licences/` ).then(r => r.ok ? r.json() : {groups:[], copyleft_direct_count: 0}),
fetch(`${API}/repos/` ).then(r => r.ok ? r.json() : []),
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
fetch(`${API}/domains/` ).then(r => r.ok ? r.json() : []),
2026-03-02 23:39:17 +01:00
fetch(`${API}/sbom/snapshots/` ).then(r => r.ok ? r.json() : []),
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>
2026-02-28 17:28:41 +01:00
]);
} catch {}
```
```js
2026-03-02 23:39:17 +01:00
const entries = _entries ?? [];
const report = _report ?? {groups: [], copyleft_direct_count: 0};
const repos = _repos ?? [];
const domains = _domains ?? [];
const snapshots = _snapshots ?? [];
const groups = report.groups ?? [];
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>
2026-02-28 17:28:41 +01:00
const riskCount = report.copyleft_direct_count ?? 0;
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
// Domain + repo lookups
const domainById = Object.fromEntries(domains.map(d => [d.id, d]));
const repoById = Object.fromEntries(repos.map(r => [r.id, r]));
const repoDomain = Object.fromEntries(repos.map(r => [r.id, domainById[r.domain_id]?.slug ?? "—"]));
const domainSlugs = [...new Set(repos.map(r => repoDomain[r.id]).filter(s => s !== "—"))].sort();
// Copyleft detector (mirrors server-side logic)
const COPYLEFT_KW = ["GPL", "AGPL", "LGPL", "EUPL", "CDDL", "MPL"];
const isCopyleft = spdx => spdx & & COPYLEFT_KW.some(k => spdx.toUpperCase().includes(k));
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>
2026-02-28 17:28:41 +01:00
```
# SBOM
2026-03-01 19:29:20 +01:00
```js
import {withDocHelp} from "./components/doc-overlay.js";
const _h1 = document.querySelector("#observablehq -main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/sbom"); }
```
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
## Overview
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>
2026-02-28 17:28:41 +01:00
```js
const riskBadge = riskCount === 0
? html`<span class="risk-ok">✓ No copyleft in direct prod deps</span>`
: html`<span class="risk-warn">⚠ ${riskCount} direct prod dep(s) with copyleft licence</span>` ;
display(html`< div class = "kpi-row" >
< div class = "card" >
< h3 > Total Packages< / h3 >
< p class = "big-num" > ${entries.length}< / p >
< / div >
< div class = "card" >
< h3 > Repos Scanned< / h3 >
< p class = "big-num" > ${new Set(entries.map(e => e.repo_id)).size}< / p >
< / div >
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
< div class = "card" >
< h3 > Domains Covered< / h3 >
< p class = "big-num" > ${domainSlugs.length || new Set(Object.values(repoDomain).filter(s => s !== "—")).size}< / p >
< / div >
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>
2026-02-28 17:28:41 +01:00
< div class = "card ${riskCount > 0 ? 'card-warn' : ''}" >
< h3 > Licence Risk< / h3 >
< p class = "big-num" > ${riskCount}< / p >
< small > ${riskBadge}< / small >
< / div >
< div class = "card" >
< h3 > Unique Licences< / h3 >
< p class = "big-num" > ${groups.length}< / p >
< / div >
< / div > `);
```
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
## By Domain
```js
if (entries.length === 0) {
display(html`<p style="color:gray">No SBOM data ingested yet. Run <code>make ingest-sbom REPO=<slug> SCAN=1 REPO_PATH=<path></code>.</p>` );
} else {
// Group entries by domain
const byDomain = {};
for (const e of entries) {
const slug = repoDomain[e.repo_id] ?? "—";
(byDomain[slug] = byDomain[slug] ?? []).push(e);
}
const domainTableRows = Object.entries(byDomain).map(([slug, es]) => {
const dom = domains.find(d => d.slug === slug);
const repoCount = new Set(es.map(e => e.repo_id)).size;
const directProd = es.filter(e => e.is_direct & & !e.is_dev);
const copyleftRisk = directProd.filter(e => isCopyleft(e.license_spdx)).length;
const ecosystems = [...new Set(es.map(e => e.ecosystem))].sort().join(", ");
return {
domain: dom?.name ?? slug,
repos: repoCount,
packages: es.length,
direct: directProd.length,
copyleft: copyleftRisk,
ecosystems,
};
}).sort((a, b) => a.domain.localeCompare(b.domain));
display(Inputs.table(domainTableRows, {
columns: ["domain", "repos", "packages", "direct", "copyleft", "ecosystems"],
header: {domain: "Domain", repos: "Repos", packages: "All Pkgs", direct: "Direct Prod", copyleft: "Copyleft ⚠", ecosystems: "Ecosystems"},
maxWidth: 900,
}));
}
```
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>
2026-02-28 17:28:41 +01:00
## Licence Distribution
```js
import * as Plot from "npm:@observablehq/plot ";
if (groups.length === 0) {
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
display(html`<p style="color:gray">No SBOM data ingested yet.</p>` );
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>
2026-02-28 17:28:41 +01:00
} else {
const plotData = groups.slice(0, 15).map(g => ({
licence: g.license_spdx ?? "(unknown)",
count: g.count,
copyleft: g.is_copyleft,
}));
display(Plot.plot({
x: {label: "Packages"},
y: {label: null, domain: plotData.map(d => d.licence)},
color: {domain: [false, true], range: ["steelblue", "#e53935 "], legend: true, tickFormat: d => d ? "Copyleft" : "Permissive"},
marks: [
Plot.barX(plotData, {y: "licence", x: "count", fill: "copyleft", tip: true}),
Plot.ruleX([0]),
],
marginLeft: 130,
height: Math.max(80, plotData.length * 30 + 50),
width: 600,
}));
}
```
## Copyleft Risk Detail
```js
const copyleftGroups = groups.filter(g => g.is_copyleft);
if (copyleftGroups.length === 0) {
display(html`<p style="color:green">✓ No copyleft packages found.</p>` );
} else {
display(html`< div class = "copyleft-section" >
${copyleftGroups.map(g => html`
< div class = "copyleft-card" >
< span class = "copyleft-badge" > ${g.license_spdx ?? "unknown"}< / span >
< span class = "copyleft-count" > ${g.count} package(s)< / span >
< span class = "copyleft-repos" > ${g.repos.join(", ")}< / span >
< / div >
`)}
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
< / div >
< p style = "font-size:0.8rem;color:gray" > Note: dual-licensed packages (e.g. "MIT OR GPL-3.0") are flagged conservatively. Review if the non-copyleft variant is used.< / p > `);
}
```
## By Repo
```js
// Group entries by repo, sorted by domain then repo name
const byRepo = {};
for (const e of entries) {
(byRepo[e.repo_id] = byRepo[e.repo_id] ?? []).push(e);
}
const repoSections = Object.entries(byRepo)
.map(([repoId, es]) => {
const repo = repoById[repoId];
const domSlug = repoDomain[repoId] ?? "—";
const dom = domains.find(d => d.slug === domSlug);
const directProd = es.filter(e => e.is_direct & & !e.is_dev);
const copyleftRisk = directProd.filter(e => isCopyleft(e.license_spdx)).length;
const ecosystems = [...new Set(es.map(e => e.ecosystem))].sort();
return { repoId, repo, dom, domSlug, es, directProd, copyleftRisk, ecosystems };
})
.sort((a, b) => (a.domSlug + a.repo?.slug).localeCompare(b.domSlug + b.repo?.slug));
if (repoSections.length === 0) {
display(html`<p style="color:gray">No repo data.</p>` );
} else {
display(html`< div class = "repo-list" >
${repoSections.map(({repoId, repo, dom, domSlug, es, directProd, copyleftRisk, ecosystems}) => html`
< details class = "repo-details" >
< summary class = "repo-summary" >
< span class = "repo-domain-tag" > ${dom?.name ?? domSlug}< / span >
< span class = "repo-name" > ${repo?.slug ?? repoId.slice(0,8)}< / span >
< span class = "repo-meta" > ${es.length} pkgs · ${ecosystems.join(" + ")} · ${directProd.length} direct< / span >
${copyleftRisk > 0 ? html`<span class="repo-risk-badge">⚠ ${copyleftRisk} copyleft</span>` : ""}
< / summary >
< div class = "repo-pkg-table" >
${Inputs.table(es.slice(0, 200).map(e => ({
Package: e.package_name,
Version: e.package_version ?? "—",
Ecosystem: e.ecosystem,
Licence: e.license_spdx ?? "—",
Direct: e.is_direct ? "✓" : "",
Dev: e.is_dev ? "✓" : "",
})), {maxWidth: 860})}
${es.length > 200 ? html`<p style="font-size:0.8rem;color:gray">Showing first 200 of ${es.length}</p>` : ""}
< / div >
< / details >
`)}
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>
2026-02-28 17:28:41 +01:00
< / div > `);
}
```
2026-03-02 23:39:17 +01:00
## Snapshot History
```js
if (snapshots.length === 0) {
display(html`<p style="color:gray">No snapshots recorded yet.</p>` );
} else {
// Group by repo, sort newest first within each group
const snapByRepo = {};
for (const s of snapshots) {
(snapByRepo[s.repo_id] = snapByRepo[s.repo_id] ?? []).push(s);
}
const repoOrder = Object.keys(snapByRepo).sort((a, b) => {
const ra = repos.find(r => r.id === a);
const rb = repos.find(r => r.id === b);
return (ra?.slug ?? a).localeCompare(rb?.slug ?? b);
});
display(html`< div class = "snap-list" >
${repoOrder.map(repoId => {
const repo = repos.find(r => r.id === repoId);
const domSlug = repo ? domains.find(d => d.id === repo.domain_id)?.slug ?? "—" : "—";
const snaps = snapByRepo[repoId]; // already sorted newest-first by API
return html`< details class = "snap-repo-block" >
< summary class = "snap-repo-summary" >
< span class = "repo-domain-tag" > ${domSlug}< / span >
< span class = "snap-repo-name" > ${repo?.slug ?? repoId.slice(0,8)}< / span >
< span class = "snap-meta" > ${snaps.length} snapshot${snaps.length !== 1 ? "s" : ""}< / span >
< / summary >
< div class = "snap-table-wrap" >
${Inputs.table(snaps.map(s => ({
"Snapshot At": new Date(s.snapshot_at).toLocaleString(),
Packages: s.entry_count,
Source: s.source ?? "—",
ID: s.id.slice(0, 8) + "…",
})), {maxWidth: 700})}
< / div >
< / details > `;
})}
< / div > `);
}
```
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>
2026-02-28 17:28:41 +01:00
## Package Table
```js
// Filters
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
const domainOpts = ["all", ...domainSlugs];
const domainFilter = Inputs.select(domainOpts, {label: "Domain", value: "all"});
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>
2026-02-28 17:28:41 +01:00
const ecoFilter = Inputs.select(["all", "python", "node", "rust", "go", "java", "other"], {label: "Ecosystem", value: "all"});
const directOnly = Inputs.toggle({label: "Direct deps only", value: false});
const prodOnly = Inputs.toggle({label: "Prod deps only (no dev)", value: false});
display(html`< div style = "display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem" >
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
${domainFilter}${ecoFilter}${directOnly}${prodOnly}
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>
2026-02-28 17:28:41 +01:00
< / div > `);
```
```js
const filteredEntries = entries.filter(e =>
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
(domainFilter.value === "all" || repoDomain[e.repo_id] === domainFilter.value) & &
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>
2026-02-28 17:28:41 +01:00
(ecoFilter.value === "all" || e.ecosystem === ecoFilter.value) & &
(!directOnly.value || e.is_direct) & &
(!prodOnly.value || !e.is_dev)
);
display(Inputs.table(filteredEntries.map(e => ({
Package: e.package_name,
Version: e.package_version ?? "—",
Ecosystem: e.ecosystem,
Licence: e.license_spdx ?? "—",
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
Domain: repoDomain[e.repo_id] ?? "—",
Repo: repoById[e.repo_id]?.slug ?? e.repo_id?.slice(0, 8) ?? "—",
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>
2026-02-28 17:28:41 +01:00
Direct: e.is_direct ? "✓" : "",
Dev: e.is_dev ? "✓" : "",
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
})), {maxWidth: 960}));
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>
2026-02-28 17:28:41 +01:00
```
< style >
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
.card-warn { border: 2px solid #e53935 ; }
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
.kpi-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
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>
2026-02-28 17:28:41 +01:00
.big-num { font-size: 2.2rem; font-weight: bold; margin: 0.25rem 0; }
.risk-ok { color: #2e7d32 ; font-weight: 600; }
.risk-warn { color: #e53935 ; font-weight: 600; }
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
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>
2026-02-28 17:28:41 +01:00
.copyleft-section { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.copyleft-card { background: #fde8e8 ; border-left: 4px solid #e53935 ; border-radius: 6px; padding: 0.5rem 0.9rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
.copyleft-badge { font-weight: 700; font-size: 0.85rem; color: #c62828 ; }
.copyleft-count { font-size: 0.82rem; color: #555 ; }
.copyleft-repos { font-size: 0.8rem; color: gray; font-family: monospace; }
feat(sbom): scan mode, domain grouping dashboard, SBOM convention doc
- ingest_sbom.py: add --scan flag (recursive lockfile discovery) +
--lockfile repeatable for explicit multi-file ingestion; skip
.venv/node_modules/.git/dist/etc; Makefile gains SCAN= and REPO_PATH= vars
- sbom.md: add /domains/ fetch; domain-level summary table; per-repo
accordion with details/summary; domain filter on package table; dual-
licence false-positive note; +1 KPI card (Domains Covered)
- canon/standards/sbom-convention_v0.1.md: authoritative lockfile table,
ingest workflow (single/scan/explicit), snapshot semantics, direct-vs-
transitive caveats, licence governance + copyleft escalation, update
cadence, multi-repo domain pattern, planned enhancements
First ingest: the-custodian — 420 pkgs (88 python + 332 node), 13 licence
groups, 1 copyleft flag (jszip dual-licensed MIT OR GPL-3.0-or-later)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:15:40 +01:00
.repo-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1.5rem; }
.repo-details { background: var(--theme-background-alt); border-radius: 8px; }
.repo-details[open] { border: 1px solid var(--theme-foreground-faint); }
.repo-summary { cursor: pointer; padding: 0.65rem 0.9rem; display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; list-style: none; }
.repo-summary::-webkit-details-marker { display: none; }
.repo-summary::before { content: "▶"; font-size: 0.7rem; color: gray; flex-shrink: 0; }
details[open] > .repo-summary::before { content: "▼"; }
.repo-domain-tag { font-size: 0.7rem; font-weight: 600; background: var(--theme-background); border: 1px solid var(--theme-foreground-faint); border-radius: 10px; padding: 0.1rem 0.45rem; color: steelblue; }
.repo-name { font-weight: 600; font-size: 0.9rem; font-family: monospace; }
.repo-meta { font-size: 0.78rem; color: gray; }
.repo-risk-badge { font-size: 0.75rem; font-weight: 600; color: #c62828 ; background: #fde8e8 ; border-radius: 4px; padding: 0.1rem 0.4rem; }
.repo-pkg-table { padding: 0.5rem 0.75rem 0.75rem; }
2026-03-02 23:39:17 +01:00
/* ── Snapshot history ─────────────────────────────────────────────────────── */
.snap-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1.5rem; }
.snap-repo-block { background: var(--theme-background-alt); border-radius: 8px; }
.snap-repo-block[open] { border: 1px solid var(--theme-foreground-faint); }
.snap-repo-summary { cursor: pointer; padding: 0.65rem 0.9rem; display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; list-style: none; }
.snap-repo-summary::-webkit-details-marker { display: none; }
.snap-repo-summary::before { content: "▶"; font-size: 0.7rem; color: gray; flex-shrink: 0; }
details[open] > .snap-repo-summary::before { content: "▼"; }
.snap-repo-name { font-weight: 600; font-size: 0.9rem; font-family: monospace; }
.snap-meta { font-size: 0.78rem; color: gray; }
.snap-table-wrap { padding: 0.5rem 0.75rem 0.75rem; }
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>
2026-02-28 17:28:41 +01:00
< / style >