diff --git a/editorial/agent-rewrites/077.json b/editorial/agent-rewrites/077.json index d64b87a..f98f217 100644 --- a/editorial/agent-rewrites/077.json +++ b/editorial/agent-rewrites/077.json @@ -2,6 +2,6 @@ "index": 77, "slug": "editorial-2025-11-mechanism-research-method", "title": "Как проверить источник до того, как он повлияет на решение", - "excerpt": "ETag, дата публикации и цифровая подпись подтверждают разные свойства документа. Разбираем границы этих сигналов и собираем проверку, которая останавливает слишком сильный вывод.", - "contentHtml": "
Команда находит страницу с нужным API, видит свежую дату и сразу переносит совет в интеграцию. Через месяц endpoint ведёт себя иначе. Оказывается, страница описывала другой режим, а заголовок ETag приняли за номер релиза. Ошибка стоит дороже, чем время на чтение: меняется код, растёт число обходов, а причину трудно восстановить после инцидента.
Тезис статьи прост: источник не равен доказательству решения. Проверка должна разделять публикацию, конкретное представление документа, наблюдаемый факт и действие, которое из него следует. Каждый переход требует своей опоры. Если опоры нет, проверка должна остановиться и показать, чего не хватает.
\nПубликация — RFC, стандарт, релиз или датированная рекомендация. Она задаёт документ и его область. Представление — именно тот HTML, PDF или HTTP-ответ, который прочитал инженер. Оно может зависеть от языка, заголовков запроса, авторизации и времени.
\nНаблюдение — короткая фраза, которую можно проверить в представлении: «в разделе указано условие X» или «ответ содержит заголовок ETag». Утверждение — уже интерпретация наблюдения. Решение — изменение кода, конфигурации или процесса. Страница может подтвердить наблюдение, но не обязана подтверждать решение для любого продукта.
\nУдобно хранить эти уровни раздельно. Для публикации нужен идентификатор и дата. Для представления — URL, версия, commit или снимок. Для наблюдения — точный раздел, строка или заголовок ответа. Для решения — условия применимости, риск и способ проверки.
\nВ HTTP заголовок ETag служит validator выбранного представления ресурса. Клиент сравнивает значение при условных запросах. Сервер может вычислить его по содержимому, назначить внутренний идентификатор или учитывать согласование формата. Само значение не обязано раскрывать эту схему.
Из ETag: \"a81c\" нельзя вывести, что документ относится к релизу 8.1, что он был опубликован в конкретную дату или что смысл каждого абзаца сохранится для другого endpoint. Last-Modified также говорит о времени изменения представления, а не о полном жизненном цикле продукта.
Для исторической проверки нужен более сильный якорь: датированный RFC, тег релиза, commit, опубликованный PDF или сохранённый снимок. Validator полезно записать дополнительно. Он помогает повторить протокольную проверку, но не превращается в семантическую версию.
\nПодпись помогает проверить целостность защищённого объекта и связь с подписантом. Она не превращает каждое утверждение внутри объекта в доказанный факт. Документ может быть подлинным и одновременно описывать ограниченный сценарий, старую конфигурацию или другой объект.
\nПоэтому у записи проверки нужны две разные строки: integrity и claimEvidence. Первая отвечает на вопрос «документ не изменили после подписи?». Вторая — «есть ли в нём наблюдение, которое поддерживает именно нашу фразу?». Подмена одной строки другой создаёт ложную уверенность.
const record = {\n source: {\n url: 'https://example.test/api-guide',\n etag: '\"a81c\"',\n signature: 'valid'\n },\n observation: 'Документ описывает режим read-only для версии 2.',\n claim: 'API безопасен для записи в любой версии.'\n};\n\nconst canRecommend =\n Boolean(record.source.url) &&\n Boolean(record.source.etag) &&\n record.source.signature === 'valid' &&\n record.claim.includes('версии 2') &&\n record.observation.includes('версии 2');\n\nconsole.log(canRecommend); // false\nЭто учебный пример, а не проверка реальной подписи и не production-код. У записи есть адрес, validator и валидная подпись. Но наблюдение ограничивает вывод версией 2 и режимом read-only, а утверждение расширяет его до записи и любой версии. Проверка возвращает отказ. Правильное действие — сузить утверждение или найти отдельное наблюдение для записи и нужной версии.
\nОтрицательный путь важен не меньше успешного. Если система продолжает работу со статусом «достаточно» после отсутствия версии, locator или области применимости, она маскирует неизвестное. Средний балл доверия не исправит отсутствие конкретного факта.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| ETag выглядит как номер версии | Протокольный validator смешали с релизом | Открыть правила версий издателя и сравнить с RFC | Сохранить ETag отдельно, найти release pin |
| Ссылка открывается, но текст уже другой | Зафиксирован только текущий URL | Проверить дату, commit, PDF или архивный снимок | Сузить исторический вывод или сменить источник |
| Подписанный документ подтверждает всё сразу | Целостность приняли за истинность тезиса | Разделить signature и claim evidence | Оставить только вывод об авторстве либо добавить факт |
| Документ верен, но совет не работает | Scope документа не совпал с продуктом | Сопоставить endpoint, версию, права и режим | Добавить условие применимости и отдельный тест |
| В отчёте нет причины остановки | Отказ заменили общим статусом доверия | Проверить обязательные поля и отрицательные ветки | Вернуть статус hold с конкретным недостающим полем |
Эта модель не делает веб неизменяемым. Она не устанавливает истинность заявления одной ссылкой и не заменяет предметного эксперта. Официальный стандарт может не описывать operational-ограничение конкретного сервиса. Подпись может быть корректной, но относиться не к тому представлению. Архивный снимок фиксирует текст, но не доказывает, что система в тот день работала по нему.
\nМетод также не требует сохранять весь интернет. Для дорогих решений достаточно закрепить минимальный набор: первичный документ, конкретное представление, наблюдение, область применимости и способ проверки. Если какой-то элемент нельзя получить, вывод должен стать уже. Отказ — нормальный результат, когда данных недостаточно.
\nПроверка готова, когда другой инженер может открыть тот же документ или его закреплённое представление, найти указанный locator, увидеть наблюдение и понять, почему оно поддерживает решение именно в заданном scope. Если он меняет версию, режим или объект, отрицательный путь возвращает остановку с понятной причиной. Ни один из этих результатов не требует доверять интонации автора.
\nETag и Last-Modified как validator fields для выбранного представления. Документ не назначает ETag номером релиза.Команда находит страницу с нужным API и сразу переносит совет в интеграцию. В адресе есть знакомый номер, в ответе — ETag, внизу — свежая дата. Через месяц обновлённый endpoint ведёт себя иначе: номер был частью адреса документа, а ETag оказался валидатором представления, а не номером релиза. Ошибка уже превратилась в код, обходы и спор о том, какую страницу читали.
Источник полезен только тогда, когда понятно, что именно он подтверждает. Для инженерного решения нужно разделить публикацию, конкретное представление, наблюдение, утверждение и действие. У каждого уровня своя граница. Если версия, локатор или область применимости неизвестны, правильный результат проверки — остановка и точное описание недостающего факта.
\nНачинайте с формулировки решения, которое может измениться. «Документация про кеш» слишком широко. «Для GET-запроса к ресурсу X можно повторить условный запрос, если сервер прислал тот же валидатор» уже указывает объект, метод и условие. Теперь ясно, какой факт искать и какой результат нельзя обещать без отдельного теста.
\nПолезно различать пять уровней:
\nRFC может подтвердить смысл поля протокола, но не совместимость нашего SDK. Commit может подтвердить содержимое файла, но не успешность запуска в другой среде. Отчёт теста может подтвердить один сценарий, но не «работает всегда». Скачок от наблюдения к решению и есть место, где чаще всего появляется лишняя уверенность.
\nHTTP передаёт не сам ресурс, а его представление — данные и метаданные, выбранные для конкретного запроса. Поэтому один URL может давать разные представления из-за языка, формата, кодировки или других условий согласования. В RFC 9110 поле ETag определено как непрозрачный валидатор выбранного представления. Сервер сам выбирает способ его формирования; клиенту нужно сравнивать значение по правилам HTTP, а не расшифровывать его.
Из ETag: \"8.1\" нельзя заключить, что перед нами релиз 8.1, документ опубликован в августе или другой endpoint поддерживает тот же контракт. Это может быть хеш, внутренний идентификатор или другое значение, различающее представления. Слабый валидатор с префиксом W/ дополнительно имеет иные правила сравнения.
Last-Modified сообщает дату последнего изменения представления в смысле HTTP-валидатора. Она не обязана совпадать с датой выпуска продукта, датой утверждения API или временем, когда команда впервые прочитала документ. Заголовок Vary может указать, какие поля запроса участвуют в выборе представления. Ни один из этих заголовков не заменяет pin релиза, commit или датированный документ.
Практическая запись должна хранить их раздельно:
\nsourceUrl: https://api.example.test/docs\nsourcePin: vendor-api-2.4.1\nlocator: \"GET /reports, section Conditional requests\"\nobservedAt: 2025-11-15T10:00:00Z\netag: \"8.1\"\nlastModified: Sat, 15 Nov 2025 10:00:00 GMT\nЗначения в примере учебные. sourcePin отвечает на вопрос «какую версию документа закрепили», а etag — «какой валидатор сообщил сервер для выбранного представления». Сохранять оба полезно, но подменять одно другим нельзя.
Цифровая подпись решает криптографическую задачу: при корректной проверке она даёт сведения о происхождении и целостности подписанных данных, а также поддерживает неотказуемость подписанта в заданной инфраструктуре. Это не означает, что каждый тезис документа подходит нашему продукту или что авторский прогноз стал измеренным результатом.
\nНапример, подписанный файл может описывать старую версию, другой метод или условие, которого нет в нашей системе. Подпись защищает связь с конкретными данными; область применимости и смысл нужно проверять отдельно. У записи исследования поэтому должны быть как минимум две независимые строки: integrity и claimEvidence.
Эту же границу помогает увидеть модель происхождения W3C PROV. В ней документ — сущность, чтение или преобразование — действие, а человек, сервис или программа — участник, связанный с ответственностью. Время, версия и связь между редакциями делают историю проверяемой. PROV описывает происхождение, но не выдаёт автоматический рейтинг качества и не доказывает деловую пригодность решения.
\nНиже — самостоятельный скрипт без сети, базы данных и настоящей криптографии. Он проверяет только структуру записи и не притворяется проверкой внешнего мира. Сохраните фрагмент в файл claim-check.mjs, затем выполните команды:
node --version\nnode claim-check.mjs\nconst record = {\n sourceUrl: 'https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.3',\n sourcePin: 'RFC 9110',\n locator: 'section 8.8.3',\n scope: 'HTTP response; selected representation',\n observation: 'ETag is an opaque validator for differentiating representations',\n claim: 'ETag is the software release number'\n};\n\nfunction assess(item) {\n const required = ['sourceUrl', 'sourcePin', 'locator', 'scope', 'observation', 'claim'];\n const missing = required.filter((field) => !item[field]);\n\n if (missing.length > 0) {\n return { status: 'hold', reason: 'missing-field', missing };\n }\n\n if (item.claim === 'ETag is the software release number') {\n return { status: 'hold', reason: 'observation-does-not-support-claim' };\n }\n\n return { status: 'ready-for-scoped-use', reason: 'record-is-addressable' };\n}\n\nconsole.log(assess(record));\n// { status: 'hold', reason: 'observation-does-not-support-claim' }\nНа обычном Node.js скрипт напечатает отказ: запись адресуемая, но наблюдение говорит о валидаторе представления, а утверждение называет его номером релиза. Если заменить claim на «ETag различает представления в данном HTTP-контексте», локальная проверка пройдёт. Это всё ещё не доказывает поведение конкретного сервера: для него нужен отдельный запрос с зафиксированными входами.
Отрицательная ветка важнее красивого статуса ready. Удалите sourcePin, измените метод или расширьте утверждение с одного ресурса на весь API — запись должна перейти в hold либо потребовать нового наблюдения. Список обязательных полей защищает журнал от пустой ссылки, но не превращает заполненную карточку в эксперимент.
| Сигнал | Что подтверждает | Чего не подтверждает | Следующая проверка |
|---|---|---|---|
| Номер RFC, релиз или commit | К какому закреплённому материалу относится цитата | Что система внедрила этот контракт | Сверить локатор и версию клиента/сервера |
ETag | Валидатор выбранного HTTP-представления | Номер релиза и бизнес-смысл ресурса | Проверить условный запрос и правила сравнения |
Last-Modified | Дату изменения представления по правилам HTTP | Дату выпуска продукта или дату утверждения API | Найти официальный release note или тег |
| Цифровая подпись | Целостность и происхождение подписанных данных при корректной проверке | Истинность каждого тезиса и совместимость с нашим кодом | Проверить scope, ключ, политику и независимый факт |
| Текущий URL | Куда можно перейти сейчас | Как выглядел документ в момент решения | Закрепить версию, commit, PDF или архивный снимок |
| Результат теста | Поведение в указанных входах и среде | Поведение во всех версиях, ролях и нагрузках | Повторить сценарий и перечислить ограничения |
Остановитесь, если документ нельзя однозначно закрепить, локатор ведёт в другой раздел, текущая страница изменилась, а исторического представления нет, или подпись относится не к тому файлу. Точно так же остановитесь, если в тексте есть только рекламное обещание, а решение требует измерения производительности, безопасности или совместимости.
\nНе заменяйте остановку усреднённым баллом доверия. Две ссылки на разные версии не становятся сильнее от сложения. Подписанный документ другой области не подтверждает наш endpoint. Пустое поле «дата» не компенсируется свежим впечатлением от страницы. Полезный результат в таких случаях — список: какого факта не хватает, где его получить и какой вывод пока запрещён.
\nМетод не делает интернет неизменяемым и не превращает ссылку в лабораторный результат. Он не измеряет задержку, не проверяет нагрузку, не выполняет цифровую подпись и не устанавливает, что конкретный сервис соблюдает документ. Для этого нужны реальные входы, разрешённая среда, измерительный инструмент и отдельный критерий успеха.
\nRFC описывает семантику HTTP, но не знает, как владелец API строит ETag. NIST описывает свойства цифровой подписи, но не проверяет доверенную цепочку ключей в вашем проекте. W3C PROV задаёт vocabulary для происхождения, но не требует конкретного формата журнала и не решает конфликт двух корректных источников. Эти границы нужно переносить в карточку решения, а не прятать в примечании.
\nУчебный скрипт намеренно не обращается к сети. Его зелёный результат означает лишь, что локальная запись заполнена и не содержит известного слишком сильного вывода. Перед изменением production-кода дополнительно проверьте конкретную версию, права, данные, обратимость и влияние на пользователей.
\nЗапись готова к использованию, когда другой инженер может без устного пояснения открыть закреплённый источник, перейти к локатору, увидеть наблюдение, назвать область применения и воспроизвести следующий шаг. При изменении версии, метода или объекта проверка должна показать, почему старый вывод больше не действует.
\nМинимальная форма результата выглядит так: «В RFC 9110, разделе 8.8.3, ETag назван непрозрачным валидатором выбранного представления. Для GET ресурса X в версии Y проверяем условный запрос. Это не доказывает номер релиза, совместимость записи или поведение другого endpoint». Такая формулировка уже не обещает лишнего и оставляет команде проверяемое действие.
ETag как непрозрачный валидатор.Ошибка в исследовании источников обычно обнаруживается поздно. В тексте стоит ссылка на документацию, но читатель не знает, какую версию открывал автор, где находится нужный факт и при каком условии он перестаёт работать. В коде такой совет превращается в неверный запрос, несовместимую настройку или лишнюю переделку. Цена ошибки — часы на повторную проверку и решение, которое нельзя уверенно объяснить после обновления документа.
\nСсылка — это адрес. Утверждение — это ограниченная фраза, которую можно проверить. Между ними нужен короткий журнал: statement, scope, source pin, locator, observation и status. Такая запись не делает источник истинным. Она показывает, какой факт прочитан, где он находится и какое действие он разрешает.
\nНачинайте с одного предложения. «ETag ускоряет кеширование» слишком широко: здесь не названы протокол, представление, условие запроса и ожидаемый результат. «Для выбранного HTTP-представления сильное сравнение ETag позволяет проверить условие If-None-Match» уже имеет границу. Его можно сопоставить с разделом спецификации и с наблюдаемым запросом.
\nУ утверждения есть шесть полей. statement хранит фразу. scope описывает, где она действует. sourcePin фиксирует версию первичного материала: RFC, tagged release или датированный PDF. locator указывает раздел, таблицу или endpoint. observation записывает минимальный факт без расширения смысла. status управляет следующим шагом: можно передавать запись дальше или нужно остановиться.
URL без версии ведёт на текущую страницу. Он не доказывает, что документ выглядел так же в момент чтения. Поэтому журнал хранит pin отдельно. Датированный RFC или commit отвечает на вопрос «какой материал открыт». Locator отвечает на вопрос «где искать». Observation отвечает на вопрос «что там написано или возвращено».
\nЭто разделение не формальность. Заголовок ETag — непрозрачный валидатор выбранного представления. Он помогает отличать представления ресурса, но сам по себе не описывает бизнес-смысл данных и не обещает совместимость конкретного SDK. Из факта о протоколе нельзя автоматически вывести факт о продукте.
Полезно также различать целостность и смысл. Подписанный документ подтверждает происхождение или неизменность представления, если проверка подписи прошла. Это не доказывает, что интерпретация автора подходит другому endpoint, региону, праву доступа или версии клиента. Если источник сообщает только «подпись действительна», журнал должен остановить более сильное утверждение.
\nНиже учебный пример. Он не обращается к сети и не имитирует результат production-сервиса. Функция проверяет только полноту локальной записи: pin, область, locator и наблюдение. Такой код полезен для демонстрации границы метода, а не для оценки качества реального источника.
\nconst claim = {\n statement: 'If-None-Match compares the selected representation',\n scope: 'HTTP conditional GET',\n sourcePin: 'RFC-9110',\n locator: 'section 8.8.3',\n observation: 'ETag is an opaque validator for a selected representation',\n};\n\nfunction assess(record) {\n const required = ['statement', 'scope', 'sourcePin', 'locator', 'observation'];\n const missing = required.filter((field) => !record[field]);\n\n if (missing.length) {\n return { status: 'hold', nextAction: 'repair-record', missing };\n }\n\n return { status: 'ready-for-scoped-handoff', nextAction: 'use-with-scope' };\n}\n\nconsole.log(assess(claim));\n// { status: 'ready-for-scoped-handoff', nextAction: 'use-with-scope' }\nСтатус ready-for-scoped-handoff означает только одно: запись адресуемая и содержит наблюдение. Он не означает, что измерена производительность, проверена совместимость или доказан эффект для продукта. Чтобы не потерять эту границу, следующий шаг хранится рядом со статусом.
Отрицательная ветка обязательна. Если удалить sourcePin, функция вернёт hold. Добавление ещё одной цитаты вокруг текущей страницы не исправит отсутствие версии. Нужно найти неизменяемый первичный материал, открыть locator заново и записать новый observation. Если источник содержит маркетинговое обещание, его можно сохранить как наблюдение текста, но нельзя выдавать за измеренный результат.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Ссылка открывается, но факт не находится | Нет locator или он относится к другой версии | Открыть pinned material и найти раздел заново | Добавить точный locator; при несовпадении остановить вывод |
| Совет звучит уверенно, но не имеет условия | Тема заменяет проверяемое утверждение | Назвать объект, вход, результат и границу | Сузить statement до одной проверяемой фразы |
| Подписанный файл принимают за доказательство поведения | Целостность смешали с семантической истинностью | Сверить observation с заявленным выводом | Оставить только вывод об авторстве или найти независимый факт |
| Две ссылки дают «среднюю уверенность» | Сравнивают разные классы источников | Проверить одинаковые scope, термин и объект | Не агрегировать записи до устранения несовместимости |
| После обновления API совет ломается | Зафиксирован только корневой URL | Сравнить дату, release или commit с датой решения | Добавить source pin и повторить проверку |
Журнал не заменяет эксперимент. Он не измеряет задержку, не проверяет нагрузку и не устанавливает, что API одинаково ведёт себя во всех средах. Он только связывает фразу с источником и наблюдением. Для утверждения о производительности нужен воспроизводимый тест с входами, условиями и метрикой. Для утверждения о безопасности нужны модель угроз, область действия и отдельная проверка.
\nМетод также не решает спор между двумя корректными первичными источниками. Если версии или области различаются, журнал должен показать это различие. Решение появится после явного критерия: совместимость, дата поддержки, стоимость миграции или другой измеримый приоритет. Нельзя скрывать отсутствие критерия под статусом «достаточно надёжно».
\nУчебный код выше проверяет структуру записи, а не внешний мир. В настоящем исследовании нужно сохранить ссылку на доступный первичный документ, дату чтения и способ воспроизвести observation. Если источник исчез, изменился или не отвечает на нужный вопрос, корректный результат — hold.
\nЗапись готова к использованию, когда другой инженер без устного пояснения может открыть зафиксированный источник, перейти к locator, увидеть observation, назвать scope и понять следующий шаг. Проверка должна пройти и для отрицательной ветки: при отсутствии версии, несовпадении области или подмене факта обещанием статус блокирует передачу.
\nЭто небольшой критерий, но он защищает решение от самой дорогой подмены: ссылка начинает выглядеть как доказательство только потому, что её никто не перепроверил.
\nОшибка в исследовании источников обычно обнаруживается поздно. В тексте стоит ссылка на документацию, но читатель не знает, какую версию открывал автор, где находится нужный факт и при каком условии он перестаёт работать. В коде такой совет превращается в неверный запрос, несовместимую настройку или лишнюю переделку. Цена ошибки — часы на повторную проверку и решение, которое нельзя уверенно объяснить после обновления документа.
\nСсылка — это адрес. Утверждение — ограниченная фраза, которую можно проверить. Между ними нужен короткий журнал: statement, scope, source pin, locator, observation и status. Такая запись не делает источник истинным. Она показывает, какой факт прочитан, где он находится и какое действие он разрешает.
\nХорошее исследование начинается не с коллекции ссылок, а с вопроса, на который нужно принять решение. Например: «Можно ли включить условный запрос к нашему API, не меняя смысл ответа?» Это лучше, чем «как работает кеширование»: в первом вопросе названы действие, объект и риск.
\nЗафиксируйте симптом до чтения документа. Клиент повторно скачивает один и тот же JSON, время ответа растёт, а команда предлагает добавить заголовок из статьи в интернете. Пока неизвестны версия API, поддержка прокси и семантика ответа, это гипотеза, а не план исправления. Следующий шаг — найти первичный контракт и проверить его на нужном представлении.
\nURL без версии ведёт на текущую страницу. Он не доказывает, что документ выглядел так же в момент чтения. Поэтому журнал хранит pin отдельно: датированный RFC, commit, тег релиза или опубликованный PDF отвечает на вопрос «какой материал открыт». Locator отвечает на вопрос «где искать». Observation отвечает на вопрос «что там написано или возвращено».
\nНужно разделять как минимум три свойства. Происхождение показывает, кто опубликовал документ и к какому объекту он относится. Целостность показывает, не изменилось ли сохранённое представление после захвата. Смысл показывает, поддерживает ли найденный фрагмент именно наше решение. Подписанный файл или совпавший хеш усиливает первые два вывода, но не превращает интерпретацию в доказанный факт.
\nВ HTTP эта граница хорошо видна на примере ETag. RFC 9110 описывает его как валидатор выбранного представления ресурса. Значение может быть непрозрачным: из ETag: \"a81c\" нельзя вывести релиз 8.1, дату публикации или совместимость любого SDK. Правила сравнения зависят от контекста условного запроса. Поэтому честное утверждение звучит так: «Для выбранного HTTP-представления ETag можно использовать в условном запросе по правилам RFC». Вывод «ETag ускоряет наше приложение на 20%» требует уже измерения.
Перед тем как писать совет, заполните одну карточку. Для производственного решения в ней стоит хранить и исходный запрос: язык, заголовки, права, параметры и среду. Иначе две команды могут открыть один URL и получить разные представления.
\nconst claim = {\n statement: 'ETag can participate in a conditional HTTP request',\n scope: 'HTTP; selected representation; target service contract',\n source: {\n url: 'https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.3',\n pin: 'RFC 9110, published June 2022',\n locator: 'section 8.8.3'\n },\n observation: 'ETag is a validator for a selected representation',\n evidence: {\n capturedAt: '2025-11-07T10:00:00Z',\n sha256: 'record-the-local-file-hash'\n },\n nextCheck: 'send a conditional request to the target service'\n};\n\nfunction assess(record) {\n const required = [\n 'statement', 'scope', 'source', 'observation', 'evidence', 'nextCheck'\n ];\n const missing = required.filter((key) => !record[key]);\n const sourceReady = record.source?.url &&\n record.source.pin && record.source.locator;\n const evidenceReady = record.evidence?.capturedAt &&\n record.evidence.sha256;\n\n if (missing.length || !sourceReady || !evidenceReady) {\n return { status: 'hold', missing };\n }\n return { status: 'ready-for-scoped-check' };\n}\n\nconsole.log(assess(claim));\nФункция проверяет структуру карточки, а не истинность утверждения. Статус ready-for-scoped-check означает только, что другой инженер может найти источник и понять следующий шаг. Он не означает, что измерена производительность, проверена авторизация или доказан эффект в конкретном продукте.
Отрицательная ветка обязательна. Если удалить source.pin, изменить scope или оставить пустым хеш, карточка должна перейти в hold. Добавление ещё одной ссылки вокруг текущей страницы не исправит отсутствие версии. Нужно найти неизменяемый первичный материал, открыть locator заново и записать новое наблюдение.
Сначала сохраните представление, которое собираетесь цитировать. Команда ниже фиксирует ответ и его хеш, чтобы позднее обнаружить изменение файла. Она не доказывает, что страница навсегда неизменна, и не заменяет чтение нужного раздела. Каталог создаётся локально: путь следует выбрать по правилам проекта.
\nset -eu\nmkdir -p evidence/rfc-9110\ncurl --fail --silent --show-error --location \\\n --dump-header evidence/rfc-9110/headers.txt \\\n --output evidence/rfc-9110/body.html \\\n 'https://www.rfc-editor.org/rfc/rfc9110.html'\nshasum -a 256 evidence/rfc-9110/body.html \\\n | tee evidence/rfc-9110/body.sha256\ngrep -niE '^(HTTP/|etag:|last-modified:|content-type:)' \\\n evidence/rfc-9110/headers.txt || true\ncurl --fail останавливает команду на HTTP-ошибке, а --location следует редиректам. shasum -a 256 есть в macOS; в Linux его можно заменить на sha256sum. Хеш относится к сохранённому телу, а не к смыслу абзаца и не к версии API. В карточке дополнительно запишите дату, фактический URL после редиректа, заголовки запроса, версию клиента и locator.
Проверка повтора должна сравнивать тот же артефакт, а не только снова открывать URL:
\nshasum -a 256 --check evidence/rfc-9110/body.sha256\nЕсли команда сообщает о несовпадении, сначала выясните, изменился ли файл, кодировка или путь. Не обновляйте вывод автоматически: повторно откройте locator и решите, осталось ли утверждение применимым. Если документ доступен только после авторизации, публичный читатель не сможет воспроизвести представление; это нужно назвать ограничением, а не замаскировать пересказом.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Ссылка открывается, но факт не находится | Нет локатора или он относится к другой версии | Открыть зафиксированный файл и найти раздел заново | Добавить точный locator; при несовпадении поставить hold |
| Совет обещает «быстрее» или «надёжнее» | Нет объекта, условия и метрики | Назвать вход, результат и способ измерения | Сузить фразу или провести отдельный тест |
| Совпадение хеша принимают за доказательство поведения | Целостность смешали со смыслом | Сопоставить observation и claim буквально | Оставить вывод о файле; продуктовый вывод проверить отдельно |
| Документ доступен только после входа | Читатель не может воспроизвести представление | Проверить права, среду и разрешённый способ публикации | Дать общедоступный первичный источник или описать ограничение |
| После обновления API совет ломается | Сохранён только корневой URL | Сравнить релиз, commit, дату и контракт endpoint | Обновить карточку и повторить проверку с новой областью |
Держите несколько уровней, чтобы не выдавать гипотезу за факт. Наблюдение — то, что видно в документе или ответе. Ограниченное утверждение — интерпретация с явно указанной областью. Рекомендация — действие для конкретной системы после проверки входов и рисков. Результат — наблюдаемое изменение в этой системе с методикой измерения.
\nНапример, из RFC можно получить наблюдение о валидаторе выбранного представления и утверждение о семантике условного HTTP-запроса. Нельзя из него сразу получить результат «наша CDN экономит 20% трафика». Для такого вывода нужны URL, размеры представлений, политика кеша, доля условных запросов и повторяемое измерение до и после изменения.
\nЦифровая подпись или хеш усиливают цепочку происхождения, но не расширяют scope. Если подписанный документ описывает режим read-only для версии 2, подпись не подтверждает запись в версии 3. Если официальный источник отвечает на другой вопрос, чем тот, который стоит перед командой, его авторитетность не устраняет несовпадение.
\nЖурнал источников не заменяет эксперимент, ревью угроз или контрактную проверку. Он не измеряет задержку, не проверяет нагрузку и не устанавливает, что API одинаково ведёт себя во всех регионах и ролях. Для производительности нужны входы, условия, число повторов и метрика. Для безопасности нужны модель угроз, права, негативные сценарии и проверка фактической реализации.
\nХеш не защищает от неверного первоисточника, устаревшей спецификации или ошибки копирования. ETag не является универсальным идентификатором релиза. Дата публикации не говорит, что ваш клиент поддерживает описанный режим. Даже официальный документ может быть неполным для конкретной интеграции.
\nМетод также не разрешает конфликт двух корректных источников автоматически. Если версии или области различаются, карточки должны показать различие. Решение появляется после явного критерия: совместимость, дата поддержки, стоимость миграции, риск или измеримый эффект. Нельзя скрывать отсутствие критерия под словом «надёжно».
\nКарточка готова к передаче, когда инженер без устного пояснения может открыть зафиксированный материал, перейти к locator, увидеть observation, назвать scope и выполнить next check. Проверка должна пройти и для отрицательного пути: при отсутствии версии, несовпадении области, изменившемся хеше или подмене факта обещанием статус блокирует рекомендацию.
\nДля практического ревью достаточно задать пять вопросов: какой именно файл или ответ мы проверили; где расположен факт; что подтверждает хеш; какое утверждение разрешено в этой области; какое наблюдение подтвердит действие в нашей системе. Если на последний вопрос отвечают ссылкой, работа ещё не закончена: ссылка даёт исходный материал, а результат должен появиться в отдельном воспроизводимом тесте.
\nСимптом появляется в момент первой ошибки. Код ждёт несколько операций через await Promise.all(tasks), одна операция отклоняется, обработчик сразу переходит в catch, а автор считает остальные операции остановленными. Позже выясняется, что один запрос всё ещё пишет данные, таймер всё ещё выполняется, а зависимая логика уже очистила состояние. Цена ошибки — повторные записи, лишние запросы и расследование, в котором приходится восстанавливать границу ответственности по логам.
Тезис простой: Promise.all объединяет наблюдаемые исходы promises, но не является протоколом отмены работы. Он сообщает результат aggregate promise. Он не получает автоматически право остановить операцию, которая создала входной promise. Чтобы объяснить такой код без ложной гарантии, нужно разделить три объекта: входной promise, aggregate promise и внешнюю работу.
Рецепт становится опасным, когда его показывают раньше задачи. Возьмём учебный сценарий: нужно получить профиль и настройки, а затем построить экран. Если любой результат недоступен, экран строить нельзя. Здесь Promise.all подходит как условие для зависимого шага: зависимый код запускается только после успешного исхода двух входов.
Но это условие не отвечает на другой вопрос: что делать с операцией, которая уже началась и ещё не завершилась? Объединение результатов и остановка работы относятся к разным контрактам. Первый задаёт JavaScript. Второй должен задавать приложение, библиотека или владелец ресурса.
\nПусть в Promise.all переданы profilePromise и settingsPromise. JavaScript создаёт новый aggregate promise. Он выполнится успешно, если все входы выполнятся. Значения попадут в массив в порядке входного iterable, а не в порядке завершения. Если один вход отклонится, aggregate promise отклонится с первой причиной отказа.
Это не означает, что второй вход получил команду отмены. Второй promise может уже завершиться, продолжить ожидание или скрывать за собой работу, которую можно прервать только отдельным API. Вызов catch наблюдает отказ aggregate. Сам по себе он не меняет жизненный цикл запроса, чтения файла, вычисления или записи.
Отсюда следует отрицательный путь. Если профиль отклонился, нельзя выводить «настройки отменены». Можно вывести только «общий результат недоступен с такой причиной». Дальше код обязан использовать собственный контракт: сигнал отмены, закрытие ресурса, остановку очереди, компенсацию или безопасное ожидание остатка. Если такого контракта нет, честный ответ — отмена не определена.
\nНиже намеренно узкий пример. Он не обращается к сети, диску, часам или данным пользователей. Один вход отклоняется сразу. Второй вход удерживается до явного вызова finishRemaining. Это позволяет увидеть порядок событий и не приписывать JavaScript поведение, которого в коде нет.
let finishRemaining;\nconst remaining = new Promise((resolve) => {\n finishRemaining = () => resolve('settings');\n});\n\nconst combined = Promise.all([\n Promise.reject(new Error('profile failed')),\n remaining,\n]).catch(() => 'aggregate handled');\n\nawait combined;\nconsole.log('aggregate rejected');\nfinishRemaining();\nconsole.log(await remaining);\n// aggregate rejected\n// settings\nПосле первой строки вывода aggregate уже обработал отказ. Но второй promise ещё существует. Функция finishRemaining завершает его позже. Пример доказывает только это: rejected aggregate не равен отмене каждого входа. Он не доказывает, как поведёт себя HTTP-клиент, база данных или очередь сообщений. Для каждого такого ресурса нужна отдельная документация и отдельный тест.
Контрпример полезнее общей фразы «promises выполняются параллельно». Это слово слишком широкое. В JavaScript promise представляет состояние будущего результата; он не является универсальным дескриптором процесса, который можно остановить одним методом. Операции могут стартовать до создания aggregate, а их остановка может быть невозможна или требовать согласия внешнего владельца.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
catch сработал, но запрос продолжился | Aggregate наблюдает отказ, а клиент запроса не получил сигнал отмены | Посмотреть API запроса и его обработчик сигнала | Добавить явный cancel-контракт или признать работу неотменяемой |
| Результаты приходят в неожиданном порядке | Порядок завершения перепутан с порядком iterable | Сравнить индексы входов и trace завершения | Читать массив по исходным индексам, а не по времени |
| После одной ошибки обработчик пишет частичный результат | Зависимый шаг запускается не только после fulfilled aggregate | Проверить место вызова и ветки после catch | Разделить partial result и готовый aggregate |
Заменили all на allSettled, но проблема осталась | Нужен был протокол остановки, а изменили только форму отчёта | Назвать требование: все исходы или остановка работы | Выбрать combinator для отчёта и отдельно спроектировать отмену |
| Текст обещает «остановить всё» | Рецепт подменил причинную модель | Попросить показать объект, который посылает cancel | Сузить утверждение до aggregate и добавить отрицательный путь |
Начните с одной проверяемой фразы: «Экран строится только после двух успешных результатов». Затем назовите aggregate promise и покажите, где читается его результат. После этого добавьте отказ одного входа. Читатель должен увидеть, что зависимый шаг не запускается. Только теперь задайте вопрос о втором входе.
\nОтвет должен быть конкретным. Если второй вход уже создан, у него есть собственное состояние. Promise.all не предоставляет в этом вызове метода cancel. Если вход связан с fetch, автор может передать AbortSignal и вызвать AbortController.abort(). Но это уже договор Fetch и конкретного кода приложения, а не свойство Promise.all. Даже сигнал не превращает любую серверную операцию в гарантированно отменённую: сервер мог принять запрос, а клиент мог лишь прекратить ожидание ответа.
Такой ответ не должен звучать как универсальный рецепт отмены. Для учебного фрагмента достаточно показать место ответственности. В production нужно проверить, что делает клиент после abort, что происходит на сервере, как закрывается ресурс и допустима ли повторная попытка. Если эти условия не описаны, статья должна остановиться на границе знания.
\nPromise.all выбирают, когда нужен общий успех всех входов и ранний отказ aggregate при первой ошибке. Promise.allSettled выбирают, когда нужно дождаться и сохранить статус каждого входа. Это разные требования. Переключение на allSettled не отменяет операции и не исправляет частичную запись.
Например, экран может показывать независимые виджеты. Тогда полезно получить массив состояний и отрисовать ошибку только у одного виджета. Но платёжный сценарий, в котором нельзя продолжать без обязательного ответа, требует другой проверки: dependent action не должна начаться после rejected aggregate. Если же один вызов уже создал побочный эффект, combinator не решает вопрос компенсации. Его должен решить доменный контракт.
\nНе стоит заменять Promise.all на последовательный await только ради иллюзии контроля. Последовательный запуск уменьшает число одновременно начатых операций, но не отменяет первую операцию при ошибке второй. Он также меняет задержку и нагрузку. Сначала зафиксируйте требование, затем выбирайте форму ожидания.
Пример выше фиксирует порядок наблюдений в памяти. Он не моделирует latency, сетевые разрывы, retry, таймауты, серверную обработку, блокировки базы, очередь или пользовательскую сессию. Нельзя переносить его вывод как готовую архитектуру. Его задача уже: показать, почему aggregate rejection не доказывает отмену входа.
\nЕсть и другой отрицательный путь: остановка может быть технически доступна, но логически запрещена. Например, сервер уже принял команду, и отмена клиентского ожидания не должна приводить к повторной команде без idempotency key. В таком случае «прервать ожидание» и «отменить побочный эффект» — два разных решения. Текст, который объединяет их одним словом «cancel», скрывает риск.
\nНе заявляйте результат обучения или production-эффект по одному примеру. Можно проверить, что читатель назвал aggregate, вход и отдельный cancel-контракт. Нельзя из этого вывести, что он безопасно спроектирует любой асинхронный pipeline. Для такого вывода нужна другая проверка, привязанная к конкретной системе.
\nОбъяснение готово, если независимый читатель может без подсказки ответить на четыре вопроса: какой результат объединяет Promise.all; что происходит при первом reject; может ли оставшийся вход закончиться позже; кто именно останавливает внешнюю работу. Код должен проходить тесты для fulfilled-пути и для отказа, а текст — не обещать отмену там, где в API нет такого контракта.
Практическая финальная проверка короткая. Уберите названия методов и попросите восстановить модель по событиям. Затем верните код и сравните каждое утверждение с наблюдаемым результатом. Если для фразы «всё остановилось» нельзя назвать объект, который отправил сигнал остановки, замените её на точное утверждение об aggregate. Такая редактура сохраняет полезный рецепт и не переносит его за пределы условий задачи.
\nPromise.allSettled(). Она не обещает автоматическую отмену входов.Симптом появляется в момент первой ошибки. Код ждёт несколько операций через await Promise.all(tasks), одна операция отклоняется, обработчик сразу переходит в catch, а автор считает остальные операции остановленными. Позже выясняется, что один запрос всё ещё пишет данные, таймер всё ещё выполняется, а зависимая логика уже очистила состояние. Цена ошибки — повторные записи, лишние запросы и расследование, в котором приходится восстанавливать границу ответственности по логам.
Тезис простой: Promise.all объединяет наблюдаемые исходы promises, но не является протоколом отмены работы. Он сообщает результат aggregate promise. Он не получает автоматически право остановить операцию, которая создала входной promise. Чтобы объяснить такой код без ложной гарантии, нужно разделить три объекта: входной promise, aggregate promise и внешнюю работу.
Рецепт становится опасным, когда его показывают раньше задачи. Возьмём учебный сценарий: нужно получить профиль и настройки, а затем построить экран. Если любой результат недоступен, экран строить нельзя. Здесь Promise.all подходит как условие для зависимого шага: зависимый код запускается только после успешного исхода двух входов.
Но это условие не отвечает на другой вопрос: что делать с операцией, которая уже началась и ещё не завершилась? Объединение результатов и остановка работы относятся к разным контрактам. Первый задаёт JavaScript. Второй должен задавать приложение, библиотека или владелец ресурса.
\nПусть в Promise.all переданы profilePromise и settingsPromise. JavaScript создаёт новый aggregate promise. Он выполнится успешно, если все входы выполнятся. Значения попадут в массив в порядке входного iterable, а не в порядке завершения. Если один вход отклонится, aggregate promise отклонится с первой причиной отказа.
Это не означает, что второй вход получил команду отмены. Второй promise может уже завершиться, продолжить ожидание или скрывать за собой работу, которую можно прервать только отдельным API. Вызов catch наблюдает отказ aggregate. Сам по себе он не меняет жизненный цикл запроса, чтения файла, вычисления или записи.
Отсюда следует отрицательный путь. Если профиль отклонился, нельзя выводить «настройки отменены». Можно вывести только «общий результат недоступен с такой причиной». Дальше код обязан использовать собственный контракт: сигнал отмены, закрытие ресурса, остановку очереди, компенсацию или безопасное ожидание остатка. Если такого контракта нет, честный ответ — отмена не определена.
\nНиже намеренно узкий пример. Он не обращается к сети, диску, часам или данным пользователей. Один вход отклоняется сразу. Второй вход удерживается до явного вызова finishRemaining. Это позволяет увидеть порядок событий и не приписывать JavaScript поведение, которого в коде нет.
let finishRemaining;\nconst remaining = new Promise((resolve) => {\n finishRemaining = () => resolve('settings');\n});\n\nconst combined = Promise.all([\n Promise.reject(new Error('profile failed')),\n remaining,\n]).catch(() => 'aggregate handled');\n\nawait combined;\nconsole.log('aggregate rejected');\nfinishRemaining();\nconsole.log(await remaining);\n// aggregate rejected\n// settings\nПосле первой строки вывода aggregate уже обработал отказ. Но второй promise ещё существует. Функция finishRemaining завершает его позже. Пример доказывает только это: rejected aggregate не равен отмене каждого входа. Он не доказывает, как поведёт себя HTTP-клиент, база данных или очередь сообщений. Для каждого такого ресурса нужна отдельная документация и отдельный тест.
Контрпример полезнее общей фразы «promises выполняются параллельно». Это слово слишком широкое. В JavaScript promise представляет состояние будущего результата; он не является универсальным дескриптором процесса, который можно остановить одним методом. Операции могут стартовать до создания aggregate, а их остановка может быть невозможна или требовать согласия внешнего владельца.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
catch сработал, но запрос продолжился | Aggregate наблюдает отказ, а клиент запроса не получил сигнал отмены | Посмотреть API запроса и его обработчик сигнала | Добавить явный cancel-контракт или признать работу неотменяемой |
| Результаты приходят в неожиданном порядке | Порядок завершения перепутан с порядком iterable | Сравнить индексы входов и trace завершения | Читать массив по исходным индексам, а не по времени |
| После одной ошибки обработчик пишет частичный результат | Зависимый шаг запускается не только после fulfilled aggregate | Проверить место вызова и ветки после catch | Разделить partial result и готовый aggregate |
Заменили all на allSettled, но проблема осталась | Нужен был протокол остановки, а изменили только форму отчёта | Назвать требование: все исходы или остановка работы | Выбрать combinator для отчёта и отдельно спроектировать отмену |
| Текст обещает «остановить всё» | Рецепт подменил причинную модель | Попросить показать объект, который посылает cancel | Сузить утверждение до aggregate и добавить отрицательный путь |
Начните с одной проверяемой фразы: «Экран строится только после двух успешных результатов». Затем назовите aggregate promise и покажите, где читается его результат. После этого добавьте отказ одного входа. Читатель должен увидеть, что зависимый шаг не запускается. Только теперь задайте вопрос о втором входе.
\nОтвет должен быть конкретным. Если второй вход уже создан, у него есть собственное состояние. Promise.all не предоставляет в этом вызове метода cancel. Если вход связан с fetch, автор может передать AbortSignal и вызвать AbortController.abort(). Но это уже договор Fetch и конкретного кода приложения, а не свойство Promise.all. Даже сигнал не превращает любую серверную операцию в гарантированно отменённую: сервер мог принять запрос, а клиент мог лишь прекратить ожидание ответа.
Такой ответ не должен звучать как универсальный рецепт отмены. Для учебного фрагмента достаточно показать место ответственности. В production нужно проверить, что делает клиент после abort, что происходит на сервере, как закрывается ресурс и допустима ли повторная попытка. Если эти условия не описаны, статья должна остановиться на границе знания.
\nЕсли входами являются fetch-запросы, общий сигнал делает отмену явной. Сохраните следующий фрагмент как load-pages.mjs и запустите на Node.js 18 или новее командой node load-pages.mjs. URL-ы должны быть доступны вашему тестовому серверу: один endpoint верните с ошибкой, другой задержите.
async function loadPages(urls) {\n const controller = new AbortController();\n const { signal } = controller;\n\n try {\n return await Promise.all(\n urls.map(async (url) => {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${url}`);\n }\n return response.json();\n }),\n );\n } catch (error) {\n controller.abort();\n throw error;\n }\n}\nЭтот код отменяет только pending fetch, которым передан тот же сигнал. Он не откатывает данные, которые сервер уже принял, и не влияет на операцию, игнорирующую signal. Поэтому тест должен проверять две вещи: второй клиентский запрос получил abort, а серверная команда не требует повторения без идемпотентного ключа.
Для полностью детерминированной проверки границы сети не нужно симулировать production-сервис. Поднимите тестовый endpoint с управляемой задержкой, добавьте в лог request id и сравните время abort() с серверным trace. Если сервер успел выполнить побочный эффект, результатом проверки будет не «всё отменено», а зафиксированное правило компенсации или сверки.
Promise.all выбирают, когда нужен общий успех всех входов и ранний отказ aggregate при первой ошибке. Promise.allSettled выбирают, когда нужно дождаться и сохранить статус каждого входа. Это разные требования. Переключение на allSettled не отменяет операции и не исправляет частичную запись.
Например, экран может показывать независимые виджеты. Тогда полезно получить массив состояний и отрисовать ошибку только у одного виджета. Но платёжный сценарий, в котором нельзя продолжать без обязательного ответа, требует другой проверки: dependent action не должна начаться после rejected aggregate. Если же один вызов уже создал побочный эффект, combinator не решает вопрос компенсации. Его должен решить доменный контракт.
\nНе стоит заменять Promise.all на последовательный await только ради иллюзии контроля. Последовательный запуск уменьшает число одновременно начатых операций, но не отменяет первую операцию при ошибке второй. Он также меняет задержку и нагрузку. Сначала зафиксируйте требование, затем выбирайте форму ожидания.
Пример выше фиксирует порядок наблюдений в памяти. Он не моделирует latency, сетевые разрывы, retry, таймауты, серверную обработку, блокировки базы, очередь или пользовательскую сессию. Нельзя переносить его вывод как готовую архитектуру. Его задача уже: показать, почему aggregate rejection не доказывает отмену входа.
\nЕсть и другой отрицательный путь: остановка может быть технически доступна, но логически запрещена. Например, сервер уже принял команду, и отмена клиентского ожидания не должна приводить к повторной команде без idempotency key. В таком случае «прервать ожидание» и «отменить побочный эффект» — два разных решения. Текст, который объединяет их одним словом «cancel», скрывает риск.
\nНе заявляйте production-эффект по одному примеру. Проверка должна установить, что aggregate, входной promise и cancel-контракт связаны наблюдаемыми событиями. Она не доказывает безопасность любого асинхронного pipeline; для этого нужен отдельный тест конкретной системы.
\nОбъяснение готово, если независимый читатель может без подсказки ответить на четыре вопроса: какой результат объединяет Promise.all; что происходит при первом reject; может ли оставшийся вход закончиться позже; кто именно останавливает внешнюю работу. Код должен проходить тесты для fulfilled-пути и для отказа, а текст — не обещать отмену там, где в API нет такого контракта.
Практическая финальная проверка короткая. Уберите названия методов и попросите восстановить модель по событиям. Затем верните код и сравните каждое утверждение с наблюдаемым результатом. Если для фразы «всё остановилось» нельзя назвать объект, который отправил сигнал остановки, замените её на точное утверждение об aggregate. Такой порядок сохраняет полезный рецепт и не переносит его за пределы условий задачи.
\nPromise.all описывает aggregate outcome, входной iterable и обработку отказа, но не задаёт отмену внешних ресурсов.Promise.allSettled().AbortController.abort() и результата abort для Fetch.Сервис собирает профиль пользователя из трёх запросов: настройки, лимиты и историю операций. Один запрос завершается ошибкой. Обработчик сразу попадает в catch и возвращает ответ об ошибке. Через секунду другой запрос всё ещё пишет в лог, держит соединение или меняет локальное состояние.
Симптом — разработчик видит отклонённый Promise.all и говорит: «набор остановился». Цена ошибки — лишняя нагрузка, гонка за общим состоянием и неверная очистка ресурсов. Код может повторить операцию, пока первый запуск ещё работает. Расследование усложняется: aggregate уже отклонён, а позднее событие живёт в другом promise.
Тезис простой: Promise.all сообщает исход группы. Он не является командой отмены для входных promise и не знает, какая внешняя операция их породила. Ранний reject останавливает ожидание успешного aggregate, но не доказывает остановку работы. Для остановки нужен отдельный контракт: владелец сигнала, способ передать его операции и подтверждение результата.
Первый объект — входной promise. Он представляет один будущий исход: значение или ошибку. Второй — aggregate promise, который возвращает Promise.all(iterable). Он собирает значения по позиции входного iterable и принимает решение о собственном исходе. Третий — внешняя операция: HTTP-запрос, чтение файла, запрос к базе или вычисление. Она может существовать за пределами promise-модели.
Связь направлена только в одну сторону. Вход сообщает aggregate, что он fulfilled или rejected. Aggregate сообщает вызывающему коду свой исход. Такая связь не содержит команды «остановись» для другого входа. Даже после reject aggregate другой вход может позже fulfilled или rejected.
\nconst left = Promise.reject(new Error('parse failed'));\nconst right = new Promise((resolve) => {\n setTimeout(() => resolve('right is done'), 100);\n});\n\ntry {\n await Promise.all([left, right]);\n} catch (error) {\n console.log('aggregate rejected:', error.message);\n}\n\n// Это не команда отмены для right.\nЗдесь Promise.all отклоняется из-за left. Ожидание текущей функции заканчивается. Но таймер получил отдельное право завершить right. Никакой код не передал ему сигнал отмены. Поэтому позднее значение появится, даже если вызывающий код больше его не читает.
Это учебный пример с фиксированными promise и таймером. Он не измеряет задержку сети, поведение браузера или расход ресурсов в production. Он проверяет только границу между исходом aggregate и исходом другого входа.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
catch сработал, но поздний лог продолжился | Другой вход завершает собственную работу | Добавить trace для каждого входа | Не называть aggregate отменой; найти владельца |
| Значения пришли в неожиданном порядке | Ожидалась скорость, а не позиция iterable | Сравнить индексы и порядок событий | Читать результат по позиции или хранить ключ |
| Нужно увидеть каждую ошибку, но видна одна | Promise.all завершает aggregate на первом reject | Проверить требование к полному отчёту | Рассмотреть Promise.allSettled |
| После ошибки запросы продолжаются | Операции не получили общий сигнал | Проверить API и обработчик сигнала | Передать AbortSignal или другой cancel contract |
| Остановка считается доказанной по reject | Смешаны исход результата и управление ресурсом | Назвать реальное действие прерывания | Разделить зависимую логику и протокол остановки |
При успешном исходе массив значений сохраняет порядок входного iterable, а не порядок завершения. Быстрый второй promise всё равно окажется во второй позиции. Это удобно для сопоставления, пока массив не меняется между запуском и чтением.
\nПри reject aggregate получает ошибку входа, который первым сообщил отклонение в рамках алгоритма наблюдения. Это не отчёт обо всех ошибках и не журнал времени завершения. Promise.allSettled решает другую задачу: ждёт завершения всех входов и возвращает статус каждого. Он тоже не добавляет отмену.
Для операции, которая умеет принимать сигнал, отмену можно связать с обработкой aggregate. В браузерном или серверном коде это часто AbortController и его signal. Контроллер создаёт вызывающий код. Операции получают сигнал. При ошибке вызывающий код вызывает abort(). Конкретный API должен обработать сигнал по своему контракту.
async function loadPages(urls) {\n const controller = new AbortController();\n const signal = controller.signal;\n\n try {\n return await Promise.all(\n urls.map((url) => fetch(url, { signal }).then((response) => {\n if (!response.ok) throw new Error('HTTP ' + response.status);\n return response.json();\n })),\n );\n } catch (error) {\n controller.abort();\n throw error;\n }\n}\nФрагмент ограничен операциями fetch, которые получили сигнал. Он не отменяет синхронную функцию, уже записанную транзакцию или promise, который игнорирует signal. abort() означает запрос на остановку поддерживаемой операции, а не откат каждого побочного эффекта.
Запишите события каждого уровня. Первый promise отклоняет aggregate. Затем ручной владелец второго promise вызывает сохранённый resolve. Ожидаемая последовательность показывает два факта: aggregate rejected, а другой вход позже fulfilled.
const trace = [];\nlet finishRight;\n\nconst left = Promise.reject(new Error('left failed'));\nconst right = new Promise((resolve) => {\n finishRight = () => {\n trace.push('right fulfilled');\n resolve('ok');\n };\n});\n\nawait Promise.all([left, right]).catch(() => trace.push('aggregate rejected'));\nfinishRight();\nawait right;\n\nconsole.log(trace);\n// ['aggregate rejected', 'right fulfilled']\nТрассировка не доказывает, что реальный запрос продолжился: в ней нет запроса. Она доказывает более узкое утверждение о promise, которым мы управляем вручную. Для сетевой отмены нужен отдельный тест API, который принимает сигнал, и проверка его наблюдаемого завершения.
\nPromise.all, Promise.allSettled или другой способ композиции.Promise.all не обещает параллельное выполнение в смысле потоков. JavaScript может передать управление между асинхронными продолжениями, а конкретный API сам определяет выполнение. Нельзя выводить пропускную способность, порядок сетевых пакетов или освобождение ресурса из одного вызова combinator.
Нельзя считать abort() универсальным откатом. Он не возвращает отправленные данные, не отменяет побочный эффект на сервере и не исправляет уже завершённую запись. Он также не заменяет дедупликацию, идемпотентность и таймаут.
Нельзя выбирать allSettled только потому, что первая ошибка неудобна. Если следующий шаг требует всех значений, ранний reject не даёт начать неполный расчёт. Если нужен отчёт о каждом независимом входе, подходит all-settled-семантика. Решение зависит от зависимости между работами.
Разбор готов, когда команда отвечает на четыре вопроса: какой promise отклоняет aggregate; какой вход может завершиться позже; какое действие останавливает внешнюю работу; какое наблюдение подтверждает остановку. Учебный код готов, если trace показывает независимые исходы. Код с реальными операциями готов, если тест проверяет поддержку сигнала и отрицательный путь, где один вход отвергнут, а другой ещё выполняется.
\nЕсли на вопрос об остановке отвечают «Promise.all», модель не готова. Добавьте владельца отмены или честно зафиксируйте, что операция не поддерживает остановку.
AbortController для отмены поддерживаемых fetch-запросов.Сервис собирает профиль из настроек, лимитов и истории операций. Код запускает три запроса через Promise.all. Если настройки отвечают ошибкой, обработчик сразу получает отказ и возвращает клиенту ошибку. В это время история может ещё выполняться: соединение занято, лог продолжает поступать, а завершившийся запрос может попытаться обновить общее состояние.
Такой симптом часто описывают словами «Promise.all остановил набор». Это неточное объяснение. Если его принять за контракт отмены, команда начнёт повторять сбор профиля поверх ещё работающего запуска или решит, что серверный побочный эффект уже невозможен. Правильный вопрос звучит иначе: какой объект сообщил об ошибке и каким механизмом владелец работы может остановить саму работу?
В этой ситуации есть три разных объекта. Входной promise представляет один будущий исход: значение или ошибку. Aggregate promise, который вернул Promise.all, представляет решение о всей группе. Внешняя операция — сетевой запрос, чтение файла, обращение к базе или вычисление — создаётся конкретным API и может иметь собственный жизненный цикл.
Promise.all связывает исходы первых двух уровней. Он подписывается на входы, ждёт их успешного завершения и кладёт значения в массив по позиции входного iterable. Первый отказ переводит aggregate в состояние rejected. В этом алгоритме нет универсальной команды, которая должна остановить остальные входы или ресурс, породивший их.
У метода есть две полезные и проверяемые гарантии. При успехе он возвращает массив значений в порядке входного iterable, даже если второй запрос завершился раньше первого. При отказе он отклоняет возвращённый promise с причиной отказа входа, который первым отклонился. После этого ожидание aggregate заканчивается, но остальные входы всё ещё могут перейти в свой исход.
\nСлово «первым» относится к моменту, когда отказ наблюдён для aggregate, а не к позиции элемента и не к полному отчёту о всех ошибках. Последующие входы всё равно получают обработчики через внутреннюю композицию. Поэтому поздний отказ не обязан превращаться в отдельное необработанное исключение, а позднее выполнение не исчезает только потому, что вызывающий код перестал ждать aggregate.
\nconst trace = [];\nconst task = (name, delay, shouldReject = false) => new Promise((resolve, reject) => {\n setTimeout(() => {\n trace.push(name + ': finished');\n if (shouldReject) {\n reject(new Error(name + ': failed'));\n return;\n }\n resolve(name);\n }, delay);\n});\n\nconst tasks = [\n task('settings', 10, true),\n task('limits', 40),\n task('history', 70),\n];\n\nawait Promise.all(tasks).catch((error) => {\n trace.push('aggregate: ' + error.message);\n});\n\nconsole.log(trace);\n// ['settings: finished', 'aggregate: settings: failed']\n\nawait Promise.allSettled(tasks);\nconsole.log(trace);\n// ['settings: finished', 'aggregate: settings: failed',\n// 'limits: finished', 'history: finished']\nСохраните фрагмент в promise-all-trace.mjs и запустите командой node promise-all-trace.mjs в Node.js с поддержкой ES-модулей. Первый вывод появляется примерно через 10 мс, второй — после окончания всех таймеров. Точные интервалы зависят от планировщика, но порядок событий задаётся задержками: aggregate уже отклонён, когда два других входа ещё не завершились.
Это намеренно маленький тест. Таймер не моделирует TCP-соединение, запрос к базе или отмену на сервере. Он воспроизводит только границу между состоянием aggregate и состояниями уже запущенных входов.
\nРезультат успешного вызова сопоставляется с исходным массивом, а не со скоростью задач. Вызов Promise.all([loadSettings(), loadHistory()]) вернёт настройки в позиции 0, историю в позиции 1, даже если история пришла раньше. Это делает композицию удобной, пока список входов не меняется между запуском и чтением.
Если нужны статусы всех независимых операций, выбирайте Promise.allSettled. Он ждёт завершения каждого входа и возвращает элементы вида { status: 'fulfilled', value } или { status: 'rejected', reason }. Этот метод меняет форму отчёта, но не добавляет отмену: ни all, ни allSettled не знают, как остановить произвольную работу.
| Метод | Когда выбирать | Что возвращает | Чего не делает |
|---|---|---|---|
Promise.all | Нужны все значения, и один отказ делает результат непригодным | Массив значений по позициям или первая причина отказа aggregate | Не отменяет остальные операции |
Promise.allSettled | Каждый вход нужно учесть независимо от его исхода | Массив статусов всех входов после их завершения | Не отменяет и не исправляет побочные эффекты |
Promise.race | Нужен первый завершившийся исход, например гонка с таймером | Значение или ошибка первого settled promise | Таймером нельзя отменить проигравшую работу |
Promise.any | Достаточно первого успешного результата | Первое fulfilled-значение или AggregateError, если все отказали | Не останавливает остальные попытки |
Например, таймаут через Promise.race меняет то, что увидит вызывающий код, но сам по себе не выключает медленный запрос. Если запрос дорогой или меняет данные, таймаут должен идти вместе с реальным контрактом отмены либо с отдельной защитой от повторной операции.
Отмена должна иметь владельца и канал связи. В браузерном Fetch API таким каналом служит AbortSignal: вызывающий код создаёт AbortController, передаёт его сигнал каждой поддерживаемой операции и вызывает abort(), когда группа больше не нужна. Сам Promise.all при этом остаётся только агрегатором.
async function fetchJson(url, signal) {\n const response = await fetch(url, { signal });\n if (!response.ok) {\n throw new Error(url + ': HTTP ' + response.status);\n }\n return response.json();\n}\n\nasync function loadProfile(urls) {\n const controller = new AbortController();\n const requests = urls.map((url) => fetchJson(url, controller.signal));\n\n try {\n return await Promise.all(requests);\n } catch (error) {\n controller.abort();\n throw error;\n }\n}\n\ntry {\n const [settings, limits, history] = await loadProfile([\n 'https://api.example.test/settings',\n 'https://api.example.test/limits',\n 'https://api.example.test/history',\n ]);\n console.log({ settings, limits, history });\n} catch (error) {\n console.error('profile failed:', error.message);\n}\nВ примере ошибка одного fetch попадает в catch, после чего контроллер посылает сигнал всем трём запросам. Поддерживаемый браузером запрос обычно завершается отказом, связанным с abort. Между исходной ошибкой и вызовом abort() есть короткая гонка: операция могла уже завершиться, поэтому код не должен обещать, что каждый запрос будет остановлен.
В прикладном коде контроллер часто принадлежит экрану, обработчику запроса или задаче верхнего уровня. Тогда функция принимает внешний signal, а не создаёт скрытый контроллер, чтобы закрытие страницы или отмена родительской задачи тоже дошли до fetch. Контракт следует описать явно: кто вызывает abort(), какие API принимают сигнал и какое состояние видит вызывающий код после отмены.
AbortController работает только там, где операция действительно слушает переданный сигнал. Он не прерывает синхронный цикл, обычный promise с игнорируемым аргументом, уже отправленную сервером команду или запись, которая успела зафиксироваться до отмены. Для базы данных, очереди и внешнего сервиса нужен их собственный протокол: отмена запроса, дедлайн, идемпотентный ключ или компенсационное действие.
Отмена также не равна откату. Клиент может прекратить ждать тело ответа, а сервер уже успеть списать деньги, создать задачу или записать событие. Поэтому границу побочных эффектов проверяют на стороне исполнителя. Для повторных запусков добавляют идемпотентность и корреляционный идентификатор; для диагностики логируют начало, отмену, успешное завершение и причину отказа каждой операции.
\nasync function cancellableStep(signal) {\n if (signal.aborted) {\n throw signal.reason ?? new Error('cancelled before start');\n }\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => resolve('done'), 100);\n signal.addEventListener('abort', () => {\n clearTimeout(timer);\n reject(signal.reason ?? new Error('cancelled'));\n }, { once: true });\n });\n}\n\nconst controller = new AbortController();\nconst work = cancellableStep(controller.signal);\ncontroller.abort(new Error('caller no longer needs the result'));\n\nawait work.catch((error) => console.log(error.message));\n// caller no longer needs the result\nЭтот фрагмент показывает минимальный контракт для собственной операции: она проверяет сигнал до старта, слушает событие во время ожидания и освобождает таймер при отмене. Для реальной работы нужно также снять обработчик после обычного завершения, закрыть ресурс и отдельно решить, что делать с уже выполненным побочным эффектом.
\n| Наблюдение | Гипотеза | Проверка | Следующее действие |
|---|---|---|---|
Общий catch сработал, но поздний лог продолжается | Другой вход ещё выполняется | Добавить started, fulfilled, rejected и cancelled с request id | Разделить исход aggregate и жизненный цикл операции |
| Результаты «перепутались» | Сопоставили скорость завершения с позицией массива | Вывести имя входа и его индекс | Читать массив по позиции или возвращать объект с ключом |
| Видна только одна ошибка | Выбран fail-fast отчёт | Проверить, нужен ли полный список статусов | Для независимых входов рассмотреть allSettled |
| После отказа лишние запросы расходуют ресурс | Операции не получили общий сигнал | Проверить сигнатуру API и тест отмены | Передать AbortSignal или реализовать собственный cancel contract |
| После abort данные всё равно изменились | Серверный эффект уже произошёл | Сверить корреляционный id с журналом исполнителя | Добавить идемпотентность или компенсацию, а не обещать откат |
Promise.all, Promise.allSettled, race или any.Promise — это модель будущего значения и его исхода, а не поток и не диспетчер ресурсов. Из самого факта совместного запуска нельзя вывести число потоков, порядок сетевых пакетов, освобождение соединения или нагрузку на сервер. Эти свойства определяются средой исполнения и конкретным API.
\nРанний отказ aggregate подходит, когда зависимый следующий шаг больше нельзя выполнять с неполным набором данных. Он не подходит как единственный механизм остановки дорогих независимых задач. allSettled полезен для отчёта о независимых результатах, но не превращает ошибки в успех и не скрывает необходимость очистки.
Пример с fetch применим к операциям, которые принимают AbortSignal. Пример с собственной функцией применим только после того, как операция реально проверяет сигнал и освобождает свой ресурс. Для CPU-bound вычисления, транзакции и внешней очереди нужны отдельные ограничения и тесты.
allSettled.Итоговая проверка проста: назовите promise, который отклоняет aggregate; назовите работу, которая может продолжиться; назовите владельца сигнала; покажите тест, подтверждающий остановку или её отсутствие. Если на последний вопрос отвечают только «сработал catch», в коде смешаны два разных контракта.
Симптом появляется после первой же ошибки: один запрос отклонился, обработчик получил исключение, а соседние запросы продолжают выполняться. В логах уже виден общий отказ, но база данных, сетевой клиент или внешний сервис ещё получают работу. Цена ошибки — лишний трафик, ненужные записи, гонки при очистке и неверное сообщение пользователю. Если эту границу объяснить неточно, команда начнёт искать отмену в месте, которое умеет только ждать.
\nТезис простой: Promise.all описывает исход группы promises, а не жизненный цикл операций, которые за ними стоят. Aggregate promise может отклониться на первой ошибке. Остальные входы при этом не получают приказ остановиться. Значит, ожидание и отмена — два разных контракта. Их нужно проектировать и проверять раздельно.
Представьте два входа: left и right. Каждый из них обещает значение в будущем. Вызов Promise.all([left, right]) создаёт третий promise. Он становится успешным, когда все входы успешны, и отклоняется, когда один вход отклоняется. При успехе значения идут в том же порядке, что и входы.
Третий promise не владеет внутренней работой left и right. Он наблюдает их состояния и сообщает общий результат. Если left завершился ошибкой, aggregate promise может завершиться сразу. Уже начатый right не обязан завершаться в тот же момент. Если right сам изменяет состояние внешней системы, наблюдение за ним не откатывает это изменение.
Учебный пример ниже не обращается к сети, файлам или базе. Он вручную завершает второй promise после того, как общий promise уже отклонился. Так видна только семантика ожидания. Пример не доказывает, что какой-либо реальный клиент продолжит работу именно с такой задержкой.
\nlet finishRight;\n\nconst right = new Promise((resolve) => {\n finishRight = () => {\n console.log('right finished');\n resolve('cache value');\n };\n});\n\nconst combined = Promise.all([\n Promise.reject(new Error('left failed')),\n right,\n]).catch(() => {\n console.log('combined rejected');\n});\n\nawait combined;\nfinishRight();\nawait right;\nСначала появится combined rejected. Затем появится right finished. Это не означает, что aggregate promise «забыл» второй вход. Он уже сообщил общий отказ, но второй вход всё ещё может перейти в состояние fulfilled. Ожидание результата группы не стало командой отмены.
Важна и другая деталь. Promise.all не создаёт сами операции. К моменту вызова promises часто уже запущены: функция вернула promise, сетевой запрос уже отправлен, чтение уже поставлено в очередь. Обёртка видит результат этой работы, но не получает универсального доступа к её внутренним ресурсам.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| После первой ошибки соседний запрос всё ещё виден в логах | Aggregate promise сообщает отказ, но не отменяет вход | Добавить отдельные метки старта и завершения каждой операции | Решить, нужна ли отмена, и передать сигнал в API операции |
| Код ловит общий отказ и считает работу законченной | Смешаны состояние aggregate promise и состояние ресурсов | Проверить, что происходит с каждым входом после catch | Дождаться нужных cleanup-действий или описать их отдельный контракт |
| Пользователь нажал «отмена», но запрос продолжился | Кнопка меняет интерфейс, а сигнал не дошёл до транспорта | Проверить прохождение AbortSignal до вызова API | Связать действие пользователя с поддерживаемым механизмом отмены |
| После ошибки меняется общий объект | Одна из операций имеет побочный эффект | Сравнить состояние до запуска, после отказа и после завершения остальных входов | Сделать операцию идемпотентной, добавить компенсацию или изменить порядок |
| В сообщении указано «все запросы остановлены» | Вывод сделан по первой ошибке, а не по наблюдению ресурсов | Найти подтверждение остановки для каждого участника группы | Сузить формулировку до «общий результат отклонён» |
Отмена появляется только там, где её поддерживает исполнитель. Для браузерного fetch обычно передают AbortSignal, полученный от AbortController. Контроллер не превращает любой promise в отменяемый. Он передаёт сигнал в конкретный API, а API решает, как остановить или прервать свою работу.
const controller = new AbortController();\n\nconst requests = [\n fetch('/api/profile', { signal: controller.signal }),\n fetch('/api/limits', { signal: controller.signal }),\n];\n\ntry {\n const responses = await Promise.all(requests);\n // Обрабатываем ответы только после успеха всей группы.\n} catch (error) {\n if (error.name === 'AbortError') {\n // Это отдельный путь отмены, а не обычная ошибка данных.\n }\n throw error;\n}\n\n// Вызывается по явному решению приложения, например по кнопке.\ncontroller.abort();\nВ этом фрагменте есть важное ограничение: вызов abort() стоит после try только для наглядности механизма. В рабочем коде его вызывают из отдельного события или политики таймаута. Если один fetch уже успел изменить серверное состояние, прекращение ожидания ответа не отменит это изменение. Для серверной операции нужен серверный контракт: ключ идемпотентности, отмена задания, транзакция или компенсационное действие.
Не следует использовать контроллер как универсальный откат. Он помогает остановить поддерживаемую передачу данных. Он не возвращает уже отправленный платёж, не удаляет запись и не отменяет произвольную функцию, которая просто вернула promise.
\nПоложительный путь начинается с успешных входов. Тогда aggregate promise возвращает массив значений в порядке входного массива. Это удобно, когда зависимый код может начать работу только после получения всех значений. Проверка должна подтвердить не только наличие двух ответов, но и соответствие позиции: первый результат относится к первому входу.
\nОтрицательный путь начинается с отклонения одного входа. Aggregate promise сообщает ошибку, но у приложения остаются вопросы. Нужно ли дождаться остальных результатов? Нужно ли отменить их? Можно ли показать частичные данные? Нужна ли компенсация побочного эффекта? На эти вопросы Promise.all не отвечает. Если нужен итог каждого входа, включая ошибки, применяют Promise.allSettled. Если нужна остановка, добавляют API отмены. Если нужна повторная попытка, описывают её лимит и область действия отдельно.
Отдельно проверяйте поздние события. Отмена может прийти после ответа сервера. Таймаут может сработать после записи на сервере. Первый отказ может появиться раньше, чем второй запрос заметит закрытый сигнал. Обработчик должен различать «мы перестали ждать», «транспорт получил отмену» и «внешняя операция действительно прекратилась». Это три разных наблюдения.
\nЭта модель объясняет семантику группы promises, но не задаёт политику приложения. Она не решает, какой запрос важнее, как долго ждать, сколько раз повторять и как сообщать частичный результат. Для этих решений нужны отдельные правила и наблюдаемые сигналы.
\nОна также не заменяет документацию конкретной библиотеки. Один клиент может поддерживать AbortSignal, другой — собственный метод отмены, третий — только закрытие соединения. Нельзя переносить поведение одного транспорта на другой по одному имени promise.
Учебный код с ручным finishRight показывает порядок переходов в памяти. Он не является нагрузочным тестом, не измеряет задержки и не подтверждает поведение production-системы. В реальном сервисе проверяйте контракт библиотеки, логи транспорта и состояние внешнего ресурса.
Объяснение и код готовы, если читатель может ответить на четыре вопроса без догадки: какой promise сообщает общий результат, что происходит с остальными входами после первой ошибки, какой объект или API действительно принимает отмену и что происходит с уже выполненным побочным эффектом. В проверочном запуске должны быть видны отдельные события для aggregate promise и каждой операции. Для сценария отмены нужно подтвердить получение сигнала исполнителем, а для серверного изменения — отдельное подтверждение остановки или компенсации.
\nЕсли на вопрос об остановке отвечают только названием Promise.all, объяснение не готово. Добавьте контрпример и укажите владельца отмены. Если такого владельца нет, честный результат звучит так: общий promise отклонился, а начатая работа может продолжиться.
Promise.allSettled().Симптом появляется после первой же ошибки: один запрос отклонился, обработчик попал в catch, а соседний запрос продолжил выполняться. В логах уже виден общий отказ, но сетевой клиент, чтение файла или внешний сервис ещё получает работу. Цена ошибки — лишний трафик, гонка за общим состоянием и сообщение пользователю, которое не совпадает с реальным состоянием ресурсов.
Главная граница такова: Promise.all сообщает исход группы promises, но не является командой остановки. Aggregate promise отклоняется при первом отклонении входа, однако остальные входы не получают от этого сигнала отмены. Поэтому в объяснении нужно разделять три вещи: исход aggregate, исход каждого входного promise и состояние внешней операции, которая за ним стоит.
Вызов Promise.all(iterable) создаёт новый promise. Если каждый элемент iterable успешно завершился, новый promise получает массив значений. Позиции массива соответствуют позициям входов, а не скорости их завершения. Если один вход отклоняется, aggregate отклоняется с причиной этого отклонения и больше не ждёт успешного результата группы.
При этом Promise.all не запускает отмену и не откатывает побочные эффекты. К моменту вызова функции вроде fetchProfile() или readFile() работа обычно уже началась. Комбинатор добавляет обработчики к полученным promises и собирает их исходы; у него нет универсального доступа к сокету, таймеру, файловому дескриптору или транзакции.
Сначала проверим только семантику promises, без сети и базы данных. Первый вход отклоняется сразу, второй вручную завершается позднее. Запустите фрагмент в Node.js с поддержкой ES-модулей: команда не требует пакетов и показывает два независимых события.
\nnode --input-type=module <<'EOF'\nlet finishRight;\n\nconst right = new Promise((resolve) => {\n finishRight = () => {\n console.log('right fulfilled');\n resolve('cache value');\n };\n});\n\nconst combined = Promise.all([\n Promise.reject(new Error('left failed')),\n right,\n]).catch(() => {\n console.log('aggregate rejected');\n});\n\nawait combined;\nfinishRight();\nawait right;\nEOF\nОжидаемый вывод: сначала aggregate rejected, затем right fulfilled. Второй promise не «забыт»: у него остаётся собственный обработчик и возможность перейти из pending в fulfilled. Пример не имитирует поведение сетевого транспорта и не измеряет освобождение ресурсов. Он доказывает более узкую границу: отклонение результата группы не меняет состояние другого promise.
Порядок значений проверяется отдельно. Если быстрый запрос стоит вторым, он всё равно попадёт во вторую позицию успешного массива. Поэтому при деструктуризации вроде const [profile, limits] = await Promise.all([...]) порядок массива должен быть закреплён тестом или заменён явными ключами.
| Симптом | Что реально произошло | Проверка | Действие |
|---|---|---|---|
catch сработал, а поздний лог продолжился | Aggregate уже отклонён, другой вход ещё выполняется | Добавить метки старта и завершения для каждого входа | Не называть общий отказ отменой; найти API остановки |
| Результаты перепутались | Порядок входного iterable принят за порядок завершения | Записать имя операции рядом с индексом результата | Закрепить порядок или возвращать объект с ключами |
| Видна только одна ошибка | Promise.all сообщает первый отказ, а не полный отчёт | Проверить, нужен ли исход каждого входа | Для полного отчёта рассмотреть Promise.allSettled |
После ошибки соседний fetch продолжился | Запрос не получил сигнал или исполнитель его не поддерживает | Проверить передачу signal до транспорта | Передать общий AbortSignal и проверить событие отмены |
| После отмены серверная запись осталась | Остановлено ожидание ответа, а не уже выполненный побочный эффект | Проверить состояние на сервере отдельным запросом | Использовать идемпотентность, транзакцию или компенсацию |
Для независимых чтений Promise.all подходит, когда следующий шаг требует всех значений. Например, экран можно построить только после получения профиля и лимитов. Вызовы функций ставят работу в очередь до ожидания aggregate: Promise.all([loadProfile(), loadLimits()]). Передача самих функций — ошибка: Promise.all([loadProfile, loadLimits]) передаст обычные значения-функции, а не результаты их вызова.
Если нужны все исходы, включая ошибки, выбирайте другой контракт. Promise.allSettled ждёт, пока каждый вход завершится, и возвращает для каждого статус fulfilled или rejected. Это не добавляет отмену и не делает операции безопаснее; метод лишь меняет то, когда и в каком виде приложение получает отчёт.
Если работы зависят друг от друга, не следует искусственно объединять их в один массив. Последовательный код с отдельными проверками может быть понятнее и дешевле: сначала получить идентификатор, затем запросить данные по нему. Параллельный запуск оправдан только для действительно независимых операций и при принятом риске их одновременного выполнения.
\nОтмена появляется там, где конкретный исполнитель принимает сигнал. Для браузерного fetch это обычно AbortController и его signal. Один контроллер можно передать нескольким запросам. Вызов abort() отправит сигнал каждому поддерживаемому запросу, но не превратит произвольную функцию, таймер или уже выполненную запись в отменяемую операцию.
async function loadDashboard() {\n const controller = new AbortController();\n const { signal } = controller;\n\n const json = (url) => fetch(url, { signal }).then((response) => {\n if (!response.ok) {\n throw new Error(url + ': HTTP ' + response.status);\n }\n return response.json();\n });\n\n try {\n return await Promise.all([\n json('/api/profile'),\n json('/api/limits'),\n ]);\n } catch (error) {\n controller.abort();\n throw error;\n }\n}\nЗдесь вторая проверка намеренная. fetch обычно выполняется успешно на уровне promise даже при HTTP 404 или 500: сервер ответил, поэтому нужно самостоятельно проверить response.ok или response.status. Если проверка обнаружит HTTP-ошибку, catch вызовет abort() для ещё работающего соседа.
Фрагмент гарантирует только контракт поддерживаемых fetch-запросов. Успевший запрос может уже получить ответ до вызова abort(). Сервер мог принять данные до отмены клиента. Поэтому «клиент перестал ждать» и «сервер отменил действие» — разные результаты, которые проверяются разными наблюдениями.
Сигнал нельзя повторно использовать как возобновляемый ресурс: после отмены он остаётся aborted, а новый запрос с ним будет отклонён. Для нового запуска создайте новый контроллер. Таймаут можно выразить через AbortSignal.timeout(milliseconds), но проверьте поддержку в целевых браузерах и отдельно обработайте причину таймаута, если интерфейсу нужно отличать её от действия пользователя.
Проверка должна наблюдать не только состояние aggregate. Для каждого входа запишите имя операции, время старта, время завершения, причину отказа и факт получения сигнала. Если операция меняет внешний ресурс, добавьте проверку состояния ресурса после отказа. Лог «Promise.all отклонён» сам по себе доказывает только исход aggregate.
abort() из явной политики: кнопки, таймаута или обработчика ошибки.AbortError и таймаут, если среда предоставляет разные причины.Эта модель описывает стандартный combinator, а не политику приложения. Она не решает, сколько запросов можно выполнять одновременно, сколько раз повторять ошибку, что показывать при частичных данных и кто владеет отменой. Эти правила должны быть явными в коде и тестах.
\nPromise.all не означает параллельные потоки. JavaScript-движок и конкретный API сами определяют, как выполняется работа. Из успешного aggregate нельзя вывести пропускную способность, порядок сетевых пакетов или момент освобождения соединения. Для таких утверждений нужны метрики и документация транспорта.
Отмена не равна откату. Она может прекратить поддерживаемый fetch, чтение тела ответа или поток, но не возвращает уже отправленный платёж, не удаляет запись и не отменяет произвольную синхронную функцию. Серверную операцию защищают отдельные механизмы: идемпотентный ключ, транзакция, отменяемое задание или компенсационное действие.
Пример с ручным finishRight проверяет переходы promises в памяти. Пример с fetch проверяет передачу сигнала клиентскому API, но не гарантирует поведение конкретного прокси или сервера. Для старого браузера, Node.js или библиотеки с собственным клиентом сверяйте документацию именно этой версии.
Материал и реализация готовы, если читатель может ответить на четыре вопроса: какой promise сообщает общий результат; что делает другой вход после первой ошибки; какой объект или API реально принимает отмену; что происходит с уже выполненным побочным эффектом. В тестовом запуске должны быть видны отдельные события aggregate и каждой операции.
\nМинимальный набор проверок содержит положительный сценарий с массивом значений, отрицательный сценарий с поздним входом и сценарий с поддерживаемым AbortSignal. Если доказательством остановки служит только catch, проверка неполна. Честный вывод в таком случае: aggregate отклонён, а начатая работа может продолжиться.
Promise.allSettled() и прямое указание, что отклонение не отменяет оставшиеся операции.fetch, одноразовость сигнала, AbortSignal.timeout() и обработка причин отмены.fetch не отклоняется из-за HTTP-статуса сам по себе; статус нужно проверять через Response.ok или Response.status.Два reviewer-а читают один технический ответ. Один видит аккуратную гипотезу. Другой — недостаток глубины. Через пять минут спор уже идёт о впечатлении, а не о тексте. В журнале нет ссылки на строку, где началось расхождение.
\nЦена ошибки высока. Команда меняет rubric после самого громкого мнения. Кандидат или сотрудник получает вывод, который нельзя проверить. Следующий review повторяет тот же спор. Калибровка не исправляет это обсуждением в конце встречи. Она должна сначала зафиксировать одинаковый вход, независимые факты и границу того, чего проба не показывает.
\nТезис: технический review калибруется через цепочку «наблюдение → неизвестное → интерпретация → следующий запрос». Reviewer-ы не обязаны прийти к одинаковой интерпретации. Они обязаны показать, на каких фактах она стоит и где заканчивается evidence. Если в записи появляется score, ranking или вывод о личности, процесс должен остановиться.
\nВозьмём учебную карточку с ответом на вопрос об устаревшем API-ответе. В тексте есть max-age=0, маршрут /v1/report и описание симптома. Правило origin-сервера не указано. Это намеренный пробел. Он позволяет проверить, умеет ли reviewer отделить видимое от неизвестного.
Observation отвечает только на вопрос «что видно в sample?». Например: «В заголовке указан max-age=0». Unknown отвечает на вопрос «чего здесь нет?»: «Правило, по которому origin формирует cache-control, не показано». Interpretation связывает эти записи: «Нужен запрос правила origin; изменение cache policy пока не доказано». Такой порядок не запрещает гипотезы. Он не даёт гипотезе притвориться фактом.
| Слой | Допустимая запись | Запрещённый скачок |
|---|---|---|
| Observation | В sample есть max-age=0. | «Сервис неправильно настроен». |
| Unknown | Origin rule не показано. | «Автор не понимает кеширование». |
| Interpretation | Нужно запросить правило origin. | «Reviewer B глубже разобрался». |
| Hand-off | Запросить один отсутствующий факт. | Создать score, ranking или hiring outcome. |
Независимость нужна до обсуждения. Каждый reviewer получает тот же sampleId, ту же role rubric и те же критерии. Он сначала пишет факты и неизвестные, затем интерпретацию. Если начать с общей беседы, участники быстро выровняют формулировки, но потеряют момент, где они увидели разное.
Ниже — учебный TypeScript-подобный код. Он работает с объектом в памяти. Он не обращается к сети, не пишет файл и не создаёт оценку человека. В production этот пример ничего не доказывает.
\ntype ReviewRecord = {\n sampleId: string;\n criterionId: string;\n evidenceRef: string;\n kind: 'observation' | 'unknown' | 'interpretation' | 'hand-off';\n text: string;\n};\n\nfunction acceptHandOff(record: ReviewRecord) {\n const safePrefix = 'hand-off:';\n const forbidden = /score|ranking|hiring|person|candidate/i;\n\n if (record.kind !== 'hand-off' || !record.text.startsWith(safePrefix)) {\n return { accepted: false, action: 'stop-and-repair-boundary' };\n }\n if (forbidden.test(record.text)) {\n return { accepted: false, action: 'stop-and-repair-boundary' };\n }\n return { accepted: true, action: 'request-missing-evidence' };\n}\n\nconst next = acceptHandOff({\n sampleId: 'api-cache-fixed-v1',\n criterionId: 'evidence-boundary',\n evidenceRef: 'sample.headers.cache-control',\n kind: 'hand-off',\n text: 'hand-off: request the origin cache rule',\n});\nВызов возвращает учебный запрос недостающего evidence. Он не разрешает менять конфигурацию. Ветка с текстом hiring: reject должна вернуть stop-and-repair-boundary. Это отрицательный путь, а не дополнительная функция: он показывает, что процесс заметил незаконное расширение задачи.
Проверять нужно не только результат функции. Сверьте четыре поля: один sampleId, существующий evidenceRef, применимый criterionId и допустимый тип hand-off. Пустая ссылка, общий ярлык или другой sample делают запись непроверяемой. В таком случае правильное действие — остановка, а не попытка угадать недостающий факт.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Reviewer-ы спорят о «глубине». | Критерий не описывает наблюдаемое поведение. | Найдите строку sample, на которую ссылается каждый. | Сузьте criterion до проверяемого признака. |
| Оба reviewer-а используют одинаковые слова, но делают разные выводы. | Они смешали observation и interpretation. | Перепишите записи в два отдельных поля. | Запросите недостающий факт вместо решения. |
| В hand-off появляется score или ranking. | Учебная проба пересекла границу оценки человека. | Проверьте текст и допустимые типы записи. | Верните stop-and-repair-boundary; не сохраняйте outcome. |
| После калибровки меняют сразу sample, rubric и инструкцию. | Нельзя понять, что исправило расхождение. | Сопоставьте первую непарную запись с изменённым артефактом. | Измените один артефакт и повторите тот же sample. |
| Reviewer просит данные из сети или реального разговора. | Проба не содержит нужного evidence и маскирует это. | Проверьте boundary и список разрешённых источников. | Остановите прогон или замените sample на явно новый учебный кейс. |
Synthetic-проба проверяет процесс чтения и границу доказательства. Она не измеряет производительность инженера, качество найма, способность работать в команде или результат реального проекта. Нельзя переносить её вывод на человека. Нельзя называть отсутствие расхождения доказательством валидности rubric.
\nОдин fixed sample быстро устаревает. Изменился API, критерий или рабочая задача — изменился и смысл пробы. Набор из нескольких sample требует отдельного дизайна: иначе команда начнёт сравнивать разные задачи как одну шкалу. Если нужен реальный отбор, его должны спроектировать владельцы процесса с учётом применимых требований. Учебный журнал не заменяет такую процедуру.
\nМетод также не спасает от плохого критерия. Если criterion требует «понять намерение автора», его нельзя проверить ссылкой на текст. Если sample скрывает несколько причин, reviewer-ы будут расходиться по делу, а не из-за плохого review. Сначала уменьшите область утверждения. Потом добавляйте сложность.
\nСессия готова, если независимые записи можно открыть без устного пояснения и ответить на четыре вопроса: какой sample читали; какой факт увидели; какое неизвестное осталось; почему hand-off разрешён или остановлен. Для каждого расхождения указан один артефакт, который изменили. Повторный прогон использует тот же идентификатор пробы и не создаёт score, ranking, personal outcome, сетевой запрос или production effect.
\nМинимальная проверка — прогнать положительную и отрицательную ветки. Положительная ветка принимает только hand-off с ссылкой на существующий evidence и запросом одного недостающего факта. Отрицательная ветка отклоняет текст с оценкой человека, невалидную ссылку и неизвестный критерий. Если хотя бы одна ветка проходит без явного результата, материал не готов к использованию даже как учебный пример.
\nДва инженера разбирают один и тот же ответ о протухшем API-кеше. Один пишет: «в заголовке есть max-age=0». Второй сразу заключает: «автор не умеет работать с кешированием». Спор начинается не с текста и не с проверяемого критерия, а с впечатления. Через пять минут команда уже обсуждает, кто из рецензентов «глубже понял» ответ.
Такая ошибка ломает сам объект калибровки. Учебная проба должна показать, одинаково ли рецензенты читают заданный признак и одинаково ли понимают рубрику. Она не должна тайком превращаться в рейтинг человека. Поэтому сначала фиксируем один вход, затем отделяем наблюдение от неизвестного и только после этого разрешаем трактовку. Любой переход к личностному выводу или решению по человеку — стоп-сигнал.
\nГлавный вопрос статьи: как обнаружить первую точку расхождения и вернуть разговор к evidence — конкретному фрагменту, который можно открыть и проверить. Ниже — учебный контракт записи, запускаемый пример на Node.js, таблица диагностики и границы применимости.
\nНачните с маленькой фиксированной пробы. В ней есть ответ на вопрос об API, один фрагмент заголовка и явно пропущенное правило сервера. Например, в sample указано cache-control: max-age=0, но не показано, кто и по какой конфигурации сформировал этот заголовок. Отсутствующий факт — часть входа, а не приглашение его додумывать.
Запись первого рецензента может выглядеть так: «sample.headers.cache-control содержит max-age=0; правило origin не представлено; нужно запросить его». Это наблюдение, неизвестное и следующий запрос. Запись «сервис неправильно настроен» уже выходит за пределы пробы: заголовок виден, причина его появления — нет.
До общей встречи сохраните идентификатор пробы, версию рубрики и ссылки на evidence. Рецензенты читают один и тот же набор и пишут независимо. Если обсуждение начнётся раньше фиксации записей, участники синхронизируют формулировки и сотрут сам момент расхождения.
\nКалибровка становится проверяемой, когда у записи есть один тип и одна функция. Observation описывает то, что можно открыть в sample. Unknown называет отсутствующее условие. Interpretation связывает факт с гипотезой, но помечает её как гипотезу. Hand-off передаёт ровно один безопасный запрос на следующий факт.
\n| Тип | Что хранит | Пример | Что нельзя выводить |
|---|---|---|---|
| Observation | Точное наблюдение со ссылкой | sample.headers.cache-control равно max-age=0 | Сервис настроен неверно |
| Unknown | Недостающий факт или условие | Правило origin не показано | Автор не знает кеширование |
| Interpretation | Гипотеза, привязанная к фактам | Нужно проверить источник заголовка | Рецензент умнее другого |
| Hand-off | Один следующий запрос | request: origin cache rule | Score, ranking или решение по человеку |
Здесь «ссылка» — не обязательно URL. Это стабильный путь к полю фикстуры, номер строки ответа или идентификатор артефакта. Ссылка должна приводить к существующему объекту. Пустой ярлык вроде ответ кандидата не позволяет повторить проверку и потому не является evidence.
Не смешивайте калибровку с оценкой качества самого критерия. Фраза «проверяет системное мышление» недостаточна, пока не описано наблюдаемое действие: например, «называет отсутствующее условие и формулирует следующий безопасный запрос». Сначала проверяем, можно ли увидеть признак в тексте. Потом обсуждаем, нужен ли этот признак для конкретной роли.
\nБезопасный hand-off имеет четыре обязательных поля: sampleId, criterionId, evidenceRef и короткий текст запроса. Валидатор обязан сверить их со справочниками текущей пробы. Одного префикса hand-off: недостаточно: такой префикс может стоять у любой строки, включая запрещённое решение.
В этом примере разрешены только два критерия и две ссылки, заранее перечисленные в фикстуре. Проверка возвращает причину отказа, а не бросает её в общий лог. Это позволяет увидеть, почему запись остановилась: неизвестный критерий, отсутствующий evidence или попытка передать личный outcome.
\nСкопируйте код в файл calibration.mjs и запустите на Node.js 18 или новее. Пример не обращается к сети, не читает персональные данные и не оценивает реального человека. Он проверяет только форму записи в памяти; это полезная защита границы, но не доказательство валидности интервью.
const allowedCriteria = new Set(['evidence-boundary', 'next-check']);\nconst allowedEvidence = new Set([\n 'sample.headers.cache-control',\n 'sample.response.api-version',\n]);\nconst forbiddenWords = ['score', 'ranking', 'hiring', 'candidate', 'person'];\n\nfunction validateHandOff(record) {\n const errors = [];\n const text = typeof record?.text === 'string' ? record.text : '';\n\n if (record?.kind !== 'hand-off') errors.push('kind');\n if (!record?.sampleId) errors.push('sampleId');\n if (!allowedCriteria.has(record?.criterionId)) errors.push('criterionId');\n if (!allowedEvidence.has(record?.evidenceRef)) errors.push('evidenceRef');\n if (!text.startsWith('request: ')) errors.push('request-prefix');\n if (forbiddenWords.some((word) => text.toLowerCase().includes(word))) {\n errors.push('personal-or-decision-outcome');\n }\n\n return errors.length === 0\n ? { accepted: true, action: 'request-missing-evidence', errors: [] }\n : { accepted: false, action: 'stop-and-repair-boundary', errors };\n}\n\nconst cases = [\n {\n name: 'valid request',\n record: {\n sampleId: 'api-cache-fixed-v1',\n criterionId: 'evidence-boundary',\n evidenceRef: 'sample.headers.cache-control',\n kind: 'hand-off',\n text: 'request: origin cache rule',\n },\n accepted: true,\n },\n {\n name: 'unknown evidence',\n record: {\n sampleId: 'api-cache-fixed-v1',\n criterionId: 'evidence-boundary',\n evidenceRef: 'sample.guess.about-author',\n kind: 'hand-off',\n text: 'request: origin cache rule',\n },\n accepted: false,\n },\n {\n name: 'personal outcome',\n record: {\n sampleId: 'api-cache-fixed-v1',\n criterionId: 'next-check',\n evidenceRef: 'sample.response.api-version',\n kind: 'hand-off',\n text: 'request: candidate ranking',\n },\n accepted: false,\n },\n];\n\nconst results = cases.map(({ name, record, accepted }) => {\n const result = validateHandOff(record);\n return { name, expected: accepted, actual: result.accepted, result };\n});\n\nconsole.log(JSON.stringify(results, null, 2));\nif (results.some(({ expected, actual }) => expected !== actual)) {\n process.exitCode = 1;\n}\nПроверка запуска:
\nnode --check calibration.mjs\nnode calibration.mjs\nОжидаемый результат — первая запись принята с действием request-missing-evidence, две следующие отклонены с действием stop-and-repair-boundary. Ключевой отрицательный тест — candidate ranking: даже существующие идентификаторы не разрешают передать личный outcome. Если убрать проверку evidenceRef или список запрещённых слов, тесты перестанут охранять заявленную границу.
В реальном сервисе справочники должны быть частью версии конкретной пробы, а не глобальными константами. Полезно также валидировать типы, длину текста, владельца артефакта и права доступа. Эти поля намеренно не реализованы здесь: добавление их без контракта проекта создало бы видимость универсального решения.
\nСравнивайте записи слева направо, от менее интерпретируемого к более интерпретируемому. Сначала убедитесь, что совпали sampleId и версия рубрики. Затем откройте каждую ссылку evidence и выпишите наблюдение дословно. После этого сравните unknown. Только при совпадающих фактах переходите к interpretation и hand-off.
| Первая разница | Вероятная причина | Проверка | Исправление |
|---|---|---|---|
sampleId или версия rubric | Рецензенты получили разные входы | Сверить идентификаторы и хеши фикстуры | Повторить прогон на одном входе |
| Observation | В тексте неоднозначный фрагмент или его по-разному прочитали | Открыть одну и ту же ссылку и записать видимые символы | Уточнить sample или инструкцию чтения |
| Unknown | Критерий скрывает недостающее условие | Проверить, названо ли отсутствие явно | Добавить поле unknown и не заполнять его догадкой |
| Interpretation | Одинаковые факты связывают с разными гипотезами | Найти предложение, где появляется причинный вывод | Записать гипотезу и один тест, не менять сразу всю rubric |
| Hand-off | Граница действия не описана | Прогнать положительный и запрещённый текст через валидатор | Оставить только запрос evidence или остановку |
Один цикл должен менять один артефакт. Если одновременно переписать sample, rubric и инструкцию рецензента, следующий результат не объяснит, что устранило расхождение. Старую версию сохраняйте как обезличенную учебную фикстуру с понятным статусом, а не как запись о конкретном собеседнике.
\nsampleId, одну версию текста и одну версию рубрики. Зафиксируйте отсутствие нужных фактов.Фиксированная проба проверяет согласованность чтения и качество границы evidence. Она не измеряет производительность инженера, будущую работу в команде, результат реального проекта или вероятность успеха на должности. Отсутствие расхождения на одном sample не доказывает, что rubric валидна для всех задач.
\nДля реального отбора нужны отдельные решения владельцев процесса: анализ работы, набор релевантных критериев, одинаковые условия, правила хранения записей и проверка применимых требований. OPM описывает structured interview как метод с заранее заданными одинаковыми вопросами и общей шкалой; это полезная опорная идея, но не готовый регламент для частной команды и не совет по законодательству конкретной страны.
\nЮридическая применимость особенно зависит от юрисдикции и способа использования результата. Страница EEOC перечисляет федеральные правила США, включая 29 CFR 1607 — Uniform Guidelines on Employee Selection Procedures. Российская, европейская или другая процедура требует собственной правовой проверки. Учебный валидатор из этой статьи не заменяет юриста, HR-политику, оценку доступности или требования к защите персональных данных.
\nМетод также не исправляет плохой вопрос. Если ответ можно получить случайным угадыванием, если отсутствуют ключевые входы или если критерий требует «понять намерение автора», рецензенты будут расходиться по содержательной причине. Сначала уменьшите утверждение до наблюдаемого действия, затем добавляйте сложность и новые пробы.
\nКалибровка готова к повторному использованию, если независимая запись отвечает на четыре вопроса без устного пояснения: какой вход читали; какой факт открыт по ссылке; что осталось неизвестным; почему следующий hand-off принят или остановлен. Для каждого расхождения назван один изменённый артефакт. Положительный запуск возвращает только запрос недостающего evidence. Отрицательные запуски отклоняют несуществующую ссылку, неизвестный критерий и попытку создать личное решение.
\nХраните рядом версию sample, версию rubric и результаты отрицательных тестов. Тогда следующий рецензент сможет воспроизвести не только успешный путь, но и причину остановки. Это делает калибровку техническим процессом с проверяемыми границами, а не голосованием за самое убедительное впечатление.
\n