longTasks.maxCountP95 / maxTotalP95Ms sum every window of a sample,
switchCycle included. Since #735 that window is warm, and the 30 tasks /
12000 ms of the cold cycle sat 1.25-3.2 times above the family maxima;
performance_smoke judges only these absolute ceilings.
Series (CI only, as #747): every Full Performance run 2026-10-01..10-05,
14 runs, each with a runner containing #735 (5e318168), both sides,
7 samples, one runner type (linux x64, Chromium 151.0.7922.34, Node
22.23.3) - 27 reports / 189 samples per profile. Point = what the ceiling
judges, the nearest-rank p95 of a report (its largest sample). The window
is warm in all of them: largest single Long Task in switchCycle 190 ms
against 427-1008 ms for the smallest cold floor switch; the #735 guard
fired in none.
count p50/p95/max total p50/p95/max (ms)
large-house-v1 10 / 12 / 13 2826 / 3140 / 3301
plan-snap 14 / 14 / 15 3387 / 3610 / 3759
interaction 12 / 13 / 13 3056 / 3242 / 3289
isometric 20 / 21 / 22 4761 / 5101 / 5104
stage3-dense 23 / 24 / 24 5516 / 5935 / 5935
isometric-backdrop 21 / 22 / 22 4931 / 5286 / 5331
Rule (#747, per metric, one number per family): first step (one task,
50 ms) at or above 1.15 x M, no higher than 1.2 x M:
flat (lh, plan-snap, interaction, interaction smoke)
30 -> 18 (1.15 x 15 = 17.25, +20 %), 12000 -> 4350 (1.15 x 3759, +15.7 %)
2.5D (isometric, dense, backdrop, isometric smoke)
30 -> 28 (1.15 x 24 = 27.6, +16.7 %), 12000 -> 6850 (1.15 x 5935, +15.4 %)
Out of M: the base of the v1.79.0 stable comparison 37149329461 is
v1.78.0 (7d4d75bd, 09-28, before #735 and #694/#725/#739). On the same
runner as its candidate it reads 1.2-2 x higher - code, not noise - and
serves as the known slower variant: its dense sum of 8754 ms fails 6850;
its other points (isometric 24/6783, backdrop 22/5159, flat at most
17/3959) pass and stay the relative comparison's job. The one 3-sample
interaction smoke of the period (36910217188, 12/3047) passes. The
relative half (ratios, 3/5 tasks, 150 ms) is unchanged.
The test pins one pair per family in every file, replays all 162 points
and the smoke point through the --absolute-only evaluation, recomputes
the rule, requires doubling to fail on each metric and the v1.78.0
verdicts above, and keeps the relative fields; #507's count pin follows
to 28. Red on the previous budgets, and red with 12000 left in the
backdrop file.
The README records the series and the rule ("CI contracts") and, in
"Changing budgets", the recalibration method; it also drops the stale
#585 text: the 450/150/120 gesture allowances returned to 150/60/75 with
6f226a5b (v1.77.0-beta.2), and the backdrop twin carries the iso count
allowance too.
Issue: #770
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Three profiles walk the 2.5D candidate contract of the large-house
runner - large-house-isometric-v1, its Stage 3 dense twin and the
backdrop twin of #743 - but both of its errors said
"large-house-isometric-v1 candidate has no ...", so a dense or backdrop
failure pointed at the wrong profile and job.
The two checks move into assertIsometricCandidate(card, profile, stage)
in demo/performance/card-contract.mjs, self-contained like
assertCardContract and injected the same way with toString(); the
runner calls it with the profile it measures for the pre-#448 Labs hook
and for the renderer contract. Conditions and wording are otherwise
unchanged, so a base or candidate that passed before passes now.
The unit test runs both the export and its serialized copy for all three
2.5D profiles and requires "<profile> candidate has no Labs fixture hook"
and "<profile> candidate has no renderer contract"; it also pins that the
runner injects and calls it and no longer names one profile for all. Red
on the previous runner (the anchor), and red when the message is
hardcoded back to large-house-isometric-v1 (dense profile).
Issue: #770
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The isometric and interaction perf-smoke profiles joined Validate only on
a src/** diff. A change of their budgets alone - or of the shared
large-house runner, its fixture, the private card contract or the
evaluator - ran performance_smoke with Glow alone, so a new ceiling was
first judged by the beta candidate, and the profile set of the reuse key
stayed "glow" for a diff that changed what that set is judged by.
perf_iso and perf_interaction now also take their own budget files (the
smoke and the full profile's, which the smoke repeats, #473 AC4) and the
large-house harness, which brings in both. The Glow runner and
compare.mjs stay out: the Glow smoke always runs and exercises them; the
Stage 3 dense fixture belongs to a profile without a smoke. Tests still
select nothing.
The new unit test reads the profile of each smoke step from validate.yml
(its --budgets file), finds every budget of that profile in
demo/performance by its "profile" field, and requires each to select the
profile, change the profile set of the reuse key computed from the
workflow's own set lines, and be an input of the performance_smoke hash.
Budgets of profiles without a smoke select neither. On the previous
classifier both #770 tests are red:
"demo/performance/budgets-isometric-smoke.json судит
large-house-isometric-v1 - профиль обязан войти в смок".
Issue: #770
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
(iconDevs.map) and opening symbols (resolvedHosted.map) were still bare
map() calls. The card takes the full cardStyles, so `.device-core`
(background/color 0.15 s), `.device-shell-frame` (border-color) and the
door `.op-leaf`/`.op-arc` (0.6 s) transition, and Lit's positional reuse
turned a list change into an animation of something that never happened.
Measured on dev with a browser witness (one card element, doors on a
partition in both spaces, neighbours in different states):
- setConfig f1 -> garden: 4 nodes outlive the switch (closed door
hp764-g-shut drawn in frame 0 at f1's open angle, matrix(0,-1,1,0); the
same-id door hp764-shared in f1's closed state; mower and gate start in
the ceiling light's orange), 16 transitionrun, 39/65 frames off-final.
- config push (a door inserted first, the first marker hidden): 11 nodes
handed to a neighbour, 35 transitionrun, 38/68 frames off-final.
Both lists are now keyed(space.id, repeat(..., id, ...)), the shape of the
full card (#525/#534) and of the rooms (#745). The outer key makes a door
with the same id on another floor a new object; an id-less opening keys by
its index. The transitions stay: a real contact or light change on the same
object still animates on the same node, and the witness asserts it (catches
a `transition: none` fix). The #742/#745 note in plan.styles.ts now names
these lists too.
Witness: demo/smoke_space_card_identity.mjs (new, linked in smoke-links to
src/space-render.ts). It samples computed styles per frame and compares
each object to its settled look, not just DOM ids (#528). Red on dev as
above, green after: 0 transitions, 0 frames off-final, 0 reused nodes.
Mutants (checked by hand, each red on its own case): markers by map() and
openings by map() (config push), openings without the space key (same-id
door on setConfig). The marker space key has no observable case here, as
device ids are global; it is kept for parity with #534.
Issue: #764
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Both pre-review comments of _process.yml start with «**Ревью не
запускалось:**»: the rebase conflict and the red or missing Validate on
the material. The waiter matched the shared prefix alone and printed
«конфликт разрешает автор» for both, sending the author to rebase a branch
that has no conflict instead of reading the Validate run.
PIPELINE_EVENTS now holds three signs instead of one: validate-red (its
own line: Validate on the material is red or missing, read the run, a
code change is not required), conflict (the rebase conflict, the old
line) and not-run (the family with an unrecognised continuation: read the
comment). All of them exit 3, as before. The two cause regexes used to
live in process-metrics.mjs as a copy of the templates; process-metrics
now takes them from PIPELINE_EVENTS by kind and re-exports them under the
same names, so the waiter and the K3 return reasons share one source.
The process-metrics contract test now finds the common prefix under kind
not-run, which is what the family entry is called now.
Tests build the bodies by running the two _process.yml steps as the
runner does (runStep, gh and git stubbed), for the conflict and for both
Validate kinds. The three new #810 cases are red on the original
scripts. The new mutant wait-verdict-not-run-validate-unrecognised was
checked by hand: its guard goes red.
Issue: #810
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Review r1 M1: summaryParagraph() took the LAST paragraph starting with
«High: N», trusting «retellings first, conclusion last». The section order
is not fixed: «## Вердикт» comes before «## Унаследовано из r1» in
SPEC-REVIEW-728-r2 and after it in 774-r2/806-r2. An inherited section
quoting an old counter as its own «- High: 2, Medium: 1» paragraph gave the
previous round's numbers (reproduced by the reviewer).
The source is now chosen by structure, not position. ownText() blanks
retelling sections with their subsections and blockquotes (with lazy
continuation), keeping line numbers:
- a retelling section is «Закрытие раунда», «Унаследовано», «Inherited»,
«Предыдущий раунд», or any level-2+ heading naming a round that is not the
document's own (round from the file name, else from the «# …-rN» title);
examples are «Вердикт по находкам r1» in r2 and «Дельта r1 → r2».
The own verdict line, the «## Вердикт» section and the summary paragraph are
searched only in that text. The summary is preferred from the section of the
own verdict line, else the last one in own text. Code blocks stay own text:
reviewers put their §7.2 comment template there (SPEC-REVIEW-288-r1). Only a
«#» inside a block is not a heading. The later fallbacks (verdict mention,
unique counter, headings) read the whole text as on dev. Moving them to own
text fixes some legacy rows but loses verdict recognition in others
(289-r2, 376-r2, 462-r2/r3), which is a separate decision.
Index against dev, every docs/reviews/*.md: still exactly one row changed
(#662 r3 → 🟡 0/2). In legacy/reviews (control, not indexed) there is one
more than before: SPEC-REVIEW-403-r2 yellow → green. Its first «Вердикт»
section is the retelling «## 1. Вердикт r1 и SHA…»; the own «## Итог» says
«Вердикт: зелёный».
The new test covers both layouts (verdict before/after the retelling) and a
§7.2-shaped retelling. All three are red on the branch before this commit
(2/1, 0/0, red 1/0) and green after. New mutant
reviews-index-retold-sections-own; anchors of four neighbours updated. All
five were checked by hand: their guards go red.
Issue: #779
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
INDEX.md showed SPEC-REVIEW-662-r3 as green 0/0 while the document ends
with «**Вердикт: жёлтый.**» and «High: 0 · Medium: 2». The §2.10
«Закрытие раунда r2» section retells the previous round as «Вердикт r2 —
зелёный, … (High: 0, Medium: 0)»; that line also starts with «Вердикт», so
the own-line regex (any ≤60 chars before a colour) and the own-line counter
both took it.
Own verdict is now tiered:
- «Вердикт:»/«Verdict:» with the colon right after the word (markup and
spaces around allowed, per the §7.2 template), plus the pipeline's own
anchor line «Вердикт конвейера: `green`» written by review-doc-guard;
- then a line merely starting with the word («Вердикт зелёный; …» in old
documents), never «Вердикт rN …»;
- the «вердикт … цвет» mention fallback also skips «Вердикт rN».
Counts: own line → «## Вердикт» section → the document's summary paragraph
starting with «High: N» (last one; a wrapped bullet continuation does not
count) → a verdict mention that is not a retelling.
Before/after over every docs/reviews/*.md: exactly one INDEX.md row
changed (#662 r3 → 🟡 0/2). Across legacy/reviews (not indexed, used as a
wider regression corpus) 15 rows changed, all the same class — «Вердикт
r1: жёлтый» / «Вердикт r2 — …» retellings no longer win over the round's
own verdict (or yield «—» instead of a foreign colour).
The colon-only rule taken literally regressed 9 docs/reviews rows whose
only own line is «Вердикт конвейера» and one legacy document with
«Вердикт зелёный;» — hence the pipeline line and the second tier, both
pinned by a test.
Mutant reviews-index-own-verdict-any-tail restores the old any-tail regex;
the guard is red on it (checked by hand). Anchors of three neighbouring
reviews-index mutants follow the changed lines.
Issue: #779
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The on-plan radar setup multiplied SpaceModel room contours by 1000 a
second time. They are render units already, so the outline was drawn at
40000… far outside the fixed "0 0 1000 1000" viewBox, and the
administrator placed mount, heading and references on an empty field.
Dropping the multiplier alone is not enough: the fixed board also cut
every room drawn outside the historical square, and the pointer mapping
ignored the letterbox and clamped presses to [0,1].
The setup now frames the plan with the main card's content frame
(docs/CANVAS.md §4, the same items through the editor host port). It
opens on core and switches to all when core would cut the configured
room; the stored view_box remains the empty-content fallback. The frame
is computed once per session. One pure projection serves every layer:
the contour is drawn as is, mount, heading, pending/reference marks and
the live trail go through plan units x NORM_W, and a press maps back
through xMidYMid meet to unclamped plan units, a press in the letterbox
placing nothing. Stored radar data, the calibration solve, Apply/Save
and View are unchanged.
Tests: a unit table for the projection and the frame (offset frame,
letterbox, negative and >1 plan units, outlier core -> all, empty-content
fallback) and an extended smoke_radar_setup (a room outside [0,1] with
view_box [0,0,1,1], vertices and bbox, known presses -> draft and DOM,
frame stability across presses, live trail and resize, Apply, Cancel,
Escape and reopen, a room without contour, an unknown room). Five
unit-guarded mutants are registered.
Issue: #774
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
night-red.mjs fell back to `https://github.com/<repo>/actions/runs/<id>` when
the API did not return html_url, and five publishing steps pushed to
`https://x-access-token:$TOKEN@github.com/<repo>`. On github.com nothing
breaks today; on any other server (GHES) the links and pushes would point to
the wrong host while GITHUB_API_URL is already honoured (#751).
- ci-proof.mjs: githubServerUrl(env) - GITHUB_SERVER_URL without a trailing
slash, github.com when unset; night-red threads it as `server` into
commentBody and nightRed, html_url from the API still wins.
- _beta-derived, _process (rebase and review document), _ship-review and
release-review: `server="${GITHUB_SERVER_URL:-https://github.com}"` and
push_url built from it; every token push uses "$push_url".
Tests: the night step executed on a non-standard server writes comment and
summary links to that host; every token push in every workflow takes its host
from GITHUB_SERVER_URL; the four publishing steps executed with
GITHUB_SERVER_URL=https://ghe.example.test push to that host, and to
github.com when it is empty. Checked: on the previous workflows and with the
hard-coded fallback restored in night-red these tests are red.
Issue: #766
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The draft trailer of #729 (PROCESS.md 11.8) was checked only by rule 10 of
process-gate, on push or in CI: validateCommitMessage returned [] for
`sha256:wrong`. The commit-msg hook now refuses, at commit time, a draft
trailer that is repeated or not `sha256:<64 lowercase hex>`. An ordinary
commit carries no such trailer and is not asked for one; the S4 epoch, the
track and the green SPEC-REVIEW still need the network and history and stay
with rule 10.
The value format (SPEC_DRAFT_VALUE) and the parser (specDraftValues: a
`Spec-Draft` line anywhere in the message, key case-insensitive) now live in
validate-commit-provenance.mjs, and process-gate uses the same function for
rule 10, so the hook rejects exactly what rule 10 would. Editor comment lines
are ignored as before. The CI provenance job runs the same validator.
Tests: wrong, short, upper-case, prefix-less and empty values and a repeat are
refused, a valid draft and an ordinary commit pass; rule 10 and the hook read
the same values; the real .githooks/commit-msg in a temporary repository
refuses both bad commits and accepts the good ones. Checked by hand: with the
check removed from validateCommitMessage all three tests turn red.
Issue: #766
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
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
A 3 s hold in kiosk mode opens the per-screen size dialog. The core card
draws it (`_renderKioskDialog`, `hp-dialog` from the core bundle, core
i18n keys) and the summary-panel runtime, which every card connects on
mount, saves it. Yet `_kioskDialog` was listed in `editorRuntimeRequested`,
so every hold on a wall tablet downloaded the whole editor chunk; with the
chunk unreachable the kiosk also showed "Could not load the editor" (#757
only capped the retries). Dropping it from the condition is the whole fix;
the loader and the gesture are untouched.
Proof: new smoke_kiosk_scale_no_editor runs a cold page, a real 3.4 s touch
hold (CDP synthesizeTapGesture: a long press, not a tap on the new backdrop),
sliders by keyboard, Reset/Close taps, a second hold to reopen, the chunk
refused by the network, and the ordinary editor entry as control. On dev it
is red in 3 network checks (chunk fetched; refused chunk = 2 requests and a
toast); with the fix all 20 checks pass. smoke_lazy_editor_chunk case (a)
pinned the old cycle and now asserts zero requests and zero notices; the
(checked by hand). New mutant kiosk-scale-dialog-loads-editor-runtime
(caught by hand), listed under browser lifecycle guards; the smoke is
linked in smoke-links because it names none of the touched members.
Issue: #763
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
A topology snapshot is labelled stale after one hour instead of five
minutes. Nothing refreshes the map on its own and a Zigbee2MQTT scan alone
can take more than ten minutes, so the old age marked almost every map. A
failed refresh still marks the retained snapshot stale at once.
Route lines and arrows now paint over battery indicators. Endpoint markers
rise above the route layer as a whole (#464), battery included, so the
overlay renders a copy of the routes one level higher (z 9, before the
captions in DOM order) clipped to the endpoint battery frames. The frames
are measured in the existing layout frame of the overlay; outside them the
picture is unchanged: endpoint cores stay above routes, captions above
routes and batteries (#792 AC7). The copy carries no data-hp, so route
counts and selectors stay as they were; parent-caption arrows update in both
layers.
Tests: smoke_device_battery_zigbee gains a local link whose route crosses a
whole battery frame — the hovered endpoint's and the neighbour endpoint's,
Flat and 2.5D, sampled along the drawn line. On dev three of the four probes
are red (2.5D neighbours are not raised today). New mutant
zigbee-route-copy-under-battery (manually red); the parent-LQI mutant anchor
follows the refactored route list. smoke_zigbee_topology_hover age fixtures
move to the one-hour boundary. The golden routes control hides both layers;
device-battery-zigbee-overlap-dark changes (arrowhead now over the battery)
and needs acceptance via ci:golden.
Issue: #808
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The volumetric settings smoke still expected the retired battery checkbox.
Assert the three radio segments in their real DOM order after the established
Display toggles.
Issue: #807
User-Visible: no
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
smoke_settings_dialog_cards took the first radiogroup of General settings as
the background segment. The battery choice of #807 is a segmented control in
the Display card above it, so the smoke now selects the group that owns the
gs-bg-mode radios.
Issue: #807
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
bundleBytes 2 712 875 → 2 715 446 (+2 571, over the 2 000 band). Clean dev
after #806 already measures 2 714 174 (+1 299 for the per-device opt-out and
the shadow, inside the band); #807 adds +1 272: the three-way mode resolver
and writer, the segmented control replacing the toggle in General settings
with its three option labels, and the batteryLowOnly option passed by View,
static card, space-card render and the device dialog preview. hostRefs
5 072 stays inside its band and is not raised.
Issue: #807
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
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
Эталон подтверждает увеличенную тень четырёх состояний и трёх размеров
непосредственно на нейтральной серой плоскости. Полная матрица: 205 сцен.
Issue: #806
User-Visible: no
Release: v1.80.0-beta.6
Baseline-Reviewed-Local: sha256:d5e785e772731545943419b719df10f952592b82bbfb65db61a6727892d8b61c