Задания, отчёты и патчи, лежавшие в C:\Users\Ochenstarik\projects и в домашней папке, перенесены в agents/. Разложено по агентам там, где имя файла позволяло определить автора; остальное — в _salvage-2026-08-18/ и разбирается вручную. Патчи в notes/salvage-2026-08-18/ — незакоммиченная работа из брошенных рабочих копий: она существовала только на диске. Тяжёлое (релизные архивы, инсталляторы, наборы данных) в репозиторий не попало: оно лежит рядом, в Agent_projects/_archive и Agent_projects/_data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
13 KiB
Markdown
122 lines
13 KiB
Markdown
# B-3R: завершить реконсиляцию от фактического состояния
|
||
|
||
## Репозиторий и ветка
|
||
|
||
- Репозиторий: https://github.com/ochenstarik-ui/server-monitor-manager
|
||
- Рабочая копия: `C:\Users\Ochenstarik\projects\server-monitor-manager-task3-b3`
|
||
- Ветка: `hermes/task3-b3-fact-reconciliation`, **изменения не закоммичены** — 24 файла, около +1276/−141
|
||
- База: `b11c277ac7f79a18670932eca4622982d9ff48e0`
|
||
- Ветки на GitHub нет; коммит и push ещё не выполнялись
|
||
|
||
Нормативные документы задания: `B3R_SPEC.md` и исходное ТЗ B-3 — оба в `C:\Users\Ochenstarik\Сюда\Panel control\Task3-B3-2026-08-04\`. Они остаются в силе целиком; ниже — только то, что осталось не сделано.
|
||
|
||
## Зачем
|
||
|
||
Проход реконсиляции стал fact-first: сначала один `link-list`, потом сверка с базой. Но верификация результата мутации осталась пер-линковой, а классификация результата неполна. Из-за этого два дефекта, оба на пути kill switch:
|
||
|
||
- удаление правила может быть засчитано без доказательства, что правило исчезло;
|
||
- результат, не попавший ни в `Converged`, ни в `Failed`, позволяет фоновому сервису потребить marker внеочередной реконсиляции при неподтверждённом исходе.
|
||
|
||
## Что уже закрыто — проверено по коду, переделывать не нужно
|
||
|
||
- **R3 полностью.** `lookup_node_ip` читает четвёртое поле `nodes.tsv` и требует `active`; проверка статуса стоит раньше проверки IP, поэтому `reserved` + валидный адрес даёт exit 80, а `active` + битый адрес даёт 78.
|
||
- **R1 на уровне отдельной мутации.** Появился `VerifyExactFactualCountAsync`, считающий записи через `ListRulesAsync`, то есть через `link-list`, а не `link-status`. Условие `if (persisted)` вокруг верификации снято — DB-less orphan теперь тоже проверяется. Синтетическое присваивание `ActualState` выполняется после доказанного нуля записей, а не вместо доказательства.
|
||
- `list_rules` разбирает только managed-comment, проверяет арность, node-паттерны, протокол и диапазон порта, подделки отбрасывает в stderr, чужие правила не выводит.
|
||
- `disconnect_rule` больше не вызывает `lookup_node_ip` — удаление правила не зависит от `nodes.tsv`. Это верно, сохранить.
|
||
|
||
## Работы
|
||
|
||
### 1. Пакетная финальная верификация
|
||
|
||
Сейчас `VerifyExactFactualCountAsync` вызывается на каждую мутацию: проход с `k` мутациями делает `1 + k` вызовов `link-list`.
|
||
|
||
Требуется: один стартовый `link-list` и, при наличии мутаций, **ровно один финальный** `link-list`, по которому сверяются все затронутые ключи разом. Проход без расхождений — один привилегированный вызов, проход с мутациями — ровно два.
|
||
|
||
Проверяется счётчиком вызовов фейкового applier и журналом вызовов в интеграционном тесте.
|
||
|
||
### 2. Класс `Deferred` и инвариант классификации
|
||
|
||
Ввести третий класс результата и инвариант:
|
||
|
||
```
|
||
Converged + Failed + Deferred == Examined
|
||
```
|
||
|
||
Инвариант проверять в коде — нарушение это `LogError` с идентификаторами политик — и отдельным тестом на смешанном наборе.
|
||
|
||
Решение, которое нужно реализовать именно так:
|
||
|
||
- `PendingActivation` — это `Deferred`, а не `Failed`. Node зарезервирован, `peer-add` не выполнен, состояние может держаться сутками и ошибкой не является.
|
||
- `Deferred` **не блокирует** потребление marker. Иначе один зарезервированный Node навсегда удержит marker и вернёт бесконечный полный проход каждые 30 секунд — ровно то, что закрывали в B-2 и B3-3.
|
||
- Marker блокирует только `Failed`, и на него уже действует ограничение в три попытки.
|
||
- `Deferred` обязан быть видимым: событие `link.pending-node-activation`, отдельная подпись в Desktop, отражение в журнале прохода. Ошибкой не называется.
|
||
|
||
`LinkFullReconciliationResult` и `LinkReconciliationResult` дополнить полем `Deferred` и списком `DeferredPolicyIds` по образцу `FailedPolicyIds`.
|
||
|
||
### 3. Source-generated сериализация orphan audit (R4)
|
||
|
||
В `ControlStore.cs` запись audit для orphan-правила сериализуется анонимным типом через reflection, тогда как остальной Control последовательно использует source-generated `JsonTypeInfo`.
|
||
|
||
Отказ произойдёт **после** мутации firewall и **после** публикации `link.orphan-removed`, дав противоречивую телеметрию: правило удалено, событие отправлено, проход помечен неуспешным.
|
||
|
||
- ввести именованный DTO, добавить его в source-generated контекст Control, сериализовать через `Context.Default.<Type>`;
|
||
- зафиксировать порядок: запись audit до публикации финального события успеха;
|
||
- проверить на **опубликованном** `linux-x64 PublishTrimmed` артефакте, прогнав orphan-путь и сверив содержимое audit-записи. Приложить вывод к отчёту.
|
||
|
||
### 4. Linux integration test на fact-first протокол (R5)
|
||
|
||
`tests/ServerMonitorManager.Control.Tests/LinkPolicyApplierIntegrationTests.cs` остался на протоколе `link-status`-first: fake helper не реализует `link-list`, ожидаемый журнал вызовов начинается с `link-status`.
|
||
|
||
На Windows тест выходит по `OperatingSystem.IsLinux() == false`, поэтому локальные прогоны его не показывают. `linux-control-agent.yml` гоняет Control-тесты на Ubuntu, где пропуска не будет.
|
||
|
||
- реализовать `link-list` в fake helper;
|
||
- переписать ожидаемый журнал на fact-first протокол;
|
||
- проверить: no-drift → ровно один `link-list`; отсутствующая таблица классифицируется прямо на `link-list` без промежуточной мутации; мутация сопровождается одной финальной верификацией; DB-less raw disconnect проходит верификацию;
|
||
- **прогнать Control suite на Linux.** Windows-прогон этот тест не выполняет и заявленные локальные результаты дефект не выявляют.
|
||
|
||
### 5. Убрать fail-open default в интерфейсе (R6)
|
||
|
||
`ILinkPolicyApplier.ListRulesAsync` имеет тело по умолчанию, возвращающее пустой список. Забытая реализация молча сообщает «firewall пуст» — худший возможный дефолт для источника истины о факте, и он уже позволил старым test doubles скомпилироваться без нового протокола.
|
||
|
||
Сделать `ListRulesAsync` и raw `ApplyDisconnectAsync(LinkRule, …)` обязательными членами интерфейса, все test doubles обновить явно.
|
||
|
||
### 6. Читаемость вложенных scope (R7)
|
||
|
||
`LinkService.cs`, участки обработки persisted-кандидатов и factual-orphans: тела `try` визуально на уровне внешнего scope, порядок блокировок `sorted node locks → per-Link gate` восстанавливается только разбором отступов. Переформатировать либо вынести обработку одного tuple и одного orphan в приватные методы без дублирования логики сходимости.
|
||
|
||
### 7. Мусор в поле статуса `nodes.tsv`
|
||
|
||
Сейчас любое не-`active` значение даёт exit 80 и уходит в `Deferred` навсегда. Разграничить: известные значения (`reserved` и подобные) → 80; значение, не проходящее валидацию формата, → 78. Повреждённое состояние — это отказ, который должен всплыть, а не ожидаемое состояние, которое можно откладывать бесконечно.
|
||
|
||
## Тесты, без которых задание не принимается
|
||
|
||
1. DB-less orphan: helper сообщает успех удаления, но правило осталось → классифицируется как `Failed`, не как успех.
|
||
2. Persisted `Disabled`, узел отсутствует в `nodes.tsv` → удаление подтверждается через `link-list`, состояние `Disabled`, не `PendingActivation`.
|
||
3. Marker не потребляется при наличии `Failed`; потребляется, если остались только `Converged` и `Deferred`.
|
||
4. Инвариант `Converged + Failed + Deferred == Examined` на наборе из шести политик со всеми тремя исходами.
|
||
5. Проход с `k` мутациями выполняет ровно два вызова `link-list`.
|
||
6. Production-shape фикстура `nodes.tsv` из четырёх полей: `reserved` + валидный IP → 80; `active` + валидный IP → успех; `active` + невалидный IP → 78; мусор в статусе → 78.
|
||
7. Зарезервированный Node на десяти последовательных проходах: каждый раз `Deferred`, marker потребляется, число проходов не превышает расписание.
|
||
|
||
## Критерий приёмки
|
||
|
||
- все семь пунктов работ закрыты;
|
||
- семь тестов выше зелёные;
|
||
- Control suite прогнан **на Linux**, а не только на Windows;
|
||
- orphan audit проверен на опубликованном trimmed-артефакте;
|
||
- все критерии исходного ТЗ B-3 по-прежнему выполняются;
|
||
- PR создан, CI зелёный — впервые для этой ветки;
|
||
- независимый review получил полный текст этого задания, `B3R_SPEC.md` и исходное ТЗ B-3.
|
||
|
||
## Границы
|
||
|
||
- не трогать файлы вне области реконсиляции Links; изменения в Desktop ограничены отображением `Deferred`;
|
||
- не менять порядок блокировок и не вводить новых классов блокировок;
|
||
- не ослаблять fail-closed поведение: неизвестная ошибка `nft` остаётся отказом, а не «правил нет»;
|
||
- `TreatWarningsAsErrors` в этой задаче не включать.
|
||
|
||
## Отчётность
|
||
|
||
Отдельно перечислить: что запущено локально, что в CI, что не запускалось и почему. Физический acceptance (`SMM_ACCEPT_RESTORE=1 SMM_ACCEPT_REBOOT=1 tests/acceptance/three-server-mesh.sh`) выполнить нельзя — не выданы topology inputs; contract- и mock-тесты за него не выдавать.
|
||
|
||
Статус после merge — `merged / verified, physical acceptance pending`, а не «выполнено».
|