diff --git a/editorial/production/README.md b/editorial/production/README.md
index 2104f5f..57ee7d8 100644
--- a/editorial/production/README.md
+++ b/editorial/production/README.md
@@ -1,6 +1,6 @@
# Производство редакционных партий
-На 31 июля 2026 года строгий аудит проходит 265 из 358 созданных материалов. Остальные 93 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
+На 31 июля 2026 года строгий аудит проходит 268 из 358 созданных материалов. Остальные 90 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия
diff --git a/editorial/reviews/2025-07-draft.md b/editorial/reviews/2025-07-draft.md
new file mode 100644
index 0000000..ba8bc2a
--- /dev/null
+++ b/editorial/reviews/2025-07-draft.md
@@ -0,0 +1,80 @@
+# P89 — июль 2025: метрики продукта для инженера
+
+## Область draft-пакета
+
+Пакет заменяет только три overlay-статьи:
+
+- editorial-2025-07-practice-product-metrics — «Метрики продукта для инженера: построить цепочку, а не поднять conversion».
+- editorial-2025-07-mechanism-product-metrics — «Почему рост метрики не является решением: denominator, cohort и guardrail».
+- editorial-2025-07-field-product-metrics — «Полевой журнал продуктовой метрики: evidence до решения».
+
+Единственный исполняемый артефакт — web/scripts/upgrade-2025-07.mjs. Все events, cohorts, subjects, requestId, периоды, ratios, решения и результаты — fixed synthetic in-memory JS literals. Скрипт не читает и не пишет files, Git, network, CI, clock, telemetry, production или user data и не выполняет side effect. Пакет не подключает overlay к registry, не меняет README, очередь, articles.json, app-страницы, docs, staging, commit или push.
+
+## Исследование и историческая граница
+
+Проверка источников выполнена 31.07.2026 обычными HTTPS-запросами без авторизации и TLS bypass. Историческая граница — 31 июля 2025: OpenTelemetry закреплён immutable commit, работы Microsoft Research опубликованы в 2017 и 2019 годах.
+
+| Источник | Закрепление | Узко подтверждаемое утверждение | Граница |
+| --- | --- | --- | --- |
+| [OpenTelemetry events](https://raw.githubusercontent.com/open-telemetry/semantic-conventions/c01aa89d9a13042e56536c60975139c50e764796/docs/general/events.md) | tag v1.31.0, commit c01aa89d9a13042e56536c60975139c50e764796, 11.03.2025 | event имеет уникальное имя; convention документирует его структуру и attributes; dynamic values не включаются в event name. | Не определяет продуктовую causal effect, denominator, guardrail или ship criterion. |
+| [Safe Velocity with Controlled Rollouts](https://www.microsoft.com/en-us/research/uploads/prod/2020/06/Safe-Velocity-ICSE-SEI.pdf) | ICSE-SEIP 2019, author version | local feature, success и guardrail metrics имеют разные роли; guardrail не должен деградировать при rollout. | Описывает controlled rollouts Microsoft, не доказывает эффект произвольной local metric в другом продукте. |
+| [A Dirty Dozen Metric Interpretation Pitfalls](https://www.microsoft.com/en-us/research/uploads/prod/2020/08/2017-08-KDDMetricInterpretationPitfalls.pdf) | KDD 2017, author version | другой состав наблюдений и telemetry loss могут исказить metric delta; numerator и denominator полезно разбирать отдельно. | Не заменяет дизайн конкретного эксперимента, power analysis, privacy policy или pipeline validation. |
+
+Причинная цепочка, названия полей, synthetic input и правило HOLD — собственная инженерная конструкция статей. Они не приписаны источникам как framework guarantee.
+
+## Проход 1 — факты, техника и источники
+
+- createFixedProductMetricInput выдаёт только один из четырёх frozen fixed cases. inspectFixedProductMetric считает unique subjects для opened, confirmed и render_failed; не принимает произвольный input как доказательство.
+- Успешный case требует attribution, declared denominator и единственный cohort-period. prepareFixedProductDecision после PASS создаёт только needs-human-owner-decision; production effect остаётся not-attempted.
+- Negative paths проверены отдельно: нет attribution → missing-attribution-rule; другой период → mixed-cohort-or-period; all-events вместо unique opened subjects → wrong-denominator.
+- Из источников в тексте взяты только документированная форма события, разные роли local/success/guardrail и риск неверного состава наблюдений. Нет утверждения, что рост confirm сам доказывает причинность или что конкретный threshold применим вне synthetic example.
+
+**Вердикт прохода 1:** пройдено. Код и текст разделяют зафиксированный учебный contract, human decision и реальные продуктовые выводы.
+
+## Проход 2 — голос, плотность, ситуация и цена
+
+| Статья | Первые два абзаца | Основной текст | Артефакт | Проверяемый финал |
+| --- | --- | ---: | --- | --- |
+| practice | Изолированный рост conversion и цена false release/повторного расследования | 6 648 | causal funnel, event contract, fixture | test на confirm без requestId возвращает HOLD |
+| mechanism | Разный состав numerator/denominator и цена false positive | 6 410 | matrix, ratio contract, negative cases | событие из другого period останавливает расчёт |
+| field | Число отделено от context и цена неповторимого decision | 6 291 | decision record, evidence loop, stop example | другой инженер восстанавливает contract по карточке |
+
+Три текста соответствуют голосу М8: автор не продаёт «рост метрик», а сравнивает риск, owner и воспроизводимый путь. Термины раскрыты в момент первого использования: cohort, attribution, denominator, guardrail, local metric и decision record. Формула «симптом → причина → проверка → действие» сохраняется в каждом разделе. Синтетические числа не выдаются за telemetry, реальные деньги, людей или инциденты.
+
+**Вердикт прохода 2:** пройдено. Усиленный audit:draft подтвердил проблему и цену ошибки в первых двух абзацах, диапазон 5–15 тыс. знаков, таблицу, figure, code example, ordered sequence и источники.
+
+## Проход 3 — SVG, fixture и выпуск
+
+Последовательность фактически выполненных проверок:
+
+- node --check web/scripts/upgrade-2025-07.mjs — PASS.
+- node web/scripts/upgrade-2025-07.mjs --verify-fixture — PASS, **11/11** assertions.
+- cd web && npm run audit:draft -- scripts/upgrade-2025-07.mjs — PASS: **6 648 / 6 410 / 6 291** body chars после независимой редакторской правки.
+- xmllint --noout для трёх SVG — PASS; safety scan чист: нет script, foreignObject, javascript:, data:image и event-handler attributes.
+- Sharp renders на 375 px просмотрены вручную. Funnel сохраняет отдельную красную ветку guardrail; matrix читается в четыре строки; evidence loop показывает HOLD ветку и не обрезает подписи.
+
+| Asset | Смысл | Результат 375 px |
+| --- | --- | --- |
+| product-metrics-2025-causal-funnel.svg | change → opened → confirmed → human review и guardrail | PASS: стрелки, подписи шагов и красный guardrail видимы. |
+| product-metrics-2025-metric-guardrail-matrix.svg | четыре измерительных условия и HOLD | PASS: контраст и строки читаемы, нижняя оговорка не обрезана. |
+| product-metrics-2025-decision-evidence-loop.svg | evidence loop с веткой repair contract | PASS: пять шагов и HOLD branch различимы на узком экране. |
+
+Full build и audit:articles намеренно не запускались: overlay не подключён к registry. Их должен выполнить независимый интегратор после подключения. Commit и публикация не выполнялись.
+
+**Вердикт прохода 3:** draft-пакет готов к независимой приёмке, но не опубликован.
+
+## Выпуск после независимой приёмки
+
+### 1. Факты и техника
+
+Редактор проверил источники заново. Immutable OpenTelemetry commit `c01aa89d9a13042e56536c60975139c50e764796` от 11 марта 2025 подтверждает уникальное имя события, документированную структуру и низкую кардинальность имени. PDF Safe Velocity (ICSE-SEIP 2019) подтверждает разные роли data-quality, guardrail, success и local metrics; PDF Dirty Dozen (KDD 2017) подтверждает, что sample/telemetry mismatch может сделать delta недостоверной и требует разбирать numerator и denominator. Эти факты не используются как доказательство causal effect или ship decision для synthetic набора.
+
+Заново выполнены `node --check`, fixture 11/11 и усиленный draft audit. Все три code examples выполнены вручную: valid case заканчивается `needs-human-owner-decision`, wrong denominator и mixed period — `stop-before-product-decision` с точной причиной. Скрипт сохраняет границу: фиксированные literals в памяти, без запроса dashboard, telemetry или production action.
+
+### 2. Редактура и голос
+
+Полное чтение выявило чрезмерный общий вводный блок у mechanism и field. Он удалён и заменён на два специальных раздела: проверку ratio до интерпретации и чтение карточки решения. Поэтому статьи не пересказывают practice, а раскрывают собственный вопрос: доказательство измерения и воспроизводимое decision record. После правки основной текст составляет 6 648, 6 410 и 6 291 знак; в первых двух абзацах каждой названы проблема и цена ошибки. Термины раскрыты в контексте, а причинность оставлена проверяемой гипотезой, не обещанием фреймворка.
+
+### 3. Визуал и выпуск
+
+Три SVG прошли XML и safety scan. Независимый Sharp render на 375 px подтвердил, что causal funnel, metric matrix и evidence loop читаются без обрезания; у каждой figure есть содержательный alt и подпись. После подключения overlay целевой audit подтвердил: practice — одна figure, две таблицы и один example; mechanism и field — по одной figure, таблице и примеру. Registry содержит 259 уникальных ревизий, `git diff --check` чист, production build успешно сгенерировал 374 страницы.
diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs
index 36f68f1..77853b3 100644
--- a/web/data/editorial-revisions.mjs
+++ b/web/data/editorial-revisions.mjs
@@ -84,6 +84,7 @@ import { revisions as february2025Revisions } from '../scripts/upgrade-2025-02.m
import { revisions as march2025Revisions } from '../scripts/upgrade-2025-03.mjs';
import { revisions as april2025Revisions } from '../scripts/upgrade-2025-04.mjs';
import { revisions as may2025Revisions } from '../scripts/upgrade-2025-05.mjs';
+import { revisions as july2025Revisions } from '../scripts/upgrade-2025-07.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -173,4 +174,5 @@ export const editorialRevisions = [
...march2025Revisions,
...april2025Revisions,
...may2025Revisions,
+ ...july2025Revisions,
];
diff --git a/web/public/assets/editorial/2025/product-metrics-2025-causal-funnel.svg b/web/public/assets/editorial/2025/product-metrics-2025-causal-funnel.svg
new file mode 100644
index 0000000..eeb6fc3
--- /dev/null
+++ b/web/public/assets/editorial/2025/product-metrics-2025-causal-funnel.svg
@@ -0,0 +1,7 @@
+
diff --git a/web/public/assets/editorial/2025/product-metrics-2025-decision-evidence-loop.svg b/web/public/assets/editorial/2025/product-metrics-2025-decision-evidence-loop.svg
new file mode 100644
index 0000000..103f919
--- /dev/null
+++ b/web/public/assets/editorial/2025/product-metrics-2025-decision-evidence-loop.svg
@@ -0,0 +1 @@
+
diff --git a/web/public/assets/editorial/2025/product-metrics-2025-metric-guardrail-matrix.svg b/web/public/assets/editorial/2025/product-metrics-2025-metric-guardrail-matrix.svg
new file mode 100644
index 0000000..e5df3fc
--- /dev/null
+++ b/web/public/assets/editorial/2025/product-metrics-2025-metric-guardrail-matrix.svg
@@ -0,0 +1 @@
+
diff --git a/web/scripts/upgrade-2025-07.mjs b/web/scripts/upgrade-2025-07.mjs
new file mode 100644
index 0000000..6f38100
--- /dev/null
+++ b/web/scripts/upgrade-2025-07.mjs
@@ -0,0 +1,180 @@
+function escapeHtml(value) {
+ return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
+}
+
+const p = (text) => '
' + text + '
'; +const h2 = (text) => '' + escapeHtml(text) + '';
+const ol = (items) => '| ' + item + ' | ').join('') + '
|---|
| ' + item + ' | ').join('') + '
product.checkout_confirmed не содержит динамический id, а subject, requestId, cohort и period лежат отдельно. Но соглашение об имени не доказывает связь с решением пользователя. Эту связь сначала формулирует владелец продукта и проверяет инженерный контракт.'),
+ h2('Один fixed synthetic пример'),
+ code("import { createFixedProductMetricInput, inspectFixedProductMetric, prepareFixedProductDecision } from './upgrade-2025-07.mjs';\n\nconst report = inspectFixedProductMetric(\n createFixedProductMetricInput('fixed-valid-v1'),\n);\nconst decision = prepareFixedProductDecision(report);\n\nconsole.log({ accepted: report.accepted, cohorts: report.byCohort, status: decision.status });\n// Fixed literals only: no HTTP, file, telemetry, clock or production action."),
+ p('В наборе четыре открытия, два подтверждения и одно synthetic render failure. Модель считает unique subjects, а не число строк: один повторный event не должен незаметно увеличить denominator. У treatment один из двух открывших подтвердил действие; у control подтверждает один из двух. Это не результат реального продукта и не статистический вывод: небольшой fixed набор нужен только для проверки того, что contract сохраняет cohort, period, denominator и guardrail рядом.'),
+ table('Контракт события и роль поля', ['Поле', 'Пример', 'Проверка', 'Нельзя заключать'], [['event', 'product.checkout_opened', 'имя описывает тип факта', 'что пользователь доволен'], ['subject', 'u-b', 'deduplicate denominator', 'что это реальная личность'], ['cohort', 'treatment', 'сравнить один вариант с control', 'что варианты случайно распределены'], ['period', '2025-07-14', 'не смешать окна', 'что эффект сохранится завтра'], ['requestId', 'r-2', 'связать шаги одной попытки', 'что причина уже доказана'], ['errorType', 'synthetic-timeout', 'посчитать guardrail', 'что это production incident']]),
+ h2('Общий маршрут до решения'),
+ ol(['Назвать решение. Не «смотрим conversion», а «оставляем вариант только если подтверждение не растёт ценой failure».', 'Записать причинную цепочку. Техническое изменение → видимый шаг → действие пользователя → outcome и отдельный guardrail.', 'Зафиксировать cohort, период и denominator. Эти поля пишут до подсчёта, иначе результат подстраивают под удобный ответ.', 'Проверить negative path. Нехватка attribution, иной period или иной denominator должны остановить decision, а не превратиться в null на графике.', 'Сохранить decision record. Указать owner, evidence, ограничения и следующий запуск; human owner, а не функция, принимает product decision.']),
+ p(SYNTHETIC_BOUNDARY),
+];
+
+const practice = revision({ slug: 'editorial-2025-07-practice-product-metrics', title: 'Метрики продукта для инженера: построить цепочку, а не поднять conversion', categories: ['Метрики', 'Инженерия'], cover: '/assets/editorial/2025/product-metrics-2025-causal-funnel.svg', excerpt: 'Практический маршрут: связать изменение, событие, действие пользователя, guardrail и решение так, чтобы ошибка измерения останавливалась раньше релиза.', readingMinutes: 12 }, [
+ p('Инженер ускоряет экран и видит рост conversion на графике. Через несколько дней оказывается, что поменялся не выбор пользователя, а способ считать открытие: часть повторных попыток исчезла из denominator. Цена ошибки — выпуск решения по красивому числу, повторное расследование и потеря времени у продукта, аналитики и разработки.'),
+ p('Причина в разорванной цепочке. Latency, click и confirm часто живут в разных отчётах, а failure остаётся техническим alert. Проверка начинается не с нового dashboard: нужно назвать одно решение, один вход в воронку, одно правило attribution и один guardrail. Действие — записать их в contract до того, как смотреть delta.'),
+ h2('От технического изменения к пользовательскому решению'),
+ p('Для примера возьмём synthetic изменение: экран подтверждения получает предзагруженную форму. Гипотеза не звучит как «latency важен». Она звучит уже: «если форма открывается без дополнительного ожидания, больше пользователей, которые открыли checkout, доходят до confirm; при этом render failures не должны стать чаще». Здесь loading step — механизм, opened и confirmed — наблюдаемые точки, а failure — цена, которую нельзя спрятать за conversion.'),
+ figure('/assets/editorial/2025/product-metrics-2025-causal-funnel.svg', 'Причинная воронка fixed synthetic примера: техническое изменение ведёт к открытию checkout, подтверждению и решению пользователя; сбоку отдельно проходит guardrail render failure. Каждое ребро подписано событием и границей интерпретации.', 'Воронка не утверждает причинность сама по себе. Она заставляет до измерения указать, какой шаг наблюдаем, где attribution и какое ухудшение останавливает выпуск.'),
+ table('Цепочка и проверка', ['Звено', 'Что фиксируем', 'Симптом ошибки', 'Действие'], [['Изменение', 'variant и owner', 'невозможно сказать, что сравнивали', 'остановить карточку'], ['Открытие', 'checkout_opened, subject, period', 'denominator плавает', 'считать unique subjects'], ['Подтверждение', 'checkout_confirmed + requestId', 'шаг нельзя отнести к попытке', 'записать attribution rule'], ['Guardrail', 'render_failed / opened', 'conversion растёт вместе с отказами', 'hold и расследовать']]),
+ ...common,
+ h2('Что считать, а что не выдавать за доказательство'),
+ p('Microsoft Research различает local feature metrics, более общие success metrics и guardrail. Это хороший ориентир для раскладки, но не готовая формула для любого продукта. В нашем примере confirm/opened — local metric: он показывает, добрался ли пользователь до целевого шага. Он не равен долгосрочному outcome и не доказывает, что предзагрузка стала причиной. Поэтому decision record хранит формулировку гипотезы и отдельно пишет: «нужна проверка design и достаточного наблюдения».') ,
+ p('Если attribution отсутствует, функция возвращает missing-attribution-rule. Это важнее нулевого графика: нельзя понять, относится ли confirm к показанному варианту, другому устройству или повторной попытке. Следующий шаг не «взять больше строк», а зафиксировать ключ связи и проверить, что он есть у обоих событий. Только после этого имеет смысл обсуждать latency или текст кнопки.'),
+ h2('Ограничение и следующий шаг'),
+ p('Эта практика не заменяет randomized experiment, расчёт мощности, privacy review или реальный ownership. Fixed literals не показывают статистическую значимость и не содержат денег, команд или production telemetry. Следующий шаг: выберите одну воронку, выпишите denominator в виде множества субъектов и добавьте test, где confirmation пришёл без requestId. Ожидаемый результат — hold с понятной причиной, а не искусственный рост conversion.'),
+ h2('Историческая граница июля 2025'),
+ p('Использованы OpenTelemetry v1.31.0 от 11 марта 2025 и работы Microsoft Research 2017 и 2019 годов. Они существовали до 31 июля 2025. Из них взяты только дисциплина события, различение metric roles и риск разного набора наблюдений. Конкретная causal chain, поля и stop decisions — инженерная конструкция этой статьи, не нормативное следствие источников.'),
+]);
+
+const mechanism = revision({ slug: 'editorial-2025-07-mechanism-product-metrics', title: 'Почему рост метрики не является решением: denominator, cohort и guardrail', categories: ['Метрики', 'Качество'], cover: '/assets/editorial/2025/product-metrics-2025-metric-guardrail-matrix.svg', excerpt: 'Механика проверки: отделить movement локальной метрики от допустимого решения и остановить расчёт при смешанных периодах, неверном denominator или отсутствии attribution.', readingMinutes: 13 }, [
+ p('На отчёте treatment выглядит лучше control, но numerator и denominator собраны из разных наборов. Иногда это случается после невинной оптимизации: страница перестаёт перезагружаться, клиентский event теряется или cohort получает другое окно. Цена ошибки — false positive: команда объясняет пользователю эффект, которого не было, и выпускает изменение с неизвестным ущербом.'),
+ p('Причина не в самой дроби. Любая ratio скрывает состав числителя и знаменателя. Проверка — разложить metric на набор субъектов, события и период, затем убедиться, что treatment и control отвечают на один и тот же вопрос. Действие — сделать эти условия частью result; если хотя бы одно не выполнено, решение получает stop status.'),
+ h2('Metric — это контракт, а не имя на графике'),
+ p('В fixed model conversion задан как unique subjects с checkout_confirmed, делённое на unique subjects с checkout_opened, внутри одного cohort и одного дня. Такой contract намеренно мал. Он не говорит, сколько ждать пользователя, как обрабатывать два устройства и что считать «успешным заказом». Но он не даёт случайно заменить denominator на all-events и выдать плотность telemetry за поведение людей.'),
+ figure('/assets/editorial/2025/product-metrics-2025-metric-guardrail-matrix.svg', 'Матрица fixed synthetic расчёта: local metric, success interpretation и guardrail разделены; строки missing attribution, wrong denominator и mixed period ведут к HOLD, а не к ship.', 'Матрица показывает границу: удачный local signal разрешает только human review, а любое нарушение измерительного контракта останавливает product decision.'),
+ table('Negative paths, которые нельзя сгладить', ['Сбой', 'Почему delta нельзя читать', 'Машинный result', 'Ремонт'], [['Нет attribution', 'confirm не связан с вариантом', 'missing-attribution-rule', 'добавить ключ связи и test'], ['Смешан period', 'группы не наблюдались в одном окне', 'mixed-cohort-or-period', 'зафиксировать окно до запроса'], ['Неверный denominator', 'ratio отвечает на другой вопрос', 'wrong-denominator', 'считать unique opened subjects'], ['Guardrail деградирует', 'локальный выигрыш куплен отказами', 'eligible, но human hold', 'разобрать error path']]),
+ h2('Проверить ratio до интерпретации'),
+ p('Numerator — число подтверждений, denominator — число открывших, но оба значения имеют смысл только вместе с правилом inclusion. Cohort здесь означает вариант сравнения, attribution связывает подтверждение с попыткой, а guardrail описывает недопустимое ухудшение рядом. Эти термины не доказывают причинность: они лишь делают видимыми условия, без которых ratio отвечает на другой вопрос.'),
+ code("import { createFixedProductMetricInput, inspectFixedProductMetric, prepareFixedProductDecision } from './upgrade-2025-07.mjs';\n\nconst report = inspectFixedProductMetric(\n createFixedProductMetricInput('fixed-wrong-denominator-v1'),\n);\nconst decision = prepareFixedProductDecision(report);\n\nconsole.log({ accepted: report.accepted, reasons: report.reasons, status: decision.status });\n// Expected: wrong-denominator and stop-before-product-decision.\n// The input is fixed synthetic data in memory; no metric is sent or published."),
+ p('Этот пример не вычисляет «правильную conversion» задним числом. Он показывает, что имя метрики не переносится на другой denominator. Report с all-events получает stop до human decision, хотя набор событий внешне похож на valid case. Для реального расчёта отдельно нужны randomization, задержка доставки, политика идентификаторов и проверка pipeline.'),
+ h2('Порядок проверки сравнения'),
+ ol(['Назвать decision. Сформулировать, какое действие будет разрешено или остановлено, а не просто открыть dashboard.', 'Зафиксировать population. До запроса записать cohort, period, inclusion и attribution.', 'Разложить дробь. Показать numerator и denominator отдельно и проверить, что они относятся к одной задаче.', 'Положить рядом guardrail. Сравнить его по тому же окну и назвать владельца порога.', 'Вернуть HOLD при разрыве. Repair contract предшествует product discussion.']),
+ p(SYNTHETIC_BOUNDARY),
+ h2('Почему guardrail живёт рядом, а не в конце отчёта'),
+ p('Guardrail не обязан быть «самой важной» метрикой. Его работа проще: заранее назвать ухудшение, которое делает локальный выигрыш недостаточным. В synthetic наборе один render_failed у двух opened в control не создаёт сравнения для реального продукта. Он показывает контракт: failure имеет тот же cohort и тот же denominator base, поэтому его нельзя потерять при смене панели. В реальном проекте порог и owner определяются до запуска, а не после удачного результата.'),
+ p('Работа Safe Velocity описывает guardrail как показатель, который не должен ухудшаться при rollout, и local metric как средство объяснить изменения. Нельзя перевернуть эту фразу в гарантию: если local metric вырос, значит outcome улучшился. Между ними остаются выборка, вариант, сезонность, качество событий и другие механизмы. Поэтому автор оставляет человеческое решение последним шагом и требует доказательства, достаточного для конкретного риска.'),
+ h2('Разбор numerator и denominator'),
+ p('Работа Microsoft о metric pitfalls особенно полезна именно здесь: она показывает пример, где другой состав наблюдений делает delta ненадёжной, и предлагает декомпозицию numerator и denominator. Наше практическое следствие скромнее: вместе с числом хранить список правил, из которого оно получено. Если после изменения request path исчезла часть opened, alert должен говорить о denominator, а не радоваться conversion.'),
+ p('Проверка в модуле добавляет один confirm из следующего дня. Результат mixed-cohort-or-period останавливает расчёт до сравнения. Это не статистический тест и не защита от всех ошибок: код не проверяет рандомизацию, задержку доставки или личность subject. Зато он делает распространённую подмену видимой и не позволяет default-фильтру quietly собрать удобную выборку.'),
+ h2('Ограничение и следующий шаг'),
+ p('Не делайте из guardrail универсальный список. У одной операции критичен отказ, у другой — отмена, доступность или безопасность; метрика получает смысл только рядом с пользовательским решением и владельцем риска. Следующий шаг: возьмите одну ratio из своего dashboard, напишите её numerator и denominator словами, затем подайте в test события из другого периода. Ожидаемый результат — расчёт остановлен с причиной, которую может исправить конкретный owner.'),
+ h2('Историческая граница июля 2025'),
+ p('Microsoft Research PDF 2017 года использован для риска sample/metric mismatch, а работа 2019 года — для ролей local, success и guardrail. OpenTelemetry v1.31.0 закреплён commit от марта 2025. Они не доказывают причинность fixed example и не задают пороги. Структура decision record и её stop reasons — ограниченный проектный выбор автора.'),
+]);
+
+const field = revision({ slug: 'editorial-2025-07-field-product-metrics', title: 'Полевой журнал продуктовой метрики: evidence до решения', categories: ['Метрики', 'Процессы'], cover: '/assets/editorial/2025/product-metrics-2025-decision-evidence-loop.svg', excerpt: 'Полевой формат decision record: собрать цепочку, metric definition, guardrail, negative path и ограничение так, чтобы владелец мог принять или остановить решение.', readingMinutes: 13 }, [
+ p('Самая дорогая ошибка в продуктовой метрике часто происходит не в SQL и не в графике. В review остаётся фраза «conversion вырос», но не остаётся периода, cohort, definition и guardrail. Через неделю никто не может повторить расчёт или объяснить, почему выпуск состоялся. Цена ошибки — повторная работа, спор об owner и решение, которое нельзя защитить evidence.'),
+ p('Причина — результат отделён от решения. Dashboard хранит число, а контекст живёт в сообщениях и памяти участников. Проверка — собрать короткий decision record до discussion: изменение, causal hypothesis, event contract, denominator, cohort/period, guardrail, evidence и limitation. Действие — отправить на human decision только карточку без red flags; остальные остановить и вернуть владельцу измерения.'),
+ h2('Карточка решения должна помещаться в review'),
+ table('Fixed synthetic decision record', ['Поле', 'Значение', 'Вопрос reviewer'], [['Decision', 'leave / hold после human review', 'какой риск принимаем?'], ['Hypothesis', 'форма без ожидания помогает пройти confirm', 'какое звено наблюдаем?'], ['Attribution', 'same subject and request', 'как confirm связан с вариантом?'], ['Window', '2025-07-14', 'одинаков ли период cohort?'], ['Local metric', 'confirmed unique subjects / opened unique subjects', 'каков denominator?'], ['Guardrail', 'render_failed / opened', 'что останавливает выпуск?'], ['Limitation', 'fixed synthetic only', 'чего evidence не доказывает?']]),
+ figure('/assets/editorial/2025/product-metrics-2025-decision-evidence-loop.svg', 'Цикл evidence для product decision: определить change и hypothesis, зафиксировать event contract, проверить cohort-period-denominator и guardrail, затем создать human decision record либо HOLD с причиной ремонта.', 'Decision loop специально заканчивается человеком и ограничением. Рисунок не превращает один metric delta в автоматический ship.'),
+ h2('Карточка хранит условия решения, а не красивый итог'),
+ p('Cohort — это группа с одним вариантом, attribution — правило связи попытки и подтверждения, denominator — множество открывших, guardrail — отдельное ограничение на ущерб. В field record они нужны не для терминологической полноты, а чтобы reviewer увидел, какое поле надо вернуть владельцу измерения. Если отсутствует хотя бы одно, фраза «conversion вырос» не становится решением.'),
+ p('Здесь нет настоящего пользователя, записи сессии или telemetry: subject и requestId — fixed synthetic labels. Они показывают место для связи, но не советуют хранить реальный идентификатор. Модуль создаёт только decision hand-off или HOLD; он не запрашивает dashboard и не выпускает вариант.'),
+ h2('Как выглядит полезный negative path'),
+ p('В этой теме остановка — результат, а не исключение. Если checkout_confirmed пришёл без requestId, отчёт не должен гадать, к какому открытию его отнести. Если в treatment попал следующий день, отчёт не должен выровнять его средним. Если denominator стал all-events, расчёт не должен сохранять прежнее название conversion. Каждая ветка возвращает короткую причину и владельца ремонта: instrumentation owner, query owner или experiment owner.'),
+ code("import { createFixedProductMetricInput, inspectFixedProductMetric, prepareFixedProductDecision } from './upgrade-2025-07.mjs';\n\nconst report = inspectFixedProductMetric(\n createFixedProductMetricInput('fixed-mixed-period-v1'),\n);\nconsole.log(prepareFixedProductDecision(report));\n// Expected: stop-before-product-decision with mixed-cohort-or-period.\n// The input is fixed in-memory synthetic data; nothing is published or sent."),
+ p('Такой путь полезен на review: вместо «данных мало» он говорит, какое именно условие нарушено. Вызов не исправляет cohort и не меняет production. Он только не разрешает перейти к решению. Это важно для ответственности: владелец интерфейса не обязан чинить SQL, а владелец metric contract не обязан объяснять UX-гипотезу. Карточка связывает их по явному полю, а не по догадке.'),
+ h2('Порядок полевой проверки'),
+ ol(['Сначала прочитать решение. Если нельзя закончить фразой «оставить или остановить вариант при таких условиях», метрика ещё не выбрана.', 'Затем проверить событие. Имя, subject, cohort, period и ключ attribution должны быть записаны до query.', 'Разложить ratio. Отдельно вывести unique numerator и unique denominator; не подменять субъект строкой telemetry.', 'Посмотреть guardrail тем же срезом. Если у него другой период или population, назвать ограничение, не сравнивать молча.', 'Прогнать один отрицательный вход. Missing attribution, mixed period или wrong denominator обязан привести к HOLD.', 'Зафиксировать evidence. Ссылка на definition, запрос, owner и следующий check делают результат воспроизводимым.']),
+ h2('Что делать, когда local metric двигается'),
+ p('Сначала не объявлять успех. Сверить, что метрика использует тот же event schema и что набор субъектов не менялся. Затем показать guardrail и diagnostic split: отдельно cohorts, period и numerator/denominator. Только после этого reviewer решает, нужно ли расширять наблюдение, повторять запуск или менять реализацию. Этот порядок короче бесконечного dashboard tour, потому что каждый шаг отвечает на отдельный риск.'),
+ p('Статья о metric pitfalls предупреждает, что другой состав выборок может исказить направление delta, а потеря telemetry может быть одним из источников bias. Это подтверждает необходимость проверки, но не создаёт готовый pipeline. В конкретном проекте нужно отдельно проверить privacy, retention, идентификаторы и задержку событий. Synthetic requestId здесь лишь показывает место связи; он не является советом хранить реальный идентификатор пользователя.'),
+ h2('Ограничение и следующий проверяемый шаг'),
+ p('Полевой журнал не обещает, что команда научится доказывать все product effects. Он делает неизвестное явным: какую связь мы наблюдаем, какое ухудшение не принимаем и чего не хватает для решения. В этом пакете нет реальных пользователей, денег, инцидентов, SQL, сети или production query. Поэтому его fixture проверяет только deterministic stop branches и отсутствие side effect.'),
+ p('Следующий шаг: добавьте к одному существующему review ровно четыре строки — decision, denominator, guardrail и limitation. Затем попросите другого инженера восстановить query только по карточке. Если он не может понять cohort или period, не улучшайте визуализацию: сначала исправьте event contract. Ожидаемый результат — спор о выпуске превращается в список проверяемых условий.'),
+ h2('Историческая граница июля 2025'),
+ p('Источники ограничены материалами, существовавшими до 31 июля 2025: Microsoft Research 2017/2019 и OpenTelemetry v1.31.0. Их утверждения отделены от нашего процесса: они поддерживают ценность раздельных metric roles, документированного события и проверки состава выборки. Формат журнала, synthetic literals и решение HOLD — практическая схема автора, а не обещание framework.'),
+]);
+
+export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
+if (process.argv.includes('--verify-fixture')) {
+ const assertions = runProductMetricFixture().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');