|
|
|
@@ -0,0 +1,153 @@
|
|
|
|
|
function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }
|
|
|
|
|
const p = (value) => '<p>' + value + '</p>';
|
|
|
|
|
const h2 = (value) => '<h2>' + value + '</h2>';
|
|
|
|
|
const code = (value) => '<pre><code>' + escapeHtml(Array.isArray(value) ? value.join('\n') : value) + '</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((x) => '<th scope="col">' + x + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((x) => '<td>' + x + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
|
|
|
|
const sources = (items) => '<ul>' + items.map((x) => '<li><a href="' + x.url + '" target="_blank" rel="noopener noreferrer">' + x.title + '</a> — ' + x.note + '</li>').join('') + '</ul>';
|
|
|
|
|
function plainText(html) { return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
|
|
|
|
|
function bodyText(html) { return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
|
|
|
|
|
function revision(meta, chunks) { const contentHtml = chunks.join('\n') + '\n' + h2('Проверяемые источники') + sources(referenceList); const proseLength = bodyText(contentHtml).length; if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': основной текст вне диапазона: ' + proseLength); return { ...meta, contentHtml, proseLength }; }
|
|
|
|
|
|
|
|
|
|
const planningGuide = {
|
|
|
|
|
title: 'GOV.UK Service Manual: Plan user research for your service, snapshot 5 декабря 2022 года',
|
|
|
|
|
url: 'https://web.archive.org/web/20221205231709id_/https://www.gov.uk/service-manual/user-research/plan-user-research-for-your-service',
|
|
|
|
|
note: 'датированный архивный снимок официального руководства. Он предлагает превращать непроверенные предположения в исследовательские вопросы и связывать метод с тем, что нужно узнать.',
|
|
|
|
|
};
|
|
|
|
|
const introductionGuide = {
|
|
|
|
|
title: 'GOV.UK Service Manual: User research for government services, snapshot 26 ноября 2022 года',
|
|
|
|
|
url: 'https://web.archive.org/web/20221126155827id_/https://www.gov.uk/service-manual/user-research/how-user-research-improves-service-design',
|
|
|
|
|
note: 'датированный архивный снимок официального руководства. Он отделяет потребности и исходы пользователя от того, что просто популярно, и описывает исследование как непрерывную работу.',
|
|
|
|
|
};
|
|
|
|
|
const referenceList = [planningGuide, introductionGuide];
|
|
|
|
|
|
|
|
|
|
/** Учебная evidence-модель. Это не интервью, не аналитика, не CRM, не browser trace и не продуктовый сигнал. */
|
|
|
|
|
function createEvidenceModel() { return { boundary: Object.freeze({ kind: 'teaching-evidence-model-v1', participants: 'not-created', analytics: 'not-connected', product: 'not-connected', interview: 'not-conducted', metric: 'not-measured' }), records: [], nextId: 1 }; }
|
|
|
|
|
function snapshot(model) { return Object.freeze({ nextId: model.nextId, records: model.records.map((x) => ({ ...x })) }); }
|
|
|
|
|
function addObservation(model, input) { if (!input?.text || !input?.source) return Object.freeze({ kind: 'observation-rejected', changed: false }); const record = Object.freeze({ id: 'e-' + String(model.nextId++).padStart(2, '0'), observation: String(input.text), source: String(input.source), interpretation: null, hypothesis: null, decision: null }); model.records.push(record); return Object.freeze({ kind: 'observation-recorded', changed: true, record }); }
|
|
|
|
|
function addInterpretation(model, id, text) { const current = model.records.find((x) => x.id === id); if (!current || !text) return Object.freeze({ kind: 'interpretation-rejected', changed: false }); const record = Object.freeze({ ...current, interpretation: String(text) }); model.records[model.records.indexOf(current)] = record; return Object.freeze({ kind: 'interpretation-recorded', changed: true, record }); }
|
|
|
|
|
function setHypothesis(model, id, text) { const current = model.records.find((x) => x.id === id); if (!current?.interpretation || !text) return Object.freeze({ kind: 'hypothesis-rejected', changed: false }); const record = Object.freeze({ ...current, hypothesis: String(text) }); model.records[model.records.indexOf(current)] = record; return Object.freeze({ kind: 'hypothesis-recorded', changed: true, record }); }
|
|
|
|
|
function planExperiment(model, id, action) { const current = model.records.find((x) => x.id === id); if (!current?.hypothesis || !action?.nextExperiment) return Object.freeze({ kind: 'decision-rejected', changed: false }); const record = Object.freeze({ ...current, decision: Object.freeze({ decision: action.decision || 'defer-solution', nextExperiment: String(action.nextExperiment) }) }); model.records[model.records.indexOf(current)] = record; return Object.freeze({ kind: 'experiment-planned', changed: true, record }); }
|
|
|
|
|
function rollback(model, before) { model.nextId = before.nextId; model.records = before.records.map((x) => ({ ...x })); return Object.freeze({ kind: 'rollback-to-snapshot', changed: true, recordCount: model.records.length }); }
|
|
|
|
|
|
|
|
|
|
export function runUserResearchFixture() {
|
|
|
|
|
const model = createEvidenceModel(); const before = snapshot(model);
|
|
|
|
|
const malformed = addObservation(model, { text: 'Поле оставлено пустым', source: '' });
|
|
|
|
|
const observed = addObservation(model, { text: 'В учебной карточке есть незаполненное обязательное поле.', source: 'synthetic-card-A' });
|
|
|
|
|
const earlyDecision = planExperiment(model, observed.record.id, { nextExperiment: 'Проверить понятность обозначения поля в отдельном исследовании.' });
|
|
|
|
|
const earlyHypothesis = setHypothesis(model, observed.record.id, 'Если обозначение поля непонятно целевой группе, проверка прототипа может выявить затруднение.');
|
|
|
|
|
const interpreted = addInterpretation(model, observed.record.id, 'Наблюдение само по себе не объясняет причину незаполненного поля.');
|
|
|
|
|
const hypothesised = setHypothesis(model, observed.record.id, 'Если обозначение поля непонятно целевой группе, проверка прототипа может выявить затруднение.');
|
|
|
|
|
const planned = planExperiment(model, observed.record.id, { decision: 'defer-solution', nextExperiment: 'Проверить на согласованном прототипе, понимают ли участники назначение поля без подсказки.' });
|
|
|
|
|
const duplicateInterpretation = addInterpretation(model, 'e-99', 'Несуществующая запись');
|
|
|
|
|
const restored = rollback(model, before);
|
|
|
|
|
return Object.freeze({ boundary: model.boundary, evidence: { malformed, observed, earlyDecision, earlyHypothesis, interpreted, hypothesised, planned, duplicateInterpretation, restored }, assertions: Object.freeze({ noParticipants: model.boundary.participants === 'not-created', noAnalytics: model.boundary.analytics === 'not-connected', observationNeedsSource: malformed.kind === 'observation-rejected', observationKeepsSource: observed.record.source === 'synthetic-card-A', noDecisionBeforeHypothesis: earlyDecision.kind === 'decision-rejected', interpretationKeepsLevelsSeparate: interpreted.record.observation === observed.record.observation && interpreted.record.hypothesis === null && interpreted.record.decision === null, hypothesisRequiresInterpretation: earlyHypothesis.kind === 'hypothesis-rejected' && hypothesised.kind === 'hypothesis-recorded', experimentKeepsDecisionDeferred: planned.record.decision.decision === 'defer-solution', experimentIsNextNotResult: planned.record.decision.nextExperiment.includes('Проверить'), unknownRecordRejected: duplicateInterpretation.changed === false, rollbackRemovesSyntheticRecord: restored.recordCount === 0, rollbackRestoresCounter: model.nextId === 1 }) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const example = [
|
|
|
|
|
"import { runUserResearchFixture } from './upgrade-2022-12.mjs';",
|
|
|
|
|
'const report = runUserResearchFixture();',
|
|
|
|
|
"if (!Object.values(report.assertions).every(Boolean)) throw new Error('evidence contract failed');",
|
|
|
|
|
"console.log(report.evidence.planned.record.decision.decision); // 'defer-solution'",
|
|
|
|
|
"console.log(report.boundary.interview); // 'not-conducted'",
|
|
|
|
|
];
|
|
|
|
|
const route = [
|
|
|
|
|
'<strong>Симптом.</strong> В тикете уже есть решение, но нет проверяемого описания проблемы и источника.',
|
|
|
|
|
'<strong>Причина.</strong> Наблюдение, его толкование и предпочтение команды записаны одной фразой.',
|
|
|
|
|
'<strong>Проверка.</strong> Для каждого утверждения найдите отдельные поля observation, source, interpretation и hypothesis.',
|
|
|
|
|
'<strong>Действие.</strong> Сформулируйте следующий опыт, который может подтвердить или опровергнуть гипотезу, не обещая релиз.',
|
|
|
|
|
'<strong>Решение.</strong> Только после evidence выберите маленькое изменение либо честно отложите его.',
|
|
|
|
|
'<strong>Rollback.</strong> Если запись смешивает уровни, удалите решение и вернитесь к исходному наблюдению; не повышайте предположение до факта.',
|
|
|
|
|
];
|
|
|
|
|
const practice = revision({ slug: 'editorial-2022-12-practice-user-research', title: 'Исследование пользовательской проблемы: сначала разделить evidence', categories: ['Product', 'Практика'], cover: '/assets/editorial/2022/user-research-2022-evidence-flow.svg', excerpt: 'Как превратить желание улучшить интерфейс в проверяемую цепочку наблюдения, гипотезы и следующего опыта.', readingMinutes: 12 }, [
|
|
|
|
|
p('Инженер видит неровный интерфейс и сразу берётся за улучшение: переставить блок, добавить подсказку, упростить форму. Проблема в том, что у этого изменения может не быть подтверждённой пользовательской задачи. Цена — не только потраченный спринт. Команда закрепляет удобный для себя ответ, а исходная трудность остаётся неописанной и потому не проверяется после релиза.'),
|
|
|
|
|
p('Нужна строгая, но небольшая единица evidence: наблюдение, источник, интерпретация, гипотеза, решение или следующий эксперимент. Это не бюрократическая анкета и не обещание статистической достоверности. Она нужна, чтобы reviewer увидел границу между тем, что было замечено, тем, как это поняли, и тем, что команда пока лишь предлагает проверить. Ниже все карточки учебные; никаких интервью, продуктовых событий и людей здесь нет.'),
|
|
|
|
|
h2('Не принимайте решение за наблюдение'),
|
|
|
|
|
p('Фраза «пользователи не понимают поле, поэтому добавим tooltip» содержит минимум три разных утверждения. Первое могло быть наблюдением, если есть воспроизводимый источник. Второе — интерпретация: по одному действию нельзя узнать мотив. Третье — решение, которое уже зафиксировало форму ответа. Пока они склеены, нельзя проверить, не поменяли ли мы дизайн ради вкуса команды. Разделение делает неизвестное видимым и оставляет право не выпускать изменение.'),
|
|
|
|
|
table('Пять уровней одной записи', ['Уровень', 'Что допустимо записать', 'Чего он не доказывает', 'Следующий шаг'], [
|
|
|
|
|
['Наблюдение', 'буквально увиденное действие или высказывание', 'причину и частотность', 'указать источник'],
|
|
|
|
|
['Источник', 'сессия, документ, согласованный сигнал или артефакт', 'репрезентативность сам по себе', 'проверить доступ и контекст'],
|
|
|
|
|
['Интерпретация', 'осторожное объяснение наблюдения', 'факт о мотивации', 'сформулировать альтернативу'],
|
|
|
|
|
['Гипотеза', 'проверяемое условие и ожидаемое наблюдение', 'готовое решение', 'выбрать опыт'],
|
|
|
|
|
['Решение/эксперимент', 'обратимое изменение или способ узнать больше', 'эффект в продукте', 'сохранить outcome и границу'],
|
|
|
|
|
]),
|
|
|
|
|
h2('Источник — не украшение записи'),
|
|
|
|
|
p('Источник отвечает на очень приземлённый вопрос: откуда взялось наблюдение и кто может сверить его контекст. Он не обязан быть числом. Это может быть заметка из согласованной сессии, артефакт поддержки или специально проведённый опыт — если его допустимо хранить и обсуждать. Но строка «все знают» источником не является. Без него фраза не выдерживает повторного чтения и незаметно превращается в авторитет самого громкого участника обсуждения.'),
|
|
|
|
|
p('Официальное руководство GOV.UK в архивном снимке декабря 2022 года предлагает начать с исследовательских вопросов и превращать непроверенные предположения в такие вопросы. Это важная дисциплина, а не готовая методика для любого продукта. Руководство не доказывает нашу учебную запись и не даёт ей статистический вес. Оно лишь поддерживает направление: сначала назвать то, чего команда не знает, затем подобрать способ узнать это.'),
|
|
|
|
|
figure('/assets/editorial/2022/user-research-2022-evidence-flow.svg', 'Схема учебной evidence-цепочки: наблюдение с источником отдельно ведёт к интерпретации, затем к гипотезе и к отложенному решению или следующему эксперименту; стрелка rollback возвращает к наблюдению.', 'Схема показывает структуру локальной записи. Она не является записью интервью, аналитическим pipeline или доказательством поведения реальных людей.'),
|
|
|
|
|
h2('Выполните маленькую модель перед обсуждением решения'),
|
|
|
|
|
p('Fixture ниже создаёт синтетическую карточку и запрещает планировать опыт, пока нет интерпретации и гипотезы. Она также отклоняет наблюдение без источника. После rollback запись исчезает: так пример демонстрирует, что в нём нет накопленного исследования и реального результата. PASS означает только то, что локальные правила объекта соблюдены; он не означает, что кто-то понял интерфейс или что будущий опыт уже проведён.'),
|
|
|
|
|
code(example),
|
|
|
|
|
p('Полезная проверка на ревью проста: можно ли удалить последнее решение и всё ещё понять исходную проблему? Если нельзя, решение замаскировало evidence. Верните отдельную карточку с наблюдением, сохраните дословную формулировку, если она допустима, и назовите её источник. Затем перечислите минимум одну альтернативную интерпретацию. Это замедляет ранний дизайн на несколько минут, но уменьшает вероятность, что команда будет защищать уже нарисованный интерфейс вместо вопроса.'),
|
|
|
|
|
h2('Маршрут: симптом → причина → проверка → действие'), ol(route),
|
|
|
|
|
h2('Граница и rollback'),
|
|
|
|
|
p('Учебная модель не проводит интервью, не собирает consent, не подключается к аналитике, не хранит персональные данные и не измеряет частоту. Её `synthetic-card-A` выдумана специально для детерминированного fixture. Поэтому нельзя переносить её поля в production как достаточный процесс исследования. Реальный план обязан отдельно решить, какие данные допустимы, кто их читает, как учесть исключённые группы и какой метод отвечает на конкретный вопрос.'),
|
|
|
|
|
p('Rollback здесь не означает «забыть неудобное evidence». Он означает отменить поспешное решение, если оно не связано с наблюдением или эксперимент пока не согласован. Оставьте проблему открытой и зафиксируйте, чего не хватает. Следующий шаг — взять одну настоящую инициативу и написать пять полей без названия фичи. Если поле решения заполняется раньше гипотезы, вернитесь на шаг назад.'),
|
|
|
|
|
h2('Историческая граница декабря 2022'),
|
|
|
|
|
p('Использованы только архивные снимки официального GOV.UK Service Manual, доступные до конца декабря 2022 года. Они не заменяют исследование конкретного сервиса, не подтверждают реальный продуктовый сигнал и не разрешают выдумывать участника, интервью или эффект.'),
|
|
|
|
|
]);
|
|
|
|
|
const mechanism = revision({ slug: 'editorial-2022-12-mechanism-user-research', title: 'Механика evidence: не путать источник, смысл и гипотезу', categories: ['Product', 'Метод'], cover: '/assets/editorial/2022/user-research-2022-evidence-contract.svg', excerpt: 'Технический контракт учебной evidence-модели: какие переходы допустимы до решения об интерфейсе.', readingMinutes: 13 }, [
|
|
|
|
|
p('Самая опасная ошибка в обсуждении интерфейса — выдать интерпретацию за факт. Тогда инженер получает красивую задачу, но не получает проблему, которую должен решать. Цена видна поздно: решение уже собрано, а на вопрос «почему именно оно?» остаются ссылка на старый созвон и уверенная формулировка без источника. Такое изменение трудно отменить, потому что спор идёт о вкусах, а не о проверяемой цепочке.'),
|
|
|
|
|
p('Разберём evidence как контракт состояний. Наблюдение допускается только с источником. Интерпретация прикрепляется к конкретной записи и обязана говорить о неопределённости, а не объявлять мотив установленным. Гипотеза появляется после интерпретации. Только затем модель разрешает запланировать следующий эксперимент или явное `defer-solution`. Это учебный reducer; он не моделирует людей, сессии, consent, аналитику, browser или метрики.'),
|
|
|
|
|
h2('Контракт должен запрещать короткий путь'),
|
|
|
|
|
p('Если в трекере можно сразу написать «сделать кнопку заметнее», процесс поощряет решение до вопроса. Контракт не запрещает инженерную инициативу; он заставляет пометить её как предположение. В модели `planExperiment` возвращает `decision-rejected`, пока у записи нет гипотезы. Это полезная граница: решение не становится доказательством самого себя, а следующий опыт остаётся отдельным объектом, который можно не проводить, изменить или откатить.'),
|
|
|
|
|
table('Переходы учебной модели', ['Вход', 'Допустимое условие', 'Результат', 'Недопустимая подмена'], [
|
|
|
|
|
['observation', 'есть text и source', 'создана запись', 'наблюдение без происхождения'],
|
|
|
|
|
['interpretation', 'существует id', 'добавлен осторожный смысл', 'объявить его фактом'],
|
|
|
|
|
['hypothesis', 'есть interpretation', 'записано проверяемое допущение', 'сразу указать UI-решение'],
|
|
|
|
|
['experiment', 'есть hypothesis и nextExperiment', 'решение отложено или опыт спланирован', 'назвать его проведённым'],
|
|
|
|
|
['rollback', 'есть snapshot', 'синтетическая запись снята', 'стирать исходный источник'],
|
|
|
|
|
]),
|
|
|
|
|
h2('Интерпретация должна переживать несогласие'),
|
|
|
|
|
p('Хорошая интерпретация не звучит как приговор. Вместо «человек не понимает термин» она говорит: «это наблюдение не объясняет, почему поле осталось пустым». Такая запись хуже продаёт инициативу, но лучше работает на следующем шаге: можно проверить язык, порядок полей, контекст задачи или недоступность действия. Несколько гипотез могут относиться к одному наблюдению. Если команда выбрала одну из них из-за ограничений времени, это решение надо назвать, а не маскировать под вывод исследования.'),
|
|
|
|
|
p('Архивный GOV.UK Service Manual отделяет вопросы команды от её непроверенных предположений и советует выбирать исследовательские активности по тому, что нужно узнать. Мы не превращаем это в универсальную норму и не заимствуем у руководства реальный кейс. Здесь это только подтверждение принципа: метод не должен быть выбран по привычке, а вопрос не должен быть подменён заранее выбранным компонентом.'),
|
|
|
|
|
figure('/assets/editorial/2022/user-research-2022-evidence-contract.svg', 'Диаграмма контракта: observation требует source; interpretation требует существующую запись; hypothesis требует interpretation; experiment требует hypothesis; раннее решение отклонено; rollback возвращает snapshot.', 'Это схема переходов учебного JavaScript-объекта, не диаграмма настоящего исследовательского процесса и не product analytics.'),
|
|
|
|
|
h2('Проверяемый код и то, чего он не проверяет'),
|
|
|
|
|
p('Выполнение fixture создаёт одну синтетическую запись. Первая попытка без source отвергается. Ранняя попытка планировать опыт отвергается. После интерпретации и гипотезы модель принимает `defer-solution` с текстом следующего опыта, а затем rollback очищает запись. Assertions намеренно не содержат «пользователь согласен», «conversion выросла» или «интервью завершено»: у модели нет таких входных данных.'),
|
|
|
|
|
code(example),
|
|
|
|
|
p('Этот код годится как проверяемый пример для соглашения в команде, но не как библиотека исследования. В production нельзя использовать его ids, строки и отсутствие политики хранения. Если нужен настоящий инструмент, отдельно спроектируйте роли, доступ, аудит изменений, удаление данных и связь с согласованными исследованиями. Не расширяйте значение двенадцати assertions до этих задач: они проверяют только порядок локальных переходов.'),
|
|
|
|
|
h2('Маршрут: симптом → причина → проверка → действие'), ol(route),
|
|
|
|
|
h2('Как не превратить модель в театральный процесс'),
|
|
|
|
|
p('Пять заполненных полей не делают слабое наблюдение сильным. Запись может быть неполной, источник — узким, а гипотеза — неверной. Контракт нужен, чтобы это было видно до разработки. В поле source полезно указывать тип и границу: что именно доступно для сверки, чего оно не говорит и на какой вопрос не отвечает. Если таких данных нет, честный статус — «неизвестно», а не аккуратно оформленная история о пользователе.'),
|
|
|
|
|
p('Второй риск — спросить людей только о выбранном решении. Тогда запись формально содержит experiment, но фактически проверяет вкус. Сначала сформулируйте задачу и наблюдаемый критерий понимания или выполнения, затем выбирайте прототип, интервью, наблюдение или существующий артефакт. В этой статье не назначается метод и не задаётся размер выборки: это зависит от вопроса, рисков и контекста, которого учебная модель не содержит.'),
|
|
|
|
|
h2('Граница и следующий шаг'),
|
|
|
|
|
p('Модель не является интервью-скриптом, исследовательским репозиторием или системой принятия решений. Она не оценивает качество источника, не гарантирует отсутствие bias и не знает юридические требования. Документы GOV.UK описывают подход для государственных сервисов; они не являются спецификацией вашего продукта. Их неизменяемые архивные URL нужны здесь только для честной исторической рамки декабря 2022 года.'),
|
|
|
|
|
p('Следующий шаг: выберите одну уже написанную фичу и проведите статический разбор. Отделите предложения, которые являются наблюдениями, от предложений, которые являются объяснениями. Для каждого объяснения добавьте альтернативу и вопрос. Если не удаётся сформулировать следующий опыт без слова «реализовать», остановите дизайн. Это нормальный результат: проблема пока не определена достаточно, чтобы кодировать решение.'),
|
|
|
|
|
h2('Историческая граница декабря 2022'), p('Ссылки ведут на архивные snapshots GOV.UK от 26 ноября и 5 декабря 2022 года. Они доступны до заданного месяца и использованы как официальные методические источники, а не как свидетельства о конкретных пользователях или системах.'),
|
|
|
|
|
]);
|
|
|
|
|
const field = revision({ slug: 'editorial-2022-12-field-user-research', title: 'Полевой маршрут: остановить фичу без подтверждённой проблемы', categories: ['Product', 'Практика'], cover: '/assets/editorial/2022/user-research-2022-diagnosis-rollback.svg', excerpt: 'Практический разбор инициативы без доказанной проблемы: как сохранить вопрос, выбрать следующий опыт и не выдумать evidence.', readingMinutes: 12 }, [
|
|
|
|
|
p('Задача на улучшение интерфейса часто приходит уже с макетом, названием компонента и оценкой. Проблема — никто не может показать, какая пользовательская трудность подтверждает это решение. Цена такой скорости — дорогая ложная определённость: разработка быстро закрывает тикет, но команда не узнаёт, исчезла ли исходная преграда, и получает ещё один слой UI, который придётся объяснять и поддерживать.'),
|
|
|
|
|
p('Полевой маршрут начинается не с поиска «самого частого» сигнала и не с имитации интервью. Он берёт только то, что можно показать reviewer: наблюдение и источник. Затем отдельно описывает возможные смыслы, гипотезу и следующий опыт. В примере ниже используются учебные записи. Мы не называем их отзывами, не рисуем воронку и не сообщаем эффект, потому что такого evidence в статье нет.'),
|
|
|
|
|
h2('Диагностируйте утверждение в задаче'),
|
|
|
|
|
p('Откройте инициативу и подчеркните глаголы: «не понимают», «часто бросают», «нужно упростить», «лучше добавить». Первые два могут звучать как факты, последние два — как решения, но без источника все четыре остаются допущениями. Не спорьте с автором задачи о правоте. Спросите точнее: что было наблюдаемо, где это зафиксировано, какой контекст известен и что из этого ещё не ясно. Если ответа нет, это не провал автора; это корректно обнаруженная исследовательская дыра.'),
|
|
|
|
|
table('Карта разбора инициативы', ['Симптом в задаче', 'Что обычно смешано', 'Проверка', 'Безопасное действие'], [
|
|
|
|
|
['«сделать проще»', 'решение и критерий успеха', 'есть ли описанная трудность', 'отложить UI и записать вопрос'],
|
|
|
|
|
['«пользователь путается»', 'наблюдение и мотив', 'есть ли источник и альтернативы', 'сохранить буквальный факт'],
|
|
|
|
|
['«добавим подсказку»', 'гипотеза и реализация', 'что должно стать наблюдаемым', 'спланировать отдельный опыт'],
|
|
|
|
|
['«так просили»', 'авторитет и evidence', 'можно ли сверить контекст', 'назвать ограничение источника'],
|
|
|
|
|
['«быстро выпустим»', 'скорость и полезность', 'можно ли откатить решение', 'выбрать обратимый шаг'],
|
|
|
|
|
]),
|
|
|
|
|
h2('Сохраните два независимых артефакта'),
|
|
|
|
|
p('Первый артефакт — карточка evidence. В ней нет выбранной фичи, пока не появился переход к решению. Второй — список инженерных вариантов: изменение текста, порядка, навигации, состояния или отказ от изменения. Их нельзя хранить одной строкой, потому что список вариантов неизбежно влияет на чтение evidence. На review сначала рассмотрите карточку, потом варианты. Это простое разделение защищает исследование от желания оправдать первый доступный патч.'),
|
|
|
|
|
p('Официальный материал GOV.UK, доступный в архивном снимке ноября 2022 года, предлагает фокусироваться на том, помогает ли сервис человеку достигнуть нужного исхода, а не на том, что популярно. Для текущей задачи это не готовый KPI и не разрешение игнорировать ограничения продукта. Это полезная проверка формулировки: «какой исход и какая преграда?» сильнее, чем «какой компонент нравится команде?».'),
|
|
|
|
|
figure('/assets/editorial/2022/user-research-2022-diagnosis-rollback.svg', 'Маршрут диагностики: утверждение о фиче разбирается на наблюдение и источник; затем проверяются альтернативные интерпретации; гипотеза ведёт к следующему опыту или отложенному решению; недостаток evidence ведёт к rollback решения.', 'Рисунок — чек-лист для редакционного и технического ревью. Он не изображает реальные исследования, пользователей, сроки или результаты.'),
|
|
|
|
|
h2('Проверьте форму записи локально'),
|
|
|
|
|
p('CLI не читает трекер и не связывается с продуктом. Он лишь показывает порядок допустимых операций на искусственном объекте: observation без source отклонено; experiment до hypothesis отклонён; после гипотезы план содержит `defer-solution`; rollback удаляет синтетическую запись. Эти ограничения полезны, когда хочется написать «сделаем X, потому что Y» одним предложением. Модель заставляет сначала признать, где отсутствует связка.'),
|
|
|
|
|
code(example),
|
|
|
|
|
p('После этого не надо переписывать все старые задачи. Начните с одной ставки, которая достаточно дорога, чтобы её стоило остановить на час. Добавьте source, перепишите интерпретацию в осторожной форме, укажите альтернативу и назначьте следующий способ узнать больше. Если команда всё равно обязана выпустить изменение по правовому, операционному или иному внешнему ограничению, назовите это отдельно. Вынужденное решение не надо выдавать за исследовательский вывод.'),
|
|
|
|
|
h2('Маршрут: симптом → причина → проверка → действие'), ol(route),
|
|
|
|
|
h2('Проверка перед реализацией'),
|
|
|
|
|
p('До pull request проведите короткий gate. Может ли другой человек открыть источник? Понимает ли он, какое предложение является наблюдением, а какое — нашей трактовкой? Есть ли формулировка, при которой гипотеза окажется неверной? Описан ли следующий опыт так, чтобы его можно было отменить или изменить? Если на любой вопрос ответ «нет», преждевременно обсуждать компоненты и оценку. Запишите unknown как unknown — это рабочий результат, а не пустая форма.'),
|
|
|
|
|
p('Не создавайте имитацию точности. Не добавляйте выдуманные цитаты, количество людей, процент проблем или «типичного пользователя», чтобы повысить убедительность карточки. Не переносите единичное наблюдение в правило для всех. Не объявляйте любой существующий лог продуктовым доказательством без владельца и контекста. Когда evidence слаб, корректное действие — выбрать опыт с большей ясностью или снизить размер обратимого изменения, а не сделать текст увереннее.'),
|
|
|
|
|
h2('Rollback и граница утверждений'),
|
|
|
|
|
p('Rollback — это удалить либо отложить решение, но сохранить вопрос, источник и причины неопределённости. Он нужен, когда между наблюдением и фичей нет проверяемой гипотезы, когда источник недоступен для review или когда предполагаемый опыт проверяет только предпочтение интерфейса. Не используйте rollback как способ стереть несогласные данные. Если evidence законно хранится, его разбор остаётся частью контекста; отменяется только необоснованный переход к реализации.'),
|
|
|
|
|
p('Статья не устанавливает обязательный процесс исследований, не определяет sample size и не утверждает эффективность конкретного метода. Локальный fixture не даёт права принимать продуктовые решения автоматически. Он лишь делает одну мыслительную ошибку труднее: нельзя назвать решение подтверждённым, если до него не существует отдельно описанного наблюдения, источника, интерпретации и гипотезы.'),
|
|
|
|
|
h2('Следующий шаг и историческая граница'),
|
|
|
|
|
p('Возьмите ближайшую задачу с готовым UI-ответом и проведите разбор на одном листе. Завершите его либо одним проверяемым следующим опытом, либо статусом `defer-solution`. Источники этой статьи — официальные архивные страницы GOV.UK, доступные в ноябре и декабре 2022 года. Они задают методическую рамку, но не доказывают ни один реальный случай из вашего продукта.'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
|
|
|
|
|
function verifyFixture() { const report = runUserResearchFixture(); const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key); if (failed.length) { process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n'); process.exitCode = 1; return; } process.stdout.write('PASS fixture: ' + Object.keys(report.assertions).length + '/' + Object.keys(report.assertions).length + ' assertions\n'); }
|
|
|
|
|
if (process.argv.includes('--verify-fixture')) verifyFixture();
|
|
|
|
|
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');
|