# https://taskfile.dev

version: '3'

vars:
  PROJECT: "transcriber"
  # База диффа для шагов, которым нужна разница с основной веткой.
  BASE: '{{.BASE | default "origin/master"}}'
  # Пути скриптов проверки. Умолчания — канонические пути маркетплейса, чтобы
  # переустановка плагина не меняла Taskfile. Шаги независимы: каждый ловит
  # дрейф своего каталога, и выпадение одного не подменяется другим.
  #
  # Недостающий скрипт — отказ окружения у всех обёрток ниже, и код у него 3 по
  # общему словарю (CLAUDE.md, раздел «Гейт»). Прежний код 1 значил «дрейф» и
  # отправлял читателя искать разъехавшееся там, где просто неполно дерево. Сам
  # `task` отдаёт наружу свой 201 на любой отказ шага, поэтому словарь читается
  # по коду скрипта, а не по коду `task`.
  DOCS_PY: '{{.DOCS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/canon/scripts/docs.py"}}'
  TASKS_PY: '{{.TASKS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/task-track/scripts/tasks.py"}}'
  OPENSPEC_PY: '{{.OPENSPEC_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/code-openspec/scripts/openspec.py"}}'

tasks:

  gate:
    desc: 'Все проверки разом. База диффа: task gate BASE=<rev>'
    cmds:
      - go build ./...
      - go vet ./...
      - |
        unformatted=$(gofmt -l .)
        if [ -n "$unformatted" ]; then
          echo "$unformatted"
          echo "gofmt: файлы выше не отформатированы"
          exit 1
        fi
      - task: tests
      - golangci-lint run
      - task: shell
      - task: dockerfile
      - task: go-version
      - task: migrations
      - task: docs
      - task: tasks
      - task: openspec
      # Последним: единственный шаг, которому нужна сеть, и самый долгий.
      - task: vulns

  tests:
    desc: 'Тесты с детектором гонок'
    cmds:
      # Гонки ищет детектор, а не чтение кода: у сервиса три воркера ходят в одну
      # очередь, и «результат пишет только держатель захвата» — утверждение о
      # одновременном доступе. Детектору нужен CGO и компилятор C; сборка
      # приложения по-прежнему обходится без них (CLAUDE.md, «Стек»), поэтому их
      # отсутствие — отказ окружения, код 3, а не отказ проверки.
      # Окружение проверяется **после** обычного прогона, а не вместо него:
      # отсутствие компилятора отнимает у гейта поиск гонок, но не должно
      # отнимать сами тесты. Порядок проверок — сперва компилятор: без него
      # совет «включи CGO_ENABLED=1» бесполезен.
      - |
        if ! command -v gcc >/dev/null 2>&1 && ! command -v clang >/dev/null 2>&1; then
          go test ./... || exit 1
          echo "тесты прошли, но гонки не искали: детектору нужен компилятор C"
          echo "ни gcc, ни clang не найдены в PATH; поставь: apt install gcc"
          exit 3
        fi
        if [ "$(go env CGO_ENABLED)" != "1" ]; then
          go test ./... || exit 1
          echo "тесты прошли, но гонки не искали: детектору нужен CGO"
          echo "CGO_ENABLED=$(go env CGO_ENABLED); включи: CGO_ENABLED=1 task gate"
          exit 3
        fi
        go test -race ./...

  migrations:
    desc: 'Применённый шаг схемы не переписывается'
    cmds:
      # Инвариант CLAUDE.md (critical): хранилище считает применённое по имени
      # файла шага, поэтому изменить уехавший шаг нельзя — только добавить новый.
      # Компилятор этого не держит, и до этого шага не держало ничто.
      #
      # Судится каталог шагов против базы диффа: у файла шага допустим один
      # статус — `A`. Правка (`M`), удаление (`D`) и переименование (`R`) красят.
      # `migrations.go` под правило не подпадает: строка `Register` у нового шага
      # прибавляется именно там, и запрет на него запретил бы заведение шага.
      - |
        if ! git rev-parse --verify --quiet "{{.BASE}}" >/dev/null 2>&1; then
          echo "база диффа не найдена: {{.BASE}}"
          echo "задай свою: task migrations BASE=<rev>"
          exit 3
        fi
        # Каталог шагов берётся из .av-dev.toml — там он уже записан ключом
        # `migrations` секции `[docs]` для сверки документов. Свой литерал завёл
        # бы факту второй дом: каталог переехал бы, а один из двух стражей молча
        # позеленел. До слияния плагинов файл звался docs/.docs.json.
        dir=$(python3 -c 'import tomllib; print(tomllib.load(open(".av-dev.toml","rb"))["docs"]["migrations"])' 2>/dev/null) || dir=""
        if [ -z "$dir" ] || [ ! -d "$dir" ]; then
          echo "каталог шагов схемы не найден: ключ [docs] migrations в .av-dev.toml → '$dir'"
          exit 3
        fi
        # Страж предмета: правило, потерявшее файлы, стало бы вечно зелёным от
        # одного переименования — тот же приём, что у правил `internal/archrules`.
        if [ -z "$(ls "$dir" | grep -E '^[0-9]{12}_.*\.go$')" ]; then
          echo "в $dir нет ни одного файла шага: правило потеряло предмет"
          echo "поправь шаблон имени в этом шаге либо ключ [docs] migrations в .av-dev.toml"
          exit 3
        fi
        # Баз две, и вторая обязательна. `{{.BASE}}` отвечает на «шаг уже уехал»
        # ровно настолько, насколько свеж `origin/master`: отставшая ссылка
        # читает весь каталог как добавленный, и правило молчит. `HEAD` ловит
        # правку закоммиченного шага в рабочем дереве независимо от ссылки.
        for base in {{.BASE}} HEAD; do
          touched=$(git diff --name-status "$base" -- "$dir" \
            | grep -E '[0-9]{12}_[^/]*\.go$' \
            | grep -vE '^A[[:space:]]' || true)
          if [ -n "$touched" ]; then
            echo "база $base:"
            echo "$touched"
            echo "применённый шаг схемы переписан: изменение схемы — только новым файлом шага"
            echo "(CLAUDE.md, «Инварианты», critical: хранилище считает применённое по имени файла)"
            exit 1
          fi
        done

  shell:
    desc: 'shellcheck на скрипты оболочки'
    cmds:
      # Скриптов два и оба свои: шаг сверки версий и `docker/entrypoint.sh`.
      # Второй в образ копируется, но не исполняется — `ENTRYPOINT` в
      # `Dockerfile` закомментирован, — и проверяется он именно поэтому: код,
      # который никто не гоняет, портится незаметно. Ни один из двух не виден ни
      # `go vet`, ни `golangci-lint`.
      - |
        if ! command -v shellcheck >/dev/null 2>&1; then
          echo "shellcheck не найден в PATH"
          echo "поставь: apt install shellcheck (или https://github.com/koalaman/shellcheck)"
          exit 3
        fi
        shellcheck scripts/check-go-version.sh docker/entrypoint.sh

  dockerfile:
    desc: 'hadolint на Dockerfile'
    cmds:
      # DL3007 (`alpine:latest` у рантайм-слоя) подавлен: это открытая задача
      # `pin-runtime-image-base`, и до её решения шаг краснел бы на известном.
      # DL3018 (закрепить версии пакетов `apk`) подавлен по существу: alpine не
      # держит старые версии в репозитории, и закрепление ломает сборку через
      # недели — то есть лечение хуже болезни.
      - |
        if ! command -v hadolint >/dev/null 2>&1; then
          echo "hadolint не найден в PATH"
          echo "поставь: https://github.com/hadolint/hadolint/releases"
          exit 3
        fi
        hadolint --ignore DL3007 --ignore DL3018 Dockerfile

  go-version:
    desc: 'Одна версия Go в go.mod, Dockerfile, CLAUDE.md и README.md'
    cmds:
      # Скрипт лежит в самом репозитории, а не в плагине: его отсутствие значит
      # сломанное дерево, а не непоставленный плагин, и переопределять путь
      # нечем и незачем. Код отсутствия — 3, как у прочих обёрток.
      - |
        py=scripts/check-go-version.sh
        if [ ! -f "$py" ]; then
          echo "$py не найден: дерево репозитория неполно"
          exit 3
        fi
        sh "$py"

  docs:
    desc: 'Раскладка docs/ против канона'
    cmds:
      # Шаг обязан краснеть внятно, если скрипта нет, а не пропускаться молча.
      - |
        py=$(eval echo {{.DOCS_PY}})
        if [ ! -f "$py" ]; then
          echo "docs.py не найден: $py"
          echo "поставь плагин av-dev либо задай путь: task docs DOCS_PY=<путь>"
          exit 3
        fi
        python3 "$py" check --base {{.BASE}}

  tasks:
    desc: 'Согласованность каталога задач'
    cmds:
      - |
        py=$(eval echo {{.TASKS_PY}})
        if [ ! -f "$py" ]; then
          echo "tasks.py не найден: $py"
          echo "поставь плагин av-dev либо задай путь: task tasks TASKS_PY=<путь>"
          exit 3
        fi
        python3 "$py" check --dir tasks

  openspec:
    desc: 'Форма openspec/config.yaml'
    cmds:
      - |
        py=$(eval echo {{.OPENSPEC_PY}})
        if [ ! -f "$py" ]; then
          echo "openspec.py не найден: $py"
          echo "поставь плагин av-dev либо задай путь: task openspec OPENSPEC_PY=<путь>"
          exit 3
        fi
        python3 "$py" check --dir .

  vulns:
    desc: 'Достижимые из кода уязвимости в зависимостях'
    cmds:
      # `govulncheck` — внешний инструмент, а не плагин и не файл репозитория:
      # ставится `go install golang.org/x/vuln/cmd/govulncheck@latest`. Его
      # отсутствие — отказ окружения, код 3, как у прочих обёрток.
      #
      # Свой код 3 у самого инструмента значит «уязвимость найдена» и с кодом
      # обёртки совпадает; различает их сообщение — обёртка называет недостающий
      # инструмент. Шагу нужна сеть: база уязвимостей живёт на vuln.go.dev, и без
      # сети шаг краснеет, а не пропускается молча.
      - |
        if ! command -v govulncheck >/dev/null 2>&1; then
          echo "govulncheck не найден в PATH"
          echo "поставь: go install golang.org/x/vuln/cmd/govulncheck@latest"
          exit 3
        fi
        govulncheck ./...

  # Контракт роли app_image (pet-project-server): собрать полный образ и затегать
  # его $BUILD_ID. Деплой целиком: `inv pl -- transcriber` в pet-project-server.
  image:
    desc: 'Docker-образ. Тег из $BUILD_ID (по умолчанию dev)'
    cmds:
      - docker build --pull --file Dockerfile --tag "{{.PROJECT}}:${BUILD_ID:-dev}" .
