From 1e00e1ac992528a4af9bd04e75e97b73abe309fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralf=20D=2E=20M=C3=BCller?= Date: Mon, 5 Oct 2026 14:19:37 +0200 Subject: [PATCH 1/3] feat: display order and extra cards from overrides.yaml order: lists repos and extras first, the rest follows alphabetically; unknown or duplicate names fail the build. extras: cards without a repository link, validated against repo names, shortlinks and reserved keys. Co-Authored-By: Claude Opus 5.5 --- VERSION | 2 +- generate.py | 62 +++++++++++++++++++-- overrides.yaml | 18 ++++++ tests/test_order_extras.py | 109 +++++++++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 tests/test_order_extras.py diff --git a/VERSION b/VERSION index 0d91a54..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/generate.py b/generate.py index 6494b41..0797c0a 100644 --- a/generate.py +++ b/generate.py @@ -24,6 +24,7 @@ SITE_URL = "https://llm-coding.github.io/" ORG_SITE_REPO = "llm-coding.github.io" RESERVED_KEYS = {"icons", "sl", "index", "llms", "style"} +LAYOUT_KEYS = ("order", "extras") # top-level fields in overrides.yaml that are not repositories KEY_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$") ICON_TYPES = {"image/png": "png", "image/svg+xml": "svg", "image/x-icon": "ico", "image/vnd.microsoft.icon": "ico", "image/jpeg": "jpg", "image/webp": "webp"} @@ -83,6 +84,8 @@ def load_overrides(path): data = yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else None result = {} for key, v in (data or {}).items(): + if key in LAYOUT_KEYS: + continue v = v or {} entry = {f: str(v[f]) for f in ("name", "description") if v.get(f)} entry.update({f: True for f in ("include", "exclude") if v.get(f) is True}) @@ -90,6 +93,50 @@ def load_overrides(path): return result +def load_layout(path): + """Return (order, extras) from overrides.yaml: display order and cards without a repository.""" + data = (yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else None) or {} + order = [str(k) for k in data.get("order") or []] + extras = {str(k): {f: str(v[f]) for f in ("name", "url", "description") if (v or {}).get(f)} + for k, v in (data.get("extras") or {}).items()} + return order, extras + + +def extra_projects(extras): + return [{"name": k, "title": e.get("name") or k, "description": e.get("description", ""), + "url": e.get("url"), "repo_url": None, "icon": None} for k, e in extras.items()] + + +def order_projects(projects, order, known_names): + """Listed names first, in the given order; the rest alphabetically. + Unknown or duplicate names are an error; known but unlisted ones (e.g. archived) are skipped.""" + known = {n.lower() for n in known_names} + rank = {} + for i, name in enumerate(order): + if name.lower() not in known: + raise ValueError(f"Unknown name {name!r} in order") + if name.lower() in rank: + raise ValueError(f"Duplicate name {name!r} in order") + rank[name.lower()] = i + return sorted(projects, key=lambda p: (rank.get(p["name"].lower(), len(order)), + p["name"].lower())) + + +def validate_extras(extras, repo_names, links): + names = {n.lower() for n in repo_names} + for key, e in extras.items(): + if not KEY_RE.match(key): + raise ValueError(f"Invalid extras key {key!r}: use a-z, 0-9 and '-'") + if key in RESERVED_KEYS: + raise ValueError(f"Extras key {key!r} is reserved") + if key in names: + raise ValueError(f"Extras key {key!r} collides with a repository") + if key in links: + raise ValueError(f"Extras key {key!r} collides with a shortlink") + if not is_http(e.get("url") or ""): + raise ValueError(f"Extra {key!r} needs an http(s) URL, got {e.get('url')!r}") + + def load_yaml(path): data = yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else None return {str(k): str(v) for k, v in (data or {}).items()} @@ -217,11 +264,12 @@ def render_index(projects, links, version, generated): for p in projects: icon = (f'' if p.get("icon") else initial_icon(p["name"])) + repo = f' · Repository' if p.get("repo_url") else "" items.append(f"""
  • {icon}

    {esc(p.get("title") or p["name"])}

    {esc(p["description"])}

    -

    {esc(p["url"])} · Repository

    +

    {esc(p["url"])}{repo}

  • """) body = f"""

    LLM-Coding projects

    Open projects about software development with large language models. @@ -317,9 +365,12 @@ def main(): all_repos = gh_api_list(f"orgs/{args.org}/repos?per_page=100") overrides = load_overrides(ROOT / "overrides.yaml") repos = select_projects(all_repos, overrides) + order, extras = load_layout(ROOT / "overrides.yaml") links = load_yaml(ROOT / "links.yaml") # Archived repos keep serving their Pages, so check against every repo name - validate_links(links, [r["name"] for r in all_repos]) + repo_names = [r["name"] for r in all_repos] + validate_links(links, repo_names) + validate_extras(extras, repo_names, links) projects, icons = [], {} for r in repos: @@ -328,9 +379,12 @@ def main(): projects.append({"name": r["name"], "title": display_name(r, overrides), "description": description(r, overrides), "url": url, "repo_url": r["html_url"], "icon": None}) - icon = fetch_icon(url) + projects += extra_projects(extras) + projects = order_projects(projects, order, repo_names + list(extras)) + for p in projects: + icon = fetch_icon(p["url"]) if icon: - icons[r["name"]] = icon + icons[p["name"]] = icon icons = drop_shared_icons(icons) version = (ROOT / "VERSION").read_text().strip() diff --git a/overrides.yaml b/overrides.yaml index 33f42e6..e06b44b 100644 --- a/overrides.yaml +++ b/overrides.yaml @@ -8,6 +8,24 @@ # description: Short description # include: true # list this repo although it is a fork # exclude: true # never list this repo +# +# Two top-level fields are not repositories: +# order: display order on the page and in llms.txt. Repos and extras not listed follow +# alphabetically. Unknown names fail the build. +# extras: cards without a repository. Keys use a-z, 0-9 and '-' and must not collide with +# a repository name or a shortlink. url is required (http/https). + +order: [Semantic-Anchors, vibe-coding-risk-radar, harness-wheel, + ultrathink.club, fahrrad-reparatur-architektur, mistral-vibe, doctoolchain] +extras: + harness-wheel: + name: Harness Coverage Wheel + url: https://llm-coding.github.io/Semantic-Anchors/harness-coverage-wheel.html + description: Check which parts of your agent harness are covered – an interactive wheel, part of Semantic Anchors. + doctoolchain: + name: docToolchain + url: https://doctoolchain.org/ + description: "Docs-as-Code toolchain: AsciiDoc, arc42 and diagrams, built and published automatically." mistral-vibe: name: Mistral Vibe Doku diff --git a/tests/test_order_extras.py b/tests/test_order_extras.py new file mode 100644 index 0000000..308235b --- /dev/null +++ b/tests/test_order_extras.py @@ -0,0 +1,109 @@ +"""Tests for the display order and the extra (non-repository) cards from overrides.yaml.""" +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +import generate as g # noqa: E402 + +LAYOUT = """\ +order: [b, wheel, a] +extras: + wheel: + name: Harness Wheel + url: https://x.org/wheel.html + description: "An interactive wheel: try it." +a: + name: A +""" + + +def card(name, repo_url="https://github.com/LLM-Coding/x"): + return {"name": name, "title": name, "description": "D", + "url": f"https://x.org/{name}/", "repo_url": repo_url, "icon": None} + + +def test_load_overrides_ignores_order_and_extras(tmp_path): + f = tmp_path / "o.yaml" + f.write_text(LAYOUT) + assert g.load_overrides(f) == {"a": {"name": "A"}} + + +def test_load_layout_reads_order_and_extras(tmp_path): + f = tmp_path / "o.yaml" + f.write_text(LAYOUT) + order, extras = g.load_layout(f) + assert order == ["b", "wheel", "a"] + assert extras == {"wheel": {"name": "Harness Wheel", "url": "https://x.org/wheel.html", + "description": "An interactive wheel: try it."}} + + +def test_load_layout_without_fields(tmp_path): + f = tmp_path / "o.yaml" + f.write_text("a:\n name: A\n") + assert g.load_layout(f) == ([], {}) + assert g.load_layout(tmp_path / "missing.yaml") == ([], {}) + + +def test_extra_cards_become_projects_without_repository(): + extras = {"wheel": {"name": "Harness Wheel", "url": "https://x.org/w.html", + "description": "Wheel"}} + assert g.extra_projects(extras) == [{"name": "wheel", "title": "Harness Wheel", + "description": "Wheel", "url": "https://x.org/w.html", + "repo_url": None, "icon": None}] + + +def test_order_puts_listed_first_then_rest_alphabetically(): + cards = [card("a"), card("B-new"), card("b"), card("c"), card("wheel", None)] + ordered = g.order_projects(cards, ["b", "wheel", "a"], ["a", "b", "B-new", "c", "wheel"]) + assert [p["name"] for p in ordered] == ["b", "wheel", "a", "B-new", "c"] + + +def test_order_matches_repository_names_case_insensitively(): + ordered = g.order_projects([card("a"), card("Semantic-Anchors")], + ["semantic-anchors"], ["a", "Semantic-Anchors"]) + assert [p["name"] for p in ordered] == ["Semantic-Anchors", "a"] + + +def test_order_skips_known_but_unlisted_repos(): + """An archived repo in the order list must not break the daily build.""" + ordered = g.order_projects([card("a")], ["old", "a"], ["a", "old"]) + assert [p["name"] for p in ordered] == ["a"] + + +@pytest.mark.parametrize("order", [["nope"], ["a", "a"]]) +def test_order_rejects_unknown_or_duplicate_keys(order): + with pytest.raises(ValueError, match="order"): + g.order_projects([card("a")], order, ["a"]) + + +@pytest.mark.parametrize("extras,err", [ + ({"Bad Key": {"name": "X", "url": "https://t/"}}, "key"), + ({"a": {"name": "X", "url": "https://t/"}}, "repository"), + ({"A": {"name": "X", "url": "https://t/"}}, "key"), + ({"f": {"name": "X", "url": "https://t/"}}, "shortlink"), + ({"icons": {"name": "X", "url": "https://t/"}}, "reserved"), + ({"w": {"name": "X", "url": "javascript:alert(1)"}}, "URL"), + ({"w": {"name": "X"}}, "URL"), +]) +def test_validate_extras_rejects_bad_entries(extras, err): + with pytest.raises(ValueError, match=err): + g.validate_extras(extras, ["a"], {"f": "https://t/"}) + + +def test_validate_extras_rejects_repo_name_in_other_case(): + with pytest.raises(ValueError, match="repository"): + g.validate_extras({"semantic-anchors": {"name": "X", "url": "https://t/"}}, + ["Semantic-Anchors"], {}) + + +def test_extra_card_has_no_repository_link_and_no_label(): + html = g.render_index([card("wheel", None)], {}, "1", "d") + assert "Repository" not in html and "None" not in html + assert 'href="https://x.org/wheel/"' in html + assert "external" not in html.lower() and "prototype" not in html.lower() + + +def test_repo_card_keeps_repository_link(): + assert ">Repository" in g.render_index([card("a")], {}, "1", "d") From b400b3fb27ab80a9c14b4fc05639ce1ad6511e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralf=20D=2E=20M=C3=BCller?= Date: Mon, 5 Oct 2026 14:20:11 +0200 Subject: [PATCH 2/3] feat: shortlinks for every project on the start page Co-Authored-By: Claude Opus 5.5 --- links.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/links.yaml b/links.yaml index b4606df..2837aed 100644 --- a/links.yaml +++ b/links.yaml @@ -2,3 +2,8 @@ # Keys: lowercase letters, digits and hyphens. They must not collide with a repository name. f: https://llm-coding.github.io/fahrrad-reparatur-architektur/ sa: https://llm-coding.github.io/Semantic-Anchors/ +rr: https://llm-coding.github.io/vibe-coding-risk-radar/ +hcw: https://llm-coding.github.io/Semantic-Anchors/harness-coverage-wheel.html +utc: https://ultrathink.club/ +mv: https://llm-coding.github.io/mistral-vibe/ +dtc: https://doctoolchain.org/ From 9e2bb3cb931c5a9861eebab0283ac2dbcfc388f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralf=20D=2E=20M=C3=BCller?= Date: Mon, 5 Oct 2026 14:21:30 +0200 Subject: [PATCH 3/3] fix: harden order/extras after review Extras never take a repository's icon away, malformed order/extras fail with a clear ValueError, and extras need a description. Co-Authored-By: Claude Opus 5.5 --- generate.py | 22 +++++++++++++++++----- overrides.yaml | 3 ++- tests/test_order_extras.py | 28 +++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/generate.py b/generate.py index 0797c0a..6843d63 100644 --- a/generate.py +++ b/generate.py @@ -96,9 +96,14 @@ def load_overrides(path): def load_layout(path): """Return (order, extras) from overrides.yaml: display order and cards without a repository.""" data = (yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else None) or {} + if not isinstance(data.get("order") or [], list): + raise ValueError("order must be a list, e.g. order: [a, b]") + extras = data.get("extras") or {} + if not isinstance(extras, dict) or not all(isinstance(v, dict) for v in extras.values()): + raise ValueError("extras must map each key to name, url and description") order = [str(k) for k in data.get("order") or []] extras = {str(k): {f: str(v[f]) for f in ("name", "url", "description") if (v or {}).get(f)} - for k, v in (data.get("extras") or {}).items()} + for k, v in extras.items()} return order, extras @@ -135,6 +140,8 @@ def validate_extras(extras, repo_names, links): raise ValueError(f"Extras key {key!r} collides with a shortlink") if not is_http(e.get("url") or ""): raise ValueError(f"Extra {key!r} needs an http(s) URL, got {e.get('url')!r}") + if not e.get("description"): + raise ValueError(f"Extra {key!r} needs a description") def load_yaml(path): @@ -212,12 +219,17 @@ def fetch_icon(site_url): return None -def drop_shared_icons(icons): - """Drop icons used by several sites (e.g. the docToolchain default) — they tell nothing apart.""" +def drop_shared_icons(icons, extras=()): + """Drop icons used by several sites (e.g. the docToolchain default) — they tell nothing apart. + An extra that shares an icon loses it, but never takes it away from a repository.""" digests = {} for name, (_, data, *_) in icons.items(): digests.setdefault(hashlib.sha256(data).hexdigest(), []).append(name) - shared = {n for names in digests.values() if len(names) > 1 for n in names} + shared = set() + for names in digests.values(): + repos = [n for n in names if n not in extras] + shared |= set(names) - set(repos) if len(names) > 1 else set() + shared |= set(repos) if len(repos) > 1 else set() return {n: v for n, v in icons.items() if n not in shared} @@ -385,7 +397,7 @@ def main(): icon = fetch_icon(p["url"]) if icon: icons[p["name"]] = icon - icons = drop_shared_icons(icons) + icons = drop_shared_icons(icons, set(extras)) version = (ROOT / "VERSION").read_text().strip() generated = datetime.now(timezone.utc).strftime("%Y-%m-%d") diff --git a/overrides.yaml b/overrides.yaml index e06b44b..65b9996 100644 --- a/overrides.yaml +++ b/overrides.yaml @@ -13,7 +13,8 @@ # order: display order on the page and in llms.txt. Repos and extras not listed follow # alphabetically. Unknown names fail the build. # extras: cards without a repository. Keys use a-z, 0-9 and '-' and must not collide with -# a repository name or a shortlink. url is required (http/https). +# a repository name or a shortlink. url (http/https) and description are required. +# Because of these fields, repositories named order or extras cannot be overridden. order: [Semantic-Anchors, vibe-coding-risk-radar, harness-wheel, ultrathink.club, fahrrad-reparatur-architektur, mistral-vibe, doctoolchain] diff --git a/tests/test_order_extras.py b/tests/test_order_extras.py index 308235b..7b5f108 100644 --- a/tests/test_order_extras.py +++ b/tests/test_order_extras.py @@ -84,7 +84,7 @@ def test_order_rejects_unknown_or_duplicate_keys(order): ({"A": {"name": "X", "url": "https://t/"}}, "key"), ({"f": {"name": "X", "url": "https://t/"}}, "shortlink"), ({"icons": {"name": "X", "url": "https://t/"}}, "reserved"), - ({"w": {"name": "X", "url": "javascript:alert(1)"}}, "URL"), + ({"w": {"name": "X", "url": "javascript:alert(1)", "description": "D"}}, "URL"), ({"w": {"name": "X"}}, "URL"), ]) def test_validate_extras_rejects_bad_entries(extras, err): @@ -107,3 +107,29 @@ def test_extra_card_has_no_repository_link_and_no_label(): def test_repo_card_keeps_repository_link(): assert ">Repository" in g.render_index([card("a")], {}, "1", "d") + + +def test_extra_sharing_a_repo_icon_loses_it_but_the_repo_keeps_it(): + """Review: an extra inside a repo's site must not take that repo's icon away.""" + icons = {"repo": ("png", b"same", "u"), "extra": ("png", b"same", "u"), + "r1": ("png", b"dup", "u"), "r2": ("png", b"dup", "u"), + "own": ("png", b"own", "u")} + assert g.drop_shared_icons(icons, extras={"extra", "own"}) == { + "repo": ("png", b"same", "u"), "own": ("png", b"own", "u")} + + +@pytest.mark.parametrize("text", [ + "order: harness-wheel\n", + "extras: [a, b]\n", + "extras:\n w: https://x.org/\n", +]) +def test_load_layout_rejects_malformed_fields(tmp_path, text): + f = tmp_path / "o.yaml" + f.write_text(text) + with pytest.raises(ValueError, match="order|extras"): + g.load_layout(f) + + +def test_validate_extras_requires_description(): + with pytest.raises(ValueError, match="description"): + g.validate_extras({"w": {"name": "X", "url": "https://t/"}}, [], {})