diff --git a/editorial/production/README.md b/editorial/production/README.md
index 0dc1d91..4c92405 100644
--- a/editorial/production/README.md
+++ b/editorial/production/README.md
@@ -1,6 +1,6 @@
# Производство редакционных партий
-На 31 июля 2026 года строгий аудит проходит 256 из 358 созданных материалов. Остальные 102 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
+На 31 июля 2026 года строгий аудит проходит 259 из 358 созданных материалов. Остальные 99 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия
diff --git a/editorial/reviews/2025-03-draft.md b/editorial/reviews/2025-03-draft.md
new file mode 100644
index 0000000..1386ba0
--- /dev/null
+++ b/editorial/reviews/2025-03-draft.md
@@ -0,0 +1,175 @@
+# P85 — март 2025: поиск по инженерной базе знаний
+
+## Область draft-пакета
+
+Пакет заменяет только три overlay-статьи:
+
+- editorial-2025-03-practice-knowledge-retrieval — «Поиск по инженерной базе: источник важнее уверенного пересказа».
+- editorial-2025-03-mechanism-knowledge-retrieval — «Почему vector score не делает документ доказательством».
+- editorial-2025-03-field-knowledge-retrieval — «Когда поиск обязан остановиться: нет цитаты — нет ответа».
+
+Единственный исполняемый артефакт — web/scripts/upgrade-2025-03.mjs.
+Все records, scores, labels доступа, timestamps, expiry, URI, anchors, excerpts,
+reports, citations и decisions внутри него являются fixed synthetic in-memory
+значениями. Скрипт не читает и не пишет files, Git, network, CI, clock,
+telemetry, production, user data и не выполняет external search. Его
+synthetic:// citations не выданы за реальные документы.
+
+Пакет не меняет registry, README, очередь, articles.json,
+app-страницы, staging, commit, push или full build. Это самостоятельный draft
+для последующей интеграции.
+
+## Исследование и историческая граница
+
+Проверка URL выполнена 31.07.2026 обычными HTTPS-запросами
+curl -L --fail / curl -I -L --fail, без TLS bypass и
+без авторизации. Текущая доступность не подменяет историческую дату: для каждого
+источника ниже есть версия, RFC или immutable commit, существовавшие до марта
+2025.
+
+| Источник | Историческое закрепление | Узко подтверждаемое утверждение | Граница утверждения |
+| --- | --- | --- | --- |
+| [OpenSearch k-NN README](https://raw.githubusercontent.com/opensearch-project/k-NN/150c589849a8ec3bc442d830b43a3eaf4e25fa0c/README.md) | tag 2.15.0.0, immutable commit 150c589849a8ec3bc442d830b43a3eaf4e25fa0c, commit date 12.06.2024 | k-NN предназначен для nearest-neighbor similarity search; README упоминает filters для уточнения similarity search. | Не подтверждает истинность claim, свежесть source, право reader на fragment, полноту corpus или качество конкретного ranker. |
+| [RFC 9110, §8.8.2](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.2) | IETF Standards Track RFC, June 2022 | Last-Modified сообщает время, в которое origin server считает selected representation изменённым; способ вычисления остаётся implementation detail. | HTTP metadata не доказывает semantic correctness текста, owner, права доступа или отсутствие более новой редакции. |
+| [NIST SP 800-207 PDF](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-207.pdf) | dated final publication, 2020-08-11 | authentication и authorization описаны как отдельные функции до сессии к enterprise resource; неявное доверие по сетевому положению не предполагается. | Это не retrieval design, не схема labels и не готовая access policy для базы знаний. |
+
+Вывод «vector score не доказывает истину» отмечен в статьях как собственный
+инженерный вывод. Он следует из того, что ranking, access, freshness,
+citability и human verification отвечают на разные вопросы; он не приписан
+OpenSearch, RFC или NIST как формальная гарантия.
+
+## Проход 1 — факты, техника и границы
+
+**Проверена модель.**
+
+- createFixedSyntheticKnowledgeRetrievalInput принимает case id и
+ возвращает строго shaped input для fixed corpus.
+- В corpus есть version, access labels, publishedAt, indexedAt, expiresAt,
+ synthetic URI, exact anchor, excerpt и score. Output отдельно содержит
+ candidate records, reject reasons и citations.
+- inspectFixedSyntheticKnowledgeRetrieval пропускает в citation
+ только active, allowed, fresh и anchor-addressable record. Один score не
+ снимает ни одно из этих условий.
+- prepareSyntheticKnowledgeHumanReview создаёт лишь in-memory
+ review draft. Даже accepted citation получает статус
+ requires-human-verification, а не заверенный факт.
+- stopSyntheticKnowledgeAnswer discard-ит только accepted
+ in-memory review draft и возвращает productionEffect:
+ not-attempted. Это не withdrawal реального ответа и не изменение
+ источника.
+
+**Проверены отрицательные ветки.**
+
+| Случай | Fixed record | Ожидаемое решение | Проверяемая причина |
+| --- | --- | --- | --- |
+| freshness | synthetic-adapter-guideline-v1, score 0.97 | не становится citation | expired-at-fixed-retrieval-time |
+| access | synthetic-security-override-v2, score 0.99 | не становится citation | access-label-not-granted |
+| citation | synthetic-note-without-anchor-v1 | не становится citation | missing-exact-citation-anchor |
+| stale-only / closed-only corpus | один неразрешённый candidate | answer stops | stop-no-fresh-authorized-citable-source |
+| forged / sparse / cyclic objects | не совпадают с canonical fixed output | review / stop rejects object | нет доступа к произвольным input или report |
+
+Fixture намеренно проверяет shape, dense arrays, unknown case, extra input
+field, forged timestamp, frozen fixed corpus, forged report, sparse citations,
+циклические input/report и отсутствие external effects. Она не проверяет
+качество реального embedding, реальный identity check, HTTP fetch, corpus
+полноту или production outcome.
+
+**Вердикт прохода 1:** пройдено. Утверждения об источниках ограничены их
+содержанием; example не использует реальные данные и не делает score
+доказательством истины.
+
+## Проход 2 — редактура, голос и плотность
+
+Март 2025 соответствует уровню М8: автор ведёт от наблюдаемого симптома к
+стоимости, владельцу policy, проверяемому контракта и небольшому маршруту
+внедрения. Речь сохраняет формулу «симптом → причина → проверка → действие»,
+не объявляет fixed model реальными данными и не обещает универсальный RAG
+процесс.
+
+| Статья | Проблема и цена ошибки в первых двух абзацах | Основной текст | Практический артефакт | Проверяемый финал |
+| --- | --- | ---: | --- | --- |
+| practice | Релевантная запись может быть expired, closed или без anchor; цена — неверная миграция, раскрытие fragment или невозможность проверить правило. | 10 124 знака | source-record contract, маршрут filtering, fixed fixture | Удалить одно metadata поле и получить явную stop condition. |
+| mechanism | High-score document может описывать прежний contract; цена — повторное расследование и потеря доверия к базе. | 10 920 знаков | matrix signals, strict candidate output, filter order | Добавить один rejection test для expired / denied candidate. |
+| field | Search может найти только closed или stale source; цена — неверная миграция, disclosure или неразрешимый спор о правиле. | 10 952 знака | structured stop path, comparison table, human-review card | Создать test: top-score candidate не допускается к answer без citation. |
+
+Редакторская вычитка:
+
+- В каждом из первых двух <p> явно названы problem и
+ цена ошибки. Это повторно подтверждено усиленным
+ audit:draft.
+- В каждой статье есть HTML-table, figure с meaningful alt и
+ caption, компактный воспроизводимый example, limitation, next step и не
+ меньше двух первичных/официальных источников.
+- Длинные технические термины привязаны к полю, decision или fragment:
+ score, access labels, publishedAt, indexedAt, expiresAt, URI#anchor и human
+ verification не оставлены декларациями.
+- Source fact и policy автора разнесены явно. Ни fixed timestamps, ни
+ synthetic URI, ни scores не выданы за telemetry или реальный corpus.
+- Убраны повторы в полевой статье: объём снижен с 13,5 тыс. до 10,95 тыс.
+ знаков без потери table, stop path, limitation или next action.
+
+**Вердикт прохода 2:** пройдено. Три текста остаются в диапазоне 5–15 тыс.
+знаков и целевом диапазоне 8–11 тыс. с допустимым отклонением field-текста до
+10,95 тыс.; тон 2025 года наставнический, но технический и не стратегический.
+
+## Проход 3 — SVG, fixture и выпуск
+
+Проверены три собственных SVG:
+
+| Asset | Смысл | Проверка 375 px |
+| --- | --- | --- |
+| knowledge-retrieval-2025-query-retrieval-citation.svg | index → retrieval → access/freshness → exact citation → human verification | Первый render обнаружил обрезанные длинные подписи. Подписи сокращены, повторный Sharp-render читаем, STOP path виден полностью. |
+| knowledge-retrieval-2025-freshness-access-matrix.svg | matrix допуска: high-score expired, closed и no-anchor records отвергаются | Колонки и footer читаемы на 375 px; нет обрезания справа. |
+| knowledge-retrieval-2025-verification-loop.svg | human verification loop и видимая stop ветка | Первый render обнаружил длинные labels в карточках; после сокращения все пять шагов и STOP card читаемы на 375 px. |
+
+Последовательность проверок:
+
+- node --check scripts/upgrade-2025-03.mjs — PASS.
+- node scripts/upgrade-2025-03.mjs --verify-fixture — PASS,
+ 34/34 assertions.
+- npm run audit:draft -- scripts/upgrade-2025-03.mjs — PASS для
+ всех трёх slug, включая усиленное требование о цене ошибки в первых двух
+ <p>.
+- xmllint --noout для трёх SVG — PASS.
+- SVG safety scan — чисто: нет script,
+ foreignObject, javascript:,
+ data:image и event-handler attributes.
+- Sharp render на ширине 375 px и ручной visual review — PASS после одного
+ исправления copy length в каждом из трёх SVG.
+
+Full build намеренно не запускался. Registry, production README, очередь,
+стадия Git, commit и push намеренно не менялись. Это не освобождает будущего
+интегратора от audit:articles, production build и отдельной
+проверки registry, но сохраняет независимость P85 draft-пакета.
+
+**Вердикт прохода 3:** пройдено для draft-пакета. Публикация не выполнена.
+
+## Выпуск после независимой приёмки
+
+Основной редактор повторил три прохода после подключения overlay:
+
+1. **Факты и техника.** Закреплённый README OpenSearch k-NN по commit
+ 150c589849a8ec3bc442d830b43a3eaf4e25fa0c подтверждает дату
+ 12 июня 2024, nearest-neighbor similarity search и фильтры для уточнения
+ результатов. RFC 9110 §8.8.2 определяет `Last-Modified` как timestamp,
+ который origin server считает датой изменения selected representation, и
+ называет способ вычисления implementation detail. NIST SP 800-207
+ подтверждает отдельные dynamic authentication/authorization checks до
+ доступа к enterprise resource. Статья корректно оставляет правило о том,
+ что score не является доказательством, собственным инженерным выводом.
+ Fixture проходит **34/34**.
+2. **Редактура и голос.** Полные тела перечитаны: в первых двух абзацах
+ названы риск и цена ошибочной цитаты, а значение score, дат, label,
+ URI#anchor и human verification раскрыто рядом с действием. Три текста не
+ обещают RAG-платформу и не имитируют реальный corpus. Усиленный
+ `audit:draft` подтвердил 10 124 / 10 920 / 10 952 знака и структурную
+ полноту.
+3. **Визуал и выпуск.** Три SVG повторно отрисованы Sharp и просмотрены на
+ 375 px: query → citation — 375×513, freshness/access matrix — 375×464,
+ verification loop — 375×488. Карточки, стрелки, reject reasons и STOP
+ остаются читаемыми; XML и safety scan чисты. Целевой аудит подтверждает
+ рисунок, код и минимум две таблицы в каждой статье. Реестр содержит
+ **250 уникальных** ревизий, `git diff --check` чист, production build
+ успешно сгенерировал **374** страницы.
+
+Вердикт: мартовская партия готова к отдельному публикационному коммиту.
diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs
index 03394c5..8a116fd 100644
--- a/web/data/editorial-revisions.mjs
+++ b/web/data/editorial-revisions.mjs
@@ -81,6 +81,7 @@ import { revisions as november2024Revisions } from '../scripts/upgrade-2024-11.m
import { revisions as december2024Revisions } from '../scripts/upgrade-2024-12.mjs';
import { revisions as january2025Revisions } from '../scripts/upgrade-2025-01.mjs';
import { revisions as february2025Revisions } from '../scripts/upgrade-2025-02.mjs';
+import { revisions as march2025Revisions } from '../scripts/upgrade-2025-03.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -167,4 +168,5 @@ export const editorialRevisions = [
...december2024Revisions,
...january2025Revisions,
...february2025Revisions,
+ ...march2025Revisions,
];
diff --git a/web/public/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg b/web/public/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg
new file mode 100644
index 0000000..5b47143
--- /dev/null
+++ b/web/public/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg
@@ -0,0 +1,54 @@
+
diff --git a/web/public/assets/editorial/2025/knowledge-retrieval-2025-query-retrieval-citation.svg b/web/public/assets/editorial/2025/knowledge-retrieval-2025-query-retrieval-citation.svg
new file mode 100644
index 0000000..0e3942c
--- /dev/null
+++ b/web/public/assets/editorial/2025/knowledge-retrieval-2025-query-retrieval-citation.svg
@@ -0,0 +1,58 @@
+
diff --git a/web/public/assets/editorial/2025/knowledge-retrieval-2025-verification-loop.svg b/web/public/assets/editorial/2025/knowledge-retrieval-2025-verification-loop.svg
new file mode 100644
index 0000000..d87eaa6
--- /dev/null
+++ b/web/public/assets/editorial/2025/knowledge-retrieval-2025-verification-loop.svg
@@ -0,0 +1,48 @@
+
diff --git a/web/scripts/upgrade-2025-03.mjs b/web/scripts/upgrade-2025-03.mjs
new file mode 100644
index 0000000..66377a5
--- /dev/null
+++ b/web/scripts/upgrade-2025-03.mjs
@@ -0,0 +1,866 @@
+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('') + '
Last-Modified сообщает, когда origin server считает выбранное представление изменённым, и сам RFC оставляет способ получения значения implementation detail. Значит, HTTP timestamp можно сохранить как один источник metadata, но нельзя выдать его за полную политику свежести. Внутренняя база может иметь schedule пересмотра, документ — deprecation date, а индекс — собственную дату ingestion. В ответе нужно назвать, какую из этих дат проверили и зачем.'),
+ code(fixtureExample),
+ p('Пример намеренно не читает базу, Git, сеть, CI, часы, telemetry или production. В нём три fixed synthetic cases лежат прямо в модуле. В первом case высокий score у просроченной записи и у закрытого фрагмента; в citation выходит только более низкий, но разрешённый, свежий и адресуемый record. Во втором и третьем case пересечение пустое: один candidate истёк, другой недоступен по labels. Результат не «поиск не нашёл ответ», а точнее: stop-no-fresh-authorized-citable-source.'),
+ h2('Metadata превращает проверку в короткий маршрут'),
+ p('У маршрута есть владелец на каждом переходе. Владелец corpus решает, какие источники разрешено индексировать. Владелец access policy решает, кому доступен source class. Владелец freshness rule определяет expiry и порядок обновления. Автор ответа не должен молча заменять ни одну из этих политик своим ощущением релевантности. Его задача уже: показать query, дату retrieval, выбранную запись и ссылку на fragment; затем прочитать fragment и сказать, подтверждает ли он тезис.'),
+ p('NIST SP 800-207 формулирует общий принцип: нельзя выдавать неявное доверие только из сетевого положения, а authentication и authorization — отдельные функции до доступа к enterprise resource. В статье это не превращается в обещание готовой zero-trust архитектуры для документации. Вывод уже авторский и уже узкий: similarity score не может заменить отдельную проверку access label. Если label не совпал, запись остаётся в диагностике с reject reason, но не становится material для ответа.'),
+ table('Четыре даты и их разные вопросы', ['Дата', 'Вопрос', 'Пример реакции', 'Граница'], [
+ ['publishedAt', 'существовал ли документ к moment retrieval?', 'не брать future-dated запись', 'не подтверждает, что содержимое действует сейчас'],
+ ['indexedAt', 'когда corpus увидел эту редакцию?', 'искать lag и пересобрать index по policy', 'не является датой документа'],
+ ['expiresAt', 'разрешает ли local rule использовать запись?', 'отклонить expired candidate', 'не доказывает удаление исходного URI'],
+ ['retrievedAt', 'какой срез проверял ответ?', 'показать timestamp рядом с citation', 'не равен live observation после ответа'],
+ ]),
+ h2('Exact citation нужна читателю, а не только audit'),
+ p('Citation без anchor заставляет читателя заново угадывать, какой абзац имелся в виду. Citation без sourceVersion не объясняет, относится ли правило к старому adapter или к новой схеме. Citation без retrievedAt прячет, на каком срезе был сделан вывод. Полный record не делает утверждение автоматически верным, но делает проверку короткой: открыть URI, перейти к anchor, сравнить фрагмент с текстом ответа, увидеть date и rights boundary. Это важнее длинного confidence-пояснения после готового текста.'),
+ p('Не надо отдавать в ответ весь internal record. Для читателя достаточно минимальной provenance карточки: title, version, citation, publishedAt, indexedAt, expiresAt, retrievedAt и краткая причина допуска. Access labels можно раскрывать как класс, если сама policy это разрешает; конкретные роли, токены и закрытый metadata не становятся частью ответа. Отделение proof path от секретов — ещё одна причина не строить цитату из одного snippet поля.'),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('Эта модель не измеряет качество production retrieval, не открывает реальный document, не проверяет identity и не доказывает, что corpus полный. Все names, timestamps, excerpts, labels, scores, URI, anchors и decisions внутри upgrade-2025-03.mjs — fixed synthetic in-memory values. Они нужны, чтобы проверить contract обработки: expired, closed и anchorless candidate не проходят в citations. Они не являются данными инженерной базы, доступом к ней или историей реальной команды.'),
+ p('Следующий шаг для одного вопроса: выпишите одну запись index в формате таблицы выше. Затем попробуйте удалить любое поле — version, expiry, access label или anchor. Ожидаемый результат не «система всё равно ответила», а явная stop condition. Если значение нельзя назвать, ответ должен завершиться запросом к owner или human review. После этого уже можно обсуждать retriever, chunking и reranker: они ускоряют выбор кандидата, но не меняют границу допуска к цитате.'),
+ h2('Историческая граница марта 2025'),
+ p('Материал использует только узкие утверждения источников, доступных до марта 2025: immutable commit OpenSearch k-NN от 12 июня 2024 для механики similarity candidates; RFC 9110 от июня 2022 для смысла Last-Modified; NIST SP 800-207 от августа 2020 для явной authorization boundary. Правило «не считать score доказательством истины» — вывод автора из того, что эти сигналы отвечают на разные вопросы. Оно не приписывается OpenSearch, RFC или NIST как их формальная гарантия.'),
+]);
+
+const mechanism = revision({
+ slug: 'editorial-2025-03-mechanism-knowledge-retrieval',
+ title: 'Почему vector score не делает документ доказательством',
+ categories: ['AI', 'Документация'],
+ cover: '/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg',
+ excerpt: 'Разбор механики index → candidates → access/freshness/citation filter: vector score ранжирует похожесть, но не доказывает право, дату или истинность claim.',
+ readingMinutes: 14,
+}, [
+ p('У команды появляется аккуратный retrieval: запрос возвращает top-k документов, у каждого есть score, а первый фрагмент почти дословно отвечает на вопрос. Через неделю выясняется, что лучший candidate описывал прежний contract, а актуальная заметка набрала меньше. Ошибка обычно обнаруживается уже после изменения: validation проходит не там, owner видит другую версию документа, а в answer нет anchor, по которому можно быстро сверить формулировку. Цена ошибки — не только неверный совет; она включает повторное расследование и потерю доверия к базе.'),
+ p('Причина не в том, что vector search «плохой». Similarity search решает узкую задачу: упорядочить candidates в выбранном пространстве. Права, актуальность, цитируемость и истинность утверждения — другие свойства и другие owners. Когда их склеивают с score, система получает один красивый number, которым нельзя объяснить ни reject, ни allow. Я предпочитаю сделать это разложение явным: index хранит metadata, retrieval даёт candidates, а decision допускает к ответу только пересечение access, freshness и exact citation.'),
+ h2('Что именно сообщает score'),
+ p('В зафиксированном README OpenSearch k-NN сказано, что инструмент делает nearest-neighbor similarity search и допускает filters для уточнения similarity search. Это достаточная отправная точка: score связан с тем, насколько candidate похож на query в выбранной модели. В нём нет поля «утверждение истинно». В нём нет информации о том, кто может читать fragment, когда owner прекратил действие документа и открывал ли человек source по указанному anchor.'),
+ p('Отсюда следует практическое правило: score ранжирует очередь проверки, а не разрешает публикацию claim. Это собственный инженерный вывод, не цитата из спецификации. Его легко проверить на фиксированном corpus: expired record может иметь 0.96, closed record — 0.98, а свежая разрешённая запись — 0.91. Если filter использует только порог score ≥ 0.9, он выберет как раз два записи, которые не должны стать ответом.'),
+ table('Сигнал и граница его смысла', ['Сигнал', 'На какой вопрос отвечает', 'Что делает с candidate', 'Чего нельзя выводить'], [
+ ['vectorScore', 'насколько record похож на query в fixed ranking', 'ставит в очередь чтения', 'что claim верен или source актуален'],
+ ['accessLabels', 'допускает ли declared label этот source class', 'отсеивает закрытый fragment', 'что requester прошёл реальную authentication'],
+ ['publishedAt / expiresAt', 'входит ли record в declared freshness window', 'отсеивает future и expired record', 'что в мире нет другой редакции'],
+ ['citationUri#anchor', 'можно ли показать точное место в source', 'делает candidate цитируемым', 'что человек согласился с трактовкой'],
+ ['human verification', 'совпадает ли claim с открытым fragment и его boundary', 'разрешает сформулировать answer', 'что работа заменяет owner policy'],
+ ]),
+ figure('/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg', 'Матрица четырёх синтетических records: fresh-v3 проходит access, срок и citation; higher-score expired-v1, closed-v2 и no-anchor отклоняются. Подпись фиксирует, что score сокращает очередь проверки, но не доказывает истину.', 'Матрица специально показывает неприятный вариант: зелёная запись имеет не самый высокий score. Порядок похожести не может отменить права, срок или отсутствие exact anchor.'),
+ h2('Index должен не терять provenance при chunking'),
+ p('Chunking часто обрезает именно тот контекст, который нужен для проверки. Вектор хранит фрагмент, а version, owner и chapter остаются в исходном документе. После retrieval появляется удобный snippet без ответа на вопрос «из какой редакции он взят?». Поэтому metadata наследуются каждым index record либо доступно связываются по stable source id. Нельзя ожидать, что генератор восстановит provenance из текста: одинаковая фраза может встречаться в migration guide, old RFC и закрытом exception.'),
+ p('Минимальная связь проста. Source record задаёт URI, version, publishedAt, owner и policy. Chunk record хранит sourceRecordId, anchor, indexedAt, expiry и access labels, применимые к fragment. Retrieval output возвращает оба слоя. Если какой-то слой отсутствует, candidate остаётся candidate. Он может подсказать, где искать, но не может попасть в answer как exact citation. Это важнее дополнительного reranker, потому что без provenance даже идеально ранжированный fragment остаётся непроверяемым.'),
+ table('Где должна жить metadata', ['Слой', 'Хранит', 'Проверка перед answer', 'Типичная ошибка'], [
+ ['source record', 'title, sourceVersion, owner, publishedAt', 'связь с конкретной редакцией', 'разрешить chunk без source id'],
+ ['chunk record', 'anchor, excerpt, indexedAt, expiresAt, access labels', 'fragment addressable и допустим', 'сохранить только text и embedding'],
+ ['retrieval event', 'query, retrievedAt, candidates, reject reasons', 'можно повторить decision на том же срезе', 'перезаписать output готовым answer'],
+ ['human review', 'claim, citation, conclusion, limitation', 'source действительно поддерживает формулировку', 'считать top-1 автоматическим подтверждением'],
+ ]),
+ h2('Последовательность допуска должна быть детерминированной'),
+ ol([
+ 'Зафиксировать query и retrievalAt. Без момента проверки невозможно понять, почему expiry приняла или отклонила record.',
+ 'Получить candidates. Сохранить recordId, score и metadata, а не только text snippet.',
+ 'Проверить access. Неподходящий label не становится приемлемым из-за сильного semantic match.',
+ 'Проверить freshness. Сравнить publishedAt и expiresAt с retrievalAt по явно записанной policy.',
+ 'Проверить citation. URI и anchor должны вести к конкретной редакции и фрагменту, а не к title source.',
+ 'Открыть source человеку. Сопоставить claim с fragment, назвать limitation и только затем ответить.',
+ ]),
+ p('Такой порядок помогает спорить конструктивно. Если ответ не вышел, можно назвать точную причину: access-label-not-granted, expired-at-fixed-retrieval-time или missing-exact-citation-anchor. Это лучше статуса «relevance low», который смешивает разные проблемы. Owner access policy исправляет access; owner documentation исправляет expiry или anchor; owner index pipeline исправляет ingestion. Одна red status не заставляет всех искать ошибку в embedding model.'),
+ h2('Небольшая модель показывает неприятные ветки'),
+ code([
+ "import {",
+ " createFixedSyntheticKnowledgeRetrievalInput,",
+ " inspectFixedSyntheticKnowledgeRetrieval,",
+ " prepareSyntheticKnowledgeHumanReview,",
+ "} from './upgrade-2025-03.mjs';",
+ '',
+ "const report = inspectFixedSyntheticKnowledgeRetrieval(",
+ " createFixedSyntheticKnowledgeRetrievalInput('fixed-fresh-allowed-v1'),",
+ ");",
+ 'const review = prepareSyntheticKnowledgeHumanReview(report);',
+ '',
+ 'console.log({',
+ ' accepted: report.accepted,',
+ ' allowedCitation: report.citations[0]?.citation,',
+ ' rejected: report.retrieval.candidateOutput',
+ ' .filter((candidate) => !candidate.admissible)',
+ ' .map((candidate) => [candidate.recordId, candidate.reasons]),',
+ ' needsHumanVerification: review.status,',
+ '});',
+ '',
+ '// All values are fixed synthetic in-memory records.',
+ '// No real source, identity, clock, file, network, Git, CI, telemetry or production system is read.',
+ ].join('\n')),
+ p('В первом fixed case output содержит четыре records. Сортировка ставит closed exception с 0.99 первым, expired guideline с 0.97 вторым, а fresh guideline с 0.91 ниже. После access, freshness и anchor checks остаётся один citation. Fixture не говорит, что этот fragment верен для чьего-то реального adapter. Она только доказывает контракт кода: reject reasons не исчезли, closed и expired snippets не попали в citations, а accepted report требует human verification.'),
+ p('Во втором и третьем fixed cases хорошего candidate нет вовсе. Один corpus состоит только из expired record, другой — только из inaccessible record. Function возвращает stop result вместо пустого ответа, дополненного догадкой. Это важная разница для UX и audit: если assertion нельзя доказать, система должна сообщить, какого evidence не хватает, а не заставить человека отличать осторожный тон от уверенной галлюцинации.'),
+ h2('Freshness — это policy, а не одно HTTP поле'),
+ p('RFC 9110 называет Last-Modified timestamp, в который origin server считает selected representation изменённым. Там же сказано, что способ его определения — implementation detail. Для retrieval это означает две вещи. Во-первых, timestamp полезно хранить как evidence от HTTP source. Во-вторых, нельзя автоматически подменить им правила инженерной актуальности. Документ может быть не изменён, но потерять применимость после migration; наоборот, versioned specification может оставаться пригодной для historical question после более новой публикации.'),
+ p('Политика должна быть обозримой: например, у записи есть publishedAt, indexedAt, expiresAt и explicit reason срока. Какое поле воздействует на answer, фиксируется до query. Для historical question expiry может быть осознанно шире, но тогда answer должен назвать версию и дату, а не выдавать старое правило за текущую конфигурацию. Для operational question record с expiredAt в прошлом отвергается до генерации. Модель не угадывает это из semantic similarity; policy появляется в metadata и в owner decision.'),
+ h2('Права доступа — независимая ветка'),
+ p('NIST SP 800-207 говорит о resource-centric подходе: no implicit trust только из места в сети, authentication и authorization выполняются отдельно перед сессией к enterprise resource. Этот документ не проектирует поиск по wiki и не даёт формат labels. Но он помогает не допустить простую подмену: если retriever технически увидел closed fragment, это ещё не означает, что его можно отдать requester или положить в citation. Read path и answer path должны уважать одну access boundary.'),
+ p('В production label редко равен одному слову, а policy зависит от identity, resource, purpose и срока. В fixed model labels намеренно упрощены до строк, чтобы проверить ветвление без реальных identities. Это ограничение важно: PASS fixture не является проверкой RBAC, ABAC или SSO. Он лишь проверяет, что код не считает vectorScore полномочием и не выкидывает access reason из report. Реальная интеграция потребует approved policy owner и безопасного способа получить decision.'),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('Здесь нет benchmark, live corpus, автоматической оценки ответов, наблюдения за пользователями или утверждения, что vector search всегда уступает keyword search. Внутри модуля есть только fixed synthetic records, timestamps, labels, scores и excerpts. Source URLs в разделе ниже подтверждают узкие protocol и security facts; они не подтверждают наши synthetic output. Все выводы о том, как совместить signals, явно помечены как инженерная политика статьи.'),
+ p('Следующий шаг: выберите один failure mode из своей базы — expired document, inaccessible document или fragment без anchor. Добавьте в index record только одно отсутствующее поле и напишите один rejection test. Ожидаемый эффект: result начинает объяснять, почему candidate нельзя цитировать. Затем закрепите owner и policy для этого поля. Только после этого имеет смысл тратить время на настройки top-k, reranking или дополнительные embeddings: их выигрыш не заменяет evidence boundary.'),
+ h2('Историческая граница марта 2025'),
+ p('OpenSearch commit 150c589849a8ec3bc442d830b43a3eaf4e25fa0c датирован 12 июня 2024 и закрепляет использованный факт о similarity search. RFC 9110 опубликован в июне 2022, NIST SP 800-207 — 11 августа 2020. Они доступны до марта 2025. В статье не сказано, что любой HTTP timestamp является всей freshness policy, что NIST разрешает конкретную схему labels или что OpenSearch score даёт проверку фактов; это были бы более сильные и ложные атрибуции.'),
+]);
+
+const field = revision({
+ slug: 'editorial-2025-03-field-knowledge-retrieval',
+ title: 'Когда поиск обязан остановиться: нет цитаты — нет ответа',
+ categories: ['AI', 'Документация'],
+ cover: '/assets/editorial/2025/knowledge-retrieval-2025-verification-loop.svg',
+ excerpt: 'Полевой маршрут для случая, когда retriever нашёл только закрытый, просроченный или нецитируемый источник: остановить ответ, сохранить причину и передать вопрос на human verification.',
+ readingMinutes: 13,
+}, [
+ p('Самый неприятный результат поиска по инженерной базе — не пустой список. Хуже, когда retriever возвращает точное, но закрытое исключение, или старую инструкцию с отличным semantic match. Если скрыть это за уверенным ответом, ошибку трудно заметить: человек не видит, что цитата отсутствует, а change уже попал в review. Цена ошибки выше, чем задержка: неверная миграция, раскрытие ограниченного материала или спор о том, откуда взялось правило.'),
+ p('В такой ситуации система должна уметь сказать «стоп» до генерации текста. Это не отказ от помощи и не пустая формальность. Stop path превращает неизвестность в конкретный запрос: нужен источник с другим access scope, свежая редакция, точный anchor или owner, который подтвердит policy. Взрослый процесс не заставляет retriever угадывать, какой компромисс допустим. Он показывает, что найдено, что отброшено, почему отброшено и кто может снять ограничение.'),
+ h2('Stop condition — часть контракта ответа'),
+ p('У ответа есть положительные и отрицательные условия. Положительное: есть candidate, который разрешён, свеж и содержит exact citation. Отрицательное: хотя бы одно обязательное свойство отсутствует у всех candidates. Тогда system не пишет «вероятно», не делает summary из закрытого snippet и не подменяет stale rule свежей интерпретацией. Она возвращает code stop-no-fresh-authorized-citable-source, query и reject reasons. Это уже достаточный артефакт для следующего человека.'),
+ p('Важная деталь: stop не означает, что corpus пуст. В fixed stale case corpus содержит record с score 0.96; в closed case — record с 0.98. Оба фрагмента подходят по словам, но не проходят другой contract. Если UI показывает только «0 results», он теряет диагностическую ценность. Если UI показывает закрытый excerpt, он нарушает rights. Нужен третий вариант: показать безопасный reason без текста, который нельзя раскрывать, и route для human review.'),
+ table('Решение после retrieval', ['Состояние candidate', 'Что можно показать requester', 'Ответ на вопрос', 'Следующий владелец'], [
+ ['fresh + allowed + exact citation', 'version, timestamp, URI#anchor, permitted excerpt', 'draft только после human verification', 'reviewer и owner source'],
+ ['expired', 'recordId, sourceVersion, expiry и reason без ложной актуальности', 'stop', 'owner documentation или index freshness policy'],
+ ['closed', 'recordId и access reason без restricted excerpt', 'stop', 'owner access policy'],
+ ['no anchor', 'recordId и missing-anchor reason', 'stop', 'owner source structure или ingestion pipeline'],
+ ['unknown policy', 'не притворяться allow или deny', 'stop / clarify', 'назначенный owner policy'],
+ ]),
+ figure('/assets/editorial/2025/knowledge-retrieval-2025-verification-loop.svg', 'Вертикальная схема human verification: вопрос, retrieval candidates, проверки прав и срока, открытие exact fragment, сохранение answer с citation. Красная ветка ведёт к STOP при отсутствии права, срока или anchor.', 'Stop расположен внутри рабочего цикла, а не в конце как аварийная ветка. Благодаря этому rejected candidate оставляет диагностическую причину, но не превращается в недоказанный ответ.'),
+ h2('Честная остановка дешевле скрытой догадки'),
+ p('Польза ответа не равна числу слов. Closed source нельзя пересказывать, expired document нельзя назвать текущим. Stop path удерживает спор у границы evidence: query, record id, version, reject reason и requested action. Вместо «модель сомневается» команда получает проверяемое «expired at retrieval time» или «access label not granted» и направляет его владельцу.'),
+ h2('Минимальный воспроизводимый пример'),
+ code([
+ "import {",
+ " createFixedSyntheticKnowledgeRetrievalInput,",
+ " inspectFixedSyntheticKnowledgeRetrieval,",
+ " prepareSyntheticKnowledgeHumanReview,",
+ "} from './upgrade-2025-03.mjs';",
+ '',
+ "const stale = inspectFixedSyntheticKnowledgeRetrieval(",
+ " createFixedSyntheticKnowledgeRetrievalInput('fixed-stale-only-v1'),",
+ ");",
+ 'const review = prepareSyntheticKnowledgeHumanReview(stale);',
+ '',
+ 'console.log({',
+ ' answerAllowed: stale.accepted,',
+ ' stop: stale.decision.code,',
+ ' reason: stale.retrieval.candidateOutput[0].reasons[0],',
+ ' reviewStatus: review.status,',
+ '});',
+ '',
+ '// Expected: false, stop-no-fresh-authorized-citable-source,',
+ '// expired-at-fixed-retrieval-time, stopped-before-answer.',
+ '// The model is fixed synthetic in memory. It performs no external read or side effect.',
+ ].join('\n')),
+ p('Этот example не спрашивает настоящую wiki и не пытается определить, имеет ли реальный человек доступ. Он использует record, timestamp и expiry, жёстко записанные в JS module. Поэтому его можно запускать как fixture: вход имеет строгую shape, report совпадает только с canonical fixed case, extra telemetry field отвергается, forged date не проходит сравнение, cyclic input и report возвращают reject вместо исключения. Такая строгость не делает пример production system, но не позволяет тесту незаметно превратиться в генератор произвольных ответов.'),
+ p('Accepted branch не менее важна. Fresh allowed case формирует citation и status requires-human-verification. Он всё ещё не выдаёт готовый fact. Отдельная функция создаёт только in-memory review draft с шагами: открыть source outside model, сравнить claim с exact anchor, подтвердить access и freshness policy с owner, сохранить answer вместе с citation. Если draft надо остановить, stopSyntheticKnowledgeAnswer удаляет только этот in-memory artefact и явно не пытается менять production, source или document.'),
+ h2('Сравнить варианты до того, как спор уйдёт в prompt'),
+ table('Три реакции на непроверяемый candidate', ['Вариант', 'Краткосрочная выгода', 'Цена ошибки', 'Решение автора'], [
+ ['Сгенерировать ответ по top-1', 'быстро выглядит полезным', 'stale или closed rule превращается в implementation decision', 'не применять: score не является evidence'],
+ ['Скрыть candidate и ответить «ничего нет»', 'не раскрывает fragment', 'теряется причина и owner не понимает, что исправлять', 'показать safe reject reason без закрытого excerpt'],
+ ['Остановить answer с structured reason', 'нужен следующий человек или источник', 'есть задержка и явный ownership', 'применять: это сохраняет access и freshness boundary'],
+ ]),
+ h2('Human verification проверяет claim, а не доверяет pipeline'),
+ p('После положительного filter остаётся последний переход, который нельзя выкинуть: человек открывает exact source и сравнивает его с proposed claim. Нужны четыре коротких вопроса. Тот ли это version? Есть ли в anchor именно это условие, а не похожая рекомендация? Не потерялось ли ограничение из соседнего абзаца? Разрешает ли policy использовать source в этом контексте? Если любой ответ отрицательный или неизвестный, candidate возвращается в stop path.'),
+ p('Retrieval уже сократил очередь до нескольких candidates, но final verification нельзя заменить confidence: связный текст может опираться на неверный fragment. В fixture human verification остаётся status, а не true; функция не открывает source и не принимает policy. Поэтому automated report не выдан за утверждение о реальном документе.'),
+ table('Карточка human verification', ['Поле', 'Что записать', 'Что считается stop'], [
+ ['Claim', 'одно проверяемое предложение, не summary всей темы', 'claim шире, чем exact fragment'],
+ ['Citation', 'URI, anchor, sourceVersion, retrievedAt', 'любое поле отсутствует или ведёт не к той редакции'],
+ ['Freshness', 'publishedAt, expiry и policy reason', 'record expired или period не определён'],
+ ['Access', 'разрешённый source class без раскрытия секретной policy', 'label не допускает reader или purpose'],
+ ['Decision', 'accept с limitation либо stop с reason', 'нет named owner для остаточного риска'],
+ ]),
+ h2('Откуда берётся дата и почему её нельзя переоценивать'),
+ p('RFC 9110 даёт полезную, но ограниченную модель Last-Modified: origin server сообщает время, в которое он считает selected representation изменённым. Это подходящий input для indexer, если source отдаёт header. Но из него не следует, что документ не утратил применимость, что все составные части страницы синхронизированы или что previous cached copy идентична current source. Поэтому рабочая карточка содержит несколько дат, а policy прямо называет, какое правило использует answer.'),
+ p('publishedAt отвечает на historical boundary, indexedAt — на lag pipeline, expiresAt — на local rule, retrievedAt — на срез answer. Эти поля отделяют ошибку document owner от ошибки indexer и не позволяют лечить retriever там, где нужно обновить source.'),
+ h2('Access check не должен раскрывать закрытый source'),
+ p('NIST SP 800-207 отдельно выделяет authentication и authorization до доступа к enterprise resource. В поисковой системе из этого не следует, что labels из example достаточны для enterprise policy. Следствие уже практическое: decision deny должен произойти до того, как excerpt станет частью answer context. Иначе retrieval формально откажет после того, как закрытый текст уже обработан downstream component или увиден пользователем. В real integration scope и порядок enforcement определяет security owner, а не автор статьи.'),
+ p('В fixed closed case restricted excerpt не выводится. Report содержит record id, version, timestamp, required labels и access-label-not-granted. Это не реальная authorization decision, но интерфейс уже сохраняет нужную границу: причина доступна для диагностики, а текст остаётся закрытым. Уровень раскрытия определяет policy, не layer генерации.'),
+ h2('Практический маршрут для одной базы'),
+ ol([
+ 'Выберите один question type. Разделите operational и historical вопросы; у них могут быть разные freshness rules.',
+ 'Опишите source record. Добавьте stable id, version, owner, даты, access class, URI и exact anchor до настройки ranker.',
+ 'Сделайте reject reasons явными. Минимум: denied, expired, not-yet-published, missing-anchor и unknown-policy.',
+ 'Проведите один denial test. Высокий score закрытого или expired record не должен попасть в citation output.',
+ 'Назначьте human review. Reviewer открывает разрешённый source и подтверждает claim, а не доверяет составленному answer.',
+ 'Зафиксируйте stop owner. Вопрос без acceptable citation идёт к documentation, access или index owner с короткой карточкой причины.',
+ ]),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('Полевая статья не описывает реальный incident, не предоставляет реальные правила доступа и не измеряет время ответа. Все score, names, URI, dates, excerpts, allow/deny decisions и outputs принадлежат fixed synthetic in-memory model. Она не читает файлы, Git, сеть, CI, production, часы или telemetry; не извлекает документы и не определяет identity. Поэтому её fixture проверяет только shape и negative branches кода. Она не может подтвердить, что конкретная организация соблюдает access policy или что конкретная база свежа.'),
+ p('Следующий шаг: возьмите один вопрос, на который ваша система сегодня отвечает без citation. Добавьте structured result с accepted, reason, retrievedAt и source card. Затем составьте один test, где highest-score candidate expired или denied. Ожидаемый результат — stop before answer и короткий маршрут к owner. Если этот test нельзя написать из-за отсутствующих metadata, это уже полезный результат: сначала исправьте corpus contract, затем улучшайте модель поиска.'),
+ h2('Историческая граница марта 2025'),
+ p('Для этой статьи OpenSearch используется только как подтверждение mechanics similarity search в immutable commit от 12 июня 2024. RFC 9110 используется только для смысла Last-Modified, а NIST SP 800-207 — только для принципа явной authorization boundary. Все три источника существовали до марта 2025 и были закреплены versioned release, RFC либо dated final publication. Stop policy, fields fixed model и human-review sequence — собственная инженерная конструкция автора; она не выдана за обязательную норму этих документов.'),
+]);
+
+export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
+
+function verifyFixture() {
+ const report = runKnowledgeRetrievalFixture();
+ const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key);
+ if (failed.length > 0) {
+ process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
+ process.exitCode = 1;
+ return;
+ }
+ const count = Object.keys(report.assertions).length;
+ process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
+}
+
+if (process.argv.includes('--verify-fixture')) verifyFixture();
+if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');