mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
Profiling #694 found three costs on every View pass, paid even with the summary panel hidden. The summary panel read the safe-area probe's computed style in layout(), which the card reaches up to five times per render (renderControls, menuItems, renderPanel twice, the clock check), and its updated() measured the stage, probe and kiosk buttons after every DOM commit. The insets now live in the measured state: measureLayout is the only method that reads style or layout, and updated() calls it only when an input of the measurement changed (probe, kiosk buttons or stage element, title, language, mode, kiosk, kiosk scale, narrow, HA theme), after connect() or an identity change, on visibility, once after document.fonts.ready, and from resized() as before. A floor switch or an HA tick no longer measures. The _model getter rebuilt the config fingerprint (a walk over every space and room with JSON.stringify of room settings) on each of its dozens of reads per render. ConfigFingerprintPass remembers the whole cache key (epoch and fingerprint) from the start of willUpdate() to the end of render() while the epoch, the config object and its spaces array are unchanged. Remembering only the fingerprint and concatenating the key on every read was tried first: in 2.5D on the large house the switch cycle measured slower than without any memo, and CPU profiles showed several times more garbage collection on load and on the first visit of a floor; one remembered key per pass has neither. Outside the pass (handlers, updated(), timers) every read still builds the key, so an in-place edit without an epoch bump stays visible (HP-1454-04). No write to the fingerprinted fields is reachable from willUpdate() or render(). _isoScene read the stage box during render only to feed an aspect into the overlay fit, whose frame has not depended on the aspect since #713. It now uses the frame's own aspect and passes stageSize: null. render-layout-read.mjs now also judges _isoScene and the whole summary runtime except measureLayout, forbids layout property reads (clientWidth, offsetTop, ...) besides the two calls, and reports every violation. Two registered mutants restore the old reads. No visible change: panel caps, side, offsets and kiosk clearance are computed from the same values; the 2.5D frame is the same. Issue: #725 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
802 lines
45 KiB
Markdown
802 lines
45 KiB
Markdown
# Development and deployment
|
||
|
||
## Input support contract
|
||
|
||
Read `docs/TOUCH-SUPPORT.md` before changing interaction code.
|
||
|
||
- View and kiosk must work well on touch and remain release-blocking surfaces.
|
||
- Editors are implemented and accepted against a desktop browser with
|
||
mouse/keyboard first.
|
||
- Full editor parity on phones/tablets is not required. If correct touch support
|
||
is expensive, an intentionally reduced or absent touch path is allowed.
|
||
- Every editor feature/spec/code review must classify touch as supported,
|
||
best-effort/degraded, or not exposed.
|
||
- A degradation is valid only when documented in the same change. It may not
|
||
compromise data integrity, permissions, confirmations or ordinary View.
|
||
- Do not add complex gesture state solely to claim touch parity. Prefer a clear
|
||
desktop recommendation or safe unavailable action over unreliable editing.
|
||
|
||
Existing touch editor behaviour is not silently disposable: when changing a
|
||
covered workflow, update its test and documentation explicitly and record why
|
||
the degradation is accepted.
|
||
|
||
## Local contour in 5 minutes (локальный контур за 5 минут, #633)
|
||
|
||
Three commands take a fresh Linux sandbox (agent session, WSL, a clean VM) from
|
||
nothing to a green smoke, a full unit run in parts and a pre-push gate. Each
|
||
step fits the ≈3-minute limit of one sandbox command; everything is idempotent,
|
||
so after a timeout or a sandbox restart the same command is simply repeated.
|
||
|
||
```bash
|
||
# 1. Worktree + dependencies + Chromium + bundle + one Playwright page.
|
||
# HP_BRANCH picks the branch (taken from origin if it exists there);
|
||
# without it the worktree is a detached origin/dev.
|
||
HP_BRANCH=issue/NNN-slug HP_WORKTREE=/tmp/w-NNN bash scripts/sandbox-bootstrap.sh
|
||
# or step by step: worktree | deps | chromium | bundle | check
|
||
cd /tmp/w-NNN && node demo/smoke_edge_cases.mjs # AC2 of #633: green
|
||
|
||
# 2. The full unit suite in parts that each fit one command.
|
||
npm run test:chunk -- 1/6 # builds test-build/, then the first sixth
|
||
npm run test:chunk -- 2/6 --no-build
|
||
npm run test:chunk -- 3/6 --list # only print the files of the part
|
||
|
||
# 3. Push: the pre-push hook runs npm run gate:small for issue/* branches.
|
||
git push origin issue/NNN-slug
|
||
HP_PREPUSH_GATE=0 git push origin issue/NNN-slug # explicit opt-out
|
||
```
|
||
|
||
What each command guarantees:
|
||
|
||
- **`scripts/sandbox-bootstrap.sh`** — the worktree comes from the clone the
|
||
script lives in (`HP_CLONE` overrides), `npm ci --ignore-scripts` runs only
|
||
when `package-lock.json` changed (`HP_SHARED_NODE_MODULES` links a ready
|
||
`node_modules` instead), Chromium comes from the npm package
|
||
`@sparticuz/chromium@152.0.0` (the Playwright CDN is closed in the sandbox,
|
||
the npm registry is not) and gets a shim at every path Playwright expects —
|
||
a real browser already there is left alone. `bundle` is `npm run
|
||
bundle:sync`; `check` opens one page in Playwright. The script carries no
|
||
owner paths and no credentials: pushing is configured separately. Golden
|
||
frames are still captured only in Linux CI (#455).
|
||
- **`npm run test:chunk -- N/M`** — `test/*.test.mjs` sorted by code point,
|
||
file *i* goes to part *i* mod M + 1 (round-robin). Chosen over size-balanced
|
||
parts so that a file stays in the same part while tests are edited; the M
|
||
parts together cover every file exactly once (`test/test-chunk.test.mjs`).
|
||
The test build runs in every part unless `--no-build`, so a part is
|
||
self-contained after a sandbox restart.
|
||
- **pre-push** — see [TESTING.md «Локальный набор перед пушем»](TESTING.md#локальный-набор-перед-пушем-343):
|
||
on by default for `issue/*` branches with an executable diff, skipped when the
|
||
branch diff against `origin/dev` is class C/D only (review documents,
|
||
changelogs, bundle), off with `HP_PREPUSH_GATE=0`, forced for any branch with
|
||
`HP_PREPUSH_GATE=1`. `gate:small` takes minutes; when a push must fit a
|
||
3-minute command, run `npm run gate:small` separately and push with
|
||
`HP_PREPUSH_GATE=0`.
|
||
|
||
## Local Windows workstation
|
||
|
||
The CI contract is **Node.js 22 + Python 3.14**. Do not use Codex's bundled
|
||
Node 24 or an unpinned machine Python environment as proof that a release will
|
||
pass. The pins are not declared twice: `node scripts/toolchain-pins.mjs` reads
|
||
them from `validate.yml`, `tests_backend/requirements.txt` and the lockfile, and
|
||
`npm run toolchain:check` compares the machine with them (#496). `.nvmrc` and
|
||
`.python-version` carry the same values for nvm/uv/pyenv; a test keeps them equal.
|
||
|
||
The supported native setup is repository-scoped and does not change the
|
||
machine's default Node, Python or persistent `PATH` (#557):
|
||
|
||
```powershell
|
||
# One-time/idempotent setup. Requires uv; installs a verified portable Node 22
|
||
# under %LOCALAPPDATA% and Python 3.14 in the dedicated .venv-ci.
|
||
.\scripts\windows-toolchain.ps1 setup
|
||
|
||
# Read-only proof: actual versions and executable/package/browser paths.
|
||
.\scripts\windows-toolchain.ps1 check
|
||
|
||
# Explicit pinned entrypoints for ordinary commands; no accidental PATH tools.
|
||
.\scripts\windows-toolchain.ps1 npm run gate:small
|
||
.\scripts\windows-toolchain.ps1 python -Arguments @(
|
||
'-m', 'pytest', '-p', 'pytest_asyncio.plugin',
|
||
'tests_backend/test_validation.py', 'tests_backend/test_trails.py',
|
||
'tests_backend/test_trail_recorder.py', '-q'
|
||
)
|
||
.\scripts\windows-toolchain.ps1 playwright install chromium
|
||
```
|
||
|
||
The Node archive is selected from the official release index for the major in
|
||
`.nvmrc` and checked against Node's `SHASUMS256.txt`. The script prepends that
|
||
directory to `PATH` only for its child process. It never removes an existing
|
||
venv: if the requested `-VenvPath` contains another Python minor, setup stops
|
||
and asks for another path. Playwright remains in its normal shared Windows
|
||
cache. Install `uv` once with `winget install --id astral-sh.uv --source winget`
|
||
if it is absent; GitHub access still uses the separately installed `gh`.
|
||
|
||
WSL2 is optional for the ordinary frontend and pure-backend loop. Keep its clone
|
||
inside Linux ext4, not under `/mnt/c`; on a fresh checkout run:
|
||
|
||
```bash
|
||
cd ~/houseplan-card
|
||
bash scripts/wsl-setup.sh # idempotent setup in dedicated .venv-ci
|
||
bash scripts/wsl-setup.sh --check # no installation; paths + versions only
|
||
bash scripts/wsl-setup.sh --verify # setup, real HA subset and one golden capture
|
||
```
|
||
|
||
The script provisions the CI pins (nvm → Node, uv → Python and the HA test stack
|
||
from `tests_backend/requirements.txt`, Playwright Chromium from the lockfile).
|
||
`--verify` imports Unix-only `fcntl` and the pinned Home Assistant, runs
|
||
`tests_backend/test_ha_setup.py`, builds the card and captures
|
||
`panel-wide-view-light-en` under `artifacts/golden/`; it records elapsed time and
|
||
the resulting PNG path. `HOUSEPLAN_VENV` selects another dedicated venv without
|
||
deleting or rewriting an existing one. WSL is normally early feedback. For an
|
||
intentional baseline change it may also produce the reviewed candidate with the
|
||
fail-closed command below; the canonical merge/release proof still lives in
|
||
GitHub Linux CI at the final exact SHA.
|
||
It is required only when running the full HA harness locally: current Home
|
||
Assistant imports the Unix-only `fcntl` module and cannot start its pytest plugin
|
||
on native Windows. Keep a WSL clone inside the Linux ext4 filesystem rather than
|
||
under `/mnt/c`, otherwise dependency installs become slower. The release CI
|
||
always runs this harness on Ubuntu and gates the exact tagged commit. Docker
|
||
Desktop is not currently required. Do not install the full Home Assistant pytest
|
||
stack natively just for this repository: its pinned `lru-dict==1.3.0` first
|
||
requires Visual Studio Build Tools to compile, but the resulting plugin still
|
||
cannot run without `fcntl`.
|
||
|
||
Useful repo-local Git settings on NTFS (optional for this small repository):
|
||
|
||
```powershell
|
||
git config core.fsmonitor true
|
||
git config core.untrackedCache true
|
||
```
|
||
|
||
## Local repository maintenance (#628)
|
||
|
||
Owner decision on 2026-09-25: use only a one-time local garbage collection for
|
||
the Windows clone. This issue does **not** introduce Git LFS, stop tracking
|
||
`dist/**`, remove the committed integration bundle, or rewrite published Git
|
||
history.
|
||
|
||
Run the maintenance command only when no Git process is active in the shared
|
||
clone (all worktrees use the same object database):
|
||
|
||
```powershell
|
||
git gc --prune=now
|
||
git count-objects -vH
|
||
git fsck --no-dangling --no-progress
|
||
```
|
||
|
||
The 2026-09-25 measurement before GC was 26,243 loose objects / 749.84 MiB,
|
||
41 packs / 334.55 MiB and 98 garbage entries / 959.60 KiB. After GC it is zero
|
||
loose objects, two packs / 467.63 MiB, zero `tmp_obj_*` files, and a clean
|
||
`git fsck`. `size-pack` grew because reachable loose objects moved into packs;
|
||
the meaningful total (`size` + `size-pack`) fell from 1,084.39 MiB to
|
||
467.63 MiB, a reduction of 616.76 MiB.
|
||
|
||
This post-GC value is the #628 monthly-growth baseline. To evaluate AC2 on or
|
||
after 2026-10-25, run the same GC and `count-objects` sequence and compare the
|
||
new `size-pack` with 467.63 MiB; the accepted upper bound is 487.63 MiB.
|
||
|
||
## Tests
|
||
|
||
- Frontend: `npm test` — compiles src/logic.ts+rules.ts (tsconfig.test.json) and runs node:test
|
||
(test/*.test.mjs). Strict typing: `npm run typecheck` (tsc --noEmit, part of `npm run build`).
|
||
- Pure backend on native Windows (with no HA plugin autoload): use the explicit
|
||
`python -Arguments @(...)` invocation above after setting
|
||
`$env:PYTEST_DISABLE_PLUGIN_AUTOLOAD='1'`.
|
||
- Full backend (including `test_ha_*.py`): `python -m pytest tests_backend/ -q`
|
||
in CI or WSL/Linux only.
|
||
- Junction-limit TS/Python parity (clean, no Home Assistant):
|
||
`npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs &&
|
||
python tests_backend/junction_parity.py --build-dir=test-build/junction-parity`.
|
||
Its dedicated reusable Validate job owns this proof; missing compiled modules
|
||
fail setup instead of turning into a pytest skip.
|
||
- IMPORTANT (audit lesson): the rollup typescript plugin reports a syntax error as a WARNING and still
|
||
builds the bundle — a truncated file can "pass". That is why the build starts with `tsc --noEmit`,
|
||
which fails on such errors. Always build with `npm run build`, never bare `rollup -c`.
|
||
- The committed bundle changes only in a beta/release candidate (#657). Rollup writes
|
||
`dist/houseplan-card.js`, `dist/houseplan-assets.json` and content-hashed chunks under
|
||
`dist/houseplan-assets/`; `npm run bundle:sync` builds and lays that tree out into the
|
||
untracked demo copy, and `npm run bundle:clean` restores the tracked `dist/` before an
|
||
ordinary commit (the `commit-msg` hook refuses bundle paths without a `Release:`
|
||
trailer). The candidate runs `npm run bundle:release`, which also updates the
|
||
integration snapshot `custom_components/houseplan/frontend`.
|
||
`node scripts/bundle-policy.mjs --verify HEAD` is the check CI and `gate:small` run:
|
||
build integrity always, byte parity with the committed copy only on a commit that
|
||
changes the bundle or is a candidate; `node scripts/bundle-tree.mjs dist
|
||
custom_components/houseplan/frontend` stays the read-only parity check of release automation.
|
||
- The first-space/import dialog is a separate `houseplan-onboarding-runtime-*`
|
||
chunk. Do not fold it into `houseplan-editor-runtime-*`: empty-install
|
||
onboarding is a View prerequisite, while a configured View must request
|
||
neither lazy runtime until the corresponding user intent.
|
||
- 2.5D rendering is a separate `iso-scene-render-*` chunk. A View with
|
||
`settings.volumetric_view` off (#649) must not request it. Its normal import and content-hashed retry
|
||
must pass the same source-fingerprint handshake before Iso installs atomically;
|
||
`houseplan-assets.json` records the graph as `lazyIsometricFiles`.
|
||
|
||
## Maintenance diagnostics
|
||
|
||
### Labs presentation flags
|
||
|
||
Labs is an internal, presentation-only runtime in `src/labs.ts`; it must never
|
||
gate config/schema migrations, persistence writes, HA services or network
|
||
requests. `?hp_alpha=1` or `#hp_alpha=1&space=<id>` enables every experimental
|
||
capability in the current build and persists `1` in
|
||
`houseplan_card_alpha_v1`; `hp_alpha=0` disables them and persists `0`. Query is
|
||
applied before hash and the last exact `1`/`0` wins. The URL is not rewritten,
|
||
unknown values fail closed for the current resolution, and the legacy
|
||
`hp-labs`/`houseplan_card_labs_v1` inputs are not read or migrated. Diagnostics
|
||
expose the boolean `window.__hpAlpha` together with the frozen sorted
|
||
`window.__hpLabs` capability array. Since #649 the registry is empty: 2.5D left
|
||
alpha for the General settings switch `settings.volumetric_view`; smokes turn it
|
||
on with `window.__hpTest.setVolumetricView(true)`.
|
||
|
||
To add a capability, add one unique lowercase id plus issue and a non-empty
|
||
summary to `LABS_FLAGS`, then cover registry validation and the alpha-on active
|
||
set; invalid or duplicate entries fail closed. Capabilities have no individual public key or version lifetime: the one
|
||
persisted alpha switch is deliberately indefinite until the owner changes the
|
||
contract. See `docs/ISOMETRIC.md` for the current use.
|
||
|
||
These commands are read-only diagnostics, not release gates:
|
||
|
||
```bash
|
||
# Show registered legacy/internal fields or inspect an exported config locally.
|
||
npm run audit:config
|
||
npm run audit:config -- path/to/houseplan-config.json
|
||
|
||
# Reproducible synthetic large-house report (seven measured samples + warm-up).
|
||
npm run benchmark:large-house -- --samples=7 --warmups=1 --output=artifacts/performance/local.json
|
||
|
||
# Hidden isometric profile; diagnostic only outside exact-SHA Linux CI.
|
||
npm run benchmark:large-house-isometric -- --samples=7 --warmups=1 --output=artifacts/performance/isometric-local.json
|
||
|
||
# Dense Stage 3 overlay/opening profile; also diagnostic outside exact-SHA Linux CI.
|
||
npm run benchmark:isometric-stage3-dense -- --samples=7 --warmups=1 --output=artifacts/performance/isometric-stage3-local.json
|
||
|
||
# Golden candidates never overwrite reviewed references.
|
||
npm run golden:capture
|
||
npm run golden:verify
|
||
npm run golden:accept -- --reviewed
|
||
|
||
# Full attested candidate for baseline acceptance. Run only in the ext4 WSL
|
||
# clone, on a clean named branch whose HEAD already equals origin/<branch>.
|
||
npm run golden:wsl:capture -- --expect-change=<scene-id,scene-id>
|
||
npm run golden:accept -- --reviewed --from=artifacts/golden \
|
||
--expect-change=<scene-id,scene-id>
|
||
|
||
# Docs screenshots whose pixels did not change (a version bump, a refactor):
|
||
# re-capture locally, compare decoded RGBA against the committed frames and,
|
||
# if every frame is identical, refresh only the manifest fingerprints (#512).
|
||
npm run docs:accept -- --identical
|
||
```
|
||
|
||
Golden frames never show the real card version: the harness sets the test-only
|
||
seam `window.__HP_VERSION_OVERRIDE__ = '0.0.0-golden'` before the card is
|
||
created, so a version bump alone changes no baseline (#512, see
|
||
`demo/golden/README.md`).
|
||
|
||
`golden:wsl:capture` refuses native Windows, `/mnt/c`, dirty or detached trees,
|
||
unpublished/mismatched branch SHAs, stale source fingerprints, toolchain drift,
|
||
partial matrices, undeclared differences and an insufficient witness floor. It
|
||
writes `artifacts/golden/wsl-attestation.json`, which self-hashes the source
|
||
identity, environment, pinned toolchain, report, every PNG and the acceptance
|
||
intent. Acceptance verifies the passport again and records it under
|
||
`localAttestation` in the baseline index. Copy the printed
|
||
`Baseline-Reviewed-Local: sha256:…` line to the baseline commit together with
|
||
`Release:`; never add the GitHub `Baseline-Reviewed:` trailer to the same commit.
|
||
Push that commit and wait for the full GitHub Validate on its exact SHA before
|
||
S7/merge. The local path removes the earlier expected-red capture run, not this
|
||
independent final check. A downloaded `golden-images` artifact from GitHub stays
|
||
supported and keeps the existing `Baseline-Reviewed: <run URL>` provenance.
|
||
|
||
The config audit performs no network requests and does not rewrite the input.
|
||
Its registry and lifecycle rules are documented in `CONFIG-COMPATIBILITY.md`.
|
||
The blocking performance workflow runs its independent profile pairs in
|
||
parallel. Inside each pair it captures the base SHA and candidate sequentially
|
||
on one pinned Chromium/CI runner, applies the same relative and absolute budget,
|
||
and uploads both reports plus the comparison. This preserves same-machine
|
||
comparability without serialising the whole matrix beyond the job timeout. A
|
||
developer-laptop report remains a diagnostic and must not be used to loosen CI
|
||
limits. See
|
||
`demo/performance/README.md`.
|
||
When the comparison base predates `scripts/bundle-sync.mjs`, the workflow still
|
||
builds that exact tree and materializes its fresh bundle through the equivalent
|
||
legacy copy path. This keeps old stable releases usable as performance baselines
|
||
without borrowing build output from the candidate. Comparative benchmark
|
||
launches also pass that target tree as the freshness authority; the ordinary
|
||
smoke launcher continues to default to the current repository root.
|
||
Both browser diagnostics require a freshly built/copied demo bundle. Rollup
|
||
embeds a SHA-256 fingerprint of `src/` plus the locked package and
|
||
Rollup/TypeScript build inputs; benchmark/golden runners fail before
|
||
capturing anything when `demo/srv/assets/houseplan-card.js` is stale;
|
||
`demo/bundle-freshness.mjs` also verifies every manifest-listed asset hash, so a
|
||
partially copied tree fails too. Golden commands and the explicit review
|
||
workflow are documented in `demo/golden/README.md`.
|
||
|
||
## Build
|
||
|
||
```bash
|
||
npm ci # once
|
||
npm run bundle:sync # build + entry/manifest/chunks → demo
|
||
npm run bundle:budget # initial View graph within INITIAL_VIEW_GZIP_BUDGET (scripts/bundle-budget.mjs)
|
||
npm run bundle:clean # before an ordinary commit (#657)
|
||
npm run bundle:release # candidate only: also → custom_components/houseplan/frontend
|
||
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend # candidate parity
|
||
```
|
||
|
||
## Deployment
|
||
|
||
Installations update themselves through HACS by release tag (`PROCESS.md` §12: no
|
||
manual copying into a running Home Assistant); the closed dev stand auto-deploys
|
||
the `dev` branch. Access to the owner's instances is not documented here.
|
||
|
||
## Frontend cache and the "empty view"
|
||
|
||
- The card module URL contains `?v=<VERSION from const.py>`. Browsers keep the ES module in
|
||
memory cache: after deploying new JS **bump VERSION in const.py and restart HA**,
|
||
otherwise a plain F5 will keep the old version. This is a deployment step — a
|
||
release candidate or your own local stand. An ordinary task commit bumps neither
|
||
`VERSION` nor the committed bundle: both change only in a commit with a
|
||
`Release:` trailer (#657, `PROCESS.md` §1).
|
||
- After a page reload the HA frontend (with kiosk-mode) sometimes leaves the view empty
|
||
("InvalidStateError: Transition was aborted", hui-view is not created for 1–2 min).
|
||
Cured by repeating the SPA navigation: pushState + a location-changed event, or just waiting.
|
||
|
||
## Resource registration and version recovery (#462)
|
||
|
||
- A writable Lovelace resource registry is the loader authority. Registration
|
||
returns a typed outcome; a pending or transient first attempt installs the
|
||
`extra_module_url` fallback immediately and schedules exactly one retry after
|
||
Home Assistant reaches running state plus a fixed one-second delay. Listener,
|
||
timer and retry task must all be cancelled through config-entry unload.
|
||
- Runtime registration facts belong in `hass.data[DOMAIN]` and System Health,
|
||
not in plan/layout storage. The persistent hard-reload notification is
|
||
localized from the backend `issues` translation category and its config-entry
|
||
flag is written only after `persistent_notification.async_create` returns
|
||
without an exception.
|
||
- Every successful `houseplan/config/get` is authoritative for
|
||
`integration_version`. A missing, non-string or whitespace-only value clears
|
||
a previously known version. The full-card version controller stays in the
|
||
initial View graph; do not move it behind the lazy editor runtime or add it to
|
||
`houseplan-space-card`.
|
||
- Targeted checks while changing this contract are:
|
||
|
||
```text
|
||
python -m pytest tests_backend/test_ha_frontend_registration.py tests_backend/test_ha_setup.py -q
|
||
npx tsc -p tsconfig.test.json
|
||
node scripts/fix-test-build.mjs
|
||
node --test test/version-recovery.test.mjs
|
||
node demo/smoke_version_recovery.mjs
|
||
node scripts/check-docs.mjs
|
||
```
|
||
|
||
The full HA pytest harness requires Linux/WSL; native Windows lacks `fcntl`.
|
||
|
||
## The stylesheet minifier sees TypeScript output, not the source (#526)
|
||
|
||
`scripts/css-template-minifier.mjs` runs as a Rollup transform, and by then the
|
||
module has already been through TypeScript. The TS printer puts a space between
|
||
a tag and its template, so the source `css`…`` arrives as `css `…``.
|
||
|
||
The plugin used to look for the exact string `css` + backtick and therefore
|
||
returned `null` for every stylesheet in the project: minification never ran
|
||
once, and roughly 23 KB of explanatory comments were shipped to every user —
|
||
12.8 KB gzipped in the initial chunk.
|
||
|
||
Two consequences for anyone touching this area:
|
||
|
||
- match the tag as a word followed by optional whitespace, never as a literal
|
||
two-character string;
|
||
- the guard that keeps this honest is not inside the plugin but in
|
||
`test/bundle-assets.test.mjs`: it takes real comment text out of
|
||
`src/styles/*.ts` and asserts none of it appears in `dist/**`. A plugin that
|
||
silently stops working cannot pass it.
|
||
|
||
## Do not animate container-relative properties on the plan (#524)
|
||
|
||
A CSS property whose value is expressed in container query units — `cqw`,
|
||
`cqh`, or any custom property derived from them, such as `--dev-size` — must
|
||
not appear in a `transition` on elements the plan draws in quantity.
|
||
|
||
Container query styles are re-evaluated whenever the container's inline size
|
||
changes: a tooltip, a scrollbar, a rotation, a panel resize. Every such
|
||
re-evaluation produces a new computed value and therefore **restarts the
|
||
transition on every one of those elements at once**.
|
||
|
||
That is how `box-shadow` on device markers cost a real user 9.4 frames per
|
||
second in Firefox 155: sixty-one markers started a 150 ms non-composited
|
||
shadow transition four times in two seconds, and the refresh driver spent the
|
||
window waiting for paint. Chromium starts exactly the same transitions — it
|
||
merely pays less for them, which is why the defect hid there.
|
||
|
||
The witness is browser-independent and lives in
|
||
`demo/smoke_marker_shadow_transitions.mjs`: change the stage container width by
|
||
one pixel and assert that no `transitionrun` for `box-shadow` arrives.
|
||
|
||
## Updating a pinned Action (#556)
|
||
|
||
Every `uses:` in `.github/workflows/**` is a full commit SHA with the human
|
||
version in a trailing comment; `node scripts/action-pins.mjs` enforces it and the
|
||
Validate preflight runs it. The comment is not decoration — it is the only thing
|
||
that tells a reader which release they audited.
|
||
|
||
To move a pin: read what the tag points at today,
|
||
|
||
```bash
|
||
gh api repos/<owner>/<repo>/commits/<tag> -q .sha
|
||
```
|
||
|
||
read the delta from the currently pinned SHA, then change **both** the SHA and
|
||
the comment in one commit. `node scripts/action-pins.mjs --list` prints every
|
||
third-party action with its pin, which is the fastest way to see what is behind.
|
||
|
||
Two of these are branches upstream, not releases — `home-assistant/actions`
|
||
(`master`) and `hacs/action` (`main`) — so their comment carries the date the
|
||
branch head was read. They have no tags to follow; the only honest record is
|
||
"this commit, read on this day".
|
||
|
||
## Moving the runner image (#658)
|
||
|
||
Every job that runs on a GitHub runner names an explicit image version in
|
||
`runs-on:`, and all workflows name the same one; `test/workflow-hygiene.test.mjs`
|
||
rejects a floating label such as `ubuntu-latest` and any drift between files.
|
||
The reason is the evidence tied to the image: golden baselines, documentation
|
||
screenshots and performance budgets were captured on it with the pinned
|
||
Playwright/Chromium (#455, #557), and Chromium's system libraries come from the
|
||
image itself (the install steps deliberately skip `--with-deps`). A floating
|
||
label moves under that evidence — `ubuntu-latest` becomes Ubuntu 26 from
|
||
2026-10-19 (actions/runner-images#14748) — and would turn a day with no change
|
||
into mass golden `different` and a red performance smoke.
|
||
|
||
Move the image on purpose, as its own infra issue:
|
||
|
||
1. change `runs-on:` in every workflow in one commit;
|
||
2. capture golden in CI on the new image and accept the differences with
|
||
review (`npm run golden:accept -- --reviewed …`), re-capture the documentation
|
||
screenshots (`demo/docs/capture.mjs`);
|
||
3. run the full Validate and `performance.yml`, and recalibrate a budget only
|
||
with measured evidence next to the number (the #483 and #675 pattern);
|
||
4. mirror the thin callers into `main` (see `workflow_sync` in `validate.yml`)
|
||
and remember that `release.yml`, `performance.yml` and the other files `main`
|
||
executes pick the new image up only with the next stable promotion.
|
||
|
||
The same test requires `timeout-minutes` on every runner job (at most 180; the
|
||
default is 360) and keeps `cron` off minutes 0/15/30/45, which GitHub's
|
||
scheduler delays by hours. A job that waits for other runs, such as the release
|
||
gate, gets a timeout above the sum of its own waits and says so in a comment.
|
||
|
||
## What the review model is allowed to do (#556)
|
||
|
||
The `model_review` job is the only untrusted stage of the review pipeline: it
|
||
runs a model with `Read/Write/Bash` over the material. Its `permissions:` block
|
||
is the real ceiling **only because** the `Review` step is handed
|
||
`github_token: ${{ secrets.GITHUB_TOKEN }}`.
|
||
|
||
Without that input `claude-code-action` exchanges the job's OIDC token for its
|
||
own GitHub App installation token (`src/github/token.ts`), and that exchange
|
||
defaults to `contents: write`, `pull_requests: write`, `issues: write` no matter
|
||
what the calling job declared — the `ghs_…` token then sits in the environment of
|
||
the model's own Bash tool. Removing the one line therefore widens the model's
|
||
rights silently, with every test still green, which is why there is a witness
|
||
(`test/review-doc-guard.test.mjs`) and a mutant
|
||
(`review-job-trusts-the-app-token`) standing on it.
|
||
|
||
`issues: write` is the single write scope the model keeps, because the process
|
||
asks the reviewer for the verdict comment (§7.2) and a separate issue for
|
||
out-of-scope Medium findings (§12). It buys comments, labels and issue-body
|
||
edits — not a commit, not a merge (that is decided in `integrate` from the sealed
|
||
`verdict.json`), not a release. Dropping it means moving both duties into
|
||
`integrate`, which is a pipeline change and not part of this one.
|
||
|
||
## Dependency and cache gotchas
|
||
|
||
- **polygon-clipping is a trap**: its `.d.ts` declares named exports but the ESM build has only
|
||
a default export — tsc or the runtime breaks, whichever you appease. Use **polyclip-ts**
|
||
(proper ESM + native types; same results, +~50 KB bundle via bignumber.js).
|
||
- **Redeploying the same version keeps the resource URL** (`/houseplan_files/houseplan-card.js?v=X`),
|
||
so browsers may serve the previous bundle from cache. Bump the version for anything users must
|
||
pick up, or hard-refresh (Ctrl+Shift+R) when testing a hotfix redeploy.
|
||
- **CSS `filter: blur()` on an SVG group is applied in name only** in Chromium:
|
||
`getComputedStyle` returns `blur(1px)`, and the rendered result changes by a
|
||
couple of hundred pixels on a whole plan — i.e. not at all. Use an SVG
|
||
`<filter>` with `feGaussianBlur` and `filterUnits="userSpaceOnUse"`. One
|
||
filter over the light layer costs about a fifth of one blurred mask per
|
||
source (60 sources: 66 ms vs 206 ms).
|
||
- **A geometry cache must be keyed by the geometry, not by `_cfgEpoch`.** The
|
||
epoch lags behind edits made in place (boundary/opening tools mutate the
|
||
space object), and a stale barrier set is invisible: the plan keeps lighting
|
||
through a wall that already exists. `_lightBarriers` hashes its own inputs
|
||
instead, and the same fingerprint keys the per-source region cache.
|
||
- **Segments that cross must be split before a visibility sweep.** The sweep
|
||
casts a ray at every barrier ENDPOINT; two faces crossing in their middles —
|
||
normal where wall bodies meet at a junction — leave that corner unsampled and
|
||
the fan closes it with a chord, so a sliver of floor next to a corner the
|
||
lamp plainly sees goes dark. `splitAtIntersections` removes the whole class.
|
||
- **Layout reads in the render path are judged by `scripts/render-layout-read.mjs`**
|
||
(`gate:small`, #654, #725): `getComputedStyle`, `getBoundingClientRect` and reads such as
|
||
`clientWidth`/`offsetTop`. The guarded methods (and the one summary-panel measurement method
|
||
that may read) are listed in the script; measure in `updated()` or an observer instead.
|
||
|
||
## Release
|
||
|
||
This section is the only home of the release mechanics; `docs/STATUS.md`,
|
||
`docs/ARCHITECTURE.md`, `AGENTS.md` and `CONTRIBUTING.md` link here instead of
|
||
retelling it. The process side — when an issue closes, what a stable commit may
|
||
contain, the independent line review — is `PROCESS.md` §2.8, §3 and §11.5.
|
||
|
||
The shape in one paragraph: a pre-release is one tested `dev` commit and tag
|
||
with `prerelease=true`, published from `dev`; `main` stays untouched. A stable
|
||
release fast-forwards `main` to the exact tested `dev` SHA and is produced only
|
||
by `release.yml`. Installations then update through HACS by tag («Deployment»
|
||
above); the dev stand takes the head of `dev` from the `dev-build` branch
|
||
(`demo/stand/README.md`).
|
||
|
||
### Primary prerelease path
|
||
|
||
Prepare the candidate as usual: synchronize every version field, add dated RU
|
||
and EN changelog sections, update the production bundle snapshots with
|
||
`npm run bundle:release` (since #657 the only commit that may change them; it
|
||
carries the `Release:` trailer), then lower the ratchets to the candidate's
|
||
facts with `node scripts/ratchets.mjs tighten` (#699: it rewrites the core line
|
||
caps, the gzip graph ceilings and `scripts/monolith-baseline.json` from the
|
||
fresh `dist/`; commit them with the candidate) and write the
|
||
short bilingual body in `docs/RELEASE-NOTES.md`. That file is the one current
|
||
instance of the canonical `## Основное` / `## Highlights` template; its two
|
||
changelog links must be pinned to the new tag.
|
||
|
||
After the candidate commit is pushed to `dev` and its exact-SHA Validate is
|
||
green, publication is one command:
|
||
|
||
```powershell
|
||
npm run release:prerelease -- v1.61.0-beta.4 --issues=63,64 --yes
|
||
```
|
||
|
||
Omit `--yes` for an interactive tag confirmation. Use the same fail-closed
|
||
preflight without creating a tag or release with:
|
||
|
||
```powershell
|
||
npm run release:check -- v1.61.0-beta.4 --issues=63,64
|
||
```
|
||
|
||
The orchestrator requires a clean, synchronized `dev`, byte-identical bundle
|
||
snapshots and a completed green Validate for `HEAD`; like `release.yml`, it
|
||
refuses a candidate without the `Release:` trailer and a committed bundle whose
|
||
embedded source fingerprint differs from the tree. Snapshot hashes and the
|
||
uploaded standalone JS are read from the exact Git blobs rather than checkout
|
||
bytes, so Windows CRLF conversion cannot disagree with the LF-tagged archive.
|
||
The archive command additionally forces `core.autocrlf=false` for that one
|
||
operation; it does not modify the developer's Git configuration.
|
||
It creates or verifies an
|
||
annotated exact-SHA tag, builds `houseplan.zip` directly from that committed
|
||
tree, verifies its manifest and embedded frontend against the candidate hash,
|
||
and creates `RELEASE-MEMBERSHIP.json`. Every explicitly supplied issue must have
|
||
an `Issue: #NN` trailer in the candidate history since the previous release;
|
||
issue authorship is irrelevant. The prerelease is staged as a draft and receives
|
||
both installable assets, the membership manifest and their `SHA256SUMS` passport.
|
||
Only then does it become public. The downloaded public bytes and membership are
|
||
verified against the candidate, followed by the paginated HACS order and
|
||
manifest-driven issue bookkeeping. Nothing else re-uploads assets after
|
||
publication (#540): the bytes it verified are the bytes that stay. Re-running
|
||
the same command after a partial failure is safe when the checkout still points
|
||
to the tagged candidate: stale assets are repaired, while a hidden per-release
|
||
comment marker, manifest membership and postcondition check make comment,
|
||
status-label removal and close individually repeatable (#547). ZIP inspection
|
||
is implemented in Node and does not depend on the host's `tar`/`unzip` variant.
|
||
A per-tag local lock and GitHub workflow
|
||
concurrency reject parallel runs; the local lock is removed on normal exit and
|
||
on handled `SIGHUP`/`SIGINT`/`SIGTERM` interruption (`SIGKILL` cannot be handled
|
||
by any process).
|
||
|
||
`Publish prerelease` in GitHub Actions is the browser/button equivalent. Select
|
||
the `dev` branch and enter the exact tag. It performs the same contract and
|
||
draft-first publication entirely on GitHub, including both assets. Prereleases
|
||
are intentionally silent in Telegram; only stable releases are announced.
|
||
GitHub exposes a `workflow_dispatch` button only after
|
||
the workflow file exists on the default branch; until the next promotion to
|
||
`main`, use the local command. The workflow snapshots the current S8 candidates
|
||
before publication, then retains only issues proven by Git history in the pinned
|
||
SHA. Its `close-merged` job consumes that immutable manifest after public asset
|
||
verification, so work merged later remains open and an accepted external issue
|
||
is treated like an owner-authored one. A retry reuses the published manifest and
|
||
resumes bookkeeping even when the release is already public (#120, #547).
|
||
|
||
**Independent line review (#638, `PROCESS.md` §11.5).** Right after the
|
||
candidate SHA is pinned, the `independent-review` job of `release.yml` queues
|
||
`.github/workflows/release-review.yml` on `dev` for the same tag and SHA. It
|
||
reviews every product surface changed since the previous stable tag against
|
||
`docs/SCOPE.md` and `docs/USER-GUIDE.ru.md` — without specs or review rounds —
|
||
and publishes `docs/reviews/RELEASE-REVIEW-vX.Y.Z.md` to `dev`. It runs in
|
||
parallel and **never blocks the release**: no release job needs it, a failure
|
||
is a warning, and the findings are the owner's call. Manual run:
|
||
`gh workflow run release-review.yml --ref dev -f tag=vX.Y.Z`; a repeat for a
|
||
tag whose document already exists is skipped unless `-f force=true`.
|
||
|
||
**Stable releases** go through `.github/workflows/release.yml`, the only
|
||
publisher of installable assets (#540). Run it with `workflow_dispatch` on
|
||
`main` with the exact tag: when the tag does not exist yet it is created on the
|
||
`main` tip; when it exists, its commit is the candidate. The workflow resolves
|
||
the tag to its exact commit, requires the `Release: <tag>` trailer on it,
|
||
checks the release contract (`release-contract.mjs --stable`) and
|
||
requires a complete Validate proof for its SHA and
|
||
Git tree (#541). The proof is tied to the workflow run ID and attempt and lists
|
||
both the requested checks and the jobs that actually executed. A skipped heavy job counts only
|
||
when its content-addressed reuse marker names an independently verified
|
||
successful source job. Since #573 the proof also carries composite evidence:
|
||
the identity of the product tree (every tracked path except the accepted
|
||
golden overlay `demo/golden/baselines/**`), the overlay itself (its Git tree,
|
||
the SHA-256 of `baselines-index.json` and either the run named by the commit's
|
||
`Baseline-Reviewed:` trailer or the attestation hash stored by a
|
||
`Baseline-Reviewed-Local:` commit) and the content key of every reusable job,
|
||
executed or reused. Release consumers standing on the candidate checkout
|
||
(`release-gate.mjs`, `release-prerelease.mjs`) recompute all of it locally and
|
||
fail closed on any mismatch, on a reused marker whose key is not the
|
||
candidate's, and on a declared review run that does not exist, was cancelled
|
||
or is not a Validate run; a proof without the block is stale for them. Review
|
||
and merge consumers pass no expectations, do not query the declared review run
|
||
and keep the #541 semantics unchanged. The
|
||
practical consequence is the beta.3 path: a candidate red only in golden,
|
||
then a baseline-only commit that reuses smoke, performance smoke, parity and
|
||
backend from the candidate's green jobs, skips every caught witness in the
|
||
mutation ledger and re-runs golden, preflight and frontend only. Review, merge and release use the
|
||
same `missing` / `pending` / `cancelled` / `stale` / `failed` state machine. A
|
||
cancelled or light run is not a release verdict. The newest compatible full run is the verdict:
|
||
a later failed full run blocks an older green proof, while a later cancelled,
|
||
light or otherwise stale run is skipped because it does not answer the same
|
||
release policy. A later complete green full run can refresh an older failure
|
||
(#511, #619, #656). The release also requires Full
|
||
Performance and a green E2E run on a
|
||
real Home Assistant — `e2e-gate.mjs --ref=<sha>` dispatches `e2e.yml` in
|
||
`Matysh/houseplan-e2e` on the **candidate commit**, whose
|
||
`custom_components/houseplan` tree the suite installs from the codeload tarball
|
||
(#514, #540) — then builds once, archives `houseplan.zip` from that same tree
|
||
(`git archive <sha>:custom_components/houseplan`, deterministic), writes
|
||
`SHA256SUMS`, uploads everything into a draft, publishes, downloads the public
|
||
assets back and checks them against the passport, and only then announces.
|
||
Before staging, a stable release also runs `npm run continuity:screencast`: the
|
||
CDP compositor screencast fails the run on an empty or black presented frame and
|
||
uploads the failed frames as `continuity-screencast`. The
|
||
tree hash printed in the run summary is the identity between what E2E installed
|
||
and what HACS downloads.
|
||
|
||
**After a stable release**, once its line review
|
||
(`RELEASE-REVIEW-vX.Y.Z.md`) is in `dev` and the `S7-code-review` queue is
|
||
empty, archive the line's review documents (`PROCESS.md` §2.10):
|
||
`node scripts/reviews-archive.mjs --through=vX.Y.Z` prints the plan,
|
||
`--apply` moves the files into `legacy/reviews/vX.Y.Z/` with `git mv`, rewrites
|
||
the relative Markdown links in and to the moved documents and rebuilds
|
||
`docs/reviews/INDEX.md` (`--check-links` lists what is still broken); commit the result as one class C commit
|
||
whose `Issue:` trailer names the issue doing the move or the repository-hygiene
|
||
umbrella (`PROCESS.md` §11.3).
|
||
|
||
Publishing a stable release by hand in the GitHub form still works, but
|
||
fail-closed: `release: published` starts the same workflow, which immediately
|
||
turns the release back into a draft and walks the same path; nothing installable
|
||
is public while the gates run. A red gate leaves the draft in place — open the
|
||
linked run, the Playwright traces and screenshots are in its artifacts; fix,
|
||
then cut a new tag. Re-dispatching the workflow on an already public tag is a
|
||
**repair**: the gates run again on the SHA, missing assets are added, and an
|
||
existing asset whose hash differs from the rebuilt one fails the run instead of
|
||
being replaced. Hand-published betas are ignored by this workflow — prereleases
|
||
have their own staged path above. Bump the version in every source
|
||
`scripts/release-contract.mjs` reads (`parseVersionSources`: `package.json`,
|
||
`package-lock.json`, the integration `manifest.json` and `const.py`,
|
||
`CARD_VERSION` in the card and in the editor runtime); the snapshot table in
|
||
`docs/STATUS.md` reports whether they agree.
|
||
|
||
Validate intentionally runs on branch pushes, not tag pushes, so an annotated
|
||
release tag does not duplicate the expensive browser/performance matrix; a new
|
||
push cancels an unfinished Validate for the same branch, and the gates accept
|
||
only a completed run for the exact SHA, never "the last green one". Every
|
||
tagged SHA must therefore already be pushed to a branch and have a completed
|
||
green full exact-SHA Validate proof. For an owner-approved emergency hotfix, push a
|
||
temporary `hotfix/*` branch and wait for Validate before creating the tag;
|
||
never tag a detached or otherwise unpushed commit, because the release gate
|
||
will wait for a run that cannot exist and then fail closed after one hour.
|
||
|
||
The GitHub Release body is a concise **bilingual user summary**, not a copy of
|
||
the exhaustive changelog. Put Russian first and English second, with equivalent
|
||
meaning in both sections. Give separate bullets only to significant features
|
||
and user-visible behaviour changes. Collapse minor fixes, visual polish,
|
||
refactors, tests and purely internal improvements into one final bullet:
|
||
`Мелкие исправления и улучшения.` / `Small fixes and improvements.` Keep the
|
||
body short because HACS displays it inside Home Assistant and concatenates the
|
||
bodies of skipped releases. The full detail remains in both
|
||
`docs/CHANGELOG.ru.md` and `docs/CHANGELOG.md`; finish every release body with
|
||
two explicit links, one to each language version of the changelog.
|
||
|
||
A **stable** body aggregates the changelog since the previous **stable**
|
||
release, never since the last beta (#328, owner rules 2026-08-27): everything
|
||
the line's beta changelogs describe reaches it, while a bug introduced and fixed
|
||
strictly inside the beta line — never shipped in any stable — stays out. Every
|
||
bullet links its GitHub issue so the rules stay machine-checkable. Draft with
|
||
`npm run release:notes -- <tag>` (it lists each candidate item with its source
|
||
section so the curator can strike in-line-only fixes), curate by hand, then
|
||
`npm run release:notes -- <tag> --verify` must pass. The small-fixes bullet is
|
||
allowed only when the range really contains user-visible work the body does not
|
||
itemise; a single-issue hotfix ships without it (the verifier enforces this).
|
||
Open or partially delivered issues are never presented as shipped.
|
||
|
||
Changelog entries may link directly to a **closed** GitHub Issue when that
|
||
issue is the canonical task for the shipped change. Append a normal Markdown
|
||
link such as `([#55](https://github.com/Matysh/houseplan-card/issues/55))` to
|
||
the relevant bullet in both language changelogs. Keep this optional: do not
|
||
invent issues for minor work, do not link open or partially delivered issues,
|
||
and do not expand the grouped small-fixes bullet into an issue inventory.
|
||
|
||
```md
|
||
<!-- release: vX.Y.Z -->
|
||
|
||
## Основное
|
||
- Значимое изменение.
|
||
- Исправлена конкретная проблема ([#123](https://github.com/Matysh/houseplan-card/issues/123)).
|
||
- Мелкие исправления и улучшения.
|
||
|
||
## Highlights
|
||
- Significant change.
|
||
- Fixed a specific problem ([#123](https://github.com/Matysh/houseplan-card/issues/123)).
|
||
- Small fixes and improvements.
|
||
|
||
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/vX.Y.Z/docs/CHANGELOG.ru.md)
|
||
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/vX.Y.Z/docs/CHANGELOG.md)
|
||
```
|
||
|
||
The literal `## Основное` and `## Highlights` headings above are the canonical
|
||
release-body format. Do not maintain a second `## Русский` / `## English`
|
||
template in scripts or release notes; change this single template if the
|
||
product format changes again.
|
||
|
||
Replace `vX.Y.Z` with the release tag so the links remain pinned to the
|
||
published version instead of drifting with `dev` or `main`.
|
||
|
||
Local release gates are deliberately different. A pre-release runs
|
||
`npm run build` plus only the unit tests and browser smokes selected for the
|
||
changed surfaces; record the exact selection in the release handoff. A stable
|
||
release runs the complete local frontend, backend and smoke gates before its
|
||
tag is created. The exact-SHA Validate required by `release.yml` remains in
|
||
force for both and can run a broader matrix automatically; the local policy
|
||
does not weaken the publication guard.
|
||
|
||
Feature promotion has an additional hard gate: every new feature or material
|
||
behaviour change must spend at least one published beta/RC before stable. A
|
||
stable release commit may change only version fields, generated bundle
|
||
snapshots and changelog/release metadata; feature source changes belong in the
|
||
preceding pre-release commit. Skip this step only for an explicit owner-approved
|
||
emergency hotfix, and document the exception in the handoff. A
|
||
`Release vX.Y.Z-beta.N candidate` commit is **not** promotion-only: it carries
|
||
the work itself and follows the ordinary rules, trailers included.
|
||
|
||
## Smoke tests (since 2026-07-27)
|
||
|
||
Every `demo/smoke_*.mjs` ends with:
|
||
|
||
```js
|
||
checkAll(out); // every key must be true...
|
||
checkAll(out, { n: 4 }); // ...unless an expected value is given
|
||
await finish(browser, out);
|
||
```
|
||
|
||
`finish` prints the JSON dump (useful on failure), reports named mismatches and
|
||
sets a non-zero exit code — including when the card threw during the run. The
|
||
suite runs in CI (`smoke` job) against a freshly built bundle; never test the
|
||
`demo/srv/assets/houseplan-card.js` copy, which `npm run bundle:sync` writes and which is not committed (#255).
|
||
|
||
`updateComplete` proves only that Lit finished its own update. Pointer-owned
|
||
editor state can be painted later by `live-editor`, outside that cycle. Browser
|
||
checks which read live editor DOM must await the lazy runtime's
|
||
`_whenLiveEditorSettled()` contract; a timeout, sleep or fixed number of RAFs is
|
||
not evidence that the latest projection was applied. If a smoke changes editor
|
||
mode before dispatching synthetic gestures, it must also wait for the observable
|
||
mode-transition and viewport-refit state to settle, because the stage can finish
|
||
its physical resize after the Lit update (#460).
|
||
|
||
When adding a checklist line marked `[auto: ...]` in docs/TESTING.md, add the
|
||
failing check in the same commit — that is what the marker now promises.
|
||
|
||
The fake `hass` in `demo/srv/demo.html` is set once: opened directly in a
|
||
browser, the page renders the plan but **device icons appear only after a
|
||
re-render** (F5, or `card.hass = {...card.hass}`). `demo/serve.mjs` does that
|
||
nudge for smokes; a plain browser session does not. It is a harness limitation,
|
||
not a card bug.
|
||
|
||
Known environment-sensitive smoke: `demo/smoke_opening_measure.mjs` fails two
|
||
sub-checks (`place_dialog_x_magnetised`, `place_committed_x_center`) under the
|
||
pinned Chromium — a `1e-6`-tolerance magnet snap on the opening *placement*
|
||
path. It reproduces against the pristine committed bundle: pre-existing pixel
|
||
precision, not a regression of the change under test.
|