engine/wow: add ## Title section (khai-tests v0.1.19 contract) #215
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: culture-review | |
| # NLP review of culture content. Runs the khai_tests.review checks | |
| # (gpt-4o-mini via GitHub Models, no external key) on changed culture | |
| # files in PRs into culture/release, and posts a PASS/FLAG comment. | |
| # | |
| # Checks (universal khai-tier rules; see KAIHACKS khai_tests.review): | |
| # - prose naturalness per-changed-file native-speaker writing | |
| # - persona distinctness a culture's personas aren't the same stereotype | |
| # - vocab overlap a piece/place/process doesn't restate its | |
| # linked position's Has/Orders (DRY R1-R2) | |
| # - Shadow != position persona's Shadow isn't the linked position's | |
| # Loses restated (#290 rule 2) | |
| # - persona typicality persona is typical of the culture (#290 rule 4) | |
| # - language-faithful when frontmatter language: maps to an | |
| # nlp_languages entry, verify the prose IS | |
| # in that language (lingua doesn't cover it, | |
| # so the LLM is the gate -- Nigeria mother- | |
| # tongue arc Stage 2c) | |
| # | |
| # Supersedes prose-review.yml -- the prose-naturalness check is now one | |
| # of five, run through the shared khai_tests.review wheel. | |
| # | |
| # Advisory: posts a PR comment, does not block merge. Promotable to a | |
| # required check once it's proven on real PRs (e.g. ChBrain/Cultures#290). | |
| on: | |
| pull_request: | |
| branches: [culture/release] | |
| jobs: | |
| culture-review: | |
| name: Culture content review (NLP) | |
| runs-on: ubuntu-latest | |
| # Same-repo only -- the GitHub Models call and the KAIHACKS wheel | |
| # both need org-scoped tokens; forked PRs cannot run this. | |
| if: github.event.pull_request.head.repo.full_name == github.repository | |
| permissions: | |
| models: read | |
| pull-requests: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Find changed culture files | |
| id: changed | |
| run: | | |
| FILES=$(git diff --name-only --diff-filter=ACMRT "origin/${{ github.base_ref }}...HEAD" \ | |
| | grep -E '^regions/.*/(culture_.*\.md|persona_.*\.md|.*_persona_.*\.md)$' || true) | |
| if [ -z "$FILES" ]; then | |
| echo "any=false" >> "$GITHUB_OUTPUT" | |
| else | |
| { | |
| echo "files<<EOF" | |
| echo "$FILES" | |
| echo "EOF" | |
| echo "any=true" | |
| } >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Run culture review | |
| if: steps.changed.outputs.any == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CHANGED_FILES: ${{ steps.changed.outputs.files }} | |
| run: | | |
| python3 <<'PYEOF' > /tmp/review.md | |
| # Dispatcher: groups changed files by country, calls the five | |
| # khai_tests.review checks with the right inputs, concatenates | |
| # markdown for one PR comment. Inline (governance-scoped via | |
| # this workflow file) -- scripts/ orchestration scripts are | |
| # not yet in GOVERNANCE_GLOB_PATTERNS. | |
| import os | |
| import subprocess | |
| import sys | |
| from collections import defaultdict | |
| from pathlib import Path | |
| # tests/validate_language.py owns the per-file frontmatter | |
| # -> route resolution (Stage 2c). Reusing it here keeps the | |
| # NLP routing single-sourced -- the workflow can't drift from | |
| # the dispatcher. | |
| sys.path.insert(0, "tests") | |
| try: | |
| from validate_language import ( | |
| ROUTE_NLP, dispatch_route, load_policy, POLICY_PATH, | |
| ) | |
| _POLICY = load_policy(POLICY_PATH) | |
| except Exception: | |
| ROUTE_NLP = "nlp" | |
| dispatch_route = None | |
| _POLICY = None | |
| changed = [f for f in os.environ.get("CHANGED_FILES", "").splitlines() if f] | |
| by_dir = defaultdict(list) | |
| for f in changed: | |
| by_dir[Path(f).parent].append(Path(f)) | |
| def is_persona(name: str) -> bool: | |
| return ( | |
| "_persona_" in name | |
| or "_male_" in name | |
| or "_female_" in name | |
| or name.startswith("persona_") | |
| ) | |
| def is_position(name: str) -> bool: | |
| # The main culture-level position: culture_<adj>_position.md. | |
| # Sub-positions (culture_*_position_language.md) keep their | |
| # _position_ infix; only the bare _position.md is the parent. | |
| return name.endswith("_position.md") and "_position_" not in name | |
| def run(*args): | |
| return subprocess.run( | |
| ["python3", "-m", "khai_tests.review", *args], | |
| capture_output=True, text=True, check=False, | |
| ).stdout | |
| def nlp_route_iso(file_path: Path) -> str | None: | |
| # Returns the ISO code when the file's frontmatter routes | |
| # to the NLP path, else None. The dispatcher is the single | |
| # source of truth for "is this an NLP-language file?". | |
| if dispatch_route is None or _POLICY is None: | |
| return None | |
| try: | |
| route, iso, _ = dispatch_route(file_path, _POLICY) | |
| except Exception: | |
| return None | |
| return iso if route == ROUTE_NLP else None | |
| print("## khai_tests.review on this PR\n") | |
| for d, changed_in_dir in sorted(by_dir.items()): | |
| country = d.name | |
| display = country.replace("_", " ").title() | |
| all_files = sorted(set( | |
| list(d.glob("culture_*.md")) | |
| + list(d.glob("persona_*.md")) | |
| + [f for f in d.glob("*_persona_*.md") if not f.name.startswith("culture_")] | |
| )) | |
| personas = [f for f in all_files if is_persona(f.name)] | |
| positions = [f for f in all_files if is_position(f.name)] | |
| position = positions[0] if positions else None | |
| others = [ | |
| f for f in all_files | |
| if not is_persona(f.name) and not is_position(f.name) | |
| ] | |
| print(f"### `{country}`\n") | |
| # prose-naturalness: only on files this PR actually changed | |
| prose_args = [] | |
| for f in changed_in_dir: | |
| prose_args += ["--file", str(f)] | |
| if prose_args: | |
| print(run("prose", *prose_args)) | |
| # persona-distinct: needs at least two personas in the culture | |
| if len(personas) >= 2: | |
| print(run( | |
| "persona-distinct", | |
| "--culture", display, | |
| "--files", *[str(p) for p in personas], | |
| )) | |
| # shadow-not-position + typical-of: need the culture's position | |
| if position: | |
| for ps in personas: | |
| print(run( | |
| "shadow-not-position", | |
| "--persona", str(ps), | |
| "--position", str(position), | |
| )) | |
| print(run( | |
| "typical-of", | |
| "--file", str(ps), | |
| "--of", f"{display} culture", | |
| )) | |
| # vocab-overlap: each non-persona, non-position file vs the position | |
| if position: | |
| for f in others: | |
| print(run( | |
| "vocab-overlap", | |
| "--file", str(f), | |
| "--position", str(position), | |
| )) | |
| # language-faithful (Stage 2c): on changed files whose | |
| # frontmatter language: routes through the NLP path | |
| # (Igbo, Hausa, Pidgin, ...), call khai_tests.review | |
| # language-faithful so the LLM gates that the prose is in | |
| # the declared language. Lingua-known and default-language | |
| # files are gated by tests/validate_language.py instead. | |
| for f in changed_in_dir: | |
| iso = nlp_route_iso(f) | |
| if iso is None: | |
| continue | |
| print(run( | |
| "language-faithful", | |
| "--file", str(f), | |
| "--language", iso, | |
| )) | |
| print() | |
| print("---") | |
| print("*Advisory -- does not block merge. Built on `khai_tests.review`.*") | |
| PYEOF | |
| cat /tmp/review.md | |
| - name: Post PR comment | |
| if: steps.changed.outputs.any == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const body = fs.readFileSync('/tmp/review.md', 'utf8'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); |