Три состояния владельца позиции вместо двух

Ревью r1, Medium: наблюдение о записи на удалённом пространстве говорило
«владелец жив» и про ключ, который ни во что не резолвится. Это заявление о
доказанности там, где её нет: продукт в этом состоянии ничего не удаляет не
потому, что владелец жив, а потому, что не знает — `space-reference-repair`
ведёт `live`, `absent` и `unverified`, три состояния, а не два.

Проверка ведёт те же три. Нерезолвящийся ключ получает наблюдение
`unknown_owner` с той же формулировкой, что и в живом пространстве: «владелец не
найден в конфигурации (возможно устройство HA)». Тест сверяет теперь и ТЕКСТ
причины — вид наблюдения без текста эту асимметрию пропускал, ровно так она и
проехала.

Issue: #566
User-Visible: no
This commit is contained in:
Codex
2026-09-14 02:28:50 +03:00
committed by Codex
parent 9cb1f4be6c
commit 733cd5b86b
4 changed files with 59 additions and 20 deletions
+22 -8
View File
@@ -212,21 +212,35 @@ export function checkReferences({ config, layout = {} } = {}, { notes = [] } = {
// пользователя. Значит конфиг, только что прошедший Optimize, законно
// содержит такие записи, и объявлять их нарушением — врать про модель.
//
// Судим тем же правилом: нарушение только там, где владелец ОТСУТСТВУЕТ
// по самой конфигурации. Остальное — наблюдение, как и у ветки
// `unknown_owner` ниже: проверка с ложными срабатываниями умирает первой.
// Судим тем же правилом и ТЕМИ ЖЕ ТРЕМЯ состояниями, что продукт: `absent`
// (нарушение), `live` и `unverified` (наблюдения). Разница между двумя
// последними — не педантизм: сказать «владелец жив» про ключ, который ни
// во что не резолвится, значит заявить доказанность там, где её нет, и
// следующий разбор поверит записи вместо проверки (r1 Medium).
const roomOwner = key.startsWith('rl_') ? key.slice(3) : '';
const areaOwner = key.startsWith('grp_') ? key.slice(4) : '';
const ownerGone = roomOwner ? !allRoomIds.has(roomOwner)
: areaOwner ? !allAreas.has(areaOwner)
: removedMarkerIds.has(key);
if (ownerGone) {
const owner = roomOwner
? (allRoomIds.has(roomOwner) ? 'live' : 'absent')
: areaOwner
? (allAreas.has(areaOwner) ? 'live' : 'absent')
: removedMarkerIds.has(key) ? 'absent'
: activeMarkerIds.has(key) ? 'live'
: 'unverified';
if (owner === 'absent') {
add('layout_space', key, space,
'пространства не существует, и владельца позиции тоже нет');
} else {
} else if (owner === 'live') {
notes.push({ invariant: 'references', kind: 'stale_layout_space', owner: key,
reference: space,
detail: 'пространства не существует, владелец жив — Optimize хранит позицию намеренно' });
} else {
// Тот же факт, что у ветки `unknown_owner` ниже, и та же формулировка:
// владельца в конфигурации не видно, отличить устройство HA от мусора
// по одному конфигу нельзя. Продукт в этом состоянии тоже ничего не
// удаляет — но не потому, что владелец жив, а потому, что не знает.
notes.push({ invariant: 'references', kind: 'unknown_owner', owner: key,
reference: key,
detail: 'владелец не найден в конфигурации (возможно устройство HA); пространства тоже нет' });
}
continue;
}
+18 -4
View File
@@ -80,8 +80,20 @@ const MUTANT_DEFINITIONS = [
+ 'после Optimize — и следующий разбор начнётся с ложного сигнала',
patches: [{
file: 'scripts/model-invariants.mjs',
find: ' const ownerGone = roomOwner ? !allRoomIds.has(roomOwner)',
replace: ' const ownerGone = true || roomOwner ? !allRoomIds.has(roomOwner)',
find: " if (owner === 'absent') {",
replace: " if (owner === 'absent' || true) {",
}],
},
{
id: 'invariants-claim-proof-for-an-unknown-owner',
guard: 'node --test --test-name-pattern="#566" test/model-invariants.test.mjs',
because: '#566 r1: ключ, который ни во что не резолвится, и живой маркер — разные '
+ 'состояния; общая формулировка «владелец жив» заявляет доказанность там, где её нет, '
+ 'и следующий разбор поверит записи вместо проверки',
patches: [{
file: 'scripts/model-invariants.mjs',
find: " : activeMarkerIds.has(key) ? 'live'\n : 'unverified';",
replace: " : 'live';",
}],
},
{
@@ -92,8 +104,10 @@ const MUTANT_DEFINITIONS = [
+ 'отключение проверки',
patches: [{
file: 'scripts/model-invariants.mjs',
find: ' : removedMarkerIds.has(key);',
replace: ' : false;',
// Файл на JS, поэтому литеральная ложь здесь безопасна: сужения типов,
// которые ломает статически мёртвая ветка в .ts (#568), тут нет.
find: " : removedMarkerIds.has(key) ? 'absent'",
replace: " : false ? 'absent'",
}],
},
{
+4 -1
View File
@@ -250,7 +250,10 @@ const CORPUS = [
// хранит их намеренно, а инварианты сообщают о них НАБЛЮДЕНИЕМ. Нарушением
// была бы только запись, чей владелец отсутствует по самой конфигурации.
expectedViolations: [],
expectedNotes: ['references/stale_layout_space', 'references/stale_layout_space'],
// Владельцев этих двух ключей по конфигурации не видно, поэтому наблюдение
// именно `unknown_owner`: «владелец жив» здесь было бы заявлением о
// доказанности, которой нет (r1 Medium).
expectedNotes: ['references/unknown_owner', 'references/unknown_owner'],
condition(space, layout) {
const known = new Set([space.id]);
const orphans = Object.values(layout).filter((entry) => !known.has(entry.s));
+15 -7
View File
@@ -150,14 +150,22 @@ test('#566: на удалённом пространстве нарушение
'layout_space:m_gone',
'layout_space:rl_room_never_existed',
]);
// Три состояния, а не два: продукт различает `live` и `unverified`, и
// проверка обязана различать их тоже. Сказать «владелец жив» про ключ,
// который ни во что не резолвится, — заявить доказанность там, где её нет
// (находка r1). Поэтому сверяется и вид наблюдения, и ТЕКСТ причины: вид без
// текста эту асимметрию пропускает — именно так она и проехала.
assert.deepEqual(notes.filter((n) => n.kind === 'stale_layout_space')
.map((n) => n.owner).sort(), [
'980f1446c4ec1a3a9fa9ff5f6d93caed', 'grp_kitchen', 'm1', 'rl_r1',
]);
// Наблюдение обязано называть причину, иначе читатель решит, что проверка
// просто ослабла.
assert.match(notes.find((n) => n.kind === 'stale_layout_space').detail,
/Optimize хранит позицию намеренно/);
.map((n) => n.owner).sort(), ['grp_kitchen', 'm1', 'rl_r1']);
assert.deepEqual(notes.filter((n) => n.kind === 'unknown_owner')
.map((n) => n.owner), ['980f1446c4ec1a3a9fa9ff5f6d93caed']);
for (const note of notes.filter((n) => n.kind === 'stale_layout_space')) {
assert.match(note.detail, /владелец жив — Optimize хранит позицию намеренно/);
}
for (const note of notes.filter((n) => n.kind === 'unknown_owner')) {
assert.match(note.detail, /владелец не найден в конфигурации/);
assert.doesNotMatch(note.detail, /владелец жив/);
}
});
test('позиция устройства без записи маркера — наблюдение, а не нарушение (#254)', () => {