diff --git a/.github/workflows/project-index-ci.yml b/.github/workflows/project-index-ci.yml new file mode 100644 index 0000000..90b84ea --- /dev/null +++ b/.github/workflows/project-index-ci.yml @@ -0,0 +1,20 @@ +name: Project index CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + validate-project-index: + name: Validate project index + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Validate docs/project-index.json + run: python scripts/validate_project_index.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec221f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Python bytecode and cache +__pycache__/ +*.py[cod] +*$py.class diff --git a/README.md b/README.md index 7ee9172..afda329 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Generators and ops tooling live in - [`profile/README.md`](profile/README.md) — organization profile - [`docs/`](docs/) — roadmap, architecture, evaluation summary +- [`docs/project-index.json`](docs/project-index.json) — machine-readable project index (source of truth for the project list) - [`claims/`](claims/), [`releases/`](releases/), [`media/`](media/) — public ledgers - [`assets/`](assets/) — logo and product matrix @@ -28,6 +29,8 @@ references are never orphaned when content moves between repos. Prefer the - Product matrix image (raw URL for embedding): `https://raw.githubusercontent.com/WasmAgent/.github/main/assets/product-matrix.svg` +- Project index (machine-readable repo, role, and status registry): + `https://github.com/WasmAgent/.github/blob/main/docs/project-index.json` - Claims registry: `https://github.com/WasmAgent/.github/blob/main/claims/public-claims.yml` - Release ledger: diff --git a/docs/project-index.json b/docs/project-index.json new file mode 100644 index 0000000..13aec20 --- /dev/null +++ b/docs/project-index.json @@ -0,0 +1,139 @@ +{ + "schema_version": 1, + "org": "WasmAgent", + "description": "Machine-readable source of truth for the WasmAgent project index. Lists every repository in the organization with its category, role, status, and visibility. Consumed by the org profile (profile/README.md project table) and the living roadmap (docs/roadmap.md) so the public project matrix is rendered from a single registry instead of maintained by hand, preventing omissions.", + "source_url": "https://github.com/WasmAgent/.github/blob/main/docs/project-index.json", + "last_reviewed": "2026-07-02", + "consumers": [ + "profile/README.md — Projects table", + "docs/roadmap.md — project layers" + ], + "status_legend": { + "shipped": "Public repository exists and is the source of truth for its layer.", + "in_progress": "Spec or reference implementation landing.", + "planned": "Not yet public." + }, + "categories": { + "project-home": "Public landing page that directs readers to the org hub.", + "org-hub": "Org-wide documentation, ledgers, and source of truth for the project list.", + "runtime": "Embedded agent runtime.", + "workload": "Reference agent workload.", + "evidence-pipeline": "Trace ingestion, evidence admission, and training audit.", + "trust-artifacts": "Machine-readable identity and policy posture artifacts.", + "audit": "Enterprise audit product.", + "evaluation": "Adversarial evaluation protocol.", + "internal-tool": "Internal automation or operations; ships no public product." + }, + "repos": [ + { + "name": "wasmagent", + "category": "project-home", + "role": "Project home", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "Public landing page that directs readers to .github for the full roadmap and project list.", + "url": "https://github.com/WasmAgent/wasmagent" + }, + { + "name": ".github", + "category": "org-hub", + "role": "Org hub", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "Org-wide documentation and ledger hub; public home for the roadmap, claims registry, release ledger, and cross-repo coordination.", + "url": "https://github.com/WasmAgent/.github" + }, + { + "name": "wasmagent-js", + "category": "runtime", + "role": "Runtime", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "Embedded agent runtime: WASM sandbox, MCP firewall, capability manifests, signed AEP event emitter.", + "url": "https://github.com/WasmAgent/wasmagent-js" + }, + { + "name": "bscode", + "category": "workload", + "role": "Workload", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "Reference coding-agent workload on Cloudflare Workers with AEP evidence export.", + "url": "https://github.com/WasmAgent/bscode" + }, + { + "name": "trace-pipeline", + "category": "evidence-pipeline", + "role": "Evidence pipeline", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "Trace-to-training backend and data factory; ingests AEP traces, gates training-data admission, and records every training run as auditable evidence.", + "url": "https://github.com/WasmAgent/trace-pipeline" + }, + { + "name": "agent-trust-infra", + "category": "trust-artifacts", + "role": "Trust artifacts", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "AgentBOM, MCP Posture, and Trust Passport spec, reference implementation, and CLI.", + "url": "https://github.com/WasmAgent/agent-trust-infra" + }, + { + "name": "open-agent-audit", + "category": "audit", + "role": "Audit", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "Enterprise audit product; deployed at trustavo.com.", + "url": "https://github.com/WasmAgent/open-agent-audit" + }, + { + "name": "fresharena", + "category": "evaluation", + "role": "Evaluation", + "status": "shipped", + "visibility": "public", + "in_profile": true, + "summary": "Dynamic, verifiable, adversarial evaluation protocol for coding agents.", + "url": "https://github.com/WasmAgent/fresharena" + }, + { + "name": "claude-bot", + "category": "internal-tool", + "role": "Internal automation", + "status": "shipped", + "visibility": "internal", + "in_profile": true, + "summary": "Internal automation: issue triage, PR review, and cross-repo coherence patrol. Not a public product.", + "url": "https://github.com/WasmAgent/claude-bot" + }, + { + "name": "wasmagent-ops", + "category": "internal-tool", + "role": "Internal operations", + "status": "shipped", + "visibility": "internal", + "in_profile": true, + "summary": "Private operations hub: media, release, research, and security operations for the org. Not a public product.", + "url": "https://github.com/WasmAgent/wasmagent-ops" + }, + { + "name": "erp-agent", + "category": "workload", + "role": "Workload (planned)", + "status": "planned", + "visibility": "public", + "in_profile": false, + "summary": "Planned ERP-domain workload with order-state and ledger verifiers, mirroring the role bscode plays for coding tasks.", + "url": "https://github.com/WasmAgent/erp-agent" + } + ] +} diff --git a/docs/roadmap.md b/docs/roadmap.md index 917f866..a34240a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,7 +3,8 @@ Living roadmap for the WasmAgent organization. Ticked items ship as public repositories under [github.com/WasmAgent](https://github.com/WasmAgent); unticked items are planned or in progress. This document mirrors -`gh repo list WasmAgent --visibility public`. +`gh repo list WasmAgent --visibility public`, and its machine-readable +counterpart is [`project-index.json`](project-index.json). ## Status legend diff --git a/profile/README.md b/profile/README.md index ace8f13..c5525cc 100644 --- a/profile/README.md +++ b/profile/README.md @@ -4,6 +4,11 @@ Protect agent runs. Record evidence. Audit claims. Train only from trusted trace ## Projects +The table below is the human-readable view of +[`docs/project-index.json`](../docs/project-index.json), the machine-readable +source of truth for the project list. Profile generation consumes that index so +the public matrix stays complete and in sync across repos. + | Repository | Role | | --- | --- | | [wasmagent](https://github.com/WasmAgent/wasmagent) | **Project home** · Public landing page that directs readers to [`.github`](https://github.com/WasmAgent/.github) for the full roadmap and project list | @@ -77,6 +82,7 @@ org, not any single product. - [Claims registry](../claims/public-claims.yml) — org claims mapped to evidence and review status - [Release ledger](../releases/public-release-ledger.yml) — public releases across repositories - [Media & posts](../media/posts.yml) — talks, posts, and appearances +- [Project index](../docs/project-index.json) — machine-readable source of truth for the project list - [Roadmap](../docs/roadmap.md) — living roadmap mirroring the public repo list ## Disclaimer diff --git a/scripts/validate_project_index.py b/scripts/validate_project_index.py new file mode 100755 index 0000000..15fb16e --- /dev/null +++ b/scripts/validate_project_index.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Validate docs/project-index.json and check coherence with the org profile. + +The project index is the machine-readable source of truth for the WasmAgent +project list. This validator enforces: + +1. JSON is well-formed and matches the expected schema. +2. Controlled vocabularies (status, visibility, category) are respected. +3. Repository names are unique and each url matches the org + name. +4. Bidirectional coherence with profile/README.md: + - every index repo flagged ``in_profile`` appears in the profile table; + - every repo in the profile table is present in the index with + ``in_profile: true``. + +The coherence check is what prevents the profile project table from silently +omitting repositories (the failure mode described in WasmAgent/.github#53). + +Exit code is 0 on success, 1 on any validation failure. Uses only the Python +standard library so it runs in any CI image with Python. +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from datetime import date + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +INDEX_PATH = os.path.join(REPO_ROOT, "docs", "project-index.json") +PROFILE_PATH = os.path.join(REPO_ROOT, "profile", "README.md") + +REQUIRED_TOP = ("schema_version", "org", "last_reviewed", "repos") +REQUIRED_REPO = ( + "name", + "category", + "role", + "status", + "visibility", + "in_profile", + "summary", + "url", +) +VALID_STATUS = {"shipped", "in_progress", "planned"} +VALID_VISIBILITY = {"public", "internal"} +URL_RE = re.compile(r"^https://github\.com/WasmAgent/(?P[^)/\s]+)$") +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +PROFILE_LINK_RE = re.compile(r"https://github\.com/WasmAgent/([^)/\s]+)") + + +class ValidationFailed(Exception): + """Raised when validation encounters an error.""" + + +def fail(msg: str) -> None: + raise ValidationFailed(msg) + + +def load_index() -> dict: + if not os.path.isfile(INDEX_PATH): + fail(f"missing project index: {INDEX_PATH}") + try: + with open(INDEX_PATH, encoding="utf-8") as fh: + data = json.load(fh) + except json.JSONDecodeError as exc: + fail(f"project index is not valid JSON: {exc}") + if not isinstance(data, dict): + fail("project index top level must be a JSON object") + return data + + +def validate_top_level(data: dict) -> None: + for key in REQUIRED_TOP: + if key not in data: + fail(f"missing top-level field: {key!r}") + if not isinstance(data["schema_version"], int): + fail("schema_version must be an integer") + if data["schema_version"] != 1: + fail(f"unsupported schema_version {data['schema_version']!r}; expected 1") + if data.get("org") != "WasmAgent": + fail(f"unexpected org {data.get('org')!r}; expected 'WasmAgent'") + last = data.get("last_reviewed") + if not isinstance(last, str) or not DATE_RE.match(last): + fail(f"last_reviewed must be YYYY-MM-DD, got {last!r}") + try: + date.fromisoformat(last) + except ValueError: + fail(f"last_reviewed is not a real calendar date: {last!r}") + categories = data.get("categories", {}) + if not isinstance(categories, dict): + fail("categories must be an object mapping category -> description") + + +def validate_repo(repo: dict, categories: dict, seen: set[str]) -> None: + if not isinstance(repo, dict): + fail("each repo entry must be a JSON object") + for key in REQUIRED_REPO: + if key not in repo: + fail(f"repo entry missing required field: {key!r} (in {repo})") + name = repo["name"] + if not isinstance(name, str) or not name: + fail(f"repo name must be a non-empty string (got {name!r})") + if name in seen: + fail(f"duplicate repo name in index: {name!r}") + seen.add(name) + if repo["category"] not in categories: + fail( + f"repo {name!r} has unknown category {repo['category']!r}; " + f"expected one of {sorted(categories)}" + ) + if repo["status"] not in VALID_STATUS: + fail( + f"repo {name!r} has invalid status {repo['status']!r}; " + f"expected one of {sorted(VALID_STATUS)}" + ) + if repo["visibility"] not in VALID_VISIBILITY: + fail( + f"repo {name!r} has invalid visibility {repo['visibility']!r}; " + f"expected one of {sorted(VALID_VISIBILITY)}" + ) + if not isinstance(repo["in_profile"], bool): + fail(f"repo {name!r} in_profile must be boolean") + for text_field in ("role", "summary"): + if not isinstance(repo[text_field], str) or not repo[text_field].strip(): + fail(f"repo {name!r} {text_field} must be a non-empty string") + url = repo["url"] + match = URL_RE.match(url) if isinstance(url, str) else None + if not match: + fail(f"repo {name!r} url must be https://github.com/WasmAgent/, got {url!r}") + if match.group("name") != name: + fail( + f"repo {name!r} url slug {match.group('name')!r} does not match repo name" + ) + + +def profile_table_repos() -> list[str]: + """Return repo slugs linked from the profile project table.""" + if not os.path.isfile(PROFILE_PATH): + fail(f"missing profile README: {PROFILE_PATH}") + with open(PROFILE_PATH, encoding="utf-8") as fh: + text = fh.read() + # Isolate the "## Projects" section up to the next H2 heading. + parts = text.split("## Projects", 1) + if len(parts) != 2: + fail("profile README has no '## Projects' section") + section = parts[1].split("\n## ", 1)[0] + slugs: list[str] = [] + for line in section.splitlines(): + stripped = line.strip() + if not stripped.startswith("|"): + continue + # Skip the header row and the alignment separator row. + if "Repository" in stripped or re.match(r"^\|\s*:?-{2,}", stripped): + continue + match = PROFILE_LINK_RE.search(stripped) + if match: + slugs.append(match.group(1)) + return slugs + + +def check_coherence(index_repos: list[dict]) -> None: + in_profile = sorted(r["name"] for r in index_repos if r["in_profile"]) + table = sorted(set(profile_table_repos())) + if not table: + fail("profile project table is empty; cannot verify coherence") + + missing_from_table = [n for n in in_profile if n not in table] + missing_from_index = [n for n in table if n not in in_profile] + if missing_from_table: + fail( + "repos marked in_profile but absent from profile/README.md table: " + + ", ".join(missing_from_table) + ) + if missing_from_index: + fail( + "repos in profile/README.md table but not in index with in_profile=true: " + + ", ".join(missing_from_index) + ) + + +def main() -> int: + try: + data = load_index() + validate_top_level(data) + repos = data.get("repos") + if not isinstance(repos, list) or not repos: + fail("repos must be a non-empty list") + seen: set[str] = set() + for repo in repos: + validate_repo(repo, data.get("categories", {}), seen) + check_coherence(repos) + except ValidationFailed as exc: + print(f"project-index validation FAILED: {exc}", file=sys.stderr) + return 1 + print( + f"project-index OK: {len(repos)} repos, " + f"{sum(1 for r in repos if r['in_profile'])} in profile, " + f"coherent with profile/README.md" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())