From d77ab327070e35b994ef82539da0eaf37e022a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20SAILLANT?= <108685187+electron-rare@users.noreply.github.com> Date: Sat, 21 Feb 2026 03:23:39 +0100 Subject: [PATCH] [codex] add repo_state generator and workflow --- .github/workflows/repo_state.yml | 30 ++++++ docs/REPO_STATE.md | 11 ++ docs/repo_state.json | 22 ++++ tools/repo_state/collect.py | 166 +++++++++++++++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 .github/workflows/repo_state.yml create mode 100644 docs/REPO_STATE.md create mode 100644 docs/repo_state.json create mode 100755 tools/repo_state/collect.py diff --git a/.github/workflows/repo_state.yml b/.github/workflows/repo_state.yml new file mode 100644 index 0000000..6fefadd --- /dev/null +++ b/.github/workflows/repo_state.yml @@ -0,0 +1,30 @@ +name: Repo State + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + repo-state: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Generate repo state + run: | + python3 tools/repo_state/collect.py --repo-name RTC_BL_PHONE + + - name: Upload repo-state artifact + uses: actions/upload-artifact@v4 + with: + name: repo-state + path: | + docs/REPO_STATE.md + docs/repo_state.json diff --git a/docs/REPO_STATE.md b/docs/REPO_STATE.md new file mode 100644 index 0000000..dba61f2 --- /dev/null +++ b/docs/REPO_STATE.md @@ -0,0 +1,11 @@ + +Repo: RTC_BL_PHONE +Branch: codex/repo-state-rtc +HEAD: b97794c967f39a816b9d423544f96178b985f1c7 +HeadDate: 2026-02-21T03:09:10+01:00 +HeadSubject: Merge pull request #20 from electron-rare/codex/rtc-zeroclaw-orchestration-issue-18 +RepoURL: https://github.com/electron-rare/RTC_BL_PHONE.git +ProjectKind: firmware_embedded +PivotChanges: [{"path": "(none)", "tags": ["general_change"]}] +ImpactGates: general_change +GeneratedAtUTC: 2026-02-21T02:23:01Z diff --git a/docs/repo_state.json b/docs/repo_state.json new file mode 100644 index 0000000..80fd3dc --- /dev/null +++ b/docs/repo_state.json @@ -0,0 +1,22 @@ +{ + "schema_version": "repo_state.v1", + "generated_at_utc": "2026-02-21T02:23:01Z", + "repo": "RTC_BL_PHONE", + "repo_url": "https://github.com/electron-rare/RTC_BL_PHONE.git", + "branch": "codex/repo-state-rtc", + "head": "b97794c967f39a816b9d423544f96178b985f1c7", + "head_date": "2026-02-21T03:09:10+01:00", + "head_subject": "Merge pull request #20 from electron-rare/codex/rtc-zeroclaw-orchestration-issue-18", + "project_kind": "firmware_embedded", + "pivot_changes": [ + { + "path": "(none)", + "tags": [ + "general_change" + ] + } + ], + "impact_gates": [ + "general_change" + ] +} diff --git a/tools/repo_state/collect.py b/tools/repo_state/collect.py new file mode 100755 index 0000000..38e9c3f --- /dev/null +++ b/tools/repo_state/collect.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Generate repo state files (Markdown + JSON) for one repository.""" +from __future__ import annotations + +import argparse +import json +import subprocess +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, List, Set + + +REQUIRED_SCHEMA_VERSION = "repo_state.v1" + + +def run_git(repo_root: Path, *args: str) -> str: + result = subprocess.run( + ["git", *args], + cwd=repo_root, + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + + +def detect_project_kind(repo_root: Path) -> str: + if (repo_root / "tools" / "ai").exists() and (repo_root / "zeroclaw").exists(): + return "agentic_orchestrator" + if (repo_root / "platformio.ini").exists(): + return "firmware_embedded" + if (repo_root / "hardware" / "firmware" / "platformio.ini").exists(): + return "hardware_firmware_hybrid" + if (repo_root / "kit-maitre-du-jeu").exists(): + return "narrative_hardware_hybrid" + return "general" + + +def gates_for_path(path: str) -> Set[str]: + p = path.strip().lstrip("./") + gates: Set[str] = set() + + if p.startswith("tools/ai/") or p.startswith("tools/repo_state/") or p.startswith("zeroclaw/"): + gates.add("agentic_orchestration") + if p.startswith(".github/workflows/"): + gates.add("ci_integrity") + if ( + p == "platformio.ini" + or p.startswith("firmware/") + or p.startswith("src/") + or p.startswith("hardware/firmware/") + ): + gates.add("firmware_build_test") + if p.startswith("hardware/"): + gates.add("hardware_validation") + if p == "README.md" or p.startswith("docs/") or p.startswith("specs/"): + gates.add("docs_specs_sync") + if p.startswith("compliance/") or p.startswith("security/") or p.startswith("standards/"): + gates.add("compliance_security") + + if not gates: + gates.add("general_change") + + return gates + + +def get_changed_paths(repo_root: Path, limit: int = 5) -> List[str]: + raw = run_git(repo_root, "show", "--name-only", "--pretty=format:", "HEAD") + paths = [line.strip() for line in raw.splitlines() if line.strip()] + return paths[:limit] + + +def build_state(repo_root: Path, repo_name_override: str | None) -> Dict[str, object]: + repo_name = repo_name_override or repo_root.name + + head = run_git(repo_root, "rev-parse", "HEAD") + branch = run_git(repo_root, "rev-parse", "--abbrev-ref", "HEAD") + head_date = run_git(repo_root, "show", "-s", "--format=%cI", "HEAD") + head_subject = run_git(repo_root, "show", "-s", "--format=%s", "HEAD") + + try: + repo_url = run_git(repo_root, "config", "--get", "remote.origin.url") + except subprocess.CalledProcessError: + repo_url = "" + + changed_paths = get_changed_paths(repo_root) + pivot_changes: List[Dict[str, object]] = [] + all_gates: Set[str] = set() + + for path in changed_paths: + gates = sorted(gates_for_path(path)) + pivot_changes.append({"path": path, "tags": gates}) + all_gates.update(gates) + + if not pivot_changes: + pivot_changes = [{"path": "(none)", "tags": ["general_change"]}] + all_gates.add("general_change") + + generated_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + return { + "schema_version": REQUIRED_SCHEMA_VERSION, + "generated_at_utc": generated_at, + "repo": repo_name, + "repo_url": repo_url, + "branch": branch, + "head": head, + "head_date": head_date, + "head_subject": head_subject, + "project_kind": detect_project_kind(repo_root), + "pivot_changes": pivot_changes, + "impact_gates": sorted(all_gates), + } + + +def write_outputs(state: Dict[str, object], out_md: Path, out_json: Path) -> None: + out_md.parent.mkdir(parents=True, exist_ok=True) + out_json.parent.mkdir(parents=True, exist_ok=True) + + pivot_changes_inline = json.dumps(state["pivot_changes"], ensure_ascii=False) + impact_gates_inline = ", ".join(state["impact_gates"]) + + md_content = "\n".join( + [ + "", + f"Repo: {state['repo']}", + f"Branch: {state['branch']}", + f"HEAD: {state['head']}", + f"HeadDate: {state['head_date']}", + f"HeadSubject: {state['head_subject']}", + f"RepoURL: {state['repo_url']}", + f"ProjectKind: {state['project_kind']}", + f"PivotChanges: {pivot_changes_inline}", + f"ImpactGates: {impact_gates_inline}", + f"GeneratedAtUTC: {state['generated_at_utc']}", + "", + ] + ) + out_md.write_text(md_content, encoding="utf-8") + out_json.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Generate docs/REPO_STATE.md and docs/repo_state.json") + parser.add_argument("--repo-name", default=None, help="Override repo name") + parser.add_argument("--repo-root", default=".", help="Path to the git repository root (default: .)") + parser.add_argument("--out-md", default="docs/REPO_STATE.md", help="Output markdown path") + parser.add_argument("--out-json", default="docs/repo_state.json", help="Output json path") + args = parser.parse_args() + + repo_root = Path(args.repo_root).resolve() + if not (repo_root / ".git").exists(): + raise SystemExit(f"[fail] not a git repository: {repo_root}") + + state = build_state(repo_root, args.repo_name) + out_md = (repo_root / args.out_md).resolve() + out_json = (repo_root / args.out_json).resolve() + write_outputs(state, out_md, out_json) + + print(f"[ok] generated {out_md}") + print(f"[ok] generated {out_json}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())