generated from coulomb/repo-seed
Some checks failed
ci / validate-registry (push) Has been cancelled
Register six new capabilities (12 total), add searchable catalog UI and graph explorer, introduce pytest suite with CI fail-on-warnings, and close gap analysis priorities 13 and 16. WP-0010 remains backlog for network federation.
220 lines
7.8 KiB
Python
220 lines
7.8 KiB
Python
from __future__ import annotations
|
|
|
|
import html
|
|
import json
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
CATALOG_MD = ROOT / "docs" / "CapabilityCatalog.md"
|
|
CATALOG_HTML_DIR = ROOT / "docs" / "catalog"
|
|
CATALOG_HTML = CATALOG_HTML_DIR / "index.html"
|
|
CATALOG_JSON = CATALOG_HTML_DIR / "registry.json"
|
|
CATALOG_SEARCH = CATALOG_HTML_DIR / "search.html"
|
|
GRAPH_HTML = ROOT / "docs" / "graph" / "index.html"
|
|
|
|
|
|
def _grouped_capabilities(
|
|
indexed_entries: list[tuple[dict[str, Any], dict[str, Any]]],
|
|
) -> dict[str, list[tuple[dict[str, Any], dict[str, Any]]]]:
|
|
grouped: dict[str, list[tuple[dict[str, Any], dict[str, Any]]]] = defaultdict(
|
|
list
|
|
)
|
|
for index_item, entry in indexed_entries:
|
|
domain = index_item.get("domain", "unknown")
|
|
grouped[domain].append((index_item, entry))
|
|
return dict(sorted(grouped.items()))
|
|
|
|
|
|
def render_markdown(
|
|
index: dict[str, Any],
|
|
indexed_entries: list[tuple[dict[str, Any], dict[str, Any]]],
|
|
) -> str:
|
|
lines = [
|
|
"# Capability Catalog",
|
|
"",
|
|
f"**Domain:** {index.get('domain', 'unknown')} ",
|
|
f"**Updated:** {index.get('updated', 'unknown')} ",
|
|
f"**Entries:** {len(indexed_entries)}",
|
|
"",
|
|
"Generated by `reuse-surface catalog`. Do not edit manually.",
|
|
"",
|
|
]
|
|
for domain, items in _grouped_capabilities(indexed_entries).items():
|
|
lines.extend([f"## {domain}", ""])
|
|
for index_item, entry in sorted(items, key=lambda pair: pair[0]["id"]):
|
|
lines.extend(
|
|
[
|
|
f"### {index_item['name']}",
|
|
"",
|
|
f"- **ID:** `{index_item['id']}`",
|
|
f"- **Vector:** {index_item['vector']}",
|
|
f"- **Owner:** {index_item.get('owner', 'unknown')}",
|
|
f"- **Path:** `{index_item['path']}`",
|
|
f"- **Summary:** {index_item['summary']}",
|
|
"",
|
|
]
|
|
)
|
|
guidance = entry.get("consumer_guidance") or {}
|
|
limitations = guidance.get("known_limitations") or []
|
|
if limitations:
|
|
lines.append("**Known limitations:**")
|
|
lines.extend(f"- {item}" for item in limitations)
|
|
lines.append("")
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def render_html(
|
|
index: dict[str, Any],
|
|
indexed_entries: list[tuple[dict[str, Any], dict[str, Any]]],
|
|
) -> str:
|
|
sections: list[str] = []
|
|
for domain, items in _grouped_capabilities(indexed_entries).items():
|
|
cards: list[str] = []
|
|
for index_item, entry in sorted(items, key=lambda pair: pair[0]["id"]):
|
|
name = html.escape(index_item["name"])
|
|
summary = html.escape(index_item["summary"])
|
|
cap_id = html.escape(index_item["id"])
|
|
vector = html.escape(index_item["vector"])
|
|
path = html.escape(index_item["path"])
|
|
cards.append(
|
|
f"""<article class="card">
|
|
<h3>{name}</h3>
|
|
<p class="meta"><code>{cap_id}</code> · {vector}</p>
|
|
<p>{summary}</p>
|
|
<p class="path">{path}</p>
|
|
</article>"""
|
|
)
|
|
sections.append(
|
|
f"<section><h2>{html.escape(domain)}</h2>\n" + "\n".join(cards) + "</section>"
|
|
)
|
|
|
|
body = "\n".join(sections)
|
|
title = html.escape(f"Capability Catalog — {index.get('domain', 'unknown')}")
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{title}</title>
|
|
<style>
|
|
body {{ font-family: system-ui, sans-serif; margin: 2rem; line-height: 1.5; }}
|
|
h1 {{ margin-bottom: 0.2rem; }}
|
|
.subtitle {{ color: #555; margin-bottom: 2rem; }}
|
|
section {{ margin-bottom: 2rem; }}
|
|
.card {{ border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; }}
|
|
.meta {{ color: #444; font-size: 0.95rem; }}
|
|
.path {{ font-size: 0.85rem; color: #666; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Capability Catalog</h1>
|
|
<p class="subtitle">Updated {html.escape(str(index.get('updated', 'unknown')))} · {len(indexed_entries)} entries</p>
|
|
{body}
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def render_search_html() -> str:
|
|
return """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Capability Catalog Search</title>
|
|
<style>
|
|
body { font-family: system-ui, sans-serif; margin: 2rem; line-height: 1.5; }
|
|
input { width: 100%; max-width: 40rem; padding: 0.5rem; font-size: 1rem; }
|
|
.card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; }
|
|
.meta { color: #555; font-size: 0.9rem; }
|
|
.hidden { display: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Capability Catalog</h1>
|
|
<p>Client-side search over <code>registry.json</code>. Generated by <code>reuse-surface catalog</code>.</p>
|
|
<input id="q" type="search" placeholder="Search name, summary, tags, vector..." autofocus>
|
|
<p id="count"></p>
|
|
<div id="results"></div>
|
|
<script>
|
|
let items = [];
|
|
fetch('registry.json').then(r => r.json()).then(data => {
|
|
items = data.capabilities || [];
|
|
render('');
|
|
});
|
|
document.getElementById('q').addEventListener('input', e => render(e.target.value));
|
|
function render(query) {
|
|
const q = query.trim().toLowerCase();
|
|
const matches = items.filter(item => {
|
|
const hay = [item.id, item.name, item.summary, item.vector,
|
|
...(item.tags || []), ...(item.consumption_modes || [])].join(' ').toLowerCase();
|
|
return !q || hay.includes(q);
|
|
});
|
|
document.getElementById('count').textContent = matches.length + ' match(es)';
|
|
document.getElementById('results').innerHTML = matches.map(item => `
|
|
<article class="card">
|
|
<h3>${item.name}</h3>
|
|
<p class="meta"><code>${item.id}</code> · ${item.vector} · ${item.owner}</p>
|
|
<p>${item.summary}</p>
|
|
</article>`).join('');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def render_graph_explorer(mermaid_source: str) -> str:
|
|
escaped = json.dumps(mermaid_source)
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Capability Relation Graph</title>
|
|
<script type="module">
|
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
mermaid.initialize({{ startOnLoad: true, theme: 'neutral' }});
|
|
</script>
|
|
<style>
|
|
body {{ font-family: system-ui, sans-serif; margin: 2rem; }}
|
|
.legend {{ color: #555; margin-bottom: 1rem; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Capability Relation Graph</h1>
|
|
<p class="legend">Generated from entry <code>relations</code> fields. Regenerate with <code>reuse-surface graph</code>.</p>
|
|
<pre class="mermaid" id="graph"></pre>
|
|
<script>
|
|
document.getElementById('graph').textContent = {escaped};
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def write_catalog(
|
|
index: dict[str, Any],
|
|
indexed_entries: list[tuple[dict[str, Any], dict[str, Any]]],
|
|
*,
|
|
mermaid_source: str | None = None,
|
|
) -> list[Path]:
|
|
CATALOG_HTML_DIR.mkdir(parents=True, exist_ok=True)
|
|
written: list[Path] = []
|
|
CATALOG_MD.write_text(render_markdown(index, indexed_entries), encoding="utf-8")
|
|
written.append(CATALOG_MD)
|
|
CATALOG_HTML.write_text(render_html(index, indexed_entries), encoding="utf-8")
|
|
written.append(CATALOG_HTML)
|
|
payload = {
|
|
"domain": index.get("domain"),
|
|
"updated": index.get("updated"),
|
|
"capabilities": [item for item, _ in indexed_entries],
|
|
}
|
|
CATALOG_JSON.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
written.append(CATALOG_JSON)
|
|
CATALOG_SEARCH.write_text(render_search_html(), encoding="utf-8")
|
|
written.append(CATALOG_SEARCH)
|
|
if mermaid_source is not None:
|
|
GRAPH_HTML.parent.mkdir(parents=True, exist_ok=True)
|
|
GRAPH_HTML.write_text(render_graph_explorer(mermaid_source), encoding="utf-8")
|
|
written.append(GRAPH_HTML)
|
|
return written |