diff --git a/.github/workflows/mirror-sync.yml b/.github/workflows/mirror-sync.yml new file mode 100644 index 0000000..e33e561 --- /dev/null +++ b/.github/workflows/mirror-sync.yml @@ -0,0 +1,33 @@ +name: mirror-sync + +# 한국어 정본을 고치면서 docs/en 미러를 두고 가는 PR을 막는다. +# 번역의 정확성이 아니라 "같이 갱신했는가"만 본다. +# 한쪽만 바꾸는 것이 의도된 PR에는 'mirror-sync-exempt' 라벨을 붙인다. + +on: + pull_request: + paths: + - "CLAUDE.md" + - "AGENTS.md" + - "agents/**" + - "reference/**" + - "docs/en/**" + - "README.md" + - "README.ko.md" + - "scripts/check-mirror-sync.sh" + - ".github/workflows/mirror-sync.yml" + +permissions: + contents: read + +jobs: + mirror-sync: + if: ${{ !contains(github.event.pull_request.labels.*.name, 'mirror-sync-exempt') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 정본과 docs/en 미러가 함께 갱신되었는지 검사 + run: bash scripts/check-mirror-sync.sh "origin/${{ github.base_ref }}" diff --git a/CLAUDE.md b/CLAUDE.md index 189e06f..4b13713 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,13 @@ claude plugin validate . # 매니페스트·구조 검증 `.claude/agents/`나 `.claude/settings.json`으로 되돌리지 마라 — 플러그인 레이아웃(`agents/`, `hooks/hooks.json`)이 정본이다. +**한국어 정본을 고치면 `docs/en/` 미러도 같은 PR에서 고쳐라.** `CLAUDE.md`·`AGENTS.md`·`agents/`·`reference/`가 대상이고, CI(`mirror-sync`)가 한쪽만 바뀐 PR을 막는다. 한쪽만 바꾸는 것이 의도된 PR에는 `mirror-sync-exempt` 라벨을 붙인다. + +```bash +bash scripts/check-mirror-sync.sh # PR 모드 — origin/main과의 변경분 검사 +bash scripts/check-mirror-sync.sh --audit # 감사 모드 — 저장소 전체의 미러 최신성 +``` + ## 경로 규칙 (중요) 플러그인 사용자는 **자기 작업 폴더**에서 이걸 쓴다. 저장소 안이 아니다. 따라서: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb6afb0..68806db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,9 +37,12 @@ evaluation. Contributions of all sizes are welcome. 1. Open an issue describing the change (or comment on an existing one). 2. Fork, create a topic branch, make your change. -3. Keep Korean and English docs in sync where practical (the Korean files under - `.claude/`, `CLAUDE.md`, `AGENTS.md`, `reference/` are the executable source; - English lives under `docs/en/`). +3. Keep Korean and English docs in sync (the Korean files `CLAUDE.md`, + `AGENTS.md`, `agents/`, and `reference/` are the executable source; English + lives under `docs/en/`, mirroring the same path). Update both sides in the + same PR — the `mirror-sync` check enforces this, and a deliberately + one-sided PR needs the `mirror-sync-exempt` label. Run it yourself with + `bash scripts/check-mirror-sync.sh` (or `--audit` for the whole repo). 4. Open a pull request with a clear description and, for methodology changes, the supporting citation. diff --git a/README.ko.md b/README.ko.md index c5aaee0..c55ddb6 100644 --- a/README.ko.md +++ b/README.ko.md @@ -162,7 +162,7 @@ python3 scripts/open_runner.py --out docs/open-model-demo-output.md DevEval의 증거 게이트는 "근거 없으면 등급 없음"이다. 같은 관리자의 [ODA Intelligence 플러그인](https://github.com/amnotyoung/oda-intelligence-plugin)을 함께 설치하면 **대조 가능한 근거의 범위**가 넓어진다 — 공개 read-only MCP -게이트웨이 도구 29종: 국가 지표·타 공여기관 활동(IATI·세계은행 등, 적절성·일관성), +게이트웨이 도구 30종: 국가 지표·타 공여기관 활동(IATI·세계은행 등, 적절성·일관성), 같은 나라 한국 ODA 사업 지도(중복·연계 확인), KOICA 규정 조문 전문, 그리고 보고서가 인용한 조문의 실재를 검증하는 `verify_citation`. diff --git a/README.md b/README.md index d58bdb9..15100f2 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ Or reproduce it on free Google Colab: [`notebooks/open-model-demo.ipynb`](notebo DevEval's evidence gate says *no evidence → no grade*. The optional [ODA Intelligence plugin](https://github.com/amnotyoung/oda-intelligence-plugin) -(same maintainer) widens what counts as checkable evidence: 29 read-only tools +(same maintainer) widens what counts as checkable evidence: 30 read-only tools over one public MCP gateway — country indicators and other donors' activities (IATI, World Bank, …) for relevance/coherence, the Korean ODA project map for duplication checks, KOICA regulation full text, and `verify_citation`, which diff --git a/docs/en/CLAUDE.md b/docs/en/CLAUDE.md index ddf1c05..9843783 100644 --- a/docs/en/CLAUDE.md +++ b/docs/en/CLAUDE.md @@ -38,6 +38,13 @@ claude plugin validate . # validate the manifest & structure Do not fall back to `.claude/agents/` or `.claude/settings.json` — the plugin layout (`agents/`, `hooks/hooks.json`) is canonical. +**When you change a Korean canonical file, update its `docs/en/` mirror in the same PR.** This covers `CLAUDE.md`, `AGENTS.md`, `agents/`, and `reference/`; CI (`mirror-sync`) blocks a PR that changes only one side. Label a deliberately one-sided PR `mirror-sync-exempt`. + +```bash +bash scripts/check-mirror-sync.sh # PR mode — check the diff against origin/main +bash scripts/check-mirror-sync.sh --audit # audit mode — mirror freshness across the repo +``` + ## Path rules (important) Plugin users run this from **their own working folder** — not from inside the repo. Therefore: diff --git a/scripts/check-mirror-sync.sh b/scripts/check-mirror-sync.sh new file mode 100755 index 0000000..7f3452c --- /dev/null +++ b/scripts/check-mirror-sync.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# +# 정본(한국어 실행본)과 docs/en 미러가 함께 갱신되는지 검사한다. +# +# scripts/check-mirror-sync.sh [base-ref] PR 모드 — base와의 변경분만 본다 (기본 origin/main) +# scripts/check-mirror-sync.sh --audit 감사 모드 — 저장소 전체의 미러 최신성을 본다 +# +# 왜 필요한가: CONTRIBUTING의 규칙대로 한국어 파일이 실행 정본이고 영문은 +# docs/en/ 미러다. 정본만 고치고 미러를 두면 미러가 조용히 낡는다 — +# docs/en/CLAUDE.md가 실제로 한 달간 옛 판에 머물렀다. +# +# 번역의 정확성은 검사하지 않는다. "같이 갱신했는가"만 본다. + +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" || exit 1 + +MIRROR_PREFIX="docs/en/" +# 미러를 두는 정본 — 루트 파일과 디렉터리 +MIRRORED_FILES=("CLAUDE.md" "AGENTS.md") +MIRRORED_DIRS=("agents" "reference") +# 한 쌍이지만 한쪽만 고치는 일이 정상인 문서 — 실패가 아니라 경고 +SOFT_PAIR_A="README.md" +SOFT_PAIR_B="README.ko.md" + +# 한글 파일명이 \354... 로 이스케이프되면 경로 비교가 깨진다 +git() { command git -c core.quotepath=false "$@"; } + +is_mirrored() { # 경로가 미러를 둬야 하는 정본인가 + local path=$1 f d + for f in "${MIRRORED_FILES[@]}"; do + [ "$path" = "$f" ] && return 0 + done + for d in "${MIRRORED_DIRS[@]}"; do + case "$path" in "$d"/*.md) return 0 ;; esac + done + return 1 +} + +annotate() { # GitHub Actions 주석 (로컬에서는 평문) + local level=$1 message=$2 + if [ -n "${GITHUB_ACTIONS:-}" ]; then + printf '::%s::%s\n' "$level" "$message" + else + printf ' [%s] %s\n' "$level" "$message" + fi +} + +audit_mode() { + local failures=0 canonical mirror c_time m_time + echo "미러 감사 — 정본이 미러보다 나중에 바뀐 쌍을 찾는다" + echo + + while IFS= read -r canonical; do + is_mirrored "$canonical" || continue + mirror="${MIRROR_PREFIX}${canonical}" + + if [ ! -f "$mirror" ]; then + annotate error "미러 없음: $canonical → $mirror" + failures=$((failures + 1)) + continue + fi + + c_time=$(git log -1 --format=%ct -- "$canonical") + m_time=$(git log -1 --format=%ct -- "$mirror") + if [ -n "$c_time" ] && [ -n "$m_time" ] && [ "$c_time" -gt "$m_time" ]; then + annotate error "미러가 낡음: $mirror (정본 $canonical 이 더 최신)" + failures=$((failures + 1)) + fi + done < <(git ls-files) + + if [ "$failures" -eq 0 ]; then + echo "모든 미러가 정본과 같거나 더 최신이다." + return 0 + fi + echo + echo "낡은 미러 ${failures}건. 정본을 다시 읽고 docs/en 쪽을 갱신하라." + return 1 +} + +pr_mode() { + local base=$1 base_sha changed failures=0 canonical mirror + base_sha=$(git merge-base "$base" HEAD 2>/dev/null) + if [ -z "$base_sha" ]; then + echo "base ref를 해석할 수 없다: $base" >&2 + return 2 + fi + + changed=$(git diff --name-only "$base_sha" HEAD) + if [ -z "$changed" ]; then + echo "변경된 파일이 없다." + return 0 + fi + + echo "base $(git rev-parse --short "$base_sha") 와 비교 — 변경 $(printf '%s\n' "$changed" | wc -l | tr -d ' ')건" + echo + + while IFS= read -r canonical; do + [ -z "$canonical" ] && continue + is_mirrored "$canonical" || continue + mirror="${MIRROR_PREFIX}${canonical}" + + if printf '%s\n' "$changed" | grep -qxF "$mirror"; then + echo " OK $canonical → $mirror 같이 갱신됨" + else + annotate error "$canonical 이 바뀌었는데 $mirror 는 그대로다. 같은 PR에서 미러도 갱신하라." + failures=$((failures + 1)) + fi + done < <(printf '%s\n' "$changed") + + # README 쌍은 한쪽만 고치는 일이 정상이라 경고만 한다 + local a_changed=no b_changed=no + printf '%s\n' "$changed" | grep -qxF "$SOFT_PAIR_A" && a_changed=yes + printf '%s\n' "$changed" | grep -qxF "$SOFT_PAIR_B" && b_changed=yes + if [ "$a_changed" != "$b_changed" ]; then + annotate warning "$SOFT_PAIR_A 와 $SOFT_PAIR_B 중 한쪽만 바뀌었다. 의도한 것이면 그대로 두라." + fi + + echo + if [ "$failures" -eq 0 ]; then + echo "정본과 미러가 함께 갱신되었다." + return 0 + fi + echo "미러가 빠진 정본 ${failures}건." + echo "의도적으로 한쪽만 바꾸는 PR이면 'mirror-sync-exempt' 라벨을 붙이면 이 검사를 건너뛴다." + return 1 +} + +case "${1:---pr}" in + --audit) audit_mode ;; + --pr) pr_mode "origin/main" ;; + -h | --help) + sed -n '3,12p' "$0" + ;; + *) pr_mode "$1" ;; +esac