mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Три состояния владельца позиции вместо двух
Ревью r1, Medium: наблюдение о записи на удалённом пространстве говорило «владелец жив» и про ключ, который ни во что не резолвится. Это заявление о доказанности там, где её нет: продукт в этом состоянии ничего не удаляет не потому, что владелец жив, а потому, что не знает — `space-reference-repair` ведёт `live`, `absent` и `unverified`, три состояния, а не два. Проверка ведёт те же три. Нерезолвящийся ключ получает наблюдение `unknown_owner` с той же формулировкой, что и в живом пространстве: «владелец не найден в конфигурации (возможно устройство HA)». Тест сверяет теперь и ТЕКСТ причины — вид наблюдения без текста эту асимметрию пропускал, ровно так она и проехала. Issue: #566 User-Visible: no
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'",
|
||||
}],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user