revise October 2025 teaching engineering articles
Build and deploy / deploy (push) Successful in 15s

This commit is contained in:
2026-07-31 18:30:31 +03:00
parent 195477c0e4
commit 775ae9b71a
7 changed files with 203 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 277 из 358 созданных материалов. Остальные 81 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 280 из 358 созданных материалов. Остальные 78 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия
+65
View File
@@ -0,0 +1,65 @@
# P92 — октябрь 2025: объяснение сложной инженерной темы
## Область draft-пакета
Пакет заменяет только три overlay-статьи: `editorial-2025-10-practice-teaching-engineering`, `editorial-2025-10-mechanism-teaching-engineering`, `editorial-2025-10-field-teaching-engineering`. Единственный исполняемый артефакт — `web/scripts/upgrade-2025-10.mjs`. Все attempts, feedback, trace и results — fixed synthetic in-memory literals; модуль не читает files, network, Git, CI, clock, telemetry, production или user data и не выполняет side effect. Registry, README, очередь, `articles.json`, app-страницы, `docs/`, Git и чужие draft-пакеты не менялись.
## Исследование и историческая граница
Граница — 31 октября 2025. Оба источника доступны обычным HTTPS без авторизации и TLS bypass. [ECMA-262 16th edition](https://ecma-international.org/wp-content/uploads/ECMA-262_16th_edition_june_2025.pdf) — неизменяемый опубликованный PDF июня 2025, §27.2.4.1/PerformPromiseAll. [MDN Promise.all](https://raw.githubusercontent.com/mdn/content/3fad0447b4901e28fe88769976787d8d8b87d66d/files/en-us/web/javascript/reference/global_objects/promise/all/index.md) закреплён commit `3fad0447b4901e28fe88769976787d8d8b87d66d` от 19.08.2025. Они подтверждают семантику aggregate outcome, порядок результатов, first rejection и allSettled; не подтверждают cancellation конкретной внешней работы. Вывод «cancel требует отдельного contract» в статье — явно обозначенная инженерная граница, не обещание источника.
## Проход 1 — факты, техника и источники
- `runFixedPromiseExercise` создаёт только две in-memory promises: первая сразу rejected, вторая завершается вручную после обработанного aggregate reject. Trace проверяет порядок `aggregate-rejected`, затем `remaining-input-fulfilled`; он не моделирует сеть, timer или ресурс.
- `checkFixedTeachingAttempt` принимает только frozen fixed literals. `recipe-only-v1` останавливается на `missing-model` и `missing-counterexample`, `wrong-cancellation-v1` — на `wrong-boundary`; positive path выдаёт только `synthetic-ready-for-human-review`.
- В текст не перенесено утверждение, что Promise.all отменяет работу. ECMA-262 и MDN использованы только в заявленных границах; allSettled описан как получение всех исходов, а не cancel mechanism.
**Вердикт:** пройдено.
## Проход 2 — редактура, голос, объём, цена и cross-article
Все статьи начинаются с наблюдаемой проблемы и цены ошибки в первых двух абзацах. Тон М8 — короткий, наставнический, без claims об эффективности: симптом → причина → проверка → действие. Термины раскрыты при первом появлении. Practice отвечает за маршрут объяснения, mechanism — за разделение трёх уровней состояния, field — за форму конкретной обратной связи.
| Статья | Главный вопрос | Самостоятельный артефакт | Цена ошибки |
| --- | --- | --- | --- |
| practice | Как выстроить объяснение? | путь model → exercise → check | рецепт копируют вне условий |
| mechanism | Что именно делает aggregate? | матрица допустимых выводов | ложная очистка и неверное владение |
| field | Как вернуть конкретный пробел? | fixed feedback card | неверный hand-off следующему инженеру |
Cross-article repetition check после финальной редакции: **0** одинаковых абзацев длиной от 160 знаков и **0** общих последовательностей из 12 слов; общий большой вводный блок отсутствует.
**Вердикт:** пройдено. Независимый audit зафиксировал основной текст: practice — **5 725**, mechanism — **6 071**, field — **5 457** знаков.
## Проход 3 — SVG, fixture и выпуск
- `node --check web/scripts/upgrade-2025-10.mjs` — PASS.
- `node web/scripts/upgrade-2025-10.mjs --verify-fixture` — PASS, 10/10 assertions.
- `npm run audit:draft -- scripts/upgrade-2025-10.mjs` — PASS: **5 725 / 6 071 / 5 457**; диапазон 5–15 тыс., table, figure, alt/caption, code, ordered sequence, два источника, проблема и цена.
- `xmllint --noout` для трёх SVG — PASS; safety scan чист: нет `script`, `foreignObject`, `javascript:`, `data:image` и inline event handlers.
- Sharp renders на 375 px просмотрены: model path сохраняет пять карточек, matrix читает три уровня, feedback loop показывает возврат gap и hand-off без обрезания.
Overlay намеренно не подключён. README, registry, очередь и Git не менялись; интеграционный audit/build остаётся задачей независимого редактора.
## Независимый выпускной проход — 31.07.2026
Источники перепроверены на закреплённых версиях. ECMA-262 16th edition PDF показывает `Promise.all`, `NewPromiseCapability`, iterator и `PerformPromiseAll`; MDN commit `3fad0447` подтверждает first rejection, iterable order и границу `allSettled`. Ни один текст не выводит отмену внешней операции из этих фактов.
Все три статьи прочитаны целиком. Практика объясняет модель и упражнение, mechanism отделяет input, aggregate и внешнюю операцию, field возвращает recipe к проверяемому feedback. Попарная проверка основного текста: 0 одинаковых абзацев от 160 знаков и 0 общих 12-словных фрагментов. Три SVG повторно отрендерены Sharp на 375 px и остались читаемыми.
При буквальном исполнении practice example обнаружен неполный комментарий к `console.log(report)`: объект также содержал `productionEffect`. Snippet изменён на вывод трёх конкретно показываемых полей; результат теперь точно соответствует комментарию. Повторный fixture проходит.
~~~text
node --check scripts/upgrade-2025-10.mjs
PASS
node scripts/upgrade-2025-10.mjs --verify-fixture
PASS fixture: 10/10 assertions
npm run audit:draft -- scripts/upgrade-2025-10.mjs
PASS: 5725 / 6071 / 5457 body chars
npm run audit:articles -- <3 October slugs>
PASS: each article has 1 table, 1 code example and 1 figure
~~~
После приёмки overlay подключён в `editorial-revisions.mjs` после September 2025. Все examples остаются fixed in-memory teaching models; они не создают production result, отмену внешней работы или claim об эффективности обучения.
+2
View File
@@ -88,6 +88,7 @@ import { revisions as june2025Revisions } from '../scripts/upgrade-2025-06.mjs';
import { revisions as july2025Revisions } from '../scripts/upgrade-2025-07.mjs';
import { revisions as august2025Revisions } from '../scripts/upgrade-2025-08.mjs';
import { revisions as september2025Revisions } from '../scripts/upgrade-2025-09.mjs';
import { revisions as october2025Revisions } from '../scripts/upgrade-2025-10.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -181,4 +182,5 @@ export const editorialRevisions = [
...july2025Revisions,
...august2025Revisions,
...september2025Revisions,
...october2025Revisions,
];
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="500" viewBox="0 0 960 500" role="img" aria-labelledby="title desc"><title id="title">Цикл проверяемой обратной связи</title><desc id="desc">Fixed synthetic attempt проверяется на модель, контрпример и границу отмены; конкретный пробел возвращается как упражнение, полный ответ передаётся на human review.</desc><rect width="960" height="500" fill="#f7fee7"/><g font-family="Arial, sans-serif" fill="#1e293b"><text x="52" y="62" font-size="30" font-weight="700">Feedback без оценки личности</text><text x="52" y="94" font-size="17">fixed attempt → конкретный пробел → короткое упражнение → проверка</text></g><g font-family="Arial, sans-serif" text-anchor="middle"><g fill="#ecfccb" stroke="#4d7c0f" stroke-width="3"><rect x="70" y="190" width="165" height="105" rx="18"/><rect x="305" y="190" width="165" height="105" rx="18"/><rect x="540" y="190" width="165" height="105" rx="18"/><rect x="775" y="190" width="135" height="105" rx="18"/></g><g font-size="18" font-weight="700"><text x="152" y="230">Attempt</text><text x="387" y="230">Check</text><text x="622" y="230">Exercise</text><text x="842" y="230">Hand-off</text></g><g font-size="14"><text x="152" y="259">fixed literal</text><text x="387" y="259">specific gap</text><text x="622" y="259">one boundary</text><text x="842" y="259">human review</text></g></g><g stroke="#4d7c0f" stroke-width="4" fill="none"><path d="M235 242h62m173 0h62m173 0h62"/><path d="M620 296c-35 105-420 100-450 0"/></g><g fill="#4d7c0f"><path d="M292 234l15 8-15 8zm235 0l15 8-15 8zm235 0l15 8-15 8z"/><path d="M160 300l12-12 5 17z"/></g><text x="54" y="430" font-family="Arial, sans-serif" font-size="16" fill="#475569">Положительный исход ограничен synthetic hand-off: цикл не измеряет знания, людей или эффективность команды.</text></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="510" viewBox="0 0 960 510" role="img" aria-labelledby="title desc"><title id="title">Матрица исходов Promise.all</title><desc id="desc">Таблица отличает input promise, aggregate promise и внешнюю операцию; reject aggregate не доказывает отмену двух последних.</desc><rect width="960" height="510" fill="#fffaf5"/><g font-family="Arial, sans-serif" fill="#1f2937"><text x="48" y="60" font-size="30" font-weight="700">Что следует из reject aggregate</text><text x="48" y="92" font-size="17">Три уровня состояния нельзя склеивать в одну фразу</text></g><g font-family="Arial, sans-serif" font-size="18"><rect x="48" y="135" width="864" height="65" fill="#e2e8f0"/><rect x="48" y="200" width="864" height="78" fill="#dcfce7"/><rect x="48" y="278" width="864" height="78" fill="#fee2e2"/><rect x="48" y="356" width="864" height="78" fill="#f8fafc"/><g stroke="#64748b" stroke-width="2"><path d="M48 135h864M48 200h864M48 278h864M48 356h864M48 434h864M265 135v299M575 135v299"/></g><g font-weight="700"><text x="70" y="176">Объект</text><text x="287" y="176">Можно вывести</text><text x="597" y="176">Нельзя вывести</text></g><text x="70" y="245">aggregate promise</text><text x="287" y="245">переходит в rejected</text><text x="597" y="245">входы остановлены</text><text x="70" y="323">remaining input</text><text x="287" y="323">имеет свой исход</text><text x="597" y="323">aggregate станет fulfilled</text><text x="70" y="401">внешняя операция</text><text x="287" y="401">нужен отдельный contract</text><text x="597" y="401">она автоматически cancel</text></g><text x="48" y="475" font-family="Arial, sans-serif" font-size="16" fill="#475569">Зелёная строка — наблюдаемый факт; красная колонка — запрещённый вывод без дополнительного контракта.</text></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="480" viewBox="0 0 960 480" role="img" aria-labelledby="title desc"><title id="title">Путь объяснения сложной инженерной темы</title><desc id="desc">Пять карточек: задача, модель, контрпример, упражнение и проверка hand-off.</desc><rect width="960" height="480" fill="#f6f8fb"/><g font-family="Arial, sans-serif" fill="#152238"><text x="54" y="65" font-size="30" font-weight="700">Модель → упражнение → проверка</text><text x="54" y="99" font-size="18">Promise.all: результат набора не равен отмене входов</text></g><g font-family="Arial, sans-serif" text-anchor="middle"><g fill="#dbeafe" stroke="#2563eb" stroke-width="3"><rect x="45" y="180" width="145" height="130" rx="16"/><rect x="225" y="180" width="145" height="130" rx="16"/><rect x="405" y="180" width="145" height="130" rx="16"/><rect x="585" y="180" width="145" height="130" rx="16"/><rect x="765" y="180" width="145" height="130" rx="16"/></g><g font-size="18" fill="#152238" font-weight="700"><text x="117" y="225">Задача</text><text x="297" y="225">Модель</text><text x="477" y="225">Контрпример</text><text x="657" y="225">Упражнение</text><text x="837" y="225">Проверка</text></g><g font-size="14" fill="#334155"><text x="117" y="255">два значения</text><text x="297" y="255">aggregate</text><text x="477" y="255">remaining input</text><text x="657" y="255">назвать границу</text><text x="837" y="255">specific gap</text></g></g><g stroke="#2563eb" stroke-width="4" fill="none"><path d="M190 245h30m150 0h30m150 0h30m150 0h30"/></g><g fill="#2563eb"><path d="M216 237l14 8-14 8zm180 0l14 8-14 8zm180 0l14 8-14 8zm180 0l14 8-14 8z"/></g><text x="54" y="405" font-family="Arial, sans-serif" font-size="16" fill="#475569">Рецепт появляется только после того, как видна причина и проверяемая граница.</text></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+132
View File
@@ -0,0 +1,132 @@
function escapeHtml(value) { return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;'); }
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function plainText(content) { return content.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
function bodyText(content) { return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
function deepFreeze(value) { if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(deepFreeze); Object.freeze(value); } return value; }
const sources = deepFreeze([
{ title: 'ECMA-262, 16th edition (June 2025), §27.2.4.1 Promise.all', url: 'https://ecma-international.org/wp-content/uploads/ECMA-262_16th_edition_june_2025.pdf', version: '16th edition, June 2025, immutable published PDF', claim: 'Алгоритм Promise.all создаёт одну capability, обходит iterable и передаёт его элементы в PerformPromiseAll; результат завершается ошибкой через reject capability.', boundary: 'Спецификация описывает семантику языка. Она не проектирует отмену работы, timeout, retry или API конкретного приложения.' },
{ title: 'MDN Promise.all(), immutable content commit 3fad0447 (19 August 2025)', url: 'https://raw.githubusercontent.com/mdn/content/3fad0447b4901e28fe88769976787d8d8b87d66d/files/en-us/web/javascript/reference/global_objects/promise/all/index.md', version: 'mdn/content commit 3fad0447b4901e28fe88769976787d8d8b87d66d, 2025-08-19', claim: 'Promise.all выполняется успешно после выполнения всех входов либо отклоняется на первой ошибке; результат успеха сохраняет порядок входного iterable, а allSettled ждёт все исходы.', boundary: 'Документация не утверждает, что Promise.all отменяет уже начатую внешнюю работу или заменяет протокол остановки.' },
]);
function sourceList() { return '<ul>' + sources.map((source) => '<li><a href="' + source.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(source.title) + '</a> — версия: ' + escapeHtml(source.version) + '. ' + escapeHtml(source.claim) + ' Граница: ' + escapeHtml(source.boundary) + '</li>').join('') + '</ul>'; }
const SYNTHETIC_BOUNDARY = 'Все карточки, попытки, ответы, результаты и hand-off ниже — fixed synthetic JS literals в памяти. Модуль не читает files, сеть, Git, CI, clock, telemetry, production data или user data и не выполняет side effect.';
const FIELD_SYNTHETIC_BOUNDARY = 'В этом разборе используются только заранее заданные объекты JavaScript в памяти. Ни один шаг не обращается к HTTP, диску, часам, телеметрии, CI, Git, production или данным людей; результат остаётся учебным synthetic hand-off.';
const FIXED_ATTEMPTS = deepFreeze({
'model-first-v1': { task: 'combine-two-results', model: 'aggregate-order-and-first-rejection', counterexample: 'remaining-input-can-finish-after-aggregate-rejection', exercise: 'state-what-needs-explicit-cancellation', answer: 'Promise.all-observes-settlement-not-cancellation' },
'recipe-only-v1': { task: 'combine-two-results', model: '', counterexample: '', exercise: 'copy-await-Promise-all', answer: 'unknown' },
'wrong-cancellation-v1': { task: 'combine-two-results', model: 'aggregate-order-and-first-rejection', counterexample: 'remaining-input-can-finish-after-aggregate-rejection', exercise: 'state-what-needs-explicit-cancellation', answer: 'Promise.all-cancels-everything' },
});
function assessFixedAttempt(input) {
if (!Object.values(FIXED_ATTEMPTS).includes(input)) return deepFreeze({ accepted: false, reasons: ['unknown-fixed-attempt'], handoff: 'not-created' });
const reasons = [];
if (!input.model) reasons.push('missing-model');
if (!input.counterexample) reasons.push('missing-counterexample');
if (input.answer !== 'Promise.all-observes-settlement-not-cancellation') reasons.push('wrong-boundary');
return deepFreeze({ accepted: reasons.length === 0, reasons, handoff: reasons.length ? 'return-with-specific-gap' : 'synthetic-ready-for-human-review', productionEffect: 'not-attempted' });
}
export function createFixedTeachingAttempt(id) { return FIXED_ATTEMPTS[id] || null; }
export function checkFixedTeachingAttempt(input) { return assessFixedAttempt(input); }
export async function runFixedPromiseExercise() {
const trace = [];
let completeRemaining;
const remaining = new Promise((resolve) => { completeRemaining = () => { trace.push('remaining-input-fulfilled'); resolve('fixed-cache-value'); }; });
const combined = Promise.all([Promise.reject(new Error('fixed-parse-error')), remaining]).catch(() => { trace.push('aggregate-rejected'); return 'handled'; });
await combined;
completeRemaining();
await remaining;
return deepFreeze({ trace, interpretation: 'aggregate-rejection-did-not-finish-or-cancel-the-manually-controlled-remaining-input', productionEffect: 'not-attempted' });
}
export async function runTeachingFixture() {
const good = assessFixedAttempt(FIXED_ATTEMPTS['model-first-v1']);
const recipe = assessFixedAttempt(FIXED_ATTEMPTS['recipe-only-v1']);
const wrong = assessFixedAttempt(FIXED_ATTEMPTS['wrong-cancellation-v1']);
const exercise = await runFixedPromiseExercise();
return deepFreeze({ assertions: deepFreeze({ goodAccepted: good.accepted, goodHandoff: good.handoff === 'synthetic-ready-for-human-review', recipeStops: recipe.reasons.includes('missing-model'), recipeNeedsCounterexample: recipe.reasons.includes('missing-counterexample'), wrongStops: wrong.reasons.includes('wrong-boundary'), aggregateRejectsFirst: exercise.trace[0] === 'aggregate-rejected', remainingCanStillFinish: exercise.trace[1] === 'remaining-input-fulfilled', noProduction: exercise.productionEffect === 'not-attempted', frozenAttempts: Object.isFrozen(FIXED_ATTEMPTS), unknownStops: !assessFixedAttempt({}).accepted }) });
}
function revision(meta, parts) { const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + sourceList(); const proseLength = bodyText(contentHtml).length; if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength); return deepFreeze({ ...meta, contentHtml, proseLength }); }
const practice = revision({ slug: 'editorial-2025-10-practice-teaching-engineering', title: 'Как объяснить Promise.all: сначала модель, затем рецепт и проверка', categories: ['JavaScript', 'Наставничество'], cover: '/assets/editorial/2025/teaching-engineering-2025-model-exercise-check.svg', excerpt: 'Практический маршрут для объяснения Promise.all без ложного обещания отмены: задача, модель, контрпример, короткое упражнение и проверяемый synthetic hand-off.', readingMinutes: 12 }, [
p('Симптом знакомый: объяснение начинается с <code>await Promise.all(tasks)</code>, код копируют в обработчик, а после первой ошибки считают оставшуюся работу отменённой. Цена ошибки — повторный разбор уже начатых действий, лишняя логика очистки и обучение рецепту, который ломается при первом изменении условий.'),
p('Причина не в одной строке JavaScript. Рецепт скрывает модель: <code>Promise.all</code> собирает исходы входных 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 ошибкой. Второй намеренно удерживается функцией <code>completeRemaining</code>. Мы сначала ждём обработанный aggregate reject, потом вручную завершаем второй вход. Порядок trace — проверяемая модель: aggregate уже сообщил об ошибке, но второй input всё ещё может завершиться. Это не измеряет скорость и не описывает реальную интеграцию; это демонстрация того, какую границу обязан назвать автор объяснения.'),
h2('Контрпример важнее второго рецепта'),
p('Неправильная фраза звучит удобно: «<code>Promise.all</code> остановит всё при ошибке». Контрпример не спорит с удобством, он проверяет условие. У aggregate promise есть ранний rejected исход; у созданного ранее input нет в этом вызове отдельной команды cancel. Поэтому после reject нельзя делать вывод, что внешний запрос, таймер или вычисление исчезли. Для их остановки проектируют отдельный контракт и проверяют его отдельно.'),
h2('Короткое упражнение вместо пересказа'),
ol(['Дайте только задачу: «два значения нужны до зависимого расчёта».', 'Покажите trace и попросите назвать два разных объекта: aggregate и remaining input.', 'Попросите закончить фразу: «Для остановки внешней работы нужен отдельный …».', 'Проверьте не словом «верно», а fixed answer: <code>Promise.all-observes-settlement-not-cancellation</code>.', 'Передайте дальше только 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('Затем верните код и проверьте соответствие строк модели. Массив в <code>Promise.all</code> — набор inputs; переменная результата — aggregate; ветка <code>catch</code> — реакция зависимой логики на его 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 — новый объект, который создаёт <code>Promise.all(iterable)</code>. Внешняя операция — условная работа за пределами этой учебной модели; она может вообще отсутствовать, как в нашем ручном 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 <code>Promise.all</code> получает iterable, создаёт promise capability и передаёт iterator в <code>PerformPromiseAll</code>. Это важная, но узкая опора: combinator работает с promises и capability результата. Она не говорит «закрой сокет», «прерви вычисление» или «освободи файл». Поэтому из языковой семантики нельзя тихо вывести контракт ресурса приложения.'),
p('MDN закреплённым commit формулирует тот же видимый результат: aggregate fulfils после всех входов и rejects при первой ошибке; порядок значений следует порядку iterable, а не скорости завершения. Слова «первая ошибка» относятся к observable outcome aggregate. Они не превращают уже созданные входы в отменённые. Это различие и есть минимальная модель, которую надо передать до примера с <code>await</code>.'),
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. Он не добавляет прав остановки и не делает зависимый расчёт безопасным автоматически. Выбор между <code>all</code> и <code>allSettled</code> начинается с нужного результата aggregate: ранний stop дальнейшей логики или полный список исходов. Отмена живёт в третьем, отдельном вопросе.'),
h2('Упражнение на причинность'),
ol(['Прочитайте trace без кода и отметьте, чей исход зафиксирован на каждой строке.', 'Сформулируйте правило: aggregate reject запрещает какой именно зависимый расчёт?', 'Назовите факт, которого в trace нет: «внешняя работа отменена».', 'Добавьте в synthetic карточку отдельное поле <code>cancellationOwner</code>; в текущей модели оно намеренно отсутствует.', 'Сверьте вывод с negative attempt: ответ «Promise.all-cancels-everything» обязан вернуть <code>wrong-boundary</code>.']),
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 <code>recipe-only-v1</code>. В нём задача названа, но поля <code>model</code> и <code>counterexample</code> пусты; ответ не фиксирует границу отмены. Это не запись о кандидате, 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 этого модуля. Так не возникает видимости, будто мы валидируем реальные объяснения или строим универсальную систему оценки. В ответе есть <code>productionEffect: not-attempted</code>; функция не создаёт задачу, не пишет комментарий и не меняет состояние вне памяти.'),
h2('Как дать feedback, который можно выполнить'),
p('Неполезный feedback: «раскрой тему глубже». Он не называет наблюдение и даёт автору слишком много вариантов. Полезный feedback: «в строке с <code>Promise.all</code> добавь модель aggregate и один контрпример: после aggregate reject remaining input может завершиться; затем назови, какой отдельный contract отвечает за отмену». В нём есть объект, условие, ожидаемое изменение и граница, которую нельзя додумать автоматически.'),
h2('Короткая последовательность разбора'),
ol(['Зафиксируйте исходную задачу одной фразой, не начиная с имени API.', 'Найдите модель: входы, aggregate и зависимый расчёт.', 'Попросите один контрпример, который ломает слишком широкий рецепт.', 'Запустите fixed exercise и сравните trace с выводом автора.', 'Верните только конкретные gaps или synthetic hand-off для human review; не объявляйте результат доказательством обучения.']),
h2('Когда рецепт допустим'),
p('Рецепт полезен, когда модель уже названа и условия узкие. Например, после объяснения можно показать <code>await Promise.all([left, right])</code> как способ дождаться обоих значений перед зависимым расчётом. Но рядом остаётся ограничение: этот вызов не описывает отмену внешних действий и не выбирает обработку каждого результата. Если нужны все outcomes, вопрос меняется, и читатель должен сравнить его с <code>Promise.allSettled</code>, а не механически заменить имя.'),
h2('Проверяемый следующий шаг'),
p('Проверьте у собственного объяснения только четыре предложения: задача, модель, контрпример, отдельный владелец отмены. Если одно отсутствует, не добавляйте ещё один фрагмент кода — заполните конкретный пробел. Положительный исход этого упражнения ограничен: <code>model-first-v1</code> получает synthetic-ready-for-human-review. Он не доказывает улучшение человека, команды или production-кода.'),
h2('Три вопроса для независимого читателя'),
p('Независимый читатель не должен угадывать намерение автора. Поэтому после feedback дайте ему три вопроса без подсказки: «что вернёт aggregate при первой ошибке?», «что показывает второй элемент trace?» и «какой объект имеет право остановить внешнюю работу?». Ответы проверяют не запоминание имени метода, а наличие границы. Если третий ответ заменяется словом <code>Promise.all</code>, возвращаем только gap <code>wrong-boundary</code>.'),
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');