The #744 key formats were copied: the wall-union key `${floorKey}|${rooms}`
in the card's miss branch and three times in the resize runtime, the
physical-bodies key `${floorKey}|${cellCm}|${gridPitch}` in the card, the
runtime twice and the LED editor, the pool bound 8 as a literal three times
and the `sourceFingerprint` attachment twice. A copy that drifts makes the
resize lookup and the cancel alias stop hitting without any error.
src/floor-geometry-key.ts now owns `wallUnionKey`, `physicalBodiesKey`,
`WALL_UNION_POOL_LIMIT`, the entry factory `wallUnionPoolEntry` (it attaches
`lightGeometryFingerprint` of the record the union was built from) and
`writeWallUnionPool`. The card's miss branch, `_rszEdgeDown`, the
`_rszCancelDrag` alias and both bodies re-keys use only them; the LED editor
keys its own bodies with the same helper. `_rszEdgeDown` now reads the key
of the shown floor's model, as the card does, instead of its own room list.
The union seed in `_rszAcceptPreview` is removed, not moved: since #451 the
live preflight publishes no artifact (`artifact: null`), so the seed never
ran. A host render mid-drag builds the preview union once through the card's
miss branch and every later render hits it. The bodies re-key stays: a
resize never moves partitions, columns or a partition opening's host, and
the smoke proves the re-keyed bodies equal a fresh card's on the preview
record, a door cut into a partition next to the moving wall included.
AC8 in demo/smoke_floor_geometry_cache.mjs on the held AC2c drag: (a) host
renders mid-drag build no union and the drawn union equals a fresh card's on
the preview record, paths and `sourceFingerprint`; (b) cancel and the way back
to View build no union, bodies, contours or clean floors, and the union and
bodies equal a fresh card's on the stored record; (c) the drag never rebuilds
the bodies. Unit: the key formats, the bound and the entry
(test/floor-geometry-key.test.mjs) and a source contract that no copy of the
formats, the pool write or the fingerprint attachment is left outside the
module (test/performance-contract.test.mjs, red on dev). Two registry mutants
(bodies re-key in its own format, union fingerprint from the stored record),
caught by the smoke and checked by hand, as were the two #744 mutants whose
anchor moved; browser inventory 247/200.
Budgets: initial View 303032 B gzip (ceiling 302803 + 2000), lazy editor
245094 B (ceiling 244872 + 2000); monolith ratchet unchanged.
Issue: #769
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
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
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
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
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