From d6c6f4f1c727714df0e339e5a2c49c9c2479be24 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 2 Jul 2026 06:45:17 +0800 Subject: [PATCH 1/2] fix: Provide machine-readable project index from wasmagent to sync org profile tables (#55) Closes #55 --- .github/workflows/project-index-ci.yml | 20 ++ README.md | 3 + docs/project-index.json | 139 ++++++++++++ docs/roadmap.md | 3 +- profile/README.md | 6 + .../validate_project_index.cpython-312.pyc | Bin 0 -> 11332 bytes scripts/validate_project_index.py | 205 ++++++++++++++++++ 7 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/project-index-ci.yml create mode 100644 docs/project-index.json create mode 100644 scripts/__pycache__/validate_project_index.cpython-312.pyc create mode 100755 scripts/validate_project_index.py 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/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/__pycache__/validate_project_index.cpython-312.pyc b/scripts/__pycache__/validate_project_index.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1565ae292893a77ac58b2f710e6b698427684a8 GIT binary patch literal 11332 zcmcIqeNY?cnctOG(&`H#z+f=Oi}45I(-^P`CQiY^Hkg2ei4zkive2$UvXIE`%77vg zl4hoGbq74{oXI(v&}(LjC%!3nb92Ydb>@<^bJLmoM?`W>_qdtNw0~Uxqp>?}-GA9kRBeR8Ke^xA%k`X&C*E{pm=Kk^zV>JG(I z!xT^RhA=&D7&g%OYz!NRjpWV@Gvsa>Hj%q|*bH|jY#C>VS(*}bavGWp~yoon|YE<73yZ%=#Ej+8Y%;&AcZr(OrFh=qA zAJD^vyn}a+8HS4tlwbpW+LIQhj7) z{64<$eP(zcU(Of7y`10A?}7V%zJf1?dj)@hFM<03p5ynzo#QL{Qn*(N2l=ua)f6Q< zq3rp)SDJC{1M z#Epa9o!%F_8^-x|8^>|=oN!ZsKGmfbVG!K)uzz$E=5y^@FyhO!x^|88PfQ4YQ36V` z%9#pI#=SU|%Zf1BjSAOy0%i?cqj6EYMNvQw+_pa3abJaN*D^wpl^j))aI?nzGIG2L z9Mvt-Mg@12L3MQ8bul{5Ng!VlIXrEPjt6Bq7#W2P$SgV8Fcih>{$Mx;>pmXkL6`+8 zAO=TZC15hTP&GD;!f0b7Rjth)7+LpJP==KTav0As4rE=51wg7`=bP}CKQgV!9PEnL zK?C=)1X~=9gr~`3UYwT4q7fU&OoaD~JQog*hlL@^QsH5gJXOAZAQ%#G zQj8#GiZkq&WFM?`P?!{W#Y7g*!zv6)tHNrctuSyJ1jVe1zhcXZzhcXZlVXu#Y+TIDG$Y~{|q0!eINsY z`n6&w0tA6TCc$M5qgHlGhfG8V>XhtKzB%pcCwSbRR%U7znB^$17xWS~(J zZ#D+P{unRRk3{9h$*6ck76n0SAinNs0;et*wnH;S;jdn1MwB&pRF*p9$yi5!#)gJ)2zg2y)alble@Qk%B2rRJ3FK$1B?I^MPsW*{Mb#(@p=2|v{<1Xj+@ zD}*!HammPmT-q3=0@Tll5q;;3gu}} z*?V?fW-VtQ4&1~Hr*A9hJUw?Oc}3G zlXMj&J2XBal-nKGAxhjkW60slBn;co*X!Hv(F%*g&I3ujavKf8CqS2aEQmI4=i>F-N#y7R7Y48g9T<~jZUhvg zs<)#fD6<~BVu(r#GvSxV6mw8QgF>=BH z0fK9HE}Wb{xo~>^^oKQR`{7O2k#rsTB)%L@I-ApM%a(yLwQV}x->X@kOgkI5DB4=N zdVI66T|ZZ%V_+(f`#WV zjHWJ!#iN3|k zxKiZe;q6^R?)kY@!cek)9upLAK(NW4q#Q}laxvyedZq=>+&Bp!Rglw{N#l0cz?(nX z)+VH%?99O6##=P5JWF_0u+c^A~3t8PogU9pH>i$-_6IEVbG zPIpv9;ff#GW-ICAxdK=Hi!auDy>;9uU|qa+J6#3>{a~1)`R)gM6|6xHZMb$$v1Lpw zFjnIo*=L3Mfgr5eWL*8}j4?ZbmZ8U?I0n04dbww?+v^(|xJYnOJccti z!g?vjQ2`1`78Q%PbEw-l*sYj|(;+fQ<3wvZC`Hjp^2>@1%$%4CoMIEydl<2qu(}~k z1HgjOC_V?jC2R|bnSsm#-Qa*d^^rXly+F}}q#oEUfKJ!GuPKA+#GG-%p0~*V)eD=B z{A6L>%B7WaNq5TwN9&deY9E^kFedlat{2zc52WqK2|ON4vrQWod(u(9ZrQ)={?c-g zz+ufwOY+z^)@wVHHQvwqlFpaX>>!YRYwF#pMbkImS*rcgf)G9T&XWA`)CW^5rn~PX zE1Hr`=aSCzX|@LdccE>*ZE+xFKQ!yzwC@2#K9ja{_boqi-gB<<>FQIYo7Po+_0^>F zbeer(%V0Eh)A05oTei-YEw!XsZp%y+6yMoZJ3`&si^Gc{lw;!h`*jygl9m>Zb)I-a6w?YiTG0 z`uT!tL>Tx@Kl&Mz+pRKLw~_R7Y|ox^p4Ko6PONSv0m=hV&lnR%9p!aA;|&QT;33r9 zrFiC}9J}P!J_b;RHzCH)FgcbXBjaf!mo^5`kYFTN&N$2o2DmcYP!?>Ho0OQBLuEm# zTn&>jgmeo{>w}^aOm#cU@p0gtqpHKSHb%wSt+xA3V zA$RVc@C6_nr(l0gx!e+D&v)77q6DSZ?G`uG;!PrbtLCW{EKHa{zKXWV*YiZa_Tb28 z%sKs=cj$z3sh-mQ_+b(iA^{hOZ9u?aSHn17l}nMa&_&_R7zB%WE~850CaxOd z98YMEh({(~L)xI1nZq;^jnoU{6EZ|5WD!+NHPK@gd~5=IbBq9HTLH6-^)I5&5hoD? zP@LSDUxL8ajYxDdl2uXhf*ge!jgYXOh^HuCL>dGmL}Ng|7*^v%G>hVUw^e0B4V;Lw z9^VH8&;nTcBu1hTpYTUK1`+)-5e=j#GzYZVrQ1165zj&Vgc^OzcplqB20J1TYSFVk08M>k{I90`jJ2j!0UmolOO)kEGY)qqKG&X_3 z8{96UpA|+DgoxM$)nxw_n=DMpJ|Y}s{-mXZNR3(?FV*>R>YAMJ8Ug|z{7EN3v7rBh zI#TyqK>M|By2??n_tD!G`-xR)b#(3J&ssh^@-sT=?tS36fC|2k25srcUo_8mz1b60W`0A z*UHyh&L*3CfI~0gkUnxEb>L*Oq77NJ6BZdaaH(UxxcWYr3yr{IF19$dcs^NvGU;qf zv(If5dvJL#&~vs;SLxCrm}^X1jl3 z&(9gaVDi%C#cX+T0D`mlr8b9{gqknrS@gvhc~`|WA#Yt$aBfV z9m$TDla9;3H5y=rjTBq;ry@K%{LTOG2+v;<-vQ4_kMMxtXD&^9K~#sqf0_oRsdwnm zETel9AqU3ps}%a!5H+lYQNj)D!AFoyI9 zlQwSfLNkw`$tO{hCrp1zJI|OC=Flz~I$rm7AqsGqcO=X=DXCc2b#16D+oSfxJ3;Se z#tB;g8*;f;6T)cYjw`F9iGHSUy_#S`l{<*wC+OCyr{BVgpic;wc84h0G*}SKF3~e3 z6XD8>w+#w%OpK_wjhHS$tUhuB)hOl|Foys!D~vqpAuy64X^8LDoLH@1jktpkdl&{G z9)Y4iZm+81E~*h&NradN^=n*?ccvps#?*=U(qhwnj4K$y%VfsO~q z5WG^wMxJ5jf+Y4M38pSYS;9n)Vh{yY&A2pveLejb(2X=pkU#EJ)9SnxUVbHQtp^}Bm97?SxZDeq^ON76o?{>sXDS6?bh;OM z=X)1tmi;M5^=$X1#WvUa&G_O=@4W+Xm~^(M*;9yho%3T0k@?8dD?pc5z2aWatDQZ! zQBpeFy^&w?F#q6s{=roKp|2^c$uq}nK+LcGcKdfb=GYBa$wSw{b=Se==CteRhNI{! zd&yGUy1nWX@rP62p8^l|v+DHG3sA9{f8b&Mk@fr|%TuZRV{<0Op1}tTG(QEe^L&5;T?99grU@+_9Er2!JODz}!hEXbDc-;zc zgifbH4J{^&w+*-K3;-FtHS9tME**v-bvhH!G2;H5o{T)RTWBxqAjV;Zv~j{{Okzb& zYtu7Y>jwY!)@ZZ`AS53!%orzgo*;0(ZQ;>#hY$+=2+zd@h~D3U<$fJ>2F6u+3LzAx zWrb%NpZ}V^Lj!iX8EV?_cg9KDV)+et^9qX8$+!`&sJClHO(iTIE{bzcwz>OGiGku9x)w zff$GH*-Gf|z+e2NhFNgWNyS2pi#QOc!1TvlB?n2y;PvTjvO3;LjxE^7BEo8p1wRs# zIsXXB9uu(5bdXJGtJz-28yM=i_<5BLcp=ZA8P zt{wX!f+D8=7XGAvf+cL^-`%BgfoZ_3>Z;Uh=E_nOlsHK~%iRAGI}(V&@TN$1%# zd+w{ljXy8#TBcW<*1G@U^xsq_OS_UxF_asJYe>b4e>MJPLuXFSFD#{Ko;n6L@(LD$ z^TFianp9rxZ@>_(BZla2z!WYc6qlc-_$@J(!IUfe!`CjV>#wd)Y` zxsAcH$lB#$KCfV)3_zWJ!7*2(RX--c15xaZrq0?XkX6G|LauBEjYP5a2#!_{fTBlh z0HSo5Nzl8*1q=xqpa>mca^*r%Hu|Fh82}f+)9YZHKx|yM=`bp`Tb7YGW@G7K)n&#H zC$|!^AX$4)A1g%M^slLSi>yZ$a$`?CE9>7i@m7Fk7>_nO4YnXoV%ruwutPLnkL*A+ zz8a!&*`64PdqI{nCq9B3x8u5R((E+MFhYccd7GIwtM{CYprc>J&||zJbENgGUDm_K zPjiR^s^tuDq1r-pn~$- z4yZhxfzrI4P>PnA3Z-Hg9&=$-jN$}K#{I#_E~xqMNR62i6*cpk7hhU#TE6;&7jC_| zp4Y4)W)b}zOA#HfLnO+870f{WP)b%&=_w2&2A;=7@{J^NE@IYEjpBo~1dCdQzbCLj zpPWDl$+?4#1)&>^@K3{66_U)u2M6L+Ab~fraX>rJJrvnDaS)$g!lDHVlKjs>DF!Dk z&O!~MlJq4Mv(%QsVRD1x`R&9UxaexUdn3Q-4ttwjDoN#6&Y93FJ^V>g+I8fffFXT% zQ`&KSv$Xu~9x#H6N+H;7?V{(rn+|YZmzf{3-)C3KSH%?9zUF=4=mcLFs(;0nESb`5 z1^B}z+lHIvJSemiqo{0{s?T_Wv) z5+sSHf5Q~e%x|kHy6Tsd{XZ!y`EUC#s`M*I-om;0a|;9W14~^WpZnn4#{(Y>taSb8 z+`V%@8n`#Gc0N_tohm<@a-5sBZa7^Fugt%)aCQFbl4tpHvc5Cr?3%S@9EXL(d}66* zd3v=r)0|_-G*7`*F{u*>#Sp=sC+4uEc7I){0(d2T){%wd|9%nCuQxO zHE-C8<_<2@%-1CM)TV58vzCp5^2LL9YHrsg_n%J{^vt>*n`!#Q;`Cz*ZjV@+<{q&& zI{#5wA^jr#sO~6jdo)BB)9y#d-SAjlNISRcD8?}t|I%3c3tQ3cq3>Q!*~*hlImBL= IGO}#{15@Ic&Hw-a literal 0 HcmV?d00001 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()) From 36d35b86826ce1052b01a3e4e1c74fcfd2ca5c90 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 2 Jul 2026 06:46:19 +0800 Subject: [PATCH 2/2] fix: address review findings (#55) --- .gitignore | 4 ++++ .../validate_project_index.cpython-312.pyc | Bin 11332 -> 0 bytes 2 files changed, 4 insertions(+) create mode 100644 .gitignore delete mode 100644 scripts/__pycache__/validate_project_index.cpython-312.pyc 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/scripts/__pycache__/validate_project_index.cpython-312.pyc b/scripts/__pycache__/validate_project_index.cpython-312.pyc deleted file mode 100644 index e1565ae292893a77ac58b2f710e6b698427684a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11332 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 IGO}#{15@Ic&Hw-a