generated from coulomb/repo-seed
Complete WP-0006 through WP-0009: registry expansion, catalog, graph, tests
Some checks failed
ci / validate-registry (push) Has been cancelled
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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -9,6 +10,9 @@ 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(
|
||||
@@ -112,11 +116,105 @@ def render_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]]],
|
||||
) -> tuple[Path, Path]:
|
||||
*,
|
||||
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")
|
||||
return CATALOG_MD, CATALOG_HTML
|
||||
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
|
||||
@@ -64,7 +64,7 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
||||
for error in errors:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
|
||||
if errors:
|
||||
if errors or (args.fail_on_warnings and warnings):
|
||||
return 1
|
||||
print(f"ok: validated {len(paths)} capability entr{'y' if len(paths) == 1 else 'ies'}")
|
||||
return 0
|
||||
@@ -167,18 +167,27 @@ def cmd_graph(args: argparse.Namespace) -> int:
|
||||
print(content, end="")
|
||||
else:
|
||||
path = write_graph()
|
||||
from reuse_surface.catalog import GRAPH_HTML, render_graph_explorer
|
||||
|
||||
GRAPH_HTML.parent.mkdir(parents=True, exist_ok=True)
|
||||
GRAPH_HTML.write_text(render_graph_explorer(content), encoding="utf-8")
|
||||
print(f"ok: wrote {path.relative_to(ROOT)}")
|
||||
print(f"ok: wrote {GRAPH_HTML.relative_to(ROOT)}")
|
||||
for warning in warnings:
|
||||
print(f"warning: {warning}", file=sys.stderr)
|
||||
if args.fail_on_warnings and warnings:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_catalog(args: argparse.Namespace) -> int:
|
||||
index = load_index()
|
||||
indexed_entries = _load_indexed_entries()
|
||||
md_path, html_path = write_catalog(index, indexed_entries)
|
||||
print(f"ok: wrote {md_path.relative_to(ROOT)}")
|
||||
print(f"ok: wrote {html_path.relative_to(ROOT)}")
|
||||
paths = write_catalog(
|
||||
index, indexed_entries, mermaid_source=render_mermaid()
|
||||
)
|
||||
for path in paths:
|
||||
print(f"ok: wrote {path.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -237,6 +246,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
action="store_true",
|
||||
help="check relation cycles and broken references",
|
||||
)
|
||||
validate.add_argument(
|
||||
"--fail-on-warnings",
|
||||
action="store_true",
|
||||
help="exit non-zero when warnings are present",
|
||||
)
|
||||
validate.set_defaults(func=cmd_validate)
|
||||
|
||||
federation = subparsers.add_parser(
|
||||
@@ -290,6 +304,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
action="store_true",
|
||||
help="report depends_on cycles and broken relation references",
|
||||
)
|
||||
graph.add_argument(
|
||||
"--fail-on-warnings",
|
||||
action="store_true",
|
||||
help="exit non-zero when relation warnings are present",
|
||||
)
|
||||
graph.set_defaults(func=cmd_graph)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
Reference in New Issue
Block a user