Compare commits

..
Author SHA1 Message Date
claude[bot] cc17109249 docs: review document for #123
Issue: #123
User-Visible: no
2026-08-13 18:55:48 +00:00
Sergey Matyunin e79f8f5aa1 Fix corner split smoke geometry input
Issue: #123
User-Visible: no
2026-08-13 21:48:28 +03:00
claude[bot] 024a1accd8 docs: code review document for #123
Issue: #123
User-Visible: no
2026-08-13 18:45:40 +00:00
Sergey Matyunin 47c6f10a9d Fix corner split exterior walls
Issue: #123
User-Visible: yes
2026-08-13 21:29:34 +03:00
Sergey Matyunin 52ec0fb54f Merge dev into issue/123 branch
Issue: #123
User-Visible: no
2026-08-13 20:57:00 +03:00
claude[bot] bcd280afb9 docs: review document for #123
Issue: #123
User-Visible: no
2026-08-13 17:55:48 +00:00
Sergey Matyunin ba56d4f768 Specify corner split wall geometry
Issue: #123
User-Visible: no
2026-08-13 20:18:35 +03:00
43 changed files with 220 additions and 314 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do
echo "process-gate: $local_ref, диапазон ${base}..${local_sha}" >&2
# shellcheck disable=SC2086
if ! node "$gate" --range "${base}..${local_sha}" --target-ref "$remote_ref" $issues_flag >&2; then
if ! node "$gate" --range "${base}..${local_sha}" $issues_flag >&2; then
status=1
fi
done
+12 -88
View File
@@ -48,7 +48,6 @@ jobs:
BLOCKED: ${{ contains(github.event.issue.labels.*.name, 'blocked') }}
EXHAUSTED: ${{ contains(github.event.issue.labels.*.name, 'review-4') }}
SMALL: ${{ contains(github.event.issue.labels.*.name, 'small') }}
TRIVIAL: ${{ contains(github.event.issue.labels.*.name, 'trivial') }}
NUM: ${{ github.event.issue.number }}
run: |
# Этап определяется первым: от него зависит, какие вердикты считать.
@@ -59,9 +58,8 @@ jobs:
*) echo "метка $LABEL конвейер не запускает" ;;
esac
# Лимит циклов: 4 обычный, 2 на лёгком и коротком треке (PROCESS.md §4).
limit=4
if [ "$SMALL" = "true" ] || [ "$TRIVIAL" = "true" ]; then limit=2; fi
# Лимит циклов: 4 обычный, 2 на лёгком треке (PROCESS.md §4).
limit=4; [ "$SMALL" = "true" ] && limit=2
# Счётчик считает вердикты ТОЛЬКО своего этапа. Раньше он брал все
# подряд, и вердикт по ТЗ съедал цикл из бюджета код-ревью: на #89
@@ -136,15 +134,8 @@ jobs:
fetch-depth: 0
ref: dev
# Окружение готовит workflow, а не модель своими ходами. Раньше промпт
# велел ревьюеру самому выполнить `npm ci`: минуты уходили на установку без
# кэша, платились из бюджета 45 минут и из лимитов подписки, а ходы модели
# тратились на работу инфраструктуры. В validate.yml кэш стоит на всех
# тяжёлых job, здесь его не было.
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
with: { node-version: 22 }
# Материал ревью живёт в ветке задачи: ТЗ в docs/specs/ и код коммитятся
# в issue/<NN>-slug. Если ветка запушена — переключаемся на неё, иначе
@@ -165,24 +156,6 @@ jobs:
echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY"
fi
# Зависимости ставятся ПОСЛЕ переключения на ветку задачи: lockfile мог
# измениться именно в ней, и установка по копии из dev дала бы не то дерево.
- name: Установить зависимости
run: npm ci
# Браузер нужен не всякому ревью (см. правило выбора гейтов в промпте),
# но когда нужен — качать его заново дороже, чем держать в кэше.
- name: Кэш браузеров Playwright
id: pw
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Установить Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Review
id: review
uses: anthropics/claude-code-action@v1
@@ -232,38 +205,10 @@ jobs:
По каждому AC: либо он доказан автотестом и ты убедился, что тест
умеет падать, либо разобран по коду с явной записью «проверено
чтением, не исполнением». «Verified» без названной команды и её
результата доказательством не является. Зависимости уже установлены
workflow, Chromium тоже — `npm ci` выполнять не нужно. Проверь
трейлеры Issue и User-Visible, при User-Visible: yes — правки в оба
changelog в том же коммите.
**Объём гейтов соразмерен задаче.** Прогонять весь набор на каждой
правке — не тщательность, а потеря времени: полные наборы это
предрелизный гейт (PROCESS.md §8), а не гейт ревью.
Всегда, они дешёвые:
`npx tsc --noEmit`, `npm test`, `npm run build` со сверкой трёх
копий бандла.
По необходимости, и «необходимость» определяется diff'ом и AC:
- браузерные смоки `demo/smoke_*.mjs` — названные в AC плюс
относящиеся к тронутым поверхностям. Их 127; прогон всех уместен
только когда задача действительно задевает всё;
- `npm run golden:verify` — если diff может изменить видимый
результат: рендер, геометрия, стили, слои;
- `python -m pytest tests_backend -q` — если тронут
`custom_components/**/*.py`;
- performance-профили — если названы в AC либо тронуты
чувствительные к перфу пути.
Дисциплина «тест должен уметь падать» не отменяется, но применяется к
тем тестам, которые ты прогонял.
**В комментарии обязателен перечень: какие гейты прогнал, какие нет и
почему.** Это условие честности такого сужения: непрогнанный гейт
становится видимым решением, а не молчаливым пропуском. Раздел «чего
не проверял» в документе ревью — не формальность, а главный его
раздел на коротких задачах.
результата доказательством не является. Зависимостей в рабочей
копии нет: перед гейтами выполни `npm ci`. Проверь трейлеры Issue и
User-Visible, при User-Visible: yes — правки в оба changelog в том же
коммите.
Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь.
@@ -304,21 +249,14 @@ jobs:
BRANCH: ${{ steps.branch.outputs.name }}
NUM: ${{ github.event.issue.number }}
run: |
# Ветки задачи может не быть: у задач, размеченных до появления
# конвейера, ТЗ лежит прямо в dev. Раньше шаг в этом случае молча
# выходил с нулём, и разбор ревью терялся — оставался только вердикт
# комментарием. Это тот же тихий отказ: шаг сообщал об успехе тем, что
# ничего не сделал. Документ ложится туда же, где лежит само ТЗ.
target="${BRANCH:-dev}"
if [ -z "$BRANCH" ]; then
echo "::warning::ветки задачи нет — документ ревью ляжет в dev"
echo "ветки задачи нет — документ некуда класть"; exit 0
fi
git checkout -- . 2>/dev/null || true
git clean -fd -e docs/reviews -e node_modules >/dev/null 2>&1 || true
git add docs/reviews 2>/dev/null || true
if git diff --cached --quiet; then
echo "::warning::документ ревью не создан"
exit 0
echo "документ ревью не создан"; exit 0
fi
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
@@ -328,23 +266,9 @@ jobs:
Issue: #$NUM
User-Visible: no
EOF
# Публикация в dev идёт из детачнутого состояния поверх ветки задачи
# либо dev, поэтому push нужен с явным перебазированием при гонке:
# dev мог уйти вперёд, пока шло ревью — оно длится до 45 минут.
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
"HEAD:$target"; then
git fetch -q origin "$target"
if ! git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
rebase "origin/$target"; then
git rebase --abort || true
echo "::error::документ ревью не удалось опубликовать в $target: конфликт"
exit 0
fi
git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
"HEAD:$target"
fi
echo "документ опубликован в $target"
git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
"HEAD:$BRANCH"
echo "документ опубликован в $BRANCH"
- name: Решение по вердикту
id: decide
-1
View File
@@ -51,7 +51,6 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.sha }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
TARGET_REF: ${{ github.ref }}
# Публичный репозиторий: штатного токена хватает на чтение issue.
GH_TOKEN: ${{ github.token }}
run: |
-9
View File
@@ -41,15 +41,6 @@ of a status and `rejected` on a closed issue. Exactly one `S*` label per open
issue. [GitHub Projects (v2)](https://github.com/users/Matysh/projects/1) is a
human-facing view synchronised from the labels, not the source of truth.
Two shortcuts exist for small work. `small` — the light track: the spec lives in
the issue body and its review is a comment. `trivial` — the short track: no spec
stage at all, `S2-analysis` straight to `S5-ready`, with the AC written into the
issue body first. `trivial` requires a bug confined to one surface with no new UX
contract, no migration, no i18n, no perf or touch impact, at most three checkable
AC, **and expected behaviour already on record** — nothing left to decide. Code
review is never skipped on either track; it is what stands in for testing.
`PROCESS.md` §5 and §5.1 hold the criteria.
An issue filed by an outsider is worked exactly like one of the owner's own, once
the owner has decided to take it. The check sits **at the entrance**, not on every
step: while an issue carries no status label it is outside the process and the
+4 -59
View File
@@ -70,8 +70,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
→ S6-in-progress → S7-code-review ⟲ → S8-merged → закрыт при выпуске беты
служебные: blocked (поверх статуса) rejected (закрыт)
⟲ — возврат на правки, не более 4 циклов (§4), на лёгком и коротком треке 2
короткий трек (`trivial`, §5.1) идёт S2-analysis → S5-ready, минуя S3 и S4
⟲ — возврат на правки, не более 4 циклов (§4), на лёгком треке 2
```
Переходы `S4-spec-review` и `S7-code-review` выполняются **автоматически**: метка
@@ -295,42 +294,6 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
модуль) — метка `small` снимается, issue возвращается в `S3-spec` и получает
нормальный файл ТЗ. Это не провал, это ранняя диагностика.
### 5.1 Короткий трек (метка `trivial`)
Решение владельца 2026-08-13, issue #128. Лёгкий трек делает ТЗ дешёвым; короткий
обходится без него совсем.
**Маршрут:** `S1-new` → `S2-analysis` → `S5-ready` → `S6-in-progress` →
`S7-code-review` → `S8-merged`. Стадии `S3-spec` и `S4-spec-review` пропускаются.
`S2-analysis` остаётся: это комментарий, а не прогон CI, и именно там владелец
решает приоритет и ценность. AC пишет автор в теле issue при переводе в
`S5-ready` — до перехода, иначе ревьюеру нечего будет сверять.
**Критерии, все обязательны:**
- тип `bug`;
- правка ограничена одной поверхностью, нового UX-контракта нет;
- нет миграции конфига, новых ключей i18n, влияния на перф и touch;
- AC выражаются тремя проверяемыми утверждениями или меньше;
- **ожидаемое поведение уже зафиксировано** — в `docs/USER-GUIDE.ru.md`, в
каноническом документе подсистемы либо однозначно в самом отчёте. Решать нечего.
Если есть что решать, это `S3-spec`, и никакая экономия этого не отменяет.
Метка ставится в `S2-analysis` вместе с остальными оценками, одним комментарием,
где владелец утверждает и приоритет.
**Что не упрощается:** issue, оценка, статусы, трейлеры, changelog и **код-ревью**.
Лимит циклов код-ревью — 2, как на лёгком треке.
Если по ходу выясняется, что критерий нарушен, метка снимается и issue уходит в
`S3-spec` за нормальным ТЗ. Как и на лёгком треке, это не провал, а ранняя
диагностика.
**Чем этот трек опасен.** Он убирает единственное место, где решение проверялось
до написания кода. Признак «решать нечего» держит всю конструкцию, и его нельзя
подтверждать ощущением — только ссылкой на уже зафиксированное поведение.
---
## 6. Роли
@@ -468,17 +431,6 @@ npm run golden:verify # если менялся визуал
python -m pytest tests_backend -q # py3.13, если менялся бэкенд
```
**Объём гейтов на код-ревью соразмерен задаче** (issue #127). Всегда:
`typecheck`, `npm test`, `npm run build` со сверкой трёх копий бандла. По
необходимости, определяемой diff'ом и AC: браузерные смоки (их 127 — прогон всех
уместен только когда задача задевает всё), `golden:verify` при изменении видимого
результата, `pytest tests_backend` при правках в Python, performance-профили при
названном в AC влиянии. **Полные наборы — предрелизный гейт, а не гейт ревью.**
Условие честности такого сужения: ревьюер обязан перечислить, какие гейты прогнал,
какие нет и почему. Непрогнанный гейт становится видимым решением, а не молчаливым
пропуском.
**Гейт беты** (условие закрытия issue): CI Validate зелёный на точном SHA тега.
Часть гейтов запускается только здесь, то есть **после** пройденного код-ревью.
@@ -512,8 +464,8 @@ Project v2 остаётся человеческим представление
| `blocked` | Ждём внешнего или владельца, **поверх** статусной метки |
| `rejected` | Отклонено, issue закрыт |
Модификаторы: `small` (лёгкий трек, сложность ≤3), `trivial` (короткий трек,
§5.1), `hotfix`, `process`, `review-4`; приоритет `P1`/`P2`/`P3`; тип `bug`/`feature`/`tech-debt`.
Модификаторы: `small` (лёгкий трек, сложность ≤3), `hotfix`, `process`,
`review-4`; приоритет `P1`/`P2`/`P3`; тип `bug`/`feature`/`tech-debt`.
Тематические метки (`polish`, `infra`, `tests`, `docs`, `security`, `vacuum`)
ортогональны процессу.
@@ -608,7 +560,7 @@ Project v2 остаётся человеческим представление
{`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`}; закрытый,
недоступный или помеченный `blocked` — отказ (**fail closed**).
Три оговорки к проверке 8 выяснились при реализации.
Две оговорки к проверке 8, обе выяснились при реализации.
**`S8-merged` входит в множество**, хотя по смыслу задача уже принята. Причина
механическая: конвейер (§10.4) сливает ветку в `dev` **раньше**, чем ставит метку,
@@ -621,13 +573,6 @@ Validate стартует от этого push и успевает прочит
документ ревью: он ложится в ветку задачи, пока та в `S4-spec-review` или
`S7-code-review`, то есть заведомо вне рабочего множества.
**При продвижении в `main` не перепроверяются коммиты, уже достижимые из
prerelease-тега.** После выпуска беты их issue по §2.8 должны быть закрыты, а
stable fast-forward снова включает эти коммиты в диапазон `old-main..candidate`.
Pre-push передаёт целевую remote ref через `--target-ref`, а Validate — через
`TARGET_REF`; оба исключают только уже опубликованную prerelease-историю. Любой
post-beta коммит остаётся в проверке и по закрытому issue отклоняется fail-closed.
Не реализовано и остаётся долгом:
9. `npm run release:prerelease -- --issues=…` не проверяет, есть ли у issue
+1 -1
View File
@@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600
SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.63.0"
VERSION = "1.63.0-beta.1"
# Portable backup format. This is deliberately independent from the Home
# Assistant Store version above: storage migrations and files exported by a
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.63.0"
"version": "1.63.0-beta.1"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

+22 -25
View File
@@ -1,32 +1,29 @@
{
"schema": 1,
"matrixVersion": 18,
"acceptedAt": "2026-08-13T19:26:19.387Z",
"sourceFingerprint": "27fda3d75e4cda95b9d85a9481f15ccd44b96d8c9d3311e0476718d36e2588c5",
"matrixVersion": 17,
"acceptedAt": "2026-08-13T14:30:05.929Z",
"sourceFingerprint": "66f31850eafc963848acdc4e56e363a364bbb9b189bf4ade73d3c111ae2fc375",
"chromium": "151.0.7922.34",
"scenarios": {
"split-corner-wall-before-dark": "3176dc67f54d5309f87c94e1077b4f69eb1db9f660469fbf97953038323430f3",
"split-corner-wall-thin-dark": "6da64905a3a4f8e4b4d457e5b20d2d55e0e7c2c601316a088c4bcc6557d35cc6",
"split-corner-wall-thick-dark": "494d559aa71ee85f90b8cfa11c1d3087fa123e963dec2b0975e7ce6c1520852a",
"isometric-geometry-view-dark": "73939fa61bebe2254591a788fad455a026a5e3b7394eeff27d4fe9a905de7ad2",
"isometric-geometry-view-light": "fc62f6ee8935d2e0c57b4918bf5fe554056a5956747b53a752ae265349ce662e",
"isometric-geometry-view-dark": "6db601f322fe55e33a1a89fe62f5d3a7d53f12bac4a68c7911b12f6382ef4fca",
"isometric-geometry-view-light": "c4c005fa55f64280abf9fccccdf7dc8efc3497bd626572bc11f45cc3cddaa159",
"isometric-live-layers-dark": "869c62bf9cd762c36d342d1a3bbd992969425b46b132f3243439735e2c75c14f",
"isometric-no-borders-dark": "36f972f95704bff81ea1a59bdf3cd2cf7b636ec7871780460e98b23e3ebc3da2",
"isometric-touch-kiosk-dark": "5eba7794e563e1ef5b9387184693c819976454d0efd222bd12b38aad8ea03e20",
"isometric-touch-kiosk-dark": "36c83bbe39809f346ebb5a0f4ed633c938e2fed028defaa48097c281db8e23a3",
"isometric-large-warm-remount-dark": "798d312671dffebf59034a39f2865a65ece27a84b150e77bc59038bb2567d07c",
"geometry-view-dark-fit": "3df272f6c3c3d20e9e375ea037f3dbb885657b94b0b29d0067505f4a73741237",
"geometry-view-light-fit": "a7f2c9667d9872dd84a37d5318fd238c9eabcc0f413017eb19e02204587b4e1a",
"geometry-plan-editor-dark": "19b5c84943b70074ba1aae51d1e54f9a59c44dde34fff89e32f0babb1e90a2ca",
"opening-placement-door-thick-wall-dark": "395c03bbf5d968e83664fd6621f0ac25902e718022f2e92ffbcddb8ce629cf9c",
"geometry-devices-editor-dark": "a9e4846ce5453400b87e6ad3d575882bc23a07b59dc3eecb612ed872b0c871ec",
"geometry-decor-editor-dark": "435b36096bbb2996d56ff0af262ddebff4a727edd841b0fad9b8d4f507b987ac",
"tray-wide-selection-en": "024ac666ac4dac01a36f7d1fdbf3bb41f76a627377f7c02dc373f429dfe96d43",
"tray-wide-tool-ru": "669fc1cf04433b936c0e9d05799ad19eda8bc003d5ab71937d0512c8f49ba921",
"tray-medium-group-en": "50cd3980b21f554bec2e76f4d2e8032f4bd30c0679477eb2f5cbe22f68935b7c",
"tray-medium-selection-ru": "4e5f235be8ed6296e136641d172e727a6a6b7a9d061f0928a96ccc1103c19f1f",
"tray-narrow-palette-en": "88b9846e4b451ed95b7ae7d2c3183a2ea191d7668768992a1364c6a7a53eb0c6",
"tray-narrow-tool-ru": "c4130715b3cb31c68619dfc706a3aa308e86edf272bfae6833b20666764ece2b",
"geometry-diagonal-45-opening-dark": "01206d25631c8fd09fa077932fbb5c1ee115b65ba76b38e6f7b09315dbbf6002",
"geometry-view-dark-fit": "a538deed6141b98b3e396d7da1024b18aee345312edd7954ad458751f08f48a3",
"geometry-view-light-fit": "0c57dee930f30a1f9c16e4e704675156f57a7623a4edf839c14a4fecbf12881c",
"geometry-plan-editor-dark": "6b06213324c5ff50c7176451307a33d8ac31ee636c96698f8efe61e8db763bae",
"opening-placement-door-thick-wall-dark": "24f472818f2246eeaa6568648ca4a6c42d9d84723fbcdd879435253399e2995d",
"geometry-devices-editor-dark": "c850e83f1af747f063b895109fe26d6a5804356fab505b0976073c28ad146104",
"geometry-decor-editor-dark": "44a95fd0b397c2729fea65c750fedcb11df10042ae198f3690151ead077c84e1",
"tray-wide-selection-en": "468ac6acfd7fcdcbfa947e032843ff117a32ae01b4ec30937a6cddecff534504",
"tray-wide-tool-ru": "388e03d7bf7a2391d0581e8446d0692048b9792e80bf3b9dd9bca86222fe9418",
"tray-medium-group-en": "190d5c356461ad2aa8b63b132416e8958a285047f8dea92301213678c7ac5a92",
"tray-medium-selection-ru": "ab91e88583b2863305434ce2775e31328638414671ff0a4f8ee82c8274c04405",
"tray-narrow-palette-en": "fb63483e7101d457d7fb1c83ae50435410ed797771adcbfde14dc2798a000ce1",
"tray-narrow-tool-ru": "60ce3c75de88b72fdb5d3fe7a16790184a8265c74f0f97e5be4fd83dbe0259fd",
"geometry-diagonal-45-opening-dark": "3736a75163d47a71f60c45cd554d604f27cfbc81119b5b55e8134da61a2a0f0a",
"openings-thick-wall-dark": "5aa0b3d26894bef9ab9fca25c31bbef2f13f2c410f5f6d3f61c8d608ceb929f8",
"openings-filled-tunnel-dark": "167d92c11e6a8b3ff0f31177ac5905f8db4b5fb03ee78b4965c40bc45aeee50f",
"openings-hidden-view-dark": "c85cc04d1d8622b98215e2bb83f5bb233a7cfb0ac684c912475ef7bc44245897",
@@ -45,15 +42,15 @@
"lighting-temp-glow-room-override-dark": "0a35d3508526187ea18e44456cfb8cd9e578a1864e896fad1c3eec2892c753e0",
"lighting-manual-auto-spill-overlap-dark": "6324dbe2079a255e7a194720c8c19f210549ac734e564270bc1373e6385b9cac",
"hover-over-glow-dark": "fc14ba6f6b670e61c0fb5be277e67551ea2da7a06b5c167a8c2c989f1de08910",
"hover-nested-room-dark": "6c09526ad885c4555063def5b43287b41124a81d90972c425084dba6e622d055",
"hover-nested-room-dark": "b70e385829a4fe45a77f5e1c3f8af4e18efec5282fb9594b26fab08c1f113d52",
"large-house-zoom-040-dark": "5f11c4b78318a64c2a7cf803716661eea506609d4f0a6bb3d64a709f8c49db1d",
"large-house-zoom-250-dark": "c906426f888ff4e306c5c334c6329b387fc5ca368e229f55acc33c202351a1ac",
"large-house-warm-remount-dark": "6baf4baed1c735c64dfe1e69d9864ca287ffc0e8452d00e801f0873e98b187ee",
"device-dialog-desktop-en": "d6fcc83aa1335df1041e2b1aa445b0019d1e3567f98ef8f47a889894051f2b62",
"device-dialog-mobile-ru": "8cb928853ddacb61882804c3d00ead31da31bc4559ee6a8e293ef6b55cd5a463",
"device-help-popover-light-ru": "f1bf21d62a5dd349aa57b746069c5aef58d7a26b0b9d9e0c233fde0c1d56d7eb",
"decor-color-popover-mobile-ru": "46d4c2e4dd20c3a38e90efe3db59b3e878bdbcf273fbc1aa23de4b230723fa6e",
"backup-full-preview-desktop-en": "1957c1c797c7793fb8dcf592ca74c9f2e3eccfcf4bac1d0187482c055bebb93b",
"decor-color-popover-mobile-ru": "16d2859d1ed3715c1c4d3d451a8428c37e91ba00da17272c59cf83420a7b6c31",
"backup-full-preview-desktop-en": "7f12943d1027c89f6fe46978fa1f4e1bcdbf85a1216281d850d467e68f52cbda",
"backup-space-preview-mobile-ru": "998c6b52c1cc95109feb440a9966cade738154ceeae387246d40f8c56f9e8a3a"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 321 KiB

File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
-16
View File
@@ -2,27 +2,11 @@
## Unreleased
## v1.63.0 — 2026-08-13
- Preserved explicit door, window and gate bindings when their standalone
sensor or lock marker is removed, and fixed the supported empty state after
deleting the last space
([#104](https://github.com/Matysh/houseplan-card/issues/104),
[#111](https://github.com/Matysh/houseplan-card/issues/111)).
- Splitting a room from an existing corner no longer deforms the exterior
facade, including with thick dividers. Flat, static and hidden isometric
rendering use the same preserved wall geometry
([#123](https://github.com/Matysh/houseplan-card/issues/123)).
- Small fixes and improvements.
## v1.63.0-beta.2 — 2026-08-13
- Splitting a room from an existing corner no longer deforms the exterior wall
or pulls a thick internal divider through the facade. Plan, View, kiosk,
static cards, hidden isometric rendering and light obstacles now use the same
preserved exterior geometry, including already saved plans
([#123](https://github.com/Matysh/houseplan-card/issues/123)).
- Small fixes and improvements.
## v1.63.0-beta.1 — 2026-08-13
-16
View File
@@ -8,27 +8,11 @@
## Unreleased
## v1.63.0 — 2026-08-13
- Сохранены явные привязки дверей, окон и ворот после удаления отдельного
маркера датчика или замка; исправлено предусмотренное пустое состояние после
удаления последнего пространства
([#104](https://github.com/Matysh/houseplan-card/issues/104),
[#111](https://github.com/Matysh/houseplan-card/issues/111)).
- Split из существующего угла комнаты больше не деформирует наружный фасад,
в том числе с толстым разделителем. Плоский, статичный и скрытый
изометрический рендер используют одну сохранённую геометрию стен
([#123](https://github.com/Matysh/houseplan-card/issues/123)).
- Мелкие исправления и улучшения.
## v1.63.0-beta.2 — 2026-08-13
- Split из существующего угла комнаты больше не деформирует наружную стену и
не вытягивает толстый внутренний разделитель сквозь фасад. Редактор плана,
View, киоск, статичная карточка, скрытая изометрия и световые препятствия
используют одну сохранённую наружную геометрию, в том числе для уже
сохранённых планов ([#123](https://github.com/Matysh/houseplan-card/issues/123)).
- Мелкие исправления и улучшения.
## v1.63.0-beta.1 — 2026-08-13
+7 -9
View File
@@ -1,18 +1,16 @@
<!-- release: v1.63.0 -->
<!-- release: v1.63.0-beta.1 -->
## Основное
- Удаление отдельного маркера больше не разрывает явную привязку двери, окна или ворот.
- После удаления последнего пространства интеграция остаётся рабочей и позволяет создать новое.
- Разделение комнаты из существующего угла больше не искажает наружный фасад даже при толстом разделителе.
- Удаление маркера датчика или замка больше не разрывает его привязку к двери, окну или воротам.
- Исправлено падение после удаления последнего пространства; из пустого состояния снова можно добавить пространство.
- Мелкие исправления и улучшения.
## Highlights
- Removing a standalone marker no longer breaks an explicit door, window or gate binding.
- The integration remains usable after deleting the last space and allows a new one to be created.
- Splitting a room from an existing corner no longer deforms the exterior facade, even with a thick divider.
- Deleting a sensor or lock marker no longer breaks its door, window or gate binding.
- Fixed the crash after deleting the last space; a space can again be added from the empty state.
- Small fixes and improvements.
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.63.0/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.63.0/docs/CHANGELOG.md)
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.63.0-beta.1/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.63.0-beta.1/docs/CHANGELOG.md)
+2 -2
View File
@@ -21,8 +21,8 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
| Item | State |
|---|---|
| Version | **v1.63.0** everywhere (manifest, const.py, package.json, CARD_VERSION) — stable promotion candidate after published v1.63.0-beta.2 |
| Current local cycle | v1.63.0 promotes the published beta line without product-code changes. v1.63.0-beta.2 preserves the exterior facade when Split starts or ends at a room corner (#123), using one wall geometry for flat/static/isometric rendering and light; v1.63.0-beta.1 contains the empty-plan and opening-reference fixes (#111, #104), plus reviewed process automation #105 and #118–#121. |
| Version | **v1.63.0-beta.1** everywhere (manifest, const.py, package.json, CARD_VERSION) — prerelease candidate |
| Current local cycle | v1.63.0-beta.1 fixes the empty-plan crash (#111) and preserves explicit opening sensor/lock references after standalone marker deletion (#104). Development after that beta preserves the exterior facade when Split starts or ends at a room corner (#123), using one wall geometry for flat/static/isometric rendering and light. The line also carries the reviewed process automation work #105 and #118–#121. |
| Hidden Labs Stage | #89 Stage 1 ships in v1.63.0-beta.1 as a hidden, expiring `iso` Labs experiment: a fixed near-top orthographic volumetric View. Flat remains default; editors and `houseplan-space-card` remain flat; all existing floor live effects and HA actions are preserved. This is internal, not a public feature. |
| Workflow | Owner's rule since 2026-08-07: ordinary fixes/features are made **locally, without tests and without commits**. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Release bodies are short and bilingual (Russian first): only significant user changes get individual bullets, while minor/code-only work is grouped as `Мелкие исправления и улучшения` / `Small fixes and improvements`; every body ends with separate links to the Russian and English changelogs. Detailed RU/EN changelog bullets may link the corresponding closed GitHub Issues; open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- <tag> --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand |
| GitHub | https://github.com/Matysh/houseplan-card — [Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical task records and the linked [Project v2](https://github.com/users/Matysh/projects/1) is the canonical priority/status view; both must stay current. `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
+155
View File
@@ -0,0 +1,155 @@
# CODE-REVIEW-123-r2
- **Issue:** https://github.com/Matysh/houseplan-card/issues/123
- **ТЗ:** `docs/specs/123-corner-split-wall.md` (зелёное ревью
`docs/reviews/SPEC-REVIEW-123-r1.md`)
- **Диапазон:** `git log --oneline origin/dev..HEAD` — 6 коммитов; относительно
предыдущего цикла (`docs/reviews/CODE-REVIEW-123-r1.md`, снят на коммите
`024a1ac`) диапазон вырос ровно на один коммит:
`e79f8f5 Fix corner split smoke geometry input` (`Issue: #123`,
`User-Visible: no`). `git diff 024a1ac..HEAD --stat` подтверждает: изменён
только `demo/smoke_split_corner_wall.mjs` (+6/−1 строк), продуктовый код
(`src/wall-thickness.ts`, `src/houseplan-card.ts`, `src/space-render.ts`) не
тронут ни байтом.
- **Роль:** ревьюер кода (не исполнитель), этап `S7-code-review`
- **Цикл:** r2/4
## Скоуп ревью
Единственная блокирующая находка r1 (`High-1`) — сломанная сигнатура вызова
`_lightBarriers(c._spaceModel())` в `demo/smoke_split_corner_wall.mjs:87`,
из-за которой смок падал необработанным исключением до выполнения хотя бы
одной проверки, и AC7 (`unit + smoke`)/AC8 (`smoke + golden`)/AC9
(`unit + smoke`) не были подтверждены доставленным доказательством. Скоуп
этого цикла: (1) убедиться, что фикс `e79f8f5` действительно чинит вызов, а не
маскирует падение; (2) прогнать смок и убедиться, что все 14 полей — `true`;
(3) убедиться, что тест по-прежнему умеет **содержательно** падать, а не
превратился в тавтологию; (4) поскольку продуктовый код не менялся с r1,
повторно прогнать быстрые гейты и точечные смоки по затронутым поверхностям
для очистки от сомнений, не переделывая заново детальное чтение
`src/wall-thickness.ts`, уже выполненное в r1 (не изменилось — см. diff-статы
выше).
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный, без вывода |
| Unit | `npm test` | `752/752` (`npm run inventory` подтверждает то же число), 0 fail |
| Build | `npm run build` | зелёный |
| Bundle sync | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` и `cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | оба совпадают побайтно; sha256 всех трёх `182fb55a…483ff` — идентичен значению из r1 (ожидаемо: продуктовый код не менялся) |
| Process gate | `node scripts/process-gate.mjs` | `диапазон origin/dev..HEAD, коммитов 6`, `гейт пройден, предупреждений 0` |
| Process gate + issues | `node scripts/process-gate.mjs --issues` | `гейт пройден, предупреждений 0` (метка issue #123 подтверждена через `gh`: ровно одна `S*` — `S7-code-review`) |
| Целевой browser smoke | `node demo/smoke_split_corner_wall.mjs` (после `npm run build`, синхронизация `demo/srv/assets/houseplan-card.js`) | **`OK`**, все 14 полей `true`: `beforeDrawn`, `wall0/15/100KeepsFacade`, `paper0/15/100KeepsFacade`, `dividerChangesInterior`, `lightUsesFacade`, `planViewParity`, `kioskParity`, `isoUsesCanonicalBody`, `staticParity`, `renderDoesNotRewriteConfig` |
| Regression-can-fail (сам доставленный смок, не независимая копия) | доставленный `demo/smoke_split_corner_wall.mjs` (версия из `e79f8f5`) скопирован в чистый `git worktree` на `52ec0fb` (коммит непосредственно перед продуктовым фиксом `47c6f10`, т.е. добаговый `wallBodiesGeometry`), пересобран и прогнан там | `FAILED (7)`: `wall0/15/100KeepsFacade`, `paper0/15/100KeepsFacade`, `lightUsesFacade` — все `expected true, got false`, `planViewParity`/`kioskParity`/`isoUsesCanonicalBody`/`staticParity`/`renderDoesNotRewriteConfig` остаются `true` (паритет между поверхностями держится даже на баге — расходится именно ожидаемый факт «фасад сохранён»). Падение содержательное (конкретные `false`, не исключение), т.е. смок доказывает именно то, что называет AC, а не тавтологию |
| Точечные browser smokes по затронутым поверхностям (split/wall-thickness/glow/iso/static-card) | `node demo/smoke_wall_thickness.mjs`, `smoke_merge_split.mjs`, `smoke_split_nonsnap.mjs`, `smoke_split_polyline.mjs`, `smoke_glow.mjs`, `smoke_isometric_contract.mjs`, `smoke_space_card.mjs` | все `OK`, регрессий на смежных поверхностях нет |
| Golden/performance/backend | не запускались | пре-релизные гейты по `PROCESS.md` §8/§11.4; визуальный/перф/backend-код не менялся с r1 (см. diff-статы), решение о непрогоне уже обосновано в r1 и остаётся в силе |
Полный набор из 128 browser-смоков не прогонялся — правка этого цикла точечная
(один файл демо-гарнеса), затронутые поверхности перечислены выше и покрыты.
## Находки
Блокирующих (High/Medium) находок нет. High-1 из r1 закрыт.
### Low-1 — смок строит `lightPolys` не буквально через хелпер `roomPoly(r)`
**Файл:** `demo/smoke_split_corner_wall.mjs:88-90`
```js
const lightPolys = lightSpace.rooms
.filter((room) => Array.isArray(room.poly))
.map((room) => ({ r: room, poly: room.poly }));
```
Продуктовый `_renderGlowLayer` (`src/houseplan-card.ts:13250-13252`) строит тот
же список через `roomPoly(r)` (`src/logic.ts:103-108`), которая (а) достаёт
`r.poly`, только если в нём **не менее 3** точек, и (б) для комнаты без
явного `poly` вычисляет прямоугольник из `x/y/w/h`. Смок вместо этого
фильтрует `Array.isArray(room.poly)` без проверки длины и не имеет пути для
`x/y/w/h`-комнат.
Для фикстуры issue (все комнаты заданы явным `poly` длиной 3 или 4)
результат совпадает с продуктовым один в один — расхождение не проявляется,
и AC7 доказан корректно для того сценария, который называет ТЗ. Но если этот
файл когда-нибудь расширят на комнату без явного `poly` (`x/y/w/h`), копия
молча исключит такую комнату из `lightPolys` там, где продукт бы её включил
— тихое расхождение, а не падение с сообщением.
**Решение ревьюера:** Low, не блокирует зелёный вердикт — фактическое
поведение для покрываемого сценария корректно, откладываю на усмотрение
автора при следующей правке этого файла (например, заменить построение на
прямой вызов `roomPoly` из продукта, если он становится доступен смоку).
## Что проверено и корректно
- **High-1 (r1) закрыт:** `_lightBarriers(lightSpace, lightPolys, lightPhysical)`
теперь вызывается с тем же числом и порядком аргументов, что и
`_renderGlowLayer` (`polys`, `physical` строятся явно, `physical` — через
тот же `c._physicalBodiesR(lightSpace)`, что и в продукте). Смок выполняется
до конца, `checkAll`/`finish` печатают `OK`, все 14 полей — `true`.
- **AC7 (`unit + smoke`):** `lightUsesFacade: true` — Glow использует то же
исправленное препятствие (`masonryGeometry` из `_lightBarriers`), что и
рендер стен; подтверждено смоком и независимо не расходится с unit-уровнем
r1 (`src/wall-thickness.ts` не менялся).
- **AC8 (`smoke + golden`):** `planViewParity`, `kioskParity`,
`isoUsesCanonicalBody`, `staticParity` — все `true`; Plan, View/kiosk,
скрытая изометрия и `houseplan-space-card` рисуют идентичный путь `d` для
сценария из issue. Golden-эталоны (второй тип доказательства AC8) —
пре-релизный гейт, не запускался, консистентно с r1/§11.4 PROCESS.md.
- **AC9 (`unit + smoke`):** `renderDoesNotRewriteConfig: true` — рендер не
мутирует сохранённые `rooms`/`walls`; сравнение JSON до/после рендера
совпадает.
- **Дисциплина «тест умеет падать» — усилена относительно r1.** В r1 AC7–AC9
были подтверждены независимой копией сценария вне репозитория (сам
доставленный файл падал необработанным исключением). В этом цикле
содержательное падение показано на **самом доставленном** файле — прогон в
чистом worktree на добаговом коде (`52ec0fb`, до `47c6f10`) даёт `FAILED (7)`
с конкретными `expected/got`, не крах. Это закрывает главное сомнение r1:
теперь именно тот файл, что лежит в репозитории, доказывает регресс, а не
только рассуждение ревьюера о нём.
- **Продуктовый код не менялся с r1:** `git diff 024a1ac..HEAD --stat`
показывает изменения только в `demo/smoke_split_corner_wall.mjs`. Всё, что
r1 проверил чтением и тестами по AC1–AC6, AC9 (unit-часть), AC10, AC11
(кэширование), AC12, AC13 (документация/changelog), остаётся в силе без
повторного разбора — предмет разбора не менялся, и разбор `r1` уже прошёл
свой цикл ревью.
- **Трейлеры и процесс:** `node scripts/process-gate.mjs` /
`--issues` — зелёные без предупреждений; коммит `e79f8f5` несёт
`Issue: #123` и `User-Visible: no` — корректно, это правка тестового
гарнеса (`demo/**`, класс B), поведение продукта не меняет, изменений в
changelog не требует и их нет. Метка issue — ровно одна, `S7-code-review`.
`origin/dev` не сдвинулся с момента слияния в ветку задачи (`merge-base`
совпадает с текущим `origin/dev`), ребейз перед мержем не потребуется.
- **Точечные смоки по затронутым поверхностям** (`smoke_wall_thickness`,
`smoke_merge_split`, `smoke_split_nonsnap`, `smoke_split_polyline`,
`smoke_glow`, `smoke_isometric_contract`, `smoke_space_card`) — все `OK`,
регрессий не найдено.
## Чего не проверял
- Полный набор из 128 browser-смоков — правка точечная (один файл демо-
гарнеса), полный прогон не пропорционален объёму изменения; прогнаны
целевой смок AC7–AC9 плюс смоки по затронутым поверхностям (см. таблицу).
- `npm run golden:verify` и `performance_smoke` — пре-релизные гейты
(`PROCESS.md` §8/§11.4), визуальный рендер и перф-чувствительные пути не
менялись с r1; будущий провал чинится по §11.4 без нового код-ревью.
- `tests_backend` — `custom_components/houseplan/**/*.py` не входит в
диапазон.
- Повторное детальное чтение `src/wall-thickness.ts`/`src/houseplan-card.ts`/
`src/space-render.ts` построчно — не требовалось: файлы не изменились со
времени r1, где это чтение уже выполнено и задокументировано.
- Low-1 не проверялся на альтернативной фикстуре (комната без явного `poly`)
— вне сценария, который называет ТЗ; см. решение ревьюера в находке.
## Вердикт
Зелёный. High: 0, Medium: 0 (Low: 1, не блокирует, решение зафиксировано в
находке Low-1 выше — оставлено на усмотрение автора без нового цикла).
Единственная блокирующая находка r1 устранена: доставленный
`demo/smoke_split_corner_wall.mjs` теперь вызывает `_lightBarriers` с полной
сигнатурой, проходит до конца с `OK` по всем 14 полям и содержательно падает
на добаговом коде того же файла (не независимой копии) — AC7, AC8, AC9
подтверждены доказательством, которое называет ТЗ. Продуктовый код не менялся
с r1 и остаётся подтверждённым: 752/752 unit, три bundle-снимка побайтно
идентичны, трейлеры и процесс-гейт зелёные.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "houseplan-card",
"version": "1.63.0",
"version": "1.63.0-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "houseplan-card",
"version": "1.63.0",
"version": "1.63.0-beta.1",
"license": "MIT",
"dependencies": {
"lit": "^3.1.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.63.0",
"version": "1.63.0-beta.1",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+4 -43
View File
@@ -67,12 +67,6 @@ const CLASS_C = [
const CHANGELOGS = ['docs/CHANGELOG.md', 'docs/CHANGELOG.ru.md'];
// Метки, при которых файла ТЗ в docs/specs/ быть не должно: на лёгком треке ТЗ
// живёт в теле issue (§5), на коротком — там же, и ревью ТЗ вообще не проводится
// (§5.1, issue #128). Офлайн эти случаи неотличимы от «ТЗ не написано», поэтому
// проверка 3 краснеет только когда метки прочитаны.
export const NO_SPEC_FILE = ['small', 'trivial'];
export const ALLOWED_STATUS = ['S5-ready', 'S6-in-progress', 'S7-code-review', 'S8-merged'];
export const STRICT_STATUS = ['S5-ready', 'S6-in-progress', 'S7-code-review'];
@@ -241,12 +235,12 @@ export function checkSpecs(commits, specFiles, labelsOf = null) {
if (labels === null) {
out.push({
level: 'warn', rule: 3, sha: c.short,
msg: `класс A по ${t}, но ТЗ docs/specs/${nn}-*.md не найдено — допустимо при метке small или trivial`,
msg: `класс A по ${t}, но ТЗ docs/specs/${nn}-*.md не найдено — допустимо только при метке small`,
});
} else if (!labels.some((l) => NO_SPEC_FILE.includes(l))) {
} else if (!labels.includes('small')) {
out.push({
level: 'fail', rule: 3, sha: c.short,
msg: `класс A по ${t}: ТЗ docs/specs/${nn}-*.md нет, и метки ${NO_SPEC_FILE.join(' / ')} на issue нет — код без ТЗ`,
msg: `класс A по ${t}: ТЗ docs/specs/${nn}-*.md нет, и метки small на issue нет — код без ТЗ`,
});
}
}
@@ -289,22 +283,6 @@ export function commitsUnderRuleOne(commits) {
);
}
export function isStableTarget(targetRef) {
return /^(?:refs\/heads\/)?main$/.test(targetRef ?? '');
}
// При stable promotion диапазон main..candidate закономерно содержит коммиты,
// уже выпущенные prerelease-тегом. Их issue к этому моменту обязаны быть закрыты
// (§2.8), поэтому повторная online-проверка статуса дала бы ложный отказ. Новые
// post-beta коммиты остаются в выборке и проверяются fail-closed как обычно.
export function commitsNeedingIssueStatus(
commits, { targetRef = '', isPublishedPrereleaseCommit = () => false } = {},
) {
const underRuleOne = commitsUnderRuleOne(commits);
if (!isStableTarget(targetRef)) return underRuleOne;
return underRuleOne.filter((commit) => !isPublishedPrereleaseCommit(commit.sha));
}
// 8. статус issue. Fail closed: недоступный или закрытый issue — отказ, а не
// пропуск. Гейт, который молчит при недоступном источнике правды, бесполезен.
export function checkIssueStatuses(numbers, runner, { allowed = ALLOWED_STATUS } = {}) {
@@ -398,7 +376,6 @@ function main(argv) {
const repo = value('repo', process.cwd());
const allowed = flag('no-merged') ? STRICT_STATUS : ALLOWED_STATUS;
const targetRef = value('target-ref', process.env.TARGET_REF ?? '');
let range = value('range');
if (!range && flag('github-range')) {
@@ -445,23 +422,7 @@ function main(argv) {
// проверки 3. Второй запрос по тому же issue — лишний сетевой вызов.
let labelsOf = null;
if (flag('issues')) {
const prereleaseTags = isStableTarget(targetRef)
? git(['tag', '--list'], repo).split('\n').map((s) => s.trim()).filter((tag) =>
/^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)-[0-9A-Za-z.-]+$/.test(tag))
: [];
const publishedCache = new Map();
const isPublishedPrereleaseCommit = (sha) => {
if (!publishedCache.has(sha)) {
publishedCache.set(sha, prereleaseTags.some((tag) =>
spawnSync('git', ['-C', repo, 'merge-base', '--is-ancestor', sha, `${tag}^{commit}`],
{ encoding: 'utf8' }).status === 0));
}
return publishedCache.get(sha);
};
const statusCommits = commitsNeedingIssueStatus(commits, {
targetRef, isPublishedPrereleaseCommit,
});
const numbers = [...new Set(statusCommits.flatMap((c) => c.issues).map((t) => t.slice(1)))];
const numbers = [...new Set(commitsUnderRuleOne(commits).flatMap((c) => c.issues).map((t) => t.slice(1)))];
const runner = ghRunner(process.env.HP_REPO ?? 'Matysh/houseplan-card', process.env.GH_BIN ?? 'gh');
const cache = new Map();
const cached = (nn) => {
+1 -1
View File
@@ -208,7 +208,7 @@ import {
} from './opening-placement';
import { safeStoredColor } from './color';
const CARD_VERSION = '1.63.0';
const CARD_VERSION = '1.63.0-beta.1';
const DISPLAY_LABEL_KEYS: Record<DeviceDisplayMode, I18nKey> = {
badge: 'display.badge',
icon_ripple: 'display.icon_ripple',
+1 -33
View File
@@ -14,7 +14,6 @@ import {
checkReviewDocLimit,
checkSpecs,
classify,
commitsNeedingIssueStatus,
commitsUnderRuleOne,
evaluateCommit,
makeCommit,
@@ -135,9 +134,8 @@ test('a class A commit without a spec warns offline and fails with labels', () =
assert.equal(offline[0].level, 'warn');
assert.equal(offline[0].rule, 3);
// С метками: small и trivial оправдывают отсутствие файла, их отсутствие — нет.
// С метками: small оправдывает отсутствие файла, его отсутствие — нет.
assert.deepEqual(checkSpecs([c], [], () => ['small', 'S5-ready']), []);
assert.deepEqual(checkSpecs([c], [], () => ['trivial', 'S5-ready']), []);
const strict = checkSpecs([c], [], () => ['S5-ready']);
assert.equal(strict.length, 1);
assert.equal(strict[0].level, 'fail');
@@ -171,36 +169,6 @@ test('only class A/B commits are held to the issue status', () => {
assert.deepEqual(commitsUnderRuleOne([reviewDoc]), []);
});
test('stable promotion skips status recheck only for commits already published in a prerelease', () => {
const published = makeCommit({
sha: 'a'.repeat(40), subject: 'Fix shipped in beta', body: 'Issue: #123', files: ['src/a.ts'],
});
const postBeta = makeCommit({
sha: 'b'.repeat(40), subject: 'New promotion work', body: 'Issue: #130', files: ['scripts/a.mjs'],
});
const publishedShas = new Set([published.sha]);
assert.deepEqual(
commitsNeedingIssueStatus([published, postBeta], {
targetRef: 'refs/heads/main',
isPublishedPrereleaseCommit: (sha) => publishedShas.has(sha),
}).map((commit) => commit.sha),
[postBeta.sha],
);
assert.deepEqual(
rules(checkIssueStatuses(['130'], () => ({
ok: true, json: { state: 'CLOSED', labels: [] },
}))),
[8],
);
assert.deepEqual(
commitsNeedingIssueStatus([published, postBeta], {
targetRef: 'refs/heads/dev',
isPublishedPrereleaseCommit: (sha) => publishedShas.has(sha),
}).map((commit) => commit.sha),
[published.sha, postBeta.sha],
);
});
test('issue status check is fail closed when the source of truth is unreachable', () => {
// AC3: недоступный gh должен давать отказ, а не молчаливый пропуск.
const broken = () => ({ ok: false, error: 'gh: could not resolve to a Repository' });