Задания, отчёты и патчи, лежавшие в 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>
13 KiB
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. Повреждённое состояние — это отказ, который должен всплыть, а не ожидаемое состояние, которое можно откладывать бесконечно.
Тесты, без которых задание не принимается
- DB-less orphan: helper сообщает успех удаления, но правило осталось → классифицируется как
Failed, не как успех. - Persisted
Disabled, узел отсутствует вnodes.tsv→ удаление подтверждается черезlink-list, состояниеDisabled, неPendingActivation. - Marker не потребляется при наличии
Failed; потребляется, если остались толькоConvergedиDeferred. - Инвариант
Converged + Failed + Deferred == Examinedна наборе из шести политик со всеми тремя исходами. - Проход с
kмутациями выполняет ровно два вызоваlink-list. - Production-shape фикстура
nodes.tsvиз четырёх полей:reserved+ валидный IP → 80;active+ валидный IP → успех;active+ невалидный IP → 78; мусор в статусе → 78. - Зарезервированный 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, а не «выполнено».