From 4930c69d9033643e922054a11c7e26aad0e86b90 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 31 Jul 2026 17:42:56 +0300 Subject: [PATCH] revise April 2025 AI data privacy articles --- editorial/production/README.md | 2 +- editorial/reviews/2025-04-draft.md | 110 +++ web/data/editorial-revisions.mjs | 2 + ...-data-privacy-2025-allowability-matrix.svg | 55 ++ .../ai-data-privacy-2025-context-boundary.svg | 58 ++ .../ai-data-privacy-2025-egress-review.svg | 47 + web/scripts/upgrade-2025-04.mjs | 863 ++++++++++++++++++ 7 files changed, 1136 insertions(+), 1 deletion(-) create mode 100644 editorial/reviews/2025-04-draft.md create mode 100644 web/public/assets/editorial/2025/ai-data-privacy-2025-allowability-matrix.svg create mode 100644 web/public/assets/editorial/2025/ai-data-privacy-2025-context-boundary.svg create mode 100644 web/public/assets/editorial/2025/ai-data-privacy-2025-egress-review.svg create mode 100644 web/scripts/upgrade-2025-04.mjs diff --git a/editorial/production/README.md b/editorial/production/README.md index 4c92405..14ca585 100644 --- a/editorial/production/README.md +++ b/editorial/production/README.md @@ -1,6 +1,6 @@ # Производство редакционных партий -На 31 июля 2026 года строгий аудит проходит 259 из 358 созданных материалов. Остальные 99 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. +На 31 июля 2026 года строгий аудит проходит 262 из 358 созданных материалов. Остальные 96 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. ## Одна партия diff --git a/editorial/reviews/2025-04-draft.md b/editorial/reviews/2025-04-draft.md new file mode 100644 index 0000000..71548d5 --- /dev/null +++ b/editorial/reviews/2025-04-draft.md @@ -0,0 +1,110 @@ +# P86 · 2025-04 · Данные и приватность в AI-инструментах — три прохода ревью + +## Граница самостоятельного draft-пакета + +- Overlay затрагивает только три April 2025 slug через web/scripts/upgrade-2025-04.mjs. +- Все context records, labels, authorization, cases, citations и outcomes — versioned fixed synthetic JavaScript literals в памяти. В strings нет customer code, PII, secret, реального лога, тикета, source file или внешнего идентификатора. +- Скрипт не выполняет prompt/model call, external request, filesystem, Git, CI, network, telemetry, clock, production action или иной side effect. Положительный outcome означает только synthetic hand-off; egressPerformed и productionEffect остаются false / not-attempted. +- Registry, README, очередь, articles.json, app-страницы, full production build, staging, commit и push намеренно не входят в этот пакет. + +## База исследования и историческая граница + +Все четыре ссылки ниже проверены обычным HTTPS GET без TLS bypass и без авторизации: каждая вернула HTTP 200. Два mutable GitHub Docs источника закреплены immutable commit от 30 апреля 2025; NIST публикации ведут на final page или датированный final PDF. Тексты не делают общий юридический вывод и не переносят terms одного service на другой. + +| Источник | Версия / дата | Узкое утверждение в статьях | Явная граница | +| --- | --- | --- | --- | +| [GitHub Docs: Responsible use of GitHub Copilot Chat in GitHub](https://github.com/github/docs/blob/0d17a12af77011b390abd8aea9e8626e153e0522/content/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-github.md) | immutable commit 0d17a12af77011b390abd8aea9e8626e153e0522, 30.04.2025 | Prompt предварительно обрабатывается и сочетается с context; конкретная surface может получить repository context, а при включённом Bing сформированный query передаётся Bing Search API. | Это описание одной surface на фиксированной ревизии. Оно не классифицирует данные, не фиксирует retention/training для другой организации и не разрешает передачу сотруднику. | +| [GitHub Docs reusable: Copilot SKU isolation](https://github.com/github/docs/blob/0d17a12af77011b390abd8aea9e8626e153e0522/data/reusables/copilot/sku-isolation.md) | immutable commit 0d17a12af77011b390abd8aea9e8626e153e0522, 30.04.2025 | Firewall allow-list может разрешать Business/Enterprise plan endpoint, а block-list — запрещать Individual endpoint в корпоративной сети. | Это endpoint-level routing control, не inspection payload. Он не доказывает class, contract/retention, training terms или human authorization. | +| [NIST SP 800-207: Zero Trust Architecture](https://csrc.nist.gov/pubs/sp/800/207/final) | final, 11.08.2020 | Network location не даёт implicit trust; authentication и authorization — отдельные policy-based функции для resource access. | Это security architecture, не DPA и не локальная классификация данных. Документ не описывает AI vendor routing, storage или конкретный service scope. | +| [NIST AI 100-1: AI RMF 1.0](https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf) | final PDF, January 2023 | AI RMF связывает GOVERN, MAP, MEASURE и MANAGE; privacy рассматривается вместе с организационными и техническими практиками управления риском. | Добровольная рамка не делает юридический вывод, не назначает owner и не заменяет vendor contract, egress measurement или access check. | + +Историческая оговорка: в пакет не добавлены поздние model names, product modes, routing claims, provider terms или agent features после апреля 2025. Перед реальным использованием документация, contract и конфигурация должны быть проверены заново для своего plan, surface, region, account и destination. + +## Проход 1 — факты, историческая граница и техника + +Проверены все утверждения о внешних системах против таблицы выше. В статьях использованы только узкие факты: потенциальное расширение context на конкретной GitHub surface, plan-specific endpoint isolation и принцип policy-based authorization. Ни один источник не используется как доказательство фактического хранения, обучения, region, subprocessor, legal basis или допустимости конкретного фрагмента. + +Техническая модель разделяет четыре решения: + +1. dataClass и allowability описывают локальный fixed record. +2. retentionLabel обозначает наличие отдельного fixed contract-condition, но не утверждает реальное vendor behavior. +3. egressScope сопоставляет record и symbolic destination, но не является hostname или proxy configuration. +4. authorization содержит issuer role, record scope, requester labels, date range, reason и citation; она не может переопределить deny-class. + +Граница безопасности формы проверяется отрицательными cases: extra/missing keys, forged authorization и расширенный scope, requester access mismatch, expiry, другой egress scope, sparse arrays и cyclic values. Любое расхождение создаёт closed decision и safe stop без payload. Even accepted case заканчивается до внешнего вызова. + +~~~text +node --check scripts/upgrade-2025-04.mjs +PASS + +node scripts/upgrade-2025-04.mjs --verify-fixture +PASS fixture: 28/28 assertions +~~~ + +Ограничение прохода: fixture доказывает только contract fixed in-memory модели. Она не тестирует model provider, firewall, DLP, identity system, actual network, document retention, human action или production access. + +## Проход 2 — редактура, M8 и усиленный draft audit + +Проверены первые два абзаца каждой статьи: они называют наблюдаемую проблему и цену ошибки — утрату проверяемой цепочки class → contract → egress → authority, повторную инвентаризацию и остановку решения. В текст не добавлены вымышленные инциденты, деньги или утечки; field case остаётся synthetic. + +Тон соответствует уровню М8: автор не обещает «безопасный prompt», сравнивает scope доказательств, разделяет control layers и оставляет короткий воспроизводимый процесс. Связка «симптом → причина → проверка → действие» выдержана в practice, mechanism и field case. Технические термины привязаны к record, label, destination, authorization или expiry, а не используются как общая оценка. + +| Статья | Основной текст | Содержание, проверенное редактурой | +| --- | ---: | --- | +| Practice | 8 227 знаков | Карточка контекста, distinction class/contract/egress/authority, in-memory example, pre-flight order и next step. | +| Mechanism | 9 014 знаков | Матрица доказательств, fail-closed порядок, scope retention label, zero-trust boundary и four-column evidence sheet. | +| Field | 10 247 знаков | Synthetic log/ticket case, scoped authorization, negative path, role split, concrete review order и tabletop next step. | + +Повторный усиленный audit после добавления формальной последовательности действий в field case: + +~~~text +npm run audit:draft -- scripts/upgrade-2025-04.mjs +PASS editorial-2025-04-practice-ai-data-privacy: 8227 body chars +PASS editorial-2025-04-mechanism-ai-data-privacy: 9014 body chars +PASS editorial-2025-04-field-ai-data-privacy: 10247 body chars +~~~ + +Audit также проверяет пять смысловых разделов, таблицу с thead, figure/caption/meaningful alt, code example, ordered sequence, две внешние ссылки, стартовую постановку проблемы и цену ошибки в первых двух paragraphs. Сначала field article не прошла ровно по отсутствию ordered sequence; добавлен отдельный краткий review order, после чего полный audit повторён и прошёл. + +Ограничение прохода: объём, структура и ясность терминов не подтверждают применимость source или policy к чужому service. Они лишь делают неизвестный слой заметным и требуют owner question. + +## Проход 3 — SVG, fixture и выпускная граница + +Каждая статья получает отдельный SVG с осмысленным alt и caption: + +| Asset | Sharp render 375 px | Ручная проверка | +| --- | --- | --- | +| ai-data-privacy-2025-context-boundary.svg | 375 × 513 | Карточки class, contract/retention, egress и authority читаемы; красная safe-stop ветка не обрезана. | +| ai-data-privacy-2025-allowability-matrix.svg | 375 × 523 | Четыре synthetic rows, колонки evidence и итоговая формула различимы без горизонтального overflow. | +| ai-data-privacy-2025-egress-review.svg | 375 × 493 | Пять review steps, stop branch и финальная граница no external request читаемы на узком экране. | + +~~~text +xmllint --noout <3 SVG> +PASS + +SVG safety scan for script, foreignObject, javascript:, data:image and event handlers +PASS + +Sharp 375 px + manual visual review +PASS +~~~ + +Первый narrow-screen render выявил переполнение длинных строк в context-boundary.svg и egress-review.svg. Текст разбит на короткие строки, SVG снова прошли XML/safety checks и повторный Sharp/manual review. Матрица не требовала правки. + +Полная production build, registry update, staging, commit и push сознательно не запускались: они запрещены scope самостоятельного P86 draft-пакета. До будущей публикации нужен отдельный интеграционный выпускной проход, который подключит overlay к registry, повторит audit на собранных статьях и проверит полный build. + +## Выпуск после независимой приёмки + +### 1. Факты и техническая граница + +Редактор заново прочитал все три текста и проверил первичные источники обычным HTTPS без авторизации и обхода TLS. GitHub Docs закреплён commit `0d17a12af77011b390abd8aea9e8626e153e0522` от 30 апреля 2025 года: raw-файлы подтверждают предобработку prompt с context, дополнительный repository/Bing context и plan-specific firewall isolation. Официальная страница NIST SP 800-207 подтверждает отсутствие implicit trust по network location и раздельные authentication/authorization; final PDF NIST AI RMF 1.0 подтверждает функции GOVERN, MAP, MEASURE и MANAGE. В статьях эти факты не расширены до вывода о конкретном vendor, хранении, обучении, договоре или юридической допустимости. + +Проверены `node --check`, 28 из 28 fixture assertions и усиленный draft audit. Основной текст статей содержит соответственно 8 227, 9 014 и 10 247 знаков; в первых двух абзацах каждой названы ситуация и цена неполного evidence. Форма примеров намеренно ограничена fixed synthetic records: positive outcome остаётся hand-off, а `egressPerformed` не становится фактической отправкой. + +### 2. Редактура и голос автора + +Полностью вычитаны practice, mechanism и field case. У каждой есть постановка проблемы, различение class/policy-contract/egress/authority, таблица, схема, воспроизводимый отрицательный или положительный synthetic example, последовательность проверки, граница применимости и следующий проверяемый шаг. Термины привязаны к конкретному record и не используются как рекламная оценка инструмента. Тон М8 остаётся прагматичным: «симптом → причина → проверка → действие» важнее обещания безопасного prompt. + +### 3. Визуальная и выпускная проверка + +Все три SVG прошли XML и safety scan; для каждого выполнен Sharp render на ширине 375 px и ручная проверка. Карточки, матрица и маршрут review читаются без обрезания или горизонтального переполнения; у каждого рисунка есть содержательные `alt` и подпись. Overlay подключён к общему registry. До коммита выполнены целевой audit собранных статей, проверка уникальности slug, `git diff --check` и полная production build. diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs index 8a116fd..34192d7 100644 --- a/web/data/editorial-revisions.mjs +++ b/web/data/editorial-revisions.mjs @@ -82,6 +82,7 @@ import { revisions as december2024Revisions } from '../scripts/upgrade-2024-12.m 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'; +import { revisions as april2025Revisions } from '../scripts/upgrade-2025-04.mjs'; // This layer replaces archived source entries without losing their stable slug and date. export const editorialRevisions = [ @@ -169,4 +170,5 @@ export const editorialRevisions = [ ...january2025Revisions, ...february2025Revisions, ...march2025Revisions, + ...april2025Revisions, ]; diff --git a/web/public/assets/editorial/2025/ai-data-privacy-2025-allowability-matrix.svg b/web/public/assets/editorial/2025/ai-data-privacy-2025-allowability-matrix.svg new file mode 100644 index 0000000..e8f7382 --- /dev/null +++ b/web/public/assets/editorial/2025/ai-data-privacy-2025-allowability-matrix.svg @@ -0,0 +1,55 @@ + + Матрица допустимости синтетического контекста + Четыре синтетические записи различаются по классу, rule, retention label, egress gate и human authorization. Только первая имеет полный fixed набор свидетельств, но всё равно не отправляется наружу. + + Матрица: что именно допускается к hand-off + Все строки — fixed synthetic labels, не классификация реальных данных. + + + + ЗаписьКлассRuleGateИтог + и labelданных+ retention+ authorityмодели + + + + + public-summaryv1 + syntheticpublic + allow +contract label + scope +valid auth + handoffonly + + + + + internal-notev2 + syntheticinternal + reviewnot settled + scopenot enough + stoppolicy + + + + + log-shapev1 + syntheticrestricted + deny +no retention + authoritycannot override + stopclass + + + + + ticket-shapev1 + syntheticpersonal-data + deny +no retention + expiredauth + stopexpiry + + + + Решение = class ∩ rule/contract ∩ egress ∩ authority + «Handoff» означает только готовность задать следующий вопрос владельцу. + + Контракт или UI setting не заменяют техническую и человеческую границы. + diff --git a/web/public/assets/editorial/2025/ai-data-privacy-2025-context-boundary.svg b/web/public/assets/editorial/2025/ai-data-privacy-2025-context-boundary.svg new file mode 100644 index 0000000..5e4b17e --- /dev/null +++ b/web/public/assets/editorial/2025/ai-data-privacy-2025-context-boundary.svg @@ -0,0 +1,58 @@ + + Пять границ перед передачей синтетического контекста в AI-инструмент + Вертикальная схема отделяет классификацию данных, условие допустимости, контракт retention, технический egress и человеческое разрешение. Красная ветка останавливает передачу до внешней границы. + + + + + + + + + + Перед AI-границей — + пять отдельных вопросов + Один allow-list, DPA или UI setting не отвечает на все пять. + + + + 1 + Класс данных + Что это: public, internal, restricted + или другой локальный label? + + + + + 2 + Допустимость и retention + Какое правило и contract нужны? + Retention и training проверяют отдельно. + + + + + 3 + Технический egress + Разрешён ли этот destination + и маршрут, а не только продукт? + + + + + 4 + Human authorization + Кто разрешил именно этот scope, + кому и до какой даты? + + + + 5. Только hand-off, не автоматический send + Пакет фиксирует decision и citation; + реальный egress остаётся вне модели. + + + + Нет доказательства → safe stop + Не смешивать class, egress и authority. + diff --git a/web/public/assets/editorial/2025/ai-data-privacy-2025-egress-review.svg b/web/public/assets/editorial/2025/ai-data-privacy-2025-egress-review.svg new file mode 100644 index 0000000..e627568 --- /dev/null +++ b/web/public/assets/editorial/2025/ai-data-privacy-2025-egress-review.svg @@ -0,0 +1,47 @@ + + Маршрут review для передачи синтетического AI-контекста + Последовательность review начинается с record id и class, затем сверяет contract, egress destination, access и срок human authorization. Любое расхождение идёт в safe stop без внешней отправки. + + + + + + Review egress: evidence до действия + Фиксированный учебный маршрут: он не делает реальный request. + + + 1. Record и class + Сверить id, version, class, allowability + и retention label. + + + + 2. Policy и contract + Проверить rule для class, + не название инструмента. + + + + 3. Egress gate + Destination и route соответствуют scope; + allow-list — только один факт. + + + + 4. Access и authorization + Права requester, owner, scope и expiry + сходятся на одной записи. + + + + Decision + citation + next owner question + Только fixed hand-off; egressPerformed + остаётся false. + + + + Mismatch → safe stop + Записать reason/citation; + не обходить policy, не делать send. + Ни DPA, ни auth, ни firewall отдельно не равны всему маршруту. + diff --git a/web/scripts/upgrade-2025-04.mjs b/web/scripts/upgrade-2025-04.mjs new file mode 100644 index 0000000..d334dd2 --- /dev/null +++ b/web/scripts/upgrade-2025-04.mjs @@ -0,0 +1,863 @@ +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, ' ') + .replaceAll(' ', ' ') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&', '&') + .replace(/\s+/g, ' ') + .trim(); +} + +function bodyText(content) { + return plainText(content.replace(/

Проверяемые источники<\/h2>[\s\S]*?(?=

|$)/, '')); +} + +const sources = Object.freeze([ + Object.freeze({ + title: 'GitHub Docs: Responsible use of GitHub Copilot Chat in GitHub', + url: 'https://github.com/github/docs/blob/0d17a12af77011b390abd8aea9e8626e153e0522/content/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-github.md', + version: 'immutable GitHub Docs commit 0d17a12af77011b390abd8aea9e8626e153e0522, 30 April 2025', + claim: 'На закреплённой ревизии GitHub описывает, что prompt предварительно обрабатывается и сочетается с контекстом; модель может получать дополнительный repository context, а при включённом Bing query строится из prompt и доступного context и передаётся Bing Search API.', + boundary: 'Это описание конкретной поверхности Copilot Chat на указанной ревизии. Оно не устанавливает класс данных, срок retention, обучение модели, договор конкретной организации или разрешение сотруднику передавать фрагмент.', + }), + Object.freeze({ + title: 'GitHub Docs reusable: Copilot SKU isolation', + url: 'https://github.com/github/docs/blob/0d17a12af77011b390abd8aea9e8626e153e0522/data/reusables/copilot/sku-isolation.md', + version: 'immutable GitHub Docs commit 0d17a12af77011b390abd8aea9e8626e153e0522, 30 April 2025', + claim: 'Документ описывает, что firewall allow-list плановых endpoint может разрешать Business или Enterprise endpoint и block-list может запрещать Individual endpoint внутри корпоративной сети.', + boundary: 'SKU isolation относится к маршрутизации и выбору plan endpoint. Он не показывает payload, не проверяет data class, не подтверждает retention или training terms и не заменяет human authorization.', + }), + Object.freeze({ + title: 'NIST SP 800-207: Zero Trust Architecture', + url: 'https://csrc.nist.gov/pubs/sp/800/207/final', + version: 'NIST SP 800-207, final publication 11 August 2020', + claim: 'NIST не считает network location основанием для неявного доверия и описывает authentication и authorization как отдельные функции перед доступом к enterprise resource; доступ определяется policy для конкретного resource.', + boundary: 'Публикация не даёт vendor DPA, список AI endpoint, схему классификации или доказательство того, что конкретный внешний сервис не хранит и не использует переданные данные.', + }), + Object.freeze({ + title: 'NIST AI 100-1: Artificial Intelligence Risk Management Framework 1.0', + url: 'https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf', + version: 'NIST AI 100-1, January 2023 final PDF', + claim: 'AI RMF организует управление риском вокруг GOVERN, MAP, MEASURE и MANAGE и рассматривает privacy вместе с организационными и техническими практиками, а не как одну настройку продукта.', + boundary: 'AI RMF — добровольная рамка управления риском. Она не создаёт юридический вывод, не классифицирует запись за команду и не заменяет договор, техническое измерение egress или полномочия владельца.', + }), +]); + +export const sourceReport = Object.freeze(sources.map((source) => Object.freeze({ ...source }))); + +function sourceList() { + return ''; +} + +function revision(meta, parts) { + const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + '\n' + sourceList(); + const proseLength = bodyText(contentHtml).length; + if (proseLength < 5000 || proseLength > 15000) { + throw new Error(meta.slug + ': основной текст вне диапазона 5 000–15 000 знаков: ' + proseLength); + } + return Object.freeze({ ...meta, contentHtml, proseLength }); +} + +const MODEL_VERSION = 'fixed-synthetic-ai-data-privacy-2025-04-v1'; +const FIXTURE_SCOPE = 'p86-ai-data-privacy-2025-04'; +const MODEL_LIMIT = 'versioned fixed synthetic JavaScript literals in memory only; no customer code, PII, secret, prompt, model call, external request, file, Git, CI, network, telemetry, clock, production or side effect'; +const CONTEXT_KIND = 'fixed-synthetic-ai-context-record/v1'; +const REQUEST_KIND = 'fixed-synthetic-ai-egress-request/v1'; +const AUTHORIZATION_KIND = 'fixed-synthetic-ai-human-authorization/v1'; +const DECISION_KIND = 'fixed-synthetic-ai-egress-decision/v1'; +const STOP_KIND = 'fixed-synthetic-ai-egress-stop/v1'; + +const CONTEXT_KEYS = Object.freeze(['kind', 'recordId', 'contextVersion', 'dataClass', 'allowability', 'retentionLabel', 'egressScope', 'authorityRequired', 'accessLabels', 'expiresAt', 'citation', 'payload']); +const REQUEST_KEYS = Object.freeze(['kind', 'caseId', 'syntheticOnly', 'modelVersion', 'requestId', 'requestedAt', 'requesterLabels', 'recordIds', 'requestedEgress', 'authorization', 'citationRequirement']); +const AUTHORIZATION_KEYS = Object.freeze(['kind', 'authorizationId', 'syntheticOnly', 'authorityRole', 'scopeRecordIds', 'egressScope', 'validFrom', 'expiresAt', 'requesterLabels', 'decision', 'reason', 'citation']); +const DECISION_KEYS = Object.freeze(['kind', 'accepted', 'syntheticOnly', 'modelVersion', 'requestProof', 'reason', 'citation', 'recordIds', 'classificationDecision', 'contractDecision', 'egressGate', 'authorizationDecision', 'stopCondition', 'nextAction', 'egressPerformed', 'modelLimit', 'productionEffect']); +const STOP_KEYS = Object.freeze(['kind', 'stopped', 'syntheticOnly', 'modelVersion', 'sourceDecisionProof', 'reason', 'citation', 'stopCondition', 'nextAction', 'egressPerformed', 'modelLimit', 'productionEffect']); + +function isPlainRecord(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function hasExactKeys(value, expected) { + if (!isPlainRecord(value) || Object.getOwnPropertySymbols(value).length > 0) return false; + const keys = Object.keys(value); + return keys.length === expected.length + && expected.every((key) => Object.hasOwn(value, key)) + && keys.every((key) => expected.includes(key)); +} + +function hasDenseArray(value) { + if (!Array.isArray(value)) return false; + const ownKeys = Reflect.ownKeys(value); + return ownKeys.length === value.length + 1 + && ownKeys.includes('length') + && Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)).every(Boolean); +} + +export function canonicalJson(value) { + const visited = new WeakSet(); + + function visit(current) { + if (current === null) return 'null'; + if (typeof current === 'string' || typeof current === 'boolean') return JSON.stringify(current); + if (typeof current === 'number') return Number.isFinite(current) ? JSON.stringify(current) : null; + + if (Array.isArray(current)) { + if (!hasDenseArray(current) || visited.has(current)) return null; + visited.add(current); + const values = current.map((item) => visit(item)); + visited.delete(current); + return values.every((item) => item !== null) ? '[' + values.join(',') + ']' : null; + } + + if (!isPlainRecord(current) || visited.has(current)) return null; + visited.add(current); + const values = Object.keys(current).sort().map((key) => { + const nested = visit(current[key]); + return nested === null ? null : JSON.stringify(key) + ':' + nested; + }); + visited.delete(current); + return values.every((item) => item !== null) ? '{' + values.join(',') + '}' : null; + } + + return visit(value); +} + +export function hasSameCanonicalJson(left, right) { + const leftJson = canonicalJson(left); + const rightJson = canonicalJson(right); + return leftJson !== null && rightJson !== null && leftJson === rightJson; +} + +function deepFreeze(value, seen = new WeakSet()) { + if (!value || typeof value !== 'object' || seen.has(value)) return value; + seen.add(value); + for (const child of Object.values(value)) deepFreeze(child, seen); + return Object.freeze(value); +} + +function cloneFixed(value) { + const json = canonicalJson(value); + if (json === null) throw new Error('fixed synthetic value must be canonical'); + return JSON.parse(json); +} + +function sameIsoOrBefore(left, right) { + return typeof left === 'string' && typeof right === 'string' && left <= right; +} + +function labelsCover(actual, required) { + return hasDenseArray(actual) + && hasDenseArray(required) + && required.every((label) => typeof label === 'string' && actual.includes(label)); +} + +const fixedContexts = deepFreeze({ + 'synthetic-public-interface-summary-v1': { + kind: CONTEXT_KIND, + recordId: 'synthetic-public-interface-summary-v1', + contextVersion: 'v1', + dataClass: 'synthetic-public', + allowability: 'allow-after-human-authorization', + retentionLabel: 'synthetic-contract-retention-reviewed', + egressScope: 'fixed-external-ai-boundary-alpha', + authorityRequired: 'synthetic-data-steward', + accessLabels: ['synthetic-engineering-read'], + expiresAt: '2025-04-30T23:59:59Z', + citation: 'synthetic://p86/context/public-interface-summary-v1', + payload: 'Synthetic interface summary: currency is a documented label; no customer, secret, source file or external identifier is present.', + }, + 'synthetic-internal-design-note-v2': { + kind: CONTEXT_KIND, + recordId: 'synthetic-internal-design-note-v2', + contextVersion: 'v2', + dataClass: 'synthetic-internal', + allowability: 'review-before-egress', + retentionLabel: 'synthetic-contract-not-yet-attached', + egressScope: 'fixed-external-ai-boundary-alpha', + authorityRequired: 'synthetic-data-steward', + accessLabels: ['synthetic-engineering-read'], + expiresAt: '2025-04-30T23:59:59Z', + citation: 'synthetic://p86/context/internal-design-note-v2', + payload: 'Synthetic design note: the record exists only to exercise the policy-review stop; it contains no repository or customer material.', + }, + 'synthetic-restricted-log-shape-v1': { + kind: CONTEXT_KIND, + recordId: 'synthetic-restricted-log-shape-v1', + contextVersion: 'v1', + dataClass: 'synthetic-restricted', + allowability: 'deny-external-egress', + retentionLabel: 'synthetic-no-external-retention', + egressScope: 'fixed-external-ai-boundary-alpha', + authorityRequired: 'synthetic-security-owner', + accessLabels: ['synthetic-security-read'], + expiresAt: '2025-04-30T23:59:59Z', + citation: 'synthetic://p86/context/restricted-log-shape-v1', + payload: 'Synthetic log shape: actor=[symbolic label], token=[not-present], request=[not-present]; this string intentionally contains no log event or personal data.', + }, + 'synthetic-ticket-shape-v1': { + kind: CONTEXT_KIND, + recordId: 'synthetic-ticket-shape-v1', + contextVersion: 'v1', + dataClass: 'synthetic-personal-data-shape', + allowability: 'deny-external-egress', + retentionLabel: 'synthetic-no-external-retention', + egressScope: 'fixed-external-ai-boundary-alpha', + authorityRequired: 'synthetic-privacy-owner', + accessLabels: ['synthetic-privacy-read'], + expiresAt: '2025-04-30T23:59:59Z', + citation: 'synthetic://p86/context/ticket-shape-v1', + payload: 'Synthetic ticket shape: requester=[not-present], contact=[not-present], narrative=[placeholder]; no ticket or person is represented.', + }, +}); + +const fixedAuthorizations = deepFreeze({ + 'synthetic-public-summary-approval-v1': { + kind: AUTHORIZATION_KIND, + authorizationId: 'synthetic-public-summary-approval-v1', + syntheticOnly: true, + authorityRole: 'synthetic-data-steward', + scopeRecordIds: ['synthetic-public-interface-summary-v1'], + egressScope: 'fixed-external-ai-boundary-alpha', + validFrom: '2025-04-01T00:00:00Z', + expiresAt: '2025-04-20T00:00:00Z', + requesterLabels: ['synthetic-engineering-read'], + decision: 'allow-one-fixed-public-summary-only', + reason: 'fixed training authorization: one public synthetic record, one symbolic egress scope, no external request', + citation: 'synthetic://p86/authorization/public-summary-v1', + }, + 'synthetic-expired-public-summary-approval-v1': { + kind: AUTHORIZATION_KIND, + authorizationId: 'synthetic-expired-public-summary-approval-v1', + syntheticOnly: true, + authorityRole: 'synthetic-data-steward', + scopeRecordIds: ['synthetic-public-interface-summary-v1'], + egressScope: 'fixed-external-ai-boundary-alpha', + validFrom: '2025-04-01T00:00:00Z', + expiresAt: '2025-04-10T00:00:00Z', + requesterLabels: ['synthetic-engineering-read'], + decision: 'allow-one-fixed-public-summary-only', + reason: 'fixed expired authorization used only to test the expiry stop', + citation: 'synthetic://p86/authorization/expired-public-summary-v1', + }, +}); + +const fixedCases = deepFreeze({ + 'authorized-public-summary': { + request: { + kind: REQUEST_KIND, + caseId: 'authorized-public-summary', + syntheticOnly: true, + modelVersion: MODEL_VERSION, + requestId: 'synthetic-request-public-summary-v1', + requestedAt: '2025-04-16T10:00:00Z', + requesterLabels: ['synthetic-engineering-read'], + recordIds: ['synthetic-public-interface-summary-v1'], + requestedEgress: 'fixed-external-ai-boundary-alpha', + authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'], + citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required', + }, + }, + 'internal-policy-review-stop': { + request: { + kind: REQUEST_KIND, + caseId: 'internal-policy-review-stop', + syntheticOnly: true, + modelVersion: MODEL_VERSION, + requestId: 'synthetic-request-internal-note-v1', + requestedAt: '2025-04-16T10:00:00Z', + requesterLabels: ['synthetic-engineering-read'], + recordIds: ['synthetic-internal-design-note-v2'], + requestedEgress: 'fixed-external-ai-boundary-alpha', + authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'], + citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required', + }, + }, + 'restricted-class-stop': { + request: { + kind: REQUEST_KIND, + caseId: 'restricted-class-stop', + syntheticOnly: true, + modelVersion: MODEL_VERSION, + requestId: 'synthetic-request-restricted-log-v1', + requestedAt: '2025-04-16T10:00:00Z', + requesterLabels: ['synthetic-security-read'], + recordIds: ['synthetic-restricted-log-shape-v1'], + requestedEgress: 'fixed-external-ai-boundary-alpha', + authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'], + citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required', + }, + }, + 'access-boundary-stop': { + request: { + kind: REQUEST_KIND, + caseId: 'access-boundary-stop', + syntheticOnly: true, + modelVersion: MODEL_VERSION, + requestId: 'synthetic-request-access-boundary-v1', + requestedAt: '2025-04-16T10:00:00Z', + requesterLabels: ['synthetic-observer-read'], + recordIds: ['synthetic-public-interface-summary-v1'], + requestedEgress: 'fixed-external-ai-boundary-alpha', + authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'], + citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required', + }, + }, + 'expired-authorization-stop': { + request: { + kind: REQUEST_KIND, + caseId: 'expired-authorization-stop', + syntheticOnly: true, + modelVersion: MODEL_VERSION, + requestId: 'synthetic-request-expired-authorization-v1', + requestedAt: '2025-04-16T10:00:00Z', + requesterLabels: ['synthetic-engineering-read'], + recordIds: ['synthetic-public-interface-summary-v1'], + requestedEgress: 'fixed-external-ai-boundary-alpha', + authorization: fixedAuthorizations['synthetic-expired-public-summary-approval-v1'], + citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required', + }, + }, + 'egress-boundary-stop': { + request: { + kind: REQUEST_KIND, + caseId: 'egress-boundary-stop', + syntheticOnly: true, + modelVersion: MODEL_VERSION, + requestId: 'synthetic-request-wrong-egress-v1', + requestedAt: '2025-04-16T10:00:00Z', + requesterLabels: ['synthetic-engineering-read'], + recordIds: ['synthetic-public-interface-summary-v1'], + requestedEgress: 'fixed-unapproved-egress-scope', + authorization: fixedAuthorizations['synthetic-public-summary-approval-v1'], + citationRequirement: 'fixed-synthetic-record-and-authorization-citation-required', + }, + }, +}); + +function decisionFor(request, values) { + return deepFreeze({ + kind: DECISION_KIND, + accepted: values.accepted, + syntheticOnly: true, + modelVersion: MODEL_VERSION, + requestProof: canonicalJson(request) || 'invalid-fixed-request-proof', + reason: values.reason, + citation: values.citation, + recordIds: values.recordIds, + classificationDecision: values.classificationDecision, + contractDecision: values.contractDecision, + egressGate: values.egressGate, + authorizationDecision: values.authorizationDecision, + stopCondition: values.stopCondition, + nextAction: values.nextAction, + egressPerformed: false, + modelLimit: MODEL_LIMIT, + productionEffect: 'not-attempted', + }); +} + +function rejectedDecision(request, reason, citation = 'synthetic://p86/decision/invalid-request', stopCondition = 'fixed request validation did not establish a safe synthetic scope') { + return decisionFor(request, { + accepted: false, + reason, + citation, + recordIds: [], + classificationDecision: 'not-established', + contractDecision: 'not-established', + egressGate: 'closed', + authorizationDecision: 'not-established', + stopCondition, + nextAction: 'retain no payload and ask for a new fixed synthetic request with one named boundary', + }); +} + +function validateFixedRequest(request) { + if (!hasExactKeys(request, REQUEST_KEYS)) return { ok: false, reason: 'request-exact-contract-failed' }; + if (request.kind !== REQUEST_KIND || request.syntheticOnly !== true || request.modelVersion !== MODEL_VERSION) { + return { ok: false, reason: 'request-version-or-synthetic-boundary-failed' }; + } + if (!hasDenseArray(request.requesterLabels) || !hasDenseArray(request.recordIds)) { + return { ok: false, reason: 'request-array-density-failed' }; + } + const fixed = fixedCases[request.caseId]; + if (!fixed || !hasSameCanonicalJson(request, fixed.request)) { + return { ok: false, reason: 'request-proof-or-authorization-forged' }; + } + return { ok: true, fixed }; +} + +function recordForId(recordId) { + const record = fixedContexts[recordId]; + if (!record || !hasExactKeys(record, CONTEXT_KEYS) || !hasDenseArray(record.accessLabels)) return null; + return record; +} + +function authorizationIsWellFormed(authorization) { + return hasExactKeys(authorization, AUTHORIZATION_KEYS) + && authorization.kind === AUTHORIZATION_KIND + && authorization.syntheticOnly === true + && hasDenseArray(authorization.scopeRecordIds) + && hasDenseArray(authorization.requesterLabels); +} + +function authorizationCovers(request, records) { + const authorization = request.authorization; + if (!authorizationIsWellFormed(authorization)) return { ok: false, reason: 'human-authorization-shape-failed' }; + if (authorization.egressScope !== request.requestedEgress) return { ok: false, reason: 'authorization-egress-scope-mismatch' }; + if (!sameIsoOrBefore(authorization.validFrom, request.requestedAt) || !sameIsoOrBefore(request.requestedAt, authorization.expiresAt)) { + return { ok: false, reason: 'human-authorization-expired-or-not-yet-valid' }; + } + if (!labelsCover(request.requesterLabels, authorization.requesterLabels)) return { ok: false, reason: 'requester-access-does-not-match-authorization' }; + if (!records.every((record) => authorization.scopeRecordIds.includes(record.recordId))) return { ok: false, reason: 'human-authorization-does-not-cover-record' }; + if (!records.every((record) => record.authorityRequired === authorization.authorityRole)) return { ok: false, reason: 'human-authorization-owner-mismatch' }; + if (authorization.decision !== 'allow-one-fixed-public-summary-only') return { ok: false, reason: 'human-authorization-decision-not-fixed' }; + return { ok: true }; +} + +export function assessSyntheticAiEgress(request) { + const checked = validateFixedRequest(request); + if (!checked.ok) return rejectedDecision(request, checked.reason); + + const records = request.recordIds.map(recordForId); + if (records.some((record) => record === null)) { + return rejectedDecision(request, 'record-id-is-not-a-fixed-versioned-context', 'synthetic://p86/decision/record-boundary'); + } + + const citation = records.map((record) => record.citation).join(' | '); + const classesAllow = records.every((record) => record.allowability === 'allow-after-human-authorization'); + if (!classesAllow) { + return decisionFor(request, { + accepted: false, + reason: 'data class or allowability denies this synthetic external boundary', + citation, + recordIds: request.recordIds, + classificationDecision: 'denied-by-fixed-class-or-allowability', + contractDecision: 'not-accepted', + egressGate: 'closed', + authorizationDecision: 'not-consulted-as-an-override', + stopCondition: 'a restricted or review-required record cannot be promoted by this model', + nextAction: 'keep the record in memory, record its class and ask the owner for a policy/contract decision outside this package', + }); + } + + const retentionReviewed = records.every((record) => record.retentionLabel === 'synthetic-contract-retention-reviewed'); + if (!retentionReviewed) { + return decisionFor(request, { + accepted: false, + reason: 'retention or contract label is not established for the fixed context', + citation, + recordIds: request.recordIds, + classificationDecision: 'class-permits-conditional-review', + contractDecision: 'retention-label-missing-or-denied', + egressGate: 'closed', + authorizationDecision: 'not-consulted-as-an-override', + stopCondition: 'a class label does not prove the required contract or retention condition', + nextAction: 'stop and obtain a separately checkable policy/contract record; do not infer it from a product setting', + }); + } + + const egressMatches = records.every((record) => record.egressScope === request.requestedEgress); + if (!egressMatches) { + return decisionFor(request, { + accepted: false, + reason: 'requested egress scope differs from the fixed context boundary', + citation, + recordIds: request.recordIds, + classificationDecision: 'class-permits-conditional-review', + contractDecision: 'retention-label-present', + egressGate: 'closed', + authorizationDecision: 'not-consulted-as-an-override', + stopCondition: 'destination or route is outside the record-specific synthetic scope', + nextAction: 'keep the context local and verify the destination with the network and service owners before a new authorized review', + }); + } + + const accessMatches = records.every((record) => labelsCover(request.requesterLabels, record.accessLabels)); + if (!accessMatches) { + return decisionFor(request, { + accepted: false, + reason: 'requester access labels do not cover the fixed context record', + citation, + recordIds: request.recordIds, + classificationDecision: 'class-permits-conditional-review', + contractDecision: 'retention-label-present', + egressGate: 'scope-matches-but-closed', + authorizationDecision: 'requester-access-mismatch', + stopCondition: 'reading or handling access is not established for this requester', + nextAction: 'safe stop; ask the access owner to review the named record and requester scope, without copying its payload', + }); + } + + const recordsFresh = records.every((record) => sameIsoOrBefore(request.requestedAt, record.expiresAt)); + if (!recordsFresh) { + return decisionFor(request, { + accepted: false, + reason: 'fixed context record is outside its review expiry boundary', + citation, + recordIds: request.recordIds, + classificationDecision: 'class-permits-conditional-review', + contractDecision: 'expired-context-record', + egressGate: 'closed', + authorizationDecision: 'not-accepted', + stopCondition: 'a versioned record past its fixed expiry cannot support hand-off', + nextAction: 'refresh the local classification and contract evidence with its owner; do not re-label the old record in place', + }); + } + + const authorization = authorizationCovers(request, records); + if (!authorization.ok) { + return decisionFor(request, { + accepted: false, + reason: authorization.reason, + citation, + recordIds: request.recordIds, + classificationDecision: 'class-permits-conditional-review', + contractDecision: 'retention-label-present', + egressGate: 'scope-matches-but-closed', + authorizationDecision: 'missing-expired-or-mismatched', + stopCondition: 'human authorization is scoped, dated and non-transferable in this fixed model', + nextAction: 'safe stop and request a fresh owner decision for the exact record, requester and destination', + }); + } + + return decisionFor(request, { + accepted: true, + reason: 'all fixed labels align for an authorized synthetic hand-off; no external request is performed', + citation: citation + ' | ' + request.authorization.citation, + recordIds: request.recordIds, + classificationDecision: 'fixed-class-permits-conditional-handoff', + contractDecision: 'fixed-retention-label-present', + egressGate: 'fixed-scope-matches', + authorizationDecision: 'fixed-human-authorization-valid-for-scope', + stopCondition: 'the model ends before any external request, payload transfer or production action', + nextAction: 'hand the named evidence to the real policy, network and data owners; collect fresh facts there before any actual transmission', + }); +} + +export function stopSyntheticAiEgress(decision) { + const recognized = hasExactKeys(decision, DECISION_KEYS) + && decision.kind === DECISION_KIND + && decision.syntheticOnly === true + && decision.modelVersion === MODEL_VERSION + && decision.egressPerformed === false + && decision.productionEffect === 'not-attempted' + && canonicalJson(decision) !== null; + const blocked = recognized ? !decision.accepted : true; + + return deepFreeze({ + kind: STOP_KIND, + stopped: true, + syntheticOnly: true, + modelVersion: MODEL_VERSION, + sourceDecisionProof: recognized ? canonicalJson(decision) : 'invalid-fixed-decision-proof', + reason: recognized + ? (blocked ? decision.reason : 'model intentionally stops before any real egress') + : 'invalid decision cannot open an egress path', + citation: recognized ? decision.citation : 'synthetic://p86/stop/invalid-decision', + stopCondition: recognized ? decision.stopCondition : 'invalid decision lacks a fixed synthetic boundary', + nextAction: recognized ? decision.nextAction : 'discard the decision and create a new fixed in-memory request', + egressPerformed: false, + modelLimit: MODEL_LIMIT, + productionEffect: 'not-attempted', + }); +} + +export function createFixedSyntheticContextRequest(caseId = 'authorized-public-summary') { + const fixed = fixedCases[caseId]; + if (!fixed) throw new Error('unknown fixed synthetic case: ' + caseId); + return cloneFixed(fixed.request); +} + +export function runAiDataPrivacyFixture() { + const allowed = createFixedSyntheticContextRequest('authorized-public-summary'); + const allowedDecision = assessSyntheticAiEgress(allowed); + const allowedStop = stopSyntheticAiEgress(allowedDecision); + + const internal = createFixedSyntheticContextRequest('internal-policy-review-stop'); + const internalDecision = assessSyntheticAiEgress(internal); + const restricted = createFixedSyntheticContextRequest('restricted-class-stop'); + const restrictedDecision = assessSyntheticAiEgress(restricted); + const access = createFixedSyntheticContextRequest('access-boundary-stop'); + const accessDecision = assessSyntheticAiEgress(access); + const expired = createFixedSyntheticContextRequest('expired-authorization-stop'); + const expiredDecision = assessSyntheticAiEgress(expired); + const wrongEgress = createFixedSyntheticContextRequest('egress-boundary-stop'); + const wrongEgressDecision = assessSyntheticAiEgress(wrongEgress); + + const extraKey = { ...allowed, unexpected: true }; + const missingKey = { ...allowed }; + delete missingKey.requestId; + const forgedAuthorization = cloneFixed(allowed); + forgedAuthorization.authorization.decision = 'allow-all-synthetic-context'; + const forgedScope = cloneFixed(allowed); + forgedScope.authorization.scopeRecordIds.push('synthetic-restricted-log-shape-v1'); + const sparseRecordIds = cloneFixed(allowed); + delete sparseRecordIds.recordIds[0]; + const sparseAuthorization = cloneFixed(allowed); + delete sparseAuthorization.authorization.scopeRecordIds[0]; + const cyclicRequest = cloneFixed(allowed); + cyclicRequest.requesterLabels.push(cyclicRequest); + const cyclicAuthorization = cloneFixed(allowed); + cyclicAuthorization.authorization.scopeRecordIds.push(cyclicAuthorization.authorization); + const forgedDecision = { ...allowedDecision, accepted: true, egressPerformed: true }; + + const fixedBefore = canonicalJson(fixedContexts['synthetic-public-interface-summary-v1']); + assessSyntheticAiEgress(allowed); + const fixedAfter = canonicalJson(fixedContexts['synthetic-public-interface-summary-v1']); + + const assertions = { + fixtureScopeIsFixed: FIXTURE_SCOPE === 'p86-ai-data-privacy-2025-04', + modelIsStrictlyMemoryOnly: MODEL_LIMIT.includes('in memory only') && MODEL_LIMIT.includes('no customer code') && MODEL_LIMIT.includes('network') && MODEL_LIMIT.includes('side effect'), + everyContextHasExactContract: Object.values(fixedContexts).every((record) => hasExactKeys(record, CONTEXT_KEYS)), + everyContextAccessListIsDense: Object.values(fixedContexts).every((record) => hasDenseArray(record.accessLabels)), + everyAuthorizationHasExactContract: Object.values(fixedAuthorizations).every((authorization) => hasExactKeys(authorization, AUTHORIZATION_KEYS)), + allowedRequestHasExactContract: hasExactKeys(allowed, REQUEST_KEYS), + allowedRequestHasDenseLists: hasDenseArray(allowed.requesterLabels) && hasDenseArray(allowed.recordIds) && hasDenseArray(allowed.authorization.scopeRecordIds), + allowedDecisionHasExactContract: hasExactKeys(allowedDecision, DECISION_KEYS), + allowedDecisionIsOnlyHandoff: allowedDecision.accepted === true && allowedDecision.egressPerformed === false && allowedDecision.productionEffect === 'not-attempted', + allowedDecisionSeparatesBoundaries: allowedDecision.classificationDecision === 'fixed-class-permits-conditional-handoff' && allowedDecision.contractDecision === 'fixed-retention-label-present' && allowedDecision.egressGate === 'fixed-scope-matches' && allowedDecision.authorizationDecision === 'fixed-human-authorization-valid-for-scope', + allowedPathStillStopsBeforeIO: allowedStop.stopped === true && allowedStop.egressPerformed === false && allowedStop.productionEffect === 'not-attempted', + internalPolicyGapStops: internalDecision.accepted === false && internalDecision.reason.includes('data class or allowability') && internalDecision.egressGate === 'closed', + restrictedClassStopsBeforeAuthorizationOverride: restrictedDecision.accepted === false && restrictedDecision.classificationDecision === 'denied-by-fixed-class-or-allowability' && restrictedDecision.authorizationDecision === 'not-consulted-as-an-override', + accessBoundaryStops: accessDecision.accepted === false && accessDecision.reason.includes('requester access') && accessDecision.egressGate.includes('closed'), + expiredAuthorizationStops: expiredDecision.accepted === false && expiredDecision.reason.includes('expired-or-not-yet-valid') && expiredDecision.authorizationDecision === 'missing-expired-or-mismatched', + egressBoundaryStops: wrongEgressDecision.accepted === false && wrongEgressDecision.reason.includes('egress scope differs') && wrongEgressDecision.egressGate === 'closed', + extraRequestKeyFailsClosed: assessSyntheticAiEgress(extraKey).accepted === false, + missingRequestKeyFailsClosed: assessSyntheticAiEgress(missingKey).accepted === false, + forgedAuthorizationFailsClosed: assessSyntheticAiEgress(forgedAuthorization).accepted === false, + forgedAuthorizationScopeFailsClosed: assessSyntheticAiEgress(forgedScope).accepted === false, + sparseRecordListFailsClosed: assessSyntheticAiEgress(sparseRecordIds).accepted === false, + sparseAuthorizationListFailsClosed: assessSyntheticAiEgress(sparseAuthorization).accepted === false, + cyclicRequestFailsClosedWithoutThrow: assessSyntheticAiEgress(cyclicRequest).accepted === false, + cyclicAuthorizationFailsClosedWithoutThrow: assessSyntheticAiEgress(cyclicAuthorization).accepted === false, + forgedDecisionStopsClosed: stopSyntheticAiEgress(forgedDecision).reason === 'invalid decision cannot open an egress path', + fixedContextWasNotMutated: fixedBefore === fixedAfter, + resultDoesNotExposePayload: !Object.hasOwn(allowedDecision, 'payload') && !Object.hasOwn(allowedStop, 'payload'), + unsupportedCaseThrowsBeforeAnyIO: (() => { + try { + createFixedSyntheticContextRequest('not-a-fixed-case'); + return false; + } catch { + return true; + } + })(), + }; + + return deepFreeze({ kind: 'fixed-synthetic-ai-data-privacy-fixture/v1', assertions }); +} + +const practice = revision({ + slug: 'editorial-2025-04-practice-ai-data-privacy', + title: 'Контекст без класса не должен уходить в AI-инструмент', + excerpt: 'Короткий маршрут, который отделяет класс данных, contract, egress и human authorization до передачи фрагмента в AI-инструмент.', + readingMinutes: 11, +}, [ + p('Фрагмент кода, лога или тикета часто выглядит безобидно, пока его не нужно вставить в AI-инструмент. Но у фрагмента уже есть происхождение, класс, владелец и возможные получатели контекста. Симптом — инженер видит полезный вопрос и отправляет «маленький кусок», не зная, что к prompt может добавиться контекст продукта или поиска. Цена ошибки — не абстрактная приватность: команда теряет возможность объяснить, что именно ушло за границу, кто это разрешил и какой договорный режим должен был работать.'), + p('Причина обычно в смешении четырёх разных решений. Data classification отвечает, что представляет запись. Policy и contract отвечают, допускается ли такой класс и при каких retention или training conditions. Технический egress отвечает, куда может пойти трафик. Human authorization отвечает, кто разрешил именно этот scope и на какой срок. Если заменить все четыре вопроса одним «у нас есть DPA» или «в интерфейсе выключена галочка», следующее расследование начнётся без исходных доказательств.'), + h2('Передавать не текст, а описанную единицу контекста'), + p('Первый рабочий шаг — не редактировать prompt на глаз, а присвоить фрагменту versioned record. В нём нужны хотя бы record id, version, class, allowability, retention label, egress scope, required authority, access label, expiry и citation. Это не универсальная схема для права или закупки. Это инженерная карточка, благодаря которой человек может проверить ровно один фрагмент, а не спорить о слове «внутренний». Если значения неизвестны, карточка должна останавливаться, а не получать наиболее оптимистичный label.'), + p('Class — это не разрешение. Запись с class synthetic-public в учебной модели всё равно требует policy/contract label и отдельного разрешения владельца. И наоборот, даже корректный человек не получает права поднять synthetic-restricted до допустимого класса одной подписью. Такое разделение неприятно в первый день, потому что прибавляет несколько полей. Зато на следующем review не приходится угадывать, относим ли мы обсуждение к содержимому, маршруту или полномочиям.'), + figure('/assets/editorial/2025/ai-data-privacy-2025-context-boundary.svg', 'Пять независимых границ перед AI-инструментом: class данных, допустимость и retention, technical egress, human authorization и hand-off без реальной отправки.', 'Схема показывает, что отсутствие доказательства на любом шаге ведёт к safe stop. Зелёный hand-off означает только готовность передать вопрос владельцам, а не выполненный внешний запрос.'), + h2('Минимальная карточка: какие поля отвечают на какие вопросы'), + table('Поля fixed synthetic record', ['Поле', 'Проверяемый вопрос', 'Пример label', 'Чего поле не доказывает'], [ + ['dataClass', 'Какой локальный класс у содержимого?', 'synthetic-public или synthetic-restricted', 'условия vendor contract и сетевой маршрут'], + ['allowability', 'Можно ли вообще рассматривать egress?', 'allow-after-human-authorization', 'что разрешение уже выдано конкретному requester'], + ['retentionLabel', 'Есть ли проверенное условие хранения/обучения для нужного scope?', 'synthetic-contract-retention-reviewed', 'фактический payload, endpoint или будущую версию сервиса'], + ['egressScope', 'Какой symbolic destination разрешён карточке?', 'fixed-external-ai-boundary-alpha', 'что firewall действительно пропустил данные или что payload безопасен'], + ['authorityRequired и expiry', 'Кто и до какой даты вправе одобрить hand-off?', 'synthetic-data-steward до 20 апреля', 'права всех будущих пользователей или другой record'], + ['citation', 'На какую карточку и решение можно сослаться?', 'synthetic://p86/…', 'доказательство реальной политики вне учебной модели'], + ]), + p('В реальном процессе поля могут жить в каталоге данных, тикете, policy-as-code или системе заявок. Место не важно для этой статьи; важна связь. Если class записан в одном месте, contract в другом, egress в третьем, а approval лежит в личной переписке, review не может собрать их в один проверяемый chain. Тогда короткий prompt экономит минуту, но создаёт дорогой вопрос «почему мы решили, что это допустимо?».'), + h2('Воспроизводимый пример: только фиксированные записи в памяти'), + p('Ниже не показан запрос к модели и не передаётся даже синтетический payload. Скрипт создаёт заранее заданную карточку, проверяет равенство class, contract label, egress scope и dated authorization, а затем возвращает decision. Даже положительный outcome заканчивается на hand-off: поле egressPerformed остаётся false. Такая форма полезна для обучения review, потому что нельзя случайно выдать пример за интеграцию.'), + code([ + "import {", + " createFixedSyntheticContextRequest,", + " assessSyntheticAiEgress,", + " stopSyntheticAiEgress,", + "} from './upgrade-2025-04.mjs';", + '', + "const request = createFixedSyntheticContextRequest('authorized-public-summary');", + 'const decision = assessSyntheticAiEgress(request);', + 'const boundaryStop = stopSyntheticAiEgress(decision);', + '', + 'console.log(decision.accepted); // true: fixed hand-off only', + 'console.log(decision.egressPerformed); // false', + 'console.log(boundaryStop.stopped); // true: no external request exists', + ].join('\n')), + p('У модели есть и отрицательные cases: internal record без settled contract label, restricted log shape, requester без нужного access label, expired authorization и другой egress scope. Каждый возвращает reason, citation, stop condition и next action. Важна формулировка stop: «на этом evidence нельзя открыть путь», а не «данные навсегда запрещены». Внешний мир может содержать дополнительные договоры и технические меры; эта модель просто не имеет права их придумывать.'), + h2('Как проводить короткий pre-flight review'), + ol([ + 'Назовите единицу. Запишите record id, version и class до того, как копировать фрагмент в prompt или подключать инструмент к репозиторию.', + 'Разведите policy и contract. Сверьте allowability с нужным retention/training condition для данного service scope; не переносите вывод с другого plan или старой версии.', + 'Проверьте egress отдельно. Сопоставьте destination, route и технический allow-list. Наличие сети или лицензии не делает payload допустимым.', + 'Сверьте access и authority. Requester должен иметь доступ к record, а владелец — выдать scoped, dated решение. Approval без scope не переносится на соседний фрагмент.', + 'Запишите safe stop. Если один факт неизвестен, оставьте payload локально, укажите owner question и не ищите обход через другой UI или account.', + ]), + h2('Почему интерфейс и договор не закрывают весь маршрут'), + p('Фиксированный GitHub Docs на апрель 2025 показывает полезную инженерную деталь: prompt может обрабатываться вместе с контекстом, а отдельные возможности могут собирать repository context или сформировать Bing query при включённом поиске. Это не утверждение о каждом AI-инструменте. Оно показывает, почему проверять только видимый кусок текста недостаточно: нужно знать, какая поверхность и какие дополнительные источники контекста включены в данном режиме.'), + p('Другой GitHub документ описывает SKU isolation через plan-specific endpoint в firewall. Это хороший пример технического control: он может ограничить, какой plan endpoint используют люди в сети. Но он не видит data class, не сравнивает условие retention, не проверяет, что человек вправе раскрыть record, и не заменяет решение владельца. NIST SP 800-207 так же отделяет policy-based access от одного лишь network location. В статье это не юридическая консультация, а причина не считать один control доказательством всех остальных.'), + h2('Ограничения и следующий проверяемый шаг'), + p('Весь пример — versioned fixed synthetic JavaScript literals в памяти. Здесь нет customer code, PII, secret, реального лога, тикета, файла, Git, CI, telemetry, сети, clock, model call или production side effect. Labels synthetic-public, synthetic-restricted и synthetic-contract-retention-reviewed не описывают ни одну вашу систему и не доказывают фактическое хранение или обучение у поставщика. Они нужны только для проверки формы решения и fail-closed пути.'), + p('Следующий шаг: возьмите один тип контекста, который команда реально хочет использовать с AI, но не копируйте его в шаблон. Сначала создайте пустую карточку с class, allowability, contract/retention evidence, egress scope, owner, expiry и citation. Если хотя бы одно поле нельзя честно заполнить, результат review уже полезен: это safe stop и конкретный вопрос к data, security, legal или service owner.'), + h2('Историческая граница апреля 2025'), + p('Здесь используются только источники, существовавшие к 30 апреля 2025: два immutable GitHub Docs commit, NIST SP 800-207 final 2020 и NIST AI RMF 1.0 final 2023. В текст не перенесены поздние model names, provider terms или функции агентов. Любая будущая проверка должна заново закрепить версию документа и применимость к своему plan, region, contract и маршруту.'), +]); + +const mechanism = revision({ + slug: 'editorial-2025-04-mechanism-ai-data-privacy', + title: 'Четыре разных доказательства для AI-egress', + excerpt: 'Почему class, policy/contract, technical egress и human authorization должны сходиться на одной версии контекста, а не заменять друг друга.', + readingMinutes: 12, +}, [ + p('Самая дорогая ошибка в работе с AI-контекстом часто происходит без утечки, сбоя или громкого инцидента. Команда уже не может восстановить решение: был ли фрагмент разрешён, к какому plan он относился, куда шёл трафик и кто согласовал срок. Цена ошибки — остановка внедрения, повторная инвентаризация и риск принять второе решение по памяти вместо фактов. Особенно плохо, когда evidence распределён между настройкой IDE, DPA, сетевым правилом и устным «можно».'), + p('Причина — попытка сделать один артефакт универсальным. Classification не равна policy, policy не равна contract, contract не равен egress, egress не равен authorization. Они пересекаются только в том, что все относятся к одной передаче. М8-автор не предлагает «самую строгую» кнопку: он строит короткую цепочку, в которой каждое звено отвечает на свой вопрос и честно оставляет границу видимой.'), + h2('Разложить решение на четыре оси'), + p('Data classification описывает содержимое локальным языком команды: public, internal, restricted, personal-data-shape или другой явно определённый class. Это вход в решение, а не вывод о поставщике. Policy задаёт допустимость: например, class может требовать отдельного review или прямо закрывать внешнюю передачу. Contract уточняет, какие обязательства проверены для выбранного service scope: retention, processing, training terms, region или subprocessor — ровно те, которые важны организации. Нельзя заменить неизвестное условие словом «корпоративный».'), + p('Technical egress отвечает уже на другой вопрос: есть ли путь к конкретному destination и соответствует ли он разрешённой конфигурации. Firewall allow-list, proxy rule, DNS policy или network segmentation полезны, но они работают с route и endpoint, а не со смыслом фрагмента. Human authorization отвечает на третий слой принятия решения: какой owner рассмотрел record, requester, destination и срок. Владелец не должен переопределять deny-class молча, а сеть не должна доказывать его полномочия.'), + figure('/assets/editorial/2025/ai-data-privacy-2025-allowability-matrix.svg', 'Матрица четырёх synthetic context record показывает, что class, policy/contract, egress и authority дают разные outcomes: hand-off только у одного fixed public case, остальные ведут в stop.', 'Матрица не классифицирует реальные данные. Она делает заметным, почему положительный факт в одной колонке не переносится в соседнюю.'), + h2('Матрица доказательств: какой артефакт имеет какой scope'), + table('Не путать proof с похожим сигналом', ['Вопрос', 'Нужное evidence', 'Что может помочь', 'Чего этого недостаточно доказать'], [ + ['Какой class у record?', 'версионированная карточка и owner vocabulary', 'название папки или тега', 'допустимость внешней передачи'], + ['Допустим ли class?', 'policy decision и contract condition для service scope', 'DPA или UI setting как вход в review', 'фактический destination и применимость к конкретному plan'], + ['Куда может пойти трафик?', 'проверенный destination, route и egress control', 'firewall allow-list', 'что payload соответствует class или что contract покрывает usage'], + ['Кто разрешил передачу?', 'dated authorization с record, requester и scope', 'лицензия пользователя или одобрение продукта', 'права на другой record, срок или destination'], + ['Что делать при пробеле?', 'reason, citation, stop condition и owner question', 'общее «надо уточнить»', 'обход через другой account, device или tool'], + ]), + p('Это не бюрократическая таблица ради таблицы. Она сокращает конфликт review. Когда security говорит «endpoint разрешён», а privacy отвечает «record не классифицирован», оба могут быть правы, просто отвечают на разные вопросы. Правильное действие — не голосовать, чей control сильнее, а сохранить два факта и закрыть egress до тех пор, пока policy/contract факт не появится. Так stop остаётся техническим решением о неполном evidence, а не личной оценкой осторожности коллеги.'), + h2('Механизм fail-closed: сначала проверки, затем только hand-off'), + p('В P86 одинаково важны порядок и отрицательный результат. Модель сначала проверяет exact keys и canonical form request, потом вытаскивает только known fixed records. Далее она оценивает allowability и retention label, сравнивает egress scope, access label и датированную authorization. Нельзя начать с approval: иначе оно станет способом скрыть restricted class. Нельзя начать с network: иначе успешный route станет ложным доказательством допустимости.'), + code([ + "const restricted = createFixedSyntheticContextRequest('restricted-class-stop');", + 'const decision = assessSyntheticAiEgress(restricted);', + 'const stop = stopSyntheticAiEgress(decision);', + '', + 'console.log(decision.classificationDecision);', + '// denied-by-fixed-class-or-allowability', + 'console.log(decision.authorizationDecision);', + '// not-consulted-as-an-override', + 'console.log(stop.egressPerformed);', + '// false', + ].join('\n')), + p('Низкий уровень реализации здесь нарочно скучный. Record ids, authorization и expected fields не принимаются «похожими». Лишний key, missing key, sparse array, cyclic value или forged authorization не получают частичный accept: fixture возвращает closed decision. Это защищает учебный пример от обычной ошибки автоматизации, когда сериализатор или удобный spread silently добавляет неоговорённую информацию. В production нужны другие controls, но fail-closed форма полезна уже до выбора tool.'), + h2('Retentions и training terms — отдельная проверка, не магический label'), + p('Retention label в примере называется synthetic-contract-retention-reviewed. Он не утверждает, что какой-либо vendor реально не хранит prompt, не обучается на нём или не передаёт его subprocessors. Он говорит гораздо уже: для одной synthetic карточки в учебной модели существует проверяемое условие, без которого hand-off закрыт. В реальной работе label должен ссылаться на документ и версию, applicable plan, region, accounts, features и исключения. Если термины меняются, изменяется не только ссылка, но и вывод.'), + p('Именно здесь полезна дисциплина immutable sources. GitHub Docs на закреплённом commit фиксирует, что конкретная chat surface может сочетать user input с дополнительным context и при включённом Bing отправлять сформированный query в Bing Search API. Это не доказывает retention или training для вашего продукта, но помогает задать правильный вопрос: какой контекст собирает именно этот surface и есть ли дополнительные outbound paths? Необходимо проверять не только marketing page, а технический режим, который включён команде.'), + h2('Egress control и zero trust: полезный, но ограниченный слой'), + p('GitHub SKU isolation описывает plan-specific endpoint и firewall allow/block list. Такой control может снизить риск того, что сотрудник в корпоративной сети использует не тот subscription path. Но данные не приобретают класс в момент TLS connection. Поэтому в таблице egress gate идёт после class и contract, а не вместо них. Если proxy разрешил host, остаются вопросы: какой feature включён, какой context он добавляет, соответствует ли record нужному scope и есть ли human authorization на этот date.'), + p('NIST SP 800-207 формулирует эту мысль шире: network location сама по себе не даёт implicit trust, а access к resource опирается на policy и отдельные authentication/authorization functions. В этой статье AI-инструмент не объявляется enterprise resource из NIST и не переносится схема стандарта один к одному. Важен принцип разделения: факт «путь существует» не отвечает на вопрос «данный субъект вправе передать данный record по этому пути».'), + h2('Порядок review, который можно повторить'), + ol([ + 'Соберите одну строку identity. Record id, version, class и owner должны быть видны до обсуждения prompt.', + 'Спросите policy/contract question. Какое условие допустимости и retention/training evidence нужно именно для этой surface, plan и destination?', + 'Спросите egress question. Какая техническая граница разрешена и кто подтвердил её применимость, не подменяя этим class?', + 'Спросите authority question. Кого достаточно для решения, какой requester и date покрыты, а какие нет?', + 'Сделайте один outcome. Hand-off в отдельный authorised workflow или safe stop с reason, citation и следующим владельцем. Не делайте send в ходе review.', + ]), + h2('Ограничения и следующий проверяемый шаг'), + p('Ни матрица, ни fixture не являются privacy impact assessment, договором или архитектурой DLP. Они не обнаруживают PII, не проверяют endpoint, не читают logs, не знают region, не вызывают model и не оценивают legal basis. В фиксированных strings намеренно нет customer data, secret, исходного кода, реального тикета или telemetry. Поэтому положительный synthetic outcome нельзя превращать в разрешение на любой настоящий prompt, а отрицательный нельзя использовать как общий запрет на технологию.'), + p('Следующий шаг: для одного включённого AI feature создайте four-column evidence sheet: class/owner, policy+contract, egress, authorization. В каждой колонке оставьте URL или immutable reference, date и boundary: что этот факт не доказывает. Затем возьмите один известный gap и проверьте, что система безопасно останавливается без доступа к payload. Результат должен быть короче policy memo, но достаточен, чтобы другой reviewer повторил вопрос.'), + h2('Историческая граница апреля 2025'), + p('Пакет опирается на GitHub Docs commit от 30 апреля 2025 и на финальные NIST публикации 2020 и 2023. Он не предполагает, что поведение, storage, model routing или product terms позднее не изменятся. При реальном внедрении документация поставщика, договор и конфигурация проверяются заново на дату решения; эта статья показывает форму проверки, а не переносимый verdict.'), +]); + +const field = revision({ + slug: 'editorial-2025-04-field-ai-data-privacy', + title: 'Когда synthetic log shape должен остановить AI-review', + excerpt: 'Кейс без реальных логов и PII: как пройти class, contract, egress, access и expiry до того, как AI-инструмент увидит контекст.', + readingMinutes: 12, +}, [ + p('Во время разбора ошибки инженеру хочется показать AI-инструменту «всего один лог и один тикет»: так легче получить гипотезу о причине. Но если у фрагмента нет class, нельзя сказать, допустим ли сам вопрос, а не только удобен ли ответ. Цена ошибки — потеря control над цепочкой: в следующем review никто не отличит разрешённый public example от restricted event shape, не увидит destination и не сможет доказать, что approval действовал в тот момент.'), + p('В этом кейсе нет настоящего лога, клиента, идентификатора, секретного значения, файла или сетевого запроса. Есть только fixed synthetic records с нарочно скучными placeholder. Это важно: пример учит остановить передачу до egress, а не демонстрирует «безопасный способ» отправить production context. Симптом — кто-то хочет дать ассистенту debugging material. Причина — четыре решения слились в просьбу «разрешите prompt». Проверка и действие разложены дальше по шагам.'), + h2('Case 1. Class звучит как «log», но decision зависит от содержимого и rule'), + p('Первый record называется synthetic-restricted-log-shape-v1. Его payload не содержит события: это строка с symbolic labels actor=[symbolic label], token=[not-present] и request=[not-present]. Всё равно record получает class synthetic-restricted, allowability deny-external-egress и retention label synthetic-no-external-retention. Название «shape» не пытается спрятать риск; наоборот, заставляет review решить, нужен ли реальный schema owner до обсуждения материала.'), + p('Симптом: «мы же удалили значения, значит фрагмент можно передать». Причина: redaction, class и contract — разные операции. Проверка: спросить, кто утвердил class именно после преобразования, что осталось в structure, какие условия обработки применимы и есть ли evidence для конкретного tool surface. Действие: если class или allowability не доказаны, хранить фрагмент локально и открыть owner question. Нельзя поднять deny-class тем, что reviewer уверен в хороших намерениях автора.'), + h2('Case 2. Egress gate закрыт даже при знакомом продукте'), + p('В model egressScope — символическая строка fixed-external-ai-boundary-alpha. Она не является hostname, proxy configuration или реальным endpoint. Её задача — показать правило сопоставления: record, request и authorization должны назвать один scope. Если request просит fixed-unapproved-egress-scope, decision закрывается до проверки human authorization. Это предотвращает подмену «у человека есть approval» на «он может выбрать любой путь».'), + p('Симптом: команда знает, что продукт закуплен, и поэтому считает любой его UI одинаковой границей. Причина: plan, surface, подключённый search, extension и route могут иметь разный context flow. Проверка: выписать destination как отдельную сущность и найти техническое evidence для этого режима. Действие: если destination не совпал с record scope, вернуть safe stop с reason и citation. Не искать удобный альтернативный account или device: он меняет путь, но не закрывает policy gap.'), + figure('/assets/editorial/2025/ai-data-privacy-2025-egress-review.svg', 'Вертикальный маршрут review: record/class, policy/contract, egress, access/authorization и decision с citation; любое mismatch ведёт в safe stop без передачи наружу.', 'Последовательность отделяет evidence от действия. Даже зелёный hand-off остаётся внутри fixed synthetic model и не выполняет external request.'), + h2('Case 3. Authorization имеет scope, access и expiry'), + p('В положительном fixed case approval принадлежит роли synthetic-data-steward, покрывает только synthetic-public-interface-summary-v1, только fixed-external-ai-boundary-alpha, requester label synthetic-engineering-read и заканчивается 20 апреля 2025. Он не покрывает restricted log shape и не действует после срока. Такая точность может казаться излишней для одного фрагмента, но без неё authorization становится переносимой печатью «можно AI».'), + p('Симптом: у человека есть старое одобрение, но никто не сверил requester и date. Причина: approval считают свойством tool или команды, а не решением для конкретного resource. Проверка: сопоставить record ids, destination, requester labels, issuer role, validFrom и expiresAt. Действие: при любом mismatch model возвращает safe stop; новый owner должен принять новое решение на свежем evidence. Не изменяйте expiry в старой карточке задним числом: это убирает историю, которая нужна следующему reviewer.'), + table('Review record для одного synthetic request', ['Шаг', 'Наблюдение', 'Decision в модели', 'Следующее действие'], [ + ['Class', 'log-shape имеет synthetic-restricted и deny-external-egress', 'classification denied', 'не передавать payload; спросить owner о допустимой учебной замене'], + ['Contract', 'retention label — synthetic-no-external-retention', 'contract not accepted', 'не выводить условия vendor по аналогии с другим plan'], + ['Egress', 'scope request не совпадает с record', 'gate closed', 'уточнить destination/route у network и service owner'], + ['Access', 'requester не имеет required label', 'authorization mismatch', 'проверить право на record без расширения scope в prompt'], + ['Expiry', 'approval закончился до requestedAt', 'safe stop', 'запросить новое dated решение или отказаться от передачи'], + ]), + h2('Воспроизводимый negative path без внешнего контекста'), + p('В коде ниже case уже содержит fixed record id и fixed authorization. Он не читает лог, не строит prompt и не делает HTTP call. Именно поэтому его можно запускать как fixture: мы проверяем, что denied class закрывается до approval override, а safe stop не выдаёт payload. В настоящей системе этот пример не заменяет data inventory или access control; он даёт небольшой контракт для теста: если доказательства нет, функция не открывает путь.'), + code([ + "const request = createFixedSyntheticContextRequest('restricted-class-stop');", + 'const decision = assessSyntheticAiEgress(request);', + 'const stop = stopSyntheticAiEgress(decision);', + '', + 'console.log(decision.accepted); // false', + 'console.log(decision.classificationDecision); // denied-by-fixed-class-or-allowability', + 'console.log(decision.nextAction); // owner question, no payload copy', + 'console.log(stop.productionEffect); // not-attempted', + ].join('\n')), + p('Fixture добавляет более жёсткие границы, чем happy path. Extra key и missing key не проходят exact contract. Forged authorization и расширенный scope не совпадают с fixed request proof. Sparse array и cyclic value не становятся «пустым списком», а закрываются без исключения. Отдельные cases проверяют requester access, authorization expiry и wrong egress scope. Это не антифрод и не DLP; это проверка, что сам учебный gate не переходит от отсутствующих фактов к неявному разрешению.'), + h2('Как оформить human review без ложного юридического вывода'), + p('Вопрос владельцу должен быть конкретнее, чем «можно ли использовать AI?». Например: «для record class X, surface Y и destination Z: какое allowability rule действует, каким immutable document подтверждается retention/training condition, кто является authority, какой requester и expiry покрыты?» Такой вопрос не утверждает, что DPA сам по себе разрешает передачу или что UI setting гарантирует место обработки. Он просит evidence, по которому организация вправе сделать собственный вывод.'), + p('Полезно разделить роли. Data owner подтверждает class и преобразование. Service or legal owner сопоставляет policy/contract с выбранной surface. Network owner подтверждает path и egress control. Security or privacy owner проверяет access and authorization process. Один человек может совмещать роли в небольшой компании, но в review всё равно стоит записать, какой вопрос он закрыл. Это снижает риск «одобрения вообще» и позволяет вернуть только спорный слой на доработку.'), + h2('Что дают источники, а чего они не дают'), + p('GitHub Docs на commit 30 апреля 2025 говорит, что в конкретной Copilot Chat surface prompt может обрабатываться вместе с context, а Bing search при включении отправляет сформированный query в Bing Search API. Это поддерживает постановку вопроса о расширенном context и отдельной внешней границе. Документ SKU isolation показывает пример endpoint-level firewall control. Он не описывает class конкретного лога, не подтверждает retention или training terms организации и не назначает человека, который может выдать approval.'), + p('NIST SP 800-207 поддерживает принцип явного, policy-based access вместо доверия к network location; NIST AI RMF помещает privacy и governance в постоянное управление риском. Оба источника не являются DPA, конфигурацией proxy или локальным classification policy. Поэтому они не используются для вывода «можно передавать». Их роль — удержать структуру решения: resource, scope, control, authority и documented risk должны быть видны раздельно.'), + h2('Короткая последовательность для реального review'), + ol([ + 'Остановите копирование. Сначала назовите record и class, не открывая AI surface и не отправляя сокращённый «пример».', + 'Проверьте допускаемость. Сведите policy и contract evidence к выбранным plan, feature, destination и сроку, а не к бренду поставщика.', + 'Сверьте путь. Отдельно подтвердите egress scope и технический control; он должен совпасть с карточкой, а не просто существовать в сети.', + 'Сверьте людей и дату. Requester access, authority role, record ids и expiry должны относиться к одной операции.', + 'Выпустите outcome. Либо передайте evidence в authorised workflow, либо зафиксируйте safe stop без payload и назначьте следующего владельца вопроса.', + ]), + h2('Ограничения и следующий проверяемый шаг'), + p('Этот field case — полностью synthetic. Его text не содержит настоящего production log, source code, тикета, PII, secret, customer identifier, file path или network address. Скрипт не обращается к filesystem, Git, CI, network, telemetry, clock, AI model или production. A positive decision не отправляет даже synthetic string: egressPerformed всегда false, а stop object имеет productionEffect not-attempted.'), + p('Следующий шаг: проведите tabletop review на пустой карточке одного реального use case. Пусть четыре владельца независимо заполнят class, policy/contract evidence, egress scope и authorization scope, не прикладывая payload. Затем внесите один deliberate gap: например, истёкший approval или неизвестный destination. Если процесс не может назвать reason, citation и next owner без копирования данных, сначала улучшите контур review, а уже потом подключайте инструмент к рабочему контексту.'), + h2('Историческая граница апреля 2025'), + p('Все внешние утверждения ограничены immutable GitHub Docs commit от 30 апреля 2025 и финальными NIST документами 2020/2023. Нет предположений о функциях, contract terms, data routing или гарантиях, появившихся позднее. Перед фактическим use case необходимо повторить проверку обычным HTTPS GET для своих документов и зафиксировать актуальные scope, date и exceptions.'), +]); + +export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => Object.freeze(item)); + +function verifyFixture() { + const report = runAiDataPrivacyFixture(); + 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');