generated from coulomb/repo-seed
feat(doi): Repository DoI automated gate and dashboard integration (CUST-WP-0024)
Implements the 14-criterion DoI checklist as a runnable gate with API,
MCP tools, CLI script, and dashboard integration.
Core components:
- api/doi_engine.py — async engine evaluating all 14 criteria (asyncio.to_thread
for non-blocking HTTP self-calls), shared by API and CLI
- api/schemas/doi.py — DoICriterion, DoIReport, DoISummaryEntry schemas
- api/routers/repos.py — GET /repos/{slug}/doi + GET /repos/doi/summary
- scripts/check_doi.py — CLI: make check-doi REPO=<slug> / check-doi-all
- mcp_server/server.py — check_repo_doi(), get_doi_summary() tools
Dashboard (repos.md):
- DoI tier badge per repo (None/Core/Standard/Full) colour-coded red→green
- Domain block shows lowest DoI tier across its repos
- DoI KPI card in summary row
- DoI filter in All Repos Table
- Link to Repository DoI policy page
Also fixes: TPSC snapshots 500 error (missing nested selectinload for
catalog_entry relationship in list_snapshots endpoint).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,15 +7,16 @@ import {API} from "./components/config.js";
|
||||
```
|
||||
|
||||
```js
|
||||
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = [];
|
||||
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = [], _doi = [];
|
||||
try {
|
||||
[_repos, _domains, _sbom, _eps, _tds, _workstreams] = await Promise.all([
|
||||
[_repos, _domains, _sbom, _eps, _tds, _workstreams, _doi] = await Promise.all([
|
||||
fetch(`${API}/repos/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/domains/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/sbom/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/extension-points/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/technical-debt/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/workstreams/`).then(r => r.ok ? r.json() : []),
|
||||
fetch(`${API}/repos/doi/summary`).then(r => r.ok ? r.json() : []),
|
||||
]);
|
||||
} catch {}
|
||||
```
|
||||
@@ -27,6 +28,14 @@ const sbom = _sbom ?? [];
|
||||
const eps = _eps ?? [];
|
||||
const tds = _tds ?? [];
|
||||
const workstreams = _workstreams ?? [];
|
||||
const doi = _doi ?? [];
|
||||
|
||||
// DoI lookups
|
||||
const doiBySlug = Object.fromEntries(doi.map(d => [d.repo_slug, d]));
|
||||
const DOI_TIER_ORDER = {none: 0, core: 1, standard: 2, full: 3};
|
||||
const DOI_TIER_COLOR = {none: "#ef4444", core: "#f97316", standard: "#eab308", full: "#22c55e"};
|
||||
const DOI_TIER_BG = {none: "#fef2f2", core: "#fff7ed", standard: "#fefce8", full: "#f0fdf4"};
|
||||
const DOI_TIER_LABEL = {none: "None", core: "Core", standard: "Standard", full: "Full"};
|
||||
|
||||
// Lookups
|
||||
const domainById = Object.fromEntries(domains.map(d => [d.id, d]));
|
||||
@@ -79,11 +88,14 @@ const repoRows = repos
|
||||
? new Date(r.last_sbom_at).toLocaleDateString()
|
||||
: (sbomData?.snapshot_at ? new Date(sbomData.snapshot_at).toLocaleDateString() : null);
|
||||
const integrating = !!integratingBySlug[r.slug];
|
||||
const doiEntry = doiBySlug[r.slug] ?? null;
|
||||
const doiTier = doiEntry?.tier ?? "none";
|
||||
return {
|
||||
_id: r.id,
|
||||
_domSlug: domSlug,
|
||||
_hasSbom: hasSbom,
|
||||
_integrating: integrating,
|
||||
_doiTier: doiTier,
|
||||
repo: r.slug,
|
||||
domain: domName,
|
||||
status: integrating ? "⚙ integrating" : "ready",
|
||||
@@ -96,9 +108,11 @@ const repoRows = repos
|
||||
})
|
||||
.sort((a, b) => a._domSlug.localeCompare(b._domSlug) || a.repo.localeCompare(b.repo));
|
||||
|
||||
const gapCount = repoRows.filter(r => !r._hasSbom).length;
|
||||
const coveredCount = repoRows.filter(r => r._hasSbom).length;
|
||||
const gapCount = repoRows.filter(r => !r._hasSbom).length;
|
||||
const coveredCount = repoRows.filter(r => r._hasSbom).length;
|
||||
const integratingCount = repoRows.filter(r => r._integrating).length;
|
||||
const doiFullCount = repoRows.filter(r => r._doiTier === "full").length;
|
||||
const doiNoneCount = repoRows.filter(r => r._doiTier === "none").length;
|
||||
```
|
||||
|
||||
# Repos
|
||||
@@ -107,6 +121,13 @@ const integratingCount = repoRows.filter(r => r._integrating).length;
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
const _h1 = document.querySelector("#observablehq-main h1");
|
||||
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/repos"); }
|
||||
display(html`<p style="font-size:0.85rem;color:#6b7280;margin-top:-0.5rem;">
|
||||
DoI tiers: <strong style="color:#ef4444;">None</strong> →
|
||||
<strong style="color:#f97316;">Core</strong> →
|
||||
<strong style="color:#eab308;">Standard</strong> →
|
||||
<strong style="color:#22c55e;">Full</strong> —
|
||||
<a href="/policy/repo-doi" style="color:#1d4ed8;">Definition of Integrated policy ↗</a>
|
||||
</p>`);
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -134,6 +155,11 @@ display(html`<div class="kpi-row">
|
||||
<p class="big-num">${gapCount}</p>
|
||||
<small>${gapCount === 0 ? "✓ All repos covered" : `⚠ ${gapCount} repo(s) not ingested`}</small>
|
||||
</div>
|
||||
<div class="card ${doiNoneCount > 0 ? 'card-warn' : 'card-ok'}">
|
||||
<h3>DoI: Fully Integrated</h3>
|
||||
<p class="big-num">${doiFullCount} / ${repoRows.length}</p>
|
||||
<small>${doiNoneCount > 0 ? `⚠ ${doiNoneCount} at tier None` : "✓ All pass Core tier"}</small>
|
||||
</div>
|
||||
</div>`);
|
||||
```
|
||||
|
||||
@@ -147,6 +173,15 @@ function _sbomGap() {
|
||||
return el;
|
||||
}
|
||||
|
||||
function _doiBadge(tier) {
|
||||
const color = DOI_TIER_COLOR[tier] || "#9ca3af";
|
||||
const bg = DOI_TIER_BG[tier] || "#f9fafb";
|
||||
const label = DOI_TIER_LABEL[tier] || tier;
|
||||
return html`<span style="background:${bg}; color:${color}; border:1px solid ${color}60;
|
||||
border-radius:4px; padding:1px 7px; font-size:0.72rem; font-weight:700; white-space:nowrap;">
|
||||
DoI: ${label}</span>`;
|
||||
}
|
||||
|
||||
// Group by domain
|
||||
const byDomain = {};
|
||||
for (const r of repoRows) {
|
||||
@@ -162,6 +197,9 @@ if (domainBlocks.length === 0) {
|
||||
${domainBlocks.map(([slug, rows]) => {
|
||||
const dom = domainBySlug[slug];
|
||||
const allCovered = rows.every(r => r._hasSbom);
|
||||
const doiWorst = rows.map(r => DOI_TIER_ORDER[r._doiTier] ?? 0);
|
||||
const doiMin = Math.min(...doiWorst);
|
||||
const doiMinKey = Object.keys(DOI_TIER_ORDER).find(k => DOI_TIER_ORDER[k] === doiMin) ?? "none";
|
||||
const hasEps = (epByDomain[slug] ?? 0) > 0;
|
||||
const hasTds = (tdByDomain[slug] ?? 0) > 0;
|
||||
return html`
|
||||
@@ -178,11 +216,15 @@ if (domainBlocks.length === 0) {
|
||||
${hasTds
|
||||
? html`<span class="chip chip-ok">TDs: ${tdByDomain[slug]}</span>`
|
||||
: html`<span class="chip chip-neutral">TDs: —</span>`}
|
||||
<span style="font-size:0.72rem; color:${DOI_TIER_COLOR[doiMinKey]}; font-weight:600;">
|
||||
DoI min: ${DOI_TIER_LABEL[doiMinKey]}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<table class="repo-table">
|
||||
<thead><tr>
|
||||
<th>Repo</th>
|
||||
<th>DoI Tier</th>
|
||||
<th>Status</th>
|
||||
<th>SBOM</th>
|
||||
<th>Packages</th>
|
||||
@@ -191,6 +233,7 @@ 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>${_doiBadge(r._doiTier)}</td>
|
||||
<td>${r._integrating
|
||||
? html`<span class="chip chip-integrating">⚙ integrating</span>`
|
||||
: html`<span class="chip chip-ok">ready</span>`}</td>
|
||||
@@ -211,26 +254,29 @@ if (domainBlocks.length === 0) {
|
||||
|
||||
```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}${gapFilter}</div>`);
|
||||
display(html`<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem">${domainFilter}${doiFilter}${gapFilter}</div>`);
|
||||
```
|
||||
|
||||
```js
|
||||
const filteredRows = repoRows.filter(r =>
|
||||
(domainFilter.value === "all" || r._domSlug === domainFilter.value) &&
|
||||
(doiFilter.value === "all" || r._doiTier === doiFilter.value) &&
|
||||
(!gapFilter.value || !r._hasSbom)
|
||||
);
|
||||
|
||||
display(Inputs.table(filteredRows.map(r => ({
|
||||
Repo: r.repo,
|
||||
Domain: r.domain,
|
||||
Status: r.status,
|
||||
SBOM: r.sbom,
|
||||
Pkgs: r.pkgs,
|
||||
Repo: r.repo,
|
||||
Domain: r.domain,
|
||||
"DoI Tier": DOI_TIER_LABEL[r._doiTier] ?? r._doiTier,
|
||||
Status: r.status,
|
||||
SBOM: r.sbom,
|
||||
Pkgs: r.pkgs,
|
||||
"EPs (domain)": r.eps || "—",
|
||||
"TDs (domain)": r.tds || "—",
|
||||
Path: r.path,
|
||||
})), {maxWidth: 1100}));
|
||||
Path: r.path,
|
||||
})), {maxWidth: 1200}));
|
||||
```
|
||||
|
||||
## Onboard a New Repo
|
||||
|
||||
Reference in New Issue
Block a user