# ADR-265 D1 — the npm-package gate.
#
# Every Node package in this repo (published or private) gets: install, build,
# tests, a version-literal gate (D3 — package.json is the only place a version
# lives), a pack-content gate (no source maps, unpacked-size budget), a
# tarball-install smoke test (would have caught ADR-264 F1's broken `require`
# export), and the claim-check honesty lint on the README (D4).

name: npm packages

on:
  push:
    branches: [main]
    paths:
      - 'harness/ruview/**'
      - 'tools/ruview-mcp/**'
      - 'tools/ruview-cli/**'
      - '.github/workflows/npm-packages.yml'
  pull_request:
    paths:
      - 'harness/ruview/**'
      - 'tools/ruview-mcp/**'
      - 'tools/ruview-cli/**'
      - '.github/workflows/npm-packages.yml'

permissions:
  contents: read

jobs:
  gate:
    name: ${{ matrix.package.dir }} (node ${{ matrix.node }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node: ['20', '22']
        package:
          - dir: harness/ruview
            build: false
            publishable: true
            # ADR-263: dependency-free harness; budget guards against dep creep.
            unpacked_budget: 65536
          - dir: tools/ruview-mcp
            build: true
            publishable: true
            # ADR-264 O2: map-free tarball (was 188 kB with maps).
            unpacked_budget: 140000
          - dir: tools/ruview-cli
            build: true
            publishable: false
            unpacked_budget: 0
    defaults:
      run:
        working-directory: ${{ matrix.package.dir }}
    steps:
      - uses: actions/checkout@v4

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

      # Repo policy gitignores lockfiles under harness/ (the harness is
      # dependency-free anyway); the TS packages commit theirs.
      - name: Install
        run: |
          if [ -f package-lock.json ]; then npm ci; else npm install --no-fund --no-audit; fi

      - name: Build
        if: ${{ matrix.package.build }}
        run: npm run build

      - name: Test
        run: npm test --if-present

      # ADR-265 D3 — package.json is the only place a version string lives.
      - name: Version-literal gate
        run: |
          set -euo pipefail
          hits=""
          for d in src bin; do
            if [ -d "$d" ]; then
              hits+=$(grep -rEn '\b[0-9]+\.[0-9]+\.[0-9]+\b' "$d" | grep -vE '127\.0\.0\.1|0\.0\.0\.0' || true)
            fi
          done
          if [ -n "$hits" ]; then
            echo "Hardcoded version-like literals found (read package.json instead — ADR-265 D3):"
            echo "$hits"
            exit 1
          fi

      # ADR-265 D1.3 — pack-content gate: no maps, size budget enforced.
      - name: Pack gate
        if: ${{ matrix.package.publishable }}
        run: |
          npm pack --dry-run --json 2>/dev/null | node -e "
            const [info] = JSON.parse(require('fs').readFileSync(0, 'utf8'));
            const budget = Number(process.env.UNPACKED_BUDGET);
            const maps = info.files.filter((f) => f.path.endsWith('.map'));
            if (maps.length > 0) {
              console.error('Tarball contains source maps (ADR-264 F2):', maps.map((m) => m.path));
              process.exit(1);
            }
            if (info.unpackedSize > budget) {
              console.error(\`Unpacked size \${info.unpackedSize} B exceeds budget \${budget} B\`);
              process.exit(1);
            }
            console.log(\`pack gate OK: \${info.files.length} files, \${info.unpackedSize} B unpacked (budget \${budget} B), 0 maps\`);
          "
        env:
          UNPACKED_BUDGET: ${{ matrix.package.unpacked_budget }}

      # ADR-265 D1.4 — install the real tarball and drive each bin/export.
      - name: Tarball smoke test
        if: ${{ matrix.package.publishable }}
        run: |
          set -euo pipefail
          TGZ="$PWD/$(npm pack --silent 2>/dev/null | tail -1)"
          SMOKE="$(mktemp -d)"
          cd "$SMOKE"
          npm init -y > /dev/null
          npm i --no-fund --no-audit "$TGZ"
          case "${{ matrix.package.dir }}" in
            harness/ruview)
              ./node_modules/.bin/ruview --version
              ./node_modules/.bin/ruview doctor
              # the honesty gate must fail closed on empty input (ADR-263 F1)
              if ./node_modules/.bin/ruview claim-check; then
                echo 'claim-check passed with no input — fail-open regression'; exit 1
              fi
              node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);"
              ;;
            tools/ruview-mcp)
              # initialize over stdio; server must answer and exit 0 on EOF
              printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}\n' \
                | timeout 30 ./node_modules/.bin/rvagent | grep -q '"serverInfo"'
              # the ESM export must resolve from the installed tarball (ADR-264 F1)
              timeout 30 node --input-type=module -e "await import('@ruvnet/rvagent');" < /dev/null
              ;;
          esac

      # ADR-265 D4 — package READMEs must pass the project's own honesty lint.
      - name: Claim-check README
        run: |
          if [ -f README.md ]; then
            node "$GITHUB_WORKSPACE/harness/ruview/bin/cli.js" claim-check --file README.md
          else
            echo "no README.md — skipping"
          fi
