DarkRiDDeR17 мин

Разбор: как диагностировать архитектурное решение до опасного исправления

АрхитектураКоманда

Симптом в полевом разборе конкретен: рабочая заметка уже сохранена, но публичное чтение её не подтверждает. Дорогая реакция — удалить заметку, создать вторую или «на всякий случай» отправить ещё одно событие. После этого исчезает исходная версия, два intent становятся неотличимы, а исправление может создать ещё одну projection. Сначала нужен отчёт, который переживёт вмешательство: decision id, note id, revision, intent key, observed read result и граница, на которой сделано наблюдение. Цена ошибки — потерять исходную версию и усложнить повторную доставку.

Учебная fixture даёт такой маршрут без реального production. Она хранит decision, canonical note, outbox, projection и accepted intent keys в памяти. По одному ключу noteId:revision она различает pending relay и duplicate delivery. Она не знает БД, HTTP, broker, retry сети, внешнего consumer, инцидента, SLO или реальных прав. Поэтому результат «public-read-ready-in-training» не означает, что текст доступен пользователю; он означает только, что локальная модель дошла до своего объявленного состояния.

Собираем evidence раньше, чем меняем источник

Первый вопрос: «что уже доказано?». В карточке нужны immutable identifiers, а не пересказ симптома. Для кейса это decision id, note id, revision и intent key. Затем — фактическое read observation: какой путь чтения проверяли, какой результат получили, когда и в каком scope. В учебной модели нет времени и прав доступа, поэтому эти поля не выдуманы. Реальная система добавляет только разрешённые данные, которые различают ветки: storage commit, relay receipt, projection version, filter или permission check.

Диагностические состояния учебной публикации: наблюдение определяет следующий шаг
НаблюдениеГраница причиныЧто проверитьДействие без потери evidence
Нет decision id в stateРешение не зафиксированоcontext, options, requirements, evidence и reversibilityостановить реализацию и записать decision до новой публикации
Decision есть, note отсутствуетcanonical writeid, revision, подтверждение write pathне создавать копию; сначала проверить исходную запись
Note есть, intent отсутствуетwrite boundaryправило note + publication intent и фактическую границу храненияне запускать relay; вернуть review к записи
Intent pendingrelay pathkey, revision, owner следующего шагасохранить intent и наблюдать обработку, не менять source
Intent relayed, projection нет или staleread projectionintent key и revision проекциисравнить следы и controlled replay только текущего ключа
Projection совпадает с noteread contractscope, filter и ожидаемую форму публичного чтениязафиксировать результат и проверить внешний query отдельно

Таблица не предсказывает причину по одной метрике. Она задаёт порядок, в котором доказательства становятся достаточными. Особенно важно не прыгать от «не видно в public read» к «outbox сломан». Публичный результат может отличаться из-за scope, фильтра, политики публикации или неверной версии. Пока не проверены note и intent, даже широкая повторная доставка ничего не доказывает. А после повторной доставки уже сложнее понять, какая именно операция была исходной.

Decision missing — не operational incident, а стоп-сигнал

Если state не содержит decision, диагностика возвращает decision-missing. Это не значит, что рабочая заметка обязательно потеряна. Это значит, что нельзя корректно решить, нужна ли independent projection, допустима ли задержка и как распознать replay. В такой ветке безопасное действие — зафиксировать context, варианты, evidence и reversibility до новой попытки write. Иначе команда может починить видимость одним способом, а затем обнаружить, что выбранный путь нарушил требование, которое просто не было записано.

const decision = decidePublicationArchitecture({
  brief: architectureReviewBrief,
  evidence: trainingEvidence,
  assumption: trainingAssumption,
});
if (decision.status !== 'accepted-for-training') {
  throw new Error('review before implementation');
}

В fixture decision можно получить только с корректным evidence и отдельным assumption. Попытка передать assumption вместо evidence возвращает review-needed. Это полезная отрицательная ветка: не подтверждённое свойство local commit boundary нельзя использовать для принятия outbox. Когда реальный storage проверен, вместо скрытого изменения строки нужно приложить новое evidence к следующей ревизии decision. Тогда видно, какая часть дизайна была условной и что именно изменилось.

Проверяем канонический write и publication intent раздельно

После recorded decision fixture позволяет одну операцию commitTrainingPublication. В ней note и intent появляются в новых копиях Map как local-pair-recorded. Это модель требования: canonical note и intent не должны расходиться внутри обучающей функции. Она не даёт гарантию между отдельными машинами, таблицами или процессами. Поэтому нельзя из наличия двух записей делать вывод, что delivery гарантирована. Relay ещё не запускался, а public projection ещё не существует.

const state = createArchitectureTrainingState();
recordTrainingDecision(state, decision);
const note = { id: 'note-2021-12-architecture', revision: 1, title: 'Граница решения' };
const commit = commitTrainingPublication(state, { decisionId: decision.id, note });
if (commit.state !== 'local-pair-recorded') throw new Error(commit.reason);
// Map copies model only one local policy boundary, not a distributed transaction.

Pending intent не равен потерянной публикации

const beforeRelay = diagnoseTrainingPublication(state, {
  decisionId: decision.id, noteId: note.id, revision: note.revision,
});
// beforeRelay.stage === 'outbox-pending'
relayTrainingPublication(state, commit.intent.key);
const afterRelay = diagnoseTrainingPublication(state, {
  decisionId: decision.id, noteId: note.id, revision: note.revision,
});
// afterRelay.stage === 'public-read-ready-in-training'

В живой системе аналог pending должен иметь собственное evidence: строка outbox, offset, receipt, job status или иной разрешённый след. Название может быть другим. Важно, что он отвечает на тот же вопрос: «есть ли подтверждённое намерение, которое ещё не дало read effect?» Если следа нет, не надо называть состояние pending по интуиции. Вернуться к write boundary честнее, чем достроить диагностику на ощущении, что «очередь обычно работает».

Дерево безопасной диагностики: сначала decision, затем canonical note, publication intent и его состояние; pending ведёт к наблюдению relay, relayed без текущей projection — к сравнению ключа и revision, совпадающая projection — к проверке read contract; на всех ветках запрещено удалять source без доказанной причины
Диаграмма сохраняет различие между отсутствием решения, записью, intent, relay и публичным чтением.

Controlled replay требует текущий ключ и известный owner

Когда intent уже relayed, но projection не совпадает с note, нельзя говорить «переиграем публикацию» без уточнения. В модели relay проверяет intent key и revision canonical note, затем создаёт projection. Повтор ключа после успешного effect подавляется через acceptedIntentKeys. Это не полная стратегия recovery; это минимальная защита от невидимого двойного effect внутри Map. Она показывает, какие данные должны быть доступны перед replay: ключ, текущая revision, результат первого применения и владелец projection.

Когда projection есть, расследование меняет объект

Если projection с той же revision существует, fixture возвращает public-read-ready-in-training. Это не означает успех в браузере и не даёт права закрыть пользовательскую проблему. Теперь технический объект расследования меняется: не note и не relay, а read contract. Нужно проверить scope, фильтр, формат, права, кеш или конкретный query, которым пользуется внешний читатель. Попытка ещё раз обработать outbox в этой ветке не добавляет доказательства, потому что оно уже существует для projection state.

Такое переключение особенно полезно для сложных систем. Иногда прямое чтение по id подтверждает source, а поиск или список скрывает элемент из-за фильтра. Иногда read model корректна, но identity для URL не совпадает с identity source. Иногда пользователь видит устаревший cache. Все эти случаи требуют own evidence. Архитектурный review не должен заставлять outbox отвечать за них. Его задача — закончить свою причинную цепочку и передать расследование следующему owner без разрушения исходного состояния.

Маршрут безопасного разбора

  1. Зафиксировать decision id, note id, revision, intent key и точный public read, который не совпал с ожиданием. Не исправлять запись до фиксации этой карточки.
  2. Проверить, существует ли decision и закрывает ли он актуальный brief. При missing decision остановиться и оформить требования, варианты, evidence, assumption и reversibility.
  3. Проверить canonical note. Если его нет, исследовать write path; не создавать замену с новым id и не запускать relay без source.
  4. Проверить publication intent для точной пары noteId:revision. Если его нет, вернуться к storage boundary, а не к очереди или public UI.
  5. Если intent pending, сохранить ключ и собрать evidence relay path. Не менять source, пока не известно, был ли effect вообще разрешён и кому он принадлежит.
  6. Если intent relayed, сравнить intent key и revision projection. Controlled replay допустим только для явно выбранного current key и отдельного contract consumer.
  7. Если projection совпадает, завершить эту ветку и проверить read contract: scope, filter, rights, cache и формат. Затем записать новый evidence вместо повторной обработки старой причины.

Fixture проверяет также ветки, которых не хочется видеть

Семнадцать assertions фиксируют не только успешный relay. Они доказывают, что unknown architecture option не становится accepted, assumption не принимается за evidence, synchronous alternatives имеют declared gaps, commit отвергается без decision, pending diagnosis безопасен, duplicate relay не записывает вторую projection, а ready diagnosis остаётся недеструктивным. Утверждение «недеструктивный» здесь очень конкретно: объект результата содержит destructiveAction: false; он не удаляет Map и не создаёт новую note.

const report = runArchitectureReviewFixture();
if (!report.passed) throw new Error('training assertions failed');
console.log(report.assertionCount); // 17
// Assertions are evidence about this local model only.

Исторические источники, границы и следующий шаг

C4 snapshot от 1 декабря 2021 полезен как напоминание не смешивать context и code: диагноз начинается с владельцев и границ, затем доходит до конкретного ключа. RFC 2119 помогает различить обязательное требование от пожелания. AWS Builders’ Library от января 2021 объясняет осторожность с повторным effect. Заархивированный snapshot transactional outbox от 18 ноября 2021 называет отдельный relay после записи intent. Это документы, существовавшие к исторической дате статьи. Они не содержат нашу fixture и не подтверждают ни одну production-метрику.

Следующий практический шаг — не развернуть универсальный recovery worker. Возьмите один обезличенный flow, составьте evidence card и проверьте, на какой ветке он останавливается: decision, write, intent, relay, projection или read contract. Затем сделайте самое маленькое обратимое действие только на найденной границе. Если для этого действия нельзя назвать ключ, owner и evidence результата, остановитесь и дополните decision. Такой подход оставляет после разбора не только исправленный экран, но и понятную техническую причину, которую можно проверить при следующем изменении.

Проверяемые источники