revise September 2025 engineering interview articles
Build and deploy / deploy (push) Successful in 20s

This commit is contained in:
2026-07-31 18:24:57 +03:00
parent 513468c63d
commit 195477c0e4
7 changed files with 357 additions and 1 deletions
+2
View File
@@ -87,6 +87,7 @@ import { revisions as may2025Revisions } from '../scripts/upgrade-2025-05.mjs';
import { revisions as june2025Revisions } from '../scripts/upgrade-2025-06.mjs';
import { revisions as july2025Revisions } from '../scripts/upgrade-2025-07.mjs';
import { revisions as august2025Revisions } from '../scripts/upgrade-2025-08.mjs';
import { revisions as september2025Revisions } from '../scripts/upgrade-2025-09.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -179,4 +180,5 @@ export const editorialRevisions = [
...june2025Revisions,
...july2025Revisions,
...august2025Revisions,
...september2025Revisions,
];
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="560" viewBox="0 0 960 560" role="img" aria-labelledby="title desc">
<title id="title">Матрица калибровки reviewer-ов</title>
<desc id="desc">Таблица отделяет observation, interpretation и hand-off. Каждой строке соответствует допустимое evidence и стоп-сигнал.</desc>
<rect width="960" height="560" fill="#111827"/>
<text x="50" y="65" fill="#f9fafb" font-family="Arial, sans-serif" font-size="30" font-weight="700">Калибровка: спорим о ссылке, не о впечатлении</text>
<g font-family="Arial, sans-serif" font-size="18">
<rect x="50" y="112" width="860" height="72" fill="#1e3a8a"/>
<text x="76" y="156" fill="#dbeafe" font-weight="700">СЛОЙ</text><text x="276" y="156" fill="#dbeafe" font-weight="700">ДОПУСТИМО</text><text x="570" y="156" fill="#dbeafe" font-weight="700">СТОП-СИГНАЛ</text>
<rect x="50" y="184" width="860" height="88" fill="#172554" stroke="#334155"/>
<text x="76" y="232" fill="#bfdbfe" font-weight="700">OBSERVATION</text><text x="276" y="222" fill="#e5e7eb">fact + evidence ref</text><text x="276" y="248" fill="#e5e7eb">+ explicit unknown</text><text x="570" y="232" fill="#fca5a5">real data or personal inference</text>
<rect x="50" y="272" width="860" height="88" fill="#064e3b" stroke="#334155"/>
<text x="76" y="320" fill="#99f6e4" font-weight="700">INTERPRETATION</text><text x="276" y="310" fill="#e5e7eb">narrow claim +</text><text x="276" y="336" fill="#e5e7eb">observation ids</text><text x="570" y="320" fill="#fca5a5">cause stated as fact</text>
<rect x="50" y="360" width="860" height="88" fill="#713f12" stroke="#334155"/>
<text x="76" y="408" fill="#fde68a" font-weight="700">HAND-OFF</text><text x="276" y="398" fill="#e5e7eb">request missing evidence</text><text x="276" y="424" fill="#e5e7eb">or stop</text><text x="570" y="408" fill="#fca5a5">score, ranking, hiring outcome</text>
</g>
<text x="50" y="505" fill="#94a3b8" font-family="Arial, sans-serif" font-size="16">Одинаковый criterion не требует одинакового мнения: он требует восстанавливаемого основания.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="560" viewBox="0 0 960 560" role="img" aria-labelledby="title desc">
<title id="title">Цикл evidence review</title>
<desc id="desc">Fixed sample проходит независимые observations, интерпретации со ссылками и журнал расхождений. Итогом является только запрос доказательства или остановка.</desc>
<rect width="960" height="560" fill="#0b1120"/>
<text x="52" y="70" fill="#f8fafc" font-family="Arial, sans-serif" font-size="30" font-weight="700">Цикл review возвращает неизвестное в работу</text>
<g font-family="Arial, sans-serif" font-size="18" text-anchor="middle">
<circle cx="180" cy="292" r="92" fill="#172554" stroke="#60a5fa" stroke-width="3"/><text x="180" y="282" fill="#bfdbfe" font-weight="700">FIXED</text><text x="180" y="308" fill="#bfdbfe" font-weight="700">SAMPLE</text>
<circle cx="390" cy="292" r="92" fill="#134e4a" stroke="#5eead4" stroke-width="3"/><text x="390" y="282" fill="#99f6e4" font-weight="700">INDEPENDENT</text><text x="390" y="308" fill="#99f6e4" font-weight="700">OBSERVATIONS</text>
<circle cx="600" cy="292" r="92" fill="#4c1d95" stroke="#c4b5fd" stroke-width="3"/><text x="600" y="282" fill="#ddd6fe" font-weight="700">LINKED</text><text x="600" y="308" fill="#ddd6fe" font-weight="700">INTERPRETATION</text>
<circle cx="810" cy="292" r="92" fill="#713f12" stroke="#fbbf24" stroke-width="3"/><text x="810" y="282" fill="#fde68a" font-weight="700">REQUEST</text><text x="810" y="308" fill="#fde68a" font-weight="700">OR STOP</text>
</g>
<g stroke-width="5" fill="none"><path d="M272 292H292" stroke="#60a5fa"/><path d="M482 292H502" stroke="#5eead4"/><path d="M692 292H712" stroke="#c4b5fd"/></g>
<path d="M810 392 C810 500 180 500 180 392" fill="none" stroke="#fbbf24" stroke-width="4" stroke-dasharray="10 9"/>
<text x="495" y="510" fill="#94a3b8" font-family="Arial, sans-serif" font-size="16" text-anchor="middle">Новая проверка начинается только с недостающего факта; outcome о человеке отсутствует.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="560" viewBox="0 0 960 560" role="img" aria-labelledby="title desc">
<title id="title">Рубрика и рабочая проба</title>
<desc id="desc">Три наблюдаемых критерия ограничивают synthetic рабочую пробу. Из факта и неизвестного получается только запрос недостающего доказательства.</desc>
<rect width="960" height="560" fill="#0f172a"/>
<text x="60" y="72" fill="#f8fafc" font-family="Arial, sans-serif" font-size="30" font-weight="700">Рубрика задаёт границу работы</text>
<text x="60" y="108" fill="#cbd5e1" font-family="Arial, sans-serif" font-size="18">fixed synthetic sample · без оценки человека</text>
<g font-family="Arial, sans-serif">
<rect x="60" y="164" width="248" height="250" rx="18" fill="#172554" stroke="#60a5fa" stroke-width="2"/>
<text x="88" y="210" fill="#bfdbfe" font-size="22" font-weight="700">ROLE RUBRIC</text>
<text x="88" y="252" fill="#e2e8f0" font-size="17">1. граница evidence</text>
<text x="88" y="288" fill="#e2e8f0" font-size="17">2. безопасность change</text>
<text x="88" y="324" fill="#e2e8f0" font-size="17">3. связь symptom → step</text>
<text x="88" y="370" fill="#fca5a5" font-size="15">исключено: память, личность,</text>
<text x="88" y="392" fill="#fca5a5" font-size="15">итог о человеке</text>
<path d="M324 289H404" stroke="#93c5fd" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="420" y="164" width="230" height="250" rx="18" fill="#134e4a" stroke="#5eead4" stroke-width="2"/>
<text x="448" y="210" fill="#99f6e4" font-size="22" font-weight="700">WORK SAMPLE</text>
<text x="448" y="252" fill="#e2e8f0" font-size="17">header literal</text>
<text x="448" y="288" fill="#e2e8f0" font-size="17">route literal</text>
<text x="448" y="324" fill="#e2e8f0" font-size="17">rollback note</text>
<text x="448" y="370" fill="#a7f3d0" font-size="15">origin rule: unknown</text>
<path d="M666 289H746" stroke="#5eead4" stroke-width="5" marker-end="url(#arrow)"/>
<rect x="762" y="164" width="150" height="250" rx="18" fill="#713f12" stroke="#fbbf24" stroke-width="2"/>
<text x="790" y="210" fill="#fde68a" font-size="20" font-weight="700">HAND-OFF</text>
<text x="790" y="270" fill="#f8fafc" font-size="16">request</text>
<text x="790" y="296" fill="#f8fafc" font-size="16">missing</text>
<text x="790" y="322" fill="#f8fafc" font-size="16">evidence</text>
<text x="790" y="370" fill="#fde68a" font-size="14">or STOP</text>
</g>
<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#93c5fd"/></marker></defs>
<text x="60" y="500" fill="#94a3b8" font-family="Arial, sans-serif" font-size="16">Наблюдение не становится интерпретацией, пока не названо неизвестное.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+245
View File
@@ -0,0 +1,245 @@
function escapeHtml(value) {
return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function plainText(content) { return content.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
function bodyText(content) { return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
function deepFreeze(value) { if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(deepFreeze); Object.freeze(value); } return value; }
function revision(meta, parts) {
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + sourceList();
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength);
return deepFreeze({ ...meta, contentHtml, proseLength });
}
const sources = deepFreeze([
{
title: 'U.S. Office of Personnel Management: Structured Interviews — A Practical Guide',
url: 'https://web.archive.org/web/20130625142525id_/http://www.opm.gov:80/policy-data-oversight/assessment-and-selection/structured-interviews/guide.pdf',
version: 'immutable Internet Archive PDF capture 2013-06-25T14:25:25Z; document dated September 2008',
claim: 'Руководство описывает связку анализа работы, компетенций, вопросов, общих шкал, индивидуальных оценок и обсуждения расхождений.',
boundary: 'Это историческое руководство федерального ведомства США. Оно не подтверждает пригодность конкретной роли, не задаёт policy другой организации и не доказывает исход реального отбора.',
},
{
title: 'U.S. Office of Personnel Management: Structured Interview Example',
url: 'https://web.archive.org/web/20130227005426id_/https://opm.gov/policy-data-oversight/assessment-and-selection/examples/structured-interview-example.pdf',
version: 'immutable Internet Archive PDF capture 2013-02-27T00:54:26Z of official OPM example',
claim: 'Пример показывает, что вопрос и признаки ответа можно хранить рядом, а не заменять их общим впечатлением reviewer-а.',
boundary: 'Короткий пример не является готовой рубрикой для инженерной работы и не обосновывает автоматическую либо персональную оценку.',
},
{
title: 'U.S. Office of Personnel Management: Writing Samples Summary',
url: 'https://web.archive.org/web/20140327034249id_/http://www.opm.gov:80/policy-data-oversight/assessment-and-selection/other-assessment-methods/writing-samples-summary.pdf',
version: 'immutable Internet Archive PDF capture 2014-03-27T03:42:49Z of official OPM summary',
claim: 'Сводка различает портфолио и заданную работу, отмечая типичность задачи и одинаковые условия review как свойства рабочей пробы.',
boundary: 'Источник относится к письменным образцам и не переносит выводы на инженерное интервью, конкретных людей или любой процесс другой команды.',
},
]);
function sourceList() { return '<ul>' + sources.map((source) => '<li><a href="' + source.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(source.title) + '</a> — версия: ' + escapeHtml(source.version) + '. ' + escapeHtml(source.claim) + ' Граница: ' + escapeHtml(source.boundary) + '</li>').join('') + '</ul>'; }
const MODEL_VERSION = 'p91-engineering-interviews-fixed-synthetic-v1';
const SYNTHETIC_BOUNDARY = 'Все role cards, work samples, observations, interpretations, review records, calibration notes и hand-off ниже — fixed synthetic JavaScript literals только в памяти. Модуль не читает и не пишет files, не использует сеть, PII, audio, clock, telemetry, Git, CI или production side effect. Он не оценивает человека и не создаёт hiring outcome.';
const ROLE_KEYS = ['id', 'syntheticOnly', 'workBoundary', 'uncertainty', 'reviewDimensions', 'excludedDimensions'];
const SAMPLE_KEYS = ['id', 'syntheticOnly', 'roleId', 'startingState', 'task', 'allowedEvidence', 'forbiddenEvidence', 'stopCondition'];
const OBSERVATION_KEYS = ['id', 'syntheticOnly', 'sampleId', 'fact', 'evidenceRef', 'unknown'];
const INTERPRETATION_KEYS = ['id', 'syntheticOnly', 'observationIds', 'dimensionId', 'claim', 'uncertainty'];
const REVIEW_KEYS = ['id', 'syntheticOnly', 'interpretationIds', 'criterionId', 'decision', 'boundary'];
function isPlainRecord(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null); }
function hasKeys(value, keys) { return isPlainRecord(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key)); }
const FIXED_ROLE = deepFreeze({
id: 'synthetic-role-service-change-v1', syntheticOnly: true,
workBoundary: 'Разобрать изменение в сервисе с неполным входом и оставить проверяемый plan; не писать production code и не принимать решение о человеке.',
uncertainty: 'Исходные данные намеренно неполны: известен symptom, но неизвестны причина, владелец интеграции и допустимый rollback.',
reviewDimensions: [
{ id: 'evidence-boundary', label: 'граница доказательства', observable: 'называет, что известно, чего нет и какой факт нужен дальше' },
{ id: 'change-safety', label: 'безопасность изменения', observable: 'отделяет обратимое действие от необратимого и называет stop condition' },
{ id: 'technical-communication', label: 'техническая коммуникация', observable: 'связывает symptom, проверку и следующий шаг без жаргона вместо причины' },
],
excludedDimensions: ['личность', 'темперамент', 'культурное совпадение', 'память на термин', 'hiring recommendation'],
});
const FIXED_SAMPLE = deepFreeze({
id: 'synthetic-work-sample-cache-header-v1', syntheticOnly: true, roleId: FIXED_ROLE.id,
startingState: 'В synthetic карточке API ответ помечен stale, а origin rule неизвестен. Разрешены только три literals: response header, route name и rollback note.',
task: 'Составить короткий investigation plan: observation, unknown, безопасная проверка и условие остановки. Не исправлять ответ и не выбирать человека.',
allowedEvidence: ['fixed-response-header', 'fixed-route-name', 'fixed-rollback-note'],
forbiddenEvidence: ['real-repository', 'network', 'personal-history', 'audio', 'personality-inference'],
stopCondition: 'Остановиться, если следующее действие требует незаданного доступа, реальных данных или предположения о намерениях.',
});
const FIXED_OBSERVATIONS = deepFreeze([
{ id: 'synthetic-observation-header-v1', syntheticOnly: true, sampleId: FIXED_SAMPLE.id, fact: 'В fixed header literal присутствует Cache-Control: max-age=0, но не указан источник stale response.', evidenceRef: 'fixed-response-header', unknown: 'Нельзя установить origin rule, историю изменения или частоту симптома.' },
{ id: 'synthetic-observation-route-v1', syntheticOnly: true, sampleId: FIXED_SAMPLE.id, fact: 'В fixed route literal указан /v1/report, а owner интеграции не задан.', evidenceRef: 'fixed-route-name', unknown: 'Нельзя назначить владельца или выполнить запрос к маршруту.' },
]);
const FIXED_INTERPRETATIONS = deepFreeze([
{ id: 'synthetic-interpretation-boundary-v1', syntheticOnly: true, observationIds: ['synthetic-observation-header-v1'], dimensionId: 'evidence-boundary', claim: 'Есть основание запросить origin rule; нет основания объяснять stale response конкретной причиной.', uncertainty: 'Один header literal не показывает путь между origin и consumer.' },
{ id: 'synthetic-interpretation-safety-v1', syntheticOnly: true, observationIds: ['synthetic-observation-header-v1', 'synthetic-observation-route-v1'], dimensionId: 'change-safety', claim: 'Следующий шаг должен быть read-only investigation plan с stop condition, а не change.', uncertainty: 'В карточке нет прав, окружения и rollback процедуры.' },
]);
const FIXED_REVIEWS = deepFreeze([
{ id: 'synthetic-review-boundary-v1', syntheticOnly: true, interpretationIds: ['synthetic-interpretation-boundary-v1'], criterionId: 'evidence-boundary', decision: 'hand-off: request-missing-origin-rule', boundary: 'Не содержит вывода о компетентности или решении о человеке.' },
{ id: 'synthetic-review-safety-v1', syntheticOnly: true, interpretationIds: ['synthetic-interpretation-safety-v1'], criterionId: 'change-safety', decision: 'hand-off: preserve-stop-condition', boundary: 'Не разрешает запрос, код, деплой или production change.' },
]);
const FIXED_CASES = deepFreeze({
'fixed-valid-v1': { role: FIXED_ROLE, sample: FIXED_SAMPLE, observations: FIXED_OBSERVATIONS, interpretations: FIXED_INTERPRETATIONS, reviews: FIXED_REVIEWS },
'fixed-memory-test-v1': { role: { ...FIXED_ROLE, excludedDimensions: FIXED_ROLE.excludedDimensions.filter((item) => item !== 'память на термин') }, sample: FIXED_SAMPLE, observations: FIXED_OBSERVATIONS, interpretations: FIXED_INTERPRETATIONS, reviews: FIXED_REVIEWS },
'fixed-unsupported-interpretation-v1': { role: FIXED_ROLE, sample: FIXED_SAMPLE, observations: FIXED_OBSERVATIONS, interpretations: [{ ...FIXED_INTERPRETATIONS[0], observationIds: ['missing-observation'] }], reviews: [FIXED_REVIEWS[0]] },
'fixed-personal-outcome-v1': { role: FIXED_ROLE, sample: FIXED_SAMPLE, observations: FIXED_OBSERVATIONS, interpretations: FIXED_INTERPRETATIONS, reviews: [{ ...FIXED_REVIEWS[0], decision: 'hiring-recommendation' }] },
'fixed-real-evidence-v1': { role: FIXED_ROLE, sample: { ...FIXED_SAMPLE, allowedEvidence: ['fixed-response-header', 'real-repository'] }, observations: FIXED_OBSERVATIONS, interpretations: FIXED_INTERPRETATIONS, reviews: FIXED_REVIEWS },
});
function inspectSyntheticInterview(input) {
if (!input || !Object.values(FIXED_CASES).includes(input)) return deepFreeze({ accepted: false, reasons: ['unrecognised-fixed-input'], handOff: 'stop', productionEffect: 'not-attempted' });
const reasons = [];
const { role, sample, observations, interpretations, reviews } = input;
if (!hasKeys(role, ROLE_KEYS) || !role.syntheticOnly || !role.excludedDimensions.includes('память на термин') || !role.excludedDimensions.includes('hiring recommendation')) reasons.push('role-rubric-must-exclude-memory-and-outcome');
if (!hasKeys(sample, SAMPLE_KEYS) || sample.roleId !== role.id || sample.allowedEvidence.some((item) => !item.startsWith('fixed-')) || !sample.forbiddenEvidence.includes('real-repository')) reasons.push('work-sample-boundary-invalid');
if (!Array.isArray(observations) || observations.some((item) => !hasKeys(item, OBSERVATION_KEYS) || item.sampleId !== sample.id || !sample.allowedEvidence.includes(item.evidenceRef) || !item.unknown)) reasons.push('observation-must-be-fixed-and-unknown-explicit');
const observationIds = new Set((observations || []).map((item) => item.id));
if (!Array.isArray(interpretations) || interpretations.some((item) => !hasKeys(item, INTERPRETATION_KEYS) || !item.observationIds.every((id) => observationIds.has(id)) || !role.reviewDimensions.some((dimension) => dimension.id === item.dimensionId) || !item.uncertainty)) reasons.push('interpretation-not-traceable');
const interpretationIds = new Set((interpretations || []).map((item) => item.id));
if (!Array.isArray(reviews) || reviews.some((item) => !hasKeys(item, REVIEW_KEYS) || !item.interpretationIds.every((id) => interpretationIds.has(id)) || !role.reviewDimensions.some((dimension) => dimension.id === item.criterionId) || !item.decision.startsWith('hand-off:') || /hiring|person/i.test(item.decision))) reasons.push('review-must-stop-at-synthetic-hand-off');
return deepFreeze({ scope: MODEL_VERSION, accepted: reasons.length === 0, reasons, handOff: reasons.length ? 'stop-and-repair-boundary' : 'request-missing-evidence-only', productionEffect: 'not-attempted' });
}
export function createFixedInterviewInput(caseId) { return FIXED_CASES[caseId] || null; }
export function inspectEngineeringInterview(input) { return inspectSyntheticInterview(input); }
export function prepareSyntheticInterviewHandOff(report) { return deepFreeze({ kind: 'fixed-synthetic-engineering-interview-hand-off-v1', accepted: report?.accepted === true, status: report?.accepted ? 'request-missing-evidence-only' : 'stop-and-repair-boundary', productionEffect: 'not-attempted' }); }
export function runEngineeringInterviewFixture() {
const valid = inspectSyntheticInterview(FIXED_CASES['fixed-valid-v1']);
const memory = inspectSyntheticInterview(FIXED_CASES['fixed-memory-test-v1']);
const unsupported = inspectSyntheticInterview(FIXED_CASES['fixed-unsupported-interpretation-v1']);
const outcome = inspectSyntheticInterview(FIXED_CASES['fixed-personal-outcome-v1']);
const realEvidence = inspectSyntheticInterview(FIXED_CASES['fixed-real-evidence-v1']);
return deepFreeze({ assertions: deepFreeze({ validAccepted: valid.accepted, handOffOnly: prepareSyntheticInterviewHandOff(valid).status === 'request-missing-evidence-only', validNoProduction: valid.productionEffect === 'not-attempted', memoryStops: memory.reasons.includes('role-rubric-must-exclude-memory-and-outcome'), unsupportedStops: unsupported.reasons.includes('interpretation-not-traceable'), outcomeStops: outcome.reasons.includes('review-must-stop-at-synthetic-hand-off'), realEvidenceStops: realEvidence.reasons.includes('work-sample-boundary-invalid'), roleFrozen: Object.isFrozen(FIXED_ROLE), sampleFrozen: Object.isFrozen(FIXED_SAMPLE), observationsFrozen: Object.isFrozen(FIXED_OBSERVATIONS), handOffNoProduction: prepareSyntheticInterviewHandOff(valid).productionEffect === 'not-attempted' }) });
}
const practice = revision({ slug: 'editorial-2025-09-practice-engineering-interviews', title: 'Инженерное интервью: рубрика и рабочая проба вместо экзамена по терминам', categories: ['Команда', 'Интервью'], cover: '/assets/editorial/2025/engineering-interviews-2025-rubric-work-sample.svg', excerpt: 'Практический маршрут для технического разговора: ограниченная роль, рабочая проба, наблюдаемые критерии и synthetic hand-off без оценки человека.', readingMinutes: 13 }, [
p('Инженерный разговор часто ломается ещё до первого вопроса: ведущий разбора (interviewer) спрашивает определение термина, получает уверенный ответ и принимает его за способ работы. Симптом заметен в разборе: нельзя показать, какой факт человек отделил от догадки и где остановился бы без доступа. Цена ошибки — несколько часов команды уходят на спор о впечатлении, а следующая техническая задача снова приходит без проверяемого плана.'),
p('Причина не в том, что терминов стало мало. У разговора нет role rubric — короткого контракта о наблюдаемой работе. Проверка проста: дать одну ограниченную рабочую пробу с неполным входом, заранее записать критерий и запретить вывод, которого evidence не поддерживает. Действие — закончить упражнение только synthetic hand-off, то есть учебным запросом недостающего факта, а не оценкой личности или исходом найма.'),
h2('Рубрика начинается с работы, а не с качеств человека'),
p('Для начала полезно убрать слова «сильный инженер», «подходит команде» и «хорошо рассуждает». Это ярлыки: два reviewer-а вкладывают в них разные наблюдения и не могут восстановить решение через неделю. Рубрика вместо этого называет границу работы. В нашем учебном случае нужно разобрать stale ответ API с тремя fixed literals. Неизвестны правило origin, владелец интеграции и безопасный rollback. Значит, проверяем не память о cache header, а порядок: назвать известное, запросить недостающее, не выдавать change за проверку.'),
figure('/assets/editorial/2025/engineering-interviews-2025-rubric-work-sample.svg', 'Схема synthetic инженерной пробы: role rubric задаёт три наблюдаемых измерения, fixed вход ведёт к observation и unknown, затем reviewer формирует только запрос недостающего доказательства.', 'Рубрика ограничивает работу до того, как начинается разбор. На схеме нет имени, рейтинга или решения о человеке.'),
table('Минимальная role rubric для fixed упражнения', ['Измерение', 'Что можно увидеть', 'Чего нельзя выводить'], [['Граница доказательства', 'Отдельно названы факт, unknown и следующий источник', 'знание всей системы или качество человека'], ['Безопасность изменения', 'Есть read-only проверка и stop condition', 'право выполнять change'], ['Техническая коммуникация', 'Symptom связан с проверкой короткой цепочкой', 'скорость работы в настоящем проекте'], ['Исключено', 'Личность, память терминов, cultural fit', 'любое итоговое решение']]),
h2('Собираем рабочую пробу с ограниченной поверхностью'),
p('Рабочая проба не обязана копировать настоящий сервис и не должна просить доступ к нему. Её задача — оставить ровно столько материала, чтобы виден был маршрут мысли. Поэтому fixed карточка хранит response header, route name и rollback note как литералы в памяти. В ней нет репозитория, сети, логов, времени, аудио или персональных данных. Такой объём нарочно тесный: если для следующего действия нужен внешний факт, хороший результат — остановка и корректный запрос, а не уверенная догадка.'),
p('OPM в датированном руководстве 2008 года связывает вопросы и шкалы с анализом конкретной работы, а не с общим впечатлением. Для технической заметки из этого следует более узкое правило: каждый criterion должен иметь observable form — фразу, артефакт или порядок шагов, который можно показать на одной пробе. Источник не доказывает, что наша рубрика верна для другой роли. Он только поддерживает дисциплину «задача → критерий → наблюдение», которую можно проверить до использования.'),
h2('Воспроизводимый пример: принять только фиксированный вход'),
code(`import {
createFixedInterviewInput,
inspectEngineeringInterview,
prepareSyntheticInterviewHandOff,
} from './upgrade-2025-09.mjs';
const input = createFixedInterviewInput('fixed-valid-v1');
const report = inspectEngineeringInterview(input);
console.log(report.accepted); // true
console.log(prepareSyntheticInterviewHandOff(report));
// { status: 'request-missing-evidence-only', productionEffect: 'not-attempted' }
// No person is scored. The input is fixed synthetic data in memory.`),
p('У примера есть намеренно скучное свойство: он не возвращает score. В accepted ветке результатом становится запрос origin rule и сохранённое stop condition. Это проверяемый hand-off между частями упражнения, а не автоматическая оценка. Если заменить fixed literal на реальный репозиторий или попытаться вынести «подходит/не подходит», инспектор должен остановиться. Такой отказ полезнее красивой таблицы баллов: он не даёт технической модели тихо расшириться до обработки реального человека.'),
h2('Как выбрать критерии, которые не дублируют друг друга'),
p('Три критерия в рубрике нужны не для полноты списка, а для трёх разных ошибок. Evidence boundary ловит выдуманную причину: header увидели, а origin rule не видели. Change safety ловит опасный следующий шаг: из симптома сразу делают правку policy. Technical communication ловит потерю связи между symptom и проверкой: вместо маршрута остаётся набор слов про кеш. Если два критерия требуют одного и того же предложения, один из них лишний. Например, «знает HTTP» и «знает Cache-Control» оба заставляют вспоминать термин, но не показывают работу с неопределённостью.'),
p('У каждого критерия полезно записать контрпример. Для границы доказательства контрпример — «max-age=0 значит origin сломан». Для безопасности — «сразу выставим новый TTL». Для коммуникации — «тут сложная кеш-инвалидация». Контрпример не нужен, чтобы поймать человека на ошибке. Он проверяет саму rubric: reviewer заранее видит, какую фразу нельзя принять за evidence. Если контрпример невозможно написать без биографии, интонации или предполагаемых мотивов, criterion надо заменить на наблюдаемую операцию.'),
h2('Порядок одного практического прохода'),
ol(['<strong>Назвать work boundary.</strong> Одним предложением записать результат упражнения и то, чего оно не делает.', '<strong>Выбрать один symptom.</strong> Оставить в карточке только те literals, которые нужны, чтобы сформулировать unknown.', '<strong>Связать criterion с наблюдением.</strong> У критерия должна быть наблюдаемая форма, а не оценочное прилагательное.', '<strong>Записать stop condition.</strong> Показать, когда дальнейшее действие потребует реального доступа или неподтверждённой причины.', '<strong>Провести review отдельно.</strong> Сначала reviewer фиксирует observation, затем interpretation; не смешивает два шага в одну заметку.', '<strong>Собрать synthetic hand-off.</strong> Разрешить только запрос недостающего evidence или остановку; не производить hiring outcome.']),
h2('Почему вопрос на термин даёт ложную экономию'),
p('Вопрос «что такое stale-while-revalidate?» дешёв в проведении, но почти не показывает, как человек ограничит изменение, когда header противоречит ожиданию. Можно знать определение и всё равно не спросить, где origin rule, кто владеет интеграцией и можно ли откатить header. Можно не вспомнить термин, но сначала назвать факт, неизвестное и безопасный способ получить следующий сигнал. Поэтому память может быть вспомогательным контекстом, но она исключена из criteria этого учебного упражнения.'),
p('Цена более строгой пробы тоже есть: её нужно собрать, прочитать вслух и проверить на лишние подсказки. Слишком широкий сценарий превращает разговор в проектирование системы; слишком узкий — в угадывание формулировки. Полезный компромисс — одна техническая развилка и явная граница evidence. Если reviewer не может объяснить, зачем в карточке каждый literal, его лучше удалить. Нагрузка уменьшается не сокращением критериев до одного впечатления, а сокращением поверхности до проверяемой задачи.'),
h2('Граница evidence и следующий шаг'),
p('Граница этого практического упражнения жёсткая: role card, входные literals и hand-off существуют только как frozen JavaScript values в памяти. Скрипт не видит человека, не получает аудио или PII, не открывает сеть, файл, репозиторий, CI либо production-систему. Его единственная положительная ветка просит отсутствующий технический факт; ни score, ни record о личности, ни hiring outcome он создать не способен.'),
p('Эта практика не обещает качество интервью, точность процесса или справедливость решения. Она не заменяет требования организации, подготовку reviewer-ов или проверку применимости роли. Её ожидаемый результат уже: после упражнения остаётся цепочка «symptom → observation → unknown → request». Следующий шаг — возьмите одну внутреннюю техническую задачу, удалите реальные данные, оставьте три fixed literals и попросите reviewer-а найти одно место, где interpretation выдана за факт. Если место найдено, рубрика ещё не готова.'),
h2('Историческая граница сентября 2025'),
p('В тексте использованы только публичные источники, опубликованные до 30 сентября 2025, и датированный PDF OPM 2008 года. Они описывают структуру критериев и шкал, но не подтверждают сценарий, политику или результат другой организации. Синтетическая модель ниже этой границы не имитирует реальное собеседование и не создаёт запись о человеке.'),
]);
const mechanism = revision({ slug: 'editorial-2025-09-mechanism-engineering-interviews', title: 'Инженерное интервью: как не перепутать наблюдение, интерпретацию и решение', categories: ['Команда', 'Интервью'], cover: '/assets/editorial/2025/engineering-interviews-2025-calibration-matrix.svg', excerpt: 'Механика технического review: доказательство, трактовка и разрешённый hand-off живут в разных записях, чтобы догадка не стала итогом.', readingMinutes: 14 }, [
p('После рабочей пробы заметка reviewer-а (того, кто читает и сверяет запись) нередко выглядит так: «понимает кеширование, можно продолжать». В ней исчезают исходный symptom, услышанный факт и неизвестное; следующий читатель не отличит наблюдение от мнения. Цена ошибки — команда калибрует не критерии, а память о разговоре, и одинаковая работа получает разные трактовки у разных reviewer-ов.'),
p('Причина — три разных операции пишут в одно поле. Observation отвечает «что было в fixed входе», interpretation — «какой узкий вывод разрешён», review decision — «какое действие допустимо дальше». Проверка: у каждого interpretation есть ссылки на observations и фраза об unknown; у decision есть ссылка на interpretation и запрет на personal outcome. Действие — сделать эти записи отдельными объектами и принимать только synthetic hand-off — учебный запрос недостающего факта.'),
h2('Три слоя работают как границы модуля'),
p('Можно представить заметку как маленькую систему с типами. Observation не объясняет, почему stale ответ появился: она сохраняет, что в header задан max-age=0. Interpretation вправе сказать лишь «нужен origin rule», потому что это следует из наблюдения. Review decision не создаёт изменение в сервисе: он просит недостающий rule. Как только слой перепрыгивает границу, цепочка становится непроверяемой. Фраза «не стал бы рисковать» может быть полезной для беседы, но без наблюдаемого условия она не является evidence.'),
figure('/assets/editorial/2025/engineering-interviews-2025-calibration-matrix.svg', 'Матрица калибровки для synthetic review: строки разделяют observation, interpretation и hand-off, столбцы показывают допустимый факт, вопрос и стоп-сигнал; личностный вывод исключён.', 'Схема показывает, что agreement достигается на форме доказательства, а не на общем впечатлении.'),
table('Контракт записей в review', ['Слой', 'Допустимое содержимое', 'Обязательная связь', 'Стоп-сигнал'], [['Observation', 'Факт fixed literal и unknown', 'sample id, evidence ref', 'появились реальный доступ или личные сведения'], ['Interpretation', 'Узкий claim о следующем факте', 'список observation id', 'claim объясняет причину без evidence'], ['Review decision', 'request или stop', 'список interpretation id', 'решение о человеке, change или score'], ['Calibration note', 'расхождение по criterion', 'одна и та же цепочка id', 'переписывание исходного observation']]),
h2('Наблюдение должно пережить несогласие'),
p('Хорошее observation можно перечитать, даже если reviewer не согласен с выводом. «Route задан как /v1/report; owner не указан» переживает спор. «Не разобрался с ownership» уже содержит трактовку и предполагает, что другой стороне был доступен способ разобраться. Поэтому у observation есть поле unknown. Оно не делает заметку слабой; оно фиксирует стоимость следующего шага. До тех пор пока origin rule отсутствует, объяснение stale ответа должно оставаться за границей записи.'),
p('Эта дисциплина особенно важна там, где технический язык маскирует догадку. Слова «architecture smell», «сложный edge case» или «не хватает системного мышления» могут звучать точно, но без симптома и свидетельства они ничего не передают следующему reviewer-у. Лучше сохранить один header literal, чем абзац профессиональных эпитетов. Поздний автор не делает вид, что видит больше данных: он называет, какие данные нужны, и оставляет простой путь проверки.'),
h2('Кодовая проверка traceability'),
code(`import {
createFixedInterviewInput,
inspectEngineeringInterview,
} from './upgrade-2025-09.mjs';
const broken = createFixedInterviewInput('fixed-unsupported-interpretation-v1');
const report = inspectEngineeringInterview(broken);
console.log(report.accepted); // false
console.log(report.reasons); // ['interpretation-not-traceable']
// The module stops before a hand-off; it never contacts a service or rates a person.`),
p('Negative path здесь важнее happy path. Он показывает, что interpretation без существующего observation id не получает право перейти в review. Технически это похоже на ссылочную целостность: объект может быть оформлен красиво, но ссылка на отсутствующий источник делает его недействительным. В настоящем процессе эту проверку можно делать вручную по таблице. Скрипт нужен только для того, чтобы фиксированные правила упражнения можно было повторить без сети, файлов и скрытого состояния.'),
h2('Что не следует нормализовать в записи'),
p('Удобная, но опасная привычка — переписать observation так, чтобы reviewer-ы быстрее согласились. Например, один записал «в header есть max-age=0», другой — «ответ явно stale из-за cache». После нормализации может остаться только вторая формулировка, и обсуждение станет гладким, но неверным: причина вошла в журнал без источника. Поэтому у слоёв разный статус. Fact можно уточнить только ссылкой на тот же literal; interpretation можно заменить, оставив старую как расхождение; hand-off можно сузить до stop. Нельзя редактировать ранний слой ради согласованного позднего вывода.'),
p('Такая строгость помогает и при изменении rubric. Если команда решила добавить criterion «владелец следующего шага», она не должна задним числом искать owner в старом sample. Правильный вариант — выпустить новую fixed карточку с явным owner literal либо признать, что старый sample этот criterion не проверяет. Версионирование здесь не бюрократия: оно не даёт сравнению разных заданий выглядеть как несогласие reviewer-ов. В этом пакете модель имеет version id именно для такого разделения, хотя сама не хранит историю и не читает внешнее состояние.'),
h2('Калибровка начинается до обсуждения'),
p('Калибровка не означает, что два reviewer-а обязаны одинаково думать. Она отвечает на более узкий вопрос: одинаково ли они видят criterion и его evidence boundary. До совместного обсуждения каждый читает один fixed sample, выписывает observations и привязывает interpretation к id. Затем сравнивают не итоговую фразу, а первую расходящуюся строку. Если один считает header достаточной причиной, а другой — только симптомом, предмет разговора уже найден: missing origin rule, а не «кто строже».'),
ol(['<strong>Заморозить sample.</strong> Оба reviewer-а используют одни literals и один stop condition.', '<strong>Писать observation отдельно.</strong> Не обсуждать результат, пока не заполнены факт, evidence ref и unknown.', '<strong>Привязать interpretation.</strong> Для каждой трактовки показать observation id и criterion id.', '<strong>Сравнить первую развилку.</strong> Найти строку, где выводы расходятся, а не усреднять впечатления.', '<strong>Проверить boundary.</strong> Удалить всё, что требует личности, реальных данных или незаданного доступа.', '<strong>Сформировать hand-off.</strong> Оставить request missing evidence либо stop-and-repair-boundary.']),
h2('Почему общая шкала без evidence не спасает'),
p('Общая шкала полезна, когда её якоря описывают наблюдаемую работу. Но сама цифра не объясняет, какие слова, действия или ограничения увидел reviewer. В учебной модели мы вообще не используем numeric score: это уменьшает соблазн превратить упражнение в ранжирование. Вместо него есть criterion id и decision, который нельзя читать как рекомендацию. Такой выбор не доказывает, что числа всегда вредны; он подходит именно для цели пакета — проверить разделение слоёв на synthetic данных.'),
p('OPM guide 2008 описывает common rating scale и индивидуальные оценки до обсуждения расхождений. Из этого не следует, что любой технический процесс обязан воспроизводить федеральную процедуру. Здесь взят только переносимый механизм: договориться о критерии до review и сохранять основание для разногласия. Наши поля observation/interpretation/decision — авторская инженерная схема, не цитата и не нормативная модель. Она должна быть проверена на собственных ограниченных примерах, прежде чем использоваться где-либо ещё.'),
h2('Ограничение механизма'),
p('Механизм ограничен fixed synthetic records: все ids, facts, claims и decisions заранее заданы в модуле и не переживают процесс выполнения. У него нет сетевого транспорта, файлового ввода, clock, telemetry, Git, CI или production side effect. В нём отсутствуют реальные собеседования, люди и их данные. Поэтому accepted report означает только целостность ссылок между учебными объектами, а не достоверность характеристики кого-либо.'),
p('Разделение записей не устраняет ошибку формулировки role rubric и не даёт полного контекста реальной инженерной работы. Оно также не измеряет скорость, продуктивность, знания или свойства личности. Зато его легко проверить: откройте любую interpretation, удалите мысленно её ссылки на observation и спросите, остаётся ли она доказуемой. Если да, в цепочке, вероятно, спрятан общий ярлык. Следующий шаг — переписать его как request к отсутствующему факту или как явное unknown.'),
h2('Историческая граница сентября 2025'),
p('Источники в разделе ниже публичны и относятся к периоду до 30 сентября 2025; датированный guide OPM выпущен в 2008 году. Они дают контекст для стандартизированных критериев и примеров ответов. Они не подтверждают качество этой модели, не распространяются автоматически на другую организацию и не легитимируют решение о реальном человеке.'),
]);
const field = revision({ slug: 'editorial-2025-09-field-engineering-interviews', title: 'Инженерное интервью: калибровка reviewer-ов без итоговой оценки человека', categories: ['Команда', 'Интервью'], cover: '/assets/editorial/2025/engineering-interviews-2025-evidence-review-loop.svg', excerpt: 'Полевой технический протокол для одной synthetic пробы: одинаковый вход, независимый review, журнал расхождений и безопасный evidence hand-off.', readingMinutes: 14 }, [
p('Два reviewer-а (проверяющих записи) читают один технический ответ и за пять минут получают противоположные выводы: один видит осторожный plan, другой — недостаток глубины. В большинстве заметок нельзя восстановить, на какой строке они разошлись. Цена ошибки — calibration превращается в спор авторитетов, а следующий разбор зависит от того, кто говорил громче, а не от одинакового критерия.'),
p('Причина — калибровку проводят после того, как появился итоговый ярлык. Нужен другой порядок: одинаковая synthetic проба, независимые observations, сравнение первой развилки и только затем ограниченный hand-off — учебный запрос недостающего факта. Проверка — каждый review record содержит criterion, ссылку на interpretation и boundary. Действие — завести короткий журнал расхождений, который разрешает запрос evidence, но не создаёт score, ranking или hiring promise.'),
h2('Полевой маршрут начинается с условия остановки'),
p('В этом материале «полевой» не значит реальный разговор. Это проверка процесса на fixed карточке, которую можно повторить локально. Карточка описывает stale API response и намеренно не даёт origin rule. Первый reviewer может назвать header симптомом, второй — поводом немедленно менять cache policy. Разница полезна только если оба записали observation и unknown. Если же один из них пишет «недостаточно системно мыслит», калибровать уже нечего: это не технический факт и не применимый критерий.'),
figure('/assets/editorial/2025/engineering-interviews-2025-evidence-review-loop.svg', 'Замкнутый цикл synthetic review: fixed sample проходит независимые observation записи, затем interpretation с ссылками, журнал расхождения и hand-off для запроса недостающего evidence; стрелка stop блокирует любой личный или production outcome.', 'Цикл не повышает статус результата: он возвращает неопределённость в следующий проверяемый запрос.'),
table('Журнал расхождений для одного fixed sample', ['Шаг', 'Reviewer A', 'Reviewer B', 'Что проверяем вместе'], [['Observation', 'max-age=0 указан', 'route /v1/report указан', 'оба ли факта имеют evidence ref'], ['Unknown', 'нет origin rule', 'нет owner интеграции', 'не заменён ли unknown догадкой'], ['Interpretation', 'запросить rule', 'сначала определить owner', 'какой запрос следует из доступного fact'], ['Hand-off', 'request missing rule', 'request missing owner', 'не появился ли change или вывод о человеке']]),
h2('Независимый review защищает порядок, а не истину'),
p('Независимость нужна, чтобы заметить расхождение до согласования слов. Reviewer A и B не обязаны писать одинаковые interpretation. Они обязаны оставить путь, по которому разницу можно обсудить. Если оба используют один criterion — evidence boundary, но один считает header причиной, видно точное место ошибки: header не содержит origin rule. Если они используют разные criteria, это уже вопрос к rubric. В обоих случаях дискуссия становится короче, потому что объект обсуждения лежит в записи, а не в памяти о беседе.'),
p('Важно не превращать calibration в поиск «правильного reviewer-а». У роли может быть неудачная формулировка, у sample — лишняя подсказка, у criterion — слишком широкая observable form. Журнал расхождений позволяет исправить именно эти артефакты. Он не разрешает приписать причину человеку, который взаимодействовал бы с заданием. В synthetic пакете людей нет вообще; поэтому любое поле, которое требует личного объяснения, означает ошибку модели, а не полезный сигнал.'),
h2('Воспроизводимый пример: остановить personal outcome'),
code(`import {
createFixedInterviewInput,
inspectEngineeringInterview,
} from './upgrade-2025-09.mjs';
const unsafe = createFixedInterviewInput('fixed-personal-outcome-v1');
const report = inspectEngineeringInterview(unsafe);
console.log(report.accepted); // false
console.log(report.handOff); // 'stop-and-repair-boundary'
// The rejected decision is never converted into a score, record, network call or file.`),
p('Эта ветка специально проверяет не техническое знание, а предел процесса. В records разрешён только текст, начинающийся с <code>hand-off:</code>; «hiring-recommendation» отклоняется. Приём не обещает защитить любой реальный процесс — он проще: делает запрещённое расширение заметным в fixed fixture. Если такой же boundary нужен в живой работе, его должны отдельно определить компетентные владельцы процесса. Код не подменяет этот шаг и не сохраняет данные за пределами памяти.'),
h2('Как менять rubric после расхождения'),
p('После калибровки хочется исправить всё сразу: добавить новые вопросы, больше критериев и подробную шкалу. Это увеличивает стоимость следующего прохода и скрывает, что именно было неясно. В synthetic журнале изменение начинается с классификации расхождения. Если reviewer-ы увидели разные facts, надо поправить sample. Если facts совпали, но interpretation не связана с criterion, надо уточнить rubric. Если interpretation совпала, а hand-off различается, надо поправить допустимые решения. Один тип расхождения — одно изменение артефакта. Так следующий прогон показывает, уменьшилась ли именно прежняя развилка.'),
p('Полезно сохранять старую карточку рядом с новой как учебный контрпример, но не сравнивать их как результаты людей. Смена route literal, stop condition или наблюдаемого критерия меняет задачу. Поэтому калибровка проверяет устойчивость процесса на одном замороженном входе, а не строит статистику между разными входами. Здесь нет чисел намеренно: без заранее определённого метода они создадут видимость точности. Для автора M8 честнее назвать стоимость обновления rubric и оставить воспроизводимый change note, чем объявить процесс измеренным.'),
h2('Как провести короткую calibration session'),
ol(['<strong>Проверить synthetic boundary.</strong> Убедиться, что sample содержит только fixed literals и исключает реальный доступ.', '<strong>Раздать role rubric.</strong> Прочитать work boundary, observable criteria и excluded dimensions до просмотра sample.', '<strong>Собрать независимые записи.</strong> Каждый reviewer создаёт observation, unknown и interpretation без общего обсуждения.', '<strong>Сопоставить ссылки.</strong> Сравнить sample id, evidence ref и criterion id; найти первую непарную строку.', '<strong>Обсудить artifact, не человека.</strong> Исправить rubric, sample или трактовку только через явную границу evidence.', '<strong>Выпустить безопасный hand-off.</strong> Зафиксировать request недостающего факта либо stop; ничего не выполнять.']),
h2('Цена калибровки и способ удержать её маленькой'),
p('Калибровка требует времени двух reviewer-ов, а значит её нельзя назначать на каждую фразу. Дешёвый вариант — раз в изменении rubric прогнать один fixed sample и сохранить только первую разницу. Не нужно собирать досье, запись разговора или статистику о людях. Если расхождений нет, журнал содержит только ссылку на fixture и boundary. Если разница есть, меняется один артефакт: criterion, instruction или sample. Так расходы остаются видимыми, а процесс не разрастается в самостоятельную систему наблюдения.'),
p('Рабочая проба тоже имеет цену: хороший вариант требует обновления, когда техническая задача перестаёт быть похожей на реальную работу. Writing Samples Summary OPM отмечает типичность задания и одинаковые условия review как свойства такого метода. Это не лицензия копировать чужую практику и не гарантия того, что короткая проба охватывает работу инженера. Для нашей модели достаточно более скромного критерия: sample должен проверять один способ обращения с неизвестным и явно сообщать, чего он не показывает.'),
h2('Граница доказательства в полевом журнале'),
p('Полевой журнал хранится только как fixed literals в памяти запущенного примера. Он не собирает звук, текст реального разговора, PII, файлы, время, telemetry или данные из сети; также в нём нет Git, CI, запроса к сервису и production effect. Любая ветка, которая пытается добавить реальный evidence или персональный итог, заканчивается stop-and-repair-boundary. Это техническая защита учебного hand-off, а не средство оценивания.'),
p('Ни одна строка журнала не является выводом о способности, потенциале или найме. Нет реального кандидата, интервью, PII, аудио, файлов, сети, telemetry, часов, Git, CI и production effect. «Полевой» здесь означает только повторяемую проверку процесса на литералах. Ожидаемый результат — reviewer может открыть decision и увидеть, какой факт нужен дальше. Следующий шаг: добавьте в свой учебный пример столбец boundary; если его нельзя заполнить без имени или внешнего доступа, sample надо сократить.'),
h2('Историческая граница сентября 2025'),
p('Ни один источник здесь не новее 30 сентября 2025. OPM guide и публичные PDF-материалы описывают структурированные вопросы, критерии и примеры рабочей пробы. Они не задают юридические, HR или hiring обязательства, не валидируют конкретную рубрику и не доказывают корректность решения организации. Этот текст использует их как ограниченный контекст, а не как обещание результата.'),
]);
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
if (process.argv.includes('--verify-fixture')) {
const assertions = runEngineeringInterviewFixture().assertions;
const failed = Object.entries(assertions).filter(([, value]) => value !== true).map(([key]) => key);
if (failed.length) { process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n'); process.exitCode = 1; }
else process.stdout.write('PASS fixture: ' + Object.keys(assertions).length + '/' + Object.keys(assertions).length + ' assertions\n');
}
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');