name: CI

on:
  push:
    branches: [main, master]
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  checks:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node: [24, 26]
    name: node ${{ matrix.node }}
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node }}

      - name: Enable corepack (pnpm)
        run: corepack enable

      - name: Install (immutable)
        run: pnpm install --frozen-lockfile

      - name: Constraints
        run: pnpm run constraints

      # Before lint: root typecheck validates the package/vendor reference graph
      # and refreshes TSC intermediates so type-aware ESLint sees the same project
      # boundaries as the build.
      - name: Typecheck (src + tests + examples)
        run: pnpm run typecheck

      - name: Lint
        run: pnpm run lint

      # Doc-sync gates (doc-sync-enforcement RFC). doc-typecheck compiles the
      # fenced ts blocks against the root project-reference graph. The cordis
      # catalog freshness check, type-equiv check, and markdown wrap/link checks
      # only read source. Same `doc-sync` script the pre-push hook runs
      # (quality-gates RFC: one source of truth).
      - name: Doc-sync gates (doc code blocks + cordis catalog + type-equiv + markdown wrap/links)
        run: pnpm run doc-sync

      # Module-graph freshness: regenerate docs/module-graph.md from the
      # packages' peerDependencies and fail if it differs from the committed
      # file. Only reads source package.json — no build needed.
      - name: Module-graph freshness
        run: pnpm run verify-module-graph

      - name: Tests with coverage gate (per-file 100%)
        run: pnpm run test:coverage

      # ACP snapshot tests (acp-snapshot-tests RFC): boot the real acp-agent
      # subprocess and replay recorded session-log fixtures, diffing the
      # normalized stdout transcript + re-persisted log against committed
      # goldens. KEYLESS by design — the same `test:snapshot` script the pre-push
      # hook runs (one source of truth), so the full-transcript regression net
      # is part of every PR gate, not just local pre-push.
      - name: Snapshot tests (ACP transcript replay)
        run: pnpm run test:snapshot

      # Before hygiene: publint validates the packed artifacts (lib/index.js),
      # which only the tsdown bundling step emits, and verify-node-next-types
      # validates the built declarations.
      - name: Build (tsc -b + tsdown bundles)
        run: pnpm run build

      - name: Hygiene (knip + publint + constraints + NodeNext types)
        run: pnpm run hygiene

      - name: Demo smoke test
        run: |
          set -euo pipefail
          out=$(printf 'echo ci smoke\n' | timeout 60 pnpm run demo:echo 2>&1)
          echo "$out"
          echo "$out" | grep -q '\[tool call\] echo({"text":"ci smoke"})'
          echo "$out" | grep -q '\[tool result\] ECHO: CI SMOKE'
          # The JSONL backend (root ./.sessions, no cwd → _no-cwd bucket) writes a
          # per-run session log named main-session-<uuid>.jsonl. Assert one exists.
          ls .sessions/_no-cwd/main-session-*.jsonl >/dev/null
          rm -rf .sessions

      # The published `bin` is `lib/bin.js`, run under plain `node` by a real
      # consumer — NOT the tsx dev path the demo smoke and demo:* scripts use.
      # These keyless smokes boot the BUILT bins (this step runs AFTER the build)
      # in a temp dir that mirrors a real install, catching a regression in the
      # published artifact that tsx would mask. They self-skip if lib/ is absent,
      # so the e2e job (which does not build) does not run them.
      - name: Built-bin smoke test (published lib/bin.js under node)
        run: pnpm exec vitest run --config vitest.e2e.config.ts packages/ui/stdio-agent/tests/built-bin.e2e.ts packages/ui/acp-agent/tests/built-bin.e2e.ts
