revise September 2025 engineering interview articles
Build and deploy / deploy (push) Successful in 20s
Build and deploy / deploy (push) Successful in 20s
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
function escapeHtml(value) {
|
||||
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||
}
|
||||
|
||||
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');
|
||||
Reference in New Issue
Block a user