chore: add test strategy to lifecycle #544
Workflow file for this run
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: validate | |
| on: | |
| pull_request: | |
| branches: [main, culture/release] | |
| jobs: | |
| khai-encoding: | |
| name: khai - Encoding rules | |
| needs: [setup, cultures-stamp-check] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run encoding tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_encoding --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-process: | |
| name: khai - Process rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run process tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_process --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-position: | |
| name: khai - Position rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run position tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_position --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-piece: | |
| name: khai - Piece rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run piece tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_piece --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-place: | |
| name: khai - Place rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run place tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_place --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-persona: | |
| name: khai - Persona rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run persona tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_persona --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-sections: | |
| name: cultures - Section structure | |
| needs: [setup, khai-process, khai-position, khai-piece, khai-place, khai-persona] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run section tests | |
| run: python -m pytest tests/test_sections.py --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-branch-scope-tests: | |
| name: cultures - Branch scope tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| # PyYAML required for E2E test (hook invokes validate_country_bag.py). | |
| run: pip install -r tests/requirements.txt pytest | |
| - name: Run branch_scope unit + E2E tests | |
| run: python -m pytest tests/test_branch_scope.py tests/test_hook_scope_e2e.py -v | |
| cultures-branch-scope-diff-check: | |
| name: cultures - Branch scope diff check | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Check PR diff against branch scope rules | |
| env: | |
| BRANCH: ${{ github.head_ref }} | |
| BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| set -e | |
| git diff --name-only --diff-filter=ACMRT "origin/${BASE_REF}...HEAD" > /tmp/files.txt | |
| if [ ! -s /tmp/files.txt ]; then | |
| echo "No files changed; skipping scope check." | |
| exit 0 | |
| fi | |
| python3 - <<'PY' | |
| import os, sys | |
| sys.path.insert(0, "tests") | |
| from branch_scope import classify_branch, check_scope | |
| branch = os.environ["BRANCH"] | |
| with open("/tmp/files.txt") as f: | |
| files = [l.strip() for l in f if l.strip()] | |
| kind = classify_branch(branch) | |
| if kind == "main": | |
| print(f"ERROR: PR head branch is main; impossible state.") | |
| sys.exit(1) | |
| ok, unsafe = check_scope(kind, files, branch) | |
| if not ok: | |
| print(f"ERROR: branch '{branch}' (classified '{kind}') has scope violations:") | |
| for u in unsafe: | |
| print(f" - {u}") | |
| sys.exit(1) | |
| print(f"OK: branch '{branch}' (classified '{kind}') passes scope check ({len(files)} file(s)).") | |
| PY | |
| cultures-pr-base-tests: | |
| name: cultures - PR base tests | |
| # Unit-test gate for tests/validate_pr_base.py (the PR base routing | |
| # contract). Independent, always runs, no culture-file dependency. | |
| # Catches routing regressions before pr-gate.yml enforces them in | |
| # production. CLI file exercises the script pr-gate.yml actually calls. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run PR base routing tests | |
| run: python -m pytest tests/test_validate_pr_base.py tests/test_validate_pr_base_cli.py -v | |
| cultures-audit-readme-bands-tests: | |
| name: cultures - Audit README bands tests | |
| # Unit-test gate for scripts/audit_readme_bands.py (the canonical Hofstede | |
| # band contract: 0-39 Low, 40-69 Moderate, 70-100 High). Mirrors the | |
| # cultures-branch-scope-tests pattern: independent, always runs, exercises the | |
| # contract on every PR. Closes the gap noted during the v2 stage 2 | |
| # sequence where this test had no CI runner. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run audit_readme_bands tests | |
| run: python -m pytest tests/test_audit_readme_bands.py -v | |
| cultures-hofstede-readme-updater-tests: | |
| name: cultures - Hofstede README updater tests | |
| # Unit-test gate for scripts/update_hofstede_readme.py (deterministic | |
| # rewriter for the Hofstede Cultural Dimensions + Alignment Status tables | |
| # in country READMEs). Pins the EXCELLENT/PASS/WARN/FAIL thresholds, the | |
| # canonical band-label rendering, and the table-replacement contract. | |
| # Mirrors the cultures-audit-readme-bands-tests pattern: independent, always runs. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| # Mirrors cultures-hofstede-derived / cultures-hofstede-bag-loader-tests: tests/requirements.txt | |
| # has lingua-language-detector, PyYAML, and pytest. Required because | |
| # update_hofstede_readme imports data.hofstede_keywords transitively | |
| # via hofstede_bag_loader, and data.hofstede_keywords imports lingua | |
| # at module level (for detect_language). The unit tests don't call | |
| # detect_language but the import has to succeed for the module to | |
| # load. | |
| run: pip install -r tests/requirements.txt | |
| - name: Run update_hofstede_readme tests | |
| run: python -m pytest tests/test_update_hofstede_readme.py -v | |
| cultures-history-arc: | |
| name: cultures - History arc | |
| # Always-run gate for the history-as-defining-moments-arc methodology | |
| # (khai-cultures-create skill v0.1.1+). Validates every | |
| # culture_*_history_*.md file in regions/** has: | |
| # - *khai: piece* declaration footer | |
| # - Yearbook section with >= 12 dated entries | |
| # - Date range spanning >= 5 centuries (broad arc, not single event) | |
| # Catches the class of bug PR #105 surfaced: a history file shipping | |
| # as a narrow-event essay (the original Beeldenstorm-only Netherlands | |
| # history) rather than a broad-arc Yearbook. Mirrors the | |
| # cultures-audit-readme-bands-tests pattern: independent, always runs. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run history-arc tests | |
| run: python -m pytest tests/test_history_arc.py -v | |
| cultures-language-preflight: | |
| name: cultures - Language pre-flight | |
| # Always-run pre-flight: every country README's **Language(s):** slug | |
| # must already appear in data/language_policy.yaml. Mirrors the | |
| # cultures-audit-readme-bands-tests / cultures-history-arc pattern: independent, | |
| # always runs, no setup-job dependency. Fast (no lingua, no whl | |
| # install) -- under 30 seconds. | |
| # | |
| # Why a separate job: the same assertion runs late in the chain via | |
| # cultures-language, which depends on khai-language, which depends | |
| # on the components chain (encoding -> process/position/piece/place/ | |
| # persona -> sections). When the registry is missing an entry, that | |
| # failure surfaces 5-10 minutes into CI after every other validator | |
| # has run. This job surfaces it in ~30 seconds on every PR so a | |
| # missing-language case fails before the rest of the chain spins up. | |
| # | |
| # Incident: PR #166 (culture/spain) cleared every other validator | |
| # then failed on this assertion because `spanish` was not in | |
| # data/language_policy.yaml. Emergency PRs #167 (governance) and | |
| # #168 (sync) unblocked the release at the cost of two extra round | |
| # trips. The check itself was correct; it just ran too late to be | |
| # useful for prevention. | |
| # | |
| # LANGUAGES.md section "Pre-flight: check language enablement before | |
| # starting a culture" documents the human-side procedure that this | |
| # gate enforces. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest + pyyaml | |
| run: pip install pytest pyyaml | |
| - name: Run registry pre-flight | |
| run: python -m pytest tests/test_language.py::test_readme_language_in_registry -v | |
| setup: | |
| name: Setup - Compute changed files | |
| runs-on: ubuntu-latest | |
| outputs: | |
| any: ${{ steps.changed.outputs.any }} | |
| files: ${{ steps.changed.outputs.files }} | |
| # `data_changed`: true when the PR diff touches the Hofstede reference | |
| # dataset (data/hofstede_scores.json). Drives cultures-hofstede-reference into audit mode (run | |
| # across all countries) so a reference update doesn't silently widen | |
| # divergence for cultures whose declared scores are now off-band. | |
| data_changed: ${{ steps.changed.outputs.data_changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get changed regions/*.md files | |
| id: changed | |
| env: | |
| HEAD_REF: ${{ github.head_ref }} | |
| run: | | |
| # Sync branches (sync/<name>) are pointer branches at main's HEAD. | |
| # Content was already validated when it landed on main. The PR diff | |
| # against culture/release would re-surface every file that has | |
| # advanced on main since the last sync, which is pure waste -- | |
| # short-circuit here so all downstream content jobs gated on | |
| # `any == 'true'` skip cleanly. | |
| if [[ "$HEAD_REF" == sync/* ]]; then | |
| echo "Sync branch detected ($HEAD_REF); skipping content-validator file detection." | |
| echo "any=false" >> "$GITHUB_OUTPUT" | |
| echo "files=" >> "$GITHUB_OUTPUT" | |
| echo "data_changed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Validate fires only on pull_request, so the diff range is always | |
| # the merge base. (The previous push trigger required handling | |
| # `github.event.before` and HEAD~1 fallback; dropped along with | |
| # the push trigger -- post-merge re-validation added no value.) | |
| RANGE="origin/${{ github.base_ref }}...HEAD" | |
| # --diff-filter=ACMRT skips deletions; the validator must not be handed paths that no longer exist. | |
| ALL_CHANGED=$(git diff --name-only --diff-filter=ACMRT "$RANGE") | |
| FILES=$(echo "$ALL_CHANGED" | { grep -E '^regions/.*\.md$' || true; } | tr '\n' ' ' | sed 's/ *$//') | |
| ANY=$(if [ -z "$FILES" ]; then echo "false"; else echo "true"; fi) | |
| DATA_CHANGED=$(if echo "$ALL_CHANGED" | grep -qx 'data/hofstede_scores.json'; then echo "true"; else echo "false"; fi) | |
| echo "any=$ANY" >> "$GITHUB_OUTPUT" | |
| echo "files=$FILES" >> "$GITHUB_OUTPUT" | |
| echo "data_changed=$DATA_CHANGED" >> "$GITHUB_OUTPUT" | |
| cultures-stamp-check: | |
| name: cultures - Validation stamp gate | |
| needs: [setup] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check validation stamp exists | |
| run: | | |
| if [ ! -f ".validation-stamp" ]; then | |
| echo "❌ VALIDATION FAILED: .validation-stamp missing" | |
| echo "" | |
| echo "This file records that local validators passed before commit." | |
| echo "Run locally to generate it:" | |
| echo " git config core.hooksPath .githooks" | |
| echo " scripts/setup-hooks.sh # or setup-hooks.bat on Windows" | |
| echo " git commit --amend -m 'your message'" | |
| exit 1 | |
| fi | |
| echo "✔️ Validation stamp present" | |
| - name: Check validation stamp shape | |
| # Gate against malformed stamps (CRLF, NUL padding, BOM, wrong length, | |
| # non-hex content). .githooks/pre-commit writes the stamp as exactly | |
| # 40 hex chars + LF in UTF-8 (see the closing lines: | |
| # tree_hash = run_git("write-tree") | |
| # Path(".validation-stamp").write_text(tree_hash + "\n", encoding="utf-8") | |
| # ). Any deviation means the stamp was written through a non-hook | |
| # path -- typically an LLM agent's file write API (Claude Code in | |
| # VS Code, Copilot, MCP create_or_update_file) on a Windows host | |
| # that normalised line endings or added UTF-16 padding. The stamp | |
| # value itself isn't checked against tree state (the hook owns | |
| # that); this step only enforces shape so byte-exact three-way | |
| # merges don't trip on encoding artifacts. | |
| # | |
| # Incident: PR #105 (culture/netherlands -> culture/release) was | |
| # unmergeable for hours because culture/release's stamp had been | |
| # written by Claude Code in VS Code with `\r\n\r\x00\n\x00` after | |
| # the hash (46 bytes total, binary on disk). Both Copilot and a | |
| # second Claude Code session tried to "just pick one" via clean | |
| # 41-byte UTF-8 writes; git's three-way blob comparison rejected | |
| # every attempt because the bytes never matched. Resolution | |
| # required reverting the stamp to the merge-base blob -- a | |
| # non-obvious path that no LLM-driven agent found on its own. | |
| run: | | |
| python3 - <<'PY' | |
| import re | |
| import sys | |
| from pathlib import Path | |
| raw = Path(".validation-stamp").read_bytes() | |
| if len(raw) == 41 and re.fullmatch(rb"[0-9a-f]{40}\n", raw): | |
| print("✔️ Validation stamp shape OK (40 hex chars + LF, 41 bytes)") | |
| sys.exit(0) | |
| print("❌ VALIDATION FAILED: .validation-stamp is malformed") | |
| print(f" Expected: 41 bytes, [0-9a-f]{{40}} + LF (UTF-8, no BOM)") | |
| print(f" Actual: {len(raw)} bytes") | |
| print(f" Hex dump: {raw.hex()[:120]}" + ("..." if len(raw) > 60 else "")) | |
| print() | |
| print(" Most common cause: an LLM-driven file write (Claude Code in") | |
| print(" VS Code, Copilot suggesting a fix, MCP create_or_update_file)") | |
| print(" produced CRLF, UTF-16 padding, or BOM bytes instead of the") | |
| print(" clean UTF-8 LF the pre-commit hook writes. See PR #105.") | |
| print() | |
| print(" Fix: re-run the pre-commit hook locally to rewrite cleanly.") | |
| print(" touch a staged file (or git add --renormalize .)") | |
| print(" git commit --amend --no-edit") | |
| print(" git push --force-with-lease") | |
| sys.exit(1) | |
| PY | |
| khai-language: | |
| name: khai - Language rules | |
| needs: [setup, cultures-sections] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests + lingua + pyyaml | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" lingua-language-detector pyyaml | |
| - name: Run language detection tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_language --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-language: | |
| name: cultures - Language policy | |
| needs: [setup, khai-language] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest + pyyaml | |
| run: pip install pytest pyyaml | |
| - name: Run static language checks | |
| run: python -m pytest tests/test_language.py -v | |
| khai-links: | |
| name: khai - Link rules | |
| needs: [setup, cultures-language] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run link tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_links --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-links: | |
| name: cultures - Link integrity | |
| needs: [setup, khai-links] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run link tests | |
| run: python -m pytest tests/test_links.py --khai-roots=engine --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-completeness: | |
| name: cultures - Culture completeness | |
| needs: [setup, cultures-links] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Validate culture completeness | |
| run: python -m pytest tests/test_completeness.py -v | |
| cultures-audit-readme: | |
| name: cultures - Audit README status tables | |
| needs: [setup, cultures-completeness] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Validate audit README | |
| run: python -m pytest tests/test_audit_readme.py -v | |
| cultures-audit-consistency: | |
| name: cultures - Audit table consistency | |
| needs: [setup, cultures-audit-readme] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Validate audit consistency | |
| run: python -m pytest tests/test_audit_consistency.py -v | |
| khai-plagiarism: | |
| name: khai - Phrase denylist rules | |
| needs: [setup, cultures-audit-consistency] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| run: | | |
| RELEASE=$(curl -s \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| https://api.github.com/repos/ChBrain/KAIHACKS/releases/tags/khai-tests-v0.1.7) | |
| ASSET_INFO=$(echo "$RELEASE" | python3 -c "import sys,json; a=next(x for x in json.load(sys.stdin)['assets'] if x['name'].endswith('.whl')); print(a['url'],a['name'])") | |
| ASSET_URL=$(echo "$ASSET_INFO" | cut -d' ' -f1) | |
| ASSET_NAME=$(echo "$ASSET_INFO" | cut -d' ' -f2) | |
| curl -sL \ | |
| -H "Authorization: Bearer ${{ secrets.KAIHACKS }}" \ | |
| -H "Accept: application/octet-stream" \ | |
| "$ASSET_URL" -o "/tmp/$ASSET_NAME" | |
| pip install "/tmp/$ASSET_NAME" | |
| - name: Run phrase denylist tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_plagiarism --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-plagiarism: | |
| name: cultures - Plagiarism structure | |
| needs: [setup, khai-plagiarism] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run plagiarism structural checks | |
| run: python -m pytest tests/test_plagiarism.py -v | |
| cultures-hofstede-structure: | |
| name: cultures - Hofstede structure | |
| needs: [setup, cultures-plagiarism] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Validate Hofstede structure | |
| run: python -m pytest tests/test_hofstede_structure.py -v | |
| cultures-hofstede-derived: | |
| name: cultures - Hofstede derived vs declared | |
| needs: [setup, cultures-hofstede-structure] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| env: | |
| PR_CHANGED_FILES: ${{ needs.setup.outputs.files }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| run: pip install -r tests/requirements.txt | |
| - name: Validate Hofstede derived scores | |
| run: python -m pytest tests/test_hofstede_derived.py -v | |
| cultures-hofstede-reference: | |
| name: cultures - Hofstede declared vs reference | |
| needs: [setup, cultures-hofstede-derived] | |
| if: needs.setup.outputs.any == 'true' || needs.setup.outputs.data_changed == 'true' | |
| runs-on: ubuntu-latest | |
| env: | |
| PR_CHANGED_FILES: ${{ needs.setup.outputs.files }} | |
| PR_DATA_CHANGED: ${{ needs.setup.outputs.data_changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run Hofstede reference tests | |
| run: python -m pytest tests/test_hofstede_reference.py -v | |
| cultures-hofstede-bag-loader-tests: | |
| name: cultures - Hofstede bag loader tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| run: pip install -r tests/requirements.txt | |
| - name: Run loader tests | |
| run: python -m pytest tests/test_hofstede_bag_loader.py -v | |
| cultures-hofstede-bag-integrity: | |
| name: cultures - Hofstede bag integrity | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| run: pip install -r tests/requirements.txt | |
| - name: Run bag integrity tests | |
| run: | | |
| python3 -m pytest tests/test_hofstede_bag_shape.py \ | |
| tests/test_hofstede_bag_quality.py \ | |
| tests/test_hofstede_bag_locked.py \ | |
| tests/test_hofstede_bag_completeness.py \ | |
| tests/test_hofstede_bag_fork.py \ | |
| -v | |
| no-changes: | |
| name: No regions/*.md files changed | |
| needs: [setup] | |
| if: needs.setup.outputs.any == 'false' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: echo "No regions/*.md files changed - skipping validation" |