После первого движения камеры переносит фильтр контура на ограниченный размером viewport слой, сохраняя исходный статичный рендер без изменений.
Issue: #582
User-Visible: no
Режим заливки комнаты и её цвет — два поля конфига под одним переключателем.
«Как у пространства» снимало только режим; цвет оставался и продолжал
применяться, потому что `roomCustomFillOf` отдавал цвет комнаты независимо от
того, чей режим `custom` действует. Так возникало безымянное состояние «режим
наследую, цвет свой» — Cabinet на даче.
Теперь цвет комнаты участвует в раскраске только вместе с её собственным
`fill_mode: 'custom'` (одна функция — все поверхности: карточка, space-card,
PDF, черновик диалога). Диалог загружает цвет в черновик только при своём
режиме, обнуляет его при уходе с «Свой цвет» и показывает строку цвета только
под этим радио; сохранение пишет `custom_fill` только с `fill_mode: 'custom'`,
иначе удаляет — включая сироту от прежнего редактора. Чтение конфиг не
переписывает: застрявшие комнаты выздоравливают обновлением.
- `test/logic.test.mjs`: AC1 — сирота и любой чужой режим → цвет пространства
- `demo/smoke_room_settings.mjs` шаг 7: свой цвет → «Как у пространства» →
ни режима, ни цвета, во View цвет пространства; сирота открывается как
наследование, сохранение её удаляет (проверено красным на базе: 9 фактов)
- `demo/smoke_space_settings.mjs`: override с собственным режимом + сирота
- `demo/golden/harness.mjs`: `roomCustomFill` ставит комнате её режим —
кадры `lighting-custom-glow-*` не меняются
- мутант `room-orphan-colour-wins-again`
- docs: ARCHITECTURE (#56), CONFIG-COMPATIBILITY, USER-GUIDE ru/en, TESTING;
отпечаток скриншотов принят попиксельно (11 кадров)
Issue: #581
User-Visible: yes
Новый Linux-кадр закрепляет две нарисованные части внешнего луча: физический тоннель окна и продолжение по чистому полу без шва. Два help-кадра ожидаемо включают новый глобальный селектор #577. Safe Resize отличается только стабильным растровым сглаживанием на 895 из 1062000 пикселей; геометрия и состояние интерфейса визуально совпадают.
Issue: #577
User-Visible: no
Release: v1.76.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34901679608
Accept the 13 visually reviewed Stage 4 isometric frames from the complete Linux Validate artifact. The other 156 baselines remain unchanged and 101 environment witnesses match.
Issue: #570
User-Visible: no
Release: v1.76.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34853080375
Полевая проверка основного View по #560: шесть обезличенных планов корпуса и
цепочка import → Optimize → Optimize → Resize → сохранение с численными
оракулами, семь household-путей с независимыми оракулами в двух ширинах
карточки, аудит доступности с измеренными числами. Продуктовых правок нет:
находки заведены отдельно (#564, #565, #566).
Issue: #560
User-Visible: no
Run 34760156615 exposed stale registry witnesses under the honest #550 outcome taxonomy. Keep behavioural checks out of setup chains, retarget the preflight mutation to the editor host, and make the junction cache smoke exercise same-object in-place geometry changes.
Release: v1.76.0-beta.1
Issue: #550
User-Visible: no
К2 задачи #532 назвала это заранее: промоушен поверхности контура в свой
композиционный слой делит SVG надвое, оставшееся содержимое ложится на другую
субпиксельную сетку, и диагональная штриховка стен сглаживается иначе. Кадры
полагалось пересмотреть на кандидате — они дожили до кандидата стабильного,
потому что golden-джоба гоняется только на дереве с трейлером `Release:`, а
мерж #532 такого трейлера не нёс.
Проверено по критериям AC3 самой #532: разошлись **ровно четыре** кадра
`day-cycle-{dawn,day,dusk,night}-dark`, средний цвет кадра сдвинулся на
0,001–0,080 из 255 при пороге 0,1, средний знаковый сдвиг на разошедшихся
пикселях от −0,19 до +0,87. Кадры просмотрены глазами: отличается только
сглаживание штриховки стен, палитра, геометрия, подписи и ореол те же.
Приняты из артефакта полного прогона Linux CI на этом же дереве, остальные
165 сцен сохранены без изменений, свидетелей среды 129.
Issue: #532
User-Visible: no
Release: v1.74.0
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34641155082
#525 увёл оба списка сцены с `map()` на `repeat(items, (item) => item.id, …)`,
чтобы Lit не переиспользовал узлы по позиции и не проигрывал анимацию двери,
которой не было. Правка верная, но на плане с двумя сотнями маркеров она
оказалась дорогой ровно там, где пользы не приносит: при смене пространства
ключи не пересекаются вовсе, и `repeat` строит две карты ключей и обходит оба
списка, чтобы затем всё равно выбросить всё и создать заново.
Бисект по медиане `switchCycleMs` на `large-house`: 871,2 перед #525 → 953,5
после. На раннере эти 80 мс распадаются на шесть-девять дополнительных длинных
задач, и `longTask.countP95` вышел за порог стабильного гейта.
Теперь оба списка рендерятся как `keyed(space.id, repeat(…))`. Внешний ключ
делает смену пространства: поддерево выбрасывается целиком, дифа нет. Внутренний
остаётся, потому что состав списков едет и внутри пространства — у маркеров от
призраков редактора и живого синка, у проёмов от записи с нерешённым хостом,
которая живёт только в режиме plan, — а переходы на `.device-shell-frame`,
`.op-leaf` и `.op-arc` никуда не делись.
Замер после правки: `switchCycleMs` 889,1 против 936,0 на `main` и 871,2 до
#525. Потолок карточки поднят на одну строку — на `import { keyed }`;
переносить нечего, сам рендер не вырос ни на символ.
Issue: #534
User-Visible: yes
The "Follows the sun" background made the plan crawl in Firefox: 23 MB of
textures per frame, sixteen of about twenty picture-cache tiles thrown
away every frame, nine frames per second. The card's own JavaScript was
idle for 89 % of that.
The cost is the outer outline. It is a triple drop-shadow over the
grouped paper footprint, and although the group holds paper silhouettes
only and never changes on hover or pan, the filter lived in the plan's
own layer — so every repaint of the plan re-ran three blur passes over
the whole sheet. One hint moves the filtered paper into its own layer
and unhooks it from the plan's repaints.
Measured on a demo-stand pan (Chromium, CDP, summed RasterTask): 1456.6
ms against a static background's 96.7 ms before, 84.7 ms against 103.2
ms after. The new smoke measures that ratio and fails above two.
The picture does not change: the outline outside the plan matches byte
for byte and the frame's mean colour moves from 176.59 to 176.66 of 255.
Splitting the layer does move the remaining content onto a different
sub-pixel grid, so the diagonal wall hatch anti-aliases differently and
the four day-cycle baselines are re-taken on the beta candidate.
Issue: #532
User-Visible: yes
Medium: две обёртки унаследовали имя соседа. Блок `aria-disabled`-ручки звался
`owner_boundary`, а блок с проверками `owner_boundary_*` — `range_role`, имени,
которого нет ни у одной проверки. Логика при этом верна, страдает ровно то,
ради чего правка и делалась: чтение красной строки с раннера.
Теперь `disabled.*` и `owner_boundary.*` стоят на своих блоках. Смок зелёный.
Issue: #533
User-Visible: no
Medium: обёрнуты были три вызова из двадцати двух — главный сценарий. Остальные
девятнадцать по-прежнему молча отбрасывали возвращаемое значение, и жест,
не доехавший до ручки, оставлял смок зелёным.
Теперь `sent()` стоит на каждом вызове, имя проверки называет сценарий и тип
события. Отрицательный прогон: подмена `cx` на несуществующую ручку в
`mixed_role` красит ровно `mixed_role.pointerdown_sent`.
Issue: #533
User-Visible: no
Смок считал экранные точки один раз, заранее, а карточка переводит их обратно в
момент события — от текущего размера стейджа и текущего вида. Стоило раскладке
осесть между замером и жестом, и 34 экранных пикселя превращались уже не в 50
единиц плана: ресайз коммитил не ту величину, а свидетель сообщал об этом
четырьмя немыми `expected true, got false`. Раннер это ловил, локальная машина —
нет, и красный гейт закрыл выпуск v1.74.0.
Теперь координаты передаются в единицах плана, а перевод живёт внутри того же
кадра, что и отправка события. Доставка события проверяется (прежде
возвращаемое значение хелпера для `pointermove` игнорировалось молча), а
устойчивость отображения — отдельной проверкой `safe_resize.mapping_stable`:
масштаб на захвате и на движении обязан совпасть, иначе в имени проверки
печатаются оба масштаба и оба размера стейджа.
Продуктовый код не тронут: ни один из экспериментов не указал на дефект
ресайза. Если `mapping_stable` когда-нибудь покраснеет на раннере — это и будет
доказательством обратного, с числами в первой же строке.
Issue: #533
User-Visible: no
Golden-джоба запускается только на кандидате с трейлером `Release:`, поэтому
Validate на мерже #530 её не гонял, и первый же релизный прогон
v1.74.0-beta.3 показал расхождение ровно в трёх сценах экспорта PDF:
`pdf-export-geometry-light`, `pdf-export-polish-light`,
`pdf-export-stepped-dimensions-light`.
Расхождение — это и есть предмет #530: план на листе крупнее, подписи
размеров стоят на чертеже, столбца выносов сбоку больше нет
(`sceneCoverage` 0.577). Кадры приняты с явным `--expect-change` из
артефакта того самого прогона (отпечаток исходников совпадает),
остальные 166 сцен сохранены без изменений, свидетелей среды 133.
Issue: #530
User-Visible: no
Release: v1.74.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34597337319
Перезапись `viewBox` — это не сдвиг, а инвалидация растеризации всей сцены.
Кадр жеста делал её каждый раз: в профиле владельца (Firefox 155, 144 Гц) кадр
доезжал до экрана 200 мс, а драйвер пропускал 124–144 тика в секунду с пометкой
«ждём краску».
Теперь `paintLiveViewport` держит якорь — кадр, чей `viewBox` записан в DOM, и
момент записи. Кадр жеста двигает узлы сцены тем же проективным преобразованием,
которым уже двигались HTML-слои, а `viewBox` переписывается по бюджету: 100 мс
либо 15 % сдвига/масштаба. Ни атрибут, ни стиль не пишутся, если строка не
изменилась.
Issue: #531
User-Visible: yes
It looked for the tag written as `css` immediately followed by a
backtick. The plugin is a Rollup transform, so the module has already
been through TypeScript by the time it arrives, and the TS printer puts
a space there: `css `. The guard therefore returned null for every
stylesheet in the project, and minification never ran once — around
23 KB of explanatory comments went to every user in every release.
Matching the tag as a word with optional whitespace turns it on:
chunk, raw 1 079 508 -> 1 021 115 B (-58 393)
initial view 300 111 -> 287 284 B gzip (-12 816)
room to the budget 955 -> 13 782 B
The ceiling moves down with the fact, as the tool asks when a graph
shrinks past the band.
The risk is not the two lines; it is that 23 KB of CSS is minified for
the first time. Two witnesses cover it: a browser smoke that puts the
original and the minified text into separate stylesheets and compares
the serialised rules — 1 049 of them, identical up to the whitespace
policy the minifier declares — and a test that takes real comment text
out of src/styles and requires it to be absent from dist, so a plugin
that silently stops working cannot pass again.
Issue: #526
User-Visible: yes
The marker half of the space-switch witness asked whether a box-shadow
transition was running on the shell. That worked only because such a
transition existed; #524 removed it — the shadow is sized in container
units and animating it cost a real user 9.4 frames per second — and the
check became trivially true. The mutant that removes the keys from the
marker list has been surviving ever since, and nobody noticed until the
next gate ran it.
The witness now keeps references to the marker nodes and requires that
none of them stays in the DOM under a different data-id after the
switch. Node identity is what the keys are for, and it does not depend
on any stylesheet.
The door half is untouched: there the transition is part of the product
contract, not a side effect.
Issue: #528
User-Visible: no
The shadow of a device marker is sized from the marker, and the marker is
sized from the container: --device-shell-shadow is expressed in
--dev-size, which resolves to 2.5cqw. With box-shadow in the transition
list, every container-query re-evaluation — a tooltip, a scrollbar, a
rotation — produced a new computed value and restarted a 150 ms
non-composited transition on every marker at once.
The owner's Firefox profile shows what that costs: 244 box-shadow
transitions, all on span.device-shell-frame, all oncompositor:false, in
bursts of exactly 61 (the markers on screen), four bursts in two
seconds. During them the tab presented 103 frames in 11 seconds — 9.4
per second, CONTENT_FRAME_TIME median 149 ms and up to 320 — while the
refresh driver waited for paint 381 times. Our JavaScript in the worst
three seconds: 16 ms. Chromium starts the same transitions (measured:
one pixel of container width starts two per marker in both engines) and
merely pays less for them, which is why this hid there.
The shadow itself is unchanged; it simply applies at once. The core
keeps the same treatment, so the selection and focus rings appear
without a fade — an instant ring is ordinary feedback, a faded one costs
a full-frame repaint per marker. Hover still animates border-color.
Issue: #524
User-Visible: yes
Lit reuses list nodes by position. The opening list and the device markers had
no keys, so on a space switch the leaf that held a slot kept its DOM node and
only changed values — and `.op-leaf` (transform) and `.op-arc`
(stroke-dashoffset) carry a 0.6 s transition, so the browser animated a door
that never moved: the new floor's leaf drove in from the previous floor's
opening angle. Measured on two spaces with a door in the same place and
opposite contact states: the node is reused, transform goes
`rotate(-90deg) → rotate(0deg)`, dash offset `0 → 125.66`, both transitions
`running`. The marker shell adds two more with its `box-shadow`.
Both lists are now rendered through `repeat(…, (item) => item.id, …)`, the
same lesson `glow-scene.ts` already learned for the Glow spots. The trap is
written where it starts — above the two transitions in `plan.styles.ts` —
because that is the file someone edits when adding the next animated property.
`houseplan-card.ts` is at its line ceiling, and the note would have cost the
budget a dozen lines for nothing: the swap itself is line-for-line.
The witness walks the shadow tree per element. `document.getAnimations()` is
empty here EVEN ON THE BROKEN CODE — the card lives in a shadow root and the
document-level call does not reach into it, and the issue proposed exactly
that call. The smoke also builds its own fixture: the demo home has no
openings at all, so two doors in two spaces are prepared in the smoke, and it
asserts the other half of the contract as well — a real contact change inside
one space still animates the leaf.
On `origin/dev` the smoke fails on five facts, naming the offenders:
`op-arc:stroke-dashoffset`, `op-leaf:transform`, `device-shell-frame:box-shadow`
twice. Mutants `openings-rendered-without-keys` and
`device-markers-rendered-without-keys` put each `map` back.
Perf, 7 samples against `19e421b3`: spaceSwitchMs 524.8 (limit 769.35, base
512.9), switchCycleMs 1293.9 (1696.28, 1256.5), firstStableRenderMs 2533.8
(3000, 2529.2), modelReadyMs 732.7 (944.97, 726.9), longTask.maxSingleMs 663
(910, 660) — `benchmark:compare` green in full.
The initial View graph grows 241 B gzip: `repeat` enters it for the first
time. The #438 ceiling is recentred 300 400 → 300 700 with the usual dated
note; measured 300 059 B keeps 641 B above and 1 359 B below the band. The
301 066 B budget is untouched, but only 366 B now separate the ceiling from
it — the #367 headroom debt has stopped being theoretical.
Issue: #525
User-Visible: yes
Code review r1 was right that AC5's evidence was empty: the smoke never
forced a settled render during a gesture, so the settled copy of
`.hp-editor-only-layer` was empty at every point it looked, and
`groups() === 1` held whether the copy was hidden or not.
Measuring the case the reviewer named turned up more than a weak assertion.
Ownership of the layer alternates on its own — every settled render ends in
`updated()` → `_commitLiveEditor()`, which empties the live root — so a
settled render mid-gesture takes the guides back and draws them itself, from
the same live `_alignPoint`. That much needs no suppression. But the copy it
leaves behind stays in the settled scene, and the NEXT live paint adds a
second one: measured two `.alignline` on one alignment, the settled one a
grid step behind the marker. So the suppression stays, and now it stays with
a witness.
The smoke counts what is visible, not what is in the DOM: the hidden copy is
still a node, and counting nodes is how this check could have looked green
while showing the user two lines. Its device scenario now drives the whole
handover — force an unrelated settled render mid-drag (`_hdrH`, the same
header-height observer that masked the defect in the S2 measurements), assert
the render actually happened, that the layer went back to the settled scene
with the live point on it, and that one real move later the live painter owns
it again — exactly one visible guide at every step.
Mutant `live-editor-keeps-the-settled-guides-visible` puts the suppression
back under the plan branch, as it was before this issue, and the smoke goes
red on `nextMoveTakesTheLayerBack`.
Issue: #521
User-Visible: no
#451 moved every editor gesture onto the live painter, and the guides stayed
behind in the settled scene. While a gesture runs, the settled scene is not
re-rendered at all, so the guides did not follow the marker in the device
editor, the shape in the backdrop editor, or the cursor while a contour is
drawn in the plan editor. Measured with real pointer events on the demo stand
against `origin/dev`, after waiting for the editor chrome to settle: three
gestures, each exactly on another object's axis, 0 settled render cycles,
`.alignline` 0 and no `.alignguides` group in all three.
The report called it two breaks. It is one — the layer — plus one thing that
would have broken the repair: `_alignPoint` read `_pos`, which during a live
gesture answers from the snapshot of the last settled render. Over one drag:
live 254.17 → 220.83 while `_pos` stayed at 254.17, eight grid steps behind,
so a restored layer would have drawn the guide at the marker's old place.
The live template now paints the guides in all three modes (the device editor
had no template at all — `paintDevice` only moves the marker element), and
`_alignPoint` takes the live position. The settled copy of
`.hp-editor-only-layer` is made transparent for the duration of any editor
gesture, not only in plan mode: two guides, one of them stale, is what the
user would otherwise see when an unrelated settled render lands mid-gesture.
`_renderAlignGuides` on the card becomes soft — a gesture that starts while
the editor runtime is still loading must cost nothing, and an exception inside
a `requestAnimationFrame` paint would take the whole gesture with it.
The witness is rewritten around the defect that hid this for two stable
releases: the old smoke assigned `_deviceDrag`/`_decorDraft` wholesale, and an
assignment with `oldValue == null` does not route to the live path — it
verified a state a real gesture never reaches. Every scenario now drives real
`PointerEvent`s, waits for silence first (the `_hdrH` settling window right
after entering a mode hands out settled frames that make even the broken code
draw a guide), and asserts zero settled cycles during the movements plus
exactly one `.alignguides` group. #400's exclusion is checked without touching
the drag state: the dragged marker must simply be absent from the candidates.
On `origin/dev` the smoke fails on nine of its facts; a witness that stays
green before the fix was the actual bug here.
Mutants: `live-editor-devices-drops-align-guides`,
`live-editor-decor-drops-align-guides`, `live-editor-plan-drops-align-guides`,
`align-point-reads-frozen-snapshot` — one per AC, all guarded by the smoke.
`test/smoke-harness-contract.test.mjs` pins that the smoke cannot go back to
fabricating gesture state.
Issue: #521
User-Visible: yes
#500 gave `_serverCfg` and `_layout` prototype accessors but left them in
`static properties`. Lit marks such a property `wrapped` and, on the FIRST
update, force-writes it into `changedProperties` with an `undefined` old
value even though nobody assigned anything (`reactive-element.js:249-252`
and `:880-886`). `willUpdate` reads that as a config replacement, raises
`_cfgEpoch`, the memoized model key changes, and a 60-room house builds and
paints its model a second time: measured 19 update cycles, 4 builds and 4
epochs against 18 / 3 / 3 before #500, worth ~550 ms of `modelReadyMs` and
the same on `firstStableRenderMs` (3355 against a 3000 ceiling).
The declaration goes; the bodies stay reactive through the owner —
`_adoption` → `onBodyReplaced` → `requestUpdate(field, previous)` — which
needs no declaration: `getPropertyOptions` falls back to the default and
`changed.has('_serverCfg')` works as before. `noAccessor: true` would not
help, `wrapped` is set before that flag is read. The trap is written above
`static properties`, where someone would put the declaration back.
`cache.entries.cleanFloor` returns to 100 in both interaction budgets: the
120 entries were the extra epoch re-keying the per-room cache, not a
property of the design — the reasoning in 914e8402 was wrong.
Witness: test/config-adoption-ownership.test.mjs pins that neither body is
declared; the mutant `adoption-bodies-declared-reactive` puts the
declaration back and reddens it.
The boot diagnostics of the previous three commits touch four private
members, so they are declared in the performance contract: `_buildModel` and
`_cfgEpoch` outright (both exist in every supported comparison base), and the
adoption entry point as a current/legacy pair — #500 turned the private
`_adoptStructuralResponses` into the public `_adoptAuthoritative`, and an
undeclared rename would have the counter report zero adoptions instead of
failing.
The same commits carried a `node_modules` symlink: `.gitignore` had the
pattern with a trailing slash, which does not cover a symbolic link, and
`git add -A` in a sandbox worktree committed it. The link is removed and the
pattern loses the slash; a mutant run on this branch failed with `EEXIST` on
it.
Issue: #520
User-Visible: no
The comparison already names the mechanism: base does 18 update cycles, 3
model builds and ends at epoch 3, the candidate does 19, 4 and epoch 4, and
the extra build is the whole ~550 ms. What is still missing is the caller.
The diagnostic now installs an instance-level setter over `_cfgEpoch` and
records `from->to` with the top stack frames, so the extra bump names itself.
Issue: #520
User-Visible: no
The first attempt printed them with console.log inside page.evaluate, and
nothing forwards the page console to Node — the numbers went nowhere. The
sample now returns `bootDiag`, the runner prints it and strips it before the
row is recorded, so the budgeted record keeps its shape.
Issue: #520
User-Visible: no
The full comparison says model readiness grew by ~500 ms inside #500 and
that the growth sits in one long task, but neither contentFingerprint
(2.8 ms on this fixture) nor spaceModels (0.1 ms) can account for it. The
benchmark now prints, per sample, how many Lit update cycles ran before the
first stable frame, how long they took together, how many models were built,
how many adoptions happened and the config epoch. Diagnostics only: printed
to the log, never part of the budgeted record, and the same harness runs
against the comparison bundle, so candidate and base are counted alike.
Issue: #520
User-Visible: no
The pre-release perf gate of the v1.74.0-beta.1 candidate reported
cache.entries.cleanFloor 120 against a ceiling of 100 (run 34480302982,
large-house-interaction-v1). The ceiling was calibrated when only the visible
space populated `_cleanFloorCache`; since #509 the summary panel computes the
clean-floor total in per-room slices through the same cache, so the sixty
fixture rooms are cached under both config epochs the interaction profile
creates — 60 × 2 = 120, exactly what the run measured.
The ceiling moves to 180 in the smoke and in the full interaction profile:
one entry per room per epoch with room for a third epoch, far below the LRU
cap of 600 and far below anything a per-frame or per-marker regression would
produce. The leak detector is untouched: cacheGrowth.cleanFloor stays 0.
Issue: #509
User-Visible: no
Release: v1.74.0-beta.1
Review r2 M1. The smoke's reset() left the previous scenario's debounced
config/layout write pending; _deleteSpace flushes whatever is pending
before it writes, the fake socket answers without a rev, and the
documented rev+1 fallback then moved the revision on a body from another
scenario. The refused branch looked as if it had adopted:
onboardingDeleteRefusedAdoptsNothing was red on every run. The scenarios
are supposed to be independent, so reset() now cancels both debounced
writers, as smoke_danger_confirmation already does.
37/37 green, three runs in a row; with the cancel removed the same
single check goes red again.
The refused-tail fix from r1 had no witness in CI at all: no mutant
named this smoke as its guard, so the review gate never ran it and a red
witness survived a whole round. A witness that never runs is not a
witness, so the early return in _undoPlanOptimization now has a mutant
that names the smoke.
Issue: #500
User-Visible: no
`space/delete` (both runtimes), Optimize Undo and Import apply now treat
`asset-wait` like every reload path: nothing was adopted, so no toast, no
space switch, no history/undo reset — the dialog is released and the
scheduled reload owns the rest. Unit and smoke cover the refused branch for
all four paths; the spec's reactivity risk row states the real mechanism.
Issue: #500
User-Visible: no
`test/config-adoption-ownership.test.mjs` pins identity writes to the owner
and ratchets body staging (AC1/AC2). `demo/smoke_post_write_adoption.mjs`
drives space/delete (both runtimes), Optimize Undo and Import apply with a
concurrent backdrop change between the write and the re-read (AC4). Smokes
that seed revisions from outside the card keep working through the
`seedIdentity` harness seam behind the card's delegate setters. The initial
View ceiling is re-centred with the measured fact; ARCHITECTURE.md gets the
boundary paragraph.
Issue: #500
User-Visible: no