revise April 2027 build evolution plan articles
Build and deploy / deploy (push) Successful in 16s

This commit is contained in:
2026-07-31 20:28:42 +03:00
parent f6bf2914fe
commit 21b249b6c6
7 changed files with 338 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий # Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 331 из 358 созданных материалов. Остальные 27 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. На 31 июля 2026 года строгий аудит проходит 334 из 358 созданных материалов. Остальные 24 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия ## Одна партия
+96
View File
@@ -0,0 +1,96 @@
# P110 — April 2027: «Эволюция frontend-сборки»
## Статус, temporal boundary и write scope
Редакторская дата: **2026-07-31**. Апрель 2027 ещё не наступил. Три статьи — явно помеченный **план/сценарий на 2027-04** с source cutoff **2026-07-31**. В них не существуют и не заявляются: реальный config, CI artifact, build timing, cache hit, bundle size, migration, tool release, build result, winner, rollout или production change.
Очередь P110 запрашивает исторические config/CI-артефакты и одинаковый build-замер. В этом наборе они смоделированы только как именованные поля synthetic shape со значением `not-collected`; это не данные, не фрагменты системы и не скрытая методика. Единственный разрешённый положительный output — `synthetic-plan-hand-off`, всегда с `productionEffect: not-attempted`.
Изменены только:
- `web/scripts/upgrade-2027-04.mjs`
- `editorial/reviews/2027-04-draft.md`
- `web/public/assets/editorial/2027/build-evolution-2027-configuration-timeline.svg`
- `web/public/assets/editorial/2027/build-evolution-2027-comparable-conditions-matrix.svg`
- `web/public/assets/editorial/2027/build-evolution-2027-evidence-handoff-loop.svg`
Registry, README, `articles.json`, app, queue, P102–P109 и Git state не менялись.
## Pinned official primary sources
1. [webpack — Caching guide, immutable source snapshot](https://github.com/webpack/webpack.js.org/blob/b7fa07c852d24449e9445717c41395bdd2ca4784/src/content/guides/caching.mdx), official webpack documentation at immutable `b7fa07c852d24449e9445717c41395bdd2ca4784`, resolved on source cutoff **2026-07-31**. Используется только для терминов output/content hash/cache как частей configuration; не доказывает local config, timing, artifact или размер.
2. [actions/cache v4.2.3 — immutable README](https://github.com/actions/cache/blob/5a3ec84eff668545956fd18022155c47e93e2684/README.md), official GitHub Action at immutable `5a3ec84eff668545956fd18022155c47e93e2684`, pinned before source cutoff **2026-07-31**. Используется только для различения cache key, restore и save; не подтверждает workflow, runner, cache hit или запуск.
3. [SLSA v1.0 — Provenance](https://slsa.dev/spec/v1.0/provenance), official SLSA specification v1.0, pinned before cutoff. Используется как vocabulary для provenance артефакта; не утверждает существование, создание или проверку attestation.
Все источники первичные, official, фиксированы не позднее cutoff. Никаких источников о будущих релизах инструментов, будущем CI или будущих build results не использовано.
## Review pass 1 — редакционный фокус, голос и temporal honesty
- **Practice**: сравнение configuration до названия инструмента. В первых двух абзацах названы цена ложного «новая лучше» и цена выдуманной CI-картины. Timeline ведёт к question hand-off, а не к migration result.
- **Mechanism**: cache, CI, artifact и предел inference. В первых двух абзацах разделены разные слои evidence и цена сравнения разных условий. Matrix отделяет future provenance от недопустимых выводов о timing, hit и размере.
- **Field**: передача synthetic evidence, а не build report. В первых двух абзацах названы цена ложной операционной памяти и подмена пустой карточки полевым отчётом. Loop заканчивается future owner, не approval или rollout.
- Все тексты держат M10: прагматичный русский технический голос, без hype по инструментам и без retrospective tone. У каждого есть mechanism, доступная HTML table, meaningful русский alt/caption, runnable safe example, ordered actions, limitations и next step.
## Review pass 2 — literal safety, отсутствие фактов и fail-closed
- `configuration-hand-off-v1` требует `planDate: 2027-04`, `sourceCutoff: 2026-07-31`, две named synthetic configurations, named `comparableCondition` со state `not-collected`, а также `config`, `ci`, `buildTiming`, `cacheHit` и `bundleSize` строго `not-collected`.
- `undated-future-scenario-v1` возвращает `stop-undated-future-scenario-or-cutoff`.
- `missing-comparable-condition-v1` возвращает `stop-missing-comparable-condition`.
- `hidden-configuration-or-cache-assumption-v1` возвращает `stop-hidden-configuration-or-cache-assumption`; скрытая configuration и assumed cache не получают default.
- `disallowed-positive-result-v1` возвращает `stop-disallowed-positive-result`; нельзя заявить build improvement, migration или иной future outcome.
- Factory делает JSON clone fixed literal и recursive deep freeze, включая `historicalArtifacts`. Evaluator принимает только JSON-равный известный literal; unknown или mutation fail-closed. Модуль не читает файлы или конфигурацию, не запускает build/CI/cache и не обращается к environment, clock, network, secrets, telemetry или production.
## Review pass 3 — accessibility, visual inspection и uniqueness
- Все три SVG имеют русские `title` и `desc`, а статьи — осмысленные русские alt/caption. В SVG нет URL, измеренных значений или claims о build result.
- XML/safety проверка исключает `script`, `foreignObject`, `javascript:`, `data:image` и inline handlers.
- Sharp 375 px visual inspection проведена по PNG. Timeline: читаются четыре верхних шага, обе stop-ветви и подпись. Matrix: сохраняются пять строк, четыре колонки и нижний красный guard. Loop: различимы вход, дата, сравнимость, hand-off и центральный stop; текст не обрезан.
- Strict pairwise uniqueness включает code и исключает section `Проверяемые источники`. Для каждой пары `shared-12-word-fragments=0`.
## Actual checks and results
Выполнено из `web/`:
```sh
node --check scripts/upgrade-2027-04.mjs
node scripts/upgrade-2027-04.mjs --verify-fixture
npm run audit:draft -- scripts/upgrade-2027-04.mjs
node --input-type=module -e "import { createFixedBuildCase, assessFixedBuildPlan } from './scripts/upgrade-2027-04.mjs'; const ids=['configuration-hand-off-v1','undated-future-scenario-v1','missing-comparable-condition-v1','hidden-configuration-or-cache-assumption-v1','disallowed-positive-result-v1']; for (const id of ids) console.log(id, assessFixedBuildPlan(createFixedBuildCase(id)).status); const accepted=assessFixedBuildPlan(createFixedBuildCase()); console.log('exports-frozen', Object.isFrozen(createFixedBuildCase()), Object.isFrozen(createFixedBuildCase().historicalArtifacts), accepted.productionEffect);"
xmllint --noout public/assets/editorial/2027/build-evolution-2027-configuration-timeline.svg public/assets/editorial/2027/build-evolution-2027-comparable-conditions-matrix.svg public/assets/editorial/2027/build-evolution-2027-evidence-handoff-loop.svg
! rg -n -i '<(script|foreignObject)\\b|javascript:|data:image|(?:^|[[:space:]])on[a-z]+=' public/assets/editorial/2027/build-evolution-2027-configuration-timeline.svg public/assets/editorial/2027/build-evolution-2027-comparable-conditions-matrix.svg public/assets/editorial/2027/build-evolution-2027-evidence-handoff-loop.svg
node --input-type=module -e "import sharp from 'sharp'; for (const n of ['build-evolution-2027-configuration-timeline','build-evolution-2027-comparable-conditions-matrix','build-evolution-2027-evidence-handoff-loop']) await sharp('public/assets/editorial/2027/'+n+'.svg').resize({width:375}).png().toFile('/private/tmp/'+n+'-375.png');"
node --input-type=module -e "import { revisions } from './scripts/upgrade-2027-04.mjs'; const text=h=>h.replace(/<h2>Проверяемые источники<\\/h2>[\\s\\S]*/, '').replace(/<[^>]+>/g,' ').replace(/&(?:quot|amp|lt|gt|#039);/g,' ').toLowerCase().replace(/[^a-zа-яё0-9]+/giu,' ').trim().split(/\\s+/); const grams=a=>new Set(a.slice(0,-11).map((_,i)=>a.slice(i,i+12).join(' '))); let failed=false; for(let i=0;i<revisions.length;i++) for(let j=i+1;j<revisions.length;j++){const left=text(revisions[i].contentHtml), right=text(revisions[j].contentHtml), common=[...grams(left)].filter(x=>grams(right).has(x)); console.log(revisions[i].slug+' <> '+revisions[j].slug, 'shared-12-word-fragments='+common.length); if(common.length){failed=true;console.log(common.join('\\n'));}} if(failed) process.exitCode=1;"
```
Результаты фактического запуска:
- Syntax: PASS, exit 0.
- Fixture и article guards: PASS `8/8`; fixture `5/5`, все три article checks `true`.
- Exact body chars, без section sources: `editorial-2027-04-practice-build-evolution` — `10846`; `editorial-2027-04-mechanism-build-evolution` — `11098`; `editorial-2027-04-field-build-evolution` — `9766`. Каждый текст находится в общем диапазоне 5k–15k и целевом 9k–13k.
- Draft audit: PASS для трёх exact slugs.
- Literal public exports: PASS. Статусы cases: `synthetic-plan-hand-off`, `stop-undated-future-scenario-or-cutoff`, `stop-missing-comparable-condition`, `stop-hidden-configuration-or-cache-assumption`, `stop-disallowed-positive-result`; `exports-frozen true true not-attempted`.
- `xmllint`: PASS без output. SVG safety scan: PASS, `rg` не нашёл запрещённый content.
- Sharp rendering и 375 px manual inspection: PASS для всех трёх SVG.
- Strict pairwise duplicate scan (code included, sources excluded): PASS. `practice <> mechanism = 0`, `practice <> field = 0`, `mechanism <> field = 0` shared 12-word fragments.
Единственный не-failing output — существующие npm warnings о неизвестных user config `store-dir`, `cache-dir`, `public-hoist-pattern`; audit прошёл, warnings не относятся к P110.
## Независимая приёмка — 2026-07-31
### Проход 1 — источники, постановка и голос
- Mutable guide links заменены на проверяемые immutable snapshots: `webpack.js.org` commit `b7fa07c852d24449e9445717c41395bdd2ca4784` и `actions/cache` v4.2.3 commit `5a3ec84eff668545956fd18022155c47e93e2684`. Оба первичных источника доступны по закреплённым путям; SLSA остаётся версионной спецификацией v1.0.
- Проверена фактическая граница claim: источники задают терминологию content hash, cache key/restore/save и provenance, но ни один не превращён в доказательство local config, CI run, cache hit, bundle size или migration.
- У всех трёх статей собственная цена ошибки и самостоятельный вопрос: сравнимость config, уровни evidence и честная передача будущему владельцу. Автор говорит о стоимости решения и воспроизводимом методе, не рекламируя bundler.
### Проход 2 — исполнимый контракт и автоматический аудит
- `node --check`, fixture `8/8`, literal export/deep-freeze check и `npm run audit:draft -- scripts/upgrade-2027-04.mjs` завершились успешно. Длины body: 10 890, 11 098 и 9 766 знаков.
- Пять fixed cases дают ровно один `synthetic-plan-hand-off` и четыре fail-closed stop-ветви; positive result и скрытая config/cache assumption не проходят. Accepted branch сохраняет `productionEffect: not-attempted`.
- `xmllint` прошёл без output; SVG safety scan не нашёл executable или external active content.
### Проход 3 — визуал, повторяемость и выпуск
- Три SVG отрендерены и просмотрены в ширине 375 px: timeline, матрица и loop сохраняют заголовки, ячейки, стрелки и stop-блоки без обрезки.
- Cross-package scan P102–P110 включает код и исключает источники. Первый запуск выявил шесть общих 12-словных фрагментов у practice runnable example с P108/P109; example переписан как отдельная boolean-проверка question contract. Повторный запуск по 27 статьям: `collisions: []`.
- В реестр добавлены только три апрельские revisions; пользовательские app/data files не включены.
+2
View File
@@ -106,6 +106,7 @@ import { revisions as december2026Revisions } from '../scripts/upgrade-2026-12.m
import { revisions as january2027Revisions } from '../scripts/upgrade-2027-01.mjs'; import { revisions as january2027Revisions } from '../scripts/upgrade-2027-01.mjs';
import { revisions as february2027Revisions } from '../scripts/upgrade-2027-02.mjs'; import { revisions as february2027Revisions } from '../scripts/upgrade-2027-02.mjs';
import { revisions as march2027Revisions } from '../scripts/upgrade-2027-03.mjs'; import { revisions as march2027Revisions } from '../scripts/upgrade-2027-03.mjs';
import { revisions as april2027Revisions } from '../scripts/upgrade-2027-04.mjs';
// This layer replaces archived source entries without losing their stable slug and date. // This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [ export const editorialRevisions = [
@@ -217,4 +218,5 @@ export const editorialRevisions = [
...january2027Revisions, ...january2027Revisions,
...february2027Revisions, ...february2027Revisions,
...march2027Revisions, ...march2027Revisions,
...april2027Revisions,
]; ];
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
<title id="title">Матрица сравнимых условий frontend-сборки</title><desc id="desc">Русская таблица связывает вход, конфигурацию, кэш, CI и артефакт с будущим evidence. Нижняя красная строка сообщает, что скрытое условие и положительный вывод останавливают сценарий.</desc>
<rect width="1200" height="720" fill="#f8fafc"/><style>text{font-family:Arial,sans-serif;fill:#172033}.h{font-size:30px;font-weight:700}.s{font-size:19px;font-weight:700}.b{font-size:16px}.head{fill:#1f5f8b}.cell{fill:#fff;stroke:#8aa8be;stroke-width:2}.warn{fill:#fff0f0;stroke:#bd3f3f;stroke-width:3}</style>
<text x="55" y="58" class="h">Сравнимость: condition до числа и вывода</text><text x="55" y="86" class="b">План 2027-04 · все исторические evidence остаются not-collected</text>
<rect x="55" y="115" width="1090" height="62" class="head"/><g font-family="Arial" font-size="18" font-weight="700" fill="#fff"><text x="82" y="153" fill="#fff">Слой</text><text x="300" y="153" fill="#fff">Сейчас можно назвать</text><text x="590" y="153" fill="#fff">Нужно future evidence</text><text x="895" y="153" fill="#fff">Нельзя вывести</text></g>
<g class="b"><rect x="55" y="177" width="1090" height="72" class="cell"/><text x="82" y="220">Input</text><text x="300" y="220">synthetic shape</text><text x="590" y="220">граница входа</text><text x="895" y="220">что приложения равны</text><rect x="55" y="249" width="1090" height="72" class="cell"/><text x="82" y="292">Configuration</text><text x="300" y="292">два placeholder</text><text x="590" y="292">flags и provenance</text><text x="895" y="292">эквивалентность</text><rect x="55" y="321" width="1090" height="72" class="cell"/><text x="82" y="364">Cache</text><text x="300" y="364">not-collected</text><text x="590" y="364">key и state</text><text x="895" y="364">hit или ускорение</text><rect x="55" y="393" width="1090" height="72" class="cell"/><text x="82" y="436">CI</text><text x="300" y="436">condition question</text><text x="590" y="436">runner и boundary</text><text x="895" y="436">сравнимый timing</text><rect x="55" y="465" width="1090" height="72" class="cell"/><text x="82" y="508">Artifact</text><text x="300" y="508">artifact question</text><text x="590" y="508">manifest и состав</text><text x="895" y="508">меньший bundle</text></g>
<rect x="55" y="575" width="1090" height="88" rx="12" class="warn"/><text x="85" y="615" class="s">STOP: missing comparable condition, hidden configuration/cache assumption или positive result</text><text x="85" y="646" class="b">Разрешён только synthetic-plan-hand-off; productionEffect: not-attempted.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
<title id="title">Временная схема планового сравнения конфигураций frontend-сборки</title><desc id="desc">Русская схема ведёт от даты и двух synthetic конфигураций через условие сравнения к передаче вопроса будущему владельцу. Красные блоки останавливают недатированный сценарий и положительный результат.</desc>
<rect width="1200" height="720" fill="#f8fafc"/><style>text{font-family:Arial,sans-serif;fill:#172033}.h{font-size:31px;font-weight:700}.s{font-size:20px;font-weight:700}.b{font-size:16px}.box{fill:#fff;stroke:#3976a8;stroke-width:3}.ok{fill:#edf8f1;stroke:#278456;stroke-width:3}.stop{fill:#fff0f0;stroke:#bd3f3f;stroke-width:3}.line{stroke:#5b7187;stroke-width:4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0 0L0 6L9 3z" fill="#5b7187"/></marker></defs>
<text x="55" y="58" class="h">2027-04: сначала конфигурации, затем evidence</text><text x="55" y="88" class="b">source cutoff 2026-07-31 · future scenario · productionEffect: not-attempted</text>
<rect x="55" y="176" width="230" height="125" rx="12" class="box"/><text x="82" y="217" class="s">1. Граница времени</text><text x="82" y="250" class="b">2027-04 / cutoff</text><text x="82" y="276" class="b">не report</text>
<rect x="355" y="176" width="230" height="125" rx="12" class="box"/><text x="383" y="217" class="s">2. Две стороны</text><text x="383" y="250" class="b">baseline / candidate</text><text x="383" y="276" class="b">synthetic names</text>
<rect x="655" y="176" width="230" height="125" rx="12" class="box"/><text x="682" y="217" class="s">3. Условие</text><text x="682" y="250" class="b">input + output + CI</text><text x="682" y="276" class="b">not-collected</text>
<rect x="955" y="176" width="190" height="125" rx="12" class="ok"/><text x="982" y="217" class="s">4. Hand-off</text><text x="982" y="250" class="b">future owner</text><text x="982" y="276" class="b">без winner</text>
<path d="M285 238H355M585 238H655M885 238H955" class="line"/>
<rect x="90" y="455" width="310" height="115" rx="12" class="stop"/><text x="118" y="496" class="s">STOP: нет даты</text><text x="118" y="529" class="b">undated future scenario</text><text x="118" y="553" class="b">не становится планом</text>
<rect x="770" y="455" width="330" height="115" rx="12" class="stop"/><text x="798" y="496" class="s">STOP: «стало лучше»</text><text x="798" y="529" class="b">нет config, artifact, timing,</text><text x="798" y="553" class="b">cache hit или migration result</text>
<path d="M170 301V455M1045 301V455" class="line"/><text x="600" y="655" text-anchor="middle" class="b">Исторические config/CI-артефакты и результаты — только not-collected shape, не данные.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
<title id="title">Петля передачи синтетического evidence для frontend-сборки</title><desc id="desc">Русская диаграмма показывает путь от синтетического вопроса к проверке даты, двух конфигураций и условия сравнения. Ошибка уходит в красный stop; корректная форма передаётся будущему владельцу без результата production.</desc>
<rect width="1200" height="720" fill="#f8fafc"/><style>text{font-family:Arial,sans-serif;fill:#172033}.h{font-size:30px;font-weight:700}.s{font-size:20px;font-weight:700}.b{font-size:16px}.box{fill:#fff;stroke:#3976a8;stroke-width:3}.ok{fill:#edf8f1;stroke:#278456;stroke-width:3}.stop{fill:#fff0f0;stroke:#bd3f3f;stroke-width:3}.line{stroke:#5b7187;stroke-width:4;fill:none;marker-end:url(#a)}</style><defs><marker id="a" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><path d="M0 0L0 6L9 3z" fill="#5b7187"/></marker></defs>
<text x="55" y="58" class="h">Synthetic evidence hand-off: вопрос не становится отчётом</text><text x="55" y="88" class="b">2027-04 scenario · source cutoff 2026-07-31 · no build report</text>
<rect x="55" y="178" width="230" height="120" rx="12" class="box"/><text x="82" y="219" class="s">Вход</text><text x="82" y="251" class="b">fixed in-memory literal</text><text x="82" y="276" class="b">not-collected shape</text>
<rect x="355" y="178" width="230" height="120" rx="12" class="box"/><text x="382" y="219" class="s">Проверка даты</text><text x="382" y="251" class="b">2027-04 / cutoff</text><text x="382" y="276" class="b">не retrospective</text>
<rect x="655" y="178" width="230" height="120" rx="12" class="box"/><text x="682" y="219" class="s">Сравнимость</text><text x="682" y="251" class="b">две configuration</text><text x="682" y="276" class="b">named condition</text>
<rect x="955" y="178" width="190" height="120" rx="12" class="ok"/><text x="981" y="219" class="s">Передача</text><text x="981" y="251" class="b">future owner</text><text x="981" y="276" class="b">not-attempted</text>
<path d="M285 238H355M585 238H655M885 238H955" class="line"/>
<rect x="305" y="462" width="590" height="118" rx="12" class="stop"/><text x="335" y="504" class="s">STOP: дата отсутствует, condition не назван или configuration/cache скрыты</text><text x="335" y="538" class="b">Запрещено: cache hit, build timing, bundle size, migration или «сборка улучшена».</text><text x="335" y="563" class="b">Следующий ход — уточнить вопрос, а не заполнить пробел предположением.</text>
<path d="M470 298V462M770 298V462" class="line"/><text x="600" y="656" text-anchor="middle" class="b">Допустимый output: synthetic-plan-hand-off → future evidence owner</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+206
View File
@@ -0,0 +1,206 @@
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((cell) => '<th scope="col">' + cell + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function cloneFixed(value) { return JSON.parse(JSON.stringify(value)); }
function deepFreeze(value) { if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(deepFreeze); Object.freeze(value); } return value; }
function plainText(html) { return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
function bodyText(html) { return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
const REFERENCES = deepFreeze({
webpackCaching: { title: 'webpack — Caching guide (immutable source snapshot)', url: 'https://github.com/webpack/webpack.js.org/blob/b7fa07c852d24449e9445717c41395bdd2ca4784/src/content/guides/caching.mdx', version: 'webpack.js.org commit b7fa07c852d24449e9445717c41395bdd2ca4784, resolved at source cutoff 2026-07-31' },
githubCache: { title: 'actions/cache v4.2.3 — immutable README', url: 'https://github.com/actions/cache/blob/5a3ec84eff668545956fd18022155c47e93e2684/README.md', version: 'actions/cache v4.2.3 commit 5a3ec84eff668545956fd18022155c47e93e2684, pinned before source cutoff 2026-07-31' },
slsa: { title: 'SLSA v1.0 — Provenance', url: 'https://slsa.dev/spec/v1.0/provenance', version: 'SLSA specification v1.0, pinned before source cutoff 2026-07-31' },
});
function sources(entries) { return '<ul>' + entries.map(({ key, use, boundary }) => { const ref = REFERENCES[key]; return '<li><a href="' + ref.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(ref.title) + '</a> — ' + escapeHtml(ref.version) + '. ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>'; }).join('') + '</ul>'; }
const FIXED_BUILD_CASES = deepFreeze({
'configuration-hand-off-v1': {
id: 'configuration-hand-off-v1', planDate: '2027-04', sourceCutoff: '2026-07-31',
leftConfiguration: { id: 'named-synthetic-baseline-configuration', state: 'named-synthetic' },
rightConfiguration: { id: 'named-synthetic-candidate-configuration', state: 'named-synthetic' },
comparableCondition: { id: 'named-synthetic-comparable-condition', state: 'not-collected' },
historicalArtifacts: { config: 'not-collected', ci: 'not-collected', buildTiming: 'not-collected', cacheHit: 'not-collected', bundleSize: 'not-collected' },
requestedConclusion: 'synthetic-plan-hand-off',
boundary: 'Fixed in-memory planning literal. No repository, configuration, CI artifact, build, cache, bundle, timing, migration, release, tool, environment, clock, network, secret, telemetry, customer data or production system is read, created, changed or inferred.',
},
'undated-future-scenario-v1': {
id: 'undated-future-scenario-v1', planDate: '', sourceCutoff: '2026-07-31',
leftConfiguration: { id: 'named-synthetic-baseline-configuration', state: 'named-synthetic' }, rightConfiguration: { id: 'named-synthetic-candidate-configuration', state: 'named-synthetic' }, comparableCondition: { id: 'named-synthetic-comparable-condition', state: 'not-collected' }, historicalArtifacts: { config: 'not-collected', ci: 'not-collected', buildTiming: 'not-collected', cacheHit: 'not-collected', bundleSize: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.',
},
'missing-comparable-condition-v1': {
id: 'missing-comparable-condition-v1', planDate: '2027-04', sourceCutoff: '2026-07-31',
leftConfiguration: { id: 'named-synthetic-baseline-configuration', state: 'named-synthetic' }, rightConfiguration: { id: 'named-synthetic-candidate-configuration', state: 'named-synthetic' }, comparableCondition: { id: '', state: 'missing' }, historicalArtifacts: { config: 'not-collected', ci: 'not-collected', buildTiming: 'not-collected', cacheHit: 'not-collected', bundleSize: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.',
},
'hidden-configuration-or-cache-assumption-v1': {
id: 'hidden-configuration-or-cache-assumption-v1', planDate: '2027-04', sourceCutoff: '2026-07-31',
leftConfiguration: { id: 'named-synthetic-baseline-configuration', state: 'named-synthetic' }, rightConfiguration: { id: 'hidden', state: 'hidden-assumption' }, comparableCondition: { id: 'named-synthetic-comparable-condition', state: 'not-collected' }, historicalArtifacts: { config: 'not-collected', ci: 'not-collected', buildTiming: 'not-collected', cacheHit: 'assumed', bundleSize: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.',
},
'disallowed-positive-result-v1': {
id: 'disallowed-positive-result-v1', planDate: '2027-04', sourceCutoff: '2026-07-31',
leftConfiguration: { id: 'named-synthetic-baseline-configuration', state: 'named-synthetic' }, rightConfiguration: { id: 'named-synthetic-candidate-configuration', state: 'named-synthetic' }, comparableCondition: { id: 'named-synthetic-comparable-condition', state: 'not-collected' }, historicalArtifacts: { config: 'not-collected', ci: 'not-collected', buildTiming: 'not-collected', cacheHit: 'not-collected', bundleSize: 'not-collected' }, requestedConclusion: 'build-improved', boundary: 'Negative fixed literal only.',
},
});
export function createFixedBuildCase(id = 'configuration-hand-off-v1') { const value = FIXED_BUILD_CASES[id]; return value ? deepFreeze(cloneFixed(value)) : undefined; }
function stop(status, reason, nextAction) { return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' }); }
export function assessFixedBuildPlan(input) {
if (!Object.values(FIXED_BUILD_CASES).some((item) => JSON.stringify(item) === JSON.stringify(input))) return stop('stop-unknown-fixed-input', 'input-is-not-a-known-named-fixed-literal', 'select-a-named-fixed-case');
if (input.planDate !== '2027-04' || input.sourceCutoff !== '2026-07-31') return stop('stop-undated-future-scenario-or-cutoff', 'april-2027-plan-and-july-2026-cutoff-are-required', 'name-2027-04-and-2026-07-31');
if (!input.leftConfiguration?.id || input.leftConfiguration.state !== 'named-synthetic' || !input.rightConfiguration?.id || input.rightConfiguration.state !== 'named-synthetic') return stop('stop-hidden-configuration-or-cache-assumption', 'each-side-must-be-a-named-synthetic-configuration', 'name-both-configuration-placeholders-without-claiming-real-config');
if (!input.comparableCondition?.id || input.comparableCondition.state !== 'not-collected') return stop('stop-missing-comparable-condition', 'a-named-comparable-condition-must-remain-not-collected', 'name-the-future-comparison-condition');
const artifacts = input.historicalArtifacts;
if (!artifacts || !['config', 'ci', 'buildTiming', 'cacheHit', 'bundleSize'].every((key) => artifacts[key] === 'not-collected')) return stop('stop-hidden-configuration-or-cache-assumption', 'historical-artifacts-and-cache-assumptions-must-remain-not-collected', 'do-not-infer-config-ci-cache-or-result');
if (input.requestedConclusion !== 'synthetic-plan-hand-off') return stop('stop-disallowed-positive-result', 'future-plan-cannot-claim-a-better-build-or-migration-result', 'use-synthetic-plan-hand-off');
return deepFreeze({ status: 'synthetic-plan-hand-off', caseId: input.id, leftConfiguration: deepFreeze(cloneFixed(input.leftConfiguration)), rightConfiguration: deepFreeze(cloneFixed(input.rightConfiguration)), comparableCondition: deepFreeze(cloneFixed(input.comparableCondition)), historicalArtifacts: deepFreeze(cloneFixed(artifacts)), productionEffect: 'not-attempted', nextAction: 'give-the-named-synthetic-comparison-question-to-a-future-evidence-owner' });
}
export function runFixedBuildFixture() {
const expected = [['configuration-hand-off-v1', 'synthetic-plan-hand-off'], ['undated-future-scenario-v1', 'stop-undated-future-scenario-or-cutoff'], ['missing-comparable-condition-v1', 'stop-missing-comparable-condition'], ['hidden-configuration-or-cache-assumption-v1', 'stop-hidden-configuration-or-cache-assumption'], ['disallowed-positive-result-v1', 'stop-disallowed-positive-result']];
const checks = expected.map(([id, status]) => ({ id, expected: status, actual: assessFixedBuildPlan(createFixedBuildCase(id)).status }));
const sample = createFixedBuildCase();
return deepFreeze({ passed: checks.filter((item) => item.expected === item.actual).length, total: checks.length, accepted: checks.every((item) => item.expected === item.actual) && Object.isFrozen(sample) && Object.isFrozen(sample.historicalArtifacts), checks: deepFreeze(checks) });
}
function revision(meta, parts, referenceEntries) { const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries); const proseLength = bodyText(contentHtml).length; if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength); return deepFreeze({ ...meta, contentHtml, proseLength }); }
const refs = [
{ key: 'webpackCaching', use: 'Официальный материал задаёт словарь output, content hash и cache как свойств конфигурации.', boundary: 'Не является данными о данном приложении, CI, артефакте, времени или размере.' },
{ key: 'githubCache', use: 'Официальный материал нужен только для различения cache key, restore и save как частей будущего условия.', boundary: 'Не подтверждает наличие workflow, cache hit, runner или результат запуска.' },
{ key: 'slsa', use: 'Спецификация помогает отделить вопрос о provenance артефакта от самого артефакта.', boundary: 'Не утверждает, что provenance создана, проверена или применена здесь.' },
];
const practice = revision({ slug: 'editorial-2027-04-practice-build-evolution', title: 'Эволюция frontend-сборки: сначала сравнить конфигурации', categories: ['Frontend', 'Инженерные практики'], cover: '/assets/editorial/2027/build-evolution-2027-configuration-timeline.svg', excerpt: 'План на апрель 2027: как ставить вопрос о frontend-сборке без легенды о более быстром новом инструменте.', readingMinutes: 22 }, [
p('Это план/сценарий на 2027-04 с source cutoff 2026-07-31, не отчёт о сделанной миграции frontend-сборки. Дорогая ошибка начинается с удобной фразы: «новая сборка лучше старой». В неё одним словом складывают другой тип приложения, другой вход, другую оптимизацию, иное разбиение output и новый способ доставки. Цена не в терминологии: команда тратит недели на перенос, а потом не может сказать, что именно стало объектом сравнения и где возникла разница.'),
p('Вторая стоимость появляется, когда будущую CI-картину дорисовывают задним числом: якобы был cache hit, артефакт оказался меньше, job закончилась быстрее или переход уже завершён. На редакторскую дату апрель 2027 ещё впереди; у P110 нет config, CI artifact, build timing, cache hit, bundle size, migration, tool release или результата. Единственный разрешённый положительный выход — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>. Остальное в этом тексте — вопрос, а не наблюдение.'),
h2('Сравнивать не ярлыки, а действующие условия'),
p('Название сборщика слишком грубо для инженерного решения. Оно не показывает, какие entry points существуют, какие transforms включены, куда попадает CSS, какие внешние зависимости считаются частью output и какую цель преследует сборка. Два процесса с одинаковой надписью могут различаться сильнее, чем два процесса с разными названиями. Поэтому практический старт — назвать левую и правую configuration как synthetic placeholders, а не объявить один инструмент «устаревшим».'),
p('В этом материале <code>named-synthetic-baseline-configuration</code> и <code>named-synthetic-candidate-configuration</code> не являются файлами, package manifest или fragment реального pipeline. Это фиксированные in-memory literals, запрещающие подмену вопроса правдоподобными деталями. Они помогают сформулировать будущему owner задачу: восстановить provenance каждого условия, если для этого появится отдельный scope. Пока доказательств нет, даже слово «baseline» здесь не означает существующую сборку.'),
figure('/assets/editorial/2027/build-evolution-2027-configuration-timeline.svg', 'Русская временная схема: от именования двух synthetic конфигураций через условия сравнения к future evidence hand-off; красные остановки запрещают недатированный сценарий и вывод о преимуществе.', 'Схема описывает плановый порядок вопросов на 2027-04. Она не содержит конфигурации проекта, CI-лога, артефакта или измерения.'),
table('Что требуется сопоставить до разговора о замене', ['Плоскость', 'Допустимое имя в плане', 'Чего это не доказывает', 'Будущий вопрос'], [
['Вход', 'named synthetic application shape', 'что приложения одинаковы', 'какой route и input сравниваем'],
['Configuration', 'baseline / candidate placeholder', 'что известны flags и plugins', 'какие параметры имели provenance'],
['Output', 'artifact question', 'размер или состав файла', 'какой manifest и способ подсчёта нужен'],
['CI', 'comparable condition question', 'runner, очередь или timing', 'какая среда считается одинаковой'],
['Итог', 'synthetic-plan-hand-off', 'улучшение или завершение миграции', 'нужен ли authorised evidence scope'],
]),
h2('Configuration — это контракт, а не фон'),
p('Практическая карта должна отделять намерение от механизма. Намерение можно назвать без факта: например, будущая сторона сравнения должна отвечать на один и тот же вопрос о доставляемом клиентском output. Механизм требует списка условий: source revision, dependency resolution, target environment, transform chain, optimisation policy, asset treatment и правила очистки output. Пока ни одно из этих значений не собрано, их нельзя заменить общими словами «обычная production-сборка».'),
p('Это ограничение защищает и от симметричной ошибки. Не следует защищать прошлый путь только потому, что у него привычный config. Привычность не свидетельствует о неизменности входов, качества кэша или пригодности артефакта. Сравнение начинается с симметричного пробела знаний: обе стороны получают явные placeholders, а доказательства для обеих должны иметь одинаково читаемую provenance. Иначе новая сторона обязана раскрывать условия, а старая остаётся легендой.'),
h2('Почему cache нельзя считать результатом'),
p('Кэш может участвовать в будущем условии, но не должен становиться украшением текста. Cache key, restore source, момент сохранения, доступность workspace и холодное состояние способны менять ход job; из этого не следует ни конкретная семантика CI, ни будущий cache hit. Даже если инструментальная документация описывает caching, она описывает возможности и ограничения механизма, а не событие в чужом workflow. В P110 поле <code>cacheHit</code> оставлено <code>not-collected</code>.'),
p('Тот же принцип работает для artifact. Bundle size — это не свойство ярлыка и не обязательный признак прогресса. Нужно заранее согласовать, что считается output, как обходятся source map, сжатие, generated assets, code splitting и служебные файлы. Нельзя называть число, пока не определены единица, состав и момент сбора. Меньшее значение без договора способно скрыть вынесенный ресурс, изменённый путь или просто другую границу подсчёта.'),
h2('Runnable пример проверяет границу утверждения'),
code("import { createFixedBuildCase, assessFixedBuildPlan } from './upgrade-2027-04.mjs';\n\nconst question = createFixedBuildCase('configuration-hand-off-v1');\nconst handoff = assessFixedBuildPlan(question);\nconst planOnly = handoff.status === 'synthetic-plan-hand-off'\n && handoff.productionEffect === 'not-attempted';\nconsole.log(planOnly ? 'question-contract' : 'reject');\n// question-contract"),
p('Пример буквально запускается только над named fixed in-memory literal. Factory сначала делает JSON clone, затем рекурсивный deep freeze; caller не получает общую изменяемую ссылку. Evaluator принимает лишь JSON-равный известный literal и fail-closed отклоняет произвольный object. Код не читает конфигурацию, не вызывает CI, не создаёт артефакт, не запускает build и не смотрит часы, environment, сеть, secrets или telemetry. Поэтому его output — контроль честности hand-off, не synthetic benchmark.'),
h2('Как читать старую и новую сторону без ретроспективы'),
p('Когда будущая работа всё-таки получит доступ к материалам, полезно не превращать их в один огромный diff. Сначала читают договорённость о входе: откуда берётся исходный код, какие generated parts считаются входом, какая зависимость фиксируется и что не входит в вопрос. Потом отдельно смотрят преобразования и output contract. Такое разбиение не гарантирует, что сравнение возможно, зато быстро показывает, где одна из сторон описана лишь частично. До появления материалов это не инструкция на конкретные команды, а порядок будущего чтения.'),
p('Слово «новая» тоже нуждается в разборе. Оно может означать другой toolchain, но также другой layout проекта, новый способ разделять код, иной формат статических файлов или иной контракт с платформой доставки. Если эти изменения свернуть в один коммит и один вывод, невозможно определить, что именно следует откатывать при проблеме. Плановая карточка поэтому не требует доказать превосходство candidate. Она требует назвать, какие независимые изменения вообще могут существовать и какие из них нельзя скрывать за общим названием migration.'),
h2('Одинаковый вопрос не обязан иметь одинаковый ответ'),
p('Comparable condition не пытается стереть реальность двух подходов. Условия могут позволять разную структуру output, если именно эта разница входит в заранее названный вопрос. Недопустимо другое: одной стороне разрешают исключить ресурс из подсчёта, а другой нет; одной стороне дают подготовленное состояние, а для другой измеряют первое получение зависимостей. В таких случаях корректный conclusion не «кто-то проиграл», а «собранные записи не отвечают на один и тот же вопрос». Это ценный отрицательный результат будущего evidence scope.'),
p('Нужна также граница между сравнением для пользователя и сравнением для разработчика. Стабильность хеша, размер доставляемого набора, длительность локального rebuild и длительность CI job могут быть отдельными вопросами с разными input и ценой. Склеить их в слово «быстрее» — значит дать метрике чужого уровня решать архитектуру. P110 не выбирает, какой из вопросов важнее. Он требует, чтобы future owner не использовал сигнал одного уровня как доказательство результата на другом.'),
h2('Честная запись о том, чего нет'),
p('Иногда команда опасается, что поле <code>not-collected</code> выглядит как незавершённость. В плановом документе это наоборот полезная информация: она сообщает, что автор видел необходимость evidence, но не получил права или материала его собрать. Такое поле делает hand-off проверяемым: поздний читатель видит, что отсутствует именно config, CI artifact или cache observation, а не всё знание о задаче. Вымышленный ноль, примерный процент или безымянный «успех» были бы хуже, потому что создавали бы ложный объект для обсуждения.'),
p('Порог для перехода к данным следует назвать заранее: новый scope должен позволять читать конкретные inputs, сохранять их provenance и обсуждать последствия их публикации. Без этого даже аккуратная команда рискует вынести служебные детали в редакционную статью или, наоборот, заменить их обобщением, которое больше нельзя проверить. В текущем plan такого порога нет. Он заканчивается не началом работы с системой, а точной причиной, по которой следующему владельцу потребуется отдельное решение.'),
h2('Порядок действий для будущего владельца'),
ol(['Сохранить пару <code>planDate: 2027-04</code> и <code>sourceCutoff: 2026-07-31</code>; недатированный future scenario остановить.', 'Назвать две стороны как placeholders и не подставлять реальный config без отдельного разрешения.', 'Определить один named comparable condition: input, границу output и среду должны можно было прочитать у обеих сторон.', 'Запросить provenance historical config и CI artifact; до получения оставить все поля <code>not-collected</code>.', 'Отдельно решить, нужен ли build-запуск; если нужен, документировать методику до запуска, а не после числа.', 'Передать question contract future evidence owner без winner, migration claim и production effect.']),
h2('Четыре стоп-сигнала в практической карточке'),
p('Практическая карточка должна быть способна назвать свою причину остановки. <code>undated-future-scenario-v1</code> не даёт спрятать обещание за неопределённым «потом». <code>missing-comparable-condition-v1</code> не позволяет выдать два похожих запуска за одну проверку. <code>hidden-configuration-or-cache-assumption-v1</code> не заменяет неизвестный config или cache удобным default. Наконец, <code>disallowed-positive-result-v1</code> закрывает «сборка улучшена», пока нет отдельного evidence record. Эти labels не описывают инцидент; они делают видимой форму ошибки в плановом документе.'),
h2('Ограничения и следующий шаг'),
p('Этот practice не говорит, какой bundler выбрать, как переписать config, какую версию выпустить или какие шаги CI использовать. Он не оценивает совместимость, безопасность, лицензии, доступность plugin, стоимость runner и риск миграции. Официальные источники внизу дают язык для разговора о caching и provenance, но не поставляют ни одного local artifact. Их нельзя использовать как замену историческому config или записи запуска.'),
p('Следующий шаг — не «начать миграцию», а решить, есть ли полномочия собрать доказательства для названного condition. Если такие полномочия появятся, новый материал обязан явно отделить реальные inputs, метод и результат от этого апрельского плана. Если нет, корректно оставить hand-off незакрытым. Это менее эффектно, чем обещание эволюции, но дешевле, чем сделать архитектурное решение на основании названия инструмента.'),
], refs);
const mechanism = revision({ slug: 'editorial-2027-04-mechanism-build-evolution', title: 'Эволюция frontend-сборки: сравнимость кэша, CI и артефакта', categories: ['Frontend', 'Сборка'], cover: '/assets/editorial/2027/build-evolution-2027-comparable-conditions-matrix.svg', excerpt: 'План на апрель 2027: механизм честного сравнения конфигураций без выдуманного времени сборки и cache hit.', readingMinutes: 23 }, [
p('План/сценарий на 2027-04 с source cutoff 2026-07-31 разбирает механизм сравнимости, а не удачную оптимизацию. Конкретная проблема: два build запускают с разными входами и называют разницу временем сборки. У одного есть предварительно подготовленный workspace, у другого меняется dependency graph; один публикует часть output, другой считает полный каталог. Цена такого сравнения — ложная причинность: следующий change закрепляет случайное преимущество и усложняет откат.'),
p('Вторая проблема — неразличённые уровни evidence. Config, CI trace, cache state, artifact manifest и итоговый вывод выглядят как один отчёт, хотя отвечают на разные вопросы. В будущем апреле 2027 они не собраны: нет run, CI artifact, timing, cache hit, bundle size, migration, release или production outcome. Позитивный результат здесь запрещён; evaluator может вернуть только <code>synthetic-plan-hand-off</code>, а productionEffect остаётся <code>not-attempted</code>.'),
h2('Сравнимость сначала строят, затем измеряют'),
p('Сравнимость — не обещание равных чисел и не настройка одного инструмента. Это явное правило, по которому future owner сможет решить, относятся ли две записи к одному вопросу. В минимальном виде правило связывает форму входа, границу config, способ получения зависимостей, состояние cache, runner context, состав output и метод фиксации artefact. Отсутствие одного звена не делает результат «приблизительно честным»: оно меняет смысл inference.'),
p('Особенно опасно оставлять скрытыми параметры, которые кажутся техническим шумом. Смена target, optimisation mode, lockfile, source map policy или restore key может изменить ход обработки раньше, чем появится финальный файл. Перечисление не означает, что такие значения существуют в P110; это список классов вопросов для будущей методики. Именно поэтому fixed literal не содержит правдоподобных flags. Он хранит только named placeholders и <code>not-collected</code>.'),
figure('/assets/editorial/2027/build-evolution-2027-comparable-conditions-matrix.svg', 'Русская матрица сопоставляет вход, configuration, cache, CI и artifact с требуемой future provenance; красная строка останавливает скрытое условие и положительный вывод.', 'Матрица показывает логическую полноту будущего сравнения. В ячейках нет реальных значений, timing, cache hit или размера bundle.'),
table('Матрица inference без измеренного результата', ['Слой', 'Что можно назвать сейчас', 'Какое evidence потребуется потом', 'Какой вывод пока запрещён'], [
['Input', 'synthetic application shape', 'зафиксированная форма входа', 'что приложения равны'],
['Configuration', 'два named placeholders', 'полная provenance параметров', 'что замена эквивалентна'],
['Cache', 'state not-collected', 'ключ, источник, холодный/тёплый режим', 'что был hit или выигрыш'],
['CI', 'comparable condition question', 'runner и порядок шагов', 'что timing сравним'],
['Artifact', 'artifact question', 'manifest и метод подсчёта', 'что bundle меньше или лучше'],
]),
h2('Кэш меняет эксперимент, а не только скорость'),
p('В разговоре о CI cache часто появляется как уже понятная оптимизация. Для inference этого недостаточно. Нужно отделить cache, который ускоряет повторное получение зависимостей, от промежуточных результатов transform и от сохранённого output. Их границы, ключи и invalidation могут быть различны. Без этой карты фраза «второй запуск быстрее» ничего не говорит: возможно, менялась не configuration, а история рабочего каталога или очередь runner.'),
p('Даже корректно названный cache condition не позволяет перейти к будущему результату. Сначала owner должен решить, сравниваются холодные состояния, повторные состояния или оба режима; затем описать одинаковый порядок подготовки для двух сторон. Это не приглашение выполнить команды в данном draft. Это требование к будущему доказательству. P110 не создаёт key, directory, workflow или результат restore, поэтому evaluator stop-веткой закрывает любое значение, отличное от <code>not-collected</code>.'),
h2('CI timing не существует без границы времени'),
p('Время job нельзя считать цельным свойством сборщика. В него могут войти ожидание, checkout, сеть, dependency install, transform, архивирование, публикация, cleanup и отчётность. Полезный будущий протокол обязан назвать начало и конец отсчёта, а также перечислить, какие внешние фазы исключены. Иначе одна сторона получает полное wall-clock время, а другая — только узкий участок, и цифры становятся риторикой.'),
p('Здесь нельзя улучшить текст выдуманным диапазоном или synthetic milliseconds. Synthetic shape допустима только как форма поля, не как измеренное значение. Она может сказать «будущий timing evidence должен иметь границу», но не «ожидаемая сборка займёт X». Такое различение важно: правдоподобная цифра переживёт draft и станет ложным требованием. Отсутствующее число менее опасно, потому что не имитирует наблюдение.'),
h2('Artifact сравнивают по составу и provenance'),
p('Artifact — не синоним одного zip, каталога или bundle. Прежде чем говорить о размере, будущей проверке надо выбрать состав: входят ли HTML, JavaScript, CSS, map, статические ресурсы, generated metadata и compression representation. Затем необходим способ связать состав с configuration и input. SLSA provenance полезна как терминология происхождения, но сама спецификация не сообщает, что у P110 существовал или проверялся какой-либо attestation.'),
p('Из этого следует предел inference. Одинаковый filename не подтверждает одинаковое содержимое; разный hash не сообщает причину; один manifest без условия не доказывает преимущество. Будущий owner может получить материал, который опровергает исходную гипотезу или показывает, что вопросы несопоставимы. Это хороший результат evidence scope, но он не является результатом текущего плана. Current output остаётся передачей необходимого question contract.'),
h2('Почему один diff не образует контроль'),
p('Configuration diff полезен как указатель, но не как контроль эксперимента. Он показывает, что текст параметров отличается, однако не гарантирует одинаковую зависимостную базу, порядок подготовки, окружение runner или состав потребляемого output. Обратное также верно: небольшой diff может скрыть существенную разницу за внешним default либо за изменённой версией зависимости. Поэтому будущему reviewer потребуется не только сравнение файлов, а карта того, откуда каждая существенная настройка пришла и на какую часть процесса могла воздействовать.'),
p('Эта карта не обязана быть огромной. Её ценность в том, что она даёт каждому утверждению адрес: вход относится к source boundary, cache — к состоянию повторного использования, CI — к выполнению workflow, artifact — к выходу. Если адрес отсутствует, утверждение нельзя усилить знаком процента или словом «стабильнее». В P110 намеренно нет таких процентов. Неопределённость остаётся явной, пока будущая работа не свяжет её с конкретным разрешённым evidence.'),
h2('Граница между измерением и интерпретацией'),
p('Даже реальное значение не говорит само за себя. Метод может сообщить длительность выбранного участка, но не объяснить, почему она изменилась; manifest может показать состав, но не указать, какой элемент приемлем для пользователя; cache event может быть записан, но не сообщить, повторится ли он в другом контексте. Будущий документ обязан отделить raw observation от interpretation и перечислить альтернативные объяснения. Этот separation нужен до того, как команда выберет язык итогового сообщения.'),
p('Для апрельского сценария предел ещё строже: отсутствуют и raw observation, и метод. Поэтому нельзя написать даже осторожное «вероятно». Вероятность без population, условий и полученного значения быстро становится скрытым прогнозом. Mechanism article оставляет только структуру будущего вывода: сначала доказать, что стороны отвечают на одно и то же условие, затем прочитать материал, и лишь после этого решить, имеет ли смысл объяснение.'),
h2('Что делать с несопоставимостью'),
p('Несопоставимость не следует маскировать нормализацией, которую выбрали после результата. Если один путь требует другого deployment contract или иной ownership boundary, возможно, это два независимых инженерных решения, а не кандидаты одной гонки. Тогда future owner может подготовить два отдельных decision records либо отказаться от количественного сравнения. Такой выход не означает провал сборки или плана; он сообщает, что первоначальная форма вопроса была слишком широкой.'),
p('Честный stop экономит и время, и доверие. Он не запрещает продолжить исследование, но запрещает превращать накопленные полуфакты в готовую рекомендацию. Именно это делает fixed validator частью редакционной дисциплины: он не знает, как собрать artefact, зато не даёт модели случайно принять отсутствие comparable condition за основание писать о прогрессе.'),
h2('Безопасный literal вместо скрытой методики'),
code("import { createFixedBuildCase, assessFixedBuildPlan } from './upgrade-2027-04.mjs';\n\nconst plan = createFixedBuildCase('missing-comparable-condition-v1');\nconsole.log(assessFixedBuildPlan(plan));\n// { status: 'stop-missing-comparable-condition', productionEffect: 'not-attempted', ... }"),
p('Этот runnable example доказывает только реакцию validator на известный fixed literal. JSON clone отделяет объект caller от шаблона, deep freeze защищает вложенные historicalArtifacts, а JSON-equality не позволяет тихо добавить cache assumption. Fail-closed важен не как стилистика: если условие не названо, система возвращает stop вместо попытки выбрать «разумное» default. Модуль не запускает package manager, build, cache restore, CI job или анализ файлов.'),
h2('Последовательность построения доказательства'),
ol(['Назвать один вопрос, который будет одинаков для обеих сторон, и отметить его как synthetic до authorised scope.', 'Зафиксировать две configuration boundaries без инструментальных версий, flags и файлов, которых нет в evidence.', 'Сформулировать cache states и временную границу как будущие условия, не как уже известную методику.', 'Определить состав artifact и provenance chain, не приписывая manifest или attestation текущему плану.', 'Собрать evidence только после отдельного разрешения и сохранить raw inputs рядом с методом.', 'Разрешить итоговый inference только новому owner; P110 передаёт ему вопрос, а не результат.']),
h2('Полная карта отказов evaluator'),
p('В механизме важен не только accepted branch. Case <code>undated-future-scenario-v1</code> останавливает документ до обсуждения условий; <code>missing-comparable-condition-v1</code> показывает, что отсутствие контрольной рамки не восполняется похожестью; <code>hidden-configuration-or-cache-assumption-v1</code> не допускает молчаливой методики; <code>disallowed-positive-result-v1</code> не пропускает conclusion о пользе. Такой набор не моделирует запуск и не говорит о будущем tool release. Он проверяет, что логическая форма hand-off не превратится в report после одного удачного слова.'),
h2('Ограничения механизма и следующий шаг'),
p('Механизм не заменяет performance budget, security review, reproducibility policy или договорённость команды о стоимости CI. Он не утверждает, что cache полезен, что один bundle важнее другого или что временная граница уже выбрана. Документация webpack и GitHub описывает свои механизмы, но не даёт универсальную метрику для неизвестного приложения. Тем более она не подтверждает факты про апрель 2027.'),
p('Следующий шаг — future evidence owner решает, достаточно ли конкретен comparable condition для отдельной работы. Если нет, честный исход — расширить или закрыть вопрос без запуска. Если да, он создаёт новый dated artefact с provenance, методикой и ограничениями, не переписывая этот draft как retrospective. Именно это разделение удерживает эволюцию сборки от подмены: сначала сравнимость, затем данные, после этого — возможно — вывод.'),
], refs);
const field = revision({ slug: 'editorial-2027-04-field-build-evolution', title: 'Эволюция frontend-сборки: передача синтетического evidence hand-off', categories: ['Frontend', 'Практика команд'], cover: '/assets/editorial/2027/build-evolution-2027-evidence-handoff-loop.svg', excerpt: 'План на апрель 2027: как передать вопрос о сборке следующему владельцу, не превратив план в фиктивный build report.', readingMinutes: 21 }, [
p('Этот field-текст — явный план/сценарий на 2027-04 с source cutoff 2026-07-31. Его проблема не в отсутствии ещё одного dashboard, а в опасной передаче контекста: будущая команда получает фразу «сборка стала лучше», но не получает исходный config, условие CI или состав артефакта. Цена — повторная работа и ложная операционная память. Следующий инженер вынужден либо верить легенде, либо воспроизводить решение с нуля.'),
p('Вторая цена возникает, когда незаполненную карточку называют полевым отчётом. Для P110 не существуют собранные historical config/CI artifacts, build timing, cache hit, bundle size, migration, tool release или результат. Очередь требует уметь мыслить их shape, но не выдумывать content. Поэтому hand-off не способен завершиться report, approval или production change: допустим только <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>.'),
h2('Передавать вопрос вместе с запретами'),
p('Хороший hand-off короче плохого отчёта, потому что не маскирует отсутствующие данные. Он содержит дату, source cutoff, два named synthetic configuration placeholders, named comparable condition и список классов evidence, которые остаются <code>not-collected</code>. Рядом нужны запреты: не считать cache hit известным, не называть artifact маленьким, не объявлять миграцию завершённой. Эти отрицательные условия важны: без них новый владелец может принять форму карточки за заполненный факт.'),
p('Такая передача не перекладывает техническое мышление на следующего человека. Она оставляет ему проверяемую границу решения. Owner видит, что вопрос относится к сравнению configuration, а не к рейтингу инструментов; что CI и cache — потенциальные условия, а не фон; что historical artifacts запрошены, но не получены. Он может выбрать другой путь: уточнить condition, запросить разрешение, отказаться от измерения или доказать, что сравнение не нужно. Никакой из этих вариантов не задним числом делает P110 report.'),
figure('/assets/editorial/2027/build-evolution-2027-evidence-handoff-loop.svg', 'Русская петля передачи evidence: synthetic вопрос проходит дату, две конфигурации и comparable condition, затем либо останавливается при скрытом предположении, либо передаётся future owner без production effect.', 'Петля показывает границы hand-off. Она не изображает реальную CI задачу, config, cache restore, артефакт или итог измерения.'),
table('Минимальная карточка передачи без фальшивого отчёта', ['Поле', 'Допустимое состояние сейчас', 'Кому адресован вопрос', 'Недопустимая подмена'], [
['Дата и cutoff', '2027-04 / 2026-07-31', 'редактору и future owner', 'недатированный сценарий'],
['Стороны', 'named synthetic placeholders', 'владельцу сравнения', 'реальные flags без provenance'],
['Condition', 'named, not-collected', 'автору методики', 'похожая, но другая среда'],
['Исторический evidence', 'config/CI/timing/cache/artifact not-collected', 'разрешённому сборщику данных', 'вымышленный build report'],
['Вывод', 'synthetic-plan-hand-off', 'следующему решающему', 'improved, migrated, released'],
]),
h2('Synthetic shape полезна, если не похожа на данные'),
p('Синтетическая форма должна быть достаточно конкретной, чтобы validator мог отклонить ошибку, и достаточно пустой, чтобы не напоминать секретный production snapshot. Поэтому literal называет classes: <code>config</code>, <code>ci</code>, <code>buildTiming</code>, <code>cacheHit</code>, <code>bundleSize</code>. У каждого значение <code>not-collected</code>. Названия не сообщают формат файла, имя job, ключ cache, число байтов или миллисекунды. Это не неполнота реализации, а сознательная граница редакционного scope.'),
p('Не нужно лечить эту границу более реалистичными placeholders вроде вымышленных runner image, branch или JSON manifest. Они выглядят безобидно, но создают фиктивную provenance: читатель начинает искать их в системе или переносить в настоящий pipeline. Если future owner действительно получит artefact, он обязан зафиксировать его происхождение в новом разрешённом документе. Апрельский текст может дать ему вопрос «какой artefact нужен», но не адрес, имя или содержимое этого artefact.'),
h2('Negative branches экономят время команды'),
p('Fail-closed в hand-off означает, что неполный input не становится допустимым за счёт догадки. Недатированный future scenario, отсутствующий comparable condition, hidden configuration или assumed cache приводят к отдельному stop status. Это не бюрократическая кара. Каждая ветка показывает следующему owner, какую именно неопределённость нельзя перескочить. Она лучше общей фразы «нужно больше данных», потому что не приглашает заполнить пробел интуицией.'),
p('Отдельная ветка защищает от положительного результата. Фраза <code>build-improved</code> остановлена не потому, что улучшение невозможно, а потому, что не существует ни метода, ни данных, ни времени, к которым её можно привязать. В перспективе evidence может показать как выгодный, так и нейтральный или отрицательный исход. Хорошая карточка передаёт право получить любой из этих ответов; плохая заранее требует подтвердить историю про прогресс.'),
h2('Пример не имитирует CI'),
code("import { createFixedBuildCase, assessFixedBuildPlan } from './upgrade-2027-04.mjs';\n\nconst handOff = assessFixedBuildPlan(createFixedBuildCase());\nconsole.log(JSON.stringify({ next: handOff.nextAction, effect: handOff.productionEffect }));\n// {\"next\":\"give-the-named-synthetic-comparison-question-to-a-future-evidence-owner\",\"effect\":\"not-attempted\"}"),
p('Этот example запускается в Node как чистая проверка exports. JSON clone не позволяет одному запуску испортить fixed literal для другого, recursive deep freeze блокирует изменения во вложенных объектах, а evaluator принимает только известные cases. У него нет чтения disk, env, clock, network, CI API, cache storage или build process. Следовательно, строка в консоли не является логом pipeline; она подтверждает лишь то, что документ умеет отказать при неправильной форме hand-off.'),
h2('Кому принадлежит следующий вывод'),
p('Редактор статьи не должен незаметно стать владельцем будущего CI-эксперимента. У него нет ни контекста доступа, ни права выбирать исходные данные, ни возможности договориться о рисках публикации. Поэтому полезно адресовать hand-off не абстрактной «команде», а роли future evidence owner: человеку или группе, которым новый scope явно даст право собрать и интерпретировать материалы. Эта роль может не появиться вовсе, и план не становится от этого ошибочным.'),
p('Передаваемая карточка должна помочь этой роли задать первые вопросы: какой decision она обслуживает, какие данные допустимо читать, где будет храниться raw evidence, кто проверит comparison condition и какой итог считается честным, включая отказ от измерения. Ответы не вписываются заранее в значения literal. Иначе технический долг hand-off превращается в организационный долг: owner формально назначен, но уже связан неподтверждёнными предпосылками.'),
h2('Как не превратить документ в очередь задач'),
p('У подобного сценария есть соблазн закончиться списком операций: «сделать workflow», «настроить cache», «снять bundle», «опубликовать отчёт». В P110 эти глаголы опасны, потому что подразумевают существующие интеграции и согласованные права. Более точный порядок сохраняет форму вопроса: запросить authorisation, определить allowed evidence, сформировать метод, после этого собрать данные. Пока первый шаг не выполнен, следующие остаются не работой, а возможными обязанностями другого scope.'),
p('Это ограничение не снижает практическую ценность field-статьи. Напротив, получатель видит, что ему не нужно верить в прошлый запуск и не требуется защищать заранее обещанное улучшение. Он может начать с проверки того, существует ли вообще decision, ради которого стоит платить за доказательство. Если такого decision нет, лучший hand-off — закрыть сценарий без migration story и сохранить отсутствие production effect как честный итог.'),
h2('Слова, которые стоит остановить до публикации'),
p('Перед передачей полезно вычеркнуть конструкции «получили cache hit», «сократили build», «уменьшили bundle», «перешли на новую сборку» и «подтвердили эффект». В реальном отчёте эти слова могут быть точными при наличии датированных материалов. Здесь они делают из будущего сценария вымышленное прошлое. Безопасные формулировки скучнее: «требуется определить», «не собрано», «нужна provenance», «может быть передано». Но именно они не дают следующему читателю спутать план с данными.'),
p('Тон также важен для практики команды. Не нужно оправдывать отсутствие результата длинными оговорками или обвинять будущий owner в задержке. Достаточно назвать границу и следующее решение. Такой текст не делает неопределённость красивой; он делает её видимой и ограниченной. Если позже появится проверяемый материал, он будет сильнее именно потому, что не вырос из текста, где conclusion уже был объявлен до evidence.'),
h2('Порядок передачи будущему evidence owner'),
ol(['Проверить temporal boundary: только 2027-04 и cutoff 2026-07-31 допускают будущий сценарий.', 'Проверить, что левая и правая стороны названы как synthetic configuration, а не как реальные конфиги.', 'Остановить карточку, если comparable condition не имеет имени или скрыта за словом «аналогично».', 'Оставить каждый класс historical evidence в <code>not-collected</code> до нового authorisation и provenance.', 'Не допускать слова о hit, timing, size, migration или release как о свершившихся событиях.', 'Передать named question следующему owner вместе с причиной, почему productionEffect не пытались получить.']),
h2('Ограничения и следующий шаг'),
p('Полевой hand-off не создаёт ticket, job, access, owner assignment, CI workflow или хранилище артефактов. Он не оценивает, кто будет оплачивать runner, допустим ли доступ к историческому config, можно ли публиковать manifest и нужен ли компании сам эксперимент. Официальные источники помогают не путать caching и provenance с фактами, но не снимают эти организационные вопросы. У текста нет скрытого обещания, что данные станут доступны в апреле 2027.'),
p('Следующий шаг прост и ограничен: будущий owner принимает или отклоняет evidence question в новом scope. При принятии он обязан сформировать отдельный dated record с реальными inputs, методикой, raw artifacts и выводом, включая возможность отсутствия улучшения. При отклонении hand-off остаётся корректным незавершённым планом. В обоих случаях P110 сохраняет полезное: он не превращает красивую историю об эволюции frontend-сборки в ложное свидетельство.'),
], refs);
export const revisions = deepFreeze([practice, mechanism, field]);
export function verifyRevisionsAgainstFixture() {
const fixture = runFixedBuildFixture();
const articleChecks = revisions.map((item) => { const text = bodyText(item.contentHtml); return text.length >= 5000 && text.length <= 15000 && /(цен[аы]|стоимост|потрат|повторн)/i.test(text.slice(0, 1100)) && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && /2027-04/.test(text) && /2026-07-31/.test(text) && /productionEffect: not-attempted/.test(text); });
return deepFreeze({ passed: fixture.passed + articleChecks.filter(Boolean).length, total: fixture.total + articleChecks.length, accepted: fixture.accepted && articleChecks.every(Boolean), fixture, articleChecks, characters: Object.fromEntries(revisions.map((item) => [item.slug, bodyText(item.contentHtml).length])) });
}
if (process.argv.includes('--verify-fixture')) { const result = verifyRevisionsAgainstFixture(); process.stdout.write(JSON.stringify(result, null, 2) + '\n'); if (!result.accepted) process.exitCode = 1; }
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');