revise February 2027 legacy lessons plan articles
Build and deploy / deploy (push) Successful in 16s

This commit is contained in:
2026-07-31 20:18:11 +03:00
parent 32837c91e4
commit 9e3cd52f80
7 changed files with 230 additions and 1 deletions
+151
View File
@@ -0,0 +1,151 @@
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({
rfc2119: { title: 'RFC 2119: Key words for use in RFCs', url: 'https://www.rfc-editor.org/rfc/rfc2119', version: 'BCP 14, March 1997, DOI 10.17487/RFC2119' },
rfc8174: { title: 'RFC 8174: Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words', url: 'https://www.rfc-editor.org/rfc/rfc8174', version: 'BCP 14, May 2017, DOI 10.17487/RFC8174' },
nist160: { title: 'NIST SP 800-160 Vol. 1 Rev. 1: Engineering Trustworthy Secure Systems', url: 'https://doi.org/10.6028/NIST.SP.800-160v1r1', version: 'Revision 1, November 2022, DOI 10.6028/NIST.SP.800-160v1r1' },
});
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_LESSON_CASES = deepFreeze({
'keep-wrap-replace-v1': { id: 'keep-wrap-replace-v1', planDate: '2027-02', sourceCutoff: '2026-07-31', legacyContract: { name: 'unnamed-legacy-contract', state: 'named-synthetic' }, versionBoundary: { name: 'unnamed-version-boundary', state: 'named-synthetic' }, route: ['keep', 'wrap', 'replace'], migrationEvidence: { state: 'not-collected', kind: 'synthetic-input' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Fixed in-memory planning literal. No Bitrix installation, project, version, API call, user, field, file, test, release, database, network, browser, environment, clock, secret, telemetry or production system is read, created or changed.' },
'undated-future-scenario-v1': { id: 'undated-future-scenario-v1', planDate: '', sourceCutoff: '2026-07-31', legacyContract: { name: 'unnamed-legacy-contract', state: 'named-synthetic' }, versionBoundary: { name: 'unnamed-version-boundary', state: 'named-synthetic' }, route: ['keep', 'wrap', 'replace'], migrationEvidence: { state: 'not-collected', kind: 'synthetic-input' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'unnamed-contract-v1': { id: 'unnamed-contract-v1', planDate: '2027-02', sourceCutoff: '2026-07-31', legacyContract: { name: '', state: 'named-synthetic' }, versionBoundary: { name: 'unnamed-version-boundary', state: 'named-synthetic' }, route: ['keep', 'wrap', 'replace'], migrationEvidence: { state: 'not-collected', kind: 'synthetic-input' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'unnamed-version-boundary-v1': { id: 'unnamed-version-boundary-v1', planDate: '2027-02', sourceCutoff: '2026-07-31', legacyContract: { name: 'unnamed-legacy-contract', state: 'named-synthetic' }, versionBoundary: { name: '', state: 'named-synthetic' }, route: ['keep', 'wrap', 'replace'], migrationEvidence: { state: 'not-collected', kind: 'synthetic-input' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'missing-migration-evidence-v1': { id: 'missing-migration-evidence-v1', planDate: '2027-02', sourceCutoff: '2026-07-31', legacyContract: { name: 'unnamed-legacy-contract', state: 'named-synthetic' }, versionBoundary: { name: 'unnamed-version-boundary', state: 'named-synthetic' }, route: ['keep', 'wrap', 'replace'], migrationEvidence: { state: 'missing', kind: 'synthetic-input' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'positive-conclusion-v1': { id: 'positive-conclusion-v1', planDate: '2027-02', sourceCutoff: '2026-07-31', legacyContract: { name: 'unnamed-legacy-contract', state: 'named-synthetic' }, versionBoundary: { name: 'unnamed-version-boundary', state: 'named-synthetic' }, route: ['keep', 'wrap', 'replace'], migrationEvidence: { state: 'not-collected', kind: 'synthetic-input' }, requestedConclusion: 'migration-succeeded', boundary: 'Negative fixed literal only.' },
});
export function createFixedLessonCase(id = 'keep-wrap-replace-v1') { const value = FIXED_LESSON_CASES[id]; return value ? deepFreeze(cloneFixed(value)) : undefined; }
function stop(status, reason, nextAction) { return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' }); }
export function assessFixedLessonPlan(input) {
if (!Object.values(FIXED_LESSON_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-02' || input.sourceCutoff !== '2026-07-31') return stop('stop-undated-future-scenario-or-cutoff', 'february-2027-plan-and-july-2026-cutoff-are-required', 'name-2027-02-and-2026-07-31');
if (!input.legacyContract?.name || input.legacyContract.state !== 'named-synthetic') return stop('stop-unnamed-legacy-contract', 'legacy-contract-must-be-a-named-synthetic-placeholder', 'name-a-placeholder-without-claiming-a-real-contract');
if (!input.versionBoundary?.name || input.versionBoundary.state !== 'named-synthetic') return stop('stop-unnamed-version-boundary', 'version-boundary-must-be-a-named-synthetic-placeholder', 'name-a-placeholder-without-claiming-a-version');
if (JSON.stringify(input.route) !== JSON.stringify(['keep', 'wrap', 'replace'])) return stop('stop-invalid-change-tree', 'the-fixed-route-must-preserve-keep-wrap-replace-order', 'use-the-fixed-planning-tree');
if (input.migrationEvidence?.state !== 'not-collected' || input.migrationEvidence?.kind !== 'synthetic-input') return stop('stop-missing-or-claimed-migration-evidence', 'migration-evidence-is-not-collected-in-this-plan', 'hand-off-an-evidence-question-to-a-future-owner');
if (input.requestedConclusion !== 'synthetic-plan-hand-off') return stop('stop-disallowed-positive-conclusion', 'a-future-plan-cannot-claim-migration-success-or-release', 'use-synthetic-plan-hand-off');
return deepFreeze({ status: 'synthetic-plan-hand-off', caseId: input.id, legacyContract: deepFreeze(cloneFixed(input.legacyContract)), versionBoundary: deepFreeze(cloneFixed(input.versionBoundary)), route: deepFreeze(cloneFixed(input.route)), migrationEvidence: deepFreeze(cloneFixed(input.migrationEvidence)), productionEffect: 'not-attempted', nextAction: 'give-the-fixed-synthetic-contract-to-a-future-evidence-owner' });
}
export function runFixedLessonFixture() {
const expected = [['keep-wrap-replace-v1', 'synthetic-plan-hand-off'], ['undated-future-scenario-v1', 'stop-undated-future-scenario-or-cutoff'], ['unnamed-contract-v1', 'stop-unnamed-legacy-contract'], ['unnamed-version-boundary-v1', 'stop-unnamed-version-boundary'], ['missing-migration-evidence-v1', 'stop-missing-or-claimed-migration-evidence'], ['positive-conclusion-v1', 'stop-disallowed-positive-conclusion']];
const checks = expected.map(([id, expectedStatus]) => ({ id, expected: expectedStatus, actual: assessFixedLessonPlan(createFixedLessonCase(id)).status }));
const sample = createFixedLessonCase();
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.route), 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 practice = revision({ slug: 'editorial-2027-02-practice-bitrix-lessons', title: 'Уроки Bitrix для legacy-разработки: сохранить, обернуть, заменить', categories: ['Bitrix', 'Legacy', 'Практика'], cover: '/assets/editorial/2027/bitrix-lessons-2027-keep-wrap-replace-tree.svg', excerpt: 'План на февраль 2027: дерево решений для legacy-контракта без выдуманного проекта или вызова API.', readingMinutes: 22 }, [
p('Это план/сценарий на 2027-02 с source cutoff 2026-07-31, а не рассказ о сделанной миграции Bitrix. Первая проблема legacy-команды проста: знакомый участок кода объявляют «старым» и заменяют до того, как названо правило, ради которого он живёт. Цена — потерянное неявное соглашение, повторная работа и спор уже после изменения, когда вернуть контекст дороже, чем остановиться раньше.'),
p('Вторая проблема — язык уверенности. Фразы «в этой CMS так работает» и «после обновления будет то же самое» маскируют отсутствие версии, контракта и evidence. Цена вымысла выше косметической: читатель получает ложную память о проекте, тесте или релизе. Поэтому здесь нет существующего Bitrix-проекта, API-вызова, пользователя, поля, migration test или будущего результата. Допустим только <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>.'),
h2('Не переписывать ярлык вместо правила'),
p('Название платформы не заменяет карту поведения. Под «Bitrix legacy» в этом материале понимается только учебная рамка: код, границы и договорённости могут пережить автора. Она не сообщает, какие модули установлены, как устроен конкретный шаблон, какие методы существуют и как они ведут себя. Практическая польза начинается не с поисков универсального рецепта, а с вопроса: что именно обязан сохранить следующий шаг, если подробности контракта ещё не подтверждены?'),
p('Для такого вопроса полезно дерево из трёх глаголов. <code>keep</code> означает оставить участок без заявления, что он правильный навсегда. <code>wrap</code> означает поставить вокруг него собственную границу наблюдения или адаптации, не приписывая старому коду новый смысл. <code>replace</code> означает лишь будущую кандидатуру замены после отдельного разрешённого discovery. Порядок важен: замена не становится исходной точкой только потому, что старый код неприятен.'),
figure('/assets/editorial/2027/bitrix-lessons-2027-keep-wrap-replace-tree.svg', 'Дерево планового решения: назвать synthetic legacy-контракт, проверить границу версии, затем сохранить, обернуть или передать кандидатуру замены; красные ветки останавливают недатированный сценарий и чрезмерный вывод.', 'Схема — редакционная модель будущего сценария на 2027-02. Она не описывает поведение конкретной установки Bitrix и не объявляет миграцию выполненной.'),
table('Дерево без предположения о реальной системе', ['Ветвь', 'Допустимое действие в плане', 'Сохраняемая неопределённость', 'Запрещённый вывод'], [['Сохранить', 'зафиксировать вопрос у named synthetic contract', 'правило пока не проверено', 'код уже одобрен'], ['Обернуть', 'описать будущий adapter boundary', 'внутренности legacy неизвестны', 'вызов API существует'], ['Заменить', 'передать кандидатуру будущему owner', 'версия и миграционный путь не подтверждены', 'релиз или тест состоялся'], ['Остановить', 'вернуть stop-status', 'evidence не собран', 'проблемы нет']]),
h2('Что значит сохранить'),
p('Сохранение часто ошибочно называют бездействием. В зрелой разработке это контролируемое решение не менять слой, пока у изменения нет адреса. Оно требует больше дисциплины, чем механическое переписывание: надо назвать область неизвестности, не превратить её в баг и оставить получателю честный next step. В сценарии область называется <code>unnamed-legacy-contract</code>. Это не имя файла, не сущность Bitrix и не скрытая ссылка на реальный договор; это literal, который не разрешает добавить правдоподобные детали.'),
p('Зрелость здесь не равна терпимости к долгу. Если участок действительно мешает, его нельзя оправдывать словом legacy. Но и давление на срок не превращает догадку в доказательство. Сохранить означает признать: на дату cutoff нет утверждённого описания входов, выходов, владельца и допустимого изменения. Такая запись не закрывает задачу сопровождения; она не даёт закрыть её ложным позитивным результатом.'),
h2('Обернуть — отделить свой код от чужой неизвестности'),
p('Обёртка имеет смысл только как новая, узкая граница ответственности. Она не должна симулировать точное знание внутреннего API. В будущем authorized scope может решить, что нужны преобразование формата, проверка ошибки или изоляция зависимого места. В текущем сценарии этого решения нет. Поэтому слово <code>wrap</code> фиксирует форму разговора: будущее изменение обязано назвать наблюдаемый контракт и версию, а не скрыть предположение в удобном helper.'),
p('У обёртки есть цена. Она добавляет поверхность поддержки, новую точку расхождения и соблазн объявить старую часть безопасной лишь потому, что рядом появился современный интерфейс. Если boundary не назван, такая прослойка увеличивает долг. Именно поэтому fixture закрывает <code>unnamed-version-boundary-v1</code>: неизвестная граница версии не может быть украшена универсальной адаптацией. Fail closed здесь полезнее умной догадки.'),
h2('Заменить — не значит уже мигрировать'),
p('Замена начинается с перечисления того, чего мы не знаем. Нет migration evidence, значит нет основания писать «эквивалентно», «проверено» или «работает после переноса». Нет подтверждённой версии, значит нельзя обещать совместимость. Нет конкретного API-вызова, значит нельзя подменять модель псевдокодом, который выглядит как инструкция для реального проекта. Практический материал обязан быть исполнимым, но его исполнимость может ограничиваться безопасной проверкой литерала.'),
code("import { createFixedLessonCase, assessFixedLessonPlan } from './upgrade-2027-02.mjs';\n\nconst preservedQuestion = createFixedLessonCase('keep-wrap-replace-v1');\nconst handOff = assessFixedLessonPlan(preservedQuestion);\nconsole.log(JSON.stringify({ route: handOff.route, status: handOff.status, effect: handOff.productionEffect }));\n// {\"route\":[\"keep\",\"wrap\",\"replace\"],\"status\":\"synthetic-plan-hand-off\",\"effect\":\"not-attempted\"}"),
p('Пример буквально запускается только с exports этого модуля. Factory создаёт JSON clone fixed in-memory literal и рекурсивно применяет deep freeze; caller не получает общую изменяемую ссылку. Evaluator сверяет вход с известными named literals и закрывает произвольный object. В нём нет файлов, сети, browser, environment, часов, secrets, telemetry и внешних инструментов. Он не выполняет Bitrix-вызов и не проводит migration test; его output — только безопасная передача вопроса.'),
h2('Цена каждого преждевременного шага'),
p('У дерева есть не только техническая, но и экономическая логика. Ранняя замена переносит цену неизвестности в review, поддержку и откат: новый слой приходится объяснять одновременно с тем, что он пытается изменить. Раннее оборачивание обычно дешевле замены, но тоже не бесплатно — появляются преобразование, новая диагностика и отдельная ответственность за ошибки. Сохранение дешевле прямо сейчас, но становится дорогим, если команда не оставила понятный вопрос. Поэтому выбор нельзя сводить к «ничего не делать» против «сделать современно». Он сравнивает разные формы обязательств.'),
p('В учебной модели нет чисел, потому что числа без контекста были бы ещё одним вымыслом. Нет оценки часов, количества экранов, размера базы, числа интеграций или доли пользователей. Можно зафиксировать только направление риска: чем больше неподтверждённых предпосылок помещено в изменение, тем труднее отделить дефект нового кода от старого правила. Эта формулировка не предсказывает результат. Она объясняет, почему будущему owner передают список неизвестных, а не уверенное решение.'),
h2('Как не превратить обёртку в новый монолит'),
p('Будущая обёртка должна иметь маленький контракт и явное место удаления. Иначе она быстро превращается в комнату, куда складывают исключения: входы исправляются наугад, ошибки переименовываются, а вызывающий код перестаёт видеть исходную границу. В P108 запрещено придумывать такую реализацию, но разрешено назвать критерий для будущей проверки: у прослойки должен быть свой наблюдаемый input, свой output и основание существования. Если хотя бы один пункт нельзя назвать, решение остаётся на ветке keep.'),
p('Важно не путать изоляцию с сокрытием. Изоляция оставляет возможность спросить, какой контракт защищается и при каких версиях. Сокрытие делает старое поведение невидимым и заставляет следующий слой компенсировать симптомы. Первый вариант облегчает проверку, второй просто меняет место долга. Поэтому дерево не предлагает «обернуть всё» как безопасный default. Оно требует пройти через named placeholders, чтобы даже отсутствие знания было видно в интерфейсе решения.'),
h2('Порядок действий для будущего владельца'),
ol(['Сохранить редакционные метки <code>planDate: 2027-02</code> и <code>sourceCutoff: 2026-07-31</code>; без них сценарий закрывается.', 'Назвать только synthetic placeholder legacy-контракта, не превращая его в метод, проект, поле или пользовательский кейс.', 'Отдельно назвать synthetic version boundary; неизвестная версия — причина остановки, а не повод выбрать «наиболее вероятную».', 'Пройти дерево <code>keep → wrap → replace</code> как порядок рассмотрения, не как план работ или очередь.', 'Оставить migration evidence в состоянии <code>not-collected</code>; не заменять отсутствие test фиктивным положительным словом.', 'Передать вопрос будущему evidence owner, который отдельно решит, нужен ли authorized discovery и какие факты допустимы.']),
h2('Где дерево заканчивается'),
p('Это дерево не выбирает архитектуру, не измеряет производительность, не оценивает безопасность и не знает условий эксплуатации. Оно не говорит, что legacy-код надо оставить, что adapter нужен или что замена выгодна. Также оно не покрывает реальную совместимость версий Bitrix: у текста нет подтверждённой версии и нет права превращать архивную документацию в текущий факт. Модель намеренно меньше проекта, чтобы не создать фиктивный проект в редакционной статье.'),
p('Следующий шаг возможен только вне этого draft: будущий владелец может открыть отдельный scope, получить разрешение на discovery и зафиксировать provenance реальных материалов. До этого момента честный итог — hand-off. Февраль 2027 ещё не наступил относительно редакторской даты; обещать результат миграции означало бы выдать план за историю. Legacy не оправдывает бездействие, но требует, чтобы действие начиналось с названной границы, а не с красивого пересказа неизвестности.')
], [{ key: 'rfc2119', use: 'Использована только дисциплина явной модальности слов MUST/SHOULD в техническом контракте.', boundary: 'Не описывает Bitrix, migration или реальный API.' }, { key: 'rfc8174', use: 'Использовано уточнение, что модальные слова имеют заданный контекст.', boundary: 'Не подтверждает версию, тест или будущий результат.' }, { key: 'nist160', use: 'Использована общая инженерная граница между системой, контрактом и evidence.', boundary: 'Не подтверждает существование проекта или его поведения.' }]);
const mechanism = revision({ slug: 'editorial-2027-02-mechanism-bitrix-lessons', title: 'Уроки Bitrix для legacy-разработки: граница версии за вызовом', categories: ['Bitrix', 'Legacy', 'Архитектура'], cover: '/assets/editorial/2027/bitrix-lessons-2027-version-boundary-matrix.svg', excerpt: 'План на февраль 2027: как не принять имя API за инвариант неизвестной версии.', readingMinutes: 21 }, [
p('План/сценарий на 2027-02 с source cutoff 2026-07-31 разбирает дорогую ошибку механизма: имя вызова принимают за гарантию поведения. В legacy-разработке это быстро превращает догадку в интерфейс нового кода. Цена — адаптер вокруг ложного инварианта, спор о «совместимости» и новый долг, который уже труднее увидеть, потому что он написан современным синтаксисом.'),
p('Следующая ловушка — приписать этот механизм реальному Bitrix. У данного текста нет установки, проекта, версии, API-вызова, релиза, поля, пользователя или теста. Цена такой конкретизации — вымышленный факт, который могут использовать как инструкцию. Поэтому все имена ниже — synthetic placeholders; положительного вывода о миграции нет. Разрешён только <code>synthetic-plan-hand-off</code> с неизменным <code>productionEffect: not-attempted</code>.'),
h2('Вызов виден, инвариант спрятан'),
p('Вызов — это форма обращения к границе. Инвариант — свойство, которое следующий код рассчитывает сохранить: порядок, отсутствие, преобразование, ошибка, идемпотентность или другой явно названный эффект. Эти сущности нельзя склеивать. Один и тот же внешний вид строки кода может на разных границах означать разные обязательства, а похожие вызовы могут не иметь общего контракта. Мы не можем выяснить это по названию и не должны подменять проверку привычкой.'),
p('Чтобы не создавать фальшивую точность, модель использует <code>unnamed-version-boundary</code>. Это не «неизвестная версия Bitrix», а явная synthetic метка отсутствующего знания. Рядом стоит <code>unnamed-legacy-contract</code>, также не связанный с API. Пара нужна, чтобы удержать вопрос в двух плоскостях: что обещает контракт и при каком version boundary это вообще можно обсуждать. Если отсутствует любая из них, evaluator останавливает сценарий.'),
figure('/assets/editorial/2027/bitrix-lessons-2027-version-boundary-matrix.svg', 'Матрица плановой границы версии: имя вызова, named synthetic contract, named synthetic version boundary и допустимый вывод; красные ячейки показывают stop при отсутствующем контракте или версии.', 'Матрица не сопоставляет настоящие версии Bitrix и не документирует API. Она показывает, почему форма вызова не доказывает инвариант.'),
table('Четыре уровня утверждения', ['Уровень', 'Что можно назвать в этом draft', 'Что требуется для усиления', 'Что запрещено'], [['Лексема', 'synthetic имя boundary', 'отдельный источник и scope', 'реальный метод'], ['Контракт', 'placeholder без поведения', 'проверяемое описание input/output', 'обещание совместимости'], ['Версия', 'placeholder без номера', 'подтверждённый provenance', 'название релиза'], ['Вывод', 'hand-off вопроса', 'реальное разрешённое evidence', 'migration succeeded']]),
h2('Почему версия — не число в комментарии'),
p('Граница версии — не декоративное поле. Она определяет, к чему относится утверждение: к документации, развертыванию, интеграции или предположению автора. Пока нет provenance, номер версии опаснее пустоты: он создаёт ощущение проверяемости и направляет людей к несуществующему релизу. В данном будущем сценарии даже правдоподобный номер был бы выдумкой. Поэтому model требует имя placeholder, но запрещает превращать его в факт.'),
p('У этой строгости есть практическая цена сейчас: документ получается менее эффектным, а следующий инженер не получает готового рецепта. Это приемлемая цена по сравнению с обратной стоимостью ложной совместимости. Legacy не получает иммунитет от ответственности; наоборот, зрелая система требует различать «не знаем» и «можем безопасно предположить». Здесь безопасного предположения нет, поэтому evaluator fail closed.'),
h2('Инвариант нужно формулировать без магии API'),
p('Полезный инвариант начинается не с объекта библиотеки, а с наблюдаемого условия: что вход считается допустимым, что выход считается тем же, где ошибка остаётся ошибкой и кто вправе трактовать результат. Но и это условие нельзя сочинить за чужую систему. В P108 условие нарочно не заполнено; named literal показывает место, в которое будущий owner может положить отдельный, подтверждённый контракт. До того никто не получает право назвать конкретное поведение «стандартным для Bitrix».'),
p('Такой пробел не надо маскировать псевдокодом. Особенно вредны примеры, которые выглядят как настоящий API-вызов: они переживают оговорку и начинают жить в копипасте. Исполнимый пример ниже проверяет только дисциплину модели. Он показывает, что отсутствие version boundary останавливает передачу, а не подталкивает систему к запасной ветке. Это меньшая демонстрация, но она честна относительно source cutoff.'),
h2('Исполнимая проверка границы'),
code("import { createFixedLessonCase, assessFixedLessonPlan } from './upgrade-2027-02.mjs';\n\nconst boundaryIsMissing = createFixedLessonCase('unnamed-version-boundary-v1');\nconst gate = assessFixedLessonPlan(boundaryIsMissing);\nconsole.log([gate.status, gate.productionEffect, gate.nextAction].join(' | '));\n// stop-unnamed-version-boundary | not-attempted | name-a-placeholder-without-claiming-a-version"),
p('Этот runnable fragment обращается только к fixed in-memory literal. В factory сначала происходит JSON clone, затем deep freeze всех вложенных значений, поэтому нельзя тайно дописать версию после выдачи case. Оценка принимает только known cases и возвращает stop для неназванной границы. Нет request, файлового fixture, process environment, времени, browser, API, базы или telemetry. Вывод не описывает реальный failure; он лишь охраняет границу редакционной модели.'),
h2('Как псевдоинвариант попадает в код'),
p('Обычно ложный инвариант начинается с мелочи: в обсуждении кто-то говорит «этот вызов всегда возвращает нужную форму», а через неделю фраза становится условием в adapter. Потом вокруг условия появляются обработка ошибки, кеш или fallback, и уже кажется, будто договор подтверждён самим количеством кода. На деле объём не заменяет provenance. Чем удобнее ложное правило для реализации, тем настойчивее надо требовать источник, version boundary и альтернативу, при которой правило не действует.'),
p('У этого механизма есть коварная особенность: он может пережить смену людей. Новый инженер видит хорошо названную функцию и считает, что имя отражает установленный факт. Поэтому хороший контракт не прячет неизвестность за неймингом. В плановом literal слово <code>unnamed</code> намеренно неприятно: оно не позволяет случайно принять placeholder за доменное понятие. Когда future owner получит реальные основания, он создаст новый артефакт с отдельной датой, а не переименует задним числом эту метку.'),
h2('Совместимость — составное утверждение'),
p('Говорить о совместимости можно только после разложения вопроса. Совпадает ли формат входа? Сохраняется ли смысл отсутствующего значения? Одинаково ли обрабатывается ошибка? Есть ли различие в порядке побочных действий? В настоящем проекте эти вопросы требуют конкретных материалов. В P108 их нельзя заполнить, но можно не потерять структуру. Матрица удерживает составные части рядом и не позволяет одной знакомой лексеме выдать себя за весь набор гарантий.'),
p('Именно поэтому table не содержит колонку «поддерживается». Поддержка — вывод о реальном сочетании версии, конфигурации и применения, которого здесь нет. Проставить зелёную галочку означало бы совершить тот же логический скачок, от которого материал защищает. Зелёный в схеме означает только допустимость hand-off формы вопроса. Он не означает работоспособность, vendor commitment или возможность переноса. Это различие нужно проговорить явно, потому что цвет и таблица слишком легко читаются как verdict.'),
h2('Кому полезен stop-status'),
p('Stop нужен не только ревьюеру. Автору изменения он экономит время на документ, который иначе пришлось бы переписывать после первого уточнения. Получателю он даёт право не соглашаться с исходной постановкой: можно вернуть карточку, если граница названа слишком широко, а evidence не позволяет выбрать безопасный следующий шаг. Руководителю stop показывает, что работа не исчезла, но ещё не получила право называться реализацией. Такая прозрачность лучше оптимистичного статуса, которому никто не может показать основание.'),
p('Fail closed также предотвращает тихую эскалацию scope. Когда evaluator принимает только byte-identical fixed cases, нельзя передать произвольный object со скрытым API-именем, реальной версией или положительным claim. Ограничение намеренно грубое: оно не моделирует домен, а охраняет редакционную границу. В будущем более богатая модель возможна, но она должна появиться вместе с доказуемым источником, владельцем и правилами её использования. До этого простая остановка честнее универсальной функции.'),
h2('Как разбирать изменение за API-вызовом'),
ol(['Отделить текст вызова от утверждения о его эффекте; в этом draft не считать ни одно имя вызова реальным.', 'Записать legacy contract как synthetic placeholder, пока отдельный владелец не подтвердит вход, выход и ошибку.', 'Записать version boundary отдельным placeholder; не вставлять номер, релиз или ссылку на непроверяемый архив.', 'Проверить, что change tree сохраняет порядок <code>keep → wrap → replace</code>, а не перескакивает к миграции.', 'Отклонить case с отсутствующим contract, version boundary или migration evidence, даже если остальная история выглядит убедительно.', 'Передать только вопрос и его stop-conditions будущему evidence owner; не создавать test, project task или production action.']),
h2('Матрица не заменяет исследование'),
p('Матрица полезна тем, что делает видимыми пропуски, но она не добывает данные. Она не отвечает, существует ли версия, повторяется ли ошибка, вызовет ли переход регрессию или какой слой принадлежит конкретной команде. Она также не устанавливает ценность обёртки: одна и та же прослойка может быть защитой или лишним посредником, если контракт не доказан. Любая попытка назначить ответ по одной таблице была бы тем самым неверным инвариантом.'),
p('Следующий шаг — отдельный authorized discovery после будущей даты сценария: получатель сам решает, нужны ли документы, исходный код, изолированная проверка или иной evidence. Если такого scope нет, stop — корректный конечный результат данного материала. Это не оправдание бездействия; это запрет на действие под видом знания. Февраль 2027 не наступил, и редакторская модель не может рассказывать о его совместимости, релизе или выполненной миграции.')
], [{ key: 'rfc2119', use: 'Использована только идея точно ограниченной нормативной лексики.', boundary: 'Не задаёт поведение API и не доказывает инвариант Bitrix.' }, { key: 'rfc8174', use: 'Использована граница контекста, в котором модальные слова приобретают смысл.', boundary: 'Не подтверждает номер версии или релиз.' }, { key: 'nist160', use: 'Использовано общее различение инженерных границ и свидетельств.', boundary: 'Не является источником migration test или проекта.' }]);
const field = revision({ slug: 'editorial-2027-02-field-bitrix-lessons', title: 'Уроки Bitrix для legacy-разработки: evidence hand-off без отчёта', categories: ['Bitrix', 'Legacy', 'Надёжность'], cover: '/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg', excerpt: 'План на февраль 2027: передать вопрос о миграционном evidence, не выдумывая тест, релиз или успех.', readingMinutes: 20 }, [
p('Редакторская дата — 31.07.2026; плановый номер обозначен 2027-02, то есть февраль 2027, а источники ограничены cutoff 2026-07-31. В field-работе самая дорогая ошибка начинается с фразы «миграция подтверждена», когда у передачи нет ни provenance, ни материалов, ни даже утверждённого теста. Цена — поиск несуществующего evidence, неверные решения и операционная память, которую придётся разбирать уже после того, как на неё оперлись.'),
p('Особенно опасно делать такую фразу правдоподобной деталями про Bitrix: назвать проект, пользователя, поле, API-вызов, выпуск или migration test. Ничего этого здесь не существует и не предполагается. Февраль 2027 ещё впереди; положительный outcome запрещён. Модель заканчивается только <code>synthetic-plan-hand-off</code> и всегда сообщает <code>productionEffect: not-attempted</code>, а не результат работы в production.'),
h2('Evidence начинается с происхождения, не с вывода'),
p('Evidence — это не красивое существительное для уверенной фразы. Чтобы материал мог поддержать решение, нужны источник, дата, условия получения, граница интерпретации и владелец. В synthetic сценарии этих вещей нет, поэтому состояние названо <code>not-collected</code>. Это не плохое значение и не намёк на скрытый файл: оно означает, что статья передаёт вопрос, а не артефакт. Любое усиление до «наблюдалось» сделало бы модель ложной.'),
p('Legacy требует такой же честности, как новая система. Старый слой может хранить много неявных правил, но его возраст не даёт разрешения дорисовывать результат. Если будущее изменение действительно понадобится, evidence должен появиться в отдельном authorized scope с собственными границами и датой. Нельзя ретроспективно переписать этот draft после появления новых данных: тогда получится другой артефакт, а не уточнение исходного hand-off.'),
figure('/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg', 'Петля плановой передачи evidence: fixed synthetic question ведёт к будущему владельцу, который либо открывает отдельный scope, либо возвращает отсутствие evidence; красная ветка блокирует claim о готовой миграции.', 'Схема показывает только маршрутизацию неопределённости. Она не утверждает наличие миграционного теста, результата, релиза либо доказательств для Bitrix.'),
table('Что передаётся и чего в пакете нет', ['Элемент', 'Состояние P108', 'Кому принадлежит решение', 'Недопустимая подмена'], [['Вопрос', 'named synthetic literal', 'future evidence owner', 'готовый incident'], ['Контракт', 'placeholder', 'отдельный scope', 'реальное поле или API'], ['Migration evidence', 'not-collected', 'авторизованный процесс', 'пройденный test'], ['Вывод', 'synthetic-plan-hand-off', 'будущая оценка', 'success/release']]),
h2('Пустота evidence должна быть видна'),
p('Иногда команда скрывает отсутствие материалов за термином «полевая проверка». Это создаёт опасный мост между планом и утверждением: читатель видит familiar labels и предполагает, что кто-то запускал систему. В P108 поле намеренно пустое в фактическом смысле. Есть только fixed literal, который показывает форму будущей передачи. У него нет имени окружения, ссылки на задачу, записи лога, снимка экрана, тестового пользователя или вызова внешнего сервиса.'),
p('Такая пустота не отменяет стоимость проблемы. Напротив, она предотвращает дорогой вторичный дефект: решение принимают на основании artefact, который невозможно найти. Прагматичный hand-off говорит получателю меньше, но говорит надёжно: что неизвестно, какая граница обязана быть названа и какой результат сейчас запрещён. Это лучше, чем «отчёт» с реалистичной терминологией и несуществующей ответственностью.'),
h2('Negative case важнее удобного успеха'),
p('Fixture содержит <code>missing-migration-evidence-v1</code>. Он не пытается вылечить отсутствие evidence резервным значением и не делает вид, что отсутствие равно чистому результату. Вместо этого возвращается <code>stop-missing-or-claimed-migration-evidence</code>. Такая ветка проверяет редакционную честность: если author позже добавит слово collected или positive conclusion, модуль не превратит это в hand-off по умолчанию.'),
p('Отрицательная ветка защищает и от другого соблазна — выставить migration succeeded как желаемый статус. Желаемое не равно наблюдаемому, а план не равен релизу. В будущем у владельца могут быть основания исследовать проблему; в настоящей модели их нет. Fail closed не решает, нужна ли миграция. Он не даёт использовать этот текст как свидетельство того, что миграция была начата, проведена или завершена.'),
h2('Безопасный hand-off в памяти'),
code("import { createFixedLessonCase, assessFixedLessonPlan } from './upgrade-2027-02.mjs';\n\nconst evidenceWasNotProvided = createFixedLessonCase('missing-migration-evidence-v1');\nconst refusal = assessFixedLessonPlan(evidenceWasNotProvided);\nconsole.log({ refusal: refusal.status, action: refusal.nextAction, production: refusal.productionEffect });\n// { refusal: 'stop-missing-or-claimed-migration-evidence', action: 'hand-off-an-evidence-question-to-a-future-owner', production: 'not-attempted' }"),
p('Этот code example runnable, но его предмет узок: known literal клонируется через JSON и замораживается deep freeze до оценки. Никакой fixture не читается с диска; никакой веб-запрос, тестовый runner, Bitrix API, браузер, переменная окружения, часы или секрет не используются. Публичный export позволяет проверить stop-status, но не создаёт migration evidence. В частности, он не называет API, проект, пользователя или реальное поле, потому что этого knowledge у сценария нет.'),
h2('Почему test name не является evidence'),
p('Название теста может быть полезным указателем, но само по себе не говорит, что именно запускалось, на чём, с какими входами и кто интерпретировал результат. В будущей миграции это различие особенно важно: одинаковая метка может скрывать разный набор правил, а зелёный сигнал — быть следствием неполного охвата. Поэтому P108 не создаёт даже synthetic migration test. Создать его было бы проще для повествования, но читатель неизбежно начал бы спрашивать о фикстурах, окружении и результате, которых не существует.'),
p('Вместо test name hand-off несёт более скромную, но проверяемую информацию: evidence не collected, его сила не повышалась, а утверждение об успехе запрещено. Эта структура позволяет будущему owner начать с правильного вопроса — нужен ли вообще evidence для выбранного решения и какому уровню доверия он должен соответствовать. Она не подсказывает ответ заранее и не создаёт обязательства обязательно запускать проверку. Иногда корректным выходом discovery будет отказ от изменения, а не доказательство миграции.'),
h2('Провенанс нельзя дописать позже'),
p('После source cutoff могут появиться документы, снимки, реальные тесты и результаты. Они не делают исходный draft ретроспективным отчётом. У новых материалов будет своя дата, доступ, условия и автор; смешать их со старым сценарием значит стереть разницу между планированием и наблюдением. Такая подмена кажется безобидной, пока не возникает вопрос, какие именно факты были известны до решения. Тогда аккуратно сохраненная пустота становится важнее подробной, но переписанной истории.'),
p('Провенанс важен и когда outcome отрицательный. Будущий owner может обнаружить, что доказательства недоступны, контракт слишком широкий или сама постановка ошибочна. Это не делает hand-off неудачным: его задача не добиться красивого направления, а сохранить возможность честно выбрать направление позже. Поэтому <code>productionEffect: not-attempted</code> — не декоративный флаг. Он не разрешает читать его как «ничего не произошло в реальном мире»; он говорит лишь, что данный модуль не пытался воздействовать на production.'),
h2('Синтетический пакет не становится очередью'),
p('Даже хорошо оформленный hand-off способен незаметно стать обязательством, если в нём появляется исполнитель, срок или обещанный результат. В этом материале их нет. Future evidence owner — роль, а не человек; next action — передача вопроса, а не команда получить данные; маршрут на схеме — объяснение границы, а не workflow существующей организации. Такое ограничение сохраняет автономию получателя и не выдаёт редакторский draft за управленческое решение.'),
p('Если получателю нужно действие, он должен завести его в новом авторизованном контексте с собственными данными и рисками. P108 не может сделать это вместо него, как не может создать совместимость одной строкой. Сила пакета в другом: он не оставляет лазейки для утверждения «мы уже проверили». Когда отсутствует migration evidence, честная остановка — полезный результат подготовки, а не повод сочинить positive outcome, чтобы текст выглядел законченным.'),
h2('Порядок передачи вместо псевдоотчёта'),
ol(['Сохранить в hand-off дату будущего плана и source cutoff, чтобы не спутать его с фактической работой.', 'Оставить legacy contract и version boundary synthetic placeholders, не дополняя их знакомыми, но неподтверждёнными именами.', 'Зафиксировать migration evidence только как <code>not-collected/synthetic-input</code>; отсутствие не трактовать как успех.', 'Запустить fixture против negative literal и убедиться, что он выдаёт stop, а не fallback.', 'Не создавать ticket, очередь, release note, test report или production change из результата этой функции.', 'Передать будущему owner вопрос, ограничения и next action; разрешение на получение реальных материалов остаётся вне P108.']),
h2('Ограничения и следующий шаг'),
p('Эта статья не является планом миграции, политикой доказательств или инструкцией для конкретной CMS. Она не устанавливает, какие тесты обязательны, где хранить логи, какие данные безопасны и кто владеет legacy-системой. Официальные источники ниже помогают не размывать модальность и engineering boundary, но не добавляют недостающие факты. Непроверяемая архивная документация Bitrix не используется как факт именно потому, что version boundary не подтверждён.'),
p('Следующий шаг — только future owner с отдельным полномочием: он может признать hand-off достаточным, открыть discovery либо запросить иной контракт. Пока такого решения нет, безопасный результат уже достигнут в узком смысле: сформирован synthetic plan hand-off, а production effect не предпринимался. Это не положительный вывод о системе. Это отказ делать вид, что у будущего февраля 2027 уже есть тест, релиз и доказанный итог.')
], [{ key: 'rfc2119', use: 'Использована только точность требований и запретов в тексте передачи.', boundary: 'Не превращает hand-off в выполненную миграцию.' }, { key: 'rfc8174', use: 'Использовано различение контекста нормативного утверждения.', boundary: 'Не подтверждает evidence, API или тест.' }, { key: 'nist160', use: 'Использована общая дисциплина инженерного evidence и границ.', boundary: 'Не описывает реальный Bitrix-проект или release.' }]);
export const revisions = deepFreeze([practice, mechanism, field]);
export function verifyRevisionsAgainstFixture() { const fixture = runFixedLessonFixture(); const articleChecks = revisions.map((item) => { const text = bodyText(item.contentHtml); return text.length >= 5000 && text.length <= 15000 && /(цен[аы]|стоимост|издержк|потер)/i.test(text.slice(0, 1000)) && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && /2027-02/.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');