# Continuous integration.
#
# Two jobs, split by language, because they prove different things and the
# Python one is the slow half.
#
# What this actually gates, in order of how likely each is to catch something:
#
#   1. The two implementations of the game agree. `engine.py` and `engine.ts`
#      score every ordered pair of words in the answer list — 4,603^2 = 21.2M
#      feedback patterns — and their SHA-256 digests must match each other and
#      the committed value. The page claims it VERIFIES a recorded run in your
#      browser; that claim is only worth something if the browser's scorer and
#      the environment's scorer cannot drift.
#   2. The reward measures something. `probe.py` plays seven deliberately
#      crafted policies and asserts the reward orders them the way a person
#      would, PER WEIGHTED COMPONENT — a component that is flat across every
#      policy is measuring nothing and still moves the total. It also asserts
#      neither good policy dominates the other, because the reward editor on
#      the site is a claim about a real trade-off.
#   3. Every committed fixture replays through the engine and reproduces its
#      own recorded rewards. A fixture that cannot be regenerated is a claim
#      with no receipt behind it.
#   4. The word lists rebuild byte-identically from their committed sources.
#      If they do not, the conformance digest is describing a different game.
#   5. The demo contract holds — see scripts/check-demos.mjs.
#   6. Four things that fail SILENTLY in production and nowhere else:
#      prerendered routes carrying their own <head>, sitemap.xml existing,
#      no blob-backed worker, and the bundle budget.
#
# NO MODEL IS EVER CALLED. Rollouts are captured by hand and committed; a gate
# that cost a paid inference call would be switched off within a month.
#
# This runner is aarch64 and configured with `container.network: host`. Nothing
# here needs a service container, so the traps that cost PIG three failed runs
# do not apply — but do not add `services:` here without reading that repo's
# workflow first.

name: ci

on:
  push:
    branches: [main]
  pull_request:

jobs:
  web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      # Corepack ships with Node and installs the exact pnpm pinned by
      # `packageManager`, so CI and a laptop run the same version. The download
      # prompt is disabled because a non-interactive runner cannot answer it and
      # would otherwise hang until the job times out.
      - name: Enable pnpm
        env:
          COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'
        run: |
          corepack enable
          corepack prepare --activate
          test "$(pnpm -v)" = "11.21.0" || { echo "pnpm $(pnpm -v) != 11.21.0"; exit 1; }

      - name: Install
        env:
          CI: 'true'
        run: pnpm install --frozen-lockfile

      # `pnpm build` ends in the prerender pass, which drives a real browser.
      # Without this the build fails on its very last action, after everything
      # else has gone green — the most expensive place to find a missing
      # dependency.
      - name: Install chromium
        run: pnpm exec playwright install --with-deps chromium

      - run: pnpm typecheck
      - run: pnpm check
      - run: pnpm test
      - run: pnpm build
      - run: node scripts/bundle-budget.mjs

      # Crawlers do not run JavaScript. Without a real per-route HTML file every
      # shared link previews as the homepage, and the failure is invisible from
      # inside the app.
      - name: The prerendered head is real
        run: |
          test -f dist/demos/wordle/index.html || { echo "no prerendered demo route"; exit 1; }
          grep -q 'og:title' dist/demos/wordle/index.html || { echo "og tags missing"; exit 1; }
          grep -q '<title>Word Five' dist/demos/wordle/index.html \
            || { echo "the demo route kept the homepage title"; exit 1; }
          test -f dist/404.html || { echo "no 404.html"; exit 1; }
          test -f dist/sitemap.xml || { echo "no sitemap.xml"; exit 1; }

      # The site's CSP has no `worker-src`, so it falls back to
      # `default-src 'self'` and a blob-backed worker is blocked with no console
      # error — in production only. Grep for construction FROM a blob, not for
      # the string anywhere: React's bundle contains it in a scheme check, and a
      # check that fails on a risk which is not present teaches everyone to
      # ignore it.
      - name: No blob-backed workers
        run: |
          if grep -rEo "new (Shared)?Worker\([^)]{0,80}" dist/assets/*.js \
             | grep -E "blob:|createObjectURL"; then
            echo "a worker is constructed from a blob URL; production CSP blocks it silently"
            exit 1
          fi
          echo "ok — no blob-backed worker construction in the bundle"

  python:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        run: |
          curl -LsSf https://astral.sh/uv/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - run: uv sync --all-packages

      # If these do not reproduce, every downstream number — the conformance
      # digest included — is describing a different game.
      - name: The word lists rebuild identically
        run: |
          uv run python envs/wordle_five/words/build_words.py
          git diff --exit-code envs/wordle_five/words/*.json

      - run: uv run pytest envs/wordle_five/tests -q
      - run: uv run python envs/probe.py
      - run: uv run python envs/verify_fixtures.py

      # The other half of the cross-language gate. Node scores the same 21.2M
      # pairs and must produce the digest Python committed.
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - name: Enable pnpm
        env:
          COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'
        run: |
          corepack enable
          corepack prepare --activate
      - name: Install
        env:
          CI: 'true'
        run: pnpm install --frozen-lockfile
      - run: pnpm conformance
