Page now renders in ~200ms. DoI badges and KPI card show a spinner while the background fetch resolves (~6s), then update reactively via Observable Mutable pattern (doiData / doiLoading). Fast path: repos, SBOM, domains, workstreams — immediate render. Slow path: /repos/doi/summary — background, non-blocking. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
391 lines
17 KiB
Markdown
391 lines
17 KiB
Markdown
---
|
|
title: Repositories
|
|
---
|
|
|
|
```js
|
|
import {API} from "./components/config.js";
|
|
```
|
|
|
|
```js
|
|
// Fast data — page renders as soon as this resolves (~200ms)
|
|
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = [];
|
|
try {
|
|
[_repos, _domains, _sbom, _eps, _tds, _workstreams] = 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() : []),
|
|
]);
|
|
} catch {}
|
|
```
|
|
|
|
```js
|
|
// DoI data — lazy-loaded. Starts as [] so the page renders immediately.
|
|
// When the fetch resolves (~6s), doiData updates and DoI badges appear.
|
|
import {Mutable} from "observablehq:stdlib";
|
|
const doiData = Mutable([]);
|
|
const doiLoading = Mutable(true);
|
|
fetch(`${API}/repos/doi/summary`)
|
|
.then(r => r.ok ? r.json() : [])
|
|
.catch(() => [])
|
|
.then(data => { doiData.value = data; doiLoading.value = false; });
|
|
```
|
|
|
|
```js
|
|
const repos = _repos ?? [];
|
|
const domains = _domains ?? [];
|
|
const sbom = _sbom ?? [];
|
|
const eps = _eps ?? [];
|
|
const tds = _tds ?? [];
|
|
const workstreams = _workstreams ?? [];
|
|
const doi = doiData; // reactive — updates when lazy fetch completes
|
|
|
|
// 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]));
|
|
const domainBySlug = Object.fromEntries(domains.map(d => [d.slug, d]));
|
|
|
|
// Active "repo-integration-{slug}" workstreams — signals onboarding in progress
|
|
const integratingBySlug = Object.fromEntries(
|
|
workstreams
|
|
.filter(w => w.status === "active" && w.slug?.startsWith("repo-integration-"))
|
|
.map(w => [w.slug.replace("repo-integration-", ""), w])
|
|
);
|
|
|
|
// Per-repo SBOM stats (from sbom entries)
|
|
const sbomByRepo = {};
|
|
for (const e of sbom) {
|
|
if (!sbomByRepo[e.repo_id]) sbomByRepo[e.repo_id] = { count: 0, snapshot_at: e.snapshot_at };
|
|
sbomByRepo[e.repo_id].count++;
|
|
}
|
|
|
|
// Per-domain counts
|
|
const epByDomain = {};
|
|
const tdByDomain = {};
|
|
const contribByDomain = {};
|
|
|
|
// EPs are domain-scoped
|
|
for (const ep of eps) {
|
|
if (!ep.status || ep.status === "open" || ep.status === "in_progress") {
|
|
epByDomain[ep.domain] = (epByDomain[ep.domain] ?? 0) + 1;
|
|
}
|
|
}
|
|
for (const td of tds) {
|
|
if (!td.status || td.status === "open" || td.status === "in_progress") {
|
|
tdByDomain[td.domain] = (tdByDomain[td.domain] ?? 0) + 1;
|
|
}
|
|
}
|
|
// Contributions: try to map via workstream → topic → domain (not available here; skip for now)
|
|
// Use domain slug from contributions' related_workstream if available — fallback: count by type only
|
|
|
|
// Build enriched repo rows
|
|
const repoRows = repos
|
|
.filter(r => r.status === "active")
|
|
.map(r => {
|
|
const domain = domainById[r.domain_id];
|
|
const domSlug = domain?.slug ?? "—";
|
|
const domName = domain?.name ?? "—";
|
|
const sbomData = sbomByRepo[r.id];
|
|
const hasSbom = !!sbomData || !!r.last_sbom_at;
|
|
const pkgCount = sbomData?.count ?? 0;
|
|
const lastScan = r.last_sbom_at
|
|
? 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",
|
|
path: r.local_path ?? "—",
|
|
sbom: hasSbom ? `✓ ${lastScan}` : "⚠ not ingested",
|
|
pkgs: pkgCount || (hasSbom ? "—" : 0),
|
|
eps: epByDomain[domSlug] ?? 0,
|
|
tds: tdByDomain[domSlug] ?? 0,
|
|
};
|
|
})
|
|
.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 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;
|
|
```
|
|
|
|
# Repositories
|
|
|
|
```js
|
|
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;display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
|
|
<span>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></span>
|
|
${doiLoading ? html`<span style="display:inline-flex;align-items:center;gap:0.35rem;color:#9ca3af;font-size:0.8rem;">
|
|
<span class="doi-spinner"></span> Loading DoI tiers…
|
|
</span>` : html`<span style="color:#16a34a;font-size:0.8rem;">✓ DoI tiers loaded</span>`}
|
|
</p>`);
|
|
```
|
|
|
|
```js
|
|
// Summary KPIs
|
|
display(html`<div class="kpi-row">
|
|
<div class="card">
|
|
<h3>Registered Repos</h3>
|
|
<p class="big-num">${repoRows.length}</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Domains</h3>
|
|
<p class="big-num">${new Set(repoRows.map(r => r._domSlug)).size}</p>
|
|
</div>
|
|
<div class="card ${integratingCount > 0 ? 'card-integrating' : ''}">
|
|
<h3>Integrating</h3>
|
|
<p class="big-num">${integratingCount}</p>
|
|
<small>${integratingCount === 0 ? "✓ All repos integrated" : `⚙ ${integratingCount} onboarding`}</small>
|
|
</div>
|
|
<div class="card ${coveredCount < repoRows.length ? '' : ''}">
|
|
<h3>SBOM Ingested</h3>
|
|
<p class="big-num">${coveredCount} / ${repoRows.length}</p>
|
|
</div>
|
|
<div class="card ${gapCount > 0 ? 'card-warn' : 'card-ok'}">
|
|
<h3>SBOM Gaps</h3>
|
|
<p class="big-num">${gapCount}</p>
|
|
<small>${gapCount === 0 ? "✓ All repos covered" : `⚠ ${gapCount} repo(s) not ingested`}</small>
|
|
</div>
|
|
<div class="card ${doiLoading ? '' : doiNoneCount > 0 ? 'card-warn' : 'card-ok'}">
|
|
<h3>DoI: Fully Integrated</h3>
|
|
<p class="big-num">${doiLoading ? html`<span class="doi-spinner" style="width:1.4rem;height:1.4rem;"></span>` : `${doiFullCount} / ${repoRows.length}`}</p>
|
|
<small>${doiLoading ? "Loading…" : doiNoneCount > 0 ? `⚠ ${doiNoneCount} at tier None` : "✓ All pass Core tier"}</small>
|
|
</div>
|
|
</div>`);
|
|
```
|
|
|
|
## Coverage Map
|
|
|
|
```js
|
|
// Returns a new "⚠ not ingested" span with a ? help button each time it's called.
|
|
function _sbomGap() {
|
|
const el = html`<span class="sbom-warn sbom-gap-hint">⚠ not ingested</span>`;
|
|
withDocHelp(el, "/docs/sbom");
|
|
return el;
|
|
}
|
|
|
|
function _doiBadge(tier) {
|
|
if (doiLoading) return html`<span style="color:#d1d5db;font-size:0.72rem;">…</span>`;
|
|
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) {
|
|
(byDomain[r._domSlug] = byDomain[r._domSlug] ?? []).push(r);
|
|
}
|
|
|
|
const domainBlocks = Object.entries(byDomain).sort(([a], [b]) => a.localeCompare(b));
|
|
|
|
if (domainBlocks.length === 0) {
|
|
display(html`<p style="color:gray">No repos registered. Run <code>make add-repo DOMAIN=<slug> SLUG=<slug> NAME="..." PATH=/path</code>.</p>`);
|
|
} else {
|
|
display(html`<div class="domain-list">
|
|
${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`
|
|
<div class="domain-block ${allCovered ? '' : 'domain-gap'}">
|
|
<div class="domain-header">
|
|
<span class="domain-name">${dom?.name ?? slug}</span>
|
|
<span class="domain-chips">
|
|
${allCovered
|
|
? html`<span class="chip chip-ok">SBOM ✓</span>`
|
|
: html`<span class="chip chip-warn">SBOM ⚠</span>`}
|
|
${hasEps
|
|
? html`<span class="chip chip-ok">EPs: ${epByDomain[slug]}</span>`
|
|
: html`<span class="chip chip-neutral">EPs: —</span>`}
|
|
${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>
|
|
<th>Local path</th>
|
|
</tr></thead>
|
|
<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>
|
|
<td class="${r._hasSbom ? 'sbom-ok' : 'sbom-warn'}">${r._hasSbom ? r.sbom : _sbomGap()}</td>
|
|
<td>${r.pkgs}</td>
|
|
<td class="path-cell">${r.path}</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>`);
|
|
```
|
|
|
|
```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,
|
|
"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: 1200}));
|
|
```
|
|
|
|
## Onboard a New Repo
|
|
|
|
```js
|
|
const _h2onboard = [...document.querySelectorAll("#observablehq-main h2")]
|
|
.find(h => h.textContent.includes("Onboard a New Repo"));
|
|
if (_h2onboard) { _h2onboard.style.position = "relative"; withDocHelp(_h2onboard, "/docs/repo-integration"); }
|
|
```
|
|
|
|
```js
|
|
display(html`<div class="onboard-panel">
|
|
<div class="onboard-step">
|
|
<span class="onboard-num">1</span>
|
|
<div>
|
|
<strong>Clone the repo locally</strong>
|
|
<pre>git clone <remote-url> /path/to/repo</pre>
|
|
</div>
|
|
</div>
|
|
<div class="onboard-step">
|
|
<span class="onboard-num">2</span>
|
|
<div>
|
|
<strong>Register from the repo root</strong>
|
|
<pre>cd /path/to/repo
|
|
custodian register-project --domain <slug></pre>
|
|
<p class="onboard-note">The custodian writes <code>CLAUDE.custodian.md</code>, registers the repo, and creates 4 onboarding tasks in the domain's topic.</p>
|
|
</div>
|
|
</div>
|
|
<div class="onboard-step">
|
|
<span class="onboard-num">3</span>
|
|
<div>
|
|
<strong>Open the repo in Claude Code and run /init</strong>
|
|
<pre>cd /path/to/repo && claude</pre>
|
|
<p class="onboard-note">Once Claude starts, run <code>/init</code> to trigger the integration. The repo agent reads <code>CLAUDE.custodian.md</code>, picks up the onboarding tasks, and integrates autonomously.</p>
|
|
</div>
|
|
</div>
|
|
<div class="onboard-step">
|
|
<span class="onboard-num">4</span>
|
|
<div>
|
|
<strong>Monitor here</strong>
|
|
<p class="onboard-note">The <strong>⚙ integrating</strong> badge clears when the repo agent completes all 4 onboarding tasks and closes the workstream.</p>
|
|
</div>
|
|
</div>
|
|
</div>`);
|
|
```
|
|
|
|
<style>
|
|
.kpi-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
|
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
|
|
.card-warn { border: 2px solid #e53935; }
|
|
.card-ok { border: 2px solid #2e7d32; }
|
|
.big-num { font-size: 2.2rem; font-weight: bold; margin: 0.25rem 0; }
|
|
|
|
.domain-list { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 1.5rem; }
|
|
.domain-block { background: var(--theme-background-alt); border-radius: 8px; padding: 0.9rem 1rem; }
|
|
.domain-gap { border-left: 4px solid #f0a500; }
|
|
.domain-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.65rem; flex-wrap: wrap; }
|
|
.domain-name { font-weight: 700; font-size: 1rem; }
|
|
.domain-chips { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
|
.chip { font-size: 0.72rem; font-weight: 600; border-radius: 10px; padding: 0.1rem 0.5rem; }
|
|
.chip-ok { background: #e8f5e9; color: #2e7d32; }
|
|
.chip-warn { background: #fff3cd; color: #856404; }
|
|
.chip-neutral { background: var(--theme-background); color: gray; border: 1px solid var(--theme-foreground-faint); }
|
|
|
|
.repo-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
.repo-table th { text-align: left; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: gray; padding: 0.3rem 0.5rem; border-bottom: 1px solid var(--theme-foreground-faint); }
|
|
.repo-table td { padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--theme-foreground-faint); }
|
|
.repo-table tr:last-child td { border-bottom: none; }
|
|
.row-gap { background: #fffbf0; }
|
|
.repo-cell { font-family: monospace; }
|
|
.sbom-ok { color: #2e7d32; font-weight: 600; }
|
|
.sbom-warn { color: #856404; font-weight: 600; }
|
|
.sbom-gap-hint { position: relative; display: inline-block; padding-right: 1.8rem; }
|
|
.path-cell { font-family: monospace; font-size: 0.78rem; color: gray; }
|
|
|
|
.card-integrating { border: 2px solid #7c3aed; }
|
|
.chip-integrating { background: #ede9fe; color: #5b21b6; }
|
|
.row-integrating { background: #faf5ff; }
|
|
|
|
.onboard-panel { display: flex; flex-direction: column; gap: 0; margin-top: 0.5rem; background: var(--theme-background-alt); border-radius: 8px; overflow: hidden; }
|
|
.onboard-step { display: flex; gap: 1rem; align-items: flex-start; padding: 0.9rem 1.1rem; border-bottom: 1px solid var(--theme-foreground-faint, #eee); }
|
|
.onboard-step:last-child { border-bottom: none; }
|
|
.onboard-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--theme-foreground-focus, #1a1a1a); color: var(--theme-background, white); font-size: 0.8rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; }
|
|
.onboard-step strong { font-size: 0.9rem; display: block; margin-bottom: 0.3rem; }
|
|
.onboard-step pre { background: var(--theme-background); border-radius: 4px; padding: 0.4rem 0.7rem; font-size: 0.8rem; overflow-x: auto; margin: 0 0 0.35rem; }
|
|
.onboard-note { font-size: 0.82rem; color: var(--theme-foreground-muted, gray); margin: 0; line-height: 1.45; }
|
|
|
|
.doi-spinner {
|
|
display: inline-block; width: 0.9rem; height: 0.9rem;
|
|
border: 2px solid #e5e7eb; border-top-color: #9ca3af;
|
|
border-radius: 50%; animation: doi-spin 0.7s linear infinite;
|
|
vertical-align: middle;
|
|
}
|
|
@keyframes doi-spin { to { transform: rotate(360deg); } }
|
|
</style>
|