function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); } const p = (text) => '
' + text + '
'; const h2 = (text) => '' + escapeHtml(text) + '';
const ol = (items) => '| ' + item + ' | ').join('') + '
|---|
| ' + item + ' | ').join('') + '
await Promise.all(tasks), код копируют в обработчик, а после первой ошибки считают оставшуюся работу отменённой. Цена ошибки — повторный разбор уже начатых действий, лишняя логика очистки и обучение рецепту, который ломается при первом изменении условий.'),
p('Причина не в одной строке JavaScript. Рецепт скрывает модель: Promise.all собирает исходы входных promises в один aggregate promise, но не получает отдельного механизма остановки внешней работы. Проверка — попросить объяснить, что произойдёт с ещё не завершившимся входом после первой ошибки. Действие — строить объяснение в пять коротких шагов.'),
h2('Исходная задача и узкая граница'),
p('Исходная задача synthetic: получить два независимых значения и не продолжать зависимый расчёт, если одно значение не получено. Здесь promise — объект будущего результата; aggregate promise — один promise, который описывает результат набора. Мы не моделируем HTTP, файл или базу: важна только граница наблюдения за двумя значениями в памяти. Такой пример не учит отмене, потому что отмены в нём нет.'),
figure('/assets/editorial/2025/teaching-engineering-2025-model-exercise-check.svg', 'Схема объяснения: исходная задача ведёт к модели aggregate promise, затем к контрпримеру с оставшимся входом, короткому упражнению и точной проверке hand-off.', 'Путь специально отделяет результат aggregate promise от жизненного цикла каждого входа. Это предотвращает подмену модели готовым рецептом.'),
table('Пять частей объяснения', ['Часть', 'Вопрос читателю', 'Артефакт', 'Ошибка без части'], [['Задача', 'что должно дождаться двух значений?', 'два fixed inputs', 'код кажется универсальным'], ['Модель', 'кто хранит общий исход?', 'aggregate promise', 'путают ожидание и остановку'], ['Контрпример', 'что остаётся после reject?', 'ручное завершение входа', 'ошибка выглядит как отмена'], ['Упражнение', 'какую границу надо назвать?', 'одно предложение', 'проверяют термин'], ['Проверка', 'что передаём дальше?', 'specific gap или hand-off', 'обратная связь расплывчата']]),
h2('Минимальная модель в исполняемом примере'),
code("import { runFixedPromiseExercise } from './upgrade-2025-10.mjs';\n\nconst result = await runFixedPromiseExercise();\nconsole.log(result.trace);\n// ['aggregate-rejected', 'remaining-input-fulfilled']\n// Fixed in-memory promises only; no clock, HTTP, files or side effects."),
p('Первый вход уже отклонён с fixed ошибкой. Второй намеренно удерживается функцией completeRemaining. Мы сначала ждём обработанный aggregate reject, потом вручную завершаем второй вход. Порядок trace — проверяемая модель: aggregate уже сообщил об ошибке, но второй input всё ещё может завершиться. Это не измеряет скорость и не описывает реальную интеграцию; это демонстрация того, какую границу обязан назвать автор объяснения.'),
h2('Контрпример важнее второго рецепта'),
p('Неправильная фраза звучит удобно: «Promise.all остановит всё при ошибке». Контрпример не спорит с удобством, он проверяет условие. У aggregate promise есть ранний rejected исход; у созданного ранее input нет в этом вызове отдельной команды cancel. Поэтому после reject нельзя делать вывод, что внешний запрос, таймер или вычисление исчезли. Для их остановки проектируют отдельный контракт и проверяют его отдельно.'),
h2('Короткое упражнение вместо пересказа'),
ol(['Дайте только задачу: «два значения нужны до зависимого расчёта».', 'Покажите trace и попросите назвать два разных объекта: aggregate и remaining input.', 'Попросите закончить фразу: «Для остановки внешней работы нужен отдельный …».', 'Проверьте не словом «верно», а fixed answer: Promise.all-observes-settlement-not-cancellation.', 'Передайте дальше только synthetic карточку с model, counterexample и точным пробелом, если ответ неполный.']),
h2('Проверяемая обратная связь'),
code("import { createFixedTeachingAttempt, checkFixedTeachingAttempt } from './upgrade-2025-10.mjs';\n\nconst report = checkFixedTeachingAttempt(\n createFixedTeachingAttempt('wrong-cancellation-v1'),\n);\nconsole.log({\n accepted: report.accepted,\n reasons: report.reasons,\n handoff: report.handoff,\n});\n// { accepted: false, reasons: ['wrong-boundary'], handoff: 'return-with-specific-gap' }"),
p('Такой feedback не говорит, что попытка «плохая». Он называет ровно одну отсутствующую границу: aggregate не равен отмене. Positive result здесь скромный: корректный fixed hand-off для human review, а не доказательство, что кто-то научился лучше или что команда стала эффективнее.'),
h2('Проверка объяснения до публикации'),
p('Перед публикацией полезно прогнать текст как маленький тест, а не как лекцию. Уберите из него код и оставьте четыре вопроса: какая исходная задача, какой объект агрегирует исходы, какой контрпример запрещает слишком широкий вывод и кто владеет отменой. Если на один вопрос нельзя ответить по тексту, фрагмент кода ещё не объяснение. Он может быть верным для исходного случая, но не передаёт условие безопасного переноса.'),
p('Затем верните код и проверьте соответствие строк модели. Массив в Promise.all — набор inputs; переменная результата — aggregate; ветка catch — реакция зависимой логики на его rejected outcome. В этой минимальной форме нет claim о поведении сети. Такое ограничение кажется менее эффектным, чем «запускаем параллельно и всё отменится», но оно дешевле в сопровождении: читатель не получает несуществующую гарантию и знает, какой контракт искать дальше.'),
table('Чек-лист переноса рецепта', ['Вопрос', 'Ответ в модели', 'Что сделать при отсутствии'], [['Какие значения нужны вместе?', 'fixed left и right inputs', 'сузить задачу'], ['Что меняет Promise.all?', 'aggregate outcome', 'не писать про ресурс'], ['Что доказывает reject?', 'не начинать зависимый расчёт', 'добавить контрпример'], ['Кто отменяет работу?', 'не определено этим вызовом', 'спроектировать отдельный contract']]),
h2('Ограничение и следующий шаг'),
p('MDN в закреплённой версии описывает успешный исход всех inputs, первый reject и порядок результатов; ECMA-262 задаёт алгоритм combinator. Ни один источник не даёт готового протокола отмены. Поэтому следующий шаг — взять один собственный API и отдельно выписать: кто создаёт работу, кто имеет право остановить её, как выглядит подтверждение остановки. Не переносите synthetic trace как production design.')
]);
const mechanism = revision({ slug: 'editorial-2025-10-mechanism-teaching-engineering', title: 'Модель Promise.all: где заканчивается aggregate и начинается работа входов', categories: ['JavaScript', 'Архитектура'], cover: '/assets/editorial/2025/teaching-engineering-2025-misconception-matrix.svg', excerpt: 'Причинная модель Promise.all для объяснения: порядок результатов, первая ошибка, оставшийся вход и отдельная граница отмены.', readingMinutes: 12 }, [
p('Симптом механического объяснения — читатель знает, что написать, но не может предсказать состояние набора после reject. Цена ошибки — код, который очищает несуществующее состояние или считает внешний вызов остановленным без подтверждения, а затем требует дорогостоящего расследования границ владения.'),
p('Причина — смешение трёх уровней: входной promise, aggregate promise и внешняя операция, которая могла создать вход. Проверка — нарисовать для каждого уровня собственный исход и спросить, какое действие соединяет их. Действие — объяснять Promise.all как наблюдатель набора результатов, а отмену оставлять отдельным протоколом с владельцем.'),
h2('Три объекта, которые нельзя назвать одним словом'),
p('Входной promise представляет один будущий исход. Aggregate promise — новый объект, который создаёт Promise.all(iterable). Внешняя операция — условная работа за пределами этой учебной модели; она может вообще отсутствовать, как в нашем ручном fixed input. Разделение полезно не терминологией, а вопросом владения: чей исход меняет combinator и у кого есть право потребовать остановку работы.'),
figure('/assets/editorial/2025/teaching-engineering-2025-misconception-matrix.svg', 'Матрица заблуждений: input promise, aggregate promise и внешняя операция расположены отдельно; зелёные и красные клетки показывают, какой факт можно вывести из reject aggregate.', 'У aggregate есть определённый rejected исход. Из него нельзя автоматически вывести cancel другого input или завершение внешней операции.'),
table('Наблюдение и допустимый вывод', ['Наблюдение', 'Допустимый вывод', 'Нельзя утверждать', 'Следующая проверка'], [['вход A rejected', 'aggregate может перейти в rejected', 'вход B отменён', 'наблюдать B отдельно'], ['вход B fulfilled позже', 'B имеет собственный исход', 'aggregate станет fulfilled', 'прочитать aggregate outcome'], ['aggregate rejected', 'зависимый расчёт можно не начинать', 'внешняя работа остановлена', 'найти cancel contract'], ['allSettled fulfilled', 'доступны все исходы', 'ошибки исчезли', 'разобрать каждый status']]),
h2('Что фиксирует спецификация'),
p('В ECMA-262 16th edition Promise.all получает iterable, создаёт promise capability и передаёт iterator в PerformPromiseAll. Это важная, но узкая опора: combinator работает с promises и capability результата. Она не говорит «закрой сокет», «прерви вычисление» или «освободи файл». Поэтому из языковой семантики нельзя тихо вывести контракт ресурса приложения.'),
p('MDN закреплённым commit формулирует тот же видимый результат: aggregate fulfils после всех входов и rejects при первой ошибке; порядок значений следует порядку iterable, а не скорости завершения. Слова «первая ошибка» относятся к observable outcome aggregate. Они не превращают уже созданные входы в отменённые. Это различие и есть минимальная модель, которую надо передать до примера с await.'),
h2('Контрпример с ручным управлением'),
code("import { runFixedPromiseExercise } from './upgrade-2025-10.mjs';\n\nconst { trace, interpretation } = await runFixedPromiseExercise();\nconsole.log({ trace, interpretation });\n// aggregate-rejected появляется раньше remaining-input-fulfilled"),
p('В этом примере нет сети и нет таймера, поэтому нельзя спрятать объяснение за планировщиком. Второй input завершается только после вызова сохранённой функции. Если aggregate reject мог бы отменить input сам, второй элемент trace не появился бы после первого. Но он появляется, потому что ручной владелец всё ещё способен завершить свой promise. Это ровно контрпример к ложному переносу «short-circuit результата» на «остановку всех работ». '),
h2('Почему allSettled не является отменой'),
p('Иногда ложную модель меняют на другой рецепт: «тогда всегда используем allSettled». Он решает другой вопрос: дождаться всех исходов, чтобы получить statuses. Он не добавляет прав остановки и не делает зависимый расчёт безопасным автоматически. Выбор между all и allSettled начинается с нужного результата aggregate: ранний stop дальнейшей логики или полный список исходов. Отмена живёт в третьем, отдельном вопросе.'),
h2('Упражнение на причинность'),
ol(['Прочитайте trace без кода и отметьте, чей исход зафиксирован на каждой строке.', 'Сформулируйте правило: aggregate reject запрещает какой именно зависимый расчёт?', 'Назовите факт, которого в trace нет: «внешняя работа отменена».', 'Добавьте в synthetic карточку отдельное поле cancellationOwner; в текущей модели оно намеренно отсутствует.', 'Сверьте вывод с negative attempt: ответ «Promise.all-cancels-everything» обязан вернуть wrong-boundary.']),
h2('Граница, которую стоит оставить в коде'),
p('Узкая инженерная польза модели — не в том, чтобы запрещать Promise.all. Она помогает отделить ветвление зависимой логики от управления ресурсом. Когда оба вопроса названы, API можно обсуждать предметно: нужен ли отдельный signal, кто его создаёт, что значит acknowledgement. В этом draft-пакете такого API нет; positive result — только synthetic hand-off с указанной неизвестной границей.'),
h2('Как не превратить модель в новую догму'),
p('Модель не требует ждать все результаты всегда. Если зависимый расчёт бессмыслен без любого одного значения, ранний rejected aggregate даёт ясную точку остановки именно для этого расчёта. Если для отчёта нужны все statuses, подходит другой aggregate outcome. Если нужно прекратить внешнее действие, появляется четвёртый объект — cancellation contract. Эти три решения могут находиться в одном модуле, но отвечают на разные вопросы и должны иметь разные проверки.'),
p('Полезный self-check: замените слово «параллельно» на конкретный факт. Для Promise.all это не обещание времени выполнения, а способ собрать несколько promises. Замените слово «отменится» на имя команды и acknowledgement, если они существуют. Если имени нет, честный текст говорит «не определено». Такая формулировка даёт читателю точку для проектирования, а не создаёт ложное чувство завершённости после одной конструкции языка.'),
table('Три решения, три критерия', ['Нужный эффект', 'Инструмент модели', 'Критерий проверки'], [['не начинать зависимый расчёт', 'aggregate reject', 'ветка catch не вычисляет следующий шаг'], ['получить каждый исход', 'allSettled', 'в списке есть status каждого input'], ['остановить внешнюю работу', 'отдельный contract', 'владелец и acknowledgement названы'], ['собрать значения по порядку', 'Promise.all fulfill', 'массив следует iterable, не скорости']]),
p(FIELD_SYNTHETIC_BOUNDARY)
]);
const field = revision({ slug: 'editorial-2025-10-field-teaching-engineering', title: 'Разбор объяснения Promise.all: как вернуть рецепт к условиям задачи', categories: ['JavaScript', 'Практика'], cover: '/assets/editorial/2025/teaching-engineering-2025-feedback-loop.svg', excerpt: 'Полевой разбор synthetic объяснения: распознать рецепт без модели, дать точную обратную связь и передать следующий шаг без заявления об эффективности.', readingMinutes: 12 }, [
p('В разборе объяснения чаще всего виден готовый рецепт: «оберни массив в Promise.all», но отсутствуют условия, при которых результат нужен и безопасен. Цена ошибки — hand-off с неявной ложью об отмене: следующий инженер копирует фрагмент в другую границу и тратит время на последствия, которые текст обещал убрать.'),
p('Причина в том, что обратная связь проверяет наличие API-имени, а не причинную модель. Проверка должна быть короткой: есть ли исходная задача, модель aggregate, контрпример и ответ про отмену. Действие — вернуть не оценку личности, а fixed synthetic карточку с одним конкретным пробелом и упражнением на его закрытие.'),
h2('Полевая ситуация без реальных людей'),
p('Возьмём fixed attempt recipe-only-v1. В нём задача названа, но поля model и counterexample пусты; ответ не фиксирует границу отмены. Это не запись о кандидате, pull request или учебной группе. Это маленький объект, которым можно воспроизводимо проверить саму форму объяснения. Его ценность в том, что другой редактор получит те же причины, а не будет угадывать настроение автора.'),
figure('/assets/editorial/2025/teaching-engineering-2025-feedback-loop.svg', 'Замкнутый цикл обратной связи: fixed attempt проходит проверку модели, контрпримера и границы; конкретный пробел возвращается как короткое упражнение, а положительный исход становится только synthetic hand-off.', 'Цикл не измеряет обучение. Он делает следующий технический шаг и причину возврата наблюдаемыми.'),
table('Сигнал в объяснении и точный feedback', ['Сигнал', 'Диагноз', 'Короткое упражнение', 'Результат'], [['есть только await Promise.all', 'missing-model', 'назвать aggregate и inputs', 'return-with-specific-gap'], ['нет оставшегося input', 'missing-counterexample', 'прочитать trace из двух строк', 'return-with-specific-gap'], ['«всё отменяется»', 'wrong-boundary', 'найти отдельный cancel contract', 'return-with-specific-gap'], ['все три части есть', 'модель проверяема', 'передать ограничение', 'synthetic-ready-for-human-review']]),
h2('Исполняемая карточка проверки'),
code("import { createFixedTeachingAttempt, checkFixedTeachingAttempt } from './upgrade-2025-10.mjs';\n\nconst attempt = createFixedTeachingAttempt('recipe-only-v1');\nconst report = checkFixedTeachingAttempt(attempt);\nconsole.log(report.reasons);\n// ['missing-model', 'missing-counterexample', 'wrong-boundary']"),
p('Проверка намеренно не принимает произвольный объект как доказательство. Она принимает только frozen fixed literals этого модуля. Так не возникает видимости, будто мы валидируем реальные объяснения или строим универсальную систему оценки. В ответе есть productionEffect: not-attempted; функция не создаёт задачу, не пишет комментарий и не меняет состояние вне памяти.'),
h2('Как дать feedback, который можно выполнить'),
p('Неполезный feedback: «раскрой тему глубже». Он не называет наблюдение и даёт автору слишком много вариантов. Полезный feedback: «в строке с Promise.all добавь модель aggregate и один контрпример: после aggregate reject remaining input может завершиться; затем назови, какой отдельный contract отвечает за отмену». В нём есть объект, условие, ожидаемое изменение и граница, которую нельзя додумать автоматически.'),
h2('Короткая последовательность разбора'),
ol(['Зафиксируйте исходную задачу одной фразой, не начиная с имени API.', 'Найдите модель: входы, aggregate и зависимый расчёт.', 'Попросите один контрпример, который ломает слишком широкий рецепт.', 'Запустите fixed exercise и сравните trace с выводом автора.', 'Верните только конкретные gaps или synthetic hand-off для human review; не объявляйте результат доказательством обучения.']),
h2('Когда рецепт допустим'),
p('Рецепт полезен, когда модель уже названа и условия узкие. Например, после объяснения можно показать await Promise.all([left, right]) как способ дождаться обоих значений перед зависимым расчётом. Но рядом остаётся ограничение: этот вызов не описывает отмену внешних действий и не выбирает обработку каждого результата. Если нужны все outcomes, вопрос меняется, и читатель должен сравнить его с Promise.allSettled, а не механически заменить имя.'),
h2('Проверяемый следующий шаг'),
p('Проверьте у собственного объяснения только четыре предложения: задача, модель, контрпример, отдельный владелец отмены. Если одно отсутствует, не добавляйте ещё один фрагмент кода — заполните конкретный пробел. Положительный исход этого упражнения ограничен: model-first-v1 получает synthetic-ready-for-human-review. Он не доказывает улучшение человека, команды или production-кода.'),
h2('Три вопроса для независимого читателя'),
p('Независимый читатель не должен угадывать намерение автора. Поэтому после feedback дайте ему три вопроса без подсказки: «что вернёт aggregate при первой ошибке?», «что показывает второй элемент trace?» и «какой объект имеет право остановить внешнюю работу?». Ответы проверяют не запоминание имени метода, а наличие границы. Если третий ответ заменяется словом Promise.all, возвращаем только gap wrong-boundary.'),
p('Такой способ удобно применять к собственной документации. Он не требует реального класса, оценки кандидата или pull request. Достаточно fixed примера и редакторского чтения. Важна дисциплина формулировки: feedback сообщает, что нужно добавить в текст, а не что автор «не понимает async». Это сокращает спор о тоне и оставляет один проверяемый технический предмет для следующей версии объяснения.'),
table('Граница обратной связи', ['Можно проверить', 'Нельзя заключить', 'Безопасный следующий шаг'], [['trace имеет два разных события', 'реальная операция завершилась', 'описать synthetic модель'], ['ответ называет aggregate', 'автор применит знание везде', 'добавить контрпример'], ['gap конкретен', 'человек стал сильнее', 'передать на human review'], ['нет side effect', 'production безопасен', 'отдельно проектировать API']]),
p(SYNTHETIC_BOUNDARY)
]);
export const revisions = deepFreeze([practice, mechanism, field]);
if (process.argv.includes('--verify-fixture')) { const result = await runTeachingFixture(); const failed = Object.entries(result.assertions).filter(([, value]) => value !== true).map(([key]) => key); if (failed.length) { process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n'); process.exitCode = 1; } else process.stdout.write('PASS fixture: ' + Object.keys(result.assertions).length + '/' + Object.keys(result.assertions).length + ' assertions\n'); }
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');