diff --git a/Makefile b/Makefile index 098ecde..fe25938 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ CHANGE ?= slice-0-skeleton # Текущая версия модели данных — по ней рисуются диаграммы (make diagrams) DBML ?= model/data-model-v5.18.dbml -.PHONY: venv validate-contracts validate-openspec test-snapshot check diagrams +.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web sync-web check diagrams # Единственная зависимость скриптов проверки контракта — jsonschema venv: @@ -26,7 +26,15 @@ validate-openspec: test-snapshot: scripts/test_prepare_contour_snapshot.sh -check: validate-contracts validate-openspec test-snapshot +# sync-web.sh на синтетических репозиториях: точка синхронизации, перенос набора, отказ при расхождении, пайплайн переноса +test-sync-web: + scripts/test_sync_web.sh + +# Синхронизация arm-core/web с arm-web@test одной командой (worktree, issue, PR); SYNC_ARGS="--check" или "--dry-run" +sync-web: + scripts/sync-web.sh $(SYNC_ARGS) + +check: validate-contracts validate-openspec test-snapshot test-sync-web # Диаграммы model/diagrams/part01…09.{svg,png} из $(DBML); нужен npx (Node.js), зависимостей Python нет diagrams: diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md index 928f371..eff26e7 100644 --- a/dev/contour-transfer-guide.md +++ b/dev/contour-transfer-guide.md @@ -64,6 +64,13 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour Что делает скрипт (репозиторий-источник не меняется): +0. Только arm-core: проверяет, не отстаёт ли встроенный фронт `web/` от `RSHB/arm-web@test` (`sync-web.sh --check`, + раздел 3.1). Если отстаёт — спрашивает «перенести как есть? [y/N]»: `y` — снимок делается, отставание записывается в + `SNAPSHOT.md`; `n` (или пустой ответ) — запускается синхронизация (worktree, issue, PR в arm-core) и перенос + останавливается с кодом 3 до merge PR и закрытия issue («закрывай»), затем снимок повторяется. Без терминала вопрос не + задаётся: нужен флаг `--web-as-is` первым аргументом, иначе код 3. Если `web/` правили напрямую в arm-core мимо arm-web + (точки синхронизации нет) — снимок не делается, разбирать вручную. Если `arm-web` рядом не найден — предупреждение, + снимок делается. 1. `git archive ` → `/tmp/contour//` — дерево коммита без `.git` и без игнорируемых файлов. 2. Удаляет `.claude/`, `.codegraph/`, `CLAUDE.md`, `openspec/config.yaml`, `docs/superpowers/` на любой глубине (в т.ч. `web/` arm-core; опустевший `docs/` тоже), `Dockerfile` и `.dockerignore` — в корне. @@ -76,7 +83,33 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour `git.aepif.ru`, на месте платформенные файлы (`raisa_config.yml`, `setup/`, `run_server.py`, `requirements.txt`, `.env.test`, `contracts/`). Любая проблема — код возврата 1 и список. -Тест скрипта — `make test-snapshot` (синтетический репозиторий с артефактами агента в корне и в `web/`). +Тест скрипта — `make test-snapshot` (синтетический репозиторий с артефактами агента в корне и в `web/`); проверка +отставания `web/` и остановка переноса — в `make test-sync-web`. + +### 3.1. Синхронизация `arm-core/web` с `arm-web`: `scripts/sync-web.sh` + +Фронт ведётся в двух местах: отдельный репозиторий `RSHB/arm-web` (ветка `test`, прототип и демо-стенд) и встроенный +`arm-core/web`, из которого собирается контур. Правила: `arm-core/web` напрямую не редактируется — все правки фронта идут в +arm-web и переносятся скриптом; состояние синхронизации не хранится — точкой считается коммит arm-web, у которого дерево +`src` совпадает с `web/src` в arm-core main (сравнение по хэшу дерева git). + +```bash +make -C docs sync-web SYNC_ARGS=--check # 0 — синхронно; 10 — arm-core/web отстаёт (список коммитов); 20 — расхождение +make -C docs sync-web SYNC_ARGS=--dry-run # worktree .wt/web-sync-/arm-core с перенесённым набором, без issue/PR +make -C docs sync-web # полный цикл: worktree, issue и PR в RSHB/arm-core, проверки, коммит, push +``` + +Полный цикл делает то же, что делалось вручную в arm-core #94 и #100: worktree по абсолютному пути на ветке +`feat/-web-sync-` с `.bounded`; перенос набора синхронизации — всё дерево arm-web, кроме служебного +(`CLAUDE.md`, `.claude/`, `openspec/`, `docs/`, `scripts/`, `Makefile`, `ARCHITECTURE.md`, `.gitignore`, `package-lock.json`; +`README.md` входит в набор — решение владельца 10.10.2026); контроль `diff -rq` с деревом arm-web; проверки `vitest`, +`tsc --noEmit`, `biome check src` (предупреждений не больше, чем на main), `rsbuild build`; коммит и PR с диапазоном +коммитов arm-web и рулингами R-1/R-2. Любая красная проверка останавливает скрипт до коммита, worktree остаётся для разбора. + +`node_modules` для проверок берётся из `WEB_NODE_MODULES` (по умолчанию `.wt/web-proto/arm-web/node_modules` — стенд демо), +иначе `npm install` в worktree. Скрипт ничего не мержит и не удаляет: ревью перенесённого кода против бэкенда, merge и +чеклист «закрывай» — за исполнителем. Код 20 (расхождение) означает прямые правки `web/` в arm-core: их нужно перенести в +arm-web или откатить, иначе автоперенос их затёр бы. Проверено 06.10.2026: снимки arm-core@1d7f2c8, tabular-parser@3b32462, scan-gateway@dffc18a проходят `make check` без `docs` и `fixtures` (1084 / 1087 / 726 passed, пропуски только по причинам из раздела 5). @@ -204,7 +237,9 @@ git diff <коммит sync> HEAD -- . ':!SNAPSHOT.md' > /tmp/contour-arm-core.p 1. Снаружи: нужные PR слиты в `main` трёх сервисов; `make check-full` зелёный в каждом; контракт в `docs/contracts` и копии `api/contracts` идентичны. 2. Правки из контура (раздел 7) возвращены наружу и слиты. -3. `prepare-contour-snapshot.sh` по каждому сервису; скрипт завершился словами «снимок готов». +3. `prepare-contour-snapshot.sh` по каждому сервису; скрипт завершился словами «снимок готов». Для arm-core скрипт сам + проверяет отставание `web/` от arm-web (раздел 3.1): при остановке с кодом 3 — дождаться merge PR синхронизации и + «закрывай», затем повторить. 4. В контуре: клон, ветка, `rsync --delete`, просмотр `git status`, коммит `sync: @ (<дата>)`, `make venv`, `make check` (пропуски — только из раздела 5), для arm-core `make web-check`, merge request по регламенту GitLab контура. 5. Порядок мержа при смене версии контракта: arm-core → tabular-parser → scan-gateway. diff --git a/scripts/prepare-contour-snapshot.sh b/scripts/prepare-contour-snapshot.sh index 3549ff4..efd9d1a 100755 --- a/scripts/prepare-contour-snapshot.sh +++ b/scripts/prepare-contour-snapshot.sh @@ -6,13 +6,22 @@ # Nexus (как `setup/setup.sh` шаблона), убирает цель docker-build, заменяет ссылки на внешний Gitea, пишет SNAPSHOT.md с # провенансом и проверяет результат. Репозиторий-источник не меняется. # -# Запуск из workspace: docs/scripts/prepare-contour-snapshot.sh <каталог репозитория> <каталог вывода> [ref] +# Запуск из workspace: docs/scripts/prepare-contour-snapshot.sh [--web-as-is] <каталог репозитория> <каталог вывода> [ref] # ref по умолчанию — origin/main (снимок всегда с сервера, не с рабочего дерева). # Пример: docs/scripts/prepare-contour-snapshot.sh tabular-parser /tmp/contour # Результат: <каталог вывода>/<имя репозитория>/ — готовое дерево для rsync в клон репозитория контура. +# +# Для arm-core перед снимком проверяется, не отстаёт ли встроенный фронт web/ от RSHB/arm-web (sync-web.sh --check). +# Если отстаёт — вопрос «перенести как есть?»: «y» — снимок с отставанием (оно записывается в SNAPSHOT.md); «n» — +# запускается синхронизация (sync-web.sh: worktree, issue, PR) и перенос останавливается (код 3) до merge PR и закрытия +# issue. Без терминала вопрос не задаётся: нужен флаг --web-as-is, иначе код 3. Расхождение (прямые правки web/ в arm-core) — +# всегда стоп. arm-web ищется рядом с репозиторием (../arm-web) или в $ARM_WEB; если его нет — предупреждение, снимок делается. +# $SYNC_WEB_ARGS — дополнительные аргументы sync-web.sh (для теста: --dry-run --wt-root …). set -euo pipefail -usage() { echo "usage: $0 [ref]" >&2; exit 2; } +usage() { echo "usage: $0 [--web-as-is] [ref]" >&2; exit 2; } +WEB_AS_IS=0 +if [ "${1:-}" = "--web-as-is" ]; then WEB_AS_IS=1; shift; fi [ $# -ge 2 ] || usage REPO=$(cd "$1" && pwd) @@ -31,6 +40,42 @@ if [ -e "$DEST" ]; then exit 2 fi +# --- 0. arm-core: встроенный фронт web/ не должен отставать от arm-web ----------------------------------------------- +WEB_LAG="" +if [ "$NAME" = arm-core ]; then + SYNC_WEB="$(cd "$(dirname "$0")" && pwd)/sync-web.sh" + ARM_WEB_DIR=${ARM_WEB:-$(dirname "$REPO")/arm-web} + # shellcheck disable=SC2206 + SYNC_ARGS=(--arm-core "$REPO" --arm-web "$ARM_WEB_DIR" --core-ref "$REF" ${SYNC_WEB_ARGS:-}) + if [ ! -e "$ARM_WEB_DIR/.git" ]; then + echo "предупреждение: arm-web не найден ($ARM_WEB_DIR) — отставание web/ от arm-web не проверено" >&2 + else + rc=0; "$SYNC_WEB" --check "${SYNC_ARGS[@]}" || rc=$? + case "$rc" in + 0) ;; + 10) + if [ "$WEB_AS_IS" = 1 ]; then + answer=y + elif [ -t 0 ] || [ -p /dev/stdin ] || [ -f /dev/stdin ]; then + read -r -p "web/ в arm-core отстаёт от arm-web. Перенести в контур как есть? [y/N] " answer || answer=n + else + answer=n + echo "нет терминала и нет --web-as-is: перенос как есть не подтверждён" >&2 + fi + case "$answer" in + y|Y|д|Д) WEB_LAG="отстаёт от arm-web (перенесён как есть по решению оператора, см. sync-web.sh --check)" ;; + *) + echo "запускается синхронизация arm-core/web с arm-web (sync-web.sh)…" >&2 + "$SYNC_WEB" "${SYNC_ARGS[@]}" || true + echo "перенос в контур остановлен до полного завершения синхронизации: merge PR и закрытие issue (чеклист «закрывай»), затем повторите снимок." >&2 + exit 3 ;; + esac ;; + 20) echo "ошибка: web/ в arm-core расходится с arm-web (прямые правки) — снимок не делается, разберитесь вручную" >&2; exit 1 ;; + *) echo "ошибка: sync-web.sh --check завершился с кодом $rc" >&2; exit 1 ;; + esac + fi +fi + git -C "$REPO" fetch -q origin HASH=$(git -C "$REPO" rev-parse "$REF") DATE=$(git -C "$REPO" show -s --format=%cs "$REF") @@ -144,6 +189,9 @@ cat > "$DEST/SNAPSHOT.md" <> "$DEST/SNAPSHOT.md" +fi # --- 5. Проверки ---------------------------------------------------------------------------------------------------- fail=0 diff --git a/scripts/sync-web.sh b/scripts/sync-web.sh new file mode 100755 index 0000000..0a0652d --- /dev/null +++ b/scripts/sync-web.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# Синхронизация встроенного фронта arm-core/web с отдельным репозиторием фронта RSHB/arm-web (ветка test). +# Делает одной командой то, что делалось вручную в arm-core #94 и #100: worktree, issue, перенос набора файлов, +# проверки фронта, коммит, PR. Гайд: dev/contour-transfer-guide.md §3.1. +# +# Точка синхронизации не хранится: это коммит arm-web@, у которого хэш дерева `src` равен хэшу `web/src` +# в arm-core@. Если такого коммита нет — arm-core/web правили напрямую, мимо arm-web; перенос затёр бы +# эту работу, поэтому скрипт останавливается (код 20). +# +# Набор синхронизации — всё дерево arm-web, кроме служебного (контекст агента, спеки UI, Makefile и скрипты +# демо-стенда): CLAUDE.md, .claude/, openspec/, docs/, scripts/, Makefile, ARCHITECTURE.md, .gitignore, package-lock.json. +# +# Запуск из workspace: docs/scripts/sync-web.sh [--check] [--dry-run] [--ref ] [--core-ref ] +# [--arm-web ] [--arm-core ] [--wt-root ] +# --check только сравнить: 0 — синхронно, 10 — arm-core/web отстаёт, 20 — расхождение (правки напрямую) +# --dry-run worktree, перенос и контроль идентичности без issue, PR, push и проверок npm (посмотреть, что перенесётся) +# --ref ветка arm-web, по умолчанию origin/test; --core-ref — база arm-core, по умолчанию origin/main +# WEB_NODE_MODULES — откуда взять node_modules для проверок (по умолчанию /web-proto/arm-web/node_modules); +# если каталога нет — npm install в worktree +# Скрипт пишет только в новый worktree arm-core на ветке фичи; ничего не мержит, не удаляет и не закрывает — +# закрытие после merge идёт обычным чеклистом «закрывай». +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# Корень workspace — где рядом лежат arm-core и arm-web. Скрипт запускают и из docs/ (корень = ../..), и из worktree +# .wt/<задача>/docs (корень = ../../../..); внешний кандидат проверяется первым — там основные checkout'ы. +WS="$(cd "$HERE/../.." && pwd)" +for cand in "$HERE/../../../.." "$HERE/../.."; do + if [ -e "$cand/arm-core/.git" ] && [ -e "$cand/arm-web/.git" ]; then WS="$(cd "$cand" && pwd)"; break; fi +done +ARM_WEB="$WS/arm-web" +ARM_CORE="$WS/arm-core" +WT_ROOT="$WS/.wt" +REF=origin/test +CORE_REF=origin/main +MODE=sync +CHECK=0 + +while [ $# -gt 0 ]; do + case "$1" in + --check) CHECK=1 ;; # имеет приоритет над --dry-run независимо от порядка (пайплайн добавляет $SYNC_WEB_ARGS в конец) + --dry-run) MODE=dry ;; + --ref) REF=$2; shift ;; + --core-ref) CORE_REF=$2; shift ;; + --arm-web) ARM_WEB=$2; shift ;; + --arm-core) ARM_CORE=$2; shift ;; + --wt-root) WT_ROOT=$2; shift ;; + -h|--help) sed -n '2,24p' "$0"; exit 0 ;; + *) echo "sync-web: неизвестный аргумент $1" >&2; exit 2 ;; + esac + shift +done +[ "$CHECK" = 0 ] || MODE=check + +# Исключения набора синхронизации: pathspec для git diff и имена для diff -rq +EXCLUDE_NAMES=(CLAUDE.md .claude openspec docs scripts Makefile ARCHITECTURE.md .gitignore package-lock.json) +PATHSPEC=(.) +DIFF_EXCLUDES=(--exclude=node_modules --exclude=dist --exclude=.codegraph --exclude=.superpowers --exclude=.bounded) +for n in "${EXCLUDE_NAMES[@]}"; do PATHSPEC+=(":(exclude,top)$n"); DIFF_EXCLUDES+=("--exclude=$n"); done + +die() { echo "sync-web: $1" >&2; exit "${2:-1}"; } # die <сообщение> [код] +[ -d "$ARM_WEB/.git" ] || [ -f "$ARM_WEB/.git" ] || die "arm-web не найден: $ARM_WEB (--arm-web )" 2 +[ -d "$ARM_CORE/.git" ] || [ -f "$ARM_CORE/.git" ] || die "arm-core не найден: $ARM_CORE (--arm-core )" 2 +ARM_WEB=$(cd "$ARM_WEB" && pwd); ARM_CORE=$(cd "$ARM_CORE" && pwd) + +git -C "$ARM_WEB" fetch -q origin +git -C "$ARM_CORE" fetch -q origin +git -C "$ARM_WEB" rev-parse -q --verify "$REF^{commit}" >/dev/null || die "в arm-web нет $REF" 2 +git -C "$ARM_CORE" rev-parse -q --verify "$CORE_REF^{commit}" >/dev/null || die "в arm-core нет $CORE_REF" 2 + +# --- 1. Точка синхронизации ------------------------------------------------------------------------------------------- +CORE_SRC=$(git -C "$ARM_CORE" rev-parse -q --verify "$CORE_REF:web/src") || die "в arm-core@$CORE_REF нет web/src" 2 +HEAD_SHA=$(git -C "$ARM_WEB" rev-parse "$REF") +HEAD7=${HEAD_SHA:0:7} +POINT="" +while IFS= read -r c; do + t=$(git -C "$ARM_WEB" rev-parse -q --verify "$c:src" 2>/dev/null || true) + if [ "$t" = "$CORE_SRC" ]; then POINT=$c; break; fi +done < <(git -C "$ARM_WEB" rev-list "$REF") + +if [ -z "$POINT" ]; then + echo "sync-web: расхождение — ни у одного коммита arm-web@$REF дерево src не совпадает с arm-core@$CORE_REF:web/src." >&2 + echo "arm-core/web правили напрямую, мимо arm-web; автоперенос затёр бы эти правки. Коммиты arm-core по web/:" >&2 + git -C "$ARM_CORE" log --format=' %h %s' -10 "$CORE_REF" -- web/ >&2 + echo "Разберитесь вручную: перенесите прямые правки в arm-web (или откатите их), затем повторите." >&2 + exit 20 +fi + +if [ "$POINT" = "$HEAD_SHA" ]; then + echo "sync-web: arm-core@$CORE_REF web/ синхронен с arm-web@$REF ($HEAD7) — переносить нечего" + exit 0 +fi + +COMMITS=$(git -C "$ARM_WEB" log --format='%h %s' --no-merges "$POINT..$REF") +N_COMMITS=$(printf '%s\n' "$COMMITS" | grep -c . || true) +FILES=$(git -C "$ARM_WEB" diff --name-only "$POINT" "$REF" -- "${PATHSPEC[@]}") +N_FILES=$(printf '%s\n' "$FILES" | grep -c . || true) + +echo "sync-web: arm-core@$CORE_REF web/ отстаёт от arm-web@$REF ($HEAD7)." +echo "точка синхронизации: arm-web ${POINT:0:7}; непереносённых коммитов: $N_COMMITS, файлов в наборе: $N_FILES" +printf '%s\n' "$COMMITS" | sed 's/^/ /' +[ "$MODE" != check ] || exit 10 + +# --- 2. Worktree ----------------------------------------------------------------------------------------------------- +TMP_BRANCH="tmp/web-sync-$HEAD7" +WT="$WT_ROOT/web-sync-$HEAD7/arm-core" +[ ! -e "$WT" ] || die "worktree уже существует: $WT — завершите или удалите предыдущую синхронизацию" 2 +git -C "$ARM_CORE" rev-parse -q --verify "$TMP_BRANCH" >/dev/null && die "ветка $TMP_BRANCH уже есть в arm-core" 2 +mkdir -p "$(dirname "$WT")" +git -C "$ARM_CORE" worktree add -q -b "$TMP_BRANCH" "$WT" "$CORE_REF" +touch "$WT/.bounded" +echo "worktree: $WT ($TMP_BRANCH от $CORE_REF)" + +# --- 3. Issue и ветка (не в dry-run) --------------------------------------------------------------------------------- +TEA=(tea --login home --repo RSHB/arm-core) +ISSUE="" +BRANCH="$TMP_BRANCH" +if [ "$MODE" = sync ]; then + TITLE="web: синхронизация встроенного фронта с arm-web@${REF#origin/} ($HEAD7)" + BODY="Встроенный фронт \`arm-core/web\` отстаёт от \`RSHB/arm-web@${REF#origin/}\` ($HEAD7); точка синхронизации — arm-web ${POINT:0:7}. +Непереносённые коммиты arm-web ($N_COMMITS): +$(printf '%s\n' "$COMMITS" | sed 's/^/- /') + +Bounded-задача, выполнена \`docs/scripts/sync-web.sh\`: перенос набора синхронизации ($N_FILES файлов) в \`web/\`, +проверки vitest / tsc / biome / build, PR в main. Ревью перенесённого кода против бэкенда — за исполнителем." + ISSUE_URL=$("${TEA[@]}" issues create --title "$TITLE" --description "$BODY" | grep -o 'https://[^ ]*/issues/[0-9]*' | tail -1) + ISSUE=${ISSUE_URL##*/} + [ -n "$ISSUE" ] || die "не удалось создать issue через tea" + BRANCH="feat/$ISSUE-web-sync-$HEAD7" + git -C "$WT" branch -m "$TMP_BRANCH" "$BRANCH" + echo "issue: $ISSUE_URL; ветка: $BRANCH" +fi + +# --- 4. Перенос набора и контроль идентичности ------------------------------------------------------------------------ +git -C "$ARM_WEB" diff --binary "$POINT" "$REF" -- "${PATHSPEC[@]}" | git -C "$WT" apply --directory=web +REF_TREE=$(mktemp -d); trap 'rm -rf "$REF_TREE"' EXIT +git -C "$ARM_WEB" archive "$REF" | tar -x -C "$REF_TREE" +if ! diff -rq "${DIFF_EXCLUDES[@]}" "$REF_TREE" "$WT/web" >/dev/null; then + echo "sync-web: после переноса web/ не совпадает с arm-web@$REF:" >&2 + diff -rq "${DIFF_EXCLUDES[@]}" "$REF_TREE" "$WT/web" >&2 || true + exit 1 +fi +echo "перенесено: $N_FILES файлов; web/ идентичен arm-web@$REF по набору синхронизации" +if [ "$MODE" = dry ]; then + echo "dry-run: issue, проверки, коммит и PR не выполнялись. Посмотреть: git -C $WT status; убрать:" + echo " git -C $ARM_CORE worktree remove --force $WT && git -C $ARM_CORE branch -D $TMP_BRANCH" + exit 0 +fi + +# --- 5. Проверки фронта --------------------------------------------------------------------------------------------- +NM=${WEB_NODE_MODULES:-$WT_ROOT/web-proto/arm-web/node_modules} +if [ -d "$NM" ]; then + ln -s "$NM" "$WT/web/node_modules" + echo "node_modules: симлинк на $NM" +else + echo "node_modules: $NM нет — npm install в worktree" + (cd "$WT/web" && npm install --no-audit --no-fund >/dev/null) +fi +run_check() { # имя, команда... — печатает хвост вывода при ошибке и останавливает скрипт + local name=$1; shift + local log; log=$(mktemp) + if (cd "$WT/web" && "$@") >"$log" 2>&1; then rm -f "$log"; return 0; fi + echo "sync-web: проверка «$name» не прошла; worktree оставлен для разбора: $WT" >&2 + tail -40 "$log" >&2; rm -f "$log"; exit 1 +} +run_check vitest npx vitest run +TESTS=$(cd "$WT/web" && npx vitest run 2>&1 | grep -E '^\s*Tests\s' | sed 's/^ *//' || true) +run_check tsc npx tsc --noEmit +biome_warnings() { (cd "$WT/web" && npx biome check "$1" 2>&1 | grep -oE 'Found [0-9]+ warning' | grep -oE '[0-9]+') || echo 0; } +W_MAIN=$(biome_warnings "$ARM_CORE/web/src") +W_NEW=$(biome_warnings src) +[ "$W_NEW" -le "$W_MAIN" ] || { echo "sync-web: biome — предупреждений стало больше: $W_NEW против $W_MAIN на $CORE_REF; worktree: $WT" >&2; exit 1; } +run_check build npm run build +echo "проверки: $TESTS; tsc чисто; biome $W_NEW предупреждений (на $CORE_REF — $W_MAIN); build ок" + +# --- 6. Коммит, push, PR ------------------------------------------------------------------------------------------- +git -C "$WT" add -A web -- ':!web/node_modules' ':!web/dist' +git -C "$WT" -c commit.gpgsign=false commit -q -F - < +EOF +COMMIT=$(git -C "$WT" rev-parse --short=7 HEAD) +git -C "$WT" push -q -u origin "$BRANCH" +PR_BODY="Закрывает #$ISSUE. + +Встроенный фронт \`arm-core/web\` отставал от \`RSHB/arm-web@${REF#origin/}\` ($HEAD7). Диапазон arm-web ${POINT:0:7}..$HEAD7 +перенесён скриптом \`docs/scripts/sync-web.sh\` без правок; после переноса \`web/\` и arm-web идентичны по набору синхронизации. + +Коммиты arm-web ($N_COMMITS): +$(printf '%s\n' "$COMMITS" | sed 's/^/- /') + +Файлы ($N_FILES): +$(printf '%s\n' "$FILES" | sed 's/^/- web\//') + +Проверки в worktree: $TESTS; \`tsc --noEmit\` чисто; \`biome check src\` — $W_NEW предупреждений (на ${CORE_REF#origin/} — $W_MAIN); \`rsbuild build\` собирается. + +## Rulings I made +- R-1 — служебные файлы arm-web (CLAUDE.md, .claude/, openspec/, docs/, scripts/, Makefile, ARCHITECTURE.md, .gitignore, lockfile) не переносятся: UI-спеки и демо-стенд живут в arm-web. +- R-2 — bounded-задача без OpenSpec change: перенос готового, проревьюенного в arm-web кода. + +Ревью перенесённого кода против бэкенда — за исполнителем, находки с маркерами в комментариях к PR. + +🤖 Generated with [Claude Code](https://claude.com/claude-code)" +PR_URL=$("${TEA[@]}" pulls create --head "$BRANCH" --base "${CORE_REF#origin/}" --title "fix(web): синхронизация встроенного фронта с arm-web@${REF#origin/} ($HEAD7) (#$ISSUE)" \ + --description "$PR_BODY" | grep -o 'https://[^ ]*/pulls/[0-9]*' | tail -1) + +echo +echo "готово: коммит $COMMIT, ветка $BRANCH" +echo "issue: $ISSUE_URL" +echo "PR: ${PR_URL:-создать вручную: tea pulls create --head $BRANCH}" +echo "исполнителю: проверить перенесённый код против бэкенда (коммиты arm-web выше), затем — merge и «закрывай»." +echo "worktree $WT, симлинк web/node_modules и .bounded остаются до закрытия." diff --git a/scripts/test_sync_web.sh b/scripts/test_sync_web.sh new file mode 100755 index 0000000..6095934 --- /dev/null +++ b/scripts/test_sync_web.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# Тест sync-web.sh на синтетических репозиториях arm-web и arm-core (bare origin + клон), без tea, npm и сети. +# Сценарии: синхронно; arm-core отстаёт (--check и --dry-run); arm-core правили напрямую (отказ); +# пайплайн переноса в контур (prepare-contour-snapshot.sh спрашивает / останавливается / идёт «как есть»). +# Запуск: make test-sync-web (или напрямую из корня docs). +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +SYNC="$HERE/sync-web.sh" +SNAPSHOT="$HERE/prepare-contour-snapshot.sh" +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t + +fail=0 +check() { if [ "$1" = 0 ]; then echo "ok: $2"; else echo "FAIL: $2" >&2; fail=1; fi; } + +put() { mkdir -p "$(dirname "$1")"; printf '%s\n' "${2:-x}" > "$1"; } +commit_push() { git -C "$1" add -A; git -C "$1" commit -qm "$2"; git -C "$1" push -q origin "HEAD:$3" 2>/dev/null; git -C "$1" fetch -q origin; } + +# --- синтетический arm-web: ветка test --------------------------------------------------------------------------- +WEB="$WORK/arm-web" +git init -q --bare "$WORK/arm-web.git" +git clone -q "$WORK/arm-web.git" "$WEB" 2>/dev/null +git -C "$WEB" checkout -q -b test +put "$WEB/src/a.ts" "export const a = 1;" +put "$WEB/README.md" "arm-web v1" +put "$WEB/package.json" '{"name":"web"}' +for p in CLAUDE.md .claude/skills/s/SKILL.md openspec/config.yaml openspec/specs/ui/spec.md docs/superpowers/plans/p.md \ + scripts/demo-prepare.sh Makefile ARCHITECTURE.md .gitignore; do put "$WEB/$p" "/Users/someone/arm-web"; done +commit_push "$WEB" "web v1" test +POINT=$(git -C "$WEB" rev-parse HEAD) + +# --- синтетический arm-core: web/ = снимок arm-web v1 без служебных файлов, плюс платформенные файлы ---------------- +CORE="$WORK/arm-core" +git init -q --bare "$WORK/arm-core.git" +git clone -q "$WORK/arm-core.git" "$CORE" 2>/dev/null +git -C "$CORE" checkout -q -b main +put "$CORE/web/src/a.ts" "export const a = 1;" +put "$CORE/web/README.md" "arm-web v1" +put "$CORE/web/package.json" '{"name":"web"}' +printf 'venv:\n\tuv venv api/.venv\n' > "$CORE/Makefile" +for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/requirements.txt api/.env.test \ + api/contracts/schema.json; do put "$CORE/$p"; done +commit_push "$CORE" "core v1" main + +COMMON=(--arm-web "$WEB" --arm-core "$CORE" --wt-root "$WORK/wt") + +# --- 1. синхронно --------------------------------------------------------------------------------------------------- +rc=0; "$SYNC" --check "${COMMON[@]}" >/dev/null || rc=$? +check "$([ "$rc" = 0 ] && echo 0 || echo 1)" "синхронно: --check возвращает 0 (rc=$rc)" +rc=0; out=$("$SYNC" --dry-run "${COMMON[@]}") || rc=$? +check "$([ "$rc" = 0 ] && [ ! -d "$WORK/wt" ] && echo 0 || echo 1)" "синхронно: --dry-run ничего не делает (rc=$rc)" + +# --- arm-web уходит вперёд: правка src, новый файл, README; служебные файлы тоже меняются --------------------------- +put "$WEB/src/a.ts" "export const a = 2;" +put "$WEB/src/b.ts" "export const b = 1;" +put "$WEB/README.md" "arm-web v2" +put "$WEB/CLAUDE.md" "/Users/someone/arm-web v2" +put "$WEB/scripts/demo-prepare.sh" "v2" +commit_push "$WEB" "web v2" test +HEAD7=$(git -C "$WEB" rev-parse --short=7 HEAD) + +# --- 2. отстаёт: --check ---------------------------------------------------------------------------------------------- +rc=0; out=$("$SYNC" --check "${COMMON[@]}" 2>&1) || rc=$? +check "$([ "$rc" = 10 ] && echo 0 || echo 1)" "отстаёт: --check возвращает 10 (rc=$rc)" +check "$(grep -q "web v2" <<<"$out" && echo 0 || echo 1)" "отстаёт: --check перечисляет непереносённые коммиты" +check "$(grep -q "${POINT:0:7}" <<<"$out" && echo 0 || echo 1)" "отстаёт: --check называет точку синхронизации" + +# --- 3. отстаёт: --dry-run переносит набор в worktree ------------------------------------------------------------------- +rc=0; "$SYNC" --dry-run "${COMMON[@]}" >/dev/null || rc=$? +WT="$WORK/wt/web-sync-$HEAD7/arm-core" +check "$([ "$rc" = 0 ] && [ -d "$WT" ] && echo 0 || echo 1)" "dry-run: worktree $WT создан (rc=$rc)" +check "$([ "$(git -C "$WT" rev-parse --abbrev-ref HEAD)" = "tmp/web-sync-$HEAD7" ] && echo 0 || echo 1)" "dry-run: временная ветка tmp/web-sync-$HEAD7" +check "$([ -f "$WT/.bounded" ] && echo 0 || echo 1)" "dry-run: .bounded создан" +check "$([ "$(cat "$WT/web/src/a.ts")" = "export const a = 2;" ] && echo 0 || echo 1)" "dry-run: изменённый src/a.ts перенесён" +check "$([ -f "$WT/web/src/b.ts" ] && echo 0 || echo 1)" "dry-run: новый src/b.ts перенесён" +check "$([ "$(cat "$WT/web/README.md")" = "arm-web v2" ] && echo 0 || echo 1)" "dry-run: README перенесён" +check "$([ ! -e "$WT/web/CLAUDE.md" ] && [ ! -e "$WT/web/scripts" ] && echo 0 || echo 1)" "dry-run: служебные файлы arm-web не перенесены" +git -C "$WT" add -A web >/dev/null +TREE_WT=$(git -C "$WT" write-tree) +check "$([ "$(git -C "$WT" rev-parse "$TREE_WT:web/src")" = "$(git -C "$WEB" rev-parse test:src)" ] && echo 0 || echo 1)" "dry-run: дерево web/src совпадает с arm-web test:src" +git -C "$WT" reset -q +rc=0; "$SYNC" --dry-run "${COMMON[@]}" >/dev/null 2>&1 || rc=$? +check "$([ "$rc" != 0 ] && echo 0 || echo 1)" "dry-run повторно: отказ, worktree уже существует (rc=$rc)" +git -C "$CORE" worktree remove --force "$WT"; git -C "$CORE" branch -q -D "tmp/web-sync-$HEAD7" + +# --- 4. пайплайн переноса: prepare-contour-snapshot.sh видит отставание --------------------------------------------- +export ARM_WEB="$WEB" SYNC_WEB_ARGS="--dry-run --wt-root $WORK/wt" +rc=0; out=$("$SNAPSHOT" "$CORE" "$WORK/out1" origin/main &1) || rc=$? +check "$([ "$rc" = 3 ] && echo 0 || echo 1)" "пайплайн без терминала: перенос остановлен, rc=3 (rc=$rc)" +check "$(grep -qi "остановлен" <<<"$out" && echo 0 || echo 1)" "пайплайн: сообщение «перенос остановлен до завершения синхронизации»" +check "$([ -d "$WT" ] && [ -f "$WT/web/src/b.ts" ] && echo 0 || echo 1)" "пайплайн: синхронизация запущена, worktree создан" +check "$([ ! -d "$WORK/out1/arm-core" ] && echo 0 || echo 1)" "пайплайн: снимок не создан" +git -C "$CORE" worktree remove --force "$WT"; git -C "$CORE" branch -q -D "tmp/web-sync-$HEAD7" + +rc=0; out=$(printf 'y\n' | "$SNAPSHOT" "$CORE" "$WORK/out2" origin/main 2>&1) || rc=$? +check "$([ "$rc" = 0 ] && [ -f "$WORK/out2/arm-core/web/src/a.ts" ] && [ ! -e "$WORK/out2/arm-core/web/src/b.ts" ] && echo 0 || echo 1)" \ + "пайплайн, ответ «y»: снимок как есть (rc=$rc)" +rc=0; out=$(printf 'n\n' | "$SNAPSHOT" "$CORE" "$WORK/out3" origin/main 2>&1) || rc=$? +check "$([ "$rc" = 3 ] && [ -d "$WT" ] && echo 0 || echo 1)" "пайплайн, ответ «n»: синхронизация запущена, перенос остановлен (rc=$rc)" +git -C "$CORE" worktree remove --force "$WT"; git -C "$CORE" branch -q -D "tmp/web-sync-$HEAD7" + +rc=0; out=$("$SNAPSHOT" --web-as-is "$CORE" "$WORK/out4" origin/main &1) || rc=$? +check "$([ "$rc" = 0 ] && [ -f "$WORK/out4/arm-core/SNAPSHOT.md" ] && echo 0 || echo 1)" "пайплайн, --web-as-is: снимок без вопроса (rc=$rc)" +check "$(grep -q "отстаёт" "$WORK/out4/arm-core/SNAPSHOT.md" && echo 0 || echo 1)" "пайплайн, --web-as-is: отставание записано в SNAPSHOT.md" +unset SYNC_WEB_ARGS + +# arm-web рядом нет (в тесте он есть как сосед $CORE, поэтому путь задаётся явно несуществующим) +export ARM_WEB="$WORK/no-such-arm-web" +rc=0; out=$("$SNAPSHOT" "$CORE" "$WORK/out5" origin/main &1) || rc=$? +check "$([ "$rc" = 0 ] && grep -q "arm-web не найден" <<<"$out" && echo 0 || echo 1)" "пайплайн без arm-web рядом: предупреждение, снимок сделан (rc=$rc)" +unset ARM_WEB + +# --- 5. arm-core правили напрямую: точки синхронизации нет ---------------------------------------------------------- +put "$CORE/web/src/a.ts" "export const a = 'direct';" +commit_push "$CORE" "core: прямая правка web" main +rc=0; out=$("$SYNC" --check "${COMMON[@]}" 2>&1) || rc=$? +check "$([ "$rc" = 20 ] && echo 0 || echo 1)" "прямая правка arm-core: --check возвращает 20 (rc=$rc)" +check "$(grep -q "прямая правка web" <<<"$out" && echo 0 || echo 1)" "прямая правка arm-core: названы коммиты arm-core по web/" +export ARM_WEB="$WEB" +rc=0; out=$("$SNAPSHOT" "$CORE" "$WORK/out6" origin/main &1) || rc=$? +check "$([ "$rc" != 0 ] && [ ! -d "$WORK/out6/arm-core" ] && echo 0 || echo 1)" "пайплайн при расхождении: снимок не делается (rc=$rc)" +unset ARM_WEB + +[ "$fail" = 0 ] || exit 1 +echo "ok: sync-web.sh — точка синхронизации, перенос набора, отказ при расхождении, пайплайн переноса"