Симптом в полевом разборе конкретен: рабочая заметка уже сохранена, но публичное чтение её не подтверждает. Дорогая реакция — удалить заметку, создать вторую или «на всякий случай» отправить ещё одно событие. После этого исчезает исходная версия, два 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 write | id, revision, подтверждение write path | не создавать копию; сначала проверить исходную запись |
| Note есть, intent отсутствует | write boundary | правило note + publication intent и фактическую границу хранения | не запускать relay; вернуть review к записи |
| Intent pending | relay path | key, revision, owner следующего шага | сохранить intent и наблюдать обработку, не менять source |
| Intent relayed, projection нет или stale | read projection | intent key и revision проекции | сравнить следы и controlled replay только текущего ключа |
| Projection совпадает с note | read contract | scope, 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 честнее, чем достроить диагностику на ощущении, что «очередь обычно работает».
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 без разрушения исходного состояния.
Маршрут безопасного разбора
- Зафиксировать decision id, note id, revision, intent key и точный public read, который не совпал с ожиданием. Не исправлять запись до фиксации этой карточки.
- Проверить, существует ли decision и закрывает ли он актуальный brief. При missing decision остановиться и оформить требования, варианты, evidence, assumption и reversibility.
- Проверить canonical note. Если его нет, исследовать write path; не создавать замену с новым id и не запускать relay без source.
- Проверить publication intent для точной пары noteId:revision. Если его нет, вернуться к storage boundary, а не к очереди или public UI.
- Если intent pending, сохранить ключ и собрать evidence relay path. Не менять source, пока не известно, был ли effect вообще разрешён и кому он принадлежит.
- Если intent relayed, сравнить intent key и revision projection. Controlled replay допустим только для явно выбранного current key и отдельного contract consumer.
- Если 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. Такой подход оставляет после разбора не только исправленный экран, но и понятную техническую причину, которую можно проверить при следующем изменении.
Проверяемые источники
- C4 model — заархивированный официальный снимок от 1 декабря 2021 года — снимок подтверждает, что к декабрю 2021 C4 уже описывала уровни context, container, component и code. Наши SVG используют только идею явной границы и не заявляют соответствие нотации или инструменту.
- RFC 2119: Key words for use in RFCs to Indicate Requirement Levels — март 1997 года — первичный документ IETF для слов MUST, SHOULD и MAY. В статьях они обозначают только условия учебного решения, а не требования к чужой системе.
- AWS Builders’ Library: Making retries safe with idempotent APIs — 15 января 2021 года — источник разделяет повтор запроса и семантический effect. Fixture использует локальный ключ intent, но не реализует HTTP API, сеть, таймауты или policy AWS.
- AWS What’s New: публикация статьи Making retries safe with idempotent APIs — 15 января 2021 года — официальная публикационная запись подтверждает дату Builder’s Library article. Дата нужна только для исторической рамки декабря 2021, а не как доказательство свойств учебной модели.
- Transactional outbox: заархивированный авторский снимок паттерна от 18 ноября 2021 года — снимок до исторической границы описывает запись сообщения в транзакции с данными и отдельный message relay. Он задаёт терминологию паттерна, но fixture не реализует БД, broker, 2PC или exactly-once delivery.