2026-03-01 18:53:25 +01:00
---
title: Repos
---
```js
2026-03-11 01:40:52 +01:00
import {API} from "./components/config.js";
2026-03-01 18:53:25 +01:00
```
```js
2026-03-02 08:42:30 +01:00
let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = [];
2026-03-01 18:53:25 +01:00
try {
2026-03-02 08:42:30 +01:00
[_repos, _domains, _sbom, _eps, _tds, _workstreams] = await Promise.all([
2026-03-01 18:53:25 +01:00
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() : []),
2026-03-02 08:42:30 +01:00
fetch(`${API}/workstreams/` ).then(r => r.ok ? r.json() : []),
2026-03-01 18:53:25 +01:00
]);
} catch {}
```
```js
2026-03-02 08:42:30 +01:00
const repos = _repos ?? [];
const domains = _domains ?? [];
const sbom = _sbom ?? [];
const eps = _eps ?? [];
const tds = _tds ?? [];
const workstreams = _workstreams ?? [];
2026-03-01 18:53:25 +01:00
// Lookups
const domainById = Object.fromEntries(domains.map(d => [d.id, d]));
const domainBySlug = Object.fromEntries(domains.map(d => [d.slug, d]));
2026-03-02 08:42:30 +01:00
// 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])
);
2026-03-01 18:53:25 +01:00
// 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);
2026-03-02 08:42:30 +01:00
const integrating = !!integratingBySlug[r.slug];
2026-03-01 18:53:25 +01:00
return {
2026-03-02 08:42:30 +01:00
_id: r.id,
_domSlug: domSlug,
_hasSbom: hasSbom,
_integrating: integrating,
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,
2026-03-01 18:53:25 +01:00
};
})
.sort((a, b) => a._domSlug.localeCompare(b._domSlug) || a.repo.localeCompare(b.repo));
2026-03-02 08:42:30 +01:00
const gapCount = repoRows.filter(r => !r._hasSbom).length;
const coveredCount = repoRows.filter(r => r._hasSbom).length;
const integratingCount = repoRows.filter(r => r._integrating).length;
2026-03-01 18:53:25 +01:00
```
# Repos
feat(dashboard): nav restructure, full context-help coverage, 11 new ref docs
Navigation:
- New order: Overview · Todo · Domains · Repos · Workstreams (collapsible,
open:false, with atomic sub-entries: Decisions, Tasks, Debt, Extends,
Dependencies) · Contributions · SBOM · Progress · Reference (collapsible)
- Reference section gains path:/reference landing page; all 18 doc pages
listed in nav (alphabetical) and in reference.md table
New pages:
- todo.md — Internal / Ecosystem / Third-party todo classification
- dependencies.md — dependency edge table derived from state/summary
- reference.md — Reference landing page with full doc index
New reference doc pages (11):
contributions, debt, dependencies, domains, extensions, overview,
repos, tasks, todo + reference (meta) already added previously
doc-overlay.js — lazy bubblehelp tooltip:
- _titleCache Map + _fetchDocTitle(docPath): on first hover of any ?
button, fetches the target doc page, parses <h1>, sets btn.title
- Native browser tooltip appears exactly on the ? circle on subsequent hover
Context-help wired on all 14 dashboard pages:
- h1 withDocHelp added to: index, todo, domains, repos, tasks, techdept,
extensions, dependencies (contributions/workstreams/decisions/sbom/
progress/reference were already wired)
- domains.md + repos.md: added missing withDocHelp import and live-data link
- tasks/techdept/extensions: removed duplicate _h1 const that caused
SyntaxError: Identifier '_h1' has already been declared
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 23:46:26 +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/repos"); }
```
2026-03-01 18:53:25 +01:00
```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 >
2026-03-02 08:42:30 +01:00
< 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 >
2026-03-01 18:53:25 +01:00
< 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 > `);
```
## Coverage Map
```js
2026-03-02 13:14:16 +01:00
// 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>` ;
2026-03-02 13:31:08 +01:00
withDocHelp(el, "/docs/sbom");
2026-03-02 13:14:16 +01:00
return el;
}
2026-03-01 18:53:25 +01:00
// 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 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 >
< / div >
< table class = "repo-table" >
< thead > < tr >
< th > Repo< / th >
2026-03-02 08:42:30 +01:00
< th > Status< / th >
2026-03-01 18:53:25 +01:00
< th > SBOM< / th >
< th > Packages< / th >
< th > Local path< / th >
< / tr > < / thead >
< tbody >
2026-03-02 08:42:30 +01:00
${rows.map(r => html`< tr class = "${r._integrating ? 'row-integrating' : r._hasSbom ? '' : 'row-gap'}" >
2026-03-01 18:53:25 +01:00
< td class = "repo-cell" > < code > ${r.repo}< / code > < / td >
2026-03-02 08:42:30 +01:00
< td > ${r._integrating
? html`<span class="chip chip-integrating">⚙ integrating</span>`
: html`<span class="chip chip-ok">ready</span>` }</ td >
2026-03-02 13:14:16 +01:00
< td class = "${r._hasSbom ? 'sbom-ok' : 'sbom-warn'}" > ${r._hasSbom ? r.sbom : _sbomGap()}< / td >
2026-03-01 18:53:25 +01:00
< 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 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>` );
```
```js
const filteredRows = repoRows.filter(r =>
(domainFilter.value === "all" || r._domSlug === domainFilter.value) & &
(!gapFilter.value || !r._hasSbom)
);
display(Inputs.table(filteredRows.map(r => ({
Repo: r.repo,
Domain: r.domain,
2026-03-02 08:42:30 +01:00
Status: r.status,
2026-03-01 18:53:25 +01:00
SBOM: r.sbom,
Pkgs: r.pkgs,
"EPs (domain)": r.eps || "—",
"TDs (domain)": r.tds || "—",
Path: r.path,
2026-03-02 08:42:30 +01:00
})), {maxWidth: 1100}));
2026-03-01 18:53:25 +01:00
```
2026-03-02 08:42:30 +01:00
## Onboard a New Repo
2026-03-01 18:53:25 +01:00
```js
2026-03-02 08:42:30 +01:00
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"); }
```
2026-03-01 18:53:25 +01:00
2026-03-02 08:42:30 +01:00
```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 >
2026-03-02 11:16:08 +01:00
< strong > Open the repo in Claude Code and run /init< / strong >
2026-03-02 08:42:30 +01:00
< pre > cd /path/to/repo & & claude< / pre >
2026-03-02 11:16:08 +01:00
< 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 >
2026-03-02 08:42:30 +01:00
< / 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 >
2026-03-01 18:53:25 +01:00
< / 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; }
2026-03-02 13:14:16 +01:00
.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; }
2026-03-01 18:53:25 +01:00
.path-cell { font-family: monospace; font-size: 0.78rem; color: gray; }
2026-03-02 08:42:30 +01:00
.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; }
2026-03-01 18:53:25 +01:00
< / style >