diff --git a/editorial/production/README.md b/editorial/production/README.md index 03edac7..69d2703 100644 --- a/editorial/production/README.md +++ b/editorial/production/README.md @@ -1,6 +1,6 @@ # Производство редакционных партий -На 31 июля 2026 года строгий аудит проходит 271 из 358 созданных материалов. Остальные 87 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. +На 31 июля 2026 года строгий аудит проходит 274 из 358 созданных материалов. Остальные 84 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. ## Одна партия diff --git a/editorial/reviews/2025-08-draft.md b/editorial/reviews/2025-08-draft.md new file mode 100644 index 0000000..469990f --- /dev/null +++ b/editorial/reviews/2025-08-draft.md @@ -0,0 +1,43 @@ +# P90 · Август 2025 · Исследование UX внутреннего инструмента + +## Проход 1 — факты, техника и источники + +- Проверен `web/scripts/upgrade-2025-08.mjs`: модель содержит только frozen fixed synthetic literals. В ней нет чтения/записи файлов, сети, clock, telemetry, production, PII, recording, Git, CI или real API; `productionEffect` в положительной и отрицательных ветках — `not-attempted`. +- Источники — первичные, публичные HTTPS-руководства GOV.UK. У consent-страницы видны публикация 24 May 2016 и update 5 November 2018; руководства по заметкам и планированию также опубликованы до исторической границы 31 August 2025. Изменяемые оригиналы закреплены точными immutable archive captures 2025-07-02; в source list у каждого показаны version/date, claim и boundary. Они не использованы как универсальная legal policy и не выданы за доказательство UX-эффекта. +- Механика проверена по negative paths: withdrawn consent, recording вне notes-only boundary, leading prompt и decision без ссылки на observation останавливают synthetic hand-off. Положительная ветка разрешает только `observe-same-fixed-scenario-only`, а не UI release. + +## Проход 2 — редактура, голос и плотность + +- Прочитаны все три тела без списка источников. Практика, механизм и полевой журнал начинают с наблюдаемой проблемы и цены ошибки в первых двух абзацах; далее используют последовательность «симптом → причина → проверка → действие». Большой общий блок удалён: у каждого текста собственные определения, executable negative example, sequence и next step. Проверка тел не нашла совпадающих предложений длиной от 90 знаков; пересечение словаря попарно 0.257–0.264. +- Термины раскрыты при первом появлении: neutral task scenario, informed consent, evidence coding, consent boundary, observation, code, theme, decision log и follow-up. Нет вымышленных сотрудников, интервью, денег, метрик, записей или утверждений о production-результате. +- Тон соответствует M8: решение отделено от evidence, названа цена повторной разработки и спорного UI-изменения, а финал каждой статьи оставляет ограничение и следующий проверяемый шаг. Прогоны audit ниже фиксируют фактический объём в диапазоне 5 000–15 000 знаков. + +## Проход 3 — визуал, fixture и выпускной контроль + +- `node --check`, `--verify-fixture` (15/15), `npm run audit:draft -- scripts/upgrade-2025-08.mjs` (7 053 / 7 536 / 7 836 знаков), XML-проверка и SVG safety scan пройдены. +- Все три SVG имеют title/desc, содержательный `alt` и подпись в статье. Sharp render и ручной просмотр на ширине 375 px пройдены: evidence map — путь consent → scenario → observation → decision; matrix — условия STOP; feedback loop — isolated follow-up без автоматического релиза. +- Пакет намеренно не подключён к registry, README, очереди, базе статей или Git. Для публикации нужен отдельный независимый редакторский приёмочный проход. + +## Независимый выпускной проход — 31.07.2026 + +Три статьи прочитаны целиком вне self-review. Их роли не совпадают: practice строит нейтральный сценарий сессии, mechanism проверяет traceability между evidence и decision, field ведёт короткий review journal. Автоматическая попарная проверка основного текста дала 0 одинаковых абзацев от 160 знаков и 0 общих 12-словных фрагментов. + +Архивные GOV.UK captures от 02.07.2025 запрошены повторно. Consent capture подтверждает informed consent и withdrawal; notes/recording capture — consent до заметок и записей и использование только в согласованных границах; planning capture — порядок «problems, assumptions, what needs to be learned for a next decision». Это ровно граница утверждений в статьях, без legal или outcome claim. + +Три видимых code example исполнены через публичные exports: leading prompt возвращает `neutral-task-scenario-required`; unlinked decision и withdrawn consent возвращают `stop-and-repair-boundary`. SVG повторно отрендерены Sharp на 375 px: evidence map, matrix и feedback loop читаемы, safety scan чист. + +~~~text +node --check scripts/upgrade-2025-08.mjs +PASS + +node scripts/upgrade-2025-08.mjs --verify-fixture +PASS fixture: 15/15 assertions + +npm run audit:draft -- scripts/upgrade-2025-08.mjs +PASS: 7053 / 7536 / 7836 body chars + +npm run audit:articles -- <3 August slugs> +PASS: each article has 1 table, 1 code example and 1 figure +~~~ + +После приёмки overlay подключён в `editorial-revisions.mjs` после July 2025. Его fixed synthetic boundary, `not-attempted` production effect и запрет на реальный UI release остаются неизменными. diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs index afe33a6..a2f675c 100644 --- a/web/data/editorial-revisions.mjs +++ b/web/data/editorial-revisions.mjs @@ -86,6 +86,7 @@ import { revisions as april2025Revisions } from '../scripts/upgrade-2025-04.mjs' 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'; // This layer replaces archived source entries without losing their stable slug and date. export const editorialRevisions = [ @@ -177,4 +178,5 @@ export const editorialRevisions = [ ...may2025Revisions, ...june2025Revisions, ...july2025Revisions, + ...august2025Revisions, ]; diff --git a/web/public/assets/editorial/2025/tool-ux-research-2025-evidence-map.svg b/web/public/assets/editorial/2025/tool-ux-research-2025-evidence-map.svg new file mode 100644 index 0000000..3e70e79 --- /dev/null +++ b/web/public/assets/editorial/2025/tool-ux-research-2025-evidence-map.svg @@ -0,0 +1,10 @@ + + Карта доказательств исследования UX внутреннего инструментаConsent card ограничивает допустимую заметку, нейтральный сценарий порождает coded observation, затем decision log разрешает только follow-up. + Evidence map: от consent к следующему сценарию + Consent cardnotes onlyno recordingwithdrawal → stop + Task scenarioneutral taskno chosen UI + Observationfact + codeuncertainty + Decisionlinked idsnot established + + Follow-up: тот же scenario, новый isolated recordне автоматический релиз и не claim об эффекте + diff --git a/web/public/assets/editorial/2025/tool-ux-research-2025-observation-decision-matrix.svg b/web/public/assets/editorial/2025/tool-ux-research-2025-observation-decision-matrix.svg new file mode 100644 index 0000000..aa2364a --- /dev/null +++ b/web/public/assets/editorial/2025/tool-ux-research-2025-observation-decision-matrix.svg @@ -0,0 +1,8 @@ + + Матрица от наблюдения к решениюДве синтетические заметки с кодами образуют тему видимости следующего шага. Решение возможно только при ссылках на заметки, consent и нейтральном сценарии; иначе стоп. + Observation → theme → candidate change + ObservationCodeThemeDecision gate + status read; ownerquestion followsuncertainty recordedowner-unclearnext-stepvisibilitylinked idsclaim: not established + processing termdoes not name next actuncertainty recordedstatus-hiddenlanguage-mismatch + STOP: withdrawn consent · recording outside boundary · leading scenario · unlinked decision + diff --git a/web/public/assets/editorial/2025/tool-ux-research-2025-research-feedback-loop.svg b/web/public/assets/editorial/2025/tool-ux-research-2025-research-feedback-loop.svg new file mode 100644 index 0000000..7b2354b --- /dev/null +++ b/web/public/assets/editorial/2025/tool-ux-research-2025-research-feedback-loop.svg @@ -0,0 +1,12 @@ + + Цикл исследовательской обратной связиЦикл из согласия, нейтрального сценария, обезличенного наблюдения, кодирования, решения и следующего сценария. Негативные ветви останавливают процесс до изменения интерфейса. + Research feedback loop: evidence before UI change + same neutraltask scenario + consent boundary + observation + code + decision + follow-up + new isolated record + uncertainty recorded + + boundary fails → STOP, no UI claim or release + diff --git a/web/scripts/upgrade-2025-08.mjs b/web/scripts/upgrade-2025-08.mjs new file mode 100644 index 0000000..d4b5dc3 --- /dev/null +++ b/web/scripts/upgrade-2025-08.mjs @@ -0,0 +1,212 @@ +function escapeHtml(value) { + return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); +} + +const p = (text) => '

' + text + '

'; +const h2 = (text) => '

' + text + '

'; +const code = (text) => '
' + escapeHtml(text) + '
'; +const ol = (items) => '
    ' + items.map((item) => '
  1. ' + item + '
  2. ').join('') + '
'; +const figure = (src, alt, caption) => '
' + alt + '
' + caption + '
'; +const table = (caption, headers, rows) => '
' + headers.map((item) => '').join('') + '' + rows.map((row) => '' + row.map((item) => '').join('') + '').join('') + '
' + caption + '
' + item + '
' + item + '
'; +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>[\s\S]*?(?=

|$)/, '')); } +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: 'GOV.UK Service Manual: Getting informed consent for user research', + url: 'https://web.archive.org/web/20250702004027id_/https://www.gov.uk/service-manual/user-research/getting-users-consent-for-research', + version: 'immutable archive capture 2025-07-02T00:40:27Z of official GOV.UK guidance; published 24 May 2016, last updated 5 November 2018', + claim: 'Информированное согласие описывает цель, собираемые данные, наблюдение или запись, использование результатов, добровольность и возможность отзыва.', + boundary: 'Это руководство для государственных сервисов Великобритании, а не универсальная юридическая политика или готовая форма для другой организации.', + }, + { + title: 'GOV.UK Service Manual: Taking notes and recording user research sessions', + url: 'https://web.archive.org/web/20250702003709id_/https://www.gov.uk/service-manual/user-research/taking-notes-and-recording-user-research-sessions', + version: 'immutable archive capture 2025-07-02T00:37:09Z of official GOV.UK guidance; published 25 June 2017', + claim: 'Заметки и записи требуют согласия; при чувствительной теме руководство предлагает избегать идентифицирующих деталей и проверять материалы перед распространением.', + boundary: 'Источник не устанавливает качество конкретного интервью, не задаёт коды наблюдений и не доказывает, что обезличивание устраняет весь риск.', + }, + { + title: 'GOV.UK Service Manual: Plan a round of user research', + url: 'https://web.archive.org/web/20250702003743id_/https://www.gov.uk/service-manual/user-research/plan-round-of-user-research', + version: 'immutable archive capture 2025-07-02T00:37:43Z of official GOV.UK guidance; published 24 May 2016', + claim: 'План предлагает начать с проблем, предположений и того, что нужно узнать для следующего решения, а не с заранее выбранного решения интерфейса.', + boundary: 'Это планировочная рекомендация. Она не превращает один сценарий, одну цитату или один synthetic пример в подтверждение гипотезы.', + }, +]); +function sourceList() { return ''; } + +const MODEL_VERSION = 'p90-tool-ux-research-fixed-synthetic-v1'; +const SYNTHETIC_BOUNDARY = 'Все consent cards, роли, сценарии, наблюдения, коды, темы, решения и follow-up ниже — fixed synthetic JavaScript literals только в памяти. Модуль не читает и не пишет files, не использует сеть, clock, telemetry, production, PII, записи, real API, Git или CI и не выполняет side effect.'; +const CONSENT_KEYS = ['id', 'syntheticOnly', 'purpose', 'permittedEvidence', 'recording', 'observerLabels', 'withdrawal', 'status']; +const SCENARIO_KEYS = ['id', 'syntheticOnly', 'roleLabel', 'task', 'startingState', 'successCondition', 'neutralPrompt', 'forbiddenPrompt']; +const OBSERVATION_KEYS = ['id', 'syntheticOnly', 'scenarioId', 'evidenceKind', 'statement', 'codeIds', 'uncertainty']; +const DECISION_KEYS = ['id', 'syntheticOnly', 'observationIds', 'theme', 'candidateChange', 'claim', 'followUp', 'status']; +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)); } +function clone(value) { return JSON.parse(JSON.stringify(value)); } + +const FIXED_CODEBOOK = deepFreeze({ + 'code-owner-unclear-v1': { label: 'owner-unclear', meaning: 'Наблюдение описывает невозможность найти владельца следующего шага; не объясняет причину задержки.' }, + 'code-status-hidden-v1': { label: 'status-hidden', meaning: 'Наблюдение описывает отсутствие видимого статуса; не утверждает, что пользователь не умеет работать.' }, + 'code-language-mismatch-v1': { label: 'language-mismatch', meaning: 'Термин интерфейса не совпал со словом в задаче; не доказывает, что его надо немедленно переименовать.' }, +}); +const FIXED_CONSENT = deepFreeze({ + id: 'synthetic-consent-notes-only-v1', syntheticOnly: true, + purpose: 'Observe one fixed task scenario to locate a UI question, not to evaluate a person.', + permittedEvidence: ['de-identified-note'], recording: 'not-permitted', observerLabels: ['synthetic-researcher', 'synthetic-note-taker'], + withdrawal: 'stop-and-discard-synthetic-observation', status: 'confirmed', +}); +const FIXED_SCENARIO = deepFreeze({ + id: 'synthetic-scenario-access-request-v1', syntheticOnly: true, roleLabel: 'synthetic-engineer', + task: 'Find the current state of one fixed access request and decide which stated owner can answer the next question.', + startingState: 'A fixed request card exists with a request label, a status label and an owner field.', + successCondition: 'The declared next question and owner are identified; no real request is opened or changed.', + neutralPrompt: 'Покажите, как вы бы разобрались со статусом этой заявки. Комментируйте только то, что видите на карточке.', + forbiddenPrompt: 'Вам ведь не хватает большой кнопки «Написать владельцу», верно?', +}); +const FIXED_OBSERVATIONS = deepFreeze([ + { id: 'synthetic-observation-owner-v1', syntheticOnly: true, scenarioId: FIXED_SCENARIO.id, evidenceKind: 'de-identified-note', statement: 'На карточке сначала прочитан статус, затем курсор остановился у поля owner; следующий вопрос сформулирован как «кто отвечает после отправки?».', codeIds: ['code-owner-unclear-v1'], uncertainty: 'Одна fixed заметка не показывает частоту вопроса и не объясняет, почему owner не виден.' }, + { id: 'synthetic-observation-status-v1', syntheticOnly: true, scenarioId: FIXED_SCENARIO.id, evidenceKind: 'de-identified-note', statement: 'Термин «processing» был прочитан, но не связан с ожидаемым следующим действием в task scenario.', codeIds: ['code-status-hidden-v1', 'code-language-mismatch-v1'], uncertainty: 'Наблюдение не доказывает, что термин непонятен другим людям или что его нужно заменить.' }, +]); +const FIXED_DECISION = deepFreeze({ + id: 'synthetic-decision-owner-status-v1', syntheticOnly: true, observationIds: ['synthetic-observation-owner-v1', 'synthetic-observation-status-v1'], theme: 'next-step-visibility', + candidateChange: 'Add a compact fixed next-step block that names the declared owner and explains the status in task language.', + claim: 'not-established: the model does not claim that the block makes the tool easier, faster or better.', + followUp: 'Repeat only the same fixed neutral scenario with a separately declared synthetic observation record; stop if consent, scenario or evidence boundary differs.', status: 'ready-for-synthetic-follow-up', +}); +const FIXED_CASES = deepFreeze({ + 'fixed-valid-v1': { consent: FIXED_CONSENT, scenario: FIXED_SCENARIO, observations: FIXED_OBSERVATIONS, decision: FIXED_DECISION }, + 'fixed-withdrawn-consent-v1': { consent: { ...FIXED_CONSENT, status: 'withdrawn' }, scenario: FIXED_SCENARIO, observations: FIXED_OBSERVATIONS, decision: FIXED_DECISION }, + 'fixed-recording-outside-consent-v1': { consent: { ...FIXED_CONSENT, permittedEvidence: ['de-identified-note', 'screen-recording'] }, scenario: FIXED_SCENARIO, observations: FIXED_OBSERVATIONS, decision: FIXED_DECISION }, + 'fixed-leading-prompt-v1': { consent: FIXED_CONSENT, scenario: { ...FIXED_SCENARIO, neutralPrompt: FIXED_SCENARIO.forbiddenPrompt }, observations: FIXED_OBSERVATIONS, decision: FIXED_DECISION }, + 'fixed-unlinked-decision-v1': { consent: FIXED_CONSENT, scenario: FIXED_SCENARIO, observations: FIXED_OBSERVATIONS, decision: { ...FIXED_DECISION, observationIds: ['missing-observation'] } }, +}); + +function inspectFixedResearch(input) { + if (!input || !Object.values(FIXED_CASES).includes(input)) return deepFreeze({ accepted: false, reasons: ['unrecognised-fixed-input'], productionEffect: 'not-attempted' }); + const reasons = []; + const { consent, scenario, observations, decision } = input; + if (!hasKeys(consent, CONSENT_KEYS) || consent.status !== 'confirmed') reasons.push('consent-not-confirmed'); + if (!Array.isArray(consent.permittedEvidence) || consent.permittedEvidence.length !== 1 || consent.permittedEvidence[0] !== 'de-identified-note' || consent.recording !== 'not-permitted') reasons.push('consent-boundary-notes-only-required'); + if (!hasKeys(scenario, SCENARIO_KEYS) || scenario.neutralPrompt === scenario.forbiddenPrompt || /верно\?$/i.test(scenario.neutralPrompt)) reasons.push('neutral-task-scenario-required'); + if (!Array.isArray(observations) || observations.length < 1 || observations.some((item) => !hasKeys(item, OBSERVATION_KEYS) || item.scenarioId !== scenario.id || item.evidenceKind !== 'de-identified-note' || item.codeIds.some((id) => !FIXED_CODEBOOK[id]) || !item.uncertainty)) reasons.push('coded-observation-boundary-invalid'); + const ids = new Set((observations || []).map((item) => item.id)); + if (!hasKeys(decision, DECISION_KEYS) || !decision.observationIds.every((id) => ids.has(id)) || !decision.claim.startsWith('not-established') || !decision.followUp.includes('same fixed neutral scenario')) reasons.push('decision-not-traceable-to-evidence'); + return deepFreeze({ scope: MODEL_VERSION, accepted: reasons.length === 0, reasons, consentBoundary: consent.recording, observationCount: Array.isArray(observations) ? observations.length : 0, decision: reasons.length ? 'stop-before-change' : 'synthetic-follow-up-only', productionEffect: 'not-attempted' }); +} +export function createFixedResearchInput(caseId) { return FIXED_CASES[caseId] || null; } +export function inspectToolUxResearch(input) { return inspectFixedResearch(input); } +export function prepareSyntheticFollowUp(report) { return deepFreeze({ kind: 'fixed-synthetic-tool-ux-follow-up-v1', accepted: report?.accepted === true, status: report?.accepted ? 'observe-same-fixed-scenario-only' : 'stop-and-repair-boundary', reasons: report?.reasons || ['missing-report'], productionEffect: 'not-attempted' }); } +export function runToolUxResearchFixture() { + const valid = inspectFixedResearch(FIXED_CASES['fixed-valid-v1']); + const withdrawn = inspectFixedResearch(FIXED_CASES['fixed-withdrawn-consent-v1']); + const recording = inspectFixedResearch(FIXED_CASES['fixed-recording-outside-consent-v1']); + const leading = inspectFixedResearch(FIXED_CASES['fixed-leading-prompt-v1']); + const unlinked = inspectFixedResearch(FIXED_CASES['fixed-unlinked-decision-v1']); + return deepFreeze({ assertions: deepFreeze({ validAccepted: valid.accepted, validFollowUpOnly: prepareSyntheticFollowUp(valid).status === 'observe-same-fixed-scenario-only', notesOnly: valid.consentBoundary === 'not-permitted', observationCount: valid.observationCount === 2, withdrawnStops: withdrawn.reasons.includes('consent-not-confirmed'), recordingStops: recording.reasons.includes('consent-boundary-notes-only-required'), leadingStops: leading.reasons.includes('neutral-task-scenario-required'), unlinkedStops: unlinked.reasons.includes('decision-not-traceable-to-evidence'), unknownStops: !inspectFixedResearch({}).accepted, fixedConsentFrozen: Object.isFrozen(FIXED_CONSENT), fixedScenarioFrozen: Object.isFrozen(FIXED_SCENARIO), fixedObservationsFrozen: Object.isFrozen(FIXED_OBSERVATIONS), fixedCodebookFrozen: Object.isFrozen(FIXED_CODEBOOK), noProduction: valid.productionEffect === 'not-attempted', followUpNoProduction: prepareSyntheticFollowUp(valid).productionEffect === 'not-attempted' }) }); +} + +const practice = revision({ slug: 'editorial-2025-08-practice-tool-ux-research', title: 'Исследование UX внутреннего инструмента: наблюдать задачу, а не свою кнопку', categories: ['Инструменты', 'UX'], cover: '/assets/editorial/2025/tool-ux-research-2025-evidence-map.svg', excerpt: 'Практический маршрут: согласие, нейтральный сценарий, обезличенная заметка и следующий проверяемый шаг вместо цитат в пользу уже выбранного интерфейса.', readingMinutes: 12 }, [ + p('Внутренний инструмент получает просьбу «сделать удобнее», и интервью быстро подтверждает готовую кнопку автора. Наблюдаемый симптом — вопрос звучит как подсказка решения, а заметка хранит удачную цитату вместо пути по задаче. Цена ошибки — команда тратит разработку на локальную правку, но коллега по-прежнему не понимает следующий шаг и возвращается с тем же вопросом.'), + p('Причина в том, что интервью используют как голосование за макет. Проверка проще: убрать название контроля, дать человеку нейтральную задачу и записать только наблюдаемое действие, вопрос и границу неизвестного. Действие — связать заметку с code и decision log, где изменение остаётся гипотезой до следующего сценария.'), + h2('Начинаем не с вопроса «нравится ли»'), + p('Для внутреннего инструмента полезно выбрать одну работу, а не абстрактное мнение. В fixed сценарии инженер должен понять состояние одной заявки и найти владельца следующего вопроса. Он не просит оценить экран и не говорит «кнопка связи». Поэтому ответ может опровергнуть автора: человек может сразу найти owner, но не понять status; может наоборот знать статус, но не знать, куда адресовать исключение. Оба пути ценнее согласия с заранее нарисованным решением.'), + figure('/assets/editorial/2025/tool-ux-research-2025-evidence-map.svg', 'Карта доказательств fixed synthetic UX-исследования: consent card ограничивает тип заметки, нейтральный сценарий ведёт к наблюдениям с кодами и неопределённостью, затем decision log разрешает только следующий сценарий, а не автоматическое изменение интерфейса.', 'Карта показывает границы передачи: observation не равен theme, theme не равна решению, а решение не является доказанным эффектом.'), + table('Что фиксировать в карточке сессии', ['Поле', 'Fixed пример', 'Зачем', 'Чего не доказывает'], [['Purpose', 'понять следующий шаг по заявке', 'не подменять цель оценкой личности', 'что интерфейс плох для всех'], ['Evidence', 'de-identified note', 'не вынести лишний материал за consent boundary', 'что заметка безопасна при любом контексте'], ['Scenario', 'проверить статус и owner', 'дать наблюдаемый маршрут', 'частоту проблемы'], ['Uncertainty', 'неясна причина невидимости owner', 'не склеить факт и объяснение', 'причинность'], ['Follow-up', 'тот же fixed scenario', 'проверить candidate change', 'рост удобства в production']]), + h2('Минимальные определения для практического прохода'), + p('Нейтральный task scenario — это задача с исходным состоянием и критерием окончания, но без названия будущей кнопки. Informed consent, информированное согласие, задаёт цель, допустимый материал и возможность остановиться. Consent boundary — его техническая граница: в этом примере разрешена только de-identified note, то есть обезличенная заметка без записи экрана, имени, тикета или production-данных.'), + p('Evidence coding здесь означает короткую метку к конкретной заметке. Она не объясняет поведение человека. Code owner-unclear говорит только, что в одном маршруте следующий owner не найден. Рядом обязательна uncertainty: неизвестна частота этого вопроса и причина невидимости поля. Такая пара не позволяет превратить единичную фразу в утверждение о всех коллегах.'), + h2('Воспроизводимый маршрут практики'), + code("import { createFixedResearchInput, inspectToolUxResearch } from './upgrade-2025-08.mjs';\n\nconst report = inspectToolUxResearch(\n createFixedResearchInput('fixed-leading-prompt-v1'),\n);\nconsole.log(report.reasons);\n// Expected: ['neutral-task-scenario-required'].\n// The input is fixed in-memory synthetic data; no session or UI is started."), + p('Этот negative path намеренно подменяет task prompt ведущим вопросом о большой кнопке. Инспектор останавливает hand-off до coding и до candidate change. Он не определяет качество реальной беседы по одному шаблону текста: это ограниченный executable пример того, что сценарий задачи и просьба подтвердить решение автора — разные входы.'), + h2('Шесть действий ведущего сессии'), + ol(['Записать неизвестное решение. Не «добавить кнопку», а «понять, что мешает назвать следующий шаг».', 'Показать consent card до сценария. Уточнить цель, notes-only boundary, отсутствие записи и путь отзыва.', 'Дать задачу без интерфейсной подсказки. Попросить разобраться со status и owner, а не оценить нарисованный блок.', 'Записать маршрут буквально. Порядок прочтения, остановка, вопрос; без ярлыка «запутался».', 'Добавить code и uncertainty. Метка связывает заметку с дальнейшим разбором, а неизвестное не даёт раздуть её силу.', 'Передать только candidate follow-up. Новая проверка повторяет ту же задачу; выпуск UI не следует из одной заметки.']), + p(SYNTHETIC_BOUNDARY), + h2('Consent card — это техническая граница материала'), + p('В руководстве GOV.UK informed consent включает цель, собираемые данные, способ использования, наблюдение или запись, добровольность и возможность отзыва. Для инженера это не формальность в конце календарного приглашения. Карточка определяет, какие поля имеет право получить протокол. Если согласованы только обезличенные notes, исследователь не добавляет запись «на всякий случай», не пересылает экран и не оставляет имя в decision log.'), + p('В модели consent status withdrawn приводит к stop. Второй negative path разрешает screen recording при notes-only boundary и тоже останавливает работу. Такой код не исполняет удаление реальных материалов; у него нет файлов и реальных записей. Он делает видимым правило, которое легко потерять в ручной таблице: evidence вне согласованного набора не превращается в «полезный дополнительный контекст».'), + h2('Заметка сначала, тема потом'), + p('Хорошая observation record скучна: «прочитан status; курсор остановился у owner; сформулирован вопрос о следующем ответственном». В ней нет фразы «пользователь растерялся» и нет диагноза команды. Code owner-unclear связывает запись с темой, но его значение ограничено: он указывает, что owner не найден в этом маршруте, а не почему это случилось и не сколько людей столкнутся с тем же.'), + p('Неопределённость записывают рядом с кодом. Это экономит спор: дизайнер не обязан доказывать, что текст плох, а инженер не обязан угадывать, что имелось в виду. Следующий вопрос становится конкретным: показать ли owner до отправки, пояснить ли status, изменить ли порядок полей или сначала собрать ещё один observation. Когда evidence не различает варианты, решение — не «самый убедительный спикер», а новый сценарий.'), + h2('Ограничение и следующий шаг'), + p('Один сценарий не является исследованием всей организации. Он не даёт метрик, денег, реальных сотрудников, записей, репрезентативности или причины задержки. GOV.UK guidance поддерживает идею явного согласия и планирования вопросов, но не переносит свои policy на другой контекст. Следующий шаг: возьмите одну внутреннюю задачу, вычеркните из prompt название будущей кнопки и добавьте к каждой заметке одну строку uncertainty. Ожидаемый результат — decision log содержит гипотезу, которую можно опровергнуть на следующем проходе.'), + h2('Историческая граница августа 2025'), + p('Использованы три первичных руководства GOV.UK, опубликованные до 31 августа 2025; для каждого в источниках видна датированная версия страницы. Они применены только к consent, заметкам и постановке исследовательского вопроса. Fixed data model, codebook, stop reasons и candidate change — инженерская конструкция автора. Она не доказывает, что гипотеза верна, и не подменяет policy, legal review или живое исследование.'), +]); + +const mechanism = revision({ slug: 'editorial-2025-08-mechanism-tool-ux-research', title: 'Исследование UX внутреннего инструмента: от заметки к решению без удобной причинности', categories: ['Инструменты', 'UX'], cover: '/assets/editorial/2025/tool-ux-research-2025-observation-decision-matrix.svg', excerpt: 'Механика evidence coding: отделить факт от темы, тему от candidate change и остановить вывод, если consent, сценарий или ссылка на наблюдение неполны.', readingMinutes: 13 }, [ + p('После внутреннего интервью в заметках остаётся фраза «коллеге неудобно», а в задаче уже появляется выбранный редизайн. Наблюдаемый симптом — решение не содержит исходного действия, consent boundary или неизвестного; его нельзя перепроверить. Цена ошибки — невидимая подмена: команда называет правку UX-исследованием и теряет время, когда следующий сценарий снова не проходит.'), + p('Причина — четыре разных объекта сливаются в одну цитату. Наблюдение говорит, что произошло; code группирует похожие записи; theme формулирует проверяемую область; decision выбирает следующий шаг. Проверка — требовать ссылки decision на конкретные observation ids и явный статус not-established. Действие — остановить карточку, если связь, consent или neutral scenario отсутствуют.'), + h2('Четыре уровня evidence'), + p('Observation — это самый узкий уровень. В fixed модели он сообщает последовательность на карточке заявки и вопрос об owner. Codebook даёт этому фрагменту метку owner-unclear. Theme next-step-visibility объединяет owner и status только как рабочий вопрос: видит ли исполнитель, что делать после текущего состояния. Candidate change добавляет компактный блок следующего шага, но не заявляет, что он «улучшит UX». Так каждая ступень сохраняет собственную силу утверждения.'), + figure('/assets/editorial/2025/tool-ux-research-2025-observation-decision-matrix.svg', 'Матрица observation-to-decision: две fixed заметки с кодами ведут к теме next-step visibility; candidate change допускается только с ссылками на обе заметки и с claim not-established. Отсутствие consent, нейтральности или ссылки даёт STOP.', 'Матрица намеренно не содержит процент удобства. Она показывает, какое доказательство разрешает следующий synthetic follow-up, а не выпуск интерфейса.'), + table('Граница силы утверждения', ['Уровень', 'Содержит', 'Проверка', 'Запрещённый вывод'], [['Observation', 'видимое действие или вопрос', 'есть scenario id и uncertainty', '«все не понимают»'], ['Code', 'короткая метка записи', 'code существует в codebook', '«код объяснил причину»'], ['Theme', 'объединённый вопрос', 'можно назвать альтернативы', '«нужна именно эта кнопка»'], ['Decision', 'candidate change и follow-up', 'есть ссылки на observations', '«изменение уже помогло»']]), + h2('Словарь механизма: что разрешает каждая запись'), + p('Consent boundary отвечает на вопрос «какой evidence разрешён». Observation отвечает «что произошло в заданном сценарии». Code — стабильное имя наблюдаемой детали; theme — вопрос, который объединяет несколько coded observations. Decision log — карточка следующего действия с ссылками на evidence. Эти уровни нельзя переставлять: theme без observations становится мнением, а decision без theme — списком любимых UI-идей.'), + p('В fixed наборе разрешены de-identified notes, а screen recording запрещён. Это не готовая политика хранения. Настоящая команда отдельно определяет владельца, срок, доступ и процесс отзыва. Модель проверяет только собственный контракт: один тип evidence, confirmed consent, существующие codes и ссылочная целостность decision. Если хоть одна граница нарушена, итог — stop-before-change.'), + h2('Воспроизводимый разбор связи evidence и решения'), + code("import { createFixedResearchInput, inspectToolUxResearch, prepareSyntheticFollowUp } from './upgrade-2025-08.mjs';\n\nconst input = createFixedResearchInput('fixed-unlinked-decision-v1');\nconst report = inspectToolUxResearch(input);\nconsole.log(prepareSyntheticFollowUp(report));\n// Expected: stop-and-repair-boundary with an untraceable decision.\n// No files, recordings, APIs or production data participate."), + p('В этой ветке candidate change выглядит правдоподобно, но ссылается на отсутствующий observation id. Поэтому нельзя проверить, что решение выросло из работы по задаче, а не из памяти workshop. Инспектор не предлагает другую кнопку и не ремонтирует ссылку сам. Он возвращает причину, после которой владелец должен либо восстановить evidence, либо сформулировать новый сценарий.'), + h2('Проверка механики по порядку'), + ol(['Проверить consent status. Withdrawn означает stop, даже если заметка кажется полезной.', 'Проверить тип evidence. Notes-only boundary не расширяется записью ради более удобного анализа.', 'Проверить prompt. Scenario описывает действие и успех, а не ответ, который хочет услышать автор.', 'Проверить coding. У каждой observation есть допустимый code и uncertainty, иначе theme не имеет опоры.', 'Проверить ссылки decision. Каждый cited id должен существовать в том же наборе наблюдений.', 'Ограничить hand-off. Положительный результат разрешает лишь isolated follow-up на той же задаче.']), + p('Пакет хранит только fixed synthetic JavaScript literals в памяти. Он не читает, не пишет и не отправляет данные; `productionEffect` всегда `not-attempted`. Поэтому пример воспроизводит проверку контракта, но не проверяет людей, реальный интерфейс или effect candidate change.'), + h2('Почему neutral scenario важнее красивого interview guide'), + p('Ведущий вопрос сокращает дорогу к согласию, но убирает информацию. Формулировка «вам ведь не хватает большой кнопки?» уже содержит объект, размер и оценку. Нейтральный сценарий говорит только о задаче: найти состояние заявки и назвать следующий owner. Он позволяет наблюдать обход, термин, отсутствие статуса или другой путь. Если исследователь хочет проверить конкретную кнопку, это отдельный usability test после того, как проблема описана без решения.'), + p('Fixture показывает этот разрыв буквально. Когда neutralPrompt заменён на forbidden prompt, инспектор возвращает neutral-task-scenario-required. В реальной работе нельзя свести качество вопроса к регулярному выражению. Но эта fixed проверка полезна как отрицательный пример: модель обязана различать сценарий задачи и аргумент автора, иначе удачный ответ будет встроен в входные данные.'), + h2('Decision log должен говорить, чего он не знает'), + p('Полезный decision log не прячет неуверенность за словами «инсайт» и «паттерн». Он перечисляет observation ids, тему, candidate change, владельца следующего шага и границу претензии. В данном пакете claim начинается с not-established: ни удобство, ни скорость, ни снижение нагрузки не были измерены. Это не ослабляет документ. Наоборот, reviewer видит, что обсуждать: достаточно ли evidence для следующего сценария, а не для массового запуска.'), + p('Вторая защита — одинаковый сценарий follow-up. Если baseline просит проверить статус, а после правки просит «оценить новый блок», сравниваются разные действия и контекст. Fixed record требует тот же objective, starting state и neutral prompt. Это не экспериментальный дизайн и не случайное распределение. Это минимальная защита от очень частой ошибки: принять новую формулировку вопроса за эффект новой поверхности.'), + h2('Отрицательные ветки — часть исследования'), + p('Consent withdrawn останавливает работу до coding. Recording outside notes-only boundary останавливает её даже при полезной заметке. Unlinked decision останавливает candidate change, потому что reviewer не может восстановить его происхождение. Эти стопы не означают, что реальный человек или организация нарушили правило: все values synthetic. Они показывают, что безопасная система не должна лечить пробел в evidence догадкой или удачной цитатой.'), + h2('Ограничение и следующий шаг'), + p('Codebook не является нейтральной машиной истины: выбор меток влияет на то, что команда замечает. Руководства GOV.UK помогают описать consent и план, но не дают универсальный список кодов и не переносят согласие одной организации на другую. Следующий шаг: возьмите один старый research note, отдельно выпишите факт, code, theme и decision. Если decision нельзя связать с конкретной заметкой, не спорьте о формулировке кнопки — сначала заведите новый нейтральный сценарий.'), + h2('Историческая граница августа 2025'), + p('Источники относятся к доступным до 31 августа 2025 руководствам GOV.UK; их даты и границы приведены в списке источников. Из них взяты только требования к осмысленному согласию, работе с заметками и планированию вопросов. Разделение evidence levels, exact JS literals и stop rules не являются предписанием GOV.UK и не доказывают реальный outcome.'), +]); + +const field = revision({ slug: 'editorial-2025-08-field-tool-ux-research', title: 'Полевой журнал UX внутреннего инструмента: short evidence before UI change', categories: ['Инструменты', 'UX'], cover: '/assets/editorial/2025/tool-ux-research-2025-research-feedback-loop.svg', excerpt: 'Полевой формат: собрать consent boundary, observation record, uncertainty, decision log и follow-up так, чтобы изменение интерфейса можно было честно проверить на следующей задаче.', readingMinutes: 13 }, [ + p('Команда получает просьбу «починить форму заявки» и сразу собирает board из цитат. Наблюдаемый симптом — в карточке решения нет ни задачи, ни границы согласия, ни того, что именно видел коллега. Цена ошибки — новый интерфейс становится дорогим спором: никто не может показать, какая проблема была проверена, а следующая обратная связь снова сводится к вкусу.'), + p('Причина — материал исследования живёт в заметках, а изменение — в отдельной задаче. Проверка — вести короткий журнал, в котором consent card, scenario, observation, code, uncertainty, decision и follow-up соединены идентификаторами. Действие — выпускать только hypothesis for follow-up; если журнал не восстанавливает путь к заметке, интерфейс не меняют под видом evidence.'), + h2('Минимальный журнал помещается в один review'), + p('Журнал не заменяет репозиторий записей и не собирает персональные данные. Ему достаточно семи полей: purpose, consent boundary, scenario, observation, code, uncertainty, decision. Для fixed примера purpose — найти следующий шаг в заявке; consent разрешает только de-identified note; scenario не называет будущий блок; decision предлагает проверить компактное пояснение status и owner. Каждая строка указывает, что она знает, а что оставляет неизвестным.'), + figure('/assets/editorial/2025/tool-ux-research-2025-research-feedback-loop.svg', 'Замкнутый цикл fixed synthetic UX-исследования: consent card ограничивает запись, нейтральный task scenario порождает observation record с code и uncertainty, decision log формирует candidate change, затем тот же сценарий создаёт новый isolated record или STOP. Цикл не содержит автоматического релиза.', 'Цикл отделяет исследовательскую обратную связь от delivery: решение порождает следующий проверяемый материал, но не объявляет результат интерфейса заранее.'), + table('Fixed synthetic decision log', ['Поле', 'Значение', 'Вопрос reviewer'], [['Consent boundary', 'de-identified note only; no recording', 'какой материал вообще допустим?'], ['Scenario', 'найти status и next owner', 'нет ли подсказки решения?'], ['Observation ids', 'owner-v1, status-v1', 'что именно было замечено?'], ['Theme', 'next-step visibility', 'какие альтернативы ещё возможны?'], ['Candidate change', 'блок owner + status explanation', 'что проверяем, а не обещаем?'], ['Claim', 'not-established', 'какой эффект запрещено заявлять?'], ['Follow-up', 'тот же fixed neutral scenario', 'с чем сравним следующий record?']]), + h2('Поля журнала и их самостоятельный смысл'), + p('В полевом журнале consent boundary — не ссылка на шаблон, а ответ: можно ли использовать именно эту заметку. Scenario фиксирует работу без UI-подсказки. Observation сохраняет видимое действие, code даёт ей короткое имя, uncertainty ограничивает трактовку. Decision log связывает эти элементы и формулирует follow-up. Такое разнесение делает журнал полезным, когда автор сессии недоступен: reviewer восстанавливает цепочку без пересказа встречи.'), + p('Термин traceability означает прослеживаемость решения назад до наблюдения. Он не равен доказанной причинности. Если карточка советует добавить owner block, reviewer должен увидеть две заметки, из которых возникла тема next-step visibility, и то, что они не устанавливают частоту или эффект. Иначе один удачный рассказ незаметно становится нормативом для всего внутреннего инструмента.'), + h2('Воспроизводимый тест полевого журнала'), + code("import { createFixedResearchInput, inspectToolUxResearch, prepareSyntheticFollowUp } from './upgrade-2025-08.mjs';\n\nconst report = inspectToolUxResearch(\n createFixedResearchInput('fixed-withdrawn-consent-v1'),\n);\nconsole.log(prepareSyntheticFollowUp(report));\n// Expected: stop-and-repair-boundary.\n// This only validates fixed literals; it does not delete or retain real research material."), + p('Ветка показывает важный порядок: withdrawn consent проверяется раньше темы и UI-решения. В настоящей работе реакция на отзыв определяется policy организации и может включать удаление. Скрипт ничего не удаляет и не симулирует человека; его результат только запрещает synthetic hand-off. Поэтому он не выдаёт технический пример за инструкцию по обработке реальных материалов.'), + h2('Порядок review для одной UX-задачи'), + ol(['Открыть boundary. Убедиться, что purpose, evidence type, запись и отзыв сформулированы до чтения цитат.', 'Прочитать scenario без макета. Проверить, что задача может привести и к неудобному для автора ответу.', 'Проверить notes. В каждой остались действие или вопрос, а не оценка человека.', 'Сверить codes. Метка существует в codebook и не обещает объяснить причину наблюдения.', 'Прочитать uncertainty вслух. Она должна назвать то, чего evidence не установил.', 'Разрешить следующий шаг, не результат. Decision получает follow-up на том же сценарии или STOP при сломанной ссылке.']), + p('Все карточки, labels и outcomes в этом журнале — fixed synthetic JavaScript literals. Нет реальных сотрудников, интервью, записей, денег, метрик, файлов, API или production-систем. Положительный результат модели означает только корректный synthetic hand-off; это не usability finding и не разрешение менять внешний или внутренний продукт.'), + h2('Как провести review журнала'), + p('Сначала reviewer читает consent boundary, а не candidate change. Если цель и разрешённый evidence неясны, полезность заметки не оправдывает её использование. Затем он читает scenario вслух и вычёркивает слова, которые называют будущий control. После этого проверяет observation: есть ли действие или вопрос, code и uncertainty. Лишь в конце он смотрит на theme и решение. Такой порядок уменьшает confirmation bias — склонность видеть только то, что поддерживает первоначальную идею.'), + p('Следующий тест — traceability, прослеживаемость. Для каждого предложения в decision log reviewer открывает observation id и задаёт один вопрос: «что именно здесь наблюдали?» Если ответом служит память участника встречи, связь не восстановлена. В fixed инспекторе unlinked id даёт decision-not-traceable-to-evidence. Код не заменяет совместный разбор, но делает отсутствие ссылки явным до того, как задача попадёт в backlog.'), + h2('Как не выдать follow-up за результат'), + p('Candidate change здесь предельно маленькое: добавить блок следующего шага. У него нет обещания «сократить время», «снизить обращения» или «повысить satisfaction». Следующий synthetic hand-off допускает только новую обезличенную observation record на той же задаче. Если changed scenario добавит обучение, другое исходное состояние или вопрос об оценке блока, журнал должен написать, что условия различаются. Смешивать два маршрута и называть разницу улучшением нельзя.'), + p('Это особенно важно для внутренних инструментов, где участники знают автора и могут помогать ему ответом. Нейтральная задача, пауза после вопроса и заметка о неизвестном не устраняют социальное давление полностью. Они дают команде место, где это давление можно заметить. Если человек соглашается с решением, но не может завершить сценарий, для интерфейса важнее наблюдаемый маршрут, а не вежливое одобрение.'), + h2('Граница приватности и распространения'), + p('GOV.UK guidance подчёркивает, что notes и recordings могут содержать personal data и должны использоваться в пределах согласия. Отсюда не следует, что слово «анонимный» снимает все риски. В настоящей среде нужно заранее определить владельца данных, место хранения, доступ, срок и процесс отзыва. В этом пакете нет ни одного реального поля или записи; synthetic labels созданы именно чтобы пример нельзя было ошибочно принять за шаблон хранения данных коллег.'), + h2('Ограничение и следующий шаг'), + p('Полевой журнал не обещает объективность и не превращает две заметки в представительную выборку. Он не измеряет деньги, производительность, качество сотрудника или эффект интерфейса. Его практическая ценность уже: плохое evidence останавливается раньше разработки, а хорошее оставляет путь для проверки. Следующий шаг: добавьте в ближайшую UX-задачу ссылки на scenario и observation, одну uncertainty и статус claim. Если хотя бы одна ссылка отсутствует, заведите follow-up research task вместо UI ticket.'), + h2('Историческая граница августа 2025'), + p('Все использованные официальные руководства были опубликованы до 31 августа 2025; список источников показывает дату и ограничение каждого. Они поддерживают дисциплину согласия, работы с заметками и исследовательских вопросов. Наша модель decision log не является универсальным стандартом, не переносит consent между организациями и не доказывает корректность гипотезы о внутреннем интерфейсе.'), +]); + +export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item); +if (process.argv.includes('--verify-fixture')) { + const assertions = runToolUxResearchFixture().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');