The #735 guard compared cache sizes around the warmed twelve-switch cycle.
Once an LRU is full a cold build evicts one entry and every size stays: a
union planted cold in the full pool (8/8) changed no field of the snapshot and
the guard stayed green. Its `openingWallIndex` field read
`card._openingWallIndexCache ? 1 : 0` of a Map and was always 1.
The card now counts real builds of each floor-geometry cache in its miss
branch (`_floorCacheBuilds`: wall union, inner contours, clean floors via a
`cleanFloorForRoom` hook, opening wall index, light barriers, Glow clips,
physical bodies, opening tunnels, light physical bodies) — never on a hit, a
recency refresh, a resize seed or alias, or a clear. One pure decision
(demo/performance/switch-cycle-guard.mjs) judges the window for the benchmark,
the floor-cache smoke and the unit tests: any build of a pooled family fails
and names the family and its growth; the single-slot families may rebuild once
per switch; 2.5D keeps `_isoStructuralBuildCount`. A base bundle without the
counters reads null and is not judged; a candidate must expose them. Each row
reports `switchCycleBuilds` with the size snapshot as a diagnostic (the real
index size there). The budgeted cacheEntries/cacheGrowth keep the index as the
presence flag their `openingWallIndex: 1` ceilings were set on: no budget moves.
Counted run, one sample per profile on the dev base with the counters: every
large-house* profile and isometric-stage3-dense-v1 build 0 in every judged
family, 12/12 physical bodies and opening tunnels, nothing in 2.5D; no family
moved to the reported list. The same runner against a dev bundle reports
`supported: false` and passes.
AC7: demo/smoke_floor_geometry_cache.mjs fills the pool to 8 and drops one
floor's union; a four-switch cycle builds exactly that union (+1, an eviction,
every size the same) and the decision names "wall union" — the old size
comparison sees nothing. AC1 of the smoke also reads the counters. Registry
mutant: a counter that skips the evicting miss (caught by the smoke, checked
by hand); browser inventory 245/200.
Issue: #769
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The summary panel's Total room area was memoised on the global config epoch:
any `_saveConfig()` or adopted server config — a rename on one floor, a
device or a setting — made the memo stale and the next View render ran
`cleanFloorAreaSteps` over every floor again (71 portions on large-house).
The area of a floor reads only its stored record, the model built from its
own record and constants, so each floor's value is now memoised by the
content key of those records. The key comes from the helper extracted out of
the #744 reader (`floorRecordKeyMemo`, src/floor-geometry-key.ts): one
fingerprint per floor and epoch, both records when a preview or a duplicate
id makes them differ, and independent of the shown floor. The total is the
sum of the floor values in `models` order, summed anew on every pass, so it
stays bitwise equal to a fresh `totalCleanFloorAreaM2`. A failed floor is
memoised as `null` until its record changes; the freshness check stays one
epoch comparison; the #509 portions, skeleton and the old value on screen
during a recomputation are unchanged; a lifecycle reset clears the memo.
The furniture and stairs magnet keeps its epoch key by measurement (its own
edits change the floor record anyway); the decision is recorded at the key.
Unit: test/summary-panel-area-memo.test.mjs (AC1–AC3 on a three-floor fixture
with walls, a door, a partition, a column and a stair; floors counted through
the injected geometry and portions through the step generator), and
test/floor-geometry-key.test.mjs for the shared key helper, now in the test
build. Two registry mutants (epoch key, rooms-only key); the #744 and #509
anchors follow the moved lines.
Issue: #769
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
In the 2.5D View every marker sits on z-index 2 through
`.stage.projection-iso.mode-view .dev` (0,4,0), which beats the #464
endpoint lift `.dev[data-hp-zigbee-topology-endpoint]` (0,2,0). The
hovered endpoint survived only through its higher-specificity hover
duplicate, so the unhovered neighbour endpoint stayed under the route
layer (z 7) and the line crossed its face. Flat was unaffected.
The endpoint contract is repeated for the 2.5D View at (0,5,0), next to
the #464 rules. Narrowing the 2.5D rule with :not(endpoint) was rejected:
it would reach (0,5,0) and beat the hovered non-endpoint's z-index 5.
No stylesheet order or new file changes; .oplock is untouched.
Witnesses in demo/smoke_device_battery_zigbee.mjs:
- {flat,iso}_neighbourCorePaintsOverRoutes: along the drawn local route,
inside the neighbour core (outside its battery frame) the route layers
change no pixels, outside both cores they do. Red on dev in 2.5D
(16 of 16 core samples changed), green here (0 of 16).
- iso_nonEndpointKeepsBaseLayer / iso_hoveredNonEndpointKeepsHoverLayer:
computed z-index 2 for unlinked markers and 5 for a hovered marker
outside the topology; the :not(endpoint) variant turns the latter red.
Mutant zigbee-iso-neighbour-endpoint-under-routes removes the new rule;
the browser guard is listed in the inventory (paint 44, total 248).
Issue: #809
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Code review r1 of #778 (Medium, in scope): point 3 of the brief asks to hand
the concrete product optimization to a product task, and the closing comment
pointed at a findings document that does not resolve from the repository.
The owner's directive for this wave forbids filing new issues, so the two
candidates are written down where the next reader of a failure looks: the
live-label area union in _rszEdgeLabels (about 24 % of the task) and the
whole-floor masonry union of the live preflight (about 70 %, BigNumber in
polyclip-ts about 29 % of the self time), with the measurement they come
from. The same entries sit in the owner's findings document (F23) for
triage. No product code, budget or ceiling changes.
Issue: #778
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Validate on #762 failed once on longTask.editorSeries.maxSingleMs (135/129/163
against 150 ms) and passed on the re-run (127/132/132) without a change. The
report carried only the task's maximum and the physical preflight time, so
nobody could tell whether the extra time was the preflight, the rest of the
resize step, or something else on the runner.
The investigation found no measurement defect. The resize part's single Long
Task is the task of the one accepted pointer move: ResizeController.move with
the live physical preflight inside project, publish and the live labels. No
Lit update, forced layout or paint is in it (Long Animation Frames and a
Chrome trace show the live paint and the frame's layout in the next rendering
step); pointerdown stays in an earlier task. Across 202 CI samples (14 Full
Performance runs and both Validate attempts) the task minus the preflight is
20-47 ms with a within-report sigma of at most 2.2 ms; the failed sample had a
normal preflight and +25 ms elsewhere, plus extra Long Tasks in its load and
space-switch windows: one slow sample of the whole page on top of a level that
sits 3-25 % under the ceiling.
Each interaction row now reports interactionDiagnostics.resizeLongTask: the
judged task split into preflight, projection, publish, labels, the rest of the
move, Lit updates and other time, plus the following frame's render and any
forced style/layout from Long Animation Frames. The split is computed by a
self-contained function from the entries of the very window the gate judges,
and the wrappers and observer are installed before the runner's yield, so
nothing new runs inside the measured task. A base without the
ResizeController.move callback shape reports supported: false with a reason
instead of zero shares; the contract declares _resize.move as an optional
method. The runner prints one line per sample for the job log. No budget,
window or product code changes; the README records the series, the method and
how to read the next failure.
Issue: #778
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
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