rewrite 2026-09 and 2027 articles for reader-facing quality
Build and deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-07-31 22:26:56 +03:00
parent 3bfc3f21c4
commit 440c8721dc
69 changed files with 4424 additions and 3533 deletions
+255 -142
View File
@@ -1,159 +1,272 @@
function escapeHtml(value) { return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;'); }
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 figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + escapeHtml(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({
d112: { title: 'D Programming Language — Change Log 2.112.0', url: 'https://dlang.org/changelog/2.112.0.html', version: 'D 2.112.0, released 07 January 2026' },
d111: { title: 'D Programming Language — Change Log 2.111.0', url: 'https://dlang.org/changelog/2.111.0.html', version: 'D 2.111.0, released 01 April 2025' },
d110: { title: 'D Programming Language — Change Log 2.110.0', url: 'https://dlang.org/changelog/2.110.0.html', version: 'D 2.110.0, released 07 March 2025' },
});
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> — pinned release material: ' + escapeHtml(ref.version) + '. ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>'; }).join('') + '</ul>'; }
const FIXED_RUNTIME_CASES = deepFreeze({
'planned-runtime-hand-off-v1': { id: 'planned-runtime-hand-off-v1', planDate: '2027-03', sourceCutoff: '2026-07-31', workload: { id: 'named-synthetic-request-shape', statement: 'question-only-no-service' }, constraints: [{ id: 'named-latency-budget-question', role: 'planned-constraint' }, { id: 'named-ownership-boundary-question', role: 'planned-constraint' }], benchmark: { state: 'not-run', configuration: 'not-collected', result: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Fixed in-memory planning literal. No runtime selection, compiler execution, benchmark, profile, service scenario, repository, file, environment, clock, network, secret, telemetry, production system or customer data is read, created or changed.' },
'undated-scenario-v1': { id: 'undated-scenario-v1', planDate: '', sourceCutoff: '2026-07-31', workload: { id: 'named-synthetic-request-shape', statement: 'question-only-no-service' }, constraints: [{ id: 'named-latency-budget-question', role: 'planned-constraint' }], benchmark: { state: 'not-run', configuration: 'not-collected', result: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'unnamed-workload-or-constraint-v1': { id: 'unnamed-workload-or-constraint-v1', planDate: '2027-03', sourceCutoff: '2026-07-31', workload: { id: '', statement: '' }, constraints: [{ id: '', role: 'planned-constraint' }], benchmark: { state: 'not-run', configuration: 'not-collected', result: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'hidden-benchmark-configuration-v1': { id: 'hidden-benchmark-configuration-v1', planDate: '2027-03', sourceCutoff: '2026-07-31', workload: { id: 'named-synthetic-request-shape', statement: 'question-only-no-service' }, constraints: [{ id: 'named-latency-budget-question', role: 'planned-constraint' }], benchmark: { state: 'not-run', configuration: 'hidden', result: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'disallowed-winner-v1': { id: 'disallowed-winner-v1', planDate: '2027-03', sourceCutoff: '2026-07-31', workload: { id: 'named-synthetic-request-shape', statement: 'question-only-no-service' }, constraints: [{ id: 'named-latency-budget-question', role: 'planned-constraint' }], benchmark: { state: 'not-run', configuration: 'not-collected', result: 'not-collected' }, requestedConclusion: 'd-runtime-wins', boundary: 'Negative fixed literal only.' },
});
export function createFixedRuntimeCase(id = 'planned-runtime-hand-off-v1') { const value = FIXED_RUNTIME_CASES[id]; return value ? deepFreeze(cloneFixed(value)) : undefined; }
function stop(status, reason, nextAction) { return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' }); }
export function assessFixedRuntimePlan(input) {
if (!Object.values(FIXED_RUNTIME_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-03' || input.sourceCutoff !== '2026-07-31') return stop('stop-undated-scenario-or-cutoff', 'march-2027-plan-and-july-2026-cutoff-are-required', 'name-2027-03-and-2026-07-31');
if (!input.workload?.id || !input.workload?.statement || !Array.isArray(input.constraints) || input.constraints.length < 1 || input.constraints.some((item) => !item.id || item.role !== 'planned-constraint')) return stop('stop-unnamed-workload-or-constraint', 'workload-and-every-constraint-must-be-named-synthetic-questions', 'name-the-workload-and-constraints');
if (input.benchmark?.state !== 'not-run' || input.benchmark?.configuration !== 'not-collected' || input.benchmark?.result !== 'not-collected') return stop('stop-hidden-benchmark-configuration', 'a-future-benchmark-must-remain-not-run-with-no-configuration-or-result', 'keep-benchmark-as-not-run-and-not-collected');
if (input.requestedConclusion !== 'synthetic-plan-hand-off') return stop('stop-disallowed-winner-or-positive-result', 'runtime-winner-or-positive-result-is-forbidden', 'use-synthetic-plan-hand-off');
return deepFreeze({ status: 'synthetic-plan-hand-off', caseId: input.id, workload: deepFreeze(cloneFixed(input.workload)), constraints: deepFreeze(cloneFixed(input.constraints)), benchmark: deepFreeze(cloneFixed(input.benchmark)), boundary: input.boundary, productionEffect: 'not-attempted', nextAction: 'give-the-fixed-runtime-question-contract-to-a-future-evidence-owner' });
function plainText(html) {
return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim();
}
export function runFixedRuntimeFixture() { const expected = [['planned-runtime-hand-off-v1', 'synthetic-plan-hand-off'], ['undated-scenario-v1', 'stop-undated-scenario-or-cutoff'], ['unnamed-workload-or-constraint-v1', 'stop-unnamed-workload-or-constraint'], ['hidden-benchmark-configuration-v1', 'stop-hidden-benchmark-configuration'], ['disallowed-winner-v1', 'stop-disallowed-winner-or-positive-result']]; const checks = expected.map(([id, status]) => ({ id, expected: status, actual: assessFixedRuntimePlan(createFixedRuntimeCase(id)).status })); return deepFreeze({ passed: checks.filter((item) => item.expected === item.actual).length, total: checks.length, accepted: checks.every((item) => item.expected === item.actual) && Object.isFrozen(createFixedRuntimeCase()) && Object.isFrozen(createFixedRuntimeCase().constraints), 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: 'd112', use: 'Зафиксирована дата последнего учитываемого официального D release и только перечень его изменений.', boundary: 'Не подтверждает выбор runtime, запуск, benchmark, profile или сервисный сценарий.' },
{ key: 'd111', use: 'Использован как pinned официальный материал о D 2.111.0 и его версии.', boundary: 'Не превращает языковую возможность в результат сравнения.' },
{ key: 'd110', use: 'Использован как pinned официальный material о D 2.110.0 и его дате.', boundary: 'Не даёт данных о чужой нагрузке, конфигурации или production effect.' },
function bodyText(html) {
return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
const REFERENCES = Object.freeze({
functions: {
title: 'D Language Specification: Functions and Function Safety',
url: 'https://dlang.org/spec/function.html',
version: 'D language specification, page generated 25 July 2026',
},
memory: {
title: 'D Language Specification: Memory Safety',
url: 'https://dlang.org/spec/memory-safe-d.html',
version: 'D language specification, page generated 23 July 2026',
},
abi: {
title: 'D Language Specification: Application Binary Interface',
url: 'https://dlang.org/spec/abi.html',
version: 'D language specification, page checked 31 July 2026',
},
cInterface: {
title: 'D Language Specification: Interfacing to C',
url: 'https://dlang.org/spec/interfaceToC.html',
version: 'D language specification, page checked 31 July 2026',
},
});
function sources(entries) {
return '<ul>' + entries.map(({ key, use, boundary }) => {
const reference = REFERENCES[key];
return '<li><a href="' + reference.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(reference.title) + '</a> — версия и дата: ' + escapeHtml(reference.version) + '. Применение: ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>';
}).join('') + '</ul>';
}
export function validateDWorkload(input) {
const throughput = Number(input?.throughput ?? 0);
const latencyBudgetMs = Number(input?.latencyBudgetMs ?? 0);
const nativeBoundary = Boolean(input?.nativeBoundary);
const deploymentTargets = Number(input?.deploymentTargets ?? 0);
if (!Number.isFinite(throughput) || throughput <= 0) return { decision: 'reject', reason: 'нужна измеримая нагрузка' };
if (latencyBudgetMs <= 0) return { decision: 'reject', reason: 'нужен бюджет задержки' };
if (deploymentTargets > 2 && !nativeBoundary) return { decision: 'compare', reason: 'широкий target matrix требует сравнить toolchain' };
if (nativeBoundary && throughput > 10000) return { decision: 'consider-d', reason: 'нагрузка и нативная граница дают основание проверить D' };
return { decision: 'keep-current-tool', reason: 'ограничения не требуют смены языка' };
}
export function checkSafeBoundary(input) {
const capacity = Number(input?.capacity ?? 0);
const declaredLength = Number(input?.declaredLength ?? 0);
const pointerChecked = Boolean(input?.pointerChecked);
if (!pointerChecked) return { class: 'system', action: 'проверить указатель и владельца буфера вручную' };
if (!Number.isInteger(declaredLength) || declaredLength < 0 || declaredLength > capacity) return { class: 'reject', action: 'остановить вызов: длина выходит за буфер' };
return { class: 'safe-interface', action: 'передать slice с проверенной длиной' };
}
export function validateCAbiPacket(packet, contract) {
const errors = [];
if (packet?.byteLength !== contract?.byteLength) errors.push('размер структуры');
if (packet?.endianness !== contract?.endianness) errors.push('порядок байтов');
for (const field of contract?.fields ?? []) {
if (!Object.hasOwn(packet ?? {}, field.name)) errors.push('поле ' + field.name);
}
return { accepted: errors.length === 0, errors };
}
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 Object.freeze({ ...meta, contentHtml, proseLength });
}
const practice = revision({
slug: 'editorial-2027-03-practice-d-lessons',
title: 'D для прикладной утилиты: сначала контракт входа, потом язык',
categories: ['D', 'Инженерные практики'],
cover: '/assets/editorial/2027/d-lessons-2027-runtime-tradeoff-map.svg',
excerpt: 'Как понять, есть ли у задачи основание для D: нагрузка, native boundary, target matrix и стоимость toolchain.',
readingMinutes: 15,
}, [
p('Проблема выбора языка для утилиты начинается с красивого свойства: D компилируется в native binary, поддерживает контрактные проверки и даёт доступ к C ABI. Если принять это свойство за готовое решение, небольшая команда получает новый компилятор, пакетный менеджер и набор правил сборки, хотя исходная задержка могла возникать в запросе, формате файла или неверном контракте входа. Цена — месяцы сопровождения ради проблемы, которую язык не решает.'),
p('Чтобы решение было инженерным, нужно сначала описать workload: сколько элементов проходит через программу, какой бюджет задержки, какие target-платформы, есть ли C-библиотека и как будет собираться бинарник. D появляется в таблице как один вариант рядом с текущим языком. Этот порядок не принижает язык. Он защищает проект от решения по вкусу и оставляет проверяемый критерий, когда выбор оправдан.'),
h2('Что именно хотим улучшить'),
p('«Нужна производительность» — слишком широкая формулировка. Для CLI важны время запуска, скорость обработки, память и размер артефакта. Для сервиса добавляется модель конкуренции, timeout и наблюдаемость. Для инструмента около C-библиотеки важны соглашение вызова, layout структуры и способ владения памятью. Один и тот же язык может быть уместен для одного пункта и лишним для другого.'),
p('Опишите минимум два кандидата. Текущий инструмент часто выигрывает скоростью разработки и готовыми библиотеками; D может выиграть у места, где важны native deployment, явная работа с памятью или C ABI. Но каждое преимущество имеет стоимость: новый toolchain, обучение, platform packages, время сборки и диагностика production binary. Сравнение должно показывать эту цену рядом с эффектом.'),
figure('/assets/editorial/2027/d-lessons-2027-runtime-tradeoff-map.svg', 'Карта выбора языка для прикладной утилиты: workload и ограничения ведут к сравнению D с текущим инструментом и проверяемому решению.', 'Схема начинает решение с нагрузки и ограничений. Ветка D появляется только после определения границы задачи, а не из-за отдельного свойства языка.'),
table('Матрица выбора D для утилиты', ['Ограничение', 'Вопрос', 'Сигнал в пользу D', 'Цена решения'], [
['Нагрузка', 'где расходуется CPU и память?', 'узкое место внутри вычисления', 'нужен профиль, а не предположение'],
['Native boundary', 'есть ли C ABI или системный вызов?', 'контракт можно проверить на границе', 'ручная проверка unsafe-участка'],
['Targets', 'сколько платформ и архитектур?', 'матрица поддерживается toolchain', 'сборки и бинарные артефакты'],
['Команда', 'кто будет читать и менять код?', 'есть owner и code review', 'обучение и время поддержки'],
['Доставка', 'как версионируется бинарник?', 'простая доставка без runtime', 'размер, лицензии, упаковка'],
]),
h2('Учебный локальный фильтр требований'),
p('Функция ниже делает не рекламный вывод, а фиксирует форму входа. В ней есть положительная ветка только тогда, когда одновременно названы нагрузка, latency budget и native boundary. Если target matrix шире двух платформ, результат предлагает сравнение, а не автоматический переход. Все числа учебные: перед решением их заменяют измерениями конкретной команды.'),
code(`import { validateDWorkload } from './upgrade-2027-03.mjs';
const inputs = [
{ throughput: 12000, latencyBudgetMs: 20, nativeBoundary: true, deploymentTargets: 1 },
{ throughput: 300, latencyBudgetMs: 500, nativeBoundary: false, deploymentTargets: 1 },
{ throughput: 12000, latencyBudgetMs: 20, nativeBoundary: false, deploymentTargets: 4 },
];
const practice = revision({ slug: 'editorial-2027-03-practice-d-lessons', title: 'Уроки D для прикладного инженера: практический маршрут', categories: ['Инженерные практики', 'Языки программирования'], cover: '/assets/editorial/2027/d-lessons-2027-runtime-tradeoff-map.svg', excerpt: 'План на март 2027: как поставить вопрос о D среди ограничений, не объявляя его языком по умолчанию.', readingMinutes: 21 }, [
p('План на 2027-03 начинается не с установки D, а с конкретной дорогой ошибки: прикладной инженер принимает язык за готовый ответ до того, как назвал границы задачи. Тогда небольшая задача получает новый toolchain, сложный маршрут сборки и спор о вкусе, хотя исходная проблема могла быть в контракте, владении или задержке между слоями. Цена ошибки — не абстрактная «сложность», а время на интеграцию, повторные изменения и невозможность объяснить, что именно сравнивали.'),
p('Вторая дорогая ошибка — описать будущий benchmark как уже существующий profile или сервисный сценарий. На редакторскую дату 2026-07-31 март 2027 ещё не наступил: у этого номера нет повторного profile, benchmark, запуска D, выбранного runtime, выпущенной версии или результата сравнения. Поэтому перед вами только явно маркированный план/сценарий на 2027-03 с source cutoff 2026-07-31. Единственный положительный output — <code>synthetic-plan-hand-off</code> и <code>productionEffect: not-attempted</code>.'),
h2('Не начинать с языка по умолчанию'),
p('D полезно рассматривать как один из кандидатов, когда вопрос уже имеет форму. Не «нужен ли быстрый язык», а «какой именованный участок требует проверяемого свойства и не ломает границу владения». Язык способен выразить ограничения, но не отменяет формат обмена, политику релиза, ownership зависимостей и стоимость сопровождения. Если эти условия не названы, сравнение превращается в выбор любимого инструмента, а не в инженерное решение.'),
p('Практический маршрут намеренно оставляет D не-default. Для существующей системы, где основной риск — несовместимый контракт или отсутствие владельца, смена языка может не дать никакого полезного сигнала. Для маленькой утилиты, где уже согласован другой runtime, цена нового пути может оказаться выше потенциальной выгоды. Для участка с жёсткими ограничениями вопрос может быть уместен, но это ещё не означает, что D должен победить. Уместность — приглашение к проверке, а не verdict.'),
figure('/assets/editorial/2027/d-lessons-2027-runtime-tradeoff-map.svg', 'Карта планового сравнения runtime: от именованной synthetic нагрузки и ограничений к вариантам D и не-D; красные блоки запрещают default, скрытый benchmark и объявление победителя.', 'Карта помогает сформировать будущий вопрос о компромиссах. Она не показывает существующий сервис, не выбирает runtime и не содержит результата benchmark.'),
table('Карта контекстного сравнения на 2027-03', ['Вопрос', 'Допустимый плановый ответ', 'Почему D не default', 'Что пока неизвестно'], [
['Нагрузка', 'named synthetic request shape', 'форма вопроса не равна реальной системе', 'объём, данные и окружение'],
['Ограничение', 'named latency или ownership question', 'одно ограничение не заменяет остальные', 'приоритет и допустимый компромисс'],
['Runtime', 'D и альтернативы как варианты', 'вариант не равен выбранному стеку', 'совместимость и стоимость доставки'],
['Benchmark', 'not-run / not-collected', 'без config нельзя сравнивать', 'методика, machine, flags, samples'],
['Вывод', 'synthetic-plan-hand-off', 'план не назначает winner', 'нужен ли отдельный scope'],
for (const input of inputs) console.log(validateDWorkload(input));
// consider-d; keep-current-tool; compare`),
p('Первый вход имеет узкую вычислительную задачу и нативную границу — D стоит проверить измерением. Второй не требует смены toolchain по заданным ограничениям. Третий слишком широк для одного выбора: сначала нужно сравнить способы сборки и доставки на всех targets. Функция полезна как шаблон карточки требований, а не как замена профилированию.'),
h2('Контрактная проверка в D'),
p('У D есть function contracts: precondition через <code>in</code> и postcondition через <code>out</code>. Это не универсальная валидация входа и не замена тестам. Контракт полезен, когда условие принадлежит самой функции: размер диапазона, допустимый индекс, инвариант результата. Для пользовательского файла всё равно нужна отдельная ошибка с безопасным сообщением и понятным форматом.'),
p('При выборе языка не обещайте, что contract автоматически ускорит программу или найдёт бизнес-ошибку. Он проверяет условие в точке выполнения и зависит от режима сборки. Важнее сначала назвать, кто владеет условием: parser, domain service или boundary с C. Тогда один и тот же контракт можно повторить в тесте и в обработчике ошибки, не пряча смысл в assertion.'),
h2('Действия по порядку'),
ol([
'Записать единицу нагрузки, бюджет задержки, размер данных, target-платформы и входные ограничения.',
'Найти измеряемый bottleneck и проверить, находится ли он внутри кода, который язык действительно изменит.',
'Сравнить D и текущий вариант по сборке, библиотекам, отладке, размеру бинарника и навыкам поддержки.',
'Для нативной границы описать C ABI, владение памятью и ошибку; unsafe-участок выделить отдельно.',
'Собрать минимальный прототип с одним workload и одинаковой методикой замера, затем зафиксировать результат и цену поддержки.',
]),
h2('Сначала назвать нагрузку, затем границу'),
p('Слово «нагрузка» нельзя оставлять декоративным. В этом draft <code>named-synthetic-request-shape</code> — только имя вопроса, без URL, клиента, количества запросов, payload и реального сервиса. Такой уровень намеренно слабый: он позволяет проверить, что вопрос назван, но не позволяет имитировать данные. Если будущей команде потребуется фактическая нагрузка, она должна появиться в отдельном authorised scope с собственными правилами доступа и provenance, а не быть тайно дописана в мартовский план.'),
p('Ограничение должно быть столь же конкретным по форме. «Нужна производительность» не различает latency, throughput, старт, память, хвост распределения, стоимость обслуживания или риск интеграции. В literal есть <code>named-latency-budget-question</code> и <code>named-ownership-boundary-question</code>: это labels для будущей развилки, а не бюджет и не замер. Если имя отсутствует, evaluator останавливает сценарий. Подставлять удобное default-ограничение опаснее, чем вернуть пустую карточку: оно незаметно меняет смысл выбора.'),
h2('Механизм практического решения'),
p('Контекстное сравнение состоит из трёх разных операций. Первая фиксирует, что требуется различить: например, путь, где ограничение выражается в исходном коде, и путь, где оно обеспечивается вокруг кода. Вторая перечисляет, какие trade-off могут сделать сравнение нечестным: сборка, ABI, отладка, доступность библиотек, deployment и ownership. Третья решает, существуют ли полномочия и данные для benchmark. Смешать операции — значит сделать «быстро» одновременно требованием, методом и результатом.'),
p('Официальные changelog D помогают удержать версионную дисциплину. На cutoff последняя используемая здесь публикация — D 2.112.0 от 07 January 2026; в ней перечислены изменения compiler, runtime и library. Это источник слов о конкретном release, но не источник о пригодности к нашей неописанной нагрузке. Равным образом упоминание 2.111.0 или 2.110.0 не доказывает, что будущая сборка совместима, что runtime выбран или что изменение будет заметно в сервисе.'),
h2('Runnable literal вместо поддельного запуска'),
code("import { createFixedRuntimeCase, assessFixedRuntimePlan } from './upgrade-2027-03.mjs';\n\nconst plan = createFixedRuntimeCase('planned-runtime-hand-off-v1');\nconst result = assessFixedRuntimePlan(plan);\nconsole.log({ status: result.status, effect: result.productionEffect });\n// { status: 'synthetic-plan-hand-off', effect: 'not-attempted' }"),
p('Этот example runnable и безопасен, потому что работает только с fixed in-memory literal. Factory делает JSON clone и затем deep freeze всех вложенных значений; evaluator принимает JSON-равный известный literal и fail-closed закрывает другой input. Код не запускает компилятор, не выбирает D runtime, не читает файлы, environment, clock, сеть, secrets, profile или telemetry. Он проверяет только дисциплину будущего вопроса и не является microbenchmark, performance test или сервисной репетицией.'),
h2('Порядок действий для будущего owner'),
ol(['Зафиксировать <code>planDate: 2027-03</code> и <code>sourceCutoff: 2026-07-31</code>; без этой пары остановить сценарий.', 'Назвать synthetic workload без описания несуществующего сервиса, данных или profile.', 'Назвать каждое constraint и отдельно записать, кто вправе изменить его приоритет.', 'Поставить D рядом с альтернативами как subject of comparison, не как default и не как решение.', 'Оставить benchmark строго <code>not-run</code>, configuration/result — <code>not-collected</code>; скрытая configuration запрещена.', 'Передать только question contract будущему evidence owner; не объявлять winner, rollout, runtime selection или production gain.']),
h2('Где полезно остановиться до сравнения'),
p('Есть несколько случаев, когда честный маршрут заканчивается раньше benchmark. Если неизвестно, кто владеет boundary между вызывающим кодом и библиотекой, сначала нужен contract; иначе любой runtime будет оцениваться на плавающей постановке. Если delivery path не может принять новый compiler или артефакт, это не «минус D в целом», а ограничение конкретного контура. Если предметом тревоги служит только общее ощущение медленности, полезнее сначала назвать операцию и ожидание. Сценарий не обязан доходить до измерения, чтобы принести пользу: ранний stop сохраняет время, которое иначе ушло бы на спор о неназванной цели.'),
p('Не-default позиция означает и право оставить текущий runtime. Такой выбор не требует доказывать, что существующий стек абсолютный победитель. Достаточно увидеть, что future question пока не различает варианты или что стоимость введения нового пути превышает известную ценность. Особенно вредно превращать отказ от эксперимента в скрытую оценку языка. «Не открываем scope» и «D не подходит» — разные statements: первое описывает решение о работе, второе потребовало бы evidence, которого в этом draft нет.'),
h2('Как не спутать сравнение с миграцией'),
p('Сравнение и миграция имеют разный предмет. Сравнение спрашивает, какой вариант стоит исследовать при заданной форме задачи. Миграция уже предполагает выбранную точку назначения, инвентаризацию зависимостей, переходную архитектуру, rollback и владение результатом. Если в карточку сравнения незаметно попадают слова «перенести», «заменить» или «выпустить», она начинает обещать организационную работу. P109 не создаёт такой работы: он не содержит списка модулей, совместимости, плана релиза или бизнес-эффекта.'),
p('Точно так же нельзя использовать маленький runnable literal как proof of language behaviour. JavaScript validator здесь выбран только потому, что он проверяет редакционный контракт пакета; он не моделирует D, ABI или memory management. Его значение в том, что любой reader может увидеть одинаковый stop для недатированного сценария и declared winner. Будущее D-пример, если оно потребуется, должно быть собственным artifact с pinned compiler, исходным кодом и объяснённой целью. До этого граница между meta-check и техническим test должна оставаться видимой.'),
h2('Ограничения маршрута и следующий шаг'),
p('Маршрут не покрывает выбор команды, бюджет, лицензионную проверку, security review, ABI-интеграцию, compatibility matrix или обслуживание пакетов. Он не говорит, что D быстрее, безопаснее или дешевле какой-либо альтернативы. Публичный release material не заменяет фактов о target platform и не позволяет вычислить стоимость миграции. Особенно важно не подменять отсутствие benchmark словом «интуитивно»: интуиция может быть поводом сформулировать гипотезу, но не положительным результатом.'),
p('Следующий шаг — будущему владельцу решить, открывать ли отдельный scope с названной workload, прозрачной конфигурацией benchmark и заранее согласованными критериями falsification. Если полномочия или данные не появляются, <code>synthetic-plan-hand-off</code> остаётся корректным финалом этого draft. В марте 2027 он может стать входом нового исследования, но не задним числом доказательством. Практический урок D здесь простой: сначала сделать условия выбора видимыми, затем позволить языку проиграть, если условия ему не подходят.'),
], refs);
h2('Ограничения и следующий шаг'),
p('Фильтр требований не измеряет скорость и не говорит, что D лучше. Порог throughput в примере вымышленный и не переносится на другое железо. Контракты не устраняют логические ошибки, зависимость от внешней библиотеки или стоимость сборки. D также не делает код переносимым автоматически: platform ABI, linker и runtime остаются частью решения.'),
p('Следующий шаг — взять одну горячую функцию и сделать парный прототип на текущем языке и D с одинаковым входом и выходом. Замерьте cold start, steady-state, память и время разработчика на исправление намеренной ошибки. Решение «остаться» будет таким же полезным результатом, как переход, если оно опирается на эти поля.'),
], [
{ key: 'functions', use: 'Function contracts и атрибуты D используются для объяснения pre/post conditions и границы функции.', boundary: 'Спецификация не выбирает язык для продукта и не даёт benchmark конкретного workload.' },
{ key: 'memory', use: 'Категории @safe, @trusted и @system используются при оценке нативной границы.', boundary: 'Memory safety не гарантирует отсутствие логических, portability и performance ошибок.' },
{ key: 'abi', use: 'ABI-граница включена в матрицу выбора как часть доставки бинарника.', boundary: 'Спецификация ABI не описывает настройки конкретного компилятора и платформы.' },
]);
const mechanism = revision({ slug: 'editorial-2027-03-mechanism-d-lessons', title: 'Уроки D для прикладного инженера: как принять инженерное решение', categories: ['Архитектура', 'Языки программирования'], cover: '/assets/editorial/2027/d-lessons-2027-constraint-matrix.svg', excerpt: 'План на март 2027: runtime trade-off как проверяемая гипотеза с ограничениями и falsifiability.', readingMinutes: 21 }, [
p('Конкретная проблема mechanism-статьи — спутать свойство языка со свойством готовой системы. После этого из фразы о безопасности, управлении памятью или компиляции делают обещание о latency и надёжности целого маршрута. Цена ошибки — неверный scope: инженер чинит или сравнивает runtime, хотя решение определяется границей FFI, планом сборки, аллокациями в библиотеке либо операционной моделью.'),
p('Вторая проблема — скрыть условия будущего benchmark и затем назвать его победителя заранее. По состоянию на 2026-07-31 нет benchmark, повторного profile, service scenario, runtime selection, release D после cutoff или результатов сравнения для этого выпуска. Это только план/сценарий на 2027-03. Мы не утверждаем, что D или иной runtime выиграл; единственный допустимый positive outcome — synthetic plan hand-off с <code>productionEffect: not-attempted</code>.'),
h2('Runtime trade-off — это не таблица сильных сторон'),
p('Runtime trade-off полезен, когда он связывает именованное ограничение с конкретным механизмом и условием опровержения. «Меньше аллокаций» без пути исполнения — рекламный ярлык. «Нужен предсказуемый хвост» без метода наблюдения — только желание. В будущем сравнении каждое утверждение должно иметь владельца: кто определил workload, кто согласовал baseline, кто описал machine и compiler flags, кто вправе считать результат достаточным. Пока этих элементов нет, модель должна говорить not-collected, а не заполнять пробелы правдоподобными цифрами.'),
p('D приносит в такой разговор выразительные языковые механизмы и собственный runtime-контекст, но не снимает обязанность проверить границу. Например, декларация атрибутов или изменение в release может изменить пространство вариантов; она не доказывает поведение интегрированного приложения. Changelog 2.112.0 фиксирует официальный перечень изменений релиза от 07 January 2026, а не performance claim для будущего сценария. Именно это различие защищает обсуждение от подмены «доступно в версии» на «подходит системе». '),
figure('/assets/editorial/2027/d-lessons-2027-constraint-matrix.svg', 'Матрица ограничений runtime: именованные вопросы о задержке, владении, поставке и наблюдаемости сопоставлены с необходимым будущим evidence; нижняя строка запрещает скрытую конфигурацию и победителя.', 'Матрица показывает falsifiability будущего сравнения. Она не содержит измерений, профиля, runtime decision или факта о production.'),
table('Матрица falsifiability будущего runtime-вопроса', ['Ограничение', 'Проверяемый механизм', 'Минимальный future evidence', 'Что опровергнет гипотезу'], [
['Latency question', 'граница работы и измеряемая операция', 'полная benchmark configuration', 'не тот путь или не тот budget'],
['Ownership question', 'владение памятью и API boundary', 'явный contract и lifecycle', 'скрытая передача или aliasing'],
['Delivery question', 'compiler, runtime, packaging', 'target/flags/dependency record', 'несовместимый путь поставки'],
['Observability question', 'какие события допустимо видеть', 'согласованный provenance', 'неполный или недоступный input'],
['Сейчас', 'fixed literal validator', 'not-run/not-collected', 'любая claim о winner или result'],
const mechanism = revision({
slug: 'editorial-2027-03-mechanism-d-lessons',
title: 'D: @safe и @trusted на границе C API',
categories: ['D', 'Безопасность памяти'],
cover: '/assets/editorial/2027/d-lessons-2027-constraint-matrix.svg',
excerpt: 'Как провести маленький unsafe-участок через проверенный интерфейс и не считать атрибут @safe доказательством всей системы.',
readingMinutes: 16,
}, [
p('Проблема FFI-кода появляется там, где D вызывает C-функцию с указателем и отдельной длиной буфера. Если длина пришла из другого источника, вызов может прочитать за пределами памяти, даже когда внешний метод выглядит коротким. Цена ошибки — повреждение памяти, падение процесса или уязвимость, которую трудно воспроизвести по обычному input. Один атрибут на публичной функции не исправляет неверное условие границы.'),
p('В D для этой границы различаются @safe, @trusted и @system. @safe-код ограничивает операции, которые могут привести к memory corruption. @trusted разрешает узкий участок, но ответственность за его интерфейс остаётся у автора. @system не даёт компилятору такого обещания. Механизм работает, если unsafe-код короткий, его входы проверены, а наружу выходит безопасное представление данных.'),
h2('Сначала проверяем размер, потом вызываем C'),
p('C-функция часто получает <code>pointer + length</code>. Сам указатель не содержит длину, поэтому компилятор не может вывести, что заявленный диапазон действителен. В D безопасный wrapper должен принять массив или slice, проверить нужное условие и передать только диапазон, размер которого известен. Если C API требует null-terminated string, одного массива байт тоже недостаточно: нужна отдельная проверка завершающего байта.'),
p('Изолируйте правила владельца. Если C-функция сохраняет указатель после возврата, wrapper должен либо запретить такой вызов, либо передать копию с понятным временем жизни. Атрибут <code>scope</code> помогает выражать ограничения escape там, где включена соответствующая проверка, но он не заменяет договорённость с внешней библиотекой. Любая функция, которая сохраняет адрес, требует отдельного чтения API и теста.'),
figure('/assets/editorial/2027/d-lessons-2027-constraint-matrix.svg', 'Матрица границы D и C API: размер буфера, владелец, атрибут безопасности и допустимый результат проверки.', 'Схема связывает техническое ограничение с проверкой и стоп-условием. Зелёный путь начинается только после проверки длины и времени жизни.'),
table('Роли атрибутов на FFI-границе', ['Уровень', 'Что разрешает', 'Что обязан проверить инженер', 'Типичная ошибка'], [
['@safe', 'ограниченный набор операций', 'что вызовы и значения остаются безопасными', 'считать весь вызванный C безопасным'],
['@trusted', 'узкая ручная обёртка', 'инвариант указателя, длины и lifetime', 'поместить большой модуль в trusted'],
['@system', 'низкоуровневые операции', 'каждый callsite и контракт ABI', 'передать raw pointer без проверки'],
['slice', 'указатель и длина вместе', 'что slice не выходит за объект', 'довериться внешней length'],
]),
h2('Ограничения задают форму причинности'),
p('Условия нельзя собирать после вывода. Если latency budget назван только в заключении, получится круг: выбранный runtime объявляют подходящим, потому что именно его измеряли, а измеряли его, потому что он уже объявлен подходящим. Если ownership boundary не зафиксирован, один вариант может незаметно переложить работу в обвязку, а другой — оставить её внутри процесса. Оба тогда выглядят разными языковыми ответами на вопрос, который на самом деле не был сформулирован.'),
p('Falsifiability требует разрешить неприятный результат заранее. Для D это означает: определить, какие наблюдения будущего authorized scope сделают вариант неуместным, и не прятать их за словом «зависит». Это может быть неприемлемая поставка, нужда в отсутствующей интеграции, неоправданный operational burden или несовпадение с ownership rule. В нашем draft эти варианты не измеряются и не оцениваются. Но они должны быть названы как классы стоп-сигнала, иначе у сравнения остаётся лишь путь к заранее выбранному ответу.'),
h2('Configuration — часть evidence, а не приложение мелким шрифтом'),
p('Benchmark configuration — не техническая мелочь. В неё в будущем войдут хотя бы версия compiler/runtime, target, flags, машина, ввод, число прогонов, прогрев, baseline, способ агрегации и правило исключения выбросов. Пока ни одно из этих значений не определено в данном scope, нельзя написать даже «условно измерено». Наша fixed case прямо содержит <code>state: not-run</code>, <code>configuration: not-collected</code> и <code>result: not-collected</code>; любое другое значение останавливается как hidden configuration.'),
p('Такой fail-closed ответ намеренно строже, чем обычная черновая заметка. Он не пытается угадать, какой флаг или machine будет разумным в 2027-03, потому что угадывание сразу стало бы частично вымышленным benchmark. Кроме того, оптимальная конфигурация зависит от вопроса: тестировать build time и response path одинаковой таблицей бессмысленно. Механизм статьи не предлагает template для всех сравнений; он требует, чтобы future owner сначала отделил benchmark contract от результата и смог показать оба.'),
h2('Литеральная проверка границы'),
code("import { createFixedRuntimeCase, assessFixedRuntimePlan } from './upgrade-2027-03.mjs';\n\nconst hidden = createFixedRuntimeCase('hidden-benchmark-configuration-v1');\nconsole.log(assessFixedRuntimePlan(hidden));\n// { status: 'stop-hidden-benchmark-configuration', productionEffect: 'not-attempted', ... }"),
p('Пример не компилирует D и не измеряет программу. Он only evaluates a named in-memory literal, полученный через JSON clone и recursively deep freeze. Изменённый object или неизвестный id также закрываются, поэтому caller не может пронести результат под видом plan. Это маленький, но важный механизм: validator не утверждает, что configuration хорошая; он утверждает только, что у будущего comparison нет права скрывать её или заменить результатом ещё не проведённого запуска.'),
h2('Официальный release не равен compatibility promise'),
p('Источники этого номера намеренно ограничены official pinned primary D release materials. Они датируют 2.110.0, 2.111.0 и 2.112.0 и дают контекст изменениям, которые были опубликованы не позднее cutoff. Они не заменяются блогами, benchmark-агрегаторами, чужими кейсами или документацией, которая успела измениться после cutoff. Такая узость не обедняет draft: она не позволяет тихо импортировать современную репутацию экосистемы туда, где требуется доказательство конкретной интеграции.'),
p('Из release notes можно корректно извлечь только ограниченные факты о release. Например, 2.112.0 перечисляет изменения compiler и runtime; это повод включить version pin в будущую конфигурацию. Некорректный следующий шаг — превратить список в утверждение, что определённая service path станет лучше. Между ними лежат target, код, allocator, libraries, ABI, операционная среда и метод измерения. Даже если все эти элементы появятся позже, это будет evidence нового scope, а не свойство этого мартовского текста.'),
h2('Порядок для проверяемого trade-off'),
ol(['Назвать synthetic workload и не дописывать несуществующий endpoint, client или объём.', 'Выделить ограничения: latency, ownership, delivery и observability не сводить к одному слову «быстро».', 'Для каждого ограничения назвать механизм и заранее записать, что могло бы его опровергнуть.', 'Зафиксировать, что benchmark пока not-run, а configuration и result not-collected.', 'Проверить литерал fail-closed validator; скрытая config, unnamed constraint и winner должны вернуть stop.', 'Передать only the question contract будущему owner, который отдельно решит доступ, методику и выбор runtime.']),
h2('Почему один результат не закрывает trade-off'),
p('Даже будущий корректный замер не отменит matrix. Один result отвечает только на один заранее описанный вопрос при одной configuration; он не переносится автоматически на другой target, другой вход или другой lifecycle. Это важно для прикладного инженера: желание закончить спор одной цифрой понятно, но часто именно цифра скрывает смену scope. Если ownership или delivery остаются нерешёнными, быстрый путь может быть плохим вариантом. Если latency не критична, сложная настройка может не окупить даже честно найденную разницу.'),
p('Поэтому outcome будущего исследования должен быть двусторонним. Он может дать основание продолжить изучение D, оставить текущий вариант, изменить постановку или признать, что разница несущественна. Ни один из этих результатов нельзя назначить в мартовском draft. Здесь допускается только проверка готовности вопроса к такой развилке. Это меньше похоже на сравнительную презентацию, зато даёт получателю возможность проверить предпосылки до того, как значение метрики получит незаслуженный авторитет.'),
h2('Ограничения механизма и следующий шаг'),
p('Механизм не покрывает proof of correctness, security posture, лицензии, стоимость обучения, зрелость команды, policy закупок или поддержку через годы. Он не делает вывод о GC, manual memory control, @safe, Better C или определённом compiler: для такого вывода нужен отдельно зафиксированный вопрос и источник, а не общее имя D. Он также не говорит, что любой benchmark полезен. Плохо поставленный benchmark может дать больше уверенности и меньше знания, чем честный отказ от запуска.'),
p('Следующий шаг — создать новый, явно authorised evidence scope только после того, как владелец примет named workload и constraint matrix. В нём можно будет выбрать версии, описать конфигурацию и сформулировать критерий остановки; сейчас этого нет. Если процесс не начнётся, outcome не меняется: <code>synthetic-plan-hand-off</code> не является провалом, а фиксирует, что вопрос пока поставлен без выдуманного выигрыша. Самый полезный урок runtime trade-off — сделать условия поражения видимыми до попытки найти победителя.'),
], refs);
h2('Локальная проверка буфера'),
p('Вместо вызова реальной C-библиотеки сначала можно прогнать boundary checker на данных теста. Он принимает capacity, заявленную длину и признак проверки указателя. Результат разделяет отсутствие проверки, неверный диапазон и безопасный интерфейс. Это предметный пример входа в FFI: он проверяет именно опасную пару pointer/length, а не абстрактный статус карточки.'),
code(`import { checkSafeBoundary } from './upgrade-2027-03.mjs';
const field = revision({ slug: 'editorial-2027-03-field-d-lessons', title: 'Уроки D для прикладного инженера: кейс с ограничениями и выводами', categories: ['Полевые заметки', 'Инженерные решения'], cover: '/assets/editorial/2027/d-lessons-2027-evidence-handoff-loop.svg', excerpt: 'План на март 2027: synthetic evidence hand-off вместо report, profile или результата сравнения.', readingMinutes: 21 }, [
p('Самая дорогая ошибка field-формата — выдать hand-off будущего исследования за готовый кейс. Читатель видит знакомые слова «profile», «сервис», «runtime» и считает, что уже есть данные, владелец, измерение и результат. Цена ошибки — ложная операционная память: следующая команда ищет несуществующий артефакт, защищает несуществующий вывод и может построить реальное решение на авторитете аккуратно написанного текста.'),
p('Здесь этого не будет. На 2026-07-31 март 2027 ещё не наступил, поэтому P109 — только план/сценарий на 2027-03, source cutoff 2026-07-31. Нет повторного profile, benchmark, сервисного сценария, выбора runtime, выпуска D после cutoff или результата сравнения. Field focus означает synthetic evidence hand-off: карточку вопроса, которую будущий owner может принять, вернуть или развить в другом scope. Единственный positive outcome — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>.'),
h2('Hand-off передаёт вопрос, а не доказательство'),
p('В обычной полевой заметке полезно передать следы: конфигурацию, наблюдение, ссылку на код, лог, замер, дату и известный эффект. В этой заметке ни одного такого артефакта нет и не должно появиться. Synthetic hand-off содержит только date/cutoff, named synthetic workload, named planned constraints, состояние evidence и следующую роль. Он не содержит пользователя, проекта, machine, пакета, URL, команды, значения метрики или последовательности действий реальной системы.'),
p('Такой аскетизм не делает передачу бесполезной. Он делает provenance проверяемым. Будущий получатель видит, что <code>not-collected</code> означает отсутствие сбора внутри текущей модели, а не потерянный файл. Он может спросить, нужна ли вообще реальная проверка, кто имеет право открыть доступ к данным и какие claims допустимы. Автор draft не навязывает ответ через детали, которые нельзя проверить. Внутри сценария только один результат считается хорошим: вопрос назван достаточно честно, чтобы его можно было передать без production action.'),
figure('/assets/editorial/2027/d-lessons-2027-evidence-handoff-loop.svg', 'Петля synthetic evidence hand-off: дата, named workload и constraints проверяются до benchmark; при скрытой конфигурации или winner возникает stop, иначе question contract передаётся будущему владельцу.', 'Диаграмма описывает проверку fixed in-memory literals. Это не report о сервисе, profile, benchmark или принятый выбор runtime.'),
table('Контракт передачи synthetic evidence', ['Поле', 'Допустимое состояние сейчас', 'Что получает будущий owner', 'Чего поле не доказывает'], [
['Время', '2027-03 и cutoff 2026-07-31', 'границу редакционного знания', 'что мартовская работа сделана'],
['Workload', 'named synthetic question', 'предмет для уточнения', 'существование сервиса или input'],
['Constraints', 'named planned constraints', 'список развилок', 'приоритет или согласование'],
['Benchmark', 'not-run / not-collected', 'запрет выдумывать config/result', 'измерение или profile'],
['Conclusion', 'synthetic-plan-hand-off', 'право решить следующий scope', 'winner, rollout или gain'],
const calls = [
{ capacity: 16, declaredLength: 8, pointerChecked: true },
{ capacity: 16, declaredLength: 24, pointerChecked: true },
{ capacity: 16, declaredLength: 8, pointerChecked: false },
];
for (const call of calls) console.log(checkSafeBoundary(call));
// safe-interface; reject; system`),
p('Первый вход даёт диапазон внутри буфера. Второй останавливается до вызова: внешний контракт обещает 24 байта, а доступно 16. Третий не принимает решение за инженера, потому что адрес не прошёл проверку владельца. В D такой проверкой должен владеть маленький wrapper, а в тесте нужны граничные значения 0, capacity и capacity+1.'),
h2('Что означает @trusted'),
p('@trusted — не «проверено компилятором». Это обещание, что внешняя форма функции безопасна, хотя тело содержит операции, которые компилятор не может проверить. Поэтому у trusted-функции должны быть короткий исходник, явные preconditions и тесты на invalid length, null, пустой slice и повторный вызов. Не прячьте в ней преобразование формата, ownership и обработку ошибок одновременно.'),
p('Если внешняя C-функция возвращает указатель, проверка должна ответить на два вопроса: объект жив и его размер известен? При ответе «нет» безопасный интерфейс невозможен без копирования или дополнительного контракта. После вызова нельзя использовать старый slice, если C-функция освобождает память. Ошибка lifetime часто переживает тесты на успешном input, поэтому негативная матрица обязательна.'),
h2('Действия по порядку'),
ol([
'Прочитать C-прототип и зафиксировать смысл каждого указателя, длины, возвращаемого адреса и кода ошибки.',
'Выделить минимальный wrapper; не переносить внутрь @trusted парсинг, бизнес-правила и сетевой код.',
'Проверять указатель, диапазон, нулевую длину, overflow и время жизни до перехода в C.',
'Поставить unit tests на валидные и граничные значения, затем прогнать sanitizers или инструменты платформы.',
'Оставить публичную функцию @safe только при доказанном безопасном интерфейсе; остальную зону явно маркировать @system.',
]),
h2('Отсутствие evidence тоже нужно маркировать'),
p('Нельзя заменить отсутствие evidence нейтральным тоном. Фразы «на практике оказалось», «в сервисе видно» и «после повторного profile» звучат как обычная связка, но здесь они создают ложный факт. Даже если будущий reader легко вообразит такой путь, он не становится частью P109. У draft нет hidden fixture, приватной конфигурации, замера «для себя» или негласного решения. Если в 2027 году появится реальный материал, он должен жить в новом artefact с новой датой, scope, source set и ответственностью за данные.'),
p('То же касается слов о release. Официальный D 2.112.0 от 07 January 2026 находится до cutoff и может быть указан как pinned primary material; однако это не означает, что именно эта версия будет взята будущей командой. Любая более поздняя версия, даже если когда-нибудь станет известна, не принадлежит этому тексту. Полезная передача не пытается быть живой сводкой. Она хранит границу знаний так, чтобы позднее обновление не маскировалось под то, что якобы было известно заранее.'),
h2('Fail-closed оставляет выбор получателю'),
p('Если workload или constraint неназваны, получатель не знает, что сравнивать и какое несоответствие важно. Если benchmark configuration скрыта, получатель не может судить о методе, но может ошибочно довериться числу. Если conclusion объявляет winner, ему остаётся только спорить с результатом, которого нет. Поэтому эти случаи не получают default-значения и не продолжаются «с оговорками». Validator возвращает precise stop, сохраняя возможность переписать вопрос до того, как он станет ложной задачей в чужой очереди.'),
p('Fail-closed не говорит, что реальное исследование невозможно. Он говорит ровно обратное: для реального исследования нужен новый владелец evidence, явное право на данные и отдельная методика. У получателя остаётся несколько законных ответов: отклонить вопрос как неважный, запросить уточнение constraints, открыть discovery scope или согласовать transparent benchmark. Ни один из них не должен быть неявно выбран текстом. Это отличие hand-off от скрытого roadmap: hand-off передаёт неопределённость вместе с её границей.'),
h2('Runnable contract передачи'),
code("import { createFixedRuntimeCase, assessFixedRuntimePlan } from './upgrade-2027-03.mjs';\n\nconst rejected = createFixedRuntimeCase('disallowed-winner-v1');\nconst refusal = assessFixedRuntimePlan(rejected);\nif (refusal.productionEffect !== 'not-attempted') throw new Error('unexpected-effect');\nconsole.log(refusal.status);\n// stop-disallowed-winner-or-positive-result"),
p('Этот runnable example deliberately demonstrates a refusal, not a success claim. Все значения fixed and in-memory; JSON clone отделяет literal от caller, deep freeze закрепляет nested workload, constraints и benchmark. Evaluator не обращается к D compiler, runtime, shell, files, network, environment, telemetry или clock. Его output не может стать deploy decision: productionEffect остаётся not-attempted и на stop, и на hand-off. Такой маленький contract полезен, потому что его поведение можно повторить без поддельного сервиса.'),
h2('Порядок synthetic evidence hand-off'),
ol(['Проверить, что hand-off явно относится к плану 2027-03 и сохраняет cutoff 2026-07-31.', 'Передать только named synthetic workload; не добавлять service name, profile, data sample или наблюдение.', 'Передать named constraints как вопросы, не как уже согласованные SLA, ownership или delivery policy.', 'Оставить benchmark not-run и configuration/result not-collected; любой hidden value вернуть с stop.', 'Не называть D, другой runtime или конфигурацию победителем и не писать positive result.', 'Отдать future evidence owner boundary и nextAction; решение об authorised scope остаётся за ним.']),
h2('Что будущий owner должен проверить отдельно'),
p('Если owner возьмёт вопрос, ему придётся создать собственный contract: что именно является workload, какие inputs разрешены, какую информацию нельзя сохранять, какие версии и target проверяются, как устроен baseline и кто читает результат. Нужно отдельно решить, является ли выбранная операция representative, какие ошибки измерения приемлемы и какой исход остановит работу. Эти решения не переносятся из P109, потому что P109 их не принимал. В этом и смысл honest hand-off: не выдавать пустой шаблон за готовую методику.'),
p('Возможен и более короткий ответ: owner может решить, что сравнение не нужно, а проблема решается контрактом, кэшированием, разделением владения или изменением текущего пути. Такой итог не делает D «плохим» и не требует защитить альтернативу. Он лишь показывает, что конкретный runtime-question не получил достаточного основания. Хорошая field-передача допускает этот исход заранее; плохая превращает отсутствие запуска в давление «раз уж начали, надо доказать пользу». '),
h2('Какие слова нельзя передавать по инерции'),
p('Получателю стоит отдельно вычёркивать слова, которые создают несуществующий факт: «повторили», «подтвердили», «профиль показал», «сервис выдержал», «runtime выбран», «релиз вышел» и «победитель». В обычной заметке они могут быть точными, если рядом есть material и scope. В этом hand-off они меняют тип документа. Проблема не в уверенном тоне, а в том, что поздний reader уже не отличит заготовку от свидетельства и начнёт передавать эту подмену дальше.'),
p('Вместо этого допустимы более точные конструкции: «планируется назвать», «будущему owner требуется решить», «configuration не собрана», «результат не получен», «вариант не выбран». Они не звучат эффектно, зато оставляют следующее действие там, где оно действительно может быть принято. У future owner появляется свобода изменить весь вопрос, а не только подобрать параметр к заранее обещанному benchmark. Это особенно ценно в прикладной работе, где контекст меняется быстрее, чем редакционный план.'),
h2('Ограничения hand-off и следующий шаг'),
p('Этот план не создаёт ticket, queue, benchmark configuration, owner, approval или production change. Он не проверяет compatibility, безопасность, доступность, стоимость, vendor support, licenses или performance. Публикации D 2.110.0, 2.111.0 и 2.112.0 помогают точно назвать датированные release materials, но не делают их evidence для неназванной системы. Здесь нет даже claim, что D будет рассмотрен в 2027-03: есть лишь сценарий того, как не выдать ожидание за результат.'),
p('Следующий шаг — хранить this draft as a synthetic hand-off until a future owner explicitly authorises different work. Тогда новый artefact может содержать реальные данные и проверяемый вывод, но должен отличаться датой и provenance. До этого момента safest conclusion уже записан: question contract можно передать, production effect не пытались получить. Полевой урок для прикладного инженера не про красивый кейс; он про способность оставить следующему человеку честно ограниченное знание вместо убедительной, но вымышленной истории.'),
], refs);
h2('Ограничения и следующий шаг'),
p('Проверка capacity в JavaScript — учебная модель числовой границы, а не анализ D-памяти. Она не видит aliasing, alignment, calling convention, null termination или освобождение в C. Даже корректный @safe wrapper может передать семантически неверный enum или структуру. Нужен compile-time и runtime тест именно тем компилятором и ABI, с которыми собирается продукт.'),
p('Следующий шаг — выбрать один extern(C) вызов и оформить для него таблицу: pointer, length, ownership, error, thread-safety. Напишите маленький wrapper, который принимает D slice, и отдельно проведите review trusted-тела. Если один из пунктов не имеет ответа, вызов нельзя считать готовым к безопасной границе.'),
], [
{ key: 'memory', use: 'Определения @safe, @trusted, @system, scope и границы memory safety.', boundary: 'Спецификация не проверяет контракт внешней C-библиотеки и не гарантирует portability или отсутствие логических ошибок.' },
{ key: 'functions', use: 'Правила function attributes и contract expressions используются для precondition и postcondition.', boundary: 'Документация не создаёт unit tests и не решает lifetime конкретного объекта.' },
{ key: 'cInterface', use: 'Интерфейс D/C и отдельные соглашения вызова используются для описания wrapper boundary.', boundary: 'Страница не подтверждает прототип, ABI и ownership неизвестной библиотеки.' },
]);
const field = revision({
slug: 'editorial-2027-03-field-d-lessons',
title: 'D и C ABI: разобрать пакет до вызова',
categories: ['D', 'Интеграции'],
cover: '/assets/editorial/2027/d-lessons-2027-evidence-handoff-loop.svg',
excerpt: 'Полевой разбор FFI-ошибки: размер структуры, порядок байтов и обязательные поля проверяются до вызова C.',
readingMinutes: 15,
}, [
p('Проблема C ABI редко выглядит как ошибка на строке вызова. D-программа передаёт структуру, C читает её и возвращает код, но значение поля оказывается неверным или процесс падает только на одной архитектуре. Цена — непредсказуемый сбой на границе, где обычный unit test видит только один компилятор и одну раскладку памяти. Чем дольше ошибка живёт, тем труднее отличить формат данных от ошибки бизнес-логики.'),
p('Полевой разбор начинается с пакета, который реально пересекает ABI: byte length, endianness, поля, alignment и calling convention. Дальше нужно сопоставить его с C header и настройками компилятора. Нельзя проверять только имя struct. Два типа с одинаковыми полями могут иметь разный padding, порядок байтов или размер указателя. Пакет — это физический контракт, а не только исходный текст.'),
h2('Размер структуры — первый стоп'),
p('ABI определяет, как типы и функции представлены для взаимодействия с машинным кодом. Для структуры важны не только поля, но и выравнивание. Добавленный <code>int</code> может изменить offsets следующего поля; на 32- и 64-битной платформе размер указателя различается. Если D и C собраны с разными ожиданиями, чтение смещается, а ошибка проявится как «неверное значение» далеко от причины.'),
p('Порядок байтов — отдельная ось. Файл может быть little-endian, а внешний протокол — big-endian; автоматическое копирование структуры не является преобразованием формата. Для числового поля запишите wire representation и проверяйте её на известном значении вроде <code>0x01020304</code>. Так видно, поменялись байты или перепутана длина пакета.'),
figure('/assets/editorial/2027/d-lessons-2027-evidence-handoff-loop.svg', 'Цикл проверки D и C ABI: пакет, размер, поля и порядок байтов проходят сопоставление с контрактом до вызова функции.', 'Схема отделяет физическую проверку пакета от самого вызова. Красная ветка возвращает данные на границу, если размер или поле не совпали.'),
table('Проверка ABI-пакета до FFI-вызова', ['Проверка', 'Пример входа', 'Ожидаемое действие', 'Если пропустить'], [
['byte length', '24 байта', 'сверить sizeof на обеих сторонах', 'смещение полей'],
['endianness', 'little-endian', 'декодировать число явно', 'неверный id или размер'],
['field set', 'version, flags, payload', 'проверить обязательные поля', 'чтение мусора'],
['alignment', 'offset 8 вместо 4', 'сверить compiler layout', 'сбой на другой архитектуре'],
['error code', '0 или отрицательное значение', 'перевести в D-ошибку', 'успех при частичном чтении'],
]),
h2('Учебный валидатор пакета'),
p('Локальная функция ниже принимает пакет и описание ожидаемой структуры. Она проверяет три вещи, которые можно увидеть ещё до вызова: размер, порядок байтов и набор полей. Числа и названия в примере учебные, но сам порядок повторяет рабочую проверку. В реальном проекте contract строится из header и результатов компилятора, а не из догадки автора wrapper.'),
code(`import { validateCAbiPacket } from './upgrade-2027-03.mjs';
const contract = {
byteLength: 12,
endianness: 'little',
fields: [{ name: 'version' }, { name: 'flags' }, { name: 'payload' }],
};
const packet = { byteLength: 12, endianness: 'little', version: 2, flags: 1, payload: 4096 };
const broken = { byteLength: 16, endianness: 'big', version: 2, flags: 1 };
console.log(validateCAbiPacket(packet, contract));
console.log(validateCAbiPacket(broken, contract));
// { accepted: true, errors: [] }
// { accepted: false, errors: ['размер структуры', 'порядок байтов', 'поле payload'] }`),
p('Валидатор не вызывает C и поэтому не доказывает, что ABI корректен. Он делает видимыми три несовпадения до опасной операции. Для полноценного теста добавьте golden bytes, сборку маленького C helper и проверку результата на каждой целевой архитектуре. Смысл локального примера в том, что ошибка в contract table должна быть заметна раньше падения процесса.'),
h2('Где заканчивается автоматическая проверка'),
p('Размер и поля можно сравнить автоматически, но ownership и смысл flags требуют чтения C API. Поле <code>payload</code> может быть указателем, длиной или offset внутри того же пакета. Значение <code>0</code> может означать пусто, success или null. Не называйте результат «валидным пакетом», пока не проверены эти семантические значения и код ошибки.'),
p('Особенно опасен частичный успех. C-функция могла записать структуру, но вернуть ошибку; D-код видит заполненное поле и продолжает обработку. Обёртка должна сначала проверить код возврата, затем интерпретировать output по версии и только потом отдавать его доменному коду. Версия пакета должна быть частью ключа выбора декодера, а не обычным полем, которое можно проигнорировать.'),
h2('Действия по порядку'),
ol([
'Сохранить точный C header, compiler flags, target architecture и calling convention рядом с исходником wrapper.',
'Составить layout table с размером, offset, alignment, типом и смыслом каждого поля.',
'Проверить golden bytes для little/big-endian и граничных значений длины до вызова внешней функции.',
'Разделить код возврата, output и ошибку; не считать частично заполненную структуру успешным результатом.',
'Собрать тест на каждой поддерживаемой архитектуре и сохранить hex-пакет, версию контракта и итог проверки.',
]),
h2('Ограничения и следующий шаг'),
p('JavaScript-валидатор не моделирует padding, pointer alignment, compiler lowering и реальные байты. Он проверяет форму contract table, поэтому не заменяет C helper, D compiler и тест на целевом ABI. Документ D описывает правила языка, но конкретный vendor header может добавлять свои packing directives, версии и ownership соглашения.'),
p('Следующий шаг — выбрать один extern(C) вызов, получить маленький golden packet и сравнить layout D/C в автоматической проверке. В отчёте оставьте hex, размер, архитектуру и код возврата. Это позволит отличить изменение компилятора от изменения данных и быстро вернуть ошибку к физической границе.'),
], [
{ key: 'abi', use: 'Правила D ABI используются для объяснения layout, alignment и представления типов.', boundary: 'Спецификация не знает vendor header, compiler flags и архитектуру конкретного проекта.' },
{ key: 'cInterface', use: 'Правила extern(C) и взаимодействия с C используются для выбора contract table и calling convention.', boundary: 'Документация не гарантирует корректность неизвестного прототипа или ownership.' },
{ key: 'memory', use: 'Граница memory safety используется для отделения безопасного анализа пакета от raw pointer операций.', boundary: 'Проверка памяти не подтверждает семантику полей и код возврата внешней библиотеки.' },
]);
export const revisions = Object.freeze([practice, mechanism, field]);
export function verifyRevisionsAgainstFixture() {
const checks = revisions.map((item) => {
const body = bodyText(item.contentHtml);
return body.length >= 5000 && body.length <= 15000 && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && !/(synthetic-plan-hand-off|productionEffect|future-only|plan\/scenario|source cutoff|not-collected|not-attempted|future owner|развитие автора)/i.test(body);
});
const workload = validateDWorkload({ throughput: 12000, latencyBudgetMs: 20, nativeBoundary: true, deploymentTargets: 1 });
const abi = validateCAbiPacket({ byteLength: 4, endianness: 'little', version: 1 }, { byteLength: 4, endianness: 'little', fields: [{ name: 'version' }] });
const fixtureOk = workload.decision === 'consider-d' && abi.accepted;
return Object.freeze({ passed: checks.filter(Boolean).length + (fixtureOk ? 1 : 0), total: checks.length + 1, accepted: checks.every(Boolean) && fixtureOk, 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;
}
export const revisions = deepFreeze([practice, mechanism, field]);
export function verifyRevisionsAgainstFixture() { const fixture = runFixedRuntimeFixture(); 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-03/.test(text) && /2026-07-31/.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');