From f927a5d1e13241d8672bcf13c97702ed098fd1f0 Mon Sep 17 00:00:00 2001 From: Mohsen Seyedkazemi Ardebili Date: Fri, 2 Oct 2026 23:10:55 +0200 Subject: [PATCH 1/6] fix(docs): emit valid structured data --- docs/overrides/main.html | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 9010f7d..0435541 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -75,11 +75,11 @@ "@type": "Dataset", "@id": "https://mskazemi.com/aobench/#dataset", "name": "AOBench benchmark corpus", - "description": "88 HPC operational task specifications across 10 question categories and 5 operator roles, paired with 29 deterministic environment snapshot bundles. Six environments and eight tasks are grounded in the public Marconi100 ExaData release from CINECA.", + "description": "90 HPC operational task specifications across 10 question categories and 5 operator roles, paired with 29 deterministic environment snapshot bundles. Six environments and eight tasks are grounded in the public Marconi100 ExaData release from CINECA.", "url": "https://mskazemi.com/aobench/reference/task-catalog/", "license": "https://www.apache.org/licenses/LICENSE-2.0", "identifier": "https://doi.org/10.5281/zenodo.21854862", - "creator": { "@id": "https://mskazemi.com/#person" }, + "creator": { "@type": "Person", "@id": "https://mskazemi.com/#person" }, "isAccessibleForFree": true, "measurementTechnique": "Trace-based scoring across outcome correctness, tool-use correctness, governance/RBAC, grounding, robustness (pass^k), and efficiency", "variableMeasured": [ @@ -141,6 +141,7 @@ "author": { "@id": "https://mskazemi.com/#person" }, "license": "https://www.apache.org/licenses/LICENSE-2.0" }, + {%- if page and page.canonical_url != config.site_url %} { "@type": "BreadcrumbList", "@id": "{{ page.canonical_url }}#breadcrumb", @@ -149,38 +150,38 @@ "@type": "ListItem", "position": 1, "name": "AOBench", - "item": "https://mskazemi.com/aobench/" + "item": "{{ config.site_url }}" } - {%- if page and page.ancestors %} + {%- set crumb = namespace(position=1) %} + {%- if page.ancestors %} {%- for ancestor in page.ancestors | reverse %} + {%- if ancestor.url %} + {%- set crumb.position = crumb.position + 1 %} ,{ "@type": "ListItem", - "position": {{ loop.index + 1 }}, - "name": {{ ancestor.title | string | tojson }} + "position": {{ crumb.position }}, + "name": {{ ancestor.title | string | tojson }}, + "item": "{{ config.site_url }}{{ ancestor.url }}" } + {%- endif %} {%- endfor %} + {%- endif %} + {%- set crumb.position = crumb.position + 1 %} ,{ "@type": "ListItem", - "position": {{ page.ancestors | length + 2 }}, - "name": {{ page_title | tojson }}, - "item": "{{ page.canonical_url }}" - } - {%- elif page and page.canonical_url != config.site_url %} - ,{ - "@type": "ListItem", - "position": 2, + "position": {{ crumb.position }}, "name": {{ page_title | tojson }}, "item": "{{ page.canonical_url }}" } - {%- endif %} ] } + {%- endif %} ] } {% endblock %} {% block announce %} - AOBench v0.4.1 — 88 tasks · 29 environments · 6 grounded in real Marconi100 data. + AOBench v0.4.1 — 90 tasks · 29 environments · 6 grounded in real Marconi100 data.  Read the changelog → {% endblock %} From f47b1b147eb553e1404b3c202c9b3e302999561f Mon Sep 17 00:00:00 2001 From: Mohsen Seyedkazemi Ardebili Date: Fri, 2 Oct 2026 23:10:58 +0200 Subject: [PATCH 2/6] test(docs): audit JSON-LD sitewide --- scripts/seo_check.py | 122 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/scripts/seo_check.py b/scripts/seo_check.py index d27ff0e..2f10064 100644 --- a/scripts/seo_check.py +++ b/scripts/seo_check.py @@ -21,6 +21,7 @@ import re import sys from pathlib import Path +from urllib.parse import urlparse from xml.etree import ElementTree ROOT = Path(__file__).resolve().parent.parent @@ -32,7 +33,6 @@ "Person", "WebSite", "TechArticle", - "BreadcrumbList", } #: Crawlers that must not be blocked. Being absent from robots.txt is fine (the @@ -173,9 +173,126 @@ def check_page(self, rel: str) -> None: for node in data.get("@graph", [data]): if isinstance(node, dict) and "@type" in node: types.add(str(node["@type"])) - missing = REQUIRED_SCHEMA_TYPES - types + required = set(REQUIRED_SCHEMA_TYPES) + if rel != "index.html": + required.add("BreadcrumbList") + missing = required - types self.check(not missing, f"{rel}: JSON-LD schema types", f"missing {sorted(missing)}") + def _breadcrumb_target_exists(self, url: str) -> bool: + parsed = urlparse(url) + if parsed.scheme != "https" or parsed.netloc != "mskazemi.com": + return False + prefix = "/aobench/" + if not parsed.path.startswith(prefix): + return False + suffix = parsed.path[len(prefix) :].lstrip("/") + if not suffix: + return (self.site / "index.html").is_file() + target = self.site / suffix + if parsed.path.endswith("/"): + target = target / "index.html" + return target.is_file() + + def check_structured_data(self, rel: str, html: str) -> None: + blocks = re.findall( + r'', + html, + re.DOTALL, + ) + nodes: list[dict[str, object]] = [] + for block in blocks: + try: + data = json.loads(block) + except json.JSONDecodeError: + continue + if not isinstance(data, dict): + continue + graph = data.get("@graph", [data]) + if isinstance(graph, list): + nodes.extend(node for node in graph if isinstance(node, dict)) + + breadcrumbs = [node for node in nodes if node.get("@type") == "BreadcrumbList"] + if rel == "index.html": + self.check( + not breadcrumbs, + f"{rel}: homepage omits single-item BreadcrumbList", + "Google requires at least two ListItems", + ) + else: + self.check(bool(breadcrumbs), f"{rel}: has BreadcrumbList") + + for breadcrumb in breadcrumbs: + items = breadcrumb.get("itemListElement") + valid_items = isinstance(items, list) and len(items) >= 2 + self.check(valid_items, f"{rel}: breadcrumb has at least two ListItems") + if not valid_items: + continue + + positions = [ + item.get("position") if isinstance(item, dict) else None for item in items + ] + self.check( + positions == list(range(1, len(items) + 1)), + f"{rel}: breadcrumb positions are contiguous", + f"found {positions}", + ) + + for position, item in enumerate(items, start=1): + is_list_item = isinstance(item, dict) and item.get("@type") == "ListItem" + self.check(is_list_item, f"{rel}: breadcrumb {position} is a ListItem") + if not isinstance(item, dict): + continue + self.check( + isinstance(item.get("name"), str) and bool(item.get("name")), + f"{rel}: breadcrumb {position} has a name", + ) + url = item.get("item") + valid_url = isinstance(url, str) and url.startswith("https://") + self.check(valid_url, f"{rel}: breadcrumb {position} has an absolute item URL") + if valid_url: + self.check( + self._breadcrumb_target_exists(url), + f"{rel}: breadcrumb {position} resolves inside the built site", + str(url), + ) + + datasets = [node for node in nodes if node.get("@type") == "Dataset"] + for dataset in datasets: + creators = dataset.get("creator") + if not isinstance(creators, list): + creators = [creators] + typed_creators = bool(creators) and all( + isinstance(creator, dict) + and creator.get("@type") in {"Person", "Organization"} + for creator in creators + ) + self.check( + typed_creators, + f"{rel}: Dataset.creator is typed Person/Organization", + ) + + distributions = dataset.get("distribution") + if not isinstance(distributions, list): + distributions = [distributions] + valid_distributions = bool(distributions) and all( + isinstance(distribution, dict) + and bool(distribution.get("contentUrl")) + and bool(distribution.get("encodingFormat")) + for distribution in distributions + ) + self.check( + valid_distributions, + f"{rel}: Dataset distributions declare URL and encodingFormat", + ) + + def check_sitewide_structured_data(self) -> None: + pages = sorted(self.site.rglob("index.html")) + self.check(bool(pages), "structured-data audit found built pages") + for path in pages: + rel = path.relative_to(self.site).as_posix() + self.check_structured_data(rel, path.read_text(encoding="utf-8")) + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) @@ -195,6 +312,7 @@ def main() -> int: checker.check_llms_txt() for page in KEY_PAGES: checker.check_page(page) + checker.check_sitewide_structured_data() if checker.failures: print(f"SEO check FAILED — {len(checker.failures)} problem(s):\n", file=sys.stderr) From 286212c0880f0bdbe23675490558526bf6fa7bb6 Mon Sep 17 00:00:00 2001 From: Mohsen Seyedkazemi Ardebili Date: Fri, 2 Oct 2026 23:11:01 +0200 Subject: [PATCH 3/6] docs(deploy): sync current task count --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 126fee6..3f7c363 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -103,7 +103,7 @@ jobs: AOBench — benchmark for AI agents that operate HPC systems - + @@ -111,7 +111,7 @@ jobs:

AOBench — Agent Operations Benchmark

AOBench is an open-source benchmark for AI agents that operate High-Performance - Computing systems: 88 tasks across 10 question categories and 5 operator roles, 29 + Computing systems: 90 tasks across 10 question categories and 5 operator roles, 29 deterministic environment snapshots — 6 of them rebuilt from real CINECA Marconi100 ExaData — role-based access control enforced so that a policy violation hard-fails the task, and 12 scorers over 7 weighted dimensions. It runs offline, with no From 4a1c53f8cf649805e9286d8ed58fcd152f785ec8 Mon Sep 17 00:00:00 2001 From: Mohsen Seyedkazemi Ardebili Date: Fri, 2 Oct 2026 23:11:04 +0200 Subject: [PATCH 4/6] docs(changelog): record structured-data fix --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74184d8..608c47f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +### Fixed — structured data now matches Google's breadcrumb and Dataset requirements + +- Breadcrumb JSON-LD now emits only real navigable ancestors, gives every emitted + `ListItem` an absolute `item` URL, and keeps positions contiguous when a grouping + section has no index page. The docs homepage no longer publishes an ineligible + single-item `BreadcrumbList`. +- The AOBench `Dataset.creator` is explicitly typed as `Person`, and structured + discovery surfaces now report the current 90-task corpus. +- `scripts/seo_check.py` now audits JSON-LD on every built documentation page, including + breadcrumb shape/link resolution and typed Dataset creators/distributions, so this + Search Console failure class is regression-gated. + ### Added — a second README demo, recorded on Windows - `docs/assets/demo-windows.gif` ([issue #9](https://github.com/MSKazemi/aobench/issues/9), From 66b3621d825b5612c415c000ac18f569a66ff2eb Mon Sep 17 00:00:00 2001 From: Mohsen Seyedkazemi Ardebili Date: Fri, 2 Oct 2026 23:12:15 +0200 Subject: [PATCH 5/6] fix(docs): correct JSON-LD audit regex --- scripts/seo_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/seo_check.py b/scripts/seo_check.py index 2f10064..901ccfa 100644 --- a/scripts/seo_check.py +++ b/scripts/seo_check.py @@ -196,7 +196,7 @@ def _breadcrumb_target_exists(self, url: str) -> bool: def check_structured_data(self, rel: str, html: str) -> None: blocks = re.findall( - r'', + r'', html, re.DOTALL, ) From 5638fd0736796803d43c02001b6a07be19b4ad36 Mon Sep 17 00:00:00 2001 From: Mohsen Seyedkazemi Ardebili Date: Fri, 2 Oct 2026 23:14:51 +0200 Subject: [PATCH 6/6] fix(docs): keep homepage JSON-LD valid --- docs/overrides/main.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 0435541..77baf01 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -140,9 +140,9 @@ "about": { "@id": "https://mskazemi.com/aobench/#software" }, "author": { "@id": "https://mskazemi.com/#person" }, "license": "https://www.apache.org/licenses/LICENSE-2.0" - }, + } {%- if page and page.canonical_url != config.site_url %} - { + ,{ "@type": "BreadcrumbList", "@id": "{{ page.canonical_url }}#breadcrumb", "itemListElement": [