[codex] repo_state global refresh + mandatory header gate #10
@@ -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: |
|
||||
python tools/repo_state/collect.py --repo-name Kill_LIFE
|
||||
|
||||
- name: Upload repo-state artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: repo-state
|
||||
path: |
|
||||
docs/REPO_STATE.md
|
||||
docs/repo_state.json
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Repo State Header Gate
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
header-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Kill_LIFE
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Clone sibling repos
|
||||
run: |
|
||||
cd ..
|
||||
git clone --depth=1 https://github.com/electron-rare/RTC_BL_PHONE.git RTC_BL_PHONE
|
||||
git clone --depth=1 https://github.com/electron-rare/le-mystere-professeur-zacus.git le-mystere-professeur-zacus
|
||||
|
||||
- name: Build global header
|
||||
run: |
|
||||
tools/repo_state/repo_refresh.sh --header-only
|
||||
|
||||
- name: Lint header contract
|
||||
run: |
|
||||
python tools/repo_state/lint_header_contract.py
|
||||
|
||||
- name: Upload global repo-state artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: global-repo-state
|
||||
path: |
|
||||
artifacts/repo_state/global_summary.md
|
||||
artifacts/repo_state/global_index.json
|
||||
artifacts/repo_state/header.latest.md
|
||||
@@ -0,0 +1,11 @@
|
||||
<!-- REPO_STATE:v1 -->
|
||||
Repo: Kill_LIFE
|
||||
Branch: codex/repo-state-global-index
|
||||
HEAD: 432c76a57e5cd9c1603d68f56cc6d012168be1b3
|
||||
HeadDate: 2026-02-21T03:03:06+01:00
|
||||
HeadSubject: Merge pull request #9 from electron-rare/codex/autonomy-plan-todo-hw-local
|
||||
RepoURL: https://github.com/electron-rare/Kill_LIFE.git
|
||||
ProjectKind: agentic_orchestrator
|
||||
PivotChanges: [{"path": "(none)", "tags": ["general_change"]}]
|
||||
ImpactGates: general_change
|
||||
GeneratedAtUTC: 2026-02-21T02:22:47Z
|
||||
@@ -0,0 +1,64 @@
|
||||
# REPO_STATE Header Contract
|
||||
|
||||
Version: `v1`
|
||||
|
||||
## Source of truth
|
||||
|
||||
The global response header MUST be derived only from each repository file:
|
||||
|
||||
- `docs/REPO_STATE.md`
|
||||
|
||||
No free-form manual edits are allowed in generated outputs.
|
||||
|
||||
## Required local files (Kill_LIFE)
|
||||
|
||||
- `tools/repo_state/collect.py`
|
||||
- `tools/repo_state/repo_refresh.sh`
|
||||
- `tools/repo_state/lint_header_contract.py`
|
||||
- `docs/REPO_STATE.md`
|
||||
- `docs/repo_state.json`
|
||||
|
||||
## Header format (mandatory)
|
||||
|
||||
```md
|
||||
[REPO-STATE UTC: <timestamp>]
|
||||
Kill_LIFE | HEAD <sha> | pivots: <...> | gates: <...>
|
||||
RTC_BL_PHONE | HEAD <sha> | pivots: <...> | gates: <...>
|
||||
le-mystere-professeur-zacus | HEAD <sha> | pivots: <...> | gates: <...>
|
||||
[/REPO-STATE]
|
||||
```
|
||||
|
||||
## Contract checks
|
||||
|
||||
`repo_state_header_gate` MUST fail if any of the following is true:
|
||||
|
||||
- missing `<!-- REPO_STATE:v1 -->` marker in `docs/REPO_STATE.md`
|
||||
- missing required keys in `docs/REPO_STATE.md` or `docs/repo_state.json`
|
||||
- malformed header markers in `artifacts/repo_state/header.latest.md`
|
||||
- missing one of the 3 repository lines in the generated header
|
||||
|
||||
## Commands
|
||||
|
||||
Generate local state:
|
||||
|
||||
```bash
|
||||
python tools/repo_state/collect.py --repo-name Kill_LIFE
|
||||
```
|
||||
|
||||
Generate global artifacts from sibling repos:
|
||||
|
||||
```bash
|
||||
tools/repo_state/repo_refresh.sh
|
||||
```
|
||||
|
||||
Header only:
|
||||
|
||||
```bash
|
||||
tools/repo_state/repo_refresh.sh --header-only
|
||||
```
|
||||
|
||||
JSON only:
|
||||
|
||||
```bash
|
||||
tools/repo_state/repo_refresh.sh --json-only
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schema_version": "repo_state.v1",
|
||||
"generated_at_utc": "2026-02-21T02:22:47Z",
|
||||
"repo": "Kill_LIFE",
|
||||
"repo_url": "https://github.com/electron-rare/Kill_LIFE.git",
|
||||
"branch": "codex/repo-state-global-index",
|
||||
"head": "432c76a57e5cd9c1603d68f56cc6d012168be1b3",
|
||||
"head_date": "2026-02-21T03:03:06+01:00",
|
||||
"head_subject": "Merge pull request #9 from electron-rare/codex/autonomy-plan-todo-hw-local",
|
||||
"project_kind": "agentic_orchestrator",
|
||||
"pivot_changes": [
|
||||
{
|
||||
"path": "(none)",
|
||||
"tags": [
|
||||
"general_change"
|
||||
]
|
||||
}
|
||||
],
|
||||
"impact_gates": [
|
||||
"general_change"
|
||||
]
|
||||
}
|
||||
Executable
+166
@@ -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(
|
||||
[
|
||||
"<!-- REPO_STATE:v1 -->",
|
||||
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())
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lint repo-state contract for local/global header generation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REQUIRED_MD_KEYS = [
|
||||
"Repo",
|
||||
"Branch",
|
||||
"HEAD",
|
||||
"HeadDate",
|
||||
"ProjectKind",
|
||||
"PivotChanges",
|
||||
"ImpactGates",
|
||||
]
|
||||
REQUIRED_JSON_KEYS = [
|
||||
"schema_version",
|
||||
"generated_at_utc",
|
||||
"repo",
|
||||
"repo_url",
|
||||
"branch",
|
||||
"head",
|
||||
"head_date",
|
||||
"head_subject",
|
||||
"project_kind",
|
||||
"pivot_changes",
|
||||
"impact_gates",
|
||||
]
|
||||
REQUIRED_REPOS = ["Kill_LIFE", "RTC_BL_PHONE", "le-mystere-professeur-zacus"]
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
print(f"[fail] {msg}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def parse_kv_lines(path: Path) -> dict:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
if not lines or lines[0].strip() != "<!-- REPO_STATE:v1 -->":
|
||||
fail(f"missing REPO_STATE marker in {path}")
|
||||
|
||||
data: dict[str, str] = {}
|
||||
for line in lines:
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, value = line.split(":", 1)
|
||||
data[key.strip()] = value.strip()
|
||||
return data
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repo-root", default=".")
|
||||
parser.add_argument("--header-file", default="artifacts/repo_state/header.latest.md")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path(args.repo_root).resolve()
|
||||
md_file = repo_root / "docs/REPO_STATE.md"
|
||||
json_file = repo_root / "docs/repo_state.json"
|
||||
refresh_script = repo_root / "tools/repo_state/repo_refresh.sh"
|
||||
collect_script = repo_root / "tools/repo_state/collect.py"
|
||||
header_file = (repo_root / args.header_file).resolve()
|
||||
|
||||
for path in [md_file, json_file, refresh_script, collect_script, header_file]:
|
||||
if not path.exists():
|
||||
fail(f"missing required file: {path}")
|
||||
|
||||
kv = parse_kv_lines(md_file)
|
||||
missing_md = [k for k in REQUIRED_MD_KEYS if k not in kv]
|
||||
if missing_md:
|
||||
fail(f"missing keys in {md_file}: {', '.join(missing_md)}")
|
||||
|
||||
try:
|
||||
json.loads(kv["PivotChanges"])
|
||||
except json.JSONDecodeError as exc:
|
||||
fail(f"invalid PivotChanges JSON in {md_file}: {exc}")
|
||||
|
||||
state = json.loads(json_file.read_text(encoding="utf-8"))
|
||||
missing_json = [k for k in REQUIRED_JSON_KEYS if k not in state]
|
||||
if missing_json:
|
||||
fail(f"missing keys in {json_file}: {', '.join(missing_json)}")
|
||||
|
||||
header_text = header_file.read_text(encoding="utf-8")
|
||||
if "[REPO-STATE UTC:" not in header_text or "[/REPO-STATE]" not in header_text:
|
||||
fail(f"invalid header markers in {header_file}")
|
||||
|
||||
for repo in REQUIRED_REPOS:
|
||||
pattern = re.compile(rf"^{re.escape(repo)}\s+\| HEAD [0-9a-f]{{7,40}} \| pivots: .+ \| gates: .+$", re.MULTILINE)
|
||||
if not pattern.search(header_text):
|
||||
fail(f"missing repo line in header for {repo}")
|
||||
|
||||
print("[ok] repo-state header contract is valid")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [--header-only] [--json-only] [--no-refresh]
|
||||
|
||||
Options:
|
||||
--header-only print only artifacts/repo_state/header.latest.md
|
||||
--json-only print only artifacts/repo_state/global_index.json
|
||||
--no-refresh skip running collect.py in the 3 repos
|
||||
USAGE
|
||||
}
|
||||
|
||||
HEADER_ONLY=0
|
||||
JSON_ONLY=0
|
||||
NO_REFRESH=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--header-only)
|
||||
HEADER_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--json-only)
|
||||
JSON_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--no-refresh)
|
||||
NO_REFRESH=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[fail] unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$HEADER_ONLY" == "1" && "$JSON_ONLY" == "1" ]]; then
|
||||
echo "[fail] --header-only and --json-only are mutually exclusive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
KILL_LIFE_REPO="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
PARENT_DIR="$(cd "$KILL_LIFE_REPO/.." && pwd)"
|
||||
|
||||
REPO_ROOT_KILL_LIFE="${REPO_ROOT_KILL_LIFE:-$PARENT_DIR/Kill_LIFE}"
|
||||
REPO_ROOT_RTC_BL_PHONE="${REPO_ROOT_RTC_BL_PHONE:-$PARENT_DIR/RTC_BL_PHONE}"
|
||||
REPO_ROOT_ZACUS="${REPO_ROOT_ZACUS:-$PARENT_DIR/le-mystere-professeur-zacus}"
|
||||
|
||||
ART_DIR="${REPO_STATE_ART_DIR:-$KILL_LIFE_REPO/artifacts/repo_state}"
|
||||
SUMMARY_FILE="$ART_DIR/global_summary.md"
|
||||
INDEX_JSON_FILE="$ART_DIR/global_index.json"
|
||||
HEADER_FILE="$ART_DIR/header.latest.md"
|
||||
|
||||
mkdir -p "$ART_DIR"
|
||||
|
||||
refresh_one_repo() {
|
||||
local repo_name="$1"
|
||||
local repo_root="$2"
|
||||
|
||||
if [[ ! -d "$repo_root/.git" ]]; then
|
||||
echo "[fail] missing repo for $repo_name at $repo_root" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local collector="$repo_root/tools/repo_state/collect.py"
|
||||
if [[ -f "$collector" ]]; then
|
||||
python3 "$collector" --repo-name "$repo_name" --repo-root "$repo_root"
|
||||
else
|
||||
python3 "$KILL_LIFE_REPO/tools/repo_state/collect.py" --repo-root "$repo_root" --repo-name "$repo_name"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$NO_REFRESH" != "1" ]]; then
|
||||
refresh_one_repo "Kill_LIFE" "$REPO_ROOT_KILL_LIFE"
|
||||
refresh_one_repo "RTC_BL_PHONE" "$REPO_ROOT_RTC_BL_PHONE"
|
||||
refresh_one_repo "le-mystere-professeur-zacus" "$REPO_ROOT_ZACUS"
|
||||
fi
|
||||
|
||||
KILL_MD="$REPO_ROOT_KILL_LIFE/docs/REPO_STATE.md"
|
||||
RTC_MD="$REPO_ROOT_RTC_BL_PHONE/docs/REPO_STATE.md"
|
||||
ZACUS_MD="$REPO_ROOT_ZACUS/docs/REPO_STATE.md"
|
||||
|
||||
python3 - "$SUMMARY_FILE" "$INDEX_JSON_FILE" "$HEADER_FILE" "$KILL_MD" "$RTC_MD" "$ZACUS_MD" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
summary_file = Path(sys.argv[1])
|
||||
index_json_file = Path(sys.argv[2])
|
||||
header_file = Path(sys.argv[3])
|
||||
md_files = [Path(p) for p in sys.argv[4:]]
|
||||
|
||||
required_keys = [
|
||||
"Repo",
|
||||
"Branch",
|
||||
"HEAD",
|
||||
"HeadDate",
|
||||
"ProjectKind",
|
||||
"PivotChanges",
|
||||
"ImpactGates",
|
||||
]
|
||||
|
||||
|
||||
def parse_md(path: Path) -> dict:
|
||||
if not path.exists():
|
||||
raise SystemExit(f"[fail] missing file: {path}")
|
||||
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
if not lines or lines[0].strip() != "<!-- REPO_STATE:v1 -->":
|
||||
raise SystemExit(f"[fail] invalid header marker in {path}")
|
||||
|
||||
data = {}
|
||||
for line in lines:
|
||||
if ":" not in line:
|
||||
continue
|
||||
k, v = line.split(":", 1)
|
||||
data[k.strip()] = v.strip()
|
||||
|
||||
missing = [k for k in required_keys if k not in data]
|
||||
if missing:
|
||||
raise SystemExit(f"[fail] missing keys in {path}: {', '.join(missing)}")
|
||||
|
||||
try:
|
||||
pivots = json.loads(data["PivotChanges"])
|
||||
except json.JSONDecodeError as exc:
|
||||
raise SystemExit(f"[fail] invalid PivotChanges JSON in {path}: {exc}")
|
||||
|
||||
gates = [g.strip() for g in data["ImpactGates"].split(",") if g.strip()]
|
||||
|
||||
return {
|
||||
"repo": data["Repo"],
|
||||
"branch": data["Branch"],
|
||||
"head": data["HEAD"],
|
||||
"head_date": data["HeadDate"],
|
||||
"project_kind": data["ProjectKind"],
|
||||
"pivot_changes": pivots,
|
||||
"impact_gates": gates,
|
||||
"source": str(path),
|
||||
}
|
||||
|
||||
|
||||
entries = [parse_md(p) for p in md_files]
|
||||
now_utc = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
def pivot_text(entry: dict) -> str:
|
||||
pivots = entry["pivot_changes"]
|
||||
if not pivots:
|
||||
return "(none)"
|
||||
paths = []
|
||||
for item in pivots[:2]:
|
||||
p = str(item.get("path", "")).strip() or "(none)"
|
||||
paths.append(p)
|
||||
if len(pivots) > 2:
|
||||
paths.append("...")
|
||||
return "; ".join(paths)
|
||||
|
||||
header_lines = [f"[REPO-STATE UTC: {now_utc}]"]
|
||||
for entry in entries:
|
||||
head_short = entry["head"][:8]
|
||||
pivots = pivot_text(entry)
|
||||
gates = ", ".join(entry["impact_gates"]) if entry["impact_gates"] else "general_change"
|
||||
header_lines.append(f"{entry['repo']:<28} | HEAD {head_short} | pivots: {pivots} | gates: {gates}")
|
||||
header_lines.append("[/REPO-STATE]")
|
||||
header_text = "\n".join(header_lines) + "\n"
|
||||
header_file.write_text(header_text, encoding="utf-8")
|
||||
|
||||
summary_lines = ["# Global Repo State", "", f"GeneratedAtUTC: `{now_utc}`", ""]
|
||||
for entry in entries:
|
||||
summary_lines.append(f"## {entry['repo']}")
|
||||
summary_lines.append(f"- Branch: `{entry['branch']}`")
|
||||
summary_lines.append(f"- HEAD: `{entry['head']}`")
|
||||
summary_lines.append(f"- HeadDate: `{entry['head_date']}`")
|
||||
summary_lines.append(f"- ProjectKind: `{entry['project_kind']}`")
|
||||
summary_lines.append(f"- ImpactGates: `{', '.join(entry['impact_gates'])}`")
|
||||
summary_lines.append(f"- Pivots: `{pivot_text(entry)}`")
|
||||
summary_lines.append("")
|
||||
summary_file.write_text("\n".join(summary_lines), encoding="utf-8")
|
||||
|
||||
index_data = {
|
||||
"schema_version": "global_repo_index.v1",
|
||||
"generated_at_utc": now_utc,
|
||||
"repos": entries,
|
||||
}
|
||||
index_json_file.write_text(json.dumps(index_data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
PY
|
||||
|
||||
if [[ "$HEADER_ONLY" == "1" ]]; then
|
||||
cat "$HEADER_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$JSON_ONLY" == "1" ]]; then
|
||||
cat "$INDEX_JSON_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[ok] wrote $SUMMARY_FILE"
|
||||
echo "[ok] wrote $INDEX_JSON_FILE"
|
||||
echo "[ok] wrote $HEADER_FILE"
|
||||
cat "$HEADER_FILE"
|
||||
Reference in New Issue
Block a user