HOOK_FILES in test/pre-push-gate.test.mjs listed the hook's modules by hand.
After #729 process-gate pulled in review-doc-guard and friends, and #737
broke Validate with a new transitive import (model-usage.mjs) that the list
did not carry: the temporary repository lacked the module and the hook died
with ERR_MODULE_NOT_FOUND.
The list is now computed: the hook itself, the scripts it runs by path
(`$repo_root/scripts/...`, today process-gate.mjs and pre-push-gate.mjs), and
the transitive closure of their local imports (static import/export ... from,
side-effect imports, literal dynamic import() and new URL(..., import.meta.url)
reads). gate-small.mjs stays out: the test writes its stub. A referenced file
that does not exist fails loudly instead of shortening the list. On the
current tree the derived set equals the old manual one.
Witness: a copy of the hook tree with a new module imported from
model-usage.mjs (with its own import, a dynamic import and a data read) gets
all four files automatically. Checked by hand: the old manual list with such
an import makes the real-hook test fail with ERR_MODULE_NOT_FOUND, the
derived list passes. Module names in the witness are built from base names,
because check-inputs reads a `scripts/...` string literal as a data leaf and
stops following that module's imports.
Issue: #766
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Thirteen test harnesses executed workflow `run:` bodies with their own bash
flags. Four of them used `bash -eo pipefail` "as in Actions", but the runner
executes a step without `shell:` as `bash -e {0}`: pipefail comes only from an
explicit `shell: bash` or from `set -o pipefail` in the body. The harness
supplied protection the step did not have, so a step that lost its pipefail
stayed green in tests (#729, #737, #751); the reverse also happened - the
#793 guard test was red only because of the harness flag.
test/helpers/workflow-step.mjs resolves the shell like the runner (step ->
jobs.<id>.defaults.run.shell -> workflow defaults.run.shell -> unset), maps it
to the runner's command lines (unset -> `bash -e {0}`, bash -> `bash
--noprofile --norc -e -o pipefail {0}`, sh, python, custom templates with
{0}), writes the body to a file and executes it by path. Unsupported YAML or
shells are refused loudly instead of guessed. All step-executing tests now go
through runStep(findStep(...)).
Witnesses: a step whose left pipeline side fails is green without a shell
(negative test) and red with `shell: bash`, job defaults or `set -o pipefail`;
the #751 executed test now also shows that the same real steps without their
pipefail line stay green, i.e. the test sees a removed pipefail; a guard fails
on any test that runs a step with its own bash flags. TESTING.md rule 7 names
the helper.
Issue: #766
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The waiter knew two of the eleven merge-candidate outcomes, and those
through copied prefixes. A red or stuck candidate, a moving dev, a push
refusal (#705) or a failed merge step went unrecognised. In the window where
the comment is already posted but the label still reads S7, the author waited
until the timeout. Once the label moved to S6, only «S7 → S6» was printed and
the reason was lost.
Comments are now classified by merge-candidate's own catalog (outcomeOf)
first, then by the pipeline's own comments as before. The outcomes are
mapped by kind, not collapsed into one error:
- terminal refusals (reject-stale, conflict, validation-red, give-up,
push-refused[-workflow] at merge and rebase stage, error) — exit 3 with
their own reason even while S7 is unchanged;
- validation-missing (the merge stopped waiting for Validate) — exit 3,
with «no code change needed, re-apply S7 after a green Validate»;
- rereview — printed, the wait goes on: the task is already back in S7 and
the new round starts by itself;
- a green merge has no sign in the catalog — the S8 label reports it (0).
A label change keeps exit 0 and now carries the reason in its lines. The
round anchor (latest S4/S7), the baseline for older comments, blocked and
review-4 are unchanged. «stale» and «merge-conflict» stay in
PIPELINE_EVENTS: they are the fallback for pre-#752 bodies, and
process-metrics.mjs imports them by kind.
Tests use the actual bodies from commentFor/describePushRefusal; each of the
four new cases is red on the original wait-verdict.mjs. The new mutant
wait-verdict-merge-outcomes-unknown was checked by hand: its guard goes
red. AGENTS.md, where the exit codes are described, is updated to match.
Issue: #768
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
On Windows the fixture-type test failed with "Cannot read properties of
undefined (reading 'text')". TypeScript normalizes root file names before
it asks the compiler host (C:\repo\test\probe.mjs becomes
C:/repo/test/probe.mjs), while the host compared that name with the
node:path string by ===. The probe was never served, the program had no
probe file, and the verdict read undefined.text. The module-resolution
filter compared dirname(containing) with TEST_DIR the same way, so on
Windows it let ../test-build/* and node:* imports resolve.
The host now compares names by the compiler's own key: '/' separators and
the host's getCanonicalFileName (case-insensitive where the file system
is). readFile serves the probe as well. A source file the check depends on
that is missing from the program now fails with its name, the compiler's
own diagnostic and a truncated list of the files the program knows,
instead of a TypeError.
Portability witness, runnable on Linux: the program builder takes the path
implementation, root and file system, and a new test builds the same
program over this checkout seen as C:\houseplan-card\ through path.win32
with case-insensitive names. It asserts that the probe is found, that the
dead-field verdict equals the native one, and that nothing under test-build
is looked up. With the old === comparisons it is red (probe missing,
test-build looked up); with the old lookup it reproduces the Windows
TypeError. The dead-field and direct-call witnesses are unchanged.
Issue: #777
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Ship the reviewed battery-indicator shadow and per-device opt-out together
with the global All / Low only / None display modes. Rebuild the committed
bundle, refresh the pixel-identical documentation fingerprint, and tighten
the beta ratchets.
Issue: #806
Issue: #807
User-Visible: yes
Release: v1.80.0-beta.6
General settings offer All / Low only / None for battery indicators as a
three-button segmented control, the same visual as the light-source choice
in the device dialog. Low only keeps just the red low state (below 20 %, or
a binary battery sensor that is on); normal, warning and unknown are hidden.
Storage keeps #792 compatibility: absent = All, false = None, the exact
string "low" = Low only. The backend accepts bool or "low" and refuses any
other value; old frontends read "low" as All, old backends refuse to save it
(documented in CONFIG-COMPATIBILITY). One presentation option
(batteryLowOnly) filters the resolved indicator, so View, kiosk, 2.5D,
space-card and the device dialog preview share the same rule.
Track raised ship -> show: new i18n keys, a new config value and Python.
Tests: unit (mode resolver, write, dirty, low-only presentation), backend
validator and support package, smoke_device_battery (low-only keeps only
red), smoke_general_settings_form (radios, save/reopen/clean, All removes
the key). New mutant battery-low-only-shows-every-state; the backend mutant
anchor moves to the new validator.
Issue: #807
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Count delivered page errors after awaited browser close. Exercise the public
harness lifecycle with deterministic negative subprocess cases and retain the
unchanged Chromium probes for the real transport. Move the premature-snapshot
mutation witness to the deterministic Node suite.
Issue: #776
User-Visible: no
Address the in-scope Medium from CODE-REVIEW-792-r1 with a deterministic
golden scenario that uses actual topology rendering and overlapping battery
ink. Keep the existing battery boards and their reviewed images unchanged.
Align the English guide's setting name with the shipped locale (Low).
Issue: #792
User-Visible: no
Resolve one stable battery source independently of the functional device face,
include battery-only ticks in immutable render snapshots, and add a default-on
shared preference. Keep LED strips excluded and Zigbee captions above the
passive, out-of-flow indicator. Use the owner's accepted four MDI icons in the
designer's frame geometry and colours.
Include resolver/backend/browser/pixel contracts and three new golden scenes.
Recalibrate the owner-approved absolute/raw size budgets; preserve the existing
initial View rolling ceiling and band. Lazy extraction remains separate #805.
Issue: #792
User-Visible: yes
Keep diagnostic captions and the matching pointer tooltip readable, including
late overlay paint, camera/resize changes and narrow-card fallback. Preserve
focus, touch and device actions. Add pure placement and real-mouse witnesses.
Issue: #802
User-Visible: yes
Prepare the next beta from integrated dev: provider-confirmed Zigbee routes,
LED zoom optimization and on-core colour, plus test/process fixes.
Synchronize seven version fields, both changelogs, release notes, status,
generated bundles and measured ratchets. No new feature source in this commit.
Local checks: bundle:release PASS; 117 selected unit tests PASS;
smoke_zigbee_topology_hover (547 checks), smoke_led_strip_tube and
smoke_led_zoom_quality PASS; check-docs --screenshots=strict PASS;
release-contract v1.80.0-beta.1 PASS. Full exact-SHA CI required before tag.
Derived docs: run 37310388990, unchanged PNGs; fingerprint-only commit fd406acea.
Batch ship review for #767 remains required before publication.
Release: v1.80.0-beta.1
Issue: #767
Issue: #789
Issue: #790
Issue: #791
Issue: #793
Issue: #794
Issue: #795
Issue: #798
User-Visible: yes
Replace inferred BFS topology with confirmed end-device parents and active
router routes from ZHA and Zigbee2MQTT. Fail closed on ambiguous evidence,
preserve stale snapshots, and request routing tables without hover traffic.
Use solid LQI-coloured arrows, plan-scaled unknown-LQI outlines, and named
unplaced targets. Add provider/runtime/visual regression coverage.
Owner authorized author self-review and merge to dev while the review model
is unavailable; no release or issue closure is included.
Issue: #798
User-Visible: yes
Preserve both independently reviewed code commits without rewriting history.
The owner explicitly approved manual integration after the review model failed.
The combined candidate must pass exact-SHA Validate before the dev push.
Issue: #794
Issue: #795
User-Visible: no
Split the full registry into ten shards without increasing the one-hour
limit. Preserve summary and step outcome separately so cancellation after
194/194 remains red without claiming the summary was missing.
Issue: #795
User-Visible: no
Avoid grep -q closing the pipe before printf completes under pipefail.
Exercise the actual guard with a large label snapshot and retain refusal cases.
Issue: #793
User-Visible: no
Clarify nightly review and beta coverage without changing gates or schedules.
Cover the real night/beta publication summaries and reject stale category
counts, totals and membership in the browser-guard documentation.
Issue: #767
User-Visible: no
Exercise a non-null marker absent from the render-device roster alongside a
valid lit neighbour. The previous null-marker smoke returned before the
mutated guard, so it never tested this defensive consumer boundary.
Move this witness to the direct Node runtime suite and update its inventory.
Issue: #791
User-Visible: no
Retain the 48-band DOM and exact idle raster, paint 24 midpoint bands during actual zoom, and restore after 160 ms. Keep input, HA and lifecycle guards observable; measure the full camera/restore/restart cycle without relaxing historical budgets.
Issue: #789
User-Visible: yes
Follow the render-local barrier wrapper without weakening empty-space or
confirmation assertions. Register the cold-import connection guard mutation.
Issue: #789
User-Visible: no
Coalesce source-entry updates without changing the 500 ms transition or
field geometry. Resolve shared barrier revisions once within a synchronous
render and always recheck content on the next pass. Release main/static
LED owners on no-strip exits and static owner/config teardown.
Protect scheduling, real browser fades, cold imports and the bundled
render-pass wiring. Performance acceptance remains a separate exact-SHA
full Linux run; this commit alone does not assert that AC3 has passed.
Issue: #789
User-Visible: yes
Keep the executable shared-masonry proof without adding new text anchors
against the monolith. Update the unchanged circle-event mutation oracle.
Issue: #788
User-Visible: no
Keep real end and corner emitters, robust decimal joins and wall-circle
sweep events. Render one positive-winding compound visibility clip so
Chromium cannot cancel or cut away overlapping light regions.
Add independent pixel oracles for glow falloff and wall-following tubes.
Replace the lossy fan-count limit with explicit cached-path bounds while
retaining the original timing and warm-cycle heap-growth limits.
Issue: #788
User-Visible: yes
Keep repeated --expect-change flags compatible while accepting the comma-separated workflow input and ignoring whitespace, empty values, and duplicates.
Issue: #783
User-Visible: no
Publish the three integrated S8 issues since beta.2. Synchronize all seven version sources, rebuild committed frontend assets, tighten ratchets to measured facts, and update release metadata for LED strips. Canonical documentation screenshots were reviewed from Linux CI and accepted in the preceding derived commit.
Local checks: build/typecheck, bundle policy, bundle budgets, monolith checks and release-contract tests passed. Native-Windows unit run passed 3517 tests; two GNU-bash tests and one TypeScript diagnostic-shape test remain Linux-only. Full exact-SHA Validate is mandatory before publication.
Release: v1.79.0-beta.3
Issue: #765
Issue: #780
Issue: #781
User-Visible: yes
r1 M5: the runtime and field chunks release a card's frame and field cache
on disconnect (ledRelease from disconnectedCallback), never cache for a
disconnected card, and a chunk that lands after disconnect renders nothing
(the card's LED hook is connected-only). The profile now reports the three
caches of the shown space separately (shapes ≤ 50, visibility ≤ 50, retained
per-emitter fans ≤ 2500) through the runtime's ledStats, asserts 0 retained
entries and 0 live LED timers/frames/observers after every disconnect,
judges the Long Tasks of a 100-step camera series as well as the interaction
profile's camera scenario, runs one extra cold mount whose runtime response
is held while the card is removed, and enforces ≥ 7 samples after ≥ 1
warm-up — also on reports merged from parts (--warmup-only, --merge).
Issue: #780
User-Visible: no
r1 of the code review:
- M1: a strip keeps its marker's value badge, passive, at the half-length
anchor on the card and on the static card (no icon core, pulse or slot).
- M2: one room resolver for the strip's Glow — an explicit valid room_id of
the marker wins over the anchor room; a stale one falls back (stripRoom).
- M3: the chain remembers the space it is drawn in; a space switch finishes
it there, never in the space shown next, and opens no picker over it.
- M4: visibility is decided before import(): a hidden marker or an
HA-disabled device loads no LED chunk (ledVisible, also checks the stored
marker so a just-hidden one does not slip through a stale device list).
- M6: the static card is a full light_pools × live_states browser matrix.
Unit tests for M2/M3, smokes for M1/M3/M4/M6, five registered mutants.
Issue: #780
User-Visible: yes
Stage 5 of #780.
- Import summary: «Strips left unbound after import: {n}» from the backend
`unbound_led_strips` count; «Optimize plans» reports strips passing
through walls per space and edits none (AC16).
- Linear field for long strips (ТЗ §13.2): pieces of at most the radius
along the polyline, emitters thinned to r/4, each piece clipped to the
visibility fans of its own emitters as separate clipPath children (no
boolean pass per piece), one floor clip for the whole layer, no fan at
all where nothing blocks within the radius; a grid index of body faces
and boxed inside tests; unchanged fields skip re-diffing. 50×50 on the
large house: first stable frame ~1.4 s, warm space ~1.1 s locally.
- led-strips-v1 profile: demo/benchmark_led_strips.mjs with the derived
large-house fixture (10×5, 50×50, none), absolute limits of the ТЗ table
in demo/performance/budgets-led-strips.json, exact counters (zero
recomputes on HA ticks/camera/colour, ≤50 cache entries, no growth over
20 cycles); added to the full performance workflow.
- Bundle: LAZY_LED_GZIP_CEILING 10 KiB, LAZY_LED_EDITOR_GZIP_CEILING 11 KiB
(measured + 10 %, rounded up); overlaps with the initial and editor
graphs refused; the lazy editor graph stays inside its ceiling.
- Smokes smoke_led_strip_draw/bind/glow, linked in smoke-links; 13 mutants
in the registry (7 browser guards in the inventory); config field registry
entry `spaces[].led_strips`.
- Golden: five new scenes on the `golden-led` space of the visual fixture
(`ledStrips` option, the designer's four strips on #868D94), matrix v71.
- Docs: LIGHT, DEVICE-PRESENTATION, USER-GUIDE (en/ru), UX-MODES,
ARCHITECTURE, ISOMETRIC, CONFIG-COMPATIBILITY, TOUCH-SUPPORT, demo/stand
README, performance README; docs/design/led-strips with the unchanged
designer archive, two paired frames and ACCEPTANCE.md; both changelogs.
Issue: #780
User-Visible: yes
Stage 4 of #780 (ТЗ §4–§5). «LED strip» next to «Add» draws a chain with
clean clicks only (pan, pinch, a second finger, cancel and the synthetic
click after navigation add nothing), snaps to the grid and to physical wall
faces / zero-wall axes, stops at the first face of masonry, partitions,
columns and windows (doors, gates and passages are cut by geometry), Shift
gives 45°. Ctrl+Z removes the chain's own point first, Esc finishes, a
double click or a click on the first point (≥3 vertices) closes; fewer than
two distinct points write nothing. A finished strip opens the device picker
(lights first, taken markers explained, «New device…» binds in that
dialog's own write, «Later» keeps unbound geometry).
A selected strip shows its vertex handles (a drag re-checks both neighbours
and the whole path) and a new Devices tray branch: device settings,
bind/change, unbind, show as icon, delete. The device dialog gets the
representation section: show as strip (restores a hidden shape at once or
draws one for this marker, behind the dialog's own save/discard guard),
show as icon, unbind/delete for a hidden shape. Every geometry or
representation change is one optimistic write and one LED command of the
device history; Undo/Redo restores only its own strip record and refuses
when a newer change sits on it. A rebinding renames the link in the marker
save, a deleted marker leaves an unbound strip, a bound marker may not move
to another space. Plan/Background show strips as a passive translucent mark.
The tool, its `led` dictionary (en static, ru/de/fr lazy) and placement
geometry are a new lazy `led-strip-editor` chunk (9.5 KB gzip), loaded only
on the tool, an editable strip in the shown space or a strip device's
dialog. The initial graph gets the loader and delegation only
(src/led-strip-card.ts); the lazy editor graph +122 B, inside its ceiling.
Mutant anchors follow the moved code (marker dialog guard, static LED layer).
Issue: #780
User-Visible: no
ТЗ §13.1: the static card with light_pools:false must not load the linear
field. led-strip-field.ts (2.1 KB gzip) is a dynamic import of the LED
runtime (5.0 KB gzip), loaded only when a strip is on in a Glow room with
a light scene, with the same fingerprint handshake and hashed-URL retry
(manifest role led-field, retry token counted).
Issue: #780
User-Visible: no
Stage 3a of #780.
- src/led-strip-gate.ts is the only initial-graph foothold (ТЗ §13.1): which
markers a space shows as a strip (active and bound), the half-length
anchor that replaces their icon position, and one page-wide load of the
lazy led-strip-runtime chunk (fingerprint handshake, a hashed-URL retry on
the next explicit entry, never in a loop).
- The card: a represented marker takes no auto slot, draws no icon, casts no
round pool and is placed at the anchor; two call sites render the stripe
layer and the linear field from the chunk. The space model carries the
stored strips untouched; scaling, validation and geometry are in the chunk.
- src/led-strip-runtime.ts: the stripe is two strokes of one derived path
(outline #383838 t, core t/2, round joins and caps, t = 0.08/0.12 D),
white off, white core + field on with Glow, source-colour core without
Glow, grey dashed unavailable without a field. The hit path takes the
card's own device handlers (one action path) with radius max(22 px, t/2)
and the nearest stripe as owner. The linear field is the exact distance
field with the shared falloff: opaque grey bands of a luminance mask per
piece, pieces joined by lighten (the maximum), each piece clipped to what
its own emitters see, so a hidden part never lights through another part's
visibility; buried emitters emit nothing; a failed clip is dark. A bounded
per-space cache (50) counts geometry rebuilds.
- The initial View graph had 501 B of headroom. The furniture library copy
(furn.*, 104 keys × 4 languages, editor-only) moves into a new lazy `tools`
namespace (#627 mechanism) that the editor runtime awaits; the initial
View graph is 298 686 B gzip with the LED gate in it (−1 879 B vs the base).
Tests: test/led-strip-runtime.test.mjs (6: representation, gate anchor =
geometry anchor, falloff, states and radius, per-piece clipping and buried
strips, bounded cache), bundle and i18n fixtures for the LED chunk and the
fourth namespace.
Issue: #780
User-Visible: no
Stage 2 of #780. src/led-strip-geometry.ts (pure, not imported by the
initial graph): the anchor at half the polyline length; stripPieces and
visibleStripPath — a segment lying on a thick body face within
epsilonGeom is shifted t/2 into free floor, free floor and zero-wall axes
stay at 0, a face→floor transition is a connector without gap; emitter
samples epsilon outward on a face and none inside a body; placement that
stops at the first face and lets a strip touch and slide along it, a
vertex drag clamped on its path and both neighbours; the screen hit owner
with radius max(22 px, t/2) and a stable-id tie.
SpaceModel gains optional led_strips (render units, data only, no geometry
import in space-geometry.ts).
Tests: test/led-strip-geometry.test.mjs (10).
Issue: #780
User-Visible: no
Stage 1 of #780 — the data model. A space carries an optional
`led_strips: [{id, points, marker, active?}]` (custom_components/houseplan/
led_strips.py, pure, strict mypy).
- Type schema inside SPACE_SCHEMA: 2–50 finite numeric points (no strings,
booleans, NaN or off-canvas values), ≤50 strips per space including hidden
shapes, strict boolean `active`, marker = non-empty string or null.
- A config-level step after coordinate canonicalisation judges the shape
(two distinct points, non-zero length, a closed strip needs three distinct
vertices, a hidden shape needs a marker, unique ids per space) and the links
in the order the spec fixes: duplicates are rejected before any
normalisation (two links to the same missing id still conflict); a link to
a marker that is not live becomes an unbound strip (marker null, active
true, id and points kept), so a client that does not know strips can delete
a bound marker without its save failing; a live marker with an empty space
adopts the strip's space; a non-empty foreign space rejects the write.
- config/set answers with `led_strips: {unbound, space_adopted}` when the
write was normalised, so a new client re-reads; old clients ignore it.
- Space import remaps links through the marker id map; a skipped or
virtualised duplicate leaves the strip unbound; a coinciding old id never
binds. Plan-only export keeps geometry and nulls every link. Import details
report `unbound_led_strips`, computed by the server, never read from the file.
- Coordinates get JSON-noise cleanup only, like stairs (face contacts are
off-lattice), in both canonicalisers with a shared fixture case.
- Support package: counters only (total/unbound/hidden), no coordinates or ids.
Tests: tests_backend/test_led_strips.py (41, pure), test_ha_import_export
(6 cases: full round trip with a hidden shape, orphan count, remap against a
coinciding id, skip and virtual duplicates, plan-only), test_ha_websocket
(old client deletes a bound marker → save stands, counters, foreign space
rejects without a new revision). Full backend with the HA harness: 969 passed.
Mutating the shape check, the duplicate check, the orphan normalisation or the
space adoption each turns the pure suite red.
Issue: #780
User-Visible: no